Chromium Code Reviews| Index: perf_insights/perf_insights/timeline_based_measurement/rendering_stats.html |
| diff --git a/perf_insights/perf_insights/timeline_based_measurement/rendering_stats.html b/perf_insights/perf_insights/timeline_based_measurement/rendering_stats.html |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..e35b4a2716acf024006e5f3d98cf0e240ec2ded9 |
| --- /dev/null |
| +++ b/perf_insights/perf_insights/timeline_based_measurement/rendering_stats.html |
| @@ -0,0 +1,407 @@ |
| +<!DOCTYPE HTML> |
|
dsinclair
2015/09/22 16:08:31
nit: s/HTML/html
|
| +<!-- |
|
nduca
2015/09/21 19:55:15
i think we should probably put tbm stuff in metric
nednguyen
2015/09/21 20:09:54
Do you mean mappers/ folder? I don't see any metri
|
| +Copyright (c) 2015 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. |
| +--> |
| + |
| +<link rel="import" |
| + href="/perf_insights/timeline_based_measurement/rendering_frame.html"> |
| + |
| +<link rel="import" href="/tracing/base/math.html"> |
| +<link rel="import" href="/tracing/base/range.html"> |
| +<link rel="import" href="/tracing/extras/chrome/cc/constants.html"> |
| + |
| +<script> |
| +'use strict'; |
| + |
| +tr.exportTo('pi.tbm', function() { |
| + var constants = tr.e.cc.constants; |
| + var round = tr.b.round; |
| + |
| + // These are keys used in the 'errors' field dictionary located in |
| + // RenderingStats in this file. |
| + var APPROXIMATED_PIXEL_ERROR = 'approximatedPixelPercentages'; |
| + var CHECKERBOARDED_PIXEL_ERROR = 'checkerboardedPixelPercentages'; |
| + |
| + /** |
| + * Get LatencyInfo trace events from the process's trace buffer that are |
| + * within the timeRange. |
| + * |
| + * Input events dump their LatencyInfo into trace buffer as async trace |
| + * event of name starting with "InputLatency". Non-input events with name |
| + * starting with "Latency". The trace event has a member 'data' containing |
| + * its latency history. |
| + **/ |
| + function getLatencyEvents(process, timeRange) { |
| + var latencyEvents = []; |
| + if (!process) |
| + return latencyEvents; |
| + var i = 0; |
| + process.iterateAllEvents(function(event) { |
| + i += 1; |
| + if ((event.title === 'InputLatency' || event.title === 'Latency') && |
| + event.start >= timeRange.min && |
| + event.end <= timeRange.max) { |
| + event.subSlices.forEach(function(e) { |
| + if (e.args['data'] !== undefined) |
| + latencyEvents.push(e); |
| + }); |
| + } |
| + }); |
| + return latencyEvents; |
| + } |
| + |
| + /** |
| + * Compute input event latencies. |
| + * |
| + * Input event latency is the time from when the input event is created to |
| + * when its resulted page is swap buffered. |
| + * Input event on differnt platforms uses different LatencyInfo component to |
| + * record its creation timestamp. We go through the following component list |
| + * to find the creation timestamp: |
| + * 1. INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT -- when event is created in OS |
| + * 2. INPUT_EVENT_LATENCY_UI_COMPONENT -- when event reaches Chrome |
| + * 3. INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT -- when event reaches |
| + * RenderWidget |
| + |
| + * If the latency starts with a |
| + * LATENCY_BEGIN_SCROLL_UPDATE_MAIN_COMPONENT component, then it is |
| + * classified as a scroll update instead of a normal input latency measure. |
| + |
| + * Returns: |
| + * A list sorted by increasing start time of latencies which are tuples of |
| + * (input_eventTitle, latency_in_ms). |
| + **/ |
| + function computeEventLatencies(inputEvents) { |
| + var inputEventLatencies = []; |
| + for (var i = 0; i < inputEvents.length; i++) { |
| + var event = inputEvents[i]; |
| + var data = event.args['data']; |
| + var endTime = undefined; |
| + var startTime = undefined; |
| + if (data[constants.END_COMP_NAME]) { |
| + endTime = data[constants.END_COMP_NAME]['time']; |
| + [constants.ORIGINAL_COMP_NAME, |
| + constants.UI_COMP_NAME, |
| + constants.BEGIN_COMP_NAME, |
| + constants.BEGIN_SCROLL_UPDATE_COMP_NAME].forEach(function(name) { |
| + if (data[name] && startTime === undefined) { |
| + startTime = data[name]['time']; |
| + } |
| + }); |
| + if (startTime === undefined) |
| + throw Error('LatencyInfo has no begin component'); |
| + var latency = (endTime - startTime) / 1000.0; |
| + inputEventLatencies.push({ |
| + start: startTime, |
| + title: event.title, |
| + latency: latency |
| + }); |
| + } |
| + } |
| + inputEventLatencies.sort( |
| + function(a, b) { |
| + return a.start - b.start; |
| + }); |
| + return inputEventLatencies; |
| + } |
| + |
| + /** |
| + * Returns true if the process contains at least one |
| + * BenchmarkInstrumentation::*RenderingStats event with a frame. |
| + **/ |
| + function hasRenderingStats(process) { |
| + if (!process) |
| + return false; |
| + var processHasRenderingStats = false; |
| + process.iterateAllEvents( |
| + function(event) { |
| + if ((event.title === constants.BENCHMARK_DISPLAY_RENDERING_STATS || |
| + event.title === constants.BENCHMARK_IMPL_THREAD_RENDERING_STATS) && |
| + event.args['data'] && event.args['data']['frame_count'] === 1) { |
| + processHasRenderingStats = true; |
| + } |
| + }); |
| + return processHasRenderingStats; |
| + } |
| + |
| + /* Returns the name of the events used to count frame timestamps. */ |
| + function getTimestampEventName(process) { |
| + if (process.title === 'SurfaceFlinger') |
| + return constants.VSYNC_BEFORE; |
| + |
| + var eventTitle = constants.BENCHMARK_IMPL_THREAD_RENDERING_STATS; |
| + process.iterateAllEvents( |
| + function(event) { |
| + if (event.title === constants.BENCHMARK_DISPLAY_RENDERING_STATS && |
| + event.args['data'] !== undefined && |
| + event.args['data']['frame_count'] === 1) { |
| + eventTitle = constants.BENCHMARK_DISPLAY_RENDERING_STATS; |
| + } |
| + }); |
| + return eventTitle; |
| + } |
| + |
| + /** |
| + * Utility class for extracting rendering statistics from the timeline (or |
| + * other loggin facilities), and providing them in a common format to |
| + * classes that compute benchmark metrics from this data. |
| + * |
| + * Stats are lists of lists of numbers. The outer list stores one list per |
| + * timeline range. |
| + * |
| + * All *_time values are measured in milliseconds. |
| + **/ |
| + function RenderingStats(rendererProcess, browserProcess, |
| + surfaceFlingerProcess, timeRanges) { |
| + if (timeRanges.length === 0) |
| + throw new Error('timeRanges is empty'); |
| + timeRanges.forEach(function(range) { |
| + if (!(range instanceof tr.b.Range)) |
| + throw new Error('timeRanges must contain only Range objects'); |
| + }); |
| + this.refreshPeriod = undefined; |
| + |
| + var timestampProcess; |
| + // Find the top level process with rendering stats (browser or renderer). |
| + if (surfaceFlingerProcess) { |
| + timestampProcess = surfaceFlingerProcess; |
| + this.getRefreshPeriodFromSurfaceFlingerProcess_( |
| + surfaceFlingerProcess); |
| + } else if (hasRenderingStats(browserProcess)) { |
| + timestampProcess = browserProcess; |
| + } else { |
| + timestampProcess = rendererProcess; |
| + } |
| + |
| + var timestampEventTitle = getTimestampEventName(timestampProcess); |
| + |
| + // A lookup from list names below to any errors or exceptions encountered |
| + // in attempting to generate that list. |
| + this.errors = {}; |
| + |
| + this.frameTimestamps_ = []; |
| + this.frameTimes_ = []; |
| + this.approximatedPixelPercentages_ = []; |
| + this.checkerboardedPixelPercentages_ = []; |
| + |
| + // End-to-end latency for input event - from when input event is |
| + // generated to when the its resulted page is swap buffered. |
| + this.inputEventLatency_ = []; |
| + this.frameQueueingDurations_ = []; |
| + |
| + // Latency from when a scroll update is sent to the main thread until the |
| + // resulting frame is swapped. |
| + this.scrollUpdateLatency_ = []; |
| + |
| + // Latency for a GestureScrollUpdate input event. |
| + this.gestureScrollUpdateLatency_ = []; |
| + |
| + for (var i = 0; i < timeRanges.length; i++) { |
| + var timeRange = timeRanges[i]; |
| + this.frameTimestamps_.push([]); |
| + this.frameTimes_.push([]); |
| + this.approximatedPixelPercentages_.push([]); |
| + this.checkerboardedPixelPercentages_.push([]); |
| + this.inputEventLatency_.push([]); |
| + this.scrollUpdateLatency_.push([]); |
| + this.gestureScrollUpdateLatency_.push([]); |
| + |
| + this.initFrameTimestampsFromTimeline_( |
| + timestampProcess, timestampEventTitle, timeRange); |
| + this.initImplThreadRenderingStatsFromTimeline_( |
| + rendererProcess, timeRange); |
| + this.initInputLatencyStatsFromTimeline_( |
| + browserProcess, rendererProcess, timeRange); |
| + this.initFrameQueueingDurationsFromTimeline_( |
| + rendererProcess, timeRange); |
| + } |
| + |
| + } |
| + |
| + RenderingStats.prototype = { |
| + |
| + get frameTimestamps() { |
| + return this.frameTimestamps_; |
| + }, |
| + |
| + get frameTimes() { |
| + return this.frameTimes_; |
| + }, |
| + |
| + get approximatedPixelPercentages() { |
| + return this.approximatedPixelPercentages_; |
| + }, |
| + |
| + get checkerboardedPixelPercentages() { |
| + return this.checkerboardedPixelPercentages_; |
| + }, |
| + |
| + get inputEventLatency() { |
| + return this.inputEventLatency_; |
| + }, |
| + |
| + get frameQueueingDurations() { |
| + return this.frameQueueingDurations_; |
| + }, |
| + |
| + get scrollUpdateLatency() { |
| + return this.scrollUpdateLatency_; |
| + }, |
| + |
| + get gestureScrollUpdateLatency() { |
| + return this.gestureScrollUpdateLatency_; |
| + }, |
| + |
| + getRefreshPeriodFromSurfaceFlingerProcess_: function( |
| + surfaceFlingerProcess) { |
| + surfaceFlingerProcess.iterateAllEvents( |
| + function(event) { |
| + if (event.title === constants.VSYNC_BEFORE) { |
| + this.refreshPeriod = event.args['data'].refreshPeriod; |
| + } |
| + }, this); |
| + }, |
| + |
| + initInputLatencyStatsFromTimeline_: function(browserProcess, |
| + rendererProcess, timeRange) { |
| + var latencyEvents = getLatencyEvents(browserProcess, timeRange); |
| + // Plugin input event's latency slice is generated in renderer process. |
| + latencyEvents.push.apply(latencyEvents, |
| + getLatencyEvents(rendererProcess, timeRange)); |
| + var eventLatencies = computeEventLatencies(latencyEvents); |
| + // Don't include scroll updates in the overall input latency |
| + // measurement, because scroll updates can take much more time to |
| + // process than other input events and would therefore add noise to |
| + // overall latency numbers. |
| + |
| + eventLatencies.forEach(function(event) { |
| + if (event.title !== constants.SCROLL_UPDATE_EVENT_NAME) { |
| + this.inputEventLatency_[this.inputEventLatency_.length - 1].push( |
| + event.latency); |
| + } |
| + if (event.title === constants.SCROLL_UPDATE_EVENT_NAME) { |
| + this.scrollUpdateLatency_[this.scrollUpdateLatency_.length - 1].push( |
| + event.latency); |
| + } |
| + if (event.title === constants.GESTURE_SCROLL_UPDATE_EVENT_NAME) { |
| + this.gestureScrollUpdateLatency_[ |
| + this.gestureScrollUpdateLatency_.length - 1].push(event.latency); |
| + } |
| + }, this); |
| + }, |
| + |
| + gatherEvents_: function(eventTitle, process, timeRange) { |
| + var events = []; |
| + process.iterateAllEvents(function(event) { |
| + if (event.title === eventTitle && |
| + event.start >= timeRange.min && |
| + event.end <= timeRange.max && |
| + event.args['data'] !== undefined) |
| + events.push(event); |
| + }); |
| + events.sort(function(a, b) { |
| + return a.start - b.start; |
| + }); |
| + return events; |
| + }, |
| + |
| + addFrameTimestamp_: function(event) { |
| + var frameCount = event.args['data']['frame_count']; |
| + if (frameCount > 1) |
| + throw Error('trace contains multi-frame render stats'); |
| + if (frameCount === 0) |
| + return; |
| + var lastFrameTimestamps = |
| + this.frameTimestamps_[this.frameTimestamps_.length - 1]; |
| + lastFrameTimestamps.push(event.start); |
| + if (lastFrameTimestamps.length >= 2) { |
| + this.frameTimes_[this.frameTimes_.length - 1].push( |
| + lastFrameTimestamps[lastFrameTimestamps.length - 1] - |
| + lastFrameTimestamps[lastFrameTimestamps.length - 2]); |
| + } |
| + }, |
| + |
| + initFrameTimestampsFromTimeline_: function( |
| + process, timestampEventTitle, timeRange) { |
| + this.gatherEvents_( |
| + timestampEventTitle, process, timeRange).forEach( |
| + function(event) { |
| + this.addFrameTimestamp_(event); |
| + }, this); |
| + }, |
| + |
| + initImplThreadRenderingStatsFromTimeline_: function( |
| + process, timeRange) { |
| + var eventTitle = constants.BENCHMARK_IMPL_THREAD_RENDERING_STATS; |
| + this.gatherEvents_(eventTitle, process, timeRange).forEach( |
| + function(event) { |
| + var data = event.args['data']; |
| + if (data[constants.VISIBLE_CONTENT_DATA] === undefined) { |
| + this.errors[APPROXIMATED_PIXEL_ERROR] = |
| + 'Calculating approximatedPixelPercentages not possible ' + |
| + 'because visible_content_area was missing.'; |
| + this.errors[CHECKERBOARDED_PIXEL_ERROR] = |
| + 'Calculating checkerboardedPixelPercentages not possible ' + |
| + 'because visible_content_area was missing.'; |
| + return; |
| + } |
| + var visible_content_area = data[constants.VISIBLE_CONTENT_DATA]; |
| + if (visible_content_area === 0) { |
| + this.errors[APPROXIMATED_PIXEL_ERROR] = |
| + 'Calculating approximatedPixelPercentages would have ' + |
| + 'caused a divide-by-zero'; |
| + this.errors[CHECKERBOARDED_PIXEL_ERROR] = |
| + 'Calculating checkerboardedPixelPercentages would have ' + |
| + 'caused a divide-by-zero'; |
| + return; |
| + } |
| + if (constants.APPROXIMATED_VISIBLE_CONTENT_DATA in data) { |
| + var last_index = this.approximatedPixelPercentages_.length - 1; |
| + this.approximatedPixelPercentages_[last_index].push( |
| + round(data[constants.APPROXIMATED_VISIBLE_CONTENT_DATA] / |
| + data[constants.VISIBLE_CONTENT_DATA] * 100.0, 3)); |
| + } else { |
| + this.errors[APPROXIMATED_PIXEL_ERROR] = ( |
| + 'approximatedPixelPercentages was not recorded'); |
| + } |
| + if (constants.CHECKERBOARDED_VISIBLE_CONTENT_DATA in data) { |
| + var last_index = this.checkerboardedPixelPercentages.length - 1; |
| + this.checkerboardedPixelPercentages[last_index].push( |
| + round(data[constants.CHECKERBOARDED_VISIBLE_CONTENT_DATA] / |
| + data[constants.VISIBLE_CONTENT_DATA] * 100.0, 3)); |
| + } else { |
| + this.errors[CHECKERBOARDED_PIXEL_ERROR] = |
| + 'checkerboardedPixelPercentages was not recorded'; |
| + } |
| + }, this); |
| + }, |
| + |
| + initFrameQueueingDurationsFromTimeline_: function(process, timeRange) { |
| + try { |
| + var events = |
| + pi.tbm.RenderingFrame.getFrameEventsInsideRange( |
| + process, timeRange); |
| + var new_frameQueueingDurations = events.map(function(e) { |
| + return e.queueing_duration; |
| + }); |
| + this.frameQueueingDurations_.push(new_frameQueueingDurations); |
| + } catch (e) { |
| + this.errors['frameQueueingDurations'] = |
| + 'Current chrome version does not support the queueing ' + |
| + ' delay metric. Error: ' + e.message; |
| + } |
| + } |
| + }; |
| + |
| + return { |
| + RenderingStats: RenderingStats, |
| + RenderingStatsHelpers: { |
| + getLatencyEvents: getLatencyEvents, |
| + computeEventLatencies: computeEventLatencies, |
| + hasRenderingStats: hasRenderingStats |
| + } |
| + }; |
| +}); |
| +</script> |