diff options
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing')
15 files changed, 2921 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html new file mode 100644 index 00000000000..8e579d90921 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/about_tracing.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> +<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order> + +<link rel="stylesheet" href="/tracing/ui/extras/about_tracing/common.css"> +<link rel="import" href="/tracing/ui/extras/about_tracing/profiling_view.html"> +<link rel="import" href="/tracing/ui/extras/full_config.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + window.profilingView = undefined; // Made global for debugging purposes only. + + document.addEventListener('DOMContentLoaded', function() { + window.profilingView = new tr.ui.e.about_tracing.ProfilingView(); + profilingView.timelineView.globalMode = true; + Polymer.dom(document.body).appendChild(profilingView); + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css new file mode 100644 index 00000000000..3db21b67ffa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/common.css @@ -0,0 +1,25 @@ +/* Copyright (c) 2012 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. + */ +html, +body { + height: 100%; +} + +body { + flex-direction: column; + display: flex; + margin: 0; + padding: 0; +} + +body > x-profiling-view { + flex: 1 1 auto; + min-height: 0; +} + +body > x-profiling-view > x-timeline-view:focus { + outline: 0 +} + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html new file mode 100644 index 00000000000..fa348a7b661 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/devtools_stream.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<!-- +Copyright 2017 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. +--> +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/base64.html"> +<script> + +'use strict'; + +/** + * A devtools protocol stream object. + * + * This reads a stream of data over the remote debugging connection. + */ +tr.exportTo('tr.ui.e.about_tracing', function() { + class DevtoolsStream { + constructor(connection, streamHandle) { + this.connection_ = connection; + this.streamHandle_ = streamHandle; + this.closed_ = false; + } + + async read() { + if (this.closed_) { + throw new Error('stream is closed'); + } + + const pendingRequests = []; + + const READ_REQUEST_BYTES = 32768; + const makeRequest = () => { + pendingRequests.push(this.connection_.req( + 'IO.read', + { + handle: this.streamHandle_, + size: READ_REQUEST_BYTES, + })); + }; + + const MAX_CONCURRENT_REQUESTS = 2; + for (let i = 0; i < MAX_CONCURRENT_REQUESTS; ++i) { + makeRequest(); + } + + const chunks = []; + let base64 = false; + while (true) { + const request = pendingRequests.shift(); + const response = await request; + + chunks.push(response.data); + if (response.base64Encoded) { + base64 = true; + } + if (response.eof) { + break; + } + + makeRequest(); + } + + if (base64) { + let totalSize = 0; + for (const chunk of chunks) { + totalSize += tr.b.Base64.getDecodedBufferLength(chunk); + } + const buffer = new ArrayBuffer(totalSize); + let offset = 0; + for (const chunk of chunks) { + offset += tr.b.Base64.DecodeToTypedArray( + chunk, + new DataView(buffer, offset)); + } + return buffer; + } + + return chunks.join(''); + } + + close() { + this.closed_ = true; + return this.connection_.req('IO.close', { handle: this.streamHandle_ }); + } + + async readAndClose() { + const data = await this.read(); + this.close(); + return data; + } + } + + return { + DevtoolsStream, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html new file mode 100644 index 00000000000..791f5e77705 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_connection.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> +<link rel="import" href="/tracing/base/base.html"> +<script> + +'use strict'; + +/** + * Contains connection code that inspector's embedding framework calls on + * tracing, and that tracing can use to talk to inspector. + */ +tr.exportTo('tr.ui.e.about_tracing', function() { + class InspectorConnection { + constructor(windowGlobal) { + if (!windowGlobal.DevToolsHost) { + throw new Error('Requires window.DevToolsHost'); + } + this.devToolsHost_ = windowGlobal.DevToolsHost; + this.installDevToolsAPI_(windowGlobal); + + this.nextRequestId_ = 1; + this.pendingRequestResolversId_ = {}; + + this.notificationListenersByMethodName_ = {}; + } + + req(method, params) { + const id = this.nextRequestId_++; + const msg = JSON.stringify({ + id, + method, + params + }); + const devtoolsMessageStr = JSON.stringify( + {id, 'method': 'dispatchProtocolMessage', 'params': [msg]}); + this.devToolsHost_.sendMessageToEmbedder(devtoolsMessageStr); + + return new Promise(function(resolve, reject) { + this.pendingRequestResolversId_[id] = { + resolve, + reject + }; + }.bind(this)); + } + + setNotificationListener(method, listener) { + this.notificationListenersByMethodName_[method] = listener; + } + + dispatchMessage_(payload) { + const isStringPayload = typeof payload === 'string'; + // Special handling for Tracing.dataCollected because it is high + // bandwidth. + const isDataCollectedMessage = isStringPayload ? + payload.includes('"method": "Tracing.dataCollected"') : + payload.method === 'Tracing.dataCollected'; + if (isDataCollectedMessage) { + const listener = this.notificationListenersByMethodName_[ + 'Tracing.dataCollected']; + if (listener) { + // FIXME(loislo): trace viewer should be able to process + // raw message object because string based version a few times + // slower on the browser side. + // see https://codereview.chromium.org/784513002. + listener(isStringPayload ? payload : JSON.stringify(payload)); + return; + } + } + + const message = isStringPayload ? JSON.parse(payload) : payload; + if (message.id) { + const resolver = this.pendingRequestResolversId_[message.id]; + if (resolver === undefined) { + return; + } + if (message.error) { + resolver.reject(message.error); + return; + } + resolver.resolve(message.result); + return; + } + + if (message.method) { + const listener = this.notificationListenersByMethodName_[ + message.method]; + if (listener === undefined) return; + listener(message.params); + return; + } + } + + installDevToolsAPI_(windowGlobal) { + // Interface used by inspector when it hands data to us from the backend. + windowGlobal.DevToolsAPI = { + setToolbarColors() { }, + addExtensions() { }, + setInspectedPageId() { }, + dispatchMessage: this.dispatchMessage_.bind(this), + }; + + // Temporary until inspector backend switches to DevToolsAPI. + windowGlobal.InspectorFrontendAPI = windowGlobal.DevToolsAPI; + } + } + + return { + InspectorConnection, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html new file mode 100644 index 00000000000..ac5afabae12 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html @@ -0,0 +1,216 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> + +<link rel="import" href="/tracing/ui/extras/about_tracing/devtools_stream.html"> +<link rel="import" href="/tracing/ui/extras/about_tracing/inspector_connection.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/tracing_controller_client.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + function createResolvedPromise(data) { + const promise = new Promise(function(resolve, reject) { + if (data) { + resolve(data); + } else { + resolve(); + } + }); + return promise; + } + + function appendTraceChunksTo(chunks, messageString) { + if (typeof messageString !== 'string') { + throw new Error('Invalid data'); + } + const re = /"params":\s*\{\s*"value":\s*\[([^]+)\]\s*\}\s*\}/; + const m = re.exec(messageString); + if (!m) { + throw new Error('Malformed response'); + } + + if (chunks.length > 1) { + chunks.push(','); + } + chunks.push(m[1]); + } + + /** + * Controls tracing using the inspector's FrontendAgentHost APIs. + */ + class InspectorTracingControllerClient extends + tr.ui.e.about_tracing.TracingControllerClient { + constructor(connection) { + super(); + this.recording_ = false; + this.bufferUsage_ = 0; + this.conn_ = connection; + this.currentTraceTextChunks_ = undefined; + } + + beginMonitoring(monitoringOptions) { + throw new Error('Not implemented'); + } + + endMonitoring() { + throw new Error('Not implemented'); + } + + captureMonitoring() { + throw new Error('Not implemented'); + } + + getMonitoringStatus() { + return createResolvedPromise({ + isMonitoring: false, + categoryFilter: '', + useSystemTracing: false, + useContinuousTracing: false, + useSampling: false + }); + } + + getCategories() { + const res = this.conn_.req('Tracing.getCategories', {}); + return res.then(function(result) { + return result.categories; + }, function(err) { + return []; + }); + } + + beginRecording(recordingOptions) { + if (this.recording_) { + throw new Error('Already recording'); + } + this.recording_ = 'starting'; + + // The devtools and tracing endpoints have slightly different parameter + // configurations. Noteably, recordMode has different spelling + // requirements. + function RewriteRecordMode(recordMode) { + if (recordMode === 'record-until-full') { + return 'recordUntilFull'; + } + if (recordMode === 'record-continuously') { + return 'recordContinuously'; + } + if (recordMode === 'record-as-much-as-possible') { + return 'recordAsMuchAsPossible'; + } + return 'unsupported record mode'; + } + + const traceConfigStr = { + includedCategories: recordingOptions.included_categories, + excludedCategories: recordingOptions.excluded_categories, + recordMode: RewriteRecordMode(recordingOptions.record_mode), + enableSystrace: recordingOptions.enable_systrace + }; + if ('memory_dump_config' in recordingOptions) { + traceConfigStr.memoryDumpConfig = recordingOptions.memory_dump_config; + } + let res = this.conn_.req( + 'Tracing.start', + { + traceConfig: traceConfigStr, + transferMode: 'ReturnAsStream', + streamCompression: 'gzip', + bufferUsageReportingInterval: 1000 + }); + res = res.then( + function ok() { + this.conn_.setNotificationListener( + 'Tracing.bufferUsage', + this.onBufferUsageUpdateFromInspector_.bind(this)); + this.recording_ = true; + }.bind(this), + function error() { + this.recording_ = false; + }.bind(this)); + return res; + } + + onBufferUsageUpdateFromInspector_(params) { + this.bufferUsage_ = params.value || params.percentFull; + } + + beginGetBufferPercentFull() { + return tr.b.timeout(100).then(() => this.bufferUsage_); + } + + onDataCollected_(messageString) { + appendTraceChunksTo(this.currentTraceTextChunks_, messageString); + } + + async endRecording() { + if (this.recording_ === false) { + return createResolvedPromise(); + } + + if (this.recording_ !== true) { + throw new Error('Cannot end'); + } + + this.currentTraceTextChunks_ = ['[']; + const clearListeners = () => { + this.conn_.setNotificationListener( + 'Tracing.bufferUsage', undefined); + this.conn_.setNotificationListener( + 'Tracing.tracingComplete', undefined); + this.conn_.setNotificationListener( + 'Tracing.dataCollected', undefined); + }; + + try { + this.conn_.setNotificationListener( + 'Tracing.dataCollected', this.onDataCollected_.bind(this)); + + const tracingComplete = new Promise((resolve, reject) => { + this.conn_.setNotificationListener( + 'Tracing.tracingComplete', resolve); + }); + + this.recording_ = 'stopping'; + await this.conn_.req('Tracing.end', {}); + const params = await tracingComplete; + + this.traceName_ = 'trace.json'; + if ('stream' in params) { + const stream = new tr.ui.e.about_tracing.DevtoolsStream( + this.conn_, params.stream); + const streamCompression = params.streamCompression || 'none'; + if (streamCompression === 'gzip') { + this.traceName_ = 'trace.json.gz'; + } + + return await stream.readAndClose(); + } + + this.currentTraceTextChunks_.push(']'); + const traceText = this.currentTraceTextChunks_.join(''); + this.currentTraceTextChunks_ = undefined; + return traceText; + } finally { + clearListeners(); + this.recording_ = false; + } + } + + defaultTraceName() { + return this.traceName_; + } + } + + return { + InspectorTracingControllerClient, + appendTraceChunksTo, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html new file mode 100644 index 00000000000..4a6585ac9e8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/inspector_tracing_controller_client_test.html @@ -0,0 +1,396 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> + +<link rel="import" + href="/tracing/ui/extras/about_tracing/inspector_connection.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html"> + +<script> +'use strict'; + +function makeController() { + const controller = + new tr.ui.e.about_tracing.InspectorTracingControllerClient(); + controller.conn_ = new (function() { + this.req = function(method, params) { + const msg = JSON.stringify({ + id: 1, + method, + params + }); + return new (function() { + this.msg = msg; + this.then = function(m1, m2) { + return this; + }; + })(); + }; + this.setNotificationListener = function(method, listener) { + }; + })(); + return controller; +} + +tr.b.unittest.testSuite(function() { + test('beginRecording_sendCategoriesAndOptions', function() { + const controller = makeController(); + + const recordingOptions = { + included_categories: ['a', 'b', 'c'], + excluded_categories: ['e'], + enable_systrace: false, + record_mode: 'record-until-full', + }; + + const result = JSON.parse(controller.beginRecording(recordingOptions).msg); + assert.deepEqual( + result.params.traceConfig.includedCategories, ['a', 'b', 'c']); + assert.deepEqual( + result.params.traceConfig.excludedCategories, ['e']); + assert.strictEqual( + result.params.traceConfig.recordMode, 'recordUntilFull'); + assert.isFalse( + result.params.traceConfig.enableSystrace); + assert.isFalse('memoryDumpConfig' in result.params.traceConfig); + }); + + test('beginRecording_sendCategoriesAndOptionsWithMemoryInfra', function() { + const controller = makeController(); + + const memoryConfig = { triggers: [] }; + memoryConfig.triggers.push( + {'mode': 'detailed', 'periodic_interval_ms': 10000}); + const recordingOptions = { + included_categories: ['c', 'disabled-by-default-memory-infra', 'a'], + excluded_categories: ['e'], + enable_systrace: false, + record_mode: 'test-mode', + memory_dump_config: memoryConfig, + }; + + const result = JSON.parse(controller.beginRecording(recordingOptions).msg); + assert.isTrue( + result.params.traceConfig.memoryDumpConfig.triggers.length === 1); + assert.strictEqual(result.params.traceConfig.memoryDumpConfig. + triggers[0].mode, 'detailed'); + assert.strictEqual(result.params.traceConfig.memoryDumpConfig. + triggers[0].periodic_interval_ms, 10000); + }); + + test('oldFormat', function() { + const chunks = []; + tr.ui.e.about_tracing.appendTraceChunksTo(chunks, '"{ "method": "Tracing.dataCollected", "params": { "value": [ {"cat":"__metadata","pid":28871,"tid":0,"ts":0,"ph":"M","name":"num_cpus","args":{"number":4}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_sort_index","args":{"sort_index":-5}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_name","args":{"name":"Renderer"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_labels","args":{"labels":"JS Bin"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_sort_index","args":{"sort_index":-1}},{"cat":"__metadata","pid":28871,"tid":28917,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Compositor"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Chrome_ChildIOThread"}},{"cat":"__metadata","pid":28871,"tid":28919,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CompositorRasterWorker1/28919"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CrRendererMain"}},{"cat":"ipc,toplevel","pid":28871,"tid":28911,"ts":22000084746,"ph":"X","name":"ChannelReader::DispatchInputData","args":{"class":64,"line":25},"tdur":0,"tts":1853064},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"overhead","args":{"average_overhead":0.015}} ] } }"'); // @suppress longLineCheck + assert.strictEqual(chunks.length, 1); + JSON.parse('[' + chunks.join('') + ']'); + }); + + test('newFormat', function() { + const chunks = []; + tr.ui.e.about_tracing.appendTraceChunksTo(chunks, '"{ "method": "Tracing.dataCollected", "params": { "value": [{"cat":"__metadata","pid":28871,"tid":0,"ts":0,"ph":"M","name":"num_cpus","args":{"number":4}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_sort_index","args":{"sort_index":-5}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_name","args":{"name":"Renderer"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_labels","args":{"labels":"JS Bin"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_sort_index","args":{"sort_index":-1}},{"cat":"__metadata","pid":28871,"tid":28917,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Compositor"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Chrome_ChildIOThread"}},{"cat":"__metadata","pid":28871,"tid":28919,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CompositorRasterWorker1/28919"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CrRendererMain"}},{"cat":"ipc,toplevel","pid":28871,"tid":28911,"ts":22000084746,"ph":"X","name":"ChannelReader::DispatchInputData","args":{"class":64,"line":25},"tdur":0,"tts":1853064},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"overhead","args":{"average_overhead":0.015}}] } }"'); // @suppress longLineCheck + assert.strictEqual(chunks.length, 1); + JSON.parse('[' + chunks.join('') + ']'); + }); + + test('stringAndObjectPayload', function() { + const connection = + new tr.ui.e.about_tracing.InspectorConnection({DevToolsHost: {}}); + connection.setNotificationListener('Tracing.dataCollected', + function(message) { + assert.typeOf(message, 'string'); + JSON.parse(message); + } + ); + connection.dispatchMessage_('{ "method": "Tracing.dataCollected", "params": { "value": [] } }'); // @suppress longLineCheck + connection.dispatchMessage_({'method': 'Tracing.dataCollected', 'params': {'value': [] } }); // @suppress longLineCheck + }); + + // Makes a fake version of DevToolsHost, which is the object injected + // by the chrome inspector to allow tracing a remote instance of chrome. + // + // The fake host doesn't do much by itself - you have to install + // callbacks for incoming messages via handleMessage(). + function makeFakeDevToolsHost() { + return new (function() { + this.pendingMethods_ = []; + this.messageHandlers_ = []; + + // Sends a message to DevTools host. This is used by + // InspectorTracingControllerClient to communicate with the remote + // debugging tracing backend. + this.sendMessageToEmbedder = function(devtoolsMessageStr) { + this.pendingMethods_.push(JSON.parse(devtoolsMessageStr)); + this.tryMessageHandlers_(); + }; + + // Runs remote debugging message handlers. Handlers are installed + // by test code via handleMessage(). + this.tryMessageHandlers_ = function() { + while (this.pendingMethods_.length !== 0) { + const message = this.pendingMethods_[0]; + const params = JSON.parse(message.params); + let handled = false; + const handlersToRemove = []; + + // Try to find a handler for this method. + for (const handler of this.messageHandlers_) { + if (handler(params, () => handlersToRemove.push(handler))) { + handled = true; + break; + } + } + + // Remove any handlers that requested removal. + this.messageHandlers_ = this.messageHandlers_.filter( + (handler) => !handlersToRemove.includes(handler)); + + // Remove any handled messages. + if (handled) { + this.pendingMethods_.shift(); + } else { + return; // Methods must be handled in order. + } + } + }; + + // Installs a message handler that will be invoked for each + // incoming message from InspectorTracingControllerClient. + // + // handleMessage((message, removeSelf) => { + // // Try to handle |message|. + // // Call |removeSelf| to remove this handler for future messages. + // // Return whether |message| was handled. Otherwise other handlers + // // will be run until one of them succeeds. + // } + this.handleMessage = function(handler) { + this.messageHandlers_.push(handler); + this.tryMessageHandlers_(); + }; + + // Installs a message handler that will handle the first call to the named + // method. Returns a promise for the parameters passed to the method. + this.handleMethod = function(method) { + const result = new Promise((resolve, reject) => { + this.handleMessage( + (requestParams, removeHandler) => { + if (requestParams.method === method) { + removeHandler(); + resolve(requestParams); + return true; + } + return false; + }); + }); + return result; + }; + + // Sends a response to a remote debugging method call (i.e., + // "return") to InspectorTracingControllerClient. + this.respondToMethod = function(id, params) { + this.devToolsAPI_.dispatchMessage(JSON.stringify({ + id, + result: params, + })); + }; + + // Sets the object used to send messages back to + // InspectorTracingControllerClient. + this.setDevToolsAPI = function(api) { + this.devToolsAPI_ = api; + }; + + // Sends a notification to InspectorTracingControllerClient. + this.sendNotification = function(method, params) { + this.devToolsAPI_.dispatchMessage(JSON.stringify({ method, params })); + }; + })(); + } + + test('shouldUseLegacyTraceFormatIfNoStreamId', async function() { + const fakeDevToolsHost = makeFakeDevToolsHost(); + const fakeWindow = { + DevToolsHost: fakeDevToolsHost, + }; + const controller = + new tr.ui.e.about_tracing.InspectorTracingControllerClient( + new tr.ui.e.about_tracing.InspectorConnection(fakeWindow)); + fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI); + + const runHost = (async() => { + const startParams = await fakeDevToolsHost.handleMethod('Tracing.start'); + fakeDevToolsHost.respondToMethod(startParams.id, {}); + const endParams = await fakeDevToolsHost.handleMethod('Tracing.end'); + fakeDevToolsHost.respondToMethod(endParams.id, {}); + fakeDevToolsHost.sendNotification('Tracing.tracingComplete', {}); + })(); + + await controller.beginRecording({}); + const traceData = await controller.endRecording(); + await runHost; + + assert.strictEqual('[]', traceData); + }); + + test('shouldReassembleTextDataChunks', async function() { + const fakeDevToolsHost = makeFakeDevToolsHost(); + const fakeWindow = { + DevToolsHost: fakeDevToolsHost, + }; + const controller = + new tr.ui.e.about_tracing.InspectorTracingControllerClient( + new tr.ui.e.about_tracing.InspectorConnection(fakeWindow)); + fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI); + + const STREAM_HANDLE = 7; + + const streamChunks = [ + '[', + ']', + '\n', + ]; + + let streamClosed = false; + + const handleIoRead = (index, params) => { + if (params.params.handle !== STREAM_HANDLE) { + throw new Error('Invalid stream handle'); + } + if (streamClosed) { + throw new Error('stream is closed'); + } + let data = ''; + if (index < streamChunks.length) { + data = streamChunks[index]; + } + const eof = (index >= streamChunks.length - 1); + fakeDevToolsHost.respondToMethod(params.id, { + eof, + base64Encoded: false, + data, + }); + const nextIndex = eof ? streamChunks.length : index + 1; + return (async() => + handleIoRead(nextIndex, await fakeDevToolsHost.handleMethod('IO.read')) + )(); + }; + + const runHost = (async() => { + const startParams = await fakeDevToolsHost.handleMethod('Tracing.start'); + fakeDevToolsHost.respondToMethod(startParams.id, {}); + const endParams = await fakeDevToolsHost.handleMethod('Tracing.end'); + fakeDevToolsHost.respondToMethod(endParams.id, {}); + fakeDevToolsHost.sendNotification('Tracing.tracingComplete', { + 'stream': STREAM_HANDLE, + }); + + const closePromise = (async() => { + const closeParams = await fakeDevToolsHost.handleMethod('IO.close'); + assert.strictEqual(closeParams.params.handle, STREAM_HANDLE); + streamClosed = true; + })(); + + const readPromise = (async() => + handleIoRead(0, await fakeDevToolsHost.handleMethod('IO.read')) + )(); + + await Promise.race([closePromise, readPromise]); + await closePromise; + })(); + + await controller.beginRecording({}); + const traceData = await controller.endRecording(); + await runHost; + + assert.strictEqual(traceData, '[]\n'); + }); + + test('shouldReassembleBase64TraceDataChunks', async function() { + const fakeDevToolsHost = makeFakeDevToolsHost(); + const fakeWindow = { + DevToolsHost: fakeDevToolsHost, + }; + const controller = + new tr.ui.e.about_tracing.InspectorTracingControllerClient( + new tr.ui.e.about_tracing.InspectorConnection(fakeWindow)); + fakeDevToolsHost.setDevToolsAPI(fakeWindow.DevToolsAPI); + + const STREAM_HANDLE = 7; + + // This is the empty trace ('[]') gzip compressed and chunked to make + // sure reassembling base64 strings works properly. + const streamChunks = [ + 'Hw==', + 'iwg=', + 'ALg4', + 'L1oAA4uOBQApu0wNAgAAAA==', + ]; + + let streamClosed = false; + + const handleIoRead = (index, params) => { + if (params.params.handle !== STREAM_HANDLE) { + throw new Error('Invalid stream handle'); + } + if (streamClosed) { + throw new Error('stream is closed'); + } + let data = ''; + if (index < streamChunks.length) { + data = streamChunks[index]; + } + const eof = (index >= streamChunks.length - 1); + fakeDevToolsHost.respondToMethod(params.id, { + eof, + base64Encoded: true, + data, + }); + const nextIndex = eof ? streamChunks.length : index + 1; + return (async() => { + handleIoRead(nextIndex, await fakeDevToolsHost.handleMethod('IO.read')); + })(); + }; + + const runHost = (async() => { + const startParams = await fakeDevToolsHost.handleMethod('Tracing.start'); + fakeDevToolsHost.respondToMethod(startParams.id, {}); + const endParams = await fakeDevToolsHost.handleMethod('Tracing.end'); + fakeDevToolsHost.respondToMethod(endParams.id, {}); + fakeDevToolsHost.sendNotification('Tracing.tracingComplete', { + 'stream': STREAM_HANDLE, + 'streamCompression': 'gzip' + }); + const closePromise = (async() => { + const closeParams = await fakeDevToolsHost.handleMethod('IO.close'); + assert.strictEqual(closeParams.params.handle, STREAM_HANDLE); + streamClosed = true; + })(); + + const readPromise = (async() => { + handleIoRead(0, await fakeDevToolsHost.handleMethod('IO.read')); + })(); + + await Promise.race([closePromise, readPromise]); + await closePromise; + })(); + + await controller.beginRecording({}); + const traceData = await controller.endRecording(); + await runHost; + + const dataArray = new Uint8Array(traceData); + const expectedArray = new Uint8Array([ + 0x1f, 0x8b, 0x8, 0x0, 0xb8, 0x38, 0x2f, 0x5a, 0x0, 0x3, 0x8b, 0x8e, + 0x5, 0x0, 0x29, 0xbb, 0x4c, 0xd, 0x2, 0x0, 0x0, 0x0]); + + assert.strictEqual(dataArray.length, expectedArray.length); + + for (let i = 0; i < dataArray.length; ++i) { + assert.strictEqual(dataArray[i], expectedArray[i]); + } + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html new file mode 100644 index 00000000000..cfefdc05cc7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> + +<link rel="import" + href="/tracing/ui/extras/about_tracing/tracing_controller_client.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + function MockTracingControllerClient() { + this.requests = []; + this.nextRequestIndex = 0; + this.allowLooping = false; + } + + MockTracingControllerClient.prototype = { + __proto__: tr.ui.e.about_tracing.TracingControllerClient.prototype, + + expectRequest(method, generateResponse) { + let generateResponseCb; + if (typeof generateResponse === 'function') { + generateResponseCb = generateResponse; + } else { + generateResponseCb = function() { + return generateResponse; + }; + } + + this.requests.push({ + method, + generateResponseCb}); + }, + + _request(method, args) { + return new Promise(function(resolve) { + const requestIndex = this.nextRequestIndex; + if (requestIndex >= this.requests.length) { + throw new Error('Unhandled request'); + } + if (!this.allowLooping) { + this.nextRequestIndex++; + } else { + this.nextRequestIndex = (this.nextRequestIndex + 1) % + this.requests.length; + } + + const req = this.requests[requestIndex]; + assert.strictEqual(method, req.method); + const resp = req.generateResponseCb(args); + resolve(resp); + }.bind(this)); + }, + + assertAllRequestsHandled() { + if (this.allowLooping) { + throw new Error('Incompatible with allowLooping'); + } + assert.strictEqual(this.requests.length, this.nextRequestIndex); + }, + + getCategories() { + return this._request('getCategories'); + }, + + beginRecording(recordingOptions) { + return this._request('beginRecording', recordingOptions); + }, + + beginGetBufferPercentFull() { + return this._request('beginGetBufferPercentFull'); + }, + + endRecording() { + return this._request('endRecording'); + } + }; + + return { + MockTracingControllerClient, + }; +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html new file mode 100644 index 00000000000..77c0e80af12 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view.html @@ -0,0 +1,372 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> + +<link rel="import" href="/tracing/base/base64.html"> +<link rel="import" href="/tracing/importer/import.html"> +<link rel="import" href="/tracing/ui/base/file.html"> +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> +<link rel="import" href="/tracing/ui/base/info_bar_group.html"> +<link rel="import" href="/tracing/ui/base/overlay.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/inspector_tracing_controller_client.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/record_controller.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html"> +<link rel="import" href="/tracing/ui/timeline_view.html"> + +<style> +x-profiling-view { + flex-direction: column; + display: flex; + padding: 0; +} + +x-profiling-view .controls #save-button { + margin-left: 64px !important; +} + +x-profiling-view > tr-ui-timeline-view { + flex: 1 1 auto; + min-height: 0; +} + +.report-id-message { + -webkit-user-select: text; +} + +x-timeline-view-buttons { + display: flex; + align-items: center; +} +</style> + +<template id="profiling-view-template"> + <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group> + <x-timeline-view-buttons> + <button id="record-button">Record</button> + <button id="save-button">Save</button> + <button id="load-button">Load</button> + </x-timeline-view-buttons> + <tr-ui-timeline-view> + <track-view-container id='track_view_container'></track-view-container> + </tr-ui-timeline-view> +</template> + +<script> +'use strict'; + +/** + * @fileoverview ProfilingView glues the View control to + * TracingController. + */ +tr.exportTo('tr.ui.e.about_tracing', function() { + /** + * ProfilingView + * @constructor + * @extends {HTMLDivElement} + */ + const ProfilingView = tr.ui.b.define('x-profiling-view'); + const THIS_DOC = document.currentScript.ownerDocument; + + ProfilingView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate(tracingControllerClient) { + Polymer.dom(this).appendChild( + tr.ui.b.instantiateTemplate('#profiling-view-template', THIS_DOC)); + + this.timelineView_ = + Polymer.dom(this).querySelector('tr-ui-timeline-view'); + this.infoBarGroup_ = + Polymer.dom(this).querySelector('tr-ui-b-info-bar-group'); + + // Detach the buttons. We will reattach them to the timeline view. + // TODO(nduca): Make timeline-view have a content select="x-buttons" + // that pulls in any buttons. + this.recordButton_ = Polymer.dom(this).querySelector('#record-button'); + this.loadButton_ = Polymer.dom(this).querySelector('#load-button'); + this.saveButton_ = Polymer.dom(this).querySelector('#save-button'); + + const buttons = Polymer.dom(this).querySelector( + 'x-timeline-view-buttons'); + Polymer.dom(buttons.parentElement).removeChild(buttons); + Polymer.dom(this.timelineView_.leftControls).appendChild(buttons); + this.initButtons_(); + + this.timelineView_.hotkeyController.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + keyCode: 'r'.charCodeAt(0), + callback(e) { + this.beginRecording(); + event.stopPropagation(); + }, + thisArg: this + })); + + this.initDragAndDrop_(); + + if (tracingControllerClient) { + this.tracingControllerClient_ = tracingControllerClient; + } else if (window.DevToolsHost !== undefined) { + this.tracingControllerClient_ = + new tr.ui.e.about_tracing.InspectorTracingControllerClient( + new tr.ui.e.about_tracing.InspectorConnection(window)); + } else { + this.tracingControllerClient_ = + new tr.ui.e.about_tracing.XhrBasedTracingControllerClient(); + } + + this.isRecording_ = false; + this.activeTrace_ = undefined; + + this.updateTracingControllerSpecificState_(); + }, + + // Detach all document event listeners. Without this the tests can get + // confused as the element may still be listening when the next test runs. + detach_() { + this.detachDragAndDrop_(); + }, + + get isRecording() { + return this.isRecording_; + }, + + set tracingControllerClient(tracingControllerClient) { + this.tracingControllerClient_ = tracingControllerClient; + this.updateTracingControllerSpecificState_(); + }, + + updateTracingControllerSpecificState_() { + const isInspector = this.tracingControllerClient_ instanceof + tr.ui.e.about_tracing.InspectorTracingControllerClient; + + if (isInspector) { + this.infoBarGroup_.addMessage( + 'This about:tracing is connected to a remote device...', + [{buttonText: 'Wow!', onClick() {}}]); + } + }, + + beginRecording() { + if (this.isRecording_) { + throw new Error('Already recording'); + } + this.isRecording_ = true; + const resultPromise = tr.ui.e.about_tracing.beginRecording( + this.tracingControllerClient_); + resultPromise.then( + function(data) { + this.isRecording_ = false; + const traceName = tr.ui.e.about_tracing.defaultTraceName( + this.tracingControllerClient_); + this.setActiveTrace(traceName, data, false); + }.bind(this), + function(err) { + this.isRecording_ = false; + if (err instanceof tr.ui.e.about_tracing.UserCancelledError) { + return; + } + tr.ui.b.Overlay.showError('Error while recording', err); + }.bind(this)); + return resultPromise; + }, + + get timelineView() { + return this.timelineView_; + }, + + /////////////////////////////////////////////////////////////////////////// + + clearActiveTrace() { + this.saveButton_.disabled = true; + this.activeTrace_ = undefined; + }, + + setActiveTrace(filename, data) { + this.activeTrace_ = { + filename, + data + }; + + this.infoBarGroup_.clearMessages(); + this.updateTracingControllerSpecificState_(); + this.saveButton_.disabled = false; + this.timelineView_.viewTitle = filename; + + const m = new tr.Model(); + const i = new tr.importer.Import(m); + const p = i.importTracesWithProgressDialog([data]); + p.then( + function() { + this.timelineView_.model = m; + this.timelineView_.updateDocumentFavicon(); + }.bind(this), + function(err) { + tr.ui.b.Overlay.showError('While importing: ', err); + }.bind(this)); + }, + + /////////////////////////////////////////////////////////////////////////// + + initButtons_() { + this.recordButton_.addEventListener( + 'click', function(event) { + event.stopPropagation(); + this.beginRecording(); + }.bind(this)); + + this.loadButton_.addEventListener( + 'click', function(event) { + event.stopPropagation(); + this.onLoadClicked_(); + }.bind(this)); + + this.saveButton_.addEventListener('click', + this.onSaveClicked_.bind(this)); + this.saveButton_.disabled = true; + }, + + requestFilename_() { + // unsafe filename patterns: + const illegalRe = /[\/\?<>\\:\*\|":]/g; + const controlRe = /[\x00-\x1f\x80-\x9f]/g; + const reservedRe = /^\.+$/; + + const defaultName = this.activeTrace_.filename; + let fileExtension = '.json'; + let fileRegex = /\.json$/; + if (/[.]gz$/.test(defaultName)) { + fileExtension += '.gz'; + fileRegex = /\.json\.gz$/; + } else if (/[.]zip$/.test(defaultName)) { + fileExtension = '.zip'; + fileRegex = /\.zip$/; + } + + const custom = prompt('Filename? (' + fileExtension + + ' appended) Or leave blank:'); + if (custom === null) { + return undefined; + } + + let name; + if (custom) { + name = ' ' + custom; + } else { + const date = new Date(); + const dateText = ' ' + date.toDateString() + + ' ' + date.toLocaleTimeString(); + name = dateText; + } + + const filename = defaultName.replace(fileRegex, name) + fileExtension; + + return filename + .replace(illegalRe, '.') + .replace(controlRe, '\u2022') + .replace(reservedRe, '') + .replace(/\s+/g, '_'); + }, + + onSaveClicked_() { + // Create a blob URL from the binary array. + const blob = new Blob([this.activeTrace_.data], + {type: 'application/octet-binary'}); + const blobUrl = window.webkitURL.createObjectURL(blob); + + // Create a link and click on it. BEST API EVAR! + const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); + link.href = blobUrl; + const filename = this.requestFilename_(); + if (filename) { + link.download = filename; + link.click(); + } + }, + + onLoadClicked_() { + const inputElement = document.createElement('input'); + inputElement.type = 'file'; + inputElement.multiple = false; + + let changeFired = false; + inputElement.addEventListener( + 'change', + function(e) { + if (changeFired) return; + changeFired = true; + + const file = inputElement.files[0]; + tr.ui.b.readFile(file).then( + function(data) { + this.setActiveTrace(file.name, data); + }.bind(this), + function(err) { + tr.ui.b.Overlay.showError('Error while loading file: ' + err); + }); + }.bind(this), false); + inputElement.click(); + }, + + /////////////////////////////////////////////////////////////////////////// + + initDragAndDrop_() { + this.dropHandler_ = this.dropHandler_.bind(this); + this.ignoreDragEvent_ = this.ignoreDragEvent_.bind(this); + document.addEventListener('dragstart', this.ignoreDragEvent_, false); + document.addEventListener('dragend', this.ignoreDragEvent_, false); + document.addEventListener('dragenter', this.ignoreDragEvent_, false); + document.addEventListener('dragleave', this.ignoreDragEvent_, false); + document.addEventListener('dragover', this.ignoreDragEvent_, false); + document.addEventListener('drop', this.dropHandler_, false); + }, + + detachDragAndDrop_() { + document.removeEventListener('dragstart', this.ignoreDragEvent_); + document.removeEventListener('dragend', this.ignoreDragEvent_); + document.removeEventListener('dragenter', this.ignoreDragEvent_); + document.removeEventListener('dragleave', this.ignoreDragEvent_); + document.removeEventListener('dragover', this.ignoreDragEvent_); + document.removeEventListener('drop', this.dropHandler_); + }, + + ignoreDragEvent_(e) { + e.preventDefault(); + return false; + }, + + dropHandler_(e) { + if (this.isAnyDialogUp_) return; + + e.stopPropagation(); + e.preventDefault(); + + const files = e.dataTransfer.files; + if (files.length !== 1) { + tr.ui.b.Overlay.showError('1 file supported at a time.'); + return; + } + + tr.ui.b.readFile(files[0]).then( + function(data) { + this.setActiveTrace(files[0].name, data); + }.bind(this), + function(err) { + tr.ui.b.Overlay.showError('Error while loading file: ' + err); + }); + return false; + } + }; + + return { + ProfilingView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html new file mode 100644 index 00000000000..f52c491207f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/profiling_view_test.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> + +<link rel="import" href="/tracing/base/base.html"> +<link rel="import" href="/tracing/base/base64.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html"> +<link rel="import" href="/tracing/ui/extras/about_tracing/profiling_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const Base64 = tr.b.Base64; + const testData = [ + {name: 'a', args: {}, pid: 52, ts: 15000, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 19000, cat: 'foo', tid: 53, ph: 'E'}, + {name: 'b', args: {}, pid: 52, ts: 32000, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'b', args: {}, pid: 52, ts: 54000, cat: 'foo', tid: 53, ph: 'E'} + ]; + + const monitoringOptions = { + isMonitoring: false, + categoryFilter: '*', + useSystemTracing: false, + useContinuousTracing: false, + useSampling: false + }; + + const ProfilingView = tr.ui.e.about_tracing.ProfilingView; + + test('recording', function() { + const mock = new tr.ui.e.about_tracing.MockTracingControllerClient(); + mock.allowLooping = true; + mock.expectRequest('endRecording', function() { + return ''; + }); + mock.expectRequest('getCategories', function() { + return ['a', 'b', 'c']; + }); + mock.expectRequest('beginRecording', function(data) { + return ''; + }); + mock.expectRequest('endRecording', function(data) { + return JSON.stringify(testData); + }); + + const view = new ProfilingView(mock); + view.style.height = '400px'; + view.style.border = '1px solid black'; + this.addHTMLOutput(view); + + const recordingPromise = view.beginRecording(); + + let didAbort = false; + + tr.b.timeout(60).then(() => { + if (didAbort) return; + recordingPromise.selectionDlg.clickRecordButton(); + }).then(() => tr.b.timeout(60)).then(() => { + recordingPromise.progressDlg.clickStopButton(); + }); + + return recordingPromise.then(null, err => { + didAbort = true; + assert.fail(err); + }); + }); +}); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html new file mode 100644 index 00000000000..a9b42b589d8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller.html @@ -0,0 +1,187 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> + +<link rel="import" href="/tracing/ui/extras/about_tracing/record_selection_dialog.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + function beginRecording(tracingControllerClient) { + let finalPromiseResolver; + const finalPromise = new Promise(function(resolve, reject) { + finalPromiseResolver = { + resolve, + reject + }; + }); + finalPromise.selectionDlg = undefined; + finalPromise.progressDlg = undefined; + + function beginRecordingError(err) { + finalPromiseResolver.reject(err); + } + + // Step 0: End recording. This is necessary when the user reloads the + // about:tracing page when we are recording. Window.onbeforeunload is not + // reliable to end recording on reload. + endRecording(tracingControllerClient).then( + getCategories, + getCategories); // Ignore error. + + // But just in case, bind onbeforeunload anyway. + window.onbeforeunload = function(e) { + endRecording(tracingControllerClient); + }; + + // Step 1: Get categories. + function getCategories() { + const p = tracingControllerClient.getCategories().then( + showTracingDialog, + beginRecordingError); + p.catch(function(err) { + beginRecordingError(err); + }); + } + + // Step 2: Show tracing dialog. + let selectionDlg; + function showTracingDialog(categories) { + selectionDlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + selectionDlg.categories = categories; + selectionDlg.settings_key = + 'tr.ui.e.about_tracing.record_selection_dialog'; + selectionDlg.addEventListener('recordclick', startTracing); + selectionDlg.addEventListener('closeclick', cancelRecording); + selectionDlg.visible = true; + + finalPromise.selectionDlg = selectionDlg; + } + + function cancelRecording() { + finalPromise.selectionDlg = undefined; + finalPromiseResolver.reject(new UserCancelledError()); + } + + // Step 2: Do the actual tracing dialog. + let progressDlg; + let bufferPercentFullDiv; + function startTracing() { + progressDlg = new tr.ui.b.Overlay(); + Polymer.dom(progressDlg).textContent = 'Recording...'; + progressDlg.userCanClose = false; + + bufferPercentFullDiv = document.createElement('div'); + Polymer.dom(progressDlg).appendChild(bufferPercentFullDiv); + + const stopButton = document.createElement('button'); + Polymer.dom(stopButton).textContent = 'Stop'; + progressDlg.clickStopButton = function() { + stopButton.click(); + }; + Polymer.dom(progressDlg).appendChild(stopButton); + + const categories = selectionDlg.includedAndExcludedCategories(); + const recordingOptions = { + included_categories: categories.included, + excluded_categories: categories.excluded, + enable_systrace: selectionDlg.useSystemTracing, + record_mode: selectionDlg.tracingRecordMode, + }; + if (categories.included.indexOf( + 'disabled-by-default-memory-infra') !== -1) { + const memoryConfig = { triggers: [] }; + memoryConfig.triggers.push( + {'mode': 'detailed', 'periodic_interval_ms': 10000}); + recordingOptions.memory_dump_config = memoryConfig; + } + + const requestPromise = tracingControllerClient.beginRecording( + recordingOptions); + requestPromise.then( + function() { + progressDlg.visible = true; + stopButton.focus(); + updateBufferPercentFull('0'); + }, + recordFailed); + + stopButton.addEventListener('click', function() { + // TODO(chrishenry): Currently, this only dismiss the progress + // dialog when tracingComplete event is received. When performing + // remote debugging, the tracingComplete event may be delayed + // considerable. We should indicate to user that we are waiting + // for tracingComplete event instead of being unresponsive. (For + // now, I disable the "stop" button, since clicking on the button + // again now cause exception.) + const recordingPromise = endRecording(tracingControllerClient); + recordingPromise.then( + recordFinished, + recordFailed); + stopButton.disabled = true; + bufferPercentFullDiv = undefined; + }); + finalPromise.progressDlg = progressDlg; + } + + function recordFinished(tracedData) { + progressDlg.visible = false; + finalPromise.progressDlg = undefined; + finalPromiseResolver.resolve(tracedData); + } + + function recordFailed(err) { + progressDlg.visible = false; + finalPromise.progressDlg = undefined; + finalPromiseResolver.reject(err); + } + + function getBufferPercentFull() { + if (!bufferPercentFullDiv) return; + + tracingControllerClient.beginGetBufferPercentFull().then( + updateBufferPercentFull); + } + + function updateBufferPercentFull(percentFull) { + if (!bufferPercentFullDiv) return; + + percentFull = Math.round(100 * parseFloat(percentFull)); + const newText = 'Buffer usage: ' + percentFull + '%'; + if (Polymer.dom(bufferPercentFullDiv).textContent !== newText) { + Polymer.dom(bufferPercentFullDiv).textContent = newText; + } + + window.setTimeout(getBufferPercentFull, 500); + } + + // Thats it! We're done. + return finalPromise; + } + + function endRecording(tracingControllerClient) { + return tracingControllerClient.endRecording(); + } + + function defaultTraceName(tracingControllerClient) { + return tracingControllerClient.defaultTraceName(); + } + + function UserCancelledError() { + Error.apply(this, arguments); + } + UserCancelledError.prototype = { + __proto__: Error.prototype + }; + + return { + beginRecording, + UserCancelledError, + defaultTraceName, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html new file mode 100644 index 00000000000..e3e0438f3a2 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_controller_test.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> + +<link rel="import" + href="/tracing/ui/extras/about_tracing/mock_tracing_controller_client.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/record_controller.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const testData = [ + {name: 'a', args: {}, pid: 52, ts: 15000, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'a', args: {}, pid: 52, ts: 19000, cat: 'foo', tid: 53, ph: 'E'}, + {name: 'b', args: {}, pid: 52, ts: 32000, cat: 'foo', tid: 53, ph: 'B'}, + {name: 'b', args: {}, pid: 52, ts: 54000, cat: 'foo', tid: 53, ph: 'E'} + ]; + + test('fullRecording', function() { + const mock = new tr.ui.e.about_tracing.MockTracingControllerClient(); + mock.expectRequest('endRecording', function() { + return ''; + }); + mock.expectRequest('getCategories', function() { + tr.b.timeout(20).then(() => + recordingPromise.selectionDlg.clickRecordButton()); + return ['a', 'b', 'c']; + }); + mock.expectRequest('beginRecording', function(recordingOptions) { + assert.typeOf(recordingOptions.included_categories, 'array'); + assert.typeOf(recordingOptions.excluded_categories, 'array'); + assert.typeOf(recordingOptions.enable_systrace, 'boolean'); + assert.typeOf(recordingOptions.record_mode, 'string'); + tr.b.timeout(10).then(() => + recordingPromise.progressDlg.clickStopButton()); + return ''; + }); + mock.expectRequest('endRecording', function(data) { + return JSON.stringify(testData); + }); + + const recordingPromise = tr.ui.e.about_tracing.beginRecording(mock); + + return recordingPromise.then(function(data) { + mock.assertAllRequestsHandled(); + assert.strictEqual(data, JSON.stringify(testData)); + }, function(error) { + assert.fail(error); + }); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html new file mode 100644 index 00000000000..a5383973a80 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog.html @@ -0,0 +1,689 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> +<link rel="import" href="/tracing/core/filter.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/info_bar_group.html"> +<link rel="import" href="/tracing/ui/base/overlay.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> + +<template id="record-selection-dialog-template"> + <style> + .categories-column-view { + display: flex; + flex-direction: column; + font-family: sans-serif; + max-width: 640px; + min-height: 0; + min-width: 0; + opacity: 1; + transition: max-height 1s ease, max-width 1s ease, opacity 1s ease; + will-change: opacity; + } + + .categories-column-view-hidden { + max-height: 0; + max-width: 0; + opacity: 0; + overflow: hidden; + display: none; + } + + .categories-selection { + display: flex; + flex-direction: row; + } + + .category-presets { + padding: 4px; + } + + .category-description { + color: #aaa; + font-size: small; + max-height: 1em; + opacity: 1; + padding-left: 4px; + padding-right: 4px; + text-align: right; + transition: max-height 1s ease, opacity 1s ease; + will-change: opacity; + } + + .category-description-hidden { + max-height: 0; + opacity: 0; + } + + .default-enabled-categories, + .default-disabled-categories { + flex: 1 1 auto; + display: flex; + flex-direction: column; + padding: 4px; + width: 300px; + } + + .default-enabled-categories > div, + .default-disabled-categories > div { + padding: 4px; + } + + .tracing-modes { + flex: 1 0 auto; + display: flex; + flex-direction: reverse; + padding: 4px; + border-bottom: 2px solid #ddd; + border-top: 2px solid #ddd; + } + + .default-disabled-categories { + border-left: 2px solid #ddd; + } + + .warning-default-disabled-categories { + display: inline-block; + font-weight: bold; + text-align: center; + color: #BD2E2E; + width: 2.0ex; + height: 2.0ex; + border-radius: 2.0ex; + border: 1px solid #BD2E2E; + } + + .categories { + font-size: 80%; + padding: 10px; + flex: 1 1 auto; + } + + .group-selectors { + font-size: 80%; + border-bottom: 1px solid #ddd; + padding-bottom: 6px; + flex: 0 0 auto; + } + + .group-selectors button { + padding: 1px; + } + + .record-selection-dialog .labeled-option-group { + flex: 0 0 auto; + flex-direction: column; + display: flex; + } + + .record-selection-dialog .labeled-option { + border-top: 5px solid white; + border-bottom: 5px solid white; + } + + .record-selection-dialog .edit-categories { + padding-left: 6px; + } + + .record-selection-dialog .edit-categories:after { + padding-left: 15px; + font-size: 125%; + } + + .record-selection-dialog .labeled-option-group:not(.categories-expanded) + .edit-categories:after { + content: '\25B8'; /* Right triangle */ + } + + .record-selection-dialog .labeled-option-group.categories-expanded + .edit-categories:after { + content: '\25BE'; /* Down triangle */ + } + + </style> + + <div class="record-selection-dialog"> + <tr-ui-b-info-bar-group></tr-ui-b-info-bar-group> + <div class="category-presets"> + </div> + <div class="category-description"></div> + <div class="categories-column-view"> + <div class="tracing-modes"></div> + <div class="categories-selection"> + <div class="default-enabled-categories"> + <div>Record Categories</div> + <div class="group-selectors"> + Select + <button class="all-btn">All</button> + <button class="none-btn">None</button> + </div> + <div class="categories"></div> + </div> + <div class="default-disabled-categories"> + <div>Disabled by Default Categories + <a class="warning-default-disabled-categories">!</a> + </div> + <div class="group-selectors"> + Select + <button class="all-btn">All</button> + <button class="none-btn">None</button> + </div> + <div class="categories"></div> + </div> + </div> + </div> + </div> +</template> + +<script> +'use strict'; + +/** + * @fileoverview RecordSelectionDialog presents the available categories + * to be enabled/disabled during tr.c. + */ +tr.exportTo('tr.ui.e.about_tracing', function() { + const THIS_DOC = document.currentScript.ownerDocument; + const RecordSelectionDialog = tr.ui.b.define('div'); + + const DEFAULT_PRESETS = [ + {title: 'Web developer', + categoryFilter: ['blink', 'cc', 'netlog', 'renderer.scheduler', + 'sequence_manager', 'toplevel', 'v8']}, + {title: 'Input latency', + categoryFilter: ['benchmark', 'input', 'evdev', 'renderer.scheduler', + 'sequence_manager', 'toplevel']}, + {title: 'Rendering', + categoryFilter: ['blink', 'cc', 'gpu', 'toplevel', 'viz']}, + {title: 'Javascript and rendering', + categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler', + 'sequence_manager', 'v8', 'toplevel', 'viz']}, + {title: 'Frame Viewer', + categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler', + 'sequence_manager', 'v8', 'toplevel', + 'disabled-by-default-blink.invalidation', + 'disabled-by-default-cc.debug', + 'disabled-by-default-cc.debug.picture', + 'disabled-by-default-cc.debug.display_items']}, + {title: 'Manually select settings', + categoryFilter: []} + ]; + const RECORDING_MODES = [ + {'label': 'Record until full', + 'value': 'record-until-full'}, + {'label': 'Record continuously', + 'value': 'record-continuously'}, + {'label': 'Record as much as possible', + 'value': 'record-as-much-as-possible'}]; + const DEFAULT_RECORD_MODE = 'record-until-full'; + const DEFAULT_CONTINUOUS_TRACING = true; + const DEFAULT_SYSTEM_TRACING = true; + const DEFAULT_SAMPLING_TRACING = false; + + RecordSelectionDialog.prototype = { + __proto__: tr.ui.b.Overlay.prototype, + + decorate() { + tr.ui.b.Overlay.prototype.decorate.call(this); + this.title = 'Record a new trace...'; + + Polymer.dom(this).classList.add('record-dialog-overlay'); + + const node = + tr.ui.b.instantiateTemplate('#record-selection-dialog-template', + THIS_DOC); + Polymer.dom(this).appendChild(node); + + this.recordButtonEl_ = document.createElement('button'); + Polymer.dom(this.recordButtonEl_).textContent = 'Record'; + this.recordButtonEl_.addEventListener( + 'click', + this.onRecordButtonClicked_.bind(this)); + this.recordButtonEl_.style.fontSize = '110%'; + Polymer.dom(this.buttons).appendChild(this.recordButtonEl_); + + this.categoriesView_ = Polymer.dom(this).querySelector( + '.categories-column-view'); + this.presetsEl_ = Polymer.dom(this).querySelector('.category-presets'); + Polymer.dom(this.presetsEl_).appendChild(tr.ui.b.createOptionGroup( + this, 'currentlyChosenPreset', + 'about_tracing.record_selection_dialog_preset', + DEFAULT_PRESETS[0].categoryFilter, + DEFAULT_PRESETS.map(function(p) { + return { label: p.title, value: p.categoryFilter }; + }))); + + this.tracingRecordModeSltr_ = tr.ui.b.createSelector( + this, 'tracingRecordMode', + 'recordSelectionDialog.tracingRecordMode', + DEFAULT_RECORD_MODE, RECORDING_MODES); + + this.systemTracingBn_ = tr.ui.b.createCheckBox( + undefined, undefined, + 'recordSelectionDialog.useSystemTracing', DEFAULT_SYSTEM_TRACING, + 'System tracing'); + this.samplingTracingBn_ = tr.ui.b.createCheckBox( + undefined, undefined, + 'recordSelectionDialog.useSampling', DEFAULT_SAMPLING_TRACING, + 'State sampling'); + this.tracingModesContainerEl_ = Polymer.dom(this).querySelector( + '.tracing-modes'); + Polymer.dom(this.tracingModesContainerEl_).appendChild( + this.tracingRecordModeSltr_); + Polymer.dom(this.tracingModesContainerEl_).appendChild( + this.systemTracingBn_); + Polymer.dom(this.tracingModesContainerEl_).appendChild( + this.samplingTracingBn_); + + this.enabledCategoriesContainerEl_ = + Polymer.dom(this).querySelector( + '.default-enabled-categories .categories'); + + this.disabledCategoriesContainerEl_ = + Polymer.dom(this).querySelector( + '.default-disabled-categories .categories'); + + this.createGroupSelectButtons_( + Polymer.dom(this).querySelector('.default-enabled-categories')); + this.createGroupSelectButtons_( + Polymer.dom(this).querySelector('.default-disabled-categories')); + this.createDefaultDisabledWarningDialog_( + Polymer.dom(this).querySelector( + '.warning-default-disabled-categories')); + this.editCategoriesOpened_ = false; + + // TODO(chrishenry): When used with tr.ui.b.Overlay (such as in + // chrome://tracing, this does not yet look quite right due to + // the 10px overlay content padding (but it's good enough). + this.infoBarGroup_ = Polymer.dom(this).querySelector( + 'tr-ui-b-info-bar-group'); + + this.addEventListener('visible-change', this.onVisibleChange_.bind(this)); + }, + + set supportsSystemTracing(s) { + if (s) { + this.systemTracingBn_.style.display = undefined; + } else { + this.systemTracingBn_.style.display = 'none'; + this.useSystemTracing = false; + } + }, + + get tracingRecordMode() { + return this.tracingRecordModeSltr_.selectedValue; + }, + set tracingRecordMode(value) { + this.tracingRecordMode_ = value; + }, + + get useSystemTracing() { + return this.systemTracingBn_.checked; + }, + set useSystemTracing(value) { + this.systemTracingBn_.checked = !!value; + }, + + get useSampling() { + return this.samplingTracingBn_.checked; + }, + set useSampling(value) { + this.samplingTracingBn_.checked = !!value; + }, + + set categories(c) { + if (!(c instanceof Array)) { + throw new Error('categories must be an array'); + } + this.categories_ = c; + + for (let i = 0; i < this.categories_.length; i++) { + const split = this.categories_[i].split(','); + this.categories_[i] = split.shift(); + if (split.length > 0) { + this.categories_ = this.categories_.concat(split); + } + } + }, + + set settings_key(k) { + this.settings_key_ = k; + }, + + set settings(s) { + throw new Error('Dont use this!'); + }, + + usingPreset_() { + return this.currentlyChosenPreset_.length > 0; + }, + + get currentlyChosenPreset() { + return this.currentlyChosenPreset_; + }, + + set currentlyChosenPreset(preset) { + if (!(preset instanceof Array)) { + throw new Error('RecordSelectionDialog.currentlyChosenPreset:' + + ' preset must be an array.'); + } + this.currentlyChosenPreset_ = preset; + + if (this.usingPreset_()) { + this.changeEditCategoriesState_(false); + } else { + this.updateCategoryColumnView_(true); + this.changeEditCategoriesState_(true); + } + this.updateManualSelectionView_(); + this.updatePresetDescription_(); + }, + + updateManualSelectionView_() { + const classList = Polymer.dom(this.categoriesView_).classList; + if (!this.usingPreset_()) { + classList.remove('categories-column-view-hidden'); + } else { + if (this.editCategoriesOpened_) { + classList.remove('categories-column-view-hidden'); + } else { + classList.add('categories-column-view-hidden'); + } + } + }, + + updateCategoryColumnView_(shouldReadFromSettings) { + const categorySet = Polymer.dom(this).querySelectorAll('.categories'); + for (let i = 0; i < categorySet.length; ++i) { + const categoryGroup = categorySet[i].children; + for (let j = 0; j < categoryGroup.length; ++j) { + const categoryEl = categoryGroup[j].children[0]; + categoryEl.checked = shouldReadFromSettings ? + tr.b.Settings.get(categoryEl.value, false, this.settings_key_) : + false; + } + } + }, + + onClickEditCategories() { + if (!this.usingPreset_()) return; + + if (!this.editCategoriesOpened_) { + this.updateCategoryColumnView_(false); + for (let i = 0; i < this.currentlyChosenPreset_.length; ++i) { + const categoryEl = this.querySelector('#' + + this.currentlyChosenPreset_[i]); + if (!categoryEl) continue; + categoryEl.checked = true; + } + } + + this.changeEditCategoriesState_(!this.editCategoriesOpened_); + this.updateManualSelectionView_(); + this.recordButtonEl_.focus(); + }, + + changeEditCategoriesState_(editCategoriesState) { + const presetOptionsGroup = Polymer.dom(this).querySelector( + '.labeled-option-group'); + if (!presetOptionsGroup) return; + + this.editCategoriesOpened_ = editCategoriesState; + if (this.editCategoriesOpened_) { + Polymer.dom(presetOptionsGroup).classList.add('categories-expanded'); + } else { + Polymer.dom(presetOptionsGroup).classList.remove( + 'categories-expanded'); + } + }, + + updatePresetDescription_() { + const description = Polymer.dom(this).querySelector( + '.category-description'); + if (this.usingPreset_()) { + description.innerText = this.currentlyChosenPreset_; + Polymer.dom(description).classList.remove( + 'category-description-hidden'); + } else { + description.innerText = ''; + if (!Polymer.dom(description).classList.contains( + 'category-description-hidden')) { + Polymer.dom(description).classList.add('category-description-hidden'); + } + } + }, + + includedAndExcludedCategories() { + let includedCategories = []; + let excludedCategories = []; + if (this.usingPreset_()) { + const allCategories = this.allCategories_(); + for (const category in allCategories) { + const disabledByDefault = + category.indexOf('disabled-by-default-') === 0; + if (this.currentlyChosenPreset_.indexOf(category) >= 0) { + if (disabledByDefault) { + includedCategories.push(category); + } + } else { + if (!disabledByDefault) { + excludedCategories.push(category); + } + } + } + return { + included: includedCategories, + excluded: excludedCategories + }; + } + + excludedCategories = this.unselectedCategories_(); + includedCategories = this.enabledDisabledByDefaultCategories_(); + return { + included: includedCategories, + excluded: excludedCategories + }; + }, + + clickRecordButton() { + this.recordButtonEl_.click(); + }, + + onRecordButtonClicked_() { + this.visible = false; + tr.b.dispatchSimpleEvent(this, 'recordclick'); + return false; + }, + + collectInputs_(inputs, isChecked) { + const inputsLength = inputs.length; + const categories = []; + for (let i = 0; i < inputsLength; ++i) { + const input = inputs[i]; + if (input.checked === isChecked) { + categories.push(input.value); + } + } + return categories; + }, + + unselectedCategories_() { + const inputs = + Polymer.dom(this.enabledCategoriesContainerEl_).querySelectorAll( + 'input'); + return this.collectInputs_(inputs, false); + }, + + enabledDisabledByDefaultCategories_() { + const inputs = + Polymer.dom(this.disabledCategoriesContainerEl_).querySelectorAll( + 'input'); + return this.collectInputs_(inputs, true); + }, + + onVisibleChange_() { + if (this.visible) { + this.updateForm_(); + } + }, + + buildInputs_(inputs, checkedDefault, parent) { + const inputsLength = inputs.length; + for (let i = 0; i < inputsLength; i++) { + const category = inputs[i]; + + const inputEl = document.createElement('input'); + inputEl.type = 'checkbox'; + inputEl.id = category; + inputEl.value = category; + + inputEl.checked = tr.b.Settings.get( + category, checkedDefault, this.settings_key_); + inputEl.onclick = this.updateSetting_.bind(this); + + const labelEl = document.createElement('label'); + Polymer.dom(labelEl).textContent = + category.replace('disabled-by-default-', ''); + Polymer.dom(labelEl).setAttribute('for', category); + + const divEl = document.createElement('div'); + Polymer.dom(divEl).appendChild(inputEl); + Polymer.dom(divEl).appendChild(labelEl); + + Polymer.dom(parent).appendChild(divEl); + } + }, + + allCategories_() { + // Dedup the categories. We may have things in settings that are also + // returned when we query the category list. + const categorySet = {}; + const allCategories = + this.categories_.concat(tr.b.Settings.keys(this.settings_key_)); + const allCategoriesLength = allCategories.length; + for (let i = 0; i < allCategoriesLength; ++i) { + categorySet[allCategories[i]] = true; + } + return categorySet; + }, + + updateForm_() { + function ignoreCaseCompare(a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + } + + // Clear old categories + Polymer.dom(this.enabledCategoriesContainerEl_).innerHTML = ''; + Polymer.dom(this.disabledCategoriesContainerEl_).innerHTML = ''; + + this.recordButtonEl_.focus(); + + const allCategories = this.allCategories_(); + let categories = []; + let disabledCategories = []; + for (const category in allCategories) { + if (category.indexOf('disabled-by-default-') === 0) { + disabledCategories.push(category); + } else { + categories.push(category); + } + } + disabledCategories = disabledCategories.sort(ignoreCaseCompare); + categories = categories.sort(ignoreCaseCompare); + + if (this.categories_.length === 0) { + this.infoBarGroup_.addMessage( + 'No categories found; recording will use default categories.'); + } + + this.buildInputs_(categories, true, this.enabledCategoriesContainerEl_); + + if (disabledCategories.length > 0) { + this.disabledCategoriesContainerEl_.hidden = false; + this.buildInputs_(disabledCategories, false, + this.disabledCategoriesContainerEl_); + } + }, + + updateSetting_(e) { + const checkbox = e.target; + tr.b.Settings.set(checkbox.value, checkbox.checked, this.settings_key_); + + // Change the current record mode to 'Manually select settings' from + // preset mode if and only if currently user is in preset record mode + // and user selects/deselects any category in 'Edit Categories' mode. + if (this.usingPreset_()) { + this.currentlyChosenPreset_ = []; /* manually select settings */ + const categoryEl = this.querySelector( + '#category-preset-Manually-select-settings'); + categoryEl.checked = true; + const description = Polymer.dom(this).querySelector( + '.category-description'); + description.innerText = ''; + Polymer.dom(description).classList.add('category-description-hidden'); + } + }, + + createGroupSelectButtons_(parent) { + const flipInputs = function(dir) { + const inputs = Polymer.dom(parent).querySelectorAll('input'); + for (let i = 0; i < inputs.length; i++) { + if (inputs[i].checked === dir) continue; + // click() is used so the settings will be correclty stored. Setting + // checked does not trigger the onclick (or onchange) callback. + inputs[i].click(); + } + }; + + const allBtn = Polymer.dom(parent).querySelector('.all-btn'); + allBtn.onclick = function(evt) { + flipInputs(true); + evt.preventDefault(); + }; + + const noneBtn = Polymer.dom(parent).querySelector('.none-btn'); + noneBtn.onclick = function(evt) { + flipInputs(false); + evt.preventDefault(); + }; + }, + + setWarningDialogOverlayText_(messages) { + const contentDiv = document.createElement('div'); + + for (let i = 0; i < messages.length; ++i) { + const messageDiv = document.createElement('div'); + Polymer.dom(messageDiv).textContent = messages[i]; + Polymer.dom(contentDiv).appendChild(messageDiv); + } + Polymer.dom(this.warningOverlay_).textContent = ''; + Polymer.dom(this.warningOverlay_).appendChild(contentDiv); + }, + + createDefaultDisabledWarningDialog_(warningLink) { + function onClickHandler(evt) { + this.warningOverlay_ = tr.ui.b.Overlay(); + this.warningOverlay_.parentEl_ = this; + this.warningOverlay_.title = 'Warning...'; + this.warningOverlay_.userCanClose = true; + this.warningOverlay_.visible = true; + + this.setWarningDialogOverlayText_([ + 'Enabling the default disabled categories may have', + 'performance and memory impact while tr.c.' + ]); + + evt.preventDefault(); + } + warningLink.onclick = onClickHandler.bind(this); + } + }; + + return { + RecordSelectionDialog, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html new file mode 100644 index 00000000000..7c62b487305 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/record_selection_dialog_test.html @@ -0,0 +1,426 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> + +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/ui/extras/about_tracing/record_selection_dialog.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantitate', function() { + const showButton = document.createElement('button'); + Polymer.dom(showButton).textContent = 'Show record selection dialog'; + this.addHTMLOutput(showButton); + + showButton.addEventListener('click', function(e) { + e.stopPropagation(); + + const categories = []; + for (let i = 0; i < 30; i++) { + categories.push('cat-' + i); + } + for (let i = 0; i < 20; i++) { + categories.push('disabled-by-default-cat-' + i); + } + categories.push( + 'really-really-really-really-really-really-very-loong-cat'); + categories.push('first,second,third'); + categories.push('cc,disabled-by-default-cc.debug'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = categories; + dlg.settings_key = 'key'; + dlg.visible = true; + }); + }); + + test('recordSelectionDialog_splitCategories', function() { + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = + ['cc,disabled-by-default-one,cc.debug', 'two,three', 'three']; + dlg.settings_key = 'key'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + const expected = + ['"cc"', '"cc.debug"', '"disabled-by-default-one"', '"three"', '"two"']; + + const labels = Polymer.dom(dlg).querySelectorAll('.categories input'); + let results = []; + for (let i = 0; i < labels.length; i++) { + results.push('"' + labels[i].value + '"'); + } + results = results.sort(); + + assert.deepEqual(results, expected); + }); + + test('recordSelectionDialog_UpdateForm_NoSettings', function() { + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one', 'two', 'three']; + dlg.settings_key = 'key'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input'); + assert.strictEqual(checkboxes.length, 3); + assert.strictEqual(checkboxes[0].id, 'three'); + assert.strictEqual(checkboxes[0].value, 'three'); + assert.isTrue(checkboxes[0].checked); + assert.strictEqual(checkboxes[1].id, 'two'); + assert.strictEqual(checkboxes[1].value, 'two'); + assert.isTrue(checkboxes[1].checked); + assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one'); + assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one'); + assert.isFalse(checkboxes[2].checked); + + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + + const labels = Polymer.dom(dlg).querySelectorAll('.categories label'); + assert.strictEqual(labels.length, 3); + assert.strictEqual(Polymer.dom(labels[0]).textContent, 'three'); + assert.strictEqual(Polymer.dom(labels[1]).textContent, 'two'); + assert.strictEqual(Polymer.dom(labels[2]).textContent, 'one'); + }); + + test('recordSelectionDialog_UpdateForm_Settings', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input'); + assert.strictEqual(checkboxes.length, 3); + assert.strictEqual(checkboxes[0].id, 'three'); + assert.strictEqual(checkboxes[0].value, 'three'); + assert.isFalse(checkboxes[0].checked); + assert.strictEqual(checkboxes[1].id, 'two'); + assert.strictEqual(checkboxes[1].value, 'two'); + assert.isTrue(checkboxes[1].checked); + assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one'); + assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one'); + assert.isFalse(checkboxes[2].checked); + + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']); + + const labels = Polymer.dom(dlg).querySelectorAll('.categories label'); + assert.strictEqual(labels.length, 3); + assert.strictEqual(Polymer.dom(labels[0]).textContent, 'three'); + assert.strictEqual(Polymer.dom(labels[1]).textContent, 'two'); + assert.strictEqual(Polymer.dom(labels[2]).textContent, 'one'); + }); + + test('recordSelectionDialog_UpdateForm_DisabledByDefault', function() { + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-bar', 'baz']; + dlg.settings_key = 'categories'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + + const inputs = + Polymer.dom(dlg).querySelector('input#disabled-by-default-bar').click(); + + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-bar']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + + assert.isFalse( + tr.b.Settings.get('disabled-by-default-foo', false, 'categories')); + }); + + test('selectAll', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + }); + + test('selectNone', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + // Enables the three option, two already enabled. + Polymer.dom(dlg).querySelector('.default-enabled-categories .all-btn') + .click(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + assert.isTrue(tr.b.Settings.get('three', false, 'categories')); + + // Disables three and two. + Polymer.dom(dlg).querySelector('.default-enabled-categories .none-btn') + .click(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['three', 'two']); + assert.isFalse(tr.b.Settings.get('two', false, 'categories')); + assert.isFalse(tr.b.Settings.get('three', false, 'categories')); + + // Turn categories back on so they can be ignored. + Polymer.dom(dlg).querySelector('.default-enabled-categories .all-btn') + .click(); + + // Enables disabled category. + Polymer.dom(dlg).querySelector('.default-disabled-categories .all-btn') + .click(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-one']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + assert.isTrue( + tr.b.Settings.get('disabled-by-default-one', false, 'categories')); + + // Turn disabled by default back off. + Polymer.dom(dlg).querySelector('.default-disabled-categories .none-btn') + .click(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + assert.isFalse( + tr.b.Settings.get('disabled-by-default-one', false, 'categories')); + }); + + test('recordSelectionDialog_noPreset', function() { + tr.b.Settings.set('about_tracing.record_selection_dialog_preset', []); + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + assert.isFalse(dlg.usingPreset_()); + }); + + test('recordSelectionDialog_defaultPreset', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + // Note: currentlyChosenPreset is not set here, so the default is used. + dlg.updateForm_(); + + // Make sure the default filter is returned. + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['three', 'two']); + + // Make sure the default tracing types are returned. + assert.strictEqual(dlg.tracingRecordMode, 'record-until-full'); + assert.isTrue(dlg.useSystemTracing); + assert.isFalse(dlg.useSampling); + + // Make sure the manual settings are not visible. + const classList = Polymer.dom(dlg.categoriesView_).classList; + assert.isTrue(classList.contains('categories-column-view-hidden')); + + // Verify manual settings do not modify the checkboxes. + const checkboxes = Polymer.dom(dlg).querySelectorAll('.categories input'); + assert.strictEqual(checkboxes.length, 3); + assert.strictEqual(checkboxes[0].id, 'three'); + assert.strictEqual(checkboxes[0].value, 'three'); + assert.isFalse(checkboxes[0].checked); + assert.strictEqual(checkboxes[1].id, 'two'); + assert.strictEqual(checkboxes[1].value, 'two'); + assert.isTrue(checkboxes[1].checked); + assert.strictEqual(checkboxes[2].id, 'disabled-by-default-one'); + assert.strictEqual(checkboxes[2].value, 'disabled-by-default-one'); + assert.isFalse(checkboxes[2].checked); + }); + + test('recordSelectionDialog_editPreset', function() { + function createDialog() { + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['one', 'two', 'disabled-by-default-three']; + dlg.settings_key = 'categories'; + // Note: currentlyChosenPreset is not set here, so the default is used. + dlg.updateForm_(); + return dlg; + } + + // After the dialog is created, it should be using the default preset. + let dlg = createDialog(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['one', 'two']); + assert.isTrue(dlg.usingPreset_()); + assert.isFalse( + dlg.querySelector('#category-preset-Manually-select-settings').checked); + + // After clicking on "Edit Categories", the default preset should still be + // used. + dlg.onClickEditCategories(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['one', 'two']); + assert.isTrue(dlg.usingPreset_()); + assert.isFalse( + dlg.querySelector('#category-preset-Manually-select-settings').checked); + + // After clicking on category checkbox(es), the mode should be changed to + // "Manually select settings". + Array.prototype.forEach.call(dlg.querySelectorAll('.categories input'), + checkbox => checkbox.click()); + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-three']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, []); + assert.isFalse(dlg.usingPreset_()); + assert.isTrue( + dlg.querySelector('#category-preset-Manually-select-settings').checked); + + // After the dialog is opened again, it should be using the default preset. + // More importantly, the default preset should NOT be modified. + dlg = createDialog(); + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['one', 'two']); + assert.isTrue(dlg.usingPreset_()); + assert.isFalse( + dlg.querySelector('#category-preset-Manually-select-settings').checked); + }); + + test('recordSelectionDialog_changePresets', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + tr.b.Settings.set('disabled-by-default-cc.debug', true, 'categories'); + tr.b.Settings.set('recordSelectionDialog.tracingRecordMode', + 'record-as-much-as-possible'); + tr.b.Settings.set('recordSelectionDialog.useSystemTracing', true); + tr.b.Settings.set('recordSelectionDialog.useSampling', false); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + // Note: currentlyChosenPreset is not set here, so the default is used. + dlg.updateForm_(); + + // Preset mode is on. + assert.isTrue(dlg.usingPreset_()); + + // Make sure the default filter is returned. + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['three', 'two']); + + // Make sure the default tracing types are returned. + assert.strictEqual(dlg.tracingRecordMode, 'record-as-much-as-possible'); + assert.isTrue(dlg.useSystemTracing); + assert.isFalse(dlg.useSampling); + + // Make sure the manual settings are not visible. + const classList = Polymer.dom(dlg.categoriesView_).classList; + assert.isTrue(classList.contains('categories-column-view-hidden')); + + // Switch to manual settings and verify the default values are not returned. + dlg.currentlyChosenPreset = []; + + // Preset mode is off. + assert.isFalse(dlg.usingPreset_()); + + // Make sure the default filter is returned. + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-cc.debug']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']); + + // Make sure the tracing types set by catalog are returned. + assert.strictEqual(dlg.tracingRecordMode, 'record-as-much-as-possible'); + assert.isTrue(dlg.useSystemTracing); + assert.isFalse(dlg.useSampling); + assert.isFalse(classList.contains('categories-column-view-hidden')); + + // Switch to the graphics, rendering, and rasterization preset. + dlg.currentlyChosenPreset = ['blink', 'cc', 'renderer', + 'disabled-by-default-cc.debug']; + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-cc.debug']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['three', 'two']); + }); + + test('recordSelectionDialog_savedPreset', function() { + tr.b.Settings.set('two', true, 'categories'); + tr.b.Settings.set('three', false, 'categories'); + tr.b.Settings.set('recordSelectionDialog.tracingRecordMode', + 'record-continuously'); + tr.b.Settings.set('recordSelectionDialog.useSystemTracing', true); + tr.b.Settings.set('recordSelectionDialog.useSampling', true); + tr.b.Settings.set('tr.ui.e.about_tracing.record_selection_dialog_preset', + ['blink', 'cc', 'renderer', 'cc.debug']); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.categories = ['disabled-by-default-one']; + dlg.settings_key = 'categories'; + dlg.updateForm_(); + + // Make sure the correct filter is returned. + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['three', 'two']); + + // Make sure the correct tracing types are returned. + assert.strictEqual(dlg.tracingRecordMode, 'record-continuously'); + assert.isTrue(dlg.useSystemTracing); + assert.isTrue(dlg.useSampling); + + // Make sure the manual settings are not visible. + const classList = Polymer.dom(dlg.categoriesView_).classList; + assert.isTrue(classList.contains('categories-column-view-hidden')); + + // Switch to manual settings and verify the default values are not returned. + dlg.currentlyChosenPreset = []; + assert.deepEqual(dlg.includedAndExcludedCategories().included, []); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, ['three']); + assert.strictEqual(dlg.tracingRecordMode, 'record-continuously'); + assert.isTrue(dlg.useSystemTracing); + assert.isTrue(dlg.useSampling); + assert.isFalse(classList.contains('categories-column-view-hidden')); + }); + + test('recordSelectionDialog_categoryFilters', function() { + tr.b.Settings.set('default1', true, 'categories'); + tr.b.Settings.set('disabled1', false, 'categories'); + tr.b.Settings.set('disabled-by-default-cc.disabled2', false, 'categories'); + tr.b.Settings.set('input', true, 'categories'); + tr.b.Settings.set('blink', true, 'categories'); + tr.b.Settings.set('cc', false, 'categories'); + tr.b.Settings.set('disabled-by-default-cc.debug', true, 'categories'); + + const dlg = new tr.ui.e.about_tracing.RecordSelectionDialog(); + dlg.settings_key = 'categories'; + dlg.categories = []; + dlg.currentlyChosenPreset = []; + dlg.updateForm_(); + + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-cc.debug']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['cc', 'disabled1']); + + // Switch to the graphics, rendering, and rasterization preset. + dlg.currentlyChosenPreset = ['blink', 'cc', 'renderer', + 'disabled-by-default-cc.debug']; + assert.deepEqual(dlg.includedAndExcludedCategories().included, + ['disabled-by-default-cc.debug']); + assert.deepEqual(dlg.includedAndExcludedCategories().excluded, + ['default1', 'disabled1', 'input']); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html new file mode 100644 index 00000000000..c00bbe915e4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/tracing_controller_client.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> + +<link rel="import" href="/tracing/base/base.html"> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + /** + * Communicates with content/browser/tracing_controller_impl.cc + * + * @constructor + */ + class TracingControllerClient { + beginMonitoring(monitoringOptions) { } + endMonitoring() { } + captureMonitoring() { } + getMonitoringStatus() { } + getCategories() { } + beginRecording(recordingOptions) { } + beginGetBufferPercentFull() { } + endRecording() { } + defaultTraceName() { } + } + + return { + TracingControllerClient, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html new file mode 100644 index 00000000000..d2c6adcac2a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/about_tracing/xhr_based_tracing_controller_client.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2013 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. +--> + +<link rel="import" href="/tracing/base/base64.html"> +<link rel="import" + href="/tracing/ui/extras/about_tracing/tracing_controller_client.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.about_tracing', function() { + const Base64 = tr.b.Base64; + + function beginXhr(method, path, data) { + if (data === undefined) data = null; + + return new Promise(function(resolve, reject) { + const req = new XMLHttpRequest(); + if (method !== 'POST' && data !== null) { + throw new Error('Non-POST should have data==null'); + } + req.open(method, path, true); + req.onreadystatechange = function(e) { + if (req.readyState === 4) { + window.setTimeout(function() { + if (req.status === 200 && req.responseText !== '##ERROR##') { + resolve(req.responseText); + } else { + reject(new Error('Error occured at ' + path)); + } + }, 0); + } + }; + req.send(data); + }); + } + + /** + * @constructor + */ + function XhrBasedTracingControllerClient() { } + + XhrBasedTracingControllerClient.prototype = { + __proto__: tr.ui.e.about_tracing.TracingControllerClient.prototype, + + beginMonitoring(monitoringOptions) { + const monitoringOptionsB64 = Base64.btoa(JSON.stringify( + monitoringOptions)); + return beginXhr('GET', '/json/begin_monitoring?' + monitoringOptionsB64); + }, + + endMonitoring() { + return beginXhr('GET', '/json/end_monitoring'); + }, + + captureMonitoring() { + return beginXhr('GET', '/json/capture_monitoring_compressed').then( + function(data) { + const decodedSize = Base64.getDecodedBufferLength(data); + const buffer = new ArrayBuffer(decodedSize); + Base64.DecodeToTypedArray(data, new DataView(buffer)); + return buffer; + } + ); + }, + + getMonitoringStatus() { + return beginXhr('GET', '/json/get_monitoring_status').then( + function(monitoringOptionsB64) { + return JSON.parse(Base64.atob(monitoringOptionsB64)); + }); + }, + + getCategories() { + return beginXhr('GET', '/json/categories').then( + function(json) { + return JSON.parse(json); + }); + }, + + beginRecording(recordingOptions) { + const recordingOptionsB64 = Base64.btoa(JSON.stringify(recordingOptions)); + return beginXhr('GET', '/json/begin_recording?' + + recordingOptionsB64); + }, + + beginGetBufferPercentFull() { + return beginXhr('GET', '/json/get_buffer_percent_full'); + }, + + endRecording() { + return beginXhr('GET', '/json/end_recording_compressed').then( + function(data) { + const decodedSize = Base64.getDecodedBufferLength(data); + const buffer = new ArrayBuffer(decodedSize); + Base64.DecodeToTypedArray(data, new DataView(buffer)); + return buffer; + } + ); + }, + + defaultTraceName() { + return 'trace.json.gz'; + } + }; + + return { + XhrBasedTracingControllerClient, + }; +}); +</script> |