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'; 17import {assertTrue} from '../base/logging'; 18import {isString} from '../base/object_utils'; 19import {showModal} from '../widgets/modal'; 20import {utf8Decode} from '../base/string_utils'; 21import {convertToJson} from './trace_converter'; 22import {assetSrc} from '../base/assets'; 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 124function 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 header = utf8Decode(data.slice(0, 128)); 137 if (header.includes(CTRACE_HEADER)) { 138 const offset = header.indexOf(CTRACE_HEADER) + CTRACE_HEADER.length; 139 data = inflate(new Uint8Array(data.slice(offset)), {to: 'string'}); 140 } 141 } 142 143 // The location.pathname mangling is to make this code work also when hosted 144 // in a non-root sub-directory, for the case of CI artifacts. 145 const catapultUrl = assetSrc('assets/catapult_trace_viewer.html'); 146 const newWin = window.open(catapultUrl); 147 if (newWin) { 148 // Popup succeedeed. 149 newWin.addEventListener('load', (e: Event) => { 150 const doc = e.target as Document; 151 const ctl = doc.querySelector('x-profiling-view') as TraceViewerAPI; 152 ctl.setActiveTrace(name, data); 153 }); 154 return; 155 } 156 157 // Popup blocker detected. 158 showModal({ 159 title: 'Open trace in the legacy Catapult Trace Viewer', 160 content: m( 161 'div', 162 m('div', 'You are seeing this interstitial because popups are blocked'), 163 m('div', 'Enable popups to skip this dialog next time.'), 164 ), 165 buttons: [ 166 { 167 text: 'Open legacy UI', 168 primary: true, 169 action: () => openBufferWithLegacyTraceViewer(name, data, size), 170 }, 171 ], 172 }); 173} 174 175export async function openInOldUIWithSizeCheck(trace: Blob): Promise<void> { 176 // Perfetto traces smaller than 50mb can be safely opened in the legacy UI. 177 if (trace.size < 1024 * 1024 * 50) { 178 return await convertToJson(trace, openBufferWithLegacyTraceViewer); 179 } 180 181 // Give the user the option to truncate larger perfetto traces. 182 const size = Math.round(trace.size / (1024 * 1024)); 183 184 // If the user presses one of the buttons below, remember the promise that 185 // they trigger, so we await for it before returning. 186 let nextPromise: Promise<void> | undefined; 187 const setNextPromise = (p: Promise<void>) => (nextPromise = p); 188 189 await showModal({ 190 title: 'Legacy UI may fail to open this trace', 191 content: m( 192 'div', 193 m( 194 'p', 195 `This trace is ${size}mb, opening it in the legacy UI ` + `may fail.`, 196 ), 197 m( 198 'p', 199 'More options can be found at ', 200 m( 201 'a', 202 { 203 href: 'https://goto.google.com/opening-large-traces', 204 target: '_blank', 205 }, 206 'go/opening-large-traces', 207 ), 208 '.', 209 ), 210 ), 211 buttons: [ 212 { 213 text: 'Open full trace (not recommended)', 214 action: () => 215 setNextPromise(convertToJson(trace, openBufferWithLegacyTraceViewer)), 216 }, 217 { 218 text: 'Open beginning of trace', 219 action: () => 220 setNextPromise( 221 convertToJson( 222 trace, 223 openBufferWithLegacyTraceViewer, 224 /* truncate*/ 'start', 225 ), 226 ), 227 }, 228 { 229 text: 'Open end of trace', 230 primary: true, 231 action: () => 232 setNextPromise( 233 convertToJson( 234 trace, 235 openBufferWithLegacyTraceViewer, 236 /* truncate*/ 'end', 237 ), 238 ), 239 }, 240 ], 241 }); 242 // nextPromise is undefined if the user just dimisses the dialog with ESC. 243 if (nextPromise !== undefined) { 244 await nextPromise; 245 } 246} 247 248// TraceViewer method that we wire up to trigger the file load. 249interface TraceViewerAPI extends Element { 250 setActiveTrace(name: string, data: ArrayBuffer | string): void; 251} 252