| <!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/quad.html"> |
| <link rel="import" href="/tracing/base/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() { |
| var Process = tr.model.Process; |
| var Device = tr.model.Device; |
| var Kernel = tr.model.Kernel; |
| var GlobalMemoryDump = tr.model.GlobalMemoryDump; |
| var GlobalInstantEvent = tr.model.GlobalInstantEvent; |
| var FlowEvent = tr.model.FlowEvent; |
| var Alert = tr.model.Alert; |
| var 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: function(stableId) { |
| if (this.eventsByStableId_ === undefined) { |
| this.eventsByStableId_ = {}; |
| for (var event of this.getDescendantEvents()) { |
| this.eventsByStableId_[event.stableId] = event; |
| } |
| } |
| return this.eventsByStableId_[stableId]; |
| }, |
| |
| getOrCreateHelper: function(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: function*() { |
| yield * this.globalMemoryDumps; |
| yield * this.instantEvents; |
| yield * this.flowEvents; |
| yield * this.alerts; |
| yield * this.samples; |
| }, |
| |
| childEventContainers: function*() { |
| yield this.userModel; |
| yield this.device; |
| yield this.kernel; |
| yield * tr.b.dictionaryValues(this.processes); |
| }, |
| |
| /** |
| * Some objects in the model can persist their state in ModelSettings. |
| * |
| * This iterates through them. |
| */ |
| iterateAllPersistableObjects: function(callback) { |
| this.kernel.iterateAllPersistableObjects(callback); |
| for (var pid in this.processes) |
| this.processes[pid].iterateAllPersistableObjects(callback); |
| }, |
| |
| updateBounds: function() { |
| this.bounds.reset(); |
| var bounds = this.bounds; |
| for (var ec of this.childEventContainers()) { |
| ec.updateBounds(); |
| bounds.addRange(ec.bounds); |
| } |
| for (var event of this.childEvents()) |
| event.addBoundsToRange(bounds); |
| }, |
| |
| shiftWorldToZero: function() { |
| var shiftAmount = -this.bounds.min; |
| this.timestampShiftToZeroAmount_ = shiftAmount; |
| for (var ec of this.childEventContainers()) |
| ec.shiftTimestampsForward(shiftAmount); |
| |
| for (var event of this.childEvents()) |
| event.start += shiftAmount; |
| this.updateBounds(); |
| }, |
| |
| convertTimestampToModelTime: function(sourceClockDomainName, ts) { |
| if (sourceClockDomainName !== 'traceEventClock') |
| throw new Error('Only traceEventClock is supported.'); |
| return tr.b.Unit.timestampFromUs(ts) + |
| this.timestampShiftToZeroAmount_; |
| }, |
| |
| get numProcesses() { |
| var n = 0; |
| for (var 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: function(pid) { |
| return this.processes[pid]; |
| }, |
| |
| /** |
| * @return {Process} Gets a TimelineProcess for a specified pid or |
| * creates one if it does not exist. |
| */ |
| getOrCreateProcess: function(pid) { |
| if (!this.processes[pid]) |
| this.processes[pid] = new Process(this, pid); |
| return this.processes[pid]; |
| }, |
| |
| addStackFrame: function(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_: function() { |
| var categoriesDict = {}; |
| this.userModel.addCategoriesToDict(categoriesDict); |
| this.device.addCategoriesToDict(categoriesDict); |
| this.kernel.addCategoriesToDict(categoriesDict); |
| for (var pid in this.processes) |
| this.processes[pid].addCategoriesToDict(categoriesDict); |
| |
| this.categories = []; |
| for (var category in categoriesDict) |
| if (category != '') |
| this.categories.push(category); |
| }, |
| |
| getAllThreads: function() { |
| var threads = []; |
| for (var tid in this.kernel.threads) { |
| threads.push(process.threads[tid]); |
| } |
| for (var pid in this.processes) { |
| var process = this.processes[pid]; |
| for (var 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: function(opt_predicate) { |
| var processes = []; |
| for (var pid in this.processes) { |
| var 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: function() { |
| var counters = []; |
| counters.push.apply( |
| counters, tr.b.dictionaryValues(this.device.counters)); |
| counters.push.apply( |
| counters, tr.b.dictionaryValues(this.kernel.counters)); |
| for (var pid in this.processes) { |
| var process = this.processes[pid]; |
| for (var tid in process.counters) { |
| counters.push(process.counters[tid]); |
| } |
| } |
| return counters; |
| }, |
| |
| getAnnotationByGUID: function(guid) { |
| return this.annotationsByGuid_[guid]; |
| }, |
| |
| addAnnotation: function(annotation) { |
| if (!annotation.guid) |
| throw new Error('Annotation with undefined guid given'); |
| |
| this.annotationsByGuid_[annotation.guid] = annotation; |
| tr.b.dispatchSimpleEvent(this, 'annotationChange'); |
| }, |
| |
| removeAnnotation: function(annotation) { |
| this.annotationsByGuid_[annotation.guid].onRemove(); |
| delete this.annotationsByGuid_[annotation.guid]; |
| tr.b.dispatchSimpleEvent(this, 'annotationChange'); |
| }, |
| |
| getAllAnnotations: function() { |
| return tr.b.dictionaryValues(this.annotationsByGuid_); |
| }, |
| |
| addUserFriendlyCategoryDriver: function(ufcd) { |
| this.userFriendlyCategoryDrivers_.push(ufcd); |
| }, |
| |
| /** |
| * Gets the user friendly category string from an event. |
| * |
| * Returns undefined if none is known. |
| */ |
| getUserFriendlyCategoryFromEvent: function(event) { |
| for (var i = 0; i < this.userFriendlyCategoryDrivers_.length; i++) { |
| var 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: function(name) { |
| var namedThreads = []; |
| namedThreads.push.apply( |
| namedThreads, |
| this.kernel.findAllThreadsNamed(name)); |
| for (var 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: function(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; |
| |
| if (this.importOptions_.showImportWarnings) |
| console.warn(data.message); |
| |
| 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: function() { |
| // Sort the samples. |
| this.samples.sort(function(x, y) { |
| return x.start - y.start; |
| }); |
| |
| this.updateBounds(); |
| this.kernel.autoCloseOpenSlices(); |
| for (var pid in this.processes) |
| this.processes[pid].autoCloseOpenSlices(); |
| }, |
| |
| createSubSlices: function() { |
| this.kernel.createSubSlices(); |
| for (var pid in this.processes) |
| this.processes[pid].createSubSlices(); |
| }, |
| |
| preInitializeObjects: function() { |
| for (var pid in this.processes) |
| this.processes[pid].preInitializeObjects(); |
| }, |
| |
| initializeObjects: function() { |
| for (var pid in this.processes) |
| this.processes[pid].initializeObjects(); |
| }, |
| |
| pruneEmptyContainers: function() { |
| this.kernel.pruneEmptyContainers(); |
| for (var pid in this.processes) |
| this.processes[pid].pruneEmptyContainers(); |
| }, |
| |
| mergeKernelWithUserland: function() { |
| for (var pid in this.processes) |
| this.processes[pid].mergeKernelWithUserland(); |
| }, |
| |
| computeWorldBounds: function(shiftWorldToZero) { |
| this.updateBounds(); |
| this.updateCategories_(); |
| |
| if (shiftWorldToZero) |
| this.shiftWorldToZero(); |
| }, |
| |
| buildFlowEventIntervalTree: function() { |
| for (var i = 0; i < this.flowEvents.length; ++i) { |
| var flowEvent = this.flowEvents[i]; |
| this.flowIntervalTree.insert(flowEvent); |
| } |
| this.flowIntervalTree.updateHighValues(); |
| }, |
| |
| cleanupUndeletedObjects: function() { |
| for (var pid in this.processes) |
| this.processes[pid].autoDeleteObjects(this.bounds.max); |
| }, |
| |
| sortMemoryDumps: function() { |
| this.globalMemoryDumps.sort(function(x, y) { |
| return x.start - y.start; |
| }); |
| |
| for (var pid in this.processes) |
| this.processes[pid].sortMemoryDumps(); |
| }, |
| |
| finalizeMemoryGraphs: function() { |
| this.globalMemoryDumps.forEach(function(dump) { |
| dump.finalizeGraph(); |
| }); |
| }, |
| |
| buildEventIndices: function() { |
| this.modelIndices = new tr.model.ModelIndices(this); |
| }, |
| |
| sortAlerts: function() { |
| this.alerts.sort(function(x, y) { |
| return x.start - y.start; |
| }); |
| }, |
| |
| applyObjectRefPatchups: function() { |
| // Change all the fields pointing at id_refs to their real values. |
| var unresolved = []; |
| this.patchupsToApply_.forEach(function(patchup) { |
| if (patchup.pidRef in this.processes) { |
| var 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: function(old_pid_ref, new_pid_ref) { |
| this.patchupsToApply_.forEach(function(patchup) { |
| if (patchup.pidRef === old_pid_ref) |
| patchup.pidRef = new_pid_ref; |
| }); |
| }, |
| |
| /** |
| * Called by the model to join references between objects, after final model |
| * bounds have been computed. |
| */ |
| joinRefs: function() { |
| this.joinObjectRefs_(); |
| this.applyObjectRefPatchups(); |
| }, |
| |
| joinObjectRefs_: function() { |
| tr.b.iterItems(this.processes, function(pid, process) { |
| this.joinObjectRefsForProcess_(pid, process); |
| }, this); |
| }, |
| |
| joinObjectRefsForProcess_: function(pid, process) { |
| // Iterate the world, looking for id_refs |
| tr.b.iterItems(process.threads, function(tid, thread) { |
| thread.asyncSliceGroup.slices.forEach(function(item) { |
| this.searchItemForIDRefs_(pid, 'start', item); |
| }, this); |
| thread.sliceGroup.slices.forEach(function(item) { |
| this.searchItemForIDRefs_(pid, 'start', item); |
| }, this); |
| }, this); |
| process.objects.iterObjectInstances(function(instance) { |
| instance.snapshots.forEach(function(item) { |
| this.searchItemForIDRefs_(pid, 'ts', item); |
| }, this); |
| }, this); |
| }, |
| |
| searchItemForIDRefs_: function(pid, itemTimestampField, item) { |
| if (!item.args && !item.contexts) |
| return; |
| var patchupsToApply = this.patchupsToApply_; |
| |
| function handleField(object, fieldName, fieldValue) { |
| if (!fieldValue || (!fieldValue.id_ref && !fieldValue.idRef)) |
| return; |
| |
| var scope = fieldValue.scope || tr.model.OBJECT_DEFAULT_SCOPE; |
| var idRef = fieldValue.id_ref || fieldValue.idRef; |
| var scopedId = new tr.model.ScopedId(scope, idRef); |
| var pidRef = fieldValue.pid_ref || fieldValue.pidRef || pid; |
| var 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: item, |
| object: object, |
| field: fieldName, |
| pidRef: pidRef, |
| scopedId: scopedId, |
| ts: ts}); |
| } |
| function iterObjectFieldsRecursively(object) { |
| if (!(object instanceof Object)) |
| return; |
| |
| if ((object instanceof tr.model.ObjectSnapshot) || |
| (object instanceof Float32Array) || |
| (object instanceof tr.b.Quad)) |
| return; |
| |
| if (object instanceof Array) { |
| for (var i = 0; i < object.length; i++) { |
| handleField(object, i, object[i]); |
| iterObjectFieldsRecursively(object[i]); |
| } |
| return; |
| } |
| |
| for (var key in object) { |
| var value = object[key]; |
| handleField(object, key, value); |
| iterObjectFieldsRecursively(value); |
| } |
| } |
| |
| iterObjectFieldsRecursively(item.args); |
| iterObjectFieldsRecursively(item.contexts); |
| } |
| }; |
| |
| return { |
| Model: Model |
| }; |
| }); |
| </script> |