summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/catapult/tracing/tracing/importer
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/importer')
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/clock_sync_test.html125
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/context_processor.html206
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/context_processor_test.html297
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/empty_importer.html49
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html1409
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/find_load_expectations.html325
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/find_startup_expectations.html88
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/import.html339
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/import_test.html228
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/importer.html86
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/proto_expectation.html202
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/simple_line_reader.html93
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/user_expectation_verifier.html111
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/user_model_builder.html277
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/user_model_builder_test.html1649
15 files changed, 5484 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/clock_sync_test.html b/chromium/third_party/catapult/tracing/tracing/importer/clock_sync_test.html
new file mode 100644
index 00000000000..ce164eb881b
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/clock_sync_test.html
@@ -0,0 +1,125 @@
+<!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/battor_importer.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/model/clock_sync_manager.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview This file contains end-to-end clock sync tests that ensure
+ * clock sync behavior works as expected across traces. There are too many
+ * possible combinations of trace types to test all of them, but we aim to test
+ * many of the important ones in this file.
+ */
+tr.b.unittest.testSuite(function() {
+ test('import_noClockDomains', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([]);
+
+ assert.isFalse(m.hasImportWarnings);
+ });
+
+ test('import_traceEvent', function() {
+ const trace = JSON.stringify({
+ traceEvents: [
+ {ts: 0, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}},
+ {ts: 1000, pid: 0, tid: 0, ph: 'c', cat: 'metadata',
+ args: { issue_ts: 500, sync_id: 'abc' }},
+ {ts: 2000, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}}
+ ]});
+ const m = tr.c.TestUtils.newModelWithEvents([trace]);
+
+ assert.isFalse(m.hasImportWarnings);
+
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[0].start, 0);
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[1].start, 2);
+ });
+
+
+ test('import_ftrace', function() {
+ const ftrace =
+ 'SurfaceFlinger-0 [001] ...1 0.001: 0: B|1|taskA\n' +
+ ' chrome-3 [001] ...1 0.010: 0: trace_event_clock_sync: ' +
+ 'parent_ts=0.020\n';
+
+ const m = tr.c.TestUtils.newModelWithEvents([ftrace]);
+
+ assert.isFalse(m.hasImportWarnings);
+ assert.strictEqual(m.processes[1].threads[0].sliceGroup.slices[0].start, 0);
+ });
+
+ test('import_traceEventWithNoClockDomainAndFtrace', function() {
+ // Include a clock sync marker that indicates the LINUX_CLOCK_MONOTONIC time
+ // of 20ms is equal to the LINUX_FTRACE_GLOBAL time of 10ms, effectively
+ // shifting all ftrace timestamps forward by 10ms.
+ const ftrace =
+ 'SurfaceFlinger-0 [001] ...1 0.001: 0: B|1|taskA\n' +
+ ' chrome-3 [001] ...1 0.010: 0: trace_event_clock_sync: ' +
+ 'parent_ts=0.020\n';
+
+ const trace = JSON.stringify({
+ traceEvents: [
+ {ts: 0, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}},
+ {ts: 1000, pid: 0, tid: 0, ph: 'c', cat: 'metadata',
+ args: { issue_ts: 500, sync_id: 'abc' }},
+ {ts: 2000, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}}
+ ],
+ systemTraceEvents: ftrace
+ });
+ const m = tr.c.TestUtils.newModelWithEvents([trace]);
+
+ assert.isFalse(m.hasImportWarnings);
+
+ // Chrome events shouldn't be shifted.
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[0].start, 0);
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[1].start, 2);
+
+ // Ftrace events should be shifted forward by 10ms.
+ assert.strictEqual(
+ m.processes[1].threads[0].sliceGroup.slices[0].start, 11);
+ });
+
+ test('import_traceEventWithClockDomainAndFtrace', function() {
+ // Include a clock sync marker that indicates the LINUX_CLOCK_MONOTONIC time
+ // of 20ms is equal to the LINUX_FTRACE_GLOBAL time of 10ms, effectively
+ // shifting all ftrace timestamps forward by 10ms.
+ const ftrace =
+ 'SurfaceFlinger-0 [001] ...1 0.001: 0: B|1|taskA\n' +
+ ' chrome-3 [001] ...1 0.010: 0: trace_event_clock_sync: ' +
+ 'parent_ts=0.020\n';
+
+ const trace = JSON.stringify({
+ traceEvents: [
+ {ts: 0, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}},
+ {ts: 1000, pid: 0, tid: 0, ph: 'c', cat: 'metadata',
+ args: { issue_ts: 500, sync_id: 'abc' }},
+ {ts: 2000, pid: 0, tid: 0, ph: 'i', cat: 'c', name: 'taskA', args: {}}
+ ],
+ metadata: {
+ 'clock-domain': 'LINUX_CLOCK_MONOTONIC'
+ },
+ systemTraceEvents: ftrace
+ });
+ const m = tr.c.TestUtils.newModelWithEvents([trace]);
+
+ assert.isFalse(m.hasImportWarnings);
+
+ // Chrome events shouldn't be shifted.
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[0].start, 0);
+ assert.strictEqual(m.processes[0].threads[0].sliceGroup.slices[1].start, 2);
+
+ // Ftrace events should be shifted forward by 10ms.
+ assert.strictEqual(
+ m.processes[1].threads[0].sliceGroup.slices[0].start, 11);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/context_processor.html b/chromium/third_party/catapult/tracing/tracing/importer/context_processor.html
new file mode 100644
index 00000000000..c82e80645a5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/context_processor.html
@@ -0,0 +1,206 @@
+<!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">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ /**
+ * The context processor consumes context events and maintains a set of
+ * active contexts for a single thread.
+ *
+ * @constructor
+ */
+ function ContextProcessor(model) {
+ this.model_ = model;
+ this.activeContexts_ = [];
+ this.stackPerType_ = {};
+ // Cache of unique context objects.
+ this.contextCache_ = {};
+ // Cache of unique context object sets.
+ this.contextSetCache_ = {};
+ this.cachedEntryForActiveContexts_ = undefined;
+ // All seen context object snapshots.
+ this.seenSnapshots_ = {};
+ }
+
+ ContextProcessor.prototype = {
+ enterContext(contextType, scopedId) {
+ const newActiveContexts = [
+ this.getOrCreateContext_(contextType, scopedId),
+ ];
+ for (const oldContext of this.activeContexts_) {
+ if (oldContext.type === contextType) {
+ // If a previous context of the same type is active, it is removed
+ // and pushed onto the stack for this type.
+ this.pushContext_(oldContext);
+ } else {
+ // Otherwise the old context is it is still active.
+ newActiveContexts.push(oldContext);
+ }
+ }
+ this.activeContexts_ = newActiveContexts;
+ this.cachedEntryForActiveContexts_ = undefined;
+ },
+
+ leaveContext(contextType, scopedId) {
+ this.leaveContextImpl_(context =>
+ context.type === contextType &&
+ context.snapshot.scope === scopedId.scope &&
+ context.snapshot.idRef === scopedId.id);
+ },
+
+ destroyContext(scopedId) {
+ // Remove all matching contexts from stacks.
+ for (const stack of Object.values(this.stackPerType_)) {
+ // Perform in-place filtering instead of Array.prototype.filter to
+ // prevent creating a new array.
+ let newLength = 0;
+ for (let i = 0; i < stack.length; ++i) {
+ if (stack[i].snapshot.scope !== scopedId.scope ||
+ stack[i].snapshot.idRef !== scopedId.id) {
+ stack[newLength++] = stack[i];
+ }
+ }
+ stack.length = newLength;
+ }
+
+ // Remove all matching contexts from active context set.
+ this.leaveContextImpl_(context =>
+ context.snapshot.scope === scopedId.scope &&
+ context.snapshot.idRef === scopedId.id);
+ },
+
+ leaveContextImpl_(predicate) {
+ const newActiveContexts = [];
+ for (const oldContext of this.activeContexts_) {
+ if (predicate(oldContext)) {
+ // If we left this context, remove it from the active set and
+ // restore any previous context of the same type.
+ const previousContext = this.popContext_(oldContext.type);
+ if (previousContext) {
+ newActiveContexts.push(previousContext);
+ }
+ } else {
+ newActiveContexts.push(oldContext);
+ }
+ }
+ this.activeContexts_ = newActiveContexts;
+ this.cachedEntryForActiveContexts_ = undefined;
+ },
+
+ getOrCreateContext_(contextType, scopedId) {
+ const context = {
+ type: contextType,
+ snapshot: {
+ scope: scopedId.scope,
+ idRef: scopedId.id
+ }
+ };
+ const key = this.getContextKey_(context);
+ if (key in this.contextCache_) {
+ return this.contextCache_[key];
+ }
+ this.contextCache_[key] = context;
+ const snapshotKey = this.getSnapshotKey_(scopedId);
+ this.seenSnapshots_[snapshotKey] = true;
+ return context;
+ },
+
+ pushContext_(context) {
+ if (!(context.type in this.stackPerType_)) {
+ this.stackPerType_[context.type] = [];
+ }
+ this.stackPerType_[context.type].push(context);
+ },
+
+ popContext_(contextType) {
+ if (!(contextType in this.stackPerType_)) {
+ return undefined;
+ }
+ return this.stackPerType_[contextType].pop();
+ },
+
+ getContextKey_(context) {
+ return [
+ context.type,
+ context.snapshot.scope,
+ context.snapshot.idRef
+ ].join('\x00');
+ },
+
+ getSnapshotKey_(scopedId) {
+ return [
+ scopedId.scope,
+ scopedId.idRef
+ ].join('\x00');
+ },
+
+ get activeContexts() {
+ // Keep a single instance for each unique set of active contexts to
+ // reduce memory usage.
+ if (this.cachedEntryForActiveContexts_ === undefined) {
+ let key = [];
+ for (const context of this.activeContexts_) {
+ key.push(this.getContextKey_(context));
+ }
+ key.sort();
+ key = key.join('\x00');
+ if (key in this.contextSetCache_) {
+ this.cachedEntryForActiveContexts_ = this.contextSetCache_[key];
+ } else {
+ this.activeContexts_.sort(function(a, b) {
+ const keyA = this.getContextKey_(a);
+ const keyB = this.getContextKey_(b);
+ if (keyA < keyB) {
+ return -1;
+ }
+ if (keyA > keyB) {
+ return 1;
+ }
+ return 0;
+ }.bind(this));
+ this.contextSetCache_[key] = Object.freeze(this.activeContexts_);
+ this.cachedEntryForActiveContexts_ = this.contextSetCache_[key];
+ }
+ }
+ return this.cachedEntryForActiveContexts_;
+ },
+
+ invalidateContextCacheForSnapshot(scopedId) {
+ const snapshotKey = this.getSnapshotKey_(scopedId);
+ if (!(snapshotKey in this.seenSnapshots_)) return;
+
+ this.contextCache_ = {};
+ this.contextSetCache_ = {};
+ this.cachedEntryForActiveContexts_ = undefined;
+ this.activeContexts_ = this.activeContexts_.map(function(context) {
+ // Do not alter unrelated contexts.
+ if (context.snapshot.scope !== scopedId.scope ||
+ context.snapshot.idRef !== scopedId.id) {
+ return context;
+ }
+ // Replace the invalidated context by a deep copy.
+ return {
+ type: context.type,
+ snapshot: {
+ scope: context.snapshot.scope,
+ idRef: context.snapshot.idRef
+ }
+ };
+ });
+ this.seenSnapshots_ = {};
+ },
+ };
+
+ return {
+ ContextProcessor,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/context_processor_test.html b/chromium/third_party/catapult/tracing/tracing/importer/context_processor_test.html
new file mode 100644
index 00000000000..c033f7c28ff
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/context_processor_test.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/core/test_utils.html">
+<link rel="import" href="/tracing/importer/context_processor.html">
+<link rel="import" href="/tracing/model/scoped_id.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const ContextProcessor = tr.importer.ContextProcessor;
+
+ test('empty', function() {
+ const processor = new ContextProcessor();
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('enterAndLeave', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+ processor.enterContext('type', id);
+ assert.deepEqual(processor.activeContexts, [expectedContext]);
+ processor.leaveContext('type', id);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('parallelContexts', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContextA = {type: 'A', snapshot: {scope: 'ptr', idRef: 123}};
+ const expectedContextB = {type: 'B', snapshot: {scope: 'idx', idRef: 456}};
+
+ // Entering and leaving in order.
+ processor.enterContext('A', idA);
+ assert.deepEqual(processor.activeContexts, [expectedContextA]);
+ processor.enterContext('B', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextA,
+ expectedContextB]);
+ processor.leaveContext('B', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextA]);
+ processor.leaveContext('A', idA);
+ assert.deepEqual(processor.activeContexts, []);
+
+ // Entering and leaving out of order.
+ processor.enterContext('B', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextB]);
+ processor.enterContext('A', idA);
+ assert.deepEqual(processor.activeContexts, [expectedContextA,
+ expectedContextB]);
+ processor.leaveContext('B', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextA]);
+ processor.leaveContext('A', idA);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('contextStack', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContextA = {
+ type: 'type', snapshot: {scope: 'ptr', idRef: 123}};
+ const expectedContextB = {
+ type: 'type', snapshot: {scope: 'idx', idRef: 456}};
+
+ // Entering and leaving the same context type.
+ processor.enterContext('type', idA);
+ assert.deepEqual(processor.activeContexts, [expectedContextA]);
+ processor.enterContext('type', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextB]);
+ processor.leaveContext('type', idB);
+ assert.deepEqual(processor.activeContexts, [expectedContextA]);
+ processor.leaveContext('type', idA);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('contextCached', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContextA = {
+ type: 'A', snapshot: {scope: 'ptr', idRef: 123}};
+ const expectedContextB = {
+ type: 'B', snapshot: {scope: 'idx', idRef: 456}};
+
+ processor.enterContext('A', idA);
+ const firstSet = processor.activeContexts;
+ processor.enterContext('B', idB);
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('B', idB);
+ processor.leaveContext('A', idA);
+
+ assert.deepEqual(firstSet, [expectedContextA]);
+ assert.deepEqual(secondSet, [expectedContextA, expectedContextB]);
+
+ // Identical context objects should be the same instance.
+ assert(Object.is(firstSet[0], secondSet[0]));
+ });
+
+ test('contextSetCached', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+
+ processor.enterContext('type', id);
+ const firstSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ processor.enterContext('type', id);
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ assert.deepEqual(firstSet, [expectedContext]);
+ assert(Object.is(firstSet, secondSet));
+ });
+
+ test('contextSetIsOrdered', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContextA = {type: 'A', snapshot: {scope: 'ptr', idRef: 123}};
+ const expectedContextB = {type: 'B', snapshot: {scope: 'idx', idRef: 456}};
+
+ processor.enterContext('A', idA);
+ processor.enterContext('B', idB);
+ const firstSet = processor.activeContexts;
+ processor.leaveContext('B', idB);
+ processor.leaveContext('A', idA);
+
+ processor.enterContext('B', idB);
+ processor.enterContext('A', idA);
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('A', idA);
+ processor.leaveContext('B', idB);
+
+ assert.deepEqual(firstSet, [expectedContextA, expectedContextB]);
+ assert(Object.is(firstSet, secondSet));
+ });
+
+ test('contextSetIsFrozen', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ assert(Object.isFrozen(processor.activeContexts));
+ processor.enterContext('type', id);
+ assert(Object.isFrozen(processor.activeContexts));
+ processor.leaveContext('type', id);
+ assert(Object.isFrozen(processor.activeContexts));
+ });
+
+ test('cacheInvalidation', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+
+ processor.enterContext('type', id);
+ const firstSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ processor.invalidateContextCacheForSnapshot(id);
+
+ processor.enterContext('type', id);
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ assert.deepEqual(firstSet, [expectedContext]);
+ assert.deepEqual(secondSet, [expectedContext]);
+ assert(!Object.is(firstSet, secondSet));
+ assert(!Object.is(firstSet[0], secondSet[0]));
+ assert(!Object.is(firstSet[0].snapshot, secondSet[0].snapshot));
+ });
+
+ test('cacheInvalidationOfAnActiveContext', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+
+ processor.enterContext('type', id);
+ const firstSet = processor.activeContexts;
+
+ processor.invalidateContextCacheForSnapshot(id);
+
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ assert.deepEqual(firstSet, [expectedContext]);
+ assert.deepEqual(secondSet, [expectedContext]);
+ assert(!Object.is(firstSet, secondSet));
+ assert(!Object.is(firstSet[0], secondSet[0]));
+ assert(!Object.is(firstSet[0].snapshot, secondSet[0].snapshot));
+ });
+
+ test('cacheInvalidationForUnrelatedSnapshot', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const unrelatedId = new tr.model.ScopedId('ofs', 789);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+
+ processor.enterContext('type', id);
+ const firstSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ processor.invalidateContextCacheForSnapshot(unrelatedId);
+
+ processor.enterContext('type', id);
+ const secondSet = processor.activeContexts;
+ processor.leaveContext('type', id);
+
+ assert.deepEqual(firstSet, [expectedContext]);
+ assert.deepEqual(secondSet, [expectedContext]);
+ assert(Object.is(firstSet, secondSet));
+ });
+
+ test('destroyBasic', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+ processor.enterContext('type', id);
+ assert.deepEqual(processor.activeContexts, [expectedContext]);
+ processor.destroyContext(id);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('destroyActiveContextWithNonEmptyStack', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'ptr', idRef: 123},
+ };
+ processor.enterContext('type', idA);
+ processor.enterContext('type', idB);
+ processor.destroyContext(idB);
+ assert.deepEqual(processor.activeContexts, [expectedContext]);
+ processor.leaveContext('type', idA);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('destroyInactiveContextInStack', function() {
+ const processor = new ContextProcessor();
+ const idA = new tr.model.ScopedId('ptr', 123);
+ const idB = new tr.model.ScopedId('idx', 456);
+ const expectedContext = {
+ type: 'type',
+ snapshot: {scope: 'idx', idRef: 456},
+ };
+ processor.enterContext('type', idA);
+ processor.enterContext('type', idB);
+ processor.destroyContext(idA);
+ assert.deepEqual(processor.activeContexts, [expectedContext]);
+ processor.leaveContext('type', idB);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('destroyContextEnteredWithMultipleTypes', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ processor.enterContext('A', id);
+ processor.enterContext('B', id);
+ processor.destroyContext(id);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+
+ test('destroyReenteredContext', function() {
+ const processor = new ContextProcessor();
+ const id = new tr.model.ScopedId('ptr', 123);
+ processor.enterContext('type', id);
+ processor.enterContext('type', id);
+ processor.destroyContext(id);
+ assert.deepEqual(processor.activeContexts, []);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/empty_importer.html b/chromium/third_party/catapult/tracing/tracing/importer/empty_importer.html
new file mode 100644
index 00000000000..2f876b3708e
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/empty_importer.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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/importer/importer.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for trace data importers.
+ */
+tr.exportTo('tr.importer', function() {
+ /**
+ * Importer for empty strings and arrays.
+ * @constructor
+ */
+ function EmptyImporter(events) {
+ this.importPriority = 0;
+ }
+
+ EmptyImporter.canImport = function(eventData) {
+ if (eventData instanceof Array && eventData.length === 0) {
+ return true;
+ }
+ if (typeof(eventData) === 'string' || eventData instanceof String) {
+ return eventData.length === 0;
+ }
+ return false;
+ };
+
+ EmptyImporter.prototype = {
+ __proto__: tr.importer.Importer.prototype,
+
+ get importerName() {
+ return 'EmptyImporter';
+ }
+ };
+
+ tr.importer.Importer.register(EmptyImporter);
+
+ return {
+ EmptyImporter,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html b/chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html
new file mode 100644
index 00000000000..464a41853d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html
@@ -0,0 +1,1409 @@
+<!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/range_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/importer/proto_expectation.html">
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ const ProtoExpectation = tr.importer.ProtoExpectation;
+ const INITIATOR_TYPE = tr.model.um.INITIATOR_TYPE;
+ const INPUT_TYPE = tr.e.cc.INPUT_EVENT_TYPE_NAMES;
+
+ const KEYBOARD_TYPE_NAMES = [
+ INPUT_TYPE.CHAR,
+ INPUT_TYPE.KEY_DOWN_RAW,
+ INPUT_TYPE.KEY_DOWN,
+ INPUT_TYPE.KEY_UP
+ ];
+ const MOUSE_RESPONSE_TYPE_NAMES = [
+ INPUT_TYPE.CLICK,
+ INPUT_TYPE.CONTEXT_MENU
+ ];
+ const MOUSE_WHEEL_TYPE_NAMES = [
+ INPUT_TYPE.MOUSE_WHEEL
+ ];
+ const MOUSE_DRAG_TYPE_NAMES = [
+ INPUT_TYPE.MOUSE_DOWN,
+ INPUT_TYPE.MOUSE_MOVE,
+ INPUT_TYPE.MOUSE_UP
+ ];
+ const TAP_TYPE_NAMES = [
+ INPUT_TYPE.TAP,
+ INPUT_TYPE.TAP_CANCEL,
+ INPUT_TYPE.TAP_DOWN
+ ];
+ const PINCH_TYPE_NAMES = [
+ INPUT_TYPE.PINCH_BEGIN,
+ INPUT_TYPE.PINCH_END,
+ INPUT_TYPE.PINCH_UPDATE
+ ];
+ const FLING_TYPE_NAMES = [
+ INPUT_TYPE.FLING_CANCEL,
+ INPUT_TYPE.FLING_START
+ ];
+ const TOUCH_TYPE_NAMES = [
+ INPUT_TYPE.TOUCH_END,
+ INPUT_TYPE.TOUCH_MOVE,
+ INPUT_TYPE.TOUCH_START
+ ];
+ const SCROLL_TYPE_NAMES = [
+ INPUT_TYPE.SCROLL_BEGIN,
+ INPUT_TYPE.SCROLL_END,
+ INPUT_TYPE.SCROLL_UPDATE
+ ];
+ const ALL_HANDLED_TYPE_NAMES = [].concat(
+ KEYBOARD_TYPE_NAMES,
+ MOUSE_RESPONSE_TYPE_NAMES,
+ MOUSE_WHEEL_TYPE_NAMES,
+ MOUSE_DRAG_TYPE_NAMES,
+ PINCH_TYPE_NAMES,
+ TAP_TYPE_NAMES,
+ FLING_TYPE_NAMES,
+ TOUCH_TYPE_NAMES,
+ SCROLL_TYPE_NAMES
+ );
+
+ const RENDERER_FLING_TITLE = 'InputHandlerProxy::HandleGestureFling::started';
+ const PLAYBACK_EVENT_TITLE = 'VideoPlayback';
+
+ const CSS_ANIMATION_TITLE = 'Animation';
+
+ const VR_COUNTER_NAMES = [
+ 'gpu.WebVR FPS',
+ 'gpu.WebVR frame time (ms)',
+ 'gpu.WebVR pose prediction (ms)',
+ ];
+ const VR_EVENT_NAMES = [
+ 'VrShellGl::AcquireFrame',
+ 'VrShellGl::DrawFrame',
+ 'VrShellGl::DrawSubmitFrameWhenReady',
+ 'VrShellGl::DrawUiView',
+ 'VrShellGl::UpdateController',
+ ];
+ /* 1s is a bit arbitrary, but it reliably avoids all the jank caused by
+ * VR entry.
+ */
+ const VR_RESPONSE_MS = 1000;
+
+ /**
+ * If there's less than this much time between the end of one event and the
+ * start of the next, then they might be merged.
+ * There was not enough thought given to this value, so if you have any slight
+ * reason to change it, then please do so. It might also be good to split this
+ * into multiple values.
+ */
+ const INPUT_MERGE_THRESHOLD_MS = 200;
+ const ANIMATION_MERGE_THRESHOLD_MS = 32; // 2x 60FPS frames
+
+ /**
+ * If two MouseWheel events begin this close together, then they're an
+ * Animation, not two responses.
+ */
+ const MOUSE_WHEEL_THRESHOLD_MS = 40;
+
+ /**
+ * If two MouseMoves are more than this far apart, then they're two Responses,
+ * not Animation.
+ */
+ const MOUSE_MOVE_THRESHOLD_MS = 40;
+
+ // TODO(#3813) Move this.
+ function compareEvents(x, y) {
+ if (x.start !== y.start) {
+ return x.start - y.start;
+ }
+ if (x.end !== y.end) {
+ return x.end - y.end;
+ }
+ if (x.guid && y.guid) {
+ return x.guid - y.guid;
+ }
+ return 0;
+ }
+
+ function forEventTypesIn(events, typeNames, cb, opt_this) {
+ events.forEach(function(event) {
+ if (typeNames.indexOf(event.typeName) >= 0) {
+ cb.call(opt_this, event);
+ }
+ });
+ }
+
+ function causedFrame(event) {
+ return event.associatedEvents.some(isImplFrameEvent);
+ }
+
+ function getSortedFrameEventsByProcess(modelHelper) {
+ const frameEventsByPid = {};
+ for (const [pid, rendererHelper] of
+ Object.entries(modelHelper.rendererHelpers)) {
+ frameEventsByPid[pid] = rendererHelper.getFrameEventsInRange(
+ tr.model.helpers.IMPL_FRAMETIME_TYPE, modelHelper.model.bounds);
+ }
+ return frameEventsByPid;
+ }
+
+ function getSortedInputEvents(modelHelper) {
+ const inputEvents = [];
+
+ const browserProcess = modelHelper.browserHelper.process;
+ const mainThread = browserProcess.findAtMostOneThreadNamed(
+ 'CrBrowserMain');
+ for (const slice of mainThread.asyncSliceGroup.getDescendantEvents()) {
+ if (!slice.isTopLevel) continue;
+
+ if (!(slice instanceof tr.e.cc.InputLatencyAsyncSlice)) continue;
+
+ if (isNaN(slice.start) ||
+ isNaN(slice.duration) ||
+ isNaN(slice.end)) {
+ continue;
+ }
+
+ inputEvents.push(slice);
+ }
+
+ return inputEvents.sort(compareEvents);
+ }
+
+ function findProtoExpectations(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ // This order is not important. Handlers are independent.
+ const handlers = [
+ handleKeyboardEvents,
+ handleMouseResponseEvents,
+ handleMouseWheelEvents,
+ handleMouseDragEvents,
+ handleTapResponseEvents,
+ handlePinchEvents,
+ handleFlingEvents,
+ handleTouchEvents,
+ handleScrollEvents,
+ handleCSSAnimations,
+ handleWebGLAnimations,
+ handleVideoAnimations,
+ handleVrAnimations,
+ ];
+ handlers.forEach(function(handler) {
+ protoExpectations.push.apply(protoExpectations, handler(
+ modelHelper, sortedInputEvents, warn));
+ });
+ protoExpectations.sort(compareEvents);
+ return protoExpectations;
+ }
+
+ /**
+ * Every keyboard event is a Response.
+ */
+ function handleKeyboardEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ forEventTypesIn(sortedInputEvents, KEYBOARD_TYPE_NAMES, function(event) {
+ const pe = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.KEYBOARD);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Some mouse events can be translated directly into Responses.
+ */
+ function handleMouseResponseEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ forEventTypesIn(
+ sortedInputEvents, MOUSE_RESPONSE_TYPE_NAMES, function(event) {
+ const pe = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ });
+ return protoExpectations;
+ }
+ /**
+ * MouseWheel events are caused either by a physical wheel on a physical
+ * mouse, or by a touch-drag gesture on a track-pad. The physical wheel
+ * causes MouseWheel events that are much more spaced out, and have no
+ * chance of hitting 60fps, so they are each turned into separate Response
+ * UEs. The track-pad causes MouseWheel events that are much closer
+ * together, and are expected to be 60fps, so the first event in a sequence
+ * is turned into a Response, and the rest are merged into an Animation.
+ * NB this threshold uses the two events' start times, unlike
+ * ProtoExpectation.isNear, which compares the end time of the previous event
+ * with the start time of the next.
+ */
+ function handleMouseWheelEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let prevEvent_ = undefined;
+ forEventTypesIn(
+ sortedInputEvents, MOUSE_WHEEL_TYPE_NAMES, function(event) {
+ // Switch prevEvent in one place so that we can early-return later.
+ const prevEvent = prevEvent_;
+ prevEvent_ = event;
+
+ if (currentPE &&
+ (prevEvent.start + MOUSE_WHEEL_THRESHOLD_MS) >= event.start) {
+ if (currentPE.type === ProtoExpectation.ANIMATION_TYPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
+ INITIATOR_TYPE.MOUSE_WHEEL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ return;
+ }
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE_WHEEL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Down events followed closely by Up events are click Responses, but the
+ * Response doesn't start until the Up event.
+ *
+ * RRR
+ * DDD UUU
+ *
+ * If there are any Move events in between a Down and an Up, then the Down
+ * and the first Move are a Response, then the rest of the Moves are an
+ * Animation:
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * DDD MMM MMM MMM MMM MMM UUU
+ */
+ function handleMouseDragEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let mouseDownEvent = undefined;
+ forEventTypesIn(
+ sortedInputEvents, MOUSE_DRAG_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.MOUSE_DOWN:
+ if (causedFrame(event)) {
+ const pe = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ } else {
+ // Responses typically don't start until the mouse up event.
+ // Add this MouseDown to the Response that starts at the
+ // MouseUp.
+ mouseDownEvent = event;
+ }
+ break;
+
+ // There may be more than 100ms between the start of the mouse
+ // down and the start of the mouse up. Chrome and the web don't
+ // start to respond until the mouse up. Responses start deducting
+ // comfort at 100ms duration. If more than that 100ms duration is
+ // burned through while waiting for the user to release the mouse
+ // button, then ResponseExpectation will unfairly start deducting
+ // comfort before Chrome even has a mouse up to respond to. It is
+ // technically possible for a site to afford one response on mouse
+ // down and another on mouse up, but that is an edge case. The
+ // vast majority of mouse downs are not responses.
+
+ case INPUT_TYPE.MOUSE_MOVE:
+ if (!causedFrame(event)) {
+ // Ignore MouseMoves that do not affect the screen. They are not
+ // part of an interaction record by definition.
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ } else if (!currentPE ||
+ !currentPE.isNear(event, MOUSE_MOVE_THRESHOLD_MS)) {
+ // The first MouseMove after a MouseDown or after a while is a
+ // Response.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ currentPE.pushEvent(event);
+ if (mouseDownEvent) {
+ currentPE.associatedEvents.push(mouseDownEvent);
+ mouseDownEvent = undefined;
+ }
+ protoExpectations.push(currentPE);
+ } else {
+ // Merge this event into an Animation.
+ if (currentPE.type === ProtoExpectation.ANIMATION_TYPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.MOUSE);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ }
+ break;
+
+ case INPUT_TYPE.MOUSE_UP:
+ if (!mouseDownEvent) {
+ const pe = new ProtoExpectation(
+ causedFrame(event) ? ProtoExpectation.RESPONSE_TYPE :
+ ProtoExpectation.IGNORED_TYPE,
+ INITIATOR_TYPE.MOUSE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ if (mouseDownEvent) {
+ currentPE.associatedEvents.push(mouseDownEvent);
+ }
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ mouseDownEvent = undefined;
+ currentPE = undefined;
+ break;
+ }
+ });
+ if (mouseDownEvent) {
+ currentPE = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ currentPE.pushEvent(mouseDownEvent);
+ protoExpectations.push(currentPE);
+ }
+ return protoExpectations;
+ }
+
+ /**
+ * Solitary Tap events are simple Responses:
+ *
+ * RRR
+ * TTT
+ *
+ * TapDowns are part of Responses.
+ *
+ * RRRRRRR
+ * DDD TTT
+ *
+ * TapCancels are part of Responses, which seems strange. They always go
+ * with scrolls, so they'll probably be merged with scroll Responses.
+ * TapCancels can take a significant amount of time and account for a
+ * significant amount of work, which should be grouped with the scroll UEs
+ * if possible.
+ *
+ * RRRRRRR
+ * DDD CCC
+ **/
+ function handleTapResponseEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ forEventTypesIn(sortedInputEvents, TAP_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.TAP_DOWN:
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TAP);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ break;
+
+ case INPUT_TYPE.TAP:
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ // Sometimes we get Tap events with no TapDown, sometimes we get
+ // TapDown events. Handle both.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TAP);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ currentPE = undefined;
+ break;
+
+ case INPUT_TYPE.TAP_CANCEL:
+ if (!currentPE) {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+
+ if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TAP);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ currentPE = undefined;
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * The PinchBegin and the first PinchUpdate comprise a Response, then the
+ * rest of the PinchUpdates comprise an Animation.
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * BBB UUU UUU UUU UUU UUU EEE
+ */
+ function handlePinchEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let sawFirstUpdate = false;
+ const modelBounds = modelHelper.model.bounds;
+ forEventTypesIn(sortedInputEvents, PINCH_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.PINCH_BEGIN:
+ if (currentPE &&
+ currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE.pushEvent(event);
+ break;
+ }
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.PINCH);
+ currentPE.pushEvent(event);
+ currentPE.isAnimationBegin = true;
+ protoExpectations.push(currentPE);
+ sawFirstUpdate = false;
+ break;
+
+ case INPUT_TYPE.PINCH_UPDATE:
+ // Like ScrollUpdates, the Begin and the first Update constitute a
+ // Response, then the rest of the Updates constitute an Animation
+ // that begins when the Response ends. If the user pauses in the
+ // middle of an extended pinch gesture, then multiple Animations
+ // will be created.
+ if (!currentPE ||
+ ((currentPE.type === ProtoExpectation.RESPONSE_TYPE) &&
+ sawFirstUpdate) ||
+ !currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.PINCH);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ } else {
+ currentPE.pushEvent(event);
+ sawFirstUpdate = true;
+ }
+ break;
+
+ case INPUT_TYPE.PINCH_END:
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ }
+ currentPE = undefined;
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Flings are defined by 3 types of events: FlingStart, FlingCancel, and the
+ * renderer fling event. Flings do not begin with a Response. Flings end
+ * either at the beginning of a FlingCancel, or at the end of the renderer
+ * fling event.
+ *
+ * AAAAAAAAAAAAAAAAAAAAAAAAAA
+ * SSS
+ * RRRRRRRRRRRRRRRRRRRRRR
+ *
+ *
+ * AAAAAAAAAAA
+ * SSS CCC
+ */
+ function handleFlingEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+
+ function isRendererFling(event) {
+ return event.title === RENDERER_FLING_TITLE;
+ }
+ const browserHelper = modelHelper.browserHelper;
+ const flingEvents = browserHelper.getAllAsyncSlicesMatching(
+ isRendererFling);
+
+ forEventTypesIn(sortedInputEvents, FLING_TYPE_NAMES, function(event) {
+ flingEvents.push(event);
+ });
+ flingEvents.sort(compareEvents);
+
+ flingEvents.forEach(function(event) {
+ if (event.title === RENDERER_FLING_TITLE) {
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.FLING);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ return;
+ }
+
+ switch (event.typeName) {
+ case INPUT_TYPE.FLING_START:
+ if (currentPE) {
+ warn({
+ type: 'UserModelBuilder',
+ message: 'Unexpected FlingStart',
+ showToUser: false,
+ });
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.FLING);
+ currentPE.pushEvent(event);
+ // Set end to an invalid value so that it can be noticed and fixed
+ // later.
+ currentPE.end = 0;
+ protoExpectations.push(currentPE);
+ }
+ break;
+
+ case INPUT_TYPE.FLING_CANCEL:
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ // FlingCancel events start when TouchStart events start, which is
+ // typically when a Response starts. FlingCancel events end when
+ // chrome acknowledges them, not when they update the screen. So
+ // there might be one more frame during the FlingCancel, after
+ // this Animation ends. That won't affect the scoring algorithms,
+ // and it will make the UEs look more correct if they don't
+ // overlap unnecessarily.
+ currentPE.end = event.start;
+ currentPE = undefined;
+ } else {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ }
+ break;
+ }
+ });
+ // If there was neither a FLING_CANCEL nor a renderer fling after the
+ // FLING_START, then assume that it ends at the end of the model, so set
+ // the end of currentPE to the end of the model.
+ if (currentPE && !currentPE.end) {
+ currentPE.end = modelHelper.model.bounds.max;
+ }
+ return protoExpectations;
+ }
+
+ /**
+ * The TouchStart and the first TouchMove comprise a Response, then the
+ * rest of the TouchMoves comprise an Animation.
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * SSS MMM MMM MMM MMM MMM EEE
+ *
+ * If there are no TouchMove events in between a TouchStart and a TouchEnd,
+ * then it's just a Response.
+ *
+ * RRRRRRR
+ * SSS EEE
+ */
+ function handleTouchEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let sawFirstMove = false;
+ forEventTypesIn(sortedInputEvents, TOUCH_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.TOUCH_START:
+ if (currentPE) {
+ // NB: currentPE will probably be merged with something from
+ // handlePinchEvents(). Multiple TouchStart events without an
+ // intervening TouchEnd logically implies that multiple fingers
+ // are on the screen, so this is probably a pinch gesture.
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TOUCH);
+ currentPE.pushEvent(event);
+ currentPE.isAnimationBegin = true;
+ protoExpectations.push(currentPE);
+ sawFirstMove = false;
+ }
+ break;
+
+ case INPUT_TYPE.TOUCH_MOVE:
+ if (!currentPE) {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.TOUCH);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ break;
+ }
+
+ // Like Scrolls and Pinches, the Response is defined to be the
+ // TouchStart plus the first TouchMove, then the rest of the
+ // TouchMoves constitute an Animation.
+ if ((sawFirstMove &&
+ (currentPE.type === ProtoExpectation.RESPONSE_TYPE)) ||
+ !currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ // If there's already a touchmove in the currentPE or it's not
+ // near event, then finish it and start a new animation.
+ const prevEnd = currentPE.end;
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.TOUCH);
+ currentPE.pushEvent(event);
+ // It's possible for there to be a gap between TouchMoves, but
+ // that doesn't mean that there should be an Idle UE there.
+ currentPE.start = prevEnd;
+ protoExpectations.push(currentPE);
+ } else {
+ currentPE.pushEvent(event);
+ sawFirstMove = true;
+ }
+ break;
+
+ case INPUT_TYPE.TOUCH_END:
+ if (!currentPE) {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+ if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE.pushEvent(event);
+ } else {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ }
+ currentPE = undefined;
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * The first ScrollBegin and the first ScrollUpdate comprise a Response,
+ * then the rest comprise an Animation.
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * BBB UUU UUU UUU UUU UUU EEE
+ */
+ function handleScrollEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let sawFirstUpdate = false;
+ forEventTypesIn(sortedInputEvents, SCROLL_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.SCROLL_BEGIN:
+ // Always begin a new PE even if there already is one, unlike
+ // PinchBegin.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.SCROLL);
+ currentPE.pushEvent(event);
+ currentPE.isAnimationBegin = true;
+ protoExpectations.push(currentPE);
+ sawFirstUpdate = false;
+ break;
+
+ case INPUT_TYPE.SCROLL_UPDATE:
+ if (currentPE) {
+ if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS) &&
+ ((currentPE.type === ProtoExpectation.ANIMATION_TYPE) ||
+ !sawFirstUpdate)) {
+ currentPE.pushEvent(event);
+ sawFirstUpdate = true;
+ } else {
+ currentPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
+ INITIATOR_TYPE.SCROLL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ } else {
+ // ScrollUpdate without ScrollBegin.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.SCROLL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ break;
+
+ case INPUT_TYPE.SCROLL_END:
+ if (!currentPE) {
+ warn({
+ type: 'UserModelBuilder',
+ message: 'Unexpected ScrollEnd',
+ showToUser: false,
+ });
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+ currentPE.pushEvent(event);
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Returns proto expectations for video animation events.
+ *
+ * Video animations represent video playback, and are based on
+ * VideoPlayback async events (going from the VideoFrameCompositor::Start
+ * to VideoFrameCompositor::Stop calls)
+ */
+ function handleVideoAnimations(modelHelper, sortedInputEvents, warn) {
+ const events = [];
+ for (const pid in modelHelper.rendererHelpers) {
+ for (const tid in modelHelper.rendererHelpers[pid].process.threads) {
+ for (const asyncSlice of
+ modelHelper.rendererHelpers[pid].process.threads[tid]
+ .asyncSliceGroup.slices) {
+ if (asyncSlice.title === PLAYBACK_EVENT_TITLE) {
+ events.push(asyncSlice);
+ }
+ }
+ }
+ }
+
+ events.sort(tr.importer.compareEvents);
+
+ const protoExpectations = [];
+ for (const event of events) {
+ const currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.VIDEO);
+ currentPE.start = event.start;
+ currentPE.end = event.end;
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+
+ return protoExpectations;
+ }
+
+ /**
+ * Returns proto expectations for VR animation events.
+ */
+ function handleVrAnimations(modelHelper, sortedInputEvents, warn) {
+ const events = [];
+
+ // Find all the processes we should check
+ const processes = [];
+ if (typeof modelHelper.gpuHelper !== 'undefined') {
+ processes.push(modelHelper.gpuHelper.process);
+ }
+ for (const helper of Object.values(modelHelper.rendererHelpers)) {
+ processes.push(helper.process);
+ }
+ for (const helper of Object.values(modelHelper.browserHelpers)) {
+ processes.push(helper.process);
+ }
+
+ // Add all counter samples to the list of events we care about
+ let vrCounterStart = Number.MAX_SAFE_INTEGER;
+ let vrEventStart = Number.MAX_SAFE_INTEGER;
+ for (const proc of processes) {
+ for (const [counterName, counterSeries] of
+ Object.entries(proc.counters)) {
+ if (VR_COUNTER_NAMES.includes(counterName)) {
+ for (const series of counterSeries.series) {
+ for (const sample of series.samples) {
+ events.push(sample);
+ vrCounterStart = Math.min(vrCounterStart, sample.timestamp);
+ }
+ }
+ }
+ }
+ for (const thread of Object.values(proc.threads)) {
+ for (const container of thread.childEventContainers()) {
+ for (const slice of container.slices) {
+ if (VR_EVENT_NAMES.includes(slice.title)) {
+ events.push(slice);
+ vrEventStart = Math.min(vrEventStart, slice.start);
+ }
+ }
+ }
+ }
+ }
+
+ if (events.length === 0) {
+ return [];
+ }
+
+ events.sort(function(x, y) {
+ if (x.range.min !== y.range.min) {
+ return x.range.min - y.range.min;
+ }
+ return x.guid - y.guid;
+ });
+
+ vrCounterStart = (vrCounterStart === Number.MAX_SAFE_INTEGER) ?
+ 0 : vrCounterStart;
+ vrEventStart = (vrEventStart === Number.MAX_SAFE_INTEGER) ?
+ 0 : vrEventStart;
+ const vrAnimationStart = Math.max(vrCounterStart, vrEventStart) +
+ VR_RESPONSE_MS;
+ const responsePE = new ProtoExpectation(ProtoExpectation.RESPONSE_TYPE,
+ INITIATOR_TYPE.VR);
+ const animationPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
+ INITIATOR_TYPE.VR);
+ let lastResponseEvent;
+
+ for (const event of events) {
+ // Categorize the first 1s of VR time as entry/response
+ // TODO(bsheedy): Make this smarter by basing response duration off trace
+ // data instead of a fixed duration
+ if (event.range.min < vrAnimationStart) {
+ if (event instanceof tr.model.CounterSample) {
+ responsePE.pushSample(event);
+ } else {
+ responsePE.pushEvent(event);
+ }
+ lastResponseEvent = event;
+ } else {
+ if (event instanceof tr.model.CounterSample) {
+ animationPE.pushSample(event);
+ } else {
+ animationPE.pushEvent(event);
+ }
+ }
+ }
+
+ // Make sure that there isn't a gap between the two expectations
+ if (lastResponseEvent instanceof tr.model.CounterSample) {
+ animationPE.pushSample(lastResponseEvent);
+ } else {
+ animationPE.pushEvent(lastResponseEvent);
+ }
+ return [responsePE, animationPE];
+ }
+
+ /**
+ * CSS Animations are merged into AnimationExpectations when they intersect.
+ */
+ function handleCSSAnimations(modelHelper, sortedInputEvents, warn) {
+ // First find all the top-level CSS Animation async events.
+ const animationEvents = modelHelper.browserHelper.
+ getAllAsyncSlicesMatching(function(event) {
+ return ((event.title === CSS_ANIMATION_TITLE) &&
+ event.isTopLevel &&
+ (event.duration > 0));
+ });
+
+
+ // Time ranges where animations are actually running will be collected here.
+ // Each element will contain {min, max, animation}.
+ const animationRanges = [];
+
+ // This helper function will be called when a time range is found
+ // during which the animation is actually running.
+ function pushAnimationRange(start, end, animation) {
+ const range = tr.b.math.Range.fromExplicitRange(start, end);
+ range.animation = animation;
+ animationRanges.push(range);
+ }
+
+ animationEvents.forEach(function(animation) {
+ if (animation.subSlices.length === 0) {
+ pushAnimationRange(animation.start, animation.end, animation);
+ } else {
+ // Now run a state machine over the animation's subSlices, which
+ // indicate the animations running/paused/finished states, in order to
+ // find ranges where the animation was actually running.
+ let start = undefined;
+ animation.subSlices.forEach(function(sub) {
+ if ((sub.args.data.state === 'running') &&
+ (start === undefined)) {
+ // It's possible for the state to alternate between running and
+ // pending, but the animation is still running in that case,
+ // so only set start if the state is changing from one of the halted
+ // states.
+ start = sub.start;
+ } else if ((sub.args.data.state === 'paused') ||
+ (sub.args.data.state === 'idle') ||
+ (sub.args.data.state === 'finished')) {
+ if (start === undefined) {
+ // An animation was already running when the trace started.
+ // (Actually, it's possible that the animation was in the 'idle'
+ // state when tracing started, but that should be rare, and will
+ // be fixed when async events are buffered.)
+ // http: //crbug.com/565627
+ start = modelHelper.model.bounds.min;
+ }
+
+ pushAnimationRange(start, sub.start, animation);
+ start = undefined;
+ }
+ });
+
+ // An animation was still running when the
+ // top-level animation event ended.
+ if (start !== undefined) {
+ pushAnimationRange(start, animation.end, animation);
+ }
+ }
+ });
+
+ // Now we have a set of time ranges when css animations were actually
+ // running.
+ // Leave merging intersecting animations to mergeIntersectingAnimations(),
+ // after findFrameEventsForAnimations removes frame-less animations.
+
+ return animationRanges.map(function(range) {
+ const protoExpectation = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.CSS);
+ protoExpectation.start = range.min;
+ protoExpectation.end = range.max;
+ protoExpectation.associatedEvents.push(range.animation);
+ return protoExpectation;
+ });
+ }
+
+ /**
+ * Get all the events (prepareMailbox and serviceScriptedAnimations)
+ * relevant to WebGL. Note that modelHelper is the helper object containing
+ * the model, and mailboxEvents and animationEvents are arrays where the
+ * events are being pushed into (DrawingBuffer::prepareMailbox events go
+ * into mailboxEvents; PageAnimator::serviceScriptedAnimations events go
+ * into animationEvents). The function does not return anything but
+ * modifies mailboxEvents and animationEvents.
+ */
+ function findWebGLEvents(modelHelper, mailboxEvents, animationEvents) {
+ for (const event of modelHelper.model.getDescendantEvents()) {
+ if (event.title === 'DrawingBuffer::prepareMailbox') {
+ mailboxEvents.push(event);
+ } else if (event.title === 'PageAnimator::serviceScriptedAnimations') {
+ animationEvents.push(event);
+ }
+ }
+ }
+
+ /**
+ * Returns a list of events in mailboxEvents that have an event in
+ * animationEvents close by (within ANIMATION_MERGE_THRESHOLD_MS).
+ */
+ function findMailboxEventsNearAnimationEvents(
+ mailboxEvents, animationEvents) {
+ if (animationEvents.length === 0) return [];
+
+ mailboxEvents.sort(compareEvents);
+ animationEvents.sort(compareEvents);
+ const animationIterator = animationEvents[Symbol.iterator]();
+ let animationEvent = animationIterator.next().value;
+
+ const filteredEvents = [];
+
+ // We iterate through the mailboxEvents. With each event, we check if
+ // there is a animationEvent near it, and if so, add it to the result.
+ for (const event of mailboxEvents) {
+ // If the current animationEvent is too far before the mailboxEvent,
+ // we advance until we get to the next animationEvent that is not too
+ // far before the animationEvent.
+ while (animationEvent &&
+ (animationEvent.start < (
+ event.start - ANIMATION_MERGE_THRESHOLD_MS))) {
+ animationEvent = animationIterator.next().value;
+ }
+
+ // If there aren't any more animationEvents, then that means all the
+ // remaining mailboxEvents are too far after the animationEvents, so
+ // we can quit now.
+ if (!animationEvent) break;
+
+ // If there's a animationEvent close to the mailboxEvent, then we push
+ // the current mailboxEvent onto the stack.
+ if (animationEvent.start < (event.start + ANIMATION_MERGE_THRESHOLD_MS)) {
+ filteredEvents.push(event);
+ }
+ }
+ return filteredEvents;
+ }
+
+ /**
+ * Merge consecutive mailbox events into a ProtoExpectation. Note: Only
+ * the drawingBuffer::prepareMailbox events will end up in the
+ * associatedEvents. The PageAnimator::serviceScriptedAnimations events
+ * will not end up in the associatedEvents.
+ */
+ function createProtoExpectationsFromMailboxEvents(mailboxEvents) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ for (const event of mailboxEvents) {
+ if (currentPE === undefined || !currentPE.isNear(
+ event, ANIMATION_MERGE_THRESHOLD_MS)) {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.WEBGL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ } else {
+ currentPE.pushEvent(event);
+ }
+ }
+ return protoExpectations;
+ }
+
+ // WebGL animations are identified by the DrawingBuffer::prepareMailbox
+ // and PageAnimator::serviceScriptedAnimations events (one of each per frame)
+ // and consecutive frames are merged into the same animation.
+ function handleWebGLAnimations(modelHelper, sortedInputEvents, warn) {
+ // Get the prepareMailbox and scriptedAnimation events.
+ const prepareMailboxEvents = [];
+ const scriptedAnimationEvents = [];
+
+ findWebGLEvents(modelHelper, prepareMailboxEvents, scriptedAnimationEvents);
+ const webGLMailboxEvents = findMailboxEventsNearAnimationEvents(
+ prepareMailboxEvents, scriptedAnimationEvents);
+
+ return createProtoExpectationsFromMailboxEvents(webGLMailboxEvents);
+ }
+
+
+ function postProcessProtoExpectations(modelHelper, protoExpectations) {
+ // protoExpectations is input only. Returns a modified set of
+ // ProtoExpectations. The order is important.
+ protoExpectations = findFrameEventsForAnimations(
+ modelHelper, protoExpectations);
+ protoExpectations = mergeIntersectingResponses(protoExpectations);
+ protoExpectations = mergeIntersectingAnimations(protoExpectations);
+ protoExpectations = fixResponseAnimationStarts(protoExpectations);
+ protoExpectations = fixTapResponseTouchAnimations(protoExpectations);
+ return protoExpectations;
+ }
+
+ /**
+ * TouchStarts happen at the same time as ScrollBegins.
+ * It's easier to let multiple handlers create multiple overlapping
+ * Responses and then merge them, rather than make the handlers aware of the
+ * other handlers' PEs.
+ *
+ * For example:
+ * RR
+ * RRR -> RRRRR
+ * RR
+ *
+ * protoExpectations is input only.
+ * Returns a modified set of ProtoExpectations.
+ */
+ function mergeIntersectingResponses(protoExpectations) {
+ const newPEs = [];
+ while (protoExpectations.length) {
+ const pe = protoExpectations.shift();
+ newPEs.push(pe);
+
+ // Only consider Responses for now.
+ if (pe.type !== ProtoExpectation.RESPONSE_TYPE) continue;
+
+ for (let i = 0; i < protoExpectations.length; ++i) {
+ const otherPE = protoExpectations[i];
+
+ if (otherPE.type !== pe.type) continue;
+
+ if (!otherPE.intersects(pe)) continue;
+
+ // Don't merge together Responses of the same type.
+ // If handleTouchEvents wanted two of its Responses to be merged, then
+ // it would have made them that way to begin with.
+ const typeNames = pe.associatedEvents.map(function(event) {
+ return event.typeName;
+ });
+ if (otherPE.containsTypeNames(typeNames)) continue;
+
+ pe.merge(otherPE);
+ protoExpectations.splice(i, 1);
+
+ // Don't skip the next otherPE!
+ --i;
+ }
+ }
+ return newPEs;
+ }
+
+ /**
+ * An animation is simply an expectation of 60fps between start and end.
+ * If two animations overlap, then merge them.
+ *
+ * For example:
+ * AA
+ * AAA -> AAAAA
+ * AA
+ *
+ * protoExpectations is input only.
+ * Returns a modified set of ProtoExpectations.
+ */
+ function mergeIntersectingAnimations(protoExpectations) {
+ const newPEs = [];
+ while (protoExpectations.length) {
+ const pe = protoExpectations.shift();
+ newPEs.push(pe);
+
+ // Only consider Animations for now.
+ if (pe.type !== ProtoExpectation.ANIMATION_TYPE) continue;
+
+ const isCSS = pe.initiatorType === INITIATOR_TYPE.CSS;
+ const isFling = pe.containsTypeNames([INPUT_TYPE.FLING_START]);
+ const isVideo = pe.initiatorType === INITIATOR_TYPE.VIDEO;
+
+ for (let i = 0; i < protoExpectations.length; ++i) {
+ const otherPE = protoExpectations[i];
+
+ if (otherPE.type !== pe.type) continue;
+
+ // Don't merge some animation types with others.
+ if ((isCSS && otherPE.initiatorType !== INITIATOR_TYPE.CSS) ||
+ isFling !== otherPE.containsTypeNames([INPUT_TYPE.FLING_START]) ||
+ isVideo && otherPE.initiatorType !== INITIATOR_TYPE.VIDEO ||
+ otherPE.initiatorType === INITIATOR_TYPE.VR) {
+ continue;
+ }
+
+ if (isCSS) {
+ if (!pe.isNear(otherPE, ANIMATION_MERGE_THRESHOLD_MS)) {
+ continue;
+ }
+ } else if (!otherPE.intersects(pe)) {
+ continue;
+ }
+
+ pe.merge(otherPE);
+ protoExpectations.splice(i, 1);
+ // Don't skip the next otherPE!
+ --i;
+ }
+ }
+ return newPEs;
+ }
+
+ /**
+ * The ends of responses frequently overlap the starts of animations.
+ * Fix the animations to reflect the fact that the user can only start to
+ * expect 60fps after the response.
+ *
+ * For example:
+ * RRR -> RRRAA
+ * AAAA
+ *
+ * protoExpectations is input only.
+ * Returns a modified set of ProtoExpectations.
+ */
+ function fixResponseAnimationStarts(protoExpectations) {
+ protoExpectations.forEach(function(ape) {
+ // Only consider animations for now.
+ if (ape.type !== ProtoExpectation.ANIMATION_TYPE) {
+ return;
+ }
+
+ protoExpectations.forEach(function(rpe) {
+ // Only consider responses for now.
+ if (rpe.type !== ProtoExpectation.RESPONSE_TYPE) {
+ return;
+ }
+
+ // Only consider responses that end during the animation.
+ if (!ape.containsTimestampInclusive(rpe.end)) {
+ return;
+ }
+
+ // Ignore Responses that are entirely contained by the animation.
+ if (ape.containsTimestampInclusive(rpe.start)) {
+ return;
+ }
+
+ // Move the animation start to the response end.
+ ape.start = rpe.end;
+ // Remove any frames that were part of the animation but are now before
+ // the animation.
+ if (ape.associatedEvents !== undefined) {
+ ape.associatedEvents = ape.associatedEvents.filter(
+ e => (!isImplFrameEvent(e) || e.start >= ape.start));
+ }
+ });
+ });
+ return protoExpectations;
+ }
+
+ function isImplFrameEvent(event) {
+ return event.title === tr.model.helpers.IMPL_RENDERING_STATS;
+ }
+
+ /**
+ * Merge Tap Responses that overlap Touch-only Animations.
+ * https: *github.com/catapult-project/catapult/issues/1431
+ */
+ function fixTapResponseTouchAnimations(protoExpectations) {
+ function isTapResponse(pe) {
+ return (pe.type === ProtoExpectation.RESPONSE_TYPE) &&
+ pe.containsTypeNames([INPUT_TYPE.TAP]);
+ }
+ function isTouchAnimation(pe) {
+ return (pe.type === ProtoExpectation.ANIMATION_TYPE) &&
+ pe.containsTypeNames([INPUT_TYPE.TOUCH_MOVE]) &&
+ !pe.containsTypeNames([
+ INPUT_TYPE.SCROLL_UPDATE, INPUT_TYPE.PINCH_UPDATE]);
+ }
+ const newPEs = [];
+ while (protoExpectations.length) {
+ const pe = protoExpectations.shift();
+ newPEs.push(pe);
+
+ // protoExpectations are sorted by start time, and we don't know whether
+ // the Tap Response or the Touch Animation will be first
+ const peIsTapResponse = isTapResponse(pe);
+ const peIsTouchAnimation = isTouchAnimation(pe);
+ if (!peIsTapResponse && !peIsTouchAnimation) {
+ continue;
+ }
+
+ for (let i = 0; i < protoExpectations.length; ++i) {
+ const otherPE = protoExpectations[i];
+
+ if (!otherPE.intersects(pe)) continue;
+
+ if (peIsTapResponse && !isTouchAnimation(otherPE)) continue;
+
+ if (peIsTouchAnimation && !isTapResponse(otherPE)) continue;
+
+ // pe might be the Touch Animation, but the merged ProtoExpectation
+ // should be a Response.
+ pe.type = ProtoExpectation.RESPONSE_TYPE;
+
+ pe.merge(otherPE);
+ protoExpectations.splice(i, 1);
+ // Don't skip the next otherPE!
+ --i;
+ }
+ }
+ return newPEs;
+ }
+
+ function findFrameEventsForAnimations(modelHelper, protoExpectations) {
+ const newPEs = [];
+ const frameEventsByPid = getSortedFrameEventsByProcess(modelHelper);
+
+ for (const pe of protoExpectations) {
+ if (pe.type !== ProtoExpectation.ANIMATION_TYPE) {
+ newPEs.push(pe);
+ continue;
+ }
+
+ const frameEvents = [];
+ for (const pid of Object.keys(modelHelper.rendererHelpers)) {
+ const range = tr.b.math.Range.fromExplicitRange(pe.start, pe.end);
+ frameEvents.push.apply(frameEvents,
+ range.filterArray(frameEventsByPid[pid], e => e.start));
+ }
+
+ // If a tree falls in a forest...
+ // If there were not actually any frames while the animation was
+ // running, then it wasn't really an animation, now, was it?
+ // Philosophy aside, the system_health Animation metrics fail hard if
+ // there are no frames in an AnimationExpectation.
+ // Since WebGL and VR animations don't generate this type of frame
+ // event, don't remove them if it's a WebGL or VR animation.
+ if (frameEvents.length === 0 &&
+ !(pe.initiatorType === INITIATOR_TYPE.WEBGL ||
+ pe.initiatorType === INITIATOR_TYPE.VR)) {
+ pe.type = ProtoExpectation.IGNORED_TYPE;
+ newPEs.push(pe);
+ continue;
+ }
+
+ pe.associatedEvents.addEventSet(frameEvents);
+ newPEs.push(pe);
+ }
+
+ return newPEs;
+ }
+
+ /**
+ * Check that none of the handlers accidentally ignored an input event.
+ */
+ function checkAllInputEventsHandled(
+ modelHelper, sortedInputEvents, protoExpectations, warn) {
+ const handledEvents = [];
+ protoExpectations.forEach(function(protoExpectation) {
+ protoExpectation.associatedEvents.forEach(function(event) {
+ // Ignore CSS Animations that might have multiple active ranges.
+ if ((event.title === CSS_ANIMATION_TITLE) &&
+ (event.subSlices.length > 0)) {
+ return;
+ }
+
+ if ((handledEvents.indexOf(event) >= 0) &&
+ (!isImplFrameEvent(event))) {
+ warn({
+ type: 'UserModelBuilder',
+ message: `double-handled event: ${event.typeName} @ ${event.start}`,
+ showToUser: false,
+ });
+ return;
+ }
+ handledEvents.push(event);
+ });
+ });
+
+ sortedInputEvents.forEach(function(event) {
+ if (handledEvents.indexOf(event) < 0) {
+ warn({
+ type: 'UserModelBuilder',
+ message: `double-handled event: ${event.typeName} @ ${event.start}`,
+ showToUser: false,
+ });
+ }
+ });
+ }
+
+ /**
+ * Find ProtoExpectations, post-process them, convert them to real UEs.
+ */
+ function findInputExpectations(modelHelper) {
+ // Prevent helper functions from producing too many import warnings.
+ let warning;
+ function warn(w) {
+ // Keep only the first warning.
+ if (warning) return;
+ warning = w;
+ }
+
+ const sortedInputEvents = getSortedInputEvents(modelHelper);
+ let protoExpectations = findProtoExpectations(
+ modelHelper, sortedInputEvents, warn);
+ protoExpectations = postProcessProtoExpectations(
+ modelHelper, protoExpectations);
+ checkAllInputEventsHandled(
+ modelHelper, sortedInputEvents, protoExpectations, warn);
+
+ if (warning) modelHelper.model.importWarning(warning);
+
+ const expectations = [];
+ protoExpectations.forEach(function(protoExpectation) {
+ const ir = protoExpectation.createInteractionRecord(modelHelper.model);
+ if (ir) {
+ expectations.push(ir);
+ }
+ });
+ return expectations;
+ }
+
+ return {
+ findInputExpectations,
+ compareEvents,
+ CSS_ANIMATION_TITLE,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/find_load_expectations.html b/chromium/third_party/catapult/tracing/tracing/importer/find_load_expectations.html
new file mode 100644
index 00000000000..a5a3265e0b8
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/find_load_expectations.html
@@ -0,0 +1,325 @@
+<!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/event_finder_utils.html">
+<link rel="import" href="/tracing/extras/chrome/time_to_interactive.html">
+<link rel="import" href="/tracing/model/user_model/load_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ const LONG_TASK_THRESHOLD_MS = 50;
+
+ const IGNORE_URLS = [
+ // Blank URLs correspond to initial empty loads and we want to ignore
+ // them.
+ '',
+ 'about:blank',
+ ];
+
+
+ /**
+ * @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;
+ }
+
+ 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 findFirstMeaningfulPaintCandidates(rendererHelper) {
+ const candidatesForFrameId = {};
+ for (const ev of rendererHelper.process.getDescendantEvents()) {
+ if (!tr.e.chrome.EventFinderUtils.hasCategoryAndName(
+ ev, 'loading', 'firstMeaningfulPaintCandidate')) {
+ continue;
+ }
+ if (rendererHelper.isTelemetryInternalEvent(ev)) continue;
+ const frameIdRef = ev.args.frame;
+ if (frameIdRef === undefined) continue;
+ let list = candidatesForFrameId[frameIdRef];
+ if (list === undefined) {
+ candidatesForFrameId[frameIdRef] = list = [];
+ }
+ list.push(ev);
+ }
+ return candidatesForFrameId;
+ }
+
+ /**
+ * Returns Time to Interactive and First CPU Idle for the
+ * given parameters. See the time_to_interactive.html module for detailed
+ * description and implementation of these metrics. The two metrics are
+ * computed together in the same function because almost all the computed
+ * parameters, for example list of relevant long tasks, are same for these two
+ * metrics, and this helps avoid duplicate computation.
+ *
+ * @param {tr.model.helpers.ChromeRendererHelper} rendererHelper - Renderer
+ * helper for the renderer of interest.
+ * @param {tr.model.ThreadSlice} navigationStart - The navigation start
+ * event for which loading metrics is being computed.
+ * @param {tr.model.ThreadSlice} fmpEvent - The first meaningful paint
+ * event for which loading metrics is being computed.
+ * @param {tr.model.ThreadSlice} domContentLoadedEndEvent - Event
+ * corresponding to finish of dom content loading
+ * @param {number} searchWindowEnd - Time till when to search for a TTI. This
+ * value is either the start of next navigation or the end of the trace.
+ * @returns {interactiveSample: {number}|undefined,
+ * firstCpuIdleTime: {number}|undefined}
+ */
+ function computeInteractivityMetricSample_(rendererHelper, navigationStart,
+ fmpEvent, domContentLoadedEndEvent, searchWindowEnd) {
+ // Cannot determine TTI if DomContentLoadedEnd was never reached or if
+ // there is no corresponding fmpEvent.
+ if (domContentLoadedEndEvent === undefined || fmpEvent === undefined) {
+ return {interactiveTime: undefined, firstCpuIdleTime: undefined};
+ }
+
+ const firstMeaningfulPaintTime = fmpEvent.start;
+ const mainThreadTasks =
+ tr.e.chrome.EventFinderUtils.findToplevelSchedulerTasks(
+ rendererHelper.mainThread);
+
+ const longTasks = mainThreadTasks.filter(
+ task => task.duration >= LONG_TASK_THRESHOLD_MS);
+ const longTasksInWindow = longTasks.filter(
+ task => task.range.intersectsExplicitRangeInclusive(
+ firstMeaningfulPaintTime, searchWindowEnd));
+
+ const resourceLoadEvents = getNetworkEventsInRange(rendererHelper.process,
+ tr.b.math.Range.fromExplicitRange(navigationStart.start,
+ searchWindowEnd));
+
+ const firstCpuIdleTime =
+ tr.e.chrome.findFirstCpuIdleTime(
+ firstMeaningfulPaintTime, searchWindowEnd,
+ domContentLoadedEndEvent.start, longTasksInWindow);
+
+ // If we did not find any resource load events, interactiveTime should not
+ // be computed to avoid reporting misleading values.
+ const interactiveTime = resourceLoadEvents.length > 0 ?
+ tr.e.chrome.findInteractiveTime(
+ firstMeaningfulPaintTime, searchWindowEnd,
+ domContentLoadedEndEvent.start, longTasksInWindow,
+ resourceLoadEvents) : undefined;
+
+ return {interactiveTime, firstCpuIdleTime};
+ }
+
+ /* Constructs a loading metrics for the specified navigation start event and
+ * the corresponding fmpEvent and returns a sample including the metrics and
+ * navigationStartEvent, fmpEvent, url and the frameId.
+ *
+ * @param {tr.model.helpers.ChromeRendererHelper} rendererHelper - Renderer
+ * helper for the renderer of interest.
+ * @param {Map.<string, Array<!tr.model.ThreadSlice>>} frameToNavStartEvents -
+ * Map from frame ids to sorted array of navigation start events.
+ * @param {Map.<string, Array<!tr.model.ThreadSlice>>}
+ * frameToDomContentLoadedEndEvents - Map from frame ids to sorted array
+ * of DOMContentLoadedEnd events.
+ * @param {tr.model.ThreadSlice} navigationStart - The navigation start
+ * event for which loading metrics is being computed.
+ * @param {tr.model.ThreadSlice} fmpEvent - The first meaningful paint
+ * event for which loading metrics is being computed.
+ * @param {number} searchWindowEnd - The end of the current navigation either
+ * because new navigation has started or the trace has ended.
+ * @param {string} url - URL of the current main frame document.
+ * @param {number} frameId - fameId.
+ * @returns {{start: {number}, duration: {number},
+ * fmpEvent: {tr.model.ThreadSlice}, navStart: {tr.model.ThreadSlice},
+ * dclEndTime: {tr.model.ThreadSlice}, firstCpuIdleTime: {number}|undefined,
+ * interactiveSample: {number}|undefined, url: {string}, frameId: {number}}}
+ */
+ function constructLoadingExpectation_(rendererHelper,
+ frameToDomContentLoadedEndEvents, navigationStart, fmpEvent,
+ searchWindowEnd, url, frameId) {
+ // Find when dom content has loaded.
+ const dclTimesForFrame =
+ frameToDomContentLoadedEndEvents.get(frameId) || [];
+ const dclSearchRange = tr.b.math.Range.fromExplicitRange(
+ navigationStart.start, searchWindowEnd);
+ const dclTimesInWindow =
+ dclSearchRange.filterArray(dclTimesForFrame, event => event.start);
+ let domContentLoadedEndEvent = undefined;
+ if (dclTimesInWindow.length !== 0) {
+ // TODO(catapult:#3796): Ideally a frame should reach DomContentLoadedEnd
+ // at most once within two navigationStarts, but sometimes there is a
+ // strange DclEnd event immediately following the navigationStart, and
+ // then the 'real' dclEnd happens later. It is not clear how to best
+ // determine the correct dclEnd value. For now, if there are multiple
+ // DclEnd events in the search window, we just pick the last one.
+ domContentLoadedEndEvent =
+ dclTimesInWindow[dclTimesInWindow.length - 1];
+ }
+
+ const {interactiveTime, firstCpuIdleTime} =
+ computeInteractivityMetricSample_(
+ rendererHelper, navigationStart, fmpEvent,
+ domContentLoadedEndEvent, searchWindowEnd);
+
+ const duration = (interactiveTime === undefined) ?
+ searchWindowEnd - navigationStart.start :
+ interactiveTime - navigationStart.start;
+
+ return new tr.model.um.LoadExpectation(
+ rendererHelper.modelHelper.model,
+ tr.model.um.LOAD_SUBTYPE_NAMES.SUCCESSFUL, navigationStart.start,
+ duration, rendererHelper.process, navigationStart, fmpEvent,
+ domContentLoadedEndEvent, firstCpuIdleTime, interactiveTime, url,
+ frameId);
+ }
+
+ /**
+ * Computes the loading expectations for a renderer represented by
+ * |rendererHelper| and returns a list of samples. The loading
+ * expectation is the time between navigation start and the time to
+ * be interactive. There will be one load expectation corresponding
+ * to each navigation start for loading main frames.
+ *
+ * Also, computes Time to First Meaningful Paint (TTFMP), and
+ * Time to First CPU Idle (TTFCI) along with time to interactive (TTI)
+ * and returns them along with the load expectation.
+ *
+ * First meaningful paint is the paint following the layout with the highest
+ * "Layout Significance". The Layout Significance is computed inside Blink,
+ * by FirstMeaningfulPaintDetector class. It logs
+ * "firstMeaningfulPaintCandidate" event every time the Layout Significance
+ * marks a record. TTFMP is the time between NavigationStart and the last
+ * firstMeaningfulPaintCandidate event.
+ *
+ * Design doc: https://goo.gl/vpaxv6
+ *
+ * Time to Interactive and Time to First CPU Idle is based on heuristics
+ * involving main thread and network activity, as well as First Meaningful
+ * Paint and DOMContentLoadedEnd event. See time_to_interactive.html module
+ * for detailed description and implementation of these two metrics.
+ */
+ function collectLoadExpectationsForRenderer(
+ rendererHelper) {
+ const samples = [];
+ const frameToNavStartEvents =
+ tr.e.chrome.EventFinderUtils.getSortedMainThreadEventsByFrame(
+ rendererHelper, 'navigationStart', 'blink.user_timing');
+ const frameToDomContentLoadedEndEvents =
+ tr.e.chrome.EventFinderUtils.getSortedMainThreadEventsByFrame(
+ rendererHelper, 'domContentLoadedEventEnd', 'blink.user_timing');
+
+ function addSamples(frameIdRef, navigationStart, fmpCandidateEvents,
+ searchWindowEnd, url) {
+ let fmpMarkerEvent =
+ tr.e.chrome.EventFinderUtils.
+ findLastEventStartingOnOrBeforeTimestamp(fmpCandidateEvents,
+ searchWindowEnd);
+ if (fmpMarkerEvent !== undefined &&
+ navigationStart.start > fmpMarkerEvent.start) {
+ // Don't use fmpCandidate if it is not corresponding this navigation.
+ fmpMarkerEvent = undefined;
+ }
+ samples.push(constructLoadingExpectation_(
+ rendererHelper, frameToDomContentLoadedEndEvents, navigationStart,
+ fmpMarkerEvent, searchWindowEnd, url, frameIdRef));
+ }
+
+ const candidatesForFrameId =
+ findFirstMeaningfulPaintCandidates(rendererHelper);
+
+ for (const [frameIdRef, navStartEvents] of frameToNavStartEvents) {
+ const fmpCandidateEvents = candidatesForFrameId[frameIdRef] || [];
+ let prevNavigation = {navigationEvent: undefined, url: undefined};
+
+ for (let index = 0; index < navStartEvents.length; index++) {
+ const currNavigation = navStartEvents[index];
+ let url;
+ let isLoadingMainFrame = false;
+
+ if (currNavigation.args.data) {
+ url = currNavigation.args.data.documentLoaderURL;
+ isLoadingMainFrame = currNavigation.args.data.isLoadingMainFrame;
+ } else {
+ // TODO(#4358): Delete old path of obtaining URL.
+ const snapshot = findFrameLoaderSnapshotAt(
+ rendererHelper, frameIdRef, currNavigation.start);
+ if (snapshot) {
+ url = snapshot.args.documentLoaderURL;
+ isLoadingMainFrame = snapshot.args.isLoadingMainFrame;
+ }
+ }
+
+ // Filter navigationStartEvents that do not correspond to a loading main
+ // frame, or has a URL that we do not care about.
+ if (!isLoadingMainFrame) continue;
+ if (url === undefined || IGNORE_URLS.includes(url)) continue;
+
+ if (prevNavigation.navigationEvent !== undefined) {
+ // Add a LoadExpectation for the previous navigation ending on or
+ // before current navigation.
+ addSamples(frameIdRef, prevNavigation.navigationEvent,
+ fmpCandidateEvents, currNavigation.start, prevNavigation.url);
+ }
+
+ prevNavigation = {navigationEvent: currNavigation, url};
+ }
+
+ // Handle the last navigation here.
+ if (prevNavigation.navigationEvent !== undefined) {
+ addSamples(frameIdRef, prevNavigation.navigationEvent,
+ fmpCandidateEvents, rendererHelper.modelHelper.chromeBounds.max,
+ prevNavigation.url);
+ }
+ }
+ return samples;
+ }
+
+
+ function findLoadExpectations(modelHelper) {
+ const loads = [];
+
+ const chromeHelper = modelHelper.model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ for (const pid in chromeHelper.rendererHelpers) {
+ const rendererHelper = chromeHelper.rendererHelpers[pid];
+ if (rendererHelper.isChromeTracingUI) continue;
+
+ loads.push.apply(loads,
+ collectLoadExpectationsForRenderer(rendererHelper));
+ }
+ return loads;
+ }
+
+ return {
+ findLoadExpectations,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/find_startup_expectations.html b/chromium/third_party/catapult/tracing/tracing/importer/find_startup_expectations.html
new file mode 100644
index 00000000000..72415a62604
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/find_startup_expectations.html
@@ -0,0 +1,88 @@
+<!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/model/user_model/startup_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ function getAllFrameEvents(modelHelper) {
+ const frameEvents = [];
+ frameEvents.push.apply(frameEvents,
+ modelHelper.browserHelper.getFrameEventsInRange(
+ tr.model.helpers.IMPL_FRAMETIME_TYPE, modelHelper.model.bounds));
+
+ for (const renderer of Object.values(modelHelper.rendererHelpers)) {
+ frameEvents.push.apply(frameEvents, renderer.getFrameEventsInRange(
+ tr.model.helpers.IMPL_FRAMETIME_TYPE, modelHelper.model.bounds));
+ }
+ return frameEvents.sort(tr.importer.compareEvents);
+ }
+
+ // If a thread contains a typical initialization slice, then the first event
+ // on that thread is a startup event.
+ function getStartupEvents(modelHelper) {
+ function isStartupSlice(slice) {
+ return slice.title === 'BrowserMainLoop::CreateThreads';
+ }
+ const events = modelHelper.browserHelper.getAllAsyncSlicesMatching(
+ isStartupSlice);
+ const deduper = new tr.model.EventSet();
+ events.forEach(function(event) {
+ const sliceGroup = event.parentContainer.sliceGroup;
+ const slice = sliceGroup && sliceGroup.findFirstSlice();
+ if (slice) {
+ deduper.push(slice);
+ }
+ });
+ return deduper.toArray();
+ }
+
+ // Match every event in |openingEvents| to the first following event from
+ // |closingEvents| and return an array containing a load interaction record
+ // for each pair.
+ function findStartupExpectations(modelHelper) {
+ const openingEvents = getStartupEvents(modelHelper);
+ const closingEvents = getAllFrameEvents(modelHelper);
+ const startups = [];
+ openingEvents.forEach(function(openingEvent) {
+ closingEvents.forEach(function(closingEvent) {
+ // Ignore opening event that already have a closing event.
+ if (openingEvent.closingEvent) return;
+
+ // Ignore closing events that already belong to an opening event.
+ if (closingEvent.openingEvent) return;
+
+ // Ignore closing events before |openingEvent|.
+ if (closingEvent.start <= openingEvent.start) return;
+
+ // Ignore events from different threads.
+ if (openingEvent.parentContainer.parent.pid !==
+ closingEvent.parentContainer.parent.pid) {
+ return;
+ }
+
+ // This is the first closing event for this opening event, record it.
+ openingEvent.closingEvent = closingEvent;
+ closingEvent.openingEvent = openingEvent;
+ const se = new tr.model.um.StartupExpectation(
+ modelHelper.model, openingEvent.start,
+ closingEvent.end - openingEvent.start);
+ se.associatedEvents.push(openingEvent);
+ se.associatedEvents.push(closingEvent);
+ startups.push(se);
+ });
+ });
+ return startups;
+ }
+
+ return {
+ findStartupExpectations,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/import.html b/chromium/third_party/catapult/tracing/tracing/importer/import.html
new file mode 100644
index 00000000000..8893120c2c5
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/import.html
@@ -0,0 +1,339 @@
+<!DOCTYPE html>
+<!--
+Copyright 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/base.html">
+<link rel="import" href="/tracing/base/timing.html">
+<link rel="import" href="/tracing/importer/empty_importer.html">
+<link rel="import" href="/tracing/importer/importer.html">
+<link rel="import" href="/tracing/importer/user_model_builder.html">
+<link rel="import" href="/tracing/ui/base/overlay.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ const Timing = tr.b.Timing;
+
+ function ImportOptions() {
+ this.shiftWorldToZero = true;
+ this.pruneEmptyContainers = true;
+ this.showImportWarnings = true;
+ this.trackDetailedModelStats = false;
+
+ // Callback called after
+ // importers run in which more data can be added to the model, before it is
+ // finalized.
+ this.customizeModelCallback = undefined;
+
+ const auditorTypes = tr.c.Auditor.getAllRegisteredTypeInfos();
+ this.auditorConstructors = auditorTypes.map(function(typeInfo) {
+ return typeInfo.constructor;
+ });
+ }
+
+ function Import(model, opt_options) {
+ if (model === undefined) {
+ throw new Error('Must provide model to import into.');
+ }
+
+ // TODO(dsinclair): Check the model is empty.
+
+ this.importing_ = false;
+ this.importOptions_ = opt_options || new ImportOptions();
+
+ this.model_ = model;
+ this.model_.importOptions = this.importOptions_;
+ }
+
+ Import.prototype = {
+ __proto__: Object.prototype,
+
+ /**
+ * Imports the provided traces into the model. The eventData type
+ * is undefined and will be passed to all the importers registered
+ * via Importer.register. The first importer that returns true
+ * for canImport(events) will be used to import the events.
+ *
+ * The primary trace is provided via the eventData variable. If multiple
+ * traces are to be imported, specify the first one as events, and the
+ * remainder in the opt_additionalEventData array.
+ *
+ * @param {Array} traces An array of eventData to be imported. Each
+ * eventData should correspond to a single trace file and will be handled by
+ * a separate importer.
+ */
+ importTraces(traces) {
+ const progressMeter = {
+ update(msg) {}
+ };
+
+ tr.b.Task.RunSynchronously(
+ this.createImportTracesTask(progressMeter, traces));
+ },
+
+ /**
+ * Imports a trace with the usual options from importTraces, but
+ * does so using idle callbacks, putting up an import dialog
+ * during the import process.
+ */
+ importTracesWithProgressDialog(traces) {
+ if (tr.isHeadless) {
+ throw new Error('Cannot use this method in headless mode.');
+ }
+
+ const overlay = tr.ui.b.Overlay();
+ overlay.title = 'Importing...';
+ overlay.userCanClose = false;
+ overlay.msgEl = document.createElement('div');
+ Polymer.dom(overlay).appendChild(overlay.msgEl);
+ overlay.msgEl.style.margin = '20px';
+ overlay.update = function(msg) {
+ Polymer.dom(this.msgEl).textContent = msg;
+ };
+ overlay.visible = true;
+
+ const promise =
+ tr.b.Task.RunWhenIdle(this.createImportTracesTask(overlay, traces));
+ promise.then(
+ function() { overlay.visible = false; },
+ function(err) { overlay.visible = false; }
+ );
+ return promise;
+ },
+
+ /**
+ * Creates a task that will import the provided traces into the model,
+ * updating the progressMeter as it goes. Parameters are as defined in
+ * importTraces.
+ */
+ createImportTracesTask(progressMeter, traces) {
+ const importStartTimeMs = tr.b.Timing.getCurrentTimeMs();
+
+ if (this.importing_) {
+ throw new Error('Already importing.');
+ }
+ this.importing_ = true;
+
+ // Just some simple setup. It is useful to have a no-op first
+ // task so that we can set up the lastTask = lastTask.after()
+ // pattern that follows.
+ const importTask = new tr.b.Task(function prepareImport() {
+ progressMeter.update('I will now import your traces for you...');
+ }, this);
+ let lastTask = importTask;
+
+ const importers = [];
+
+ function addImportStage(title, callback) {
+ lastTask = lastTask.after(() => progressMeter.update(title));
+ lastTask.updatesUi = true;
+ lastTask = lastTask.after(callback);
+ }
+
+ function addStageForEachImporter(title, callback) {
+ lastTask = lastTask.after((task) => {
+ importers.forEach((importer, index) => {
+ const uiSubTask = task.subTask(() => {
+ progressMeter.update(
+ `${title} ${index + 1} of ${importers.length}`);
+ });
+ uiSubTask.updatesUi = true;
+ task.subTask(() => callback(importer));
+ });
+ });
+ }
+
+ addImportStage('Creating importers...', () => {
+ // Copy the traces array, we may mutate it.
+ traces = traces.slice(0);
+ progressMeter.update('Creating importers...');
+ // Figure out which importers to use.
+ for (let i = 0; i < traces.length; ++i) {
+ importers.push(this.createImporter_(traces[i]));
+ }
+
+ // Some traces have other traces inside them. Before doing the full
+ // import, ask the importer if it has any subtraces, and if so, create
+ // importers for them, also.
+ for (let i = 0; i < importers.length; i++) {
+ const subtraces = importers[i].extractSubtraces();
+ for (let j = 0; j < subtraces.length; j++) {
+ try {
+ traces.push(subtraces[j]);
+ importers.push(this.createImporter_(subtraces[j]));
+ } catch (error) {
+ this.model_.importWarning({
+ type: error.name,
+ message: error.message,
+ showToUser: true,
+ });
+ continue;
+ }
+ }
+ }
+
+ if (traces.length && !this.hasEventDataDecoder_(importers)) {
+ throw new Error(
+ 'Could not find an importer for the provided eventData.');
+ }
+
+ // Sort them on priority. This ensures importing happens in a
+ // predictable order, e.g. ftrace_importer before
+ // trace_event_importer.
+ importers.sort(function(x, y) {
+ return x.importPriority - y.importPriority;
+ });
+ });
+
+ // We import clock sync markers before all other events. This is necessary
+ // because we need the clock sync markers in order to know by how much we
+ // need to shift the timestamps of other events.
+ addStageForEachImporter('Importing clock sync markers',
+ importer => importer.importClockSyncMarkers());
+
+ addStageForEachImporter('Importing', importer => importer.importEvents());
+
+ // Run the cusomizeModelCallback if needed.
+ if (this.importOptions_.customizeModelCallback) {
+ addImportStage('Customizing', () => {
+ this.importOptions_.customizeModelCallback(this.model_);
+ });
+ }
+
+ // Import sample data.
+ addStageForEachImporter('Importing sample data',
+ importer => importer.importSampleData());
+
+ // Autoclose open slices and create subSlices.
+ addImportStage('Autoclosing open slices...', () => {
+ this.model_.autoCloseOpenSlices();
+ this.model_.createSubSlices();
+ });
+
+ // Finalize import.
+ addStageForEachImporter('Finalizing import',
+ importer => importer.finalizeImport());
+
+ // Run preinit.
+ addImportStage('Initializing objects (step 1/2)...',
+ () => this.model_.preInitializeObjects());
+
+ // Prune empty containers.
+ if (this.importOptions_.pruneEmptyContainers) {
+ addImportStage('Pruning empty containers...',
+ () => this.model_.pruneEmptyContainers());
+ }
+
+ // Merge kernel and userland slices on each thread.
+ addImportStage('Merging kernel with userland...',
+ () => this.model_.mergeKernelWithUserland());
+
+ // Create auditors
+ let auditors = [];
+ addImportStage('Adding arbitrary data to model...', () => {
+ auditors = this.importOptions_.auditorConstructors.map(
+ auditorConstructor => new auditorConstructor(this.model_));
+ auditors.forEach((auditor) => {
+ auditor.runAnnotate();
+ auditor.installUserFriendlyCategoryDriverIfNeeded();
+ });
+ });
+
+ addImportStage('Computing final world bounds...', () => {
+ this.model_.computeWorldBounds(this.importOptions_.shiftWorldToZero);
+ });
+
+ addImportStage('Building flow event map...',
+ () => this.model_.buildFlowEventIntervalTree());
+
+ // Join refs.
+ addImportStage('Joining object refs...', () => this.model_.joinRefs());
+
+ // Delete any undeleted objects.
+ addImportStage('Cleaning up undeleted objects...',
+ () => this.model_.cleanupUndeletedObjects());
+
+ // Sort global and process memory dumps.
+ addImportStage('Sorting memory dumps...',
+ () => this.model_.sortMemoryDumps());
+
+ // Finalize memory dump graphs.
+ addImportStage('Finalizing memory dump graphs...',
+ () => this.model_.finalizeMemoryGraphs());
+
+ // Run initializers.
+ addImportStage('Initializing objects (step 2/2)...',
+ () => this.model_.initializeObjects());
+
+ // Build event indices mapping from an event id to all flow events.
+ addImportStage('Building event indices...',
+ () => this.model_.buildEventIndices());
+
+ // Build the UserModel.
+ addImportStage('Building UserModel...', () => {
+ const userModelBuilder = new tr.importer.UserModelBuilder(this.model_);
+ userModelBuilder.buildUserModel();
+ });
+
+ // Sort Expectations.
+ addImportStage('Sorting user expectations...',
+ () => this.model_.userModel.sortExpectations());
+
+ // Run audits.
+ addImportStage('Running auditors...', () => {
+ auditors.forEach(auditor => auditor.runAudit());
+ });
+
+ addImportStage('Updating alerts...', () => this.model_.sortAlerts());
+
+ addImportStage('Update bounds...', () => this.model_.updateBounds());
+
+ addImportStage('Looking for warnings...', () => {
+ // Log an import warning if the clock is low resolution.
+ if (!this.model_.isTimeHighResolution) {
+ this.model_.importWarning({
+ type: 'low_resolution_timer',
+ message: 'Trace time is low resolution, trace may be unusable.',
+ showToUser: true
+ });
+ }
+ });
+
+ // Cleanup.
+ lastTask.after(() => {
+ this.importing_ = false;
+ this.model_.stats.traceImportDurationMs =
+ tr.b.Timing.getCurrentTimeMs() - importStartTimeMs;
+ });
+ return importTask;
+ },
+
+ createImporter_(eventData) {
+ const importerConstructor = tr.importer.Importer.findImporterFor(
+ eventData);
+ if (!importerConstructor) {
+ throw new Error('Couldn\'t create an importer for the provided ' +
+ 'eventData.');
+ }
+ return new importerConstructor(this.model_, eventData);
+ },
+
+ hasEventDataDecoder_(importers) {
+ for (let i = 0; i < importers.length; ++i) {
+ if (!importers[i].isTraceDataContainer()) return true;
+ }
+
+ return false;
+ }
+ };
+
+ return {
+ ImportOptions,
+ Import,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/import_test.html b/chromium/third_party/catapult/tracing/tracing/importer/import_test.html
new file mode 100644
index 00000000000..77787402719
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/import_test.html
@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<!--
+Copyright 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/base64.html">
+<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/extras/importer/v8/v8_log_importer.html">
+<link rel="import" href="/tracing/extras/importer/zip_importer.html">
+<link rel="import" href="/tracing/model/model.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const Base64 = tr.b.Base64;
+
+ test('canImportEmpty', function() {
+ let m = tr.c.TestUtils.newModelWithEvents([]);
+ assert.isDefined(m.modelIndices);
+ m = new tr.Model('');
+ });
+
+ test('canImportSubtraces', function() {
+ const systraceLines = [
+ 'SurfaceFlinger-2 [001] ...1 1000.0: 0: B|1|taskA',
+ 'SurfaceFlinger-2 [001] ...1 2000.0: 0: E',
+ ' chrome-3 [001] ...1 2000.0: 0: trace_event_clock_sync: ' +
+ 'parent_ts=0'
+ ];
+ const traceEvents = [
+ {ts: 1000, pid: 1, tid: 3, ph: 'B', cat: 'c', name: 'taskB', args: {
+ my_object: {id_ref: '0x1000'}
+ }},
+ {ts: 2000, pid: 1, tid: 3, ph: 'E', cat: 'c', name: 'taskB', args: {}}
+ ];
+
+ const combined = JSON.stringify({
+ traceEvents,
+ systemTraceEvents: systraceLines.join('\n')
+ });
+
+ const m = tr.c.TestUtils.newModelWithEvents([combined]);
+ assert.strictEqual(Object.values(m.processes).length, 1);
+
+ const p1 = m.processes[1];
+ assert.isDefined(p1);
+
+ const t2 = p1.threads[2];
+ const t3 = p1.threads[3];
+ assert.isDefined(t2);
+ assert.isDefined(t3);
+
+ assert.strictEqual(1, 1, t2.sliceGroup.length);
+ assert.strictEqual(t2.sliceGroup.slices[0].title, 'taskA');
+
+ assert.strictEqual(t3.sliceGroup.length, 1);
+ assert.strictEqual(t3.sliceGroup.slices[0].title, 'taskB');
+ });
+
+ test('canImportCompressedSingleSubtrace', function() {
+ const compressedTrace = Base64.atob(
+ 'H4sIACKfFVUC/wsuLUpLTE51y8nMS08t0jVSUIg2MDCMV' +
+ 'dDT0zNUMDQwMNAzsFIAIqcaw5qSxOJsR65gfDqMEDpcATiC61ZbAAAA');
+ const m = tr.c.TestUtils.newModelWithEvents([compressedTrace]);
+ assert.strictEqual(1, Object.values(m.processes).length);
+
+ const p1 = m.processes[1];
+ assert.isDefined(p1);
+
+ const t2 = p1.threads[2];
+ assert.isDefined(t2);
+
+ assert.strictEqual(1, t2.sliceGroup.length, 1);
+ assert.strictEqual('taskA', t2.sliceGroup.slices[0].title);
+ });
+
+ test('canImportSubtracesRecursively', function() {
+ const systraceLines = [
+ 'SurfaceFlinger-2 [001] ...1 1000.0: 0: B|1|taskA',
+ 'SurfaceFlinger-2 [001] ...1 2000.0: 0: E',
+ ' chrome-3 [001] ...1 2000.0: 0: trace_event_clock_sync: ' +
+ 'parent_ts=0'
+ ];
+ const outerTraceEvents = [
+ {ts: 1000, pid: 1, tid: 3, ph: 'B', cat: 'c', name: 'taskB', args: {
+ my_object: {id_ref: '0x1000'}
+ }}
+ ];
+
+ const innerTraceEvents = [
+ {ts: 2000, pid: 1, tid: 3, ph: 'E', cat: 'c', name: 'taskB', args: {}}
+ ];
+
+ const innerTrace = JSON.stringify({
+ traceEvents: innerTraceEvents,
+ systemTraceEvents: systraceLines.join('\n')
+ });
+
+ const outerTrace = JSON.stringify({
+ traceEvents: outerTraceEvents,
+ systemTraceEvents: innerTrace
+ });
+
+ const m = tr.c.TestUtils.newModelWithEvents([outerTrace]);
+ assert.strictEqual(Object.values(m.processes).length, 1);
+
+ const p1 = m.processes[1];
+ assert.isDefined(p1);
+
+ const t2 = p1.threads[2];
+ const t3 = p1.threads[3];
+ assert.isDefined(t2);
+ assert.isDefined(t3);
+
+ assert.strictEqual(1, 1, t2.sliceGroup.length);
+ assert.strictEqual(t2.sliceGroup.slices[0].title, 'taskA');
+
+ assert.strictEqual(t3.sliceGroup.length, 1);
+ assert.strictEqual(t3.sliceGroup.slices[0].title, 'taskB');
+ });
+
+ test('withImportFailure', function() {
+ assert.throw(function() {
+ tr.c.TestUtils.newModelWithEvents([malformed]);
+ });
+ });
+
+ test('customizeCallback', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ shiftWorldToZero: false,
+ pruneContainers: false,
+ customizeModelCallback(m) {
+ const browserProcess = m.getOrCreateProcess(1);
+ const browserMain = browserProcess.getOrCreateThread(2);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 0);
+ browserMain.sliceGroup.beginSlice('cat', 'SubTask', 1);
+ browserMain.sliceGroup.endSlice(9);
+ browserMain.sliceGroup.endSlice(10);
+ browserMain.sliceGroup.beginSlice('cat', 'Task', 20);
+ browserMain.sliceGroup.endSlice(30);
+ }
+ });
+ const t2 = m.processes[1].threads[2];
+ assert.strictEqual(t2.sliceGroup.length, 3);
+ assert.strictEqual(t2.sliceGroup.topLevelSlices.length, 2);
+ });
+
+ test('sortsSamples', function() {
+ // The 184, 0 and 185 are the tick-times
+ // and irrespective of the order
+ // in which the lines appear in the trace,
+ // the samples should always be sorted by sampling time.
+ const m = tr.c.TestUtils.newModelWithEvents([
+ 'tick,0x9a,184,0,0x0,5',
+ 'tick,0x9b,0,0,0x0,5',
+ 'tick,0x9c,185,0,0x0,5']);
+ assert.strictEqual(m.samples[0].start, 0);
+ assert.strictEqual(m.samples[1].start, 0.184);
+ assert.strictEqual(m.samples[2].start, 0.185);
+ });
+
+ test('sortsGlobalMemoryDumps', function() {
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ pruneContainers: false,
+ customizeModelCallback(m) {
+ m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 1));
+ m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 5));
+ m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 3));
+ }
+ });
+ assert.strictEqual(m.globalMemoryDumps[0].start, 0);
+ assert.strictEqual(m.globalMemoryDumps[1].start, 2);
+ assert.strictEqual(m.globalMemoryDumps[2].start, 4);
+ });
+
+ test('finalizesProcessMemoryDumps', function() {
+ let p;
+ const m = tr.c.TestUtils.newModelWithEvents([], {
+ pruneContainers: false,
+ customizeModelCallback(m) {
+ p = m.getOrCreateProcess(1);
+
+ const g = new tr.model.GlobalMemoryDump(m, -1);
+ m.globalMemoryDumps.push(g);
+
+ const pmd1 = new tr.model.ProcessMemoryDump(g, p, 1);
+ p.memoryDumps.push(pmd1);
+
+ const pmd2 = new tr.model.ProcessMemoryDump(g, p, 5);
+ p.memoryDumps.push(pmd2);
+
+ const pmd3 = new tr.model.ProcessMemoryDump(g, p, 3);
+ p.memoryDumps.push(pmd3);
+ pmd3.vmRegions = [];
+ }
+ });
+
+ // Check the sort order.
+ assert.strictEqual(p.memoryDumps[0].start, 2);
+ assert.strictEqual(p.memoryDumps[1].start, 4);
+ assert.strictEqual(p.memoryDumps[2].start, 6);
+
+ // Check that the most recent VM regions are linked correctly.
+ assert.isUndefined(p.memoryDumps[0].mostRecentVmRegions);
+ assert.lengthOf(p.memoryDumps[1].mostRecentVmRegions, 0);
+ assert.strictEqual(
+ p.memoryDumps[1].mostRecentVmRegions,
+ p.memoryDumps[2].mostRecentVmRegions);
+ });
+
+ test('setsModelStatsTraceImportDurationMs', function() {
+ const traceEvents = [
+ {ts: 1000, pid: 1, tid: 3, ph: 'B', cat: 'c', name: 'taskB', args: {
+ my_object: {id_ref: '0x1000'}
+ }},
+ {ts: 2000, pid: 1, tid: 3, ph: 'E', cat: 'c', name: 'taskB', args: {}}
+ ];
+ const m = tr.c.TestUtils.newModelWithEvents(JSON.stringify({traceEvents}));
+
+ assert.isAbove(m.stats.traceImportDurationMs, 0);
+ });
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/importer.html b/chromium/third_party/catapult/tracing/tracing/importer/importer.html
new file mode 100644
index 00000000000..66accd2b840
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/importer.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 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">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for trace data importers.
+ */
+tr.exportTo('tr.importer', function() {
+ function Importer() { }
+
+ Importer.prototype = {
+ __proto__: Object.prototype,
+
+ get importerName() {
+ return 'Importer';
+ },
+
+ /**
+ * Called by the Model to check whether the importer type stores the actual
+ * trace data or just holds it as container for further extraction.
+ */
+ isTraceDataContainer() {
+ return false;
+ },
+
+ /**
+ * Called by the Model to extract one or more subtraces from the event data.
+ */
+ extractSubtraces() {
+ return [];
+ },
+
+ /**
+ * Called to import clock sync markers into the Model.
+ */
+ importClockSyncMarkers() {
+ },
+
+ /**
+ * Called to import events into the Model.
+ */
+ importEvents() {
+ },
+
+ /**
+ * Called to import sample data into the Model.
+ */
+ importSampleData() {
+ },
+
+ /**
+ * Called by the Model after all other importers have imported their
+ * events.
+ */
+ finalizeImport() {
+ }
+ };
+
+
+ const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE);
+ options.defaultMetadata = {};
+ options.mandatoryBaseClass = Importer;
+ tr.b.decorateExtensionRegistry(Importer, options);
+
+ Importer.findImporterFor = function(eventData) {
+ const typeInfo = Importer.findTypeInfoMatching(function(ti) {
+ return ti.constructor.canImport(eventData);
+ });
+ if (typeInfo) {
+ return typeInfo.constructor;
+ }
+ return undefined;
+ };
+
+ return {
+ Importer,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/proto_expectation.html b/chromium/third_party/catapult/tracing/tracing/importer/proto_expectation.html
new file mode 100644
index 00000000000..3deaac7d934
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/proto_expectation.html
@@ -0,0 +1,202 @@
+<!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/base.html">
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/core/auditor.html">
+<link rel="import" href="/tracing/model/event_info.html">
+<link rel="import" href="/tracing/model/user_model/animation_expectation.html">
+<link rel="import" href="/tracing/model/user_model/response_expectation.html">
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ // This is an intermediate data format between InputLatencyAsyncSlices and
+ // Responses and Animations.
+ function ProtoExpectation(type, initiatorType) {
+ this.type = type;
+ this.initiatorType = initiatorType;
+ this.start = Infinity;
+ this.end = -Infinity;
+ this.associatedEvents = new tr.model.EventSet();
+ this.isAnimationBegin = false;
+ }
+
+ ProtoExpectation.RESPONSE_TYPE = 'r';
+ ProtoExpectation.ANIMATION_TYPE = 'a';
+
+ // Explicitly ignore some input events to allow
+ // UserModelBuilder.checkAllInputEventsHandled() to determine which events
+ // were unintentionally ignored due to a bug.
+ ProtoExpectation.IGNORED_TYPE = 'ignored';
+
+ /**
+ * Combine initiator titles by selecting the initiator title first in a
+ * hard-coded hierarchy. Higher up in the hierarchy are more "specific"
+ * initiator titles (e.g. a scroll is higher than a touch, because a
+ * touch could mean many different things, of which a scroll is one)
+ */
+ const INITIATOR_HIERARCHY = [
+ tr.model.um.INITIATOR_TYPE.PINCH,
+ tr.model.um.INITIATOR_TYPE.FLING,
+ tr.model.um.INITIATOR_TYPE.MOUSE_WHEEL,
+ tr.model.um.INITIATOR_TYPE.SCROLL,
+ tr.model.um.INITIATOR_TYPE.VR,
+ tr.model.um.INITIATOR_TYPE.VIDEO,
+ tr.model.um.INITIATOR_TYPE.WEBGL,
+ tr.model.um.INITIATOR_TYPE.CSS,
+ tr.model.um.INITIATOR_TYPE.MOUSE,
+ tr.model.um.INITIATOR_TYPE.KEYBOARD,
+ tr.model.um.INITIATOR_TYPE.TAP,
+ tr.model.um.INITIATOR_TYPE.TOUCH
+ ];
+
+ function combineInitiatorTypes(title1, title2) {
+ for (const item of INITIATOR_HIERARCHY) {
+ if (title1 === item || title2 === item) return item;
+ }
+ throw new Error('Invalid titles in combineInitiatorTypes');
+ }
+
+ ProtoExpectation.prototype = {
+ get isValid() {
+ return this.end > this.start;
+ },
+
+ // Return true if any associatedEvent's typeName is in typeNames.
+ containsTypeNames(typeNames) {
+ return this.associatedEvents.some(
+ x => typeNames.indexOf(x.typeName) >= 0);
+ },
+
+ containsSliceTitle(title) {
+ return this.associatedEvents.some(x => title === x.title);
+ },
+
+ createInteractionRecord(model) {
+ if (this.type !== ProtoExpectation.IGNORED_TYPE && !this.isValid) {
+ model.importWarning({
+ type: 'ProtoExpectation',
+ message: 'Please file a bug with this trace. ' + this.debug(),
+ showToUser: true
+ });
+ return undefined;
+ }
+
+ const duration = this.end - this.start;
+
+ let ir = undefined;
+ switch (this.type) {
+ case ProtoExpectation.RESPONSE_TYPE:
+ ir = new tr.model.um.ResponseExpectation(
+ model, this.initiatorType, this.start, duration,
+ this.isAnimationBegin);
+ break;
+ case ProtoExpectation.ANIMATION_TYPE:
+ ir = new tr.model.um.AnimationExpectation(
+ model, this.initiatorType, this.start, duration);
+ break;
+ }
+ if (!ir) return undefined;
+
+ ir.sourceEvents.addEventSet(this.associatedEvents);
+
+ function pushAssociatedEvents(event) {
+ ir.associatedEvents.push(event);
+
+ // |event| is either an InputLatencyAsyncSlice (which collects all of
+ // its associated events transitively) or a CSS Animation (which doesn't
+ // have any associated events). So this does not need to recurse.
+ if (event.associatedEvents) {
+ ir.associatedEvents.addEventSet(event.associatedEvents);
+ }
+ }
+
+ this.associatedEvents.forEach(function(event) {
+ pushAssociatedEvents(event);
+
+ // Old-style InputLatencyAsyncSlices have subSlices.
+ if (event.subSlices) {
+ event.subSlices.forEach(pushAssociatedEvents);
+ }
+ });
+
+ return ir;
+ },
+
+ // Merge the other ProtoExpectation into this one.
+ // The types need not match: ignored ProtoExpectations might be merged
+ // into overlapping ProtoExpectations, and Touch-only Animations are merged
+ // into Tap Responses.
+ merge(other) {
+ this.initiatorType = combineInitiatorTypes(
+ this.initiatorType, other.initiatorType);
+
+ // Don't use pushEvent(), which would lose special start, end.
+ this.associatedEvents.addEventSet(other.associatedEvents);
+ this.start = Math.min(this.start, other.start);
+ this.end = Math.max(this.end, other.end);
+ if (other.isAnimationBegin) {
+ this.isAnimationBegin = true;
+ }
+ },
+
+ // Include |event| in this ProtoExpectation, expanding start/end to include
+ // it.
+ pushEvent(event) {
+ // Usually, this method will be called while iterating over a list of
+ // events sorted by start time, so this method won't usually change
+ // this.start. However, this will sometimes be called for
+ // ProtoExpectations created by previous handlers, in which case
+ // event.start could possibly be before this.start.
+ this.start = Math.min(this.start, event.start);
+ this.end = Math.max(this.end, event.end);
+ this.associatedEvents.push(event);
+ },
+
+ // Include |sample| in this ProtoExpectation, expanding start/end to
+ // include it.
+ pushSample(sample) {
+ this.start = Math.min(this.start, sample.timestamp);
+ this.end = Math.max(this.end, sample.timestamp);
+ this.associatedEvents.push(sample);
+ },
+
+ // Returns true if timestamp is contained in this ProtoExpectation.
+ containsTimestampInclusive(timestamp) {
+ return (this.start <= timestamp) && (timestamp <= this.end);
+ },
+
+ // Return true if the other event intersects this ProtoExpectation.
+ intersects(other) {
+ // http://stackoverflow.com/questions/325933
+ return (other.start < this.end) && (other.end > this.start);
+ },
+
+ isNear(event, threshold) {
+ return (this.end + threshold) > event.start;
+ },
+
+ // Return a string describing this ProtoExpectation for debugging.
+ debug() {
+ let debugString = this.type + '(';
+ debugString += parseInt(this.start) + ' ';
+ debugString += parseInt(this.end);
+ this.associatedEvents.forEach(function(event) {
+ debugString += ' ' + event.typeName;
+ });
+ return debugString + ')';
+ }
+ };
+
+ return {
+ ProtoExpectation,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/simple_line_reader.html b/chromium/third_party/catapult/tracing/tracing/importer/simple_line_reader.html
new file mode 100644
index 00000000000..ff94c9d74ba
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/simple_line_reader.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 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';
+
+tr.exportTo('tr.importer', function() {
+ class SimpleLineReader {
+ constructor(text) {
+ this.data_ = text instanceof tr.b.TraceStream ?
+ text : text.split(new RegExp('\r?\n'));
+ this.curLine_ = 0;
+ this.readLastLine_ = false;
+ this.savedLines_ = undefined;
+ }
+
+ * [Symbol.iterator]() {
+ let lastLine = undefined;
+ while (this.hasData_) {
+ if (this.readLastLine_) {
+ this.curLine_++;
+ this.readLastLine_ = false;
+ } else if (this.data_ instanceof tr.b.TraceStream) {
+ this.curLine_++;
+ const line = this.data_.readUntilDelimiter('\n');
+ if (line.endsWith('\r\n')) {
+ lastLine = line.slice(0, -2);
+ } else if (line.endsWith('\n')) {
+ lastLine = line.slice(0, -1);
+ } else {
+ lastLine = line;
+ }
+ } else {
+ this.curLine_++;
+ lastLine = this.data_[this.curLine_ - 1];
+ }
+ yield lastLine;
+ }
+ }
+
+ get curLineNumber() {
+ return this.curLine_;
+ }
+
+ get hasData_() {
+ if (this.data_ instanceof tr.b.TraceStream) return this.data_.hasData;
+ return this.curLine_ < this.data_.length;
+ }
+
+ advanceToLineMatching(regex) {
+ for (const line of this) {
+ if (this.savedLines_ !== undefined) this.savedLines_.push(line);
+ if (regex.test(line)) {
+ this.goBack_();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ goBack_() {
+ if (this.readLastLine_) {
+ throw new Error('There should be at least one nextLine call between ' +
+ 'any two goBack calls.');
+ }
+ if (this.curLine_ === 0) {
+ throw new Error('There should be at least one nextLine call before ' +
+ 'the first goBack call.');
+ }
+ this.readLastLine_ = true;
+ this.curLine_--;
+ }
+
+ beginSavingLines() {
+ this.savedLines_ = [];
+ }
+
+ endSavingLinesAndGetResult() {
+ const tmp = this.savedLines_;
+ this.savedLines_ = undefined;
+ return tmp;
+ }
+ }
+
+ return {
+ SimpleLineReader,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/user_expectation_verifier.html b/chromium/third_party/catapult/tracing/tracing/importer/user_expectation_verifier.html
new file mode 100644
index 00000000000..ccaef12de51
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/user_expectation_verifier.html
@@ -0,0 +1,111 @@
+<!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_test_utils.html">
+<link rel="import" href="/tracing/importer/user_model_builder.html">
+
+<script>
+'use strict';
+tr.exportTo('tr.importer', function() {
+ function compareEvents(x, y) {
+ if (x.start !== y.start) return x.start - y.start;
+ return x.guid - y.guid;
+ }
+
+ class UserExpectationVerifier {
+ constructor() {
+ this.customizeModelCallback_ = undefined;
+ this.expectedUEs_ = undefined;
+ this.expectedSegments_ = undefined;
+ }
+
+ set customizeModelCallback(cmc) {
+ this.customizeModelCallback_ = cmc;
+ }
+
+ /**
+ * @param {!Array.<!Object>} ues must be sorted by start time.
+ */
+ set expectedUEs(ues) {
+ this.expectedUEs_ = ues;
+ }
+
+ get expectedUEs() {
+ return this.expectedUEs_;
+ }
+
+ /**
+ * @param {!Array.<!Object>} segments must be sorted by start time.
+ */
+ set expectedSegments(segments) {
+ this.expectedSegments_ = segments;
+ }
+
+ verify() {
+ const model = tr.e.chrome.ChromeTestUtils.newChromeModel(
+ this.customizeModelCallback_);
+
+ let failure = undefined;
+ try {
+ this.verifyExpectations_([...model.userModel.expectations]);
+ this.verifySegments_(model.userModel.segments);
+ } catch (e) {
+ failure = e;
+ }
+
+ if (failure) throw failure;
+ }
+
+ verifyExpectations_(expectations) {
+ assert.lengthOf(expectations, this.expectedUEs.length);
+ for (let i = 0; i < this.expectedUEs.length; ++i) {
+ this.verifyExpectation_(
+ this.expectedUEs[i], expectations[i], `UEs[${i}]`);
+ }
+ }
+
+ verifySegments_(segments) {
+ assert.lengthOf(segments, this.expectedSegments_.length);
+ for (let i = 0; i < this.expectedSegments_.length; ++i) {
+ this.verifySegment_(
+ this.expectedSegments_[i], segments[i], `segments[${i}].`);
+ }
+ }
+
+ verifyExpectation_(expected, actual, at) {
+ assert.strictEqual(expected.title, actual.title, at + 'title');
+ if (expected.name !== undefined) {
+ assert.strictEqual(expected.name, actual.name, at + 'name');
+ }
+ assert.strictEqual(expected.start, actual.start, at + 'start');
+ assert.strictEqual(expected.end, actual.end, at + 'end');
+ assert.strictEqual(expected.eventCount,
+ actual.associatedEvents.length, at + 'eventCount');
+ if (actual instanceof tr.model.um.ResponseExpectation) {
+ assert.strictEqual(expected.isAnimationBegin || false,
+ actual.isAnimationBegin, at + 'isAnimationBegin');
+ }
+ }
+
+ verifySegment_(expected, actual, at) {
+ assert.strictEqual(expected.start, actual.start, at + 'start');
+ assert.strictEqual(expected.end, actual.end, at + 'end');
+ assert.lengthOf(actual.expectations, expected.expectations.length,
+ at + 'expectations.length');
+ for (let i = 0; i < expected.expectations.length; ++i) {
+ this.verifyExpectation_(
+ expected.expectations[i], actual.expectations[i],
+ at + `expectations[${i}].`);
+ }
+ }
+ }
+
+ return {
+ UserExpectationVerifier,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder.html b/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder.html
new file mode 100644
index 00000000000..c5cf14137f0
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder.html
@@ -0,0 +1,277 @@
+<!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/base.html">
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/core/auditor.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/importer/find_input_expectations.html">
+<link rel="import" href="/tracing/importer/find_load_expectations.html">
+<link rel="import" href="/tracing/importer/find_startup_expectations.html">
+<link rel="import" href="/tracing/model/event_info.html">
+<link rel="import" href="/tracing/model/ir_coverage.html">
+<link rel="import" href="/tracing/model/user_model/idle_expectation.html">
+<link rel="import" href="/tracing/model/user_model/segment.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ const INSIGNIFICANT_MS = 1;
+
+ class UserModelBuilder {
+ constructor(model) {
+ this.model = model;
+ this.modelHelper = model.getOrCreateHelper(
+ tr.model.helpers.ChromeModelHelper);
+ }
+
+ static supportsModelHelper(modelHelper) {
+ return modelHelper.browserHelper !== undefined;
+ }
+
+ /**
+ * This is called during the trace model import process.
+ */
+ buildUserModel() {
+ if (!this.modelHelper || !this.modelHelper.browserHelper) return;
+
+ try {
+ for (const ue of this.findUserExpectations()) {
+ // This is an EventSet, not an Array, so it can't use push(...).
+ // https://github.com/catapult-project/catapult/issues/3157
+ this.model.userModel.expectations.push(ue);
+ }
+ this.model.userModel.segments.push(...this.findSegments());
+ // There are not currently any known cases when this could throw,
+ // but there have been in the past and there could be again, so
+ // keep handling exceptions here to be friendly to the future.
+ } catch (error) {
+ this.model.importWarning({
+ type: 'UserModelBuilder',
+ message: error,
+ showToUser: true
+ });
+ }
+ }
+
+ /**
+ * Returns an array of Segments covering the trace Model. A Segment
+ * represents a range of time during which the set of active
+ * UserExpectations does not change. Because of this, segments are
+ * guaranteed to not overlap, whereas UserExpectations can.
+ *
+ * @return {!Array.<!tr.model.um.Segment>}
+ */
+ findSegments() {
+ let timestamps = new Set();
+ for (const expectation of this.model.userModel.expectations) {
+ timestamps.add(expectation.start);
+ timestamps.add(expectation.end);
+ }
+ timestamps = [...timestamps];
+ timestamps.sort((x, y) => x - y);
+ const segments = [];
+ for (let i = 0; i < timestamps.length - 1; ++i) {
+ const segment = new tr.model.um.Segment(
+ timestamps[i], timestamps[i + 1] - timestamps[i]);
+ segments.push(segment);
+ const segmentRange = tr.b.math.Range.fromExplicitRange(
+ segment.start, segment.end);
+ for (const expectation of this.model.userModel.expectations) {
+ const expectationRange = tr.b.math.Range.fromExplicitRange(
+ expectation.start, expectation.end);
+ if (segmentRange.intersectsRangeExclusive(expectationRange)) {
+ segment.expectations.push(expectation);
+ }
+ }
+ }
+ return segments;
+ }
+
+ /**
+ * Returns an array of UserExpectations covering the trace Model. A
+ * UserExpectation represents a range of time during which the user is
+ * expecting something from Chrome, either to startup or load a page or
+ * respond to input or play an animation, or just sit there idle. Users can
+ * have multiple expectations at any given time, so UserExpectations can
+ * overlap.
+ *
+ * @return {!Array.<!tr.model.um.UserExpectation>}
+ */
+ findUserExpectations() {
+ const expectations = [];
+ expectations.push.apply(expectations, tr.importer.findStartupExpectations(
+ this.modelHelper));
+ expectations.push.apply(expectations, tr.importer.findLoadExpectations(
+ this.modelHelper));
+ expectations.push.apply(expectations, tr.importer.findInputExpectations(
+ this.modelHelper));
+ // findIdleExpectations must be called last!
+ expectations.push.apply(
+ expectations, this.findIdleExpectations(expectations));
+ this.collectUnassociatedEvents_(expectations);
+ return expectations;
+ }
+
+ // Find all unassociated top-level ThreadSlices. If they start during an
+ // Idle or Load UE, then add their entire hierarchy to that UE.
+ collectUnassociatedEvents_(expectations) {
+ const vacuumUEs = [];
+ for (const expectation of expectations) {
+ if (expectation instanceof tr.model.um.IdleExpectation ||
+ expectation instanceof tr.model.um.LoadExpectation ||
+ expectation instanceof tr.model.um.StartupExpectation) {
+ vacuumUEs.push(expectation);
+ }
+ }
+ if (vacuumUEs.length === 0) return;
+
+ const allAssociatedEvents = tr.model.getAssociatedEvents(expectations);
+ const unassociatedEvents = tr.model.getUnassociatedEvents(
+ this.model, allAssociatedEvents);
+
+ for (const event of unassociatedEvents) {
+ if (!(event instanceof tr.model.ThreadSlice)) continue;
+
+ if (!event.isTopLevel) continue;
+
+ for (let index = 0; index < vacuumUEs.length; ++index) {
+ const expectation = vacuumUEs[index];
+
+ if ((event.start >= expectation.start) &&
+ (event.start < expectation.end)) {
+ expectation.associatedEvents.addEventSet(event.entireHierarchy);
+ break;
+ }
+ }
+ }
+ }
+
+ // Fill in the empty space between UEs with IdleUEs.
+ findIdleExpectations(otherUEs) {
+ if (this.model.bounds.isEmpty) return;
+
+ const emptyRanges = tr.b.math.findEmptyRangesBetweenRanges(
+ tr.b.math.convertEventsToRanges(otherUEs),
+ this.model.bounds);
+ const expectations = [];
+ const model = this.model;
+ for (const range of emptyRanges) {
+ // Ignore insignificantly tiny idle ranges.
+ if (range.max < (range.min + INSIGNIFICANT_MS)) continue;
+
+ expectations.push(new tr.model.um.IdleExpectation(
+ model, range.min, range.max - range.min));
+ }
+ return expectations;
+ }
+ }
+
+ function createCustomizeModelLinesFromModel(model) {
+ const modelLines = [];
+ modelLines.push(' audits.addEvent(model.browserMain,');
+ modelLines.push(' {title: \'model start\', start: 0, end: 1});');
+
+ const typeNames = {};
+ for (const typeName in tr.e.cc.INPUT_EVENT_TYPE_NAMES) {
+ typeNames[tr.e.cc.INPUT_EVENT_TYPE_NAMES[typeName]] = typeName;
+ }
+
+ let modelEvents = new tr.model.EventSet();
+ for (const ue of model.userModel.expectations) {
+ modelEvents.addEventSet(ue.sourceEvents);
+ }
+ modelEvents = modelEvents.toArray();
+ modelEvents.sort(tr.importer.compareEvents);
+
+ for (const event of modelEvents) {
+ const startAndEnd = 'start: ' + parseInt(event.start) + ', ' +
+ 'end: ' + parseInt(event.end) + '});';
+ if (event instanceof tr.e.cc.InputLatencyAsyncSlice) {
+ modelLines.push(' audits.addInputEvent(model, INPUT_TYPE.' +
+ typeNames[event.typeName] + ',');
+ } else if (event.title === 'RenderFrameImpl::didCommitProvisionalLoad') {
+ modelLines.push(' audits.addCommitLoadEvent(model,');
+ } else if (event.title ===
+ 'InputHandlerProxy::HandleGestureFling::started') {
+ modelLines.push(' audits.addFlingAnimationEvent(model,');
+ } else if (event.title === tr.model.helpers.IMPL_RENDERING_STATS) {
+ modelLines.push(' audits.addFrameEvent(model,');
+ } else if (event.title === tr.importer.CSS_ANIMATION_TITLE) {
+ modelLines.push(' audits.addEvent(model.rendererMain, {');
+ modelLines.push(' title: \'Animation\', ' + startAndEnd);
+ return;
+ } else {
+ throw new Error(
+ 'You must extend createCustomizeModelLinesFromModel()' +
+ 'to support this event:\n' + event.title + '\n');
+ }
+ modelLines.push(' {' + startAndEnd);
+ }
+
+ modelLines.push(' audits.addEvent(model.browserMain,');
+ modelLines.push(' {' +
+ 'title: \'model end\', ' +
+ 'start: ' + (parseInt(model.bounds.max) - 1) + ', ' +
+ 'end: ' + parseInt(model.bounds.max) + '});');
+ return modelLines;
+ }
+
+ function createExpectedUELinesFromModel(model) {
+ const expectedLines = [];
+ const ueCount = model.userModel.expectations.length;
+ for (let index = 0; index < ueCount; ++index) {
+ const expectation = model.userModel.expectations[index];
+ let ueString = ' {';
+ ueString += 'title: \'' + expectation.title + '\', ';
+ ueString += 'start: ' + parseInt(expectation.start) + ', ';
+ ueString += 'end: ' + parseInt(expectation.end) + ', ';
+ ueString += 'eventCount: ' + expectation.sourceEvents.length;
+ ueString += '}';
+ if (index < (ueCount - 1)) ueString += ',';
+ expectedLines.push(ueString);
+ }
+ return expectedLines;
+ }
+
+ function createUEFinderTestCaseStringFromModel(model) {
+ const filename = window.location.hash.substr(1);
+ let testName = filename.substr(filename.lastIndexOf('/') + 1);
+ testName = testName.substr(0, testName.indexOf('.'));
+
+ // createCustomizeModelLinesFromModel() throws an error if there's an
+ // unsupported event.
+ try {
+ const testLines = [];
+ testLines.push(' /*');
+ testLines.push(' This test was generated from');
+ testLines.push(' ' + filename + '');
+ testLines.push(' */');
+ testLines.push(' test(\'' + testName + '\', function() {');
+ testLines.push(' const verifier = new UserExpectationVerifier();');
+ testLines.push(' verifier.customizeModelCallback = function(model) {');
+ testLines.push.apply(testLines,
+ createCustomizeModelLinesFromModel(model));
+ testLines.push(' };');
+ testLines.push(' verifier.expectedUEs = [');
+ testLines.push.apply(testLines, createExpectedUELinesFromModel(model));
+ testLines.push(' ];');
+ testLines.push(' verifier.verify();');
+ testLines.push(' });');
+ return testLines.join('\n');
+ } catch (error) {
+ return error;
+ }
+ }
+
+ return {
+ UserModelBuilder,
+ createUEFinderTestCaseStringFromModel,
+ };
+});
+</script>
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder_test.html b/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder_test.html
new file mode 100644
index 00000000000..4379316e8ae
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/user_model_builder_test.html
@@ -0,0 +1,1649 @@
+<!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/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html">
+<link rel="import" href="/tracing/extras/chrome/event_finder_utils.html">
+<link rel="import" href="/tracing/importer/user_expectation_verifier.html">
+
+<script>
+'use strict';
+
+tr.b.unittest.testSuite(function() {
+ const INPUT_TYPE = tr.e.cc.INPUT_EVENT_TYPE_NAMES;
+ const ChromeTestUtils = tr.e.chrome.ChromeTestUtils;
+ const UserExpectationVerifier = tr.importer.UserExpectationVerifier;
+
+ function addFrameEventForInput(model, event) {
+ const frame = ChromeTestUtils.addFrameEvent(model,
+ {start: event.start, end: event.end, isTopLevel: true});
+ model.flowEvents.push(tr.c.TestUtils.newFlowEventEx({
+ id: event.id,
+ start: event.start,
+ end: event.end,
+ startSlice: frame,
+ endSlice: frame
+ }));
+ }
+
+ test('empty', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ };
+ verifier.expectedUEs = [];
+ verifier.expectedSegments = [];
+ verifier.verify();
+ });
+
+ test('vrExpectations', function() {
+ const verifier = new UserExpectationVerifier();
+
+ verifier.customizeModelCallback = function(model) {
+ model.gpuProcess = model.getOrCreateProcess(3);
+ model.gpuMain = model.gpuProcess.getOrCreateThread(6);
+ model.gpuMain.name = 'CrGpuMain';
+
+ const series = new tr.model.CounterSeries('gpu.WebVR FPS');
+ series.addCounterSample(0, 1);
+ series.addCounterSample(990, 2);
+ series.addCounterSample(1005, 3);
+ series.addCounterSample(1500, 4);
+ model.gpuProcess.getOrCreateCounter('gpu',
+ 'WebVR FPS').addSeries(series);
+
+ model.gpuMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {title: 'VrShellGl::DrawFrame', start: 5, end: 10,
+ type: tr.model.ThreadSlice}));
+ model.gpuMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {title: 'VrShellGl::DrawFrame', start: 995, end: 1000,
+ type: tr.model.ThreadSlice}));
+ model.gpuMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx(
+ {title: 'VrShellGl::DrawFrame', start: 1010, end: 1050,
+ type: tr.model.ThreadSlice}));
+ };
+
+ verifier.expectedUEs = [
+ {title: 'VR Response', start: 0, end: 1000, eventCount: 4},
+ {title: 'VR Animation', start: 1000, end: 1500, eventCount: 4},
+ ];
+
+ verifier.expectedSegments = [
+ {start: 0, end: 1000, expectations: [verifier.expectedUEs[0]]},
+ {start: 1000, end: 1500, expectations: [verifier.expectedUEs[1]]},
+ ];
+
+ verifier.verify();
+ });
+
+ test('videoExpectations_gapInMiddle', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 0, end: 100, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 200, end: 300, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 210, end: 220});
+ };
+ verifier.expectedUEs = [
+ {title: 'Video Animation', start: 0, end: 100, eventCount: 2},
+ {title: 'Idle', start: 100, end: 200, eventCount: 0},
+ {title: 'Video Animation', start: 200, end: 300, eventCount: 2},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 200, expectations: [verifier.expectedUEs[1]]},
+ {start: 200, end: 300, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('videoExpectations_overlapping', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 0, end: 200, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 100, end: 300, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 210, end: 220});
+ };
+ verifier.expectedUEs = [
+ {title: 'Video Animation', start: 0, end: 300, eventCount: 4},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 300, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('videoExpectations_oneInTheOther', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 0, end: 300, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 100, end: 200, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 110, end: 120});
+ };
+ verifier.expectedUEs = [
+ {title: 'Video Animation', start: 0, end: 300, eventCount: 4},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 300, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('videoExpectations_dontMergeWithOtherAnimations', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = model => {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'VideoPlayback', start: 0, end: 100, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20}),
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 90, end: 190, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 110, end: 120});
+ };
+ verifier.expectedUEs = [
+ {title: 'Video Animation', start: 0, end: 100, eventCount: 2},
+ {title: 'CSS Animation', start: 90, end: 190, eventCount: 2},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 90, expectations: [verifier.expectedUEs[0]]},
+ {
+ start: 90,
+ end: 100,
+ expectations: [verifier.expectedUEs[0], verifier.expectedUEs[1]]
+ },
+ {start: 100, end: 190, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('slowMouseMoveResponses', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_DOWN, {start: 0, end: 10});
+ let mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 10, end: 20, id: '0x100'});
+ addFrameEventForInput(model, mouseMove);
+
+ mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 70, end: 80, id: '0x101'});
+ addFrameEventForInput(model, mouseMove);
+
+ mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 130, end: 140, id: '0x102'});
+ addFrameEventForInput(model, mouseMove);
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 10, eventCount: 0},
+ {title: 'Mouse Response', start: 10, end: 20, eventCount: 4},
+ {title: 'Idle', start: 20, end: 70, eventCount: 0},
+ {title: 'Mouse Response', start: 70, end: 80, eventCount: 3},
+ {title: 'Idle', start: 80, end: 130, eventCount: 0},
+ {title: 'Mouse Response', start: 130, end: 140, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 10, expectations: [verifier.expectedUEs[0]]},
+ {start: 10, end: 20, expectations: [verifier.expectedUEs[1]]},
+ {start: 20, end: 70, expectations: [verifier.expectedUEs[2]]},
+ {start: 70, end: 80, expectations: [verifier.expectedUEs[3]]},
+ {start: 80, end: 130, expectations: [verifier.expectedUEs[4]]},
+ {start: 130, end: 140, expectations: [verifier.expectedUEs[5]]}
+ ];
+ verifier.verify();
+ });
+
+ test('mouseEventResponses', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ const mouseDown = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_DOWN, {start: 0, end: 50, id: '0x100'});
+ addFrameEventForInput(model, mouseDown);
+
+ const mouseUp = ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_UP,
+ {start: 50, end: 100, id: '0x101'});
+ addFrameEventForInput(model, mouseUp);
+
+ const mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 200, end: 250, id: '0x102'});
+ addFrameEventForInput(model, mouseMove);
+ };
+ verifier.expectedUEs = [
+ {title: 'Mouse Response', start: 0, end: 50, eventCount: 3},
+ {title: 'Mouse Response', start: 50, end: 100, eventCount: 3},
+ {title: 'Idle', start: 100, end: 200, eventCount: 0},
+ {title: 'Mouse Response', start: 200, end: 250, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 50, expectations: [verifier.expectedUEs[0]]},
+ {start: 50, end: 100, expectations: [verifier.expectedUEs[1]]},
+ {start: 100, end: 200, expectations: [verifier.expectedUEs[2]]},
+ {start: 200, end: 250, expectations: [verifier.expectedUEs[3]]}
+ ];
+ verifier.verify();
+ });
+
+ test('mouseEventsIgnored', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_MOVE,
+ {start: 0, end: 50});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_DOWN,
+ {start: 50, end: 100});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 100, eventCount: 0}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]}
+ ];
+ verifier.verify();
+ });
+
+ test('unassociatedEvents', function() {
+ // Unassociated ThreadSlices that start during an Idle should be associated
+ // with it. Expect the IdleExpectation to have 2 associated events: both of
+ // the ThreadSlices in the model.
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ const start = tr.c.TestUtils.newSliceEx(
+ {title: 'model start', start: 0, end: 1, type: tr.model.ThreadSlice});
+ start.isTopLevel = true;
+ model.browserMain.sliceGroup.pushSlice(start);
+
+ const end = tr.c.TestUtils.newSliceEx(
+ {title: 'model end', start: 9, end: 10, type: tr.model.ThreadSlice});
+ end.isTopLevel = true;
+ model.browserMain.sliceGroup.pushSlice(end);
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 10, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 10, expectations: [verifier.expectedUEs[0]]}
+ ];
+ verifier.verify();
+ });
+
+ test('stillLoading', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader',
+ 25, {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 11,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ ChromeTestUtils.addFirstContentfulPaintEvent(model, {start: 20});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 30,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 40,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ ChromeTestUtils.addFrameEvent(model, {start: 100, end: 130});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 11, eventCount: 1},
+ {title: 'Successful Load', start: 11, end: 130, eventCount: 4}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 11, expectations: [verifier.expectedUEs[0]]},
+ {start: 11, end: 130, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('overlappingIdleAndLoadCollectUnassociatedEvents', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ model.rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader',
+ 15, {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+ ChromeTestUtils.addFrameEvent(model, {start: 20, end: 40});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 20,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start: 20,
+ duration: 5.0,
+ }));
+ ChromeTestUtils.addFirstContentfulPaintEvent(model, {start: 20});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 30,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 40,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ ChromeTestUtils.addFrameEvent(model, {start: 5000, end: 5050});
+ // 3 Idle events.
+ ChromeTestUtils.addRenderingEvent(model, {start: 5, end: 15});
+ ChromeTestUtils.addRenderingEvent(model, {start: 11, end: 15});
+ ChromeTestUtils.addRenderingEvent(model, {start: 13, end: 15});
+ // 1 Idle event.
+ ChromeTestUtils.addRenderingEvent(model, {start: 5045, end: 5046});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 20, eventCount: 4},
+ {title: 'Successful Load', start: 20, end: 40, eventCount: 4},
+ {title: 'Idle', start: 40, end: 5050, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 20, expectations: [verifier.expectedUEs[0]]},
+ {start: 20, end: 40, expectations: [verifier.expectedUEs[1]]},
+ {start: 40, end: 5050, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('flingFlingFling', function() {
+ // This trace gave me so many different kinds of trouble that I'm just going
+ // to copy it straight in here, without trying to clarify it at all.
+ // measurmt-traces/mobile/cnet_fling_up_fling_down_motox_2013.json
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 919, end: 998});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_CANCEL,
+ {start: 919, end: 1001});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 919, end: 1001});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_CANCEL,
+ {start: 974, end: 1020});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 974, end: 1020});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 974, end: 1040});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 1039, end: 1040, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 974, end: 1054});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 990, end: 1021});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 990, end: 1052});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 1051, end: 1052, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1006, end: 1021});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1022, end: 1036});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1022, end: 1052});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1038, end: 1049});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1038, end: 1068});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 1067, end: 1068, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 1046, end: 1050});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 1046, end: 1077});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 1432, end: 2238});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_CANCEL,
+ {start: 1432, end: 2241});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1516, end: 2605});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 1532, end: 2274});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1532, end: 2294});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 2293, end: 2294, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1549, end: 2310});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 2309, end: 2310, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 1627, end: 2275});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 1627, end: 2310});
+ ChromeTestUtils.addFrameEvent(model, {start: 2990, end: 3000});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 919, eventCount: 1},
+ {title: 'Scroll Response', start: 919, end: 1054,
+ eventCount: 6, isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 1054, end: 1068,
+ eventCount: 9},
+ {title: 'Fling Animation', start: 1054, end: 1432,
+ eventCount: 3},
+ {title: 'Scroll Response', start: 1432, end: 2605,
+ eventCount: 5, isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 1549, end: 2310,
+ eventCount: 3},
+ {title: 'Fling Animation', start: 2605, end: 3000,
+ eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 919, expectations: [verifier.expectedUEs[0]]},
+ {start: 919, end: 1054, expectations: [verifier.expectedUEs[1]]},
+ {start: 1054, end: 1068, expectations: [
+ verifier.expectedUEs[2],
+ verifier.expectedUEs[3],
+ ]},
+ {start: 1068, end: 1432, expectations: [verifier.expectedUEs[3]]},
+ {start: 1432, end: 1549, expectations: [verifier.expectedUEs[4]]},
+ {start: 1549, end: 2310, expectations: [
+ verifier.expectedUEs[4],
+ verifier.expectedUEs[5],
+ ]},
+ {start: 2310, end: 2605, expectations: [verifier.expectedUEs[4]]},
+ {start: 2605, end: 3000, expectations: [verifier.expectedUEs[6]]},
+ ];
+ verifier.verify();
+ });
+
+ test('keyboardEvents', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.KEY_DOWN_RAW,
+ {start: 0, end: 45});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.CHAR,
+ {start: 10, end: 50});
+ };
+ verifier.expectedUEs = [
+ {title: 'Keyboard Response', start: 0, end: 50, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 50, expectations: [verifier.expectedUEs[0]]}
+ ];
+ verifier.verify();
+ });
+
+ test('mouseResponses', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.CLICK,
+ {start: 0, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.CONTEXT_MENU,
+ {start: 200, end: 300});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 400, end: 500});
+ };
+ verifier.expectedUEs = [
+ {title: 'Mouse Response', start: 0, end: 100, eventCount: 1},
+ {title: 'Idle', start: 100, end: 200, eventCount: 0},
+ {title: 'Mouse Response', start: 200, end: 300, eventCount: 1},
+ {title: 'Idle', start: 300, end: 400, eventCount: 0},
+ {title: 'MouseWheel Response', start: 400, end: 500,
+ eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 200, expectations: [verifier.expectedUEs[1]]},
+ {start: 200, end: 300, expectations: [verifier.expectedUEs[2]]},
+ {start: 300, end: 400, expectations: [verifier.expectedUEs[3]]},
+ {start: 400, end: 500, expectations: [verifier.expectedUEs[4]]},
+ ];
+ verifier.verify();
+ });
+
+ test('mouseWheelAnimation', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 0, end: 20});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 19, end: 20, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 16, end: 36});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 35, end: 36, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 55, end: 75});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 74, end: 75, isTopLevel: true});
+
+ // This threshold uses both events' start times, not end...start.
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 100, end: 150});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 149, end: 150, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 141, end: 191});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 190, end: 191, isTopLevel: true});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_WHEEL,
+ {start: 182, end: 200});
+ ChromeTestUtils.addFrameEvent(model,
+ {start: 199, end: 200, isTopLevel: true});
+ };
+ verifier.expectedUEs = [
+ {title: 'MouseWheel Response', start: 0, end: 20, eventCount: 1},
+ {title: 'MouseWheel Animation', start: 20, end: 75,
+ eventCount: 4},
+ {title: 'Idle', start: 75, end: 100, eventCount: 0},
+ {title: 'MouseWheel Response', start: 100, end: 150,
+ eventCount: 1},
+ {title: 'MouseWheel Response', start: 141, end: 191,
+ eventCount: 1},
+ {title: 'MouseWheel Response', start: 182, end: 200,
+ eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 20, expectations: [verifier.expectedUEs[0]]},
+ {start: 20, end: 75, expectations: [verifier.expectedUEs[1]]},
+ {start: 75, end: 100, expectations: [verifier.expectedUEs[2]]},
+ {start: 100, end: 141, expectations: [verifier.expectedUEs[3]]},
+ {start: 141, end: 150, expectations: [
+ verifier.expectedUEs[3],
+ verifier.expectedUEs[4],
+ ]},
+ {start: 150, end: 182, expectations: [verifier.expectedUEs[4]]},
+ {start: 182, end: 191, expectations: [
+ verifier.expectedUEs[4],
+ verifier.expectedUEs[5],
+ ]},
+ {start: 191, end: 200, expectations: [verifier.expectedUEs[5]]},
+ ];
+ verifier.verify();
+ });
+
+ test('mouseDownUpResponse', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_DOWN,
+ {start: 0, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_UP,
+ {start: 200, end: 210});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 200, eventCount: 0},
+ {title: 'Mouse Response', start: 200, end: 210, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 200, expectations: [verifier.expectedUEs[0]]},
+ {start: 200, end: 210, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('ignoreLoneMouseMoves', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.MOUSE_MOVE,
+ {start: 0, end: 100});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 100, eventCount: 0}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]}
+ ];
+ verifier.verify();
+ });
+
+ test('mouseDrags', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_DOWN, {start: 0, end: 100});
+ let mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 200, end: 215});
+ addFrameEventForInput(model, mouseMove);
+ mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 210, end: 220});
+ addFrameEventForInput(model, mouseMove);
+ mouseMove = ChromeTestUtils.addInputEvent(
+ model, INPUT_TYPE.MOUSE_MOVE, {start: 221, end: 240});
+ addFrameEventForInput(model, mouseMove);
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 200, eventCount: 0},
+ {title: 'Mouse Response', start: 200, end: 215, eventCount: 4},
+ {title: 'Mouse Animation', start: 215, end: 240, eventCount: 6}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 200, expectations: [verifier.expectedUEs[0]]},
+ {start: 200, end: 215, expectations: [verifier.expectedUEs[1]]},
+ {start: 215, end: 240, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('twoScrollsNoFling', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 0, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 20, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 40, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 60, end: 150});
+ ChromeTestUtils.addFrameEvent(model, {start: 149, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 70, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_END,
+ {start: 80, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 300, end: 400});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 320, end: 400});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 330, end: 450});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 340, end: 450});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 350, end: 500});
+ ChromeTestUtils.addFrameEvent(model, {start: 499, end: 500});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_END,
+ {start: 360, end: 500});
+ };
+ verifier.expectedUEs = [
+ {title: 'Scroll Response', start: 0, end: 100, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 100, end: 150, eventCount: 5},
+ {title: 'Idle', start: 150, end: 300, eventCount: 0},
+ {title: 'Scroll Response', start: 300, end: 400, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 400, end: 500, eventCount: 5}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 150, expectations: [verifier.expectedUEs[1]]},
+ {start: 150, end: 300, expectations: [verifier.expectedUEs[2]]},
+ {start: 300, end: 400, expectations: [verifier.expectedUEs[3]]},
+ {start: 400, end: 500, expectations: [verifier.expectedUEs[4]]},
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_oneAnimation', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'DrawingBuffer::prepareMailbox', start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 18, end: 19});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 20, end: 22});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 38, end: 39});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 40, end: 42});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 58, end: 59});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 60, end: 62});
+ };
+ verifier.expectedUEs = [
+ {title: 'WebGL Animation', start: 0, end: 62, eventCount: 4},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 62, expectations: [verifier.expectedUEs[0]]}
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_twoAnimations', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'DrawingBuffer::prepareMailbox', start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 18, end: 19});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 20, end: 22});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 38, end: 39});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 40, end: 42});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 58, end: 59});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 60, end: 62});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 218, end: 19});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 220, end: 222});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 238, end: 39});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 240, end: 242});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 258, end: 59});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 260, end: 262});
+ };
+ verifier.expectedUEs = [
+ {title: 'WebGL Animation', start: 0, end: 62, eventCount: 4},
+ {title: 'Idle', start: 62, end: 220, eventCount: 0},
+ {title: 'WebGL Animation', start: 220, end: 262, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 62, expectations: [verifier.expectedUEs[0]]},
+ {start: 62, end: 220, expectations: [verifier.expectedUEs[1]]},
+ {start: 220, end: 262, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_oneWithAnimationEventsOneWithout', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'DrawingBuffer::prepareMailbox', start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 18, end: 19});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 20, end: 22});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 38, end: 39});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 40, end: 42});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 58, end: 59});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 60, end: 62});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 220, end: 222});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 240, end: 242});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 260, end: 262});
+ };
+ verifier.expectedUEs = [
+ {title: 'WebGL Animation', start: 0, end: 62, eventCount: 4},
+ {title: 'Idle', start: 62, end: 262, eventCount: 0},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 62, expectations: [verifier.expectedUEs[0]]},
+ {start: 62, end: 262, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_noAnimationEvents', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'DrawingBuffer::prepareMailbox', start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 20, end: 22});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 40, end: 42});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 60, end: 62});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 220, end: 222});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 240, end: 242});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'DrawingBuffer::prepareMailbox', start: 260, end: 262});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 262, eventCount: 0},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 262, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_animationEventsOnly', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'PageAnimator::serviceScriptedAnimations',
+ start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 20, end: 22});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 40, end: 42});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 42, eventCount: 0},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 42, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('webGLAnimations_oneEvent', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'DrawingBuffer::prepareMailbox', start: 0, end: 2}));
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'PageAnimator::serviceScriptedAnimations',
+ start: 4, end: 6});
+ };
+ verifier.expectedUEs = [
+ {title: 'WebGL Animation', start: 0, end: 2, eventCount: 1},
+ {title: 'Idle', start: 2, end: 6, eventCount: 0},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 2, expectations: [verifier.expectedUEs[0]]},
+ {start: 2, end: 6, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('cssAnimations', function() {
+ // CSS Animations happen on the renderer process, not the browser process.
+ // They are merged if they overlap.
+ // They are merged with other kinds of animations.
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 0, end: 130, isTopLevel: true}));
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'Animation', start: 131, end: 200, isTopLevel: true});
+ ChromeTestUtils.addFrameEvent(model, {start: 150, end: 160});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 150, end: 180});
+ ChromeTestUtils.addFrameEvent(model, {start: 290, end: 300});
+ };
+ verifier.expectedUEs = [
+ {title: 'CSS Animation', start: 0, end: 200, eventCount: 4},
+ {title: 'Fling Animation', start: 150, end: 300, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 150, expectations: [verifier.expectedUEs[0]]},
+ {start: 150, end: 200, expectations: [
+ verifier.expectedUEs[0],
+ verifier.expectedUEs[1],
+ ]},
+ {start: 200, end: 300, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('cssAnimationStatesRunningAtEnd', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ // If a top-level Animation async slice has state-change instant
+ // events and the last one is a "running" event, then it will run
+ // to the end of the top level event.
+ const animationA = tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 0, end: 500, isTopLevel: true});
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 100, args: {data: {state: 'running'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 200, args: {data: {state: 'idle'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 300, args: {data: {state: 'running'}}}));
+ model.rendererMain.asyncSliceGroup.push(animationA);
+ ChromeTestUtils.addFrameEvent(model, {start: 50, end: 60});
+ ChromeTestUtils.addFrameEvent(model, {start: 150, end: 160});
+ ChromeTestUtils.addFrameEvent(model, {start: 250, end: 260});
+ ChromeTestUtils.addFrameEvent(model, {start: 350, end: 360});
+ ChromeTestUtils.addFrameEvent(model, {start: 450, end: 460});
+ // We include a frame event off the end of the top level animation slice
+ // so we can test that it correctly stops the AnimationExpectation
+ // at the end of the top-level event, not tne end of the whole trace,
+ ChromeTestUtils.addFrameEvent(model, {start: 1050, end: 1060});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 100, eventCount: 1},
+ {title: 'CSS Animation', start: 100, end: 200, eventCount: 5},
+ {title: 'Idle', start: 200, end: 300, eventCount: 1},
+ {title: 'CSS Animation', start: 300, end: 500, eventCount: 6},
+ {title: 'Idle', start: 500, end: 1060, eventCount: 1},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 200, expectations: [verifier.expectedUEs[1]]},
+ {start: 200, end: 300, expectations: [verifier.expectedUEs[2]]},
+ {start: 300, end: 500, expectations: [verifier.expectedUEs[3]]},
+ {start: 500, end: 1060, expectations: [verifier.expectedUEs[4]]},
+ ];
+ verifier.verify();
+ });
+
+ test('cssAnimationStates', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ // If a top-level Animation async slice does not have state-change instant
+ // subSlices, then assume that the animation was running throughout the
+ // async slice.
+ ChromeTestUtils.addEvent(model.rendererMain, {
+ title: 'Animation', start: 181, end: 250, isTopLevel: true});
+ ChromeTestUtils.addFrameEvent(model, {start: 200, end: 240});
+
+ // Animation ranges should be merged if there is less than 32ms dead time
+ // between them.
+
+ // If a top-level Animation async slice has state-change instant events,
+ // then run a state machine to find the time ranges when the animation was
+ // actually running.
+
+ // This animation was running from 10-40 and 50-60.
+ const animationA = tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 50, end: 500, isTopLevel: true});
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 71, args: {data: {state: 'running'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 104, args: {data: {state: 'pending'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 137, args: {data: {state: 'running'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 150, args: {data: {state: 'paused'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 281, args: {data: {state: 'running'}}}));
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 350, args: {data: {state: 'idle'}}}));
+ model.rendererMain.asyncSliceGroup.push(animationA);
+ ChromeTestUtils.addFrameEvent(model, {start: 80, end: 90});
+ ChromeTestUtils.addFrameEvent(model, {start: 290, end: 300});
+
+ // An animation without a frame event isn't really an animation.
+ const animationC = tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 350, end: 382, isTopLevel: true});
+ model.rendererMain.asyncSliceGroup.push(animationC);
+ animationA.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 350, args: {data: {state: 'idle'}}}));
+
+ // This animation was running from model.bounds.min-50 and
+ // 70-model.bounds.max.
+ const animationB = tr.c.TestUtils.newAsyncSliceEx(
+ {title: 'Animation', start: 0, end: 500, isTopLevel: true});
+ animationB.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 40, args: {data: {state: 'finished'}}}));
+ animationB.subSlices.push(tr.c.TestUtils.newInstantEvent(
+ {title: 'Animation', start: 382, args: {data: {state: 'running'}}}));
+ model.rendererMain.asyncSliceGroup.push(animationB);
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ ChromeTestUtils.addFrameEvent(model, {start: 390, end: 400});
+ };
+ verifier.expectedUEs = [
+ {title: 'CSS Animation', start: 0, end: 350, eventCount: 16},
+ {title: 'Idle', start: 350, end: 382, eventCount: 0},
+ {title: 'CSS Animation', start: 382, end: 500, eventCount: 4},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 350, expectations: [verifier.expectedUEs[0]]},
+ {start: 350, end: 382, expectations: [verifier.expectedUEs[1]]},
+ {start: 382, end: 500, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('flingThatIsntstopped', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 32, end: 100});
+ ChromeTestUtils.addFlingAnimationEvent(model, {start: 38, end: 200});
+ ChromeTestUtils.addFrameEvent(model, {start: 199, end: 200});
+ ChromeTestUtils.addFrameEvent(model, {start: 290, end: 300});
+ };
+ verifier.expectedUEs = [
+ {title: 'Fling Animation', start: 32, end: 200, eventCount: 3},
+ {title: 'Idle', start: 200, end: 300, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 32, end: 200, expectations: [verifier.expectedUEs[0]]},
+ {start: 200, end: 300, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('flingThatIsStopped', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 32, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_CANCEL,
+ {start: 105, end: 150});
+ ChromeTestUtils.addFrameEvent(model, {start: 104, end: 105});
+ ChromeTestUtils.addFrameEvent(model, {start: 149, end: 150});
+ };
+ verifier.expectedUEs = [
+ {title: 'Fling Animation', start: 32, end: 105, eventCount: 3},
+ {title: 'Idle', start: 105, end: 150, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 32, end: 105, expectations: [verifier.expectedUEs[0]]},
+ {start: 105, end: 150, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('flingFling', function() {
+ // measurmt-traces/mobile/facebook_obama_scroll_dialog_box.html
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 0, end: 30});
+ ChromeTestUtils.addFrameEvent(model, {start: 40, end: 41});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 100, end: 130});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_CANCEL,
+ {start: 100, end: 130});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 110, end: 140});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 170, end: 180});
+ ChromeTestUtils.addFrameEvent(model, {start: 150, end: 151});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 200, end: 210});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 200, end: 220});
+ ChromeTestUtils.addFrameEvent(model, {start: 230, end: 240});
+ };
+ verifier.expectedUEs = [
+ {title: 'Fling Animation', start: 0, end: 100, eventCount: 3},
+ {title: 'Touch Response', start: 100, end: 140, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Touch Animation', start: 140, end: 210, eventCount: 3},
+ {title: 'Fling Animation', start: 200, end: 240, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 140, expectations: [verifier.expectedUEs[1]]},
+ {start: 140, end: 200, expectations: [verifier.expectedUEs[2]]},
+ {start: 200, end: 210, expectations: [
+ verifier.expectedUEs[2],
+ verifier.expectedUEs[3],
+ ]},
+ {start: 210, end: 240, expectations: [verifier.expectedUEs[3]]},
+ ];
+ verifier.verify();
+ });
+
+ test('load', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ model.rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader',
+ 25, {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 0,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start: 0,
+ duration: 5.0,
+ }));
+ ChromeTestUtils.addFirstContentfulPaintEvent(model, {start: 20});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 30,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 40,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 50,
+ duration: 300,
+ }));
+ ChromeTestUtils.addFrameEvent(model, {start: 7000, end: 7130});
+ };
+
+ verifier.expectedUEs = [
+ {title: 'Successful Load', start: 0, end: 350, eventCount: 4},
+ {title: 'Idle', start: 350, end: 7130, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 350, expectations: [verifier.expectedUEs[0]]},
+ {start: 350, end: 7130, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('loadStartup', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addRenderingEvent(model, {start: 2, end: 3});
+ ChromeTestUtils.addCreateThreadsEvent(model, {start: 5, end: 10});
+ // findStartupExpectations() should ignore subsequent CreateThreads
+ // events.
+ ChromeTestUtils.addCreateThreadsEvent(model, {start: 25, end: 30});
+ ChromeTestUtils.addFrameEvent(model, {start: 11, end: 20});
+ };
+ verifier.expectedUEs = [
+ {title: 'Startup', start: 2, end: 20, eventCount: 3},
+ {title: 'Idle', start: 20, end: 30, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 2, end: 20, expectations: [verifier.expectedUEs[0]]},
+ {start: 20, end: 30, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('totalIdle', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 10, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 10, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('multipleIdles', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ model.rendererProcess.objects.addSnapshot('ptr', 'loading', 'FrameLoader',
+ 25, {isLoadingMainFrame: true, frame: {id_ref: '0xdeadbeef'},
+ documentLoaderURL: 'http://example.com'});
+ ChromeTestUtils.addFrameEvent(model, {start: 10, end: 20});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'navigationStart',
+ start: 10,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.asyncSliceGroup.push(tr.c.TestUtils.newSliceEx({
+ cat: 'disabled-by-default-network',
+ title: 'ResourceLoad',
+ start: 10,
+ duration: 5.0,
+ }));
+ ChromeTestUtils.addFirstContentfulPaintEvent(model, {start: 20});
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'loading',
+ title: 'firstMeaningfulPaintCandidate',
+ start: 30,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'blink.user_timing',
+ title: 'domContentLoadedEventEnd',
+ start: 40,
+ duration: 0.0,
+ args: {frame: '0xdeadbeef'}
+ }));
+ model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({
+ cat: 'toplevel',
+ title: tr.e.chrome.SCHEDULER_TOP_LEVEL_TASK_TITLE,
+ start: 50,
+ duration: 300,
+ }));
+ ChromeTestUtils.addFrameEvent(model, {start: 7000, end: 7130});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 10, eventCount: 1},
+ {title: 'Successful Load', start: 10, end: 350, eventCount: 5},
+ {title: 'Idle', start: 350, end: 7130, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 10, expectations: [verifier.expectedUEs[0]]},
+ {start: 10, end: 350, expectations: [verifier.expectedUEs[1]]},
+ {start: 350, end: 7130, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('touchStartTouchEndTap', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 200, end: 210});
+ };
+ verifier.expectedUEs = [
+ {title: 'Touch Response', start: 0, end: 210, eventCount: 2,
+ isAnimationBegin: true}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 210, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('touchMoveResponseAnimation', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 50, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 70, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 200, end: 300});
+ ChromeTestUtils.addFrameEvent(model, {start: 299, end: 300});
+ };
+ verifier.expectedUEs = [
+ {title: 'Touch Response', start: 0, end: 100, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Touch Animation', start: 100, end: 300, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 300, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ test('tapEvents', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP,
+ {start: 0, end: 50});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 300, end: 310});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP,
+ {start: 320, end: 350});
+ };
+ verifier.expectedUEs = [
+ {title: 'Tap Response', start: 0, end: 50, eventCount: 1},
+ {title: 'Idle', start: 50, end: 300, eventCount: 0},
+ {title: 'Tap Response', start: 300, end: 350, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 50, expectations: [verifier.expectedUEs[0]]},
+ {start: 50, end: 300, expectations: [verifier.expectedUEs[1]]},
+ {start: 300, end: 350, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('tapAndTapCancelResponses', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 0, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_CANCEL,
+ {start: 300, end: 350});
+ };
+ verifier.expectedUEs = [
+ {title: 'Tap Response', start: 0, end: 100, eventCount: 1},
+ {title: 'Idle', start: 100, end: 300, eventCount: 0},
+ {title: 'Tap Response', start: 300, end: 350, eventCount: 1}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 300, expectations: [verifier.expectedUEs[1]]},
+ {start: 300, end: 350, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('tapCancelResponse', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 0, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_CANCEL,
+ {start: 150, end: 200});
+ };
+ verifier.expectedUEs = [
+ {title: 'Tap Response', start: 0, end: 200, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 200, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('pinchResponseAnimation', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_BEGIN,
+ {start: 100, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 130, end: 160});
+ ChromeTestUtils.addFrameEvent(model, {start: 159, end: 160});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 140, end: 200});
+ ChromeTestUtils.addFrameEvent(model, {start: 199, end: 200});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 150, end: 205});
+ ChromeTestUtils.addFrameEvent(model, {start: 204, end: 205});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 210, end: 220});
+ ChromeTestUtils.addFrameEvent(model, {start: 219, end: 220});
+ // pause > 200ms
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 421, end: 470});
+ ChromeTestUtils.addFrameEvent(model, {start: 469, end: 470});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_END,
+ {start: 460, end: 500});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 100, eventCount: 1},
+ {title: 'Pinch Response', start: 100, end: 160, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Pinch Animation', start: 160, end: 220, eventCount: 6},
+ {title: 'Idle', start: 220, end: 421, eventCount: 0},
+ {title: 'Pinch Animation', start: 421, end: 500, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ {start: 100, end: 160, expectations: [verifier.expectedUEs[1]]},
+ {start: 160, end: 220, expectations: [verifier.expectedUEs[2]]},
+ {start: 220, end: 421, expectations: [verifier.expectedUEs[3]]},
+ {start: 421, end: 500, expectations: [verifier.expectedUEs[4]]},
+ ];
+ verifier.verify();
+ });
+
+ test('tapThenScroll', function() {
+ // measurmt-traces/mobile/google_io_instrument_strumming.json
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 0, end: 20});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 40, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 50, end: 120});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 80, end: 150});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 180, end: 200});
+ ChromeTestUtils.addFrameEvent(model, {start: 199, end: 200});
+ };
+ verifier.expectedUEs = [
+ {title: 'Touch Response', start: 0, end: 100, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Touch Response', start: 50, end: 150, eventCount: 2,
+ isAnimationBegin: true},
+ {title: 'Touch Animation', start: 150, end: 200, eventCount: 2}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 50, expectations: [verifier.expectedUEs[0]]},
+ {start: 50, end: 100, expectations: [
+ verifier.expectedUEs[0],
+ verifier.expectedUEs[1],
+ ]},
+ {start: 100, end: 150, expectations: [verifier.expectedUEs[1]]},
+ {start: 150, end: 200, expectations: [verifier.expectedUEs[2]]},
+ ];
+ verifier.verify();
+ });
+
+ test('pinchFlingTapTouchEventsOverlap', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addFrameEvent(model, {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 20, end: 50});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 20, end: 30});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_CANCEL,
+ {start: 20, end: 50});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 60, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 60, end: 110});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_BEGIN,
+ {start: 60, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_CANCEL,
+ {start: 65, end: 75});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 70, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.PINCH_UPDATE,
+ {start: 70, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 75, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 80, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 85, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 75, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 150, end: 200});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 150, end: 200});
+ ChromeTestUtils.addFrameEvent(model, {start: 199, end: 200});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 180, end: 210});
+ ChromeTestUtils.addFrameEvent(model, {start: 209, end: 210});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 190, end: 210});
+ ChromeTestUtils.addFrameEvent(model, {start: 215, end: 220});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 20, eventCount: 1},
+ {title: 'Pinch Response', start: 20, end: 110,
+ eventCount: 9, isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 110, end: 210,
+ eventCount: 7},
+ {title: 'Fling Animation', start: 180, end: 220, eventCount: 4}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 20, expectations: [verifier.expectedUEs[0]]},
+ {start: 20, end: 110, expectations: [verifier.expectedUEs[1]]},
+ {start: 110, end: 180, expectations: [verifier.expectedUEs[2]]},
+ {start: 180, end: 210, expectations: [
+ verifier.expectedUEs[2],
+ verifier.expectedUEs[3],
+ ]},
+ {start: 210, end: 220, expectations: [verifier.expectedUEs[3]]},
+ ];
+ verifier.verify();
+ });
+
+ test('scrollThenFling', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 0, end: 40});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 50, end: 100});
+ ChromeTestUtils.addFrameEvent(model, {start: 99, end: 100});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 80, end: 100});
+ ChromeTestUtils.addFrameEvent(model, {start: 190, end: 200});
+ };
+ verifier.expectedUEs = [
+ {title: 'Scroll Animation', start: 0, end: 100, eventCount: 3},
+ {title: 'Fling Animation', start: 80, end: 200, eventCount: 3}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 80, expectations: [verifier.expectedUEs[0]]},
+ {start: 80, end: 100, expectations: [
+ verifier.expectedUEs[0],
+ verifier.expectedUEs[1],
+ ]},
+ {start: 100, end: 200, expectations: [verifier.expectedUEs[1]]},
+ ];
+ verifier.verify();
+ });
+
+ /*
+ This test was generated from
+ /test_data/measurmt-traces/mobile/fling_HN_to_rest.json
+ */
+ test('flingHNToRest', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addEvent(model.browserMain,
+ {title: 'model start', start: 0, end: 1});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_START,
+ {start: 1274, end: 1297});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_DOWN,
+ {start: 1274, end: 1305});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1343, end: 1350});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1359, end: 1366});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP_CANCEL,
+ {start: 1359, end: 1366});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 1359, end: 1367});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1359, end: 1387});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1375, end: 1385});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1375, end: 1416});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1389, end: 1404});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1389, end: 1429});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1405, end: 1418});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1405, end: 1449});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 1419, end: 1432});
+ ChromeTestUtils.addFrameEvent(model, {start: 1431, end: 1432});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 1419, end: 1474});
+ ChromeTestUtils.addFrameEvent(model, {start: 1473, end: 1474});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_END,
+ {start: 1427, end: 1435});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.FLING_START,
+ {start: 1427, end: 1474});
+ ChromeTestUtils.addFlingAnimationEvent(model, {start: 1440, end: 2300});
+ ChromeTestUtils.addEvent(model.browserMain,
+ {title: 'model end', start: 3184, end: 3185});
+ };
+ verifier.expectedUEs = [
+ {title: 'Idle', start: 0, end: 1274, eventCount: 0},
+ {title: 'Scroll Response', start: 1274, end: 1387,
+ eventCount: 6, isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 1387, end: 1474,
+ eventCount: 12},
+ {title: 'Fling Animation', start: 1427, end: 2300,
+ eventCount: 4},
+ {title: 'Idle', start: 2300, end: 3185, eventCount: 0}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 1274, expectations: [verifier.expectedUEs[0]]},
+ {start: 1274, end: 1387, expectations: [verifier.expectedUEs[1]]},
+ {start: 1387, end: 1427, expectations: [verifier.expectedUEs[2]]},
+ {start: 1427, end: 1474, expectations: [
+ verifier.expectedUEs[2],
+ verifier.expectedUEs[3],
+ ]},
+ {start: 1474, end: 2300, expectations: [verifier.expectedUEs[3]]},
+ {start: 2300, end: 3185, expectations: [verifier.expectedUEs[4]]},
+ ];
+ verifier.verify();
+ });
+
+ test('TapResponseOverlappingTouchAnimation', function() {
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 0, end: 10});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 5, end: 15});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TOUCH_MOVE,
+ {start: 10, end: 20});
+ ChromeTestUtils.addFrameEvent(model, {start: 19, end: 20});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.TAP,
+ {start: 15, end: 100});
+ };
+ verifier.expectedUEs = [
+ {title: 'Tap Response', start: 0, end: 100,
+ eventCount: 5}
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 100, expectations: [verifier.expectedUEs[0]]},
+ ];
+ verifier.verify();
+ });
+
+ test('responseFramesNotInScrollAnimation', function() {
+ // fixResponseAnimationStarts in find_input_expectations moves the start of
+ // the Scroll Animation and needs to remove frame events that now lie
+ // out of the Scroll Animation's interval
+ const verifier = new UserExpectationVerifier();
+ verifier.customizeModelCallback = function(model) {
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_BEGIN,
+ {start: 0, end: 20});
+ ChromeTestUtils.addFrameEvent(model, {start: 5, end: 6});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 20, end: 40});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_UPDATE,
+ {start: 20, end: 210});
+ // don't associate the follwing event to the Scroll Animation
+ ChromeTestUtils.addFrameEvent(model, {start: 25, end: 26});
+ ChromeTestUtils.addFrameEvent(model, {start: 190, end: 191});
+ ChromeTestUtils.addInputEvent(model, INPUT_TYPE.SCROLL_END,
+ {start: 200, end: 250});
+ };
+ verifier.expectedUEs = [
+ {title: 'Scroll Response', start: 0, end: 40,
+ eventCount: 2, isAnimationBegin: true},
+ {title: 'Scroll Animation', start: 40, end: 250, eventCount: 3},
+ ];
+ verifier.expectedSegments = [
+ {start: 0, end: 40, expectations: [verifier.expectedUEs[0]]},
+ {start: 40, end: 250, expectations: [verifier.expectedUEs[1]]}
+ ];
+ verifier.verify();
+ });
+});
+</script>