#!/usr/bin/python # Copyright 2018 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import collections import logging import re import threading from autotest_lib.client.bin import utils class SystemSampler(): """A sampler class used to probe various system stat along with FPS. Sample usage: def get_consumer_pid(): # Returns all memory consumers whose memory usage we care about. sampler = SystemSampler(get_consumer_pid) fps = ... sampler.sample(fps) """ Sample = collections.namedtuple( 'Sample', ['pswpin', 'pswpout', 'free_mem', 'buff_mem', 'cached_mem', 'anon_mem', 'file_mem', 'swap_free', 'swap_used', 'consumer_num', 'consumer_rss', 'consumer_swap', 'cpuload', 'fps']) VMStat = collections.namedtuple('VMStat', ['pswpin', 'pswpout']) def __init__(self, get_consumer_pid_func): self._get_consumer_pid_func = get_consumer_pid_func self._samples_lock = threading.Lock() self._samples = [] self.reset() def reset(self): """Resets its internal stats.""" self._prev_vmstat = self.get_vmstat() self._prev_cpustat = utils.get_cpu_usage() def get_samples(self): with self._samples_lock: return self._samples def get_last_avg_fps(self, num): """Gets average fps rate of last |num| samples. Returns None if there's not enough samples in hand. """ if num <= 0: logging.warning('Num of samples must be > 0') return with self._samples_lock: if len(self._samples) >= num: return sum([s.fps for s in self._samples[-num:]])/float(num) logging.warning('Not enough samples') return None def sample(self, fps_info): """Gets a fps sample with system state.""" vmstat = self.get_vmstat() vmstat_diff = self.VMStat(*[(end - start) for start, end in zip(self._prev_vmstat, vmstat)]) self._prev_vmstat = vmstat cpustat = utils.get_cpu_usage() cpuload = utils.compute_active_cpu_time( self._prev_cpustat, cpustat) self._prev_cpustat = cpustat mem_info_in_kb = utils.get_meminfo() # Converts mem_info from KB to MB. mem_info = collections.namedtuple('MemInfo', mem_info_in_kb._fields)( *[v/float(1024) for v in mem_info_in_kb]) consumer_pids = self._get_consumer_pid_func() logging.info('Consumers %s', consumer_pids) consumer_rss, consumer_swap = self.get_consumer_meminfo(consumer_pids) # fps_info = (frame_info, frame_times) fps_count = len([f for f in fps_info[0] if f != ' ']) sample = self.Sample( pswpin=vmstat_diff.pswpin, pswpout=vmstat_diff.pswpout, free_mem=mem_info.MemFree, buff_mem=mem_info.Buffers, cached_mem=mem_info.Cached, anon_mem=mem_info.Active_anon + mem_info.Inactive_anon, file_mem=mem_info.Active_file + mem_info.Inactive_file, swap_free=mem_info.SwapFree, swap_used=mem_info.SwapTotal - mem_info.SwapFree, consumer_num=len(consumer_pids), consumer_rss=consumer_rss, consumer_swap=consumer_swap, cpuload=cpuload, fps=fps_count) logging.info(sample) with self._samples_lock: self._samples.append(sample) @staticmethod def parse_meminfo_from_proc_entry(pid): """Parses memory related info in /proc//totmaps like: Rss: 144956 kB Pss: 74923 kB Shared_Clean: 50596 kB Shared_Dirty: 41660 kB Private_Clean: 1032 kB Private_Dirty: 51668 kB Referenced: 137424 kB Anonymous: 91772 kB AnonHugePages: 30720 kB Swap: 0 kB """ mem_info = {} line_pattern = re.compile(r'^(\w+):\s+(\d+)\s+kB') proc_entry = '/proc/%s/totmaps' % pid try: with open(proc_entry) as f: for line in f: m = line_pattern.match(line) if m: key, value = m.groups() mem_info[key] = float(value)/1024 except IOError as e: logging.warning('Failed to open %s: %s', proc_entry, e) return mem_info @classmethod def get_consumer_meminfo(cls, pids): rss = 0.0 swap = 0.0 for pid in pids: mem_info = cls.parse_meminfo_from_proc_entry(pid) rss += mem_info.get('Rss', 0) swap += mem_info.get('Swap', 0) return rss, swap @classmethod def get_vmstat(cls): with open('/proc/vmstat') as f: lines = f.readlines() all_fields = dict([l.strip().split(' ') for l in lines]) return cls.VMStat( *[int(all_fields.get(f, 0)) for f in cls.VMStat._fields])