blob: 0487e8f80f439d1fb5a92b5bb4888a6bea57d772 [file] [log] [blame]
<!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/base64.html">
<link rel="import" href="/tracing/base/color_scheme.html">
<link rel="import" href="/tracing/base/range.html">
<link rel="import" href="/tracing/base/unit.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/context_processor.html">
<link rel="import" href="/tracing/importer/importer.html">
<link rel="import" href="/tracing/model/comment_box_annotation.html">
<link rel="import" href="/tracing/model/constants.html">
<link rel="import" href="/tracing/model/container_memory_dump.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/scoped_id.html">
<link rel="import" href="/tracing/model/slice_group.html">
<link rel="import" href="/tracing/model/vm_region.html">
<link rel="import" href="/tracing/model/x_marker_annotation.html">
<link rel="import" href="/tracing/value/numeric.html">
<script>
'use strict';
/**
* @fileoverview TraceEventImporter imports TraceEvent-formatted data
* into the provided model.
*/
tr.exportTo('tr.e.importer', function() {
var Base64 = tr.b.Base64;
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 PRODUCER = 'producer';
var CONSUMER = 'consumer';
var STEP = 'step';
var BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;
var LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
var DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
var MEMORY_DUMP_LEVEL_OF_DETAIL_ORDER = [undefined, BACKGROUND, LIGHT,
DETAILED];
var GLOBAL_MEMORY_ALLOCATOR_DUMP_PREFIX = 'global/';
var ASYNC_CLOCK_SYNC_EVENT_TITLE_PREFIX = 'ClockSyncEvent.';
// Map from raw memory dump byte stat names to model byte stat names. See
// //base/trace_event/process_memory_maps.cc in Chromium.
var BYTE_STAT_NAME_MAP = {
'pc': 'privateCleanResident',
'pd': 'privateDirtyResident',
'sc': 'sharedCleanResident',
'sd': 'sharedDirtyResident',
'pss': 'proportionalResident',
'sw': 'swapped'
};
// See tr.model.MemoryAllocatorDump 'weak' field and
// base::trace_event::MemoryAllocatorDump::Flags::WEAK in the Chromium
// codebase.
var WEAK_MEMORY_ALLOCATOR_DUMP_FLAG = 1 << 0;
// Object type name patterns for various compilers.
var OBJECT_TYPE_NAME_PATTERNS = [
{
// Clang.
prefix: 'const char *WTF::getStringWithTypeName() [T = ',
suffix: ']'
},
{
// GCC.
prefix: 'const char* WTF::getStringWithTypeName() [with T = ',
suffix: ']'
},
{
// Microsoft Visual C++
prefix: 'const char *__cdecl WTF::getStringWithTypeName<',
suffix: '>(void)'
}
];
// The list of fields on the trace that are known to contain subtraces.
var SUBTRACE_FIELDS = new Set([
'powerTraceAsString',
'systemTraceEvents',
]);
// The complete list of fields on the trace that should not be treated as
// trace metadata.
var NON_METADATA_FIELDS = new Set([
'samples',
'stackFrames',
'traceAnnotations',
'traceEvents'
]);
// TODO(charliea): Replace this with the spread (...) operator in literal
// above once v8 is updated to a sufficiently recent version (>M45).
for (var subtraceField in SUBTRACE_FIELDS)
NON_METADATA_FIELDS.add(subtraceField);
function TraceEventImporter(model, eventData) {
this.importPriority = 1;
this.model_ = model;
this.events_ = undefined;
this.sampleEvents_ = undefined;
this.stackFrameEvents_ = undefined;
this.subtraces_ = [];
this.eventsWereFromString_ = false;
this.softwareMeasuredCpuCount_ = undefined;
this.allAsyncEvents_ = [];
this.allFlowEvents_ = [];
this.allObjectEvents_ = [];
this.contextProcessorPerThread = {};
this.traceEventSampleStackFramesByName_ = {};
this.v8ProcessCodeMaps_ = {};
this.v8ProcessRootStackFrame_ = {};
this.v8SamplingData_ = [];
// For tracking async events that is used to create back-compat clock sync
// event.
this.asyncClockSyncStart_ = undefined;
this.asyncClockSyncFinish_ = undefined;
// Dump ID -> PID -> [process memory dump events].
this.allMemoryDumpEvents_ = {};
// PID -> Object type ID -> Object type name.
this.objectTypeNameMap_ = {};
// For old Chrome traces with no clock domain metadata, just use a
// placeholder clock domain.
this.clockDomainId_ = tr.model.ClockDomainId.UNKNOWN_CHROME_LEGACY;
// A function able to transform timestamps in |clockDomainId| to timestamps
// in the model clock domain.
this.toModelTime_ = undefined;
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 authors store subtraces as specific properties of the trace.
for (var subtraceField of SUBTRACE_FIELDS)
if (container[subtraceField])
this.subtraces_.push(container[subtraceField]);
// 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.TimeDisplayModes[unitName];
if (unit === undefined) {
throw new Error('Unit ' + unitName + ' is not supported.');
}
this.model_.intrinsicTimeUnit = unit;
}
// Any other fields in the container should be treated as metadata.
for (var fieldName in container) {
if (NON_METADATA_FIELDS.has(fieldName))
continue;
this.model_.metadata.push(
{ name: fieldName, value: container[fieldName] });
if (fieldName === 'metadata') {
var metadata = container[fieldName];
if (metadata['highres-ticks'])
this.model_.isTimeHighResolution = metadata['highres-ticks'];
if (metadata['clock-domain'])
this.clockDomainId_ = metadata['clock-domain'];
}
}
}
}
/**
* @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,
get importerName() {
return 'TraceEventImporter';
},
extractSubtraces: function() {
// Because subtraces can be quite large, we need to make sure that we
// don't hold a reference to the memory.
var subtraces = this.subtraces_;
this.subtraces_ = [];
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.getLastSimpleGuid(),
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 = this.toModelTimeFromUs_(event.ts);
ctr.series.forEach(function(series) {
var val = event.args[series.name] ? event.args[series.name] : 0;
series.addCounterSample(ts, val);
});
},
scopedIdForEvent_: function(event) {
return new tr.model.ScopedId(
event.scope || tr.model.OBJECT_DEFAULT_SCOPE, event.id);
},
processObjectEvent: function(event) {
var thread = this.model_.getOrCreateProcess(event.pid).
getOrCreateThread(event.tid);
this.allObjectEvents_.push({
sequenceNumber: this.allObjectEvents_.length,
event: event,
thread: thread});
if (thread.guid in this.contextProcessorPerThread) {
var processor = this.contextProcessorPerThread[thread.guid];
var scopedId = this.scopedIdForEvent_(event);
if (event.ph === 'D')
processor.destroyContext(scopedId);
// The context processor maintains a cache of unique context objects and
// active context sets to reduce memory usage. If an object is modified,
// we should invalidate this cache, because otherwise context sets from
// before and after the modification may erroneously point to the same
// context snapshot (as both are the same set/object instances).
processor.invalidateContextCacheForSnapshot(scopedId);
}
},
processContextEvent: function(event) {
var thread = this.model_.getOrCreateProcess(event.pid).
getOrCreateThread(event.tid);
if (!(thread.guid in this.contextProcessorPerThread)) {
this.contextProcessorPerThread[thread.guid] =
new tr.importer.ContextProcessor(this.model_);
}
var scopedId = this.scopedIdForEvent_(event);
var contextType = event.name;
var processor = this.contextProcessorPerThread[thread.guid];
if (event.ph === '(') {
processor.enterContext(contextType, scopedId);
} else if (event.ph === ')') {
processor.leaveContext(contextType, scopedId);
} else {
this.model_.importWarning({
type: 'unknown_context_phase',
message: 'Unknown context event phase: ' + event.ph + '.'
});
}
},
setContextsFromThread_: function(thread, slice) {
if (thread.guid in this.contextProcessorPerThread) {
slice.contexts =
this.contextProcessorPerThread[thread.guid].activeContexts;
}
},
processDurationEvent: function(event) {
var thread = this.model_.getOrCreateProcess(event.pid)
.getOrCreateThread(event.tid);
var ts = this.toModelTimeFromUs_(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, this.toModelTimeFromUs_(event.ts),
this.deepCopyIfNeeded_(event.args),
this.toModelTimeFromUs_(event.tts), event.argsStripped,
getEventColor(event));
slice.startStackFrame = this.getStackFrameForEvent_(event);
this.setContextsFromThread_(thread, slice);
} 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,
this.toModelTimeFromUs_(event.ts),
this.deepCopyIfNeeded_(event.args),
this.toModelTimeFromUs_(event.tts),
event.argsStripped,
getEventColor(event));
var slice = thread.sliceGroup.endSlice(
this.toModelTimeFromUs_(event.ts),
this.toModelTimeFromUs_(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(
this.toModelTimeFromUs_(event.ts),
this.toModelTimeFromUs_(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,
this.toModelTimeFromUs_(event.ts),
this.maybeToModelTimeFromUs_(event.dur),
this.maybeToModelTimeFromUs_(event.tts),
this.maybeToModelTimeFromUs_(event.tdur),
this.deepCopyIfNeeded_(event.args),
event.argsStripped,
getEventColor(event),
event.bind_id);
slice.startStackFrame = this.getStackFrameForEvent_(event);
slice.endStackFrame = this.getStackFrameForEvent_(event, true);
this.setContextsFromThread_(thread, slice);
return slice;
},
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;
// 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.
// Handle JitCodeMoved and JitCodeAdded event.
if (event.name === 'JitCodeMoved')
map.moveEntry(data.code_start, data.new_code_start, data.code_len);
else // event.name === 'JitCodeAdded'
map.addEntry(data.code_start, data.code_len, data.name, data.script_id);
},
processMetadataEvent: function(event) {
// V8 JIT events are currently logged as phase 'M' so we need to
// separate them out and handle specially.
if (event.name === 'JitCodeAdded' || event.name === 'JitCodeMoved') {
this.v8SamplingData_.push(event);
return;
}
// 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 + ':');
}
} else if (event.name === 'typeNames') {
var objectTypeNameMap = event.args.typeNames;
if (objectTypeNameMap === undefined) {
this.model_.importWarning({
type: 'metadata_parse_error',
message: 'No mapping from object type IDs to names found in a \'' +
event.name + '\' metadata event'
});
} else {
this.importObjectTypeNameMap_(objectTypeNameMap, event.pid);
}
} else if (event.name === 'TraceConfig') {
this.model_.metadata.push(
{name: 'TraceConfig', value: event.args.value});
} else {
this.model_.importWarning({
type: 'metadata_parse_error',
message: 'Unrecognized metadata name: ' + event.name
});
}
},
processInstantEvent: function(event) {
// V8 JIT events were logged as phase 'I' in the old format,
// so we need to separate them out and handle specially.
if (event.name === 'JitCodeAdded' || event.name === 'JitCodeMoved') {
this.v8SamplingData_.push(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), this.toModelTimeFromUs_(event.ts),
this.deepCopyIfNeeded_(event.args));
switch (instantEvent.type) {
case tr.model.InstantEventType.GLOBAL:
this.model_.instantEvents.push(instantEvent);
break;
case tr.model.InstantEventType.PROCESS:
var process = this.model_.getOrCreateProcess(event.pid);
process.instantEvents.push(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.allocateSimple(), 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',
this.toModelTimeFromUs_(event.ts), lastStackFrame, 1 /* weight */,
this.deepCopyIfNeeded_(event.args));
this.model_.samples.push(sample);
},
processTraceSampleEvent: function(event) {
if (event.name === 'V8Sample') {
this.v8SamplingData_.push(event);
return;
}
var stackFrame = this.getStackFrameForEvent_(event);
if (stackFrame === undefined) {
stackFrame = this.traceEventSampleStackFramesByName_[
event.name];
}
if (stackFrame === undefined) {
var id = 'te-' + tr.b.GUID.allocateSimple();
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',
this.toModelTimeFromUs_(event.ts), stackFrame, 1,
this.deepCopyIfNeeded_(event.args));
this.setContextsFromThread_(thread, sample);
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);
},
processClockSyncEvent: function(event) {
if (event.ph !== 'c')
throw new Error('Invalid clock sync event phase "' + event.ph + '".');
var syncId = event.args.sync_id;
if (syncId === undefined) {
this.model_.importWarning({
type: 'clock_sync_parse_error',
message: 'Clock sync at time ' + event.ts + ' without an ID.'
});
return;
}
if (event.args && event.args.issue_ts !== undefined) {
// When Chrome is the tracing controller and is the requester of the
// clock sync, the clock sync event looks like:
//
// {
// "args": {
// "sync_id": "abc123",
// "issue_ts": 12340
// }
// "ph": "c"
// "ts": 12345
// ...
// }
this.model_.clockSyncManager.addClockSyncMarker(
this.clockDomainId_, syncId,
tr.b.Unit.timestampFromUs(event.args.issue_ts),
tr.b.Unit.timestampFromUs(event.ts));
} else {
// When Chrome is a tracing agent and is the recipient of the clock
// sync request, the clock sync event looks like:
//
// {
// "args": { "sync_id": "abc123" }
// "ph": "c"
// "ts": 12345
// ...
// }
this.model_.clockSyncManager.addClockSyncMarker(
this.clockDomainId_, syncId, tr.b.Unit.timestampFromUs(event.ts));
}
},
// Because the order of Jit code events and V8 samples are not guaranteed,
// We store them in an array, sort by timestamp, and then process them.
processV8Events: function() {
this.v8SamplingData_.sort(function(a, b) {
if (a.ts !== b.ts)
return a.ts - b.ts;
if (a.ph === 'M' || a.ph === 'I')
return -1;
else if (b.ph === 'M' || b.ph === 'I')
return 1;
return 0;
});
var length = this.v8SamplingData_.length;
for (var i = 0; i < length; ++i) {
var event = this.v8SamplingData_[i];
if (event.ph === 'M' || event.ph === 'I') {
this.processJitCodeEvent(event);
} else if (event.ph === 'P') {
this.processV8Sample(event);
}
}
},
initBackcompatClockSyncEventTracker_: function(event) {
if (event.name !== undefined &&
event.name.startsWith(ASYNC_CLOCK_SYNC_EVENT_TITLE_PREFIX) &&
event.ph === 'S')
this.asyncClockSyncStart_ = event;
if (event.name !== undefined &&
event.name.startsWith(ASYNC_CLOCK_SYNC_EVENT_TITLE_PREFIX) &&
event.ph === 'F')
this.asyncClockSyncFinish_ = event;
if (this.asyncClockSyncStart_ == undefined ||
this.asyncClockSyncFinish_ == undefined)
return;
// Older version of Chrome doesn't support clock sync API, hence
// telemetry get around it by marking the clock sync events with
// console.time & console.timeEnd. When we encounter async events
// with named started with 'ClockSyncEvent.' prefix, create a
// synthetic clock sync events based on their timestamps.
var syncId =
this.asyncClockSyncStart_.name.substring(
ASYNC_CLOCK_SYNC_EVENT_TITLE_PREFIX.length);
if (syncId !==
this.asyncClockSyncFinish_.name.substring(
ASYNC_CLOCK_SYNC_EVENT_TITLE_PREFIX.length)) {
throw new Error('Inconsistent clock sync id of async clock sync ' +
'events.');
}
var clockSyncEvent = {
ph: 'c',
args: {
sync_id: syncId,
issue_ts: this.asyncClockSyncStart_.ts
},
ts: this.asyncClockSyncFinish_.ts,
};
this.asyncClockSyncStart_ = undefined;
this.asyncClockSyncFinish_ = undefined;
return clockSyncEvent;
},
importClockSyncMarkers: function() {
var asyncClockSyncStart, asyncClockSyncFinish;
for (var i = 0; i < this.events_.length; i++) {
var event = this.events_[i];
var possibleBackCompatClockSyncEvent =
this.initBackcompatClockSyncEventTracker_(event);
if (possibleBackCompatClockSyncEvent)
this.processClockSyncEvent(possibleBackCompatClockSyncEvent);
if (event.ph !== 'c')
continue;
var eventSizeInBytes =
this.model_.importOptions.trackDetailedModelStats ?
JSON.stringify(event).length : undefined;
this.model_.stats.willProcessBasicTraceEvent(
'clock_sync', event.cat, event.name, event.ts, eventSizeInBytes);
this.processClockSyncEvent(event);
}
},
/**
* Walks through the events_ list and outputs the structures discovered to
* model_.
*/
importEvents: function() {
if (this.stackFrameEvents_)
this.importStackFrames_(this.stackFrameEvents_, 'g');
if (this.traceAnnotations_)
this.importAnnotations_();
var importOptions = this.model_.importOptions;
var trackDetailedModelStats = importOptions.trackDetailedModelStats;
var modelStats = this.model_.stats;
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;
}
var eventSizeInBytes;
if (trackDetailedModelStats)
eventSizeInBytes = JSON.stringify(event).length;
else
eventSizeInBytes = undefined;
if (event.ph === 'B' || event.ph === 'E') {
modelStats.willProcessBasicTraceEvent(
'begin_end (non-compact)', event.cat, event.name, event.ts,
eventSizeInBytes);
this.processDurationEvent(event);
} else if (event.ph === 'X') {
modelStats.willProcessBasicTraceEvent(
'begin_end (compact)', event.cat, event.name, event.ts,
eventSizeInBytes);
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') {
modelStats.willProcessBasicTraceEvent(
'async', event.cat, event.name, event.ts, eventSizeInBytes);
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') {
modelStats.willProcessBasicTraceEvent(
'instant', event.cat, event.name, event.ts, eventSizeInBytes);
this.processInstantEvent(event);
} else if (event.ph === 'P') {
modelStats.willProcessBasicTraceEvent(
'samples', event.cat, event.name, event.ts, eventSizeInBytes);
this.processTraceSampleEvent(event);
} else if (event.ph === 'C') {
modelStats.willProcessBasicTraceEvent(
'counters', event.cat, event.name, event.ts, eventSizeInBytes);
this.processCounterEvent(event);
} else if (event.ph === 'M') {
modelStats.willProcessBasicTraceEvent(
'metadata', event.cat, event.name, event.ts, eventSizeInBytes);
this.processMetadataEvent(event);
} else if (event.ph === 'N' || event.ph === 'D' || event.ph === 'O') {
modelStats.willProcessBasicTraceEvent(
'objects', event.cat, event.name, event.ts, eventSizeInBytes);
this.processObjectEvent(event);
} else if (event.ph === 's' || event.ph === 't' || event.ph === 'f') {
modelStats.willProcessBasicTraceEvent(
'flows', event.cat, event.name, event.ts, eventSizeInBytes);
this.processFlowEvent(event);
} else if (event.ph === 'v') {
modelStats.willProcessBasicTraceEvent(
'memory_dumps', event.cat, event.name, event.ts,
eventSizeInBytes);
this.processMemoryDumpEvent(event);
} else if (event.ph === '(' || event.ph === ')') {
this.processContextEvent(event);
} else if (event.ph === 'c') {
// No-op. Clock sync events have already been processed in
// importClockSyncMarkers().
} else {
modelStats.willProcessBasicTraceEvent(
'unknown', event.cat, event.name, event.ts, eventSizeInBytes);
this.model_.importWarning({
type: 'parse_error',
message: 'Unrecognized event phase: ' +
event.ph + ' (' + event.name + ')'
});
}
}
this.processV8Events();
// 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) {
var model = this.model_;
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 = undefined;
} 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 +
').'
});
}
}
stackFrame.parentFrame = parentStackFrame;
}
},
importObjectTypeNameMap_: function(rawObjectTypeNameMap, pid) {
if (pid in this.objectTypeNameMap_) {
this.model_.importWarning({
type: 'metadata_parse_error',
message: 'Mapping from object type IDs to names provided for pid=' +
pid + ' multiple times.'
});
return;
}
var objectTypeNamePrefix = undefined;
var objectTypeNameSuffix = undefined;
var objectTypeNameMap = {};
for (var objectTypeId in rawObjectTypeNameMap) {
var rawObjectTypeName = rawObjectTypeNameMap[objectTypeId];
// If we haven't figured out yet which compiler the object type names
// come from, we try to do it now.
if (objectTypeNamePrefix === undefined) {
for (var i = 0; i < OBJECT_TYPE_NAME_PATTERNS.length; i++) {
var pattern = OBJECT_TYPE_NAME_PATTERNS[i];
if (rawObjectTypeName.startsWith(pattern.prefix) &&
rawObjectTypeName.endsWith(pattern.suffix)) {
objectTypeNamePrefix = pattern.prefix;
objectTypeNameSuffix = pattern.suffix;
break;
}
}
}
if (objectTypeNamePrefix !== undefined &&
rawObjectTypeName.startsWith(objectTypeNamePrefix) &&
rawObjectTypeName.endsWith(objectTypeNameSuffix)) {
// With compiler-specific prefix and suffix (automatically annotated
// object types).
objectTypeNameMap[objectTypeId] = rawObjectTypeName.substring(
objectTypeNamePrefix.length,
rawObjectTypeName.length - objectTypeNameSuffix.length);
} else {
// Without compiler-specific prefix and suffix (manually annotated
// object types and '[unknown]').
objectTypeNameMap[objectTypeId] = rawObjectTypeName;
}
}
this.objectTypeNameMap_[pid] = objectTypeNameMap;
},
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,
this.toModelTimeFromUs_(event.ts),
stackFrame,
event.weight);
m.samples.push(sample);
}
},
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 = {};
var nestableMeasureAsyncEventsByKey = {};
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;
}
if (event.cat === 'blink.user_timing') {
var matched = /([^\/:]+):([^\/:]+)\/?(.*)/.exec(event.name);
if (matched !== null) {
var key = matched[1] + ':' + event.cat;
event.args = JSON.parse(Base64.atob(matched[3]) || '{}');
if (nestableMeasureAsyncEventsByKey[key] === undefined)
nestableMeasureAsyncEventsByKey[key] = [];
nestableMeasureAsyncEventsByKey[key].push(asyncEventState);
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 measure async events into AsyncSlices.
this.createNestableAsyncSlices_(nestableMeasureAsyncEventsByKey);
// Parse nestable async events into AsyncSlices.
this.createNestableAsyncSlices_(nestableAsyncEventsByKey);
},
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.subTypes.getConstructor(
events[0].event.cat,
name);
var slice = new asyncSliceConstructor(
events[0].event.cat,
name,
getEventColor(events[0].event),
this.toModelTimeFromUs_(events[0].event.ts),
tr.b.concatenateObjects(events[0].event.args,
events[events.length - 1].event.args),
this.toModelTimeFromUs_(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.subTypes.getConstructor(
events[0].event.cat,
subName);
var subSlice = new asyncSliceConstructor(
events[0].event.cat,
subName,
getEventColor(event, subName + j),
this.toModelTimeFromUs_(events[startIndex].event.ts),
this.deepCopyIfNeeded_(events[j].event.args),
this.toModelTimeFromUs_(
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];
}
}
}
},
createNestableAsyncSlices_: function(nestableEventsByKey) {
for (var key in nestableEventsByKey) {
var eventStateEntries = nestableEventsByKey[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.subTypes.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 = this.toModelTimeFromUs_(startState.event.tts);
if (endState.event.tts) {
var thread_end = this.toModelTimeFromUs_(endState.event.tts);
thread_duration = thread_end - thread_start;
}
}
var slice = new asyncSliceConstructor(
eventStateEntry.event.cat,
eventStateEntry.event.name,
getEventColor(endState.event),
this.toModelTimeFromUs_(startState.event.ts),
sliceArgs,
this.toModelTimeFromUs_(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]);
}
}
},
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;
}
var createFlowEvent = function(thread, event, opt_slice) {
var startSlice, flowId, flowStartTs;
if (event.bind_id) {
// Support Flow API v2.
startSlice = opt_slice;
flowId = event.bind_id;
flowStartTs = this.toModelTimeFromUs_(event.ts + event.dur);
} else {
// Support Flow API v1.
var ts = this.toModelTimeFromUs_(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;
}.bind(this);
var finishFlowEventWith = function(
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 = this.toModelTimeFromUs_(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 =
this.toModelTimeFromUs_(event.ts) - flowEvent.start;
flowEvent.endStackFrame = that.getStackFrameForEvent_(event);
that.mergeArgsInto_(flowEvent.args, event.args, flowEvent.title);
return true;
}.bind(this);
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;
}
// 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;
var processEvent = function(objectEventState) {
var event = objectEventState.event;
var scopedId = this.scopedIdForEvent_(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 (scopedId.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 = this.toModelTimeFromUs_(event.ts);
var instance;
if (event.ph === 'N') {
try {
instance = process.objects.idWasCreated(
scopedId, event.cat, event.name, ts);
} catch (e) {
this.model_.importWarning({
type: 'object_parse_error',
message: 'While processing create of ' +
scopedId + ' 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 ' + scopedId + ' 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(
scopedId, cat, event.name, ts, args, baseTypename);
snapshot.snapshottedOnThread = thread;
} catch (e) {
this.model_.importWarning({
type: 'object_parse_error',
message: 'While processing snapshot of ' +
scopedId + ' at ts=' + ts + ': ' + e
});
return;
}
instance = snapshot.objectInstance;
} else if (event.ph === 'D') {
try {
process.objects.idWasDeleted(scopedId, event.cat, event.name, ts);
var instanceMap = process.objects.getOrCreateInstanceMap_(scopedId);
instance = instanceMap.lastInstance;
} catch (e) {
this.model_.importWarning({
type: 'object_parse_error',
message: 'While processing delete of ' +
scopedId + ' at ts=' + ts + ': ' + e
});
return;
}
}
if (instance)
instance.colorId = getEventColor(event, instance.typeName);
}.bind(this);
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;
var scope = containingSnapshot.objectInstance.scopedId.scope;
try {
res = process.objects.addSnapshot(
new tr.model.ScopedId(scope, 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(this.toModelTimeFromUs_(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 levelsOfDetail = {};
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, levelsOfDetail,
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 = levelsOfDetail.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, levelsOfDetail,
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(this.toModelTimeFromUs_(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_(levelsOfDetail, dumps, pid,
dumpId);
this.parseMemoryDumpAllocatorDumps_(processMemoryDump, globalMemoryDump,
processMemoryAllocatorDumpsByFullName,
globalMemoryAllocatorDumpsByFullName,
allMemoryAllocatorDumpsByGuid, dumps, pid, dumpId);
}
if (levelsOfDetail.process === undefined) {
// Infer level of detail from the presence of VM regions in legacy
// traces (where raw process memory dump events don't contain the
// level_of_detail field). These traces will not have BACKGROUND mode.
levelsOfDetail.process = processMemoryDump.vmRegions ? DETAILED : LIGHT;
}
if (!this.updateMemoryDumpLevelOfDetail_(
levelsOfDetail, 'global', levelsOfDetail.process)) {
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'diffent levels of detail provided for global memory' +
' dump (dump ID=' + dumpId + ').'
});
}
processMemoryDump.levelOfDetail = levelsOfDetail.process;
delete levelsOfDetail.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;
}
// See //base/trace_event/process_memory_maps.cc in Chromium.
var vmRegions = new Array(rawVmRegions.length);
for (var i = 0; i < rawVmRegions.length; i++) {
var rawVmRegion = rawVmRegions[i];
var byteStats = {};
var rawByteStats = rawVmRegion.bs;
for (var rawByteStatName in rawByteStats) {
var rawByteStatValue = rawByteStats[rawByteStatName];
if (rawByteStatValue === undefined) {
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'Byte stat \'' + rawByteStatName + '\' of VM region ' +
i + ' (' + rawVmRegion.mf + ') in process memory dump for ' +
'PID=' + pid + ' and dump ID=' + dumpId +
' does not have a value.'
});
continue;
}
var byteStatName = BYTE_STAT_NAME_MAP[rawByteStatName];
if (byteStatName === undefined) {
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'Unknown byte stat name \'' + rawByteStatName + '\' (' +
rawByteStatValue + ') of VM region ' + i + ' (' +
rawVmRegion.mf + ') in process memory dump for PID=' + pid +
' and dump ID=' + dumpId + '.'
});
continue;
}
byteStats[byteStatName] = parseInt(rawByteStatValue, 16);
}
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 =
tr.model.VMRegionClassificationNode.fromRegions(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 = {};
var objectTypeNameMap = this.objectTypeNameMap_[pid];
if (objectTypeNameMap === undefined) {
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'Missing mapping from object type IDs to names.'
});
}
for (var allocatorName in rawHeapDumps) {
var entries = rawHeapDumps[allocatorName].entries;
if (entries === undefined || entries.length === 0) {
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'No heap entries in a ' + allocatorName +
' heap dump for PID=' + pid + ' and dump ID=' + dumpId + '.'
});
continue;
}
// The old format always starts with a {size: <total>} entry.
// See https://goo.gl/WYStil
// TODO(petrcermak): Remove support for the old format once the new
// format has been around long enough.
var isOldFormat = entries[0].bt === undefined;
if (!isOldFormat && objectTypeNameMap === undefined) {
// Mapping from object type IDs to names must be provided in the new
// format.
continue;
}
var heapDump = new tr.model.HeapDump(processMemoryDump, allocatorName);
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var leafStackFrameIndex = entry.bt;
var leafStackFrame;
// There are two possible mappings from leaf stack frame indices
// (provided in the trace) to the corresponding stack frames
// depending on the format.
if (isOldFormat) {
// Old format:
// Undefined index -> / (root)
// Defined index for /A/B -> /A/B/<self>
if (leafStackFrameIndex === undefined) {
leafStackFrame = undefined /* root */;
} else {
// Get the leaf stack frame corresponding to the provided index.
var leafStackFrameId = idPrefix + leafStackFrameIndex;
if (leafStackFrameIndex === '') {
leafStackFrame = undefined /* root */;
} else {
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;
}
}
// Inject an artificial <self> leaf stack frame.
leafStackFrameId += ':self';
if (model.stackFrames[leafStackFrameId] !== undefined) {
// The frame might already exist if there are multiple process
// memory dumps (for the same process) in the trace.
leafStackFrame = model.stackFrames[leafStackFrameId];
} else {
leafStackFrame = new tr.model.StackFrame(
leafStackFrame, leafStackFrameId, '<self>',
undefined /* colorId */);
model.addStackFrame(leafStackFrame);
}
}
} else {
// New format:
// Undefined index -> (invalid value)
// Defined index for /A/B -> /A/B
if (leafStackFrameIndex === undefined) {
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'Missing stack frame ID of heap entry ' + i +
' (size ' + size + ') in a ' + allocatorName +
' heap dump for PID=' + pid + '.'
});
continue;
}
// Get the leaf stack frame corresponding to the provided index.
var leafStackFrameId = idPrefix + leafStackFrameIndex;
if (leafStackFrameIndex === '') {
leafStackFrame = undefined /* root */;
} else {
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;
}
}
}
var objectTypeId = entry.type;
var objectTypeName;
if (objectTypeId === undefined) {
objectTypeName = undefined /* total over all types */;
} else if (objectTypeNameMap === undefined) {
// This can only happen when the old format is used.
continue;
} else {
objectTypeName = objectTypeNameMap[objectTypeId];
if (objectTypeName === undefined) {
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'Missing object type name (ID ' + objectTypeId +
') of heap entry ' + i + ' (size ' + size + ') in a ' +
allocatorName + ' heap dump for pid=' + pid + '.'
});
continue;
}
}
var size = parseInt(entry.size, 16);
var count = entry.count === undefined ? undefined :
parseInt(entry.count, 16);
heapDump.addEntry(leafStackFrame, objectTypeName, size, count);
}
// Throw away heap dumps with no entries. This can happen if all raw
// entries in the trace are skipped for some reason (e.g. invalid leaf
// stack frame ID).
if (heapDump.entries.length > 0)
heapDumps[allocatorName] = heapDump;
}
if (Object.keys(heapDumps).length > 0)
processMemoryDump.heapDumps = heapDumps;
},
parseMemoryDumpLevelOfDetail_: function(levelsOfDetail, dumps, pid,
dumpId) {
var rawLevelOfDetail = dumps.level_of_detail;
var level;
switch (rawLevelOfDetail) {
case 'background':
level = BACKGROUND;
break;
case 'light':
level = LIGHT;
break;
case 'detailed':
level = DETAILED;
break;
case undefined:
level = undefined;
break;
default:
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'unknown raw level of detail \'' + rawLevelOfDetail +
'\' of process memory dump for PID=' + pid +
' and dump ID=' + dumpId + '.'
});
return;
}
if (!this.updateMemoryDumpLevelOfDetail_(
levelsOfDetail, 'process', level)) {
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'diffent levels of detail provided for process memory' +
' dump for PID=' + pid + ' (dump ID=' + dumpId + ').'
});
}
},
updateMemoryDumpLevelOfDetail_: function(levelsOfDetail, scope, level) {
// If all process memory dump events have the same level of detail (for
// the particular 'process' or 'global' scope), return true.
if (!(scope in levelsOfDetail) || level === levelsOfDetail[scope]) {
levelsOfDetail[scope] = level;
return true;
}
// If the process memory dump events have different levels of detail (for
// the particular 'process' or 'global' scope), use the highest level and
// return false.
if (MEMORY_DUMP_LEVEL_OF_DETAIL_ORDER.indexOf(level) >
MEMORY_DUMP_LEVEL_OF_DETAIL_ORDER.indexOf(levelsOfDetail[scope])) {
levelsOfDetail[scope] = level;
}
return false;
},
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.'
});
}
// A memory allocator dump can have optional flags.
var flags = rawAllocatorDump.flags || 0;
var isWeakDump = !!(flags & WEAK_MEMORY_ALLOCATOR_DUMP_FLAG);
// 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);
allocatorDump.weak = isWeakDump;
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;
}
if (!isWeakDump) {
// A MemoryAllocatorDump is non-weak if at least one process dumped
// it without WEAK_MEMORY_ALLOCATOR_DUMP_FLAG.
allocatorDump.weak = false;
}
}
// 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) {
var attrArgs = attributes[attrName];
var attrType = attrArgs.type;
var attrValue = attrArgs.value;
switch (attrType) {
case 'scalar':
if (attrName in allocatorDump.numerics) {
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'Multiple values provided for scalar attribute ' +
attrName + ' of memory allocator dump ' + fullName +
' (GUID=' + guid + ') for PID=' + pid + ' and dump ID=' +
dumpId + '.'
});
break;
}
var unit = attrArgs.units === 'bytes' ?
tr.b.Unit.byName.sizeInBytes_smallerIsBetter :
tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
var value = parseInt(attrValue, 16);
allocatorDump.addNumeric(attrName,
new tr.v.ScalarNumeric(unit, value));
break;
case 'string':
if (attrName in allocatorDump.diagnostics) {
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'Multiple values provided for string attribute ' +
attrName + ' of memory allocator dump ' + fullName +
' (GUID=' + guid + ') for PID=' + pid + ' and dump ID=' +
dumpId + '.'
});
break;
}
allocatorDump.addDiagnostic(attrName, attrValue);
break;
default:
this.model_.importWarning({
type: 'memory_dump_parse_error',
message: 'Unknown type provided for attribute ' + attrName +
' of memory allocator dump ' + fullName + ' (GUID=' + guid +
') for PID=' + pid + ' and dump ID=' + dumpId + ': ' +
attrType
});
break;
}
}
}
},
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);
if (allocatorDump.weak !== false) {
// If we are inferring a parent dump (e.g. 'root/parent') of a
// current dump (e.g. 'root/parent/current') which is weak (or
// was also inferred and we don't know yet whether it's weak or
// not), then we clear the weak flag on the parent dump because
// we don't know yet whether it should be weak or non-weak:
//
// * We can't mark the parent as non-weak straightaway because
// the parent might have no non-weak descendants (in which
// case we want the inferred parent to be weak, so that it
// would be later removed like the current dump).
// * We can't mark the parent as weak immediately either. If we
// did and later encounter a non-weak child of the parent
// (e.g. 'root/parent/another_child'), then we couldn't
// retroactively mark the inferred parent dump as non-weak
// because we couldn't tell whether the parent dump was
// dumped in the trace as weak (in which case it should stay
// weak and be subsequently removed) or whether it was
// inferred as weak (in which case it should be changed to
// non-weak).
//
// Therefore, we defer marking the inferred parent as
// weak/non-weak. If an inferred parent dump does not have any
// non-weak child, it will be marked as weak at the end of this
// method.
//
// Note that this should not be confused with the recursive
// propagation of the weak flag from parent dumps to their
// children and from owned dumps to their owners, which is
// performed in GlobalMemoryDump.prototype.removeWeakDumps().
parentAllocatorDump.weak = undefined;
}
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) {
if (!allocatorDump.weak) {
// If the current dump is non-weak, then we must ensure that all
// its inferred ancestors are also non-weak.
while (parentAllocatorDump !== undefined &&
parentAllocatorDump.weak === undefined) {
parentAllocatorDump.weak = false;
parentAllocatorDump = parentAllocatorDump.parent;
}
}
break;
}
fullName = parentFullName;
allocatorDump = parentAllocatorDump;
}
}
// All inferred ancestor dumps that have a non-weak child have already
// been marked as non-weak. We now mark the rest as weak.
for (var fullName in memoryAllocatorDumpsByFullName) {
var allocatorDump = memoryAllocatorDumpsByFullName[fullName];
if (allocatorDump.weak === undefined)
allocatorDump.weak = true;
}
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 + ').'
});
}
}
}
}
},
/**
* Converts |ts| (in microseconds) to a timestamp in the model clock domain
* (in milliseconds).
*/
toModelTimeFromUs_: function(ts) {
if (!this.toModelTime_) {
this.toModelTime_ =
this.model_.clockSyncManager.getModelTimeTransformer(
this.clockDomainId_);
}
return this.toModelTime_(tr.b.Unit.timestampFromUs(ts));
},
/**
* Converts |ts| (in microseconds) to a timestamp in the model clock domain
* (in milliseconds). If |ts| is undefined, undefined is returned.
*/
maybeToModelTimeFromUs_: function(ts) {
if (ts === undefined)
return undefined;
return this.toModelTimeFromUs_(ts);
}
};
tr.importer.Importer.register(TraceEventImporter);
return {
TraceEventImporter: TraceEventImporter
};
});
</script>