blob: 00970e3db5775ef52e406a5a351aba828dd7ae1b [file] [log] [blame]
#!python
# Copyright 2012 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A script to create plots for page fault traffic per module."""
import optparse
import os.path
import random
import re
# TODO(siggi): Figure out why Tk is broken in Chrome's python26 interpreter
# and fix it, then revisit this code.
# pylint: disable=F0401
from etw import EventConsumer, EventHandler, TraceEventSource
from etw_db import FileNameDatabase, ModuleDatabase, ProcessThreadDatabase
import etw.descriptors.pagefault as pagefault
import matplotlib
# This is ugly, but the back-end has to be selected before importing any
# submodules.
matplotlib.use('PDF')
import matplotlib.ticker as ticker
import matplotlib.pyplot as pyplot
import matplotlib.colors as colors
# pylint: enable=F0401
_PAGE_SIZE = 4096
class _ModuleFaults(object):
"""Implementation class that stores faults per module."""
def __init__(self, module):
self.file_name = os.path.basename(module.file_name)
self.module_base = module.file_name
self.page_faults = []
def AddFault(self, thread_id, time, fault_type, address, size):
self.page_faults.append((thread_id, time, fault_type, address, size))
class _ProcessFaults(object):
"""Implementation class that stores faults per process."""
def __init__(self, process):
self.cmd_line = process.cmd_line
self.start_time = process.start_time
self.process_id = process.process_id
self.modules = {}
def AddFault(self, module, thread_id, time, fault_type, address, size):
# Adjust the time to process-relative.
assert(time >= self.start_time)
time = time - self.start_time
mod = self.modules.get(module.file_name)
if not mod:
mod = _ModuleFaults(module)
self.modules[module.file_name] = mod
# Adjust the address to module-relative.
assert(address >= module.module_base or
address < module.module_base + module.module_size)
address = address - module.module_base
mod.AddFault(thread_id, time, fault_type, address, size)
class _PageFaultHandler(EventConsumer):
"""An implementation class to collect information about page faults."""
def __init__(self, process_database, file_database, module_database):
self._process_database = process_database
self._file_database = file_database
self._module_database = module_database
self._module_filter = re.compile('.*')
self._process_filter = re.compile('.*')
self._processes = {}
def SetModuleFilter(self, module_pattern):
self._module_filter = re.compile(module_pattern)
def SetProcessFilter(self, process_pattern):
self._process_filter = re.compile(process_pattern)
def _ShouldRecord(self, process, module):
if not process or not module:
return False
return (self._process_filter.search(process.cmd_line) and
self._module_filter.search(module.file_name))
def _RecordFault(self, process, module, thread_id, time, fault_type,
address, size):
proc = self._processes.get(process.process_id)
if not proc:
proc = _ProcessFaults(process)
self._processes[process.process_id] = proc
proc.AddFault(module, thread_id, time, fault_type, address, size)
@EventHandler(pagefault.Event.HardFault)
def _OnHardFault(self, event):
process = self._process_database.GetThreadProcess(event.TThreadId)
module = self._module_database.GetProcessModuleAt(
process and process.process_id, event.VirtualAddress)
if self._ShouldRecord(process, module):
self._RecordFault(process,
module,
event.thread_id,
event.time_stamp,
"Hard",
event.VirtualAddress & ~0xFFF,
event.ByteCount)
@EventHandler(pagefault.Event.TransitionFault)
def _OnTransitionFault(self, event):
self.OnSoftFault('Transition', event)
@EventHandler(pagefault.Event.DemandZeroFault)
def _OnDemandZeroFault(self, event):
self.OnSoftFault('DemandZeroFault', event)
@EventHandler(pagefault.Event.CopyOnWrite)
def _OnCopyOnWrite(self, event):
self.OnSoftFault('CopyOnWrite', event)
@EventHandler(pagefault.Event.GuardPageFault)
def _OnGuardPageFault(self, event):
self.OnSoftFault('GuardPageFault', event)
@EventHandler(pagefault.Event.HardPageFault)
def _OnHardPageFault(self, event):
pass
# self.OnSoftFault('HardPageFault', event)
@EventHandler(pagefault.Event.AccessViolation)
def _OnAccessViolation(self, event):
self.OnSoftFault('AccessViolation', event)
def OnSoftFault(self, fault_type, event):
process_id = event.process_id
process = self._process_database.GetProcess(process_id)
module = self._module_database.GetProcessModuleAt(
process and process.process_id, event.VirtualAddress)
if self._ShouldRecord(process, module):
self._RecordFault(process,
module,
event.thread_id,
event.time_stamp,
fault_type,
event.VirtualAddress & ~0xFFF,
_PAGE_SIZE)
def DataStartOptionCallback(dummy_option, dummy_opt, value, parser):
try:
# Split the parameter into 'module_name,rva_address'.
match = re.match('^([^,]+),([^,]+)$', value)
if match == None:
raise
module = match.groups()[0]
address = int(match.groups()[1], 0) # Auto-detect base 8/10/16.
except:
raise optparse.OptionValueError(
'Invalid data_start option: \'%s\'.' % value)
if parser.values.data_start == None:
parser.values.data_start = {}
parser.values.data_start[module] = address
def GetOptionParser():
parser = optparse.OptionParser()
parser.add_option('-p', '--processes', dest='processes',
help='A regular expression that matches the command lines '
'of the processes to collect information about.',
default='.*')
parser.add_option('-m', '--modules', dest='modules',
help='A regular expression that matches the modules '
'to collect information about.',
default='.*')
parser.add_option('-d', '--data_start', dest='data_start',
help='A comma separated "module,rva_start" pair '
'which specifies where non-text data starts. '
'To be called once per module.',
action='callback', type='string',
callback=DataStartOptionCallback)
parser.add_option('-c', '--categorize', dest='categorize',
help='A category indicated how to group fault events. '
'One of {process, module, thread} (default: process).',
default='process')
parser.add_option('-o', '--output', dest='output',
help='The file where the graph is written, if not '
'supplied the graph will be displayed interactively.',
default=None)
parser.add_option('--width', dest='width', type='int',
help='Width of the generated graph (inches, default: 16).',
default=16)
parser.add_option('--height', dest='height', type='int',
help='Height of the generated graph (inches, default: 9).',
default=9)
parser.add_option('--dpi', dest='dpi', type='int',
help='DPI of the generated graph (default: 80).',
default=80)
return parser
def ConsumeLogs(files, process_filter, module_filter):
"""Consumes a set of kernel logs and collects page fault information.
Args:
files: a list of paths to kernel trace logs (.etl files) to consume.
process_filter: a regular expression that matches the processes of
interest. Example: "chrome.exe|regsvr32.exe".
module_filter: a regular expression that matches the modules of interest.
Example: "chrome.dll".
"""
source = TraceEventSource(raw_time=True)
process_database = ProcessThreadDatabase()
file_database = FileNameDatabase()
module_database = ModuleDatabase()
pf_handler = _PageFaultHandler(process_database,
file_database,
module_database)
pf_handler.SetModuleFilter(module_filter)
pf_handler.SetProcessFilter(process_filter)
source.AddHandler(process_database)
source.AddHandler(file_database)
source.AddHandler(module_database)
source.AddHandler(pf_handler)
# Open the trace files.
for trace_file in files:
source.OpenFileSession(trace_file)
# And consume them.
source.Consume()
return pf_handler._processes # pylint: disable=W0212
def WhitenColor(color, factor):
"""Makes a color whiter.
Args:
color: the color to whiten, in any of the formats supported
by matplotlib.colors.
factor: the amount by which to whiten the color.
(0=do not change the color, 1=make it completely white)
"""
assert(factor >= 0 and factor <= 1)
if isinstance(color, (int, long, float)):
color = str(color)
color = colors.colorConverter.to_rgb(color)
color = map(lambda x: 1 - ((1 - x) * (1 - factor)), color)
color = colors.rgb2hex(color)
return color
def PlotStackedBar(ax, x, heights, scale=100.0, width=0.6, color=0.5,
labels=None, annotate=None):
"""Adds a stacked bar chart to a graph.
Args:
ax: axes object.
x: horizontal placement of the center of the bar.
heights: tuple/list of bar heights.
scale: used to scale the heights (default: 100).
width: width of bar chart (default: 0.6).
color: tuple/list of colors. If a single color is specified,
will decrease saturation for each successive bar (default: 0.5).
labels: tuple/list of labels that will be applied to the bars
(default: None).
annotate: format specifier used to annotate bar labels with values
values from 'heights' (default: None).
"""
real = (int, long, float)
assert(isinstance(x, real))
assert(isinstance(heights, (list, tuple)))
assert(isinstance(scale, real) and scale > 0)
assert(isinstance(width, real) and width > 0)
n = len(heights)
# Get partial sums.
heights_partialsum_0 = [0] * n # [0, h[0], h[0] + h[1], ...]
heights_partialsum_1 = [heights[0]] * n # [h[0], h[0] + h[1], ...]
for i in range(1, n):
heights_partialsum_0[i] = heights_partialsum_0[i - 1] + heights[i - 1]
heights_partialsum_1[i] = heights_partialsum_1[i - 1] + heights[i]
# Scale the partial sums so they sum to 'scale'.
scale_factor = heights_partialsum_1[n - 1] / scale
for i in range(0, n):
heights_partialsum_0[i] /= scale_factor
heights_partialsum_1[i] /= scale_factor
# Set up colors for each bar.
if isinstance(color, (list, tuple)):
assert(len(color) == n)
else:
color = [color] * n
for i in range(0, n):
color[i] = WhitenColor(color[0], i * 1.0 / n)
# Plot the actual bar.
ax.bar([x] * n, heights_partialsum_1,
bottom=heights_partialsum_0,
color=color, width=width, align='center')
if labels != None:
assert(len(labels) == n)
for i in range(0, n):
y = (heights_partialsum_0[i] + heights_partialsum_1[i]) / 2
label = labels[i]
if annotate != None:
label = label + (annotate % heights[i])
ax.annotate(label, (x, y), horizontalalignment='center',
verticalalignment='center', rotation='vertical')
def GenerateGraph(info, file_name, width, height, dpi, data_start=None,
categorize=None):
"""Generates a graph from collected information.
Args:
info: a dictionary of pid->_ProcessFault instances.
file_name: output file name, or None to show the graph interactively.
width: the width (in inches) of the generated graph.
height: the height (in inches) of the generated graph.
dpi: the DPI of the generated graph.
data_start: dict of of rva_start addresses keyed by module name
(default: {}).
categorize: function that receives (process_id, module_id, thread_id)
and returns a key used to group faults (default: None).
"""
fig = pyplot.figure(figsize=(width, height), dpi=dpi)
ax = fig.add_axes([0.1, 0.2, 0.75, 0.7])
ax_cpf = fig.add_axes([0.1, 0.1, 0.75, 0.1])
ax_bar = fig.add_axes([0.85, 0.1, 0.05, 0.8])
if categorize == None:
categorize = lambda p, m, t: 0
if data_start == None:
data_start = {}
# Get the earliest start time across all processes.
start_time = None
for process_faults in info.itervalues():
if not start_time or process_faults.start_time < start_time:
start_time = process_faults.start_time
max_addr = 0
max_time = 0
fault_times = {}
start_times = {}
faults = {}
for fault_type in ['hard_code', 'hard_data', 'soft_code', 'soft_data']:
faults[fault_type] = {}
# Categorize the faults and calculate summary information.
for process_faults in info.itervalues():
process_id = process_faults.process_id
for module_faults in process_faults.modules.itervalues():
module_id = module_faults.file_name
for (thread_id, time, kind, address, size) in module_faults.page_faults:
time = time + process_faults.start_time - start_time
max_addr = max(max_addr, address + size)
max_time = max(max_time, time)
# Categorize the fault event.
category = categorize(process_id, module_id, thread_id)
# Classify the fault type.
hard = kind in ['HardFault', 'Hard']
code = True
if data_start.has_key(module_id):
code = address < data_start[module_id]
fault_type = ('hard_' if hard else 'soft_')
fault_type += ('code' if code else 'data')
if not faults[fault_type].has_key(category):
faults[fault_type][category] = []
faults[fault_type][category].append((time, address))
# Keep track of earliest start time per category.
if not start_times.has_key(category):
start_times[category] = time
start_times[category] = min(time, start_times[category])
# We are only interested in hard code faults for the cumulative
# display. So keep track of the set of unique times across all
# categories for this fault type.
if hard and code:
fault_times[time] = True
# A small set of preferred colors that we use for consistent coloring. When
# this is exhausted we start generating random colors.
pref_colors = ['red', 'blue', 'green', 'orange', 'magenta', 'brown']
# Get the categories as a list, sorted by start time.
categories = map(lambda x: x[0],
sorted(start_times.items(), lambda x, y: cmp(x[1], y[1])))
# Assign category colors.
category_colors = {}
pref_color_index = 0
for category in categories:
if pref_color_index < len(pref_colors):
category_colors[category] = pref_colors[pref_color_index]
else:
category_colors[category] = (random.random(), random.random(),
random.random())
pref_color_index += 1
# Display data_start lines as horizontal pale yellow marker lines.
for module_id, rva in data_start.items():
if rva < max_addr:
ax.axhline(y=(rva), color=WhitenColor('y', 0.8), zorder=0)
# Plot the fault events.
size_data = 3
size_code = 5
marker_soft = 'o'
marker_hard = 's'
data_whiten = 0.5
soft_whiten = 0.7
for category in categories:
color = WhitenColor(category_colors[category],
1 - data_whiten * soft_whiten)
for (time, address) in faults['soft_data'].get(category, []):
ax.plot(time, address, markeredgecolor=color, markeredgewidth=0.5,
marker=marker_soft, markersize=size_data, markerfacecolor='None',
zorder=1)
color = WhitenColor(category_colors[category], 1 - data_whiten)
for (time, address) in faults['hard_data'].get(category, []):
ax.plot(time, address, markeredgecolor=color, markeredgewidth=0.5,
marker=marker_hard, markersize=size_data, markerfacecolor='None',
zorder=2)
color = WhitenColor(category_colors[category], 1 - soft_whiten)
for (time, address) in faults['soft_code'].get(category, []):
ax.plot(time, address, markeredgecolor=color, markeredgewidth=0.5,
marker=marker_soft, markersize=size_code, markerfacecolor='None',
zorder=3)
color = category_colors[category]
for (time, address) in faults['hard_code'].get(category, []):
ax.plot(time, address, markeredgecolor=color, markeredgewidth=0.5,
marker=marker_hard, markersize=size_code, markerfacecolor='None',
zorder=4)
# Build and plot the cumulative hard_code plots.
fault_times = sorted(fault_times.keys())
fault_counts = [0] * len(fault_times)
zorder = 0
for category in categories:
fault_sum = 0
fault_dict = {}
for (time, address) in faults['hard_code'].get(category, []):
fault_dict[time] = fault_dict.get(time, 0) + 1
fault_sum = 0
for time_index in range(len(fault_counts)):
time = fault_times[time_index]
delta = fault_dict.get(time, 0)
fault_sum += delta
fault_counts[time_index] += fault_sum
ax_cpf.fill_between(fault_times, fault_counts,
color=category_colors[category], zorder=zorder)
zorder -= 1
# Display the process start times as vertical yellow marker lines.
for process_faults in info.itervalues():
time = process_faults.start_time - start_time
if time > 0:
ax.axvline(x=(process_faults.start_time - start_time),
color="y",
label="PID: %d" % process_faults.process_id, zorder=5)
ax_cpf.axvline(x=(process_faults.start_time - start_time), color="y",
zorder=5)
# Do the bar plots of total faults.
hard = 0
soft = 0
hard_code = 0
hard_data = 0
for category in categories:
hc = len(faults['hard_code'].get(category, []))
hd = len(faults['hard_data'].get(category, []))
soft += len(faults['soft_code'].get(category, [])) + \
len(faults['soft_data'].get(category, []))
hard += hc + hd
hard_code += hc
hard_data += hd
PlotStackedBar(ax_bar, 0.5, (hard_code, hard_data),
labels=('hard code', 'hard data'), annotate=' (%d)')
PlotStackedBar(ax_bar, 1.5, (hard, soft),
labels=('hard', 'soft'), annotate=' (%d)')
ax_cpf.set_xlim(0, max_time)
ax_cpf.set_xlabel('Time (s)')
ax_cpf.set_ylabel('Hard code faults')
ax.set_xlim(0, max_time)
ax.xaxis.set_major_formatter(ticker.NullFormatter())
ax.set_ylabel('Address')
ax.set_ylim(0, max_addr)
ax.yaxis.tick_left()
formatter = ticker.FormatStrFormatter('0x%08X')
ax.yaxis.set_major_formatter(formatter)
for label in ax.yaxis.get_ticklabels():
label.set_rotation(-45)
label.set_verticalalignment('bottom')
ax_bar.set_ylim(0, 100.0)
ax_bar.yaxis.tick_right()
ax_bar.yaxis.set_major_formatter(ticker.FormatStrFormatter('%d%%'))
ax_bar.set_xlim(0, 2)
ax_bar.xaxis.set_major_locator(ticker.NullLocator())
ax_bar.xaxis.set_major_formatter(ticker.NullFormatter())
if file_name:
pyplot.savefig(file_name)
else:
pyplot.show()
def main():
"""Script's main function."""
parser = GetOptionParser()
options, args = parser.parse_args()
if not args:
parser.error('You must provide one or more trace files to parse.')
categorize = {'process': lambda p, m, t: p,
'module': lambda p, m, t: m,
'thread': lambda p, m, t: t}
categorize = categorize.get(options.categorize, None)
info = ConsumeLogs(args, options.processes, options.modules)
GenerateGraph(info, options.output, options.width, options.height,
options.dpi, categorize=categorize,
data_start=options.data_start)
if __name__ == '__main__':
main()