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