| // Copyright (C) 2018 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import m from 'mithril'; |
| import {inflate} from 'pako'; |
| import {assertTrue} from '../base/logging'; |
| import {isString} from '../base/object_utils'; |
| import {showModal} from '../widgets/modal'; |
| import {utf8Decode} from '../base/string_utils'; |
| import {convertToJson} from './trace_converter'; |
| import {assetSrc} from '../base/assets'; |
| |
| const CTRACE_HEADER = 'TRACE:\n'; |
| |
| async function isCtrace(file: File): Promise<boolean> { |
| const fileName = file.name.toLowerCase(); |
| |
| if (fileName.endsWith('.ctrace')) { |
| return true; |
| } |
| |
| // .ctrace files sometimes end with .txt. We can detect these via |
| // the presence of TRACE: near the top of the file. |
| if (fileName.endsWith('.txt')) { |
| const header = await readText(file.slice(0, 128)); |
| if (header.includes(CTRACE_HEADER)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| function readText(blob: Blob): Promise<string> { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| reader.onload = () => { |
| if (isString(reader.result)) { |
| return resolve(reader.result); |
| } |
| }; |
| reader.onerror = (err) => { |
| reject(err); |
| }; |
| reader.readAsText(blob); |
| }); |
| } |
| |
| export async function isLegacyTrace(file: File): Promise<boolean> { |
| const fileName = file.name.toLowerCase(); |
| if ( |
| fileName.endsWith('.json') || |
| fileName.endsWith('.json.gz') || |
| fileName.endsWith('.zip') || |
| fileName.endsWith('.html') |
| ) { |
| return true; |
| } |
| |
| if (await isCtrace(file)) { |
| return true; |
| } |
| |
| // Sometimes systrace formatted traces end with '.trace'. This is a |
| // little generic to assume all such traces are systrace format though |
| // so we read the beginning of the file and check to see if is has the |
| // systrace header (several comment lines): |
| if (fileName.endsWith('.trace')) { |
| const header = await readText(file.slice(0, 512)); |
| const lines = header.split('\n'); |
| let commentCount = 0; |
| for (const line of lines) { |
| if (line.startsWith('#')) { |
| commentCount++; |
| } |
| } |
| if (commentCount > 5) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| export async function openFileWithLegacyTraceViewer(file: File) { |
| const reader = new FileReader(); |
| reader.onload = () => { |
| if (reader.result instanceof ArrayBuffer) { |
| return openBufferWithLegacyTraceViewer( |
| file.name, |
| reader.result, |
| reader.result.byteLength, |
| ); |
| } else { |
| const str = reader.result as string; |
| return openBufferWithLegacyTraceViewer(file.name, str, str.length); |
| } |
| }; |
| reader.onerror = (err) => { |
| console.error(err); |
| }; |
| if ( |
| file.name.endsWith('.gz') || |
| file.name.endsWith('.zip') || |
| (await isCtrace(file)) |
| ) { |
| reader.readAsArrayBuffer(file); |
| } else { |
| reader.readAsText(file); |
| } |
| } |
| |
| function openBufferWithLegacyTraceViewer( |
| name: string, |
| data: ArrayBuffer | string, |
| size: number, |
| ) { |
| if (data instanceof ArrayBuffer) { |
| assertTrue(size <= data.byteLength); |
| if (size !== data.byteLength) { |
| data = data.slice(0, size); |
| } |
| |
| // Handle .ctrace files. |
| const header = utf8Decode(data.slice(0, 128)); |
| if (header.includes(CTRACE_HEADER)) { |
| const offset = header.indexOf(CTRACE_HEADER) + CTRACE_HEADER.length; |
| data = inflate(new Uint8Array(data.slice(offset)), {to: 'string'}); |
| } |
| } |
| |
| // The location.pathname mangling is to make this code work also when hosted |
| // in a non-root sub-directory, for the case of CI artifacts. |
| const catapultUrl = assetSrc('assets/catapult_trace_viewer.html'); |
| const newWin = window.open(catapultUrl); |
| if (newWin) { |
| // Popup succeedeed. |
| newWin.addEventListener('load', (e: Event) => { |
| const doc = e.target as Document; |
| const ctl = doc.querySelector('x-profiling-view') as TraceViewerAPI; |
| ctl.setActiveTrace(name, data); |
| }); |
| return; |
| } |
| |
| // Popup blocker detected. |
| showModal({ |
| title: 'Open trace in the legacy Catapult Trace Viewer', |
| content: m( |
| 'div', |
| m('div', 'You are seeing this interstitial because popups are blocked'), |
| m('div', 'Enable popups to skip this dialog next time.'), |
| ), |
| buttons: [ |
| { |
| text: 'Open legacy UI', |
| primary: true, |
| action: () => openBufferWithLegacyTraceViewer(name, data, size), |
| }, |
| ], |
| }); |
| } |
| |
| export async function openInOldUIWithSizeCheck(trace: Blob): Promise<void> { |
| // Perfetto traces smaller than 50mb can be safely opened in the legacy UI. |
| if (trace.size < 1024 * 1024 * 50) { |
| return await convertToJson(trace, openBufferWithLegacyTraceViewer); |
| } |
| |
| // Give the user the option to truncate larger perfetto traces. |
| const size = Math.round(trace.size / (1024 * 1024)); |
| |
| // If the user presses one of the buttons below, remember the promise that |
| // they trigger, so we await for it before returning. |
| let nextPromise: Promise<void> | undefined; |
| const setNextPromise = (p: Promise<void>) => (nextPromise = p); |
| |
| await showModal({ |
| title: 'Legacy UI may fail to open this trace', |
| content: m( |
| 'div', |
| m( |
| 'p', |
| `This trace is ${size}mb, opening it in the legacy UI ` + `may fail.`, |
| ), |
| m( |
| 'p', |
| 'More options can be found at ', |
| m( |
| 'a', |
| { |
| href: 'https://goto.google.com/opening-large-traces', |
| target: '_blank', |
| }, |
| 'go/opening-large-traces', |
| ), |
| '.', |
| ), |
| ), |
| buttons: [ |
| { |
| text: 'Open full trace (not recommended)', |
| action: () => |
| setNextPromise(convertToJson(trace, openBufferWithLegacyTraceViewer)), |
| }, |
| { |
| text: 'Open beginning of trace', |
| action: () => |
| setNextPromise( |
| convertToJson( |
| trace, |
| openBufferWithLegacyTraceViewer, |
| /* truncate*/ 'start', |
| ), |
| ), |
| }, |
| { |
| text: 'Open end of trace', |
| primary: true, |
| action: () => |
| setNextPromise( |
| convertToJson( |
| trace, |
| openBufferWithLegacyTraceViewer, |
| /* truncate*/ 'end', |
| ), |
| ), |
| }, |
| ], |
| }); |
| // nextPromise is undefined if the user just dimisses the dialog with ESC. |
| if (nextPromise !== undefined) { |
| await nextPromise; |
| } |
| } |
| |
| // TraceViewer method that we wire up to trigger the file load. |
| interface TraceViewerAPI extends Element { |
| setActiveTrace(name: string, data: ArrayBuffer | string): void; |
| } |