 Chromium Code Reviews
 Chromium Code Reviews Issue 1336373002:
  Port rendering_stats' implementation to javascript  (Closed) 
  Base URL: https://github.com/catapult-project/catapult@master
    
  
    Issue 1336373002:
  Port rendering_stats' implementation to javascript  (Closed) 
  Base URL: https://github.com/catapult-project/catapult@master| 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> | 
| +<!-- | 
| +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) { | 
| 
dsinclair
2015/09/22 16:08:31
not: no {}'s
 | 
| + 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) { | 
| 
dsinclair
2015/09/22 16:08:32
event.args.data && event.args.data.frame_count ===
 | 
| + processHasRenderingStats = true; | 
| + } | 
| + }); | 
| + return processHasRenderingStats; | 
| + } | 
| + | 
| + /* Returns the name of the events used to count frame timestamps. */ | 
| + function getTimestampEventName(process) { | 
| + if (process.title === 'SurfaceFlinger') | 
| 
dsinclair
2015/09/22 16:08:31
SurfaceFlinger should be a const.
 | 
| + 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 | 
| 
dsinclair
2015/09/22 16:08:32
nit: s/loggin/logging
 | 
| + * 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. | 
| 
dsinclair
2015/09/22 16:08:32
It seems inconsistent that these times are millise
 | 
| + **/ | 
| + 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); | 
| 
dsinclair
2015/09/22 16:08:32
Does this need to be wrapped?
 | 
| + } 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_ = []; | 
| 
dsinclair
2015/09/22 16:08:31
How does this data get used? The structure of list
 | 
| + | 
| + // Latency for a GestureScrollUpdate input event. | 
| + this.gestureScrollUpdateLatency_ = []; | 
| + | 
| + for (var i = 0; i < timeRanges.length; i++) { | 
| 
dsinclair
2015/09/22 16:08:31
What about:
timeRanges.forEach(function(timeRange
 | 
| + 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( | 
| 
dsinclair
2015/09/22 16:08:31
nit: s/getRefresh/refresh
 | 
| + surfaceFlingerProcess) { | 
| + surfaceFlingerProcess.iterateAllEvents( | 
| + function(event) { | 
| + if (event.title === constants.VSYNC_BEFORE) { | 
| + this.refreshPeriod = event.args['data'].refreshPeriod; | 
| + } | 
| 
dsinclair
2015/09/22 16:08:32
nit: remove {}'s
nit: event.args.data.refreshPeri
 | 
| + }, 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)); | 
| 
dsinclair
2015/09/22 16:08:32
nit: indent 4 on wrap
 | 
| + var eventLatencies = computeEventLatencies(latencyEvents); | 
| 
dsinclair
2015/09/22 16:08:32
nit: add blank line before
 | 
| + // 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]; | 
| 
dsinclair
2015/09/22 16:08:32
nit: indent 4
 | 
| + lastFrameTimestamps.push(event.start); | 
| + if (lastFrameTimestamps.length >= 2) { | 
| + this.frameTimes_[this.frameTimes_.length - 1].push( | 
| + lastFrameTimestamps[lastFrameTimestamps.length - 1] - | 
| 
dsinclair
2015/09/22 16:08:32
nit: indenting
 | 
| + lastFrameTimestamps[lastFrameTimestamps.length - 2]); | 
| + } | 
| + }, | 
| 
dsinclair
2015/09/22 16:08:32
nit: outdent 4
 | 
| + | 
| + initFrameTimestampsFromTimeline_: function( | 
| + process, timestampEventTitle, timeRange) { | 
| + this.gatherEvents_( | 
| + timestampEventTitle, process, timeRange).forEach( | 
| + function(event) { | 
| + this.addFrameTimestamp_(event); | 
| + }, this); | 
| + }, | 
| + | 
| + initImplThreadRenderingStatsFromTimeline_: function( | 
| + process, timeRange) { | 
| 
dsinclair
2015/09/22 16:08:32
nit: indenting  (does this need to wrap?)
 | 
| + 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 ' + | 
| 
dsinclair
2015/09/22 16:08:32
This isn't going to be a complete set of errors, i
 | 
| + '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'] = | 
| 
dsinclair
2015/09/22 16:08:31
Why isn't this a const like the others?
 | 
| + 'Current chrome version does not support the queueing ' + | 
| + ' delay metric. Error: ' + e.message; | 
| + } | 
| + } | 
| + }; | 
| + | 
| + return { | 
| + RenderingStats: RenderingStats, | 
| + RenderingStatsHelpers: { | 
| + getLatencyEvents: getLatencyEvents, | 
| + computeEventLatencies: computeEventLatencies, | 
| + hasRenderingStats: hasRenderingStats | 
| + } | 
| + }; | 
| +}); | 
| +</script> |