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