blob: b48a6e7c629216f9f5abc8a1caebaad4c3e63e30 [file] [log] [blame]
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
__author__ = 'namnguyen@chromium.org'
import collections
import itertools
import logging
import operator
import os
import re
import string
from autotest_lib.client.bin import utils, test
GeneralUsage = collections.namedtuple('GeneralUsage', 'total used free')
ProcessUsage = collections.namedtuple('ProcessUsage', 'pid user virtual '
'resident shared command') # command does NOT have arguments
def parse_mem(s):
"""Extracts a number out of a string such as 467m, 123k, 999g.
@param s: a string to parse
@return a float that s represents
"""
multipliers = {'k': 1024, 'm': 1024**2, 'g': 1024**3}
multiplier = multipliers.get(s[-1], 1)
if multiplier != 1:
s = s[:-1]
return float(s) * multiplier
def parse_general_usage(line):
"""Extracts general memory usage from a line from top.
@param line: string a general memory consumption line from top
@return a GeneralUsage tuple
"""
items = re.search(
r'\s+(\d+) total,\s+(\d+) used,\s+(\d+) free', line).groups()
return GeneralUsage(*[float(x) for x in items])
def parse_process_usage(line, headers):
"""Extracts memory usage numbers from a process line from top.
@param line: string a process line from `top`
@param headers: array of strings naming each field in the line
@return a ProcessUsage tuple
"""
interested_fields = {
'pid': ('pid', int),
'user': ('user', str),
'virt': ('virtual', parse_mem),
'res': ('resident', parse_mem),
'shr': ('shared', parse_mem),
'command': ('command', str),
}
fields = line.split()
current_interest_idx = 0
record = {}
for i, field in enumerate(fields):
if headers[i] not in interested_fields:
continue
key, extractor = interested_fields[headers[i]]
record[key] = extractor(field)
return ProcessUsage(**record)
def parse_processes(lines):
"""Extracts information about processes from `top`.
@param lines: a list of lines from top, the header must be the first
entry in this list
@return a list of ProcessUsage
"""
headers = [x.lower() for x in lines[0].split()]
processes = []
for line in lines[1:]:
process_usage = parse_process_usage(line, headers)
ignored = [process_usage.command.startswith(cmd) for cmd in
('autotest', 'top')]
if any(ignored):
continue
processes.append(process_usage)
logging.debug('Process usage: %r', process_usage)
return processes
def report_top_processes(processes, n=10):
"""Returns a dictionary of top n processes.
For example:
{
'top_1': 4000,
'top_2': 3000,
'top_3': 2500,
}
@param processes: a list of ProcessUsage
@param n: maximum number of processes to return
@return dictionary whose key correlate to the ranking, and values are
amount of resident memory
"""
get_resident = operator.attrgetter('resident')
top_users = sorted(processes, key=get_resident, reverse=True)
logging.info('Top 10 memory users:')
perf_values = {}
for i, process in enumerate(top_users[:n]):
logging.info('%r', process)
perf_values['top_%d' % (i + 1)] = process.resident
return perf_values
def group_by_command(processes):
"""Returns resident memory of processes with the same command.
For example:
{
'process_shill': 20971520,
'process_sshd': 4792,
}
@param processes: a list of ProcessUsage
@return dictionary whose keys correlate to the command line, and values
the sum of resident memory used by all processes with the same
command
"""
get_command = operator.attrgetter('command')
sorted_by_command = sorted(processes, key=get_command)
grouped_by_command = itertools.groupby(sorted_by_command,
key=get_command)
top_by_command = []
for command, grouped_processes in grouped_by_command:
resident=sum(p.resident for p in grouped_processes)
top_by_command.append((resident, command))
top_by_command.sort(reverse=True)
logging.info('Top processes by sum of memory consumption:')
perf_values = {}
for resident, command in top_by_command:
command = command.replace(':', '_').replace('/', '_')
logging.info('Command: %s, Resident: %f', command, resident)
perf_values['process_%s' % command] = resident
return perf_values
def group_by_service(processes):
"""Returns a collection of startup services and their memory usage.
For example:
{
'service_chapsd': 6568,
'service_cras': 3788,
'service_ui': 329284024
}
@param processes: a list of ProcessUsage
@returns dictionary whose keys correlate to the service name, and
values are sum of resident memory used by that service
"""
processes = dict((p.pid, p.resident) for p in processes)
top_by_service = []
initctl = utils.system_output('initctl list')
logging.debug('Service list:\n%s', initctl)
for line in initctl.split('\n'):
if 'process' not in line:
continue
fields = line.split()
service, main_process = fields[0], int(fields[3])
resident = 0
pstree = utils.system_output('pstree -p %d' % main_process)
logging.debug('Service %s:\n%s', service, pstree)
for pid in re.findall(r'\((\d+)\)', pstree, re.MULTILINE):
pid = int(pid)
logging.debug('Summing process %d', pid)
resident += processes.get(pid, 0)
top_by_service.append((resident, service))
top_by_service.sort(reverse=True)
logging.info('Top services:')
perf_values = {}
for resident, service in top_by_service:
logging.info('Service: %s, Resident: %f', service, resident)
perf_values['service_%s' % service] = resident
return perf_values
def parse_smap(filename):
"""Parses /proc/*/smaps file to extract detailed memory usage of a process.
The return value is a dictionary of component paths (such as "/bin/bash",
"[vdso]") and 2-tuple (shared, private) sums of memory in KB.
For example:
{
'/bin/bash': (460, 0),
'/lib64/ld-2.15.so': (4, 50),
'[vdso]': (4, 0),
'[stack]': (20, 0),
}
@param filename: The full path to an smaps file
@returns dictionary of component paths (such as "/bin/bash", "[vdso]") and
2-tuple (shared, private) sums of memory in KB.
"""
region_regexp = re.compile(
r"""(?P<Address>[0-9A-Fa-f]+-[0-9A-Fa-f]+)\s+
(?P<Permissions>[rwxsp-]{4})\s+
(?P<Offset>[0-9A-Fa-f]+)\s+
(?P<Device>[0-9A-Fa-f]+:[0-9A-Fa-f]+)\s+
(?P<Inode>\d+)\s+
(?P<Path>.+)?""", re.X)
stat_regexp = re.compile(
r"""(?P<Name>\w+):\s+
(?P<Total>\d+)\s(?P<Unit>\w\w)""", re.X)
regions = {}
with open(filename, 'r') as smaps:
current_address = None
for line in smaps:
parsed_region = region_regexp.match(line)
if parsed_region:
current_address = parsed_region.group('Address')
if current_address in regions:
raise Exception('reused address %s' % current_address)
regions[current_address] = {
'Path': parsed_region.group('Path'),
'Permissions': parsed_region.group('Permissions'),
'Offset': parsed_region.group('Offset')
}
continue
parsed_stat = stat_regexp.match(line)
if parsed_stat:
name = parsed_stat.group('Name')
total = int(parsed_stat.group('Total'))
regions[current_address][name] = total
continue
paths = collections.defaultdict(lambda: (0, 0))
for stats in regions.values():
path = stats['Path']
shared = stats['Shared_Clean'] + stats['Shared_Dirty']
private = stats['Private_Clean'] + stats['Private_Dirty']
paths[path] = (paths[path][0] + shared, paths[path][1] + private)
return paths
def report_shared_libraries(processes):
"""Report memory used by shared libraries.
This is the sum of (maximum shared memory across all processes, and sum of
all private memory across all processes) in bytes.
For example:
{
'lib_usr_lib64_xorg_modules_input_evdev_drv.so': 69632,
'lib_opt_google_chrome_pepper_libpepflashplayer.so': 23928832,
}
@param processes: List of ProcessUsage objects
@returns dictionary whose keys correlate to the library name, and
values are sum of resident memory used by that library
"""
allowed_chars = string.ascii_letters + string.digits + '.-_'
libs_max_shared_sum_private = collections.defaultdict(lambda: (0, 0))
for process in processes:
proc_exe = None
try:
exe_link = '/proc/%d/exe' % process.pid
proc_exe = os.readlink(exe_link)
except OSError:
# os.readlink() raises OSError if file is not found.
continue
components = None
try:
smap_file = '/proc/%d/smaps' % process.pid
components = parse_smap(smap_file)
except IOError:
# IOError can be raised if smaps is not found.
continue
for lib, (shared, private) in components.items():
# smaps file contains info about stack, and heap too but we are
# only interested in the shared library.
if not lib or lib.startswith('[') or lib == proc_exe:
continue
# Filter key to comply with OutputPerfValues().
key = [(c if c in allowed_chars else '_') for c in lib]
key = 'lib' + ''.join(key)
max_shared, sum_private = libs_max_shared_sum_private[key]
max_shared = max(max_shared, shared)
sum_private += private
libs_max_shared_sum_private[key] = (max_shared, sum_private)
smaps = dict((key, sum(values) * 1024) for key, values in
libs_max_shared_sum_private.items())
return smaps
class platform_MemoryMonitor(test.test):
"""Monitor memory usage trend."""
version = 1
def run_once(self):
"""Execute the test logic."""
cmd = 'top -b -n 1'
output = utils.system_output(cmd)
logging.debug('Output from top:\n%s', output)
lines = output.split('\n')
# Ignore the first 3 lines, they're not relevant in this test.
lines = lines[3:]
mem_general = parse_general_usage(lines[0])
logging.info('Total, used, and free memory (in KiB): %r, %r, %r',
*mem_general)
swap_general = parse_general_usage(lines[1])
logging.info('Total, used, and free swap (in KiB): %r, %r, %r',
*swap_general)
perf_values = {
'mem_total': mem_general.total * 1024,
'mem_used': mem_general.used * 1024,
'mem_free': mem_general.free * 1024,
'swap_total': swap_general.total * 1024,
'swap_used': swap_general.used * 1024,
'swap_free': swap_general.free * 1024,
}
# Ignore general mem, swap and a blank line.
lines = lines[3:]
processes = parse_processes(lines)
perf_values.update(report_top_processes(processes))
perf_values.update(group_by_command(processes))
perf_values.update(group_by_service(processes))
perf_values.update(report_shared_libraries(processes))
for key, val in perf_values.items():
graph_name = key.split('_')[0]
self.output_perf_value(key, val, units="bytes",
higher_is_better=False, graph=graph_name)
self.write_perf_keyval(perf_values)