| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @implements {Common.OutputStream} |
| * @unrestricted |
| */ |
| Timeline.TimelineLoader = class { |
| /** |
| * @param {!Timeline.TimelineLoader.Client} client |
| */ |
| constructor(client) { |
| this._client = client; |
| |
| this._backingStorage = new Bindings.TempFileBackingStorage(); |
| this._tracingModel = new SDK.TracingModel(this._backingStorage); |
| |
| /** @type {?function()} */ |
| this._canceledCallback = null; |
| this._state = Timeline.TimelineLoader.State.Initial; |
| this._buffer = ''; |
| this._firstRawChunk = true; |
| this._firstChunk = true; |
| this._loadedBytes = 0; |
| /** @type {number} */ |
| this._totalSize; |
| this._jsonTokenizer = new TextUtils.BalancedJSONTokenizer(this._writeBalancedJSON.bind(this), true); |
| } |
| |
| /** |
| * @param {!File} file |
| * @param {!Timeline.TimelineLoader.Client} client |
| * @return {!Timeline.TimelineLoader} |
| */ |
| static loadFromFile(file, client) { |
| const loader = new Timeline.TimelineLoader(client); |
| const fileReader = new Bindings.ChunkedFileReader(file, Timeline.TimelineLoader.TransferChunkLengthBytes); |
| loader._canceledCallback = fileReader.cancel.bind(fileReader); |
| loader._totalSize = file.size; |
| fileReader.read(loader).then(success => { |
| if (!success) { |
| loader._reportErrorAndCancelLoading(fileReader.error().message); |
| } |
| }); |
| return loader; |
| } |
| |
| /** |
| * @param {!Array.<!SDK.TracingManager.EventPayload>} events |
| * @param {!Timeline.TimelineLoader.Client} client |
| * @return {!Timeline.TimelineLoader} |
| */ |
| static loadFromEvents(events, client) { |
| const loader = new Timeline.TimelineLoader(client); |
| |
| setTimeout(async () => { |
| const eventsPerChunk = 5000; |
| client.loadingStarted(); |
| for (let i = 0; i < events.length; i += eventsPerChunk) { |
| const chunk = events.slice(i, i + eventsPerChunk); |
| loader._tracingModel.addEvents(chunk); |
| client.loadingProgress((i + chunk.length) / events.length); |
| await new Promise(r => setTimeout(r)); // Yield event loop to paint. |
| } |
| loader.close(); |
| }); |
| |
| return loader; |
| } |
| |
| /** |
| * @param {string} url |
| * @param {!Timeline.TimelineLoader.Client} client |
| * @return {!Timeline.TimelineLoader} |
| */ |
| static loadFromURL(url, client) { |
| const loader = new Timeline.TimelineLoader(client); |
| Host.ResourceLoader.loadAsStream(url, null, loader); |
| return loader; |
| } |
| |
| cancel() { |
| this._tracingModel = null; |
| this._backingStorage.reset(); |
| this._client.loadingComplete(null); |
| this._client = null; |
| if (this._canceledCallback) { |
| this._canceledCallback(); |
| } |
| } |
| |
| /** |
| * @override |
| * @param {string} chunk |
| * @return {!Promise} |
| */ |
| write(chunk) { |
| if (!this._client) { |
| return Promise.resolve(); |
| } |
| this._loadedBytes += chunk.length; |
| if (this._firstRawChunk) { |
| this._client.loadingStarted(); |
| } else { |
| this._client.loadingProgress(this._totalSize ? this._loadedBytes / this._totalSize : undefined); |
| } |
| this._firstRawChunk = false; |
| |
| if (this._state === Timeline.TimelineLoader.State.Initial) { |
| if (chunk.startsWith('{"nodes":[')) { |
| this._state = Timeline.TimelineLoader.State.LoadingCPUProfileFormat; |
| } else if (chunk[0] === '{') { |
| this._state = Timeline.TimelineLoader.State.LookingForEvents; |
| } else if (chunk[0] === '[') { |
| this._state = Timeline.TimelineLoader.State.ReadingEvents; |
| } else { |
| this._reportErrorAndCancelLoading(Common.UIString('Malformed timeline data: Unknown JSON format')); |
| return Promise.resolve(); |
| } |
| } |
| |
| if (this._state === Timeline.TimelineLoader.State.LoadingCPUProfileFormat) { |
| this._buffer += chunk; |
| return Promise.resolve(); |
| } |
| |
| if (this._state === Timeline.TimelineLoader.State.LookingForEvents) { |
| const objectName = '"traceEvents":'; |
| const startPos = this._buffer.length - objectName.length; |
| this._buffer += chunk; |
| const pos = this._buffer.indexOf(objectName, startPos); |
| if (pos === -1) { |
| return Promise.resolve(); |
| } |
| chunk = this._buffer.slice(pos + objectName.length); |
| this._state = Timeline.TimelineLoader.State.ReadingEvents; |
| } |
| |
| if (this._state !== Timeline.TimelineLoader.State.ReadingEvents) { |
| return Promise.resolve(); |
| } |
| if (this._jsonTokenizer.write(chunk)) { |
| return Promise.resolve(); |
| } |
| this._state = Timeline.TimelineLoader.State.SkippingTail; |
| if (this._firstChunk) { |
| this._reportErrorAndCancelLoading(Common.UIString('Malformed timeline input, wrong JSON brackets balance')); |
| } |
| return Promise.resolve(); |
| } |
| |
| /** |
| * @param {string} data |
| */ |
| _writeBalancedJSON(data) { |
| let json = data + ']'; |
| |
| if (!this._firstChunk) { |
| const commaIndex = json.indexOf(','); |
| if (commaIndex !== -1) { |
| json = json.slice(commaIndex + 1); |
| } |
| json = '[' + json; |
| } |
| |
| let items; |
| try { |
| items = /** @type {!Array.<!SDK.TracingManager.EventPayload>} */ (JSON.parse(json)); |
| } catch (e) { |
| this._reportErrorAndCancelLoading(Common.UIString('Malformed timeline data: %s', e.toString())); |
| return; |
| } |
| |
| if (this._firstChunk) { |
| this._firstChunk = false; |
| if (this._looksLikeAppVersion(items[0])) { |
| this._reportErrorAndCancelLoading(Common.UIString('Legacy Timeline format is not supported.')); |
| return; |
| } |
| } |
| |
| try { |
| this._tracingModel.addEvents(items); |
| } catch (e) { |
| this._reportErrorAndCancelLoading(Common.UIString('Malformed timeline data: %s', e.toString())); |
| } |
| } |
| |
| /** |
| * @param {string=} message |
| */ |
| _reportErrorAndCancelLoading(message) { |
| if (message) { |
| Common.console.error(message); |
| } |
| this.cancel(); |
| } |
| |
| /** |
| * @param {*} item |
| * @return {boolean} |
| */ |
| _looksLikeAppVersion(item) { |
| return typeof item === 'string' && item.indexOf('Chrome') !== -1; |
| } |
| |
| /** |
| * @override |
| */ |
| async close() { |
| if (!this._client) { |
| return; |
| } |
| this._client.processingStarted(); |
| setTimeout(() => this._finalizeTrace(), 0); |
| } |
| |
| _finalizeTrace() { |
| if (this._state === Timeline.TimelineLoader.State.LoadingCPUProfileFormat) { |
| this._parseCPUProfileFormat(this._buffer); |
| this._buffer = ''; |
| } |
| this._tracingModel.tracingComplete(); |
| this._client.loadingComplete(this._tracingModel); |
| } |
| |
| /** |
| * @param {string} text |
| */ |
| _parseCPUProfileFormat(text) { |
| let traceEvents; |
| try { |
| const profile = JSON.parse(text); |
| traceEvents = TimelineModel.TimelineJSProfileProcessor.buildTraceProfileFromCpuProfile( |
| profile, /* tid */ 1, /* injectPageEvent */ true); |
| } catch (e) { |
| this._reportErrorAndCancelLoading(Common.UIString('Malformed CPU profile format')); |
| return; |
| } |
| this._tracingModel.addEvents(traceEvents); |
| } |
| }; |
| |
| |
| Timeline.TimelineLoader.TransferChunkLengthBytes = 5000000; |
| |
| /** |
| * @interface |
| */ |
| Timeline.TimelineLoader.Client = function() {}; |
| |
| Timeline.TimelineLoader.Client.prototype = { |
| loadingStarted() {}, |
| |
| /** |
| * @param {number=} progress |
| */ |
| loadingProgress(progress) {}, |
| |
| processingStarted() {}, |
| |
| /** |
| * @param {?SDK.TracingModel} tracingModel |
| */ |
| loadingComplete(tracingModel) {}, |
| }; |
| |
| /** |
| * @enum {symbol} |
| */ |
| Timeline.TimelineLoader.State = { |
| Initial: Symbol('Initial'), |
| LookingForEvents: Symbol('LookingForEvents'), |
| ReadingEvents: Symbol('ReadingEvents'), |
| SkippingTail: Symbol('SkippingTail'), |
| LoadingCPUProfileFormat: Symbol('LoadingCPUProfileFormat') |
| }; |