diff options
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/importer')
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> |