diff options
author | nduca@chromium.org <nduca@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-12-20 20:13:28 +0000 |
---|---|---|
committer | nduca@chromium.org <nduca@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-12-20 20:13:28 +0000 |
commit | c93e46886678366ae916d622334d5748d60cf999 (patch) | |
tree | daa88333633174cca0feb3ddbcdc5f51c70edbc3 | |
parent | 02f996c2e04c10fa4a2afe2f29bf7d3bcc069443 (diff) | |
download | chromium_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
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> |