blob: 19db3d59ad1985c1cebcc645f45719e60ae55cb0 [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.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"]
ReportFastPathResults = AllThreads
ReportFastPathDetails = NoThreads
ReportSilkResults = ["renderer_main", "total_all"]
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 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 ThreadTimeResultName(thread_category):
return "thread_" + thread_category + "_clock_time_per_frame"
def ThreadCpuTimeResultName(thread_category):
return "thread_" + thread_category + "_cpu_time_per_frame"
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
@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))
def AddResults(self, num_frames, results):
cpu_per_frame = (float(self.cpu_time) / num_frames) if num_frames else 0
results.AddValue(scalar.ScalarValue(
results.current_page, ThreadCpuTimeResultName(self.name),
'ms', cpu_per_frame))
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)
all_action_time = \
sum([record_range.bounds for record_range in self.record_ranges])
idle_time = max(0, 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))
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 CountSlices(self, slices, substring):
count = 0
for event in slices:
if substring in event.name:
count += 1
return count
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_slices = thread_category_results[FrameTraceThreadName].all_slices
num_frames = self.CountSlices(frame_slices, 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)