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 {defer, Deferred} from '../base/deferred'; 16import {assertTrue} from '../base/logging'; 17import {WasmBridgeRequest, WasmBridgeResponse} from '../engine/wasm_bridge'; 18 19import {Engine, LoadingTracker} from './engine'; 20 21const activeWorkers = new Map<string, Worker>(); 22let warmWorker: null|Worker = null; 23 24function createWorker(): Worker { 25 return new Worker('engine_bundle.js'); 26} 27 28// Take the warm engine and start creating a new WASM engine in the background 29// for the next call. 30export function createWasmEngine(id: string): Worker { 31 if (warmWorker === null) { 32 throw new Error('warmupWasmEngine() not called'); 33 } 34 if (activeWorkers.has(id)) { 35 throw new Error(`Duplicate worker ID ${id}`); 36 } 37 const activeWorker = warmWorker; 38 warmWorker = createWorker(); 39 activeWorkers.set(id, activeWorker); 40 return activeWorker; 41} 42 43export function destroyWasmEngine(id: string) { 44 if (!activeWorkers.has(id)) { 45 throw new Error(`Cannot find worker ID ${id}`); 46 } 47 activeWorkers.get(id)!.terminate(); 48 activeWorkers.delete(id); 49} 50 51/** 52 * It's quite slow to compile WASM and (in Chrome) this happens every time 53 * a worker thread attempts to load a WASM module since there is no way to 54 * cache the compiled code currently. To mitigate this we can always keep a 55 * WASM backend 'ready to go' just waiting to be provided with a trace file. 56 * warmupWasmEngineWorker (together with getWasmEngineWorker) 57 * implement this behaviour. 58 */ 59export function warmupWasmEngine(): void { 60 if (warmWorker !== null) { 61 throw new Error('warmupWasmEngine() already called'); 62 } 63 warmWorker = createWorker(); 64} 65 66interface PendingRequest { 67 id: number; 68 respHandler: Deferred<Uint8Array>; 69} 70 71/** 72 * This implementation of Engine uses a WASM backend hosted in a separate 73 * worker thread. 74 */ 75export class WasmEngineProxy extends Engine { 76 readonly id: string; 77 private readonly worker: Worker; 78 private pendingRequests = new Array<PendingRequest>(); 79 private nextRequestId = 0; 80 81 constructor(id: string, worker: Worker, loadingTracker?: LoadingTracker) { 82 super(loadingTracker); 83 this.id = id; 84 this.worker = worker; 85 this.worker.onmessage = this.onMessage.bind(this); 86 } 87 88 async parse(reqData: Uint8Array): Promise<void> { 89 // We don't care about the response data (the method is actually a void). We 90 // just want to linearize and wait for the call to have been completed on 91 // the worker. 92 await this.queueRequest('trace_processor_parse', reqData); 93 } 94 95 async notifyEof(): Promise<void> { 96 // We don't care about the response data (the method is actually a void). We 97 // just want to linearize and wait for the call to have been completed on 98 // the worker. 99 await this.queueRequest('trace_processor_notify_eof', new Uint8Array()); 100 } 101 102 restoreInitialTables(): Promise<void> { 103 // We should never get here, restoreInitialTables() should be called only 104 // when using the HttpRpcEngine. 105 throw new Error('restoreInitialTables() not supported by the WASM engine'); 106 } 107 108 rawQuery(rawQueryArgs: Uint8Array): Promise<Uint8Array> { 109 return this.queueRequest('trace_processor_raw_query', rawQueryArgs); 110 } 111 112 rawComputeMetric(rawComputeMetric: Uint8Array): Promise<Uint8Array> { 113 return this.queueRequest( 114 'trace_processor_compute_metric', rawComputeMetric); 115 } 116 117 async enableMetatrace(): Promise<void> { 118 await this.queueRequest( 119 'trace_processor_enable_metatrace', new Uint8Array()); 120 } 121 122 disableAndReadMetatrace(): Promise<Uint8Array> { 123 return this.queueRequest( 124 'trace_processor_disable_and_read_metatrace', new Uint8Array()); 125 } 126 127 // Enqueues a request to the worker queue via postMessage(). The returned 128 // promised will be resolved once the worker replies to the postMessage() 129 // with the paylad of the response, a proto-encoded object which wraps the 130 // method return value (e.g., RawQueryResult for SQL query results). 131 private queueRequest(methodName: string, reqData: Uint8Array): 132 Deferred<Uint8Array> { 133 const respHandler = defer<Uint8Array>(); 134 const id = this.nextRequestId++; 135 const request: WasmBridgeRequest = {id, methodName, data: reqData}; 136 this.pendingRequests.push({id, respHandler}); 137 this.worker.postMessage(request); 138 return respHandler; 139 } 140 141 onMessage(m: MessageEvent) { 142 const response = m.data as WasmBridgeResponse; 143 assertTrue(this.pendingRequests.length > 0); 144 const request = this.pendingRequests.shift()!; 145 146 // Requests should be executed and ACKed by the worker in the same order 147 // they came in. 148 assertTrue(request.id === response.id); 149 150 // If the Wasm call fails (e.g. hits a PERFETTO_CHECK) it will throw an 151 // error in wasm_bridge.ts and show the crash dialog. In no case we can 152 // gracefully handle a Wasm crash, so we fail fast there rather than 153 // propagating the error here rejecting the promise. 154 request.respHandler.resolve(response.data); 155 } 156} 157