Home | History | Annotate | Download | only in metrics
      1 # Copyright 2014 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 import collections
      5 
      6 from telemetry.web_perf.metrics import timeline_based_metric
      7 from telemetry.value import scalar
      8 
      9 
     10 class LoadTimesTimelineMetric(timeline_based_metric.TimelineBasedMetric):
     11   def __init__(self):
     12     super(LoadTimesTimelineMetric, self).__init__()
     13     self.report_main_thread_only = True
     14 
     15   def AddResults(self, model, renderer_thread, interaction_records, results):
     16     assert model
     17     assert len(interaction_records) == 1, (
     18       'LoadTimesTimelineMetric cannot compute metrics for more than 1 time '
     19       'range.')
     20     interaction_record = interaction_records[0]
     21     if self.report_main_thread_only:
     22       thread_filter = 'CrRendererMain'
     23     else:
     24       thread_filter = None
     25 
     26     events_by_name = collections.defaultdict(list)
     27     renderer_process = renderer_thread.parent
     28 
     29     for thread in renderer_process.threads.itervalues():
     30 
     31       if thread_filter and not thread.name in thread_filter:
     32         continue
     33 
     34       thread_name = thread.name.replace('/','_')
     35       for e in thread.IterAllSlicesInRange(interaction_record.start,
     36                                            interaction_record.end):
     37         events_by_name[e.name].append(e)
     38 
     39       for event_name, event_group in events_by_name.iteritems():
     40         times = [event.self_time for event in event_group]
     41         total = sum(times)
     42         biggest_jank = max(times)
     43 
     44         # Results objects cannot contain the '.' character, so remove that here.
     45         sanitized_event_name = event_name.replace('.', '_')
     46 
     47         full_name = thread_name + '|' + sanitized_event_name
     48         results.AddValue(scalar.ScalarValue(
     49             results.current_page, full_name, 'ms', total))
     50         results.AddValue(scalar.ScalarValue(
     51             results.current_page, full_name + '_max', 'ms', biggest_jank))
     52         results.AddValue(scalar.ScalarValue(
     53             results.current_page, full_name + '_avg', 'ms', total / len(times)))
     54 
     55     for counter_name, counter in renderer_process.counters.iteritems():
     56       total = sum(counter.totals)
     57 
     58       # Results objects cannot contain the '.' character, so remove that here.
     59       sanitized_counter_name = counter_name.replace('.', '_')
     60 
     61       results.AddValue(scalar.ScalarValue(
     62           results.current_page, sanitized_counter_name, 'count', total))
     63       results.AddValue(scalar.ScalarValue(
     64           results.current_page, sanitized_counter_name + '_avg', 'count',
     65           total / float(len(counter.totals))))
     66 
     67 # We want to generate a consistant picture of our thread usage, despite
     68 # having several process configurations (in-proc-gpu/single-proc).
     69 # Since we can't isolate renderer threads in single-process mode, we
     70 # always sum renderer-process threads' times. We also sum all io-threads
     71 # for simplicity.
     72 TimelineThreadCategories =  {
     73   "Chrome_InProcGpuThread": "GPU",
     74   "CrGpuMain"             : "GPU",
     75   "AsyncTransferThread"   : "GPU_transfer",
     76   "CrBrowserMain"         : "browser",
     77   "Browser Compositor"    : "browser",
     78   "CrRendererMain"        : "renderer_main",
     79   "Compositor"            : "renderer_compositor",
     80   "IOThread"              : "IO",
     81   "CompositorRasterWorker": "raster",
     82   "DummyThreadName1"      : "other",
     83   "DummyThreadName2"      : "total_fast_path",
     84   "DummyThreadName3"      : "total_all"
     85 }
     86 
     87 _MatchBySubString = ["IOThread", "CompositorRasterWorker"]
     88 
     89 AllThreads = TimelineThreadCategories.values()
     90 NoThreads = []
     91 FastPathThreads = ["GPU", "renderer_compositor", "browser", "IO"]
     92 
     93 ReportMainThreadOnly = ["renderer_main"]
     94 ReportFastPathResults = AllThreads
     95 ReportFastPathDetails = NoThreads
     96 ReportSilkResults = ["renderer_main", "total_all"]
     97 ReportSilkDetails = ["renderer_main"]
     98 
     99 # TODO(epenner): Thread names above are likely fairly stable but trace names
    100 # could change. We should formalize these traces to keep this robust.
    101 OverheadTraceCategory = "trace_event_overhead"
    102 OverheadTraceName = "overhead"
    103 FrameTraceName = "::SwapBuffers"
    104 FrameTraceThreadName = "renderer_compositor"
    105 
    106 
    107 def ClockOverheadForEvent(event):
    108   if (event.category == OverheadTraceCategory and
    109       event.name == OverheadTraceName):
    110     return event.duration
    111   else:
    112     return 0
    113 
    114 def CpuOverheadForEvent(event):
    115   if (event.category == OverheadTraceCategory and
    116       event.thread_duration):
    117     return event.thread_duration
    118   else:
    119     return 0
    120 
    121 def ThreadCategoryName(thread_name):
    122   thread_category = "other"
    123   for substring, category in TimelineThreadCategories.iteritems():
    124     if substring in _MatchBySubString and substring in thread_name:
    125       thread_category = category
    126   if thread_name in TimelineThreadCategories:
    127     thread_category = TimelineThreadCategories[thread_name]
    128   return thread_category
    129 
    130 def ThreadTimeResultName(thread_category):
    131   return "thread_" + thread_category + "_clock_time_per_frame"
    132 
    133 def ThreadCpuTimeResultName(thread_category):
    134   return "thread_" + thread_category + "_cpu_time_per_frame"
    135 
    136 def ThreadDetailResultName(thread_category, detail):
    137   detail_sanitized = detail.replace('.','_')
    138   return "thread_" + thread_category + "|" + detail_sanitized
    139 
    140 
    141 class ResultsForThread(object):
    142   def __init__(self, model, record_ranges, name):
    143     self.model = model
    144     self.toplevel_slices = []
    145     self.all_slices = []
    146     self.name = name
    147     self.record_ranges = record_ranges
    148 
    149   @property
    150   def clock_time(self):
    151     clock_duration = sum([x.duration for x in self.toplevel_slices])
    152     clock_overhead = sum([ClockOverheadForEvent(x) for x in self.all_slices])
    153     return clock_duration - clock_overhead
    154 
    155   @property
    156   def cpu_time(self):
    157     cpu_duration = 0
    158     cpu_overhead = sum([CpuOverheadForEvent(x) for x in self.all_slices])
    159     for x in self.toplevel_slices:
    160       # Only report thread-duration if we have it for all events.
    161       #
    162       # A thread_duration of 0 is valid, so this only returns 0 if it is None.
    163       if x.thread_duration == None:
    164         if not x.duration:
    165           continue
    166         else:
    167           return 0
    168       else:
    169         cpu_duration += x.thread_duration
    170     return cpu_duration - cpu_overhead
    171 
    172   def SlicesInActions(self, slices):
    173     slices_in_actions = []
    174     for event in slices:
    175       for record_range in self.record_ranges:
    176         if record_range.ContainsInterval(event.start, event.end):
    177           slices_in_actions.append(event)
    178           break
    179     return slices_in_actions
    180 
    181   def AppendThreadSlices(self, thread):
    182     self.all_slices.extend(self.SlicesInActions(thread.all_slices))
    183     self.toplevel_slices.extend(self.SlicesInActions(thread.toplevel_slices))
    184 
    185   def AddResults(self, num_frames, results):
    186     cpu_per_frame = (float(self.cpu_time) / num_frames) if num_frames else 0
    187     results.AddValue(scalar.ScalarValue(
    188         results.current_page, ThreadCpuTimeResultName(self.name),
    189         'ms', cpu_per_frame))
    190 
    191   def AddDetailedResults(self, num_frames, results):
    192     slices_by_category = collections.defaultdict(list)
    193     for s in self.all_slices:
    194       slices_by_category[s.category].append(s)
    195     all_self_times = []
    196     for category, slices_in_category in slices_by_category.iteritems():
    197       self_time = sum([x.self_time for x in slices_in_category])
    198       all_self_times.append(self_time)
    199       self_time_result = (float(self_time) / num_frames) if num_frames else 0
    200       results.AddValue(scalar.ScalarValue(
    201           results.current_page, ThreadDetailResultName(self.name, category),
    202           'ms', self_time_result))
    203     all_measured_time = sum(all_self_times)
    204     all_action_time = \
    205         sum([record_range.bounds for record_range in self.record_ranges])
    206     idle_time = max(0, all_action_time - all_measured_time)
    207     idle_time_result = (float(idle_time) / num_frames) if num_frames else 0
    208     results.AddValue(scalar.ScalarValue(
    209         results.current_page, ThreadDetailResultName(self.name, "idle"),
    210         'ms', idle_time_result))
    211 
    212 
    213 class ThreadTimesTimelineMetric(timeline_based_metric.TimelineBasedMetric):
    214   def __init__(self):
    215     super(ThreadTimesTimelineMetric, self).__init__()
    216     # Minimal traces, for minimum noise in CPU-time measurements.
    217     self.results_to_report = AllThreads
    218     self.details_to_report = NoThreads
    219 
    220   def CountSlices(self, slices, substring):
    221     count = 0
    222     for event in slices:
    223       if substring in event.name:
    224         count += 1
    225     return count
    226 
    227   def AddResults(self, model, _, interaction_records, results):
    228     # Set up each thread category for consistant results.
    229     thread_category_results = {}
    230     for name in TimelineThreadCategories.values():
    231       thread_category_results[name] = ResultsForThread(
    232         model, [r.GetBounds() for r in interaction_records], name)
    233 
    234     # Group the slices by their thread category.
    235     for thread in model.GetAllThreads():
    236       thread_category = ThreadCategoryName(thread.name)
    237       thread_category_results[thread_category].AppendThreadSlices(thread)
    238 
    239     # Group all threads.
    240     for thread in model.GetAllThreads():
    241       thread_category_results['total_all'].AppendThreadSlices(thread)
    242 
    243     # Also group fast-path threads.
    244     for thread in model.GetAllThreads():
    245       if ThreadCategoryName(thread.name) in FastPathThreads:
    246         thread_category_results['total_fast_path'].AppendThreadSlices(thread)
    247 
    248     # Calculate the number of frames.
    249     frame_slices = thread_category_results[FrameTraceThreadName].all_slices
    250     num_frames = self.CountSlices(frame_slices, FrameTraceName)
    251 
    252     # Report the desired results and details.
    253     for thread_results in thread_category_results.values():
    254       if thread_results.name in self.results_to_report:
    255         thread_results.AddResults(num_frames, results)
    256       # TOOD(nduca): When generic results objects are done, this special case
    257       # can be replaced with a generic UI feature.
    258       if thread_results.name in self.details_to_report:
    259         thread_results.AddDetailedResults(num_frames, results)
    260