1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5'use strict'; 6 7/** 8 * @fileoverview Implements a WebSocket client that receives 9 * a stream of slices from a server. 10 * 11 */ 12 13base.require('timeline_model'); 14base.require('timeline_slice'); 15 16base.exportTo('tracing', function() { 17 18 var STATE_PAUSED = 0x1; 19 var STATE_CAPTURING = 0x2; 20 21 /** 22 * Converts a stream of trace data from a websocket into a 23 * timeline model. 24 * 25 * Events consumed by this importer have the following JSON structure: 26 * 27 * { 28 * 'cmd': 'commandName', 29 * ... command specific data 30 * } 31 * 32 * The importer understands 2 commands: 33 * 'ptd' (Process Thread Data) 34 * 'pcd' (Process Counter Data) 35 * 36 * The command specific data is as follows: 37 * 38 * { 39 * 'pid': 'Remote Process Id', 40 * 'td': { 41 * 'n': 'Thread Name Here', 42 * 's: [ { 43 * 'l': 'Slice Label', 44 * 's': startTime, 45 * 'e': endTime 46 * }, ... ] 47 * } 48 * } 49 * 50 * { 51 * 'pid' 'Remote Process Id', 52 * 'cd': { 53 * 'n': 'Counter Name', 54 * 'sn': ['Series Name',...] 55 * 'sc': [seriesColor, ...] 56 * 'c': [ 57 * { 58 * 't': timestamp, 59 * 'v': [value0, value1, ...] 60 * }, 61 * .... 62 * ] 63 * } 64 * } 65 * @param {TimelineModel} model that will be updated 66 * when events are received. 67 * @constructor 68 */ 69 function TimelineStreamImporter(model) { 70 var self = this; 71 this.model_ = model; 72 this.connection_ = undefined; 73 this.state_ = STATE_CAPTURING; 74 this.connectionOpenHandler_ = 75 this.connectionOpenHandler_.bind(this); 76 this.connectionCloseHandler_ = 77 this.connectionCloseHandler_.bind(this); 78 this.connectionErrorHandler_ = 79 this.connectionErrorHandler_.bind(this); 80 this.connectionMessageHandler_ = 81 this.connectionMessageHandler_.bind(this); 82 } 83 84 TimelineStreamImporter.prototype = { 85 __proto__: base.EventTarget.prototype, 86 87 cleanup_: function() { 88 if (!this.connection_) 89 return; 90 this.connection_.removeEventListener('open', 91 this.connectionOpenHandler_); 92 this.connection_.removeEventListener('close', 93 this.connectionCloseHandler_); 94 this.connection_.removeEventListener('error', 95 this.connectionErrorHandler_); 96 this.connection_.removeEventListener('message', 97 this.connectionMessageHandler_); 98 }, 99 100 connectionOpenHandler_: function() { 101 this.dispatchEvent({'type': 'connect'}); 102 }, 103 104 connectionCloseHandler_: function() { 105 this.dispatchEvent({'type': 'disconnect'}); 106 this.cleanup_(); 107 }, 108 109 connectionErrorHandler_: function() { 110 this.dispatchEvent({'type': 'connectionerror'}); 111 this.cleanup_(); 112 }, 113 114 connectionMessageHandler_: function(event) { 115 var packet = JSON.parse(event.data); 116 var command = packet['cmd']; 117 var pid = packet['pid']; 118 var modelDirty = false; 119 if (command == 'ptd') { 120 var process = this.model_.getOrCreateProcess(pid); 121 var threadData = packet['td']; 122 var threadName = threadData['n']; 123 var threadSlices = threadData['s']; 124 var thread = process.getOrCreateThread(threadName); 125 for (var s = 0; s < threadSlices.length; s++) { 126 var slice = threadSlices[s]; 127 thread.slices.push(new tracing.TimelineSlice('streamed', 128 slice['l'], 129 0, 130 slice['s'], 131 {}, 132 slice['e'] - slice['s'])); 133 } 134 modelDirty = true; 135 } else if (command == 'pcd') { 136 var process = this.model_.getOrCreateProcess(pid); 137 var counterData = packet['cd']; 138 var counterName = counterData['n']; 139 var counterSeriesNames = counterData['sn']; 140 var counterSeriesColors = counterData['sc']; 141 var counterValues = counterData['c']; 142 var counter = process.getOrCreateCounter('streamed', counterName); 143 if (counterSeriesNames.length != counterSeriesColors.length) { 144 var importError = 'Streamed counter name length does not match' + 145 'counter color length' + counterSeriesNames.length + 146 ' vs ' + counterSeriesColors.length; 147 this.model_.importErrors.push(importError); 148 return; 149 } 150 if (counter.seriesNames.length == 0) { 151 // First time 152 counter.seriesNames = counterSeriesNames; 153 counter.seriesColors = counterSeriesColors; 154 } else { 155 if (counter.seriesNames.length != counterSeriesNames.length) { 156 var importError = 'Streamed counter ' + counterName + 157 'changed number of seriesNames'; 158 this.model_.importErrors.push(importError); 159 return; 160 } else { 161 for (var i = 0; i < counter.seriesNames.length; i++) { 162 var oldSeriesName = counter.seriesNames[i]; 163 var newSeriesName = counterSeriesNames[i]; 164 if (oldSeriesName != newSeriesName) { 165 var importError = 'Streamed counter ' + counterName + 166 'series name changed from ' + 167 oldSeriesName + ' to ' + 168 newSeriesName; 169 this.model_.importErrors.push(importError); 170 return; 171 } 172 } 173 } 174 } 175 for (var c = 0; c < counterValues.length; c++) { 176 var count = counterValues[c]; 177 var x = count['t']; 178 var y = count['v']; 179 counter.timestamps.push(x); 180 counter.samples = counter.samples.concat(y); 181 } 182 modelDirty = true; 183 } 184 if (modelDirty == true) { 185 this.model_.updateBounds(); 186 this.dispatchEvent({'type': 'modelchange', 187 'model': this.model_}); 188 } 189 }, 190 191 get connected() { 192 if (this.connection_ !== undefined && 193 this.connection_.readyState == WebSocket.OPEN) { 194 return true; 195 } 196 return false; 197 }, 198 199 get paused() { 200 return this.state_ == STATE_PAUSED; 201 }, 202 203 /** 204 * Connects the stream to a websocket. 205 * @param {WebSocket} wsConnection The websocket to use for the stream 206 */ 207 connect: function(wsConnection) { 208 this.connection_ = wsConnection; 209 this.connection_.addEventListener('open', 210 this.connectionOpenHandler_); 211 this.connection_.addEventListener('close', 212 this.connectionCloseHandler_); 213 this.connection_.addEventListener('error', 214 this.connectionErrorHandler_); 215 this.connection_.addEventListener('message', 216 this.connectionMessageHandler_); 217 }, 218 219 pause: function() { 220 if (this.state_ == STATE_PAUSED) 221 throw new Error('Already paused.'); 222 if (!this.connection_) 223 throw new Error('Not connected.'); 224 this.connection_.send(JSON.stringify({'cmd': 'pause'})); 225 this.state_ = STATE_PAUSED; 226 }, 227 228 resume: function() { 229 if (this.state_ == STATE_CAPTURING) 230 throw new Error('Already capturing.'); 231 if (!this.connection_) 232 throw new Error('Not connected.'); 233 this.connection_.send(JSON.stringify({'cmd': 'resume'})); 234 this.state_ = STATE_CAPTURING; 235 } 236 }; 237 238 return { 239 TimelineStreamImporter: TimelineStreamImporter 240 }; 241}); 242