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 protos from '../protos'; 17import {assertExists} from '../base/logging'; 18import {VERSION} from '../gen/perfetto_version'; 19import {HttpRpcEngine} from '../trace_processor/http_rpc_engine'; 20import {showModal} from '../widgets/modal'; 21import {AppImpl} from '../core/app_impl'; 22 23const CURRENT_API_VERSION = 24 protos.TraceProcessorApiVersion.TRACE_PROCESSOR_CURRENT_API_VERSION; 25 26function getPromptMessage(tpStatus: protos.StatusResult): string { 27 return `Trace Processor detected on ${HttpRpcEngine.hostAndPort} with: 28${tpStatus.loadedTraceName} 29 30YES, use loaded trace: 31Will load from the current state of Trace Processor. If you did run 32trace_processor_shell --httpd file.pftrace this is likely what you want. 33 34YES, but reset state: 35Use this if you want to open another trace but still use the 36accelerator. This is the equivalent of killing and restarting 37trace_processor_shell --httpd. 38 39NO, Use builtin WASM: 40Will not use the accelerator in this tab. 41 42Using the native accelerator has some minor caveats: 43- Only one tab can be using the accelerator. 44- Sharing, downloading and conversion-to-legacy aren't supported. 45`; 46} 47 48function getIncompatibleRpcMessage(tpStatus: protos.StatusResult): string { 49 return `The Trace Processor instance on ${HttpRpcEngine.hostAndPort} is too old. 50 51This UI requires TraceProcessor features that are not present in the 52Trace Processor native accelerator you are currently running. 53If you continue, this is almost surely going to cause UI failures. 54 55Please update your local Trace Processor binary: 56 57curl -LO https://get.perfetto.dev/trace_processor 58chmod +x ./trace_processor 59./trace_processor --httpd 60 61UI version code: ${VERSION} 62UI RPC API: ${CURRENT_API_VERSION} 63 64Trace processor version: ${tpStatus.humanReadableVersion} 65Trace processor version code: ${tpStatus.versionCode} 66Trace processor RPC API: ${tpStatus.apiVersion} 67`; 68} 69 70function getVersionMismatchMessage(tpStatus: protos.StatusResult): string { 71 return `The Trace Processor instance on ${HttpRpcEngine.hostAndPort} is a different build from the UI. 72 73This may cause problems. Where possible it is better to use the matched version of the UI. 74You can do this by clicking the button below. 75 76UI version code: ${VERSION} 77UI RPC API: ${CURRENT_API_VERSION} 78 79Trace processor version: ${tpStatus.humanReadableVersion} 80Trace processor version code: ${tpStatus.versionCode} 81Trace processor RPC API: ${tpStatus.apiVersion} 82`; 83} 84 85// The flow is fairly complicated: 86// +-----------------------------------+ 87// | User loads the UI | 88// +-----------------+-----------------+ 89// | 90// +-----------------+-----------------+ 91// | Is trace_processor present at | 92// | HttpRpcEngine.hostAndPort? | 93// +--------------------------+--------+ 94// |No |Yes 95// | +--------------+-------------------------------+ 96// | | Does version code of UI and TP match? | 97// | +--------------+----------------------------+--+ 98// | |No |Yes 99// | | | 100// | | | 101// | +-------------+-------------+ | 102// | |Is a build of the UI at the| | 103// | |TP version code existant | | 104// | |and reachable? | | 105// | +---+----------------+------+ | 106// | | No | Yes | 107// | | | | 108// | | +--------+-------+ | 109// | | |Dialog: Mismatch| | 110// | | |Load matched UI +-------------------------------+ 111// | | |Continue +-+ | | 112// | | +----------------+ | | | 113// | | | | | 114// | +------+--------------------------+----+ | | 115// | |TP RPC version >= UI RPC version | | | 116// | +----+-------------------+-------------+ | | 117// | | No |Yes | | 118// | +----+--------------+ | | | 119// | |Dialog: Bad RPC | | | | 120// | +---+Use built-in WASM | | | | 121// | | |Continue anyway +----| | | 122// | | +-------------------+ | +-----------+-----------+ | 123// | | +--------+TP has preloaded trace?| | 124// | | +-+---------------+-----+ | 125// | | |No |Yes | 126// | | | +---------------------+ | 127// | | | | Dialog: Preloaded? | | 128// | | | + YES, use loaded trace | 129// | | +--------| YES, but reset state| | 130// | | +---------------------------------------| NO, Use builtin Wasm| | 131// | | | | | +---------------------+ | 132// | | | | | | 133// | | | Reset TP | | 134// | | | | | | 135// | | | | | | 136// Show the UI Show the UI Link to 137// (WASM mode) (RPC mode) matched UI 138 139// There are three options in the end: 140// - Show the UI (WASM mode) 141// - Show the UI (RPC mode) 142// - Redirect to a matched version of the UI 143 144// Try to connect to the external Trace Processor HTTP RPC accelerator (if 145// available, often it isn't). If connected it will populate the 146// |httpRpcState| in the frontend local state. In turn that will show the UI 147// chip in the sidebar. trace_controller.ts will repeat this check before 148// trying to load a new trace. We do this ahead of time just to have a 149// consistent UX (i.e. so that the user can tell if the RPC is working without 150// having to open a trace). 151export async function CheckHttpRpcConnection(): Promise<void> { 152 const state = await HttpRpcEngine.checkConnection(); 153 AppImpl.instance.httpRpc.httpRpcAvailable = state.connected; 154 if (!state.connected) { 155 // No RPC = exit immediately to the WASM UI. 156 return; 157 } 158 const tpStatus = assertExists(state.status); 159 160 function forceWasm() { 161 AppImpl.instance.httpRpc.newEngineMode = 'FORCE_BUILTIN_WASM'; 162 } 163 164 // Check short version: 165 if (tpStatus.versionCode !== '' && tpStatus.versionCode !== VERSION) { 166 const url = await isVersionAvailable(tpStatus.versionCode); 167 if (url !== undefined) { 168 // If matched UI available show a dialog asking the user to 169 // switch. 170 const result = await showDialogVersionMismatch(tpStatus, url); 171 switch (result) { 172 case MismatchedVersionDialog.Dismissed: 173 case MismatchedVersionDialog.UseMatchingUi: 174 navigateToVersion(tpStatus.versionCode); 175 return; 176 case MismatchedVersionDialog.UseMismatchedRpc: 177 break; 178 case MismatchedVersionDialog.UseWasm: 179 forceWasm(); 180 return; 181 default: 182 const x: never = result; 183 throw new Error(`Unsupported result ${x}`); 184 } 185 } 186 } 187 188 // Check the RPC version: 189 if (tpStatus.apiVersion < CURRENT_API_VERSION) { 190 const result = await showDialogIncompatibleRPC(tpStatus); 191 switch (result) { 192 case IncompatibleRpcDialogResult.Dismissed: 193 case IncompatibleRpcDialogResult.UseWasm: 194 forceWasm(); 195 return; 196 case IncompatibleRpcDialogResult.UseIncompatibleRpc: 197 break; 198 default: 199 const x: never = result; 200 throw new Error(`Unsupported result ${x}`); 201 } 202 } 203 204 // Check if pre-loaded: 205 if (tpStatus.loadedTraceName) { 206 // If a trace is already loaded in the trace processor (e.g., the user 207 // launched trace_processor_shell -D trace_file.pftrace), prompt the user to 208 // initialize the UI with the already-loaded trace. 209 const result = await showDialogToUsePreloadedTrace(tpStatus); 210 switch (result) { 211 case PreloadedDialogResult.Dismissed: 212 case PreloadedDialogResult.UseRpcWithPreloadedTrace: 213 AppImpl.instance.openTraceFromHttpRpc(); 214 return; 215 case PreloadedDialogResult.UseRpc: 216 // Resetting state is the default. 217 return; 218 case PreloadedDialogResult.UseWasm: 219 forceWasm(); 220 return; 221 default: 222 const x: never = result; 223 throw new Error(`Unsupported result ${x}`); 224 } 225 } 226} 227 228enum MismatchedVersionDialog { 229 UseMatchingUi = 'useMatchingUi', 230 UseWasm = 'useWasm', 231 UseMismatchedRpc = 'useMismatchedRpc', 232 Dismissed = 'dismissed', 233} 234 235async function showDialogVersionMismatch( 236 tpStatus: protos.StatusResult, 237 url: string, 238): Promise<MismatchedVersionDialog> { 239 let result = MismatchedVersionDialog.Dismissed; 240 await showModal({ 241 title: 'Version mismatch', 242 content: m('.modal-pre', getVersionMismatchMessage(tpStatus)), 243 buttons: [ 244 { 245 primary: true, 246 text: `Open ${url}`, 247 action: () => { 248 result = MismatchedVersionDialog.UseMatchingUi; 249 }, 250 }, 251 { 252 text: 'Use builtin Wasm', 253 action: () => { 254 result = MismatchedVersionDialog.UseWasm; 255 }, 256 }, 257 { 258 text: 'Use mismatched version regardless (might crash)', 259 action: () => { 260 result = MismatchedVersionDialog.UseMismatchedRpc; 261 }, 262 }, 263 ], 264 }); 265 return result; 266} 267 268enum IncompatibleRpcDialogResult { 269 UseWasm = 'useWasm', 270 UseIncompatibleRpc = 'useIncompatibleRpc', 271 Dismissed = 'dismissed', 272} 273 274async function showDialogIncompatibleRPC( 275 tpStatus: protos.StatusResult, 276): Promise<IncompatibleRpcDialogResult> { 277 let result = IncompatibleRpcDialogResult.Dismissed; 278 await showModal({ 279 title: 'Incompatible RPC version', 280 content: m('.modal-pre', getIncompatibleRpcMessage(tpStatus)), 281 buttons: [ 282 { 283 text: 'Use builtin Wasm', 284 primary: true, 285 action: () => { 286 result = IncompatibleRpcDialogResult.UseWasm; 287 }, 288 }, 289 { 290 text: 'Use old version regardless (will crash)', 291 action: () => { 292 result = IncompatibleRpcDialogResult.UseIncompatibleRpc; 293 }, 294 }, 295 ], 296 }); 297 return result; 298} 299 300enum PreloadedDialogResult { 301 UseRpcWithPreloadedTrace = 'useRpcWithPreloadedTrace', 302 UseRpc = 'useRpc', 303 UseWasm = 'useWasm', 304 Dismissed = 'dismissed', 305} 306 307async function showDialogToUsePreloadedTrace( 308 tpStatus: protos.StatusResult, 309): Promise<PreloadedDialogResult> { 310 let result = PreloadedDialogResult.Dismissed; 311 await showModal({ 312 title: 'Use trace processor native acceleration?', 313 content: m('.modal-pre', getPromptMessage(tpStatus)), 314 buttons: [ 315 { 316 text: 'YES, use loaded trace', 317 primary: true, 318 action: () => { 319 result = PreloadedDialogResult.UseRpcWithPreloadedTrace; 320 }, 321 }, 322 { 323 text: 'YES, but reset state', 324 action: () => { 325 result = PreloadedDialogResult.UseRpc; 326 }, 327 }, 328 { 329 text: 'NO, Use builtin WASM', 330 action: () => { 331 result = PreloadedDialogResult.UseWasm; 332 }, 333 }, 334 ], 335 }); 336 return result; 337} 338 339function getUrlForVersion(versionCode: string): string { 340 const url = `${window.location.origin}/${versionCode}/`; 341 return url; 342} 343 344async function isVersionAvailable( 345 versionCode: string, 346): Promise<string | undefined> { 347 if (versionCode === '') { 348 return undefined; 349 } 350 const controller = new AbortController(); 351 const timeoutId = setTimeout(() => controller.abort(), 1000); 352 const url = getUrlForVersion(versionCode); 353 let r; 354 try { 355 r = await fetch(url, {signal: controller.signal}); 356 } catch (e) { 357 console.error( 358 `No UI version for ${versionCode} at ${url}. ` + 359 `This is an error if ${versionCode} is a released Perfetto version`, 360 ); 361 return undefined; 362 } finally { 363 clearTimeout(timeoutId); 364 } 365 if (!r.ok) { 366 return undefined; 367 } 368 return url; 369} 370 371function navigateToVersion(versionCode: string): void { 372 const url = getUrlForVersion(versionCode); 373 if (url === undefined) { 374 throw new Error(`No URL known for UI version ${versionCode}.`); 375 } 376 window.location.replace(url); 377} 378