diff options
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/model')
135 files changed, 24883 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/tracing/model/activity.html b/chromium/third_party/catapult/tracing/tracing/model/activity.html new file mode 100644 index 00000000000..2db257c7e06 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/activity.html @@ -0,0 +1,56 @@ +<!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/color_scheme.html"> + +<script> +'use strict'; + +/** + * @fileoverview Class representing a user activity that is running + * in the process. + * On the Android platform, activities are mapped to Android Activities + * running in the foreground of the process. + * On Windows/OS X this could for example represent + * the currently active window of the process. + */ +tr.exportTo('tr.model', function() { + const ColorScheme = tr.b.ColorScheme; + + /** + * @constructor + * @param {String} name Name of the activity + * @param {String} category Category of the activities + * @param {String} range The time range where the activity was running + * @param {String} args Additional arguments + */ + function Activity(name, category, range, args) { + tr.model.TimedEvent.call(this, range.min); + this.title = name; + this.category = category; + this.colorId = ColorScheme.getColorIdForGeneralPurposeString(name); + this.duration = range.duration; + this.args = args; + this.name = name; + } + + Activity.prototype = { + __proto__: tr.model.TimedEvent.prototype, + + shiftTimestampsForward(amount) { + this.start += amount; + }, + + addBoundsToRange(range) { + range.addValue(this.start); + range.addValue(this.end); + } + }; + return { + Activity, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/alert.html b/chromium/third_party/catapult/tracing/tracing/model/alert.html new file mode 100644 index 00000000000..a8955caec93 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/alert.html @@ -0,0 +1,55 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/model/event_info.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/timed_event.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + function Alert(info, start, opt_associatedEvents, opt_args) { + tr.model.TimedEvent.call(this, start); + this.info = info; + this.args = opt_args || {}; + this.associatedEvents = new tr.model.EventSet(opt_associatedEvents); + this.associatedEvents.forEach(function(event) { + event.addAssociatedAlert(this); + }, this); + } + + Alert.prototype = { + __proto__: tr.model.TimedEvent.prototype, + + get title() { + return this.info.title; + }, + + get colorId() { + return this.info.colorId; + }, + + get userFriendlyName() { + return 'Alert ' + this.title + ' at ' + + tr.b.Unit.byName.timeStampInMs.format(this.start); + } + }; + + tr.model.EventRegistry.register( + Alert, + { + name: 'alert', + pluralName: 'alerts' + }); + + return { + Alert, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/annotation.html b/chromium/third_party/catapult/tracing/tracing/model/annotation.html new file mode 100644 index 00000000000..9a3c1a726ba --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/annotation.html @@ -0,0 +1,86 @@ +<!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/extension_registry.html"> +<link rel="import" href="/tracing/base/guid.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + /** + * Annotation is a base class that represents all annotation objects that + * can be drawn on the timeline. + * + * @constructor + */ + function Annotation() { + this.guid_ = tr.b.GUID.allocateSimple(); + this.view_ = undefined; + } + + Annotation.fromDictIfPossible = function(args) { + if (args.typeName === undefined) { + throw new Error('Missing typeName argument'); + } + + const typeInfo = Annotation.findTypeInfoMatching(function(typeInfo) { + return typeInfo.metadata.typeName === args.typeName; + }); + + if (typeInfo === undefined) return undefined; + + return typeInfo.constructor.fromDict(args); + }; + + Annotation.fromDict = function() { + throw new Error('Not implemented'); + }; + + Annotation.prototype = { + get guid() { + return this.guid_; + }, + + // Invoked by trace model when this annotation is removed. + onRemove() { + }, + + toDict() { + throw new Error('Not implemented'); + }, + + getOrCreateView(viewport) { + if (!this.view_) { + this.view_ = this.createView_(viewport); + } + return this.view_; + }, + + createView_() { + throw new Error('Not implemented'); + } + }; + + const options = new tr.b.ExtensionRegistryOptions(tr.b. BASIC_REGISTRY_MODE); + tr.b.decorateExtensionRegistry(Annotation, options); + + Annotation.addEventListener('will-register', function(e) { + if (!e.typeInfo.constructor.hasOwnProperty('fromDict')) { + throw new Error('Must have fromDict method'); + } + + if (!e.typeInfo.metadata.typeName) { + throw new Error('Registered Annotations must provide typeName'); + } + }); + + return { + Annotation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/annotation_test.html b/chromium/third_party/catapult/tracing/tracing/model/annotation_test.html new file mode 100644 index 00000000000..f0d38843a3f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/annotation_test.html @@ -0,0 +1,44 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/comment_box_annotation.html"> +<link rel="import" href="/tracing/model/location.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/rect_annotation.html"> +<link rel="import" href="/tracing/model/x_marker_annotation.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('rectAnnotation', function() { + const fakeYComponents1 = [{stableId: '1.2', yPercentOffset: 0.3}]; + const fakeYComponents2 = [{stableId: '1.2', yPercentOffset: 0.9}]; + const start = new tr.model.Location(50, fakeYComponents1); + const end = new tr.model.Location(100, fakeYComponents2); + const rectAnnotation = new tr.model.RectAnnotation(start, end); + assert.strictEqual(rectAnnotation.startLocation, start); + assert.strictEqual(rectAnnotation.endLocation, end); + }); + + test('xMarkerAnnotation', function() { + const xMarkerAnnotation = new tr.model.XMarkerAnnotation(90); + assert.strictEqual(xMarkerAnnotation.timestamp, 90); + }); + + test('commentBoxAnnotation', function() { + const fakeYComponents = [{stableId: '1.2', yPercentOffset: 0.5}]; + const location = new tr.model.Location(120, fakeYComponents); + const text = 'abc'; + const commentBoxAnnotation = + new tr.model.CommentBoxAnnotation(location, text); + assert.strictEqual(commentBoxAnnotation.location, location); + assert.strictEqual(commentBoxAnnotation.text, text); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/async_slice.html b/chromium/third_party/catapult/tracing/tracing/model/async_slice.html new file mode 100644 index 00000000000..9e395ab4cb7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/async_slice.html @@ -0,0 +1,169 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/model/timed_event.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the AsyncSlice class. + */ +tr.exportTo('tr.model', function() { + /** + * A AsyncSlice represents an interval of time during which an + * asynchronous operation is in progress. An AsyncSlice consumes no CPU time + * itself and so is only associated with Threads at its start and end point. + * + * @constructor + */ + function AsyncSlice(category, title, colorId, start, args, duration, + opt_isTopLevel, opt_cpuStart, opt_cpuDuration, + opt_argsStripped) { + tr.model.TimedEvent.call(this, start); + + this.category = category || ''; + // We keep the original title from the trace file in originalTitle since + // some sub-classes, e.g. NetAsyncSlice, change the title field. + this.originalTitle = title; + this.title = title; + this.colorId = colorId; + this.args = args; + this.startStackFrame = undefined; + this.endStackFrame = undefined; + this.didNotFinish = false; + this.important = false; + this.subSlices = []; + this.parentContainer_ = undefined; + + this.id = undefined; + this.startThread = undefined; + this.endThread = undefined; + this.cpuStart = undefined; + this.cpuDuration = undefined; + this.argsStripped = false; + + this.startStackFrame = undefined; + this.endStackFrame = undefined; + + this.duration = duration; + + // isTopLevel is set at import because only NESTABLE_ASYNC events might not + // be topLevel. All legacy async events are toplevel by definition. + this.isTopLevel = (opt_isTopLevel === true); + + if (opt_cpuStart !== undefined) { + this.cpuStart = opt_cpuStart; + } + + if (opt_cpuDuration !== undefined) { + this.cpuDuration = opt_cpuDuration; + } + + if (opt_argsStripped !== undefined) { + this.argsStripped = opt_argsStripped; + } + } + + AsyncSlice.prototype = { + __proto__: tr.model.TimedEvent.prototype, + + get analysisTypeName() { + return this.title; + }, + + get parentContainer() { + return this.parentContainer_; + }, + + set parentContainer(parentContainer) { + this.parentContainer_ = parentContainer; + for (let i = 0; i < this.subSlices.length; i++) { + const subSlice = this.subSlices[i]; + if (subSlice.parentContainer === undefined) { + subSlice.parentContainer = parentContainer; + } + } + }, + + get viewSubGroupTitle() { + return this.title; + }, + + // Certain AsyncSlices can provide a grouping key to group a set of + // independent tracks, while grouping by |viewSubGroupTitle| puts slices + // into a single (maybe multi-row but single) track. + get viewSubGroupGroupingKey() { + return undefined; + }, + + get userFriendlyName() { + return 'Async slice ' + this.title + ' at ' + + tr.b.Unit.byName.timeStampInMs.format(this.start); + }, + + get stableId() { + const parentAsyncSliceGroup = this.parentContainer.asyncSliceGroup; + return parentAsyncSliceGroup.stableId + '.' + + parentAsyncSliceGroup.slices.indexOf(this); + }, + + * findTopmostSlicesRelativeToThisSlice(eventPredicate, opt_this) { + if (eventPredicate(this)) { + yield this; + return; + } + for (const s of this.subSlices) { + yield* s.findTopmostSlicesRelativeToThisSlice(eventPredicate); + } + }, + + findDescendentSlice(targetTitle) { + if (!this.subSlices) return undefined; + + for (let i = 0; i < this.subSlices.length; i++) { + if (this.subSlices[i].title === targetTitle) { + return this.subSlices[i]; + } + const slice = this.subSlices[i].findDescendentSlice(targetTitle); + if (slice) return slice; + } + return undefined; + }, + + * enumerateAllDescendents() { + for (const slice of this.subSlices) { + yield slice; + } + for (const slice of this.subSlices) { + // Slices might contain sub-events different from AsyncSlice. + // We don't go any deeper in that case. + if (slice.enumerateAllDescendents !== undefined) { + yield* slice.enumerateAllDescendents(); + } + } + }, + + compareTo(that) { + return this.title.localeCompare(that.title); + } + }; + + tr.model.EventRegistry.register( + AsyncSlice, + { + name: 'asyncSlice', + pluralName: 'asyncSlices' + }); + + + return { + AsyncSlice, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/async_slice_group.html b/chromium/third_party/catapult/tracing/tracing/model/async_slice_group.html new file mode 100644 index 00000000000..249e6396667 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/async_slice_group.html @@ -0,0 +1,211 @@ +<!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/guid.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/model/async_slice.html"> +<link rel="import" href="/tracing/model/event_container.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the AsyncSliceGroup class. + */ +tr.exportTo('tr.model', function() { + /** + * Group of AsyncSlices associated with a thread or an upper-level + * AsyncSliceGroup. Thread can have number of AsyncSliceGroups and these + * groups can have nested groups too. No further nested levels are allowed. + */ + class AsyncSliceGroup extends tr.model.EventContainer { + /** + * @param {Thread} parentContainer Thread on which async slices start. + * @param {String} opt_name Optional name (no IDs please) for settings key. + */ + constructor(parentContainer, opt_name) { + super(); + this.parentContainer_ = parentContainer; + this.name_ = opt_name; + this.slices = []; + this.viewSubGroups_ = undefined; + + // Default values for the root group. + // Nested groups will get these values reassigned. + this.nestedLevel_ = 0; + this.hasNestedSubGroups_ = true; + this.title_ = undefined; + } + + get parentContainer() { + return this.parentContainer_; + } + + get model() { + return this.parentContainer_.parent.model; + } + + get stableId() { + return this.parentContainer_.stableId + '.AsyncSliceGroup'; + } + + get title() { + // Title isn't defined for the root group (nested level 0) because + // slices it contains aren't grouped on that level. + // All the nested groups have their title which is: + // - |slice.viewSubGroupGroupingKey| if defined (level 1 only), or + // - |slice.viewSubGroupTitle| otherwise (most cases). + if (this.nested_level_ === 0) { + return '<root>'; + } + return this.title_; + } + + getSettingsKey() { + if (this.name_ === undefined) { + return undefined; + } + const parentKey = this.parentContainer_.getSettingsKey(); + if (parentKey === undefined) { + return undefined; + } + return parentKey + '.' + this.name_; + } + + /** + * Helper function that pushes the provided slice onto the slices array. + */ + push(slice) { + if (this.viewSubGroups_ !== undefined) { + throw new Error( + 'No new slices are allowed when view sub-groups already formed.'); + } + slice.parentContainer = this.parentContainer; + this.slices.push(slice); + return slice; + } + + /** + * @return {Number} The number of slices in this group. + */ + get length() { + return this.slices.length; + } + + /** + * Shifts all the timestamps inside this group forward by the amount + * specified, including all nested subSlices if there are any. + */ + shiftTimestampsForward(amount) { + for (const slice of this.childEvents()) { + slice.start += amount; + } + } + + /** + * Updates the bounds for this group based on the slices it contains. + */ + updateBounds() { + this.bounds.reset(); + for (let i = 0; i < this.slices.length; i++) { + this.bounds.addValue(this.slices[i].start); + this.bounds.addValue(this.slices[i].end); + } + } + + /** + * Closes any open slices. + * All open slices assumed as finished at the end of model's time bounds. + */ + autoCloseOpenSlices() { + const maxTimestamp = this.parentContainer_.parent.model.bounds.max; + for (const slice of this.childEvents()) { + if (slice.didNotFinish) { + slice.duration = maxTimestamp - slice.start; + } + } + } + + /** + * Get AsyncSlice sub-groups arranged by title and grouping key. + * + * @return {Array} An array of AsyncSliceGroups where each group has + * a title and sub-set of the original slices. Returns an empty array + * if group can't be sub-divided. + */ + get viewSubGroups() { + // Only 2 nested levels are allowed (see class description). + // Also it's always known in advance whether group will be sub-divided or + // not (most of them). Root group is always divisible. + if (!this.hasNestedSubGroups_ || this.nestedLevel_ === 2) { + return []; + } + if (this.viewSubGroups_ !== undefined) { + return this.viewSubGroups_; + } + + const subGroupsByTitle = new Map(); + + for (const slice of this.slices) { + // Group by title by default. + let subGroupTitle = slice.viewSubGroupTitle; + let hasNestedSubGroups = false; + + // Apply custom grouping rules for special slice classes. + if (this.nestedLevel_ === 0 && + slice.viewSubGroupGroupingKey !== undefined) { + subGroupTitle = slice.viewSubGroupGroupingKey; + hasNestedSubGroups = true; + } + + let subGroup = subGroupsByTitle.get(subGroupTitle); + if (subGroup === undefined) { + let name; + if (this.name_ !== undefined) { + name = this.name_ + '.' + subGroupTitle; + } else { + name = subGroupTitle; + } + subGroup = new AsyncSliceGroup(this.parentContainer_, name); + subGroup.title_ = subGroupTitle; + subGroup.hasNestedSubGroups_ = hasNestedSubGroups; + subGroup.nestedLevel_ = this.nestedLevel_ + 1; + subGroupsByTitle.set(subGroupTitle, subGroup); + } + subGroup.push(slice); + } + + this.viewSubGroups_ = Array.from(subGroupsByTitle.values()); + this.viewSubGroups_.sort((a, b) => a.title.localeCompare(b.title)); + return this.viewSubGroups_; + } + + * findTopmostSlicesInThisContainer(eventPredicate, opt_this) { + for (const slice of this.slices) { + if (slice.isTopLevel) { + yield* slice.findTopmostSlicesRelativeToThisSlice( + eventPredicate, opt_this); + } + } + } + + * childEvents() { + for (const slice of this.slices) { + yield slice; + yield* slice.enumerateAllDescendents(); + } + } + + * childEventContainers() { + } + } + + return { + AsyncSliceGroup, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/async_slice_group_test.html b/chromium/third_party/catapult/tracing/tracing/model/async_slice_group_test.html new file mode 100644 index 00000000000..f996a63f2b9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/async_slice_group_test.html @@ -0,0 +1,171 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome_config.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Process = tr.model.Process; + const Thread = tr.model.Thread; + const AsyncSlice = tr.model.AsyncSlice; + const AsyncSliceGroup = tr.model.AsyncSliceGroup; + const newAsyncSlice = tr.c.TestUtils.newAsyncSlice; + const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx; + const newModel = tr.c.TestUtils.newModel; + + test('asyncSliceGroupBounds_Empty', function() { + const thread = {}; + const g = new AsyncSliceGroup(thread); + g.updateBounds(); + assert.isTrue(g.bounds.isEmpty); + }); + + test('asyncSliceGroupBounds_Basic', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = p1.getOrCreateThread(456); + const g = new AsyncSliceGroup(t1); + g.push(newAsyncSlice(0, 1, t1, t1)); + g.push(newAsyncSlice(1, 1.5, t1, t1)); + assert.strictEqual(g.length, 2); + g.updateBounds(); + assert.strictEqual(g.bounds.min, 0); + assert.strictEqual(g.bounds.max, 2.5); + }); + + test('asyncSliceGroupChildEvents', function() { + const t1 = {}; // Fake thread. + const g = new AsyncSliceGroup(t1); + const sl1 = newAsyncSlice(0, 1, t1, t1); + const sl2 = newAsyncSlice(1, 3, t1, t1); + const sl2sub1 = newAsyncSlice(1, 2, t1, t1); + sl2.subSlices.push(sl2sub1); + const sl2sub1sub1 = newAsyncSlice(2, 1, t1, t1); + sl2sub1.subSlices.push(sl2sub1sub1); + const sl2sub2 = newAsyncSlice(3, 1, t1, t1); + sl2.subSlices.push(sl2sub2); + g.push(sl1); + g.push(sl2); + + assert.sameMembers( + Array.from(g.childEvents()), [sl1, sl2, sl2sub1, sl2sub1sub1, sl2sub2]); + }); + + test('asyncSliceGroupShiftTimestamps', function() { + const t1 = {}; // Fake thread. + const g = new AsyncSliceGroup(t1); + const sl1 = newAsyncSlice(1, 2, t1, t1); + const sl2 = newAsyncSlice(3, 4, t1, t1); + const sl2sub1 = newAsyncSlice(3.5, 2, t1, t1); + sl2.subSlices.push(sl2sub1); + const sl2sub1sub1 = newAsyncSlice(4, 0.5, t1, t1); + sl2sub1.subSlices.push(sl2sub1sub1); + g.push(sl1); + g.push(sl2); + + g.updateBounds(); + assert.strictEqual(g.bounds.min, 1); + assert.strictEqual(g.bounds.max, 7); + + g.shiftTimestampsForward(1.5); + g.updateBounds(); + assert.strictEqual(g.bounds.min, 2.5); + assert.strictEqual(g.bounds.max, 8.5); + assert.strictEqual(sl2sub1.start, 5); + assert.strictEqual(sl2sub1.duration, 2); + assert.strictEqual(sl2sub1sub1.start, 5.5); + assert.strictEqual(sl2sub1sub1.duration, 0.5); + }); + + test('asyncSliceGroupViewSubGroups', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + p1.name = 'Renderer'; + const t1 = p1.getOrCreateThread(321); + t1.name = 'MainThread'; + const g = new AsyncSliceGroup(t1); + g.push(newAsyncSliceEx( + { title: 'VeryBusy', + start: 0, duration: 1 })); + g.push(newAsyncSliceEx( + { cat: 'renderer.scheduler', + id: ':ptr:0xdeadbeef', title: 'FrameScheduler.Foo', + start: 0.5, duration: 0.1 })); + g.push(newAsyncSliceEx( + { cat: 'renderer.scheduler', + id: ':ptr:0xdeadbeef', title: 'FrameScheduler.Bar', + start: 0.55, duration: 0.2 })); + g.push(newAsyncSliceEx( + { cat: 'renderer.scheduler', + id: ':ptr:0x1ee7beef', title: 'FrameScheduler.Baz', + start: 0.3, duration: 0.3 })); + g.push(newAsyncSliceEx( + { cat: 'renderer.scheduler', + id: ':ptr:0x1ee7beef', title: 'FrameScheduler.Baz', + start: 0.7, duration: 0.2 })); + g.push(newAsyncSliceEx( + { title: 'VeryBusy', + start: 1, duration: 1.5 })); + g.push(newAsyncSliceEx( + { title: 'Loading', + start: 0, duration: 5 })); + assert.strictEqual(g.length, 7); + + const vsg = g.viewSubGroups; + assert.strictEqual(vsg.length, 4); + // Groups must be sorted by title. + assert.strictEqual(vsg[0].title, 'Frame:ptr:0x1ee7beef'); + assert.strictEqual(vsg[1].title, 'Frame:ptr:0xdeadbeef'); + assert.strictEqual(vsg[2].title, 'Loading'); + assert.strictEqual(vsg[3].title, 'VeryBusy'); + // Check nested view sub-groups. + assert.strictEqual(vsg[2].viewSubGroups.length, 0); + assert.strictEqual(vsg[3].viewSubGroups.length, 0); + const wf1vsg = vsg[0].viewSubGroups; + assert.strictEqual(wf1vsg.length, 1); + assert.strictEqual(wf1vsg[0].title, 'Baz'); + assert.strictEqual(wf1vsg[0].getSettingsKey(), + 'processes.Renderer.MainThread.Frame:ptr:0x1ee7beef.Baz'); + const wf2vsg = vsg[1].viewSubGroups; + assert.strictEqual(wf2vsg.length, 2); + assert.strictEqual(wf2vsg[0].title, 'Bar'); + assert.strictEqual(wf2vsg[0].getSettingsKey(), + 'processes.Renderer.MainThread.Frame:ptr:0xdeadbeef.Bar'); + assert.strictEqual(wf2vsg[1].title, 'Foo'); + assert.strictEqual(wf2vsg[1].getSettingsKey(), + 'processes.Renderer.MainThread.Frame:ptr:0xdeadbeef.Foo'); + }); + + test('asyncSliceGroupStableId', function() { + const model = new tr.Model(); + const process = model.getOrCreateProcess(123); + const thread = process.getOrCreateThread(456); + const group = new AsyncSliceGroup(thread); + + assert.strictEqual(process.stableId, 123); + assert.strictEqual(thread.stableId, '123.456'); + assert.strictEqual(group.stableId, '123.456.AsyncSliceGroup'); + }); + + test('asyncSliceParentContainerSetAtPush', function() { + const m = newModel(function(m) { + m.process = m.getOrCreateProcess(123); + m.thread = m.process.getOrCreateThread(456); + m.group = new AsyncSliceGroup(m.thread); + + m.sA = m.group.push(newAsyncSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + }); + + assert.deepEqual(m.sA.parentContainer, m.thread); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/async_slice_test.html b/chromium/third_party/catapult/tracing/tracing/model/async_slice_test.html new file mode 100644 index 00000000000..1de8493035e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/async_slice_test.html @@ -0,0 +1,57 @@ +<!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/core/test_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const AsyncSlice = tr.model.AsyncSlice; + const Process = tr.model.Process; + const Thread = tr.model.Thread; + const newAsyncSlice = tr.c.TestUtils.newAsyncSlice; + const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx; + const newFakeThread = tr.c.TestUtils.newFakeThread; + + test('stableId', function() { + const thread = newFakeThread(); + const group = thread.asyncSliceGroup; + + const sA = group.push(newAsyncSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + const sB = group.push(newAsyncSliceEx( + { title: 'sB', start: 10.0, duration: 20.0 })); + const sC = group.push(newAsyncSliceEx( + { title: 'sC', start: 20.0, duration: 30.0 })); + + assert.strictEqual(group.stableId + '.0', sA.stableId); + assert.strictEqual(group.stableId + '.1', sB.stableId); + assert.strictEqual(group.stableId + '.2', sC.stableId); + }); + + test('setParentContainerForSubSlices', function() { + const model = new tr.Model(); + const p1 = new Process(model, 1); + const t1 = new Thread(p1, 1); + const asyncSlice = newAsyncSlice(0, 10, t1, t1); + const subSlice1 = newAsyncSlice(1, 5, t1, t1); + const subSlice2 = newAsyncSlice(6, 9, t1, t1); + const subSlice3 = newAsyncSlice(2, 3, t1, t1); + subSlice1.subSlices.push(subSlice3); + asyncSlice.subSlices.push(subSlice1); + asyncSlice.subSlices.push(subSlice2); + asyncSlice.parentContainer = t1; + assert.strictEqual(asyncSlice.subSlices.length, 2); + assert.strictEqual(subSlice1.subSlices.length, 1); + assert.deepEqual(asyncSlice.parentContainer, t1); + assert.deepEqual(subSlice1.parentContainer, t1); + assert.deepEqual(subSlice2.parentContainer, t1); + assert.deepEqual(subSlice3.parentContainer, t1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager.html b/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager.html new file mode 100644 index 00000000000..cb9a30bad79 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager.html @@ -0,0 +1,467 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/utils.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const ClockDomainId = { + BATTOR: 'BATTOR', + + // NOTE: Exists for backwards compatibility with old Chrome traces which + // didn't explicitly specify the clock being used. + UNKNOWN_CHROME_LEGACY: 'UNKNOWN_CHROME_LEGACY', + + LINUX_CLOCK_MONOTONIC: 'LINUX_CLOCK_MONOTONIC', + LINUX_FTRACE_GLOBAL: 'LINUX_FTRACE_GLOBAL', + MAC_MACH_ABSOLUTE_TIME: 'MAC_MACH_ABSOLUTE_TIME', + WIN_ROLLOVER_PROTECTED_TIME_GET_TIME: + 'WIN_ROLLOVER_PROTECTED_TIME_GET_TIME', + WIN_QPC: 'WIN_QPC', + + // 'TELEMETRY' and 'SYSTRACE' aren't really clock domains because they + // actually can use one of several clock domains, just like Chrome. However, + // because there's a chance that they are running off of the same clock as + // Chrome (e.g. LINUX_CLOCK_MONOTONIC) but on a separate device (i.e. on a + // a host computer with Chrome running on an attached phone), there's a + // chance that Chrome and the controller will erroneously get put into + // the same clock domain. The solution for this is that clock domains should + // actually be some (unique_device_id, clock_id) tuple. For now, though, + // we'll hack around this by putting Telemetry into its own clock domain. + SYSTRACE: 'SYSTRACE', + TELEMETRY: 'TELEMETRY' + }; + + const POSSIBLE_CHROME_CLOCK_DOMAINS = new Set([ + ClockDomainId.UNKNOWN_CHROME_LEGACY, + ClockDomainId.LINUX_CLOCK_MONOTONIC, + ClockDomainId.MAC_MACH_ABSOLUTE_TIME, + ClockDomainId.WIN_ROLLOVER_PROTECTED_TIME_GET_TIME, + ClockDomainId.WIN_QPC + ]); + + // The number of milliseconds above which the BattOr sync is no longer + // considered "fast", and it's more accurate to use the sync start timestamp + // instead of the normal sync timestamp due to a bug in the Chrome serial code + // making serial reads too slow. + const BATTOR_FAST_SYNC_THRESHOLD_MS = 3; + + /** + * A ClockSyncManager holds clock sync markers and uses them to shift + * timestamps from agents' clock domains onto the model's clock domain. + * + * In this context, a "clock domain" is a single perspective on the passage + * of time. A single computer can have multiple clock domains because it + * can have multiple methods of retrieving a timestamp (e.g. + * clock_gettime(CLOCK_MONOTONIC) and clock_gettime(CLOCK_REALTIME) on Linux). + * Another common reason for multiple clock domains within a single trace + * is that traces can span devices (e.g. a laptop collecting a Chrome trace + * can have its power consumption recorded by a second device and the two + * traces can be viewed alongside each other). + * + * For more information on how to synchronize multiple time domains using this + * method, see: http://bit.ly/1OVkqju. + * + * @constructor + */ + function ClockSyncManager() { + // A set of all domains seen by the ClockSyncManager. + this.domainsSeen_ = new Set(); + this.markersBySyncId_ = new Map(); + // transformerMapByDomainId_[fromDomainId][toDomainId] returns the function + // that converts timestamps in the "from" domain to timestamps in the "to" + // domain. + this.transformerMapByDomainId_ = {}; + } + + ClockSyncManager.prototype = { + /** + * Adds a clock sync marker to the list of known markers. + * + * @param {string} domainId The clock domain that the marker is in. + * @param {string} syncId The identifier shared by both sides of the clock + * sync marker. + * @param {number} startTs The time (in ms) at which the sync started. + * @param {number=} opt_endTs The time (in ms) at which the sync ended. If + * unspecified, it's assumed to be the same as the start, + * indicating an instantaneous sync. + */ + addClockSyncMarker(domainId, syncId, startTs, opt_endTs) { + this.onDomainSeen_(domainId); + + if (Object.values(ClockDomainId).indexOf(domainId) < 0) { + throw new Error('"' + domainId + '" is not in the list of known ' + + 'clock domain IDs.'); + } + + if (this.modelDomainId_) { + throw new Error('Cannot add new clock sync markers after getting ' + + 'a model time transformer.'); + } + + const marker = new ClockSyncMarker(domainId, startTs, opt_endTs); + + if (!this.markersBySyncId_.has(syncId)) { + this.markersBySyncId_.set(syncId, [marker]); + return; + } + + const markers = this.markersBySyncId_.get(syncId); + + if (markers.length === 2) { + throw new Error('Clock sync with ID "' + syncId + '" is already ' + + 'complete - cannot add a third clock sync marker to it.'); + } + + if (markers[0].domainId === domainId) { + throw new Error('A clock domain cannot sync with itself.'); + } + + markers.push(marker); + this.onSyncCompleted_(markers[0], marker); + }, + + // TODO(charliea): Remove this once the clockSyncMetric is no longer using + // it. + get markersBySyncId() { + return this.markersBySyncId_; + }, + + /** @return {Set<String>} The string IDs of the domains seen so far. */ + get domainsSeen() { + return this.domainsSeen_; + }, + + /** + * Returns a function that, given a timestamp in the domain with |domainId|, + * returns a timestamp in the model's clock domain. + * + * NOTE: All clock sync markers should be added before calling this function + * for the first time. This is because the first time that this function is + * called, a model clock domain is selected. This clock domain must have + * syncs connecting it with all other clock domains. If multiple clock + * domains are viable candidates, the one with the clock domain ID that is + * the first alphabetically is selected. + */ + getModelTimeTransformer(domainId) { + this.onDomainSeen_(domainId); + + if (!this.modelDomainId_) { + this.selectModelDomainId_(); + } + + return this.getTimeTransformerRaw_(domainId, this.modelDomainId_).fn; + }, + + /** + * Returns the error associated with the transformation given by + * |getModelTimeTransformer(domainId)|. + */ + getTimeTransformerError(fromDomainId, toDomainId) { + this.onDomainSeen_(fromDomainId); + this.onDomainSeen_(toDomainId); + return this.getTimeTransformerRaw_(fromDomainId, toDomainId).error; + }, + + getTimeTransformerRaw_(fromDomainId, toDomainId) { + const transformer = + this.getTransformerBetween_(fromDomainId, toDomainId); + if (!transformer) { + throw new Error('No clock sync markers exist pairing clock domain "' + + fromDomainId + '" ' + 'with target clock domain "' + + toDomainId + '".'); + } + + return transformer; + }, + + /** + * Returns a function that, given a timestamp in the "from" domain, returns + * a timestamp in the "to" domain. + */ + getTransformerBetween_(fromDomainId, toDomainId) { + // Do a breadth-first search from the "from" domain until we reach the + // "to" domain. + const visitedDomainIds = new Set(); + // Keep a queue of nodes to visit, starting with the "from" domain. + const queue = [{ + domainId: fromDomainId, + transformer: Transformer.IDENTITY + }]; + + while (queue.length > 0) { + // NOTE: Using a priority queue here would theoretically be much more + // efficient, but the actual performance difference is negligible given + // how few clock domains we have in a trace. + queue.sort((domain1, domain2) => + domain1.transformer.error - domain2.transformer.error); + + const current = queue.shift(); + + if (current.domainId === toDomainId) { + return current.transformer; + } + + if (visitedDomainIds.has(current.domainId)) { + continue; + } + visitedDomainIds.add(current.domainId); + + const outgoingTransformers = + this.transformerMapByDomainId_[current.domainId]; + + if (!outgoingTransformers) continue; + + // Add all nodes that are directly connected to this one to the queue. + for (const outgoingDomainId in outgoingTransformers) { + // We have two transformers: one to get us from the "from" domain to + // the current domain, and another to get us from the current domain + // to the next domain. By composing those two transformers, we can + // create one that gets us from the "from" domain to the next domain. + const toNextDomainTransformer = + outgoingTransformers[outgoingDomainId]; + const toCurrentDomainTransformer = current.transformer; + + queue.push({ + domainId: outgoingDomainId, + transformer: Transformer.compose( + toNextDomainTransformer, toCurrentDomainTransformer) + }); + } + } + + return undefined; + }, + + /** + * Selects the domain to use as the model domain from among the domains + * with registered markers. + * + * This is necessary because some common domain must be chosen before all + * timestamps can be shifted onto the same domain. + * + * For the time being, preference is given to Chrome clock domains. If no + * Chrome clock domain is present, the first clock domain alphabetically + * is selected. + */ + selectModelDomainId_() { + this.ensureAllDomainsAreConnected_(); + + // While we're migrating to the new clock sync system, we have to make + // sure to prefer the Chrome clock domain because legacy clock sync + // mechanisms assume that's the case. + for (const chromeDomainId of POSSIBLE_CHROME_CLOCK_DOMAINS) { + if (this.domainsSeen_.has(chromeDomainId)) { + this.modelDomainId_ = chromeDomainId; + return; + } + } + + const domainsSeenArray = Array.from(this.domainsSeen_); + domainsSeenArray.sort(); + this.modelDomainId_ = domainsSeenArray[0]; + }, + + /** Throws an error if all domains are not connected. */ + ensureAllDomainsAreConnected_() { + // NOTE: this is a ridiculously inefficient way to do this. Given how few + // clock domains we're likely to have, this shouldn't be a problem. + let firstDomainId = undefined; + for (const domainId of this.domainsSeen_) { + if (!firstDomainId) { + firstDomainId = domainId; + continue; + } + + if (!this.getTransformerBetween_(firstDomainId, domainId)) { + throw new Error('Unable to select a master clock domain because no ' + + 'path can be found from "' + firstDomainId + '" to "' + domainId + + '".'); + } + } + + return true; + }, + + /** Observer called each time that a clock domain is seen. */ + onDomainSeen_(domainId) { + if (domainId === ClockDomainId.UNKNOWN_CHROME_LEGACY && + !this.domainsSeen_.has(ClockDomainId.UNKNOWN_CHROME_LEGACY)) { + // UNKNOWN_CHROME_LEGACY was just seen for the first time: collapse it + // and the other Chrome clock domains into one. + // + // This makes sure that we don't have two separate clock sync graphs: + // one attached to UNKNOWN_CHROME_LEGACY and the other attached to the + // real Chrome clock domain. + for (const chromeDomainId of POSSIBLE_CHROME_CLOCK_DOMAINS) { + if (chromeDomainId === ClockDomainId.UNKNOWN_CHROME_LEGACY) { + continue; + } + + this.collapseDomains_( + ClockDomainId.UNKNOWN_CHROME_LEGACY, chromeDomainId); + } + } + + this.domainsSeen_.add(domainId); + }, + + /** + * Observer called when a complete sync is made involving |marker1| and + * |marker2|. + */ + onSyncCompleted_(marker1, marker2) { + const forwardTransformer = Transformer.fromMarkers(marker1, marker2); + const backwardTransformer = Transformer.fromMarkers(marker2, marker1); + + const existingTransformer = + this.getOrCreateTransformerMap_(marker1.domainId)[marker2.domainId]; + if (!existingTransformer || + forwardTransformer.error < existingTransformer.error) { + this.getOrCreateTransformerMap_(marker1.domainId)[marker2.domainId] = + forwardTransformer; + this.getOrCreateTransformerMap_(marker2.domainId)[marker1.domainId] = + backwardTransformer; + } + }, + + /** Makes timestamps in the two clock domains interchangeable. */ + collapseDomains_(domain1Id, domain2Id) { + this.getOrCreateTransformerMap_(domain1Id)[domain2Id] = + this.getOrCreateTransformerMap_(domain2Id)[domain1Id] = + Transformer.IDENTITY; + }, + + /** + * Returns (and creates if it doesn't exist) the transformer map describing + * how to transform timestamps between directly connected clock domains. + */ + getOrCreateTransformerMap_(domainId) { + if (!this.transformerMapByDomainId_[domainId]) { + this.transformerMapByDomainId_[domainId] = {}; + } + + return this.transformerMapByDomainId_[domainId]; + }, + + /** + * @return {string} The clock sync graph represented in the DOT language. + * This is useful for debugging incorrect clock sync behavior. + */ + computeDotGraph() { + let dotString = 'graph {\n'; + + const domainsSeen = [...this.domainsSeen_].sort(); + for (const domainId of domainsSeen) { + dotString += ` ${domainId}[shape=box]\n`; + } + + const markersBySyncIdEntries = [...this.markersBySyncId_.entries()].sort( + ([syncId1, markers1], [syncId2, markers2]) => + syncId1.localeCompare(syncId2)); + + for (const [syncId, markers] of markersBySyncIdEntries) { + const sortedMarkers = markers.sort( + (a, b) => a.domainId.localeCompare(b.domainId)); + for (const m of markers) { + dotString += ` "${syncId}" -- ${m.domainId} `; + dotString += `[label="[${m.startTs}, ${m.endTs}]"]\n`; + } + } + + dotString += '}'; + + return dotString; + } + }; + + /** + * A ClockSyncMarker is an internal entity that represents a marker in a + * trace log indicating that a clock sync happened at a specified time. + * + * If no end timestamp argument is specified in the constructor, it's assumed + * that the end timestamp is the same as the start (i.e. the clock sync + * was instantaneous). + */ + function ClockSyncMarker(domainId, startTs, opt_endTs) { + this.domainId = domainId; + this.startTs = startTs; + this.endTs = opt_endTs === undefined ? startTs : opt_endTs; + } + + ClockSyncMarker.prototype = { + get duration() { return this.endTs - this.startTs; }, + get ts() { return this.startTs + this.duration / 2; } + }; + + /** + * A Transformer encapsulates information about how to turn timestamps in one + * clock domain into timestamps in another. It also stores additional data + * about the maximum error involved in doing so. + */ + function Transformer(fn, error) { + this.fn = fn; + this.error = error; + } + + Transformer.IDENTITY = new Transformer((x => x), 0); + + /** + * Given two transformers, creates a third that's a composition of the two. + * + * @param {function(Number): Number} aToB A function capable of converting a + * timestamp from domain A to domain B. + * @param {function(Number): Number} bToC A function capable of converting a + * timestamp from domain B to domain C. + * + * @return {function(Number): Number} A function capable of converting a + * timestamp from domain A to domain C. + */ + Transformer.compose = function(aToB, bToC) { + return new Transformer( + (ts) => bToC.fn(aToB.fn(ts)), aToB.error + bToC.error); + }; + + /** + * Returns a function that, given a timestamp in |fromMarker|'s domain, + * returns a timestamp in |toMarker|'s domain. + */ + Transformer.fromMarkers = function(fromMarker, toMarker) { + let fromTs = fromMarker.ts; + let toTs = toMarker.ts; + + // TODO(charliea): Usually, we estimate that the clock sync marker is + // issued by the agent exactly in the middle of the controller's start and + // end timestamps. However, there's currently a bug in the Chrome serial + // code that's making the clock sync ack for BattOr take much longer to + // read than it should (by about 8ms). This is causing the above estimate + // of the controller's sync timestamp to be off by a substantial enough + // amount that it makes traces hard to read. For now, make an exception + // for BattOr and just use the controller's start timestamp as the sync + // time. In the medium term, we should fix the Chrome serial code in order + // to remove this special logic and get an even more accurate estimate. + if (fromMarker.domainId === ClockDomainId.BATTOR && + toMarker.duration > BATTOR_FAST_SYNC_THRESHOLD_MS) { + toTs = toMarker.startTs; + } else if (toMarker.domainId === ClockDomainId.BATTOR && + fromMarker.duration > BATTOR_FAST_SYNC_THRESHOLD_MS) { + fromTs = fromMarker.startTs; + } + + const tsShift = toTs - fromTs; + return new Transformer( + (ts) => ts + tsShift, fromMarker.duration + toMarker.duration); + }; + + return { + ClockDomainId, + ClockSyncManager, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager_test.html b/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager_test.html new file mode 100644 index 00000000000..a35049396de --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/clock_sync_manager_test.html @@ -0,0 +1,471 @@ +<!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/model/clock_sync_manager.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ClockDomainId = tr.model.ClockDomainId; + const ClockSyncManager = tr.model.ClockSyncManager; + + const testOptions = { + setUp() { + // Add a few testing clock domains to the list of permissible domains. + ClockDomainId.DOMAIN_1 = 'DOMAIN1'; + ClockDomainId.DOMAIN_2 = 'DOMAIN2'; + ClockDomainId.DOMAIN_3 = 'DOMAIN3'; + ClockDomainId.DOMAIN_4 = 'DOMAIN4'; + ClockDomainId.DOMAIN_5 = 'DOMAIN5'; + }, + + tearDown() { + delete ClockDomainId.DOMAIN_1; + delete ClockDomainId.DOMAIN_2; + delete ClockDomainId.DOMAIN_3; + delete ClockDomainId.DOMAIN_4; + delete ClockDomainId.DOMAIN_5; + } + }; + + test('addClockSyncMarker_throwsWithUnknownDomain', function() { + const mgr = new ClockSyncManager(); + + assert.throws(function() { + mgr.addClockSyncMarker('unknown', 'sync1', 100, 200); + }, '"unknown" is not in the list of known clock domain IDs.'); + }, testOptions); + + + test('addClockSyncMarker_throwsWhenSelfSyncing', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 200); + + assert.throws(function() { + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 200, 300); + }, 'A clock domain cannot sync with itself.'); + }, testOptions); + + test('addClockSyncMarker_throwsWhenAddingThirdSyncMarkerToSync', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 100); + + assert.throws(function() { + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync1', 100); + }, 'Clock sync with ID "sync1" is already complete - cannot add a third ' + + 'clock sync marker to it.'); + }, testOptions); + + test('addClockSyncMarker_throwsAfterGetModelTimeTransformer', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 100); + + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1); + + assert.throws(function() { + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 100); + }, 'Cannot add new clock sync markers after getting a model time ' + + 'transformer.'); + }, testOptions); + + test('getModelTimeTransformer_noMarkers', function() { + const mgr = new ClockSyncManager(); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 100); + }, testOptions); + + test('getModelTimeTransformer_noMarkersSecondDomainThrows', function() { + const mgr = new ClockSyncManager(); + + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1); + + assert.throws(function() { + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2); + }, 'No clock sync markers exist pairing clock domain "DOMAIN2" with' + + ' target clock domain "DOMAIN1".'); + }, testOptions); + + test('getModelTimeTransformer_noMarkersChromeLegacyFirst', function() { + const mgr = new ClockSyncManager(); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.UNKNOWN_CHROME_LEGACY)(100), + 100); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.LINUX_CLOCK_MONOTONIC)(100), + 100); + }, testOptions); + + test('getModelTimeTransformer_noMarkersChromeLegacySecond', function() { + const mgr = new ClockSyncManager(); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.LINUX_CLOCK_MONOTONIC)(100), + 100); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.UNKNOWN_CHROME_LEGACY)(100), + 100); + }, testOptions); + + test('getModelTimeTransformer_oneMarker', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 100); + }, testOptions); + + test('getModelTimeTransformer_oneCompleteSync', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(350), 100); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 100); + }, testOptions); + + test('getModelTimeTransformer_oneCompleteSyncWithChromeLegacyBefore', + function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker( + ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350); + + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.UNKNOWN_CHROME_LEGACY)(350), + 350); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350); + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.LINUX_CLOCK_MONOTONIC)(350), + 350); + }, testOptions); + + test('getModelTimeTransformer_oneCompleteSyncWithChromeLegacyAfter', + function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker( + ClockDomainId.LINUX_CLOCK_MONOTONIC, 'sync1', 350); + + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.UNKNOWN_CHROME_LEGACY)(350), + 350); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350); + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.LINUX_CLOCK_MONOTONIC)(350), + 350); + }, testOptions); + + test('getModelTimeTransformer_twoCompleteSyncs', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350); + + const tx = mgr.getTransformerBetween_('DOMAIN1', 'DOMAIN3'); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync2', 200); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync2', 250); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(350), 100); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 100); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_3)(250), 200); + }, testOptions); + + test('getModelTimeTransformer_twoSyncMarkersWithEndTs', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 200); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(350), 150); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(150), 150); + }, testOptions); + + test('getModelTimeTransformer_indirectlyConnectedGraph', + function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 200); + + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 200); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync2', 300); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 100); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_3)(300), 100); + }, testOptions); + + test('getModelTimeTransformer_usesPathWithLeastError', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync2', 100, 150); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 100); + + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 200); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 100); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(125), 125); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(100), 125); + }, testOptions); + + // NOTE: This is the same test as the above, but reversed to ensure that + // the result didn't stem from some ordering coincidence. + test('getModelTimeTransformer_usesPathWithLeastErrorReverse', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 150); + + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync2', 100, 200); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(125), 125); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(100), 125); + }, testOptions); + + test('getModelTimeTransformer_battorSyncUsesNormalTimestampWhenFast', + function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 102); + mgr.addClockSyncMarker(ClockDomainId.BATTOR, 'sync1', 350); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(101), + 350); + }, testOptions); + + test('getModelTimeTransformer_battorSyncUsesChromeLegacyStartTsWhenTooSlow', + function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100, 200); + mgr.addClockSyncMarker(ClockDomainId.BATTOR, 'sync1', 350); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), + 350); + }, testOptions); + + test('getModelTimeTransformer_prefersChromeLegacyDomain', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), + 350); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.UNKNOWN_CHROME_LEGACY)(350), + 350); + }, testOptions); + + test('getModelTimeTransformer_collapsesUnknownChromeLegacyDomainLinux', + function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker( + ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350); + + mgr.addClockSyncMarker( + ClockDomainId.LINUX_CLOCK_MONOTONIC, 'sync2', 350); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 450); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(450), 350); + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.LINUX_CLOCK_MONOTONIC)(350), + 350); + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.UNKNOWN_CHROME_LEGACY)(350), + 350); + }, testOptions); + + test('getModelTimeTransformer_collapsesUnknownChromeLegacyDomainMac', + function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker( + ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350); + + mgr.addClockSyncMarker( + ClockDomainId.MAC_MACH_ABSOLUTE_TIME, 'sync2', 350); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 450); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(450), 350); + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.MAC_MACH_ABSOLUTE_TIME)(350), + 350); + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.UNKNOWN_CHROME_LEGACY)(350), + 350); + }, testOptions); + + test('getModelTimeTransformer_collapsesUnknownChromeLegacyDomainWinLoRes', + function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker( + ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350); + + mgr.addClockSyncMarker( + ClockDomainId.WIN_ROLLOVER_PROTECTED_TIME_GET_TIME, 'sync2', 350); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 450); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(450), 350); + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.WIN_ROLLOVER_PROTECTED_TIME_GET_TIME)(350), + 350); + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.UNKNOWN_CHROME_LEGACY)(350), + 350); + }, testOptions); + + test('getModelTimeTransformer_collapsesChromeLegacyDomainWinHiRes', + function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker( + ClockDomainId.UNKNOWN_CHROME_LEGACY, 'sync1', 350); + + mgr.addClockSyncMarker(ClockDomainId.WIN_QPC, 'sync2', 350); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 450); + + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_1)(100), 350); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_2)(450), 350); + assert.strictEqual( + mgr.getModelTimeTransformer(ClockDomainId.WIN_QPC)(350), 350); + assert.strictEqual( + mgr.getModelTimeTransformer( + ClockDomainId.UNKNOWN_CHROME_LEGACY)(350), + 350); + }, testOptions); + + test('getModelTimeTransformer_throwsWithTwoDistinctGraphs', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 100); + + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync2', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync2', 100); + + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_4, 'sync3', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_5, 'sync3', 100); + + assert.throws(function() { + mgr.getModelTimeTransformer(ClockDomainId.DOMAIN_5); + }, 'Unable to select a master clock domain because no path can be found ' + + 'from "DOMAIN1" to "DOMAIN4"'); + }, testOptions); + + test('computeDotGraph_noMarkers', function() { + const mgr = new ClockSyncManager(); + + assert.strictEqual(mgr.computeDotGraph(), [ + 'graph {', + '}' + ].join('\n')); + }, testOptions); + + test('computeDotGraph_oneMarker', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + + assert.strictEqual(mgr.computeDotGraph(), [ + 'graph {', + ' DOMAIN1[shape=box]', + ' "sync1" -- DOMAIN1 [label="[100, 100]"]', + '}' + ].join('\n')); + }, testOptions); + + test('computeDotGraph_oneCompleteSync', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350); + + assert.strictEqual(mgr.computeDotGraph(), [ + 'graph {', + ' DOMAIN1[shape=box]', + ' DOMAIN2[shape=box]', + ' "sync1" -- DOMAIN1 [label="[100, 100]"]', + ' "sync1" -- DOMAIN2 [label="[350, 350]"]', + '}' + ].join('\n')); + }, testOptions); + + test('computeDotGraph_twoCompleteSyncs', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync2', 0, 50); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync2', 400); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350); + + assert.strictEqual(mgr.computeDotGraph(), [ + 'graph {', + ' DOMAIN1[shape=box]', + ' DOMAIN2[shape=box]', + ' DOMAIN3[shape=box]', + ' "sync1" -- DOMAIN1 [label="[100, 100]"]', + ' "sync1" -- DOMAIN2 [label="[350, 350]"]', + ' "sync2" -- DOMAIN2 [label="[0, 50]"]', + ' "sync2" -- DOMAIN3 [label="[400, 400]"]', + '}' + ].join('\n')); + }, testOptions); + + test('computeDotGraph_oneCompleteSyncOneIncompleteSync', function() { + const mgr = new ClockSyncManager(); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_1, 'sync1', 100); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_2, 'sync1', 350); + mgr.addClockSyncMarker(ClockDomainId.DOMAIN_3, 'sync2', 450); + + assert.strictEqual(mgr.computeDotGraph(), [ + 'graph {', + ' DOMAIN1[shape=box]', + ' DOMAIN2[shape=box]', + ' DOMAIN3[shape=box]', + ' "sync1" -- DOMAIN1 [label="[100, 100]"]', + ' "sync1" -- DOMAIN2 [label="[350, 350]"]', + ' "sync2" -- DOMAIN3 [label="[450, 450]"]', + '}' + ].join('\n')); + }, testOptions); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/comment_box_annotation.html b/chromium/third_party/catapult/tracing/tracing/model/comment_box_annotation.html new file mode 100644 index 00000000000..16d2cb098bc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/comment_box_annotation.html @@ -0,0 +1,60 @@ +<!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/annotation.html"> +<link rel="import" href="/tracing/model/location.html"> +<link rel="import" href="/tracing/model/rect_annotation.html"> +<link rel="import" href="/tracing/ui/annotations/comment_box_annotation_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + function CommentBoxAnnotation(location, text) { + tr.model.Annotation.apply(this, arguments); + + this.location = location; + this.text = text; + } + + CommentBoxAnnotation.fromDict = function(dict) { + const args = dict.args; + const location = + new tr.model.Location(args.location.xWorld, args.location.yComponents); + return new tr.model.CommentBoxAnnotation(location, args.text); + }; + + CommentBoxAnnotation.prototype = { + __proto__: tr.model.Annotation.prototype, + + onRemove() { + this.view_.removeTextArea(); + }, + + toDict() { + return { + typeName: 'comment_box', + args: { + text: this.text, + location: this.location.toDict() + } + }; + }, + + createView_(viewport) { + return new tr.ui.annotations.CommentBoxAnnotationView(viewport, this); + } + }; + + tr.model.Annotation.register( + CommentBoxAnnotation, {typeName: 'comment_box'}); + + return { + CommentBoxAnnotation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/compound_event_selection_state.html b/chromium/third_party/catapult/tracing/tracing/model/compound_event_selection_state.html new file mode 100644 index 00000000000..cefc2247095 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/compound_event_selection_state.html @@ -0,0 +1,39 @@ +<!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"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + /** + * Indicates how much of a compound-event is selected [if any]. + * + * The CompoundEventSelectionState enum is used with events that are + * directly selectable, but also have associated events, too, that can be + * selected. In this situation, there are a variety of different + * selected states other than just "yes, no". This enum encodes those + * various possible states. + */ + const CompoundEventSelectionState = { + // Basic bit states. + NOT_SELECTED: 0, + EVENT_SELECTED: 0x1, + SOME_ASSOCIATED_EVENTS_SELECTED: 0x2, + ALL_ASSOCIATED_EVENTS_SELECTED: 0x4, + + // Common combinations. + EVENT_AND_SOME_ASSOCIATED_SELECTED: 0x1 | 0x2, + EVENT_AND_ALL_ASSOCIATED_SELECTED: 0x1 | 0x4 + }; + + return { + CompoundEventSelectionState, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/constants.html b/chromium/third_party/catapult/tracing/tracing/model/constants.html new file mode 100644 index 00000000000..0bf134ec7fe --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/constants.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2012 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.model', function() { + return { + // Since the PID of the browser process is not known to the child processes, + // we let them use "pid_ref = -1" to reference an object created in the + // browser process. + BROWSER_PROCESS_PID_REF: -1, + + // The default scope of object events, when not explicitly specified. + OBJECT_DEFAULT_SCOPE: 'ptr', + + // Event phases that have process-local IDs, unless a global ID is + // explicitly specified. + LOCAL_ID_PHASES: new Set(['N', 'D', 'O', '(', ')']) + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump.html b/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump.html new file mode 100644 index 00000000000..1078b764862 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump.html @@ -0,0 +1,92 @@ +<!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/timed_event.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the ContainerMemoryDump class. + */ +tr.exportTo('tr.model', function() { + /** + * The ContainerMemoryDump represents an abstract container memory dump. + * @constructor + */ + function ContainerMemoryDump(start) { + tr.model.TimedEvent.call(this, start); + + this.levelOfDetail = undefined; + + this.memoryAllocatorDumps_ = undefined; + this.memoryAllocatorDumpsByFullName_ = undefined; + } + + /** + * Memory dump level of detail. See base::trace_event::MemoryDumpLevelOfDetail + * in the Chromium repository. + * + * @enum + */ + ContainerMemoryDump.LevelOfDetail = { + BACKGROUND: 0, + LIGHT: 1, + DETAILED: 2 + }; + + ContainerMemoryDump.prototype = { + __proto__: tr.model.TimedEvent.prototype, + + shiftTimestampsForward(amount) { + this.start += amount; + }, + + get memoryAllocatorDumps() { + return this.memoryAllocatorDumps_; + }, + + set memoryAllocatorDumps(memoryAllocatorDumps) { + this.memoryAllocatorDumps_ = memoryAllocatorDumps; + this.forceRebuildingMemoryAllocatorDumpByFullNameIndex(); + }, + + getMemoryAllocatorDumpByFullName(fullName) { + if (this.memoryAllocatorDumps_ === undefined) return undefined; + + // Lazily generate the index if necessary. + if (this.memoryAllocatorDumpsByFullName_ === undefined) { + const index = {}; + function addDumpsToIndex(dumps) { + dumps.forEach(function(dump) { + index[dump.fullName] = dump; + addDumpsToIndex(dump.children); + }); + } + addDumpsToIndex(this.memoryAllocatorDumps_); + this.memoryAllocatorDumpsByFullName_ = index; + } + + return this.memoryAllocatorDumpsByFullName_[fullName]; + }, + + forceRebuildingMemoryAllocatorDumpByFullNameIndex() { + // Clear the index and generate it lazily. + this.memoryAllocatorDumpsByFullName_ = undefined; + }, + + iterateRootAllocatorDumps(fn, opt_this) { + if (this.memoryAllocatorDumps === undefined) return; + this.memoryAllocatorDumps.forEach(fn, opt_this || this); + } + }; + + return { + ContainerMemoryDump, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump_test.html b/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump_test.html new file mode 100644 index 00000000000..a6b2009c96e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/container_memory_dump_test.html @@ -0,0 +1,181 @@ +<!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/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/container_memory_dump.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ContainerMemoryDump = tr.model.ContainerMemoryDump; + const MemoryAllocatorDump = tr.model.MemoryAllocatorDump; + const Scalar = tr.b.Scalar; + const unitlessNumber_smallerIsBetter = + tr.b.Unit.byName.unitlessNumber_smallerIsBetter; + const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump; + const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump; + + test('memoryAllocatorDumps_undefined', function() { + const md = new ContainerMemoryDump(42); + + assert.isUndefined(md.memoryAllocatorDumps); + assert.isUndefined(md.getMemoryAllocatorDumpByFullName('malloc')); + }); + + test('memoryAllocatorDumps_zero', function() { + const md = new ContainerMemoryDump(42); + md.memoryAllocatorDumps = []; + + assert.lengthOf(md.memoryAllocatorDumps, 0); + assert.isUndefined(md.getMemoryAllocatorDumpByFullName('malloc')); + }); + + test('memoryAllocatorDumps_flat', function() { + const md = new ContainerMemoryDump(42); + + const oilpanDump = newAllocatorDump(md, 'oilpan', { + size: 1024, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7), + inner_size: 768 + }); + const v8Dump = newAllocatorDump(md, 'v8', { + size: 2048, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 15), + inner_size: 1999 + }); + md.memoryAllocatorDumps = [oilpanDump, v8Dump]; + + assert.lengthOf(md.memoryAllocatorDumps, 2); + assert.strictEqual(md.memoryAllocatorDumps[0], oilpanDump); + assert.strictEqual(md.memoryAllocatorDumps[1], v8Dump); + + assert.strictEqual( + md.getMemoryAllocatorDumpByFullName('oilpan'), oilpanDump); + assert.strictEqual(md.getMemoryAllocatorDumpByFullName('v8'), v8Dump); + assert.isUndefined(md.getMemoryAllocatorDumpByFullName('malloc')); + }); + + test('memoryAllocatorDumps_nested', function() { + const md = new ContainerMemoryDump(42); + + const oilpanDump = newAllocatorDump(md, 'oilpan', { + size: 1024, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7), + inner_size: 768 + }); + + const oilpanBucket1Dump = addChildDump(oilpanDump, 'bucket1', { + size: 512, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 3), + inner_size: 256 + }); + + const oilpanBucket2Dump = addChildDump(oilpanDump, 'bucket2', { + size: 512, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 4), + inner_size: 512 + }); + + const oilpanBucket2StringsDump = addChildDump( + oilpanBucket2Dump, 'strings', { + size: 512, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 4), + inner_size: 512 + }); + + const v8Dump = newAllocatorDump(md, 'v8', { + size: 2048, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 15), + inner_size: 1999 + }); + + md.memoryAllocatorDumps = [oilpanDump, v8Dump]; + + assert.lengthOf(md.memoryAllocatorDumps, 2); + assert.strictEqual(md.memoryAllocatorDumps[0], oilpanDump); + assert.strictEqual(md.memoryAllocatorDumps[1], v8Dump); + + assert.strictEqual( + md.getMemoryAllocatorDumpByFullName('oilpan'), oilpanDump); + assert.strictEqual(md.getMemoryAllocatorDumpByFullName('oilpan/bucket1'), + oilpanBucket1Dump); + assert.strictEqual(md.getMemoryAllocatorDumpByFullName('oilpan/bucket2'), + oilpanBucket2Dump); + assert.strictEqual( + md.getMemoryAllocatorDumpByFullName('oilpan/bucket2/strings'), + oilpanBucket2StringsDump); + assert.strictEqual(md.getMemoryAllocatorDumpByFullName('v8'), v8Dump); + assert.isUndefined(md.getMemoryAllocatorDumpByFullName('malloc')); + }); + + test('iterateRootAllocatorDumps', function() { + const containerDump = new ContainerMemoryDump(42); + + const oilpanDump = new MemoryAllocatorDump(containerDump, 'oilpan'); + const v8Dump = new MemoryAllocatorDump(containerDump, 'v8'); + addChildDump(v8Dump, 'heaps'); + + containerDump.memoryAllocatorDumps = [oilpanDump, v8Dump]; + + const visitedAllocatorDumps = []; + containerDump.iterateRootAllocatorDumps( + function(dump) { this.visitedAllocatorDumps.push(dump); }, + { visitedAllocatorDumps }); + assert.sameMembers(visitedAllocatorDumps, [oilpanDump, v8Dump]); + }); + + test('forceRebuildingMemoryAllocatorDumpByFullNameIndex', function() { + const containerDump = new ContainerMemoryDump(42); + + const v8Dump = new MemoryAllocatorDump(containerDump, 'v8'); + const v8HeapsDump = addChildDump(v8Dump, 'heaps'); + const v8HeapSmallDump = addChildDump(v8HeapsDump, 'S'); + + // Setting the memory allocator dumps should update the index properly. + containerDump.memoryAllocatorDumps = [v8Dump]; + assert.strictEqual( + containerDump.getMemoryAllocatorDumpByFullName('v8'), v8Dump); + assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName( + 'v8/heaps'), v8HeapsDump); + assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName( + 'v8/heaps/S'), v8HeapSmallDump); + + // Add a second grandchild (v8/heaps/L). + const v8HeapLargeDump = addChildDump(v8HeapsDump, 'L'); + + // Setting the memory allocator dumps again should update the index + // properly again. + containerDump.memoryAllocatorDumps = [v8Dump]; + assert.strictEqual( + containerDump.getMemoryAllocatorDumpByFullName('v8'), v8Dump); + assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName( + 'v8/heaps'), v8HeapsDump); + assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName( + 'v8/heaps/S'), v8HeapSmallDump); + assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName( + 'v8/heaps/L'), v8HeapLargeDump); + + // Remove the first grandchild (v8/heaps/S). + v8HeapsDump.children.splice(0, 1); + + // Force rebuilding the index and check that it was updated properly. + containerDump.forceRebuildingMemoryAllocatorDumpByFullNameIndex(); + assert.strictEqual( + containerDump.getMemoryAllocatorDumpByFullName('v8'), v8Dump); + assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName( + 'v8/heaps'), v8HeapsDump); + assert.isUndefined(containerDump.getMemoryAllocatorDumpByFullName( + 'v8/heaps/S')); + assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName( + 'v8/heaps/L'), v8HeapLargeDump); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/counter.html b/chromium/third_party/catapult/tracing/tracing/model/counter.html new file mode 100644 index 00000000000..5b276984fd2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/counter.html @@ -0,0 +1,190 @@ +<!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/guid.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/model/counter_series.html"> +<link rel="import" href="/tracing/model/event_container.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + /** + * A container holding all series of a given type of measurement. + * + * As an example, if we're measuring the throughput of data sent over several + * USB connections, the throughput of each cable might be added as a separate + * series to a single counter. + * + * @constructor + * @extends {EventContainer} + */ + function Counter(parent, id, category, name) { + tr.model.EventContainer.call(this); + + this.parent_ = parent; + this.id_ = id; + this.category_ = category || ''; + this.name_ = name; + + this.series_ = []; + this.totals = []; + } + + Counter.prototype = { + __proto__: tr.model.EventContainer.prototype, + + get parent() { + return this.parent_; + }, + + get id() { + return this.id_; + }, + + get category() { + return this.category_; + }, + + get name() { + return this.name_; + }, + + * childEvents() { + }, + + * childEventContainers() { + yield* this.series; + }, + + set timestamps(arg) { + throw new Error('Bad counter API. No cookie.'); + }, + + set seriesNames(arg) { + throw new Error('Bad counter API. No cookie.'); + }, + + set seriesColors(arg) { + throw new Error('Bad counter API. No cookie.'); + }, + + set samples(arg) { + throw new Error('Bad counter API. No cookie.'); + }, + + addSeries(series) { + series.counter = this; + series.seriesIndex = this.series_.length; + this.series_.push(series); + return series; + }, + + getSeries(idx) { + return this.series_[idx]; + }, + + get series() { + return this.series_; + }, + + get numSeries() { + return this.series_.length; + }, + + get numSamples() { + if (this.series_.length === 0) return 0; + return this.series_[0].length; + }, + + get timestamps() { + if (this.series_.length === 0) return []; + return this.series_[0].timestamps; + }, + + /** + * Obtains min, max, avg, values, start, and end for different series for + * a given counter + * getSampleStatistics([0,1]) + * The statistics objects that this returns are an array of objects, one + * object for each series for the counter in the form: + * {min: minVal, max: maxVal, avg: avgVal, start: startVal, end: endVal} + * + * @param {Array.<Number>} Indices to summarize. + * @return {Object} An array of statistics. Each element in the array + * has data for one of the series in the selected counter. + */ + getSampleStatistics(sampleIndices) { + sampleIndices.sort(); + + const ret = []; + this.series_.forEach(function(series) { + ret.push(series.getStatistics(sampleIndices)); + }); + return ret; + }, + + /** + * Shifts all the timestamps inside this counter forward by the amount + * specified. + */ + shiftTimestampsForward(amount) { + for (let i = 0; i < this.series_.length; ++i) { + this.series_[i].shiftTimestampsForward(amount); + } + }, + + /** + * Updates the bounds for this counter based on the samples it contains. + */ + updateBounds() { + this.totals = []; + this.maxTotal = 0; + this.bounds.reset(); + + if (this.series_.length === 0) return; + + const firstSeries = this.series_[0]; + const lastSeries = this.series_[this.series_.length - 1]; + + this.bounds.addValue(firstSeries.getTimestamp(0)); + this.bounds.addValue(lastSeries.getTimestamp(lastSeries.length - 1)); + + const numSeries = this.numSeries; + this.maxTotal = -Infinity; + + // Sum the samples at each timestamp. + // Note, this assumes that all series have all timestamps. + for (let i = 0; i < firstSeries.length; ++i) { + let total = 0; + this.series_.forEach(function(series) { + total += series.getSample(i).value; + this.totals.push(total); + }.bind(this)); + + this.maxTotal = Math.max(total, this.maxTotal); + } + } + }; + + /** + * Comparison between counters that orders by parent.compareTo, then name. + */ + Counter.compare = function(x, y) { + let tmp = x.parent.compareTo(y.parent); + if (tmp !== 0) return tmp; + tmp = x.name.localeCompare(y.name); + if (tmp === 0) return x.tid - y.tid; + return tmp; + }; + + return { + Counter, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/counter_sample.html b/chromium/third_party/catapult/tracing/tracing/model/counter_sample.html new file mode 100644 index 00000000000..3da38c349c7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/counter_sample.html @@ -0,0 +1,95 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/model/event_registry.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + /** + * The value of a given measurement at a given time. + * + * As an example, if we're measuring the throughput of data sent over a USB + * connection, each counter sample might represent the instantaneous + * throughput of the connection at a given time. + * + * @constructor + * @extends {Event} + */ + function CounterSample(series, timestamp, value) { + tr.model.Event.call(this); + this.series_ = series; + this.timestamp_ = timestamp; + this.value_ = value; + } + + CounterSample.groupByTimestamp = function(samples) { + const samplesByTimestamp = tr.b.groupIntoMap(samples, s => s.timestamp); + const timestamps = Array.from(samplesByTimestamp.keys()); + timestamps.sort(); + const groups = []; + for (const ts of timestamps) { + const group = samplesByTimestamp.get(ts); + group.sort((x, y) => x.series.seriesIndex - y.series.seriesIndex); + groups.push(group); + } + return groups; + }; + + CounterSample.prototype = { + __proto__: tr.model.Event.prototype, + + get series() { + return this.series_; + }, + + get timestamp() { + return this.timestamp_; + }, + + get value() { + return this.value_; + }, + + set timestamp(timestamp) { + this.timestamp_ = timestamp; + }, + + addBoundsToRange(range) { + range.addValue(this.timestamp); + }, + + getSampleIndex() { + return tr.b.findLowIndexInSortedArray( + this.series.timestamps, + function(x) { return x; }, + this.timestamp_); + }, + + get userFriendlyName() { + return 'Counter sample from ' + this.series_.title + ' at ' + + tr.b.Unit.byName.timeStampInMs.format(this.timestamp); + } + }; + + + tr.model.EventRegistry.register( + CounterSample, + { + name: 'counterSample', + pluralName: 'counterSamples' + }); + + return { + CounterSample, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/counter_sample_test.html b/chromium/third_party/catapult/tracing/tracing/model/counter_sample_test.html new file mode 100644 index 00000000000..d4ad8fcb0d4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/counter_sample_test.html @@ -0,0 +1,46 @@ +<!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/model/counter.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Counter = tr.model.Counter; + const CounterSeries = tr.model.CounterSeries; + const CounterSample = tr.model.CounterSample; + + test('groupByTimestamp', function() { + const counter = new Counter(); + const slice0 = counter.addSeries(new CounterSeries('x', 0)); + const slice1 = counter.addSeries(new CounterSeries('y', 1)); + + const slice0Sample0 = slice0.addCounterSample(0, 100); + const slice0Sample1 = slice1.addCounterSample(0, 200); + const slice1Sample0 = slice0.addCounterSample(1, 100); + const slice1Sample1 = slice1.addCounterSample(1, 200); + + const groups = CounterSample.groupByTimestamp([slice0Sample1, slice0Sample0, + slice1Sample1, slice1Sample0]); + assert.strictEqual(groups.length, 2); + assert.deepEqual(groups[0], [slice0Sample0, slice0Sample1]); + assert.deepEqual(groups[1], [slice1Sample0, slice1Sample1]); + }); + + test('getSampleIndex', function() { + const ctr = new Counter(null, 0, '', 'myCounter'); + const slice0 = new CounterSeries('a', 0); + ctr.addSeries(slice0); + + const slice0Sample0 = slice0.addCounterSample(0, 0); + const slice0Sample1 = slice0.addCounterSample(1, 100); + assert.strictEqual(slice0Sample0.getSampleIndex(), 0); + assert.strictEqual(slice0Sample1.getSampleIndex(), 1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/counter_series.html b/chromium/third_party/catapult/tracing/tracing/model/counter_series.html new file mode 100644 index 00000000000..b2ab8f4fe58 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/counter_series.html @@ -0,0 +1,124 @@ +<!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/model/counter_sample.html"> +<link rel="import" href="/tracing/model/event_container.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const CounterSample = tr.model.CounterSample; + + /** + * A container holding all samples of a given measurement over time. + * + * As an example, a counter series might measure the throughput of data sent + * over a USB connection, with each sample representing the instantaneous + * throughput of the connection. + * + * @constructor + * @extends {EventContainer} + */ + function CounterSeries(name, color) { + tr.model.EventContainer.call(this); + + this.name_ = name; + this.color_ = color; + + this.timestamps_ = []; + this.samples_ = []; + + // Set by counter.addSeries + this.counter = undefined; + this.seriesIndex = undefined; + } + + CounterSeries.prototype = { + __proto__: tr.model.EventContainer.prototype, + + get length() { + return this.timestamps_.length; + }, + + get name() { + return this.name_; + }, + + get color() { + return this.color_; + }, + + get samples() { + return this.samples_; + }, + + get timestamps() { + return this.timestamps_; + }, + + getSample(idx) { + return this.samples_[idx]; + }, + + getTimestamp(idx) { + return this.timestamps_[idx]; + }, + + addCounterSample(ts, val) { + const sample = new CounterSample(this, ts, val); + this.addSample(sample); + return sample; + }, + + addSample(sample) { + this.timestamps_.push(sample.timestamp); + this.samples_.push(sample); + }, + + getStatistics(sampleIndices) { + let sum = 0; + let min = Number.MAX_VALUE; + let max = -Number.MAX_VALUE; + + for (let i = 0; i < sampleIndices.length; ++i) { + const sample = this.getSample(sampleIndices[i]).value; + + sum += sample; + min = Math.min(sample, min); + max = Math.max(sample, max); + } + + return { + min, + max, + avg: (sum / sampleIndices.length), + start: this.getSample(sampleIndices[0]).value, + end: this.getSample(sampleIndices.length - 1).value + }; + }, + + shiftTimestampsForward(amount) { + for (let i = 0; i < this.timestamps_.length; ++i) { + this.timestamps_[i] += amount; + this.samples_[i].timestamp = this.timestamps_[i]; + } + }, + + * childEvents() { + yield* this.samples_; + }, + + * childEventContainers() { + } + }; + + return { + CounterSeries, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/counter_test.html b/chromium/third_party/catapult/tracing/tracing/model/counter_test.html new file mode 100644 index 00000000000..5c64e7c273d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/counter_test.html @@ -0,0 +1,124 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/counter.html"> +<link rel="import" href="/tracing/model/counter_series.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/process.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Counter = tr.model.Counter; + const CounterSeries = tr.model.CounterSeries; + const CounterSample = tr.model.CounterSample; + + const createCounterWithTwoSeries = function() { + const ctr = new Counter(null, 0, '', 'myCounter'); + const aSeries = new CounterSeries('a', 0); + const bSeries = new CounterSeries('b', 0); + ctr.addSeries(aSeries); + ctr.addSeries(bSeries); + + aSeries.addCounterSample(0, 5); + aSeries.addCounterSample(1, 6); + aSeries.addCounterSample(2, 5); + aSeries.addCounterSample(3, 7); + + bSeries.addCounterSample(0, 10); + bSeries.addCounterSample(1, 15); + bSeries.addCounterSample(2, 12); + bSeries.addCounterSample(3, 16); + + return ctr; + }; + + test('getSampleStatisticsWithSingleSelection', function() { + const ctr = createCounterWithTwoSeries(); + const ret = ctr.getSampleStatistics([0]); + + assert.strictEqual(ret[0].min, 5); + assert.strictEqual(ret[0].max, 5); + assert.strictEqual(ret[0].avg, 5); + assert.strictEqual(ret[0].start, 5); + assert.strictEqual(ret[0].end, 5); + + assert.strictEqual(ret[1].min, 10); + assert.strictEqual(ret[1].max, 10); + assert.strictEqual(ret[1].avg, 10); + assert.strictEqual(ret[1].start, 10); + assert.strictEqual(ret[1].end, 10); + }); + + test('getSampleStatisticsWithMultipleSelections', function() { + const ctr = createCounterWithTwoSeries(); + const ret = ctr.getSampleStatistics([0, 1]); + + assert.strictEqual(ret[0].min, 5); + assert.strictEqual(ret[0].max, 6); + assert.strictEqual(ret[0].avg, (5 + 6) / 2); + assert.strictEqual(ret[0].start, 5); + assert.strictEqual(ret[0].end, 6); + + assert.strictEqual(ret[1].min, 10); + assert.strictEqual(ret[1].max, 15); + assert.strictEqual(ret[1].avg, (10 + 15) / 2); + assert.strictEqual(ret[1].start, 10); + assert.strictEqual(ret[1].end, 15); + }); + + test('getSampleStatisticsWithOutofOrderIndices', function() { + const ctr = createCounterWithTwoSeries(); + const ret = ctr.getSampleStatistics([1, 0]); + + assert.strictEqual(ret[0].min, 5); + assert.strictEqual(ret[0].max, 6); + assert.strictEqual(ret[0].avg, (5 + 6) / 2); + assert.strictEqual(ret[0].start, 5); + assert.strictEqual(ret[0].end, 6); + + assert.strictEqual(ret[1].min, 10); + assert.strictEqual(ret[1].max, 15); + assert.strictEqual(ret[1].avg, (10 + 15) / 2); + assert.strictEqual(ret[1].start, 10); + assert.strictEqual(ret[1].end, 15); + }); + + test('getSampleStatisticsWithAllSelections', function() { + const ctr = createCounterWithTwoSeries(); + const ret = ctr.getSampleStatistics([1, 0, 2, 3]); + + assert.strictEqual(ret[0].min, 5); + assert.strictEqual(ret[0].max, 7); + assert.strictEqual(ret[0].avg, (5 + 6 + 5 + 7) / 4); + assert.strictEqual(ret[0].start, 5); + assert.strictEqual(ret[0].end, 7); + + assert.strictEqual(ret[1].min, 10); + assert.strictEqual(ret[1].max, 16); + assert.strictEqual(ret[1].avg, (10 + 15 + 12 + 16) / 4); + assert.strictEqual(ret[1].start, 10); + assert.strictEqual(ret[1].end, 16); + }); + + test('testCounterSortRemainInOrder', function() { + const model = new tr.Model(); + const process = new tr.model.Process(model, 4); + const ctr1 = new Counter(process, 0, '', 'a'); + const ctr2 = new Counter(process, 0, '', 'b'); + + const array = [ctr1, ctr2]; + array.sort(tr.model.Counter.compare); + + assert.strictEqual(array[0], ctr1); + assert.strictEqual(array[1], ctr2); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/cpu.html b/chromium/third_party/catapult/tracing/tracing/model/cpu.html new file mode 100644 index 00000000000..beb2f63e7cf --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/cpu.html @@ -0,0 +1,282 @@ +<!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/math/range.html"> +<link rel="import" href="/tracing/model/counter.html"> +<link rel="import" href="/tracing/model/cpu_slice.html"> +<link rel="import" href="/tracing/model/process_base.html"> +<link rel="import" href="/tracing/model/thread_time_slice.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Cpu class. + */ +tr.exportTo('tr.model', function() { + const ColorScheme = tr.b.ColorScheme; + const Counter = tr.model.Counter; + const CpuSlice = tr.model.CpuSlice; + + /** + * The Cpu represents a Cpu from the kernel's point of view. + * @constructor + */ + function Cpu(kernel, number) { + if (kernel === undefined || number === undefined) { + throw new Error('Missing arguments'); + } + this.kernel = kernel; + this.cpuNumber = number; + this.slices = []; + this.counters = {}; + this.bounds_ = new tr.b.math.Range(); + this.samples_ = undefined; // Set during createSubSlices + + // Start timestamp of the last active thread. + this.lastActiveTimestamp_ = undefined; + + // Identifier of the last active thread. On Linux, it's a pid while on + // Windows it's a thread id. + this.lastActiveThread_ = undefined; + + // Name and arguments of the last active thread. + this.lastActiveName_ = undefined; + this.lastActiveArgs_ = undefined; + } + + Cpu.prototype = { + __proto__: tr.model.EventContainer.prototype, + + get samples() { + return this.samples_; + }, + + get userFriendlyName() { + return 'CPU ' + this.cpuNumber; + }, + + * findTopmostSlicesInThisContainer(eventPredicate, opt_this) { + // All CpuSlices are toplevel since CpuSlices do not nest. + for (const s of this.slices) { + yield* s.findTopmostSlicesRelativeToThisSlice( + eventPredicate, opt_this); + } + }, + + * childEvents() { + yield* this.slices; + + if (this.samples_) { + yield* this.samples_; + } + }, + + * childEventContainers() { + yield* Object.values(this.counters); + }, + + /** + * @return {Counter} The counter on this CPU with the given category/name + * combination, creating it if it doesn't exist. + */ + getOrCreateCounter(cat, name) { + const id = cat + '.' + name; + if (!this.counters[id]) { + this.counters[id] = new Counter(this, id, cat, name); + } + return this.counters[id]; + }, + + /** + * @return {Counter} the counter on this CPU with the given category/name + * combination, or undefined if it doesn't exist. + */ + getCounter(cat, name) { + const id = cat + '.' + name; + if (!this.counters[id]) { + return undefined; + } + return this.counters[id]; + }, + + /** + * Shifts all the timestamps inside this CPU forward by the amount + * specified. + */ + shiftTimestampsForward(amount) { + for (let sI = 0; sI < this.slices.length; sI++) { + this.slices[sI].start = (this.slices[sI].start + amount); + } + for (const id in this.counters) { + this.counters[id].shiftTimestampsForward(amount); + } + }, + + /** + * Updates the range based on the current slices attached to the cpu. + */ + updateBounds() { + this.bounds_.reset(); + if (this.slices.length) { + this.bounds_.addValue(this.slices[0].start); + this.bounds_.addValue(this.slices[this.slices.length - 1].end); + } + for (const id in this.counters) { + this.counters[id].updateBounds(); + this.bounds_.addRange(this.counters[id].bounds); + } + if (this.samples_ && this.samples_.length) { + this.bounds_.addValue(this.samples_[0].start); + this.bounds_.addValue( + this.samples_[this.samples_.length - 1].end); + } + }, + + createSubSlices() { + this.samples_ = this.kernel.model.samples.filter(function(sample) { + return sample.cpu === this; + }, this); + }, + + addCategoriesToDict(categoriesDict) { + for (let i = 0; i < this.slices.length; i++) { + categoriesDict[this.slices[i].category] = true; + } + for (const id in this.counters) { + categoriesDict[this.counters[id].category] = true; + } + for (let i = 0; i < this.samples_.length; i++) { + categoriesDict[this.samples_[i].category] = true; + } + }, + + /* + * Returns the index of the slice in the CPU's slices, or undefined. + */ + indexOf(cpuSlice) { + const i = tr.b.findLowIndexInSortedArray( + this.slices, + function(slice) { return slice.start; }, + cpuSlice.start); + if (this.slices[i] !== cpuSlice) return undefined; + return i; + }, + + /** + * Closes the thread running on the CPU. |endTimestamp| is the timestamp + * at which the thread was unscheduled. |args| is merged with the arguments + * specified when the thread was initially scheduled. + */ + closeActiveThread(endTimestamp, args) { + // Don't generate a slice if the last active thread is the idle task. + if (this.lastActiveThread_ === undefined || + this.lastActiveThread_ === 0) { + return; + } + + if (endTimestamp < this.lastActiveTimestamp_) { + throw new Error('The end timestamp of a thread running on CPU ' + + this.cpuNumber + ' is before its start timestamp.'); + } + + // Merge |args| with |this.lastActiveArgs_|. If a key is in both + // dictionaries, the value from |args| is used. + for (const key in args) { + this.lastActiveArgs_[key] = args[key]; + } + + const duration = endTimestamp - this.lastActiveTimestamp_; + const slice = new tr.model.CpuSlice( + '', this.lastActiveName_, + ColorScheme.getColorIdForGeneralPurposeString(this.lastActiveName_), + this.lastActiveTimestamp_, + this.lastActiveArgs_, + duration); + slice.cpu = this; + this.slices.push(slice); + + // Clear the last state. + this.lastActiveTimestamp_ = undefined; + this.lastActiveThread_ = undefined; + this.lastActiveName_ = undefined; + this.lastActiveArgs_ = undefined; + }, + + switchActiveThread(timestamp, oldThreadArgs, newThreadId, + newThreadName, newThreadArgs) { + // Close the previous active thread and generate a slice. + this.closeActiveThread(timestamp, oldThreadArgs); + + // Keep track of the new thread. + this.lastActiveTimestamp_ = timestamp; + this.lastActiveThread_ = newThreadId; + this.lastActiveName_ = newThreadName; + this.lastActiveArgs_ = newThreadArgs; + }, + + /** + * Returns the frequency statistics for this CPU; + * the returned object contains the frequencies as keys, + * and the duration at this frequency in milliseconds as the value, + * for the range that was specified. + */ + getFreqStatsForRange(range) { + const stats = {}; + + function addStatsForFreq(freqSample, index) { + // Counters don't have an explicit end or duration; + // calculate the end by looking at the starting point + // of the next value in the series, or if that doesn't + // exist, assume this frequency is held until the end. + const freqEnd = (index < freqSample.series_.length - 1) ? + freqSample.series_.samples_[index + 1].timestamp : range.max; + + const freqRange = tr.b.math.Range.fromExplicitRange( + freqSample.timestamp, freqEnd); + const intersection = freqRange.findIntersection(range); + if (!(freqSample.value in stats)) { + stats[freqSample.value] = 0; + } + stats[freqSample.value] += intersection.duration; + } + + const freqCounter = this.getCounter('', 'Clock Frequency'); + if (freqCounter !== undefined) { + const freqSeries = freqCounter.getSeries(0); + if (!freqSeries) return; + + tr.b.iterateOverIntersectingIntervals(freqSeries.samples_, + function(x) { return x.timestamp; }, + function(x, index) { + if (index < freqSeries.length - 1) { + return freqSeries.samples_[index + 1].timestamp; + } + return range.max; + }, + range.min, + range.max, + addStatsForFreq); + } + + return stats; + } + }; + + /** + * Comparison between processes that orders by cpuNumber. + */ + Cpu.compare = function(x, y) { + return x.cpuNumber - y.cpuNumber; + }; + + + return { + Cpu, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/cpu_slice.html b/chromium/third_party/catapult/tracing/tracing/model/cpu_slice.html new file mode 100644 index 00000000000..6faf5d143fa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/cpu_slice.html @@ -0,0 +1,68 @@ +<!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/math/range.html"> +<link rel="import" href="/tracing/model/thread_time_slice.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the CpuSlice class. + */ +tr.exportTo('tr.model', function() { + const Slice = tr.model.Slice; + + /** + * A CpuSlice represents a slice of time on a CPU. + * + * @constructor + */ + function CpuSlice(cat, title, colorId, start, args, opt_duration) { + Slice.apply(this, arguments); + this.threadThatWasRunning = undefined; + this.cpu = undefined; + } + + CpuSlice.prototype = { + __proto__: Slice.prototype, + + get analysisTypeName() { + return 'tr.ui.analysis.CpuSlice'; + }, + + getAssociatedTimeslice() { + if (!this.threadThatWasRunning) { + return undefined; + } + const timeSlices = this.threadThatWasRunning.timeSlices; + for (let i = 0; i < timeSlices.length; i++) { + const timeSlice = timeSlices[i]; + if (timeSlice.start !== this.start) { + continue; + } + if (timeSlice.duration !== this.duration) { + continue; + } + return timeSlice; + } + return undefined; + } + }; + + tr.model.EventRegistry.register( + CpuSlice, + { + name: 'cpuSlice', + pluralName: 'cpuSlices' + }); + + return { + CpuSlice, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/cpu_test.html b/chromium/third_party/catapult/tracing/tracing/model/cpu_test.html new file mode 100644 index 00000000000..49f01dff4f3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/cpu_test.html @@ -0,0 +1,206 @@ +<!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/math/range.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ColorScheme = tr.b.ColorScheme; + const Cpu = tr.model.Cpu; + const newThreadSlice = tr.c.TestUtils.newThreadSlice; + + test('cpuBounds_Empty', function() { + const cpu = new Cpu({}, 1); + cpu.updateBounds(); + assert.isUndefined(cpu.bounds.min); + assert.isUndefined(cpu.bounds.max); + }); + + test('cpuBounds_OneSlice', function() { + const cpu = new Cpu({}, 1); + cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3})); + cpu.updateBounds(); + assert.strictEqual(cpu.bounds.min, 1); + assert.strictEqual(cpu.bounds.max, 4); + }); + + test('getOrCreateCounter', function() { + const cpu = new Cpu({}, 1); + const ctrBar = cpu.getOrCreateCounter('foo', 'bar'); + const ctrBar2 = cpu.getOrCreateCounter('foo', 'bar'); + assert.strictEqual(ctrBar, ctrBar2); + }); + + test('shiftTimestampsForward', function() { + const cpu = new Cpu({}, 1); + const ctr = cpu.getOrCreateCounter('foo', 'bar'); + cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3})); + let shiftCount = 0; + ctr.shiftTimestampsForward = function(ts) { + if (ts === 0.32) { + shiftCount++; + } + }; + cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3})); + cpu.shiftTimestampsForward(0.32); + assert.strictEqual(1, shiftCount); + assert.strictEqual(cpu.slices[0].start, 1.32); + }); + + + function newCpuSliceNamed(cpu, name, start, duration, opt_thread) { + const s = new tr.model.CpuSlice( + 'cat', name, 0, start, {}, duration); + s.cpu = cpu; + if (opt_thread) { + s.threadThatWasRunning = opt_thread; + } + return s; + } + + test('getTimesliceForCpuSlice', function() { + const m = new tr.Model(); + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + const cpu = m.kernel.getOrCreateCpu(1); + const t2 = m.getOrCreateProcess(1).getOrCreateThread(2); + t2.timeSlices = [newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 0, 10, cpu), + newThreadSlice(t2, SCHEDULING_STATE.SLEEPING, 10, 10), + newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 20, 10, cpu)]; + cpu.slices = [newCpuSliceNamed(cpu, 'x', 0, 10, t2), + newCpuSliceNamed(cpu, 'x', 20, 10, t2)]; + assert.strictEqual( + cpu.slices[0].getAssociatedTimeslice(), t2.timeSlices[0]); + assert.strictEqual( + cpu.slices[1].getAssociatedTimeslice(), t2.timeSlices[2]); + + assert.strictEqual(t2.timeSlices[0].getAssociatedCpuSlice(), cpu.slices[0]); + assert.isUndefined(t2.timeSlices[1].getAssociatedCpuSlice()); + assert.strictEqual(t2.timeSlices[2].getAssociatedCpuSlice(), cpu.slices[1]); + + assert.strictEqual(cpu.indexOf(cpu.slices[0]), 0); + assert.strictEqual(cpu.indexOf(cpu.slices[1]), 1); + + assert.strictEqual(t2.indexOfTimeSlice(t2.timeSlices[0]), 0); + assert.strictEqual(t2.indexOfTimeSlice(t2.timeSlices[1]), 1); + assert.strictEqual(t2.indexOfTimeSlice(t2.timeSlices[2]), 2); + }); + + test('putToSleepFor', function() { + const m = new tr.Model(); + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + const cpu = m.kernel.getOrCreateCpu(1); + + const t2 = m.getOrCreateProcess(1).getOrCreateThread(2); + const t3 = m.getOrCreateProcess(1).getOrCreateThread(3); + t2.timeSlices = [newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 0, 10, cpu), + newThreadSlice(t2, SCHEDULING_STATE.SLEEPING, 10, 10), + newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 20, 10, cpu)]; + t3.timeSlices = [newThreadSlice(t3, SCHEDULING_STATE.RUNNING, 10, 5, cpu)]; + cpu.slices = [newCpuSliceNamed(cpu, 'x', 0, 10, t2), + newCpuSliceNamed(cpu, 'x', 10, 5, t3), + newCpuSliceNamed(cpu, 'x', 20, 10, t2)]; + + // At timeslice 0, the thread is running. + assert.isUndefined(t2.timeSlices[0].getCpuSliceThatTookCpu()); + + // t2 lost the cpu to t3 at t=10 + assert.strictEqual( + cpu.slices[1], + t2.timeSlices[1].getCpuSliceThatTookCpu()); + }); + + test('putToSleepForNothing', function() { + const m = new tr.Model(); + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + const cpu = m.kernel.getOrCreateCpu(1); + + const t2 = m.getOrCreateProcess(1).getOrCreateThread(2); + const t3 = m.getOrCreateProcess(1).getOrCreateThread(3); + t2.timeSlices = [newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 0, 10, cpu), + newThreadSlice(t2, SCHEDULING_STATE.SLEEPING, 10, 10), + newThreadSlice(t2, SCHEDULING_STATE.RUNNING, 20, 10, cpu)]; + t3.timeSlices = [newThreadSlice(t3, SCHEDULING_STATE.RUNNING, 15, 5, cpu)]; + cpu.slices = [newCpuSliceNamed(cpu, 'x', 0, 10, t2), + newCpuSliceNamed(cpu, 'x', 15, 5, t3), + newCpuSliceNamed(cpu, 'x', 20, 10, t2)]; + assert.isUndefined(t2.timeSlices[1].getCpuSliceThatTookCpu()); + }); + + test('switchActiveThread', function() { + const m = new tr.Model(); + const cpu = m.kernel.getOrCreateCpu(1); + + cpu.switchActiveThread(5, {}, 0, 'idle thread', {}); + cpu.switchActiveThread(10, {}, 1, 'thread one', {a: 1}); + cpu.switchActiveThread(15, {b: 2}, 2, 'thread two', {c: 3}); + cpu.switchActiveThread(30, {c: 4, d: 5}, 3, 'thread three', {e: 6}); + cpu.closeActiveThread(40, {f: 7}); + cpu.switchActiveThread(50, {}, 4, 'thread four', {g: 8}); + cpu.switchActiveThread(60, {}, 1, 'thread one', {}); + cpu.closeActiveThread(70, {}); + + assert.strictEqual(cpu.slices.length, 5); + + assert.strictEqual(cpu.slices[0].title, 'thread one'); + assert.strictEqual(cpu.slices[0].start, 10); + assert.strictEqual(cpu.slices[0].duration, 5); + assert.strictEqual(Object.keys(cpu.slices[0].args).length, 2); + assert.strictEqual(cpu.slices[0].args.a, 1); + assert.strictEqual(cpu.slices[0].args.b, 2); + + assert.strictEqual(cpu.slices[1].title, 'thread two'); + assert.strictEqual(cpu.slices[1].start, 15); + assert.strictEqual(cpu.slices[1].duration, 15); + assert.strictEqual(Object.keys(cpu.slices[1].args).length, 2); + assert.strictEqual(cpu.slices[1].args.c, 4); + assert.strictEqual(cpu.slices[1].args.d, 5); + + assert.strictEqual(cpu.slices[2].title, 'thread three'); + assert.strictEqual(cpu.slices[2].start, 30); + assert.strictEqual(cpu.slices[2].duration, 10); + assert.strictEqual(Object.keys(cpu.slices[2].args).length, 2); + assert.strictEqual(cpu.slices[2].args.e, 6); + assert.strictEqual(cpu.slices[2].args.f, 7); + + assert.strictEqual(cpu.slices[3].title, 'thread four'); + assert.strictEqual(cpu.slices[3].start, 50); + assert.strictEqual(cpu.slices[3].duration, 10); + assert.strictEqual(Object.keys(cpu.slices[3].args).length, 1); + assert.strictEqual(cpu.slices[3].args.g, 8); + + assert.strictEqual(cpu.slices[4].title, 'thread one'); + assert.strictEqual(cpu.slices[4].start, 60); + assert.strictEqual(cpu.slices[4].duration, 10); + assert.strictEqual(Object.keys(cpu.slices[4].args).length, 0); + }); + + test('getFrequencyStats', function() { + const m = new tr.Model(); + const cpu = m.kernel.getOrCreateCpu(1); + const powerCounter = cpu.getOrCreateCounter('', 'Clock Frequency'); + const series = powerCounter.addSeries(new tr.model.CounterSeries('state', + ColorScheme.getColorIdForGeneralPurposeString('test'))); + + series.addCounterSample(0, 100000); + series.addCounterSample(20, 300000); + series.addCounterSample(30, 100000); + series.addCounterSample(80, 500000); + series.addCounterSample(100, 300000); + + const range = tr.b.math.Range.fromExplicitRange(10, 90); + const stats = cpu.getFreqStatsForRange(range); + assert.strictEqual(stats[100000], 60); + assert.strictEqual(stats[300000], 10); + assert.strictEqual(stats[500000], 10); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/device.html b/chromium/third_party/catapult/tracing/tracing/model/device.html new file mode 100644 index 00000000000..5dca1e03151 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/device.html @@ -0,0 +1,124 @@ +<!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/guid.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/model/cpu.html"> +<link rel="import" href="/tracing/model/event_container.html"> +<link rel="import" href="/tracing/model/power_series.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Device class. + */ +tr.exportTo('tr.model', function() { + /** + * Device represents the device-level objects in the model. + * @constructor + * @extends {tr.model.EventContainer} + */ + function Device(model) { + if (!model) { + throw new Error('Must provide a model.'); + } + + tr.model.EventContainer.call(this); + + this.powerSeries_ = undefined; + this.cpuUsageSeries_ = undefined; + this.vSyncTimestamps_ = []; + } + + Device.compare = function(x, y) { + return x.guid - y.guid; + }; + + Device.prototype = { + __proto__: tr.model.EventContainer.prototype, + + compareTo(that) { + return Device.compare(this, that); + }, + + get userFriendlyName() { + return 'Device'; + }, + + get userFriendlyDetails() { + return 'Device'; + }, + + get stableId() { + return 'Device'; + }, + + getSettingsKey() { + return 'device'; + }, + + get powerSeries() { + return this.powerSeries_; + }, + + set powerSeries(powerSeries) { + this.powerSeries_ = powerSeries; + }, + + get cpuUsageSeries() { + return this.cpuUsageSeries_; + }, + + set cpuUsageSeries(cpuUsageSeries) { + this.cpuUsageSeries_ = cpuUsageSeries; + }, + + get vSyncTimestamps() { + return this.vSyncTimestamps_; + }, + + set vSyncTimestamps(value) { + this.vSyncTimestamps_ = value; + }, + + updateBounds() { + this.bounds.reset(); + for (const child of this.childEventContainers()) { + child.updateBounds(); + this.bounds.addRange(child.bounds); + } + }, + + shiftTimestampsForward(amount) { + for (const child of this.childEventContainers()) { + child.shiftTimestampsForward(amount); + } + + for (let i = 0; i < this.vSyncTimestamps_.length; i++) { + this.vSyncTimestamps_[i] += amount; + } + }, + + addCategoriesToDict(categoriesDict) { + }, + + * childEventContainers() { + if (this.powerSeries_) { + yield this.powerSeries_; + } + if (this.cpuUsageSeries_) { + yield this.cpuUsageSeries_; + } + } + }; + + return { + Device, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/device_test.html b/chromium/third_party/catapult/tracing/tracing/model/device_test.html new file mode 100644 index 00000000000..6e46cced01b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/device_test.html @@ -0,0 +1,71 @@ +<!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/device.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/power_series.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Device = tr.model.Device; + const Model = tr.Model; + const PowerSeries = tr.model.PowerSeries; + + test('updateBounds', function() { + const device = new Device(new Model()); + device.powerSeries = new PowerSeries(device); + + // Verify that the bounds match the lowest and highest timestamps. + device.powerSeries.addPowerSample(100, 5); + device.powerSeries.addPowerSample(200, 5); + device.updateBounds(); + + assert.strictEqual(device.bounds.min, 100); + assert.strictEqual(device.bounds.max, 200); + + // Add a new sample and verify that the bounds change. + device.powerSeries.addPowerSample(700, 5); + device.updateBounds(); + + assert.strictEqual(device.bounds.min, 100); + assert.strictEqual(device.bounds.max, 700); + }); + + test('shiftTimestampsForward', function() { + const device = new Device(new Model()); + device.powerSeries = new PowerSeries(device); + + device.powerSeries.addPowerSample(100, 2); + device.powerSeries.addPowerSample(200, 2); + device.shiftTimestampsForward(2); + + assert.strictEqual(device.powerSeries.samples[0].start, 102); + assert.strictEqual(device.powerSeries.samples[1].start, 202); + }); + + test('childEventContainers_noPowerSeries', function() { + const device = new Device(new Model()); + const childEventContainers = []; + for (const container of device.childEventContainers()) { + childEventContainers.push(container); + } + assert.deepEqual(childEventContainers, []); + }); + + test('childEventContainers_powerSeries', function() { + const device = new Device(new Model()); + device.powerSeries = new PowerSeries(device); + const childEventContainers = []; + for (const container of device.childEventContainers()) { + childEventContainers.push(container); + } + assert.deepEqual(childEventContainers, [device.powerSeries]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/event.html b/chromium/third_party/catapult/tracing/tracing/model/event.html new file mode 100644 index 00000000000..99495fcc913 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/event.html @@ -0,0 +1,77 @@ +<!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/guid.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/selectable_item.html"> +<link rel="import" href="/tracing/model/selection_state.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Event class. + */ +tr.exportTo('tr.model', function() { + const SelectableItem = tr.model.SelectableItem; + const SelectionState = tr.model.SelectionState; + const IMMUTABLE_EMPTY_SET = tr.model.EventSet.IMMUTABLE_EMPTY_SET; + + /** + * An Event is the base type for any non-container, selectable piece + * of data in the trace model. + * + * @constructor + * @extends {SelectableItem} + */ + function Event() { + SelectableItem.call(this, this /* modelItem */); + this.guid_ = tr.b.GUID.allocateSimple(); + this.selectionState = SelectionState.NONE; + this.info = undefined; + } + + Event.prototype = { + __proto__: SelectableItem.prototype, + + get guid() { + return this.guid_; + }, + + get stableId() { + return undefined; + }, + + get range() { + const range = new tr.b.math.Range(); + this.addBoundsToRange(range); + return range; + }, + + // Empty by default. Lazily initialized on an instance in + // addAssociatedAlert(). See #1930. + associatedAlerts: IMMUTABLE_EMPTY_SET, + + addAssociatedAlert(alert) { + if (this.associatedAlerts === IMMUTABLE_EMPTY_SET) { + this.associatedAlerts = new tr.model.EventSet(); + } + this.associatedAlerts.push(alert); + }, + + // Adds the range of timestamps for this event to the specified range. + // If this is not overridden in subclass, it means that type of event + // doesn't have timestamps. + addBoundsToRange(range) {} + }; + + return { + Event, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_container.html b/chromium/third_party/catapult/tracing/tracing/model/event_container.html new file mode 100644 index 00000000000..8c71eb4b688 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/event_container.html @@ -0,0 +1,167 @@ +<!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/guid.html"> +<link rel="import" href="/tracing/base/math/range.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + /** + * EventContainer is a base class for any class in the trace model that + * contains child events or child EventContainers. + * + * For all EventContainers, updateBounds() must be called after modifying the + * container's events if an up-to-date bounds is expected. + * + * @constructor + */ + function EventContainer() { + this.guid_ = tr.b.GUID.allocateSimple(); + this.important = true; + this.bounds_ = new tr.b.math.Range(); + } + + EventContainer.prototype = { + get guid() { + return this.guid_; + }, + + /** + * @return {String} A stable and unique identifier that describes this + * container's position in the event tree relative to the root. If an event + * container 'B' is a child to another event container 'A', then container + * B's stable ID would be 'A.B'. + */ + get stableId() { + throw new Error('Not implemented'); + }, + + /** + * Returns the bounds of the event container, which describe the range + * of timestamps for all ancestor events. + */ + get bounds() { + return this.bounds_; + }, + + // TODO(charliea): A default implementation of this method could likely be + // provided that iterates throuch getDescendantEvents. + /** + * Updates the bounds of the event container. After updating, this.bounds + * will describe the range of timestamps of all ancestor events. + */ + updateBounds() { + throw new Error('Not implemented'); + }, + + // TODO(charliea): A default implementation of this method could likely be + // provided that iterates through getDescendantEvents. + /** + * Shifts the timestamps for ancestor events by 'amount' milliseconds. + */ + shiftTimestampsForward(amount) { + throw new Error('Not implemented'); + }, + + + /** + * Returns an iterable of all child events. + */ + * childEvents() { + }, + + /** + * Returns an iterable of all events in this and descendant + * event containers. + */ + * getDescendantEvents() { + yield* this.childEvents(); + for (const container of this.childEventContainers()) { + yield* container.getDescendantEvents(); + } + }, + + /** + * Returns an iterable of all child event containers. + */ + * childEventContainers() { + }, + + /** + * Returns an iterable containing this and all descendant event containers. + */ + * getDescendantEventContainers() { + yield this; + for (const container of this.childEventContainers()) { + yield* container.getDescendantEventContainers(); + } + }, + + /** + * Returns an iterable of all events in this and descendant event containers + * in a given set of ranges. + * + * This base class provides a default implementation with no assumptions + * about the order of events. A container can override this implementation + * with a more efficient one, for example if its events are sorted. + */ + * getDescendantEventsInSortedRanges(ranges, opt_containerPredicate) { + if (opt_containerPredicate === undefined || + opt_containerPredicate(this)) { + for (const event of this.childEvents()) { + const i = tr.b.findFirstTrueIndexInSortedArray( + ranges, range => event.start <= range.max); + if (i < ranges.length && event.end >= ranges[i].min) yield event; + } + } + + for (const container of this.childEventContainers()) { + yield* container.getDescendantEventsInSortedRanges( + ranges, opt_containerPredicate); + } + }, + + /** + * Finds topmost slices in this container (see docstring for + * findTopmostSlices). + */ + * findTopmostSlicesInThisContainer(eventPredicate, opt_this) { + }, + + /** + * The findTopmostSlices* series of helpers find all topmost slices + * satisfying the given predicates. + * + * As an example, suppose we are trying to find slices named 'C', with the + * following thread: + * + * -> |---C---| |-----D-----| + * |-C-| |---C---| <- + * + * findTopmostSlices would locate the pointed-to Cs, because the bottom C on + * the left is not the topmost C, and the right one is, even though it is + * not itself a top-level slice. + */ + * findTopmostSlices(eventPredicate) { + for (const ec of this.getDescendantEventContainers()) { + yield* ec.findTopmostSlicesInThisContainer(eventPredicate); + } + }, + + * findTopmostSlicesNamed(name) { + yield* this.findTopmostSlices(e => e.title === name); + } + }; + + return { + EventContainer, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_container_test.html b/chromium/third_party/catapult/tracing/tracing/model/event_container_test.html new file mode 100644 index 00000000000..06b06a391c2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/event_container_test.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/model/event_container.html"> +<link rel="import" href="/tracing/model/timed_event.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('getDescendantEventsInSortedRanges', function() { + class ContainerTypeX extends tr.model.EventContainer { + * childEvents() { + const event1 = new tr.model.TimedEvent(4); + event1.duration = 2; + event1.title = 'X-1'; + yield event1; + + const event2 = new tr.model.TimedEvent(1); + event2.duration = 2; + event2.title = 'X-2'; + yield event2; + } + } + + class ContainerTypeY extends tr.model.EventContainer { + constructor() { + super(); + this.childContainer_ = new ContainerTypeX(); + } + + * childEvents() { + const event1 = new tr.model.TimedEvent(5); + event1.duration = 4; + event1.title = 'Y-1'; + yield event1; + + const event2 = new tr.model.TimedEvent(6); + event2.duration = 2; + event2.title = 'Y-2'; + yield event2; + } + + * childEventContainers() { + yield this.childContainer_; + } + } + + // We have the following timed events: + // 1 2 3 4 5 6 7 8 9 + // ContainerTypeY <----- Y-1 -----> + // <- Y2 -> + // ContainerTypeX <- X-2 -> <- X-1 -> + const container = new ContainerTypeY(); + + // [2, 5] intersect X-1, X-2, and Y-1. + const r1 = new tr.b.math.Range.fromExplicitRange(2, 5); + let slices = [...container.getDescendantEventsInSortedRanges([r1])]; + slices = slices.map(s => s.title).sort(); + assert.strictEqual(slices.length, 3); + assert.strictEqual(slices[0], 'X-1'); + assert.strictEqual(slices[1], 'X-2'); + assert.strictEqual(slices[2], 'Y-1'); + + // [2, 5], [7, 8] intersect X-1, X-2, Y-1, and Y-2. + const r2 = new tr.b.math.Range.fromExplicitRange(7, 8); + slices = [...container.getDescendantEventsInSortedRanges([r1, r2])]; + slices = slices.map(s => s.title).sort(); + assert.strictEqual(slices.length, 4); + assert.strictEqual(slices[0], 'X-1'); + assert.strictEqual(slices[1], 'X-2'); + assert.strictEqual(slices[2], 'Y-1'); + assert.strictEqual(slices[3], 'Y-2'); + + // We should see events from ContainerTypeX only. + slices = [...container.getDescendantEventsInSortedRanges( + [r1], container => container instanceof ContainerTypeX)]; + slices = slices.map(s => s.title).sort(); + assert.strictEqual(slices.length, 2); + assert.strictEqual(slices[0], 'X-1'); + assert.strictEqual(slices[1], 'X-2'); + + // We should see events from ContainerTypeY only. + slices = [...container.getDescendantEventsInSortedRanges( + [r1], container => container instanceof ContainerTypeY)]; + slices = slices.map(s => s.title).sort(); + assert.strictEqual(slices.length, 1); + assert.strictEqual(slices[0], 'Y-1'); + }); +}); diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_info.html b/chromium/third_party/catapult/tracing/tracing/model/event_info.html new file mode 100644 index 00000000000..ecc309d471a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/event_info.html @@ -0,0 +1,39 @@ +<!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/color_scheme.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const ColorScheme = tr.b.ColorScheme; + + /** + * EventInfo is an annotation added to Events in order to document + * what they represent, and override their title/colorId values. + * + * TODO(ccraik): eventually support more complex structure/paragraphs. + * + * @param {string} title A user-visible title for the event. + * @param {string} description A user-visible description of the event. + * @param {Array} docLinks A list of Objects, each of the form + * {label: str, textContent: str, href: str} + * + * @constructor + */ + function EventInfo(title, description, docLinks) { + this.title = title; + this.description = description; + this.docLinks = docLinks; + this.colorId = ColorScheme.getColorIdForGeneralPurposeString(title); + } + + return { + EventInfo, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_registry.html b/chromium/third_party/catapult/tracing/tracing/model/event_registry.html new file mode 100644 index 00000000000..7913d7ffe80 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/event_registry.html @@ -0,0 +1,91 @@ +<!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/extension_registry.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the EventRegistry class. + */ +tr.exportTo('tr.model', function() { + // Create the type registry. + function EventRegistry() { + } + + const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE); + tr.b.decorateExtensionRegistry(EventRegistry, options); + + // Enforce all options objects have the right fields. + EventRegistry.addEventListener('will-register', function(e) { + const metadata = e.typeInfo.metadata; + if (metadata.name === undefined) { + throw new Error('Registered events must provide name metadata'); + } + if (metadata.pluralName === undefined) { + throw new Error('Registered events must provide pluralName metadata'); + } + + // Add a subtype registry to every event so that all events can be + // extended + if (metadata.subTypes === undefined) { + metadata.subTypes = {}; + const options = new tr.b.ExtensionRegistryOptions( + tr.b.TYPE_BASED_REGISTRY_MODE); + options.mandatoryBaseClass = e.typeInfo.constructor; + options.defaultConstructor = e.typeInfo.constructor; + tr.b.decorateExtensionRegistry(metadata.subTypes, options); + } else { + if (!metadata.subTypes.register) { + throw new Error('metadata.subTypes must be an extension registry.'); + } + } + + e.typeInfo.constructor.subTypes = metadata.subTypes; + }); + + // Helper: lookup Events indexed by type name. + let eventsByTypeName = undefined; + EventRegistry.getEventTypeInfoByTypeName = function(typeName) { + if (eventsByTypeName === undefined) { + eventsByTypeName = {}; + EventRegistry.getAllRegisteredTypeInfos().forEach(function(typeInfo) { + eventsByTypeName[typeInfo.metadata.name] = typeInfo; + }); + } + return eventsByTypeName[typeName]; + }; + + // Ensure eventsByTypeName stays current. + EventRegistry.addEventListener('registry-changed', function() { + eventsByTypeName = undefined; + }); + + function convertCamelCaseToTitleCase(name) { + let result = name.replace(/[A-Z]/g, ' $&'); + result = result.charAt(0).toUpperCase() + result.slice(1); + return result; + } + + EventRegistry.getUserFriendlySingularName = function(typeName) { + const typeInfo = EventRegistry.getEventTypeInfoByTypeName(typeName); + const str = typeInfo.metadata.name; + return convertCamelCaseToTitleCase(str); + }; + + EventRegistry.getUserFriendlyPluralName = function(typeName) { + const typeInfo = EventRegistry.getEventTypeInfoByTypeName(typeName); + const str = typeInfo.metadata.pluralName; + return convertCamelCaseToTitleCase(str); + }; + + return { + EventRegistry, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_set.html b/chromium/third_party/catapult/tracing/tracing/model/event_set.html new file mode 100644 index 00000000000..8805a6302ba --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/event_set.html @@ -0,0 +1,302 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2012 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/event.html"> +<link rel="import" href="/tracing/base/guid.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_registry.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const EventRegistry = tr.model.EventRegistry; + + const RequestSelectionChangeEvent = tr.b.Event.bind( + undefined, 'requestSelectionChange', true, false); + + /** + * Represents a event set within a and its associated set of tracks. + * @constructor + */ + function EventSet(opt_events) { + this.bounds_ = new tr.b.math.Range(); + this.events_ = new Set(); + this.guid_ = tr.b.GUID.allocateSimple(); + + if (opt_events) { + if (opt_events instanceof Array) { + for (const event of opt_events) { + this.push(event); + } + } else if (opt_events instanceof EventSet) { + this.addEventSet(opt_events); + } else { + this.push(opt_events); + } + } + } + + EventSet.prototype = { + __proto__: Object.prototype, + + get bounds() { + return this.bounds_; + }, + + get duration() { + if (this.bounds_.isEmpty) return 0; + return this.bounds_.max - this.bounds_.min; + }, + + get length() { + return this.events_.size; + }, + + get guid() { + return this.guid_; + }, + + * [Symbol.iterator]() { + for (const event of this.events_) { + yield event; + } + }, + + clear() { + this.bounds_ = new tr.b.math.Range(); + this.events_.clear(); + }, + + /** + * Pushes each argument onto the EventSet. Returns the number of + * arguments pushed. + */ + push(...events) { + let numPushed; + for (const event of events) { + if (event.guid === undefined) { + throw new Error('Event must have a GUID'); + } + + if (!this.events_.has(event)) { + this.events_.add(event); + // Some uses of eventSet (e.g. in tests) have Events as objects that + // don't have addBoundsToRange as a function. Thus we need to handle + // this case. + if (event.addBoundsToRange) { + if (this.bounds_ !== undefined) { + event.addBoundsToRange(this.bounds_); + } + } + } + numPushed++; + } + return numPushed; + }, + + contains(event) { + if (this.events_.has(event)) return event; + return undefined; + }, + + addEventSet(eventSet) { + for (const event of eventSet) { + this.push(event); + } + }, + + intersectionIsEmpty(otherEventSet) { + return !this.some(event => otherEventSet.contains(event)); + }, + + equals(that) { + if (this.length !== that.length) return false; + return this.every(event => that.contains(event)); + }, + + sortEvents(compare) { + // Convert to array, then sort, then convert back + const ary = this.toArray(); + ary.sort(compare); + + this.clear(); + for (const event of ary) { + this.push(event); + } + }, + + getEventsOrganizedByBaseType(opt_pruneEmpty) { + const allTypeInfos = EventRegistry.getAllRegisteredTypeInfos(); + + const events = this.getEventsOrganizedByCallback(function(event) { + let maxEventIndex = -1; + let maxEventTypeInfo = undefined; + + allTypeInfos.forEach(function(eventTypeInfo, eventIndex) { + if (!(event instanceof eventTypeInfo.constructor)) return; + + if (eventIndex > maxEventIndex) { + maxEventIndex = eventIndex; + maxEventTypeInfo = eventTypeInfo; + } + }); + + if (maxEventIndex === -1) { + throw new Error(`Unrecognized event type: ${event.constructor.name}`); + } + + return maxEventTypeInfo.metadata.name; + }); + + if (!opt_pruneEmpty) { + allTypeInfos.forEach(function(eventTypeInfo) { + if (events[eventTypeInfo.metadata.name] === undefined) { + events[eventTypeInfo.metadata.name] = new EventSet(); + } + }); + } + + return events; + }, + + getEventsOrganizedByTitle() { + return this.getEventsOrganizedByCallback(function(event) { + if (event.title === undefined) { + throw new Error('An event didn\'t have a title!'); + } + return event.title; + }); + }, + + /** + * @param {!function(!tr.model.Event):string} cb + * @param {*=} opt_this + * @return {!Object} TODO(#3432) Return Map. + */ + getEventsOrganizedByCallback(cb, opt_this) { + const groupedEvents = tr.b.groupIntoMap(this, cb, opt_this || this); + const groupedEventsDict = {}; + for (const [k, events] of groupedEvents) { + groupedEventsDict[k] = new EventSet(events); + } + return groupedEventsDict; + }, + + enumEventsOfType(type, func) { + for (const event of this) { + if (event instanceof type) { + func(event); + } + } + }, + + get userFriendlyName() { + if (this.length === 0) { + throw new Error('Empty event set'); + } + + const eventsByBaseType = this.getEventsOrganizedByBaseType(true); + const eventTypeName = Object.keys(eventsByBaseType)[0]; + + if (this.length === 1) { + const tmp = EventRegistry.getUserFriendlySingularName(eventTypeName); + return tr.b.getOnlyElement(this.events_).userFriendlyName; + } + + const numEventTypes = Object.keys(eventsByBaseType).length; + if (numEventTypes !== 1) { + return this.length + ' events of various types'; + } + + const tmp = EventRegistry.getUserFriendlyPluralName(eventTypeName); + return this.length + ' ' + tmp; + }, + + filter(fn, opt_this) { + const res = new EventSet(); + for (const event of this) { + if (fn.call(opt_this, event)) { + res.push(event); + } + } + + return res; + }, + + toArray() { + const ary = []; + for (const event of this) { + ary.push(event); + } + return ary; + }, + + forEach(fn, opt_this) { + for (const event of this) { + fn.call(opt_this, event); + } + }, + + map(fn, opt_this) { + const res = []; + for (const event of this) { + res.push(fn.call(opt_this, event)); + } + return res; + }, + + every(fn, opt_this) { + for (const event of this) { + if (!fn.call(opt_this, event)) { + return false; + } + } + return true; + }, + + some(fn, opt_this) { + for (const event of this) { + if (fn.call(opt_this, event)) { + return true; + } + } + return false; + }, + + asDict() { + const stableIds = []; + for (const event of this) { + stableIds.push(event.stableId); + } + return {'events': stableIds}; + }, + + asSet() { + return this.events_; + } + }; + + EventSet.IMMUTABLE_EMPTY_SET = (function() { + const s = new EventSet(); + s.push = function() { + throw new Error('Cannot push to an immutable event set'); + }; + s.addEventSet = function() { + throw new Error('Cannot add to an immutable event set'); + }; + Object.freeze(s); + return s; + })(); + + return { + EventSet, + RequestSelectionChangeEvent, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_set_test.html b/chromium/third_party/catapult/tracing/tracing/model/event_set_test.html new file mode 100644 index 00000000000..9f597ed0984 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/event_set_test.html @@ -0,0 +1,360 @@ +<!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/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newModel = tr.c.TestUtils.newModel; + const newSliceEx = tr.c.TestUtils.newSliceEx; + + test('eventSetObject', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 5, {}, 1)); + + const eventSet = new tr.model.EventSet(); + eventSet.push(t1.sliceGroup.slices[0]); + + assert.strictEqual(eventSet.bounds.min, 1); + assert.strictEqual(eventSet.bounds.max, 4); + assert.deepEqual(eventSet.asSet(), new tr.model.EventSet( + t1.sliceGroup.slices[0]).asSet()); + + eventSet.push(t1.sliceGroup.slices[1]); + assert.strictEqual(eventSet.bounds.min, 1); + assert.strictEqual(eventSet.bounds.max, 6); + assert.deepEqual(eventSet.asSet(), new tr.model.EventSet( + [t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]]).asSet()); + + eventSet.clear(); + assert.strictEqual(eventSet.length, 0); + }); + + test('push_noEvents', function() { + const eventSet = new tr.model.EventSet(); + eventSet.push(); + assert.strictEqual(eventSet.length, 0); + }); + + test('push_oneEvent', function() { + const eventSet = new tr.model.EventSet(); + eventSet.push({guid: 1}); + assert.strictEqual(eventSet.length, 1); + }); + + test('push_multipleEvents', function() { + const eventSet = new tr.model.EventSet(); + eventSet.push({guid: 1}, {guid: 2}, {guid: 3}); + assert.strictEqual(eventSet.length, 3); + }); + + test('push_multiplePushes', function() { + const eventSet = new tr.model.EventSet(); + eventSet.push({guid: 1}, {guid: 2}, {guid: 3}); + eventSet.push({guid: 4}, {guid: 5}, {guid: 6}); + assert.strictEqual(eventSet.length, 6); + }); + + test('push_noGuidThrows', function() { + const eventSet = new tr.model.EventSet(); + assert.throws(() => eventSet.push({guid: 1}, {badItem: 2}, {guid: 3}), + 'Event must have a GUID'); + }); + + test('iteration', function() { + const eventSet = new tr.model.EventSet([{guid: 1}, {guid: 2}, {guid: 3}]); + + let expectedId = 1; + for (const event of eventSet) { + assert.strictEqual(event.guid, expectedId++); + } + }); + + test('uniqueContents', function() { + const sample1 = {guid: 1}; + const sample2 = {guid: 2}; + + const eventSet = new tr.model.EventSet(); + + eventSet.push(sample1); + eventSet.push(sample2); + assert.strictEqual(eventSet.length, 2); + + eventSet.push(sample1); + assert.strictEqual(eventSet.length, 2); + }); + + test('userFriendlyNameSingular', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + const selection = new tr.model.EventSet(t1.sliceGroup.slices[0]); + assert.isDefined(selection.userFriendlyName); + }); + + test('userFriendlyNamePlural', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3)); + const eventSet = new tr.model.EventSet([ + t1.sliceGroup.slices[0], + t1.sliceGroup.slices[1] + ]); + assert.isDefined(eventSet.userFriendlyName); + }); + + test('userFriendlyNameMixedPlural', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3)); + + const i10 = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'name', 10); + const s10 = i10.addSnapshot(10, {foo: 1}); + + const eventSet = new tr.model.EventSet([ + t1.sliceGroup.slices[0], + s10 + ]); + assert.isDefined(eventSet.userFriendlyName); + }); + + test('groupEventsByTitle', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'b', 0, 3, {}, 3)); + const eventSet = new tr.model.EventSet([ + t1.sliceGroup.slices[0], + t1.sliceGroup.slices[1], + t1.sliceGroup.slices[2] + ]); + + const eventsByTitle = eventSet.getEventsOrganizedByTitle(); + assert.strictEqual(2, Object.keys(eventsByTitle).length); + assert.sameMembers(eventsByTitle.a.toArray(), + [t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]]); + assert.sameMembers(eventsByTitle.b.toArray(), + [t1.sliceGroup.slices[2]]); + }); + + test('groupEventsByCallback', function() { + const a1 = new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3); + const a2 = new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3); + const b1 = new tr.model.ThreadSlice('', 'b', 0, 1, {}, 3); + const eventSet = new tr.model.EventSet([a1, a2, b1]); + function getEventKey(event) { + return 's' + event.start; + } + const eventsByCallback = eventSet.getEventsOrganizedByCallback(getEventKey); + assert.strictEqual(2, Object.keys(eventsByCallback).length); + assert.sameMembers(eventsByCallback.s1.toArray(), [a1, b1]); + assert.sameMembers(eventsByCallback.s2.toArray(), [a2]); + }); + + test('groupEventsByBaseType', function() { + const a = new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3); + const b = new tr.model.ThreadSlice('', 'b', 0, 1, {}, 3); + const c = new tr.model.AsyncSlice('', 'c', 0, 1, {}, 3); + const eventSet = new tr.model.EventSet([a, b, c]); + let eventsByBaseType = eventSet.getEventsOrganizedByBaseType(true); + assert.strictEqual(2, Object.keys(eventsByBaseType).length); + assert.sameMembers(eventsByBaseType.slice.toArray(), [a, b]); + assert.sameMembers(eventsByBaseType.asyncSlice.toArray(), [c]); + + eventsByBaseType = eventSet.getEventsOrganizedByBaseType(false); + assert.isTrue(2 < Object.keys(eventsByBaseType).length); + assert.sameMembers(eventsByBaseType.slice.toArray(), [a, b]); + assert.sameMembers(eventsByBaseType.asyncSlice.toArray(), [c]); + for (const baseType in eventsByBaseType) { + if (baseType !== 'slice' && baseType !== 'asyncSlice') { + assert.strictEqual(0, eventsByBaseType[baseType].length); + } + } + }); + + test('intersectionIsEmpty1', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'b', 0, 3, {}, 3)); + + const set1 = new tr.model.EventSet([ + t1.sliceGroup.slices[0], + t1.sliceGroup.slices[1], + t1.sliceGroup.slices[2] + ]); + const set2 = new tr.model.EventSet([ + t1.sliceGroup.slices[2] + ]); + assert.isFalse(set1.intersectionIsEmpty(set2)); + }); + + test('intersectionIsNonEmpty2', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 2, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'b', 0, 3, {}, 3)); + + const set1 = new tr.model.EventSet([ + t1.sliceGroup.slices[0], + t1.sliceGroup.slices[1] + ]); + const set2 = new tr.model.EventSet([ + t1.sliceGroup.slices[2] + ]); + assert.isTrue(set1.intersectionIsEmpty(set2)); + }); + + test('equals', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'b', 0, 2, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'c', 0, 3, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'd', 0, 4, {}, 5)); + + const eventSet1 = new tr.model.EventSet([ + t1.sliceGroup.slices[0], + t1.sliceGroup.slices[1], + t1.sliceGroup.slices[2] + ]); + const eventSet2 = new tr.model.EventSet([ + t1.sliceGroup.slices[1], + t1.sliceGroup.slices[0], + t1.sliceGroup.slices[2] + ]); + const eventSet3 = new tr.model.EventSet([ + t1.sliceGroup.slices[1], + t1.sliceGroup.slices[0], + t1.sliceGroup.slices[3] + ]); + const eventSet4 = new tr.model.EventSet([ + t1.sliceGroup.slices[1], + t1.sliceGroup.slices[0] + ]); + assert.isTrue(eventSet1.equals(eventSet2)); + assert.isFalse(eventSet1.equals(eventSet3)); + assert.isFalse(eventSet1.equals(eventSet4)); + }); + + test('filter', function() { + const m = newModel(function(m) { + const p = m.getOrCreateProcess(1); + const t = p.getOrCreateThread(1); + + m.s0 = t.sliceGroup.pushSlice(newSliceEx( + { title: 's0', start: 0.0, duration: 1.0 })); + m.s1 = t.sliceGroup.pushSlice(newSliceEx( + { title: 's1', start: 0.0, duration: 1.0 })); + m.s2 = t.sliceGroup.pushSlice(newSliceEx( + { title: 's2', start: 0.0, duration: 1.0 })); + + m.eventSet = new tr.model.EventSet([m.s0, m.s1, m.s2]); + }); + + const res = m.eventSet.filter(function(slice) { + return slice.title === 's0'; + }); + + assert.isTrue(res.equals(new tr.model.EventSet([m.s0]))); + }); + + test('toArray', function() { + const samples = [ + {guid: 1}, + {guid: 2} + ]; + const eventSet = new tr.model.EventSet(samples); + + assert.deepEqual(eventSet.toArray(), samples); + }); + + test('asDict', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const t1 = p1.getOrCreateThread(1); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'a', 0, 1, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'b', 0, 2, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'c', 0, 3, {}, 3)); + t1.sliceGroup.pushSlice( + new tr.model.ThreadSlice('', 'd', 0, 4, {}, 5)); + + const eventSet = new tr.model.EventSet([ + t1.sliceGroup.slices[0], + t1.sliceGroup.slices[1], + t1.sliceGroup.slices[2] + ]); + + assert.deepEqual( + {'events': + ['1.1.SliceGroup.0', '1.1.SliceGroup.1', '1.1.SliceGroup.2']}, + eventSet.asDict()); + }); + + test('immutableEmptySet', function() { + const s = tr.model.EventSet.IMMUTABLE_EMPTY_SET; + assert.lengthOf(s, 0); + assert.isTrue(s.bounds.isEmpty); + + // Check that the iteration methods still work correctly. + function throwOnCall() { + throw new Error('This function should never be called!!!'); + } + assert.deepEqual(s.map(throwOnCall), []); + s.forEach(throwOnCall); + + // Check that the set is indeed immutable. + assert.throws(function() { s[0] = b; }); + assert.throws(function() { s.push(b); }); + assert.throws(function() { s.addEventSet(new tr.model.EventSet()); }); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/event_test.html b/chromium/third_party/catapult/tracing/tracing/model/event_test.html new file mode 100644 index 00000000000..efbf26b1d13 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/event_test.html @@ -0,0 +1,46 @@ +<!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/model/alert.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/model/event_info.html"> +<link rel="import" href="/tracing/model/event_set.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Alert = tr.model.Alert; + const Event = tr.model.Event; + const EventInfo = tr.model.EventInfo; + const EventSet = tr.model.EventSet; + const ImmutableEventSet = tr.model.ImmutableEventSet; + + test('checkModelItem', function() { + const event = new Event; + assert.strictEqual(event.modelItem, event); + }); + + test('checkAssociatedAlerts', function() { + const event = new Event(); + assert.strictEqual(event.associatedAlerts, EventSet.IMMUTABLE_EMPTY_SET); + assert.sameMembers(event.associatedAlerts.toArray(), []); + + const info1 = new EventInfo('Critical', 'Critical alert!!!', []); + const alert1 = new Alert(info1, 7); + event.addAssociatedAlert(alert1); + assert.instanceOf(event.associatedAlerts, EventSet); + assert.sameMembers(event.associatedAlerts.toArray(), [alert1]); + + const info2 = new EventInfo('Warning', 'Warning alert???', []); + const alert2 = new Alert(info2, 42); + event.addAssociatedAlert(alert2); + assert.instanceOf(event.associatedAlerts, EventSet); + assert.sameMembers(event.associatedAlerts.toArray(), [alert1, alert2]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/flow_event.html b/chromium/third_party/catapult/tracing/tracing/model/flow_event.html new file mode 100644 index 00000000000..9b85217d6f8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/flow_event.html @@ -0,0 +1,67 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/model/timed_event.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Flow class. + */ +tr.exportTo('tr.model', function() { + /** + * A Flow represents an interval of time plus parameters associated + * with that interval. + * + * @constructor + */ + function FlowEvent(category, id, title, colorId, start, args, opt_duration) { + tr.model.TimedEvent.call(this, start); + + this.category = category || ''; + this.title = title; + this.colorId = colorId; + this.start = start; + this.args = args; + + this.id = id; + + this.startSlice = undefined; + this.endSlice = undefined; + + this.startStackFrame = undefined; + this.endStackFrame = undefined; + + if (opt_duration !== undefined) { + this.duration = opt_duration; + } + } + + FlowEvent.prototype = { + __proto__: tr.model.TimedEvent.prototype, + + get userFriendlyName() { + return 'Flow event named ' + this.title + ' at ' + + tr.b.Unit.byName.timeStampInMs.format(this.timestamp); + } + }; + + tr.model.EventRegistry.register( + FlowEvent, + { + name: 'flowEvent', + pluralName: 'flowEvents' + }); + + return { + FlowEvent, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/frame.html b/chromium/third_party/catapult/tracing/tracing/model/frame.html new file mode 100644 index 00000000000..6d1051a603d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/frame.html @@ -0,0 +1,96 @@ +<!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/color_scheme.html"> +<link rel="import" href="/tracing/base/math/statistics.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/model/event_set.html"> + +<script> +'use strict'; + +/** + * @fileoverview Class describing rendered frames. + * + * Because a frame is produced by multiple threads, it does not inherit from + * TimedEvent, and has no duration. + */ +tr.exportTo('tr.model', function() { + const ColorScheme = tr.b.ColorScheme; + const Statistics = tr.b.math.Statistics; + + const FRAME_PERF_CLASS = { + GOOD: 'good', + BAD: 'bad', + TERRIBLE: 'terrible', + NEUTRAL: 'generic_work' + }; + + /** + * @constructor + * @param {Array} associatedEvents Selection of events composing the frame. + * @param {Array} threadTimeRanges Array of {thread, start, end} + * for each thread, describing the critical path of the frame. + */ + function Frame(associatedEvents, threadTimeRanges, opt_args) { + tr.model.Event.call(this); + + this.threadTimeRanges = threadTimeRanges; + this.associatedEvents = new tr.model.EventSet(associatedEvents); + this.args = opt_args || {}; + + this.title = 'Frame'; + this.start = Statistics.min( + threadTimeRanges, function(x) { return x.start; }); + this.end = Statistics.max( + threadTimeRanges, function(x) { return x.end; }); + this.totalDuration = Statistics.sum( + threadTimeRanges, function(x) { return x.end - x.start; }); + + this.perfClass = FRAME_PERF_CLASS.NEUTRAL; + } + + Frame.prototype = { + __proto__: tr.model.Event.prototype, + + set perfClass(perfClass) { + this.colorId = ColorScheme.getColorIdForReservedName(perfClass); + this.perfClass_ = perfClass; + }, + + get perfClass() { + return this.perfClass_; + }, + + shiftTimestampsForward(amount) { + this.start += amount; + this.end += amount; + + for (let i = 0; i < this.threadTimeRanges.length; i++) { + this.threadTimeRanges[i].start += amount; + this.threadTimeRanges[i].end += amount; + } + }, + + addBoundsToRange(range) { + range.addValue(this.start); + range.addValue(this.end); + } + }; + + tr.model.EventRegistry.register( + Frame, + { + name: 'frame', + pluralName: 'frames' + }); + + return { + Frame, + FRAME_PERF_CLASS, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump.html b/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump.html new file mode 100644 index 00000000000..058f52f14e9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump.html @@ -0,0 +1,849 @@ +<!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/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/container_memory_dump.html"> +<link rel="import" href="/tracing/model/event_registry.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the GlobalMemoryDump class. + */ +tr.exportTo('tr.model', function() { + /** + * The GlobalMemoryDump represents a simultaneous memory dump of all + * processes. + * @constructor + */ + function GlobalMemoryDump(model, start) { + tr.model.ContainerMemoryDump.call(this, start); + this.model = model; + this.processMemoryDumps = {}; + } + + // Size numeric names. + const SIZE_NUMERIC_NAME = tr.model.MemoryAllocatorDump.SIZE_NUMERIC_NAME; + const EFFECTIVE_SIZE_NUMERIC_NAME = + tr.model.MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME; + + // Size numeric info types. + const MemoryAllocatorDumpInfoType = tr.model.MemoryAllocatorDumpInfoType; + const PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN = + MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN; + const PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER = + MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER; + + function getSize(dump) { + const numeric = dump.numerics[SIZE_NUMERIC_NAME]; + if (numeric === undefined) return 0; + return numeric.value; + } + + function hasSize(dump) { + return dump.numerics[SIZE_NUMERIC_NAME] !== undefined; + } + + function optional(value, defaultValue) { + if (value === undefined) return defaultValue; + return value; + } + + GlobalMemoryDump.prototype = { + __proto__: tr.model.ContainerMemoryDump.prototype, + + get userFriendlyName() { + return 'Global memory dump at ' + + tr.b.Unit.byName.timeStampInMs.format(this.start); + }, + + get containerName() { + return 'global space'; + }, + + finalizeGraph() { + // 1. Transitively remove weak memory allocator dumps and all their + // owners and descendants from the model. This must be performed before + // any other steps. + this.removeWeakDumps(); + + // 2. Add ownership links from tracing MADs to descendants of malloc or + // winheap MADs so that tracing would be automatically discounted from + // them later (step 3). + this.setUpTracingOverheadOwnership(); + + // 3. Aggregate all other numerics of all MADs (*excluding* sizes and + // effective sizes) and propagate numerics from global MADs to their + // owners (*including* sizes and effective sizes). This step must be + // carried out before the sizes of all MADs are calculated (step 3). + // Otherwise, the propagated sizes of all MADs would not be aggregated. + this.aggregateNumerics(); + + // 4. Calculate the sizes of all memory allocator dumps (MADs). This step + // requires that the memory allocator dump graph has been finalized (step + // 1) and numerics were propagated from global MADs (step 2). Subsequent + // modifications of the graph will most likely break the calculation + // invariants. + this.calculateSizes(); + + // 5. Calculate the effective sizes of all MADs. This step requires that + // the sizes of all MADs have already been calculated (step 3). + this.calculateEffectiveSizes(); + + // 6. Discount tracing from VM regions stats. This steps requires that + // resident sizes (step 2) and sizes (step 3) of the tracing MADs have + // already been calculated. + this.discountTracingOverheadFromVmRegions(); + + // 7. The above steps (especially steps 1 and 3) can create new memory + // allocator dumps, so we force rebuilding the memory allocator dump + // indices of all container memory dumps. + this.forceRebuildingMemoryAllocatorDumpByFullNameIndices(); + }, + + removeWeakDumps() { + // Mark all transitive owners and children of weak memory allocator dumps + // as weak. + this.traverseAllocatorDumpsInDepthFirstPreOrder(function(dump) { + if (dump.weak) return; + if ((dump.owns !== undefined && dump.owns.target.weak) || + (dump.parent !== undefined && dump.parent.weak)) { + dump.weak = true; + } + }); + + function removeWeakDumpsFromListRecursively(dumps) { + tr.b.inPlaceFilter(dumps, function(dump) { + if (dump.weak) { + // The dump is weak, so remove it. This will implicitly remove all + // its descendants, which are also weak due to the initial marking + // step. + return false; + } + + // This dump is non-weak, so keep it. Recursively remove its weak + // descendants and ownership links from weak dumps instead. + removeWeakDumpsFromListRecursively(dump.children); + tr.b.inPlaceFilter(dump.ownedBy, function(ownershipLink) { + return !ownershipLink.source.weak; + }); + + return true; + }); + } + + this.iterateContainerDumps(function(containerDump) { + const memoryAllocatorDumps = containerDump.memoryAllocatorDumps; + if (memoryAllocatorDumps !== undefined) { + removeWeakDumpsFromListRecursively(memoryAllocatorDumps); + } + }); + }, + + /** + * Calculate the size of all memory allocator dumps in the dump graph. + * + * The size refers to the allocated size of a (sub)component. It is a + * natural extension of the optional size numeric provided by + * MemoryAllocatorDump(s): + * + * - If a MAD provides a size numeric, then its size is assumed to be + * equal to it. + * - If a MAD does not provide a size numeric, then its size is assumed + * to be the maximum of (1) the size of the largest owner of the MAD + * and (2) the aggregated size of the MAD's children. + * + * Metric motivation: "How big is a (sub)system?" + * + * Please refer to the Memory Dump Graph Metric Calculation design document + * for more details (https://goo.gl/fKg0dt). + */ + calculateSizes() { + this.traverseAllocatorDumpsInDepthFirstPostOrder( + this.calculateMemoryAllocatorDumpSize_.bind(this)); + }, + + /** + * Calculate the size of the given MemoryAllocatorDump. This method assumes + * that the size of both the children and owners of the dump has already + * been calculated. + */ + calculateMemoryAllocatorDumpSize_(dump) { + // This flag becomes true if the size numeric of the current dump should + // be defined, i.e. if (1) the current dump's size numeric is defined, + // (2) the size of at least one of its children is defined or (3) the + // size of at least one of its owners is defined. + let shouldDefineSize = false; + + // This helper function returns the value of the size numeric of the + // given dependent memory allocator dump. If the numeric is defined, the + // shouldDefineSize flag above is also set to true (because condition + // (2) or (3) is satisfied). Otherwise, zero is returned (and the flag is + // left unchanged). + function getDependencySize(dependencyDump) { + const numeric = dependencyDump.numerics[SIZE_NUMERIC_NAME]; + if (numeric === undefined) return 0; + shouldDefineSize = true; + return numeric.value; + } + + // 1. Get the size provided by the dump. If present, define a function + // for checking dependent size consistency (a dump must always be bigger + // than all its children aggregated together and/or its largest owner). + const sizeNumeric = dump.numerics[SIZE_NUMERIC_NAME]; + let size = 0; + let checkDependencySizeIsConsistent = function() { /* no-op */ }; + if (sizeNumeric !== undefined) { + size = sizeNumeric.value; + shouldDefineSize = true; + if (sizeNumeric.unit !== tr.b.Unit.byName.sizeInBytes_smallerIsBetter) { + this.model.importWarning({ + type: 'memory_dump_parse_error', + message: 'Invalid unit of \'size\' numeric of memory allocator ' + + 'dump ' + dump.quantifiedName + ': ' + + sizeNumeric.unit.unitName + '.' + }); + } + checkDependencySizeIsConsistent = function( + dependencySize, dependencyInfoType, dependencyName) { + if (size >= dependencySize) return; + this.model.importWarning({ + type: 'memory_dump_parse_error', + message: 'Size provided by memory allocator dump \'' + + dump.fullName + '\'' + + tr.b.Unit.byName.sizeInBytes.format(size) + + ') is less than ' + dependencyName + ' (' + + tr.b.Unit.byName.sizeInBytes.format(dependencySize) + ').' + }); + dump.infos.push({ + type: dependencyInfoType, + providedSize: size, + dependencySize + }); + }.bind(this); + } + + // 2. Aggregate size of children. The recursive function traverses all + // descendants and ensures that double-counting due to ownership within a + // subsystem is avoided. + let aggregatedChildrenSize = 0; + // Owned child dump name -> (Owner child dump name -> overlapping size). + const allOverlaps = {}; + dump.children.forEach(function(childDump) { + function aggregateDescendantDump(descendantDump) { + // Don't count this descendant dump if it owns another descendant of + // the current dump (would cause double-counting). + const ownedDumpLink = descendantDump.owns; + if (ownedDumpLink !== undefined && + ownedDumpLink.target.isDescendantOf(dump)) { + // If the target owned dump is a descendant of a *different* child + // of the the current dump (i.e. not childDump), then we remember + // the ownership so that we could explain why the size of the + // current dump is not equal to the sum of its children. + let ownedChildDump = ownedDumpLink.target; + while (ownedChildDump.parent !== dump) { + ownedChildDump = ownedChildDump.parent; + } + if (childDump !== ownedChildDump) { + const ownedBySiblingSize = getDependencySize(descendantDump); + if (ownedBySiblingSize > 0) { + const previousTotalOwnedBySiblingSize = + ownedChildDump.ownedBySiblingSizes.get(childDump) || 0; + const updatedTotalOwnedBySiblingSize = + previousTotalOwnedBySiblingSize + ownedBySiblingSize; + ownedChildDump.ownedBySiblingSizes.set( + childDump, updatedTotalOwnedBySiblingSize); + } + } + return; + } + + // If this descendant dump is a leaf node, add its size to the + // aggregated size. + if (descendantDump.children.length === 0) { + aggregatedChildrenSize += getDependencySize(descendantDump); + return; + } + + // If this descendant dump is an intermediate node, recurse down into + // its children. Note that the dump's size is NOT added because it is + // an aggregate of its children (would cause double-counting). + descendantDump.children.forEach(aggregateDescendantDump); + } + aggregateDescendantDump(childDump); + }); + checkDependencySizeIsConsistent( + aggregatedChildrenSize, + PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN, + 'the aggregated size of its children'); + + // 3. Calculate the largest owner size. + let largestOwnerSize = 0; + dump.ownedBy.forEach(function(ownershipLink) { + const owner = ownershipLink.source; + const ownerSize = getDependencySize(owner); + largestOwnerSize = Math.max(largestOwnerSize, ownerSize); + }); + checkDependencySizeIsConsistent( + largestOwnerSize, + PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER, + 'the size of its largest owner'); + + // If neither the dump nor any of its dependencies (children and owners) + // provide a size, do NOT add a zero size numeric. + if (!shouldDefineSize) { + // The rest of the pipeline relies on size being either a valid + // Scalar, or undefined. + delete dump.numerics[SIZE_NUMERIC_NAME]; + return; + } + + // A dump must always be bigger than all its children aggregated + // together and/or its largest owner. + size = Math.max(size, aggregatedChildrenSize, largestOwnerSize); + + dump.numerics[SIZE_NUMERIC_NAME] = new tr.b.Scalar( + tr.b.Unit.byName.sizeInBytes_smallerIsBetter, size); + + // Add a virtual child to make up for extra size of the dump with + // respect to its children (if applicable). + if (aggregatedChildrenSize < size && + dump.children !== undefined && dump.children.length > 0) { + const virtualChild = new tr.model.MemoryAllocatorDump( + dump.containerMemoryDump, dump.fullName + '/<unspecified>'); + virtualChild.parent = dump; + dump.children.unshift(virtualChild); + virtualChild.numerics[SIZE_NUMERIC_NAME] = new tr.b.Scalar( + tr.b.Unit.byName.sizeInBytes_smallerIsBetter, + size - aggregatedChildrenSize); + } + }, + + /** + * Calculate the effective size of all memory allocator dumps in the dump + * graph. + * + * The effective size refers to the amount of memory a particular component + * is using/consuming. In other words, every (reported) byte of used memory + * is uniquely attributed to exactly one component. Consequently, unlike + * size, effective size is cumulative, i.e. the sum of the effective sizes + * of (top-level) components is equal to the total amount of (reported) + * used memory. + * + * Metric motivation: "How much memory does a (sub)system use?" or "For how + * much memory should a (sub)system be 'charged'?" + * + * Please refer to the Memory Dump Graph Metric Calculation design document + * for more details (https://goo.gl/fKg0dt). + * + * This method assumes that the size of all contained memory allocator + * dumps has already been calculated [see calculateSizes()]. + */ + calculateEffectiveSizes() { + // 1. Calculate not-owned and not-owning sub-sizes of all MADs + // (depth-first post-order traversal). + this.traverseAllocatorDumpsInDepthFirstPostOrder( + this.calculateDumpSubSizes_.bind(this)); + + // 2. Calculate owned and owning coefficients of owned and owner MADs + // respectively (arbitrary traversal). + this.traverseAllocatorDumpsInDepthFirstPostOrder( + this.calculateDumpOwnershipCoefficient_.bind(this)); + + // 3. Calculate cumulative owned and owning coefficients of all MADs + // (depth-first pre-order traversal). + this.traverseAllocatorDumpsInDepthFirstPreOrder( + this.calculateDumpCumulativeOwnershipCoefficient_.bind(this)); + + // 4. Calculate the effective sizes of all MADs (depth-first post-order + // traversal). + this.traverseAllocatorDumpsInDepthFirstPostOrder( + this.calculateDumpEffectiveSize_.bind(this)); + }, + + /** + * Calculate not-owned and not-owning sub-sizes of a memory allocator dump + * from its children's (sub-)sizes. + * + * Not-owned sub-size refers to the aggregated memory of all children which + * is not owned by other MADs. Conversely, not-owning sub-size is the + * aggregated memory of all children which do not own another MAD. The + * diagram below illustrates these two concepts: + * + * ROOT 1 ROOT 2 + * size: 4 size: 5 + * not-owned sub-size: 4 not-owned sub-size: 1 (!) + * not-owning sub-size: 0 (!) not-owning sub-size: 5 + * + * ^ ^ + * | | + * + * PARENT 1 ===== owns =====> PARENT 2 + * size: 4 size: 5 + * not-owned sub-size: 4 not-owned sub-size: 5 + * not-owning sub-size: 4 not-owning sub-size: 5 + * + * ^ ^ + * | | + * + * CHILD 1 CHILD 2 + * size [given]: 4 size [given]: 5 + * not-owned sub-size: 4 not-owned sub-size: 5 + * not-owning sub-size: 4 not-owning sub-size: 5 + * + * This method assumes that (1) the size of the dump, its children, and its + * owners [see calculateSizes()] and (2) the not-owned and not-owning + * sub-sizes of both the children and owners of the dump have already been + * calculated [depth-first post-order traversal]. + */ + calculateDumpSubSizes_(dump) { + // Completely skip dumps with undefined size. + if (!hasSize(dump)) return; + + // If the dump is a leaf node, then both sub-sizes are equal to the size. + if (dump.children === undefined || dump.children.length === 0) { + const size = getSize(dump); + dump.notOwningSubSize_ = size; + dump.notOwnedSubSize_ = size; + return; + } + + // Calculate this dump's not-owning sub-size by summing up the not-owning + // sub-sizes of children MADs which do not own another MAD. + let notOwningSubSize = 0; + dump.children.forEach(function(childDump) { + if (childDump.owns !== undefined) return; + notOwningSubSize += optional(childDump.notOwningSubSize_, 0); + }); + dump.notOwningSubSize_ = notOwningSubSize; + + // Calculate this dump's not-owned sub-size. + let notOwnedSubSize = 0; + dump.children.forEach(function(childDump) { + // If the child dump is not owned, then add its not-owned sub-size. + if (childDump.ownedBy.length === 0) { + notOwnedSubSize += optional(childDump.notOwnedSubSize_, 0); + return; + } + // If the child dump is owned, then add the difference between its size + // and the largest owner. + let largestChildOwnerSize = 0; + childDump.ownedBy.forEach(function(ownershipLink) { + largestChildOwnerSize = Math.max( + largestChildOwnerSize, getSize(ownershipLink.source)); + }); + notOwnedSubSize += getSize(childDump) - largestChildOwnerSize; + }); + dump.notOwnedSubSize_ = notOwnedSubSize; + }, + + /** + * Calculate owned and owning coefficients of a memory allocator dump and + * its owners. + * + * The owning coefficient refers to the proportion of a dump's not-owning + * sub-size which is attributed to the dump (only relevant to owning MADs). + * Conversely, the owned coefficient is the proportion of a dump's + * not-owned sub-size, which is attributed to it (only relevant to owned + * MADs). + * + * The not-owned size of the owned dump is split among its owners in the + * order of the ownership importance as demonstrated by the following + * example: + * + * memory allocator dumps + * OWNED OWNER1 OWNER2 OWNER3 OWNER4 + * not-owned sub-size [given] 10 - - - - + * not-owning sub-size [given] - 6 7 5 8 + * importance [given] - 2 2 1 0 + * attributed not-owned sub-size 2 - - - - + * attributed not-owning sub-size - 3 4 0 1 + * owned coefficient 2/10 - - - - + * owning coefficient - 3/6 4/7 0/5 1/8 + * + * Explanation: Firstly, 6 bytes are split equally among OWNER1 and OWNER2 + * (highest importance). OWNER2 owns one more byte, so its attributed + * not-owning sub-size is 6/2 + 1 = 4 bytes. OWNER3 is attributed no size + * because it is smaller than the owners with higher priority. However, + * OWNER4 is larger, so it's attributed the difference 8 - 7 = 1 byte. + * Finally, 2 bytes remain unattributed and are hence kept in the OWNED + * dump as attributed not-owned sub-size. The coefficients are then + * directly calculated as fractions of the sub-sizes and corresponding + * attributed sub-sizes. + * + * Note that we always assume that all ownerships of a dump overlap (e.g. + * OWNER3 is subsumed by both OWNER1 and OWNER2). Hence, the table could + * be alternatively represented as follows: + * + * owned memory range + * 0 1 2 3 4 5 6 7 8 9 10 + * Priority 2 | OWNER1 + OWNER2 (split) | OWNER2 | + * Priority 1 | (already attributed) | + * Priority 0 | - - - (already attributed) - - - | OWNER4 | + * Remainder | - - - - - (already attributed) - - - - - - | OWNED | + * + * This method assumes that (1) the size of the dump [see calculateSizes()] + * and (2) the not-owned size of the dump and not-owning sub-sizes of its + * owners [see the first step of calculateEffectiveSizes()] have already + * been calculated. Note that the method doesn't make any assumptions about + * the order in which dumps are visited. + */ + calculateDumpOwnershipCoefficient_(dump) { + // Completely skip dumps with undefined size. + if (!hasSize(dump)) return; + + // We only need to consider owned dumps. + if (dump.ownedBy.length === 0) return; + + // Sort the owners in decreasing order of ownership importance and + // increasing order of not-owning sub-size (in case of equal importance). + const owners = dump.ownedBy.map(function(ownershipLink) { + return { + dump: ownershipLink.source, + importance: optional(ownershipLink.importance, 0), + notOwningSubSize: optional(ownershipLink.source.notOwningSubSize_, 0) + }; + }); + owners.sort(function(a, b) { + if (a.importance === b.importance) { + return a.notOwningSubSize - b.notOwningSubSize; + } + return b.importance - a.importance; + }); + + // Loop over the list of owners and distribute the owned dump's not-owned + // sub-size among them according to their ownership importance and + // not-owning sub-size. + let currentImportanceStartPos = 0; + let alreadyAttributedSubSize = 0; + while (currentImportanceStartPos < owners.length) { + const currentImportance = owners[currentImportanceStartPos].importance; + + // Find the position of the first owner with lower priority. + let nextImportanceStartPos = currentImportanceStartPos + 1; + while (nextImportanceStartPos < owners.length && + owners[nextImportanceStartPos].importance === + currentImportance) { + nextImportanceStartPos++; + } + + // Visit the owners with the same importance in increasing order of + // not-owned sub-size, split the owned memory among them appropriately, + // and calculate their owning coefficients. + let attributedNotOwningSubSize = 0; + for (let pos = currentImportanceStartPos; pos < nextImportanceStartPos; + pos++) { + const owner = owners[pos]; + const notOwningSubSize = owner.notOwningSubSize; + if (notOwningSubSize > alreadyAttributedSubSize) { + attributedNotOwningSubSize += + (notOwningSubSize - alreadyAttributedSubSize) / + (nextImportanceStartPos - pos); + alreadyAttributedSubSize = notOwningSubSize; + } + + let owningCoefficient = 0; + if (notOwningSubSize !== 0) { + owningCoefficient = attributedNotOwningSubSize / notOwningSubSize; + } + owner.dump.owningCoefficient_ = owningCoefficient; + } + + currentImportanceStartPos = nextImportanceStartPos; + } + + // Attribute the remainder of the owned dump's not-owned sub-size to + // the dump itself and calculate its owned coefficient. + const notOwnedSubSize = optional(dump.notOwnedSubSize_, 0); + const remainderSubSize = notOwnedSubSize - alreadyAttributedSubSize; + let ownedCoefficient = 0; + if (notOwnedSubSize !== 0) { + ownedCoefficient = remainderSubSize / notOwnedSubSize; + } + dump.ownedCoefficient_ = ownedCoefficient; + }, + + /** + * Calculate cumulative owned and owning coefficients of a memory allocator + * dump from its (non-cumulative) owned and owning coefficients and the + * cumulative coefficients of its parent and/or owned dump. + * + * The cumulative coefficients represent the total effect of all + * (non-strict) ancestor ownerships on a memory allocator dump. The + * cumulative owned coefficient of a MAD can be calculated simply as: + * + * cumulativeOwnedC(M) = ownedC(M) * cumulativeOwnedC(parent(M)) + * + * This reflects the assumption that if a parent of a child MAD is + * (partially) owned, then the parent's owner also indirectly owns (a part + * of) the child MAD. + * + * The cumulative owning coefficient of a MAD depends on whether the MAD + * owns another dump: + * + * [if M doesn't own another MAD] + * / cumulativeOwningC(parent(M)) + * cumulativeOwningC(M) = + * \ [if M owns another MAD] + * owningC(M) * cumulativeOwningC(owned(M)) + * + * The reasoning behind the first case is similar to the one for cumulative + * owned coefficient above. The only difference is that we don't need to + * include the dump's (non-cumulative) owning coefficient because it is + * implicitly 1. + * + * The formula for the second case is derived as follows: Since the MAD + * owns another dump, its memory is not included in its parent's not-owning + * sub-size and hence shouldn't be affected by the parent's corresponding + * cumulative coefficient. Instead, the MAD indirectly owns everything + * owned by its owned dump (and so it should be affected by the + * corresponding coefficient). + * + * Note that undefined coefficients (and coefficients of non-existent + * dumps) are implicitly assumed to be 1. + * + * This method assumes that (1) the size of the dump [see calculateSizes()], + * (2) the (non-cumulative) owned and owning coefficients of the dump [see + * the second step of calculateEffectiveSizes()], and (3) the cumulative + * coefficients of the dump's parent and owned MADs (if present) + * [depth-first pre-order traversal] have already been calculated. + */ + calculateDumpCumulativeOwnershipCoefficient_(dump) { + // Completely skip dumps with undefined size. + if (!hasSize(dump)) return; + + let cumulativeOwnedCoefficient = optional(dump.ownedCoefficient_, 1); + const parent = dump.parent; + if (dump.parent !== undefined) { + cumulativeOwnedCoefficient *= dump.parent.cumulativeOwnedCoefficient_; + } + dump.cumulativeOwnedCoefficient_ = cumulativeOwnedCoefficient; + + let cumulativeOwningCoefficient; + if (dump.owns !== undefined) { + cumulativeOwningCoefficient = dump.owningCoefficient_ * + dump.owns.target.cumulativeOwningCoefficient_; + } else if (dump.parent !== undefined) { + cumulativeOwningCoefficient = dump.parent.cumulativeOwningCoefficient_; + } else { + cumulativeOwningCoefficient = 1; + } + dump.cumulativeOwningCoefficient_ = cumulativeOwningCoefficient; + }, + + /** + * Calculate the effective size of a memory allocator dump. + * + * In order to simplify the (already complex) calculation, we use the fact + * that effective size is cumulative (unlike regular size), i.e. the + * effective size of a non-leaf node is equal to the sum of effective sizes + * of its children. The effective size of a leaf MAD is calculated as: + * + * effectiveSize(M) = size(M) * cumulativeOwningC(M) * cumulativeOwnedC(M) + * + * This method assumes that (1) the size of the dump and its children [see + * calculateSizes()] and (2) the cumulative owning and owned coefficients + * of the dump (if it's a leaf node) [see the third step of + * calculateEffectiveSizes()] or the effective sizes of its children (if + * it's a non-leaf node) [depth-first post-order traversal] have already + * been calculated. + */ + calculateDumpEffectiveSize_(dump) { + // Completely skip dumps with undefined size. As a result, each dump will + // have defined effective size if and only if it has defined size. + if (!hasSize(dump)) { + // The rest of the pipeline relies on effective size being either a + // valid Scalar, or undefined. + delete dump.numerics[EFFECTIVE_SIZE_NUMERIC_NAME]; + return; + } + + let effectiveSize; + if (dump.children === undefined || dump.children.length === 0) { + // Leaf dump. + effectiveSize = getSize(dump) * dump.cumulativeOwningCoefficient_ * + dump.cumulativeOwnedCoefficient_; + } else { + // Non-leaf dump. + effectiveSize = 0; + dump.children.forEach(function(childDump) { + if (!hasSize(childDump)) return; + effectiveSize += + childDump.numerics[EFFECTIVE_SIZE_NUMERIC_NAME].value; + }); + } + dump.numerics[EFFECTIVE_SIZE_NUMERIC_NAME] = new tr.b.Scalar( + tr.b.Unit.byName.sizeInBytes_smallerIsBetter, effectiveSize); + }, + + aggregateNumerics() { + // 1. Aggregate numerics in this global memory dump. + this.iterateRootAllocatorDumps(function(dump) { + dump.aggregateNumericsRecursively(this.model); + }); + + // 2. Propagate numerics and diagnostics from global memory allocator + // dumps to their owners. + this.iterateRootAllocatorDumps( + this.propagateNumericsAndDiagnosticsRecursively); + + // 3. Aggregate numerics in the associated process memory dumps. + for (const processMemoryDump of Object.values(this.processMemoryDumps)) { + processMemoryDump.iterateRootAllocatorDumps(function(dump) { + dump.aggregateNumericsRecursively(this.model); + }, this); + } + }, + + propagateNumericsAndDiagnosticsRecursively(globalAllocatorDump) { + ['numerics', 'diagnostics'].forEach(function(field) { + for (const [name, value] of + Object.entries(globalAllocatorDump[field])) { + globalAllocatorDump.ownedBy.forEach(function(ownershipLink) { + const processAllocatorDump = ownershipLink.source; + if (processAllocatorDump[field][name] !== undefined) { + // Numerics and diagnostics provided by process memory allocator + // dumps themselves have precedence over numerics and diagnostics + // propagated from global memory allocator dumps. + return; + } + processAllocatorDump[field][name] = value; + }); + } + }); + + // Recursively propagate numerics from all child memory allocator dumps. + globalAllocatorDump.children.forEach( + this.propagateNumericsAndDiagnosticsRecursively, this); + }, + + setUpTracingOverheadOwnership() { + for (const dump of Object.values(this.processMemoryDumps)) { + dump.setUpTracingOverheadOwnership(this.model); + } + }, + + discountTracingOverheadFromVmRegions() { + // TODO(petrcermak): Consider factoring out all the finalization code and + // constants to a single file. + for (const dump of Object.values(this.processMemoryDumps)) { + dump.discountTracingOverheadFromVmRegions(this.model); + } + }, + + forceRebuildingMemoryAllocatorDumpByFullNameIndices() { + this.iterateContainerDumps(function(containerDump) { + containerDump.forceRebuildingMemoryAllocatorDumpByFullNameIndex(); + }); + }, + + iterateContainerDumps(fn) { + fn.call(this, this); + for (const processDump of Object.values(this.processMemoryDumps)) { + fn.call(this, processDump); + } + }, + + iterateAllRootAllocatorDumps(fn) { + this.iterateContainerDumps(function(containerDump) { + containerDump.iterateRootAllocatorDumps(fn, this); + }); + }, + + /** + * Traverse the memory dump graph in a depth first post-order, i.e. + * children and owners of a memory allocator dump are visited before the + * dump itself. This method will throw an exception if the graph contains + * a cycle. + */ + traverseAllocatorDumpsInDepthFirstPostOrder(fn) { + const visitedDumps = new WeakSet(); + const openDumps = new WeakSet(); + + function visit(dump) { + if (visitedDumps.has(dump)) return; + + if (openDumps.has(dump)) { + throw new Error(dump.userFriendlyName + ' contains a cycle'); + } + openDumps.add(dump); + + // Visit owners before the dumps they own. + dump.ownedBy.forEach(function(ownershipLink) { + visit.call(this, ownershipLink.source); + }, this); + + // Visit children before parents. + dump.children.forEach(visit, this); + + // Actually visit the current memory allocator dump. + fn.call(this, dump); + visitedDumps.add(dump); + + openDumps.delete(dump); + } + + this.iterateAllRootAllocatorDumps(visit); + }, + + /** + * Traverse the memory dump graph in a depth first pre-order, i.e. + * children and owners of a memory allocator dump are visited after the + * dump itself. This method will not visit some dumps if the graph contains + * a cycle. + */ + traverseAllocatorDumpsInDepthFirstPreOrder(fn) { + const visitedDumps = new WeakSet(); + + function visit(dump) { + if (visitedDumps.has(dump)) return; + + // If this dumps owns another dump which hasn't been visited yet, then + // wait for this dump to be visited later. + if (dump.owns !== undefined && !visitedDumps.has(dump.owns.target)) { + return; + } + + // If this dump's parent hasn't been visited yet, then wait for this + // dump to be visited later. + if (dump.parent !== undefined && !visitedDumps.has(dump.parent)) { + return; + } + + // Actually visit the current memory allocator dump. + fn.call(this, dump); + visitedDumps.add(dump); + + // Visit owners after the dumps they own. + dump.ownedBy.forEach(function(ownershipLink) { + visit.call(this, ownershipLink.source); + }, this); + + // Visit children after parents. + dump.children.forEach(visit, this); + } + + this.iterateAllRootAllocatorDumps(visit); + } + }; + + tr.model.EventRegistry.register( + GlobalMemoryDump, + { + name: 'globalMemoryDump', + pluralName: 'globalMemoryDumps' + }); + + return { + GlobalMemoryDump, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump_test.html b/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump_test.html new file mode 100644 index 00000000000..219ce0f28d8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/global_memory_dump_test.html @@ -0,0 +1,3954 @@ +<!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/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/importer/import.html"> +<link rel="import" href="/tracing/model/global_memory_dump.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/process_memory_dump.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const GlobalMemoryDump = tr.model.GlobalMemoryDump; + const ProcessMemoryDump = tr.model.ProcessMemoryDump; + const MemoryAllocatorDump = tr.model.MemoryAllocatorDump; + const MemoryAllocatorDumpLink = tr.model.MemoryAllocatorDumpLink; + const Scalar = tr.b.Scalar; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump; + const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump; + const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink; + const checkDumpNumericsAndDiagnostics = + tr.model.MemoryDumpTestUtils.checkDumpNumericsAndDiagnostics; + const SIZE_DELTA = tr.model.MemoryDumpTestUtils.SIZE_DELTA; + const MemoryAllocatorDumpInfoType = tr.model.MemoryAllocatorDumpInfoType; + const PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN = + MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN; + const PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER = + MemoryAllocatorDumpInfoType.PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER; + + function buildArgPusher(array) { + return function(arg) { array.push(arg); }; + } + + function assertEqualUniqueMembers(actualArray, expectedArray) { + assert.lengthOf(actualArray, expectedArray.length); + assert.sameMembers(actualArray, expectedArray); + } + + function assertUndefinedNumeric(dump, numericName) { + const numeric = dump.numerics[numericName]; + assert.isUndefined(numeric, 'expected numeric \'' + numericName + + '\' of memory allocator dump \'' + dump.fullName + '\' in ' + + dump.containerMemoryDump.userFriendlyName + ' to be undefined'); + } + + function assertDefinedNumeric(dump, numericName, expectedUnit, expectedValue, + opt_delta) { + const numeric = dump.numerics[numericName]; + const errorMessagePrefix = 'expected numeric \'' + numericName + + '\' of memory allocator dump \'' + dump.fullName + '\' in ' + + dump.containerMemoryDump.userFriendlyName + ' to '; + + assert.instanceOf(numeric, Scalar, + errorMessagePrefix + 'be an instance of Scalar'); + assert.strictEqual(numeric.unit, expectedUnit, + errorMessagePrefix + 'have unit \'' + expectedUnit.unitName + + '\' but got \'' + numeric.unit.unitName + '\' instead'); + + const valueErrorMessage = errorMessagePrefix + 'have value \'' + + expectedValue + '\' but got \'' + numeric.value + '\''; + if (opt_delta !== undefined) { + assert.closeTo( + numeric.value, expectedValue, opt_delta, valueErrorMessage); + } else { + assert.strictEqual(numeric.value, expectedValue, valueErrorMessage); + } + } + + function assertSizeNumeric(dump, sizeName, expectedValue) { + if (expectedValue === undefined) { + assertUndefinedNumeric(dump, sizeName); + } else { + assertDefinedNumeric(dump, sizeName, sizeInBytes_smallerIsBetter, + expectedValue, SIZE_DELTA); + } + } + + function assertDumpSizes(dump, expectedSize, expectedEffectiveSize, + opt_expectedInfos, opt_expectedOwnedBySiblingSizes) { + // Check the 'size' numeric. + assertSizeNumeric(dump, 'size', expectedSize); + + // Check the 'effective_size' numeric. + assertSizeNumeric(dump, 'effective_size', expectedEffectiveSize); + + // Check the 'infos' list. + const expectedInfos = opt_expectedInfos || []; + const actualInfos = dump.infos; + assert.lengthOf(actualInfos, expectedInfos.length, + 'expected memory allocator dump \'' + dump.fullName + '\' in ' + + dump.containerMemoryDump.userFriendlyName + ' to have ' + + expectedInfos.length + ' infos but got ' + actualInfos.length); + for (let k = 0; k < actualInfos.length; k++) { + assert.deepEqual(actualInfos[k], expectedInfos[k], + 'info ' + k + ' of memory allocator dump \'' + dump.fullName + + '\' in ' + dump.containerMemoryDump.userFriendlyName + + ' doesn\'t match the expected info'); + } + + // Checked the 'ownedBySiblingSizes' map. + const expectedOwnedBySiblingSizes = opt_expectedOwnedBySiblingSizes || {}; + const actualOwnedBySiblingSizes = {}; + for (const siblingDump of dump.ownedBySiblingSizes.keys()) { + assert.strictEqual(siblingDump.parent, dump.parent); + actualOwnedBySiblingSizes[siblingDump.name] = + dump.ownedBySiblingSizes.get(siblingDump); + } + assert.deepEqual(actualOwnedBySiblingSizes, expectedOwnedBySiblingSizes, + 'ownedBySiblingSizes of memory allocator dump \'' + dump.fullName + + '\' in ' + dump.containerMemoryDump.userFriendlyName + + ' doesn\'t contain the expected values'); + } + + function createContainerDumps(processMemoryDumpCount, opt_model) { + let model = opt_model; + if (model === undefined) { + model = new Model(); + } + + const gmd = new GlobalMemoryDump(model, 0); + model.globalMemoryDumps.push(gmd); + + const pmds = []; + for (let i = 0; i < processMemoryDumpCount; i++) { + const process = model.getOrCreateProcess(i); + const pmd = new ProcessMemoryDump(gmd, process, 0); + gmd.processMemoryDumps[i] = pmd; + process.memoryDumps.push(pmd); + pmds.push(pmd); + } + + return [gmd].concat(pmds); + } + + /** + * Build container memory dumps from tree recipes. This function returns + * a list containing a global memory dump and zero or more process memory + * dumps constructed from the provided function argument as follows: + * + * allTreeRecipes (argument): + * + * [ + * [tree recipe GA, tree recipe GB, ...], + * [tree recipe P1A, tree recipe P1B, ...], + * [tree recipe P2A, tree recipe P2B ...], + * ... + * ] + * + * return value: + * + * [ + * GlobalMemoryDump with root MemoryAllocatorDump(s) [GA, GB, ...], + * ProcessMemoryDump with root MemoryAllocatorDump(s) [P1A, P1B, ...], + * ProcessMemoryDump with root MemoryAllocatorDump(s) [P2A, P2B, ...], + * ... + * ] + * + * where a tree recipe is an object (a recursive data structure) with the + * following fields: + * + * name: Name of the resulting MAD. + * guid: GUID of the resulting MAD (can be undefined). + * owns: GUID of another MAD owned by the resulting MAD (no owned MAD if + * undefined). + * importance: Importance of the above ownership (can be undefined). + * size: Value of the 'size' numeric of the resulting MAD (no 'size' + * numeric if undefined). + * numerics: Extra numerics of the resulting MAD (dictionary). + * diagnostics: Extra diagnostics of the resulting MAD (dictionary). + * weak: Whether the resulting MAD should be weak (undefined implies + * non-weak). + * children: List of tree recipes for child MADs (no children if undefined). + * skip_build: If this optional property is set to true, this function will + * skip the corresponding tree recipe node and will not create a MAD + * for it (not allowed in root recipes). + * + * Other fields (most importantly 'expected_size') of a tree recipe are + * ignored by this function. + */ + function buildDumpTrees(allTreeRecipes, opt_model) { + assert.isAbove(allTreeRecipes.length, 0); + + // owned GUID -> {dump: owner, importance: optional}. + const ownerDumps = {}; + const ownableDumps = {}; // ownable GUID -> ownable dump. + + function buildAndAddDumpTrees(containerDump, treeRecipes) { + if (treeRecipes === undefined) return; + + function buildDumpTreeRecursively(treeRecipe, namePrefix) { + const skipBuild = treeRecipe.skip_build; + const name = treeRecipe.name; + const guid = treeRecipe.guid; + const owns = treeRecipe.owns; + const size = treeRecipe.size; + const numerics = treeRecipe.numerics; + const diagnostics = treeRecipe.diagnostics; + const importance = treeRecipe.importance; + const weak = treeRecipe.weak; + + assert.notStrictEqual(skipBuild, true); + assert.isDefined(name); + + const fullName = namePrefix + name; + const dump = new MemoryAllocatorDump(containerDump, fullName, guid); + + if (size !== undefined) { + dump.addNumeric( + 'size', new Scalar(sizeInBytes_smallerIsBetter, size)); + } + if (guid !== undefined) { + assert.notProperty(guid, ownableDumps); + ownableDumps[guid] = dump; + } + if (owns !== undefined) { + if (!(owns in ownerDumps)) { + ownerDumps[owns] = []; + } + ownerDumps[owns].push({dump, importance}); + } else { + assert.isUndefined(importance); // Test sanity check. + } + + if (treeRecipe.children !== undefined) { + treeRecipe.children.forEach(function(childTreeRecipe) { + // Virtual children are added during size calculation. + if (childTreeRecipe.skip_build === true) return; + const childDump = + buildDumpTreeRecursively(childTreeRecipe, fullName + '/'); + childDump.parent = dump; + dump.children.push(childDump); + }); + } + + if (numerics !== undefined) { + for (const [name, item] of Object.entries(numerics)) { + dump.addNumeric(name, item); + } + } + if (diagnostics !== undefined) { + for (const [name, item] of Object.entries(diagnostics)) { + dump.addDiagnostic(name, item); + } + } + + if (weak) dump.weak = true; + + return dump; + } + + const memoryAllocatorDumps = treeRecipes.map(function(treeRecipe) { + return buildDumpTreeRecursively(treeRecipe, ''); + }); + containerDump.memoryAllocatorDumps = memoryAllocatorDumps; + } + + // Recursively build memory allocator dump trees for all container dumps. + const containerDumps = createContainerDumps( + allTreeRecipes.length - 1, opt_model); + for (let i = 0; i < allTreeRecipes.length; i++) { + buildAndAddDumpTrees(containerDumps[i], allTreeRecipes[i]); + } + + // Hook up ownership links. + for (const [ownedGuid, ownershipInfos] of Object.entries(ownerDumps)) { + const ownedDump = ownableDumps[ownedGuid]; + assert.isDefined(ownedDump, 'Tree recipes don\'t contain a memory ' + + 'allocator dump with guid \'' + ownedGuid + '\''); + + ownershipInfos.forEach(function(ownershipInfo) { + addOwnershipLink( + ownershipInfo.dump, ownedDump, ownershipInfo.importance); + }); + } + + return containerDumps; + } + + // Check that the buildDumpTrees testing helper method above builds a + // hierarchy of container and memory allocator dumps from tree recipes + // correctly. + test('testSanityCheck_buildDumpTrees', function() { + const containerDumps = buildDumpTrees([ + [ // GMD. + { + 'name': 'globalSharedDump1', + 'size': 123 + }, + { + 'name': 'globalSharedDump2', + 'subsystem_size': 999, + 'owns': 7, + 'importance': -1 + } + ], + undefined, // PMD1. + [ // PMD2. + { + 'name': 'v8', + 'children': [ + { + 'name': 'isolate1', + 'guid': 7, + 'weak': true + }, + { + 'name': 'isolate2', + 'skip_build': true + }, + { + 'name': 'isolate3', + 'size': 54, + 'guid': 60, + 'children': [ + { + 'name': 'obj1', + 'size': 89, + 'guid': 3 + }, + { + 'name': 'obj2', + 'owns': 3, + 'weak': true + }, + { + 'name': 'obj3', + 'owns': 3, + 'importance': 2 + } + ] + } + ] + } + ] + ]); + assert.lengthOf(containerDumps, 3); + const gmd = containerDumps[0]; + const pmd1 = containerDumps[1]; + const pmd2 = containerDumps[2]; + + function checkDump(dump, expectedGuid, expectedFullName, expectedParent, + expectedChildrenCount, expectedSize, expectedIsOwner, + expectedOwnersCount, expectedContainerDump, opt_expectedWeak) { + assert.isDefined(dump); + assert.instanceOf(dump, MemoryAllocatorDump); + assert.strictEqual(dump.guid, expectedGuid); + assert.strictEqual(dump.fullName, expectedFullName); + assert.strictEqual(dump.parent, expectedParent); + assert.lengthOf(dump.children, expectedChildrenCount); + + assertSizeNumeric(dump, 'size', expectedSize); + assertSizeNumeric(dump, 'subsystem_size', undefined); + + if (expectedIsOwner) { + assert.isDefined(dump.owns); + } else { + assert.isUndefined(dump.owns); + } + assert.lengthOf(dump.ownedBy, expectedOwnersCount); + + assert.strictEqual(dump.containerMemoryDump, expectedContainerDump); + assert.strictEqual(expectedContainerDump.getMemoryAllocatorDumpByFullName( + expectedFullName), dump); + assert.strictEqual(dump.weak, !!opt_expectedWeak); + } + + function checkOwnershipLink(expectedSourceDump, expectedTargetDump, + expectedImportance) { + const link = expectedSourceDump.owns; + assert.isDefined(link); + assert.instanceOf(link, MemoryAllocatorDumpLink); + assert.strictEqual(link.source, expectedSourceDump); + assert.strictEqual(link.target, expectedTargetDump); + assert.strictEqual(link.importance, expectedImportance); + assert.include(expectedTargetDump.ownedBy, link); + } + + // GMD memory allocator dumps. + assert.lengthOf(gmd.memoryAllocatorDumps, 2); + const globalSharedDump1 = gmd.memoryAllocatorDumps[0]; + checkDump(globalSharedDump1, undefined, 'globalSharedDump1', undefined, 0, + 123, false, 0, gmd); + const globalSharedDump2 = gmd.memoryAllocatorDumps[1]; + checkDump(globalSharedDump2, undefined, 'globalSharedDump2', undefined, 0, + undefined, true, 0, gmd); + + // PMD1 memory allocator dumps. + assert.isUndefined(pmd1.memoryAllocatorDumps); + + // PMD2 memory allocator dumps. + assert.lengthOf(pmd2.memoryAllocatorDumps, 1); + const v8Dump = pmd2.memoryAllocatorDumps[0]; + checkDump(v8Dump, undefined, 'v8', undefined, 2, undefined, false, 0, + pmd2); + const isolate1Dump = v8Dump.children[0]; + checkDump(isolate1Dump, 7, 'v8/isolate1', v8Dump, 0, undefined, false, 1, + pmd2, true /* weak dump */); + const isolate3Dump = v8Dump.children[1]; + checkDump(isolate3Dump, 60, 'v8/isolate3', v8Dump, 3, 54, false, 0, pmd2); + const obj1Dump = isolate3Dump.children[0]; + checkDump(obj1Dump, 3, 'v8/isolate3/obj1', isolate3Dump, 0, 89, false, 2, + pmd2); + const obj2Dump = isolate3Dump.children[1]; + checkDump(obj2Dump, undefined, 'v8/isolate3/obj2', isolate3Dump, 0, + undefined, true, 0, pmd2, true /* weak dump */); + const obj3Dump = isolate3Dump.children[2]; + checkDump(obj3Dump, undefined, 'v8/isolate3/obj3', isolate3Dump, 0, + undefined, true, 0, pmd2); + + // Ownership links. + checkOwnershipLink(globalSharedDump2, isolate1Dump, -1); + checkOwnershipLink(obj2Dump, obj1Dump, undefined); + checkOwnershipLink(obj3Dump, obj1Dump, 2); + }); + + /** + * Check that container memory dumps have the expected structure with sizes + * as described by tree recipes. The fields of a tree recipe are used by this + * function to check the properties of a MemoryAllocatorDump as follows (see + * the buildDumpTrees documentation for more details about the structure of + * tree recipes): + * + * name: Expected name of the MAD. + * expected_removed: If provided and true, it is expected that there is no + * dump for the recipe. + * expected_size: Expected value of the 'size' numeric of the MAD (no + * 'size' numeric expected if undefined). + * expected_effective_size: Expected value of the 'effective_size' + * numeric of the MAD (no 'effective_size' numeric expected if + * undefined). + * expected_infos: List of expected MAD infos (zero infos expected if + * undefined). + * weak: Whether the MAD is expected to be weak (non-weak if undefined). + * owns: Expected GUID of the dump owned by the MAD. + * importance: Expected importance of the owhership from this MAD. + * expected_owned_by_links_count: Expected number of 'ownedBy' links of the + * MAD. + * children: List of tree recipes for child MADs (no children expected if + * undefined). + * + * Other fields of a tree recipe (including 'skip_build') are ignored by this + * function. + */ + function checkDumpTrees(containerDumps, allTreeRecipes) { + assert.lengthOf(containerDumps, allTreeRecipes.length); + + for (let i = 0; i < containerDumps.length; i++) { + const containerDump = containerDumps[i]; + const treeRecipes = allTreeRecipes[i]; + + const memoryAllocatorDumps = containerDump.memoryAllocatorDumps; + if (treeRecipes === undefined) { + assert.isUndefined(memoryAllocatorDumps, + 'expected undefined memory allocator dumps in ' + + containerDump.userFriendlyName); + continue; + } + + const expectedTreeRecipes = treeRecipes.filter(function(treeRecipe) { + return !treeRecipe.expected_removed; + }); + + assert.isDefined(memoryAllocatorDumps, + 'expected defined memory allocator dumps in ' + + containerDump.userFriendlyName); + assert.lengthOf(memoryAllocatorDumps, expectedTreeRecipes.length, + 'expected ' + expectedTreeRecipes.length + ' root memory allocator ' + + 'dumps but got ' + memoryAllocatorDumps.length + ' in ' + + containerDump.userFriendlyName); + + function checkDumpTree(dump, treeRecipe, expectedParent, namePrefix) { + // Test sanity check. + assert.isFalse(!!treeRecipe.expected_removed); + + // Check full name, parent, and container dump. + const expectedFullName = namePrefix + treeRecipe.name; + const quantifiedName = dump.quantifiedName; + assert.strictEqual(dump.fullName, expectedFullName, + quantifiedName + ' has invalid full name'); + assert.strictEqual(dump.parent, expectedParent, + quantifiedName + ' has invalid parent'); + assert.strictEqual(dump.containerMemoryDump, containerDump, + quantifiedName + ' has invalid container memory dump'); + assert.strictEqual(containerDump.getMemoryAllocatorDumpByFullName( + expectedFullName), dump, quantifiedName + + 'is not indexed in its container memory dump'); + + // Check the guid of the dump. + assert.strictEqual(dump.guid, treeRecipe.guid, + quantifiedName + ' has invalid guid'); + + // Check that the 'weak' flag is correct. + assert.strictEqual(dump.weak, !!treeRecipe.weak, + quantifiedName + ' has invalid weak flag'); + + // Check that sizes were calculated correctly. + assertDumpSizes(dump, + treeRecipe.expected_size, + treeRecipe.expected_effective_size, + treeRecipe.expected_infos, + treeRecipe.expected_owned_by_sibling_sizes); + + // Check that the 'owns' link is correct. + if (treeRecipe.owns === undefined) { + assert.isUndefined(dump.owns, + quantifiedName + ' was expected not to own another dump'); + } else { + const ownershipLink = dump.owns; + assert.isDefined(dump.owns, quantifiedName + + ' was expected to have an \'owns\' link'); + assert.strictEqual(ownershipLink.source, dump, + 'the \'owns\' link of ' + quantifiedName + ' has invalid source'); + const expectedImportance = treeRecipe.importance; + assert.strictEqual(ownershipLink.importance, expectedImportance, + 'expected the importance of the \'owns\' link of ' + + quantifiedName + ' to be ' + expectedImportance + + ' but got ' + ownershipLink.importance); + const ownedDump = ownershipLink.target; + assert.strictEqual(ownedDump.guid, treeRecipe.owns, + 'the \'owns\' link of ' + quantifiedName + + ' has an invalid target'); + assert.include(ownedDump.ownedBy, ownershipLink, + 'the target of the \'owns\' link of ' + quantifiedName + + ' doesn\'t have the link in its \'ownedBy\' list'); + } + + // Check that the number of 'ownedBy' links is correct. + const expectedOwnedByLinksCount = + treeRecipe.expected_owned_by_links_count; + if (expectedOwnedByLinksCount !== undefined) { + assert.lengthOf(dump.ownedBy, expectedOwnedByLinksCount, + 'expected ' + quantifiedName + ' to have ' + + expectedOwnedByLinksCount + ' \'ownedBy\' links but got ' + + dump.ownedBy.length); + } + + // Check children recursively. + const actualChildren = dump.children; + const expectedChildren = (treeRecipe.children || []).filter( + function(childRecipe) { + return !childRecipe.expected_removed; + }); + assert.lengthOf(actualChildren, expectedChildren.length, + 'expected ' + quantifiedName + ' to have ' + + expectedChildren.length + ' children but got ' + + actualChildren.length); + for (let k = 0; k < actualChildren.length; k++) { + checkDumpTree(actualChildren[k], expectedChildren[k], dump, + expectedFullName + '/'); + } + } + + for (let j = 0; j < memoryAllocatorDumps.length; j++) { + checkDumpTree( + memoryAllocatorDumps[j], expectedTreeRecipes[j], undefined, ''); + } + } + } + + // Check that the checkDumpTrees testing helper method above actually + // performs the expected checks. Since it will be used heavily throughout + // this file (where it is expected to pass), we only need to verify that it + // does indeed fail in several cases where it should. + test('testSanityCheck_checkDumpTrees_invalidName', function() { + const containerDumps = createContainerDumps(0); + const gmd = containerDumps[0]; + const v8Dump = new MemoryAllocatorDump(gmd, 'v8'); + const heapsDump = + new MemoryAllocatorDump(gmd, 'heaps'); // Should be 'v8/heaps'. + v8Dump.children.push(heapsDump); + heapsDump.parent = v8Dump; + gmd.memoryAllocatorDumps = [v8Dump]; + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'v8', + 'children': [ + { + 'name': 'heaps' + } + ] + } + ] + ]); + }, /'heaps'.*invalid full name/); + }); + + test('testSanityCheck_checkDumpTrees_invalidGuid', function() { + const containerDumps = createContainerDumps(0); + const gmd = containerDumps[0]; + gmd.memoryAllocatorDumps = [new MemoryAllocatorDump(gmd, 'v8', 42)]; + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'v8', + 'guid': 43 // This should be 42. + } + ] + ]); + }, /'v8'.*\binvalid guid\b/); + }); + + test('testSanityCheck_checkDumpTrees_invalidStructure', function() { + const containerDumps = createContainerDumps(0); + const gmd = containerDumps[0]; + const rootDump = new MemoryAllocatorDump(gmd, 'root'); + addChildDump(rootDump, 'child1'); + gmd.memoryAllocatorDumps = [rootDump]; + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'root', + 'children': [ + { + 'name': 'child1' + }, + { + // This child is not present in the dump. + 'name': 'child2', + 'skip_build': true + } + ] + } + ] + ]); + }, /expected.*\b2\b.*children.*got.*\b1\b/); + }); + + test('testSanityCheck_checkDumpTrees_invalidParentLink', function() { + const containerDumps = createContainerDumps(0); + const gmd = containerDumps[0]; + const rootDump = new MemoryAllocatorDump(gmd, 'root'); + const parentDump = addChildDump(rootDump, 'parent'); + const childDump = addChildDump(parentDump, 'child'); + childDump.parent = rootDump; // This should correctly be parentDump. + gmd.memoryAllocatorDumps = [rootDump]; + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'root', + 'children': [ + { + 'name': 'parent', + 'children': [ + { + 'name': 'child' + } + ] + } + ] + } + ] + ]); + }, 'invalid parent'); + }); + + test('testSanityCheck_checkDumpTrees_invalidSize', function() { + const containerDumps = createContainerDumps(1); + const gmd = containerDumps[0]; + const pmd = containerDumps[1]; + const rootDump = newAllocatorDump(pmd, 'root', {numerics: {size: 100}}); + addChildDump(rootDump, 'parent', {numerics: {size: 49}}); + pmd.memoryAllocatorDumps = [rootDump]; + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + undefined, + [ + { + 'name': 'root', + 'expected_size': 100, + 'children': [ + { + 'name': 'parent', + 'expected_size': 50 // This should be 49. + } + ] + } + ] + ]); + }, /expected.*'size'.*value.*\b50\b.*got.*\b49\b/); + }); + + test('testSanityCheck_checkDumpTrees_invalidEffectiveSize', function() { + const containerDumps = createContainerDumps(1); + const gmd = containerDumps[0]; + const pmd = containerDumps[1]; + const rootDump = newAllocatorDump(pmd, 'root', + {numerics: {effective_size: 99}}); + addChildDump(rootDump, 'parent', {numerics: {effective_size: 50}}); + pmd.memoryAllocatorDumps = [rootDump]; + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + undefined, + [ + { + 'name': 'root', + 'expected_effective_size': 100, // This should be 99. + 'children': [ + { + 'name': 'parent', + 'expected_effective_size': 50 + } + ] + } + ] + ]); + }, /expected.*'effective_size'.*value.*\b100\b.*got.*\b99\b/); + }); + + test('testSanityCheck_checkDumpTrees_invalidInfoCount', function() { + const containerDumps = createContainerDumps(0); + const gmd = containerDumps[0]; + gmd.memoryAllocatorDumps = [ + newAllocatorDump(gmd, 'v8', {numerics: {size: 50}}) + ]; + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'v8', + 'expected_size': 50, + 'expected_infos': [ + { + type: PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN, + providedSize: 50, + dependencySize: 60 + } + ] + } + ] + ]); + }, /expected.*'v8'.*\b1 infos\b.*\bgot\b.*\b0\b/); + }); + + test('testSanityCheck_checkDumpTrees_invalidInfo', function() { + const containerDumps = createContainerDumps(0); + const gmd = containerDumps[0]; + const v8Dump = newAllocatorDump(gmd, 'v8', {numerics: {size: 50}}); + v8Dump.infos.push({ + type: PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN, + providedSize: 40, + dependencySize: 50 + }); + gmd.memoryAllocatorDumps = [v8Dump]; + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'v8', + 'expected_size': 50, + 'expected_infos': [ + { + // Should be PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN below. + type: PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER, + providedSize: 40, + dependencySize: 50 + } + ] + } + ] + ]); + }, /\binfo 0\b.*'v8'.*\bexpected\b/); + }); + + test('testSanityCheck_checkDumpTrees_invalidOwnedBySiblingSizes', function() { + const containerDumps = createContainerDumps(0); + const gmd = containerDumps[0]; + const v8Dump = new MemoryAllocatorDump(gmd, 'v8'); + addChildDump(v8Dump, 'child1', {guid: 42}); + addChildDump(v8Dump, 'child2'); + gmd.memoryAllocatorDumps = [v8Dump]; + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'v8', + 'children': [ + { + 'name': 'child1', + 'guid': 42 + }, + { + 'name': 'child2', + 'expected_owned_by_sibling_sizes': { + 'child1': 40 // This should be 30. + } + } + ] + } + ] + ]); + }, /\bownedBySiblingSizes\b.*'v8\/child2'.*\bexpected\b/); + }); + + test('testSanityCheck_checkDumpTrees_invalidWeakFlag', + function() { + const containerDumps = createContainerDumps(0); + const gmd = containerDumps[0]; + const parentDump = new MemoryAllocatorDump(gmd, 'parent'); + const childDump = addChildDump(parentDump, 'child'); + childDump.weak = true; + gmd.memoryAllocatorDumps = [parentDump]; + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'parent', + 'children': [ + { + 'name': 'child', + // Missing "'weak': true". + } + ] + } + ] + ]); + }, /'parent\/child'.*\binvalid weak flag\b/); + + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'parent', + 'weak': true, // This should be false (or not provided). + 'children': [ + { + 'name': 'child', + 'weak': true + } + ] + } + ] + ]); + }, /'parent'.*\binvalid weak flag\b/); + }); + + test('testSanityCheck_checkDumpTrees_dumpNotRemoved', function() { + const containerDumps = createContainerDumps(0); + const gmd = containerDumps[0]; + const parentDump = new MemoryAllocatorDump(gmd, 'parent'); + for (let i = 1; i <= 3; i++) { + addChildDump(parentDump, 'child' + i); + } + const otherDump = new MemoryAllocatorDump(gmd, 'other'); + gmd.memoryAllocatorDumps = [parentDump, otherDump]; + + // Child MAD not removed. + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'parent', + 'children': [ + { + 'name': 'child1', + }, + { + 'name': 'child2', + 'expected_removed': true + }, + { + 'name': 'child3', + } + ] + }, + { + 'name': 'other' + } + ] + ]); + }, /\bexpected\b.*'parent'.*\b2 children\b.*\bgot 3\b/); + + // Root MAD not removed. + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'parent', + 'children': [ + { + 'name': 'child1', + }, + { + 'name': 'child2' + }, + { + 'name': 'child3', + } + ] + }, + { + 'name': 'other', + 'expected_removed': true + } + ] + ]); + }, /\bexpected\b.*\b1 root memory allocator dumps\b.*\bgot 2\b/); + }); + + test('testSanityCheck_checkDumpTrees_invalidOwnership', function() { + const containerDumps = createContainerDumps(1); + const gmd = containerDumps[0]; + const pmd1 = containerDumps[1]; + const ownedDump = new MemoryAllocatorDump(gmd, 'owned', 42); + const ownerDump1 = new MemoryAllocatorDump(pmd1, 'owner1'); + const link1 = addOwnershipLink(ownerDump1, ownedDump); + const ownerDump2 = new MemoryAllocatorDump(pmd1, 'owner2'); + const link2 = addOwnershipLink(ownerDump2, ownedDump, 3); + const nonOwnerDump = new MemoryAllocatorDump(pmd1, 'non-owner', 90); + gmd.memoryAllocatorDumps = [ownedDump]; + pmd1.memoryAllocatorDumps = [ownerDump1, ownerDump2, nonOwnerDump]; + + // Missing 'owns' link. + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'owned', + 'guid': 42 + } + ], + [ + { + 'name': 'owner1', + 'owns': 42 + }, + { + 'name': 'owner2', + 'importance': 3, + 'owns': 42 + }, + { + 'name': 'non-owner', + 'guid': 90, + 'owns': 42 // This should not be here. + } + ] + ]); + }, /'non-owner'.*\bwas expected to have\b.*'owns' link\b/); + + // Extra 'owns' link. + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'owned', + 'guid': 42 + } + ], + [ + { + 'name': 'owner1' + // Missing: "'owns': 42". + }, + { + 'name': 'owner2', + 'importance': 3, + 'owns': 42 + }, + { + 'name': 'non-owner', + 'guid': 90 + } + ] + ]); + }, /'owner1'.*\bwas expected not to own\b/); + + // Invalid ownership importance. + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'owned', + 'guid': 42 + } + ], + [ + { + 'name': 'owner1', + 'owns': 42 + }, + { + 'name': 'owner2', + 'importance': 2, // This should be 3. + 'owns': 42 + }, + { + 'name': 'non-owner', + 'guid': 90 + } + ] + ]); + }, /\bexpected\b.*\bimportance\b.*'owner2'.*\b2 but got 3\b/); + + // Invalid ownership target. + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'owned', + 'guid': 42 + } + ], + [ + { + 'name': 'owner1', + 'owns': 90 // This should be 42. + }, + { + 'name': 'owner2', + 'importance': 3, + 'owns': 42 + }, + { + 'name': 'non-owner', + 'guid': 90 + } + ] + ]); + }, /'owner1'.*\binvalid target\b/); + + // Invalid 'ownedBy' ownership links count. + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'owned', + 'guid': 42, + 'expected_owned_by_links_count': 3 // This should be 2. + } + ], + [ + { + 'name': 'owner1', + 'owns': 42 + }, + { + 'name': 'owner2', + 'importance': 3, + 'owns': 42 + }, + { + 'name': 'non-owner', + 'guid': 90 + } + ] + ]); + }, /'owned'.*\bhave 3 'ownedBy' links\b.*\bgot 2\b/); + + // Invalid ownership source. + link1.source = ownerDump2; + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'owned', + 'guid': 42 + } + ], + [ + { + 'name': 'owner1', + 'owns': 42 + }, + { + 'name': 'owner2', + 'importance': 3, + 'owns': 42 + }, + { + 'name': 'non-owner', + 'guid': 90 + } + ] + ]); + }, /'owns' link\b.*'owner1'.*\binvalid source\b/); + link1.source = ownerDump1; + + // Ownership link not in target's 'ownedBy' list. + ownedDump.ownedBy.pop(); + assert.throws(function() { + checkDumpTrees(containerDumps, [ + [ + { + 'name': 'owned', + 'guid': 42 + } + ], + [ + { + 'name': 'owner1', + 'owns': 42 + }, + { + 'name': 'owner2', + 'importance': 3, + 'owns': 42 + }, + { + 'name': 'non-owner', + 'guid': 90 + } + ] + ]); + }, /\btarget of\b.*'owner2'.*'ownedBy' list\b/); + ownedDump.ownedBy.push(link2); + }); + + /** + * Build container memory dumps from tree recipes, let the resulting + * GlobalMemoryDump calculate sizes and effective sizes, and then check that + * the augmented container memory dumps have the expected structure with + * correct sizes and effective sizes (as described by the same tree recipes). + * + * See the documentation for buildDumpTrees and checkDumpTrees for more + * details about the structure of tree recipes. + */ + function testSizesCalculation(allTreeRecipes) { + const m = new Model(); + const io = new tr.importer.ImportOptions(); + io.showImportWarnings = false; + m.importOptions = io; + + const containerDumps = buildDumpTrees(allTreeRecipes, m); + const gmd = containerDumps[0]; + gmd.calculateSizes(); + gmd.calculateEffectiveSizes(); + gmd.forceRebuildingMemoryAllocatorDumpByFullNameIndices(); + checkDumpTrees(containerDumps, allTreeRecipes); + } + + // Check that the testSizesCalculation testing helper method above + // actually performs the expected checks. Since it will be used heavily + // throughout this file (where it is expected to pass), we only need to + // verify that it does indeed fail when it should. + test('testSanityCheck_testSizesCalculation', function() { + assert.throws(function() { + testSizesCalculation([ + [], + undefined, + [ + { + 'name': 'winheap' + }, + { + 'name': 'malloc', + 'expected_size': 100, + 'children': [ + { + 'name': 'allocated_objects', + 'size': 100, + 'expected_size': 100 + }, + { + 'name': 'extra', + 'size': 20, + 'expected_size': 20 + } + ] + } + ] + ]); + }, /expected.*'size'.*value.*\b100\b.*got.*\b120\b/); + }); + + function calculationTest(caseName, treeRecipes) { + test('calculateSizes_' + caseName, function() { + testSizesCalculation(treeRecipes); + }); + } + + /** + * Build container memory dumps from tree recipes, let the resulting + * GlobalMemoryDump remove weak memory dumps, and then check that the updated + * container memory dumps have the expected structure (as described by the + * same tree recipes). + * + * See the documentation for buildDumpTrees and checkDumpTrees for more + * details about the structure of tree recipes. + */ + function testWeakDumpRemoval(allTreeRecipes) { + const m = new tr.Model(); + const io = new tr.importer.ImportOptions(); + io.showImportWarnings = false; + m.importOptions = io; + + const containerDumps = buildDumpTrees(allTreeRecipes, m); + const gmd = containerDumps[0]; + gmd.removeWeakDumps(); + gmd.forceRebuildingMemoryAllocatorDumpByFullNameIndices(); + checkDumpTrees(containerDumps, allTreeRecipes); + } + + // Similarly to testSanityCheck_testSizesCalculation, check that the + // testWeakDumpRemoval testing helper method above actually performs the + // expected checks. + test('testSanityCheck_testWeakDumpRemoval', function() { + assert.throws(function() { + testWeakDumpRemoval([ + [], + undefined, + [ + { + 'name': 'winheap' + }, + { + 'name': 'malloc', + 'children': [ + { + 'name': 'allocated_objects' + }, + { + 'name': 'directly_weak', + 'guid': 42, + 'weak': true, + 'expected_removed': true + }, + { + 'name': 'indirectly_weak', + 'owns': 42 + // Missing: "'expected_removed': true". + } + ] + } + ] + ]); + }, /expected.*'malloc'.*\b2 children\b.*\bgot 1\b/); + }); + + function weakDumpRemovalTest(caseName, treeRecipes) { + test('removeWeakDumps_' + caseName, function() { + testWeakDumpRemoval(treeRecipes); + }); + } + + ///////////////////////////////////////////////////////////////////////////// + // Actual tests begin here. + ///////////////////////////////////////////////////////////////////////////// + + test('iterateContainerDumps_withoutProcessMemoryDumps', function() { + const containerDumps = createContainerDumps(0); + const gmd = containerDumps[0]; + + const visitedContainerDumps = []; + gmd.iterateContainerDumps(buildArgPusher(visitedContainerDumps)); + assertEqualUniqueMembers(visitedContainerDumps, containerDumps); + }); + + test('iterateContainerDumps_withProcessMemoryDumps', function() { + const containerDumps = createContainerDumps(2); + const gmd = containerDumps[0]; + + const visitedContainerDumps = []; + gmd.iterateContainerDumps(buildArgPusher(visitedContainerDumps)); + assertEqualUniqueMembers(visitedContainerDumps, containerDumps); + }); + + test('iterateAllRootAllocatorDumps', function() { + const containerDumps = buildDumpTrees([ + [ // GMD. + { + 'name': 'globalSharedDump1' + }, + { + 'name': 'globalSharedDump2' + } + ], + [ // PMD. + { + 'name': 'v8', + 'children': [ + { + 'name': 'isolate' + } + ] + } + ] + ]); + const gmd = containerDumps[0]; + const pmd = containerDumps[1]; + + const visitedAllocatorDumps = []; + gmd.iterateAllRootAllocatorDumps(buildArgPusher(visitedAllocatorDumps)); + assertEqualUniqueMembers(visitedAllocatorDumps, [ + gmd.getMemoryAllocatorDumpByFullName('globalSharedDump1'), + gmd.getMemoryAllocatorDumpByFullName('globalSharedDump2'), + pmd.getMemoryAllocatorDumpByFullName('v8') + ]); + }); + + test('traverseAllocatorDumpsInDepthFirstOrder_oneTreeWithoutOwners', + function() { + const containerDumps = buildDumpTrees([ + [ // GMD. + { + 'name': 'root', + 'children': [ + { + 'name': 'parent', + 'children': [ + { + 'name': 'child1' + }, + { + 'name': 'child2' + } + ] + } + ] + } + ] + ]); + const gmd = containerDumps[0]; + + // Post-order. + let visitedAllocatorDumps = []; + gmd.traverseAllocatorDumpsInDepthFirstPostOrder( + buildArgPusher(visitedAllocatorDumps)); + assert.deepEqual(visitedAllocatorDumps, [ + gmd.getMemoryAllocatorDumpByFullName('root/parent/child1'), + gmd.getMemoryAllocatorDumpByFullName('root/parent/child2'), + gmd.getMemoryAllocatorDumpByFullName('root/parent'), + gmd.getMemoryAllocatorDumpByFullName('root') + ]); + + // Pre-order. + visitedAllocatorDumps = []; + gmd.traverseAllocatorDumpsInDepthFirstPreOrder( + buildArgPusher(visitedAllocatorDumps)); + assert.deepEqual(visitedAllocatorDumps, [ + gmd.getMemoryAllocatorDumpByFullName('root'), + gmd.getMemoryAllocatorDumpByFullName('root/parent'), + gmd.getMemoryAllocatorDumpByFullName('root/parent/child1'), + gmd.getMemoryAllocatorDumpByFullName('root/parent/child2') + ]); + }); + + test('traverseAllocatorDumpsInDepthFirstOrder_oneTreeWithOwners', + function() { + const containerDumps = buildDumpTrees([ + [], // GMD. + [ // PMD. + { + 'name': 'root', + 'children': [ + { + 'name': 'parent', + 'children': [ + { + 'name': 'child1', + 'owns': 0 + }, + { + 'name': 'child2', + 'guid': 0 + }, + { + 'name': 'child3', + 'owns': 0 + } + ] + } + ] + } + ] + ]); + const gmd = containerDumps[0]; + const pmd = containerDumps[1]; + + // Post-order. + let visitedAllocatorDumps = []; + gmd.traverseAllocatorDumpsInDepthFirstPostOrder( + buildArgPusher(visitedAllocatorDumps)); + assert.deepEqual(visitedAllocatorDumps, [ + pmd.getMemoryAllocatorDumpByFullName('root/parent/child1'), + pmd.getMemoryAllocatorDumpByFullName('root/parent/child3'), + pmd.getMemoryAllocatorDumpByFullName('root/parent/child2'), + pmd.getMemoryAllocatorDumpByFullName('root/parent'), + pmd.getMemoryAllocatorDumpByFullName('root') + ]); + + // Pre-order. + visitedAllocatorDumps = []; + gmd.traverseAllocatorDumpsInDepthFirstPreOrder( + buildArgPusher(visitedAllocatorDumps)); + assert.deepEqual(visitedAllocatorDumps, [ + pmd.getMemoryAllocatorDumpByFullName('root'), + pmd.getMemoryAllocatorDumpByFullName('root/parent'), + pmd.getMemoryAllocatorDumpByFullName('root/parent/child2'), + pmd.getMemoryAllocatorDumpByFullName('root/parent/child1'), + pmd.getMemoryAllocatorDumpByFullName('root/parent/child3') + ]); + }); + + test('traverseAllocatorDumpsInDepthFirstOrder_multipleTrees', function() { + const containerDumps = buildDumpTrees([ + [ // GMD. + { + 'name': 'shared', + 'children': [ + { + 'name': 'pool1', + 'guid': 1 + }, + { + 'name': 'pool2', + 'owns': 3 + } + ] + } + ], + [ // PMD. + { + 'name': 'oilpan', + 'children': [ + { + 'name': 'objects' + }, + { + 'name': 'heaps', + 'owns': 1, + 'children': [ + { + 'name': 'small', + 'guid': 2 + }, + { + 'name': 'large' + } + ] + } + ] + }, + { + 'name': 'v8', + 'children': [ + { + 'name': 'isolate1', + 'owns': 2 + }, + { + 'name': 'isolate2', + 'guid': 3 + } + ] + } + ] + ]); + const gmd = containerDumps[0]; + const pmd = containerDumps[1]; + + // Post-order. + let visitedAllocatorDumps = []; + gmd.traverseAllocatorDumpsInDepthFirstPostOrder( + buildArgPusher(visitedAllocatorDumps)); + assert.deepEqual(visitedAllocatorDumps, [ + pmd.getMemoryAllocatorDumpByFullName('v8/isolate1'), + pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps/small'), + pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps/large'), + pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps'), + gmd.getMemoryAllocatorDumpByFullName('shared/pool1'), + gmd.getMemoryAllocatorDumpByFullName('shared/pool2'), + gmd.getMemoryAllocatorDumpByFullName('shared'), + pmd.getMemoryAllocatorDumpByFullName('oilpan/objects'), + pmd.getMemoryAllocatorDumpByFullName('oilpan'), + pmd.getMemoryAllocatorDumpByFullName('v8/isolate2'), + pmd.getMemoryAllocatorDumpByFullName('v8') + ]); + + // Pre-order. + visitedAllocatorDumps = []; + gmd.traverseAllocatorDumpsInDepthFirstPreOrder( + buildArgPusher(visitedAllocatorDumps)); + assert.deepEqual(visitedAllocatorDumps, [ + gmd.getMemoryAllocatorDumpByFullName('shared'), + gmd.getMemoryAllocatorDumpByFullName('shared/pool1'), + pmd.getMemoryAllocatorDumpByFullName('oilpan'), + pmd.getMemoryAllocatorDumpByFullName('oilpan/objects'), + pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps'), + pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps/small'), + pmd.getMemoryAllocatorDumpByFullName('oilpan/heaps/large'), + pmd.getMemoryAllocatorDumpByFullName('v8'), + pmd.getMemoryAllocatorDumpByFullName('v8/isolate1'), + pmd.getMemoryAllocatorDumpByFullName('v8/isolate2'), + gmd.getMemoryAllocatorDumpByFullName('shared/pool2') + ]); + }); + + test('traverseAllocatorDumpsInDepthFirstPostOrder_cycle', function() { + const containerDumps = buildDumpTrees([ + [ // GMD. + { + 'name': 'shared', + 'owns': 2, + 'children': [ + { + 'name': 'pool', + 'guid': 1 + } + ] + } + ], + [ // PMD. + { + 'name': 'oilpan', + 'owns': 1, + 'children': [ + { + 'name': 'objects', + 'guid': 2 + } + ] + } + ] + ]); + const gmd = containerDumps[0]; + + // Post-order. + assert.throws(function() { + gmd.traverseAllocatorDumpsInDepthFirstPostOrder(function() {}); + }, /contains.*cycle/); + + // Pre-order. + const visitedAllocatorDumps = []; + gmd.traverseAllocatorDumpsInDepthFirstPreOrder( + buildArgPusher(visitedAllocatorDumps)); + assert.deepEqual(visitedAllocatorDumps, []); + }); + + // Just check that the method doesn't crash upon encountering empty and/or + // undefined memory allocator dumps. + calculationTest('noDumps', [ + undefined, // GMD. + [], // PMD1. + undefined // PMD2. + ]); + + calculationTest('flatDumps', [ + [ // GMD. + { + 'name': 'shared', + 'size': 1024, + 'expected_size': 1024, + 'expected_effective_size': 1024 + } + ], + [ // PMD. + { + 'name': 'gpu' + } + ] + ]); + + calculationTest('zeroSizes', [ + [ // GMD. + { + 'name': 'shared', + 'size': 0, + 'expected_size': 0, + 'expected_effective_size': 0 + } + ], + [ // PMD1. + { + 'name': 'gpu', + 'expected_size': 0, + 'expected_effective_size': 0, + 'children': [ + { + 'name': 'zero', + 'size': 0, + 'expected_size': 0, + 'expected_effective_size': 0 + } + ] + } + ], + [ // PMD2. + { + 'name': 'gpu', + 'expected_size': 0, + 'expected_effective_size': 0, + 'children': [ + { + 'name': 'zero', + 'size': 0, + 'expected_size': 0, + 'expected_effective_size': 0 + }, + { + 'name': 'undefined' + } + ] + } + ] + ]); + + calculationTest('children_allSizesUndefined', [ + [ + { + 'name': 'parent', + 'children': [ + { + 'name': 'child1' + }, + { + 'name': 'child2' + } + ] + } + ] + ]); + + calculationTest('children_parentSizeUndefined', [ + [ + { + 'name': 'parent', + 'expected_size': 384, + 'expected_effective_size': 384, + 'children': [ + { + 'name': 'child1', + 'size': 128, + 'expected_size': 128, + 'expected_effective_size': 128 + }, + { + 'name': 'child2', + 'size': 256, + 'expected_size': 256, + 'expected_effective_size': 256 + } + ] + } + ] + ]); + + calculationTest('children_parentSizeDefined_childrenAddUp', [ + [ + { + 'name': 'parent', + 'size': 0, + 'expected_size': 0, + 'expected_effective_size': 0, + 'children': [ + { + 'name': 'child1' + }, + { + 'name': 'child2' + } + ] + } + ] + ]); + + calculationTest('children_parentSizeDefined_childrenDontAddUp', [ + [ + { + 'name': 'parent', + 'size': 2048, + 'expected_size': 2048, + 'expected_effective_size': 2048, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 2048, + 'expected_effective_size': 2048 + }, + { + 'name': 'child1' + }, + { + 'name': 'child2' + } + ] + } + ] + ]); + + calculationTest('children_oneChildSizeUndefined_childrenAddUp', [ + [ + { + 'name': 'parent', + 'size': 4096, + 'expected_size': 4096, + 'expected_effective_size': 4096, + 'children': [ + { + 'name': 'child1' + }, + { + 'name': 'child2', + 'size': 4096, + 'expected_size': 4096, + 'expected_effective_size': 4096 + } + ] + } + ] + ]); + + calculationTest('children_oneChildSizeUndefined_childrenDontAddUp', [ + [ + { + 'name': 'parent', + 'size': 6144, + 'expected_size': 6144, + 'expected_effective_size': 6144, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 2048, + 'expected_effective_size': 2048 + }, + { + 'name': 'child1' + }, + { + 'name': 'child2', + 'size': 4096, + 'expected_size': 4096, + 'expected_effective_size': 4096 + } + ] + } + ] + ]); + + calculationTest('children_allSizesDefined_childrenAddUp', [ + [ + { + 'name': 'parent', + 'size': 100, + 'expected_size': 100, + 'expected_effective_size': 100, + 'children': [ + { + 'name': 'child1', + 'size': 70, + 'expected_size': 70, + 'expected_effective_size': 70 + }, + { + 'name': 'child2', + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 30 + } + ] + } + ] + ]); + + calculationTest('children_allSizesDefined_childrenDontUp', [ + [ + { + 'name': 'parent', + 'size': 150, + 'expected_size': 150, + 'expected_effective_size': 150, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 50, + 'expected_effective_size': 50 + }, + { + 'name': 'child1', + 'size': 70, + 'expected_size': 70, + 'expected_effective_size': 70 + }, + { + 'name': 'child2', + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 30 + } + ] + } + ] + ]); + + calculationTest('children_oneChildSizeDefined', [ + [ + { + 'name': 'parent', + 'expected_size': 49, + 'expected_effective_size': 49, + 'children': [ + { + 'name': 'child1', + 'size': 49, + 'expected_size': 49, + 'expected_effective_size': 49 + }, + { + 'name': 'child2' + } + ] + } + ] + ]); + + calculationTest('children_multipleLevels', [ + [], // GMD. + [ // PMD. + { + 'name': 'v8', + 'expected_size': 36, + 'expected_effective_size': 36, + 'children': [ + { + 'name': 'isolate1', + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 10, + 'children': [ + { + 'skip_build': true, + 'name': '<unspecified>', + 'expected_size': 3, + 'expected_effective_size': 3 + }, + { + 'name': 'objects', + 'size': 3, + 'expected_size': 3, + 'expected_effective_size': 3 + }, + { + 'name': 'heaps', + 'size': 4, + 'expected_size': 4, + 'expected_effective_size': 4 + } + ] + }, + { + 'name': 'isolate2', + 'size': 12, + 'expected_size': 12, + 'expected_effective_size': 12, + 'children': [ + { + 'name': 'objects', + 'size': 5, + 'expected_size': 5, + 'expected_effective_size': 5 + }, + { + 'name': 'heaps', + 'size': 7, + 'expected_size': 7, + 'expected_effective_size': 7 + } + ] + }, + { + 'name': 'isolate3', + 'expected_size': 14, + 'expected_effective_size': 14, + 'children': [ + { + 'name': 'objects', + 'size': 14, + 'expected_size': 14, + 'expected_effective_size': 14 + }, + { + 'name': 'heaps' + } + ] + } + ] + } + ] + ]); + + calculationTest('owners_allSizesUndefined', [ + [ // GMD. + { + 'name': 'bitmap', + 'guid': 7 + } + ], + [ // PMD1. + { + 'name': 'tile', + 'owns': 7, + 'importance': 1 + } + ], + [ // PMD2. + { + 'name': 'chunk', + 'owns': 7 + } + ] + ]); + + calculationTest('owners_ownedSizeDefined', [ + [ // GMD. + { + 'name': 'bitmap', + 'size': 15, + 'expected_size': 15, + 'expected_effective_size': 15, + 'guid': 7 + } + ], + [ // PMD1. + { + 'name': 'tile', + 'owns': 7 + } + ], + [ // PMD2. + { + 'name': 'chunk', + 'owns': 7 + } + ] + ]); + + calculationTest('owners_ownedSizeUndefined', [ + [ // GMD. + { + 'name': 'bitmap', + 'expected_size': 9, + 'expected_effective_size': 0, + 'guid': 7 + } + ], + [ // PMD1. + { + 'name': 'tile', + 'size': 5, + 'expected_size': 5, + 'expected_effective_size': 2.5, + 'owns': 7 + } + ], + [ // PMD2. + { + 'name': 'chunk', + 'size': 9, + 'expected_size': 9, + 'expected_effective_size': 6.5, + 'owns': 7 + } + ] + ]); + + calculationTest('owners_oneOwnerSizeDefined', [ + [ // GMD. + { + 'name': 'bitmap', + 'expected_size': 16, + 'expected_effective_size': 0, + 'guid': 7 + } + ], + [ // PMD1. + { + 'name': 'tile', + 'size': 16, + 'expected_size': 16, + 'expected_effective_size': 16, + 'owns': 7 + } + ], + [ // PMD2. + { + 'name': 'chunk', + 'owns': 7 + } + ] + ]); + + calculationTest('owners_oneOwnerSizeUndefined', [ + [ // GMD. + { + 'name': 'bitmap', + 'size': 20, + 'expected_size': 20, + 'expected_effective_size': 2, + 'guid': 7 + } + ], + [ // PMD1. + { + 'name': 'tile', + 'owns': 7 + } + ], + [ // PMD2. + { + 'name': 'chunk', + 'size': 18, + 'expected_size': 18, + 'expected_effective_size': 18, + 'owns': 7 + } + ] + ]); + + calculationTest('owners_allSizesDefined', [ + [ // GMD. + { + 'name': 'bitmap', + 'size': 60, + 'expected_size': 60, + 'expected_effective_size': 31, + 'guid': 7 + } + ], + [ // PMD1. + { + 'name': 'tile', + 'size': 29, + 'expected_size': 29, + 'expected_effective_size': 19.5, + 'owns': 7 + } + ], + [ // PMD2. + { + 'name': 'chunk', + 'size': 19, + 'expected_size': 19, + 'expected_effective_size': 9.5, + 'owns': 7 + } + ] + ]); + + calculationTest('owners_hierarchy', [ + [ // GMD. + { + 'name': 'bitmap', + 'expected_size': 50, + 'expected_effective_size': 0, + 'guid': 7 + } + ], + [ // PMD1. + { + 'name': 'tile', + 'expected_size': 50, + 'expected_effective_size': 0, + 'owns': 7, + 'guid': 0 + }, + { + 'name': 'object1', + 'size': 30, + 'owns': 0, + 'expected_size': 30, + 'expected_effective_size': 9 + }, + { + 'name': 'object2', + 'owns': 0 + }, + { + 'name': 'object3', + 'size': 50, + 'owns': 0, + 'expected_size': 50, + 'expected_effective_size': 21 + } + ], + [ // PMD2. + { + 'name': 'chunk', + 'size': 40, + 'expected_size': 40, + 'expected_effective_size': 20, + 'owns': 7 + } + ] + ]); + + calculationTest('owners_withChildren', [ + [ // GMD. + { + 'name': 'bitmap', + 'guid': 7, + 'expected_size': 48, + 'expected_effective_size': 17, + 'children': [ + { + 'name': 'subbitmap1', + 'size': 32, + 'expected_size': 32, + 'expected_effective_size': 17 * (32 / 48) + }, + { + 'name': 'subbitmap2', + 'size': 16, + 'expected_size': 16, + 'expected_effective_size': 17 * (16 / 48) + } + ] + } + ], + [ // PMD. + { + 'name': 'tile', + 'expected_size': 31, + 'expected_effective_size': 0, + 'guid': 8, + 'owns': 7, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 7, + 'expected_effective_size': 0 + }, + { + 'name': 'subtile', + 'size': 24, + 'expected_size': 24, + 'expected_effective_size': 0 + } + ] + }, + { + 'name': 'cc', + 'owns': 8, + 'size': 31, + 'expected_size': 31, + 'expected_effective_size': 31 + } + ] + ]); + + calculationTest('owners_withParents', [ + [ // GMD. + { + 'name': 'bitmap', + 'size': 96, + 'expected_size': 96, + 'expected_effective_size': 32, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 32, + 'expected_effective_size': 32 + }, + { + 'name': 'subbitmap', + 'guid': 2, + 'expected_size': 64, + 'expected_effective_size': 0 + } + ] + } + ], + [ // PMD. + { + 'name': 'tile', + 'expected_size': 64, + 'expected_effective_size': 0, + 'children': [ + { + 'name': 'subtile', + 'guid': 1, + 'owns': 2, + 'expected_size': 64, + 'expected_effective_size': 0 + } + ] + }, + { + 'name': 'cc', + 'owns': 1, + 'size': 64, + 'expected_size': 64, + 'expected_effective_size': 64 + } + ] + ]); + + calculationTest('owners_multipleLevels', [ + [ // GMD. + { + 'name': 'bitmap', + 'size': 96, + 'expected_size': 96, + 'expected_effective_size': 32, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 32, + 'expected_effective_size': 32 + }, + { + 'name': 'subbitmap', + 'guid': 2, + 'expected_size': 64, + 'expected_effective_size': 0 + } + ] + } + ], + [ // PMD. + { + 'name': 'tile', + 'expected_size': 64, + 'expected_effective_size': 0, + 'owns': 2, + 'children': [ + { + 'name': 'subtile', + 'guid': 1, + 'expected_size': 64, + 'expected_effective_size': 0 + } + ] + }, + { + 'name': 'cc', + 'owns': 1, + 'size': 64, + 'expected_size': 64, + 'expected_effective_size': 64 + } + ] + ]); + + calculationTest('views_allSizesUndefined', [ + [ + { + 'name': 'v8', + 'children': [ + { + 'name': 'v8/heaps', + 'guid': 1 + }, + { + 'name': 'v8/objects', + 'owns': 1 + } + ] + } + ] + ]); + + calculationTest('views_ownedSizeDefined', [ + [ + { + 'name': 'v8', + 'expected_size': 10, + 'expected_effective_size': 10, + 'children': [ + { + 'name': 'heaps', + 'guid': 1, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 10 + }, + { + 'name': 'objects', + 'owns': 1 + } + ] + } + ] + ]); + + calculationTest('views_ownerSizeDefined', [ + [ + { + 'name': 'v8', + 'expected_size': 20, + 'expected_effective_size': 20, + 'children': [ + { + 'name': 'heaps', + 'guid': 1, + 'expected_size': 20, + 'expected_effective_size': 0, + 'expected_owned_by_sibling_sizes': { + 'objects': 20 + } + }, + { + 'name': 'objects', + 'owns': 1, + 'size': 20, + 'expected_size': 20, + 'expected_effective_size': 20 + } + ] + } + ] + ]); + + calculationTest('views_parentSizeUndefined', [ + [ + { + 'name': 'v8', + 'expected_size': 30, + 'expected_effective_size': 30, + 'children': [ + { + 'name': 'heaps', + 'guid': 1, + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 10, + 'expected_owned_by_sibling_sizes': { + 'objects': 20 + } + }, + { + 'name': 'objects', + 'owns': 1, + 'size': 20, + 'expected_size': 20, + 'expected_effective_size': 20 + } + ] + } + ] + ]); + + calculationTest('views_ownerSizeUndefined_childrenAddUp', [ + [ + { + 'name': 'v8', + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 30, + 'children': [ + { + 'name': 'heaps', + 'guid': 1, + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 30 + }, + { + 'name': 'objects', + 'owns': 1 + } + ] + } + ] + ]); + + calculationTest('views_ownerSizeUndefined_childrenDontAddUp', [ + [ + { + 'name': 'v8', + 'size': 40, + 'expected_size': 40, + 'expected_effective_size': 40, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 10, + 'expected_effective_size': 10 + }, + { + 'name': 'heaps', + 'guid': 1, + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 30 + }, + { + 'name': 'objects', + 'owns': 1 + } + ] + } + ] + ]); + + calculationTest('views_ownedSizeUndefined_childrenAddUp', [ + [ + { + 'name': 'v8', + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 30, + 'children': [ + { + 'name': 'heaps', + 'guid': 1, + 'expected_size': 30, + 'expected_effective_size': 0, + 'expected_owned_by_sibling_sizes': { + 'objects': 30 + } + }, + { + 'name': 'objects', + 'owns': 1, + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 30 + } + ] + } + ] + ]); + + calculationTest('views_ownedSizeUndefined_childrenDontAddUp', [ + [ + { + 'name': 'v8', + 'size': 40, + 'expected_size': 40, + 'expected_effective_size': 40, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 10, + 'expected_effective_size': 10 + }, + { + 'name': 'heaps', + 'guid': 1, + 'expected_size': 30, + 'expected_effective_size': 0, + 'expected_owned_by_sibling_sizes': { + 'objects': 30 + } + }, + { + 'name': 'objects', + 'owns': 1, + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 30 + } + ] + } + ] + ]); + + calculationTest('views_allSizesDefined_childrenAddUp', [ + [ + { + 'name': 'v8', + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 30, + 'children': [ + { + 'name': 'heaps', + 'guid': 1, + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 16, + 'expected_owned_by_sibling_sizes': { + 'objects': 14 + } + }, + { + 'name': 'objects', + 'owns': 1, + 'size': 14, + 'expected_size': 14, + 'expected_effective_size': 14 + } + ] + } + ] + ]); + + calculationTest('views_allSizesDefined_childrenDontAddUp', [ + [ + { + 'name': 'v8', + 'size': 35, + 'expected_size': 35, + 'expected_effective_size': 35, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 5, + 'expected_effective_size': 5 + }, + { + 'name': 'heaps', + 'guid': 1, + 'size': 30, + 'expected_size': 30, + 'expected_effective_size': 16, + 'expected_owned_by_sibling_sizes': { + 'objects': 14 + } + }, + { + 'name': 'objects', + 'owns': 1, + 'size': 14, + 'expected_size': 14, + 'expected_effective_size': 14 + } + ] + } + ] + ]); + + calculationTest('views_deep', [ + [ + { + 'name': 'root', + 'expected_size': 17, + 'expected_effective_size': 17, + 'children': [ + { + 'name': 'parent1', + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 5, + 'expected_owned_by_sibling_sizes': { + 'parent2': 5 + }, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 2, + 'expected_effective_size': 2 + }, + { + 'name': 'child', + 'guid': 1, + 'size': 8, + 'expected_size': 8, + 'expected_effective_size': 3 + } + ] + }, + { + 'name': 'parent2', + 'size': 8, + 'expected_size': 8, + 'expected_effective_size': 5, + 'expected_owned_by_sibling_sizes': { + 'parent3': 3 + }, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 3, + 'expected_effective_size': 3 + }, + { + 'name': 'child', + 'guid': 2, + 'owns': 1, + 'size': 5, + 'expected_size': 5, + 'expected_effective_size': 2 + } + ] + }, + { + 'name': 'parent3', + 'size': 7, + 'expected_size': 7, + 'expected_effective_size': 7, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 4, + 'expected_effective_size': 4 + }, + { + 'name': 'child', + 'owns': 2, + 'size': 3, + 'expected_size': 3, + 'expected_effective_size': 3 + } + ] + } + ] + } + ] + ]); + + calculationTest('views_nested', [ + [ + { + 'name': 'system', + 'expected_size': 7, + 'expected_effective_size': 7, + 'children': [ + { + 'name': 'subsystem-A', + 'owns': 15, + 'expected_size': 5, + 'expected_effective_size': 5, + 'children': [ + { + 'name': 'objects', + 'owns': 30, + 'size': 3, + 'expected_size': 3, + 'expected_effective_size': 3 + }, + { + 'name': 'heaps', + 'guid': 30, + 'size': 5, + 'expected_size': 5, + 'expected_effective_size': 2, + 'expected_owned_by_sibling_sizes': { + 'objects': 3 + } + } + ] + }, + { + 'name': 'subsystem-B', + 'guid': 15, + 'expected_size': 7, + 'expected_effective_size': 2, + 'expected_owned_by_sibling_sizes': { + 'subsystem-A': 5 + }, + 'children': [ + { + 'name': 'objects', + 'owns': 40, + 'size': 7, + 'expected_size': 7, + 'expected_effective_size': 2 + }, + { + 'name': 'heaps', + 'guid': 40, + 'expected_size': 7, + 'expected_effective_size': 0, + 'expected_owned_by_sibling_sizes': { + 'objects': 7 + } + } + ] + } + ] + } + ] + ]); + + calculationTest('importance_equal', [ + [ // GMD (both importances undefined and equal sizes). + { + 'name': 'owned', + 'guid': 1, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 4 + }, + { + 'name': 'owner1', + 'owns': 1, + 'size': 6, + 'expected_size': 6, + 'expected_effective_size': 3 + }, + { + 'name': 'owner2', + 'owns': 1, + 'size': 6, + 'expected_size': 6, + 'expected_effective_size': 3 + } + ], + [ // PMD1 (only one importance defined and different sizes). + { + 'name': 'owned', + 'guid': 2, + 'size': 20, + 'expected_size': 20, + 'expected_effective_size': 5 + }, + { + 'name': 'owner1', + 'owns': 2, + 'importance': 0, + 'size': 15, + 'expected_size': 15, + 'expected_effective_size': 10 / 2 + 5 + }, + { + 'name': 'owner2', + 'owns': 2, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 10 / 2 + } + ], + [ // PMD2 (all importances defined and different sizes). + { + 'name': 'owned', + 'guid': 3, + 'size': 15, + 'expected_size': 15, + 'expected_effective_size': 5 + }, + { + 'name': 'owner1', + 'owns': 3, + 'importance': 3, + 'size': 8, + 'expected_size': 8, + 'expected_effective_size': 8 / 3 + }, + { + 'name': 'owner2', + 'owns': 3, + 'importance': 3, + 'size': 9, + 'expected_size': 9, + 'expected_effective_size': 8 / 3 + 1 / 2 + }, + { + 'name': 'owner3', + 'owns': 3, + 'importance': 3, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 8 / 3 + 1 / 2 + 1 + } + ] + ]); + + calculationTest('importance_notEqual', [ + [ // GMD (one importance undefined and equal sizes). + { + 'name': 'owned', + 'guid': 1, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 4 + }, + { + 'name': 'owner1', + 'owns': 1, + 'size': 6, + 'expected_size': 6, + 'expected_effective_size': 0 + }, + { + 'name': 'owner2', + 'owns': 1, + 'importance': 1, + 'size': 6, + 'expected_size': 6, + 'expected_effective_size': 6 + } + ], + [ // PMD1 (one importance undefined and different sizes). + { + 'name': 'owned', + 'guid': 2, + 'size': 20, + 'expected_size': 20, + 'expected_effective_size': 4 + }, + { + 'name': 'owner1', + 'owns': 2, + 'importance': -1, + 'size': 16, + 'expected_size': 16, + 'expected_effective_size': 6 + }, + { + 'name': 'owner2', + 'owns': 2, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 10 + } + ], + [ // PMD2 (all importances defined and different sizes). + { + 'name': 'owned', + 'guid': 3, + 'size': 15, + 'expected_size': 15, + 'expected_effective_size': 5 + }, + { + 'name': 'owner1', + 'owns': 3, + 'importance': 4, + 'size': 8, + 'expected_size': 8, + 'expected_effective_size': 8 + }, + { + 'name': 'owner2', + 'owns': 3, + 'importance': 3, + 'size': 6, + 'expected_size': 6, + 'expected_effective_size': 0 + }, + { + 'name': 'owner3', + 'owns': 3, + 'importance': 2, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 2 + } + ] + ]); + + // Example taken from GlobalMemoryDump.calculateDumpOwnershipCoefficient_() + // documentation. + calculationTest('importance_manyOwners', [ + [ // GMD. + { + 'name': 'owned', + 'guid': 4, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 2 + } + ], + [ // PMD1. + { + 'name': 'owner1', + 'owns': 4, + 'importance': 2, + 'size': 6, + 'expected_size': 6, + 'expected_effective_size': 6 / 2 + } + ], + [ // PMD2. + { + 'name': 'some_parent', + 'expected_size': 7, + 'expected_effective_size': 6 / 2 + 1, + 'children': [ + { + 'name': 'owner2', + 'owns': 4, + 'importance': 2, + 'size': 7, + 'expected_size': 7, + 'expected_effective_size': 6 / 2 + 1 + } + ] + } + ], + [ // PMD3. + { + 'name': 'owner3', + 'owns': 4, + 'importance': 1, + 'size': 5, + 'expected_size': 5, + 'expected_effective_size': 0 + }, + { + 'name': 'owner4', + 'owns': 4, + 'importance': 0, + 'size': 8, + 'expected_size': 8, + 'expected_effective_size': 1 + } + ] + ]); + + calculationTest('importance_chainOwnerships', [ + [ // GMD. + { + 'name': 'owned', + 'guid': 5, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 2 + } + ], + [ // PMD1. + { + 'name': 'owner1', + 'owns': 5, + 'importance': 2, + 'guid': 6, + 'size': 6, + 'expected_size': 6, + 'expected_effective_size': 2 + }, + { + 'name': 'subowner1', + 'owns': 6, + 'size': 4, + 'expected_size': 4, + 'expected_effective_size': 4 + } + ], + [ // PMD2. + { + 'name': 'owner2', + 'owns': 5, + 'importance': 1, + 'guid': 8, + 'size': 8, + 'expected_size': 8, + 'expected_effective_size': 2 - 2 / 4 + }, + { + 'name': 'subowner2', + 'owns': 8, + 'size': 2, + 'expected_size': 2, + 'expected_effective_size': 2 / 4 + } + ] + ]); + + calculationTest('importance_nested', [ + [ + { + 'name': 'grey', + 'guid': 15, + 'size': 20, + 'expected_size': 20, + 'expected_effective_size': 6 + }, + { + 'name': 'blue', + 'guid': 18, + 'owns': 15, + 'importance': 1, + 'size': 14, + 'expected_size': 14, + 'expected_effective_size': 1 + }, + { + 'name': 'purple', + 'owns': 15, + 'importance': 2, + 'size': 7, + 'expected_size': 7, + 'expected_effective_size': 7 + }, + { + 'name': 'yellow', + 'owns': 21, + 'importance': 3, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 3 + }, + { + 'name': 'red', + 'guid': 21, + 'owns': 18, + 'size': 12, + 'expected_size': 12, + 'expected_effective_size': 1 + }, + { + 'name': 'green', + 'owns': 21, + 'importance': 3, + 'size': 8, + 'expected_size': 8, + 'expected_effective_size': 2 + } + ] + ]); + + calculationTest('importance_singleRefinement', [ + [ + { + 'name': 'v8', + 'expected_size': 13, + 'expected_effective_size': 13, + 'children': [ + { + 'name': 'objects', + 'owns': 1, + 'size': 11, + 'expected_size': 11, + 'expected_effective_size': 11, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 4, + 'expected_effective_size': 4 + }, + { + 'name': 'object1', + 'owns': 2, + 'size': 7, + 'expected_size': 7, + 'expected_effective_size': 7 + } + ] + }, + { + 'name': 'heaps', + 'guid': 1, + 'size': 13, + 'expected_size': 13, + 'expected_effective_size': 2, + 'expected_owned_by_sibling_sizes': { + 'objects': 11 + }, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 3, + 'expected_effective_size': 1 + }, + { + 'name': 'heap1', + 'guid': 2, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 1, + } + ] + } + ] + } + ] + ]); + + calculationTest('importance_sharedRefinement', [ + [ // GMD. + { + 'name': 'shared_bitmap', + 'guid': 0, + 'size': 23, + 'expected_size': 23, + 'expected_effective_size': 5, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 13, + 'expected_effective_size': 13 * 5 / (13 + 3) + }, + { + 'name': 'bitmap0x7', + 'guid': 999, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 3 * 5 / (13 + 3), + } + ] + } + ], + [ // PMD1. + { + 'name': 'tile_manager', + 'owns': 0, + 'importance': 2, + 'size': 12, + 'expected_size': 12, + 'expected_effective_size': 5 + 2, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 5, + 'expected_effective_size': 5 + }, + { + 'name': 'tile42', + 'owns': 999, + 'importance': 1, + 'size': 7, + 'expected_size': 7, + 'expected_effective_size': 2, + } + ] + } + ], + [ // PMD2. + { + 'name': 'gpu', + 'owns': 0, + 'importance': 1, + 'size': 16, + 'expected_size': 16, + 'expected_effective_size': 6 + 5, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 11, + 'expected_effective_size': 6 + }, + { + 'name': 'chunk-3.14', + 'owns': 999, + 'importance': 2, + 'size': 5, + 'expected_size': 5, + 'expected_effective_size': 5, + } + ] + } + ] + ]); + + // Example taken from https://goo.gl/fKg0dt. + calculationTest('documentationExample', [ + [ // GMD, Global (shared) memory. + { + 'name': 'unknown', + 'guid': 2, + 'expected_size': 16, + 'expected_effective_size': 0, + } + ], + [ // PMD1, Browser process. + { + 'name': 'sharedbitmap', + 'size': 17, + 'expected_size': 17, + 'expected_effective_size': 9, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 1, + 'expected_effective_size': 1 + }, + { + 'name': '0x7', + 'size': 16, + 'expected_size': 16, + 'expected_effective_size': 8, + 'owns': 2, + 'importance': 1, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 16, + 'expected_effective_size': 8 + }, + { + 'name': 'y' + } + ] + } + ] + } + ], + [ // PMD2, Renderer process. + { + 'name': 'v8', + 'expected_size': 13, + 'expected_effective_size': 13, + 'children': [ + { + 'name': 'heaps', + 'guid': 100, + 'expected_size': 12, + 'expected_effective_size': 3, + 'expected_owned_by_sibling_sizes': { + 'objects': 9 + }, + 'children': [ + { + 'name': '1', + 'size': 8, + 'expected_size': 8, + 'expected_effective_size': 2, + 'owns': 2, + 'importance': 2 + }, + { + 'name': '2', + 'expected_size': 4, + 'expected_effective_size': 1, + 'size': 4 + } + ] + }, + { + 'name': 'objects', + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 10, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 1, + 'expected_effective_size': 1 + }, + { + 'name': 'strings', + 'size': 9, + 'expected_size': 9, + 'expected_effective_size': 9, + 'owns': 100 + } + ] + } + ] + } + ] + ]); + + // This should never happen. Nevertheless, this test checks that we can + // handle invalid sizes (parent dump being smaller than its aggregated + // children and owned dump being smaller than its largest owner) gracefully. + calculationTest('invalidSizes', [ + [ + { + 'name': 'root1', + 'size': 24, + 'expected_size': 24, + 'expected_effective_size': 4, + 'children': [ + { + 'name': '<unspecified>', + 'skip_build': true, + 'expected_size': 4, + 'expected_effective_size': 4 + }, + { + 'name': 'parent', + 'guid': 2, + 'size': 17, // Invalid: child has larger size. + 'expected_size': 20, + 'expected_infos': [ + { + type: PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN, + providedSize: 17, + dependencySize: 20 + }, + { + type: PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER, + providedSize: 17, + dependencySize: 18 + } + ], + 'expected_effective_size': 0, + 'children': [ + { + 'name': 'child', + 'guid': 1, + 'size': 10, // Invalid: owner has larger size. + 'expected_size': 20, + 'expected_infos': [ + { + type: PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER, + providedSize: 10, + dependencySize: 20 + } + ], + 'expected_effective_size': 0, + } + ] + } + ] + }, + { + 'name': 'root2', + 'owns': 1, + 'size': 20, + 'expected_size': 20, + 'expected_effective_size': 20 + }, + { + 'name': 'root3', + 'owns': 2, + 'importance': -1, + 'size': 18, + 'expected_size': 18, + 'expected_effective_size': 18 + } + ] + ]); + + calculationTest('multipleInfos', [ + [ + { + 'name': 'root', + 'expected_size': 10, + 'expected_effective_size': 10, + 'children': [ + { + 'name': 'parent1', + 'size': 5, + 'expected_size': 10, + 'expected_infos': [ + { + type: PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN, + providedSize: 5, + dependencySize: 10 + } + ], + 'expected_effective_size': 1, + 'expected_owned_by_sibling_sizes': { + 'parent2': 17, + 'parent3': 7 + }, + 'children': [ + { + 'name': 'child', + 'guid': 3, + 'size': 10, + 'expected_size': 10, + 'expected_effective_size': 1, + } + ] + }, + { + 'name': 'parent2', + // NOTE(petrcermak): The expected size here is a little strange + // because the children both own the same dump (namely + // root/parent1/child). It would, therefore, probably make more + // sense for the calculated size to be 9. Since this is an unlikely + // case and would complicate the (already complex) size + // calculation, we will now keep the algorithm as is. + 'expected_size': 17, + 'expected_effective_size': 14 / 3 + 2, + 'children': [ + { + 'name': 'child1', + 'owns': 3, + 'size': 9, + 'expected_size': 9, + 'expected_effective_size': 7 / 3 + 1 / 2 + 1, + }, + { + 'name': 'child2', + 'owns': 3, + 'size': 8, + 'expected_size': 8, + 'expected_effective_size': 7 / 3 + 1 / 2, + } + ] + }, + { + 'name': 'parent3', + 'size': 7, + 'expected_size': 7, + 'expected_effective_size': 7 / 3, + 'owns': 3 + } + ] + } + ] + ]); + + // Check that size calculation is NOT preceded by numeric aggregation, which + // would recursively sum up size numerics. + test('finalizeGraph_aggregation', function() { + const model = tr.c.TestUtils.newModel(function(model) { + buildDumpTrees([ + undefined, // GMD. + [ // PMD. + { + 'name': 'root', + 'children': [ + { + 'name': 'owner_child', + 'owns': 9, + 'size': 7 + }, + { + 'name': 'owned_child', + 'guid': 9, + 'size': 20 + } + ] + } + ] + ], model); + }); + const pmd = model.getProcess(0).memoryDumps[0]; + + const rootDump = pmd.getMemoryAllocatorDumpByFullName('root'); + assertDumpSizes(rootDump, 20, 20); + + const ownerChildDump = pmd.getMemoryAllocatorDumpByFullName( + 'root/owner_child'); + assertDumpSizes(ownerChildDump, 7, 7); + + const ownedChildDump = pmd.getMemoryAllocatorDumpByFullName( + 'root/owned_child'); + assertDumpSizes(ownedChildDump, 20, 13, [] /* expectedInfos */, + {'owner_child': 7} /* expectedOwnedBySiblingSizes */); + }); + + // Check that numeric and diagnostics propagation and aggregation are + // performed in the correct order. + test('finalizeGraph_propagation', function() { + const model = tr.c.TestUtils.newModel(function(model) { + buildDumpTrees([ + [ // GMD. + { + 'name': 'owned_root', + 'guid': 1, + 'size': 10, + 'diagnostics': { + 'url': 'https://hello.world.com:42' + }, + 'children': [ + { + 'name': 'owned_child1', + 'numerics': { + 'summed': new Scalar(sizeInBytes_smallerIsBetter, 12) + }, + 'diagnostics': { + 'url2': 'http://not.aggregated.to/owned/parent/dump' + } + }, + { + 'name': 'owned_child2', + 'numerics': { + 'summed': new Scalar(sizeInBytes_smallerIsBetter, 15) + } + } + ] + } + ], + [ // PMD. + { + 'name': 'direct_owner', + 'owns': 1, + 'guid': 2, + 'diagnostics': { + 'url': 'file://not_overriden.html' + } + }, + { + 'name': 'parent_owner', + 'children': [ + { + 'name': 'child_owner', + 'owns': 1 + }, + { + 'name': 'sibling', + 'size': 5, + 'numerics': { + 'summed': new Scalar(sizeInBytes_smallerIsBetter, 13) + } + } + ] + }, + { + 'name': 'precedent_owner', + 'owns': 1, + 'numerics': { + 'summed': new Scalar(sizeInBytes_smallerIsBetter, 0) + } + }, + { + 'name': 'indirect_owner', + 'owns': 2 + } + ] + ], model); + }); + const pmd = model.getProcess(0).memoryDumps[0]; + + checkDumpNumericsAndDiagnostics( + pmd.getMemoryAllocatorDumpByFullName('direct_owner'), + { + size: 10, + effective_size: 3.3333, + summed: 27 + }, + { + url: 'file://not_overriden.html' + }); + checkDumpNumericsAndDiagnostics( + pmd.getMemoryAllocatorDumpByFullName('parent_owner/child_owner'), + { + size: 10, + effective_size: 3.3333, + summed: 27 + }, + { + url: 'https://hello.world.com:42' + }); + checkDumpNumericsAndDiagnostics( + pmd.getMemoryAllocatorDumpByFullName('parent_owner'), + { + size: 15, + effective_size: 8.3333, + summed: 40 + }, {}); + checkDumpNumericsAndDiagnostics( + pmd.getMemoryAllocatorDumpByFullName('precedent_owner'), + { + size: 10, + effective_size: 3.3333, + summed: 0 + }, + { + url: 'https://hello.world.com:42' + }); + checkDumpNumericsAndDiagnostics( + pmd.getMemoryAllocatorDumpByFullName('indirect_owner'), {}, {}); + }); + + // Check that weak dumps are removed before size size calculation. + test('finalizeGraph_weakDumpRemoval', function() { + const model = tr.c.TestUtils.newModel(function(model) { + buildDumpTrees([ + undefined, // GMD. + [ // PMD. + { + 'name': 'root', + 'children': [ + { + 'name': 'directly_weak_child', + 'weak': true, + 'guid': 5, + 'owns': 10, + 'size': 100 + }, + { + 'name': 'strong_child', + 'guid': 10, + 'size': 120 + }, + { + 'name': 'indirectly_weak_child', + 'owns': 5, + 'size': 70 + }, + { + 'name': 'separate_weak_child', + 'weak': true, + 'size': 300 + } + ] + } + ] + ], model); + }); + const pmd = model.getProcess(0).memoryDumps[0]; + + const rootDump = pmd.getMemoryAllocatorDumpByFullName('root'); + assertDumpSizes(rootDump, 120, 120); + assert.lengthOf(rootDump.children, 1); + + const strongChildDump = pmd.getMemoryAllocatorDumpByFullName( + 'root/strong_child'); + assertDumpSizes(strongChildDump, 120, 120); + assert.lengthOf(strongChildDump.ownedBy, 0); + + assert.isUndefined(pmd.getMemoryAllocatorDumpByFullName( + 'root/directly_weak_child')); + assert.isUndefined(pmd.getMemoryAllocatorDumpByFullName( + 'root/indirectly_weak_child')); + assert.isUndefined(pmd.getMemoryAllocatorDumpByFullName( + 'root/separate_weak_child')); + }); + + test('indicesUpdatedCorrectly', function() { + let gmd; + let rootDump; + let childDump; + const model = tr.c.TestUtils.newModel(function(model) { + gmd = new GlobalMemoryDump(model, 10); + model.globalMemoryDumps.push(gmd); + + rootDump = newAllocatorDump(gmd, 'root', {numerics: {size: 64}}); + childDump = addChildDump(rootDump, 'child', {numerics: {size: 48}}); + + gmd.memoryAllocatorDumps = [rootDump]; + + // Before model is finalized. + assert.strictEqual( + gmd.getMemoryAllocatorDumpByFullName('root'), rootDump); + assert.strictEqual( + gmd.getMemoryAllocatorDumpByFullName('root/child'), childDump); + assert.isUndefined( + gmd.getMemoryAllocatorDumpByFullName('root/<unspecified>')); + }); + + // Test sanity check. + assert.isDefined(gmd); + assert.isDefined(rootDump); + assert.isDefined(childDump); + + // After model is finalized. + assert.strictEqual(gmd.getMemoryAllocatorDumpByFullName('root'), rootDump); + assert.strictEqual( + gmd.getMemoryAllocatorDumpByFullName('root/child'), childDump); + const unspecifiedDump = + gmd.getMemoryAllocatorDumpByFullName('root/<unspecified>'); + assert.strictEqual(unspecifiedDump.fullName, 'root/<unspecified>'); + assert.strictEqual(unspecifiedDump.parent, rootDump); + assert.strictEqual(rootDump.children[0], unspecifiedDump); + }); + + weakDumpRemovalTest('allDumpsNonWeak', [ + [ // GMD. + { + 'name': 'malloc', + 'children': [ + { + 'name': 'allocated_objects', + 'children': [ + { + 'name': 'obj42', + 'guid': 5, + 'expected_owned_by_links_count': 2 + } + ] + } + ] + } + ], + undefined, // PMD1. + [ // PMD2. + { + 'name': 'oilpan' + }, + { + 'name': 'v8', + 'children': [ + { + 'name': 'heaps', + 'children': [ + { + 'name': 'S', + 'owns': 5 + }, + { + 'name': 'L', + 'owns': 5 + } + ] + } + ] + } + ] + ]); + + weakDumpRemovalTest('weakRootDump', [ + [], // GMD. + [ // PMD1. + { + 'name': 'strong1' + }, + { + 'name': 'weak', + 'weak': true, + 'expected_removed': true + }, + { + 'name': 'strong2' + } + ] + ]); + + weakDumpRemovalTest('weakChildDump', [ + [ // GMD. + { + 'name': 'root', + 'children': [ + { + 'name': 'parent', + 'children': [ + { + 'name': 'strong1' + }, + { + 'name': 'weak', + 'weak': true, + 'expected_removed': true, + 'children': [ + { + 'name': 'implicitly-removed' + } + ] + }, + { + 'name': 'strong2' + } + ] + } + ] + } + ] + ]); + + weakDumpRemovalTest('transitiveOwnerRemoval', [ + [ // GMD. + { + 'name': 'not-removed-strong-dump', + 'guid': 0, + 'expected_owned_by_links_count': 1 + }, + { + 'name': 'weak-owned-dump', + 'guid': 1, + 'owns': 0, + 'weak': true, + 'expected_removed': true + } + ], + [ // PMD1. + { + 'name': 'direct-owner-dump', + 'guid': 2, + 'owns': 1, + 'expected_removed': true + }, + { + 'name': 'also-not-removed-strong-dump', + 'owns': 0 + } + ], + [ // PMD2. + { + 'name': 'indirect-owner-dump', + 'owns': 2, + 'expected_removed': true + } + ] + ]); + + weakDumpRemovalTest('transitiveDescendantRemoval', [ + [ // GMD. + { + 'name': 'A', + 'owns': 10, + // A =owns=> B -child-of-> C -> D => E -> F -> G (weak). + 'expected_removed': true + }, + { + 'name': 'D', + 'owns': 5, + 'expected_removed': true, // D =owns=> E -child-of-> F -> G (weak). + 'children': [ + { + 'name': 'C', + 'children': [ + { + 'name': 'B', + 'guid': 10 + } + ] + } + ] + } + ], + undefined, // PMD1. + [ // PMD2. + { + 'name': 'first-retained-dump', + 'children': [ + { + 'name': 'G', + 'weak': true, + 'expected_removed': true, + 'children': [ + { + 'name': 'F', + 'children': [ + { + 'name': 'E', + 'guid': 5 + } + ] + }, + { + 'name': 'H', + 'children': [ + { + 'name': 'I', + 'children': [ + { + 'name': 'J', + 'owns': 2 + } + ] + } + ] + } + ] + } + ] + } + ], + [ // PMD3. + { + 'name': 'second-retained-dump', + 'guid': 2, + // The only owner (J) is removed because J -child-of-> I -> H -> + // G (weak). + 'expected_owned_by_links_count': 0 + } + ] + ]); + + weakDumpRemovalTest('subownerships', [ + [ // GMD. + { + 'name': 'root1', + 'owns': 20, + 'expected_removed': true, // root1 =owns=> root2 (weak). + 'children': [ + { + 'name': 'child1', + 'owns': 2 + } + ] + }, + { + 'name': 'root2', + 'guid': 20, + 'owns': 30, + 'weak': true, + 'expected_removed': true, + 'children': [ + { + 'name': 'child2', + 'guid': 2, + 'owns': 3 + } + ] + }, + { + 'name': 'root3', + 'guid': 30, + 'owns': 40, + 'expected_owned_by_links_count': 0, + 'children': [ + { + 'name': 'child3', + 'guid': 3, + 'owns': 4, + 'weak': true, + 'expected_removed': true + } + ] + } + ], + [ // PMD1. + { + 'name': 'root4', + 'guid': 40, + 'expected_owned_by_links_count': 1, + 'children': [ + { + 'name': 'child4', + 'guid': 4, + 'expected_owned_by_links_count': 0 + } + ] + } + ], + [ // PMD2. + { + 'name': 'root5', + 'owns': 60, + 'expected_removed': true, // root5 =owns=> root6 => root7 (weak). + 'children': [ + { + 'name': 'child5', + 'owns': 6 + } + ] + }, + { + 'name': 'root6', + 'guid': 60, + 'owns': 70, + 'expected_removed': true, // root6 =owns=> root7 (weak). + 'children': [ + { + 'name': 'child6', + 'guid': 6, + 'owns': 7 + } + ] + }, + { + 'name': 'root7', + 'guid': 70, + 'owns': 40, + 'weak': true, + 'expected_removed': true, + 'children': [ + { + 'name': 'child7', + 'guid': 7, + 'owns': 4 + } + ] + } + ] + ]); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/heap_dump.html b/chromium/third_party/catapult/tracing/tracing/model/heap_dump.html new file mode 100644 index 00000000000..ec81a382b6f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/heap_dump.html @@ -0,0 +1,76 @@ +<!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"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + /** + * HeapEntry represents a single value describing the state of the heap of an + * allocator in a single process. + * + * An entry specifies how much space (e.g. 19 MiB) was allocated in a + * particular context, which consists of a codepath (e.g. drawQuad <- draw <- + * MessageLoop::RunTask) and an object type (e.g. HTMLImportLoader). + * + * If |valuesAreTotals| is true the size and count of this entry are totals + * for this and all more specific entries, otherwise they are values just for + * this specific entry. + * + * @{constructor} + */ + function HeapEntry( + heapDump, leafStackFrame, objectTypeName, size, count, valuesAreTotals) { + this.heapDump = heapDump; + + // The leaf stack frame of the associated backtrace (e.g. drawQuad for the + // drawQuad <- draw <- MessageLoop::RunTask backtrace). If undefined, the + // backtrace is empty. + this.leafStackFrame = leafStackFrame; + + // The name of the allocated object type (e.g. 'HTMLImportLoader'). If + // undefined, the entry represents the sum over all object types. + this.objectTypeName = objectTypeName; + + this.size = size; + this.count = count; + this.valuesAreTotals = valuesAreTotals; + } + + /** + * HeapDump represents a dump of the heap of an allocator in a single process + * at a particular timestamp. + * + * @{constructor} + */ + function HeapDump(processMemoryDump, allocatorName, isComplete) { + this.processMemoryDump = processMemoryDump; + this.allocatorName = allocatorName; + this.isComplete = isComplete; + this.entries = []; + } + + HeapDump.prototype = { + addEntry( + leafStackFrame, objectTypeName, size, count, opt_valuesAreTotals) { + if (opt_valuesAreTotals === undefined) opt_valuesAreTotals = true; + const valuesAreTotals = opt_valuesAreTotals; + const entry = new HeapEntry( + this, leafStackFrame, objectTypeName, size, count, valuesAreTotals); + this.entries.push(entry); + return entry; + } + }; + + return { + HeapEntry, + HeapDump, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/heap_dump_test.html b/chromium/third_party/catapult/tracing/tracing/model/heap_dump_test.html new file mode 100644 index 00000000000..9ce01725c03 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/heap_dump_test.html @@ -0,0 +1,61 @@ +<!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/model/global_memory_dump.html"> +<link rel="import" href="/tracing/model/heap_dump.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/process_memory_dump.html"> +<link rel="import" href="/tracing/model/stack_frame.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const GlobalMemoryDump = tr.model.GlobalMemoryDump; + const ProcessMemoryDump = tr.model.ProcessMemoryDump; + const StackFrame = tr.model.StackFrame; + const HeapEntry = tr.model.HeapEntry; + const HeapDump = tr.model.HeapDump; + + test('heapDumps', function() { + const model = new tr.Model(); + const process = model.getOrCreateProcess(89); + const gmd = new GlobalMemoryDump(model, 42); + model.globalMemoryDumps.push(gmd); + const pmd = new ProcessMemoryDump(gmd, process, 42); + process.memoryDumps.push(pmd); + + const rootFrame = new StackFrame( + undefined, tr.b.GUID.allocateSimple(), undefined); + const childFrame = new StackFrame( + rootFrame, tr.b.GUID.allocateSimple(), 'draw'); + rootFrame.addChild(childFrame); + + const dump = new HeapDump(pmd); + assert.strictEqual(dump.processMemoryDump, pmd); + assert.lengthOf(dump.entries, 0); + + const entry1 = dump.addEntry( + childFrame, 'HTMLImportLoader', 1024, undefined); + assert.strictEqual(entry1.heapDump, dump); + assert.strictEqual(entry1.leafStackFrame, childFrame); + assert.strictEqual(entry1.objectTypeName, 'HTMLImportLoader'); + assert.strictEqual(entry1.size, 1024); + assert.isUndefined(entry1.count); + + const entry2 = dump.addEntry(undefined, undefined, 1048576, 42); + assert.strictEqual(entry2.heapDump, dump); + assert.isUndefined(entry2.leafStackFrame); + assert.isUndefined(entry2.objectTypeName); + assert.strictEqual(entry2.size, 1048576); + assert.strictEqual(entry2.count, 42); + + assert.deepEqual(dump.entries, [entry1, entry2]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/android_app.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_app.html new file mode 100644 index 00000000000..9cabb504606 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_app.html @@ -0,0 +1,344 @@ +<!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/base/math/statistics.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/frame.html"> + +<script> +'use strict'; + +/** + * @fileoverview Class for managing android-specific model meta data, + * such as rendering apps, and frames rendered. + */ +tr.exportTo('tr.model.helpers', function() { + const Frame = tr.model.Frame; + const Statistics = tr.b.math.Statistics; + + const UI_DRAW_TYPE = { + NONE: 'none', + LEGACY: 'legacy', + MARSHMALLOW: 'marshmallow' + }; + + const UI_THREAD_DRAW_NAMES = { + 'performTraversals': UI_DRAW_TYPE.LEGACY, + 'Choreographer#doFrame': UI_DRAW_TYPE.MARSHMALLOW + }; + + const RENDER_THREAD_DRAW_NAME = 'DrawFrame'; + const RENDER_THREAD_INDEP_DRAW_NAME = 'doFrame'; + const RENDER_THREAD_QUEUE_NAME = 'queueBuffer'; + const RENDER_THREAD_SWAP_NAME = 'eglSwapBuffers'; + const THREAD_SYNC_NAME = 'syncFrameState'; + + function getSlicesForThreadTimeRanges(threadTimeRanges) { + const ret = []; + threadTimeRanges.forEach(function(threadTimeRange) { + const slices = []; + + threadTimeRange.thread.sliceGroup.iterSlicesInTimeRange( + function(slice) { slices.push(slice); }, + threadTimeRange.start, threadTimeRange.end); + ret.push.apply(ret, slices); + }); + return ret; + } + + function makeFrame(threadTimeRanges, surfaceFlinger) { + const args = {}; + if (surfaceFlinger && surfaceFlinger.hasVsyncs) { + const start = Statistics.min(threadTimeRanges, + function(threadTimeRanges) { return threadTimeRanges.start; }); + args.deadline = surfaceFlinger.getFrameDeadline(start); + args.frameKickoff = surfaceFlinger.getFrameKickoff(start); + } + const events = getSlicesForThreadTimeRanges(threadTimeRanges); + return new Frame(events, threadTimeRanges, args); + } + + function findOverlappingDrawFrame(renderThread, uiDrawSlice) { + if (!renderThread) return undefined; + + // of all top level renderthread slices, find the one that has a 'sync' + // within the uiDrawSlice + let overlappingDrawFrame; + const slices = tr.b.iterateOverIntersectingIntervals( + renderThread.sliceGroup.slices, + function(range) { return range.start; }, + function(range) { return range.end; }, + uiDrawSlice.start, + uiDrawSlice.end, + function(rtDrawSlice) { + if (rtDrawSlice.title === RENDER_THREAD_DRAW_NAME) { + const rtSyncSlice = rtDrawSlice.findDescendentSlice( + THREAD_SYNC_NAME); + if (rtSyncSlice && + rtSyncSlice.start >= uiDrawSlice.start && + rtSyncSlice.end <= uiDrawSlice.end) { + // sync observed which overlaps ui draw. This means the RT draw + // corresponds to the UI draw + overlappingDrawFrame = rtDrawSlice; + } + } + }); + return overlappingDrawFrame; + } + + /** + * Builds an array of {start, end} ranges grouping common work of a frame + * that occurs just before performTraversals(). + * + * Only necessary before Choreographer#doFrame tracing existed. + */ + function getPreTraversalWorkRanges(uiThread) { + if (!uiThread) return []; + + // gather all frame work that occurs outside of performTraversals + const preFrameEvents = []; + uiThread.sliceGroup.slices.forEach(function(slice) { + if (slice.title === 'obtainView' || + slice.title === 'setupListItem' || + slice.title === 'deliverInputEvent' || + slice.title === 'RV Scroll') { + preFrameEvents.push(slice); + } + }); + uiThread.asyncSliceGroup.slices.forEach(function(slice) { + if (slice.title === 'deliverInputEvent') { + preFrameEvents.push(slice); + } + }); + + return tr.b.math.mergeRanges( + tr.b.math.convertEventsToRanges(preFrameEvents), + 3, + function(events) { + return { + start: events[0].min, + end: events[events.length - 1].max + }; + }); + } + + function getFrameStartTime(traversalStart, preTraversalWorkRanges) { + const preTraversalWorkRange = + tr.b.findClosestIntervalInSortedIntervals( + preTraversalWorkRanges, + function(range) { return range.start; }, + function(range) { return range.end; }, + traversalStart, + 3); + + if (preTraversalWorkRange) { + return preTraversalWorkRange.start; + } + return traversalStart; + } + + function getRtFrameEndTime(rtDrawSlice) { + // First try and get time that frame is queued: + const rtQueueSlice = rtDrawSlice.findDescendentSlice( + RENDER_THREAD_QUEUE_NAME); + if (rtQueueSlice) { + return rtQueueSlice.end; + } + // failing that, end of swapbuffers: + const rtSwapSlice = rtDrawSlice.findDescendentSlice( + RENDER_THREAD_SWAP_NAME); + if (rtSwapSlice) { + return rtSwapSlice.end; + } + // failing that, end of renderthread frame trace + return rtDrawSlice.end; + } + + function getUiThreadDrivenFrames(app) { + if (!app.uiThread) return []; + + let preTraversalWorkRanges = []; + if (app.uiDrawType === UI_DRAW_TYPE.LEGACY) { + preTraversalWorkRanges = getPreTraversalWorkRanges(app.uiThread); + } + + const frames = []; + app.uiThread.sliceGroup.slices.forEach(function(slice) { + if (!(slice.title in UI_THREAD_DRAW_NAMES)) { + return; + } + + const threadTimeRanges = []; + const uiThreadTimeRange = { + thread: app.uiThread, + start: getFrameStartTime(slice.start, preTraversalWorkRanges), + end: slice.end + }; + threadTimeRanges.push(uiThreadTimeRange); + + // on SDK 21+ devices with RenderThread, + // account for time taken on RenderThread + const rtDrawSlice = findOverlappingDrawFrame( + app.renderThread, slice); + if (rtDrawSlice) { + const rtSyncSlice = rtDrawSlice.findDescendentSlice(THREAD_SYNC_NAME); + if (rtSyncSlice) { + // Generally, the UI thread is only on the critical path + // until the start of sync. + uiThreadTimeRange.end = Math.min(uiThreadTimeRange.end, + rtSyncSlice.start); + } + + threadTimeRanges.push({ + thread: app.renderThread, + start: rtDrawSlice.start, + end: getRtFrameEndTime(rtDrawSlice) + }); + } + frames.push(makeFrame(threadTimeRanges, app.surfaceFlinger)); + }); + return frames; + } + + function getRenderThreadDrivenFrames(app) { + if (!app.renderThread) return []; + + const frames = []; + app.renderThread.sliceGroup.getSlicesOfName(RENDER_THREAD_INDEP_DRAW_NAME) + .forEach(function(slice) { + const threadTimeRanges = [{ + thread: app.renderThread, + start: slice.start, + end: slice.end + }]; + frames.push(makeFrame(threadTimeRanges, app.surfaceFlinger)); + }); + return frames; + } + + function getUiDrawType(uiThread) { + if (!uiThread) { + return UI_DRAW_TYPE.NONE; + } + + const slices = uiThread.sliceGroup.slices; + for (let i = 0; i < slices.length; i++) { + if (slices[i].title in UI_THREAD_DRAW_NAMES) { + return UI_THREAD_DRAW_NAMES[slices[i].title]; + } + } + return UI_DRAW_TYPE.NONE; + } + + function getInputSamples(process) { + let samples = undefined; + for (const counterName in process.counters) { + if (/^android\.aq\:pending/.test(counterName) && + process.counters[counterName].numSeries === 1) { + samples = process.counters[counterName].series[0].samples; + break; + } + } + + if (!samples) return []; + + // output rising edges only, since those are user inputs + const inputSamples = []; + let lastValue = 0; + samples.forEach(function(sample) { + if (sample.value > lastValue) { + inputSamples.push(sample); + } + lastValue = sample.value; + }); + return inputSamples; + } + + function getAnimationAsyncSlices(uiThread) { + if (!uiThread) return []; + + const slices = []; + for (const slice of uiThread.asyncSliceGroup.getDescendantEvents()) { + if (/^animator\:/.test(slice.title)) { + slices.push(slice); + } + } + return slices; + } + + /** + * Model for Android App specific data. + * @constructor + */ + function AndroidApp(process, uiThread, renderThread, surfaceFlinger, + uiDrawType) { + this.process = process; + this.uiThread = uiThread; + this.renderThread = renderThread; + this.surfaceFlinger = surfaceFlinger; + this.uiDrawType = uiDrawType; + + this.frames_ = undefined; + this.inputs_ = undefined; + } + + AndroidApp.createForProcessIfPossible = function(process, surfaceFlinger) { + let uiThread = process.getThread(process.pid); + const uiDrawType = getUiDrawType(uiThread); + if (uiDrawType === UI_DRAW_TYPE.NONE) { + uiThread = undefined; + } + const renderThreads = process.findAllThreadsNamed('RenderThread'); + const renderThread = (renderThreads.length === 1 ? + renderThreads[0] : undefined); + + if (uiThread || renderThread) { + return new AndroidApp(process, uiThread, renderThread, surfaceFlinger, + uiDrawType); + } + }; + + AndroidApp.prototype = { + /** + * Returns a list of all frames in the trace for the app, + * constructed on first query. + */ + getFrames() { + if (!this.frames_) { + const uiFrames = getUiThreadDrivenFrames(this); + const rtFrames = getRenderThreadDrivenFrames(this); + this.frames_ = uiFrames.concat(rtFrames); + + // merge frames by sorting by end timestamp + this.frames_.sort(function(a, b) { a.end - b.end; }); + } + return this.frames_; + }, + + /** + * Returns list of CounterSamples for each input event enqueued to the app. + */ + getInputSamples() { + if (!this.inputs_) { + this.inputs_ = getInputSamples(this.process); + } + return this.inputs_; + }, + + getAnimationAsyncSlices() { + if (!this.animations_) { + this.animations_ = getAnimationAsyncSlices(this.uiThread); + } + return this.animations_; + } + }; + + return { + AndroidApp, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper.html new file mode 100644 index 00000000000..76239a31942 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper.html @@ -0,0 +1,105 @@ +<!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/guid.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/helpers/android_app.html"> +<link rel="import" href="/tracing/model/helpers/android_surface_flinger.html"> + +<script> +'use strict'; + +/** + * @fileoverview Class for managing android-specific model meta data, + * such as rendering apps, frames rendered, and SurfaceFlinger. + */ +tr.exportTo('tr.model.helpers', function() { + const AndroidApp = tr.model.helpers.AndroidApp; + const AndroidSurfaceFlinger = tr.model.helpers.AndroidSurfaceFlinger; + + const IMPORTANT_SURFACE_FLINGER_SLICES = { + 'doComposition': true, + 'updateTexImage': true, + 'postFramebuffer': true + }; + const IMPORTANT_UI_THREAD_SLICES = { + 'Choreographer#doFrame': true, + 'performTraversals': true, + 'deliverInputEvent': true + }; + const IMPORTANT_RENDER_THREAD_SLICES = { + 'doFrame': true + }; + + function iterateImportantThreadSlices(thread, important, callback) { + if (!thread) return; + + thread.sliceGroup.slices.forEach(function(slice) { + if (slice.title in important) { + callback(slice); + } + }); + } + + /** + * Model for Android-specific data. + * @constructor + */ + function AndroidModelHelper(model) { + this.model = model; + this.apps = []; + this.surfaceFlinger = undefined; + + const processes = model.getAllProcesses(); + for (let i = 0; i < processes.length && !this.surfaceFlinger; i++) { + this.surfaceFlinger = + AndroidSurfaceFlinger.createForProcessIfPossible(processes[i]); + } + + model.getAllProcesses().forEach(function(process) { + const app = AndroidApp.createForProcessIfPossible( + process, this.surfaceFlinger); + if (app) { + this.apps.push(app); + } + }, this); + } + + AndroidModelHelper.guid = tr.b.GUID.allocateSimple(); + + AndroidModelHelper.supportsModel = function(model) { + return true; + }; + + AndroidModelHelper.prototype = { + iterateImportantSlices(callback) { + if (this.surfaceFlinger) { + iterateImportantThreadSlices( + this.surfaceFlinger.thread, + IMPORTANT_SURFACE_FLINGER_SLICES, + callback); + } + + this.apps.forEach(function(app) { + iterateImportantThreadSlices( + app.uiThread, + IMPORTANT_UI_THREAD_SLICES, + callback); + iterateImportantThreadSlices( + app.renderThread, + IMPORTANT_RENDER_THREAD_SLICES, + callback); + }); + } + }; + + return { + AndroidModelHelper, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper_test.html new file mode 100644 index 00000000000..0add58195af --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_model_helper_test.html @@ -0,0 +1,267 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/extras/android/android_auditor.html"> +<link rel="import" href="/tracing/extras/importer/linux_perf/ftrace_importer.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const AndroidModelHelper = tr.model.helpers.AndroidModelHelper; + const newAsyncSliceNamed = tr.c.TestUtils.newAsyncSliceNamed; + const newSliceEx = tr.c.TestUtils.newSliceEx; + const newCounterNamed = tr.c.TestUtils.newCounterNamed; + const newCounterSeries = tr.c.TestUtils.newCounterSeries; + + function createSurfaceFlinger(model, vsyncCallback) { + if (model.getProcess(2)) { + throw new Error('process already exists'); + } + + const sfProcess = model.getOrCreateProcess(2); + const sfThread = sfProcess.getOrCreateThread(2); // main thread, tid = pid + sfThread.name = '/system/bin/surfaceflinger'; + + // ensure slicegroup has data + sfThread.sliceGroup.pushSlice(newSliceEx({ + title: 'doComposition', + start: 8, + duration: 2 + })); + + vsyncCallback(sfProcess); + } + + function createSurfaceFlingerWithVsync(model) { + createSurfaceFlinger(model, function(sfProcess) { + const counter = sfProcess.getOrCreateCounter('android', 'VSYNC'); + const series = newCounterSeries(); + for (let i = 0; i <= 10; i++) { + series.addCounterSample(i * 10, i % 2); + } + counter.addSeries(series); + }); + } + + /* + * List of customizeModelCallbacks which produce different 80ms frames, + * each starting at 10ms, and with a single important slice + */ + const SINGLE_FRAME_CUSTOM_MODELS = [ + function(model) { + // UI thread only + const uiThread = model.getOrCreateProcess(120).getOrCreateThread(120); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 10, duration: 80})); + + model.uiThread = uiThread; + }, + + function(model) { + // RenderThread only + const renderThread = model.getOrCreateProcess(120).getOrCreateThread(200); + renderThread.name = 'RenderThread'; + renderThread.sliceGroup.pushSlice(newSliceEx( + {title: 'doFrame', start: 10, duration: 80})); + + model.renderThread = renderThread; + }, + + function(model) { + const uiThread = model.getOrCreateProcess(120).getOrCreateThread(120); + + // UI thread time - 19 (from 10 to 29, ignored after) + uiThread.asyncSliceGroup.push( + newAsyncSliceNamed('deliverInputEvent', 10, 9, uiThread, uiThread)); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 20, duration: 110})); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'draw', start: 20, duration: 108})); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'Record View#draw()', start: 20, duration: 8})); + + // RenderThread time - 61 (from 29 to 90, ignored after) + const renderThread = model.getOrCreateProcess(120).getOrCreateThread(200); + renderThread.name = 'RenderThread'; + renderThread.sliceGroup.pushSlice(newSliceEx( + {title: 'DrawFrame', start: 29, duration: 70})); + renderThread.sliceGroup.pushSlice(newSliceEx( + {title: 'syncFrameState', start: 29, duration: 1})); + renderThread.sliceGroup.pushSlice(newSliceEx( + {title: 'queueBuffer', start: 89, duration: 1})); + + model.uiThread = uiThread; + model.renderThread = renderThread; + } + ]; + + test('getThreads', function() { + SINGLE_FRAME_CUSTOM_MODELS.forEach(function(customizeModelCallback) { + const model = tr.c.TestUtils.newModel(customizeModelCallback); + const helper = model.getOrCreateHelper(AndroidModelHelper); + assert.strictEqual(helper.apps[0].uiThread, model.uiThread); + assert.strictEqual(helper.apps[0].renderThread, model.renderThread); + }); + }); + + test('iterateImportantSlices', function() { + SINGLE_FRAME_CUSTOM_MODELS.forEach(function(customizeModelCallback) { + const model = tr.c.TestUtils.newModel(customizeModelCallback); + const helper = model.getOrCreateHelper(AndroidModelHelper); + + let seen = 0; + helper.iterateImportantSlices(function(importantSlice) { + assert.isTrue(importantSlice instanceof tr.model.Slice); + seen++; + }); + assert.strictEqual(seen, 1); + }); + }); + + test('getFrames', function() { + SINGLE_FRAME_CUSTOM_MODELS.forEach(function(customizeModelCallback) { + const model = tr.c.TestUtils.newModel(customizeModelCallback); + const helper = model.getOrCreateHelper(AndroidModelHelper); + assert.strictEqual(helper.apps.length, 1); + + const frames = helper.apps[0].getFrames(); + assert.strictEqual(frames.length, 1); + assert.closeTo(frames[0].totalDuration, 80, 1e-5); + + assert.closeTo(frames[0].start, 10, 1e-5); + assert.closeTo(frames[0].end, 90, 1e-5); + }); + }); + + test('surfaceFlingerVsyncs', function() { + const model = tr.c.TestUtils.newModel(createSurfaceFlingerWithVsync); + const helper = model.getOrCreateHelper(AndroidModelHelper); + assert.isTrue(helper.surfaceFlinger.hasVsyncs); + + // test querying the vsyncs + assert.closeTo(helper.surfaceFlinger.getFrameKickoff(5), 0, 1e-5); + assert.closeTo(helper.surfaceFlinger.getFrameDeadline(95), 100, 1e-5); + + assert.closeTo(helper.surfaceFlinger.getFrameKickoff(10), 10, 1e-5); + assert.closeTo(helper.surfaceFlinger.getFrameDeadline(90), 100, 1e-5); + + // test undefined behavior outside of vsyncs. + assert.isUndefined(helper.surfaceFlinger.getFrameKickoff(-5)); + assert.isUndefined(helper.surfaceFlinger.getFrameDeadline(105)); + }); + + test('surfaceFlingerShiftedVsyncs', function() { + const model = tr.c.TestUtils.newModel(function(model) { + createSurfaceFlinger(model, function(sfProcess) { + const appSeries = newCounterSeries(); + const sfSeries = newCounterSeries(); + for (let i = 0; i <= 10; i++) { + // SF vsync is 4ms after app + appSeries.addCounterSample(i * 16, i % 2); + sfSeries.addCounterSample(i * 16 + 4, i % 2); + } + sfProcess.getOrCreateCounter('android', 'VSYNC-sf') + .addSeries(sfSeries); + sfProcess.getOrCreateCounter('android', 'VSYNC-app') + .addSeries(appSeries); + }); + }); + const helper = model.getOrCreateHelper(AndroidModelHelper); + assert.isTrue(helper.surfaceFlinger.hasVsyncs); + + // test querying the vsyncs - Frames should have 20ms window + assert.closeTo(helper.surfaceFlinger.getFrameKickoff(0), 0, 1e-5); + assert.closeTo(helper.surfaceFlinger.getFrameDeadline(0), 20, 1e-5); + + assert.closeTo(helper.surfaceFlinger.getFrameKickoff(16), 16, 1e-5); + assert.closeTo(helper.surfaceFlinger.getFrameDeadline(16), 36, 1e-5); + + // test undefined behavior outside of vsyncs. + assert.isUndefined(helper.surfaceFlinger.getFrameKickoff(-5)); + assert.isUndefined(helper.surfaceFlinger.getFrameDeadline(165)); + }); + + test('frameVsyncInterop', function() { + const model = tr.c.TestUtils.newModel(function(model) { + // app - 3 good, 3 bad frames + const uiThread = model.getOrCreateProcess(1).getOrCreateThread(1); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 1, duration: 8})); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 10, duration: 8})); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 20, duration: 8})); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 31, duration: 11})); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 45, duration: 6})); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 60, duration: 20})); + + // surface flinger - vsync every 10ms + createSurfaceFlingerWithVsync(model); + }); + const helper = model.getOrCreateHelper(AndroidModelHelper); + + const frames = helper.apps[0].getFrames(); + assert.strictEqual(frames.length, 6); + for (let i = 0; i < 6; i++) { + const shouldMissDeadline = i >= 3; + const missedDeadline = frames[i].args.deadline < frames[i].end; + assert.strictEqual(shouldMissDeadline, missedDeadline); + } + }); + + test('appInputs', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(120); + const uiThread = process.getOrCreateThread(120); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 20, duration: 4})); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 40, duration: 4})); + + const counter = process.getOrCreateCounter('android', 'aq:pending:foo'); + const series = newCounterSeries(); + series.addCounterSample(10, 1); + series.addCounterSample(20, 0); + series.addCounterSample(30, 1); + series.addCounterSample(40, 2); + series.addCounterSample(50, 0); + counter.addSeries(series); + }); + const helper = model.getOrCreateHelper(AndroidModelHelper); + assert.strictEqual(helper.apps.length, 1); + + const inputSamples = helper.apps[0].getInputSamples(); + assert.strictEqual(inputSamples.length, 3); + assert.strictEqual(inputSamples[0].timestamp, 10); + assert.strictEqual(inputSamples[1].timestamp, 30); + assert.strictEqual(inputSamples[2].timestamp, 40); + }); + + test('appAnimations', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(120); + const uiThread = process.getOrCreateThread(120); + uiThread.sliceGroup.pushSlice(newSliceEx( + {title: 'performTraversals', start: 10, duration: 10})); + uiThread.asyncSliceGroup.push(newAsyncSliceNamed('animator:foo', 0, 10, + uiThread, uiThread)); + }); + const helper = model.getOrCreateHelper(AndroidModelHelper); + assert.strictEqual(helper.apps.length, 1); + + const animations = helper.apps[0].getAnimationAsyncSlices(); + assert.strictEqual(animations.length, 1); + assert.strictEqual(animations[0].start, 0); + assert.strictEqual(animations[0].end, 10); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/android_surface_flinger.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_surface_flinger.html new file mode 100644 index 00000000000..7595be059b3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/android_surface_flinger.html @@ -0,0 +1,115 @@ +<!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/utils.html"> + +<script> +'use strict'; + +/** + * @fileoverview Class for representing SurfaceFlinger process and its Vsyncs. + */ +tr.exportTo('tr.model.helpers', function() { + const findLowIndexInSortedArray = tr.b.findLowIndexInSortedArray; + + const VSYNC_SF_NAME = 'android.VSYNC-sf'; + const VSYNC_APP_NAME = 'android.VSYNC-app'; + const VSYNC_FALLBACK_NAME = 'android.VSYNC'; + + // when sampling vsync, push samples back by this much to ensure + // frame start samples *between* vsyncs + const TIMESTAMP_FUDGE_MS = 0.01; + + function getVsyncTimestamps(process, counterName) { + let vsync = process.counters[counterName]; + if (!vsync) { + vsync = process.counters[VSYNC_FALLBACK_NAME]; + } + + if (vsync && vsync.numSeries === 1 && vsync.numSamples > 1) { + return vsync.series[0].timestamps; + } + return undefined; + } + + /** + * Model for SurfaceFlinger specific data. + * @constructor + */ + function AndroidSurfaceFlinger(process, thread) { + this.process = process; + this.thread = thread; + + this.appVsync_ = undefined; + this.sfVsync_ = undefined; + + this.appVsyncTimestamps_ = getVsyncTimestamps(process, VSYNC_APP_NAME); + this.sfVsyncTimestamps_ = getVsyncTimestamps(process, VSYNC_SF_NAME); + + // separation of vsync of app vs sf - assume app has at least window of 5ms + this.deadlineDelayMs_ = + this.appVsyncTimestamps_ !== this.sfVsyncTimestamps_ ? + 5 : TIMESTAMP_FUDGE_MS; + } + + AndroidSurfaceFlinger.createForProcessIfPossible = function(process) { + const mainThread = process.getThread(process.pid); + + // newer versions - main thread, lowercase name, preceeding forward slash + if (mainThread && mainThread.name && + /surfaceflinger/.test(mainThread.name)) { + return new AndroidSurfaceFlinger(process, mainThread); + } + + // older versions - another thread is named SurfaceFlinger + const primaryThreads = process.findAllThreadsNamed('SurfaceFlinger'); + if (primaryThreads.length === 1) { + return new AndroidSurfaceFlinger(process, primaryThreads[0]); + } + return undefined; + }; + + AndroidSurfaceFlinger.prototype = { + get hasVsyncs() { + return !!this.appVsyncTimestamps_ && !!this.sfVsyncTimestamps_; + }, + + getFrameKickoff(timestamp) { + if (!this.hasVsyncs) { + throw new Error('cannot query vsync info without vsyncs'); + } + + const firstGreaterIndex = + findLowIndexInSortedArray(this.appVsyncTimestamps_, + function(x) { return x; }, + timestamp + TIMESTAMP_FUDGE_MS); + + if (firstGreaterIndex < 1) return undefined; + return this.appVsyncTimestamps_[firstGreaterIndex - 1]; + }, + + getFrameDeadline(timestamp) { + if (!this.hasVsyncs) { + throw new Error('cannot query vsync info without vsyncs'); + } + + const firstGreaterIndex = + findLowIndexInSortedArray(this.sfVsyncTimestamps_, + function(x) { return x; }, + timestamp + this.deadlineDelayMs_); + if (firstGreaterIndex >= this.sfVsyncTimestamps_.length) { + return undefined; + } + return this.sfVsyncTimestamps_[firstGreaterIndex]; + } + }; + + return { + AndroidSurfaceFlinger, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper.html new file mode 100644 index 00000000000..9c9cb6e473b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper.html @@ -0,0 +1,119 @@ +<!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/utils.html"> +<link rel="import" href="/tracing/model/helpers/chrome_process_helper.html"> + +<script> +'use strict'; + +/** + * @fileoverview Utilities for accessing trace data about the Chrome browser. + */ +tr.exportTo('tr.model.helpers', function() { + function ChromeBrowserHelper(modelHelper, process) { + tr.model.helpers.ChromeProcessHelper.call(this, modelHelper, process); + this.mainThread_ = process.findAtMostOneThreadNamed('CrBrowserMain'); + if (!process.name) { + process.name = ChromeBrowserHelper.PROCESS_NAME; + } + } + + ChromeBrowserHelper.PROCESS_NAME = 'Browser'; + + ChromeBrowserHelper.isBrowserProcess = function(process) { + return !!process.findAtMostOneThreadNamed('CrBrowserMain'); + }; + + ChromeBrowserHelper.prototype = { + __proto__: tr.model.helpers.ChromeProcessHelper.prototype, + + // TODO(petrcermak): Pass browser name in a metadata event (see + // crbug.com/605088). + get browserName() { + const hasInProcessRendererThread = this.process.findAllThreadsNamed( + 'Chrome_InProcRendererThread').length > 0; + return hasInProcessRendererThread ? 'webview' : 'chrome'; + }, + + get mainThread() { + return this.mainThread_; + }, + + get rendererHelpers() { + return this.modelHelper.rendererHelpers; + }, + + getLoadingEventsInRange(rangeOfInterest) { + return this.getAllAsyncSlicesMatching(function(slice) { + return slice.title.indexOf('WebContentsImpl Loading') === 0 && + rangeOfInterest.intersectsExplicitRangeInclusive( + slice.start, slice.end); + }); + }, + + getCommitProvisionalLoadEventsInRange(rangeOfInterest) { + return this.getAllAsyncSlicesMatching(function(slice) { + return slice.title === 'RenderFrameImpl::didCommitProvisionalLoad' && + rangeOfInterest.intersectsExplicitRangeInclusive( + slice.start, slice.end); + }); + }, + + get hasLatencyEvents() { + let hasLatency = false; + for (const thread of this.modelHelper.model.getAllThreads()) { + for (const event of thread.getDescendantEvents()) { + if (!event.isTopLevel) continue; + if (!(event instanceof tr.e.cc.InputLatencyAsyncSlice)) { + continue; + } + hasLatency = true; + } + } + return hasLatency; + }, + + getLatencyEventsInRange(rangeOfInterest) { + return this.getAllAsyncSlicesMatching(function(slice) { + return (slice.title.indexOf('InputLatency') === 0) && + rangeOfInterest.intersectsExplicitRangeInclusive( + slice.start, slice.end); + }); + }, + + getAllAsyncSlicesMatching(pred, opt_this) { + const events = []; + this.iterAllThreads(function(thread) { + for (const slice of thread.getDescendantEvents()) { + if (pred.call(opt_this, slice)) { + events.push(slice); + } + } + }); + return events; + }, + + iterAllThreads(func, opt_this) { + for (const thread of Object.values(this.process.threads)) { + func.call(opt_this, thread); + } + + for (const rendererHelper of Object.values(this.rendererHelpers)) { + const rendererProcess = rendererHelper.process; + for (const thread of Object.values(rendererProcess.threads)) { + func.call(opt_this, thread); + } + } + } + }; + + return { + ChromeBrowserHelper, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper_test.html new file mode 100644 index 00000000000..a53f31127ea --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_browser_helper_test.html @@ -0,0 +1,74 @@ +<!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/core/test_utils.html"> +<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/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/helpers/chrome_browser_helper.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const INPUT_TYPE = tr.e.cc.INPUT_EVENT_TYPE_NAMES; + + function getRange(min, max) { + const range = new tr.b.math.Range(); + range.min = min; + range.max = max; + return range; + } + + test('LoadingEvent', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function() { }); + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + tr.e.chrome.ChromeTestUtils.addLoadingEvent(model, {start: 1, end: 10}); + assert.strictEqual(1, modelHelper.browserHelper.getLoadingEventsInRange( + getRange(0, 100)).length); + }); + + test('ProvisionalLoadEvent', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function() { }); + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + tr.e.chrome.ChromeTestUtils.addCommitLoadEvent(model, {start: 1, end: 10}); + assert.strictEqual(1, + modelHelper.browserHelper.getCommitProvisionalLoadEventsInRange( + getRange(0, 100)).length); + }); + + test('LatencyEvent', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function() { }); + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + tr.e.chrome.ChromeTestUtils.addInputEvent( + model, INPUT_TYPE.UNKNOWN, {start: 1, end: 10}); + assert.strictEqual(1, modelHelper.browserHelper.getLatencyEventsInRange( + getRange(0, 100)).length); + }); + + test('browserName_chrome', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function() { }); + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + assert.strictEqual(modelHelper.browserHelper.browserName, 'chrome'); + }); + + test('browserName_webview', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function() { }); + model.browserProcess.getOrCreateThread(42).name = + 'Chrome_InProcRendererThread'; + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + assert.strictEqual(modelHelper.browserHelper.browserName, 'webview'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper.html new file mode 100644 index 00000000000..cf3857f1636 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper.html @@ -0,0 +1,47 @@ +<!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/helpers/chrome_process_helper.html"> + +<script> +'use strict'; + +/** + * @fileoverview Utilities for accessing the Chrome GPU Process. + */ +tr.exportTo('tr.model.helpers', function() { + // TODO(charliea): This method should probably throw if this isn't a Chrome + // GPU process. + function ChromeGpuHelper(modelHelper, process) { + tr.model.helpers.ChromeProcessHelper.call(this, modelHelper, process); + if (!process.name) { + process.name = ChromeGpuHelper.PROCESS_NAME; + } + } + + ChromeGpuHelper.PROCESS_NAME = 'GPU Process'; + + ChromeGpuHelper.isGpuProcess = function(process) { + // In some Android builds the GPU thread is not in a separate process. + if (process.findAtMostOneThreadNamed('CrBrowserMain') || + process.findAtMostOneThreadNamed('CrRendererMain')) { + return false; + } + + // On Android, there can sometimes be GPU processes with multiple main + // threads. We need to recognize those processes as GPU processes. + return process.findAllThreadsNamed('CrGpuMain').length > 0; + }; + + ChromeGpuHelper.prototype = { + __proto__: tr.model.helpers.ChromeProcessHelper.prototype + }; + + return { + ChromeGpuHelper, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper_test.html new file mode 100644 index 00000000000..11f3be603b0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_gpu_helper_test.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/helpers/chrome_gpu_helper.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ChromeGpuHelper = tr.model.helpers.ChromeGpuHelper; + + test('constructor_doesntThrowIfMultipleMainThreads', function() { + const model = tr.c.TestUtils.newModel(model => { + model.getOrCreateProcess(1).getOrCreateThread(1).name = 'CrGpuMain'; + model.getOrCreateProcess(1).getOrCreateThread(2).name = 'CrGpuMain'; + }); + + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + + new ChromeGpuHelper(modelHelper, model.getOrCreateProcess(1)); + }); + + test('constructor_doesntThrowIfNoMainThread', function() { + const model = tr.c.TestUtils.newModel(model => { + model.getOrCreateProcess(1); + }); + + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + + new ChromeGpuHelper(modelHelper, model.getOrCreateProcess(1)); + }); + + test('constructor_namesProcessIfUnnamed', function() { + const model = tr.c.TestUtils.newModel(model => { + model.getOrCreateProcess(1); + }); + + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + + new ChromeGpuHelper(modelHelper, model.getOrCreateProcess(1)); + assert.strictEqual(model.getOrCreateProcess(1).name, 'GPU Process'); + }); + + test('constructor_doesntNameProcessIfNamed', function() { + const model = tr.c.TestUtils.newModel(model => { + model.getOrCreateProcess(1).name = 'Example process name'; + }); + + const modelHelper = model.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + + new ChromeGpuHelper(modelHelper, model.getOrCreateProcess(1)); + assert.strictEqual( + model.getOrCreateProcess(1).name, 'Example process name'); + }); + + test('isGpuProcess_falseIfNoMainThread', function() { + const model = tr.c.TestUtils.newModel(model => { + model.getOrCreateProcess(1); + }); + + assert.isFalse(ChromeGpuHelper.isGpuProcess(model.getOrCreateProcess(1))); + }); + + test('isGpuProcess', function() { + const model = tr.c.TestUtils.newModel(model => { + model.getOrCreateProcess(1).getOrCreateThread(1).name = 'CrGpuMain'; + }); + + assert.isTrue(ChromeGpuHelper.isGpuProcess(model.getOrCreateProcess(1))); + }); + + test('isGpuProcess_trueIfMultipleMainThreads', function() { + const model = tr.c.TestUtils.newModel(model => { + model.getOrCreateProcess(1).getOrCreateThread(1).name = 'CrGpuMain'; + model.getOrCreateProcess(1).getOrCreateThread(2).name = 'CrGpuMain'; + }); + + assert.isTrue(ChromeGpuHelper.isGpuProcess(model.getOrCreateProcess(1))); + }); + + test('isGpuProcess_falseIfBrowserProcess', function() { + const model = tr.c.TestUtils.newModel(model => { + model.getOrCreateProcess(1).getOrCreateThread(1).name = 'CrGpuMain'; + model.getOrCreateProcess(1).getOrCreateThread(2).name = 'CrBrowserMain'; + }); + + assert.isFalse(ChromeGpuHelper.isGpuProcess(model.getOrCreateProcess(1))); + }); + + test('isGpuProcess_falseIfRendererProcess', function() { + const model = tr.c.TestUtils.newModel(model => { + model.getOrCreateProcess(1).getOrCreateThread(1).name = 'CrGpuMain'; + model.getOrCreateProcess(1).getOrCreateThread(2).name = 'CrRendererMain'; + }); + + assert.isFalse(ChromeGpuHelper.isGpuProcess(model.getOrCreateProcess(1))); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper.html new file mode 100644 index 00000000000..156a96b1304 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper.html @@ -0,0 +1,168 @@ +<!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/guid.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/helpers/chrome_browser_helper.html"> +<link rel="import" href="/tracing/model/helpers/chrome_gpu_helper.html"> +<link rel="import" href="/tracing/model/helpers/chrome_renderer_helper.html"> +<link rel="import" href="/tracing/model/helpers/telemetry_helper.html"> + +<script> +'use strict'; + +/** + * @fileoverview Utilities for accessing trace data about the Chrome browser. + */ +tr.exportTo('tr.model.helpers', function() { + function findChromeBrowserProcesses(model) { + return model.getAllProcesses( + tr.model.helpers.ChromeBrowserHelper.isBrowserProcess); + } + + function findChromeRenderProcesses(model) { + return model.getAllProcesses( + tr.model.helpers.ChromeRendererHelper.isRenderProcess); + } + + function findChromeGpuProcess(model) { + const gpuProcesses = model.getAllProcesses( + tr.model.helpers.ChromeGpuHelper.isGpuProcess); + if (gpuProcesses.length !== 1) return undefined; + return gpuProcesses[0]; + } + + function findTelemetrySurfaceFlingerProcess(model) { + const surfaceFlingerProcesses = model.getAllProcesses( + process => (process.name === 'SurfaceFlinger')); + if (surfaceFlingerProcesses.length !== 1) return undefined; + return surfaceFlingerProcesses[0]; + } + + function ChromeModelHelper(model) { + this.model_ = model; + + // Find browserHelpers. + const browserProcesses = findChromeBrowserProcesses(model); + this.browserHelpers_ = browserProcesses.map( + p => new tr.model.helpers.ChromeBrowserHelper(this, p)); + + // Find gpuHelper. + const gpuProcess = findChromeGpuProcess(model); + if (gpuProcess) { + this.gpuHelper_ = new tr.model.helpers.ChromeGpuHelper( + this, gpuProcess); + } else { + this.gpuHelper_ = undefined; + } + + // Find rendererHelpers. + const rendererProcesses_ = findChromeRenderProcesses(model); + + this.rendererHelpers_ = {}; + rendererProcesses_.forEach(function(renderProcess) { + const rendererHelper = new tr.model.helpers.ChromeRendererHelper( + this, renderProcess); + this.rendererHelpers_[rendererHelper.pid] = rendererHelper; + }, this); + + this.surfaceFlingerProcess_ = findTelemetrySurfaceFlingerProcess(model); + + this.chromeBounds_ = undefined; + + this.telemetryHelper_ = new tr.model.helpers.TelemetryHelper(this); + } + + ChromeModelHelper.guid = tr.b.GUID.allocateSimple(); + + ChromeModelHelper.supportsModel = function(model) { + if (findChromeBrowserProcesses(model).length) return true; + if (findChromeRenderProcesses(model).length) return true; + return false; + }; + + ChromeModelHelper.prototype = { + get pid() { + throw new Error('woah'); + }, + + get process() { + throw new Error('woah'); + }, + + get model() { + return this.model_; + }, + + // TODO: Make all users of ChromeModelHelper support multiple browsers and + // remove this getter (see #2119). + get browserProcess() { + if (this.browserHelper === undefined) return undefined; + return this.browserHelper.process; + }, + + // TODO: Make all users of ChromeModelHelper support multiple browsers and + // remove this getter (see #2119). + get browserHelper() { + return this.browserHelpers_[0]; + }, + + get browserHelpers() { + return this.browserHelpers_; + }, + + get gpuHelper() { + return this.gpuHelper_; + }, + + get rendererHelpers() { + return this.rendererHelpers_; + }, + + get surfaceFlingerProcess() { + return this.surfaceFlingerProcess_; + }, + + /** + * Returns the minimal bounds that includes all Chrome-related slices, or + * undefined if no such minimal bounds could be established. This relies on + * the assumption that all Chrome-relevant traces are bounded by the browser + * process. + */ + get chromeBounds() { + if (!this.chromeBounds_) { + this.chromeBounds_ = new tr.b.math.Range(); + for (const browserHelper of Object.values(this.browserHelpers)) { + this.chromeBounds_.addRange(browserHelper.process.bounds); + } + + for (const rendererHelper of Object.values(this.rendererHelpers)) { + this.chromeBounds_.addRange(rendererHelper.process.bounds); + } + + if (this.gpuHelper) { + this.chromeBounds_.addRange(this.gpuHelper.process.bounds); + } + } + + if (this.chromeBounds_.isEmpty) { + return undefined; + } + + return this.chromeBounds_; + }, + + get telemetryHelper() { + return this.telemetryHelper_; + } + }; + + return { + ChromeModelHelper, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper_test.html new file mode 100644 index 00000000000..e347cd883ba --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_model_helper_test.html @@ -0,0 +1,238 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/chrome_test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/helpers/chrome_browser_helper.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newAsyncSliceEx = tr.c.TestUtils.newAsyncSliceEx; + + test('getLatencyData', function() { + const m = tr.e.chrome.ChromeTestUtils.newChromeModel(function(m) { + m.browserMain.asyncSliceGroup.push(newAsyncSliceEx({ + title: 'InputLatency::GestureScrollUpdate', + cat: 'benchmark', + start: 0, + end: 10, + id: '0x100', + isTopLevel: true, + args: { + data: { + INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT: {'time': 0}, + INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT: {time: 10} + } + } + })); + }); + + const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + const latencyEvents = modelHelper.browserHelper.getLatencyEventsInRange( + m.bounds); + assert.strictEqual(latencyEvents.length, 1); + }); + + test('getFrametime', function() { + let frameTs; + const events = []; + // Browser process 3507 + events.push({'cat': '__metadata', 'pid': 3507, 'tid': 3507, 'ts': 0, 'ph': 'M', 'name': 'thread_name', 'args': {'name': 'CrBrowserMain'}}); // @suppress longLineCheck + + // Renderer process 3508 + events.push({'cat': '__metadata', 'pid': 3508, 'tid': 3508, 'ts': 0, 'ph': 'M', 'name': 'thread_name', 'args': {'name': 'CrRendererMain'}}); // @suppress longLineCheck + // Compositor thread 3510 + events.push({'cat': '__metadata', 'pid': 3508, 'tid': 3510, 'ts': 0, 'ph': 'M', 'name': 'thread_name', 'args': {'name': 'Compositor'}}); // @suppress longLineCheck + + // Renderer process 3509 + events.push({'cat': '__metadata', 'pid': 3509, 'tid': 3509, 'ts': 0, 'ph': 'M', 'name': 'thread_name', 'args': {'name': 'CrRendererMain'}}); // @suppress longLineCheck + + // Compositor thread 3511 + events.push({'cat': '__metadata', 'pid': 3509, 'tid': 3511, 'ts': 0, 'ph': 'M', 'name': 'thread_name', 'args': {'name': 'Compositor'}}); // @suppress longLineCheck + + frameTs = 0; + // Add impl rendering stats for browser process 3507 + for (let i = 0; i < 10; i++) { + events.push({'cat': 'benchmark', 'pid': 3507, 'tid': 3507, 'ts': frameTs, 'ph': 'i', 'name': 'BenchmarkInstrumentation::ImplThreadRenderingStats', 's': 't'}); // @suppress longLineCheck + frameTs += 16000 + 1000 * (i % 2); + } + + frameTs = 0; + // Add main rendering stats for renderer process 3508 + for (let i = 0; i < 10; i++) { + events.push({'cat': 'benchmark', 'pid': 3508, 'tid': 3508, 'ts': frameTs, 'ph': 'i', 'name': 'BenchmarkInstrumentation::MainThreadRenderingStats', 's': 't'}); // @suppress longLineCheck + frameTs += 16000 + 1000 * (i % 2); + } + events.push({'cat': 'benchmark', 'pid': 3508, 'tid': 3510, 'ts': 1600, 'ph': 'i', 'name': 'KeepAlive', 's': 't'}); // @suppress longLineCheck + + frameTs = 0; + // Add impl and main rendering stats for renderer process 3509 + for (let i = 0; i < 10; i++) { + events.push({'cat': 'benchmark', 'pid': 3509, 'tid': 3511, 'ts': frameTs, 'ph': 'i', 'name': 'BenchmarkInstrumentation::ImplThreadRenderingStats', 's': 't'}); // @suppress longLineCheck + events.push({'cat': 'benchmark', 'pid': 3509, 'tid': 3509, 'ts': frameTs, 'ph': 'i', 'name': 'BenchmarkInstrumentation::MainThreadRenderingStats', 's': 't'}); // @suppress longLineCheck + frameTs += 16000 + 1000 * (i % 2); + } + + const m = tr.c.TestUtils.newModelWithEvents([events]); + const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + + // Testing browser impl and main rendering stats. + let frameEvents = modelHelper.browserHelper.getFrameEventsInRange( + tr.model.helpers.IMPL_FRAMETIME_TYPE, m.bounds); + let frametimeData = tr.model.helpers.getFrametimeDataFromEvents( + frameEvents); + assert.strictEqual(frametimeData.length, 9); + for (let i = 0; i < frametimeData.length; i++) { + assert.strictEqual(frametimeData[i].frametime, 16 + i % 2); + } + // No main rendering stats. + frameEvents = modelHelper.browserHelper.getFrameEventsInRange( + tr.model.helpers.MAIN_FRAMETIME_TYPE, m.bounds); + assert.strictEqual(frameEvents.length, 0); + + + // Testing renderer 3508 impl and main rendering stats. + frameEvents = modelHelper.rendererHelpers[3508].getFrameEventsInRange( + tr.model.helpers.MAIN_FRAMETIME_TYPE, m.bounds); + frametimeData = tr.model.helpers.getFrametimeDataFromEvents(frameEvents); + assert.strictEqual(frametimeData.length, 9); + for (let i = 0; i < frametimeData.length; i++) { + assert.strictEqual(frametimeData[i].frametime, 16 + i % 2); + } + + // No impl rendering stats. + frameEvents = modelHelper.rendererHelpers[3508].getFrameEventsInRange( + tr.model.helpers.IMPL_FRAMETIME_TYPE, m.bounds); + assert.strictEqual(frameEvents.length, 0); + + + // Testing renderer 3509 impl and main rendering stats. + frameEvents = modelHelper.rendererHelpers[3509].getFrameEventsInRange( + tr.model.helpers.IMPL_FRAMETIME_TYPE, m.bounds); + frametimeData = tr.model.helpers.getFrametimeDataFromEvents(frameEvents); + assert.strictEqual(frametimeData.length, 9); + for (let i = 0; i < frametimeData.length; i++) { + assert.strictEqual(frametimeData[i].frametime, 16 + i % 2); + } + + frameEvents = modelHelper.rendererHelpers[3509].getFrameEventsInRange( + tr.model.helpers.MAIN_FRAMETIME_TYPE, m.bounds); + frametimeData = tr.model.helpers.getFrametimeDataFromEvents(frameEvents); + assert.strictEqual(frametimeData.length, 9); + for (let i = 0; i < frametimeData.length; i++) { + assert.strictEqual(frametimeData[i].frametime, 16 + i % 2); + } + }); + + test('multipleBrowsers', function() { + const m = tr.c.TestUtils.newModel(function(model) { + const browserProcess1 = model.getOrCreateProcess(1); + browserProcess1.getOrCreateThread(2).name = 'CrBrowserMain'; + + const browserProcess2 = model.getOrCreateProcess(3); + browserProcess2.getOrCreateThread(4); + browserProcess2.getOrCreateThread(5).name = 'CrBrowserMain'; + + const nonBrowserProcess = model.getOrCreateProcess(6); + nonBrowserProcess.getOrCreateThread(7); + + const browserProcess3 = model.getOrCreateProcess(8); + browserProcess3.getOrCreateThread(9).name = 'CrBrowserMain'; + browserProcess3.getOrCreateThread(10); + }); + + const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + const browserHelpers = modelHelper.browserHelpers; + + // Check that the correct processes were marked as Chrome browser processes. + assert.sameMembers(browserHelpers.map(h => h.process.pid), [1, 3, 8]); + + // Check that the browser helpers have the correct structure. + browserHelpers.forEach(function(helper) { + assert.instanceOf(helper, tr.model.helpers.ChromeBrowserHelper); + assert.strictEqual(helper.modelHelper, modelHelper); + }); + }); + + test('chromeBounds_considersAllChromeProcesses', function() { + const model1 = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + model.browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: 100, + })); + model.rendererMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 200, + duration: 100, + })); + }); + + const model2 = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + const gpuProcess = model.getOrCreateProcess(42); + const gpuMainThread = gpuProcess.getOrCreateThread(1); + gpuMainThread.name = 'CrGpuMain'; + gpuMainThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: 50, + })); + model.browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 100, + duration: 50, + })); + }); + + const modelHelper1 = + model1.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + assert.strictEqual(modelHelper1.chromeBounds.min, 0); + assert.strictEqual(modelHelper1.chromeBounds.max, 300); + + const modelHelper2 = + model2.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + assert.strictEqual(modelHelper2.chromeBounds.min, 0); + assert.strictEqual(modelHelper2.chromeBounds.max, 150); + }); + + test('chromeBounds_onlyConsidersChromeProcesses', function() { + const model = tr.e.chrome.ChromeTestUtils.newChromeModel(function(model) { + model.browserMain.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 0, + duration: 100, + })); + + // The bounds of this process should not be included in chrome bounds. + const nonChromeProcess = model.getOrCreateProcess(1234); + nonChromeProcess.name = 'Telemetry'; + const nonChromeThread = nonChromeProcess.getOrCreateThread(1); + nonChromeThread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: 200, + duration: 100, + })); + }); + + const modelHelper = + model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + assert.strictEqual(modelHelper.chromeBounds.min, 0); + assert.strictEqual(modelHelper.chromeBounds.max, 100); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_process_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_process_helper.html new file mode 100644 index 00000000000..208a4131c34 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_process_helper.html @@ -0,0 +1,120 @@ +<!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'; + +/** + * @fileoverview Utilities for accessing trace data about the Chrome browser. + */ +tr.exportTo('tr.model.helpers', function() { + const MAIN_FRAMETIME_TYPE = 'main_frametime_type'; + const IMPL_FRAMETIME_TYPE = 'impl_frametime_type'; + + const MAIN_RENDERING_STATS = + 'BenchmarkInstrumentation::MainThreadRenderingStats'; + const IMPL_RENDERING_STATS = + 'BenchmarkInstrumentation::ImplThreadRenderingStats'; + + + function getSlicesIntersectingRange(rangeOfInterest, slices) { + const slicesInFilterRange = []; + for (let i = 0; i < slices.length; i++) { + const slice = slices[i]; + if (rangeOfInterest.intersectsExplicitRangeInclusive( + slice.start, slice.end)) { + slicesInFilterRange.push(slice); + } + } + return slicesInFilterRange; + } + + + function ChromeProcessHelper(modelHelper, process) { + this.modelHelper = modelHelper; + this.process = process; + this.telemetryInternalRanges_ = undefined; + } + + ChromeProcessHelper.prototype = { + get pid() { + return this.process.pid; + }, + + isTelemetryInternalEvent(slice) { + if (this.telemetryInternalRanges_ === undefined) { + this.findTelemetryInternalRanges_(); + } + for (const range of this.telemetryInternalRanges_) { + if (range.containsExplicitRangeInclusive(slice.start, slice.end)) { + return true; + } + } + return false; + }, + + findTelemetryInternalRanges_() { + this.telemetryInternalRanges_ = []; + let start = 0; + for (const thread of Object.values(this.process.threads)) { + for (const slice of thread.asyncSliceGroup.getDescendantEvents()) { + if (/^telemetry\.internal\..*\.start$/.test(slice.title)) { + start = slice.start; + } else if (/^telemetry\.internal\..*\.end$/.test(slice.title) && + start !== undefined) { + this.telemetryInternalRanges_.push( + tr.b.math.Range.fromExplicitRange(start, slice.end)); + start = undefined; + } + } + } + }, + + getFrameEventsInRange(frametimeType, range) { + const titleToGet = (frametimeType === MAIN_FRAMETIME_TYPE ? + MAIN_RENDERING_STATS : IMPL_RENDERING_STATS); + + const frameEvents = []; + for (const event of this.process.getDescendantEvents()) { + if (event.title === titleToGet) { + if (range.intersectsExplicitRangeInclusive(event.start, event.end)) { + frameEvents.push(event); + } + } + } + + frameEvents.sort(function(a, b) {return a.start - b.start;}); + return frameEvents; + } + }; + + function getFrametimeDataFromEvents(frameEvents) { + const frametimeData = []; + for (let i = 1; i < frameEvents.length; i++) { + const diff = frameEvents[i].start - frameEvents[i - 1].start; + frametimeData.push({ + 'x': frameEvents[i].start, + 'frametime': diff + }); + } + return frametimeData; + } + + return { + ChromeProcessHelper, + + MAIN_FRAMETIME_TYPE, + IMPL_FRAMETIME_TYPE, + MAIN_RENDERING_STATS, + IMPL_RENDERING_STATS, + + getSlicesIntersectingRange, + getFrametimeDataFromEvents, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper.html new file mode 100644 index 00000000000..a10e7eaff01 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper.html @@ -0,0 +1,76 @@ +<!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/math/range.html"> +<link rel="import" href="/tracing/model/helpers/chrome_process_helper.html"> +<link rel="import" href="/tracing/model/helpers/chrome_thread_helper.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.helpers', function() { + const ChromeThreadHelper = tr.model.helpers.ChromeThreadHelper; + + function ChromeRendererHelper(modelHelper, process) { + tr.model.helpers.ChromeProcessHelper.call(this, modelHelper, process); + this.mainThread_ = process.findAtMostOneThreadNamed('CrRendererMain') || + process.findAtMostOneThreadNamed('Chrome_InProcRendererThread'); + this.compositorThread_ = process.findAtMostOneThreadNamed('Compositor'); + this.rasterWorkerThreads_ = process.findAllThreadsMatching(function(t) { + if (t.name === undefined) return false; + if (t.name.indexOf('CompositorTileWorker') === 0) return true; + if (t.name.indexOf('CompositorRasterWorker') === 0) return true; + return false; + }); + + if (!process.name) { + process.name = ChromeRendererHelper.PROCESS_NAME; + } + } + + ChromeRendererHelper.PROCESS_NAME = 'Renderer'; + + // Returns true if there is either a main thread or a compositor thread. + ChromeRendererHelper.isRenderProcess = function(process) { + if (process.findAtMostOneThreadNamed('CrRendererMain')) return true; + if (process.findAtMostOneThreadNamed('Compositor')) return true; + return false; + }; + + ChromeRendererHelper.isTracingProcess = function(process) { + return process.labels !== undefined && + process.labels.length === 1 && + process.labels[0] === 'chrome://tracing'; + }; + + ChromeRendererHelper.prototype = { + __proto__: tr.model.helpers.ChromeProcessHelper.prototype, + + // May be undefined. + get mainThread() { + return this.mainThread_; + }, + + // May be undefined. + get compositorThread() { + return this.compositorThread_; + }, + + // May be empty. + get rasterWorkerThreads() { + return this.rasterWorkerThreads_; + }, + + get isChromeTracingUI() { + return ChromeRendererHelper.isTracingProcess(this.process); + }, + }; + + return { + ChromeRendererHelper, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper_test.html new file mode 100644 index 00000000000..8ca62f70d25 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_renderer_helper_test.html @@ -0,0 +1,16 @@ +<!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/model/helpers/chrome_model_helper.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper.html new file mode 100644 index 00000000000..39d6159ec60 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/base/base.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.helpers', function() { + const NET_CATEGORIES = new Set(['net', 'netlog', + 'disabled-by-default-netlog', 'disabled-by-default-network']); + + class ChromeThreadHelper { + constructor(thread) { + this.thread = thread; + } + + getNetworkEvents() { + const networkEvents = []; + for (const slice of this.thread.asyncSliceGroup.slices) { + const categories = tr.b.getCategoryParts(slice.category); + const isNetEvent = category => NET_CATEGORIES.has(category); + if (categories.filter(isNetEvent).length === 0) continue; + networkEvents.push(slice); + } + return networkEvents; + } + } + + return { + ChromeThreadHelper, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper_test.html new file mode 100644 index 00000000000..49dbdd63938 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/chrome_thread_helper_test.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/helpers/chrome_thread_helper.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Process = tr.model.Process; + const Thread = tr.model.Thread; + const ChromeThreadHelper = tr.model.helpers.ChromeThreadHelper; + + test('getNetworkEvents_empty', function() { + const model = new tr.Model(); + const t = new Thread(new tr.model.Process(model, 7), 1); + const threadHelper = new ChromeThreadHelper(t); + assert.sameDeepMembers([], + threadHelper.getNetworkEvents()); + }); + + test('getNetworkEvents_withIrrelevantEvents', function() { + const model = new tr.Model(); + const t = new Thread(new Process(model, 7), 1); + const netEvent1 = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'netlog', + title: 'Generic Network event', + start: 100, + duration: 200, + }); + t.asyncSliceGroup.push(netEvent1); + const netEvent2 = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'net', + title: 'Generic Network event', + start: 101, + duration: 201, + }); + t.asyncSliceGroup.push(netEvent2); + t.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceEx({ + cat: 'irrelevant async event', + title: 'irrelevant async event', + start: 100, + duration: 200, + })); + t.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + cat: 'irrelevant sync event', + title: 'irrelevant sync event', + start: 100, + duration: 200, + })); + const threadHelper = new ChromeThreadHelper(t); + assert.sameDeepMembers([netEvent1, netEvent2], + threadHelper.getNetworkEvents()); + }); + + test('getNetworkEvents_allTypes', function() { + const model = new tr.Model(); + const t = new Thread(new Process(model, 7), 1); + const netEvent1 = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'netlog', + title: 'Generic Network event', + start: 100, + duration: 200, + }); + t.asyncSliceGroup.push(netEvent1); + const netEvent2 = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'net', + title: 'Generic Network event', + start: 101, + duration: 201, + }); + t.asyncSliceGroup.push(netEvent2); + const netEvent3 = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'disabled-by-default-netlog', + title: 'Generic Network event', + start: 101, + duration: 201, + }); + t.asyncSliceGroup.push(netEvent3); + const netEvent4 = tr.c.TestUtils.newAsyncSliceEx({ + cat: 'disabled-by-default-network', + title: 'Generic Network event', + start: 101, + duration: 201, + }); + t.asyncSliceGroup.push(netEvent4); + const threadHelper = new ChromeThreadHelper(t); + assert.sameDeepMembers([netEvent1, netEvent2, netEvent3, netEvent4], + threadHelper.getNetworkEvents()); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper.html new file mode 100644 index 00000000000..0c4486f8b54 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/model/helpers/chrome_renderer_helper.html"> +<link rel="import" href="/tracing/model/user_model/segment.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.helpers', function() { + const GESTURE_EVENT = 'SyntheticGestureController::running'; + const IR_REG_EXP = /Interaction\.([^/]+)(\/[^/]*)?$/; + const ChromeRendererHelper = tr.model.helpers.ChromeRendererHelper; + + class TelemetryHelper { + constructor(modelHelper) { + this.modelHelper = modelHelper; + + this.renderersWithIR_ = undefined; + this.segments_ = undefined; + this.uiSegments_ = undefined; + } + + get renderersWithIR() { + this.findIRs_(); + return this.renderersWithIR_; + } + + get segments() { + this.findIRs_(); + return this.segments_; + } + + get uiSegments() { + this.findIRs_(); + return this.uiSegments_; + } + + /** + * Finds interesting segments we want to compute metrics in. We use trace + * events produced by Telemetry. One drawback of this method is that we + * cannot compute the metric from Chrome traces that are not produced by + * Telemetry. Alternatively, we could use the user model to detect + * interesting segments, like animation segments in the following way: + * + * const animationSegments = model.userModel.segments.filter( + * segment => segment.expectations.find( + * ue => ue instanceof tr.model.um.AnimationExpectation)); + * + * However, the user model cannot detect all types of animations, yet. For + * more discussion about using test generated interaction records vs the + * user model please refer to http://bit.ly/ir-tbmv2. TODO(chiniforooshan): + * Improve the user model detection of animations. + * + * Also, some of the metrics we compute here are not animations specific. + */ + findIRs_() { + if (this.segments_ !== undefined) return; + + this.renderersWithIR_ = []; + const gestureEvents = []; + const interactionRecords = []; + const processes = Object.values(this.modelHelper.rendererHelpers) + .concat(this.modelHelper.browserHelpers) + .map(processHelper => processHelper.process); + for (const process of processes) { + let foundIR = false; + for (const thread of Object.values(process.threads)) { + for (const slice of thread.asyncSliceGroup.slices) { + if (slice.title === GESTURE_EVENT) { + gestureEvents.push(slice); + } else if (IR_REG_EXP.test(slice.title)) { + interactionRecords.push(slice); + foundIR = true; + } + } + } + if (foundIR && ChromeRendererHelper.isRenderProcess(process) && + !ChromeRendererHelper.isTracingProcess(process)) { + this.renderersWithIR_.push( + new ChromeRendererHelper(this.modelHelper, process)); + } + } + + // Convert interaction record slices to segments. If an interaction record + // contains a gesture whose time range overlaps with the interaction + // record's range, use the gesture's time range. + // + // The synthetic gesture controller inserts a trace marker to precisely + // demarcate when the gesture was running. We check for overlap, not + // inclusion, because gesture actions can start/end slightly outside the + // telemetry markers on Windows. + this.segments_ = []; + this.uiSegments_ = []; + for (const ir of interactionRecords) { + const parts = IR_REG_EXP.exec(ir.title); + let gestureEventFound = false; + if (parts[1].startsWith('Gesture_')) { + for (const gestureEvent of gestureEvents) { + if (ir.boundsRange.intersectsRangeInclusive( + gestureEvent.boundsRange)) { + this.segments_.push(new tr.model.um.Segment( + gestureEvent.start, gestureEvent.duration)); + gestureEventFound = true; + break; + } + } + } else if (parts[1].startsWith('ui_')) { + this.uiSegments_.push(new tr.model.um.Segment(ir.start, ir.duration)); + } + if (!gestureEventFound) { + this.segments_.push(new tr.model.um.Segment(ir.start, ir.duration)); + } + } + + this.segments_.sort((x, y) => x.start - y.start); + this.uiSegments_.sort((x, y) => x.start - y.start); + } + } + + return { + TelemetryHelper, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper_test.html b/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper_test.html new file mode 100644 index 00000000000..1434603886e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/helpers/telemetry_helper_test.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/helpers/chrome_renderer_helper.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('renderersWithIR', function() { + const m = tr.c.TestUtils.newModel((m) => { + m.getOrCreateProcess(0).getOrCreateThread(0).name = 'CrBrowserMain'; + + // There is no IR in this renderer process. + const r1 = m.getOrCreateProcess(1).getOrCreateThread(1); + r1.name = 'CrRendererMain'; + r1.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('AnAsyncSlice', 1, 1)); + + // This is the renderer created by Telemetry. + const r2 = m.getOrCreateProcess(2).getOrCreateThread(2); + r2.name = 'CrRendererMain'; + r2.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1)); + + // This is the Telemetry process, not a real renderer process. + const r3 = m.getOrCreateProcess(3).getOrCreateThread(3); + r3.name = 'CrRendererMain'; + r3.parent.labels = ['chrome://tracing']; + r3.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.Action', 1, 1)); + }); + const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + const renderers = modelHelper.telemetryHelper.renderersWithIR; + assert.strictEqual(1, renderers.length); + assert.strictEqual(2, renderers[0].process.pid); + }); + + test('segments', function() { + const m = tr.c.TestUtils.newModel((m) => { + // There is no IR in this renderer process. + const r = m.getOrCreateProcess(1).getOrCreateThread(1); + r.name = 'CrRendererMain'; + r.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceNamed( + 'SyntheticGestureController::running', 6, 2)); + r.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.A', 1, 1)); + r.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.Gesture_C', 5, 2)); + r.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.ui_B', 3, 1)); + }); + + // Async slices are: + // + // 1 2 3 4 5 6 7 8 + // Interactions <-- A --> <-- B --> <------ C ------> + // Gestures <---------------> + // + // Segments should be: [1, 2], [3, 4], and [6, 8]. + const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + const segments = modelHelper.telemetryHelper.segments; + assert.strictEqual(3, segments.length); + assert.deepEqual([1, 2], [segments[0].start, segments[0].end]); + assert.deepEqual([3, 4], [segments[1].start, segments[1].end]); + assert.deepEqual([6, 8], [segments[2].start, segments[2].end]); + }); + + test('uiSegments', function() { + const m = tr.c.TestUtils.newModel((m) => { + // There is no IR in this renderer process. + const r = m.getOrCreateProcess(1).getOrCreateThread(1); + r.name = 'CrRendererMain'; + r.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSliceNamed( + 'SyntheticGestureController::running', 6, 2)); + r.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.A', 1, 1)); + r.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.Gesture_C', 5, 2)); + r.asyncSliceGroup.push( + tr.c.TestUtils.newAsyncSliceNamed('Interaction.ui_B', 3, 1)); + }); + + // Async slices are: + // + // 1 2 3 4 5 6 7 8 + // Interactions <-- A --> <-- B --> <------ C ------> + // Gestures <---------------> + // + // The only UI segment is [3, 4]. + const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + const uiSegments = modelHelper.telemetryHelper.uiSegments; + assert.strictEqual(1, uiSegments.length); + assert.deepEqual([3, 4], [uiSegments[0].start, uiSegments[0].end]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/instant_event.html b/chromium/third_party/catapult/tracing/tracing/model/instant_event.html new file mode 100644 index 00000000000..d2f6cfbfd0b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/instant_event.html @@ -0,0 +1,100 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/model/timed_event.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const InstantEventType = { + GLOBAL: 1, + PROCESS: 2 + }; + + /** + * An InstantEvent is a zero-duration event. + * + * @constructor + */ + function InstantEvent(category, title, colorId, start, args) { + tr.model.TimedEvent.call(this, start); + + this.category = category || ''; + this.title = title; + this.colorId = colorId; + this.args = args; + + this.type = undefined; + } + + InstantEvent.prototype = { + __proto__: tr.model.TimedEvent.prototype + }; + + /** + * A GlobalInstantEvent is a zero-duration event that's not tied to any + * particular process. + * + * An example is a trace event that's issued when a new USB device is plugged + * into the machine. + * + * @constructor + */ + function GlobalInstantEvent(category, title, colorId, start, args) { + InstantEvent.apply(this, arguments); + this.type = InstantEventType.GLOBAL; + } + + GlobalInstantEvent.prototype = { + __proto__: InstantEvent.prototype, + get userFriendlyName() { + return 'Global instant event ' + this.title + ' @ ' + + tr.b.Unit.byName.timeStampInMs.format(start); + } + }; + + /** + * A ProcessInstantEvent is a zero-duration event that's tied to a + * particular process. + * + * An example is a trace event that's issued when a kill signal is received. + * + * @constructor + */ + function ProcessInstantEvent(category, title, colorId, start, args) { + InstantEvent.apply(this, arguments); + this.type = InstantEventType.PROCESS; + } + + ProcessInstantEvent.prototype = { + __proto__: InstantEvent.prototype, + + get userFriendlyName() { + return 'Process-level instant event ' + this.title + ' @ ' + + tr.b.Unit.byName.timeStampInMs.format(start); + } + }; + + tr.model.EventRegistry.register( + InstantEvent, + { + name: 'instantEvent', + pluralName: 'instantEvents' + }); + + return { + GlobalInstantEvent, + ProcessInstantEvent, + + InstantEventType, + InstantEvent, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/interaction_record_test.html b/chromium/third_party/catapult/tracing/tracing/model/interaction_record_test.html new file mode 100644 index 00000000000..8380bcb0849 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/interaction_record_test.html @@ -0,0 +1,115 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/user_model/stub_expectation.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TestUtils = tr.c.TestUtils; + const CompoundEventSelectionState = tr.model.CompoundEventSelectionState; + + function createModel(opt_customizeModelCallback) { + return TestUtils.newModel(function(model) { + model.p1 = model.getOrCreateProcess(1); + model.t2 = model.p1.getOrCreateThread(2); + + model.s1 = model.t2.sliceGroup.pushSlice(TestUtils.newSliceEx({ + title: 'a', start: 10, end: 20 + })); + model.s2 = model.t2.sliceGroup.pushSlice(TestUtils.newSliceEx({ + title: 'b', start: 20, end: 30 + })); + + model.ir1 = new tr.model.um.StubExpectation({ + parentModel: model, + start: 100, end: 200, + typeName: 'Response', + normalizedEfficiency: 1., + normalizedUserComfort: 0.0 + }); + model.userModel.expectations.push(model.ir1); + model.ir1.associatedEvents.push(model.s1); + model.ir1.associatedEvents.push(model.s2); + + if (opt_customizeModelCallback) { + opt_customizeModelCallback(model); + } + }); + } + test('notSelected', function() { + const model = createModel(); + + const sel = new tr.model.EventSet(); + + assert.strictEqual(CompoundEventSelectionState.NOT_SELECTED, + model.ir1.computeCompoundEvenSelectionState(sel)); + }); + + test('directSelected', function() { + const model = createModel(); + + const sel = new tr.model.EventSet(); + sel.push(model.ir1); + + assert.strictEqual(CompoundEventSelectionState.EVENT_SELECTED, + model.ir1.computeCompoundEvenSelectionState(sel)); + }); + + test('directAndSomeAssociatedSelected', function() { + const model = createModel(); + + const sel = new tr.model.EventSet(); + sel.push(model.ir1); + sel.push(model.s1); + + assert.strictEqual( + CompoundEventSelectionState.EVENT_AND_SOME_ASSOCIATED_SELECTED, + model.ir1.computeCompoundEvenSelectionState(sel)); + }); + + test('allAssociatedEventsSelected', function() { + const model = createModel(); + + const sel = new tr.model.EventSet(); + sel.push(model.s1); + sel.push(model.s2); + + assert.strictEqual( + CompoundEventSelectionState.ALL_ASSOCIATED_EVENTS_SELECTED, + model.ir1.computeCompoundEvenSelectionState(sel)); + }); + + test('directAndAllAssociated', function() { + const model = createModel(); + + const sel = new tr.model.EventSet(); + sel.push(model.ir1); + sel.push(model.s1); + sel.push(model.s2); + + assert.strictEqual( + CompoundEventSelectionState.EVENT_AND_ALL_ASSOCIATED_SELECTED, + model.ir1.computeCompoundEvenSelectionState(sel)); + }); + + test('stableId', function() { + const model = TestUtils.newModel(); + + const ir1 = TestUtils.newInteractionRecord(model, 0, 10); + const ir2 = TestUtils.newInteractionRecord(model, 10, 10); + const ir3 = TestUtils.newInteractionRecord(model, 20, 10); + + assert.strictEqual('UserExpectation.' + ir1.guid, ir1.stableId); + assert.strictEqual('UserExpectation.' + ir2.guid, ir2.stableId); + assert.strictEqual('UserExpectation.' + ir3.guid, ir3.stableId); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/ir_coverage.html b/chromium/third_party/catapult/tracing/tracing/model/ir_coverage.html new file mode 100644 index 00000000000..2b8264ac276 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/ir_coverage.html @@ -0,0 +1,91 @@ +<!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/utils.html"> +<link rel="import" href="/tracing/model/event_set.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + function getAssociatedEvents(irs) { + const allAssociatedEvents = new tr.model.EventSet(); + irs.forEach(function(ir) { + ir.associatedEvents.forEach(function(event) { + // FlowEvents don't have parentContainers or cpuDurations, and it's + // annoying to highlight them. + if (event instanceof tr.model.FlowEvent) return; + allAssociatedEvents.push(event); + }); + }); + return allAssociatedEvents; + } + + function getUnassociatedEvents(model, associatedEvents) { + const unassociatedEvents = new tr.model.EventSet(); + // The set of unassociated events contains only events that are not in + // the set of associated events. + // Only add event to the set of unassociated events if it is not in + // the set of associated events. + for (const proc of model.getAllProcesses()) { + for (const thread of Object.values(proc.threads)) { + for (const event of thread.sliceGroup.getDescendantEvents()) { + if (!associatedEvents.contains(event)) { + unassociatedEvents.push(event); + } + } + } + } + return unassociatedEvents; + } + + function getTotalCpuDuration(events) { + let cpuMs = 0; + events.forEach(function(event) { + // Add up events' cpu self time if they have any. + if (event.cpuSelfTime) { + cpuMs += event.cpuSelfTime; + } + }); + return cpuMs; + } + + function getIRCoverageFromModel(model) { + const associatedEvents = getAssociatedEvents(model.userModel.expectations); + + if (!associatedEvents.length) return undefined; + + const unassociatedEvents = getUnassociatedEvents( + model, associatedEvents); + + const associatedCpuMs = getTotalCpuDuration(associatedEvents); + const unassociatedCpuMs = getTotalCpuDuration(unassociatedEvents); + + const totalEventCount = associatedEvents.length + unassociatedEvents.length; + const totalCpuMs = associatedCpuMs + unassociatedCpuMs; + let coveredEventsCpuTimeRatio = undefined; + if (totalCpuMs !== 0) { + coveredEventsCpuTimeRatio = associatedCpuMs / totalCpuMs; + } + + return { + associatedEventsCount: associatedEvents.length, + unassociatedEventsCount: unassociatedEvents.length, + associatedEventsCpuTimeMs: associatedCpuMs, + unassociatedEventsCpuTimeMs: unassociatedCpuMs, + coveredEventsCountRatio: associatedEvents.length / totalEventCount, + coveredEventsCpuTimeRatio + }; + } + + return { + getIRCoverageFromModel, + getAssociatedEvents, + getUnassociatedEvents, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/ir_coverage_test.html b/chromium/third_party/catapult/tracing/tracing/model/ir_coverage_test.html new file mode 100644 index 00000000000..4a3b471ae6f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/ir_coverage_test.html @@ -0,0 +1,90 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/ir_coverage.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + + function createModel() { + return tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + const thread = process.getOrCreateThread(2); + const s0 = thread.sliceGroup.pushSlice(newSliceEx( + {title: 's0', start: 0.0, duration: 1.0})); + s0.isTopLevel = true; + const unassociatedEvent = thread.sliceGroup.pushSlice(newSliceEx( + {title: 's1', start: 6.0, duration: 1.0})); + unassociatedEvent.isTopLevel = true; + const s2 = thread.sliceGroup.pushSlice(newSliceEx( + {title: 's2', start: 2.0, duration: 1.0})); + s2.isTopLevel = true; + const f0 = tr.c.TestUtils.newFlowEventEx({ + title: 'test1', + start: 0, + end: 10, + startSlice: s0, + endSlice: s2, + id: '0x100' + }); + model.flowEvents.push(f0); + const as1 = tr.c.TestUtils.newAsyncSliceEx({ + title: 'InputLatency::GestureTap', + cat: 'benchmark,latencyInfo', + start: 2, + end: 10, + id: '0x100', + isTopLevel: true, + startThread: thread + }); + thread.asyncSliceGroup.push(as1); + const ir = new tr.model.um.StubExpectation( + {parentModel: model, start: 0, duration: 7}); + ir.associatedEvents.push(as1); + ir.associatedEvents.push(s0); + ir.associatedEvents.push(s2); + ir.associatedEvents.push(f0); + model.userModel.expectations.push(ir); + }); + } + + test('computeCoverage', function() { + const model = createModel(); + for (const event of model.getDescendantEvents()) { + if (event.title === 's0' || event.title === 's2') { + event.cpuSelfTime = 0.4; + } else if (event.title === 's1') { + event.cpuSelfTime = 0.8; + } + } + + const coverage = tr.model.getIRCoverageFromModel(model); + assert.strictEqual(3, coverage.associatedEventsCount); + assert.strictEqual(1, coverage.unassociatedEventsCount); + assert.closeTo(0.75, coverage.coveredEventsCountRatio, 1e-3); + assert.closeTo(0.8, coverage.associatedEventsCpuTimeMs, 1e-3); + assert.closeTo(0.8, coverage.unassociatedEventsCpuTimeMs, 1e-3); + assert.closeTo(0.5, coverage.coveredEventsCpuTimeRatio, 1e-3); + }); + + test('zeroCPU', function() { + const model = createModel(); + const coverage = tr.model.getIRCoverageFromModel(model); + assert.strictEqual(3, coverage.associatedEventsCount); + assert.strictEqual(1, coverage.unassociatedEventsCount); + assert.closeTo(0.75, coverage.coveredEventsCountRatio, 1e-3); + assert.closeTo(0.0, coverage.associatedEventsCpuTimeMs, 1e-3); + assert.closeTo(0.0, coverage.unassociatedEventsCpuTimeMs, 1e-3); + assert.strictEqual(undefined, coverage.coveredEventsCpuTimeRatio, 1e-3); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/kernel.html b/chromium/third_party/catapult/tracing/tracing/model/kernel.html new file mode 100644 index 00000000000..834a91eea06 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/kernel.html @@ -0,0 +1,137 @@ +<!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/utils.html"> +<link rel="import" href="/tracing/model/cpu.html"> +<link rel="import" href="/tracing/model/process_base.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Process class. + */ +tr.exportTo('tr.model', function() { + const Cpu = tr.model.Cpu; + const ProcessBase = tr.model.ProcessBase; + + /** + * The Kernel represents kernel-level objects in the model. + * @constructor + */ + function Kernel(model) { + ProcessBase.call(this, model); + + this.cpus = {}; + this.softwareMeasuredCpuCount_ = undefined; + } + + /** + * Comparison between kernels is pretty meaningless. + */ + Kernel.compare = function(x, y) { + return 0; + }; + + Kernel.prototype = { + __proto__: ProcessBase.prototype, + + compareTo(that) { + return Kernel.compare(this, that); + }, + + get userFriendlyName() { + return 'Kernel'; + }, + + get userFriendlyDetails() { + return 'Kernel'; + }, + + get stableId() { + return 'Kernel'; + }, + + /** + * @return {Cpu} Gets a specific Cpu or creates one if + * it does not exist. + */ + getOrCreateCpu(cpuNumber) { + if (!this.cpus[cpuNumber]) { + this.cpus[cpuNumber] = new Cpu(this, cpuNumber); + } + return this.cpus[cpuNumber]; + }, + + get softwareMeasuredCpuCount() { + return this.softwareMeasuredCpuCount_; + }, + + set softwareMeasuredCpuCount(softwareMeasuredCpuCount) { + if (this.softwareMeasuredCpuCount_ !== undefined && + this.softwareMeasuredCpuCount_ !== softwareMeasuredCpuCount) { + throw new Error( + 'Cannot change the softwareMeasuredCpuCount once it is set'); + } + + this.softwareMeasuredCpuCount_ = softwareMeasuredCpuCount; + }, + + /** + * Estimates how many cpus are in the system, for use in system load + * estimation. + * + * If kernel trace was provided, uses that data. Otherwise, uses the + * software measured cpu count. + */ + get bestGuessAtCpuCount() { + const realCpuCount = Object.keys(this.cpus).length; + if (realCpuCount !== 0) { + return realCpuCount; + } + return this.softwareMeasuredCpuCount; + }, + + updateBounds() { + ProcessBase.prototype.updateBounds.call(this); + for (const cpuNumber in this.cpus) { + const cpu = this.cpus[cpuNumber]; + cpu.updateBounds(); + this.bounds.addRange(cpu.bounds); + } + }, + + createSubSlices() { + ProcessBase.prototype.createSubSlices.call(this); + for (const cpuNumber in this.cpus) { + const cpu = this.cpus[cpuNumber]; + cpu.createSubSlices(); + } + }, + + addCategoriesToDict(categoriesDict) { + ProcessBase.prototype.addCategoriesToDict.call(this, categoriesDict); + for (const cpuNumber in this.cpus) { + this.cpus[cpuNumber].addCategoriesToDict(categoriesDict); + } + }, + + getSettingsKey() { + return 'kernel'; + }, + + * childEventContainers() { + yield* ProcessBase.prototype.childEventContainers.call(this); + yield* Object.values(this.cpus); + }, + }; + + return { + Kernel, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/kernel_test.html b/chromium/third_party/catapult/tracing/tracing/model/kernel_test.html new file mode 100644 index 00000000000..ffe145adef6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/kernel_test.html @@ -0,0 +1,73 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/kernel.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function newModel(events, callback) { + return tr.c.TestUtils.newModelWithEvents([events], { + shiftWorldToZero: false, + pruneContainers: false, + customizeModelCallback: callback + }); + } + + test('bestGuessAtCpuCountWithNoData', function() { + const m = newModel([]); + assert.isUndefined(m.kernel.bestGuessAtCpuCount); + }); + + test('bestGuessAtCpuCountWithCpuData', function() { + const m = newModel([], function(m) { + const c1 = m.kernel.getOrCreateCpu(1); + const c2 = m.kernel.getOrCreateCpu(2); + }); + assert.strictEqual(m.kernel.bestGuessAtCpuCount, 2); + }); + + test('bestGuessAtCpuCountWithSoftwareCpuCount', function() { + const m = newModel([], function(m) { + m.kernel.softwareMeasuredCpuCount = 2; + }); + assert.strictEqual(m.kernel.bestGuessAtCpuCount, 2); + }); + + test('kernelStableId', function() { + const model = newModel([]); + assert.strictEqual(model.kernel.stableId, 'Kernel'); + }); + + test('kernelTimeShifting', function() { + const m = newModel([]); + const ctr1 = m.kernel.getOrCreateCounter('cat', 'ctr1'); + const series1 = new tr.model.CounterSeries('a', 0); + series1.addCounterSample(100, 5); + series1.addCounterSample(200, 5); + ctr1.addSeries(series1); + + const ctr2 = m.kernel.getOrCreateCpu(1).getOrCreateCounter('cat', 'ctr2'); + const series2 = new tr.model.CounterSeries('b', 0); + series2.addCounterSample(300, 5); + series2.addCounterSample(400, 5); + ctr2.addSeries(series2); + + m.kernel.shiftTimestampsForward(2); + + assert.strictEqual(ctr1.series[0].samples[0].timestamp, 102); + assert.strictEqual(ctr1.series[0].samples[1].timestamp, 202); + + assert.strictEqual(ctr2.series[0].samples[0].timestamp, 302); + assert.strictEqual(ctr2.series[0].samples[1].timestamp, 402); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/location.html b/chromium/third_party/catapult/tracing/tracing/model/location.html new file mode 100644 index 00000000000..b6403ae4c3f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/location.html @@ -0,0 +1,158 @@ +<!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"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + /** + * YComponent is a class that handles storing the stableId and the percentage + * offset in the y direction of all tracks within a specific viewX and viewY + * coordinate. + * @constructor + */ + function YComponent(stableId, yPercentOffset) { + this.stableId = stableId; + this.yPercentOffset = yPercentOffset; + } + + YComponent.prototype = { + toDict() { + return { + stableId: this.stableId, + yPercentOffset: this.yPercentOffset + }; + } + }; + + /** + * Location is a class that represents a spatial location on the timeline + * that is specified by percent offsets within tracks rather than specific + * points. + * + * @constructor + */ + function Location(xWorld, yComponents) { + this.xWorld_ = xWorld; + this.yComponents_ = yComponents; + } + + /** + * Returns a new Location given by x and y coordinates with respect to + * the timeline's drawing canvas. + */ + Location.fromViewCoordinates = function(viewport, viewX, viewY) { + const dt = viewport.currentDisplayTransform; + const xWorld = dt.xViewToWorld(viewX); + const yComponents = []; + + // Since we're given coordinates within the timeline canvas, we need to + // convert them to document coordinates to get the element. + let elem = document.elementFromPoint( + viewX + viewport.modelTrackContainer.canvas.offsetLeft, + viewY + viewport.modelTrackContainer.canvas.offsetTop); + // Build yComponents by calculating percentage offset with respect to + // each parent track. + while (elem instanceof tr.ui.tracks.Track) { + if (elem.eventContainer) { + const boundRect = elem.getBoundingClientRect(); + const yPercentOffset = (viewY - boundRect.top) / boundRect.height; + yComponents.push( + new YComponent(elem.eventContainer.stableId, yPercentOffset)); + } + elem = elem.parentElement; + } + + if (yComponents.length === 0) return; + return new Location(xWorld, yComponents); + }; + + Location.fromStableIdAndTimestamp = function(viewport, stableId, ts) { + const xWorld = ts; + const yComponents = []; + + // The y components' percentage offsets will be calculated with respect to + // the boundingRect's top of containing track. + const containerToTrack = viewport.containerToTrackMap; + let elem = containerToTrack.getTrackByStableId(stableId); + if (!elem) return; + + const firstY = elem.getBoundingClientRect().top; + while (elem instanceof tr.ui.tracks.Track) { + if (elem.eventContainer) { + const boundRect = elem.getBoundingClientRect(); + const yPercentOffset = (firstY - boundRect.top) / boundRect.height; + yComponents.push( + new YComponent(elem.eventContainer.stableId, yPercentOffset)); + } + elem = elem.parentElement; + } + + if (yComponents.length === 0) return; + return new Location(xWorld, yComponents); + }; + + Location.prototype = { + + get xWorld() { + return this.xWorld_; + }, + + /** + * Returns the first valid containing track based on the + * internal yComponents. + */ + getContainingTrack(viewport) { + const containerToTrack = viewport.containerToTrackMap; + for (const i in this.yComponents_) { + const yComponent = this.yComponents_[i]; + const track = containerToTrack.getTrackByStableId(yComponent.stableId); + if (track !== undefined) return track; + } + }, + + /** + * Calculates and returns x and y coordinates of the current location with + * respect to the timeline's canvas. + */ + toViewCoordinates(viewport) { + const dt = viewport.currentDisplayTransform; + const containerToTrack = viewport.containerToTrackMap; + const viewX = dt.xWorldToView(this.xWorld_); + + let viewY = -1; + for (const index in this.yComponents_) { + const yComponent = this.yComponents_[index]; + const track = containerToTrack.getTrackByStableId(yComponent.stableId); + if (track !== undefined) { + const boundRect = track.getBoundingClientRect(); + viewY = yComponent.yPercentOffset * boundRect.height + boundRect.top; + break; + } + } + + return { + viewX, + viewY + }; + }, + + toDict() { + return { + xWorld: this.xWorld_, + yComponents: this.yComponents_ + }; + } + }; + + return { + Location, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump.html b/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump.html new file mode 100644 index 00000000000..0a723b8a9d9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump.html @@ -0,0 +1,221 @@ +<!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/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the MemoryAllocatorDump class. + */ +tr.exportTo('tr.model', function() { + /** + * @constructor + */ + function MemoryAllocatorDump(containerMemoryDump, fullName, opt_guid) { + this.fullName = fullName; + this.parent = undefined; + this.children = []; + + // String -> Scalar. + this.numerics = {}; + + // String -> string. + this.diagnostics = {}; + + // The associated container memory dump. + this.containerMemoryDump = containerMemoryDump; + + // Ownership relationship between memory allocator dumps. + this.owns = undefined; + this.ownedBy = []; + + // Map from sibling dumps (other children of this dump's parent) to the + // proportion of this dump's size which they (or their descendants) own. + this.ownedBySiblingSizes = new Map(); + + // Retention relationship between memory allocator dumps. + this.retains = []; + this.retainedBy = []; + + // Weak memory allocator dumps are removed from the model after import in + // tr.model.GlobalMemoryDump.removeWeakDumps(). See + // base::trace_event::MemoryAllocatorDump::Flags::WEAK in the Chromium + // codebase. + this.weak = false; + + // A list of information about the memory allocator dump (e.g. about how + // its fields were calculated). Each item should be an object with + // a mandatory 'type' property and type-specific extra arguments (see + // MemoryAllocatorDumpInfoType). + this.infos = []; + + // For debugging purposes. + this.guid = opt_guid; + } + + /** + * Size numeric names. Please refer to the Memory Dump Graph Metric + * Calculation design document for more details (https://goo.gl/fKg0dt). + */ + MemoryAllocatorDump.SIZE_NUMERIC_NAME = 'size'; + MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME = 'effective_size'; + MemoryAllocatorDump.RESIDENT_SIZE_NUMERIC_NAME = 'resident_size'; + MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME = + MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME; + + MemoryAllocatorDump.prototype = { + get name() { + return this.fullName.substring(this.fullName.lastIndexOf('/') + 1); + }, + + get quantifiedName() { + return '\'' + this.fullName + '\' in ' + + this.containerMemoryDump.containerName; + }, + + getDescendantDumpByFullName(fullName) { + return this.containerMemoryDump.getMemoryAllocatorDumpByFullName( + this.fullName + '/' + fullName); + }, + + isDescendantOf(otherDump) { + if (this === otherDump) return true; + if (this.parent === undefined) return false; + return this.parent.isDescendantOf(otherDump); + }, + + addNumeric(name, numeric) { + if (!(numeric instanceof tr.b.Scalar)) { + throw new Error('Numeric value must be an instance of Scalar.'); + } + if (name in this.numerics) { + throw new Error('Duplicate numeric name: ' + name + '.'); + } + this.numerics[name] = numeric; + }, + + addDiagnostic(name, text) { + if (typeof text !== 'string') { + throw new Error('Diagnostic text must be a string.'); + } + if (name in this.diagnostics) { + throw new Error('Duplicate diagnostic name: ' + name + '.'); + } + this.diagnostics[name] = text; + }, + + aggregateNumericsRecursively(opt_model) { + const numericNames = new Set(); + + // Aggregate descendants's numerics recursively and gather children's + // numeric names. + this.children.forEach(function(child) { + child.aggregateNumericsRecursively(opt_model); + for (const [item, value] of Object.entries(child.numerics)) { + numericNames.add(item, value); + } + }, this); + + // Aggregate children's numerics. + numericNames.forEach(function(numericName) { + if (numericName === MemoryAllocatorDump.SIZE_NUMERIC_NAME || + numericName === MemoryAllocatorDump.EFFECTIVE_SIZE_NUMERIC_NAME || + this.numerics[numericName] !== undefined) { + // Don't aggregate size and effective size numerics. These are + // calculated in GlobalMemoryDump.prototype.calculateSizes() and + // GlobalMemoryDump.prototype.calculateEffectiveSizes respectively. + // Also don't aggregate numerics that the parent already has. + return; + } + + this.numerics[numericName] = MemoryAllocatorDump.aggregateNumerics( + this.children.map(function(child) { + return child.numerics[numericName]; + }), opt_model); + }, this); + } + }; + + // TODO(petrcermak): Consider moving this to tr.v.Histogram. + MemoryAllocatorDump.aggregateNumerics = function(numerics, opt_model) { + let shouldLogWarning = !!opt_model; + let aggregatedUnit = undefined; + let aggregatedValue = 0; + + // Aggregate the units and sum up the values of the numerics. + numerics.forEach(function(numeric) { + if (numeric === undefined) return; + + const unit = numeric.unit; + if (aggregatedUnit === undefined) { + aggregatedUnit = unit; + } else if (aggregatedUnit !== unit) { + if (shouldLogWarning) { + opt_model.importWarning({ + type: 'numeric_parse_error', + message: 'Multiple units provided for numeric: \'' + + aggregatedUnit.unitName + '\' and \'' + unit.unitName + '\'.' + }); + shouldLogWarning = false; // Don't log multiple warnings. + } + // Use the most generic unit when the numerics don't agree (best + // effort). + aggregatedUnit = tr.b.Unit.byName.unitlessNumber_smallerIsBetter; + } + + aggregatedValue += numeric.value; + }, this); + + if (aggregatedUnit === undefined) return undefined; + + return new tr.b.Scalar(aggregatedUnit, aggregatedValue); + }; + + /** + * @constructor + */ + function MemoryAllocatorDumpLink(source, target, opt_importance) { + this.source = source; + this.target = target; + this.importance = opt_importance; + this.size = undefined; + } + + /** + * Types of size numeric information. + * + * @enum + */ + const MemoryAllocatorDumpInfoType = { + // The provided size of a MemoryAllocatorDump was less than the aggregated + // size of its children. + // + // Mandatory extra args: + // * providedSize: The inconsistent provided size. + // * dependencySize: The aggregated size of the children. + PROVIDED_SIZE_LESS_THAN_AGGREGATED_CHILDREN: 0, + + // The provided size of a MemoryAllocatorDump was less than the size of its + // largest owner. + // + // Mandatory extra args: + // * providedSize: The inconsistent provided size. + // * dependencySize: The size of the largest owner. + PROVIDED_SIZE_LESS_THAN_LARGEST_OWNER: 1 + }; + + return { + MemoryAllocatorDump, + MemoryAllocatorDumpLink, + MemoryAllocatorDumpInfoType, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump_test.html b/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump_test.html new file mode 100644 index 00000000000..7cc100dce25 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/memory_allocator_dump_test.html @@ -0,0 +1,221 @@ +<!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/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/container_memory_dump.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ContainerMemoryDump = tr.model.ContainerMemoryDump; + const MemoryAllocatorDump = tr.model.MemoryAllocatorDump; + const MemoryAllocatorDumpLink = tr.model.MemoryAllocatorDumpLink; + const Scalar = tr.b.Scalar; + const unitlessNumber_smallerIsBetter = + tr.b.Unit.byName.unitlessNumber_smallerIsBetter; + const sizeInBytes = tr.b.Unit.byName.sizeInBytes; + const powerInWatts = tr.b.Unit.byName.powerInWatts; + const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump; + const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump; + const checkDumpNumericsAndDiagnostics = + tr.model.MemoryDumpTestUtils.checkDumpNumericsAndDiagnostics; + + test('memoryAllocatorDump_instantiate', function() { + const containerDump = new ContainerMemoryDump(42); + containerDump.containerName = 'super dump'; + const dump = new MemoryAllocatorDump(containerDump, 'v8/objects/object7'); + + assert.strictEqual(dump.name, 'object7'); + assert.strictEqual(dump.fullName, 'v8/objects/object7'); + assert.strictEqual(dump.containerMemoryDump, containerDump); + assert.strictEqual( + dump.quantifiedName, '\'v8/objects/object7\' in super dump'); + }); + + test('memoryAllocatorDumps_aggregateNumericsRecursively', function() { + const md = new ContainerMemoryDump(42); + + const oilpanDump = newAllocatorDump(md, 'oilpan', {numerics: { + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7) + }}); + + addChildDump(oilpanDump, 'bucket1', {numerics: { + size: 512, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 3), + inner_size: 256, + outer_size: 1024 + }}); + + const oilpanBucket2Dump = addChildDump(oilpanDump, 'bucket2'); + + const oilpanBucket2StringsDump = addChildDump( + oilpanBucket2Dump, 'strings', {numerics: { + size: 512, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 4), + inner_size: 512, + outer_size: 2048 + }}); + + oilpanDump.aggregateNumericsRecursively(); + + // oilpan has *some* numerics aggregated. + checkDumpNumericsAndDiagnostics(oilpanDump, { + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7), + inner_size: 768, + outer_size: 3072 + }, {}); + + // oilpan/bucket2 has *all* numerics aggregated (except for size). + checkDumpNumericsAndDiagnostics(oilpanBucket2Dump, { + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 4), + inner_size: 512, + outer_size: 2048 + }, {}); + + // oilpan/bucket2/strings has *no* numerics aggregated. + checkDumpNumericsAndDiagnostics(oilpanBucket2StringsDump, { + size: 512, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 4), + inner_size: 512, + outer_size: 2048 + }, {}); + }); + + test('memoryAllocatorDump_aggregateNumerics', function() { + function checkAggregateNumerics(numerics, expectedValue, expectedUnit, + opt_expectedWarningCount) { + function checkResult(result) { + if (expectedValue === undefined) { + assert.isUndefined(result); + assert.isUndefined(expectedUnit); // Test sanity check. + } else { + assert.instanceOf(result, Scalar); + assert.strictEqual(result.value, expectedValue); + assert.strictEqual(result.unit, expectedUnit); + } + } + + // Without model parameter. + const result1 = MemoryAllocatorDump.aggregateNumerics(numerics); + checkResult(result1); + + // With model parameter. + const mockModel = { + warnings: [], + importWarning(warning) { + this.warnings.push(warning); + } + }; + const result2 = MemoryAllocatorDump.aggregateNumerics( + numerics, mockModel); + checkResult(result2); + assert.lengthOf(mockModel.warnings, opt_expectedWarningCount || 0); + } + + // No defined numerics. + checkAggregateNumerics([], undefined); + checkAggregateNumerics([undefined], undefined); + checkAggregateNumerics([undefined, undefined], undefined); + + // Consistent units. + checkAggregateNumerics( + [new Scalar(unitlessNumber_smallerIsBetter, 10)], + 10, unitlessNumber_smallerIsBetter); + checkAggregateNumerics( + [new Scalar(sizeInBytes, 10), + new Scalar(sizeInBytes, 20), + new Scalar(sizeInBytes, 40)], + 70, sizeInBytes); + checkAggregateNumerics( + [undefined, + new Scalar(sizeInBytes, 16), + undefined, + new Scalar(sizeInBytes, 32), + undefined], + 48, sizeInBytes); + + // Inconsistent units. + checkAggregateNumerics( + [new Scalar(sizeInBytes, 10), + new Scalar(powerInWatts, 20)], + 30, unitlessNumber_smallerIsBetter, 1 /* opt_expectedWarningCount */); + checkAggregateNumerics( + [undefined, + new Scalar(powerInWatts, 16), + undefined, + new Scalar(unitlessNumber_smallerIsBetter, 32), + undefined, + new Scalar(sizeInBytes, 64), + undefined], + 112, unitlessNumber_smallerIsBetter, 1 /* opt_expectedWarningCount */); + }); + + test('memoryAllocatorDumps_isDescendantOf', function() { + const md = new ContainerMemoryDump(42); + + const v8Dump = new MemoryAllocatorDump(md, 'v8'); + const v8HeapsDump = addChildDump(v8Dump, 'heaps'); + const v8ObjectsDump = addChildDump(v8Dump, 'objects'); + const v8Object1Dump = addChildDump(v8ObjectsDump, 'obj1'); + const v8Object2Dump = addChildDump(v8ObjectsDump, 'obj2'); + + const oilpanDump = new MemoryAllocatorDump(md, 'oilpan'); + + assert.isTrue(v8Dump.isDescendantOf(v8Dump)); + assert.isTrue(v8HeapsDump.isDescendantOf(v8Dump)); + assert.isTrue(v8ObjectsDump.isDescendantOf(v8Dump)); + assert.isTrue(v8Object1Dump.isDescendantOf(v8Dump)); + assert.isTrue(v8Object2Dump.isDescendantOf(v8Dump)); + assert.isTrue(v8ObjectsDump.isDescendantOf(v8ObjectsDump)); + assert.isTrue(v8Object1Dump.isDescendantOf(v8ObjectsDump)); + assert.isTrue(v8Object2Dump.isDescendantOf(v8ObjectsDump)); + assert.isTrue(oilpanDump.isDescendantOf(oilpanDump)); + + assert.isFalse(v8Dump.isDescendantOf(oilpanDump)); + assert.isFalse(v8Dump.isDescendantOf(v8HeapsDump)); + assert.isFalse(v8Dump.isDescendantOf(v8ObjectsDump)); + assert.isFalse(v8Dump.isDescendantOf(v8Object1Dump)); + assert.isFalse(v8Dump.isDescendantOf(v8Object2Dump)); + assert.isFalse(v8Object1Dump.isDescendantOf(v8Object2Dump)); + assert.isFalse(v8Object2Dump.isDescendantOf(v8Object1Dump)); + }); + + test('memoryAllocatorDumps_getDescendantDumpByFullName', function() { + const containerDump = new ContainerMemoryDump(42); + + const gpuDump = new MemoryAllocatorDump(containerDump, 'gpu'); + containerDump.memoryAllocatorDumps = [gpuDump]; + + const memtrackDump = addChildDump(gpuDump, 'android_memtrack'); + const glDump = addChildDump(memtrackDump, 'gl'); + const gfxDump = addChildDump(memtrackDump, 'gfx'); + const tileDump = addChildDump(gfxDump, 'tile'); + + assert.strictEqual(gpuDump.getDescendantDumpByFullName( + 'android_memtrack'), memtrackDump); + assert.strictEqual(gpuDump.getDescendantDumpByFullName( + 'android_memtrack/gfx/tile'), tileDump); + assert.strictEqual(memtrackDump.getDescendantDumpByFullName('gl'), glDump); + assert.strictEqual(memtrackDump.getDescendantDumpByFullName( + 'gfx/tile'), tileDump); + }); + + test('memoryAllocatorDumpLink_instantiate', function() { + const d1 = new MemoryAllocatorDump('v8/isolate1'); + const d2 = new MemoryAllocatorDump('oilpan/document1'); + const link = new MemoryAllocatorDumpLink(d1, d2, 3); + + assert.strictEqual(link.source, d1); + assert.strictEqual(link.target, d2); + assert.strictEqual(link.importance, 3); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/memory_dump_test_utils.html b/chromium/third_party/catapult/tracing/tracing/model/memory_dump_test_utils.html new file mode 100644 index 00000000000..de3413f7f44 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/memory_dump_test_utils.html @@ -0,0 +1,220 @@ +<!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/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/container_memory_dump.html"> +<link rel="import" href="/tracing/model/global_memory_dump.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/model/process_memory_dump.html"> +<link rel="import" href="/tracing/model/vm_region.html"> + +<script> +'use strict'; + +/** + * @fileoverview Helper functions for tests involving memory dumps. + */ +tr.exportTo('tr.model', function() { + const GlobalMemoryDump = tr.model.GlobalMemoryDump; + const ProcessMemoryDump = tr.model.ProcessMemoryDump; + const MemoryAllocatorDump = tr.model.MemoryAllocatorDump; + const MemoryAllocatorDumpLink = tr.model.MemoryAllocatorDumpLink; + const VMRegion = tr.model.VMRegion; + const VMRegionClassificationNode = tr.model.VMRegionClassificationNode; + const Scalar = tr.b.Scalar; + const sizeInBytes_smallerIsBetter = + tr.b.Unit.byName.sizeInBytes_smallerIsBetter; + const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT; + + function castToScalar(value) { + if (typeof value === 'number') { + return new Scalar(sizeInBytes_smallerIsBetter, value); + } + assert.instanceOf(value, Scalar); + return value; + } + + function getOption(opt_options, key, opt_defaultValue) { + if (opt_options && (key in opt_options)) { + return opt_options[key]; + } + return opt_defaultValue; + } + + function MemoryDumpTestUtils() { + throw new Error('Static class'); + } + + MemoryDumpTestUtils.SIZE_DELTA = 0.0001; + + /** + * Create a new global memory dump and add it to a model. + * + * @param {!tr.Model} model The trace model to which the new global dump + * should be added. + * @param {!{ + * ts: (number|undefined), + * duration: (number|undefined), + * levelOfDetail: (!tr.model.ContainerMemoryDump.LevelOfDetail|undefined) + * }=} opt_options Options for creating the new global dump. + * @return {!tr.model.GlobalMemoryDump} The newly created global memory dump. + */ + MemoryDumpTestUtils.addGlobalMemoryDump = function(model, opt_options) { + const timestamp = getOption(opt_options, 'ts', 0); + const gmd = new GlobalMemoryDump(model, timestamp); + gmd.levelOfDetail = getOption(opt_options, 'levelOfDetail', LIGHT); + gmd.duration = getOption(opt_options, 'duration', 0); + model.globalMemoryDumps.push(gmd); + return gmd; + }; + + /** + * Create a new process memory dump and add it to a global memory dump. + * + * @param {!tr.model.GlobalMemoryDump} gmd The global dump to which the new + * process dump should be added. + * @param {!tr.model.Process} pmd The process associated with the process + * dump. + * @param {!{ + * ts: (number|undefined) + * }=} opt_options Options for creating the new process dump. + * @return {!tr.model.ProcessMemoryDump} The newly created process memory + * dump. + */ + MemoryDumpTestUtils.addProcessMemoryDump = + function(gmd, process, opt_options) { + const timestamp = getOption(opt_options, 'ts', gmd.start); + const pmd = new ProcessMemoryDump(gmd, process, timestamp); + process.memoryDumps.push(pmd); + if (process.pid in gmd.processMemoryDumps) { + // Test sanity check. + throw new Error('Process memory dump for process with pid=' + + process.pid + ' has already been provided'); + } + gmd.processMemoryDumps[process.pid] = pmd; + return pmd; + }; + + /** + * Create a new memory allocator dump. + * + * @param {!tr.model.ContainerMemoryDump} containerDump The container dump + * associated with the new allocator dump. + * @param {string} fullName The full name of the new allocator dump + * (including ancestors). + * @param {!{ + * guid: (number|undefined), + * numerics: (!Object<string, (number|!tr.b.Scalar)>|undefined) + * }=} opt_options Options for creating the new allocator dump. + * @return {!tr.model.MemoryAllocatorDump} The newly created memory allocator + * dump. + */ + MemoryDumpTestUtils.newAllocatorDump = function( + containerDump, fullName, opt_options) { + const dump = new MemoryAllocatorDump(containerDump, fullName, + getOption(opt_options, 'guid')); + const numerics = getOption(opt_options, 'numerics'); + if (numerics) { + for (const [numericName, value] of Object.entries(numerics)) { + dump.addNumeric(numericName, castToScalar(value)); + } + } + const children = getOption(opt_options, 'children'); + if (children) dump.children = children; + return dump; + }; + + /** + * Create a new child memory allocator dump and add it to a parent memory + * allocator dump. + * + * @param {!tr.model.MemoryAllocatorDump} parentDump The parent allocator + * dump. + * @param {string} name The name of the child allocator dump (excluding + * ancestors). + * @param {!{ + * guid: (number|undefined), + * numerics: (!Object<string, (number|!tr.b.Scalar)>|undefined) + * }=} opt_options Options for creating the child allocator dump. + * @return {!tr.model.MemoryAllocatorDump} The newly created child memory + * allocator dump. + */ + MemoryDumpTestUtils.addChildDump = function(parentDump, name, opt_options) { + const childDump = MemoryDumpTestUtils.newAllocatorDump( + parentDump.containerMemoryDump, parentDump.fullName + '/' + name, + opt_options); + childDump.parent = parentDump; + parentDump.children.push(childDump); + return childDump; + }; + + MemoryDumpTestUtils.addOwnershipLink = function( + ownerDump, ownedDump, opt_importance) { + assert.isUndefined(ownerDump.owns); // Sanity check. + const ownershipLink = + new MemoryAllocatorDumpLink(ownerDump, ownedDump, opt_importance); + ownerDump.owns = ownershipLink; + ownedDump.ownedBy.push(ownershipLink); + return ownershipLink; + }; + + MemoryDumpTestUtils.checkDumpNumericsAndDiagnostics = + function(dump, expectedNumerics, expectedDiagnostics) { + const actualNumerics = dump.numerics; + assert.sameMembers( + Object.keys(actualNumerics), Object.keys(expectedNumerics)); + for (const numericName in actualNumerics) { + const actualNumeric = actualNumerics[numericName]; + const expectedNumeric = castToScalar(expectedNumerics[numericName]); + assert.instanceOf(actualNumeric, tr.b.Scalar); + assert.strictEqual(actualNumeric.unit, expectedNumeric.unit); + assert.closeTo(actualNumeric.value, expectedNumeric.value, + MemoryDumpTestUtils.SIZE_DELTA); + } + + assert.deepEqual(dump.diagnostics, expectedDiagnostics); + }; + + MemoryDumpTestUtils.checkVMRegions = function(vmRegions, expectedRegions) { + if (vmRegions instanceof VMRegionClassificationNode) { + vmRegions = vmRegions.allRegionsForTesting; + } + + const expectedRegionsMap = new Map(); + expectedRegions.forEach(function(region) { + if (!(region instanceof VMRegion)) { + region = VMRegion.fromDict(region); + } + expectedRegionsMap.set(region.uniqueIdWithinProcess, region); + }); + const actualRegionsMap = new Map(); + vmRegions.forEach(function(region) { + actualRegionsMap.set(region.uniqueIdWithinProcess, region); + }); + + assert.strictEqual(actualRegionsMap.size, expectedRegionsMap.size); + for (const id of expectedRegionsMap.keys()) { + const expectedRegion = expectedRegionsMap.get(id); + const actualRegion = actualRegionsMap.get(id); + + assert.instanceOf(actualRegion, VMRegion); + assert.strictEqual(actualRegion.startAddress, + expectedRegion.startAddress); + assert.strictEqual(actualRegion.sizeInBytes, expectedRegion.sizeInBytes); + assert.strictEqual(actualRegion.protectionFlags, + expectedRegion.protectionFlags); + assert.strictEqual(actualRegion.mappedFile, expectedRegion.mappedFile); + assert.deepEqual(actualRegion.byteStats, expectedRegion.byteStats); + } + }; + + return { + MemoryDumpTestUtils, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/model.html b/chromium/third_party/catapult/tracing/tracing/model/model.html new file mode 100644 index 00000000000..022b7cf7fbe --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/model.html @@ -0,0 +1,670 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2012 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/event.html"> +<link rel="import" href="/tracing/base/interval_tree.html"> +<link rel="import" href="/tracing/base/math/quad.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/task.html"> +<link rel="import" href="/tracing/base/time_display_modes.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/core/auditor.html"> +<link rel="import" href="/tracing/core/filter.html"> +<link rel="import" href="/tracing/model/alert.html"> +<link rel="import" href="/tracing/model/clock_sync_manager.html"> +<link rel="import" href="/tracing/model/constants.html"> +<link rel="import" href="/tracing/model/device.html"> +<link rel="import" href="/tracing/model/flow_event.html"> +<link rel="import" href="/tracing/model/frame.html"> +<link rel="import" href="/tracing/model/global_memory_dump.html"> +<link rel="import" href="/tracing/model/instant_event.html"> +<link rel="import" href="/tracing/model/kernel.html"> +<link rel="import" href="/tracing/model/model_indices.html"> +<link rel="import" href="/tracing/model/model_stats.html"> +<link rel="import" href="/tracing/model/object_snapshot.html"> +<link rel="import" href="/tracing/model/process.html"> +<link rel="import" href="/tracing/model/process_memory_dump.html"> +<link rel="import" href="/tracing/model/sample.html"> +<link rel="import" href="/tracing/model/stack_frame.html"> +<link rel="import" href="/tracing/model/user_model/user_expectation.html"> +<link rel="import" href="/tracing/model/user_model/user_model.html"> + +<script> +'use strict'; + +/** + * @fileoverview Model is a parsed representation of the + * TraceEvents obtained from base/trace_event in which the begin-end + * tokens are converted into a hierarchy of processes, threads, + * subrows, and slices. + * + * The building block of the model is a slice. A slice is roughly + * equivalent to function call executing on a specific thread. As a + * result, slices may have one or more subslices. + * + * A thread contains one or more subrows of slices. Row 0 corresponds to + * the "root" slices, e.g. the topmost slices. Row 1 contains slices that + * are nested 1 deep in the stack, and so on. We use these subrows to draw + * nesting tasks. + * + */ +tr.exportTo('tr', function() { + const Process = tr.model.Process; + const Device = tr.model.Device; + const Kernel = tr.model.Kernel; + const GlobalMemoryDump = tr.model.GlobalMemoryDump; + const GlobalInstantEvent = tr.model.GlobalInstantEvent; + const FlowEvent = tr.model.FlowEvent; + const Alert = tr.model.Alert; + const Sample = tr.model.Sample; + + /** + * @constructor + */ + function Model() { + tr.model.EventContainer.call(this); + tr.b.EventTarget.decorate(this); + + this.timestampShiftToZeroAmount_ = 0; + + this.faviconHue = 'blue'; // Should be a key from favicons.html + + this.device = new Device(this); + this.kernel = new Kernel(this); + this.processes = {}; + this.metadata = []; + this.categories = []; + this.instantEvents = []; + this.flowEvents = []; + this.clockSyncManager = new tr.model.ClockSyncManager(); + this.intrinsicTimeUnit_ = undefined; + + this.stackFrames = {}; + this.samples = []; + + this.alerts = []; + this.userModel = new tr.model.um.UserModel(this); + + this.flowIntervalTree = new tr.b.IntervalTree((f) => f.start, (f) => f.end); + this.globalMemoryDumps = []; + + this.userFriendlyCategoryDrivers_ = []; + + this.annotationsByGuid_ = {}; + this.modelIndices = undefined; + + this.stats = new tr.model.ModelStats(); + + this.importWarnings_ = []; + this.reportedImportWarnings_ = {}; + + this.isTimeHighResolution_ = true; + + this.patchupsToApply_ = []; + + this.doesHelperGUIDSupportThisModel_ = {}; + this.helpersByConstructorGUID_ = {}; + this.eventsByStableId_ = undefined; + } + + Model.prototype = { + __proto__: tr.model.EventContainer.prototype, + + getEventByStableId(stableId) { + if (this.eventsByStableId_ === undefined) { + this.eventsByStableId_ = {}; + for (const event of this.getDescendantEvents()) { + this.eventsByStableId_[event.stableId] = event; + } + } + return this.eventsByStableId_[stableId]; + }, + + getOrCreateHelper(constructor) { + if (!constructor.guid) { + throw new Error('Helper constructors must have GUIDs'); + } + + if (this.helpersByConstructorGUID_[constructor.guid] === undefined) { + if (this.doesHelperGUIDSupportThisModel_[constructor.guid] === + undefined) { + this.doesHelperGUIDSupportThisModel_[constructor.guid] = + constructor.supportsModel(this); + } + + if (!this.doesHelperGUIDSupportThisModel_[constructor.guid]) { + return undefined; + } + + this.helpersByConstructorGUID_[constructor.guid] = new constructor( + this); + } + return this.helpersByConstructorGUID_[constructor.guid]; + }, + + * childEvents() { + yield* this.globalMemoryDumps; + yield* this.instantEvents; + yield* this.flowEvents; + yield* this.alerts; + yield* this.samples; + }, + + * childEventContainers() { + yield this.userModel; + yield this.device; + yield this.kernel; + yield* Object.values(this.processes); + }, + + /** + * Some objects in the model can persist their state in ModelSettings. + * + * This iterates through them. + */ + iterateAllPersistableObjects(callback) { + this.kernel.iterateAllPersistableObjects(callback); + for (const pid in this.processes) { + this.processes[pid].iterateAllPersistableObjects(callback); + } + }, + + updateBounds() { + this.bounds.reset(); + const bounds = this.bounds; + for (const ec of this.childEventContainers()) { + ec.updateBounds(); + bounds.addRange(ec.bounds); + } + for (const event of this.childEvents()) { + event.addBoundsToRange(bounds); + } + }, + + shiftWorldToZero() { + const shiftAmount = -this.bounds.min; + this.timestampShiftToZeroAmount_ = shiftAmount; + for (const ec of this.childEventContainers()) { + ec.shiftTimestampsForward(shiftAmount); + } + + for (const event of this.childEvents()) { + event.start += shiftAmount; + } + this.updateBounds(); + }, + + convertTimestampToModelTime(sourceClockDomainName, ts) { + if (sourceClockDomainName !== 'traceEventClock') { + throw new Error('Only traceEventClock is supported.'); + } + return tr.b.Unit.timestampFromUs(ts) + + this.timestampShiftToZeroAmount_; + }, + + get numProcesses() { + let n = 0; + for (const p in this.processes) { + n++; + } + return n; + }, + + /** + * @return {Process} Gets a TimelineProcess for a specified pid. Returns + * undefined if the process doesn't exist. + */ + getProcess(pid) { + return this.processes[pid]; + }, + + /** + * @return {Process} Gets a TimelineProcess for a specified pid or + * creates one if it does not exist. + */ + getOrCreateProcess(pid) { + if (!this.processes[pid]) { + this.processes[pid] = new Process(this, pid); + } + return this.processes[pid]; + }, + + addStackFrame(stackFrame) { + if (this.stackFrames[stackFrame.id]) { + throw new Error('Stack frame already exists'); + } + this.stackFrames[stackFrame.id] = stackFrame; + return stackFrame; + }, + + /** + * Generates the set of categories from the slices and counters. + */ + updateCategories_() { + const categoriesDict = {}; + this.userModel.addCategoriesToDict(categoriesDict); + this.device.addCategoriesToDict(categoriesDict); + this.kernel.addCategoriesToDict(categoriesDict); + for (const pid in this.processes) { + this.processes[pid].addCategoriesToDict(categoriesDict); + } + + this.categories = []; + for (const category in categoriesDict) { + if (category !== '') { + this.categories.push(category); + } + } + }, + + getAllThreads() { + const threads = []; + for (const tid in this.kernel.threads) { + threads.push(process.threads[tid]); + } + for (const pid in this.processes) { + const process = this.processes[pid]; + for (const tid in process.threads) { + threads.push(process.threads[tid]); + } + } + return threads; + }, + + /** + * @param {(!function(!tr.model.Process): boolean)=} opt_predicate Optional + * predicate for filtering the returned processes. If undefined, all + * process in the model will be returned. + * @return {!Array<!tr.model.Process>} An array of processes in the model. + */ + getAllProcesses(opt_predicate) { + const processes = []; + for (const pid in this.processes) { + const process = this.processes[pid]; + if (opt_predicate === undefined || opt_predicate(process)) { + processes.push(process); + } + } + return processes; + }, + + /** + * @return {Array} An array of all the counters in the model. + */ + getAllCounters() { + const counters = []; + counters.push.apply( + counters, Object.values(this.device.counters || {})); + counters.push.apply( + counters, Object.values(this.kernel.counters || {})); + for (const pid in this.processes) { + const process = this.processes[pid]; + for (const tid in process.counters) { + counters.push(process.counters[tid]); + } + } + return counters; + }, + + getAnnotationByGUID(guid) { + return this.annotationsByGuid_[guid]; + }, + + addAnnotation(annotation) { + if (!annotation.guid) { + throw new Error('Annotation with undefined guid given'); + } + + this.annotationsByGuid_[annotation.guid] = annotation; + tr.b.dispatchSimpleEvent(this, 'annotationChange'); + }, + + removeAnnotation(annotation) { + this.annotationsByGuid_[annotation.guid].onRemove(); + delete this.annotationsByGuid_[annotation.guid]; + tr.b.dispatchSimpleEvent(this, 'annotationChange'); + }, + + getAllAnnotations() { + return Object.values(this.annotationsByGuid_); + }, + + addUserFriendlyCategoryDriver(ufcd) { + this.userFriendlyCategoryDrivers_.push(ufcd); + }, + + /** + * Gets the user friendly category string from an event. + * + * Returns undefined if none is known. + */ + getUserFriendlyCategoryFromEvent(event) { + for (let i = 0; i < this.userFriendlyCategoryDrivers_.length; i++) { + const ufc = this.userFriendlyCategoryDrivers_[i].fromEvent(event); + if (ufc !== undefined) return ufc; + } + return undefined; + }, + + /** + * @param {String} The name of the thread to find. + * @return {Array} An array of all the matched threads. + */ + findAllThreadsNamed(name) { + const namedThreads = []; + namedThreads.push.apply( + namedThreads, + this.kernel.findAllThreadsNamed(name)); + for (const pid in this.processes) { + namedThreads.push.apply( + namedThreads, + this.processes[pid].findAllThreadsNamed(name)); + } + return namedThreads; + }, + + get importOptions() { + return this.importOptions_; + }, + + set importOptions(options) { + this.importOptions_ = options; + }, + + /** + * Returns a time unit that is used to format values and determines the + * precision of the timestamp values. + */ + get intrinsicTimeUnit() { + if (this.intrinsicTimeUnit_ === undefined) { + return tr.b.TimeDisplayModes.ms; + } + return this.intrinsicTimeUnit_; + }, + + set intrinsicTimeUnit(value) { + if (this.intrinsicTimeUnit_ === value) return; + if (this.intrinsicTimeUnit_ !== undefined) { + throw new Error('Intrinsic time unit already set'); + } + this.intrinsicTimeUnit_ = value; + }, + + get isTimeHighResolution() { + return this.isTimeHighResolution_; + }, + + set isTimeHighResolution(value) { + this.isTimeHighResolution_ = value; + }, + + /** + * Returns a link to a trace data file that this model was imported from. + * This is NOT the URL of a site being traced, but instead an indicator of + * where the data is stored. + */ + get canonicalUrl() { + return this.canonicalUrl_; + }, + + set canonicalUrl(value) { + if (this.canonicalUrl_ === value) return; + if (this.canonicalUrl_ !== undefined) { + throw new Error('canonicalUrl already set'); + } + this.canonicalUrl_ = value; + }, + + /** + * Saves a warning that happened during import. + * + * Warnings are typically logged to the console, and optionally, the + * more critical ones are shown to the user. + * + * @param {Object} data The import warning data. Data must provide two + * accessors: type, message. The types are used to determine if we + * should output the message, we'll only output one message of each type. + * The message is the actual warning content. + */ + importWarning(data) { + data.showToUser = !!data.showToUser; + + this.importWarnings_.push(data); + + // Only log each warning type once. We may want to add some kind of + // flag to allow reporting all importer warnings. + if (this.reportedImportWarnings_[data.type] === true) return; + + this.reportedImportWarnings_[data.type] = true; + }, + + get hasImportWarnings() { + return (this.importWarnings_.length > 0); + }, + + get importWarnings() { + return this.importWarnings_; + }, + + get importWarningsThatShouldBeShownToUser() { + return this.importWarnings_.filter(function(warning) { + return warning.showToUser; + }); + }, + + autoCloseOpenSlices() { + // Sort the samples. + this.samples.sort(function(x, y) { + return x.start - y.start; + }); + + this.updateBounds(); + this.kernel.autoCloseOpenSlices(); + for (const pid in this.processes) { + this.processes[pid].autoCloseOpenSlices(); + } + }, + + createSubSlices() { + this.kernel.createSubSlices(); + for (const pid in this.processes) { + this.processes[pid].createSubSlices(); + } + }, + + preInitializeObjects() { + for (const pid in this.processes) { + this.processes[pid].preInitializeObjects(); + } + }, + + initializeObjects() { + for (const pid in this.processes) { + this.processes[pid].initializeObjects(); + } + }, + + pruneEmptyContainers() { + this.kernel.pruneEmptyContainers(); + for (const pid in this.processes) { + this.processes[pid].pruneEmptyContainers(); + } + }, + + mergeKernelWithUserland() { + for (const pid in this.processes) { + this.processes[pid].mergeKernelWithUserland(); + } + }, + + computeWorldBounds(shiftWorldToZero) { + this.updateBounds(); + this.updateCategories_(); + + if (shiftWorldToZero) { + this.shiftWorldToZero(); + } + }, + + buildFlowEventIntervalTree() { + for (let i = 0; i < this.flowEvents.length; ++i) { + const flowEvent = this.flowEvents[i]; + this.flowIntervalTree.insert(flowEvent); + } + this.flowIntervalTree.updateHighValues(); + }, + + cleanupUndeletedObjects() { + for (const pid in this.processes) { + this.processes[pid].autoDeleteObjects(this.bounds.max); + } + }, + + sortMemoryDumps() { + this.globalMemoryDumps.sort(function(x, y) { + return x.start - y.start; + }); + + for (const pid in this.processes) { + this.processes[pid].sortMemoryDumps(); + } + }, + + finalizeMemoryGraphs() { + this.globalMemoryDumps.forEach(function(dump) { + dump.finalizeGraph(); + }); + }, + + buildEventIndices() { + this.modelIndices = new tr.model.ModelIndices(this); + }, + + sortAlerts() { + this.alerts.sort(function(x, y) { + return x.start - y.start; + }); + }, + + applyObjectRefPatchups() { + // Change all the fields pointing at id_refs to their real values. + const unresolved = []; + this.patchupsToApply_.forEach(function(patchup) { + if (patchup.pidRef in this.processes) { + const snapshot = this.processes[patchup.pidRef].objects.getSnapshotAt( + patchup.scopedId, patchup.ts); + if (snapshot) { + patchup.object[patchup.field] = snapshot; + snapshot.referencedAt(patchup.item, patchup.object, patchup.field); + return; + } + } + unresolved.push(patchup); + }, this); + this.patchupsToApply_ = unresolved; + }, + + replacePIDRefsInPatchups(oldPidRef, newPidRef) { + this.patchupsToApply_.forEach(function(patchup) { + if (patchup.pidRef === oldPidRef) { + patchup.pidRef = newPidRef; + } + }); + }, + + /** + * Called by the model to join references between objects, after final model + * bounds have been computed. + */ + joinRefs() { + this.joinObjectRefs_(); + this.applyObjectRefPatchups(); + }, + + joinObjectRefs_() { + for (const [pid, process] of Object.entries(this.processes)) { + this.joinObjectRefsForProcess_(pid, process); + } + }, + + joinObjectRefsForProcess_(pid, process) { + // Iterate the world, looking for id_refs + for (const thread of Object.values(process.threads)) { + thread.asyncSliceGroup.slices.forEach(function(item) { + this.searchItemForIDRefs_(pid, 'start', item); + }, this); + thread.sliceGroup.slices.forEach(function(item) { + this.searchItemForIDRefs_(pid, 'start', item); + }, this); + } + process.objects.iterObjectInstances(function(instance) { + instance.snapshots.forEach(function(item) { + this.searchItemForIDRefs_(pid, 'ts', item); + }, this); + }, this); + }, + + searchItemForIDRefs_(pid, itemTimestampField, item) { + if (!item.args && !item.contexts) return; + const patchupsToApply = this.patchupsToApply_; + + function handleField(object, fieldName, fieldValue) { + if (!fieldValue || (!fieldValue.id_ref && !fieldValue.idRef)) { + return; + } + + const scope = fieldValue.scope || tr.model.OBJECT_DEFAULT_SCOPE; + const idRef = fieldValue.id_ref || fieldValue.idRef; + const scopedId = new tr.model.ScopedId(scope, idRef); + const pidRef = fieldValue.pid_ref || fieldValue.pidRef || pid; + const ts = item[itemTimestampField]; + // We have to delay the actual change to the new value until after all + // refs have been located. Otherwise, we could end up recursing in + // ways we definitely didn't intend. + patchupsToApply.push({ + item, + object, + field: fieldName, + pidRef, + scopedId, + ts}); + } + function iterObjectFieldsRecursively(object) { + if (!(object instanceof Object)) return; + + if ((object instanceof tr.model.ObjectSnapshot) || + (object instanceof Float32Array) || + (object instanceof tr.b.math.Quad)) { + return; + } + + if (object instanceof Array) { + for (let i = 0; i < object.length; i++) { + handleField(object, i, object[i]); + iterObjectFieldsRecursively(object[i]); + } + return; + } + + for (const key in object) { + const value = object[key]; + handleField(object, key, value); + iterObjectFieldsRecursively(value); + } + } + + iterObjectFieldsRecursively(item.args); + iterObjectFieldsRecursively(item.contexts); + } + }; + + return { + Model, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_indices.html b/chromium/third_party/catapult/tracing/tracing/model/model_indices.html new file mode 100644 index 00000000000..27f89dba0b0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/model_indices.html @@ -0,0 +1,56 @@ +<!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"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Event Index class. + */ +tr.exportTo('tr.model', function() { + /** + * A Event Index maps an id to all the events that have that particular id + * + * @constructor + */ + function ModelIndices(model) { + // For now the only indices we construct are for flowEvents + this.flowEventsById_ = {}; + model.flowEvents.forEach(function(fe) { + if (fe.id !== undefined) { + if (!this.flowEventsById_.hasOwnProperty(fe.id)) { + this.flowEventsById_[fe.id] = []; + } + this.flowEventsById_[fe.id].push(fe); + } + }, this); + } + + ModelIndices.prototype = { + addEventWithId(id, event) { + if (!this.flowEventsById_.hasOwnProperty(id)) { + this.flowEventsById_[id] = []; + } + this.flowEventsById_[id].push(event); + }, + + getFlowEventsWithId(id) { + if (!this.flowEventsById_.hasOwnProperty(id)) { + return []; + } + return this.flowEventsById_[id]; + } + }; + + return { + ModelIndices, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_indices_test.html b/chromium/third_party/catapult/tracing/tracing/model/model_indices_test.html new file mode 100644 index 00000000000..bc5d99a0a29 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/model_indices_test.html @@ -0,0 +1,46 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newFlowEventEx = tr.c.TestUtils.newFlowEventEx; + const newModel = tr.c.TestUtils.newModel; + + test('getCorrectModelIndices', function() { + const m = newModel(function(m) { + m.f1 = newFlowEventEx({ + title: 'test1', + start: 0, + end: 10, + id: '0x100' + }); + + m.f2 = newFlowEventEx({ + title: 'test2', + start: 0, + end: 10, + id: '0x100' + }); + + m.flowEvents.push(m.f1); + m.flowEvents.push(m.f2); + }); + + assert.isDefined(m.modelIndices); + const modelIndices = m.modelIndices; + assert.strictEqual(modelIndices.getFlowEventsWithId('0x100').length, 2); + assert.strictEqual( + modelIndices.getFlowEventsWithId('0x100')[0].id, '0x100'); + assert.strictEqual(modelIndices.getFlowEventsWithId('0x101').length, 0); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_settings.html b/chromium/third_party/catapult/tracing/tracing/model/model_settings.html new file mode 100644 index 00000000000..c34040183d2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/model_settings.html @@ -0,0 +1,148 @@ +<!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/settings.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const Settings = tr.b.Settings; + + /** + * A way to persist settings specific to parts of a trace model. + * + * This object should not be persisted because it builds up internal data + * structures that map model objects to settings keys. It should thus be + * created for the duration of whatever interaction(s) you're going to do with + * model settings, and then discarded. + * + * This system works on a notion of an object key: for an object's key, it + * considers all the other keys in the model. If it is unique, then the key is + * persisted to tr.b.Settings. However, if it is not unique, then the + * setting is stored on the object itself. Thus, objects with unique keys will + * be persisted across page reloads, whereas objects with nonunique keys will + * not. + */ + function ModelSettings(model) { + this.model = model; + this.objectsByKey_ = []; + this.nonuniqueKeys_ = []; + this.buildObjectsByKeyMap_(); + this.removeNonuniqueKeysFromSettings_(); + this.ephemeralSettingsByGUID_ = {}; + } + + ModelSettings.prototype = { + buildObjectsByKeyMap_() { + const objects = []; + this.model.iterateAllPersistableObjects(function(o) { + objects.push(o); + }); + + const objectsByKey = {}; + const NONUNIQUE_KEY = 'nonuniqueKey'; + for (let i = 0; i < objects.length; i++) { + const object = objects[i]; + const objectKey = object.getSettingsKey(); + if (!objectKey) continue; + if (objectsByKey[objectKey] === undefined) { + objectsByKey[objectKey] = object; + continue; + } + objectsByKey[objectKey] = NONUNIQUE_KEY; + } + + const nonuniqueKeys = {}; + Object.keys(objectsByKey).forEach(function(objectKey) { + if (objectsByKey[objectKey] !== NONUNIQUE_KEY) { + return; + } + delete objectsByKey[objectKey]; + nonuniqueKeys[objectKey] = true; + }); + + this.nonuniqueKeys = nonuniqueKeys; + this.objectsByKey_ = objectsByKey; + }, + + removeNonuniqueKeysFromSettings_() { + const settings = Settings.get('trace_model_settings', {}); + let settingsChanged = false; + Object.keys(settings).forEach(function(objectKey) { + if (!this.nonuniqueKeys[objectKey]) { + return; + } + settingsChanged = true; + delete settings[objectKey]; + }, this); + if (settingsChanged) { + Settings.set('trace_model_settings', settings); + } + }, + + hasUniqueSettingKey(object) { + const objectKey = object.getSettingsKey(); + if (!objectKey) return false; + return this.objectsByKey_[objectKey] !== undefined; + }, + + getSettingFor(object, objectLevelKey, defaultValue) { + const objectKey = object.getSettingsKey(); + if (!objectKey || !this.objectsByKey_[objectKey]) { + const settings = this.getEphemeralSettingsFor_(object); + const ephemeralValue = settings[objectLevelKey]; + if (ephemeralValue !== undefined) { + return ephemeralValue; + } + return defaultValue; + } + + const settings = Settings.get('trace_model_settings', {}); + if (!settings[objectKey]) { + settings[objectKey] = {}; + } + const value = settings[objectKey][objectLevelKey]; + if (value !== undefined) { + return value; + } + return defaultValue; + }, + + setSettingFor(object, objectLevelKey, value) { + const objectKey = object.getSettingsKey(); + if (!objectKey || !this.objectsByKey_[objectKey]) { + this.getEphemeralSettingsFor_(object)[objectLevelKey] = value; + return; + } + + const settings = Settings.get('trace_model_settings', {}); + if (!settings[objectKey]) { + settings[objectKey] = {}; + } + if (settings[objectKey][objectLevelKey] === value) { + return; + } + settings[objectKey][objectLevelKey] = value; + Settings.set('trace_model_settings', settings); + }, + + getEphemeralSettingsFor_(object) { + if (object.guid === undefined) { + throw new Error('Only objects with GUIDs can be persisted'); + } + if (this.ephemeralSettingsByGUID_[object.guid] === undefined) { + this.ephemeralSettingsByGUID_[object.guid] = {}; + } + return this.ephemeralSettingsByGUID_[object.guid]; + } + }; + + return { + ModelSettings, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_settings_test.html b/chromium/third_party/catapult/tracing/tracing/model/model_settings_test.html new file mode 100644 index 00000000000..cec8898180c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/model_settings_test.html @@ -0,0 +1,178 @@ +<!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/model/model.html"> +<link rel="import" href="/tracing/model/model_settings.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('process_name_uniqueness_0', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const settings = new tr.model.ModelSettings(model); + assert.isFalse(settings.hasUniqueSettingKey(p1)); + }); + + test('process_name_uniqueness_1', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + p1.name = 'Browser'; + const settings = new tr.model.ModelSettings(model); + assert.isTrue(settings.hasUniqueSettingKey(p1)); + }); + + test('process_name_uniqueness_2', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const p2 = model.getOrCreateProcess(2); + p1.name = 'Renderer'; + p2.name = 'Renderer'; + const settings = new tr.model.ModelSettings(model); + assert.isFalse(settings.hasUniqueSettingKey(p1)); + assert.isFalse(settings.hasUniqueSettingKey(p2)); + }); + + test('process_name_uniqueness_3', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const p2 = model.getOrCreateProcess(2); + p1.name = 'Renderer'; + p1.labels.push('Google Search'); + p2.name = 'Renderer'; + const settings = new tr.model.ModelSettings(model); + assert.isTrue(settings.hasUniqueSettingKey(p1)); + assert.isTrue(settings.hasUniqueSettingKey(p2)); + }); + + test('thread_name_uniqueness_0', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const p2 = model.getOrCreateProcess(2); + const t1 = p1.getOrCreateThread(1); + const t2 = p2.getOrCreateThread(2); + p1.name = 'Browser'; + p2.name = 'Renderer'; + t1.name = 'Main'; + t2.name = 'Main'; + const settings = new tr.model.ModelSettings(model); + assert.isTrue(settings.hasUniqueSettingKey(t1)); + assert.isTrue(settings.hasUniqueSettingKey(t2)); + }); + + test('thread_name_uniqueness_1', function() { + const model = new tr.Model(); + const p1 = model.getOrCreateProcess(1); + const p2 = model.getOrCreateProcess(2); + const t1 = p1.getOrCreateThread(1); + const t2 = p2.getOrCreateThread(2); + p1.name = 'Renderer'; + p2.name = 'Renderer'; + t1.name = 'Main'; + t2.name = 'Main'; + const settings = new tr.model.ModelSettings(model); + assert.isFalse(settings.hasUniqueSettingKey(t1)); + assert.isFalse(settings.hasUniqueSettingKey(t2)); + }); + + test('process_persistence_when_not_unique', function() { + let model = new tr.Model(); + let p1 = model.getOrCreateProcess(1); + let settings = new tr.model.ModelSettings(model); + assert.isTrue(settings.getSettingFor(p1, 'true_by_default', true)); + + settings.setSettingFor(p1, 'true_by_default', false); + assert.isFalse(settings.getSettingFor(p1, 'true_by_default', true)); + + // Now, clobber the model, and verify that it didn't persist. + model = new tr.Model(); + p1 = model.getOrCreateProcess(1); + settings = new tr.model.ModelSettings(model); + assert.isTrue(settings.getSettingFor(p1, 'true_by_default', true)); + }); + + test('process_persistence_when_not_unique_with_name', function() { + let model = new tr.Model(); + let p1 = model.getOrCreateProcess(1); + p1.name = 'Browser'; + let settings = new tr.model.ModelSettings(model); + assert.isTrue(settings.getSettingFor(p1, 'true_by_default', true)); + + settings.setSettingFor(p1, 'true_by_default', false); + assert.isFalse(settings.getSettingFor(p1, 'true_by_default', true)); + + // Now, clobber the model, and verify that it persisted. + model = new tr.Model(); + p1 = model.getOrCreateProcess(1); + p1.name = 'Browser'; + settings = new tr.model.ModelSettings(model); + assert.isFalse(settings.getSettingFor(p1, 'true_by_default', true)); + }); + + test('thread_persistence_when_not_unique', function() { + let model = new tr.Model(); + let p1 = model.getOrCreateProcess(1); + let p2 = model.getOrCreateProcess(2); + let t1 = p1.getOrCreateThread(1); + let t2 = p2.getOrCreateThread(2); + p1.name = 'Renderer'; + p2.name = 'Renderer'; + t1.name = 'Main'; + t2.name = 'Main'; + let settings = new tr.model.ModelSettings(model); + assert.isTrue(settings.getSettingFor(t1, 'true_by_default', true)); + + settings.setSettingFor(t1, 'true_by_default', false); + assert.isFalse(settings.getSettingFor(t1, 'true_by_default', true)); + + // Now, clobber the model, and verify that it persisted. + model = new tr.Model(); + p1 = model.getOrCreateProcess(1); + p2 = model.getOrCreateProcess(2); + t1 = p1.getOrCreateThread(1); + t2 = p2.getOrCreateThread(2); + p1.name = 'Renderer'; + p2.name = 'Renderer'; + t1.name = 'Main'; + t2.name = 'Main'; + settings = new tr.model.ModelSettings(model); + assert.isTrue(settings.getSettingFor(t1, 'true_by_default', true)); + }); + + test('thread_persistence_when_unique', function() { + let model = new tr.Model(); + let p1 = model.getOrCreateProcess(1); + let p2 = model.getOrCreateProcess(2); + let t1 = p1.getOrCreateThread(1); + let t2 = p2.getOrCreateThread(2); + p1.name = 'Browser'; + p2.name = 'Renderer'; + t1.name = 'Main'; + t2.name = 'Main'; + let settings = new tr.model.ModelSettings(model); + assert.isTrue(settings.getSettingFor(t1, 'true_by_default', true)); + + settings.setSettingFor(t1, 'true_by_default', false); + assert.isFalse(settings.getSettingFor(t1, 'true_by_default', true)); + + // Now, clobber the model, and verify that it persisted. + model = new tr.Model(); + p1 = model.getOrCreateProcess(1); + p2 = model.getOrCreateProcess(2); + t1 = p1.getOrCreateThread(1); + t2 = p2.getOrCreateThread(2); + p1.name = 'Browser'; + p2.name = 'Renderer'; + t1.name = 'Main'; + t2.name = 'Main'; + settings = new tr.model.ModelSettings(model); + assert.isFalse(settings.getSettingFor(t1, 'true_by_default', true)); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_stats.html b/chromium/third_party/catapult/tracing/tracing/model/model_stats.html new file mode 100644 index 00000000000..41f7dd64602 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/model_stats.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + /** + * @constructor + */ + function ModelStats() { + this.traceEventCountsByKey_ = new Map(); + this.allTraceEventStats_ = []; + + this.traceEventStatsInTimeIntervals_ = new Map(); + this.allTraceEventStatsInTimeIntervals_ = []; + + this.hasEventSizesinBytes_ = false; + + this.traceImportDurationMs_ = undefined; + } + + ModelStats.prototype = { + TIME_INTERVAL_SIZE_IN_MS: 100, + + willProcessBasicTraceEvent(phase, category, title, ts, + opt_eventSizeinBytes) { + const key = phase + '/' + category + '/' + title; + let eventStats = this.traceEventCountsByKey_.get(key); + if (eventStats === undefined) { + eventStats = { + phase, + category, + title, + numEvents: 0, + totalEventSizeinBytes: 0 + }; + this.traceEventCountsByKey_.set(key, eventStats); + this.allTraceEventStats_.push(eventStats); + } + eventStats.numEvents++; + + const timeIntervalKey = Math.floor( + tr.b.Unit.timestampFromUs(ts) / this.TIME_INTERVAL_SIZE_IN_MS); + let eventStatsByTimeInverval = + this.traceEventStatsInTimeIntervals_.get(timeIntervalKey); + if (eventStatsByTimeInverval === undefined) { + eventStatsByTimeInverval = { + timeInterval: timeIntervalKey, + numEvents: 0, + totalEventSizeinBytes: 0 + }; + this.traceEventStatsInTimeIntervals_.set(timeIntervalKey, + eventStatsByTimeInverval); + this.allTraceEventStatsInTimeIntervals_.push(eventStatsByTimeInverval); + } + eventStatsByTimeInverval.numEvents++; + + if (opt_eventSizeinBytes !== undefined) { + this.hasEventSizesinBytes_ = true; + eventStats.totalEventSizeinBytes += opt_eventSizeinBytes; + eventStatsByTimeInverval.totalEventSizeinBytes += opt_eventSizeinBytes; + } + }, + + get allTraceEventStats() { + return this.allTraceEventStats_; + }, + + get allTraceEventStatsInTimeIntervals() { + return this.allTraceEventStatsInTimeIntervals_; + }, + + get hasEventSizesinBytes() { + return this.hasEventSizesinBytes_; + }, + + get traceImportDurationMs() { + return this.traceImportDurationMs_; + }, + + set traceImportDurationMs(traceImportDurationMs) { + this.traceImportDurationMs_ = traceImportDurationMs; + } + }; + + return { + ModelStats, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_stats_test.html b/chromium/third_party/catapult/tracing/tracing/model/model_stats_test.html new file mode 100644 index 00000000000..a1108b1b57a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/model_stats_test.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<link rel="import" href="/tracing/model/model_stats.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ModelStats = tr.model.ModelStats; + + test('getTraceEventStatsByCategory', function() { + const modelStats = new ModelStats(); + modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1'); + modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1'); + modelStats.willProcessBasicTraceEvent('X', 'cat2', 'title3'); + + assert.strictEqual(modelStats.allTraceEventStats.length, 2); + assert.strictEqual( + modelStats.traceEventCountsByKey_.get('X/cat1/title1').numEvents, + 2); + assert.strictEqual( + modelStats.traceEventCountsByKey_.get('X/cat2/title3').numEvents, + 1); + }); + + test('getTraceEventStatsInTimeIntervals', function() { + const modelStats = new ModelStats(); + const timeIntervalSizeInUs = modelStats.TIME_INTERVAL_SIZE_IN_MS * 1000; + modelStats.willProcessBasicTraceEvent('X', 'cat1', 'title1', 1, 1); + modelStats.willProcessBasicTraceEvent( + 'X', 'cat1', 'title1', timeIntervalSizeInUs + 1, 2); + modelStats.willProcessBasicTraceEvent( + 'X', 'cat1', 'title1', 2 * timeIntervalSizeInUs + 1, 3); + modelStats.willProcessBasicTraceEvent( + 'X', 'cat2', 'title3', 2 * timeIntervalSizeInUs + 2, 4); + + assert.strictEqual(modelStats.allTraceEventStatsInTimeIntervals.length, 3); + assert.strictEqual( + modelStats.traceEventStatsInTimeIntervals_.get(0).numEvents, 1); + assert.strictEqual( + modelStats.traceEventStatsInTimeIntervals_.get(1).numEvents, 1); + assert.strictEqual( + modelStats.traceEventStatsInTimeIntervals_.get(2).numEvents, 2); + + assert.isTrue(modelStats.hasEventSizesinBytes); + assert.strictEqual( + modelStats.traceEventStatsInTimeIntervals_.get(0).totalEventSizeinBytes, + 1); + assert.strictEqual( + modelStats.traceEventStatsInTimeIntervals_.get(1).totalEventSizeinBytes, + 2); + assert.strictEqual( + modelStats.traceEventStatsInTimeIntervals_.get(2).totalEventSizeinBytes, + 7); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/model_test.html b/chromium/third_party/catapult/tracing/tracing/model/model_test.html new file mode 100644 index 00000000000..7613b421254 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/model_test.html @@ -0,0 +1,343 @@ +<!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/time_display_modes.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/importer/import.html"> +<link rel="import" href="/tracing/model/annotation.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ThreadSlice = tr.model.ThreadSlice; + const TitleOrCategoryFilter = tr.c.TitleOrCategoryFilter; + const Frame = tr.model.Frame; + + const createModelWithOneOfEverything = function() { + const m = new tr.Model(); + const cpu = m.kernel.getOrCreateCpu(1); + cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3})); + + const p = m.getOrCreateProcess(1); + const t = p.getOrCreateThread(1); + const slice = new ThreadSlice('', 'a', 0, 1, {}, 4); + t.sliceGroup.pushSlice(slice); + t.asyncSliceGroup.push(tr.c.TestUtils.newAsyncSlice(0, 1, t, t)); + + const c = p.getOrCreateCounter('', 'ProcessCounter'); + let aSeries = new tr.model.CounterSeries('a', 0); + let bSeries = new tr.model.CounterSeries('b', 0); + c.addSeries(aSeries); + c.addSeries(bSeries); + + aSeries.addCounterSample(0, 5); + aSeries.addCounterSample(1, 6); + aSeries.addCounterSample(2, 5); + aSeries.addCounterSample(3, 7); + + bSeries.addCounterSample(0, 10); + bSeries.addCounterSample(1, 15); + bSeries.addCounterSample(2, 12); + bSeries.addCounterSample(3, 16); + + const c1 = cpu.getOrCreateCounter('', 'CpuCounter'); + aSeries = new tr.model.CounterSeries('a', 0); + bSeries = new tr.model.CounterSeries('b', 0); + c1.addSeries(aSeries); + c1.addSeries(bSeries); + + aSeries.addCounterSample(0, 5); + aSeries.addCounterSample(1, 6); + aSeries.addCounterSample(2, 5); + aSeries.addCounterSample(3, 7); + + bSeries.addCounterSample(0, 10); + bSeries.addCounterSample(1, 15); + bSeries.addCounterSample(2, 12); + bSeries.addCounterSample(3, 16); + + const frame1 = new Frame([slice], [{thread: t, start: 1, end: 5}]); + p.frames.push.apply(p.frames, frame1); + + const gd = new tr.model.GlobalMemoryDump(m, 2); + const pd = new tr.model.ProcessMemoryDump(gd, p, 2); + gd.processMemoryDumps[1] = pd; + m.globalMemoryDumps.push(gd); + p.memoryDumps.push(pd); + + m.updateBounds(); + + return m; + }; + + test('helper', function() { + function Helper(model) { + this.model = model; + } + Helper.guid = tr.b.GUID.allocateSimple(); + Helper.supportsModel = function(model) { + return true; + }; + + const m = new tr.Model(); + const h = m.getOrCreateHelper(Helper); + assert.isTrue(h instanceof Helper); + assert.isTrue(h === m.getOrCreateHelper(Helper)); + + function UnsupportedHelper(model) { + this.model = model; + } + UnsupportedHelper.guid = tr.b.GUID.allocateSimple(); + UnsupportedHelper.supportsModel = function(model) { + return false; + }; + + assert.isUndefined(m.getOrCreateHelper(UnsupportedHelper)); + // Try again to test doesHelperGUIDSupportThisModel_ . + assert.isUndefined(m.getOrCreateHelper(UnsupportedHelper)); + }); + + test('modelBounds_EmptyModel', function() { + const m = new tr.Model(); + m.updateBounds(); + assert.isUndefined(m.bounds.min); + assert.isUndefined(m.bounds.max); + }); + + test('modelBounds_OneEmptyThread', function() { + const m = new tr.Model(); + const t = m.getOrCreateProcess(1).getOrCreateThread(1); + m.updateBounds(); + assert.isUndefined(m.bounds.min); + assert.isUndefined(m.bounds.max); + }); + + test('modelBounds_OneThread', function() { + const m = new tr.Model(); + const t = m.getOrCreateProcess(1).getOrCreateThread(1); + t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3)); + m.updateBounds(); + assert.strictEqual(m.bounds.min, 1); + assert.strictEqual(m.bounds.max, 4); + }); + + test('modelBounds_OneThreadAndOneEmptyThread', function() { + const m = new tr.Model(); + const t1 = m.getOrCreateProcess(1).getOrCreateThread(1); + t1.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3)); + const t2 = m.getOrCreateProcess(1).getOrCreateThread(1); + m.updateBounds(); + assert.strictEqual(m.bounds.min, 1); + assert.strictEqual(m.bounds.max, 4); + }); + + test('modelBounds_OneCpu', function() { + const m = new tr.Model(); + const cpu = m.kernel.getOrCreateCpu(1); + cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3})); + m.updateBounds(); + assert.strictEqual(m.bounds.min, 1); + assert.strictEqual(m.bounds.max, 4); + }); + + test('modelBounds_OneCpuOneThread', function() { + const m = new tr.Model(); + const cpu = m.kernel.getOrCreateCpu(1); + cpu.slices.push(tr.c.TestUtils.newSliceEx({start: 1, duration: 3})); + + const t = m.getOrCreateProcess(1).getOrCreateThread(1); + t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 4)); + + m.updateBounds(); + assert.strictEqual(m.bounds.min, 1); + assert.strictEqual(m.bounds.max, 5); + }); + + test('modelBounds_GlobalMemoryDumps', function() { + const m = new tr.Model(); + m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 1)); + m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 3)); + m.globalMemoryDumps.push(new tr.model.GlobalMemoryDump(m, 5)); + + m.updateBounds(); + assert.strictEqual(m.bounds.min, 1); + assert.strictEqual(m.bounds.max, 5); + }); + + test('modelBounds_ProcessMemoryDumps', function() { + const m = new tr.Model(); + const p = m.getOrCreateProcess(1); + const gd = new tr.model.GlobalMemoryDump(m, -1); + p.memoryDumps.push(new tr.model.ProcessMemoryDump(gd, m, 1)); + p.memoryDumps.push(new tr.model.ProcessMemoryDump(gd, m, 3)); + p.memoryDumps.push(new tr.model.ProcessMemoryDump(gd, m, 5)); + + m.updateBounds(); + assert.strictEqual(m.bounds.min, 1); + assert.strictEqual(m.bounds.max, 5); + }); + + + test('modelConvertsTimestampToModelTime', function() { + const m = new tr.Model(); + const traceEvents = [ + {ts: 1000, pid: 1, tid: 1, ph: 'B', cat: 'a', name: 'taskA', args: {}}, + {ts: 2000, pid: 1, tid: 1, ph: 'E', cat: 'a', name: 'taskA', args: {}} + ]; + const i = new tr.importer.Import(m); + i.importTraces([traceEvents]); + assert.strictEqual( + m.convertTimestampToModelTime('traceEventClock', 1000), 0); + assert.strictEqual( + m.convertTimestampToModelTime('traceEventClock', 2000), 1); + }); + + test('TitleOrCategoryFilter', function() { + const s0 = tr.c.TestUtils.newSliceEx({start: 1, duration: 3}); + assert.isTrue(new TitleOrCategoryFilter('a').matchSlice(s0)); + assert.isFalse(new TitleOrCategoryFilter('x').matchSlice(s0)); + + const s1 = tr.c.TestUtils.newSliceEx({title: 'ba', start: 1, duration: 3}); + assert.isTrue(new TitleOrCategoryFilter('a').matchSlice(s1)); + assert.isTrue(new TitleOrCategoryFilter('ba').matchSlice(s1)); + assert.isFalse(new TitleOrCategoryFilter('x').matchSlice(s1)); + }); + + test('model_findAllThreadsNamed', function() { + const m = new tr.Model(); + const t = m.getOrCreateProcess(1).getOrCreateThread(1); + t.name = 'CrBrowserMain'; + + m.updateBounds(); + let f = m.findAllThreadsNamed('CrBrowserMain'); + assert.deepEqual([t], f); + f = m.findAllThreadsNamed('NoSuchThread'); + assert.strictEqual(f.length, 0); + }); + + test('model_updateCategories', function() { + const m = new tr.Model(); + const t = m.getOrCreateProcess(1).getOrCreateThread(1); + t.sliceGroup.pushSlice(new ThreadSlice('categoryA', 'a', 0, 1, {}, 3)); + t.sliceGroup.pushSlice(new ThreadSlice('categoryA', 'a', 0, 1, {}, 3)); + t.sliceGroup.pushSlice(new ThreadSlice('categoryB', 'a', 0, 1, {}, 3)); + t.sliceGroup.pushSlice(new ThreadSlice('categoryA', 'a', 0, 1, {}, 3)); + t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3)); + m.updateCategories_(); + assert.deepEqual(['categoryA', 'categoryB'], m.categories); + }); + + test('getEventByStableId', function() { + const m = new tr.Model(); + const p = m.getOrCreateProcess(0); + const t = p.getOrCreateThread(1); + const slice = tr.c.TestUtils.newSliceEx({start: 0, duration: 10}); + t.sliceGroup.pushSlice(slice); + const ue = tr.c.TestUtils.newInteractionRecord(m, 0, 10); + m.userModel.expectations.push(ue); + const gie = tr.c.TestUtils.newInstantEvent({ + title: 'gie', + start: 0, + colorId: 0 + }); + m.instantEvents.push(gie); + + assert.strictEqual(slice, m.getEventByStableId(slice.stableId)); + assert.strictEqual(ue, m.getEventByStableId(ue.stableId)); + assert.strictEqual(gie, m.getEventByStableId(gie.stableId)); + }); + + test('model_annotationAddRemove', function() { + const m = new tr.Model(); + const a1 = new tr.model.Annotation(); + const a2 = new tr.model.Annotation(); + + assert.strictEqual(m.getAllAnnotations().length, 0); + m.addAnnotation(a1); + assert.strictEqual(m.getAllAnnotations().length, 1); + m.addAnnotation(a2); + assert.strictEqual(m.getAllAnnotations().length, 2); + + assert.strictEqual(m.getAnnotationByGUID(a1.guid), a1); + assert.strictEqual(m.getAnnotationByGUID(a2.guid), a2); + + m.removeAnnotation(a1); + assert.isUndefined(m.getAnnotationByGUID(a1.guid)); + assert.strictEqual(m.getAnnotationByGUID(a2.guid), a2); + assert.strictEqual(m.getAllAnnotations().length, 1); + }); + + test('model_intrinsicTimeUnit', function() { + const unit = tr.b.TimeDisplayModes; + const m = new tr.Model(); + + // by default it should be milliseconds + assert.strictEqual(m.intrinsicTimeUnit, unit.ms); + + m.intrinsicTimeUnit = unit.ns; + assert.strictEqual(m.intrinsicTimeUnit, unit.ns); + // should be able to set to the same + m.intrinsicTimeUnit = unit.ns; + assert.strictEqual(m.intrinsicTimeUnit, unit.ns); + // should not be able to change it after fixing it + assert.throw(function() { m.intrinsicTimeUnit = unit.ms; }); + assert.strictEqual(m.intrinsicTimeUnit, unit.ns); + }); + + test('model_getAllProcesses', function() { + const m = new tr.Model(); + const p1 = m.getOrCreateProcess(1); + const p2 = m.getOrCreateProcess(2); + const p3 = m.getOrCreateProcess(3); + const p4 = m.getOrCreateProcess(4); + const p5 = m.getOrCreateProcess(5); + + assert.sameMembers(m.getAllProcesses(), [p1, p2, p3, p4, p5]); + assert.sameMembers(m.getAllProcesses(p => true), [p1, p2, p3, p4, p5]); + assert.sameMembers(m.getAllProcesses(p => false), []); + assert.sameMembers(m.getAllProcesses(p => p.pid % 2 === 0), [p2, p4]); + }); + + test('model_joinRefs', function() { + function RefCountingSnapshot() { + tr.model.ObjectSnapshot.apply(this, arguments); + this.refCount = 0; + } + + RefCountingSnapshot.prototype = { + __proto__: tr.model.ObjectSnapshot.prototype, + + referencedAt() { + ++this.refCount; + } + }; + + const typeName = 'RefCountingSnapshot'; + tr.model.ObjectSnapshot.subTypes.register( + RefCountingSnapshot, + {typeName}); + + const m = new tr.Model(); + const p = m.getOrCreateProcess(1); + const s1 = p.objects.addSnapshot(new tr.model.ScopedId(typeName, '0x1'), + 'cat', typeName, 1000, {}); + const s2 = p.objects.addSnapshot(new tr.model.ScopedId(typeName, '0x2'), + 'cat', typeName, 2000, { + myRef: { + scope: typeName, + id_ref: '0x1' + } + }); + m.joinRefs(); + assert.strictEqual(s1.refCount, 1); + assert.strictEqual(s2.refCount, 0); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_collection.html b/chromium/third_party/catapult/tracing/tracing/model/object_collection.html new file mode 100644 index 00000000000..5b4ed2325dc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/object_collection.html @@ -0,0 +1,229 @@ +<!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/math/range.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_container.html"> +<link rel="import" href="/tracing/model/object_instance.html"> +<link rel="import" href="/tracing/model/time_to_object_instance_map.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the ObjectCollection class. + */ +tr.exportTo('tr.model', function() { + const ObjectInstance = tr.model.ObjectInstance; + const ObjectSnapshot = tr.model.ObjectSnapshot; + + /** + * A collection of object instances and their snapshots, accessible by id and + * time, or by object name. + * + * @constructor + */ + function ObjectCollection(parent) { + tr.model.EventContainer.call(this); + this.parent = parent; + // scope -> {id -> TimeToObjectInstanceMap} + this.instanceMapsByScopedId_ = {}; + this.instancesByTypeName_ = {}; + this.createObjectInstance_ = this.createObjectInstance_.bind(this); + } + + ObjectCollection.prototype = { + __proto__: tr.model.EventContainer.prototype, + + * childEvents() { + for (const instance of this.getAllObjectInstances()) { + yield instance; + yield* instance.snapshots; + } + }, + + createObjectInstance_( + parent, scopedId, category, name, creationTs, opt_baseTypeName) { + const constructor = tr.model.ObjectInstance.subTypes.getConstructor( + category, name); + const instance = new constructor( + parent, scopedId, category, name, creationTs, opt_baseTypeName); + const typeName = instance.typeName; + let instancesOfTypeName = this.instancesByTypeName_[typeName]; + if (!instancesOfTypeName) { + instancesOfTypeName = []; + this.instancesByTypeName_[typeName] = instancesOfTypeName; + } + instancesOfTypeName.push(instance); + return instance; + }, + + getOrCreateInstanceMap_(scopedId) { + let dict; + if (scopedId.scope in this.instanceMapsByScopedId_) { + dict = this.instanceMapsByScopedId_[scopedId.scope]; + } else { + dict = {}; + this.instanceMapsByScopedId_[scopedId.scope] = dict; + } + let instanceMap = dict[scopedId.id]; + if (instanceMap) return instanceMap; + instanceMap = new tr.model.TimeToObjectInstanceMap( + this.createObjectInstance_, this.parent, scopedId); + dict[scopedId.id] = instanceMap; + return instanceMap; + }, + + idWasCreated(scopedId, category, name, ts) { + const instanceMap = this.getOrCreateInstanceMap_(scopedId); + return instanceMap.idWasCreated(category, name, ts); + }, + + addSnapshot( + scopedId, category, name, ts, args, opt_baseTypeName) { + const instanceMap = this.getOrCreateInstanceMap_(scopedId); + const snapshot = instanceMap.addSnapshot( + category, name, ts, args, opt_baseTypeName); + if (snapshot.objectInstance.category !== category) { + const msg = 'Added snapshot name=' + name + ' with cat=' + category + + ' impossible. It instance was created/snapshotted with cat=' + + snapshot.objectInstance.category + ' name=' + + snapshot.objectInstance.name; + throw new Error(msg); + } + if (opt_baseTypeName && + snapshot.objectInstance.baseTypeName !== opt_baseTypeName) { + throw new Error('Could not add snapshot with baseTypeName=' + + opt_baseTypeName + '. It ' + + 'was previously created with name=' + + snapshot.objectInstance.baseTypeName); + } + if (snapshot.objectInstance.name !== name) { + throw new Error('Could not add snapshot with name=' + name + '. It ' + + 'was previously created with name=' + + snapshot.objectInstance.name); + } + return snapshot; + }, + + idWasDeleted(scopedId, category, name, ts) { + const instanceMap = this.getOrCreateInstanceMap_(scopedId); + const deletedInstance = instanceMap.idWasDeleted(category, name, ts); + if (!deletedInstance) return; + + if (deletedInstance.category !== category) { + const msg = 'Deleting object ' + deletedInstance.name + + ' with a different category ' + + 'than when it was created. It previous had cat=' + + deletedInstance.category + ' but the delete command ' + + 'had cat=' + category; + throw new Error(msg); + } + if (deletedInstance.baseTypeName !== name) { + throw new Error('Deletion requested for name=' + + name + ' could not proceed: ' + + 'An existing object with baseTypeName=' + + deletedInstance.baseTypeName + ' existed.'); + } + }, + + autoDeleteObjects(maxTimestamp) { + for (const imapById of Object.values(this.instanceMapsByScopedId_)) { + for (const i2imap of Object.values(imapById)) { + const lastInstance = i2imap.lastInstance; + if (lastInstance.deletionTs !== Number.MAX_VALUE) continue; + i2imap.idWasDeleted( + lastInstance.category, lastInstance.name, maxTimestamp); + // idWasDeleted will cause lastInstance.deletionTsWasExplicit to be + // set to true. Unset it here. + lastInstance.deletionTsWasExplicit = false; + } + } + }, + + getObjectInstanceAt(scopedId, ts) { + let instanceMap; + if (scopedId.scope in this.instanceMapsByScopedId_) { + instanceMap = this.instanceMapsByScopedId_[scopedId.scope][scopedId.id]; + } + if (!instanceMap) return undefined; + return instanceMap.getInstanceAt(ts); + }, + + getSnapshotAt(scopedId, ts) { + const instance = this.getObjectInstanceAt(scopedId, ts); + if (!instance) return undefined; + return instance.getSnapshotAt(ts); + }, + + iterObjectInstances(iter, opt_this) { + opt_this = opt_this || this; + for (const imapById of Object.values(this.instanceMapsByScopedId_)) { + for (const i2imap of Object.values(imapById)) { + i2imap.instances.forEach(iter, opt_this); + } + } + }, + + getAllObjectInstances() { + const instances = []; + this.iterObjectInstances(function(i) { instances.push(i); }); + return instances; + }, + + getAllInstancesNamed(name) { + return this.instancesByTypeName_[name]; + }, + + getAllInstancesByTypeName() { + return this.instancesByTypeName_; + }, + + preInitializeAllObjects() { + this.iterObjectInstances(function(instance) { + instance.preInitialize(); + }); + }, + + initializeAllObjects() { + this.iterObjectInstances(function(instance) { + instance.initialize(); + }); + }, + + initializeInstances() { + this.iterObjectInstances(function(instance) { + instance.initialize(); + }); + }, + + updateBounds() { + this.bounds.reset(); + this.iterObjectInstances(function(instance) { + instance.updateBounds(); + this.bounds.addRange(instance.bounds); + }, this); + }, + + shiftTimestampsForward(amount) { + this.iterObjectInstances(function(instance) { + instance.shiftTimestampsForward(amount); + }); + }, + + addCategoriesToDict(categoriesDict) { + this.iterObjectInstances(function(instance) { + categoriesDict[instance.category] = true; + }); + } + }; + + return { + ObjectCollection, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_collection_test.html b/chromium/third_party/catapult/tracing/tracing/model/object_collection_test.html new file mode 100644 index 00000000000..a04c23ff656 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/object_collection_test.html @@ -0,0 +1,230 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/object_collection.html"> +<link rel="import" href="/tracing/model/scoped_id.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TestObjectInstance = function( + parent, scopedId, category, name, creationTs) { + tr.model.ObjectInstance.call( + this, parent, scopedId, category, name, creationTs); + }; + + TestObjectInstance.prototype = { + __proto__: tr.model.ObjectInstance.prototype + }; + + test('objectInstanceSubtype', function() { + // Register that TestObjects are bound to TestObjectInstance. + tr.model.ObjectInstance.subTypes.register( + TestObjectInstance, + {typeName: 'TestObject'}); + + try { + const collection = new tr.model.ObjectCollection({ }); + const scopedId = new tr.model.ScopedId('ptr', '0x1000'); + collection.idWasCreated( + scopedId, 'tr.e.cc', 'Frame', 10); + collection.idWasDeleted( + scopedId, 'tr.e.cc', 'Frame', 15); + collection.idWasCreated( + scopedId, 'skia', 'TestObject', 20); + collection.idWasDeleted( + scopedId, 'skia', 'TestObject', 25); + + const testFrame = collection.getObjectInstanceAt(scopedId, 10); + assert.instanceOf(testFrame, tr.model.ObjectInstance); + assert.notInstanceOf(testFrame, TestObjectInstance); + + const testObject = collection.getObjectInstanceAt(scopedId, 20); + assert.instanceOf(testObject, tr.model.ObjectInstance); + assert.instanceOf(testObject, TestObjectInstance); + } finally { + tr.model.ObjectInstance.subTypes.unregister(TestObjectInstance); + } + }); + + test('twoSnapshots', function() { + const collection = new tr.model.ObjectCollection({}); + const scopedId = new tr.model.ScopedId('ptr', '0x1000'); + collection.idWasCreated( + scopedId, 'cat', 'Frame', 10); + collection.addSnapshot( + scopedId, 'cat', 'Frame', 10, {foo: 1}); + collection.addSnapshot( + scopedId, 'cat', 'Frame', 20, {foo: 2}); + + collection.updateBounds(); + assert.strictEqual(collection.bounds.min, 10); + assert.strictEqual(collection.bounds.max, 20); + + const s0 = collection.getSnapshotAt(scopedId, 1); + assert.isUndefined(s0); + + const s1 = collection.getSnapshotAt(scopedId, 10); + assert.strictEqual(s1.args.foo, 1); + + const s2 = collection.getSnapshotAt(scopedId, 15); + assert.strictEqual(s2.args.foo, 1); + assert.strictEqual(s1, s2); + + const s3 = collection.getSnapshotAt(scopedId, 20); + assert.strictEqual(s3.args.foo, 2); + assert.strictEqual(s1.object, s3.object); + + const s4 = collection.getSnapshotAt(scopedId, 25); + assert.strictEqual(s4, s3); + }); + + test('twoObjectsSharingOneID', function() { + const collection = new tr.model.ObjectCollection({}); + const scopedId = new tr.model.ScopedId('ptr', '0x1000'); + collection.idWasCreated( + scopedId, 'tr.e.cc', 'Frame', 10); + collection.idWasDeleted( + scopedId, 'tr.e.cc', 'Frame', 15); + collection.idWasCreated( + scopedId, 'skia', 'Picture', 20); + collection.idWasDeleted( + scopedId, 'skia', 'Picture', 25); + + const frame = collection.getObjectInstanceAt(scopedId, 10); + assert.strictEqual(frame.category, 'tr.e.cc'); + assert.strictEqual(frame.name, 'Frame'); + + const picture = collection.getObjectInstanceAt(scopedId, 20); + assert.strictEqual(picture.category, 'skia'); + assert.strictEqual(picture.name, 'Picture'); + + const typeNames = Object.keys(collection.getAllInstancesByTypeName()); + typeNames.sort(); + assert.deepEqual( + ['Frame', 'Picture'], + typeNames); + assert.deepEqual( + [frame], + collection.getAllInstancesByTypeName().Frame); + assert.deepEqual( + [picture], + collection.getAllInstancesByTypeName().Picture); + }); + + test('createSnapDelete', function() { + const collection = new tr.model.ObjectCollection({}); + const scopedId = new tr.model.ScopedId('ptr', '0x1000'); + collection.idWasCreated( + scopedId, 'cat', 'Frame', 10); + collection.addSnapshot( + scopedId, 'cat', 'Frame', 10, {foo: 1}); + collection.idWasDeleted( + scopedId, 'cat', 'Frame', 15); + + collection.updateBounds(); + assert.strictEqual(collection.bounds.min, 10); + assert.strictEqual(collection.bounds.max, 15); + + const s10 = collection.getSnapshotAt(scopedId, 10); + const i10 = s10.objectInstance; + assert.strictEqual(i10.creationTs, 10); + assert.strictEqual(i10.deletionTs, 15); + }); + + test('boundsOnUndeletedObject', function() { + const collection = new tr.model.ObjectCollection({}); + const scopedId = new tr.model.ScopedId('ptr', '0x1000'); + collection.idWasCreated( + scopedId, 'cat', 'Frame', 10); + collection.addSnapshot( + scopedId, 'cat', 'Frame', 15, {foo: 1}); + + collection.updateBounds(); + assert.strictEqual(10, collection.bounds.min); + assert.strictEqual(15, collection.bounds.max); + }); + + test('snapshotWithCustomBaseTypeThenDelete', function() { + const collection = new tr.model.ObjectCollection({}); + const scopedId = new tr.model.ScopedId('ptr', '0x1000'); + const s10 = collection.addSnapshot( + scopedId, 'cat', 'cc::PictureLayerImpl', 10, {}, 'cc::LayerImpl'); + collection.idWasDeleted( + scopedId, 'cat', 'cc::LayerImpl', 15); + collection.updateBounds(); + assert.strictEqual(10, collection.bounds.min); + assert.strictEqual(15, collection.bounds.max); + assert.strictEqual(s10.objectInstance.name, 'cc::PictureLayerImpl'); + assert.strictEqual(s10.objectInstance.baseTypeName, 'cc::LayerImpl'); + }); + + test('newWithSnapshotThatChangesBaseType', function() { + const collection = new tr.model.ObjectCollection({}); + const scopedId = new tr.model.ScopedId('ptr', '0x1000'); + const i10 = collection.idWasCreated( + scopedId, 'cat', 'cc::LayerImpl', 10); + const s15 = collection.addSnapshot( + scopedId, 'cat', 'cc::PictureLayerImpl', 15, {}, 'cc::LayerImpl'); + collection.updateBounds(); + assert.strictEqual(10, collection.bounds.min); + assert.strictEqual(15, collection.bounds.max); + assert.strictEqual(s15.objectInstance, i10); + assert.strictEqual(i10.name, 'cc::PictureLayerImpl'); + assert.strictEqual(i10.baseTypeName, 'cc::LayerImpl'); + }); + + test('deleteThenSnapshotWithCustomBase', function() { + const collection = new tr.model.ObjectCollection({}); + const scopedId = new tr.model.ScopedId('ptr', '0x1000'); + collection.idWasDeleted( + scopedId, 'cat', 'cc::LayerImpl', 10); + const s15 = collection.addSnapshot( + scopedId, 'cat', 'cc::PictureLayerImpl', 15, {}, 'cc::LayerImpl'); + collection.updateBounds(); + assert.strictEqual(10, collection.bounds.min); + assert.strictEqual(15, collection.bounds.max); + assert.strictEqual(s15.objectInstance.name, 'cc::PictureLayerImpl'); + }); + + test('autoDelete', function() { + const collection = new tr.model.ObjectCollection({}); + const scopedId = new tr.model.ScopedId('ptr', '0x1000'); + collection.idWasCreated( + scopedId, 'cat', 'Frame', 10); + collection.addSnapshot( + scopedId, 'cat', 'Frame', 10, {foo: 1}); + collection.autoDeleteObjects(15); + + const s10 = collection.getSnapshotAt(scopedId, 10); + const i10 = s10.objectInstance; + assert.strictEqual(15, i10.deletionTs); + }); + + test('differentScopes', function() { + const collection = new tr.model.ObjectCollection({}); + const scopedId1 = new tr.model.ScopedId('ptr', '0x1000'); + const scopedId2 = new tr.model.ScopedId('cc', '0x1000'); + collection.idWasCreated( + scopedId1, 'cat', 'ptr::object', 10); + collection.idWasDeleted( + scopedId1, 'cat', 'ptr::object', 15); + collection.idWasCreated( + scopedId2, 'cat', 'cc::object', 10); + collection.idWasDeleted( + scopedId2, 'cat', 'cc::object', 15); + + let instance = collection.getObjectInstanceAt(scopedId1, 10); + assert.strictEqual(instance.name, 'ptr::object'); + + instance = collection.getObjectInstanceAt(scopedId2, 10); + assert.strictEqual(instance.name, 'cc::object'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_instance.html b/chromium/third_party/catapult/tracing/tracing/model/object_instance.html new file mode 100644 index 00000000000..659ed22ad11 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/object_instance.html @@ -0,0 +1,213 @@ +<!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/math/range.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/model/object_snapshot.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the ObjectSnapshot and ObjectHistory classes. + */ +tr.exportTo('tr.model', function() { + const ObjectSnapshot = tr.model.ObjectSnapshot; + + /** + * An object with a specific id, whose state has been snapshotted several + * times. + * + * @constructor + */ + function ObjectInstance( + parent, scopedId, category, name, creationTs, opt_baseTypeName) { + tr.model.Event.call(this); + this.parent = parent; + this.scopedId = scopedId; + this.category = category; + this.baseTypeName = opt_baseTypeName ? opt_baseTypeName : name; + this.name = name; + this.creationTs = creationTs; + this.creationTsWasExplicit = false; + this.deletionTs = Number.MAX_VALUE; + this.deletionTsWasExplicit = false; + this.colorId = 0; + this.bounds = new tr.b.math.Range(); + this.snapshots = []; + this.hasImplicitSnapshots = false; + } + + ObjectInstance.prototype = { + __proto__: tr.model.Event.prototype, + + get typeName() { + return this.name; + }, + + addBoundsToRange(range) { + range.addRange(this.bounds); + }, + + addSnapshot(ts, args, opt_name, opt_baseTypeName) { + if (ts < this.creationTs) { + throw new Error('Snapshots must be >= instance.creationTs'); + } + if (ts >= this.deletionTs) { + throw new Error('Snapshots cannot be added after ' + + 'an objects deletion timestamp.'); + } + + let lastSnapshot; + if (this.snapshots.length > 0) { + lastSnapshot = this.snapshots[this.snapshots.length - 1]; + if (lastSnapshot.ts === ts) { + throw new Error('Snapshots already exists at this time!'); + } + if (ts < lastSnapshot.ts) { + throw new Error( + 'Snapshots must be added in increasing timestamp order'); + } + } + + // Update baseTypeName if needed. + if (opt_name && + (this.name !== opt_name)) { + if (!opt_baseTypeName) { + throw new Error('Must provide base type name for name update'); + } + if (this.baseTypeName !== opt_baseTypeName) { + throw new Error('Cannot update type name: base types dont match'); + } + this.name = opt_name; + } + + const snapshotConstructor = + tr.model.ObjectSnapshot.subTypes.getConstructor( + this.category, this.name); + const snapshot = new snapshotConstructor(this, ts, args); + this.snapshots.push(snapshot); + return snapshot; + }, + + wasDeleted(ts) { + let lastSnapshot; + if (this.snapshots.length > 0) { + lastSnapshot = this.snapshots[this.snapshots.length - 1]; + if (lastSnapshot.ts > ts) { + throw new Error( + 'Instance cannot be deleted at ts=' + + ts + '. A snapshot exists that is older.'); + } + } + this.deletionTs = ts; + this.deletionTsWasExplicit = true; + }, + + /** + * See ObjectSnapshot constructor notes on object initialization. + */ + preInitialize() { + for (let i = 0; i < this.snapshots.length; i++) { + this.snapshots[i].preInitialize(); + } + }, + + /** + * See ObjectSnapshot constructor notes on object initialization. + */ + initialize() { + for (let i = 0; i < this.snapshots.length; i++) { + this.snapshots[i].initialize(); + } + }, + + isAliveAt(ts) { + if (ts < this.creationTs && this.creationTsWasExplicit) { + return false; + } + if (ts > this.deletionTs) { + return false; + } + + return true; + }, + + getSnapshotAt(ts) { + if (ts < this.creationTs) { + if (this.creationTsWasExplicit) { + throw new Error('ts must be within lifetime of this instance'); + } + return this.snapshots[0]; + } + if (ts > this.deletionTs) { + throw new Error('ts must be within lifetime of this instance'); + } + + const snapshots = this.snapshots; + const i = tr.b.findIndexInSortedIntervals( + snapshots, + function(snapshot) { return snapshot.ts; }, + function(snapshot, i) { + if (i === snapshots.length - 1) { + return snapshots[i].objectInstance.deletionTs; + } + return snapshots[i + 1].ts - snapshots[i].ts; + }, + ts); + if (i < 0) { + // Note, this is a little bit sketchy: this lets early ts point at the + // first snapshot, even before it is taken. We do this because raster + // tasks usually post before their tile snapshots are dumped. This may + // be a good line of code to re-visit if we start seeing strange and + // confusing object references showing up in the traces. + return this.snapshots[0]; + } + if (i >= this.snapshots.length) { + return this.snapshots[this.snapshots.length - 1]; + } + return this.snapshots[i]; + }, + + updateBounds() { + this.bounds.reset(); + this.bounds.addValue(this.creationTs); + if (this.deletionTs !== Number.MAX_VALUE) { + this.bounds.addValue(this.deletionTs); + } else if (this.snapshots.length > 0) { + this.bounds.addValue(this.snapshots[this.snapshots.length - 1].ts); + } + }, + + shiftTimestampsForward(amount) { + this.creationTs += amount; + if (this.deletionTs !== Number.MAX_VALUE) { + this.deletionTs += amount; + } + this.snapshots.forEach(function(snapshot) { + snapshot.ts += amount; + }); + }, + + get userFriendlyName() { + return this.typeName + ' object ' + this.scopedId; + } + }; + + tr.model.EventRegistry.register( + ObjectInstance, + { + name: 'objectInstance', + pluralName: 'objectInstances' + }); + + return { + ObjectInstance, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_instance_test.html b/chromium/third_party/catapult/tracing/tracing/model/object_instance_test.html new file mode 100644 index 00000000000..703e98d37c0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/object_instance_test.html @@ -0,0 +1,70 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/object_instance.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('getSnapshotAtWithImplicitCreation', function() { + const instance = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'n', 10); + let s10 = instance.addSnapshot(10, 'a'); + instance.addSnapshot(40, 'b'); + instance.wasDeleted(60); + + const s1 = instance.getSnapshotAt(1); + assert.strictEqual(s1, s10); + + s10 = instance.getSnapshotAt(10); + assert.strictEqual(s10.args, 'a'); + assert.strictEqual(instance.getSnapshotAt(15), s10); + + const s40 = instance.getSnapshotAt(40); + assert.strictEqual(s40.args, 'b'); + assert.strictEqual(instance.getSnapshotAt(50), s40); + assert.strictEqual(instance.getSnapshotAt(59.9), s40); + }); + + test('getSnapshotAtWithExplicitCreation', function() { + const instance = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'n', 10); + instance.creationTsWasExplicit = true; + instance.addSnapshot(10, 'a'); + instance.wasDeleted(60); + + assert.throws(function() { + instance.getSnapshotAt(1); + }); + + const s10 = instance.getSnapshotAt(10); + assert.strictEqual(s10.args, 'a'); + assert.strictEqual(instance.getSnapshotAt(15), s10); + }); + + test('getSnapshotBeforeFirstSnapshot', function() { + const instance = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'n', 10); + const s15 = instance.addSnapshot(15, 'a'); + instance.wasDeleted(40); + + assert.strictEqual(instance.getSnapshotAt(10), s15); + }); + + test('getSnapshotAfterLastSnapshot', function() { + const instance = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'n', 10); + const s15 = instance.addSnapshot(15, 'a'); + instance.wasDeleted(40); + + assert.strictEqual(instance.getSnapshotAt(20), s15); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_snapshot.html b/chromium/third_party/catapult/tracing/tracing/model/object_snapshot.html new file mode 100644 index 00000000000..09678f34a45 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/object_snapshot.html @@ -0,0 +1,90 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/model/event.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + /** + * A snapshot of an object instance, at a given moment in time. + * + * Initialization of snapshots and instances is three phased: + * + * 1. Instances and snapshots are constructed. This happens during event + * importing. Little should be done here, because the object's data + * are still being used by the importer to reconstruct object references. + * + * 2. Instances and snapshtos are preinitialized. This happens after implicit + * objects have been found, but before any references have been found and + * switched to direct references. Thus, every snapshot stands on its own. + * This is a good time to do global field renaming and type conversion, + * e.g. recognizing domain-specific types and converting from C++ naming + * convention to JS. + * + * 3. Instances and snapshtos are initialized. At this point, {id_ref: + * '0x1000'} fields have been converted to snapshot references. This is a + * good time to generic initialization steps and argument verification. + * + * @constructor + */ + function ObjectSnapshot(objectInstance, ts, args) { + tr.model.Event.call(this); + this.objectInstance = objectInstance; + this.ts = ts; + this.args = args; + } + + ObjectSnapshot.prototype = { + __proto__: tr.model.Event.prototype, + + /** + * See ObjectSnapshot constructor notes on object initialization. + */ + preInitialize() { + }, + + /** + * See ObjectSnapshot constructor notes on object initialization. + */ + initialize() { + }, + + /** + * Called when an object reference is resolved as this ObjectSnapshot. + * @param {Object} item The event (async slice, slice or object) containing + * the resolved reference. + * @param {Object} object The object directly containing the reference. + * @param {String} field The field name of the reference in |object|. + */ + referencedAt(item, object, field) { + }, + + addBoundsToRange(range) { + range.addValue(this.ts); + }, + + get userFriendlyName() { + return 'Snapshot of ' + this.objectInstance.userFriendlyName + ' @ ' + + tr.b.Unit.byName.timeStampInMs.format(this.ts); + } + }; + + tr.model.EventRegistry.register( + ObjectSnapshot, + { + name: 'objectSnapshot', + pluralName: 'objectSnapshots' + }); + + return { + ObjectSnapshot, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/object_snapshot_test.html b/chromium/third_party/catapult/tracing/tracing/model/object_snapshot_test.html new file mode 100644 index 00000000000..c026851e5aa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/object_snapshot_test.html @@ -0,0 +1,40 @@ +<!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/model/object_instance.html"> +<link rel="import" href="/tracing/model/object_snapshot.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('snapshotTypeRegistry', function() { + function MySnapshot() { + tr.model.ObjectSnapshot.apply(this, arguments); + this.myFoo = this.args.foo; + } + + MySnapshot.prototype = { + __proto__: tr.model.ObjectSnapshot.prototype + }; + + const instance = new tr.model.ObjectInstance( + {}, '0x1000', 'cat', 'MySnapshot', 10); + try { + tr.model.ObjectSnapshot.subTypes.register( + MySnapshot, + {typeName: 'MySnapshot'}); + const snapshot = instance.addSnapshot(15, {foo: 'bar'}); + assert.instanceOf(snapshot, MySnapshot); + assert.strictEqual(snapshot.myFoo, 'bar'); + } finally { + tr.model.ObjectSnapshot.subTypes.unregister(MySnapshot); + } + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/power_sample.html b/chromium/third_party/catapult/tracing/tracing/model/power_sample.html new file mode 100644 index 00000000000..b9816f76159 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/power_sample.html @@ -0,0 +1,71 @@ +<!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/event.html"> +<link rel="import" href="/tracing/model/event_registry.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const Event = tr.model.Event; + const EventRegistry = tr.model.EventRegistry; + + /** + * A sample that contains a power measurement (in W). + * + * @constructor + * @extends {Event} + */ + function PowerSample(series, start, powerInW) { + Event.call(this); + + this.series_ = series; + this.start_ = parseFloat(start); + this.powerInW_ = parseFloat(powerInW); + } + + PowerSample.prototype = { + __proto__: Event.prototype, + + get series() { + return this.series_; + }, + + get start() { + return this.start_; + }, + + set start(value) { + this.start_ = value; + }, + + get powerInW() { + return this.powerInW_; + }, + + set powerInW(value) { + this.powerInW_ = value; + }, + + addBoundsToRange(range) { + range.addValue(this.start); + } + }; + + EventRegistry.register( + PowerSample, + { + name: 'powerSample', + pluralName: 'powerSamples' + }); + + return { + PowerSample, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/power_sample_test.html b/chromium/third_party/catapult/tracing/tracing/model/power_sample_test.html new file mode 100644 index 00000000000..ebed90bd2b5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/power_sample_test.html @@ -0,0 +1,51 @@ +<!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.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/power_sample.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const PowerSample = tr.model.PowerSample; + + test('powerSample', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const sample1 = new PowerSample(series, 0.0, 1000.0); + const sample2 = new PowerSample(series, 1.0, 2000.0); + + assert.strictEqual(sample1.series, series); + assert.strictEqual(sample1.start, 0.0); + assert.strictEqual(sample1.powerInW, 1000.0); + + assert.strictEqual(sample2.series, series); + assert.strictEqual(sample2.start, 1.0); + assert.strictEqual(sample2.powerInW, 2000.0); + }); + + test('addBoundsToRange', function() { + const series = new tr.model.PowerSeries(new tr.Model().device); + + const sample1 = new PowerSample(series, 0.0, 1000.0); + const sample2 = new PowerSample(series, 1.0, 2000.0); + + const range = new tr.b.math.Range(); + sample1.addBoundsToRange(range); + + assert.strictEqual(range.min, 0); + assert.strictEqual(range.max, 0); + + sample2.addBoundsToRange(range); + + assert.strictEqual(range.min, 0); + assert.strictEqual(range.max, 1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/power_series.html b/chromium/third_party/catapult/tracing/tracing/model/power_series.html new file mode 100644 index 00000000000..c496b1e4810 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/power_series.html @@ -0,0 +1,129 @@ +<!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.html"> +<link rel="import" href="/tracing/base/unit_scale.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_container.html"> +<link rel="import" href="/tracing/model/power_sample.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const PowerSample = tr.model.PowerSample; + + /** + * A container holding a time series of power samples. + * + * @constructor + * @extends {EventContainer} + */ + function PowerSeries(device) { + tr.model.EventContainer.call(this); + + this.device_ = device; + this.samples_ = []; + } + + PowerSeries.prototype = { + __proto__: tr.model.EventContainer.prototype, + + get device() { + return this.device_; + }, + + get samples() { + return this.samples_; + }, + + get stableId() { + return this.device_.stableId + '.PowerSeries'; + }, + + /** + * Adds a power sample to the series and returns it. + * + * Note: Samples must be added in chronological order. + */ + addPowerSample(ts, val) { + const sample = new PowerSample(this, ts, val); + this.samples_.push(sample); + return sample; + }, + + /** + * Returns the total energy (in Joules) consumed between the specified + * start and end timestamps (in milliseconds). + */ + getEnergyConsumedInJ(start, end) { + const measurementRange = tr.b.math.Range.fromExplicitRange(start, end); + + let energyConsumedInJ = 0; + let startIndex = tr.b.findLowIndexInSortedArray( + this.samples, x => x.start, start) - 1; + const endIndex = tr.b.findLowIndexInSortedArray( + this.samples, x => x.start, end); + + if (startIndex < 0) { + startIndex = 0; + } + + for (let i = startIndex; i < endIndex; i++) { + const sample = this.samples[i]; + const nextSample = this.samples[i + 1]; + + const sampleRange = new tr.b.math.Range(); + sampleRange.addValue(sample.start); + sampleRange.addValue(nextSample ? nextSample.start : sample.start); + + const intersectionRangeInMs = measurementRange.findIntersection( + sampleRange); + + const durationInS = tr.b.convertUnit(intersectionRangeInMs.duration, + tr.b.UnitPrefixScale.METRIC.MILLI, + tr.b.UnitPrefixScale.METRIC.NONE); + + energyConsumedInJ += durationInS * sample.powerInW; + } + + return energyConsumedInJ; + }, + + getSamplesWithinRange(start, end) { + const startIndex = tr.b.findLowIndexInSortedArray( + this.samples, x => x.start, start); + const endIndex = tr.b.findLowIndexInSortedArray( + this.samples, x => x.start, end); + return this.samples.slice(startIndex, endIndex); + }, + + shiftTimestampsForward(amount) { + for (let i = 0; i < this.samples_.length; ++i) { + this.samples_[i].start += amount; + } + }, + + updateBounds() { + this.bounds.reset(); + + if (this.samples_.length === 0) return; + + this.bounds.addValue(this.samples_[0].start); + this.bounds.addValue(this.samples_[this.samples_.length - 1].start); + }, + + * childEvents() { + yield* this.samples_; + }, + }; + + return { + PowerSeries, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/power_series_test.html b/chromium/third_party/catapult/tracing/tracing/model/power_series_test.html new file mode 100644 index 00000000000..aa1e1c46d54 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/power_series_test.html @@ -0,0 +1,151 @@ +<!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/device.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/power_series.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const PowerSeries = tr.model.PowerSeries; + + test('stableId', function() { + const device = { stableId: 'test' }; + const series = new PowerSeries(device); + + assert.strictEqual(series.stableId, 'test.PowerSeries'); + }); + + test('device', function() { + const device = new tr.model.Device(new Model()); + const series = new PowerSeries(device); + + assert.strictEqual(series.device, device); + }); + + test('addPowerSample', function() { + const series = new PowerSeries(new Model().device); + + assert.strictEqual(series.samples.length, 0); + + const sample1 = series.addPowerSample(0, 1); + const sample2 = series.addPowerSample(1, 2); + + assert.deepEqual(series.samples, [sample1, sample2]); + }); + + test('getEnergyConsumed_oneInterval', function() { + const series = new PowerSeries(new Model().device); + series.addPowerSample(0, 1); + series.addPowerSample(1000, 2); + + assert.strictEqual(series.getEnergyConsumedInJ(0, 1000), 1); + }); + + test('getEnergyConsumed_twoIntervals', function() { + const series = new PowerSeries(new Model().device); + series.addPowerSample(0, 1); + series.addPowerSample(1000, 2); + series.addPowerSample(2000, 2); + + assert.strictEqual(series.getEnergyConsumedInJ(0, 2000), 3); + }); + + test('getEnergyConsumed_oneSample', function() { + const series = new PowerSeries(new Model().device); + series.addPowerSample(1000, 1); + + assert.strictEqual(series.getEnergyConsumedInJ(0, 2000), 0); + }); + + test('getEnergyConsumed_samplesAfterStart', function() { + const series = new PowerSeries(new Model().device); + series.addPowerSample(1000, 1); + series.addPowerSample(2000, 2); + + assert.strictEqual(series.getEnergyConsumedInJ(0, 2000), 1); + }); + + test('getEnergyConsumed_extraSamplesBeforeStart', function() { + const series = new PowerSeries(new Model().device); + series.addPowerSample(0, 10); + series.addPowerSample(1000, 1); + series.addPowerSample(2000, 1); + series.addPowerSample(3000, 1); + + assert.strictEqual(series.getEnergyConsumedInJ(2000, 4000), 1); + }); + + test('getEnergyConsumed_extraSamplesAfterEnd', function() { + const series = new PowerSeries(new Model().device); + series.addPowerSample(0, 1); + series.addPowerSample(1000, 1); + series.addPowerSample(2000, 1); + series.addPowerSample(3000, 10); + + assert.strictEqual(series.getEnergyConsumedInJ(0, 2000), 2); + }); + + test('shiftTimestampsForward', function() { + const series = new PowerSeries(new Model().device); + + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + + series.shiftTimestampsForward(2); + + assert.strictEqual(series.samples[0].start, 2); + assert.strictEqual(series.samples[1].start, 3); + + series.shiftTimestampsForward(-4); + + assert.strictEqual(series.samples[0].start, -2); + assert.strictEqual(series.samples[1].start, -1); + }); + + + test('updateBounds', function() { + const series = new PowerSeries(new Model().device); + + series.addPowerSample(0, 1); + series.addPowerSample(1, 2); + series.updateBounds(); + + assert.strictEqual(series.bounds.min, 0); + assert.strictEqual(series.bounds.max, 1); + + series.addPowerSample(4, 3); + series.updateBounds(); + + assert.strictEqual(series.bounds.min, 0); + assert.strictEqual(series.bounds.max, 4); + }); + + test('childEvents_empty', function() { + const series = new PowerSeries(new Model().device); + const eventsInSeries = []; + for (const event of series.childEvents()) { + eventsInSeries.push(event); + } + assert.deepEqual(eventsInSeries, []); + }); + + test('childEvents_nonempty', function() { + const series = new PowerSeries(new Model().device); + const sample1 = series.addPowerSample(0, 1); + const sample2 = series.addPowerSample(1, 2); + const eventsInSeries = []; + for (const event of series.childEvents()) { + eventsInSeries.push(event); + } + assert.deepEqual(eventsInSeries, [sample1, sample2]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/process.html b/chromium/third_party/catapult/tracing/tracing/model/process.html new file mode 100644 index 00000000000..facaa3b1923 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/process.html @@ -0,0 +1,175 @@ +<!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/model/process_base.html"> +<link rel="import" href="/tracing/model/process_memory_dump.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Process class. + */ +tr.exportTo('tr.model', function() { + const ProcessBase = tr.model.ProcessBase; + const ProcessInstantEvent = tr.model.ProcessInstantEvent; + const Frame = tr.model.Frame; + const ProcessMemoryDump = tr.model.ProcessMemoryDump; + + /** + * The Process represents a single userland process in the + * trace. + * @constructor + */ + function Process(model, pid) { + if (model === undefined) { + throw new Error('model must be provided'); + } + if (pid === undefined) { + throw new Error('pid must be provided'); + } + tr.model.ProcessBase.call(this, model); + this.pid = pid; + this.name = undefined; + this.labels = []; + this.uptime_seconds = 0; + this.instantEvents = []; + this.memoryDumps = []; + this.frames = []; + this.activities = []; + } + + /** + * Comparison between processes that orders by pid. + */ + Process.compare = function(x, y) { + let tmp = tr.model.ProcessBase.compare(x, y); + if (tmp) return tmp; + + if (x.name !== undefined) { + if (y.name !== undefined) { + tmp = x.name.localeCompare(y.name); + } else { + tmp = -1; + } + } else if (y.name !== undefined) { + tmp = 1; + } + if (tmp) return tmp; + + tmp = tr.b.compareArrays(x.labels, y.labels, + function(x, y) { return x.localeCompare(y); }); + if (tmp) return tmp; + + return x.pid - y.pid; + }; + + Process.prototype = { + __proto__: tr.model.ProcessBase.prototype, + + get stableId() { + return this.pid; + }, + + compareTo(that) { + return Process.compare(this, that); + }, + + * childEvents() { + yield* ProcessBase.prototype.childEvents.call(this); + yield* this.instantEvents; + yield* this.frames; + yield* this.memoryDumps; + }, + + addLabelIfNeeded(labelName) { + for (let i = 0; i < this.labels.length; i++) { + if (this.labels[i] === labelName) return; + } + this.labels.push(labelName); + }, + + get userFriendlyName() { + let res; + if (this.name) { + res = this.name + ' (pid ' + this.pid + ')'; + } else { + res = 'Process ' + this.pid; + } + if (this.labels.length) { + res += ': ' + this.labels.join(', '); + } + if (this.uptime_seconds) { + res += ', uptime:' + this.uptime_seconds + 's'; + } + return res; + }, + + get userFriendlyDetails() { + if (this.name) { + return this.name + ' (pid ' + this.pid + ')'; + } + return 'pid: ' + this.pid; + }, + + getSettingsKey() { + if (!this.name) return undefined; + if (!this.labels.length) return 'processes.' + this.name; + return 'processes.' + this.name + '.' + this.labels.join('.'); + }, + + shiftTimestampsForward(amount) { + for (let i = 0; i < this.instantEvents.length; i++) { + this.instantEvents[i].start += amount; + } + + for (let i = 0; i < this.frames.length; i++) { + this.frames[i].shiftTimestampsForward(amount); + } + + for (let i = 0; i < this.memoryDumps.length; i++) { + this.memoryDumps[i].shiftTimestampsForward(amount); + } + + for (let i = 0; i < this.activities.length; i++) { + this.activities[i].shiftTimestampsForward(amount); + } + + tr.model.ProcessBase.prototype + .shiftTimestampsForward.apply(this, arguments); + }, + + updateBounds() { + tr.model.ProcessBase.prototype.updateBounds.apply(this); + + for (let i = 0; i < this.frames.length; i++) { + this.frames[i].addBoundsToRange(this.bounds); + } + + for (let i = 0; i < this.memoryDumps.length; i++) { + this.memoryDumps[i].addBoundsToRange(this.bounds); + } + + for (let i = 0; i < this.activities.length; i++) { + this.activities[i].addBoundsToRange(this.bounds); + } + }, + + sortMemoryDumps() { + this.memoryDumps.sort(function(x, y) { + return x.start - y.start; + }); + tr.model.ProcessMemoryDump.hookUpMostRecentVmRegionsLinks( + this.memoryDumps); + } + }; + + return { + Process, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/process_base.html b/chromium/third_party/catapult/tracing/tracing/model/process_base.html new file mode 100644 index 00000000000..8ba031a764c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/process_base.html @@ -0,0 +1,244 @@ +<!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/guid.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/model/counter.html"> +<link rel="import" href="/tracing/model/event_container.html"> +<link rel="import" href="/tracing/model/object_collection.html"> +<link rel="import" href="/tracing/model/thread.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the ProcessBase class. + */ +tr.exportTo('tr.model', function() { + const Thread = tr.model.Thread; + const Counter = tr.model.Counter; + + /** + * The ProcessBase is a partial base class, upon which Kernel + * and Process are built. + * + * @constructor + * @extends {tr.model.EventContainer} + */ + function ProcessBase(model) { + if (!model) { + throw new Error('Must provide a model'); + } + tr.model.EventContainer.call(this); + this.model = model; + this.threads = {}; + this.counters = {}; + this.objects = new tr.model.ObjectCollection(this); + this.sortIndex = 0; + } + + ProcessBase.compare = function(x, y) { + return x.sortIndex - y.sortIndex; + }; + + ProcessBase.prototype = { + __proto__: tr.model.EventContainer.prototype, + + get stableId() { + throw new Error('Not implemented'); + }, + + * childEventContainers() { + yield* Object.values(this.threads); + yield* Object.values(this.counters); + yield this.objects; + }, + + iterateAllPersistableObjects(cb) { + cb(this); + for (const tid in this.threads) { + this.threads[tid].iterateAllPersistableObjects(cb); + } + }, + + /** + * Gets the number of threads in this process. + */ + get numThreads() { + let n = 0; + for (const p in this.threads) { + n++; + } + return n; + }, + + /** + * Shifts all the timestamps inside this process forward by the amount + * specified. + */ + shiftTimestampsForward(amount) { + for (const child of this.childEventContainers()) { + child.shiftTimestampsForward(amount); + } + }, + + /** + * Closes any open slices. + */ + autoCloseOpenSlices() { + for (const tid in this.threads) { + const thread = this.threads[tid]; + thread.autoCloseOpenSlices(); + } + }, + + autoDeleteObjects(maxTimestamp) { + this.objects.autoDeleteObjects(maxTimestamp); + }, + + /** + * Called by the model after finalizing imports, + * but before joining refs. + */ + preInitializeObjects() { + this.objects.preInitializeAllObjects(); + }, + + /** + * Called by the model after joining refs. + */ + initializeObjects() { + this.objects.initializeAllObjects(); + }, + + /** + * Merge slices from the kernel with those from userland for each thread. + */ + mergeKernelWithUserland() { + for (const tid in this.threads) { + const thread = this.threads[tid]; + thread.mergeKernelWithUserland(); + } + }, + + updateBounds() { + this.bounds.reset(); + for (const tid in this.threads) { + this.threads[tid].updateBounds(); + this.bounds.addRange(this.threads[tid].bounds); + } + for (const id in this.counters) { + this.counters[id].updateBounds(); + this.bounds.addRange(this.counters[id].bounds); + } + this.objects.updateBounds(); + this.bounds.addRange(this.objects.bounds); + }, + + addCategoriesToDict(categoriesDict) { + for (const tid in this.threads) { + this.threads[tid].addCategoriesToDict(categoriesDict); + } + for (const id in this.counters) { + categoriesDict[this.counters[id].category] = true; + } + this.objects.addCategoriesToDict(categoriesDict); + }, + + findAllThreadsMatching(predicate, opt_this) { + const threads = []; + for (const tid in this.threads) { + const thread = this.threads[tid]; + if (predicate.call(opt_this, thread)) { + threads.push(thread); + } + } + return threads; + }, + + /** + * @param {String} The name of the thread to find. + * @return {Array} An array of all the matched threads. + */ + findAllThreadsNamed(name) { + const threads = this.findAllThreadsMatching(function(thread) { + if (!thread.name) return false; + return thread.name === name; + }); + return threads; + }, + + findAtMostOneThreadNamed(name) { + const threads = this.findAllThreadsNamed(name); + if (threads.length === 0) return undefined; + if (threads.length > 1) { + throw new Error('Expected no more than one ' + name); + } + return threads[0]; + }, + + /** + * Removes threads from the process that are fully empty. + */ + pruneEmptyContainers() { + const threadsToKeep = {}; + for (const tid in this.threads) { + const thread = this.threads[tid]; + if (!thread.isEmpty) { + threadsToKeep[tid] = thread; + } + } + this.threads = threadsToKeep; + }, + + /** + * @return {TimelineThread} The thread identified by tid on this process, + * or undefined if it doesn't exist. + */ + getThread(tid) { + return this.threads[tid]; + }, + + /** + * @return {TimelineThread} The thread identified by tid on this process, + * creating it if it doesn't exist. + */ + getOrCreateThread(tid) { + if (!this.threads[tid]) { + this.threads[tid] = new Thread(this, tid); + } + return this.threads[tid]; + }, + + /** + * @return {Counter} The counter on this process with the given + * category/name combination, creating it if it doesn't exist. + */ + getOrCreateCounter(cat, name) { + const id = cat + '.' + name; + if (!this.counters[id]) { + this.counters[id] = new Counter(this, id, cat, name); + } + return this.counters[id]; + }, + + getSettingsKey() { + throw new Error('Not implemented'); + }, + + createSubSlices() { + for (const tid in this.threads) { + this.threads[tid].createSubSlices(); + } + } + }; + + return { + ProcessBase, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump.html b/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump.html new file mode 100644 index 00000000000..1c9248e6eaf --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump.html @@ -0,0 +1,247 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/model/container_memory_dump.html"> +<link rel="import" href="/tracing/model/memory_allocator_dump.html"> +<link rel="import" href="/tracing/model/vm_region.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the ProcessMemoryDump class. + */ +tr.exportTo('tr.model', function() { + // Names of MemoryAllocatorDump(s) from which tracing overhead should be + // discounted. + const DISCOUNTED_ALLOCATOR_NAMES = ['winheap', 'malloc']; + + // The path to where the tracing overhead dump should be added to the + // winheap/malloc allocator dump tree. + const TRACING_OVERHEAD_PATH = ['allocated_objects', 'tracing_overhead']; + + const SIZE_NUMERIC_NAME = tr.model.MemoryAllocatorDump.SIZE_NUMERIC_NAME; + const RESIDENT_SIZE_NUMERIC_NAME = + tr.model.MemoryAllocatorDump.RESIDENT_SIZE_NUMERIC_NAME; + + function getSizeNumericValue(dump, sizeNumericName) { + const sizeNumeric = dump.numerics[sizeNumericName]; + if (sizeNumeric === undefined) return 0; + return sizeNumeric.value; + } + + /** + * The ProcessMemoryDump represents a memory dump of a single process. + * @constructor + */ + function ProcessMemoryDump(globalMemoryDump, process, start) { + tr.model.ContainerMemoryDump.call(this, start); + this.process = process; + this.globalMemoryDump = globalMemoryDump; + + // Process memory totals (optional object) with the following fields (also + // optional): + // - residentBytes: Total resident bytes (number) + // - peakResidentBytes: Peak resident bytes (number) + // - arePeakResidentBytesResettable: Flag whether peak resident bytes are + // resettable (boolean) + // - privateFootprintBytes: Private footprint bytes (number) + // - platformSpecific: Map from OS-specific total names (string) to sizes + // (number) + this.totals = undefined; + + this.vmRegions = undefined; + + // Map from allocator names to heap dumps. + this.heapDumps = undefined; + + this.tracingOverheadOwnershipSetUp_ = false; + this.tracingOverheadDiscountedFromVmRegions_ = false; + } + + ProcessMemoryDump.prototype = { + __proto__: tr.model.ContainerMemoryDump.prototype, + + get userFriendlyName() { + return 'Process memory dump at ' + + tr.b.Unit.byName.timeStampInMs.format(this.start); + }, + + get containerName() { + return this.process.userFriendlyName; + }, + + get processMemoryDumps() { + const dumps = {}; + dumps[this.process.pid] = this; + return dumps; + }, + + get hasOwnVmRegions() { + return this.vmRegions !== undefined; + }, + + setUpTracingOverheadOwnership(opt_model) { + // Make sure that calling this method twice won't lead to + // 'double-discounting'. + if (this.tracingOverheadOwnershipSetUp_) return; + + this.tracingOverheadOwnershipSetUp_ = true; + + const tracingDump = this.getMemoryAllocatorDumpByFullName('tracing'); + if (tracingDump === undefined || tracingDump.owns !== undefined) { + // The tracing dump either doesn't exist, or it already owns another + // dump. + return; + } + + if (tracingDump.owns !== undefined) return; + + // Add an ownership link from tracing to + // malloc/allocated_objects/tracing_overhead or + // winheap/allocated_objects/tracing_overhead. + const hasDiscountedFromAllocatorDumps = DISCOUNTED_ALLOCATOR_NAMES.some( + function(allocatorName) { + // First check if the allocator root exists. + const allocatorDump = this.getMemoryAllocatorDumpByFullName( + allocatorName); + if (allocatorDump === undefined) { + return false; // Allocator doesn't exist, try another one. + } + + let nextPathIndex = 0; + let currentDump = allocatorDump; + let currentFullName = allocatorName; + + // Descend from the root towards tracing_overhead as long as the + // dumps on the path exist. + for (; nextPathIndex < TRACING_OVERHEAD_PATH.length; + nextPathIndex++) { + const childFullName = currentFullName + '/' + + TRACING_OVERHEAD_PATH[nextPathIndex]; + const childDump = this.getMemoryAllocatorDumpByFullName( + childFullName); + if (childDump === undefined) break; + + currentDump = childDump; + currentFullName = childFullName; + } + + // Create the missing descendant dumps on the path from the root + // towards tracing_overhead. + for (; nextPathIndex < TRACING_OVERHEAD_PATH.length; + nextPathIndex++) { + const childFullName = currentFullName + '/' + + TRACING_OVERHEAD_PATH[nextPathIndex]; + const childDump = new tr.model.MemoryAllocatorDump( + currentDump.containerMemoryDump, childFullName); + childDump.parent = currentDump; + currentDump.children.push(childDump); + + currentFullName = childFullName; + currentDump = childDump; + } + + // Add the ownership link. + const ownershipLink = + new tr.model.MemoryAllocatorDumpLink(tracingDump, currentDump); + tracingDump.owns = ownershipLink; + currentDump.ownedBy.push(ownershipLink); + return true; + }, this); + + // Force rebuilding the memory allocator dump index (if we've just added + // a new memory allocator dump). + if (hasDiscountedFromAllocatorDumps) { + this.forceRebuildingMemoryAllocatorDumpByFullNameIndex(); + } + }, + + discountTracingOverheadFromVmRegions(opt_model) { + // Make sure that calling this method twice won't lead to + // 'double-discounting'. + if (this.tracingOverheadDiscountedFromVmRegions_) return; + this.tracingOverheadDiscountedFromVmRegions_ = true; + + const tracingDump = this.getMemoryAllocatorDumpByFullName('tracing'); + if (tracingDump === undefined) return; + + const discountedSize = + getSizeNumericValue(tracingDump, SIZE_NUMERIC_NAME); + const discountedResidentSize = + getSizeNumericValue(tracingDump, RESIDENT_SIZE_NUMERIC_NAME); + + if (discountedSize <= 0 && discountedResidentSize <= 0) return; + + // Subtract the tracing size from the totals. + if (this.totals !== undefined) { + if (this.totals.residentBytes !== undefined) { + this.totals.residentBytes -= discountedResidentSize; + } + if (this.totals.peakResidentBytes !== undefined) { + this.totals.peakResidentBytes -= discountedResidentSize; + } + } + + // Subtract the tracing size from VM regions. More precisely, subtract + // tracing resident_size from byte stats (private dirty and PSS) and + // tracing size from virtual size by injecting a fake VM region with + // negative values. + if (this.vmRegions !== undefined) { + const hasSizeInBytes = this.vmRegions.sizeInBytes !== undefined; + const hasPrivateDirtyResident = + this.vmRegions.byteStats.privateDirtyResident !== undefined; + const hasProportionalResident = + this.vmRegions.byteStats.proportionalResident !== undefined; + + if ((hasSizeInBytes && discountedSize > 0) || + ((hasPrivateDirtyResident || hasProportionalResident) && + discountedResidentSize > 0)) { + const byteStats = {}; + if (hasPrivateDirtyResident) { + byteStats.privateDirtyResident = -discountedResidentSize; + } + if (hasProportionalResident) { + byteStats.proportionalResident = -discountedResidentSize; + } + this.vmRegions.addRegion(tr.model.VMRegion.fromDict({ + mappedFile: '[discounted tracing overhead]', + sizeInBytes: hasSizeInBytes ? -discountedSize : undefined, + byteStats + })); + } + } + } + }; + + ProcessMemoryDump.hookUpMostRecentVmRegionsLinks = function(processDumps) { + let mostRecentVmRegions = undefined; + + processDumps.forEach(function(processDump) { + // Update the most recent VM regions from the current dump. + if (processDump.vmRegions !== undefined) { + mostRecentVmRegions = processDump.vmRegions; + } + + // Set the most recent VM regions of the current dump. + processDump.mostRecentVmRegions = mostRecentVmRegions; + }); + }; + + tr.model.EventRegistry.register( + ProcessMemoryDump, + { + name: 'processMemoryDump', + pluralName: 'processMemoryDumps' + }); + + return { + ProcessMemoryDump, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump_test.html b/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump_test.html new file mode 100644 index 00000000000..3d507e879c9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/process_memory_dump_test.html @@ -0,0 +1,561 @@ +<!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/scalar.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/global_memory_dump.html"> +<link rel="import" href="/tracing/model/memory_dump_test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/process_memory_dump.html"> +<link rel="import" href="/tracing/model/vm_region.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const GlobalMemoryDump = tr.model.GlobalMemoryDump; + const ProcessMemoryDump = tr.model.ProcessMemoryDump; + const VMRegion = tr.model.VMRegion; + const VMRegionClassificationNode = tr.model.VMRegionClassificationNode; + const Scalar = tr.b.Scalar; + const unitlessNumber_smallerIsBetter = + tr.b.Unit.byName.unitlessNumber_smallerIsBetter; + const newAllocatorDump = tr.model.MemoryDumpTestUtils.newAllocatorDump; + const addChildDump = tr.model.MemoryDumpTestUtils.addChildDump; + const addOwnershipLink = tr.model.MemoryDumpTestUtils.addOwnershipLink; + const checkDumpNumericsAndDiagnostics = + tr.model.MemoryDumpTestUtils.checkDumpNumericsAndDiagnostics; + const checkVMRegions = tr.model.MemoryDumpTestUtils.checkVMRegions; + + function createClassificationNode(opt_sizeInBytes, opt_byteStats) { + const node = new VMRegionClassificationNode(); + if (opt_sizeInBytes !== undefined || opt_byteStats !== undefined) { + node.addRegion(VMRegion.fromDict({ + mappedFile: 'mock.so', + sizeInBytes: opt_sizeInBytes, + byteStats: opt_byteStats + })); + } + return node; + } + + function createProcessMemoryDump(timestamp, model) { + const gmd = new GlobalMemoryDump(model, timestamp); + model.globalMemoryDumps.push(gmd); + const p = model.getOrCreateProcess(123); + const pmd = new ProcessMemoryDump(gmd, p, timestamp + 1); + gmd.processMemoryDumps[123] = pmd; + p.memoryDumps.push(pmd); + return pmd; + } + + function createFinalizedProcessMemoryDump(timestamp, opt_createdCallback) { + return createFinalizedProcessMemoryDumps([timestamp], function(pmds) { + if (opt_createdCallback !== undefined) { + opt_createdCallback(pmds[0]); + } + })[0]; + } + + function createFinalizedProcessMemoryDumps(timestamps, createdCallback) { + const model = tr.c.TestUtils.newModel(function(model) { + const pmds = timestamps.map(function(timestamp) { + return createProcessMemoryDump(timestamp, model); + }); + createdCallback(pmds); + }); + const pmds = model.getProcess(123).memoryDumps; + assert.lengthOf(pmds, timestamps.length); + return pmds; + } + + test('processMemoryDumps', function() { + const pmd = createFinalizedProcessMemoryDump(42); + const pmds = pmd.processMemoryDumps; + assert.lengthOf(Object.keys(pmds), 1); + assert.strictEqual(pmds[123], pmd); + }); + + test('hookUpMostRecentVmRegionsLinks_emptyArray', function() { + const dumps = []; + ProcessMemoryDump.hookUpMostRecentVmRegionsLinks(dumps); + assert.lengthOf(dumps, 0); + }); + + test('hookUpMostRecentVmRegionsLinks_nonEmptyArray', function() { + const m = new tr.Model(); + + // A dump with no VM regions or allocator dumps. + const dump1 = createProcessMemoryDump(1, m); + + // A dump with VM regions and malloc and Oilpan allocator dumps. + const dump2 = createProcessMemoryDump(2, m); + dump2.vmRegions = createClassificationNode(); + dump2.memoryAllocatorDumps = [ + newAllocatorDump(dump2, 'oilpan', {numerics: { + size: 1024, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7), + inner_size: 768 + }}), + newAllocatorDump(dump2, 'v8', {numerics: { + size: 2048, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 15), + inner_size: 1999 + }}) + ]; + + // A dump with malloc and V8 allocator dumps. + const dump3 = createProcessMemoryDump(3, m); + dump3.memoryAllocatorDumps = [ + newAllocatorDump(dump3, 'malloc', {numerics: { + size: 1024, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 7), + inner_size: 768 + }}), + newAllocatorDump(dump3, 'v8', {numerics: { + size: 2048, + objects_count: new Scalar(unitlessNumber_smallerIsBetter, 15), + inner_size: 1999 + }}) + ]; + + // A dump with VM regions. + const dump4 = createProcessMemoryDump(4, m); + dump4.vmRegions = createClassificationNode(); + + const dumps = [dump1, dump2, dump3, dump4]; + ProcessMemoryDump.hookUpMostRecentVmRegionsLinks(dumps); + + assert.lengthOf(dumps, 4); + + assert.strictEqual(dumps[0], dump1); + assert.isUndefined(dump1.mostRecentVmRegions); + + assert.strictEqual(dumps[1], dump2); + assert.strictEqual(dump2.mostRecentVmRegions, dump2.vmRegions); + + assert.strictEqual(dumps[2], dump3); + assert.strictEqual(dump3.mostRecentVmRegions, dump2.vmRegions); + + assert.strictEqual(dumps[3], dump4); + assert.strictEqual(dump4.mostRecentVmRegions, dump4.vmRegions); + }); + + test('checkDiscountTracingOverhead_undefinedFields', function() { + const pmd = createFinalizedProcessMemoryDump(42, function(pmd) { + pmd.memoryAllocatorDumps = [ + newAllocatorDump(pmd, 'v8', {numerics: {size: 2048}}), + newAllocatorDump(pmd, 'tracing', {numerics: {size: 1024}}) + ]; + }); + + assert.isUndefined(pmd.totals); + assert.isUndefined(pmd.vmRegions); + + const v8Dump = pmd.getMemoryAllocatorDumpByFullName('v8'); + checkDumpNumericsAndDiagnostics(v8Dump, { + size: 2048, + effective_size: 2048 + }, {}); + + const tracingDump = pmd.getMemoryAllocatorDumpByFullName('tracing'); + checkDumpNumericsAndDiagnostics(tracingDump, { + size: 1024, + effective_size: 1024 + }, {}); + }); + + test('checkDiscountTracingOverhead_definedFields', function() { + const pmd = createFinalizedProcessMemoryDump(42, function(pmd) { + pmd.totals = {residentBytes: 10240}; + pmd.vmRegions = createClassificationNode(6000, { + privateDirtyResident: 4096, + proportionalResident: 5120, + swapped: 1536 + }); + + const mallocDump = newAllocatorDump(pmd, 'malloc', + {numerics: {size: 3072}}); + addChildDump(mallocDump, 'allocated_objects', {numerics: {size: 2560}}); + + const tracingDump = newAllocatorDump( + pmd, 'tracing', {numerics: {size: 1024, resident_size: 1000}}); + + pmd.memoryAllocatorDumps = [mallocDump, tracingDump]; + }); + + assert.strictEqual(pmd.totals.residentBytes, 9240); + assert.isUndefined(pmd.totals.peakResidentBytes); + + const vmRegions = pmd.vmRegions; + assert.strictEqual(vmRegions.sizeInBytes, 4976); + assert.deepEqual(vmRegions.byteStats, { + privateDirtyResident: 3096, + proportionalResident: 4120, + swapped: 1536 + }); + + checkVMRegions(vmRegions, [ + { + mappedFile: 'mock.so', + sizeInBytes: 6000, + byteStats: { + privateDirtyResident: 4096, + proportionalResident: 5120, + swapped: 1536 + } + }, + { + mappedFile: '[discounted tracing overhead]', + sizeInBytes: -1024, + byteStats: { + privateDirtyResident: -1000, + proportionalResident: -1000 + } + } + ]); + + const mallocDump = pmd.getMemoryAllocatorDumpByFullName('malloc'); + checkDumpNumericsAndDiagnostics(mallocDump, { + size: 3072, + effective_size: 2048 + }, {}); + assert.lengthOf( + mallocDump.children, 2 /* 'allocated_objects' and '<unspecified>' */); + + const allocatedObjectsDump = pmd.getMemoryAllocatorDumpByFullName( + 'malloc/allocated_objects'); + checkDumpNumericsAndDiagnostics(allocatedObjectsDump, { + size: 2560, + effective_size: 1536 + }, {}); + assert.lengthOf( + allocatedObjectsDump.children, + 2 /* 'tracing_overhead' and '<unspecified>' */); + + const discountDump = pmd.getMemoryAllocatorDumpByFullName( + 'malloc/allocated_objects/tracing_overhead'); + assert.strictEqual(discountDump.parent, allocatedObjectsDump); + assert.include(allocatedObjectsDump.children, discountDump); + checkDumpNumericsAndDiagnostics(discountDump, { + size: 1024, + effective_size: 0 + }, {}); + + const tracingDump = pmd.getMemoryAllocatorDumpByFullName('tracing'); + checkDumpNumericsAndDiagnostics(tracingDump, { + size: 1024, + effective_size: 1024, + resident_size: 1000 + }, {}); + assert.strictEqual(tracingDump.owns.target, discountDump); + }); + + test('checkDiscountTracingOverhead_winheap', function() { + const pmd = createFinalizedProcessMemoryDump(42, function(pmd) { + pmd.memoryAllocatorDumps = [ + newAllocatorDump(pmd, 'tracing', {numerics: {size: 2048}}), + newAllocatorDump(pmd, 'winheap', {numerics: {size: 5120}}) + ]; + }); + + assert.isUndefined(pmd.totals); + assert.isUndefined(pmd.vmRegions); + + const winheapDump = pmd.getMemoryAllocatorDumpByFullName('winheap'); + checkDumpNumericsAndDiagnostics(winheapDump, { + size: 5120, + effective_size: 3072 + }, {}); + assert.lengthOf(winheapDump.children, + 2 /* 'allocated_objects' and '<unspecified>' */); + + const allocatedObjectsDump = pmd.getMemoryAllocatorDumpByFullName( + 'winheap/allocated_objects'); + checkDumpNumericsAndDiagnostics(allocatedObjectsDump, { + size: 2048, + effective_size: 0 + }, {}); + assert.lengthOf( + allocatedObjectsDump.children, 1 /* 'tracing_overhead' */); + + const discountDump = pmd.getMemoryAllocatorDumpByFullName( + 'winheap/allocated_objects/tracing_overhead'); + assert.strictEqual(discountDump.parent, allocatedObjectsDump); + assert.include(allocatedObjectsDump.children, discountDump); + checkDumpNumericsAndDiagnostics(discountDump, { + size: 2048, + effective_size: 0 + }, {}); + + const tracingDump = pmd.getMemoryAllocatorDumpByFullName('tracing'); + checkDumpNumericsAndDiagnostics(tracingDump, { + size: 2048, + effective_size: 2048 + }, {}); + assert.strictEqual(tracingDump.owns.target, discountDump); + }); + + test('checkDiscountTracingOverhead_withMostRecentVmRegionsLinks', function() { + const pmds = createFinalizedProcessMemoryDumps([42, 90], function(pmds) { + pmds[0].totals = {residentBytes: 1000, peakResidentBytes: 2000}; + pmds[0].vmRegions = createClassificationNode(6000, { + privateDirtyResident: 4096 + }); + pmds[0].memoryAllocatorDumps = [ + newAllocatorDump(pmds[0], 'tracing', + {numerics: {size: 300, resident_size: 100}}) + ]; + + pmds[1].totals = {peakResidentBytes: 3000}; + pmds[1].memoryAllocatorDumps = [ + newAllocatorDump(pmds[0], 'tracing', {numerics: {resident_size: 200}}) + ]; + }); + + // First PMD: Both total resident and private dirty resident size should be + // reduced by 100. Virtual size should be reduced by 300. + assert.strictEqual(pmds[0].totals.residentBytes, 900); + assert.strictEqual(pmds[0].totals.peakResidentBytes, 1900); + assert.strictEqual(pmds[0].vmRegions.sizeInBytes, 5700); + assert.deepEqual(pmds[0].vmRegions.byteStats, { + privateDirtyResident: 3996 + }); + checkVMRegions(pmds[0].vmRegions, [ + { + mappedFile: 'mock.so', + sizeInBytes: 6000, + byteStats: { + privateDirtyResident: 4096, + } + }, + { + mappedFile: '[discounted tracing overhead]', + sizeInBytes: -300, + byteStats: { + privateDirtyResident: -100 + } + } + ]); + assert.strictEqual(pmds[0].mostRecentVmRegions, pmds[0].vmRegions); + + // Second PMD: Total resident size should be reduced by 200, whereas private + // dirty resident size should be reduced by 100 (because it comes from + // the VM regions in the first dump). Similarly, virtual size should be + // reduced by 300. + assert.isUndefined(pmds[1].totals.residentBytes); + assert.strictEqual(pmds[1].totals.peakResidentBytes, 2800); + assert.isUndefined(pmds[1].vmRegions); + assert.strictEqual(pmds[1].mostRecentVmRegions, pmds[0].vmRegions); + }); + + test('checkDiscountTracingOverhead_allDiscountedVmRegionFields', function() { + const pmd = createFinalizedProcessMemoryDump(42, function(pmd) { + pmd.vmRegions = createClassificationNode(10000, { + privateDirtyResident: 4096, + proportionalResident: 8192, + swapped: 1536 + }); + pmd.memoryAllocatorDumps = [ + newAllocatorDump(pmd, 'tracing', + {numerics: {size: 1000, resident_size: 1024}}) + ]; + }); + + const vmRegions = pmd.vmRegions; + assert.strictEqual(vmRegions.sizeInBytes, 9000); + assert.deepEqual(vmRegions.byteStats, { + privateDirtyResident: 3072, + proportionalResident: 7168, + swapped: 1536 + }); + checkVMRegions(vmRegions, [ + { + mappedFile: 'mock.so', + sizeInBytes: 10000, + byteStats: { + privateDirtyResident: 4096, + proportionalResident: 8192, + swapped: 1536 + } + }, + { + mappedFile: '[discounted tracing overhead]', + sizeInBytes: -1000, + byteStats: { + privateDirtyResident: -1024, + proportionalResident: -1024 + } + } + ]); + }); + + test('checkDiscountTracingOverhead_twoDiscountedVmRegionField', function() { + const pmd = createFinalizedProcessMemoryDump(42, function(pmd) { + pmd.vmRegions = createClassificationNode(10000, { + privateDirtyResident: 4096, + swapped: 1536 + }); + pmd.memoryAllocatorDumps = [ + newAllocatorDump(pmd, 'tracing', + {numerics: {size: 1000, resident_size: 1024}}) + ]; + }); + + const vmRegions = pmd.vmRegions; + assert.strictEqual(vmRegions.sizeInBytes, 9000); + assert.deepEqual(vmRegions.byteStats, { + privateDirtyResident: 3072, + swapped: 1536 + }); + checkVMRegions(vmRegions, [ + { + mappedFile: 'mock.so', + sizeInBytes: 10000, + byteStats: { + privateDirtyResident: 4096, + swapped: 1536 + } + }, + { + mappedFile: '[discounted tracing overhead]', + sizeInBytes: -1000, + byteStats: { + privateDirtyResident: -1024 + } + } + ]); + }); + + test('checkDiscountTracingOverhead_oneDiscountedVmRegionField', function() { + const pmd = createFinalizedProcessMemoryDump(42, function(pmd) { + pmd.vmRegions = createClassificationNode(10000); + pmd.memoryAllocatorDumps = [ + newAllocatorDump(pmd, 'tracing', + {numerics: {size: 1000, resident_size: 1024}}) + ]; + }); + + const vmRegions = pmd.vmRegions; + assert.strictEqual(vmRegions.sizeInBytes, 9000); + assert.deepEqual(vmRegions.byteStats, {}); + checkVMRegions(vmRegions, [ + { + mappedFile: 'mock.so', + sizeInBytes: 10000 + }, + { + mappedFile: '[discounted tracing overhead]', + sizeInBytes: -1000 + } + ]); + }); + + test('checkDiscountTracingOverhead_noDiscountedVmRegionFields', function() { + const pmd = createFinalizedProcessMemoryDump(42, function(pmd) { + pmd.vmRegions = createClassificationNode(undefined, { + swapped: 1536 + }); + pmd.memoryAllocatorDumps = [ + newAllocatorDump(pmd, 'tracing', + {numerics: {size: 1000, resident_size: 1024}}) + ]; + }); + + const vmRegions = pmd.vmRegions; + assert.isUndefined(vmRegions.sizeInBytes); + assert.deepEqual(vmRegions.byteStats, { + swapped: 1536 + }); + checkVMRegions(vmRegions, [ + { + mappedFile: 'mock.so', + byteStats: { + swapped: 1536 + } + } + ]); + }); + + test('checkDiscountTracingOverhead_existingLink', function() { + const pmd = createFinalizedProcessMemoryDump(42, function(pmd) { + pmd.totals = {residentBytes: 10240}; + + pmd.vmRegions = createClassificationNode(6000, { + privateDirtyResident: 4096, + swapped: 1536, + proportionalResident: 5120 + }); + + const mallocDump = newAllocatorDump(pmd, 'malloc', + {numerics: {size: 3072}}); + const tracingDump = newAllocatorDump(pmd, 'tracing', + {numerics: {size: 1024, resident_size: 1000}}); + const ownedDump = newAllocatorDump(pmd, 'owned'); + + // The code for discounting tracing overhead should *not* override an + // existing ownership. + addOwnershipLink(tracingDump, ownedDump); + + pmd.memoryAllocatorDumps = [mallocDump, tracingDump, ownedDump]; + }); + + assert.strictEqual(pmd.totals.residentBytes, 9240); + assert.isUndefined(pmd.totals.peakResidentBytes); + + const vmRegions = pmd.vmRegions; + assert.strictEqual(vmRegions.sizeInBytes, 4976); + assert.deepEqual(vmRegions.byteStats, { + privateDirtyResident: 3096, + proportionalResident: 4120, + swapped: 1536 + }); + checkVMRegions(vmRegions, [ + { + mappedFile: 'mock.so', + sizeInBytes: 6000, + byteStats: { + privateDirtyResident: 4096, + proportionalResident: 5120, + swapped: 1536 + } + }, + { + mappedFile: '[discounted tracing overhead]', + sizeInBytes: -1024, + byteStats: { + privateDirtyResident: -1000, + proportionalResident: -1000 + } + } + ]); + + const mallocDump = pmd.getMemoryAllocatorDumpByFullName('malloc'); + checkDumpNumericsAndDiagnostics(mallocDump, { + size: 3072, + effective_size: 3072 + }, {}); + assert.lengthOf(mallocDump.children, 0); + + const ownedDump = pmd.getMemoryAllocatorDumpByFullName('owned'); + checkDumpNumericsAndDiagnostics(ownedDump, { + size: 1024, + effective_size: 0 + }, {}); + assert.lengthOf(ownedDump.children, 0); + + const tracingDump = pmd.getMemoryAllocatorDumpByFullName('tracing'); + checkDumpNumericsAndDiagnostics(tracingDump, { + size: 1024, + effective_size: 1024, + resident_size: 1000 + }, {}); + assert.strictEqual(tracingDump.owns.target, ownedDump); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/process_test.html b/chromium/third_party/catapult/tracing/tracing/model/process_test.html new file mode 100644 index 00000000000..9355971c099 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/process_test.html @@ -0,0 +1,127 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/frame.html"> +<link rel="import" href="/tracing/model/global_memory_dump.html"> +<link rel="import" href="/tracing/model/instant_event.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/process.html"> +<link rel="import" href="/tracing/model/thread_slice.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('getOrCreateCounter', function() { + const model = new tr.Model(); + const process = new tr.model.Process(model, 7); + const ctrBar = process.getOrCreateCounter('foo', 'bar'); + const ctrBar2 = process.getOrCreateCounter('foo', 'bar'); + assert.strictEqual(ctrBar2, ctrBar); + }); + + test('shiftTimestampsForward', function() { + const model = new tr.Model(); + const process = new tr.model.Process(model, 7); + const ctr = process.getOrCreateCounter('foo', 'bar'); + const thread = process.getOrCreateThread(1); + + const instantEvent = new tr.model.InstantEvent('cat', 'event1', 1, 100); + process.instantEvents.push(instantEvent); + + const slice = new tr.model.ThreadSlice('', 'a', 0, 1, {}, 4); + const frame = + new tr.model.Frame([slice], [{thread, start: 100, end: 200}]); + process.frames.push(frame); + + const memoryDump = new tr.model.GlobalMemoryDump(model, 100); + process.memoryDumps.push(memoryDump); + + let shiftCount = 0; + thread.shiftTimestampsForward = function(ts) { + if (ts === 0.32) { + shiftCount++; + } + }; + ctr.shiftTimestampsForward = function(ts) { + if (ts === 0.32) { + shiftCount++; + } + }; + + process.shiftTimestampsForward(0.32); + assert.strictEqual(shiftCount, 2); + assert.strictEqual(instantEvent.start, 100.32); + assert.strictEqual(frame.start, 100.32); + assert.strictEqual(frame.end, 200.32); + assert.strictEqual(memoryDump.start, 100.32); + }); + + test('compareOnPID', function() { + let model = new tr.Model(); + const p1 = new tr.model.Process(model, 1); + p1.name = 'Renderer'; + + model = new tr.Model(); + const p2 = new tr.model.Process(model, 2); + p2.name = 'Renderer'; + + assert.isBelow(p1.compareTo(p2), 0); + }); + + test('compareOnSortIndex', function() { + const model = new tr.Model(); + const p1 = new tr.model.Process(model, 1); + p1.name = 'Renderer'; + p1.sortIndex = 1; + + const p2 = new tr.model.Process(model, 2); + p2.name = 'Renderer'; + + assert.isAbove(p1.compareTo(p2), 0); + }); + + test('compareOnName', function() { + const model = new tr.Model(); + const p1 = new tr.model.Process(model, 1); + p1.name = 'Browser'; + + const p2 = new tr.model.Process(model, 2); + p2.name = 'Renderer'; + + assert.isBelow(p1.compareTo(p2), 0); + }); + + test('compareOnLabels', function() { + const model = new tr.Model(); + const p1 = new tr.model.Process(model, 1); + p1.name = 'Renderer'; + p1.labels = ['a']; + + const p2 = new tr.model.Process(model, 2); + p2.name = 'Renderer'; + p2.labels = ['b']; + + assert.isBelow(p1.compareTo(p2), 0); + }); + + test('compareOnUptime', function() { + const model = new tr.Model(); + const p1 = new tr.model.Process(model, 1); + p1.name = 'Renderer'; + p1.uptime_seconds = 10; + + const p2 = new tr.model.Process(model, 2); + p2.name = 'Renderer'; + p2.uptime_seconds = 20; + + assert.isBelow(p1.compareTo(p2), 0); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/profile_node.html b/chromium/third_party/catapult/tracing/tracing/model/profile_node.html new file mode 100644 index 00000000000..edb47b77852 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/profile_node.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/color_scheme.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the ProfileNode class. + */ +tr.exportTo('tr.model', function() { + /** + * A ProfileNode represents a node in the profile tree, + * it is essentially a frame in the stack when the sample gets recorded. + */ + // TODO(lpy) Move V8 specific part out of ProfileNode. + function ProfileNode(id, title, parentNode) { + this.id_ = id; + this.title_ = title; + this.parentNode_ = parentNode; + this.colorId_ = -1; + // Cache the constructed call stack starting from this node to root. + this.userFriendlyStack_ = []; + } + + ProfileNode.prototype = { + __proto__: Object.prototype, + + get title() { + return this.title_; + }, + + get parentNode() { + return this.parentNode_; + }, + + set parentNode(value) { + this.parentNode_ = value; + }, + + get id() { + return this.id_; + }, + + get colorId() { + return this.colorId_; + }, + + set colorId(value) { + this.colorId_ = value; + }, + + get userFriendlyName() { + return this.title_; + }, + + get userFriendlyStack() { + if (this.userFriendlyStack_.length === 0) { + this.userFriendlyStack_ = [this.userFriendlyName]; + if (this.parentNode_ !== undefined) { + this.userFriendlyStack_ = + this.userFriendlyStack_.concat(this.parentNode_.userFriendlyStack); + } + } + return this.userFriendlyStack_; + }, + + get sampleTitle() { + throw new Error('Not implemented.'); + } + }; + + tr.model.EventRegistry.register( + ProfileNode, + { + name: 'Node', + pluralName: 'Nodes' + } + ); + + return { + ProfileNode, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/profile_tree.html b/chromium/third_party/catapult/tracing/tracing/model/profile_tree.html new file mode 100644 index 00000000000..57e73ed27b0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/profile_tree.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Sample class. + */ +tr.exportTo('tr.model', function() { + /** + * A ProfileTree represents all call stack we collect in sampling + * in the form of a tree. + * By traversing from root to a leaf we get a call stack + * that belongs to some samples we collect. + */ + function ProfileTree() { + this.startTime_ = undefined; + this.endTime_ = undefined; + this.tree_ = new Map(); + this.pid_ = -1; + this.tid_ = -1; + } + + ProfileTree.prototype = { + __proto__: Object.prototype, + + get pid() { + return this.pid_; + }, + + set pid(value) { + this.pid_ = value; + }, + + get tid() { + return this.tid_; + }, + + set tid(value) { + this.tid_ = value; + }, + + get tree() { + return this.tree_; + }, + + get startTime() { + return this.startTime_; + }, + + set startTime(value) { + this.startTime_ = value; + this.endTime_ = value; + }, + + get endTime() { + return this.endTime_; + }, + + set endTime(value) { + this.endTime_ = value; + }, + + add(node) { + if (this.tree_.has(node.id)) { + throw new Error('Conflict id in the profile tree.'); + } + this.tree_.set(node.id, node); + return node; + }, + + getNode(nodeId) { + return this.tree_.get(nodeId); + } + }; + + return { + ProfileTree, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item.html b/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item.html new file mode 100644 index 00000000000..d61a944569b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item.html @@ -0,0 +1,45 @@ +<!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/selectable_item.html"> +<link rel="import" href="/tracing/model/selection_state.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const SelectableItem = tr.model.SelectableItem; + const SelectionState = tr.model.SelectionState; + + /** + * A ProxySelectableItem is a selectable item which is not a model item itself + * but instead acts as a proxy for a model item. + * + * @constructor + * @extends {SelectableItem} + */ + function ProxySelectableItem(modelItem) { + SelectableItem.call(this, modelItem); + } + + ProxySelectableItem.prototype = { + __proto__: SelectableItem.prototype, + + get selectionState() { + const modelItem = this.modelItem_; + if (modelItem === undefined) { + return SelectionState.NONE; + } + return modelItem.selectionState; + } + }; + + return { + ProxySelectableItem, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item_test.html b/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item_test.html new file mode 100644 index 00000000000..c7dba2c88a8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/proxy_selectable_item_test.html @@ -0,0 +1,39 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/proxy_selectable_item.html"> +<link rel="import" href="/tracing/model/selection_state.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ProxySelectableItem = tr.model.ProxySelectableItem; + const SelectionState = tr.model.SelectionState; + + test('checkSelectionState_undefinedModelItem', function() { + const proxyItem = new ProxySelectableItem(undefined); + assert.isFalse(proxyItem.selected); + assert.strictEqual(proxyItem.selectionState, SelectionState.NONE); + }); + + test('checkSelectionState_definedModelItem', function() { + // Start with a selected model event. + const modelItem = {selectionState: SelectionState.SELECTED}; + const proxyItem = new ProxySelectableItem(modelItem); + assert.isTrue(proxyItem.selected); + assert.strictEqual(proxyItem.selectionState, SelectionState.SELECTED); + + // Change the selection state of the model event to highlighted. + modelItem.selectionState = SelectionState.HIGHLIGHTED; + assert.isFalse(proxyItem.selected); + assert.strictEqual(proxyItem.selectionState, SelectionState.HIGHLIGHTED); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/rect_annotation.html b/chromium/third_party/catapult/tracing/tracing/model/rect_annotation.html new file mode 100644 index 00000000000..0ba5daf3ca5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/rect_annotation.html @@ -0,0 +1,65 @@ +<!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/annotation.html"> +<link rel="import" href="/tracing/model/location.html"> +<link rel="import" href="/tracing/ui/annotations/rect_annotation_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + function RectAnnotation(start, end) { + tr.model.Annotation.apply(this, arguments); + + this.startLocation_ = start; // Location of top-left corner. + this.endLocation_ = end; // Location of bottom-right corner. + this.fillStyle = 'rgba(255, 180, 0, 0.3)'; + } + + RectAnnotation.fromDict = function(dict) { + const args = dict.args; + const startLoc = + new tr.model.Location(args.start.xWorld, args.start.yComponents); + const endLoc = + new tr.model.Location(args.end.xWorld, args.end.yComponents); + return new tr.model.RectAnnotation(startLoc, endLoc); + }; + + RectAnnotation.prototype = { + __proto__: tr.model.Annotation.prototype, + + get startLocation() { + return this.startLocation_; + }, + + get endLocation() { + return this.endLocation_; + }, + + toDict() { + return { + typeName: 'rect', + args: { + start: this.startLocation.toDict(), + end: this.endLocation.toDict() + } + }; + }, + + createView_(viewport) { + return new tr.ui.annotations.RectAnnotationView(viewport, this); + } + }; + + tr.model.Annotation.register(RectAnnotation, {typeName: 'rect'}); + + return { + RectAnnotation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample.html b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample.html new file mode 100644 index 00000000000..5ede6b97cff --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/model/event_registry.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const Event = tr.model.Event; + const EventRegistry = tr.model.EventRegistry; + + /** + * A sample containing data about what fraction of a resource + * (CPU or GPU) is being used at a given point in time. + */ + class ResourceUsageSample extends Event { + /** + * @param {ResourceUsageSeries } series The ResourceUsageSeries that this + * sample will be a part of. + * @param {float} start Time of the sample. + * @param {float} usage Fraction of the resource (CPU or GPU) in use at the + * time of the sample. + */ + constructor(series, start, usage) { + super(); + + this.series_ = series; + this.start_ = start; + this.usage_ = usage; + } + + get series() { + return this.series_; + } + + get start() { + return this.start_; + } + + set start(value) { + this.start_ = value; + } + + get usage() { + return this.usage_; + } + + set usage(value) { + this.usage_ = value; + } + + addBoundsToRange(range) { + range.addValue(this.start); + } + } + + EventRegistry.register( + ResourceUsageSample, + { + name: 'resourceUsageSample', + pluralName: 'resourceUsageSamples' + }); + + return { + ResourceUsageSample, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample_test.html b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample_test.html new file mode 100644 index 00000000000..003ef6a327e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_sample_test.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/resource_usage_sample.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ResourceUsageSample = tr.model.ResourceUsageSample; + + test('usageSample', function() { + const series = new tr.model.ResourceUsageSeries(new tr.Model().device); + + const sample1 = new ResourceUsageSample(series, 0.0, 0.11); + const sample2 = new ResourceUsageSample(series, 1.0, 0.22); + + assert.strictEqual(sample1.series, series); + assert.strictEqual(sample1.start, 0.0); + assert.strictEqual(sample1.usage, 0.11); + + assert.strictEqual(sample2.series, series); + assert.strictEqual(sample2.start, 1.0); + assert.strictEqual(sample2.usage, 0.22); + }); + + test('addBoundsToRange', function() { + const series = new tr.model.ResourceUsageSeries(new tr.Model().device); + + const sample1 = new ResourceUsageSample(series, 0.0, 0.11); + const sample2 = new ResourceUsageSample(series, 1.0, 0.22); + + const range = new tr.b.math.Range(); + sample1.addBoundsToRange(range); + + assert.strictEqual(range.min, 0); + assert.strictEqual(range.max, 0); + + sample2.addBoundsToRange(range); + + assert.strictEqual(range.min, 0); + assert.strictEqual(range.max, 1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series.html b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series.html new file mode 100644 index 00000000000..16f28c8d2a1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series.html @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/unit_scale.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/model/event_container.html"> +<link rel="import" href="/tracing/model/resource_usage_sample.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const ResourceUsageSample = tr.model.ResourceUsageSample; + + /** + * A container holding a time series of samples giving the + * fraction of usage of a resource (e.g. CPU or GPU) at each + * sample time. + */ + + class ResourceUsageSeries extends tr.model.EventContainer { + constructor(device) { + super(); + + this.device_ = device; + this.samples_ = []; + } + + get device() { + return this.device_; + } + + get samples() { + return this.samples_; + } + + get stableId() { + return this.device_.stableId + '.ResourceUsageSeries'; + } + + /** + * Adds a usage sample to the series and returns it. + * + * Note: Samples must be added in chronological order. + */ + addUsageSample(ts, val) { + const sample = new ResourceUsageSample(this, ts, val); + this.samples_.push(sample); + return sample; + } + + /** + * Returns the total time consumed by a resource (e.g. CPU or GPU) between + * the specified start and end timestamps (in milliseconds). + */ + computeResourceTimeConsumedInMs(start, end) { + const measurementRange = tr.b.math.Range.fromExplicitRange(start, end); + + let resourceTimeInMs = 0; + let startIndex = tr.b.findLowIndexInSortedArray( + this.samples, x => x.start, start) - 1; + const endIndex = tr.b.findLowIndexInSortedArray( + this.samples, x => x.start, end); + + if (startIndex < 0) startIndex = 0; + + for (let i = startIndex; i < endIndex; i++) { + const sample = this.samples[i]; + const nextSample = this.samples[i + 1]; + + const sampleRange = new tr.b.math.Range(); + sampleRange.addValue(sample.start); + sampleRange.addValue(nextSample ? nextSample.start : sample.start); + + const intersectionRangeInMs = measurementRange.findIntersection( + sampleRange); + + resourceTimeInMs += intersectionRangeInMs.duration * sample.usage; + } + + return resourceTimeInMs; + } + + getSamplesWithinRange(start, end) { + const startIndex = tr.b.findLowIndexInSortedArray( + this.samples, x => x.start, start); + const endIndex = tr.b.findLowIndexInSortedArray( + this.samples, x => x.start, end); + return this.samples.slice(startIndex, endIndex); + } + + shiftTimestampsForward(amount) { + for (let i = 0; i < this.samples_.length; ++i) { + this.samples_[i].start += amount; + } + } + + updateBounds() { + this.bounds.reset(); + + if (this.samples_.length === 0) return; + + this.bounds.addValue(this.samples_[0].start); + this.bounds.addValue(this.samples_[this.samples_.length - 1].start); + } + + * childEvents() { + yield* this.samples_; + } + } + + return { + ResourceUsageSeries, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series_test.html b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series_test.html new file mode 100644 index 00000000000..80981e0ca26 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/resource_usage_series_test.html @@ -0,0 +1,152 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/device.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/resource_usage_series.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Model = tr.Model; + const ResourceUsageSeries = tr.model.ResourceUsageSeries; + + test('stableId', function() { + const device = { stableId: 'test' }; + const series = new ResourceUsageSeries(device); + + assert.strictEqual(series.stableId, 'test.ResourceUsageSeries'); + }); + + test('device', function() { + const device = new tr.model.Device(new Model()); + const series = new ResourceUsageSeries(device); + + assert.strictEqual(series.device, device); + }); + + test('addUsageSample', function() { + const series = new ResourceUsageSeries(new Model().device); + + assert.strictEqual(series.samples.length, 0); + + const sample1 = series.addUsageSample(0, 1); + const sample2 = series.addUsageSample(1, 2); + + assert.deepEqual(series.samples, [sample1, sample2]); + }); + + test('getResourceTimeConsumed_oneInterval', function() { + const series = new ResourceUsageSeries(new Model().device); + series.addUsageSample(0, 1); + series.addUsageSample(1000, 2); + + assert.strictEqual(series.computeResourceTimeConsumedInMs(0, 1000), 1000); + }); + + test('getResourceTimeConsumed_twoIntervals', function() { + const series = new ResourceUsageSeries(new Model().device); + series.addUsageSample(0, 1); + series.addUsageSample(1000, 2); + series.addUsageSample(2000, 2); + + assert.strictEqual(series.computeResourceTimeConsumedInMs(0, 2000), 3000); + }); + + test('getResourceTimeConsumed_oneSample', function() { + const series = new ResourceUsageSeries(new Model().device); + series.addUsageSample(1000, 1); + + assert.strictEqual(series.computeResourceTimeConsumedInMs(0, 2000), 0); + }); + + test('getResourceTimeConsumed_samplesAfterStart', function() { + const series = new ResourceUsageSeries(new Model().device); + series.addUsageSample(1000, 1); + series.addUsageSample(2000, 2); + + assert.strictEqual(series.computeResourceTimeConsumedInMs(0, 2000), 1000); + }); + + test('getResourceTimeConsumed_extraSamplesBeforeStart', function() { + const series = new ResourceUsageSeries(new Model().device); + series.addUsageSample(0, 10); + series.addUsageSample(1000, 1); + series.addUsageSample(2000, 1); + series.addUsageSample(3000, 1); + + assert.strictEqual( + series.computeResourceTimeConsumedInMs(2000, 4000), 1000); + }); + + test('getResourceTimeConsumed_extraSamplesAfterEnd', function() { + const series = new ResourceUsageSeries(new Model().device); + series.addUsageSample(0, 1); + series.addUsageSample(1000, 1); + series.addUsageSample(2000, 1); + series.addUsageSample(3000, 10); + + assert.strictEqual(series.computeResourceTimeConsumedInMs(0, 2000), 2000); + }); + + test('shiftTimestampsForward', function() { + const series = new ResourceUsageSeries(new Model().device); + + series.addUsageSample(0, 1); + series.addUsageSample(1, 2); + + series.shiftTimestampsForward(2); + + assert.strictEqual(series.samples[0].start, 2); + assert.strictEqual(series.samples[1].start, 3); + + series.shiftTimestampsForward(-4); + + assert.strictEqual(series.samples[0].start, -2); + assert.strictEqual(series.samples[1].start, -1); + }); + + + test('updateBounds', function() { + const series = new ResourceUsageSeries(new Model().device); + + series.addUsageSample(0, 1); + series.addUsageSample(1, 2); + series.updateBounds(); + + assert.strictEqual(series.bounds.min, 0); + assert.strictEqual(series.bounds.max, 1); + + series.addUsageSample(4, 3); + series.updateBounds(); + + assert.strictEqual(series.bounds.min, 0); + assert.strictEqual(series.bounds.max, 4); + }); + + test('childEvents_empty', function() { + const series = new ResourceUsageSeries(new Model().device); + const eventsInSeries = []; + for (const event of series.childEvents()) { + eventsInSeries.push(event); + } + assert.deepEqual(eventsInSeries, []); + }); + + test('childEvents_nonempty', function() { + const series = new ResourceUsageSeries(new Model().device); + const sample1 = series.addUsageSample(0, 1); + const sample2 = series.addUsageSample(1, 2); + const eventsInSeries = []; + for (const event of series.childEvents()) { + eventsInSeries.push(event); + } + assert.deepEqual(eventsInSeries, [sample1, sample2]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/sample.html b/chromium/third_party/catapult/tracing/tracing/model/sample.html new file mode 100644 index 00000000000..59a34771d8b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/sample.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/timed_event.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Sample class. + */ +tr.exportTo('tr.model', function() { + /** + * A Sample represents a sample taken at an instant in time, + * plus its call stack and parameters associated with that sample. + * + * @constructor + */ + function Sample(start, title, leafNode, thread, opt_cpu, opt_weight, + opt_args) { + tr.model.TimedEvent.call(this, start); + + this.start_ = start; + this.title_ = title; + this.leafNode_ = leafNode; + this.thread_ = thread; + this.colorId_ = leafNode.colorId; + + this.cpu_ = opt_cpu; + this.weight_ = opt_weight; + this.args = opt_args || {}; + } + + Sample.prototype = { + __proto__: tr.model.TimedEvent.prototype, + + get title() { + return this.title_; + }, + + get colorId() { + return this.colorId_; + }, + + get thread() { + return this.thread_; + }, + + get leafNode() { + return this.leafNode_; + }, + + get userFriendlyName() { + return 'Sample at ' + + tr.b.Unit.byName.timeStampInMs.format(this.start); + }, + + get userFriendlyStack() { + return this.leafNode_.userFriendlyStack; + }, + + getNodesAsArray() { + const nodes = []; + let node = this.leafNode_; + while (node !== undefined) { + nodes.push(node); + node = node.parentNode; + } + return nodes; + }, + + get cpu() { + return this.cpu_; + }, + + get weight() { + return this.weight_; + }, + }; + + tr.model.EventRegistry.register( + Sample, + { + name: 'Sample', + pluralName: 'Samples' + }); + + return { + Sample, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/sample_test.html b/chromium/third_party/catapult/tracing/tracing/model/sample_test.html new file mode 100644 index 00000000000..f329f1096d9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/sample_test.html @@ -0,0 +1,31 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Sample = tr.model.Sample; + const StackFrame = tr.model.StackFrame; + const Thread = tr.model.Thread; + + test('sampleStackTrace', function() { + const thread = tr.c.TestUtils.newFakeThread(); + + const model = new tr.Model(); + const node = tr.c.TestUtils.newProfileNodes(model, ['a', 'b', 'c']); + + const s = new Sample( + 10, 'instructions_retired', node, thread, undefined, 10); + assert.deepEqual(s.userFriendlyStack, ['c', 'b', 'a']); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/scoped_id.html b/chromium/third_party/catapult/tracing/tracing/model/scoped_id.html new file mode 100644 index 00000000000..b7b64a2e443 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/scoped_id.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/model/constants.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + function ScopedId(scope, id, pid) { + if (scope === undefined) { + throw new Error('Scope should be defined. Use \'' + + tr.model.OBJECT_DEFAULT_SCOPE + + '\' as the default scope.'); + } + this.scope = scope; + this.id = id; + this.pid = pid; + } + + ScopedId.prototype = { + toString() { + const pidStr = this.pid === undefined ? '' : 'pid: ' + this.pid + ', '; + return '{' + pidStr + 'scope: ' + this.scope + ', id: ' + this.id + '}'; + }, + + toStringWithDelimiter(delim) { + return (this.pid === undefined ? '' : this.pid) + delim + + this.scope + delim + this.id; + } + }; + + return { + ScopedId, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/selectable_item.html b/chromium/third_party/catapult/tracing/tracing/model/selectable_item.html new file mode 100644 index 00000000000..3492dc00b9f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/selectable_item.html @@ -0,0 +1,57 @@ +<!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/selection_state.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the SelectableItem class. + */ +tr.exportTo('tr.model', function() { + const SelectionState = tr.model.SelectionState; + + /** + * A SelectableItem is the abstract base class for any non-container data that + * has an associated model item in the trace model (possibly itself). + * + * Subclasses must provide a selectionState property (or getter). + * + * @constructor + */ + function SelectableItem(modelItem) { + this.modelItem_ = modelItem; + } + + SelectableItem.prototype = { + get modelItem() { + return this.modelItem_; + }, + + get selected() { + return this.selectionState === SelectionState.SELECTED; + }, + + addToSelection(selection) { + const modelItem = this.modelItem_; + if (!modelItem) return; + selection.push(modelItem); + }, + + addToTrackMap(eventToTrackMap, track) { + const modelItem = this.modelItem_; + if (!modelItem) return; + eventToTrackMap.addEvent(modelItem, track); + } + }; + + return { + SelectableItem, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/selectable_item_test.html b/chromium/third_party/catapult/tracing/tracing/model/selectable_item_test.html new file mode 100644 index 00000000000..120773852d9 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/selectable_item_test.html @@ -0,0 +1,80 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/model/selectable_item.html"> +<link rel="import" href="/tracing/model/selection_state.html"> +<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Event = tr.model.Event; + const EventToTrackMap = tr.ui.tracks.EventToTrackMap; + const SelectableItem = tr.model.SelectableItem; + const SelectionState = tr.model.SelectionState; + + test('checkModelItem', function() { + const selectableItem1 = new SelectableItem(undefined); + assert.isUndefined(selectableItem1.modelItem); + + const event = new Event(); + const selectableItem2 = new SelectableItem(event); + assert.strictEqual(selectableItem2.modelItem, event); + }); + + test('checkSelected', function() { + const selectableItem = new SelectableItem(undefined); + + selectableItem.selectionState = SelectionState.NONE; + assert.isFalse(selectableItem.selected); + + selectableItem.selectionState = SelectionState.SELECTED; + assert.isTrue(selectableItem.selected); + + selectableItem.selectionState = SelectionState.HIGHLIGHTED; + assert.isFalse(selectableItem.selected); + }); + + test('checkAddToSelection_undefinedModelItem', function() { + const selectableItem = new SelectableItem(undefined); + const selection = []; + selectableItem.addToSelection(selection); + assert.lengthOf(selection, 0); + }); + + test('checkAddToSelection_definedModelItem', function() { + const event = new Event(); + const selectableItem = new SelectableItem(event); + const selection = []; + selectableItem.addToSelection(selection); + assert.lengthOf(selection, 1); + assert.strictEqual(selection[0], event); + }); + + test('checkAddToTrackMap_undefinedModelItem', function() { + const selectableItem = new SelectableItem(undefined); + const eventToTrackMap = new EventToTrackMap(); + const track = {}; + selectableItem.addToTrackMap(eventToTrackMap, track); + assert.lengthOf(Object.keys(eventToTrackMap), 0); + }); + + test('checkAddToTrackMap_definedModelItem', function() { + const event = new Event(); + const selectableItem = new SelectableItem(event); + const eventToTrackMap = new EventToTrackMap(); + const track = {}; + selectableItem.addToTrackMap(eventToTrackMap, track); + assert.lengthOf(Object.keys(eventToTrackMap), 1); + assert.strictEqual(eventToTrackMap[event.guid], track); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/selection_state.html b/chromium/third_party/catapult/tracing/tracing/model/selection_state.html new file mode 100644 index 00000000000..3e68d65a72b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/selection_state.html @@ -0,0 +1,72 @@ +<!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/color_scheme.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the SelectionState class. + */ +tr.exportTo('tr.model', function() { + const ColorScheme = tr.b.ColorScheme; + + /** + * Describes the level of visual highlighting to apply to an event when shown. + * + * color_scheme.html defines N variations off of a base color palette, + * one for each selection state, all concatenated into one flat array. To + * pick the final colorId for a given variations, the SelectionState is + * multiplied by the number of base colors. + * + * Thus, the values here must be kept in sync with color_scheme's palette + * layout. + */ + const SelectionState = { + NONE: 0, + + // Legacy names. + SELECTED: ColorScheme.properties.brightenedOffsets[0], + HIGHLIGHTED: ColorScheme.properties.brightenedOffsets[1], + DIMMED: ColorScheme.properties.dimmedOffsets[0], + + // Modern names. + BRIGHTENED0: ColorScheme.properties.brightenedOffsets[0], + BRIGHTENED1: ColorScheme.properties.brightenedOffsets[1], + BRIGHTENED2: ColorScheme.properties.brightenedOffsets[2], + + DIMMED0: ColorScheme.properties.dimmedOffsets[0], + DIMMED1: ColorScheme.properties.dimmedOffsets[1], + DIMMED2: ColorScheme.properties.dimmedOffsets[2] + }; + + const brighteningLevels = [ + SelectionState.NONE, + SelectionState.BRIGHTENED0, + SelectionState.BRIGHTENED1, + SelectionState.BRIGHTENED2 + ]; + SelectionState.getFromBrighteningLevel = function(level) { + return brighteningLevels[level]; + }; + + const dimmingLevels = [ + SelectionState.DIMMED0, + SelectionState.DIMMED1, + SelectionState.DIMMED2 + ]; + SelectionState.getFromDimmingLevel = function(level) { + return dimmingLevels[level]; + }; + + return { + SelectionState, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/slice.html b/chromium/third_party/catapult/tracing/tracing/model/slice.html new file mode 100644 index 00000000000..2ea422561e6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/slice.html @@ -0,0 +1,302 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/model/timed_event.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Slice class. + */ +tr.exportTo('tr.model', function() { + /** + * A Slice represents an interval of time plus parameters associated + * with that interval. + * + * @constructor + */ + function Slice(category, title, colorId, start, args, opt_duration, + opt_cpuStart, opt_cpuDuration, opt_argsStripped, + opt_bindId) { + if (new.target) { + throw new Error('Can\'t instantiate pure virtual class Slice'); + } + tr.model.TimedEvent.call(this, start); + + this.category = category || ''; + this.title = title; + this.colorId = colorId; + this.args = args; + this.startStackFrame = undefined; + this.endStackFrame = undefined; + this.didNotFinish = false; + this.inFlowEvents = []; + this.outFlowEvents = []; + this.subSlices = []; + this.selfTime = undefined; + this.cpuSelfTime = undefined; + this.important = false; + this.parentContainer = undefined; + this.argsStripped = false; + + this.bind_id_ = opt_bindId; + + // parentSlice and isTopLevel will be set by SliceGroup. + this.parentSlice = undefined; + this.isTopLevel = false; + // After SliceGroup processes Slices, isTopLevel should be equivalent to + // !parentSlice. + + if (opt_duration !== undefined) { + this.duration = opt_duration; + } + + if (opt_cpuStart !== undefined) { + this.cpuStart = opt_cpuStart; + } + + if (opt_cpuDuration !== undefined) { + this.cpuDuration = opt_cpuDuration; + } + + if (opt_argsStripped !== undefined) { + this.argsStripped = true; + } + } + + Slice.prototype = { + __proto__: tr.model.TimedEvent.prototype, + + + get analysisTypeName() { + return this.title; + }, + + get userFriendlyName() { + return 'Slice ' + this.title + ' at ' + + tr.b.Unit.byName.timeStampInMs.format(this.start); + }, + + get stableId() { + const parentSliceGroup = this.parentContainer.sliceGroup; + return parentSliceGroup.stableId + '.' + + parentSliceGroup.slices.indexOf(this); + }, + + get bindId() { + return this.bind_id_; + }, + + findDescendentSlice(targetTitle) { + if (!this.subSlices) { + return undefined; + } + + for (let i = 0; i < this.subSlices.length; i++) { + if (this.subSlices[i].title === targetTitle) { + return this.subSlices[i]; + } + const slice = this.subSlices[i].findDescendentSlice(targetTitle); + if (slice) return slice; + } + return undefined; + }, + + get mostTopLevelSlice() { + if (!this.parentSlice) return this; + return this.parentSlice.mostTopLevelSlice; + }, + + getProcess() { + const thread = this.parentContainer; + if (thread && thread.getProcess) { + return thread.getProcess(); + } + return undefined; + }, + + get model() { + const process = this.getProcess(); + if (process !== undefined) { + return this.getProcess().model; + } + return undefined; + }, + + /** + * Finds all topmost slices relative to this slice. + * + * Slices may have multiple direct descendants which satisfy + * |eventPredicate|, and in this case, all of them are topmost as long as + * this slice does not satisfy the predicate. + * + * For instance, suppose we are passing a predicate that checks whether + * events titles begin with 'C'. + * C1.findTopmostSlicesRelativeToThisSlice() returns C1 in this example: + * [ C1 ] + * [ C2 ] + * + * and D.findTopmostSlicesRelativeToThisSlice() returns C1 and C2 in this + * example: + * [ D ] + * [C1] [C2] + */ + * findTopmostSlicesRelativeToThisSlice(eventPredicate) { + if (eventPredicate(this)) { + yield this; + return; + } + for (const s of this.subSlices) { + yield* s.findTopmostSlicesRelativeToThisSlice(eventPredicate); + } + }, + + /** + * Obtains all subsequent slices of this slice. + * + * Subsequent slices are slices that get executed after a particular + * slice, i.e., all the functions that are called after the current one. + * + * For instance, E.iterateAllSubsequentSlices() in the following example: + * [ A ] + * [ B][ D ][ G ] + * [C] [E][F] [H] + * will pass F, G, then H to the provided callback. + * + * The reason we need subsequent slices of a particular slice is that + * when there is flow event goes into, e.g., E, we only want to highlight + * E's subsequent slices to indicate the execution order. + * + * The idea to calculate the subsequent slices of slice E is to view + * the slice group as a tree where the top-level slice A is the root node. + * The preorder depth-first-search (DFS) order is naturally equivalent + * to the function call order. We just need to perform a DFS, and start + * recording the slices after we see the occurance of E. + */ + iterateAllSubsequentSlices(callback, opt_this) { + const parentStack = []; + let started = false; + + // get the root node and push it to the DFS stack + const topmostSlice = this.mostTopLevelSlice; + parentStack.push(topmostSlice); + + // Using the stack to perform DFS + while (parentStack.length !== 0) { + const curSlice = parentStack.pop(); + + if (started) { + callback.call(opt_this, curSlice); + } else { + started = (curSlice.guid === this.guid); + } + + for (let i = curSlice.subSlices.length - 1; i >= 0; i--) { + parentStack.push(curSlice.subSlices[i]); + } + } + }, + + get subsequentSlices() { + const res = []; + + this.iterateAllSubsequentSlices(function(subseqSlice) { + res.push(subseqSlice); + }); + + return res; + }, + + /** + * Obtains the parents of a slice, from the most immediate to the root. + * + * For instance, E.enumerateAllAncestors() in the following example: + * [ A ] + * [ B][ D ][ G ] + * [C] [E][F] [H] + * will yield D, then A, in the order from the leaves to the root. + */ + * enumerateAllAncestors() { + let curSlice = this.parentSlice; + while (curSlice) { + yield curSlice; + curSlice = curSlice.parentSlice; + } + }, + + get ancestorSlices() { + return Array.from(this.enumerateAllAncestors()); + }, + + iterateEntireHierarchy(callback, opt_this) { + const mostTopLevelSlice = this.mostTopLevelSlice; + callback.call(opt_this, mostTopLevelSlice); + mostTopLevelSlice.iterateAllSubsequentSlices(callback, opt_this); + }, + + get entireHierarchy() { + const res = []; + + this.iterateEntireHierarchy(function(slice) { + res.push(slice); + }); + + return res; + }, + + /** + * Returns this slice, and its ancestor and subsequent slices. + * + * For instance, E.ancestorAndSubsequentSlices in the following example: + * [ A ] + * [ B][ D ][ G ] + * [C] [E][F] [H] + * will return E, D, A, F, G, and H, where E is itself, D and A are + * E's ancestors, and F, G, and H are subsequent slices of E + */ + get ancestorAndSubsequentSlices() { + const res = []; + + res.push(this); + + for (const aSlice of this.enumerateAllAncestors()) { + res.push(aSlice); + } + + this.iterateAllSubsequentSlices(function(sSlice) { + res.push(sSlice); + }); + + return res; + }, + + * enumerateAllDescendents() { + for (const slice of this.subSlices) { + yield slice; + } + for (const slice of this.subSlices) { + yield* slice.enumerateAllDescendents(); + } + }, + + get descendentSlices() { + const res = []; + for (const slice of this.enumerateAllDescendents()) { + res.push(slice); + } + return res; + } + + }; + + return { + Slice, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/slice_group.html b/chromium/third_party/catapult/tracing/tracing/model/slice_group.html new file mode 100644 index 00000000000..82d24283cf4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/slice_group.html @@ -0,0 +1,720 @@ +<!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/color_scheme.html"> +<link rel="import" href="/tracing/base/guid.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/filter.html"> +<link rel="import" href="/tracing/model/event_container.html"> +<link rel="import" href="/tracing/model/thread_slice.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the SliceGroup class. + */ +tr.exportTo('tr.model', function() { + const ColorScheme = tr.b.ColorScheme; + const ThreadSlice = tr.model.ThreadSlice; + + function getSliceLo(s) { + return s.start; + } + + function getSliceHi(s) { + return s.end; + } + + /** + * A group of Slices, plus code to create them from B/E events, as + * well as arrange them into subRows. + * + * Do not mutate the slices array directly. Modify it only by + * SliceGroup mutation methods. + * + * @constructor + * @param {function(new:Slice, category, title, colorId, start, args)=} + * opt_sliceConstructor The constructor to use when creating slices. + * @extends {tr.model.EventContainer} + */ + function SliceGroup(parentContainer, opt_sliceConstructor, opt_name) { + tr.model.EventContainer.call(this); + + this.parentContainer_ = parentContainer; + + const sliceConstructor = opt_sliceConstructor || ThreadSlice; + this.sliceConstructor = sliceConstructor; + this.sliceConstructorSubTypes = this.sliceConstructor.subTypes; + if (!this.sliceConstructorSubTypes) { + throw new Error('opt_sliceConstructor must have a subtype registry.'); + } + + this.openPartialSlices_ = []; + + this.slices = []; + this.topLevelSlices = []; + this.haveTopLevelSlicesBeenBuilt = false; + this.name_ = opt_name; + + if (this.model === undefined) { + throw new Error('SliceGroup must have model defined.'); + } + } + + SliceGroup.prototype = { + __proto__: tr.model.EventContainer.prototype, + + get parentContainer() { + return this.parentContainer_; + }, + + get model() { + return this.parentContainer_.model; + }, + + get stableId() { + return this.parentContainer_.stableId + '.SliceGroup'; + }, + + getSettingsKey() { + if (!this.name_) return undefined; + const parentKey = this.parentContainer_.getSettingsKey(); + if (!parentKey) return undefined; + return parentKey + '.' + this.name; + }, + + /** + * @return {Number} The number of slices in this group. + */ + get length() { + return this.slices.length; + }, + + /** + * Helper function that pushes the provided slice onto the slices array. + * @param {Slice} slice The slice to be added to the slices array. + */ + pushSlice(slice) { + this.haveTopLevelSlicesBeenBuilt = false; + slice.parentContainer = this.parentContainer_; + this.slices.push(slice); + return slice; + }, + + /** + * Helper function that pushes the provided slices onto the slices array. + * @param {Array.<Slice>} slices An array of slices to be added. + */ + pushSlices(slices) { + this.haveTopLevelSlicesBeenBuilt = false; + slices.forEach(function(slice) { + slice.parentContainer = this.parentContainer_; + this.slices.push(slice); + }, this); + }, + + /** + * Opens a new slice in the group's slices. + * + * Calls to beginSlice and + * endSlice must be made with non-monotonically-decreasing timestamps. + * + * @param {String} category Category name of the slice to add. + * @param {String} title Title of the slice to add. + * @param {Number} ts The timetsamp of the slice, in milliseconds. + * @param {Object.<string, Object>=} opt_args Arguments associated with + * the slice. + * @param {Number=} opt_colorId The color of the slice, defined by + * its palette id (see base/color_scheme.html). + */ + beginSlice(category, title, ts, opt_args, opt_tts, + opt_argsStripped, opt_colorId, opt_bindId) { + const colorId = opt_colorId || + ColorScheme.getColorIdForGeneralPurposeString(title); + const sliceConstructorSubTypes = this.sliceConstructorSubTypes; + const sliceType = sliceConstructorSubTypes.getConstructor( + category, title); + const slice = new sliceType(category, title, colorId, ts, + opt_args ? opt_args : {}, null, + opt_tts, undefined, + opt_argsStripped, opt_bindId); + this.openPartialSlices_.push(slice); + slice.didNotFinish = true; + this.pushSlice(slice); + + return slice; + }, + + isTimestampValidForBeginOrEnd(ts) { + if (!this.openPartialSlices_.length) return true; + const top = this.openPartialSlices_[this.openPartialSlices_.length - 1]; + return ts >= top.start; + }, + + /** + * @return {Number} The number of beginSlices for which an endSlice has not + * been issued. + */ + get openSliceCount() { + return this.openPartialSlices_.length; + }, + + get mostRecentlyOpenedPartialSlice() { + if (!this.openPartialSlices_.length) return undefined; + return this.openPartialSlices_[this.openPartialSlices_.length - 1]; + }, + + /** + * Ends the last begun slice in this group and pushes it onto the slice + * array. + * + * @param {Number} ts Timestamp when the slice ended + * @param {Number=} opt_colorId The color of the slice, defined by + * its palette id (see base/color_scheme.html). + * @return {Slice} slice. + */ + endSlice(ts, opt_tts, opt_colorId) { + if (!this.openSliceCount) { + throw new Error('endSlice called without an open slice'); + } + + const slice = this.openPartialSlices_[this.openSliceCount - 1]; + this.openPartialSlices_.splice(this.openSliceCount - 1, 1); + if (ts < slice.start) { + throw new Error('Slice ' + slice.title + + ' end time is before its start.'); + } + + slice.duration = ts - slice.start; + slice.didNotFinish = false; + slice.colorId = opt_colorId || slice.colorId; + + if (opt_tts && slice.cpuStart !== undefined) { + slice.cpuDuration = opt_tts - slice.cpuStart; + } + + return slice; + }, + + /** + * Push a complete event as a Slice into the slice list. + * The timestamp can be in any order. + * + * @param {String} category Category name of the slice to add. + * @param {String} title Title of the slice to add. + * @param {Number} ts The timetsamp of the slice, in milliseconds. + * @param {Number} duration The duration of the slice, in milliseconds. + * @param {Object.<string, Object>=} opt_args Arguments associated with + * the slice. + * @param {Number=} opt_colorId The color of the slice, as defined by + * its palette id (see base/color_scheme.html). + */ + pushCompleteSlice(category, title, ts, duration, tts, + cpuDuration, opt_args, opt_argsStripped, + opt_colorId, opt_bindId) { + const colorId = opt_colorId || + ColorScheme.getColorIdForGeneralPurposeString(title); + const sliceConstructorSubTypes = this.sliceConstructorSubTypes; + const sliceType = sliceConstructorSubTypes.getConstructor( + category, title); + const slice = new sliceType(category, title, colorId, ts, + opt_args ? opt_args : {}, + duration, tts, cpuDuration, + opt_argsStripped, opt_bindId); + if (duration === undefined) { + slice.didNotFinish = true; + } + this.pushSlice(slice); + return slice; + }, + + /** + * Closes any open slices. + * @param {Number=} opt_maxTimestamp The end time to use for the closed + * slices. If not provided, + * the max timestamp for this slice is provided. + */ + autoCloseOpenSlices() { + this.updateBounds(); + const maxTimestamp = this.bounds.max; + for (let sI = 0; sI < this.slices.length; sI++) { + const slice = this.slices[sI]; + if (slice.didNotFinish) { + slice.duration = maxTimestamp - slice.start; + } + } + this.openPartialSlices_ = []; + }, + + /** + * Shifts all the timestamps inside this group forward by the amount + * specified. + */ + shiftTimestampsForward(amount) { + for (let sI = 0; sI < this.slices.length; sI++) { + const slice = this.slices[sI]; + slice.start = (slice.start + amount); + } + }, + + /** + * Updates the bounds for this group based on the slices it contains. + */ + updateBounds() { + this.bounds.reset(); + for (let i = 0; i < this.slices.length; i++) { + this.bounds.addValue(this.slices[i].start); + this.bounds.addValue(this.slices[i].end); + } + }, + + copySlice(slice) { + const sliceConstructorSubTypes = this.sliceConstructorSubTypes; + const sliceType = sliceConstructorSubTypes.getConstructor(slice.category, + slice.title); + const newSlice = new sliceType(slice.category, slice.title, + slice.colorId, slice.start, + slice.args, slice.duration, slice.cpuStart, slice.cpuDuration); + newSlice.didNotFinish = slice.didNotFinish; + return newSlice; + }, + + * findTopmostSlicesInThisContainer(eventPredicate, opt_this) { + if (!this.haveTopLevelSlicesBeenBuilt) { + throw new Error('Nope'); + } + + for (const s of this.topLevelSlices) { + yield* s.findTopmostSlicesRelativeToThisSlice(eventPredicate); + } + }, + + * childEvents() { + yield* this.slices; + }, + + * childEventContainers() { + }, + + /** + * Provides a more efficient implementation than the default implementation + * in event_container.html, since child events are sorted. + */ + * getDescendantEventsInSortedRanges(ranges, opt_containerPredicate) { + if (opt_containerPredicate === undefined || + opt_containerPredicate(this)) { + let rangeIndex = 0; + let range = ranges[rangeIndex]; + for (const event of this.childEvents()) { + while (event.start > range.max) { + rangeIndex++; + if (rangeIndex >= ranges.length) return; + range = ranges[rangeIndex]; + } + if (event.end >= range.min) yield event; + } + } + }, + + getSlicesOfName(title) { + const slices = []; + for (let i = 0; i < this.slices.length; i++) { + if (this.slices[i].title === title) { + slices.push(this.slices[i]); + } + } + return slices; + }, + + iterSlicesInTimeRange(callback, start, end) { + const ret = []; + tr.b.iterateOverIntersectingIntervals( + this.topLevelSlices, + function(s) { return s.start; }, + function(s) { return s.duration; }, + start, + end, + function(topLevelSlice) { + callback(topLevelSlice); + for (const slice of topLevelSlice.enumerateAllDescendents()) { + callback(slice); + } + }); + return ret; + }, + + findFirstSlice() { + if (!this.haveTopLevelSlicesBeenBuilt) { + throw new Error('Nope'); + } + if (0 === this.slices.length) return undefined; + return this.slices[0]; + }, + + findSliceAtTs(ts) { + if (!this.haveTopLevelSlicesBeenBuilt) throw new Error('Nope'); + let i = tr.b.findIndexInSortedClosedIntervals( + this.topLevelSlices, + getSliceLo, getSliceHi, + ts); + if (i === -1 || i === this.topLevelSlices.length) { + return undefined; + } + + let curSlice = this.topLevelSlices[i]; + + // Now recurse on slice looking for subSlice of given ts. + while (true) { + i = tr.b.findIndexInSortedClosedIntervals( + curSlice.subSlices, + getSliceLo, getSliceHi, + ts); + if (i === -1 || i === curSlice.subSlices.length) { + return curSlice; + } + curSlice = curSlice.subSlices[i]; + } + }, + + findNextSliceAfter(ts, refGuid) { + let i = tr.b.findLowIndexInSortedArray(this.slices, getSliceLo, ts); + if (i === this.slices.length) { + return undefined; + } + for (; i < this.slices.length; i++) { + const slice = this.slices[i]; + if (slice.start > ts) return slice; + if (slice.guid <= refGuid) continue; + return slice; + } + return undefined; + }, + + /** + * This function assumes that if any slice has a cpu duration then + * then the group is considered to have cpu duration. + */ + hasCpuDuration_() { + if (this.slices.some(function(slice) { + return slice.cpuDuration !== undefined; + })) return true; + return false; + }, + + /** + * Construct subSlices for this group. + * Populate the group topLevelSlices, parent slices get a subSlices[], + * a selfThreadTime and a selfTime, child slices get a parentSlice + * reference. + */ + createSubSlices() { + this.haveTopLevelSlicesBeenBuilt = true; + this.createSubSlicesImpl_(); + // If another source has cpu time, we can augment the cpuDuration of the + // slices in the group with that cpu time. This should be done only if + // the original source does not include cpuDuration. + if (!this.hasCpuDuration_() && this.parentContainer.timeSlices) { + this.addCpuTimeToSubslices_(this.parentContainer.timeSlices); + } + this.slices.forEach(function(slice) { + let selfTime = slice.duration; + for (let i = 0; i < slice.subSlices.length; i++) { + selfTime -= slice.subSlices[i].duration; + } + slice.selfTime = selfTime; + + if (slice.cpuDuration === undefined) return; + + let cpuSelfTime = slice.cpuDuration; + for (let i = 0; i < slice.subSlices.length; i++) { + if (slice.subSlices[i].cpuDuration !== undefined) { + cpuSelfTime -= slice.subSlices[i].cpuDuration; + } + } + slice.cpuSelfTime = cpuSelfTime; + }); + }, + createSubSlicesImpl_() { + const precisionUnit = this.model.intrinsicTimeUnit; + + + // Note that this doesn't check whether |child| should be added to + // |parent|'s descendant slices instead of |parent| directly. + function addSliceIfBounds(parent, child) { + if (parent.bounds(child, precisionUnit)) { + child.parentSlice = parent; + if (parent.subSlices === undefined) { + parent.subSlices = []; + } + parent.subSlices.push(child); + return true; + } + return false; + } + + if (!this.slices.length) return; + + const ops = []; + for (let i = 0; i < this.slices.length; i++) { + if (this.slices[i].subSlices) { + this.slices[i].subSlices.splice(0, + this.slices[i].subSlices.length); + } + ops.push(i); + } + + const originalSlices = this.slices; + ops.sort(function(ix, iy) { + const x = originalSlices[ix]; + const y = originalSlices[iy]; + if (x.start !== y.start) { + return x.start - y.start; + } + + // Elements get inserted into the slices array in order of when the + // slices start. Because slices must be properly nested, we break + // start-time ties by assuming that the elements appearing earlier + // in the slices array (and thus ending earlier) start earlier. + return ix - iy; + }); + + const slices = new Array(this.slices.length); + for (let i = 0; i < ops.length; i++) { + slices[i] = originalSlices[ops[i]]; + } + + // Actually build the subrows. + let rootSlice = slices[0]; + this.topLevelSlices = []; + this.topLevelSlices.push(rootSlice); + rootSlice.isTopLevel = true; + for (let i = 1; i < slices.length; i++) { + const slice = slices[i]; + while (rootSlice !== undefined && + (!addSliceIfBounds(rootSlice, slice))) { + rootSlice = rootSlice.parentSlice; + } + if (rootSlice === undefined) { + this.topLevelSlices.push(slice); + slice.isTopLevel = true; + } + rootSlice = slice; + } + + // Keep the slices in sorted form. + this.slices = slices; + }, + addCpuTimeToSubslices_(timeSlices) { + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + let sliceIdx = 0; + timeSlices.forEach(function(timeSlice) { + if (timeSlice.schedulingState === SCHEDULING_STATE.RUNNING) { + while (sliceIdx < this.topLevelSlices.length) { + if (this.addCpuTimeToSubslice_(this.topLevelSlices[sliceIdx], + timeSlice)) { + // The current top-level slice and children are fully + // accounted for, proceed to next top-level slice. + sliceIdx++; + } else { + // The current top-level runs beyond the time slice, break out + // so we can potentially add more time slices to it + break; + } + } + } + }, this); + }, + /* Add run-time of this timeSlice to the passed in slice + * and all of it's children (recursively). + * Returns whether the slice ends before or at the end of the + * time slice, signaling we are done with this slice. + */ + addCpuTimeToSubslice_(slice, timeSlice) { + // Make sure they overlap + if (slice.start > timeSlice.end || slice.end < timeSlice.start) { + return slice.end <= timeSlice.end; + } + + // Compute actual overlap + let duration = timeSlice.duration; + if (slice.start > timeSlice.start) { + duration -= slice.start - timeSlice.start; + } + if (timeSlice.end > slice.end) { + duration -= timeSlice.end - slice.end; + } + + if (slice.cpuDuration) { + slice.cpuDuration += duration; + } else { + slice.cpuDuration = duration; + } + + for (let i = 0; i < slice.subSlices.length; i++) { + this.addCpuTimeToSubslice_(slice.subSlices[i], timeSlice); + } + + return slice.end <= timeSlice.end; + } + }; + + /** + * Merge two slice groups. + * + * If the two groups do not nest properly some of the slices of groupB will + * be split to accomodate the improper nesting. This is done to accomodate + * combined kernel and userland call stacks on Android. Because userland + * tracing is done by writing to the trace_marker file, the kernel calls + * that get invoked as part of that write may not be properly nested with + * the userland call trace. For example the following sequence may occur: + * + * kernel enter sys_write (the write to trace_marker) + * user enter some_function + * kernel exit sys_write + * ... + * kernel enter sys_write (the write to trace_marker) + * user exit some_function + * kernel exit sys_write + * + * This is handled by splitting the sys_write call into two slices as + * follows: + * + * | sys_write | some_function | sys_write (cont.) | + * | sys_write (cont.) | | sys_write | + * + * The colorId of both parts of the split slices are kept the same, and the + * " (cont.)" suffix is appended to the later parts of a split slice. + * + * The two input SliceGroups are not modified by this, and the merged + * SliceGroup will contain a copy of each of the input groups' slices (those + * copies may be split). + */ + SliceGroup.merge = function(groupA, groupB) { + // This is implemented by traversing the two slice groups in reverse + // order. The slices in each group are sorted by ascending end-time, so + // we must do the traversal from back to front in order to maintain the + // sorting. + // + // We traverse the two groups simultaneously, merging as we go. At each + // iteration we choose the group from which to take the next slice based + // on which group's next slice has the greater end-time. During this + // traversal we maintain a stack of currently "open" slices for each input + // group. A slice is considered "open" from the time it gets reached in + // our input group traversal to the time we reach an slice in this + // traversal with an end-time before the start time of the "open" slice. + // + // Each time a slice from groupA is opened or closed (events corresponding + // to the end-time and start-time of the input slice, respectively) we + // split all of the currently open slices from groupB. + + if (groupA.openPartialSlices_.length > 0) { + throw new Error('groupA has open partial slices'); + } + + if (groupB.openPartialSlices_.length > 0) { + throw new Error('groupB has open partial slices'); + } + + if (groupA.parentContainer !== groupB.parentContainer) { + throw new Error('Different parent threads. Cannot merge'); + } + + if (groupA.sliceConstructor !== groupB.sliceConstructor) { + throw new Error('Different slice constructors. Cannot merge'); + } + + const result = new SliceGroup(groupA.parentContainer, + groupA.sliceConstructor, + groupA.name_); + + const slicesA = groupA.slices; + const slicesB = groupB.slices; + let idxA = 0; + let idxB = 0; + const openA = []; + const openB = []; + + const splitOpenSlices = function(when) { + for (let i = 0; i < openB.length; i++) { + const oldSlice = openB[i]; + const oldEnd = oldSlice.end; + if (when < oldSlice.start || oldEnd < when) { + throw new Error('slice should not be split'); + } + + const newSlice = result.copySlice(oldSlice); + newSlice.start = when; + newSlice.duration = oldEnd - when; + if (newSlice.title.indexOf(' (cont.)') === -1) { + newSlice.title += ' (cont.)'; + } + oldSlice.duration = when - oldSlice.start; + openB[i] = newSlice; + result.pushSlice(newSlice); + } + }; + + const closeOpenSlices = function(upTo) { + while (openA.length > 0 || openB.length > 0) { + const nextA = openA[openA.length - 1]; + const nextB = openB[openB.length - 1]; + const endA = nextA && nextA.end; + const endB = nextB && nextB.end; + + if ((endA === undefined || endA > upTo) && + (endB === undefined || endB > upTo)) { + return; + } + + if (endB === undefined || endA < endB) { + splitOpenSlices(endA); + openA.pop(); + } else { + openB.pop(); + } + } + }; + + while (idxA < slicesA.length || idxB < slicesB.length) { + const sA = slicesA[idxA]; + const sB = slicesB[idxB]; + let nextSlice; + let isFromB; + + if (sA === undefined || (sB !== undefined && sA.start > sB.start)) { + nextSlice = result.copySlice(sB); + isFromB = true; + idxB++; + } else { + nextSlice = result.copySlice(sA); + isFromB = false; + idxA++; + } + + closeOpenSlices(nextSlice.start); + + result.pushSlice(nextSlice); + + if (isFromB) { + openB.push(nextSlice); + } else { + splitOpenSlices(nextSlice.start); + openA.push(nextSlice); + } + } + + closeOpenSlices(); + + return result; + }; + + return { + SliceGroup, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/slice_group_test.html b/chromium/third_party/catapult/tracing/tracing/model/slice_group_test.html new file mode 100644 index 00000000000..573456fba82 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/slice_group_test.html @@ -0,0 +1,935 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/model/slice_group.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Slice = tr.model.Slice; + const SliceGroup = tr.model.SliceGroup; + const newSliceEx = tr.c.TestUtils.newSliceEx; + const newThreadSlice = tr.c.TestUtils.newThreadSlice; + const newModel = tr.c.TestUtils.newModel; + const newFakeThread = tr.c.TestUtils.newFakeThread; + + test('basicBeginEnd', function() { + const group = new SliceGroup(newFakeThread()); + assert.strictEqual(group.openSliceCount, 0); + const sliceA = group.beginSlice('', 'a', 1, {a: 1}); + assert.strictEqual(group.openSliceCount, 1); + assert.strictEqual(sliceA.title, 'a'); + assert.strictEqual(sliceA.start, 1); + assert.strictEqual(sliceA.args.a, 1); + + const sliceB = group.endSlice(3); + assert.strictEqual(sliceA, sliceB); + assert.strictEqual(sliceB.duration, 2); + }); + + test('subSlicesBuilderBasic', function() { + const group = new SliceGroup(newFakeThread()); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2})); + const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 2); + assert.deepEqual(group.topLevelSlices, [sA, sB]); + assert.strictEqual(group.findSliceAtTs(0), undefined); + assert.strictEqual(group.findSliceAtTs(1), sA); + assert.strictEqual(group.findSliceAtTs(3), sA); + assert.strictEqual(group.findSliceAtTs(3.1), sB); + assert.strictEqual(group.findSliceAtTs(4), sB); + assert.strictEqual(group.findSliceAtTs(5), undefined); + }); + + test('subSlicesBuilderBasic2', function() { + const group = new SliceGroup(newFakeThread()); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4})); + const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 1); + assert.deepEqual(group.topLevelSlices, [sA]); + + assert.strictEqual(sA.subSlices.length, 1); + assert.deepEqual(sA.subSlices, [sB]); + assert.strictEqual(sA.selfTime, 3); + + assert.strictEqual(sA, sB.parentSlice); + assert.isTrue(sA.isTopLevel); + assert.isFalse(sB.isTopLevel); + }); + + test('subSlicesBuilderNestedExactly', function() { + const group = new SliceGroup(newFakeThread()); + const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 4})); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 4})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 1); + assert.deepEqual(group.topLevelSlices, [sB]); + + assert.strictEqual(sB.subSlices.length, 1); + assert.deepEqual(sB.subSlices, [sA]); + assert.strictEqual(sB.selfTime, 0); + + assert.strictEqual(sB, sA.parentSlice); + assert.isTrue(sB.isTopLevel); + assert.isFalse(sA.isTopLevel); + + assert.strictEqual(group.findSliceAtTs(0), undefined); + assert.strictEqual(group.findSliceAtTs(1), sA); + assert.strictEqual(group.findSliceAtTs(2), sA); + assert.strictEqual(group.findSliceAtTs(5), sA); + assert.strictEqual(group.findSliceAtTs(6), undefined); + }); + + test('subSlicesBuilderInstantEvents', function() { + const group = new SliceGroup(newFakeThread()); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0})); + const sB = group.pushSlice(newSliceEx({title: 'b', start: 2, duration: 0})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 2); + assert.deepEqual(group.topLevelSlices, [sA, sB]); + assert.strictEqual(group.findSliceAtTs(1), sA); + assert.strictEqual(group.findSliceAtTs(1.5), undefined); + assert.strictEqual(group.findSliceAtTs(2), sB); + }); + + test('subSlicesBuilderTwoInstantEvents', function() { + const group = new SliceGroup(newFakeThread()); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0})); + const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, duration: 0})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 1); + assert.deepEqual(group.topLevelSlices, [sA]); + + assert.strictEqual(sA.subSlices.length, 1); + assert.deepEqual(sA.subSlices, [sB]); + assert.strictEqual(sA.selfTime, 0); + + assert.strictEqual(sA, sB.parentSlice); + assert.isTrue(sA.isTopLevel); + assert.isFalse(sB.isTopLevel); + assert.strictEqual(group.findSliceAtTs(1), sB); + assert.strictEqual(group.findSliceAtTs(1.0001), undefined); + }); + + test('subSlicesBuilderOutOfOrderAddition', function() { + const group = new SliceGroup(newFakeThread()); + + // Pattern being tested: + // [ a ][ b ] + // Where insertion is done backward. + const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1})); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 2})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 2); + assert.deepEqual(group.topLevelSlices, [sA, sB]); + assert.isTrue(sA.isTopLevel); + assert.isTrue(sB.isTopLevel); + assert.strictEqual(group.findSliceAtTs(3), sA); + }); + + test('subRowBuilderOutOfOrderAddition2', function() { + const group = new SliceGroup(newFakeThread()); + + // Pattern being tested: + // [ a ] + // [ b ] + // Where insertion is done backward. + const sB = group.pushSlice(newSliceEx({title: 'b', start: 3, duration: 1})); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 5})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 1); + assert.deepEqual(group.topLevelSlices, [sA]); + + assert.strictEqual(sA.subSlices.length, 1); + assert.deepEqual(sA.subSlices, [sB]); + assert.strictEqual(sA.selfTime, 4); + + assert.strictEqual(sA, sB.parentSlice); + assert.isTrue(sA.isTopLevel); + assert.isFalse(sB.isTopLevel); + assert.strictEqual(group.findSliceAtTs(1.5), sA); + assert.strictEqual(group.findSliceAtTs(3), sB); + assert.strictEqual(group.findSliceAtTs(3.5), sB); + assert.strictEqual(group.findSliceAtTs(4), sB); + assert.strictEqual(group.findSliceAtTs(4.5), sA); + }); + + test('subSlicesBuilderOnNestedZeroLength', function() { + const group = new SliceGroup(newFakeThread()); + + // Pattern being tested: + // [ a ] + // [ b1 ] []<- b2 where b2.duration = 0 and b2.end === a.end. + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3})); + const sB1 = group.pushSlice(newSliceEx( + {title: 'b1', start: 1, duration: 2})); + const sB2 = group.pushSlice(newSliceEx( + {title: 'b2', start: 4, duration: 0})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 1); + assert.deepEqual(group.topLevelSlices, [sA]); + + assert.strictEqual(sA.subSlices.length, 2); + assert.deepEqual(sA.subSlices, [sB1, sB2]); + assert.strictEqual(sA.selfTime, 1); + + assert.strictEqual(sA, sB1.parentSlice); + assert.strictEqual(sA, sB2.parentSlice); + assert.strictEqual(group.findSliceAtTs(1), sB1); + assert.strictEqual(group.findSliceAtTs(4), sB2); + }); + + test('subSlicesBuilderOnGroup1', function() { + const group = new SliceGroup(newFakeThread()); + + // Pattern being tested: + // [ a ] [ c ] + // [ b ] + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3})); + const sB = group.pushSlice(newSliceEx( + {title: 'b', start: 1.5, duration: 1})); + const sC = group.pushSlice(newSliceEx({title: 'c', start: 5, duration: 0})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 2); + assert.deepEqual(group.topLevelSlices, [sA, sC]); + + assert.strictEqual(sA.subSlices.length, 1); + assert.deepEqual(sA.subSlices, [sB]); + assert.strictEqual(sA.selfTime, 2); + + assert.strictEqual(sA, sB.parentSlice); + assert.strictEqual(group.findSliceAtTs(1), sA); + assert.strictEqual(group.findSliceAtTs(2), sB); + assert.strictEqual(group.findSliceAtTs(3), sA); + assert.strictEqual(group.findSliceAtTs(4.5), undefined); + assert.strictEqual(group.findSliceAtTs(5), sC); + }); + + test('subSlicesBuilderOnGroup2', function() { + const group = new SliceGroup(newFakeThread()); + + // Pattern being tested: + // [ a ] [ d ] + // [ b ] + // [ c ] + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3})); + const sB = group.pushSlice(newSliceEx( + {title: 'b', start: 1.5, duration: 1})); + const sC = group.pushSlice(newSliceEx( + {title: 'c', start: 1.75, duration: 0.5})); + const sD = group.pushSlice(newSliceEx( + {title: 'd', start: 5, duration: 0.25})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 2); + assert.deepEqual(group.topLevelSlices, [sA, sD]); + + assert.strictEqual(sA.subSlices.length, 1); + assert.deepEqual(sA.subSlices, [sB]); + assert.strictEqual(sA.selfTime, 2); + + assert.strictEqual(sA, sB.parentSlice); + assert.strictEqual(sB.subSlices.length, 1); + assert.deepEqual(sB.subSlices, [sC]); + assert.strictEqual(sB.selfTime, 0.5); + + assert.strictEqual(sB, sC.parentSlice); + assert.strictEqual(group.findSliceAtTs(2), sC); + }); + + test('findFirstSlice', function() { + const group = new SliceGroup(newFakeThread()); + // Pattern being tested: + // [ a ] [ d ] + // [b] [ c ] where b is dur=0 + const sC = group.pushSlice(newSliceEx({title: 'c', start: 2, end: 3})); + const sD = group.pushSlice(newSliceEx({title: 'd', start: 5, end: 6})); + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, end: 4})); + const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, end: 1})); + + assert.throws(group.findFirstSlice); + + group.createSubSlices(); + + assert.strictEqual(group.findFirstSlice(), sA); + }); + + test('findNextSliceAfterBasic', function() { + const group = new SliceGroup(newFakeThread()); + // Pattern being tested: + // [ a ] [ d ] + // [b] [ c ] where b is dur=0 + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, end: 4})); + const sB = group.pushSlice(newSliceEx({title: 'b', start: 1, end: 1})); + const sC = group.pushSlice(newSliceEx({title: 'c', start: 2, end: 3})); + const sD = group.pushSlice(newSliceEx({title: 'd', start: 5, end: 6})); + + group.createSubSlices(); + + assert.strictEqual(group.findNextSliceAfter(0, 0), sA); + assert.strictEqual(group.findNextSliceAfter(1, sA.guid), sB); + assert.strictEqual(group.findNextSliceAfter(1, sB.guid), sC); + assert.strictEqual(group.findNextSliceAfter(2, sC.guid), sD); + assert.strictEqual(group.findNextSliceAfter(6, 0), undefined); + }); + + test('subSlicesBuilderTolerateFPInaccuracy', function() { + const group = new SliceGroup(newFakeThread()); + + // Pattern being tested: + // [ a ] + // [ b ] where b.end contains a tiny FP calculation error. + const sA = group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 3})); + const sB = group.pushSlice(newSliceEx( + {title: 'b', start: 1, duration: 3.0000000001})); + + group.createSubSlices(); + + assert.strictEqual(group.topLevelSlices.length, 1); + assert.deepEqual(group.topLevelSlices, [sA]); + + assert.strictEqual(sA.subSlices.length, 1); + assert.deepEqual(sA.subSlices, [sB]); + assert.strictEqual(sA, sB.parentSlice); + assert.strictEqual(group.findSliceAtTs(1), sB); + assert.strictEqual(group.findSliceAtTs(3), sB); + }); + + test('basicMerge', function() { + function TestSlice( + cat, title, colorId, start, args, opt_duration, + opt_cpuStart, opt_cpuDuration) { + Slice.call(this, cat, title, colorId, start, args, opt_duration, + opt_cpuStart, opt_cpuDuration); + } + TestSlice.prototype = { + __proto__: Slice.prototype + }; + TestSlice.subTypes = { + getConstructor(category, title) { + return TestSlice; + } + }; + + const thread = newFakeThread(); + const a = new SliceGroup(thread, TestSlice); + const b = new SliceGroup(thread, TestSlice); + a.beginSlice('', 'one', 1); + a.endSlice(2); + b.beginSlice('', 'two', 3); + b.endSlice(5); + + const m = SliceGroup.merge(a, b); + assert.strictEqual(m.slices.length, 2); + + assert.strictEqual(m.slices[0].title, 'one'); + assert.strictEqual(m.slices[0].start, 1); + assert.strictEqual(m.slices[0].duration, 1); + + assert.strictEqual(m.slices[1].title, 'two'); + assert.strictEqual(m.slices[1].start, 3); + assert.strictEqual(m.slices[1].duration, 2); + + // ensure constructor merged correctly + assert.strictEqual(m.sliceConstructor, TestSlice); + }); + + test('nestedMerge', function() { + const thread = newFakeThread(); + const a = new SliceGroup(thread); + const b = new SliceGroup(thread); + a.beginSlice('', 'one', 1); + a.endSlice(4); + b.beginSlice('', 'two', 2); + b.endSlice(3); + + const m = SliceGroup.merge(a, b); + assert.strictEqual(m.slices.length, 2); + + assert.strictEqual(m.slices[0].title, 'one'); + assert.strictEqual(m.slices[0].start, 1); + assert.strictEqual(m.slices[0].duration, 3); + + assert.strictEqual(m.slices[1].title, 'two'); + assert.strictEqual(m.slices[1].start, 2); + assert.strictEqual(m.slices[1].duration, 1); + }); + + test('startSplitMerge', function() { + const thread = newFakeThread(); + const a = new SliceGroup(thread); + const b = new SliceGroup(thread); + a.beginSlice('', 'one', 2); + a.endSlice(4); + b.beginSlice('', 'two', 1); + b.endSlice(3); + + const m = SliceGroup.merge(a, b); + assert.strictEqual(m.slices.length, 3); + + assert.strictEqual(m.slices[0].title, 'two'); + assert.strictEqual(m.slices[0].start, 1); + assert.strictEqual(m.slices[0].duration, 1); + + assert.strictEqual(m.slices[1].title, 'one'); + assert.strictEqual(m.slices[1].start, 2); + assert.strictEqual(m.slices[1].duration, 2); + + assert.strictEqual(m.slices[2].title, 'two (cont.)'); + assert.strictEqual(m.slices[2].start, 2); + assert.strictEqual(m.slices[2].duration, 1); + }); + + test('startSplitTwoMerge', function() { + const thread = newFakeThread(); + const a = new SliceGroup(thread); + const b = new SliceGroup(thread); + a.beginSlice('', 'one', 3); + a.endSlice(6); + b.beginSlice('', 'two', 1); + b.beginSlice('', 'three', 2); + b.endSlice(4); + b.endSlice(5); + + const m = SliceGroup.merge(a, b); + assert.strictEqual(m.slices.length, 5); + + assert.strictEqual(m.slices[0].title, 'two'); + assert.strictEqual(m.slices[0].start, 1); + assert.strictEqual(m.slices[0].duration, 2); + + assert.strictEqual(m.slices[1].title, 'three'); + assert.strictEqual(m.slices[1].start, 2); + assert.strictEqual(m.slices[1].duration, 1); + + assert.strictEqual(m.slices[2].title, 'one'); + assert.strictEqual(m.slices[2].start, 3); + assert.strictEqual(m.slices[2].duration, 3); + + assert.strictEqual(m.slices[3].title, 'two (cont.)'); + assert.strictEqual(m.slices[3].start, 3); + assert.strictEqual(m.slices[3].duration, 2); + + assert.strictEqual(m.slices[4].title, 'three (cont.)'); + assert.strictEqual(m.slices[4].start, 3); + assert.strictEqual(m.slices[4].duration, 1); + }); + + test('startSplitTwiceMerge', function() { + const thread = newFakeThread(); + const a = new SliceGroup(thread); + const b = new SliceGroup(thread); + a.beginSlice('', 'one', 2); + a.beginSlice('', 'two', 3); + a.endSlice(5); + a.endSlice(6); + b.beginSlice('', 'three', 1); + b.endSlice(4); + + const m = SliceGroup.merge(a, b); + assert.strictEqual(m.slices.length, 5); + + assert.strictEqual(m.slices[0].title, 'three'); + assert.strictEqual(m.slices[0].start, 1); + assert.strictEqual(m.slices[0].duration, 1); + + assert.strictEqual(m.slices[1].title, 'one'); + assert.strictEqual(m.slices[1].start, 2); + assert.strictEqual(m.slices[1].duration, 4); + + assert.strictEqual(m.slices[2].title, 'three (cont.)'); + assert.strictEqual(m.slices[2].start, 2); + assert.strictEqual(m.slices[2].duration, 1); + + assert.strictEqual(m.slices[3].title, 'two'); + assert.strictEqual(m.slices[3].start, 3); + assert.strictEqual(m.slices[3].duration, 2); + + assert.strictEqual(m.slices[4].title, 'three (cont.)'); + assert.strictEqual(m.slices[4].start, 3); + assert.strictEqual(m.slices[4].duration, 1); + }); + + test('endSplitMerge', function() { + const thread = newFakeThread(); + const a = new SliceGroup(thread); + const b = new SliceGroup(thread); + a.beginSlice('', 'one', 1); + a.endSlice(3); + b.beginSlice('', 'two', 2); + b.endSlice(4); + + const m = SliceGroup.merge(a, b); + assert.strictEqual(m.slices.length, 3); + + assert.strictEqual(m.slices[0].title, 'one'); + assert.strictEqual(m.slices[0].start, 1); + assert.strictEqual(m.slices[0].duration, 2); + + assert.strictEqual(m.slices[1].title, 'two'); + assert.strictEqual(m.slices[1].start, 2); + assert.strictEqual(m.slices[1].duration, 1); + + assert.strictEqual(m.slices[2].title, 'two (cont.)'); + assert.strictEqual(m.slices[2].start, 3); + assert.strictEqual(m.slices[2].duration, 1); + }); + + test('endSplitTwoMerge', function() { + const thread = newFakeThread(); + const a = new SliceGroup(thread); + const b = new SliceGroup(thread); + a.beginSlice('', 'one', 1); + a.endSlice(4); + b.beginSlice('', 'two', 2); + b.beginSlice('', 'three', 3); + b.endSlice(5); + b.endSlice(6); + + const m = SliceGroup.merge(a, b); + assert.strictEqual(m.slices.length, 5); + + assert.strictEqual(m.slices[0].title, 'one'); + assert.strictEqual(m.slices[0].start, 1); + assert.strictEqual(m.slices[0].duration, 3); + + assert.strictEqual(m.slices[1].title, 'two'); + assert.strictEqual(m.slices[1].start, 2); + assert.strictEqual(m.slices[1].duration, 2); + + assert.strictEqual(m.slices[2].title, 'three'); + assert.strictEqual(m.slices[2].start, 3); + assert.strictEqual(m.slices[2].duration, 1); + + assert.strictEqual(m.slices[3].title, 'two (cont.)'); + assert.strictEqual(m.slices[3].start, 4); + assert.strictEqual(m.slices[3].duration, 2); + + assert.strictEqual(m.slices[4].title, 'three (cont.)'); + assert.strictEqual(m.slices[4].start, 4); + assert.strictEqual(m.slices[4].duration, 1); + }); + + test('endSplitTwiceMerge', function() { + const thread = newFakeThread(); + const a = new SliceGroup(thread); + const b = new SliceGroup(thread); + a.beginSlice('', 'one', 1); + a.beginSlice('', 'two', 2); + a.endSlice(4); + a.endSlice(5); + b.beginSlice('', 'three', 3); + b.endSlice(6); + + const m = SliceGroup.merge(a, b); + assert.strictEqual(m.slices.length, 5); + + assert.strictEqual(m.slices[0].title, 'one'); + assert.strictEqual(m.slices[0].start, 1); + assert.strictEqual(m.slices[0].duration, 4); + + assert.strictEqual(m.slices[1].title, 'two'); + assert.strictEqual(m.slices[1].start, 2); + assert.strictEqual(m.slices[1].duration, 2); + + assert.strictEqual(m.slices[2].title, 'three'); + assert.strictEqual(m.slices[2].start, 3); + assert.strictEqual(m.slices[2].duration, 1); + + assert.strictEqual(m.slices[3].title, 'three (cont.)'); + assert.strictEqual(m.slices[3].start, 4); + assert.strictEqual(m.slices[3].duration, 1); + + assert.strictEqual(m.slices[4].title, 'three (cont.)'); + assert.strictEqual(m.slices[4].start, 5); + assert.strictEqual(m.slices[4].duration, 1); + }); + + // Input: + // A: | one | | two | + // + // B: | three | + // + // Output: + // | one | three | two | + // | three | | three | + test('splitTwiceMerge', function() { + const thread = newFakeThread(); + const a = new SliceGroup(thread); + const b = new SliceGroup(thread); + a.beginSlice('', 'one', 1); + a.endSlice(3); + a.beginSlice('', 'two', 4); + a.endSlice(6); + b.beginSlice('', 'three', 2); + b.endSlice(5); + + const m = SliceGroup.merge(a, b); + assert.strictEqual(m.slices.length, 5); + + assert.strictEqual(m.slices[0].title, 'one'); + assert.strictEqual(m.slices[0].start, 1); + assert.strictEqual(m.slices[0].duration, 2); + + assert.strictEqual(m.slices[1].title, 'three'); + assert.strictEqual(m.slices[1].start, 2); + assert.strictEqual(m.slices[1].duration, 1); + + assert.strictEqual(m.slices[2].title, 'three (cont.)'); + assert.strictEqual(m.slices[2].start, 3); + assert.strictEqual(m.slices[2].duration, 1); + + assert.strictEqual(m.slices[3].title, 'two'); + assert.strictEqual(m.slices[3].start, 4); + assert.strictEqual(m.slices[3].duration, 2); + + assert.strictEqual(m.slices[4].title, 'three (cont.)'); + assert.strictEqual(m.slices[4].start, 4); + assert.strictEqual(m.slices[4].duration, 1); + }); + + test('bounds', function() { + const group = new SliceGroup(newFakeThread()); + group.updateBounds(); + assert.isUndefined(group.bounds.min); + assert.isUndefined(group.bounds.max); + + group.pushSlice(newSliceEx({start: 1, duration: 3})); + group.pushSlice(newSliceEx({start: 7, duration: 2})); + group.updateBounds(); + assert.strictEqual(group.bounds.min, 1); + assert.strictEqual(group.bounds.max, 9); + }); + + test('boundsWithPartial', function() { + const group = new SliceGroup(newFakeThread()); + group.beginSlice('', 'a', 7); + group.updateBounds(); + assert.strictEqual(group.bounds.min, 7); + assert.strictEqual(group.bounds.max, 7); + }); + + test('boundsWithTwoPartials', function() { + const group = new SliceGroup(newFakeThread()); + group.beginSlice('', 'a', 0); + group.beginSlice('', 'a', 1); + group.updateBounds(); + assert.strictEqual(group.bounds.min, 0); + assert.strictEqual(group.bounds.max, 1); + }); + + test('boundsWithBothPartialAndRegular', function() { + const group = new SliceGroup(newFakeThread()); + group.updateBounds(); + assert.isUndefined(group.bounds.min); + assert.isUndefined(group.bounds.max); + + group.pushSlice(newSliceEx({start: 1, duration: 3})); + group.beginSlice('', 'a', 7); + group.updateBounds(); + assert.strictEqual(group.bounds.min, 1); + assert.strictEqual(group.bounds.max, 7); + }); + + test('autocloserBasic', function() { + const group = new SliceGroup(newFakeThread()); + assert.strictEqual(0, group.openSliceCount); + + group.pushSlice(newSliceEx({title: 'a', start: 1, duration: 0.5})); + + group.beginSlice('', 'b', 2); + group.beginSlice('', 'c', 2.5); + group.endSlice(3); + + group.autoCloseOpenSlices(); + group.updateBounds(); + + assert.strictEqual(group.bounds.min, 1); + assert.strictEqual(group.bounds.max, 3); + assert.strictEqual(group.slices.length, 3); + + assert.strictEqual(group.slices[0].title, 'a'); + assert.isFalse(group.slices[0].didNotFinish); + + assert.strictEqual(group.slices[1].title, 'b'); + assert.isTrue(group.slices[1].didNotFinish); + assert.strictEqual(group.slices[1].duration, 1); + + assert.strictEqual(group.slices[2].title, 'c'); + assert.isFalse(group.slices[2].didNotFinish); + }); + + test('autocloserWithSubTasks', function() { + const group = new SliceGroup(newFakeThread()); + assert.strictEqual(0, group.openSliceCount); + + group.beginSlice('', 'a', 1); + group.beginSlice('', 'b1', 2); + group.endSlice(3); + group.beginSlice('', 'b2', 3); + + group.autoCloseOpenSlices(); + assert.strictEqual(group.slices.length, 3); + + assert.strictEqual(group.slices[0].title, 'a'); + assert.isTrue(group.slices[0].didNotFinish); + assert.strictEqual(group.slices[0].duration, 2); + + assert.strictEqual(group.slices[1].title, 'b1'); + assert.isFalse(group.slices[1].didNotFinish); + assert.strictEqual(group.slices[1].duration, 1); + + assert.strictEqual(group.slices[2].title, 'b2'); + assert.isTrue(group.slices[2].didNotFinish); + assert.strictEqual(group.slices[2].duration, 0); + }); + + test('autocloseCompleteSlice', function() { + const group = new SliceGroup(newFakeThread()); + + group.pushCompleteSlice('', 'a', 1, undefined); + group.pushCompleteSlice('', 'b', 2, 3); + + group.autoCloseOpenSlices(); + assert.strictEqual(group.slices.length, 2); + + assert.strictEqual(group.slices[0].title, 'a'); + assert.isTrue(group.slices[0].didNotFinish); + assert.strictEqual(group.slices[0].duration, 4); + + assert.strictEqual(group.slices[1].title, 'b'); + assert.isFalse(group.slices[1].didNotFinish); + assert.strictEqual(group.slices[1].duration, 3); + }); + + test('sliceGroupStableId', function() { + const model = new tr.Model(); + const process = model.getOrCreateProcess(123); + const thread = process.getOrCreateThread(456); + const group = new SliceGroup(thread); + + assert.strictEqual(process.stableId, 123); + assert.strictEqual(thread.stableId, '123.456'); + assert.strictEqual(group.stableId, '123.456.SliceGroup'); + }); + + test('getSlicesOfName', function() { + const group = new SliceGroup(newFakeThread()); + const expected = []; + + for (let i = 0; i < 10; i++) { + const aSlice = newSliceEx({title: 'a', start: i, duration: i + 1}); + group.pushSlice(aSlice); + group.pushSlice(newSliceEx({title: 'b', start: i + 1, duration: i + 2})); + expected.push(aSlice); + } + + assert.deepEqual(group.getSlicesOfName('a'), expected); + }); + + test('iterSlicesInTimeRange', function() { + const group = new SliceGroup(newFakeThread()); + const expected = []; + + for (let i = 0; i < 10; i++) { + const slice = newSliceEx({title: 'a', start: i, duration: 1}); + group.pushSlice(slice); + if (4 <= i && i <= 7) { + expected.push(slice); + } + } + group.createSubSlices(); + + const observed = []; + group.iterSlicesInTimeRange(function(slice) { observed.push(slice); }, + 4.5, 7.5); + assert.deepEqual(observed, expected); + }); + + test('computeCpuDurationNoOverlap', function() { + const model = new tr.Model(); + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + const process = model.getOrCreateProcess(123); + const t = process.getOrCreateThread(456); + t.timeSlices = [newThreadSlice(t, SCHEDULING_STATE.RUNNING, 20, 20), + newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 40, 10), + newThreadSlice(t, SCHEDULING_STATE.RUNNING, 50, 10)]; + const group = new SliceGroup(t); + group.pushSlice(newSliceEx({title: 'draw', start: 0, duration: 20})); + group.pushSlice(newSliceEx({title: 'render', start: 60, duration: 20})); + group.createSubSlices(); + assert.strictEqual(group.slices[0].cpuDuration, 0); + assert.strictEqual(group.slices[1].cpuDuration, 0); + }); + + test('computeCpuDurationPartialOverlap', function() { + const model = new tr.Model(); + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + const process = model.getOrCreateProcess(123); + const t = process.getOrCreateThread(456); + t.timeSlices = [newThreadSlice(t, SCHEDULING_STATE.RUNNING, 20, 20), + newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 40, 10), + newThreadSlice(t, SCHEDULING_STATE.RUNNING, 50, 10)]; + const group = new SliceGroup(t); + group.pushSlice(newSliceEx({title: 'draw', start: 10, duration: 30})); + group.pushSlice(newSliceEx({title: 'render', start: 50, duration: 20})); + group.createSubSlices(); + assert.strictEqual(group.slices[0].cpuDuration, 20); + assert.strictEqual(group.slices[1].cpuDuration, 10); + }); + + test('computeCpuDurationFullOverlap', function() { + const model = new tr.Model(); + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + const process = model.getOrCreateProcess(123); + const t = process.getOrCreateThread(456); + t.timeSlices = [newThreadSlice(t, SCHEDULING_STATE.RUNNING, 20, 20), + newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 40, 10), + newThreadSlice(t, SCHEDULING_STATE.RUNNING, 50, 20)]; + const group = new SliceGroup(t); + group.pushSlice(newSliceEx({title: 'draw', start: 20, duration: 30})); + group.pushSlice(newSliceEx({title: 'render', start: 50, duration: 20})); + group.createSubSlices(); + assert.strictEqual(group.slices[0].cpuDuration, 20); + assert.strictEqual(group.slices[1].cpuDuration, 20); + }); + + test('computeCpuSelfDurationWithSubslices', function() { + const model = new tr.Model(); + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + const process = model.getOrCreateProcess(123); + const t = process.getOrCreateThread(456); + t.timeSlices = [newThreadSlice(t, SCHEDULING_STATE.RUNNING, 20, 20), + newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 40, 10), + newThreadSlice(t, SCHEDULING_STATE.RUNNING, 50, 20)]; + const group = new SliceGroup(t); + group.pushSlice(newSliceEx({title: 'draw', start: 20, duration: 30})); + group.pushSlice(newSliceEx({title: 'render', start: 21, duration: 8})); + group.pushSlice(newSliceEx({title: 'flush', start: 29, duration: 11})); + group.createSubSlices(); + assert.strictEqual(group.slices[0].cpuDuration, 20); + assert.strictEqual(group.slices[0].cpuSelfTime, 1); + assert.strictEqual(group.slices[1].cpuDuration, 8); + assert.strictEqual(group.slices[1].cpuSelfTime, 8); + assert.strictEqual(group.slices[2].cpuDuration, 11); + assert.strictEqual(group.slices[2].cpuSelfTime, 11); + }); + + test('computeCpuDurationSmallTimeslices', function() { + const model = new tr.Model(); + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + const process = model.getOrCreateProcess(123); + const t = process.getOrCreateThread(456); + t.timeSlices = [newThreadSlice(t, SCHEDULING_STATE.RUNNING, 20, 1), + newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 21, 1), + newThreadSlice(t, SCHEDULING_STATE.RUNNING, 22, 1), + newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 23, 1), + newThreadSlice(t, SCHEDULING_STATE.RUNNING, 24, 1), + newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 25, 1), + newThreadSlice(t, SCHEDULING_STATE.RUNNING, 26, 1), + newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 27, 1), + newThreadSlice(t, SCHEDULING_STATE.RUNNING, 28, 1), + newThreadSlice(t, SCHEDULING_STATE.SLEEPING, 29, 1), + newThreadSlice(t, SCHEDULING_STATE.RUNNING, 30, 1)]; + const group = new SliceGroup(t); + group.pushSlice(newSliceEx( + {title: 'draw', start: 20, duration: 11})); // 20,[22,24,26,28],30 + group.pushSlice(newSliceEx( + {title: 'render', start: 22, duration: 8})); // 22,[24, 26, 28] + group.pushSlice(newSliceEx( + {title: 'flush', start: 24, duration: 6})); // 24, 26, 28 + group.createSubSlices(); + assert.strictEqual(group.slices[0].cpuDuration, 6); + assert.strictEqual(group.slices[0].cpuSelfTime, 2); + assert.strictEqual(group.slices[1].cpuDuration, 4); + assert.strictEqual(group.slices[1].cpuSelfTime, 1); + assert.strictEqual(group.slices[2].cpuDuration, 3); + assert.strictEqual(group.slices[2].cpuSelfTime, 3); + }); + + test('sliceParentContainerSetAtPush', function() { + const m = newModel(function(m) { + m.process = m.getOrCreateProcess(123); + m.thread = m.process.getOrCreateThread(456); + m.group = new SliceGroup(m.thread); + + m.sA = m.group.pushSlice(newSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + + m.group.createSubSlices(); + }); + + assert.deepEqual(m.sA.parentContainer, m.thread); + }); + + test('getDescendantEventsInSortedRanges', function() { + // Create the following slices: + // 0 1 2 3 4 5 6 7 8 9 10 + // <------------- 0 -------------> + // <- 2 -> <---- 3 ----> + // <- 1 -> + const group = new SliceGroup(newFakeThread()); + group.pushSlice(newSliceEx({title: 's0', start: 0, end: 10})); + group.pushSlice(newSliceEx({title: 's1', start: 7, end: 9})); + group.pushSlice(newSliceEx({title: 's2', start: 3, end: 5})); + group.pushSlice(newSliceEx({title: 's3', start: 6, end: 10})); + group.createSubSlices(); + + // [0, 5] intersects s0 and s2. + const r1 = new tr.b.math.Range.fromExplicitRange(0, 5); + let slices = [...group.getDescendantEventsInSortedRanges([r1])]; + assert.strictEqual(slices.length, 2); + assert.strictEqual(slices[0].title, 's0'); + assert.strictEqual(slices[1].title, 's2'); + + // [10, 11] intersects s0 and s3. + const r2 = new tr.b.math.Range.fromExplicitRange(10, 11); + slices = [...group.getDescendantEventsInSortedRanges([r2])]; + assert.strictEqual(slices.length, 2); + assert.strictEqual(slices[0].title, 's0'); + assert.strictEqual(slices[1].title, 's3'); + + // [0, 5], [10, 11] intersects s0, s2, and s3. + slices = [...group.getDescendantEventsInSortedRanges([r1, r2])]; + assert.strictEqual(slices.length, 3); + assert.strictEqual(slices[0].title, 's0'); + assert.strictEqual(slices[1].title, 's2'); + assert.strictEqual(slices[2].title, 's3'); + + // Ranges can be nested, too. + const r3 = new tr.b.math.Range.fromExplicitRange(1, 2); + slices = [...group.getDescendantEventsInSortedRanges([r1, r3])]; + assert.strictEqual(slices.length, 2); + assert.strictEqual(slices[0].title, 's0'); + assert.strictEqual(slices[1].title, 's2'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/slice_test.html b/chromium/third_party/catapult/tracing/tracing/model/slice_test.html new file mode 100644 index 00000000000..a9d5b18ef7e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/slice_test.html @@ -0,0 +1,239 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/slice_group.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Slice = tr.model.Slice; + const SliceGroup = tr.model.SliceGroup; + const newSliceEx = tr.c.TestUtils.newSliceEx; + const newFakeThread = tr.c.TestUtils.newFakeThread; + + test('findDescendentSlice', function() { + const group = new SliceGroup(newFakeThread()); + + const sA = group.pushSlice(newSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + const sB = group.pushSlice(newSliceEx( + { title: 'sB', start: 0.0, duration: 4.0 })); + const sC = group.pushSlice(newSliceEx( + { title: 'sC', start: 0.0, duration: 2.0 })); + + group.createSubSlices(); + + assert.deepEqual(sB, sA.findDescendentSlice('sB')); + assert.deepEqual(sC, sA.findDescendentSlice('sC')); + assert.isUndefined(sA.findDescendentSlice('sD')); + }); + + test('findTopmostSlicesRelativeToThisSliceBaseCase', function() { + const PREDICATE = slice => slice.title.startsWith('sC'); + + const group = new SliceGroup(newFakeThread()); + + const sC1 = group.pushSlice(newSliceEx( + { title: 'sC1', start: 0.0, duration: 10.0 })); + const sC2 = group.pushSlice(newSliceEx( + { title: 'sC2', start: 0.0, duration: 4.0 })); + + group.createSubSlices(); + + const foundSlices = []; + for (const slice of sC1.findTopmostSlicesRelativeToThisSlice(PREDICATE)) { + foundSlices.push(slice); + } + + assert.deepEqual([sC1], foundSlices); + }); + + test('findTopmostSlicesRelativeToThisSliceRecursive', function() { + const PREDICATE = slice => slice.title.startsWith('sC'); + + const group = new SliceGroup(newFakeThread()); + + const sD = group.pushSlice(newSliceEx( + { title: 'sD', start: 0.0, duration: 10.0 })); + const sC1 = group.pushSlice(newSliceEx( + { title: 'sC1', start: 0.0, duration: 4.0 })); + const sC2 = group.pushSlice(newSliceEx( + { title: 'sC2', start: 6.0, duration: 3.0 })); + + group.createSubSlices(); + + const foundSlices = []; + for (const slice of sD.findTopmostSlicesRelativeToThisSlice(PREDICATE)) { + foundSlices.push(slice); + } + assert.deepEqual([sC1, sC2], foundSlices); + }); + + test('enumerateAllDescendents', function() { + const group = new SliceGroup(newFakeThread()); + + const sA = group.pushSlice(newSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + const sB = group.pushSlice(newSliceEx( + { title: 'sB', start: 0.0, duration: 4.0 })); + const sC = group.pushSlice(newSliceEx( + { title: 'sC', start: 0.0, duration: 2.0 })); + + group.createSubSlices(); + + assert.deepEqual(sA.descendentSlices, [sB, sC]); + assert.deepEqual(sC.descendentSlices, []); + }); + + test('mostTopLevelSlice', function() { + const group = new SliceGroup(newFakeThread()); + + const sA = group.pushSlice(newSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + const sB = group.pushSlice(newSliceEx( + { title: 'sB', start: 0.0, duration: 4.0 })); + const sC = group.pushSlice(newSliceEx( + { title: 'sC', start: 0.0, duration: 2.0 })); + + group.createSubSlices(); + + assert.strictEqual(sA.mostTopLevelSlice, sA); + assert.strictEqual(sB.mostTopLevelSlice, sA); + assert.strictEqual(sC.mostTopLevelSlice, sA); + }); + + test('enumerateAllAncestors', function() { + const group = new SliceGroup(newFakeThread()); + + const sA = group.pushSlice(newSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + const sB = group.pushSlice(newSliceEx( + { title: 'sB', start: 0.0, duration: 4.0 })); + const sC = group.pushSlice(newSliceEx( + { title: 'sC', start: 0.0, duration: 2.0 })); + + group.createSubSlices(); + + // Note that we iterate ancestors from the leaves to the root + assert.deepEqual(sC.ancestorSlices, [sB, sA]); + assert.deepEqual(sA.ancestorSlices, []); + }); + + test('iterateAllSubsequentSlices', function() { + const group = new SliceGroup(newFakeThread()); + + // [ A ] + // [ B ][ D ][F] + // [C] [E] + + const sA = group.pushSlice(newSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + const sB = group.pushSlice(newSliceEx( + { title: 'sB', start: 0.0, duration: 4.0 })); + const sC = group.pushSlice(newSliceEx( + { title: 'sC', start: 0.0, duration: 2.0 })); + const sD = group.pushSlice(newSliceEx( + { title: 'sD', start: 5.0, duration: 2.0 })); + const sE = group.pushSlice(newSliceEx( + { title: 'sE', start: 5.0, duration: 1.0 })); + const sF = group.pushSlice(newSliceEx( + { title: 'sF', start: 8.0, duration: 2.0 })); + + group.createSubSlices(); + + assert.deepEqual(sA.subsequentSlices, [sB, sC, sD, sE, sF]); + assert.deepEqual(sD.subsequentSlices, [sE, sF]); + assert.deepEqual(sF.subsequentSlices, []); + }); + + test('ancestorAndSubsequentSlices', function() { + const group = new SliceGroup(newFakeThread()); + + // [ A ] + // [ B ][ D ][F] + // [C] [E] + + const sA = group.pushSlice(newSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + const sB = group.pushSlice(newSliceEx( + { title: 'sB', start: 0.0, duration: 4.0 })); + const sC = group.pushSlice(newSliceEx( + { title: 'sC', start: 0.0, duration: 2.0 })); + const sD = group.pushSlice(newSliceEx( + { title: 'sD', start: 5.0, duration: 2.0 })); + const sE = group.pushSlice(newSliceEx( + { title: 'sE', start: 5.0, duration: 1.0 })); + const sF = group.pushSlice(newSliceEx( + { title: 'sF', start: 8.0, duration: 2.0 })); + + group.createSubSlices(); + + assert.deepEqual(sD.ancestorAndSubsequentSlices, [sD, sA, sE, sF]); + }); + + test('entireHierarchy', function() { + const group = new SliceGroup(newFakeThread()); + + // [ A ] + // [ B ][ D ][F] + // [C] [E] + + const sA = group.pushSlice(newSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + const sB = group.pushSlice(newSliceEx( + { title: 'sB', start: 0.0, duration: 4.0 })); + const sC = group.pushSlice(newSliceEx( + { title: 'sC', start: 0.0, duration: 2.0 })); + const sD = group.pushSlice(newSliceEx( + { title: 'sD', start: 5.0, duration: 2.0 })); + const sE = group.pushSlice(newSliceEx( + { title: 'sE', start: 5.0, duration: 1.0 })); + const sF = group.pushSlice(newSliceEx( + { title: 'sF', start: 8.0, duration: 2.0 })); + + group.createSubSlices(); + + assert.deepEqual(sD.entireHierarchy, [sA, sB, sC, sD, sE, sF]); + }); + + test('stableId', function() { + const thread = newFakeThread(); + const group = thread.sliceGroup; + + const sA = group.pushSlice(newSliceEx( + { title: 'sA', start: 0.0, duration: 10.0 })); + const sB = group.pushSlice(newSliceEx( + { title: 'sB', start: 10.0, duration: 20.0 })); + const sC = group.pushSlice(newSliceEx( + { title: 'sC', start: 20.0, duration: 30.0 })); + + assert.strictEqual(group.stableId + '.0', sA.stableId); + assert.strictEqual(group.stableId + '.1', sB.stableId); + assert.strictEqual(group.stableId + '.2', sC.stableId); + }); + + test('cantInstantiateDirectly', function() { + assert.throws(function() { + new Slice('', 'Test', 0, 0, { }); + }); + }); + + test('canInstantiateSubclasses', function() { + function TestSlice() { + Slice.call(this, '', 'Test', 0, 0, { }); + } + TestSlice.prototype = { + __proto__: Slice.prototype + }; + assert.doesNotThrow(function() { + new TestSlice(); + }); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/source_info/js_source_info.html b/chromium/third_party/catapult/tracing/tracing/model/source_info/js_source_info.html new file mode 100644 index 00000000000..37b975b0b6e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/source_info/js_source_info.html @@ -0,0 +1,57 @@ +<!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/model/source_info/source_info.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.source_info', function() { + function JSSourceInfo(file, line, column, isNative, scriptId, state) { + tr.model.source_info.SourceInfo.call(this, file, line, column); + + this.isNative_ = isNative; + this.scriptId_ = scriptId; + this.state_ = state; + } + + JSSourceInfo.prototype = { + __proto__: tr.model.source_info.SourceInfo.prototype, + + get state() { + return this.state_; + }, + + get isNative() { + return this.isNative_; + }, + + get scriptId() { + return this.scriptId_; + }, + + toString() { + const str = this.isNative_ ? '[native v8] ' : ''; + return str + + tr.model.source_info.SourceInfo.prototype.toString.call(this); + } + }; + + const JSSourceState = { + COMPILED: 'compiled', + OPTIMIZABLE: 'optimizable', + OPTIMIZED: 'optimized', + UNKNOWN: 'unknown', + }; + + return { + JSSourceInfo, + JSSourceState, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info.html b/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info.html new file mode 100644 index 00000000000..a8cf526d6c5 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info.html @@ -0,0 +1,60 @@ +<!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"> + +<script> +'use strict'; + +tr.exportTo('tr.model.source_info', function() { + function SourceInfo(file, opt_line, opt_column) { + this.file_ = file; + this.line_ = opt_line || -1; + this.column_ = opt_column || -1; + } + + SourceInfo.prototype = { + get file() { + return this.file_; + }, + + get line() { + return this.line_; + }, + + get column() { + return this.column_; + }, + + get domain() { + if (!this.file_) return undefined; + const domain = this.file_.match(/(.*:\/\/[^:\/]*)/i); + return domain ? domain[1] : undefined; + }, + + toString() { + let str = ''; + + if (this.file_) { + str += this.file_; + } + if (this.line_ > 0) { + str += ':' + this.line_; + } + if (this.column_ > 0) { + str += ':' + this.column_; + } + return str; + } + }; + + return { + SourceInfo, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info_test.html b/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info_test.html new file mode 100644 index 00000000000..269de596d10 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/source_info/source_info_test.html @@ -0,0 +1,27 @@ +<!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/source_info/source_info.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('domain', function() { + const urlDomains = { + 'http://www.google.com': 'http://www.google.com', + 'http://www.google.com/bla': 'http://www.google.com', + 'http://www.google.com:1234': 'http://www.google.com', + 'bad url': undefined + }; + for (const url in urlDomains) { + const sourceInfo = new tr.model.source_info.SourceInfo(url); + assert.strictEqual(urlDomains[url], sourceInfo.domain); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/stack_frame.html b/chromium/third_party/catapult/tracing/tracing/model/stack_frame.html new file mode 100644 index 00000000000..b417d20f94c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/stack_frame.html @@ -0,0 +1,111 @@ +<!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"> +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + function StackFrame(parentFrame, id, title, colorId, opt_sourceInfo) { + if (id === undefined) { + throw new Error('id must be given'); + } + this.parentFrame_ = parentFrame; + this.id = id; + this.title_ = title; + this.colorId = colorId; + this.children = []; + this.sourceInfo_ = opt_sourceInfo; + + if (this.parentFrame_) { + this.parentFrame_.addChild(this); + } + } + + StackFrame.prototype = { + get parentFrame() { + return this.parentFrame_; + }, + + get title() { + if (this.sourceInfo_) { + const src = this.sourceInfo_.toString(); + return this.title_ + (src === '' ? '' : ' ' + src); + } + return this.title_; + }, + + /** + * Attempts to find the domain of the origin of the script either from this + * stack trace or from its ancestors. + */ + get domain() { + let result = 'unknown'; + if (this.sourceInfo_ && this.sourceInfo_.domain) { + result = this.sourceInfo_.domain; + } + if (result === 'unknown' && this.parentFrame) { + result = this.parentFrame.domain; + } + return result; + }, + + get sourceInfo() { + return this.sourceInfo_; + }, + + set parentFrame(parentFrame) { + if (this.parentFrame_) { + Polymer.dom(this.parentFrame_).removeChild(this); + } + this.parentFrame_ = parentFrame; + if (this.parentFrame_) { + this.parentFrame_.addChild(this); + } + }, + + addChild(child) { + this.children.push(child); + }, + + removeChild(child) { + const i = this.children.indexOf(child.id); + if (i === -1) { + throw new Error('omg'); + } + this.children.splice(i, 1); + }, + + removeAllChildren() { + for (let i = 0; i < this.children.length; i++) { + this.children[i].parentFrame_ = undefined; + } + this.children.splice(0, this.children.length); + }, + + /** + * Returns stackFrames where the most specific frame is first. + */ + get stackTrace() { + const stack = [this]; + let cur = this.parentFrame; + while (cur) { + stack.push(cur); + cur = cur.parentFrame; + } + return stack; + }, + + getUserFriendlyStackTrace() { + return this.stackTrace.map(function(x) { return x.title; }); + } + }; + + return { + StackFrame, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/stack_frame_test.html b/chromium/third_party/catapult/tracing/tracing/model/stack_frame_test.html new file mode 100644 index 00000000000..d0b971ecb78 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/stack_frame_test.html @@ -0,0 +1,29 @@ +<!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/source_info/source_info.html"> +<link rel="import" href="/tracing/model/stack_frame.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('domain', function() { + const stackFrame1 = new tr.model.StackFrame(undefined, 1, '1', 1); + assert.strictEqual('unknown', stackFrame1.domain); + + const sourceInfo = new tr.model.source_info.SourceInfo( + 'http://www.google.com:1234'); + const stackFrame2 = new tr.model.StackFrame( + stackFrame1, 2, '2', 2, sourceInfo); + assert.strictEqual('http://www.google.com', stackFrame2.domain); + + const stackFrame3 = new tr.model.StackFrame(stackFrame2, 3, '3', 3); + assert.strictEqual('http://www.google.com', stackFrame3.domain); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/thread.html b/chromium/third_party/catapult/tracing/tracing/model/thread.html new file mode 100644 index 00000000000..54826bd29db --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/thread.html @@ -0,0 +1,358 @@ +<!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/guid.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/model/async_slice_group.html"> +<link rel="import" href="/tracing/model/event_container.html"> +<link rel="import" href="/tracing/model/slice_group.html"> +<link rel="import" href="/tracing/model/thread_slice.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Thread class. + */ +tr.exportTo('tr.model', function() { + const AsyncSlice = tr.model.AsyncSlice; + const AsyncSliceGroup = tr.model.AsyncSliceGroup; + const SliceGroup = tr.model.SliceGroup; + const ThreadSlice = tr.model.ThreadSlice; + const ThreadTimeSlice = tr.model.ThreadTimeSlice; + + /** + * A Thread stores all the trace events collected for a particular + * thread. We organize the synchronous slices on a thread by "subrows," where + * subrow 0 has all the root slices, subrow 1 those nested 1 deep, and so on. + * The asynchronous slices are stored in an AsyncSliceGroup object. + * + * The slices stored on a Thread should be instances of + * ThreadSlice. + * + * @constructor + * @extends {tr.model.EventContainer} + */ + function Thread(parent, tid) { + if (!parent) { + throw new Error('Parent must be provided.'); + } + + tr.model.EventContainer.call(this); + this.parent = parent; + this.sortIndex = 0; + this.tid = tid; + this.name = undefined; + this.samples_ = undefined; // Set during createSubSlices + + this.sliceGroup = new SliceGroup(this, ThreadSlice, 'slices'); + this.timeSlices = undefined; + this.kernelSliceGroup = new SliceGroup( + this, ThreadSlice, 'kernel-slices'); + this.asyncSliceGroup = new AsyncSliceGroup(this, 'async-slices'); + } + + Thread.prototype = { + __proto__: tr.model.EventContainer.prototype, + + get model() { + return this.parent.model; + }, + + get stableId() { + return this.parent.stableId + '.' + this.tid; + }, + + compareTo(that) { + return Thread.compare(this, that); + }, + + * childEventContainers() { + if (this.sliceGroup.length) { + yield this.sliceGroup; + } + if (this.kernelSliceGroup.length) { + yield this.kernelSliceGroup; + } + if (this.asyncSliceGroup.length) { + yield this.asyncSliceGroup; + } + }, + + * childEvents() { + if (this.timeSlices) { + yield* this.timeSlices; + } + }, + + iterateAllPersistableObjects(cb) { + cb(this); + if (this.sliceGroup.length) { + cb(this.sliceGroup); + } + this.asyncSliceGroup.viewSubGroups.forEach(cb); + }, + + /** + * Shifts all the timestamps inside this thread forward by the amount + * specified. + */ + shiftTimestampsForward(amount) { + this.sliceGroup.shiftTimestampsForward(amount); + + if (this.timeSlices) { + for (let i = 0; i < this.timeSlices.length; i++) { + const slice = this.timeSlices[i]; + slice.start += amount; + } + } + + this.kernelSliceGroup.shiftTimestampsForward(amount); + this.asyncSliceGroup.shiftTimestampsForward(amount); + }, + + /** + * Determines whether this thread is empty. If true, it usually implies + * that it should be pruned from the model. + */ + get isEmpty() { + if (this.sliceGroup.length) return false; + if (this.sliceGroup.openSliceCount) return false; + if (this.timeSlices && this.timeSlices.length) return false; + if (this.kernelSliceGroup.length) return false; + if (this.asyncSliceGroup.length) return false; + if (this.samples_.length) return false; + return true; + }, + + /** + * Updates the bounds based on the + * current objects associated with the thread. + */ + updateBounds() { + this.bounds.reset(); + + this.sliceGroup.updateBounds(); + this.bounds.addRange(this.sliceGroup.bounds); + + this.kernelSliceGroup.updateBounds(); + this.bounds.addRange(this.kernelSliceGroup.bounds); + + this.asyncSliceGroup.updateBounds(); + this.bounds.addRange(this.asyncSliceGroup.bounds); + + if (this.timeSlices && this.timeSlices.length) { + this.bounds.addValue(this.timeSlices[0].start); + this.bounds.addValue( + this.timeSlices[this.timeSlices.length - 1].end); + } + + if (this.samples_ && this.samples_.length) { + this.bounds.addValue(this.samples_[0].start); + this.bounds.addValue( + this.samples_[this.samples_.length - 1].end); + } + }, + + addCategoriesToDict(categoriesDict) { + for (let i = 0; i < this.sliceGroup.length; i++) { + categoriesDict[this.sliceGroup.slices[i].category] = true; + } + for (let i = 0; i < this.kernelSliceGroup.length; i++) { + categoriesDict[this.kernelSliceGroup.slices[i].category] = true; + } + for (let i = 0; i < this.asyncSliceGroup.length; i++) { + categoriesDict[this.asyncSliceGroup.slices[i].category] = true; + } + if (this.samples_) { + for (let i = 0; i < this.samples_.length; i++) { + categoriesDict[this.samples_[i].category] = true; + } + } + }, + + autoCloseOpenSlices() { + this.sliceGroup.autoCloseOpenSlices(); + this.asyncSliceGroup.autoCloseOpenSlices(); + this.kernelSliceGroup.autoCloseOpenSlices(); + }, + + mergeKernelWithUserland() { + if (this.kernelSliceGroup.length > 0) { + const newSlices = SliceGroup.merge( + this.sliceGroup, this.kernelSliceGroup); + this.sliceGroup.slices = newSlices.slices; + this.kernelSliceGroup = new SliceGroup(this); + this.updateBounds(); + } + }, + + createSubSlices() { + this.sliceGroup.createSubSlices(); + this.samples_ = this.parent.model.samples.filter(sample => + sample.thread === this); + }, + + /** + * @return {String} A user-friendly name for this thread. + */ + get userFriendlyName() { + return this.name || this.tid; + }, + + /** + * @return {String} User friendly details about this thread. + */ + get userFriendlyDetails() { + return 'tid: ' + this.tid + + (this.name ? ', name: ' + this.name : ''); + }, + + getSettingsKey() { + if (!this.name) return undefined; + const parentKey = this.parent.getSettingsKey(); + if (!parentKey) return undefined; + return parentKey + '.' + this.name; + }, + + getProcess() { + return this.parent; + }, + + /* + * Returns the index of the slice in the timeSlices array, or undefined. + */ + indexOfTimeSlice(timeSlice) { + const i = tr.b.findLowIndexInSortedArray( + this.timeSlices, + function(slice) { return slice.start; }, + timeSlice.start); + if (this.timeSlices[i] !== timeSlice) return undefined; + return i; + }, + + sumOverToplevelSlicesInRange(range, func) { + let sum = 0; + tr.b.iterateOverIntersectingIntervals( + this.sliceGroup.topLevelSlices, + slice => slice.start, slice => slice.end, + range.min, range.max, + slice => { + let fractionOfSliceInsideRangeOfInterest = 1; + if (slice.duration > 0) { + const intersection = range.findIntersection(slice.range); + fractionOfSliceInsideRangeOfInterest = + intersection.duration / slice.duration; + } + // We assume that if a slice doesn't lie entirely inside the range + // of interest, then |func| is evenly distributed inside of the + // slice. + sum += func(slice) * fractionOfSliceInsideRangeOfInterest; + }); + return sum; + }, + + /** + * Returns the total cpu time consumed within |range| by this thread. + */ + getCpuTimeForRange(range) { + return this.sumOverToplevelSlicesInRange( + range, slice => slice.cpuDuration || 0); + }, + + /** + * Returns the total number of top-level slices within |range| in this + * thread. If a slice overlaps with |range| and is not completely inside it, + * then we attribute the portion that is inside the range only. For example, + * |getNumToplevelSlicesForRange| will return 1 + 1/3 when we have: + * + * 01 02 03 04 05 06 07 08 09 10 + * <---------- range ----------> + * <- slice #1 -> <- slice #2 -> + */ + getNumToplevelSlicesForRange(range) { + return this.sumOverToplevelSlicesInRange(range, slice => 1); + }, + + getSchedulingStatsForRange(start, end) { + const stats = {}; + + if (!this.timeSlices) return stats; + + function addStatsForSlice(threadTimeSlice) { + const overlapStart = Math.max(threadTimeSlice.start, start); + const overlapEnd = Math.min(threadTimeSlice.end, end); + const schedulingState = threadTimeSlice.schedulingState; + + if (!(schedulingState in stats)) stats[schedulingState] = 0; + stats[schedulingState] += overlapEnd - overlapStart; + } + + tr.b.iterateOverIntersectingIntervals(this.timeSlices, + function(x) { return x.start; }, + function(x) { return x.end; }, + start, + end, + addStatsForSlice); + return stats; + }, + + get samples() { + return this.samples_; + }, + + /** + * Returns substring of this.name from beginning to the first numeric + * character or the character '/'. + * + * Example: + * ThreadName12 -> ThreadName + * ThreadName/34123 -> ThreadName + * ThreadName1/34123 -> ThreadName + */ + get type() { + const re = /^[^0-9|\/]+/; + const matches = re.exec(this.name); + if (matches && matches[0]) return matches[0]; + + // If a thread is named 42GPU, let's not try to find its type. + // We should fix the thread name. + throw new Error('Could not determine thread type for thread name ' + + this.name); + } + }; + + /** + * Comparison between threads that orders first by parent.compareTo, + * then by names, then by tid. + */ + Thread.compare = function(x, y) { + let tmp = x.parent.compareTo(y.parent); + if (tmp) return tmp; + + tmp = x.sortIndex - y.sortIndex; + if (tmp) return tmp; + + if (x.name !== undefined) { + if (y.name !== undefined) { + tmp = x.name.localeCompare(y.name); + } else { + tmp = -1; + } + } else if (y.name !== undefined) { + tmp = 1; + } + if (tmp) return tmp; + + return x.tid - y.tid; + }; + + return { + Thread, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/thread_slice.html b/chromium/third_party/catapult/tracing/tracing/model/thread_slice.html new file mode 100644 index 00000000000..f67d20d3e5d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/thread_slice.html @@ -0,0 +1,71 @@ +<!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/model/slice.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the Thread class. + */ +tr.exportTo('tr.model', function() { + const Slice = tr.model.Slice; + + /** + * A ThreadSlice represents an interval of time on a thread resource + * with associated nesting slice information. + * + * ThreadSlices are typically associated with a specific trace event pair on a + * specific thread. + * For example, + * TRACE_EVENT_BEGIN1("x","myArg", 7) at time=0.1ms + * TRACE_EVENT_END0() at time=0.3ms + * This results in a single slice from 0.1 with duration 0.2 on a + * specific thread. + * + * @constructor + */ + function ThreadSlice(cat, title, colorId, start, args, opt_duration, + opt_cpuStart, opt_cpuDuration, opt_argsStripped, + opt_bindId) { + Slice.call(this, cat, title, colorId, start, args, opt_duration, + opt_cpuStart, opt_cpuDuration, opt_argsStripped, opt_bindId); + // Do not modify this directly. + // subSlices is configured by SliceGroup.rebuildSubRows_. + this.subSlices = []; + } + + ThreadSlice.prototype = { + __proto__: Slice.prototype, + + get overlappingSamples() { + const samples = new tr.model.EventSet(); + if (!this.parentContainer || !this.parentContainer.samples) { + return samples; + } + this.parentContainer.samples.forEach(function(sample) { + if (this.start <= sample.start && sample.start <= this.end) { + samples.push(sample); + } + }, this); + return samples; + } + }; + + tr.model.EventRegistry.register( + ThreadSlice, + { + name: 'slice', + pluralName: 'slices' + }); + + return { + ThreadSlice, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/thread_slice_test.html b/chromium/third_party/catapult/tracing/tracing/model/thread_slice_test.html new file mode 100644 index 00000000000..b69efb79ac4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/thread_slice_test.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/thread_slice.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ThreadSlice = tr.model.ThreadSlice; + const newSliceEx = tr.c.TestUtils.newSliceEx; + const newFakeThread = tr.c.TestUtils.newFakeThread; + + test('getOverlappingSamples', function() { + const model = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + const node = tr.c.TestUtils.newProfileNodes(m, ['fake']); + m.samples.push( + new tr.model.Sample(1, 'a_1', node, m.t2), + new tr.model.Sample(2, 'a_2', node, m.t2), + new tr.model.Sample(3, 'a_3', node, m.t2), + new tr.model.Sample(5, 'b', node, m.t2) + ); + }); + const threadSlice = newSliceEx({title: 'a', start: 0, end: 4, + type: tr.model.ThreadSlice}); + threadSlice.parentContainer = model; + const samplesIter = threadSlice.overlappingSamples[Symbol.iterator](); + assert.strictEqual(samplesIter.next().value.title, 'a_1'); + assert.strictEqual(samplesIter.next().value.title, 'a_2'); + assert.strictEqual(samplesIter.next().value.title, 'a_3'); + assert.strictEqual(samplesIter.next().done, true); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/thread_test.html b/chromium/third_party/catapult/tracing/tracing/model/thread_test.html new file mode 100644 index 00000000000..4d2bccf0790 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/thread_test.html @@ -0,0 +1,209 @@ +<!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/math/range.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ThreadSlice = tr.model.ThreadSlice; + const Process = tr.model.Process; + const Thread = tr.model.Thread; + const newSliceEx = tr.c.TestUtils.newSliceEx; + const newAsyncSlice = tr.c.TestUtils.newAsyncSlice; + const newThreadSlice = tr.c.TestUtils.newThreadSlice; + const SCHEDULING_STATE = tr.model.SCHEDULING_STATE; + + test('threadBounds_Empty', function() { + const model = new tr.Model(); + const t = new Thread(new Process(model, 7), 1); + t.updateBounds(); + assert.isUndefined(t.bounds.min); + assert.isUndefined(t.bounds.max); + }); + + test('threadBounds_SubRow', function() { + const model = new tr.Model(); + const t = new Thread(new Process(model, 7), 1); + t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3)); + t.updateBounds(); + assert.strictEqual(t.bounds.min, 1); + assert.strictEqual(t.bounds.max, 4); + }); + + test('threadBounds_AsyncSliceGroup', function() { + const model = new tr.Model(); + const t = new Thread(new Process(model, 7), 1); + t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3)); + t.asyncSliceGroup.push(newAsyncSlice(0.1, 5, t, t)); + t.updateBounds(); + assert.strictEqual(t.bounds.min, 0.1); + assert.strictEqual(t.bounds.max, 5.1); + }); + + test('threadBounds_Cpu', function() { + const model = new tr.Model(); + const t = new Thread(new Process(model, 7), 1); + t.timeSlices = [newSliceEx({title: 'x', start: 0, duration: 1})]; + t.updateBounds(); + assert.strictEqual(t.bounds.min, 0); + assert.strictEqual(t.bounds.max, 1); + }); + + test('shiftTimestampsForwardWithCpu', function() { + const model = new tr.Model(); + const t = new Thread(new Process(model, 7), 1); + t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 0, {}, 3)); + t.asyncSliceGroup.push(newAsyncSlice(0, 5, t, t)); + t.timeSlices = [newSliceEx({title: 'x', start: 0, duration: 1})]; + + let shiftCount = 0; + t.asyncSliceGroup.shiftTimestampsForward = function(ts) { + if (ts === 0.32) { + shiftCount++; + } + }; + + t.shiftTimestampsForward(0.32); + + assert.strictEqual(shiftCount, 1); + assert.strictEqual(t.sliceGroup.slices[0].start, 0.32); + assert.strictEqual(t.timeSlices[0].start, 0.32); + }); + + test('shiftTimestampsForwardWithoutCpu', function() { + const model = new tr.Model(); + const t = new Thread(new Process(model, 7), 1); + t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 0, {}, 3)); + t.asyncSliceGroup.push(newAsyncSlice(0, 5, t, t)); + + let shiftCount = 0; + t.asyncSliceGroup.shiftTimestampsForward = function(ts) { + if (ts === 0.32) { + shiftCount++; + } + }; + + t.shiftTimestampsForward(0.32); + + assert.strictEqual(shiftCount, 1); + assert.strictEqual(t.sliceGroup.slices[0].start, 0.32); + }); + + test('getSchedulingStatsForRange', function() { + let scheduledThread = undefined; + let unscheduledThread = undefined; + const model = tr.c.TestUtils.newModel(function(model) { + unscheduledThread = model.getOrCreateProcess(1).getOrCreateThread(1); + unscheduledThread.sliceGroup.pushSlice(newSliceEx( + {title: 'work', start: 0, duration: 20})); + + scheduledThread = model.getOrCreateProcess(2).getOrCreateThread(2); + scheduledThread.sliceGroup.pushSlice(newSliceEx( + {title: 'work', start: 0, duration: 20})); + scheduledThread.timeSlices = [ + newThreadSlice(scheduledThread, SCHEDULING_STATE.RUNNING, 0, 3), + newThreadSlice(scheduledThread, SCHEDULING_STATE.RUNNABLE, 3, 5), + newThreadSlice(scheduledThread, SCHEDULING_STATE.RUNNING, 8, 2), + newThreadSlice(scheduledThread, SCHEDULING_STATE.SLEEPING, 10, 10) + ]; + }); + + // thread without scheduling states + let stats = unscheduledThread.getSchedulingStatsForRange(0, 20); + assert.deepEqual(stats, {}); + + // no scheduling info + stats = scheduledThread.getSchedulingStatsForRange(50, 100); + assert.deepEqual(stats, {}); + + // simple query + stats = scheduledThread.getSchedulingStatsForRange(0, 3); + let expected = {}; + expected[SCHEDULING_STATE.RUNNING] = 3; + assert.deepEqual(stats, expected); + + // aggregation + stats = scheduledThread.getSchedulingStatsForRange(0, 20); + expected = {}; + expected[SCHEDULING_STATE.RUNNING] = 5; + expected[SCHEDULING_STATE.RUNNABLE] = 5; + expected[SCHEDULING_STATE.SLEEPING] = 10; + assert.deepEqual(stats, expected); + }); + + test('getCpuTimeForRange', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const thread = model.getOrCreateProcess(1).getOrCreateThread(1); + const sliceSpecs = [ + {wallTimeBounds: [100, 200], cpuStart: 120, cpuDuration: 50}, + {wallTimeBounds: [300, 600], cpuStart: 350, cpuDuration: 150} + ]; + for (const sliceSpec of sliceSpecs) { + thread.sliceGroup.pushSlice(tr.c.TestUtils.newSliceEx({ + type: tr.model.ThreadSlice, + isTopLevel: true, + start: sliceSpec.wallTimeBounds[0], + duration: sliceSpec.wallTimeBounds[1] - sliceSpec.wallTimeBounds[0], + cpuStart: sliceSpec.cpuStart, + cpuDuration: sliceSpec.cpuDuration, + })); + } + }); + + const thread = model.getOrCreateProcess(1).getOrCreateThread(1); + const bounds = new tr.b.math.Range.fromExplicitRange(150, 400); + // 1/2 of first slice + 1/3 of second slice + const expectedCpuTime = 25 + 50; + + // Should be essentially equal, but choosing a very small epsilon 1e-7 + // to allow for floating point errors. + assert.closeTo(thread.getCpuTimeForRange(bounds), expectedCpuTime, 1e-7); + }); + + test('typeGetterReturnsCorrectType', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + const thread1 = process.getOrCreateThread(1); + const thread2 = process.getOrCreateThread(2); + const thread3 = process.getOrCreateThread(3); + const thread4 = process.getOrCreateThread(4); + + thread1.name = 'ThreadName12'; + thread2.name = 'ThreadName/34123'; + thread3.name = 'ThreadName1/34123'; + thread4.name = 'ThreadName'; + + assert.strictEqual(thread1.type, 'ThreadName'); + assert.strictEqual(thread2.type, 'ThreadName'); + assert.strictEqual(thread3.type, 'ThreadName'); + assert.strictEqual(thread4.type, 'ThreadName'); + }); + }); + + test('typeGetterThrowsIfThreadNameStartsWithNumberOrSlash', function() { + const model = tr.c.TestUtils.newModel(function(model) { + const process = model.getOrCreateProcess(1); + const thread1 = process.getOrCreateThread(1); + const thread2 = process.getOrCreateThread(2); + const thread3 = process.getOrCreateThread(3); + + thread1.name = '123'; + thread2.name = '42GPU'; + thread3.name = '/123'; + + assert.throws(() => thread1.type); + assert.throws(() => thread2.type); + assert.throws(() => thread3.type); + }); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/thread_time_slice.html b/chromium/third_party/catapult/tracing/tracing/model/thread_time_slice.html new file mode 100644 index 00000000000..69437c76819 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/thread_time_slice.html @@ -0,0 +1,154 @@ +<!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/math/range.html"> +<link rel="import" href="/tracing/model/slice.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + const Slice = tr.model.Slice; + + + const SCHEDULING_STATE = { + DEBUG: 'Debug', + EXIT_DEAD: 'Exit Dead', + RUNNABLE: 'Runnable', + RUNNING: 'Running', + SLEEPING: 'Sleeping', + STOPPED: 'Stopped', + TASK_DEAD: 'Task Dead', + UNINTR_SLEEP: 'Uninterruptible Sleep', + UNINTR_SLEEP_WAKE_KILL: 'Uninterruptible Sleep | WakeKill', + UNINTR_SLEEP_WAKING: 'Uninterruptible Sleep | Waking', + UNINTR_SLEEP_IO: 'Uninterruptible Sleep - Block I/O', + UNINTR_SLEEP_WAKE_KILL_IO: 'Uninterruptible Sleep | WakeKill - Block I/O', + UNINTR_SLEEP_WAKING_IO: 'Uninterruptible Sleep | Waking - Block I/O', + UNKNOWN: 'UNKNOWN', + WAKE_KILL: 'Wakekill', + WAKING: 'Waking', + ZOMBIE: 'Zombie' + }; + + /** + * A ThreadTimeSlice is a slice of time on a specific thread where that thread + * was running on a specific CPU, or in a specific sleep state. + * + * As a thread switches moves through its life, it sometimes goes to sleep and + * can't run. Other times, its runnable but isn't actually assigned to a CPU. + * Finally, sometimes it gets put on a CPU to actually execute. Each of these + * states is represented by a ThreadTimeSlice: + * + * Sleeping or runnable: cpuOnWhichThreadWasRunning is undefined + * Running: cpuOnWhichThreadWasRunning is set. + * + * @constructor + */ + function ThreadTimeSlice(thread, schedulingState, cat, + start, args, opt_duration) { + Slice.call(this, cat, schedulingState, + this.getColorForState_(schedulingState), + start, args, opt_duration); + this.thread = thread; + this.schedulingState = schedulingState; + this.cpuOnWhichThreadWasRunning = undefined; + } + + ThreadTimeSlice.prototype = { + __proto__: Slice.prototype, + + getColorForState_(state) { + const getColorIdForReservedName = + tr.b.ColorScheme.getColorIdForReservedName; + + switch (state) { + case SCHEDULING_STATE.RUNNABLE: + return getColorIdForReservedName('thread_state_runnable'); + case SCHEDULING_STATE.RUNNING: + return getColorIdForReservedName('thread_state_running'); + case SCHEDULING_STATE.SLEEPING: + return getColorIdForReservedName('thread_state_sleeping'); + case SCHEDULING_STATE.DEBUG: + case SCHEDULING_STATE.EXIT_DEAD: + case SCHEDULING_STATE.STOPPED: + case SCHEDULING_STATE.TASK_DEAD: + case SCHEDULING_STATE.UNINTR_SLEEP: + case SCHEDULING_STATE.UNINTR_SLEEP_WAKE_KILL: + case SCHEDULING_STATE.UNINTR_SLEEP_WAKING: + case SCHEDULING_STATE.UNKNOWN: + case SCHEDULING_STATE.WAKE_KILL: + case SCHEDULING_STATE.WAKING: + case SCHEDULING_STATE.ZOMBIE: + return getColorIdForReservedName('thread_state_uninterruptible'); + case SCHEDULING_STATE.UNINTR_SLEEP_IO: + case SCHEDULING_STATE.UNINTR_SLEEP_WAKE_KILL_IO: + case SCHEDULING_STATE.UNINTR_SLEEP_WAKING_IO: + return getColorIdForReservedName('thread_state_iowait'); + default: + return getColorIdForReservedName('thread_state_unknown'); + } + }, + + get analysisTypeName() { + return 'tr.ui.analysis.ThreadTimeSlice'; + }, + + getAssociatedCpuSlice() { + if (!this.cpuOnWhichThreadWasRunning) return undefined; + const cpuSlices = this.cpuOnWhichThreadWasRunning.slices; + for (let i = 0; i < cpuSlices.length; i++) { + const cpuSlice = cpuSlices[i]; + if (cpuSlice.start !== this.start) continue; + if (cpuSlice.duration !== this.duration) continue; + return cpuSlice; + } + return undefined; + }, + + getCpuSliceThatTookCpu() { + if (this.cpuOnWhichThreadWasRunning) return undefined; + let curIndex = this.thread.indexOfTimeSlice(this); + let cpuSliceWhenLastRunning; + while (curIndex >= 0) { + const curSlice = this.thread.timeSlices[curIndex]; + if (!curSlice.cpuOnWhichThreadWasRunning) { + curIndex--; + continue; + } + cpuSliceWhenLastRunning = curSlice.getAssociatedCpuSlice(); + break; + } + if (!cpuSliceWhenLastRunning) return undefined; + + const cpu = cpuSliceWhenLastRunning.cpu; + const indexOfSliceOnCpuWhenLastRunning = + cpu.indexOf(cpuSliceWhenLastRunning); + const nextRunningSlice = cpu.slices[indexOfSliceOnCpuWhenLastRunning + 1]; + if (!nextRunningSlice) return undefined; + if (Math.abs(nextRunningSlice.start - cpuSliceWhenLastRunning.end) < + 0.00001) { + return nextRunningSlice; + } + return undefined; + } + }; + + tr.model.EventRegistry.register( + ThreadTimeSlice, + { + name: 'threadTimeSlice', + pluralName: 'threadTimeSlices' + }); + + + return { + ThreadTimeSlice, + SCHEDULING_STATE, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map.html b/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map.html new file mode 100644 index 00000000000..3c05b08d4a2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map.html @@ -0,0 +1,179 @@ +<!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/math/range.html"> +<link rel="import" href="/tracing/base/utils.html"> + +<script> +'use strict'; + +/** + * @fileoverview Provides the TimeToObjectInstanceMap class. + */ +tr.exportTo('tr.model', function() { + /** + * Tracks all the instances associated with a given ID over its lifetime. + * + * A scoped id can be used multiple times throughout a trace, referring to + * different objects at different times. This data structure does the + * bookkeeping to figure out what ObjectInstance is referred to at a given + * timestamp. + * + * @constructor + */ + function TimeToObjectInstanceMap( + createObjectInstanceFunction, parent, scopedId) { + this.createObjectInstanceFunction_ = createObjectInstanceFunction; + this.parent = parent; + this.scopedId = scopedId; + this.instances = []; + } + + TimeToObjectInstanceMap.prototype = { + idWasCreated(category, name, ts) { + if (this.instances.length === 0) { + this.instances.push(this.createObjectInstanceFunction_( + this.parent, this.scopedId, category, name, ts)); + this.instances[0].creationTsWasExplicit = true; + return this.instances[0]; + } + + let lastInstance = this.instances[this.instances.length - 1]; + if (ts < lastInstance.deletionTs) { + throw new Error('Mutation of the TimeToObjectInstanceMap must be ' + + 'done in ascending timestamp order.'); + } + lastInstance = this.createObjectInstanceFunction_( + this.parent, this.scopedId, category, name, ts); + lastInstance.creationTsWasExplicit = true; + this.instances.push(lastInstance); + return lastInstance; + }, + + addSnapshot(category, name, ts, args, opt_baseTypeName) { + if (this.instances.length === 0) { + this.instances.push(this.createObjectInstanceFunction_( + this.parent, this.scopedId, category, name, ts, opt_baseTypeName)); + } + + const i = tr.b.findIndexInSortedIntervals( + this.instances, + function(inst) { return inst.creationTs; }, + function(inst) { return inst.deletionTs - inst.creationTs; }, + ts); + + let instance; + if (i < 0) { + instance = this.instances[0]; + if (ts > instance.deletionTs || + instance.creationTsWasExplicit) { + throw new Error( + 'At the provided timestamp, no instance was still alive'); + } + + if (instance.snapshots.length !== 0) { + throw new Error( + 'Cannot shift creationTs forward, ' + + 'snapshots have been added. First snap was at ts=' + + instance.snapshots[0].ts + ' and creationTs was ' + + instance.creationTs); + } + instance.creationTs = ts; + } else if (i >= this.instances.length) { + instance = this.instances[this.instances.length - 1]; + if (ts >= instance.deletionTs) { + // The snap is added after our oldest and deleted instance. This means + // that this is a new implicit instance. + instance = this.createObjectInstanceFunction_( + this.parent, this.scopedId, category, name, ts, opt_baseTypeName); + this.instances.push(instance); + } else { + // If the ts is before the last objects deletion time, then the caller + // is trying to add a snapshot when there may have been an instance + // alive. In that case, try to move an instance's creationTs to + // include this ts, provided that it has an implicit creationTs. + + // Search backward from the right for an instance that was definitely + // deleted before this ts. Any time an instance is found that has a + // moveable creationTs + let lastValidIndex; + for (let i = this.instances.length - 1; i >= 0; i--) { + const tmp = this.instances[i]; + if (ts >= tmp.deletionTs) break; + if (tmp.creationTsWasExplicit === false && + tmp.snapshots.length === 0) { + lastValidIndex = i; + } + } + if (lastValidIndex === undefined) { + throw new Error( + 'Cannot add snapshot. No instance was alive that was mutable.'); + } + instance = this.instances[lastValidIndex]; + instance.creationTs = ts; + } + } else { + instance = this.instances[i]; + } + + return instance.addSnapshot(ts, args, name, opt_baseTypeName); + }, + + get lastInstance() { + if (this.instances.length === 0) return undefined; + return this.instances[this.instances.length - 1]; + }, + + idWasDeleted(category, name, ts) { + if (this.instances.length === 0) { + this.instances.push(this.createObjectInstanceFunction_( + this.parent, this.scopedId, category, name, ts)); + } + let lastInstance = this.instances[this.instances.length - 1]; + if (ts < lastInstance.creationTs) { + throw new Error('Cannot delete an id before it was created'); + } + if (lastInstance.deletionTs === Number.MAX_VALUE) { + lastInstance.wasDeleted(ts); + return lastInstance; + } + + if (ts < lastInstance.deletionTs) { + throw new Error('id was already deleted earlier.'); + } + + // A new instance was deleted with no snapshots in-between. + // Create an instance then kill it. + lastInstance = this.createObjectInstanceFunction_( + this.parent, this.scopedId, category, name, ts); + this.instances.push(lastInstance); + lastInstance.wasDeleted(ts); + return lastInstance; + }, + + getInstanceAt(ts) { + const i = tr.b.findIndexInSortedIntervals( + this.instances, + function(inst) { return inst.creationTs; }, + function(inst) { return inst.deletionTs - inst.creationTs; }, + ts); + if (i < 0) { + if (this.instances[0].creationTsWasExplicit) { + return undefined; + } + return this.instances[0]; + } else if (i >= this.instances.length) { + return undefined; + } + return this.instances[i]; + } + }; + + return { + TimeToObjectInstanceMap, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map_test.html b/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map_test.html new file mode 100644 index 00000000000..70cd9ad0d5f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/time_to_object_instance_map_test.html @@ -0,0 +1,164 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/object_instance.html"> +<link rel="import" href="/tracing/model/time_to_object_instance_map.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const createObjectInstance = function( + parent, id, category, name, creationTs) { + return new tr.model.ObjectInstance( + parent, id, category, name, creationTs); + }; + + test('timeToObjectInstanceMap', function() { + const m = new tr.model.TimeToObjectInstanceMap( + createObjectInstance, {}, 7); + m.addSnapshot('cat', 'name', 10, 'a1'); + m.addSnapshot('cat', 'name', 20, 'a2'); + m.idWasDeleted('cat', 'name', 30); + m.addSnapshot('cat', 'name', 40, 'b'); + + assert.strictEqual(m.instances.length, 2); + + const i0 = m.getInstanceAt(0); + const i10 = m.getInstanceAt(10); + assert.strictEqual(i0, i10); + + assert.isDefined(i10); + assert.strictEqual(i10.snapshots.length, 2); + assert.strictEqual(i10.snapshots[0].args, 'a1'); + assert.strictEqual(i10.snapshots[1].args, 'a2'); + + assert.strictEqual(i10.deletionTs, 30); + + const i15 = m.getInstanceAt(15); + assert.strictEqual(i15, i10); + + const i20 = m.getInstanceAt(20); + assert.strictEqual(i20, i10); + + const i30 = m.getInstanceAt(30); + assert.isUndefined(i30); + + const i35 = m.getInstanceAt(35); + assert.isUndefined(i35); + + const i40 = m.getInstanceAt(40); + assert.isDefined(i40); + assert.notEqual(i40, i10); + assert.strictEqual(i40.snapshots.length, 1); + assert.strictEqual(i40.creationTs, 40); + assert.strictEqual(i40.deletionTs, Number.MAX_VALUE); + + const i41 = m.getInstanceAt(41); + assert.strictEqual(i40, i41); + }); + + test('timeToObjectInstanceMapsBoundsLogic', function() { + const m = new tr.model.TimeToObjectInstanceMap( + createObjectInstance, {}, 7); + m.addSnapshot('cat', 'name', 10, 'a1'); + m.addSnapshot('cat', 'name', 20, 'a2'); + m.idWasDeleted('cat', 'name', 30); + m.addSnapshot('cat', 'name', 40, 'b'); + m.addSnapshot('cat', 'name', 41, 'b'); + + m.instances.forEach(function(i) { i.updateBounds(); }); + + const iA = m.getInstanceAt(10); + assert.strictEqual(iA.bounds.min, 10); + assert.strictEqual(iA.bounds.max, 30); + + const iB = m.getInstanceAt(40); + assert.strictEqual(iB.bounds.min, 40); + assert.strictEqual(iB.bounds.max, 41); + }); + + test('earlySnapshot', function() { + const m = new tr.model.TimeToObjectInstanceMap( + createObjectInstance, {}, 7); + const i10 = m.idWasCreated('cat', 'name', 10, 'a1'); + m.idWasDeleted('cat', 'name', 20); + + assert.throws(function() { + m.addSnapshot('cat', 'name', 5, 'a1'); + }); + assert.strictEqual(i10.creationTs, 10); + assert.strictEqual(i10.deletionTs, 20); + }); + + test('earlySnapshotWithImplicitCreate', function() { + const m = new tr.model.TimeToObjectInstanceMap( + createObjectInstance, {}, 7); + const i10 = m.idWasDeleted('cat', 'name', 20); + m.addSnapshot('cat', 'name', 5, 'a1'); + assert.strictEqual(i10.creationTs, 5); + assert.strictEqual(i10.deletionTs, 20); + }); + + test('getInstanceBeforeCreationImplicitCreate', function() { + const m = new tr.model.TimeToObjectInstanceMap( + createObjectInstance, {}, 7); + const i10 = m.idWasCreated('cat', 'name', 10, 'a1'); + m.idWasDeleted('cat', 'name', 20); + assert.isUndefined(m.getInstanceAt(5)); + }); + + test('getInstanceBeforeCreationImplicitCreateWithSnapshot', function() { + const m = new tr.model.TimeToObjectInstanceMap( + createObjectInstance, {}, 7); + const s5 = m.addSnapshot('cat', 'name', 5, 'a1'); + const i10 = m.idWasDeleted('cat', 'name', 20); + assert.strictEqual(m.getInstanceAt(5), i10); + }); + + test('successiveDeletions', function() { + const m = new tr.model.TimeToObjectInstanceMap( + createObjectInstance, {}, 7); + const i20 = m.idWasDeleted('cat', 'name', 20); + const i30 = m.idWasDeleted('cat', 'name', 30); + const i40 = m.idWasDeleted('cat', 'name', 40); + assert.strictEqual(i20.creationTs, 20); + assert.isFalse(i20.creationTsWasExplicit); + assert.strictEqual(i20.deletionTs, 20); + assert.isTrue(i20.deletionTsWasExplicit); + + assert.strictEqual(i30.creationTs, 30); + assert.isFalse(i30.creationTsWasExplicit); + assert.strictEqual(i30.deletionTs, 30); + assert.isTrue(i30.deletionTsWasExplicit); + + + assert.strictEqual(i40.creationTs, 40); + assert.isFalse(i40.creationTsWasExplicit); + assert.strictEqual(i40.deletionTs, 40); + assert.isTrue(i40.deletionTsWasExplicit); + }); + + test('snapshotAfterDeletion', function() { + const m = new tr.model.TimeToObjectInstanceMap( + createObjectInstance, {}, 7); + const i10 = m.idWasCreated('cat', 'name', 10, 'a1'); + m.idWasDeleted('cat', 'name', 20); + + const s25 = m.addSnapshot('cat', 'name', 25, 'a1'); + const i25 = s25.objectInstance; + + assert.strictEqual(i10.creationTs, 10); + assert.strictEqual(i10.deletionTs, 20); + assert.notEqual(i25, i10); + assert.strictEqual(i25.creationTs, 25); + assert.strictEqual(i25.deletionTs, Number.MAX_VALUE); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/model/timed_event.html b/chromium/third_party/catapult/tracing/tracing/model/timed_event.html new file mode 100644 index 00000000000..4916162a4e1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/timed_event.html @@ -0,0 +1,69 @@ +<!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/guid.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/time_display_modes.html"> +<link rel="import" href="/tracing/model/event.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + /** + * TimedEvent is a base type for any entity in the trace model with a specific + * start and duration. + * + * @constructor + */ + function TimedEvent(start) { + tr.model.Event.call(this); + this.start = start; + this.duration = 0; + this.cpuStart = undefined; + this.cpuDuration = undefined; + // The set of contexts this event belongs to (order is unimportant). This + // array should never be modified. + this.contexts = Object.freeze([]); + } + + TimedEvent.prototype = { + __proto__: tr.model.Event.prototype, + + get end() { + return this.start + this.duration; + }, + + get boundsRange() { + return tr.b.math.Range.fromExplicitRange(this.start, this.end); + }, + + addBoundsToRange(range) { + range.addValue(this.start); + range.addValue(this.end); + }, + + // TODO(charliea): Can this be implemented in terms of Event.range()? + // Returns true if 'that' TimedEvent is fully contained within 'this' timed + // event. + bounds(that, opt_precisionUnit) { + if (opt_precisionUnit === undefined) { + opt_precisionUnit = tr.b.TimeDisplayModes.ms; + } + + const startsBefore = opt_precisionUnit.roundedLess( + that.start, this.start); + const endsAfter = opt_precisionUnit.roundedLess(this.end, that.end); + return !startsBefore && !endsAfter; + } + }; + + return { + TimedEvent, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/timed_event_test.html b/chromium/third_party/catapult/tracing/tracing/model/timed_event_test.html new file mode 100644 index 00000000000..f635dcf677d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/timed_event_test.html @@ -0,0 +1,43 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/timed_event.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('bounds_startPrecision', function() { + const unit = tr.b.TimeDisplayModes; + + const outer = new tr.model.TimedEvent(10.0001); + outer.duration = 0.9999; + const inner = new tr.model.TimedEvent(10.0000); + inner.duration = 1.0000; + + assert.isTrue(outer.bounds(inner)); + assert.isTrue(outer.bounds(inner, unit.ms)); + + assert.isFalse(outer.bounds(inner, unit.ns)); + }); + + test('bounds_endPrecision', function() { + const unit = tr.b.TimeDisplayModes; + + const outer = new tr.model.TimedEvent(10.0000); + outer.duration = 0.9999; + const inner = new tr.model.TimedEvent(10.0000); + inner.duration = 1.0000; + + assert.isTrue(outer.bounds(inner)); + assert.isTrue(outer.bounds(inner, unit.ms)); + + assert.isFalse(outer.bounds(inner, unit.ns)); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/animation_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/animation_expectation.html new file mode 100644 index 00000000000..a3fced6de2c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/animation_expectation.html @@ -0,0 +1,51 @@ +<!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/user_expectation.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.um', function() { + function AnimationExpectation( + parentModel, initiatorTitle, start, duration) { + tr.model.um.UserExpectation.call( + this, parentModel, initiatorTitle, start, duration); + this.frameEvents_ = undefined; + } + + AnimationExpectation.prototype = { + __proto__: tr.model.um.UserExpectation.prototype, + constructor: AnimationExpectation, + + get frameEvents() { + if (this.frameEvents_) { + return this.frameEvents_; + } + + this.frameEvents_ = new tr.model.EventSet(); + + this.associatedEvents.forEach(function(event) { + if (event.title === tr.model.helpers.IMPL_RENDERING_STATS) { + this.frameEvents_.push(event); + } + }, this); + + return this.frameEvents_; + } + }; + + tr.model.um.UserExpectation.subTypes.register(AnimationExpectation, { + stageTitle: 'Animation', + colorId: tr.b.ColorScheme.getColorIdForReservedName('rail_animation') + }); + + return { + AnimationExpectation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/idle_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/idle_expectation.html new file mode 100644 index 00000000000..2312d46b62a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/idle_expectation.html @@ -0,0 +1,34 @@ +<!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/user_expectation.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.um', function() { + function IdleExpectation(parentModel, start, duration) { + const initiatorTitle = ''; + tr.model.um.UserExpectation.call( + this, parentModel, initiatorTitle, start, duration); + } + + IdleExpectation.prototype = { + __proto__: tr.model.um.UserExpectation.prototype, + constructor: IdleExpectation + }; + + tr.model.um.UserExpectation.subTypes.register(IdleExpectation, { + stageTitle: 'Idle', + colorId: tr.b.ColorScheme.getColorIdForReservedName('rail_idle') + }); + + return { + IdleExpectation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/load_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/load_expectation.html new file mode 100644 index 00000000000..fb5298a46ed --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/load_expectation.html @@ -0,0 +1,93 @@ +<!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/user_expectation.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.um', function() { + const LOAD_SUBTYPE_NAMES = { + SUCCESSFUL: 'Successful', + FAILED: 'Failed', + }; + + const DOES_LOAD_SUBTYPE_NAME_EXIST = {}; + for (const key in LOAD_SUBTYPE_NAMES) { + DOES_LOAD_SUBTYPE_NAME_EXIST[LOAD_SUBTYPE_NAMES[key]] = true; + } + + function LoadExpectation(parentModel, initiatorTitle, start, duration, + renderer, navigationStart, fmpEvent, dclEndEvent, cpuIdleTime, + timeToInteractive, url, frameId) { + if (!DOES_LOAD_SUBTYPE_NAME_EXIST[initiatorTitle]) { + throw new Error(initiatorTitle + ' is not in LOAD_SUBTYPE_NAMES'); + } + + tr.model.um.UserExpectation.call( + this, parentModel, initiatorTitle, start, duration); + + // |renderProcess| is the renderer process that contains the loading + // RenderFrame. + this.renderProcess = renderer; + + // |renderMainThread| is the CrRendererMain thread in the |renderProcess| + // that contains the loading RenderFrame. + this.renderMainThread = undefined; + + // |routingId| identifies the loading RenderFrame within the renderer + // process. + this.routingId = undefined; + + // |parentRoutingId| identifies the RenderFrame that created and contains + // the loading RenderFrame. + this.parentRoutingId = undefined; + + // |loadFinishedEvent|, if present, signals that this is a main frame. + this.loadFinishedEvent = undefined; + + // Startup LoadExpectations do not have renderProcess, routingId, or + // parentRoutingId. Maybe RenderLoadExpectation should be a separate class? + + // Navigation start event. The start of this event is the start time of + // load expectation. + this.navigationStart = navigationStart; + + // First meaningful event corresponding to the navigation start event. + this.fmpEvent = fmpEvent; + + // DomcontentLoadedEndEvent corresponding to the navigation start event. + this.domContentLoadedEndEvent = dclEndEvent; + + // The computed firstCpuIdleTime. Please look at time_to_interactive.html + // for further details about this. + this.firstCpuIdleTime = cpuIdleTime; + + // The time at which renderer is interactive. Please look at + // time_to_interactive.html for further details on how this is computed. + this.timeToInteractive = timeToInteractive; + + this.url = url; + this.frameId = frameId; + } + + LoadExpectation.prototype = { + __proto__: tr.model.um.UserExpectation.prototype, + constructor: LoadExpectation + }; + + tr.model.um.UserExpectation.subTypes.register(LoadExpectation, { + stageTitle: 'Load', + colorId: tr.b.ColorScheme.getColorIdForReservedName('rail_load') + }); + + return { + LOAD_SUBTYPE_NAMES, + LoadExpectation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/response_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/response_expectation.html new file mode 100644 index 00000000000..83faacb27c3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/response_expectation.html @@ -0,0 +1,35 @@ +<!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/user_expectation.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.um', function() { + function ResponseExpectation( + parentModel, initiatorTitle, start, duration, opt_isAnimationBegin) { + tr.model.um.UserExpectation.call( + this, parentModel, initiatorTitle, start, duration); + this.isAnimationBegin = opt_isAnimationBegin || false; + } + + ResponseExpectation.prototype = { + __proto__: tr.model.um.UserExpectation.prototype, + constructor: ResponseExpectation + }; + + tr.model.um.UserExpectation.subTypes.register(ResponseExpectation, { + stageTitle: 'Response', + colorId: tr.b.ColorScheme.getColorIdForReservedName('rail_response') + }); + + return { + ResponseExpectation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/segment.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/segment.html new file mode 100644 index 00000000000..61da1e26363 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/segment.html @@ -0,0 +1,49 @@ +<!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/model/timed_event.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.um', function() { + /** + * Segment represents a range of time during which the set of active + * UserExpectations does not change. Segments are guaranteed to not overlap, + * whereas UserExpectations can overlap. After UserModelBuilder builds the + * UserExpectations in the model, it segments the timeline into + * non-overlapping Segments and adds the constituent UserExpectations to each + * Segment. + */ + class Segment extends tr.model.TimedEvent { + constructor(start, duration) { + super(start); + this.duration = duration; + this.expectations_ = []; + } + + get expectations() { + return this.expectations_; + } + + clone() { + const clone = new Segment(this.start, this.duration); + clone.expectations.push(...this.expectations); + return clone; + } + + addSegment(other) { + this.duration += other.duration; + this.expectations.push(...other.expectations); + } + } + + return { + Segment, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/startup_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/startup_expectation.html new file mode 100644 index 00000000000..809debc3fb3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/startup_expectation.html @@ -0,0 +1,33 @@ +<!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/user_expectation.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.um', function() { + function StartupExpectation(parentModel, start, duration) { + tr.model.um.UserExpectation.call( + this, parentModel, '', start, duration); + } + + StartupExpectation.prototype = { + __proto__: tr.model.um.UserExpectation.prototype, + constructor: StartupExpectation + }; + + tr.model.um.UserExpectation.subTypes.register(StartupExpectation, { + stageTitle: 'Startup', + colorId: tr.b.ColorScheme.getColorIdForReservedName('startup') + }); + + return { + StartupExpectation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/stub_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/stub_expectation.html new file mode 100644 index 00000000000..4bd48ffba58 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/stub_expectation.html @@ -0,0 +1,76 @@ +<!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/core/test_utils.html"> +<link rel="import" href="/tracing/model/user_model/user_expectation.html"> + +<script> +'use strict'; + +/** + * @fileoverview Stub version of UserExpectation for testing. + */ +tr.exportTo('tr.model.um', function() { + function StubExpectation(args) { + this.stageTitle_ = args.stageTitle || 'Idle'; + this.initiatorTitle_ = args.initiatorTitle || ''; + + this.title_ = args.title; + if (!this.title_) { + const defaultTitle = []; + if (this.initiatorTitle_) { + defaultTitle.push(this.initiatorTitle_); + } + if (this.stageTitle_) { + defaultTitle.push(this.stageTitle_); + } + this.title_ = defaultTitle.join(' ') || 'title'; + } + + this.normalizedUserComfort_ = args.normalizedUserComfort || 0; + this.normalizedEfficiency_ = args.normalizedEfficiency || 0; + + const sd = tr.c.TestUtils.getStartAndDurationFromDict(args); + + tr.model.um.UserExpectation.call( + this, args.parentModel, this.initiatorTitle, sd.start, sd.duration); + + // Must be set after base class call. + this.colorId_ = args.colorId || 0; + + if (args.associatedEvents) { + args.associatedEvents.forEach(function(event) { + this.associatedEvents.push(event); + }, this); + } + } + + StubExpectation.prototype = { + __proto__: tr.model.um.UserExpectation.prototype, + + get colorId() { + return this.colorId_; + }, + + get title() { + return this.title_; + }, + + get stageTitle() { + return this.stageTitle_; + }, + + get initiatorTitle() { + return this.initiatorTitle_; + } + }; + + return { + StubExpectation, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/user_expectation.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_expectation.html new file mode 100644 index 00000000000..04363da142a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_expectation.html @@ -0,0 +1,177 @@ +<!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/base/math/statistics.html"> +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/model/compound_event_selection_state.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/timed_event.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.um', function() { + const CompoundEventSelectionState = tr.model.CompoundEventSelectionState; + + function UserExpectation(parentModel, initiatorType, start, duration) { + tr.model.TimedEvent.call(this, start); + this.associatedEvents = new tr.model.EventSet(); + this.duration = duration; + this.initiatorType_ = initiatorType; + this.parentModel = parentModel; + this.typeInfo_ = undefined; + + // sourceEvents are the ones that caused the UserModelBuilder to create this + // UserExpectation. + this.sourceEvents = new tr.model.EventSet(); + } + + // Strings used to name UEs. + const INITIATOR_TYPE = { + KEYBOARD: 'Keyboard', + MOUSE: 'Mouse', + MOUSE_WHEEL: 'MouseWheel', + TAP: 'Tap', + PINCH: 'Pinch', + FLING: 'Fling', + TOUCH: 'Touch', + SCROLL: 'Scroll', + CSS: 'CSS', + WEBGL: 'WebGL', + VIDEO: 'Video', + VR: 'VR', + }; + + UserExpectation.prototype = { + __proto__: tr.model.TimedEvent.prototype, + + computeCompoundEvenSelectionState(selection) { + let cess = CompoundEventSelectionState.NOT_SELECTED; + if (selection.contains(this)) { + cess |= CompoundEventSelectionState.EVENT_SELECTED; + } + + if (this.associatedEvents.intersectionIsEmpty(selection)) { + return cess; + } + + const allContained = this.associatedEvents.every(function(event) { + return selection.contains(event); + }); + + if (allContained) { + cess |= CompoundEventSelectionState.ALL_ASSOCIATED_EVENTS_SELECTED; + } else { + cess |= CompoundEventSelectionState.SOME_ASSOCIATED_EVENTS_SELECTED; + } + return cess; + }, + + // Returns samples which are overlapping with V8.Execute + get associatedSamples() { + const samples = new tr.model.EventSet(); + this.associatedEvents.forEach(function(event) { + if (event instanceof tr.model.ThreadSlice) { + samples.addEventSet(event.overlappingSamples); + } + }); + return samples; + }, + + get userFriendlyName() { + return this.title + ' User Expectation at ' + + tr.b.Unit.byName.timeStampInMs.format(this.start); + }, + + get stableId() { + return ('UserExpectation.' + this.guid); + }, + + get typeInfo() { + if (!this.typeInfo_) { + this.typeInfo_ = UserExpectation.subTypes.findTypeInfo( + this.constructor); + } + + // If you set Subclass.prototype = {}, then you must explicitly specify + // constructor in that prototype object! + // http://javascript.info/tutorial/constructor + + if (!this.typeInfo_) { + throw new Error('Unregistered UserExpectation'); + } + + return this.typeInfo_; + }, + + get colorId() { + return this.typeInfo.metadata.colorId; + }, + + get stageTitle() { + return this.typeInfo.metadata.stageTitle; + }, + + get initiatorType() { + return this.initiatorType_; + }, + + get title() { + if (!this.initiatorType) { + return this.stageTitle; + } + + return this.initiatorType + ' ' + this.stageTitle; + }, + + /** + * Returns the sum of the number of CPU ms spent by this UserExpectation. + */ + get totalCpuMs() { + let cpuMs = 0; + this.associatedEvents.forEach(function(event) { + if (event.cpuSelfTime) { + cpuMs += event.cpuSelfTime; + } + }); + return cpuMs; + } + }; + + const subTypes = {}; + const options = new tr.b.ExtensionRegistryOptions(tr.b.BASIC_REGISTRY_MODE); + tr.b.decorateExtensionRegistry(subTypes, options); + + subTypes.addEventListener('will-register', function(e) { + const metadata = e.typeInfo.metadata; + + if (metadata.stageTitle === undefined) { + throw new Error('Registered UserExpectations must provide ' + + 'stageTitle'); + } + + if (metadata.colorId === undefined) { + throw new Error('Registered UserExpectations must provide ' + + 'colorId'); + } + }); + + tr.model.EventRegistry.register( + UserExpectation, + { + name: 'userExpectation', + pluralName: 'userExpectations', + subTypes + }); + + return { + UserExpectation, + INITIATOR_TYPE, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model.html new file mode 100644 index 00000000000..38b6e13a00c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/event_container.html"> +<link rel="import" href="/tracing/model/user_model/segment.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model.um', function() { + class UserModel extends tr.model.EventContainer { + constructor(parentModel) { + super(); + this.parentModel_ = parentModel; + this.expectations_ = new tr.model.EventSet(); + this.segments_ = []; + } + + get stableId() { + return 'UserModel'; + } + + get parentModel() { + return this.parentModel_; + } + + sortExpectations() { + this.expectations_.sortEvents((x, y) => (x.start - y.start)); + } + + get expectations() { + return this.expectations_; + } + + shiftTimestampsForward(amount) { + } + + addCategoriesToDict(categoriesDict) { + } + + get segments() { + return this.segments_; + } + + * childEvents() { + yield* this.expectations; + } + + * childEventContainers() { + } + + updateBounds() { + this.bounds.reset(); + for (const expectation of this.expectations) { + expectation.addBoundsToRange(this.bounds); + } + } + + /** + * Return a new array of new Segments by merging adjacent segments when + * |getKeyForSegment| returns identical keys. + * |getKeyForSegment| is called with each Segment and the index of that + * Segment. + * + * @param {!function(!tr.model.um.Segment, number):*} getKeyForSegment + * @return {!Array.<!tr.model.um.Segment>} + */ + resegment(getKeyForSegment) { + const newSegments = []; + let prevKey = undefined; + let prevSegment = undefined; + for (let i = 0; i < this.segments.length; ++i) { + const segment = this.segments[i]; + const key = getKeyForSegment(segment, i); + if (prevSegment !== undefined && key === prevKey) { + prevSegment.addSegment(segment); + } else { + prevSegment = segment.clone(); + newSegments.push(prevSegment); + } + prevKey = key; + } + return newSegments; + } + } + + return { + UserModel, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model_test.html b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model_test.html new file mode 100644 index 00000000000..c2a09c5074e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/user_model/user_model_test.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/model/user_model/segment.html"> +<link rel="import" href="/tracing/model/user_model/user_model.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('resegment', function() { + const userModel = new tr.model.um.UserModel(undefined); + userModel.segments.push(new tr.model.um.Segment(0, 1)); + userModel.segments.push(new tr.model.um.Segment(1, 1)); + userModel.segments.push(new tr.model.um.Segment(2, 1)); + userModel.segments.push(new tr.model.um.Segment(3, 1)); + + userModel.segments[0].expectations.push('a'); + userModel.segments[1].expectations.push('b'); + userModel.segments[2].expectations.push('c'); + userModel.segments[3].expectations.push('d'); + + const newSegments = userModel.resegment( + (segment, index) => Math.floor(index / 2)); + + assert.lengthOf(newSegments, 2); + assert.strictEqual(0, newSegments[0].start); + assert.strictEqual(2, newSegments[0].end); + assert.strictEqual(2, newSegments[1].start); + assert.strictEqual(4, newSegments[1].end); + + assert.lengthOf(newSegments[0].expectations, 2); + assert.lengthOf(newSegments[1].expectations, 2); + + assert.strictEqual('a', newSegments[0].expectations[0]); + assert.strictEqual('b', newSegments[0].expectations[1]); + assert.strictEqual('c', newSegments[1].expectations[0]); + assert.strictEqual('d', newSegments[1].expectations[1]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/vm_region.html b/chromium/third_party/catapult/tracing/tracing/model/vm_region.html new file mode 100644 index 00000000000..4b02f7c24cc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/vm_region.html @@ -0,0 +1,444 @@ +<!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'; + +/** + * @fileoverview Provides classes for representing and classifying VM regions. + * + * See https://goo.gl/5SSPv0 for more details. + */ +tr.exportTo('tr.model', function() { + /** + * A single virtual memory region (also called a memory map). + * + * @constructor + */ + function VMRegion(startAddress, sizeInBytes, protectionFlags, + mappedFile, byteStats) { + this.startAddress = startAddress; + this.sizeInBytes = sizeInBytes; + this.protectionFlags = protectionFlags; + this.mappedFile = mappedFile || ''; + this.byteStats = byteStats || {}; + } + + VMRegion.PROTECTION_FLAG_READ = 4; + VMRegion.PROTECTION_FLAG_WRITE = 2; + VMRegion.PROTECTION_FLAG_EXECUTE = 1; + VMRegion.PROTECTION_FLAG_MAYSHARE = 128; + + VMRegion.prototype = { + get uniqueIdWithinProcess() { + // This value is assumed to be unique within a process. + return this.mappedFile + '#' + this.startAddress; + }, + + get protectionFlagsToString() { + if (this.protectionFlags === undefined) return undefined; + return ( + (this.protectionFlags & VMRegion.PROTECTION_FLAG_READ ? 'r' : '-') + + (this.protectionFlags & VMRegion.PROTECTION_FLAG_WRITE ? 'w' : '-') + + (this.protectionFlags & VMRegion.PROTECTION_FLAG_EXECUTE ? + 'x' : '-') + + (this.protectionFlags & VMRegion.PROTECTION_FLAG_MAYSHARE ? 's' : 'p') + ); + } + }; + + VMRegion.fromDict = function(dict) { + return new VMRegion( + dict.startAddress, + dict.sizeInBytes, + dict.protectionFlags, + dict.mappedFile, + dict.byteStats); + }; + + /** + * Node in a VM region classification tree. + * + * Note: Most users of this class should use the + * VMRegionClassificationNode.fromRegions static method instead of this + * constructor because it leads to better performance due to fewer memory + * allocations. + * + * @constructor + */ + function VMRegionClassificationNode(opt_rule) { + this.rule_ = opt_rule || VMRegionClassificationNode.CLASSIFICATION_RULES; + + // True iff this node or any of its descendant classification nodes has at + // least one classified VM region. + this.hasRegions = false; + + // Total virtual size and byte stats of all regions matching this node's + // rule (including its sub-rules). + this.sizeInBytes = undefined; + this.byteStats = {}; + + // Array of child classification nodes if this is an intermediate node. + this.children_ = undefined; + + // Array of VM regions. If this is an intermediate node, then the regions + // are cached for lazy tree construction (i.e. its child classification + // nodes yet have to be built). + this.regions_ = []; + } + + /** + * Rules for classifying memory maps. + * + * These rules are derived from core/jni/android_os_Debug.cpp in Android. + */ + VMRegionClassificationNode.CLASSIFICATION_RULES = { + name: 'Total', + children: [ + { + name: 'Android', + file: /^\/dev\/ashmem(?!\/libc malloc)/, + children: [ + { + name: 'Java runtime', + file: /^\/dev\/ashmem\/dalvik-/, + children: [ + { + name: 'Spaces', + file: /\/dalvik-(alloc|main|large object|non moving|zygote) space/, // @suppress longLineCheck + children: [ + { + name: 'Normal', + file: /\/dalvik-(alloc|main)/ + }, + { + name: 'Large', + file: /\/dalvik-large object/ + }, + { + name: 'Zygote', + file: /\/dalvik-zygote/ + }, + { + name: 'Non-moving', + file: /\/dalvik-non moving/ + } + ] + }, + { + name: 'Linear Alloc', + file: /\/dalvik-LinearAlloc/ + }, + { + name: 'Indirect Reference Table', + file: /\/dalvik-indirect.ref/ + }, + { + name: 'Cache', + file: /\/dalvik-jit-code-cache/ + }, + { + name: 'Accounting' + } + ] + }, + { + name: 'Cursor', + file: /\/CursorWindow/ + }, + { + name: 'Ashmem' + } + ] + }, + { + name: 'Native heap', + file: /^((\[heap\])|(\[anon:)|(\/dev\/ashmem\/libc malloc)|(\[discounted tracing overhead\])|$)/ // @suppress longLineCheck + }, + { + name: 'Stack', + file: /^\[stack/ + }, + { + name: 'Files', + file: /\.((((jar)|(apk)|(ttf)|(odex)|(oat)|(art))$)|(dex)|(so))/, + children: [ + { + name: 'so', + file: /\.so/ + }, + { + name: 'jar', + file: /\.jar$/ + }, + { + name: 'apk', + file: /\.apk$/ + }, + { + name: 'ttf', + file: /\.ttf$/ + }, + { + name: 'dex', + file: /\.((dex)|(odex$))/ + }, + { + name: 'oat', + file: /\.oat$/ + }, + { + name: 'art', + file: /\.art$/ + } + ] + }, + { + name: 'Devices', + file: /(^\/dev\/)|(anon_inode:dmabuf)/, + children: [ + { + name: 'GPU', + file: /\/((nv)|(mali)|(kgsl))/ + }, + { + name: 'DMA', + file: /anon_inode:dmabuf/ + } + ] + } + ] + }; + VMRegionClassificationNode.OTHER_RULE = { name: 'Other' }; + + VMRegionClassificationNode.fromRegions = function(regions, opt_rules) { + const tree = new VMRegionClassificationNode(opt_rules); + tree.regions_ = regions; + for (let i = 0; i < regions.length; i++) { + tree.addStatsFromRegion_(regions[i]); + } + return tree; + }; + + VMRegionClassificationNode.prototype = { + get title() { + return this.rule_.name; + }, + + get children() { + if (this.isLeafNode) { + return undefined; // Leaf nodes don't have children (by definition). + } + if (this.children_ === undefined) { + this.buildTree_(); // Lazily classify VM regions. + } + return this.children_; + }, + + get regions() { + if (!this.isLeafNode) { + // Intermediate nodes only temporarily cache VM regions for lazy tree + // construction. + return undefined; + } + return this.regions_; + }, + + get allRegionsForTesting() { + if (this.regions_ !== undefined) { + if (this.children_ !== undefined) { + throw new Error('Internal error: a VM region classification node ' + + 'cannot have both regions and children'); + } + // Leaf node (or caching internal node). + return this.regions_; + } + + // Intermediate node. + let regions = []; + this.children_.forEach(function(childNode) { + regions = regions.concat(childNode.allRegionsForTesting); + }); + return regions; + }, + + get isLeafNode() { + const children = this.rule_.children; + return children === undefined || children.length === 0; + }, + + addRegion(region) { + this.addRegionRecursively_(region, true /* addStatsToThisNode */); + }, + + someRegion(fn, opt_this) { + if (this.regions_ !== undefined) { + // Leaf node (or caching internal node). + return this.regions_.some(fn, opt_this); + } + + // Intermediate node. + return this.children_.some(function(childNode) { + return childNode.someRegion(fn, opt_this); + }); + }, + + addRegionRecursively_(region, addStatsToThisNode) { + if (addStatsToThisNode) { + this.addStatsFromRegion_(region); + } + + if (this.regions_ !== undefined) { + if (this.children_ !== undefined) { + throw new Error('Internal error: a VM region classification node ' + + 'cannot have both regions and children'); + } + // Leaf node or an intermediate node caching VM regions (add the + // region to this node and don't classify further). + this.regions_.push(region); + return; + } + + // Non-leaf rule (classify region row further down the tree). + function regionRowMatchesChildNide(child) { + const fileRegExp = child.rule_.file; + if (fileRegExp === undefined) return true; + return fileRegExp.test(region.mappedFile); + } + + let matchedChild = this.children_.find(regionRowMatchesChildNide); + if (matchedChild === undefined) { + // Region belongs to the 'Other' node (created lazily). + if (this.children_.length !== this.rule_.children.length) { + throw new Error('Internal error'); + } + matchedChild = new VMRegionClassificationNode( + VMRegionClassificationNode.OTHER_RULE); + this.children_.push(matchedChild); + } + + matchedChild.addRegionRecursively_(region, true); + }, + + buildTree_() { + const cachedRegions = this.regions_; + this.regions_ = undefined; + + this.buildChildNodesRecursively_(); + for (let i = 0; i < cachedRegions.length; i++) { + // Note that we don't add the VM region's stats to this node because + // they have already been added to it. + this.addRegionRecursively_( + cachedRegions[i], false /* addStatsToThisNode */); + } + }, + + buildChildNodesRecursively_() { + if (this.children_ !== undefined) { + throw new Error( + 'Internal error: Classification node already has children'); + } + if (this.regions_ !== undefined && this.regions_.length !== 0) { + throw new Error( + 'Internal error: Classification node should have no regions'); + } + + if (this.isLeafNode) { + return; // Leaf node: Nothing to do. + } + + // Intermediate node: Clear regions and build children recursively. + this.regions_ = undefined; + this.children_ = this.rule_.children.map(function(childRule) { + const child = new VMRegionClassificationNode(childRule); + child.buildChildNodesRecursively_(); + return child; + }); + }, + + addStatsFromRegion_(region) { + this.hasRegions = true; + + // Aggregate virtual size. + const regionSizeInBytes = region.sizeInBytes; + if (regionSizeInBytes !== undefined) { + this.sizeInBytes = (this.sizeInBytes || 0) + regionSizeInBytes; + } + + // Aggregate byte stats. + const thisByteStats = this.byteStats; + const regionByteStats = region.byteStats; + for (const byteStatName in regionByteStats) { + const regionByteStatValue = regionByteStats[byteStatName]; + if (regionByteStatValue === undefined) continue; + thisByteStats[byteStatName] = + (thisByteStats[byteStatName] || 0) + regionByteStatValue; + } + + // Aggregate java base.* stats. + if (region.mappedFile.includes('/base.odex') || + region.mappedFile.includes('/base.vdex')) { + if (region.byteStats.proportionalResident !== undefined) { + thisByteStats.javaBasePss = + (thisByteStats.javaBasePss || 0) + + region.byteStats.proportionalResident; + } + if (region.byteStats.privateCleanResident !== undefined) { + thisByteStats.javaBaseCleanResident = + (thisByteStats.javaBaseCleanResident || 0) + + region.byteStats.privateCleanResident; + } + if (region.byteStats.sharedCleanResident !== undefined) { + thisByteStats.javaBaseCleanResident = + (thisByteStats.javaBaseCleanResident || 0) + + region.byteStats.sharedCleanResident; + } + } + + // Aggregate native library stats. + const textProtectionFlags = (VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE); + // On post-M devices, the native library is mapped from /base.apk. On M + // and earlier devices, it's mapped from /libchrome.so. In both cases only + // the regions that are readable and executable should be counted. + // TODO(mattcary): if both mappings are seen, something has gone wrong and + // some sort of error or fatal should be done. This should be tracked + // across regions, which means adding state to |this|. + if ((region.protectionFlags === textProtectionFlags) && + (region.mappedFile.includes('/base.apk') || + region.mappedFile.includes('/libchrome.so'))) { + if (regionSizeInBytes !== undefined) { + this.nativeLibrarySizeInBytes = + (this.nativeLibrarySizeInBytes || 0) + regionSizeInBytes; + } + if (region.byteStats.privateCleanResident !== undefined) { + thisByteStats.nativeLibraryPrivateCleanResident = + (thisByteStats.nativeLibraryPrivateCleanResident || 0) + + region.byteStats.privateCleanResident; + } + if (region.byteStats.sharedCleanResident !== undefined) { + thisByteStats.nativeLibrarySharedCleanResident = + (thisByteStats.nativeLibrarySharedCleanResident || 0) + + region.byteStats.sharedCleanResident; + } + if (region.byteStats.proportionalResident !== undefined) { + thisByteStats.nativeLibraryProportionalResident = + (thisByteStats.nativeLibraryProportionalResident || 0) + + region.byteStats.proportionalResident; + } + } + } + }; + + return { + VMRegion, + VMRegionClassificationNode, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/vm_region_test.html b/chromium/third_party/catapult/tracing/tracing/model/vm_region_test.html new file mode 100644 index 00000000000..93b8ad1647c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/vm_region_test.html @@ -0,0 +1,1216 @@ +<!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/model/memory_dump_test_utils.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const VMRegion = tr.model.VMRegion; + const VMRegionClassificationNode = tr.model.VMRegionClassificationNode; + const checkVMRegions = tr.model.MemoryDumpTestUtils.checkVMRegions; + + function checkProtectionFlagsToString(protectionFlags, expectedString) { + const vmRegion = VMRegion.fromDict({ + startAddress: 256, + sizeInBytes: 336, + protectionFlags, + mappedFile: '[stack:20310]', + byteStats: { + privateDirtyResident: 96, + swapped: 144, + proportionalResident: 158 + } + }); + assert.strictEqual(vmRegion.protectionFlagsToString, expectedString); + } + + const TEST_RULES = { + name: 'Root', + children: [ + { + name: 'Words', + file: /^[a-zA-Z]/, + children: [ + { + name: 'A-D', + file: /^[a-dA-D]/ + }, + { + name: 'E-H', + file: /^[e-hE-H]/ + } + ] + }, + { + name: 'Digits', + file: /\d$/, + children: [] + } + ] + }; + + // Constant representing the expectation that the children of a + // VMRegionClassificationNode have not been built yet. + const CHILDREN_NOT_BUILT_YET = {}; + + function checkTree(node, expectedStructure) { + assert.strictEqual(node.title, expectedStructure.title); + assert.strictEqual(node.hasRegions, expectedStructure.hasRegions); + assert.strictEqual(node.sizeInBytes, expectedStructure.sizeInBytes); + assert.deepEqual(node.byteStats, expectedStructure.byteStats || {}); + assert.strictEqual(node.isLeafNode, expectedStructure.isLeafNode); + + const actualRegions = node.regions; + const expectedRegions = expectedStructure.regions; + if (expectedRegions === undefined) { + assert.isUndefined(actualRegions); + } else { + assert.instanceOf(actualRegions, Array); + checkVMRegions(actualRegions, expectedRegions); + } + + const expectedChildren = expectedStructure.children; + if (expectedChildren === CHILDREN_NOT_BUILT_YET) { + assert.isUndefined(node.children_); + } else if (expectedChildren === undefined) { + assert.isUndefined(node.children); + } else { + const actualChildrenMap = new Map(); + node.children.forEach(function(childNode) { + actualChildrenMap.set(childNode.title, childNode); + }); + const expectedChildrenMap = new Map(); + expectedChildren.forEach(function(childNode) { + expectedChildrenMap.set(childNode.title, childNode); + }); + assert.strictEqual(actualChildrenMap.size, expectedChildrenMap.size); + for (const title of expectedChildrenMap.keys()) { + checkTree(actualChildrenMap.get(title), + expectedChildrenMap.get(title)); + } + } + } + + function checkClassificationRules(mappedFile, expectedPath) { + const region = VMRegion.fromDict({ + mappedFile, + sizeInBytes: 16, + byteStats: { + privateDirtyResident: 7 + } + }); + let node = VMRegionClassificationNode.fromRegions([region]); + for (const title of expectedPath) { + node = node.children.find(c => c.title === title); + } + assert.deepEqual(node.regions, [region]); + } + + test('vmRegion_protectionFlagsToString', function() { + checkProtectionFlagsToString(undefined, undefined); + checkProtectionFlagsToString(0, '---p'); + checkProtectionFlagsToString(VMRegion.PROTECTION_FLAG_READ, 'r--p'); + checkProtectionFlagsToString( + VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_MAYSHARE, + 'r--s'); + checkProtectionFlagsToString( + VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_EXECUTE, + 'r-xp'); + checkProtectionFlagsToString( + VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_WRITE, + 'rw-p'); + checkProtectionFlagsToString( + VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_WRITE | + VMRegion.PROTECTION_FLAG_EXECUTE, + 'rwxp'); + checkProtectionFlagsToString( + VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_WRITE | + VMRegion.PROTECTION_FLAG_MAYSHARE, + 'rw-s'); + checkProtectionFlagsToString( + VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_EXECUTE | + VMRegion.PROTECTION_FLAG_MAYSHARE, + 'r-xs'); + checkProtectionFlagsToString( + VMRegion.PROTECTION_FLAG_READ | VMRegion.PROTECTION_FLAG_WRITE | + VMRegion.PROTECTION_FLAG_EXECUTE | + VMRegion.PROTECTION_FLAG_MAYSHARE, + 'rwxs'); + }); + + // The add(After|Before)Build tests below check that the classification tree + // has the correct structure regardless of the ordering of adding regions and + // the lazy construction. + + test('vmRegionClassificationNode_constructor_addAfterBuild', function() { + const rootNode = new VMRegionClassificationNode(TEST_RULES); + + // Check the root node and verify that the full tree structure has *not* + // been constructed yet. + checkTree(rootNode, { + title: 'Root', + hasRegions: false, + isLeafNode: false, + children: CHILDREN_NOT_BUILT_YET + }); + + // Reading the children of the root node *should* trigger building the + // full tree. + checkTree(rootNode, { + title: 'Root', + hasRegions: false, + isLeafNode: false, + children: [ + { + title: 'Words', + hasRegions: false, + isLeafNode: false, + children: [ + { + title: 'A-D', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'E-H', + hasRegions: false, + isLeafNode: true, + regions: [] + } + ] + }, + { + title: 'Digits', + hasRegions: false, + isLeafNode: true, + regions: [] + } + ] + }); + + // Add VM regions to the tree *after* it has been fully built. + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: 'W2', // Root/Words/Other. + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + } + })); + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: '__42', // Root/Digits. + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + })); + checkTree(rootNode, { + title: 'Root', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32 + 33, + privateDirtyResident: 77, + swapped: 64 + }, + isLeafNode: false, + children: [ + { + title: 'Words', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + }, + isLeafNode: false, + children: [ + { + title: 'A-D', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'E-H', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'Other', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + }, + isLeafNode: true, + regions: [ + { + mappedFile: 'W2', + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + } + } + ] + } + ] + }, + { + title: 'Digits', + hasRegions: true, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + }, + isLeafNode: true, + regions: [ + { + mappedFile: '__42', + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + } + ] + } + ] + }); + }); + + test('vmRegionClassificationNode_constructor_addBeforeBuild', function() { + const rootNode = new VMRegionClassificationNode(TEST_RULES); + + // Add regions to the tree *before* it has been fully built. This should + // *not* trigger building the full tree (but the total sizeInBytes and + // byteStats should be updated accordingly). + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: '__42', // Root/Digits. + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + })); + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: 'W2', // Root/Words/Other. + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + } + })); + checkTree(rootNode, { + title: 'Root', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32 + 33, + privateDirtyResident: 77, + swapped: 64 + }, + isLeafNode: false, + children: CHILDREN_NOT_BUILT_YET + }); + + // Reading the children of the root node should trigger building the full + // tree. + checkTree(rootNode, { + title: 'Root', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32 + 33, + privateDirtyResident: 77, + swapped: 64 + }, + isLeafNode: false, + children: [ + { + title: 'Words', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + }, + isLeafNode: false, + children: [ + { + title: 'A-D', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'E-H', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'Other', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + }, + isLeafNode: true, + regions: [ + { + mappedFile: 'W2', + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + } + } + ] + } + ] + }, + { + title: 'Digits', + hasRegions: true, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + }, + isLeafNode: true, + regions: [ + { + mappedFile: '__42', + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + } + ] + } + ] + }); + + // Add more VM regions *after* the tree has been fully built. + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: '%invalid%', // Root/Other. + sizeInBytes: 123 + })); + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: '__43', // Root/Digits. + byteStats: { + swapped: 19 + } + })); + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: 'free', // Root/Words/E-H. + sizeInBytes: undefined + })); + checkTree(rootNode, { + title: 'Root', + hasRegions: true, + sizeInBytes: 16 + 123, + byteStats: { + proportionalResident: 32 + 33, + privateDirtyResident: 77, + swapped: 64 + 19, + }, + isLeafNode: false, + children: [ + { + title: 'Words', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + }, + isLeafNode: false, + children: [ + { + title: 'A-D', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'E-H', + hasRegions: true, + isLeafNode: true, + regions: [ + { + mappedFile: 'free' + } + ] + }, + { + title: 'Other', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + }, + isLeafNode: true, + regions: [ + { + mappedFile: 'W2', + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + } + } + ] + } + ] + }, + { + title: 'Digits', + hasRegions: true, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77, + swapped: 19 + }, + isLeafNode: true, + regions: [ + { + mappedFile: '__42', + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + }, + { + mappedFile: '__43', + byteStats: { + swapped: 19 + } + } + ] + }, + { + title: 'Other', + hasRegions: true, + sizeInBytes: 123, + isLeafNode: true, + regions: [ + { + mappedFile: '%invalid%', + sizeInBytes: 123 + } + ] + } + ] + }); + }); + + test('vmRegionClassificationNode_fromRegions_addAfterBuild', function() { + // Construct the root node from a list of regions. This should *not* + // trigger building the full tree (but the total sizeInBytes and byteStats + // should be updated accordingly). + const rootNode = VMRegionClassificationNode.fromRegions([ + VMRegion.fromDict({ + mappedFile: '__42', // Root/Digits. + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + }), + VMRegion.fromDict({ + mappedFile: '__43', // Root/Digits. + byteStats: { + swapped: 19 + } + }) + ], TEST_RULES); + checkTree(rootNode, { + title: 'Root', + hasRegions: true, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77, + swapped: 19 + }, + isLeafNode: false, + children: CHILDREN_NOT_BUILT_YET + }); + + // Reading the children of the root node should trigger building the full + // tree. + checkTree(rootNode, { + title: 'Root', + hasRegions: true, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77, + swapped: 19 + }, + isLeafNode: false, + children: [ + { + title: 'Words', + hasRegions: false, + isLeafNode: false, + children: [ + { + title: 'A-D', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'E-H', + hasRegions: false, + isLeafNode: true, + regions: [] + } + ] + }, + { + title: 'Digits', + hasRegions: true, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77, + swapped: 19 + }, + isLeafNode: true, + regions: [ + { + mappedFile: '__42', + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + }, + { + mappedFile: '__43', + byteStats: { + swapped: 19 + } + } + ] + } + ] + }); + + // Add more VM regions *after* the tree has been fully built. + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: 'W2', // Root/Words/Other. + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + } + })); + checkTree(rootNode, { + title: 'Root', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32 + 33, + privateDirtyResident: 77, + swapped: 19 + 64, + }, + isLeafNode: false, + children: [ + { + title: 'Words', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + }, + isLeafNode: false, + children: [ + { + title: 'A-D', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'E-H', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'Other', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + }, + isLeafNode: true, + regions: [ + { + mappedFile: 'W2', + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + } + } + ] + } + ] + }, + { + title: 'Digits', + hasRegions: true, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77, + swapped: 19 + }, + isLeafNode: true, + regions: [ + { + mappedFile: '__42', + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + }, + { + mappedFile: '__43', + byteStats: { + swapped: 19 + } + } + ] + } + ] + }); + }); + + test('vmRegionClassificationNode_fromRegions_addBeforeBuild', function() { + // Construct the root node from a list of regions and then add another + // region. This should *not* trigger building the full tree (but the total + // sizeInBytes and byteStats should be updated accordingly). + const rootNode = VMRegionClassificationNode.fromRegions([ + VMRegion.fromDict({ + mappedFile: '__42', // Root/Digits. + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + }), + VMRegion.fromDict({ + mappedFile: '__43', // Root/Digits. + byteStats: { + swapped: 19 + } + }) + ], TEST_RULES); + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: '__42', // Root/Digits. + startAddress: 2048, // Necessary to distinguish from the first region. + sizeInBytes: 1000, + byteStats: { + privateDirtyResident: 500 + } + })); + checkTree(rootNode, { + title: 'Root', + hasRegions: true, + sizeInBytes: 1000, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + 500, + swapped: 19 + }, + isLeafNode: false, + children: CHILDREN_NOT_BUILT_YET + }); + + // Reading the children of the root node should trigger building the full + // tree. + checkTree(rootNode, { + title: 'Root', + hasRegions: true, + sizeInBytes: 1000, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + 500, + swapped: 19 + }, + isLeafNode: false, + children: [ + { + title: 'Words', + hasRegions: false, + isLeafNode: false, + children: [ + { + title: 'A-D', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'E-H', + hasRegions: false, + isLeafNode: true, + regions: [] + } + ] + }, + { + title: 'Digits', + hasRegions: true, + sizeInBytes: 1000, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + 500, + swapped: 19 + }, + isLeafNode: true, + regions: [ + { + mappedFile: '__42', + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + }, + { + mappedFile: '__43', + byteStats: { + swapped: 19 + } + }, + { + mappedFile: '__42', + startAddress: 2048, + sizeInBytes: 1000, + byteStats: { + privateDirtyResident: 500 + } + } + ] + } + ] + }); + + // Add more VM regions *after* the tree has been fully built. + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: 'W2', // Root/Words/Other. + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + } + })); + checkTree(rootNode, { + title: 'Root', + hasRegions: true, + sizeInBytes: 1000 + 16, + byteStats: { + proportionalResident: 32 + 33, + privateDirtyResident: 500 + 77, + swapped: 19 + 64, + }, + isLeafNode: false, + children: [ + { + title: 'Words', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + }, + isLeafNode: false, + children: [ + { + title: 'A-D', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'E-H', + hasRegions: false, + isLeafNode: true, + regions: [] + }, + { + title: 'Other', + hasRegions: true, + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + }, + isLeafNode: true, + regions: [ + { + mappedFile: 'W2', + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + } + } + ] + } + ] + }, + { + title: 'Digits', + hasRegions: true, + sizeInBytes: 1000, + byteStats: { + proportionalResident: 33, + privateDirtyResident: 500 + 77, + swapped: 19 + }, + isLeafNode: true, + regions: [ + { + mappedFile: '__42', + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + }, + { + mappedFile: '__43', + byteStats: { + swapped: 19 + } + }, + { + mappedFile: '__42', + startAddress: 2048, + sizeInBytes: 1000, + byteStats: { + privateDirtyResident: 500 + } + } + ] + } + ] + }); + }); + + test('vmRegionClassificationNode_someRegion', function() { + const rootNode = new VMRegionClassificationNode(TEST_RULES); + + // There are no regions in the tree, so the method should always return + // false. + assert.isFalse(rootNode.someRegion(function(region) { + throw new Error('There are no regions in the tree!!!'); + })); + + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: 'W2', // Root/Words/Other. + sizeInBytes: 16, + byteStats: { + proportionalResident: 32, + swapped: 64 + } + })); + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: '__42', // Root/Digits. + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + })); + rootNode.addRegion(VMRegion.fromDict({ + mappedFile: '__43', // Root/Digits. + byteStats: { + proportionalResident: 33, + privateDirtyResident: 77 + } + })); + + function checkSomeRegion() { + // Find the order in which the regions are traversed and checked that all + // regions were visited. + const visitedRegionMappedFiles = []; + assert.isFalse(rootNode.someRegion(function(region) { + visitedRegionMappedFiles.push(region.mappedFile); + return false; + })); + assert.lengthOf(visitedRegionMappedFiles, 3); + assert.sameMembers(visitedRegionMappedFiles, ['W2', '__42', '__43']); + + // Assuming the traversal order is deterministic, we check that once the + // callback returns true, no further regions are visited. + visitedRegionMappedFiles.forEach( + function(mappedFileToMatch, index) { + const visitedRegionMappedFiles2 = []; + assert.isTrue(rootNode.someRegion(function(region) { + this.files.push(region.mappedFile); + return region.mappedFile === mappedFileToMatch; + }, { files: visitedRegionMappedFiles2 } /* opt_this */)); + assert.deepEqual(visitedRegionMappedFiles2, + visitedRegionMappedFiles.slice(0, index + 1)); + }); + } + + // Before lazy construction (single node with a flat list of regions). + checkSomeRegion(); + assert.isUndefined(rootNode.children_); + + // After lazy construction (tree of nodes with lists of regions). + assert.isDefined(rootNode.children); // Force building the tree. + assert.isDefined(rootNode.children_); + checkSomeRegion(); + }); + + test('vmRegionClassificationNode_libraryMemory', function() { + const regions = [ + VMRegion.fromDict({ + sizeInBytes: 20, + protectionFlags: VMRegion.PROTECTION_FLAG_READ, + mappedFile: '[stack:1234]', + byteStats: { + privateDirtyResident: 100, + proportionalResident: 124 + } + }), + VMRegion.fromDict({ + sizeInBytes: 500000, + protectionFlags: VMRegion.PROTECTION_FLAG_READ, + mappedFile: '/data/app/com.google.chrome/base.apk', + byteStats: { + privateCleanResident: 100000, + proportionalResident: 124000 + } + }), + VMRegion.fromDict({ + sizeInBytes: 1000, + protectionFlags: (VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE), + mappedFile: '/data/app/com.google.chrome/base.apk', + byteStats: { + privateCleanResident: 100, + proportionalResident: 124 + } + }), + VMRegion.fromDict({ + sizeInBytes: 300, + protectionFlags: (VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE), + mappedFile: '/data/app/com.google.chrome/base.apk', + byteStats: { + proportionalResident: 58, + } + }), + VMRegion.fromDict({ + sizeInBytes: 400, + protectionFlags: (VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE), + mappedFile: '/data/app/com.google.chrome/base.apk', + byteStats: { + privateCleanResident: 76, + } + }), + VMRegion.fromDict({ + sizeInBytes: 50, + protectionFlags: (VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE), + mappedFile: '/data/app/com.google.chrome/base.apk', + byteStats: { + privateCleanResident: 8, + proportionalResident: 24, + sharedCleanResident: 10, + } + })]; + const node = VMRegionClassificationNode.fromRegions(regions); + assert.strictEqual(node.sizeInBytes, 20 + 500000 + 1000 + 300 + 400 + 50); + assert.strictEqual(node.nativeLibrarySizeInBytes, + 1000 + 300 + 400 + 50); + + assert.deepEqual(node.byteStats, { + proportionalResident: 124 + 124000 + 124 + 58 + 24, + privateDirtyResident: 100, + privateCleanResident: 100000 + 100 + 76 + 8, + sharedCleanResident: 10, + nativeLibraryPrivateCleanResident: 100 + 76 + 8, + nativeLibrarySharedCleanResident: 10, + nativeLibraryProportionalResident: 124 + 58 + 24, + }); + }); + + test('vmRegionClassificationNode_javaBaseMemory', function() { + const regions = [ + VMRegion.fromDict({ + sizeInBytes: 20, + protectionFlags: VMRegion.PROTECTION_FLAG_READ, + mappedFile: '[stack:1234]', + byteStats: { + privateDirtyResident: 100, + proportionalResident: 124 + } + }), + VMRegion.fromDict({ + sizeInBytes: 500000, + protectionFlags: VMRegion.PROTECTION_FLAG_READ, + mappedFile: '/data/app/com.google.chrome/oat/arm/base.vdex', + byteStats: { + privateCleanResident: 100000, + proportionalResident: 124000 + } + }), + VMRegion.fromDict({ + sizeInBytes: 1000, + protectionFlags: (VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE), + mappedFile: '/data/app/com.google.chrome/some/other.odex', + byteStats: { + privateCleanResident: 100, + proportionalResident: 124 + } + }), + VMRegion.fromDict({ + sizeInBytes: 300, + protectionFlags: (VMRegion.PROTECTION_FLAG_READ), + mappedFile: '/data/app/com.google.chrome/oat/arm/base.odex', + byteStats: { + proportionalResident: 58, + } + }), + VMRegion.fromDict({ + sizeInBytes: 400, + protectionFlags: (VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE), + mappedFile: '/data/app/com.google.chrome/base.vdex', + byteStats: { + privateCleanResident: 76, + privateDirtyResident: 17, + } + }), + VMRegion.fromDict({ + sizeInBytes: 50, + protectionFlags: (VMRegion.PROTECTION_FLAG_READ | + VMRegion.PROTECTION_FLAG_EXECUTE), + mappedFile: '/data/app/com.google.chrome/another/path/base.odex', + byteStats: { + privateCleanResident: 8, + proportionalResident: 24, + sharedCleanResident: 10, + } + })]; + const node = VMRegionClassificationNode.fromRegions(regions); + assert.strictEqual(node.sizeInBytes, 20 + 500000 + 1000 + 300 + 400 + 50); + + assert.deepEqual(node.byteStats, { + proportionalResident: 124 + 124000 + 124 + 58 + 24, + privateDirtyResident: 100 + 17, + privateCleanResident: 100000 + 100 + 76 + 8, + sharedCleanResident: 10, + javaBasePss: 124000 + 58 + 24, + javaBaseCleanResident: 100000 + 76 + 8 + 10, + }); + }); + + test('classificationRules', function() { + checkClassificationRules('/dev/ashmem/dalvik-main space (deleted)', + ['Android', 'Java runtime', 'Spaces', 'Normal']); + checkClassificationRules('/dev/ashmem/dalvik-non moving space', + ['Android', 'Java runtime', 'Spaces', 'Non-moving']); + checkClassificationRules('/dev/ashmem/dalvik-zygote space (deleted)', + ['Android', 'Java runtime', 'Spaces', 'Zygote']); + checkClassificationRules('/dev/ashmem/dalvik-allocation stack (deleted)', + ['Android', 'Java runtime', 'Accounting']); + checkClassificationRules( + '/dev/ashmem/dalvik-allocspace main rosalloc space 1 live-bitmap 2', + ['Android', 'Java runtime', 'Accounting']); + checkClassificationRules( + '/dev/ashmem/dalvik-allocspace non moving space live-bitmap 4', + ['Android', 'Java runtime', 'Accounting']); + checkClassificationRules('/dev/ashmem/dalvik-allocspace zygote / ' + + 'non moving space live-bitmap 0 (deleted)', + ['Android', 'Java runtime', 'Accounting']); + checkClassificationRules('/dev/ashmem/dalvik-card table (deleted)', + ['Android', 'Java runtime', 'Accounting']); + checkClassificationRules('/dev/ashmem/dalvik-large live objects (deleted)', + ['Android', 'Java runtime', 'Accounting']); + checkClassificationRules('/dev/ashmem/dalvik-live stack (deleted)', + ['Android', 'Java runtime', 'Accounting']); + checkClassificationRules( + '/dev/ashmem/dalvik-mark sweep sweep array free buffer (deleted)', + ['Android', 'Java runtime', 'Accounting']); + checkClassificationRules('/dev/ashmem/dalvik-rosalloc page map (deleted)', + ['Android', 'Java runtime', 'Accounting']); + checkClassificationRules('/dev/ashmem/dalvik-indirect ref table (deleted)', + ['Android', 'Java runtime', 'Indirect Reference Table']); + checkClassificationRules('/dev/ashmem/dalvik-LinearAlloc (deleted)', + ['Android', 'Java runtime', 'Linear Alloc']); + checkClassificationRules('/dev/ashmem/dalvik-jit-code-cache (deleted)', + ['Android', 'Java runtime', 'Cache']); + checkClassificationRules('/dev/ashmem/CursorWindow (deleted)', + ['Android', 'Cursor']); + checkClassificationRules('/dev/ashmem (deleted)', ['Android', 'Ashmem']); + checkClassificationRules('/dev/ashmem/GFXStats-10082', + ['Android', 'Ashmem']); + + checkClassificationRules('[stack:23164]', ['Stack']); + checkClassificationRules('[stack]', ['Stack']); + + checkClassificationRules('[discounted tracing overhead]', ['Native heap']); + checkClassificationRules('', ['Native heap']); + checkClassificationRules('[heap]', ['Native heap']); + checkClassificationRules('[anon:libc_malloc]', ['Native heap']); + checkClassificationRules('[anon:thread signal stack]', ['Native heap']); + checkClassificationRules('/dev/ashmem/libc malloc (deleted)', + ['Native heap']); + + checkClassificationRules('/usr/lib/nvidia-340/libGL.so.331.79', + ['Files', 'so']); + checkClassificationRules('/usr/lib/x86_64-linux-gnu/libibus-1.0.so.5.0.505', + ['Files', 'so']); + checkClassificationRules('/data/data/com.google.android.apps.chrome/' + + 'app_chrome/RELRO:libchrome.so (deleted)', ['Files', 'so']); + checkClassificationRules( + '/usr/share/fonts/truetype/msttcorefonts/Times_New_Roman.ttf', + ['Files', 'ttf']); + checkClassificationRules( + '/data/app/com.google.android.apps.chrome-2/base.apk', + ['Files', 'apk']); + checkClassificationRules( + '/data/app/com.google.android.apps.chrome-2/lib/arm/libchrome.so', + ['Files', 'so']); + checkClassificationRules( + '/data/app/com.google.android.apps.chrome-2/oat/arm/base.odex', + ['Files', 'dex']); + checkClassificationRules( + '/data/dalvik-cache/arm/system@framework@boot.art', ['Files', 'art']); + checkClassificationRules( + '/data/dalvik-cache/arm/system@framework@boot.oat', ['Files', 'oat']); + + checkClassificationRules('/dev/nvidia0', ['Devices', 'GPU']); + checkClassificationRules('/dev/kgsl-3d0', ['Devices', 'GPU']); + checkClassificationRules('anon_inode:dmabuf', ['Devices', 'DMA']); + checkClassificationRules('/dev/binder', ['Devices', 'Other']); + + checkClassificationRules('/src/out/Release/chrome', ['Other']); + checkClassificationRules('/tmp/gluY4SVp (deleted)', ['Other']); + checkClassificationRules('/src/out/Release/resources.pak', ['Other']); + checkClassificationRules('[vdso]', ['Other']); + checkClassificationRules('[vsyscall]', ['Other']); + checkClassificationRules('[vectors]', ['Other']); + checkClassificationRules('[vvar]', ['Other']); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/model/x_marker_annotation.html b/chromium/third_party/catapult/tracing/tracing/model/x_marker_annotation.html new file mode 100644 index 00000000000..7eb79b1d78e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/model/x_marker_annotation.html @@ -0,0 +1,50 @@ +<!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/annotation.html"> +<link rel="import" href="/tracing/ui/annotations/x_marker_annotation_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.model', function() { + function XMarkerAnnotation(timestamp) { + tr.model.Annotation.apply(this, arguments); + + this.timestamp = timestamp; + this.strokeStyle = 'rgba(0, 0, 255, 0.5)'; + } + + XMarkerAnnotation.fromDict = function(dict) { + return new XMarkerAnnotation(dict.args.timestamp); + }; + + XMarkerAnnotation.prototype = { + __proto__: tr.model.Annotation.prototype, + + toDict() { + return { + typeName: 'xmarker', + args: { + timestamp: this.timestamp + } + }; + }, + + createView_(viewport) { + return new tr.ui.annotations.XMarkerAnnotationView(viewport, this); + } + }; + + tr.model.Annotation.register( + XMarkerAnnotation, {typeName: 'xmarker'}); + + return { + XMarkerAnnotation, + }; +}); +</script> |