profiling_view.html revision edfe2194ee8a857cc1e78b4e4020f9b5e7210029
1<!DOCTYPE html>
2<!--
3Copyright (c) 2013 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<link rel="import" href="/tracing/base/base64.html">
9<link rel="import"
10      href="/tracing/ui/extras/about_tracing/record_and_capture_controller.html">
11<link rel="import"
12      href="/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html">
13<link rel="import"
14      href="/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html">
15<link rel="import" href="/tracing/importer/import.html">
16<link rel="import" href="/tracing/ui/base/info_bar_group.html">
17<link rel="import" href="/tracing/ui/base/hotkey_controller.html">
18<link rel="import" href="/tracing/ui/base/overlay.html">
19<link rel="import" href="/tracing/ui/base/utils.html">
20<link rel="import" href="/tracing/ui/timeline_view.html">
21
22<style>
23x-profiling-view {
24  -webkit-flex-direction: column;
25  display: -webkit-flex;
26  padding: 0;
27}
28
29x-profiling-view .controls #save-button {
30  margin-left: 64px !important;
31}
32
33x-profiling-view .controls #upload-button {
34  display: none;
35}
36
37x-profiling-view > tr-ui-timeline-view {
38  -webkit-flex: 1 1 auto;
39  min-height: 0;
40}
41
42.report-id-message {
43  -webkit-user-select: text;
44}
45
46x-timeline-view-buttons,
47x-timeline-view-buttons > #monitoring-elements {
48  display: flex;
49  align-items: center;
50}
51</style>
52
53<template id="profiling-view-template">
54  <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group>
55  <x-timeline-view-buttons>
56    <button id="record-button">Record</button>
57    <span id="monitoring-elements">
58      <input id="monitor-checkbox" type="checkbox">
59      <label for="monitor-checkbox">Monitoring</label></input>
60      <button id="capture-button">Capture Monitoring Snapshot</button>
61    </span>
62    <button id="save-button">Save</button>
63    <button id="upload-button">Upload</button>
64    <button id="load-button">Load</button>
65  </x-timeline-view-buttons>
66  <tr-ui-timeline-view>
67    <track-view-container id='track_view_container'></track-view-container>
68  </tr-ui-timeline-view>
69</template>
70
71<script>
72'use strict';
73
74/**
75 * @fileoverview ProfilingView glues the View control to
76 * TracingController.
77 */
78tr.exportTo('tr.ui.e.about_tracing', function() {
79  function readFile(file) {
80    return new Promise(function(resolve, reject) {
81      var reader = new FileReader();
82      var filename = file.name;
83      reader.onload = function(data) {
84        resolve(data.target.result);
85      };
86      reader.onerror = function(err) {
87        reject(err);
88      }
89
90      var is_binary = /[.]gz$/.test(filename) || /[.]zip$/.test(filename);
91      if (is_binary)
92        reader.readAsArrayBuffer(file);
93      else
94        reader.readAsText(file);
95    });
96  }
97
98  /**
99   * ProfilingView
100   * @constructor
101   * @extends {HTMLUnknownElement}
102   */
103  var ProfilingView = tr.ui.b.define('x-profiling-view');
104  var THIS_DOC = document.currentScript.ownerDocument;
105  var REPORT_UPLOAD_URL = 'http://crash-staging/';
106
107  ProfilingView.prototype = {
108    __proto__: HTMLUnknownElement.prototype,
109
110    decorate: function(tracingControllerClient) {
111      this.appendChild(tr.ui.b.instantiateTemplate('#profiling-view-template',
112          THIS_DOC));
113
114      this.timelineView_ = this.querySelector('tr-ui-timeline-view');
115      this.infoBarGroup_ = this.querySelector('tr-ui-b-info-bar-group');
116
117      // Detach the buttons. We will reattach them to the timeline view.
118      // TODO(nduca): Make timeline-view have a content select="x-buttons"
119      // that pulls in any buttons.
120      this.monitoringElements_ = this.querySelector('#monitoring-elements');
121      this.monitorCheckbox_ = this.querySelector('#monitor-checkbox');
122      this.captureButton_ = this.querySelector('#capture-button');
123      this.recordButton_ = this.querySelector('#record-button');
124      this.loadButton_ = this.querySelector('#load-button');
125      this.saveButton_ = this.querySelector('#save-button');
126      this.uploadButton_ = this.querySelector('#upload-button');
127
128      var buttons = this.querySelector('x-timeline-view-buttons');
129      buttons.parentElement.removeChild(buttons);
130      this.timelineView_.leftControls.appendChild(buttons);
131      this.initButtons_();
132
133      this.timelineView_.hotkeyController.addHotKey(new tr.ui.b.HotKey({
134        eventType: 'keypress',
135        keyCode: 'r'.charCodeAt(0),
136        callback: function(e) {
137          this.beginRecording();
138          event.stopPropagation();
139        },
140        thisArg: this
141      }));
142
143      this.initDragAndDrop_();
144
145      if (tracingControllerClient) {
146        this.tracingControllerClient_ = tracingControllerClient;
147      } else if (window.DevToolsHost !== undefined) {
148        this.tracingControllerClient_ =
149            new tr.ui.e.about_tracing.InspectorTracingControllerClient();
150      } else {
151        this.tracingControllerClient_ =
152            new tr.ui.e.about_tracing.XhrBasedTracingControllerClient();
153      }
154
155      this.isRecording_ = false;
156      this.isMonitoring_ = false;
157      this.activeTrace_ = undefined;
158
159      window.onMonitoringStateChanged = function(is_monitoring) {
160        this.onMonitoringStateChanged_(is_monitoring);
161      }.bind(this);
162
163      window.onUploadError = function(error_message) {
164        this.setUploadOverlayText_(['Trace upload failed: ' + error_message]);
165      }.bind(this);
166      window.onUploadProgress = function(percent, currentAsString,
167                                         totalAsString) {
168        this.setUploadOverlayText_(
169            ['Upload progress: ' + percent + '% (' + currentAsString + ' of ' +
170            currentAsString + ' bytes)']);
171      }.bind(this);
172      window.onUploadComplete = function(reportId) {
173        var messageDiv = document.createElement('div');
174        var textNode = document.createTextNode(
175            'Trace uploaded successfully. Report id: ');
176        messageDiv.appendChild(textNode);
177        var reportLink = document.createElement('a');
178        messageDiv.appendChild(reportLink);
179        reportLink.href = REPORT_UPLOAD_URL + reportId;
180        reportLink.text = reportId;
181        reportLink.className = 'report-id-message';
182        reportLink.target = '_blank';
183        this.setUploadOverlayContent_(messageDiv);
184      }.bind(this);
185
186      this.getMonitoringStatus();
187      this.updateTracingControllerSpecificState_();
188    },
189
190    // Detach all document event listeners. Without this the tests can get
191    // confused as the element may still be listening when the next test runs.
192    detach_: function() {
193      this.detachDragAndDrop_();
194    },
195
196    get isRecording() {
197      return this.isRecording_;
198    },
199
200    get isMonitoring() {
201      return this.isMonitoring_;
202    },
203
204    set tracingControllerClient(tracingControllerClient) {
205      this.tracingControllerClient_ = tracingControllerClient;
206      this.updateTracingControllerSpecificState_();
207    },
208
209    updateTracingControllerSpecificState_: function() {
210      var isInspector = this.tracingControllerClient_ instanceof
211          tr.ui.e.about_tracing.InspectorTracingControllerClient;
212
213      if (isInspector) {
214        this.infoBarGroup_.addMessage(
215            'This about:tracing is connected to a remote device...',
216            [{buttonText: 'Wow!', onClick: function() {}}]);
217      }
218
219      this.monitoringElements_.style.display = isInspector ? 'none' : '';
220    },
221
222    beginRecording: function() {
223      if (this.isRecording_)
224        throw new Error('Already recording');
225      if (this.isMonitoring_)
226        throw new Error('Already monitoring');
227      this.isRecording_ = true;
228      this.monitorCheckbox_.disabled = true;
229      this.monitorCheckbox_.checked = false;
230      var resultPromise = tr.ui.e.about_tracing.beginRecording(
231          this.tracingControllerClient_);
232      resultPromise.then(
233          function(data) {
234            this.isRecording_ = false;
235            this.monitorCheckbox_.disabled = false;
236            var traceName = tr.ui.e.about_tracing.defaultTraceName(
237                this.tracingControllerClient_);
238            this.setActiveTrace(traceName, data, false);
239          }.bind(this),
240          function(err) {
241            this.isRecording_ = false;
242            this.monitorCheckbox_.disabled = false;
243            if (err instanceof tr.ui.e.about_tracing.UserCancelledError)
244              return;
245            tr.ui.b.Overlay.showError('Error while recording', err);
246          }.bind(this));
247      return resultPromise;
248    },
249
250    beginMonitoring: function() {
251      if (this.isRecording_)
252        throw new Error('Already recording');
253      if (this.isMonitoring_)
254        throw new Error('Already monitoring');
255      var resultPromise =
256          tr.ui.e.about_tracing.beginMonitoring(this.tracingControllerClient_);
257      resultPromise.then(
258          function() {
259          }.bind(this),
260          function(err) {
261            if (err instanceof tr.ui.e.about_tracing.UserCancelledError)
262              return;
263            tr.ui.b.Overlay.showError('Error while monitoring', err);
264          }.bind(this));
265      return resultPromise;
266    },
267
268    endMonitoring: function() {
269      if (this.isRecording_)
270        throw new Error('Already recording');
271      if (!this.isMonitoring_)
272        throw new Error('Monitoring is disabled');
273      var resultPromise =
274          tr.ui.e.about_tracing.endMonitoring(this.tracingControllerClient_);
275      resultPromise.then(
276          function() {
277          }.bind(this),
278          function(err) {
279            if (err instanceof tr.ui.e.about_tracing.UserCancelledError)
280              return;
281            tr.ui.b.Overlay.showError('Error while monitoring', err);
282          }.bind(this));
283      return resultPromise;
284    },
285
286    captureMonitoring: function() {
287      if (!this.isMonitoring_)
288        throw new Error('Monitoring is disabled');
289      var resultPromise =
290          tr.ui.e.about_tracing.captureMonitoring(
291              this.tracingControllerClient_);
292      resultPromise.then(
293          function(data) {
294            var traceName = tr.ui.e.about_tracing.defaultTraceName(
295                this.tracingControllerClient_);
296            this.setActiveTrace(traceName, data, true);
297          }.bind(this),
298          function(err) {
299            if (err instanceof tr.ui.e.about_tracing.UserCancelledError)
300              return;
301            tr.ui.b.Overlay.showError('Error while monitoring', err);
302          }.bind(this));
303      return resultPromise;
304    },
305
306    getMonitoringStatus: function() {
307      var resultPromise =
308          tr.ui.e.about_tracing.getMonitoringStatus(
309              this.tracingControllerClient_);
310      resultPromise.then(
311          function(status) {
312            this.onMonitoringStateChanged_(status.isMonitoring);
313          }.bind(this),
314          function(err) {
315            if (err instanceof tr.ui.e.about_tracing.UserCancelledError)
316              return;
317            tr.ui.b.Overlay.showError('Error while updating tracing states',
318                                      err);
319          }.bind(this));
320      return resultPromise;
321    },
322
323    onMonitoringStateChanged_: function(is_monitoring) {
324      this.isMonitoring_ = is_monitoring;
325      this.recordButton_.disabled = is_monitoring;
326      this.captureButton_.disabled = !is_monitoring;
327      this.monitorCheckbox_.checked = is_monitoring;
328    },
329
330    get timelineView() {
331      return this.timelineView_;
332    },
333
334    ///////////////////////////////////////////////////////////////////////////
335
336    clearActiveTrace: function() {
337      this.saveButton_.disabled = true;
338      this.uploadButton_.disabled = true;
339      this.activeTrace_ = undefined;
340    },
341
342    setActiveTrace: function(filename, data) {
343      this.activeTrace_ = {
344        filename: filename,
345        data: data
346      };
347
348      this.infoBarGroup_.clearMessages();
349      this.updateTracingControllerSpecificState_();
350      this.saveButton_.disabled = false;
351      this.uploadButton_.disabled = false;
352      this.timelineView_.viewTitle = filename;
353
354      var m = new tr.Model();
355      var i = new tr.importer.Import(m);
356      var p = i.importTracesWithProgressDialog([data]);
357      p.then(
358          function() {
359            this.timelineView_.model = m;
360            this.timelineView_.updateDocumentFavicon();
361          }.bind(this),
362          function(err) {
363            tr.ui.b.Overlay.showError('While importing: ', err);
364          }.bind(this));
365    },
366
367    ///////////////////////////////////////////////////////////////////////////
368
369    initButtons_: function() {
370      this.recordButton_.addEventListener(
371          'click', function(event) {
372            event.stopPropagation();
373            this.beginRecording();
374          }.bind(this));
375
376      this.monitorCheckbox_.addEventListener(
377          'click', function(event) {
378            event.stopPropagation();
379            if (this.isMonitoring_)
380              this.endMonitoring();
381            else
382              this.beginMonitoring();
383          }.bind(this));
384
385      this.captureButton_.addEventListener(
386          'click', function(event) {
387            event.stopPropagation();
388            this.captureMonitoring();
389          }.bind(this));
390      this.captureButton_.disabled = true;
391
392      this.loadButton_.addEventListener(
393          'click', function(event) {
394            event.stopPropagation();
395            this.onLoadClicked_();
396          }.bind(this));
397
398      this.saveButton_.addEventListener('click',
399                                        this.onSaveClicked_.bind(this));
400      this.saveButton_.disabled = true;
401
402      this.uploadButton_.addEventListener('click',
403                                          this.onUploadClicked_.bind(this));
404      if (typeof(chrome.send) === 'function') {
405        this.uploadButton_.style.display = 'inline-block';
406      }
407      this.uploadButton_.disabled = true;
408      this.uploadOverlay_ = null;
409    },
410
411    requestFilename_: function() {
412
413      // unsafe filename patterns:
414      var illegalRe = /[\/\?<>\\:\*\|":]/g;
415      var controlRe = /[\x00-\x1f\x80-\x9f]/g;
416      var reservedRe = /^\.+$/;
417
418      var filename;
419      var defaultName = this.activeTrace_.filename;
420      var fileExtension = '.json';
421      var fileRegex = /\.json$/;
422      if (/[.]gz$/.test(defaultName)) {
423        fileExtension += '.gz';
424        fileRegex = /\.json\.gz$/;
425      } else if (/[.]zip$/.test(defaultName)) {
426        fileExtension = '.zip';
427        fileRegex = /\.zip$/;
428      }
429
430      var custom = prompt('Filename? (' + fileExtension +
431                          ' appended) Or leave blank:');
432      if (custom === null)
433        return undefined;
434
435      var name;
436      if (custom) {
437        name = ' ' + custom;
438      } else {
439        var date = new Date();
440        var dateText = ' ' + date.toDateString() +
441                       ' ' + date.toLocaleTimeString();
442        name = dateText;
443      }
444
445      filename = defaultName.replace(fileRegex, name) + fileExtension;
446
447      return filename
448              .replace(illegalRe, '.')
449              .replace(controlRe, '\u2022')
450              .replace(reservedRe, '')
451              .replace(/\s+/g, '_');
452    },
453
454    onSaveClicked_: function() {
455      // Create a blob URL from the binary array.
456      var blob = new Blob([this.activeTrace_.data],
457                          {type: 'application/octet-binary'});
458      var blobUrl = window.webkitURL.createObjectURL(blob);
459
460      // Create a link and click on it. BEST API EVAR!
461      var link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
462      link.href = blobUrl;
463      var filename = this.requestFilename_();
464      if (filename) {
465        link.download = filename;
466        link.click();
467      }
468    },
469
470    onUploadClicked_: function() {
471      if (this.uploadOverlay_) {
472        throw new Error('Already uploading');
473      }
474      this.initUploadStatusOverlay_();
475    },
476
477    initUploadStatusOverlay_: function() {
478      this.uploadOverlay_ = tr.ui.b.Overlay();
479      this.uploadOverlay_.title = 'Uploading trace...';
480      this.uploadOverlay_.userCanClose = false;
481      this.uploadOverlay_.visible = true;
482
483      this.setUploadOverlayText_([
484        'You are about to upload trace data to Google server.',
485        'Would you like to proceed?'
486      ]);
487      var okButton = document.createElement('button');
488      okButton.textContent = 'Ok';
489      okButton.addEventListener('click', this.doTraceUpload_.bind(this));
490      this.uploadOverlay_.buttons.appendChild(okButton);
491
492      var cancelButton = document.createElement('button');
493      cancelButton.textContent = 'Cancel';
494      cancelButton.addEventListener('click',
495                                    this.hideUploadOverlay_.bind(this));
496      this.uploadOverlay_.buttons.appendChild(cancelButton);
497    },
498
499    setUploadOverlayContent_: function(content) {
500      if (!this.uploadOverlay_)
501        throw new Error('Not uploading');
502
503      this.uploadOverlay_.textContent = '';
504      this.uploadOverlay_.appendChild(content);
505    },
506
507    setUploadOverlayText_: function(messages) {
508      var contentDiv = document.createElement('div');
509
510      for (var i = 0; i < messages.length; ++i) {
511        var messageDiv = document.createElement('div');
512        messageDiv.textContent = messages[i];
513        contentDiv.appendChild(messageDiv);
514      }
515      this.setUploadOverlayContent_(contentDiv);
516    },
517
518    doTraceUpload_: function() {
519      this.setUploadOverlayText_(['Uploading trace data...']);
520      this.uploadOverlay_.buttons.removeChild(
521          this.uploadOverlay_.buttons.firstChild);
522      this.uploadOverlay_.buttons.firstChild.textContent = 'Close';
523
524      var filename = this.activeTrace_.filename;
525      var isBinary = /[.]gz$/.test(filename) || /[.]zip$/.test(filename);
526      if (isBinary) {
527        var data_base64 = tr.b.Base64.EncodeArrayBufferToString(
528            this.activeTrace_.data);
529        chrome.send('doUploadBase64', [data_base64]);
530      } else {
531        chrome.send('doUpload', [this.activeTrace_.data]);
532      }
533    },
534
535    hideUploadOverlay_: function() {
536      if (!this.uploadOverlay_)
537        throw new Error('Not uploading');
538
539      this.uploadOverlay_.visible = false;
540      this.uploadOverlay_ = null;
541    },
542
543    onLoadClicked_: function() {
544      var inputElement = document.createElement('input');
545      inputElement.type = 'file';
546      inputElement.multiple = false;
547
548      var changeFired = false;
549      inputElement.addEventListener(
550          'change',
551          function(e) {
552            if (changeFired)
553              return;
554            changeFired = true;
555
556            var file = inputElement.files[0];
557            readFile(file).then(
558                function(data) {
559                  this.setActiveTrace(file.name, data);
560                }.bind(this),
561                function(err) {
562                  tr.ui.b.Overlay.showError('Error while loading file: ' + err);
563                });
564          }.bind(this), false);
565      inputElement.click();
566    },
567
568    ///////////////////////////////////////////////////////////////////////////
569
570    initDragAndDrop_: function() {
571      this.dropHandler_ = this.dropHandler_.bind(this);
572      this.ignoreDragEvent_ = this.ignoreDragEvent_.bind(this);
573      document.addEventListener('dragstart', this.ignoreDragEvent_, false);
574      document.addEventListener('dragend', this.ignoreDragEvent_, false);
575      document.addEventListener('dragenter', this.ignoreDragEvent_, false);
576      document.addEventListener('dragleave', this.ignoreDragEvent_, false);
577      document.addEventListener('dragover', this.ignoreDragEvent_, false);
578      document.addEventListener('drop', this.dropHandler_, false);
579    },
580
581    detachDragAndDrop_: function() {
582      document.removeEventListener('dragstart', this.ignoreDragEvent_);
583      document.removeEventListener('dragend', this.ignoreDragEvent_);
584      document.removeEventListener('dragenter', this.ignoreDragEvent_);
585      document.removeEventListener('dragleave', this.ignoreDragEvent_);
586      document.removeEventListener('dragover', this.ignoreDragEvent_);
587      document.removeEventListener('drop', this.dropHandler_);
588    },
589
590    ignoreDragEvent_: function(e) {
591      e.preventDefault();
592      return false;
593    },
594
595    dropHandler_: function(e) {
596      if (this.isAnyDialogUp_)
597        return;
598
599      e.stopPropagation();
600      e.preventDefault();
601
602      var files = e.dataTransfer.files;
603      if (files.length !== 1) {
604        tr.ui.b.Overlay.showError('1 file supported at a time.');
605        return;
606      }
607
608      readFile(files[0]).then(
609          function(data) {
610            this.setActiveTrace(files[0].name, data);
611          }.bind(this),
612          function(err) {
613            tr.ui.b.Overlay.showError('Error while loading file: ' + err);
614          });
615      return false;
616    }
617  };
618
619  return {
620    ProfilingView: ProfilingView
621  };
622});
623</script>
624