// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Implements a WebSocket client that receives * a stream of slices from a server. * */ base.require('model'); base.require('model.slice'); base.exportTo('tracing.importer', function() { var STATE_PAUSED = 0x1; var STATE_CAPTURING = 0x2; /** * Converts a stream of trace data from a websocket into a model. * * Events consumed by this importer have the following JSON structure: * * { * 'cmd': 'commandName', * ... command specific data * } * * The importer understands 2 commands: * 'ptd' (Process Thread Data) * 'pcd' (Process Counter Data) * * The command specific data is as follows: * * { * 'pid': 'Remote Process Id', * 'td': { * 'n': 'Thread Name Here', * 's: [ { * 'l': 'Slice Label', * 's': startTime, * 'e': endTime * }, ... ] * } * } * * { * 'pid' 'Remote Process Id', * 'cd': { * 'n': 'Counter Name', * 'sn': ['Series Name',...] * 'sc': [seriesColor, ...] * 'c': [ * { * 't': timestamp, * 'v': [value0, value1, ...] * }, * .... * ] * } * } * @param {Model} model that will be updated * when events are received. * @constructor */ function TimelineStreamImporter(model) { var self = this; this.model_ = model; this.connection_ = undefined; this.state_ = STATE_CAPTURING; this.connectionOpenHandler_ = this.connectionOpenHandler_.bind(this); this.connectionCloseHandler_ = this.connectionCloseHandler_.bind(this); this.connectionErrorHandler_ = this.connectionErrorHandler_.bind(this); this.connectionMessageHandler_ = this.connectionMessageHandler_.bind(this); } TimelineStreamImporter.prototype = { __proto__: base.EventTarget.prototype, cleanup_: function() { if (!this.connection_) return; this.connection_.removeEventListener('open', this.connectionOpenHandler_); this.connection_.removeEventListener('close', this.connectionCloseHandler_); this.connection_.removeEventListener('error', this.connectionErrorHandler_); this.connection_.removeEventListener('message', this.connectionMessageHandler_); }, connectionOpenHandler_: function() { this.dispatchEvent({'type': 'connect'}); }, connectionCloseHandler_: function() { this.dispatchEvent({'type': 'disconnect'}); this.cleanup_(); }, connectionErrorHandler_: function() { this.dispatchEvent({'type': 'connectionerror'}); this.cleanup_(); }, connectionMessageHandler_: function(event) { var packet = JSON.parse(event.data); var command = packet['cmd']; var pid = packet['pid']; var modelDirty = false; if (command == 'ptd') { var process = this.model_.getOrCreateProcess(pid); var threadData = packet['td']; var threadName = threadData['n']; var threadSlices = threadData['s']; var thread = process.getOrCreateThread(threadName); for (var s = 0; s < threadSlices.length; s++) { var slice = threadSlices[s]; thread.slices.push(new tracing.model.Slice('streamed', slice['l'], 0, slice['s'], {}, slice['e'] - slice['s'])); } modelDirty = true; } else if (command == 'pcd') { var process = this.model_.getOrCreateProcess(pid); var counterData = packet['cd']; var counterName = counterData['n']; var counterSeriesNames = counterData['sn']; var counterSeriesColors = counterData['sc']; var counterValues = counterData['c']; var counter = process.getOrCreateCounter('streamed', counterName); if (counterSeriesNames.length != counterSeriesColors.length) { var importError = 'Streamed counter name length does not match' + 'counter color length' + counterSeriesNames.length + ' vs ' + counterSeriesColors.length; this.model_.importErrors.push(importError); return; } if (counter.seriesNames.length == 0) { // First time counter.seriesNames = counterSeriesNames; counter.seriesColors = counterSeriesColors; } else { if (counter.seriesNames.length != counterSeriesNames.length) { var importError = 'Streamed counter ' + counterName + 'changed number of seriesNames'; this.model_.importErrors.push(importError); return; } else { for (var i = 0; i < counter.seriesNames.length; i++) { var oldSeriesName = counter.seriesNames[i]; var newSeriesName = counterSeriesNames[i]; if (oldSeriesName != newSeriesName) { var importError = 'Streamed counter ' + counterName + 'series name changed from ' + oldSeriesName + ' to ' + newSeriesName; this.model_.importErrors.push(importError); return; } } } } for (var c = 0; c < counterValues.length; c++) { var count = counterValues[c]; var x = count['t']; var y = count['v']; counter.timestamps.push(x); counter.samples = counter.samples.concat(y); } modelDirty = true; } if (modelDirty == true) { this.model_.updateBounds(); this.dispatchEvent({'type': 'modelchange', 'model': this.model_}); } }, get connected() { if (this.connection_ !== undefined && this.connection_.readyState == WebSocket.OPEN) { return true; } return false; }, get paused() { return this.state_ == STATE_PAUSED; }, /** * Connects the stream to a websocket. * @param {WebSocket} wsConnection The websocket to use for the stream */ connect: function(wsConnection) { this.connection_ = wsConnection; this.connection_.addEventListener('open', this.connectionOpenHandler_); this.connection_.addEventListener('close', this.connectionCloseHandler_); this.connection_.addEventListener('error', this.connectionErrorHandler_); this.connection_.addEventListener('message', this.connectionMessageHandler_); }, pause: function() { if (this.state_ == STATE_PAUSED) throw new Error('Already paused.'); if (!this.connection_) throw new Error('Not connected.'); this.connection_.send(JSON.stringify({'cmd': 'pause'})); this.state_ = STATE_PAUSED; }, resume: function() { if (this.state_ == STATE_CAPTURING) throw new Error('Already capturing.'); if (!this.connection_) throw new Error('Not connected.'); this.connection_.send(JSON.stringify({'cmd': 'resume'})); this.state_ = STATE_CAPTURING; } }; return { TimelineStreamImporter: TimelineStreamImporter }; });