File: process_utils.py

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (170 lines) | stat: -rw-r--r-- 4,612 bytes parent folder | download | duplicates (13)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# Copyright 2023 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""
Tools for tracking process statistics like memory consumption.
"""

import platform
import time

from contextlib import contextmanager
from datetime import datetime
from threading import Thread, Event


PROBING_INTERVAL_SEC = 0.2
FLUSH_LOG_BUFFER_SEC = 2


class ProcessStats:
  """Storage class for process statistics indicating if data is available."""
  def __init__(self):
    self._max_rss = 0
    self._max_vms = 0
    self._available = False

  @property
  def max_rss(self):
    return self._max_rss

  @property
  def max_vms(self):
    return self._max_vms

  @property
  def available(self):
    return self._available

  def update(self, memory_info):
    self._max_rss = max(self._max_rss, memory_info.rss)
    self._max_vms = max(self._max_vms, memory_info.vms)
    self._available = True


class EmptyProcessLogger:
  @contextmanager
  def log_stats(self, process):
    """When wrapped, logs memory statistics of the Popen process argument.

    This base-class version can be used as a null object to turn off the
    feature, yielding null-object stats.
    """
    yield ProcessStats()

  @contextmanager
  def log_system_memory(self, log_path):
    """When wrapped, logs system memory statistics to 'log_path'.

    This base-class version keeps logging off.
    """
    yield


class PSUtilProcessLogger(EmptyProcessLogger):
  def __init__(
      self, probing_interval_sec=PROBING_INTERVAL_SEC,
      flush_log_buffer_sec=FLUSH_LOG_BUFFER_SEC):
    self.probing_interval_sec = probing_interval_sec
    self.log_buffer_max = int(flush_log_buffer_sec / probing_interval_sec)

  def get_pid(self, pid):
    return pid

  @contextmanager
  def log_stats(self, process):
    try:
      process_handle = psutil.Process(self.get_pid(process.pid))
    except (psutil.AccessDenied, psutil.NoSuchProcess):
      # Fetching process stats has an expected race condition with the
      # running process, which might just have ended already.
      yield ProcessStats()
      return

    stats = ProcessStats()
    finished = Event()
    def run_logger():
      try:
        while True:
          stats.update(process_handle.memory_info())
          if finished.wait(self.probing_interval_sec):
            break
      except (psutil.AccessDenied, psutil.NoSuchProcess):
        pass

    logger = Thread(target=run_logger)
    logger.start()
    try:
      yield stats
    finally:
      finished.set()

    # Until we have joined the logger thread, we can't access the stats
    # without a race condition.
    logger.join()

  @contextmanager
  def log_system_memory(self, log_path):
    with open(log_path, 'w') as handle:
      finished = Event()
      buffer = []

      def flush_buffer():
        time_str = datetime.utcfromtimestamp(time.time())
        values = ', '.join(map(lambda s: f'{s}%', buffer))
        print(f'{time_str} - {values}', file=handle)
        buffer.clear()

      def run_logger():
        while True:
          buffer.append(psutil.virtual_memory().percent)
          if len(buffer) >= self.log_buffer_max:
            flush_buffer()
          if finished.wait(self.probing_interval_sec):
            if buffer:
              flush_buffer()
            break

      logger = Thread(target=run_logger)
      logger.start()
      try:
        yield
      finally:
        finished.set()
        logger.join()


class LinuxPSUtilProcessLogger(PSUtilProcessLogger):

  def get_pid(self, pid):
    """Try to get the correct PID on Linux.

    On Linux, we call subprocesses using shell, which on some systems (Debian)
    has an optimization using exec and reusing the parent PID, while others
    (Ubuntu) create a child process with its own PID. We don't want to log
    memory stats of the shell parent.
    """
    try:
      with open(f'/proc/{pid}/task/{pid}/children') as f:
        children = f.read().strip().split(' ')
        if children and children[0]:
          # On Debian, we don't have child processes here.
          return int(children[0])
    except FileNotFoundError:
      # A quick process might already have finished.
      pass
    return pid


EMPTY_PROCESS_LOGGER = EmptyProcessLogger()
try:
  # Process utils are only supported when we use vpython or when psutil is
  # installed.
  import psutil
  if platform.system() == 'Linux':
    PROCESS_LOGGER = LinuxPSUtilProcessLogger()
  else:
    PROCESS_LOGGER = PSUtilProcessLogger()
except:
  PROCESS_LOGGER = EMPTY_PROCESS_LOGGER