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_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 > tr-ui-timeline-view {
34  -webkit-flex: 1 1 auto;
35  min-height: 0;
36}
37
38.report-id-message {
39  -webkit-user-select: text;
40}
41
42x-timeline-view-buttons {
43  display: flex;
44  align-items: center;
45}
46</style>
47
48<template id="profiling-view-template">
49  <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group>
50  <x-timeline-view-buttons>
51    <button id="record-button">Record</button>
52    <button id="save-button">Save</button>
53    <button id="load-button">Load</button>
54  </x-timeline-view-buttons>
55  <tr-ui-timeline-view>
56    <track-view-container id='track_view_container'></track-view-container>
57  </tr-ui-timeline-view>
58</template>
59
60<script>
61'use strict';
62
63/**
64 * @fileoverview ProfilingView glues the View control to
65 * TracingController.
66 */
67tr.exportTo('tr.ui.e.about_tracing', function() {
68  function readFile(file) {
69    return new Promise(function(resolve, reject) {
70      var reader = new FileReader();
71      var filename = file.name;
72      reader.onload = function(data) {
73        resolve(data.target.result);
74      };
75      reader.onerror = function(err) {
76        reject(err);
77      };
78
79      var is_binary = /[.]gz$/.test(filename) || /[.]zip$/.test(filename);
80      if (is_binary)
81        reader.readAsArrayBuffer(file);
82      else
83        reader.readAsText(file);
84    });
85  }
86
87  /**
88   * ProfilingView
89   * @constructor
90   * @extends {HTMLUnknownElement}
91   */
92  var ProfilingView = tr.ui.b.define('x-profiling-view');
93  var THIS_DOC = document.currentScript.ownerDocument;
94
95  ProfilingView.prototype = {
96    __proto__: HTMLUnknownElement.prototype,
97
98    decorate: function(tracingControllerClient) {
99      this.appendChild(tr.ui.b.instantiateTemplate('#profiling-view-template',
100          THIS_DOC));
101
102      this.timelineView_ = this.querySelector('tr-ui-timeline-view');
103      this.infoBarGroup_ = this.querySelector('tr-ui-b-info-bar-group');
104
105      // Detach the buttons. We will reattach them to the timeline view.
106      // TODO(nduca): Make timeline-view have a content select="x-buttons"
107      // that pulls in any buttons.
108      this.recordButton_ = this.querySelector('#record-button');
109      this.loadButton_ = this.querySelector('#load-button');
110      this.saveButton_ = this.querySelector('#save-button');
111
112      var buttons = this.querySelector('x-timeline-view-buttons');
113      buttons.parentElement.removeChild(buttons);
114      this.timelineView_.leftControls.appendChild(buttons);
115      this.initButtons_();
116
117      this.timelineView_.hotkeyController.addHotKey(new tr.ui.b.HotKey({
118        eventType: 'keypress',
119        keyCode: 'r'.charCodeAt(0),
120        callback: function(e) {
121          this.beginRecording();
122          event.stopPropagation();
123        },
124        thisArg: this
125      }));
126
127      this.initDragAndDrop_();
128
129      if (tracingControllerClient) {
130        this.tracingControllerClient_ = tracingControllerClient;
131      } else if (window.DevToolsHost !== undefined) {
132        this.tracingControllerClient_ =
133            new tr.ui.e.about_tracing.InspectorTracingControllerClient();
134      } else {
135        this.tracingControllerClient_ =
136            new tr.ui.e.about_tracing.XhrBasedTracingControllerClient();
137      }
138
139      this.isRecording_ = false;
140      this.activeTrace_ = undefined;
141
142      this.updateTracingControllerSpecificState_();
143    },
144
145    // Detach all document event listeners. Without this the tests can get
146    // confused as the element may still be listening when the next test runs.
147    detach_: function() {
148      this.detachDragAndDrop_();
149    },
150
151    get isRecording() {
152      return this.isRecording_;
153    },
154
155    set tracingControllerClient(tracingControllerClient) {
156      this.tracingControllerClient_ = tracingControllerClient;
157      this.updateTracingControllerSpecificState_();
158    },
159
160    updateTracingControllerSpecificState_: function() {
161      var isInspector = this.tracingControllerClient_ instanceof
162          tr.ui.e.about_tracing.InspectorTracingControllerClient;
163
164      if (isInspector) {
165        this.infoBarGroup_.addMessage(
166            'This about:tracing is connected to a remote device...',
167            [{buttonText: 'Wow!', onClick: function() {}}]);
168      }
169    },
170
171    beginRecording: function() {
172      if (this.isRecording_)
173        throw new Error('Already recording');
174      this.isRecording_ = true;
175      var resultPromise = tr.ui.e.about_tracing.beginRecording(
176          this.tracingControllerClient_);
177      resultPromise.then(
178          function(data) {
179            this.isRecording_ = false;
180            var traceName = tr.ui.e.about_tracing.defaultTraceName(
181                this.tracingControllerClient_);
182            this.setActiveTrace(traceName, data, false);
183          }.bind(this),
184          function(err) {
185            this.isRecording_ = false;
186            if (err instanceof tr.ui.e.about_tracing.UserCancelledError)
187              return;
188            tr.ui.b.Overlay.showError('Error while recording', err);
189          }.bind(this));
190      return resultPromise;
191    },
192
193    get timelineView() {
194      return this.timelineView_;
195    },
196
197    ///////////////////////////////////////////////////////////////////////////
198
199    clearActiveTrace: function() {
200      this.saveButton_.disabled = true;
201      this.activeTrace_ = undefined;
202    },
203
204    setActiveTrace: function(filename, data) {
205      this.activeTrace_ = {
206        filename: filename,
207        data: data
208      };
209
210      this.infoBarGroup_.clearMessages();
211      this.updateTracingControllerSpecificState_();
212      this.saveButton_.disabled = false;
213      this.timelineView_.viewTitle = filename;
214
215      var m = new tr.Model();
216      var i = new tr.importer.Import(m);
217      var p = i.importTracesWithProgressDialog([data]);
218      p.then(
219          function() {
220            this.timelineView_.model = m;
221            this.timelineView_.updateDocumentFavicon();
222          }.bind(this),
223          function(err) {
224            tr.ui.b.Overlay.showError('While importing: ', err);
225          }.bind(this));
226    },
227
228    ///////////////////////////////////////////////////////////////////////////
229
230    initButtons_: function() {
231      this.recordButton_.addEventListener(
232          'click', function(event) {
233            event.stopPropagation();
234            this.beginRecording();
235          }.bind(this));
236
237      this.loadButton_.addEventListener(
238          'click', function(event) {
239            event.stopPropagation();
240            this.onLoadClicked_();
241          }.bind(this));
242
243      this.saveButton_.addEventListener('click',
244                                        this.onSaveClicked_.bind(this));
245      this.saveButton_.disabled = true;
246    },
247
248    requestFilename_: function() {
249
250      // unsafe filename patterns:
251      var illegalRe = /[\/\?<>\\:\*\|":]/g;
252      var controlRe = /[\x00-\x1f\x80-\x9f]/g;
253      var reservedRe = /^\.+$/;
254
255      var filename;
256      var defaultName = this.activeTrace_.filename;
257      var fileExtension = '.json';
258      var fileRegex = /\.json$/;
259      if (/[.]gz$/.test(defaultName)) {
260        fileExtension += '.gz';
261        fileRegex = /\.json\.gz$/;
262      } else if (/[.]zip$/.test(defaultName)) {
263        fileExtension = '.zip';
264        fileRegex = /\.zip$/;
265      }
266
267      var custom = prompt('Filename? (' + fileExtension +
268                          ' appended) Or leave blank:');
269      if (custom === null)
270        return undefined;
271
272      var name;
273      if (custom) {
274        name = ' ' + custom;
275      } else {
276        var date = new Date();
277        var dateText = ' ' + date.toDateString() +
278                       ' ' + date.toLocaleTimeString();
279        name = dateText;
280      }
281
282      filename = defaultName.replace(fileRegex, name) + fileExtension;
283
284      return filename
285              .replace(illegalRe, '.')
286              .replace(controlRe, '\u2022')
287              .replace(reservedRe, '')
288              .replace(/\s+/g, '_');
289    },
290
291    onSaveClicked_: function() {
292      // Create a blob URL from the binary array.
293      var blob = new Blob([this.activeTrace_.data],
294                          {type: 'application/octet-binary'});
295      var blobUrl = window.webkitURL.createObjectURL(blob);
296
297      // Create a link and click on it. BEST API EVAR!
298      var link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
299      link.href = blobUrl;
300      var filename = this.requestFilename_();
301      if (filename) {
302        link.download = filename;
303        link.click();
304      }
305    },
306
307    onLoadClicked_: function() {
308      var inputElement = document.createElement('input');
309      inputElement.type = 'file';
310      inputElement.multiple = false;
311
312      var changeFired = false;
313      inputElement.addEventListener(
314          'change',
315          function(e) {
316            if (changeFired)
317              return;
318            changeFired = true;
319
320            var file = inputElement.files[0];
321            readFile(file).then(
322                function(data) {
323                  this.setActiveTrace(file.name, data);
324                }.bind(this),
325                function(err) {
326                  tr.ui.b.Overlay.showError('Error while loading file: ' + err);
327                });
328          }.bind(this), false);
329      inputElement.click();
330    },
331
332    ///////////////////////////////////////////////////////////////////////////
333
334    initDragAndDrop_: function() {
335      this.dropHandler_ = this.dropHandler_.bind(this);
336      this.ignoreDragEvent_ = this.ignoreDragEvent_.bind(this);
337      document.addEventListener('dragstart', this.ignoreDragEvent_, false);
338      document.addEventListener('dragend', this.ignoreDragEvent_, false);
339      document.addEventListener('dragenter', this.ignoreDragEvent_, false);
340      document.addEventListener('dragleave', this.ignoreDragEvent_, false);
341      document.addEventListener('dragover', this.ignoreDragEvent_, false);
342      document.addEventListener('drop', this.dropHandler_, false);
343    },
344
345    detachDragAndDrop_: function() {
346      document.removeEventListener('dragstart', this.ignoreDragEvent_);
347      document.removeEventListener('dragend', this.ignoreDragEvent_);
348      document.removeEventListener('dragenter', this.ignoreDragEvent_);
349      document.removeEventListener('dragleave', this.ignoreDragEvent_);
350      document.removeEventListener('dragover', this.ignoreDragEvent_);
351      document.removeEventListener('drop', this.dropHandler_);
352    },
353
354    ignoreDragEvent_: function(e) {
355      e.preventDefault();
356      return false;
357    },
358
359    dropHandler_: function(e) {
360      if (this.isAnyDialogUp_)
361        return;
362
363      e.stopPropagation();
364      e.preventDefault();
365
366      var files = e.dataTransfer.files;
367      if (files.length !== 1) {
368        tr.ui.b.Overlay.showError('1 file supported at a time.');
369        return;
370      }
371
372      readFile(files[0]).then(
373          function(data) {
374            this.setActiveTrace(files[0].name, data);
375          }.bind(this),
376          function(err) {
377            tr.ui.b.Overlay.showError('Error while loading file: ' + err);
378          });
379      return false;
380    }
381  };
382
383  return {
384    ProfilingView: ProfilingView
385  };
386});
387</script>
388