diff options
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/metrics')
96 files changed, 24446 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/__init__.py b/chromium/third_party/catapult/tracing/tracing/metrics/__init__.py new file mode 100644 index 00000000000..cffcee63c42 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2016 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 os +import sys + + +_CATAPULT_DIR = os.path.abspath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), '..', '..', '..')) + +_PI_PATH = os.path.join(_CATAPULT_DIR, 'perf_insights') + +if _PI_PATH not in sys.path: + sys.path.insert(1, _PI_PATH) diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric.html new file mode 100644 index 00000000000..43c6b393dbe --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics', function() { + function accessibilityMetric(histograms, model) { + const browserAccessibilityEventsHist = new tr.v.Histogram( + 'browser_accessibility_events', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter); + browserAccessibilityEventsHist.description = + 'Browser accessibility events time'; + + const renderAccessibilityEventsHist = new tr.v.Histogram( + 'render_accessibility_events', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter); + renderAccessibilityEventsHist.description = + 'Render accessibility events time'; + + const renderAccessibilityLocationsHist = new tr.v.Histogram( + 'render_accessibility_locations', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter); + renderAccessibilityLocationsHist.description = + 'Render accessibility locations time'; + + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (chromeHelper === undefined) return; + + for (const rendererHelper of Object.values(chromeHelper.rendererHelpers)) { + const mainThread = rendererHelper.mainThread; + if (mainThread === undefined) continue; + + for (const slice of mainThread.getDescendantEvents()) { + if (!(slice instanceof tr.model.ThreadSlice)) continue; + + if (slice.title === + 'RenderAccessibilityImpl::SendPendingAccessibilityEvents') { + renderAccessibilityEventsHist.addSample(slice.duration, + {event: new tr.v.d.RelatedEventSet(slice)}); + } + if (slice.title === + 'RenderAccessibilityImpl::SendLocationChanges') { + renderAccessibilityLocationsHist.addSample(slice.duration, + {event: new tr.v.d.RelatedEventSet(slice)}); + } + } + } + + for (const browserHelper of Object.values(chromeHelper.browserHelpers)) { + const mainThread = browserHelper.mainThread; + if (mainThread === undefined) continue; + + for (const slice of mainThread.getDescendantEvents()) { + if (slice.title === + 'BrowserAccessibilityManager::OnAccessibilityEvents') { + browserAccessibilityEventsHist.addSample(slice.duration, + {event: new tr.v.d.RelatedEventSet(slice)}); + } + } + } + + histograms.addHistogram(browserAccessibilityEventsHist); + histograms.addHistogram(renderAccessibilityEventsHist); + histograms.addHistogram(renderAccessibilityLocationsHist); + } + + tr.metrics.MetricRegistry.register(accessibilityMetric); + + return { + accessibilityMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric_test.html new file mode 100644 index 00000000000..27dbf178637 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/accessibility_metric_test.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/accessibility_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function makeTestModel() { + return tr.c.TestUtils.newModel(function(model) { + const browserProcess = model.getOrCreateProcess(99); + browserProcess.name = 'Browser'; + const browserThread = browserProcess.getOrCreateThread(2); + browserThread.name = 'CrBrowserMain'; + browserThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'accessibility', + title: 'BrowserAccessibilityManager::OnAccessibilityEvents', + start: 1000, + dur: 71, + end: 1071, + })); + browserThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'accessibility', + title: 'BrowserAccessibilityManager::OnAccessibilityEvents', + start: 1500, + dur: 22, + end: 1522, + })); + + const renderThread = model.getOrCreateProcess(1).getOrCreateThread(2); + renderThread.name = 'CrRendererMain'; + renderThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'accessibility', + title: 'RenderAccessibilityImpl::SendPendingAccessibilityEvents', + start: 800, + dur: 228, + end: 1028, + })); + renderThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'accessibility', + title: 'RenderAccessibilityImpl::SendLocationChanges', + start: 900, + dur: 12, + end: 912, + })); + renderThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'accessibility', + title: 'RenderAccessibilityImpl::SendPendingAccessibilityEvents', + start: 1400, + dur: 188, + end: 1588, + })); + }); + } + + test('accessibilityMetric', function() { + const histograms = new tr.v.HistogramSet(); + tr.metrics.accessibilityMetric(histograms, makeTestModel()); + assert.closeTo(93, histograms.getHistogramNamed( + 'browser_accessibility_events').sum, 1e-2); + assert.closeTo(416, histograms.getHistogramNamed( + 'render_accessibility_events').sum, 1e-2); + assert.closeTo(12, histograms.getHistogramNamed( + 'render_accessibility_locations').sum, 1e-2); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/all_fixed_color_schemes.html b/chromium/third_party/catapult/tracing/tracing/metrics/all_fixed_color_schemes.html new file mode 100644 index 00000000000..be843ee9aca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/all_fixed_color_schemes.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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. +--> + +<!-- We import all files that register fixed color schemes which are used by + metrics so that metrics code can depend on a single place. --> +<link rel="import" href="/tracing/extras/chrome/chrome_processes.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/all_metrics.html b/chromium/third_party/catapult/tracing/tracing/metrics/all_metrics.html new file mode 100644 index 00000000000..68c1b2f34ca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/all_metrics.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/metrics/accessibility_metric.html"> +<link rel="import" href="/tracing/metrics/android_startup_metric.html"> +<link rel="import" href="/tracing/metrics/android_systrace_metric.html"> +<link rel="import" href="/tracing/metrics/blink/gc_metric.html"> +<link rel="import" href="/tracing/metrics/blink/leak_detection_metric.html"> +<link rel="import" href="/tracing/metrics/console_error_metric.html"> +<link rel="import" href="/tracing/metrics/cpu_process_metric.html"> +<link rel="import" href="/tracing/metrics/media_metric.html"> +<link rel="import" href="/tracing/metrics/rendering/rendering_metric.html"> +<link rel="import" href="/tracing/metrics/sample_exception_metric.html"> +<link rel="import" href="/tracing/metrics/sample_metric.html"> +<link rel="import" href="/tracing/metrics/spa_navigation_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/clock_sync_latency_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/cpu_time_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/expected_queueing_time_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/limited_cpu_time_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/loading_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/long_tasks_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/memory_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/new_cpu_time_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/power_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/responsiveness_metric.html"> +<link rel="import" href="/tracing/metrics/system_health/webview_startup_metric.html"> +<link rel="import" href="/tracing/metrics/tabs_metric.html"> +<link rel="import" href="/tracing/metrics/tracing_metric.html"> +<link rel="import" href="/tracing/metrics/v8/execution_metric.html"> +<link rel="import" href="/tracing/metrics/v8/gc_metric.html"> +<link rel="import" href="/tracing/metrics/v8/runtime_stats_metric.html"> +<link rel="import" href="/tracing/metrics/v8/v8_metrics.html"> +<link rel="import" href="/tracing/metrics/vr/frame_cycle_duration_metric.html"> +<link rel="import" href="/tracing/metrics/vr/webvr_metric.html"> +<link rel="import" href="/tracing/metrics/webrtc/webrtc_rendering_metric.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric.html new file mode 100644 index 00000000000..4b6bc49ee68 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric.html @@ -0,0 +1,153 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +// The |androidStartupMetric| produces metrics that start counting at the +// earliest moment the Chrome code on Android is executed. +// A few histograms are produced with the names as described below: +// 1. messageloop_start_time - time till the message loop of the browser main +// starts processing posted tasts (after having loaded the Profile) +// 2. first_contentful_paint_time - time to the first contentful paint of the +// page loaded at startup +// The metric also supports multiple browser restarts, in this case multiple +// samples would be added to the histograms above. +tr.exportTo('tr.metrics.sh', function() { + const MESSAGE_LOOP_EVENT_NAME = + 'Startup.BrowserMessageLoopStartTimeFromMainEntry3'; + const FIRST_CONTENTFUL_PAINT_EVENT_NAME = 'firstContentfulPaint'; + function androidStartupMetric(histograms, model) { + // Walk the browser slices, extract timestamps for the browser start, + // message loop start. TODO(crbug.com/883290): re-introduce + // request_start_time. + let messageLoopStartEvents = []; + const chromeHelper = + model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + if (!chromeHelper) return; + for (const helper of chromeHelper.browserHelpers) { + for (const ev of helper.mainThread.asyncSliceGroup.childEvents()) { + if (ev.title === MESSAGE_LOOP_EVENT_NAME) { + messageLoopStartEvents.push(ev); + } + } + } + + // Walk the renderer slices and extract the 'first contentful paint' + // histogram samples. + let firstContentfulPaintEvents = []; + const rendererHelpers = chromeHelper.rendererHelpers; + const pids = Object.keys(rendererHelpers); + for (const rendererHelper of Object.values(chromeHelper.rendererHelpers)) { + if (!rendererHelper.mainThread) continue; + for (const ev of rendererHelper.mainThread.sliceGroup.childEvents()) { + if (ev.title === FIRST_CONTENTFUL_PAINT_EVENT_NAME) { + firstContentfulPaintEvents.push(ev); + // There are usually several 'First Contentful Paint' events recorded + // for each page load. Take only the first one per renderer. + break; + } + } + } + + // Fallback to scanning all processes if important events are not found. + let totalBrowserStarts = messageLoopStartEvents.length; + let totalFcpEvents = firstContentfulPaintEvents.length; + if (totalFcpEvents !== totalBrowserStarts || totalBrowserStarts === 0) { + messageLoopStartEvents = []; + firstContentfulPaintEvents = []; + // Sometimes either the browser process or the renderer process does not + // have the proper name attached. This often happens when both chrome + // trace and systrace are merged. Other multi-process trickery, like + // Perfetto, may also cause this. + for (const proc of Object.values(model.processes)) { + for (const ev of proc.getDescendantEvents()) { + if (ev.title === MESSAGE_LOOP_EVENT_NAME) { + messageLoopStartEvents.push(ev); + } + } + for (const ev of proc.getDescendantEvents()) { + if (ev.title === FIRST_CONTENTFUL_PAINT_EVENT_NAME) { + firstContentfulPaintEvents.push(ev); + break; + } + } + } + totalBrowserStarts = messageLoopStartEvents.length; + totalFcpEvents = firstContentfulPaintEvents.length; + } + + // Sometimes a number of early trace events are not recorded because tracing + // takes time to start. This leads to having more FCP events than + // messageloop_start events. As a workaround ignore the FCP events for which + // there are no browser starts. + function orderEvents(event1, event2) { + return event1.start - event2.start; + } + messageLoopStartEvents.sort(orderEvents); + firstContentfulPaintEvents.sort(orderEvents); + + if (totalFcpEvents < totalBrowserStarts) { + throw new Error('Found less FCP events (' + totalFcpEvents + + ') than browser starts (' + totalBrowserStarts + ')'); + } + + // Group the relevant events with the corresponding browser starts and emit + // the metrics. + const messageLoopStartHistogram = histograms.createHistogram( + 'messageloop_start_time', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, []); + const firstContentfulPaintHistogram = histograms.createHistogram( + 'first_contentful_paint_time', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, []); + // The earliest browser start is skipped because it is affected by the state + // of the system coming from the time before the benchmark started. Removing + // these influencing factors allows reducing measurement noise. + // Note: Two early starts are ignored below, the reasons for spurious + // slowdowns of the 2nd run are not known yet, see http://crbug.com/891797. + let fcpIndex = 0; + for (let loopStartIndex = 0; loopStartIndex < totalBrowserStarts;) { + const startEvent = messageLoopStartEvents[loopStartIndex]; + if (fcpIndex === totalFcpEvents) { + break; + } + + // Skip all FCP events that appear before the next browser start. + const fcpEvent = firstContentfulPaintEvents[fcpIndex]; + if (fcpEvent.start < startEvent.start) { + fcpIndex++; + continue; + } + + // The pair of matching events is found. + loopStartIndex++; + + // Skip the two initial FCP events and (potentially missing) browser + // starts. + if (fcpIndex < 2) { + continue; + } + + // Record the histograms. + messageLoopStartHistogram.addSample(startEvent.duration, + {events: new tr.v.d.RelatedEventSet([startEvent])}); + firstContentfulPaintHistogram.addSample( + fcpEvent.end - startEvent.start, + {events: new tr.v.d.RelatedEventSet([startEvent, fcpEvent])}); + } + } + + tr.metrics.MetricRegistry.register(androidStartupMetric); + + return { + androidStartupMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric_test.html new file mode 100644 index 00000000000..5ebf601fec6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/android_startup_metric_test.html @@ -0,0 +1,191 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/android_startup_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createBrowserThread(model) { + const browserProcess = model.getOrCreateProcess(tr.b.GUID.allocateSimple()); + const mainThread = browserProcess.getOrCreateThread( + tr.b.GUID.allocateSimple()); + // Initializing the thread name helps passing validation checks made by the + // ChromeModelHelper. + mainThread.name = 'CrBrowserMain'; + return mainThread; + } + + function createRendererThread(model) { + const rendererProcess = model.getOrCreateProcess( + tr.b.GUID.allocateSimple()); + const rendererMainThread = + rendererProcess.getOrCreateThread(tr.b.GUID.allocateSimple()); + rendererMainThread.name = 'CrRendererMain'; + return rendererMainThread; + } + + // Adds a browser and renderer to the process, with a few key events necessary + // to calculate the |androidStartupMetric|. An |offset| can be added to all + // events and the length of a few events can be extended by + // |incrementForMetrics|. + function fillModelWithOneBrowserSession(model, offset, incrementForMetrics) { + // In order for the tests below to succeed with strictEqual, the floating + // point values should have exact representation as IEEE754 float. + createBrowserThread(model).asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceEx({ + cat: 'startup', + title: 'Startup.BrowserMessageLoopStartTimeFromMainEntry3', + start: (offset + 6800.125), + duration: (incrementForMetrics + 1700.0625)})); + const rendererMainThread = createRendererThread(model); + rendererMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: (offset + 8400.125 + incrementForMetrics), + duration: 0.0, + args: {frame: '0x0'}})); + + // Add an extra FCP event in the same renderer process appearing after the + // initial FCP even to check that it is ignored by metric computations. + rendererMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: (offset + 8400.125 + incrementForMetrics + 0.125), + duration: 0.0, + args: {frame: '0x0'}})); + } + + // Adds early messageloop and FCP events. The metric should ignore these very + // first messageloop start and FCP events in the trace. The specific lengths + // are not important. + function addEarlyEventsToBeIgnored(model, offset) { + createBrowserThread(model).asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceEx({ + cat: 'startup', + title: 'Startup.BrowserMessageLoopStartTimeFromMainEntry3', + start: (offset + 1.0), + duration: 10.0})); + createRendererThread(model).sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: (offset + 2.0), + duration: 1.0, + args: {frame: '0x0'}})); + } + + function makeTestModel(offset, incrementForMetrics) { + return tr.c.TestUtils.newModel(function(model) { + fillModelWithOneBrowserSession(model, offset, incrementForMetrics); + addEarlyEventsToBeIgnored(model, offset); + addEarlyEventsToBeIgnored(model, offset + 20.0); + }); + } + + // Checks recording of the main histograms in the simplest case. + test('androidStartupMetric_simple', function() { + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.androidStartupMetric(histograms, makeTestModel(0.0, 0.0)); + const messageLoopStartHistogram = histograms.getHistogramNamed( + 'messageloop_start_time'); + assert.strictEqual(1, messageLoopStartHistogram.numValues); + assert.strictEqual(1700.0625, messageLoopStartHistogram.average); + const firstContentfulPaintHistogram = histograms.getHistogramNamed( + 'first_contentful_paint_time'); + assert.strictEqual(1, firstContentfulPaintHistogram.numValues); + assert.strictEqual(1600.0, firstContentfulPaintHistogram.average); + }); + + // Emulates loss of the initial message loop start event. Checks that this + // event is ignored and the |androidStartupMetric| does not crash. + test('androidStartupMetric_missingOneBrowserStart', function() { + function makeTestModelWithOneEventMissing() { + return tr.c.TestUtils.newModel(function(model) { + fillModelWithOneBrowserSession(model, 0.0, 0.0); + // Note: the initial Startup.BrowserMessageLoopStartTimeFromMainEntry3' + // is intentionally missing. + createRendererThread(model).sliceGroup.pushSlice( + tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: 2.0, + duration: 1.0, + args: {frame: '0x0'}})); + createBrowserThread(model).asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceEx({ + cat: 'startup', + title: 'Startup.BrowserMessageLoopStartTimeFromMainEntry3', + start: (20.0 + 1.0), + duration: 10.0})); + createRendererThread(model).sliceGroup.pushSlice( + tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: (20.0 + 2.0), + duration: 1.0, + args: {frame: '0x0'}})); + }); + } + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.androidStartupMetric(histograms, + makeTestModelWithOneEventMissing(0.0)); + const messageLoopStartHistogram = histograms.getHistogramNamed( + 'messageloop_start_time'); + assert.strictEqual(1, messageLoopStartHistogram.numValues); + assert.strictEqual(1700.0625, messageLoopStartHistogram.average); + const firstContentfulPaintHistogram = histograms.getHistogramNamed( + 'first_contentful_paint_time'); + assert.strictEqual(1, firstContentfulPaintHistogram.numValues); + assert.strictEqual(1600.0, firstContentfulPaintHistogram.average); + }); + + // Checks the metrics after adding an offset to events in the model, and + // making a few durations longer by a constant. + test('androidStartupMetric_withOffsetAndLongerTask', function() { + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.androidStartupMetric(histograms, makeTestModel(5.0, 7.0)); + const messageLoopStartHistogram = histograms.getHistogramNamed( + 'messageloop_start_time'); + assert.strictEqual(1, messageLoopStartHistogram.numValues); + assert.strictEqual(1707.0625, messageLoopStartHistogram.average); + const firstContentfulPaintHistogram = histograms.getHistogramNamed( + 'first_contentful_paint_time'); + assert.strictEqual(1, firstContentfulPaintHistogram.numValues); + assert.strictEqual(1607.0, firstContentfulPaintHistogram.average); + }); + + test('androidStartupMetric_twoSessions', function() { + function makeTestModelWithTwoSessionsOneDelayed( + offset, incrementForMetrics) { + return tr.c.TestUtils.newModel(function(model) { + fillModelWithOneBrowserSession(model, 0.0, 0.0); + fillModelWithOneBrowserSession(model, offset, incrementForMetrics); + addEarlyEventsToBeIgnored(model, 0.0, 0.0); + addEarlyEventsToBeIgnored(model, 0.0, 1.0); + }); + } + const delta = 0.125; + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.androidStartupMetric(histograms, + makeTestModelWithTwoSessionsOneDelayed(10000.0, delta)); + const messageLoopStartHistogram = histograms.getHistogramNamed( + 'messageloop_start_time'); + assert.strictEqual(2, messageLoopStartHistogram.numValues); + assert.strictEqual(1700.0625, messageLoopStartHistogram.min); + assert.strictEqual(1700.0625 + delta, messageLoopStartHistogram.max); + const firstContentfulPaintHistogram = histograms.getHistogramNamed( + 'first_contentful_paint_time'); + assert.strictEqual(2, firstContentfulPaintHistogram.numValues); + assert.strictEqual(1600.0, firstContentfulPaintHistogram.min); + assert.strictEqual(1600.0 + delta, firstContentfulPaintHistogram.max); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric.html new file mode 100644 index 00000000000..96c1ca0295c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric.html @@ -0,0 +1,224 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + const MAX_INPUT_EVENT_TO_STARTUP_DELAY_IN_MS = 2000; + // Post-startup activity draw delay. + const MIN_DRAW_DELAY_IN_MS = 80; + const MAX_DRAW_DELAY_IN_MS = 2000; + + function findProcess(processName, model) { + for (const pid in model.processes) { + const process = model.processes[pid]; + if (process.name === processName) { + return process; + } + } + return undefined; + } + + function findThreads(process, threadPrefix) { + if (process === undefined) return undefined; + const threads = []; + for (const tid in process.threads) { + const thread = process.threads[tid]; + if (thread.name.startsWith(threadPrefix)) { + threads.push(thread); + } + } + return threads; + } + + function findUIThread(process) { + if (process === undefined) return undefined; + const threads = findThreads(process, 'UI Thread'); + if (threads !== undefined && threads.length === 1) { + return threads[0]; + } + return process.threads[process.pid]; + } + + // Returns slices with actual app's process startup, excluding other delays. + function findLaunchSlices(model) { + const launches = []; + const binders = findThreads(findProcess('system_server', model), 'Binder'); + for (const binderId in binders) { + const binder = binders[binderId]; + for (const sliceId in binder.asyncSliceGroup.slices) { + const slice = binder.asyncSliceGroup.slices[sliceId]; + if (slice.title.startsWith('launching:')) { + launches.push(slice); + } + } + } + return launches; + } + + // Try to find draw event when activity just shown. + function findDrawSlice(appName, startNotBefore, model) { + let drawSlice = undefined; + const thread = findUIThread(findProcess(appName, model)); + if (thread === undefined) return undefined; + + for (const sliceId in thread.sliceGroup.slices) { + const slice = thread.sliceGroup.slices[sliceId]; + if (slice.start < startNotBefore + MIN_DRAW_DELAY_IN_MS || + slice.start > startNotBefore + MAX_DRAW_DELAY_IN_MS) continue; + if (slice.title !== 'draw') continue; + // TODO(kraynov): Add reportFullyDrawn() support. + if (drawSlice === undefined || slice.start < drawSlice.start) { + drawSlice = slice; + } + } + return drawSlice; + } + + // Try to find input event before a process starts. + function findInputEventSlice(endNotAfter, model) { + const endNotBefore = endNotAfter - MAX_INPUT_EVENT_TO_STARTUP_DELAY_IN_MS; + let inputSlice = undefined; + const systemUi = findUIThread(findProcess('com.android.systemui', model)); + if (systemUi === undefined) return undefined; + + for (const sliceId in systemUi.asyncSliceGroup.slices) { + const slice = systemUi.asyncSliceGroup.slices[sliceId]; + if (slice.end > endNotAfter || slice.end < endNotBefore) continue; + if (slice.title !== 'deliverInputEvent') continue; + if (inputSlice === undefined || slice.end > inputSlice.end) { + inputSlice = slice; + } + } + return inputSlice; + } + + function computeStartupTimeInMs(appName, launchSlice, model) { + let startupStart = launchSlice.start; + let startupEnd = launchSlice.end; + const drawSlice = findDrawSlice(appName, launchSlice.end, model); + if (drawSlice !== undefined) { + startupEnd = drawSlice.end; + } + const inputSlice = findInputEventSlice(launchSlice.start, model); + if (inputSlice !== undefined) { + startupStart = inputSlice.start; + } + return startupEnd - startupStart; + } + + // App startup time metric. + function measureStartup(histograms, model) { + const launches = findLaunchSlices(model); + for (const sliceId in launches) { + const launchSlice = launches[sliceId]; + const appName = launchSlice.title.split(': ')[1]; + const startupMs = computeStartupTimeInMs(appName, launchSlice, model); + histograms.createHistogram(`android:systrace:startup:${appName}`, + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, startupMs); + } + } + + // Metric which measures time spent by process threads in each thread state. + // The value of metric is a time percentage relative to the length of selected + // range of interest. + function measureThreadStates(histograms, model, rangeOfInterest) { + for (const pid in model.processes) { + const process = model.processes[pid]; + if (process.name === undefined) continue; + + let hasSlices = false; + let timeRunning = 0; + let timeRunnable = 0; + let timeSleeping = 0; + let timeUninterruptible = 0; + let timeBlockIO = 0; + let timeUnknown = 0; + + for (const tid in process.threads) { + const thread = process.threads[tid]; + if (thread.timeSlices === undefined) continue; + + for (const sliceId in thread.timeSlices) { + const slice = thread.timeSlices[sliceId]; + const sliceRange = + tr.b.math.Range.fromExplicitRange(slice.start, slice.end); + const intersection = rangeOfInterest.findIntersection(sliceRange); + const duration = intersection.duration; + if (duration === 0) continue; + hasSlices = true; + + if (slice.title === 'Running') { + timeRunning += duration; + } else if (slice.title === 'Runnable') { + timeRunnable += duration; + } else if (slice.title === 'Sleeping') { + timeSleeping += duration; + } else if (slice.title.startsWith('Uninterruptible')) { + timeUninterruptible += duration; + if (slice.title.includes('Block I/O')) timeBlockIO += duration; + } else { + timeUnknown += duration; + } + } + } + + if (hasSlices) { + // For sake of simplicity we don't count wall time for each + // thread/process and just calculate relative values against selected + // range of interest. + const wall = rangeOfInterest.max - rangeOfInterest.min; + histograms.createHistogram( + `android:systrace:threadtime:${process.name}:running`, + tr.b.Unit.byName.normalizedPercentage, timeRunning / wall); + histograms.createHistogram( + `android:systrace:threadtime:${process.name}:runnable`, + tr.b.Unit.byName.normalizedPercentage, timeRunnable / wall); + histograms.createHistogram( + `android:systrace:threadtime:${process.name}:sleeping`, + tr.b.Unit.byName.normalizedPercentage, timeSleeping / wall); + histograms.createHistogram( + `android:systrace:threadtime:${process.name}:blockio`, + tr.b.Unit.byName.normalizedPercentage, timeBlockIO / wall); + histograms.createHistogram( + `android:systrace:threadtime:${process.name}:uninterruptible`, + tr.b.Unit.byName.normalizedPercentage, timeUninterruptible / wall); + + // In case of changing names in systrace and importer. + if (timeUnknown > 0) { + histograms.createHistogram( + `android:systrace:threadtime:${process.name}:unknown`, + tr.b.Unit.byName.normalizedPercentage, timeUnknown / wall); + } + } + } + } + + function androidSystraceMetric(histograms, model, options) { + let rangeOfInterest = model.bounds; + if (options !== undefined && options.rangeOfInterest !== undefined) { + rangeOfInterest = options.rangeOfInterest; + } + + measureStartup(histograms, model); + measureThreadStates(histograms, model, rangeOfInterest); + } + + tr.metrics.MetricRegistry.register(androidSystraceMetric, { + supportsRangeOfInterest: true + }); + + return { + androidSystraceMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric_test.html new file mode 100644 index 00000000000..76936416503 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/android_systrace_metric_test.html @@ -0,0 +1,146 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/android_systrace_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const SYSTRACE_CLOCK_SYNC = + '<100>-100 (-----) [000] ...1 0.000000: tracing_mark_write: ' + + 'trace_event_clock_sync: parent_ts=0\n' + + '<100>-100 (-----) [000] ...1 0.000000: tracing_mark_write: ' + + 'trace_event_clock_sync: realtime_ts=1487002000000'; + + // Some event is required to help importer detect a parent process. + const SYSTRACE_SYSTEM_SERVER_ANNOTATION = + 'system_server-101 ( 101) [000] ...1 0.550000: ' + + 'tracing_mark_write: S|101|dummyEvent|201\n' + + 'system_server-101 ( 101) [000] ...1 0.551000: ' + + 'tracing_mark_write: F|101|dummyEvent|201'; + + const SYSTRACE_TOUCH_SLICE = + 'com.android.systemui-102 ( 102) [000] ...1 2.500000: ' + + 'tracing_mark_write: S|102|deliverInputEvent|202\n' + + 'com.android.systemui-102 ( 102) [000] ...1 2.510000: ' + + 'tracing_mark_write: F|102|deliverInputEvent|202'; + + const SYSTRACE_LAUNCH_SLICE = + 'Binder:101_C-103 ( 101) [000] ...1 2.750000: ' + + 'tracing_mark_write: S|101|launching: com.android.apps.sms|203\n' + + 'android.display-104 ( 101) [000] ...1 4.250000: ' + + 'tracing_mark_write: F|101|launching: com.android.apps.sms|203'; + + const SYSTRACE_DRAW_SLICE = + 'com.android.apps.sms-105 ( 105) [000] ...1 4.450000: ' + + 'tracing_mark_write: B|105|draw\n' + + 'com.android.apps.sms-105 ( 105) [000] ...1 4.455000: ' + + 'tracing_mark_write: E'; + + function makeModel(systraceLines) { + const events = JSON.stringify({ + traceEvents: [], + systemTraceEvents: SYSTRACE_CLOCK_SYNC + '\n' + systraceLines.join('\n') + }); + const model = tr.c.TestUtils.newModelWithEvents([events]); + // Fix missing process names. + for (const pid in model.processes) { + const process = model.processes[pid]; + if (process.name !== undefined) continue; + if (process.threads[pid] !== undefined) { + process.name = process.threads[pid].name; + } + } + return model; + } + + function testStartup(systrace, expectedTimeInMs) { + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.androidSystraceMetric(histograms, makeModel(systrace)); + assert.lengthOf(histograms, 1); + const startupHist = histograms.getHistogramNamed( + 'android:systrace:startup:com.android.apps.sms').running; + assert.strictEqual(startupHist.count, 1); + assert.closeTo(startupHist.mean, expectedTimeInMs, 1e-5); + } + + test('androidSystraceMetric_startup_noData', function() { + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.androidSystraceMetric(histograms, makeModel([])); + assert.lengthOf(histograms, 0); + }); + + test('androidSystraceMetric_startup_simple', function() { + const systrace = [ + SYSTRACE_SYSTEM_SERVER_ANNOTATION, + SYSTRACE_TOUCH_SLICE, + SYSTRACE_LAUNCH_SLICE, + SYSTRACE_DRAW_SLICE + ]; + testStartup(systrace, 1955); + }); + + test('androidSystraceMetric_startup_noTouch', function() { + const systrace = [ + SYSTRACE_SYSTEM_SERVER_ANNOTATION, + SYSTRACE_LAUNCH_SLICE, + SYSTRACE_DRAW_SLICE + ]; + testStartup(systrace, 1705); + }); + + test('androidSystraceMetric_startup_noDraw', function() { + const systrace = [ + SYSTRACE_SYSTEM_SERVER_ANNOTATION, + SYSTRACE_TOUCH_SLICE, + SYSTRACE_LAUNCH_SLICE, + ]; + testStartup(systrace, 1750); + }); + + test('androidSystraceMetric_startup_noTouchNoDraw', function() { + const systrace = [ + SYSTRACE_SYSTEM_SERVER_ANNOTATION, + SYSTRACE_LAUNCH_SLICE, + ]; + testStartup(systrace, 1500); + }); + + test('androidSystraceMetric_threadtime_simple', function() { + const model = tr.c.TestUtils.newModel(model => { + const process = model.getOrCreateProcess(42); + process.name = 'garbage_producer'; + const thread = process.getOrCreateThread(42); + thread.timeSlices = [ + tr.c.TestUtils.newThreadSlice(thread, 'Sleeping', 0, 100), + tr.c.TestUtils.newThreadSlice(thread, 'Running', 100, 400) + ]; + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.androidSystraceMetric(histograms, model); + assert.lengthOf(histograms, 5); + + const assertHistValue = function(name, expectedValue) { + const hist = histograms.getHistogramNamed( + `android:systrace:threadtime:garbage_producer:${name}`); + assert.strictEqual(hist.running.count, 1); + assert.closeTo(hist.running.mean, expectedValue, 1e-5); + }; + assertHistValue('running', 0.8); + assertHistValue('runnable', 0); + assertHistValue('sleeping', 0.2); + assertHistValue('blockio', 0); + assertHistValue('uninterruptible', 0); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric.html new file mode 100644 index 00000000000..641c8bd0c9f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric.html @@ -0,0 +1,265 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/v8/utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.blink', function() { + // Maps the Blink GC events in timeline to telemetry friendly names. + const BLINK_TOP_GC_EVENTS = { + 'BlinkGC.AtomicPhase': 'blink-gc-atomic-phase', + 'BlinkGC.CompleteSweep': 'blink-gc-complete-sweep', + 'BlinkGC.IncrementalMarkingStartMarking': 'blink-gc-incremental-start', + 'BlinkGC.IncrementalMarkingStep': 'blink-gc-incremental-step', + 'BlinkGC.LazySweepInIdle': 'blink-gc-lazy-sweep-idle', + 'BlinkGC.LazySweepOnAllocation': 'blink-gc-lazy-sweep-allocation' + }; + + function blinkGarbageCollectionEventName(event) { + return BLINK_TOP_GC_EVENTS[event.title]; + } + + function isNonForcedBlinkGarbageCollectionEvent(event) { + return event.title in BLINK_TOP_GC_EVENTS && + (!event.args || !event.args.forced) && + !tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event); + } + + function isNonNestedNonForcedBlinkGarbageCollectionEvent(event) { + return isNonForcedBlinkGarbageCollectionEvent(event) && + !tr.metrics.v8.utils.findParent(event, + tr.metrics.v8.utils.isGarbageCollectionEvent); + } + + function blinkGcMetric(histograms, model) { + addDurationOfTopEvents(histograms, model); + addTotalDurationOfTopEvents(histograms, model); + addIdleTimesOfTopEvents(histograms, model); + addTotalIdleTimesOfTopEvents(histograms, model); + addTotalDurationOfBlinkAndV8TopEvents(histograms, model); + } + + tr.metrics.MetricRegistry.register(blinkGcMetric); + + const timeDurationInMs_smallerIsBetter = + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter; + const percentage_biggerIsBetter = + tr.b.Unit.byName.normalizedPercentage_biggerIsBetter; + + // 0.1 steps from 0 to 20 since it is the most common range. + // Exponentially increasing steps from 20 to 200. + const CUSTOM_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear(0, 20, 200) + .addExponentialBins(200, 100); + + function createNumericForTopEventTime(name) { + const n = new tr.v.Histogram(name, + timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + n.customizeSummaryOptions({ + avg: true, + count: true, + max: true, + min: false, + std: true, + sum: true, + percentile: [0.90]}); + return n; + } + + function createNumericForTotalEventTime(name) { + const n = new tr.v.Histogram(name, + timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + n.customizeSummaryOptions({ + avg: false, + count: true, + max: false, + min: false, + std: false, + sum: true, + percentile: [0.90]}); + return n; + } + + function createNumericForUnifiedEventTime(name) { + const n = new tr.v.Histogram(name, + timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + n.customizeSummaryOptions({ + avg: false, + count: true, + max: true, + min: false, + std: false, + sum: true, + percentile: [0.90]}); + return n; + } + + function createNumericForIdleTime(name) { + const n = new tr.v.Histogram(name, + timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + n.customizeSummaryOptions({ + avg: true, + count: false, + max: true, + min: false, + std: false, + sum: true, + percentile: [] + }); + return n; + } + + function createPercentage(name, numerator, denominator) { + const histogram = new tr.v.Histogram(name, percentage_biggerIsBetter); + if (denominator === 0) { + histogram.addSample(0); + } else { + histogram.addSample(numerator / denominator); + } + return histogram; + } + + /** + * Example output: + * - blink-gc-atomic-phase + */ + function addDurationOfTopEvents(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + isNonForcedBlinkGarbageCollectionEvent, + blinkGarbageCollectionEventName, + function(name, events) { + const cpuDuration = createNumericForTopEventTime(name); + for (const event of events) { + cpuDuration.addSample(event.cpuDuration); + } + histograms.addHistogram(cpuDuration); + } + ); + } + + /** + * Example output: + * - blink-gc-total_sum + */ + function addTotalDurationOfTopEvents(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + isNonForcedBlinkGarbageCollectionEvent, + event => 'blink-gc-total', + function(name, events) { + const cpuDuration = createNumericForTotalEventTime(name); + for (const event of events) { + cpuDuration.addSample(event.cpuDuration); + } + histograms.addHistogram(cpuDuration); + } + ); + } + + /** + * Example output: + * - blink-gc-atomic-phase_idle_deadline_overrun, + * - blink-gc-atomic-phase_outside_idle, + * - blink-gc-atomic-phase_percentage_idle. + */ + function addIdleTimesOfTopEvents(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + isNonForcedBlinkGarbageCollectionEvent, + blinkGarbageCollectionEventName, + function(name, events) { + addIdleTimes(histograms, model, name, events); + } + ); + } + + /** + * Example output: + * - blink-gc-total_idle_deadline_overrun, + * - blink-gc-total_outside_idle, + * - blink-gc-total_percentage_idle. + */ + function addTotalIdleTimesOfTopEvents(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + isNonForcedBlinkGarbageCollectionEvent, + event => 'blink-gc-total', + function(name, events) { + addIdleTimes(histograms, model, name, events); + } + ); + } + + function addIdleTimes(histograms, model, name, events) { + const cpuDuration = createNumericForIdleTime(name + '_cpu'); + const insideIdle = createNumericForIdleTime(name + '_inside_idle'); + const outsideIdle = createNumericForIdleTime(name + '_outside_idle'); + const idleDeadlineOverrun = createNumericForIdleTime( + name + '_idle_deadline_overrun'); + for (const event of events) { + const idleTask = tr.metrics.v8.utils.findParent( + event, tr.metrics.v8.utils.isIdleTask); + let inside = 0; + let overrun = 0; + if (idleTask) { + const allottedTime = idleTask.args.allotted_time_ms; + if (event.duration > allottedTime) { + overrun = event.duration - allottedTime; + // Don't count time over the deadline as being inside idle time. + // Since the deadline should be relative to wall clock we + // compare allotted_time_ms with wall duration instead of thread + // duration, and then assume the thread duration was inside idle + // for the same percentage of time. + inside = event.cpuDuration * allottedTime / event.duration; + } else { + inside = event.cpuDuration; + } + } + cpuDuration.addSample(event.cpuDuration); + insideIdle.addSample(inside); + outsideIdle.addSample(event.cpuDuration - inside); + idleDeadlineOverrun.addSample(overrun); + } + histograms.addHistogram(idleDeadlineOverrun); + histograms.addHistogram(outsideIdle); + const percentage = createPercentage( + name + '_percentage_idle', insideIdle.sum, cpuDuration.sum); + histograms.addHistogram(percentage); + } + + function isV8OrBlinkTopLevelGarbageCollectionEvent(event) { + return tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent(event) || + isNonNestedNonForcedBlinkGarbageCollectionEvent(event); + } + + /** + * Example output: + * - unified-gc-total_sum + * - unified-gc-total_max + * - unified-gc-total_count + */ + function addTotalDurationOfBlinkAndV8TopEvents(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + isV8OrBlinkTopLevelGarbageCollectionEvent, + event => 'unified-gc-total', + function(name, events) { + const cpuDuration = createNumericForUnifiedEventTime(name); + for (const event of events) { + cpuDuration.addSample(event.cpuDuration); + } + histograms.addHistogram(cpuDuration); + } + ); + } + + return { + blinkGcMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric_test.html new file mode 100644 index 00000000000..924527f126b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/blink/gc_metric_test.html @@ -0,0 +1,347 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/blink/gc_metric.html"> +<link rel="import" href="/tracing/metrics/v8/utils.html"> +<link rel="import" href="/tracing/model/slice_group.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createModel(start, end, slices) { + const opts = { + customizeModelCallback(model) { + const process = model.getOrCreateProcess(1); + const thread = process.getOrCreateThread(2); + const group = thread.sliceGroup; + slices.forEach(function(slice) { + group.pushSlice(tr.c.TestUtils.newSliceEx(slice)); + }); + group.createSubSlices(); + } + }; + const model = tr.c.TestUtils.newModelWithEvents([], opts); + return model; + } + + function constructName(name, suffix) { + return name + '_' + suffix; + } + + function run(slices) { + const histograms = new tr.v.HistogramSet(); + const startTime = slices.reduce( + (acc, slice) => (Math.min(acc, slice.start))); + const endTime = slices.reduce((acc, slice) => (Math.max(acc, slice.end))); + const model = createModel(startTime - 1, endTime + 1, slices); + tr.metrics.blink.blinkGcMetric(histograms, model); + return histograms; + } + + test('topEvents', function() { + const events = { + 'BlinkGC.AtomicPhase': 'blink-gc-atomic-phase', + 'BlinkGC.CompleteSweep': 'blink-gc-complete-sweep', + 'BlinkGC.LazySweepInIdle': 'blink-gc-lazy-sweep-idle' + }; + for (const [timelineName, telemetryName] of Object.entries(events)) { + const slices = [ + { + title: timelineName, args: {}, start: 100, end: 200, + cpuStart: 100, cpuEnd: 200 + } + ]; + const actual = run(slices); + + let value = actual.getHistogramNamed(telemetryName); + assert.strictEqual(value.running.sum, 100); + assert.strictEqual(value.numValues, 1); + assert.strictEqual(value.average, 100); + assert.strictEqual(value.running.max, 100); + assert.closeTo(value.getApproximatePercentile(0.90), 100, 1); + + value = actual.getHistogramNamed( + `${telemetryName}_idle_deadline_overrun`); + assert.strictEqual(value.running.sum, 0); + assert.strictEqual(value.numValues, 1); + assert.strictEqual(value.average, 0); + assert.strictEqual(value.running.max, 0); + + value = actual.getHistogramNamed(`${telemetryName}_outside_idle`); + assert.strictEqual(value.running.sum, 100); + assert.strictEqual(value.numValues, 1); + assert.strictEqual(value.average, 100); + + value = actual.getHistogramNamed(`${telemetryName}_percentage_idle`); + assert.strictEqual(value.average, 0); + } + }); + + test('idleTimes', function() { + const histograms = new tr.v.HistogramSet(); + const slices = [ + { + title: 'SingleThreadIdleTaskRunner::RunTask', + args: {'allotted_time_ms': 100}, start: 100, end: 200, + cpuStart: 100, cpuEnd: 200 + }, + { + title: 'BlinkGC.AtomicPhase', args: {}, start: 110, end: 190, + cpuStart: 110, cpuEnd: 190 + } + ]; + const actual = run(slices); + + let value = actual.getHistogramNamed('blink-gc-atomic-phase'); + assert.strictEqual(value.running.sum, 80); + assert.strictEqual(value.numValues, 1); + assert.strictEqual(value.average, 80); + assert.strictEqual(value.running.max, 80); + + value = actual.getHistogramNamed( + 'blink-gc-atomic-phase_idle_deadline_overrun'); + assert.strictEqual(value.running.sum, 0); + assert.strictEqual(value.average, 0); + assert.strictEqual(value.running.max, 0); + + value = actual.getHistogramNamed('blink-gc-atomic-phase_outside_idle'); + assert.strictEqual(value.running.sum, 0); + assert.strictEqual(value.average, 0); + assert.strictEqual(value.running.max, 0); + + value = actual.getHistogramNamed('blink-gc-atomic-phase_percentage_idle'); + assert.strictEqual(value.average, 1); + }); + + test('idleTimeOverrun', function() { + const histograms = new tr.v.HistogramSet(); + const slices = [ + { + title: 'SingleThreadIdleTaskRunner::RunTask', + args: {'allotted_time_ms': 10}, start: 100, end: 200, + cpuStart: 100, cpuEnd: 200 + }, + { + title: 'BlinkGC.AtomicPhase', args: {}, start: 110, end: 190, + cpuStart: 110, cpuEnd: 190 + } + ]; + const actual = run(slices); + + let value = actual.getHistogramNamed('blink-gc-atomic-phase'); + assert.strictEqual(value.running.sum, 80); + assert.strictEqual(value.numValues, 1); + assert.strictEqual(value.average, 80); + assert.strictEqual(value.running.max, 80); + + value = actual.getHistogramNamed( + 'blink-gc-atomic-phase_idle_deadline_overrun'); + assert.strictEqual(value.running.sum, 70); + assert.strictEqual(value.average, 70); + assert.strictEqual(value.running.max, 70); + + value = actual.getHistogramNamed('blink-gc-atomic-phase_outside_idle'); + assert.strictEqual(value.running.sum, 70); + assert.strictEqual(value.average, 70); + assert.strictEqual(value.running.max, 70); + + value = actual.getHistogramNamed('blink-gc-atomic-phase_percentage_idle'); + assert.closeTo(value.average, 1 / 8, 1e-6); + }); + + test('totalTimeForBlinkGC', function() { + const histograms = new tr.v.HistogramSet(); + const slices = [ + { + title: 'BlinkGC.AtomicPhase', args: {}, start: 100, end: 200, + cpuStart: 100, cpuEnd: 200 + }, + { + title: 'BlinkGC.LazySweepInIdle', args: {}, start: 210, + end: 290, cpuStart: 210, cpuEnd: 290 + } + ]; + const actual = run(slices); + + let value = actual.getHistogramNamed('blink-gc-total'); + assert.strictEqual(value.running.sum, 180); + assert.strictEqual(value.numValues, 2); + assert.strictEqual(value.average, 90); + assert.strictEqual(value.running.max, 100); + + value = actual.getHistogramNamed('blink-gc-total_idle_deadline_overrun'); + assert.strictEqual(value.running.sum, 0); + assert.strictEqual(value.average, 0); + assert.strictEqual(value.running.max, 0); + + value = actual.getHistogramNamed('blink-gc-total_outside_idle'); + assert.strictEqual(value.running.sum, 180); + assert.strictEqual(value.average, 90); + assert.strictEqual(value.running.max, 100); + + value = actual.getHistogramNamed('blink-gc-total_percentage_idle'); + assert.strictEqual(value.average, 0); + }); + + test('totalTimeForUnifiedGC', function() { + const histograms = new tr.v.HistogramSet(); + const slices = [ + { + title: 'V8.GCFinalizeMC', args: {}, + start: 100, end: 300, cpuStart: 100, cpuEnd: 300 + }, + { + title: 'BlinkGC.AtomicPhase', args: {}, + start: 310, end: 410, cpuStart: 310, cpuEnd: 410 + } + ]; + const actual = run(slices); + + const value = actual.getHistogramNamed('unified-gc-total'); + assert.strictEqual(value.running.sum, 300); + assert.strictEqual(value.numValues, 2); + assert.strictEqual(value.average, 150); + assert.strictEqual(value.running.max, 200); + }); + + test('totalTimeForUnifiedGCBlinkNestedInV8', function() { + // Nested Blink GC in V8 top-level GC can happen during unified garbage + // collection, or when callbacks that trigger e.g. sweeping are fired + // from V8's GC. These should only be accounted once. + const histograms = new tr.v.HistogramSet(); + const slices = [ + { + title: 'V8.GCFinalizeMC', args: {}, + start: 100, end: 300, cpuStart: 100, cpuEnd: 300 + }, + // Nested events should be ignored. + { + title: 'BlinkGC.CompleteSweep', args: {}, + start: 200, end: 270, cpuStart: 200, cpuEnd: 270 + }, + { + title: 'BlinkGC.IncrementalMarkingStartMarking', args: {}, + start: 280, end: 290, cpuStart: 280, cpuEnd: 290 + }, + // Next event is outside of nesting and should be accounted for. + { + title: 'BlinkGC.IncrementalMarkingStartMarking', args: {}, + start: 310, end: 320, cpuStart: 310, cpuEnd: 320 + }, + ]; + const actual = run(slices); + + const value = actual.getHistogramNamed('unified-gc-total'); + assert.strictEqual(value.running.sum, 210); + assert.strictEqual(value.numValues, 2); + assert.strictEqual(value.average, 105); + assert.strictEqual(value.running.max, 200); + }); + + function getSlicesWithForcedV8GCs() { + return [ + { + title: tr.metrics.v8.utils.forcedGCEventName(), args: {}, + start: 100, end: 300, cpuStart: 100, cpuEnd: 300 + }, + // Following nested events should be ignored. + { + title: 'V8.GCFinalizeMC', args: {}, + start: 100, end: 300, cpuStart: 100, cpuEnd: 300 + }, + { + title: 'BlinkGC.CompleteSweep', args: {}, + start: 200, end: 270, cpuStart: 200, cpuEnd: 270 + }, + { + title: 'BlinkGC.IncrementalMarkingStartMarking', args: {}, + start: 280, end: 290, cpuStart: 280, cpuEnd: 290 + }, + // Next event happens after the forced GC and should be accounted for. + { + title: 'BlinkGC.IncrementalMarkingStartMarking', args: {}, + start: 310, end: 320, cpuStart: 310, cpuEnd: 320 + }, + { + title: 'BlinkGC.AtomicPhase', args: {'forced': false}, + start: 320, end: 330, cpuStart: 320, cpuEnd: 330 + }, + ]; + } + + test('ignoreForcedV8GCEventsForUnifiedMetric', function() { + // Any events nested in a forced GC should be ignored. + const histograms = new tr.v.HistogramSet(); + const actual = run(getSlicesWithForcedV8GCs()); + const value = actual.getHistogramNamed('unified-gc-total'); + assert.strictEqual(value.running.sum, 20); + assert.strictEqual(value.numValues, 2); + assert.strictEqual(value.average, 10); + assert.strictEqual(value.running.max, 10); + }); + + test('ignoreForcedV8GCEventsForBlinkMetric', function() { + // Any events nested in a forced GC should be ignored. + const histograms = new tr.v.HistogramSet(); + const actual = run(getSlicesWithForcedV8GCs()); + const value = actual.getHistogramNamed('blink-gc-total'); + assert.strictEqual(value.running.sum, 20); + assert.strictEqual(value.numValues, 2); + assert.strictEqual(value.average, 10); + assert.strictEqual(value.running.max, 10); + }); + + function getSlicesWithForcedBlinkGCs() { + return [ + // Following nested events should be ignored. + { + title: 'BlinkGC.CompleteSweep', args: {'forced': true}, + start: 200, end: 270, cpuStart: 200, cpuEnd: 270 + }, + { + title: 'BlinkGC.AtomicPhase', args: {'forced': true}, + start: 280, end: 290, cpuStart: 280, cpuEnd: 290 + }, + // Next events are not forced and should be accounted for. + { + title: 'BlinkGC.AtomicPhase', args: {}, + start: 310, end: 320, cpuStart: 310, cpuEnd: 320 + }, + { + title: 'BlinkGC.AtomicPhase', args: {'forced': false}, + start: 320, end: 330, cpuStart: 320, cpuEnd: 330 + }, + ]; + } + + test('ignoreForcedBlinkGCEventsForUnifiedMetric', function() { + // Any forced Blink GC events should be ignored. + const histograms = new tr.v.HistogramSet(); + const actual = run(getSlicesWithForcedBlinkGCs()); + const value = actual.getHistogramNamed('unified-gc-total'); + assert.strictEqual(value.running.sum, 20); + assert.strictEqual(value.numValues, 2); + assert.strictEqual(value.average, 10); + assert.strictEqual(value.running.max, 10); + }); + + test('ignoreForcedBlinkGCEventsForBlinkMetric', function() { + // Any forced Blink GC events should be ignored. + const histograms = new tr.v.HistogramSet(); + const actual = run(getSlicesWithForcedBlinkGCs()); + const value = actual.getHistogramNamed('blink-gc-total'); + assert.strictEqual(value.running.sum, 20); + assert.strictEqual(value.numValues, 2); + assert.strictEqual(value.average, 10); + assert.strictEqual(value.running.max, 10); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric.html new file mode 100644 index 00000000000..432b41d5d73 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/base/unit.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/system_health/utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.blink', function() { + function leakDetectionMetric(histograms, model) { + // Extract renderer pids. + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (modelHelper === undefined) { + throw new Error('Chrome is not present.'); + } + const rendererHelpers = modelHelper.rendererHelpers; + if (Object.keys(rendererHelpers).length === 0) { + throw new Error('Renderer process is not present.'); + } + const pids = Object.keys(rendererHelpers); + + // Get the dumps. + const chromeDumps = tr.metrics.sh + .splitGlobalDumpsByBrowserName(model, undefined).get('chrome'); + + const sumCounter = new Map(); + // Add up counters for all the renderer processes. + + for (const pid of pids) { + for (const [key, count] of countLeakedBlinkObjects(chromeDumps, pid)) { + sumCounter.set(key, (sumCounter.get(key) || 0) + count); + } + } + + for (const [key, count] of sumCounter) { + histograms.createHistogram('Leaked ' + key, + tr.b.Unit.byName.count_smallerIsBetter, count); + } + + for (const [key, count] of sumCounter) { + if (count > 0) { + throw new Error('Memory leak is found.'); + } + } + } + + tr.metrics.MetricRegistry.register(leakDetectionMetric); + + function countLeakedBlinkObjects(dumps, pid) { + if (dumps === undefined || dumps.length < 2) { + throw new Error('Expected at least two memory dumps.'); + } + const firstCounter = countBlinkObjects(dumps[0], pid); + const lastCounter = countBlinkObjects(dumps[dumps.length - 1], pid); + const diffCounter = new Map(); + for (const [key, lastCount] of lastCounter) { + diffCounter.set(key, lastCount - firstCounter.get(key)); + } + return diffCounter; + } + + function countBlinkObjects(dump, pid) { + const counter = new Map(); + const processesMemoryDumps = dump.processMemoryDumps; + if (processesMemoryDumps[pid] === undefined) return counter; + const blinkObjectsDump = processesMemoryDumps[pid].memoryAllocatorDumps + .find(dump => dump.fullName === 'blink_objects'); + for (const v of blinkObjectsDump.children) { + counter.set(v.name, v.numerics.object_count.value); + } + return counter; + } + + return { + leakDetectionMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric_test.html new file mode 100644 index 00000000000..f5b2e4add39 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/blink/leak_detection_metric_test.html @@ -0,0 +1,179 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/blink/leak_detection_metric.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const BLINK_OBJECT_LIST = ['AudioHandler', 'Document', 'Frame', + 'JSEventListener', 'LayoutObject', 'MediaKeys', 'MediaKeySession', 'Node', + 'Resource', 'ScriptPromise', 'PausableObject', 'V8PerContextData', + 'WorkerGlobalScope']; + const addProcessMemoryDump = + tr.model.MemoryDumpTestUtils.addProcessMemoryDump; + const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump; + const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump; + const allZeroArray = new Array(BLINK_OBJECT_LIST.length).fill(0); + const oneLeakArray = new Array(BLINK_OBJECT_LIST.length).fill(0); + oneLeakArray[1] = 1; + const multipleLeaksArray = new Array(BLINK_OBJECT_LIST.length).fill(1); + + function createProcessWithName(model, name) { + const uniquePid = + Math.max.apply(null, Object.keys(model.processes).concat([0])) + 1; + const process = model.getOrCreateProcess(uniquePid); + process.name = name; + process.getOrCreateThread(1).name = 'Cr' + name + 'Main'; + return process; + } + + function createTimestamp(model, browser, rendererValuePairs, timestamp) { + const gmd1 = addGlobalMemoryDump(model, {ts: timestamp}); + const pmdBrowser1 = addProcessMemoryDump(gmd1, browser, {ts: timestamp}); + for (const pair of rendererValuePairs) { + addDumpsToRenderer(gmd1, pair.renderer, pair.values, timestamp); + } + } + + function addDumpsToRenderer(gmd, renderer, values, timestamp) { + const pmdRenderer = addProcessMemoryDump(gmd, renderer, {ts: timestamp}); + pmdRenderer.memoryAllocatorDumps = [ + newAllocatorDump(pmdRenderer, 'blink_objects', { children: + createBlinkObjectCountList(pmdRenderer, values)})]; + } + + function getNumericLeakCount(histograms, index) { + return histograms.getHistogramNamed('Leaked ' + + BLINK_OBJECT_LIST[index]).statisticsScalars.get('sum').value; + } + + function createBlinkObjectCountList(renderer, values) { + const blinkObjectCountList = []; + for (let i = 0; i < values.length; i++) { + blinkObjectCountList.push(newAllocatorDump(renderer, + 'blink_objects/' + BLINK_OBJECT_LIST[i], { numerics: + { object_count: values[i] }})); + } + return blinkObjectCountList; + } + + test('testNoRenderer', function() { + const model = tr.c.TestUtils.newModel(function(model) { + createProcessWithName(model, 'Browser'); + }); + const histograms = new tr.v.HistogramSet(); + assert.throws( + function() {tr.metrics.blink.leakDetectionMetric(histograms, model);}, + 'Renderer process is not present.'); + }); + + test('testZeroDump', function() { + const model = tr.c.TestUtils.newModel(function(model) { + createProcessWithName(model, 'Browser'); + createProcessWithName(model, 'Renderer'); + }); + const histograms = new tr.v.HistogramSet(); + assert.throws( + function() {tr.metrics.blink.leakDetectionMetric(histograms, model);}, + 'Expected at least two memory dumps.'); + }); + + test('testOnlyOneDump', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const browser = createProcessWithName(model, 'Browser'); + const renderer = createProcessWithName(model, 'Renderer'); + const pair = [{renderer, values: allZeroArray}]; + createTimestamp(model, browser, pair, 40); + }); + const histograms = new tr.v.HistogramSet(); + assert.throws( + function() {tr.metrics.blink.leakDetectionMetric(histograms, model);}, + 'Expected at least two memory dumps.'); + }); + + test('testNoLeak', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const browser = createProcessWithName(model, 'Browser'); + const renderer = createProcessWithName(model, 'Renderer'); + const pair = [{renderer, 'values': allZeroArray}]; + createTimestamp(model, browser, pair, 20); + createTimestamp(model, browser, pair, 40); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.blink.leakDetectionMetric(histograms, model); + for (let i = 0; i < BLINK_OBJECT_LIST.length; i++) { + assert.strictEqual(getNumericLeakCount(histograms, i), 0); + } + }); + + test('testOneLeakDetection', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const browser = createProcessWithName(model, 'Browser'); + const renderer = createProcessWithName(model, 'Renderer'); + const pair1 = [{renderer, 'values': allZeroArray}]; + const pair2 = [{renderer, 'values': oneLeakArray}]; + createTimestamp(model, browser, pair1, 20); + createTimestamp(model, browser, pair2, 40); + }); + const histograms = new tr.v.HistogramSet(); + assert.throws( + function() {tr.metrics.blink.leakDetectionMetric(histograms, model);}, + 'Memory leak is found.'); + for (let i = 0; i < BLINK_OBJECT_LIST.length; i++) { + if (i === 1) { + assert.strictEqual(getNumericLeakCount(histograms, i), 1); + } else { + assert.strictEqual(getNumericLeakCount(histograms, i), 0); + } + } + }); + + test('testMultipleLeakDetections', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const browser = createProcessWithName(model, 'Browser'); + const renderer = createProcessWithName(model, 'Renderer'); + const pair1 = [{renderer, 'values': allZeroArray}]; + const pair2 = [{renderer, 'values': multipleLeaksArray}]; + createTimestamp(model, browser, pair1, 20); + createTimestamp(model, browser, pair2, 40); + }); + const histograms = new tr.v.HistogramSet(); + assert.throws( + function() {tr.metrics.blink.leakDetectionMetric(histograms, model);}, + 'Memory leak is found.'); + for (let i = 0; i < BLINK_OBJECT_LIST.length; i++) { + assert.strictEqual(getNumericLeakCount(histograms, i), 1); + } + }); + + test('testMultipleRendererWithLeaks', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const browser = createProcessWithName(model, 'Browser'); + const renderer1 = createProcessWithName(model, 'Renderer'); + const renderer2 = createProcessWithName(model, 'Renderer'); + const pair1 = [{'renderer': renderer1, 'values': allZeroArray}, + {'renderer': renderer2, 'values': allZeroArray}]; + const pair2 = [{'renderer': renderer1, 'values': multipleLeaksArray}, + {'renderer': renderer2, 'values': multipleLeaksArray}]; + createTimestamp(model, browser, pair1, 20); + createTimestamp(model, browser, pair2, 40); + }); + const histograms = new tr.v.HistogramSet(); + assert.throws( + function() {tr.metrics.blink.leakDetectionMetric(histograms, model);}, + 'Memory leak is found.'); + for (let i = 0; i < BLINK_OBJECT_LIST.length; i++) { + assert.strictEqual(getNumericLeakCount(histograms, i), 2); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/buildbot_output_for_compare_samples_test.txt b/chromium/third_party/catapult/tracing/tracing/metrics/buildbot_output_for_compare_samples_test.txt new file mode 100644 index 00000000000..a8d19745dae --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/buildbot_output_for_compare_samples_test.txt @@ -0,0 +1,187 @@ +IMPORTANT DEBUGGING NOTE: batches of tests are run inside their +own process. For debugging a test inside a debugger, use the +--gtest_filter=<your_test_name> flag along with +--single-process-tests. +Using sharding settings from environment. This is shard 0/1 +Using 1 parallel jobs. +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D31.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_13273\test_results.xml" --test-launcher-print-test-stdio=always +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D31.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_13273\test_results.xml" --test-launcher-print-test-stdio=always +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D31.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_13273\test_results.xml" --test-launcher-print-test-stdio=always +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D31.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_13273\test_results.xml" --test-launcher-print-test-stdio=always +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D31.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_13273\test_results.xml" --test-launcher-print-test-stdio=always +[ RUN ] IndexDataManagerPerfTest.Run +*RESULT IndexDataManger_run: score= 281471 score +[ OK ] IndexDataManagerPerfTest.Run (5008 ms) +[1/37] IndexDataManagerPerfTest.Run (5008 ms) +[ RUN ] BufferSubDataBenchmark.Run/d3d11_float4_every1 +*RESULT BufferSubData_d3d11_float4_every1: score= 232 score +[ OK ] BufferSubDataBenchmark.Run/d3d11_float4_every1 (5117 ms) +[2/37] BufferSubDataBenchmark.Run/d3d11_float4_every1 (5117 ms) +[ RUN ] BufferSubDataBenchmark.Run/d3d9_float4_every1 +*RESULT BufferSubData_d3d9_float4_every1: score= 237 score +[ OK ] BufferSubDataBenchmark.Run/d3d9_float4_every1 (5386 ms) +[3/37] BufferSubDataBenchmark.Run/d3d9_float4_every1 (5386 ms) +[ RUN ] BufferSubDataBenchmark.Run/gl_float4_every1 +*RESULT BufferSubData_gl_float4_every1: score= 245 score +[ OK ] BufferSubDataBenchmark.Run/gl_float4_every1 (5195 ms) +[4/37] BufferSubDataBenchmark.Run/gl_float4_every1 (5195 ms) +[ RUN ] DrawCallPerfBenchmark.Run/d3d9 +*RESULT DrawCallPerf_d3d9: score= 2993 score +[ OK ] DrawCallPerfBenchmark.Run/d3d9 (10062 ms) +[5/37] DrawCallPerfBenchmark.Run/d3d9 (10062 ms) +[ RUN ] DrawCallPerfBenchmark.Run/d3d9_null +*RESULT DrawCallPerf_d3d9_null: score= 25046 score +[ OK ] DrawCallPerfBenchmark.Run/d3d9_null (10047 ms) +[6/37] DrawCallPerfBenchmark.Run/d3d9_null (10047 ms) +[ RUN ] DrawCallPerfBenchmark.Run/d3d11 +*RESULT DrawCallPerf_d3d11: score= 2741 score +[ OK ] DrawCallPerfBenchmark.Run/d3d11 (10015 ms) +[7/37] DrawCallPerfBenchmark.Run/d3d11 (10015 ms) +[ RUN ] DrawCallPerfBenchmark.Run/d3d11_null +*RESULT DrawCallPerf_d3d11_null: score= 28607 score +[ OK ] DrawCallPerfBenchmark.Run/d3d11_null (9999 ms) +[8/37] DrawCallPerfBenchmark.Run/d3d11_null (9999 ms) +[ RUN ] DrawCallPerfBenchmark.Run/d3d11_render_to_texture_null +*RESULT DrawCallPerf_d3d11_render_to_texture_null: score= 25868 score +[ OK ] DrawCallPerfBenchmark.Run/d3d11_render_to_texture_null (10015 ms) +[9/37] DrawCallPerfBenchmark.Run/d3d11_render_to_texture_null (10015 ms) +[ RUN ] DrawCallPerfBenchmark.Run/gl +*RESULT DrawCallPerf_gl: score= 4123 score +[ OK ] DrawCallPerfBenchmark.Run/gl (10031 ms) +[10/37] DrawCallPerfBenchmark.Run/gl (10031 ms) +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D32.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_8457\test_results.xml" --test-launcher-print-test-stdio=always +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D32.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_8457\test_results.xml" --test-launcher-print-test-stdio=always +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D32.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_8457\test_results.xml" --test-launcher-print-test-stdio=always +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D32.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_8457\test_results.xml" --test-launcher-print-test-stdio=always +[ RUN ] DrawCallPerfBenchmark.Run/gl_null +*RESULT DrawCallPerf_gl_null: score= 189804 score +[ OK ] DrawCallPerfBenchmark.Run/gl_null (10015 ms) +[11/37] DrawCallPerfBenchmark.Run/gl_null (10015 ms) +[ RUN ] DrawCallPerfBenchmark.Run/gl_render_to_texture_null +*RESULT DrawCallPerf_gl_render_to_texture_null: score= 189155 score +[ OK ] DrawCallPerfBenchmark.Run/gl_render_to_texture_null (10031 ms) +[12/37] DrawCallPerfBenchmark.Run/gl_render_to_texture_null (10031 ms) +[ RUN ] DrawCallPerfBenchmark.Run/default_validation_only +*RESULT DrawCallPerf_default_validation_only: score= 2690 score +[ OK ] DrawCallPerfBenchmark.Run/default_validation_only (5023 ms) +[13/37] DrawCallPerfBenchmark.Run/default_validation_only (5023 ms) +[ RUN ] DynamicPromotionPerfTest.Run/d3d11 +*RESULT DynamicPromotion_d3d11: score= 39354 score +[ OK ] DynamicPromotionPerfTest.Run/d3d11 (6552 ms) +[14/37] DynamicPromotionPerfTest.Run/d3d11 (6552 ms) +[ RUN ] DynamicPromotionPerfTest.Run/d3d9 +*RESULT DynamicPromotion_d3d9: score= 21060 score +[ OK ] DynamicPromotionPerfTest.Run/d3d9 (5522 ms) +[15/37] DynamicPromotionPerfTest.Run/d3d9 (5522 ms) +[ RUN ] EGLInitializePerfTest.Run/ES2_D3D11 +*RESULT EGLInitialize_run: score= 155 score +*RESULT EGLInitialize_run: LoadDLLs= 0.0000000000 ms +*RESULT EGLInitialize_run: D3D11CreateDevice= 4.0051480051 ms +*RESULT EGLInitialize_run: InitResources= 0.0000000000 ms +[ OK ] EGLInitializePerfTest.Run/ES2_D3D11 (5008 ms) +[16/37] EGLInitializePerfTest.Run/ES2_D3D11 (5008 ms) +[ RUN ] IndexConversionPerfTest.Run/d3d11 +*RESULT IndexConversionPerfTest_d3d11: score= 5135 score +[ OK ] IndexConversionPerfTest.Run/d3d11 (3166 ms) +[17/37] IndexConversionPerfTest.Run/d3d11 (3166 ms) +[ RUN ] IndexConversionPerfTest.Run/index_range_d3d11 +*RESULT IndexConversionPerfTest_index_range_d3d11: score= 68785 score +[ OK ] IndexConversionPerfTest.Run/index_range_d3d11 (3011 ms) +[18/37] IndexConversionPerfTest.Run/index_range_d3d11 (3011 ms) +[ RUN ] InstancingPerfBenchmark.Run/d3d11 +*RESULT InstancingPerf_d3d11: score= 479 score +[ OK ] InstancingPerfBenchmark.Run/d3d11 (10046 ms) +[19/37] InstancingPerfBenchmark.Run/d3d11 (10046 ms) +[ RUN ] InstancingPerfBenchmark.Run/d3d9 +*RESULT InstancingPerf_d3d9: score= 464 score +[ OK ] InstancingPerfBenchmark.Run/d3d9 (10057 ms) +[20/37] InstancingPerfBenchmark.Run/d3d9 (10057 ms) +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D44.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_12336\test_results.xml" --test-launcher-print-test-stdio=always +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D44.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_12336\test_results.xml" --test-launcher-print-test-stdio=always +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D44.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_12336\test_results.xml" --test-launcher-print-test-stdio=always +[ RUN ] InstancingPerfBenchmark.Run/gl +*RESULT InstancingPerf_gl: score= 427 score +[ OK ] InstancingPerfBenchmark.Run/gl (10124 ms) +[21/37] InstancingPerfBenchmark.Run/gl (10124 ms) +[ RUN ] InterleavedAttributeDataBenchmark.Run/d3d11 +*RESULT InterleavedAttributeData_d3d11: score= 19 score +[ OK ] InterleavedAttributeDataBenchmark.Run/d3d11 (5117 ms) +[22/37] InterleavedAttributeDataBenchmark.Run/d3d11 (5117 ms) +[ RUN ] InterleavedAttributeDataBenchmark.Run/d3d11_9_3 +*RESULT InterleavedAttributeData_d3d11: score= 25 score +[ OK ] InterleavedAttributeDataBenchmark.Run/d3d11_9_3 (5225 ms) +[23/37] InterleavedAttributeDataBenchmark.Run/d3d11_9_3 (5225 ms) +[ RUN ] InterleavedAttributeDataBenchmark.Run/d3d9 +*RESULT InterleavedAttributeData_d3d9: score= 25 score +[ OK ] InterleavedAttributeDataBenchmark.Run/d3d9 (5211 ms) +[24/37] InterleavedAttributeDataBenchmark.Run/d3d9 (5211 ms) +[ RUN ] InterleavedAttributeDataBenchmark.Run/gl +*RESULT InterleavedAttributeData_gl: score= 25 score +[ OK ] InterleavedAttributeDataBenchmark.Run/gl (5101 ms) +[25/37] InterleavedAttributeDataBenchmark.Run/gl (5101 ms) +[ RUN ] PointSpritesBenchmark.Run/d3d11_10_3px_3vars +*RESULT PointSprites_d3d11_10_3px_3vars: score= 644 score +[ OK ] PointSpritesBenchmark.Run/d3d11_10_3px_3vars (5023 ms) +[26/37] PointSpritesBenchmark.Run/d3d11_10_3px_3vars (5023 ms) +[ RUN ] PointSpritesBenchmark.Run/d3d9_10_3px_3vars +*RESULT PointSprites_d3d9_10_3px_3vars: score= 730 score +[ OK ] PointSpritesBenchmark.Run/d3d9_10_3px_3vars (5141 ms) +[27/37] PointSpritesBenchmark.Run/d3d9_10_3px_3vars (5141 ms) +[ RUN ] PointSpritesBenchmark.Run/gl_10_3px_3vars +*RESULT PointSprites_gl_10_3px_3vars: score= 2159 score +[ OK ] PointSpritesBenchmark.Run/gl_10_3px_3vars (5086 ms) +[28/37] PointSpritesBenchmark.Run/gl_10_3px_3vars (5086 ms) +[ RUN ] TexSubImageBenchmark.Run/d3d11 +*RESULT TexSubImage_d3d11: score= 294 score +[ OK ] TexSubImageBenchmark.Run/d3d11 (5023 ms) +[29/37] TexSubImageBenchmark.Run/d3d11 (5023 ms) +[ RUN ] TexSubImageBenchmark.Run/d3d9 +*RESULT TexSubImage_d3d9: score= 298 score +[ OK ] TexSubImageBenchmark.Run/d3d9 (5305 ms) +[30/37] TexSubImageBenchmark.Run/d3d9 (5305 ms) +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D45.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_4706\test_results.xml" --test-launcher-print-test-stdio=always +Still waiting for the following processes to finish: + "C:\b\c\b\Win_7_ATI_GPU_Perf__2_\src\out\Release_x64\angle_perftests.exe" --gtest_flagfile="C:\Users\chrome-bot\AppData\Local\Temp\scoped_dir3192_23891\8D45.tmp" --single-process-tests --test-launcher-jobs=1 --test-launcher-output="C:\Users\CHROME~1\AppData\Local\Temp\3192_4706\test_results.xml" --test-launcher-print-test-stdio=always +[ RUN ] TexSubImageBenchmark.Run/gl +*RESULT TexSubImage_gl: score= 323 score +[ OK ] TexSubImageBenchmark.Run/gl (5070 ms) +[31/37] TexSubImageBenchmark.Run/gl (5070 ms) +[ RUN ] TextureSamplingBenchmark.Run/d3d11_2samplers +*RESULT TextureSampling_d3d11_2samplers: score= 128 score +[ OK ] TextureSamplingBenchmark.Run/d3d11_2samplers (5070 ms) +[32/37] TextureSamplingBenchmark.Run/d3d11_2samplers (5070 ms) +[ RUN ] TextureSamplingBenchmark.Run/d3d9_2samplers +*RESULT TextureSampling_d3d9_2samplers: score= 128 score +[ OK ] TextureSamplingBenchmark.Run/d3d9_2samplers (5668 ms) +[33/37] TextureSamplingBenchmark.Run/d3d9_2samplers (5668 ms) +[ RUN ] TextureSamplingBenchmark.Run/gl_2samplers +*RESULT TextureSampling_gl_2samplers: score= 136 score +[ OK ] TextureSamplingBenchmark.Run/gl_2samplers (5054 ms) +[34/37] TextureSamplingBenchmark.Run/gl_2samplers (5054 ms) +[ RUN ] UniformsBenchmark.Run/d3d11_200_vertex_uniforms_200_fragment_uniforms +*RESULT Uniforms_d3d11_200_vertex_uniforms_200_fragment_uniforms: score= 1797 score +[ OK ] UniformsBenchmark.Run/d3d11_200_vertex_uniforms_200_fragment_uniforms (5195 ms) +[35/37] UniformsBenchmark.Run/d3d11_200_vertex_uniforms_200_fragment_uniforms (5195 ms) +[ RUN ] UniformsBenchmark.Run/d3d9_200_vertex_uniforms_200_fragment_uniforms +*RESULT Uniforms_d3d9_200_vertex_uniforms_200_fragment_uniforms: score= 1912 score +[ OK ] UniformsBenchmark.Run/d3d9_200_vertex_uniforms_200_fragment_uniforms (5223 ms) +[36/37] UniformsBenchmark.Run/d3d9_200_vertex_uniforms_200_fragment_uniforms (5223 ms) +[ RUN ] UniformsBenchmark.Run/gl_200_vertex_uniforms_200_fragment_uniforms +*RESULT Uniforms_gl_200_vertex_uniforms_200_fragment_uniforms: score= 5509 score +[ OK ] UniformsBenchmark.Run/gl_200_vertex_uniforms_200_fragment_uniforms (5070 ms) +[37/37] UniformsBenchmark.Run/gl_200_vertex_uniforms_200_fragment_uniforms (5070 ms) +SUCCESS: all tests passed. +Tests took 243 seconds. diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples.py b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples.py new file mode 100644 index 00000000000..6fb10a34b6f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples.py @@ -0,0 +1,54 @@ +# Copyright 2016 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 os + +import tracing_project +import vinn + +FORMAT_TO_METHOD = { + 'chartjson': 'compareCharts', + 'buildbot': 'compareBuildbotOutputs' +} + +_COMPARE_SAMPLES_CMD_LINE = os.path.join( + os.path.dirname(__file__), 'compare_samples_cmdline.html') + + +def CompareSamples(sample_a, sample_b, metric, data_format='chartjson'): + """Compare the values of a metric from two samples from benchmark output. + + Args: + sample_a, sample_b (str): comma-separated lists of paths to the benchmark + output. + metric (str): Metric name in slash-separated format [2 or 3 part]. + data_format (str): The format the samples are in. Supported values are: + 'chartjson', 'valueset', 'buildbot'. + Returns: + JSON encoded dict with the values parsed form the samples and the result of + the hypothesis testing comparison of the samples under the 'result' key. + Possible values for the result key are: + 'NEED_MORE_DATA', 'REJECT' and 'FAIL_TO_REJECT'. + Where the null hypothesis is that the samples belong to the same population. + i.e. a 'REJECT' result would make it reasonable to conclude that + there is a significant difference between the samples. (e.g. a perf + regression). + """ + + method = FORMAT_TO_METHOD[data_format] + project = tracing_project.TracingProject() + all_source_paths = list(project.source_paths) + + def MakeAbsPaths(l): + return ','.join(map(os.path.abspath, l.split(','))) + + return vinn.RunFile( + _COMPARE_SAMPLES_CMD_LINE, + source_paths=all_source_paths, + js_args=[ + method, + MakeAbsPaths(sample_a), + MakeAbsPaths(sample_b), + metric + ]) diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_cmdline.html b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_cmdline.html new file mode 100644 index 00000000000..f774f03f1bd --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_cmdline.html @@ -0,0 +1,225 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/base/xhr.html"> + +<script> +'use strict'; +/* eslint-disable no-console */ + +const escapeChars = s => s.replace(/[\:|=\/#&,]/g, '_'); + +function findUnescapedKey(escaped, d) { + if (!d) { + return undefined; + } + + for (const k of Object.keys(d)) { + if (escapeChars(k) === escapeChars(escaped)) { + return k; + } + } +} + +function geoMeanFromHistogram(h) { + if (!h.hasOwnProperty('buckets')) return 0.0; + let count = 0; + let sumOfLogs = 0; + for (const bucket of h.buckets) { + if (bucket.hasOwnProperty('high')) { + bucket.mean = (bucket.low + bucket.high) / 2.0; + } else { + bucket.mean = bucket.low; + } + + if (bucket.mean > 0) { + sumOfLogs += Math.log(bucket.mean) * bucket.count; + count += bucket.count; + } + } + if (count === 0) return 0.0; + return Math.exp(sumOfLogs / count); +} + +function guessFullTIRMetricName(metricName) { + const parts = metricName.split('/'); + if (parts.length === 2) { + return metricName + '/summary'; + } + return undefined; +} + +function splitMetric(metricName) { + const parts = metricName.split('/'); + let interactionName; + let traceName = 'summary'; + let chartName = parts[0]; + if (parts.length === 3) { + // parts[1] is the interactionName + if (parts[1]) chartName = parts[1] + '@@' + chartName; + traceName = parts[2]; + } else if (parts.length === 2) { + if (chartName !== parts[1]) traceName = parts[1]; + } else { + throw new Error('Could not parse metric name.'); + } + return [chartName, traceName]; +} + +function valuesFromCharts(listOfCharts, metricName) { + const allValues = []; + const chartAndTrace = splitMetric(metricName); + for (const charts of listOfCharts) { + const chartName = findUnescapedKey(chartAndTrace[0], charts.charts); + if (chartName) { + const traceName = findUnescapedKey( + chartAndTrace[1], charts.charts[chartName]); + if (traceName) { + if (charts.charts[chartName][traceName].type === + 'list_of_scalar_values') { + if (charts.charts[chartName][traceName].values === null) continue; + allValues.push(tr.b.math.Statistics.mean( + charts.charts[chartName][traceName].values)); + } + if (charts.charts[chartName][traceName].type === 'histogram') { + allValues.push( + geoMeanFromHistogram(charts.charts[chartName][traceName])); + } + if (charts.charts[chartName][traceName].type === 'scalar') { + allValues.push(charts.charts[chartName][traceName].value); + } + } + } + } + return allValues; +} + +function valuesFromChartsWithFallback(listOfCharts, metricName) { + const allValues = valuesFromCharts(listOfCharts, metricName); + if (allValues.length > 0) return allValues; + + // If this had a tir_label, the "summary" part may have been stripped by + // the dashboard during upload. We can re-add it here. + const fullMetricName = guessFullTIRMetricName(metricName); + if (!fullMetricName) return []; + + return valuesFromCharts(listOfCharts, fullMetricName); +} + +function parseFiles(files) { + const results = []; + for (const path of files) { + const current = tr.b.getSync('file://' + path); + results.push(JSON.parse(current)); + } + return results; +} + +const escapeForRegExp = s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + +const strFromRE = re => re.toString().split('/')[1]; + +function valuesFromBuildbotOutput(out, metric) { + if (!out) return []; + + let stringVals = []; + const floatVals = []; + const chartAndTrace = splitMetric(metric); + const metricRE = escapeForRegExp( + 'RESULT ' + chartAndTrace[0] + ': ' + chartAndTrace[1] + '='); + const singleResultRE = new RegExp(metricRE + + strFromRE(/\s*([-]?[\d\.]+)/), 'g'); + const multiResultsRE = new RegExp(metricRE + + strFromRE(/\s*\[\s*([\d\., -]+)\s*\]/), 'g'); + const meanStdDevRE = new RegExp(metricRE + + strFromRE(/\s*\{\s*([-]?\d*(?:\.\d*)?),\s*([-]?\d*(?:\.\d*)?)\}/), 'g'); + for (const line of out.split(/\r?\n/)) { + const singleResultMatch = singleResultRE.exec(line); + const multiResultsMatch = multiResultsRE.exec(line); + const meanStdDevMatch = meanStdDevRE.exec(line); + if (singleResultMatch && singleResultMatch.length > 1) { + stringVals.push(singleResultMatch[1]); + } else if (multiResultsMatch && multiResultsMatch.length > 1) { + const values = multiResultsMatch[1].split(','); + stringVals = stringVals.concat(values); + } else if (meanStdDevMatch && meanStdDevMatch.length > 1) { + stringVals.push(meanStdDevMatch[1]); + } + } + for (const val of stringVals) { + const f = parseFloat(val); + if (!isNaN(f)) floatVals.push(f); + } + return floatVals; +} + +function parseMultipleBuildbotStreams(files, metric) { + let allValues = []; + for (const path of files) { + let contents; + try { + contents = tr.b.getSync('file://' + path); + } catch (ex) { + const err = new Error('Could not open' + path); + err.name = 'File loading error'; + throw err; + } + allValues = allValues.concat(valuesFromBuildbotOutput(contents, metric)); + } + return allValues; +} + +const buildComparisonResultOutput = function(a, b) { + let comparisonResult; + if (!a.length || !b.length) { + comparisonResult = { + significance: tr.b.math.Statistics.Significance.NEED_MORE_DATA + }; + } else { + comparisonResult = tr.b.math.Statistics.mwu( + a, b, tr.b.math.Statistics.DEFAULT_ALPHA, + tr.b.math.Statistics.MAX_SUGGESTED_SAMPLE_SIZE).asDict(); + } + return { + sampleA: a, + sampleB: b, + result: comparisonResult + }; +}; + +const SampleComparison = { + + compareBuildbotOutputs( + buildbotOutputAPathList, buildbotOutputBPathList, metric) { + const aPaths = buildbotOutputAPathList.split(','); + const bPaths = buildbotOutputBPathList.split(','); + const sampleA = parseMultipleBuildbotStreams(aPaths, metric); + const sampleB = parseMultipleBuildbotStreams(bPaths, metric); + return buildComparisonResultOutput(sampleA, sampleB); + }, + + compareCharts(chartPathListA, chartPathListB, metric) { + const aPaths = chartPathListA.split(','); + const bPaths = chartPathListB.split(','); + const chartsA = parseFiles(aPaths); + const chartsB = parseFiles(bPaths); + const sampleA = valuesFromChartsWithFallback(chartsA, metric); + const sampleB = valuesFromChartsWithFallback(chartsB, metric); + return buildComparisonResultOutput(sampleA, sampleB); + } + +}; + +if (tr.isHeadless) { + const [method, ...rest] = sys.argv.slice(1); + if (SampleComparison[method]) { + console.log(JSON.stringify(SampleComparison[method](...rest))); + } +} +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_unittest.py b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_unittest.py new file mode 100644 index 00000000000..df58e8ba8ce --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/compare_samples_unittest.py @@ -0,0 +1,336 @@ +# Copyright 2016 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. + +from __future__ import print_function + +import json +import math +import os +import random +import tempfile +import unittest + +from tracing.metrics import compare_samples + + +REJECT = 'REJECT' +FAIL_TO_REJECT = 'FAIL_TO_REJECT' +NEED_MORE_DATA = 'NEED_MORE_DATA' + + +def Mean(l): + if len(l): + return float(sum(l))/len(l) + return 0 + + +class CompareSamplesUnittest(unittest.TestCase): + def setUp(self): + self._tempfiles = [] + self._tempdir = tempfile.mkdtemp() + + def tearDown(self): + for tf in self._tempfiles: + try: + os.remove(tf) + except OSError: + pass + try: + os.rmdir(self._tempdir) + except OSError: + pass + + def NewJsonTempfile(self, jsonable_contents): + f_handle, new_json_file = tempfile.mkstemp( + suffix='.json', + dir=self._tempdir, + text=True) + os.close(f_handle) + self._tempfiles.append(new_json_file) + with open(new_json_file, 'w') as f: + json.dump(jsonable_contents, f) + return new_json_file + + def MakeMultipleChartJSONHistograms(self, metric, seed, mu, sigma, n, m): + result = [] + random.seed(seed) + for _ in range(m): + result.append(self.MakeChartJSONHistogram(metric, mu, sigma, n)) + return result + + def MakeChartJSONHistogram(self, metric, mu, sigma, n): + """Creates a histogram for a normally distributed pseudo-random sample. + + This function creates a deterministic pseudo-random sample and stores it in + chartjson histogram format to facilitate the testing of the sample + comparison logic. + + For simplicity we use sqrt(n) buckets with equal widths. + + Args: + metric (str pair): name of chart, name of the trace. + seed (hashable obj): to make the sequences deterministic we seed the RNG. + mu (float): desired mean for the sample + sigma (float): desired standard deviation for the sample + n (int): number of values to generate. + """ + chart_name, trace_name = metric + values = [random.gauss(mu, sigma) for _ in range(n)] + bucket_count = int(math.ceil(math.sqrt(len(values)))) + width = (max(values) - min(values))/(bucket_count - 1) + prev_bucket = min(values) + buckets = [] + for _ in range(bucket_count): + buckets.append({'low': prev_bucket, + 'high': prev_bucket + width, + 'count': 0}) + prev_bucket += width + for value in values: + for bucket in buckets: + if value >= bucket['low'] and value < bucket['high']: + bucket['count'] += 1 + break + charts = { + 'charts': { + chart_name: { + trace_name: { + 'type': 'histogram', + 'buckets': buckets + } + } + } + } + return self.NewJsonTempfile(charts) + + def MakeChart(self, metric, seed, mu, sigma, n, keys=None): + """Creates a normally distributed pseudo-random sample. (continuous). + + This function creates a deterministic pseudo-random sample and stores it in + chartjson format to facilitate the testing of the sample comparison logic. + + Args: + metric (str pair): name of chart, name of the trace. + seed (hashable obj): to make the sequences deterministic we seed the RNG. + mu (float): desired mean for the sample + sigma (float): desired standard deviation for the sample + n (int): number of values to generate. + """ + chart_name, trace_name = metric + random.seed(seed) + values = [random.gauss(mu, sigma) for _ in range(n)] + charts = { + 'charts': { + chart_name: { + trace_name: { + 'type': 'list_of_scalar_values', + 'values': values} + } + } + } + if keys: + grouping_keys = dict(enumerate(keys)) + charts['charts'][chart_name][trace_name]['grouping_keys'] = grouping_keys + return self.NewJsonTempfile(charts) + + def MakeNoneValuesChart(self, metric, keys=None): + """Creates a chart with merged None values. + + Args: + metric (str pair): name of chart, name of the trace. + """ + chart_name, trace_name = metric + charts = { + 'charts': { + chart_name: { + trace_name: { + 'type': 'list_of_scalar_values', + 'values': None + } + } + } + } + if keys: + grouping_keys = dict(enumerate(keys)) + charts['charts'][chart_name][trace_name]['grouping_keys'] = grouping_keys + return self.NewJsonTempfile(charts) + + def MakeCharts(self, metric, seed, mu, sigma, n, keys=None): + return [ + self.MakeChartJSONScalar(metric, seed + '%d' % i, mu, sigma, keys) + for i in range(n)] + + def MakeChartJSONScalar(self, metric, seed, mu, sigma, keys=None): + """Creates a normally distributed pseudo-random sample. (continuous). + + This function creates a deterministic pseudo-random sample and stores it in + chartjson format to facilitate the testing of the sample comparison logic. + + Args: + metric (str pair): name of chart, name of the trace. + seed (hashable obj): to make the sequences deterministic we seed the RNG. + mu (float): desired mean for the sample + sigma (float): desired standard deviation for the sample + """ + chart_name, trace_name = metric + random.seed(seed) + charts = { + 'charts': { + chart_name: { + trace_name: { + 'type': 'scalar', + 'value': random.gauss(mu, sigma)} + } + } + } + if keys: + grouping_keys = dict(enumerate(keys)) + charts['charts'][chart_name][trace_name]['grouping_keys'] = grouping_keys + return self.NewJsonTempfile(charts) + + def testCompareClearRegressionListOfScalars(self): + metric = ('some_chart', 'some_trace') + lower_values = ','.join(self.MakeCharts(metric=metric, seed='lower', + mu=10, sigma=1, n=10)) + higher_values = ','.join(self.MakeCharts(metric=metric, seed='higher', + mu=20, sigma=2, n=10)) + result = json.loads(compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(metric)).stdout) + self.assertEqual(result['result']['significance'], REJECT) + + def testCompareListOfScalarsWithNoneValue(self): + metric = ('some_chart', 'some_trace') + lower_values = ','.join(self.MakeCharts(metric=metric, seed='lower', + mu=10, sigma=1, n=10)) + lower_values += ',' + self.MakeNoneValuesChart(metric=metric) + higher_values = ','.join(self.MakeCharts(metric=metric, seed='higher', + mu=20, sigma=2, n=10)) + result = json.loads(compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(metric)).stdout) + self.assertEqual(result['result']['significance'], REJECT) + + def testCompareClearRegressionScalars(self): + metric = ('some_chart', 'some_trace') + lower_values = ','.join( + [self.MakeChartJSONScalar( + metric=metric, seed='lower', mu=10, sigma=1) for _ in range(10)]) + higher_values = ','.join( + [self.MakeChartJSONScalar( + metric=metric, seed='higher', mu=20, sigma=2) for _ in range(10)]) + result = json.loads(compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(metric)).stdout) + self.assertEqual(result['result']['significance'], REJECT) + + def testCompareUnlikelyRegressionWithMultipleRuns(self): + metric = ('some_chart', 'some_trace') + lower_values = ','.join( + self.MakeCharts( + metric=metric, seed='lower', mu=10, sigma=1, n=20)) + higher_values = ','.join( + self.MakeCharts( + metric=metric, seed='higher', mu=10.01, sigma=0.95, n=20)) + result = json.loads(compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(metric)).stdout) + self.assertEqual(result['result']['significance'], FAIL_TO_REJECT) + + def testCompareTIRLabel(self): + tir_metric = ('some_chart', 'some_label', 'some_trace') + tir_metric_name = ('%s@@%s' % (tir_metric[1], tir_metric[0]), tir_metric[2]) + lower_values = ','.join(self.MakeCharts( + metric=tir_metric_name, seed='lower', mu=10, sigma=1, n=10)) + higher_values = ','.join(self.MakeCharts( + metric=tir_metric_name, seed='higher', mu=20, sigma=2, n=10)) + result = json.loads(compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(tir_metric)).stdout) + self.assertEqual(result['result']['significance'], REJECT) + + def testCompareTIRLabelMissingSummary(self): + tir_metric = ('some_chart', 'some_label') + tir_metric_name = ('%s@@%s' % (tir_metric[1], tir_metric[0]), 'summary') + lower_values = ','.join(self.MakeCharts( + metric=tir_metric_name, seed='lower', mu=10, sigma=1, n=10)) + higher_values = ','.join(self.MakeCharts( + metric=tir_metric_name, seed='higher', mu=20, sigma=2, n=10)) + result = json.loads(compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(tir_metric)).stdout) + self.assertEqual(result['result']['significance'], REJECT) + + def testCompareInsufficientData(self): + metric = ('some_chart', 'some_trace') + lower_values = ','.join([self.MakeChart(metric=metric, seed='lower', + mu=10, sigma=1, n=5)]) + higher_values = ','.join([self.MakeChart(metric=metric, seed='higher', + mu=10.40, sigma=0.95, n=5)]) + result = json.loads(compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(metric)).stdout) + self.assertEqual(result['result']['significance'], NEED_MORE_DATA) + + def testCompareMissingFile(self): + metric = ('some_chart', 'some_trace') + lower_values = ','.join([self.MakeChart(metric=metric, seed='lower', + mu=10, sigma=1, n=5)]) + higher_values = '/path/does/not/exist.json' + with self.assertRaises(RuntimeError): + compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(metric)) + + def testCompareMissingMetric(self): + metric = ('some_chart', 'some_trace') + lower_values = ','.join([self.MakeChart(metric=metric, seed='lower', + mu=10, sigma=1, n=5)]) + higher_values = ','.join([self.MakeChart(metric=metric, seed='higher', + mu=20, sigma=2, n=5)]) + metric = ('some_chart', 'missing_trace') + result = json.loads(compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(metric)).stdout) + self.assertEqual(result['result']['significance'], NEED_MORE_DATA) + + def testCompareBadChart(self): + metric = ('some_chart', 'some_trace') + lower_values = ','.join([self.MakeChart(metric=metric, seed='lower', + mu=10, sigma=1, n=5)]) + higher_values = self.NewJsonTempfile(['obviously', 'not', 'a', 'chart]']) + result = json.loads(compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(metric)).stdout) + self.assertEqual(result['result']['significance'], NEED_MORE_DATA) + + def testCompareBuildbotOutput(self): + bb = os.path.join(os.path.dirname(__file__), + 'buildbot_output_for_compare_samples_test.txt') + result = compare_samples.CompareSamples( + bb, bb, 'DrawCallPerf_gl/score', + data_format='buildbot') + result = json.loads(result.stdout) + self.assertEqual(result['result']['significance'], NEED_MORE_DATA) + self.assertEqual(Mean(result['sampleA']), 4123) + self.assertEqual(Mean(result['sampleB']), 4123) + + def testCompareChartJsonHistogram(self): + metric = ('some_chart', 'some_trace') + lower_values = ','.join(self.MakeMultipleChartJSONHistograms( + metric=metric, seed='lower', mu=10, sigma=1, n=100, m=10)) + higher_values = ','.join(self.MakeMultipleChartJSONHistograms( + metric=metric, seed='higher', mu=20, sigma=2, n=100, m=10)) + result = json.loads(compare_samples.CompareSamples( + lower_values, higher_values, '/'.join(metric)).stdout) + self.assertEqual(result['result']['significance'], REJECT) + + def testParseComplexMetricName(self): + full_metric_name = ('memory:chrome:all_processes:reported_by_os:' + 'system_memory:native_heap:' + 'proportional_resident_size_avg/blank_about/' + 'blank_about_blank') + chart_name = ('blank_about@@memory:chrome:all_processes:reported_by_os:' + 'system_memory:native_heap:proportional_resident_size_avg') + trace_name = 'blank:about:blank' + metric = chart_name, trace_name + keys = 'blank', 'about' + lower_values = ','.join(self.MakeCharts(metric=metric, seed='lower', + mu=10, sigma=1, n=10, keys=keys)) + higher_values = ','.join(self.MakeCharts(metric=metric, seed='higher', + mu=20, sigma=2, n=10, keys=keys)) + result = compare_samples.CompareSamples( + lower_values, higher_values, full_metric_name).stdout + print(result) + result = json.loads(result) + self.assertEqual(result['result']['significance'], REJECT) diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric.html new file mode 100644 index 00000000000..5ed27d0f6be --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/base/unit.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.console', function() { + const COUNT_BOUNDARIES = tr.v.HistogramBinBoundaries.createExponential( + 1, 1e4, 30); + // We store a single value, so we only need one of the statistics to keep + // track. We choose the average for that. + const SUMMARY_OPTIONS = tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS; + + // Sources of console error message that we are interested in. + const SOURCES = ['all', 'js', 'network']; + + /** + * This metric counts slices named 'ConsoleErrorMessage' and adds: + * - console:error:all - all console error messages. + * - console:error:js - console error messages coming from JS. + * - console:error:network - console error messages coming from network. + * If a console error message does not come from JS or network, it is still + * accounted in the console_error_all. + */ + function consoleErrorMetric(histograms, model) { + const counts = {}; + for (const source of SOURCES) { + counts[source] = 0; + } + for (const slice of model.getDescendantEvents()) { + if (slice.category === 'blink.console' && + slice.title === 'ConsoleMessage::Error') { + const source = slice.args.source.toLowerCase(); + counts.all++; + if (source in counts) { + counts[source]++; + } + } + if (slice.category === 'v8.console' && ( + slice.title === 'V8ConsoleMessage::Exception' || + slice.title === 'V8ConsoleMessage::Error' || + slice.title === 'V8ConsoleMessage::Assert')) { + counts.all++; + counts.js++; + } + } + for (const source of SOURCES) { + histograms.createHistogram( + `console:error:${source}`, + tr.b.Unit.byName.count_smallerIsBetter, + counts[source], { + description: `Number of ${source} console error messages`, + summaryOptions: SUMMARY_OPTIONS + }); + } + } + + tr.metrics.MetricRegistry.register(consoleErrorMetric); + + return { + consoleErrorMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric_unittest.html b/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric_unittest.html new file mode 100644 index 00000000000..eff2917fc47 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/console_error_metric_unittest.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/console_error_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function makeEvent(cat, name, source, timestamp) { + return { + cat, + name, + args: {source}, + ts: timestamp, + pid: 52, + tid: 53, + ph: 'B' + }; + } + + test('consoleErrorMetric_noErrors', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + makeEvent('foo', '', 10), + makeEvent('bar', '', 20) + ]; + tr.metrics.console.consoleErrorMetric(histograms, + tr.c.TestUtils.newModelWithEvents([events])); + const all = histograms.getHistogramNamed('console:error:all').running; + assert.strictEqual(all.count, 1); + assert.strictEqual(all.mean, 0); + const js = histograms.getHistogramNamed('console:error:js').running; + assert.strictEqual(js.count, 1); + assert.strictEqual(js.mean, 0); + const net = histograms.getHistogramNamed('console:error:network').running; + assert.strictEqual(net.count, 1); + assert.strictEqual(net.mean, 0); + }); + + test('consoleErrorMetric_Errors', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + makeEvent('blink.console', 'foo', '', 10), + makeEvent('blink.console', 'ConsoleMessage::Error', 'XML', 20), + makeEvent('blink.console', 'bar', '', 30), + makeEvent('blink.console', 'ConsoleMessage::Error', 'Network', 40), + makeEvent('blink.console', 'ConsoleMessage::Error', 'JS', 50), + makeEvent('blink.console', 'ConsoleMessage::Error', 'Network', 60), + makeEvent('blink.console', 'ConsoleMessage::Error', 'Network', 70), + makeEvent('blink.console', 'ConsoleMessage::Error', 'JS', 80), + makeEvent('blink.console', 'ConsoleMessage::Error', 'Network', 90), + makeEvent('bar', '', 300) + ]; + tr.metrics.console.consoleErrorMetric(histograms, + tr.c.TestUtils.newModelWithEvents([events])); + const all = histograms.getHistogramNamed('console:error:all').running; + assert.strictEqual(all.count, 1); + assert.strictEqual(all.mean, 7); + const js = histograms.getHistogramNamed('console:error:js').running; + assert.strictEqual(js.count, 1); + assert.strictEqual(js.mean, 2); + const net = histograms.getHistogramNamed('console:error:network').running; + assert.strictEqual(net.count, 1); + assert.strictEqual(net.mean, 4); + }); + + test('consoleErrorMetric_V8', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + makeEvent('v8.console', 'foo', '', 10), + makeEvent('v8.console', 'V8ConsoleMessage::Error', '', 20), + makeEvent('v8.console', 'bar', '', 30), + makeEvent('v8.console', 'V8ConsoleMessage::Exception', '', 40), + makeEvent('v8.console', 'V8ConsoleMessage::Assert', '', 50), + makeEvent('v8.console', 'V8ConsoleMessage::Ignore', '', 60), + makeEvent('v8.console', 'V8ConsoleMessage::Ignore', '', 70), + makeEvent('v8.console', 'bar', '', 300) + ]; + tr.metrics.console.consoleErrorMetric(histograms, + tr.c.TestUtils.newModelWithEvents([events])); + const all = histograms.getHistogramNamed('console:error:all').running; + assert.strictEqual(all.count, 1); + assert.strictEqual(all.mean, 3); + const js = histograms.getHistogramNamed('console:error:js').running; + assert.strictEqual(js.count, 1); + assert.strictEqual(js.mean, 3); + const net = histograms.getHistogramNamed('console:error:network').running; + assert.strictEqual(net.count, 1); + assert.strictEqual(net.mean, 0); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric.html new file mode 100644 index 00000000000..c858a8bcb2d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + function getCpuSnapshotsFromModel(model) { + const snapshots = []; + for (const pid in model.processes) { + const snapshotInstances = + model.processes[pid].objects.getAllInstancesNamed('CPUSnapshots'); + if (!snapshotInstances) continue; + + for (const object of snapshotInstances[0].snapshots) { + snapshots.push(object.args.processes); + } + } + return snapshots; + } + + function getProcessSumsFromSnapshot(snapshot) { + const processSums = new Map(); + for (const processData of snapshot) { + const processName = processData.name; + if (!(processSums.has(processName))) { + processSums.set(processName, {sum: 0.0, paths: new Set()}); + } + processSums.get(processName).sum += parseFloat(processData.pCpu); + // The process path may be missing on Windows because of AccessDenied + // error thrown by psutil package used by CPU tracing agent. + if (processData.path) { + processSums.get(processName).paths.add(processData.path); + } + } + return processSums; + } + + function buildNumericsFromSnapshots(snapshots) { + const processNumerics = new Map(); + for (const snapshot of snapshots) { + const processSums = getProcessSumsFromSnapshot(snapshot); + for (const [processName, processData] of processSums.entries()) { + if (!(processNumerics.has(processName))) { + processNumerics.set(processName, { + numeric: new tr.v.Histogram('cpu:percent:' + processName, + tr.b.Unit.byName.normalizedPercentage_smallerIsBetter), + paths: new Set() + }); + } + processNumerics.get(processName).numeric.addSample( + processData.sum / 100.0); + for (const path of processData.paths) { + processNumerics.get(processName).paths.add(path); + } + } + } + return processNumerics; + } + + function cpuProcessMetric(histograms, model) { + const snapshots = getCpuSnapshotsFromModel(model); + const processNumerics = buildNumericsFromSnapshots(snapshots); + for (const [processName, processData] of processNumerics) { + const numeric = processData.numeric; + // Treat missing snapshots as zeros. A process is missing from a snapshots + // when its CPU usage was below minimum threshold when the snapshot was + // taken. + const missingSnapshotCount = snapshots.length - numeric.numValues; + for (let i = 0; i < missingSnapshotCount; i++) { + numeric.addSample(0); + } + numeric.diagnostics.set('paths', new + tr.v.d.GenericSet([...processData.paths])); + histograms.addHistogram(numeric); + } + } + + tr.metrics.MetricRegistry.register(cpuProcessMetric); + + return { + cpuProcessMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric_test.html new file mode 100644 index 00000000000..55e404c5b4d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/cpu_process_metric_test.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/cpu_process_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function makeModel(events) { + return tr.c.TestUtils.newModelWithEvents([events]); + } + + test('cpuProcessMetric_noData', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'} + ]; + tr.metrics.sh.cpuProcessMetric(histograms, makeModel(events)); + assert.lengthOf(histograms, 0); + }); + + test('cpuProcessMetric_singleSnapshots', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + { + 'name': 'CPUSnapshots', + 'args': { + 'snapshot': { + 'processes': [ + {'path': '/usr/sbin/crudd', 'pCpu': '99.0', 'pid': '13495', + 'pMem': '0.0', 'name': 'crudd'}, + {'path': '/opt/chrome/chrome', 'pCpu': '0.8', + 'pid': '29660', 'pMem': '0.9', 'name': 'chrome'}, + {'path': '/opt/chrome/chrome', 'pCpu': '0.3', + 'pid': '29661', 'pMem': '0.9', 'name': 'chrome'} + ] + } + }, + 'pid': 52, 'ts': '2226221225693.658', 'tid': 53, 'ph': 'O', + 'local': true, 'id': '0x1000' + } + ]; + tr.metrics.sh.cpuProcessMetric(histograms, makeModel(events)); + + assert.isDefined(histograms.getHistogramNamed('cpu:percent:chrome')); + assert.isDefined(histograms.getHistogramNamed('cpu:percent:crudd')); + const chromeValue = histograms.getHistogramNamed('cpu:percent:chrome'); + const chromeStatistics = chromeValue.running; + assert.strictEqual(chromeStatistics.count, 1); + assert.closeTo(chromeStatistics.mean, 0.011, 1e-5); + assert.closeTo(chromeStatistics.max, 0.011, 1e-5); + assert.instanceOf(chromeValue.diagnostics.get('paths'), tr.v.d.GenericSet); + const paths = tr.b.getOnlyElement(chromeValue.diagnostics.get('paths')); + assert.strictEqual(paths, '/opt/chrome/chrome'); + }); + + test('cpuProcessMetric_multipleSnapshots', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + { + 'name': 'CPUSnapshots', + 'args': { + 'snapshot': { + 'processes': [ + {'path': '/usr/sbin/crudd', 'pCpu': '99.0', 'pid': '13495', + 'pMem': '0.0', 'name': 'crudd'}, + {'path': '/opt/chrome/chrome', 'pCpu': '0.8', + 'pid': '29660', 'pMem': '0.9', 'name': 'chrome'} + ] + } + }, + 'pid': 52, 'ts': '2226221225693.658', 'tid': 53, + 'ph': 'O', 'local': true, 'id': '0x1000' + }, + { + 'name': 'CPUSnapshots', + 'args': { + 'snapshot': { + 'processes': [ + {'path': '/usr/sbin/crudd', 'pCpu': '1.3', 'pid': '13495', + 'pMem': '0.0', 'name': 'crudd'}, + {'path': '/opt/chrome/chrome', 'pCpu': '0.6', + 'pid': '29660', 'pMem': '0.9', 'name': 'chrome'}, + {'path': '/opt/chromium/chrome', 'pCpu': '0.1', + 'pid': '29660', 'pMem': '0.9', 'name': 'chrome'}, + {'path': '/usr/sbin/mnp_logger', 'pCpu': '0.2', 'pid': '6543', + 'pMem': '0.1', 'name': 'mnp_logger'} + ] + } + }, + 'pid': 52, 'ts': '2226222262064.4473', 'tid': 53, + 'ph': 'O', 'local': true, 'id': '0x1000' + } + ]; + tr.metrics.sh.cpuProcessMetric(histograms, makeModel(events)); + assert.isDefined(histograms.getHistogramNamed('cpu:percent:chrome')); + assert.isDefined(histograms.getHistogramNamed('cpu:percent:crudd')); + assert.isDefined(histograms.getHistogramNamed('cpu:percent:mnp_logger')); + const chromeValue = histograms.getHistogramNamed('cpu:percent:chrome'); + const chromeStatistics = chromeValue.running; + assert.strictEqual(chromeStatistics.count, 2); + assert.closeTo(chromeStatistics.mean, 0.0075, 1e-5); + assert.strictEqual(chromeStatistics.max, 0.008); + assert.instanceOf(chromeValue.diagnostics.get('paths'), tr.v.d.GenericSet); + const paths = Array.from(chromeValue.diagnostics.get('paths')); + assert.lengthOf(paths, 2); + assert.strictEqual(paths[0], '/opt/chrome/chrome'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/discover.py b/chromium/third_party/catapult/tracing/tracing/metrics/discover.py new file mode 100644 index 00000000000..ef532c6d0df --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/discover.py @@ -0,0 +1,34 @@ +# Copyright 2016 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 json +import os + +import tracing_project +import vinn + + +_DISCOVER_CMD_LINE = os.path.join( + os.path.dirname(__file__), 'discover_cmdline.html') + + +def DiscoverMetrics(modules_to_load): + """ Returns a list of registered metrics. + + Args: + modules_to_load: a list of modules (string) to be loaded before discovering + the registered metrics. + """ + assert isinstance(modules_to_load, list) + project = tracing_project.TracingProject() + all_source_paths = list(project.source_paths) + + res = vinn.RunFile( + _DISCOVER_CMD_LINE, source_paths=all_source_paths, + js_args=modules_to_load) + + if res.returncode != 0: + raise RuntimeError('Error running metrics_discover_cmdline: ' + res.stdout) + else: + return [str(m) for m in json.loads(res.stdout)] diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/discover_cmdline.html b/chromium/third_party/catapult/tracing/tracing/metrics/discover_cmdline.html new file mode 100644 index 00000000000..c8aab5cd98d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/discover_cmdline.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/metrics/metric_registry.html"> + +<script> +'use strict'; +/* eslint-disable no-console */ + +function discoverMetrics(args) { + for (let i = 0; i < args.length; i++) { + const filename = args[i]; + HTMLImportsLoader.loadHTML(filename); + } + + const metrics = tr.metrics.MetricRegistry.getAllRegisteredTypeInfos(); + const discoveredMetricNames = []; + for (let i = 0; i < metrics.length; i++) { + discoveredMetricNames.push(metrics[i].constructor.name); + } + console.log(JSON.stringify(discoveredMetricNames)); + return 0; +} + +if (tr.isHeadless) { + quit(discoverMetrics(sys.argv.slice(1))); +} + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/discover_unittest.py b/chromium/third_party/catapult/tracing/tracing/metrics/discover_unittest.py new file mode 100644 index 00000000000..10b81b547c8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/discover_unittest.py @@ -0,0 +1,20 @@ +# Copyright (c) 2016 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 unittest + +from tracing.metrics import discover + +class MetricsDiscoverUnittest(unittest.TestCase): + def testMetricsDiscoverEmpty(self): + self.assertFalse(discover.DiscoverMetrics([])) + + def testMetricsDiscoverNonEmpty(self): + self.assertEquals(['sampleMetric'], discover.DiscoverMetrics( + ['/tracing/metrics/sample_metric.html'])) + + def testMetricsDiscoverMultipleMetrics(self): + self.assertGreater( + len(discover.DiscoverMetrics( + ['/tracing/metrics/all_metrics.html'])), 1) diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/media_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/media_metric.html new file mode 100644 index 00000000000..2e1ec342a5e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/media_metric.html @@ -0,0 +1,395 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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. +--> + +<!-- +media_metric uses Chrome trace events to calculate metrics about video +and audio playback. It is meant to be used for pages with <video> and/or +<audio> elements. It is used by videostack-eng@google.com team for +regression testing. + +See class PerPlaybackData for details on each of the values that are measured. + +This metric supports media playbacks happening simultaneously over multiple +pages, supports multiple media elements on a page, and supports multiple +playbacks with each element. It does not support media playback using +flash or any other technology not provided by Chrome videostack team. + +Please inform crouleau@chromium.org and johnchen@chromium.org about +changes to this file. +--> + +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics', function() { + function mediaMetric(histograms, model) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (chromeHelper === undefined) return; + + for (const rendererHelper of Object.values(chromeHelper.rendererHelpers)) { + // Find the threads we're interested in, and if a needed thread + // is missing, no need to look further in this process. + const mainThread = rendererHelper.mainThread; + if (mainThread === undefined) continue; + + const compositorThread = rendererHelper.compositorThread; + const audioThreads = + rendererHelper.process.findAllThreadsNamed('AudioOutputDevice'); + if (compositorThread === undefined && audioThreads.length === 0) continue; + + const processData = new PerProcessData(); + + processData.recordPlayStarts(mainThread); + if (!processData.hasPlaybacks) continue; + + if (compositorThread !== undefined) { + processData.calculateTimeToVideoPlays(compositorThread); + processData.calculateDroppedFrameCounts(compositorThread); + } + + if (audioThreads.length !== 0) { + processData.calculateTimeToAudioPlays(audioThreads); + } + + processData.calculateSeekTimes(mainThread); + processData.calculateBufferingTimes(mainThread); + + processData.addMetricToHistograms(histograms); + } + } + + // PerProcessData manages all metric values associated with a renderer + // process. The process can have multiple media playbacks. + class PerProcessData { + constructor() { + // All the perf data we collect for a process are stored in a Map. + // Each key of the map is an ID of a media playback, and the value + // associated with each key is a PerPlaybackData object containing + // all the perf data for that playback. + this.playbackIdToDataMap_ = new Map(); + } + + recordPlayStarts(mainThread) { + for (const event of mainThread.sliceGroup.getDescendantEvents()) { + if (event.title === 'WebMediaPlayerImpl::DoLoad') { + const id = event.args.id; + if (this.playbackIdToDataMap_.has(id)) { + throw new Error( + 'Unexpected multiple initialization of a media playback'); + } + this.playbackIdToDataMap_.set(id, new PerPlaybackData(event.start)); + } + } + } + + get hasPlaybacks() { + return this.playbackIdToDataMap_.size > 0; + } + + calculateTimeToVideoPlays(compositorThread) { + for (const event of compositorThread.sliceGroup.getDescendantEvents()) { + if (event.title === 'VideoRendererImpl::Render') { + this.getPerPlaybackObject_(event.args.id) + .processVideoRenderTime(event.start); + } + } + } + + calculateTimeToAudioPlays(audioThreads) { + for (const audioThread of audioThreads) { + for (const event of audioThread.sliceGroup.getDescendantEvents()) { + if (event.title === 'AudioRendererImpl::Render') { + this.getPerPlaybackObject_(event.args.id) + .processAudioRenderTime(event.start); + } + } + } + } + + calculateSeekTimes(mainThread) { + for (const event of mainThread.sliceGroup.getDescendantEvents()) { + if (event.title === 'WebMediaPlayerImpl::DoSeek') { + this.getPerPlaybackObject_(event.args.id) + .processDoSeek(event.args.target, event.start); + } else if (event.title === 'WebMediaPlayerImpl::OnPipelineSeeked') { + this.getPerPlaybackObject_(event.args.id) + .processOnPipelineSeeked(event.args.target, event.start); + } else if (event.title === 'WebMediaPlayerImpl::BufferingHaveEnough') { + this.getPerPlaybackObject_(event.args.id) + .processBufferingHaveEnough(event.start); + } + } + } + + calculateBufferingTimes(mainThread) { + for (const event of mainThread.sliceGroup.getDescendantEvents()) { + if (event.title === 'WebMediaPlayerImpl::OnEnded') { + this.getPerPlaybackObject_(event.args.id) + .processOnEnded(event.start, event.args.duration); + } + } + } + + calculateDroppedFrameCounts(compositorThread) { + for (const event of compositorThread.sliceGroup.getDescendantEvents()) { + if (event.title === 'VideoFramesDropped') { + this.getPerPlaybackObject_(event.args.id) + .processVideoFramesDropped(event.args.count); + } + } + } + + addMetricToHistograms(histograms) { + for (const [id, playbackData] of this.playbackIdToDataMap_) { + playbackData.addMetricToHistograms(histograms); + } + } + + // @private + getPerPlaybackObject_(playbackId) { + let perPlaybackObject = this.playbackIdToDataMap_.get(playbackId); + if (perPlaybackObject === undefined) { + // The trace isn't complete, and didn't contain the DoLoad event for + // this playback. Create a new PerPlaybackData object for this playback. + perPlaybackObject = new PerPlaybackData(undefined); + this.playbackIdToDataMap_.set(playbackId, perPlaybackObject); + } + return perPlaybackObject; + } + } + + // PerPlaybackData contains all metric values associated with a single + // media playback. + class PerPlaybackData { + constructor(playStartTime) { + this.playStart_ = playStartTime; + this.timeToVideoPlay_ = undefined; + this.timeToAudioPlay_ = undefined; + this.bufferingTime_ = undefined; + this.droppedFrameCount_ = 0; + this.seekError_ = false; + this.seekTimes_ = new Map(); + this.currentSeek_ = undefined; + } + + // API methods for retrieving metric values. Each method returns undefined + // if no value is available (e.g., timeToVideoPlay() returns undefined for + // an audio-only playback). + + // Returns how long after a video is requested to start playing before + // the video actually starts. If time_to_video_play regresses, then users + // will click to play videos and then have to wait longer before the videos + // start actually playing. + get timeToVideoPlay() { + return this.timeToVideoPlay_; + } + + // Similar to timeToVideoPlay, but measures the time delay before audio + // starts playing. + get timeToAudioPlay() { + return this.timeToAudioPlay_; + } + + // Returns the difference between the actual play time of media vs its + // expected play time. Ideally the two should be the same. If actual play + // time is significantly longer than expected play time, it indicates that + // there were stalls during the play for buffering or some other reasons. + // Current limitation: Buffering time isn't calculated if seek occurred + // during playback, and it gives incorrect value if the playback isn't + // from beginning to end without pauses. + get bufferingTime() { + return this.bufferingTime_; + } + + // Reports the number of video frames that were dropped. Ideally this + // should be 0. If a large number of frames are dropped, the video playback + // will not be smooth. + get droppedFrameCount() { + // We should report dropped frame count as long as video was played. + return (this.timeToVideoPlay_ !== undefined) ? + this.droppedFrameCount_ : undefined; + } + + // Returns a Map containing seek times. The keys of the map are numerical + // values indicating the target location of the seek, in unit of seconds. + // The values of the map are objects with the following public properties: + // * pipelineSeekTime: amount of time taken by media pipeline to process + // this seek operation, from when the seek request is received, to when + // the pipeline starts processing at the new location, in milliseconds. + // * seekTime: how long after a user requests a seek operation before the + // seek completes and the media starts playing at the new location, as + // perceived by the user, in milliseconds. + get seekTimes() { + if (this.seekError_ || this.currentSeek_ !== undefined) return new Map(); + return this.seekTimes_; + } + + // API methods for processing data from trace events. + + processVideoRenderTime(videoRenderTime) { + // Each video playback can generate multiple Render events, one for + // each frame. For calculating time to video play, we only use the + // first Render event. + if (this.playStart_ !== undefined && + this.timeToVideoPlay_ === undefined) { + this.timeToVideoPlay_ = videoRenderTime - this.playStart_; + } + } + + processAudioRenderTime(audioRenderTime) { + if (this.playStart_ !== undefined && + this.timeToAudioPlay_ === undefined) { + this.timeToAudioPlay_ = audioRenderTime - this.playStart_; + } + } + + processVideoFramesDropped(count) { + this.droppedFrameCount_ += count; + } + + // We support multiple seeks per element, as long as they seek to different + // target time. Thus the seek times are stored in a Map instead of a scalar + // property. The key of the map is event.args.target, which is a numerical + // value indicating the target location of the seek, in unit of seconds. + // For example, with a seek to 5 seconds mark, event.args.target === 5. + // The value of the map is an object with 4 properties (the first two are + // added during object creation, the latter two are added as the data + // become available): + // * target: seek target time (same as the map key) + // * startTime: timestamp of the event marking start of seek + // * pipelineSeekTime: amount of time taken by media pipeline to process + // this seek (milliseconds) + // * seekTime: amount of seek time perceived by the user (milliseconds) + // If any unexpected conditions occur, we stop processing and set an error + // flag this.seekError_. + // TODO(https://github.com/catapult-project/catapult/issues/3976): + // Emit detailed warnings. + processDoSeek(target, startTime) { + // currentSeek_ refers to the object associated with the + // seek that is currently being processed for this media element. + // It is used to match seek end events against seek start events. + if (this.currentSeek_ !== undefined) { + // TODO(https://github.com/catapult-project/catapult/issues/3976): + // Warning 'Overlapping seek not supported'. + this.seekError_ = true; + return; + } + this.currentSeek_ = { target, startTime }; + this.seekTimes_.set(target, this.currentSeek_); + } + + processOnPipelineSeeked(target, time) { + if (this.seekError_) return; + const currentSeek = this.currentSeek_; + if (currentSeek === undefined) { + // OK to have this event when there is no active seek, as this event + // can be generated for other reasons, e.g., initial loading of media + // generates this event with target of 0 seconds. + return; + } + if (currentSeek.target !== target) { + // TODO(https://github.com/catapult-project/catapult/issues/3976): + // Warning 'WebMediaPlayerImpl::OnPipelineSeeked to unexpected target'. + this.seekError_ = true; + return; + } + if (currentSeek.pipelineSeekTime !== undefined) { + // TODO(https://github.com/catapult-project/catapult/issues/3976): + // Warning 'Multiple WebMediaPlayerImpl::OnPipelineSeeked events'. + this.seekError_ = true; + return; + } + currentSeek.pipelineSeekTime = time - currentSeek.startTime; + } + + processBufferingHaveEnough(time) { + if (this.seekError_) return; + const currentSeek = this.currentSeek_; + if (currentSeek === undefined) { + // No current seek means this event is generated by non-seek related + // events, e.g., initial loading of media. + return; + } + if (currentSeek.pipelineSeekTime === undefined) { + // Since we haven't seen WebMediaPlayerImpl::OnPipelineSeeked event + // event yet, this event is triggered by something else, e.g., a + // have_nothing->have_enough cycle due to underflow from decoders. + return; + } + currentSeek.seekTime = time - currentSeek.startTime; + // Finished processing current seek. + this.currentSeek_ = undefined; + } + + processOnEnded(playEndTime, duration) { + if (this.playStart_ === undefined) return; + // Can't calculate buffering time if there were any seeks. + if (this.seekTimes_.size !== 0 || this.seekError_) return; + // Play was resumed after it ended previously. + if (this.bufferingTime_ !== undefined) return; + // Convert duration from seconds to milliseconds. + duration = tr.b.convertUnit(duration, tr.b.UnitPrefixScale.METRIC.NONE, + tr.b.UnitPrefixScale.METRIC.MILLI); + const playTime = playEndTime - this.playStart_; + if (this.timeToVideoPlay_ !== undefined) { + this.bufferingTime_ = playTime - duration - this.timeToVideoPlay_; + } else if (this.timeToAudioPlay !== undefined) { + this.bufferingTime_ = playTime - duration - this.timeToAudioPlay_; + } + } + + addMetricToHistograms(histograms) { + this.addSample_(histograms, 'time_to_video_play', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + this.timeToVideoPlay); + this.addSample_(histograms, 'time_to_audio_play', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + this.timeToAudioPlay); + this.addSample_(histograms, 'dropped_frame_count', + tr.b.Unit.byName.count_smallerIsBetter, + this.droppedFrameCount); + for (const [key, value] of this.seekTimes.entries()) { + // key is a numerical value that can have '.' when converted to + // string. However, '.' causes problems in histogram names, so + // replace with '_'. + const keyString = key.toString().replace('.', '_'); + this.addSample_(histograms, 'pipeline_seek_time_' + keyString, + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + value.pipelineSeekTime); + this.addSample_(histograms, 'seek_time_' + keyString, + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + value.seekTime); + } + this.addSample_(histograms, 'buffering_time', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + this.bufferingTime); + } + + // @private + addSample_(histograms, name, unit, sample) { + if (sample === undefined) return; + const histogram = histograms.getHistogramNamed(name); + if (histogram === undefined) { + histograms.createHistogram(name, unit, sample); + } else { + histogram.addSample(sample); + } + } + } + + tr.metrics.MetricRegistry.register(mediaMetric); + + return { + mediaMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/media_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/media_metric_test.html new file mode 100644 index 00000000000..a404b8e1980 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/media_metric_test.html @@ -0,0 +1,423 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/media_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + // Arbitrarily selected process ID and thread IDs we'll use in test data + const procId = 52; + const tidMain = 1; + const tidCompositor = 53; + const tidAudio = 55; + + function doLoadEvent(timestamp, opt_id) { + return {name: 'WebMediaPlayerImpl::DoLoad', args: {id: opt_id || 0}, + pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'}; + } + + function videoRenderEvent(timestamp, opt_id) { + return {name: 'VideoRendererImpl::Render', args: {id: opt_id || 0}, + pid: procId, ts: timestamp, cat: 'media', tid: tidCompositor, ph: 'X'}; + } + + function audioRenderEvent(timestamp, opt_id) { + return {name: 'AudioRendererImpl::Render', args: {id: opt_id || 0}, + pid: procId, ts: timestamp, cat: 'media', tid: tidAudio, ph: 'X'}; + } + + function videoFramesDroppedEvent(timestamp, frameCount, opt_id) { + return {name: 'VideoFramesDropped', + args: {count: frameCount, id: opt_id || 0}, + pid: procId, ts: timestamp, cat: 'media', tid: tidCompositor, ph: 'X'}; + } + + function onEndedEvent(timestamp, mediaDuration, opt_id) { + return {name: 'WebMediaPlayerImpl::OnEnded', + args: {duration: mediaDuration, id: opt_id || 0}, + pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'}; + } + + function doSeekEvent(timestamp, targetTime, opt_id) { + return {name: 'WebMediaPlayerImpl::DoSeek', + args: {target: targetTime, id: opt_id || 0}, + pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'}; + } + + function seekedEvent(timestamp, targetTime, opt_id) { + return {name: 'WebMediaPlayerImpl::OnPipelineSeeked', + args: {target: targetTime, id: opt_id || 0}, + pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'}; + } + + function bufferEnoughEvent(timestamp, opt_id) { + return {name: 'WebMediaPlayerImpl::BufferingHaveEnough', + args: {id: opt_id || 0}, + pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'}; + } + + function threadMarker(threadName, threadId) { + return {name: 'thread_name', args: {name: threadName}, + pid: procId, ts: 0, cat: '__metadata', tid: threadId, ph: 'M'}; + } + + const mainThreadMarker = threadMarker('CrRendererMain', tidMain); + const compositorThreadMarker = threadMarker('Compositor', tidCompositor); + const audioThreadMarker = threadMarker('AudioOutputDevice', tidAudio); + + function makeModel(events) { + return tr.c.TestUtils.newModelWithEvents([events]); + } + + function checkCloseTo(histograms, histogramName, expectedValue) { + assert.isDefined(histograms.getHistogramNamed(histogramName)); + const value = histograms.getHistogramNamed(histogramName); + const statistics = value.running; + assert.strictEqual(statistics.count, 1); + assert.closeTo(statistics.mean, expectedValue, 1e-5); + } + + function checkCloseToMultiple(histograms, histogramName, expectedCount, + expectedMin, expectedMean, expectedMax) { + assert.isDefined(histograms.getHistogramNamed(histogramName)); + const value = histograms.getHistogramNamed(histogramName); + const statistics = value.running; + assert.strictEqual(statistics.count, expectedCount); + assert.closeTo(statistics.min, expectedMin, 1e-5); + assert.closeTo(statistics.mean, expectedMean, 1e-5); + assert.closeTo(statistics.max, expectedMax, 1e-5); + } + + function checkEqual(histograms, histogramName, expectedValue) { + assert.isDefined(histograms.getHistogramNamed(histogramName)); + const value = histograms.getHistogramNamed(histogramName); + const statistics = value.running; + assert.strictEqual(statistics.count, 1); + assert.strictEqual(statistics.mean, expectedValue); + } + + test('mediaMetric_noData', function() { + const histograms = new tr.v.HistogramSet(); + const events = []; + tr.metrics.mediaMetric(histograms, makeModel(events)); + assert.lengthOf(histograms, 0); + }); + + test('mediaMetric_videoTimeToPlay', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(100), + videoRenderEvent(300), + // Video renderer always generate multiple render events, + // one for each frame. For calculation of time-to-play, + // only the first render event is relevant. Here we put in + // a second render event to make sure it's ignored by the + // metric computation code. + videoRenderEvent(400), + mainThreadMarker, + compositorThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + checkCloseTo(histograms, 'time_to_video_play', 0.2); + }); + + test('mediaMetric_audioTimeToPlay', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + mainThreadMarker, + audioThreadMarker, + doLoadEvent(1000), + audioRenderEvent(1100), + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + checkCloseTo(histograms, 'time_to_audio_play', 0.1); + }); + + test('mediaMetric_bufferingTimeVideo', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + videoRenderEvent(1500), + videoRenderEvent(1600), + onEndedEvent(10051500, 10), + mainThreadMarker, + compositorThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + checkCloseTo(histograms, 'buffering_time', 50); + }); + + test('mediaMetric_bufferingTimeAudio', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + mainThreadMarker, + audioThreadMarker, + doLoadEvent(1000), + audioRenderEvent(1500), + onEndedEvent(5002500, 5), + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + checkCloseTo(histograms, 'buffering_time', 1); + }); + + // With seek, no buffering time should be reported + test('mediaMetric_noBufferingTime', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + videoRenderEvent(1500), + videoRenderEvent(1600), + onEndedEvent(10066666, 10), + doSeekEvent(525, 1.2), + seekedEvent(719, 1.2), + bufferEnoughEvent(825), + mainThreadMarker, + compositorThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + assert.isUndefined(histograms.getHistogramNamed('buffering_time')); + }); + + test('mediaMetric_droppedFrameCount', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + videoRenderEvent(1500), + videoFramesDroppedEvent(123456, 3), + videoFramesDroppedEvent(234567, 6), + videoFramesDroppedEvent(345678, 1), + mainThreadMarker, + compositorThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + checkEqual(histograms, 'dropped_frame_count', 10); + }); + + test('mediaMetric_droppedFrameCountZero', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + videoRenderEvent(1500), + mainThreadMarker, + compositorThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + checkEqual(histograms, 'dropped_frame_count', 0); + }); + + test('mediaMetric_droppedFrameCountNoVideo', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + audioRenderEvent(1500), + mainThreadMarker, + audioThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + assert.isUndefined(histograms.getHistogramNamed('dropped_frame_count')); + }); + + test('mediaMetric_seekTimeVideo', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + videoRenderEvent(1500), + doSeekEvent(2000, 1.2), + seekedEvent(2500, 1.2), + bufferEnoughEvent(3000), + doSeekEvent(15000, 3.7), + seekedEvent(75000, 3.7), + bufferEnoughEvent(95000), + mainThreadMarker, + compositorThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + checkCloseTo(histograms, 'pipeline_seek_time_1_2', 0.5); + checkCloseTo(histograms, 'seek_time_1_2', 1); + checkCloseTo(histograms, 'pipeline_seek_time_3_7', 60); + checkCloseTo(histograms, 'seek_time_3_7', 80); + }); + + test('mediaMetric_seekTimeAudio', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + audioRenderEvent(1500), + doSeekEvent(2000, 1.2), + seekedEvent(2500, 1.2), + bufferEnoughEvent(3000), + doSeekEvent(15000, 3.7), + seekedEvent(75000, 3.7), + bufferEnoughEvent(95000), + mainThreadMarker, + audioThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + checkCloseTo(histograms, 'pipeline_seek_time_1_2', 0.5); + checkCloseTo(histograms, 'seek_time_1_2', 1); + checkCloseTo(histograms, 'pipeline_seek_time_3_7', 60); + checkCloseTo(histograms, 'seek_time_3_7', 80); + }); + + test('mediaMetric_seekOverlap', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + videoRenderEvent(1500), + doSeekEvent(2000, 1.2), + seekedEvent(2500, 1.2), + doSeekEvent(2800, 3.7), // Out of order + bufferEnoughEvent(3000), + seekedEvent(75000, 3.7), + bufferEnoughEvent(95000), + mainThreadMarker, + compositorThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + assert.isDefined(histograms.getHistogramNamed('time_to_video_play')); + assert.isUndefined(histograms.getHistogramNamed('seek_time_1_2')); + }); + + test('mediaMetric_seekIncomplete', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + videoRenderEvent(1500), + doSeekEvent(2000, 1.2), + seekedEvent(2500, 1.2), + // Missing bufferEnoughEvent + mainThreadMarker, + compositorThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + assert.isDefined(histograms.getHistogramNamed('time_to_video_play')); + assert.isUndefined(histograms.getHistogramNamed('seek_time_1_2')); + }); + + test('mediaMetric_seekWrongTarget', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + videoRenderEvent(1500), + doSeekEvent(2000, 1.2), + seekedEvent(2500, 2.7), // Wrong target, should be 1.2 + mainThreadMarker, + compositorThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + assert.isDefined(histograms.getHistogramNamed('time_to_video_play')); + assert.isUndefined(histograms.getHistogramNamed('seek_time_1_2')); + }); + + test('mediaMetric_multiplePlaybacks', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000, 0), + doLoadEvent(1100, 1), + videoRenderEvent(1400, 1), + videoRenderEvent(1500, 0), + doLoadEvent(2000, 2), + doSeekEvent(2000, 1.2, 0), + doSeekEvent(2100, 1.2, 1), + videoRenderEvent(2100, 2), + doSeekEvent(2200, 1.2, 2), + seekedEvent(2500, 1.2, 2), + seekedEvent(2600, 1.2, 1), + seekedEvent(2700, 1.2, 0), + bufferEnoughEvent(3000, 0), + bufferEnoughEvent(3100, 1), + bufferEnoughEvent(3800, 2), + mainThreadMarker, + compositorThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + // Values are 0.5, 0.3, 0.1, with min 0.1, mean 0.3, max 0.5. + checkCloseToMultiple(histograms, 'time_to_video_play', 3, 0.1, 0.3, 0.5); + // Values are 0.7, 0.5, 0.3, with min 0.3, mean 0.5, max 0.7. + checkCloseToMultiple(histograms, + 'pipeline_seek_time_1_2', 3, 0.3, 0.5, 0.7); + // Values are 1, 1, 1.6, with min 1, mean 1.2, max 1.6 + checkCloseToMultiple(histograms, 'seek_time_1_2', 3, 1, 1.2, 1.6); + }); + + // Scenario: Play mixed audio/video from start to finish + test('mediaMetric_playVideoScenario', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(2000), + videoRenderEvent(3000), + audioRenderEvent(3200), + videoRenderEvent(3300), + videoFramesDroppedEvent(123456, 4), + videoFramesDroppedEvent(234567, 2), + onEndedEvent(10013000, 10), + mainThreadMarker, + compositorThreadMarker, + audioThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + checkCloseTo(histograms, 'time_to_video_play', 1); + checkCloseTo(histograms, 'time_to_audio_play', 1.2); + checkCloseTo(histograms, 'buffering_time', 10); + checkEqual(histograms, 'dropped_frame_count', 6); + }); + + // Scenario: Play audio from start to finish + test('mediaMetric_playAudioScenario', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + audioRenderEvent(1500), + onEndedEvent(10002500, 10), + mainThreadMarker, + audioThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + assert.isUndefined(histograms.getHistogramNamed('time_to_video_play')); + checkCloseTo(histograms, 'time_to_audio_play', 0.5); + checkCloseTo(histograms, 'buffering_time', 1); + assert.isUndefined(histograms.getHistogramNamed('dropped_frame_count')); + }); + + // Scenario: Play audio/video with two seeks + test('mediaMetric_seekScenario', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + doLoadEvent(1000), + videoRenderEvent(2000), + audioRenderEvent(2020), + videoRenderEvent(2040), + doSeekEvent(5000, 0.5), + seekedEvent(5200, 0.5), + bufferEnoughEvent(6000), + videoFramesDroppedEvent(123456, 4), + doSeekEvent(200000, 9), + seekedEvent(210000, 9), + bufferEnoughEvent(220000), + videoFramesDroppedEvent(234567, 2), + onEndedEvent(300000, 10), + mainThreadMarker, + compositorThreadMarker, + audioThreadMarker, + ]; + tr.metrics.mediaMetric(histograms, makeModel(events)); + checkCloseTo(histograms, 'time_to_video_play', 1); + checkCloseTo(histograms, 'time_to_audio_play', 1.02); + assert.isUndefined(histograms.getHistogramNamed('buffering_time')); + checkEqual(histograms, 'dropped_frame_count', 6); + checkCloseTo(histograms, 'pipeline_seek_time_0_5', 0.2); + checkCloseTo(histograms, 'seek_time_0_5', 1); + checkCloseTo(histograms, 'pipeline_seek_time_9', 10); + checkCloseTo(histograms, 'seek_time_9', 20); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function.html b/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function.html new file mode 100644 index 00000000000..1b9ac12e1cc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function.html @@ -0,0 +1,216 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/metrics/all_metrics.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/mre/failure.html"> +<link rel="import" href="/tracing/mre/function_handle.html"> +<link rel="import" href="/tracing/value/diagnostics/reserved_names.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics', function() { + /** + * @param {!tr.model.Model} model + * @param {!Object} options + * @param {!Array.<string>} options.metrics + * @param {!Function} addFailureCb + * @return {!tr.v.HistogramSet} + */ + function runMetrics(model, options, addFailureCb) { + if (options === undefined) { + throw new Error('Options are required.'); + } + + const metricNames = options.metrics; + if (!metricNames) { + throw new Error('Metric names should be specified.'); + } + + const allMetricsStart = new Date(); + const durationBreakdown = new tr.v.d.Breakdown(); + + const categories = getTraceCategories(model); + + const histograms = new tr.v.HistogramSet(); + + histograms.createHistogram('trace_import_duration', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + model.stats.traceImportDurationMs, { + binBoundaries: tr.v.HistogramBinBoundaries.createExponential( + 1e-3, 1e5, 30), + description: + 'Duration that trace viewer required to import the trace', + summaryOptions: tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS, + }); + + for (const metricName of metricNames) { + const metricStart = new Date(); + const metric = tr.metrics.MetricRegistry.findTypeInfoWithName(metricName); + if (metric === undefined) { + throw new Error(`"${metricName}" is not a registered metric.`); + } + validateTraceCategories(metric.metadata.requiredCategories, categories); + try { + metric.constructor(histograms, model, options); + } catch (e) { + const err = tr.b.normalizeException(e); + addFailureCb(new tr.mre.Failure( + undefined, 'metricMapFunction', model.canonicalUrl, err.typeName, + err.message, err.stack)); + } + const metricMs = new Date() - metricStart; + histograms.createHistogram( + metricName + '_duration', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [metricMs]); + durationBreakdown.set(metricName, metricMs); + } + + validateDiagnosticNames(histograms); + + const allMetricsMs = new Date() - allMetricsStart + + model.stats.traceImportDurationMs; + durationBreakdown.set('traceImport', model.stats.traceImportDurationMs); + durationBreakdown.set('other', allMetricsMs - tr.b.math.Statistics.sum( + durationBreakdown, ([metricName, metricMs]) => metricMs)); + const breakdownNames = tr.v.d.RelatedNameMap.fromEntries(new Map( + metricNames.map(metricName => [metricName, metricName + '_duration']))); + breakdownNames.set('traceImport', 'trace_import_duration'); + histograms.createHistogram( + 'metrics_duration', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [ + { + value: allMetricsMs, + diagnostics: {breakdown: durationBreakdown}, + }, + ], { + diagnostics: {breakdown: breakdownNames}, + }); + + return histograms; + } + + function getTraceCategories(model) { + for (const metadata of model.metadata) { + let config; + if (metadata.name === 'TraceConfig' && metadata.value) { + config = metadata.value; + } + if (metadata.name === 'metadata' && metadata.value && + metadata.value['trace-config'] && + metadata.value['trace-config'] !== '__stripped__') { + config = JSON.parse(metadata.value['trace-config']); + } + if (config) { + return { + excluded: config.excluded_categories || [], + included: config.included_categories || [], + }; + } + } + } + + function validateTraceCategories(requiredCategories, categories) { + if (!requiredCategories) return; + + if (!categories) throw new Error('Missing trace config metadata'); + + for (const cat of requiredCategories) { + const isDisabledByDefault = (cat.indexOf('disabled-by-default') === 0); + let missing = false; + if (isDisabledByDefault) { + if (!categories.included.includes(cat)) { + missing = true; + } + } else if (categories.excluded.includes(cat)) { + missing = true; + } + if (missing) { + throw new Error(`Trace is missing required category "${cat}"`); + } + } + } + + /** + * Ensure that metrics don't use reserved diagnostic names. + * + * @param {!tr.v.HistogramSet} histograms + */ + function validateDiagnosticNames(histograms) { + for (const hist of histograms) { + for (const name of hist.diagnostics.keys()) { + if (tr.v.d.RESERVED_NAMES_SET.has(name)) { + throw new Error( + `Illegal diagnostic name "${name}" on Histogram "${hist.name}"`); + } + } + } + } + + /** + * @param {!tr.v.HistogramSet} histograms + * @param {!tr.model.Model} model + */ + function addTelemetryInfo(histograms, model) { + for (const metadata of model.metadata) { + if (!metadata.value || !metadata.value.telemetry) continue; + + const traceUrls = metadata.value.telemetry[ + tr.v.d.RESERVED_NAMES.TRACE_URLS]; + if (traceUrls && model.canonicalUrl !== traceUrls[0]) { + throw new Error(`canonicalUrl "${model.canonicalUrl}" != ` + + `traceUrl "${traceUrls[0]}"`); + } + + for (const [name, value] of Object.entries(metadata.value.telemetry)) { + const type = tr.v.d.RESERVED_NAMES_TO_TYPES.get(name); + if (type === undefined) { + throw new Error(`Unexpected telemetry.${name}`); + } + histograms.addSharedDiagnosticToAllHistograms(name, new type(value)); + } + } + } + + /** + * @param {!tr.mre.MreResult} result + * @param {!tr.model.Model} model + * @param {!Object} options + * @param {!Array.<string>} options.metrics + */ + function metricMapFunction(result, model, options) { + const histograms = runMetrics( + model, options, result.addFailure.bind(result)); + addTelemetryInfo(histograms, model); + + result.addPair('histograms', histograms.asDicts()); + + const scalarDicts = []; + for (const value of histograms) { + for (const [statName, scalar] of value.statisticsScalars) { + scalarDicts.push({ + name: value.name + '_' + statName, + numeric: scalar.asDict(), + description: value.description, + }); + } + } + result.addPair('scalars', scalarDicts); + } + + tr.mre.FunctionRegistry.register(metricMapFunction); + + return { + metricMapFunction, + runMetrics, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function_test.html new file mode 100644 index 00000000000..5be41925424 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/metric_map_function_test.html @@ -0,0 +1,214 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/metric_map_function.html"> +<link rel="import" href="/tracing/metrics/sample_metric.html"> +<link rel="import" href="/tracing/mre/mre_result.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TestUtils = tr.c.TestUtils; + const ThreadSlice = tr.model.ThreadSlice; + + test('metricMapTest', function() { + const events = [ + {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'} + ]; + const m = TestUtils.newModelWithEvents(JSON.stringify(events), { + shiftWorldToZero: false, + pruneEmptyContainers: false, + trackDetailedModelStats: true, + customizeModelCallback(m) { + const p1 = m.getOrCreateProcess(1); + const t2 = p1.getOrCreateThread(2); + t2.sliceGroup.pushSlice(TestUtils.newSliceEx({ + type: ThreadSlice, + name: 'some_slice', + start: 0, end: 10 + })); + t2.sliceGroup.pushSlice(TestUtils.newSliceEx({ + type: ThreadSlice, + name: 'some_slice', + start: 20, end: 30 + })); + } + }); + + assert.throw(function() { + const result = new tr.mre.MreResult(); + tr.metrics.metricMapFunction(result, m, {}); + }, Error, 'Metric names should be specified.'); + + assert.throw(function() { + const result = new tr.mre.MreResult(); + tr.metrics.metricMapFunction(result, m, {'metrics': ['wrongMetric']}); + }, Error, '"wrongMetric" is not a registered metric.'); + + const result = new tr.mre.MreResult(); + tr.metrics.metricMapFunction( + result, m, {'metrics': ['sampleMetric']}); + assert.property(result.pairs, 'histograms'); + assert.strictEqual(result.pairs.histograms.length, 4); + assert.property(result.pairs, 'scalars'); + assert.strictEqual(result.pairs.scalars.length, 19); + assert.lengthOf(result.failures, 0); + }); + + test('exceptionMetric', function() { + const events = [ + {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'} + ]; + const m = TestUtils.newModelWithEvents(JSON.stringify(events), { + shiftWorldToZero: false, + pruneEmptyContainers: false, + trackDetailedModelStats: true, + customizeModelCallback(m) { + const p1 = m.getOrCreateProcess(1); + const t2 = p1.getOrCreateThread(2); + t2.sliceGroup.pushSlice(TestUtils.newSliceEx({ + type: ThreadSlice, + name: 'some_slice', + start: 0, end: 10 + })); + t2.sliceGroup.pushSlice(TestUtils.newSliceEx({ + type: ThreadSlice, + name: 'some_slice', + start: 20, end: 30 + })); + } + }); + + const result = new tr.mre.MreResult(); + tr.metrics.metricMapFunction( + result, m, {'metrics': ['sampleExceptionMetric']}); + assert.property(result.pairs, 'histograms'); + assert.strictEqual(result.pairs.histograms.length, 4); + assert.property(result.pairs, 'scalars'); + assert.strictEqual(result.pairs.scalars.length, 19); + assert.lengthOf(result.failures, 1); + }); + + function invalidDiagnosticNameMetric(histograms, model) { + histograms.createHistogram('a', tr.b.Unit.byName.count, [], {diagnostics: { + [tr.v.d.RESERVED_NAMES.BOTS]: new tr.v.d.GenericSet([]), + }}); + } + + tr.metrics.MetricRegistry.register(invalidDiagnosticNameMetric); + + test('validateDiagnosticNames', function() { + const result = new tr.mre.MreResult(); + const m = TestUtils.newModel(); + + assert.throw(function() { + tr.metrics.metricMapFunction(result, m, { + 'metrics': ['invalidDiagnosticNameMetric'], + }); + }, Error, 'Illegal diagnostic name ' + + `"${tr.v.d.RESERVED_NAMES.BOTS}" on Histogram "a"`); + }); + + test('validateCanonicalUrl', function() { + const result = new tr.mre.MreResult(); + const m = TestUtils.newModel(model => { + model.canonicalUrl = 'canonical url'; + model.metadata = [{ + value: { + telemetry: { + traceUrls: ['trace url'], + }, + }, + }]; + }); + assert.throw(function() { + tr.metrics.metricMapFunction(result, m, {metrics: ['sampleMetric']}); + }, Error, 'canonicalUrl "canonical url" != traceUrl "trace url"'); + }); + + function requiresDefaultCategoryMetric(histograms, model) { + } + + tr.metrics.MetricRegistry.register(requiresDefaultCategoryMetric, { + requiredCategories: ['foo'], + }); + + function requiresDisabledCategoryMetric(histograms, model) { + } + + tr.metrics.MetricRegistry.register(requiresDisabledCategoryMetric, { + requiredCategories: ['disabled-by-default-foo'], + }); + + test('validateRequiredCategories', function() { + const result = new tr.mre.MreResult(); + let m = TestUtils.newModel(model => { + model.metadata = [{ + name: 'metadata', + value: { + 'trace-config': JSON.stringify({ + excluded_categories: ['foo'], + }), + }, + }]; + }); + + assert.throw(function() { + tr.metrics.metricMapFunction(result, m, {metrics: + ['requiresDefaultCategoryMetric']}); + }, Error, 'Trace is missing required category "foo"'); + + m = TestUtils.newModel(model => { + model.metadata = [{ + name: 'TraceConfig', + value: { + }, + }]; + }); + + assert.throw(function() { + tr.metrics.metricMapFunction(result, m, {metrics: + ['requiresDisabledCategoryMetric']}); + }, Error, 'Trace is missing required category "disabled-by-default-foo"'); + }); + + test('processStrippedConfig', function() { + const result = new tr.mre.MreResult(); + const m = TestUtils.newModel(model => { + model.metadata = [{ + name: 'metadata', + value: { + 'trace-config': '__stripped__' + }, + }]; + }); + tr.metrics.metricMapFunction( + result, m, {'metrics': ['sampleMetric']}); + assert.lengthOf(result.failures, 0); + }); + + test('metricMetrics', function() { + const model = new tr.Model(); + // We can't customize the model the normal way using + // test_utils.newModel(customizeModelCallback) because that callback is run + // before the end of the import phase, so our import duration will be + // overwritten. + model.stats.traceImportDurationMs = 10; + + const histograms = tr.metrics.runMetrics( + model, {'metrics': ['sampleMetric']}); + + assert.strictEqual( + histograms.getHistogramNamed('trace_import_duration').average, 10); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry.html b/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry.html new file mode 100644 index 00000000000..05ea36dd12f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry.html @@ -0,0 +1,116 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/extension_registry.html"> +<link rel="import" href="/tracing/base/utils.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics', function() { + function MetricRegistry() {} + + const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE); + options.defaultMetadata = {}; + tr.b.decorateExtensionRegistry(MetricRegistry, options); + + function camelCaseToHackerString(camelCase) { + let hackerString = ''; + for (const c of camelCase) { + const lowered = c.toLocaleLowerCase(); + if (lowered === c) { + hackerString += c; + } else { + hackerString += '_' + lowered; + } + } + return hackerString; + } + + function getCallStack() { + try { + throw new Error(); + } catch (error) { + return error.stack; + } + } + + function getPathsFromStack(stack) { + return stack.split('\n').map(line => { + line = line.replace(/^ */, '').split(':'); + if (line.length < 4) return ''; + return line[line.length - 3].split('/'); + }).filter(x => x); + } + + MetricRegistry.checkFilename = function(metricName, opt_metricPathForTest) { + if (metricName === 'runtimeStatsTotalMetric' || + metricName === 'v8AndMemoryMetrics') { + // TODO(crbug.com/688342) Remove the runtimeStatsTotalMetric exception. + // TODO(3275) Remove the v8AndMemoryMetrics exception. + // https://github.com/catapult-project/catapult/issues/3275 + return; + } + + const expectedFilename = camelCaseToHackerString(metricName) + '.html'; + const stack = getCallStack(); + + let metricPath = opt_metricPathForTest; + if (metricPath === undefined) { + const paths = getPathsFromStack(stack); + const METRIC_STACK_INDEX = 5; + + // This filename is in paths[0]. If this file is not vulcanized, then the + // metric's filename is in paths[METRIC_STACK_INDEX]. If this file is + // vulcanized, then they are the same, and paths[METRIC_STACK_INDEX] is + // not the metric's filename. + if (paths.length <= METRIC_STACK_INDEX || + paths[METRIC_STACK_INDEX].join('/') === paths[0].join('/')) { + return; + } + + metricPath = paths[METRIC_STACK_INDEX].slice( + paths[METRIC_STACK_INDEX].length - 2); + } + + if (!metricPath[1].endsWith('_test.html') && + !metricPath[1].endsWith('_test.html.js') && + metricPath[1] !== expectedFilename && + metricPath[1] !== expectedFilename + '.js' && + metricPath.join('_') !== expectedFilename && + metricPath.join('_') !== expectedFilename + '.js') { + throw new Error( + 'Expected ' + metricName + ' to be in a file named ' + + expectedFilename + '; actual: ' + metricPath.join('/') + + '; stack: ' + stack.replace(/\n/g, '\n ')); + } + }; + + MetricRegistry.addEventListener('will-register', function(e) { + const metric = e.typeInfo.constructor; + if (!(metric instanceof Function)) { + throw new Error('Metrics must be functions.'); + } + + if (!metric.name.endsWith('Metric') && + !metric.name.endsWith('Metrics')) { + throw new Error('Metric names must end with "Metric" or "Metrics".'); + } + + if (metric.length < 2) { + throw new Error('Metrics take a HistogramSet and a Model and ' + + 'optionally an options dictionary.'); + } + + MetricRegistry.checkFilename(metric.name); + }); + + return { + MetricRegistry, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry_test.html new file mode 100644 index 00000000000..c4123024ae6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/metric_registry_test.html @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('FindMetricByName', function() { + function aMetric(histograms, model) { + const n1 = new tr.v.Histogram('foo', tr.b.Unit.byName.count); + n1.addSample(1); + histograms.addHistogram(n1); + } + tr.metrics.MetricRegistry.register(aMetric); + + function bMetric(histograms, model) { + const n1 = new tr.v.Histogram('foo', tr.b.Unit.byName.count); + const n2 = new tr.v.Histogram('bar', tr.b.Unit.byName.count); + n1.addSample(1); + n2.addSample(2); + histograms.addHistogram(n1); + histograms.addHistogram(n2); + } + tr.metrics.MetricRegistry.register(bMetric); + + function cMetric(histograms, model) { + const n1 = new tr.v.Histogram('foo', tr.b.Unit.byName.count); + const n2 = new tr.v.Histogram('bar', tr.b.Unit.byName.count); + const n3 = new tr.v.Histogram('baz', tr.b.Unit.byName.count); + n1.addSample(1); + n2.addSample(2); + n3.addSample(3); + histograms.addHistogram(n1); + histograms.addHistogram(n2); + histograms.addHistogram(n3); + } + tr.metrics.MetricRegistry.register(cMetric); + + const typeInfo = tr.metrics.MetricRegistry.findTypeInfoWithName( + 'bMetric'); + assert.strictEqual(typeInfo.constructor, bMetric); + }); + + test('registerNonFunctionThrows', function() { + // Metrics must be functions. + assert.throws(function() { + tr.metrics.MetricRegistry.register('not a function'); + }); + + // Metric names must end with "Metric" or "Metrics". + assert.throws(function() { + tr.metrics.MetricRegistry.register(function foo(histograms, model) {}); + }); + + // Metrics take a HistogramSet and a Model and optionally an options + // dictionary. + assert.throws(function() { + tr.metrics.MetricRegistry.register(function fooMetric() {}); + }); + + // Metrics take a HistogramSet and a Model and optionally an options + // dictionary. + assert.throws(function() { + tr.metrics.MetricRegistry.register(function fooMetric(a) {}); + }); + }); + + test('checkFilename', function() { + tr.metrics.MetricRegistry.checkFilename( + 'aMetric', ['metrics', 'a_metric.html']); + tr.metrics.MetricRegistry.checkFilename( + 'fooBarMetric', ['foo', 'bar_metric.html']); + + // Expected fooMetric to be in a file named foo_metric.html; actual: + // foo/bar_metric.html + assert.throws(() => tr.metrics.MetricRegistry.checkFilename( + 'fooMetric', ['foo', 'bar_metric.html'])); + + // Expected fooMetric to be in a file named foo_metric.html; actual: + // cat/bar_metric.html + assert.throws(() => tr.metrics.MetricRegistry.checkFilename( + 'fooMetric', ['cat', 'bar_metric.html'])); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/metric_runner.py b/chromium/third_party/catapult/tracing/tracing/metrics/metric_runner.py new file mode 100644 index 00000000000..b8cc2af8dde --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/metric_runner.py @@ -0,0 +1,75 @@ +# Copyright 2016 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 os +import sys + +from tracing.mre import function_handle +from tracing.mre import gtest_progress_reporter +from tracing.mre import map_runner +from tracing.mre import file_handle +from tracing.mre import job as job_module + + +try: + StringTypes = basestring +except NameError: + StringTypes = str + + +_METRIC_MAP_FUNCTION_FILENAME = 'metric_map_function.html' + +_METRIC_MAP_FUNCTION_NAME = 'metricMapFunction' + +def _GetMetricsDir(): + return os.path.dirname(os.path.abspath(__file__)) + +def _GetMetricRunnerHandle(metrics): + assert isinstance(metrics, list) + for metric in metrics: + assert isinstance(metric, StringTypes) + metrics_dir = _GetMetricsDir() + metric_mapper_path = os.path.join(metrics_dir, _METRIC_MAP_FUNCTION_FILENAME) + + modules_to_load = [function_handle.ModuleToLoad(filename=metric_mapper_path)] + options = {'metrics': metrics} + map_function_handle = function_handle.FunctionHandle( + modules_to_load, _METRIC_MAP_FUNCTION_NAME, options) + + return job_module.Job(map_function_handle, None) + +def RunMetric(filename, metrics, extra_import_options=None, + report_progress=True, canonical_url=None): + filename_url = 'file://' + filename + if canonical_url is None: + canonical_url = filename_url + trace_handle = file_handle.URLFileHandle(canonical_url, filename_url) + result = RunMetricOnTraceHandles( + [trace_handle], metrics, extra_import_options, report_progress) + return result[canonical_url] + +def RunMetricOnTraceHandles(trace_handles, metrics, extra_import_options=None, + report_progress=True): + job = _GetMetricRunnerHandle(metrics) + with open(os.devnull, 'w') as devnull_f: + o_stream = sys.stdout + if not report_progress: + o_stream = devnull_f + + runner = map_runner.MapRunner( + trace_handles, job, extra_import_options=extra_import_options, + progress_reporter=gtest_progress_reporter.GTestProgressReporter( + output_stream=o_stream)) + map_results = runner.RunMapper() + + return map_results + +def RunMetricOnTraces(filenames, metrics, + extra_import_options=None, report_progress=True): + trace_handles = [] + for filename in filenames: + filename_url = 'file://' + filename + trace_handles.append(file_handle.URLFileHandle(filename_url, filename_url)) + + return RunMetricOnTraceHandles(trace_handles, metrics, extra_import_options, + report_progress) diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization.html new file mode 100644 index 00000000000..e1ea92fd04a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization.html @@ -0,0 +1,195 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/value/diagnostics/breakdown.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +/** + * @fileoverview This file contains implementations of the following metrics. + * + * The addCpuUtilizationHistograms method can generate the following sets of + * metrics, dependeing on the input values for segments, shouldNormalize, and + * segmentCostFunc. + * + * thread_{thread group}_cpu_time_per_second + * ========================================= + * segments: any set of interesting segments + * shouldNormalize: True + * segmentCostFunc: thread.getCpuTimeForRange + * + * This set of metrics show how much a thread group was busy during specific + * segments marked by a test. More precisely, it shows the average amount of CPU + * cores per seconds the thread group consumes during the segments. + * + * thread_{thread group}_cpu_time_per_frame + * ======================================== + * segments: display compositor's frames + * shouldNormalize: False + * segmentCostFunc: thread.getCpuTimeForRange + * + * This set of metrics show the distribution of CPU usage of a thread + * group in each display compositor's frame. + * + * tasks_per_second_{thread group} + * =============================== + * segments: any set of interesting segments + * shouldNormalize: True + * segmentCostFunc: thread.getNumToplevelSlicesForRange + * + * This set of metrics show the average number of tasks per second of a thread + * group during specific segments marked by a test. + * + * tasks_per_frame_{thread group} + * ============================== + * segments: display compositor's frames + * shouldNormalize: False + * segmentCostFunc: thread.getNumToplevelSlicesForRange + * + * This set of metrics show the distribution of the number of task in each + * display compositor's frame of a thread group. + * + * Note: the CPU usage in all above-mentioned metrics, is approximated from + * top-level trace events in each thread; it does not come from the OS. So, the + * metric may be noisy and not be very meaningful for threads that do not have a + * message loop. + */ +tr.exportTo('tr.metrics.rendering', function() { + const UNKNOWN_THREAD_NAME = 'Unknown'; + + const CATEGORY_THREAD_MAP = new Map(); + CATEGORY_THREAD_MAP.set('total_all', [/.*/]); + CATEGORY_THREAD_MAP.set( + 'browser', [/^Browser Compositor$/, /^CrBrowserMain$/]); + CATEGORY_THREAD_MAP.set('display_compositor', [/^VizCompositorThread$/]); + CATEGORY_THREAD_MAP.set( + 'total_fast_path', [ + /^Browser Compositor$/, /^Chrome_InProcGpuThread$/, /^Compositor$/, + /^CrBrowserMain$/, /^CrGpuMain$/, /IOThread/, /^VizCompositorThread$/]); + CATEGORY_THREAD_MAP.set('GPU', [/^Chrome_InProcGpuThread$/, /^CrGpuMain$/]); + CATEGORY_THREAD_MAP.set('IO', [/IOThread/]); + CATEGORY_THREAD_MAP.set('raster', [/CompositorTileWorker/]); + CATEGORY_THREAD_MAP.set('renderer_compositor', [/^Compositor$/]); + CATEGORY_THREAD_MAP.set('renderer_main', [/^CrRendererMain$/]); + + const ALL_CATEGORIES = [...CATEGORY_THREAD_MAP.keys(), 'other']; + + function addValueToMap_(map, key, value) { + const oldValue = map.get(key) || 0; + map.set(key, oldValue + value); + } + + function* getCategories_(threadName) { + let isOther = true; + for (const [category, regexps] of CATEGORY_THREAD_MAP) { + for (const regexp of regexps) { + if (regexp.test(threadName)) { + if (category !== 'total_all') isOther = false; + yield category; + break; + } + } + } + if (isOther) yield 'other'; + } + + function isSubset_(regexps1, regexps2) { + for (const r1 of regexps1) { + if (regexps2.find(r2 => r2.toString() === r1.toString()) === undefined) { + return false; + } + } + return true; + } + + function addCpuUtilizationHistograms( + histograms, model, segments, shouldNormalize, segmentCostFunc, + histogramNameFunc, description, unit) { + if (!unit) unit = tr.b.Unit.byName.unitlessNumber; + const histogramMap = new Map(); + for (const category of ALL_CATEGORIES) { + const histogram = histograms.createHistogram( + histogramNameFunc(category), unit, [], { + binBoundaries: + tr.v.HistogramBinBoundaries.createExponential(1, 50, 20), + description, + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + histogramMap.set(category, histogram); + } + + // Add histogram breakdowns. + for (const [category, regexps] of CATEGORY_THREAD_MAP) { + const relatedCategories = new tr.v.d.RelatedNameMap(); + const histogram = histogramMap.get(category); + for (const [otherCategory, otherRegexps] of CATEGORY_THREAD_MAP) { + if (otherCategory === category) continue; + if (category !== 'all' && !isSubset_(otherRegexps, regexps)) continue; + const otherHistogram = histogramMap.get(otherCategory); + relatedCategories.set(otherCategory, otherHistogram.name); + } + if ([...relatedCategories.values()].length > 0) { + histogram.diagnostics.set('breakdown', relatedCategories); + } + } + + for (const segment of segments) { + const threadValues = new Map(); + // Compute and store CPU times per categories and thread name. + for (const thread of model.getAllThreads()) { + addValueToMap_( + threadValues, + thread.name || UNKNOWN_THREAD_NAME, + segmentCostFunc(thread, segment)); + } + const categoryValues = new Map(); + const breakdowns = new Map(); + for (const [threadName, coresPerSec] of threadValues) { + for (const category of getCategories_(threadName)) { + addValueToMap_(categoryValues, category, coresPerSec); + if (!breakdowns.has(category)) { + breakdowns.set(category, new tr.v.d.Breakdown()); + } + // TODO(chiniforooshan): We break down the CPU usage of each category + // by the thread name here. It will be more useful if we could add + // task names too. On the other hand, breaking down at task level may + // be too granular and we may end up with a ton of tiny slices that + // will not be that useful. Maybe we can break down by just top x + // tasks, or top x (thread, task) pairs? + // + // Another possbility to investigate is to break down by initiator + // type of the animation expectation. + breakdowns.get(category).set(threadName, coresPerSec); + } + } + + for (const category of ALL_CATEGORIES) { + let value = categoryValues.get(category) || 0; + if (shouldNormalize) value /= segment.duration; + const diagnostics = new tr.v.d.DiagnosticMap(); + const breakdown = breakdowns.get(category); + if (breakdown) diagnostics.set('breakdown', breakdown); + const histogram = histogramMap.get(category); + histogram.addSample(value, diagnostics); + } + } + } + + const SUMMARY_OPTIONS = { + percentile: [0.90, 0.95], + }; + + return { + addCpuUtilizationHistograms, + SUMMARY_OPTIONS, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization_test.html new file mode 100644 index 00000000000..3f58d2868f0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/cpu_utilization_test.html @@ -0,0 +1,280 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/rendering/cpu_utilization.html"> +<link rel="import" href="/tracing/model/user_model/segment.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('cpuPerSecond', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + // A slice during the animation with CPU duration 2. + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 1, end: 4, cpuStart: 0, cpuEnd: 2 })); + // A slice after the animation. + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 10, end: 12, cpuStart: 2, cpuEnd: 3 })); + + + const rendererMain = model.getOrCreateProcess(1).getOrCreateThread(0); + rendererMain.name = 'CrRendererMain'; + // A slice half of which intersects with the animation. + rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 5, end: 15, cpuStart: 0, cpuEnd: 8 })); + + const rendererIO = model.getOrCreateProcess(1).getOrCreateThread(1); + rendererIO.name = 'Chrome_ChildIOThread'; + rendererIO.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 5, end: 6, cpuStart: 1, cpuEnd: 2 })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addCpuUtilizationHistograms( + histograms, model, [new tr.model.um.Segment(0, 10)], true, + (thread, segment) => thread.getCpuTimeForRange(segment.boundsRange), + category => `thread_${category}_cpu_time_per_second`, 'description'); + + // Verify the browser and renderer main threads and IO threads CPU usage. + let hist = histograms.getHistogramNamed( + 'thread_browser_cpu_time_per_second'); + assert.closeTo(0.2, hist.min, 1e-6); + assert.closeTo(0.2, hist.max, 1e-6); + assert.closeTo(0.2, hist.average, 1e-6); + + hist = histograms.getHistogramNamed( + 'thread_renderer_main_cpu_time_per_second'); + assert.closeTo(0.4, hist.min, 1e-6); + assert.closeTo(0.4, hist.max, 1e-6); + assert.closeTo(0.4, hist.average, 1e-6); + + hist = histograms.getHistogramNamed('thread_IO_cpu_time_per_second'); + assert.closeTo(0.1, hist.min, 1e-6); + assert.closeTo(0.1, hist.max, 1e-6); + assert.closeTo(0.1, hist.average, 1e-6); + + // Also, verify fast_path threads, that includs IO threads and the browser + // main thread, but not the renderer main thread. + hist = histograms.getHistogramNamed( + 'thread_total_fast_path_cpu_time_per_second'); + assert.closeTo(0.3, hist.min, 1e-6); + assert.closeTo(0.3, hist.max, 1e-6); + assert.closeTo(0.3, hist.average, 1e-6); + + // Verify sum of all threads. + hist = histograms.getHistogramNamed('thread_total_all_cpu_time_per_second'); + assert.closeTo(0.7, hist.min, 1e-6); + assert.closeTo(0.7, hist.max, 1e-6); + assert.closeTo(0.7, hist.average, 1e-6); + }); + + test('cpuPerFrame', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + // A slice during the animation with CPU duration 2. + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 1, end: 4, cpuStart: 0, cpuEnd: 2 })); + // A slice after the animation. + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 10, end: 12, cpuStart: 2, cpuEnd: 3 })); + + + const rendererMain = model.getOrCreateProcess(1).getOrCreateThread(0); + rendererMain.name = 'CrRendererMain'; + // A slice half of which intersects with the animation. + rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 5, end: 15, cpuStart: 0, cpuEnd: 8 })); + + const rendererIO = model.getOrCreateProcess(1).getOrCreateThread(1); + rendererIO.name = 'Chrome_ChildIOThread'; + rendererIO.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 5, end: 6, cpuStart: 1, cpuEnd: 2 })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addCpuUtilizationHistograms( + histograms, model, [new tr.model.um.Segment(0, 10)], false, + (thread, segment) => thread.getCpuTimeForRange(segment.boundsRange), + category => `thread_${category}_cpu_time_per_frame`, 'description'); + + // Verify the browser and renderer main threads and IO threads CPU usage. + let hist = histograms.getHistogramNamed( + 'thread_browser_cpu_time_per_frame'); + assert.closeTo(2, hist.min, 1e-6); + assert.closeTo(2, hist.max, 1e-6); + assert.closeTo(2, hist.average, 1e-6); + + hist = histograms.getHistogramNamed( + 'thread_renderer_main_cpu_time_per_frame'); + assert.closeTo(4, hist.min, 1e-6); + assert.closeTo(4, hist.max, 1e-6); + assert.closeTo(4, hist.average, 1e-6); + + hist = histograms.getHistogramNamed('thread_IO_cpu_time_per_frame'); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(1, hist.max, 1e-6); + assert.closeTo(1, hist.average, 1e-6); + + // Also, verify fast_path threads, that includs IO threads and the browser + // main thread, but not the renderer main thread. + hist = histograms.getHistogramNamed( + 'thread_total_fast_path_cpu_time_per_frame'); + assert.closeTo(3, hist.min, 1e-6); + assert.closeTo(3, hist.max, 1e-6); + assert.closeTo(3, hist.average, 1e-6); + + // Verify sum of all threads. + hist = histograms.getHistogramNamed('thread_total_all_cpu_time_per_frame'); + assert.closeTo(7, hist.min, 1e-6); + assert.closeTo(7, hist.max, 1e-6); + assert.closeTo(7, hist.average, 1e-6); + }); + + test('multipleSegments', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 1, end: 4, cpuStart: 0, cpuEnd: 2 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 8, end: 12, cpuStart: 2, cpuEnd: 4 })); + + + const rendererMain = model.getOrCreateProcess(1).getOrCreateThread(0); + rendererMain.name = 'CrRendererMain'; + rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 5, end: 15, cpuStart: 0, cpuEnd: 8 })); + + const rendererIO = model.getOrCreateProcess(1).getOrCreateThread(1); + rendererIO.name = 'Chrome_ChildIOThread'; + rendererIO.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 5, end: 6, cpuStart: 1, cpuEnd: 2 })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addCpuUtilizationHistograms( + histograms, model, + [new tr.model.um.Segment(0, 5), new tr.model.um.Segment(5, 5)], false, + (thread, segment) => thread.getCpuTimeForRange(segment.boundsRange), + category => `thread_${category}_cpu_time_per_frame`, 'description'); + + // The first slice is in the first segment, using 2ms of CPU. The rest are + // in the second segment, using 1 + 4 + 1 = 6ms of CPU. + const hist = histograms.getHistogramNamed( + 'thread_total_all_cpu_time_per_frame'); + assert.closeTo(2, hist.min, 1e-6); + assert.closeTo(6, hist.max, 1e-6); + assert.closeTo(4, hist.average, 1e-6); + }); + + test('otherThreads', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + // A slice during the animation with CPU duration 2. + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 1, end: 4, cpuStart: 0, cpuEnd: 2 })); + // A slice after the animation. + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 10, end: 12, cpuStart: 2, cpuEnd: 3 })); + + + const thread1 = model.getOrCreateProcess(1).getOrCreateThread(0); + thread1.name = 'Thread1'; + // A slice half of which intersects with the animation. + thread1.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 5, end: 15, cpuStart: 0, cpuEnd: 8 })); + + const thread2 = model.getOrCreateProcess(1).getOrCreateThread(1); + thread2.name = 'Thread2'; + thread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 5, end: 6, cpuStart: 1, cpuEnd: 2 })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addCpuUtilizationHistograms( + histograms, model, [new tr.model.um.Segment(0, 10)], false, + (thread, segment) => thread.getCpuTimeForRange(segment.boundsRange), + category => `thread_${category}_cpu_time_per_frame`, 'description'); + + // Verify the browser and renderer main threads and IO threads CPU usage. + let hist = histograms.getHistogramNamed( + 'thread_browser_cpu_time_per_frame'); + assert.closeTo(2, hist.min, 1e-6); + assert.closeTo(2, hist.max, 1e-6); + assert.closeTo(2, hist.average, 1e-6); + + hist = histograms.getHistogramNamed('thread_other_cpu_time_per_frame'); + assert.closeTo(5, hist.min, 1e-6); + assert.closeTo(5, hist.max, 1e-6); + assert.closeTo(5, hist.average, 1e-6); + }); + + test('tasksPerFrame', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + // A slice during the animation with CPU duration 2. + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 1, end: 4, cpuStart: 0, cpuEnd: 2 })); + // A slice after the animation. + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 10, end: 12, cpuStart: 2, cpuEnd: 3 })); + + + const rendererMain = model.getOrCreateProcess(1).getOrCreateThread(0); + rendererMain.name = 'CrRendererMain'; + // A slice half of which intersects with the animation. + rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 5, end: 15, cpuStart: 0, cpuEnd: 8 })); + + const rendererIO = model.getOrCreateProcess(1).getOrCreateThread(1); + rendererIO.name = 'Chrome_ChildIOThread'; + rendererIO.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { start: 5, end: 6, cpuStart: 1, cpuEnd: 2 })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addCpuUtilizationHistograms( + histograms, model, [new tr.model.um.Segment(0, 10)], false, + (thread, segment) => + thread.getNumToplevelSlicesForRange(segment.boundsRange), + category => `tasks_per_frame_${category}`, 'description'); + + // Verify the browser and renderer main threads and IO threads number of + // tasks. + let hist = histograms.getHistogramNamed('tasks_per_frame_browser'); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(1, hist.max, 1e-6); + assert.closeTo(1, hist.average, 1e-6); + + hist = histograms.getHistogramNamed('tasks_per_frame_renderer_main'); + assert.closeTo(0.5, hist.min, 1e-6); + assert.closeTo(0.5, hist.max, 1e-6); + assert.closeTo(0.5, hist.average, 1e-6); + + hist = histograms.getHistogramNamed('tasks_per_frame_IO'); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(1, hist.max, 1e-6); + assert.closeTo(1, hist.average, 1e-6); + + // Also, verify fast_path threads, that includs IO threads and the browser + // main thread, but not the renderer main thread. + hist = histograms.getHistogramNamed('tasks_per_frame_total_fast_path'); + assert.closeTo(2, hist.min, 1e-6); + assert.closeTo(2, hist.max, 1e-6); + assert.closeTo(2, hist.average, 1e-6); + + // Verify sum of all threads. + hist = histograms.getHistogramNamed('tasks_per_frame_total_all'); + assert.closeTo(2.5, hist.min, 1e-6); + assert.closeTo(2.5, hist.max, 1e-6); + assert.closeTo(2.5, hist.average, 1e-6); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time.html new file mode 100644 index 00000000000..1c465423654 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time.html @@ -0,0 +1,272 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/unit_scale.html"> +<link rel="import" href="/tracing/metrics/rendering/cpu_utilization.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/model/user_model/segment.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +/** + * @fileoverview This file contains implementations of the following metrics. + * + * frame_times + * =========== + * The distribution of durations between consecutive display compositor's swap + * buffer calls, or DRM page flips on ChromeOS devices, during animations. + * + * percentage_smooth + * ================= + * The percentage of frame_times that are less than 17ms. + * + * TODO(chiniforooshan): This metric weighs all frames equally. So, e.g. + * percentage_smooth is lower (worse) if we have 10 100ms-frames instead of 5 + * 1s-frames. I think it makes more sense to compute the percentage of + * non-smooth time during animation. + * + * frame_lengths (Android only) + * ============================ + * frame_times in vsyncs instead of milli-seconds. In other words, frame_lengths + * is the distribution of durations between consecutive display compositor's + * swap buffer calls, in terms of vsyncs. Short frames (ones shorter than half + * of the refresh period) are filtered out, unlike in frame_times. + * + * avg_surface_fps (Android only) + * ============================== + * Average number of frames, ignoring short frames, per second during + * animations. + * + * jank_count (Android only) + * ========================= + * The number of times that frame lengths are increased, during animations. For + * example, if frame lengths are 1, 1, 1, 2, 3, 1, 1 vsyncs, then jank_count + * will be 2 (1 -> 2 and 2 -> 3). + * + * ui_frame_times + * ============== + * The distribution of durations between consecutive UI compositor frame's + * presentation times, during UI animations. In ChromeOS, if Ash uses its own + * instance of ui::compositor, then frames submitted by that compositor will be + * used. Otherwise, frames submitted by the browser's ui::compositor will be + * used. + * + * TODO(crbug.com/896231): we may want to consider reporting frame_times for all + * ui::compositors separately, e.g. as ash_frame_times and browser_frame_times. + * + * ui_percentage_smooth + * ==================== + * The percentage of ui_frame_times that are less than 17ms. + */ +tr.exportTo('tr.metrics.rendering', function() { + // Various tracing events. + const DISPLAY_EVENT = 'BenchmarkInstrumentation::DisplayRenderingStats'; + const DRM_EVENT = 'DrmEventFlipComplete'; + const SURFACE_FLINGER_EVENT = 'vsync_before'; + const COMPOSITOR_FRAME_PRESENTED_EVENT = 'FramePresented'; + + // In computing frame_lengths, avg_surface_fps, and jank_count, frames that + // are shorter than half a vsync are ignored. + const MIN_FRAME_LENGTH = 0.5; + + // In computing the number of janks, frame length differences that are at + // least PAUSE_THRESHOLD vsyncs are considered pauses, not janks. + const PAUSE_THRESHOLD = 20; + + const ASH_ENVIRONMENT = 'ash'; + const BROWSER_ENVIRONMENT = 'browser'; + + function getDisplayCompositorPresentationEvents_(modelHelper) { + if (!modelHelper || !modelHelper.browserProcess) return []; + // On ChromeOS, DRM events, if they exist, are the source of truth. On + // Android, Surface Flinger events are the source of truth. Otherwise, look + // for display rendering stats. With viz, display rendering stats are + // emitted from the GPU process; otherwise, they are emitted from the + // browser process. + let events = []; + if (modelHelper.surfaceFlingerProcess) { + events = [...modelHelper.surfaceFlingerProcess.findTopmostSlicesNamed( + SURFACE_FLINGER_EVENT)]; + if (events.length > 0) return events; + } + if (modelHelper.gpuHelper) { + const gpuProcess = modelHelper.gpuHelper.process; + events = [...gpuProcess.findTopmostSlicesNamed(DRM_EVENT)]; + if (events.length > 0) return events; + events = [...gpuProcess.findTopmostSlicesNamed(DISPLAY_EVENT)]; + if (events.length > 0) return events; + } + return [...modelHelper.browserProcess.findTopmostSlicesNamed( + DISPLAY_EVENT)]; + } + + function getUIPresentationEvents_(modelHelper) { + if (!modelHelper || !modelHelper.browserProcess) return []; + + const legacyEvents = []; + const eventsByEnvironment = {}; + eventsByEnvironment[ASH_ENVIRONMENT] = []; + eventsByEnvironment[BROWSER_ENVIRONMENT] = []; + for (const event of modelHelper.browserProcess.findTopmostSlicesNamed( + COMPOSITOR_FRAME_PRESENTED_EVENT)) { + if (!('environment' in event.args)) { + // For chrome versions before crrev.com/c/1282039. + legacyEvents.push(event); + } else { + eventsByEnvironment[event.args.environment].push(event); + } + } + + if (eventsByEnvironment[ASH_ENVIRONMENT].length > 0) { + return eventsByEnvironment[ASH_ENVIRONMENT]; + } + if (eventsByEnvironment[BROWSER_ENVIRONMENT].length > 0) { + return eventsByEnvironment[BROWSER_ENVIRONMENT]; + } + return legacyEvents; + } + + function addSurfaceFlingerHistograms_( + histograms, frameSegments, refreshPeriod) { + let frameLengths = frameSegments.map(x => x.duration / refreshPeriod); + frameLengths = frameLengths.filter(length => length >= MIN_FRAME_LENGTH); + histograms.createHistogram( + 'frame_lengths', + tr.b.Unit.byName.unitlessNumber_smallerIsBetter, + frameLengths, + { binBoundaries: tr.v.HistogramBinBoundaries.createLinear(0, 5, 20), + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + description: 'Frame times in vsyncs.' }); + + histograms.createHistogram( + 'avg_surface_fps', + tr.b.Unit.byName.unitlessNumber_biggerIsBetter, + frameLengths.length / tr.b.convertUnit( + tr.b.math.Statistics.sum(frameSegments, x => x.duration), + tr.b.UnitScale.TIME.MILLI_SEC, tr.b.UnitScale.TIME.SEC), + { description: 'Average frames per second.', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + + let jankCount = 0; + for (let i = 1; i < frameLengths.length; i++) { + const change = Math.round(frameLengths[i] - frameLengths[i - 1]); + if (change > 0 && change < PAUSE_THRESHOLD) jankCount++; + } + histograms.createHistogram( + 'jank_count', + tr.b.Unit.byName.unitlessNumber_smallerIsBetter, + jankCount, + { description: 'Number of changes in frame rate.', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + } + + function computeFrameSegments_(timestamps, segments) { + // We use filterArray for the sake of a cleaner code. The time complexity + // will be O(m + n log m), where m is |timestamps| and n is |segments|. + // Alternatively, we could directly loop through the timestamps and segments + // here for a slightly better time complexity of O(m + n). + const frameSegments = []; + for (const segment of segments) { + const filtered = segment.boundsRange.filterArray(timestamps, x => x[0]); + for (let i = 1; i < filtered.length; i++) { + const duration = filtered[i][1] - filtered[i - 1][1]; + frameSegments.push( + new tr.model.um.Segment(filtered[i - 1][0], duration)); + } + } + return frameSegments; + } + + function addBasicFrameTimeHistograms_(histograms, frameSegments, prefix) { + // TODO(chiniforooshan): Figure out what kind of break down makes sense + // here. Perhaps break down by tasks in the Viz/Browser process? + const frameTimes = frameSegments.map(x => x.duration); + histograms.createHistogram( + `${prefix}frame_times`, + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + frameTimes, + { binBoundaries: tr.v.HistogramBinBoundaries.createLinear(0, 50, 20), + description: 'Raw frame times.', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + histograms.createHistogram( + `${prefix}percentage_smooth`, + tr.b.Unit.byName.unitlessNumber_biggerIsBetter, + 100 * tr.b.math.Statistics.sum(frameTimes, (x => (x < 17 ? 1 : 0))) / + frameTimes.length, + { description: 'Percentage of frames that were hitting 60 FPS.', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + } + + function addFrameTimeHistograms(histograms, model, segments) { + const events = getDisplayCompositorPresentationEvents_( + model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper)); + if (!events) return; + + // Presentation Timestamps should be sorted. + const timestamps = events.map( + event => [event.start, + event.title !== DRM_EVENT ? event.start : ( + tr.b.convertUnit( + event.args.data['vblank.tv_sec'], + tr.b.UnitScale.TIME.SEC, tr.b.UnitScale.TIME.MILLI_SEC) + + tr.b.convertUnit( + event.args.data['vblank.tv_usec'], + tr.b.UnitScale.TIME.MICRO_SEC, tr.b.UnitScale.TIME.MILLI_SEC))] + ); + const frameSegments = computeFrameSegments_(timestamps, segments); + addBasicFrameTimeHistograms_(histograms, frameSegments, ''); + tr.metrics.rendering.addCpuUtilizationHistograms( + histograms, model, frameSegments, false, + (thread, segment) => thread.getCpuTimeForRange(segment.boundsRange), + category => `thread_${category}_cpu_time_per_frame`, + 'CPU cores of a thread group per frame', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter); + tr.metrics.rendering.addCpuUtilizationHistograms( + histograms, model, frameSegments, false, + (thread, segment) => + thread.getNumToplevelSlicesForRange(segment.boundsRange), + category => `tasks_per_frame_${category}`, + 'Number of tasks of a thread group per frame', + tr.b.Unit.byName.unitlessNumber_smallerIsBetter); + + // If Surface Flinger information is captured, add Surface Flinger + // histograms. + for (const metadata of model.metadata) { + if (metadata.value && metadata.value.surface_flinger) { + addSurfaceFlingerHistograms_( + histograms, frameSegments, + metadata.value.surface_flinger.refresh_period); + return; + } + } + } + + function addUIFrameTimeHistograms(histograms, model, segments) { + const events = getUIPresentationEvents_( + model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper)); + if (!events) return; + + // Presentation Timestamps should be sorted. + const timestamps = events.map(event => [event.start, event.start]); + const frameSements = computeFrameSegments_(timestamps, segments); + addBasicFrameTimeHistograms_(histograms, frameSements, 'ui_'); + } + + return { + addFrameTimeHistograms, + addUIFrameTimeHistograms, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time_test.html new file mode 100644 index 00000000000..0304a5cc0c5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/frame_time_test.html @@ -0,0 +1,301 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/rendering/frame_time.html"> +<link rel="import" href="/tracing/model/user_model/segment.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('frameTimes', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + // Add four swap buffer events, at times 1, 2, 19, 21 + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 1, end: 1 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 2, end: 2 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 19, end: 19 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 21, end: 21 })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addFrameTimeHistograms( + histograms, model, [new tr.model.um.Segment(0, 20)]); + + // The fourth frame is outside the interaction perdiod and should be + // discarded. The durations between the remaining three frames are 1 and 17. + let hist = histograms.getHistogramNamed('frame_times'); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(17, hist.max, 1e-6); + assert.closeTo(9, hist.average, 1e-6); + + // One of the two frame times is not smooth. + hist = histograms.getHistogramNamed('percentage_smooth'); + assert.closeTo(50, hist.min, 1e-6); + assert.closeTo(50, hist.max, 1e-6); + assert.closeTo(50, hist.average, 1e-6); + }); + + test('frameTimes_drmEvents', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + + const gpuMain = model.getOrCreateProcess(1).getOrCreateThread(0); + gpuMain.name = 'CrGpuMain'; + gpuMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'DrmEventFlipComplete', start: 1, end: 1, + args: { data: { 'vblank.tv_sec': 0, 'vblank.tv_usec': 1000 } } })); + gpuMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'DrmEventFlipComplete', start: 3, end: 3, + args: { data: { 'vblank.tv_sec': 0, 'vblank.tv_usec': 2000 } } })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addFrameTimeHistograms( + histograms, model, [new tr.model.um.Segment(0, 6)]); + + // When computing frame times from DRM events, VBlank times should be used. + const hist = histograms.getHistogramNamed('frame_times'); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(1, hist.max, 1e-6); + assert.closeTo(1, hist.average, 1e-6); + }); + + test('frameTimes_surfaceFlingerEvents', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 1, end: 1 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 2, end: 2 })); + + const surfaceFlingerProcess = model.getOrCreateProcess(2); + surfaceFlingerProcess.name = 'SurfaceFlinger'; + const surfaceFlingerThread = surfaceFlingerProcess.getOrCreateThread(2); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 1, end: 1})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 3, end: 3})); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addFrameTimeHistograms( + histograms, model, [new tr.model.um.Segment(0, 6)]); + + // Data from the Surface Flinger process should be used if it exists. + const hist = histograms.getHistogramNamed('frame_times'); + assert.closeTo(2, hist.min, 1e-6); + assert.closeTo(2, hist.max, 1e-6); + assert.closeTo(2, hist.average, 1e-6); + }); + + test('frameLengths', function() { + const model = tr.c.TestUtils.newModel((model) => { + model.metadata = [{ + name: 'metadata', + value: { + surface_flinger: { + refresh_period: 3, + }, + }, + }]; + + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + + const surfaceFlingerProcess = model.getOrCreateProcess(2); + surfaceFlingerProcess.name = 'SurfaceFlinger'; + const surfaceFlingerThread = surfaceFlingerProcess.getOrCreateThread(2); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 1, end: 1})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 4, end: 4})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 10, end: 10})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 13, end: 13})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 14, end: 14})); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addFrameTimeHistograms( + histograms, model, [new tr.model.um.Segment(0, 20)]); + + // Frame lengths are 3/3, 6/3, 3/3, and 1/3. The last one is too small and + // should be filtered out. So, the final result is [1, 2, 1]. + const hist = histograms.getHistogramNamed('frame_lengths'); + assert.closeTo(3, hist.numValues, 1e-6); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(2, hist.max, 1e-6); + assert.closeTo(4, hist.sum, 1e-6); + }); + + test('avgSurfaceFPS', function() { + const model = tr.c.TestUtils.newModel((model) => { + model.metadata = [{ + name: 'metadata', + value: { + surface_flinger: { + refresh_period: 3, + }, + }, + }]; + + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + + const surfaceFlingerProcess = model.getOrCreateProcess(2); + surfaceFlingerProcess.name = 'SurfaceFlinger'; + const surfaceFlingerThread = surfaceFlingerProcess.getOrCreateThread(2); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 1, end: 1})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 4, end: 4})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 10, end: 10})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 13, end: 13})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 14, end: 14})); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addFrameTimeHistograms( + histograms, model, [new tr.model.um.Segment(0, 20)]); + + // We have 3 frames (ignoring the very short ones) in 13 milliseconds. + const hist = histograms.getHistogramNamed('avg_surface_fps'); + assert.closeTo(1, hist.numValues, 1e-6); + assert.closeTo(3 / 0.013, hist.min, 1e-6); + }); + + test('jankCount', function() { + const model = tr.c.TestUtils.newModel((model) => { + model.metadata = [{ + name: 'metadata', + value: { + surface_flinger: { + refresh_period: 3, + }, + }, + }]; + + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + + const surfaceFlingerProcess = model.getOrCreateProcess(2); + surfaceFlingerProcess.name = 'SurfaceFlinger'; + const surfaceFlingerThread = surfaceFlingerProcess.getOrCreateThread(2); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 1, end: 1})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 4, end: 4})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 7, end: 7})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 13, end: 13})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 16, end: 16})); + surfaceFlingerThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'vsync_before', start: 79, end: 79})); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addFrameTimeHistograms( + histograms, model, [new tr.model.um.Segment(0, 100)]); + + // There is 1 jank in [1, 1, 2, 1, 21]. The last long frame is a pause. + const hist = histograms.getHistogramNamed('jank_count'); + assert.closeTo(1, hist.numValues, 1e-6); + assert.closeTo(1, hist.min, 1e-6); + }); + + test('uiFrameTimes', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + // Add four swap buffer events, at times 1, 2, 19, 21 + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'FramePresented', start: 1, end: 1 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'FramePresented', start: 2, end: 2 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'FramePresented', start: 19, end: 19 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'FramePresented', start: 21, end: 21 })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addUIFrameTimeHistograms( + histograms, model, [new tr.model.um.Segment(0, 20)]); + + // The fourth frame is outside the interaction perdiod and should be + // discarded. The durations between the remaining three frames are 1 and 17. + let hist = histograms.getHistogramNamed('ui_frame_times'); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(17, hist.max, 1e-6); + assert.closeTo(9, hist.average, 1e-6); + + // One of the two frame times is not smooth. + hist = histograms.getHistogramNamed('ui_percentage_smooth'); + assert.closeTo(50, hist.min, 1e-6); + assert.closeTo(50, hist.max, 1e-6); + assert.closeTo(50, hist.average, 1e-6); + }); + + test('uiFrameTimesWithEnvironmentArg', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + // Add four swap buffer events, at times 1, 2, 3, 19 + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + title: 'FramePresented', + start: 1, + end: 1, + args: { environment: 'ash' } + })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + title: 'FramePresented', + start: 2, + end: 2, + args: { environment: 'ash' } + })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + title: 'FramePresented', + start: 3, + end: 3, + args: { environment: 'browser' } + })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + title: 'FramePresented', + start: 19, + end: 19, + args: { environment: 'ash' } + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addUIFrameTimeHistograms( + histograms, model, [new tr.model.um.Segment(0, 20)]); + + // The 3rd frame is from 'browser' and should be discarded. Remaining + // timestamps are 1, 2, and 19 which indicate frame times of 1 and 17. + const hist = histograms.getHistogramNamed('ui_frame_times'); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(17, hist.max, 1e-6); + assert.closeTo(2, hist.numValues, 1e-6); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency.html new file mode 100644 index 00000000000..cbd691dacf3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency.html @@ -0,0 +1,138 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/base/unit.html"> +<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html"> +<link rel="import" href="/tracing/model/async_slice_group.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +/** + * @fileoverview This file contains implementations of the following metrics. + * + * input_event_latency + * =================== + * The distribution of durations between the time when an input event is + * generated to the time when the GPU service swaps buffers due to the input + * event. The time when an input is generated is the first of the following + * three timestamps that we can get: + * + * 1. Original kernel timestamp of the input event. + * 2. Timestamp when the UI event is created. + * 3. Timestamp when the input event is sent from RenderWidgetHost to the + * renderer. + * + * main_thread_scroll_latency + * ========================== + * The distribution of durations between the time when the main thread scroll + * listener update is begun to the time when the GPU service swaps buffers due + * to the scroll event. + * + * first_gesture_scroll_update_latency + * =================================== + * This shows the latency, as defined in input_event_latency, of the first + * gesture scroll update input event. The first event can often get delayed by + * work related to page loading. + */ +tr.exportTo('tr.metrics.rendering', function() { + // Interesting latency info component names. + const BEGIN_SCROLL_UPDATE_COMP_NAME = + 'LATENCY_BEGIN_SCROLL_LISTENER_UPDATE_MAIN_COMPONENT'; + const END_COMP_NAME = 'INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT'; + + function* iterAsyncEvents_(processHelpers, ranges, processEventFn) { + for (const processHelper of processHelpers) { + const process = processHelper.process; + for (const event of process.getDescendantEventsInSortedRanges( + ranges, container => container instanceof tr.model.AsyncSliceGroup)) { + yield* processEventFn(event); + } + } + } + + function* processInputLatencyEvent(event) { + if (!(event instanceof tr.e.cc.InputLatencyAsyncSlice)) return; + const latency = event.inputLatency; + if (latency === undefined) return; + yield tr.b.Unit.timestampFromUs(latency); + } + + function* processLatencyEvent(event) { + if (event.title !== 'Latency::ScrollUpdate' || + !('data' in event.args) || !(END_COMP_NAME in event.args.data)) { + return; + } + const data = event.args.data; + const endTime = data[END_COMP_NAME].time; + if (BEGIN_SCROLL_UPDATE_COMP_NAME in data) { + yield tr.b.Unit.timestampFromUs( + endTime - data[BEGIN_SCROLL_UPDATE_COMP_NAME].time); + } else { + throw new Error('LatencyInfo has no begin component'); + } + } + + function* processGestureScrollUpdateLatencyEvent(event) { + if (event.title !== 'InputLatency::GestureScrollUpdate') return; + if (!(event instanceof tr.e.cc.InputLatencyAsyncSlice)) { + throw new Error('Gesture scroll update latency event is not an ' + + 'instance of tr.e.cc.InputLatencyAsyncSlice'); + } + const latency = event.inputLatency; + if (latency === undefined) return; + yield [event.start, tr.b.Unit.timestampFromUs(latency)]; + } + + function addLatencyHistograms(histograms, model, segments) { + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (!modelHelper) return; + + const ranges = segments.map(s => s.boundsRange); + const inputEventLatencies = [...iterAsyncEvents_( + modelHelper.browserHelpers, ranges, processInputLatencyEvent)]; + histograms.createHistogram( + 'input_event_latency', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + inputEventLatencies, + { binBoundaries: tr.v.HistogramBinBoundaries.createLinear(0, 50, 20), + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + description: 'Input event latencies.' }); + + const mainThreadScrollLatencies = [...iterAsyncEvents_( + Object.values(modelHelper.rendererHelpers), ranges, + processLatencyEvent)]; + histograms.createHistogram( + 'main_thread_scroll_latency', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + mainThreadScrollLatencies, + { binBoundaries: tr.v.HistogramBinBoundaries.createLinear(0, 50, 50), + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + description: 'Main thread scroll latencies.' }); + + const gestureScrollUpdateLatencies = [...iterAsyncEvents_( + modelHelper.browserHelpers, ranges, + processGestureScrollUpdateLatencyEvent)].sort((x, y) => x[0] - y[0]); + if (gestureScrollUpdateLatencies.length) { + histograms.createHistogram( + 'first_gesture_scroll_update_latency', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + gestureScrollUpdateLatencies[0][1], + { binBoundaries: tr.v.HistogramBinBoundaries.createLinear(0, 50, 20), + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + description: 'Latency of the first gesture scroll update.' }); + } + } + + return { + addLatencyHistograms, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency_test.html new file mode 100644 index 00000000000..4a68b8e4655 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/latency_test.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/rendering/latency.html"> +<link rel="import" href="/tracing/model/user_model/segment.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('inputEventLatency', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browser = model.getOrCreateProcess(0).getOrCreateThread(0); + browser.name = 'CrBrowserMain'; + browser.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({ + title: 'InputLatency::GestureScrollUpdate', + start: 9, end: 10, + args: { + data: { + INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 10000}, + INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: {time: 9000} + } + } + })); + browser.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({ + title: 'InputLatency::GestureScrollUpdate', + start: 7, end: 8, + args: { + data: { + INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 8000}, + INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: {time: 7000}, + INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT: {time: 6000}, + } + } + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addLatencyHistograms( + histograms, model, [new tr.model.um.Segment(0, 10)]); + + // The first input latency is 10 - 9 = 1. The second input latency is + // 8 - 6 = 2. + const hist = histograms.getHistogramNamed('input_event_latency'); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(2, hist.max, 1e-6); + assert.closeTo(1.5, hist.average, 1e-6); + }); + + test('mainThreadScrollLatency', function() { + const model = tr.c.TestUtils.newModel((model) => { + model.getOrCreateProcess(0).getOrCreateThread(0).name = 'CrBrowserMain'; + const renderer = model.getOrCreateProcess(1).getOrCreateThread(0); + renderer.name = 'CrRendererMain'; + renderer.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({ + title: 'Latency::ScrollUpdate', + start: 9, end: 10, + args: { + data: { + INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 10000}, + LATENCY_BEGIN_SCROLL_LISTENER_UPDATE_MAIN_COMPONENT: {time: 9000} + } + } + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addLatencyHistograms( + histograms, model, [new tr.model.um.Segment(0, 10)]); + + const hist = histograms.getHistogramNamed('main_thread_scroll_latency'); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(1, hist.max, 1e-6); + assert.closeTo(1, hist.average, 1e-6); + }); + + test('firstGestureScrollUpdateLatency', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browser = model.getOrCreateProcess(0).getOrCreateThread(0); + browser.name = 'CrBrowserMain'; + browser.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({ + title: 'InputLatency::GestureScrollUpdate', + start: 10, end: 11, + args: { + data: { + INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 10000}, + INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: {time: 9000} + } + } + })); + browser.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({ + title: 'InputLatency::GestureScrollUpdate', + start: 7, end: 8, + args: { + data: { + INPUT_EVENT_GPU_SWAP_BUFFER_COMPONENT: {time: 8000}, + INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: {time: 7000}, + INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT: {time: 6000}, + } + } + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addLatencyHistograms( + histograms, model, [new tr.model.um.Segment(0, 20)]); + + // The chronologically first gesture scroll update latency is 8 - 6 = 2. + const hist = histograms.getHistogramNamed( + 'first_gesture_scroll_update_latency'); + assert.closeTo(2, hist.min, 1e-6); + assert.closeTo(2, hist.max, 1e-6); + assert.closeTo(2, hist.average, 1e-6); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline.html new file mode 100644 index 00000000000..1b68e5d6a31 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline.html @@ -0,0 +1,258 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/value/diagnostics/breakdown.html"> + +<script> +'use strict'; + +/** + * @fileoverview This file contains implementations of the following metrics. + * + * TODO(crbug.com/872334): document pipeline:* metrics here. + */ +tr.exportTo('tr.metrics.rendering', function() { + function eventIsValidGraphicsEvent_(event, eventMap) { + if (event.title !== 'Graphics.Pipeline' || !event.bindId || !event.args || + !event.args.step) { + return false; + } + const bindId = event.bindId; + if (eventMap.has(bindId) && event.args.step in eventMap.get(bindId)) { + // It is possible for a client to submit multiple compositor frames for + // one begin-message. So most steps can be present multiple times. + // However, a begin-frame is issued only once, and received only once. So + // these steps should not be repeated. + if (event.args.step === 'IssueBeginFrame' || + event.args.step === 'ReceiveBeginFrame') { + throw new Error('Unexpected duplicate step: ' + event.args.step); + } + return false; + } + return true; + } + + function generateBreakdownForCompositorPipelineInClient_(flow) { + const breakdown = new tr.v.d.Breakdown(); + breakdown.set('time before GenerateRenderPass', + flow.GenerateRenderPass.start - flow.ReceiveBeginFrame.start); + breakdown.set('GenerateRenderPass duration', + flow.GenerateRenderPass.duration); + breakdown.set('GenerateCompositorFrame duration', + flow.GenerateCompositorFrame.duration); + breakdown.set('SubmitCompositorFrame duration', + flow.SubmitCompositorFrame.duration); + return breakdown; + } + + function generateBreakdownForCompositorPipelineInService_(flow) { + const breakdown = new tr.v.d.Breakdown(); + breakdown.set('Processing CompositorFrame on reception', + flow.ReceiveCompositorFrame.duration); + breakdown.set('Delay before SurfaceAggregation', + flow.SurfaceAggregation.start - flow.ReceiveCompositorFrame.end); + breakdown.set('SurfaceAggregation duration', + flow.SurfaceAggregation.duration); + return breakdown; + } + + function generateBreakdownForDraw_(drawEvent) { + const breakdown = new tr.v.d.Breakdown(); + for (const slice of drawEvent.subSlices) { + breakdown.set(slice.title, slice.duration); + } + return breakdown; + } + + function getDisplayCompositorThread_(model) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + const gpuHelper = chromeHelper.gpuHelper; + if (gpuHelper) { + const thread = + gpuHelper.process.findAtMostOneThreadNamed('VizCompositorThread'); + if (thread) { + return thread; + } + } + if (!chromeHelper.browserProcess) return null; + return chromeHelper.browserProcess.findAtMostOneThreadNamed( + 'CrBrowserMain'); + } + + function getRasterTaskTimes(sourceFrameNumber, model) { + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + + const renderers = modelHelper.telemetryHelper.renderersWithIR; + const rasterThreads = renderers[0].rasterWorkerThreads; + + let earliestStart = undefined; + let lastEnd = undefined; + for (const rasterThread of rasterThreads) { + for (const slice of [...rasterThread. + findTopmostSlicesNamed('TaskGraphRunner::RunTask')]) { + if (slice.args && + slice.args.source_frame_number_ && + slice.args.source_frame_number_ === sourceFrameNumber) { + if (earliestStart === undefined || slice.start < earliestStart) { + earliestStart = slice.start; + } + if (lastEnd === undefined || slice.end > lastEnd) { + lastEnd = slice.end; + } + } + } + } + return {start: earliestStart, end: lastEnd}; + } + + function addPipelineHistograms(histograms, model, segments) { + const ranges = segments.map(s => s.boundsRange); + const bindEvents = new Map(); + for (const thread of model.getAllThreads()) { + for (const event of thread.sliceGroup.childEvents()) { + if (!eventIsValidGraphicsEvent_(event, bindEvents)) continue; + for (const range of ranges) { + if (range.containsExplicitRangeInclusive(event.start, event.end)) { + if (!bindEvents.has(event.bindId)) bindEvents.set(event.bindId, {}); + break; + } + } + if (bindEvents.has(event.bindId)) { + bindEvents.get(event.bindId)[event.args.step] = event; + } + } + } + + const dcThread = getDisplayCompositorThread_(model); + const drawEvents = {}; + if (dcThread) { + const events = + [...dcThread.findTopmostSlicesNamed('Graphics.Pipeline.DrawAndSwap')]; + for (const segment of segments) { + const filteredEvents = segment.boundsRange.filterArray(events, + evt => evt.start); + for (const event of filteredEvents) { + if ((event.args && event.args.status === 'canceled') || + !event.id.startsWith(':ptr:')) { + continue; + } + const id = parseInt(event.id.substring(5), 16); + if (id in drawEvents) { + throw new Error('Duplicate draw events: ' + id); + } + drawEvents[id] = event; + } + } + } + + const issueToReceipt = histograms.createHistogram( + 'pipeline:begin_frame_transport', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [], { + description: 'Latency of begin-frame message from the display ' + + 'compositor to the client, including the IPC latency and task-' + + 'queue time in the client.', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + const issueToRasterStart = histograms.createHistogram( + 'pipeline:begin_frame_to_raster_start', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [], { + description: 'Latency between begin-frame message and ' + + 'the beginning of the first CompositorTask run in the compositor.', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + const issueToRasterEnd = histograms.createHistogram( + 'pipeline:begin_frame_to_raster_end', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [], { + description: 'Latency between begin-frame message and ' + + 'the end of the last CompositorTask run in the compositor.', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + const receiptToSubmit = histograms.createHistogram( + 'pipeline:begin_frame_to_frame_submission', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [], { + description: 'Latency between begin-frame reception and ' + + 'CompositorFrame submission in the renderer.', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + const submitToAggregate = histograms.createHistogram( + 'pipeline:frame_submission_to_display', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [], { + description: 'Latency between CompositorFrame submission in the ' + + 'renderer to display in the display-compositor, including IPC ' + + 'latency, task-queue time in the display-compositor, and ' + + 'additional processing (e.g. surface-sync etc.)', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + const aggregateToDraw = histograms.createHistogram( + 'pipeline:draw', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + [], { + description: 'How long it takes for the gpu-swap step.', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + + for (const flow of bindEvents.values()) { + // Report only for the cases that go through the complete pipeline. + if (!flow.IssueBeginFrame || !flow.ReceiveBeginFrame || + !flow.SubmitCompositorFrame || !flow.SurfaceAggregation) { + continue; + } + + const sourceFrameNumber = flow.SubmitCompositorFrame.parentSlice + .args.source_frame_number_; + + const rasterDuration = + getRasterTaskTimes(sourceFrameNumber, model); + + issueToReceipt.addSample(flow.ReceiveBeginFrame.start - + flow.IssueBeginFrame.start); + receiptToSubmit.addSample( + flow.SubmitCompositorFrame.end - flow.ReceiveBeginFrame.start, + {breakdown: generateBreakdownForCompositorPipelineInClient_(flow)}); + submitToAggregate.addSample( + flow.SurfaceAggregation.end - flow.SubmitCompositorFrame.end, + {breakdown: generateBreakdownForCompositorPipelineInService_(flow)}); + + if (rasterDuration.start && rasterDuration.end) { + const receiveToStart = rasterDuration.start - + flow.ReceiveBeginFrame.start; + const receiveToEnd = rasterDuration.end - flow.ReceiveBeginFrame.end; + + // receiveToStart can be negative if the earliest raster task for + // a frame starts before receiveBeginFrame starts. + // The same is true for receiveToEnd + // Only positive samples are added. + if (receiveToEnd > 0) { + issueToRasterStart.addSample(receiveToStart > 0 ? receiveToStart : 0); + issueToRasterEnd.addSample(receiveToEnd); + } + } + if (flow.SurfaceAggregation.args && + flow.SurfaceAggregation.args.display_trace) { + const displayTrace = flow.SurfaceAggregation.args.display_trace; + if (!(displayTrace in drawEvents)) continue; + const drawEvent = drawEvents[displayTrace]; + aggregateToDraw.addSample(drawEvent.duration, + {breakdown: generateBreakdownForDraw_(drawEvent)}); + } + } + } + + return { + addPipelineHistograms, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline_test.html new file mode 100644 index 00000000000..332767c0e79 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pipeline_test.html @@ -0,0 +1,366 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/rendering/pipeline.html"> +<link rel="import" href="/tracing/model/user_model/segment.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function addPipelineForOneFrame(compositor, renderer, rasterWorker, + id, frame, displayTrace) { + const EVENT_NAME = 'Graphics.Pipeline'; + if (frame.IssueBeginFrame) { + compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: EVENT_NAME, + start: frame.IssueBeginFrame, duration: 1, bindId: id, + args: {step: 'IssueBeginFrame'}})); + } + if (frame.ReceiveBeginFrame) { + renderer.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: EVENT_NAME, + start: frame.ReceiveBeginFrame, duration: 1, bindId: id, + args: {step: 'ReceiveBeginFrame'}})); + } + renderer.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: EVENT_NAME, + start: frame.GenerateRenderPass, duration: 1, bindId: id, + args: {step: 'GenerateRenderPass'}})); + renderer.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: EVENT_NAME, + start: frame.GenerateCompositorFrame, duration: 1, bindId: id, + args: {step: 'GenerateCompositorFrame'}})); + renderer.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: EVENT_NAME, + start: frame.SubmitCompositorFrame, duration: 1, bindId: id, + args: {source_frame_number_: id}})); + renderer.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: EVENT_NAME, + start: frame.SubmitCompositorFrame, duration: 1, bindId: id, + args: {step: 'SubmitCompositorFrame'}})); + compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: EVENT_NAME, + start: frame.ReceiveCompositorFrame, duration: 1, bindId: id, + args: {step: 'ReceiveCompositorFrame'}})); + compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: EVENT_NAME, + start: frame.SurfaceAggregation, duration: 1, bindId: id, + args: {step: 'SurfaceAggregation', display_trace: displayTrace}})); + renderer.sliceGroup.createSubSlices(); + rasterWorker.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'TaskGraphRunner::RunTask', + start: frame.rasterTaskStart, duration: 6, bindId: id, + args: {source_frame_number_: id}})); + } + + function addDrawSlice(compositor, displayTrace, start, steps, opt_args) { + const EVENT_NAME = 'Graphics.Pipeline.DrawAndSwap'; + let totalDuration = 0; + for (const duration of Object.values(steps)) { + totalDuration += duration; + } + const slice = tr.c.TestUtils.newAsyncSliceNamed( + EVENT_NAME, start, totalDuration); + slice.id = ':ptr:' + displayTrace; + slice.args = opt_args; + compositor.sliceGroup.pushSlice(slice); + totalDuration = 0; + for (const step in steps) { + slice.subSlices.push(tr.c.TestUtils.newAsyncSliceNamed( + step, start + totalDuration, steps[step])); + totalDuration += steps[step]; + } + } + + test('graphicsPipeline', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + const rendererCompositor = + model.getOrCreateProcess(1).getOrCreateThread(1); + rendererCompositor.name = 'Compositor'; + + // Creates a renderer thread + const renderer = model.getOrCreateProcess(2).getOrCreateThread(2); + renderer.name = 'CrRendererMain'; + renderer.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1)); + + const rasterWorker = model.getOrCreateProcess(2).getOrCreateThread(5); + rasterWorker.name = 'CompositorTileWorker'; + + addPipelineForOneFrame(browserMain, rendererCompositor, + rasterWorker, 1, { + IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3, + GenerateCompositorFrame: 4, SubmitCompositorFrame: 5, + ReceiveCompositorFrame: 6, SurfaceAggregation: 10, + rasterTaskStart: 2 + }); + addPipelineForOneFrame(browserMain, rendererCompositor, + rasterWorker, 2, { + IssueBeginFrame: 15, ReceiveBeginFrame: 16, + GenerateRenderPass: 17, GenerateCompositorFrame: 18, + SubmitCompositorFrame: 19, ReceiveCompositorFrame: 20, + SurfaceAggregation: 21, rasterTaskStart: 22 + }); + addPipelineForOneFrame(browserMain, rendererCompositor, + rasterWorker, 3, { + IssueBeginFrame: 32, ReceiveBeginFrame: 34, + GenerateRenderPass: 35, GenerateCompositorFrame: 36, + SubmitCompositorFrame: 37, SubmitCompositorFrame: 38, + ReceiveCompositorFrame: 41, SurfaceAggregation: 44, + rasterTaskStart: 25 + }); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addPipelineHistograms( + histograms, model, [new tr.model.um.Segment(0, 50)]); + + + const beginFrameTransport = histograms.getHistogramNamed( + 'pipeline:begin_frame_transport'); + const frameSubmission = histograms.getHistogramNamed( + 'pipeline:begin_frame_to_frame_submission'); + const surfaceAggregation = histograms.getHistogramNamed( + 'pipeline:frame_submission_to_display'); + const rasterStart = histograms.getHistogramNamed( + 'pipeline:begin_frame_to_raster_start'); + const rasterEnd = histograms.getHistogramNamed( + 'pipeline:begin_frame_to_raster_end'); + assert.closeTo(beginFrameTransport.average, 4 / 3, 1e-6); + assert.closeTo(frameSubmission.average, 13 / 3, 1e-6); + assert.closeTo(surfaceAggregation.average, 13 / 3, 1e-6); + assert.closeTo(rasterStart.average, 6 / 2, 1e-6); + assert.closeTo(rasterEnd.average, 16 / 2, 1e-6); + }); + + test('graphicsPipeline_duplicateSteps', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + const rendererCompositor = + model.getOrCreateProcess(1).getOrCreateThread(1); + rendererCompositor.name = 'Compositor'; + + // Creates a renderer thread + const renderer = model.getOrCreateProcess(2).getOrCreateThread(2); + renderer.name = 'CrRendererMain'; + renderer.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1)); + + const rasterWorker = model.getOrCreateProcess(2).getOrCreateThread(5); + rasterWorker.name = 'CompositorTileWorker'; + + addPipelineForOneFrame(browserMain, rendererCompositor, + rasterWorker, 1, { + IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3, + GenerateCompositorFrame: 4, SubmitCompositorFrame: 5, + ReceiveCompositorFrame: 6, SurfaceAggregation: 9, + rasterTaskStart: 3 + }); + + // Add duplicate steps for SubmitCompositorFrame and the subsequent + // steps for the same trace-id. + addPipelineForOneFrame(browserMain, rendererCompositor, + rasterWorker, 1, { + GenerateRenderPass: 10, GenerateCompositorFrame: 11, + SubmitCompositorFrame: 12, ReceiveCompositorFrame: 15, + SurfaceAggregation: 18, rasterTaskStart: 3 + }); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addPipelineHistograms( + histograms, model, + [new tr.model.um.Segment(0, 50), new tr.model.um.Segment(0, 20)]); + + const beginFrameTransport = histograms.getHistogramNamed( + 'pipeline:begin_frame_transport'); + const frameSubmission = histograms.getHistogramNamed( + 'pipeline:begin_frame_to_frame_submission'); + const surfaceAggregation = histograms.getHistogramNamed( + 'pipeline:frame_submission_to_display'); + const rasterStart = histograms.getHistogramNamed( + 'pipeline:begin_frame_to_raster_start'); + const rasterEnd = histograms.getHistogramNamed( + 'pipeline:begin_frame_to_raster_end'); + assert.strictEqual(beginFrameTransport.average, 1); + assert.strictEqual(frameSubmission.average, 4); + assert.strictEqual(surfaceAggregation.average, 4); + assert.strictEqual(rasterStart.average, 1); + assert.strictEqual(rasterEnd.average, 6); + }); + + test('graphicsPipeline_duplicateRenderers', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + const rendererCompositor = + model.getOrCreateProcess(1).getOrCreateThread(1); + rendererCompositor.name = 'Compositor'; + + // Creates a renderer thread + const rendererWithIR = model.getOrCreateProcess(2).getOrCreateThread(2); + rendererWithIR.name = 'CrRendererMain'; + rendererWithIR.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1)); + + const rasterWorkerWithIR = model.getOrCreateProcess(2) + .getOrCreateThread(5); + rasterWorkerWithIR.name = 'CompositorTileWorker'; + + const rendererWithoutIR = model.getOrCreateProcess(3) + .getOrCreateThread(2); + rendererWithoutIR.name = 'CrRendererMain'; + + const rasterWorkerWithoutIR = model.getOrCreateProcess(3) + .getOrCreateThread(5); + rasterWorkerWithoutIR.name = 'CompositorTileWorker'; + + addPipelineForOneFrame(browserMain, rendererCompositor, + rasterWorkerWithIR, 1, { + IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3, + GenerateCompositorFrame: 4, SubmitCompositorFrame: 5, + ReceiveCompositorFrame: 6, SurfaceAggregation: 10, + rasterTaskStart: 3 + }); + addPipelineForOneFrame(browserMain, rendererCompositor, + rasterWorkerWithoutIR, 2, { + IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3, + GenerateCompositorFrame: 4, SubmitCompositorFrame: 5, + ReceiveCompositorFrame: 6, SurfaceAggregation: 10, + rasterTaskStart: 4 + }); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addPipelineHistograms( + histograms, model, [new tr.model.um.Segment(0, 50)]); + + const rasterStart = histograms.getHistogramNamed( + 'pipeline:begin_frame_to_raster_start'); + const rasterEnd = histograms.getHistogramNamed( + 'pipeline:begin_frame_to_raster_end'); + assert.strictEqual(rasterStart.average, 1); + assert.strictEqual(rasterEnd.average, 6); + }); + + test('graphicsPipeline_drawSteps', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + const rendererCompositor = + model.getOrCreateProcess(1).getOrCreateThread(1); + rendererCompositor.name = 'Compositor'; + + // Creates a renderer thread + const renderer = model.getOrCreateProcess(2).getOrCreateThread(2); + renderer.name = 'CrRendererMain'; + renderer.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1)); + + const rasterWorker = model.getOrCreateProcess(2).getOrCreateThread(5); + rasterWorker.name = 'CompositorTileWorker'; + + const displayTrace = '1'; + addPipelineForOneFrame(browserMain, rendererCompositor, + rasterWorker, 1, { + IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3, + GenerateCompositorFrame: 4, SubmitCompositorFrame: 5, + ReceiveCompositorFrame: 6, SurfaceAggregation: 9, + rasterTaskStart: 3 + }, displayTrace); + + addDrawSlice(browserMain, displayTrace, 10, + {Draw: 2, Swap: 1, WaitForAck: 5}, {}); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addPipelineHistograms( + histograms, model, [new tr.model.um.Segment(0, 50)]); + + const drawHistogram = histograms.getHistogramNamed('pipeline:draw'); + assert.strictEqual(drawHistogram.average, 8); + }); + + test('graphicsPipeline_drawCanceled', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + const rendererCompositor = + model.getOrCreateProcess(1).getOrCreateThread(1); + rendererCompositor.name = 'Compositor'; + + // Creates a renderer thread + const renderer = model.getOrCreateProcess(2).getOrCreateThread(2); + renderer.name = 'CrRendererMain'; + renderer.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1)); + + const rasterWorker = model.getOrCreateProcess(2).getOrCreateThread(5); + rasterWorker.name = 'CompositorTileWorker'; + + const displayTrace = '1'; + addPipelineForOneFrame(browserMain, rendererCompositor, + rasterWorker, 1, { + IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3, + GenerateCompositorFrame: 4, SubmitCompositorFrame: 5, + ReceiveCompositorFrame: 6, SurfaceAggregation: 9, + rasterTaskStart: 3 + }, displayTrace); + + addDrawSlice(browserMain, displayTrace, 10, + {Draw: 2, Swap: 1, WaitForAck: 5}, {status: 'canceled'}); + addDrawSlice(browserMain, displayTrace, 15, + {Draw: 2, Swap: 1, WaitForAck: 5}); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addPipelineHistograms( + histograms, model, [new tr.model.um.Segment(0, 50)]); + + const drawHistogram = histograms.getHistogramNamed('pipeline:draw'); + assert.strictEqual(drawHistogram.average, 8); + }); + + test('graphicsPipeline_receiveCFAfterAnimation', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + const rendererCompositor = + model.getOrCreateProcess(1).getOrCreateThread(1); + rendererCompositor.name = 'Compositor'; + + // Creates a renderer thread + const renderer = model.getOrCreateProcess(2).getOrCreateThread(2); + renderer.name = 'CrRendererMain'; + renderer.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1)); + + const rasterWorker = model.getOrCreateProcess(2).getOrCreateThread(5); + rasterWorker.name = 'CompositorTileWorker'; + + addPipelineForOneFrame(browserMain, rendererCompositor, + rasterWorker, 1, { + IssueBeginFrame: 1, ReceiveBeginFrame: 2, GenerateRenderPass: 3, + GenerateCompositorFrame: 4, SubmitCompositorFrame: 5, + ReceiveCompositorFrame: 11, SurfaceAggregation: 15, + rasterTaskStart: 2 + }); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addPipelineHistograms( + histograms, model, + [new tr.model.um.Segment(0, 10), new tr.model.um.Segment(14, 16)]); + + const surfaceAggregation = histograms.getHistogramNamed( + 'pipeline:frame_submission_to_display'); + assert.strictEqual(surfaceAggregation.average, 10); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels.html new file mode 100644 index 00000000000..7f3060561d8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> + +<script> +'use strict'; + +/** + * @fileoverview This file contains implementations of the following metrics. + * + * mean_pixels_approximated + * ======================== + * The percentage of tiles which are missing or of low or non-ideal resolution. + * + * TODO(crbug.com/875010): The documentation claims that + * mean_pixels_approximated should be greater than or equal to + * mean_pixels_checkerboarded. This claim is not consistent with numbers from + * the perf dashboard. We should either correct the documentations or fix the + * metrics. + * + * mean_pixels_checkerboarded + * ========================== + * The percentage of tiles which are only missing. It does not take into + * consideration tiles which are of low or non-ideal resolution. + */ +tr.exportTo('tr.metrics.rendering', function() { + const IMPL_THREAD_RENDERING_STATS_EVENT = + 'BenchmarkInstrumentation::ImplThreadRenderingStats'; + const VISIBLE_CONTENT_DATA = 'visible_content_area'; + const APPROXIMATED_VISIBLE_CONTENT_DATA = 'approximated_visible_content_area'; + const CHECKERBOARDED_VISIBLE_CONTENT_DATA = + 'checkerboarded_visible_content_area'; + + function addPixelsHistograms(histograms, model, segments) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (!chromeHelper) return; + + const approximatedPixelPercentages = []; + const checkerboardedPixelPercentages = []; + const ranges = segments.map(s => s.boundsRange); + for (const rendererHelper of Object.values(chromeHelper.rendererHelpers)) { + if (rendererHelper.compositorThread === undefined) continue; + const slices = rendererHelper.compositorThread.sliceGroup; + for (const slice of slices.getDescendantEventsInSortedRanges(ranges)) { + if (slice.title !== IMPL_THREAD_RENDERING_STATS_EVENT) continue; + const data = slice.args.data; + if (!(VISIBLE_CONTENT_DATA in data)) { + throw new Error(`${VISIBLE_CONTENT_DATA} is missing`); + } + const visibleContentArea = data[VISIBLE_CONTENT_DATA]; + if (visibleContentArea === 0) { + // TODO(crbug.com/877056): This is reported as an error in the legacy + // code, which indicates there should be no rendering stats event with + // zero visible content area. But, the TBMv2 implementation encounters + // such events. Maybe they are coming from OOPIFs which are ignored in + // the legacy code? Investigate why they exist and what should be + // done. + // + // throw new Error(`${VISIBLE_CONTENT_DATA} is zero`); + continue; + } + if (APPROXIMATED_VISIBLE_CONTENT_DATA in data) { + approximatedPixelPercentages.push( + data[APPROXIMATED_VISIBLE_CONTENT_DATA] / visibleContentArea); + } + if (CHECKERBOARDED_VISIBLE_CONTENT_DATA in data) { + checkerboardedPixelPercentages.push( + data[CHECKERBOARDED_VISIBLE_CONTENT_DATA] / visibleContentArea); + } + } + } + + // TODO(crbug.com/892501): The averages are multiplied by 100 to match an + // error in legacy code so that we have continuity in graphs in the perf + // dashboard. We should fix historic data and remove the multiplication. + histograms.createHistogram( + 'mean_pixels_approximated', + tr.b.Unit.byName.normalizedPercentage_smallerIsBetter, + 100 * tr.b.math.Statistics.mean(approximatedPixelPercentages), + { description: 'Percentage of pixels that were approximated ' + + '(checkerboarding, low-resolution tiles, etc.).', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + histograms.createHistogram( + 'mean_pixels_checkerboarded', + tr.b.Unit.byName.normalizedPercentage_smallerIsBetter, + 100 * tr.b.math.Statistics.mean(checkerboardedPixelPercentages), + { description: 'Percentage of pixels that were checkerboarded.', + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + }); + } + + return { + addPixelsHistograms, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels_test.html new file mode 100644 index 00000000000..10a77215c47 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/pixels_test.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/rendering/pixels.html"> +<link rel="import" href="/tracing/model/user_model/segment.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('pixelsApproximated', function() { + const model = tr.c.TestUtils.newModel((model) => { + // Metric computation assumes that there is always a browser process. + model.getOrCreateProcess(0).getOrCreateThread(0).name = 'CrBrowserMain'; + + const compositor = model.getOrCreateProcess(1).getOrCreateThread(1); + compositor.name = 'Compositor'; + compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + title: 'BenchmarkInstrumentation::ImplThreadRenderingStats', + start: 1, end: 1, + args: { + data: { + visible_content_area: 50, + approximated_visible_content_area: 8, + checkerboarded_visible_content_area: 3 + } + } + })); + compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + title: 'BenchmarkInstrumentation::ImplThreadRenderingStats', + start: 2, end: 2, + args: { + data: { + visible_content_area: 25, + approximated_visible_content_area: 6, + checkerboarded_visible_content_area: 5 + } + } + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addPixelsHistograms( + histograms, model, [new tr.model.um.Segment(0, 10)]); + + // The mean of 8/50 and 6/25 is 0.2. + let hist = histograms.getHistogramNamed('mean_pixels_approximated'); + assert.closeTo(20, hist.min, 1e-6); + assert.closeTo(20, hist.max, 1e-6); + assert.closeTo(20, hist.average, 1e-6); + + // The mean of 3/50 and 5/25 is 0.13. + hist = histograms.getHistogramNamed('mean_pixels_checkerboarded'); + assert.closeTo(13, hist.min, 1e-6); + assert.closeTo(13, hist.max, 1e-6); + assert.closeTo(13, hist.average, 1e-6); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration.html new file mode 100644 index 00000000000..386bd444658 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +/** + * @fileoverview This file contains implementations of the following metrics. + * + * queueing_durations + * ================== + * This quantifies how out of sync the compositor and renderer threads are. It + * is the amount of wall time that elapses between a + * ScheduledActionSendBeginMainFrame event in the compositor thread and the + * corresponding BeginMainFrame event in the main thread. + * + * TODO(chiniforooshan): Does it make sense to just ignore data from OOPIF + * processes, like what we are doing here? + */ +tr.exportTo('tr.metrics.rendering', function() { + // Various tracing events. + const BEGIN_MAIN_FRAME_EVENT = 'ThreadProxy::BeginMainFrame'; + const SEND_BEGIN_FRAME_EVENT = + 'ThreadProxy::ScheduledActionSendBeginMainFrame'; + + function getEventTimesByBeginFrameId_(thread, title, ranges) { + const out = new Map(); + const slices = thread.sliceGroup; + for (const slice of slices.getDescendantEventsInSortedRanges(ranges)) { + if (slice.title !== title) continue; + const id = slice.args.begin_frame_id; + if (id === undefined) throw new Error('Event is missing begin_frame_id'); + if (out.has(id)) throw new Error(`There must be exactly one ${title}`); + out.set(id, slice.start); + } + return out; + } + + function addQueueingDurationHistograms(histograms, model, segments) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (!chromeHelper) return; + + let targetRenderers = chromeHelper.telemetryHelper.renderersWithIR; + if (targetRenderers.length === 0) { + targetRenderers = Object.values(chromeHelper.rendererHelpers); + } + const queueingDurations = []; + const ranges = segments.map(s => s.boundsRange); + for (const rendererHelper of targetRenderers) { + const mainThread = rendererHelper.mainThread; + const compositorThread = rendererHelper.compositorThread; + if (mainThread === undefined || compositorThread === undefined) continue; + + const beginMainFrameTimes = getEventTimesByBeginFrameId_( + mainThread, BEGIN_MAIN_FRAME_EVENT, ranges); + const sendBeginFrameTimes = getEventTimesByBeginFrameId_( + compositorThread, SEND_BEGIN_FRAME_EVENT, ranges); + for (const [id, time] of sendBeginFrameTimes) { + queueingDurations.push(beginMainFrameTimes.get(id) - time); + } + } + + histograms.createHistogram( + 'queueing_durations', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, queueingDurations, { + binBoundaries: + tr.v.HistogramBinBoundaries.createExponential(0.01, 2, 20), + summaryOptions: tr.metrics.rendering.SUMMARY_OPTIONS, + description: 'Time between ScheduledActionSendBeginMainFrame in ' + + 'the compositor thread and the corresponding ' + + 'BeginMainFrame in the main thread.' + }); + } + + return { + addQueueingDurationHistograms, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration_test.html new file mode 100644 index 00000000000..2ca870810dd --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/queueing_duration_test.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/rendering/queueing_duration.html"> +<link rel="import" href="/tracing/model/user_model/segment.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('queueingDurations', function() { + const model = tr.c.TestUtils.newModel((model) => { + // Metric computation assumes that there is always a browser process. + model.getOrCreateProcess(0).getOrCreateThread(0).name = 'CrBrowserMain'; + + const compositor = model.getOrCreateProcess(1).getOrCreateThread(1); + compositor.name = 'Compositor'; + compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'ThreadProxy::ScheduledActionSendBeginMainFrame', + start: 3, end: 3, + args: { begin_frame_id: 2 }})); + compositor.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'ThreadProxy::ScheduledActionSendBeginMainFrame', + start: 4, end: 4, + args: { begin_frame_id: 1 }})); + + const main = model.getOrCreateProcess(1).getOrCreateThread(0); + main.name = 'CrRendererMain'; + main.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'ThreadProxy::BeginMainFrame', + start: 5, end: 5, + args: { begin_frame_id: 1 }})); + main.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'ThreadProxy::BeginMainFrame', + start: 6, end: 6, + args: { begin_frame_id: 2 }})); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.addQueueingDurationHistograms( + histograms, model, [new tr.model.um.Segment(0, 10)]); + + const hist = histograms.getHistogramNamed('queueing_durations'); + assert.closeTo(1, hist.min, 1e-6); + assert.closeTo(3, hist.max, 1e-6); + assert.closeTo(2, hist.average, 1e-6); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric.html new file mode 100644 index 00000000000..1024c9baa87 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/rendering/frame_time.html"> +<link rel="import" href="/tracing/metrics/rendering/latency.html"> +<link rel="import" href="/tracing/metrics/rendering/pipeline.html"> +<link rel="import" href="/tracing/metrics/rendering/pixels.html"> +<link rel="import" href="/tracing/metrics/rendering/queueing_duration.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.rendering', function() { + // Various tracing events. + const GESTURE_EVENT = 'SyntheticGestureController::running'; + + function renderingMetric(histograms, model) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (!chromeHelper) return; + + const segments = chromeHelper.telemetryHelper.segments; + if (segments.length > 0) { + tr.metrics.rendering.addFrameTimeHistograms(histograms, model, segments); + tr.metrics.rendering.addLatencyHistograms(histograms, model, segments); + tr.metrics.rendering.addPipelineHistograms(histograms, model, segments); + tr.metrics.rendering.addPixelsHistograms(histograms, model, segments); + tr.metrics.rendering.addQueueingDurationHistograms( + histograms, model, segments); + } + + const uiSegments = chromeHelper.telemetryHelper.uiSegments; + if (uiSegments.length > 0) { + tr.metrics.rendering.addUIFrameTimeHistograms( + histograms, model, chromeHelper.telemetryHelper.uiSegments); + } + } + + tr.metrics.MetricRegistry.register(renderingMetric, { + requiredCategories: ['benchmark', 'toplevel'], + }); + + return { + renderingMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric_test.html new file mode 100644 index 00000000000..77e0a2d9dd1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/rendering/rendering_metric_test.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/rendering/rendering_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('renderingMetric_gestureIR', function() { + const model = tr.c.TestUtils.newModel((model) => { + const rendererMain = model.getOrCreateProcess(1).getOrCreateThread(0); + rendererMain.name = 'CrRendererMain'; + rendererMain.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed( + 'Interaction.Gesture_LoadAction', 10, 10)); + + const browserMain = model.getOrCreateProcess(0).getOrCreateThread(0); + browserMain.name = 'CrBrowserMain'; + browserMain.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed( + 'SyntheticGestureController::running', 0, 5)); + browserMain.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed( + 'SyntheticGestureController::running', 10, 5)); + // Add four swap buffer events, at times 1, 2, 11, 13, 16 + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 1, end: 1 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 2, end: 2 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 11, end: 11 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 13, end: 13 })); + browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx( + { title: 'BenchmarkInstrumentation::DisplayRenderingStats', + start: 16, end: 16 })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.rendering.renderingMetric(histograms, model); + + // The gesture interaction record should be adjusted to [10, 15]. So, the + // first two frames and the fifth frame are outside the interaction record + // and should be discarded. The remaining frames are 11 and 13 which result + // in a single frame time of 2 = 13 - 11. + const hist = histograms.getHistogramNamed('frame_times'); + assert.closeTo(2, hist.min, 1e-6); + assert.closeTo(2, hist.max, 2e-6); + assert.closeTo(2, hist.average, 1e-6); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/sample_exception_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/sample_exception_metric.html new file mode 100644 index 00000000000..01dd58cdf9c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/sample_exception_metric.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics', function() { + function sampleExceptionMetric(histograms, model) { + const hist = new tr.v.Histogram( + 'foo', tr.b.Unit.byName.sizeInBytes_smallerIsBetter); + hist.addSample(9); + hist.addSample(91, {bar: new tr.v.d.GenericSet([{hello: 42}])}); + + for (const expectation of model.userModel.expectations) { + if (expectation instanceof tr.model.um.ResponseExpectation) { + } else if (expectation instanceof tr.model.um.AnimationExpectation) { + } else if (expectation instanceof tr.model.um.IdleExpectation) { + } else if (expectation instanceof tr.model.um.LoadExpectation) { + } + } + + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + + for (const [pid, process] of Object.entries(model.processes)) { + } + + histograms.addHistogram(hist); + throw new Error('There was an error'); + } + + tr.metrics.MetricRegistry.register(sampleExceptionMetric); + + return { + sampleExceptionMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/sample_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/sample_metric.html new file mode 100644 index 00000000000..4a8be86b0d7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/sample_metric.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics', function() { + function sampleMetric(histograms, model) { + const hist = new tr.v.Histogram( + 'foo', tr.b.Unit.byName.sizeInBytes_smallerIsBetter); + hist.addSample(9); + hist.addSample(91, {bar: new tr.v.d.GenericSet([{hello: 42}])}); + + for (const expectation of model.userModel.expectations) { + if (expectation instanceof tr.model.um.ResponseExpectation) { + } else if (expectation instanceof tr.model.um.AnimationExpectation) { + } else if (expectation instanceof tr.model.um.IdleExpectation) { + } else if (expectation instanceof tr.model.um.LoadExpectation) { + } + } + + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + + for (const [pid, process] of Object.entries(model.processes)) { + } + + histograms.addHistogram(hist); + } + + tr.metrics.MetricRegistry.register(sampleMetric); + + return { + sampleMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper.html b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper.html new file mode 100644 index 00000000000..fcc5c8df4ca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper.html @@ -0,0 +1,248 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/base/base.html"> + +<script> +'use strict'; + +/** + * @fileoverview This file contains helper functions to identify + * FrameLoader::updateForSameDocumentNavigation events on all renderer + * processes and find their preceding navigation start events. + *_______________________________________________________________ + * browser: InputLatency/NavigationControllerImpl::GoToIndex | + *---------------------------------------- + * renderer: LatencyInfo.Flow + * WebViewImpl::handleInputEvent + * FrameLoader::updateForSameDocumentNavigation + *---------------------------------------------------- + * FrameLoader::updateForSameDocumentNavigation is called when SPA + * in-app navigation occurs. + * For details about how SPA in-app navigation is defined and + * how it is found based on FrameLoader::updateForSameDocumentNavigation, + * read the doc: https://goo.gl/1I3tqd. + */ +tr.exportTo('tr.metrics', function() { + const HANDLE_INPUT_EVENT_TITLE = 'WebViewImpl::handleInputEvent'; + + /** + * @returns {Map.<tr.model.Slice, tr.model.Slice>} A map of the + * elements in eventsB which immediately precede events in eventsA. + * For instance: + * eventsA: A1 A2 A3 A4 + * eventsB: B1 B2 B3 B4 B5 + * output: {A1: B2, A2: B3, A3: B4, A4: B5} + * or + * eventsA: A1 A2 A3 A4 + * eventsB: B1 + * output: {A1: B1, A2: B1, A3: B1, A4: B1} + */ + function findPrecedingEvents_(eventsA, eventsB) { + const events = new Map(); + let eventsBIndex = 0; + for (const eventA of eventsA) { + for (; eventsBIndex < eventsB.length; eventsBIndex++) { + if (eventsB[eventsBIndex].start > eventA.start) break; + } + // If statement prevents the situation when eventsB is empty. + if (eventsBIndex > 0) { + events.set(eventA, eventsB[eventsBIndex - 1]); + } + } + return events; + } + + /** + * @returns {Map.<tr.model.Slice, tr.model.Slice>} A map of + * the elements in eventsB which immediately follow events + * in eventsA. + * For instance: + * eventsA: A1 A2 A3 A4 + * eventsB: B1 B2 B3 B4 B5 + * output: {A1:B2, A2:B3, A3:B4, A4:B5} + * or + * eventsA: A1 A2 A3 A4 + * eventsB: B1 + * output: {A1:B1, A2:B1, A3:B1} + */ + function findFollowingEvents_(eventsA, eventsB) { + const events = new Map(); + let eventsBIndex = 0; + for (const eventA of eventsA) { + for (; eventsBIndex < eventsB.length; eventsBIndex++) { + if (eventsB[eventsBIndex].start >= eventA.start) break; + } + // If statement prevents the situation when eventsB is empty + // and when it reaches the end of loop. + if (eventsBIndex >= 0 && eventsBIndex < eventsB.length) { + events.set(eventA, eventsB[eventsBIndex]); + } + } + return events; + } + + /** + * @return {Array.<tr.model.Slice>} An array of events that may + * be qualified as a SPA navigation start candidate such as + * WebViewImpl::handleInputEvent and NavigationControllerImpl::GoToIndex. + */ + function getSpaNavigationStartCandidates_(rendererHelper, browserHelper) { + const isNavStartEvent = e => { + if (e.title === HANDLE_INPUT_EVENT_TITLE && e.args.type === 'MouseUp') { + return true; + } + return e.title === 'NavigationControllerImpl::GoToIndex'; + }; + + return [ + ...rendererHelper.mainThread.sliceGroup.getDescendantEvents(), + ...browserHelper.mainThread.sliceGroup.getDescendantEvents() + ].filter(isNavStartEvent); + } + + /** + * @return {Array.<tr.model.Slice>} An array of SPA navigation events. + * A SPA navigation event indicates the happening of a SPA navigation. + */ + function getSpaNavigationEvents_(rendererHelper) { + const isNavEvent = e => e.category === 'blink' && + e.title === 'FrameLoader::updateForSameDocumentNavigation'; + + return [...rendererHelper.mainThread.sliceGroup.getDescendantEvents()] + .filter(isNavEvent); + } + + /** + * @return {Array.<tr.model.AsyncSlice>} An array of InputLatency events from + * the browser main thread. + */ + function getInputLatencyEvents_(browserHelper) { + const isInputLatencyEvent = e => e.title === 'InputLatency::MouseUp'; + + return browserHelper.getAllAsyncSlicesMatching(isInputLatencyEvent); + } + + /** + * @return {Map.<number, tr.model.Slice>} A mapping of trace_id value + * in each InputLatency event to the respective InputLatency event itself. + */ + function getInputLatencyEventByBindIdMap_(browserHelper) { + const inputLatencyEventByBindIdMap = new Map(); + for (const event of getInputLatencyEvents_(browserHelper)) { + inputLatencyEventByBindIdMap.set(event.args.data.trace_id, event); + } + return inputLatencyEventByBindIdMap; + } + + /** + * @returns {Map.<tr.model.Slice, tr.model.AsyncSlice>} A mapping + * from a FrameLoader update navigation slice to its respective + * navigation start event, which can be an InputLatency async + * slice or a NavigationControllerImpl::GoToIndex slice. + */ + function getSpaNavigationEventToNavigationStartMap_( + rendererHelper, browserHelper) { + const mainThread = rendererHelper.mainThread; + const spaNavEvents = getSpaNavigationEvents_(rendererHelper); + const navStartCandidates = getSpaNavigationStartCandidates_( + rendererHelper, browserHelper).sort(tr.importer.compareEvents); + const spaNavEventToNavStartCandidateMap = + findPrecedingEvents_(spaNavEvents, navStartCandidates); + + const inputLatencyEventByBindIdMap = + getInputLatencyEventByBindIdMap_(browserHelper); + const spaNavEventToNavStartEventMap = new Map(); + for (const [spaNavEvent, navStartCandidate] of + spaNavEventToNavStartCandidateMap) { + if (navStartCandidate.title === HANDLE_INPUT_EVENT_TITLE) { + const inputLatencySlice = inputLatencyEventByBindIdMap.get( + Number(navStartCandidate.parentSlice.bindId)); + if (inputLatencySlice) { + spaNavEventToNavStartEventMap.set(spaNavEvent, inputLatencySlice); + } + } else { + spaNavEventToNavStartEventMap.set(spaNavEvent, navStartCandidate); + } + } + return spaNavEventToNavStartEventMap; + } + + /** + * @return {Array.<tr.model.Slice>} An array of first paint events. + */ + function getFirstPaintEvents_(rendererHelper) { + const isFirstPaintEvent = e => e.category === 'blink' && + e.title === 'PaintLayerCompositor::updateIfNeededRecursive'; + + return [...rendererHelper.mainThread.sliceGroup.getDescendantEvents()] + .filter(isFirstPaintEvent); + } + + /** + * @returns {Map.<tr.model.Slice, tr.model.Slice>} A mapping + * from a FrameLoader update navigation slice to its respective + * first paint slice. + */ + function getSpaNavigationEventToFirstPaintEventMap_(rendererHelper) { + const spaNavEvents = getSpaNavigationEvents_( + rendererHelper).sort(tr.importer.compareEvents); + const firstPaintEvents = getFirstPaintEvents_( + rendererHelper).sort(tr.importer.compareEvents); + + return findFollowingEvents_(spaNavEvents, firstPaintEvents); + } + + /** + * @typedef {NavStartCandidates} + * @property {tr.model.AsyncSlice} inputLatencyAsyncSlice + * @property {tr.model.Slice} goToIndexSlice + */ + + /** + * @typedef {SpaNavObject} + * @property {NavStartCandidates} navStartCandidates + * @property {tr.model.Slice} firstPaintEvent + * @property {string} url + */ + + /** + * @returns {Array.<SpaNavObject>} + */ + function findSpaNavigationsOnRenderer(rendererHelper, browserHelper) { + const spaNavEventToNavStartMap = + getSpaNavigationEventToNavigationStartMap_( + rendererHelper, browserHelper); + const spaNavEventToFirstPaintEventMap = + getSpaNavigationEventToFirstPaintEventMap_(rendererHelper); + const spaNavigations = []; + for (const [spaNavEvent, navStartEvent] of + spaNavEventToNavStartMap) { + if (spaNavEventToFirstPaintEventMap.has(spaNavEvent)) { + const firstPaintEvent = + spaNavEventToFirstPaintEventMap.get(spaNavEvent); + const isNavStartAsyncSlice = + navStartEvent instanceof tr.model.AsyncSlice; + spaNavigations.push({ + navStartCandidates: { + inputLatencyAsyncSlice: + isNavStartAsyncSlice ? navStartEvent : undefined, + goToIndexSlice: isNavStartAsyncSlice ? undefined : navStartEvent + }, + firstPaintEvent, + url: spaNavEvent.args.url + }); + } + } + return spaNavigations; + } + + return { + findSpaNavigationsOnRenderer, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper_test.html new file mode 100644 index 00000000000..ae87c1834e7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_helper_test.html @@ -0,0 +1,258 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/spa_navigation_helper.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const RENDERER_PROCESS_ID = 1234; + const RENDERER_PROCESS_MAIN_THREAD_ID = 1; + const BROWSER_PROCESS_ID = 1; + const BROWSER_PROCESS_MAIN_THREAD_ID = 12; + const PAINT_UPDATE_TITLE = + 'PaintLayerCompositor::updateIfNeededRecursive'; + const SPA_NAVIGATION_EVENT_TITLE = + 'FrameLoader::updateForSameDocumentNavigation'; + + function createChromeProcessesOnModel(model) { + const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID); + const mainThread = rendererProcess.getOrCreateThread( + RENDERER_PROCESS_MAIN_THREAD_ID); + mainThread.name = 'CrRendererMain'; + const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID); + const browserMainThread = browserProcess.getOrCreateThread( + BROWSER_PROCESS_MAIN_THREAD_ID); + browserMainThread.name = 'CrBrowserMain'; + } + + function addThreadSlice(model, title, timestamp, args) { + const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID); + const mainThread = rendererProcess.getOrCreateThread( + RENDERER_PROCESS_MAIN_THREAD_ID); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink', + title, + start: timestamp, + duration: 0.1, + args + })); + } + + function addLatencyInfoFlowEvent(model, timestamp, bindId) { + const latencyInfoFlowSlice = tr.c.TestUtils.newSliceEx({ + cat: 'input,benchmark', + title: 'LatencyInfo.Flow', + start: timestamp, + duration: 0.1, + bindId, + args: {step: 'handleInputEventMain'} + }); + const handleInputEventSlice = tr.c.TestUtils.newSliceEx({ + cat: 'blink,rail', + title: 'WebViewImpl::handleInputEvent', + start: timestamp + 1, // Assume handleInputEvent always delays 1ms. + duration: 0.1, + args: {type: 'MouseUp'} + }); + + const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID); + const mainThread = rendererProcess.getOrCreateThread( + RENDERER_PROCESS_MAIN_THREAD_ID); + + handleInputEventSlice.parentSlice = latencyInfoFlowSlice; + mainThread.sliceGroup.pushSlice(latencyInfoFlowSlice); + mainThread.sliceGroup.pushSlice(handleInputEventSlice); + } + + function addInputLatencySlice(model, start, traceId) { + const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID); + const browserMainThread = browserProcess.getOrCreateThread( + BROWSER_PROCESS_MAIN_THREAD_ID); + + browserMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newAsyncSliceEx({ + cat: 'benchmark,latencyInfo,rail', + title: 'InputLatency::MouseUp', + start, + duration: 0.1, + args: { + data: { + trace_id: traceId + } + } + })); + } + + function addGoToIndexSlice(model, timestamp) { + const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID); + const browserMainThread = browserProcess.getOrCreateThread( + BROWSER_PROCESS_MAIN_THREAD_ID); + + browserMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'browser,navigation,benchmark', + title: 'NavigationControllerImpl::GoToIndex', + start: timestamp, + duration: 0.1 + })); + } + + function getSpaNavigations(model) { + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + const rendererHelpers = modelHelper.rendererHelpers; + const browserHelper = modelHelper.browserHelper; + let spaNavigations = []; + for (const rendererHelper of Object.values(rendererHelpers)) { + spaNavigations = spaNavigations.concat( + tr.metrics.findSpaNavigationsOnRenderer( + rendererHelper, browserHelper)); + } + return spaNavigations; + } + + test('findSpaNavigations_noSpaNavEvent', function() { + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addLatencyInfoFlowEvent(model, 75, '0x600000057'); + addInputLatencySlice(model, 55, 25769803863); + addThreadSlice(model, PAINT_UPDATE_TITLE, 101); + }); + const spaNavigations = getSpaNavigations(model); + assert.lengthOf(spaNavigations, 0); + }); + + test('findSpaNavigations_noLatencyInfoFlowEvent', function() { + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100); + addInputLatencySlice(model, 55, 25769803863); + addThreadSlice(model, PAINT_UPDATE_TITLE, 101); + }); + const spaNavigations = getSpaNavigations(model); + assert.lengthOf(spaNavigations, 0); + }); + + test('findSpaNavigations_noNavStartEvent', function() { + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100); + addLatencyInfoFlowEvent(model, 75, '0x600000057'); + addThreadSlice(model, PAINT_UPDATE_TITLE, 101); + }); + const spaNavigations = getSpaNavigations(model); + assert.lengthOf(spaNavigations, 0); + }); + + test('findSpaNavigations_noFirstPaintEvent', function() { + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100); + addLatencyInfoFlowEvent(model, 75, '0x600000057'); + addInputLatencySlice(model, 55, 25769803863); + }); + const spaNavigations = getSpaNavigations(model); + assert.lengthOf(spaNavigations, 0); + }); + + test('findSpaNavigations_inputLatencyAsNavStart', function() { + const URL = 'https://11111'; + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100, + {url: URL}); + addLatencyInfoFlowEvent(model, 75, '0x600000057'); + addInputLatencySlice(model, 55, 25769803863); + addThreadSlice(model, PAINT_UPDATE_TITLE, 101); + }); + const spaNavigations = getSpaNavigations(model); + assert.lengthOf(spaNavigations, 1); + assert.strictEqual( + spaNavigations[0].navStartCandidates.inputLatencyAsyncSlice.start, 55); + assert.strictEqual( + spaNavigations[0].navStartCandidates.goToIndexSlice, undefined); + assert.strictEqual(spaNavigations[0].firstPaintEvent.start, 101); + assert.strictEqual(spaNavigations[0].url, URL); + }); + + test('findSpaNavigations_goToIndexAsNavStart', function() { + const URL = 'https://11111'; + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100, + {url: URL}); + addGoToIndexSlice(model, 55); + addThreadSlice(model, PAINT_UPDATE_TITLE, 101); + }); + const spaNavigations = getSpaNavigations(model); + assert.lengthOf(spaNavigations, 1); + assert.strictEqual( + spaNavigations[0].navStartCandidates.goToIndexSlice.start, 55); + assert.strictEqual( + spaNavigations[0].navStartCandidates.inputLatencyAsyncSlice, undefined); + assert.strictEqual(spaNavigations[0].firstPaintEvent.start, 101); + assert.strictEqual(spaNavigations[0].url, URL); + }); + + test('findSpaNavigations_multipleSpaNavs', function() { + const URL1 = 'https://11111'; + const URL2 = 'https://22222'; + const URL3 = 'https://33333'; + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 100, + {url: URL1}); + addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 200, + {url: URL2}); + addThreadSlice(model, SPA_NAVIGATION_EVENT_TITLE, 300, + {url: URL3}); + + addLatencyInfoFlowEvent(model, 75, '0x600000057'); + addLatencyInfoFlowEvent(model, 175, '0x6000000c2'); + addLatencyInfoFlowEvent(model, 275, '0x60000010d'); + + addInputLatencySlice(model, 55, 25769803863); + addGoToIndexSlice(model, 65); + addInputLatencySlice(model, 155, 25769803970); + addInputLatencySlice(model, 255, 25769804045); + + addThreadSlice(model, PAINT_UPDATE_TITLE, 101); + addThreadSlice(model, PAINT_UPDATE_TITLE, 102); + addThreadSlice(model, PAINT_UPDATE_TITLE, 201); + addThreadSlice(model, PAINT_UPDATE_TITLE, 301); + }); + const spaNavigations = getSpaNavigations(model); + assert.lengthOf(spaNavigations, 3); + spaNavigations.sort((spa1, spa2) => + spa1.navStartCandidates.inputLatencyAsyncSlice.start - + spa2.navStartCandidates.inputLatencyAsyncSlice.start); + assert.strictEqual( + spaNavigations[0].navStartCandidates.inputLatencyAsyncSlice.start, 55); + assert.strictEqual( + spaNavigations[1].navStartCandidates.inputLatencyAsyncSlice.start, 155); + assert.strictEqual( + spaNavigations[2].navStartCandidates.inputLatencyAsyncSlice.start, 255); + + assert.strictEqual( + spaNavigations[0].navStartCandidates.goToIndexSlice, undefined); + assert.strictEqual( + spaNavigations[1].navStartCandidates.goToIndexSlice, undefined); + assert.strictEqual( + spaNavigations[2].navStartCandidates.goToIndexSlice, undefined); + + assert.strictEqual(spaNavigations[0].firstPaintEvent.start, 101); + assert.strictEqual(spaNavigations[1].firstPaintEvent.start, 201); + assert.strictEqual(spaNavigations[2].firstPaintEvent.start, 301); + + assert.strictEqual(spaNavigations[0].url, URL1); + assert.strictEqual(spaNavigations[1].url, URL2); + assert.strictEqual(spaNavigations[2].url, URL3); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric.html new file mode 100644 index 00000000000..f8d6bb537f6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/spa_navigation_helper.html"> +<link rel="import" href="/tracing/metrics/system_health/breakdown_tree_helpers.html"> +<link rel="import" href="/tracing/metrics/system_health/loading_metric.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics', function() { + const SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION_BIN_BOUNDARY = + tr.v.HistogramBinBoundaries.createExponential(1, 1000, 50); + + /** + * This metric measures the duration between the input event + * causing a SPA navigation and the first paint event after it. + */ + function spaNavigationMetric(histograms, model) { + const histogram = new tr.v.Histogram( + 'spaNavigationStartToFpDuration', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION_BIN_BOUNDARY); + histogram.description = 'Latency between the input event causing' + + ' a SPA navigation and the first paint event after it'; + histogram.customizeSummaryOptions({ + count: false, + sum: false, + }); + + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (!modelHelper) { + // Chrome isn't present. + return; + } + const rendererHelpers = modelHelper.rendererHelpers; + if (!rendererHelpers) { + // We couldn't find any renderer processes. + return; + } + const browserHelper = modelHelper.browserHelper; + for (const rendererHelper of Object.values(rendererHelpers)) { + const spaNavigations = tr.metrics.findSpaNavigationsOnRenderer( + rendererHelper, browserHelper); + for (const spaNav of spaNavigations) { + let beginTs = 0; + if (spaNav.navStartCandidates.inputLatencyAsyncSlice) { + const beginData = + spaNav.navStartCandidates.inputLatencyAsyncSlice.args.data; + // TODO(sunjian): rename convertTimestampToModelTime to something like + // convertTraceEventTsToModelTs and get rid of the first parameter. + beginTs = model.convertTimestampToModelTime( + 'traceEventClock', + beginData.INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT.time); + } else { + beginTs = spaNav.navStartCandidates.goToIndexSlice.start; + } + const rangeOfInterest = tr.b.math.Range.fromExplicitRange( + beginTs, spaNav.firstPaintEvent.start); + const networkEvents = tr.metrics.sh.getNetworkEventsInRange( + rendererHelper.process, rangeOfInterest); + const breakdownDict = tr.metrics.sh.generateWallClockTimeBreakdownTree( + rendererHelper.mainThread, networkEvents, rangeOfInterest); + const breakdownDiagnostic = new tr.v.d.Breakdown(); + breakdownDiagnostic.colorScheme = + tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER; + for (const label in breakdownDict) { + breakdownDiagnostic.set(label, + parseInt(breakdownDict[label].total * 1e3) / 1e3); + } + histogram.addSample( + rangeOfInterest.duration, + { + 'Breakdown of [navStart, firstPaint]': breakdownDiagnostic, + 'Start': new tr.v.d.RelatedEventSet(spaNav.navigationStart), + 'End': new tr.v.d.RelatedEventSet(spaNav.firstPaintEvent), + 'Navigation infos': new tr.v.d.GenericSet([{ + url: spaNav.url, + pid: rendererHelper.pid, + navStart: beginTs, + firstPaint: spaNav.firstPaintEvent.start + }]), + }); + } + } + histograms.addHistogram(histogram); + } + + tr.metrics.MetricRegistry.register(spaNavigationMetric); + + return { + spaNavigationMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric_test.html new file mode 100644 index 00000000000..44c38038d3b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/spa_navigation_metric_test.html @@ -0,0 +1,234 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/spa_navigation_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const RENDERER_PROCESS_ID = 1234; + const RENDERER_PROCESS_MAIN_THREAD_ID = 1; + const BROWSER_PROCESS_ID = 1; + const BROWSER_PROCESS_MAIN_THREAD_ID = 12; + const SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION = + 'spaNavigationStartToFpDuration'; + + function createChromeProcessesOnModel(model) { + const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID); + const mainThread = rendererProcess.getOrCreateThread( + RENDERER_PROCESS_MAIN_THREAD_ID); + mainThread.name = 'CrRendererMain'; + const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID); + const browserMainThread = browserProcess.getOrCreateThread( + BROWSER_PROCESS_MAIN_THREAD_ID); + browserMainThread.name = 'CrBrowserMain'; + } + + function addSpaNavigationEvent(model, timestamp, args) { + const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID); + const mainThread = rendererProcess.getOrCreateThread( + RENDERER_PROCESS_MAIN_THREAD_ID); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink', + title: 'FrameLoader::updateForSameDocumentNavigation', + start: timestamp, + duration: 0.1, + args + })); + } + + function addGoToIndexSlice(model, timestamp) { + const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID); + const browserMainThread = browserProcess.getOrCreateThread( + BROWSER_PROCESS_MAIN_THREAD_ID); + browserMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'browser,navigation,benchmark', + title: 'NavigationControllerImpl::GoToIndex', + start: timestamp, + duration: 0.1 + })); + } + + function addFirstPaintSlice(model, timestamp) { + const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID); + const mainThread = rendererProcess.getOrCreateThread( + RENDERER_PROCESS_MAIN_THREAD_ID); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink', + title: 'PaintLayerCompositor::updateIfNeededRecursive', + start: timestamp, + duration: 0.1 + })); + } + + function addInputLatencyRelatedSlices(model, timestamp) { + const latencyInfoFlowSlice = tr.c.TestUtils.newSliceEx({ + cat: 'input,benchmark', + title: 'LatencyInfo.Flow', + start: timestamp - 2, + duration: 0.1, + bindId: '0x600000057', + args: {step: 'handleInputEventMain'} + }); + const handleInputEventSlice = tr.c.TestUtils.newSliceEx({ + cat: 'blink,rail', + title: 'WebViewImpl::handleInputEvent', + start: timestamp - 1, // Assume handleInputEvent always delays 1ms. + duration: 0.1, + args: {type: 'MouseUp'} + }); + + const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID); + const mainThread = rendererProcess.getOrCreateThread( + RENDERER_PROCESS_MAIN_THREAD_ID); + + handleInputEventSlice.parentSlice = latencyInfoFlowSlice; + mainThread.sliceGroup.pushSlice(latencyInfoFlowSlice); + mainThread.sliceGroup.pushSlice(handleInputEventSlice); + + const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID); + const browserMainThread = browserProcess.getOrCreateThread( + BROWSER_PROCESS_MAIN_THREAD_ID); + browserMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newAsyncSliceEx({ + cat: 'benchmark,latencyInfo,rail', + title: 'InputLatency::MouseUp', + start: timestamp - 3, + duration: 0.1, + args: { + data: { + trace_id: 25769803863, + INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT: { + time: timestamp - 3 + 0.1 + } + } + } + })); + } + + function getHistogramNamed(name, model) { + const histograms = new tr.v.HistogramSet(); + tr.metrics.spaNavigationMetric(histograms, model); + const spaNavigationStartToFpDurationHist = histograms.getHistogramNamed( + name); + return spaNavigationStartToFpDurationHist; + } + + test('spaNavStartToFirstPaintDuration_noChromeProcess', function() { + const model = tr.c.TestUtils.newModel(); + const histogram = getHistogramNamed( + SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model); + assert(!histogram); + }); + + test('spaNavStartToFirstPaintDuration_noSpaNavStart', function() { + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addSpaNavigationEvent(model, 100); + addFirstPaintSlice(model, 101); + }); + const histogram = getHistogramNamed( + SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model); + assert.strictEqual(0, histogram.numValues); + }); + + test('spaNavStartToFirstPaintDuration_noFirstPaint', function() { + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addInputLatencyRelatedSlices(model, 99); + addSpaNavigationEvent(model, 100); + }); + const histogram = getHistogramNamed( + SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model); + assert.strictEqual(0, histogram.numValues); + }); + + test('spaNavStartToFirstPaintDuration_inputLatencyAsNavStart', function() { + const URL = 'https://11111'; + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addInputLatencyRelatedSlices(model, 99); + addSpaNavigationEvent(model, 100, {url: URL}); + addFirstPaintSlice(model, 101); + }); + const histogram = getHistogramNamed( + SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model); + assert.strictEqual(1, histogram.running.count); + const navStartTs = model.convertTimestampToModelTime( + 'traceEventClock', 99 - 3 + 0.1); + const expectedDuration = 101 - navStartTs; + assert.closeTo(expectedDuration, histogram.running.sum, 0.5); + + const binsWithSampleDiagnosticMaps = + histogram.allBins.filter(bin => bin.diagnosticMaps.length > 0); + const diagnostic = tr.b.getOnlyElement(binsWithSampleDiagnosticMaps[0] + .diagnosticMaps[0].get('Navigation infos')); + assert.strictEqual(diagnostic.url, URL); + assert.strictEqual(diagnostic.pid, RENDERER_PROCESS_ID); + assert.strictEqual(diagnostic.navStart, navStartTs); + assert.strictEqual(diagnostic.firstPaint, 101); + }); + + test('spaNavStartToFirstPaintDuration_goToIndexAsNavStart', function() { + const URL = 'https://11111'; + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addGoToIndexSlice(model, 99); + addSpaNavigationEvent(model, 100, {url: URL}); + addFirstPaintSlice(model, 101); + }); + const histogram = getHistogramNamed( + SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model); + assert.strictEqual(1, histogram.running.count); + const expectedDuration = 101 - 99; + assert.closeTo(expectedDuration, histogram.running.sum, 0.5); + + const binsWithSampleDiagnosticMaps = + histogram.allBins.filter(bin => bin.diagnosticMaps.length > 0); + const diagnostic = tr.b.getOnlyElement(binsWithSampleDiagnosticMaps[0] + .diagnosticMaps[0].get('Navigation infos')); + assert.strictEqual(diagnostic.url, URL); + assert.strictEqual(diagnostic.pid, RENDERER_PROCESS_ID); + assert.strictEqual(diagnostic.navStart, 99); + assert.strictEqual(diagnostic.firstPaint, 101); + }); + + test('spaNavStartToFirstPaintDuration_multipleSpaNavs', function() { + const model = tr.c.TestUtils.newModel(model => { + createChromeProcessesOnModel(model); + addInputLatencyRelatedSlices(model, 99); + addSpaNavigationEvent(model, 100); + addFirstPaintSlice(model, 101); + + addInputLatencyRelatedSlices(model, 198); + addSpaNavigationEvent(model, 200); + addFirstPaintSlice(model, 201); + + addInputLatencyRelatedSlices(model, 297); + addSpaNavigationEvent(model, 300); + addFirstPaintSlice(model, 301); + }); + const histogram = getHistogramNamed( + SPA_NAVIGATION_START_TO_FIRST_PAINT_DURATION, model); + assert.strictEqual(3, histogram.running.count); + const expectedDuration1 = 101 - model.convertTimestampToModelTime( + 'traceEventClock', 99 - 3 + 0.1); + const expectedDuration2 = 201 - model.convertTimestampToModelTime( + 'traceEventClock', 198 - 3 + 0.1); + const expectedDuration3 = 301 - model.convertTimestampToModelTime( + 'traceEventClock', 297 - 3 + 0.1); + assert.closeTo(expectedDuration1 + expectedDuration2 + expectedDuration3, + histogram.running.sum, 0.5); + assert.closeTo(expectedDuration3, histogram.running.max, 0.5); + assert.closeTo(expectedDuration1, histogram.running.min, 0.5); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers.html new file mode 100644 index 00000000000..4bbe2a694f4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers.html @@ -0,0 +1,275 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/base/category_util.html"> +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/system_health/utils.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/model/timed_event.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + /** + * Returns the total self time of |event| within |rangeOfInterest|. Total self + * time is computed by finding time ranges that do not contain a descendant + * slice. Example: + * + * [ A ] + * | [ B ] [ C ] | + * | | [ D ] | | | | + * | | | | | | + * v v v v v v + * Ts : 0 50 80 100 150 180 200 + * RoI : [ ] + * + * Total self time for A within |rangeOfInterest| is 100 - 30 = 70. + * + * @param {!tr.b.Event} event + * @param {!tr.b.math.Range} rangeOfInterest + * @return {number} + */ + function getWallClockSelfTime_(event, rangeOfInterest) { + if (event.duration === 0) return 0; + + const selfTimeRanges = [rangeOfInterest.findIntersection(event.range)]; + for (const subSlice of event.subSlices) { + if (selfTimeRanges.length === 0) return 0; + + const lastRange = selfTimeRanges.pop(); + selfTimeRanges.push( + ...tr.b.math.Range.findDifference(lastRange, subSlice.range)); + } + + return tr.b.math.Statistics.sum(selfTimeRanges, r => r.duration); + } + + /** + * Returns the CPU self time of |event| within |rangeOfInterest|. CPU self + * time of a slice is assumed to be evenly distributed over the wall clock + * self time ranges of the slice. + * + * @param {!tr.b.Event} event + * @param {!tr.b.math.Range} rangeOfInterest + * @return {number} + */ + function getCPUSelfTime_(event, rangeOfInterest) { + if (event.duration === 0 || event.selfTime === 0) return 0; + if (event.cpuSelfTime === undefined) return 0; + const cpuTimeDensity = event.cpuSelfTime / event.selfTime; + return getWallClockSelfTime_(event, rangeOfInterest) * cpuTimeDensity; + } + + /** + * @callback getEventAttributeCallback + * @param {!tr.b.Event} event The event to read an attribute from. + * @return {number} The value of the attribute. + */ + + /** + * Generate a breakdown tree from all slices of |mainThread| in + * |rangeOfInterest|. The callback function |getEventSelfTime| specify how to + * get self time from a given event. + * + * @param {!tr.model.Thread} mainThread + * @param {!tr.b.math.Range} rangeOfInterest + * @callback {getEventAttributeCallback} getEventSelfTime + * @return {Object.<string, Object>} A time breakdown object whose keys are + * Chrome userfriendly title & values are an object that show the total spent + * in |rangeOfInterest|, and the list of event labels of the + * group and their total time in |rangeOfInterest|. + * + * Example: + * { + * layout: { + * total: 100, + * events: {'FrameView::performPreLayoutTasks': 20,..}}, + * v8_runtime: { + * total: 500, + * events: {'String::NewExternalTwoByte': 0.5,..}}, + * ... + * } + */ + function generateTimeBreakdownTree(mainThread, rangeOfInterest, + getEventSelfTime) { + if (mainThread === null) return; + const breakdownTree = {}; + for (const title of + tr.e.chrome.ChromeUserFriendlyCategoryDriver.ALL_TITLES) { + breakdownTree[title] = {total: 0, events: {}}; + } + // We do not look at async slices here. + for (const event of mainThread.sliceGroup.childEvents()) { + if (!rangeOfInterest.intersectsRangeExclusive(event.range)) continue; + const eventSelfTime = getEventSelfTime(event, rangeOfInterest); + const title = + tr.e.chrome.ChromeUserFriendlyCategoryDriver.fromEvent(event); + + breakdownTree[title].total += eventSelfTime; + if (breakdownTree[title].events[event.title] === undefined) { + breakdownTree[title].events[event.title] = 0; + } + breakdownTree[title].events[event.title] += + eventSelfTime; + + let timeIntersectionRatio = 0; + if (event.duration > 0) { + timeIntersectionRatio = + rangeOfInterest.findExplicitIntersectionDuration( + event.start, event.end) / event.duration; + } + + // TODO(#3846): v8_runtime is being double counted. + // TODO(#3846): v8_runtime slice name is wrong. It's 'stats'. + // TODO(#4296): v8_runtime should not be added for cpu time. + const v8Runtime = event.args['runtime-call-stat']; + if (v8Runtime !== undefined) { + const v8RuntimeObject = JSON.parse(v8Runtime); + for (const runtimeCall in v8RuntimeObject) { + // When the V8 Runtime Object contains 2 values, the 2nd value + // always represents the V8 Runtime duration. + if (v8RuntimeObject[runtimeCall].length === 2) { + if (breakdownTree.v8_runtime.events[runtimeCall] === undefined) { + breakdownTree.v8_runtime.events[runtimeCall] = 0; + } + const runtimeTime = tr.b.Unit.timestampFromUs( + v8RuntimeObject[runtimeCall][1] * timeIntersectionRatio); + breakdownTree.v8_runtime.total += runtimeTime; + breakdownTree.v8_runtime.events[runtimeCall] += runtimeTime; + } + } + } + } + return breakdownTree; + } + + /** + * Adds 'blocked_on_network' and 'idle' to the |breakdownTree| that has been + * generated by |generateTimeBreakdownTree|. Taking into account the + * |networkEvents|, this function is able to distinguish between these two + * types of cpu idle time during the range |rangeOfInterest| not used by + * events of the main thread |mainThreadEvents|. + * + * @param {!Object.<string, Object>} breakdownTree The breakdownTree that has + * been generated by |generateTimeBreakdownTree|. + * @param {!tr.b.Event} mainThreadEvents The top level events of the main + * thread. + * @param {!tr.b.Event} networkEvents The network events in the renderer. + * @param {!tr.b.math.Range} rangeOfInterest The range for which + * |breakdownTree| is calculated. + */ + function addIdleAndBlockByNetworkBreakdown_(breakdownTree, mainThreadEvents, + networkEvents, rangeOfInterest) { + const mainThreadEventRanges = tr.b.math.convertEventsToRanges( + mainThreadEvents); + const networkEventRanges = tr.b.math.convertEventsToRanges( + networkEvents); + const eventRanges = mainThreadEventRanges.concat(networkEventRanges); + const idleRanges = + tr.b.math.findEmptyRangesBetweenRanges(eventRanges, rangeOfInterest); + const totalFreeDuration = tr.b.math.Statistics.sum(idleRanges, + range => range.duration); + breakdownTree.idle = {total: totalFreeDuration, events: {}}; + + let totalBlockedDuration = rangeOfInterest.duration; + for (const [title, component] of Object.entries(breakdownTree)) { + // v8_runtime is a subcategory of script_execute. + // See github.com/catapult-project/catapult/commit/f3881d commit message. + // TODO(#2572) Make user friendly category hierarchy friend so this is not + // needed. + if (title === 'v8_runtime') continue; + totalBlockedDuration -= component.total; + } + + breakdownTree.blocked_on_network = { + // Clamp breakdown at 0 to prevent negative values. + // TODO(#4299): Since we do not explicitly prevent overlapping slices on + // the same thread, slices can end up with negative wall clock self time + // and thus breakdown values can be negative. If we can prevent + // overlapping slices in the model, we can do this clamping for very small + // (e.g. < -0.1) values, accounting only for floating point errors, and + // fail loudly otherwise. + total: Math.max(totalBlockedDuration, 0), + events: {} + }; + } + + /** + * Generate a breakdown that attributes where wall clock time goes in + * |rangeOfInterest| on the renderer thread. + * + * @param {!tr.model.Thread} mainThread + * @param {!tr.b.math.Range} rangeOfInterest + * @return {Object.<string, Object>} A time breakdown object whose keys are + * Chrome userfriendly titles & values are an object that shows the total + * wall clock time spent in |rangeOfInterest|, and the list of event + * labels of the group and their total wall clock time in |rangeOfInterest|. + * + * Example: + * { + * layout: { + * total: 100, + * events: {'FrameView::performPreLayoutTasks': 20,..}}, + * v8_runtime: { + * total: 500, + * events: {'String::NewExternalTwoByte': 0.5,..}}, + * ... + * } + */ + function generateWallClockTimeBreakdownTree( + mainThread, networkEvents, rangeOfInterest) { + const breakdownTree = generateTimeBreakdownTree( + mainThread, rangeOfInterest, getWallClockSelfTime_); + const mainThreadEventsInRange = tr.model.helpers.getSlicesIntersectingRange( + rangeOfInterest, mainThread.sliceGroup.topLevelSlices); + addIdleAndBlockByNetworkBreakdown_( + breakdownTree, mainThreadEventsInRange, networkEvents, rangeOfInterest); + return breakdownTree; + } + + /** + * Generate a breakdown that attributes where CPU time goes in + * |rangeOfInterest| on the renderer thread. + * + * Due to approximations, it is possible for breakdowns to not add up to total + * CPU time in |rangeOfInterest|. Note that |rangeOfInterest| is a range of + * wall times, not CPU time. + * + * @param {!tr.model.Thread} mainThread + * @param {!tr.b.math.Range} rangeOfInterest + * @return {Object.<string, Object>} A time breakdown object whose keys are + * Chrome userfriendly titles & values are an object that shows the total + * CPU time spent in |rangeOfInterestCpuTime|, and the list of event labels + * of the group and their total durations in |rangeOfInterestCpuTime|. + * + * Example: + * { + * layout: { + * total: 100, + * events: {'FrameView::performPreLayoutTasks': 20,..}}, + * v8_runtime: { + * total: 500, + * events: {'String::NewExternalTwoByte': 0.5,..}}, + * ... + * } + */ + function generateCpuTimeBreakdownTree(mainThread, rangeOfInterest) { + return generateTimeBreakdownTree(mainThread, rangeOfInterest, + getCPUSelfTime_); + } + + return { + generateTimeBreakdownTree, + generateWallClockTimeBreakdownTree, + generateCpuTimeBreakdownTree, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers_test.html new file mode 100644 index 00000000000..883c915c07f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/breakdown_tree_helpers_test.html @@ -0,0 +1,337 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/system_health/loading_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const rendererPid = 12345; + const model = tr.c.TestUtils.newModel(function(model) { + const rendererProcess = model.getOrCreateProcess(rendererPid); + const mainThread = rendererProcess.getOrCreateThread(2); + mainThread.name = 'CrRendererMain'; + + // Our main thread looks like: + // + // [ parseHTML ] [ layout ] [ V8.Exec ] + // | [ V8.Exec ] | | | | [ layout ] | + // | | | | | | | | | | + // | | | | | | | | | | + // v v v v v v v v v v + // Ts: 200 250 300 400 450 550 570 600 620 650 + // Cpu:1160 1200 1240 1320 1360 1440 1456 1480 1496 1520 + + // Add layout categories + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink', + title: 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser', + start: 200, + duration: 200, + cpuStart: 1160, + cpuDuration: 160, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'devtools.timeline', + title: 'Script', + start: 250, + duration: 50, + cpuStart: 1200, + cpuDuration: 40, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + start: 250, + duration: 50, + args: {'runtime-call-stat': '{"ICMiss": [3, 150], "GC": [10, 60]}'}, + cpuStart: 1200, + cpuDuration: 40, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink', + title: 'LocalFrameView::layout', + start: 450, + duration: 100, + cpuStart: 1360, + cpuDuration: 80, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + start: 570, + duration: 80, + args: {'runtime-call-stat': '{"DeOptimize": [1, 42], "GC": [3, 50]}'}, + cpuStart: 1456, + cpuDuration: 64, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink', + title: 'WebViewImpl::updateAllLifecyclePhases', + start: 600, + duration: 20, + cpuStart: 1480, + cpuDuration: 16, + })); + }); + + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + + const rendererHelper = chromeHelper.rendererHelpers[rendererPid]; + + test('testWallClockTimeBreakdownNoIntersectingBoundary', function() { + const rangeOfInterest = tr.b.math.Range.fromExplicitRange(0, 1000); + const networkEvents = tr.metrics.sh.getNetworkEventsInRange( + rendererHelper.process, rangeOfInterest); + const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree( + rendererHelper.mainThread, networkEvents, rangeOfInterest); + assert.deepEqual({ + total: 150, + events: { + 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser': 150 + } + }, breakdownTree.parseHTML); + assert.deepEqual({ + total: 120, + events: { + 'LocalFrameView::layout': 100, + 'WebViewImpl::updateAllLifecyclePhases': 20, + } + }, breakdownTree.layout); + assert.deepEqual({ + total: 110, + events: { + 'V8.Execute': 110, + } + }, breakdownTree.script_execute); + assert.deepEqual({ + total: 0.302, + events: { + 'DeOptimize': 0.042, + 'GC': 0.11, + 'ICMiss': 0.15, + } + }, breakdownTree.v8_runtime); + }); + + test('testWallClockTimeBreakdownIntersectingBoundary', function() { + // Our main thread looks like: + // + // [ parseHTML ] [ layout ] [ V8.Exec ] + // | [ V8.Exec ] | | | | [ layout ] | + // | | | | | | | | | | + // | | | | | | | | | | + // v v v v v v v v v v + // Ts: 200 250 300 400 450 550 570 600 620 650 + // | | + // 275 610 + const rangeOfInterest = tr.b.math.Range.fromExplicitRange(275, 610); + const networkEvents = tr.metrics.sh.getNetworkEventsInRange( + rendererHelper.process, rangeOfInterest); + const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree( + rendererHelper.mainThread, networkEvents, rangeOfInterest); + assert.deepEqual({ + total: 100, + events: { + 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser': 100 + } + }, breakdownTree.parseHTML); + assert.deepEqual({ + total: 110, + events: { + 'LocalFrameView::layout': 100, + 'WebViewImpl::updateAllLifecyclePhases': 10, + } + }, breakdownTree.layout); + assert.deepEqual({ + total: 55, + events: { + 'V8.Execute': 55, + } + }, breakdownTree.script_execute); + assert.deepEqual({ + total: 0.151, + events: { + 'DeOptimize': 0.021, + 'GC': 0.055, + 'ICMiss': 0.075, + } + }, breakdownTree.v8_runtime); + }); + + test('testCpuTimeBreakdownNoIntersectingBoundary', function() { + const rangeOfInterest = tr.b.math.Range.fromExplicitRange(100, 800); + const breakdownTree = tr.metrics.sh.generateCpuTimeBreakdownTree( + rendererHelper.mainThread, + rangeOfInterest); + assert.deepEqual({ + total: 120, + events: { + 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser': 120 + } + }, breakdownTree.parseHTML); + assert.deepEqual({ + total: 96, + events: { + 'LocalFrameView::layout': 80, + 'WebViewImpl::updateAllLifecyclePhases': 16, + } + }, breakdownTree.layout); + assert.deepEqual({ + total: 88, + events: { + 'V8.Execute': 88, + } + }, breakdownTree.script_execute); + assert.deepEqual({ + total: 0.302, + events: { + 'DeOptimize': 0.042, + 'GC': 0.11, + 'ICMiss': 0.15, + } + }, breakdownTree.v8_runtime); + }); + + test('testCpuTimeBreakdownIntersectingBoundary', function() { + // Our main thread looks like: + // + // [ parseHTML ] [ layout ] [ V8.Exec ] + // | [ V8.Exec ] | | | | [ layout ] | + // | | | | | | | | | | + // | | | | | | | | | | + // v v v v v v v v v v + // Ts: 200 250 300 400 450 550 570 600 620 650 + // Cpu:1160 1200 1240 1320 1360 1440 1456 1480 1496 1520 + // [ ] + // Ts RoI 275 610 + const rangeOfInterest = tr.b.math.Range.fromExplicitRange(275, 610); + const breakdownTree = tr.metrics.sh.generateCpuTimeBreakdownTree( + rendererHelper.mainThread, + rangeOfInterest); + assert.deepEqual({ + total: 80, + events: { + 'HTMLDocumentParser::didReceiveParsedChunkFromBackgroundParser': 80 + } + }, breakdownTree.parseHTML); + assert.deepEqual({ + total: 88, + events: { + 'LocalFrameView::layout': 80, + 'WebViewImpl::updateAllLifecyclePhases': 8, + } + }, breakdownTree.layout); + assert.deepEqual({ + total: 44, + events: { + 'V8.Execute': 44, + } + }, breakdownTree.script_execute); + assert.deepEqual({ + total: 0.151, + events: { + 'DeOptimize': 0.021, + 'GC': 0.055, + 'ICMiss': 0.075 + } + }, breakdownTree.v8_runtime); + }); + + function createNetworkEvent(start, end, cat) { + return tr.c.TestUtils.newAsyncSliceEx({ + cat, + title: 'network events', + start, + duration: end - start, + }); + } + + test('testBlockedOnNetwork_onlyNetEvent', function() { + const model = tr.c.TestUtils.newModel(model => { + const mainThread = model.getOrCreateProcess(0) + .getOrCreateThread(0); + mainThread.name = 'CrRendererMain'; + const networkEvent = createNetworkEvent(100, 200, 'net'); + mainThread.asyncSliceGroup.push(networkEvent); + }); + const rendererHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper) + .rendererHelpers[0]; + const rangeOfInterest = tr.b.math.Range.fromExplicitRange(0, 150); + const networkEvents = tr.metrics.sh.getNetworkEventsInRange( + rendererHelper.process, rangeOfInterest); + const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree( + rendererHelper.mainThread, networkEvents, rangeOfInterest); + assert.strictEqual(breakdownTree.blocked_on_network.total, 50); + assert.strictEqual(breakdownTree.idle.total, 100); + }); + + test('testBlockedOnNetwork_netEventAndMainThreadEvent', function() { + const model = tr.c.TestUtils.newModel(model => { + const mainThread = model.getOrCreateProcess(0) + .getOrCreateThread(0); + mainThread.name = 'CrRendererMain'; + const networkEvent = createNetworkEvent(100, 200, 'net'); + mainThread.asyncSliceGroup.push(networkEvent); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink', + title: 'LocalFrameView::layout', + start: 160, + duration: 140, + })); + }); + const rendererHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper) + .rendererHelpers[0]; + const rangeOfInterest = tr.b.math.Range.fromExplicitRange(150, 320); + const networkEvents = tr.metrics.sh.getNetworkEventsInRange( + rendererHelper.process, rangeOfInterest); + const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree( + rendererHelper.mainThread, networkEvents, rangeOfInterest); + assert.strictEqual(breakdownTree.layout.total, 140); + assert.strictEqual(breakdownTree.blocked_on_network.total, 10); + assert.strictEqual(breakdownTree.idle.total, 20); + }); + + test('testBlockedOnNetwork_rangeEmpty', function() { + const model = tr.c.TestUtils.newModel(model => { + const mainThread = model.getOrCreateProcess(0) + .getOrCreateThread(0); + mainThread.name = 'CrRendererMain'; + const networkEvent = createNetworkEvent(100, 200, 'net'); + mainThread.asyncSliceGroup.push(networkEvent); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink', + title: 'FrameView::layout', + start: 160, + duration: 140, + })); + }); + const rendererHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper) + .rendererHelpers[0]; + const rangeOfInterest = new tr.b.math.Range(); + const networkEvents = tr.metrics.sh.getNetworkEventsInRange( + rendererHelper.process, rangeOfInterest); + const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree( + rendererHelper.mainThread, networkEvents, rangeOfInterest); + assert.strictEqual(breakdownTree.layout.total, 0); + assert.strictEqual(breakdownTree.blocked_on_network.total, 0); + assert.strictEqual(breakdownTree.idle.total, 0); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric.html new file mode 100644 index 00000000000..8493e899025 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/system_health/utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + // Bin boundaries for clock sync latency. 0-20 ms with 0.2 ms bins. + // 20 ms is a good upper limit because the highest latencies we've seen are + // around 10-15 ms, and we expect average latency to go down as we improve + // the clock sync mechanism. + const LATENCY_BOUNDS = tr.v.HistogramBinBoundaries.createLinear(0, 20, 100); + + function clockSyncLatencyMetric(values, model) { + const domains = Array.from(model.clockSyncManager.domainsSeen).sort(); + for (let i = 0; i < domains.length; i++) { + for (let j = i + 1; j < domains.length; j++) { + const latency = model.clockSyncManager.getTimeTransformerError( + domains[i], domains[j]); + const hist = new tr.v.Histogram('clock_sync_latency_' + + domains[i].toLowerCase() + '_to_' + domains[j].toLowerCase(), + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, LATENCY_BOUNDS); + hist.customizeSummaryOptions({ + avg: true, + count: false, + max: false, + min: false, + std: false, + sum: false, + }); + hist.description = 'Clock sync latency for domain ' + domains[i] + + ' to domain ' + domains[j]; + hist.addSample(latency); + values.addHistogram(hist); + } + } + } + + tr.metrics.MetricRegistry.register(clockSyncLatencyMetric); + + return { + clockSyncLatencyMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric_test.html new file mode 100644 index 00000000000..27b5371763e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/clock_sync_latency_metric_test.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/system_health/clock_sync_latency_metric.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('clockSyncLatencyMetric', function() { + const model = new tr.Model(); + model.clockSyncManager.addClockSyncMarker( + tr.model.ClockDomainId.TELEMETRY, 'ID01', 1.0, 4.0); + model.clockSyncManager.addClockSyncMarker( + tr.model.ClockDomainId.TELEMETRY, 'ID02', 2.0, 8.0); + model.clockSyncManager.addClockSyncMarker( + tr.model.ClockDomainId.BATTOR, 'ID01', 2.5); + model.clockSyncManager.addClockSyncMarker( + tr.model.ClockDomainId.WIN_QPC, 'ID02', 5.0); + + const battorToWinQpcName = 'clock_sync_latency_' + + tr.model.ClockDomainId.BATTOR.toLowerCase() + '_to_' + + tr.model.ClockDomainId.WIN_QPC.toLowerCase(); + const winQpcToTelemetryName = 'clock_sync_latency_' + + tr.model.ClockDomainId.TELEMETRY.toLowerCase() + '_to_' + + tr.model.ClockDomainId.WIN_QPC.toLowerCase(); + const battorToTelemetryName = 'clock_sync_latency_' + + tr.model.ClockDomainId.BATTOR.toLowerCase() + '_to_' + + tr.model.ClockDomainId.TELEMETRY.toLowerCase(); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.clockSyncLatencyMetric(histograms, model); + + let battorToWinQpcValue = undefined; + let winQpcToTelemetryValue = undefined; + let battorToTelemetryValue = undefined; + for (const value of histograms) { + if (value.name === battorToWinQpcName) { + battorToWinQpcValue = value; + } else if (value.name === winQpcToTelemetryName) { + winQpcToTelemetryValue = value; + } else if (value.name === battorToTelemetryName) { + battorToTelemetryValue = value; + } + } + + // Clock sync graph is: + // [WIN_QPC] --6ms-> [TELEMETRY] --3ms-> [BATTOR] + + assert.isDefined(battorToWinQpcValue); + assert.isDefined(winQpcToTelemetryValue); + assert.isDefined(battorToTelemetryValue); + assert.closeTo(battorToWinQpcValue.average, 9.0, 1e-5); + assert.closeTo(winQpcToTelemetryValue.average, 6.0, 1e-5); + assert.closeTo(battorToTelemetryValue.average, 3.0, 1e-5); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric.html new file mode 100644 index 00000000000..f8a63e84ba7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/model/helpers/chrome_renderer_helper.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + // Use a lower bound of 0.01 for the metric boundaries (when no CPU time + // is consumed) and an upper bound of 50 (fifty cores are all active + // for the entire time). We can't use zero exactly for the lower bound with an + // exponential histogram. + const CPU_TIME_PERCENTAGE_BOUNDARIES = + tr.v.HistogramBinBoundaries.createExponential(0.01, 50, 200); + + /** + * This metric measures total CPU time for Chrome processes, per second of + * clock time. + * This metric requires only the 'toplevel' tracing category. + * + * @param {!tr.v.HistogramSet} histograms + * @param {!tr.model.Model} model + * @param {!Object=} opt_options + */ + function cpuTimeMetric(histograms, model, opt_options) { + let rangeOfInterest = model.bounds; + + if (opt_options && opt_options.rangeOfInterest) { + rangeOfInterest = opt_options.rangeOfInterest; + } else { + // If no range of interest is provided, limit the relevant range to + // Chrome processes. This prevents us from normalizing against non-Chrome + // related slices in the trace. + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (chromeHelper) { + const chromeBounds = chromeHelper.chromeBounds; + if (chromeBounds) { + rangeOfInterest = chromeBounds; + } + } + } + + let allProcessCpuTime = 0; + + for (const pid in model.processes) { + const process = model.processes[pid]; + if (tr.model.helpers.ChromeRendererHelper.isTracingProcess(process)) { + continue; + } + + let processCpuTime = 0; + for (const tid in process.threads) { + const thread = process.threads[tid]; + processCpuTime += thread.getCpuTimeForRange(rangeOfInterest); + } + allProcessCpuTime += processCpuTime; + } + + // Normalize cpu time by clock time. + let normalizedAllProcessCpuTime = 0; + if (rangeOfInterest.duration > 0) { + normalizedAllProcessCpuTime = + allProcessCpuTime / rangeOfInterest.duration; + } + + const unit = tr.b.Unit.byName.normalizedPercentage_smallerIsBetter; + const cpuTimeHist = new tr.v.Histogram( + 'cpu_time_percentage', unit, CPU_TIME_PERCENTAGE_BOUNDARIES); + cpuTimeHist.description = + 'Percent CPU utilization, normalized against a single core. Can be ' + + 'greater than 100% if machine has multiple cores.'; + cpuTimeHist.customizeSummaryOptions({ + avg: true, + count: false, + max: false, + min: false, + std: false, + sum: false + }); + cpuTimeHist.addSample(normalizedAllProcessCpuTime); + histograms.addHistogram(cpuTimeHist); + } + + tr.metrics.MetricRegistry.register(cpuTimeMetric, { + supportsRangeOfInterest: true + }); + + return { + cpuTimeMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric_test.html new file mode 100644 index 00000000000..e68b452f4f7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_metric_test.html @@ -0,0 +1,163 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html"> +<link rel="import" href="/tracing/metrics/system_health/cpu_time_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function computeCpuTime(customizeModelCallback, opt_options) { + const model = tr.c.TestUtils.newModel(function(model) { + customizeModelCallback(model); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.cpuTimeMetric(histograms, model, opt_options); + return tr.b.getOnlyElement(histograms).average; + } + + // There are two slices, each of length 50. The total bounds is 3000. + // This yields total CPU time of 100ms, averaged over 3 seconds is 33ms. + test('cpuTimeMetric_oneProcess', function() { + const sliceDuration = 50; + const totalDuration = 3000; + const value = computeCpuTime(function(model) { + model.rendererProcess = model.getOrCreateProcess(2); + model.rendererMain = model.rendererProcess.getOrCreateThread(3); + model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: sliceDuration, + cpuStart: 0, + cpuDuration: sliceDuration, + })); + model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: totalDuration - sliceDuration, + duration: sliceDuration, + cpuStart: totalDuration - sliceDuration, + cpuDuration: sliceDuration, + })); + }); + assert.closeTo(value, 0.033, 0.001); + }); + + // Normalize against chrome processes, whose slices (both with and without + // CPU data) go from 2900 to 3000. + test('cpuTimeMetric_browserProcess', function() { + const sliceDuration = 50; + const totalDuration = 3000; + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + model.browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: totalDuration - 2 * sliceDuration, + duration: 2 * sliceDuration, + })); + model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: totalDuration - sliceDuration, + duration: sliceDuration, + cpuStart: totalDuration - sliceDuration, + cpuDuration: sliceDuration, + })); + + const nonChromeProcess = model.getOrCreateProcess(1234); + nonChromeProcess.name = 'Telemetry'; + const nonChromeThread = nonChromeProcess.getOrCreateThread(1); + nonChromeThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: sliceDuration, + cpuStart: 0, + cpuDuration: sliceDuration, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.cpuTimeMetric(histograms, model); + const value = tr.b.getOnlyElement(histograms).average; + assert.closeTo(value, 0.5, 0.001); + }); + + // Makes sure that rangeOfInterest works correctly. + test('cpuTimeMetric_oneProcess_rangeOfInterest', function() { + const sliceDuration = 50; + const totalDuration = 3000; + const rangeOfInterest = new tr.b.math.Range.fromExplicitRange(-10, 30); + const options = {}; + options.rangeOfInterest = rangeOfInterest; + const value = computeCpuTime(function(model) { + model.rendererProcess = model.getOrCreateProcess(2); + model.rendererMain = model.rendererProcess.getOrCreateThread(3); + model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: sliceDuration, + cpuStart: 0, + cpuDuration: sliceDuration, + })); + model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: totalDuration - sliceDuration, + duration: sliceDuration, + cpuStart: totalDuration - sliceDuration, + cpuDuration: sliceDuration, + })); + }, options); + assert.closeTo(value, 0.75, 0.001); + }); + + // Process 1: There are two slices, each of length 50. The total bounds is + // 3000. Process 2: There is one slice of length 50. + // This yields total CPU time of 150ms, averaged over 3 seconds is 50ms. + test('cpuTimeMetric_twoProcesses', function() { + const sliceDuration = 50; + const totalDuration = 3000; + const value = computeCpuTime(function(model) { + model.rendererProcess = model.getOrCreateProcess(2); + model.rendererMain = model.rendererProcess.getOrCreateThread(3); + model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: sliceDuration, + cpuStart: 0, + cpuDuration: sliceDuration, + })); + model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: totalDuration - sliceDuration, + duration: sliceDuration, + cpuStart: totalDuration - sliceDuration, + cpuDuration: sliceDuration, + })); + + const otherProcess = model.getOrCreateProcess(3); + const otherThread = otherProcess.getOrCreateThread(4); + otherThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: sliceDuration, + cpuStart: 0, + cpuDuration: sliceDuration, + })); + }); + assert.closeTo(value, 0.05, 0.001); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter.html new file mode 100644 index 00000000000..6d88c1b81ca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter.html @@ -0,0 +1,182 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + const CPU_PERCENTAGE_UNIT = + tr.b.Unit.byName.normalizedPercentage_smallerIsBetter; + const CPU_TIME_UNIT = tr.b.Unit.byName.timeDurationInMs_smallerIsBetter; + + /** + * Returns a deep clone of CPU time multidimensional view path object. + * + * @param {!Array.<!Array.<string>>} previousPath + * @returns {!Array.<!Array.<string>>} + */ + function clonePath_(previousPath) { + return previousPath.map(subPath => subPath.map(x => x)); + } + + + /** + * Returns an object containing the processType, threadType, railStage, and + * initiatorType encoded in the provided CPU time multidimensional view path + * object. Decoding the path into this object can make code easier to + * understand than indexing directly into the path array. + * + * @param {!Array.<!Array.<string>>} path - A path in a CPU time + * multidimensional tree view. + * @returns {Object.<string, string>} + */ + function decodePath_(path) { + return { + processType: path[0][0], + threadType: path[1][0], + railStage: path[2][0], + initiatorType: path[2][1] + }; + } + + /** + * Returns a unique string representation of |path|. + * + * Paths are of the following form in CPU time multidimensional trees: + * [[processType], [threadType], [railStage, initiatorType]] + * + * The returned string is of the form + * "$processtype:$threadType:$railStage:$initiatorType". + * + * @param {Array.<!Array.<string>>} path + * @returns {string} + */ + function stringifyPathName_(path) { + const decodedPath = decodePath_(path); + return [ + decodedPath.processType, + decodedPath.threadType, + decodedPath.railStage, + decodedPath.initiatorType + ].join(':'); + } + + /** + * This class is used to traverse a multidimensional tree view and report CPU + * percentage and CPU time from the tree as histograms. + */ + class CpuTimeTreeDataReporter { + constructor() { + this.visitedSet_ = new Set(); + } + + /** + * Extracts CPU percentage and CPU time values from |node| located at |path| + * and adds values as histograms to |this.histogramSet_|. Each value is + * added as a single sample histogram. + * + * @param {!tr.b.MultiDimensionalViewNode} node + * @param {!Array.<!Array.<string>>} path + */ + reportValuesFromNode_(node, path) { + const decodedPath = decodePath_(path); + const processType = decodedPath.processType || 'all_processes'; + const threadType = decodedPath.threadType || 'all_threads'; + + // We need some RAIL stage and some initiator type to process a node. + // All RAIL stages and all initiator types are handled by the special + // 'all_stages' and 'all_initiators' nodes respectively. + if (!decodedPath.railStage || !decodedPath.initiatorType) return; + const {railStage, initiatorType} = decodedPath; + + const serializedPathName = + [processType, threadType, railStage, initiatorType].join(':'); + + // node.values is a two element array. The first element holds + // cpuPercentage data and the second holds cpuTime data. The final + // '.total' (as opposed to '.self') signifies we're including all the data + // from children nodes. This is an artifact of how the multidimensional + // view data structure works and is not very relevant - we exclusively use + // '.total' for CPU time. + const cpuPercentageValue = node.values[0].total; + const cpuTimeValue = node.values[1].total; + + this.histogramSet_.createHistogram(`cpuPercentage:${serializedPathName}`, + CPU_PERCENTAGE_UNIT, cpuPercentageValue); + this.histogramSet_.createHistogram(`cpuTime:${serializedPathName}`, + CPU_TIME_UNIT, cpuTimeValue); + } + + + /** + * Traverses all the paths of a multidimensional view subtree and reports + * node data to |this.histogramSet_|. + * + * @param {!tr.b.MultiDimensionalViewNode} root - Root of the subtree. + * @param {!Array.<!Array.<string>>} rootPath - Path of the subtree root + * node with respect to |this.rootNode_|. + */ + reportDataFromTree_(root, rootPath) { + const rootPathString = stringifyPathName_(rootPath); + if (this.visitedSet_.has(rootPathString)) return; + this.visitedSet_.add(rootPathString); + + this.reportValuesFromNode_(root, rootPath); + + for (let dimension = 0; dimension < root.children.length; dimension++) { + const children = root.children[dimension]; + for (const [name, node] of children) { + const childPath = clonePath_(rootPath); + childPath[dimension].push(name); + this.reportDataFromTree_(node, childPath); + } + } + } + + /** + * Adds values from the multidimensional tree view rooted at |rootNode| as + * single value histograms in |histogramSet|. + * + * @param {!tr.b.MultiDimensionalViewNode} rootNode + * @param {!tr.v.HistogramSet} histogramSet + */ + addTreeValuesToHistogramSet(rootNode, histogramSet) { + const rootPath = [[], [], []]; + this.rootNode_ = rootNode; + this.histogramSet_ = histogramSet; + this.reportDataFromTree_(this.rootNode_, rootPath); + } + + /** + * Reports values from the multidimensional tree view rooted at |rootNode| + * as single value histograms in |histogramSet|. + * + * The histograms are dynamically generated from the tree. The histogram + * names are of the form + * "${cpuTime|cpuPercentage}:${processType}:${threadType}:" + + * "${railStage}:${railStageInitiator}" + * + * cpuTime histograms contain total consumed cpu time, while cpuPercentage + * histograms contain cpu time as a percentage of wall time. In multicore + * situations, this percentage can be larger than 100. + * + * @param {!tr.b.MultiDimensionalViewNode} rootNode + * @param {!tr.v.HistogramSet} histogramSet + */ + static reportToHistogramSet(rootNode, histogramSet) { + const reporter = new CpuTimeTreeDataReporter(); + reporter.addTreeValuesToHistogramSet(rootNode, histogramSet); + } + } + + return { + CpuTimeTreeDataReporter, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter_test.html new file mode 100644 index 00000000000..cd72b21868d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/cpu_time_tree_data_reporter_test.html @@ -0,0 +1,155 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/base/multi_dimensional_view.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_processes.html"> +<link rel="import" href="/tracing/metrics/system_health/cpu_time_tree_data_reporter.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const reportToHistogramSet = + tr.metrics.sh.CpuTimeTreeDataReporter.reportToHistogramSet; + + test('reportToHistogramSet_reportsLeafNodes', () => { + const mdvBuilder = new tr.b.MultiDimensionalViewBuilder( + 3 /* dimensions (process, thread and rail stage / initiator) */, + 2 /* valueCount (cpuPercentage and cpuTime) */); + mdvBuilder.addPath( + [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']], + [42, 43], tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL); + const rootNode = mdvBuilder.buildTopDownTreeView(); + + const histograms = new tr.v.HistogramSet(); + reportToHistogramSet(rootNode, histograms); + + const cpuPercentageHistogram = histograms.getHistogramNamed( + 'cpuPercentage:browser_process:CrBrowserMain:Animation:CSS'); + const cpuTimeHistogram = histograms.getHistogramNamed( + 'cpuTime:browser_process:CrBrowserMain:Animation:CSS'); + + // Histograms exist. + assert.isDefined(cpuPercentageHistogram); + assert.isDefined(cpuTimeHistogram); + + // Each histogram contains a single sample. + assert.strictEqual(cpuPercentageHistogram.running.count, 1); + assert.strictEqual(cpuTimeHistogram.running.count, 1); + + // Histogram sample value is correct. + assert.closeTo(cpuPercentageHistogram.sum, 42, 1e-7); + assert.closeTo(cpuTimeHistogram.sum, 43, 1e-7); + }); + + test('reportToHistogramSet_reportsAllProcesses', () => { + const mdvBuilder = new tr.b.MultiDimensionalViewBuilder( + 3 /* dimensions (process, thread and rail stage / initiator) */, + 2 /* valueCount (cpuPercentage and cpuTime) */); + mdvBuilder.addPath( + [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']], + [42, 43], tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL); + const rootNode = mdvBuilder.buildTopDownTreeView(); + + const histograms = new tr.v.HistogramSet(); + reportToHistogramSet(rootNode, histograms); + + const cpuPercentageHistogram = histograms.getHistogramNamed( + 'cpuPercentage:all_processes:CrBrowserMain:Animation:CSS'); + const cpuTimeHistogram = histograms.getHistogramNamed( + 'cpuTime:all_processes:CrBrowserMain:Animation:CSS'); + + // Histograms exist. + assert.isDefined(cpuPercentageHistogram); + assert.isDefined(cpuTimeHistogram); + + // Each histogram contains a single sample. + assert.strictEqual(cpuPercentageHistogram.running.count, 1); + assert.strictEqual(cpuTimeHistogram.running.count, 1); + + // Histogram sample value is correct. + assert.closeTo(cpuPercentageHistogram.sum, 42, 1e-7); + assert.closeTo(cpuTimeHistogram.sum, 43, 1e-7); + }); + + test('reportToHistogramSet_reportsAllThreads', () => { + const mdvBuilder = new tr.b.MultiDimensionalViewBuilder( + 3 /* dimensions (process, thread and rail stage / initiator) */, + 2 /* valueCount (cpuPercentage and cpuTime) */); + mdvBuilder.addPath( + [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']], + [42, 43], tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL); + const rootNode = mdvBuilder.buildTopDownTreeView(); + + const histograms = new tr.v.HistogramSet(); + reportToHistogramSet(rootNode, histograms); + + const cpuPercentageHistogram = histograms.getHistogramNamed( + 'cpuPercentage:browser_process:all_threads:Animation:CSS'); + const cpuTimeHistogram = histograms.getHistogramNamed( + 'cpuTime:browser_process:all_threads:Animation:CSS'); + + // Histograms exist. + assert.isDefined(cpuPercentageHistogram); + assert.isDefined(cpuTimeHistogram); + + // Each histogram contains a single sample. + assert.strictEqual(cpuPercentageHistogram.running.count, 1); + assert.strictEqual(cpuTimeHistogram.running.count, 1); + + // Histogram sample value is correct. + assert.closeTo(cpuPercentageHistogram.sum, 42, 1e-7); + assert.closeTo(cpuTimeHistogram.sum, 43, 1e-7); + }); + + test('reportToHistogramSet_doesNotAggregateStagesWithoutAllStagesNode', + () => { + const mdvBuilder = new tr.b.MultiDimensionalViewBuilder( + 3 /* dimensions (process, thread and rail stage / initiator) */, + 2 /* valueCount (cpuPercentage and cpuTime) */); + mdvBuilder.addPath( + [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']], + [42, 43], tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL); + const rootNode = mdvBuilder.buildTopDownTreeView(); + + const histograms = new tr.v.HistogramSet(); + reportToHistogramSet(rootNode, histograms); + + // all_stages:CSS does not make sense (e.g. we will never + // aggregate Scroll Response and Scroll Animation, even if that's a + // thing), so we use all_stages:all_initiators here. + assert.isUndefined(histograms.getHistogramNamed( + 'cpuPercentage:' + + 'browser_process:CrBrowserMain:all_stages:all_initiators')); + assert.isUndefined(histograms.getHistogramNamed( + 'cpuTime:' + + 'browser_process:CrBrowserMain:all_stages:all_initiators')); + }); + + test('reportToHistogramSet_' + + 'doesNotAggregateInitiatorsWithoutAllInitiatorsNode', + () => { + const mdvBuilder = new tr.b.MultiDimensionalViewBuilder( + 3 /* dimensions (process, thread and rail stage / initiator) */, + 2 /* valueCount (cpuPercentage and cpuTime) */); + mdvBuilder.addPath( + [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']], + [42, 43], tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL); + const rootNode = mdvBuilder.buildTopDownTreeView(); + + const histograms = new tr.v.HistogramSet(); + reportToHistogramSet(rootNode, histograms); + + assert.isUndefined(histograms.getHistogramNamed( + 'cpuPercentage:' + + 'browser_process:CrBrowserMain:Animation:all_initiators')); + assert.isUndefined(histograms.getHistogramNamed( + 'cpuTime:browser_process:CrBrowserMain:Animation:all_initiators')); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric.html new file mode 100644 index 00000000000..b9ef3de7ebd --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric.html @@ -0,0 +1,462 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/base/utils.html"> +<link rel="import" href="/tracing/extras/chrome/estimated_input_latency.html"> +<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html"> +<link rel="import" href="/tracing/extras/v8/runtime_stats_entry.html"> +<link rel="import" href="/tracing/metrics/v8/utils.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +/** + * @fileoverview + * This file defines the input latency metric estimated as the maximum + * expected queueing time (EQT) in sliding window of size 500ms. + * + * The EQT is defined as the average queueing time of a hypothetical input + * event arriving at a random time in the given time window. + * For more information see: + * - https://goo.gl/OQ2bX6 + * - https://goo.gl/jmWpMl + * - https://goo.gl/lga4iO + */ +tr.exportTo('tr.metrics.sh', function() { + // The size of the sliding window is chosen arbitrarily (see + // https://goo.gl/lga4iO). + const WINDOW_SIZE_MS = 500; + const EQT_BOUNDARIES = tr.v.HistogramBinBoundaries + .createExponential(0.01, WINDOW_SIZE_MS, 50); + + /** + * Returns true if the slice contains a forced GC event. Some stories force + * garbage collection before sampling memory usage. Since a forced GC takes + * long time we need to ignore it to avoid biasing the input latency results. + */ + function containsForcedGC_(slice) { + return slice.findTopmostSlicesRelativeToThisSlice( + tr.metrics.v8.utils.isForcedGarbageCollectionEvent).length > 0; + } + + /** + * @param {!string} name Name of the histogram. + * @param {!string} description Description of the histogram. + * @returns {!tr.v.Histogram} + */ + function getOrCreateHistogram_(histograms, name, description) { + return histograms.getHistogramNamed(name) || histograms.createHistogram( + name, tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], { + binBoundaries: EQT_BOUNDARIES, + description, + summaryOptions: { + avg: false, + count: false, + max: true, + min: false, + std: false, + sum: false, + }, + }); + } + + /** + * Computes the maximum expected queueing time in the sliding time window + * of size 500ms (WINDOW_SIZE_MS). The function produces four Histograms: + * - total:500ms_window:renderer_eqt, + * - total:500ms_window:renderer_eqt_cpu, + * - interactive:500ms_window:renderer_eqt. + * - interactive:500ms_window:renderer_eqt_cpu. + * The 'total' histograms are computed for the whole trace. The 'interactive' + * histograms are computed for the time while the page is interactive. + * The 'cpu' histograms use the CPU time of the events instead of the wall- + * clock times. Each renderer process adds one sample to the histograms. + */ + function expectedQueueingTimeMetric(histograms, model) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + const rendererHelpers = Object.values(chromeHelper.rendererHelpers); + const rendererToInteractiveTimestamps = + tr.e.chrome.getInteractiveTimestamps(model); + addExpectedQueueingTimeMetric_( + 'renderer_eqt', + event => {return {start: event.start, duration: event.duration};}, + false, rendererHelpers, rendererToInteractiveTimestamps, histograms, + model); + addExpectedQueueingTimeMetric_( + 'renderer_eqt_cpu', + event => {return {start: event.cpuStart, duration: event.cpuDuration};}, + true, rendererHelpers, rendererToInteractiveTimestamps, histograms, + model); + } + + /** + * @callback EventTimesCallback + * @param {!tr.b.Event} event + * @return {{start: !number, duration: !number}} event start time and duration. + */ + + /** + * The actual implementation of the EQT metric. + * @param {!string} eqtName the metric name part of the histogram name. + * @param {!EventTimesCallback} getEventTimes. + * @param {!Array.<tr.model.helpers.ChromeRendererHelper>} rendererHelpers. + * @param {!Map.<number, Array.<number>>} rendererToInteractiveTimestamps + * a map from renderer pid to an array of interactive timestamps. + */ + function addExpectedQueueingTimeMetric_(eqtName, getEventTimes, isCpuTime, + rendererHelpers, rendererToInteractiveTimestamps, histograms, model) { + /** + * Extracts tasks for EQT computation from the given renderer. + * A task is a pair of {start, end} times. + */ + function getTasks(rendererHelper) { + const tasks = []; + for (const slice of + tr.e.chrome.EventFinderUtils.findToplevelSchedulerTasks( + rendererHelper.mainThread)) { + const times = getEventTimes(slice); + if (times.duration > 0 && !containsForcedGC_(slice)) { + tasks.push({start: times.start, end: times.start + times.duration}); + } + } + return tasks; + } + const totalHistogram = getOrCreateHistogram_( + histograms, + `total:${WINDOW_SIZE_MS}ms_window:${eqtName}`, + `The maximum EQT in a ${WINDOW_SIZE_MS}ms sliding window` + + ' for a given renderer'); + const interactiveHistogram = getOrCreateHistogram_( + histograms, + `interactive:${WINDOW_SIZE_MS}ms_window:${eqtName}`, + `The maximum EQT in a ${WINDOW_SIZE_MS}ms sliding window` + + ' for a given renderer while the page is interactive'); + for (const rendererHelper of rendererHelpers) { + if (rendererHelper.isChromeTracingUI) continue; + // Renderers with lifetime smaller than WINDOW_SIZE_MS do not have + // meaningful EQT. + if (rendererHelper.mainThread.bounds.duration < WINDOW_SIZE_MS) continue; + + const tasks = getTasks(rendererHelper); + const {totalBreakdown, interactiveBreakdown} = getV8Contribution_( + eqtName, + getEventTimes, + isCpuTime, + totalHistogram, + interactiveHistogram, + rendererToInteractiveTimestamps, + histograms, + rendererHelper, + model); + totalHistogram.addSample( + tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow( + rendererHelper.mainThread.bounds.min, + rendererHelper.mainThread.bounds.max, + WINDOW_SIZE_MS, tasks), {v8: totalBreakdown}); + const interactiveTimestamps = + rendererToInteractiveTimestamps.get(rendererHelper.pid); + if (interactiveTimestamps.length === 0) continue; + if (interactiveTimestamps.length > 1) { + // TODO(ulan): Support multiple interactive time windows when + // https://crbug.com/692112 is fixed. + continue; + } + const interactiveWindow = + tr.b.math.Range.fromExplicitRange(interactiveTimestamps[0], Infinity) + .findIntersection(rendererHelper.mainThread.bounds); + interactiveHistogram.addSample( + tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow( + interactiveWindow.min, interactiveWindow.max, + WINDOW_SIZE_MS, tasks), {v8: interactiveBreakdown}); + } + } + + /** + * Computes the contribution of the selected events to the expected queueing + * time. We define the contribution as the maximum expected queueing time in + * the sliding time window of size 500ms (WINDOW_SIZE_MS) for the trace that + * is modified as follows: + * - from each top-level task remove all subevents except the selected events. + * - removing subevents shrinks a task by shifting its end time closer to + * the start time. The start time does not change. + * + * Similar to the expectedQueueingTime this function computes two histograms: + * total and interactive. For example: + * - total:500ms_window:renderer_eqt:v8, + * - interactive:500ms_window:renderer_eqt:v8. + * Each renderer process adds one sample to the histograms. + * Both histograms are added to the given histogram set. + * + * @param {!string} eqtName the metric name part of the histogram name. + * @param {!EventTimesCallback} getEventTimes. + * @param {boolean} isCpuTime + * @param {!tr.v.Histogram} totalEqtHistogram + * @param {!tr.v.Histogram} interactiveEqtHistogram + * @param {!Map.<number, Array.<number>>} rendererToInteractiveTimestamps + * a map from renderer pid to an array of interactive timestamps. + * @param {!tr.v.HistogramSet} histograms + * @param {!tr.model.helpers.ChromeRendererHelper} rendererHelper + * @param {!tr.model.Model} model + * @return {{totalBreakdown: !tr.v.d.Breakdown, + * interactiveBreakdown: !tr.v.d.Breakdown}} + */ + function getV8Contribution_( + eqtName, getEventTimes, isCpuTime, totalEqtHistogram, + interactiveEqtHistogram, rendererToInteractiveTimestamps, histograms, + rendererHelper, model) { + if (!model.categories.includes('v8')) return {}; + + const totalBreakdown = new tr.v.d.Breakdown(); + const interactiveBreakdown = new tr.v.d.Breakdown(); + // Include task extractors that use tracing. + const eventNamesWithTaskExtractors = + getV8EventNamesWithTaskExtractors_(getEventTimes); + if (!isCpuTime) { + // Include task extractors that use RCS. RCS does not provide cpu time + // so include these only for wall clock time. + const taskExtractorsUsingRCS = + getV8EventNamesWithTaskExtractorsUsingRCS_(getEventTimes); + for (const [eventName, getTasks] of taskExtractorsUsingRCS) { + eventNamesWithTaskExtractors.set(eventName, getTasks); + } + } + + let totalNames = totalEqtHistogram.diagnostics.get('v8'); + if (!totalNames) { + totalNames = new tr.v.d.RelatedNameMap(); + totalEqtHistogram.diagnostics.set('v8', totalNames); + } + let interactiveNames = interactiveEqtHistogram.diagnostics.get('v8'); + if (!interactiveNames) { + interactiveNames = new tr.v.d.RelatedNameMap(); + interactiveEqtHistogram.diagnostics.set('v8', interactiveNames); + } + + for (const [eventName, getTasks] of eventNamesWithTaskExtractors) { + const totalHistogram = getOrCreateHistogram_(histograms, + `total:${WINDOW_SIZE_MS}ms_window:${eqtName}:${eventName}`, + `Contribution to the expected queueing time by ${eventName}` + + ' for a given renderer. It is computed as the maximum EQT in' + + ` a ${WINDOW_SIZE_MS}ms sliding window after shrinking top-level` + + ` tasks to contain only ${eventName} subevents`); + const interactiveHistogram = getOrCreateHistogram_(histograms, + `interactive:${WINDOW_SIZE_MS}ms_window:${eqtName}:${eventName}`, + `Contribution to the expected queueing time by ${eventName}` + + ' for a given renderer while the page is interactive. It is' + + ` computed as the maximum EQT in a ${WINDOW_SIZE_MS}ms sliding` + + ' window after shrinking top-level tasks to contain only' + + ` ${eventName} subevents`); + + const tasks = getTasks(rendererHelper); + const totalSample = tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow( + rendererHelper.mainThread.bounds.min, + rendererHelper.mainThread.bounds.max, + WINDOW_SIZE_MS, tasks); + totalHistogram.addSample(totalSample); + totalBreakdown.set(eventName, totalSample); + totalNames.set(eventName, totalHistogram.name); + + const interactiveTimestamps = + rendererToInteractiveTimestamps.get(rendererHelper.pid); + if (interactiveTimestamps.length === 0) continue; + if (interactiveTimestamps.length > 1) { + // TODO(ulan): Support multiple interactive time windows when + // https://crbug.com/692112 is fixed. + continue; + } + const interactiveWindow = + tr.b.math.Range.fromExplicitRange(interactiveTimestamps[0], Infinity) + .findIntersection(rendererHelper.mainThread.bounds); + const interactiveSample = + tr.e.chrome.maxExpectedQueueingTimeInSlidingWindow( + interactiveWindow.min, interactiveWindow.max, + WINDOW_SIZE_MS, tasks); + interactiveHistogram.addSample(interactiveSample); + interactiveBreakdown.set(eventName, interactiveSample); + interactiveNames.set(eventName, interactiveHistogram.name); + } + return {totalBreakdown, interactiveBreakdown}; + } + + /** + * @callback TaskExtractor + * @param {!tr.model.helpers.ChromeRendererHelper} rendererHelper + * @return {Array.<{start: !number, end: !number}>} + */ + + /** + * @param {!EventTimesCallback} getEventTimes. + * @returns {!Map.<string, TaskExtractor>} a map from V8 event names to + * the corresponding task extractor functions. + */ + function getV8EventNamesWithTaskExtractors_(getEventTimes, cpuMetrics) { + /** + * @param {!tr.b.Event} slice. + * @param {!function(tr.b.Event): boolean} predicate that selects V8 events. + * @param {function(tr.b.Event): boolean} excludePredicate that excludes + * V8 events. + * @returns {!number} the total duration of topmost subslices of the given + * slice that satisfy the given |predicate| after filtering out any + * events that satisfy the |excludePredicate| in the subslices. + */ + function durationOfTopmostSubSlices(slice, predicate, excludePredicate) { + let duration = 0; + for (const sub of slice.findTopmostSlicesRelativeToThisSlice(predicate)) { + duration += getEventTimes(sub).duration; + if (excludePredicate !== null && excludePredicate !== undefined) { + duration -= durationOfTopmostSubSlices(sub, excludePredicate); + } + } + return duration; + } + + /** + * @param {!function(tr.b.Event): boolean} predicate that selects V8 events. + * @param {function(tr.b.Event): boolean} excludePredicate that excludes + * V8 events. + * @returns {!TaskExtractor} a function that extracts tasks from the given + * renderer. Each task is a pair of {start, end} times and its duration + * represents the contribution of the events selected by the + * given |predicate| and |excludePredicate|. + */ + function taskExtractor(predicate, excludePredicate) { + return function(rendererHelper) { + const slices = tr.e.chrome.EventFinderUtils.findToplevelSchedulerTasks( + rendererHelper.mainThread); + const result = []; + for (const slice of slices) { + const times = getEventTimes(slice); + if (times.duration > 0 && !containsForcedGC_(slice)) { + const duration = durationOfTopmostSubSlices( + slice, predicate, excludePredicate); + result.push({start: times.start, end: times.start + duration}); + } + } + return result; + }; + } + + return new Map([ + [ + 'v8', + taskExtractor(tr.metrics.v8.utils.isV8Event) + ], + [ + 'v8:execute', + taskExtractor(tr.metrics.v8.utils.isV8ExecuteEvent) + ], + [ + 'v8:gc', + taskExtractor(tr.metrics.v8.utils.isGarbageCollectionEvent) + ], + [ + 'v8:gc:full-mark-compactor', + taskExtractor(tr.metrics.v8.utils.isFullMarkCompactorEvent) + ], + [ + 'v8:gc:incremental-marking', + taskExtractor(tr.metrics.v8.utils.isIncrementalMarkingEvent) + ], + [ + 'v8:gc:latency-mark-compactor', + taskExtractor(tr.metrics.v8.utils.isLatencyMarkCompactorEvent) + ], + [ + 'v8:gc:memory-mark-compactor', + taskExtractor(tr.metrics.v8.utils.isMemoryMarkCompactorEvent) + ], + [ + 'v8:gc:scavenger', + taskExtractor(tr.metrics.v8.utils.isScavengerEvent) + ] + ]); + } + + /** + * @param {!EventTimesCallback} getEventTimes. + * @param {!function(!string): boolean} predicate that selects RCS category. + * @param {!tr.model.helpers.ChromeRendererHelper} rendererHelper + * @returns {Array.<{start: !number, end: !number}>} a list of tasks. Each + * task is a pair of {start, end} times and its duration represents the + * the contribution of the events selected by the given |predicate|. + */ + function extractTaskRCS(getEventTimes, predicate, rendererHelper) { + const result = []; + for (const topSlice of + rendererHelper.mainThread.sliceGroup.topLevelSlices) { + const times = getEventTimes(topSlice); + if (times.duration <= 0 || containsForcedGC_(topSlice)) { + continue; + } + // Find all V8ThreadSlices in the top level slice. + const v8ThreadSlices = []; + for (const slice of topSlice.descendentSlices) { + if (tr.metrics.v8.utils.isV8RCSEvent(slice)) { + v8ThreadSlices.push(slice); + } + } + + // Find the event specified by predicate. + const runtimeGroupCollection = + new tr.e.v8.RuntimeStatsGroupCollection(); + runtimeGroupCollection.addSlices(v8ThreadSlices); + let duration = 0; + for (const runtimeGroup of runtimeGroupCollection.runtimeGroups) { + if (predicate(runtimeGroup.name)) { + duration += runtimeGroup.time; + } + } + + duration = tr.b.convertUnit( + duration, + tr.b.UnitPrefixScale.METRIC.MICRO, + tr.b.UnitPrefixScale.METRIC.MILLI); + result.push({start: times.start, end: times.start + duration}); + } + return result; + } + + /** + * @param {!EventTimesCallback} getEventTimes. + * @returns {!Map.<string, TaskExtractor>} a map from V8 event names to + * the corresponding task extractor functions. + */ + function getV8EventNamesWithTaskExtractorsUsingRCS_(getEventTimes) { + const extractors = new Map(); + extractors.set('v8:compile_rcs', + rendererHelper => extractTaskRCS( + getEventTimes, + tr.metrics.v8.utils.isCompileRCSCategory, + rendererHelper)); + extractors.set('v8:compile:optimize_rcs', + rendererHelper => extractTaskRCS( + getEventTimes, + tr.metrics.v8.utils.isCompileOptimizeRCSCategory, + rendererHelper)); + extractors.set('v8:compile:parse_rcs', + rendererHelper => extractTaskRCS( + getEventTimes, + tr.metrics.v8.utils.isCompileParseRCSCategory, + rendererHelper)); + extractors.set('v8:compile:compile-unoptimize_rcs', + rendererHelper => extractTaskRCS( + getEventTimes, + tr.metrics.v8.utils.isCompileUnoptimizeRCSCategory, + rendererHelper)); + return extractors; + } + + tr.metrics.MetricRegistry.register(expectedQueueingTimeMetric); + + return { + expectedQueueingTimeMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric_test.html new file mode 100644 index 00000000000..b300c523964 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/expected_queueing_time_metric_test.html @@ -0,0 +1,409 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html"> +<link rel="import" + href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html"> +<link rel="import" href="/tracing/metrics/system_health/expected_queueing_time_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function addInteractiveTimestamp(rendererProcess, mainThread, timestamp) { + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: timestamp, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({ + cat: 'disabled-by-default-network', + title: 'ResourceLoad', + start: timestamp, + duration: 5.0, + })); + rendererProcess.objects.addSnapshot( + 'ptr', 'loading', 'FrameLoader', timestamp, { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: timestamp + 1, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'domContentLoadedEventEnd', + start: timestamp + 1, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + } + + test('expectedQueueingTime', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + addInteractiveTimestamp(rendererProcess, mainThread, 200); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 0, + duration: 100, + cpuStart: 0, + cpuDuration: 50, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 3000, + duration: 10, + cpuStart: 3000, + cpuDuration: 5, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 9000, + duration: 10, + cpuStart: 9000, + cpuDuration: 5, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.expectedQueueingTimeMetric(histograms, model); + const interactiveEqt = histograms.getHistogramNamed( + 'interactive:500ms_window:renderer_eqt'); + assert.strictEqual(0.1, interactiveEqt.average); + const totalEqt = histograms.getHistogramNamed( + 'total:500ms_window:renderer_eqt'); + assert.strictEqual(10, totalEqt.average); + const interactiveEqtCpu = histograms.getHistogramNamed( + 'interactive:500ms_window:renderer_eqt_cpu'); + assert.strictEqual(0.025, interactiveEqtCpu.average); + const totalEqtCpu = histograms.getHistogramNamed( + 'total:500ms_window:renderer_eqt_cpu'); + assert.strictEqual(2.5, totalEqtCpu.average); + }); + + test('expectedQueueingTime_noInteractive', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 0, + duration: 100, + cpuStart: 0, + cpuDuration: 100, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 3000, + duration: 10, + cpuStart: 3000, + cpuDuration: 10, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 9000, + duration: 10, + cpuStart: 9000, + cpuDuration: 10, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.expectedQueueingTimeMetric(histograms, model); + const interactiveEQT = histograms.getHistogramNamed( + 'interactive:500ms_window:renderer_eqt'); + assert.strictEqual(0, interactiveEQT.numValues); + }); + + test('expectedQueueingTime_multipleInteractive', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + addInteractiveTimestamp(rendererProcess, mainThread, 200); + addInteractiveTimestamp(rendererProcess, mainThread, 6000); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 0, + duration: 100, + cpuStart: 0, + cpuDuration: 100, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 3000, + duration: 10, + cpuStart: 3000, + cpuDuration: 10, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 12000, + duration: 10, + cpuStart: 9000, + cpuDuration: 10, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.expectedQueueingTimeMetric(histograms, model); + const interactiveEQT = histograms.getHistogramNamed( + 'interactive:500ms_window:renderer_eqt'); + // TODO(ulan): Support multiple interactive time windows when + // https://crbug.com/692112 is fixed. + assert.strictEqual(0, interactiveEQT.numValues); + }); + + test('expectedQueueingTime_multipleRenderersAggregates', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + addInteractiveTimestamp(rendererProcess, mainThread, 200); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 0, + duration: 100, + cpuStart: 0, + cpuDuration: 100, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 3000, + duration: 10, + cpuStart: 3000, + cpuDuration: 10, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 9000, + duration: 10, + cpuStart: 9000, + cpuDuration: 10, + })); + const rendererProcess2 = model.getOrCreateProcess(1985); + const mainThread2 = rendererProcess2.getOrCreateThread(2); + mainThread2.name = 'CrRendererMain'; + addInteractiveTimestamp(rendererProcess2, mainThread2, 200); + mainThread2.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 9000, + duration: 0, + cpuStart: 9000, + cpuDuration: 0, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.expectedQueueingTimeMetric(histograms, model); + const eqt = histograms.getHistogramNamed( + 'interactive:500ms_window:renderer_eqt'); + assert.strictEqual(0.05, eqt.average); + }); + + test('expectedQueueingTime_relatedV8Histograms', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + addInteractiveTimestamp(rendererProcess, mainThread, 200); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 0, + duration: 100, + cpuStart: 0, + cpuDuration: 100, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.GCFinalizeMC', + start: 50, + duration: 50, + cpuStart: 50, + cpuDuration: 50, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 3000, + duration: 10, + cpuStart: 3000, + cpuDuration: 10, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'disabled-by-default-v8.compile', + title: 'V8.RecompileSynchronous', + start: 3000, + duration: 5, + cpuStart: 3000, + cpuDuration: 5, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 9000, + duration: 10, + cpuStart: 9000, + cpuDuration: 10, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.GCFinalizeMC', + start: 9000, + duration: 5, + cpuStart: 9000, + cpuDuration: 5, + })); + mainThread.sliceGroup.createSubSlices(); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.expectedQueueingTimeMetric(histograms, model); + + const eqt = histograms.getHistogramNamed( + 'interactive:500ms_window:renderer_eqt'); + assert.strictEqual('interactive:500ms_window:renderer_eqt:v8', + eqt.diagnostics.get('v8').get('v8')); + assert.strictEqual(0.025, + eqt.getBinForValue(0.1).diagnosticMaps[0].get('v8').get('v8')); + + const eqtCpu = histograms.getHistogramNamed( + 'interactive:500ms_window:renderer_eqt_cpu'); + assert.strictEqual('interactive:500ms_window:renderer_eqt_cpu:v8', + eqtCpu.diagnostics.get('v8').get('v8')); + assert.strictEqual(0.025, + eqtCpu.getBinForValue(0.1).diagnosticMaps[0].get('v8').get('v8')); + }); + + test('expectedQueueingTimeRCS', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + addInteractiveTimestamp(rendererProcess, mainThread, 200); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 0, + duration: 100, + cpuStart: 0, + cpuDuration: 100, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.newInstance', + type: tr.e.v8.V8ThreadSlice, + start: 12555, + duration: 990, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 11], + ParseLazy: [5, 3], + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 3000, + duration: 100, + cpuStart: 3000, + cpuDuration: 100, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + type: tr.e.v8.V8ThreadSlice, + start: 3000, + duration: 100, + cpuStart: 3000, + cpuDuration: 100, + args: { + 'runtime-call-stats': { + CompileIgnition: [3, 5000], + OptimizeCode: [6, 40000], + JS_Execution: [1, 11000], + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 4000, + duration: 100, + cpuStart: 4000, + cpuDuration: 100, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + type: tr.e.v8.V8ThreadSlice, + start: 4000, + duration: 100, + cpuStart: 4000, + cpuDuration: 100, + args: { + 'runtime-call-stats': { + CompileIgnition: [20, 20000], + ParseLazy: [5, 10000], + CompileBackgroundIgnition: [3, 30000] + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 9000, + duration: 10, + cpuStart: 9000, + cpuDuration: 10, + })); + mainThread.sliceGroup.createSubSlices(); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.expectedQueueingTimeMetric(histograms, model); + + const eqt = histograms.getHistogramNamed( + 'total:500ms_window:renderer_eqt'); + assert.strictEqual( + 'total:500ms_window:renderer_eqt:v8:compile:optimize_rcs', + eqt.diagnostics.get('v8').get('v8:compile:optimize_rcs')); + assert.strictEqual(1.6, eqt.getBinForValue(10).diagnosticMaps[0].get( + 'v8').get('v8:compile:optimize_rcs')); + + assert.strictEqual('total:500ms_window:renderer_eqt:v8:compile:parse_rcs', + eqt.diagnostics.get('v8').get('v8:compile:parse_rcs')); + assert.strictEqual(0.1, eqt.getBinForValue(10).diagnosticMaps[0].get( + 'v8').get('v8:compile:parse_rcs')); + + assert.strictEqual( + 'total:500ms_window:renderer_eqt:v8:compile:compile-unoptimize_rcs', + eqt.diagnostics.get('v8').get('v8:compile:compile-unoptimize_rcs')); + assert.strictEqual(0.4, eqt.getBinForValue(10).diagnosticMaps[0].get( + 'v8').get('v8:compile:compile-unoptimize_rcs')); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/limited_cpu_time_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/limited_cpu_time_metric.html new file mode 100644 index 00000000000..40d5722c1a4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/limited_cpu_time_metric.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/system_health/new_cpu_time_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + const includeHistogramNames = [ + 'cpuTime:all_processes:all_threads:all_stages:all_initiators', + 'cpuPercentage:all_processes:all_threads:all_stages:all_initiators', + 'cpuTime:browser_process:all_threads:all_stages:all_initiators', + 'cpuPercentage:browser_process:all_threads:all_stages:all_initiators', + 'cpuTime:renderer_processes:all_threads:all_stages:all_initiators', + 'cpuPercentage:renderer_processes:all_threads:all_stages:all_initiators', + 'cpuTime:gpu_process:all_threads:all_stages:all_initiators', + 'cpuPercentage:gpu_process:all_threads:all_stages:all_initiators', + 'cpuTime:renderer_processes:CrRendererMain:all_stages:all_initiators', + 'cpuPercentage:renderer_processes:CrRendererMain:all_stages:all_initiators', + 'cpuTime:browser_process:CrBrowserMain:all_stages:all_initiators', + 'cpuPercentage:browser_process:CrBrowserMain:all_stages:all_initiators', + 'cpuTime:all_processes:all_threads:Load:Successful', + 'cpuPercentage:all_processes:all_threads:Load:Successful', + ]; + + /** + * This metric is a limited version of new CPU Time metric. The new cpu time + * metric produces 300-500 histograms for a trace, which is overwhelming for + * some systems. This file exposes a subset of those histograms. + * + * TODO(#4044): Remove this metric once histogram pipeline is ready. + * + * @param {!tr.v.HistogramSet} histograms + * @param {!tr.model.Model} model + * @param {!Object=} opt_options + */ + function limitedCpuTimeMetric(histograms, model, opt_options) { + const allCpuHistograms = new tr.v.HistogramSet(); + tr.metrics.sh.newCpuTimeMetric(allCpuHistograms, model, opt_options); + + for (const histogramName of includeHistogramNames) { + const histogram = allCpuHistograms.getHistogramNamed(histogramName); + if (histogram) histograms.addHistogram(histogram); + } + } + + tr.metrics.MetricRegistry.register(limitedCpuTimeMetric, { + supportsRangeOfInterest: true + }); + + return { + limitedCpuTimeMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric.html new file mode 100644 index 00000000000..b439f2e7d15 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric.html @@ -0,0 +1,623 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/category_util.html"> +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html"> +<link rel="import" href="/tracing/extras/chrome/time_to_interactive.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/system_health/breakdown_tree_helpers.html"> +<link rel="import" href="/tracing/metrics/system_health/utils.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/model/helpers/chrome_thread_helper.html"> +<link rel="import" href="/tracing/model/timed_event.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + const LONG_TASK_THRESHOLD_MS = 50; + const timeDurationInMs_smallerIsBetter = + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter; + const RelatedEventSet = tr.v.d.RelatedEventSet; + const hasCategoryAndName = tr.metrics.sh.hasCategoryAndName; + const EventFinderUtils = tr.e.chrome.EventFinderUtils; + + /** + * @param {!tr.model.Process} process + * @param {!tr.b.math.Range} range + * @return {Array.<tr.model.Event>} An array of network events of a process + * and that are intersecting a range. + */ + function getNetworkEventsInRange(process, range) { + const networkEvents = []; + for (const thread of Object.values(process.threads)) { + const threadHelper = new tr.model.helpers.ChromeThreadHelper(thread); + const events = threadHelper.getNetworkEvents(); + for (const event of events) { + if (range.intersectsExplicitRangeInclusive(event.start, event.end)) { + networkEvents.push(event); + } + } + } + return networkEvents; + } + + /** + * @param {!Object.<string, Object>} breakdownTree + * @return {tr.v.d.Breakdown} A breakdown with categories and the total time + * (ms) spent under each category. + */ + function createBreakdownDiagnostic(breakdownTree) { + const breakdownDiagnostic = new tr.v.d.Breakdown(); + breakdownDiagnostic.colorScheme = + tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER; + + for (const label in breakdownTree) { + breakdownDiagnostic.set(label, breakdownTree[label].total); + } + return breakdownDiagnostic; + } + + const LOADING_METRIC_BOUNDARIES = tr.v.HistogramBinBoundaries + .createLinear(0, 1e3, 20) // 50ms step to 1s + .addLinearBins(3e3, 20) // 100ms step to 3s + .addExponentialBins(20e3, 20); + + const TIME_TO_INTERACTIVE_BOUNDARIES = tr.v.HistogramBinBoundaries + // 90-th percentiile of TTI is around 40 seconds, across warm and cold + // loads. Data obtained through Cluster Telemetry analysis. + .createExponential(1, 40e3, 35) + .addExponentialBins(80e3, 15); + + const SUMMARY_OPTIONS = { + avg: true, + count: false, + max: true, + min: true, + std: true, + sum: false, + }; + + function findFrameLoaderSnapshotAt(rendererHelper, frameIdRef, ts) { + const objects = rendererHelper.process.objects; + const frameLoaderInstances = objects.instancesByTypeName_.FrameLoader; + if (frameLoaderInstances === undefined) return undefined; + + let snapshot; + for (const instance of frameLoaderInstances) { + if (!instance.isAliveAt(ts)) continue; + const maybeSnapshot = instance.getSnapshotAt(ts); + if (frameIdRef !== maybeSnapshot.args.frame.id_ref) continue; + snapshot = maybeSnapshot; + } + + return snapshot; + } + + function findAllEvents(rendererHelper, category, title) { + const targetEvents = []; + + for (const ev of rendererHelper.process.getDescendantEvents()) { + if (!hasCategoryAndName(ev, category, title)) continue; + targetEvents.push(ev); + } + + return targetEvents; + } + + function getMostRecentValidEvent(rendererHelper, category, title) { + const targetEvents = findAllEvents(rendererHelper, category, title); + // Want to keep the event with the most recent timestamp + let validEvent; + for (const targetEvent of targetEvents) { + if (rendererHelper.isTelemetryInternalEvent(targetEvent)) continue; + if (validEvent === undefined) { + validEvent = targetEvent; + } else { + // Want to keep the event with the most recent timestamp + if (validEvent.start < targetEvent.start) { + validEvent = targetEvent; + } + } + } + return validEvent; + } + + function getAboveTheFoldLoadedToVisibleSamples(rendererHelper, + navIdToNavStartEvents) { + const samples = []; + // Want to measure the time from when navigation starts to when the load + // event fired for all non-ad resources. This done with the associated + // navigation start event to the pc mark in the amp code, correlated by + // navigation id. + const pcEvent = getMostRecentValidEvent( + rendererHelper, 'blink.user_timing', 'pc'); + if (pcEvent === undefined) return samples; + + if (rendererHelper.isTelemetryInternalEvent(pcEvent)) return samples; + const navigationStartEvent = navIdToNavStartEvents.get( + pcEvent.args.data.navigationId); + if (navigationStartEvent === undefined) return samples; + const navStartToEventRange = tr.b.math.Range.fromExplicitRange( + navigationStartEvent.start, pcEvent.start); + const networkEvents = getNetworkEventsInRange( + rendererHelper.process, navStartToEventRange); + const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree( + rendererHelper.mainThread, networkEvents, navStartToEventRange); + samples.push({ + value: navStartToEventRange.duration, + breakdownTree, + diagnostics: { + breakdown: createBreakdownDiagnostic(breakdownTree), + Start: new RelatedEventSet(navigationStartEvent), + End: new RelatedEventSet(pcEvent) + } + }); + + return samples; + } + + function getFirstViewportReadySamples(rendererHelper) { + const samples = []; + // Want to measure the time from when the document is visible to the time + // when the load event fired for all non-ad resources. This is done with + // two marks in the amp code: pc and visible. + const pcEvent = getMostRecentValidEvent( + rendererHelper, 'blink.user_timing', 'pc'); + const visibleEvent = getMostRecentValidEvent( + rendererHelper, 'blink.user_timing', 'visible'); + if (pcEvent !== undefined && visibleEvent !== undefined) { + samples.push({ + value: pcEvent.start - visibleEvent.start, + diagnostics: { + Start: new RelatedEventSet(visibleEvent), + End: new RelatedEventSet(pcEvent) + } + }); + } + return samples; + } + + function findTimeToXEntries( + category, eventName, rendererHelper, frameToNavStartEvents, + navIdToNavStartEvents) { + const targetEvents = findAllEvents(rendererHelper, category, eventName); + const entries = []; + for (const targetEvent of targetEvents) { + if (rendererHelper.isTelemetryInternalEvent(targetEvent)) continue; + const frameIdRef = targetEvent.args.frame; + const snapshot = findFrameLoaderSnapshotAt( + rendererHelper, frameIdRef, targetEvent.start); + if (snapshot === undefined || !snapshot.args.isLoadingMainFrame) continue; + const url = snapshot.args.documentLoaderURL; + if (tr.e.chrome.CHROME_INTERNAL_URLS.includes(url)) continue; + let navigationStartEvent; + if (targetEvent.args.data === undefined || + targetEvent.args.data.navigationId === undefined) { + navigationStartEvent = + EventFinderUtils.findLastEventStartingOnOrBeforeTimestamp( + frameToNavStartEvents.get(frameIdRef) || [], targetEvent.start); + } else { + navigationStartEvent = navIdToNavStartEvents.get( + targetEvent.args.data.navigationId); + } + + // Ignore layout w/o preceding navigationStart, as they are not + // attributed to any time-to-X metric. + if (navigationStartEvent === undefined) continue; + entries.push({ + navigationStartEvent, + targetEvent, + url, + }); + } + return entries; + } + + function collectTimeToEvent(rendererHelper, timeToXEntries) { + const samples = []; + for (const { targetEvent, navigationStartEvent, url } of timeToXEntries) { + const navStartToEventRange = tr.b.math.Range.fromExplicitRange( + navigationStartEvent.start, targetEvent.start); + const networkEvents = getNetworkEventsInRange( + rendererHelper.process, navStartToEventRange); + const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree( + rendererHelper.mainThread, networkEvents, navStartToEventRange); + samples.push({ + value: navStartToEventRange.duration, + breakdownTree, + diagnostics: { + breakdown: createBreakdownDiagnostic(breakdownTree), + url: new tr.v.d.GenericSet([url]), + Start: new RelatedEventSet(navigationStartEvent), + End: new RelatedEventSet(targetEvent) + } + }); + } + return samples; + } + + function collectTimeToEventInCpuTime(rendererHelper, timeToXEntries) { + const samples = []; + for (const { targetEvent, navigationStartEvent, url } of timeToXEntries) { + const navStartToEventRange = tr.b.math.Range.fromExplicitRange( + navigationStartEvent.start, targetEvent.start); + + const mainThreadCpuTime = + rendererHelper.mainThread.getCpuTimeForRange(navStartToEventRange); + + const breakdownTree = tr.metrics.sh.generateCpuTimeBreakdownTree( + rendererHelper.mainThread, navStartToEventRange); + samples.push({ + value: mainThreadCpuTime, + breakdownTree, + diagnostics: { + breakdown: createBreakdownDiagnostic(breakdownTree), + start: new RelatedEventSet(navigationStartEvent), + end: new RelatedEventSet(targetEvent), + infos: new tr.v.d.GenericSet([{ + pid: rendererHelper.pid, + start: navigationStartEvent.start, + event: targetEvent.start, + }]), + } + }); + } + return samples; + } + + function addFirstMeaningfulPaintSample(samples, rendererHelper, + navigationStart, fmpMarkerEvent, url) { + const navStartToFMPRange = tr.b.math.Range.fromExplicitRange( + navigationStart.start, fmpMarkerEvent.start); + const networkEvents = getNetworkEventsInRange( + rendererHelper.process, navStartToFMPRange); + const timeToFirstMeaningfulPaint = navStartToFMPRange.duration; + const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree( + rendererHelper.mainThread, networkEvents, navStartToFMPRange); + samples.push({ + value: timeToFirstMeaningfulPaint, + breakdownTree, + diagnostics: { + breakdown: createBreakdownDiagnostic(breakdownTree), + start: new RelatedEventSet(navigationStart), + end: new RelatedEventSet(fmpMarkerEvent), + infos: new tr.v.d.GenericSet([{ + url, + pid: rendererHelper.pid, + start: navigationStart.start, + fmp: fmpMarkerEvent.start, + }]), + } + }); + } + + function addFirstMeaningfulPaintCpuTimeSample(samples, rendererHelper, + navigationStart, fmpMarkerEvent, url) { + const navStartToFMPRange = tr.b.math.Range.fromExplicitRange( + navigationStart.start, fmpMarkerEvent.start); + + const mainThreadCpuTime = + rendererHelper.mainThread.getCpuTimeForRange(navStartToFMPRange); + + const breakdownTree = tr.metrics.sh.generateCpuTimeBreakdownTree( + rendererHelper.mainThread, navStartToFMPRange); + samples.push({ + value: mainThreadCpuTime, + breakdownTree, + diagnostics: { + breakdown: createBreakdownDiagnostic(breakdownTree), + start: new RelatedEventSet(navigationStart), + end: new RelatedEventSet(fmpMarkerEvent), + infos: new tr.v.d.GenericSet([{ + url, + pid: rendererHelper.pid, + start: navigationStart.start, + fmp: fmpMarkerEvent.start, + }]), + } + }); + } + + /** + * Object containing one value and associated diagnostics info for that value + * for a metric. + * @typedef {{value: number, diagnostics: !tr.v.d.DiagnosticMap}} MetricSample + */ + + /** + * Returns a MetricSample for interactivity metrics - First CPU Idle and Time + * to Interactive. + * + * @param {tr.model.helpers.ChromeRendererHelper} rendererHelper + * @param {?number} eventTimestamp - Timestamp of the event for which the + * sample is being generated. + * @param {tr.model.ThreadSlice} navigationStartEvent + * @param {number} firstMeaningfulPaintTime + * @param {number} domContentLoadedEndTime + * @param {string} url - URL of the current main frame document. + * @returns {MetricSample|undefined} + */ + function decorateInteractivitySampleWithDiagnostics_(rendererHelper, + eventTimestamp, navigationStartEvent, firstMeaningfulPaintTime, + domContentLoadedEndTime, url) { + if (eventTimestamp === undefined) return undefined; + const navigationStartTime = navigationStartEvent.start; + const navStartToEventTimeRange = + tr.b.math.Range.fromExplicitRange( + navigationStartTime, eventTimestamp); + const networkEvents = getNetworkEventsInRange( + rendererHelper.process, navStartToEventTimeRange); + const breakdownTree = tr.metrics.sh.generateWallClockTimeBreakdownTree( + rendererHelper.mainThread, networkEvents, + navStartToEventTimeRange); + const breakdownDiagnostic = createBreakdownDiagnostic(breakdownTree); + return { + value: navStartToEventTimeRange.duration, + diagnostics: tr.v.d.DiagnosticMap.fromObject({ + 'Start': new RelatedEventSet(navigationStartEvent), + 'Navigation infos': new tr.v.d.GenericSet([{ + url, + pid: rendererHelper.pid, + navigationStartTime, + firstMeaningfulPaintTime, + domContentLoadedEndTime, + // eventTimestamp can be derived from value and navigationStartEvent, + // but it's useful to directly see the value in the UI. + eventTimestamp, + }]), + 'Breakdown of [navStart, eventTimestamp]': breakdownDiagnostic, + }), + }; + } + + function collectLoadingMetricsForRenderer(rendererHelper) { + const frameToNavStartEvents = + EventFinderUtils.getSortedMainThreadEventsByFrame( + rendererHelper, 'navigationStart', 'blink.user_timing'); + const navIdToNavStartEvents = + EventFinderUtils.getSortedMainThreadEventsByNavId( + rendererHelper, 'navigationStart', 'blink.user_timing'); + const firstPaintSamples = collectTimeToEvent(rendererHelper, + findTimeToXEntries('loading', 'firstPaint', rendererHelper, + frameToNavStartEvents, navIdToNavStartEvents)); + const timeToFCPEntries = findTimeToXEntries('loading', + 'firstContentfulPaint', rendererHelper, frameToNavStartEvents, + navIdToNavStartEvents); + const firstContentfulPaintSamples = collectTimeToEvent(rendererHelper, + timeToFCPEntries); + const firstContentfulPaintCpuTimeSamples = collectTimeToEventInCpuTime( + rendererHelper, timeToFCPEntries); + const onLoadSamples = collectTimeToEvent(rendererHelper, findTimeToXEntries( + 'blink.user_timing', 'loadEventStart', rendererHelper, + frameToNavStartEvents, navIdToNavStartEvents)); + const aboveTheFoldLoadedToVisibleSamples = + getAboveTheFoldLoadedToVisibleSamples( + rendererHelper, navIdToNavStartEvents); + const firstViewportReadySamples = getFirstViewportReadySamples( + rendererHelper); + + return { + frameToNavStartEvents, + firstPaintSamples, + firstContentfulPaintSamples, + firstContentfulPaintCpuTimeSamples, + onLoadSamples, + aboveTheFoldLoadedToVisibleSamples, + firstViewportReadySamples, + }; + } + + function collectMetricsFromLoadExpectations(model, chromeHelper) { + // Add FMP, firstCpuIdle and interactive samples from load UE + const interactiveSamples = []; + const firstCpuIdleSamples = []; + const firstMeaningfulPaintSamples = []; + const firstMeaningfulPaintCpuTimeSamples = []; + for (const expectation of model.userModel.expectations) { + if (!(expectation instanceof tr.model.um.LoadExpectation)) continue; + if (tr.e.chrome.CHROME_INTERNAL_URLS.includes(expectation.url)) { + continue; + } + const rendererHelper = chromeHelper.rendererHelpers[ + expectation.renderProcess.pid]; + if (expectation.fmpEvent !== undefined) { + addFirstMeaningfulPaintSample(firstMeaningfulPaintSamples, + rendererHelper, expectation.navigationStart, expectation.fmpEvent, + expectation.url); + addFirstMeaningfulPaintCpuTimeSample(firstMeaningfulPaintCpuTimeSamples, + rendererHelper, expectation.navigationStart, expectation.fmpEvent, + expectation.url); + } + if (expectation.firstCpuIdleTime !== undefined) { + firstCpuIdleSamples.push(decorateInteractivitySampleWithDiagnostics_( + rendererHelper, expectation.firstCpuIdleTime, + expectation.navigationStart, + expectation.fmpEvent.start, + expectation.domContentLoadedEndEvent.start, expectation.url)); + } + if (expectation.timeToInteractive !== undefined) { + interactiveSamples.push(decorateInteractivitySampleWithDiagnostics_( + rendererHelper, expectation.timeToInteractive, + expectation.navigationStart, + expectation.fmpEvent.start, + expectation.domContentLoadedEndEvent.start, expectation.url)); + } + } + + return { + firstMeaningfulPaintSamples, + firstMeaningfulPaintCpuTimeSamples, + firstCpuIdleSamples, + interactiveSamples, + }; + } + + function addSamplesToHistogram(samples, histogram, histograms) { + for (const sample of samples) { + histogram.addSample(sample.value, sample.diagnostics); + + // Only add breakdown histograms for FCP. + // http://crbug.com/771610 + if (histogram.name !== 'timeToFirstContentfulPaint') continue; + + if (!sample.breakdownTree) continue; + for (const [category, breakdown] of Object.entries( + sample.breakdownTree)) { + const relatedName = `${histogram.name}:${category}`; + let relatedHist = histograms.getHistogramsNamed(relatedName)[0]; + if (!relatedHist) { + relatedHist = histograms.createHistogram( + relatedName, histogram.unit, [], { + binBoundaries: LOADING_METRIC_BOUNDARIES, + summaryOptions: { + count: false, + max: false, + min: false, + sum: false, + }, + }); + + let relatedNames = histogram.diagnostics.get('breakdown'); + if (!relatedNames) { + relatedNames = new tr.v.d.RelatedNameMap(); + histogram.diagnostics.set('breakdown', relatedNames); + } + relatedNames.set(category, relatedName); + } + relatedHist.addSample(breakdown.total, { + breakdown: tr.v.d.Breakdown.fromEntries( + Object.entries(breakdown.events)), + }); + } + } + } + + function loadingMetric(histograms, model) { + const firstPaintHistogram = histograms.createHistogram( + 'timeToFirstPaint', timeDurationInMs_smallerIsBetter, [], { + binBoundaries: LOADING_METRIC_BOUNDARIES, + description: 'time to first paint', + summaryOptions: SUMMARY_OPTIONS, + }); + const firstContentfulPaintHistogram = histograms.createHistogram( + 'timeToFirstContentfulPaint', timeDurationInMs_smallerIsBetter, [], { + binBoundaries: LOADING_METRIC_BOUNDARIES, + description: 'time to first contentful paint', + summaryOptions: SUMMARY_OPTIONS, + }); + const firstContentfulPaintCpuTimeHistogram = histograms.createHistogram( + 'cpuTimeToFirstContentfulPaint', timeDurationInMs_smallerIsBetter, [], { + binBoundaries: LOADING_METRIC_BOUNDARIES, + description: 'CPU time to first contentful paint', + summaryOptions: SUMMARY_OPTIONS, + }); + const onLoadHistogram = histograms.createHistogram( + 'timeToOnload', timeDurationInMs_smallerIsBetter, [], { + binBoundaries: LOADING_METRIC_BOUNDARIES, + description: 'time to onload. ' + + 'This is temporary metric used for PCv1/v2 sanity checking', + summaryOptions: SUMMARY_OPTIONS, + }); + const firstMeaningfulPaintHistogram = histograms.createHistogram( + 'timeToFirstMeaningfulPaint', timeDurationInMs_smallerIsBetter, [], { + binBoundaries: LOADING_METRIC_BOUNDARIES, + description: 'time to first meaningful paint', + summaryOptions: SUMMARY_OPTIONS, + }); + const firstMeaningfulPaintCpuTimeHistogram = histograms.createHistogram( + 'cpuTimeToFirstMeaningfulPaint', timeDurationInMs_smallerIsBetter, [], { + binBoundaries: LOADING_METRIC_BOUNDARIES, + description: 'CPU time to first meaningful paint', + summaryOptions: SUMMARY_OPTIONS, + }); + const timeToInteractiveHistogram = histograms.createHistogram( + 'timeToInteractive', timeDurationInMs_smallerIsBetter, [], { + binBoundaries: TIME_TO_INTERACTIVE_BOUNDARIES, + description: 'Time to Interactive', + summaryOptions: SUMMARY_OPTIONS, + }); + const timeToFirstCpuIdleHistogram = histograms.createHistogram( + 'timeToFirstCpuIdle', timeDurationInMs_smallerIsBetter, [], { + binBoundaries: TIME_TO_INTERACTIVE_BOUNDARIES, + description: 'Time to First CPU Idle', + summaryOptions: SUMMARY_OPTIONS, + }); + const aboveTheFoldLoadedToVisibleHistogram = histograms.createHistogram( + 'aboveTheFoldLoadedToVisible', timeDurationInMs_smallerIsBetter, [], { + binBoundaries: TIME_TO_INTERACTIVE_BOUNDARIES, + description: 'Time from first visible to load for AMP pages only.', + summaryOptions: SUMMARY_OPTIONS, + }); + const firstViewportReadyHistogram = histograms.createHistogram( + 'timeToFirstViewportReady', timeDurationInMs_smallerIsBetter, [], { + binBoundaries: TIME_TO_INTERACTIVE_BOUNDARIES, + description: 'Time from navigation to load for AMP pages only. ', + summaryOptions: SUMMARY_OPTIONS, + }); + + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + for (const pid in chromeHelper.rendererHelpers) { + const rendererHelper = chromeHelper.rendererHelpers[pid]; + if (rendererHelper.isChromeTracingUI) continue; + + const samplesSet = + collectLoadingMetricsForRenderer(rendererHelper); + + addSamplesToHistogram( + samplesSet.firstPaintSamples, firstPaintHistogram, histograms); + addSamplesToHistogram( + samplesSet.firstContentfulPaintSamples, + firstContentfulPaintHistogram, + histograms); + addSamplesToHistogram( + samplesSet.firstContentfulPaintCpuTimeSamples, + firstContentfulPaintCpuTimeHistogram, + histograms); + addSamplesToHistogram( + samplesSet.onLoadSamples, onLoadHistogram, histograms); + addSamplesToHistogram( + samplesSet.aboveTheFoldLoadedToVisibleSamples, + aboveTheFoldLoadedToVisibleHistogram, + histograms); + addSamplesToHistogram( + samplesSet.firstViewportReadySamples, + firstViewportReadyHistogram, + histograms); + } + + const samplesSet = collectMetricsFromLoadExpectations(model, chromeHelper); + addSamplesToHistogram( + samplesSet.firstMeaningfulPaintSamples, + firstMeaningfulPaintHistogram, + histograms); + addSamplesToHistogram( + samplesSet.firstMeaningfulPaintCpuTimeSamples, + firstMeaningfulPaintCpuTimeHistogram, + histograms); + addSamplesToHistogram( + samplesSet.interactiveSamples, + timeToInteractiveHistogram, + histograms); + addSamplesToHistogram( + samplesSet.firstCpuIdleSamples, + timeToFirstCpuIdleHistogram, + histograms); + } + + tr.metrics.MetricRegistry.register(loadingMetric); + + return { + loadingMetric, + getNetworkEventsInRange, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric_test.html new file mode 100644 index 00000000000..16b2dad797f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/loading_metric_test.html @@ -0,0 +1,1142 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html"> +<link rel="import" + href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html"> +<link rel="import" href="/tracing/metrics/system_health/loading_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('timeToFirstPaint', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'rail,loading,devtools.timeline', + title: 'firstPaint', + start: 1001, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed('timeToFirstPaint'); + assert.strictEqual(1, hist.running.count); + assert.strictEqual(801, hist.running.mean); + }); + + + test('timeToFirstContentfulPaint', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'ResourceDispatcher::OnRequestComplete', + start: 200, + duration: 100, + cpuStart: 210, + cpuDuration: 25, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed('timeToFirstContentfulPaint'); + assert.strictEqual(1, hist.running.count); + assert.strictEqual(800, hist.running.mean); + const fcpResourceLoading = histograms.getHistogramNamed( + 'timeToFirstContentfulPaint:resource_loading'); + assert.strictEqual( + hist.diagnostics.get('breakdown').get( + 'resource_loading'), + 'timeToFirstContentfulPaint:resource_loading'); + assert.strictEqual(fcpResourceLoading.sum, 100); + assert.strictEqual(tr.b.getOnlyElement(hist.getBinForValue( + 800).diagnosticMaps).get('breakdown').get('resource_loading'), 100); + }); + + + test('timeToFirstViewportReady', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'pc', + start: 1200, + duration: 0.0, + args: {data: {navigationId: '0xsecondnav'}} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'pc', + start: 1300, + duration: 0.0, + args: {data: {navigationId: '0xsecondnav'}} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'visible', + start: 300, + duration: 0.0, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed('timeToFirstViewportReady'); + assert.strictEqual(1, hist.running.count); + assert.strictEqual(1000, hist.running.mean); + }); + + + test('aboveTheFoldLoadedToVisibleHistogram', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'pc', + start: 1300, + duration: 0.0, + args: {data: {navigationId: '0xsecondnav'}} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 300, + duration: 0.0, + args: {frame: '0xdeadbeef', data: {navigationId: '0xfirstnav'}} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 400, + duration: 0.0, + args: {frame: '0xdeadbeef', data: {navigationId: '0xsecondnav'}} + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed( + 'aboveTheFoldLoadedToVisible'); + assert.strictEqual(1, hist.running.count); + assert.strictEqual(900, hist.running.mean); + }); + + + test('timeToFirstContentfulPaintIgnoringWarmCache', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + // warm cache navigation + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({ + cat: 'blink.console', + title: 'telemetry.internal.warm_cache.warm.start', + start: 250, + duration: 0.0 + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({ + cat: 'blink.console', + title: 'telemetry.internal.warm_cache.warm.end', + start: 1250, + duration: 0.0 + })); + + // measurement navigation + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 2000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 2100, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: 2400, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed('timeToFirstContentfulPaint'); + assert.strictEqual(1, hist.running.count); + assert.strictEqual(400, hist.running.mean); + }); + + test('timeToFirstContentfulPaintInCpuTime', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'ResourceDispatcher::OnRequestComplete', + start: 200, + duration: 100, + cpuStart: 210, + cpuDuration: 25, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed('cpuTimeToFirstContentfulPaint'); + assert.strictEqual(1, hist.running.count); + assert.strictEqual(25, hist.running.mean); + assert.strictEqual(tr.b.getOnlyElement(hist.getBinForValue( + 25).diagnosticMaps).get('breakdown').get('resource_loading'), 25); + }); + + test('timeToFirstContentfulPaintWithNavId', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef', data: {navigationId: '0xfirstnav'}} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 400, + duration: 0.0, + args: {frame: '0xdeadbeef', data: {navigationId: '0xsecondnav'}} + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef', data: {navigationId: '0xfirstnav'}} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'ResourceDispatcher::OnRequestComplete', + start: 200, + duration: 100, + cpuStart: 210, + cpuDuration: 25, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed('timeToFirstContentfulPaint'); + assert.strictEqual(1, hist.running.count); + assert.strictEqual(800, hist.running.mean); + const fcpResourceLoading = histograms.getHistogramNamed( + 'timeToFirstContentfulPaint:resource_loading'); + assert.strictEqual( + hist.diagnostics.get('breakdown').get( + 'resource_loading'), + 'timeToFirstContentfulPaint:resource_loading'); + assert.strictEqual(fcpResourceLoading.sum, 100); + assert.strictEqual(tr.b.getOnlyElement(hist.getBinForValue( + 800).diagnosticMaps).get('breakdown').get('resource_loading'), 100); + }); + + test('timeToFirstMeaningfulPaint', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 600, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed('timeToFirstMeaningfulPaint'); + assert.strictEqual(1, hist.running.count); + assert.strictEqual(800, hist.running.mean); + }); + + // [-------CPU: 300-----------] + // | [---CPU: 100---] | + // | | | | + // v v v v + // Ts: 100 200 Start 300 500 600 + // | | + // Start FMP + test('cpuTimeToFirstMeaningfulPaint_withEmbeddedSlices', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + cpuStart: 1160, + cpuDuration: 0, + args: {frame: '0xdeadbeef'} + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 600, + duration: 0.0, + cpuStart: 1480, + cpuDuration: 0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 100, + duration: 400, + cpuStart: 1000, + cpuDuration: 300, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'ResourceDispatcher::OnRequestComplete', + start: 200, + duration: 100, + cpuStart: 1150, + cpuDuration: 100, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed('cpuTimeToFirstMeaningfulPaint'); + assert.deepEqual(hist.sampleValues, [225]); + const histBin = hist.getBinForValue(225); + assert.closeTo(histBin.diagnosticMaps[0] + .get('breakdown').get('other'), 400 / 3, 0.1); + assert.strictEqual(histBin.diagnosticMaps[0] + .get('breakdown').get('resource_loading'), 100); + }); + + // [-------------] [------------------] + // | | | | + // v v v v + // Ts: 100 Start 300 500 FMP 700 + // | | + // 200 600 + test('cpuTimeToFirstMeaningfulPaint_withIntersectingBoundary', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + cpuStart: 1160, + cpuDuration: 0, + args: {frame: '0xdeadbeef'} + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 600, + duration: 0.0, + cpuStart: 1280, + cpuDuration: 0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 100, + duration: 200, + cpuStart: 1060, + cpuDuration: 180, + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 500, + duration: 200, + cpuStart: 1250, + cpuDuration: 100, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed('cpuTimeToFirstMeaningfulPaint'); + assert.strictEqual(1, hist.running.count); + assert.strictEqual(140, hist.running.mean); + }); + + // Render 1: + // + // [--------] + // | | + // v v + // CPU Time: Start 1180 1230 FMP + // | | + // 1160 1280 + // + // Render 2: + // + // [-------------] + // | | + // v v + // CPU Time: Start 1170 1270 FMP + // | | + // 1160 1280 + test('cpuTimeToFirstMeaningfulPaint_multipleRenderers', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess1 = model.rendererProcess; + let mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + cpuStart: 1160, + cpuDuration: 0, + args: {frame: '0xdeadbeef'} + })); + rendererProcess1.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 600, + duration: 0.0, + cpuStart: 1280, + cpuDuration: 0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loader', + title: 'ResourceDispatcher::OnRequestComplete', + start: 300, + duration: 200, + cpuStart: 1180, + cpuDuration: 50 + })); + + const rendererProcess2 = model.getOrCreateProcess(10); + mainThread = rendererProcess2.getOrCreateThread(20); + mainThread.name = 'CrRendererMain'; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + cpuStart: 1160, + cpuDuration: 0, + args: {frame: '0xdeadbeef'} + })); + rendererProcess2.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 600, + duration: 0.0, + cpuStart: 1280, + cpuDuration: 0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 300, + duration: 200, + cpuStart: 1170, + cpuDuration: 100, + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const hist = histograms.getHistogramNamed('cpuTimeToFirstMeaningfulPaint'); + assert.deepEqual(hist.sampleValues, [50, 100]); + const histBin1 = hist.getBinForValue(50); + assert.strictEqual(histBin1.diagnosticMaps[0] + .get('breakdown').get('resource_loading'), 50); + const histBin2 = hist.getBinForValue(100); + assert.strictEqual(histBin2.diagnosticMaps[0] + .get('breakdown').get('other'), 100); + }); + + function addFrameLoaderObject_(rendererProcess, timestamp) { + rendererProcess.objects.addSnapshot( + 'ptr', 'loading', 'FrameLoader', timestamp, { + isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com', + }); + } + + function addNavigationStart_(rendererMain, timestamp, opt_data) { + rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: timestamp, + duration: 0.0, + args: {frame: '0xdeadbeef', data: opt_data} + })); + } + + // Some utility functions to make tests easier to read. + function addFirstMeaningfulPaint_(rendererMain, timestamp) { + rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: timestamp, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + } + + function addDomContentLoadedEnd_(rendererMain, timestamp) { + rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'domContentLoadedEventEnd', + start: timestamp, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + } + + function addTopLevelTask_(rendererMain, start, duration) { + rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start, + duration, + })); + } + + function addNetworkRequest_(rendererMain, start, duration) { + const networkEvents = []; + rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({ + cat: 'disabled-by-default-network', + title: 'ResourceLoad', + start, + duration, + })); + } + + test('loadExpectationMetrics_urlDirectlyFromNavStartEvent', function() { + // If the navigation start event contains the URL, we should not require a + // FrameLoader snapshot before the event. See https://crbug.com/824761. + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + addNavigationStart_(mainThread, 200, + {isLoadingMainFrame: true, documentLoaderURL: 'http://example.com'}); + addNetworkRequest_(mainThread, 200, 250); + // FrameLoader creation time after navigation start. + rendererProcess.objects.idWasCreated( + 'ptr', 'loading', 'FrameLoader', 250); + rendererProcess.objects.addSnapshot( + 'ptr', 'loading', 'FrameLoader', 300, { + isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com', + }); + + addFirstMeaningfulPaint_(mainThread, 500); + addDomContentLoadedEnd_(mainThread, 600); + + // New navigation to close the search window. + addNavigationStart_(mainThread, 7000, + {isLoadingMainFrame: true, documentLoaderURL: 'http://example.com'}); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const fmpHist = histograms.getHistogramNamed('timeToFirstMeaningfulPaint'); + assert.strictEqual(1, fmpHist.running.count); + assert.strictEqual(300, fmpHist.running.mean); + // Both TTI and First CPU Idle firing at DCL. + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(1, ttiHist.running.count); + assert.strictEqual(400, ttiHist.running.mean); + const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle'); + assert.strictEqual(1, firstCpuIdleHist.running.count); + assert.strictEqual(400, firstCpuIdleHist.running.mean); + }); + + test('interactivityMetrics_notAffectedByShortTasks', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + addNavigationStart_(mainThread, 200); + addNetworkRequest_(mainThread, 200, 250); + addFrameLoaderObject_(rendererProcess, 300); + addFirstMeaningfulPaint_(mainThread, 500); + addDomContentLoadedEnd_(mainThread, 600); + + const mainThreadTopLevelTasks = [ + {start: 800, duration: 100}, // Long task + {start: 1500, duration: 200}, // Last long task. TTI at 1700. + {start: 2000, duration: 49}, // Short task. + ]; + for (const task of mainThreadTopLevelTasks) { + addTopLevelTask_(mainThread, task.start, task.duration); + } + + // New navigation to close the search window. + addNavigationStart_(mainThread, 7000); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(1, ttiHist.running.count); + // 1700 - 200 (navStart) = 1500. + assert.strictEqual(1500, ttiHist.running.mean); + const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle'); + assert.strictEqual(1, firstCpuIdleHist.running.count); + // 1700 - 200 (navStart) = 1500. + assert.strictEqual(1500, firstCpuIdleHist.running.mean); + }); + + test('interactivityMetrics_longTaskBeforeFMP', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + addNavigationStart_(mainThread, 200); + addNetworkRequest_(mainThread, 200, 50); + addFrameLoaderObject_(rendererProcess, 300); + addDomContentLoadedEnd_(mainThread, 600); + addTopLevelTask_(mainThread, 600, 200); + addFirstMeaningfulPaint_(mainThread, 1000); // TTI at FMP. + // New navigation to close the search window. + addNavigationStart_(mainThread, 7000); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(1, ttiHist.running.count); + // 1000 - 200 (navStart) = 800. + assert.strictEqual(800, ttiHist.running.mean); + const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle'); + assert.strictEqual(1, firstCpuIdleHist.running.count); + // 1000 - 200 (navStart) = 800. + assert.strictEqual(800, firstCpuIdleHist.running.mean); + }); + + test('interactivityMetrics_interactiveAtDCL', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + addNavigationStart_(mainThread, 200); + addNetworkRequest_(mainThread, 200, 50); + addFrameLoaderObject_(rendererProcess, 300); + addFirstMeaningfulPaint_(mainThread, 1000); + addDomContentLoadedEnd_(mainThread, 3000); + // New navigation to close the search window. + addNavigationStart_(mainThread, 7000); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(1, ttiHist.running.count); + // 3000 - 200 (navStart) = 2800. + assert.strictEqual(2800, ttiHist.running.mean); + const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle'); + assert.strictEqual(1, firstCpuIdleHist.running.count); + // 3000 - 200 (navStart) = 2800. + assert.strictEqual(2800, firstCpuIdleHist.running.mean); + }); + + test('interactivityMetrics_networkBusyBlocksInteractivity', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + addNavigationStart_(mainThread, 200); + addFrameLoaderObject_(rendererProcess, 300); + addFirstMeaningfulPaint_(mainThread, 1000); + addDomContentLoadedEnd_(mainThread, 1100); + // Network busy requires at least three network requests. + addNetworkRequest_(mainThread, 1000, 7000); + addNetworkRequest_(mainThread, 1001, 7001); + addNetworkRequest_(mainThread, 1002, 7002); + // 400ms task makes a "heavy task cluster" for idle. + addTopLevelTask_(mainThread, 1200, 400); + // Next long task is more than five seconds away, but TTI is not reached + // yet since network is busy. TTI is at the at of this task. + addTopLevelTask_(mainThread, 6800, 200); + // New navigation to close the search window. + addNavigationStart_(mainThread, 13000); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(1, ttiHist.running.count); + // 7000 - 200 (navStart) = 6800. + assert.strictEqual(6800, ttiHist.running.mean); + const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle'); + assert.strictEqual(1, firstCpuIdleHist.running.count); + // 1600 - 200 (navStart) = 1400. CPU Idle is not affected by network. + assert.strictEqual(1400, firstCpuIdleHist.running.mean); + }); + + test('interactivityMetrics_notReportedIfTracingEndsEarly', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + addNavigationStart_(mainThread, 200); + addNetworkRequest_(mainThread, 200, 50); + addFrameLoaderObject_(rendererProcess, 300); + addDomContentLoadedEnd_(mainThread, 600); + addFirstMeaningfulPaint_(mainThread, 1000); + addTopLevelTask_(mainThread, 2000, 400); + // Last task in the model. 2501 will be considered end of tracing. + addTopLevelTask_(mainThread, 2500, 1); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(0, ttiHist.numValues); + const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle'); + assert.strictEqual(0, firstCpuIdleHist.numValues); + }); + + test('interactivityMetrics_notReportedIfNextNavigationIsEarly', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + addNavigationStart_(mainThread, 200); + addNetworkRequest_(mainThread, 200, 50); + addFrameLoaderObject_(rendererProcess, 300); + addDomContentLoadedEnd_(mainThread, 600); + addFirstMeaningfulPaint_(mainThread, 1000); + addTopLevelTask_(mainThread, 2000, 400); + // New navigation to close the search window. The window is not big enough + // to reach TTI. + addNavigationStart_(mainThread, 3000); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(0, ttiHist.numValues); + const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle'); + assert.strictEqual(0, firstCpuIdleHist.numValues); + }); + + test('interactivityMetrics_reportsValueForLastNavigation', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + addNavigationStart_(mainThread, 200); + addNetworkRequest_(mainThread, 200, 50); + addFrameLoaderObject_(rendererProcess, 300); + addDomContentLoadedEnd_(mainThread, 600); + addFirstMeaningfulPaint_(mainThread, 1000); + addTopLevelTask_(mainThread, 2000, 400); + // Last task in the model. 8001 will be considered end of tracing, so + // there is sufficiently large window to detect TTI. + addTopLevelTask_(mainThread, 8000, 1); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(1, ttiHist.running.count); + // 2400 - 200 (navStart) = 2200. + assert.strictEqual(2200, ttiHist.running.mean); + const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle'); + assert.strictEqual(1, firstCpuIdleHist.running.count); + // 2400 - 200 (navStart) = 2200. + assert.strictEqual(2200, firstCpuIdleHist.running.mean); + }); + + test('interactivityMetrics_multipleRenderers', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const renderers = + [model.getOrCreateProcess(1984), model.getOrCreateProcess(1985)]; + + for (const i of [0, 1]) { + const rendererProcess = renderers[i]; + const mainThread = rendererProcess.getOrCreateThread(2); + mainThread.name = 'CrRendererMain'; + addNavigationStart_(mainThread, 200); + addNetworkRequest_(mainThread, 200, 50); + addFrameLoaderObject_(rendererProcess, 300); + addDomContentLoadedEnd_(mainThread, 600); + addFirstMeaningfulPaint_(mainThread, 1000); + // Durations are 400 and 800 for i value of 0 an 1. + addTopLevelTask_(mainThread, 2000, (i + 1) * 400); + // New navigation to close the search window. + addNavigationStart_(mainThread, 10000); + } + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(2, ttiHist.running.count); + // 2800 - 200 (navStart) = 2200, and 2400 - 200 = 2200. + assert.strictEqual(2600, ttiHist.running.max); + assert.strictEqual(2200, ttiHist.running.min); + const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle'); + assert.strictEqual(2, firstCpuIdleHist.running.count); + // 2800 - 200 (navStart) = 2200, and 2400 - 200 = 2200. + assert.strictEqual(2600, firstCpuIdleHist.running.max); + assert.strictEqual(2200, firstCpuIdleHist.running.min); + }); + + test('interactivityMetrics_eventsFromMultipleFrame', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + addNavigationStart_(mainThread, 200); + addNetworkRequest_(mainThread, 200, 50); + addFrameLoaderObject_(rendererProcess, 300); + addFirstMeaningfulPaint_(mainThread, 1000); + // No long task. TTI is reached at 3000. + addDomContentLoadedEnd_(mainThread, 3000); + + // DomContentLoadedEnd and NavigationStart for a different frame. + rendererProcess.objects.addSnapshot( + 'ptr', 'loading', 'FrameLoader', 4000, { + isLoadingMainFrame: true, frame: {id_ref: '0xffffffff'}, + documentLoaderURL: 'http://example.com' + }); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 4000, + duration: 0.0, + args: {frame: '0xffffffff'}, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'domContentLoadedEventEnd', + start: 4500, + duration: 0.0, + args: {frame: '0xffffffff'} + })); + + // Last task in the model. 8001 will be considered end of tracing, so + // there is sufficiently large window to detect TTI. + addTopLevelTask_(mainThread, 8000, 1); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(1, ttiHist.running.count); + // 3000 - 200 (navStart) = 2800. + assert.strictEqual(2800, ttiHist.running.mean); + const firstCpuIdleHist = histograms.getHistogramNamed('timeToFirstCpuIdle'); + assert.strictEqual(1, firstCpuIdleHist.running.count); + // 3000 - 200 (navStart) = 2800. + assert.strictEqual(2800, firstCpuIdleHist.running.mean); + }); + + test('timeToInteractive_notReportedWithoutResourceLoadEvents', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + + addNavigationStart_(mainThread, 200); + addFrameLoaderObject_(rendererProcess, 300); + addDomContentLoadedEnd_(mainThread, 600); + addFirstMeaningfulPaint_(mainThread, 1000); + addTopLevelTask_(mainThread, 2000, 400); + // Last task in the model. 8001 will be considered end of tracing, so + // there is sufficiently large window to detect TTI. + addTopLevelTask_(mainThread, 8000, 1); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const ttiHist = histograms.getHistogramNamed('timeToInteractive'); + assert.strictEqual(0, ttiHist.numValues); + }); + + + test('webView', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const process = model.rendererProcess; + const rendererThread = model.rendererMain; + rendererThread.name = 'Chrome_InProcRendererThread'; + rendererThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + process.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + { + isLoadingMainFrame: true, + frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com' + }); + rendererThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading,rail,devtools.timeline', + title: 'firstContentfulPaint', + start: 600, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + rendererThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.loadingMetric(histograms, model); + const fcp = histograms.getHistogramNamed('timeToFirstContentfulPaint'); + assert.strictEqual(1, fcp.running.count); + assert.strictEqual(400, fcp.running.mean); + const fmp = histograms.getHistogramNamed('timeToFirstMeaningfulPaint'); + assert.strictEqual(1, fmp.running.count); + assert.strictEqual(800, fmp.running.mean); + }); + + test('testGetNetworkEvents', function() { + // Our renderer looks like: + // [ irrelevant syncEvent ] + // [ irrelevant asyncEvent ] + // | [ d..netlog] + // [ netlog ] [ d..network] [ net ] + // | | | | | | + // | | | | | | + // | | | | | | + // v v v v v v + // Ts: 100 200 400 450 510 520 + const rendererPid = 245; + const netEvent1 = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'netlog', + title: 'Generic Network event', + start: 100, + duration: 100, + }); + const netEvent2 = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'disabled-by-default-network', + title: 'ResourceLoad', + start: 400, + duration: 50, + }); + const netEvent3 = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'net', + title: 'ResourceLoad', + start: 510, + duration: 10, + }); + const netEvent4 = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'disabled-by-default-netlog', + title: 'ResourceLoad', + start: 510, + duration: 10, + }); + const irrelevantAsyncEvent = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'irrelevant', + title: 'ResourceLoad', + start: 0, + duration: 510, + }); + const irrelevantSyncEvent = tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 510, + args: {frame: '0xdeadbeef'} + }); + + const model = tr.c.TestUtils.newModel(function(model) { + const rendererProcess = model.getOrCreateProcess(rendererPid); + const thread1 = rendererProcess.getOrCreateThread(1); + thread1.name = 'CrRendererMain'; + thread1.asyncSliceGroup.push(netEvent1); + const thread2 = rendererProcess.getOrCreateThread(2); + thread2.name = 'thread2'; + thread2.asyncSliceGroup.push(netEvent2); + const thread3 = rendererProcess.getOrCreateThread(3); + thread3.name = 'thread2'; + thread3.asyncSliceGroup.push(netEvent3); + const thread4 = rendererProcess.getOrCreateThread(4); + thread4.name = 'thread2'; + thread4.asyncSliceGroup.push(netEvent4); + const thread5 = rendererProcess.getOrCreateThread(5); + thread5.name = 'thread5'; + thread5.asyncSliceGroup.push(irrelevantAsyncEvent); + const thread6 = rendererProcess.getOrCreateThread(6); + thread6.name = 'thread6'; + thread6.sliceGroup.pushSlice(irrelevantSyncEvent); + }); + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + const rendererHelper = chromeHelper.rendererHelpers[rendererPid]; + const allNetworkEvents = tr.metrics.sh.getNetworkEventsInRange( + rendererHelper.process, tr.b.math.Range.fromExplicitRange(0, 550)); + assert.sameDeepMembers( + [netEvent1, netEvent2, netEvent3, netEvent4], + allNetworkEvents); + + const partialNetworkEvents = tr.metrics.sh.getNetworkEventsInRange( + rendererHelper.process, tr.b.math.Range.fromExplicitRange(0, 460)); + assert.strictEqual(2, partialNetworkEvents.length); + assert.sameDeepMembers( + [netEvent1, netEvent2], + partialNetworkEvents); + + const networkEventsWithIntersecting = + tr.metrics.sh.getNetworkEventsInRange( + rendererHelper.process, tr.b.math.Range.fromExplicitRange(0, 410)); + assert.sameDeepMembers( + [netEvent1, netEvent2], + partialNetworkEvents); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric.html new file mode 100644 index 00000000000..e42683d3164 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric.html @@ -0,0 +1,137 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/extras/chrome/chrome_user_friendly_category_driver.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + const LONG_TASK_MS = 50; + + // Anything longer than this should be so rare that its length beyond this is + // uninteresting. + const LONGEST_TASK_MS = 1000; + + /** + * This helper function calls |cb| for each of the top-level tasks on the + * given thread in the given range whose duration is longer than LONG_TASK_MS. + * + * @param {tr.model.Thread} thread + * @param {tr.b.math.Range=} opt_range + * @param {function()} cb + * @param {Object=} opt_this + */ + function iterateLongTopLevelTasksOnThreadInRange( + thread, opt_range, cb, opt_this) { + thread.sliceGroup.topLevelSlices.forEach(function(slice) { + if (opt_range && + !opt_range.intersectsExplicitRangeInclusive(slice.start, slice.end)) { + return; + } + + if (slice.duration < LONG_TASK_MS) return; + + cb.call(opt_this, slice); + }); + } + + /** + * This helper function calls |cb| for each of the main renderer threads in + * the model. + * + * @param {tr.model.Model} model The model. + * @param {function()} cb Callback. + * @param {Object=} opt_this Context object. + */ + function iterateRendererMainThreads(model, cb, opt_this) { + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (modelHelper !== undefined) { + Object.values(modelHelper.rendererHelpers).forEach( + function(rendererHelper) { + if (!rendererHelper.mainThread) return; + + cb.call(opt_this, rendererHelper.mainThread); + }); + } + } + + const BIN_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear( + LONG_TASK_MS, LONGEST_TASK_MS, 40); + + /** + * This metric directly measures long tasks on renderer main threads. + * This metric requires only the 'toplevel' tracing category. + * + * @param {!tr.v.HistogramSet} histograms + * @param {!tr.model.Model} model + * @param {!Object=} opt_options + */ + function longTasksMetric(histograms, model, opt_options) { + const rangeOfInterest = opt_options ? opt_options.rangeOfInterest : + undefined; + const longTaskHist = histograms.createHistogram( + 'longTasks', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], { + binBoundaries: BIN_BOUNDARIES, + description: 'durations of long tasks', + }); + + const relatedNames = new tr.v.d.RelatedNameMap(); + longTaskHist.diagnostics.set('categories', relatedNames); + + iterateRendererMainThreads(model, function(thread) { + iterateLongTopLevelTasksOnThreadInRange( + thread, rangeOfInterest, function(task) { + const breakdown = new tr.v.d.Breakdown(); + breakdown.colorScheme = + tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER; + for (const slice of task.descendentSlices) { + const sample = slice.cpuSelfTime; + if (sample === undefined) continue; + + const category = model.getUserFriendlyCategoryFromEvent(slice); + const histName = 'longTasks:' + category; + let hist = histograms.getHistogramNamed(histName); + if (hist === undefined) { + hist = histograms.createHistogram(histName, + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], { + binBoundaries: BIN_BOUNDARIES, + }); + relatedNames.set(category, hist.name); + } + hist.addSample(sample, { + events: new tr.v.d.RelatedEventSet([slice]), + }); + breakdown.set(category, sample + breakdown.get(category)); + } + + longTaskHist.addSample(task.duration, { + events: new tr.v.d.RelatedEventSet([task]), + categories: breakdown, + }); + }); + }); + } + + tr.metrics.MetricRegistry.register(longTasksMetric, { + supportsRangeOfInterest: true, + requiredCategories: ['toplevel'], + }); + + return { + longTasksMetric, + iterateLongTopLevelTasksOnThreadInRange, + iterateRendererMainThreads, + LONG_TASK_MS, + LONGEST_TASK_MS, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric_test.html new file mode 100644 index 00000000000..ecfeabd2a5a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/long_tasks_metric_test.html @@ -0,0 +1,111 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html"> +<link rel="import" href="/tracing/metrics/system_health/long_tasks_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('longTasksMetric', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const proc = model.getOrCreateProcess(1); + const thread = proc.getOrCreateThread(2); + thread.name = 'CrRendererMain'; + const longTask = tr.c.TestUtils.newSliceEx({ + title: 'foo', + start: 0, + duration: 50, + }); + thread.sliceGroup.pushSlice(longTask); + + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + title: 'UpdateLayerTree', + start: 0, + duration: 1, + cpuStart: 0, + cpuDuration: 1, + })); + + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + title: 'minorGC', + start: 1, + duration: 1, + cpuStart: 1, + cpuDuration: 1, + })); + + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + title: 'Decode Image', + start: 2, + duration: 1, + cpuStart: 2, + cpuDuration: 1, + })); + + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + title: 'Layout', + start: 3, + duration: 1, + cpuStart: 3, + cpuDuration: 1, + })); + + model.addUserFriendlyCategoryDriver( + tr.e.chrome.ChromeUserFriendlyCategoryDriver); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.longTasksMetric(histograms, model); + + const longTaskHist = histograms.getHistogramNamed('longTasks'); + assert.strictEqual(1, longTaskHist.numValues); + + const relatedNames = longTaskHist.diagnostics.get('categories'); + assert.instanceOf(relatedNames, tr.v.d.RelatedNameMap); + assert.strictEqual(relatedNames.get('layout'), 'longTasks:layout'); + assert.strictEqual(relatedNames.get('gc'), 'longTasks:gc'); + assert.strictEqual(relatedNames.get('composite'), 'longTasks:composite'); + assert.strictEqual(relatedNames.get('imageDecode'), + 'longTasks:imageDecode'); + + const bin = longTaskHist.getBinForValue(longTaskHist.average); + const diagnostics = tr.b.getOnlyElement(bin.diagnosticMaps); + const breakdown = diagnostics.get('categories'); + assert.instanceOf(breakdown, tr.v.d.Breakdown); + assert.strictEqual(breakdown.size, 4); + + const taskEventSet = diagnostics.get('events'); + assert.instanceOf(taskEventSet, tr.v.d.RelatedEventSet); + assert.lengthOf(taskEventSet, 1); + + const layoutHist = histograms.getHistogramNamed('longTasks:layout'); + assert.strictEqual(1, layoutHist.numValues); + assert.lengthOf(tr.b.getOnlyElement(layoutHist.getBinForValue( + layoutHist.average).diagnosticMaps).get('events'), 1); + + const gcHist = histograms.getHistogramNamed('longTasks:gc'); + assert.strictEqual(1, gcHist.numValues); + assert.lengthOf(tr.b.getOnlyElement(gcHist.getBinForValue( + gcHist.average).diagnosticMaps).get('events'), 1); + + const compositeHist = histograms.getHistogramNamed('longTasks:composite'); + assert.strictEqual(1, compositeHist.numValues); + assert.lengthOf(tr.b.getOnlyElement(compositeHist.getBinForValue( + compositeHist.average).diagnosticMaps).get('events'), 1); + + const imageDecodeHist = histograms.getHistogramNamed( + 'longTasks:imageDecode'); + assert.strictEqual(1, imageDecodeHist.numValues); + assert.lengthOf(tr.b.getOnlyElement(imageDecodeHist.getBinForValue( + imageDecodeHist.average).diagnosticMaps).get('events'), 1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric.html new file mode 100644 index 00000000000..68a90814bd0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric.html @@ -0,0 +1,1332 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/multi_dimensional_view.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_processes.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/system_health/utils.html"> +<link rel="import" href="/tracing/model/container_memory_dump.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/value/diagnostics/breakdown.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND; + const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT; + const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter; + const DISPLAYED_SIZE_NUMERIC_NAME = + tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME; + + const LEVEL_OF_DETAIL_NAMES = new Map(); + LEVEL_OF_DETAIL_NAMES.set(BACKGROUND, 'background'); + LEVEL_OF_DETAIL_NAMES.set(LIGHT, 'light'); + LEVEL_OF_DETAIL_NAMES.set(DETAILED, 'detailed'); + + // Some detailed dumps contain heap profiler information. + const HEAP_PROFILER_DETAIL_NAME = 'heap_profiler'; + + const BOUNDARIES_FOR_UNIT_MAP = new WeakMap(); + // For unitless numerics (process counts), we use 20 linearly scaled bins + // from 0 to 20. + BOUNDARIES_FOR_UNIT_MAP.set(count_smallerIsBetter, + tr.v.HistogramBinBoundaries.createLinear(0, 20, 20)); + // For size numerics (subsystem and vm stats), we use 1 bin from 0 B to + // 1 KiB and 4*24 exponentially scaled bins from 1 KiB to 16 GiB (=2^24 KiB). + BOUNDARIES_FOR_UNIT_MAP.set(sizeInBytes_smallerIsBetter, + new tr.v.HistogramBinBoundaries(0) + .addBinBoundary(1024 /* 1 KiB */) + .addExponentialBins(16 * 1024 * 1024 * 1024 /* 16 GiB */, 4 * 24)); + + const CHROME_PROCESS_NAMES = + tr.e.chrome.chrome_processes.CHROME_PROCESS_NAMES; + + function memoryMetric(values, model, opt_options) { + const rangeOfInterest = + opt_options ? opt_options.rangeOfInterest : undefined; + const browserNameToGlobalDumps = + tr.metrics.sh.splitGlobalDumpsByBrowserName(model, rangeOfInterest); + addGeneralMemoryDumpValues(browserNameToGlobalDumps, values); + addDetailedMemoryDumpValues(browserNameToGlobalDumps, values); + addMemoryDumpCountValues(browserNameToGlobalDumps, values); + } + + const USER_FRIENDLY_BROWSER_NAMES = { + 'chrome': 'Chrome', + 'webview': 'WebView', + 'unknown_browser': 'an unknown browser' + }; + + /** + * Convert a canonical browser name used in value names to a user-friendly + * name used in value descriptions. + * + * Examples: + * + * CANONICAL BROWSER NAME -> USER-FRIENDLY NAME + * chrome -> Chrome + * unknown_browser -> an unknown browser + * webview2 -> WebView(2) + * unexpected -> 'unexpected' browser + */ + function convertBrowserNameToUserFriendlyName(browserName) { + for (const baseName in USER_FRIENDLY_BROWSER_NAMES) { + if (!browserName.startsWith(baseName)) continue; + + const userFriendlyBaseName = USER_FRIENDLY_BROWSER_NAMES[baseName]; + const suffix = browserName.substring(baseName.length); + if (suffix.length === 0) { + return userFriendlyBaseName; + } else if (/^\d+$/.test(suffix)) { + return userFriendlyBaseName + '(' + suffix + ')'; + } + } + return '\'' + browserName + '\' browser'; + } + + + /** + * Convert a canonical process name used in value names to a user-friendly + * name used in value descriptions. + */ + function convertProcessNameToUserFriendlyName(processName, + opt_requirePlural) { + switch (processName) { + case CHROME_PROCESS_NAMES.BROWSER: + return opt_requirePlural ? 'browser processes' : 'the browser process'; + case CHROME_PROCESS_NAMES.RENDERER: + return 'renderer processes'; + case CHROME_PROCESS_NAMES.GPU: + return opt_requirePlural ? 'GPU processes' : 'the GPU process'; + case CHROME_PROCESS_NAMES.PPAPI: + return opt_requirePlural ? 'PPAPI processes' : 'the PPAPI process'; + case CHROME_PROCESS_NAMES.ALL: + return 'all processes'; + case CHROME_PROCESS_NAMES.UNKNOWN: + return 'unknown processes'; + default: + return '\'' + processName + '\' processes'; + } + } + + /** + * Add general memory dump values calculated from all global memory dumps to + * |values|. In particular, this function adds the following values: + * + * * PROCESS COUNTS + * memory:{chrome, webview}: + * {browser_process, renderer_processes, ..., all_processes}: + * process_count + * type: tr.v.Histogram (over all matching global memory dumps) + * unit: count_smallerIsBetter + * + * * MEMORY USAGE REPORTED BY CHROME + * memory:{chrome, webview}: + * {browser_process, renderer_processes, ..., all_processes}: + * reported_by_chrome[:{v8, malloc, ...}]: + * {effective_size, allocated_objects_size, locked_size} + * type: tr.v.Histogram (over all matching global memory dumps) + * unit: sizeInBytes_smallerIsBetter + */ + function addGeneralMemoryDumpValues(browserNameToGlobalDumps, values) { + addMemoryDumpValues(browserNameToGlobalDumps, + gmd => true /* process all global memory dumps */, + function(processDump, addProcessScalar) { + // Increment memory:<browser-name>:<process-name>:process_count value. + addProcessScalar({ + source: 'process_count', + property: PROCESS_COUNT, + value: 1 + }); + + if (processDump.totals !== undefined) { + addProcessScalar({ + source: 'reported_by_os', + property: RESIDENT_SIZE, + component: ['system_memory'], + value: processDump.totals.residentBytes + }); + addProcessScalar({ + source: 'reported_by_os', + property: PEAK_RESIDENT_SIZE, + component: ['system_memory'], + value: processDump.totals.peakResidentBytes + }); + addProcessScalar({ + source: 'reported_by_os', + property: PRIVATE_FOOTPRINT_SIZE, + component: ['system_memory'], + value: processDump.totals.privateFootprintBytes, + }); + } + + // Add memory:<browser-name>:<process-name>:reported_by_chrome:... + // values. + if (processDump.memoryAllocatorDumps === undefined) return; + + processDump.memoryAllocatorDumps.forEach(function(rootAllocatorDump) { + CHROME_VALUE_PROPERTIES.forEach(function(property) { + addProcessScalar({ + source: 'reported_by_chrome', + component: [rootAllocatorDump.name], + property, + value: rootAllocatorDump.numerics[property.name] + }); + }); + // Some dump providers add allocated objects size as + // "allocated_objects" child dump. + if (rootAllocatorDump.numerics.allocated_objects_size === + undefined) { + const allocatedObjectsDump = + rootAllocatorDump.getDescendantDumpByFullName( + 'allocated_objects'); + if (allocatedObjectsDump !== undefined) { + addProcessScalar({ + source: 'reported_by_chrome', + component: [rootAllocatorDump.name], + property: ALLOCATED_OBJECTS_SIZE, + value: allocatedObjectsDump.numerics.size + }); + } + } + }); + + // Add memory:<browser-name>:<process-name>:reported_by_chrome: + // {malloc, blinkgc, partitionalloc}:<largestCategory>:... + addTopHeapDumpCategoryValue(processDump, addProcessScalar); + + // Add memory:<browser-name>:<process-name>:reported_by_chrome:v8: + // {heap, allocated_by_malloc}:... + addV8MemoryDumpValues(processDump, addProcessScalar); + }, + function(componentTree) { + // Subtract memory:<browser-name>:<process-name>:reported_by_chrome: + // tracing:<size-property> from memory:<browser-name>:<process-name>: + // reported_by_chrome:<size-property> if applicable. + const tracingNode = componentTree.children[1].get('tracing'); + if (tracingNode === undefined) return; + + for (let i = 0; i < componentTree.values.length; i++) { + componentTree.values[i].total -= tracingNode.values[i].total; + } + }, values); + } + + /** + * Add memory dump values for the top category in each allocator heap dump in + * the process dump. + * + * @param {!tr.model.ProcessMemoryDump} processDump The process memory dump. + * @param {!function} addProcessScalar The callback for adding a scalar value. + */ + function addTopHeapDumpCategoryValue(processDump, addProcessScalar) { + if (!processDump.heapDumps) { + return; + } + for (const allocatorName in processDump.heapDumps) { + const heapDump = processDump.heapDumps[allocatorName]; + if (heapDump.entries === undefined || heapDump.entries.length === 0) { + return; + } + // Create a map of category to total size. + const typeToSize = {}; + for (let i = 0; i < heapDump.entries.length; i += 1) { + const entry = heapDump.entries[i]; + // Count only the entries with empty backtrace which contains totals for + // the object type. + if (!entry.objectTypeName || entry.leafStackFrame) { + continue; + } + if (!typeToSize[entry.objectTypeName]) { + typeToSize[entry.objectTypeName] = 0; + } + typeToSize[entry.objectTypeName] += entry.size; + } + + // Find the largest type in the heap dump. + let largestValue = 0; + let largestType = ''; + for (const key in typeToSize) { + if (largestValue < typeToSize[key]) { + largestValue = typeToSize[key]; + largestType = key; + } + } + addProcessScalar({ + source: 'reported_by_chrome', + component: [allocatorName, largestType], + property: HEAP_CATEGORY_SIZE, + value: largestValue + }); + } + } + + /** + * Add memory dump values calculated from V8 components excluding + * 'heap_spaces/other_spaces'. + * + * @param {!tr.model.ProcessMemoryDump} processDump The process memory dump. + * @param {!function} addProcessScalar The callback for adding a scalar value. + */ + function addV8MemoryDumpValues(processDump, addProcessScalar) { + const v8Dump = processDump.getMemoryAllocatorDumpByFullName('v8'); + if (v8Dump === undefined) return; + + v8Dump.children.forEach(function(isolateDump) { + // v8:allocated_by_malloc:... + const mallocDump = isolateDump.getDescendantDumpByFullName('malloc'); + if (mallocDump !== undefined) { + addV8ComponentValues(mallocDump, ['v8', 'allocated_by_malloc'], + addProcessScalar); + } + // v8:heap:... + let heapDump = isolateDump.getDescendantDumpByFullName('heap'); + if (heapDump === undefined) { + // Old V8 memory dumps call this 'heap_spaces'. + heapDump = isolateDump.getDescendantDumpByFullName('heap_spaces'); + } + if (heapDump !== undefined) { + addV8ComponentValues(heapDump, ['v8', 'heap'], addProcessScalar); + heapDump.children.forEach(function(spaceDump) { + if (spaceDump.name === 'other_spaces') return; + + addV8ComponentValues(spaceDump, ['v8', 'heap', spaceDump.name], + addProcessScalar); + }); + } + }); + + // V8 generates bytecode when interpreting and code objects when + // compiling the javascript. Total code size includes the size + // of code and bytecode objects. + addProcessScalar({ + source: 'reported_by_chrome', + component: ['v8'], + property: CODE_AND_METADATA_SIZE, + value: v8Dump.numerics.code_and_metadata_size + }); + addProcessScalar({ + source: 'reported_by_chrome', + component: ['v8'], + property: CODE_AND_METADATA_SIZE, + value: v8Dump.numerics.bytecode_and_metadata_size + }); + } + + /** + * Add memory dump values calculated from the specified V8 component. + * + * @param {!tr.model.MemoryAllocatorDump} v8Dump The V8 memory dump. + * @param {!Array<string>} componentPath The component path for reporting. + * @param {!function} addProcessScalar The callback for adding a scalar value. + */ + function addV8ComponentValues(componentDump, componentPath, + addProcessScalar) { + CHROME_VALUE_PROPERTIES.forEach(function(property) { + addProcessScalar({ + source: 'reported_by_chrome', + component: componentPath, + property, + value: componentDump.numerics[property.name] + }); + }); + } + + const PROCESS_COUNT = { + unit: count_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + if (componentPath.length > 0) { + throw new Error('Unexpected process count non-empty component path: ' + + componentPath.join(':')); + } + return 'total number of ' + convertProcessNameToUserFriendlyName( + processName, true /* opt_requirePlural */); + } + }; + + const EFFECTIVE_SIZE = { + name: 'effective_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildChromeValueDescriptionPrefix(componentPath, processName, { + userFriendlyPropertyName: 'effective size', + componentPreposition: 'of' + }); + } + }; + + const ALLOCATED_OBJECTS_SIZE = { + name: 'allocated_objects_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildChromeValueDescriptionPrefix(componentPath, processName, { + userFriendlyPropertyName: 'size of all objects allocated', + totalUserFriendlyPropertyName: 'size of all allocated objects', + componentPreposition: 'by' + }); + } + }; + + const SHIM_ALLOCATED_OBJECTS_SIZE = { + name: 'shim_allocated_objects_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildChromeValueDescriptionPrefix(componentPath, processName, { + userFriendlyPropertyName: 'size of all objects allocated through shim', + totalUserFriendlyPropertyName: + 'size of all allocated objects through shim', + componentPreposition: 'by' + }); + } + }; + + const LOCKED_SIZE = { + name: 'locked_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildChromeValueDescriptionPrefix(componentPath, processName, { + userFriendlyPropertyName: 'locked (pinned) size', + componentPreposition: 'of' + }); + } + }; + + const PEAK_SIZE = { + name: 'peak_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildChromeValueDescriptionPrefix(componentPath, processName, { + userFriendlyPropertyName: 'peak size', + componentPreposition: 'of' + }); + } + }; + + const HEAP_CATEGORY_SIZE = { + name: 'heap_category_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildChromeValueDescriptionPrefix(componentPath, processName, { + userFriendlyPropertyName: 'heap profiler category size', + componentPreposition: 'for' + }); + } + }; + + const CODE_AND_METADATA_SIZE = { + name: 'code_and_metadata_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildChromeValueDescriptionPrefix(componentPath, processName, { + userFriendlyPropertyNamePrefix: 'size of', + userFriendlyPropertyName: 'code and metadata' + }); + } + }; + + const CHROME_VALUE_PROPERTIES = [ + EFFECTIVE_SIZE, + ALLOCATED_OBJECTS_SIZE, + SHIM_ALLOCATED_OBJECTS_SIZE, + LOCKED_SIZE, + PEAK_SIZE + ]; + + /** + * Build a description prefix for a memory:<browser-name>:<process-name>: + * reported_by_chrome:... value. + * + * @param {!Array<string>} componentPath The underlying component path (e.g. + * ['malloc']). + * @param {string} processName The canonical name of the process. + * @param {{ + * userFriendlyPropertyName: string, + * userFriendlyPropertyNamePrefix: (string|undefined), + * totalUserFriendlyPropertyName: (string|undefined), + * componentPreposition: (string|undefined) }} + * formatSpec Specification of how the property should be formatted. + * @return {string} Prefix for the value's description (e.g. + * 'effective size of malloc in the browser process'). + */ + function buildChromeValueDescriptionPrefix( + componentPath, processName, formatSpec) { + const nameParts = []; + if (componentPath.length === 0) { + nameParts.push('total'); + if (formatSpec.totalUserFriendlyPropertyName) { + nameParts.push(formatSpec.totalUserFriendlyPropertyName); + } else { + if (formatSpec.userFriendlyPropertyNamePrefix) { + nameParts.push(formatSpec.userFriendlyPropertyNamePrefix); + } + nameParts.push(formatSpec.userFriendlyPropertyName); + } + nameParts.push('reported by Chrome for'); + } else { + if (formatSpec.componentPreposition === undefined) { + // Use component name as an adjective + // (e.g. 'size of V8 code and metadata'). + if (formatSpec.userFriendlyPropertyNamePrefix) { + nameParts.push(formatSpec.userFriendlyPropertyNamePrefix); + } + nameParts.push(componentPath.join(':')); + nameParts.push(formatSpec.userFriendlyPropertyName); + } else { + // Use component name as a noun with a preposition + // (e.g. 'size of all objects allocated BY MALLOC'). + if (formatSpec.userFriendlyPropertyNamePrefix) { + nameParts.push(formatSpec.userFriendlyPropertyNamePrefix); + } + nameParts.push(formatSpec.userFriendlyPropertyName); + nameParts.push(formatSpec.componentPreposition); + if (componentPath[componentPath.length - 1] === 'allocated_by_malloc') { + nameParts.push('objects allocated by malloc for'); + nameParts.push( + componentPath.slice(0, componentPath.length - 1).join(':')); + } else { + nameParts.push(componentPath.join(':')); + } + } + nameParts.push('in'); + } + nameParts.push(convertProcessNameToUserFriendlyName(processName)); + return nameParts.join(' '); + } + + const RESIDENT_SIZE = { + name: 'resident_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildOsValueDescriptionPrefix(componentPath, processName, + 'resident set size (RSS)'); + } + }; + + const PEAK_RESIDENT_SIZE = { + name: 'peak_resident_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildOsValueDescriptionPrefix(componentPath, processName, + 'peak resident set size'); + } + }; + + const PROPORTIONAL_RESIDENT_SIZE = { + name: 'proportional_resident_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildOsValueDescriptionPrefix(componentPath, processName, + 'proportional resident size (PSS)'); + } + }; + + const PRIVATE_DIRTY_SIZE = { + name: 'private_dirty_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildOsValueDescriptionPrefix(componentPath, processName, + 'private dirty size'); + } + }; + + const PRIVATE_FOOTPRINT_SIZE = { + name: 'private_footprint_size', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildOsValueDescriptionPrefix(componentPath, processName, + 'private footprint size'); + } + }; + + const JAVA_BASE_CLEAN_RESIDENT = { + name: 'java_base_clean_resident', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildOsValueDescriptionPrefix(componentPath, processName, + 'java base odex and vdex total clean resident size'); + } + }; + + const JAVA_BASE_PSS = { + name: 'java_base_pss', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildOsValueDescriptionPrefix(componentPath, processName, + 'java base odex and vdex proportional resident size'); + } + }; + + const NATIVE_LIBRARY_PRIVATE_CLEAN_RESIDENT = { + name: 'native_library_private_clean_resident', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildOsValueDescriptionPrefix(componentPath, processName, + 'native library private clean resident size'); + } + }; + + const NATIVE_LIBRARY_SHARED_CLEAN_RESIDENT = { + name: 'native_library_shared_clean_resident', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildOsValueDescriptionPrefix(componentPath, processName, + 'native library shared clean resident size'); + } + }; + + const NATIVE_LIBRARY_PROPORTIONAL_RESIDENT = { + name: 'native_library_proportional_resident', + unit: sizeInBytes_smallerIsBetter, + buildDescriptionPrefix(componentPath, processName) { + return buildOsValueDescriptionPrefix(componentPath, processName, + 'native library proportional resident size'); + } + }; + + /** + * Build a description prefix for a memory:<browser-name>:<process-name>: + * reported_by_os:... value. + * + * @param {!Array<string>} componentPath The underlying component path (e.g. + * ['system', 'java_heap']). + * @param {string} processName The canonical name of the process. + * @param {string} userFriendlyPropertyName User-friendly name of the + * underlying property (e.g. 'private dirty size'). + * @return {string} Prefix for the value's description (e.g. + * 'total private dirty size of the Java heal in the GPU process'). + */ + function buildOsValueDescriptionPrefix( + componentPath, processName, userFriendlyPropertyName) { + if (componentPath.length > 2) { + throw new Error('OS value component path for \'' + + userFriendlyPropertyName + '\' too long: ' + componentPath.join(':')); + } + + const nameParts = []; + if (componentPath.length < 2) { + nameParts.push('total'); + } + + nameParts.push(userFriendlyPropertyName); + + if (componentPath.length > 0) { + switch (componentPath[0]) { + case 'system_memory': + if (componentPath.length > 1) { + const userFriendlyComponentName = + SYSTEM_VALUE_COMPONENTS[componentPath[1]].userFriendlyName; + if (userFriendlyComponentName === undefined) { + throw new Error('System value sub-component for \'' + + userFriendlyPropertyName + '\' unknown: ' + + componentPath.join(':')); + } + nameParts.push('of', userFriendlyComponentName, 'in'); + } else { + nameParts.push('of system memory (RAM) used by'); + } + break; + + case 'gpu_memory': + if (componentPath.length > 1) { + nameParts.push('of the', componentPath[1]); + nameParts.push('Android memtrack component in'); + } else { + nameParts.push('of GPU memory (Android memtrack) used by'); + } + break; + + default: + throw new Error('OS value component for \'' + + userFriendlyPropertyName + '\' unknown: ' + + componentPath.join(':')); + } + } else { + nameParts.push('reported by the OS for'); + } + + nameParts.push(convertProcessNameToUserFriendlyName(processName)); + return nameParts.join(' '); + } + + /** + * Add heavy memory dump values calculated from heavy global memory dumps to + * |values|. In particular, this function adds the following values: + * + * * MEMORY USAGE REPORTED BY THE OS + * memory:{chrome, webview}: + * {browser_process, renderer_processes, ..., all_processes}: + * reported_by_os:system_memory:[{ashmem, native_heap, java_heap}:] + * {proportional_resident_size, private_dirty_size} + * memory:{chrome, webview}: + * {browser_process, renderer_processes, ..., all_processes}: + * reported_by_os:gpu_memory:[{gl, graphics, ...}:] + * proportional_resident_size + * type: tr.v.Histogram (over matching heavy global memory dumps) + * unit: sizeInBytes_smallerIsBetter + * + * * MEMORY USAGE REPORTED BY CHROME + * memory:{chrome, webview}: + * {browser_process, renderer_processes, ..., all_processes}: + * reported_by_chrome:v8:code_and_metadata_size + * type: tr.v.Histogram (over matching heavy global memory dumps) + * unit: sizeInBytes_smallerIsBetter + */ + function addDetailedMemoryDumpValues(browserNameToGlobalDumps, values) { + addMemoryDumpValues(browserNameToGlobalDumps, + g => g.levelOfDetail === DETAILED, + function(processDump, addProcessScalar) { + // Add memory:<browser-name>:<process-name>:reported_by_os: + // system_memory:... values. + for (const [componentName, componentSpec] of + Object.entries(SYSTEM_VALUE_COMPONENTS)) { + const node = getDescendantVmRegionClassificationNode( + processDump.vmRegions, componentSpec.classificationPath); + const componentPath = ['system_memory']; + if (componentName) componentPath.push(componentName); + addProcessScalar({ + source: 'reported_by_os', + component: componentPath, + property: PROPORTIONAL_RESIDENT_SIZE, + value: node === undefined ? + 0 : (node.byteStats.proportionalResident || 0) + }); + addProcessScalar({ + source: 'reported_by_os', + component: componentPath, + property: PRIVATE_DIRTY_SIZE, + value: node === undefined ? + 0 : (node.byteStats.privateDirtyResident || 0) + }); + + // Only add java base stats when they are nonzero. + if (node) { + if (node.byteStats.javaBasePss) { + addProcessScalar({ + source: 'reported_by_os', + component: componentPath, + property: JAVA_BASE_PSS, + value: node.byteStats.javaBasePss + }); + } + if (node.byteStats.javaBaseCleanResident) { + addProcessScalar({ + source: 'reported_by_os', + component: componentPath, + property: JAVA_BASE_CLEAN_RESIDENT, + value: node.byteStats.javaBaseCleanResident + }); + } + } + + // Only add native library stats when they are nonzero. + if (node) { + if (node.byteStats.nativeLibraryPrivateCleanResident) { + addProcessScalar({ + source: 'reported_by_os', + component: componentPath, + property: NATIVE_LIBRARY_PRIVATE_CLEAN_RESIDENT, + value: node.byteStats.nativeLibraryPrivateCleanResident + }); + } + if (node.byteStats.nativeLibrarySharedCleanResident) { + addProcessScalar({ + source: 'reported_by_os', + component: componentPath, + property: NATIVE_LIBRARY_SHARED_CLEAN_RESIDENT, + value: node.byteStats.nativeLibrarySharedCleanResident + }); + } + if (node.byteStats.nativeLibraryProportionalResident) { + addProcessScalar({ + source: 'reported_by_os', + component: componentPath, + property: NATIVE_LIBRARY_PROPORTIONAL_RESIDENT, + value: node.byteStats.nativeLibraryProportionalResident + }); + } + } + } + + // Add memory:<browser-name>:<process-name>:reported_by_os: + // gpu_memory:... values. + const memtrackDump = processDump.getMemoryAllocatorDumpByFullName( + 'gpu/android_memtrack'); + if (memtrackDump !== undefined) { + memtrackDump.children.forEach(function(memtrackChildDump) { + addProcessScalar({ + source: 'reported_by_os', + component: ['gpu_memory', memtrackChildDump.name], + property: PROPORTIONAL_RESIDENT_SIZE, + value: memtrackChildDump.numerics.memtrack_pss + }); + }); + } + }, function(componentTree) {}, values); + } + + // Specifications of components reported by the system. + const SYSTEM_VALUE_COMPONENTS = { + '': { + classificationPath: [], + }, + 'java_heap': { + classificationPath: ['Android', 'Java runtime', 'Spaces'], + userFriendlyName: 'the Java heap' + }, + 'ashmem': { + classificationPath: ['Android', 'Ashmem'], + userFriendlyName: 'ashmem' + }, + 'native_heap': { + classificationPath: ['Native heap'], + userFriendlyName: 'the native heap' + }, + 'stack': { + classificationPath: ['Stack'], + userFriendlyName: 'the thread stacks' + } + }; + + /** + * Get the descendant of a VM region classification |node| specified by the + * given |path| of child node titles. If |node| is undefined or such a + * descendant does not exist, this function returns undefined. + */ + function getDescendantVmRegionClassificationNode(node, path) { + for (let i = 0; i < path.length; i++) { + if (node === undefined) break; + + node = node.children.find(c => c.title === path[i]); + } + return node; + } + + /** + * Add global memory dump counts to |values|. In particular, this function + * adds the following values: + * + * * DUMP COUNTS + * memory:{chrome, webview}:all_processes:dump_count + * [:{light, detailed, heap_profiler}] + * type: tr.v.Histogram + * unit: count_smallerIsBetter + * + * Note that unlike all other values generated by the memory metric, the + * global memory dump counts are NOT instances of tr.v.Histogram + * because it doesn't make sense to aggregate them (they are already counts + * over all global dumps associated with the relevant browser). + */ + function addMemoryDumpCountValues(browserNameToGlobalDumps, values) { + browserNameToGlobalDumps.forEach(function(globalDumps, browserName) { + let totalDumpCount = 0; + const levelOfDetailNameToDumpCount = {}; + LEVEL_OF_DETAIL_NAMES.forEach(function(levelOfDetailName) { + levelOfDetailNameToDumpCount[levelOfDetailName] = 0; + }); + levelOfDetailNameToDumpCount[HEAP_PROFILER_DETAIL_NAME] = 0; + + globalDumps.forEach(function(globalDump) { + totalDumpCount++; + + // Increment the level-of-detail-specific dump count (if possible). + const levelOfDetailName = + LEVEL_OF_DETAIL_NAMES.get(globalDump.levelOfDetail); + if (levelOfDetailName === undefined) { + return; // Unknown level of detail. + } + levelOfDetailNameToDumpCount[levelOfDetailName]++; + if (globalDump.levelOfDetail === DETAILED) { + if (detectHeapProfilerInMemoryDump(globalDump)) { + levelOfDetailNameToDumpCount[HEAP_PROFILER_DETAIL_NAME]++; + } + } + }); + + // Add memory:<browser-name>:all_processes:dump_count[:<level>] values. + reportMemoryDumpCountAsValue(browserName, undefined /* total */, + totalDumpCount, values); + for (const [levelOfDetailName, levelOfDetailDumpCount] of + Object.entries(levelOfDetailNameToDumpCount)) { + reportMemoryDumpCountAsValue(browserName, levelOfDetailName, + levelOfDetailDumpCount, values); + } + }); + } + + /** + * Check whether detailed global dump has heap profiler information or not. + */ + function detectHeapProfilerInMemoryDump(globalDump) { + for (const processDump of Object.values(globalDump.processMemoryDumps)) { + if (processDump.heapDumps && processDump.heapDumps.malloc) { + const mallocDump = processDump.heapDumps.malloc; + if (mallocDump.entries && mallocDump.entries.length > 0) { + return true; + } + } + } + return false; + } + + /** + * Add a tr.v.Histogram value to |values| reporting that the number of + * |levelOfDetailName| memory dumps added by |browserName| was + * |levelOfDetailCount|. + */ + function reportMemoryDumpCountAsValue( + browserName, levelOfDetailName, levelOfDetailDumpCount, values) { + // Construct the name of the memory value. + const nameParts = ['memory', browserName, 'all_processes', 'dump_count']; + if (levelOfDetailName !== undefined) { + nameParts.push(levelOfDetailName); + } + const name = nameParts.join(':'); + + // Build the underlying histogram for the memory value. + const histogram = new tr.v.Histogram(name, count_smallerIsBetter, + BOUNDARIES_FOR_UNIT_MAP.get(count_smallerIsBetter)); + histogram.addSample(levelOfDetailDumpCount); + + // If |levelOfDetail| argument is undefined it means a total value. + const userFriendlyLevelOfDetail = + (levelOfDetailName || 'all').replace('_', ' '); + + // Build the options for the memory value. + histogram.description = [ + 'total number of', + userFriendlyLevelOfDetail, + 'memory dumps added by', + convertBrowserNameToUserFriendlyName(browserName), + 'to the trace' + ].join(' '); + + // Report the memory value. + values.addHistogram(histogram); + } + + /** + * Add generic values extracted from process memory dumps and aggregated by + * process name and component path into |values|. + * + * For each browser and set of global dumps in |browserNameToGlobalDumps|, + * |customProcessDumpValueExtractor| is applied to every process memory dump + * associated with the global memory dump. The second argument provided to the + * callback is a function for adding extracted values: + * + * function sampleProcessDumpCallback(processDump, addProcessValue) { + * ... + * addProcessScalar({ + * source: 'reported_by_chrome', + * component: ['system', 'native_heap'], + * property: 'proportional_resident_size', + * value: pssExtractedFromProcessDump2, + * descriptionPrefixBuilder(componentPath) { + * return 'PSS of ' + componentPath.join('/') + ' in'; + * } + * }); + * ... + * } + * + * For each global memory dump, the extracted values are summed by process + * name (browser_process, renderer_processes, ..., all_processes) and + * component path (e.g. gpu is a sum of gpu:gl, gpu:graphics, ...). The sums + * are then aggregated over all global memory dumps associated with the given + * browser. For example, assuming that |customProcessDumpValueExtractor| + * extracts 'proportional_resident_size' values for component paths + * ['X', 'A'], ['X', 'B'] and ['Y'] under the same 'source' from each process + * memory dump, the following values will be reported (for Chrome): + * + * memory:chrome:browser_process:source:X:A:proportional_resident_size : + * Histogram aggregated over [ + * sum of X:A in all 'browser' process dumps in global dump 1, + * ... + * sum of X:A in all 'browser' process dumps in global dump N + * ] + * + * memory:chrome:browser_process:source:X:B:proportional_resident_size : + * Histogram aggregated over [ + * sum of X:B in all 'browser' process dumps in global dump 1, + * ... + * sum of X:B in all 'browser' process dumps in global dump N + * ] + * + * memory:chrome:browser_process:source:X:proportional_resident_size : + * Histogram aggregated over [ + * sum of X:A+X:B in all 'browser' process dumps in global dump 1, + * ... + * sum of X:A+X:B in all 'browser' process dumps in global dump N + * ] + * + * memory:chrome:browser_process:source:Y:proportional_resident_size : + * Histogram aggregated over [ + * sum of Y in all 'browser' process dumps in global dump 1, + * ... + * sum of Y in all 'browser' process dumps in global dump N + * ] + * + * memory:chrome:browser_process:source:proportional_resident_size : + * Histogram aggregated over [ + * sum of X:A+X:B+Y in all 'browser' process dumps in global dump 1, + * ... + * sum of X:A+X:B+Y in all 'browser' process dumps in global dump N + * ] + * + * ... + * + * memory:chrome:all_processes:source:X:A:proportional_resident_size : + * Histogram aggregated over [ + * sum of X:A in all process dumps in global dump 1, + * ... + * sum of X:A in all process dumps in global dump N, + * ] + * + * memory:chrome:all_processes:source:X:B:proportional_resident_size : + * Histogram aggregated over [ + * sum of X:B in all process dumps in global dump 1, + * ... + * sum of X:B in all process dumps in global dump N, + * ] + * + * memory:chrome:all_processes:source:X:proportional_resident_size : + * Histogram aggregated over [ + * sum of X:A+X:B in all process dumps in global dump 1, + * ... + * sum of X:A+X:B in all process dumps in global dump N, + * ] + * + * memory:chrome:all_processes:source:Y:proportional_resident_size : + * Histogram aggregated over [ + * sum of Y in all process dumps in global dump 1, + * ... + * sum of Y in all process dumps in global dump N + * ] + * + * memory:chrome:all_processes:source:proportional_resident_size : + * Histogram aggregated over [ + * sum of X:A+X:B+Y in all process dumps in global dump 1, + * ... + * sum of X:A+X:B+Y in all process dumps in global dump N + * ] + * + * where global dumps 1 to N are the global dumps associated with the given + * browser. + * + * @param {!Map<string, !Array<!tr.model.GlobalMemoryDump>} + * browserNameToGlobalDumps Map from browser names to arrays of global + * memory dumps. The generic values will be extracted from the associated + * process memory dumps. + * @param {!function(!tr.model.GlobalMemoryDump): boolean} + * customGlobalDumpFilter Predicate for filtering global memory dumps. + * @param {!function( + * !tr.model.ProcessMemoryDump, + * !function(!{ + * source: string, + * componentPath: (!Array<string>|undefined), + * property: !{name: string, unit: !tr.b.Unit, buildDescriptionPrefix: + * !function(!Array<string>, string): string}, + * value: (!tr.v.Histogram|number|undefined) + * }))} + * customProcessDumpValueExtractor Callback for extracting values from a + * process memory dump. + * @param {!function(!tr.b.MultiDimensionalViewNode)} + * customComponentTreeModifier Callback applied to every component tree + * wrt each process name. + * @param {!tr.v.HistogramSet} values List of values to which the + * resulting aggregated values are added. + */ + function addMemoryDumpValues(browserNameToGlobalDumps, customGlobalDumpFilter, + customProcessDumpValueExtractor, customComponentTreeModifier, + values) { + browserNameToGlobalDumps.forEach(function(globalDumps, browserName) { + const filteredGlobalDumps = globalDumps.filter(customGlobalDumpFilter); + const sourceToPropertyToBuilder = extractDataFromGlobalDumps( + filteredGlobalDumps, customProcessDumpValueExtractor); + reportDataAsValues(sourceToPropertyToBuilder, browserName, + customComponentTreeModifier, values); + }); + } + + /** + * For each global memory dump in |globalDumps|, calculate per-process-name + * sums of values extracted by |customProcessDumpValueExtractor| from the + * associated process memory dumps. + * + * This function returns the following nested map structure: + * + * Source name (Map key, e.g. 'reported_by_os') + * -> Property (Map key, e.g. PROPORTIONAL_RESIDENT_SIZE) + * -> processAndComponentTreeBuilder + * + * where |processAndComponentTreeBuilder| is a + * tr.b.MultiDimensionalViewBuilder: + * + * Process name (0th dimension key, e.g. 'browser_process') x + * Component path (1st dimension keys, e.g. ['system', 'native_heap']) + * -> Sum of value over the processes (number). + * + * See addMemoryDumpValues for more details. + */ + function extractDataFromGlobalDumps( + globalDumps, customProcessDumpValueExtractor) { + const sourceToPropertyToBuilder = new Map(); + const dumpCount = globalDumps.length; + globalDumps.forEach(function(globalDump, dumpIndex) { + for (const processDump of Object.values(globalDump.processMemoryDumps)) { + extractDataFromProcessDump( + processDump, sourceToPropertyToBuilder, dumpIndex, dumpCount, + customProcessDumpValueExtractor); + } + }); + return sourceToPropertyToBuilder; + } + + function extractDataFromProcessDump(processDump, sourceToPropertyToBuilder, + dumpIndex, dumpCount, customProcessDumpValueExtractor) { + // Process name is typically 'browser', 'renderer', etc. + const rawProcessName = processDump.process.name; + const processNamePath = + [tr.e.chrome.chrome_processes.canonicalizeProcessName(rawProcessName)]; + + customProcessDumpValueExtractor( + processDump, + function addProcessScalar(spec) { + if (spec.value === undefined) return; + + const component = spec.component || []; + function createDetailsForErrorMessage() { + return ['source=', spec.source, ', property=', + spec.property.name || '(undefined)', ', component=', + component.length === 0 ? '(empty)' : component.join(':'), + ' in ', processDump.process.userFriendlyName].join(''); + } + + let value; + if (spec.value instanceof tr.b.Scalar) { + value = spec.value.value; + if (spec.value.unit !== spec.property.unit) { + throw new Error('Scalar unit for ' + + createDetailsForErrorMessage() + ' (' + + spec.value.unit.unitName + + ') doesn\'t match the unit of the property (' + + spec.property.unit.unitName + ')'); + } + } else { + value = spec.value; + } + + let propertyToBuilder = sourceToPropertyToBuilder.get(spec.source); + if (propertyToBuilder === undefined) { + propertyToBuilder = new Map(); + sourceToPropertyToBuilder.set(spec.source, propertyToBuilder); + } + + let builder = propertyToBuilder.get(spec.property); + if (builder === undefined) { + builder = new tr.b.MultiDimensionalViewBuilder( + 2 /* dimensions (process name and component path) */, + dumpCount /* valueCount */), + propertyToBuilder.set(spec.property, builder); + } + + const values = new Array(dumpCount); + values[dumpIndex] = value; + + builder.addPath( + [processNamePath, component] /* path */, values, + tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL /* valueKind */); + }); + } + + function reportDataAsValues(sourceToPropertyToBuilder, browserName, + customComponentTreeModifier, values) { + // For each source name (e.g. 'reported_by_os')... + sourceToPropertyToBuilder.forEach(function(propertyToBuilder, sourceName) { + // For each property (e.g. EFFECTIVE_SIZE)... + propertyToBuilder.forEach(function(builders, property) { + const tree = builders.buildTopDownTreeView(); + reportComponentDataAsValues(browserName, sourceName, property, + [] /* processPath */, [] /* componentPath */, tree, values, + customComponentTreeModifier); + }); + }); + } + + /** + * For the given |browserName| (e.g. 'chrome'), |property| + * (e.g. EFFECTIVE_SIZE), |processPath| (e.g. ['browser_process']), + * |componentPath| (e.g. ['v8']), add + * a tr.v.Histogram with |unit| aggregating the total + * values of the associated |componentNode| across all timestamps + * (corresponding to global memory dumps associated with the given browser) + * |values| for each process (e.g. 'gpu_process', 'browser_process', etc). + * We also report a special 'all_processes' histogram which agregates all + * others, this has a RelatedNameMap diagnostic explaining + * how it is built from the other histograms. + * + * See addMemoryDumpValues for more details. + */ + function reportComponentDataAsValues(browserName, sourceName, property, + processPath, componentPath, tree, values, customComponentTreeModifier, + opt_cachedHistograms) { + const cachedHistograms = opt_cachedHistograms || new Map(); + function recurse(processPath, componentPath, node) { + return reportComponentDataAsValues(browserName, sourceName, property, + processPath, componentPath, node, values, + customComponentTreeModifier, cachedHistograms); + } + + function buildHistogram(processPath, componentPath, node) { + return buildNamedMemoryNumericFromNode( + browserName, + sourceName, + property, + processPath.length === 0 ? 'all_processes' : processPath[0], + componentPath, + node); + } + + customComponentTreeModifier(tree); + const histogram = buildHistogram(processPath, componentPath, tree); + if (cachedHistograms.has(histogram.name)) { + return cachedHistograms.get(histogram.name); + } + cachedHistograms.set(histogram.name, histogram); + + const processNames = new tr.v.d.RelatedNameMap(); + for (const [childProcessName, childProcessNode] of tree.children[0]) { + processPath.push(childProcessName); + const childProcessHistogram = + recurse(processPath, componentPath, childProcessNode); + processNames.set(childProcessName, childProcessHistogram.name); + processPath.pop(); + } + + const componentNames = new tr.v.d.RelatedNameMap(); + for (const [childComponentName, childComponentNode] of tree.children[1]) { + componentPath.push(childComponentName); + const childComponentHistogram = + recurse(processPath, componentPath, childComponentNode); + componentNames.set(childComponentName, childComponentHistogram.name); + componentPath.pop(); + } + + values.addHistogram(histogram); + if (tree.children[0].size > 0) { + histogram.diagnostics.set('processes', processNames); + } + if (tree.children[1].size > 0) { + histogram.diagnostics.set('components', componentNames); + } + + return histogram; + } + + /** + * Gets the name for a histogram. + * The histograms have the following naming scheme: + * memory:chrome:browser_process:reported_by_chrome:v8:heap:effective_size_avg + * ^browser ^process ^source ^component ^property + */ + function getNumericName( + browserName, sourceName, propertyName, processName, componentPath) { + // Construct the name of the memory value. + const nameParts = ['memory', browserName, processName, sourceName].concat( + componentPath); + if (propertyName !== undefined) nameParts.push(propertyName); + return nameParts.join(':'); + } + + /** + * Gets the description of a histogram. + */ + function getNumericDescription( + property, browserName, processName, componentPath) { + return [ + property.buildDescriptionPrefix(componentPath, processName), + 'in', + convertBrowserNameToUserFriendlyName(browserName) + ].join(' '); + } + + /** + * Create a memory tr.v.Histogram with |unit| and add all total values in + * |node| to it. Names and describes the histogram according to the + * |browserName|, |sourceName|, |property|, |processName| and + * |componentPath|. + */ + function buildNamedMemoryNumericFromNode( + browserName, sourceName, property, processName, componentPath, node) { + const name = getNumericName( + browserName, sourceName, property.name, processName, componentPath); + const description = getNumericDescription( + property, browserName, processName, componentPath); + + // Build the underlying numeric for the memory value. + const numeric = buildMemoryNumericFromNode(name, node, property.unit); + numeric.description = description; + return numeric; + } + + function buildSampleDiagnostics(value, node) { + if (node.children.length < 2) return undefined; + const diagnostics = new Map(); + const i = node.values.indexOf(value); + + const processBreakdown = new tr.v.d.Breakdown(); + processBreakdown.colorScheme = + tr.e.chrome.chrome_processes.PROCESS_COLOR_SCHEME_NAME; + for (const [name, subNode] of node.children[0]) { + processBreakdown.set(name, subNode.values[i].total); + } + if (processBreakdown.size > 0) { + diagnostics.set('processes', processBreakdown); + } + + const componentBreakdown = new tr.v.d.Breakdown(); + for (const [name, subNode] of node.children[1]) { + componentBreakdown.set(name, subNode.values[i].total); + } + if (componentBreakdown.size > 0) { + diagnostics.set('components', componentBreakdown); + } + + if (diagnostics.size === 0) return undefined; + return diagnostics; + } + + /** + * Create a memory tr.v.Histogram with |unit| and add all total values in + * |node| to it. + */ + function buildMemoryNumericFromNode(name, node, unit) { + const histogram = new tr.v.Histogram( + name, unit, BOUNDARIES_FOR_UNIT_MAP.get(unit)); + + node.values.forEach(v => histogram.addSample( + v.total, buildSampleDiagnostics(v, node))); + + return histogram; + } + + tr.metrics.MetricRegistry.register(memoryMetric, { + supportsRangeOfInterest: true + }); + + return { + memoryMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric_test.html new file mode 100644 index 00000000000..e3a6c40d327 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/memory_metric_test.html @@ -0,0 +1,4249 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/system_health/memory_metric.html"> +<link rel="import" href="/tracing/model/container_memory_dump.html"> +<link rel="import" href="/tracing/model/heap_dump.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> +<link rel="import" href="/tracing/model/vm_region.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const VMRegion = tr.model.VMRegion; + const VMRegionClassificationNode = tr.model.VMRegionClassificationNode; + const BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND; + const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT; + const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED; + const SIZE_DELTA = tr.model.MemoryDumpTestUtils.SIZE_DELTA; + const addProcessMemoryDump = + tr.model.MemoryDumpTestUtils.addProcessMemoryDump; + const addGlobalMemoryDump = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump; + const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump; + const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump; + const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink; + const count_smallerIsBetter = tr.b.Unit.byName.count_smallerIsBetter; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + const StackFrame = tr.model.StackFrame; + const HeapEntry = tr.model.HeapEntry; + const HeapDump = tr.model.HeapDump; + + function memoryMetricTest( + name, modelCallback, opt_options, expectedNumerics) { + test(name, function() { + // Create a model and a fake value list. + const model = tr.c.TestUtils.newModel(modelCallback); + const valueNameToValues = {}; + const fakeValueList = { + addHistogram(value) { + let values = valueNameToValues[value.name]; + if (values === undefined) { + valueNameToValues[value.name] = values = []; + } + values.push(value); + } + }; + + // Run the memory metric on the model. + tr.metrics.sh.memoryMetric(fakeValueList, model, opt_options); + + // Check that the names of the added values match expectations. + const actualValueNames = Object.keys(valueNameToValues).sort(); + const expectedValueNames = Object.keys(expectedNumerics).sort(); + assert.deepEqual(actualValueNames, expectedValueNames, { + // Build the long error message lazily. + toString() { + const errorMessageParts = []; + function addValueNamesToError(type, valueNames, otherValueNames) { + const otherValueNamesSet = new Set(otherValueNames); + errorMessageParts.push(type, ' value names:'); + if (valueNames.length === 0) { + errorMessageParts.push('\n(empty)'); + } else { + valueNames.forEach(function(valueName) { + errorMessageParts.push('\n'); + if (!otherValueNamesSet.has(valueName)) { + errorMessageParts.push('+++'); + } + errorMessageParts.push(valueName); + }); + } + } + addValueNamesToError('Expected', expectedValueNames, + actualValueNames); + errorMessageParts.push('\n'); + addValueNamesToError('Actual', actualValueNames, expectedValueNames); + return errorMessageParts.join(''); + } + }); + + // Check that the numeric values of the added values match expectations. + for (const [valueName, actualValues] of + Object.entries(valueNameToValues)) { + assert.lengthOf(actualValues, 1, + 'Multiple \'' + valueName + '\' values'); + const actualHistogram = actualValues[0]; + assert.instanceOf(actualHistogram, tr.v.Histogram); + + const expectedHistogram = expectedNumerics[valueName]; + assert.strictEqual(actualHistogram.unit, expectedHistogram.unit, + 'Invalid \'' + valueName + '\' unit (expected: ' + + expectedHistogram.unit.unitName, + ', actual: ' + + actualHistogram.unit.unitName + ')'); + + if (!(expectedHistogram.value instanceof Array)) { + assert.fail('Test sanity check: expected value must be an array'); + } + + assert.instanceOf(actualHistogram, tr.v.Histogram, + 'Invalid \'' + valueName + '\' class'); + assert.strictEqual(actualHistogram.numValues, + expectedHistogram.value.length, + 'Invalid \'' + valueName + '\' Histogram numValues'); + assert.closeTo(actualHistogram.sum, + expectedHistogram.value.reduce((a, b) => a + b, 0), SIZE_DELTA, + 'Invalid \'' + valueName + '\' Histogram sum'); + + // Check that the bin counts match. + const binToCount = new Map(); + expectedHistogram.value.forEach(function(value) { + const bin = actualHistogram.getBinForValue(value); + binToCount.set(bin, (binToCount.get(bin) || 0) + 1); + }); + actualHistogram.allBins.forEach(function(bin) { + binToCount.set(bin, (binToCount.get(bin) || 0) - bin.count); + }); + binToCount.forEach(function(count, bin) { + assert.strictEqual(count, 0, 'Invalid \'' + valueName + + '\' bin count for range ' + bin.min + '-' + bin.max); + }); + + // Check that the description matches expectations. + assert.strictEqual( + actualHistogram.description, expectedHistogram.description, + 'Invalid \'' + valueName + '\' description'); + } + }); + } + + function createProcessWithName(model, name) { + const uniquePid = + Math.max.apply(null, Object.keys(model.processes).concat([0])) + 1; + const process = model.getOrCreateProcess(uniquePid); + process.name = name; + return process; + } + + function createChromeBrowserProcess(model) { + const process = createProcessWithName(model, 'Browser'); + process.getOrCreateThread(1).name = 'CrBrowserMain'; + return process; + } + + function createWebViewProcess(model) { + const process = createChromeBrowserProcess(model); + process.getOrCreateThread(2).name = 'Chrome_InProcRendererThread'; + return process; + } + + memoryMetricTest('noDumps_noBrowser', function(model) { + createProcessWithName(model, 'Non-browser'); + }, undefined /* opt_options */, { + /* no values */ + }); + + memoryMetricTest('noDumps_chrome', function(model) { + createChromeBrowserProcess(model); + }, undefined /* opt_options */, { + 'memory:chrome:all_processes:dump_count:detailed': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:light': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by Chrome ' + + 'to the trace' + }, + 'memory:chrome:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'Chrome to the trace' + }, + 'memory:chrome:all_processes:dump_count': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by Chrome to the ' + + 'trace' + } + }); + + memoryMetricTest('noDumps_multipleBrowsers', function(model) { + createChromeBrowserProcess(model); + createWebViewProcess(model); + createProcessWithName(model, 'Non-browser'); + createChromeBrowserProcess(model); + }, undefined /* opt_options */, { + 'memory:chrome2:all_processes:dump_count:detailed': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by Chrome(2) ' + + 'to the trace' + }, + 'memory:chrome2:all_processes:dump_count:light': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by Chrome(2) to ' + + 'the trace' + }, + 'memory:chrome2:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by ' + + 'Chrome(2) to the trace' + }, + 'memory:chrome2:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'Chrome(2) to the trace' + }, + 'memory:chrome2:all_processes:dump_count': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by Chrome(2) to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:detailed': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:light': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by Chrome ' + + 'to the trace' + }, + 'memory:chrome:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'Chrome to the trace' + }, + 'memory:chrome:all_processes:dump_count': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by Chrome to the ' + + 'trace' + }, + 'memory:webview:all_processes:dump_count:detailed': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by WebView ' + + 'to the trace' + }, + 'memory:webview:all_processes:dump_count:light': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by WebView to ' + + 'the trace' + }, + 'memory:webview:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by WebView ' + + 'to the trace' + }, + 'memory:webview:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'WebView to the trace' + }, + 'memory:webview:all_processes:dump_count': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by WebView to the ' + + 'trace' + } + }); + + memoryMetricTest('dumpCountsOnly_unknownBrowser', function(model) { + addGlobalMemoryDump(model, {ts: 45, levelOfDetail: DETAILED}); + addGlobalMemoryDump(model, {ts: 65, levelOfDetail: BACKGROUND}); + addGlobalMemoryDump(model, {ts: 68, levelOfDetail: LIGHT}); + addGlobalMemoryDump(model, {ts: 89, levelOfDetail: DETAILED}); + }, undefined /* opt_options */, { + 'memory:unknown_browser:all_processes:dump_count:detailed': { + value: [2], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:light': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by an unknown ' + + 'browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:background': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count': { + value: [4], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by an unknown ' + + 'browser to the trace' + } + }); + + memoryMetricTest('dumpCountsOnly_webview', function(model) { + const p = createWebViewProcess(model); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 45, levelOfDetail: LIGHT}), p, {ts: 45}); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 68, levelOfDetail: LIGHT}), p, {ts: 68}); + }, undefined /* opt_options */, { + 'memory:webview:all_processes:dump_count:detailed': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by WebView ' + + 'to the trace' + }, + 'memory:webview:all_processes:dump_count:light': { + value: [2], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by WebView to ' + + 'the trace' + }, + 'memory:webview:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by WebView ' + + 'to the trace' + }, + 'memory:webview:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'WebView to the trace' + }, + 'memory:webview:all_processes:dump_count': { + value: [2], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by WebView to the ' + + 'trace' + }, + 'memory:webview:all_processes:process_count': { + value: [1, 1], + unit: count_smallerIsBetter, + description: 'total number of all processes in WebView' + }, + 'memory:webview:browser_process:process_count': { + value: [1, 1], + unit: count_smallerIsBetter, + description: 'total number of browser processes in WebView' + } + }); + + memoryMetricTest('generalValues_chrome', function(model) { + const pBrowser = createChromeBrowserProcess(model); + const pRendererA = createProcessWithName(model, 'Renderer'); + const pRendererB = createProcessWithName(model, 'Renderer'); + const pPpapi = createProcessWithName(model, 'PPAPI Process'); + const pUnknown = createProcessWithName(model, undefined); + + // Timestamp 1. + const gmd1 = addGlobalMemoryDump(model, {ts: 20}); + const pmdBrowser1 = addProcessMemoryDump(gmd1, pBrowser, {ts: 19}); + pmdBrowser1.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser1, 'malloc', {numerics: { + size: 8, + allocated_objects_size: 4, + shim_allocated_objects_size: 3, + }}) + ]; + pmdBrowser1.totals = { + residentBytes: 200, + peakResidentBytes: 230, + privateFootprintBytes: 240, + }; + const browserHeapDump = new HeapDump(pmdBrowser1); + const rootFrame1 = new StackFrame( + undefined, tr.b.GUID.allocateSimple(), undefined); + const childFrame1 = new StackFrame( + rootFrame1, tr.b.GUID.allocateSimple(), 'draw'); + rootFrame1.addChild(childFrame1); + browserHeapDump.addEntry( + undefined, 'HTMLImportLoader', 1024, undefined); + browserHeapDump.addEntry( + rootFrame1, 'HTMLImportLoader', 1048576, undefined); + browserHeapDump.addEntry(undefined, '[unknown]', 17332, 42); + browserHeapDump.addEntry(childFrame1, '[unknown]', 26309, 10); + pmdBrowser1.heapDumps = {}; + pmdBrowser1.heapDumps.malloc = browserHeapDump; + + const pmdRendererA1 = addProcessMemoryDump(gmd1, pRendererA, {ts: 20}); + pmdRendererA1.memoryAllocatorDumps = (function() { + const mallocDump = + newAllocatorDump(pmdRendererA1, 'malloc', {numerics: {size: 16}}); + const partitionAllocDump = + newAllocatorDump(pmdRendererA1, 'partition_alloc'); + const v8Dump = newAllocatorDump(pmdRendererA1, 'v8', + {numerics: {code_and_metadata_size: 16}}); + addOwnershipLink( + addChildDump(partitionAllocDump, 'allocated_objects', + {numerics: {size: 32}}), + addChildDump(partitionAllocDump, 'partitions', + {numerics: {size: 24}})); + return [mallocDump, partitionAllocDump, v8Dump]; + })(); + const pmdGpu1 = addProcessMemoryDump(gmd1, pPpapi, {ts: 21}); + pmdGpu1.memoryAllocatorDumps = [ + newAllocatorDump(pmdGpu1, 'gpu', {numerics: { + size: 30, + allocated_objects_size: 25 + }}) + ]; + + // Timestamp 2. + const gmd2 = addGlobalMemoryDump(model, {ts: 40}); + const pmdBrowser2 = addProcessMemoryDump(gmd2, pBrowser, {ts: 41}); + pmdBrowser2.memoryAllocatorDumps = (function() { + const mallocDump = newAllocatorDump(pmdBrowser2, 'malloc', + {numerics: {size: 120}}); + const tracingDump = + newAllocatorDump(pmdBrowser2, 'tracing', {numerics: {size: 40}}); + return [mallocDump, tracingDump]; + })(); + const pmdRendererA2 = addProcessMemoryDump(gmd2, pRendererA, {ts: 39}); + pmdRendererA2.memoryAllocatorDumps = (function() { + const partitionAllocDump = + newAllocatorDump(pmdRendererA2, 'partition_alloc'); + addOwnershipLink( + addChildDump(partitionAllocDump, 'allocated_objects', + {numerics: {size: 320}}), + addChildDump(partitionAllocDump, 'partitions', + {numerics: {size: 240}})); + const v8Dump = newAllocatorDump(pmdRendererA2, 'v8', + {numerics: {size: 650}}); + const isolateDumpA = addChildDump(v8Dump, 'isolate_A'); + addChildDump(isolateDumpA, 'malloc', {numerics: { + size: 1, + peak_size: 2 + }}); + const heapDumpA = addChildDump(isolateDumpA, 'heap_spaces', {numerics: { + size: 42, + allocated_objects_size: 36 + }}); + addChildDump(heapDumpA, 'code_space', {numerics: { + allocated_objects_size: 1, + size: 2 + }}); + addChildDump(heapDumpA, 'large_object_space', {numerics: { + allocated_objects_size: 3, + size: 4 + }}); + addChildDump(heapDumpA, 'map_space', {numerics: { + allocated_objects_size: 5, + size: 6, + }}); + addChildDump(heapDumpA, 'new_space', {numerics: { + allocated_objects_size: 7, + size: 8 + }}); + addChildDump(heapDumpA, 'old_space', {numerics: { + allocated_objects_size: 9, + size: 10 + }}); + addChildDump(heapDumpA, 'other_spaces', {numerics: { + allocated_objects_size: 11, + size: 12 + }}); + const isolateDumpB = addChildDump(v8Dump, 'isolate_B'); + addChildDump(isolateDumpB, 'malloc', {numerics: { + size: 10, + peak_size: 20 + }}); + const heapDumpB = addChildDump(isolateDumpB, 'heap_spaces', {numerics: { + size: 12, + allocated_objects_size: 6 + }}); + addChildDump(heapDumpB, 'code_space', {numerics: { + allocated_objects_size: 1, + size: 2 + }}); + addChildDump(heapDumpB, 'large_object_space', {numerics: { + allocated_objects_size: 1, + size: 2 + }}); + addChildDump(heapDumpB, 'map_space', {numerics: { + allocated_objects_size: 1, + size: 2, + }}); + addChildDump(heapDumpB, 'new_space', {numerics: { + allocated_objects_size: 1, + size: 2 + }}); + addChildDump(heapDumpB, 'old_space', {numerics: { + allocated_objects_size: 1, + size: 2 + }}); + addChildDump(heapDumpB, 'other_spaces', {numerics: { + allocated_objects_size: 1, + size: 2 + }}); + const isolateDumpC = addChildDump(v8Dump, 'isolate_C'); + addChildDump(isolateDumpC, 'malloc', {numerics: { + size: 100, + }}); + addChildDump(isolateDumpC, 'heap_spaces', {numerics: { + size: 2, + allocated_objects_size: 1 + }}); + const isolateDumpD = addChildDump(v8Dump, 'isolate_D'); + addChildDump(isolateDumpD, 'malloc', {numerics: { + peak_size: 200, + }}); + return [partitionAllocDump, v8Dump]; + })(); + const pmdRendererB2 = addProcessMemoryDump(gmd2, pRendererB, {ts: 40}); + pmdRendererB2.memoryAllocatorDumps = [ + newAllocatorDump(pmdRendererB2, 'v8', {numerics: { + size: 970, + allocated_objects_size: 860, + bytecode_and_metadata_size: 678 + }}), + newAllocatorDump(pmdRendererB2, 'malloc', + {numerics: {allocated_objects_size: 750}}) + ]; + const pmdUnknown = addProcessMemoryDump(gmd2, pUnknown, {ts: 42}); + pmdUnknown.memoryAllocatorDumps = [ + newAllocatorDump(pmdRendererB2, 'v8', {numerics: {size: 111}}) + ]; + + // Timestamp 3. + const gmd3 = addGlobalMemoryDump(model, {ts: 60}); + const pmdBrowser3 = addProcessMemoryDump(gmd3, pBrowser, {ts: 60}); + pmdBrowser3.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser3, 'malloc', {numerics: { + size: 8000, + allocated_objects_size: 4000, + shim_allocated_objects_size: 3000 + }}) + ]; + const pmdRendererB3 = addProcessMemoryDump(gmd3, pRendererB, {ts: 61}); + // Intentionally pmdRendererB3.memoryAllocatorDumps undefined. + const pmdGpu3 = addProcessMemoryDump(gmd3, pPpapi, {ts: 59}); + pmdGpu3.memoryAllocatorDumps = [ + newAllocatorDump(pmdGpu3, 'gpu', {numerics: {size: 300}}) + ]; + + // Timestamp 4. + const gmd4 = addGlobalMemoryDump(model, {ts: 80}); + const pmdBrowser4 = addProcessMemoryDump(gmd4, pBrowser, {ts: 81}); + pmdBrowser4.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser4, 'malloc', {numerics: {size: 80000}}) + ]; + const pmdRendererB4 = addProcessMemoryDump(gmd4, pRendererB, {ts: 79}); + pmdRendererB4.memoryAllocatorDumps = (function() { + const v8Dump = newAllocatorDump(pmdRendererB4, 'v8', {numerics: { + code_and_metadata_size: 21, + bytecode_and_metadata_size: 35, + size: 9e5 + }}); + const partitionAllocDump = newAllocatorDump(pmdRendererB4, + 'partition_alloc', {numerics: {size: 5e5}}); + addOwnershipLink(partitionAllocDump, v8Dump); + return [v8Dump, partitionAllocDump]; + })(); + const rendererBHeapDump4 = new HeapDump(pmdRendererB4); + rendererBHeapDump4.addEntry( + undefined, 'BlinkObject', 1687992, undefined); + rendererBHeapDump4.addEntry(undefined, undefined, 2243546, 42); + rendererBHeapDump4.addEntry(undefined, 'BlinkObject', 1252376, 10); + + pmdRendererB4.heapDumps = {}; + pmdRendererB4.heapDumps.blinkgc = rendererBHeapDump4; + + const pmdGpu4 = addProcessMemoryDump(gmd4, pPpapi, {ts: 80}); + pmdGpu4.memoryAllocatorDumps = [ + newAllocatorDump(pmdGpu4, 'gpu', + {numerics: {memtrack_pss: 666 /* ignored */}}) + ]; + }, undefined /* opt_options */, { + 'memory:chrome:all_processes:dump_count:detailed': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:light': { + value: [4], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by Chrome ' + + 'to the trace' + }, + 'memory:chrome:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'Chrome to the trace' + }, + 'memory:chrome:all_processes:dump_count': { + value: [4], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by Chrome to the ' + + 'trace' + }, + 'memory:chrome:all_processes:process_count': { + value: [3, 4, 3, 3], + unit: count_smallerIsBetter, + description: 'total number of all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:effective_size': { + value: [30 + (8 + 16) + 32, (120 - 40) + 320 + (650 + 970) + 111, + 300 + 8000, 80000 + 5e5 + 4e5], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:allocated_objects_size': { + value: [25 + 4 + 32, (36 + 6 + 1) + 750 + 860 + 320 + 40, 4000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of all allocated objects reported by Chrome ' + + 'for all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:blinkgc:BlinkObject:heap_category_size': + { + value: [0, 0, 1687992 + 1252376, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'heap profiler category size for blinkgc:BlinkObject in ' + + 'all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:blinkgc:heap_category_size': + { + value: [0, 0, 1687992 + 1252376, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'heap profiler category size for blinkgc in all processes ' + + 'in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:shim_allocated_objects_size': + { + value: [3, 0, 3000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of all allocated objects through shim ' + + 'reported by Chrome for all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:peak_size': + { + value: [0, 2 + 20 + 200, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total peak size reported by Chrome for all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:resident_size': { + value: [200, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total resident set size (RSS) reported by the OS for all' + + ' processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:peak_resident_size': { + value: [230, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total peak resident set size reported by the OS for all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:private_footprint_size': { + value: [240, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private footprint size reported by the OS for all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:resident_size': { + value: [200, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total resident set size (RSS) of system memory (RAM) ' + + 'used by all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:peak_resident_size': + { + value: [230, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total peak resident set size of system memory (RAM) ' + + 'used by all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:private_footprint_size': { // eslint-disable-line max-len + value: [240, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private footprint size of system memory (RAM) ' + + 'used by all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:gpu:effective_size': { + value: [30, 0, 300, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of gpu in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:heap_category_size': { + value: [17332, 0, 1687992 + 1252376, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total heap profiler category size reported by Chrome for ' + + 'all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:malloc:[unknown]:heap_category_size': + { + value: [17332, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'heap profiler category size for malloc:[unknown] in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:gpu:allocated_objects_size': + { + value: [25, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by gpu in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:malloc:effective_size': { + value: [8 + 16, 120 - 40, 8000, 80000], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:malloc:heap_category_size': + { + value: [17332, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'heap profiler category size for malloc in all processes ' + + 'in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:malloc:shim_allocated_objects_size': + { + value: [3, 0, 3000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated through shim by malloc in ' + + 'all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:malloc:allocated_objects_size': + { + value: [4, 40 + 750, 4000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by malloc in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:partition_alloc:allocated_objects_size': + { + value: [32, 320, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by partition_alloc in ' + + 'all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:partition_alloc:effective_size': + { + value: [32, 320, 0, 5e5], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of partition_alloc in all processes ' + + 'in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:tracing:effective_size': { + value: [0, 40, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of tracing in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:allocated_by_malloc:effective_size': + { + value: [0, 1 + 10 + 100, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of objects allocated by malloc for v8 ' + + 'in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:allocated_by_malloc:peak_size': + { + value: [0, 2 + 20 + 200, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'peak size of objects allocated by malloc for v8 ' + + 'in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:code_and_metadata_size': + { + value: [16, 678, 0, 21 + 35], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of code and metadata reported by Chrome ' + + 'for all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:code_and_metadata_size': + { + value: [16, 678, 0, 21 + 35], + unit: sizeInBytes_smallerIsBetter, + description: 'size of v8 code and metadata in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:allocated_objects_size': + { + value: [0, 36 + 6 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:effective_size': { + value: [0, 42 + 12 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:code_space:allocated_objects_size': + { + value: [0, 1 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap:code_space ' + + 'in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:code_space:effective_size': + { + value: [0, 2 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:code_space in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:large_object_space:allocated_objects_size': + { + value: [0, 3 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by ' + + 'v8:heap:large_object_space in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:large_object_space:effective_size': + { + value: [0, 4 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:large_object_space in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:map_space:allocated_objects_size': + { + value: [0, 5 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap:map_space ' + + 'in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:map_space:effective_size': + { + value: [0, 6 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:map_space in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:new_space:allocated_objects_size': + { + value: [0, 7 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap:new_space ' + + 'in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:new_space:effective_size': + { + value: [0, 8 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:new_space in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:old_space:allocated_objects_size': + { + value: [0, 9 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap:old_space ' + + 'in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:heap:old_space:effective_size': + { + value: [0, 10 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:old_space in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:effective_size': { + value: [0, 650 + 970 + 111, 0, 4e5], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8 in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:allocated_objects_size': + { + value: [0, (36 + 6 + 1) + 860, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8 in all processes ' + + 'in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:v8:peak_size': + { + value: [0, 2 + 20 + 200, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'peak size of v8 in all processes in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:peak_resident_size': { + value: [230, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total peak resident set size reported by the OS for the ' + + 'browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:peak_resident_size': + { + value: [230, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total peak resident set size of system memory (RAM) ' + + 'used by the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:private_footprint_size': { + value: [240, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private footprint size reported by the OS for the ' + + 'browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:private_footprint_size': + { + value: [240, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private footprint size of system memory (RAM) ' + + 'used by the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:resident_size': { + value: [200, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total resident set size (RSS) reported by the OS for the ' + + 'browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:resident_size': + { + value: [200, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total resident set size (RSS) of system memory (RAM) ' + + 'used by the browser process in Chrome' + }, + 'memory:chrome:browser_process:process_count': { + value: [1, 1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of browser processes in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:effective_size': { + value: [8, (120 - 40), 8000, 80000], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for the browser ' + + 'process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:heap_category_size': { + value: [17332, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total heap profiler category size reported by Chrome for ' + + 'the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:malloc:[unknown]:heap_category_size': + { + value: [17332, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'heap profiler category size for malloc:[unknown] in the ' + + 'browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:allocated_objects_size': { + value: [4 + 40, 0, 4000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of all allocated objects reported by Chrome ' + + 'for the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:shim_allocated_objects_size': + { + value: [3, 0, 3000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of all allocated objects through shim ' + + 'reported by Chrome for the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:malloc:effective_size': { + value: [8, 120 - 40, 8000, 80000], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:malloc:heap_category_size': + { + value: [17332, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'heap profiler category size for malloc in the browser ' + + 'process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:malloc:allocated_objects_size': + { + value: [4 + 40, 0, 4000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by malloc in the ' + + 'browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:malloc:shim_allocated_objects_size': + { + value: [3, 0, 3000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated through shim by malloc in ' + + 'the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:tracing:effective_size': { + value: [0, 40, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of tracing in the browser process in Chrome' + }, + 'memory:chrome:ppapi_process:process_count': { + value: [1, 0, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of PPAPI processes in Chrome' + }, + 'memory:chrome:ppapi_process:reported_by_chrome:effective_size': { + value: [30, 0, 300, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for the PPAPI ' + + 'process in Chrome' + }, + 'memory:chrome:ppapi_process:reported_by_chrome:allocated_objects_size': { + value: [25, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of all allocated objects reported by Chrome ' + + 'for the PPAPI process in Chrome' + }, + 'memory:chrome:ppapi_process:reported_by_chrome:gpu:effective_size': { + value: [30, 0, 300, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of gpu in the PPAPI process in Chrome' + }, + 'memory:chrome:ppapi_process:reported_by_chrome:gpu:allocated_objects_size': + { + value: [25, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by gpu in the PPAPI ' + + 'process in Chrome' + }, + 'memory:chrome:renderer_processes:process_count': { + value: [1, 2, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:effective_size': { + value: [16 + 32, 320 + 650 + 970, 0, 5e5 + 4e5], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:allocated_objects_size': + { + value: [32, (36 + 6 + 1) + 750 + 860 + 320, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of all allocated objects reported by ' + + 'Chrome for renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:blinkgc:BlinkObject:heap_category_size': + { + value: [0, 0, 1687992 + 1252376, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'heap profiler category size for blinkgc:BlinkObject in ' + + 'renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:blinkgc:heap_category_size': + { + value: [0, 0, 1687992 + 1252376, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'heap profiler category size for blinkgc in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:peak_size': + { + value: [0, 2 + 20 + 200, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total peak size reported by Chrome ' + + 'for renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:malloc:effective_size': + { + value: [16, 0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in renderer processes in ' + + 'Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:heap_category_size': { + value: [0, 0, 1687992 + 1252376, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total heap profiler category size reported by Chrome for ' + + 'renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:malloc:allocated_objects_size': + { + value: [0, 750, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by malloc in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:partition_alloc:allocated_objects_size': + { + value: [32, 320, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by partition_alloc in ' + + 'renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:partition_alloc:effective_size': + { + value: [32, 320, 0, 5e5], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of partition_alloc in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:allocated_by_malloc:effective_size': + { + value: [0, 1 + 10 + 100, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of objects allocated by malloc for v8 ' + + 'in renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:allocated_by_malloc:peak_size': + { + value: [0, 2 + 20 + 200, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'peak size of objects allocated by malloc for v8 ' + + 'in renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:allocated_objects_size': + { + value: [0, 36 + 6 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:code_and_metadata_size': + { + value: [16, 678, 0, 21 + 35], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of code and metadata reported by Chrome ' + + 'for renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:code_and_metadata_size': + { + value: [16, 678, 0, 21 + 35], + unit: sizeInBytes_smallerIsBetter, + description: 'size of v8 code and metadata in renderer processes ' + + 'in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:effective_size': + { + value: [0, 42 + 12 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap in renderer processes ' + + 'in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:code_space:allocated_objects_size': + { + value: [0, 1 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap:code_space ' + + 'in renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:code_space:effective_size': + { + value: [0, 2 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:code_space in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:large_object_space:allocated_objects_size': + { + value: [0, 3 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by ' + + 'v8:heap:large_object_space in renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:large_object_space:effective_size': + { + value: [0, 4 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:large_object_space in ' + + 'renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:map_space:allocated_objects_size': + { + value: [0, 5 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap:map_space ' + + 'in renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:map_space:effective_size': + { + value: [0, 6 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:map_space in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:new_space:allocated_objects_size': + { + value: [0, 7 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap:new_space ' + + 'in renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:new_space:effective_size': + { + value: [0, 8 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:new_space in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:old_space:allocated_objects_size': + { + value: [0, 9 + 1, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap:old_space ' + + 'in renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:heap:old_space:effective_size': + { + value: [0, 10 + 2, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:old_space in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:effective_size': { + value: [0, 650 + 970, 0, 4e5], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8 in renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:allocated_objects_size': + { + value: [0, (36 + 6 + 1) + 860, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8 in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:v8:peak_size': + { + value: [0, 2 + 20 + 200, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'peak size of v8 in renderer processes in Chrome' + }, + 'memory:chrome:unknown_processes:process_count': { + value: [0, 1, 0, 0], + unit: count_smallerIsBetter, + description: 'total number of unknown processes in Chrome' + }, + 'memory:chrome:unknown_processes:reported_by_chrome:effective_size': { + value: [0, 111, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for unknown ' + + 'processes in Chrome' + }, + 'memory:chrome:unknown_processes:reported_by_chrome:v8:effective_size': { + value: [0, 111, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8 in unknown processes in Chrome' + }, + }); + + + memoryMetricTest('newV8Values', function(model) { + const pRendererA = createProcessWithName(model, 'Renderer'); + const gmd1 = addGlobalMemoryDump(model, {ts: 20}); + const pmdRendererA = addProcessMemoryDump(gmd1, pRendererA, {ts: 20}); + pmdRendererA.memoryAllocatorDumps = (function() { + const v8Dump = newAllocatorDump(pmdRendererA, 'v8', + {numerics: {size: 0}}); + const isolateDumpA = addChildDump(v8Dump, 'main'); + const heapDumpA = addChildDump(isolateDumpA, 'heap'); + addChildDump(heapDumpA, 'code_space', {numerics: { + allocated_objects_size: 10, + size: 20 + }}); + const workersDump = addChildDump(v8Dump, 'workers'); + const heapDumpB = addChildDump(workersDump, 'heap'); + const codeSpaceDumpB = addChildDump(heapDumpB, 'code_space'); + addChildDump(codeSpaceDumpB, 'isolate_0x1234', {numerics: { + allocated_objects_size: 1, + size: 2 + }}); + addChildDump(codeSpaceDumpB, 'isolate_0x5678', {numerics: { + allocated_objects_size: 3, + size: 4 + }}); + return [v8Dump]; + })(); + }, undefined /* opt_options */, { + 'memory:unknown_browser:all_processes:dump_count:detailed': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:light': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'an unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by an unknown ' + + 'browser to the trace' + }, + 'memory:unknown_browser:all_processes:process_count': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:effective_size': { + value: [26], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for all ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:allocated_objects_size': + { + value: [14], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of all allocated objects reported by Chrome ' + + 'for all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:v8:heap:allocated_objects_size': + { + value: [14], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap in all ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:v8:heap:effective_size': + { + value: [26], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap in all processes in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:v8:heap:code_space:allocated_objects_size': + { + value: [14], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap:code_space ' + + 'in all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:v8:heap:code_space:effective_size': + { + value: [26], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:code_space in all ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:v8:effective_size': + { + value: [26], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8 in all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:v8:allocated_objects_size': + { + value: [14], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8 in all processes ' + + 'in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:process_count': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_chrome:effective_size': + { + value: [26], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for renderer ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_chrome:allocated_objects_size': + { + value: [14], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of all allocated objects reported by Chrome ' + + 'for renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:heap:allocated_objects_size': + { + value: [14], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap in renderer ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:heap:effective_size': + { + value: [26], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap in renderer processes in ' + + 'an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:heap:code_space:allocated_objects_size': + { + value: [14], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8:heap:code_space ' + + 'in renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:heap:code_space:effective_size': + { + value: [26], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8:heap:code_space in renderer ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:effective_size': + { + value: [26], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8 in renderer processes in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_chrome:v8:allocated_objects_size': + { + value: [14], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8 in renderer ' + + 'processes in an unknown browser' + } + }); + + + memoryMetricTest('detailedValues_unknownBrowser', function(model) { + const pBrowser = createProcessWithName(model, 'Browser'); + const pRendererA = createProcessWithName(model, 'Renderer'); + const pRendererB = createProcessWithName(model, 'Renderer'); + const pRendererC = createProcessWithName(model, 'Renderer'); + const pGpu = createProcessWithName(model, 'GPU Process'); + + // Timestamp 1. + const gmd1 = addGlobalMemoryDump(model, {ts: 10, levelOfDetail: DETAILED}); + const pmdBrowser1 = addProcessMemoryDump(gmd1, pBrowser, {ts: 9}); + pmdBrowser1.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xABCD, 128, 0, '/dev/ashmem/dalvik-non moving space', + {privateDirtyResident: 8}), + new VMRegion(0xFBCD, 64, 5, '/data/chrome-WXYZ/base.apk', + { privateCleanResident: 8, sharedCleanResident: 4, + proportionalResident: 10}), + new VMRegion(0xFCCD, 64, 5, '/data/chrome-WXYZ/out/arm/base.odex', + { privateCleanResident: 0, sharedCleanResident: 6, + proportionalResident: 5}) + ]); + pmdBrowser1.heapDumps = (function() { + const mallocDump = new tr.model.HeapDump(pmdBrowser1, 'malloc'); + mallocDump.addEntry(undefined, undefined, 100, 500); + return {'malloc': mallocDump}; + })(); + + const pmdRendererA1 = addProcessMemoryDump(gmd1, pRendererA, {ts: 10}); + pmdRendererA1.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xEF01, 256, 0, '[anon:libc_malloc]', + {privateDirtyResident: 17}), + new VMRegion(0xFBCD, 64, 5, '/data/chrome-WXYZ/base.apk', + { privateCleanResident: 3, sharedCleanResident: 4, + proportionalResident: 5}) + ]); + const pmdRendererB1 = addProcessMemoryDump(gmd1, pRendererB, {ts: 11}); + pmdRendererB1.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0x2345, 512, 0, '[heap]', + {proportionalResident: 67, privateDirtyResident: 34}), + new VMRegion(0x7f29, 128, 0, '[stack:25451]', + {proportionalResident: 20, privateDirtyResident: 16}) + ]); + const pmdGpu1 = addProcessMemoryDump(gmd1, pGpu, {ts: 10}); + pmdGpu1.memoryAllocatorDumps = (function() { + const gpuDump = newAllocatorDump(pmdGpu1, 'gpu'); + const memtrackDump = addChildDump(gpuDump, 'android_memtrack'); + addChildDump(memtrackDump, 'gl', {numerics: {memtrack_pss: 100}}); + addChildDump(memtrackDump, 'graphics', {numerics: {memtrack_pss: 200}}); + return [gpuDump]; + })(); + + // Timestamp 2 (light global memory dump, so it should be skipped for + // mmaps_* values). + const gmd2 = addGlobalMemoryDump(model, {ts: 20, levelOfDetail: LIGHT}); + const pmdBrowser2 = addProcessMemoryDump(gmd2, pBrowser, {ts: 18}); + pmdBrowser2.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0x999, 999, 999, '/dev/ashmem/dalvik-main space', + {proportionalResident: 999}) + ]); + const pmdRendererB2 = addProcessMemoryDump(gmd2, pRendererB, {ts: 21}); + const pmdRendererC2 = addProcessMemoryDump(gmd2, pRendererC, {ts: 22}); + const pmdGpu2 = addProcessMemoryDump(gmd2, pGpu, {ts: 20}); + pmdGpu2.memoryAllocatorDumps = (function() { + const gpuDump = newAllocatorDump(pmdGpu2, 'gpu'); + const memtrackDump = addChildDump(gpuDump, 'android_memtrack'); + addChildDump(memtrackDump, 'gl', {numerics: {memtrack_pss: 12345}}); + return [gpuDump]; + })(); + + // Timestamp 3. + const gmd3 = addGlobalMemoryDump(model, {ts: 30, levelOfDetail: DETAILED}); + const pmdBrowser3 = addProcessMemoryDump(gmd3, pBrowser, {ts: 30}); + pmdBrowser3.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xABCD, 1024, 0, '/dev/ashmem/dalvik-non moving space', + {proportionalResident: 3, privateDirtyResident: 80}) + ]); + const pmdRendererA3 = addProcessMemoryDump(gmd3, pRendererA, {ts: 29}); + // Intentionally pmdRendererA3.vmRegions undefined. + const pmdRendererC3 = addProcessMemoryDump(gmd3, pRendererC, {ts: 31}); + pmdRendererC3.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0x2345, 2048, 0, '/no/matching/category', + {proportionalResident: 200}), + new VMRegion(0x2345, 2048, 0, '/dev/ashmem', {proportionalResident: 500}), + ]); + const pmdGpu3 = addProcessMemoryDump(gmd3, pGpu, {ts: 30}); + pmdGpu3.memoryAllocatorDumps = (function() { + const gpuDump = newAllocatorDump(pmdGpu3, 'gpu', + {numerics: {memtrack_pss: 6000 /* ignored */}}); + const memtrackDump = addChildDump(gpuDump, 'android_memtrack', + {numerics: {memtrack_pss: 5000 /* ignored */}}); + addChildDump(memtrackDump, 'gl', {numerics: {memtrack_pss: 3000}}); + addChildDump(memtrackDump, 'graphics', {numerics: {ignored: 2000}}); + addChildDump(memtrackDump, 'gfx', {numerics: {memtrack_pss: 1000}}); + return [gpuDump]; + })(); + pmdGpu3.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xCDCD, 4096, 0, '/dev/ashmem/dalvik-zygote space', + {proportionalResident: 150, privateDirtyResident: 90}) + ]); + + // Timestamp 4. + const gmd4 = addGlobalMemoryDump(model, {ts: 40, levelOfDetail: DETAILED}); + const pmdBrowser4 = addProcessMemoryDump(gmd4, pBrowser, {ts: 40}); + }, undefined /* opt_options */, { + 'memory:unknown_browser:all_processes:dump_count:detailed': { + value: [3], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:light': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by an unknown ' + + 'browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:heap_profiler': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count': { + value: [4], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by an unknown ' + + 'browser to the trace' + }, + 'memory:unknown_browser:all_processes:process_count': { + value: [4, 4, 4, 1], + unit: count_smallerIsBetter, + description: 'total number of all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:gpu_memory:proportional_resident_size': + { + value: [100 + 200, 3000 + 1000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of GPU memory ' + + '(Android memtrack) used by all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:java_base_clean_resident': + { + value: [6, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total java base odex and vdex total clean resident size ' + + 'reported by the OS for all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:java_base_pss': + { + value: [5, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total java base odex and vdex proportional resident size ' + + 'reported by the OS for all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:native_library_private_clean_resident': + { + value: [8, 3, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library private clean resident size ' + + 'reported by the OS for all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:native_library_proportional_resident': + { + value: [10, 5, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library proportional resident size ' + + 'reported by the OS for all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:native_library_shared_clean_resident': + { + value: [4, 4, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library shared clean resident size ' + + 'reported by the OS for all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:gpu_memory:gfx:proportional_resident_size': + { + value: [0, 1000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the gfx Android ' + + 'memtrack component in all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:gpu_memory:gl:proportional_resident_size': + { + value: [100, 3000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the gl Android ' + + 'memtrack component in all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:gpu_memory:graphics:proportional_resident_size': + { + value: [200, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the graphics ' + + 'Android memtrack component in all processes in an unknown ' + + 'browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:private_dirty_size': { + value: [17 + 34 + 16 + 8, 80 + 90, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for all ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:proportional_resident_size': + { + value: [67 + 20 + 100 + 200, 700 + 3 + 1000 + 150 + 3000, 15 + 5], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by ' + + 'the OS for all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:private_dirty_size': + { + value: [17 + 34 + 16 + 8, 80 + 90, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:proportional_resident_size': + { + value: [67 + 20, 700 + 3 + 150, 15 + 5], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in all processes in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 500, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in all ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:java_base_clean_resident': + { + value: [6, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total java base odex and vdex total clean resident size ' + + 'of system memory (RAM) used by all processes in an unknown ' + + 'browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:java_base_pss': + { + value: [5, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total java base odex and vdex proportional resident size ' + + 'of system memory (RAM) used by all processes in an unknown ' + + 'browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [8, 80 + 90, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in all processes ' + + 'in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 3 + 150, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [17 + 34, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in all ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [67, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:native_library_private_clean_resident': + { + value: [8, 3, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library private clean resident size of ' + + 'system memory (RAM) used by all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:native_library_proportional_resident': + { + value: [10, 5, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library proportional resident size of ' + + 'system memory (RAM) used by all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:native_library_shared_clean_resident': + { + value: [4, 4, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library shared clean resident size of ' + + 'system memory (RAM) used by all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [16, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in all ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [20, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in all processes in an unknown browser' + }, + 'memory:unknown_browser:browser_process:process_count': { + value: [1, 1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of browser processes in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:java_base_clean_resident': + { + value: [6, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total java base odex and vdex total clean resident size ' + + 'reported by the OS for the browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:java_base_pss': + { + value: [5, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total java base odex and vdex proportional resident size ' + + 'reported by the OS for the browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:native_library_private_clean_resident': + { + value: [8, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library private clean resident size ' + + 'reported by the OS for the browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:native_library_proportional_resident': + { + value: [10, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library proportional resident size ' + + 'reported by the OS for the browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:native_library_shared_clean_resident': + { + value: [4, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library shared clean resident size ' + + 'reported by the OS for the browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:private_dirty_size': + { + value: [8, 80, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for the ' + + 'browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:proportional_resident_size': + { + value: [15, 3, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by ' + + 'the OS for the browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:private_dirty_size': + { + value: [8, 80, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by the browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:proportional_resident_size': + { + value: [13, 5, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by the browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in the browser process ' + + 'in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in the ' + + 'browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:java_base_clean_resident': + { + value: [6, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total java base odex and vdex total clean resident size ' + + 'of system memory (RAM) used by the browser process in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:java_base_pss': + { + value: [5, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total java base odex and vdex proportional resident size ' + + 'of system memory (RAM) used by the browser process in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [8, 80, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in the browser ' + + 'process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 3, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'the browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in the browser ' + + 'process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in the browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:native_library_private_clean_resident': + { + value: [8, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library private clean resident size of ' + + 'system memory (RAM) used by the browser process in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:native_library_proportional_resident': + { + value: [10, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library proportional resident size of ' + + 'system memory (RAM) used by the browser process in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:native_library_shared_clean_resident': + { + value: [4, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library shared clean resident size of ' + + 'system memory (RAM) used by the browser process in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in the browser ' + + 'process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in the browser process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:process_count': { + value: [1, 1, 1, 0], + unit: count_smallerIsBetter, + description: 'total number of GPU processes in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:proportional_resident_size': + { + value: [100 + 200, 3000 + 1000 + 150, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by ' + + 'the OS for the GPU process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:private_dirty_size': { + value: [0, 90, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for the GPU ' + + 'process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:gpu_memory:proportional_resident_size': + { + value: [100 + 200, 3000 + 1000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of GPU memory ' + + '(Android memtrack) used by the GPU process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:gpu_memory:gfx:proportional_resident_size': + { + value: [0, 1000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the gfx Android ' + + 'memtrack component in the GPU process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:gpu_memory:gl:proportional_resident_size': + { + value: [100, 3000, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the gl Android ' + + 'memtrack component in the GPU process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:gpu_memory:graphics:proportional_resident_size': + { + value: [200, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the graphics ' + + 'Android memtrack component in the GPU process in an unknown ' + + 'browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:private_dirty_size': + { + value: [0, 90, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by the GPU process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:proportional_resident_size': + { + value: [0, 150, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by the GPU process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in the GPU process in ' + + 'an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in the ' + + 'GPU process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [0, 90, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in the GPU ' + + 'process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 150, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'the GPU process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in the GPU ' + + 'process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in the GPU process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in the GPU ' + + 'process in an unknown browser' + }, + 'memory:unknown_browser:gpu_process:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in the GPU process in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:process_count': { + value: [2, 2, 2, 0], + unit: count_smallerIsBetter, + description: 'total number of renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:native_library_private_clean_resident': + { + value: [3, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library private clean resident size ' + + 'reported by the OS for renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:native_library_proportional_resident': + { + value: [5, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library proportional resident size ' + + 'reported by the OS for renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:native_library_shared_clean_resident': + { + value: [4, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library shared clean resident size ' + + 'reported by the OS for renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:private_dirty_size': + { + value: [17 + 34 + 16, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for ' + + 'renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:proportional_resident_size': + { + value: [67 + 20, 700, 5], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by ' + + 'the OS for renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:private_dirty_size': + { + value: [17 + 34 + 16, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:proportional_resident_size': + { + value: [67 + 20, 700, 5], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in renderer processes ' + + 'in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 500, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in ' + + 'renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in renderer ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [17 + 34, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in renderer ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [67, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in renderer processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:native_library_private_clean_resident': + { + value: [3, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library private clean resident size of ' + + 'system memory (RAM) used by renderer processes in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:native_library_proportional_resident': + { + value: [5, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library proportional resident size of ' + + 'system memory (RAM) used by renderer processes in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:native_library_shared_clean_resident': + { + value: [4, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total native library shared clean resident size of ' + + 'system memory (RAM) used by renderer processes in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [16, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in renderer ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:renderer_processes:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [20, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in renderer processes in an unknown browser' + } + }); + + memoryMetricTest('combined_chrome', function(model) { + const pBrowser = createChromeBrowserProcess(model); + + // Timestamp 1. + const gmd1 = addGlobalMemoryDump(model, {ts: 10, levelOfDetail: DETAILED}); + const pmdBrowser1 = addProcessMemoryDump(gmd1, pBrowser, {ts: 10}); + pmdBrowser1.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xABCD, 128, 0, '/dev/ashmem/dalvik-non moving space', + {privateDirtyResident: 100}) + ]); + + // Timestamp 2 (light global memory dump, so it should be skipped for + // mmaps_* values). + const gmd2 = addGlobalMemoryDump(model, {ts: 20, levelOfDetail: LIGHT}); + const pmdBrowser2 = addProcessMemoryDump(gmd2, pBrowser, {ts: 20}); + pmdBrowser2.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser2, 'malloc', {numerics: {size: 32}}) + ]; + + // Timestamp 3. + const gmd3 = addGlobalMemoryDump(model, {ts: 30, levelOfDetail: DETAILED}); + const pmdBrowser3 = addProcessMemoryDump(gmd3, pBrowser, {ts: 30}); + pmdBrowser3.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser3, 'malloc', {numerics: {size: 48}}) + ]; + pmdBrowser3.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xABCD, 1024, 0, '/dev/ashmem/dalvik-non moving space', + {privateDirtyResident: 150}) + ]); + }, {} /* opt_options */, { + 'memory:chrome:all_processes:dump_count:detailed': { + value: [2], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:light': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by Chrome ' + + 'to the trace' + }, + 'memory:chrome:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'Chrome to the trace' + }, + 'memory:chrome:all_processes:dump_count': { + value: [3], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by Chrome to the ' + + 'trace' + }, + 'memory:chrome:all_processes:process_count': { + value: [1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:effective_size': { + value: [0, 32, 48], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:malloc:effective_size': { + value: [0, 32, 48], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:private_dirty_size': { + value: [100, 150], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:proportional_resident_size': { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by the ' + + 'OS for all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [100, 150], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in all processes ' + + 'in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:private_dirty_size': + { + value: [100, 150], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by all processes in Chrome' + }, + 'memory:chrome:browser_process:process_count': { + value: [1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of browser processes in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:effective_size': { + value: [0, 32, 48], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for the browser ' + + 'process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:malloc:effective_size': { + value: [0, 32, 48], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:private_dirty_size': { + value: [100, 150], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for the ' + + 'browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:proportional_resident_size': { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by the ' + + 'OS for the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in the browser process ' + + 'in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in the ' + + 'browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [100, 150], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in the browser ' + + 'process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in the browser ' + + 'process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in the browser ' + + 'process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:private_dirty_size': + { + value: [100, 150], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by the browser process in Chrome' + } + }); + + memoryMetricTest('combined_multipleBrowsers', function(model) { + const pWebView = createWebViewProcess(model); + const pChrome1 = createChromeBrowserProcess(model); + const pRenderer1 = createProcessWithName(model, 'Renderer'); + const pGpu1 = createProcessWithName(model, 'GPU Process'); + const pUnknownBrowser = createProcessWithName(model, 'Browser'); + const pChrome2 = createChromeBrowserProcess(model); + const pRenderer2 = createProcessWithName(model, 'Renderer'); + + // Timestamp 1 (WebView). + const gmd1 = addGlobalMemoryDump(model, {ts: 0, levelOfDetail: LIGHT}); + const pmdBrowser1 = addProcessMemoryDump(gmd1, pWebView, {ts: 0}); + pmdBrowser1.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser1, 'malloc', {numerics: {size: 2}}), + newAllocatorDump(pmdBrowser1, 'v8', {numerics: {size: 4}}) + ]; + + // Timestamp 2 (Chrome 1 + Renderer + GPU Process). + const gmd2 = addGlobalMemoryDump(model, {ts: 10, levelOfDetail: DETAILED}); + const pmdBrowser2 = addProcessMemoryDump(gmd2, pChrome1, {ts: 12}); + pmdBrowser2.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xABCD, 9999, 0, '/dev/ashmem/dalvik-non moving space', + {privateDirtyResident: 8}) + ]); + const pmdGpu2 = addProcessMemoryDump(gmd2, pGpu1, {ts: 8}); + pmdGpu2.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xABCD, 9999, 0, '/dev/ashmem/dalvik-non moving space', + {privateDirtyResident: 16}) + ]); + const pmdRenderer2 = addProcessMemoryDump(gmd2, pRenderer1, {ts: 8}); + pmdRenderer2.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser2, 'malloc', {numerics: {size: 32}}) + ]; + + // Timestamp 3 (Chrome 2). + const gmd3 = addGlobalMemoryDump(model, {ts: 20, levelOfDetail: DETAILED}); + const pmdBrowser3 = addProcessMemoryDump(gmd3, pChrome2, {ts: 20}); + pmdBrowser3.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser3, 'malloc', {numerics: {size: 64}}), + newAllocatorDump(pmdBrowser3, 'sqlite', {numerics: {size: 128}}), + newAllocatorDump(pmdBrowser3, 'discardable', {numerics: { + size: 8388608, + locked_size: 4194304 + }}) + ]; + pmdBrowser3.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xABCD, 99, 0, '/dev/ashmem/dalvik-non moving space', + {privateDirtyResident: 256}) + ]); + + // Timestamp 4 (Chrome 2 + Renderer). + const gmd4 = addGlobalMemoryDump(model, {ts: 30, levelOfDetail: LIGHT}); + const pmdBrowser4 = addProcessMemoryDump(gmd4, pChrome2, {ts: 28}); + pmdBrowser4.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser4, 'malloc', {numerics: {size: 512}}), + newAllocatorDump(pmdBrowser3, 'discardable', {numerics: {size: 16777216}}) + ]; + const pmdRenderer4 = addProcessMemoryDump(gmd4, pRenderer2, {ts: 32}); + pmdRenderer4.memoryAllocatorDumps = [ + newAllocatorDump(pmdRenderer4, 'malloc', {numerics: {size: 1024}}), + newAllocatorDump(pmdRenderer4, 'v8', {numerics: {size: 2048}}) + ]; + + // Timestamp 5 (Unknown browser). + const gmd5 = addGlobalMemoryDump(model, {ts: 40, levelOfDetail: LIGHT}); + const pmdBrowser5 = addProcessMemoryDump(gmd5, pUnknownBrowser, {ts: 40}); + pmdBrowser5.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser5, 'malloc', {numerics: {size: 4096}}), + newAllocatorDump(pmdBrowser5, 'sqlite', {numerics: {size: 8192}}), + ]; + + // Timestamp 6 (WebView). + const gmd6 = addGlobalMemoryDump(model, {ts: 50, levelOfDetail: DETAILED}); + const pmdBrowser6 = addProcessMemoryDump(gmd6, pWebView, {ts: 50}); + pmdBrowser6.memoryAllocatorDumps = (function() { + const mallocDump = newAllocatorDump(pmdBrowser6, 'malloc', + {numerics: {size: 16384}}); + const v8Dump = newAllocatorDump(pmdBrowser6, 'v8', {numerics: { + allocated_objects_size: 32768, + code_and_metadata_size: 33554432, + size: 67108864 + }}); + const isolateDump = addChildDump(v8Dump, 'isolateA'); + addChildDump(isolateDump, 'malloc', {numerics: { + size: 1, + peak_size: 2 + }}); + return [mallocDump, v8Dump]; + })(); + pmdBrowser6.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xABCD, 99999, 0, '/dev/ashmem/dalvik-non moving space', + {privateDirtyResident: 65536}) + ]); + + // Timestamp 7 (Chrome 1 + GPU Process). + const gmd7 = addGlobalMemoryDump(model, {ts: 60, levelOfDetail: DETAILED}); + const pmdBrowser7 = addProcessMemoryDump(gmd7, pChrome1, {ts: 63}); + pmdBrowser7.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser4, 'malloc', {numerics: {size: 131072}}), + newAllocatorDump(pmdBrowser4, 'sqlite', {numerics: {size: 262144}}) + ]; + pmdBrowser7.vmRegions = VMRegionClassificationNode.fromRegions([ + new VMRegion(0xABCD, 999999, 0, '/dev/ashmem/dalvik-non moving space', + {privateDirtyResident: 524288}) + ]); + const pmdGpu7 = addProcessMemoryDump(gmd7, pGpu1, {ts: 57}); + pmdGpu7.memoryAllocatorDumps = (function() { + const gpuDump = newAllocatorDump(pmdGpu7, 'gpu', + {numerics: {size: 1048576}}); + const memtrackDump = addChildDump(gpuDump, 'android_memtrack'); + addChildDump(memtrackDump, 'gl', {numerics: {memtrack_pss: 2097152}}); + return [gpuDump]; + })(); + + // Timestamp 8 (Chrome 1). + const gmd8 = addGlobalMemoryDump( + model, {ts: 76, levelOfDetail: BACKGROUND}); + const pmdBrowser8 = addProcessMemoryDump(gmd8, pChrome1, {ts: 80}); + pmdBrowser8.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser5, 'malloc', {numerics: {size: 1024}}), + newAllocatorDump(pmdBrowser5, 'sqlite', {numerics: {size: 2048}}), + ]; + + // Timestamp 9 (WebView). + const gmd9 = addGlobalMemoryDump( + model, {ts: 90, levelOfDetail: BACKGROUND}); + const pmdBrowser9 = addProcessMemoryDump(gmd9, pWebView, {ts: 90}); + pmdBrowser9.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser5, 'v8', {numerics: {size: 4096}}), + newAllocatorDump(pmdBrowser5, 'sqlite', {numerics: {size: 2048}}), + ]; + }, undefined /* opt_options */, { + // WebView (GMD1, GMD6). + 'memory:webview:all_processes:dump_count:detailed': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by WebView ' + + 'to the trace' + }, + 'memory:webview:all_processes:dump_count:light': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by WebView to ' + + 'the trace' + }, + 'memory:webview:all_processes:dump_count:background': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by WebView ' + + 'to the trace' + }, + 'memory:webview:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'WebView to the trace' + }, + 'memory:webview:all_processes:dump_count': { + value: [3], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by WebView to the ' + + 'trace' + }, + 'memory:webview:all_processes:process_count': { + value: [1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:effective_size': { + value: [4 + 2, 16384 + 67108864, 4096 + 2048], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for all ' + + 'processes in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:code_and_metadata_size': { + value: [0, 33554432, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of code and metadata reported by Chrome for ' + + 'all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:allocated_objects_size': { + value: [0, 32768, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of all allocated objects reported by Chrome ' + + 'for all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:peak_size': + { + value: [0, 2, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total peak size reported by Chrome for all ' + + 'processes in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:malloc:effective_size': { + value: [2, 16384, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:sqlite:effective_size': { + value: [0, 2048, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of sqlite in all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:v8:allocated_by_malloc:effective_size': + { + value: [0, 1, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of objects allocated by malloc for v8 ' + + 'in all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:v8:allocated_by_malloc:peak_size': + { + value: [0, 2, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'peak size of objects allocated by malloc for v8 ' + + 'in all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:v8:effective_size': { + value: [4, 67108864, 4096], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8 in all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:v8:code_and_metadata_size': + { + value: [0, 33554432, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of v8 code and metadata in all processes in ' + + 'WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:v8:allocated_objects_size': + { + value: [0, 32768, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8 in all processes ' + + 'in WebView' + }, + 'memory:webview:all_processes:reported_by_chrome:v8:peak_size': + { + value: [0, 2, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'peak size of v8 in all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:private_dirty_size': { + value: [65536], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for all ' + + 'processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:proportional_resident_size': { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by the ' + + 'OS for all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in all processes in ' + + 'WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in all ' + + 'processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [65536], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in all processes ' + + 'in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in all ' + + 'processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in all ' + + 'processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:private_dirty_size': + { + value: [65536], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by all processes in WebView' + }, + 'memory:webview:browser_process:process_count': { + value: [1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of browser processes in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:effective_size': { + value: [4 + 2, 16384 + 67108864, 4096 + 2048], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for the browser ' + + 'process in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:allocated_objects_size': + { + value: [0, 32768, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of all allocated objects reported by ' + + 'Chrome for the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:code_and_metadata_size': + { + value: [0, 33554432, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total size of code and metadata reported by Chrome ' + + 'for the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:peak_size': + { + value: [0, 2, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total peak size reported by Chrome ' + + 'for the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:malloc:effective_size': { + value: [2, 16384, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:v8:allocated_by_malloc:effective_size': + { + value: [0, 1, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of objects allocated by malloc for v8 ' + + 'in the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:v8:allocated_by_malloc:peak_size': + { + value: [0, 2, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'peak size of objects allocated by malloc for v8 ' + + 'in the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:sqlite:effective_size': { + value: [0, 0, 2048], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of sqlite in the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:v8:effective_size': { + value: [4, 67108864, 4096], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8 in the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:v8:allocated_objects_size': + { + value: [0, 32768, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of all objects allocated by v8 in the browser ' + + 'process in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:v8:code_and_metadata_size': + { + value: [0, 33554432, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'size of v8 code and metadata in the browser process ' + + 'in WebView' + }, + 'memory:webview:browser_process:reported_by_chrome:v8:peak_size': + { + value: [0, 2, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'peak size of v8 in the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:private_dirty_size': { + value: [65536], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for the ' + + 'browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by ' + + 'the OS for the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in the browser process ' + + 'in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in the ' + + 'browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [65536], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in the browser ' + + 'process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in the browser ' + + 'process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in the browser ' + + 'process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:private_dirty_size': + { + value: [65536], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by the browser process in WebView' + }, + + // Chrome 1 (GMD3, GMD4). + 'memory:chrome:all_processes:reported_by_os:gpu_memory:gl:proportional_resident_size': + { + value: [0, 2097152], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the gl Android ' + + 'memtrack component in all processes in Chrome' + }, + 'memory:chrome:all_processes:dump_count:detailed': { + value: [2], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:light': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:background': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by Chrome ' + + 'to the trace' + }, + 'memory:chrome:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'Chrome to the trace' + }, + 'memory:chrome:all_processes:dump_count': { + value: [3], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by Chrome to the ' + + 'trace' + }, + 'memory:chrome:all_processes:process_count': { + value: [3, 2, 1], + unit: count_smallerIsBetter, + description: 'total number of all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:effective_size': { + value: [32, 1048576 + 131072 + 262144, 1024 + 2048], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:gpu:effective_size': { + value: [0, 1048576, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of gpu in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:malloc:effective_size': { + value: [32, 131072, 1024], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_chrome:sqlite:effective_size': { + value: [0, 262144, 2048], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of sqlite in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:private_dirty_size': { + value: [8 + 16, 524288], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:proportional_resident_size': { + value: [0, 2097152], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by the ' + + 'OS for all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:gpu_memory:proportional_resident_size': + { + value: [0, 2097152], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of GPU memory ' + + '(Android memtrack) used by all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [8 + 16, 524288], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in all processes ' + + 'in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in all ' + + 'processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:private_dirty_size': + { + value: [8 + 16, 524288], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by all processes in Chrome' + }, + 'memory:chrome:all_processes:reported_by_os:system_memory:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by all processes in Chrome' + }, + 'memory:chrome:browser_process:process_count': { + value: [1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of browser processes in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:effective_size': { + value: [0, 131072 + 262144, 1024 + 2048], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for the browser ' + + 'process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:malloc:effective_size': { + value: [0, 131072, 1024], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_chrome:sqlite:effective_size': { + value: [0, 262144, 2048], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of sqlite in the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:private_dirty_size': { + value: [8, 524288], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for the ' + + 'browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:proportional_resident_size': { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by the ' + + 'OS for the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in the browser process ' + + 'in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in the ' + + 'browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [8, 524288], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in the browser ' + + 'process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in the browser ' + + 'process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in the browser ' + + 'process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:private_dirty_size': + { + value: [8, 524288], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by the browser process in Chrome' + }, + 'memory:chrome:browser_process:reported_by_os:system_memory:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by the browser process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_chrome:effective_size': { + value: [0, 1048576, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for the GPU ' + + 'process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:gpu_memory:proportional_resident_size': + { + value: [0, 2097152], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of GPU memory ' + + '(Android memtrack) used by the GPU process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:gpu_memory:gl:proportional_resident_size': + { + value: [0, 2097152], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the gl Android ' + + 'memtrack component in the GPU process in Chrome' + }, + 'memory:chrome:gpu_process:process_count': { + value: [1, 1, 0], + unit: count_smallerIsBetter, + description: 'total number of GPU processes in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_chrome:gpu:effective_size': { + value: [0, 1048576, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of gpu in the GPU process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:private_dirty_size': { + value: [16, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for the GPU ' + + 'process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:proportional_resident_size': { + value: [0, 2097152], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by the ' + + 'OS for the GPU process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in the GPU process in ' + + 'Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in the ' + + 'GPU process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [16, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in the GPU ' + + 'process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'the GPU process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in the GPU ' + + 'process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in the GPU process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in the GPU ' + + 'process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in the GPU process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:system_memory:private_dirty_size': + { + value: [16, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by the GPU process in Chrome' + }, + 'memory:chrome:gpu_process:reported_by_os:system_memory:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by the GPU process in Chrome' + }, + 'memory:chrome:renderer_processes:process_count': { + value: [1, 0, 0], + unit: count_smallerIsBetter, + description: 'total number of renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:effective_size': { + value: [32, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_chrome:malloc:effective_size': + { + value: [32, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in renderer processes in ' + + 'Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:private_dirty_size': { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by ' + + 'the OS for renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in renderer processes ' + + 'in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in ' + + 'renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in renderer ' + + 'processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:system_memory:private_dirty_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by renderer processes in Chrome' + }, + 'memory:chrome:renderer_processes:reported_by_os:system_memory:proportional_resident_size': + { + value: [0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by renderer processes in Chrome' + }, + + // Chrome 2 (GMD2, GMD7). + 'memory:chrome2:all_processes:dump_count:detailed': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by Chrome(2) ' + + 'to the trace' + }, + 'memory:chrome2:all_processes:dump_count:light': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by Chrome(2) to ' + + 'the trace' + }, + 'memory:chrome2:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by ' + + 'Chrome(2) to the trace' + }, + 'memory:chrome2:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'Chrome(2) to the trace' + }, + 'memory:chrome2:all_processes:dump_count': { + value: [2], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by Chrome(2) to ' + + 'the trace' + }, + 'memory:chrome2:all_processes:process_count': { + value: [1, 2], + unit: count_smallerIsBetter, + description: 'total number of all processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_chrome:discardable:effective_size': + { + value: [8388608, 16777216], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of discardable in all processes in ' + + 'Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_chrome:locked_size': { + value: [4194304, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total locked (pinned) size reported by Chrome for all ' + + 'processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_chrome:discardable:locked_size': { + value: [4194304, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'locked (pinned) size of discardable in all processes in ' + + 'Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_chrome:effective_size': { + value: [64 + 8388608 + 128, 512 + 1024 + 2048 + 16777216], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for all ' + + 'processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_chrome:malloc:effective_size': { + value: [64, 512 + 1024], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in all processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_chrome:sqlite:effective_size': { + value: [128, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of sqlite in all processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_chrome:v8:effective_size': { + value: [0, 2048], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8 in all processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:private_dirty_size': { + value: [256], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for all ' + + 'processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:proportional_resident_size': { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by the ' + + 'OS for all processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in all processes in ' + + 'Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in all ' + + 'processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [256], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in all processes ' + + 'in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'all processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in all ' + + 'processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in all processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in all ' + + 'processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in all processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:system_memory:private_dirty_size': + { + value: [256], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by all processes in Chrome(2)' + }, + 'memory:chrome2:all_processes:reported_by_os:system_memory:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by all processes in Chrome(2)' + }, + 'memory:chrome2:browser_process:process_count': { + value: [1, 1], + unit: count_smallerIsBetter, + description: 'total number of browser processes in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_chrome:effective_size': { + value: [64 + 8388608 + 128, 512 + 16777216], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for the browser ' + + 'process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_chrome:locked_size': { + value: [4194304, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total locked (pinned) size reported by Chrome for the ' + + 'browser process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_chrome:malloc:effective_size': { + value: [64, 512], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in the browser process in ' + + 'Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_chrome:discardable:effective_size': + { + value: [8388608, 16777216], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of discardable in the browser process ' + + 'in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_chrome:discardable:locked_size': + { + value: [4194304, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'locked (pinned) size of discardable in the browser ' + + 'process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_chrome:sqlite:effective_size': { + value: [128, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of sqlite in the browser process in ' + + 'Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:private_dirty_size': { + value: [256], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for the ' + + 'browser process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by ' + + 'the OS for the browser process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in the browser process ' + + 'in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in the ' + + 'browser process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [256], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in the browser ' + + 'process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'the browser process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in the browser ' + + 'process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in the browser process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in the browser ' + + 'process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in the browser process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:system_memory:private_dirty_size': + { + value: [256], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by the browser process in Chrome(2)' + }, + 'memory:chrome2:browser_process:reported_by_os:system_memory:proportional_resident_size': + { + value: [0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by the browser process in Chrome(2)' + }, + 'memory:chrome2:renderer_processes:process_count': { + value: [0, 1], + unit: count_smallerIsBetter, + description: 'total number of renderer processes in Chrome(2)' + }, + 'memory:chrome2:renderer_processes:reported_by_chrome:effective_size': { + value: [0, 1024 + 2048], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for renderer ' + + 'processes in Chrome(2)' + }, + 'memory:chrome2:renderer_processes:reported_by_chrome:malloc:effective_size': + { + value: [0, 1024], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in renderer processes in ' + + 'Chrome(2)' + }, + 'memory:chrome2:renderer_processes:reported_by_chrome:v8:effective_size': { + value: [0, 2048], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of v8 in renderer processes in Chrome(2)' + }, + + // Unknown browser (GMD5). + 'memory:unknown_browser:all_processes:dump_count:detailed': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:light': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by an unknown ' + + 'browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by an unknown ' + + 'browser to the trace' + }, + 'memory:unknown_browser:all_processes:process_count': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of all processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:effective_size': { + value: [4096 + 8192], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for all ' + + 'processes in an unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:malloc:effective_size': + { + value: [4096], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in all processes in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:all_processes:reported_by_chrome:sqlite:effective_size': + { + value: [8192], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of sqlite in all processes in an ' + + 'unknown browser' + }, + 'memory:unknown_browser:browser_process:process_count': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of browser processes in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_chrome:effective_size': + { + value: [4096 + 8192], + unit: sizeInBytes_smallerIsBetter, + description: 'total effective size reported by Chrome for the ' + + 'browser process in an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_chrome:malloc:effective_size': + { + value: [4096], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of malloc in the browser process in ' + + 'an unknown browser' + }, + 'memory:unknown_browser:browser_process:reported_by_chrome:sqlite:effective_size': + { + value: [8192], + unit: sizeInBytes_smallerIsBetter, + description: 'effective size of sqlite in the browser process in ' + + 'an unknown browser' + } + }); + + memoryMetricTest('rangeOfInterest', function(model) { + const pChrome = createChromeBrowserProcess(model); + const pWebView = createWebViewProcess(model); + + // Chrome: only the LIGHT dumps should be kept. + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 5, duration: 4, levelOfDetail: DETAILED}), pChrome); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 10, duration: 2, levelOfDetail: LIGHT}), pChrome); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 13, duration: 3, levelOfDetail: LIGHT}), pChrome); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 20, duration: 1, levelOfDetail: BACKGROUND}), pChrome); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 22, duration: 5, levelOfDetail: DETAILED}), pChrome); + + // WebView: only the DETAILED dumps should be kept. + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 4, duration: 1, levelOfDetail: LIGHT}), pWebView); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 5, duration: 5, levelOfDetail: DETAILED}), pWebView); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 10, duration: 0, levelOfDetail: DETAILED}), pWebView); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 11, duration: 7, levelOfDetail: BACKGROUND}), pWebView); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 19, duration: 2, levelOfDetail: DETAILED}), pWebView); + addProcessMemoryDump(addGlobalMemoryDump( + model, {ts: 21, duration: 5, levelOfDetail: LIGHT}), pWebView); + + // Unknown browser: only the LIGHT dump should be kept. + addGlobalMemoryDump(model, {ts: 5, duration: 3, levelOfDetail: DETAILED}); + addGlobalMemoryDump(model, {ts: 9, duration: 12, levelOfDetail: LIGHT}); + addGlobalMemoryDump(model, {ts: 22, duration: 3, levelOfDetail: DETAILED}); + }, { /* opt_options */ + rangeOfInterest: tr.b.math.Range.fromExplicitRange(10, 20) + }, { + 'memory:chrome:all_processes:dump_count': { + value: [3], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by Chrome to the ' + + 'trace' + }, + 'memory:chrome:all_processes:dump_count:detailed': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:light': { + value: [2], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by Chrome to ' + + 'the trace' + }, + 'memory:chrome:all_processes:dump_count:background': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by Chrome ' + + 'to the trace' + }, + 'memory:chrome:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'Chrome to the trace' + }, + 'memory:chrome:all_processes:process_count': { + value: [1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of all processes in Chrome' + }, + 'memory:chrome:browser_process:process_count': { + value: [1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of browser processes in Chrome' + }, + + 'memory:webview:all_processes:dump_count': { + value: [4], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by WebView to the ' + + 'trace' + }, + 'memory:webview:all_processes:dump_count:detailed': { + value: [3], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by WebView ' + + 'to the trace' + }, + 'memory:webview:all_processes:dump_count:light': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by WebView to ' + + 'the trace' + }, + 'memory:webview:all_processes:dump_count:background': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by WebView ' + + 'to the trace' + }, + 'memory:webview:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by ' + + 'WebView to the trace' + }, + 'memory:webview:all_processes:process_count': { + value: [1, 1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:private_dirty_size': { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for all ' + + 'processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:proportional_resident_size': { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by the ' + + 'OS for all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in all processes in ' + + 'WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in all ' + + 'processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in all processes ' + + 'in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in all ' + + 'processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in all ' + + 'processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by all processes in WebView' + }, + 'memory:webview:all_processes:reported_by_os:system_memory:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by all processes in WebView' + }, + 'memory:webview:browser_process:process_count': { + value: [1, 1, 1, 1], + unit: count_smallerIsBetter, + description: 'total number of browser processes in WebView' + }, + 'memory:webview:browser_process:reported_by_os:private_dirty_size': { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size reported by the OS for the ' + + 'browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) reported by ' + + 'the OS for the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:ashmem:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of ashmem in the browser process ' + + 'in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:ashmem:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of ashmem in the ' + + 'browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:java_heap:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the Java heap in the browser ' + + 'process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:java_heap:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the Java heap in ' + + 'the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:native_heap:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the native heap in the browser ' + + 'process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:native_heap:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the native heap ' + + 'in the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:stack:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'private dirty size of the thread stacks in the browser ' + + 'process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:stack:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'proportional resident size (PSS) of the thread stacks ' + + 'in the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:private_dirty_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total private dirty size of system memory (RAM) used ' + + 'by the browser process in WebView' + }, + 'memory:webview:browser_process:reported_by_os:system_memory:proportional_resident_size': + { + value: [0, 0, 0], + unit: sizeInBytes_smallerIsBetter, + description: 'total proportional resident size (PSS) of system ' + + 'memory (RAM) used by the browser process in WebView' + }, + 'memory:unknown_browser:all_processes:dump_count': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of all memory dumps added by an unknown ' + + 'browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:detailed': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of detailed memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:light': { + value: [1], + unit: count_smallerIsBetter, + description: 'total number of light memory dumps added by an unknown ' + + 'browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:background': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of background memory dumps added by an ' + + 'unknown browser to the trace' + }, + 'memory:unknown_browser:all_processes:dump_count:heap_profiler': { + value: [0], + unit: count_smallerIsBetter, + description: 'total number of heap profiler memory dumps added by an ' + + 'unknown browser to the trace' + } + }); + + test('allProcessesHasBreakdown', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const pBrowser = createChromeBrowserProcess(model); + const pRenderer = createProcessWithName(model, 'Renderer'); + const pGpu = createProcessWithName(model, 'GPU Process'); + + const gmd = addGlobalMemoryDump(model, {ts: 20}); + const pmdBrowser = addProcessMemoryDump(gmd, pBrowser, {ts: 19}); + pmdBrowser.memoryAllocatorDumps = [ + newAllocatorDump(pmdBrowser, 'malloc', {numerics: { + size: 8, + allocated_objects_size: 4 + }}) + ]; + + const pmdRenderer = addProcessMemoryDump(gmd, pRenderer, {ts: 20}); + pmdRenderer.memoryAllocatorDumps = [ + newAllocatorDump(pmdRenderer, 'malloc', {numerics: {size: 16}}) + ]; + + const pmdGpu = addProcessMemoryDump(gmd, pGpu, {ts: 21}); + pmdGpu.memoryAllocatorDumps = [ + newAllocatorDump(pmdGpu, 'gpu', {numerics: { + size: 30, + allocated_objects_size: 25 + }}) + ]; + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.memoryMetric(histograms, model); + const prefix = 'memory:chrome:'; + const suffix = ':reported_by_chrome:malloc:effective_size'; + const allHistogram = + histograms.getHistogramNamed(`${prefix}all_processes${suffix}`); + const rendererHistogram = + histograms.getHistogramNamed(`${prefix}renderer_processes${suffix}`); + const browserHistogram = + histograms.getHistogramNamed(`${prefix}browser_process${suffix}`); + + const relatedNameMap = allHistogram.diagnostics.get('processes'); + assert.strictEqual(relatedNameMap.get('browser_process'), + browserHistogram.name); + assert.strictEqual(relatedNameMap.get('renderer_processes'), + rendererHistogram.name); + assert.strictEqual(relatedNameMap.get('gpu_process'), undefined); + + const bin = allHistogram.getBinForValue(allHistogram.average); + const breakdown = tr.b.getOnlyElement(bin.diagnosticMaps).get('processes'); + assert.strictEqual(breakdown.get('browser_process'), + browserHistogram.average); + assert.strictEqual(breakdown.get('renderer_processes'), + rendererHistogram.average); + }); + + test('componentsHaveBreakdowns', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const pBrowser = createChromeBrowserProcess(model); + + const gmd = addGlobalMemoryDump(model, {ts: 20}); + const pmdBrowser = addProcessMemoryDump(gmd, pBrowser, {ts: 19}); + const v8Dump = newAllocatorDump(pmdBrowser, 'v8', {numerics: { + size: 10, + }}); + + const isolateDumpA = addChildDump(v8Dump, 'isolate_A'); + addChildDump(isolateDumpA, 'malloc', {numerics: { + size: 1, + peak_size: 2 + }}); + + const heapDumpA = addChildDump(isolateDumpA, 'heap_spaces', {numerics: { + size: 42, + allocated_objects_size: 36 + }}); + + addChildDump(heapDumpA, 'code_space', {numerics: { + allocated_objects_size: 1, + size: 2 + }}); + + pmdBrowser.memoryAllocatorDumps = [v8Dump]; + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.memoryMetric(histograms, model); + const prefix = 'memory:chrome:browser_process:reported_by_chrome'; + const suffix = 'effective_size'; + const browserHistogram = histograms.getHistogramNamed( + `${prefix}:v8:${suffix}`); + const heapHistogram = histograms.getHistogramNamed( + `${prefix}:v8:heap:${suffix}`); + const codeSpaceHistogram = histograms.getHistogramNamed( + `${prefix}:v8:heap:code_space:${suffix}`); + + assert.strictEqual(browserHistogram.diagnostics.get('components').get( + 'heap'), heapHistogram.name); + assert.strictEqual(heapHistogram.diagnostics.get('components').get( + 'code_space'), codeSpaceHistogram.name); + + const browserBin = browserHistogram.getBinForValue( + browserHistogram.average); + const browserBreakdown = tr.b.getOnlyElement( + browserBin.diagnosticMaps).get('components'); + assert.strictEqual(browserBreakdown.get('heap'), heapHistogram.average); + + const heapBin = heapHistogram.getBinForValue(heapHistogram.average); + const heapBreakdown = tr.b.getOnlyElement( + heapBin.diagnosticMaps).get('components'); + assert.strictEqual(heapBreakdown.get('code_space'), + codeSpaceHistogram.average); + }); + + test('dumpIdBrowserClashThrows', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const pWebView = createWebViewProcess(model); + const pChrome = createChromeBrowserProcess(model); + + const gmd = addGlobalMemoryDump(model, {ts: 10}); + addProcessMemoryDump(gmd, pWebView, {ts: 9}); + addProcessMemoryDump(gmd, pChrome, {ts: 11}); + }); + const histograms = new tr.v.HistogramSet(); + + assert.throws(function() { + tr.metrics.sh.memoryMetric(histograms, model); + }, 'Memory dump ID clash across multiple browsers with PIDs: 1 and 2'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric.html new file mode 100644 index 00000000000..81b4f65ca79 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/extras/chrome/cpu_time.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/system_health/cpu_time_tree_data_reporter.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +/** + * @fileoverview Implements the new CPU time metric. This will eventually + * replace the current cpu_time_metric.html, but we're running this alongside + * the existing metric while we monitor its quality. + * + */ +tr.exportTo('tr.metrics.sh', function() { + /** + * This metric measures total CPU time and CPU time per unit of wall clock + * time for all combinations of process type, thread type, RAIL + * stage, and RAIL stage initiator present in the model. + * + * The metric generates histograms of the form + * ${cpuTime|cpuPercentage}:${process_type}:${thread_type}: + * ${rail_stage}:${rail_stage_initiator} + * + * 'cpuTime' histograms represent total consumed CPU time, while + * 'cpuPercentage' histograms represent CPU time as a percentage of wall clock + * time. + * + * Example histograms generated by this metric: + * - cpuTime:browser_process:CrBrowserMain:Animation:CSS + * - cpuPercentage:gpu_process:CrGpuMain:Response:Scroll + + * For a given model, a single sample is generated for each histogram. For + * example, if the model contains three renderer processes, and 10 different + * Scroll Response ranges, the histogram + * cpuPercentage:renderer_process:CrRendererMain:Response:Scroll will still + * contain a single sample: the total CPU time consumed by all three renderer + * main threads over all 10 Scroll Response phases, divided by the total + * duration of those ranges. Since the three different main threads can + * potentially be running on three different CPU cores, the sample value of a + * cpuPercentage histogram can be more than 100%. + * + * The histograms are created as needed from the model - if a certain + * combination of process, thread, RAIL stage and initiator does not occur in + * the model, the histogram for that combination is not added. + * + * This metric requires only the 'toplevel' tracing category. + * + * @param {!tr.v.HistogramSet} histograms + * @param {!tr.model.Model} model + * @param {!Object=} opt_options + */ + function newCpuTimeMetric(histograms, model, opt_options) { + const rangeOfInterest = opt_options && opt_options.rangeOfInterest ? + opt_options.rangeOfInterest : model.bounds; + + const rootNode = tr.e.chrome.CpuTime.constructMultiDimensionalView( + model, rangeOfInterest); + + tr.metrics.sh.CpuTimeTreeDataReporter.reportToHistogramSet( + rootNode, histograms); + } + + tr.metrics.MetricRegistry.register(newCpuTimeMetric, { + supportsRangeOfInterest: true + }); + + return { + newCpuTimeMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric_test.html new file mode 100644 index 00000000000..a21b6b83ca6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/new_cpu_time_metric_test.html @@ -0,0 +1,186 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/base/multi_dimensional_view.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/system_health/new_cpu_time_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const cpuTimeMetric = tr.metrics.sh.newCpuTimeMetric; + const CHROME_PROCESS_NAMES = + tr.e.chrome.chrome_processes.CHROME_PROCESS_NAMES; + + test('cpuTimeMetric_customRangeOfInterest', () => { + const model = tr.c.TestUtils.newModel(model => { + const process = model.getOrCreateProcess(1); + process.name = 'Browser'; + const thread = process.getOrCreateThread(1); + thread.name = 'CrBrowserMain'; + + // Slice from 0 to 50. + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: 50, + cpuStart: 0, + cpuDuration: 25 + })); + + // Slice from 300 to 350. + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 300, + duration: 50, + cpuStart: 100, + cpuDuration: 25 + })); + }); + + const metricNameSuffix = CHROME_PROCESS_NAMES.BROWSER + + ':CrBrowserMain:all_stages:all_initiators'; + const rangeOfInterest = new tr.b.math.Range.fromExplicitRange(0, 200); + const histograms = new tr.v.HistogramSet(); + cpuTimeMetric(histograms, model, {rangeOfInterest}); + + const cpuPercentageHistogram = histograms.getHistogramNamed( + 'cpuPercentage:' + metricNameSuffix); + const cpuTimeHistogram = histograms.getHistogramNamed( + 'cpuTime:' + metricNameSuffix); + + // Histograms exist. + assert.isDefined(cpuPercentageHistogram); + assert.isDefined(cpuTimeHistogram); + + // Each histogram contains a single sample. + assert.strictEqual(cpuPercentageHistogram.running.count, 1); + assert.strictEqual(cpuTimeHistogram.running.count, 1); + + // Assert histogram sample value is correct: + // rangeOfInterest only contains the first slice (with a CPU time of 25), + // and total duration of the range is 200. + assert.closeTo(cpuPercentageHistogram.sum, 25 / 200, 1e-7); + assert.closeTo(cpuTimeHistogram.sum, 25, 1e-7); + }); + + test('cpuTimeMetric_rangeOfInterestBeyondModelBounds', () => { + const model = tr.c.TestUtils.newModel(model => { + const process = model.getOrCreateProcess(1); + process.name = 'Browser'; + const thread = process.getOrCreateThread(1); + thread.name = 'CrBrowserMain'; + + // Slice from 0 to 50. + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: 50, + cpuStart: 0, + cpuDuration: 25 + })); + + // Slice from 300 to 350. + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 300, + duration: 50, + cpuStart: 100, + cpuDuration: 25 + })); + }); + + const metricNameSuffix = CHROME_PROCESS_NAMES.BROWSER + + ':CrBrowserMain:all_stages:all_initiators'; + const rangeOfInterest = new tr.b.math.Range.fromExplicitRange(-100, 200); + const histograms = new tr.v.HistogramSet(); + cpuTimeMetric(histograms, model, {rangeOfInterest}); + + const cpuPercentageHistogram = histograms.getHistogramNamed( + 'cpuPercentage:' + metricNameSuffix); + const cpuTimeHistogram = histograms.getHistogramNamed( + 'cpuTime:' + metricNameSuffix); + + // Histograms exist. + assert.isDefined(cpuPercentageHistogram); + assert.isDefined(cpuTimeHistogram); + + // Each histogram contains a single sample. + assert.strictEqual(cpuPercentageHistogram.running.count, 1); + assert.strictEqual(cpuTimeHistogram.running.count, 1); + + // Assert histogram sample value is correct: + // model.bounds is [0, 350] here. If rangeOfInterest is beyond these bounds, + // the effective range for calculating total duration for cpuPercentage is + // the intersection of rangeOfInterest and model.bounds, which is [0, 200] + // here. rangeOfInterest only contains the first slice, with a CPU time of + // 25, and the total effective duration is 200, not 300. + assert.closeTo(cpuPercentageHistogram.sum, 25 / 200, 1e-7); + assert.closeTo(cpuTimeHistogram.sum, 25, 1e-7); + }); + + test('cpuTimeMetric_defaultRangeOfInterest', () => { + const model = tr.c.TestUtils.newModel(model => { + const process = model.getOrCreateProcess(1); + process.name = 'Browser'; + const thread = process.getOrCreateThread(1); + thread.name = 'CrBrowserMain'; + + // Slice from 0 to 50. + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: 50, + cpuStart: 0, + cpuDuration: 25 + })); + + // Slice from 300 to 350. + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 300, + duration: 50, + cpuStart: 100, + cpuDuration: 25 + })); + }); + + const metricNameSuffix = CHROME_PROCESS_NAMES.BROWSER + + ':CrBrowserMain:all_stages:all_initiators'; + const histograms = new tr.v.HistogramSet(); + cpuTimeMetric(histograms, model); + + const cpuPercentageHistogram = histograms.getHistogramNamed( + 'cpuPercentage:' + metricNameSuffix); + const cpuTimeHistogram = histograms.getHistogramNamed( + 'cpuTime:' + metricNameSuffix); + + // Histograms exist. + assert.isDefined(cpuPercentageHistogram); + assert.isDefined(cpuTimeHistogram); + + // Each histogram contains a single sample. + assert.strictEqual(cpuPercentageHistogram.running.count, 1); + assert.strictEqual(cpuTimeHistogram.running.count, 1); + + // Assert histogram sample value is correct: + // When no custom rangeOfInterest is set, the effective range for + // calculating CPU usage should be model.bounds, which is [0, 350] here. + // Total CPU time is 25 + 25 from the two slices. + assert.closeTo(cpuPercentageHistogram.sum, (25 + 25) / 350, 1e-7); + assert.closeTo(cpuTimeHistogram.sum, 25 + 25, 1e-7); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric.html new file mode 100644 index 00000000000..8b343e07f49 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric.html @@ -0,0 +1,348 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/base/unit_scale.html"> +<link rel="import" href="/tracing/importer/find_input_expectations.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + // If the power series doesn't cover the entire Chrome trace, then + // the results from Chrome tracing metrics will likely be inaccurate, + // so we don't report them. However, we allow the power series bounds + // to be up to 1 ms inside the Chrome trace and still count as + // covering the Chrome trace. This is to allow for small deviations + // due to clock sync latency and the finite sampling rate of the + // BattOr. + const CHROME_POWER_GRACE_PERIOD_MS = 1; + + /** + * Creates an empty histogram to hold data for a given time interval. + * + * @returns {Object} An object of the form: + * + * { + * perSecond {boolean}: Whether the data for this time interval is given + * as per second, If not, it's given as an integral over the + * whole interval. + * energy {tr.v.Histogram}: Histogram giving energy use (i.e. energy in J + * if perSecond = False, power in W if perSecond = True) + * } + */ + function createEmptyHistogram_(interval, histograms) { + if (interval.perSecond) { + return { + perSecond: true, + energy: histograms.createHistogram(`${interval.name}:power`, + tr.b.Unit.byName.powerInWatts_smallerIsBetter, [], { + description: + `Energy consumption rate for ${interval.description}`, + summaryOptions: { + avg: true, + count: false, + max: true, + min: true, + std: false, + sum: false, + }, + }), + }; + } + return { + perSecond: false, + energy: histograms.createHistogram(`${interval.name}:energy`, + tr.b.Unit.byName.energyInJoules_smallerIsBetter, [], { + description: `Energy consumed in ${interval.description}`, + summaryOptions: { + avg: false, + count: false, + max: true, + min: true, + std: false, + sum: true, + }, + }), + }; + } + + function createHistograms_(data, interval, histograms) { + if (data.histograms[interval.name] === undefined) { + data.histograms[interval.name] = createEmptyHistogram_(interval, + histograms); + } + if (data.histograms[interval.name].perSecond) { + for (const sample of data.model.device.powerSeries.getSamplesWithinRange( + interval.bounds.min, interval.bounds.max)) { + data.histograms[interval.name].energy.addSample(sample.powerInW); + } + } else { + const energyInJ = data.model.device.powerSeries.getEnergyConsumedInJ( + interval.bounds.min, interval.bounds.max); + data.histograms[interval.name].energy.addSample(energyInJ); + } + } + + /** + * Returns the intervals of time between navigation event and time to + * interactive. + */ + function getNavigationTTIIntervals_(model) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + const intervals = []; + for (const expectation of model.userModel.expectations) { + if (!(expectation instanceof tr.model.um.LoadExpectation)) continue; + if (tr.e.chrome.CHROME_INTERNAL_URLS.includes( + expectation.url)) { + continue; + } + if (expectation.timeToInteractive !== undefined) { + intervals.push(tr.b.math.Range.fromExplicitRange( + expectation.navigationStart.start, expectation.timeToInteractive)); + } + } + return intervals.sort((x, y) => x.min - y.min); + } + + /** + * Generates the set of time intervals that metrics should be run over. + * Time intervals include each UE (for UE-based metrics), the whole + * story (for the full-story metric), etc. Each time interval is given + * in the following form: + * + * { + * bounds {tr.b.math.Range}: Boundaries of the time interval. + * name {string}: Name of this interval. Used to generate the + * metric names. + * description {string}: Human readable description of the interval. + * Used to generate the metric descriptions. + * perSecond {boolean}: Whether metrics over this interval should be + * reported as per-second values (e.g. power). If not, integrated values + * over the whole interval (e.g. energy) are reported. + * } + * + */ + function* computeTimeIntervals_(model) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + const powerSeries = model.device.powerSeries; + if (powerSeries === undefined || + powerSeries.samples.length === 0) { + return; + } + // Output the full story power metrics, which exists regardless of + // the presence of a Chrome trace. + yield { + bounds: model.bounds, + name: 'story', + description: 'user story', + perSecond: true + }; + + const chromeBounds = computeChromeBounds_(model); + if (chromeBounds.isEmpty) return; + + const powerSeriesBoundsWithGracePeriod = tr.b.math.Range.fromExplicitRange( + powerSeries.bounds.min - CHROME_POWER_GRACE_PERIOD_MS, + powerSeries.bounds.max + CHROME_POWER_GRACE_PERIOD_MS); + if (!powerSeriesBoundsWithGracePeriod.containsRangeExclusive( + chromeBounds)) { + return; + } + + // If Chrome bounds are good and the power trace covers the Chrome bounds, + // then output the Chrome specific metrics (loading and RAIL stages). Note + // that only the part of the time interval that overlaps the Chrome bounds + // should be included. + for (const interval of getRailStageIntervals_(model)) { + yield { + bounds: interval.bounds.findIntersection(chromeBounds), + name: interval.name, + description: interval.description, + perSecond: interval.perSecond + }; + } + + for (const interval of getLoadingIntervals_(model, chromeBounds)) { + yield { + bounds: interval.bounds.findIntersection(chromeBounds), + name: interval.name, + description: interval.description, + perSecond: interval.perSecond + }; + } + } + + /** + * Gets a list of time intervals for the RAIL stages. Each RAIL stage + * generates a time interval with the name equal to the name of the RAIL + * stage except made into lower case and with spaces replaced bu underscores + * e.g. "Scroll Animation" --> "scroll_animation". Each time interval is given + * in the following form: + * + * { + * bounds {tr.b.math.Range}: Boundaries of the time interval. + * name {string}: Name of this interval. Used to generate the + * metric names. + * description {string}: Human readable description of the interval. + * Used to generate the metric descriptions. + * perSecond {boolean}: Whether metrics over this interval should be + * reported as per-second values (e.g. power). If not, integrated values + * over the whole interval (e.g. energy) are reported. + * } + * + */ + function* getRailStageIntervals_(model) { + for (const exp of model.userModel.expectations) { + const histogramName = exp.title.toLowerCase().replace(' ', '_'); + const energyHist = undefined; + if (histogramName.includes('response')) { + yield { + bounds: tr.b.math.Range.fromExplicitRange(exp.start, exp.end), + name: histogramName, + description: 'RAIL stage ' + histogramName, + perSecond: false + }; + } else if (histogramName.includes('animation') || + histogramName.includes('idle')) { + yield { + bounds: tr.b.math.Range.fromExplicitRange(exp.start, exp.end), + name: histogramName, + description: 'RAIL stage ' + histogramName, + perSecond: true + }; + } + } + } + + /** + * Gets a list of time intervals for the RAIL stages. Each RAIL stage + * generates a time interval with the name equal to the name of the RAIL + * stage except made into lower case and with spaces replaced bu underscores + * e.g. "Scroll Animation" --> "scroll_animation". Each time interval is given + * in the following form: + * + * { + * bounds {tr.b.math.Range}: Boundaries of the time interval. + * name {string}: Name of this interval. Used to generate the + * metric names. + * description {string}: Human readable description of the interval. + * Used to generate the metric descriptions. + * perSecond {boolean}: Whether metrics over this interval should be + * reported as per-second values (e.g. power). If not, integrated values + * over the whole interval (e.g. energy) are reported. + * } + * + */ + function* getLoadingIntervals_(model, chromeBounds) { + const ttiIntervals = getNavigationTTIIntervals_(model); + for (const ttiInterval of ttiIntervals) { + yield { + bounds: ttiInterval, + name: 'load', + description: 'page loads', + perSecond: false + }; + } + } + + /** + * @returns {tr.b.math.Range} The boundaries of the Chrome portion of the + * trace. + */ + function computeChromeBounds_(model) { + const chromeBounds = new tr.b.math.Range(); + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (chromeHelper === undefined) return chromeBounds; + for (const helper of chromeHelper.browserHelpers) { + if (helper.mainThread) { + chromeBounds.addRange(helper.mainThread.bounds); + } + } + for (const pid in chromeHelper.rendererHelpers) { + if (chromeHelper.rendererHelpers[pid].mainThread) { + chromeBounds.addRange( + chromeHelper.rendererHelpers[pid].mainThread.bounds); + } + } + return chromeBounds; + } + + /** + * Adds the power histograms to the histogram set. + * + * Each power histogram is based on a specific time interval, and is named as + * follows: + * + * - [time_interval_name]:power, which contains a sample for each power + * sample data point during any time interval with the given type. Each + * sample represents the power draw during the period covered by that + * power sample. + * + * - [time_interval_name]:energy, which contains a sample for each time + * interval with the given type present in the trace. Each sample + * represents the total energy used over that time interval. + * + * The time intervals are as follows: + * + * - "story": The time interval covering the entire user story. There is + * always exactly one "story" interval. + * + * - "load" : The time interval covered by a page load, from navigationStart + * to timeToInteractive. There is one "load" interval for each page load + * in the trace. + * + * - "[user_expectation_type]" : Each Response, Animation, or Idle + * UE in the trace generates a time interval whose name is that of the UE, + * converted to lower case and with underscores in place of spaces. + * For instance, if there are three "Scroll Response" UEs in the trace, + * then there will be three "scroll_response" time intervals, so the + * histogram scroll_response:energy will contain three samples. + * + * Note that each time interval type only generates ONE of the "power" or + * "energy" histograms. Power histograms are generated for time intervals + * that represent events that occur over a period of time. This includes + * the following intervals + * + * - "story" + * - Any Animation or Idle UE + * + * For instance, "the energy it takes to play a video" + * does not have meaning because it depends on how long the video + * is; thus a more meaningful metric is power. In contrast, energy histograms + * are generated for time intervals that represent particular tasks + * which must be completed. This includes the following intervals: + * + * - "load" + * - Any Response UE + * + * For instance, if a change causes a response to take longer to process, then + * we want to count that as taking the energy over a longer period of time. + */ + function powerMetric(histograms, model) { + const data = { + model, + histograms: {} + }; + for (const interval of computeTimeIntervals_(model)) { + createHistograms_(data, interval, histograms); + } + } + + tr.metrics.MetricRegistry.register(powerMetric); + + return { + powerMetric + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric_test.html new file mode 100644 index 00000000000..9e8d47ea258 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/power_metric_test.html @@ -0,0 +1,742 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html"> +<link rel="import" + href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html"> +<link rel="import" href="/tracing/importer/find_input_expectations.html"> +<link rel="import" href="/tracing/metrics/system_health/power_metric.html"> +<link rel="import" href="/tracing/model/user_model/idle_expectation.html"> +<link rel="import" href="/tracing/model/user_model/load_expectation.html"> +<link rel="import" href="/tracing/model/user_model/user_expectation.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +function containsData(histograms, name) { + for (const value of histograms) { + if (value.name === name) { + return (value.running !== undefined); + } + } + + return undefined; +} + +function getMetricValueCount(histograms, name) { + for (const value of histograms) { + if (value.name === name && value.running !== undefined) { + return value.running.count; + } + } + + return undefined; +} + +function getMetricValueSum(histograms, name) { + for (const value of histograms) { + if (value.name === name && value.running !== undefined) { + return value.running.sum; + } + } + + return undefined; +} + +function getMetricValueMin(histograms, name) { + for (const value of histograms) { + if (value.name === name && value.running !== undefined) { + return value.running.min; + } + } + + return undefined; +} + +function getMetricValueAvg(histograms, name) { + for (const value of histograms) { + if (value.name === name && value.running !== undefined) { + return value.running.mean; + } + } + + return undefined; +} + +function getMetricValueMax(histograms, name) { + for (const value of histograms) { + if (value.name === name && value.running !== undefined) { + return value.running.max; + } + } + + return undefined; +} + +tr.b.unittest.testSuite(function() { + test('powerMetric_computesChromeBoundsCorrectly', function() { + // Tests if Chrome bounds are computed correctly when there + // are both browser and renderer threads in the trace. + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const rendererThread = model.rendererMain; + const browserProcess = model.browserProcess; + const browserThread = model.browserMain; + rendererThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200.0, + duration: 500.0, + args: {frame: '0xdeadbeef'} + })); + browserThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000.0, + duration: 500.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 2000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 0, 2000)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.closeTo(getMetricValueMin(histograms, 'idle:power'), 200.0, .01); + assert.closeTo(getMetricValueAvg(histograms, 'idle:power'), 849.5, .01); + assert.closeTo(getMetricValueMax(histograms, 'idle:power'), 1499.0, .01); + }); + + test('powerMetric_noPowerSeries', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.lengthOf(histograms, 0); + }); + + test('powerMetric_emptyPowerSeries', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.lengthOf(histograms, 0); + }); + + test('powerMetric_noChromeTrace', function() { + const model = tr.c.TestUtils.newModel(function(model) { + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + // Only the whole trace power metric should contain data. + assert(containsData(histograms, 'story:power')); + assert(!containsData(histograms, 'load:energy')); + assert(!containsData(histograms, 'after_load:power')); + }); + + test('powerMetric_emptyChromeTrace', function() { + const histograms = new tr.v.HistogramSet(); + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.LoadExpectation( + model, tr.model.um.LOAD_SUBTYPE_NAMES.SUCCESSFUL, 0, 500)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 500, 500)); + }); + tr.metrics.sh.powerMetric(histograms, model); + + assert(containsData(histograms, 'story:power')); + assert(!containsData(histograms, 'load:energy')); + assert(!containsData(histograms, 'after_load:power')); + }); + + test('powerMetric_powerSeriesStartsLate', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 300; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.LoadExpectation( + model, tr.model.um.LOAD_SUBTYPE_NAMES.SUCCESSFUL, 0, 500)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 500, 500)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert(containsData(histograms, 'story:power')); + assert(!containsData(histograms, 'load:energy')); + assert(!containsData(histograms, 'after_load:power')); + }); + + test('powerMetric_powerSeriesEndsEarly', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 700; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.LoadExpectation( + model, tr.model.um.LOAD_SUBTYPE_NAMES.SUCCESSFUL, 0, 500)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 500, 500)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert(containsData(histograms, 'story:power')); + assert(!containsData(histograms, 'load:energy')); + assert(!containsData(histograms, 'after_load:power')); + }); + + test('powerMetric_generic_oneStageEachType_irBeyondChrome', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const rendererProcess = model.getOrCreateProcess(1234); + const mainThread = rendererProcess.getOrCreateThread(1); + mainThread.name = 'CrRendererMain'; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.ResponseExpectation( + model, tr.model.um.INITIATOR_TYPE.SCROLL, 0, 500)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 500, 1500)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.closeTo(getMetricValueSum(histograms, + 'scroll_response:energy'), 125, 0.5); + assert.closeTo(getMetricValueAvg(histograms, + 'idle:power'), 750, 0.5); + assert.closeTo(getMetricValueAvg(histograms, + 'story:power'), 500, 0.5); + }); + + test('powerMetric_story_minAvgMax', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.ResponseExpectation( + model, tr.model.um.INITIATOR_TYPE.SCROLL, 0, 500)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 500, 1500)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.closeTo(getMetricValueMin(histograms, 'story:power'), 0, .01); + assert.closeTo(getMetricValueAvg(histograms, 'story:power'), 500, .01); + assert.closeTo(getMetricValueMax(histograms, 'story:power'), 1000, .01); + }); + + test('powerMetric_generic_oneUEBeforeChrome', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const rendererProcess = model.getOrCreateProcess(1234); + const mainThread = rendererProcess.getOrCreateThread(1); + mainThread.name = 'CrRendererMain'; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 500, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 0, 300)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 300, 1000)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.closeTo(getMetricValueAvg(histograms, + 'idle:power'), 750, 0.5); + }); + + test('powerMetric_generic_multipleStagesEachType', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const rendererProcess = model.getOrCreateProcess(1234); + const mainThread = rendererProcess.getOrCreateThread(1); + mainThread.name = 'CrRendererMain'; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.ResponseExpectation( + model, tr.model.um.INITIATOR_TYPE.SCROLL, 0, 200)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 200, 300)); + model.userModel.expectations.push(new tr.model.um.ResponseExpectation( + model, tr.model.um.INITIATOR_TYPE.SCROLL, 500, 400)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 900, 100)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.closeTo(getMetricValueSum(histograms, + 'scroll_response:energy'), 300, 0.6); + assert.closeTo(getMetricValueAvg(histograms, + 'idle:power'), 500, 0.6); + assert.strictEqual(getMetricValueCount(histograms, + 'scroll_response:energy'), 2); + assert.strictEqual(getMetricValueCount(histograms, + 'idle:power'), 400); + }); + + test('powerMetric_loading_oneInterval_samplesBeyondChrome', function() { + // Interval of load is [200, 15400]. + // Trace goes until 22150. + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({ + cat: 'disabled-by-default-network', + title: 'ResourceLoad', + start: 200, + duration: 5.0 + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com'}); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 9180, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 9200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'domContentLoadedEventEnd', + start: 9200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 9350, + duration: 100, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 11150, + duration: 100, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 12550, + duration: 100, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 14900, + duration: 500, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 22150, + duration: 10, + })); + + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 15400; i++) { + model.device.powerSeries.addPowerSample(i, 20); + } + for (let i = 15401; i <= 22160; i++) { + model.device.powerSeries.addPowerSample(i, 10); + } + for (let i = 22160; i <= 30000; i++) { + model.device.powerSeries.addPowerSample(i, 10); + } + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + // Energy for first load is 20 W * 15.2 s + // (interval from 0.2 s to 15.4 s) + assert.closeTo( + getMetricValueAvg(histograms, 'load:energy'), 304, 0.1); + }); + + test('powerMetric_loading_noMeaningfulPaint', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com'}); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 9350, + duration: 100, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 11150, + duration: 100, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 12550, + duration: 100, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 14950, + duration: 500, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 22150, + duration: 10, + })); + + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 15400; i++) { + model.device.powerSeries.addPowerSample(i, 20); + } + for (let i = 15401; i <= 22160; i++) { + model.device.powerSeries.addPowerSample(i, 10); + } + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + // Energy for first load is 20 W * 15.2 s + // (interval from 0.2 s to 15.4 s) + assert.isUndefined(getMetricValueCount(histograms, 'after_load:power')); + }); + + test('powerMetric_scroll_oneStageEachType', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.AnimationExpectation( + model, tr.model.um.INITIATOR_TYPE.SCROLL, 0, 500)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 500, 500)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.closeTo(getMetricValueAvg(histograms, + 'scroll_animation:power'), 250, 0.5); + }); + + test('powerMetric_scroll_multipleStagesEachType', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.AnimationExpectation( + model, tr.model.um.INITIATOR_TYPE.SCROLL, 0, 200)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 200, 300)); + model.userModel.expectations.push(new tr.model.um.AnimationExpectation( + model, tr.model.um.INITIATOR_TYPE.SCROLL, 500, 200)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 700, 300)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.closeTo(getMetricValueAvg(histograms, + 'scroll_animation:power'), 350, 0.6); + }); + + test('powerMetric_video_oneStageEachType', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.AnimationExpectation( + model, tr.model.um.INITIATOR_TYPE.VIDEO, 0, 500)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 500, 500)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.closeTo(getMetricValueAvg(histograms, 'video_animation:power'), + 250, 0.5); + }); + + test('powerMetric_video_multipleStagesEachType', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 0, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 1000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.device.powerSeries = new tr.model.PowerSeries(model.device); + for (let i = 0; i <= 1000; i++) { + model.device.powerSeries.addPowerSample(i, i); + } + model.userModel.expectations.push(new tr.model.um.AnimationExpectation( + model, tr.model.um.INITIATOR_TYPE.VIDEO, 0, 200)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 200, 300)); + model.userModel.expectations.push(new tr.model.um.AnimationExpectation( + model, tr.model.um.INITIATOR_TYPE.VIDEO, 500, 200)); + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 700, 300)); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.powerMetric(histograms, model); + + assert.closeTo(getMetricValueAvg(histograms, 'video_animation:power'), + 350, 0.6); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric.html new file mode 100644 index 00000000000..911c2d66d48 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric.html @@ -0,0 +1,162 @@ +<!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="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/system_health/utils.html"> +<link rel="import" href="/tracing/model/user_model/animation_expectation.html"> +<link rel="import" href="/tracing/model/user_model/load_expectation.html"> +<link rel="import" href="/tracing/model/user_model/response_expectation.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + // In the case of Response, Load, and DiscreteAnimation UEs, Responsiveness is + // derived from the time between when the user thinks they begin an interation + // (expectedStart) and the time when the screen first changes to reflect the + // interaction (actualEnd). There may be a delay between expectedStart and + // when chrome first starts processing the interaction (actualStart) if the + // main thread is busy. The user doesn't know when actualStart is, they only + // know when expectedStart is. User responsiveness, by definition, considers + // only what the user experiences, so "duration" is defined as actualEnd - + // expectedStart. + + function computeAnimationThroughput(animationExpectation) { + if (animationExpectation.frameEvents === undefined || + animationExpectation.frameEvents.length === 0) { + throw new Error('Animation missing frameEvents ' + + animationExpectation.stableId); + } + + const durationInS = tr.b.convertUnit(animationExpectation.duration, + tr.b.UnitPrefixScale.METRIC.MILLI, + tr.b.UnitPrefixScale.METRIC.NONE); + return animationExpectation.frameEvents.length / durationInS; + } + + function computeAnimationframeTimeDiscrepancy(animationExpectation) { + if (animationExpectation.frameEvents === undefined || + animationExpectation.frameEvents.length === 0) { + throw new Error('Animation missing frameEvents ' + + animationExpectation.stableId); + } + + let frameTimestamps = animationExpectation.frameEvents; + frameTimestamps = frameTimestamps.toArray().map(function(event) { + return event.start; + }); + + const absolute = true; + return tr.b.math.Statistics.timestampsDiscrepancy( + frameTimestamps, absolute); + } + + /** + * @param {!tr.v.HistogramSet} histograms + * @param {!tr.model.Model} model + * @param {!Object=} opt_options + */ + function responsivenessMetric(histograms, model, opt_options) { + const responseNumeric = new tr.v.Histogram('response latency', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + tr.v.HistogramBinBoundaries.createLinear(100, 1e3, 50)); + const throughputNumeric = new tr.v.Histogram('animation throughput', + tr.b.Unit.byName.unitlessNumber_biggerIsBetter, + tr.v.HistogramBinBoundaries.createLinear(10, 60, 10)); + const frameTimeDiscrepancyNumeric = new tr.v.Histogram( + 'animation frameTimeDiscrepancy', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + tr.v.HistogramBinBoundaries.createLinear(0, 1e3, 50). + addExponentialBins(1e4, 10)); + const latencyNumeric = new tr.v.Histogram('animation latency', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + tr.v.HistogramBinBoundaries.createLinear(0, 300, 60)); + + model.userModel.expectations.forEach(function(ue) { + if (opt_options && opt_options.rangeOfInterest && + !opt_options.rangeOfInterest.intersectsExplicitRangeInclusive( + ue.start, ue.end)) { + return; + } + + const sampleDiagnosticMap = tr.v.d.DiagnosticMap.fromObject( + {relatedEvents: new tr.v.d.RelatedEventSet([ue])}); + + // Responsiveness is not defined for Idle or Startup expectations. + if (ue instanceof tr.model.um.IdleExpectation) { + return; + } else if (ue instanceof tr.model.um.StartupExpectation) { + return; + } else if (ue instanceof tr.model.um.LoadExpectation) { + // This is already covered by loadingMetric. + } else if (ue instanceof tr.model.um.ResponseExpectation) { + responseNumeric.addSample(ue.duration, sampleDiagnosticMap); + } else if (ue instanceof tr.model.um.AnimationExpectation) { + if (ue.frameEvents === undefined || ue.frameEvents.length === 0) { + // Ignore animation stages that do not have associated frames: + // https://github.com/catapult-project/catapult/issues/2446 + return; + } + const throughput = computeAnimationThroughput(ue); + if (throughput === undefined) { + throw new Error('Missing throughput for ' + + ue.stableId); + } + + throughputNumeric.addSample(throughput, sampleDiagnosticMap); + + const frameTimeDiscrepancy = computeAnimationframeTimeDiscrepancy(ue); + if (frameTimeDiscrepancy === undefined) { + throw new Error('Missing frameTimeDiscrepancy for ' + + ue.stableId); + } + + frameTimeDiscrepancyNumeric.addSample( + frameTimeDiscrepancy, sampleDiagnosticMap); + + ue.associatedEvents.forEach(function(event) { + if (!(event instanceof tr.e.cc.InputLatencyAsyncSlice)) { + return; + } + + latencyNumeric.addSample(event.duration, sampleDiagnosticMap); + }); + } else { + throw new Error('Unrecognized stage for ' + ue.stableId); + } + }); + + [ + responseNumeric, throughputNumeric, frameTimeDiscrepancyNumeric, + latencyNumeric + ].forEach(function(numeric) { + numeric.customizeSummaryOptions({ + avg: true, + max: true, + min: true, + std: true + }); + }); + + histograms.addHistogram(responseNumeric); + histograms.addHistogram(throughputNumeric); + histograms.addHistogram(frameTimeDiscrepancyNumeric); + histograms.addHistogram(latencyNumeric); + } + + tr.metrics.MetricRegistry.register(responsivenessMetric, { + supportsRangeOfInterest: true, + requiredCategories: ['rail'], + }); + + return { + responsivenessMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric_test.html new file mode 100644 index 00000000000..dbe10addd27 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/responsiveness_metric_test.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/system_health/responsiveness_metric.html"> +<link rel="import" href="/tracing/model/slice_group.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createModel(collectionOfFrameTimestamps) { + const opts = { + customizeModelCallback(model) { + const thread = tr.c.TestUtils.newFakeThread(); + collectionOfFrameTimestamps.forEach(function(frameTimestamps) { + frameTimestamps.sort((a, b) => a - b); + const ue = new tr.model.um.AnimationExpectation( + model, 'test', frameTimestamps[0], + frameTimestamps[frameTimestamps.length - 1]); + const group = new tr.model.SliceGroup(thread); + frameTimestamps.forEach(function(time) { + group.pushSlice(tr.c.TestUtils.newSliceEx({ + title: tr.model.helpers.IMPL_RENDERING_STATS, + start: time, + end: time, + cpuStart: time, + cpuEnd: time + })); + }); + group.createSubSlices(); + group.slices.forEach(function(slice) { + ue.associatedEvents.push(slice); + }); + model.userModel.expectations.push(ue); + model.userModel.expectations.push( + new tr.model.um.StartupExpectation(model, 0, 1)); + model.userModel.expectations.push( + new tr.model.um.ResponseExpectation(model, 'test', 1, 1)); + }); + } + }; + return tr.c.TestUtils.newModelWithEvents([], opts); + } + + test('animationThroughputNoFrames', function() { + const model = createModel([[]]); + const valueList = new tr.v.HistogramSet(); + tr.metrics.sh.responsivenessMetric(valueList, model); + const value = valueList.getHistogramNamed('animation throughput'); + assert.strictEqual(value.numValues, 0); + }); + + test('animationThroughputFramesAndNoFrames', function() { + const model = createModel([[0, 100], []]); + const valueList = new tr.v.HistogramSet(); + tr.metrics.sh.responsivenessMetric(valueList, model); + const value = valueList.getHistogramNamed('animation throughput'); + assert.strictEqual(value.average, 20); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/utils.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/utils.html new file mode 100644 index 00000000000..92bb7c71fb1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/utils.html @@ -0,0 +1,152 @@ +<!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="/tracing/extras/chrome/chrome_processes.html"> +<link rel="import" href="/tracing/model/user_model/user_expectation.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + // Returns a weight for this score. + // score should be a number between 0 and 1 inclusive. + // This function is expected to be passed to tr.b.math.Statistics.weightedMean + // as its weightCallback. + function perceptualBlend(ir, index, score) { + // Lower scores are exponentially more important than higher scores + // due to the Peak-end rule. + // Other than that general rule, there is no specific reasoning behind this + // specific formula -- it is fairly arbitrary. + return Math.exp(1 - score); + } + + function filterExpectationsByRange(irs, opt_range) { + const filteredExpectations = []; + irs.forEach(function(ir) { + if (!(ir instanceof tr.model.um.UserExpectation)) return; + + if (!opt_range || + opt_range.intersectsExplicitRangeInclusive(ir.start, ir.end)) { + filteredExpectations.push(ir); + } + }); + return filteredExpectations; + } + + /** + * Splits the global memory dumps in |model| by browser name. + * + * @param {!tr.Model} model The trace model from which the global dumps + * should be extracted. + * @param {!tr.b.math.Range=} opt_rangeOfInterest If provided, global memory + * dumps that do not inclusively intersect the range will be skipped. + * @return {!Map<string, !Array<!tr.model.GlobalMemoryDump>} A map from + * browser names to the associated global memory dumps. + */ + function splitGlobalDumpsByBrowserName(model, opt_rangeOfInterest) { + const chromeModelHelper = + model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + const browserNameToGlobalDumps = new Map(); + const globalDumpToBrowserHelper = new WeakMap(); + + // 1. For each browser process in the model, add its global memory dumps to + // |browserNameToGlobalDumps|. |chromeModelHelper| can be undefined if + // it fails to find any browser, renderer or GPU process (see + // tr.model.helpers.ChromeModelHelper.supportsModel). + + if (chromeModelHelper) { + chromeModelHelper.browserHelpers.forEach(function(helper) { + // Retrieve the associated global memory dumps and check that they + // haven't been classified as belonging to another browser process. + const globalDumps = skipDumpsThatDoNotIntersectRange( + helper.process.memoryDumps.map(d => d.globalMemoryDump), + opt_rangeOfInterest); + globalDumps.forEach(function(globalDump) { + const existingHelper = globalDumpToBrowserHelper.get(globalDump); + if (existingHelper !== undefined) { + throw new Error('Memory dump ID clash across multiple browsers ' + + 'with PIDs: ' + existingHelper.pid + ' and ' + helper.pid); + } + globalDumpToBrowserHelper.set(globalDump, helper); + }); + + makeKeyUniqueAndSet(browserNameToGlobalDumps, + tr.e.chrome.chrome_processes.canonicalizeName(helper.browserName), + globalDumps); + }); + } + + // 2. If any global memory dump does not have any associated browser + // process for some reason, associate it with an 'unknown_browser' browser + // so that we don't lose the data. + + const unclassifiedGlobalDumps = skipDumpsThatDoNotIntersectRange( + model.globalMemoryDumps.filter(g => !globalDumpToBrowserHelper.has(g)), + opt_rangeOfInterest); + if (unclassifiedGlobalDumps.length > 0) { + makeKeyUniqueAndSet( + browserNameToGlobalDumps, 'unknown_browser', unclassifiedGlobalDumps); + } + + return browserNameToGlobalDumps; + } + + /** + * Function for adding entries with duplicate keys to a map without + * overriding existing entries. + * + * This is achieved by appending numeric indices (2, 3, 4, ...) to duplicate + * keys. Example: + * + * const map = new Map(); + * // map = Map {}. + * + * makeKeyUniqueAndSet(map, 'key', 'a'); + * // map = Map {"key" => "a"}. + * + * makeKeyUniqueAndSet(map, 'key', 'b'); + * // map = Map {"key" => "a", "key2" => "b"}. + * ^^^^ + * makeKeyUniqueAndSet(map, 'key', 'c'); + * // map = Map {"key" => "a", "key2" => "b", "key3" => "c"}. + * ^^^^ ^^^^ + */ + function makeKeyUniqueAndSet(map, key, value) { + let uniqueKey = key; + let nextIndex = 2; + while (map.has(uniqueKey)) { + uniqueKey = key + nextIndex; + nextIndex++; + } + map.set(uniqueKey, value); + } + + function skipDumpsThatDoNotIntersectRange(dumps, opt_range) { + if (!opt_range) return dumps; + return dumps.filter(d => opt_range.intersectsExplicitRangeInclusive( + d.start, d.end)); + } + + /** + * Returns true if |category| is one of the categories of |event|, and |event| + * has title |title|. + * + * TODO(dproy): Make this a method on a suitable parent class of the + * event/slice classes that are used with this function. + */ + function hasCategoryAndName(event, category, title) { + return event.title === title && event.category && + tr.b.getCategoryParts(event.category).includes(category); + } + + return { + hasCategoryAndName, + filterExpectationsByRange, + perceptualBlend, + splitGlobalDumpsByBrowserName + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric.html new file mode 100644 index 00000000000..f7bea627333 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/system_health/utils.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.sh', function() { + function webviewStartupMetric(histograms, model) { + const startupWallHist = new tr.v.Histogram('webview_startup_wall_time', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter); + startupWallHist.description = 'WebView startup wall time'; + const startupCPUHist = new tr.v.Histogram('webview_startup_cpu_time', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter); + startupCPUHist.description = 'WebView startup CPU time'; + const loadWallHist = new tr.v.Histogram('webview_url_load_wall_time', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter); + loadWallHist.description = 'WebView blank URL load wall time'; + const loadCPUHist = new tr.v.Histogram('webview_url_load_cpu_time', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter); + loadCPUHist.description = 'WebView blank URL load CPU time'; + + // TODO(alexandermont): Only iterate over the processes and threads that + // could contain these events. + for (const slice of model.getDescendantEvents()) { + if (!(slice instanceof tr.model.ThreadSlice)) continue; + + // WebViewStartupInterval is the title of the section of code that is + // entered (via android.os.Trace.beginSection) when WebView is started + // up. This value is defined in TelemetryActivity.java. + if (slice.title === 'WebViewStartupInterval') { + startupWallHist.addSample(slice.duration); + startupCPUHist.addSample(slice.cpuDuration); + } + + // WebViewBlankUrlLoadInterval is the title of the section of code + // that is entered (via android.os.Trace.beginSection) when WebView + // is started up. This value is defined in TelemetryActivity.java. + if (slice.title === 'WebViewBlankUrlLoadInterval') { + loadWallHist.addSample(slice.duration); + loadCPUHist.addSample(slice.cpuDuration); + } + } + + histograms.addHistogram(startupWallHist); + histograms.addHistogram(startupCPUHist); + histograms.addHistogram(loadWallHist); + histograms.addHistogram(loadCPUHist); + } + + tr.metrics.MetricRegistry.register(webviewStartupMetric); + + return { + webviewStartupMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric_test.html new file mode 100644 index 00000000000..9f3c15224d9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/system_health/webview_startup_metric_test.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/system_health/webview_startup_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + function makeTestModel() { + return tr.c.TestUtils.newModel(function(model) { + const mainThread = model.getOrCreateProcess(1).getOrCreateThread(2); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'webview', + title: 'WebViewStartupInterval', + start: 200, + duration: 42.0, + cpuStart: 150, + cpuDuration: 32.0 + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'webview', + title: 'WebViewBlankUrlLoadInterval', + start: 250, + duration: 27.0, + cpuStart: 190, + cpuDuration: 17.0 + })); + }); + } + + test('webviewStartupMetric', function() { + const histograms = new tr.v.HistogramSet(); + tr.metrics.sh.webviewStartupMetric(histograms, makeTestModel()); + assert.closeTo(42, histograms.getHistogramNamed( + 'webview_startup_wall_time').average, 1e-2); + assert.closeTo(32, histograms.getHistogramNamed( + 'webview_startup_cpu_time').average, 1e-2); + assert.closeTo(27, histograms.getHistogramNamed( + 'webview_url_load_wall_time').average, 1e-2); + assert.closeTo(17, histograms.getHistogramNamed( + 'webview_url_load_cpu_time').average, 1e-2); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric.html new file mode 100644 index 00000000000..a4b456353c3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/metrics/metric_registry.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.tabs', function() { + function tabsMetric(histograms, model, opt_options) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + if (!chromeHelper) { + // Chrome isn't present. + return; + } + + const tabSwitchRequestDelays = []; + const TAB_SWITCHING_REQUEST_TITLE = 'TabSwitchVisibilityRequest'; + // initialization, *startTabSwitchVisibilityRequest stores the + // first legal TabSwitchVisibilityRequest's start time. + let startTabSwitchVisibilityRequest = Number.MAX_SAFE_INTEGER; + for (const helper of chromeHelper.browserHelpers) { + if (!helper.mainThread) continue; + for (const slice of helper.mainThread.asyncSliceGroup.slices) { + if (slice.title === TAB_SWITCHING_REQUEST_TITLE && !slice.error) { + tabSwitchRequestDelays.push(slice.duration); + // find the first legal TabSwitchVisibilityRequest's start time + if (slice.start < startTabSwitchVisibilityRequest) { + startTabSwitchVisibilityRequest = slice.start; + } + } + } + } + + histograms.createHistogram('tab_switching_request_delay', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + tabSwitchRequestDelays, { + description: 'Delay before tab-request is made', + summaryOptions: {sum: false}}); + + const tabSwitchLatencies = []; + const TAB_SWITCHING_SLICE_TITLE = 'TabSwitching::Latency'; + + function extractLatencyFromHelpers(helpers, legacy) { + for (const helper of helpers) { + if (!helper.mainThread) { + continue; + } + const thread = helper.mainThread; + for (const slice of thread.asyncSliceGroup.slices) { + if (slice.title === TAB_SWITCHING_SLICE_TITLE && + (legacy || slice.args.latency) && + slice.start > startTabSwitchVisibilityRequest) { + // push legal tabSwitchLatency which only starts after the + // first legal TabSwitchVisibilityRequest's start time + tabSwitchLatencies.push( + legacy ? slice.duration : slice.args.latency); + } + } + } + } + + extractLatencyFromHelpers(chromeHelper.browserHelpers); + extractLatencyFromHelpers(Object.values(chromeHelper.rendererHelpers)); + + if (tabSwitchLatencies.length === 0) { + extractLatencyFromHelpers(chromeHelper.browserHelpers, true); + } + + histograms.createHistogram('tab_switching_latency', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + tabSwitchLatencies, { description: 'Tab switching time in ms', + summaryOptions: {sum: false}}); + } + + tr.metrics.MetricRegistry.register(tabsMetric, { + supportsRangeOfInterest: false, + }); + + return { + tabsMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric_test.html new file mode 100644 index 00000000000..a6d3f6b9606 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/tabs_metric_test.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/tabs_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('tabsMetric_tabSwitching', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserThread = setUpBrowserThread(model); + addTabSwitchingData(browserThread); + + const rendererMainThread = setUpRendererMainThread(model); + addTabSwitchingData(rendererMainThread); + + const group = browserThread.asyncSliceGroup; + group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency', + title: 'TabSwitchVisibilityRequest', start: 3, duration: 10})); + group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency', + title: 'TabSwitchVisibilityRequest', start: 15, duration: 6})); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.tabs.tabsMetric(histograms, model); + const hist = histograms.getHistogramNamed('tab_switching_latency'); + assert.strictEqual(hist.max, 43.0); + assert.strictEqual(hist.min, 24.0); + assert.strictEqual(hist.average, 33.0); + assert.strictEqual(hist.numValues, 6); + }); + + test('tabsMetric_tabSwitching_legacy', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserThread = setUpBrowserThread(model); + addTabSwitchingData(browserThread, true); + + const group = browserThread.asyncSliceGroup; + group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency', + title: 'TabSwitchVisibilityRequest', start: 3, duration: 10})); + group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency', + title: 'TabSwitchVisibilityRequest', start: 15, duration: 6})); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.tabs.tabsMetric(histograms, model); + const hist = histograms.getHistogramNamed('tab_switching_latency'); + assert.strictEqual(hist.max, 37.0); + assert.strictEqual(hist.min, 14.0); + assert.strictEqual(hist.average, 25.0); + assert.strictEqual(hist.numValues, 3); + }); + + test('tabsMetric_tabSwitchRequestDelay', function() { + const model = tr.c.TestUtils.newModel((model) => { + const browserThread = setUpBrowserThread(model); + const group = browserThread.asyncSliceGroup; + group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency', + title: 'TabSwitchVisibilityRequest', start: 0, duration: 10})); + group.push(tr.c.TestUtils.newAsyncSliceEx({cat: 'latency', + title: 'TabSwitchVisibilityRequest', start: 15, duration: 6})); + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.tabs.tabsMetric(histograms, model); + const hist = histograms.getHistogramNamed('tab_switching_request_delay'); + assert.strictEqual(hist.average, 8); + }); + + function setUpBrowserThread(model) { + const BROWSER_PROCESS_ID = 1234; + const browserProcess = model.getOrCreateProcess(BROWSER_PROCESS_ID); + const browserThread = browserProcess.getOrCreateThread(2); + browserThread.name = 'CrBrowserMain'; + return browserThread; + } + + function setUpRendererMainThread(model) { + const RENDERER_PROCESS_ID = 2345; + const rendererProcess = model.getOrCreateProcess(RENDERER_PROCESS_ID); + const mainThread = rendererProcess.getOrCreateThread(23); + mainThread.name = 'CrRendererMain'; + return mainThread; + } + + function addTabSwitchingData(thread, legacy) { + const group = thread.asyncSliceGroup; + group.push(tr.c.TestUtils.newAsyncSliceEx({ + cat: 'latency', + title: 'TabSwitching::Latency', + start: 2, + duration: 24, + args: legacy ? {} : {latency: 24 }, + })); + group.push(tr.c.TestUtils.newAsyncSliceEx({ + cat: 'latency', + title: 'TabSwitching::Latency', + start: 4, + duration: 24, + args: legacy ? {} : {latency: 24 }, + })); + group.push(tr.c.TestUtils.newAsyncSliceEx({ + cat: 'latency', + title: 'TabSwitching::Latency', + start: 5, + duration: 14, + args: legacy ? {} : {latency: 32 }, + })); + group.push(tr.c.TestUtils.newAsyncSliceEx({ + cat: 'latency', + title: 'TabSwitching::Latency', + start: 6, + duration: 37, + args: legacy ? {} : {latency: 43 }, + })); + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric.html new file mode 100644 index 00000000000..332a2b1cd04 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric.html @@ -0,0 +1,230 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/utils.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/diagnostics/diagnostic_map.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics', function() { + const MEMORY_INFRA_TRACING_CATEGORY = 'disabled-by-default-memory-infra'; + + const TIME_BOUNDARIES = tr.v.HistogramBinBoundaries.createExponential( + 1e-3, 1e5, 30); + + const BYTE_BOUNDARIES = tr.v.HistogramBinBoundaries.createExponential( + 1, 1e9, 30); + + const COUNT_BOUNDARIES = tr.v.HistogramBinBoundaries.createExponential( + 1, 1e5, 30); + + // By default, we store a single value, so we only need one of the + // statistics to keep track. We choose the average for that. + const SUMMARY_OPTIONS = tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS; + + // Adds histograms specific to memory-infra dumps. + function addMemoryInfraHistograms( + histograms, model, categoryNamesToTotalEventSizes) { + const memoryDumpCount = model.globalMemoryDumps.length; + if (memoryDumpCount === 0) return; + + let totalOverhead = 0; + let nonMemoryInfraThreadOverhead = 0; + const overheadByProvider = {}; + for (const process of Object.values(model.processes)) { + for (const thread of Object.values(process.threads)) { + for (const slice of Object.values(thread.sliceGroup.slices)) { + if (slice.category !== MEMORY_INFRA_TRACING_CATEGORY) continue; + + totalOverhead += slice.duration; + if (thread.name !== 'MemoryInfra') { + nonMemoryInfraThreadOverhead += slice.duration; + } + if (slice.args && slice.args['dump_provider.name']) { + const providerName = slice.args['dump_provider.name']; + let durationAndCount = overheadByProvider[providerName]; + if (durationAndCount === undefined) { + overheadByProvider[providerName] = durationAndCount = + {duration: 0, count: 0}; + } + durationAndCount.duration += slice.duration; + durationAndCount.count++; + } + } + } + } + + histograms.createHistogram('memory_dump_cpu_overhead', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + totalOverhead / memoryDumpCount, { + binBoundaries: TIME_BOUNDARIES, + description: + 'Average CPU overhead on all threads per memory-infra dump', + summaryOptions: SUMMARY_OPTIONS, + }); + + histograms.createHistogram('nonmemory_thread_memory_dump_cpu_overhead', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + nonMemoryInfraThreadOverhead / memoryDumpCount, { + binBoundaries: TIME_BOUNDARIES, + description: 'Average CPU overhead on non-memory-infra threads ' + + 'per memory-infra dump', + summaryOptions: SUMMARY_OPTIONS, + }); + + for (const [providerName, overhead] of Object.entries(overheadByProvider)) { + histograms.createHistogram(`${providerName}_memory_dump_cpu_overhead`, + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + overhead.duration / overhead.count, { + binBoundaries: TIME_BOUNDARIES, + description: + `Average CPU overhead of ${providerName} per OnMemoryDump call`, + summaryOptions: SUMMARY_OPTIONS, + }); + } + + const memoryInfraEventsSize = + categoryNamesToTotalEventSizes.get(MEMORY_INFRA_TRACING_CATEGORY); + const memoryInfraTraceBytesValue = new tr.v.Histogram( + 'total_memory_dump_size', + tr.b.Unit.byName.sizeInBytes_smallerIsBetter, BYTE_BOUNDARIES); + memoryInfraTraceBytesValue.description = + 'Total trace size of memory-infra dumps in bytes'; + memoryInfraTraceBytesValue.customizeSummaryOptions(SUMMARY_OPTIONS); + memoryInfraTraceBytesValue.addSample(memoryInfraEventsSize); + histograms.addHistogram(memoryInfraTraceBytesValue); + + const traceBytesPerDumpValue = new tr.v.Histogram( + 'memory_dump_size', + tr.b.Unit.byName.sizeInBytes_smallerIsBetter, BYTE_BOUNDARIES); + traceBytesPerDumpValue.description = + 'Average trace size of memory-infra dumps in bytes'; + traceBytesPerDumpValue.customizeSummaryOptions(SUMMARY_OPTIONS); + traceBytesPerDumpValue.addSample(memoryInfraEventsSize / memoryDumpCount); + histograms.addHistogram(traceBytesPerDumpValue); + } + + // TODO(charliea): The metrics in this file should be renamed to have names + // more consistent with those in the rest of the codebase + // (e.g. 'trace_size_growth_per_second', not 'Max event size in bytes per + // second'). + // https://github.com/catapult-project/catapult/issues/3233 + function tracingMetric(histograms, model) { + if (!model.stats.hasEventSizesinBytes) return; + + const eventStats = model.stats.allTraceEventStatsInTimeIntervals; + eventStats.sort((a, b) => a.timeInterval - b.timeInterval); + + const totalTraceBytes = eventStats.reduce( + (a, b) => a + b.totalEventSizeinBytes, 0); + + // We maintain a sliding window of records [start ... end-1] where end + // increments each time through the loop, and we move start just far enough + // to keep the window less than 1 second wide. Note that we need to compute + // the number of time intervals (i.e. units that timeInterval is given in) + // in one second to know how wide the sliding window should be. + let maxEventCountPerSec = 0; + let maxEventBytesPerSec = 0; + const INTERVALS_PER_SEC = Math.floor( + 1000 / model.stats.TIME_INTERVAL_SIZE_IN_MS); + + let runningEventNumPerSec = 0; + let runningEventBytesPerSec = 0; + let start = 0; + let end = 0; + + while (end < eventStats.length) { + // Slide the end marker forward. Moving the end marker from N + // to N+1 adds eventStats[N] to the sliding window. + runningEventNumPerSec += eventStats[end].numEvents; + runningEventBytesPerSec += eventStats[end].totalEventSizeinBytes; + end++; + + // Slide the start marker forward so that the time interval covered + // by the window is less than 1 second wide. + while ((eventStats[end - 1].timeInterval - + eventStats[start].timeInterval) >= INTERVALS_PER_SEC) { + runningEventNumPerSec -= eventStats[start].numEvents; + runningEventBytesPerSec -= eventStats[start].totalEventSizeinBytes; + start++; + } + + // Update maximum values. + maxEventCountPerSec = Math.max(maxEventCountPerSec, + runningEventNumPerSec); + maxEventBytesPerSec = Math.max(maxEventBytesPerSec, + runningEventBytesPerSec); + } + + const stats = model.stats.allTraceEventStats; + const categoryNamesToTotalEventSizes = ( + stats.reduce((map, stat) => ( + map.set(stat.category, + ((map.get(stat.category) || 0) + + stat.totalEventSizeinBytes))), new Map())); + + // Determine the category with the highest total event size. + const maxCatNameAndBytes = Array.from( + categoryNamesToTotalEventSizes.entries()).reduce( + (a, b) => ((b[1] >= a[1]) ? b : a)); + const maxEventBytesPerCategory = maxCatNameAndBytes[1]; + const categoryWithMaxEventBytes = maxCatNameAndBytes[0]; + + const maxEventCountPerSecValue = new tr.v.Histogram( + 'peak_event_rate', tr.b.Unit.byName.count_smallerIsBetter, + COUNT_BOUNDARIES); + maxEventCountPerSecValue.description = 'Max number of events per second'; + maxEventCountPerSecValue.customizeSummaryOptions(SUMMARY_OPTIONS); + maxEventCountPerSecValue.addSample(maxEventCountPerSec); + + const maxEventBytesPerSecValue = new tr.v.Histogram( + 'peak_event_size_rate', tr.b.Unit.byName.sizeInBytes_smallerIsBetter, + BYTE_BOUNDARIES); + maxEventBytesPerSecValue.description = 'Max event size in bytes per second'; + maxEventBytesPerSecValue.customizeSummaryOptions(SUMMARY_OPTIONS); + maxEventBytesPerSecValue.addSample(maxEventBytesPerSec); + + const totalTraceBytesValue = new tr.v.Histogram('trace_size', + tr.b.Unit.byName.sizeInBytes_smallerIsBetter, BYTE_BOUNDARIES); + totalTraceBytesValue.customizeSummaryOptions(SUMMARY_OPTIONS); + totalTraceBytesValue.addSample(totalTraceBytes); + + const biggestCategory = { + name: categoryWithMaxEventBytes, + size_in_bytes: maxEventBytesPerCategory + }; + + totalTraceBytesValue.diagnostics.set( + 'category_with_max_event_size', + new tr.v.d.GenericSet([biggestCategory])); + histograms.addHistogram(totalTraceBytesValue); + + maxEventCountPerSecValue.diagnostics.set( + 'category_with_max_event_size', + new tr.v.d.GenericSet([biggestCategory])); + histograms.addHistogram(maxEventCountPerSecValue); + + maxEventBytesPerSecValue.diagnostics.set( + 'category_with_max_event_size', + new tr.v.d.GenericSet([biggestCategory])); + histograms.addHistogram(maxEventBytesPerSecValue); + + addMemoryInfraHistograms(histograms, model, categoryNamesToTotalEventSizes); + } + + tr.metrics.MetricRegistry.register(tracingMetric); + + return { + tracingMetric, + // For testing only: + MEMORY_INFRA_TRACING_CATEGORY, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric_test.html new file mode 100644 index 00000000000..5fbffe98f94 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/tracing_metric_test.html @@ -0,0 +1,168 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/tracing_metric.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function makeModel(events, opt_track) { + return tr.c.TestUtils.newModelWithEvents([events], { + trackDetailedModelStats: opt_track + }); + } + + function getEventStringSize(events, indices) { + return indices.reduce(function(sum, index) { + return sum + JSON.stringify(events[index]).length; + }, 0); + } + + function checkDurationHistogram(histograms, metricName, expected) { + const histogram = histograms.getHistogramNamed(metricName); + assert.closeTo(1000 * histogram.average, expected, 0.1); + } + + test('tracingMetric_hasEventSizesInBytes', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'} + ]; + + const model = makeModel(JSON.stringify(events), true); + assert.isTrue(model.importOptions.trackDetailedModelStats); + tr.metrics.tracingMetric(histograms, model); + }); + + test('tracingMetric_totalTraceSize', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'} + ]; + const model = makeModel(JSON.stringify(events), true); + tr.metrics.tracingMetric(histograms, model); + + const eventStringSize = getEventStringSize(events, [0, 1]); + const histogram = histograms.getHistogramNamed('trace_size'); + assert.strictEqual(histogram.average, eventStringSize); + }); + + test('tracingMetric_maxValuePerSec', function() { + const ONE_SEC_IN_US = 1000000; + const events = [ + {name: 'a', pid: 52, ts: 1, cat: 'foo', ph: 'B'}, + {name: 'a', pid: 52, ts: ONE_SEC_IN_US + 1, cat: 'foo', ph: 'B'}, + {name: 'a', pid: 52, ts: 2 * ONE_SEC_IN_US + 1, cat: 'foo', ph: 'B'}, + {name: 'a', pid: 52, ts: 2 * ONE_SEC_IN_US + 3, cat: 'foo', ph: 'B'}, + {name: 'a', pid: 52, ts: ONE_SEC_IN_US + 2, cat: 'foo', ph: 'B'}, + {name: 'a', pid: 52, ts: 2 * ONE_SEC_IN_US + 2, cat: 'foo', ph: 'B'} + ]; + const model = makeModel(JSON.stringify(events), true); + const histograms = new tr.v.HistogramSet(); + tr.metrics.tracingMetric(histograms, model); + + const maxEventCountPerSec = 3; + let histogram = histograms.getHistogramNamed('peak_event_rate'); + assert.strictEqual(histogram.average, maxEventCountPerSec); + + const maxEventBytesPerSec = getEventStringSize(events, [2, 3, 5]); + histogram = histograms.getHistogramNamed('peak_event_size_rate'); + assert.strictEqual(histogram.average, maxEventBytesPerSec); + }); + + test('tracingMetric_diagnostics', function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 535, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'bb', args: {}, pid: 52, ts: 546, cat: 'bar', tid: 53, ph: 'E'}, + {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'bb', args: {}, pid: 52, ts: 578, cat: 'bar', tid: 53, ph: 'E'} + ]; + const model = makeModel(JSON.stringify(events), true); + tr.metrics.tracingMetric(histograms, model); + + const DIAGNOSTIC_HISTOGRAMS = [ + 'Max number of events per second', + 'Max event size in bytes per second', + 'Total trace size in bytes' + ]; + for (const histogram of histograms) { + if (!DIAGNOSTIC_HISTOGRAMS.includes(histogram.name)) continue; + + const d = histogram.diagnostics.get('category_with_max_event_size').value; + assert.strictEqual(d.name, 'foo'); + assert.strictEqual(d.size_in_bytes, getEventStringSize( + events, [0, 1, 3])); + } + }); + + test('tracingMetric_memoryInfraTracingMetrics', function() { + const MEMORY_INFRA_TRACING_CATEGORY = + tr.metrics.MEMORY_INFRA_TRACING_CATEGORY; + const histograms = new tr.v.HistogramSet(); + const events = [ + {name: 'OnMemoryDump', pid: 1, ts: 510, tid: 1, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp1'}, dur: 4}, // @suppress longLineCheck + {name: 'OnMemoryDump', pid: 1, ts: 520, tid: 7, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp2'}, dur: 15}, // @suppress longLineCheck + {name: 'OnMemoryDump', pid: 1, ts: 530, tid: 7, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp3'}, dur: 5}, // @suppress longLineCheck + {name: 'OnMemoryDump', pid: 2, ts: 510, tid: 2, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp1'}, dur: 9}, // @suppress longLineCheck + {name: 'OnMemoryDump', pid: 2, ts: 520, tid: 8, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp2'}, dur: 17}, // @suppress longLineCheck + {name: 'OnMemoryDump', pid: 2, ts: 530, tid: 8, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp3'}, dur: 7}, // @suppress longLineCheck + {name: 'OnMemoryDump', pid: 2, ts: 540, tid: 3, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {'dump_provider.name': 'mdp4'}, dur: 8}, // @suppress longLineCheck + {name: 'ProcessDumps', pid: 1, ts: 550, tid: 1, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {guid: 4}, dur: 8}, // @suppress longLineCheck + {name: 'ProcessDumps', pid: 2, ts: 540, tid: 2, ph: 'X', cat: MEMORY_INFRA_TRACING_CATEGORY, args: {guid: 4}, dur: 18}, // @suppress longLineCheck + {name: 'thread_name', pid: 1, ts: 0, tid: 1, ph: 'M', cat: '__metadata', args: {name: 'CrBrowsermain'}}, // @suppress longLineCheck + {name: 'thread_name', pid: 1, ts: 0, tid: 7, ph: 'M', cat: '__metadata', args: {name: 'MemoryInfra'}}, // @suppress longLineCheck + {name: 'thread_name', pid: 2, ts: 0, tid: 2, ph: 'M', cat: '__metadata', args: {name: 'CrRendererMain'}}, // @suppress longLineCheck + {name: 'thread_name', pid: 2, ts: 0, tid: 8, ph: 'M', cat: '__metadata', args: {name: 'MemoryInfra'}}, // @suppress longLineCheck + {name: 'thread_name', pid: 2, ts: 0, tid: 3, ph: 'M', cat: '__metadata', args: {name: 'Compositor'}} // @suppress longLineCheck + ]; + + const model = makeModel(JSON.stringify(events), true); + tr.model.MemoryDumpTestUtils.addGlobalMemoryDump(model, {ts: 550}); + tr.metrics.tracingMetric(histograms, model); + + const memoryCategorySize = events.filter( + slice => slice.cat === MEMORY_INFRA_TRACING_CATEGORY).reduce( + (acc, slice) => acc + JSON.stringify(slice).length, 0); + const totalSizeHistogram = histograms.getHistogramNamed( + 'total_memory_dump_size'); + assert.strictEqual(totalSizeHistogram.average, memoryCategorySize); + const sizePerDumpHistogram = histograms.getHistogramNamed( + 'memory_dump_size'); + assert.strictEqual(sizePerDumpHistogram.average, memoryCategorySize); + + checkDurationHistogram(histograms, 'mdp1_memory_dump_cpu_overhead', 6.5); + checkDurationHistogram(histograms, 'mdp2_memory_dump_cpu_overhead', 16); + checkDurationHistogram(histograms, 'mdp3_memory_dump_cpu_overhead', 6); + checkDurationHistogram(histograms, 'mdp4_memory_dump_cpu_overhead', 8); + checkDurationHistogram( + histograms, 'nonmemory_thread_memory_dump_cpu_overhead', 47); + checkDurationHistogram(histograms, 'memory_dump_cpu_overhead', 91); + }); + + test('tracingMetric_traceImportDurationMetricWithoutTrackDetailedModelStats', + function() { + const histograms = new tr.v.HistogramSet(); + const events = [ + {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'} + ]; + + const model = makeModel(JSON.stringify(events), false); + assert.isFalse(model.importOptions.trackDetailedModelStats); + tr.metrics.tracingMetric(histograms, model); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric.html new file mode 100644 index 00000000000..87992515679 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric.html @@ -0,0 +1,228 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.v8', function() { + const CUSTOM_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear( + 4, 200, 100); + + function computeExecuteMetrics(histograms, model) { + const cpuTotalExecution = new tr.v.Histogram('v8_execution_cpu_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + cpuTotalExecution.description = 'cpu total time spent in script execution'; + const wallTotalExecution = new tr.v.Histogram('v8_execution_wall_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + wallTotalExecution.description = + 'wall total time spent in script execution'; + const cpuSelfExecution = new tr.v.Histogram('v8_execution_cpu_self', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + cpuSelfExecution.description = 'cpu self time spent in script execution'; + const wallSelfExecution = new tr.v.Histogram('v8_execution_wall_self', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + wallSelfExecution.description = 'wall self time spent in script execution'; + + for (const e of model.findTopmostSlicesNamed('V8.Execute')) { + cpuTotalExecution.addSample(e.cpuDuration); + wallTotalExecution.addSample(e.duration); + cpuSelfExecution.addSample(e.cpuSelfTime); + wallSelfExecution.addSample(e.selfTime); + } + + histograms.addHistogram(cpuTotalExecution); + histograms.addHistogram(wallTotalExecution); + histograms.addHistogram(cpuSelfExecution); + histograms.addHistogram(wallSelfExecution); + } + + function computeParseLazyMetrics(histograms, model) { + const cpuSelfParseLazy = new tr.v.Histogram('v8_parse_lazy_cpu_self', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + cpuSelfParseLazy.description = + 'cpu self time spent performing lazy parsing'; + const wallSelfParseLazy = new tr.v.Histogram('v8_parse_lazy_wall_self', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + wallSelfParseLazy.description = + 'wall self time spent performing lazy parsing'; + + for (const e of model.findTopmostSlicesNamed('V8.ParseLazyMicroSeconds')) { + cpuSelfParseLazy.addSample(e.cpuSelfTime); + wallSelfParseLazy.addSample(e.selfTime); + } + for (const e of model.findTopmostSlicesNamed('V8.ParseLazy')) { + cpuSelfParseLazy.addSample(e.cpuSelfTime); + wallSelfParseLazy.addSample(e.selfTime); + } + + histograms.addHistogram(cpuSelfParseLazy); + histograms.addHistogram(wallSelfParseLazy); + } + + function computeCompileFullCodeMetrics(histograms, model) { + const cpuSelfCompileFullCode = new tr.v.Histogram( + 'v8_compile_full_code_cpu_self', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + cpuSelfCompileFullCode.description = + 'cpu self time spent performing compiling full code'; + const wallSelfCompileFullCode = new tr.v.Histogram( + 'v8_compile_full_code_wall_self', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + wallSelfCompileFullCode.description = + 'wall self time spent performing compiling full code'; + + for (const e of model.findTopmostSlicesNamed('V8.CompileFullCode')) { + cpuSelfCompileFullCode.addSample(e.cpuSelfTime); + wallSelfCompileFullCode.addSample(e.selfTime); + } + + histograms.addHistogram(cpuSelfCompileFullCode); + histograms.addHistogram(wallSelfCompileFullCode); + } + + function computeCompileIgnitionMetrics(histograms, model) { + const cpuSelfCompileIgnition = new tr.v.Histogram( + 'v8_compile_ignition_cpu_self', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + cpuSelfCompileIgnition.description = + 'cpu self time spent in compile ignition'; + const wallSelfCompileIgnition = new tr.v.Histogram( + 'v8_compile_ignition_wall_self', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + wallSelfCompileIgnition.description = + 'wall self time spent in compile ignition'; + + for (const e of model.findTopmostSlicesNamed('V8.CompileIgnition')) { + cpuSelfCompileIgnition.addSample(e.cpuSelfTime); + wallSelfCompileIgnition.addSample(e.selfTime); + } + + histograms.addHistogram(cpuSelfCompileIgnition); + histograms.addHistogram(wallSelfCompileIgnition); + } + + function computeRecompileMetrics(histograms, model) { + const cpuTotalRecompileSynchronous = new tr.v.Histogram( + 'v8_recompile_synchronous_cpu_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + cpuTotalRecompileSynchronous.description = + 'cpu total time spent in synchronous recompilation'; + const wallTotalRecompileSynchronous = new tr.v.Histogram( + 'v8_recompile_synchronous_wall_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + wallTotalRecompileSynchronous.description = + 'wall total time spent in synchronous recompilation'; + const cpuTotalRecompileConcurrent = new tr.v.Histogram( + 'v8_recompile_concurrent_cpu_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + cpuTotalRecompileConcurrent.description = + 'cpu total time spent in concurrent recompilation'; + const wallTotalRecompileConcurrent = new tr.v.Histogram( + 'v8_recompile_concurrent_wall_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + wallTotalRecompileConcurrent.description = + 'wall total time spent in concurrent recompilation'; + // TODO(eakuefner): Stop computing overall histograms once dash v2 is ready. + // https://github.com/catapult-project/catapult/issues/2180 + const cpuTotalRecompileOverall = new tr.v.Histogram( + 'v8_recompile_overall_cpu_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + cpuTotalRecompileOverall.description = + 'cpu total time spent in synchronous or concurrent recompilation'; + const wallTotalRecompileOverall = new tr.v.Histogram( + 'v8_recompile_overall_wall_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + wallTotalRecompileOverall.description = + 'wall total time spent in synchronous or concurrent recompilation'; + + for (const e of model.findTopmostSlicesNamed('V8.RecompileSynchronous')) { + cpuTotalRecompileSynchronous.addSample(e.cpuDuration); + wallTotalRecompileSynchronous.addSample(e.duration); + cpuTotalRecompileOverall.addSample(e.cpuDuration); + wallTotalRecompileOverall.addSample(e.duration); + } + + histograms.addHistogram(cpuTotalRecompileSynchronous); + histograms.addHistogram(wallTotalRecompileSynchronous); + + for (const e of model.findTopmostSlicesNamed('V8.RecompileConcurrent')) { + cpuTotalRecompileConcurrent.addSample(e.cpuDuration); + wallTotalRecompileConcurrent.addSample(e.duration); + cpuTotalRecompileOverall.addSample(e.cpuDuration); + wallTotalRecompileOverall.addSample(e.duration); + } + + histograms.addHistogram(cpuTotalRecompileConcurrent); + histograms.addHistogram(wallTotalRecompileConcurrent); + histograms.addHistogram(cpuTotalRecompileOverall); + histograms.addHistogram(wallTotalRecompileOverall); + } + + function computeOptimizeCodeMetrics(histograms, model) { + const cpuTotalOptimizeCode = new tr.v.Histogram( + 'v8_optimize_code_cpu_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + cpuTotalOptimizeCode.description = + 'cpu total time spent in code optimization'; + const wallTotalOptimizeCode = new tr.v.Histogram( + 'v8_optimize_code_wall_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + wallTotalOptimizeCode.description = + 'wall total time spent in code optimization'; + + for (const e of model.findTopmostSlicesNamed('V8.OptimizeCode')) { + cpuTotalOptimizeCode.addSample(e.cpuDuration); + wallTotalOptimizeCode.addSample(e.duration); + } + + histograms.addHistogram(cpuTotalOptimizeCode); + histograms.addHistogram(wallTotalOptimizeCode); + } + + function computeDeoptimizeCodeMetrics(histograms, model) { + const cpuTotalDeoptimizeCode = new tr.v.Histogram( + 'v8_deoptimize_code_cpu_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + cpuTotalDeoptimizeCode.description = + 'cpu total time spent in code deoptimization'; + const wallTotalDeoptimizeCode = new tr.v.Histogram( + 'v8_deoptimize_code_wall_total', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + wallTotalDeoptimizeCode.description = + 'wall total time spent in code deoptimization'; + + for (const e of model.findTopmostSlicesNamed('V8.DeoptimizeCode')) { + cpuTotalDeoptimizeCode.addSample(e.cpuDuration); + wallTotalDeoptimizeCode.addSample(e.duration); + } + + histograms.addHistogram(cpuTotalDeoptimizeCode); + histograms.addHistogram(wallTotalDeoptimizeCode); + } + + function executionMetric(histograms, model) { + computeExecuteMetrics(histograms, model); + computeParseLazyMetrics(histograms, model); + computeCompileIgnitionMetrics(histograms, model); + computeCompileFullCodeMetrics(histograms, model); + computeRecompileMetrics(histograms, model); + computeOptimizeCodeMetrics(histograms, model); + computeDeoptimizeCodeMetrics(histograms, model); + } + + tr.metrics.MetricRegistry.register(executionMetric); + + return { + executionMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric_test.html new file mode 100644 index 00000000000..d42c5476706 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/execution_metric_test.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/v8/execution_metric.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('executionMetricBasic', function() { + const model = tr.c.TestUtils.newModel(); + const histograms = new tr.v.HistogramSet(); + + tr.metrics.v8.executionMetric(histograms, model); + + [ + 'v8_execution_cpu_total', + 'v8_execution_wall_total', + 'v8_execution_cpu_self', + 'v8_execution_wall_self', + 'v8_parse_lazy_cpu_self', + 'v8_parse_lazy_wall_self', + 'v8_compile_full_code_cpu_self', + 'v8_compile_full_code_wall_self', + 'v8_compile_ignition_cpu_self', + 'v8_compile_ignition_wall_self', + 'v8_recompile_synchronous_cpu_total', + 'v8_recompile_synchronous_wall_total', + 'v8_recompile_concurrent_cpu_total', + 'v8_recompile_concurrent_wall_total', + 'v8_recompile_overall_cpu_total', + 'v8_recompile_overall_wall_total', + 'v8_optimize_code_cpu_total', + 'v8_optimize_code_wall_total', + 'v8_deoptimize_code_cpu_total', + 'v8_deoptimize_code_wall_total', + ].forEach(function(name) { + assert.isDefined(histograms.getHistogramNamed(name)); + }); + }); + + test('noDoubleCounting', function() { + const events = [ + {name: 'V8.Execute', args: {}, pid: 52, ts: 0, cat: 'foo', tid: 53, + ph: 'B'}, + {name: 'V8.Execute', args: {}, pid: 52, ts: 100, cat: 'foo', tid: 53, + ph: 'E'}, + {name: 'V8.Execute', args: {}, pid: 52, ts: 20, cat: 'foo', tid: 53, + ph: 'B'}, + {name: 'V8.Execute', args: {}, pid: 52, ts: 40, cat: 'foo', tid: 53, + ph: 'E'} + ]; + + const model = tr.c.TestUtils.newModelWithEvents(JSON.stringify(events), { + shiftWorldToZero: false + }); + const histograms = new tr.v.HistogramSet(); + tr.metrics.v8.executionMetric(histograms, model); + + const value = histograms.getHistogramNamed('v8_execution_wall_total'); + assert.closeTo(value.running.sum, 0.1, 1e-5); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric.html new file mode 100644 index 00000000000..c3d90cc2786 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric.html @@ -0,0 +1,297 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/v8/utils.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/value/diagnostics/breakdown.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.v8', function() { + // The time window size for mutator utilization computation. + // It is equal to the duration of one frame corresponding to 60 FPS rendering. + const TARGET_FPS = 60; + const MS_PER_SECOND = 1000; + const WINDOW_SIZE_MS = MS_PER_SECOND / TARGET_FPS; + + function gcMetric(histograms, model) { + addDurationOfTopEvents(histograms, model); + addTotalDurationOfTopEvents(histograms, model); + addDurationOfSubEvents(histograms, model); + addPercentageInV8ExecuteOfTopEvents(histograms, model); + addTotalPercentageInV8Execute(histograms, model); + addMarkCompactorMutatorUtilization(histograms, model); + addTotalMarkCompactorTime(histograms, model); + addTotalMarkCompactorMarkingTime(histograms, model); + } + + tr.metrics.MetricRegistry.register(gcMetric); + + const timeDurationInMs_smallerIsBetter = + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter; + const percentage_biggerIsBetter = + tr.b.Unit.byName.normalizedPercentage_biggerIsBetter; + const percentage_smallerIsBetter = + tr.b.Unit.byName.normalizedPercentage_smallerIsBetter; + + // 0.1 steps from 0 to 20 since it is the most common range. + // Exponentially increasing steps from 20 to 200. + const CUSTOM_BOUNDARIES = tr.v.HistogramBinBoundaries.createLinear(0, 20, 200) + .addExponentialBins(200, 100); + + function createNumericForTopEventTime(name) { + const n = new tr.v.Histogram(name, + timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + n.customizeSummaryOptions({ + avg: true, + count: true, + max: true, + min: false, + std: true, + sum: true, + percentile: [0.90]}); + return n; + } + + function createNumericForSubEventTime(name) { + const n = new tr.v.Histogram(name, + timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + n.customizeSummaryOptions({ + avg: true, + count: false, + max: true, + min: false, + std: false, + sum: false, + percentile: [0.90] + }); + return n; + } + + function createNumericForIdleTime(name) { + const n = new tr.v.Histogram(name, + timeDurationInMs_smallerIsBetter, CUSTOM_BOUNDARIES); + n.customizeSummaryOptions({ + avg: true, + count: false, + max: true, + min: false, + std: false, + sum: true, + percentile: [] + }); + return n; + } + + function createPercentage(name, numerator, denominator, unit) { + const hist = new tr.v.Histogram(name, unit); + if (denominator === 0) { + hist.addSample(0); + } else { + hist.addSample(numerator / denominator); + } + hist.customizeSummaryOptions({ + avg: true, + count: false, + max: false, + min: false, + std: false, + sum: false, + percentile: [] + }); + return hist; + } + + /** + * Example output: + * - v8-gc-full-mark-compactor. + */ + function addDurationOfTopEvents(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent, + tr.metrics.v8.utils.topGarbageCollectionEventName, + function(name, events) { + const cpuDuration = createNumericForTopEventTime(name); + events.forEach(function(event) { + cpuDuration.addSample(event.cpuDuration); + }); + histograms.addHistogram(cpuDuration); + } + ); + } + + /** + * Example output: + * - v8-gc-total + */ + function addTotalDurationOfTopEvents(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent, + event => 'v8-gc-total', + function(name, events) { + const cpuDuration = createNumericForTopEventTime(name); + events.forEach(function(event) { + cpuDuration.addSample(event.cpuDuration); + }); + histograms.addHistogram(cpuDuration); + } + ); + } + + function isV8MarkCompactorSummary(event) { + return !tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event) && + tr.metrics.v8.utils.isMarkCompactorSummaryEvent(event); + } + + function isV8MarkCompactorMarkingSummary(event) { + return !tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event) && + tr.metrics.v8.utils.isMarkCompactorMarkingSummaryEvent(event); + } + + function createHistogramFromSummary(histograms, name, events) { + const foregroundDuration = + createNumericForTopEventTime(name + '-foreground'); + const backgroundDuration = + createNumericForTopEventTime(name + '-background'); + const totalDuration = + createNumericForTopEventTime(name + '-total'); + const relatedNames = new tr.v.d.RelatedNameMap(); + relatedNames.set('foreground', foregroundDuration.name); + relatedNames.set('background', backgroundDuration.name); + for (const event of events) { + foregroundDuration.addSample(event.args.duration); + backgroundDuration.addSample(event.args.background_duration); + const breakdownForTotal = new tr.v.d.Breakdown(); + breakdownForTotal.set('foreground', event.args.duration); + breakdownForTotal.set('background', event.args.background_duration); + totalDuration.addSample( + event.args.duration + event.args.background_duration, + {breakdown: breakdownForTotal}); + } + histograms.addHistogram(foregroundDuration); + histograms.addHistogram(backgroundDuration); + histograms.addHistogram(totalDuration, {breakdown: relatedNames}); + } + + /** + * Example output: + * - v8-gc-mark-compactor-foreground + * - v8-gc-mark-compactor-background + * - v8-gc-mark-compactor-total + */ + function addTotalMarkCompactorTime(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + isV8MarkCompactorSummary, + event => 'v8-gc-mark-compactor', + (name, events) => createHistogramFromSummary(histograms, name, events) + ); + } + + /** + * Example output: + * - v8-gc-mark-compactor-marking-foreground + * - v8-gc-mark-compactor-marking-background + * - v8-gc-mark-compactor-marking-total + */ + function addTotalMarkCompactorMarkingTime(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + isV8MarkCompactorMarkingSummary, + event => 'v8-gc-mark-compactor-marking', + (name, events) => createHistogramFromSummary(histograms, name, events) + ); + } + + /** + * Example output: + * - v8-gc-full-mark-compactor-evacuate. + */ + function addDurationOfSubEvents(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + tr.metrics.v8.utils.isNotForcedSubGarbageCollectionEvent, + tr.metrics.v8.utils.subGarbageCollectionEventName, + function(name, events) { + const cpuDuration = createNumericForSubEventTime(name); + events.forEach(function(event) { + cpuDuration.addSample(event.cpuDuration); + }); + histograms.addHistogram(cpuDuration); + } + ); + } + + /** + * Example output: + * - v8-gc-full-mark-compactor_percentage_in_v8_execute. + */ + function addPercentageInV8ExecuteOfTopEvents(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent, + tr.metrics.v8.utils.topGarbageCollectionEventName, + function(name, events) { + addPercentageInV8Execute(histograms, model, name, events); + } + ); + } + + /** + * Example output: + * - v8-gc-total_percentage_in_v8_execute. + */ + function addTotalPercentageInV8Execute(histograms, model) { + tr.metrics.v8.utils.groupAndProcessEvents(model, + tr.metrics.v8.utils.isNotForcedTopGarbageCollectionEvent, + event => 'v8-gc-total', + function(name, events) { + addPercentageInV8Execute(histograms, model, name, events); + } + ); + } + + function addPercentageInV8Execute(histograms, model, name, events) { + let cpuDurationInV8Execute = 0; + let cpuDurationTotal = 0; + events.forEach(function(event) { + const v8Execute = tr.metrics.v8.utils.findParent( + event, tr.metrics.v8.utils.isV8ExecuteEvent); + if (v8Execute) { + cpuDurationInV8Execute += event.cpuDuration; + } + cpuDurationTotal += event.cpuDuration; + }); + const percentage = createPercentage( + name + '_percentage_in_v8_execute', cpuDurationInV8Execute, + cpuDurationTotal, percentage_smallerIsBetter); + histograms.addHistogram(percentage); + } + + /** + * Example output: + * - v8-gc-mark-compactor-mmu-100ms_window. + */ + function addMarkCompactorMutatorUtilization(histograms, model) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + const rendererHelpers = Object.values(chromeHelper.rendererHelpers); + tr.metrics.v8.utils.addMutatorUtilization( + 'v8-gc-mark-compactor-mmu', + tr.metrics.v8.utils.isNotForcedMarkCompactorEvent, + [100], + rendererHelpers, + histograms); + } + + return { + gcMetric, + WINDOW_SIZE_MS, // For testing purposes only. + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric_test.html new file mode 100644 index 00000000000..8c38b563e62 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/gc_metric_test.html @@ -0,0 +1,210 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/v8/gc_metric.html"> +<link rel="import" href="/tracing/model/slice_group.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createModel(start, end, slices) { + return tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + const group = mainThread.sliceGroup; + for (const slice of slices) { + group.pushSlice(tr.c.TestUtils.newSliceEx(slice)); + } + group.createSubSlices(); + mainThread.updateBounds(); + }); + } + + function constructName(name, suffix) { + return name + '_' + suffix; + } + + function run(slices) { + const histograms = new tr.v.HistogramSet(); + const startTime = slices.reduce( + (acc, slice) => (Math.min(acc, slice.start))); + const endTime = slices.reduce((acc, slice) => (Math.max(acc, slice.end))); + const model = createModel(startTime - 1, endTime + 1, slices); + tr.metrics.v8.gcMetric(histograms, model); + return histograms; + } + + test('topEvents', function() { + const events = { + 'V8.GCCompactor': 'v8-gc-full-mark-compactor', + 'V8.GCFinalizeMC': 'v8-gc-latency-mark-compactor', + 'V8.GCFinalizeMCReduceMemory': 'v8-gc-memory-mark-compactor', + 'V8.GCIncrementalMarking': 'v8-gc-incremental-step', + 'V8.GCIncrementalMarkingFinalize': 'v8-gc-incremental-finalize', + 'V8.GCIncrementalMarkingStart': 'v8-gc-incremental-start', + 'V8.GCPhantomHandleProcessingCallback': 'v8-gc-phantom-handle-callback', + 'V8.GCScavenger': 'v8-gc-scavenger' + }; + for (const [timelineName, telemetryName] of Object.entries(events)) { + const slices = [ + { + title: timelineName, args: {}, start: 100, end: 200, + cpuStart: 100, cpuEnd: 200 + } + ]; + const actual = run(slices); + + const value = actual.getHistogramNamed(telemetryName); + assert.strictEqual(value.running.sum, 100); + assert.strictEqual(value.numValues, 1); + assert.strictEqual(value.average, 100); + assert.strictEqual(value.running.max, 100); + assert.closeTo(value.getApproximatePercentile(0.90), 100, 1); + } + }); + + test('subEvents', function() { + const slices = [ + { + title: 'V8.GCFinalizeMC', args: {}, start: 100, end: 200, + cpuStart: 100, cpuEnd: 200 + }, + { + title: 'V8.GC_MC_MARK', args: {}, start: 110, end: 190, + cpuStart: 110, cpuEnd: 190 + }, + ]; + const actual = run(slices); + const telemetryName = 'v8-gc-latency-mark-compactor-mark'; + const value = actual.getHistogramNamed(telemetryName); + assert.strictEqual(value.average, 80); + assert.strictEqual(value.running.max, 80); + assert.closeTo(value.getApproximatePercentile(0.90), 80, 1); + }); + + test('total', function() { + const slices = [ + { + title: 'V8.GCFinalizeMC', args: {}, start: 100, end: 200, + cpuStart: 100, cpuEnd: 200 + }, + { + title: 'V8.GCIncrementalMarking', args: {}, start: 210, end: 290, + cpuStart: 210, cpuEnd: 290 + } + ]; + const actual = run(slices); + + const value = actual.getHistogramNamed('v8-gc-total'); + assert.strictEqual(value.running.sum, 180); + assert.strictEqual(value.numValues, 2); + assert.strictEqual(value.average, 90); + assert.strictEqual(value.running.max, 100); + }); + + test('percentageInV8Execute', function() { + const slices = [ + { + title: 'V8.Execute', + args: {}, start: 100, end: 200, + cpuStart: 100, cpuEnd: 200 + }, + { + title: 'V8.GCFinalizeMC', args: {}, start: 110, end: 190, + cpuStart: 110, cpuEnd: 190 + }, + { + title: 'V8.GCFinalizeMC', args: {}, start: 210, end: 220, + cpuStart: 210, cpuEnd: 220 + } + ]; + const actual = run(slices); + const value = actual.getHistogramNamed( + 'v8-gc-latency-mark-compactor_percentage_in_v8_execute'); + assert.strictEqual(value.average, + (190 - 110) / ((190 - 110) + (220 - 210))); + }); + + test('markCompactorMutatorUtilization', function() { + const slices = [ + { + title: 'V8.GCIncrementalMarkingStart', + args: {}, start: 100, end: 110, + cpuStart: 100, cpuEnd: 110 + }, + { + title: 'V8.GCIncrementalMarking', + args: {}, start: 150, end: 160, + cpuStart: 150, cpuEnd: 160 + }, + { + title: 'V8.GCIncrementalMarkingFinalize', + args: {}, start: 200, end: 220, + cpuStart: 200, cpuEnd: 220 + }, + { + title: 'V8.GCFinalizeMC', + args: {}, start: 250, end: 300, + cpuStart: 250, cpuEnd: 300 + } + ]; + const histograms = run(slices); + const mmu = histograms.getHistogramNamed( + 'v8-gc-mark-compactor-mmu-100ms_window'); + assert.closeTo(0.3, mmu.min, 1e-3); + assert.strictEqual(mmu.summaryOptions.get('min'), true); + assert.strictEqual(mmu.summaryOptions.get('max'), false); + }); + + test('markCompactorSummary', function() { + const slices = [ + { + title: 'V8.GCMarkCompactorSummary', + args: {'duration': 3.1, 'background_duration': 1.3}, + start: 100, end: 100, + cpuStart: 100, cpuEnd: 100 + }, + ]; + const histograms = run(slices); + const markCompactorForeground = histograms.getHistogramNamed( + 'v8-gc-mark-compactor-foreground'); + assert.closeTo(3.1, markCompactorForeground.sum, 1e-3); + const markCompactorBackground = histograms.getHistogramNamed( + 'v8-gc-mark-compactor-background'); + assert.closeTo(1.3, markCompactorBackground.sum, 1e-3); + const markCompactorTotal = histograms.getHistogramNamed( + 'v8-gc-mark-compactor-total'); + assert.closeTo(4.4, markCompactorTotal.sum, 1e-3); + }); + + test('markCompactorMarkingSummary', function() { + const slices = [ + { + title: 'V8.GCMarkCompactorMarkingSummary', + args: {'duration': 4.1, 'background_duration': 1.4}, + start: 100, end: 100, + cpuStart: 100, cpuEnd: 100 + }, + ]; + const histograms = run(slices); + const markCompactorForeground = histograms.getHistogramNamed( + 'v8-gc-mark-compactor-marking-foreground'); + assert.closeTo(4.1, markCompactorForeground.sum, 1e-3); + const markCompactorBackground = histograms.getHistogramNamed( + 'v8-gc-mark-compactor-marking-background'); + assert.closeTo(1.4, markCompactorBackground.sum, 1e-3); + const markCompactorTotal = histograms.getHistogramNamed( + 'v8-gc-mark-compactor-marking-total'); + assert.closeTo(5.5, markCompactorTotal.sum, 1e-3); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric.html new file mode 100644 index 00000000000..286cbaa32f1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric.html @@ -0,0 +1,374 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html"> +<link rel="import" href="/tracing/extras/v8/runtime_stats_entry.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/diagnostics/related_name_map.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.v8', function() { + const COUNT_CUSTOM_BOUNDARIES = tr.v.HistogramBinBoundaries + .createExponential(1, 1000000, 50); + const DURATION_CUSTOM_BOUNDARIES = tr.v.HistogramBinBoundaries + .createExponential(0.1, 10000, 50); + const SUMMARY_OPTIONS = { + std: false, + count: false, + sum: false, + min: false, + max: false, + }; + + function computeDomContentLoadedTime_(model) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + let domContentLoadedTime = 0; + + for (const rendererHelper of Object.values(chromeHelper.rendererHelpers)) { + for (const ev of rendererHelper.mainThread.sliceGroup.childEvents()) { + if (ev.title === 'domContentLoadedEventEnd' && + ev.start > domContentLoadedTime) { + domContentLoadedTime = ev.start; + } + } + } + return domContentLoadedTime; + } + + function computeInteractiveTime_(model) { + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + let interactiveTime = 0; + for (const expectation of model.userModel.expectations) { + if (tr.e.chrome.CHROME_INTERNAL_URLS.includes( + expectation.url)) { + continue; + } + if (!(expectation instanceof tr.model.um.LoadExpectation)) continue; + if (expectation.timeToInteractive === undefined) continue; + // TODO(fmeawad): Support multiple navigations. + if (interactiveTime !== 0) throw new Error('Too many navigations'); + interactiveTime = expectation.timeToInteractive; + } + return interactiveTime; + } + + function convertMicroToMilli_(time) { + return tr.b.convertUnit(time, + tr.b.UnitPrefixScale.METRIC.MICRO, tr.b.UnitPrefixScale.METRIC.MILLI); + } + + // TODO(crbug.com/688342): Remove this function when runtimeStatsMetric is + // removed. + function computeRuntimeStats(histograms, slices) { + const runtimeGroupCollection = new tr.e.v8.RuntimeStatsGroupCollection(); + runtimeGroupCollection.addSlices(slices); + + function addHistogramsForRuntimeGroup(runtimeGroup, optRelatedNameMaps) { + histograms.createHistogram( + `${runtimeGroup.name}:duration`, + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, { + value: convertMicroToMilli_(runtimeGroup.time), + diagnostics: optRelatedNameMaps ? + {samples: optRelatedNameMaps.durationBreakdown} : {} + }, { + binBoundaries: DURATION_CUSTOM_BOUNDARIES, + summaryOptions: SUMMARY_OPTIONS, + diagnostics: optRelatedNameMaps ? + {samples: optRelatedNameMaps.durationNames} : {} + }); + + histograms.createHistogram( + `${runtimeGroup.name}:count`, + tr.b.Unit.byName.count_smallerIsBetter, { + value: runtimeGroup.count, + diagnostics: optRelatedNameMaps ? + {samples: optRelatedNameMaps.countBreakdown} : {} + }, { + binBoundaries: COUNT_CUSTOM_BOUNDARIES, + summaryOptions: SUMMARY_OPTIONS, + diagnostics: optRelatedNameMaps ? + {samples: optRelatedNameMaps.countNames} : {} + }); + } + + function addDetailedHistogramsForRuntimeGroup(runtimeGroup) { + const durationNames = new tr.v.d.RelatedNameMap(); + const durationBreakdown = new tr.v.d.Breakdown(); + const countNames = new tr.v.d.RelatedNameMap(); + const countBreakdown = new tr.v.d.Breakdown(); + + for (const entry of runtimeGroup.values) { + const durationSampleHistogram = histograms.createHistogram( + `${entry.name}:duration`, + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + convertMicroToMilli_(entry.time), { + binBoundaries: DURATION_CUSTOM_BOUNDARIES, + summaryOptions: SUMMARY_OPTIONS, + }); + durationNames.set(entry.name, durationSampleHistogram.name); + durationBreakdown.set(entry.name, convertMicroToMilli_(entry.time)); + + const countSampleHistogram = histograms.createHistogram( + `${entry.name}:count`, + tr.b.Unit.byName.count_smallerIsBetter, + entry.count, { + binBoundaries: COUNT_CUSTOM_BOUNDARIES, + summaryOptions: SUMMARY_OPTIONS, + }); + countNames.set(entry.name, countSampleHistogram.name); + countBreakdown.set(entry.name, entry.count); + } + + addHistogramsForRuntimeGroup(runtimeGroup, { + durationNames, + durationBreakdown, + countNames, + countBreakdown + }); + } + + for (const runtimeGroup of runtimeGroupCollection.runtimeGroups) { + addHistogramsForRuntimeGroup(runtimeGroup); + } + + const blinkGroupCollection = runtimeGroupCollection.blinkRCSGroupCollection; + if (blinkGroupCollection.totalTime > 0) { + blinkGroupCollection.runtimeGroups.forEach( + addDetailedHistogramsForRuntimeGroup); + } + } + + // TODO(crbug.com/688342): Remove this metric and use runtimeStatsTotalMetric + // instead when the runtimeStatsTotalMetric is stable. + function runtimeStatsMetric(histograms, model) { + const interactiveTime = computeInteractiveTime_(model); + const domContentLoadedTime = computeDomContentLoadedTime_(model); + const endTime = Math.max(interactiveTime, domContentLoadedTime); + const slices = [...model.getDescendantEvents()].filter(event => + event instanceof tr.e.v8.V8ThreadSlice && event.start <= endTime); + computeRuntimeStats(histograms, slices); + } + + function addDurationHistogram(railStageName, runtimeGroupName, sampleValue, + histograms, durationNamesByGroupName) { + const histName = `${railStageName}_${runtimeGroupName}:duration`; + histograms.createHistogram( + histName, + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + convertMicroToMilli_(sampleValue), { + binBoundaries: DURATION_CUSTOM_BOUNDARIES, + summaryOptions: SUMMARY_OPTIONS, + }); + let relatedNames = durationNamesByGroupName.get(runtimeGroupName); + if (!relatedNames) { + relatedNames = new tr.v.d.RelatedNameMap(); + durationNamesByGroupName.set(runtimeGroupName, relatedNames); + } + relatedNames.set(railStageName, histName); + } + + function addCountHistogram(railStageName, runtimeGroupName, sampleValue, + histograms, countNamesByGroupName) { + const histName = `${railStageName}_${runtimeGroupName}:count`; + histograms.createHistogram( + histName, + tr.b.Unit.byName.count_smallerIsBetter, sampleValue, { + binBoundaries: COUNT_CUSTOM_BOUNDARIES, + summaryOptions: SUMMARY_OPTIONS, + }); + let relatedNames = countNamesByGroupName.get(runtimeGroupName); + if (!relatedNames) { + relatedNames = new tr.v.d.RelatedNameMap(); + countNamesByGroupName.set(runtimeGroupName, relatedNames); + } + relatedNames.set(railStageName, histName); + } + + function addTotalDurationHistogram(histogramName, time, histograms, + relatedNames) { + const value = convertMicroToMilli_(time); + const breakdown = new tr.v.d.Breakdown(); + if (relatedNames) { + for (const [cat, histName] of relatedNames) { + breakdown.set(cat, histograms.getHistogramNamed(histName).average); + } + } + histograms.createHistogram( + `${histogramName}:duration`, + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + {value, diagnostics: {'RAIL stages': breakdown}}, { + binBoundaries: DURATION_CUSTOM_BOUNDARIES, + summaryOptions: SUMMARY_OPTIONS, + diagnostics: {'RAIL stages': relatedNames}, + }); + } + + function addTotalCountHistogram(histogramName, value, histograms, + relatedNames) { + const breakdown = new tr.v.d.Breakdown(); + if (relatedNames) { + for (const [cat, histName] of relatedNames) { + breakdown.set(cat, histograms.getHistogramNamed(histName).average); + } + } + histograms.createHistogram( + `${histogramName}:count`, + tr.b.Unit.byName.count_smallerIsBetter, + {value, diagnostics: {'RAIL stages': breakdown}}, { + binBoundaries: COUNT_CUSTOM_BOUNDARIES, + summaryOptions: SUMMARY_OPTIONS, + diagnostics: {'RAIL stages': relatedNames}, + }); + } + + function computeRuntimeStatsBucketOnUE(histograms, slices, + v8SlicesBucketOnUEMap) { + const durationNamesByGroupName = new Map(); + const countNamesByGroupName = new Map(); + + // Compute runtimeStats in each of the UE buckets. Also record the + // histograms in RelatedNameMap. These histograms are added to the + // corresponding histograms in the total bucket as a diagnostic. This keeps + // the data grouped. + for (const [name, slicesUE] of v8SlicesBucketOnUEMap) { + const runtimeGroupCollection = new tr.e.v8.RuntimeStatsGroupCollection(); + runtimeGroupCollection.addSlices(slicesUE); + + let overallV8Time = runtimeGroupCollection.totalTime; + let overallV8Count = runtimeGroupCollection.totalCount; + for (const runtimeGroup of runtimeGroupCollection.runtimeGroups) { + addDurationHistogram(name, runtimeGroup.name, runtimeGroup.time, + histograms, durationNamesByGroupName); + if (runtimeGroup.name === 'Blink C++') { + overallV8Time -= runtimeGroup.time; + } + + addCountHistogram(name, runtimeGroup.name, runtimeGroup.count, + histograms, countNamesByGroupName); + if (runtimeGroup.name === 'Blink C++') { + overallV8Count -= runtimeGroup.count; + } + } + + if (runtimeGroupCollection.blinkRCSGroupCollection.totalTime > 0) { + const blinkRCSGroupCollection = + runtimeGroupCollection.blinkRCSGroupCollection; + for (const group of blinkRCSGroupCollection.runtimeGroups) { + addDurationHistogram(name, group.name, group.time, histograms, + durationNamesByGroupName); + addCountHistogram(name, group.name, group.count, histograms, + countNamesByGroupName); + } + } + + // Add V8 only time that is Total - Blink C++ duration. + addDurationHistogram(name, 'V8-Only', overallV8Time, histograms, + durationNamesByGroupName); + addCountHistogram(name, 'V8-Only', overallV8Count, histograms, + countNamesByGroupName); + } + + // Add the runtimeStats for all the samples. Please note, the values in + // the UE buckets may not add upto the values computed here. Since UEs + // can overlap, we count some of the samples in multiple UE buckets. + const runtimeGroupCollection = new tr.e.v8.RuntimeStatsGroupCollection(); + runtimeGroupCollection.addSlices(slices); + + let overallV8Time = runtimeGroupCollection.totalTime; + let overallV8Count = runtimeGroupCollection.totalCount; + for (const runtimeGroup of runtimeGroupCollection.runtimeGroups) { + addTotalDurationHistogram(runtimeGroup.name, runtimeGroup.time, + histograms, durationNamesByGroupName.get(runtimeGroup.name)); + if (runtimeGroup.name === 'Blink C++') { + overallV8Time -= runtimeGroup.time; + } + + addTotalCountHistogram(runtimeGroup.name, runtimeGroup.count, histograms, + countNamesByGroupName.get(runtimeGroup.name)); + if (runtimeGroup.name === 'Blink C++') { + overallV8Count -= runtimeGroup.count; + } + } + + if (runtimeGroupCollection.blinkRCSGroupCollection.totalTime > 0) { + const blinkRCSGroupCollection = + runtimeGroupCollection.blinkRCSGroupCollection; + for (const group of blinkRCSGroupCollection.runtimeGroups) { + addTotalDurationHistogram(group.name, group.time, histograms, + durationNamesByGroupName.get(group.name)); + addTotalCountHistogram(group.name, group.count, histograms, + countNamesByGroupName.get(group.name)); + } + } + + // Add V8 only time that is Total - Blink C++ duration. + addTotalDurationHistogram('V8-Only', overallV8Time, histograms, + durationNamesByGroupName.get('V8-Only')); + addTotalCountHistogram('V8-Only', overallV8Count, histograms, + countNamesByGroupName.get('V8-Only')); + } + + function runtimeStatsTotalMetric(histograms, model) { + const v8ThreadSlices = [...model.getDescendantEvents()].filter(event => + event instanceof tr.e.v8.V8ThreadSlice).sort((e1, e2) => + e1.start - e2.start); + const v8SlicesBucketOnUEMap = new Map(); + // User expectations can sometime overlap. So, certain v8 slices can be + // included in more than one expectation. We count such slices in each + // of the expectations. This is done so as to minimize the noise due to + // the differences in the extent of overlap between the runs. + for (const expectation of model.userModel.expectations) { + if (tr.e.chrome.CHROME_INTERNAL_URLS.includes( + expectation.url)) { + continue; + } + const slices = expectation.range.filterArray(v8ThreadSlices, + event => event.start); + if (slices.length === 0) continue; + // filterArray filters the array that intersects the range inclusively. + // Expectations are not inclusive i.e. expectations are like [0, 1), + // [1, 2). v8ThreadSlices that start at 1 should be counted only in [1,2) + // bucket. Filter out sample at the boundary so that they are not counted + // twice. + const lastSlice = slices[slices.length - 1]; + if (!expectation.range.intersectsRangeExclusive(lastSlice.range)) { + slices.pop(); + } + + if (v8SlicesBucketOnUEMap.get(expectation.stageTitle) === undefined) { + v8SlicesBucketOnUEMap.set(expectation.stageTitle, slices); + } else { + const totalSlices = v8SlicesBucketOnUEMap.get(expectation.stageTitle) + .concat(slices); + v8SlicesBucketOnUEMap.set(expectation.stageTitle, totalSlices); + } + } + + // Compute runtimeStats in each of the UE buckets and also compute + // runtimeStats on all of the samples. The values in UE buckets do not add + // up to the total of all samples, since we duplicate some of the samples in + // multiple buckets when the UEs overlap. + computeRuntimeStatsBucketOnUE(histograms, v8ThreadSlices, + v8SlicesBucketOnUEMap); + } + + tr.metrics.MetricRegistry.register(runtimeStatsTotalMetric); + tr.metrics.MetricRegistry.register(runtimeStatsMetric); + + return { + runtimeStatsMetric, + runtimeStatsTotalMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric_test.html new file mode 100644 index 00000000000..91bc8b7a10e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/runtime_stats_metric_test.html @@ -0,0 +1,718 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html"> +<link rel="import" + href="/tracing/extras/chrome/chrome_user_friendly_category_driver.html"> +<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html"> +<link rel="import" href="/tracing/metrics/v8/runtime_stats_metric.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function checkRuntimeHistogram_(histograms, name, count, duration, + breakdownHistograms) { + const countHistogram = histograms.getHistogramNamed(`${name}:count`); + assert.strictEqual(tr.b.getOnlyElement(countHistogram.sampleValues), count); + const durationHistogram = histograms.getHistogramNamed(`${name}:duration`); + assert.strictEqual( + tr.b.getOnlyElement(durationHistogram.sampleValues), duration); + + if (breakdownHistograms === undefined) return; + const countBin = tr.b.getOnlyElement(countHistogram.allBins.filter( + bin => bin.diagnosticMaps.length > 0)); + const durationBin = tr.b.getOnlyElement(durationHistogram.allBins.filter( + bin => bin.diagnosticMaps.length > 0)); + for (const name of breakdownHistograms) { + assert.notEqual(tr.b.getOnlyElement(countBin.diagnosticMaps) + .get('samples').get(name + ':count'), undefined); + assert.notEqual(tr.b.getOnlyElement(durationBin.diagnosticMaps) + .get('samples').get(name + ':duration'), undefined); + } + } + + test('runtimeStatsMetricUsingTTI', function() { + // The renderer thread timeline looks like: + // + // * [V8.NewInstance] * [BlinkRuntimeCallStats,V8.Execute] * ...[V8.Ignored] + // | | | + // | | | + // v v v + // First navigation FMP TTI + // 200 9200 15400 + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({ + cat: 'disabled-by-default-network', + title: 'ResourceLoad', + start: 200, + duration: 5.0 + })); + rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader', 300, + {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'}, + documentLoaderURL: 'http://example.com'}); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 9200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'domContentLoadedEventEnd', + start: 10000, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.newInstance', + type: tr.e.v8.V8ThreadSlice, + start: 12555, + duration: 990, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 11], + HandleApiCall: [2, 22], + CompileFullCode: [3, 33], + LoadIC_Miss: [4, 44], + ParseLazy: [5, 55], + OptimizeCode: [6, 66], + FunctionCallback: [7, 77], + AllocateInTargetSpace: [8, 88], + API_Object_Get: [9, 99] + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 9180, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'loading', + title: 'firstMeaningfulPaintCandidate', + start: 9200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'domContentLoadedEventEnd', + start: 9200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 9350, + duration: 100, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 11150, + duration: 100, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 12550, + duration: 1000, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'disabled-by-default-v8.runtime_stats', + title: 'runtime-call-stats', + type: tr.e.v8.V8ThreadSlice, + start: 13550, + duration: 1000, + args: { + 'runtime-call-stats': { + Blink_Layout_UpdateLayout: [15, 60], + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + type: tr.e.v8.V8ThreadSlice, + start: 13555, + duration: 990, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 11], + HandleApiCall: [2, 22], + CompileFullCode: [3, 33], + StoreIC_Miss: [4, 44], + ParseLazy: [5, 55], + OptimizeCode: [6, 66], + FunctionCallback: [7, 77], + AllocateInTargetSpace: [8, 88], + API_Object_Get: [9, 99], + ParseBackgroundFunctionLiteral: [2, 22], + CompileBackgroundIgnition: [3, 33] + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 14950, + duration: 500, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'toplevel', + title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE, + start: 22150, + duration: 10, + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Ignored', + type: tr.e.v8.V8ThreadSlice, + start: 30000, + duration: 1000, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 11], + HandleApiCall: [2, 22], + CompileFullCode: [3, 33], + LoadIC_Miss: [4, 44], + ParseLazy: [5, 55], + OptimizeCode: [6, 66], + FunctionCallback: [7, 77], + AllocateInTargetSpace: [8, 88], + API_Object_Get: [9, 99], + ParseBackgroundFunctionLiteral: [2, 22], + CompileBackgroundIgnition: [3, 33] + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'disabled-by-default-v8.runtime_stats', + title: 'V8.Ignored', + type: tr.e.v8.V8ThreadSlice, + start: 30000, + duration: 1000, + args: { + 'runtime-call-stats': { + Blink_Layout_UpdateLayout: [15, 60], + } + } + })); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.v8.runtimeStatsMetric(histograms, model); + assert.strictEqual(histograms.length, 54); + assert.strictEqual(histograms.sourceHistograms.length, 52); + + // A few of the top level ones. + checkRuntimeHistogram_(histograms, 'IC', 8, 0.088); + checkRuntimeHistogram_(histograms, 'API', 18, 0.198); + checkRuntimeHistogram_(histograms, 'Compile', 6, 0.066); + checkRuntimeHistogram_(histograms, 'Total', 95, 1.045); + // Lower level one for Blink. + checkRuntimeHistogram_(histograms, 'Blink_UpdateLayout', 15, 0.06); + }); + + test('runtimeStatsMetricUsingDomContentLoaded', function() { + // The renderer thread timeline looks like: + // + // * [V8.NewInstance] * [ V8.Execute ] * [V8.Ignored] + // | | | + // | | | + // v v v + // First navigation DCL DCL + // 200 1300 2400 + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const rendererProcess = model.rendererProcess; + const mainThread = model.rendererMain; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.newInstance', + type: tr.e.v8.V8ThreadSlice, + start: 300, + duration: 990, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 11], + HandleApiCall: [2, 22], + CompileFullCode: [3, 33], + LoadIC_Miss: [4, 44], + ParseLazy: [5, 55], + OptimizeCode: [6, 66], + FunctionCallback: [7, 77], + AllocateInTargetSpace: [8, 88], + API_Object_Get: [9, 99] + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'domContentLoadedEventEnd', + start: 1300, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + type: tr.e.v8.V8ThreadSlice, + start: 1400, + duration: 990, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 11], + HandleApiCall: [2, 22], + CompileFullCode: [3, 33], + StoreIC_Miss: [4, 44], + ParseLazy: [5, 55], + OptimizeCode: [6, 66], + FunctionCallback: [7, 77], + AllocateInTargetSpace: [8, 88], + API_Object_Get: [9, 99] + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'domContentLoadedEventEnd', + start: 2400, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Ignored', + type: tr.e.v8.V8ThreadSlice, + start: 2450, + duration: 1000, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 11], + HandleApiCall: [2, 22], + CompileFullCode: [3, 33], + LoadIC_Miss: [4, 44], + ParseLazy: [5, 55], + OptimizeCode: [6, 66], + FunctionCallback: [7, 77], + AllocateInTargetSpace: [8, 88], + API_Object_Get: [9, 99] + } + } + })); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.v8.runtimeStatsMetric(histograms, model); + assert.strictEqual(histograms.length, 38); + assert.strictEqual(histograms.sourceHistograms.length, 38); + + // A few of the top level ones. + checkRuntimeHistogram_(histograms, 'IC', 8, 0.088); + checkRuntimeHistogram_(histograms, 'API', 18, 0.198); + checkRuntimeHistogram_(histograms, 'Compile', 6, 0.066); + checkRuntimeHistogram_(histograms, 'Total', 90, 0.99); + }); + + test('runtimeStatsMetricBucketOnUE', function() { + // Test that v8 statistics are properly bucketed when UEs overlap. + // The renderer thread timeline looks like: + // + // * * [ V8 ] * [ V8 ] * [ V8 ] * [ V8 ] * [ V8 ] * + // | | | | | | | + // | | | | | | | + // v v v v v v v + // First LoadStart LoadEnd AnimStart RespEnd AnimEnd IdleEnd + // nav RespStart IdleStart + // 200 300 1000 2000 2100 3000 3500 + + const model = tr.c.TestUtils.newModel(function(model) { + const rendererProcess = model.getOrCreateProcess(1984); + const mainThread = rendererProcess.getOrCreateThread(2); + mainThread.name = 'CrRendererMain'; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + // Add User expectations. + model.userModel.expectations.push(new tr.model.um.LoadExpectation( + model, tr.model.um.LOAD_SUBTYPE_NAMES.SUCCESSFUL, 0, 1000)); + + model.userModel.expectations.push(new tr.model.um.ResponseExpectation( + model, tr.model.um.INITIATOR_TYPE.SCROLL, 1000, 1100)); + + model.userModel.expectations.push(new tr.model.um.AnimationExpectation( + model, tr.model.um.INITIATOR_TYPE.VIDEO, 2000, 1000)); + + model.userModel.expectations.push(new tr.model.um.IdleExpectation( + model, 3000, 500)); + + // Add V8 ThreadSlices corresponding to Load UE. + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.newInstance', + type: tr.e.v8.V8ThreadSlice, + start: 300, + duration: 600, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 10], + HandleApiCall: [2, 11], + CompileFullCode: [3, 12], + LoadIC_Miss: [4, 13], + ParseLazy: [5, 14], + OptimizeCode: [6, 15], + FunctionCallback: [7, 16], + AllocateInTargetSpace: [8, 17], + API_Object_Get: [9, 18] + } + } + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'disabled-by-default-v8.runtime_stats', + type: tr.e.v8.V8ThreadSlice, + start: 350, + duration: 400, + args: { + 'runtime-call-stats': { + Blink_Layout_UpdateLayout: [30, 150], + Blink_Style_UpdateStyle: [20, 100], + } + } + })); + + // Add V8 Thread slices corresponding to Response UE + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + type: tr.e.v8.V8ThreadSlice, + start: 1000, + duration: 800, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 11], + HandleApiCall: [2, 22], + CompileFullCode: [3, 33], + LoadIC_Miss: [4, 44], + ParseLazy: [5, 55], + OptimizeCode: [6, 66], + FunctionCallback: [7, 77], + AllocateInTargetSpace: [8, 88], + API_Object_Get: [9, 99] + } + } + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'disabled-by-default-v8.runtime_stats', + type: tr.e.v8.V8ThreadSlice, + start: 1100, + duration: 600, + args: { + 'runtime-call-stats': { + Blink_Layout_UpdateLayout: [10, 300], + Blink_Style_UpdateStyle: [15, 200], + } + } + })); + + // V8 slices in the overlap range of animation + response + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + type: tr.e.v8.V8ThreadSlice, + start: 2000, + duration: 99, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 1], + HandleApiCall: [2, 2], + CompileFunctionLiteral: [3, 7], + LoadIC_Miss: [4, 4], + ParseLazy: [5, 5], + OptimizeCode: [6, 6], + FunctionCallback: [7, 7], + AllocateInTargetSpace: [8, 8], + API_Object_Get: [9, 9] + } + } + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'disabled-by-default-v8.runtime_stats', + type: tr.e.v8.V8ThreadSlice, + start: 2010, + duration: 79, + args: { + 'runtime-call-stats': { + Blink_Layout_UpdateLayout: [1, 10], + Blink_Style_UpdateStyle: [1, 10], + } + } + })); + + // V8 slices in animation UE range. + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + type: tr.e.v8.V8ThreadSlice, + start: 2200, + duration: 700, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 1], + HandleApiCall: [2, 2], + CompileFullCode: [3, 3], + StoreIC_Miss: [4, 4], + ParseLazy: [5, 5], + OptimizeCode: [6, 6], + FunctionCallback: [7, 7], + AllocateInTargetSpace: [8, 8], + API_Object_Get: [9, 9] + } + } + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'disabled-by-default-v8.runtime_stats', + type: tr.e.v8.V8ThreadSlice, + start: 2300, + duration: 600, + args: { + 'runtime-call-stats': { + Blink_Layout_UpdateLayout: [20, 200], + Blink_Style_UpdateStyle: [15, 150], + } + } + })); + + // Add V8 slices corresponding to Idle UE. + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + type: tr.e.v8.V8ThreadSlice, + start: 3001, + duration: 499, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 11], + HandleApiCall: [2, 22], + CompileFullCode: [3, 33], + LoadIC_Miss: [4, 44], + ParseLazy: [5, 55], + OptimizeCode: [6, 66], + FunctionCallback: [7, 77], + AllocateInTargetSpace: [8, 88], + API_Object_Get: [9, 99] + } + } + })); + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'disabled-by-default-v8.runtime_stats', + type: tr.e.v8.V8ThreadSlice, + start: 3100, + duration: 300, + args: { + 'runtime-call-stats': { + Blink_Layout_UpdateLayout: [1, 10], + Blink_Style_UpdateStyle: [10, 100], + } + } + })); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.v8.runtimeStatsTotalMetric(histograms, model); + assert.strictEqual(histograms.length, 270); + + // Check total: + checkRuntimeHistogram_(histograms, 'IC', 20, 0.109); + checkRuntimeHistogram_(histograms, 'API', 45, 0.234); + checkRuntimeHistogram_(histograms, 'Total', 225, 1.21); + checkRuntimeHistogram_(histograms, 'Blink C++', 35, 0.184); + checkRuntimeHistogram_(histograms, 'V8-Only', 190, 1.026); + checkRuntimeHistogram_(histograms, 'Blink_Layout', 62, 0.67); + + // Check Load bucket: + checkRuntimeHistogram_(histograms, 'Load_Parse', 5, 0.014); + checkRuntimeHistogram_(histograms, 'Load_JavaScript', 1, 0.01); + checkRuntimeHistogram_(histograms, 'Load_Blink C++', 7, 0.016); + checkRuntimeHistogram_(histograms, 'Load_V8-Only', 38, 0.11); + checkRuntimeHistogram_(histograms, 'Load_Blink_Style', 20, 0.1); + + // Check Response bucket: + checkRuntimeHistogram_(histograms, 'Response_Parse', 10, 0.06); + checkRuntimeHistogram_(histograms, 'Response_Compile', 6, 0.04); + checkRuntimeHistogram_(histograms, 'Response_Blink_Layout', 11, 0.31); + + // Check Animation bucket: + checkRuntimeHistogram_(histograms, 'Animation_Parse', 10, 0.01); + checkRuntimeHistogram_(histograms, 'Animation_Blink_Style', 16, 0.16); + + // Check Idle bucket: + checkRuntimeHistogram_(histograms, 'Idle_Parse', 5, 0.055); + checkRuntimeHistogram_(histograms, 'Idle_Blink_Parsing', 0, 0); + }); + + test('runtimeStatsMetricTotalNoUE', function() { + // Test that total v8 count works even without UE. + // The renderer thread timeline looks like: + // + // * [V8.NewInstance] * [ V8.Execute ] * + // | + // v + // First navigation + // 200 + + const model = tr.c.TestUtils.newModel(function(model) { + const rendererProcess = model.getOrCreateProcess(1984); + const mainThread = rendererProcess.getOrCreateThread(2); + mainThread.name = 'CrRendererMain'; + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'blink.user_timing', + title: 'navigationStart', + start: 200, + duration: 0.0, + args: {frame: '0xdeadbeef'} + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.newInstance', + type: tr.e.v8.V8ThreadSlice, + start: 300, + duration: 600, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 10], + HandleApiCall: [2, 11], + CompileFullCode: [3, 12], + StoreIC_Miss: [4, 13], + ParseLazy: [5, 14], + OptimizeCode: [6, 15], + FunctionCallback: [7, 16], + AllocateInTargetSpace: [8, 17], + API_Object_Get: [9, 18] + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'disabled-by-default-v8.runtime_stats', + type: tr.e.v8.V8ThreadSlice, + start: 350, + duration: 400, + args: { + 'runtime-call-stats': { + Blink_Layout_UpdateLayout: [5, 100], + Blink_Style_UpdateStyle: [2, 50], + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'V8.Execute', + type: tr.e.v8.V8ThreadSlice, + start: 1100, + duration: 800, + args: { + 'runtime-call-stats': { + JS_Execution: [1, 11], + HandleApiCall: [2, 22], + CompileFullCode: [3, 33], + LoadIC_Miss: [4, 44], + ParseFunctionLiteral: [5, 55], + OptimizeCode: [6, 66], + FunctionCallback: [7, 77], + AllocateInTargetSpace: [8, 88], + API_Context_New: [9, 99] + } + } + })); + + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'v8', + title: 'disabled-by-default-v8.runtime_stats', + type: tr.e.v8.V8ThreadSlice, + start: 1200, + duration: 500, + args: { + 'runtime-call-stats': { + Blink_Layout_UpdateLayout: [10, 200], + Blink_Style_UpdateStyle: [10, 100], + } + } + })); + }); + + const histograms = new tr.v.HistogramSet(); + tr.metrics.v8.runtimeStatsTotalMetric(histograms, model); + assert.strictEqual(histograms.length, 54); + + // Check total: + checkRuntimeHistogram_(histograms, 'IC', 8, 0.057); + checkRuntimeHistogram_(histograms, 'API', 18, 0.117); + checkRuntimeHistogram_(histograms, 'Parse', 10, 0.069); + checkRuntimeHistogram_(histograms, 'Total', 90, 0.621); + checkRuntimeHistogram_(histograms, 'Blink C++', 14, 0.093); + checkRuntimeHistogram_(histograms, 'V8-Only', 76, 0.528); + checkRuntimeHistogram_(histograms, 'Blink_Layout', 15, 0.3); + checkRuntimeHistogram_(histograms, 'Blink_Style', 12, 0.15); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils.html new file mode 100644 index 00000000000..b9ec7ed80bd --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils.html @@ -0,0 +1,490 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/base/category_util.html"> +<link rel="import" href="/tracing/base/math/piecewise_linear_function.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/math/range_utils.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.v8.utils', function() { + // The title of the idle task event. + const IDLE_TASK_EVENT = 'SingleThreadIdleTaskRunner::RunTask'; + + // V8 execution event. + const V8_EXECUTE = 'V8.Execute'; + + // GC events start with this prefix. + const GC_EVENT_PREFIX = 'V8.GC'; + + // Special handling is required for full GCs inside low memory notification. + const FULL_GC_EVENT = 'V8.GCCompactor'; + + const LOW_MEMORY_EVENT = 'V8.GCLowMemoryNotification'; + + const MAJOR_GC_EVENT = 'MajorGC'; + const MINOR_GC_EVENT = 'MinorGC'; + + // Maps the top-level GC events in timeline to telemetry friendly names. + const TOP_GC_EVENTS = { + 'V8.GCCompactor': 'v8-gc-full-mark-compactor', + 'V8.GCFinalizeMC': 'v8-gc-latency-mark-compactor', + 'V8.GCFinalizeMCReduceMemory': 'v8-gc-memory-mark-compactor', + 'V8.GCIncrementalMarking': 'v8-gc-incremental-step', + 'V8.GCIncrementalMarkingFinalize': 'v8-gc-incremental-finalize', + 'V8.GCIncrementalMarkingStart': 'v8-gc-incremental-start', + 'V8.GCPhantomHandleProcessingCallback': 'v8-gc-phantom-handle-callback', + 'V8.GCScavenger': 'v8-gc-scavenger' + }; + + const MARK_COMPACTOR_EVENTS = new Set([ + 'V8.GCCompactor', + 'V8.GCFinalizeMC', + 'V8.GCFinalizeMCReduceMemory', + 'V8.GCIncrementalMarking', + 'V8.GCIncrementalMarkingFinalize', + 'V8.GCIncrementalMarkingStart', + 'V8.GCPhantomHandleProcessingCallback' + ]); + + const LOW_MEMORY_MARK_COMPACTOR = 'v8-gc-low-memory-mark-compactor'; + + /** + * Finds the first parent of the |event| for which the |predicate| holds. + */ + function findParent(event, predicate) { + let parent = event.parentSlice; + while (parent) { + if (predicate(parent)) { + return parent; + } + parent = parent.parentSlice; + } + return null; + } + + function isIdleTask(event) { + return event.title === IDLE_TASK_EVENT; + } + + function isLowMemoryEvent(event) { + return event.title === LOW_MEMORY_EVENT; + } + + function isV8Event(event) { + return event.title.startsWith('V8.'); + } + + function isV8ExecuteEvent(event) { + return event.title === V8_EXECUTE; + } + + function isTopV8ExecuteEvent(event) { + return isV8ExecuteEvent(event) && findParent(isV8ExecuteEvent) === null; + } + + function isGarbageCollectionEvent(event) { + // Low memory notification is handled specially because it contains + // several full mark compact events. + return event.title && event.title.startsWith(GC_EVENT_PREFIX) && + event.title !== LOW_MEMORY_EVENT; + } + + function isTopGarbageCollectionEvent(event) { + return event.title in TOP_GC_EVENTS; + } + + function isForcedGarbageCollectionEvent(event) { + return findParent(event, isLowMemoryEvent) !== null; + } + + function isSubGarbageCollectionEvent(event) { + // To reduce number of results, we return only the first level of GC + // subevents. Some subevents are nested in MajorGC or MinorGC events, so + // we have to check for it explicitly. + return isGarbageCollectionEvent(event) && + event.parentSlice && + (isTopGarbageCollectionEvent(event.parentSlice) || + event.parentSlice.title === MAJOR_GC_EVENT || + event.parentSlice.title === MINOR_GC_EVENT); + } + + function isNotForcedTopGarbageCollectionEvent(event) { + // We exclude garbage collection events forced by benchmark runner, + // because they cannot happen in real world. + return tr.metrics.v8.utils.isTopGarbageCollectionEvent(event) && + !tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event); + } + + function isNotForcedSubGarbageCollectionEvent(event) { + // We exclude garbage collection events forced by benchmark runner, + // because they cannot happen in real world. + return tr.metrics.v8.utils.isSubGarbageCollectionEvent(event) && + !tr.metrics.v8.utils.isForcedGarbageCollectionEvent(event); + } + + function isFullMarkCompactorEvent(event) { + return event.title === 'V8.GCCompactor'; + } + + function isMarkCompactorSummaryEvent(event) { + return event.title === 'V8.GCMarkCompactorSummary'; + } + + function isMarkCompactorMarkingSummaryEvent(event) { + return event.title === 'V8.GCMarkCompactorMarkingSummary'; + } + + function isIncrementalMarkingEvent(event) { + return event.title.startsWith('V8.GCIncrementalMarking'); + } + + function isLatencyMarkCompactorEvent(event) { + return event.title === 'V8.GCFinalizeMC'; + } + + function isMemoryMarkCompactorEvent(event) { + return event.title === 'V8.GCFinalizeMCReduceMemory'; + } + + function isScavengerEvent(event) { + return event.title === 'V8.GCScavenger'; + } + + function isCompileOptimizeRCSCategory(name) { + return name === 'Optimize'; + } + + function isCompileUnoptimizeRCSCategory(name) { + return name === 'Compile'; + } + + function isCompileParseRCSCategory(name) { + return name === 'Parse'; + } + + function isCompileRCSCategory(name) { + return name === 'Compile' || name === 'Optimize' || name === 'Parse'; + } + + function isV8RCSEvent(event) { + return event instanceof tr.e.v8.V8ThreadSlice; + } + + function isMarkCompactorEvent(event) { + return MARK_COMPACTOR_EVENTS.has(event.title); + } + + function isNotForcedMarkCompactorEvent(event) { + return !isForcedGarbageCollectionEvent(event) && + isMarkCompactorEvent(event); + } + + function forcedGCEventName() { + return LOW_MEMORY_EVENT; + } + + function topGarbageCollectionEventName(event) { + if (event.title === FULL_GC_EVENT) { + // Full mark compact events inside a low memory notification + // are counted as low memory mark compacts. + if (findParent(event, isLowMemoryEvent)) { + return LOW_MEMORY_MARK_COMPACTOR; + } + } + return TOP_GC_EVENTS[event.title]; + } + + function subGarbageCollectionEventName(event) { + const topEvent = findParent(event, isTopGarbageCollectionEvent); + const prefix = topEvent ? topGarbageCollectionEventName(topEvent) : + 'unknown'; + // Remove redundant prefixes and convert to lower case. + const name = event.title.replace('V8.GC_MC_', '') + .replace('V8.GC_SCAVENGER_', '') + .replace('V8.GC_', '') + .replace(/_/g, '-').toLowerCase(); + return prefix + '-' + name; + } + + /** + * Filters events using the |filterCallback|, then groups events by the user + * the name computed using the |nameCallback|, and then invokes + * the |processCallback| with the grouped events. + * @param {Function} filterCallback Takes an event and returns a boolean. + * @param {Function} nameCallback Takes event and returns a string. + * @param {Function} processCallback Takes a name, and an array of events. + */ + function groupAndProcessEvents(model, filterCallback, + nameCallback, processCallback) { + // Map: name -> [events]. + const nameToEvents = {}; + for (const event of model.getDescendantEvents()) { + if (!filterCallback(event)) continue; + const name = nameCallback(event); + nameToEvents[name] = nameToEvents[name] || []; + nameToEvents[name].push(event); + } + for (const [name, events] of Object.entries(nameToEvents)) { + processCallback(name, events); + } + } + + /** + * Given a list of intervals, returns a new list with all overalapping + * intervals merged into a single interval. + */ + function unionOfIntervals(intervals) { + if (intervals.length === 0) return []; + return tr.b.math.mergeRanges( + intervals.map(x => { return { min: x.start, max: x.end }; }), 1e-6, + function(ranges) { + return { + start: ranges.reduce( + (acc, x) => Math.min(acc, x.min), ranges[0].min), + end: ranges.reduce((acc, x) => Math.max(acc, x.max), ranges[0].max) + }; + } + ); + } + + function hasV8Stats(globalMemoryDump) { + let v8stats = undefined; + globalMemoryDump.iterateContainerDumps(function(dump) { + v8stats = v8stats || dump.getMemoryAllocatorDumpByFullName('v8'); + }); + return !!v8stats; + } + + function rangeForMemoryDumps(model) { + const startOfFirstDumpWithV8 = + model.globalMemoryDumps.filter(hasV8Stats).reduce( + (start, dump) => Math.min(start, dump.start), Infinity); + // Empty range. + if (startOfFirstDumpWithV8 === Infinity) return new tr.b.math.Range(); + return tr.b.math.Range.fromExplicitRange(startOfFirstDumpWithV8, Infinity); + } + + /** + * An end-point of a window that is sliding from left to right + * over |points| starting from time |start|. + * It is intended to be used only by the |mutatorUtilization| function. + * @constructor + */ + class WindowEndpoint { + constructor(start, points) { + this.points = points; + // The index of the last passed point. + this.lastIndex = -1; + // The position of the end-point in the time line. + this.position = start; + this.distanceUntilNextPoint = points[0].position - start; + // The cumulative duration of GC pauses until this position. + this.cummulativePause = 0; + // The number of entered GC intervals. + this.stackDepth = 0; + } + + // Advance the end-point by the given |delta|. + advance(delta) { + if (delta < this.distanceUntilNextPoint) { + this.position += delta; + this.cummulativePause += this.stackDepth > 0 ? delta : 0; + this.distanceUntilNextPoint = + this.points[this.lastIndex + 1].position - this.position; + } else { + this.position += this.distanceUntilNextPoint; + this.cummulativePause += + this.stackDepth > 0 ? this.distanceUntilNextPoint : 0; + this.distanceUntilNextPoint = 0; + this.lastIndex++; + if (this.lastIndex < this.points.length) { + this.stackDepth += this.points[this.lastIndex].delta; + if (this.lastIndex + 1 < this.points.length) { + this.distanceUntilNextPoint = + this.points[this.lastIndex + 1].position - this.position; + } + } + } + } + } + + /** + * Returns mutator utilization as a piecewise linear function. + * Mutator utilization for a window size w is a function of time mu_w(t) + * that shows how much time in [t, t+w] is left for the mutator relative + * to the time window size. + * More formally: + * mu_w(t) = (w - total_time_spent_in_gc_in(t, t + w)) / w. + * The range of mu_w(t) is [0..1]. + * See "A Parallel, Real-Time Garbage Collector" by Cheng et. al. for + * more info [https://www.cs.cmu.edu/~guyb/papers/gc2001.pdf]. + * + * All parameters must use the same time unit. + * @param {number} start The start time of execution. + * @param {number} end The end time of execution. + * @param {number} timeWindow The size of the time window. + * @param {!Array<!{start: number, end: number}>} intervals The list of + * GC pauses. + */ + function mutatorUtilization(start, end, timeWindow, intervals) { + const mu = new tr.b.math.PiecewiseLinearFunction(); + // If the interval is smaller than the time window, then the function is + // empty. + if (end - start <= timeWindow) { + return mu; + } + + // If there are GC pauses then the mutator utilization is 1.0. + if (intervals.length === 0) { + mu.push(start, 1.0, end - timeWindow, 1.0); + return mu; + } + + intervals = unionOfIntervals(intervals); + + // Create a point for the start and the end of each interval. + const points = []; + for (const interval of intervals) { + points.push({position: interval.start, delta: 1}); + points.push({position: interval.end, delta: -1}); + } + points.sort((a, b) => a.position - b.position); + points.push({position: end, delta: 0}); + + // The left and the right limit of the sliding window. + const left = new WindowEndpoint(start, points); + const right = new WindowEndpoint(start, points); + + // Advance the right end-point until we get the correct window size. + // Allow the floating-point precision errors of this magnitude. + const EPSILON = 1e-6; + while (right.position - left.position < timeWindow - EPSILON) { + right.advance(timeWindow - (right.position - left.position)); + } + + while (right.lastIndex < points.length) { + // Advance the window end-points by the largest possible amount + // without jumping over a point. + const distanceUntilNextPoint = + Math.min(left.distanceUntilNextPoint, right.distanceUntilNextPoint); + const position1 = left.position; + const value1 = right.cummulativePause - left.cummulativePause; + left.advance(distanceUntilNextPoint); + right.advance(distanceUntilNextPoint); + // Add a new mutator utilization segment only if it is non-trivial. + if (distanceUntilNextPoint > 0) { + const position2 = left.position; + const value2 = right.cummulativePause - left.cummulativePause; + mu.push(position1, 1.0 - value1 / timeWindow, + position2, 1.0 - value2 / timeWindow); + } + } + return mu; + } + + /** + * Computes the minimum mutator utilization (MMU) metric for the given time + * windows and the given renderers. The results are added as histograms to + * the given histogram set. + * + * For example, passing 'v8-gc-mark-compactor-mmu' as the metric name and + * [16, 50, 100] as the time windows will produce the following: + * - v8-gc-mark-compactor-mmu-16ms_window + * - v8-gc-mark-compactor-mmu-50ms_window + * - v8-gc-mark-compactor-mmu-100ms_window + * + * @param {!string} metricName the name of the metric. + * @param {!function(tr.b.Event): boolean} eventFilter the predicate for + * filtering the events that will be used for computing the MMU. + * @param {!Array.<tr.model.helpers.ChromeRendererHelper>} rendererHelpers + * @param {!tr.v.HistogramSet} histograms + */ + function addMutatorUtilization( + metricName, eventFilter, timeWindows, rendererHelpers, histograms) { + const histogramMap = new Map(); + + for (const timeWindow of timeWindows) { + const summaryOptions = { + avg: false, + count: false, + max: false, + min: true, + std: false, + sum: false + }; + const description = + `The minimum mutator utilization in ${timeWindow}ms time window`; + const histogram = histograms.createHistogram( + `${metricName}-${timeWindow}ms_window`, + tr.b.Unit.byName.normalizedPercentage_biggerIsBetter, + [], {summaryOptions, description}); + histogramMap.set(timeWindow, histogram); + } + + for (const rendererHelper of rendererHelpers) { + if (rendererHelper.isChromeTracingUI) continue; + const pauses = []; + for (const event of rendererHelper.mainThread.sliceGroup.childEvents()) { + if (eventFilter(event) && event.end > event.start) { + pauses.push({start: event.start, end: event.end}); + } + } + pauses.sort((a, b) => a.start - b.start); + const start = rendererHelper.mainThread.bounds.min; + const end = rendererHelper.mainThread.bounds.max; + for (const timeWindow of timeWindows) { + const mu = mutatorUtilization(start, end, timeWindow, pauses); + histogramMap.get(timeWindow).addSample(mu.min); + } + } + } + + + return { + addMutatorUtilization, + findParent, + forcedGCEventName, + groupAndProcessEvents, + isForcedGarbageCollectionEvent, + isFullMarkCompactorEvent, + isGarbageCollectionEvent, + isIdleTask, + isIncrementalMarkingEvent, + isLatencyMarkCompactorEvent, + isLowMemoryEvent, + isMarkCompactorSummaryEvent, + isMarkCompactorMarkingSummaryEvent, + isMemoryMarkCompactorEvent, + isNotForcedMarkCompactorEvent, + isNotForcedTopGarbageCollectionEvent, + isNotForcedSubGarbageCollectionEvent, + isScavengerEvent, + isSubGarbageCollectionEvent, + isTopGarbageCollectionEvent, + isTopV8ExecuteEvent, + isV8Event, + isV8ExecuteEvent, + isV8RCSEvent, + isCompileRCSCategory, + isCompileOptimizeRCSCategory, + isCompileUnoptimizeRCSCategory, + isCompileParseRCSCategory, + mutatorUtilization, + rangeForMemoryDumps, + subGarbageCollectionEventName, + topGarbageCollectionEventName, + unionOfIntervals, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils_test.html new file mode 100644 index 00000000000..dc45524508a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/utils_test.html @@ -0,0 +1,189 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/v8/utils.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function addDumpWithAllocator(model, process, allocator, start) { + const gmd = tr.model.MemoryDumpTestUtils.addGlobalMemoryDump( + model, {ts: start}); + const pmd = tr.model.MemoryDumpTestUtils.addProcessMemoryDump(gmd, process); + pmd.memoryAllocatorDumps = + [tr.model.MemoryDumpTestUtils.newAllocatorDump(pmd, allocator)]; + } + + const unionOfIntervals = tr.metrics.v8.utils.unionOfIntervals; + + function interval(start, end) { + return {start, end}; + } + + /** + * Brute force computation of the mutator utilization. + * The function steps from the start to the end and for each step + * computes the mutator utilization. + */ + function mutatorUtilizationSlow(start, end, timeWindow, intervals) { + const STEP = 0.001; + function timeToIndex(time) { + return Math.floor((time - start) / STEP); + } + const N = timeToIndex(end) + 1; + // bitmap[i] === true means that GC is active at time i. + const bitmap = new Array(N); + for (let i = 0; i < N; i++) { + bitmap[i] = false; + } + intervals.forEach(function(interval) { + const start = timeToIndex(interval.start); + const end = timeToIndex(interval.end); + for (let i = start; i < end; i++) { + bitmap[i] = true; + } + }); + const pause = new Array(N); + for (let i = 0; i < N; i++) { + pause[i] = (i > 0 ? pause[i - 1] : 0) + (bitmap[i] ? 1 : 0); + } + const windowWidth = timeToIndex(timeWindow); + const mu = new Array(Math.max(N - windowWidth, 0)); + for (let i = 0; i < mu.length; i++) { + const value = pause[i + windowWidth] - pause[i]; + mu[i] = 1.0 - value / windowWidth; + } + mu.sort((a, b) => a - b); + return { + average: mu.reduce((acc, x) => (acc + x), 0) / mu.length, + min: mu.reduce((acc, x) => Math.min(acc, x), 0), + max: mu.reduce((acc, x) => Math.max(acc, x), 0), + percentile(percent) { + return mu[Math.floor(percent * (mu.length - 1))]; + } + }; + } + + /** + * Constructs PiecewiseLinearFunction from pieces. + * @param {!Array<!{x1: number, y1: number, x2: number, y2: number}>} pieces + * The list of pieces ordered by the x coordinate. + */ + function createExpectedFunction(pieces) { + const f = new tr.b.math.PiecewiseLinearFunction(); + pieces.forEach(function(p) { + f.push(p.x1, p.y1, p.x2, p.y2); + }); + return f; + } + + test('unionOfIntervals', function() { + assert.deepEqual(unionOfIntervals([]), []); + assert.deepEqual(unionOfIntervals([interval(1, 1)]), [interval(1, 1)]); + assert.deepEqual( + unionOfIntervals([interval(0, 1), interval(1, 2), interval(2, 3)]), + [interval(0, 3)]); + assert.deepEqual( + unionOfIntervals([interval(0, 1), interval(1, 2), interval(3, 3)]), + [interval(0, 2), interval(3, 3)]); + assert.deepEqual( + unionOfIntervals([interval(0, 10), interval(1, 2), interval(3, 3)]), + [interval(0, 10)]); + assert.deepEqual( + unionOfIntervals([interval(0, 10), interval(1, 2), interval(3, 11)]), + [interval(0, 11)]); + assert.deepEqual( + unionOfIntervals([interval(3, 10), interval(1, 2), interval(11, 11)]), + [interval(1, 2), interval(3, 10), interval(11, 11)]); + }); + + test('basicMutatorUtilization', function() { + assert.deepEqual( + tr.metrics.v8.utils.mutatorUtilization(0, 40, 10, [interval(10, 20)]), + createExpectedFunction([ + { x1: 0, y1: 1.0, x2: 10, y2: 0.0 }, + { x1: 10, y1: 0.0, x2: 20, y2: 1.0 }, + { x1: 20, y1: 1.0, x2: 30, y2: 1.0 }]) + ); + assert.deepEqual( + tr.metrics.v8.utils.mutatorUtilization(0, 40, 10, [interval(10, 15)]), + createExpectedFunction([ + { x1: 0, y1: 1.0, x2: 5, y2: 0.5 }, + { x1: 5, y1: 0.5, x2: 10, y2: 0.5 }, + { x1: 10, y1: 0.5, x2: 15, y2: 1.0 }, + { x1: 15, y1: 1.0, x2: 30, y2: 1.0 }]) + ); + assert.deepEqual( + tr.metrics.v8.utils.mutatorUtilization(0, 60, 20, + [interval(30, 35), interval(40, 45)]), + createExpectedFunction([ + { x1: 0, y1: 1.0, x2: 10, y2: 1.0 }, + { x1: 10, y1: 1.0, x2: 15, y2: 0.75 }, + { x1: 15, y1: 0.75, x2: 20, y2: 0.75 }, + { x1: 20, y1: 0.75, x2: 25, y2: 0.5 }, + { x1: 25, y1: 0.5, x2: 30, y2: 0.5 }, + { x1: 30, y1: 0.5, x2: 35, y2: 0.75 }, + { x1: 35, y1: 0.75, x2: 40, y2: 0.75 }]) + ); + }); + + test('mutatorUtilization', function() { + const pauses = [ + interval(10, 20), + interval(15, 23), + interval(30, 31), + interval(33, 34), + interval(60, 61), + interval(61, 63), + interval(80, 88)]; + const actual = tr.metrics.v8.utils.mutatorUtilization(0, 100, 7, pauses); + const expected = mutatorUtilizationSlow(0, 100, 7, pauses); + assert.closeTo(expected.average, actual.average, 1e-3); + assert.closeTo(expected.max, actual.max, 1e-3); + assert.closeTo(expected.min, actual.min, 1e-3); + assert.closeTo(expected.percentile(0.5), actual.percentile(0.5), 1e-3); + assert.closeTo(expected.percentile(0.9), actual.percentile(0.9), 1e-3); + }); + + test('mutatorUtilizationMakesProgress', function() { + const start = 44173.32375717163; + const end = 80378.0457572937; + const pauses = [ + interval(500, 700), + interval(900, 990)]; + const actual = tr.metrics.v8.utils.mutatorUtilization( + start, end, 16.67, pauses); + assert.closeTo(0.9548, actual.average, 1e-3); + }); + + + test('rangeForMemoryDumps', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + addDumpWithAllocator(model, process, 'dummy', 10); + addDumpWithAllocator(model, process, 'v8', 20); + }); + const range = tr.metrics.v8.utils.rangeForMemoryDumps(model); + assert.isFalse(range.isEmpty); + assert.strictEqual(range.min, 20); + assert.strictEqual(range.max, Infinity); + }); + + test('rangeForMemoryDumpsEmpty', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + addDumpWithAllocator(model, process, 'dummy', 10); + addDumpWithAllocator(model, process, 'dummy', 20); + }); + const range = tr.metrics.v8.utils.rangeForMemoryDumps(model); + assert.isTrue(range.isEmpty); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/v8/v8_metrics.html b/chromium/third_party/catapult/tracing/tracing/metrics/v8/v8_metrics.html new file mode 100644 index 00000000000..507d23e6119 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/v8/v8_metrics.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/system_health/memory_metric.html"> +<link rel="import" href="/tracing/metrics/v8/execution_metric.html"> +<link rel="import" href="/tracing/metrics/v8/gc_metric.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.v8', function() { + function v8AndMemoryMetrics(histograms, model) { + tr.metrics.v8.executionMetric(histograms, model); + tr.metrics.v8.gcMetric(histograms, model); + tr.metrics.sh.memoryMetric(histograms, model, + {rangeOfInterest: tr.metrics.v8.utils.rangeForMemoryDumps(model)}); + } + + tr.metrics.MetricRegistry.register(v8AndMemoryMetrics); + + return { + v8AndMemoryMetrics, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric.html new file mode 100644 index 00000000000..1a3f4b4d5af --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric.html @@ -0,0 +1,261 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.vr', function() { + function createHistograms(histograms, name, options, hasCpuTime) { + const createdHistograms = { + wall: histograms.createHistogram( + name + '_wall', tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], + options) + }; + if (hasCpuTime) { + createdHistograms.cpu = histograms.createHistogram(name + '_cpu', + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, [], options); + } + return createdHistograms; + } + + function frameCycleDurationMetric(histograms, model, opt_options) { + const histogramsByEventTitle = new Map(); + histogramsByEventTitle.set('Vr.DrawFrame', + createHistograms(histograms, 'draw_frame', + {description: 'Duration to render one frame'}, true)); + histogramsByEventTitle.set('Vr.AcquireGvrFrame', + createHistograms(histograms, 'acquire_frame', + {description: 'Duration acquire a frame from GVR'}, true)); + histogramsByEventTitle.set( + 'Vr.ProcessControllerInput', + createHistograms( + histograms, 'update_controller', + {description: 'Duration to query input from the controller'}, + true)); + histogramsByEventTitle.set( + 'Vr.ProcessControllerInputForWebXr', + createHistograms( + histograms, 'update_controller_webxr', + {description: 'Duration to query input from the controller ' + + 'for WebXR'}, + true)); + histogramsByEventTitle.set('Vr.SubmitFrameNow', + createHistograms(histograms, 'submit_frame', + {description: 'Duration to submit a frame to GVR'}, true)); + histogramsByEventTitle.set('Vr.PostSubmitDrawOnGpu', + createHistograms(histograms, 'post_submit_draw_on_gpu', + {description: 'Duration to draw a frame on GPU post submit to ' + + 'GVR. Note this duration may include time spent on ' + + 'reprojection'}, false)); + + // TODO(crbug/884259): Remove the following aliases after some time. + histogramsByEventTitle.set('VrShellGl::DrawFrame', + histogramsByEventTitle.get('Vr.DrawFrame')); + histogramsByEventTitle.set('VrShellGl::AcquireFrame', + histogramsByEventTitle.get('Vr.AcquireGvrFrame')); + histogramsByEventTitle.set('VrShellGl::UpdateController', + histogramsByEventTitle.get('Vr.ProcessControllerInput')); + histogramsByEventTitle.set('VrShellGl::DrawFrameSubmitNow', + histogramsByEventTitle.get('Vr.SubmitFrameNow')); + histogramsByEventTitle.set('VrShellGl::PostSubmitDrawOnGpu', + histogramsByEventTitle.get('Vr.PostSubmitDrawOnGpu')); + + // Scene update metrics. + histogramsByEventTitle.set( + 'UiScene::OnBeginFrame.UpdateAnimationsAndOpacity', + createHistograms( + histograms, 'update_animations_and_opacity', + {description: 'Duration to apply animation and opacity changes'}, + true)); + histogramsByEventTitle.set( + 'UiScene::OnBeginFrame.UpdateBindings', + createHistograms( + histograms, 'update_bindings', + {description: 'Duration to push binding values'}, true)); + histogramsByEventTitle.set( + 'UiScene::OnBeginFrame.UpdateLayout', + createHistograms(histograms, 'update_layout', { + description: 'Duration to compute element sizes, layout and textures' + }, true)); + histogramsByEventTitle.set( + 'UiScene::OnBeginFrame.UpdateWorldSpaceTransform', + createHistograms(histograms, 'update_world_space_transforms', { + description: 'Duration to calculate element transforms in world space' + }, true)); + // Draw metrics. + histogramsByEventTitle.set( + 'UiRenderer::DrawUiView', + createHistograms(histograms, 'draw_ui', { + description: 'Duration to draw the UI' + }, true)); + histogramsByEventTitle.set( + 'UiElementRenderer::DrawTexturedQuad', + createHistograms(histograms, 'draw_textured_quad', { + description: 'Duration to draw a textured element' + }, true)); + histogramsByEventTitle.set( + 'UiElementRenderer::DrawGradientQuad', + createHistograms(histograms, 'draw_gradient_quad', { + description: 'Duration to draw a gradient element' + }, true)); + histogramsByEventTitle.set( + 'UiElementRenderer::DrawGradientGridQuad', + createHistograms(histograms, 'draw_gradient_grid_quad', { + description: 'Duration to draw a gradient grid element' + }, true)); + histogramsByEventTitle.set( + 'UiElementRenderer::DrawController', + createHistograms(histograms, 'draw_controller', { + description: 'Duration to draw the controller' + }, true)); + histogramsByEventTitle.set( + 'UiElementRenderer::DrawLaser', + createHistograms(histograms, 'draw_laser', { + description: 'Duration to draw the laser' + }, true)); + histogramsByEventTitle.set( + 'UiElementRenderer::DrawReticle', + createHistograms(histograms, 'draw_reticle', { + description: 'Duration to draw the reticle' + }, true)); + histogramsByEventTitle.set( + 'UiElementRenderer::DrawShadow', + createHistograms(histograms, 'draw_shadow', { + description: 'Duration to draw a shadow element' + }, true)); + histogramsByEventTitle.set( + 'UiElementRenderer::DrawStars', + createHistograms(histograms, 'draw_stars', { + description: 'Duration to draw the stars' + }, true)); + histogramsByEventTitle.set( + 'UiElementRenderer::DrawBackground', + createHistograms(histograms, 'draw_background', { + description: 'Duration to draw the textured background' + }, true)); + histogramsByEventTitle.set( + 'UiElementRenderer::DrawKeyboard', + createHistograms(histograms, 'draw_keyboard', { + description: 'Duration to draw the keyboard' + }, true)); + + // Maps from the GUID of a UiRenderer::DrawUiView slice to each of its + // children slices. + const drawUiSubSlicesMap = new Map(); + + const chromeHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + + let rangeOfInterest = model.bounds; + const userExpectationsOfInterest = [tr.model.um.AnimationExpectation]; + + if (opt_options && opt_options.rangeOfInterest) { + rangeOfInterest = opt_options.rangeOfInterest; + userExpectationsOfInterest.push(tr.model.um.ResponseExpectation); + } + + for (const ue of model.userModel.expectations) { + // Skip user expecations not of the right type or not inside the range of + // interest. + if (ue.initiatorType !== tr.model.um.INITIATOR_TYPE.VR) { + continue; + } + if (!userExpectationsOfInterest.some(function(ueOfInterest) { + return ue instanceof ueOfInterest; + })) { + continue; + } + if (!rangeOfInterest.intersectsExplicitRangeInclusive( + ue.start, ue.end)) { + continue; + } + + for (const helper of chromeHelper.browserHelpers) { + // The events are traced on the GL thread in the browser process. + // Unfortunately, this thread has no name. + // TODO(tiborg): Give GL thread a name and reference the thread by + // the given name. + const glThreads = helper.process.findAllThreadsMatching( + thread => !thread.name); + + for (const glThread of glThreads) { + for (const event of glThread.getDescendantEvents()) { + // Skip events that are neither in the user expecation, range of + // interest nor part of the frame cycle durations. + if (!(histogramsByEventTitle.has(event.title))) { + continue; + } + if (event.start < ue.start || event.end > ue.end) { + continue; + } + if (event.start < rangeOfInterest.min || + event.end > rangeOfInterest.max) { + continue; + } + + // There can be multiple UiElementRenderer::Draw... events per frame + // but this metric should calculate the across frame average of + // durations. Thus, collect UiElementRenderer::Draw... slices here + // in order to process them below. + if (event.parentSlice && + event.parentSlice.title === 'UiRenderer::DrawUiView') { + const guid = event.parentSlice.guid; + if (!drawUiSubSlicesMap.has(guid)) { + drawUiSubSlicesMap.set(guid, []); + } + drawUiSubSlicesMap.get(guid).push(event); + continue; + } + + const {wall: wallHist, cpu: cpuHist} = + histogramsByEventTitle.get(event.title); + wallHist.addSample(event.duration); + if (cpuHist !== undefined) { + cpuHist.addSample(event.cpuDuration); + } + } + } + } + } + + // Calculate the average of the per frame sums of UiElementRenderer::Draw... + // events. + for (const subSlices of drawUiSubSlicesMap.values()) { + const eventMap = new Map(); + for (const event of subSlices) { + if (!eventMap.has(event.title)) { + eventMap.set(event.title, { + wall: 0, + cpu: 0 + }); + } + eventMap.get(event.title).wall += event.duration; + eventMap.get(event.title).cpu += event.cpuDuration; + } + for (const [title, values] of eventMap.entries()) { + const {wall: wallHist, cpu: cpuHist} = + histogramsByEventTitle.get(title); + wallHist.addSample(values.wall); + if (cpuHist !== undefined) { + cpuHist.addSample(values.cpu); + } + } + } + } + + tr.metrics.MetricRegistry.register(frameCycleDurationMetric, { + supportsRangeOfInterest: true, + }); + + return { + frameCycleDurationMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric_test.html new file mode 100644 index 00000000000..e6500713167 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/vr/frame_cycle_duration_metric_test.html @@ -0,0 +1,333 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/vr/frame_cycle_duration_metric.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; +tr.b.unittest.testSuite(function() { + const TOLERANCE = 1e-6; + + function createSubSlices(durations, currentTime, sliceGroup) { + if (durations === undefined) { + return []; + } + const slices = []; + for (const duration of durations) { + const option = { + cat: 'gpu', + title: duration.title, + start: currentTime, + end: currentTime + duration.wall, + }; + if (duration.cpu !== undefined) { + option.cpuStart = currentTime; + option.cpuEnd = currentTime + duration.cpu; + } + const slice = tr.c.TestUtils.newSliceEx(option); + slice.subSlices = createSubSlices( + duration.sub, currentTime + 0.1, sliceGroup); + sliceGroup.pushSlice(slice); + slices.push(slice); + const maxDuration = duration.cpu === undefined ? + duration.wall : Math.max(duration.wall, duration.cpu); + currentTime += maxDuration + 1; + } + return slices; + } + + test('frameCycleDurationMetric', function() { + const durations = [ + { + title: 'Vr.DrawFrame', + wall: 390, + cpu: 340, + sub: [ + {title: 'Vr.AcquireGvrFrame', wall: 2, cpu: 2}, + {title: 'Vr.AcquireGvrFrame', wall: 2.5, cpu: 1.5}, + { + title: 'UiScene::OnBeginFrame.UpdateAnimationsAndOpacity', + wall: 3, + cpu: 2 + }, + { + title: 'UiScene::OnBeginFrame.UpdateAnimationsAndOpacity', + wall: 2, + cpu: 1 + }, + {title: 'UiScene::OnBeginFrame.UpdateBindings', wall: 3, cpu: 2}, + {title: 'UiScene::OnBeginFrame.UpdateBindings', wall: 2, cpu: 1}, + {title: 'UiScene::OnBeginFrame.UpdateLayout', wall: 3, cpu: 2}, + {title: 'UiScene::OnBeginFrame.UpdateLayout', wall: 2, cpu: 1}, + { + title: 'UiScene::OnBeginFrame.UpdateWorldSpaceTransform', + wall: 3, + cpu: 2 + }, + { + title: 'UiScene::OnBeginFrame.UpdateWorldSpaceTransform', + wall: 2, + cpu: 1 + }, + {title: 'Vr.ProcessControllerInput', wall: 1, cpu: 0.5}, + {title: 'Vr.ProcessControllerInput', wall: 0.5, cpu: 0.4}, + {title: 'Vr.ProcessControllerInputForWebXr', wall: 1, cpu: 0.5}, + {title: 'Vr.ProcessControllerInputForWebXr', wall: 1.5, cpu: 0.6}, + { + title: 'UiRenderer::DrawUiView', + wall: 250, + cpu: 230, + sub: [ + {title: 'UiElementRenderer::DrawTexturedQuad', wall: 1, cpu: 0.5}, + {title: 'UiElementRenderer::DrawGradientQuad', wall: 3, cpu: 2}, + {title: 'UiElementRenderer::DrawGradientQuad', wall: 4, cpu: 3}, + {title: 'UiElementRenderer::DrawTexturedQuad', wall: 2, cpu: 1}, + { + title: 'UiElementRenderer::DrawGradientGridQuad', + wall: 5, + cpu: 4 + }, + { + title: 'UiElementRenderer::DrawGradientGridQuad', + wall: 6, + cpu: 5 + }, + {title: 'UiElementRenderer::DrawController', wall: 7, cpu: 6}, + {title: 'UiElementRenderer::DrawController', wall: 8, cpu: 7}, + {title: 'UiElementRenderer::DrawLaser', wall: 9, cpu: 8}, + {title: 'UiElementRenderer::DrawLaser', wall: 10, cpu: 9}, + {title: 'UiElementRenderer::DrawReticle', wall: 11, cpu: 10}, + {title: 'UiElementRenderer::DrawReticle', wall: 12, cpu: 11}, + {title: 'UiElementRenderer::DrawShadow', wall: 13, cpu: 12}, + {title: 'UiElementRenderer::DrawShadow', wall: 14, cpu: 13}, + {title: 'UiElementRenderer::DrawStars', wall: 15, cpu: 14}, + {title: 'UiElementRenderer::DrawStars', wall: 16, cpu: 15}, + {title: 'UiElementRenderer::DrawBackground', wall: 17, cpu: 16}, + {title: 'UiElementRenderer::DrawBackground', wall: 18, cpu: 17}, + {title: 'UiElementRenderer::DrawKeyboard', wall: 19, cpu: 18}, + {title: 'UiElementRenderer::DrawKeyboard', wall: 20, cpu: 19}, + ] + }, + { + title: 'UiRenderer::DrawUiView', + wall: 80, + cpu: 75, + sub: [ + {title: 'UiElementRenderer::DrawTexturedQuad', wall: 2, cpu: 1}, + {title: 'UiElementRenderer::DrawGradientQuad', wall: 3, cpu: 2}, + { + title: 'UiElementRenderer::DrawGradientGridQuad', + wall: 4, + cpu: 3 + }, + {title: 'UiElementRenderer::DrawController', wall: 5, cpu: 4}, + {title: 'UiElementRenderer::DrawLaser', wall: 6, cpu: 5}, + {title: 'UiElementRenderer::DrawReticle', wall: 7, cpu: 6}, + {title: 'UiElementRenderer::DrawShadow', wall: 8, cpu: 7}, + {title: 'UiElementRenderer::DrawStars', wall: 9, cpu: 8}, + {title: 'UiElementRenderer::DrawBackground', wall: 10, cpu: 9}, + {title: 'UiElementRenderer::DrawKeyboard', wall: 11, cpu: 10}, + ] + }, + {title: 'Vr.SubmitFrameNow', wall: 3, cpu: 0.5}, + {title: 'Vr.SubmitFrameNow', wall: 3.5, cpu: 0.5}, + {title: 'Vr.PostSubmitDrawOnGpu', wall: 3}, + {title: 'Vr.PostSubmitDrawOnGpu', wall: 4}, + ] + }, + {title: 'Vr.DrawFrame', wall: 20, cpu: 10}, + // This event should be filtered out by the user expecation. + {title: 'Vr.DrawFrame', wall: 20, cpu: 10}, + ]; + const histograms = new tr.v.HistogramSet(); + const model = tr.c.TestUtils.newModel(function(model) { + model.userModel.expectations.push( + new tr.model.um.AnimationExpectation(model, + tr.model.um.INITIATOR_TYPE.VR, 0, 411)); + const browserProcess = model.getOrCreateProcess(0); + const browserMainThread = browserProcess.getOrCreateThread(0); + browserMainThread.name = 'CrBrowserMain'; + const browserGlThread = browserProcess.getOrCreateThread(1); + const group = browserGlThread.sliceGroup; + createSubSlices(durations, 0, group); + group.createSubSlices(); + }); + + tr.metrics.vr.frameCycleDurationMetric(histograms, model); + + assert.closeTo(histograms.getHistogramNamed('draw_frame_wall').average, + 205, TOLERANCE); + assert.closeTo(histograms.getHistogramNamed('draw_frame_cpu').average, + 175, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('acquire_frame_wall').average, + 2.25, TOLERANCE); + assert.closeTo(histograms.getHistogramNamed('acquire_frame_cpu').average, + 1.75, TOLERANCE); + + assert.isUndefined( + histograms.getHistogramNamed('post_submit_draw_on_gpu_cpu')); + assert.closeTo( + histograms.getHistogramNamed('post_submit_draw_on_gpu_wall').average, + 3.5, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('update_controller_wall').average, + 0.75, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('update_controller_cpu').average, + 0.45, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('update_controller_webxr_wall').average, + 1.25, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('update_controller_webxr_cpu').average, + 0.55, TOLERANCE); + + assert.closeTo(histograms.getHistogramNamed('submit_frame_wall').average, + 3.25, TOLERANCE); + assert.closeTo(histograms.getHistogramNamed('submit_frame_cpu').average, + 0.5, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('update_animations_and_opacity_wall') + .average, + 2.5, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('update_animations_and_opacity_cpu') + .average, + 1.5, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('update_bindings_wall').average, 2.5, + TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('update_bindings_cpu').average, 1.5, + TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('update_layout_wall').average, 2.5, + TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('update_layout_cpu').average, 1.5, + TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('update_world_space_transforms_wall') + .average, + 2.5, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('update_world_space_transforms_cpu') + .average, + 1.5, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_ui_wall') + .average, + 165, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_ui_cpu') + .average, + 152.5, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_textured_quad_wall') + .average, + 2.5, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_textured_quad_cpu') + .average, + 1.25, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_gradient_quad_wall') + .average, + 5, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_gradient_quad_cpu') + .average, + 3.5, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_gradient_grid_quad_wall') + .average, + 7.5, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_gradient_grid_quad_cpu') + .average, + 6, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_controller_wall') + .average, + 10, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_controller_cpu') + .average, + 8.5, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_laser_wall') + .average, + 12.5, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_laser_cpu') + .average, + 11, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_reticle_wall') + .average, + 15, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_reticle_cpu') + .average, + 13.5, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_shadow_wall') + .average, + 17.5, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_shadow_cpu') + .average, + 16.0, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_stars_wall') + .average, + 20, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_stars_cpu') + .average, + 18.5, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_background_wall') + .average, + 22.5, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_background_cpu') + .average, + 21, TOLERANCE); + + assert.closeTo( + histograms.getHistogramNamed('draw_keyboard_wall') + .average, + 25, TOLERANCE); + assert.closeTo( + histograms.getHistogramNamed('draw_keyboard_cpu') + .average, + 23.5, TOLERANCE); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric.html new file mode 100644 index 00000000000..254267053ea --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric.html @@ -0,0 +1,117 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/value/histogram.html"> + +<script> +'use strict'; + +tr.exportTo('tr.metrics.vr', function() { + function webvrMetric(histograms, model, opt_options) { + // Maps VR trace counters to histogram. + const WEBVR_COUNTERS = new Map([ + ['gpu.WebVR FPS', { + name: 'webvr_fps', + unit: tr.b.Unit.byName.count_biggerIsBetter, + samples: {}, + options: { + description: 'WebVR frame per second', + binBoundaries: tr.v.HistogramBinBoundaries.createLinear(20, 120, 25), + }, + }], + ['gpu.WebVR frame time (ms)', { + name: 'webvr_frame_time', + unit: tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + samples: {}, + options: { + description: 'WebVR frame time in ms', + binBoundaries: tr.v.HistogramBinBoundaries.createLinear(20, 120, 25), + }, + }], + ['gpu.WebVR pose prediction (ms)', { + name: 'webvr_pose_prediction', + unit: tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, + samples: {}, + options: { + description: 'WebVR pose prediction in ms', + binBoundaries: tr.v.HistogramBinBoundaries.createLinear(20, 120, 25), + }, + }], + ]); + + for (const ue of model.userModel.expectations) { + const rangeOfInterestEnabled = opt_options && opt_options.rangeOfInterest; + if (rangeOfInterestEnabled && + !opt_options.rangeOfInterest.intersectsExplicitRangeInclusive( + ue.start, ue.end)) { + continue; + } + + // By default, only do calculations in the VR animation expectation, i.e. + // some time after we've entered VR, in order to avoid skewed results + // caused by VR entry, but allow calculation on the response expectation + // if we've manually selected it as a range of interest + if (ue.initiatorType !== tr.model.um.INITIATOR_TYPE.VR) continue; + if (!rangeOfInterestEnabled) { + if (!(ue instanceof tr.model.um.AnimationExpectation)) continue; + } else { + if (!(ue instanceof tr.model.um.AnimationExpectation || + ue instanceof tr.model.um.ResponseExpectation)) continue; + } + + for (const counter of model.getAllCounters()) { + if (!(WEBVR_COUNTERS.has(counter.id))) continue; + + for (const series of counter.series) { + if (!(series.name in WEBVR_COUNTERS.get(counter.id).samples)) { + WEBVR_COUNTERS.get(counter.id).samples[series.name] = []; + } + for (const sample of series.samples) { + if (sample.timestamp < ue.start || sample.timestamp >= ue.end) { + continue; + } + if (rangeOfInterestEnabled && + !opt_options.rangeOfInterest.intersectsExplicitRangeInclusive( + sample.timestamp, sample.timestamp)) { + continue; + } + + WEBVR_COUNTERS.get(counter.id).samples[series.name].push( + sample.value); + } + } + } + } + + // Make sure we always report a value for WebVR FPS so that failing to + // submit frames will show up as a regression + if (!('value' in WEBVR_COUNTERS.get('gpu.WebVR FPS').samples)) { + WEBVR_COUNTERS.get('gpu.WebVR FPS').samples.value = [0]; + } + + for (const [key, value] of WEBVR_COUNTERS) { + for (const [seriesName, samples] of Object.entries(value.samples)) { + let histogramName = value.name; + if (seriesName !== 'value') { + histogramName = `${histogramName}_${seriesName}`; + } + histograms.createHistogram(histogramName, value.unit, + samples, value.options); + } + } + } + + tr.metrics.MetricRegistry.register(webvrMetric, { + supportsRangeOfInterest: true, + }); + + return { + webvrMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric_test.html new file mode 100644 index 00000000000..75d73aeeb8a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/vr/webvr_metric_test.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/metrics/vr/webvr_metric.html"> +<link rel="import" href="/tracing/model/counter.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createModel() { + const model = tr.c.TestUtils.newModel( + function(model) { + model.userModel.expectations.push( + new tr.model.um.AnimationExpectation(model, + tr.model.um.INITIATOR_TYPE.VR, 0, 5)); + const process = model.getOrCreateProcess(1); + const fpsCounter = process.getOrCreateCounter('gpu', 'WebVR FPS'); + const fpsSeries = new tr.model.CounterSeries('value', 0); + fpsSeries.addCounterSample(1, 59); + fpsSeries.addCounterSample(3, 60); + fpsCounter.addSeries(fpsSeries); + + const frameTimeCounter = process.getOrCreateCounter( + 'gpu', 'WebVR frame time (ms)'); + const renderingSeries = new tr.model.CounterSeries('rendering', 0); + renderingSeries.addCounterSample(1, 3); + renderingSeries.addCounterSample(2, 4); + frameTimeCounter.addSeries(renderingSeries); + const javascriptSeries = new tr.model.CounterSeries('javascript', 0); + javascriptSeries.addCounterSample(1, 5); + javascriptSeries.addCounterSample(2, 6); + frameTimeCounter.addSeries(javascriptSeries); + }); + return model; + } + + test('webvrMetric', function() { + const histograms = new tr.v.HistogramSet(); + const model = createModel(); + tr.metrics.vr.webvrMetric(histograms, model); + + const fpsValue = histograms.getHistogramNamed('webvr_fps'); + assert.strictEqual(fpsValue.max, 60); + assert.strictEqual(fpsValue.min, 59); + assert.strictEqual(fpsValue.average, 59.5); + + const renderingValue = histograms.getHistogramNamed( + 'webvr_frame_time_rendering'); + assert.strictEqual(renderingValue.max, 4); + assert.strictEqual(renderingValue.min, 3); + assert.strictEqual(renderingValue.average, 3.5); + + const javascriptValue = histograms.getHistogramNamed( + 'webvr_frame_time_javascript'); + assert.strictEqual(javascriptValue.max, 6); + assert.strictEqual(javascriptValue.min, 5); + assert.strictEqual(javascriptValue.average, 5.5); + }); + + test('webvrMetric_alwaysReportFps', function() { + const histograms = new tr.v.HistogramSet(); + const model = tr.c.TestUtils.newModel(); + tr.metrics.vr.webvrMetric(histograms, model); + + const fpsValue = histograms.getHistogramNamed('webvr_fps'); + assert.strictEqual(fpsValue.max, 0); + assert.strictEqual(fpsValue.min, 0); + + const renderingValue = histograms.getHistogramNamed( + 'webvr_frame_time_rendering'); + assert.strictEqual(renderingValue, undefined); + + const javascriptValue = histograms.getHistogramNamed( + 'webvr_frame_time_javascript'); + assert.strictEqual(javascriptValue, undefined); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html b/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html new file mode 100644 index 00000000000..a8414ec46d4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html @@ -0,0 +1,364 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/metrics/metric_registry.html"> +<link rel="import" href="/tracing/metrics/v8/utils.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/value/histogram.html"> + + +<script> +'use strict'; + +tr.exportTo('tr.metrics.webrtc', function() { + const DISPLAY_HERTZ = 60.0; + const VSYNC_DURATION_US = 1e6 / DISPLAY_HERTZ; + // How much more severe is a 'Badly out of sync' render event compared to an + // 'Out of sync' one when calculating the smoothness score. + const SEVERITY = 3; + // How many vsyncs a frame should be displayed to be considered frozen. + const FROZEN_FRAME_VSYNC_COUNT_THRESHOLD = 6; + + const WEB_MEDIA_PLAYER_UPDATE_TITLE = 'UpdateCurrentFrame'; + // These four are args for WebMediaPlayerMS update events. + const IDEAL_RENDER_INSTANT_NAME = 'Ideal Render Instant'; + const ACTUAL_RENDER_BEGIN_NAME = 'Actual Render Begin'; + const ACTUAL_RENDER_END_NAME = 'Actual Render End'; + // The events of interest have a 'Serial' argument which represents the + // stream ID. + const STREAM_ID_NAME = 'Serial'; + + const REQUIRED_EVENT_ARGS_NAMES = [ + IDEAL_RENDER_INSTANT_NAME, ACTUAL_RENDER_BEGIN_NAME, ACTUAL_RENDER_END_NAME, + STREAM_ID_NAME + ]; + + // By default, we store a single value, so we only need one of the + // statistics to keep track. We choose the average for that. + const SUMMARY_OPTIONS = tr.v.Histogram.AVERAGE_ONLY_SUMMARY_OPTIONS; + + const count_smallerIsBetter = + tr.b.Unit.byName.count_smallerIsBetter; + const percentage_biggerIsBetter = + tr.b.Unit.byName.normalizedPercentage_biggerIsBetter; + const percentage_smallerIsBetter = + tr.b.Unit.byName.normalizedPercentage_smallerIsBetter; + const timeDurationInMs_smallerIsBetter = + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter; + const unitlessNumber_biggerIsBetter = + tr.b.Unit.byName.unitlessNumber_biggerIsBetter; + + /* + * Verify that the event is a valid event. + * + * An event is valid if it is a UpdateCurrentFrame event, + * and has all of the mandatory arguments. See MANDATORY above. + */ + function isValidEvent(event) { + if (event.title !== WEB_MEDIA_PLAYER_UPDATE_TITLE || !event.args) { + return false; + } + for (const parameter of REQUIRED_EVENT_ARGS_NAMES) { + if (!(parameter in event.args)) { + return false; + } + } + return true; + } + + function webrtcRenderingMetric(histograms, model) { + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + let webMediaPlayerMSEvents = []; + for (const rendererPid in modelHelper.rendererHelpers) { + const rendererHelper = modelHelper.rendererHelpers[rendererPid]; + const compositorThread = rendererHelper.compositorThread; + if (compositorThread !== undefined) { + webMediaPlayerMSEvents = webMediaPlayerMSEvents.concat( + compositorThread.sliceGroup.slices.filter(isValidEvent)); + } + } + const eventsByStreamName = tr.b.groupIntoMap( + webMediaPlayerMSEvents, + event => event.args[STREAM_ID_NAME] + ); + for (const [streamName, events] of eventsByStreamName) { + getTimeStats(histograms, streamName, events); + } + } + + tr.metrics.MetricRegistry.register(webrtcRenderingMetric); + + function getTimeStats(histograms, streamName, events) { + const frameHist = getFrameDistribution(histograms, events); + addFpsFromFrameDistribution(histograms, frameHist); + addFreezingScore(histograms, frameHist); + + const driftTimeStats = getDriftStats(events); + histograms.createHistogram('WebRTCRendering_drift_time', + timeDurationInMs_smallerIsBetter, driftTimeStats.driftTime, { + summaryOptions: { + count: false, + min: false, + percentile: [0.75, 0.9], + }, + }); + histograms.createHistogram('WebRTCRendering_rendering_length_error', + percentage_smallerIsBetter, + driftTimeStats.renderingLengthError, { + summaryOptions: SUMMARY_OPTIONS, + }); + + const smoothnessStats = getSmoothnessStats(driftTimeStats.driftTime); + histograms.createHistogram('WebRTCRendering_percent_badly_out_of_sync', + percentage_smallerIsBetter, smoothnessStats.percentBadlyOutOfSync, { + summaryOptions: SUMMARY_OPTIONS, + }); + histograms.createHistogram('WebRTCRendering_percent_out_of_sync', + percentage_smallerIsBetter, smoothnessStats.percentOutOfSync, { + summaryOptions: SUMMARY_OPTIONS, + }); + histograms.createHistogram('WebRTCRendering_smoothness_score', + percentage_biggerIsBetter, smoothnessStats.smoothnessScore, { + summaryOptions: SUMMARY_OPTIONS, + }); + histograms.createHistogram('WebRTCRendering_frames_out_of_sync', + count_smallerIsBetter, smoothnessStats.framesOutOfSync, { + summaryOptions: SUMMARY_OPTIONS, + }); + histograms.createHistogram('WebRTCRendering_frames_badly_out_of_sync', + count_smallerIsBetter, smoothnessStats.framesSeverelyOutOfSync, { + summaryOptions: SUMMARY_OPTIONS, + }); + } + + const FRAME_DISTRIBUTION_BIN_BOUNDARIES = + tr.v.HistogramBinBoundaries.createLinear(1, 50, 49); + + /** + * Create the frame distribution. + * + * If the overall display distribution is A1:A2:..:An, this will tell how + * many times a frame stays displayed during Ak*VSYNC_DURATION_US, also known + * as 'source to output' distribution. + * + * In other terms, a distribution B where + * B[k] = number of frames that are displayed k times. + * + * @param {tr.v.HistogramSet} histograms + * @param {Array.<event>} events - An array of events. + * @returns {tr.v.Histogram} frameHist - The frame distribution. + */ + function getFrameDistribution(histograms, events) { + const cadence = tr.b.runLengthEncoding( + events.map(e => e.args[IDEAL_RENDER_INSTANT_NAME])); + return histograms.createHistogram('WebRTCRendering_frame_distribution', + count_smallerIsBetter, cadence.map(ticks => ticks.count), { + binBoundaries: FRAME_DISTRIBUTION_BIN_BOUNDARIES, + summaryOptions: { + percentile: [0.75, 0.9], + }, + }); + } + + /** + * Calculate the apparent FPS from frame distribution. + * + * Knowing the display frequency and the frame distribution, it is possible to + * calculate the video apparent frame rate as played by WebMediaPlayerMs + * module. + * + * @param {tr.v.HistogramSet} histograms + * @param {tr.v.Histogram} frameHist - The frame distribution. See + * getFrameDistribution. + */ + function addFpsFromFrameDistribution(histograms, frameHist) { + let numberFrames = 0; + let numberVsyncs = 0; + for (let ticks = 1; ticks < frameHist.allBins.length; ++ticks) { + const count = frameHist.allBins[ticks].count; + numberFrames += count; + numberVsyncs += ticks * count; + } + const meanRatio = numberVsyncs / numberFrames; + histograms.createHistogram('WebRTCRendering_fps', + unitlessNumber_biggerIsBetter, DISPLAY_HERTZ / meanRatio, { + summaryOptions: SUMMARY_OPTIONS, + }); + } + + /** + * Returns the weighted penalty for a number of frozen frames. + * + * In a series of repeated frames of length > 5, all frames after the first + * are considered frozen. Conversely, no frames in a series of repeated frames + * of length <= 5 will be considered frozen. + * + * This means the weight for 0 to 4 frozen frames is 0. + * + * @param {Number} numberFrozenFrames - The number of frozen frames. + * @returns {Number} - The weight penalty for the number of frozen frames. + */ + function frozenPenaltyWeight(numberFrozenFrames) { + const penalty = { + 5: 1, + 6: 5, + 7: 15, + 8: 25 + }; + return penalty[numberFrozenFrames] || (8 * (numberFrozenFrames - 4)); + } + + /** + * Adds the freezing score. + * + * @param {tr.v.HistogramSet} histograms + * @param {tr.v.Histogram} frameHist - The frame distribution. + * See getFrameDistribution. + */ + function addFreezingScore(histograms, frameHist) { + let numberVsyncs = 0; + let freezingScore = 0; + let frozenFramesCount = 0; + for (let ticks = 1; ticks < frameHist.allBins.length; ++ticks) { + const count = frameHist.allBins[ticks].count; + numberVsyncs += ticks * count; + if (ticks >= FROZEN_FRAME_VSYNC_COUNT_THRESHOLD) { + // The first frame of the series is not considered frozen. + frozenFramesCount += count * (ticks - 1); + freezingScore += count * frozenPenaltyWeight(ticks - 1); + } + } + freezingScore = 1 - freezingScore / numberVsyncs; + if (freezingScore < 0) { + freezingScore = 0; + } + histograms.createHistogram('WebRTCRendering_frozen_frames_count', + count_smallerIsBetter, frozenFramesCount, { + summaryOptions: SUMMARY_OPTIONS, + }); + histograms.createHistogram('WebRTCRendering_freezing_score', + percentage_biggerIsBetter, freezingScore, { + summaryOptions: SUMMARY_OPTIONS, + }); + } + + /** + * Get the drift time statistics. + * + * This method will calculate: + * - Drift Time: The difference between the Actual Render Begin and the Ideal + * Render Instant for each event. + * - Rendering Length Error: The alignment error of the Ideal Render + * Instants. The Ideal Render Instants should be equally spaced by + * intervals of length VSYNC_DURATION_US. The Rendering Length error + * measures how much they are misaligned. + * + * @param {Array.<event>} events - An array of events. + * @returns {Object.<Array.<Number>, Number>} - The drift time and rendering + * length error. + */ + function getDriftStats(events) { + const driftTime = []; + const discrepancy = []; + let oldIdealRender = 0; + let expectedIdealRender = 0; + + for (const event of events) { + const currentIdealRender = event.args[IDEAL_RENDER_INSTANT_NAME]; + // The expected time of the next 'Ideal Render' event begins as the + // current 'Ideal Render' time and increases by VSYNC_DURATION_US on every + // frame. + expectedIdealRender += VSYNC_DURATION_US; + if (currentIdealRender === oldIdealRender) { + continue; + } + const actualRenderBegin = event.args[ACTUAL_RENDER_BEGIN_NAME]; + // When was the frame rendered vs. when it would've been ideal. + driftTime.push(actualRenderBegin - currentIdealRender); + // The discrepancy is the absolute difference between the current Ideal + // Render and the expected Ideal Render. + discrepancy.push(Math.abs(currentIdealRender - expectedIdealRender)); + expectedIdealRender = currentIdealRender; + oldIdealRender = currentIdealRender; + } + + const discrepancySum = tr.b.math.Statistics.sum(discrepancy) - + discrepancy[0]; + const lastIdealRender = + events[events.length - 1].args[IDEAL_RENDER_INSTANT_NAME]; + const firstIdealRender = events[0].args[IDEAL_RENDER_INSTANT_NAME]; + const idealRenderSpan = lastIdealRender - firstIdealRender; + + const renderingLengthError = discrepancySum / idealRenderSpan; + + return {driftTime, renderingLengthError}; + } + + /** + * Get the smoothness stats from the normalized drift time. + * + * This method will calculate the smoothness score, along with the percentage + * of frames badly out of sync and the percentage of frames out of sync. + * To be considered badly out of sync, a frame has to have missed rendering by + * at least 2 * VSYNC_DURATION_US. + * To be considered out of sync, a frame has to have missed rendering by at + * least one VSYNC_DURATION_US. + * The smoothness score is a measure of how out of sync the frames are. + * + * @param {Array.<Number>} driftTimes - See getDriftStats. + * @returns {Object.<Number, Number, Number>} - The percentBadlyOutOfSync, + * percentOutOfSync and smoothnesScore calculated from the driftTimes array. + */ + function getSmoothnessStats(driftTimes) { + const meanDriftTime = tr.b.math.Statistics.mean(driftTimes); + const normDriftTimes = driftTimes.map(driftTime => + Math.abs(driftTime - meanDriftTime)); + + // How many times is a frame later/earlier than T=2*VSYNC_DURATION_US. Time + // is in microseconds + const framesSeverelyOutOfSync = normDriftTimes + .filter(driftTime => driftTime > 2 * VSYNC_DURATION_US) + .length; + // How many times is a frame later/earlier than VSYNC_DURATION_US. + const framesOutOfSync = normDriftTimes + .filter(driftTime => driftTime > VSYNC_DURATION_US) + .length; + + const percentBadlyOutOfSync = framesSeverelyOutOfSync / + driftTimes.length; + const percentOutOfSync = framesOutOfSync / driftTimes.length; + + const framesOutOfSyncOnlyOnce = framesOutOfSync - framesSeverelyOutOfSync; + + // Calculate smoothness metric. From the formula, we can see that smoothness + // score can be negative. + let smoothnessScore = 1 - (framesOutOfSyncOnlyOnce + + SEVERITY * framesSeverelyOutOfSync) / driftTimes.length; + + // Minimum smoothness_score value allowed is zero. + if (smoothnessScore < 0) { + smoothnessScore = 0; + } + + return { + framesOutOfSync, + framesSeverelyOutOfSync, + percentBadlyOutOfSync, + percentOutOfSync, + smoothnessScore + }; + } + + return { + webrtcRenderingMetric, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric_test.html b/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric_test.html new file mode 100644 index 00000000000..17505faa9d7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/metrics/webrtc/webrtc_rendering_metric_test.html @@ -0,0 +1,456 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/metrics/webrtc/webrtc_rendering_metric.html"> +<link rel="import" href="/tracing/model/slice_group.html"> +<link rel="import" href="/tracing/value/histogram.html"> +<link rel="import" href="/tracing/value/histogram_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const DISPLAY_HERTZ = 60.0; + const VSYNC_DURATION_US = 1e6 / DISPLAY_HERTZ; + + /** + * @param {Array.<Number>} pair - An array of length 2, from which a valid + * WebMediaPlayerMS event will be generated. + * @returns {event} A valid WebMediaPlayerMS event where the Ideal Render + * Instant is the first element and the Actual Render Begin is the second + * element. + */ + function eventFromPair(pair) { + return { + title: 'UpdateCurrentFrame', + start: pair[1], + duration: 1, + args: { + 'Ideal Render Instant': pair[0], + 'Actual Render Begin': pair[1], + 'Actual Render End': 0, + 'Serial': 0, + } + }; + } + + /** + * @param {Array.<Number>} driftTimes - An array with the desired driftTimes. + * @return {Array.<event>} An array of events such that the drift times + * computed by webrtcRenderingMetric is the same as driftTimes. + */ + function eventsFromDriftTimes(driftTimes) { + const pairs = []; + for (let i = 1; i <= driftTimes.length; ++i) { + pairs.push([i, i + driftTimes[i - 1]]); + } + return pairs.map(eventFromPair); + } + + /** + * @param {Array.<Number>} normDriftTimes - To decide if a frame is out of + * sync or badly out of sync, we use the normalized drift times, that we get + * by subtracting the mean from each entry of the drift times array. The sum + * of the normDriftTimes must equal 0. + * @return {Array.<event>) An array of events such that when we normalize the + * drift times computed by webrtcRenderingMetric, we get the normDriftTimes + * array. + */ + function eventsFromNormDriftTimes(normDriftTimes) { + /* Let + * B[i] = normDriftTimes[i] + * A[i] = driftTimes[i] be the array we want to find. + * + * We require that: + * sum(B[i]) = 0 + * + * Then + * B[i] = A[i] - mean(A) + * => B[i] - B[0] = A[i] - mean(A) - A[0] + mean(A) + * => B[i] - B[0] = A[i] - A[0] + * => A[i] = B[i] - B[0] + A[0] + * + * We can fix A[0] to any number we want. + * + * Let's make sure that the array A we found generates the array B when + * normalized: + * A[i] - mean(A) + * = A[i] - sum(A[j]) / n + * = B[i] - B[0] + A[0] - sum(B[j] - B[0] + A[0]) / n + * = B[i] - B[0] + A[0] - (sum(B[j]) - n B[0] / n + n A[0] / n) + * = B[i] - B[0] + A[0] - sum(B[j]) + B[0] - A[0] + * = B[i] - sum(B[j]) + * = B[i] since we require sum(B[j]) = 0 + */ + const driftTimes = [10000]; + for (let i = 1; i < normDriftTimes.length; ++i) { + driftTimes.push(normDriftTimes[i] - normDriftTimes[0] + driftTimes[0]); + } + return eventsFromDriftTimes(driftTimes); + } + + /** + * @param {Array.<Array.<Number>>} frameDistribution - An array of pairs + * encoding the source to output distribution. That is an array where each + * [ticks, count] entry says that there are 'count' frames that are displayed + * 'ticks' times. + * @returns {Array.<events>} The events that give rise to the given + * frameDistribution. + */ + function eventsFromFrameDistribution(frameDistribution) { + let frameId = 0; + const pairs = []; + for (const [ticks, count] of frameDistribution) { + // We need 'count' runs, each run consisting of 'ticks' repeated elements. + for (let i = 0; i < count; ++i) { + frameId += 1; + for (let j = 0; j < ticks; ++j) { + // Frames are decided by the Ideal Render Instant. + pairs.push([frameId, 0]); + } + } + } + return pairs.map(eventFromPair); + } + + function newModel(fakeEvents) { + function customizeModelCallback(model) { + const rendererProcess = model.getOrCreateProcess(1); + const mainThread = rendererProcess.getOrCreateThread(2); + mainThread.name = 'Compositor'; + for (const event of fakeEvents) { + mainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(event)); + } + } + return tr.c.TestUtils.newModelWithEvents([], {customizeModelCallback}); + } + + function runWebrtcRenderingMetric(fakeEvents) { + const histograms = new tr.v.HistogramSet(); + const model = newModel(fakeEvents); + tr.metrics.webrtc.webrtcRenderingMetric(histograms, model); + return histograms; + } + + test('frameDistribution', function() { + // These numbers don't mean anything, we just want to make sure we can + // recover them after running webrtcRenderingMetric. + const frameDistribution = [[10, 3], [5, 15], [3, 146], [1, 546], [2, 10]]; + const frameHist = new tr.v.Histogram('', tr.b.Unit.byName.unitlessNumber); + for (const [ticks, count] of frameDistribution) { + for (let i = 0; i < count; ++i) { + frameHist.addSample(ticks); + } + } + const fakeEvents = eventsFromFrameDistribution(frameDistribution); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + // We don't have access to the values stored in the histogram, so we check + // for equality in the summary statistics. + const hist = + histograms.getHistogramNamed('WebRTCRendering_frame_distribution'); + assert.strictEqual(hist.sum, frameHist.sum); + assert.strictEqual(hist.numValues, frameHist.numValues); + assert.strictEqual(hist.running.min, frameHist.running.min); + assert.strictEqual(hist.running.max, frameHist.running.max); + assert.closeTo(hist.standardDeviation, frameHist.standardDeviation, 1e-2); + }); + + test('driftTime', function() { + // These numbers don't mean anything. We just want to make sure we can + // recover them after running the metric. + const fakeDriftTimes = [16700, 17640, 15000, 24470, 16700, 14399, 17675]; + const fakeEvents = eventsFromDriftTimes(fakeDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + // We don't have access to the values stored in the histogram, so we check + // for equality in the summary statistics. + const hist = histograms.getHistogramNamed('WebRTCRendering_drift_time'); + assert.strictEqual(hist.sum, tr.b.math.Statistics.sum(fakeDriftTimes)); + assert.strictEqual(hist.numValues, fakeDriftTimes.length); + assert.strictEqual(hist.running.min, + tr.b.math.Statistics.min(fakeDriftTimes)); + assert.strictEqual(hist.running.max, + tr.b.math.Statistics.max(fakeDriftTimes)); + assert.closeTo(hist.standardDeviation, + tr.b.math.Statistics.stddev(fakeDriftTimes), 1e-2); + }); + + test('framesBadlyOutOfSyncPerfect', function() { + // None of these will exceed the threshold for badly out of sync events, + // which is about 33 333. + const normDriftTimes = [-16700, 17640, 15000, -17640, -15000, 16700]; + assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0); + const fakeEvents = eventsFromNormDriftTimes(normDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_frames_badly_out_of_sync'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 0); + }); + + test('framesBadylOutOfSync', function() { + // Only 34 000 will exceed the threshold for badly out of sync events, + // which is about 33 333. + const normDriftTimes = [-34000, 10000, 10000, 10000, 4000]; + assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0); + const fakeEvents = eventsFromNormDriftTimes(normDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_frames_badly_out_of_sync'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 1); + }); + + test('framesOutOfSyncPerfect', function() { + // None of these will exceed the threshold for badly out of sync, which is + // about 16 667. + const normDriftTimes = [-16600, 15640, 15000, -15640, -15000, 16600]; + assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0); + const fakeEvents = eventsFromNormDriftTimes(normDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_frames_out_of_sync'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 0); + }); + + test('framesOutOfSync', function() { + // Only 17000 will exceed the threshold for badly out of sync, which is + // about 16 667. + const normDriftTimes = [-17000, 5000, 5000, 5000, 2000]; + assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0); + const fakeEvents = eventsFromNormDriftTimes(normDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_frames_out_of_sync'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 1); + }); + + test('percentBadlyOutOfSyncPerfect', function() { + // None of these will exceed the threshold for badly out of sync events, + // which is about 33 333. + const normDriftTimes = [-16700, 17640, 15000, -17640, -15000, 16700]; + assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0); + const fakeEvents = eventsFromNormDriftTimes(normDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_percent_badly_out_of_sync'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 0); + }); + + test('percentBadylOutOfSync', function() { + // Only 34 000 will exceed the threshold for badly out of sync events, + // which is about 33 333. + const normDriftTimes = [-34000, 10000, 10000, 10000, 4000]; + assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0); + const fakeEvents = eventsFromNormDriftTimes(normDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_percent_badly_out_of_sync'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, .2); + }); + + test('percentOutOfSyncPerfect', function() { + // None of these will exceed the threshold for badly out of sync, which is + // about 16 667. + const normDriftTimes = [-16600, 15640, 15000, -15640, -15000, 16600]; + assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0); + const fakeEvents = eventsFromNormDriftTimes(normDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_percent_out_of_sync'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 0); + }); + + test('percentOutOfSync', function() { + // Only 17000 will exceed the threshold for badly out of sync, which is + // about 16 667. + const normDriftTimes = [-17000, 5000, 5000, 5000, 2000]; + assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0); + const fakeEvents = eventsFromNormDriftTimes(normDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_percent_out_of_sync'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, .2); + }); + + test('smoothnessScorePerfect', function() { + // None of these will exceed the threshold for badly out of sync, which is + // about 16 667, so the smoothnessScore wil be perfect. + const normDriftTimes = [-16600, 15640, 15000, -15640, -15000, 16600]; + assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0); + const fakeEvents = eventsFromNormDriftTimes(normDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_smoothness_score'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 1); + }); + + test('smoothnessScore', function() { + // One will exceed the threshold for frames badly out of sync (33 333) and + // another two the threshold for frames out of sync (16 667). So the + // smoothness score is + // 1 - (frames out of sync + 3 * frames badly out of sync) / n + // = 1 - (2 + 3) / 5 = 0 + const normDriftTimes = [-17000, 34000, -17000, -10000, 10000]; + assert.strictEqual(tr.b.math.Statistics.sum(normDriftTimes), 0); + const fakeEvents = eventsFromNormDriftTimes(normDriftTimes); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_smoothness_score'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 0); + }); + + test('fpsPerfect', function() { + // Every frame is displayed once. This is a perfect FPS of 60. + const frameDistribution = [[1, 10]]; + const fakeEvents = eventsFromFrameDistribution(frameDistribution); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = histograms.getHistogramNamed('WebRTCRendering_fps'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 60); + }); + + test('fps', function() { + // Every frame is displayed 15 times. This means an FPS of 4. + const frameDistribution = [[15, 10]]; + const fakeEvents = eventsFromFrameDistribution(frameDistribution); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = histograms.getHistogramNamed('WebRTCRendering_fps'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 4); + }); + + test('frozenFramesCountPerfect', function() { + // 10 frames are displayed one time, and 10 frames are displayed twice. + // This means no frames exceed the threshold of 6, and so no frames are + // considered frozen. + const frameDistribution = [[1, 10], [2, 10]]; + const fakeEvents = eventsFromFrameDistribution(frameDistribution); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_frozen_frames_count'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 0); + }); + + test('frozenFramesCount', function() { + // 82 frames are displayed 1 time, 5 frames are displayed 2 times, + // and 1 frame is displayed 6 times. + // Only the drame displayed 6 times satisfies the threshold of 6. Since the + // first appearance is not considered frozen, there are 5 frozen frames. + const frameDistribution = [[1, 82], [2, 5], [6, 1]]; + const fakeEvents = eventsFromFrameDistribution(frameDistribution); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_frozen_frames_count'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 5); + }); + + test('freezingScorePerfect', function() { + // Every frame is displayed 1 times. This means a perfect freezing score of + // 100. + const frameDistribution = [[1, 10]]; + const fakeEvents = eventsFromFrameDistribution(frameDistribution); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = histograms.getHistogramNamed('WebRTCRendering_freezing_score'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, 1); + }); + + test('freezingScore', function() { + // 82 frames are displayed 1 time, 5 frames are displayed 2 times, + // and 1 frame is displayed 8 times. + // This means that the total number of frames displayed is + // 82 * 1 + 5 * 2 + 1 * 8 = 100 + // And the freezing score is + // 1 - 82 / 100 * weight[0] + // - 5 / 100 * weight[1] + // - 1 / 100 * weight[7] + // = 1 - .82 * 0 since weight[0] = 0 + // - .05 * 0 since weight[1] = 0 too + // - .01 * 15 since weight[7] = 15 + // = 1 - .15 = .85 + // See frozenPenaltyWeight for information on the weights and + // addFreezingScore for the definition of the freezingScore. + const frameDistribution = [[1, 82], [2, 5], [8, 1]]; + const fakeEvents = eventsFromFrameDistribution(frameDistribution); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = histograms.getHistogramNamed('WebRTCRendering_freezing_score'); + assert.strictEqual(hist.numValues, 1); + assert.strictEqual(hist.running.mean, .85); + }); + + test('renderingLengthErrorPerfect', function() { + const fakePairs = []; + for (let i = 1; i < 10; ++i) { + // Each frame's Ideal Render Instant is exactly VSYNC_DURATION_US after + // the previous one, so that the rendering length error is 0. + fakePairs.push([VSYNC_DURATION_US * i, 0]); + } + const fakeEvents = fakePairs.map(eventFromPair); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_rendering_length_error'); + assert.strictEqual(hist.numValues, 1); + assert.closeTo(hist.running.mean, 0, 1e-3); + }); + + test('renderingLengthError', function() { + const errors = [1000, 3000, 0, 5000, 0, 2000]; + const fakePairs = [[1, 0]]; + for (let i = 0; i < errors.length; ++i) { + // Each frame's Ideal Render Instant is close to VSYNC_DURATION_US after + // the previous one, but with a known delay. + fakePairs.push([fakePairs[i][0] + VSYNC_DURATION_US + errors[i], 0]); + } + + // The rendering length error is then the sum of the errors, normalized by + // the span between the first and the last Ideal Render Instants. + const idealRenderSpan = fakePairs[fakePairs.length - 1][0] - + fakePairs[0][0]; + const expectedRenderingLengthError = tr.b.math.Statistics.sum(errors) / + idealRenderSpan; + + const fakeEvents = fakePairs.map(eventFromPair); + const histograms = runWebrtcRenderingMetric(fakeEvents); + + const hist = + histograms.getHistogramNamed('WebRTCRendering_rendering_length_error'); + assert.strictEqual(hist.numValues, 1); + assert.closeTo(hist.running.mean, expectedRenderingLengthError, 1e-3); + }); +}); +</script> |