diff options
Diffstat (limited to 'meta/lib/buildstats.py')
-rw-r--r-- | meta/lib/buildstats.py | 88 |
1 files changed, 71 insertions, 17 deletions
diff --git a/meta/lib/buildstats.py b/meta/lib/buildstats.py index 8627ed3c31..1ffe679801 100644 --- a/meta/lib/buildstats.py +++ b/meta/lib/buildstats.py | |||
@@ -1,4 +1,6 @@ | |||
1 | # | 1 | # |
2 | # Copyright OpenEmbedded Contributors | ||
3 | # | ||
2 | # SPDX-License-Identifier: GPL-2.0-only | 4 | # SPDX-License-Identifier: GPL-2.0-only |
3 | # | 5 | # |
4 | # Implements system state sampling. Called by buildstats.bbclass. | 6 | # Implements system state sampling. Called by buildstats.bbclass. |
@@ -14,13 +16,27 @@ class SystemStats: | |||
14 | bn = d.getVar('BUILDNAME') | 16 | bn = d.getVar('BUILDNAME') |
15 | bsdir = os.path.join(d.getVar('BUILDSTATS_BASE'), bn) | 17 | bsdir = os.path.join(d.getVar('BUILDSTATS_BASE'), bn) |
16 | bb.utils.mkdirhier(bsdir) | 18 | bb.utils.mkdirhier(bsdir) |
19 | file_handlers = [('diskstats', self._reduce_diskstats), | ||
20 | ('meminfo', self._reduce_meminfo), | ||
21 | ('stat', self._reduce_stat)] | ||
22 | |||
23 | # Some hosts like openSUSE have readable /proc/pressure files | ||
24 | # but throw errors when these files are opened. Catch these error | ||
25 | # and ensure that the reduce_proc_pressure directory is not created. | ||
26 | if os.path.exists("/proc/pressure"): | ||
27 | try: | ||
28 | with open('/proc/pressure/cpu', 'rb') as source: | ||
29 | source.read() | ||
30 | pressuredir = os.path.join(bsdir, 'reduced_proc_pressure') | ||
31 | bb.utils.mkdirhier(pressuredir) | ||
32 | file_handlers.extend([('pressure/cpu', self._reduce_pressure), | ||
33 | ('pressure/io', self._reduce_pressure), | ||
34 | ('pressure/memory', self._reduce_pressure)]) | ||
35 | except Exception: | ||
36 | pass | ||
17 | 37 | ||
18 | self.proc_files = [] | 38 | self.proc_files = [] |
19 | for filename, handler in ( | 39 | for filename, handler in (file_handlers): |
20 | ('diskstats', self._reduce_diskstats), | ||
21 | ('meminfo', self._reduce_meminfo), | ||
22 | ('stat', self._reduce_stat), | ||
23 | ): | ||
24 | # The corresponding /proc files might not exist on the host. | 40 | # The corresponding /proc files might not exist on the host. |
25 | # For example, /proc/diskstats is not available in virtualized | 41 | # For example, /proc/diskstats is not available in virtualized |
26 | # environments like Linux-VServer. Silently skip collecting | 42 | # environments like Linux-VServer. Silently skip collecting |
@@ -37,24 +53,32 @@ class SystemStats: | |||
37 | # Last time that we sampled /proc data resp. recorded disk monitoring data. | 53 | # Last time that we sampled /proc data resp. recorded disk monitoring data. |
38 | self.last_proc = 0 | 54 | self.last_proc = 0 |
39 | self.last_disk_monitor = 0 | 55 | self.last_disk_monitor = 0 |
40 | # Minimum number of seconds between recording a sample. This | 56 | # Minimum number of seconds between recording a sample. This becames relevant when we get |
41 | # becames relevant when we get called very often while many | 57 | # called very often while many short tasks get started. Sampling during quiet periods |
42 | # short tasks get started. Sampling during quiet periods | ||
43 | # depends on the heartbeat event, which fires less often. | 58 | # depends on the heartbeat event, which fires less often. |
44 | self.min_seconds = 1 | 59 | # By default, the Heartbeat events occur roughly once every second but the actual time |
45 | 60 | # between these events deviates by a few milliseconds, in most cases. Hence | |
46 | self.meminfo_regex = re.compile(b'^(MemTotal|MemFree|Buffers|Cached|SwapTotal|SwapFree):\s*(\d+)') | 61 | # pick a somewhat arbitary tolerance such that we sample a large majority |
47 | self.diskstats_regex = re.compile(b'^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$') | 62 | # of the Heartbeat events. This ignores rare events that fall outside the minimum |
63 | # and may lead an extra sample in a given second every so often. However, it allows for fairly | ||
64 | # consistent intervals between samples without missing many events. | ||
65 | self.tolerance = 0.01 | ||
66 | self.min_seconds = 1.0 - self.tolerance | ||
67 | |||
68 | self.meminfo_regex = re.compile(rb'^(MemTotal|MemFree|Buffers|Cached|SwapTotal|SwapFree):\s*(\d+)') | ||
69 | self.diskstats_regex = re.compile(rb'^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$') | ||
48 | self.diskstats_ltime = None | 70 | self.diskstats_ltime = None |
49 | self.diskstats_data = None | 71 | self.diskstats_data = None |
50 | self.stat_ltimes = None | 72 | self.stat_ltimes = None |
73 | # Last time we sampled /proc/pressure. All resources stored in a single dict with the key as filename | ||
74 | self.last_pressure = {"pressure/cpu": None, "pressure/io": None, "pressure/memory": None} | ||
51 | 75 | ||
52 | def close(self): | 76 | def close(self): |
53 | self.monitor_disk.close() | 77 | self.monitor_disk.close() |
54 | for _, output, _ in self.proc_files: | 78 | for _, output, _ in self.proc_files: |
55 | output.close() | 79 | output.close() |
56 | 80 | ||
57 | def _reduce_meminfo(self, time, data): | 81 | def _reduce_meminfo(self, time, data, filename): |
58 | """ | 82 | """ |
59 | Extracts 'MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree' | 83 | Extracts 'MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree' |
60 | and writes their values into a single line, in that order. | 84 | and writes their values into a single line, in that order. |
@@ -75,7 +99,7 @@ class SystemStats: | |||
75 | disk = linetokens[2] | 99 | disk = linetokens[2] |
76 | return self.diskstats_regex.match(disk) | 100 | return self.diskstats_regex.match(disk) |
77 | 101 | ||
78 | def _reduce_diskstats(self, time, data): | 102 | def _reduce_diskstats(self, time, data, filename): |
79 | relevant_tokens = filter(self._diskstats_is_relevant_line, map(lambda x: x.split(), data.split(b'\n'))) | 103 | relevant_tokens = filter(self._diskstats_is_relevant_line, map(lambda x: x.split(), data.split(b'\n'))) |
80 | diskdata = [0] * 3 | 104 | diskdata = [0] * 3 |
81 | reduced = None | 105 | reduced = None |
@@ -104,10 +128,10 @@ class SystemStats: | |||
104 | return reduced | 128 | return reduced |
105 | 129 | ||
106 | 130 | ||
107 | def _reduce_nop(self, time, data): | 131 | def _reduce_nop(self, time, data, filename): |
108 | return (time, data) | 132 | return (time, data) |
109 | 133 | ||
110 | def _reduce_stat(self, time, data): | 134 | def _reduce_stat(self, time, data, filename): |
111 | if not data: | 135 | if not data: |
112 | return None | 136 | return None |
113 | # CPU times {user, nice, system, idle, io_wait, irq, softirq} from first line | 137 | # CPU times {user, nice, system, idle, io_wait, irq, softirq} from first line |
@@ -126,14 +150,41 @@ class SystemStats: | |||
126 | self.stat_ltimes = times | 150 | self.stat_ltimes = times |
127 | return reduced | 151 | return reduced |
128 | 152 | ||
153 | def _reduce_pressure(self, time, data, filename): | ||
154 | """ | ||
155 | Return reduced pressure: {avg10, avg60, avg300} and delta total compared to the previous sample | ||
156 | for the cpu, io and memory resources. A common function is used for all 3 resources since the | ||
157 | format of the /proc/pressure file is the same in each case. | ||
158 | """ | ||
159 | if not data: | ||
160 | return None | ||
161 | tokens = data.split(b'\n', 1)[0].split() | ||
162 | avg10 = float(tokens[1].split(b'=')[1]) | ||
163 | avg60 = float(tokens[2].split(b'=')[1]) | ||
164 | avg300 = float(tokens[3].split(b'=')[1]) | ||
165 | total = int(tokens[4].split(b'=')[1]) | ||
166 | |||
167 | reduced = None | ||
168 | if self.last_pressure[filename]: | ||
169 | delta = total - self.last_pressure[filename] | ||
170 | reduced = (time, (avg10, avg60, avg300, delta)) | ||
171 | self.last_pressure[filename] = total | ||
172 | return reduced | ||
173 | |||
129 | def sample(self, event, force): | 174 | def sample(self, event, force): |
175 | """ | ||
176 | Collect and log proc or disk_monitor stats periodically. | ||
177 | Return True if a new sample is collected and hence the value last_proc or last_disk_monitor | ||
178 | is changed. | ||
179 | """ | ||
180 | retval = False | ||
130 | now = time.time() | 181 | now = time.time() |
131 | if (now - self.last_proc > self.min_seconds) or force: | 182 | if (now - self.last_proc > self.min_seconds) or force: |
132 | for filename, output, handler in self.proc_files: | 183 | for filename, output, handler in self.proc_files: |
133 | with open(os.path.join('/proc', filename), 'rb') as input: | 184 | with open(os.path.join('/proc', filename), 'rb') as input: |
134 | data = input.read() | 185 | data = input.read() |
135 | if handler: | 186 | if handler: |
136 | reduced = handler(now, data) | 187 | reduced = handler(now, data, filename) |
137 | else: | 188 | else: |
138 | reduced = (now, data) | 189 | reduced = (now, data) |
139 | if reduced: | 190 | if reduced: |
@@ -150,6 +201,7 @@ class SystemStats: | |||
150 | data + | 201 | data + |
151 | b'\n') | 202 | b'\n') |
152 | self.last_proc = now | 203 | self.last_proc = now |
204 | retval = True | ||
153 | 205 | ||
154 | if isinstance(event, bb.event.MonitorDiskEvent) and \ | 206 | if isinstance(event, bb.event.MonitorDiskEvent) and \ |
155 | ((now - self.last_disk_monitor > self.min_seconds) or force): | 207 | ((now - self.last_disk_monitor > self.min_seconds) or force): |
@@ -159,3 +211,5 @@ class SystemStats: | |||
159 | for dev, sample in event.disk_usage.items()]).encode('ascii') + | 211 | for dev, sample in event.disk_usage.items()]).encode('ascii') + |
160 | b'\n') | 212 | b'\n') |
161 | self.last_disk_monitor = now | 213 | self.last_disk_monitor = now |
214 | retval = True | ||
215 | return retval \ No newline at end of file | ||