1// Copyright 2014 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<include src="../../../../third_party/polymer_legacy/platform/platform.js">
6<include src="../../../../third_party/polymer_legacy/polymer/polymer.js">
7
8/**
9 * Formats size to a human readable form.
10 * @param {number} size Size in bytes.
11 * @return {string} Output string in a human-readable format.
12 */
13function formatSizeCommon(size) {
14  if (size < 1024)
15    return size + ' B';
16  if (size < 1024 * 1024)
17    return Math.round(size / 1024) + ' KB';
18  return Math.round(size / 1024 / 1024) + ' MB';
19}
20
21// Defines the file-systems element.
22Polymer('file-systems', {
23  /**
24   * Called when the element is created.
25   */
26  ready: function() {
27  },
28
29  /**
30   * Selects an active file system from the list.
31   * @param {Event} event Event.
32   * @param {number} detail Detail.
33   * @param {HTMLElement} sender Sender.
34   */
35  rowClicked: function(event, detail, sender) {
36    var requestEventsNode = document.querySelector('#request-events');
37    requestEventsNode.hidden = false;
38    requestEventsNode.model = [];
39
40    var requestTimelineNode = document.querySelector('#request-timeline');
41    requestTimelineNode.hidden = false;
42    requestTimelineNode.model = [];
43
44    chrome.send('selectFileSystem', [sender.dataset.extensionId,
45      sender.dataset.id]);
46  },
47
48  /**
49   * List of provided file system information maps.
50   * @type {Array.<Object>}
51   */
52  model: []
53});
54
55// Defines the request-log element.
56Polymer('request-events', {
57  /**
58   * Called when the element is created.
59   */
60  ready: function() {
61  },
62
63  /**
64   * Formats time to a hh:mm:ss.xxxx format.
65   * @param {Date} time Input time.
66   * @return {string} Output string in a human-readable format.
67   */
68  formatTime: function(time) {
69    return ('0' + time.getHours()).slice(-2) + ':' +
70           ('0' + time.getMinutes()).slice(-2) + ':' +
71           ('0' + time.getSeconds()).slice(-2) + '.' +
72           ('000' + time.getMilliseconds()).slice(-3);
73  },
74
75  /**
76   * Formats size to a human readable form.
77   * @param {number} size Size in bytes.
78   * @return {string} Output string in a human-readable format.
79   */
80  formatSize: function(size) {
81    return formatSizeCommon(size);
82  },
83
84  /**
85   * Formats a boolean value to human-readable form.
86   * @param {boolean=} opt_hasMore Input value.
87   * @return {string} Output string in a human-readable format.
88   */
89  formatHasMore: function(opt_hasMore) {
90    if (opt_hasMore == undefined)
91      return '';
92
93    return opt_hasMore ? 'HAS_MORE' : 'LAST';
94  },
95
96  /**
97   * Formats execution time to human-readable form.
98   * @param {boolean=} opt_executionTime Input value.
99   * @return {string} Output string in a human-readable format.
100   */
101  formatExecutionTime: function(opt_executionTime) {
102    if (opt_executionTime == undefined)
103      return '';
104
105    return opt_executionTime + ' ms';
106  },
107
108  /**
109   * List of events.
110   * @type {Array.<Object>}
111   */
112  model: []
113});
114
115// Defines the request-timeline element.
116Polymer('request-timeline', {
117  /**
118   * Step for zoomin in and out.
119   * @type {number}
120   * @const
121   */
122  SCALE_STEP: 1.5,
123
124  /**
125   * Height of each row in the chart in pixels.
126   * @type {number}
127   * @const
128   */
129  ROW_HEIGHT: 14,
130
131  /**
132   * Observes changes in the model.
133   * @type {Object.<string, string>}
134   */
135  observe: {
136    'model.length': 'chartUpdate'
137  },
138
139  /**
140   * Called when the element is created.
141   */
142  ready: function() {
143    // Update active requests in the background for nice animation.
144    var activeUpdateAnimation = function() {
145      this.activeUpdate();
146      requestAnimationFrame(activeUpdateAnimation);
147    }.bind(this);
148    activeUpdateAnimation();
149  },
150
151  /**
152   * Formats size to a human readable form.
153   * @param {number} size Size in bytes.
154   * @return {string} Output string in a human-readable format.
155   */
156  formatSize: function(size) {
157    return formatSizeCommon(size);
158  },
159
160  /**
161   * Zooms in the timeline.
162   * @param {Event} event Event.
163   * @param {number} detail Detail.
164   * @param {HTMLElement} sender Sender.
165   */
166  zoomInClicked: function(event, detail, sender) {
167    this.scale *= this.SCALE_STEP;
168  },
169
170  /**
171   * Zooms out the timeline.
172   * @param {Event} event Event.
173   * @param {number} detail Detail.
174   * @param {HTMLElement} sender Sender.
175   */
176  zoomOutClicked: function(event, detail, sender) {
177    this.scale /= this.SCALE_STEP;
178  },
179
180  /**
181   * Selects or deselects an element on the timeline.
182   * @param {Event} event Event.
183   * @param {number} detail Detail.
184   * @param {HTMLElement} sender Sender.
185   */
186  elementClicked: function(event, detail, sender) {
187    if (sender.dataset.id in this.selected) {
188      delete this.selected[sender.dataset.id];
189      sender.classList.remove('selected');
190    } else {
191      this.selected[sender.dataset.id] = true;
192      sender.classList.add('selected');
193    }
194
195    var requestEventsNode = document.querySelector('#request-events');
196    requestEventsNode.hidden = false;
197
198    requestEventsNode.model = [];
199    for (var i = 0; i < this.model.length; i++) {
200      if (this.model[i].id in this.selected)
201        requestEventsNode.model.push(this.model[i]);
202    }
203  },
204
205  /**
206   * Updates chart elements of active requests, so they grow with time.
207   */
208  activeUpdate: function() {
209    if (Object.keys(this.active).length == 0)
210      return;
211
212    for (var id in this.active) {
213      var index = this.active[id];
214      this.chart[index].length = Date.now() - this.chart[index].time;
215    }
216  },
217
218  /**
219   * Generates <code>chart</code> from the new <code>model</code> value.
220   */
221  chartUpdate: function(oldLength, newLength) {
222    // If the new value is empty, then clear the model.
223    if (!newLength) {
224      this.active = {};
225      this.rows = [];
226      this.chart = [];
227      this.timeStart = null;
228      this.idleStart = null;
229      this.idleTotal = 0;
230      this.selected = [];
231      return;
232    }
233
234    // Only adding new entries to the model is supported (or clearing).
235    console.assert(newLength >= oldLength);
236
237    for (var i = oldLength; i < newLength; i++) {
238      var event = this.model[i];
239      switch (event.eventType) {
240        case 'created':
241          // If this is the first creation event in the chart, then store its
242          // time as beginning time of the chart.
243          if (!this.timeStart)
244            this.timeStart = event.time;
245
246          // If this event terminates idling, then add the idling time to total
247          // idling time. This is used to avoid gaps in the chart while idling.
248          if (Object.keys(this.active).length == 0 && this.idleStart)
249            this.idleTotal += event.time.getTime() - this.idleStart.getTime();
250
251          // Find the appropriate row for this chart element.
252          var rowIndex = 0;
253          while (true) {
254            // Add to this row only if there is enough space, and if the row
255            // is of the same type.
256            var addToRow = (rowIndex >= this.rows.length) ||
257                (this.rows[rowIndex].time.getTime() <= event.time.getTime() &&
258                 !this.rows[rowIndex].active &&
259                 (this.rows[rowIndex].requestType == event.requestType));
260
261            if (addToRow) {
262              this.chart.push({
263                index: this.chart.length,
264                id: event.id,
265                time: event.time,
266                executionTime: 0,
267                length: 0,
268                requestType: event.requestType,
269                left: event.time - this.timeStart - this.idleTotal,
270                row: rowIndex,
271                modelIndexes: [i]
272              });
273
274              this.rows[rowIndex] = {
275                requestType: event.requestType,
276                time: event.time,
277                active: true
278              };
279
280              this.active[event.id] = this.chart.length - 1;
281              break;
282            }
283
284            rowIndex++;
285          }
286          break;
287
288        case 'fulfilled':
289        case 'rejected':
290          if (!(event.id in this.active))
291            return;
292          var chartIndex = this.active[event.id];
293          this.chart[chartIndex].state = event.eventType;
294          this.chart[chartIndex].executionTime = event.executionTime;
295          this.chart[chartIndex].valueSize = event.valueSize;
296          this.chart[chartIndex].modelIndexes.push(i);
297          break;
298
299        case 'destroyed':
300          if (!(event.id in this.active))
301            return;
302
303          var chartIndex = this.active[event.id];
304          this.chart[chartIndex].length =
305              event.time - this.chart[chartIndex].time;
306          this.chart[chartIndex].modelIndexes.push(i);
307          this.rows[this.chart[chartIndex].row].time = event.time;
308          this.rows[this.chart[chartIndex].row].active = false;
309          delete this.active[event.id];
310
311          // If this was the last active request, then idling starts.
312          if (Object.keys(this.active).length == 0)
313            this.idleStart = event.time;
314          break;
315      }
316    }
317  },
318
319  /**
320   * Map of selected requests.
321   * @type {Object.<number, boolean>}
322   */
323  selected: {},
324
325  /**
326   * Map of requests which has started, but are not completed yet, from
327   * a request id to the chart element index.
328   * @type {Object.<number, number>}}
329   */
330  active: {},
331
332  /**
333   * List of chart elements, calculated from the model.
334   * @type {Array.<Object>}
335   */
336  chart: [],
337
338  /**
339   * List of rows in the chart, with the last endTime value on it.
340   * @type {Array.<Object>}
341   */
342  rows: [],
343
344  /**
345   * Scale of the chart.
346   * @type {number}
347   */
348  scale: 1,
349
350  /**
351   * Time of the first created request.
352   * @type {Date}
353   */
354  timeStart: null,
355
356  /**
357   * Time of the last idling started.
358   * @type {Date}
359   */
360  idleStart: null,
361
362  /**
363   * Total idling time since chart generation started. Used to avoid
364   * generating gaps in the chart when there is no activity. In milliseconds.
365   * @type {number}
366   */
367  idleTotal: 0,
368
369  /**
370   * List of requests information maps.
371   * @type {Array.<Object>}
372   */
373  model: []
374});
375
376/*
377 * Updates the mounted file system list.
378 * @param {Array.<Object>} fileSystems Array containing provided file system
379 *     information.
380 */
381function updateFileSystems(fileSystems) {
382  var fileSystemsNode = document.querySelector('#file-systems');
383  fileSystemsNode.model = fileSystems;
384}
385
386/**
387 * Called when a request is created.
388 * @param {Object} event Event.
389 */
390function onRequestEvent(event) {
391  event.time = new Date(event.time);  // Convert to a real Date object.
392  var requestTimelineNode = document.querySelector('#request-timeline');
393  requestTimelineNode.model.push(event);
394}
395
396document.addEventListener('DOMContentLoaded', function() {
397  var context = document.getCSSCanvasContext('2d', 'dashedPattern', 4, 4);
398  context.beginPath();
399  context.strokeStyle = '#ffffff';
400  context.moveTo(0, 0);
401  context.lineTo(4, 4);
402  context.stroke();
403
404  chrome.send('updateFileSystems');
405
406  // Refresh periodically.
407  setInterval(function() {
408    chrome.send('updateFileSystems');
409  }, 1000);
410});
411