1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import m from 'mithril'; 16import {inflate} from 'pako'; 17 18import {assertTrue} from '../base/logging'; 19import {isString} from '../base/object_utils'; 20import {showModal} from '../widgets/modal'; 21 22import {globals} from './globals'; 23 24const CTRACE_HEADER = 'TRACE:\n'; 25 26async function isCtrace(file: File): Promise<boolean> { 27 const fileName = file.name.toLowerCase(); 28 29 if (fileName.endsWith('.ctrace')) { 30 return true; 31 } 32 33 // .ctrace files sometimes end with .txt. We can detect these via 34 // the presence of TRACE: near the top of the file. 35 if (fileName.endsWith('.txt')) { 36 const header = await readText(file.slice(0, 128)); 37 if (header.includes(CTRACE_HEADER)) { 38 return true; 39 } 40 } 41 42 return false; 43} 44 45function readText(blob: Blob): Promise<string> { 46 return new Promise((resolve, reject) => { 47 const reader = new FileReader(); 48 reader.onload = () => { 49 if (isString(reader.result)) { 50 return resolve(reader.result); 51 } 52 }; 53 reader.onerror = (err) => { 54 reject(err); 55 }; 56 reader.readAsText(blob); 57 }); 58} 59 60export async function isLegacyTrace(file: File): Promise<boolean> { 61 const fileName = file.name.toLowerCase(); 62 if ( 63 fileName.endsWith('.json') || 64 fileName.endsWith('.json.gz') || 65 fileName.endsWith('.zip') || 66 fileName.endsWith('.html') 67 ) { 68 return true; 69 } 70 71 if (await isCtrace(file)) { 72 return true; 73 } 74 75 // Sometimes systrace formatted traces end with '.trace'. This is a 76 // little generic to assume all such traces are systrace format though 77 // so we read the beginning of the file and check to see if is has the 78 // systrace header (several comment lines): 79 if (fileName.endsWith('.trace')) { 80 const header = await readText(file.slice(0, 512)); 81 const lines = header.split('\n'); 82 let commentCount = 0; 83 for (const line of lines) { 84 if (line.startsWith('#')) { 85 commentCount++; 86 } 87 } 88 if (commentCount > 5) { 89 return true; 90 } 91 } 92 93 return false; 94} 95 96export async function openFileWithLegacyTraceViewer(file: File) { 97 const reader = new FileReader(); 98 reader.onload = () => { 99 if (reader.result instanceof ArrayBuffer) { 100 return openBufferWithLegacyTraceViewer( 101 file.name, 102 reader.result, 103 reader.result.byteLength, 104 ); 105 } else { 106 const str = reader.result as string; 107 return openBufferWithLegacyTraceViewer(file.name, str, str.length); 108 } 109 }; 110 reader.onerror = (err) => { 111 console.error(err); 112 }; 113 if ( 114 file.name.endsWith('.gz') || 115 file.name.endsWith('.zip') || 116 (await isCtrace(file)) 117 ) { 118 reader.readAsArrayBuffer(file); 119 } else { 120 reader.readAsText(file); 121 } 122} 123 124export function openBufferWithLegacyTraceViewer( 125 name: string, 126 data: ArrayBuffer | string, 127 size: number, 128) { 129 if (data instanceof ArrayBuffer) { 130 assertTrue(size <= data.byteLength); 131 if (size !== data.byteLength) { 132 data = data.slice(0, size); 133 } 134 135 // Handle .ctrace files. 136 const enc = new TextDecoder('utf-8'); 137 const header = enc.decode(data.slice(0, 128)); 138 if (header.includes(CTRACE_HEADER)) { 139 const offset = header.indexOf(CTRACE_HEADER) + CTRACE_HEADER.length; 140 data = inflate(new Uint8Array(data.slice(offset)), {to: 'string'}); 141 } 142 } 143 144 // The location.pathname mangling is to make this code work also when hosted 145 // in a non-root sub-directory, for the case of CI artifacts. 146 const catapultUrl = globals.root + 'assets/catapult_trace_viewer.html'; 147 const newWin = window.open(catapultUrl); 148 if (newWin) { 149 // Popup succeedeed. 150 newWin.addEventListener('load', (e: Event) => { 151 const doc = e.target as Document; 152 const ctl = doc.querySelector('x-profiling-view') as TraceViewerAPI; 153 ctl.setActiveTrace(name, data); 154 }); 155 return; 156 } 157 158 // Popup blocker detected. 159 showModal({ 160 title: 'Open trace in the legacy Catapult Trace Viewer', 161 content: m( 162 'div', 163 m('div', 'You are seeing this interstitial because popups are blocked'), 164 m('div', 'Enable popups to skip this dialog next time.'), 165 ), 166 buttons: [ 167 { 168 text: 'Open legacy UI', 169 primary: true, 170 action: () => openBufferWithLegacyTraceViewer(name, data, size), 171 }, 172 ], 173 }); 174} 175 176// TraceViewer method that we wire up to trigger the file load. 177interface TraceViewerAPI extends Element { 178 setActiveTrace(name: string, data: ArrayBuffer | string): void; 179} 180