blob: f1c4d9ab5bb60ae04e702457333b57ac2b2a6939 [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.
import collections
from telemetry.util.statistics import DivideIfPossibleOrZero
from telemetry.web_perf.metrics import timeline_based_metric
from telemetry.value import scalar
class LoadTimesTimelineMetric(timeline_based_metric.TimelineBasedMetric):
def __init__(self):
super(LoadTimesTimelineMetric, self).__init__()
self.report_main_thread_only = True
def AddResults(self, model, renderer_thread, interaction_records, results):
assert model
assert len(interaction_records) == 1, (
'LoadTimesTimelineMetric cannot compute metrics for more than 1 time '
'range.')
interaction_record = interaction_records[0]
if self.report_main_thread_only:
thread_filter = 'CrRendererMain'
else:
thread_filter = None
events_by_name = collections.defaultdict(list)
renderer_process = renderer_thread.parent
for thread in renderer_process.threads.itervalues():
if thread_filter and not thread.name in thread_filter:
continue
thread_name = thread.name.replace('/','_')
for e in thread.IterAllSlicesInRange(interaction_record.start,
interaction_record.end):
events_by_name[e.name].append(e)
for event_name, event_group in events_by_name.iteritems():
times = [event.self_time for event in event_group]
total = sum(times)
biggest_jank = max(times)
# Results objects cannot contain the '.' character, so remove that here.
sanitized_event_name = event_name.replace('.', '_')
full_name = thread_name + '|' + sanitized_event_name
results.AddValue(scalar.ScalarValue(
results.current_page, full_name, 'ms', total))
results.AddValue(scalar.ScalarValue(
results.current_page, full_name + '_max', 'ms', biggest_jank))
results.AddValue(scalar.ScalarValue(
results.current_page, full_name + '_avg', 'ms', total / len(times)))
for counter_name, counter in renderer_process.counters.iteritems():
total = sum(counter.totals)
# Results objects cannot contain the '.' character, so remove that here.
sanitized_counter_name = counter_name.replace('.', '_')
results.AddValue(scalar.ScalarValue(
results.current_page, sanitized_counter_name, 'count', total))
results.AddValue(scalar.ScalarValue(
results.current_page, sanitized_counter_name + '_avg', 'count',
total / float(len(counter.totals))))
# We want to generate a consistant picture of our thread usage, despite
# having several process configurations (in-proc-gpu/single-proc).
# Since we can't isolate renderer threads in single-process mode, we
# always sum renderer-process threads' times. We also sum all io-threads
# for simplicity.
TimelineThreadCategories = {
"Chrome_InProcGpuThread": "GPU",
"CrGpuMain" : "GPU",
"AsyncTransferThread" : "GPU_transfer",
"CrBrowserMain" : "browser",
"Browser Compositor" : "browser",
"CrRendererMain" : "renderer_main",
"Compositor" : "renderer_compositor",
"IOThread" : "IO",
"CompositorRasterWorker": "raster",
"DummyThreadName1" : "other",
"DummyThreadName2" : "total_fast_path",
"DummyThreadName3" : "total_all"
}
_MatchBySubString = ["IOThread", "CompositorRasterWorker"]
AllThreads = TimelineThreadCategories.values()
NoThreads = []
FastPathThreads = ["GPU", "renderer_compositor", "browser", "IO"]
ReportMainThreadOnly = ["renderer_main"]
ReportSilkDetails = ["renderer_main"]
# TODO(epenner): Thread names above are likely fairly stable but trace names
# could change. We should formalize these traces to keep this robust.
OverheadTraceCategory = "trace_event_overhead"
OverheadTraceName = "overhead"
FrameTraceName = "::SwapBuffers"
FrameTraceThreadName = "renderer_compositor"
def Rate(numerator, denominator):
return DivideIfPossibleOrZero(numerator, denominator)
def ClockOverheadForEvent(event):
if (event.category == OverheadTraceCategory and
event.name == OverheadTraceName):
return event.duration
else:
return 0
def CpuOverheadForEvent(event):
if (event.category == OverheadTraceCategory and
event.thread_duration):
return event.thread_duration
else:
return 0
def ThreadCategoryName(thread_name):
thread_category = "other"
for substring, category in TimelineThreadCategories.iteritems():
if substring in _MatchBySubString and substring in thread_name:
thread_category = category
if thread_name in TimelineThreadCategories:
thread_category = TimelineThreadCategories[thread_name]
return thread_category
def ThreadCpuTimeResultName(thread_category):
# This isn't a good name, but I don't want to change it and lose continuity.
return "thread_" + thread_category + "_cpu_time_per_frame"
def ThreadTasksResultName(thread_category):
return "tasks_per_frame_" + thread_category
def ThreadMeanFrameTimeResultName(thread_category):
return "mean_frame_time_" + thread_category
def ThreadDetailResultName(thread_category, detail):
detail_sanitized = detail.replace('.','_')
return "thread_" + thread_category + "|" + detail_sanitized
class ResultsForThread(object):
def __init__(self, model, record_ranges, name):
self.model = model
self.toplevel_slices = []
self.all_slices = []
self.name = name
self.record_ranges = record_ranges
self.all_action_time = \
sum([record_range.bounds for record_range in self.record_ranges])
@property
def clock_time(self):
clock_duration = sum([x.duration for x in self.toplevel_slices])
clock_overhead = sum([ClockOverheadForEvent(x) for x in self.all_slices])
return clock_duration - clock_overhead
@property
def cpu_time(self):
cpu_duration = 0
cpu_overhead = sum([CpuOverheadForEvent(x) for x in self.all_slices])
for x in self.toplevel_slices:
# Only report thread-duration if we have it for all events.
#
# A thread_duration of 0 is valid, so this only returns 0 if it is None.
if x.thread_duration == None:
if not x.duration:
continue
else:
return 0
else:
cpu_duration += x.thread_duration
return cpu_duration - cpu_overhead
def SlicesInActions(self, slices):
slices_in_actions = []
for event in slices:
for record_range in self.record_ranges:
if record_range.ContainsInterval(event.start, event.end):
slices_in_actions.append(event)
break
return slices_in_actions
def AppendThreadSlices(self, thread):
self.all_slices.extend(self.SlicesInActions(thread.all_slices))
self.toplevel_slices.extend(self.SlicesInActions(thread.toplevel_slices))
# Currently we report cpu-time per frame, tasks per frame, and possibly
# the mean frame (if there is a trace specified to find it).
def AddResults(self, num_frames, results):
cpu_per_frame = Rate(self.cpu_time, num_frames)
tasks_per_frame = Rate(len(self.toplevel_slices), num_frames)
results.AddValue(scalar.ScalarValue(
results.current_page, ThreadCpuTimeResultName(self.name),
'ms', cpu_per_frame))
results.AddValue(scalar.ScalarValue(
results.current_page, ThreadTasksResultName(self.name),
'tasks', tasks_per_frame))
# Report mean frame time if this is the thread we are using for normalizing
# other results. We could report other frame rates (eg. renderer_main) but
# this might get confusing.
if self.name == FrameTraceThreadName:
num_frames = self.CountTracesWithName(FrameTraceName)
mean_frame_time = Rate(self.all_action_time, num_frames)
results.AddValue(scalar.ScalarValue(
results.current_page, ThreadMeanFrameTimeResultName(self.name),
'ms', mean_frame_time))
def AddDetailedResults(self, num_frames, results):
slices_by_category = collections.defaultdict(list)
for s in self.all_slices:
slices_by_category[s.category].append(s)
all_self_times = []
for category, slices_in_category in slices_by_category.iteritems():
self_time = sum([x.self_time for x in slices_in_category])
all_self_times.append(self_time)
self_time_result = (float(self_time) / num_frames) if num_frames else 0
results.AddValue(scalar.ScalarValue(
results.current_page, ThreadDetailResultName(self.name, category),
'ms', self_time_result))
all_measured_time = sum(all_self_times)
idle_time = max(0, self.all_action_time - all_measured_time)
idle_time_result = (float(idle_time) / num_frames) if num_frames else 0
results.AddValue(scalar.ScalarValue(
results.current_page, ThreadDetailResultName(self.name, "idle"),
'ms', idle_time_result))
def CountTracesWithName(self, substring):
count = 0
for event in self.all_slices:
if substring in event.name:
count += 1
return count
class ThreadTimesTimelineMetric(timeline_based_metric.TimelineBasedMetric):
def __init__(self):
super(ThreadTimesTimelineMetric, self).__init__()
# Minimal traces, for minimum noise in CPU-time measurements.
self.results_to_report = AllThreads
self.details_to_report = NoThreads
def AddResults(self, model, _, interaction_records, results):
# Set up each thread category for consistant results.
thread_category_results = {}
for name in TimelineThreadCategories.values():
thread_category_results[name] = ResultsForThread(
model, [r.GetBounds() for r in interaction_records], name)
# Group the slices by their thread category.
for thread in model.GetAllThreads():
thread_category = ThreadCategoryName(thread.name)
thread_category_results[thread_category].AppendThreadSlices(thread)
# Group all threads.
for thread in model.GetAllThreads():
thread_category_results['total_all'].AppendThreadSlices(thread)
# Also group fast-path threads.
for thread in model.GetAllThreads():
if ThreadCategoryName(thread.name) in FastPathThreads:
thread_category_results['total_fast_path'].AppendThreadSlices(thread)
# Calculate the number of frames.
frame_rate_thread = thread_category_results[FrameTraceThreadName]
num_frames = frame_rate_thread.CountTracesWithName(FrameTraceName)
# Report the desired results and details.
for thread_results in thread_category_results.values():
if thread_results.name in self.results_to_report:
thread_results.AddResults(num_frames, results)
# TOOD(nduca): When generic results objects are done, this special case
# can be replaced with a generic UI feature.
if thread_results.name in self.details_to_report:
thread_results.AddDetailedResults(num_frames, results)