| <!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/color_scheme.html"> |
| <link rel="import" href="/tracing/base/quad.html"> |
| <link rel="import" href="/tracing/base/range.html"> |
| <link rel="import" href="/tracing/base/units/units.html"> |
| <link rel="import" href="/tracing/base/utils.html"> |
| <link rel="import" href="/tracing/extras/importer/trace_code_entry.html"> |
| <link rel="import" href="/tracing/extras/importer/trace_code_map.html"> |
| <link rel="import" href="/tracing/extras/importer/v8/codemap.html"> |
| <link rel="import" href="/tracing/importer/importer.html"> |
| <link rel="import" href="/tracing/model/attribute.html"> |
| <link rel="import" href="/tracing/model/comment_box_annotation.html"> |
| <link rel="import" href="/tracing/model/counter_series.html"> |
| <link rel="import" href="/tracing/model/flow_event.html"> |
| <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/instant_event.html"> |
| <link rel="import" href="/tracing/model/memory_allocator_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/rect_annotation.html"> |
| <link rel="import" href="/tracing/model/slice_group.html"> |
| <link rel="import" href="/tracing/model/x_marker_annotation.html"> |
| |
| <script> |
| 'use strict'; |
| |
| /** |
| * @fileoverview TraceEventImporter imports TraceEvent-formatted data |
| * into the provided model. |
| */ |
| tr.exportTo('tr.e.importer', function() { |
| var deepCopy = tr.b.deepCopy; |
| var ColorScheme = tr.b.ColorScheme; |
| |
| function getEventColor(event, opt_customName) { |
| if (event.cname) |
| return ColorScheme.getColorIdForReservedName(event.cname); |
| else if (opt_customName || event.name) { |
| return ColorScheme.getColorIdForGeneralPurposeString( |
| opt_customName || event.name); |
| } |
| } |
| |
| var timestampFromUs = tr.b.u.Units.timestampFromUs; |
| var maybeTimestampFromUs = tr.b.u.Units.maybeTimestampFromUs; |
| |
| var PRODUCER = 'producer'; |
| var CONSUMER = 'consumer'; |
| var STEP = 'step'; |
| |
| var MEMORY_DUMP_LEVELS_OF_DETAIL = [undefined, 'light', 'detailed']; |
| var GLOBAL_MEMORY_ALLOCATOR_DUMP_PREFIX = 'global/'; |
| |
| function TraceEventImporter(model, eventData) { |
| this.importPriority = 1; |
| this.model_ = model; |
| this.events_ = undefined; |
| this.sampleEvents_ = undefined; |
| this.stackFrameEvents_ = undefined; |
| this.systemTraceEvents_ = undefined; |
| this.battorData_ = undefined; |
| this.eventsWereFromString_ = false; |
| this.softwareMeasuredCpuCount_ = undefined; |
| |
| this.allAsyncEvents_ = []; |
| this.allFlowEvents_ = []; |
| this.allObjectEvents_ = []; |
| |
| this.traceEventSampleStackFramesByName_ = {}; |
| |
| this.v8ProcessCodeMaps_ = {}; |
| this.v8ProcessRootStackFrame_ = {}; |
| |
| // Dump ID -> PID -> [process memory dump events]. |
| this.allMemoryDumpEvents_ = {}; |
| |
| if (typeof(eventData) === 'string' || eventData instanceof String) { |
| eventData = eventData.trim(); |
| // If the event data begins with a [, then we know it should end with a ]. |
| // The reason we check for this is because some tracing implementations |
| // cannot guarantee that a ']' gets written to the trace file. So, we are |
| // forgiving and if this is obviously the case, we fix it up before |
| // throwing the string at JSON.parse. |
| if (eventData[0] === '[') { |
| eventData = eventData.replace(/\s*,\s*$/, ''); |
| if (eventData[eventData.length - 1] !== ']') |
| eventData = eventData + ']'; |
| } |
| |
| this.events_ = JSON.parse(eventData); |
| this.eventsWereFromString_ = true; |
| } else { |
| this.events_ = eventData; |
| } |
| |
| this.traceAnnotations_ = this.events_.traceAnnotations; |
| |
| // Some trace_event implementations put the actual trace events |
| // inside a container. E.g { ... , traceEvents: [ ] } |
| // If we see that, just pull out the trace events. |
| if (this.events_.traceEvents) { |
| var container = this.events_; |
| this.events_ = this.events_.traceEvents; |
| |
| // Some trace_event implementations put ftrace_importer traces as a |
| // huge string inside container.systemTraceEvents. If we see that, pull it |
| // out. It will be picked up by extractSubtraces later on. |
| this.systemTraceEvents_ = container.systemTraceEvents; |
| |
| // Some trace_event implementations put battor power traces as a |
| // huge string inside container.battorLogAsString. If we see that, pull |
| // it out. It will be picked up by extractSubtraces later on. |
| this.battorData_ = container.battorLogAsString; |
| |
| // Sampling data. |
| this.sampleEvents_ = container.samples; |
| this.stackFrameEvents_ = container.stackFrames; |
| |
| // Some implementations specify displayTimeUnit |
| if (container.displayTimeUnit) { |
| var unitName = container.displayTimeUnit; |
| var unit = tr.b.u.TimeDisplayModes[unitName]; |
| if (unit === undefined) { |
| throw new Error('Unit ' + unitName + ' is not supported.'); |
| } |
| this.model_.intrinsicTimeUnit = unit; |
| } |
| |
| var knownFieldNames = { |
| battorLogAsString: true, |
| samples: true, |
| stackFrames: true, |
| systemTraceEvents: true, |
| traceAnnotations: true, |
| traceEvents: true |
| }; |
| // Any other fields in the container should be treated as metadata. |
| for (var fieldName in container) { |
| if (fieldName in knownFieldNames) |
| continue; |
| this.model_.metadata.push({name: fieldName, |
| value: container[fieldName]}); |
| if (fieldName === 'metadata' && container[fieldName]['highres-ticks']) { |
| this.model_.isTimeHighResolution = |
| container[fieldName]['highres-ticks']; |
| } |
| } |
| } |
| } |
| |
| /** |
| * @return {boolean} Whether obj is a TraceEvent array. |
| */ |
| TraceEventImporter.canImport = function(eventData) { |
| // May be encoded JSON. But we dont want to parse it fully yet. |
| // Use a simple heuristic: |
| // - eventData that starts with [ are probably trace_event |
| // - eventData that starts with { are probably trace_event |
| // May be encoded JSON. Treat files that start with { as importable by us. |
| if (typeof(eventData) === 'string' || eventData instanceof String) { |
| eventData = eventData.trim(); |
| return eventData[0] === '{' || eventData[0] === '['; |
| } |
| |
| // Might just be an array of events |
| if (eventData instanceof Array && eventData.length && eventData[0].ph) |
| return true; |
| |
| // Might be an object with a traceEvents field in it. |
| if (eventData.traceEvents) { |
| if (eventData.traceEvents instanceof Array) { |
| if (eventData.traceEvents.length && eventData.traceEvents[0].ph) |
| return true; |
| if (eventData.samples.length && eventData.stackFrames !== undefined) |
| return true; |
| } |
| } |
| |
| return false; |
| }; |
| |
| TraceEventImporter.prototype = { |
| __proto__: tr.importer.Importer.prototype, |
| |
| extractSubtraces: function() { |
| var systemEventsTmp = this.systemTraceEvents_; |
| var battorDataTmp = this.battorData_; |
| this.systemTraceEvents_ = undefined; |
| this.battorData_ = undefined; |
| var subTraces = systemEventsTmp ? [systemEventsTmp] : []; |
| if (battorDataTmp) |
| subTraces.push(battorDataTmp); |
| return subTraces; |
| }, |
| |
| /** |
| * Deep copying is only needed if the trace was given to us as events. |
| */ |
| deepCopyIfNeeded_: function(obj) { |
| if (obj === undefined) |
| obj = {}; |
| if (this.eventsWereFromString_) |
| return obj; |
| return deepCopy(obj); |
| }, |
| |
| /** |
| * Always perform deep copying. |
| */ |
| deepCopyAlways_: function(obj) { |
| if (obj === undefined) |
| obj = {}; |
| return deepCopy(obj); |
| }, |
| |
| /** |
| * Helper to process an async event. |
| */ |
| processAsyncEvent: function(event) { |
| var thread = this.model_.getOrCreateProcess(event.pid). |
| getOrCreateThread(event.tid); |
| this.allAsyncEvents_.push({ |
| sequenceNumber: this.allAsyncEvents_.length, |
| event: event, |
| thread: thread |
| }); |
| }, |
| |
| /** |
| * Helper to process a flow event. |
| */ |
| processFlowEvent: function(event, opt_slice) { |
| var thread = this.model_.getOrCreateProcess(event.pid). |
| getOrCreateThread(event.tid); |
| this.allFlowEvents_.push({ |
| refGuid: tr.b.GUID.getLastGuid(), |
| sequenceNumber: this.allFlowEvents_.length, |
| event: event, |
| slice: opt_slice, // slice for events that have flow info |
| thread: thread |
| }); |
| }, |
| |
| /** |
| * Helper that creates and adds samples to a Counter object based on |
| * 'C' phase events. |
| */ |
| processCounterEvent: function(event) { |
| var ctr_name; |
| if (event.id !== undefined) |
| ctr_name = event.name + '[' + event.id + ']'; |
| else |
| ctr_name = event.name; |
| |
| var ctr = this.model_.getOrCreateProcess(event.pid) |
| .getOrCreateCounter(event.cat, ctr_name); |
| var reservedColorId = event.cname ? getEventColor(event) : undefined; |
| |
| // Initialize the counter's series fields if needed. |
| if (ctr.numSeries === 0) { |
| for (var seriesName in event.args) { |
| var colorId = reservedColorId || |
| getEventColor(event, ctr.name + '.' + seriesName); |
| ctr.addSeries(new tr.model.CounterSeries(seriesName, colorId)); |
| } |
| |
| if (ctr.numSeries === 0) { |
| this.model_.importWarning({ |
| type: 'counter_parse_error', |
| message: 'Expected counter ' + event.name + |
| ' to have at least one argument to use as a value.' |
| }); |
| |
| // Drop the counter. |
| delete ctr.parent.counters[ctr.name]; |
| return; |
| } |
| } |
| |
| var ts = timestampFromUs(event.ts); |
| ctr.series.forEach(function(series) { |
| var val = event.args[series.name] ? event.args[series.name] : 0; |
| series.addCounterSample(ts, val); |
| }); |
| }, |
| |
| processObjectEvent: function(event) { |
| var thread = this.model_.getOrCreateProcess(event.pid). |
| getOrCreateThread(event.tid); |
| this.allObjectEvents_.push({ |
| sequenceNumber: this.allObjectEvents_.length, |
| event: event, |
| thread: thread}); |
| }, |
| |
| processDurationEvent: function(event) { |
| var thread = this.model_.getOrCreateProcess(event.pid) |
| .getOrCreateThread(event.tid); |
| var ts = timestampFromUs(event.ts); |
| if (!thread.sliceGroup.isTimestampValidForBeginOrEnd(ts)) { |
| this.model_.importWarning({ |
| type: 'duration_parse_error', |
| message: 'Timestamps are moving backward.' |
| }); |
| return; |
| } |
| |
| if (event.ph === 'B') { |
| var slice = thread.sliceGroup.beginSlice( |
| event.cat, event.name, timestampFromUs(event.ts), |
| this.deepCopyIfNeeded_(event.args), |
| timestampFromUs(event.tts), event.argsStripped, |
| getEventColor(event)); |
| slice.startStackFrame = this.getStackFrameForEvent_(event); |
| } else if (event.ph === 'I' || event.ph === 'i' || event.ph === 'R') { |
| if (event.s !== undefined && event.s !== 't') |
| throw new Error('This should never happen'); |
| |
| thread.sliceGroup.beginSlice(event.cat, event.name, |
| timestampFromUs(event.ts), |
| this.deepCopyIfNeeded_(event.args), |
| timestampFromUs(event.tts), |
| event.argsStripped, |
| getEventColor(event)); |
| var slice = thread.sliceGroup.endSlice(timestampFromUs(event.ts), |
| timestampFromUs(event.tts)); |
| slice.startStackFrame = this.getStackFrameForEvent_(event); |
| slice.endStackFrame = undefined; |
| } else { |
| if (!thread.sliceGroup.openSliceCount) { |
| this.model_.importWarning({ |
| type: 'duration_parse_error', |
| message: 'E phase event without a matching B phase event.' |
| }); |
| return; |
| } |
| |
| var slice = thread.sliceGroup.endSlice(timestampFromUs(event.ts), |
| timestampFromUs(event.tts), |
| getEventColor(event)); |
| if (event.name && slice.title != event.name) { |
| this.model_.importWarning({ |
| type: 'title_match_error', |
| message: 'Titles do not match. Title is ' + |
| slice.title + ' in openSlice, and is ' + |
| event.name + ' in endSlice' |
| }); |
| } |
| slice.endStackFrame = this.getStackFrameForEvent_(event); |
| |
| this.mergeArgsInto_(slice.args, event.args, slice.title); |
| } |
| }, |
| |
| mergeArgsInto_: function(dstArgs, srcArgs, eventName) { |
| for (var arg in srcArgs) { |
| if (dstArgs[arg] !== undefined) { |
| this.model_.importWarning({ |
| type: 'arg_merge_error', |
| message: 'Different phases of ' + eventName + |
| ' provided values for argument ' + arg + '.' + |
| ' The last provided value will be used.' |
| }); |
| } |
| dstArgs[arg] = this.deepCopyIfNeeded_(srcArgs[arg]); |
| } |
| }, |
| |
| processCompleteEvent: function(event) { |
| // Preventing the overhead slices from making it into the model. This |
| // only applies to legacy traces, as the overhead traces have been |
| // removed from the chromium code. |
| if (event.cat !== undefined && |
| event.cat.indexOf('trace_event_overhead') > -1) |
| return undefined; |
| |
| var thread = this.model_.getOrCreateProcess(event.pid) |
| .getOrCreateThread(event.tid); |
| |
| if (event.flow_out) { |
| if (event.flow_in) |
| event.flowPhase = STEP; |
| else |
| event.flowPhase = PRODUCER; |
| } else if (event.flow_in) { |
| event.flowPhase = CONSUMER; |
| } |
| |
| var slice = thread.sliceGroup.pushCompleteSlice(event.cat, event.name, |
| timestampFromUs(event.ts), |
| maybeTimestampFromUs(event.dur), |
| maybeTimestampFromUs(event.tts), |
| maybeTimestampFromUs(event.tdur), |
| this.deepCopyIfNeeded_(event.args), |
| event.argsStripped, |
| getEventColor(event), |
| event.bind_id); |
| slice.startStackFrame = this.getStackFrameForEvent_(event); |
| slice.endStackFrame = this.getStackFrameForEvent_(event, true); |
| |
| return slice; |
| }, |
| |
| processMetadataEvent: function(event) { |
| // The metadata events aren't useful without args. |
| if (event.argsStripped) |
| return; |
| |
| if (event.name === 'process_name') { |
| var process = this.model_.getOrCreateProcess(event.pid); |
| process.name = event.args.name; |
| } else if (event.name === 'process_labels') { |
| var process = this.model_.getOrCreateProcess(event.pid); |
| var labels = event.args.labels.split(','); |
| for (var i = 0; i < labels.length; i++) |
| process.addLabelIfNeeded(labels[i]); |
| } else if (event.name === 'process_sort_index') { |
| var process = this.model_.getOrCreateProcess(event.pid); |
| process.sortIndex = event.args.sort_index; |
| } else if (event.name === 'thread_name') { |
| var thread = this.model_.getOrCreateProcess(event.pid). |
| getOrCreateThread(event.tid); |
| thread.name = event.args.name; |
| } else if (event.name === 'thread_sort_index') { |
| var thread = this.model_.getOrCreateProcess(event.pid). |
| getOrCreateThread(event.tid); |
| thread.sortIndex = event.args.sort_index; |
| } else if (event.name === 'num_cpus') { |
| var n = event.args.number; |
| // Not all render processes agree on the cpu count in trace_event. Some |
| // processes will report 1, while others will report the actual cpu |
| // count. To deal with this, take the max of what is reported. |
| if (this.softwareMeasuredCpuCount_ !== undefined) |
| n = Math.max(n, this.softwareMeasuredCpuCount_); |
| this.softwareMeasuredCpuCount_ = n; |
| } else if (event.name === 'stackFrames') { |
| var stackFrames = event.args.stackFrames; |
| if (stackFrames === undefined) { |
| this.model_.importWarning({ |
| type: 'metadata_parse_error', |
| message: 'No stack frames found in a \'' + event.name + |
| '\' metadata event' |
| }); |
| } else { |
| this.importStackFrames_( |
| stackFrames, 'p' + event.pid + ':', true /* addRootFrame */); |
| } |
| } else { |
| this.model_.importWarning({ |
| type: 'metadata_parse_error', |
| message: 'Unrecognized metadata name: ' + event.name |
| }); |
| } |
| }, |
| |
| processJitCodeEvent: function(event) { |
| if (this.v8ProcessCodeMaps_[event.pid] === undefined) |
| this.v8ProcessCodeMaps_[event.pid] = new tr.e.importer.TraceCodeMap(); |
| var map = this.v8ProcessCodeMaps_[event.pid]; |
| |
| var data = event.args.data; |
| if (event.name === 'JitCodeMoved') |
| map.moveEntry(data.code_start, data.new_code_start, data.code_len); |
| else |
| map.addEntry(data.code_start, data.code_len, data.name, data.script_id); |
| }, |
| |
| processInstantEvent: function(event) { |
| // V8 JIT events are logged as phase 'I' so we need to separate them out |
| // and handle specially. |
| // |
| // TODO(dsinclair): There are _a lot_ of JitCode events so I'm skipping |
| // the display for now. Can revisit later if we want to show them. |
| if (event.name === 'JitCodeAdded' || event.name === 'JitCodeMoved') { |
| this.processJitCodeEvent(event); |
| return; |
| } |
| |
| // Thread-level instant events are treated as zero-duration slices. |
| if (event.s === 't' || event.s === undefined) { |
| this.processDurationEvent(event); |
| return; |
| } |
| |
| var constructor; |
| switch (event.s) { |
| case 'g': |
| constructor = tr.model.GlobalInstantEvent; |
| break; |
| case 'p': |
| constructor = tr.model.ProcessInstantEvent; |
| break; |
| default: |
| this.model_.importWarning({ |
| type: 'instant_parse_error', |
| message: 'I phase event with unknown "s" field value.' |
| }); |
| return; |
| } |
| |
| var instantEvent = new constructor(event.cat, event.name, |
| getEventColor(event), timestampFromUs(event.ts), |
| this.deepCopyIfNeeded_(event.args)); |
| |
| switch (instantEvent.type) { |
| case tr.model.InstantEventType.GLOBAL: |
| this.model_.pushInstantEvent(instantEvent); |
| break; |
| |
| case tr.model.InstantEventType.PROCESS: |
| var process = this.model_.getOrCreateProcess(event.pid); |
| process.pushInstantEvent(instantEvent); |
| break; |
| |
| default: |
| throw new Error('Unknown instant event type: ' + event.s); |
| } |
| }, |
| |
| processV8Sample: function(event) { |
| var data = event.args.data; |
| |
| // As-per DevTools, the backend sometimes creates bogus samples. Skip it. |
| if (data.vm_state === 'js' && !data.stack.length) |
| return; |
| |
| var rootStackFrame = this.v8ProcessRootStackFrame_[event.pid]; |
| if (!rootStackFrame) { |
| rootStackFrame = new tr.model.StackFrame( |
| undefined /* parent */, 'v8-root-stack-frame' /* id */, |
| 'v8-root-stack-frame' /* title */, 0 /* colorId */); |
| this.v8ProcessRootStackFrame_[event.pid] = rootStackFrame; |
| } |
| |
| function findChildWithEntryID(stackFrame, entryID) { |
| return tr.b.findFirstInArray(stackFrame.children, function(child) { |
| return child.entryID === entryID; |
| }); |
| } |
| |
| var model = this.model_; |
| function addStackFrame(lastStackFrame, entry) { |
| var childFrame = findChildWithEntryID(lastStackFrame, entry.id); |
| if (childFrame) |
| return childFrame; |
| |
| var frame = new tr.model.StackFrame( |
| lastStackFrame, tr.b.GUID.allocate(), entry.name, |
| ColorScheme.getColorIdForGeneralPurposeString(entry.name), |
| entry.sourceInfo); |
| |
| frame.entryID = entry.id; |
| model.addStackFrame(frame); |
| return frame; |
| } |
| |
| var lastStackFrame = rootStackFrame; |
| |
| // There are several types of v8 sample events, gc, native, compiler, etc. |
| // Some of these types have stacks and some don't, we handle those two |
| // cases differently. For types that don't have any stack frames attached |
| // we synthesize one based on the type of thing that's happening so when |
| // we view all the samples we'll see something like 'external' or 'gc' |
| // as a fraction of the time spent. |
| if (data.stack.length > 0 && this.v8ProcessCodeMaps_[event.pid]) { |
| var map = this.v8ProcessCodeMaps_[event.pid]; |
| |
| // Stacks have the leaf node first, flip them around so the root |
| // comes first. |
| data.stack.reverse(); |
| |
| for (var i = 0; i < data.stack.length; i++) { |
| var entry = map.lookupEntry(data.stack[i]); |
| if (entry === undefined) { |
| entry = { |
| id: 'unknown', |
| name: 'unknown', |
| sourceInfo: undefined |
| }; |
| } |
| |
| lastStackFrame = addStackFrame(lastStackFrame, entry); |
| } |
| } else { |
| var entry = { |
| id: data.vm_state, |
| name: data.vm_state, |
| sourceInfo: undefined |
| }; |
| lastStackFrame = addStackFrame(lastStackFrame, entry); |
| } |
| |
| var thread = this.model_.getOrCreateProcess(event.pid) |
| .getOrCreateThread(event.tid); |
| |
| var sample = new tr.model.Sample( |
| undefined /* cpu */, thread, 'V8 Sample', |
| timestampFromUs(event.ts), lastStackFrame, 1 /* weight */, |
| this.deepCopyIfNeeded_(event.args)); |
| this.model_.samples.push(sample); |
| }, |
| |
| processTraceSampleEvent: function(event) { |
| if (event.name === 'V8Sample') { |
| this.processV8Sample(event); |
| return; |
| } |
| |
| var stackFrame = this.getStackFrameForEvent_(event); |
| if (stackFrame === undefined) { |
| stackFrame = this.traceEventSampleStackFramesByName_[ |
| event.name]; |
| } |
| if (stackFrame === undefined) { |
| var id = 'te-' + tr.b.GUID.allocate(); |
| stackFrame = new tr.model.StackFrame( |
| undefined, id, event.name, |
| ColorScheme.getColorIdForGeneralPurposeString(event.name)); |
| this.model_.addStackFrame(stackFrame); |
| this.traceEventSampleStackFramesByName_[event.name] = stackFrame; |
| } |
| |
| var thread = this.model_.getOrCreateProcess(event.pid) |
| .getOrCreateThread(event.tid); |
| |
| var sample = new tr.model.Sample( |
| undefined, thread, 'Trace Event Sample', |
| timestampFromUs(event.ts), stackFrame, 1, |
| this.deepCopyIfNeeded_(event.args)); |
| this.model_.samples.push(sample); |
| }, |
| |
| processMemoryDumpEvent: function(event) { |
| if (event.ph !== 'v') |
| throw new Error('Invalid memory dump event phase "' + event.ph + '".'); |
| |
| var dumpId = event.id; |
| if (dumpId === undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Memory dump event (phase \'' + event.ph + |
| '\') without a dump ID.' |
| }); |
| return; |
| } |
| |
| var pid = event.pid; |
| if (pid === undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Memory dump event (phase\'' + event.ph + '\', dump ID \'' + |
| dumpId + '\') without a PID.' |
| }); |
| return; |
| } |
| |
| // Dump ID -> PID -> [process memory dump events]. |
| var allEvents = this.allMemoryDumpEvents_; |
| |
| // PID -> [process memory dump events]. |
| var dumpIdEvents = allEvents[dumpId]; |
| if (dumpIdEvents === undefined) |
| allEvents[dumpId] = dumpIdEvents = {}; |
| |
| // [process memory dump events]. |
| var processEvents = dumpIdEvents[pid]; |
| if (processEvents === undefined) |
| dumpIdEvents[pid] = processEvents = []; |
| |
| processEvents.push(event); |
| }, |
| |
| /** |
| * Walks through the events_ list and outputs the structures discovered to |
| * model_. |
| */ |
| importEvents: function() { |
| var csr = new tr.ClockSyncRecord('ftrace_importer', 0, {}); |
| this.model_.clockSyncRecords.push(csr); |
| if (this.stackFrameEvents_) { |
| this.importStackFrames_( |
| this.stackFrameEvents_, 'g', false /* addRootFrame */); |
| } |
| |
| if (this.traceAnnotations_) |
| this.importAnnotations_(); |
| |
| var events = this.events_; |
| for (var eI = 0; eI < events.length; eI++) { |
| var event = events[eI]; |
| if (event.args === '__stripped__') { |
| event.argsStripped = true; |
| event.args = undefined; |
| } |
| |
| if (event.ph === 'B' || event.ph === 'E') { |
| this.processDurationEvent(event); |
| |
| } else if (event.ph === 'X') { |
| var slice = this.processCompleteEvent(event); |
| // TODO(yuhaoz): If Chrome supports creating other events with flow, |
| // we will need to call processFlowEvent for them also. |
| // https://github.com/catapult-project/catapult/issues/1259 |
| if (slice !== undefined && event.bind_id !== undefined) |
| this.processFlowEvent(event, slice); |
| |
| } else if (event.ph === 'b' || event.ph === 'e' || event.ph === 'n' || |
| event.ph === 'S' || event.ph === 'F' || event.ph === 'T' || |
| event.ph === 'p') { |
| this.processAsyncEvent(event); |
| |
| // Note, I is historic. The instant event marker got changed, but we |
| // want to support loading old trace files so we have both I and i. |
| } else if (event.ph === 'I' || event.ph === 'i' || event.ph === 'R') { |
| this.processInstantEvent(event); |
| |
| } else if (event.ph === 'P') { |
| this.processTraceSampleEvent(event); |
| |
| } else if (event.ph === 'C') { |
| this.processCounterEvent(event); |
| |
| } else if (event.ph === 'M') { |
| this.processMetadataEvent(event); |
| |
| } else if (event.ph === 'N' || event.ph === 'D' || event.ph === 'O') { |
| this.processObjectEvent(event); |
| |
| } else if (event.ph === 's' || event.ph === 't' || event.ph === 'f') { |
| this.processFlowEvent(event); |
| |
| } else if (event.ph === 'v') { |
| this.processMemoryDumpEvent(event); |
| |
| } else { |
| this.model_.importWarning({ |
| type: 'parse_error', |
| message: 'Unrecognized event phase: ' + |
| event.ph + ' (' + event.name + ')' |
| }); |
| } |
| } |
| |
| // Remove all the root stack frame children as they should |
| // already be added. |
| tr.b.iterItems(this.v8ProcessRootStackFrame_, function(name, frame) { |
| frame.removeAllChildren(); |
| }); |
| }, |
| |
| importStackFrames_: function(rawStackFrames, idPrefix, addRootFrame) { |
| var model = this.model_; |
| |
| var rootStackFrame; |
| if (addRootFrame) { |
| // In certain cases (heap dumps), we need to be able to distinguish |
| // between an empty and an undefined stack trace. To this end, we add |
| // an auxiliary root stack frame which is common to all stack frames |
| // in a process. An empty stack trace is then represented by setting |
| // the root stack frame as the leaf stack frame (of the relevant model |
| // object with an associated empty stack trace, e.g. HeapEntry in the |
| // case of heap dumps). |
| rootStackFrame = new tr.model.StackFrame( |
| undefined /* parentFrame */, idPrefix, undefined /* title */, |
| undefined /* colorId */); |
| model.addStackFrame(rootStackFrame); |
| } else { |
| rootStackFrame = undefined; |
| } |
| |
| for (var id in rawStackFrames) { |
| var rawStackFrame = rawStackFrames[id]; |
| var fullId = idPrefix + id; |
| var textForColor = rawStackFrame.category ? |
| rawStackFrame.category : rawStackFrame.name; |
| var stackFrame = new tr.model.StackFrame( |
| undefined /* parentFrame */, fullId, rawStackFrame.name, |
| ColorScheme.getColorIdForGeneralPurposeString(textForColor)); |
| model.addStackFrame(stackFrame); |
| } |
| |
| for (var id in rawStackFrames) { |
| var fullId = idPrefix + id; |
| var stackFrame = model.stackFrames[fullId]; |
| if (stackFrame === undefined) |
| throw new Error('Internal error'); |
| |
| var rawStackFrame = rawStackFrames[id]; |
| var parentId = rawStackFrame.parent; |
| var parentStackFrame; |
| if (parentId === undefined) { |
| parentStackFrame = rootStackFrame; |
| } else { |
| var parentFullId = idPrefix + parentId; |
| parentStackFrame = model.stackFrames[parentFullId]; |
| if (parentStackFrame === undefined) { |
| this.model_.importWarning({ |
| type: 'metadata_parse_error', |
| message: 'Missing parent frame with ID ' + parentFullId + |
| ' for stack frame \'' + stackFrame.name + '\' (ID ' + fullId + |
| ').' |
| }); |
| parentStackFrame = rootStackFrame; |
| } |
| } |
| stackFrame.parentFrame = parentStackFrame; |
| } |
| }, |
| |
| importAnnotations_: function() { |
| for (var id in this.traceAnnotations_) { |
| var annotation = tr.model.Annotation.fromDictIfPossible( |
| this.traceAnnotations_[id]); |
| if (!annotation) { |
| this.model_.importWarning({ |
| type: 'annotation_warning', |
| message: 'Unrecognized traceAnnotation typeName \"' + |
| this.traceAnnotations_[id].typeName + '\"' |
| }); |
| continue; |
| } |
| this.model_.addAnnotation(annotation); |
| } |
| }, |
| |
| /** |
| * Called by the Model after all other importers have imported their |
| * events. |
| */ |
| finalizeImport: function() { |
| if (this.softwareMeasuredCpuCount_ !== undefined) { |
| this.model_.kernel.softwareMeasuredCpuCount = |
| this.softwareMeasuredCpuCount_; |
| } |
| this.createAsyncSlices_(); |
| this.createFlowSlices_(); |
| this.createExplicitObjects_(); |
| this.createImplicitObjects_(); |
| this.createMemoryDumps_(); |
| }, |
| |
| /* Events can have one or more stack frames associated with them, but |
| * that frame might be encoded either as a stack trace of program counters, |
| * or as a direct stack frame reference. This handles either case and |
| * if found, returns the stackframe. |
| */ |
| getStackFrameForEvent_: function(event, opt_lookForEndEvent) { |
| var sf; |
| var stack; |
| if (opt_lookForEndEvent) { |
| sf = event.esf; |
| stack = event.estack; |
| } else { |
| sf = event.sf; |
| stack = event.stack; |
| } |
| if (stack !== undefined && sf !== undefined) { |
| this.model_.importWarning({ |
| type: 'stack_frame_and_stack_error', |
| message: 'Event at ' + event.ts + |
| ' cannot have both a stack and a stackframe.' |
| }); |
| return undefined; |
| } |
| |
| if (stack !== undefined) |
| return this.model_.resolveStackToStackFrame_(event.pid, stack); |
| if (sf === undefined) |
| return undefined; |
| |
| var stackFrame = this.model_.stackFrames['g' + sf]; |
| if (stackFrame === undefined) { |
| this.model_.importWarning({ |
| type: 'sample_import_error', |
| message: 'No frame for ' + sf |
| }); |
| return; |
| } |
| return stackFrame; |
| }, |
| |
| resolveStackToStackFrame_: function(pid, stack) { |
| // TODO(alph,fmeawad): Add codemap resolution code here. |
| return undefined; |
| }, |
| |
| importSampleData: function() { |
| if (!this.sampleEvents_) |
| return; |
| var m = this.model_; |
| |
| // If this is the only importer, then fake-create the threads. |
| var events = this.sampleEvents_; |
| if (this.events_.length === 0) { |
| for (var i = 0; i < events.length; i++) { |
| var event = events[i]; |
| m.getOrCreateProcess(event.tid).getOrCreateThread(event.tid); |
| } |
| } |
| |
| var threadsByTid = {}; |
| m.getAllThreads().forEach(function(t) { |
| threadsByTid[t.tid] = t; |
| }); |
| |
| for (var i = 0; i < events.length; i++) { |
| var event = events[i]; |
| var thread = threadsByTid[event.tid]; |
| if (thread === undefined) { |
| m.importWarning({ |
| type: 'sample_import_error', |
| message: 'Thread ' + events.tid + 'not found' |
| }); |
| continue; |
| } |
| |
| var cpu; |
| if (event.cpu !== undefined) |
| cpu = m.kernel.getOrCreateCpu(event.cpu); |
| |
| var stackFrame = this.getStackFrameForEvent_(event); |
| |
| var sample = new tr.model.Sample( |
| cpu, thread, |
| event.name, |
| timestampFromUs(event.ts), |
| stackFrame, |
| event.weight); |
| m.samples.push(sample); |
| } |
| }, |
| |
| /** |
| * Called by the model to join references between objects, after final model |
| * bounds have been computed. |
| */ |
| joinRefs: function() { |
| this.joinObjectRefs_(); |
| }, |
| |
| createAsyncSlices_: function() { |
| if (this.allAsyncEvents_.length === 0) |
| return; |
| |
| this.allAsyncEvents_.sort(function(x, y) { |
| var d = x.event.ts - y.event.ts; |
| if (d !== 0) |
| return d; |
| return x.sequenceNumber - y.sequenceNumber; |
| }); |
| |
| var legacyEvents = []; |
| // Group nestable async events by ID. Events with the same ID should |
| // belong to the same parent async event. |
| var nestableAsyncEventsByKey = {}; |
| for (var i = 0; i < this.allAsyncEvents_.length; i++) { |
| var asyncEventState = this.allAsyncEvents_[i]; |
| var event = asyncEventState.event; |
| if (event.ph === 'S' || event.ph === 'F' || event.ph === 'T' || |
| event.ph === 'p') { |
| legacyEvents.push(asyncEventState); |
| continue; |
| } |
| if (event.cat === undefined) { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'Nestable async events (ph: b, e, or n) require a ' + |
| 'cat parameter.' |
| }); |
| continue; |
| } |
| |
| if (event.name === undefined) { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'Nestable async events (ph: b, e, or n) require a ' + |
| 'name parameter.' |
| }); |
| continue; |
| } |
| |
| if (event.id === undefined) { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'Nestable async events (ph: b, e, or n) require an ' + |
| 'id parameter.' |
| }); |
| continue; |
| } |
| var key = event.cat + ':' + event.id; |
| if (nestableAsyncEventsByKey[key] === undefined) |
| nestableAsyncEventsByKey[key] = []; |
| nestableAsyncEventsByKey[key].push(asyncEventState); |
| } |
| // Handle legacy async events. |
| this.createLegacyAsyncSlices_(legacyEvents); |
| |
| // Parse nestable async events into AsyncSlices. |
| for (var key in nestableAsyncEventsByKey) { |
| var eventStateEntries = nestableAsyncEventsByKey[key]; |
| // Stack of enclosing BEGIN events. |
| var parentStack = []; |
| for (var i = 0; i < eventStateEntries.length; ++i) { |
| var eventStateEntry = eventStateEntries[i]; |
| // If this is the end of an event, match it to the start. |
| if (eventStateEntry.event.ph === 'e') { |
| // Walk up the parent stack to find the corresponding BEGIN for |
| // this END. |
| var parentIndex = -1; |
| for (var k = parentStack.length - 1; k >= 0; --k) { |
| if (parentStack[k].event.name === eventStateEntry.event.name) { |
| parentIndex = k; |
| break; |
| } |
| } |
| if (parentIndex === -1) { |
| // Unmatched end. |
| eventStateEntry.finished = false; |
| } else { |
| parentStack[parentIndex].end = eventStateEntry; |
| // Pop off all enclosing unmatched BEGINs util parentIndex. |
| while (parentIndex < parentStack.length) { |
| parentStack.pop(); |
| } |
| } |
| } |
| // Inherit the current parent. |
| if (parentStack.length > 0) |
| eventStateEntry.parentEntry = parentStack[parentStack.length - 1]; |
| if (eventStateEntry.event.ph === 'b') |
| parentStack.push(eventStateEntry); |
| } |
| var topLevelSlices = []; |
| for (var i = 0; i < eventStateEntries.length; ++i) { |
| var eventStateEntry = eventStateEntries[i]; |
| // Skip matched END, as its slice will be created when we |
| // encounter its corresponding BEGIN. |
| if (eventStateEntry.event.ph === 'e' && |
| eventStateEntry.finished === undefined) { |
| continue; |
| } |
| var startState = undefined; |
| var endState = undefined; |
| var sliceArgs = eventStateEntry.event.args || {}; |
| var sliceError = undefined; |
| if (eventStateEntry.event.ph === 'n') { |
| startState = eventStateEntry; |
| endState = eventStateEntry; |
| } else if (eventStateEntry.event.ph === 'b') { |
| if (eventStateEntry.end === undefined) { |
| // Unmatched BEGIN. End it when last event with this ID ends. |
| eventStateEntry.end = |
| eventStateEntries[eventStateEntries.length - 1]; |
| sliceError = |
| 'Slice has no matching END. End time has been adjusted.'; |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'Nestable async BEGIN event at ' + |
| eventStateEntry.event.ts + ' with name=' + |
| eventStateEntry.event.name + |
| ' and id=' + eventStateEntry.event.id + ' was unmatched.' |
| }); |
| } else { |
| // Include args for both END and BEGIN for a matched pair. |
| function concatenateArguments(args1, args2) { |
| if (args1.params === undefined || args2.params === undefined) |
| return tr.b.concatenateObjects(args1, args2); |
| // Make an argument object to hold the combined params. |
| var args3 = {}; |
| args3.params = tr.b.concatenateObjects(args1.params, |
| args2.params); |
| return tr.b.concatenateObjects(args1, args2, args3); |
| } |
| var endArgs = eventStateEntry.end.event.args || {}; |
| sliceArgs = concatenateArguments(sliceArgs, endArgs); |
| } |
| startState = eventStateEntry; |
| endState = eventStateEntry.end; |
| } else { |
| // Unmatched END. Start it at the first event with this ID starts. |
| sliceError = |
| 'Slice has no matching BEGIN. Start time has been adjusted.'; |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'Nestable async END event at ' + |
| eventStateEntry.event.ts + ' with name=' + |
| eventStateEntry.event.name + |
| ' and id=' + eventStateEntry.event.id + ' was unmatched.' |
| }); |
| startState = eventStateEntries[0]; |
| endState = eventStateEntry; |
| } |
| |
| var isTopLevel = (eventStateEntry.parentEntry === undefined); |
| var asyncSliceConstructor = |
| tr.model.AsyncSlice.getConstructor( |
| eventStateEntry.event.cat, |
| eventStateEntry.event.name); |
| |
| var thread_start = undefined; |
| var thread_duration = undefined; |
| if (startState.event.tts && startState.event.use_async_tts) { |
| thread_start = timestampFromUs(startState.event.tts); |
| if (endState.event.tts) { |
| var thread_end = timestampFromUs(endState.event.tts); |
| thread_duration = thread_end - thread_start; |
| } |
| } |
| |
| var slice = new asyncSliceConstructor( |
| eventStateEntry.event.cat, |
| eventStateEntry.event.name, |
| getEventColor(endState.event), |
| timestampFromUs(startState.event.ts), |
| sliceArgs, |
| timestampFromUs(endState.event.ts - startState.event.ts), |
| isTopLevel, |
| thread_start, |
| thread_duration, |
| startState.event.argsStripped); |
| |
| slice.startThread = startState.thread; |
| slice.endThread = endState.thread; |
| |
| slice.startStackFrame = this.getStackFrameForEvent_(startState.event); |
| slice.endStackFrame = this.getStackFrameForEvent_(endState.event); |
| |
| slice.id = key; |
| if (sliceError !== undefined) |
| slice.error = sliceError; |
| eventStateEntry.slice = slice; |
| // Add the slice to the topLevelSlices array if there is no parent. |
| // Otherwise, add the slice to the subSlices of its parent. |
| if (isTopLevel) { |
| topLevelSlices.push(slice); |
| } else if (eventStateEntry.parentEntry.slice !== undefined) { |
| eventStateEntry.parentEntry.slice.subSlices.push(slice); |
| } |
| } |
| for (var si = 0; si < topLevelSlices.length; si++) { |
| topLevelSlices[si].startThread.asyncSliceGroup.push( |
| topLevelSlices[si]); |
| } |
| } |
| }, |
| |
| createLegacyAsyncSlices_: function(legacyEvents) { |
| if (legacyEvents.length === 0) |
| return; |
| |
| legacyEvents.sort(function(x, y) { |
| var d = x.event.ts - y.event.ts; |
| if (d != 0) |
| return d; |
| return x.sequenceNumber - y.sequenceNumber; |
| }); |
| |
| var asyncEventStatesByNameThenID = {}; |
| |
| for (var i = 0; i < legacyEvents.length; i++) { |
| var asyncEventState = legacyEvents[i]; |
| |
| var event = asyncEventState.event; |
| var name = event.name; |
| if (name === undefined) { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'Async events (ph: S, T, p, or F) require a name ' + |
| ' parameter.' |
| }); |
| continue; |
| } |
| |
| var id = event.id; |
| if (id === undefined) { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'Async events (ph: S, T, p, or F) require an id parameter.' |
| }); |
| continue; |
| } |
| |
| // TODO(simonjam): Add a synchronous tick on the appropriate thread. |
| |
| if (event.ph === 'S') { |
| if (asyncEventStatesByNameThenID[name] === undefined) |
| asyncEventStatesByNameThenID[name] = {}; |
| if (asyncEventStatesByNameThenID[name][id]) { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'At ' + event.ts + ', a slice of the same id ' + id + |
| ' was alrady open.' |
| }); |
| continue; |
| } |
| asyncEventStatesByNameThenID[name][id] = []; |
| asyncEventStatesByNameThenID[name][id].push(asyncEventState); |
| } else { |
| if (asyncEventStatesByNameThenID[name] === undefined) { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'At ' + event.ts + ', no slice named ' + name + |
| ' was open.' |
| }); |
| continue; |
| } |
| if (asyncEventStatesByNameThenID[name][id] === undefined) { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'At ' + event.ts + ', no slice named ' + name + |
| ' with id=' + id + ' was open.' |
| }); |
| continue; |
| } |
| var events = asyncEventStatesByNameThenID[name][id]; |
| events.push(asyncEventState); |
| |
| if (event.ph === 'F') { |
| // Create a slice from start to end. |
| var asyncSliceConstructor = |
| tr.model.AsyncSlice.getConstructor( |
| events[0].event.cat, |
| name); |
| var slice = new asyncSliceConstructor( |
| events[0].event.cat, |
| name, |
| getEventColor(events[0].event), |
| timestampFromUs(events[0].event.ts), |
| tr.b.concatenateObjects(events[0].event.args, |
| events[events.length - 1].event.args), |
| timestampFromUs(event.ts - events[0].event.ts), |
| true, undefined, undefined, events[0].event.argsStripped); |
| slice.startThread = events[0].thread; |
| slice.endThread = asyncEventState.thread; |
| slice.id = id; |
| |
| var stepType = events[1].event.ph; |
| var isValid = true; |
| |
| // Create subSlices for each step. Skip the start and finish events, |
| // which are always first and last respectively. |
| for (var j = 1; j < events.length - 1; ++j) { |
| if (events[j].event.ph === 'T' || events[j].event.ph === 'p') { |
| isValid = this.assertStepTypeMatches_(stepType, events[j]); |
| if (!isValid) |
| break; |
| } |
| |
| if (events[j].event.ph === 'S') { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'At ' + event.event.ts + ', a slice named ' + |
| event.event.name + ' with id=' + event.event.id + |
| ' had a step before the start event.' |
| }); |
| continue; |
| } |
| |
| if (events[j].event.ph === 'F') { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'At ' + event.event.ts + ', a slice named ' + |
| event.event.name + ' with id=' + event.event.id + |
| ' had a step after the finish event.' |
| }); |
| continue; |
| } |
| |
| var startIndex = j + (stepType === 'T' ? 0 : -1); |
| var endIndex = startIndex + 1; |
| |
| var subName = events[j].event.name; |
| if (!events[j].event.argsStripped && |
| (events[j].event.ph === 'T' || events[j].event.ph === 'p')) |
| subName = subName + ':' + events[j].event.args.step; |
| |
| var asyncSliceConstructor = |
| tr.model.AsyncSlice.getConstructor( |
| events[0].event.cat, |
| subName); |
| var subSlice = new asyncSliceConstructor( |
| events[0].event.cat, |
| subName, |
| getEventColor(event, subName + j), |
| timestampFromUs(events[startIndex].event.ts), |
| this.deepCopyIfNeeded_(events[j].event.args), |
| timestampFromUs( |
| events[endIndex].event.ts - events[startIndex].event.ts), |
| undefined, undefined, |
| events[startIndex].event.argsStripped); |
| subSlice.startThread = events[startIndex].thread; |
| subSlice.endThread = events[endIndex].thread; |
| subSlice.id = id; |
| |
| slice.subSlices.push(subSlice); |
| } |
| |
| if (isValid) { |
| // Add |slice| to the start-thread's asyncSlices. |
| slice.startThread.asyncSliceGroup.push(slice); |
| } |
| |
| delete asyncEventStatesByNameThenID[name][id]; |
| } |
| } |
| } |
| }, |
| |
| assertStepTypeMatches_: function(stepType, event) { |
| if (stepType != event.event.ph) { |
| this.model_.importWarning({ |
| type: 'async_slice_parse_error', |
| message: 'At ' + event.event.ts + ', a slice named ' + |
| event.event.name + ' with id=' + event.event.id + |
| ' had both begin and end steps, which is not allowed.' |
| }); |
| return false; |
| } |
| return true; |
| }, |
| |
| createFlowSlices_: function() { |
| if (this.allFlowEvents_.length === 0) |
| return; |
| |
| var that = this; |
| |
| function validateFlowEvent() { |
| if (event.name === undefined) { |
| that.model_.importWarning({ |
| type: 'flow_slice_parse_error', |
| message: 'Flow events (ph: s, t or f) require a name parameter.' |
| }); |
| return false; |
| } |
| |
| // Support Flow API v1. |
| if (event.ph === 's' || event.ph === 'f' || event.ph === 't') { |
| if (event.id === undefined) { |
| that.model_.importWarning({ |
| type: 'flow_slice_parse_error', |
| message: 'Flow events (ph: s, t or f) require an id parameter.' |
| }); |
| return false; |
| } |
| return true; |
| } |
| |
| // Support Flow API v2. |
| if (event.bind_id) { |
| if (event.flow_in === undefined && event.flow_out === undefined) { |
| that.model_.importWarning({ |
| type: 'flow_slice_parse_error', |
| message: 'Flow producer or consumer require flow_in or flow_out.' |
| }); |
| return false; |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| function createFlowEvent(thread, event, opt_slice) { |
| var startSlice, flowId, flowStartTs; |
| |
| if (event.bind_id) { |
| // Support Flow API v2. |
| startSlice = opt_slice; |
| flowId = event.bind_id; |
| flowStartTs = timestampFromUs(event.ts + event.dur); |
| } else { |
| // Support Flow API v1. |
| var ts = timestampFromUs(event.ts); |
| startSlice = thread.sliceGroup.findSliceAtTs(ts); |
| if (startSlice === undefined) |
| return undefined; |
| flowId = event.id; |
| flowStartTs = ts; |
| } |
| |
| var flowEvent = new tr.model.FlowEvent( |
| event.cat, |
| flowId, |
| event.name, |
| getEventColor(event), |
| flowStartTs, |
| that.deepCopyAlways_(event.args)); |
| flowEvent.startSlice = startSlice; |
| flowEvent.startStackFrame = that.getStackFrameForEvent_(event); |
| flowEvent.endStackFrame = undefined; |
| startSlice.outFlowEvents.push(flowEvent); |
| return flowEvent; |
| } |
| |
| function finishFlowEventWith(flowEvent, thread, event, |
| refGuid, bindToParent, opt_slice) { |
| var endSlice; |
| |
| if (event.bind_id) { |
| // Support Flow API v2. |
| endSlice = opt_slice; |
| } else { |
| // Support Flow API v1. |
| var ts = timestampFromUs(event.ts); |
| if (bindToParent) { |
| endSlice = thread.sliceGroup.findSliceAtTs(ts); |
| } else { |
| endSlice = thread.sliceGroup.findNextSliceAfter(ts, refGuid); |
| } |
| if (endSlice === undefined) |
| return false; |
| } |
| |
| endSlice.inFlowEvents.push(flowEvent); |
| flowEvent.endSlice = endSlice; |
| flowEvent.duration = timestampFromUs(event.ts) - flowEvent.start; |
| flowEvent.endStackFrame = that.getStackFrameForEvent_(event); |
| that.mergeArgsInto_(flowEvent.args, event.args, flowEvent.title); |
| return true; |
| } |
| |
| function processFlowConsumer(flowIdToEvent, sliceGuidToEvent, event, |
| slice) { |
| var flowEvent = flowIdToEvent[event.bind_id]; |
| if (flowEvent === undefined) { |
| that.model_.importWarning({ |
| type: 'flow_slice_ordering_error', |
| message: 'Flow consumer ' + event.bind_id + ' does not have ' + |
| 'a flow producer'}); |
| return false; |
| } else if (flowEvent.endSlice) { |
| // One flow producer can have more than one flow consumers. |
| // In this case, create a new flow event using the flow producer. |
| var flowProducer = flowEvent.startSlice; |
| flowEvent = createFlowEvent(undefined, |
| sliceGuidToEvent[flowProducer.guid], flowProducer); |
| } |
| |
| var ok = finishFlowEventWith(flowEvent, undefined, event, |
| refGuid, undefined, slice); |
| if (ok) { |
| that.model_.flowEvents.push(flowEvent); |
| } else { |
| that.model_.importWarning({ |
| type: 'flow_slice_end_error', |
| message: 'Flow consumer ' + event.bind_id + ' does not end ' + |
| 'at an actual slice, so cannot be created.'}); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| function processFlowProducer(flowIdToEvent, flowStatus, event, slice) { |
| if (flowIdToEvent[event.bind_id] && |
| flowStatus[event.bind_id]) { |
| // Can't open the same flow again while it's still open. |
| // This is essentially the multi-producer case which we don't support |
| that.model_.importWarning({ |
| type: 'flow_slice_start_error', |
| message: 'Flow producer ' + event.bind_id + ' already seen'}); |
| return false; |
| } |
| |
| var flowEvent = createFlowEvent(undefined, event, slice); |
| if (!flowEvent) { |
| that.model_.importWarning({ |
| type: 'flow_slice_start_error', |
| message: 'Flow producer ' + event.bind_id + ' does not start' + |
| 'a flow'}); |
| return false; |
| } |
| flowIdToEvent[event.bind_id] = flowEvent; |
| |
| return; |
| } |
| |
| // Actual import. |
| this.allFlowEvents_.sort(function(x, y) { |
| var d = x.event.ts - y.event.ts; |
| if (d != 0) |
| return d; |
| return x.sequenceNumber - y.sequenceNumber; |
| }); |
| |
| var flowIdToEvent = {}; |
| var sliceGuidToEvent = {}; |
| var flowStatus = {}; // true: open; false: closed. |
| for (var i = 0; i < this.allFlowEvents_.length; ++i) { |
| var data = this.allFlowEvents_[i]; |
| var refGuid = data.refGuid; |
| var event = data.event; |
| var thread = data.thread; |
| if (!validateFlowEvent(event)) |
| continue; |
| |
| // Support for Flow API v2. |
| if (event.bind_id) { |
| var slice = data.slice; |
| sliceGuidToEvent[slice.guid] = event; |
| |
| if (event.flowPhase === PRODUCER) { |
| if (!processFlowProducer(flowIdToEvent, flowStatus, event, slice)) |
| continue; |
| flowStatus[event.bind_id] = true; // open the flow. |
| } |
| else { |
| if (!processFlowConsumer(flowIdToEvent, sliceGuidToEvent, |
| event, slice)) |
| continue; |
| flowStatus[event.bind_id] = false; // close the flow. |
| |
| if (event.flowPhase === STEP) { |
| if (!processFlowProducer(flowIdToEvent, flowStatus, |
| event, slice)) |
| continue; |
| flowStatus[event.bind_id] = true; // open the flow again. |
| } |
| } |
| continue; |
| } |
| |
| // Support for Flow API v1. |
| var flowEvent; |
| if (event.ph === 's') { |
| if (flowIdToEvent[event.id]) { |
| this.model_.importWarning({ |
| type: 'flow_slice_start_error', |
| message: 'event id ' + event.id + ' already seen when ' + |
| 'encountering start of flow event.'}); |
| continue; |
| } |
| flowEvent = createFlowEvent(thread, event); |
| if (!flowEvent) { |
| this.model_.importWarning({ |
| type: 'flow_slice_start_error', |
| message: 'event id ' + event.id + ' does not start ' + |
| 'at an actual slice, so cannot be created.'}); |
| continue; |
| } |
| flowIdToEvent[event.id] = flowEvent; |
| |
| } else if (event.ph === 't' || event.ph === 'f') { |
| flowEvent = flowIdToEvent[event.id]; |
| if (flowEvent === undefined) { |
| this.model_.importWarning({ |
| type: 'flow_slice_ordering_error', |
| message: 'Found flow phase ' + event.ph + ' for id: ' + event.id + |
| ' but no flow start found.' |
| }); |
| continue; |
| } |
| |
| var bindToParent = event.ph === 't'; |
| |
| if (event.ph === 'f') { |
| if (event.bp === undefined) { |
| // TODO(yuhaoz): In flow V2, there is no notion of binding point. |
| // Removal of binding point is tracked in |
| // https://github.com/google/trace-viewer/issues/991. |
| if (event.cat.indexOf('input') > -1) |
| bindToParent = true; |
| else if (event.cat.indexOf('ipc.flow') > -1) |
| bindToParent = true; |
| } else { |
| if (event.bp !== 'e') { |
| this.model_.importWarning({ |
| type: 'flow_slice_bind_point_error', |
| message: 'Flow event with invalid binding point (event.bp).' |
| }); |
| continue; |
| } |
| bindToParent = true; |
| } |
| } |
| |
| var ok = finishFlowEventWith(flowEvent, thread, event, |
| refGuid, bindToParent); |
| if (ok) { |
| that.model_.flowEvents.push(flowEvent); |
| } else { |
| this.model_.importWarning({ |
| type: 'flow_slice_end_error', |
| message: 'event id ' + event.id + ' does not end ' + |
| 'at an actual slice, so cannot be created.'}); |
| } |
| flowIdToEvent[event.id] = undefined; |
| |
| // If this is a step, then create another flow event. |
| if (ok && event.ph === 't') { |
| flowEvent = createFlowEvent(thread, event); |
| flowIdToEvent[event.id] = flowEvent; |
| } |
| } |
| } |
| }, |
| |
| /** |
| * This function creates objects described via the N, D, and O phase |
| * events. |
| */ |
| createExplicitObjects_: function() { |
| if (this.allObjectEvents_.length === 0) |
| return; |
| |
| function processEvent(objectEventState) { |
| var event = objectEventState.event; |
| var thread = objectEventState.thread; |
| if (event.name === undefined) { |
| this.model_.importWarning({ |
| type: 'object_parse_error', |
| message: 'While processing ' + JSON.stringify(event) + ': ' + |
| 'Object events require an name parameter.' |
| }); |
| } |
| |
| if (event.id === undefined) { |
| this.model_.importWarning({ |
| type: 'object_parse_error', |
| message: 'While processing ' + JSON.stringify(event) + ': ' + |
| 'Object events require an id parameter.' |
| }); |
| } |
| var process = thread.parent; |
| var ts = timestampFromUs(event.ts); |
| var instance; |
| if (event.ph === 'N') { |
| try { |
| instance = process.objects.idWasCreated( |
| event.id, event.cat, event.name, ts); |
| } catch (e) { |
| this.model_.importWarning({ |
| type: 'object_parse_error', |
| message: 'While processing create of ' + |
| event.id + ' at ts=' + ts + ': ' + e |
| }); |
| return; |
| } |
| } else if (event.ph === 'O') { |
| if (event.args.snapshot === undefined) { |
| this.model_.importWarning({ |
| type: 'object_parse_error', |
| message: 'While processing ' + event.id + ' at ts=' + ts + ': ' + |
| 'Snapshots must have args: {snapshot: ...}' |
| }); |
| return; |
| } |
| var snapshot; |
| try { |
| var args = this.deepCopyIfNeeded_(event.args.snapshot); |
| var cat; |
| if (args.cat) { |
| cat = args.cat; |
| delete args.cat; |
| } else { |
| cat = event.cat; |
| } |
| |
| var baseTypename; |
| if (args.base_type) { |
| baseTypename = args.base_type; |
| delete args.base_type; |
| } else { |
| baseTypename = undefined; |
| } |
| snapshot = process.objects.addSnapshot( |
| event.id, cat, event.name, ts, |
| args, baseTypename); |
| snapshot.snapshottedOnThread = thread; |
| } catch (e) { |
| this.model_.importWarning({ |
| type: 'object_parse_error', |
| message: 'While processing snapshot of ' + |
| event.id + ' at ts=' + ts + ': ' + e |
| }); |
| return; |
| } |
| instance = snapshot.objectInstance; |
| } else if (event.ph === 'D') { |
| try { |
| process.objects.idWasDeleted(event.id, event.cat, event.name, ts); |
| var instanceMap = process.objects.getOrCreateInstanceMap_(event.id); |
| instance = instanceMap.lastInstance; |
| } catch (e) { |
| this.model_.importWarning({ |
| type: 'object_parse_error', |
| message: 'While processing delete of ' + |
| event.id + ' at ts=' + ts + ': ' + e |
| }); |
| return; |
| } |
| } |
| |
| if (instance) |
| instance.colorId = getEventColor(event, instance.typeName); |
| } |
| |
| this.allObjectEvents_.sort(function(x, y) { |
| var d = x.event.ts - y.event.ts; |
| if (d != 0) |
| return d; |
| return x.sequenceNumber - y.sequenceNumber; |
| }); |
| |
| var allObjectEvents = this.allObjectEvents_; |
| for (var i = 0; i < allObjectEvents.length; i++) { |
| var objectEventState = allObjectEvents[i]; |
| try { |
| processEvent.call(this, objectEventState); |
| } catch (e) { |
| this.model_.importWarning({ |
| type: 'object_parse_error', |
| message: e.message |
| }); |
| } |
| } |
| }, |
| |
| createImplicitObjects_: function() { |
| tr.b.iterItems(this.model_.processes, function(pid, process) { |
| this.createImplicitObjectsForProcess_(process); |
| }, this); |
| }, |
| |
| // Here, we collect all the snapshots that internally contain a |
| // Javascript-level object inside their args list that has an "id" field, |
| // and turn that into a snapshot of the instance referred to by id. |
| createImplicitObjectsForProcess_: function(process) { |
| |
| function processField(referencingObject, |
| referencingObjectFieldName, |
| referencingObjectFieldValue, |
| containingSnapshot) { |
| if (!referencingObjectFieldValue) |
| return; |
| |
| if (referencingObjectFieldValue instanceof |
| tr.model.ObjectSnapshot) |
| return null; |
| if (referencingObjectFieldValue.id === undefined) |
| return; |
| |
| var implicitSnapshot = referencingObjectFieldValue; |
| |
| var rawId = implicitSnapshot.id; |
| var m = /(.+)\/(.+)/.exec(rawId); |
| if (!m) |
| throw new Error('Implicit snapshots must have names.'); |
| delete implicitSnapshot.id; |
| var name = m[1]; |
| var id = m[2]; |
| var res; |
| |
| var cat; |
| if (implicitSnapshot.cat !== undefined) |
| cat = implicitSnapshot.cat; |
| else |
| cat = containingSnapshot.objectInstance.category; |
| |
| var baseTypename; |
| if (implicitSnapshot.base_type) |
| baseTypename = implicitSnapshot.base_type; |
| else |
| baseTypename = undefined; |
| |
| try { |
| res = process.objects.addSnapshot( |
| id, cat, |
| name, containingSnapshot.ts, |
| implicitSnapshot, baseTypename); |
| } catch (e) { |
| this.model_.importWarning({ |
| type: 'object_snapshot_parse_error', |
| message: 'While processing implicit snapshot of ' + |
| rawId + ' at ts=' + containingSnapshot.ts + ': ' + e |
| }); |
| return; |
| } |
| res.objectInstance.hasImplicitSnapshots = true; |
| res.containingSnapshot = containingSnapshot; |
| res.snapshottedOnThread = containingSnapshot.snapshottedOnThread; |
| referencingObject[referencingObjectFieldName] = res; |
| if (!(res instanceof tr.model.ObjectSnapshot)) |
| throw new Error('Created object must be instanceof snapshot'); |
| return res.args; |
| } |
| |
| /** |
| * Iterates over the fields in the object, calling func for every |
| * field/value found. |
| * |
| * @return {object} If the function does not want the field's value to be |
| * iterated, return null. If iteration of the field value is desired, then |
| * return either undefined (if the field value did not change) or the new |
| * field value if it was changed. |
| */ |
| function iterObject(object, func, containingSnapshot, thisArg) { |
| if (!(object instanceof Object)) |
| return; |
| |
| if (object instanceof Array) { |
| for (var i = 0; i < object.length; i++) { |
| var res = func.call(thisArg, object, i, object[i], |
| containingSnapshot); |
| if (res === null) |
| continue; |
| if (res) |
| iterObject(res, func, containingSnapshot, thisArg); |
| else |
| iterObject(object[i], func, containingSnapshot, thisArg); |
| } |
| return; |
| } |
| |
| for (var key in object) { |
| var res = func.call(thisArg, object, key, object[key], |
| containingSnapshot); |
| if (res === null) |
| continue; |
| if (res) |
| iterObject(res, func, containingSnapshot, thisArg); |
| else |
| iterObject(object[key], func, containingSnapshot, thisArg); |
| } |
| } |
| |
| // TODO(nduca): We may need to iterate the instances in sorted order by |
| // creationTs. |
| process.objects.iterObjectInstances(function(instance) { |
| instance.snapshots.forEach(function(snapshot) { |
| if (snapshot.args.id !== undefined) |
| throw new Error('args cannot have an id field inside it'); |
| iterObject(snapshot.args, processField, snapshot, this); |
| }, this); |
| }, this); |
| }, |
| |
| createMemoryDumps_: function() { |
| for (var dumpId in this.allMemoryDumpEvents_) |
| this.createGlobalMemoryDump_(this.allMemoryDumpEvents_[dumpId], dumpId); |
| }, |
| |
| createGlobalMemoryDump_: function(dumpIdEvents, dumpId) { |
| // 1. Create a GlobalMemoryDump for the provided process memory dump |
| // the events, all of which have the same dump ID. |
| |
| // Calculate the range of the global memory dump. |
| var globalRange = new tr.b.Range(); |
| for (var pid in dumpIdEvents) { |
| var processEvents = dumpIdEvents[pid]; |
| for (var i = 0; i < processEvents.length; i++) |
| globalRange.addValue(timestampFromUs(processEvents[i].ts)); |
| } |
| if (globalRange.isEmpty) |
| throw new Error('Internal error: Global memory dump without events'); |
| |
| // Create the global memory dump. |
| var globalMemoryDump = new tr.model.GlobalMemoryDump( |
| this.model_, globalRange.min); |
| globalMemoryDump.duration = globalRange.range; |
| this.model_.globalMemoryDumps.push(globalMemoryDump); |
| |
| var globalMemoryAllocatorDumpsByFullName = {}; |
| var levelOfDetailIndices = {}; |
| var allMemoryAllocatorDumpsByGuid = {}; |
| |
| // 2. Create a ProcessMemoryDump for each PID in the provided process |
| // memory dump events. Everything except for edges between memory |
| // allocator dumps is parsed from the process memory dump trace events at |
| // this step. |
| for (var pid in dumpIdEvents) { |
| this.createProcessMemoryDump_(globalMemoryDump, |
| globalMemoryAllocatorDumpsByFullName, levelOfDetailIndices, |
| allMemoryAllocatorDumpsByGuid, dumpIdEvents[pid], pid, dumpId); |
| } |
| |
| // 3. Set the level of detail and memory allocator dumps of the |
| // GlobalMemoryDump, which come from the process memory dump trace |
| // events parsed in the prebvious step. |
| globalMemoryDump.levelOfDetail = |
| MEMORY_DUMP_LEVELS_OF_DETAIL[levelOfDetailIndices.global]; |
| |
| // Find the root allocator dumps and establish the parent links of |
| // the global memory dump. |
| globalMemoryDump.memoryAllocatorDumps = |
| this.inferMemoryAllocatorDumpTree_( |
| globalMemoryAllocatorDumpsByFullName); |
| |
| // 4. Finally, parse the edges between all memory allocator dumps within |
| // the GlobalMemoryDump. This can only be done once all memory allocator |
| // dumps have been parsed (i.e. it is necessary to iterate over the |
| // process memory dump trace events once more). |
| this.parseMemoryDumpAllocatorEdges_(allMemoryAllocatorDumpsByGuid, |
| dumpIdEvents, dumpId); |
| }, |
| |
| createProcessMemoryDump_: function(globalMemoryDump, |
| globalMemoryAllocatorDumpsByFullName, levelOfDetailIndices, |
| allMemoryAllocatorDumpsByGuid, processEvents, pid, dumpId) { |
| // Calculate the range of the process memory dump. |
| var processRange = new tr.b.Range(); |
| for (var i = 0; i < processEvents.length; i++) |
| processRange.addValue(timestampFromUs(processEvents[i].ts)); |
| if (processRange.isEmpty) |
| throw new Error('Internal error: Process memory dump without events'); |
| |
| // Create the process memory dump. |
| var process = this.model_.getOrCreateProcess(pid); |
| var processMemoryDump = new tr.model.ProcessMemoryDump( |
| globalMemoryDump, process, processRange.min); |
| processMemoryDump.duration = processRange.range; |
| process.memoryDumps.push(processMemoryDump); |
| globalMemoryDump.processMemoryDumps[pid] = processMemoryDump; |
| |
| var processMemoryAllocatorDumpsByFullName = {}; |
| |
| // Parse all process memory dump trace events for the newly created |
| // ProcessMemoryDump. |
| for (var i = 0; i < processEvents.length; i++) { |
| var processEvent = processEvents[i]; |
| |
| var dumps = processEvent.args.dumps; |
| if (dumps === undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: '\'dumps\' field not found in a process memory dump' + |
| ' event for PID=' + pid + ' and dump ID=' + dumpId + '.' |
| }); |
| continue; |
| } |
| |
| // Totals, VM regions, and heap dumps for the newly created |
| // ProcessMemoryDump should be present in at most one event, so they |
| // can be added to the ProcessMemoryDump immediately. |
| this.parseMemoryDumpTotals_(processMemoryDump, dumps, pid, dumpId); |
| this.parseMemoryDumpVmRegions_(processMemoryDump, dumps, pid, dumpId); |
| this.parseMemoryDumpHeapDumps_(processMemoryDump, dumps, pid, dumpId); |
| |
| // All process memory dump trace events for the newly created |
| // ProcessMemoryDump must be processed before level of detail and |
| // allocator dumps can be added to it. |
| this.parseMemoryDumpLevelOfDetail_(levelOfDetailIndices, dumps, pid, |
| dumpId); |
| this.parseMemoryDumpAllocatorDumps_(processMemoryDump, globalMemoryDump, |
| processMemoryAllocatorDumpsByFullName, |
| globalMemoryAllocatorDumpsByFullName, |
| allMemoryAllocatorDumpsByGuid, dumps, pid, dumpId); |
| } |
| |
| processMemoryDump.levelOfDetail = |
| MEMORY_DUMP_LEVELS_OF_DETAIL[levelOfDetailIndices.process]; |
| delete levelOfDetailIndices.process; // Reused for all process dumps. |
| |
| // Find the root allocator dumps and establish the parent links of |
| // the process memory dump. |
| processMemoryDump.memoryAllocatorDumps = |
| this.inferMemoryAllocatorDumpTree_( |
| processMemoryAllocatorDumpsByFullName); |
| }, |
| |
| parseMemoryDumpTotals_: function(processMemoryDump, dumps, pid, dumpId) { |
| var rawTotals = dumps.process_totals; |
| if (rawTotals === undefined) |
| return; |
| |
| if (processMemoryDump.totals !== undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Process totals provided multiple times for' + |
| ' process memory dump for PID=' + pid + |
| ' and dump ID=' + dumpId + '.' |
| }); |
| return; |
| } |
| |
| var totals = {}; |
| var platformSpecificTotals = undefined; |
| |
| for (var rawTotalName in rawTotals) { |
| var rawTotalValue = rawTotals[rawTotalName]; |
| if (rawTotalValue === undefined) |
| continue; |
| |
| // Total resident bytes. |
| if (rawTotalName === 'resident_set_bytes') { |
| totals.residentBytes = parseInt(rawTotalValue, 16); |
| continue; |
| } |
| |
| // Peak resident bytes. |
| if (rawTotalName === 'peak_resident_set_bytes') { |
| totals.peakResidentBytes = parseInt(rawTotalValue, 16); |
| continue; |
| } |
| if (rawTotalName === 'is_peak_rss_resetable') { |
| totals.arePeakResidentBytesResettable = !!rawTotalValue; |
| continue; |
| } |
| |
| // OS-specific totals (e.g. private resident on Mac). |
| if (platformSpecificTotals === undefined) { |
| platformSpecificTotals = {}; |
| totals.platformSpecific = platformSpecificTotals; |
| } |
| platformSpecificTotals[rawTotalName] = parseInt(rawTotalValue, 16); |
| } |
| |
| // Either both peak_resident_set_bytes and is_peak_rss_resetable should |
| // be present in the trace, or neither. |
| if (totals.peakResidentBytes === undefined && |
| totals.arePeakResidentBytesResettable !== undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Optional field peak_resident_set_bytes found' + |
| ' but is_peak_rss_resetable not found in' + |
| ' process memory dump for PID=' + pid + |
| ' and dump ID=' + dumpId + '.' |
| }); |
| } |
| if (totals.arePeakResidentBytesResettable !== undefined && |
| totals.peakResidentBytes === undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Optional field is_peak_rss_resetable found' + |
| ' but peak_resident_set_bytes not found in' + |
| ' process memory dump for PID=' + pid + |
| ' and dump ID=' + dumpId + '.' |
| }); |
| } |
| |
| processMemoryDump.totals = totals; |
| }, |
| |
| parseMemoryDumpVmRegions_: function(processMemoryDump, dumps, pid, dumpId) { |
| var rawProcessMmaps = dumps.process_mmaps; |
| if (rawProcessMmaps === undefined) |
| return; |
| |
| var rawVmRegions = rawProcessMmaps.vm_regions; |
| if (rawVmRegions === undefined) |
| return; |
| |
| if (processMemoryDump.vmRegions !== undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'VM regions provided multiple times for' + |
| ' process memory dump for PID=' + pid + |
| ' and dump ID=' + dumpId + '.' |
| }); |
| return; |
| } |
| |
| function parseByteStat(rawValue) { |
| if (rawValue === undefined) |
| return undefined; |
| return parseInt(rawValue, 16); |
| } |
| |
| var vmRegions = new Array(rawVmRegions.length); |
| |
| for (var i = 0; i < rawVmRegions.length; i++) { |
| var rawVmRegion = rawVmRegions[i]; |
| |
| // See //base/trace_event/process_memory_maps.cc in Chromium. |
| var byteStats = new tr.model.VMRegionByteStats( |
| parseByteStat(rawVmRegion.bs.pc), |
| parseByteStat(rawVmRegion.bs.pd), |
| parseByteStat(rawVmRegion.bs.sc), |
| parseByteStat(rawVmRegion.bs.sd), |
| parseByteStat(rawVmRegion.bs.pss), |
| parseByteStat(rawVmRegion.bs.sw)); |
| |
| vmRegions[i] = new tr.model.VMRegion( |
| parseInt(rawVmRegion.sa, 16), // startAddress |
| parseInt(rawVmRegion.sz, 16), // sizeInBytes |
| rawVmRegion.pf, // protectionFlags |
| rawVmRegion.mf, // mappedFile |
| byteStats); |
| } |
| |
| processMemoryDump.vmRegions = vmRegions; |
| }, |
| |
| parseMemoryDumpHeapDumps_: function(processMemoryDump, dumps, pid, dumpId) { |
| var rawHeapDumps = dumps.heaps; |
| if (rawHeapDumps === undefined) |
| return; |
| |
| if (processMemoryDump.heapDumps !== undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Heap dumps provided multiple times for' + |
| ' process memory dump for PID=' + pid + |
| ' and dump ID=' + dumpId + '.' |
| }); |
| return; |
| } |
| |
| var model = this.model_; |
| var idPrefix = 'p' + pid + ':'; |
| var heapDumps = {}; |
| |
| for (var allocatorName in rawHeapDumps) { |
| var entries = rawHeapDumps[allocatorName].entries; |
| if (entries === undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Missing heap entries in a ' + allocatorName + |
| ' heap dump for PID=' + pid + ' and dump ID=' + dumpId + '.' |
| }); |
| continue; |
| } |
| |
| var heapDump = new tr.model.HeapDump(processMemoryDump, allocatorName); |
| |
| for (var i = 0; i < entries.length; i++) { |
| var entry = entries[i]; |
| var type = entry.type; |
| if (type !== undefined) { |
| // Heap dump entries with specified object type ID need to be |
| // ignored for the time being because Chrome doesn't dump the |
| // mapping from object type IDs to object type names yet. See |
| // crbug.com/524631. |
| continue; |
| } |
| var size = parseInt(entry.size, 16); |
| var leafStackFrameIndex = entry.bt; |
| var leafStackFrame; |
| if (leafStackFrameIndex === undefined) { |
| leafStackFrame = undefined; |
| } else { |
| var leafStackFrameId = idPrefix + leafStackFrameIndex; |
| leafStackFrame = model.stackFrames[leafStackFrameId]; |
| if (leafStackFrame === undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Missing leaf stack frame (ID ' + leafStackFrameId + |
| ') of heap entry ' + i + ' (size ' + size + ') in a ' + |
| allocatorName + ' heap dump for PID=' + pid + '.' |
| }); |
| continue; |
| } |
| } |
| heapDump.addEntry(leafStackFrame, size); |
| } |
| heapDumps[allocatorName] = heapDump; |
| } |
| |
| processMemoryDump.heapDumps = heapDumps; |
| }, |
| |
| parseMemoryDumpLevelOfDetail_: function(levelOfDetailIndices, dumps, pid, |
| dumpId) { |
| var rawLevelOfDetail = dumps.level_of_detail; |
| |
| // Determine the index of the level of detail of the dump. |
| var index = MEMORY_DUMP_LEVELS_OF_DETAIL.indexOf(rawLevelOfDetail); |
| if (index < 0) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'unknown level of detail \'' + rawLevelOfDetail + |
| '\' of process memory dump for PID=' + pid + |
| ' and dump ID=' + dumpId + '.' |
| }); |
| return; |
| } |
| |
| // If the process memory dump events have different levels of detail |
| // (for the particular process and/or globally), show a warning and use |
| // the highest level. |
| |
| if (levelOfDetailIndices.process === undefined) { |
| levelOfDetailIndices.process = index; |
| } else if (index !== levelOfDetailIndices.process) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'diffent levels of detail provided for process memory' + |
| ' dump for PID=' + pid + ' (dump ID=' + dumpId + ').' |
| }); |
| levelOfDetailIndices.process = |
| Math.max(levelOfDetailIndices.process, index); |
| } |
| |
| if (levelOfDetailIndices.global === undefined) { |
| levelOfDetailIndices.global = index; |
| } else if (index !== levelOfDetailIndices.global) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'diffent levels of detail provided for global memory' + |
| ' dump (dump ID=' + dumpId + ').' |
| }); |
| levelOfDetailIndices.global = |
| Math.max(levelOfDetailIndices.global, index); |
| } |
| }, |
| |
| parseMemoryDumpAllocatorDumps_: function(processMemoryDump, |
| globalMemoryDump, processMemoryAllocatorDumpsByFullName, |
| globalMemoryAllocatorDumpsByFullName, allMemoryAllocatorDumpsByGuid, |
| dumps, pid, dumpId) { |
| var rawAllocatorDumps = dumps.allocators; |
| if (rawAllocatorDumps === undefined) |
| return; |
| |
| // Construct the MemoryAllocatorDump objects without parent links |
| // and add them to the processMemoryAllocatorDumpsByName and |
| // globalMemoryAllocatorDumpsByName indices appropriately. |
| for (var fullName in rawAllocatorDumps) { |
| var rawAllocatorDump = rawAllocatorDumps[fullName]; |
| |
| // Every memory allocator dump should have a GUID. If not, then |
| // it cannot be associated with any edges. |
| var guid = rawAllocatorDump.guid; |
| if (guid === undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Memory allocator dump ' + fullName + ' for PID=' + pid + |
| ' and dump ID=' + dumpId + ' does not have a GUID.' |
| }); |
| } |
| |
| // Determine if this is a global memory allocator dump (check if |
| // it's prefixed with 'global/'). |
| var containerMemoryDump; |
| var dstIndex; |
| if (fullName.startsWith(GLOBAL_MEMORY_ALLOCATOR_DUMP_PREFIX)) { |
| // Global memory allocator dump. |
| fullName = fullName.substring( |
| GLOBAL_MEMORY_ALLOCATOR_DUMP_PREFIX.length); |
| containerMemoryDump = globalMemoryDump; |
| dstIndex = globalMemoryAllocatorDumpsByFullName; |
| } else { |
| // Process memory allocator dump. |
| containerMemoryDump = processMemoryDump; |
| dstIndex = processMemoryAllocatorDumpsByFullName; |
| } |
| |
| // Construct or retrieve a memory allocator dump with the provided |
| // GUID. |
| var allocatorDump = allMemoryAllocatorDumpsByGuid[guid]; |
| if (allocatorDump === undefined) { |
| if (fullName in dstIndex) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Multiple GUIDs provided for' + |
| ' memory allocator dump ' + fullName + ': ' + |
| dstIndex[fullName].guid + ', ' + guid + ' (ignored) for' + |
| ' PID=' + pid + ' and dump ID=' + dumpId + '.' |
| }); |
| continue; |
| } |
| allocatorDump = new tr.model.MemoryAllocatorDump( |
| containerMemoryDump, fullName, guid); |
| dstIndex[fullName] = allocatorDump; |
| if (guid !== undefined) |
| allMemoryAllocatorDumpsByGuid[guid] = allocatorDump; |
| } else { |
| // A memory allocator dump with this GUID has already been |
| // dumped (so we will only add new attributes). Check that it |
| // belonged to the same process or was also global. |
| if (allocatorDump.containerMemoryDump !== containerMemoryDump) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Memory allocator dump ' + fullName + |
| ' (GUID=' + guid + ') for PID=' + pid + ' and dump ID=' + |
| dumpId + ' dumped in different contexts.' |
| }); |
| continue; |
| } |
| // Check that the names of the memory allocator dumps match. |
| if (allocatorDump.fullName !== fullName) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Memory allocator dump with GUID=' + guid + ' for PID=' + |
| pid + ' and dump ID=' + dumpId + ' has multiple names: ' + |
| allocatorDump.fullName + ', ' + fullName + ' (ignored).' |
| }); |
| continue; |
| } |
| } |
| |
| // Add all new attributes to the memory allocator dump. |
| var attributes = rawAllocatorDump.attrs; |
| if (attributes === undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Memory allocator dump ' + fullName + ' (GUID=' + guid + |
| ') for PID=' + pid + ' and dump ID=' + dumpId + |
| ' does not have attributes.' |
| }); |
| attributes = {}; |
| } |
| |
| for (var attrName in attributes) { |
| if (attrName in allocatorDump.attributes) { |
| // Skip existing attributes of the memory allocator dump. |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Multiple values provided for attribute ' + attrName + |
| ' of memory allocator dump ' + fullName + ' (GUID=' + guid + |
| ') for PID=' + pid + ' and dump ID=' + dumpId + '.' |
| }); |
| continue; |
| } |
| |
| var attrArgs = attributes[attrName]; |
| var attrValue = tr.model.Attribute.fromDictIfPossible(attrArgs); |
| allocatorDump.addAttribute(attrName, attrValue); |
| } |
| } |
| }, |
| |
| inferMemoryAllocatorDumpTree_: function(memoryAllocatorDumpsByFullName) { |
| var rootAllocatorDumps = []; |
| |
| var fullNames = Object.keys(memoryAllocatorDumpsByFullName); |
| fullNames.sort(); |
| for (var i = 0; i < fullNames.length; i++) { |
| var fullName = fullNames[i]; |
| var allocatorDump = memoryAllocatorDumpsByFullName[fullName]; |
| |
| // This is a loop because we might need to build implicit |
| // ancestors in case they were not present in the trace. |
| while (true) { |
| var lastSlashIndex = fullName.lastIndexOf('/'); |
| if (lastSlashIndex === -1) { |
| // If the dump is a root, add it to the top-level |
| // rootAllocatorDumps list. |
| rootAllocatorDumps.push(allocatorDump); |
| break; |
| } |
| |
| // If the dump is not a root, find its parent. |
| var parentFullName = fullName.substring(0, lastSlashIndex); |
| var parentAllocatorDump = |
| memoryAllocatorDumpsByFullName[parentFullName]; |
| |
| // If the parent dump does not exist yet, we build an implicit |
| // one and continue up the ancestor chain. |
| var parentAlreadyExisted = true; |
| if (parentAllocatorDump === undefined) { |
| parentAlreadyExisted = false; |
| parentAllocatorDump = new tr.model.MemoryAllocatorDump( |
| allocatorDump.containerMemoryDump, parentFullName); |
| memoryAllocatorDumpsByFullName[parentFullName] = |
| parentAllocatorDump; |
| } |
| |
| // Setup the parent <-> children relationships |
| allocatorDump.parent = parentAllocatorDump; |
| parentAllocatorDump.children.push(allocatorDump); |
| |
| // If the parent already existed, then its ancestors were/will be |
| // constructed in another iteration of the forEach loop. |
| if (parentAlreadyExisted) |
| break; |
| |
| fullName = parentFullName; |
| allocatorDump = parentAllocatorDump; |
| } |
| } |
| |
| return rootAllocatorDumps; |
| }, |
| |
| parseMemoryDumpAllocatorEdges_: function(allMemoryAllocatorDumpsByGuid, |
| dumpIdEvents, dumpId) { |
| for (var pid in dumpIdEvents) { |
| var processEvents = dumpIdEvents[pid]; |
| |
| for (var i = 0; i < processEvents.length; i++) { |
| var processEvent = processEvents[i]; |
| |
| var dumps = processEvent.args.dumps; |
| if (dumps === undefined) |
| continue; |
| |
| var rawEdges = dumps.allocators_graph; |
| if (rawEdges === undefined) |
| continue; |
| |
| for (var j = 0; j < rawEdges.length; j++) { |
| var rawEdge = rawEdges[j]; |
| |
| var sourceGuid = rawEdge.source; |
| var sourceDump = allMemoryAllocatorDumpsByGuid[sourceGuid]; |
| if (sourceDump === undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Edge for PID=' + pid + ' and dump ID=' + dumpId + |
| ' is missing source memory allocator dump (GUID=' + |
| sourceGuid + ').' |
| }); |
| continue; |
| } |
| |
| var targetGuid = rawEdge.target; |
| var targetDump = allMemoryAllocatorDumpsByGuid[targetGuid]; |
| if (targetDump === undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Edge for PID=' + pid + ' and dump ID=' + dumpId + |
| ' is missing target memory allocator dump (GUID=' + |
| targetGuid + ').' |
| }); |
| continue; |
| } |
| |
| var importance = rawEdge.importance; |
| var edge = new tr.model.MemoryAllocatorDumpLink( |
| sourceDump, targetDump, importance); |
| |
| switch (rawEdge.type) { |
| case 'ownership': |
| if (sourceDump.owns !== undefined) { |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Memory allocator dump ' + sourceDump.fullName + |
| ' (GUID=' + sourceGuid + ') already owns a memory' + |
| ' allocator dump (' + |
| sourceDump.owns.target.fullName + ').' |
| }); |
| } else { |
| sourceDump.owns = edge; |
| targetDump.ownedBy.push(edge); |
| } |
| break; |
| |
| case 'retention': |
| sourceDump.retains.push(edge); |
| targetDump.retainedBy.push(edge); |
| break; |
| |
| default: |
| this.model_.importWarning({ |
| type: 'memory_dump_parse_error', |
| message: 'Invalid edge type: ' + rawEdge.type + |
| ' (PID=' + pid + ', dump ID=' + dumpId + |
| ', source=' + sourceGuid + ', target=' + targetGuid + |
| ', importance=' + importance + ').' |
| }); |
| } |
| } |
| } |
| } |
| }, |
| |
| joinObjectRefs_: function() { |
| tr.b.iterItems(this.model_.processes, function(pid, process) { |
| this.joinObjectRefsForProcess_(process); |
| }, this); |
| }, |
| |
| joinObjectRefsForProcess_: function(process) { |
| // Iterate the world, looking for id_refs |
| var patchupsToApply = []; |
| tr.b.iterItems(process.threads, function(tid, thread) { |
| thread.asyncSliceGroup.slices.forEach(function(item) { |
| this.searchItemForIDRefs_( |
| patchupsToApply, process.objects, 'start', item); |
| }, this); |
| thread.sliceGroup.slices.forEach(function(item) { |
| this.searchItemForIDRefs_( |
| patchupsToApply, process.objects, 'start', item); |
| }, this); |
| }, this); |
| process.objects.iterObjectInstances(function(instance) { |
| instance.snapshots.forEach(function(item) { |
| this.searchItemForIDRefs_( |
| patchupsToApply, process.objects, 'ts', item); |
| }, this); |
| }, this); |
| |
| // Change all the fields pointing at id_refs to their real values. |
| patchupsToApply.forEach(function(patchup) { |
| patchup.object[patchup.field] = patchup.value; |
| }); |
| }, |
| |
| searchItemForIDRefs_: function(patchupsToApply, objectCollection, |
| itemTimestampField, item) { |
| if (!item.args) |
| throw new Error('item is missing its args'); |
| |
| function handleField(object, fieldName, fieldValue) { |
| if (!fieldValue || (!fieldValue.id_ref && !fieldValue.idRef)) |
| return; |
| |
| var id = fieldValue.id_ref || fieldValue.idRef; |
| var ts = item[itemTimestampField]; |
| var snapshot = objectCollection.getSnapshotAt(id, ts); |
| if (!snapshot) |
| return; |
| |
| // 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({object: object, |
| field: fieldName, |
| value: snapshot}); |
| } |
| 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); |
| } |
| }; |
| |
| tr.importer.Importer.register(TraceEventImporter); |
| |
| return { |
| TraceEventImporter: TraceEventImporter |
| }; |
| }); |
| </script> |