summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authornduca@chromium.org <nduca@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-12-20 20:13:28 +0000
committernduca@chromium.org <nduca@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-12-20 20:13:28 +0000
commitc93e46886678366ae916d622334d5748d60cf999 (patch)
treedaa88333633174cca0feb3ddbcdc5f51c70edbc3
parent02f996c2e04c10fa4a2afe2f29bf7d3bcc069443 (diff)
downloadchromium_src-c93e46886678366ae916d622334d5748d60cf999.zip
chromium_src-c93e46886678366ae916d622334d5748d60cf999.tar.gz
chromium_src-c93e46886678366ae916d622334d5748d60cf999.tar.bz2
Refactor about:tracing to support linux kernel traces.
Review URL: http://codereview.chromium.org/8968024 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@115171 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/resources/tracing.js2
-rw-r--r--chrome/browser/resources/tracing/README14
-rw-r--r--chrome/browser/resources/tracing/cur_trace1
-rw-r--r--chrome/browser/resources/tracing/kernel_trace_viewer.html88
-rw-r--r--chrome/browser/resources/tracing/linux_perf_importer.js503
-rw-r--r--chrome/browser/resources/tracing/linux_perf_importer_test.html220
-rw-r--r--chrome/browser/resources/tracing/profiling_view.js2
-rw-r--r--chrome/browser/resources/tracing/test_utils.js26
-rw-r--r--chrome/browser/resources/tracing/timeline.css1
-rw-r--r--chrome/browser/resources/tracing/timeline.js44
-rw-r--r--chrome/browser/resources/tracing/timeline_model.js589
-rw-r--r--chrome/browser/resources/tracing/timeline_model_test.html516
-rw-r--r--chrome/browser/resources/tracing/timeline_test.html23
-rw-r--r--chrome/browser/resources/tracing/timeline_track.js184
-rw-r--r--chrome/browser/resources/tracing/timeline_track_test.html25
-rw-r--r--chrome/browser/resources/tracing/timeline_view.js20
-rw-r--r--chrome/browser/resources/tracing/trace_event_importer.js326
-rw-r--r--chrome/browser/resources/tracing/trace_event_importer_test.html559
18 files changed, 2352 insertions, 791 deletions
diff --git a/chrome/browser/resources/tracing.js b/chrome/browser/resources/tracing.js
index 58877ca..ff64c79 100644
--- a/chrome/browser/resources/tracing.js
+++ b/chrome/browser/resources/tracing.js
@@ -6,6 +6,8 @@
<include src="tracing/overlay.js">
<include src="tracing/tracing_controller.js">
<include src="tracing/timeline_model.js">
+<include src="tracing/linux_perf_importer.js">
+<include src="tracing/trace_event_importer.js">
<include src="tracing/sorted_array_utils.js">
<include src="tracing/measuring_stick.js">
<include src="tracing/timeline.js">
diff --git a/chrome/browser/resources/tracing/README b/chrome/browser/resources/tracing/README
new file mode 100644
index 0000000..2bd6c39
--- /dev/null
+++ b/chrome/browser/resources/tracing/README
@@ -0,0 +1,14 @@
+This directory contains the UI for Chrome's about:tracing tool.
+
+To work on this code:
+ cd src/chrome/browser/resources/
+ python -m SimpleHTTPServer
+
+In any browser, navigate to
+ http://localhost:8000/tracing/
+
+We use Closure's unit test harness for testing. There isn't a master
+test file, so just run all the .html files you find. :/
+
+If you add a file, make sure to update ../tracing.js and verify all the tests,
+since we don't have a sane module system.
diff --git a/chrome/browser/resources/tracing/cur_trace b/chrome/browser/resources/tracing/cur_trace
new file mode 100644
index 0000000..70e17ac
--- /dev/null
+++ b/chrome/browser/resources/tracing/cur_trace
@@ -0,0 +1 @@
+/Users/nduca/Local/clank-remote/cur_trace/ \ No newline at end of file
diff --git a/chrome/browser/resources/tracing/kernel_trace_viewer.html b/chrome/browser/resources/tracing/kernel_trace_viewer.html
new file mode 100644
index 0000000..39482ef
--- /dev/null
+++ b/chrome/browser/resources/tracing/kernel_trace_viewer.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Copyright (c) 2011 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.
+-->
+<head i18n-values="dir:textdirection;">
+<title>Interactive Timeline Tests</title>
+<link rel="stylesheet" href="timeline.css">
+<link rel="stylesheet" href="timeline_view.css">
+<script src="../shared/js/cr.js"></script>
+<script src="../shared/js/cr/event_target.js"></script>
+<script src="../shared/js/cr/ui.js"></script>
+<script src="../shared/js/util.js"></script>
+<script src="timeline_model.js"></script>
+<script src="linux_perf_importer.js"></script>
+<script src="trace_event_importer.js"></script>
+<script src="sorted_array_utils.js"></script>
+<script src="measuring_stick.js"></script>
+<script src="timeline.js"></script>
+<script src="timeline_track.js"></script>
+<script src="timeline_view.js"></script>
+<script src="fast_rect_renderer.js"></script>
+<script src="test_utils.js"></script>
+<style>
+ .view {
+ overflow: hidden;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+
+</style>
+</head>
+<body>
+ <div class="view">
+ </div>
+ <script>
+ var linuxPerfData;
+ var traceEventData;
+ var timelineViewEl;
+ function onLoad() {
+ var dirname;
+ if (window.location.search) {
+ var q = {};
+ window.location.search.slice(1).split('&').forEach(function(x) {
+ var t = x.split('=');
+ q[t[0]] = t[1];
+ });
+ if (q.dirname)
+ dirname = q.dirname;
+ else
+ dirname = './cur_trace';
+ } else {
+ dirname = './cur_trace';
+ }
+ // Creating attached vs detached stress tests the canvas- and viewport-
+ // setup code.
+ test_utils.getAsync(dirname + '/kernel.trace', function(data) {
+ linuxPerfData = data;
+ reload();
+ });
+ test_utils.getAsync(dirname + '/chrome.json', function(data) {
+ traceEventData = data;
+ reload();
+ });
+ }
+ function reload() {
+ if (!linuxPerfData || !traceEventData)
+ return;
+
+ var m = new tracing.TimelineModel();
+ m.importEvents(traceEventData, true, [linuxPerfData]);
+
+ timelineViewEl = document.querySelector('.view');
+ cr.ui.decorate(timelineViewEl, tracing.TimelineView);
+ timelineViewEl.model = m;
+ timelineViewEl.tabIndex = 1;
+ timelineViewEl.timeline.focusElement = timelineViewEl;
+ }
+
+ document.addEventListener('DOMContentLoaded', onLoad);
+ </script>
+</body>
+</html>
diff --git a/chrome/browser/resources/tracing/linux_perf_importer.js b/chrome/browser/resources/tracing/linux_perf_importer.js
new file mode 100644
index 0000000..a4bf40a
--- /dev/null
+++ b/chrome/browser/resources/tracing/linux_perf_importer.js
@@ -0,0 +1,503 @@
+// Copyright (c) 2011 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.
+
+/**
+ * @fileoverview Imports text files in the Linux event trace format into the
+ * timeline model. This format is output both by sched_trace and by Linux's perf
+ * tool.
+ *
+ * This importer assumes the events arrive as a string. The unit tests provide
+ * examples of the trace format.
+ *
+ * Linux scheduler traces use a definition for 'pid' that is different than
+ * tracing uses. Whereas tracing uses pid to identify a specific process, a pid
+ * in a linux trace refers to a specific thread within a process. Within this
+ * file, we the definition used in Linux traces, as it improves the importing
+ * code's readability.
+ */
+cr.define('tracing', function() {
+ /**
+ * Represents the scheduling state for a single thread.
+ * @constructor
+ */
+ function CpuState(cpu) {
+ this.cpu = cpu;
+ }
+
+ CpuState.prototype = {
+ __proto__: Object.prototype,
+
+ /**
+ * Switches the active pid on this Cpu. If necessary, add a TimelineSlice
+ * to the cpu representing the time spent on that Cpu since the last call to
+ * switchRunningLinuxPid.
+ */
+ switchRunningLinuxPid: function(importer, prevState, ts, pid, comm, prio) {
+ // Generate a slice if the last active pid was not the idle task
+ if (this.lastActivePid !== undefined && this.lastActivePid != 0) {
+ var duration = ts - this.lastActiveTs;
+ var thread = importer.threadsByLinuxPid[this.lastActivePid];
+ if (thread)
+ name = thread.userFriendlyName;
+ else
+ name = this.lastActiveComm;
+
+ var slice = new tracing.TimelineSlice(name,
+ tracing.getStringColorId(name),
+ this.lastActiveTs,
+ {
+ comm: this.lastActiveComm,
+ tid: this.lastActivePid,
+ prio: this.lastActivePrio,
+ stateWhenDescheduled: prevState
+ },
+ duration);
+ this.cpu.slices.push(slice);
+ }
+
+ this.lastActiveTs = ts;
+ this.lastActivePid = pid;
+ this.lastActiveComm = comm;
+ this.lastActivePrio = prio;
+ }
+ };
+
+ /**
+ * Imports linux perf events into a specified model.
+ * @constructor
+ */
+ function LinuxPerfImporter(model, events, isAdditionalImport) {
+ this.isAdditionalImport_ = isAdditionalImport;
+ this.model_ = model;
+ this.events_ = events;
+ this.clockSyncRecords_ = [];
+ this.cpuStates_ = {};
+ this.kernelThreadStates_ = {};
+ this.buildMapFromLinuxPidsToTimelineThreads();
+ }
+
+ TestExports = {};
+
+ // Matches the generic trace record:
+ // <idle>-0 [001] 1.23: sched_switch
+ var lineRE = /^\s*(.+?)\s+\[(\d+)\]\s*(\d+\.\d+):\s+(\S+):\s(.*)$/;
+ TestExports.lineRE = lineRE;
+
+ // Matches the sched_switch record
+ var schedSwitchRE = new RegExp(
+ 'prev_comm=(.+) prev_pid=(\\d+) prev_prio=(\\d+) prev_state=(\\S) ==> ' +
+ 'next_comm=(.+) next_pid=(\\d+) next_prio=(\\d+)');
+ TestExports.schedSwitchRE = schedSwitchRE;
+
+ // Matches the sched_wakeup record
+ var schedWakeupRE =
+ /comm=(.+) pid=(\d+) prio=(\d+) success=(\d+) target_cpu=(\d+)/;
+ TestExports.schedWakeupRE = schedWakeupRE;
+
+ // Matches the trace_event_clock_sync record
+ // 0: trace_event_clock_sync: parent_ts=19581477508
+ var traceEventClockSyncRE = /trace_event_clock_sync: parent_ts=(\d+\.?\d*)/;
+ TestExports.traceEventClockSyncRE = traceEventClockSyncRE;
+
+ // Matches the workqueue_execute_start record
+ // workqueue_execute_start: work struct c7a8a89c: function MISRWrapper
+ var workqueueExecuteStartRE = /work struct (.+): function (\S+)/;
+
+ // Matches the workqueue_execute_start record
+ // workqueue_execute_end: work struct c7a8a89c
+ var workqueueExecuteEndRE = /work struct (.+)/;
+
+ /**
+ * Guesses whether the provided events is a Linux perf string.
+ * Looks for the magic string "# tracer" at the start of the file,
+ * or the typical task-pid-cpu-timestamp-function sequence of a typical
+ * trace's body.
+ *
+ * @return {boolean} True when events is a linux perf array.
+ */
+ LinuxPerfImporter.canImport = function(events) {
+ if (!(typeof(events) === 'string' || events instanceof String))
+ return false;
+
+ if (/^# tracer:/.exec(events))
+ return true;
+
+ var m = /^(.+)\n/.exec(events);
+ if (m)
+ events = m[1];
+ if (lineRE.exec(events))
+ return true;
+
+ return false;
+ };
+
+ LinuxPerfImporter.prototype = {
+ __proto__: Object.prototype,
+
+ /**
+ * Precomputes a lookup table from linux pids back to existing
+ * TimelineThreads. This is used during importing to add information to each
+ * timeline thread about whether it was running, descheduled, sleeping, et
+ * cetera.
+ */
+ buildMapFromLinuxPidsToTimelineThreads: function() {
+ this.threadsByLinuxPid = {};
+ this.model_.getAllThreads().forEach(
+ function(thread) {
+ this.threadsByLinuxPid[thread.tid] = thread;
+ }.bind(this));
+ },
+
+ /**
+ * @return {CpuState} A CpuState corresponding to the given cpuNumber.
+ */
+ getOrCreateCpuState: function(cpuNumber) {
+ if (!this.cpuStates_[cpuNumber]) {
+ var cpu = this.model_.getOrCreateCpu(cpuNumber);
+ this.cpuStates_[cpuNumber] = new CpuState(cpu);
+ }
+ return this.cpuStates_[cpuNumber];
+ },
+
+ /**
+ * @return {TimelinThread} A thread corresponding to the kernelThreadName.
+ */
+ getOrCreateKernelThread: function(kernelThreadName) {
+ if (!this.kernelThreadStates_[kernelThreadName]) {
+ var pid = /.+-(\d+)/.exec(kernelThreadName)[1];
+ pid = parseInt(pid);
+
+ var thread = this.model_.getOrCreateProcess(pid).getOrCreateThread(pid);
+ thread.name = kernelThreadName;
+ this.kernelThreadStates_[kernelThreadName] = {
+ pid: pid,
+ thread: thread,
+ openSlice: undefined,
+ openSliceTS: undefined
+ };
+ this.threadsByLinuxPid[pid] = thread;
+ }
+ return this.kernelThreadStates_[kernelThreadName];
+ },
+
+ /**
+ * Imports the data in this.events_ into model_.
+ */
+ importEvents: function() {
+ this.importCpuData();
+ if (!this.alignClocks())
+ return;
+ this.buildPerThreadCpuSlicesFromCpuState();
+ },
+
+ /**
+ * Builds the cpuSlices array on each thread based on our knowledge of what
+ * each Cpu is doing. This is done only for TimelineThreads that are
+ * already in the model, on the assumption that not having any traced data
+ * on a thread means that it is not of interest to the user.
+ */
+ buildPerThreadCpuSlicesFromCpuState: function() {
+ // Push the cpu slices to the threads that they run on.
+ for (var cpuNumber in this.cpuStates_) {
+ var cpuState = this.cpuStates_[cpuNumber];
+ var cpu = cpuState.cpu;
+
+ for (var i = 0; i < cpu.slices.length; i++) {
+ var slice = cpu.slices[i];
+
+ var thread = this.threadsByLinuxPid[slice.args.tid];
+ if (!thread)
+ continue;
+ if (!thread.tempCpuSlices)
+ thread.tempCpuSlices = [];
+ thread.tempCpuSlices.push(slice);
+ }
+ }
+
+ // Create slices for when the thread is not running.
+ var runningId = tracing.getColorIdByName('running');
+ var runnableId = tracing.getColorIdByName('runnable');
+ var sleepingId = tracing.getColorIdByName('sleeping');
+ var ioWaitId = tracing.getColorIdByName('iowait');
+ this.model_.getAllThreads().forEach(function(thread) {
+ if (!thread.tempCpuSlices)
+ return;
+ var origSlices = thread.tempCpuSlices;
+ delete thread.tempCpuSlices;
+
+ origSlices.sort(function(x, y) {
+ return x.start - y.start;
+ });
+
+ // Walk the slice list and put slices between each original slice
+ // to show when the thread isn't running
+ var slices = [];
+ if (origSlices.length) {
+ var slice = origSlices[0];
+ slices.push(new tracing.TimelineSlice('Running', runningId,
+ slice.start, {}, slice.duration));
+ }
+ for (var i = 1; i < origSlices.length; i++) {
+ var prevSlice = origSlices[i - 1];
+ var nextSlice = origSlices[i];
+ var midDuration = nextSlice.start - prevSlice.end;
+ if (prevSlice.args.stateWhenDescheduled == 'S') {
+ slices.push(new tracing.TimelineSlice('Sleeping', sleepingId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'R') {
+ slices.push(new tracing.TimelineSlice('Runnable', runnableId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'D') {
+ slices.push(new tracing.TimelineSlice('I/O Wait', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'T') {
+ slices.push(new tracing.TimelineSlice('__TASK_STOPPED', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 't') {
+ slices.push(new tracing.TimelineSlice('debug', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'Z') {
+ slices.push(new tracing.TimelineSlice('Zombie', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'X') {
+ slices.push(new tracing.TimelineSlice('Exit Dead', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'x') {
+ slices.push(new tracing.TimelineSlice('Task Dead', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'W') {
+ slices.push(new tracing.TimelineSlice('WakeKill', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else {
+ throw 'Unrecognized state: ' + prevSlice.args.stateWhenDescheduled;
+ }
+
+ slices.push(new tracing.TimelineSlice('Running', runningId,
+ nextSlice.start, {}, nextSlice.duration));
+ }
+ thread.cpuSlices = slices;
+ });
+ },
+
+ /**
+ * Walks the slices stored on this.cpuStates_ and adjusts their timestamps
+ * based on any alignment metadata we discovered.
+ */
+ alignClocks: function() {
+ if (this.clockSyncRecords_.length == 0) {
+ // If this is an additional import, and no clock syncing records were
+ // found, then abort the import. Otherwise, just skip clock alignment.
+ if (!this.isAdditionalImport_)
+ return;
+
+ // Remove the newly imported CPU slices from the model.
+ this.abortImport();
+ return false;
+ }
+
+ // Shift all the slice times based on the sync record.
+ var sync = this.clockSyncRecords_[0];
+ var timeShift = sync.parentTS - sync.perfTS;
+ for (var cpuNumber in this.cpuStates_) {
+ var cpuState = this.cpuStates_[cpuNumber];
+ var cpu = cpuState.cpu;
+
+ for (var i = 0; i < cpu.slices.length; i++) {
+ var slice = cpu.slices[i];
+ slice.start = slice.start + timeShift;
+ slice.duration = slice.duration;
+ }
+
+ for (var counterName in cpu.counters) {
+ var counter = cpu.counters[counterName];
+ for (var sI = 0; sI < counter.timestamps.length; sI++)
+ counter.timestamps[sI] = (counter.timestamps[sI] + timeShift);
+ }
+ }
+ for (var kernelThreadName in this.kernelThreadStates_) {
+ var kthread = this.kernelThreadStates_[kernelThreadName];
+ var thread = kthread.thread;
+ for (var i = 0; i < thread.subRows[0].length; i++) {
+ thread.subRows[0][i].start += timeShift;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Removes any data that has been added to the model because of an error
+ * detected during the import.
+ */
+ abortImport: function() {
+ if (this.pushedEventsToThreads)
+ throw 'Cannot abort, have alrady pushedCpuDataToThreads.';
+
+ for (var cpuNumber in this.cpuStates_)
+ delete this.model_.cpus[cpuNumber];
+ for (var kernelThreadName in this.kernelThreadStates_) {
+ var kthread = this.kernelThreadStates_[kernelThreadName];
+ var thread = kthread.thread;
+ var process = thread.parent;
+ delete process.threads[thread.tid];
+ delete this.model_.processes[process.pid];
+ }
+ this.model_.importErrors.push(
+ 'Cannot import kernel trace without a clock sync.');
+ },
+
+ /**
+ * Records the fact that a pid has become runnable. This data will
+ * eventually get used to derive each thread's cpuSlices array.
+ */
+ markPidRunnable: function(ts, pid, comm, prio) {
+ // TODO(nduca): implement this functionality.
+ },
+
+ /**
+ * Walks the this.events_ structure and creates TimelineCpu objects.
+ */
+ importCpuData: function() {
+ this.lines_ = this.events_.split('\n');
+
+ for (var lineNumber = 0; lineNumber < this.lines_.length; ++lineNumber) {
+ var line = this.lines_[lineNumber];
+ if (/^#/.exec(line) || line.length == 0)
+ continue;
+ var eventBase = lineRE.exec(line);
+ if (!eventBase) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Unrecognized line: ' + line);
+ continue;
+ }
+
+ var cpuState = this.getOrCreateCpuState(parseInt(eventBase[2]));
+ var ts = parseFloat(eventBase[3]) * 1000;
+
+ var eventName = eventBase[4];
+
+ if (eventName == 'sched_switch') {
+ var event = schedSwitchRE.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed sched_switch event');
+ continue;
+ }
+
+ var prevState = event[4];
+ var nextComm = event[5];
+ var nextPid = parseInt(event[6]);
+ var nextPrio = parseInt(event[7]);
+ cpuState.switchRunningLinuxPid(
+ this, prevState, ts, nextPid, nextComm, nextPrio);
+
+ } else if (eventName == 'sched_wakeup') {
+ var event = schedWakeupRE.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed sched_wakeup event');
+ continue;
+ }
+
+ var comm = event[1];
+ var pid = parseInt(event[2]);
+ var prio = parseInt(event[3]);
+ this.markPidRunnable(ts, pid, comm, prio);
+
+ } else if (eventName == 'power_start') {
+ var event = /type=(\d+) state=(\d) cpu_id=(\d)+/.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed power_start event');
+ continue;
+ }
+ var targetCpuNumber = parseInt(event[3]);
+ var targetCpu = this.getOrCreateCpuState(targetCpuNumber);
+ var powerCounter;
+ if (event[1] == '1') {
+ powerCounter = targetCpu.cpu.getOrCreateCounter('', 'C-State');
+ } else {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Don\'t understand power_start events of type ' + event[1]);
+ continue;
+ }
+ if (powerCounter.numSeries == 0) {
+ powerCounter.seriesNames.push('state');
+ powerCounter.seriesColors.push(
+ tracing.getStringColorId(powerCounter.name + '.' + 'state'));
+ }
+ var powerState = parseInt(event[2]);
+ powerCounter.timestamps.push(ts);
+ powerCounter.samples.push(powerState);
+ } else if (eventName == 'power_frequency') {
+ var event = /type=(\d+) state=(\d+) cpu_id=(\d)+/.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed power_start event');
+ continue;
+ }
+ var targetCpuNumber = parseInt(event[3]);
+ var targetCpu = this.getOrCreateCpuState(targetCpuNumber);
+ var powerCounter =
+ targetCpu.cpu.getOrCreateCounter('', 'Power Frequency');
+ if (powerCounter.numSeries == 0) {
+ powerCounter.seriesNames.push('state');
+ powerCounter.seriesColors.push(
+ tracing.getStringColorId(powerCounter.name + '.' + 'state'));
+ }
+ var powerState = parseInt(event[2]);
+ powerCounter.timestamps.push(ts);
+ powerCounter.samples.push(powerState);
+ } else if (eventName == 'workqueue_execute_start') {
+ var event = workqueueExecuteStartRE.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed workqueue_execute_start event');
+ continue;
+ }
+ var kthread = this.getOrCreateKernelThread(eventBase[1]);
+ kthread.openSliceTS = ts;
+ kthread.openSlice = event[2];
+
+ } else if (eventName == 'workqueue_execute_end') {
+ var event = workqueueExecuteEndRE.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed workqueue_execute_start event');
+ continue;
+ }
+ var kthread = this.getOrCreateKernelThread(eventBase[1]);
+ if (kthread.openSlice) {
+ var slice = new tracing.TimelineSlice(kthread.openSlice,
+ tracing.getStringColorId(kthread.openSlice),
+ kthread.openSliceTS,
+ {},
+ ts - kthread.openSliceTS);
+
+ kthread.thread.subRows[0].push(slice);
+ }
+ kthread.openSlice = undefined;
+
+ } else if (eventName == '0') { // trace_mark's show up with 0 prefixes.
+ var event = traceEventClockSyncRE.exec(eventBase[5]);
+ if (event)
+ this.clockSyncRecords_.push({
+ perfTS: ts,
+ parentTS: event[1] * 1000
+ });
+ else
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Unrecognized event: ' + eventBase[5]);
+ }
+ }
+ }
+ };
+
+ tracing.TimelineModel.registerImporter(LinuxPerfImporter);
+
+ return {
+ LinuxPerfImporter: LinuxPerfImporter,
+ _LinuxPerfImporterTestExports: TestExports
+ };
+
+});
diff --git a/chrome/browser/resources/tracing/linux_perf_importer_test.html b/chrome/browser/resources/tracing/linux_perf_importer_test.html
new file mode 100644
index 0000000..ccdac64
--- /dev/null
+++ b/chrome/browser/resources/tracing/linux_perf_importer_test.html
@@ -0,0 +1,220 @@
+<!DOCTYPE html>
+<html>
+<!--
+Copyright (c) 2011 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.
+-->
+<head>
+<title>Perf Importer tests</title>
+<script src=
+ "http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js">
+</script>
+<script src="../shared/js/cr.js"></script>
+<script src="../shared/js/cr/event_target.js"></script>
+<script src="test_utils.js"></script>
+<script src="timeline_model.js"></script>
+<script src="linux_perf_importer.js"></script>
+<script>
+ goog.require('goog.testing.jsunit');
+</script>
+
+</head>
+<body>
+<script>
+function testLineRE() {
+ var re = tracing._LinuxPerfImporterTestExports.lineRE;
+ var x = re.exec(' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112');
+ assertNotNull(x);
+ assertEquals('<idle>-0', x[1]);
+ assertEquals('001', x[2]);
+ assertEquals('4467.843475', x[3]);
+ assertEquals('sched_switch', x[4]);
+ assertEquals('prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112', x[5]);
+
+ var x = re.exec('Binder Thread #-647 [001] 260.464294: sched_switch: ' +
+ 'prev_comm=Binder Thread # prev_pid=647 prev_prio=120 prev_state=D ==> ' +
+ 'next_comm=.android.chrome next_pid=1562 next_prio=120');
+ assertNotNull(x);
+}
+
+function testSchedSwitchRE() {
+ var re = tracing._LinuxPerfImporterTestExports.schedSwitchRE;
+ var x = re.exec('prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ' +
+ '==> next_comm=SurfaceFlinger next_pid=178 next_prio=112');
+ assertNotNull(x);
+ assertEquals('swapper', x[1]);
+ assertEquals('0', x[2]);
+ assertEquals('120', x[3]);
+ assertEquals('R', x[4]);
+ assertEquals('SurfaceFlinger', x[5]);
+ assertEquals('178', x[6]);
+ assertEquals('112', x[7]);
+
+ var x = re.exec('prev_comm=.android.chrome prev_pid=1562 prev_prio=120 ' +
+ 'prev_state=R ==> next_comm=Binder Thread # next_pid=195 next_prio=120');
+ assertNotNull(x);
+ assertEquals('.android.chrome', x[1]);
+ assertEquals('Binder Thread #', x[5]);
+
+ var x = re.exec('prev_comm=Binder Thread # prev_pid=1562 prev_prio=120 ' +
+ 'prev_state=R ==> next_comm=.android.chrome next_pid=195 next_prio=120');
+ assertNotNull(x);
+ assertEquals('Binder Thread #', x[1]);
+ assertEquals('.android.chrome', x[5]);
+}
+
+function testSchedWakeupRE() {
+ var re = tracing._LinuxPerfImporterTestExports.schedWakeupRE;
+ var x = re.exec(
+ 'comm=SensorService pid=207 prio=112 success=1 target_cpu=000');
+ assertNotNull(x);
+ assertEquals('SensorService', x[1]);
+ assertEquals('207', x[2]);
+ assertEquals('112', x[3]);
+ assertEquals('1', x[4]);
+ assertEquals('000', x[5]);
+}
+
+function testTraceEventClockSyncRE() {
+ var re = tracing._LinuxPerfImporterTestExports.traceEventClockSyncRE;
+ var x = re.exec('trace_event_clock_sync: parent_ts=19581477508');
+ assertNotNull(x);
+ assertEquals('19581477508', x[1]);
+
+ var x = re.exec('trace_event_clock_sync: parent_ts=123.456');
+ assertNotNull(x);
+ assertEquals('123.456', x[1]);
+}
+
+function testCanImport() {
+ lines = [
+ '# tracer: nop',
+ '#',
+ '# TASK-PID CPU# TIMESTAMP FUNCTION',
+ '# | | | | |',
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+
+ ' SurfaceFlinger-178 [001] 4467.843536: sched_switch: ' +
+ 'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 prev_state=S ' +
+ '==> next_comm=kworker/u:2 next_pid=2844 next_prio=120',
+
+ ' kworker/u:2-2844 [001] 4467.843567: sched_switch: ' +
+ 'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 prev_state=S ' +
+ '==> next_comm=swapper next_pid=0 next_prio=120',
+
+ ' <idle>-0 [001] 4467.844208: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=kworker/u:2 next_pid=2844 next_prio=120'
+ ];
+ assertTrue(tracing.LinuxPerfImporter.canImport(lines.join('\n')));
+
+ lines = [
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112'
+ ];
+ assertTrue(tracing.LinuxPerfImporter.canImport(lines.join('\n')));
+
+ lines = [
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+
+ ' SurfaceFlinger-178 [001] 4467.843536: sched_switch: ' +
+ 'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 ' +
+ 'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+ 'next_prio=120'
+ ];
+ assertTrue(tracing.LinuxPerfImporter.canImport(lines.join('\n')));
+
+ lines = [
+ 'SomeRandomText',
+ 'More random text'
+ ];
+ assertFalse(tracing.LinuxPerfImporter.canImport(lines.join('\n')));
+}
+
+function testImportOneSequence() {
+ lines = [
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+ 'next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+
+ ' SurfaceFlinger-178 [001] 4467.843536: sched_switch: ' +
+ 'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 ' +
+ 'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+ 'next_prio=120',
+
+ ' kworker/u:2-2844 [001] 4467.843567: sched_switch: ' +
+ 'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 ' +
+ 'prev_state=S ==> next_comm=swapper next_pid=0 next_prio=120'
+ ];
+ var m = new tracing.TimelineModel(lines.join('\n'), false);
+ assertEquals(0, m.importErrors.length);
+
+ var c = m.cpus[1];
+ assertEquals(2, c.slices.length);
+
+ assertEquals('SurfaceFlinger', c.slices[0].title);
+ assertEquals(4467843.475, c.slices[0].start);
+ assertAlmostEquals(.536 - .475, c.slices[0].duration);
+}
+
+function testImportOneSequenceWithSchedWakeUp() {
+ // TODO(nduca): write test for this.
+}
+
+function testImportWithNewline() {
+ lines = [
+ ''
+ ];
+ var m = new tracing.TimelineModel(lines.join('\n'));
+ assertEquals(0, m.importErrors.length);
+}
+
+function testClockSync() {
+ lines = [
+ ' <idle>-0 [001] 4467.843475: sched_switch: ' +
+ 'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ' +
+ '==> next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+ ' SurfaceFlinger-178 [001] 4467.843536: sched_switch: ' +
+ 'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 ' +
+ 'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+ 'next_prio=120',
+ ' kworker/u:2-2844 [001] 4467.843567: sched_switch: ' +
+ 'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 ' +
+ 'prev_state=S ==> next_comm=swapper next_pid=0 ' +
+ 'next_prio=120',
+ ' kworker/u:2-2844 [001] 4467.843000: 0: ' +
+ 'trace_event_clock_sync: parent_ts=0.0'
+ ];
+ var m = new tracing.TimelineModel(lines.join('\n'), false);
+ assertEquals(0, m.importErrors.length);
+
+ var c = m.cpus[1];
+ assertEquals(2, c.slices.length);
+
+ assertAlmostEquals((467.843475 - 467.843) * 1000, c.slices[0].start);
+}
+
+function testImportWithoutClockSyncDeletesEverything() {
+}
+
+function testWorkQueueImport() {
+}
+
+function testPowerStartImport() {
+}
+
+function testCpuFrequencyImport() {
+}
+
+</script>
+</body>
+</html>
diff --git a/chrome/browser/resources/tracing/profiling_view.js b/chrome/browser/resources/tracing/profiling_view.js
index 11cd270..26ea755 100644
--- a/chrome/browser/resources/tracing/profiling_view.js
+++ b/chrome/browser/resources/tracing/profiling_view.js
@@ -72,7 +72,7 @@ cr.define('tracing', function() {
this.saveBn_.disabled = !hasEvents;
- this.timelineView_.traceEvents = this.traceEvents_;
+ this.timelineView_.traceData = this.traceEvents_;
},
onKeypress_: function(event) {
diff --git a/chrome/browser/resources/tracing/test_utils.js b/chrome/browser/resources/tracing/test_utils.js
index 09fc2d4..8ae3c83 100644
--- a/chrome/browser/resources/tracing/test_utils.js
+++ b/chrome/browser/resources/tracing/test_utils.js
@@ -5,19 +5,31 @@
/**
* @fileoverview Helper functions for use in tracing tests.
*/
+
+
+/**
+ * goog.testing.assertion's assertEquals tweaked to do equality-to-a-constant.
+ * @param {*} a First value.
+ * @param {*} b Second value.
+ */
+function assertAlmostEquals(a, b) {
+ _validateArguments(2, arguments);
+ var var1 = nonCommentArg(1, 2, arguments);
+ var var2 = nonCommentArg(2, 2, arguments);
+ _assert(commentArg(2, arguments), Math.abs(var1 - var2) < 0.00001,
+ 'Expected ' + _displayStringForValue(var1) + ' but was ' +
+ _displayStringForValue(var2));
+}
+
cr.define('test_utils', function() {
- function getJSON(url, cb) {
+ function getAsync(url, cb) {
var req = new XMLHttpRequest();
req.open('GET', url, true);
req.onreadystatechange = function(aEvt) {
if (req.readyState == 4) {
window.setTimeout(function() {
if (req.status == 200) {
- var resp = JSON.parse(req.responseText);
- if (resp.traceEvents)
- cb(resp.traceEvents);
- else
- cb(resp);
+ cb(req.responseText);
} else {
console.log('Failed to load ' + url);
}
@@ -27,6 +39,6 @@ cr.define('test_utils', function() {
req.send(null);
}
return {
- getJSON: getJSON
+ getAsync: getAsync
};
});
diff --git a/chrome/browser/resources/tracing/timeline.css b/chrome/browser/resources/tracing/timeline.css
index 64a6507..a92cf35 100644
--- a/chrome/browser/resources/tracing/timeline.css
+++ b/chrome/browser/resources/tracing/timeline.css
@@ -46,6 +46,7 @@ found in the LICENSE file.
.timeline-canvas-based-track-canvas {
-webkit-box-flex: 1;
+ display: block;
height: 100%;
width: 100%;
}
diff --git a/chrome/browser/resources/tracing/timeline.js b/chrome/browser/resources/tracing/timeline.js
index efd31c35..4e94e63 100644
--- a/chrome/browser/resources/tracing/timeline.js
+++ b/chrome/browser/resources/tracing/timeline.js
@@ -308,6 +308,9 @@ cr.define('tracing', function() {
model.getAllCounters().forEach(function(c) {
allHeadings.push(c.name);
});
+ model.getAllCpus().forEach(function(c) {
+ allHeadings.push('CPU ' + c.cpuNumber);
+ });
// Figure out the maximum heading size.
var maxHeadingWidth = 0;
@@ -331,10 +334,32 @@ cr.define('tracing', function() {
this.tracks_.children[i].detach();
this.tracks_.textContent = '';
+ // Get a sorted list of CPUs
+ var cpus = model.getAllCpus();
+ cpus.sort(tracing.TimelineCpu.compare);
+
+ // Create tracks for each CPU.
+ cpus.forEach(function(cpu) {
+ var track = new tracing.TimelineCpuTrack();
+ track.heading = 'CPU ' + cpu.cpuNumber + ':';
+ track.headingWidth = maxHeadingWidth;
+ track.viewport = this.viewport_;
+ track.cpu = cpu;
+ this.tracks_.appendChild(track);
+
+ for (var counterName in cpu.counters) {
+ var counter = cpu.counters[counterName];
+ track = new tracing.TimelineCounterTrack();
+ track.heading = 'CPU ' + cpu.cpuNumber + ' ' + counter.name + ':';
+ track.headingWidth = maxHeadingWidth;
+ track.viewport = this.viewport_;
+ track.counter = counter;
+ this.tracks_.appendChild(track);
+ }
+ }.bind(this));
+
// Get a sorted list of processes.
- var processes = [];
- for (var pid in model.processes)
- processes.push(model.processes[pid]);
+ var processes = model.getAllProcesses();
processes.sort(tracing.TimelineProcess.compare);
// Create tracks for each process.
@@ -630,12 +655,13 @@ cr.define('tracing', function() {
},
onMouseDown_: function(e) {
- rect = this.getClientRects()[0];
- if (!rect ||
- e.clientX < rect.left ||
- e.clientX >= rect.right ||
- e.clientY < rect.top ||
- e.clientY >= rect.bottom)
+ rect = this.tracks_.getClientRects()[0];
+ var inside = rect &&
+ e.clientX >= rect.left &&
+ e.clientX < rect.right &&
+ e.clientY >= rect.top &&
+ e.clientY < rect.bottom;
+ if (!inside)
return;
var canv = this.firstCanvas;
diff --git a/chrome/browser/resources/tracing/timeline_model.js b/chrome/browser/resources/tracing/timeline_model.js
index b7c90be..f788350 100644
--- a/chrome/browser/resources/tracing/timeline_model.js
+++ b/chrome/browser/resources/tracing/timeline_model.js
@@ -21,11 +21,18 @@
*/
cr.define('tracing', function() {
/**
- * A TimelineSlice represents an interval of time on a given thread
- * associated with a specific trace event. For example,
+ * A TimelineSlice represents an interval of time on a given resource plus
+ * parameters associated with that interval.
+ *
+ * A slice is typically associated with a specific trace event pair on a
+ * specific thread.
+ * For example,
* TRACE_EVENT_BEGIN1("x","myArg", 7) at time=0.1ms
* TRACE_EVENT_END() at time=0.3ms
- * Results in a single timeline slice from 0.1 with duration 0.2.
+ * This results in a single timeline slice from 0.1 with duration 0.2 on a
+ * specific thread.
+ *
+ * A slice can also be an interval of time on a Cpu on a TimelineCpu.
*
* All time units are stored in milliseconds.
* @constructor
@@ -282,6 +289,131 @@ cr.define('tracing', function() {
};
/**
+ * The TimelineCpu represents a Cpu from the kernel's point of view.
+ * @constructor
+ */
+ function TimelineCpu(number) {
+ this.cpuNumber = number;
+ this.slices = [];
+ this.counters = {};
+ };
+
+ TimelineCpu.prototype = {
+ /**
+ * @return {TimlineCounter} The counter on this process named 'name',
+ * creating it if it doesn't exist.
+ */
+ getOrCreateCounter: function(cat, name) {
+ var id;
+ if (cat.length)
+ id = cat + '.' + name;
+ else
+ id = name;
+ if (!this.counters[id])
+ this.counters[id] = new TimelineCounter(this, id, name);
+ return this.counters[id];
+ },
+
+ /**
+ * Updates the minTimestamp and maxTimestamp fields based on the
+ * current slices attached to the cpu.
+ */
+ updateBounds: function() {
+ var values = [];
+ if (this.slices.length) {
+ this.minTimestamp = this.slices[0].start;
+ this.maxTimestamp = this.slices[this.slices.length - 1].end;
+ } else {
+ this.minTimestamp = undefined;
+ this.maxTimestamp = undefined;
+ }
+ }
+ };
+
+ /**
+ * Comparison between processes that orders by cpuNumber.
+ */
+ TimelineCpu.compare = function(x, y) {
+ return x.cpuNumber - y.cpuNumber;
+ };
+
+ // The color pallette is split in half, with the upper
+ // half of the pallette being the "highlighted" verison
+ // of the base color. So, color 7's highlighted form is
+ // 7 + (pallette.length / 2).
+ //
+ // These bright versions of colors are automatically generated
+ // from the base colors.
+ //
+ // Within the color pallette, there are "regular" colors,
+ // which can be used for random color selection, and
+ // reserved colors, which are used when specific colors
+ // need to be used, e.g. where red is desired.
+ const palletteBase = [
+ {r: 138, g: 113, b: 152},
+ {r: 175, g: 112, b: 133},
+ {r: 127, g: 135, b: 225},
+ {r: 93, g: 81, b: 137},
+ {r: 116, g: 143, b: 119},
+ {r: 178, g: 214, b: 122},
+ {r: 87, g: 109, b: 147},
+ {r: 119, g: 155, b: 95},
+ {r: 114, g: 180, b: 160},
+ {r: 132, g: 85, b: 103},
+ {r: 157, g: 210, b: 150},
+ {r: 148, g: 94, b: 86},
+ {r: 164, g: 108, b: 138},
+ {r: 139, g: 191, b: 150},
+ {r: 110, g: 99, b: 145},
+ {r: 80, g: 129, b: 109},
+ {r: 125, g: 140, b: 149},
+ {r: 93, g: 124, b: 132},
+ {r: 140, g: 85, b: 140},
+ {r: 104, g: 163, b: 162},
+ {r: 132, g: 141, b: 178},
+ {r: 131, g: 105, b: 147},
+ {r: 135, g: 183, b: 98},
+ {r: 152, g: 134, b: 177},
+ {r: 141, g: 188, b: 141},
+ {r: 133, g: 160, b: 210},
+ {r: 126, g: 186, b: 148},
+ {r: 112, g: 198, b: 205},
+ {r: 180, g: 122, b: 195},
+ {r: 203, g: 144, b: 152},
+ // Reserved Entires
+ {r: 182, g: 125, b: 143},
+ {r: 126, g: 200, b: 148},
+ {r: 133, g: 160, b: 210},
+ {r: 240, g: 240, b: 240}];
+
+ // Make sure this number tracks the number of reserved entries in the
+ // pallette.
+ const numReservedColorIds = 4;
+
+ function brighten(c) {
+ var k;
+ if (c.r >= 240 && c.g >= 240 && c.b >= 240)
+ k = -0.20;
+ else
+ k = 0.45;
+
+ return {r: Math.min(255, c.r + Math.floor(c.r * k)),
+ g: Math.min(255, c.g + Math.floor(c.g * k)),
+ b: Math.min(255, c.b + Math.floor(c.b * k))};
+ }
+ function colorToString(c) {
+ return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')';
+ }
+
+ /**
+ * The number of color IDs that getStringColorId can choose from.
+ */
+ const numRegularColorIds = palletteBase.length - numReservedColorIds;
+ const highlightIdBoost = palletteBase.length;
+
+ const pallette = palletteBase.concat(palletteBase.map(brighten)).
+ map(colorToString);
+ /**
* Computes a simplistic hashcode of the provide name. Used to chose colors
* for slices.
* @param {string} name The string to hash.
@@ -294,31 +426,82 @@ cr.define('tracing', function() {
}
/**
- * The number of color IDs that getStringColorId can choose from.
+ * Gets the color pallette.
+ */
+ function getPallette() {
+ return pallette;
+ }
+
+ /**
+ * @return {Number} The value to add to a color ID to get its highlighted
+ * colro ID. E.g. 7 + getPalletteHighlightIdBoost() yields a brightened from
+ * of 7's base color.
*/
- const numColorIds = 30;
+ function getPalletteHighlightIdBoost() {
+ return highlightIdBoost;
+ }
+
+ /**
+ * @param {String} name The color name.
+ * @return {Number} The color ID for the given color name.
+ */
+ function getColorIdByName(name) {
+ if (name == 'iowait')
+ return numRegularColorIds;
+ if (name == 'running')
+ return numRegularColorIds + 1;
+ if (name == 'runnable')
+ return numRegularColorIds + 2;
+ if (name == 'sleeping')
+ return numRegularColorIds + 3;
+ throw 'Unrecognized color ' + name;
+ }
+
+ // Previously computed string color IDs. They are based on a stable hash, so
+ // it is safe to save them throughout the program time.
+ var stringColorIdCache = {};
/**
* @return {Number} A color ID that is stably associated to the provided via
- * the getStringHash method.
+ * the getStringHash method. The color ID will be chosen from the regular
+ * ID space only, e.g. no reserved ID will be used.
*/
function getStringColorId(string) {
- var hash = getStringHash(string);
- return hash % numColorIds;
+ if (stringColorIdCache[string] === undefined) {
+ var hash = getStringHash(string);
+ stringColorIdCache[string] = hash % numRegularColorIds;
+ }
+ return stringColorIdCache[string];
}
/**
* Builds a model from an array of TraceEvent objects.
- * @param {Array} events An array of TraceEvents created by
- * TraceEvent.ToJSON().
+ * @param {Object=} opt_data The event data to import into the new model.
+ * See TimelineModel.importEvents for details and more advanced ways to
+ * import data.
+ * @param {bool=} opt_zeroAndBoost Whether to align to zero and boost the
+ * by 15%. Defaults to true.
* @constructor
*/
- function TimelineModel(events) {
+ function TimelineModel(opt_eventData, opt_zeroAndBoost) {
+ this.cpus = {};
this.processes = {};
this.importErrors = [];
- if (events)
- this.importEvents(events);
+ if (opt_eventData)
+ this.importEvents(opt_eventData, opt_zeroAndBoost);
+ }
+
+ var importerConstructors = [];
+
+ /**
+ * Registers an importer. All registered importers are considered
+ * when processing an import request.
+ *
+ * @param {Function} importerConstructor The importer's constructor function.
+ */
+ TimelineModel.registerImporter = function(importerConstructor) {
+ importerConstructors.push(importerConstructor);
}
TimelineModel.prototype = {
@@ -331,6 +514,20 @@ cr.define('tracing', function() {
return n;
},
+ /**
+ * @return {TimelineProcess} Gets a specific TimelineCpu or creates one if
+ * it does not exist.
+ */
+ getOrCreateCpu: function(cpuNumber) {
+ if (!this.cpus[cpuNumber])
+ this.cpus[cpuNumber] = new TimelineCpu(cpuNumber);
+ return this.cpus[cpuNumber];
+ },
+
+ /**
+ * @return {TimelineProcess} Gets a TimlineProcess for a specified pid or
+ * creates one if it does not exist.
+ */
getOrCreateProcess: function(pid) {
if (!this.processes[pid])
this.processes[pid] = new TimelineProcess(pid);
@@ -341,223 +538,6 @@ cr.define('tracing', function() {
* The import takes an array of json-ified TraceEvents and adds them into
* the TimelineModel as processes, threads, and slices.
*/
- importEvents: function(events) {
- // A ptid is a pid and tid joined together x:y fashion, eg 1024:130
- // The ptid is a unique key for a thread in the trace.
- this.importErrors = [];
-
- // Threadstate.
- function ThreadState(tid) {
- this.openSlices = [];
- this.openNonNestedSlices = {};
- }
- var threadStateByPTID = {};
-
- var nameToColorMap = {};
- function getColor(name) {
- if (!(name in nameToColorMap)) {
- nameToColorMap[name] = getStringColorId(name);
- }
- return nameToColorMap[name];
- }
-
- var self = this;
-
- /**
- * Helper to process a 'begin' event (e.g. initiate a slice).
- * @param {ThreadState} state Thread state (holds slices).
- * @param {Object} event The current trace event.
- */
- function processBegin(state, event) {
- var colorId = getColor(event.name);
- var slice =
- { index: eI,
- slice: new TimelineSlice(event.name, colorId, event.ts,
- event.args) };
-
- if (event.uts)
- slice.slice.startInUserTime = event.uts;
-
- if (event.args['ui-nest'] === '0') {
- var sliceID = event.name;
- for (var x in event.args)
- sliceID += ';' + event.args[x];
- if (state.openNonNestedSlices[sliceID])
- this.importErrors.push('Event ' + sliceID + ' already open.');
- state.openNonNestedSlices[sliceID] = slice;
- } else {
- state.openSlices.push(slice);
- }
- }
-
- /**
- * Helper to process an 'end' event (e.g. close a slice).
- * @param {ThreadState} state Thread state (holds slices).
- * @param {Object} event The current trace event.
- */
- function processEnd(state, event) {
- if (event.args['ui-nest'] === '0') {
- var sliceID = event.name;
- for (var x in event.args)
- sliceID += ';' + event.args[x];
- var slice = state.openNonNestedSlices[sliceID];
- if (!slice)
- return;
- slice.slice.duration = event.ts - slice.slice.start;
- if (event.uts)
- slice.durationInUserTime = event.uts - slice.slice.startInUserTime;
-
- // Store the slice in a non-nested subrow.
- var thread =
- self.getOrCreateProcess(event.pid).getOrCreateThread(event.tid);
- thread.addNonNestedSlice(slice.slice);
- delete state.openNonNestedSlices[name];
- } else {
- if (state.openSlices.length == 0) {
- // Ignore E events that are unmatched.
- return;
- }
- var slice = state.openSlices.pop().slice;
- slice.duration = event.ts - slice.start;
- if (event.uts)
- slice.durationInUserTime = event.uts - slice.startInUserTime;
-
- // Store the slice on the correct subrow.
- var thread = self.getOrCreateProcess(event.pid)
- .getOrCreateThread(event.tid);
- var subRowIndex = state.openSlices.length;
- thread.getSubrow(subRowIndex).push(slice);
-
- // Add the slice to the subSlices array of its parent.
- if (state.openSlices.length) {
- var parentSlice = state.openSlices[state.openSlices.length - 1];
- parentSlice.slice.subSlices.push(slice);
- }
- }
- }
-
- // Walk through events
- for (var eI = 0; eI < events.length; eI++) {
- var event = events[eI];
- var ptid = event.pid + ':' + event.tid;
-
- if (!(ptid in threadStateByPTID))
- threadStateByPTID[ptid] = new ThreadState();
- var state = threadStateByPTID[ptid];
-
- if (event.ph == 'B') {
- processBegin(state, event);
- } else if (event.ph == 'E') {
- processEnd(state, event);
- } else if (event.ph == 'I') {
- // Treat an Instant event as a duration 0 slice.
- // TimelineSliceTrack's redraw() knows how to handle this.
- processBegin(state, event);
- processEnd(state, event);
- } else if (event.ph == 'C') {
- var ctr_name;
- if (event.id !== undefined)
- ctr_name = event.name + '[' + event.id + ']';
- else
- ctr_name = event.name;
-
- var ctr = this.getOrCreateProcess(event.pid)
- .getOrCreateCounter(event.cat, ctr_name);
- // Initialize the counter's series fields if needed.
- if (ctr.numSeries == 0) {
- for (var seriesName in event.args) {
- ctr.seriesNames.push(seriesName);
- ctr.seriesColors.push(
- getStringColorId(ctr.name + '.' + seriesName));
- }
- if (ctr.numSeries == 0) {
- this.importErrors.push('Expected counter ' + event.name +
- ' to have at least one argument to use as a value.');
- // Drop the counter.
- delete ctr.parent.counters[ctr.name];
- continue;
- }
- }
-
- // Add the sample values.
- ctr.timestamps.push(event.ts);
- for (var i = 0; i < ctr.numSeries; i++) {
- var seriesName = ctr.seriesNames[i];
- if (event.args[seriesName] === undefined) {
- ctr.samples.push(0);
- continue;
- }
- ctr.samples.push(event.args[seriesName]);
- }
-
- } else if (event.ph == 'M') {
- if (event.name == 'thread_name') {
- var thread = this.getOrCreateProcess(event.pid)
- .getOrCreateThread(event.tid);
- thread.name = event.args.name;
- } else {
- this.importErrors.push('Unrecognized metadata name: ' + event.name);
- }
- } else {
- this.importErrors.push('Unrecognized event phase: ' + event.ph +
- '(' + event.name + ')');
- }
- }
- this.pruneEmptyThreads();
- this.updateBounds();
-
- // Adjust the model's max value temporarily to include the max value of
- // any of the open slices, since they wouldn't have been included in the
- // bounds calculation. We need the true global max value because the
- // duration for any open slices is set so that they end at this global
- // maximum.
- for (var ptid in threadStateByPTID) {
- var state = threadStateByPTID[ptid];
- for (var i = 0; i < state.openSlices.length; i++) {
- var slice = state.openSlices[i];
- this.minTimestamp = Math.min(this.minTimestamp, slice.slice.start);
- this.maxTimestamp = Math.max(this.maxTimestamp, slice.slice.start);
- for (var s = 0; s < slice.slice.subSlices.length; s++) {
- var subSlice = slice.slice.subSlices[s];
- this.minTimestamp = Math.min(this.minTimestamp, subSlice.start);
- this.maxTimestamp = Math.max(this.maxTimestamp, subSlice.start);
- if (subSlice.duration)
- this.maxTimestamp = Math.max(this.maxTimestamp, subSlice.end);
- }
- }
- }
-
- // Automatically close any slices are still open. These occur in a number
- // of reasonable situations, e.g. deadlock. This pass ensures the open
- // slices make it into the final model.
- for (var ptid in threadStateByPTID) {
- var state = threadStateByPTID[ptid];
- while (state.openSlices.length > 0) {
- var slice = state.openSlices.pop();
- slice.slice.duration = this.maxTimestamp - slice.slice.start;
- slice.slice.didNotFinish = true;
- var event = events[slice.index];
-
- // Store the slice on the correct subrow.
- var thread = this.getOrCreateProcess(event.pid)
- .getOrCreateThread(event.tid);
- var subRowIndex = state.openSlices.length;
- thread.getSubrow(subRowIndex).push(slice.slice);
-
- // Add the slice to the subSlices array of its parent.
- if (state.openSlices.length) {
- var parentSlice = state.openSlices[state.openSlices.length - 1];
- parentSlice.slice.subSlices.push(slice.slice);
- }
- }
- }
-
- this.shiftWorldToMicroseconds();
-
- var boost = (this.maxTimestamp - this.minTimestamp) * 0.15;
- this.minTimestamp = this.minTimestamp - boost;
- this.maxTimestamp = this.maxTimestamp + boost;
- },
/**
* Removes threads from the model that are fully empty.
@@ -588,6 +568,8 @@ cr.define('tracing', function() {
updateBounds: function() {
var wmin = Infinity;
var wmax = -wmin;
+ var hasData = false;
+
var threads = this.getAllThreads();
for (var tI = 0; tI < threads.length; tI++) {
var thread = threads[tI];
@@ -596,6 +578,7 @@ cr.define('tracing', function() {
thread.maxTimestamp != undefined) {
wmin = Math.min(wmin, thread.minTimestamp);
wmax = Math.max(wmax, thread.maxTimestamp);
+ hasData = true;
}
}
var counters = this.getAllCounters();
@@ -604,15 +587,35 @@ cr.define('tracing', function() {
counter.updateBounds();
if (counter.minTimestamp != undefined &&
counter.maxTimestamp != undefined) {
+ hasData = true;
wmin = Math.min(wmin, counter.minTimestamp);
wmax = Math.max(wmax, counter.maxTimestamp);
}
}
- this.minTimestamp = wmin;
- this.maxTimestamp = wmax;
+
+ for (var cpuNumber in this.cpus) {
+ var cpu = this.cpus[cpuNumber];
+ cpu.updateBounds();
+ if (cpu.minTimestamp != undefined &&
+ cpu.maxTimestamp != undefined) {
+ hasData = true;
+ wmin = Math.min(wmin, cpu.minTimestamp);
+ wmax = Math.max(wmax, cpu.maxTimestamp);
+ }
+ }
+
+ if (hasData) {
+ this.minTimestamp = wmin;
+ this.maxTimestamp = wmax;
+ } else {
+ this.maxTimestamp = undefined;
+ this.minTimestamp = undefined;
+ }
},
- shiftWorldToMicroseconds: function() {
+ shiftWorldToZero: function() {
+ if (this.minTimestamp === undefined)
+ return;
var timeBase = this.minTimestamp;
var threads = this.getAllThreads();
for (var tI = 0; tI < threads.length; tI++) {
@@ -620,14 +623,13 @@ cr.define('tracing', function() {
var shiftSubRow = function(subRow) {
for (var tS = 0; tS < subRow.length; tS++) {
var slice = subRow[tS];
- slice.start = (slice.start - timeBase) / 1000;
- slice.duration /= 1000;
- if (slice.startInUserTime)
- slice.startInUserTime /= 1000;
- if (slice.durationInUserTime)
- slice.durationInUserTime /= 1000;
+ slice.start = (slice.start - timeBase);
}
};
+
+ if (thread.cpuSlices)
+ shiftSubRow(thread.cpuSlices);
+
for (var tSR = 0; tSR < thread.subRows.length; tSR++) {
shiftSubRow(thread.subRows[tSR]);
}
@@ -639,7 +641,13 @@ cr.define('tracing', function() {
for (var tI = 0; tI < counters.length; tI++) {
var counter = counters[tI];
for (var sI = 0; sI < counter.timestamps.length; sI++)
- counter.timestamps[sI] = (counter.timestamps[sI] - timeBase) / 1000;
+ counter.timestamps[sI] = (counter.timestamps[sI] - timeBase);
+ }
+ var cpus = this.getAllCpus();
+ for (var tI = 0; tI < cpus.length; tI++) {
+ var cpu = cpus[tI];
+ for (var sI = 0; sI < cpu.slices.length; sI++)
+ cpu.slices[sI].start = (cpu.slices[sI].start - timeBase);
}
this.updateBounds();
},
@@ -656,6 +664,26 @@ cr.define('tracing', function() {
},
/**
+ * @return {Array} An array of all cpus in the model.
+ */
+ getAllCpus: function() {
+ var cpus = [];
+ for (var cpu in this.cpus)
+ cpus.push(this.cpus[cpu]);
+ return cpus;
+ },
+
+ /**
+ * @return {Array} An array of all processes in the model.
+ */
+ getAllProcesses: function() {
+ var processes = [];
+ for (var pid in this.processes)
+ processes.push(this.processes[pid]);
+ return processes;
+ },
+
+ /**
* @return {Array} An array of all the counters in the model.
*/
getAllCounters: function() {
@@ -666,18 +694,97 @@ cr.define('tracing', function() {
counters.push(process.counters[tid]);
}
}
+ for (var cpuNumber in this.cpus) {
+ var cpu = this.cpus[cpuNumber];
+ for (var counterName in cpu.counters)
+ counters.push(cpu.counters[counterName]);
+ }
return counters;
- }
+ },
+ /**
+ * Imports the provided events into the model. The eventData type
+ * is undefined and will be passed to all the timeline importers registered
+ * via TimelineModel.registerImporter. The first importer that returns true
+ * for canImport(events) will be used to import the events.
+ *
+ * @param {Object} events Events to import.
+ * @param {boolean} isChildImport True the eventData being imported is an
+ * additional trace after the primary eventData.
+ */
+ importOneTrace_: function(eventData, isAdditionalImport) {
+ var importerConstructor;
+ for (var i = 0; i < importerConstructors.length; ++i) {
+ if (importerConstructors[i].canImport(eventData)) {
+ importerConstructor = importerConstructors[i];
+ break;
+ }
+ }
+ if (!importerConstructor)
+ throw 'Could not find an importer for the provided eventData.';
+
+ var importer = new importerConstructor(
+ this, eventData, isAdditionalImport);
+ importer.importEvents();
+ this.pruneEmptyThreads();
+ },
+
+ /**
+ * Imports the provided traces into the model. The eventData type
+ * is undefined and will be passed to all the timeline importers registered
+ * via TimelineModel.registerImporter. The first importer that returns true
+ * for canImport(events) will be used to import the events.
+ *
+ * The primary trace is provided via the eventData variable. If multiple
+ * traces are to be imported, specify the first one as events, and the
+ * remainder in the opt_additionalEventData array.
+ *
+ * @param {Object} eventData Events to import.
+ * @param {bool=} opt_zeroAndBoost Whether to align to zero and boost the
+ * by 15%. Defaults to true.
+ * @param {Array=} opt_additionalEventData An array of eventData objects
+ * (e.g. array of arrays) to
+ * import after importing the primary events.
+ */
+ importEvents: function(eventData,
+ opt_zeroAndBoost, opt_additionalEventData) {
+ if (opt_zeroAndBoost === undefined)
+ opt_zeroAndBoost = true;
+
+ this.importOneTrace_(eventData, false);
+ if (opt_additionalEventData) {
+ for (var i = 0; i < opt_additionalEventData.length; ++i) {
+ this.importOneTrace_(opt_additionalEventData[i], true);
+ }
+ }
+
+ this.updateBounds();
+
+ if (opt_zeroAndBoost)
+ this.shiftWorldToZero();
+
+ if (opt_zeroAndBoost &&
+ this.minTimestamp !== undefined &&
+ this.maxTimestamp !== undefined) {
+ var boost = (this.maxTimestamp - this.minTimestamp) * 0.15;
+ this.minTimestamp = this.minTimestamp - boost;
+ this.maxTimestamp = this.maxTimestamp + boost;
+ }
+ }
};
return {
+ getPallette: getPallette,
+ getPalletteHighlightIdBoost: getPalletteHighlightIdBoost,
+ getColorIdByName: getColorIdByName,
getStringHash: getStringHash,
getStringColorId: getStringColorId,
+
TimelineSlice: TimelineSlice,
TimelineThread: TimelineThread,
TimelineCounter: TimelineCounter,
TimelineProcess: TimelineProcess,
+ TimelineCpu: TimelineCpu,
TimelineModel: TimelineModel
};
});
diff --git a/chrome/browser/resources/tracing/timeline_model_test.html b/chrome/browser/resources/tracing/timeline_model_test.html
index f0a7a5a..2889cd1 100644
--- a/chrome/browser/resources/tracing/timeline_model_test.html
+++ b/chrome/browser/resources/tracing/timeline_model_test.html
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<!--
-Copyright (c) 2010 The Chromium Authors. All rights reserved.
+Copyright (c) 2011 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.
-->
@@ -18,475 +18,101 @@ found in the LICENSE file.
</head>
<body>
<script>
-function testBasicSingleThreadNonnestedParsing() {
- var events = [
- {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
- {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'},
- {name: 'b', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'B'},
- {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'}
- ];
- var m = new tracing.TimelineModel(events);
- assertEquals(1, m.numProcesses);
- var p = m.processes[52];
- assertNotUndefined(p);
+var TimelineCpu = tracing.TimelineCpu;
+var TimelineSlice = tracing.TimelineSlice;
+var TimelineProcess = tracing.TimelineProcess;
+var TimelineThread = tracing.TimelineThread;
+var TimelineModel = tracing.TimelineModel;
- assertEquals(1, p.numThreads);
- var t = p.threads[53];
- assertNotUndefined(t);
- assertEquals(1, t.subRows.length);
- assertEquals(53, t.tid);
- var subRow = t.subRows[0];
- assertEquals(2, subRow.length);
- var slice = subRow[0];
- assertEquals('a', slice.title);
- assertEquals(0, slice.start);
- assertEquals((560 - 524) / 1000, slice.duration);
- assertEquals(0, slice.subSlices.length);
-
- slice = subRow[1];
- assertEquals('b', slice.title);
- assertEquals((629 - 524) / 1000, slice.start);
- assertEquals((631 - 629) / 1000, slice.duration);
- assertEquals(0, slice.subSlices.length);
+function testThreadBounds_Empty() {
+ var t = new TimelineThread(undefined, 1);
+ t.updateBounds();
+ assertEquals(undefined, t.minTimestamp);
+ assertEquals(undefined, t.maxTimestamp);
}
-function testNestedParsing() {
- var events = [
- {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'b', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'E'},
- {name: 'a', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'E'}
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var t = p.threads[1];
- assertEquals(2, t.subRows.length);
- var subRow = t.subRows[0];
- assertEquals(1, subRow.length);
- var slice = subRow[0];
- assertEquals('a', slice.title);
- assertEquals((4 - 1) / 1000, slice.duration);
- assertEquals(1, slice.subSlices.length);
-
- slice = slice.subSlices[0];
- assertEquals('b', slice.title);
- assertEquals((2 - 1) / 1000, slice.start);
- assertEquals((3 - 2) / 1000, slice.duration);
- assertEquals(0, slice.subSlices.length);
-
- subRow = t.subRows[1];
- slice = subRow[0];
- assertEquals(t.subRows[0][0].subSlices[0], slice);
+function testThreadBounds_SubRow() {
+ var t = new TimelineThread(undefined, 1);
+ t.subRows[0].push(new TimelineSlice('a', 0, 1, {}, 3));
+ t.updateBounds();
+ assertEquals(1, t.minTimestamp);
+ assertEquals(4, t.maxTimestamp);
}
-function testAutoclosing() {
- var events = [
- // Slice that doesn't finish.
- {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
-
- // Slice that does finish to give an 'end time' to make autoclosing work.
- {name: 'b', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 2, ph: 'B'},
- {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 2, ph: 'E'}
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var t = p.threads[1];
- var subRow = t.subRows[0];
- var slice = subRow[0];
- assertEquals('a', slice.title);
- assertTrue(slice.didNotFinish);
- assertEquals(0, slice.start);
- assertEquals((2 - 1) / 1000, slice.duration);
+function testThreadBounds_NestedSubrow() {
+ var t = new TimelineThread(undefined, 1);
+ t.nonNestedSubRows.push([]);
+ t.nonNestedSubRows[0].push(new TimelineSlice('a', 0, 1, {}, 3));
+ t.updateBounds();
+ assertEquals(1, t.minTimestamp);
+ assertEquals(4, t.maxTimestamp);
}
-function testAutoclosingLoneBegin() {
- var events = [
- // Slice that doesn't finish.
- {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'}
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var t = p.threads[1];
- var subRow = t.subRows[0];
- var slice = subRow[0];
- assertEquals('a', slice.title);
- assertTrue(slice.didNotFinish);
- assertEquals(0, slice.start);
- assertEquals(0, slice.duration);
+function testThreadBounds_SubRowAndNonNestedSubRow() {
+ var t = new TimelineThread(undefined, 1);
+ t.subRows[0].push(new TimelineSlice('a', 0, 0.5, {}, 3));
+ t.nonNestedSubRows.push([]);
+ t.nonNestedSubRows[0].push(new TimelineSlice('b', 0, 1, {}, 4.5));
+ t.updateBounds();
+ assertEquals(0.5, t.minTimestamp);
+ assertEquals(5.5, t.maxTimestamp);
}
-function testAutoclosingWithSubTasks() {
- var events = [
- {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'b1', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'b1', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'E'},
- {name: 'b2', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'}
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var t = p.threads[1];
- assertEquals(2, t.subRows.length);
- assertEquals(1, t.subRows[0].length);
- assertEquals(2, t.subRows[1].length);
+function testModelBounds_EmptyModel() {
+ var m = new TimelineModel();
+ m.updateBounds();
+ assertEquals(undefined, m.minTimestamp);
+ assertEquals(undefined, m.maxTimestamp);
}
-function testAutoclosingWithEventsOutsideRange() {
- var events = [
- // Slice that begins before min and ends after max of the other threads.
- {name: 'a', args: {}, pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'a', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'},
-
- // Slice that does finish to give an 'end time' to establish a basis
- {name: 'b', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 2, ph: 'B'},
- {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 2, ph: 'E'}
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var t = p.threads[1];
- var subRow = t.subRows[0];
- assertEquals('a', subRow[0].title);
- assertEquals(0, subRow[0].start);
- assertEquals(0.003, subRow[0].duration);
-
- var t = p.threads[2];
- var subRow = t.subRows[0];
- assertEquals('b', subRow[0].title);
- assertEquals(0.001, subRow[0].start);
- assertEquals(0.001, subRow[0].duration);
-
- // 0.00345 instead of 0.003 because TimelineModel bloats the world range by
- // 15%.
- assertEquals(-0.00045, m.minTimestamp);
- assertEquals(0.00345, m.maxTimestamp);
-
+function testModelBounds_OneEmptyThread() {
+ var m = new TimelineModel();
+ var t = m.getOrCreateProcess(1).getOrCreateThread(1);
+ m.updateBounds();
+ assertEquals(undefined, m.minTimestamp);
+ assertEquals(undefined, m.maxTimestamp);
}
-function testNestedAutoclosing() {
- var events = [
- // Tasks that dont finish.
- {name: 'a1', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'a2', args: {}, pid: 1, ts: 1.5, cat: 'foo', tid: 1, ph: 'B'},
-
- // Slice that does finish to give an 'end time' to make autoclosing work.
- {name: 'b', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 2, ph: 'B'},
- {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 2, ph: 'E'}
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var t = p.threads[1];
- var subRow = t.subRows[0];
- var slice = subRow[0];
- assertEquals('a1', slice.title);
- assertTrue(slice.didNotFinish);
- assertEquals(0, slice.start);
- assertEquals((2 - 1) / 1000, slice.duration);
-
- var slice = slice.subSlices[0];
- assertEquals('a2', slice.title);
- assertTrue(slice.didNotFinish);
- assertEquals((1.5 - 1) / 1000, slice.start);
- assertEquals((2 - 1.5) / 1000, slice.duration);
+function testModelBounds_OneThrad() {
+ var m = new TimelineModel();
+ var t = m.getOrCreateProcess(1).getOrCreateThread(1);
+ t.subRows[0].push(new TimelineSlice('a', 0, 1, {}, 3));
+ m.updateBounds();
+ assertEquals(1, m.minTimestamp);
+ assertEquals(4, m.maxTimestamp);
}
-function testTaskColoring() {
- // The test below depends on hashing of 'a' != 'b'. Fail early if that
- // assumption is incorrect.
- assertNotEquals(tracing.getStringHash('a'), tracing.getStringHash('b'));
-
- var events = [
- {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
- {name: 'b', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'b', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'E'},
- {name: 'a', args: {}, pid: 1, ts: 5, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'a', args: {}, pid: 1, ts: 6, cat: 'foo', tid: 1, ph: 'E'}
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var t = p.threads[1];
- var subRow = t.subRows[0];
- var a1 = subRow[0];
- assertEquals('a', a1.title);
- var b = subRow[1];
- assertEquals('b', b.title);
- assertNotEquals(a1.colorId, b.colorId);
- var a2 = subRow[0];
- assertEquals('a', a2.title);
- assertEquals(a1.colorId, a2.colorId);
+function testModelBounds_OneThreadAndOneEmptyThread() {
+ var m = new TimelineModel();
+ var t1 = m.getOrCreateProcess(1).getOrCreateThread(1);
+ t1.subRows[0].push(new TimelineSlice('a', 0, 1, {}, 3));
+ var t2 = m.getOrCreateProcess(1).getOrCreateThread(1);
+ m.updateBounds();
+ assertEquals(1, m.minTimestamp);
+ assertEquals(4, m.maxTimestamp);
}
-function testMultipleThreadParsing() {
- var events = [
- {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
- {name: 'b', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 2, ph: 'B'},
- {name: 'b', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 2, ph: 'E'}
- ];
- var m = new tracing.TimelineModel(events);
- assertEquals(1, m.numProcesses);
- var p = m.processes[1];
- assertNotUndefined(p);
-
- assertEquals(2, p.numThreads);
-
- // Check thread 1.
- var t = p.threads[1];
- assertNotUndefined(t);
- assertEquals(1, t.subRows.length);
- assertEquals(1, t.tid);
-
- var subRow = t.subRows[0];
- assertEquals(1, subRow.length);
- var slice = subRow[0];
- assertEquals('a', slice.title);
- assertEquals(0, slice.start);
- assertEquals((2 - 1) / 1000, slice.duration);
- assertEquals(0, slice.subSlices.length);
-
- // Check thread 2.
- var t = p.threads[2];
- assertNotUndefined(t);
- assertEquals(1, t.subRows.length);
- assertEquals(2, t.tid);
-
- subRow = t.subRows[0];
- assertEquals(1, subRow.length);
- slice = subRow[0];
- assertEquals('b', slice.title);
- assertEquals((3 - 1) / 1000, slice.start);
- assertEquals((4 - 3) / 1000, slice.duration);
- assertEquals(0, slice.subSlices.length);
+function testCpuBounds_Empty() {
+ var cpu = new TimelineCpu(undefined, 1);
+ cpu.updateBounds();
+ assertEquals(undefined, cpu.minTimestamp);
+ assertEquals(undefined, cpu.maxTimestamp);
}
-function testMultiplePidParsing() {
- var events = [
- {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
- {name: 'b', args: {}, pid: 2, ts: 3, cat: 'foo', tid: 2, ph: 'B'},
- {name: 'b', args: {}, pid: 2, ts: 4, cat: 'foo', tid: 2, ph: 'E'}
- ];
- var m = new tracing.TimelineModel(events);
- assertEquals(2, m.numProcesses);
- var p = m.processes[1];
- assertNotUndefined(p);
-
- assertEquals(1, p.numThreads);
-
- // Check process 1 thread 1.
- var t = p.threads[1];
- assertNotUndefined(t);
- assertEquals(1, t.subRows.length);
- assertEquals(1, t.tid);
-
- var subRow = t.subRows[0];
- assertEquals(1, subRow.length);
- var slice = subRow[0];
- assertEquals('a', slice.title);
- assertEquals(0, slice.start);
- assertEquals((2 - 1) / 1000, slice.duration);
- assertEquals(0, slice.subSlices.length);
-
- // Check process 2 thread 2.
- var p = m.processes[2];
- assertNotUndefined(p);
- assertEquals(1, p.numThreads);
- var t = p.threads[2];
- assertNotUndefined(t);
- assertEquals(1, t.subRows.length);
- assertEquals(2, t.tid);
- subRow = t.subRows[0];
- assertEquals(1, subRow.length);
- slice = subRow[0];
- assertEquals('b', slice.title);
- assertEquals((3 - 1) / 1000, slice.start);
- assertEquals((4 - 3) / 1000, slice.duration);
- assertEquals(0, slice.subSlices.length);
-
- // Check getAllThreads.
- assertArrayEquals([m.processes[1].threads[1], m.processes[2].threads[2]],
- m.getAllThreads());
-}
-
-// Thread names.
-function testThreadNames() {
- var events = [
- {name: 'thread_name', args: {name: 'Thread 1'},
- pid: 1, ts: 0, tid: 1, ph: 'M'},
- {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
- {name: 'b', args: {}, pid: 2, ts: 3, cat: 'foo', tid: 2, ph: 'B'},
- {name: 'b', args: {}, pid: 2, ts: 4, cat: 'foo', tid: 2, ph: 'E'},
- {name: 'thread_name', args: {name: 'Thread 2'},
- pid: 2, ts: 0, tid: 2, ph: 'M'}
- ];
- var m = new tracing.TimelineModel(events);
- assertEquals('Thread 1', m.processes[1].threads[1].name);
- assertEquals('Thread 2', m.processes[2].threads[2].name);
-}
-
-// User time.
-function testUserTime() {
- var events = [
- {name: 'thread_name', args: {name: 'Thread 1'},
- pid: 1, ts: 0, tid: 1, ph: 'M'},
- {name: 'a', args: {}, pid: 1, ts: 1, uts: 70, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'a', args: {}, pid: 1, ts: 2, uts: 77, cat: 'foo', tid: 1, ph: 'E'},
- {name: 'a', args: {}, pid: 1, ts: 2 , uts: 80, cat: 'foo', tid: 1, ph: 'I'}
- ];
- var m = new tracing.TimelineModel(events);
- var subRow = m.processes[1].threads[1].subRows[0];
- assertEquals(subRow[0].startInUserTime, 0.07);
- assertEquals(subRow[0].durationInUserTime, 0.007);
- assertEquals(subRow[1].startInUserTime, 0.08);
- assertEquals(subRow[1].durationInUserTime, 0);
+function testCpuBounds_OneSlice() {
+ var cpu = new TimelineCpu(undefined, 1);
+ cpu.slices.push(new TimelineSlice('a', 0, 1, {}, 3));
+ cpu.updateBounds();
+ assertEquals(1, cpu.minTimestamp);
+ assertEquals(4, cpu.maxTimestamp);
}
-
-function testImmediateParsing() {
- var events = [
- // Need to include immediates inside a task so the timeline
- // recentering/zeroing doesn't clobber their timestamp.
- {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
- {name: 'immediate', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'I'},
- {name: 'a', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'E'}
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var t = p.threads[1];
- assertEquals(2, t.subRows.length);
- var subRow = t.subRows[0];
- assertEquals(1, subRow.length);
- var slice = subRow[0];
- assertEquals('a', slice.title);
- assertEquals((4 - 1) / 1000, slice.duration);
- assertEquals(1, slice.subSlices.length);
-
- var immed = slice.subSlices[0];
- assertEquals('immediate', immed.title);
- assertEquals((2 - 1) / 1000, immed.start);
- assertEquals(0, immed.duration);
- assertEquals(0, immed.subSlices.length);
-
- subRow = t.subRows[1];
- assertEquals(immed, subRow[0]);
+function testModelBounds_OneCpu() {
}
-function testSimpleCounter() {
- var events = [
- {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1,
- ph: 'C'},
- {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
- ph: 'C'},
- {name: 'ctr', args: {'value': 0}, pid: 1, ts: 20, cat: 'foo', tid: 1,
- ph: 'C'}
-
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var ctr = m.processes[1].counters['foo.ctr'];
-
- assertEquals('ctr', ctr.name);
- assertEquals(3, ctr.numSamples);
- assertEquals(1, ctr.numSeries);
-
- assertArrayEquals(['value'], ctr.seriesNames);
- assertArrayEquals([tracing.getStringColorId('ctr.value')], ctr.seriesColors);
- assertArrayEquals([0, 0.01, 0.02], ctr.timestamps);
- assertArrayEquals([0, 10, 0], ctr.samples);
- assertArrayEquals([0, 10, 0], ctr.totals);
- assertEquals(10, ctr.maxTotal);
-}
-
-function testInstanceCounter() {
- var events = [
- {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1,
- ph: 'C', id: 0},
- {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
- ph: 'C', id: 0},
- {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
- ph: 'C', id: 1},
- {name: 'ctr', args: {'value': 20}, pid: 1, ts: 15, cat: 'foo', tid: 1,
- ph: 'C', id: 1},
- {name: 'ctr', args: {'value': 30}, pid: 1, ts: 18, cat: 'foo', tid: 1,
- ph: 'C', id: 1}
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var ctr = m.processes[1].counters['foo.ctr[0]'];
- assertEquals('ctr[0]', ctr.name);
- assertEquals(2, ctr.numSamples);
- assertEquals(1, ctr.numSeries);
- assertArrayEquals([0, 0.01], ctr.timestamps);
- assertArrayEquals([0, 10], ctr.samples);
-
- var ctr = m.processes[1].counters['foo.ctr[1]'];
- assertEquals('ctr[1]', ctr.name);
- assertEquals(3, ctr.numSamples);
- assertEquals(1, ctr.numSeries);
- assertArrayEquals([0.01, 0.015, 0.018], ctr.timestamps);
- assertArrayEquals([10, 20, 30], ctr.samples);
-}
-
-function testMultiCounterUpdateBounds() {
- var ctr = new tracing.TimelineCounter(undefined, 'testBasicCounter',
-'testBasicCounter');
- ctr.numSeries = 1;
- ctr.seriesNames = ['value1', 'value2'];
- ctr.seriesColors = ['testBasicCounter.value1', 'testBasicCounter.value2'];
- ctr.timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
- ctr.samples = [0, 0,
- 1, 0,
- 1, 1,
- 2, 1.1,
- 3, 0,
- 1, 7,
- 3, 0,
- 3.1, 0.5];
- ctr.updateBounds();
- assertEquals(0, ctr.minTimestamp);
- assertEquals(7, ctr.maxTimestamp);
- assertEquals(8, ctr.maxTotal);
- assertArrayEquals([0, 0,
- 1, 1,
- 1, 2,
- 2, 3.1,
- 3, 3,
- 1, 8,
- 3, 3,
- 3.1, 3.6], ctr.totals);
-}
-
-function testMultiCounter() {
- var events = [
- {name: 'ctr', args: {'value1': 0, 'value2': 7}, pid: 1, ts: 0, cat: 'foo',
- tid: 1, ph: 'C'},
- {name: 'ctr', args: {'value1': 10, 'value2': 4}, pid: 1, ts: 10, cat: 'foo',
- tid: 1, ph: 'C'},
- {name: 'ctr', args: {'value1': 0, 'value2': 1 }, pid: 1, ts: 20, cat: 'foo',
- tid: 1, ph: 'C'}
- ];
- var m = new tracing.TimelineModel(events);
- var p = m.processes[1];
- var ctr = m.processes[1].counters['foo.ctr'];
- assertEquals('ctr', ctr.name);
-
- assertEquals('ctr', ctr.name);
- assertEquals(3, ctr.numSamples);
- assertEquals(2, ctr.numSeries);
- assertArrayEquals(['value1', 'value2'], ctr.seriesNames);
- assertArrayEquals([tracing.getStringColorId('ctr.value1'),
- tracing.getStringColorId('ctr.value2')],
- ctr.seriesColors);
- assertArrayEquals([0, 0.01, 0.02], ctr.timestamps);
- assertArrayEquals([0, 7,
- 10, 4,
- 0, 1], ctr.samples);
- assertArrayEquals([0, 7,
- 10, 14,
- 0, 1], ctr.totals);
- assertEquals(14, ctr.maxTotal);
+function testModelBounds_OneCpuOneThread() {
}
</script>
diff --git a/chrome/browser/resources/tracing/timeline_test.html b/chrome/browser/resources/tracing/timeline_test.html
index fae68b6..afdb1b5 100644
--- a/chrome/browser/resources/tracing/timeline_test.html
+++ b/chrome/browser/resources/tracing/timeline_test.html
@@ -13,6 +13,8 @@ found in the LICENSE file.
<script src="../shared/js/cr/ui.js"></script>
<script src="../shared/js/util.js"></script>
<script src="timeline_model.js"></script>
+<script src="linux_perf_importer.js"></script>
+<script src="trace_event_importer.js"></script>
<script src="sorted_array_utils.js"></script>
<script src="measuring_stick.js"></script>
<script src="timeline.js"></script>
@@ -48,6 +50,9 @@ found in the LICENSE file.
<div class="timeline-test" src="./tests/main_thread_has_unclosed_slices.json">
</div>
+ <div class="timeline-test" src="./tests/linux_perf_simple.txt">
+ </div>
+
<script>
function load(parentEl) {
var src = parentEl.getAttribute('src');
@@ -77,20 +82,16 @@ found in the LICENSE file.
// Creating attached vs detached stress tests the canvas- and viewport-
// setup code.
var create_detached = parentEl.getAttribute('create-attached') == 1;
- if (create_detached) {
- containerEl.appendChild(timelineEl);
- test_utils.getJSON(src, function(events) {
- var model = new tracing.TimelineModel(events);
- timelineEl.model = model;
- });
- } else {
- test_utils.getJSON(src, function(events) {
- var model = new tracing.TimelineModel(events);
- timelineEl.model = model;
+ function createModel(data) {
+ timelineEl.model = new tracing.TimelineModel(data);
+ if (!create_detached)
containerEl.appendChild(timelineEl);
- });
}
+ if (create_detached)
+ containerEl.appendChild(timelineEl);
+ test_utils.getAsync(src, createModel);
}
+
function onLoad() {
Array.prototype.forEach.call(document.querySelectorAll('.timeline-test'),
load);
diff --git a/chrome/browser/resources/tracing/timeline_track.js b/chrome/browser/resources/tracing/timeline_track.js
index 12bed67..90ebc44 100644
--- a/chrome/browser/resources/tracing/timeline_track.js
+++ b/chrome/browser/resources/tracing/timeline_track.js
@@ -10,51 +10,8 @@
*/
cr.define('tracing', function() {
- const palletteBase = [
- {r: 138, g: 113, b: 152},
- {r: 175, g: 112, b: 133},
- {r: 127, g: 135, b: 225},
- {r: 93, g: 81, b: 137},
- {r: 116, g: 143, b: 119},
- {r: 178, g: 214, b: 122},
- {r: 87, g: 109, b: 147},
- {r: 119, g: 155, b: 95},
- {r: 114, g: 180, b: 160},
- {r: 132, g: 85, b: 103},
- {r: 157, g: 210, b: 150},
- {r: 148, g: 94, b: 86},
- {r: 164, g: 108, b: 138},
- {r: 139, g: 191, b: 150},
- {r: 110, g: 99, b: 145},
- {r: 80, g: 129, b: 109},
- {r: 125, g: 140, b: 149},
- {r: 93, g: 124, b: 132},
- {r: 140, g: 85, b: 140},
- {r: 104, g: 163, b: 162},
- {r: 132, g: 141, b: 178},
- {r: 131, g: 105, b: 147},
- {r: 135, g: 183, b: 98},
- {r: 152, g: 134, b: 177},
- {r: 141, g: 188, b: 141},
- {r: 133, g: 160, b: 210},
- {r: 126, g: 186, b: 148},
- {r: 112, g: 198, b: 205},
- {r: 180, g: 122, b: 195},
- {r: 203, g: 144, b: 152}];
-
- function brighten(c) {
- return {r: Math.min(255, c.r + Math.floor(c.r * 0.45)),
- g: Math.min(255, c.g + Math.floor(c.g * 0.45)),
- b: Math.min(255, c.b + Math.floor(c.b * 0.45))};
- }
- function colorToString(c) {
- return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')';
- }
-
- const selectedIdBoost = palletteBase.length;
-
- const pallette = palletteBase.concat(palletteBase.map(brighten)).
- map(colorToString);
+ var pallette = tracing.getPallette();
+ var highlightIdBoost = tracing.getPalletteHighlightIdBoost();
var textWidthMap = { };
function quickMeasureText(ctx, text) {
@@ -199,6 +156,7 @@ cr.define('tracing', function() {
this.tracks_.push(track);
this.appendChild(track);
+ return track;
},
updateChildTracks_: function() {
@@ -206,6 +164,11 @@ cr.define('tracing', function() {
this.textContent = '';
this.tracks_ = [];
if (this.thread_) {
+ if (this.thread_.cpuSlices) {
+ var track = this.addTrack_(this.thread_.cpuSlices);
+ track.height = '4px';
+ }
+
for (var srI = 0; srI < this.thread_.nonNestedSubRows.length; ++srI) {
this.addTrack_(this.thread_.nonNestedSubRows[srI]);
}
@@ -213,14 +176,86 @@ cr.define('tracing', function() {
this.addTrack_(this.thread_.subRows[srI]);
}
if (this.tracks_.length > 0) {
- this.tracks_[0].heading = this.heading_;
- this.tracks_[0].tooltip = this.tooltip_;
+ if (this.thread_.cpuSlices) {
+ this.tracks_[1].heading = this.heading_;
+ this.tracks_[1].tooltip = this.tooltip_;
+ } else {
+ this.tracks_[0].heading = this.heading_;
+ this.tracks_[0].tooltip = this.tooltip_;
+ }
}
}
}
};
/**
+ * Visualizes a TimelineCpu using a series of of TimelineSliceTracks.
+ * @constructor
+ */
+ var TimelineCpuTrack = cr.ui.define(TimelineContainerTrack);
+ TimelineCpuTrack.prototype = {
+ __proto__: TimelineContainerTrack.prototype,
+
+ decorate: function() {
+ this.classList.add('timeline-thread-track');
+ },
+
+ get cpu(cpu) {
+ return this.cpu_;
+ },
+
+ set cpu(cpu) {
+ this.cpu_ = cpu;
+ this.updateChildTracks_();
+ },
+
+ get tooltip() {
+ return this.tooltip_;
+ },
+
+ set tooltip(value) {
+ this.tooltip_ = value;
+ this.updateChildTracks_();
+ },
+
+ get heading() {
+ return this.heading_;
+ },
+
+ set heading(h) {
+ this.heading_ = h;
+ this.updateChildTracks_();
+ },
+
+ get headingWidth() {
+ return this.headingWidth_;
+ },
+
+ set headingWidth(width) {
+ this.headingWidth_ = width;
+ this.updateChildTracks_();
+ },
+
+ updateChildTracks_: function() {
+ this.detach();
+ this.textContent = '';
+ this.tracks_ = [];
+ if (this.cpu_) {
+ var track = new TimelineSliceTrack();
+ track.slices = this.cpu_.slices;
+ track.headingWidth = this.headingWidth_;
+ track.viewport = this.viewport_;
+
+ this.tracks_.push(track);
+ this.appendChild(track);
+
+ this.tracks_[0].heading = this.heading_;
+ this.tracks_[0].tooltip = this.tooltip_;
+ }
+ }
+ };
+
+ /**
* A canvas-based track constructed. Provides the basic heading and
* invalidation-managment infrastructure. Subclasses must implement drawing
* and picking code.
@@ -344,6 +379,10 @@ cr.define('tracing', function() {
this.invalidate();
},
+ set height(height) {
+ this.style.height = height;
+ },
+
redraw: function() {
var ctx = this.ctx_;
var canvasW = this.canvas_.width;
@@ -390,9 +429,8 @@ cr.define('tracing', function() {
var x = slice.start;
// Less than 0.001 causes short events to disappear when zoomed in.
var w = Math.max(slice.duration, 0.001);
- var colorId;
- colorId = slice.selected ?
- slice.colorId + selectedIdBoost :
+ var colorId = slice.selected ?
+ slice.colorId + highlightIdBoost :
slice.colorId;
if (w < pixWidth)
@@ -419,25 +457,28 @@ cr.define('tracing', function() {
ctx.restore();
// Labels.
- ctx.textAlign = 'center';
- ctx.textBaseline = 'top';
- ctx.font = '10px sans-serif';
- ctx.strokeStyle = 'rgb(0,0,0)';
- ctx.fillStyle = 'rgb(0,0,0)';
- var quickDiscardThresshold = pixWidth * 20; // dont render until 20px wide
- for (var i = 0; i < slices.length; ++i) {
- var slice = slices[i];
- if (slice.duration > quickDiscardThresshold) {
- var title = slice.title;
- if (slice.didNotFinish) {
- title += ' (Did Not Finish)';
- }
- var labelWidth = quickMeasureText(ctx, title) + 2;
- var labelWidthWorld = pixWidth * labelWidth;
-
- if (labelWidthWorld < slice.duration) {
- var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration);
- ctx.fillText(title, cX, 2.5, labelWidth);
+ if (canvasH > 8) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ ctx.font = '10px sans-serif';
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.fillStyle = 'rgb(0,0,0)';
+ // Don't render text until until it is 20px wide
+ var quickDiscardThresshold = pixWidth * 20;
+ for (var i = 0; i < slices.length; ++i) {
+ var slice = slices[i];
+ if (slice.duration > quickDiscardThresshold) {
+ var title = slice.title;
+ if (slice.didNotFinish) {
+ title += ' (Did Not Finish)';
+ }
+ var labelWidth = quickMeasureText(ctx, title) + 2;
+ var labelWidthWorld = pixWidth * labelWidth;
+
+ if (labelWidthWorld < slice.duration) {
+ var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration);
+ ctx.fillText(title, cX, 2.5, labelWidth);
+ }
}
}
}
@@ -588,7 +629,7 @@ cr.define('tracing', function() {
var viewRWorld = vp.xViewToWorld(canvasW);
// Drop sampels that are less than skipDistancePix apart.
- var skipDistancePix = 16;
+ var skipDistancePix = 1;
var skipDistanceWorld = vp.xViewVectorToWorld(skipDistancePix);
// Begin rendering in world space.
@@ -696,6 +737,7 @@ cr.define('tracing', function() {
return {
TimelineCounterTrack: TimelineCounterTrack,
TimelineSliceTrack: TimelineSliceTrack,
- TimelineThreadTrack: TimelineThreadTrack
+ TimelineThreadTrack: TimelineThreadTrack,
+ TimelineCpuTrack: TimelineCpuTrack
};
});
diff --git a/chrome/browser/resources/tracing/timeline_track_test.html b/chrome/browser/resources/tracing/timeline_track_test.html
index e155c3d..a7c1dc8 100644
--- a/chrome/browser/resources/tracing/timeline_track_test.html
+++ b/chrome/browser/resources/tracing/timeline_track_test.html
@@ -1,7 +1,7 @@
<!DOCTYPE HTML>
<html>
<!--
-Copyright (c) 2010 The Chromium Authors. All rights reserved.
+Copyright (c) 2011 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.
-->
@@ -42,6 +42,8 @@ found in the LICENSE file.
<script>
var TimelineCounter = tracing.TimelineCounter;
var TimelineCounterTrack = tracing.TimelineCounterTrack;
+ var TimelineCpu = tracing.TimelineCpu;
+ var TimelineCpuTrack = tracing.TimelineCpuTrack;
var TimelineSliceTrack = tracing.TimelineSliceTrack;
var TimelineSlice = tracing.TimelineSlice;
var TimelineViewport = tracing.TimelineViewport;
@@ -124,6 +126,25 @@ found in the LICENSE file.
assertEquals(0, hits.length);
}
+ function testBasicCpu() {
+ var testEl = getTestDiv('testBasicCpu');
+
+ var cpu = new TimelineCpu(7);
+ cpu.slices = [
+ new TimelineSlice('a', 0, 1, {}, 1),
+ new TimelineSlice('b', 1, 2.1, {}, 4.8)
+ ];
+ cpu.updateBounds();
+
+ var track = TimelineCpuTrack();
+ testEl.appendChild(track);
+ track.heading = 'CPU ' + cpu.cpuNumber;
+ track.cpu = cpu;
+ track.viewport = new TimelineViewport(testEl);
+ track.viewport.setPanAndScale(0,
+ track.clientWidth / (1.1 * cpu.maxTimestamp));
+ }
+
function testBasicCounter() {
var testEl = getTestDiv('testBasicCounter');
@@ -144,7 +165,7 @@ found in the LICENSE file.
3.1, 0.5];
ctr.updateBounds();
- var track = TimelineCounterTrack();
+ var track = new TimelineCounterTrack();
testEl.appendChild(track);
track.heading = ctr.name;
track.counter = ctr;
diff --git a/chrome/browser/resources/tracing/timeline_view.js b/chrome/browser/resources/tracing/timeline_view.js
index efe4403..84a7f22 100644
--- a/chrome/browser/resources/tracing/timeline_view.js
+++ b/chrome/browser/resources/tracing/timeline_view.js
@@ -45,7 +45,7 @@ cr.define('tracing', function() {
__proto__: HTMLDivElement.prototype,
decorate: function() {
- this.className = 'timeline-view';
+ this.classList.add('timeline-view');
this.timelineContainer_ = document.createElement('div');
this.timelineContainer_.className = 'timeline-container';
@@ -63,14 +63,22 @@ cr.define('tracing', function() {
this.onSelectionChangedBoundToThis_ = this.onSelectionChanged_.bind(this);
},
- set traceEvents(traceEvents) {
- this.timelineModel_ = new tracing.TimelineModel(traceEvents);
+ set traceData(traceData) {
+ this.model = new tracing.TimelineModel(traceData);
+ },
+
+ get model(model) {
+ return this.timelineModel_;
+ },
+
+ set model(model) {
+ this.timelineModel_ = model;
// remove old timeline
this.timelineContainer_.textContent = '';
// create new timeline if needed
- if (traceEvents.length) {
+ if (this.timelineModel_.minTimestamp !== undefined) {
if (this.timeline_)
this.timeline_.detach();
this.timeline_ = new tracing.Timeline();
@@ -85,6 +93,10 @@ cr.define('tracing', function() {
}
},
+ get timeline() {
+ return this.timeline_;
+ },
+
onSelectionChanged_: function(e) {
var timeline = this.timeline_;
var selection = timeline.selection;
diff --git a/chrome/browser/resources/tracing/trace_event_importer.js b/chrome/browser/resources/tracing/trace_event_importer.js
new file mode 100644
index 0000000..56c8217
--- /dev/null
+++ b/chrome/browser/resources/tracing/trace_event_importer.js
@@ -0,0 +1,326 @@
+// Copyright (c) 2011 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.
+
+/**
+ * @fileoverview TraceEventImporter imports TraceEvent-formatted data
+ * into the provided timeline model.
+ */
+cr.define('tracing', function() {
+ function ThreadState(tid) {
+ this.openSlices = [];
+ this.openNonNestedSlices = {};
+ }
+
+ function TraceEventImporter(model, eventData) {
+ this.model_ = model;
+
+ if (typeof(eventData) === 'string' || eventData instanceof String) {
+ // 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] == '[') {
+ n = eventData.length;
+ if (eventData[n - 1] != ']' && eventData[n - 1] != '\n') {
+ eventData = eventData + ']';
+ } else if (eventData[n - 2] != ']' && eventData[n - 1] == '\n') {
+ eventData = eventData + ']';
+ } else if (eventData[n - 3] != ']' && eventData[n - 2] == '\r' &&
+ eventData[n - 1] == '\n') {
+ eventData = eventData + ']';
+ }
+ }
+ this.events_ = JSON.parse(eventData);
+
+ } else {
+ this.events_ = eventData;
+ }
+
+ // 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)
+ this.events_ = this.events_.traceEvents;
+
+ // To allow simple indexing of threads, we store all the threads by a
+ // PTID. A ptid is a pid and tid joined together x:y fashion, eg
+ // 1024:130. The ptid is a unique key for a thread in the trace.
+ this.threadStateByPTID_ = {};
+ }
+
+ /**
+ * @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) {
+ return eventData[0] == '{' || eventData[0] == '[';
+ }
+
+ // Might just be an array of events
+ if (eventData instanceof Array && eventData[0].ph)
+ return true;
+
+ // Might be an object with a traceEvents field in it.
+ if (eventData.traceEvents)
+ return eventData.traceEvents instanceof Array &&
+ eventData.traceEvents[0].ph;
+
+ return false;
+ };
+
+ TraceEventImporter.prototype = {
+
+ __proto__: Object.prototype,
+
+ /**
+ * Helper to process a 'begin' event (e.g. initiate a slice).
+ * @param {ThreadState} state Thread state (holds slices).
+ * @param {Object} event The current trace event.
+ */
+ processBegin: function(index, state, event) {
+ var colorId = tracing.getStringColorId(event.name);
+ var slice =
+ { index: index,
+ slice: new tracing.TimelineSlice(event.name, colorId,
+ event.ts / 1000,
+ event.args) };
+
+ if (event.uts)
+ slice.slice.startInUserTime = event.uts / 1000;
+
+ if (event.args['ui-nest'] === '0') {
+ var sliceID = event.name;
+ for (var x in event.args)
+ sliceID += ';' + event.args[x];
+ if (state.openNonNestedSlices[sliceID])
+ this.model_.importErrors.push('Event ' + sliceID + ' already open.');
+ state.openNonNestedSlices[sliceID] = slice;
+ } else {
+ state.openSlices.push(slice);
+ }
+ },
+
+ /**
+ * Helper to process an 'end' event (e.g. close a slice).
+ * @param {ThreadState} state Thread state (holds slices).
+ * @param {Object} event The current trace event.
+ */
+ processEnd: function(state, event) {
+ if (event.args['ui-nest'] === '0') {
+ var sliceID = event.name;
+ for (var x in event.args)
+ sliceID += ';' + event.args[x];
+ var slice = state.openNonNestedSlices[sliceID];
+ if (!slice)
+ return;
+ slice.slice.duration = (event.ts / 1000) - slice.slice.start;
+ if (event.uts)
+ slice.durationInUserTime = (event.uts / 1000) -
+ slice.slice.startInUserTime;
+
+ // Store the slice in a non-nested subrow.
+ var thread =
+ this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ thread.addNonNestedSlice(slice.slice);
+ delete state.openNonNestedSlices[name];
+ } else {
+ if (state.openSlices.length == 0) {
+ // Ignore E events that are unmatched.
+ return;
+ }
+ var slice = state.openSlices.pop().slice;
+ slice.duration = (event.ts / 1000) - slice.start;
+ if (event.uts)
+ slice.durationInUserTime = (event.uts / 1000) - slice.startInUserTime;
+
+ // Store the slice on the correct subrow.
+ var thread = this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ var subRowIndex = state.openSlices.length;
+ thread.getSubrow(subRowIndex).push(slice);
+
+ // Add the slice to the subSlices array of its parent.
+ if (state.openSlices.length) {
+ var parentSlice = state.openSlices[state.openSlices.length - 1];
+ parentSlice.slice.subSlices.push(slice);
+ }
+ }
+ },
+
+ /**
+ * Helper function that closes any open slices. This happens when a trace
+ * ends before an 'E' phase event can get posted. When that happens, this
+ * closes the slice at the highest timestamp we recorded and sets the
+ * didNotFinish flag to true.
+ */
+ autoCloseOpenSlices: function() {
+ // We need to know the model bounds in order to assign an end-time to
+ // the open slices.
+ this.model_.updateBounds();
+
+ // The model's max value in the trace is wrong at this point if there are
+ // un-closed events. To close those events, we need the true global max
+ // value. To compute this, build a list of timestamps that weren't
+ // included in the max calculation, then compute the real maximum based
+ // on that.
+ var openTimestamps = [];
+ for (var ptid in this.threadStateByPTID_) {
+ var state = this.threadStateByPTID_[ptid];
+ for (var i = 0; i < state.openSlices.length; i++) {
+ var slice = state.openSlices[i];
+ openTimestamps.push(slice.slice.start);
+ for (var s = 0; s < slice.slice.subSlices.length; s++) {
+ var subSlice = slice.slice.subSlices[s];
+ openTimestamps.push(subSlice.start);
+ if (subSlice.duration)
+ openTimestamps.push(subSlice.end);
+ }
+ }
+ }
+
+ // Figure out the maximum value of model.maxTimestamp and
+ // Math.max(openTimestamps). Made complicated by the fact that the model
+ // timestamps might be undefined.
+ var realMaxTimestamp;
+ if (this.model_.maxTimestamp) {
+ realMaxTimestamp = Math.max(this.model_.maxTimestamp,
+ Math.max.apply(Math, openTimestamps));
+ } else {
+ realMaxTimestamp = Math.max.apply(Math, openTimestamps);
+ }
+
+ // Automatically close any slices are still open. These occur in a number
+ // of reasonable situations, e.g. deadlock. This pass ensures the open
+ // slices make it into the final model.
+ for (var ptid in this.threadStateByPTID_) {
+ var state = this.threadStateByPTID_[ptid];
+ while (state.openSlices.length > 0) {
+ var slice = state.openSlices.pop();
+ slice.slice.duration = realMaxTimestamp - slice.slice.start;
+ slice.slice.didNotFinish = true;
+ var event = this.events_[slice.index];
+
+ // Store the slice on the correct subrow.
+ var thread = this.model_.getOrCreateProcess(event.pid)
+ .getOrCreateThread(event.tid);
+ var subRowIndex = state.openSlices.length;
+ thread.getSubrow(subRowIndex).push(slice.slice);
+
+ // Add the slice to the subSlices array of its parent.
+ if (state.openSlices.length) {
+ var parentSlice = state.openSlices[state.openSlices.length - 1];
+ parentSlice.slice.subSlices.push(slice.slice);
+ }
+ }
+ }
+ },
+
+ /**
+ * Helper that creates and adds samples to a TimelineCounter object based on
+ * 'C' phase events.
+ */
+ processCounter: 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);
+ // Initialize the counter's series fields if needed.
+ if (ctr.numSeries == 0) {
+ for (var seriesName in event.args) {
+ ctr.seriesNames.push(seriesName);
+ ctr.seriesColors.push(
+ tracing.getStringColorId(ctr.name + '.' + seriesName));
+ }
+ if (ctr.numSeries == 0) {
+ this.model_.importErrors.push('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;
+ }
+ }
+
+ // Add the sample values.
+ ctr.timestamps.push(event.ts / 1000);
+ for (var i = 0; i < ctr.numSeries; i++) {
+ var seriesName = ctr.seriesNames[i];
+ if (event.args[seriesName] === undefined) {
+ ctr.samples.push(0);
+ continue;
+ }
+ ctr.samples.push(event.args[seriesName]);
+ }
+ },
+
+ /**
+ * Walks through the events_ list and outputs the structures discovered to
+ * model_.
+ */
+ importEvents: function() {
+ // Walk through events
+ var events = this.events_;
+ for (var eI = 0; eI < events.length; eI++) {
+ var event = events[eI];
+ var ptid = event.pid + ':' + event.tid;
+
+ if (!(ptid in this.threadStateByPTID_))
+ this.threadStateByPTID_[ptid] = new ThreadState();
+ var state = this.threadStateByPTID_[ptid];
+
+ if (event.ph == 'B') {
+ this.processBegin(eI, state, event);
+ } else if (event.ph == 'E') {
+ this.processEnd(state, event);
+ } else if (event.ph == 'I') {
+ // Treat an Instant event as a duration 0 slice.
+ // TimelineSliceTrack's redraw() knows how to handle this.
+ this.processBegin(eI, state, event);
+ this.processEnd(state, event);
+ } else if (event.ph == 'C') {
+ this.processCounter(event);
+ } else if (event.ph == 'M') {
+ if (event.name == 'thread_name') {
+ var thread = this.model_.getOrCreateProcess(event.pid)
+ .getOrCreateThread(event.tid);
+ thread.name = event.args.name;
+ } else {
+ this.model_.importErrors.push(
+ 'Unrecognized metadata name: ' + event.name);
+ }
+ } else {
+ this.model_.importErrors.push(
+ 'Unrecognized event phase: ' + event.ph +
+ '(' + event.name + ')');
+ }
+ }
+
+ // Autoclose any open slices.
+ var hasOpenSlices = false;
+ for (var ptid in this.threadStateByPTID_) {
+ var state = this.threadStateByPTID_[ptid];
+ hasOpenSlices |= state.openSlices.length > 0;
+ }
+ if (hasOpenSlices)
+ this.autoCloseOpenSlices();
+ }
+ };
+
+ tracing.TimelineModel.registerImporter(TraceEventImporter);
+
+ return {
+ TraceEventImporter: TraceEventImporter
+ };
+});
diff --git a/chrome/browser/resources/tracing/trace_event_importer_test.html b/chrome/browser/resources/tracing/trace_event_importer_test.html
new file mode 100644
index 0000000..8dc9968
--- /dev/null
+++ b/chrome/browser/resources/tracing/trace_event_importer_test.html
@@ -0,0 +1,559 @@
+<!DOCTYPE html>
+<html>
+<!--
+Copyright (c) 2011 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.
+-->
+<head>
+<title>TraceEventImporter tests</title>
+<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script>
+<script src="../shared/js/cr.js"></script>
+<script src="../shared/js/cr/event_target.js"></script>
+<script src="test_utils.js"></script>
+<script src="timeline_model.js"></script>
+<script src="trace_event_importer.js"></script>
+<script>
+ goog.require('goog.testing.jsunit');
+</script>
+
+</head>
+<body>
+<script>
+
+function testBasicSingleThreadNonnestedParsing() {
+ var events = [
+ {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'},
+ {name: 'b', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ var m = new tracing.TimelineModel(events);
+ assertEquals(1, m.numProcesses);
+ var p = m.processes[52];
+ assertNotUndefined(p);
+
+ assertEquals(1, p.numThreads);
+ var t = p.threads[53];
+ assertNotUndefined(t);
+ assertEquals(1, t.subRows.length);
+ assertEquals(53, t.tid);
+ var subRow = t.subRows[0];
+ assertEquals(2, subRow.length);
+ var slice = subRow[0];
+ assertEquals('a', slice.title);
+ assertEquals(0, slice.start);
+ assertAlmostEquals((560 - 520) / 1000, slice.duration);
+ assertEquals(0, slice.subSlices.length);
+
+ slice = subRow[1];
+ assertEquals('b', slice.title);
+ assertAlmostEquals((629 - 520) / 1000, slice.start);
+ assertAlmostEquals((631 - 629) / 1000, slice.duration);
+ assertEquals(0, slice.subSlices.length);
+}
+
+function testNestedParsing() {
+ var events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'a', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'E'}
+ ];
+ var m = new tracing.TimelineModel(events);
+
+ var p = m.processes[1];
+ var t = p.threads[1];
+ assertEquals(2, t.subRows.length);
+ var subRow = t.subRows[0];
+ assertEquals(1, subRow.length);
+ var slice = subRow[0];
+ assertEquals('a', slice.title);
+ assertEquals((4 - 1) / 1000, slice.duration);
+ assertEquals(1, slice.subSlices.length);
+
+ slice = slice.subSlices[0];
+ assertEquals('b', slice.title);
+ assertEquals((2 - 1) / 1000, slice.start);
+ assertEquals((3 - 2) / 1000, slice.duration);
+ assertEquals(0, slice.subSlices.length);
+
+ subRow = t.subRows[1];
+ slice = subRow[0];
+ assertEquals(t.subRows[0][0].subSlices[0], slice);
+}
+
+function testAutoclosing() {
+ var events = [
+ // Slice that doesn't finish.
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+
+ // Slice that does finish to give an 'end time' to make autoclosing work.
+ {name: 'b', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 2, ph: 'E'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ var p = m.processes[1];
+ var t = p.threads[1];
+ var subRow = t.subRows[0];
+ var slice = subRow[0];
+ assertEquals('a', slice.title);
+ assertTrue(slice.didNotFinish);
+ assertEquals(0, slice.start);
+ assertEquals((2 - 1) / 1000, slice.duration);
+}
+
+function testAutoclosingLoneBegin() {
+ var events = [
+ // Slice that doesn't finish.
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ var p = m.processes[1];
+ var t = p.threads[1];
+ var subRow = t.subRows[0];
+ var slice = subRow[0];
+ assertEquals('a', slice.title);
+ assertTrue(slice.didNotFinish);
+ assertEquals(0, slice.start);
+ assertEquals(0, slice.duration);
+}
+
+function testAutoclosingWithSubTasks() {
+ var events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b1', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b1', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'b2', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ var p = m.processes[1];
+ var t = p.threads[1];
+ assertEquals(2, t.subRows.length);
+ assertEquals(1, t.subRows[0].length);
+ assertEquals(2, t.subRows[1].length);
+}
+
+function testAutoclosingWithEventsOutsideRange() {
+ var events = [
+ // Slice that begins before min and ends after max of the other threads.
+ {name: 'a', args: {}, pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'},
+
+ // Slice that does finish to give an 'end time' to establish a basis
+ {name: 'b', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 2, ph: 'E'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ var p = m.processes[1];
+ var t = p.threads[1];
+ var subRow = t.subRows[0];
+ assertEquals('a', subRow[0].title);
+ assertEquals(0, subRow[0].start);
+ assertEquals(0.003, subRow[0].duration);
+
+ var t = p.threads[2];
+ var subRow = t.subRows[0];
+ assertEquals('b', subRow[0].title);
+ assertEquals(0.001, subRow[0].start);
+ assertEquals(0.001, subRow[0].duration);
+
+ // 0.00345 instead of 0.003 because TimelineModel bloats the world range by
+ // 15%.
+ assertEquals(-0.00045, m.minTimestamp);
+ assertEquals(0.00345, m.maxTimestamp);
+
+}
+
+function testNestedAutoclosing() {
+ var events = [
+ // Tasks that dont finish.
+ {name: 'a1', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a2', args: {}, pid: 1, ts: 1.5, cat: 'foo', tid: 1, ph: 'B'},
+
+ // Slice that does finish to give an 'end time' to make autoclosing work.
+ {name: 'b', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 2, ph: 'E'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ var p = m.processes[1];
+ var t = p.threads[1];
+ var subRow = t.subRows[0];
+ var slice = subRow[0];
+ assertEquals('a1', slice.title);
+ assertTrue(slice.didNotFinish);
+ assertEquals(0, slice.start);
+ assertEquals((2 - 1) / 1000, slice.duration);
+
+ var slice = slice.subSlices[0];
+ assertEquals('a2', slice.title);
+ assertTrue(slice.didNotFinish);
+ assertEquals((1.5 - 1) / 1000, slice.start);
+ assertEquals((2 - 1.5) / 1000, slice.duration);
+}
+
+function testTaskColoring() {
+ // The test below depends on hashing of 'a' != 'b'. Fail early if that
+ // assumption is incorrect.
+ assertNotEquals(tracing.getStringHash('a'), tracing.getStringHash('b'));
+
+ var events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'b', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'a', args: {}, pid: 1, ts: 5, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 6, cat: 'foo', tid: 1, ph: 'E'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ var p = m.processes[1];
+ var t = p.threads[1];
+ var subRow = t.subRows[0];
+ var a1 = subRow[0];
+ assertEquals('a', a1.title);
+ var b = subRow[1];
+ assertEquals('b', b.title);
+ assertNotEquals(a1.colorId, b.colorId);
+ var a2 = subRow[0];
+ assertEquals('a', a2.title);
+ assertEquals(a1.colorId, a2.colorId);
+}
+
+function testMultipleThreadParsing() {
+ var events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'b', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 2, ph: 'E'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ assertEquals(1, m.numProcesses);
+ var p = m.processes[1];
+ assertNotUndefined(p);
+
+ assertEquals(2, p.numThreads);
+
+ // Check thread 1.
+ var t = p.threads[1];
+ assertNotUndefined(t);
+ assertEquals(1, t.subRows.length);
+ assertEquals(1, t.tid);
+
+ var subRow = t.subRows[0];
+ assertEquals(1, subRow.length);
+ var slice = subRow[0];
+ assertEquals('a', slice.title);
+ assertEquals(0, slice.start);
+ assertEquals((2 - 1) / 1000, slice.duration);
+ assertEquals(0, slice.subSlices.length);
+
+ // Check thread 2.
+ var t = p.threads[2];
+ assertNotUndefined(t);
+ assertEquals(1, t.subRows.length);
+ assertEquals(2, t.tid);
+
+ subRow = t.subRows[0];
+ assertEquals(1, subRow.length);
+ slice = subRow[0];
+ assertEquals('b', slice.title);
+ assertEquals((3 - 1) / 1000, slice.start);
+ assertEquals((4 - 3) / 1000, slice.duration);
+ assertEquals(0, slice.subSlices.length);
+}
+
+function testMultiplePidParsing() {
+ var events = [
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'b', args: {}, pid: 2, ts: 3, cat: 'foo', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 2, ts: 4, cat: 'foo', tid: 2, ph: 'E'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ assertEquals(2, m.numProcesses);
+ var p = m.processes[1];
+ assertNotUndefined(p);
+
+ assertEquals(1, p.numThreads);
+
+ // Check process 1 thread 1.
+ var t = p.threads[1];
+ assertNotUndefined(t);
+ assertEquals(1, t.subRows.length);
+ assertEquals(1, t.tid);
+
+ var subRow = t.subRows[0];
+ assertEquals(1, subRow.length);
+ var slice = subRow[0];
+ assertEquals('a', slice.title);
+ assertEquals(0, slice.start);
+ assertEquals((2 - 1) / 1000, slice.duration);
+ assertEquals(0, slice.subSlices.length);
+
+ // Check process 2 thread 2.
+ var p = m.processes[2];
+ assertNotUndefined(p);
+ assertEquals(1, p.numThreads);
+ var t = p.threads[2];
+ assertNotUndefined(t);
+ assertEquals(1, t.subRows.length);
+ assertEquals(2, t.tid);
+
+ subRow = t.subRows[0];
+ assertEquals(1, subRow.length);
+ slice = subRow[0];
+ assertEquals('b', slice.title);
+ assertEquals((3 - 1) / 1000, slice.start);
+ assertEquals((4 - 3) / 1000, slice.duration);
+ assertEquals(0, slice.subSlices.length);
+
+ // Check getAllThreads.
+ assertArrayEquals([m.processes[1].threads[1], m.processes[2].threads[2]],
+ m.getAllThreads());
+}
+
+// Thread names.
+function testThreadNames() {
+ var events = [
+ {name: 'thread_name', args: {name: 'Thread 1'},
+ pid: 1, ts: 0, tid: 1, ph: 'M'},
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'b', args: {}, pid: 2, ts: 3, cat: 'foo', tid: 2, ph: 'B'},
+ {name: 'b', args: {}, pid: 2, ts: 4, cat: 'foo', tid: 2, ph: 'E'},
+ {name: 'thread_name', args: {name: 'Thread 2'},
+ pid: 2, ts: 0, tid: 2, ph: 'M'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ assertEquals('Thread 1', m.processes[1].threads[1].name);
+ assertEquals('Thread 2', m.processes[2].threads[2].name);
+}
+
+// User time.
+function testUserTime() {
+ var events = [
+ {name: 'thread_name', args: {name: 'Thread 1'},
+ pid: 1, ts: 0, tid: 1, ph: 'M'},
+ {name: 'a', args: {}, pid: 1, ts: 1, uts: 10, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'a', args: {}, pid: 1, ts: 2, uts: 20, cat: 'foo', tid: 1, ph: 'E'},
+ {name: 'a', args: {}, pid: 1, ts: 2 , uts: 60, cat: 'foo', tid: 1, ph: 'I'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ var subRow = m.processes[1].threads[1].subRows[0];
+ assertEquals(0.01, subRow[0].startInUserTime);
+ assertEquals(0.01, subRow[0].durationInUserTime);
+ assertEquals(0.06, subRow[1].startInUserTime);
+ assertEquals(0, subRow[1].durationInUserTime);
+}
+
+
+function testImmediateParsing() {
+ var events = [
+ // Need to include immediates inside a task so the timeline
+ // recentering/zeroing doesn't clobber their timestamp.
+ {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+ {name: 'immediate', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'I'},
+ {name: 'a', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'E'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ var p = m.processes[1];
+ var t = p.threads[1];
+ assertEquals(2, t.subRows.length);
+ var subRow = t.subRows[0];
+ assertEquals(1, subRow.length);
+ var slice = subRow[0];
+ assertEquals('a', slice.title);
+ assertEquals((4 - 1) / 1000, slice.duration);
+ assertEquals(1, slice.subSlices.length);
+
+ var immed = slice.subSlices[0];
+ assertEquals('immediate', immed.title);
+ assertEquals((2 - 1) / 1000, immed.start);
+ assertEquals(0, immed.duration);
+ assertEquals(0, immed.subSlices.length);
+
+ subRow = t.subRows[1];
+ assertEquals(immed, subRow[0]);
+}
+
+function testSimpleCounter() {
+ var events = [
+ {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1,
+ ph: 'C'},
+ {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
+ ph: 'C'},
+ {name: 'ctr', args: {'value': 0}, pid: 1, ts: 20, cat: 'foo', tid: 1,
+ ph: 'C'}
+
+ ];
+ var m = new tracing.TimelineModel(events);
+ var p = m.processes[1];
+ var ctr = m.processes[1].counters['foo.ctr'];
+
+ assertEquals('ctr', ctr.name);
+ assertEquals(3, ctr.numSamples);
+ assertEquals(1, ctr.numSeries);
+
+ assertArrayEquals(['value'], ctr.seriesNames);
+ assertArrayEquals([tracing.getStringColorId('ctr.value')], ctr.seriesColors);
+ assertArrayEquals([0, 0.01, 0.02], ctr.timestamps);
+ assertArrayEquals([0, 10, 0], ctr.samples);
+ assertArrayEquals([0, 10, 0], ctr.totals);
+ assertEquals(10, ctr.maxTotal);
+}
+
+function testInstanceCounter() {
+ var events = [
+ {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1,
+ ph: 'C', id: 0},
+ {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
+ ph: 'C', id: 0},
+ {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
+ ph: 'C', id: 1},
+ {name: 'ctr', args: {'value': 20}, pid: 1, ts: 15, cat: 'foo', tid: 1,
+ ph: 'C', id: 1},
+ {name: 'ctr', args: {'value': 30}, pid: 1, ts: 18, cat: 'foo', tid: 1,
+ ph: 'C', id: 1}
+ ];
+ var m = new tracing.TimelineModel(events);
+ var p = m.processes[1];
+ var ctr = m.processes[1].counters['foo.ctr[0]'];
+ assertEquals('ctr[0]', ctr.name);
+ assertEquals(2, ctr.numSamples);
+ assertEquals(1, ctr.numSeries);
+ assertArrayEquals([0, 0.01], ctr.timestamps);
+ assertArrayEquals([0, 10], ctr.samples);
+
+ var ctr = m.processes[1].counters['foo.ctr[1]'];
+ assertEquals('ctr[1]', ctr.name);
+ assertEquals(3, ctr.numSamples);
+ assertEquals(1, ctr.numSeries);
+ assertArrayEquals([0.01, 0.015, 0.018], ctr.timestamps);
+ assertArrayEquals([10, 20, 30], ctr.samples);
+}
+
+function testMultiCounterUpdateBounds() {
+ var ctr = new tracing.TimelineCounter(undefined, 'testBasicCounter',
+'testBasicCounter');
+ ctr.numSeries = 1;
+ ctr.seriesNames = ['value1', 'value2'];
+ ctr.seriesColors = ['testBasicCounter.value1', 'testBasicCounter.value2'];
+ ctr.timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+ ctr.samples = [0, 0,
+ 1, 0,
+ 1, 1,
+ 2, 1.1,
+ 3, 0,
+ 1, 7,
+ 3, 0,
+ 3.1, 0.5];
+ ctr.updateBounds();
+ assertEquals(0, ctr.minTimestamp);
+ assertEquals(7, ctr.maxTimestamp);
+ assertEquals(8, ctr.maxTotal);
+ assertArrayEquals([0, 0,
+ 1, 1,
+ 1, 2,
+ 2, 3.1,
+ 3, 3,
+ 1, 8,
+ 3, 3,
+ 3.1, 3.6], ctr.totals);
+}
+
+function testMultiCounter() {
+ var events = [
+ {name: 'ctr', args: {'value1': 0, 'value2': 7}, pid: 1, ts: 0, cat: 'foo',
+ tid: 1, ph: 'C'},
+ {name: 'ctr', args: {'value1': 10, 'value2': 4}, pid: 1, ts: 10, cat: 'foo',
+ tid: 1, ph: 'C'},
+ {name: 'ctr', args: {'value1': 0, 'value2': 1 }, pid: 1, ts: 20, cat: 'foo',
+ tid: 1, ph: 'C'}
+ ];
+ var m = new tracing.TimelineModel(events);
+ var p = m.processes[1];
+ var ctr = m.processes[1].counters['foo.ctr'];
+ assertEquals('ctr', ctr.name);
+
+ assertEquals('ctr', ctr.name);
+ assertEquals(3, ctr.numSamples);
+ assertEquals(2, ctr.numSeries);
+
+ assertArrayEquals(['value1', 'value2'], ctr.seriesNames);
+ assertArrayEquals([tracing.getStringColorId('ctr.value1'),
+ tracing.getStringColorId('ctr.value2')],
+ ctr.seriesColors);
+ assertArrayEquals([0, 0.01, 0.02], ctr.timestamps);
+ assertArrayEquals([0, 7,
+ 10, 4,
+ 0, 1], ctr.samples);
+ assertArrayEquals([0, 7,
+ 10, 14,
+ 0, 1], ctr.totals);
+ assertEquals(14, ctr.maxTotal);
+}
+
+function testImportObjectInsteadOfArray() {
+ var events = { traceEvents: [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ] };
+
+ var m = new tracing.TimelineModel(events);
+ assertEquals(1, m.numProcesses);
+}
+
+function testImportString() {
+ var events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ var m = new tracing.TimelineModel(JSON.stringify(events));
+ assertEquals(1, m.numProcesses);
+}
+
+function testImportStringWithTrailingNewLine() {
+ var events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ var m = new tracing.TimelineModel(JSON.stringify(events) + '\n');
+ assertEquals(1, m.numProcesses);
+}
+
+function testImportStringWithMissingCloseSquareBracket() {
+ var events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ var tmp = JSON.stringify(events);
+ assertEquals(']', tmp[tmp.length - 1]);
+
+ // Drop off the trailing ]
+ var dropped = tmp.substring(0, tmp.length - 1);
+ var m = new tracing.TimelineModel(dropped);
+ assertEquals(1, m.numProcesses);
+}
+
+function testImportStringWithMissingCloseSquareBracketAndNewline() {
+ var events = [
+ {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+ {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+ ];
+
+ var tmp = JSON.stringify(events);
+ assertEquals(']', tmp[tmp.length - 1]);
+
+ // Drop off the trailing ] and add a newline
+ var dropped = tmp.substring(0, tmp.length - 1);
+ var m = new tracing.TimelineModel(dropped + '\n');
+ assertEquals(1, m.numProcesses);
+}
+
+</script>
+</body>
+</html>