| Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 1 | # Implements system state sampling. Called by buildstats.bbclass. | 
|  | 2 | # Because it is a real Python module, it can hold persistent state, | 
|  | 3 | # like open log files and the time of the last sampling. | 
|  | 4 |  | 
|  | 5 | import time | 
|  | 6 | import re | 
|  | 7 | import bb.event | 
|  | 8 |  | 
|  | 9 | class SystemStats: | 
|  | 10 | def __init__(self, d): | 
|  | 11 | bn = d.getVar('BUILDNAME') | 
|  | 12 | bsdir = os.path.join(d.getVar('BUILDSTATS_BASE'), bn) | 
|  | 13 | bb.utils.mkdirhier(bsdir) | 
|  | 14 |  | 
|  | 15 | self.proc_files = [] | 
|  | 16 | for filename, handler in ( | 
|  | 17 | ('diskstats', self._reduce_diskstats), | 
|  | 18 | ('meminfo', self._reduce_meminfo), | 
|  | 19 | ('stat', self._reduce_stat), | 
|  | 20 | ): | 
|  | 21 | # The corresponding /proc files might not exist on the host. | 
|  | 22 | # For example, /proc/diskstats is not available in virtualized | 
|  | 23 | # environments like Linux-VServer. Silently skip collecting | 
|  | 24 | # the data. | 
|  | 25 | if os.path.exists(os.path.join('/proc', filename)): | 
|  | 26 | # In practice, this class gets instantiated only once in | 
|  | 27 | # the bitbake cooker process.  Therefore 'append' mode is | 
|  | 28 | # not strictly necessary, but using it makes the class | 
|  | 29 | # more robust should two processes ever write | 
|  | 30 | # concurrently. | 
|  | 31 | destfile = os.path.join(bsdir, '%sproc_%s.log' % ('reduced_' if handler else '', filename)) | 
|  | 32 | self.proc_files.append((filename, open(destfile, 'ab'), handler)) | 
|  | 33 | self.monitor_disk = open(os.path.join(bsdir, 'monitor_disk.log'), 'ab') | 
|  | 34 | # Last time that we sampled /proc data resp. recorded disk monitoring data. | 
|  | 35 | self.last_proc = 0 | 
|  | 36 | self.last_disk_monitor = 0 | 
|  | 37 | # Minimum number of seconds between recording a sample. This | 
|  | 38 | # becames relevant when we get called very often while many | 
|  | 39 | # short tasks get started. Sampling during quiet periods | 
|  | 40 | # depends on the heartbeat event, which fires less often. | 
|  | 41 | self.min_seconds = 1 | 
|  | 42 |  | 
|  | 43 | self.meminfo_regex = re.compile(b'^(MemTotal|MemFree|Buffers|Cached|SwapTotal|SwapFree):\s*(\d+)') | 
|  | 44 | self.diskstats_regex = re.compile(b'^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$') | 
|  | 45 | self.diskstats_ltime = None | 
|  | 46 | self.diskstats_data = None | 
|  | 47 | self.stat_ltimes = None | 
|  | 48 |  | 
|  | 49 | def close(self): | 
|  | 50 | self.monitor_disk.close() | 
|  | 51 | for _, output, _ in self.proc_files: | 
|  | 52 | output.close() | 
|  | 53 |  | 
|  | 54 | def _reduce_meminfo(self, time, data): | 
|  | 55 | """ | 
|  | 56 | Extracts 'MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree' | 
|  | 57 | and writes their values into a single line, in that order. | 
|  | 58 | """ | 
|  | 59 | values = {} | 
|  | 60 | for line in data.split(b'\n'): | 
|  | 61 | m = self.meminfo_regex.match(line) | 
|  | 62 | if m: | 
|  | 63 | values[m.group(1)] = m.group(2) | 
|  | 64 | if len(values) == 6: | 
|  | 65 | return (time, | 
|  | 66 | b' '.join([values[x] for x in | 
|  | 67 | (b'MemTotal', b'MemFree', b'Buffers', b'Cached', b'SwapTotal', b'SwapFree')]) + b'\n') | 
|  | 68 |  | 
|  | 69 | def _diskstats_is_relevant_line(self, linetokens): | 
|  | 70 | if len(linetokens) != 14: | 
|  | 71 | return False | 
|  | 72 | disk = linetokens[2] | 
|  | 73 | return self.diskstats_regex.match(disk) | 
|  | 74 |  | 
|  | 75 | def _reduce_diskstats(self, time, data): | 
|  | 76 | relevant_tokens = filter(self._diskstats_is_relevant_line, map(lambda x: x.split(), data.split(b'\n'))) | 
|  | 77 | diskdata = [0] * 3 | 
|  | 78 | reduced = None | 
|  | 79 | for tokens in relevant_tokens: | 
|  | 80 | # rsect | 
|  | 81 | diskdata[0] += int(tokens[5]) | 
|  | 82 | # wsect | 
|  | 83 | diskdata[1] += int(tokens[9]) | 
|  | 84 | # use | 
|  | 85 | diskdata[2] += int(tokens[12]) | 
|  | 86 | if self.diskstats_ltime: | 
|  | 87 | # We need to compute information about the time interval | 
|  | 88 | # since the last sampling and record the result as sample | 
|  | 89 | # for that point in the past. | 
|  | 90 | interval = time - self.diskstats_ltime | 
|  | 91 | if interval > 0: | 
|  | 92 | sums = [ a - b for a, b in zip(diskdata, self.diskstats_data) ] | 
|  | 93 | readTput = sums[0] / 2.0 * 100.0 / interval | 
|  | 94 | writeTput = sums[1] / 2.0 * 100.0 / interval | 
|  | 95 | util = float( sums[2] ) / 10 / interval | 
|  | 96 | util = max(0.0, min(1.0, util)) | 
|  | 97 | reduced = (self.diskstats_ltime, (readTput, writeTput, util)) | 
|  | 98 |  | 
|  | 99 | self.diskstats_ltime = time | 
|  | 100 | self.diskstats_data = diskdata | 
|  | 101 | return reduced | 
|  | 102 |  | 
|  | 103 |  | 
|  | 104 | def _reduce_nop(self, time, data): | 
|  | 105 | return (time, data) | 
|  | 106 |  | 
|  | 107 | def _reduce_stat(self, time, data): | 
|  | 108 | if not data: | 
|  | 109 | return None | 
|  | 110 | # CPU times {user, nice, system, idle, io_wait, irq, softirq} from first line | 
|  | 111 | tokens = data.split(b'\n', 1)[0].split() | 
|  | 112 | times = [ int(token) for token in tokens[1:] ] | 
|  | 113 | reduced = None | 
|  | 114 | if self.stat_ltimes: | 
|  | 115 | user = float((times[0] + times[1]) - (self.stat_ltimes[0] + self.stat_ltimes[1])) | 
|  | 116 | system = float((times[2] + times[5] + times[6]) - (self.stat_ltimes[2] + self.stat_ltimes[5] + self.stat_ltimes[6])) | 
|  | 117 | idle = float(times[3] - self.stat_ltimes[3]) | 
|  | 118 | iowait = float(times[4] - self.stat_ltimes[4]) | 
|  | 119 |  | 
|  | 120 | aSum = max(user + system + idle + iowait, 1) | 
|  | 121 | reduced = (time, (user/aSum, system/aSum, iowait/aSum)) | 
|  | 122 |  | 
|  | 123 | self.stat_ltimes = times | 
|  | 124 | return reduced | 
|  | 125 |  | 
|  | 126 | def sample(self, event, force): | 
|  | 127 | now = time.time() | 
|  | 128 | if (now - self.last_proc > self.min_seconds) or force: | 
|  | 129 | for filename, output, handler in self.proc_files: | 
|  | 130 | with open(os.path.join('/proc', filename), 'rb') as input: | 
|  | 131 | data = input.read() | 
|  | 132 | if handler: | 
|  | 133 | reduced = handler(now, data) | 
|  | 134 | else: | 
|  | 135 | reduced = (now, data) | 
|  | 136 | if reduced: | 
|  | 137 | if isinstance(reduced[1], bytes): | 
|  | 138 | # Use as it is. | 
|  | 139 | data = reduced[1] | 
|  | 140 | else: | 
|  | 141 | # Convert to a single line. | 
|  | 142 | data = (' '.join([str(x) for x in reduced[1]]) + '\n').encode('ascii') | 
|  | 143 | # Unbuffered raw write, less overhead and useful | 
|  | 144 | # in case that we end up with concurrent writes. | 
|  | 145 | os.write(output.fileno(), | 
|  | 146 | ('%.0f\n' % reduced[0]).encode('ascii') + | 
|  | 147 | data + | 
|  | 148 | b'\n') | 
|  | 149 | self.last_proc = now | 
|  | 150 |  | 
|  | 151 | if isinstance(event, bb.event.MonitorDiskEvent) and \ | 
|  | 152 | ((now - self.last_disk_monitor > self.min_seconds) or force): | 
|  | 153 | os.write(self.monitor_disk.fileno(), | 
|  | 154 | ('%.0f\n' % now).encode('ascii') + | 
|  | 155 | ''.join(['%s: %d\n' % (dev, sample.total_bytes - sample.free_bytes) | 
|  | 156 | for dev, sample in event.disk_usage.items()]).encode('ascii') + | 
|  | 157 | b'\n') | 
|  | 158 | self.last_disk_monitor = now |