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