// 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 { 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 { 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 { 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 { // 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 | undefined; const setNextPromise = (p: Promise) => (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; }