diff options
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/ui/extras')
86 files changed, 14062 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> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html new file mode 100644 index 00000000000..79ba7e593c0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/cc.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/extras/chrome/cc/cc.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_list_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/tile_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html new file mode 100644 index 00000000000..f8bfd671355 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger.html @@ -0,0 +1,451 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> +<link rel="import" href="/tracing/ui/base/info_bar.html"> +<link rel="import" href="/tracing/ui/base/list_view.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_selector.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/chrome/cc/display_item_list_item.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html"> + +<template id="tr-ui-e-chrome-cc-display-item-debugger-template"> + <left-panel> + <display-item-info> + <header> + <span class='title'>Display Item List</span> + <span class='size'></span> + <div class='export'> + <input class='dlfilename' type='text' value='displayitemlist.json' /> + <button class='dlexport'>Export display item list</button> + </div> + <div class='export'> + <input class='skpfilename' type='text' value='skpicture.skp' /> + <button class='skpexport'>Export list as SkPicture</button> + </div> + </header> + </display-item-info> + </left-panel> + <right-panel> + <raster-area> + <canvas-scroller> + <canvas></canvas> + </canvas-scroller> + </raster-area> + </right-panel> +</template> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const THIS_DOC = document.currentScript.ownerDocument; + + /** + * DisplayItemDebugger is a view of a DisplayItemListSnapshot for inspecting + * a display item list and the pictures within it. + * + * @constructor + */ + const DisplayItemDebugger = tr.ui.b.define( + 'tr-ui-e-chrome-cc-display-item-debugger'); + + DisplayItemDebugger.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + const node = tr.ui.b.instantiateTemplate( + '#tr-ui-e-chrome-cc-display-item-debugger-template', THIS_DOC); + + Polymer.dom(this).appendChild(node); + this.style.flexGrow = 1; + this.style.flexShrink = 1; + this.style.flexBasis = 'auto'; + this.style.display = 'flex'; + this.style.minWidth = 0; + + this.pictureAsImageData_ = undefined; + this.zoomScaleValue_ = 1; + + this.sizeInfo_ = Polymer.dom(this).querySelector('.size'); + this.rasterArea_ = Polymer.dom(this).querySelector('raster-area'); + this.rasterArea_.style.flexGrow = 1; + this.rasterArea_.style.flexShrink = 1; + this.rasterArea_.style.flexBasis = 'auto'; + this.rasterArea_.style.backgroundColor = '#ddd'; + this.rasterArea_.style.minHeight = '200px'; + this.rasterArea_.style.minWidth = '200px'; + this.rasterArea_.style.paddingLeft = '5px'; + this.rasterArea_.style.display = 'flex'; + this.rasterArea_.style.flexDirection = 'column'; + this.rasterCanvas_ = + Polymer.dom(this.rasterArea_).querySelector('canvas'); + this.rasterCtx_ = this.rasterCanvas_.getContext('2d'); + + const canvasScroller = Polymer.dom(this).querySelector('canvas-scroller'); + canvasScroller.style.flexGrow = 1; + canvasScroller.style.flexShrink = 1; + canvasScroller.style.flexBasis = 'auto'; + canvasScroller.style.minWidth = 0; + canvasScroller.style.minHeight = 0; + canvasScroller.style.overflow = 'auto'; + + this.trackMouse_(); + + this.displayItemInfo_ = + Polymer.dom(this).querySelector('display-item-info'); + this.displayItemInfo_.addEventListener( + 'click', this.onDisplayItemInfoClick_.bind(this), false); + + this.displayItemListView_ = new tr.ui.b.ListView(); + this.displayItemListView_.addEventListener('selection-changed', + this.onDisplayItemListSelection_.bind(this)); + Polymer.dom(this.displayItemInfo_).appendChild(this.displayItemListView_); + + this.displayListFilename_ = + Polymer.dom(this).querySelector('.dlfilename'); + this.displayListExportButton_ = + Polymer.dom(this).querySelector('.dlexport'); + this.displayListExportButton_.addEventListener( + 'click', this.onExportDisplayListClicked_.bind(this)); + + this.skpFilename_ = Polymer.dom(this).querySelector('.skpfilename'); + this.skpExportButton_ = Polymer.dom(this).querySelector('.skpexport'); + this.skpExportButton_.addEventListener( + 'click', this.onExportSkPictureClicked_.bind(this)); + + const leftPanel = Polymer.dom(this).querySelector('left-panel'); + leftPanel.style.flexGrow = 0; + leftPanel.style.flexShrink = 0; + leftPanel.style.flexBasis = 'auto'; + leftPanel.style.minWidth = '200px'; + leftPanel.style.overflow = 'auto'; + + leftPanel.children[0].paddingTop = '2px'; + leftPanel.children[0].children[0].style.borderBottom = '1px solid #555'; + + const leftPanelTitle = leftPanel.querySelector('.title'); + leftPanelTitle.style.fontWeight = 'bold'; + leftPanelTitle.style.marginLeft = '5px'; + leftPanelTitle.style.marginright = '5px'; + + for (const div of leftPanel.querySelectorAll('.export')) { + div.style.margin = '5px'; + } + + const middleDragHandle = document.createElement('tr-ui-b-drag-handle'); + middleDragHandle.style.flexGrow = 0; + middleDragHandle.style.flexShrink = 0; + middleDragHandle.style.flexBasis = 'auto'; + middleDragHandle.horizontal = false; + middleDragHandle.target = leftPanel; + + const rightPanel = Polymer.dom(this).querySelector('right-panel'); + rightPanel.style.display = 'flex'; + rightPanel.style.flexGrow = 1; + rightPanel.style.flexShrink = 1; + rightPanel.style.flexBasis = 'auto'; + rightPanel.style.minWidth = 0; + + this.infoBar_ = document.createElement('tr-ui-b-info-bar'); + Polymer.dom(this.rasterArea_).insertBefore(this.infoBar_, canvasScroller); + + Polymer.dom(this).insertBefore(middleDragHandle, rightPanel); + + this.picture_ = undefined; + + this.pictureOpsListView_ = new tr.ui.e.chrome.cc.PictureOpsListView(); + this.pictureOpsListView_.style.flexGrow = 0; + this.pictureOpsListView_.style.flexShrink = 0; + this.pictureOpsListView_.style.flexBasis = 'auto'; + this.pictureOpsListView_.style.overflow = 'auto'; + this.pictureOpsListView_.style.minWidth = '100px'; + Polymer.dom(rightPanel).insertBefore( + this.pictureOpsListView_, this.rasterArea_); + + this.pictureOpsListDragHandle_ = + document.createElement('tr-ui-b-drag-handle'); + this.pictureOpsListDragHandle_.horizontal = false; + this.pictureOpsListDragHandle_.target = this.pictureOpsListView_; + Polymer.dom(rightPanel).insertBefore( + this.pictureOpsListDragHandle_, this.rasterArea_); + }, + + get picture() { + return this.picture_; + }, + + set displayItemList(displayItemList) { + this.displayItemList_ = displayItemList; + this.picture = this.displayItemList_; + + this.displayItemListView_.clear(); + this.displayItemList_.items.forEach(function(item) { + const listItem = document.createElement( + 'tr-ui-e-chrome-cc-display-item-list-item'); + listItem.data = item; + Polymer.dom(this.displayItemListView_).appendChild(listItem); + }.bind(this)); + }, + + set picture(picture) { + this.picture_ = picture; + + // Hide the ops list if we are showing the "main" display item list. + const showOpsList = picture && picture !== this.displayItemList_; + this.updateDrawOpsList_(showOpsList); + + if (picture) { + const size = this.getRasterCanvasSize_(); + this.rasterCanvas_.width = size.width; + this.rasterCanvas_.height = size.height; + } + + const bounds = this.rasterArea_.getBoundingClientRect(); + const selectorBounds = this.mouseModeSelector_.getBoundingClientRect(); + this.mouseModeSelector_.pos = { + x: (bounds.right - selectorBounds.width - 10), + y: bounds.top + }; + + this.rasterize_(); + + this.scheduleUpdateContents_(); + }, + + getRasterCanvasSize_() { + const style = window.getComputedStyle(this.rasterArea_); + let width = parseInt(style.width); + let height = parseInt(style.height); + if (this.picture_) { + width = Math.max(width, this.picture_.layerRect.width); + height = Math.max(height, this.picture_.layerRect.height); + } + + return { + width, + height + }; + }, + + scheduleUpdateContents_() { + if (this.updateContentsPending_) return; + + this.updateContentsPending_ = true; + tr.b.requestAnimationFrameInThisFrameIfPossible( + this.updateContents_.bind(this) + ); + }, + + updateContents_() { + this.updateContentsPending_ = false; + + if (this.picture_) { + Polymer.dom(this.sizeInfo_).textContent = '(' + + this.picture_.layerRect.width + ' x ' + + this.picture_.layerRect.height + ')'; + } + + // Return if picture hasn't finished rasterizing. + if (!this.pictureAsImageData_) return; + + this.infoBar_.visible = false; + this.infoBar_.removeAllButtons(); + if (this.pictureAsImageData_.error) { + this.infoBar_.message = 'Cannot rasterize...'; + this.infoBar_.addButton('More info...', function(e) { + const overlay = new tr.ui.b.Overlay(); + Polymer.dom(overlay).textContent = this.pictureAsImageData_.error; + overlay.visible = true; + e.stopPropagation(); + return false; + }.bind(this)); + this.infoBar_.visible = true; + } + + this.drawPicture_(); + }, + + drawPicture_() { + const size = this.getRasterCanvasSize_(); + if (size.width !== this.rasterCanvas_.width) { + this.rasterCanvas_.width = size.width; + } + if (size.height !== this.rasterCanvas_.height) { + this.rasterCanvas_.height = size.height; + } + + this.rasterCtx_.clearRect(0, 0, size.width, size.height); + + if (!this.picture_ || !this.pictureAsImageData_.imageData) return; + + const imgCanvas = this.pictureAsImageData_.asCanvas(); + const w = imgCanvas.width; + const h = imgCanvas.height; + this.rasterCtx_.drawImage(imgCanvas, 0, 0, w, h, + 0, 0, w * this.zoomScaleValue_, + h * this.zoomScaleValue_); + }, + + rasterize_() { + if (this.picture_) { + this.picture_.rasterize( + { + showOverdraw: false + }, + this.onRasterComplete_.bind(this)); + } + }, + + onRasterComplete_(pictureAsImageData) { + this.pictureAsImageData_ = pictureAsImageData; + this.scheduleUpdateContents_(); + }, + + onDisplayItemListSelection_(e) { + const selected = this.displayItemListView_.selectedElement; + + if (!selected) { + this.picture = this.displayItemList_; + return; + } + + const index = Array.prototype.indexOf.call( + this.displayItemListView_.children, selected); + const displayItem = this.displayItemList_.items[index]; + if (displayItem && displayItem.skp64) { + this.picture = new tr.e.cc.Picture( + displayItem.skp64, this.displayItemList_.layerRect); + } else { + this.picture = undefined; + } + }, + + onDisplayItemInfoClick_(e) { + if (e && e.target === this.displayItemInfo_) { + this.displayItemListView_.selectedElement = undefined; + } + }, + + updateDrawOpsList_(showOpsList) { + if (showOpsList) { + this.pictureOpsListView_.picture = this.picture_; + if (this.pictureOpsListView_.numOps > 0) { + this.pictureOpsListView_.style.display = 'block'; + this.pictureOpsListDragHandle_.style.display = 'block'; + } + } else { + this.pictureOpsListView_.style.display = 'none'; + this.pictureOpsListDragHandle_.style.display = 'none'; + } + }, + + trackMouse_() { + this.mouseModeSelector_ = document.createElement( + 'tr-ui-b-mouse-mode-selector'); + this.mouseModeSelector_.targetElement = this.rasterArea_; + Polymer.dom(this.rasterArea_).appendChild(this.mouseModeSelector_); + + this.mouseModeSelector_.supportedModeMask = + tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.defaultMode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.settingsKey = 'pictureDebugger.mouseModeSelector'; + + this.mouseModeSelector_.addEventListener('beginzoom', + this.onBeginZoom_.bind(this)); + this.mouseModeSelector_.addEventListener('updatezoom', + this.onUpdateZoom_.bind(this)); + this.mouseModeSelector_.addEventListener('endzoom', + this.onEndZoom_.bind(this)); + }, + + onBeginZoom_(e) { + this.isZooming_ = true; + + this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e); + + e.preventDefault(); + }, + + onUpdateZoom_(e) { + if (!this.isZooming_) return; + + const currentMouseViewPos = this.extractRelativeMousePosition_(e); + + // Take the distance the mouse has moved and we want to zoom at about + // 1/1000th of that speed. 0.01 feels jumpy. This could possibly be tuned + // more if people feel it's too slow. + this.zoomScaleValue_ += + ((this.lastMouseViewPos_.y - currentMouseViewPos.y) * 0.001); + this.zoomScaleValue_ = Math.max(this.zoomScaleValue_, 0.1); + + this.drawPicture_(); + + this.lastMouseViewPos_ = currentMouseViewPos; + }, + + onEndZoom_(e) { + this.lastMouseViewPos_ = undefined; + this.isZooming_ = false; + e.preventDefault(); + }, + + extractRelativeMousePosition_(e) { + return { + x: e.clientX - this.rasterArea_.offsetLeft, + y: e.clientY - this.rasterArea_.offsetTop + }; + }, + + saveFile_(filename, rawData) { + if (!rawData) return; + + // Convert this String into an Uint8Array + const length = rawData.length; + const arrayBuffer = new ArrayBuffer(length); + const uint8Array = new Uint8Array(arrayBuffer); + for (let c = 0; c < length; c++) { + uint8Array[c] = rawData.charCodeAt(c); + } + + // Create a blob URL from the binary array. + const blob = new Blob([uint8Array], {type: 'application/octet-binary'}); + const blobUrl = window.URL.createObjectURL(blob); + + // Create a link and click on it. + const link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); + link.href = blobUrl; + link.download = filename; + const event = document.createEvent('MouseEvents'); + event.initMouseEvent( + 'click', true, false, window, 0, 0, 0, 0, 0, + false, false, false, false, 0, null); + link.dispatchEvent(event); + }, + + onExportDisplayListClicked_() { + const rawData = JSON.stringify(this.displayItemList_.items); + this.saveFile_(this.displayListFilename_.value, rawData); + }, + + onExportSkPictureClicked_() { + const rawData = tr.b.Base64.atob(this.picture_.getBase64SkpData()); + this.saveFile_(this.skpFilename_.value, rawData); + } + }; + + return { + DisplayItemDebugger, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html new file mode 100644 index 00000000000..c10d6995db3 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_debugger_test.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/extras/chrome/cc/display_item_list.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_debugger.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const displayItemList = new tr.e.cc.DisplayItemListSnapshot( + {id: '31415'}, + 10, + { + 'params': { + 'layer_rect': [-15, -15, 46, 833], + 'items': [ + 'BeginClipDisplayItem', + 'EndClipDisplayItem' + ] + }, + 'skp64': '[another skia picture in base64]'}); + displayItemList.preInitialize(); + displayItemList.initialize(); + + const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger(); + this.addHTMLOutput(dbg); + assert.isUndefined(dbg.displayItemList_); + assert.isUndefined(dbg.picture_); + dbg.displayItemList = displayItemList; + assert.isDefined(dbg.displayItemList_); + assert.isDefined(dbg.picture_); + assert.strictEqual(dbg.displayItemList_.items.length, 2); + dbg.style.border = '1px solid black'; + }); + + test('selections', function() { + const displayItemList = new tr.e.cc.DisplayItemListSnapshot( + {id: '31415'}, + 10, + { + 'params': { + 'layer_rect': [-15, -15, 46, 833], + 'items': [ + 'BeginClipDisplayItem', + 'TransformDisplayItem', + { + 'name': 'DrawingDisplayItem', + 'skp64': '[skia picture in base64]', + }, + 'EndTransformDisplayItem', + 'EndClipDisplayItem' + ] + }, + 'skp64': '[another skia picture in base64]'}); + displayItemList.preInitialize(); + displayItemList.initialize(); + + const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger(); + this.addHTMLOutput(dbg); + dbg.displayItemList = displayItemList; + assert.isDefined(dbg.displayItemList_); + assert.isDefined(dbg.picture_); + assert.strictEqual(dbg.displayItemList_.items.length, 5); + + const initialPicture = dbg.picture_; + assert.isAbove(initialPicture.guid, 0); + + // Select the drawing display item and make sure the picture updates. + const listView = dbg.displayItemListView_; + listView.selectedElement = listView.getElementByIndex(3); + let updatedPicture = dbg.picture_; + assert.isAbove(updatedPicture.guid, 0); + assert.notEqual(initialPicture.guid, updatedPicture.guid); + + // Select the TransformDisplayItem and make sure the picture is blank. + listView.selectedElement = listView.getElementByIndex(2); + assert.isUndefined(dbg.picture_); + + // Deselect a list item and make sure the picture is reset to the original. + listView.selectedElement = undefined; + updatedPicture = dbg.picture_; + assert.isAbove(updatedPicture.guid, 0); + assert.strictEqual(initialPicture.guid, updatedPicture.guid); + + dbg.style.border = '1px solid black'; + }); + + test('export', function() { + const displayItemList = new tr.e.cc.DisplayItemListSnapshot( + {id: '31415'}, + 10, + { + 'params': { + 'layer_rect': [-15, -15, 46, 833], + 'items': [ + 'BeginClipDisplayItem', + 'EndClipDisplayItem' + ] + }, + 'skp64': 'c2twaWN0dXJl'}); + displayItemList.preInitialize(); + displayItemList.initialize(); + + const dbg = new tr.ui.e.chrome.cc.DisplayItemDebugger(); + this.addHTMLOutput(dbg); + dbg.displayItemList = displayItemList; + + let onSaveDisplayListCalled = false; + dbg.saveFile_ = function(filename, rawData) { + onSaveDisplayListCalled = true; + assert.strictEqual(filename, 'displayitemlist.json'); + assert.strictEqual( + rawData, '["BeginClipDisplayItem","EndClipDisplayItem"]'); + }; + dbg.onExportDisplayListClicked_(); + assert(onSaveDisplayListCalled); + + let onSaveSkPictureCalled = false; + dbg.saveFile_ = function(filename, rawData) { + onSaveSkPictureCalled = true; + assert.strictEqual(filename, 'skpicture.skp'); + assert.strictEqual(rawData, 'skpicture'); + }; + dbg.onExportSkPictureClicked_(); + assert(onSaveSkPictureCalled); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html new file mode 100644 index 00000000000..3024e8d2d22 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_item.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<!-- +An element displaying basic information about a display item in a list view. +--> +<dom-module id='tr-ui-e-chrome-cc-display-item-list-item'> + <template> + <style> + :host { + border-bottom: 1px solid #555; + display: block; + font-size: 12px; + padding: 3px 5px; + } + + :host(:hover) { + background-color: #f0f0f0; + cursor: pointer; + } + + .header { + font-weight: bold; + margin: 2px 0; + } + + .header > .extra { + background-color: #777; + border-radius: 4px; + color: white; + margin: 0 6px; + text-decoration: none; + padding: 2px 4px; + } + + .raw-details { + white-space: pre-wrap; + } + + .details > dl { + margin: 0; + } + + :host(:not([selected])) .details { + display: none; + } + </style> + <div class="header"> + {{name}} + <template is="dom-if" if="{{_computeIfSKP(richDetails)}}"> + <a class="extra" href$="{{_computeHref(richDetails)}}" + download="drawing.skp" on-click="{{stopPropagation}}">SKP</a> + </template> + </div> + <div class="details"> + <template is="dom-if" if="{{rawDetails}}"> + <div class="raw-details">{{rawDetails}}</div> + </template> + <template is="dom-if" if="{{richDetails}}"> + <dl> + <template is="dom-if" if="{{richDetails.visualRect}}"> + <dt>Visual rect</dt> + <dd>{{richDetails.visualRect.x}},{{richDetails.visualRect.y}} + {{richDetails.visualRect.width}}×{{richDetails.visualRect.height}} + </dd> + </template> + </dl> + </template> + </div> + </template> +<script> +'use strict'; +(function() { + // Extracts the "type" and "details" parts of the unstructured (plaintext) + // display item format, even if the details span multiple lines. + // For example, given "FooDisplayItem type=hello\nworld", produces + // "FooDisplayItem" as the first capture and "type=hello\nworld" as the + // second. Either capture could be the empty string, but this regex will + // still successfully match. + const DETAILS_SPLIT_REGEX = /^(\S*)\s*([\S\s]*)$/; + + Polymer({ + is: 'tr-ui-e-chrome-cc-display-item-list-item', + + created() { + // TODO(charliea): Why is setAttribute necessary here but not below? We + // should reach out to the Polymer team to figure out. + Polymer.dom(this).setAttribute('name', ''); + Polymer.dom(this).setAttribute('rawDetails', ''); + Polymer.dom(this).setAttribute('richDetails', undefined); + Polymer.dom(this).setAttribute('data_', undefined); + }, + + get data() { + return this.data_; + }, + + set data(data) { + this.data_ = data; + + if (!data) { + this.name = 'DATA MISSING'; + this.rawDetails = ''; + this.richDetails = undefined; + } else if (typeof data === 'string') { + const match = data.match(DETAILS_SPLIT_REGEX); + this.name = match[1]; + this.rawDetails = match[2]; + this.richDetails = undefined; + } else { + this.name = data.name; + this.rawDetails = ''; + this.richDetails = data; + } + }, + + stopPropagation(e) { + e.stopPropagation(); + }, + + _computeIfSKP(richDetails) { + return richDetails && richDetails.skp64; + }, + + _computeHref(richDetails) { + return 'data:application/octet-stream;base64,' + richDetails.skp64; + } + }); +})(); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html new file mode 100644 index 00000000000..97598aaf3a7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/display_item_list_view.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/extras/chrome/cc/display_item_list.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/display_item_debugger.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + /* + * Displays a display item snapshot in a human readable form. + * @constructor + */ + const DisplayItemSnapshotView = tr.ui.b.define( + 'tr-ui-e-chrome-cc-display-item-list-view', + tr.ui.analysis.ObjectSnapshotView); + + DisplayItemSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + this.style.display = 'flex'; + this.style.flexGrow = 1; + this.style.flexShrink = 1; + this.style.flexBasis = 'auto'; + this.style.minWidth = 0; + this.displayItemDebugger_ = new tr.ui.e.chrome.cc.DisplayItemDebugger(); + this.displayItemDebugger_.style.flexGrow = 1; + this.displayItemDebugger_.style.flexShrink = 1; + this.displayItemDebugger_.style.flexBasis = 'auto'; + this.displayItemDebugger_.style.minWidth = 0; + Polymer.dom(this).appendChild(this.displayItemDebugger_); + }, + + updateContents() { + if (this.objectSnapshot_ && this.displayItemDebugger_) { + this.displayItemDebugger_.displayItemList = this.objectSnapshot_; + } + } + }; + + tr.ui.analysis.ObjectSnapshotView.register( + DisplayItemSnapshotView, + { + typeNames: ['cc::DisplayItemList'], + showInstances: false + }); + + return { + DisplayItemSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png Binary files differnew file mode 100644 index 00000000000..a2b7710d3c4 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.png diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg new file mode 100644 index 00000000000..00531ac68d7 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/images/input-event.svg @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="744.09448819" + height="1052.3622047" + id="svg2" + version="1.1" + inkscape:version="0.48.4 r9939" + sodipodi:docname="New document 1"> + <defs + id="defs4"> + <filter + inkscape:collect="always" + id="filter3791"> + <feGaussianBlur + inkscape:collect="always" + stdDeviation="2.7246316" + id="feGaussianBlur3793" /> + </filter> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="2.8" + inkscape:cx="195.13782" + inkscape:cy="982.30556" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:window-width="1215" + inkscape:window-height="860" + inkscape:window-x="2219" + inkscape:window-y="113" + inkscape:window-maximized="0" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <g + id="g3882" + style="opacity:0.5" + inkscape:export-filename="/tmp/input-event.png" + inkscape:export-xdpi="82.07" + inkscape:export-ydpi="82.07"> + <path + transform="matrix(1.0152631,0,0,1.0152631,-0.71357503,0.46150497)" + sodipodi:type="arc" + style="opacity:0.50934604000000006;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter3791);enable-background:accumulate" + id="path3755" + sodipodi:cx="177.78685" + sodipodi:cy="100.79848" + sodipodi:rx="42.426407" + sodipodi:ry="42.426407" + d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z" /> + <path + transform="translate(-2,-2)" + d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z" + sodipodi:ry="42.426407" + sodipodi:rx="42.426407" + sodipodi:cy="100.79848" + sodipodi:cx="177.78685" + id="path2985" + style="color:#000000;fill:#d4d4d4;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + sodipodi:type="arc" /> + <path + inkscape:connector-curvature="0" + id="path3853" + d="m 175.28125,96.03125 0,8.46875 1,0 0,-8.46875 -1,0 z" + style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" /> + <path + inkscape:connector-curvature="0" + id="path3859" + d="m 171.53125,99.75 0,1 8.46875,0 0,-1 -8.46875,0 z" + style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" /> + </g> + <path + transform="matrix(1.2923213,0,0,1.2923213,-53.970887,-31.465544)" + d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z" + sodipodi:ry="42.426407" + sodipodi:rx="42.426407" + sodipodi:cy="100.79848" + sodipodi:cx="177.78685" + id="path3867" + style="color:#000000;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + sodipodi:type="arc" + inkscape:export-filename="/tmp/input-event.png" + inkscape:export-xdpi="82.07" + inkscape:export-ydpi="82.07" /> + </g> +</svg> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html new file mode 100644 index 00000000000..9f81199e358 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_picker.html @@ -0,0 +1,336 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/extras/chrome/cc/constants.html"> +<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html"> +<link rel="import" href="/tracing/extras/chrome/cc/util.html"> +<link rel="import" href="/tracing/model/event.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/base/list_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const constants = tr.e.cc.constants; + const RENDER_PASS_QUADS = + Math.max(constants.ACTIVE_TREE, constants.PENDING_TREE) + 1; + + /** + * @constructor + */ + const LayerPicker = tr.ui.b.define('tr-ui-e-chrome-cc-layer-picker'); + + LayerPicker.prototype = { + __proto__: HTMLUnknownElement.prototype, + + decorate() { + this.lthi_ = undefined; + this.controls_ = document.createElement('top-controls'); + this.renderPassQuads_ = false; + + this.style.display = 'flex'; + this.style.flexDirection = 'column'; + this.controls_.style.flexGrow = 0; + this.controls_.style.flexShrink = 0; + this.controls_.style.flexBasis = 'auto'; + this.controls_.style.backgroundImage = + '-webkit-gradient(linear, 0 0, 100% 0, from(#E5E5E5), to(#D1D1D1))'; + this.controls_.style.borderBottom = '1px solid #8e8e8e'; + this.controls_.style.borderTop = '1px solid white'; + this.controls_.style.display = 'inline'; + this.controls_.style.fontSize = '14px'; + this.controls_.style.paddingLeft = '2px'; + + this.itemList_ = new tr.ui.b.ListView(); + this.itemList_.style.flexGrow = 1; + this.itemList_.style.flexShrink = 1; + this.itemList_.style.flexBasis = 'auto'; + this.itemList_.style.fontFamily = 'monospace'; + this.itemList_.style.overflow = 'auto'; + Polymer.dom(this).appendChild(this.controls_); + + Polymer.dom(this).appendChild(this.itemList_); + + this.itemList_.addEventListener( + 'selection-changed', this.onItemSelectionChanged_.bind(this)); + + Polymer.dom(this.controls_).appendChild(tr.ui.b.createSelector( + this, 'whichTree', + 'layerPicker.whichTree', constants.ACTIVE_TREE, + [{label: 'Active tree', value: constants.ACTIVE_TREE}, + {label: 'Pending tree', value: constants.PENDING_TREE}, + {label: 'Render pass quads', value: RENDER_PASS_QUADS}])); + + this.showPureTransformLayers_ = false; + const showPureTransformLayers = tr.ui.b.createCheckBox( + this, 'showPureTransformLayers', + 'layerPicker.showPureTransformLayers', false, + 'Transform layers'); + Polymer.dom(showPureTransformLayers).classList.add( + 'show-transform-layers'); + showPureTransformLayers.title = + 'When checked, pure transform layers are shown'; + Polymer.dom(this.controls_).appendChild(showPureTransformLayers); + }, + + get lthiSnapshot() { + return this.lthiSnapshot_; + }, + + set lthiSnapshot(lthiSnapshot) { + this.lthiSnapshot_ = lthiSnapshot; + this.updateContents_(); + }, + + get whichTree() { + return this.renderPassQuads_ ? constants.ACTIVE_TREE : this.whichTree_; + }, + + set whichTree(whichTree) { + this.whichTree_ = whichTree; + this.renderPassQuads_ = (whichTree === RENDER_PASS_QUADS); + this.updateContents_(); + tr.b.dispatchSimpleEvent(this, 'selection-change', false); + }, + + get layerTreeImpl() { + if (this.lthiSnapshot === undefined) return undefined; + + return this.lthiSnapshot.getTree(this.whichTree); + }, + + get isRenderPassQuads() { + return this.renderPassQuads_; + }, + + get showPureTransformLayers() { + return this.showPureTransformLayers_; + }, + + set showPureTransformLayers(show) { + if (this.showPureTransformLayers_ === show) return; + + this.showPureTransformLayers_ = show; + this.updateContents_(); + }, + + getRenderPassInfos_() { + if (!this.lthiSnapshot_) return []; + + const renderPassInfo = []; + if (!this.lthiSnapshot_.args.frame || + !this.lthiSnapshot_.args.frame.renderPasses) { + return renderPassInfo; + } + + const renderPasses = this.lthiSnapshot_.args.frame.renderPasses; + for (let i = 0; i < renderPasses.length; ++i) { + const info = {renderPass: renderPasses[i], + depth: 0, + id: i, + name: 'cc::RenderPass'}; + renderPassInfo.push(info); + } + return renderPassInfo; + }, + + getLayerInfos_() { + if (!this.lthiSnapshot_) return []; + + const tree = this.lthiSnapshot_.getTree(this.whichTree_); + if (!tree) return []; + + const layerInfos = []; + + const showPureTransformLayers = this.showPureTransformLayers_; + + const visitedLayers = {}; + function visitLayer(layer, depth, isMask, isReplica) { + if (visitedLayers[layer.layerId]) return; + + visitedLayers[layer.layerId] = true; + const info = {layer, + depth}; + + if (layer.args.drawsContent) { + info.name = layer.objectInstance.name; + } else { + info.name = 'cc::LayerImpl'; + } + + if (layer.usingGpuRasterization) { + info.name += ' (G)'; + } + + info.isMaskLayer = isMask; + info.replicaLayer = isReplica; + + if (showPureTransformLayers || layer.args.drawsContent) { + layerInfos.push(info); + } + } + tree.iterLayers(visitLayer); + return layerInfos; + }, + + updateContents_() { + if (this.renderPassQuads_) { + this.updateRenderPassContents_(); + } else { + this.updateLayerContents_(); + } + }, + + updateRenderPassContents_() { + this.itemList_.clear(); + + let selectedRenderPassId; + if (this.selection_ && this.selection_.associatedRenderPassId) { + selectedRenderPassId = this.selection_.associatedRenderPassId; + } + + const renderPassInfos = this.getRenderPassInfos_(); + renderPassInfos.forEach(function(renderPassInfo) { + const renderPass = renderPassInfo.renderPass; + const id = renderPassInfo.id; + + const item = this.createElementWithDepth_(renderPassInfo.depth); + const labelEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan()); + + Polymer.dom(labelEl).textContent = renderPassInfo.name + ' ' + id; + item.renderPass = renderPass; + item.renderPassId = id; + Polymer.dom(this.itemList_).appendChild(item); + + if (id === selectedRenderPassId) { + renderPass.selectionState = + tr.model.SelectionState.SELECTED; + } + }, this); + }, + + updateLayerContents_() { + this.changingItemSelection_ = true; + try { + this.itemList_.clear(); + + let selectedLayerId; + if (this.selection_ && this.selection_.associatedLayerId) { + selectedLayerId = this.selection_.associatedLayerId; + } + + const layerInfos = this.getLayerInfos_(); + layerInfos.forEach(function(layerInfo) { + const layer = layerInfo.layer; + const id = layer.layerId; + + const item = this.createElementWithDepth_(layerInfo.depth); + const labelEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan()); + + Polymer.dom(labelEl).textContent = layerInfo.name + ' ' + id; + + const notesEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan()); + if (layerInfo.isMaskLayer) { + Polymer.dom(notesEl).textContent += '(mask)'; + } + if (layerInfo.isReplicaLayer) { + Polymer.dom(notesEl).textContent += '(replica)'; + } + + if ((layer.gpuMemoryUsageInBytes !== undefined) && + (layer.gpuMemoryUsageInBytes > 0)) { + const gpuUsageStr = tr.b.Unit.byName.sizeInBytes.format( + layer.gpuMemoryUsageInBytes); + Polymer.dom(notesEl).textContent += ' (' + gpuUsageStr + ' MiB)'; + } + + item.layer = layer; + Polymer.dom(this.itemList_).appendChild(item); + + if (layer.layerId === selectedLayerId) { + layer.selectionState = tr.model.SelectionState.SELECTED; + item.selected = true; + } + }, this); + } finally { + this.changingItemSelection_ = false; + } + }, + + createElementWithDepth_(depth) { + const item = document.createElement('div'); + + const indentEl = Polymer.dom(item).appendChild(tr.ui.b.createSpan()); + indentEl.style.whiteSpace = 'pre'; + for (let i = 0; i < depth; i++) { + Polymer.dom(indentEl).textContent = + Polymer.dom(indentEl).textContent + ' '; + } + return item; + }, + + onItemSelectionChanged_(e) { + if (this.changingItemSelection_) return; + if (this.renderPassQuads_) { + this.onRenderPassSelected_(e); + } else { + this.onLayerSelected_(e); + } + tr.b.dispatchSimpleEvent(this, 'selection-change', false); + }, + + onRenderPassSelected_(e) { + let selectedRenderPass; + let selectedRenderPassId; + if (this.itemList_.selectedElement) { + selectedRenderPass = this.itemList_.selectedElement.renderPass; + selectedRenderPassId = + this.itemList_.selectedElement.renderPassId; + } + + if (selectedRenderPass) { + this.selection_ = new tr.ui.e.chrome.cc.RenderPassSelection( + selectedRenderPass, selectedRenderPassId); + } else { + this.selection_ = undefined; + } + }, + + onLayerSelected_(e) { + let selectedLayer; + if (this.itemList_.selectedElement) { + selectedLayer = this.itemList_.selectedElement.layer; + } + + if (selectedLayer) { + this.selection_ = new tr.ui.e.chrome.cc.LayerSelection(selectedLayer); + } else { + this.selection_ = undefined; + } + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + if (this.selection_ === selection) return; + this.selection_ = selection; + this.updateContents_(); + } + }; + + return { + LayerPicker, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html new file mode 100644 index 00000000000..1aaee9d7fbc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html @@ -0,0 +1,142 @@ +<!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/extras/chrome/cc/layer_tree_host_impl.html"> +<link rel="import" href="/tracing/extras/chrome/cc/tile.html"> +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_picker.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + /* + * Displays a LayerTreeHostImpl snapshot in a human readable form. + * @constructor + */ + const LayerTreeHostImplSnapshotView = tr.ui.b.define( + 'tr-ui-e-chrome-cc-layer-tree-host-impl-snapshot-view', + tr.ui.analysis.ObjectSnapshotView); + + LayerTreeHostImplSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + Polymer.dom(this).classList.add('tr-ui-e-chrome-cc-lthi-s-view'); + this.style.display = 'flex'; + this.style.flexDirection = 'row'; + this.style.flexGrow = 1; + this.style.flexShrink = 1; + this.style.flexBasis = 'auto'; + this.style.minWidth = 0; + + this.selection_ = undefined; + + this.layerPicker_ = new tr.ui.e.chrome.cc.LayerPicker(); + this.layerPicker_.style.flexGrow = 0; + this.layerPicker_.style.flexShrink = 0; + this.layerPicker_.style.flexBasis = 'auto'; + this.layerPicker_.style.minWidth = '200px'; + this.layerPicker_.addEventListener( + 'selection-change', + this.onLayerPickerSelectionChanged_.bind(this)); + + this.layerView_ = new tr.ui.e.chrome.cc.LayerView(); + this.layerView_.addEventListener( + 'selection-change', + this.onLayerViewSelectionChanged_.bind(this)); + this.layerView_.style.flexGrow = 1; + this.layerView_.style.flexShrink = 1; + this.layerView_.style.flexBasis = 'auto'; + this.layerView_.style.minWidth = 0; + + this.dragHandle_ = document.createElement('tr-ui-b-drag-handle'); + this.dragHandle_.style.flexGrow = 0; + this.dragHandle_.style.flexShrink = 0; + this.dragHandle_.style.flexBasis = 'auto'; + this.dragHandle_.horizontal = false; + this.dragHandle_.target = this.layerPicker_; + + Polymer.dom(this).appendChild(this.layerPicker_); + Polymer.dom(this).appendChild(this.dragHandle_); + Polymer.dom(this).appendChild(this.layerView_); + + // Make sure we have the current values from layerView_ and layerPicker_, + // since those might have been created before we added the listener. + this.onLayerViewSelectionChanged_(); + this.onLayerPickerSelectionChanged_(); + }, + + get objectSnapshot() { + return this.objectSnapshot_; + }, + + set objectSnapshot(objectSnapshot) { + this.objectSnapshot_ = objectSnapshot; + + const lthi = this.objectSnapshot; + let layerTreeImpl; + if (lthi) { + layerTreeImpl = lthi.getTree(this.layerPicker_.whichTree); + } + + this.layerPicker_.lthiSnapshot = lthi; + this.layerView_.layerTreeImpl = layerTreeImpl; + this.layerView_.regenerateContent(); + + if (!this.selection_) return; + + this.selection = this.selection_.findEquivalent(lthi); + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + if (this.selection_ === selection) return; + + this.selection_ = selection; + this.layerPicker_.selection = selection; + this.layerView_.selection = selection; + tr.b.dispatchSimpleEvent(this, 'cc-selection-change'); + }, + + onLayerPickerSelectionChanged_() { + this.selection_ = this.layerPicker_.selection; + this.layerView_.selection = this.selection; + this.layerView_.layerTreeImpl = this.layerPicker_.layerTreeImpl; + this.layerView_.isRenderPassQuads = this.layerPicker_.isRenderPassQuads; + this.layerView_.regenerateContent(); + tr.b.dispatchSimpleEvent(this, 'cc-selection-change'); + }, + + onLayerViewSelectionChanged_() { + this.selection_ = this.layerView_.selection; + this.layerPicker_.selection = this.selection; + tr.b.dispatchSimpleEvent(this, 'cc-selection-change'); + }, + + get extraHighlightsByLayerId() { + return this.layerView_.extraHighlightsByLayerId; + }, + + set extraHighlightsByLayerId(extraHighlightsByLayerId) { + this.layerView_.extraHighlightsByLayerId = extraHighlightsByLayerId; + } + }; + + tr.ui.analysis.ObjectSnapshotView.register( + LayerTreeHostImplSnapshotView, {typeName: 'cc::LayerTreeHostImpl'}); + + return { + LayerTreeHostImplSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html new file mode 100644 index 00000000000..1831be24618 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view_test.html @@ -0,0 +1,37 @@ +<!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/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/cc/layer_tree_host_impl.html"> +<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = Object.values(m.processes)[0]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const snapshot = instance.snapshots[0]; + + const view = new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView(); + view.style.width = '900px'; + view.style.height = '400px'; + view.objectSnapshot = snapshot; + + this.addHTMLOutput(view); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html new file mode 100644 index 00000000000..2a7e5666f8b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html @@ -0,0 +1,1200 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/color.html"> +<link rel="import" href="/tracing/base/math/quad.html"> +<link rel="import" href="/tracing/base/math/range.html"> +<link rel="import" href="/tracing/base/raf.html"> +<link rel="import" href="/tracing/base/unit_scale.html"> +<link rel="import" href="/tracing/extras/chrome/cc/debug_colors.html"> +<link rel="import" href="/tracing/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/extras/chrome/cc/render_pass.html"> +<link rel="import" href="/tracing/extras/chrome/cc/tile.html"> +<link rel="import" href="/tracing/extras/chrome/cc/util.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_link.html"> +<link rel="import" href="/tracing/ui/base/info_bar.html"> +<link rel="import" href="/tracing/ui/base/quad_stack_view.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> + +<template id='tr-ui-e-chrome-cc-layer-tree-quad-stack-view-template'> + <style> + #input-event { + background-image: url('./images/input-event.png'); + display: none; + } + </style> + <img id='input-event'/> +</template> + +<script> +'use strict'; + +/** + * @fileoverview Graphical view of LayerTreeImpl, with controls for + * type of layer content shown and info bar for content-loading warnings. + */ +tr.exportTo('tr.ui.e.chrome.cc', function() { + const ColorScheme = tr.b.ColorScheme; + + const THIS_DOC = document.currentScript.ownerDocument; + const TILE_HEATMAP_TYPE = {}; + TILE_HEATMAP_TYPE.NONE = 'none'; + TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY = 'scheduledPriority'; + TILE_HEATMAP_TYPE.USING_GPU_MEMORY = 'usingGpuMemory'; + + const cc = tr.ui.e.chrome.cc; + + function createTileRectsSelectorBaseOptions() { + return [{label: 'None', value: 'none'}, + {label: 'Coverage Rects', value: 'coverage'}]; + } + + + /** + * @constructor + */ + const LayerTreeQuadStackView = + tr.ui.b.define('tr-ui-e-chrome-cc-layer-tree-quad-stack-view'); + + LayerTreeQuadStackView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.style.flexGrow = 1; + this.style.flexShrink = 1; + this.style.flexBasis = 'auto'; + this.style.flexDirection = 'column'; + this.style.minHeight = 0; + this.style.display = 'flex'; + + this.isRenderPassQuads_ = false; + this.pictureAsImageData_ = {}; // Maps picture.guid to PictureAsImageData. + this.messages_ = []; + this.controls_ = document.createElement('top-controls'); + this.controls_.style.flexGrow = 0; + this.controls_.style.flexShrink = 0; + this.controls_.style.flexBasis = 'auto'; + this.controls_.style.backgroundImage = + '-webkit-gradient(linear, 0 0, 100% 0, from(#E5E5E5), to(#D1D1D1))'; + this.controls_.style.borderBottom = '1px solid #8e8e8e'; + this.controls_.style.borderTop = '1px solid white'; + this.controls_.style.display = 'flex'; + this.controls_.style.flexDirection = 'row'; + this.controls_.style.flexWrap = 'wrap'; + this.controls_.style.fontSize = '14px'; + this.controls_.style.paddingLeft = '2px'; + this.controls_.style.overflow = 'hidden'; + this.infoBar_ = document.createElement('tr-ui-b-info-bar'); + this.quadStackView_ = new tr.ui.b.QuadStackView(); + this.quadStackView_.addEventListener( + 'selectionchange', this.onQuadStackViewSelectionChange_.bind(this)); + this.quadStackView_.style.flexGrow = 1; + this.quadStackView_.style.flexShrink = 1; + this.quadStackView_.style.flexBasis = 'auto'; + this.quadStackView_.style.minWidth = '200px'; + + this.extraHighlightsByLayerId_ = undefined; + this.inputEventImageData_ = undefined; + + const m = tr.ui.b.MOUSE_SELECTOR_MODE; + const mms = this.quadStackView_.mouseModeSelector; + mms.settingsKey = 'tr.e.cc.layerTreeQuadStackView.mouseModeSelector'; + mms.setKeyCodeForMode(m.SELECTION, 'Z'.charCodeAt(0)); + mms.setKeyCodeForMode(m.PANSCAN, 'X'.charCodeAt(0)); + mms.setKeyCodeForMode(m.ZOOM, 'C'.charCodeAt(0)); + mms.setKeyCodeForMode(m.ROTATE, 'V'.charCodeAt(0)); + + const node = tr.ui.b.instantiateTemplate( + '#tr-ui-e-chrome-cc-layer-tree-quad-stack-view-template', THIS_DOC); + Polymer.dom(this).appendChild(node); + Polymer.dom(this).appendChild(this.controls_); + Polymer.dom(this).appendChild(this.infoBar_); + Polymer.dom(this).appendChild(this.quadStackView_); + + this.tileRectsSelector_ = tr.ui.b.createSelector( + this, 'howToShowTiles', + 'layerView.howToShowTiles', 'none', + createTileRectsSelectorBaseOptions()); + Polymer.dom(this.controls_).appendChild(this.tileRectsSelector_); + + const tileHeatmapText = tr.ui.b.createSpan({ + textContent: 'Tile heatmap:' + }); + Polymer.dom(this.controls_).appendChild(tileHeatmapText); + + const tileHeatmapSelector = tr.ui.b.createSelector( + this, 'tileHeatmapType', + 'layerView.tileHeatmapType', TILE_HEATMAP_TYPE.NONE, + [{label: 'None', + value: TILE_HEATMAP_TYPE.NONE}, + {label: 'Scheduled Priority', + value: TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY}, + {label: 'Is using GPU memory', + value: TILE_HEATMAP_TYPE.USING_GPU_MEMORY} + ]); + Polymer.dom(this.controls_).appendChild(tileHeatmapSelector); + + const showOtherLayersCheckbox = tr.ui.b.createCheckBox( + this, 'showOtherLayers', + 'layerView.showOtherLayers', true, + 'Other layers/passes'); + showOtherLayersCheckbox.title = + 'When checked, show all layers, selected or not.'; + Polymer.dom(this.controls_).appendChild(showOtherLayersCheckbox); + + const showInvalidationsCheckbox = tr.ui.b.createCheckBox( + this, 'showInvalidations', + 'layerView.showInvalidations', true, + 'Invalidations'); + showInvalidationsCheckbox.title = + 'When checked, compositing invalidations are highlighted in red'; + Polymer.dom(this.controls_).appendChild(showInvalidationsCheckbox); + + const showUnrecordedRegionCheckbox = tr.ui.b.createCheckBox( + this, 'showUnrecordedRegion', + 'layerView.showUnrecordedRegion', true, + 'Unrecorded area'); + showUnrecordedRegionCheckbox.title = + 'When checked, unrecorded areas are highlighted in yellow'; + Polymer.dom(this.controls_).appendChild(showUnrecordedRegionCheckbox); + + const showBottlenecksCheckbox = tr.ui.b.createCheckBox( + this, 'showBottlenecks', + 'layerView.showBottlenecks', true, + 'Bottlenecks'); + showBottlenecksCheckbox.title = + 'When checked, scroll bottlenecks are highlighted'; + Polymer.dom(this.controls_).appendChild(showBottlenecksCheckbox); + + const showLayoutRectsCheckbox = tr.ui.b.createCheckBox( + this, 'showLayoutRects', + 'layerView.showLayoutRects', false, + 'Layout rects'); + showLayoutRectsCheckbox.title = + 'When checked, shows rects for regions where layout happened'; + Polymer.dom(this.controls_).appendChild(showLayoutRectsCheckbox); + + const showContentsCheckbox = tr.ui.b.createCheckBox( + this, 'showContents', + 'layerView.showContents', true, + 'Contents'); + showContentsCheckbox.title = + 'When checked, show the rendered contents inside the layer outlines'; + Polymer.dom(this.controls_).appendChild(showContentsCheckbox); + + const showAnimationBoundsCheckbox = tr.ui.b.createCheckBox( + this, 'showAnimationBounds', + 'layerView.showAnimationBounds', false, + 'Animation Bounds'); + showAnimationBoundsCheckbox.title = 'When checked, show a border around' + + ' a layer showing the extent of its animation.'; + Polymer.dom(this.controls_).appendChild(showAnimationBoundsCheckbox); + + const showInputEventsCheckbox = tr.ui.b.createCheckBox( + this, 'showInputEvents', + 'layerView.showInputEvents', true, + 'Input events'); + showInputEventsCheckbox.title = 'When checked, input events are ' + + 'displayed as circles.'; + Polymer.dom(this.controls_).appendChild(showInputEventsCheckbox); + + this.whatRasterizedLink_ = document.createElement( + 'tr-ui-a-analysis-link'); + this.whatRasterizedLink_.style.position = 'absolute'; + this.whatRasterizedLink_.style.bottom = '15px'; + this.whatRasterizedLink_.style.left = '10px'; + this.whatRasterizedLink_.selection = + this.getWhatRasterizedEventSet_.bind(this); + Polymer.dom(this.quadStackView_).appendChild(this.whatRasterizedLink_); + }, + + get layerTreeImpl() { + return this.layerTreeImpl_; + }, + + set isRenderPassQuads(newValue) { + this.isRenderPassQuads_ = newValue; + }, + + set layerTreeImpl(layerTreeImpl) { + if (this.layerTreeImpl_ === layerTreeImpl) return; + + // FIXME(pdr): We may want to clear pictureAsImageData_ here to save + // memory at the cost of performance. Note that + // pictureAsImageData_ will be cleared when this is + // destructed, but this view might live for several + // layerTreeImpls. + this.layerTreeImpl_ = layerTreeImpl; + this.selection = undefined; + }, + + get extraHighlightsByLayerId() { + return this.extraHighlightsByLayerId_; + }, + + set extraHighlightsByLayerId(extraHighlightsByLayerId) { + this.extraHighlightsByLayerId_ = extraHighlightsByLayerId; + this.scheduleUpdateContents_(); + }, + + get showOtherLayers() { + return this.showOtherLayers_; + }, + + set showOtherLayers(show) { + this.showOtherLayers_ = show; + this.updateContents_(); + }, + + get showAnimationBounds() { + return this.showAnimationBounds_; + }, + + set showAnimationBounds(show) { + this.showAnimationBounds_ = show; + this.updateContents_(); + }, + + get showInputEvents() { + return this.showInputEvents_; + }, + + set showInputEvents(show) { + this.showInputEvents_ = show; + this.updateContents_(); + }, + + get showContents() { + return this.showContents_; + }, + + set showContents(show) { + this.showContents_ = show; + this.updateContents_(); + }, + + get showInvalidations() { + return this.showInvalidations_; + }, + + set showInvalidations(show) { + this.showInvalidations_ = show; + this.updateContents_(); + }, + + get showUnrecordedRegion() { + return this.showUnrecordedRegion_; + }, + + set showUnrecordedRegion(show) { + this.showUnrecordedRegion_ = show; + this.updateContents_(); + }, + + get showBottlenecks() { + return this.showBottlenecks_; + }, + + set showBottlenecks(show) { + this.showBottlenecks_ = show; + this.updateContents_(); + }, + + get showLayoutRects() { + return this.showLayoutRects_; + }, + + set showLayoutRects(show) { + this.showLayoutRects_ = show; + this.updateContents_(); + }, + + get howToShowTiles() { + return this.howToShowTiles_; + }, + + set howToShowTiles(val) { + // Make sure val is something we expect. + if (val !== 'none' && val !== 'coverage' && isNaN(parseFloat(val))) { + throw new Error( + 'howToShowTiles requires "none" or "coverage" or a number'); + } + + this.howToShowTiles_ = val; + this.updateContents_(); + }, + + get tileHeatmapType() { + return this.tileHeatmapType_; + }, + + set tileHeatmapType(val) { + this.tileHeatmapType_ = val; + this.updateContents_(); + }, + + get selection() { + return this.selection_; + }, + + set selection(selection) { + if (this.selection === selection) return; + + this.selection_ = selection; + tr.b.dispatchSimpleEvent(this, 'selection-change'); + this.updateContents_(); + }, + + regenerateContent() { + this.updateTilesSelector_(); + this.updateContents_(); + }, + + loadDataForImageElement_(image, callback) { + const imageContent = window.getComputedStyle(image).backgroundImage; + if (!imageContent) { + // The style has not been applied because the view has not been added + // into the DOM tree yet. Try again in another cycle. + this.scheduleUpdateContents_(); + return; + } + image.src = tr.ui.b.extractUrlString(imageContent); + image.onload = function() { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = image.width; + canvas.height = image.height; + ctx.drawImage(image, 0, 0); + const imageData = ctx.getImageData( + 0, 0, canvas.width, canvas.height); + callback(imageData); + }; + }, + + onQuadStackViewSelectionChange_(e) { + const selectableQuads = e.quads.filter(function(q) { + return q.selectionToSetIfClicked !== undefined; + }); + if (selectableQuads.length === 0) { + this.selection = undefined; + return; + } + + // Sort the quads low to high on stackingGroupId. + selectableQuads.sort(function(x, y) { + const z = x.stackingGroupId - y.stackingGroupId; + if (z !== 0) return z; + + return x.selectionToSetIfClicked.specicifity - + y.selectionToSetIfClicked.specicifity; + }); + + // TODO(nduca): Support selecting N things at once. + const quadToSelect = selectableQuads[selectableQuads.length - 1]; + this.selection = quadToSelect.selectionToSetIfClicked; + }, + + scheduleUpdateContents_() { + if (this.updateContentsPending_) return; + + this.updateContentsPending_ = true; + tr.b.requestAnimationFrameInThisFrameIfPossible( + this.updateContents_, this); + }, + + updateContents_() { + if (!this.layerTreeImpl_) { + this.quadStackView_.headerText = 'No tree'; + this.quadStackView_.quads = []; + return; + } + + + const status = this.computePictureLoadingStatus_(); + if (!status.picturesComplete) return; + + const lthi = this.layerTreeImpl_.layerTreeHostImpl; + const lthiInstance = lthi.objectInstance; + const worldViewportRect = tr.b.math.Rect.fromXYWH( + 0, 0, + lthi.deviceViewportSize.width, lthi.deviceViewportSize.height); + this.quadStackView_.deviceRect = worldViewportRect; + if (this.isRenderPassQuads_) { + this.quadStackView_.quads = this.generateRenderPassQuads(); + } else { + this.quadStackView_.quads = this.generateLayerQuads(); + } + + this.updateWhatRasterizedLinkState_(); + + let message = ''; + if (lthi.tilesHaveGpuMemoryUsageInfo) { + const thisTreeUsageInBytes = this.layerTreeImpl_.gpuMemoryUsageInBytes; + const otherTreeUsageInBytes = lthi.gpuMemoryUsageInBytes - + thisTreeUsageInBytes; + message += + tr.b.convertUnit(thisTreeUsageInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + + ' MiB on this tree'; + if (otherTreeUsageInBytes) { + message += ', ' + + tr.b.convertUnit(otherTreeUsageInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + + ' MiB on the other tree'; + } + } else { + if (this.layerTreeImpl_) { + const thisTreeUsageInBytes = + this.layerTreeImpl_.gpuMemoryUsageInBytes; + message += + tr.b.convertUnit(thisTreeUsageInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + + ' MiB on this tree'; + + if (this.layerTreeImpl_.otherTree) { + // Older Chromes don't report enough data to know how much memory is + // being used across both trees. We know the memory consumed by each + // tree, but there is resource sharing *between the trees* so we + // can't simply sum up the per-tree costs. We need either the total + // plus one tree, to guess the unique on the other tree, etc. Newer + // chromes report memory per tile, which allows LTHI to compute the + // total tile memory usage, letting us figure things out properly. + message += ', ??? MiB on other tree. '; + } + } + } + + if (lthi.args.tileManagerBasicState) { + const tmgs = lthi.args.tileManagerBasicState.globalState; + message += ' (softMax=' + + tr.b.convertUnit(tmgs.softMemoryLimitInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + + ' MiB, hardMax=' + + tr.b.convertUnit(tmgs.hardMemoryLimitInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + ' MiB, ' + + tmgs.memoryLimitPolicy + ')'; + } else { + // Old Chromes do not have a globalState on the LTHI dump. + // But they do issue a DidManage event wiht the globalstate. Find that + // event so that we show some global state. + const thread = lthi.snapshottedOnThread; + const didManageTilesSlices = thread.sliceGroup.slices.filter(s => { + if (s.category !== 'tr.e.cc') return false; + + if (s.title !== 'DidManage') return false; + + if (s.end > lthi.ts) return false; + + return true; + }); + didManageTilesSlices.sort(function(x, y) { + return x.end - y.end; + }); + if (didManageTilesSlices.length > 0) { + const newest = didManageTilesSlices[didManageTilesSlices.length - 1]; + const tmgs = newest.args.state.global_state; + message += ' (softMax=' + + tr.b.convertUnit(tmgs.softMemoryLimitInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + + ' MiB, hardMax=' + + tr.b.convertUnit(tmgs.hardMemoryLimitInBytes, + tr.b.UnitPrefixScale.BINARY.NONE, + tr.b.UnitPrefixScale.BINARY.MEBI).toFixed(1) + ' MiB, ' + + tmgs.memoryLimitPolicy + ')'; + } + } + + if (this.layerTreeImpl_.otherTree) { + message += ' (Another tree exists)'; + } + + if (message.length) { + this.quadStackView_.headerText = message; + } else { + this.quadStackView_.headerText = undefined; + } + + this.updateInfoBar_(status.messages); + }, + + updateTilesSelector_() { + const data = createTileRectsSelectorBaseOptions(); + + if (this.layerTreeImpl_) { + // First get all of the scales information from LTHI. + const lthi = this.layerTreeImpl_.layerTreeHostImpl; + const scaleNames = lthi.getContentsScaleNames(); + for (const scale in scaleNames) { + data.push({ + label: 'Scale ' + scale + ' (' + scaleNames[scale] + ')', + value: scale + }); + } + } + + // Then create a new selector and replace the old one. + const newSelector = tr.ui.b.createSelector( + this, 'howToShowTiles', + 'layerView.howToShowTiles', 'none', + data); + this.controls_.replaceChild(newSelector, this.tileRectsSelector_); + this.tileRectsSelector_ = newSelector; + }, + + computePictureLoadingStatus_() { + // Figure out if we can draw the quads yet. While we're at it, figure out + // if we have any warnings we need to show. + const layers = this.layers; + const status = { + messages: [], + picturesComplete: true + }; + if (this.showContents) { + let hasPendingRasterizeImage = false; + let firstPictureError = undefined; + let hasMissingLayerRect = false; + let hasUnresolvedPictureRef = false; + for (let i = 0; i < layers.length; i++) { + const layer = layers[i]; + for (let ir = 0; ir < layer.pictures.length; ++ir) { + const picture = layer.pictures[ir]; + + if (picture.idRef) { + hasUnresolvedPictureRef = true; + continue; + } + if (!picture.layerRect) { + hasMissingLayerRect = true; + continue; + } + + const pictureAsImageData = this.pictureAsImageData_[picture.guid]; + if (!pictureAsImageData) { + hasPendingRasterizeImage = true; + this.pictureAsImageData_[picture.guid] = + tr.e.cc.PictureAsImageData.Pending(this); + picture.rasterize( + {stopIndex: undefined}, + function(pictureImageData) { + const picture_ = pictureImageData.picture; + this.pictureAsImageData_[picture_.guid] = pictureImageData; + this.scheduleUpdateContents_(); + }.bind(this)); + continue; + } + if (pictureAsImageData.isPending()) { + hasPendingRasterizeImage = true; + continue; + } + if (pictureAsImageData.error) { + if (!firstPictureError) { + firstPictureError = pictureAsImageData.error; + } + break; + } + } + } + if (hasPendingRasterizeImage) { + status.picturesComplete = false; + } else { + if (hasUnresolvedPictureRef) { + status.messages.push({ + header: 'Missing picture', + details: 'Your trace didn\'t have pictures for every layer. ' + + 'Old chrome versions had this problem'}); + } + if (hasMissingLayerRect) { + status.messages.push({ + header: 'Missing layer rect', + details: 'Your trace may be corrupt or from a very old ' + + 'Chrome revision.'}); + } + if (firstPictureError) { + status.messages.push({ + header: 'Cannot rasterize', + details: firstPictureError}); + } + } + } + if (this.showInputEvents && this.layerTreeImpl.tracedInputLatencies && + this.inputEventImageData_ === undefined) { + const image = Polymer.dom(this).querySelector('#input-event'); + if (!image.src) { + this.loadDataForImageElement_(image, function(imageData) { + this.inputEventImageData_ = imageData; + this.updateContentsPending_ = false; + this.scheduleUpdateContents_(); + }.bind(this)); + } + status.picturesComplete = false; + } + return status; + }, + + get selectedRenderPass() { + if (this.selection) { + return this.selection.renderPass_; + } + }, + + get selectedLayer() { + if (this.selection) { + const selectedLayerId = this.selection.associatedLayerId; + return this.layerTreeImpl_.findLayerWithId(selectedLayerId); + } + }, + + get renderPasses() { + let renderPasses = + this.layerTreeImpl.layerTreeHostImpl.args.frame.renderPasses; + if (!this.showOtherLayers) { + const selectedRenderPass = this.selectedRenderPass; + if (selectedRenderPass) { + renderPasses = [selectedRenderPass]; + } + } + return renderPasses; + }, + + get layers() { + let layers = this.layerTreeImpl.renderSurfaceLayerList; + if (!this.showOtherLayers) { + const selectedLayer = this.selectedLayer; + if (selectedLayer) { + layers = [selectedLayer]; + } + } + return layers; + }, + + appendImageQuads_(quads, layer, layerQuad) { + // Generate image quads for the layer + for (let ir = 0; ir < layer.pictures.length; ++ir) { + const picture = layer.pictures[ir]; + if (!picture.layerRect) continue; + + const unitRect = picture.layerRect.asUVRectInside(layer.bounds); + const iq = layerQuad.projectUnitRect(unitRect); + + const pictureData = this.pictureAsImageData_[picture.guid]; + if (this.showContents && pictureData && pictureData.imageData) { + iq.imageData = pictureData.imageData; + iq.borderColor = 'rgba(0,0,0,0)'; + } else { + iq.imageData = undefined; + } + + iq.stackingGroupId = layerQuad.stackingGroupId; + quads.push(iq); + } + }, + + appendAnimationQuads_(quads, layer, layerQuad) { + if (!layer.animationBoundsRect) return; + + const rect = layer.animationBoundsRect; + const abq = tr.b.math.Quad.fromRect(rect); + + abq.backgroundColor = 'rgba(164,191,48,0.5)'; + abq.borderColor = 'rgba(205,255,0,0.75)'; + abq.borderWidth = 3.0; + abq.stackingGroupId = layerQuad.stackingGroupId; + abq.selectionToSetIfClicked = new cc.AnimationRectSelection( + layer, rect); + quads.push(abq); + }, + + appendInvalidationQuads_(quads, layer, layerQuad) { + if (layer.layerTreeImpl.hasSourceFrameBeenDrawnBefore) return; + + // Generate the invalidation rect quads. + for (const rect of layer.invalidation.rects) { + const unitRect = rect.asUVRectInside(layer.bounds); + const iq = layerQuad.projectUnitRect(unitRect); + iq.backgroundColor = 'rgba(0, 255, 0, 0.1)'; + if (rect.reason === 'appeared') { + iq.backgroundColor = 'rgba(0, 255, 128, 0.1)'; + } + iq.borderColor = 'rgba(0, 255, 0, 1)'; + iq.stackingGroupId = layerQuad.stackingGroupId; + + let message = 'Invalidation rect'; + if (rect.reason) { + message += ' (' + rect.reason + ')'; + } + if (rect.client) { + message += ' for ' + rect.client; + } + + iq.selectionToSetIfClicked = new cc.LayerRectSelection( + layer, message, rect, rect); + quads.push(iq); + } + }, + + appendUnrecordedRegionQuads_(quads, layer, layerQuad) { + // Generate the unrecorded region quads. + for (let ir = 0; ir < layer.unrecordedRegion.rects.length; ir++) { + const rect = layer.unrecordedRegion.rects[ir]; + const unitRect = rect.asUVRectInside(layer.bounds); + const iq = layerQuad.projectUnitRect(unitRect); + iq.backgroundColor = 'rgba(240, 230, 140, 0.3)'; + iq.borderColor = 'rgba(240, 230, 140, 1)'; + iq.stackingGroupId = layerQuad.stackingGroupId; + iq.selectionToSetIfClicked = new cc.LayerRectSelection( + layer, 'Unrecorded area', rect, rect); + quads.push(iq); + } + }, + + appendBottleneckQuads_(quads, layer, layerQuad, stackingGroupId) { + function processRegion(region, label, borderColor) { + const backgroundColor = borderColor.clone(); + backgroundColor.a = 0.4 * (borderColor.a || 1.0); + + if (!region || !region.rects) return; + + for (let ir = 0; ir < region.rects.length; ir++) { + const rect = region.rects[ir]; + const unitRect = rect.asUVRectInside(layer.bounds); + const iq = layerQuad.projectUnitRect(unitRect); + iq.backgroundColor = backgroundColor.toString(); + iq.borderColor = borderColor.toString(); + iq.borderWidth = 4.0; + iq.stackingGroupId = stackingGroupId; + iq.selectionToSetIfClicked = new cc.LayerRectSelection( + layer, label, rect, rect); + quads.push(iq); + } + } + + processRegion(layer.touchEventHandlerRegion, 'Touch listener', + tr.b.Color.fromString('rgb(228, 226, 27)')); + processRegion(layer.wheelEventHandlerRegion, 'Wheel listener', + tr.b.Color.fromString('rgb(176, 205, 29)')); + processRegion(layer.nonFastScrollableRegion, 'Repaints on scroll', + tr.b.Color.fromString('rgb(213, 134, 32)')); + }, + + appendTileCoverageRectQuads_( + quads, layer, layerQuad, heatmapType) { + if (!layer.tileCoverageRects) return; + + const tiles = []; + for (let ct = 0; ct < layer.tileCoverageRects.length; ++ct) { + const tile = layer.tileCoverageRects[ct].tile; + if (tile !== undefined) tiles.push(tile); + } + + const lthi = this.layerTreeImpl_.layerTreeHostImpl; + const minMax = + this.getMinMaxForHeatmap_(lthi.activeTiles, heatmapType); + const heatmapResult = + this.computeHeatmapColors_(tiles, minMax, heatmapType); + let heatIndex = 0; + + for (let ct = 0; ct < layer.tileCoverageRects.length; ++ct) { + let rect = layer.tileCoverageRects[ct].geometryRect; + rect = rect.scale(1.0 / layer.geometryContentsScale); + + const tile = layer.tileCoverageRects[ct].tile; + + const unitRect = rect.asUVRectInside(layer.bounds); + const quad = layerQuad.projectUnitRect(unitRect); + + quad.backgroundColor = 'rgba(0, 0, 0, 0)'; + quad.stackingGroupId = layerQuad.stackingGroupId; + let type = tr.e.cc.tileTypes.missing; + if (tile) { + type = tile.getTypeForLayer(layer); + quad.backgroundColor = heatmapResult[heatIndex].color; + ++heatIndex; + } + + quad.borderColor = tr.e.cc.tileBorder[type].color; + quad.borderWidth = tr.e.cc.tileBorder[type].width; + let label; + if (tile) { + label = 'coverageRect'; + } else { + label = 'checkerboard coverageRect'; + } + quad.selectionToSetIfClicked = new cc.LayerRectSelection( + layer, label, rect, layer.tileCoverageRects[ct]); + + quads.push(quad); + } + }, + + appendLayoutRectQuads_(quads, layer, layerQuad) { + if (!layer.layoutRects) { + return; + } + + for (let ct = 0; ct < layer.layoutRects.length; ++ct) { + let rect = layer.layoutRects[ct].geometryRect; + rect = rect.scale(1.0 / layer.geometryContentsScale); + + const unitRect = rect.asUVRectInside(layer.bounds); + const quad = layerQuad.projectUnitRect(unitRect); + + quad.backgroundColor = 'rgba(0, 0, 0, 0)'; + quad.stackingGroupId = layerQuad.stackingGroupId; + + quad.borderColor = 'rgba(0, 0, 200, 0.7)'; + quad.borderWidth = 2; + const label = 'Layout rect'; + quad.selectionToSetIfClicked = new cc.LayerRectSelection( + layer, label, rect); + + quads.push(quad); + } + }, + + getValueForHeatmap_(tile, heatmapType) { + if (heatmapType === TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY) { + return tile.scheduledPriority === 0 ? + undefined : + tile.scheduledPriority; + } else if (heatmapType === TILE_HEATMAP_TYPE.USING_GPU_MEMORY) { + if (tile.isSolidColor) return 0.5; + return tile.isUsingGpuMemory ? 0 : 1; + } + }, + + getMinMaxForHeatmap_(tiles, heatmapType) { + const range = new tr.b.math.Range(); + if (heatmapType === TILE_HEATMAP_TYPE.USING_GPU_MEMORY) { + range.addValue(0); + range.addValue(1); + return range; + } + + for (let i = 0; i < tiles.length; ++i) { + const value = this.getValueForHeatmap_(tiles[i], heatmapType); + if (value === undefined) continue; + range.addValue(value); + } + if (range.range === 0) { + range.addValue(1); + } + return range; + }, + + computeHeatmapColors_(tiles, minMax, heatmapType) { + const min = minMax.min; + const max = minMax.max; + + const color = function(value) { + let hue = 120 * (1 - (value - min) / (max - min)); + if (hue < 0) hue = 0; + return 'hsla(' + hue + ', 100%, 50%, 0.5)'; + }; + + const values = []; + for (let i = 0; i < tiles.length; ++i) { + const tile = tiles[i]; + const value = this.getValueForHeatmap_(tile, heatmapType); + const res = { + value, + color: value !== undefined ? color(value) : undefined + }; + values.push(res); + } + + return values; + }, + + appendTilesWithScaleQuads_( + quads, layer, layerQuad, scale, heatmapType) { + const lthi = this.layerTreeImpl_.layerTreeHostImpl; + + const tiles = []; + for (let i = 0; i < lthi.activeTiles.length; ++i) { + const tile = lthi.activeTiles[i]; + + if (Math.abs(tile.contentsScale - scale) > 1e-6) { + continue; + } + + // TODO(vmpstr): Make the stiching of tiles and layers a part of + // tile construction (issue 346) + if (layer.layerId !== tile.layerId) continue; + + tiles.push(tile); + } + + const minMax = + this.getMinMaxForHeatmap_(lthi.activeTiles, heatmapType); + const heatmapResult = + this.computeHeatmapColors_(tiles, minMax, heatmapType); + + for (let i = 0; i < tiles.length; ++i) { + const tile = tiles[i]; + const rect = tile.layerRect; + if (!tile.layerRect) continue; + + const unitRect = rect.asUVRectInside(layer.bounds); + const quad = layerQuad.projectUnitRect(unitRect); + + quad.backgroundColor = 'rgba(0, 0, 0, 0)'; + quad.stackingGroupId = layerQuad.stackingGroupId; + + const type = tile.getTypeForLayer(layer); + quad.borderColor = tr.e.cc.tileBorder[type].color; + quad.borderWidth = tr.e.cc.tileBorder[type].width; + + quad.backgroundColor = heatmapResult[i].color; + const data = { + tileType: type + }; + if (heatmapType !== TILE_HEATMAP_TYPE.NONE) { + data[heatmapType] = heatmapResult[i].value; + } + quad.selectionToSetIfClicked = new cc.TileSelection(tile, data); + quads.push(quad); + } + }, + + appendHighlightQuadsForLayer_( + quads, layer, layerQuad, highlights) { + highlights.forEach(function(highlight) { + const rect = highlight.rect; + + const unitRect = rect.asUVRectInside(layer.bounds); + const quad = layerQuad.projectUnitRect(unitRect); + + let colorId = ColorScheme.getColorIdForGeneralPurposeString( + highlight.colorKey); + const offset = ColorScheme.properties.brightenedOffsets[0]; + colorId = ColorScheme.getVariantColorId(colorId, offset); + + const color = ColorScheme.colors[colorId]; + + const quadForDrawing = quad.clone(); + quadForDrawing.backgroundColor = color.withAlpha(0.5).toString(); + quadForDrawing.borderColor = color.withAlpha(1.0).darken().toString(); + quadForDrawing.stackingGroupId = layerQuad.stackingGroupId; + quads.push(quadForDrawing); + }, this); + }, + + generateRenderPassQuads() { + if (!this.layerTreeImpl.layerTreeHostImpl.args.frame) return []; + const renderPasses = this.renderPasses; + if (!renderPasses) return []; + + const quads = []; + for (let i = 0; i < renderPasses.length; ++i) { + const quadList = renderPasses[i].quadList; + for (let j = 0; j < quadList.length; ++j) { + const drawQuad = quadList[j]; + const quad = drawQuad.rectAsTargetSpaceQuad.clone(); + quad.borderColor = 'rgb(170, 204, 238)'; + quad.borderWidth = 2; + quad.stackingGroupId = i; + quads.push(quad); + } + } + return quads; + }, + + generateLayerQuads() { + this.updateContentsPending_ = false; + + // Generate the quads for the view. + const layers = this.layers; + const quads = []; + let nextStackingGroupId = 0; + const alreadyVisitedLayerIds = {}; + + + let selectionHighlightsByLayerId; + if (this.selection) { + selectionHighlightsByLayerId = this.selection.highlightsByLayerId; + } else { + selectionHighlightsByLayerId = {}; + } + + const extraHighlightsByLayerId = this.extraHighlightsByLayerId || {}; + + for (let i = 1; i <= layers.length; i++) { + // Generate quads back-to-front. + const layer = layers[layers.length - i]; + alreadyVisitedLayerIds[layer.layerId] = true; + if (layer.objectInstance.name === 'cc::NinePatchLayerImpl') { + continue; + } + + const layerQuad = layer.layerQuad.clone(); + if (layer.usingGpuRasterization) { + const pixelRatio = window.devicePixelRatio || 1; + layerQuad.borderWidth = 2.0 * pixelRatio; + layerQuad.borderColor = 'rgba(154,205,50,0.75)'; + } else { + layerQuad.borderColor = 'rgba(0,0,0,0.75)'; + } + layerQuad.stackingGroupId = nextStackingGroupId++; + layerQuad.selectionToSetIfClicked = new cc.LayerSelection(layer); + layerQuad.layer = layer; + if (this.showOtherLayers && this.selectedLayer === layer) { + layerQuad.upperBorderColor = 'rgb(156,189,45)'; + } + + if (this.showAnimationBounds) { + this.appendAnimationQuads_(quads, layer, layerQuad); + } + + this.appendImageQuads_(quads, layer, layerQuad); + quads.push(layerQuad); + + + if (this.showInvalidations) { + this.appendInvalidationQuads_(quads, layer, layerQuad); + } + if (this.showUnrecordedRegion) { + this.appendUnrecordedRegionQuads_(quads, layer, layerQuad); + } + if (this.showBottlenecks) { + this.appendBottleneckQuads_(quads, layer, layerQuad, + layerQuad.stackingGroupId); + } + if (this.showLayoutRects) { + this.appendLayoutRectQuads_(quads, layer, layerQuad); + } + + if (this.howToShowTiles === 'coverage') { + this.appendTileCoverageRectQuads_( + quads, layer, layerQuad, this.tileHeatmapType); + } else if (this.howToShowTiles !== 'none') { + this.appendTilesWithScaleQuads_( + quads, layer, layerQuad, + this.howToShowTiles, this.tileHeatmapType); + } + + let highlights; + highlights = extraHighlightsByLayerId[layer.layerId]; + if (highlights) { + this.appendHighlightQuadsForLayer_( + quads, layer, layerQuad, highlights); + } + + highlights = selectionHighlightsByLayerId[layer.layerId]; + if (highlights) { + this.appendHighlightQuadsForLayer_( + quads, layer, layerQuad, highlights); + } + } + + this.layerTreeImpl.iterLayers(function(layer, depth, isMask, isReplica) { + if (!this.showOtherLayers && this.selectedLayer !== layer) return; + if (alreadyVisitedLayerIds[layer.layerId]) return; + + const layerQuad = layer.layerQuad; + const stackingGroupId = nextStackingGroupId++; + if (this.showBottlenecks) { + this.appendBottleneckQuads_(quads, layer, layerQuad, stackingGroupId); + } + }, this); + + const tracedInputLatencies = this.layerTreeImpl.tracedInputLatencies; + if (this.showInputEvents && tracedInputLatencies) { + for (let i = 0; i < tracedInputLatencies.length; i++) { + const coordinatesArray = + tracedInputLatencies[i].args.data.coordinates; + for (let j = 0; j < coordinatesArray.length; j++) { + const inputQuad = tr.b.math.Quad.fromXYWH( + coordinatesArray[j].x - 25, + coordinatesArray[j].y - 25, + 50, + 50); + inputQuad.borderColor = 'rgba(0, 0, 0, 0)'; + inputQuad.imageData = this.inputEventImageData_; + quads.push(inputQuad); + } + } + } + + return quads; + }, + + updateInfoBar_(infoBarMessages) { + if (infoBarMessages.length) { + this.infoBar_.removeAllButtons(); + this.infoBar_.message = 'Some problems were encountered...'; + this.infoBar_.addButton('More info...', function(e) { + const overlay = new tr.ui.b.Overlay(); + Polymer.dom(overlay).textContent = ''; + infoBarMessages.forEach(function(message) { + const title = document.createElement('h3'); + Polymer.dom(title).textContent = message.header; + + const details = document.createElement('div'); + Polymer.dom(details).textContent = message.details; + + Polymer.dom(overlay).appendChild(title); + Polymer.dom(overlay).appendChild(details); + }); + overlay.visible = true; + + e.stopPropagation(); + return false; + }); + this.infoBar_.visible = true; + } else { + this.infoBar_.removeAllButtons(); + this.infoBar_.message = ''; + this.infoBar_.visible = false; + } + }, + + getWhatRasterized_() { + const lthi = this.layerTreeImpl_.layerTreeHostImpl; + const renderProcess = lthi.objectInstance.parent; + const tasks = []; + for (const event of renderProcess.getDescendantEvents()) { + if (!(event instanceof tr.model.Slice)) continue; + + const tile = tr.e.cc.getTileFromRasterTaskSlice(event); + if (tile === undefined) continue; + + if (tile.containingSnapshot === lthi) { + tasks.push(event); + } + } + return tasks; + }, + + updateWhatRasterizedLinkState_() { + const tasks = this.getWhatRasterized_(); + if (tasks.length) { + Polymer.dom(this.whatRasterizedLink_).textContent = + tasks.length + ' raster tasks'; + this.whatRasterizedLink_.style.display = ''; + } else { + Polymer.dom(this.whatRasterizedLink_).textContent = ''; + this.whatRasterizedLink_.style.display = 'none'; + } + }, + + getWhatRasterizedEventSet_() { + return new tr.model.EventSet(this.getWhatRasterized_()); + } + }; + + return { + LayerTreeQuadStackView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html new file mode 100644 index 00000000000..66932ae785f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view_test.html @@ -0,0 +1,113 @@ +<!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/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('tileCoverageRectCount', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const lthi = instance.snapshots[0]; + + const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView(); + view.layerTreeImpl = lthi.activeTree; + view.howToShowTiles = 'none'; + view.showInvalidations = false; + view.showContents = false; + + // There should be some quads drawn with all "show" checkboxes off, + // but that number can change with new features added. + const aQuads = view.generateLayerQuads(); + view.howToShowTiles = 'coverage'; + const bQuads = view.generateLayerQuads(); + const numCoverageRects = bQuads.length - aQuads.length; + + // We know we have 5 coverage rects in lthi cats. + assert.strictEqual(numCoverageRects, 5); + }); + + test('inputEvent', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const lthi = instance.snapshots[0]; + lthi.activeTree.tracedInputLatencies = + [{args: {data: {coordinates: [{x: 10, y: 20}, {x: 30, y: 40}]}}}]; + + const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView(); + view.layerTreeImpl = lthi.activeTree; + view.showInputEvents = false; + + const aQuads = view.generateLayerQuads(); + view.showInputEvents = true; + const bQuads = view.generateLayerQuads(); + const numInputEventRects = bQuads.length - aQuads.length; + + assert.strictEqual(numInputEventRects, 2); + + // We should not start loading the image until the view is added into the + // DOM tree. + const image = Polymer.dom(view).querySelector('#input-event'); + assert.strictEqual(getComputedStyle(image).backgroundImage, ''); + assert.strictEqual(image.src, ''); + + document.body.appendChild(view); + view.updateContents_(); + assert.notEqual(getComputedStyle(image).backgroundImage, ''); + assert.notEqual(image.src, ''); + view.remove(); + }); + + test('invalidation', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const lthi = instance.snapshots[0]; + + const view = new tr.ui.e.chrome.cc.LayerTreeQuadStackView(); + view.layerTreeImpl = lthi.activeTree; + view.showInvalidations = false; + + const aQuads = view.generateLayerQuads(); + view.showInvalidations = true; + const bQuads = view.generateLayerQuads(); + const numInvalidationRects = bQuads.length - aQuads.length; + + // We know we have 3 invalidation rects. + assert.strictEqual(numInvalidationRects, 3); + + const expectedRectTypes = [ + 'Invalidation rect (appeared) for client1', + 'Invalidation rect (disappeared) for client2', + 'Invalidation rect' // The non-annotated rect. + ]; + const found = []; + for (const quad of bQuads) { + const i = expectedRectTypes.indexOf(quad.selectionToSetIfClicked && + quad.selectionToSetIfClicked.rectType_); + if (i !== -1) { + found[i] = true; + } + } + assert.deepEqual(found, [true, true, true]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html new file mode 100644 index 00000000000..56ecf770ec1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view.html @@ -0,0 +1,165 @@ +<!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/raf.html"> +<link rel="import" href="/tracing/base/settings.html"> +<link rel="import" href="/tracing/extras/chrome/cc/constants.html"> +<link rel="import" href="/tracing/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_quad_stack_view.html"> + +<script> +'use strict'; + +/** + * @fileoverview LayerView coordinates graphical and analysis views of layers. + */ + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const constants = tr.e.cc.constants; + + /** + * @constructor + */ + const LayerView = tr.ui.b.define('tr-ui-e-chrome-cc-layer-view'); + + LayerView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.style.flexDirection = 'column'; + this.style.display = 'flex'; + + this.layerTreeQuadStackView_ = + new tr.ui.e.chrome.cc.LayerTreeQuadStackView(); + this.dragBar_ = document.createElement('tr-ui-b-drag-handle'); + this.analysisEl_ = + document.createElement('tr-ui-e-chrome-cc-layer-view-analysis'); + this.analysisEl_.style.flexGrow = 0; + this.analysisEl_.style.flexShrink = 0; + this.analysisEl_.style.flexBasis = 'auto'; + this.analysisEl_.style.height = '150px'; + this.analysisEl_.style.overflow = 'auto'; + this.analysisEl_.addEventListener('requestSelectionChange', + this.onRequestSelectionChangeFromAnalysisEl_.bind(this)); + + this.dragBar_.target = this.analysisEl_; + + Polymer.dom(this).appendChild(this.layerTreeQuadStackView_); + Polymer.dom(this).appendChild(this.dragBar_); + Polymer.dom(this).appendChild(this.analysisEl_); + + this.layerTreeQuadStackView_.addEventListener('selection-change', + function() { + this.layerTreeQuadStackViewSelectionChanged_(); + }.bind(this)); + this.layerTreeQuadStackViewSelectionChanged_(); + }, + + get layerTreeImpl() { + return this.layerTreeQuadStackView_.layerTreeImpl; + }, + + set layerTreeImpl(newValue) { + return this.layerTreeQuadStackView_.layerTreeImpl = newValue; + }, + + set isRenderPassQuads(newValue) { + return this.layerTreeQuadStackView_.isRenderPassQuads = newValue; + }, + + get selection() { + return this.layerTreeQuadStackView_.selection; + }, + + set selection(newValue) { + this.layerTreeQuadStackView_.selection = newValue; + }, + + regenerateContent() { + this.layerTreeQuadStackView_.regenerateContent(); + }, + + layerTreeQuadStackViewSelectionChanged_() { + const selection = this.layerTreeQuadStackView_.selection; + if (selection) { + this.dragBar_.style.display = ''; + this.analysisEl_.style.display = ''; + Polymer.dom(this.analysisEl_).textContent = ''; + + const layer = selection.layer; + if (tr.e.cc.PictureSnapshot.CanDebugPicture() && + layer && + layer.args && + layer.args.pictures && + layer.args.pictures.length) { + Polymer.dom(this.analysisEl_).appendChild( + this.createPictureBtn_(layer.args.pictures)); + } + + const analysis = selection.createAnalysis(); + Polymer.dom(this.analysisEl_).appendChild(analysis); + for (const child of this.analysisEl_.children) { + child.style.userSelect = 'text'; + } + } else { + this.dragBar_.style.display = 'none'; + this.analysisEl_.style.display = 'none'; + const analysis = Polymer.dom(this.analysisEl_).firstChild; + if (analysis) { + Polymer.dom(this.analysisEl_).removeChild(analysis); + } + this.layerTreeQuadStackView_.style.height = + window.getComputedStyle(this).height; + } + tr.b.dispatchSimpleEvent(this, 'selection-change'); + }, + + createPictureBtn_(pictures) { + if (!(pictures instanceof Array)) { + pictures = [pictures]; + } + + const link = document.createElement('tr-ui-a-analysis-link'); + link.selection = function() { + const layeredPicture = new tr.e.cc.LayeredPicture(pictures); + const snapshot = new tr.e.cc.PictureSnapshot(layeredPicture); + snapshot.picture = layeredPicture; + + const selection = new tr.model.EventSet(); + selection.push(snapshot); + return selection; + }; + Polymer.dom(link).textContent = 'View in Picture Debugger'; + return link; + }, + + onRequestSelectionChangeFromAnalysisEl_(e) { + if (!(e.selection instanceof tr.ui.e.chrome.cc.Selection)) { + return; + } + + e.stopPropagation(); + this.selection = e.selection; + }, + + get extraHighlightsByLayerId() { + return this.layerTreeQuadStackView_.extraHighlightsByLayerId; + }, + + set extraHighlightsByLayerId(extraHighlightsByLayerId) { + this.layerTreeQuadStackView_.extraHighlightsByLayerId = + extraHighlightsByLayerId; + } + }; + + return { + LayerView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html new file mode 100644 index 00000000000..ed3de7b87e1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/layer_view_test.html @@ -0,0 +1,55 @@ +<!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/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_view.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const lthi = instance.snapshots[0]; + const numLayers = lthi.activeTree.renderSurfaceLayerList.length; + const layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1]; + + const view = new tr.ui.e.chrome.cc.LayerView(); + view.style.height = '500px'; + view.layerTreeImpl = lthi.activeTree; + view.selection = new tr.ui.e.chrome.cc.LayerSelection(layer); + + this.addHTMLOutput(view); + }); + + test('instantiate_withTileHighlight', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + + const instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0]; + const lthi = instance.snapshots[0]; + const numLayers = lthi.activeTree.renderSurfaceLayerList.length; + const layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1]; + const tile = lthi.activeTiles[0]; + + const view = new tr.ui.e.chrome.cc.LayerView(); + view.style.height = '500px'; + view.layerTreeImpl = lthi.activeTree; + view.selection = new tr.ui.e.chrome.cc.TileSelection(tile); + this.addHTMLOutput(view); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html new file mode 100644 index 00000000000..5dc62b1b4f6 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger.html @@ -0,0 +1,455 @@ +<!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/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/base/drag_handle.html"> +<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> +<link rel="import" href="/tracing/ui/base/info_bar.html"> +<link rel="import" href="/tracing/ui/base/list_view.html"> +<link rel="import" href="/tracing/ui/base/mouse_mode_selector.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/chrome/cc/picture_ops_chart_summary_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html"> + +<template id="tr-ui-e-chrome-cc-picture-debugger-template"> + <left-panel> + <picture-info> + <div> + <span class='title'>Skia Picture</span> + <span class='size'></span> + </div> + <div> + <input class='filename' type='text' value='skpicture.skp' /> + <button class='export'>Export</button> + </div> + </picture-info> + </left-panel> + <right-panel> + <tr-ui-e-chrome-cc-picture-ops-chart-view> + </tr-ui-e-chrome-cc-picture-ops-chart-view> + <raster-area><canvas></canvas></raster-area> + </right-panel> +</template> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const THIS_DOC = document.currentScript.ownerDocument; + + /** + * PictureDebugger is a view of a PictureSnapshot for inspecting + * the picture in detail. (e.g., timing information, etc.) + * + * @constructor + */ + const PictureDebugger = tr.ui.b.define('tr-ui-e-chrome-cc-picture-debugger'); + + PictureDebugger.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + const node = tr.ui.b.instantiateTemplate( + '#tr-ui-e-chrome-cc-picture-debugger-template', THIS_DOC); + + Polymer.dom(this).appendChild(node); + + this.style.display = 'flex'; + this.style.flexDirection = 'row'; + + const title = this.querySelector('.title'); + title.style.fontWeight = 'bold'; + title.style.marginLeft = '5px'; + title.style.marginRight = '5px'; + + this.pictureAsImageData_ = undefined; + this.showOverdraw_ = false; + this.zoomScaleValue_ = 1; + + this.sizeInfo_ = Polymer.dom(this).querySelector('.size'); + this.rasterArea_ = Polymer.dom(this).querySelector('raster-area'); + this.rasterArea_.style.backgroundColor = '#ddd'; + this.rasterArea_.style.minHeight = '100px'; + this.rasterArea_.style.minWidth = '200px'; + this.rasterArea_.style.overflow = 'auto'; + this.rasterArea_.style.paddingLeft = '5px'; + this.rasterCanvas_ = Polymer.dom(this.rasterArea_) + .querySelector('canvas'); + this.rasterCtx_ = this.rasterCanvas_.getContext('2d'); + + this.filename_ = Polymer.dom(this).querySelector('.filename'); + this.filename_.style.userSelect = 'text'; + this.filename_.style.marginLeft = '5px'; + + this.drawOpsChartSummaryView_ = + new tr.ui.e.chrome.cc.PictureOpsChartSummaryView(); + this.drawOpsChartView_ = new tr.ui.e.chrome.cc.PictureOpsChartView(); + this.drawOpsChartView_.addEventListener( + 'selection-changed', this.onChartBarClicked_.bind(this)); + + this.exportButton_ = Polymer.dom(this).querySelector('.export'); + this.exportButton_.addEventListener( + 'click', this.onSaveAsSkPictureClicked_.bind(this)); + + this.trackMouse_(); + + const overdrawCheckbox = tr.ui.b.createCheckBox( + this, 'showOverdraw', + 'pictureView.showOverdraw', false, + 'Show overdraw'); + + const chartCheckbox = tr.ui.b.createCheckBox( + this, 'showSummaryChart', + 'pictureView.showSummaryChart', false, + 'Show timing summary'); + + const pictureInfo = Polymer.dom(this).querySelector('picture-info'); + pictureInfo.style.flexGrow = 0; + pictureInfo.style.flexShrink = 0; + pictureInfo.style.flexBasis = 'auto'; + pictureInfo.style.paddingTop = '2px'; + Polymer.dom(pictureInfo).appendChild(overdrawCheckbox); + Polymer.dom(pictureInfo).appendChild(chartCheckbox); + + this.drawOpsView_ = new tr.ui.e.chrome.cc.PictureOpsListView(); + this.drawOpsView_.flexGrow = 1; + this.drawOpsView_.flexShrink = 1; + this.drawOpsView_.flexBasis = 'auto'; + this.drawOpsView_.addEventListener( + 'selection-changed', this.onChangeDrawOps_.bind(this)); + + const leftPanel = Polymer.dom(this).querySelector('left-panel'); + leftPanel.style.flexDirection = 'column'; + leftPanel.style.display = 'flex'; + leftPanel.style.flexGrow = 0; + leftPanel.style.flexShrink = 0; + leftPanel.style.flexBasis = 'auto'; + leftPanel.style.minWidth = '200px'; + leftPanel.style.overflow = 'auto'; + Polymer.dom(leftPanel).appendChild(this.drawOpsChartSummaryView_); + Polymer.dom(leftPanel).appendChild(this.drawOpsView_); + + const middleDragHandle = document.createElement('tr-ui-b-drag-handle'); + middleDragHandle.style.flexGrow = 0; + middleDragHandle.style.flexShrink = 0; + middleDragHandle.style.flexBasis = 'auto'; + middleDragHandle.horizontal = false; + middleDragHandle.target = leftPanel; + + const rightPanel = Polymer.dom(this).querySelector('right-panel'); + rightPanel.style.flexGrow = 1; + rightPanel.style.flexShrink = 1; + rightPanel.style.flexBasis = 'auto'; + rightPanel.style.minWidth = 0; + rightPanel.style.flexDirection = 'column'; + rightPanel.style.display = 'flex'; + + const chartView = Polymer.dom(rightPanel).querySelector( + 'tr-ui-e-chrome-cc-picture-ops-chart-view'); + this.drawOpsChartView_.style.flexGrow = 0; + this.drawOpsChartView_.style.flexShrink = 0; + this.drawOpsChartView_.style.flexBasis = 'auto'; + this.drawOpsChartView_.style.minWidth = 0; + this.drawOpsChartView_.style.overflowX = 'auto'; + this.drawOpsChartView_.style.overflowY = 'hidden'; + rightPanel.replaceChild(this.drawOpsChartView_, chartView); + + this.infoBar_ = document.createElement('tr-ui-b-info-bar'); + Polymer.dom(this.rasterArea_).appendChild(this.infoBar_); + + Polymer.dom(this).insertBefore(middleDragHandle, rightPanel); + + this.picture_ = undefined; + + const hkc = document.createElement('tv-ui-b-hotkey-controller'); + hkc.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + thisArg: this, + keyCode: 'h'.charCodeAt(0), + callback(e) { + this.moveSelectedOpBy(-1); + e.stopPropagation(); + } + })); + hkc.addHotKey(new tr.ui.b.HotKey({ + eventType: 'keypress', + thisArg: this, + keyCode: 'l'.charCodeAt(0), + callback(e) { + this.moveSelectedOpBy(1); + e.stopPropagation(); + } + })); + Polymer.dom(this).appendChild(hkc); + }, + + onSaveAsSkPictureClicked_() { + // Decode base64 data into a String + const rawData = tr.b.Base64.atob(this.picture_.getBase64SkpData()); + + // Convert this String into an Uint8Array + const length = rawData.length; + const arrayBuffer = new ArrayBuffer(length); + const uint8Array = new Uint8Array(arrayBuffer); + for (let c = 0; c < length; c++) { + uint8Array[c] = rawData.charCodeAt(c); + } + + // Create a blob URL from the binary array. + const blob = new Blob([uint8Array], {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; + link.download = this.filename_.value; + const event = document.createEvent('MouseEvents'); + event.initMouseEvent( + 'click', true, false, window, 0, 0, 0, 0, 0, + false, false, false, false, 0, null); + link.dispatchEvent(event); + }, + + get picture() { + return this.picture_; + }, + + set picture(picture) { + this.drawOpsView_.picture = picture; + this.drawOpsChartView_.picture = picture; + this.drawOpsChartSummaryView_.picture = picture; + this.picture_ = picture; + + this.exportButton_.disabled = !this.picture_.canSave; + + if (picture) { + const size = this.getRasterCanvasSize_(); + this.rasterCanvas_.width = size.width; + this.rasterCanvas_.height = size.height; + } + + const bounds = this.rasterArea_.getBoundingClientRect(); + const selectorBounds = this.mouseModeSelector_.getBoundingClientRect(); + this.mouseModeSelector_.pos = { + x: (bounds.right - selectorBounds.width - 10), + y: bounds.top + }; + + this.rasterize_(); + + this.scheduleUpdateContents_(); + }, + + getRasterCanvasSize_() { + const style = window.getComputedStyle(this.rasterArea_); + const width = + Math.max(parseInt(style.width), this.picture_.layerRect.width); + const height = + Math.max(parseInt(style.height), this.picture_.layerRect.height); + + return { + width, + height + }; + }, + + scheduleUpdateContents_() { + if (this.updateContentsPending_) return; + + this.updateContentsPending_ = true; + tr.b.requestAnimationFrameInThisFrameIfPossible( + this.updateContents_.bind(this) + ); + }, + + updateContents_() { + this.updateContentsPending_ = false; + + if (this.picture_) { + Polymer.dom(this.sizeInfo_).textContent = '(' + + this.picture_.layerRect.width + ' x ' + + this.picture_.layerRect.height + ')'; + } + + this.drawOpsChartView_.updateChartContents(); + this.drawOpsChartView_.scrollSelectedItemIntoViewIfNecessary(); + + // Return if picture hasn't finished rasterizing. + if (!this.pictureAsImageData_) return; + + this.infoBar_.visible = false; + this.infoBar_.removeAllButtons(); + if (this.pictureAsImageData_.error) { + this.infoBar_.message = 'Cannot rasterize...'; + this.infoBar_.addButton('More info...', function(e) { + const overlay = new tr.ui.b.Overlay(); + Polymer.dom(overlay).textContent = this.pictureAsImageData_.error; + overlay.visible = true; + e.stopPropagation(); + return false; + }.bind(this)); + this.infoBar_.visible = true; + } + + this.drawPicture_(); + }, + + drawPicture_() { + const size = this.getRasterCanvasSize_(); + if (size.width !== this.rasterCanvas_.width) { + this.rasterCanvas_.width = size.width; + } + if (size.height !== this.rasterCanvas_.height) { + this.rasterCanvas_.height = size.height; + } + + this.rasterCtx_.clearRect(0, 0, size.width, size.height); + + if (!this.pictureAsImageData_.imageData) return; + + const imgCanvas = this.pictureAsImageData_.asCanvas(); + const w = imgCanvas.width; + const h = imgCanvas.height; + this.rasterCtx_.drawImage(imgCanvas, 0, 0, w, h, + 0, 0, w * this.zoomScaleValue_, + h * this.zoomScaleValue_); + }, + + rasterize_() { + if (this.picture_) { + this.picture_.rasterize( + { + stopIndex: this.drawOpsView_.selectedOpIndex, + showOverdraw: this.showOverdraw_ + }, + this.onRasterComplete_.bind(this)); + } + }, + + onRasterComplete_(pictureAsImageData) { + this.pictureAsImageData_ = pictureAsImageData; + this.scheduleUpdateContents_(); + }, + + moveSelectedOpBy(increment) { + if (this.selectedOpIndex === undefined) { + this.selectedOpIndex = 0; + return; + } + this.selectedOpIndex = tr.b.math.clamp( + this.selectedOpIndex + increment, + 0, this.numOps); + }, + + get numOps() { + return this.drawOpsView_.numOps; + }, + + get selectedOpIndex() { + return this.drawOpsView_.selectedOpIndex; + }, + + set selectedOpIndex(index) { + this.drawOpsView_.selectedOpIndex = index; + this.drawOpsChartView_.selectedOpIndex = index; + }, + + onChartBarClicked_(e) { + this.drawOpsView_.selectedOpIndex = + this.drawOpsChartView_.selectedOpIndex; + }, + + onChangeDrawOps_(e) { + this.rasterize_(); + this.scheduleUpdateContents_(); + + this.drawOpsChartView_.selectedOpIndex = + this.drawOpsView_.selectedOpIndex; + }, + + set showOverdraw(v) { + this.showOverdraw_ = v; + this.rasterize_(); + }, + + set showSummaryChart(chartShouldBeVisible) { + if (chartShouldBeVisible) { + this.drawOpsChartSummaryView_.show(); + } else { + this.drawOpsChartSummaryView_.hide(); + } + }, + + trackMouse_() { + this.mouseModeSelector_ = document.createElement( + 'tr-ui-b-mouse-mode-selector'); + this.mouseModeSelector_.targetElement = this.rasterArea_; + Polymer.dom(this.rasterArea_).appendChild(this.mouseModeSelector_); + + this.mouseModeSelector_.supportedModeMask = + tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.mode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.defaultMode = tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM; + this.mouseModeSelector_.settingsKey = 'pictureDebugger.mouseModeSelector'; + + this.mouseModeSelector_.addEventListener('beginzoom', + this.onBeginZoom_.bind(this)); + this.mouseModeSelector_.addEventListener('updatezoom', + this.onUpdateZoom_.bind(this)); + this.mouseModeSelector_.addEventListener('endzoom', + this.onEndZoom_.bind(this)); + }, + + onBeginZoom_(e) { + this.isZooming_ = true; + + this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e); + + e.preventDefault(); + }, + + onUpdateZoom_(e) { + if (!this.isZooming_) return; + + const currentMouseViewPos = this.extractRelativeMousePosition_(e); + + // Take the distance the mouse has moved and we want to zoom at about + // 1/1000th of that speed. 0.01 feels jumpy. This could possibly be tuned + // more if people feel it's too slow. + this.zoomScaleValue_ += + ((this.lastMouseViewPos_.y - currentMouseViewPos.y) * 0.001); + this.zoomScaleValue_ = Math.max(this.zoomScaleValue_, 0.1); + + this.drawPicture_(); + + this.lastMouseViewPos_ = currentMouseViewPos; + }, + + onEndZoom_(e) { + this.lastMouseViewPos_ = undefined; + this.isZooming_ = false; + e.preventDefault(); + }, + + extractRelativeMousePosition_(e) { + return { + x: e.clientX - this.rasterArea_.offsetLeft, + y: e.clientY - this.rasterArea_.offsetTop + }; + } + }; + + return { + PictureDebugger, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html new file mode 100644 index 00000000000..e89e6b355e1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_debugger_test.html @@ -0,0 +1,32 @@ +<!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/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_debugger.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('instantiate', function() { + const picture = new tr.e.cc.PictureSnapshot({id: '31415'}, 10, { + 'params': { + 'opaque_rect': [-15, -15, 0, 0], + 'layer_rect': [-15, -15, 46, 833] + }, + 'skp64': 'DAAAAHYEAADzAQAABwAAAAFkYWVy8AAAAAgAAB4DAAAADAAAIAAAgD8AAIA/CAAAHgMAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADoAAAACAAAHgMAAAAMAAAjAAAAAAAAAAAMAAAjAAAAAAAAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADkAAAAGAAAFQEAAAAAAAAAAAAAAADAjkQAgPlDGAAAFQIAAAAAAAAAAAAAAADAjkQAgPlDCAAAHgMAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADgAAAAGAAAFQMAAAAAAKBAAACgQAAAgEIAAIBCBAAAHAQAABwEAAAcBAAAHHRjYWYBAAAADVNrU3JjWGZlcm1vZGVjZnB0AAAAAHlhcmGgAAAAIHRucAMAAAAAAEBBAACAPwAAAAAAAIA/AAAAAAAAgEAAAP//ADABAAAAAAAAAEBBAACAPwAAAAAAAIA/AAAAAAAAgED/////AjABAAAAAAAAAAAAAAAAAAEAAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEEAAIA/AAAAAAAAgD8AAAAAAACAQP8AAP8AMAEAAAAAACBmb2U=' // @suppress longLineCheck + }); + picture.preInitialize(); + picture.initialize(); + + const dbg = new tr.ui.e.chrome.cc.PictureDebugger(); + this.addHTMLOutput(dbg); + dbg.picture = picture; + dbg.style.border = '1px solid black'; + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html new file mode 100644 index 00000000000..55a8685aee0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_summary_view.html @@ -0,0 +1,458 @@ +<!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/ui.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const OPS_TIMING_ITERATIONS = 3; + const CHART_PADDING_LEFT = 65; + const CHART_PADDING_RIGHT = 40; + const AXIS_PADDING_LEFT = 60; + const AXIS_PADDING_RIGHT = 35; + const AXIS_PADDING_TOP = 25; + const AXIS_PADDING_BOTTOM = 45; + const AXIS_LABEL_PADDING = 5; + const AXIS_TICK_SIZE = 10; + const LABEL_PADDING = 5; + const LABEL_INTERLEAVE_OFFSET = 15; + const BAR_PADDING = 5; + const VERTICAL_TICKS = 5; + const HUE_CHAR_CODE_ADJUSTMENT = 5.7; + + /** + * Provides a chart showing the cumulative time spent in Skia operations + * during picture rasterization. + * + * @constructor + */ + const PictureOpsChartSummaryView = tr.ui.b.define( + 'tr-ui-e-chrome-cc-picture-ops-chart-summary-view'); + + PictureOpsChartSummaryView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.style.flexGrow = 0; + this.style.flexShrink = 0; + this.style.flexBasis = 'auto'; + this.style.fontSize = 0; + this.style.margin = 0; + this.style.minHeight = '200px'; + this.style.minWidth = '200px'; + this.style.overflow = 'hidden'; + this.style.padding = 0; + + this.picture_ = undefined; + this.pictureDataProcessed_ = false; + + this.chartScale_ = window.devicePixelRatio; + + this.chart_ = document.createElement('canvas'); + this.chartCtx_ = this.chart_.getContext('2d'); + Polymer.dom(this).appendChild(this.chart_); + + this.opsTimingData_ = []; + + this.chartWidth_ = 0; + this.chartHeight_ = 0; + this.requiresRedraw_ = true; + + this.currentBarMouseOverTarget_ = null; + + this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this)); + new ResizeObserver(this.onResize_.bind(this)).observe(this); + }, + + get requiresRedraw() { + return this.requiresRedraw_; + }, + + set requiresRedraw(requiresRedraw) { + this.requiresRedraw_ = requiresRedraw; + }, + + get picture() { + return this.picture_; + }, + + set picture(picture) { + this.picture_ = picture; + this.pictureDataProcessed_ = false; + + if (Polymer.dom(this).classList.contains('hidden')) return; + + this.processPictureData_(); + this.requiresRedraw = true; + this.updateChartContents(); + }, + + hide() { + Polymer.dom(this).classList.add('hidden'); + this.style.display = 'none'; + }, + + show() { + Polymer.dom(this).classList.remove('hidden'); + this.style.display = ''; + + if (!this.pictureDataProcessed_) { + this.processPictureData_(); + } + this.requiresRedraw = true; + this.updateChartContents(); + }, + + onMouseMove_(e) { + const lastBarMouseOverTarget = this.currentBarMouseOverTarget_; + this.currentBarMouseOverTarget_ = null; + + const x = e.offsetX; + const y = e.offsetY; + + const chartLeft = CHART_PADDING_LEFT; + const chartRight = this.chartWidth_ - CHART_PADDING_RIGHT; + const chartTop = AXIS_PADDING_TOP; + const chartBottom = this.chartHeight_ - AXIS_PADDING_BOTTOM; + const chartInnerWidth = chartRight - chartLeft; + + if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) { + this.currentBarMouseOverTarget_ = Math.floor( + (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length); + + this.currentBarMouseOverTarget_ = tr.b.math.clamp( + this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1); + } + + if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) return; + + this.drawChartContents_(); + }, + + onResize_() { + this.requiresRedraw = true; + this.updateChartContents(); + }, + + updateChartContents() { + if (this.requiresRedraw) { + this.updateChartDimensions_(); + } + + this.drawChartContents_(); + }, + + updateChartDimensions_() { + this.chartWidth_ = this.offsetWidth; + this.chartHeight_ = this.offsetHeight; + + // Scale up the canvas according to the devicePixelRatio, then reduce it + // down again via CSS. Finally we apply a scale to the canvas so that + // things are drawn at the correct size. + this.chart_.width = this.chartWidth_ * this.chartScale_; + this.chart_.height = this.chartHeight_ * this.chartScale_; + + this.chart_.style.width = this.chartWidth_ + 'px'; + this.chart_.style.height = this.chartHeight_ + 'px'; + + this.chartCtx_.scale(this.chartScale_, this.chartScale_); + }, + + processPictureData_() { + this.resetOpsTimingData_(); + this.pictureDataProcessed_ = true; + + if (!this.picture_) return; + + let ops = this.picture_.getOps(); + if (!ops) return; + + ops = this.picture_.tagOpsWithTimings(ops); + + // Check that there are valid times. + if (ops[0].cmd_time === undefined) return; + + this.collapseOpsToTimingBuckets_(ops); + }, + + drawChartContents_() { + this.clearChartContents_(); + + if (this.opsTimingData_.length === 0) { + this.showNoTimingDataMessage_(); + return; + } + + this.drawChartAxes_(); + this.drawBars_(); + this.drawLineAtBottomOfChart_(); + + if (this.currentBarMouseOverTarget_ === null) return; + + this.drawTooltip_(); + }, + + drawLineAtBottomOfChart_() { + this.chartCtx_.strokeStyle = '#AAA'; + this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5); + this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5); + this.chartCtx_.stroke(); + }, + + drawTooltip_() { + const tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_]; + const tooltipTitle = tooltipData.cmd_string; + const tooltipTime = tooltipData.cmd_time.toFixed(4); + + const tooltipWidth = 110; + const tooltipHeight = 40; + const chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT - + CHART_PADDING_LEFT; + const barWidth = chartInnerWidth / this.opsTimingData_.length; + const tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5); + + const left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ * + barWidth - tooltipOffset; + const top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5); + + this.chartCtx_.save(); + + this.chartCtx_.shadowOffsetX = 0; + this.chartCtx_.shadowOffsetY = 5; + this.chartCtx_.shadowBlur = 4; + this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)'; + + this.chartCtx_.strokeStyle = '#888'; + this.chartCtx_.fillStyle = '#EEE'; + this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight); + + this.chartCtx_.shadowColor = 'transparent'; + this.chartCtx_.translate(0.5, 0.5); + this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight); + + this.chartCtx_.restore(); + + this.chartCtx_.fillStyle = '#222'; + this.chartCtx_.textBaseline = 'top'; + this.chartCtx_.font = '800 12px Arial'; + this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8); + + this.chartCtx_.fillStyle = '#555'; + this.chartCtx_.textBaseline = 'top'; + this.chartCtx_.font = '400 italic 10px Arial'; + this.chartCtx_.fillText('Total: ' + tooltipTime + 'ms', + left + 8, top + 22); + }, + + drawBars_() { + const len = this.opsTimingData_.length; + const max = this.opsTimingData_[0].cmd_time; + const min = this.opsTimingData_[len - 1].cmd_time; + + const width = this.chartWidth_ - CHART_PADDING_LEFT - CHART_PADDING_RIGHT; + const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; + const barWidth = Math.floor(width / len); + + let opData; + let opTiming; + let opHeight; + let opLabel; + let barLeft; + + for (let b = 0; b < len; b++) { + opData = this.opsTimingData_[b]; + opTiming = opData.cmd_time / max; + + opHeight = Math.round(Math.max(1, opTiming * height)); + opLabel = opData.cmd_string; + barLeft = CHART_PADDING_LEFT + b * barWidth; + + this.chartCtx_.fillStyle = this.getOpColor_(opLabel); + + this.chartCtx_.fillRect(barLeft + BAR_PADDING, AXIS_PADDING_TOP + + height - opHeight, barWidth - 2 * BAR_PADDING, opHeight); + } + }, + + getOpColor_(opName) { + const characters = opName.split(''); + const hue = characters.reduce(this.reduceNameToHue, 0) % 360; + + return 'hsl(' + hue + ', 30%, 50%)'; + }, + + reduceNameToHue(previousValue, currentValue, index, array) { + // Get the char code and apply a magic adjustment value so we get + // pretty colors from around the rainbow. + return Math.round(previousValue + currentValue.charCodeAt(0) * + HUE_CHAR_CODE_ADJUSTMENT); + }, + + drawChartAxes_() { + const len = this.opsTimingData_.length; + const max = this.opsTimingData_[0].cmd_time; + const min = this.opsTimingData_[len - 1].cmd_time; + + const width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT; + const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; + + const totalBarWidth = this.chartWidth_ - CHART_PADDING_LEFT - + CHART_PADDING_RIGHT; + const barWidth = Math.floor(totalBarWidth / len); + const tickYInterval = height / (VERTICAL_TICKS - 1); + let tickYPosition = 0; + const tickValInterval = (max - min) / (VERTICAL_TICKS - 1); + let tickVal = 0; + + this.chartCtx_.fillStyle = '#333'; + this.chartCtx_.strokeStyle = '#777'; + this.chartCtx_.save(); + + // Translate half a pixel to avoid blurry lines. + this.chartCtx_.translate(0.5, 0.5); + + // Sides. + + this.chartCtx_.save(); + + this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); + this.chartCtx_.moveTo(0, 0); + this.chartCtx_.lineTo(0, height); + this.chartCtx_.lineTo(width, height); + + // Y-axis ticks. + this.chartCtx_.font = '10px Arial'; + this.chartCtx_.textAlign = 'right'; + this.chartCtx_.textBaseline = 'middle'; + + for (let t = 0; t < VERTICAL_TICKS; t++) { + tickYPosition = Math.round(t * tickYInterval); + tickVal = (max - t * tickValInterval).toFixed(4); + + this.chartCtx_.moveTo(0, tickYPosition); + this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition); + this.chartCtx_.fillText(tickVal, + -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition); + } + + this.chartCtx_.stroke(); + + this.chartCtx_.restore(); + + + // Labels. + + this.chartCtx_.save(); + + this.chartCtx_.translate(CHART_PADDING_LEFT + Math.round(barWidth * 0.5), + AXIS_PADDING_TOP + height + LABEL_PADDING); + + this.chartCtx_.font = '10px Arial'; + this.chartCtx_.textAlign = 'center'; + this.chartCtx_.textBaseline = 'top'; + + let labelTickLeft; + let labelTickBottom; + for (let l = 0; l < len; l++) { + labelTickLeft = Math.round(l * barWidth); + labelTickBottom = l % 2 * LABEL_INTERLEAVE_OFFSET; + + this.chartCtx_.save(); + this.chartCtx_.moveTo(labelTickLeft, -LABEL_PADDING); + this.chartCtx_.lineTo(labelTickLeft, labelTickBottom); + this.chartCtx_.stroke(); + this.chartCtx_.restore(); + + this.chartCtx_.fillText(this.opsTimingData_[l].cmd_string, + labelTickLeft, labelTickBottom); + } + + this.chartCtx_.restore(); + + this.chartCtx_.restore(); + }, + + clearChartContents_() { + this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_); + }, + + showNoTimingDataMessage_() { + this.chartCtx_.font = '800 italic 14px Arial'; + this.chartCtx_.fillStyle = '#333'; + this.chartCtx_.textAlign = 'center'; + this.chartCtx_.textBaseline = 'middle'; + this.chartCtx_.fillText('No timing data available.', + this.chartWidth_ * 0.5, this.chartHeight_ * 0.5); + }, + + collapseOpsToTimingBuckets_(ops) { + const opsTimingDataIndexHash_ = {}; + const timingData = this.opsTimingData_; + let op; + let opIndex; + + for (let i = 0; i < ops.length; i++) { + op = ops[i]; + + if (op.cmd_time === undefined) continue; + + // Try to locate the entry for the current operation + // based on its name. If that fails, then create one for it. + opIndex = opsTimingDataIndexHash_[op.cmd_string] || null; + + if (opIndex === null) { + timingData.push({ + cmd_time: 0, + cmd_string: op.cmd_string + }); + + opIndex = timingData.length - 1; + opsTimingDataIndexHash_[op.cmd_string] = opIndex; + } + + timingData[opIndex].cmd_time += op.cmd_time; + } + + timingData.sort(this.sortTimingBucketsByOpTimeDescending_); + + this.collapseTimingBucketsToOther_(4); + }, + + collapseTimingBucketsToOther_(count) { + const timingData = this.opsTimingData_; + const otherSource = timingData.splice(count, timingData.length - count); + let otherDestination = null; + + if (!otherSource.length) return; + + timingData.push({ + cmd_time: 0, + cmd_string: 'Other' + }); + + otherDestination = timingData[timingData.length - 1]; + for (let i = 0; i < otherSource.length; i++) { + otherDestination.cmd_time += otherSource[i].cmd_time; + } + }, + + sortTimingBucketsByOpTimeDescending_(a, b) { + return b.cmd_time - a.cmd_time; + }, + + resetOpsTimingData_() { + this.opsTimingData_.length = 0; + } + }; + + return { + PictureOpsChartSummaryView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html new file mode 100644 index 00000000000..413998847aa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_chart_view.html @@ -0,0 +1,505 @@ +<!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/dom_helpers.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const BAR_PADDING = 1; + const BAR_WIDTH = 5; + const CHART_PADDING_LEFT = 65; + const CHART_PADDING_RIGHT = 30; + const CHART_PADDING_BOTTOM = 35; + const CHART_PADDING_TOP = 20; + const AXIS_PADDING_LEFT = 55; + const AXIS_PADDING_RIGHT = 30; + const AXIS_PADDING_BOTTOM = 35; + const AXIS_PADDING_TOP = 20; + const AXIS_TICK_SIZE = 5; + const AXIS_LABEL_PADDING = 5; + const VERTICAL_TICKS = 5; + const HUE_CHAR_CODE_ADJUSTMENT = 5.7; + + /** + * Provides a chart showing the cumulative time spent in Skia operations + * during picture rasterization. + * + * @constructor + */ + const PictureOpsChartView = + tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-chart-view'); + + PictureOpsChartView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.style.display = 'block'; + this.style.height = '180px'; + this.style.margin = 0; + this.style.padding = 0; + this.style.position = 'relative'; + + this.picture_ = undefined; + this.pictureOps_ = undefined; + this.opCosts_ = undefined; + + this.chartScale_ = window.devicePixelRatio; + + this.chart_ = document.createElement('canvas'); + this.chartCtx_ = this.chart_.getContext('2d'); + Polymer.dom(this).appendChild(this.chart_); + + this.selectedOpIndex_ = undefined; + this.chartWidth_ = 0; + this.chartHeight_ = 0; + this.dimensionsHaveChanged_ = true; + + this.currentBarMouseOverTarget_ = undefined; + + this.ninetyFifthPercentileCost_ = 0; + this.totalOpCost_ = 0; + + this.chart_.addEventListener('click', this.onClick_.bind(this)); + this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this)); + new ResizeObserver(this.onResize_.bind(this)).observe(this); + + this.usePercentileScale_ = false; + this.usePercentileScaleCheckbox_ = tr.ui.b.createCheckBox( + this, 'usePercentileScale', + 'PictureOpsChartView.usePercentileScale', false, + 'Limit to 95%-ile'); + Polymer.dom(this.usePercentileScaleCheckbox_).classList.add( + 'use-percentile-scale'); + this.usePercentileScaleCheckbox_.style.position = 'absolute'; + this.usePercentileScaleCheckbox_.style.left = 0; + this.usePercentileScaleCheckbox_.style.top = 0; + Polymer.dom(this).appendChild(this.usePercentileScaleCheckbox_); + }, + + get dimensionsHaveChanged() { + return this.dimensionsHaveChanged_; + }, + + set dimensionsHaveChanged(dimensionsHaveChanged) { + this.dimensionsHaveChanged_ = dimensionsHaveChanged; + }, + + get usePercentileScale() { + return this.usePercentileScale_; + }, + + set usePercentileScale(usePercentileScale) { + this.usePercentileScale_ = usePercentileScale; + this.drawChartContents_(); + }, + + get numOps() { + return this.opCosts_.length; + }, + + get selectedOpIndex() { + return this.selectedOpIndex_; + }, + + set selectedOpIndex(selectedOpIndex) { + if (selectedOpIndex < 0) throw new Error('Invalid index'); + if (selectedOpIndex >= this.numOps) throw new Error('Invalid index'); + + this.selectedOpIndex_ = selectedOpIndex; + }, + + get picture() { + return this.picture_; + }, + + set picture(picture) { + this.picture_ = picture; + this.pictureOps_ = picture.tagOpsWithTimings(picture.getOps()); + this.currentBarMouseOverTarget_ = undefined; + this.processPictureData_(); + this.dimensionsHaveChanged = true; + }, + + processPictureData_() { + if (this.pictureOps_ === undefined) return; + + let totalOpCost = 0; + + // Take a copy of the picture ops data for sorting. + this.opCosts_ = this.pictureOps_.map(function(op) { + totalOpCost += op.cmd_time; + return op.cmd_time; + }); + this.opCosts_.sort(); + + const ninetyFifthPercentileCostIndex = Math.floor( + this.opCosts_.length * 0.95); + this.ninetyFifthPercentileCost_ = + this.opCosts_[ninetyFifthPercentileCostIndex]; + this.maxCost_ = this.opCosts_[this.opCosts_.length - 1]; + + this.totalOpCost_ = totalOpCost; + }, + + extractBarIndex_(e) { + let index = undefined; + + if (this.pictureOps_ === undefined || + this.pictureOps_.length === 0) { + return index; + } + + const x = e.offsetX; + const y = e.offsetY; + + const totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length; + + const chartLeft = CHART_PADDING_LEFT; + const chartTop = 0; + const chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM; + const chartRight = chartLeft + totalBarWidth; + + if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom) { + return index; + } + + index = Math.floor((x - chartLeft) / totalBarWidth * + this.pictureOps_.length); + + index = tr.b.math.clamp(index, 0, this.pictureOps_.length - 1); + + return index; + }, + + onClick_(e) { + const barClicked = this.extractBarIndex_(e); + + if (barClicked === undefined) return; + + // If we click on the already selected item we should deselect. + if (barClicked === this.selectedOpIndex) { + this.selectedOpIndex = undefined; + } else { + this.selectedOpIndex = barClicked; + } + + e.preventDefault(); + + tr.b.dispatchSimpleEvent(this, 'selection-changed', false); + }, + + onMouseMove_(e) { + const lastBarMouseOverTarget = this.currentBarMouseOverTarget_; + this.currentBarMouseOverTarget_ = this.extractBarIndex_(e); + + if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) { + return; + } + + this.drawChartContents_(); + }, + + onResize_() { + this.dimensionsHaveChanged = true; + this.updateChartContents(); + }, + + scrollSelectedItemIntoViewIfNecessary() { + if (this.selectedOpIndex === undefined) { + return; + } + + const width = this.offsetWidth; + const left = this.scrollLeft; + const right = left + width; + const targetLeft = CHART_PADDING_LEFT + + (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex; + + if (targetLeft > left && targetLeft < right) { + return; + } + + this.scrollLeft = (targetLeft - width * 0.5); + }, + + updateChartContents() { + if (this.dimensionsHaveChanged) { + this.updateChartDimensions_(); + } + + this.drawChartContents_(); + }, + + updateChartDimensions_() { + if (!this.pictureOps_) return; + + let width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT + + ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length); + + if (width < this.offsetWidth) { + width = this.offsetWidth; + } + + // Allow the element to be its natural size as set by flexbox, then lock + // the width in before we set the width of the canvas. + this.chartWidth_ = width; + this.chartHeight_ = this.getBoundingClientRect().height; + + // Scale up the canvas according to the devicePixelRatio, then reduce it + // down again via CSS. Finally we apply a scale to the canvas so that + // things are drawn at the correct size. + this.chart_.width = this.chartWidth_ * this.chartScale_; + this.chart_.height = this.chartHeight_ * this.chartScale_; + + this.chart_.style.width = this.chartWidth_ + 'px'; + this.chart_.style.height = this.chartHeight_ + 'px'; + + this.chartCtx_.scale(this.chartScale_, this.chartScale_); + + this.dimensionsHaveChanged = false; + }, + + drawChartContents_() { + this.clearChartContents_(); + + if (this.pictureOps_ === undefined || + this.pictureOps_.length === 0 || + this.pictureOps_[0].cmd_time === undefined) { + this.showNoTimingDataMessage_(); + return; + } + + this.drawSelection_(); + this.drawBars_(); + this.drawChartAxes_(); + this.drawLinesAtTickMarks_(); + this.drawLineAtBottomOfChart_(); + + if (this.currentBarMouseOverTarget_ === undefined) { + return; + } + + this.drawTooltip_(); + }, + + drawSelection_() { + if (this.selectedOpIndex === undefined) { + return; + } + + const width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex; + this.chartCtx_.fillStyle = 'rgb(223, 235, 230)'; + this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP, + width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM); + }, + + drawChartAxes_() { + const min = this.opCosts_[0]; + const max = this.opCosts_[this.opCosts_.length - 1]; + const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; + + const tickYInterval = height / (VERTICAL_TICKS - 1); + let tickYPosition = 0; + const tickValInterval = (max - min) / (VERTICAL_TICKS - 1); + let tickVal = 0; + + this.chartCtx_.fillStyle = '#333'; + this.chartCtx_.strokeStyle = '#777'; + this.chartCtx_.save(); + + // Translate half a pixel to avoid blurry lines. + this.chartCtx_.translate(0.5, 0.5); + + // Sides. + this.chartCtx_.beginPath(); + this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); + this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ - + AXIS_PADDING_BOTTOM); + this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT, + this.chartHeight_ - AXIS_PADDING_BOTTOM); + this.chartCtx_.stroke(); + this.chartCtx_.closePath(); + + // Y-axis ticks. + this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP); + + this.chartCtx_.font = '10px Arial'; + this.chartCtx_.textAlign = 'right'; + this.chartCtx_.textBaseline = 'middle'; + + this.chartCtx_.beginPath(); + for (let t = 0; t < VERTICAL_TICKS; t++) { + tickYPosition = Math.round(t * tickYInterval); + tickVal = (max - t * tickValInterval).toFixed(4); + + this.chartCtx_.moveTo(0, tickYPosition); + this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition); + this.chartCtx_.fillText(tickVal, + -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition); + } + + this.chartCtx_.stroke(); + this.chartCtx_.closePath(); + + this.chartCtx_.restore(); + }, + + drawLinesAtTickMarks_() { + const height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM; + const width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT; + const tickYInterval = height / (VERTICAL_TICKS - 1); + let tickYPosition = 0; + + this.chartCtx_.save(); + + this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5); + this.chartCtx_.beginPath(); + this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)'; + + for (let t = 0; t < VERTICAL_TICKS; t++) { + tickYPosition = Math.round(t * tickYInterval); + + this.chartCtx_.moveTo(0, tickYPosition); + this.chartCtx_.lineTo(width, tickYPosition); + this.chartCtx_.stroke(); + } + + this.chartCtx_.restore(); + this.chartCtx_.closePath(); + }, + + drawLineAtBottomOfChart_() { + this.chartCtx_.strokeStyle = '#AAA'; + this.chartCtx_.beginPath(); + this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5); + this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5); + this.chartCtx_.stroke(); + this.chartCtx_.closePath(); + }, + + drawTooltip_() { + const tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_]; + const tooltipTitle = tooltipData.cmd_string; + const tooltipTime = tooltipData.cmd_time.toFixed(4); + const toolTipTimePercentage = + ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2); + + const tooltipWidth = 120; + const tooltipHeight = 40; + const chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT - + CHART_PADDING_LEFT; + const barWidth = BAR_WIDTH + BAR_PADDING; + const tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5); + + const left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ * + barWidth - tooltipOffset; + const top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5); + + this.chartCtx_.save(); + + this.chartCtx_.shadowOffsetX = 0; + this.chartCtx_.shadowOffsetY = 5; + this.chartCtx_.shadowBlur = 4; + this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)'; + + this.chartCtx_.strokeStyle = '#888'; + this.chartCtx_.fillStyle = '#EEE'; + this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight); + + this.chartCtx_.shadowColor = 'transparent'; + this.chartCtx_.translate(0.5, 0.5); + this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight); + + this.chartCtx_.restore(); + + this.chartCtx_.fillStyle = '#222'; + this.chartCtx_.textAlign = 'left'; + this.chartCtx_.textBaseline = 'top'; + this.chartCtx_.font = '800 12px Arial'; + this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8); + + this.chartCtx_.fillStyle = '#555'; + this.chartCtx_.font = '400 italic 10px Arial'; + this.chartCtx_.fillText(tooltipTime + 'ms (' + + toolTipTimePercentage + '%)', left + 8, top + 22); + }, + + drawBars_() { + let op; + let opColor = 0; + let opHeight = 0; + const opWidth = BAR_WIDTH + BAR_PADDING; + let opHover = false; + + const bottom = this.chartHeight_ - CHART_PADDING_BOTTOM; + const maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM - + CHART_PADDING_TOP; + + let maxValue; + if (this.usePercentileScale) { + maxValue = this.ninetyFifthPercentileCost_; + } else { + maxValue = this.maxCost_; + } + + for (let b = 0; b < this.pictureOps_.length; b++) { + op = this.pictureOps_[b]; + opHeight = Math.round( + (op.cmd_time / maxValue) * maxHeight); + opHeight = Math.max(opHeight, 1); + opHover = (b === this.currentBarMouseOverTarget_); + opColor = this.getOpColor_(op.cmd_string, opHover); + + if (b === this.selectedOpIndex) { + this.chartCtx_.fillStyle = '#FFFF00'; + } else { + this.chartCtx_.fillStyle = opColor; + } + + this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth, + bottom - opHeight, BAR_WIDTH, opHeight); + } + }, + + getOpColor_(opName, hover) { + const characters = opName.split(''); + + const hue = characters.reduce(this.reduceNameToHue, 0) % 360; + const saturation = 30; + const lightness = hover ? '75%' : '50%'; + + return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)'; + }, + + reduceNameToHue(previousValue, currentValue, index, array) { + // Get the char code and apply a magic adjustment value so we get + // pretty colors from around the rainbow. + return Math.round(previousValue + currentValue.charCodeAt(0) * + HUE_CHAR_CODE_ADJUSTMENT); + }, + + clearChartContents_() { + this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_); + }, + + showNoTimingDataMessage_() { + this.chartCtx_.font = '800 italic 14px Arial'; + this.chartCtx_.fillStyle = '#333'; + this.chartCtx_.textAlign = 'center'; + this.chartCtx_.textBaseline = 'middle'; + this.chartCtx_.fillText('No timing data available.', + this.chartWidth_ * 0.5, this.chartHeight_ * 0.5); + } + }; + + return { + PictureOpsChartView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html new file mode 100644 index 00000000000..2e45be58c33 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view.html @@ -0,0 +1,261 @@ +<!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/extras/chrome/cc/constants.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/list_view.html"> +<link rel="import" href="/tracing/ui/base/utils.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + const OPS_TIMING_ITERATIONS = 3; // Iterations to average op timing info over. + const ANNOTATION = 'Comment'; + const BEGIN_ANNOTATION = 'BeginCommentGroup'; + const END_ANNOTATION = 'EndCommentGroup'; + const ANNOTATION_ID = 'ID: '; + const ANNOTATION_CLASS = 'CLASS: '; + const ANNOTATION_TAG = 'TAG: '; + + const constants = tr.e.cc.constants; + + /** + * @constructor + */ + const PictureOpsListView = + tr.ui.b.define('tr-ui-e-chrome-cc-picture-ops-list-view'); + + PictureOpsListView.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate() { + this.style.borderTop = '1px solid grey'; + this.style.overflow = 'auto'; + this.opsList_ = new tr.ui.b.ListView(); + Polymer.dom(this).appendChild(this.opsList_); + + this.selectedOp_ = undefined; + this.selectedOpIndex_ = undefined; + this.opsList_.addEventListener( + 'selection-changed', this.onSelectionChanged_.bind(this)); + + this.picture_ = undefined; + }, + + get picture() { + return this.picture_; + }, + + set picture(picture) { + this.picture_ = picture; + this.updateContents_(); + }, + + updateContents_() { + this.opsList_.clear(); + + if (!this.picture_) return; + + let ops = this.picture_.getOps(); + if (!ops) return; + + ops = this.picture_.tagOpsWithTimings(ops); + + ops = this.opsTaggedWithAnnotations_(ops); + + for (let i = 0; i < ops.length; i++) { + const op = ops[i]; + const item = document.createElement('div'); + item.opIndex = op.opIndex; + Polymer.dom(item).textContent = i + ') ' + op.cmd_string; + + // Display the element info associated with the op, if available. + if (op.elementInfo.tag || op.elementInfo.id || op.elementInfo.class) { + const elementInfo = document.createElement('span'); + Polymer.dom(elementInfo).classList.add('elementInfo'); + elementInfo.style.color = 'purple'; + elementInfo.style.fontSize = 'small'; + elementInfo.style.fontWeight = 'bold'; + elementInfo.style.color = '#777'; + const tag = op.elementInfo.tag ? op.elementInfo.tag : 'unknown'; + const id = op.elementInfo.id ? 'id=' + op.elementInfo.id : undefined; + const className = op.elementInfo.class ? 'class=' + + op.elementInfo.class : undefined; + Polymer.dom(elementInfo).textContent = + '<' + tag + (id ? ' ' : '') + + (id ? id : '') + (className ? ' ' : '') + + (className ? className : '') + '>'; + Polymer.dom(item).appendChild(elementInfo); + } + + // Display the Skia params. + // FIXME: now that we have structured data, we should format it. + // (https://github.com/google/trace-viewer/issues/782) + if (op.info.length > 0) { + const infoItem = document.createElement('div'); + Polymer.dom(infoItem).textContent = JSON.stringify(op.info); + infoItem.style.fontSize = 'x-small'; + infoItem.style.color = '#777'; + Polymer.dom(item).appendChild(infoItem); + } + + // Display the op timing, if available. + if (op.cmd_time && op.cmd_time >= 0.0001) { + const time = document.createElement('span'); + Polymer.dom(time).classList.add('time'); + const rounded = op.cmd_time.toFixed(4); + Polymer.dom(time).textContent = '(' + rounded + 'ms)'; + time.style.fontSize = 'x-small'; + time.style.color = 'rgb(136, 0, 0)'; + Polymer.dom(item).appendChild(time); + } + + item.style.borderBottom = '1px solid #555'; + item.style.fontSize = 'small'; + item.style.fontWeight = 'bold'; + item.style.paddingBottom = '5px'; + item.style.paddingLeft = '5px'; + item.style.cursor = 'pointer'; + + for (const child of item.children) { + child.style.fontWeight = 'normal'; + child.style.marginLeft = '1em'; + child.style.maxWidth = '300px'; + } + + Polymer.dom(this.opsList_).appendChild(item); + } + }, + + onSelectionChanged_(e) { + let beforeSelectedOp = true; + + // Deselect on re-selection. + if (this.opsList_.selectedElement === this.selectedOp_) { + this.opsList_.selectedElement = undefined; + beforeSelectedOp = false; + this.selectedOpIndex_ = undefined; + } + + this.selectedOp_ = this.opsList_.selectedElement; + + // Set selection on all previous ops. + const ops = this.opsList_.children; + for (let i = 0; i < ops.length; i++) { + const op = ops[i]; + if (op === this.selectedOp_) { + beforeSelectedOp = false; + this.selectedOpIndex_ = op.opIndex; + } else if (beforeSelectedOp) { + Polymer.dom(op).setAttribute('beforeSelection', 'beforeSelection'); + op.style.backgroundColor = 'rgb(103, 199, 165)'; + } else { + Polymer.dom(op).removeAttribute('beforeSelection'); + op.style.backgroundColor = ''; + } + } + + tr.b.dispatchSimpleEvent(this, 'selection-changed', false); + }, + + get numOps() { + return this.opsList_.children.length; + }, + + get selectedOpIndex() { + return this.selectedOpIndex_; + }, + + set selectedOpIndex(s) { + this.selectedOpIndex_ = s; + + if (s === undefined) { + this.opsList_.selectedElement = this.selectedOp_; + this.onSelectionChanged_(); + } else { + if (s < 0) throw new Error('Invalid index'); + if (s >= this.numOps) throw new Error('Invalid index'); + this.opsList_.selectedElement = this.opsList_.getElementByIndex(s + 1); + tr.ui.b.scrollIntoViewIfNeeded(this.opsList_.selectedElement); + } + }, + + /** + * Return Skia operations tagged by annotation. + * + * The ops returned from Picture.getOps() contain both Skia ops and + * annotations threaded together. This function removes all annotations + * from the list and tags each op with the associated annotations. + * Additionally, the last {tag, id, class} is stored as elementInfo on + * each op. + * + * @param {Array} ops Array of Skia operations and annotations. + * @return {Array} Skia ops where op.annotations contains the associated + * annotations for a given op. + */ + opsTaggedWithAnnotations_(ops) { + // This algorithm works by walking all the ops and pushing any + // annotations onto a stack. When a non-annotation op is found, the + // annotations stack is traversed and stored with the op. + const annotationGroups = []; + const opsWithoutAnnotations = []; + for (let opIndex = 0; opIndex < ops.length; opIndex++) { + const op = ops[opIndex]; + op.opIndex = opIndex; + switch (op.cmd_string) { + case BEGIN_ANNOTATION: + annotationGroups.push([]); + break; + case END_ANNOTATION: + annotationGroups.pop(); + break; + case ANNOTATION: + annotationGroups[annotationGroups.length - 1].push(op); + break; + default: { + const annotations = []; + let elementInfo = {}; + annotationGroups.forEach(function(annotationGroup) { + elementInfo = {}; + annotationGroup.forEach(function(annotation) { + annotation.info.forEach(function(info) { + if (info.includes(ANNOTATION_TAG)) { + elementInfo.tag = info.substring( + info.indexOf(ANNOTATION_TAG) + + ANNOTATION_TAG.length).toLowerCase(); + } else if (info.includes(ANNOTATION_ID)) { + elementInfo.id = info.substring( + info.indexOf(ANNOTATION_ID) + + ANNOTATION_ID.length); + } else if (info.includes(ANNOTATION_CLASS)) { + elementInfo.class = info.substring( + info.indexOf(ANNOTATION_CLASS) + + ANNOTATION_CLASS.length); + } + + annotations.push(info); + }); + }); + }); + op.annotations = annotations; + op.elementInfo = elementInfo; + opsWithoutAnnotations.push(op); + } + } + } + + return opsWithoutAnnotations; + } + }; + + return { + PictureOpsListView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html new file mode 100644 index 00000000000..b58c1568f4f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_ops_list_view_test.html @@ -0,0 +1,56 @@ +<!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/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_ops_list_view.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const PictureOpsListView = tr.ui.e.chrome.cc.PictureOpsListView; + + test('instantiate', function() { + if (!tr.e.cc.PictureSnapshot.CanRasterize()) return; + + const m = new tr.Model(g_catLTHIEvents); + const p = Object.values(m.processes)[0]; + + const instance = p.objects.getAllInstancesNamed('cc::Picture')[0]; + const snapshot = instance.snapshots[0]; + + const view = new PictureOpsListView(); + view.picture = snapshot; + assert.strictEqual(view.opsList_.children.length, 142); + }); + + test('selection', function() { + if (!tr.e.cc.PictureSnapshot.CanRasterize()) return; + + const m = new tr.Model(g_catLTHIEvents); + const p = Object.values(m.processes)[0]; + + const instance = p.objects.getAllInstancesNamed('cc::Picture')[0]; + const snapshot = instance.snapshots[0]; + + const view = new PictureOpsListView(); + view.picture = snapshot; + let didSelectionChange = 0; + view.addEventListener('selection-changed', function() { + didSelectionChange = true; + }); + assert.isFalse(didSelectionChange); + view.opsList_.selectedElement = view.opsList_.children[3]; + assert.isTrue(didSelectionChange); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html new file mode 100644 index 00000000000..a9db575773f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/picture_view.html @@ -0,0 +1,62 @@ +<!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/extras/chrome/cc/picture.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/picture_debugger.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + /* + * Displays a picture snapshot in a human readable form. + * @constructor + */ + const PictureSnapshotView = tr.ui.b.define( + 'tr-ui-e-chrome-cc-picture-snapshot-view', + tr.ui.analysis.ObjectSnapshotView); + + PictureSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + Polymer.dom(this).classList.add( + 'tr-ui-e-chrome-cc-picture-snapshot-view'); + this.style.display = 'flex'; + this.style.flexGrow = 1; + this.style.flexShrink = 1; + this.style.flexBasis = 'auto'; + this.style.minWidth = 0; + this.pictureDebugger_ = new tr.ui.e.chrome.cc.PictureDebugger(); + this.pictureDebugger_.style.flexGrow = 1; + this.pictureDebugger_.style.flexShrink = 1; + this.pictureDebugger_.style.flexBasis = 'auto'; + this.pictureDebugger_.style.minWidth = 0; + Polymer.dom(this).appendChild(this.pictureDebugger_); + }, + + updateContents() { + if (this.objectSnapshot_ && this.pictureDebugger_) { + this.pictureDebugger_.picture = this.objectSnapshot_; + } + } + }; + + tr.ui.analysis.ObjectSnapshotView.register( + PictureSnapshotView, + { + typeNames: ['cc::Picture', 'cc::LayeredPicture'], + showInstances: false + }); + + return { + PictureSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html new file mode 100644 index 00000000000..6b1a7cb7df0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection.html @@ -0,0 +1,140 @@ +<!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/extras/chrome/cc/raster_task.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/single_event_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + /** + * @constructor + */ + function RasterTaskSelection(selection) { + tr.ui.e.chrome.cc.Selection.call(this); + const whySupported = RasterTaskSelection.whySuported(selection); + if (!whySupported.ok) { + throw new Error('Fail: ' + whySupported.why); + } + this.slices_ = Array.from(selection); + this.tiles_ = this.slices_.map(function(slice) { + const tile = tr.e.cc.getTileFromRasterTaskSlice(slice); + if (tile === undefined) { + throw new Error('This should never happen due to .supports check.'); + } + return tile; + }); + } + + RasterTaskSelection.whySuported = function(selection) { + if (!(selection instanceof tr.model.EventSet)) { + return {ok: false, why: 'Must be selection'}; + } + + if (selection.length === 0) { + return {ok: false, why: 'Selection must be non empty'}; + } + + let referenceSnapshot = undefined; + for (const event of selection) { + if (!(event instanceof tr.model.Slice)) { + return {ok: false, why: 'Not a slice'}; + } + + const tile = tr.e.cc.getTileFromRasterTaskSlice(event); + if (tile === undefined) { + return {ok: false, why: 'No tile found'}; + } + + if (!referenceSnapshot) { + referenceSnapshot = tile.containingSnapshot; + } else { + if (tile.containingSnapshot !== referenceSnapshot) { + return { + ok: false, + why: 'Raster tasks are from different compositor instances' + }; + } + } + } + return {ok: true}; + }; + + RasterTaskSelection.supports = function(selection) { + return RasterTaskSelection.whySuported(selection).ok; + }; + + RasterTaskSelection.prototype = { + __proto__: tr.ui.e.chrome.cc.Selection.prototype, + + get specicifity() { + return 3; + }, + + get associatedLayerId() { + const tile0 = this.tiles_[0]; + const allSameLayer = this.tiles_.every(function(tile) { + tile.layerId === tile0.layerId; + }); + if (allSameLayer) { + return tile0.layerId; + } + return undefined; + }, + + get extraHighlightsByLayerId() { + const highlights = {}; + this.tiles_.forEach(function(tile, i) { + if (highlights[tile.layerId] === undefined) { + highlights[tile.layerId] = []; + } + const slice = this.slices_[i]; + highlights[tile.layerId].push({ + colorKey: slice.title, + rect: tile.layerRect + }); + }, this); + return highlights; + }, + + createAnalysis() { + const sel = new tr.model.EventSet(); + this.slices_.forEach(function(slice) { + sel.push(slice); + }); + + let analysis; + if (sel.length === 1) { + analysis = document.createElement('tr-ui-a-single-event-sub-view'); + } else { + analysis = document.createElement('tr-ui-e-chrome-cc-raster-task-view'); + } + analysis.selection = sel; + return analysis; + }, + + findEquivalent(lthi) { + // Raster tasks are only valid in one LTHI. + return undefined; + }, + + // RasterTaskSelection specific stuff follows. + get containingSnapshot() { + return this.tiles_[0].containingSnapshot; + } + }; + + return { + RasterTaskSelection, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html new file mode 100644 index 00000000000..d95a7135d37 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_selection_test.html @@ -0,0 +1,39 @@ +<!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/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('basic', function() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + const rasterTasks = p.threads[1].sliceGroup.slices.filter(function(slice) { + return slice.title === 'RasterTask'; + }); + + let selection = new tr.model.EventSet(); + selection.push(rasterTasks[0]); + selection.push(rasterTasks[1]); + + assert.isTrue(tr.ui.e.chrome.cc.RasterTaskSelection.supports(selection)); + selection = new tr.ui.e.chrome.cc.RasterTaskSelection(selection); + const highlights = selection.extraHighlightsByLayerId; + assert.lengthOf(Object.keys(highlights), 1); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html new file mode 100644 index 00000000000..a5f7f5d806c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view.html @@ -0,0 +1,222 @@ +<!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/unit.html"> +<link rel="import" href="/tracing/extras/chrome/cc/raster_task.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/selection.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-e-chrome-cc-raster-task-view'> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + #heading { + flex: 0 0 auto; + } + tr-ui-b-table { + font-size: 12px; + } + </style> + + <div id="heading"> + Rasterization costs in + <tr-ui-a-analysis-link id="link"></tr-ui-a-analysis-link> + </div> + <tr-ui-b-table id="content"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; +Polymer({ + is: 'tr-ui-e-chrome-cc-raster-task-view', + + created() { + this.selection_ = undefined; + }, + + set selection(selection) { + this.selection_ = selection; + + this.updateContents_(); + }, + + updateColumns_(hadCpuDurations) { + const timeSpanConfig = { + unit: tr.b.Unit.byName.timeDurationInMs, + ownerDocument: this.ownerDocument + }; + + const columns = [ + { + title: 'Layer', + value(row) { + if (row.isTotals) return 'Totals'; + if (row.layer) { + const linkEl = document.createElement('tr-ui-a-analysis-link'); + linkEl.setSelectionAndContent( + function() { + return new tr.ui.e.chrome.cc.LayerSelection(row.layer); + }, + 'Layer ' + row.layerId); + return linkEl; + } + return 'Layer ' + row.layerId; + }, + width: '250px' + }, + { + title: 'Num Tiles', + value(row) { return row.numTiles; }, + cmp(a, b) { return a.numTiles - b.numTiles; } + }, + { + title: 'Num Analysis Tasks', + value(row) { return row.numAnalysisTasks; }, + cmp(a, b) { + return a.numAnalysisTasks - b.numAnalysisTasks; + } + }, + { + title: 'Num Raster Tasks', + value(row) { return row.numRasterTasks; }, + cmp(a, b) { return a.numRasterTasks - b.numRasterTasks; } + }, + { + title: 'Wall Duration (ms)', + value(row) { + return tr.v.ui.createScalarSpan(row.duration, timeSpanConfig); + }, + cmp(a, b) { return a.duration - b.duration; } + } + ]; + + if (hadCpuDurations) { + columns.push({ + title: 'CPU Duration (ms)', + value(row) { + return tr.v.ui.createScalarSpan(row.cpuDuration, timeSpanConfig); + }, + cmp(a, b) { return a.cpuDuration - b.cpuDuration; } + }); + } + + let colWidthPercentage; + if (columns.length === 1) { + colWidthPercentage = '100%'; + } else { + colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%'; + } + + for (let i = 1; i < columns.length; i++) { + columns[i].width = colWidthPercentage; + } + + this.$.content.tableColumns = columns; + this.$.content.sortColumnIndex = columns.length - 1; + }, + + updateContents_() { + const table = this.$.content; + + if (this.selection_.length === 0) { + this.$.link.setSelectionAndContent(undefined, ''); + table.tableRows = []; + table.footerRows = []; + table.rebuild(); + return; + } + // LTHI link. + const lthi = tr.e.cc.getTileFromRasterTaskSlice( + tr.b.getFirstElement(this.selection_)).containingSnapshot; + this.$.link.setSelectionAndContent(function() { + return new tr.model.EventSet(lthi); + }, lthi.userFriendlyName); + + // Get costs by layer. + const costsByLayerId = {}; + function getCurrentCostsForLayerId(tile) { + const layerId = tile.layerId; + const lthi = tile.containingSnapshot; + let layer; + if (lthi.activeTree) { + layer = lthi.activeTree.findLayerWithId(layerId); + } + if (layer === undefined && lthi.pendingTree) { + layer = lthi.pendingTree.findLayerWithId(layerId); + } + if (costsByLayerId[layerId] === undefined) { + costsByLayerId[layerId] = { + layerId, + layer, + numTiles: 0, + numAnalysisTasks: 0, + numRasterTasks: 0, + duration: 0, + cpuDuration: 0 + }; + } + return costsByLayerId[layerId]; + } + + let totalDuration = 0; + let totalCpuDuration = 0; + let totalNumAnalyzeTasks = 0; + let totalNumRasterizeTasks = 0; + let hadCpuDurations = false; + + const tilesThatWeHaveSeen = {}; + + this.selection_.forEach(function(slice) { + const tile = tr.e.cc.getTileFromRasterTaskSlice(slice); + const curCosts = getCurrentCostsForLayerId(tile); + + if (!tilesThatWeHaveSeen[tile.objectInstance.id]) { + tilesThatWeHaveSeen[tile.objectInstance.id] = true; + curCosts.numTiles += 1; + } + + if (tr.e.cc.isSliceDoingAnalysis(slice)) { + curCosts.numAnalysisTasks += 1; + totalNumAnalyzeTasks += 1; + } else { + curCosts.numRasterTasks += 1; + totalNumRasterizeTasks += 1; + } + curCosts.duration += slice.duration; + totalDuration += slice.duration; + if (slice.cpuDuration !== undefined) { + curCosts.cpuDuration += slice.cpuDuration; + totalCpuDuration += slice.cpuDuration; + hadCpuDurations = true; + } + }); + + // Apply to the table. + this.updateColumns_(hadCpuDurations); + table.tableRows = Object.values(costsByLayerId); + table.rebuild(); + + // Footer. + table.footerRows = [ + { + isTotals: true, + numTiles: Object.keys(tilesThatWeHaveSeen).length, + numAnalysisTasks: totalNumAnalyzeTasks, + numRasterTasks: totalNumRasterizeTasks, + duration: totalDuration, + cpuDuration: totalCpuDuration + } + ]; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html new file mode 100644 index 00000000000..56767cfcc89 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/raster_task_view_test.html @@ -0,0 +1,70 @@ +<!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/event_target.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_view.html"> +<link rel="import" href="/tracing/ui/base/deep_utils.html"> +<link rel="import" href="/tracing/ui/brushing_state_controller.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/layer_tree_host_impl_view.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_selection.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/raster_task_view.html"> + +<script src="/tracing/extras/chrome/cc/layer_tree_host_impl_test_data.js"> +</script> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function createSelection() { + const m = tr.c.TestUtils.newModelWithEvents([g_catLTHIEvents]); + const p = m.processes[1]; + const rasterTasks = p.threads[1].sliceGroup.slices.filter(function(slice) { + return slice.title === 'RasterTask' || slice.title === 'AnalyzeTask'; + }); + + const selection = new tr.model.EventSet(); + selection.model = m; + + selection.push(rasterTasks[0]); + selection.push(rasterTasks[1]); + return selection; + } + + test('basic', function() { + const selection = createSelection(); + const view = document.createElement('tr-ui-e-chrome-cc-raster-task-view'); + view.selection = selection; + this.addHTMLOutput(view); + }); + + test('analysisViewIntegration', function() { + const selection = createSelection(); + + const timelineView = {model: selection.model}; + const brushingStateController = + new tr.c.BrushingStateController(timelineView); + + const analysisEl = document.createElement('tr-ui-a-analysis-view'); + analysisEl.brushingStateController = brushingStateController; + brushingStateController.changeSelectionFromTimeline(selection); + + assert.isDefined(Polymer.dom(analysisEl).querySelector( + 'tr-ui-e-chrome-cc-raster-task-view')); + + const sv = tr.ui.b.findDeepElementMatching( + analysisEl, 'tr-ui-a-multi-thread-slice-sub-view'); + assert.isTrue(sv.requiresTallView); + this.addHTMLOutput(analysisEl); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html new file mode 100644 index 00000000000..2794540e115 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/selection.html @@ -0,0 +1,304 @@ +<!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/analysis/generic_object_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + function Selection() { + this.selectionToSetIfClicked = undefined; + } + Selection.prototype = { + /** + * When two things are picked in the UI, one must occasionally tie-break + * between them to decide what was really clicked. Things with higher + * specicifity will win. + */ + get specicifity() { + throw new Error('Not implemented'); + }, + + /** + * If a selection is related to a specific layer, then this returns the + * layerId of that layer. If the selection is not related to a layer, for + * example if the device viewport is selected, then this returns undefined. + */ + get associatedLayerId() { + throw new Error('Not implemented'); + }, + + /** + * If a selection is related to a specific render pass, then this returns + * the layerId of that layer. If the selection is not related to a layer, + * for example if the device viewport is selected, then this returns + * undefined. + */ + get associatedRenderPassId() { + throw new Error('Not implemented'); + }, + + + get highlightsByLayerId() { + return {}; + }, + + /** + * Called when the selection is made active in the layer view. Must return + * an HTMLElement that explains this selection in detail. + */ + createAnalysis() { + throw new Error('Not implemented'); + }, + + /** + * Should try to create the equivalent selection in the provided LTHI, + * or undefined if it can't be done. + */ + findEquivalent(lthi) { + throw new Error('Not implemented'); + } + }; + + /** + * @constructor + */ + function RenderPassSelection(renderPass, renderPassId) { + if (!renderPass || (renderPassId === undefined)) { + throw new Error('Render pass (with id) is required'); + } + this.renderPass_ = renderPass; + this.renderPassId_ = renderPassId; + } + + RenderPassSelection.prototype = { + __proto__: Selection.prototype, + + get specicifity() { + return 1; + }, + + get associatedLayerId() { + return undefined; + }, + + get associatedRenderPassId() { + return this.renderPassId_; + }, + + get renderPass() { + return this.renderPass_; + }, + + createAnalysis() { + const dataView = document.createElement( + 'tr-ui-a-generic-object-view-with-label'); + dataView.label = 'RenderPass ' + this.renderPassId_; + dataView.object = this.renderPass_.args; + return dataView; + }, + + get title() { + return this.renderPass_.objectInstance.typeName; + } + }; + + /** + * @constructor + */ + function LayerSelection(layer) { + if (!layer) { + throw new Error('Layer is required'); + } + this.layer_ = layer; + } + + LayerSelection.prototype = { + __proto__: Selection.prototype, + + get specicifity() { + return 1; + }, + + get associatedLayerId() { + return this.layer_.layerId; + }, + + get associatedRenderPassId() { + return undefined; + }, + + get layer() { + return this.layer_; + }, + + createAnalysis() { + const dataView = document.createElement( + 'tr-ui-a-generic-object-view-with-label'); + dataView.label = 'Layer ' + this.layer_.layerId; + if (this.layer_.usingGpuRasterization) { + dataView.label += ' (GPU-rasterized)'; + } + dataView.object = this.layer_.args; + return dataView; + }, + + get title() { + return this.layer_.objectInstance.typeName; + }, + + findEquivalent(lthi) { + const layer = lthi.activeTree.findLayerWithId(this.layer_.layerId) || + lthi.pendingTree.findLayerWithId(this.layer_.layerId); + if (!layer) return undefined; + return new LayerSelection(layer); + } + }; + + /** + * @constructor + */ + function TileSelection(tile, opt_data) { + this.tile_ = tile; + this.data_ = opt_data || {}; + } + + TileSelection.prototype = { + __proto__: Selection.prototype, + + get specicifity() { + return 2; + }, + + get associatedLayerId() { + return this.tile_.layerId; + }, + + get highlightsByLayerId() { + const highlights = {}; + highlights[this.tile_.layerId] = [ + { + colorKey: this.tile_.objectInstance.typeName, + rect: this.tile_.layerRect + } + ]; + return highlights; + }, + + createAnalysis() { + const analysis = document.createElement( + 'tr-ui-a-generic-object-view-with-label'); + analysis.label = 'Tile ' + this.tile_.objectInstance.id + ' on layer ' + + this.tile_.layerId; + if (this.data_) { + analysis.object = { + moreInfo: this.data_, + tileArgs: this.tile_.args + }; + } else { + analysis.object = this.tile_.args; + } + return analysis; + }, + + findEquivalent(lthi) { + const tileInstance = this.tile_.tileInstance; + if (lthi.ts < tileInstance.creationTs || + lthi.ts >= tileInstance.deletionTs) { + return undefined; + } + const tileSnapshot = tileInstance.getSnapshotAt(lthi.ts); + if (!tileSnapshot) return undefined; + return new TileSelection(tileSnapshot); + } + }; + + /** + * @constructor + */ + function LayerRectSelection(layer, rectType, rect, opt_data) { + this.layer_ = layer; + this.rectType_ = rectType; + this.rect_ = rect; + this.data_ = opt_data !== undefined ? opt_data : rect; + } + + LayerRectSelection.prototype = { + __proto__: Selection.prototype, + + get specicifity() { + return 2; + }, + + get associatedLayerId() { + return this.layer_.layerId; + }, + + + get highlightsByLayerId() { + const highlights = {}; + highlights[this.layer_.layerId] = [ + { + colorKey: this.rectType_, + rect: this.rect_ + } + ]; + return highlights; + }, + + createAnalysis() { + const analysis = document.createElement( + 'tr-ui-a-generic-object-view-with-label'); + analysis.label = this.rectType_ + ' on layer ' + this.layer_.layerId; + analysis.object = this.data_; + return analysis; + }, + + findEquivalent(lthi) { + return undefined; + } + }; + + /** + * @constructor + */ + function AnimationRectSelection(layer, rect) { + this.layer_ = layer; + this.rect_ = rect; + } + + AnimationRectSelection.prototype = { + __proto__: Selection.prototype, + + get specicifity() { + return 0; + }, + + get associatedLayerId() { + return this.layer_.layerId; + }, + + createAnalysis() { + const analysis = document.createElement( + 'tr-ui-a-generic-object-view-with-label'); + analysis.label = 'Animation Bounds of layer ' + this.layer_.layerId; + analysis.object = this.rect_; + return analysis; + } + }; + + return { + Selection, + RenderPassSelection, + LayerSelection, + TileSelection, + LayerRectSelection, + AnimationRectSelection, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html new file mode 100644 index 00000000000..ad1f633f334 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/cc/tile_view.html @@ -0,0 +1,56 @@ +<!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/extras/chrome/cc/tile.html"> +<link rel="import" href="/tracing/ui/analysis/generic_object_view.html"> +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> + +<script> + +'use strict'; + +tr.exportTo('tr.ui.e.chrome.cc', function() { + /* + * Displays a tile in a human readable form. + * @constructor + */ + const TileSnapshotView = tr.ui.b.define( + 'tr-ui-e-chrome-cc-tile-snapshot-view', + tr.ui.analysis.ObjectSnapshotView); + + TileSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + Polymer.dom(this).classList.add('tr-ui-e-chrome-cc-tile-snapshot-view'); + this.layerTreeView_ = + new tr.ui.e.chrome.cc.LayerTreeHostImplSnapshotView(); + Polymer.dom(this).appendChild(this.layerTreeView_); + }, + + updateContents() { + const tile = this.objectSnapshot_; + const layerTreeHostImpl = tile.containingSnapshot; + if (!layerTreeHostImpl) return; + + this.layerTreeView_.objectSnapshot = layerTreeHostImpl; + this.layerTreeView_.selection = new tr.ui.e.chrome.cc.TileSelection(tile); + } + }; + + tr.ui.analysis.ObjectSnapshotView.register( + TileSnapshotView, + { + typeName: 'cc::Tile', + showInTrackView: false + }); + + return { + TileSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html new file mode 100644 index 00000000000..af6c79447bc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/codesearch.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<!-- +Copyright 2018 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"> + +<dom-module id='tr-ui-e-chrome-codesearch'> + <template> + <style> + :host { + white-space: nowrap; + } + #codesearchLink { + font-size: x-small; + margin-left: 20px; + text-decoration: none; + } + </style> + <a id="codesearchLink" target=_blank on-click="onClick">🔍</a> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome', function() { + Polymer({ + is: 'tr-ui-e-chrome-codesearch', + + set searchPhrase(phrase) { + const link = Polymer.dom(this.$.codesearchLink); + const codeSearchURL = + 'https://cs.chromium.org/search/?sq=package:chromium&type=cs&q='; + link.setAttribute('href', codeSearchURL + encodeURIComponent(phrase)); + }, + + onClick(clickEvent) { + // Let the event trigger the default action of following the link. Stop + // the propagation of the event here, so that subsequent handlers do not + // intercept the clicks. + clickEvent.stopPropagation(); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html new file mode 100644 index 00000000000..ec7991b6640 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/gpu.html @@ -0,0 +1,10 @@ +<!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/extras/chrome/gpu/gpu_async_slice.html"> +<link rel="import" href="/tracing/extras/chrome/gpu/state.html"> +<link rel="import" href="/tracing/ui/extras/chrome/gpu/state_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png Binary files differnew file mode 100644 index 00000000000..8ea9bc726bb --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/images/checkerboard.png diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css new file mode 100644 index 00000000000..7c2c34787dc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.css @@ -0,0 +1,15 @@ +/* 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. + */ + +.tr-ui-e-chrome-gpu-state-snapshot-view { + background: url('./images/checkerboard.png'); + display: flex; + overflow: auto; +} + +.tr-ui-e-chrome-gpu-state-snapshot-view img { + display: block; + margin: 16px auto 16px auto; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html new file mode 100644 index 00000000000..ba6c345be5f --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/gpu/state_view.html @@ -0,0 +1,48 @@ +<!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="stylesheet" href="/tracing/ui/extras/chrome/gpu/state_view.css"> + +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.chrome.gpu', function() { + /* + * Displays a GPU state snapshot in a human readable form. + * @constructor + */ + const StateSnapshotView = tr.ui.b.define( + 'tr-ui-e-chrome-gpu-state-snapshot-view', + tr.ui.analysis.ObjectSnapshotView); + + StateSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + Polymer.dom(this).classList.add('tr-ui-e-chrome-gpu-state-snapshot-view'); + this.screenshotImage_ = document.createElement('img'); + Polymer.dom(this).appendChild(this.screenshotImage_); + }, + + updateContents() { + if (this.objectSnapshot_ && this.objectSnapshot_.screenshot) { + this.screenshotImage_.src = 'data:image/png;base64,' + + this.objectSnapshot_.screenshot; + } + } + }; + tr.ui.analysis.ObjectSnapshotView.register( + StateSnapshotView, + {typeName: 'gpu::State'}); + + return { + StateSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html new file mode 100644 index 00000000000..db80ef75afa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome/layout_tree_sub_view.html @@ -0,0 +1,229 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/chrome/layout_tree.html"> +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> + +<dom-module id='tr-ui-a-layout-tree-sub-view'> + <template> + <style> + tr-ui-b-table { + font-size: 12px; + } + </style> + <div id="content"></div> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.analysis', function() { + Polymer({ + is: 'tr-ui-a-layout-tree-sub-view', + behaviors: ['tr-ui-a-sub-view'], + + set selection(selection) { + this.currentSelection_ = selection; + this.updateContents_(); + }, + + get selection() { + return this.currentSelection_; + }, + + updateContents_() { + this.set('$.content.textContent', ''); + if (!this.currentSelection_) return; + + const columns = [ + { + title: 'Tag/Name', + value(layoutObject) { + return layoutObject.tag || ':' + layoutObject.name; + } + }, + + { + title: 'htmlId', + value(layoutObject) { + return layoutObject.htmlId || ''; + } + }, + + { + title: 'classNames', + value(layoutObject) { + return layoutObject.classNames || ''; + } + }, + + { + title: 'reasons', + value(layoutObject) { + return layoutObject.needsLayoutReasons.join(', '); + } + }, + + { + title: 'width', + value(layoutObject) { + return layoutObject.absoluteRect.width; + } + }, + + { + title: 'height', + value(layoutObject) { + return layoutObject.absoluteRect.height; + } + }, + + { + title: 'absX', + value(layoutObject) { + return layoutObject.absoluteRect.left; + } + }, + + { + title: 'absY', + value(layoutObject) { + return layoutObject.absoluteRect.top; + } + }, + + { + title: 'relX', + value(layoutObject) { + return layoutObject.relativeRect.left; + } + }, + + { + title: 'relY', + value(layoutObject) { + return layoutObject.relativeRect.top; + } + }, + + { + title: 'float', + value(layoutObject) { + return layoutObject.isFloat ? 'float' : ''; + } + }, + + { + title: 'positioned', + value(layoutObject) { + return layoutObject.isPositioned ? 'positioned' : ''; + } + }, + + { + title: 'relative', + value(layoutObject) { + return layoutObject.isRelativePositioned ? 'relative' : ''; + } + }, + + { + title: 'sticky', + value(layoutObject) { + return layoutObject.isStickyPositioned ? 'sticky' : ''; + } + }, + + { + title: 'anonymous', + value(layoutObject) { + return layoutObject.isAnonymous ? 'anonymous' : ''; + } + }, + + { + title: 'row', + value(layoutObject) { + if (layoutObject.tableRow === undefined) { + return ''; + } + return layoutObject.tableRow; + } + }, + + { + title: 'col', + value(layoutObject) { + if (layoutObject.tableCol === undefined) { + return ''; + } + return layoutObject.tableCol; + } + }, + + { + title: 'rowSpan', + value(layoutObject) { + if (layoutObject.tableRowSpan === undefined) { + return ''; + } + return layoutObject.tableRowSpan; + } + }, + + { + title: 'colSpan', + value(layoutObject) { + if (layoutObject.tableColSpan === undefined) { + return ''; + } + return layoutObject.tableColSpan; + } + }, + + { + title: 'address', + value(layoutObject) { + return layoutObject.id.toString(16); + } + } + ]; + + const table = this.ownerDocument.createElement('tr-ui-b-table'); + table.defaultExpansionStateCallback = function( + layoutObject, parentLayoutObject) { + return true; + }; + table.subRowsPropertyName = 'childLayoutObjects'; + table.tableColumns = columns; + table.tableRows = this.currentSelection_.map(function(snapshot) { + return snapshot.rootLayoutObject; + }); + table.rebuild(); + Polymer.dom(this.$.content).appendChild(table); + }, + }); + + return {}; +}); +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-layout-tree-sub-view', + tr.e.chrome.LayoutTreeSnapshot, + { + multi: false, + title: 'Layout Tree', + }); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-a-layout-tree-sub-view', + tr.e.chrome.LayoutTreeSnapshot, + { + multi: true, + title: 'Layout Trees', + }); +</script> + diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html new file mode 100644 index 00000000000..cea42a2d78c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/chrome_config.html @@ -0,0 +1,33 @@ +<!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. +--> + +<!-- +The chrome config is heavily used: + - chrome://tracing, + - trace2html, which in turn implies + - adb_profile_chrome + - telemetry +--> + +<!-- +TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the +same way that all non-UI files depend on tracing/base/base.html. Enforce this +dependency with a presubmit. +--> +<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order> + +<link rel="import" href="/tracing/extras/chrome_config.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/extras/chrome/cc/cc.html"> +<link rel="import" href="/tracing/ui/extras/chrome/codesearch.html"> +<link rel="import" href="/tracing/ui/extras/chrome/gpu/gpu.html"> +<link rel="import" href="/tracing/ui/extras/chrome/layout_tree_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/frame_data_side_panel.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/input_latency_side_panel.html"> +<link rel="import" href="/tracing/ui/extras/system_stats/system_stats.html"> +<link rel="import" href="/tracing/ui/extras/v8_config.html"> +<link rel="import" href="/tracing/ui/timeline_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html new file mode 100644 index 00000000000..cb3d41a88a0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/html_results.html @@ -0,0 +1,123 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/table.html"> + +<!-- +This class tries to (simply) copy the telemetry Results object, but outputs +directly to an HTML table. It takes things that look like Telemetry values, +and updates the table internally. +--> +<dom-module id='tr-ui-e-deep-reports-html-results'> + <template> + <style> + :host { + display: flex; + font-size: 12px; + } + </style> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-deep-reports-html-results', + + created() { + this.hasColumnNamed_ = {}; + this.pageToRowMap_ = new WeakMap(); + }, + + ready() { + const table = this.$.table; + table.tableColumns = [ + { + title: 'Label', + value(row) { return row.label; }, + width: '350px' + } + ]; + this.clear(); + }, + + clear() { + this.$.table.tableRows = []; + }, + + addColumnIfNeeded_(columnName) { + if (this.hasColumnNamed_[columnName]) return; + + this.hasColumnNamed_[columnName] = true; + + const column = { + title: columnName, + value(row) { + if (row[columnName] === undefined) return ''; + return row[columnName]; + } + }; + + const columns = this.$.table.tableColumns; + columns.push(column); + + // Update widths. + let colWidthPercentage; + if (columns.length === 1) { + colWidthPercentage = '100%'; + } else { + colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%'; + } + + for (let i = 1; i < columns.length; i++) { + columns[i].width = colWidthPercentage; + } + + this.$.table.tableColumns = columns; + }, + + getRowForPage_(page) { + if (!this.pageToRowMap_.has(page)) { + const i = page.url.lastIndexOf('/'); + const baseName = page.url.substring(i + 1); + + const link = document.createElement('a'); + link.href = 'trace_viewer.html#' + page.url; + Polymer.dom(link).textContent = baseName; + + const row = { + label: link, + value: '', + subRows: [], + isExpanded: true + }; + this.$.table.tableRows.push(row); + this.pageToRowMap_.set(page, row); + + // Kick table rebuild. + this.$.table.tableRows = this.$.table.tableRows; + } + return this.pageToRowMap_.get(page); + }, + + addValue(value) { + /* Value is expected to be a scalar telemetry-style Value. */ + if (value.type !== 'scalar') { + throw new Error('wat'); + } + + this.addColumnIfNeeded_(value.name); + const rowForPage = this.getRowForPage_(value.page); + rowForPage[value.name] = value.value; + + // Kick table rebuild. + this.$.table.tableRows = this.$.table.tableRows; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html new file mode 100644 index 00000000000..9cf2d7f6c79 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/main.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/utils.html"> +<link rel="import" href="/tracing/base/xhr.html"> +<link rel="import" href="/tracing/ui/extras/deep_reports/scalar_value.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.deep_reports', function() { + /** + * Runs deep reports on the provided files, and pushes telemetry-style + * values to the results object. + */ + function main(results, filesInDir) { + let lastP = new Promise(function(resolve) { resolve(); }); + + filesInDir.forEach(function(filename) { + // TODO(nduca): Make this like telemetry page. + const page = { + url: filename + }; + lastP = lastP.then(function() { + return loadModelFromFileAsync(filename); + }); + lastP = lastP.then(function(model) { + processModel(results, page, model); + }); + }); + return lastP; + } + + function loadModelFromFileAsync(filename) { + return tr.b.getAsync(filename).then(function(trace) { + const io = new tr.ImportOptions(); + io.shiftWorldToZero = true; + io.pruneEmptyContainers = false; + + const m = new tr.Model(); + try { + m.importTraces([trace], io); + } catch (e) { + throw new Error('While loading ' + filename + ' got: ' + e.toString()); + } + return m; + }); + } + + function processModel(results, page, model) { + results.addValue( + new tr.ui.e.deep_reports.ScalarValue( + page, 'numRailIRs', 'ms', model.userModel.expectations.length)); + } + + return { + main + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html new file mode 100644 index 00000000000..cb550c35b4b --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/deep_reports/scalar_value.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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.deep_reports', function() { + function ScalarValue(page, name, units, value, + opt_important, opt_description) { + this.type = 'scalar'; + this.page = page; + this.name = name; + this.units = units; + this.value = value; + this.important = opt_important !== undefined ? opt_important : false; + this.description = opt_description || ''; + } + ScalarValue.fromDict = function(page, dict) { + if (dict.type !== 'scalar') { + throw new Error('wat'); + } + const v = new ScalarValue(page, dict.name, dict.units, dict.value); + v.important = dict.important; + v.description = dict.description; + v.value = dict.value; + return v; + }; + + ScalarValue.prototype = { + + }; + + return { + ScalarValue, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html new file mode 100644 index 00000000000..1748dd12b88 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comment_element.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 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. +--> + +<dom-module id='tr-ui-e-drive-comment-element'> + <template> + <style> + :host { + display: block; + } + #comment-area { + display: flex; + flex-direction: column; + border-top: 1px solid #e8e8e8; + background-color: white; + padding: 6px; + margin-bottom: 4px; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + border-radius: 2px; + font-size: small; + } + #comment-header { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 8px; + } + #comment-header-text { + display: flex; + flex-direction: column; + padding-left: 10px; + } + #comment-img { + width: 32px; + height: 32px; + } + #comment-text-author { + padding-bottom: 2px; + } + #comment-date { + color: #777; + font-size: 11px; + } + #comment-content { + word-wrap: break-word; + } + </style> + <div id="comment-area"> + <div id="comment-header"> + <img id="comment-img" src="{{ comment.author.picture.url }}" /> + <div id="comment-header-text"> + <div id="comment-text-author">{{ comment.author.displayName }}</div> + <div id="comment-date">{{ createdDate }}</div> + </div> + </div> + <div id="comment-content">{{_computeCommentContentPrefix( comment)}} + {{ comment.content }}</div> + </div> + </template> +</dom-module> +<script> +'use strict'; +Polymer({ + is: 'tr-ui-e-drive-comment-element', + + properties: { + comment: { + type: String, + observer: '_commentChanged' + } + }, + + _commentChanged() { + this.createdDate = new Date(this.comment.createdDate).toLocaleString(); + }, + + _computeCommentContentPrefix(comment) { + return comment.anchor ? '⚓ ' : ''; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html new file mode 100644 index 00000000000..8f52839029d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel.html @@ -0,0 +1,185 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 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/drive/comment_element.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> + +<dom-module id='tr-ui-e-drive-comments-side-panel'> + <template> + <style> + :host { + flex-direction: column; + display: flex; + width: 290px; + overflow-y: scroll; + overflow-x: hidden; + background-color: #eee; + } + toolbar { + flex: 0 0 auto; + border-bottom: 1px solid black; + display: flex; + } + result-area { + flex: 1 1 auto; + display: block; + min-height: 0; + padding: 4px; + } + #comments-textarea-container { + display: flex; + } + #commentinput { + width: 100%; + } + </style> + + <toolbar id='toolbar'></toolbar> + <result-area id='result_area'> + <template is="dom-repeat" items="{{comments_}}" repeat="{{ comment in comments_ }}"> + <tr-ui-e-drive-comment-element comment="{{comment}}" + on-click="commentClick"> + </tr-ui-e-drive-comment-element> + </template> + <div id="comments-textarea-container"> + <textarea id="commentinput" on-focus='textAreaFocus' + on-blur='textAreaBlur' + on-keypress="textareaKeypress"></textarea> + </div> + </result-area> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-drive-comments-side-panel', + behaviors: [tr.ui.behaviors.SidePanel], + + ready() { + this.rangeOfInterest_ = new tr.b.math.Range(); + this.selection_ = undefined; + this.comments_ = []; + this.annotationFromComment_ = undefined; + this.textAreaFocused = false; + }, + + setCommentProvider(commentProvider) { + this.commentProvider_ = commentProvider; + }, + + attached() { + if (this.commentProvider_ === undefined) { + this.commentProvider_ = + new tr.ui.e.drive.analysis.DefaultCommentProvider(); + } + this.commentProvider_.attachToElement(this); + }, + + detached() { + this.commentProvider_.detachFromElement(); + }, + + commentClick(event) { + const anchor = event.currentTarget.comment.anchor; + if (!anchor) return; + + const uiState = + JSON.parse(anchor).a[0][tr.ui.e.drive.constants.ANCHOR_NAME]; + + const myEvent = new CustomEvent('navigateToUIState', { detail: + new tr.ui.b.UIState(new tr.model.Location(uiState.location.xWorld, + uiState.location.yComponents), + uiState.scaleX) + }); + document.dispatchEvent(myEvent); + + if (this.annotationFromComment_) { + this.model.removeAnnotation(this.annotationFromComment_); + } + const loc = new tr.model.Location(uiState.location.xWorld, + uiState.location.yComponents); + + const text = sender.comment.author.displayName + ': ' + + sender.comment.content; + this.annotationFromComment_ = + new tr.model.CommentBoxAnnotation(loc, text); + this.model.addAnnotation(this.annotationFromComment_); + }, + + textareaKeypress(event) { + // Check for return key. + if (event.keyCode === 13 && !event.ctrlKey) { + this.commentProvider_.addComment(this.$.commentinput.value); + this.$.commentinput.value = ''; + } + event.stopPropagation(); + return true; + }, + + textAreaFocus(event) { + this.textAreaFocused = true; + }, + + textAreaBlur(event) { + this.textAreaFocused = false; + }, + + get rangeOfInterest() { + return this.rangeOfInterest_; + }, + + set rangeOfInterest(rangeOfInterest) { + this.rangeOfInterest_ = rangeOfInterest; + this.updateContents_(); + }, + + get currentRangeOfInterest() { + if (this.rangeOfInterest_.isEmpty) { + return this.model_.bounds; + } + return this.rangeOfInterest_; + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.model_ = model; + this.updateContents_(); + }, + + set selection(selection) { + this.selection_ = selection; + }, + + updateContents_() { + this.commentProvider_.updateComments(); + }, + + supportsModel(m) { + if (m === undefined) { + return { + supported: false, + reason: 'Unknown tracing model' + }; + } + return { + supported: true + }; + }, + + get textLabel() { + return 'Comments'; + } +}); + +tr.ui.side_panel.SidePanelRegistry.register(function() { + return document.createElement('tr-ui-e-drive-comments-side-panel'); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html new file mode 100644 index 00000000000..639c1b9b597 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/comments_side_panel_test.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 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/test_utils.html"> +<link rel="import" href="/tracing/ui/extras/drive/comments_side_panel.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + function StubCommentProvider() { + this.addDummyComment('Lorem ipsum dolor sit amet'); + this.addDummyComment('consectetur adipiscing elit'); + this.addDummyComment('sed do eiusmod tempor incididunt ut labore et ' + + 'dolore magna aliqua. Ut enim ad minim veniam, quis nostrud ' + + 'exercitation ullamco laboris nisi ut aliquip ex ea commodo ' + + 'consequat. Duis aute irure dolor in reprehenderit in voluptate ' + + 'velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint ' + + 'occaecat cupidatat non proident, sunt in culpa qui officia deserunt ' + + 'mollit anim id est laborum.'); + } + + StubCommentProvider.prototype = { + comments_: [], + + attachToElement(attachedElement) { + this.attachedElement_ = attachedElement; + this.updateComments(); + }, + + detachFromElement() { + }, + + updateComments() { + this.attachedElement_.comments_ = this.comments_; + }, + + addDummyComment(content) { + const newComment = { + author: { + displayName: 'Casper the Friendly Ghost', + picture: { + url: 'https://lh3.googleusercontent.com/-XdUIqdMkCWA/' + + 'AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/s128/photo.jpg' + } + }, + createdDate: Date.now(), + anchor: (this.comments_.length) % 2 ? 1 : 0, + content + }; + + this.comments_.push(newComment); + }, + + addComment(body) { + this.addDummyComment(body); + this.updateComments(); + } + }; + + test('instantiate', function() { + const panel = document.createElement('tr-ui-e-drive-comments-side-panel'); + panel.setCommentProvider(new StubCommentProvider); + this.addHTMLOutput(panel); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html new file mode 100644 index 00000000000..42d3706f854 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/drive_comment_provider.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 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/model/comment_box_annotation.html"> + +<link rel="import" href="/tracing/ui/extras/drive/comments_side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> + +<script> +'use strict'; + +(function() { + function addDriveCommentWithUIState_(text, uiState) { + gapi.client.load('drive', 'v2', function() { + const request = gapi.client.drive.revisions.get({ + 'fileId': tr.ui.e.drive.getDriveFileId(), + 'revisionId': 'head' + }); + request.execute(function(resp) { + const anchorObject = {}; + anchorObject[tr.ui.e.drive.constants.ANCHOR_NAME] = uiState; + let anchor = { + 'r': resp.id, + 'a': [anchorObject] + }; + anchor = JSON.stringify(anchor); + gapi.client.load('drive', 'v2', function() { + const request = gapi.client.drive.comments.insert({ + 'fileId': tr.ui.e.drive.getDriveFileId(), + 'resource': {'content': text, anchor} + }); + request.execute(); + }); + }); + }); + } + + function onCommentWithUIState(e) { + addDriveCommentWithUIState_(e.detail.name, e.detail.location); + } + + document.addEventListener('commentWithUIState', + onCommentWithUIState.bind(this)); +}()); + +tr.exportTo('tr.ui.e.drive.analysis', function() { + function DefaultCommentProvider() { } + + DefaultCommentProvider.prototype = { + attachToElement(attachedElement) { + this.attachedElement_ = attachedElement; + this.commentsCheckTimer_ = setTimeout(this.checkForComments_.bind(this), + 5000); + }, + + detachFromElement() { + clearTimeout(this.commentsCheckTimer_); + }, + + checkForComments_() { + this.updateComments(); + this.commentsCheckTimer_ = setTimeout(this.checkForComments_.bind(this), + 5000); + }, + + updateComments() { + gapi.client.load('drive', 'v2', () => { + const request = gapi.client.drive.comments.list({ + 'fileId': tr.ui.e.drive.getDriveFileId() + }); + request.execute(results => { + this.attachedElement_.comments_ = results.items; + }); + }); + }, + + addComment(body) { + gapi.client.load('drive', 'v2', () => { + const request = gapi.client.drive.comments.insert({ + 'fileId': tr.ui.e.drive.getDriveFileId(), + 'resource': {'content': body} + }); + request.execute(resp => { + this.updateComments(); + }); + }); + } + }; + + return { + DefaultCommentProvider, + }; +}); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html new file mode 100644 index 00000000000..270dcccf2fc --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/drive/index.html @@ -0,0 +1,463 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 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. +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + + <script type="text/javascript" src="https://apis.google.com/js/api.js"></script> + + <link rel="import" href="/components/polymer/polymer.html"> + <link rel="import" href="/tracing/ui/extras/drive/drive_comment_provider.html"> + <link rel="import" href="/tracing/ui/extras/full_config.html"> + <link rel="import" href="/tracing/ui/timeline_view.html"> + + <style> + body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + } + body > x-timeline-view { + flex: 1 1 auto; + overflow: hidden; + position: absolute; + top: 0px; + bottom: 0; + left: 0; + right: 0; + } + body > x-timeline-view:focus { + outline: none; + } + nav { + display: flex; + flex-direction: row; + justify-content: flex-end; + } + #navbar button { + height: 24px; + padding-bottom: 3px; + vertical-align: middle; + box-shadow: none; + background-color: #4d90fe; + background-image: -webkit-linear-gradient(top,#4d90fe,#4787ed); + border: 1px solid #3079ed; + color: #fff; + border-radius: 2px; + cursor: default; + font-size: 11px; + font-weight: bold; + text-align: center; + white-space: nowrap; + line-height: 27px; + min-width: 54px; + outline: 0px; + padding: 0 8px; + font: normal 13px arial,sans-serif; + margin: 5px; + } + #collabs { + display: flex; + flex-direction: row; + } + .collaborator-div { + display: inline-block; + vertical-align: middle; + min-height: 0; + width: 100px; + font-size: 11px; + font-weight: bold; + font: normal 13px arial,sans-serif; + margin: 10px; + } + .collaborator-img { + margin: 2px; + } + .collaborator-tooltip { + z-index: 10000; + transition: visibility 0,opacity .13s ease-in; + background-color: #2a2a2a; + border: 1px solid #fff; + color: #fff; + cursor: default; + display: block; + font-family: arial, sans-serif; + font-size: 11px; + font-weight: bold; + margin-left: -1px; + opacity: 1; + padding: 7px 9px; + word-break: break-word; + position: absolute; + } + .collaborator-tooltip-content { + color: #fff; + } + .collaborator-tooltip-arrow { + position: absolute; + top: -6px; + } + .collaborator-tooltip-arrow-before { + border-color: #fff transparent !important; + left: -6px; + border: 6px solid; + border-top-width: 0; + content: ''; + display: block; + height: 0; + position: absolute; + width: 0; + } + .collaborator-tooltip-arrow-after { + top: 1px; + border-color: #2a2a2a transparent !important; + left: -5px; + border: 5px solid; + border-top-width: 0; + content: ''; + display: block; + height: 0; + position: absolute; + width: 0; + } + + </style> + <title>Trace Viewer</title> +</head> +<body> + <nav id="navbar"> + <div id="collabs"></div> + <button id="x-drive-save-to-disk">Save to disk</button> + <button id="x-drive-save-to-drive">Save to Drive</button> + <button id="x-drive-load-from-drive">Load from Drive</button> + <button id="x-drive-share">Share</button> + </nav> + <x-timeline-view> + </x-timeline-view> + + <script> + 'use strict'; + + // Needs to be global as it's passed through the Google API as a + // GET parameter. + let onAPIClientLoaded_ = null; + + (function() { + tr.exportTo('tr.ui.e.drive', function() { + const appId = '239864068844'; + const constants = { + APP_ID: appId, + ANCHOR_NAME: appId + '.trace_viewer', + DEVELOPER_KEY: 'AIzaSyDR-6_wL9vHg1_oz4JHk8IQAkv2_Y0Y8-M', + CLIENT_ID: '239864068844-c7gefbfdcp0j6grltulh2r88tsvl18c1.apps.' + + 'googleusercontent.com', + SCOPE: [ + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/drive.install', + 'https://www.googleapis.com/auth/drive.file', + 'profile' + ] + }; + + return { + getDriveFileId() { return driveFileId_; }, + constants + }; + }); + + + let pickerApiLoaded_ = false; + let oauthToken_ = null; + + let timelineViewEl_ = null; + let driveDocument_ = null; + let shareClient_ = null; + let fileIdToLoad_ = null; + let driveFileId_ = null; + + function parseGETParameter(val) { + let result = null; + let tmp = []; + location.search.substr(1).split('&').forEach(function(item) { + tmp = item.split('='); + if (tmp[0] === val) { + result = decodeURIComponent(tmp[1]); + } + }); + return result; + } + + // Use the Google API Loader script to load the google.picker script. + onAPIClientLoaded_ = function() { + const driveState = parseGETParameter('state'); + if (driveState !== null) { + const driveStateJson = JSON.parse(driveState); + fileIdToLoad_ = String(driveStateJson.ids); + } + + gapi.load('picker', {'callback': onPickerApiLoad}); + gapi.load('auth', {'callback'() { + onAuthApiLoad(true, onAuthResultSuccess); + return tr.b.timeout(30e3) + .then(() => onAuthApiLoad(true, function() {})) + .then(() => tr.b.timeout(30e3)) + .then(() => onRepeatAuthApiLoad); + }}); + }; + + function onAuthApiLoad(tryImmediate, resultCallback) { + window.gapi.auth.authorize( + {'client_id': tr.ui.e.drive.constants.CLIENT_ID, + 'scope': tr.ui.e.drive.constants.SCOPE, 'immediate': tryImmediate}, + function(authResult) { + handleAuthResult(authResult, tryImmediate, resultCallback); + }); + } + + function onPickerApiLoad() { + pickerApiLoaded_ = true; + if (fileIdToLoad_ === null) { + createPicker(); + } + } + + function onAuthResultSuccess() { + if (fileIdToLoad_ === null) { + createPicker(); + } else { + loadFileFromDrive(fileIdToLoad_); + } + } + + function handleAuthResult(authResult, wasImmediate, resultCallback) { + if (authResult && !authResult.error) { + oauthToken_ = authResult.access_token; + resultCallback(); + } else if (wasImmediate) { + onAuthApiLoad(false); + } + } + + function createPicker() { + if (pickerApiLoaded_ && oauthToken_) { + const view = new google.picker.View(google.picker.ViewId.DOCS); + view.setMimeTypes('application/json,application/octet-stream'); + const picker = new google.picker.PickerBuilder() + .enableFeature(google.picker.Feature.NAV_HIDDEN) + .enableFeature(google.picker.Feature.MULTISELECT_ENABLED) + .setAppId(tr.ui.e.drive.constants.APP_ID) + .setOAuthToken(oauthToken_) + .addView(view) + .addView(new google.picker.DocsUploadView()) + .setDeveloperKey(tr.ui.e.drive.constants.DEVELOPER_KEY) + .setCallback(pickerCallback) + .build(); + picker.setVisible(true); + } + } + + function pickerCallback(data) { + if (data.action === google.picker.Action.PICKED) { + loadFileFromDrive(data.docs[0].id); + } + } + + function initShareButton() { + shareClient_ = new gapi.drive.share.ShareClient( + tr.ui.e.drive.constants.APP_ID); + shareClient_.setItemIds([driveFileId_]); + } + + function loadFileFromDrive(fileId) { + gapi.client.load('drive', 'v2', function() { + const request = gapi.client.drive.files.get({fileId}); + request.execute(function(resp) { downloadFile(resp); }); + driveFileId_ = fileId; + gapi.load('drive-share', initShareButton); + }); + } + + function downloadFile(file) { + if (file.downloadUrl) { + const downloadingOverlay = tr.ui.b.Overlay(); + downloadingOverlay.title = 'Downloading...'; + downloadingOverlay.userCanClose = false; + downloadingOverlay.msgEl = document.createElement('div'); + Polymer.dom(downloadingOverlay).appendChild(downloadingOverlay.msgEl); + downloadingOverlay.msgEl.style.margin = '20px'; + downloadingOverlay.update = function(msg) { + Polymer.dom(this.msgEl).textContent = msg; + }; + downloadingOverlay.visible = true; + + const accessToken = gapi.auth.getToken().access_token; + const xhr = new XMLHttpRequest(); + xhr.open('GET', file.downloadUrl); + xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken); + xhr.onload = function() { + downloadingOverlay.visible = false; + onDownloaded(file.title, xhr.responseText); + }; + xhr.onprogress = function(evt) { + downloadingOverlay.update( + Math.floor(evt.position * 100 / file.fileSize) + '% complete'); + }; + xhr.onerror = function() { alert('Failed downloading!'); }; + xhr.send(); + } else { + alert('No URL!'); + } + } + + function displayAllCollaborators() { + const allCollaborators = driveDocument_.getCollaborators(); + const collaboratorCount = allCollaborators.length; + const collabspan = document.getElementById('collabs'); + Polymer.dom(collabspan).innerHTML = ''; + const imageList = []; + for (let i = 0; i < collaboratorCount; i++) { + const user = allCollaborators[i]; + + const img = document.createElement('img'); + img.src = user.photoUrl; + img.alt = user.displayName; + img.height = 30; + img.width = 30; + img.className = 'collaborator-img'; + Polymer.dom(collabspan).appendChild(img); + imageList.push({'image': img, 'name': user.displayName}); + } + for (i = 0; i < imageList.length; i++) { + const collabTooltip = tr.ui.b.createDiv({ + className: 'collaborator-tooltip' + }); + const collabTooltipContent = tr.ui.b.createDiv({ + className: 'collaborator-tooltip-content' + }); + Polymer.dom(collabTooltipContent).textContent = imageList[i].name; + Polymer.dom(collabTooltip).appendChild(collabTooltipContent); + Polymer.dom(collabspan).appendChild(collabTooltip); + const collabTooltipArrow = tr.ui.b.createDiv({ + className: 'collaborator-tooltip-arrow'}); + Polymer.dom(collabTooltip).appendChild(collabTooltipArrow); + const collabTooltipArrowBefore = tr.ui.b.createDiv({ + className: 'collaborator-tooltip-arrow-before'}); + Polymer.dom(collabTooltipArrow).appendChild(collabTooltipArrowBefore); + const collabTooltipArrowAfter = tr.ui.b.createDiv({ + className: 'collaborator-tooltip-arrow-after'}); + Polymer.dom(collabTooltipArrow).appendChild(collabTooltipArrowAfter); + + const rect = imageList[i].image.getBoundingClientRect(); + collabTooltip.style.top = (rect.bottom - 6) + 'px'; + collabTooltip.style.left = + (rect.left + 16 - (collabTooltip.offsetWidth / 2)) + 'px'; + collabTooltipArrow.style.left = (collabTooltip.offsetWidth / 2) + 'px'; + collabTooltip.style.visibility = 'hidden'; + function visibilityDelegate(element, visibility) { + return function() { + element.style.visibility = visibility; + }; + } + imageList[i].image.addEventListener( + 'mouseover', visibilityDelegate(collabTooltip, 'visible')); + imageList[i].image.addEventListener( + 'mouseout', visibilityDelegate(collabTooltip, 'hidden')); + } + } + + function onRealtimeFileLoaded(doc) { + if (driveDocument_) { + driveDocument_.close(); + } + driveDocument_ = doc; + doc.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_JOINED, + displayAllCollaborators); + doc.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_LEFT, + displayAllCollaborators); + + displayAllCollaborators(doc); + } + + function onRealtimeError(e) { + alert('Error loading realtime: ' + e); + } + + function onDownloaded(filename, content) { + gapi.load('auth:client,drive-realtime,drive-share', function() { + gapi.drive.realtime.load(driveFileId_, + onRealtimeFileLoaded, + null, + onRealtimeError); + }); + + const traces = []; + const filenames = []; + filenames.push(filename); + traces.push(content); + createViewFromTraces(filenames, traces); + } + + function createViewFromTraces(filenames, traces) { + const m = new tr.Model(); + const i = new tr.importer.Import(m); + const p = i.importTracesWithProgressDialog(traces); + p.then( + function() { + timelineViewEl_.model = m; + timelineViewEl_.updateDocumentFavicon(); + timelineViewEl_.globalMode = true; + timelineViewEl_.viewTitle = ''; + }, + function(err) { + const downloadingOverlay = new tr.ui.b.Overlay(); + Polymer.dom(downloadingOverlay).textContent = + tr.b.normalizeException(err).message; + downloadingOverlay.title = 'Import error'; + downloadingOverlay.visible = true; + }); + } + + function onSaveToDiskClicked() { + throw new Error('Not implemented'); + } + + function onSaveToDriveClicked() { + throw new Error('Not implemented'); + } + + function onLoadFromDriveClicked() { + createPicker(); + } + + function onLoad() { + timelineViewEl_ = Polymer.dom(document).querySelector('x-timeline-view'); + timelineViewEl_.globalMode = true; + const navbar = document.getElementById('navbar'); + timelineViewEl_.style.top = navbar.offsetHeight + 'px'; + tr.ui.b.decorate(timelineViewEl_, tr.ui.TimelineView); + } + + window.addEventListener('load', onLoad); + + document.getElementById('x-drive-save-to-disk').onclick = + onSaveToDiskClicked; + document.getElementById('x-drive-save-to-drive').onclick = + onSaveToDriveClicked; + document.getElementById('x-drive-load-from-drive').onclick = + onLoadFromDriveClicked; + document.getElementById('x-drive-share').onclick = function() { + shareClient_.showSettingsDialog(); + }; + }()); + + </script> + <script type="text/javascript" + src="https://apis.google.com/js/client.js?onload=onAPIClientLoaded_"> + </script> +</body> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html new file mode 100644 index 00000000000..6d1e29d4e20 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/full_config.html @@ -0,0 +1,19 @@ +<!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. +--> + +<!-- +TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the +same way that all non-UI files depend on tracing/base/base.html. Enforce this +dependency with a presubmit. +--> +<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order> + +<!-- The full config is all the configs slammed together. --> +<link rel="import" href="/tracing/extras/importer/gcloud_trace/gcloud_trace_importer.html"> +<link rel="import" href="/tracing/ui/extras/chrome_config.html"> +<link rel="import" href="/tracing/ui/extras/lean_config.html"> +<link rel="import" href="/tracing/ui/extras/systrace_config.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html new file mode 100644 index 00000000000..8d66352f140 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/lean_config.html @@ -0,0 +1,21 @@ +<!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. +--> + +<!-- +TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the same way that +all non-UI files depend on tracing/base/base.html. Enforce this dependency with a presubmit. +--> +<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order> + +<link rel="import" href="/tracing/extras/lean_config.html" data-suppress-import-order> + +<!-- +The lean config is just enough to import uncompressed, trace-event-formatted +json blobs. +--> +<link rel="import" href="/tracing/ui/side_panel/file_size_stats_side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/metrics_side_panel.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html new file mode 100644 index 00000000000..0971d7f5409 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel.html @@ -0,0 +1,172 @@ +<!DOCTYPE html> +<!-- +Copyright 2015 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/math/statistics.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/line_chart.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> + +<dom-module id='tr-ui-e-s-alerts-side-panel'> + <template> + <style> + :host { + display: block; + width: 250px; + } + #content { + flex-direction: column; + display: flex; + } + tr-ui-b-table { + font-size: 12px; + } + </style> + + <div id='content'> + <toolbar id='toolbar'></toolbar> + <result-area id='result_area'></result-area> + </div> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-s-alerts-side-panel', + behaviors: [tr.ui.behaviors.SidePanel], + + + ready() { + this.rangeOfInterest_ = new tr.b.math.Range(); + this.selection_ = undefined; + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.model_ = model; + this.updateContents_(); + }, + + set selection(selection) { + }, + + set rangeOfInterest(rangeOfInterest) { + }, + + /** + * Fires a selection event selecting all alerts of the specified + * type. + */ + selectAlertsOfType(alertTypeString) { + const alertsOfType = this.model_.alerts.filter(function(alert) { + return alert.title === alertTypeString; + }); + + const event = new tr.model.RequestSelectionChangeEvent(); + event.selection = new tr.model.EventSet(alertsOfType); + this.dispatchEvent(event); + }, + + /** + * Returns a map for the specified alerts where each key is the + * alert type string and each value is a list of alerts with that + * type. + */ + alertsByType_(alerts) { + const alertsByType = {}; + alerts.forEach(function(alert) { + if (!alertsByType[alert.title]) { + alertsByType[alert.title] = []; + } + + alertsByType[alert.title].push(alert); + }); + return alertsByType; + }, + + alertsTableRows_(alertsByType) { + return Object.keys(alertsByType).map(function(key) { + return { + alertType: key, + count: alertsByType[key].length + }; + }); + }, + + alertsTableColumns_() { + return [ + { + title: 'Alert type', + value(row) { return row.alertType; }, + width: '180px' + }, + { + title: 'Count', + width: '100%', + value(row) { return row.count; } + } + ]; + }, + + createAlertsTable_(alerts) { + const alertsByType = this.alertsByType_(alerts); + + const table = document.createElement('tr-ui-b-table'); + table.tableColumns = this.alertsTableColumns_(); + table.tableRows = this.alertsTableRows_(alertsByType); + table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + table.addEventListener('selection-changed', function(e) { + const row = table.selectedTableRow; + if (row) { + this.selectAlertsOfType(row.alertType); + } + }.bind(this)); + + return table; + }, + + updateContents_() { + Polymer.dom(this.$.result_area).textContent = ''; + if (this.model_ === undefined) return; + + const panel = this.createAlertsTable_(this.model_.alerts); + Polymer.dom(this.$.result_area).appendChild(panel); + }, + + supportsModel(m) { + if (m === undefined) { + return { + supported: false, + reason: 'Unknown tracing model' + }; + } else if (m.alerts.length === 0) { + return { + supported: false, + reason: 'No alerts in tracing model' + }; + } + + return { + supported: true + }; + }, + + get textLabel() { + return 'Alerts'; + } +}); + +tr.ui.side_panel.SidePanelRegistry.register(function() { + return document.createElement('tr-ui-e-s-alerts-side-panel'); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html new file mode 100644 index 00000000000..c4cb9825d1e --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/alerts_side_panel_test.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2015 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/test_utils.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/alerts_side_panel.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const ALERT_INFO_1 = new tr.model.EventInfo( + 'Alert 1', 'Critical alert'); + const ALERT_INFO_2 = new tr.model.EventInfo( + 'Alert 2', 'Warning alert'); + + test('instantiate', function() { + const panel = document.createElement('tr-ui-e-s-alerts-side-panel'); + panel.model = createModelWithAlerts([ + new tr.model.Alert(ALERT_INFO_1, 5), + new tr.model.Alert(ALERT_INFO_2, 35) + ]); + panel.style.height = '100px'; + + this.addHTMLOutput(panel); + }); + + test('selectAlertsOfType', function() { + const panel = document.createElement('tr-ui-e-s-alerts-side-panel'); + const alerts = [ + new tr.model.Alert(ALERT_INFO_1, 1), + new tr.model.Alert(ALERT_INFO_1, 2), + new tr.model.Alert(ALERT_INFO_2, 3) + ]; + + const predictedAlerts = new tr.model.EventSet([alerts[0], alerts[1]]); + panel.model = createModelWithAlerts(alerts); + panel.style.height = '100px'; + this.addHTMLOutput(panel); + + let selectionChanged = false; + panel.addEventListener('requestSelectionChange', function(e) { + selectionChanged = true; + assert.isTrue(e.selection.equals(predictedAlerts)); + }); + panel.selectAlertsOfType(ALERT_INFO_1.title); + + assert.isTrue(selectionChanged); + }); + + function createModelWithAlerts(alerts) { + const m = new tr.Model(); + m.alerts = alerts; + return m; + } +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html new file mode 100644 index 00000000000..e5fd7689479 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel.html @@ -0,0 +1,347 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/ui/base/table.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> +<link rel="import" href="/tracing/value/ui/scalar_span.html"> + +<dom-module id='tr-ui-e-s-frame-data-side-panel'> + <template> + <style> + :host { + display: flex; + width: 600px; + flex-direction: column; + } + table-container { + display: flex; + overflow: auto; + font-size: 12px; + } + </style> + <div> + Organize by: + <select id="select"> + <option value="none">None</option> + <option value="tree">Frame Tree</option> + </select> + </div> + <table-container> + <tr-ui-b-table id="table"></tr-ui-b-table> + </table-container> + </template> +</dom-module> + +<script> +'use strict'; +tr.exportTo('tr.ui.e.s', function() { + const BlameContextSnapshot = tr.e.chrome.BlameContextSnapshot; + const FrameTreeNodeSnapshot = tr.e.chrome.FrameTreeNodeSnapshot; + const RenderFrameSnapshot = tr.e.chrome.RenderFrameSnapshot; + const TopLevelSnapshot = tr.e.chrome.TopLevelSnapshot; + + const BlameContextInstance = tr.e.chrome.BlameContextInstance; + const FrameTreeNodeInstance = tr.e.chrome.FrameTreeNodeInstance; + const RenderFrameInstance = tr.e.chrome.RenderFrameInstance; + const TopLevelInstance = tr.e.chrome.TopLevelInstance; + + /** + * @constructor + * If |context| is provided, creates a row for the given context. + * Otherwise, creates an empty Row template which can be used for aggregating + * data from a group of subrows. + */ + function Row(context) { + this.subRows = undefined; + this.contexts = []; + this.type = undefined; + this.renderer = 'N/A'; + this.url = undefined; + this.time = 0; + this.eventsOfInterest = new tr.model.EventSet(); + + if (context === undefined) return; + + this.type = context.objectInstance.blameContextType; + this.contexts.push(context); + if (context instanceof FrameTreeNodeSnapshot) { + if (context.renderFrame) { + this.contexts.push(context.renderFrame); + this.renderer = context.renderFrame.objectInstance.parent.pid; + } + } else if (context instanceof RenderFrameSnapshot) { + if (context.frameTreeNode) { + this.contexts.push(context.frameTreeNode); + } + this.renderer = context.objectInstance.parent.pid; + } else if (context instanceof TopLevelSnapshot) { + this.renderer = context.objectInstance.parent.pid; + } else { + throw new Error('Unknown context type'); + } + this.eventsOfInterest.addEventSet(this.contexts); + + // TODO(xiaochengh): Handle the case where a subframe has a trivial url + // (e.g., about:blank), but inherits the origin of its parent. This is not + // needed now, but will be required if we want to group rows by origin. + this.url = context.url; + } + + const groupFunctions = { + none: rows => rows, + + // Group the rows according to the frame tree structure. + // Example: consider frame tree a(b, c(d)), where each frame has 1ms time + // attributed to it. The resulting table should look like: + // Type | Time | URL + // --------------+------+----- + // Frame Tree | 4 | a + // +- Frame | 1 | a + // +- Subframe | 1 | b + // +- Frame Tree | 2 | c + // +- Frame | 1 | c + // +- Subframe | 1 | d + tree(rows, rowMap) { + // Finds the parent of a specific row. When there is conflict between the + // browser's dump of the frame tree and the renderers', use the browser's. + const getParentRow = function(row) { + let pivot; + row.contexts.forEach(function(context) { + if (context instanceof tr.e.chrome.FrameTreeNodeSnapshot) { + pivot = context; + } + }); + if (pivot && pivot.parentContext) { + return rowMap[pivot.parentContext.guid]; + } + return undefined; + }; + + const rootRows = []; + rows.forEach(function(row) { + const parentRow = getParentRow(row); + if (parentRow === undefined) { + rootRows.push(row); + return; + } + if (parentRow.subRows === undefined) { + parentRow.subRows = []; + } + parentRow.subRows.push(row); + }); + + const aggregateAllDescendants = function(row) { + if (!row.subRows) { + if (getParentRow(row)) { + row.type = 'Subframe'; + } + return row; + } + const result = new Row(); + result.type = 'Frame Tree'; + result.renderer = row.renderer; + result.url = row.url; + result.subRows = [row]; + row.subRows.forEach( + subRow => result.subRows.push(aggregateAllDescendants(subRow))); + result.subRows.forEach(function(subRow) { + result.time += subRow.time; + result.eventsOfInterest.addEventSet(subRow.eventsOfInterest); + }); + row.subRows = undefined; + return result; + }; + + return rootRows.map(rootRow => aggregateAllDescendants(rootRow)); + } + + // TODO(xiaochengh): Add grouping by site and probably more... + }; + + Polymer({ + is: 'tr-ui-e-s-frame-data-side-panel', + behaviors: [tr.ui.behaviors.SidePanel], + + ready() { + this.model_ = undefined; + this.rangeOfInterest_ = new tr.b.math.Range(); + + this.$.table.showHeader = true; + this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + this.$.table.tableColumns = this.createFrameDataTableColumns_(); + + this.$.table.addEventListener('selection-changed', function(e) { + this.selectEventSet_(this.$.table.selectedTableRow.eventsOfInterest); + }.bind(this)); + + this.$.select.addEventListener('change', function(e) { + this.updateContents_(); + }.bind(this)); + }, + + selectEventSet_(eventSet) { + const event = new tr.model.RequestSelectionChangeEvent(); + event.selection = eventSet; + this.dispatchEvent(event); + }, + + createFrameDataTableColumns_() { + return [ + { + title: 'Renderer', + value: row => row.renderer, + cmp: (a, b) => a.renderer - b.renderer + }, + { + title: 'Type', + value: row => row.type + }, + // TODO(xiaochengh): Decide what details to show in the table: + // - URL seems necessary, but we may also want origin instead/both. + // - Distinguish between browser time and renderer time? + // - Distinguish between CPU time and wall clock time? + // - Memory? Network? ... + { + title: 'Time', + value: row => tr.v.ui.createScalarSpan(row.time, { + unit: tr.b.Unit.byName.timeStampInMs, + ownerDocument: this.ownerDocument + }), + cmp: (a, b) => a.time - b.time + }, + { + title: 'URL', + value: row => row.url, + cmp: (a, b) => (a.url || '').localeCompare(b.url || '') + } + ]; + }, + + createFrameDataTableRows_() { + if (!this.model_) return []; + + // Gather contexts into skeletons of rows. + const rows = []; + const rowMap = {}; + for (const proc of Object.values(this.model_.processes)) { + proc.objects.iterObjectInstances(function(objectInstance) { + if (!(objectInstance instanceof BlameContextInstance)) { + return; + } + objectInstance.snapshots.forEach(function(snapshot) { + if (rowMap[snapshot.guid]) return; + + const row = new Row(snapshot); + row.contexts.forEach(context => rowMap[context.guid] = row); + rows.push(row); + }, this); + }, this); + } + + // Find slices attributed to each row. + // TODO(xiaochengh): We should implement a getter + // BlameContextSnapshot.attributedEvents, instead of process the model in + // a UI component. + for (const proc of Object.values(this.model_.processes)) { + for (const thread of Object.values(proc.threads)) { + thread.sliceGroup.iterSlicesInTimeRange(function(topLevelSlice) { + topLevelSlice.contexts.forEach(function(context) { + if (!context.snapshot.guid || !rowMap[context.snapshot.guid]) { + return; + } + const row = rowMap[context.snapshot.guid]; + row.eventsOfInterest.push(topLevelSlice); + row.time += topLevelSlice.selfTime || 0; + }); + }, this.currentRangeOfInterest.min, this.currentRangeOfInterest.max); + } + } + + // Apply grouping to rows. + const select = this.$.select; + const groupOption = select.options[select.selectedIndex].value; + const groupFunction = groupFunctions[groupOption]; + return groupFunction(rows, rowMap); + }, + + updateContents_() { + this.$.table.tableRows = this.createFrameDataTableRows_(); + this.$.table.rebuild(); + }, + + supportsModel(m) { + if (!m) { + return { + supported: false, + reason: 'No model available.' + }; + } + + const ans = {supported: false}; + for (const proc of Object.values(m.processes)) { + proc.objects.iterObjectInstances(function(instance) { + if (instance instanceof BlameContextInstance) { + ans.supported = true; + } + }); + } + + if (!ans.supported) { + ans.reason = 'No frame data available'; + } + return ans; + }, + + get currentRangeOfInterest() { + if (this.rangeOfInterest_.isEmpty) { + return this.model_.bounds; + } + return this.rangeOfInterest_; + }, + + get rangeOfInterest() { + return this.rangeOfInterest_; + }, + + set rangeOfInterest(rangeOfInterest) { + this.rangeOfInterest_ = rangeOfInterest; + this.updateContents_(); + }, + + get selection() { + // Not applicable. + }, + + set selection(_) { + // Not applicable. + }, + + get textLabel() { + return 'Frame Data'; + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.model_ = model; + this.updateContents_(); + } + }); + + tr.ui.side_panel.SidePanelRegistry.register(function() { + return document.createElement('tr-ui-e-s-frame-data-side-panel'); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html new file mode 100644 index 00000000000..298afe05d42 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/frame_data_side_panel_test.html @@ -0,0 +1,165 @@ +<!DOCTYPE html> +<!-- +Copyright 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/frame_tree_node.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/render_frame.html"> +<link rel="import" href="/tracing/extras/chrome/blame_context/top_level.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/frame_data_side_panel.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const TestUtils = tr.c.TestUtils; + + function topLevelOptions(pid, id) { + return { + pid, + id, + cat: 'blink', + scope: 'PlatformThread', + name: 'TopLevel' + }; + } + + function renderFrameOptions(pid, id, parent) { + return { + pid, + id, + cat: 'blink', + scope: 'RenderFrame', + name: 'RenderFrame', + args: {parent: { + id_ref: parent.id, + scope: parent.scope + }} + }; + } + + function frameTreeNodeOptions(pid, id, opt_renderFrame, opt_parentId) { + const ans = { + pid, + id, + cat: 'navigation', + scope: 'FrameTreeNode', + name: 'FrameTreeNode', + args: {} + }; + if (opt_renderFrame) { + ans.args.renderFrame = { + id_ref: opt_renderFrame.id, + pid_ref: opt_renderFrame.pid, + scope: 'RenderFrame' + }; + } + if (opt_parentId) { + ans.args.parent = { + id_ref: opt_parentId, + scope: 'FrameTreeNode' + }; + } + return ans; + } + + /** + * Creates some independent contexts. Checks if all are present in the panel. + */ + test('basic', function() { + const panel = document.createElement('tr-ui-e-s-frame-data-side-panel'); + panel.model = TestUtils.newModel(function(model) { + TestUtils.newSnapshot(model, topLevelOptions(1, '0x1')); + TestUtils.newSnapshot(model, renderFrameOptions( + 1, '0x2', {id: '0x1', scope: 'PlatformThread'})); + TestUtils.newSnapshot(model, frameTreeNodeOptions( + 2, '0x3')); + }); + assert.lengthOf(panel.$.table.tableRows, 3); + + this.addHTMLOutput(panel); + }); + + /** + * Creates a FrameTreeNode in the browser process and a RenderFrame in a + * renderer process that are the same frame. Checks if they are merged into + * one row in the panel. + */ + test('mergeCrossProcessFrameBlameContexts', function() { + const panel = document.createElement('tr-ui-e-s-frame-data-side-panel'); + panel.model = TestUtils.newModel(function(model) { + TestUtils.newSnapshot(model, topLevelOptions(1, '0x1')); + TestUtils.newSnapshot(model, renderFrameOptions( + 1, '0x2', {id: '0x1', scope: 'PlatformThread'})); + TestUtils.newSnapshot(model, frameTreeNodeOptions( + 2, '0x3', {id: '0x2', pid: 1})); + }); + assert.lengthOf(panel.$.table.tableRows, 2); + + this.addHTMLOutput(panel); + }); + + function newAttributedSlice(model, pid, start, duration, context) { + const slice = TestUtils.newSliceEx({start, duration}); + slice.contexts = [{type: 'FrameBlameContext', snapshot: context}]; + model.getOrCreateProcess(pid).getOrCreateThread(1).sliceGroup.pushSlice( + slice); + return slice; + } + + /** + * Changes the range of interest. Checks if the panel updates correspondingly. + */ + test('respondToRangeOfInterest', function() { + let topLevel; + let slice1; + let slice2; + const panel = document.createElement('tr-ui-e-s-frame-data-side-panel'); + panel.model = TestUtils.newModel(function(model) { + topLevel = TestUtils.newSnapshot(model, topLevelOptions(1, '0x1')); + slice1 = newAttributedSlice(model, 1, 1500, 500, topLevel); + slice2 = newAttributedSlice(model, 1, 2500, 500, topLevel); + }); + + // The default range of interest contains both slices. + assert.isTrue(panel.$.table.tableRows[0].eventsOfInterest.equals( + new tr.model.EventSet([topLevel, slice1, slice2]))); + + // The new range of interest contains only slice2. + panel.rangeOfInterest = tr.b.math.Range.fromExplicitRange(slice2.start, + slice2.end); + assert.isTrue(panel.$.table.tableRows[0].eventsOfInterest.equals( + new tr.model.EventSet([topLevel, slice2]))); + + this.addHTMLOutput(panel); + }); + + /** + * Selects a row in the panel. Checks if the context(s) of the row and the + * slices attributed to the row are selected. + */ + test('selectAttributedEvents', function() { + let topLevel; + let slice; + const panel = document.createElement('tr-ui-e-s-frame-data-side-panel'); + panel.model = TestUtils.newModel(function(model) { + topLevel = TestUtils.newSnapshot(model, topLevelOptions(1, '0x1')); + slice = newAttributedSlice(model, 1, 1500, 500, topLevel); + }); + + let selectionChanged = false; + panel.addEventListener('requestSelectionChange', function(e) { + selectionChanged = true; + assert.isTrue( + e.selection.equals(new tr.model.EventSet([topLevel, slice]))); + }); + panel.$.table.selectedTableRow = panel.$.table.tableRows[0]; + assert.isTrue(selectionChanged); + + this.addHTMLOutput(panel); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html new file mode 100644 index 00000000000..14e33919922 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel.html @@ -0,0 +1,334 @@ +<!DOCTYPE html> +<!-- +Copyright 2014 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/math/statistics.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> +<link rel="import" href="/tracing/ui/base/dom_helpers.html"> +<link rel="import" href="/tracing/ui/base/line_chart.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel.html"> +<link rel="import" href="/tracing/ui/side_panel/side_panel_registry.html"> + +<dom-module id='tr-ui-e-s-input-latency-side-panel'> + <template> + <style> + :host { + flex-direction: column; + display: flex; + } + toolbar { + flex: 0 0 auto; + border-bottom: 1px solid black; + display: flex; + } + result-area { + flex: 1 1 auto; + display: block; + min-height: 0; + overflow-y: auto; + } + </style> + + <toolbar id='toolbar'></toolbar> + <result-area id='result_area'></result-area> + </template> +</dom-module> +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-s-input-latency-side-panel', + behaviors: [tr.ui.behaviors.SidePanel], + + + ready() { + this.rangeOfInterest_ = new tr.b.math.Range(); + this.frametimeType_ = tr.model.helpers.IMPL_FRAMETIME_TYPE; + this.latencyChart_ = undefined; + this.frametimeChart_ = undefined; + this.selectedProcessId_ = undefined; + this.mouseDownIndex_ = undefined; + this.curMouseIndex_ = undefined; + }, + + get model() { + return this.model_; + }, + + set model(model) { + this.model_ = model; + if (this.model_) { + this.modelHelper_ = this.model_.getOrCreateHelper( + tr.model.helpers.ChromeModelHelper); + } else { + this.modelHelper_ = undefined; + } + + this.updateToolbar_(); + this.updateContents_(); + }, + + get frametimeType() { + return this.frametimeType_; + }, + + set frametimeType(type) { + if (this.frametimeType_ === type) return; + + this.frametimeType_ = type; + this.updateContents_(); + }, + + get selectedProcessId() { + return this.selectedProcessId_; + }, + + set selectedProcessId(process) { + if (this.selectedProcessId_ === process) return; + + this.selectedProcessId_ = process; + this.updateContents_(); + }, + + set selection(selection) { + if (this.latencyChart_ === undefined) return; + + this.latencyChart_.brushedRange = selection.bounds; + }, + + // This function is for testing purpose. + setBrushedIndices(mouseDownIndex, curIndex) { + this.mouseDownIndex_ = mouseDownIndex; + this.curMouseIndex_ = curIndex; + this.updateBrushedRange_(); + }, + + updateBrushedRange_() { + if (this.latencyChart_ === undefined) return; + + let r = new tr.b.math.Range(); + if (this.mouseDownIndex_ === undefined) { + this.latencyChart_.brushedRange = r; + return; + } + r = this.latencyChart_.computeBrushRangeFromIndices( + this.mouseDownIndex_, this.curMouseIndex_); + this.latencyChart_.brushedRange = r; + + // Based on the brushed range, update the selection of LatencyInfo in + // the timeline view by sending a selectionChange event. + let latencySlices = []; + for (const thread of this.model_.getAllThreads()) { + for (const event of thread.getDescendantEvents()) { + if (event.title.indexOf('InputLatency:') === 0) { + latencySlices.push(event); + } + } + } + latencySlices = tr.model.helpers.getSlicesIntersectingRange( + r, latencySlices); + + const event = new tr.model.RequestSelectionChangeEvent(); + event.selection = new tr.model.EventSet(latencySlices); + this.latencyChart_.dispatchEvent(event); + }, + + registerMouseEventForLatencyChart_() { + this.latencyChart_.addEventListener('item-mousedown', function(e) { + this.mouseDownIndex_ = e.index; + this.curMouseIndex_ = e.index; + this.updateBrushedRange_(); + }.bind(this)); + + this.latencyChart_.addEventListener('item-mousemove', function(e) { + if (e.button === undefined) return; + + this.curMouseIndex_ = e.index; + this.updateBrushedRange_(); + }.bind(this)); + + this.latencyChart_.addEventListener('item-mouseup', function(e) { + this.curMouseIndex = e.index; + this.updateBrushedRange_(); + }.bind(this)); + }, + + updateToolbar_() { + const browserProcess = this.modelHelper_.browserProcess; + const labels = []; + + if (browserProcess !== undefined) { + const labelStr = 'Browser: ' + browserProcess.pid; + labels.push({label: labelStr, value: browserProcess.pid}); + } + + for (const rendererHelper of + Object.values(this.modelHelper_.rendererHelpers)) { + const rendererProcess = rendererHelper.process; + const labelStr = 'Renderer: ' + rendererProcess.userFriendlyName; + labels.push({label: labelStr, value: rendererProcess.userFriendlyName}); + } + + if (labels.length === 0) return; + + this.selectedProcessId_ = labels[0].value; + const toolbarEl = this.$.toolbar; + Polymer.dom(toolbarEl).appendChild(tr.ui.b.createSelector( + this, 'frametimeType', + 'inputLatencySidePanel.frametimeType', this.frametimeType_, + [{label: 'Main Thread Frame Times', + value: tr.model.helpers.MAIN_FRAMETIME_TYPE}, + {label: 'Impl Thread Frame Times', + value: tr.model.helpers.IMPL_FRAMETIME_TYPE} + ])); + Polymer.dom(toolbarEl).appendChild(tr.ui.b.createSelector( + this, 'selectedProcessId', + 'inputLatencySidePanel.selectedProcessId', + this.selectedProcessId_, + labels)); + }, + + // TODO(charliea): Delete this function in favor of rangeOfInterest. + get currentRangeOfInterest() { + if (this.rangeOfInterest_.isEmpty) { + return this.model_.bounds; + } + return this.rangeOfInterest_; + }, + + createLatencyLineChart(data, title, parentNode) { + const chart = new tr.ui.b.LineChart(); + Polymer.dom(parentNode).appendChild(chart); + let width = 600; + if (document.body.clientWidth !== undefined) { + width = document.body.clientWidth * 0.5; + } + chart.graphWidth = width; + chart.chartTitle = title; + chart.data = data; + return chart; + }, + + updateContents_() { + const resultArea = this.$.result_area; + this.latencyChart_ = undefined; + this.frametimeChart_ = undefined; + Polymer.dom(resultArea).textContent = ''; + + if (this.modelHelper_ === undefined) return; + + const rangeOfInterest = this.currentRangeOfInterest; + + let chromeProcess; + if (this.modelHelper_.rendererHelpers[this.selectedProcessId_]) { + chromeProcess = this.modelHelper_.rendererHelpers[ + this.selectedProcessId_ + ]; + } else { + chromeProcess = this.modelHelper_.browserHelper; + } + + const frameEvents = chromeProcess.getFrameEventsInRange( + this.frametimeType, rangeOfInterest); + + const frametimeData = tr.model.helpers.getFrametimeDataFromEvents( + frameEvents); + const averageFrametime = tr.b.math.Statistics.mean(frametimeData, d => + d.frametime + ); + + const latencyEvents = this.modelHelper_.browserHelper. + getLatencyEventsInRange( + rangeOfInterest); + + const latencyData = []; + latencyEvents.forEach(function(event) { + if (event.inputLatency === undefined) return; + + latencyData.push({ + x: event.start, + latency: event.inputLatency / 1000 + }); + }); + + const averageLatency = tr.b.math.Statistics.mean(latencyData, function(d) { + return d.latency; + }); + + // Create summary. + const latencySummaryText = document.createElement('div'); + Polymer.dom(latencySummaryText).appendChild(tr.ui.b.createSpan({ + textContent: 'Average Latency ' + averageLatency + ' ms', + bold: true})); + Polymer.dom(resultArea).appendChild(latencySummaryText); + + const frametimeSummaryText = document.createElement('div'); + Polymer.dom(frametimeSummaryText).appendChild(tr.ui.b.createSpan({ + textContent: 'Average Frame Time ' + averageFrametime + ' ms', + bold: true})); + Polymer.dom(resultArea).appendChild(frametimeSummaryText); + + if (latencyData.length !== 0) { + this.latencyChart_ = this.createLatencyLineChart( + latencyData, 'Latency Over Time', resultArea); + this.registerMouseEventForLatencyChart_(); + } + + if (frametimeData.length !== 0) { + this.frametimeChart_ = this.createLatencyLineChart( + frametimeData, 'Frame Times', resultArea); + } + }, + + get rangeOfInterest() { + return this.rangeOfInterest_; + }, + + set rangeOfInterest(rangeOfInterest) { + this.rangeOfInterest_ = rangeOfInterest; + this.updateContents_(); + }, + + supportsModel(m) { + if (m === undefined) { + return { + supported: false, + reason: 'Unknown tracing model' + }; + } + + if (!tr.model.helpers.ChromeModelHelper.supportsModel(m)) { + return { + supported: false, + reason: 'No Chrome browser or renderer process found' + }; + } + + const modelHelper = m.getOrCreateHelper(tr.model.helpers.ChromeModelHelper); + if (modelHelper.browserHelper && + modelHelper.browserHelper.hasLatencyEvents) { + return { + supported: true + }; + } + + return { + supported: false, + reason: 'No InputLatency events trace. Consider enabling ' + + 'benchmark" and "input" category when recording the trace' + }; + }, + + get textLabel() { + return 'Input Latency'; + } +}); + +tr.ui.side_panel.SidePanelRegistry.register(function() { + return document.createElement('tr-ui-e-s-input-latency-side-panel'); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html new file mode 100644 index 00000000000..de225416faa --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/side_panel/input_latency_side_panel_test.html @@ -0,0 +1,148 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2014 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/test_utils.html"> +<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html"> +<link rel="import" href="/tracing/extras/importer/trace_event_importer.html"> +<link rel="import" href="/tracing/model/model.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/input_latency_side_panel.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + test('basic', function() { + const latencyData = [ + { + x: 1000, + latency: 16 + }, + { + x: 2000, + latency: 17 + }, + { + x: 3000, + latency: 14 + }, + { + x: 4000, + latency: 23 + } + ]; + let lc = document.createElement('tr-ui-e-s-input-latency-side-panel'); + let container = document.createElement('div'); + this.addHTMLOutput(container); + const latencyChart = lc.createLatencyLineChart( + latencyData, 'latency', container); + + const frametimeData = [ + { + x: 1000, + frametime: 16 + }, + { + x: 2000, + frametime: 17 + }, + { + x: 3000, + frametime: 14 + }, + { + x: 4000, + frametime: 23 + } + ]; + lc = document.createElement('tr-ui-e-s-input-latency-side-panel'); + container = document.createElement('div'); + this.addHTMLOutput(container); + const frametimeChart = lc.createLatencyLineChart( + frametimeData, 'frametime', container); + }); + + test('brushedRangeChange', function() { + const events = []; + for (let i = 0; i < 10; i++) { + const startTs = i * 10000; + const endTs = startTs + 1000 * (i % 2); + events.push( + { + 'cat': 'benchmark', + 'pid': 3507, + 'tid': 3507, + 'ts': startTs, + 'ph': 'S', + 'name': 'InputLatency', + 'id': i + }); + events.push( + { + 'cat': 'benchmark', + 'pid': 3507, + 'tid': 3507, + 'ts': endTs, + 'ph': 'T', + 'name': 'InputLatency', + 'args': {'step': 'GestureScrollUpdate'}, + 'id': i + }); + events.push( + { + 'cat': 'benchmark', + 'pid': 3507, + 'tid': 3507, + 'ts': endTs, + 'ph': 'F', + 'name': 'InputLatency', + 'args': { + 'data': { + 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT': { + 'time': startTs + }, + 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT': { + 'time': endTs + } + } + }, + 'id': i + }); + } + events.push({'cat': '__metadata', + 'pid': 3507, + 'tid': 3507, + 'ts': 0, + 'ph': 'M', + 'name': 'thread_name', + 'args': {'name': 'CrBrowserMain'}}); + + const panel = document.createElement('tr-ui-e-s-input-latency-side-panel'); + this.addHTMLOutput(panel); + + let selectionChanged = false; + + panel.model = tr.c.TestUtils.newModelWithEvents([events]); + function listener(e) { + selectionChanged = true; + assert.strictEqual(e.selection.length, 3); + const predictedStarts = [20, 31, 40]; + let i = 0; + for (const event of e.selection) { + assert.strictEqual(event.start, predictedStarts[i++]); + } + } + panel.ownerDocument.addEventListener('requestSelectionChange', listener); + try { + panel.setBrushedIndices(2, 4); + } finally { + panel.ownerDocument.removeEventListener( + 'requestSelectionChange', listener); + } + assert.isTrue(selectionChanged); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html new file mode 100644 index 00000000000..31bc1dbd997 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats.html @@ -0,0 +1,12 @@ +<!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/extras/system_stats/system_stats_snapshot.html"> +<link rel="import" + href="/tracing/ui/extras/system_stats/system_stats_instance_track.html"> +<link rel="import" + href="/tracing/ui/extras/system_stats/system_stats_snapshot_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css new file mode 100644 index 00000000000..40096f5497c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.css @@ -0,0 +1,15 @@ +/* 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. + */ + +.tr-ui-e-system-stats-instance-track { + height: 500px; +} + +.tr-ui-e-system-stats-instance-track ul { + list-style: none; + list-style-position: outside; + margin: 0; + overflow: hidden; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html new file mode 100644 index 00000000000..7695086660c --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track.html @@ -0,0 +1,451 @@ +<!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="stylesheet" + href="/tracing/ui/extras/system_stats/system_stats_instance_track.css"> + +<link rel="import" href="/tracing/base/unit.html"> +<link rel="import" href="/tracing/base/utils.html"> +<link rel="import" href="/tracing/ui/base/event_presenter.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/tracks/object_instance_track.html"> +<link rel="import" href="/tracing/ui/tracks/stacked_bars_track.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.system_stats', function() { + const EventPresenter = tr.ui.b.EventPresenter; + + let statCount; + + const excludedStats = {'meminfo': { + 'pswpin': 0, + 'pswpout': 0, + 'pgmajfault': 0}, + 'diskinfo': { + 'io': 0, + 'io_time': 0, + 'read_time': 0, + 'reads': 0, + 'reads_merged': 0, + 'sectors_read': 0, + 'sectors_written': 0, + 'weighted_io_time': 0, + 'write_time': 0, + 'writes': 0, + 'writes_merged': 0}, + 'swapinfo': {}, + 'perfinfo': { + 'idle_time': 0, + 'read_transfer_count': 0, + 'write_transfer_count': 0, + 'other_transfer_count': 0, + 'read_operation_count': 0, + 'write_operation_count': 0, + 'other_operation_count': 0, + 'pagefile_pages_written': 0, + 'pagefile_pages_write_ios': 0, + 'available_pages': 0, + 'pages_read': 0, + 'page_read_ios': 0} + }; + + /** + * Tracks that display system stats data. + * + * @constructor + * @extends {StackedBarsTrack} + */ + + const SystemStatsInstanceTrack = tr.ui.b.define( + 'tr-ui-e-system-stats-instance-track', tr.ui.tracks.StackedBarsTrack); + + const kPageSizeWindows = 4096; + + SystemStatsInstanceTrack.prototype = { + + __proto__: tr.ui.tracks.StackedBarsTrack.prototype, + + decorate(viewport) { + tr.ui.tracks.StackedBarsTrack.prototype.decorate.call(this, viewport); + Polymer.dom(this).classList.add('tr-ui-e-system-stats-instance-track'); + this.objectInstance_ = null; + }, + + set objectInstances(objectInstances) { + if (!objectInstances) { + this.objectInstance_ = []; + return; + } + if (objectInstances.length !== 1) { + throw new Error('Bad object instance count.'); + } + this.objectInstance_ = objectInstances[0]; + if (this.objectInstance_ !== null) { + this.computeRates_(this.objectInstance_.snapshots); + this.maxStats_ = this.computeMaxStats_( + this.objectInstance_.snapshots); + } + }, + + computeRates_(snapshots) { + for (let i = 0; i < snapshots.length; i++) { + const snapshot = snapshots[i]; + const stats = snapshot.getStats(); + let prevSnapshot; + + if (i === 0) { + // Deltas will be zero. + prevSnapshot = snapshots[0]; + } else { + prevSnapshot = snapshots[i - 1]; + } + const prevStats = prevSnapshot.getStats(); + let timeIntervalSeconds = (snapshot.ts - prevSnapshot.ts) / 1000; + // Prevent divide by zero. + if (timeIntervalSeconds === 0) { + timeIntervalSeconds = 1; + } + + this.computeRatesRecursive_(prevStats, stats, + timeIntervalSeconds); + } + }, + + computeRatesRecursive_(prevStats, stats, + timeIntervalSeconds) { + for (const statName in stats) { + if (stats[statName] instanceof Object) { + this.computeRatesRecursive_(prevStats[statName], + stats[statName], + timeIntervalSeconds); + } else { + if (statName === 'sectors_read') { + stats.bytes_read_per_sec = (stats.sectors_read - + prevStats.sectors_read) * + 512 / timeIntervalSeconds; + } + if (statName === 'sectors_written') { + stats.bytes_written_per_sec = + (stats.sectors_written - + prevStats.sectors_written) * + 512 / timeIntervalSeconds; + } + if (statName === 'pgmajfault') { + stats.pgmajfault_per_sec = (stats.pgmajfault - + prevStats.pgmajfault) / + timeIntervalSeconds; + } + if (statName === 'pswpin') { + stats.bytes_swpin_per_sec = (stats.pswpin - + prevStats.pswpin) * + 1000 / timeIntervalSeconds; + } + if (statName === 'pswpout') { + stats.bytes_swpout_per_sec = (stats.pswpout - + prevStats.pswpout) * + 1000 / timeIntervalSeconds; + } + + // All the stats below are available only on Windows: + + if (statName === 'idle_time') { + // Total amount of idle_time, in unit of 100 nanoseconds. + const units = tr.b.convertUnit(100., + tr.b.UnitScale.TIME.NANO_SEC, tr.b.UnitScale.TIME.SEC); + const idleTile = (stats.idle_time - prevStats.idle_time) * units; + stats.idle_time_per_sec = idleTile / timeIntervalSeconds; + } + if (statName === 'read_transfer_count') { + const bytesRead = stats.read_transfer_count - + prevStats.read_transfer_count; + stats.bytes_read_per_sec = bytesRead / timeIntervalSeconds; + } + if (statName === 'write_transfer_count') { + const bytesWritten = stats.write_transfer_count - + prevStats.write_transfer_count; + stats.bytes_written_per_sec = bytesWritten / timeIntervalSeconds; + } + if (statName === 'other_transfer_count') { + const bytesTransfer = stats.other_transfer_count - + prevStats.other_transfer_count; + stats.bytes_other_per_sec = bytesTransfer / timeIntervalSeconds; + } + if (statName === 'read_operation_count') { + const readOperation = stats.read_operation_count - + prevStats.read_operation_count; + stats.read_operation_per_sec = readOperation / timeIntervalSeconds; + } + if (statName === 'write_operation_count') { + const writeOperation = stats.write_operation_count - + prevStats.write_operation_count; + stats.write_operation_per_sec = + writeOperation / timeIntervalSeconds; + } + if (statName === 'other_operation_count') { + const otherOperation = stats.other_operation_count - + prevStats.other_operation_count; + stats.other_operation_per_sec = + otherOperation / timeIntervalSeconds; + } + if (statName === 'pagefile_pages_written') { + const pageFileBytesWritten = + (stats.pagefile_pages_written - + prevStats.pagefile_pages_written) * kPageSizeWindows; + stats.pagefile_bytes_written_per_sec = + pageFileBytesWritten / timeIntervalSeconds; + } + if (statName === 'pagefile_pages_write_ios') { + const pagefileWriteOperation = + stats.pagefile_pages_write_ios - + prevStats.pagefile_pages_write_ios; + stats.pagefile_write_operation_per_sec = + pagefileWriteOperation / timeIntervalSeconds; + } + if (statName === 'available_pages') { + // Nothing to do here for now. + stats.available_pages_in_bytes = + stats.available_pages * kPageSizeWindows; + // TODO(sebmarchand): Add a available_pages_field that tracks the + // variation of this metric? + } + if (statName === 'pages_read') { + const pagesBytesRead = + (stats.pages_read - prevStats.pages_read) * kPageSizeWindows; + stats.bytes_read_per_sec = pagesBytesRead / timeIntervalSeconds; + } + if (statName === 'page_read_ios') { + const pagesBytesReadOperations = + stats.page_read_ios - prevStats.page_read_ios; + stats.pagefile_write_operation_per_sec = + pagesBytesReadOperations / timeIntervalSeconds; + } + } + } + }, + + computeMaxStats_(snapshots) { + const maxStats = {}; + statCount = 0; + + for (let i = 0; i < snapshots.length; i++) { + const snapshot = snapshots[i]; + const stats = snapshot.getStats(); + + this.computeMaxStatsRecursive_(stats, maxStats, + excludedStats); + } + + return maxStats; + }, + + computeMaxStatsRecursive_(stats, maxStats, excludedStats) { + for (const statName in stats) { + if (stats[statName] instanceof Object) { + if (!(statName in maxStats)) { + maxStats[statName] = {}; + } + + let excludedNested; + if (excludedStats && statName in excludedStats) { + excludedNested = excludedStats[statName]; + } else { + excludedNested = null; + } + + this.computeMaxStatsRecursive_(stats[statName], + maxStats[statName], + excludedNested); + } else { + if (excludedStats && statName in excludedStats) { + continue; + } + if (!(statName in maxStats)) { + maxStats[statName] = 0; + statCount++; + } + if (stats[statName] > maxStats[statName]) { + maxStats[statName] = stats[statName]; + } + } + } + }, + + get height() { + return window.getComputedStyle(this).height; + }, + + set height(height) { + this.style.height = height; + }, + + draw(type, viewLWorld, viewRWorld, viewHeight) { + switch (type) { + case tr.ui.tracks.DrawType.GENERAL_EVENT: + this.drawStatBars_(viewLWorld, viewRWorld); + break; + } + }, + + drawStatBars_(viewLWorld, viewRWorld) { + const ctx = this.context(); + const pixelRatio = window.devicePixelRatio || 1; + + const bounds = this.getBoundingClientRect(); + const width = bounds.width * pixelRatio; + const height = (bounds.height * pixelRatio) / statCount; + + // Culling parameters. + const vp = this.viewport.currentDisplayTransform; + + // Scale by the size of the largest snapshot. + const maxStats = this.maxStats_; + + const objectSnapshots = this.objectInstance_.snapshots; + let lowIndex = tr.b.findLowIndexInSortedArray( + objectSnapshots, + function(snapshot) { + return snapshot.ts; + }, + viewLWorld); + + // Assure that the stack with the left edge off screen still gets drawn + if (lowIndex > 0) lowIndex -= 1; + + for (let i = lowIndex; i < objectSnapshots.length; ++i) { + const snapshot = objectSnapshots[i]; + const trace = snapshot.getStats(); + const currentY = height; + + const left = snapshot.ts; + if (left > viewRWorld) break; + + let leftView = vp.xWorldToView(left); + if (leftView < 0) leftView = 0; + + // Compute the edges for the column graph bar. + let right; + if (i !== objectSnapshots.length - 1) { + right = objectSnapshots[i + 1].ts; + } else { + // If this is the last snapshot of multiple snapshots, use the width + // of the previous snapshot for the width. + if (objectSnapshots.length > 1) { + right = objectSnapshots[i].ts + (objectSnapshots[i].ts - + objectSnapshots[i - 1].ts); + } else { + // If there's only one snapshot, use max bounds as the width. + right = this.objectInstance_.parent.model.bounds.max; + } + } + + let rightView = vp.xWorldToView(right); + if (rightView > width) { + rightView = width; + } + + // Floor the bounds to avoid a small gap between stacks. + leftView = Math.floor(leftView); + rightView = Math.floor(rightView); + + // Descend into nested stats. + this.drawStatBarsRecursive_(snapshot, + leftView, + rightView, + height, + trace, + maxStats, + currentY); + + if (i === lowIndex) { + this.drawStatNames_(leftView, height, currentY, '', maxStats); + } + } + ctx.lineWidth = 1; + }, + + drawStatBarsRecursive_(snapshot, + leftView, + rightView, + height, + stats, + maxStats, + currentY) { + const ctx = this.context(); + + for (const statName in maxStats) { + if (stats[statName] instanceof Object) { + // Use the y-position returned from the recursive call. + currentY = this.drawStatBarsRecursive_(snapshot, + leftView, + rightView, + height, + stats[statName], + maxStats[statName], + currentY); + } else { + const maxStat = maxStats[statName]; + + // Draw a bar for the stat. The height of the bar is scaled + // against the largest value of the stat across all snapshots. + ctx.fillStyle = EventPresenter.getBarSnapshotColor( + snapshot, Math.round(currentY / height)); + + let barHeight; + if (maxStat > 0) { + barHeight = height * Math.max(stats[statName], 0) / maxStat; + } else { + barHeight = 0; + } + + ctx.fillRect(leftView, currentY - barHeight, + Math.max(rightView - leftView, 1), barHeight); + + currentY += height; + } + } + + // Return the updated y-position. + return currentY; + }, + + drawStatNames_(leftView, height, currentY, prefix, maxStats) { + const ctx = this.context(); + + ctx.textAlign = 'end'; + ctx.font = '12px Arial'; + ctx.fillStyle = '#000000'; + for (const statName in maxStats) { + if (maxStats[statName] instanceof Object) { + currentY = this.drawStatNames_(leftView, height, currentY, + statName, maxStats[statName]); + } else { + let fullname = statName; + + if (prefix !== '') { + fullname = prefix + ' :: ' + statName; + } + + ctx.fillText(fullname, leftView - 10, currentY - height / 4); + currentY += height; + } + } + + return currentY; + } + }; + + tr.ui.tracks.ObjectInstanceTrack.register( + SystemStatsInstanceTrack, + {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'}); + + return { + SystemStatsInstanceTrack, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html new file mode 100644 index 00000000000..8dc4bc28264 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_instance_track_test.html @@ -0,0 +1,116 @@ +<!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/test_utils.html"> +<link rel="import" href="/tracing/ui/extras/system_stats/system_stats.html"> +<link rel="import" href="/tracing/ui/timeline_viewport.html"> +<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const SystemStatsInstanceTrack = + tr.ui.e.system_stats.SystemStatsInstanceTrack; + const Viewport = tr.ui.TimelineViewport; + + const createObjects = function() { + const objectInstance = new tr.model.ObjectInstance({}); + const snapshots = []; + + const stats1 = {}; + const stats2 = {}; + + stats1.committed_memory = 2000000; + stats2.committed_memory = 3000000; + + stats1.meminfo = {}; + stats1.meminfo.free = 10000; + stats2.meminfo = {}; + stats2.meminfo.free = 20000; + + stats1.perfinfo = {}; + stats1.perfinfo.idle_time = 10; + stats1.perfinfo.read_transfer_count = 20; + stats1.perfinfo.write_transfer_count = 30; + stats1.perfinfo.other_transfer_count = 40; + stats1.perfinfo.read_operation_count = 2; + stats1.perfinfo.write_operation_count = 3; + stats1.perfinfo.other_operation_count = 4; + stats1.perfinfo.pagefile_pages_written = 5; + stats1.perfinfo.pagefile_pages_write_ios = 6; + + stats2.perfinfo = {}; + stats2.perfinfo.idle_time = 110; + stats2.perfinfo.read_transfer_count = 120; + stats2.perfinfo.write_transfer_count = 130; + stats2.perfinfo.other_transfer_count = 140; + stats2.perfinfo.read_operation_count = 102; + stats2.perfinfo.write_operation_count = 103; + stats2.perfinfo.other_operation_count = 104; + stats2.perfinfo.pagefile_pages_written = 105; + stats2.perfinfo.pagefile_pages_write_ios = 106; + + snapshots.push(new tr.e.system_stats.SystemStatsSnapshot(objectInstance, + 10, stats1)); + snapshots.push(new tr.e.system_stats.SystemStatsSnapshot(objectInstance, + 20, stats2)); + + objectInstance.snapshots = snapshots; + + return objectInstance; + }; + + test('instantiate', function() { + const objectInstances = []; + objectInstances.push(createObjects()); + + const div = document.createElement('div'); + const viewport = new Viewport(div); + const drawingContainer = new tr.ui.tracks.DrawingContainer(viewport); + Polymer.dom(div).appendChild(drawingContainer); + + const track = new SystemStatsInstanceTrack(viewport); + track.objectInstances = objectInstances; + Polymer.dom(drawingContainer).appendChild(track); + + const snapshot1 = track.objectInstance_.snapshots[1]; + const stats1 = snapshot1.getStats(); + + // Raw counters should not move. + assert.strictEqual(stats1.perfinfo.idle_time, 110); + assert.strictEqual(stats1.perfinfo.read_operation_count, 102); + assert.strictEqual(stats1.perfinfo.write_operation_count, 103); + assert.strictEqual(stats1.perfinfo.other_operation_count, 104); + assert.strictEqual(stats1.perfinfo.read_transfer_count, 120); + assert.strictEqual(stats1.perfinfo.write_transfer_count, 130); + assert.strictEqual(stats1.perfinfo.other_transfer_count, 140); + assert.strictEqual(stats1.perfinfo.pagefile_pages_written, 105); + assert.strictEqual(stats1.perfinfo.pagefile_pages_write_ios, 106); + + // Rates should be computed. + assert.strictEqual(stats1.perfinfo.idle_time_per_sec, 0.001); + assert.strictEqual(stats1.perfinfo.bytes_read_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.bytes_written_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.bytes_other_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.read_operation_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.write_operation_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.other_operation_per_sec, 10000); + assert.strictEqual(stats1.perfinfo.pagefile_bytes_written_per_sec, + 40960000); + assert.strictEqual(stats1.perfinfo.pagefile_write_operation_per_sec, 10000); + + this.addHTMLOutput(div); + drawingContainer.invalidate(); + + track.heading = 'testBasic'; + const dt = new tr.ui.TimelineDisplayTransform(); + dt.xSetWorldBounds(0, 50, track.clientWidth); + track.viewport.setDisplayTransformImmediately(dt); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css new file mode 100644 index 00000000000..e698b15aa70 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.css @@ -0,0 +1,28 @@ +/* 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. + */ + +.tr-ui-e-system-stats-snapshot-view .subhead { + font-size: small; + padding-bottom: 10px; +} + +.tr-ui-e-system-stats-snapshot-view ul { + background-position: 0 5px; + background-repeat: no-repeat; + cursor: pointer; + font-family: monospace; + list-style: none; + margin: 0; + padding-left: 15px; +} + +.tr-ui-e-system-stats-snapshot-view li { + background-position: 0 5px; + background-repeat: no-repeat; + cursor: pointer; + list-style: none; + margin: 0; + padding-left: 15px; +} diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html new file mode 100644 index 00000000000..54f4b869f4a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/system_stats/system_stats_snapshot_view.html @@ -0,0 +1,84 @@ +<!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="stylesheet" + href="/tracing/ui/extras/system_stats/system_stats_snapshot_view.css"> + +<link rel="import" href="/tracing/ui/analysis/object_snapshot_view.html"> + +<script> +'use strict'; + +tr.exportTo('tr.ui.e.system_stats', function() { + /* + * Displays a system stats snapshot in a human readable form. @constructor + */ + const SystemStatsSnapshotView = tr.ui.b.define( + 'tr-ui-e-system-stats-snapshot-view', tr.ui.analysis.ObjectSnapshotView); + + SystemStatsSnapshotView.prototype = { + __proto__: tr.ui.analysis.ObjectSnapshotView.prototype, + + decorate() { + Polymer.dom(this).classList.add('tr-ui-e-system-stats-snapshot-view'); + }, + + updateContents() { + const snapshot = this.objectSnapshot_; + if (!snapshot || !snapshot.getStats()) { + Polymer.dom(this).textContent = 'No system stats snapshot found.'; + return; + } + // Clear old snapshot view. + Polymer.dom(this).textContent = ''; + + const stats = snapshot.getStats(); + Polymer.dom(this).appendChild(this.buildList_(stats)); + }, + + isFloat(n) { + return typeof n === 'number' && n % 1 !== 0; + }, + + /** + * Creates nested lists. + * + * @param {Object} stats The current trace system stats entry. + * @return {Element} A ul list element. + */ + buildList_(stats) { + const statList = document.createElement('ul'); + + for (const statName in stats) { + const statText = document.createElement('li'); + Polymer.dom(statText).textContent = '' + statName + ': '; + Polymer.dom(statList).appendChild(statText); + + if (stats[statName] instanceof Object) { + Polymer.dom(statList).appendChild(this.buildList_(stats[statName])); + } else { + if (this.isFloat(stats[statName])) { + Polymer.dom(statText).textContent += stats[statName].toFixed(2); + } else { + Polymer.dom(statText).textContent += stats[statName]; + } + } + } + + return statList; + } + }; + + tr.ui.analysis.ObjectSnapshotView.register( + SystemStatsSnapshotView, + {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'}); + + return { + SystemStatsSnapshotView, + }; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html new file mode 100644 index 00000000000..fcc410b754a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/systrace_config.html @@ -0,0 +1,18 @@ +<!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. +--> + +<!-- +TODO(charliea): Make all UI files depend on tracing/ui/base/base.html in the +same way that all non-UI files depend on tracing/base/base.html. Enforce this +dependency with a presubmit. +--> +<link rel="import" href="/tracing/ui/base/base.html" data-suppress-import-order> + +<link rel="import" href="/tracing/extras/systrace_config.html"> +<link rel="import" href="/tracing/ui/base/ui.html"> +<link rel="import" href="/tracing/ui/extras/side_panel/alerts_side_panel.html"> +<link rel="import" href="/tracing/ui/timeline_view.html"> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html new file mode 100644 index 00000000000..bc3247d9289 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table.html @@ -0,0 +1,728 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/v8/v8_gc_stats_thread_slice.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-e-v8-gc-objects-stats-table'> + <template> + <style> + tr-ui-b-table { + flex: 0 0 auto; + align-self: stretch; + margin-top: 1em; + font-size: 12px; + } + .diff { + display: inline-block; + margin-top: 1em; + margin-left: 0.8em; + } + </style> + <div class="diff" id="diffOption"> + Diff + </div> + <tr-ui-b-table id="diffTable"></tr-ui-b-table> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.v8', function() { + // Instance types that should not be part of the overview as they are either + // double-attributed (e.g. also part of some other instance type) or do not + // make any sense in memory profiling. + const IGNORED_ENTRIES = { + // Ignore code aging entries as they are already accounted in their + // respective code instance types. + match: full => full.startsWith('*CODE_AGE_') + }; + + // Groups are matched on a first-matched basis, i.e., once a group matches we + // are done with an entry. + // Requires properties: + // - match(full): Return true iff |full| should be part of the group and + // false otherwise. + // - keyToName(key): Returns the human readable name for |key|. + // - nameToKey(name): Returns the key for |name|. + // Optional properties: + // - realEntry: A string representing the actual entry in the trace. If this + // entry is present an additional entry UNKNOWN will be created holding all + // the unaccounted data. + const INSTANCE_TYPE_GROUPS = { + FIXED_ARRAY_TYPE: { + match: full => full.startsWith('*FIXED_ARRAY_'), + realEntry: 'FIXED_ARRAY_TYPE', + keyToName: key => key.slice('*FIXED_ARRAY_'.length) + .slice(0, -('_SUB_TYPE'.length)), + nameToKey: name => '*FIXED_ARRAY_' + name + '_SUB_TYPE' + }, + CODE_TYPE: { + match: full => full.startsWith('*CODE_'), + realEntry: 'CODE_TYPE', + keyToName: key => key.slice('*CODE_'.length), + nameToKey: name => '*CODE_' + name + }, + JS_OBJECTS: { + match: full => full.startsWith('JS_'), + keyToName: key => key, + nameToKey: name => name + }, + Strings: { + match: full => full.endsWith('STRING_TYPE'), + keyToName: key => key, + nameToKey: name => name + } + }; + + const DIFF_COLOR = { + GREEN: '#64DD17', + RED: '#D50000' + }; + + function computePercentage(valueA, valueB) { + if (valueA === 0) return 0; + return valueA / valueB * 100; + } + + class DiffEntry { + constructor(originalEntry, diffEntry) { + this.originalEntry_ = originalEntry; + this.diffEntry_ = diffEntry; + } + get title() { + return this.diffEntry_.title; + } + get overall() { + return this.diffEntry_.overall; + } + get overAllocated() { + return this.diffEntry_.overAllocated; + } + get count() { + return this.diffEntry_.count; + } + get overallPercent() { + return this.diffEntry_.overallPercent; + } + get overAllocatedPercent() { + return this.diffEntry_.overAllocatedPercent; + } + get origin() { + return this.originalEntry_; + } + get diff() { + return this.diffEntry_; + } + get subRows() { + return this.diffEntry_.subRows; + } + } + + class Entry { + constructor(title, count, overall, overAllocated, histogram, + overAllocatedHistogram) { + this.title_ = title; + this.overall_ = overall; + this.count_ = count; + this.overAllocated_ = overAllocated; + this.histogram_ = histogram; + this.overAllocatedHistogram_ = overAllocatedHistogram; + this.bucketSize_ = this.histogram_.length; + this.overallPercent_ = 100; + this.overAllocatedPercent_ = 100; + } + + get title() { + return this.title_; + } + + get overall() { + return this.overall_; + } + + get count() { + return this.count_; + } + + get overAllocated() { + return this.overAllocated_; + } + + get histogram() { + return this.histogram_; + } + + get overAllocatedHistogram() { + return this.overAllocatedHistogram_; + } + + get bucketSize() { + return this.bucketSize_; + } + + get overallPercent() { + return this.overallPercent_; + } + + set overallPercent(value) { + this.overallPercent_ = value; + } + + get overAllocatedPercent() { + return this.overAllocatedPercent_; + } + + set overAllocatedPercent(value) { + this.overAllocatedPercent_ = value; + } + + setFromObject(obj) { + this.count_ = obj.count; + // Calculate memory in KB. + this.overall_ = obj.overall / 1024; + this.overAllocated_ = obj.over_allocated / 1024; + this.histogram_ = obj.histogram; + this.overAllocatedHistogram_ = obj.over_allocated_histogram; + } + + diff(other) { + const entry = new Entry(this.title_, other.count_ - this.count, + other.overall_ - this.overall, + other.overAllocated_ - this.overAllocated, [], []); + entry.overallPercent = computePercentage(entry.overall, this.overall); + entry.overAllocatedPercent = computePercentage(entry.overAllocated, + this.overAllocated); + return new DiffEntry(this, entry); + } + } + + class GroupedEntry extends Entry { + constructor(title, count, overall, overAllocated, histogram, + overAllocatedHistogram) { + super(title, count, overall, overAllocated, histogram, + overAllocatedHistogram); + this.histogram_.fill(0); + this.overAllocatedHistogram_.fill(0); + this.entries_ = new Map(); + } + + get title() { + return this.title_; + } + + set title(value) { + this.title_ = value; + } + + get subRows() { + return Array.from(this.entries_.values()); + } + + getEntryFromTitle(title) { + return this.entries_.get(title); + } + + add(entry) { + this.count_ += entry.count; + this.overall_ += entry.overall; + this.overAllocated_ += entry.overAllocated; + if (this.bucketSize_ === entry.bucketSize) { + for (let i = 0; i < this.bucketSize_; ++i) { + this.histogram_[i] += entry.histogram[i]; + this.overAllocatedHistogram_[i] += entry.overAllocatedHistogram[i]; + } + } + this.entries_.set(entry.title, entry); + } + + accumulateUnknown(title) { + let unknownCount = this.count_; + let unknownOverall = this.overall_; + let unknownOverAllocated = this.overAllocated_; + const unknownHistogram = tr.b.deepCopy(this.histogram_); + const unknownOverAllocatedHistogram = + tr.b.deepCopy(this.overAllocatedHistogram_); + for (const entry of this.entries_.values()) { + unknownCount -= entry.count; + unknownOverall -= entry.overall; + unknownOverAllocated -= entry.overAllocated; + for (let i = 0; i < this.bucketSize_; ++i) { + unknownHistogram[i] -= entry.histogram[i]; + unknownOverAllocatedHistogram[i] -= entry.overAllocatedHistogram[i]; + } + } + unknownOverAllocated = + unknownOverAllocated < 0 ? 0 : unknownOverAllocated; + this.entries_.set(title, new Entry(title, unknownCount, unknownOverall, + unknownOverAllocated, unknownHistogram, + unknownOverAllocatedHistogram)); + } + + calculatePercentage() { + for (const entry of this.entries_.values()) { + entry.overallPercent = computePercentage(entry.overall, this.overall_); + entry.overAllocatedPercent = + computePercentage(entry.overAllocated, this.overAllocated_); + + if (entry instanceof GroupedEntry) entry.calculatePercentage(); + } + } + + diff(other) { + let newTitle = ''; + if (this.title_.startsWith('Isolate')) { + newTitle = 'Total'; + } else { + newTitle = this.title_; + } + const result = new GroupedEntry(newTitle, 0, 0, 0, [], []); + for (const entry of this.entries_) { + const otherEntry = other.getEntryFromTitle(entry[0]); + if (otherEntry === undefined) continue; + result.add(entry[1].diff(otherEntry)); + } + result.overallPercent = computePercentage(result.overall, this.overall); + result.overAllocatedPercent = computePercentage(result.overAllocated, + this.overAllocated); + return new DiffEntry(this, result); + } + } + + function createSelector(targetEl, defaultValue, items, callback) { + const selectorEl = document.createElement('select'); + selectorEl.addEventListener('change', callback.bind(targetEl)); + const defaultOptionEl = document.createElement('option'); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const optionEl = document.createElement('option'); + Polymer.dom(optionEl).textContent = item.label; + optionEl.targetPropertyValue = item.value; + optionEl.item = item; + Polymer.dom(selectorEl).appendChild(optionEl); + } + selectorEl.__defineGetter__('selectedValue', function(v) { + if (selectorEl.children[selectorEl.selectedIndex] === undefined) { + return undefined; + } + return selectorEl.children[selectorEl.selectedIndex].targetPropertyValue; + }); + selectorEl.__defineGetter__('selectedItem', function(v) { + if (selectorEl.children[selectorEl.selectedIndex] === undefined) { + return undefined; + } + return selectorEl.children[selectorEl.selectedIndex].item; + }); + selectorEl.__defineSetter__('selectedValue', function(v) { + for (let i = 0; i < selectorEl.children.length; i++) { + const value = selectorEl.children[i].targetPropertyValue; + if (value === v) { + const changed = selectorEl.selectedIndex !== i; + if (changed) { + selectorEl.selectedIndex = i; + callback(); + } + return; + } + } + throw new Error('Not a valid value'); + }); + selectorEl.selectedIndex = -1; + + return selectorEl; + } + + function plusMinus(value, toFixed = 3) { + return (value > 0 ? '+' : '') + value.toFixed(toFixed); + } + + function addArrow(value) { + if (value === 0) return value; + if (value === Number.NEGATIVE_INFINITY) return '\u2193\u221E'; + if (value === Number.POSITIVE_INFINITY) return '\u2191\u221E'; + return (value > 0 ? '\u2191' : '\u2193') + Math.abs(value.toFixed(3)); + } + + Polymer({ + is: 'tr-ui-e-v8-gc-objects-stats-table', + + ready() { + this.$.diffOption.style.display = 'none'; + this.isolateEntries_ = []; + this.selector1_ = undefined; + this.selector2_ = undefined; + }, + + constructDiffTable_(table) { + this.$.diffTable.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + this.$.diffTable.tableColumns = [ + { + title: 'Component', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.title; + return typeEl; + }, + showExpandButtons: true + }, + { + title: 'Overall Memory(KB)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = row.origin.overall.toFixed(3); + return spanEl; + }, + cmp(a, b) { + return a.origin.overall - b.origin.overall; + } + }, + { + title: 'diff(KB)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = plusMinus(row.overall); + if (row.overall > 0) { + spanEl.style.color = DIFF_COLOR.RED; + } else if (row.overall < 0) { + spanEl.style.color = DIFF_COLOR.GREEN; + } + return spanEl; + }, + cmp(a, b) { + return a.overall - b.overall; + } + }, + { + title: 'diff(%)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = addArrow(row.overallPercent); + if (row.overall > 0) { + spanEl.style.color = DIFF_COLOR.RED; + } else if (row.overall < 0) { + spanEl.style.color = DIFF_COLOR.GREEN; + } + return spanEl; + }, + cmp(a, b) { + return a.overall - b.overall; + } + }, + { + title: 'Over Allocated Memory(KB)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = row.origin.overAllocated.toFixed(3); + return spanEl; + }, + cmp(a, b) { + return a.origin.overAllocated - b.origin.overAllocated; + } + }, + { + title: 'diff(KB)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = plusMinus(row.overAllocated); + if (row.overAllocated > 0) { + spanEl.style.color = DIFF_COLOR.RED; + } else if (row.overAllocated < 0) { + spanEl.style.color = DIFF_COLOR.GREEN; + } + return spanEl; + }, + cmp(a, b) { + return a.overAllocated - b.overAllocated; + } + }, + { + title: 'diff(%)', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = addArrow(row.overAllocatedPercent); + if (row.overAllocated > 0) { + spanEl.style.color = DIFF_COLOR.RED; + } else if (row.overAllocated < 0) { + spanEl.style.color = DIFF_COLOR.GREEN; + } + return spanEl; + }, + cmp(a, b) { + return a.overAllocated - b.overAllocated; + } + }, + { + title: 'Count', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = row.origin.count; + return spanEl; + }, + cmp(a, b) { + return a.origin.count - b.origin.count; + } + }, + { + title: 'diff', + value(row) { + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = plusMinus(row.count, 0); + if (row.count > 0) { + spanEl.style.color = DIFF_COLOR.RED; + } else if (row.count < 0) { + spanEl.style.color = DIFF_COLOR.GREEN; + } + return spanEl; + }, + cmp(a, b) { + return a.count - b.count; + } + }, + ]; + }, + + buildOptions_() { + const items = []; + for (const isolateEntry of this.isolateEntries_) { + items.push({ + label: isolateEntry.title, + value: isolateEntry + }); + } + this.$.diffOption.style.display = 'inline-block'; + this.selector1_ = createSelector( + this, '', items, this.diffOptionChanged_); + Polymer.dom(this.$.diffOption).appendChild(this.selector1_); + const spanEl = tr.ui.b.createSpan(); + spanEl.innerText = ' VS '; + Polymer.dom(this.$.diffOption).appendChild(spanEl); + this.selector2_ = createSelector( + this, '', items, this.diffOptionChanged_); + Polymer.dom(this.$.diffOption).appendChild(this.selector2_); + }, + + diffOptionChanged_() { + const isolateEntry1 = this.selector1_.selectedValue; + const isolateEntry2 = this.selector2_.selectedValue; + if (isolateEntry1 === undefined || isolateEntry2 === undefined) { + return; + } + if (isolateEntry1 === isolateEntry2) { + this.$.diffTable.tableRows = []; + this.$.diffTable.rebuild(); + return; + } + this.$.diffTable.tableRows = [isolateEntry1.diff(isolateEntry2)]; + this.$.diffTable.rebuild(); + }, + + constructTable_() { + this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + this.$.table.tableColumns = [ + { + title: 'Component', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.title; + return typeEl; + }, + showExpandButtons: true + }, + { + title: 'Overall Memory (KB)', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.overall.toFixed(3); + return typeEl; + }, + cmp(a, b) { + return a.overall - b.overall; + } + }, + { + title: 'Over Allocated Memory (KB)', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.overAllocated.toFixed(3); + return typeEl; + }, + cmp(a, b) { + return a.overAllocated - b.overAllocated; + } + }, + { + title: 'Overall Count', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.count; + return typeEl; + }, + cmp(a, b) { + return a.count - b.count; + } + }, + { + title: 'Overall Memory Percent', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.overallPercent.toFixed(3) + '%'; + return typeEl; + }, + cmp(a, b) { + return a.overall - b.overall; + } + }, + { + title: 'Overall Allocated Memory Percent', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.overAllocatedPercent.toFixed(3) + '%'; + return typeEl; + }, + cmp(a, b) { + return a.overAllocated - b.overAllocated; + } + } + ]; + + this.$.table.sortColumnIndex = 1; + this.$.table.sortDescending = true; + }, + + buildSubEntry_(objects, groupEntry, keyToName) { + const typeGroup = INSTANCE_TYPE_GROUPS[groupEntry.title]; + for (const instanceType of typeGroup) { + const e = objects[instanceType]; + if (e === undefined) continue; + delete objects[instanceType]; + let title = instanceType; + if (keyToName !== undefined) title = keyToName(title); + // Represent memery in KB unit. + groupEntry.add(new Entry(title, e.count, e.overall / 1024, + e.over_allocated / 1024, e.histogram, + e.over_allocated_histogram)); + } + }, + + buildUnGroupedEntries_(objects, objectEntry, bucketSize) { + for (const title of Object.getOwnPropertyNames(objects)) { + const obj = objects[title]; + const groupedEntry = new GroupedEntry(title, 0, 0, 0, + new Array(bucketSize), + new Array(bucketSize)); + groupedEntry.setFromObject(obj); + objectEntry.add(groupedEntry); + } + }, + + createGroupEntries_(groupEntries, objects, bucketSize) { + for (const groupName of Object.getOwnPropertyNames( + INSTANCE_TYPE_GROUPS)) { + const groupEntry = new GroupedEntry(groupName, 0, 0, 0, + new Array(bucketSize), + new Array(bucketSize)); + if (INSTANCE_TYPE_GROUPS[groupName].realEntry !== undefined) { + groupEntry.savedRealEntry = + objects[INSTANCE_TYPE_GROUPS[groupName].realEntry]; + delete objects[INSTANCE_TYPE_GROUPS[groupName].realEntry]; + } + groupEntries[groupName] = groupEntry; + } + }, + + buildGroupEntries_(groupEntries, objectEntry) { + for (const groupName of Object.getOwnPropertyNames(groupEntries)) { + const groupEntry = groupEntries[groupName]; + if (groupEntry.savedRealEntry !== undefined) { + groupEntry.setFromObject(groupEntry.savedRealEntry); + groupEntry.accumulateUnknown('UNKNOWN'); + delete groupEntry.savedRealEntry; + } + objectEntry.add(groupEntry); + } + }, + + buildSubEntriesForGroups_(groupEntries, objects) { + for (const instanceType of Object.getOwnPropertyNames(objects)) { + if (IGNORED_ENTRIES.match(instanceType)) { + delete objects[instanceType]; + continue; + } + const e = objects[instanceType]; + for (const name of Object.getOwnPropertyNames(INSTANCE_TYPE_GROUPS)) { + const group = INSTANCE_TYPE_GROUPS[name]; + if (group.match(instanceType)) { + groupEntries[name].add(new Entry( + group.keyToName(instanceType), e.count, e.overall / 1024, + e.over_allocated / 1024, e.histogram, + e.over_allocated_histogram)); + delete objects[instanceType]; + } + } + } + }, + + build_(objects, objectEntry, bucketSize) { + delete objects.END; + const groupEntries = {}; + this.createGroupEntries_(groupEntries, objects, bucketSize); + this.buildSubEntriesForGroups_(groupEntries, objects); + this.buildGroupEntries_(groupEntries, objectEntry); + this.buildUnGroupedEntries_(objects, objectEntry, bucketSize); + }, + + set selection(slices) { + slices.sortEvents(function(a, b) { + return b.start - a.start; + }); + const previous = undefined; + for (const slice of slices) { + if (!slice instanceof tr.e.v8.V8GCStatsThreadSlice) continue; + const liveObjects = slice.liveObjects; + const deadObjects = slice.deadObjects; + const isolate = liveObjects.isolate; + + const isolateEntry = + new GroupedEntry( + 'Isolate_' + isolate + ' at ' + slice.start.toFixed(3) + ' ms', + 0, 0, 0, [], []); + const liveEntry = new GroupedEntry('live objects', 0, 0, 0, [], []); + const deadEntry = new GroupedEntry('dead objects', 0, 0, 0, [], []); + + const liveBucketSize = liveObjects.bucket_sizes.length; + const deadBucketSize = deadObjects.bucket_sizes.length; + + this.build_(tr.b.deepCopy(liveObjects.type_data), liveEntry, + liveBucketSize); + isolateEntry.add(liveEntry); + + this.build_(tr.b.deepCopy(deadObjects.type_data), deadEntry, + deadBucketSize); + isolateEntry.add(deadEntry); + + isolateEntry.calculatePercentage(); + this.isolateEntries_.push(isolateEntry); + } + this.updateTable_(); + + if (slices.length > 1) { + this.buildOptions_(); + this.constructDiffTable_(); + } + }, + + updateTable_() { + this.constructTable_(); + this.$.table.tableRows = this.isolateEntries_; + this.$.table.rebuild(); + }, + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html new file mode 100644 index 00000000000..42d904d94c8 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/gc_objects_stats_table_test.html @@ -0,0 +1,198 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/v8/v8_gc_stats_thread_slice.html"> +<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + + function createModel() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.s1 = m.t2.sliceGroup.pushSlice( + newSliceEx({ + title: 'V8.GC_Objects_Stats', + start: 1, + end: 1, + type: tr.e.v8.V8GCStatsThreadSlice, + cat: 'disabled-by-default-v8.gc_stats', + args: { + // eslint-disable-next-line + live:'{"isolate":"0x00000000001","id":1,"time":111,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"JS_OBJECT_TYPE":{"type":4,"overall":5,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"JS_TYPED_ARRAY_TYPE":{"type":5,"overall":5,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"CODE_TYPE":{"type":4,"overall":6,"count":6,"over_allocated":0,"histogram":[6,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*CODE_BYTECODE_HANDLER":{"type":7,"overall":5,"count":6,"over_allocated":0,"histogram":[6,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*CODE_AGE_Quadragenarian":{"type":8,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}', + // eslint-disable-next-line + dead:'{"isolate":"0x00000000001","id":2,"time":112,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":3,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":1,"count":1,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}' + } + }) + ); + m.s2 = m.t2.sliceGroup.pushSlice( + newSliceEx({ + title: 'V8.GC_Objects_Stats', + start: 2, + end: 2, + type: tr.e.v8.V8GCStatsThreadSlice, + cat: 'disabled-by-default-v8.gc_stats', + args: { + // eslint-disable-next-line + live:'{"isolate":"0x00000000001","id":1,"time":113,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":3,"count":4,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":6,"count":7,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}', + // eslint-disable-next-line + dead:'{"isolate":"0x00000000001","id":2,"time":114,"bucket_sizes":[32,64,128,256],"type_data":{"STRING_TYPE":{"type":1,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"FIXED_ARRAY_TYPE":{"type":2,"overall":4,"count":4,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]},"*FIXED_ARRAY_CONTEXT_SUB_TYPE":{"type":3,"overall":2,"count":2,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}' + } + }) + ); + m.s3 = m.t2.sliceGroup.pushSlice( + newSliceEx({ + title: 'V8.GC_Objects_Stats', + start: 3, + end: 3, + type: tr.e.v8.V8GCStatsThreadSlice, + cat: 'disabled-by-default-v8.gc_stats', + args: { + // eslint-disable-next-line + live:'{"isolate":"0x00000000001","id":1,"time":115,"bucket_sizes":[32,64,128,256],"type_data":{"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}, "TYPE_DONT_HAVE_GROUP1":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}, "TYPE_DONT_HAVE_GROUP2":{"type":1,"overall":2,"count":3,"over_allocated":0,"histogram":[0,1,0,0],"over_allocated_histogram":[0,0,0,0]}}}', + // eslint-disable-next-line + dead:'{"isolate":"0x00000000001","id":2,"time":116,"bucket_sizes":[32,64,128,256],"type_data":{"FIXED_ARRAY_TYPE":{"type":2,"overall":5,"count":6,"over_allocated":0,"histogram":[1,0,0,0],"over_allocated_histogram":[0,0,0,0]}}}' + } + }) + ); + }); + return m; + } + + test('GCObjectTableSingleSelection', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table'); + const eventSet = new tr.model.EventSet(); + eventSet.push(m.s1); + viewEl.selection = eventSet; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 1); + const row = rows[0]; + assert.strictEqual(row.overall, 0.0263671875); + assert.strictEqual(row.count, 21); + assert.strictEqual(row.overAllocated, 0); + const subRows = row.subRows; + const live = subRows[0]; + assert.strictEqual(live.overall, 0.0224609375); + assert.strictEqual(live.count, 17); + assert.strictEqual(live.overAllocated, 0); + const dead = subRows[1]; + assert.strictEqual(dead.overall, 0.00390625); + assert.strictEqual(dead.count, 4); + assert.strictEqual(dead.overAllocated, 0); + }); + + test('GCObjectTableMultiSelection', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table'); + const eventSet = new tr.model.EventSet(); + eventSet.push(m.s1); + eventSet.push(m.s2); + viewEl.selection = eventSet; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 2); + + let row = rows[0]; + assert.strictEqual(row.overall, 0.0146484375); + assert.strictEqual(row.count, 17); + assert.strictEqual(row.overAllocated, 0); + let subRows = row.subRows; + let live = subRows[0]; + assert.strictEqual(live.overall, 0.0087890625); + assert.strictEqual(live.count, 11); + assert.strictEqual(live.overAllocated, 0); + let dead = subRows[1]; + assert.strictEqual(dead.overall, 0.005859375); + assert.strictEqual(dead.count, 6); + assert.strictEqual(dead.overAllocated, 0); + + row = rows[1]; + assert.strictEqual(row.overall, 0.0263671875); + assert.strictEqual(row.count, 21); + assert.strictEqual(row.overAllocated, 0); + subRows = row.subRows; + live = subRows[0]; + assert.strictEqual(live.overall, 0.0224609375); + assert.strictEqual(live.count, 17); + assert.strictEqual(live.overAllocated, 0); + dead = subRows[1]; + assert.strictEqual(dead.overall, 0.00390625); + assert.strictEqual(dead.count, 4); + assert.strictEqual(dead.overAllocated, 0); + }); + + test('GCObjectTableDiff', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table'); + const eventSet = new tr.model.EventSet(); + eventSet.push(m.s1); + eventSet.push(m.s2); + viewEl.selection = eventSet; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 2); + const diffEntry = rows[0].diff(rows[1]); + + assert.strictEqual(diffEntry.origin.overall.toFixed(3), '0.015'); + assert.strictEqual(diffEntry.origin.overAllocated, 0); + assert.strictEqual(diffEntry.overall.toFixed(3), '-0.004'); + assert.strictEqual(diffEntry.overAllocated, 0); + assert.strictEqual(diffEntry.overallPercent.toFixed(3), '-26.667'); + assert.strictEqual(diffEntry.overAllocatedPercent, 0); + }); + + test('GCObjectTableGroupEntryWithoutGroupDefined', function() { + const m = createModel(); + + const viewEl = document.createElement('tr-ui-e-v8-gc-objects-stats-table'); + const eventSet = new tr.model.EventSet(); + eventSet.push(m.s3); + viewEl.selection = eventSet; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 1); + const row = rows[0]; + assert.strictEqual(row.overall, 0.013671875); + assert.strictEqual(row.count, 18); + assert.strictEqual(row.overAllocated, 0); + const subRows = row.subRows; + + const live = subRows[0]; + assert.strictEqual(live.overall, 0.0087890625); + assert.strictEqual(live.count, 12); + assert.strictEqual(live.overAllocated, 0); + + // ungrouped entry should be top level entry. + const unGrouped1 = live.getEntryFromTitle('TYPE_DONT_HAVE_GROUP1'); + assert.isDefined(unGrouped1); + assert.strictEqual(unGrouped1.overall, 0.001953125); + assert.strictEqual(unGrouped1.count, 3); + assert.strictEqual(unGrouped1.overAllocated, 0); + + const unGrouped2 = live.getEntryFromTitle('TYPE_DONT_HAVE_GROUP2'); + assert.isDefined(unGrouped2); + assert.strictEqual(unGrouped2.overall, 0.001953125); + assert.strictEqual(unGrouped2.count, 3); + assert.strictEqual(unGrouped2.overAllocated, 0); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html new file mode 100644 index 00000000000..e19a367cfca --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/ic_stats_table.html @@ -0,0 +1,181 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/v8/ic_stats_entry.html"> +<link rel="import" href="/tracing/extras/v8/v8_ic_stats_thread_slice.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-e-v8-ic-stats-table'> + <template> + <style> + tr-ui-b-table { + flex: 0 0 auto; + align-self: stretch; + margin-top: 1em; + font-size: 12px; + } + #total { + margin-top: 1em; + margin-left: 0.8em; + } + #groupOption { + display: inline-block; + margin-top: 1em; + margin-left: 0.8em; + } + </style> + <div style="padding-right: 200px"> + <div style="float:right; border-style: solid; border-width: 1px; padding:20px"> + 0 uninitialized<br> + . premonomorphic<br> + 1 monomorphic<br> + ^ recompute handler<br> + P polymorphic<br> + N megamorphic<br> + G generic + </div> + </div> + <div id="total"> + </div> + <div id="groupOption"> + Group Key + </div> + <tr-ui-b-table id="table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.v8', function() { + const PROPERTIES = tr.e.v8.IC_STATS_PROPERTIES.map( + x => {return {label: x, value: x};}); + const ICStatsEntry = tr.e.v8.ICStatsEntry; + const ICStatsEntryGroup = tr.e.v8.ICStatsEntryGroup; + const ICStatsCollection = tr.e.v8.ICStatsCollection; + + Polymer({ + is: 'tr-ui-e-v8-ic-stats-table', + + ready() { + this.icStatsCollection_ = new ICStatsCollection(); + this.groupKey_ = PROPERTIES[0].value; + this.selector_ = tr.ui.b.createSelector(this, 'groupKey', + 'v8ICStatsGroupKey', + this.groupKey_, PROPERTIES); + Polymer.dom(this.$.groupOption).appendChild(this.selector_); + }, + + get groupKey() { + return this.groupKey_; + }, + + set groupKey(key) { + this.groupKey_ = key; + if (this.icStatsCollection_.length === 0) return; + this.updateTable_(this.groupKey_); + }, + + constructTable_(table, groupKey) { + table.tableColumns = [ + { + title: '', + value: row => { + let expanded = false; + const buttonEl = tr.ui.b.createButton('details', function() { + const previousSibling = Polymer.dom(this).parentNode.parentNode; + const parentNode = previousSibling.parentNode; + if (expanded) { + const trEls = parentNode.getElementsByClassName('subTable'); + Array.from(trEls).map(x => x.parentNode.removeChild(x)); + expanded = false; + return; + } + expanded = true; + const subGroups = row.createSubGroup(); + const tr = document.createElement('tr'); + tr.classList.add('subTable'); + tr.appendChild(document.createElement('td')); + const td = document.createElement('td'); + td.colSpan = 3; + for (const subGroup of subGroups) { + const property = subGroup[0]; + const all = Array.from(subGroup[1].values()); + const group = all.slice(0, 20); + const divEl = document.createElement('div'); + const spanEl = document.createElement('span'); + const subTableEl = document.createElement('tr-ui-b-table'); + + spanEl.innerText = `Top 20 out of ${all.length}`; + spanEl.style.fontWeight = 'bold'; + spanEl.style.fontSize = '14px'; + divEl.appendChild(spanEl); + + this.constructTable_(subTableEl, property); + subTableEl.tableRows = group; + subTableEl.rebuild(); + divEl.appendChild(subTableEl); + td.appendChild(divEl); + } + tr.appendChild(td); + parentNode.insertBefore(tr, previousSibling.nextSibling); + }); + return buttonEl; + } + }, + { + title: 'Percentage', + value(row) { + const spanEl = document.createElement('span'); + spanEl.innerText = (row.percentage * 100).toFixed(3) + '%'; + return spanEl; + }, + cmp: (a, b) => a.percentage - b.percentage + }, + { + title: 'Count', + value(row) { + const spanEl = document.createElement('span'); + spanEl.innerText = row.length; + return spanEl; + }, + cmp: (a, b) => a.length - b.length + }, + { + title: groupKey, + value(row) { + const spanEl = document.createElement('span'); + spanEl.innerText = row.key ? row.key : ''; + return spanEl; + } + } + ]; + + table.sortColumnIndex = 1; + table.sortDescending = true; + }, + + updateTable_(groupKey) { + this.constructTable_(this.$.table, groupKey); + this.$.table.tableRows = this.icStatsCollection_.groupBy(groupKey); + this.$.table.rebuild(); + }, + + set selection(slices) { + for (const slice of slices) { + for (const icStatsObj of slice.icStats) { + const entry = new ICStatsEntry(icStatsObj); + this.icStatsCollection_.add(entry); + } + } + this.$.total.innerText = 'Total items: ' + this.icStatsCollection_.length; + this.updateTable_(this.selector_.selectedValue); + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html new file mode 100644 index 00000000000..eded2c44a66 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html"> + +<dom-module id='tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view'> + <template> + <style> + </style> + <tr-ui-e-v8-gc-objects-stats-table id="gcObjectsStats"> + </tr-ui-e-v8-gc-objects-stats-table> + </template> +</dom-module> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.gcObjectsStats.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-multi-v8-gc-stats-thread-slice-sub-view', + tr.e.v8.V8GCStatsThreadSlice, + { + multi: true, + title: 'V8 GC Stats slices' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html new file mode 100644 index 00000000000..36b14cb7be0 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/ic_stats_table.html"> + +<dom-module id='tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view'> + <template> + <tr-ui-e-v8-ic-stats-table id="table"> + </tr-ui-e-v8-ic-stats-table> + </template> +</dom-module> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.table.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-multi-v8-ic-stats-thread-slice-sub-view', + tr.e.v8.V8ICStatsThreadSlice, + { + multi: true, + title: 'V8 IC stats slices' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html new file mode 100644 index 00000000000..48c1f05b274 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html"> + +<dom-module id='tr-ui-e-multi-v8-thread-slice-sub-view'> + <template> + <tr-ui-a-multi-thread-slice-sub-view id="content"></tr-ui-a-multi-thread-slice-sub-view> + <tr-ui-e-v8-runtime-call-stats-table id="runtimeCallStats"></tr-ui-e-v8-runtime-call-stats-table> + </template> +</dom-module> + +<script> +'use strict'; + +Polymer({ + is: 'tr-ui-e-multi-v8-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.runtimeCallStats.slices = selection; + this.$.content.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-multi-v8-thread-slice-sub-view', + tr.e.v8.V8ThreadSlice, + { + multi: true, + title: 'V8 slices' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html new file mode 100644 index 00000000000..d0d62bed87d --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view_test.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html"> +<link rel="import" href="/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + + function createModel() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.s1 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 0, + end: 10, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + CompileFullCode: [3, 345], + LoadIC_Miss: [5, 567], + ParseLazy: [8, 890] + }}})); + m.s2 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 11, + end: 15, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + HandleApiCall: [1, 123], + OptimizeCode: [7, 789] + }}})); + }); + return m; + } + + test('selectMultiV8ThreadSlices', function() { + const m = createModel(); + + const viewEl = + document.createElement('tr-ui-e-multi-v8-thread-slice-sub-view'); + const selection = new tr.model.EventSet(); + selection.push(m.s1); + selection.push(m.s2); + viewEl.selection = selection; + this.addHTMLOutput(viewEl); + const rows = viewEl.$.runtimeCallStats.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 2714, + 567, + 0, + 789, + 0, + 345, + 0, + 890, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 123 + ]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html new file mode 100644 index 00000000000..27bf0f5ef72 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table.html @@ -0,0 +1,197 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/extras/v8/runtime_stats_entry.html"> +<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html"> +<link rel="import" href="/tracing/ui/base/table.html"> + +<dom-module id='tr-ui-e-v8-runtime-call-stats-table'> + <template> + <style> + #table, #blink_rcs_table { + flex: 0 0 auto; + align-self: stretch; + margin-top: 1em; + font-size: 12px; + } + + #v8_rcs_heading, #blink_rcs_heading { + padding-top: 1em; + font-size: 18px; + } + </style> + <h1 id="v8_rcs_heading"></h1> + <tr-ui-b-table id="table"></tr-ui-b-table> + <h1 id="blink_rcs_heading"></h1> + <tr-ui-b-table id="blink_rcs_table"></tr-ui-b-table> + </template> +</dom-module> +<script> +'use strict'; + +tr.exportTo('tr.ui.e.v8', function() { + const codeSearchURL_ = 'https://cs.chromium.org/search/?sq=package:chromium&type=cs&q='; + + function removeBlinkPrefix_(name) { + if (name.startsWith('Blink_')) name = name.substring(6); + return name; + } + + function handleCodeSearchForV8_(event) { + if (event.target.parentNode === undefined) return; + let name = event.target.parentNode.entryName; + if (name.startsWith('API_')) name = name.substring(4); + const url = codeSearchURL_ + encodeURIComponent(name) + '+file:src/v8/src'; + window.open(url, '_blank'); + } + + function handleCodeSearchForBlink_(event) { + if (event.target.parentNode === undefined) return; + const name = event.target.parentNode.entryName; + const url = codeSearchURL_ + + encodeURIComponent('RuntimeCallStats::CounterId::k' + name) + + '+file:src/third_party/WebKit/|src/out/Debug/'; + window.open(url, '_blank'); + } + + function createCodeSearchEl_(handleCodeSearch) { + const codeSearchEl = document.createElement('span'); + codeSearchEl.innerText = '?'; + codeSearchEl.style.float = 'right'; + codeSearchEl.style.borderRadius = '5px'; + codeSearchEl.style.backgroundColor = '#EEE'; + codeSearchEl.addEventListener('click', + handleCodeSearch.bind(this)); + return codeSearchEl; + } + + const timeColumn_ = { + title: 'Time', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = (row.time / 1000.0).toFixed(3) + ' ms'; + return typeEl; + }, + width: '100px', + cmp(a, b) { + return a.time - b.time; + } + }; + + const countColumn_ = { + title: 'Count', + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = row.count; + return typeEl; + }, + width: '100px', + cmp(a, b) { + return a.count - b.count; + } + }; + + function percentColumn_(title, totalTime) { + return { + title, + value(row) { + const typeEl = document.createElement('span'); + typeEl.innerText = (row.time / totalTime * 100).toFixed(3) + '%'; + return typeEl; + }, + width: '100px', + cmp(a, b) { + return a.time - b.time; + } + }; + } + + function nameColumn_(handleCodeSearch, modifyName) { + return { + title: 'Name', + value(row) { + const typeEl = document.createElement('span'); + let name = row.name; + if (modifyName) name = modifyName(name); + typeEl.innerText = name; + if (!(row instanceof tr.e.v8.RuntimeStatsGroup)) { + typeEl.title = 'click ? for code search'; + typeEl.entryName = name; + const codeSearchEl = createCodeSearchEl_(handleCodeSearch); + typeEl.appendChild(codeSearchEl); + } + return typeEl; + }, + width: '200px', + showExpandButtons: true + }; + } + + function initializeCommonOptions_(table) { + table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW; + table.sortColumnIndex = 1; + table.sortDescending = true; + table.subRowsPropertyName = 'values'; + } + + Polymer({ + is: 'tr-ui-e-v8-runtime-call-stats-table', + + ready() { + this.table_ = this.$.table; + this.blink_rcs_table_ = this.$.blink_rcs_table; + this.totalTime_ = 0; + }, + + constructV8RCSTable_(totalTime) { + this.table_.tableColumns = [ + nameColumn_(handleCodeSearchForV8_), + timeColumn_, + countColumn_, + percentColumn_('Percent', totalTime) + ]; + + initializeCommonOptions_(this.table_); + }, + + constructBlinkRCSTable_(blinkCppTotalTime) { + this.blink_rcs_table_.tableColumns = [ + nameColumn_(handleCodeSearchForBlink_, removeBlinkPrefix_), + timeColumn_, + countColumn_, + percentColumn_('Percent (of \'Blink C++\' + \'API\')', + blinkCppTotalTime) + ]; + + initializeCommonOptions_(this.blink_rcs_table_); + }, + + set slices(slices) { + const runtimeGroupCollection = new tr.e.v8.RuntimeStatsGroupCollection(); + runtimeGroupCollection.addSlices(slices); + if (runtimeGroupCollection.totalTime > 0) { + this.$.v8_rcs_heading.textContent = 'V8 Runtime Call Stats'; + this.constructV8RCSTable_(runtimeGroupCollection.totalTime); + this.table_.tableRows = runtimeGroupCollection.runtimeGroups; + this.table_.rebuild(); + } + + const blinkRCSGroupCollection = + runtimeGroupCollection.blinkRCSGroupCollection; + if (runtimeGroupCollection.blinkCppTotalTime > 0 && + blinkRCSGroupCollection.totalTime > 0) { + this.$.blink_rcs_heading.textContent = 'Blink Runtime Call Stats'; + this.constructBlinkRCSTable_(runtimeGroupCollection.blinkCppTotalTime); + this.blink_rcs_table_.tableRows = blinkRCSGroupCollection.runtimeGroups; + this.blink_rcs_table_.rebuild(); + } + } + }); + + return {}; +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html new file mode 100644 index 00000000000..06698d03c4a --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/runtime_call_stats_table_test.html @@ -0,0 +1,236 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html"> +<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + const apiObjectGet = [1, 123]; + const functionCallback = [2, 234]; + const compileFullCode = [3, 345]; + const allocateInTargetSpace = [4, 456]; + const loadIcMiss = [5, 567]; + const jsExecution = [6, 678]; + const optimizeCode = [7, 789]; + const parseLazy = [8, 890]; + const handleApiCall = [9, 901]; + const compileBackground = [1, 101]; + const parseBackground = [2, 202]; + const optimizeCodeBackground = [3, 303]; + + function createModel() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.s1 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 0, + end: 10, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + JS_Execution: jsExecution, + HandleApiCall: handleApiCall, + CompileFullCode: compileFullCode, + LoadIC_Miss: loadIcMiss, + ParseLazy: parseLazy, + RecompileConcurrent: optimizeCode, + OptimizeCode: optimizeCode, + FunctionCallback: functionCallback, + AllocateInTargetSpace: allocateInTargetSpace, + API_Object_Get: apiObjectGet, + CompileBackgroundIgnition: compileBackground, + ParseBackgroundFunctionLiteral: parseBackground, + RecompileConcurrent: optimizeCodeBackground + }}})); + m.s2 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 11, + end: 15, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + JS_Execution: jsExecution, + HandleApiCall: handleApiCall, + CompileFullCode: compileFullCode, + LoadIC_Miss: loadIcMiss, + ParseLazy: parseLazy, + OptimizeCode: optimizeCode, + FunctionCallback: functionCallback, + AllocateInTargetSpace: allocateInTargetSpace, + API_Object_Get: apiObjectGet + }}})); + m.s3 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 11, + end: 15, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + LoadIC_LoadCallback: [1, 111], + StoreIC_StoreCallback: [2, 222], + }}})); + }); + return m; + } + + test('SingleSliceSelection', function() { + const m = createModel(); + + const viewEl = document.createElement( + 'tr-ui-e-v8-runtime-call-stats-table'); + viewEl.slices = [m.s1]; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 5589, + loadIcMiss[1], + optimizeCodeBackground[1], + optimizeCode[1], + compileBackground[1], + compileFullCode[1], + parseBackground[1], + parseLazy[1], + functionCallback[1], + apiObjectGet[1], + 0, + 0, + 0, + 0, + 0, + 0, + allocateInTargetSpace[1], + jsExecution[1], + handleApiCall[1] + ]); + }); + + test('MultiSliceSelection', function() { + const m = createModel(); + + const viewEl = document.createElement( + 'tr-ui-e-v8-runtime-call-stats-table'); + viewEl.slices = [m.s1, m.s2]; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 10572, + loadIcMiss[1] * 2, + optimizeCodeBackground[1], + optimizeCode[1] * 2, + compileBackground[1], + compileFullCode[1] * 2, + parseBackground[1], + parseLazy[1] * 2, + functionCallback[1] * 2, + apiObjectGet[1] * 2, + 0, + 0, + 0, + 0, + 0, + 0, + allocateInTargetSpace[1] * 2, + jsExecution[1] * 2, + handleApiCall[1] * 2 + ]); + + assert.deepEqual(rows.map(r => r.entries_.size), [ + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1 + ]); + }); + + test('groupCorrectly', function() { + const m = createModel(); + + const viewEl = document.createElement( + 'tr-ui-e-v8-runtime-call-stats-table'); + viewEl.slices = [m.s3]; + this.addHTMLOutput(viewEl); + tr.b.forceAllPendingTasksToRunForTest(); + const rows = viewEl.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 333, + 333, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ]); + + assert.deepEqual(rows.map(r => r.entries_.size), [ + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html new file mode 100644 index 00000000000..6a8d5f15b73 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html"> + +<dom-module id='tr-ui-e-single-v8-gc-stats-thread-slice-sub-view'> + <template> + <tr-ui-a-single-event-sub-view id="content"></tr-ui-a-single-event-sub-view> + <tr-ui-e-v8-gc-objects-stats-table id="gcObjectsStats"></tr-ui-e-v8-gc-objects-stats-table> + </template> +</dom-module> + +<script> +'use strict'; +Polymer({ + is: 'tr-ui-e-single-v8-gc-stats-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.content.selection = selection; + this.$.gcObjectsStats.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-single-v8-gc-stats-thread-slice-sub-view', + tr.e.v8.V8GCStatsThreadSlice, + { + multi: false, + title: 'V8 GC stats slice' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html new file mode 100644 index 00000000000..eeaf407eab1 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/ic_stats_table.html"> + +<dom-module id='tr-ui-e-single-v8-ic-stats-thread-slice-sub-view'> + <template> + <tr-ui-e-v8-ic-stats-table id="table"> + </tr-ui-e-v8-ic-stats-table> + </template> +</dom-module> + +<script> +'use strict'; +Polymer({ + is: 'tr-ui-e-single-v8-ic-stats-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.table.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-single-v8-ic-stats-thread-slice-sub-view', + tr.e.v8.V8ICStatsThreadSlice, + { + multi: false, + title: 'V8 IC stats slice' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html new file mode 100644 index 00000000000..a9b1189fc76 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/ui/analysis/analysis_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html"> + +<dom-module id='tr-ui-e-single-v8-thread-slice-sub-view'> + <template> + <tr-ui-a-single-thread-slice-sub-view id="content"></tr-ui-a-single-thread-slice-sub-view> + <tr-ui-e-v8-runtime-call-stats-table id="runtimeCallStats"></tr-ui-e-v8-runtime-call-stats-table> + </template> +</dom-module> + +<script> +'use strict'; +Polymer({ + is: 'tr-ui-e-single-v8-thread-slice-sub-view', + behaviors: [tr.ui.analysis.AnalysisSubView], + + get selection() { + return this.$.content.selection; + }, + + set selection(selection) { + this.$.runtimeCallStats.slices = selection; + this.$.content.selection = selection; + } +}); + +tr.ui.analysis.AnalysisSubView.register( + 'tr-ui-e-single-v8-thread-slice-sub-view', + tr.e.v8.V8ThreadSlice, + { + multi: false, + title: 'V8 slice' + } +); + +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html new file mode 100644 index 00000000000..9e12aa6e044 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8/single_v8_thread_slice_sub_view_test.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<!-- +Copyright (c) 2016 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<link rel="import" href="/tracing/core/test_utils.html"> +<link rel="import" href="/tracing/extras/v8/v8_thread_slice.html"> +<link rel="import" href="/tracing/model/event_set.html"> +<link rel="import" href="/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html"> + +<script> +'use strict'; + +tr.b.unittest.testSuite(function() { + const newSliceEx = tr.c.TestUtils.newSliceEx; + + function createModel() { + const m = tr.c.TestUtils.newModel(function(m) { + m.p1 = m.getOrCreateProcess(1); + m.t2 = m.p1.getOrCreateThread(2); + + m.s1 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 0, + end: 10, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + CompileFullCode: [3, 345], + LoadIC_Miss: [5, 567], + ParseLazy: [8, 890] + }}})); + m.s2 = m.t2.sliceGroup.pushSlice( + newSliceEx( + {title: 'V8.Execute', + start: 11, + end: 15, + type: tr.e.v8.V8ThreadSlice, + cat: 'v8', + args: {'runtime-call-stats': + { + HandleApiCall: [1, 123], + OptimizeCode: [7, 789] + }}})); + }); + return m; + } + + test('selectV8ThreadSlice', function() { + const m = createModel(); + + const viewEl = + document.createElement('tr-ui-e-single-v8-thread-slice-sub-view'); + const selection1 = new tr.model.EventSet(); + selection1.push(m.s1); + viewEl.selection = selection1; + this.addHTMLOutput(viewEl); + let rows = viewEl.$.runtimeCallStats.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 1802, + 567, + 0, + 0, + 0, + 345, + 0, + 890, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ]); + + const selection2 = new tr.model.EventSet(); + selection2.push(m.s2); + viewEl.selection = selection2; + rows = viewEl.$.runtimeCallStats.$.table.tableRows; + assert.lengthOf(rows, 19); + assert.deepEqual(rows.map(r => r.time), [ + 912, + 0, + 0, + 789, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 123 + ]); + }); +}); +</script> diff --git a/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html new file mode 100644 index 00000000000..f78005c2b54 --- /dev/null +++ b/chromium/third_party/catapult/tracing/tracing/ui/extras/v8_config.html @@ -0,0 +1,17 @@ +<!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/extras/v8_config.html"> +<link rel="import" href="/tracing/ui/extras/v8/gc_objects_stats_table.html"> +<link rel="import" href="/tracing/ui/extras/v8/multi_v8_gc_stats_thread_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/multi_v8_ic_stats_thread_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/multi_v8_thread_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/runtime_call_stats_table.html"> +<link rel="import" href="/tracing/ui/extras/v8/single_v8_gc_stats_thread_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/single_v8_ic_stats_thread_slice_sub_view.html"> +<link rel="import" href="/tracing/ui/extras/v8/single_v8_thread_slice_sub_view.html"> |