blob: 44cc1b10c9e58047ed54086e0f28553ac4ba15b6 [file] [log] [blame]
# Copyright 2016 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.
import json
import logging
import os
try:
import psutil
except ImportError:
psutil = None
import subprocess
from threading import Timer
from py_trace_event import trace_time
from telemetry.internal.platform import tracing_agent
from telemetry.timeline import trace_data
DEFAULT_MIN_PCPU = 0.1
class ProcessCollector(object):
def __init__(self, min_pcpu):
self._min_pcpu = min_pcpu
def GetProcesses(self):
return NotImplemented
class UnixProcessCollector(ProcessCollector):
_SHELL_COMMAND = NotImplemented
_START_LINE_NUMBER = 1
_TOKEN_COUNT = 4
_TOKEN_MAP = {
'pCpu': 2,
'pid': 0,
'pMem': 3,
'path': 1
}
def __init__(self, min_pcpu, binary_output=False):
super(UnixProcessCollector, self).__init__(min_pcpu)
self._binary_output = binary_output
def _ParseLine(self, line):
"""Parses a line from top output
Args:
line(str): a line from top output that contains the information about a
process.
Returns:
An dictionary with useful information about the process.
"""
token_list = line.strip().split()
if len(token_list) != self._TOKEN_COUNT:
return None
return {attribute_name: token_list[index]
for attribute_name, index in self._TOKEN_MAP.items()}
def GetProcesses(self):
"""Fetches the top processes returned by top command.
Returns:
A list of dictionaries, each representing one of the top processes.
"""
if self._binary_output:
processes = subprocess.check_output(self._SHELL_COMMAND).decode(
'ascii').split('\n')
else:
processes = subprocess.check_output(self._SHELL_COMMAND).split('\n')
process_lines = processes[self._START_LINE_NUMBER:]
top_processes = []
for process_line in process_lines:
process = self._ParseLine(process_line)
if (not process) or (float(process['pCpu']) < self._min_pcpu):
continue
process['name'] = os.path.split(process['path'])[1]
top_processes.append(process)
return top_processes
class WindowsProcessCollector(ProcessCollector):
"""Class for collecting information about processes on Windows.
Windows does not have a fast and simple command to list processes, so psutil
package is used instead."""
def __init__(self, min_pcpu):
super(WindowsProcessCollector, self).__init__(min_pcpu)
def GetProcesses(self):
data = []
for p in psutil.process_iter():
try:
cpu_percent = p.get_cpu_percent(interval=0)
if cpu_percent >= self._min_pcpu:
# Get full path of the process. p.exe often throws AccessDenied
# and should have its own try block so that the dictionary literal
# can be added to returned data whether p.exe throws an exception
# ot not.
try:
path = p.exe
except psutil.Error:
path = None
data.append({
'pCpu': cpu_percent,
'pMem': p.get_memory_percent(),
'name': p.name,
'pid': p.pid,
'path': path
})
except psutil.Error:
logging.exception('Failed to get process data')
data = sorted(data, key=lambda d: d['pCpu'],
reverse=True)
return data
class LinuxProcessCollector(UnixProcessCollector):
"""Class for collecting information about processes on Linux.
Example of Linux command output: '31887 com.app.Webkit 3.4 8.0'"""
_SHELL_COMMAND = ["ps", "axo", "pid,cmd,pcpu,pmem", "--sort=-pcpu"]
def __init__(self, min_pcpu):
super(LinuxProcessCollector, self).__init__(min_pcpu)
class MacProcessCollector(UnixProcessCollector):
"""Class for collecting information about processes on Mac.
Example of Mac command output:
'31887 com.app.Webkit 3.4 8.0'"""
_SHELL_COMMAND = ['ps', '-arcwwwxo', 'pid command %cpu %mem']
def __init__(self, min_pcpu):
super(MacProcessCollector, self).__init__(min_pcpu, binary_output=True)
class CpuTracingAgent(tracing_agent.TracingAgent):
SNAPSHOT_FREQUENCY = 1.0
def __init__(self, platform_backend, min_pcpu=DEFAULT_MIN_PCPU):
super(CpuTracingAgent, self).__init__(platform_backend)
self._snapshot_ongoing = False
self._snapshots = []
self._os_name = platform_backend.GetOSName()
if self._os_name == 'win':
self._collector = WindowsProcessCollector(min_pcpu)
elif self._os_name == 'mac':
self._collector = MacProcessCollector(min_pcpu)
else:
self._collector = LinuxProcessCollector(min_pcpu)
@classmethod
def IsSupported(cls, platform_backend):
os_name = platform_backend.GetOSName()
return (os_name in ['mac', 'linux']) or (os_name == 'win' and psutil)
def StartAgentTracing(self, config, timeout):
assert not self._snapshot_ongoing, (
'Agent is already taking snapshots when tracing is started.')
if not config.enable_cpu_trace:
return False
self._snapshot_ongoing = True
self._KeepTakingSnapshots()
return True
def _KeepTakingSnapshots(self):
"""Take CPU snapshots every SNAPSHOT_FREQUENCY seconds."""
if not self._snapshot_ongoing:
return
# Assume CpuTracingAgent shares the same clock domain as telemetry
self._snapshots.append(
(self._collector.GetProcesses(), trace_time.Now()))
Timer(self.SNAPSHOT_FREQUENCY, self._KeepTakingSnapshots).start()
def StopAgentTracing(self):
assert self._snapshot_ongoing, (
'Agent is not taking snapshots when tracing is stopped.')
self._snapshot_ongoing = False
def CollectAgentTraceData(self, trace_data_builder, timeout=None):
assert not self._snapshot_ongoing, (
'Agent is still taking snapshots when data is collected.')
self._snapshot_ongoing = False
if self._os_name == 'win' and self._snapshots:
self._snapshots.pop(0)
data = json.dumps(self._FormatSnapshotsData())
trace_data_builder.SetTraceFor(trace_data.CPU_TRACE_DATA, data)
def _FormatSnapshotsData(self):
"""Format raw data into Object Event specified in Trace Format document."""
pid = os.getpid()
return [{
'name': 'CPUSnapshots',
'ph': 'O',
'id': '0x1000',
'local': True,
'ts': timestamp,
'pid': pid,
'tid':None,
'args': {
'snapshot':{
'processes': snapshot
}
}
} for snapshot, timestamp in self._snapshots]