| // 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 protos from '../protos'; | |
| import {defer, type Deferred} from '../base/deferred'; | |
| import {ensureExists, assertTrue, assertUnreachable} from '../base/assert'; | |
| import {ProtoRingBuffer} from './proto_ring_buffer'; | |
| import { | |
| createQueryResult, | |
| QueryError, | |
| type QueryResult, | |
| type WritableQueryResult, | |
| } from './query_result'; | |
| import TPM = protos.TraceProcessorRpc.TraceProcessorMethod; | |
| import {exists} from '../base/utils'; | |
| import {errResult, okResult, type Result} from '../base/result'; | |
| export type EngineMode = 'WASM' | 'HTTP_RPC'; | |
| export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE' | 'FORCE_BUILTIN_WASM'; | |
| // This is used to skip the decoding of queryResult from protobufjs and deal | |
| // with it ourselves. See the comment below around `QueryResult.decode = ...`. | |
| interface QueryResultBypass { | |
| rawQueryResult: Uint8Array; | |
| } | |
| export interface TraceProcessorConfig { | |
| // When true, the trace processor will only tokenize the trace without | |
| // performing a full parse. This is a performance optimization that allows for | |
| // a faster, albeit partial, import of the trace. | |
| tokenizeOnly: boolean; | |
| cropTrackEvents: boolean; | |
| ingestFtraceInRawTable: boolean; | |
| analyzeTraceProtoContent: boolean; | |
| ftraceDropUntilAllCpusValid: boolean; | |
| extraParsingDescriptors?: ReadonlyArray<Uint8Array>; | |
| forceFullSort: boolean; | |
| } | |
| const QUERY_LOG_BUFFER_SIZE = 1024; | |
| export interface QueryLog { | |
| readonly id: number; | |
| readonly tag?: string; | |
| readonly query: string; | |
| readonly startTime: number; | |
| readonly elapsedTimeMs?: number; | |
| readonly success?: boolean; | |
| } | |
| export interface Engine { | |
| readonly mode: EngineMode; | |
| readonly engineId: string; | |
| /** | |
| * A list of the most recent queries along with their start times, elapsed | |
| * times and success status (if completed). | |
| */ | |
| readonly queryLog: ReadonlyArray<QueryLog>; | |
| /** Clear the query log. In-flight queries are removed too. */ | |
| clearQueryLog(): void; | |
| /** | |
| * Execute a query against the database, returning a promise that resolves | |
| * when the query has completed but rejected when the query fails for whatever | |
| * reason. On success, the promise will only resolve once all the resulting | |
| * rows have been received. | |
| * | |
| * The promise will be rejected if the query fails. | |
| * | |
| * @param sql The query to execute. | |
| */ | |
| query(sql: string): Promise<QueryResult>; | |
| /** | |
| * Execute a query against the database, returning a promise that resolves | |
| * when the query has completed or failed. The promise will never get | |
| * rejected, it will always successfully resolve. Use the returned wrapper | |
| * object to determine whether the query completed successfully. | |
| * | |
| * The promise will only resolve once all the resulting rows have been | |
| * received. | |
| * | |
| * @param sql The query to execute. | |
| */ | |
| tryQuery(sql: string): Promise<Result<QueryResult>>; | |
| /** | |
| * Execute one or more metric and get the result. | |
| * | |
| * @param metrics The metrics to run. | |
| * @param format The format of the response. | |
| */ | |
| computeMetric( | |
| metrics: string[], | |
| format: 'json' | 'prototext' | 'proto', | |
| ): Promise<string | Uint8Array>; | |
| summarizeTrace( | |
| summarySpecs: protos.TraceSummarySpec[] | string[], | |
| metricIds: string[] | undefined, | |
| metadataId: string | undefined, | |
| format: 'prototext' | 'proto', | |
| ): Promise<protos.TraceSummaryResult>; | |
| enableMetatrace(categories?: protos.MetatraceCategories): void; | |
| stopAndGetMetatrace(): Promise<protos.DisableAndReadMetatraceResult>; | |
| // Summarizer API for Data Explorer. | |
| // Creates a summarizer with the given ID. Returns error if ID already exists. | |
| createSummarizer( | |
| summarizerId: string, | |
| ): Promise<protos.CreateSummarizerResult>; | |
| updateSummarizerSpec( | |
| summarizerId: string, | |
| spec: protos.TraceSummarySpec, | |
| ): Promise<protos.UpdateSummarizerSpecResult>; | |
| querySummarizer( | |
| summarizerId: string, | |
| queryId: string, | |
| ): Promise<protos.QuerySummarizerResult>; | |
| destroySummarizer( | |
| summarizerId: string, | |
| ): Promise<protos.DestroySummarizerResult>; | |
| getProxy(tag: string): EngineProxy; | |
| readonly numRequestsPending: number; | |
| readonly failed: string | undefined; | |
| } | |
| // Abstract interface of a trace proccessor. | |
| // This is the TypeScript equivalent of src/trace_processor/rpc.h. | |
| // There are two concrete implementations: | |
| // 1. WasmEngineProxy: creates a Wasm module and interacts over postMessage(). | |
| // 2. HttpRpcEngine: connects to an external `trace_processor_shell --httpd`. | |
| // and interacts via fetch(). | |
| // In both cases, we have a byte-oriented pipe to interact with TraceProcessor. | |
| // The derived class is only expected to deal with these two functions: | |
| // 1. Implement the abstract rpcSendRequestBytes() function, sending the | |
| // proto-encoded TraceProcessorRpc requests to the TraceProcessor instance. | |
| // 2. Call onRpcResponseBytes() when response data is received. | |
| export abstract class EngineBase implements Engine, Disposable { | |
| abstract readonly id: string; | |
| abstract readonly mode: EngineMode; | |
| private txSeqId = 0; | |
| private rxSeqId = 0; | |
| private rxBuf = new ProtoRingBuffer(); | |
| private pendingParses = new Array<Deferred<void>>(); | |
| private pendingEOFs = new Array<Deferred<void>>(); | |
| private pendingResetTraceProcessors = new Array<Deferred<void>>(); | |
| private pendingQueries = new Array<WritableQueryResult>(); | |
| private pendingRestoreTables = new Array<Deferred<void>>(); | |
| private pendingComputeMetrics = new Array<Deferred<string | Uint8Array>>(); | |
| private pendingReadMetatrace?: Deferred<protos.DisableAndReadMetatraceResult>; | |
| private pendingRegisterSqlPackage?: Deferred<void>; | |
| private pendingTraceSummary?: Deferred<protos.TraceSummaryResult>; | |
| private pendingCreateSummarizer?: Deferred<protos.CreateSummarizerResult>; | |
| private pendingUpdateSummarizerSpec?: Deferred<protos.UpdateSummarizerSpecResult>; | |
| private pendingQuerySummarizer?: Deferred<protos.QuerySummarizerResult>; | |
| private pendingDestroySummarizer?: Deferred<protos.DestroySummarizerResult>; | |
| private _numRequestsPending = 0; | |
| private _failed: string | undefined = undefined; | |
| private _queryLog: Array<QueryLog> = []; | |
| private _nextQueryLogId = 0; | |
| get queryLog(): ReadonlyArray<QueryLog> { | |
| return this._queryLog; | |
| } | |
| clearQueryLog(): void { | |
| this._queryLog = []; | |
| } | |
| // TraceController sets this to raf.scheduleFullRedraw(). | |
| onResponseReceived?: () => void; | |
| // Called to send data to the TraceProcessor instance. This turns into a | |
| // postMessage() or a HTTP request, depending on the Engine implementation. | |
| abstract rpcSendRequestBytes(data: Uint8Array): void; | |
| // Called when an inbound message is received by the Engine implementation | |
| // (e.g. onmessage for the Wasm case, on when HTTP replies are received for | |
| // the HTTP+RPC case). | |
| onRpcResponseBytes(dataWillBeRetained: Uint8Array) { | |
| // Note: when hitting the fastpath inside ProtoRingBuffer, the |data| buffer | |
| // is returned back by readMessage() (% subarray()-ing it) and held onto by | |
| // other classes (e.g., QueryResult). For both fetch() and Wasm we are fine | |
| // because every response creates a new buffer. | |
| this.rxBuf.append(dataWillBeRetained); | |
| for (;;) { | |
| const msg = this.rxBuf.readMessage(); | |
| if (msg === undefined) break; | |
| this.onRpcResponseMessage(msg); | |
| } | |
| } | |
| // Parses a response message. | |
| // |rpcMsgEncoded| is a sub-array to to the start of a TraceProcessorRpc | |
| // proto-encoded message (without the proto preamble and varint size). | |
| private onRpcResponseMessage(rpcMsgEncoded: Uint8Array) { | |
| // Here we override the protobufjs-generated code to skip the parsing of the | |
| // new streaming QueryResult and instead passing it through like a buffer. | |
| // This is the overall problem: All trace processor responses are wrapped | |
| // into a TraceProcessorRpc proto message. In all cases % | |
| // TPM_QUERY_STREAMING, we want protobufjs to decode the proto bytes and | |
| // give us a structured object. In the case of TPM_QUERY_STREAMING, instead, | |
| // we want to deal with the proto parsing ourselves using the new | |
| // QueryResult.appendResultBatch() method, because that handled streaming | |
| // results more efficiently and skips several copies. | |
| // By overriding the decode method below, we achieve two things: | |
| // 1. We avoid protobufjs decoding the TraceProcessorRpc.query_result field. | |
| // 2. We stash (a view of) the original buffer into the |rawQueryResult| so | |
| // the `case TPM_QUERY_STREAMING` below can take it. | |
| protos.QueryResult.decode = (reader: protobuf.Reader, length: number) => { | |
| const res = protos.QueryResult.create() as {} as QueryResultBypass; | |
| res.rawQueryResult = reader.buf.subarray(reader.pos, reader.pos + length); | |
| // All this works only if protobufjs returns the original ArrayBuffer | |
| // from |rpcMsgEncoded|. It should be always the case given the | |
| // current implementation. This check mainly guards against future | |
| // behavioral changes of protobufjs. We don't want to accidentally | |
| // hold onto some internal protobufjs buffer. We are fine holding | |
| // onto |rpcMsgEncoded| because those come from ProtoRingBuffer which | |
| // is buffer-retention-friendly. | |
| assertTrue(res.rawQueryResult.buffer === rpcMsgEncoded.buffer); | |
| reader.pos += length; | |
| return res as {} as protos.QueryResult; | |
| }; | |
| const rpc = protos.TraceProcessorRpc.decode(rpcMsgEncoded); | |
| if (rpc.fatalError !== undefined && rpc.fatalError.length > 0) { | |
| this.fail(`${rpc.fatalError}`); | |
| } | |
| // Allow restarting sequences from zero (when reloading the browser). | |
| if (rpc.seq !== this.rxSeqId + 1 && this.rxSeqId !== 0 && rpc.seq !== 0) { | |
| // "(ERR:rpc_seq)" is intercepted by error_dialog.ts to show a more | |
| // graceful and actionable error. | |
| this.fail( | |
| `RPC sequence id mismatch ` + | |
| `cur=${rpc.seq} last=${this.rxSeqId} (ERR:rpc_seq)`, | |
| ); | |
| } | |
| this.rxSeqId = rpc.seq; | |
| let isFinalResponse = true; | |
| switch (rpc.response) { | |
| case TPM.TPM_APPEND_TRACE_DATA: { | |
| const appendResult = ensureExists(rpc.appendResult); | |
| const pendingPromise = ensureExists(this.pendingParses.shift()); | |
| if (exists(appendResult.error) && appendResult.error.length > 0) { | |
| pendingPromise.reject(appendResult.error); | |
| } else { | |
| pendingPromise.resolve(); | |
| } | |
| break; | |
| } | |
| case TPM.TPM_FINALIZE_TRACE_DATA: { | |
| const finalizeResult = ensureExists(rpc.finalizeDataResult); | |
| const pendingPromise = ensureExists(this.pendingEOFs.shift()); | |
| if (exists(finalizeResult.error) && finalizeResult.error.length > 0) { | |
| pendingPromise.reject(finalizeResult.error); | |
| } else { | |
| pendingPromise.resolve(); | |
| } | |
| break; | |
| } | |
| case TPM.TPM_RESET_TRACE_PROCESSOR: | |
| ensureExists(this.pendingResetTraceProcessors.shift()).resolve(); | |
| break; | |
| case TPM.TPM_RESTORE_INITIAL_TABLES: | |
| ensureExists(this.pendingRestoreTables.shift()).resolve(); | |
| break; | |
| case TPM.TPM_QUERY_STREAMING: | |
| const qRes = ensureExists(rpc.queryResult) as {} as QueryResultBypass; | |
| const pendingQuery = ensureExists(this.pendingQueries[0]); | |
| pendingQuery.appendResultBatch(qRes.rawQueryResult); | |
| if (pendingQuery.isComplete()) { | |
| this.pendingQueries.shift(); | |
| } else { | |
| isFinalResponse = false; | |
| } | |
| break; | |
| case TPM.TPM_COMPUTE_METRIC: | |
| const metricRes = ensureExists( | |
| rpc.metricResult, | |
| ) as protos.ComputeMetricResult; | |
| const pendingComputeMetric = ensureExists( | |
| this.pendingComputeMetrics.shift(), | |
| ); | |
| if (exists(metricRes.error) && metricRes.error.length > 0) { | |
| const error = new QueryError( | |
| `ComputeMetric() error: ${metricRes.error}`, | |
| { | |
| query: 'COMPUTE_METRIC', | |
| }, | |
| ); | |
| pendingComputeMetric.reject(error); | |
| } else { | |
| const result = | |
| metricRes.metricsAsPrototext ?? | |
| metricRes.metricsAsJson ?? | |
| metricRes.metrics ?? | |
| ''; | |
| pendingComputeMetric.resolve(result); | |
| } | |
| break; | |
| case TPM.TPM_DISABLE_AND_READ_METATRACE: | |
| const metatraceRes = ensureExists( | |
| rpc.metatrace, | |
| ) as protos.DisableAndReadMetatraceResult; | |
| ensureExists(this.pendingReadMetatrace).resolve(metatraceRes); | |
| this.pendingReadMetatrace = undefined; | |
| break; | |
| case TPM.TPM_REGISTER_SQL_PACKAGE: | |
| const registerResult = ensureExists(rpc.registerSqlPackageResult); | |
| const res = ensureExists(this.pendingRegisterSqlPackage); | |
| if (exists(registerResult.error) && registerResult.error.length > 0) { | |
| res.reject(registerResult.error); | |
| } else { | |
| res.resolve(); | |
| } | |
| this.pendingRegisterSqlPackage = undefined; | |
| break; | |
| case TPM.TPM_SUMMARIZE_TRACE: | |
| const summaryRes = ensureExists( | |
| rpc.traceSummaryResult, | |
| ) as protos.TraceSummaryResult; | |
| ensureExists(this.pendingTraceSummary).resolve(summaryRes); | |
| this.pendingTraceSummary = undefined; | |
| break; | |
| case TPM.TPM_CREATE_SUMMARIZER: | |
| const createSummarizerRes = ensureExists( | |
| rpc.createSummarizerResult, | |
| ) as protos.CreateSummarizerResult; | |
| ensureExists(this.pendingCreateSummarizer).resolve(createSummarizerRes); | |
| this.pendingCreateSummarizer = undefined; | |
| break; | |
| case TPM.TPM_UPDATE_SUMMARIZER_SPEC: | |
| const updateSummarizerSpecRes = ensureExists( | |
| rpc.updateSummarizerSpecResult, | |
| ) as protos.UpdateSummarizerSpecResult; | |
| ensureExists(this.pendingUpdateSummarizerSpec).resolve( | |
| updateSummarizerSpecRes, | |
| ); | |
| this.pendingUpdateSummarizerSpec = undefined; | |
| break; | |
| case TPM.TPM_QUERY_SUMMARIZER: | |
| const querySummarizerRes = ensureExists( | |
| rpc.querySummarizerResult, | |
| ) as protos.QuerySummarizerResult; | |
| ensureExists(this.pendingQuerySummarizer).resolve(querySummarizerRes); | |
| this.pendingQuerySummarizer = undefined; | |
| break; | |
| case TPM.TPM_DESTROY_SUMMARIZER: | |
| const destroySummarizerRes = ensureExists( | |
| rpc.destroySummarizerResult, | |
| ) as protos.DestroySummarizerResult; | |
| ensureExists(this.pendingDestroySummarizer).resolve( | |
| destroySummarizerRes, | |
| ); | |
| this.pendingDestroySummarizer = undefined; | |
| break; | |
| case TPM.TPM_ENABLE_METATRACE: | |
| // We don't have any pending promises for this request so just | |
| // return. | |
| break; | |
| default: | |
| console.log( | |
| 'Unexpected TraceProcessor response received: ', | |
| rpc.response, | |
| ); | |
| break; | |
| } // switch(rpc.response); | |
| if (isFinalResponse) { | |
| --this._numRequestsPending; | |
| } | |
| this.onResponseReceived?.(); | |
| } | |
| // TraceProcessor methods below this point. | |
| // The methods below are called by the various controllers in the UI and | |
| // deal with marshalling / unmarshaling requests to/from TraceProcessor. | |
| // Push trace data into the engine. The engine is supposed to automatically | |
| // figure out the type of the trace (JSON vs Protobuf). | |
| parse(data: Uint8Array): Promise<void> { | |
| const asyncRes = defer<void>(); | |
| this.pendingParses.push(asyncRes); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_APPEND_TRACE_DATA; | |
| rpc.appendTraceData = data; | |
| this.rpcSendRequest(rpc); | |
| return asyncRes; // Linearize with the worker. | |
| } | |
| // Notify the engine that we reached the end of the trace. | |
| // Called after the last parse() call. | |
| notifyEof(): Promise<void> { | |
| const asyncRes = defer<void>(); | |
| this.pendingEOFs.push(asyncRes); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_FINALIZE_TRACE_DATA; | |
| this.rpcSendRequest(rpc); | |
| return asyncRes; // Linearize with the worker. | |
| } | |
| // Updates the TraceProcessor Config. This method creates a new | |
| // TraceProcessor instance, so it should be called before passing any trace | |
| // data. | |
| resetTraceProcessor({ | |
| tokenizeOnly, | |
| cropTrackEvents, | |
| ingestFtraceInRawTable, | |
| analyzeTraceProtoContent, | |
| ftraceDropUntilAllCpusValid, | |
| extraParsingDescriptors, | |
| forceFullSort, | |
| }: TraceProcessorConfig): Promise<void> { | |
| const asyncRes = defer<void>(); | |
| this.pendingResetTraceProcessors.push(asyncRes); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_RESET_TRACE_PROCESSOR; | |
| const args = (rpc.resetTraceProcessorArgs = | |
| new protos.ResetTraceProcessorArgs()); | |
| args.dropTrackEventDataBefore = cropTrackEvents | |
| ? protos.ResetTraceProcessorArgs.DropTrackEventDataBefore | |
| .TRACK_EVENT_RANGE_OF_INTEREST | |
| : protos.ResetTraceProcessorArgs.DropTrackEventDataBefore.NO_DROP; | |
| args.ingestFtraceInRawTable = ingestFtraceInRawTable; | |
| args.analyzeTraceProtoContent = analyzeTraceProtoContent; | |
| args.ftraceDropUntilAllCpusValid = ftraceDropUntilAllCpusValid; | |
| args.sortingMode = forceFullSort | |
| ? protos.ResetTraceProcessorArgs.SortingMode.FORCE_FULL_SORT | |
| : protos.ResetTraceProcessorArgs.SortingMode.DEFAULT_HEURISTICS; | |
| args.parsingMode = tokenizeOnly | |
| ? protos.ResetTraceProcessorArgs.ParsingMode.TOKENIZE_ONLY | |
| : protos.ResetTraceProcessorArgs.ParsingMode.DEFAULT; | |
| // If extraParsingDescriptors is defined, create a mutable copy for the | |
| // protobuf object; otherwise, pass an empty array. | |
| args.extraParsingDescriptors = extraParsingDescriptors | |
| ? [...extraParsingDescriptors] | |
| : []; | |
| this.rpcSendRequest(rpc); | |
| return asyncRes; | |
| } | |
| // Resets the trace processor state by destroying any table/views created by | |
| // the UI after loading. | |
| restoreInitialTables(): Promise<void> { | |
| const asyncRes = defer<void>(); | |
| this.pendingRestoreTables.push(asyncRes); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_RESTORE_INITIAL_TABLES; | |
| this.rpcSendRequest(rpc); | |
| return asyncRes; // Linearize with the worker. | |
| } | |
| // Shorthand for sending a compute metrics request to the engine. | |
| async computeMetric( | |
| metrics: string[], | |
| format: 'json' | 'prototext' | 'proto', | |
| ): Promise<string | Uint8Array> { | |
| const asyncRes = defer<string | Uint8Array>(); | |
| this.pendingComputeMetrics.push(asyncRes); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_COMPUTE_METRIC; | |
| const args = (rpc.computeMetricArgs = new protos.ComputeMetricArgs()); | |
| args.metricNames = metrics; | |
| if (format === 'json') { | |
| args.format = protos.ComputeMetricArgs.ResultFormat.JSON; | |
| } else if (format === 'prototext') { | |
| args.format = protos.ComputeMetricArgs.ResultFormat.TEXTPROTO; | |
| } else if (format === 'proto') { | |
| args.format = protos.ComputeMetricArgs.ResultFormat.BINARY_PROTOBUF; | |
| } else { | |
| throw new Error(`Unknown compute metric format ${format}`); | |
| } | |
| this.rpcSendRequest(rpc); | |
| return asyncRes; | |
| } | |
| summarizeTrace( | |
| summarySpecs: protos.TraceSummarySpec[] | string[], | |
| metricIds: string[] | undefined, | |
| metadataId: string | undefined, | |
| format: 'prototext' | 'proto', | |
| ): Promise<protos.TraceSummaryResult> { | |
| if (this.pendingTraceSummary) { | |
| return Promise.reject(new Error('Already summarizing trace')); | |
| } | |
| if (summarySpecs.length === 0) { | |
| return Promise.reject(new Error('No summary specs provided')); | |
| } | |
| const result = defer<protos.TraceSummaryResult>(); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_SUMMARIZE_TRACE; | |
| const args = (rpc.traceSummaryArgs = new protos.TraceSummaryArgs()); | |
| const computationSpec = new protos.TraceSummaryArgs.ComputationSpec(); | |
| if (metricIds) { | |
| computationSpec.metricIds = metricIds; | |
| } else { | |
| computationSpec.runAllMetrics = true; | |
| } | |
| if (metadataId) { | |
| computationSpec.metadataQueryId = metadataId; | |
| } | |
| args.computationSpec = computationSpec; | |
| if (typeof summarySpecs[0] === 'string') { | |
| args.textprotoSpecs = summarySpecs as string[]; | |
| } else { | |
| args.protoSpecs = summarySpecs as protos.TraceSummarySpec[]; | |
| } | |
| switch (format) { | |
| case 'prototext': | |
| args.outputFormat = protos.TraceSummaryArgs.Format.TEXTPROTO; | |
| break; | |
| case 'proto': | |
| args.outputFormat = protos.TraceSummaryArgs.Format.BINARY_PROTOBUF; | |
| break; | |
| default: | |
| assertUnreachable(format); | |
| } | |
| this.pendingTraceSummary = result; | |
| this.rpcSendRequest(rpc); | |
| return result; | |
| } | |
| // Issues a streaming query and retrieve results in batches. | |
| // The returned QueryResult object will be populated over time with batches | |
| // of rows (each batch conveys ~128KB of data and a variable number of rows). | |
| // The caller can decide whether to wait that all batches have been received | |
| // (by awaiting the returned object or calling result.waitAllRows()) or handle | |
| // the rows incrementally. | |
| // | |
| // Example usage: | |
| // const res = engine.execute('SELECT foo, bar FROM table'); | |
| // console.log(res.numRows()); // Will print 0 because we didn't await. | |
| // await(res.waitAllRows()); | |
| // console.log(res.numRows()); // Will print the total number of rows. | |
| // | |
| // for (const it = res.iter({foo: NUM, bar:STR}); it.valid(); it.next()) { | |
| // console.log(it.foo, it.bar); | |
| // } | |
| // | |
| // Optional |tag| (usually a component name) can be provided to allow | |
| // attributing trace processor workload to different UI components. | |
| // NOTE: the only reason why this is public is so that Winscope (which uses a | |
| // fork of our codebase) can invoke this directly. See commit msg of #3051. | |
| streamingQuery(result: WritableQueryResult, sqlQuery: string, tag?: string) { | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_QUERY_STREAMING; | |
| rpc.queryArgs = new protos.QueryArgs(); | |
| rpc.queryArgs.sqlQuery = sqlQuery; | |
| rpc.queryArgs.tag = tag; | |
| this.pendingQueries.push(result); | |
| this.rpcSendRequest(rpc); | |
| } | |
| private logQueryStart( | |
| query: string, | |
| tag?: string, | |
| ): { | |
| elapsedTimeMs?: number; | |
| success?: boolean; | |
| } { | |
| const startTime = performance.now(); | |
| const queryLog: QueryLog = { | |
| id: this._nextQueryLogId++, | |
| query, | |
| tag, | |
| startTime, | |
| }; | |
| this._queryLog.push(queryLog); | |
| if (this._queryLog.length > QUERY_LOG_BUFFER_SIZE) { | |
| this._queryLog.shift(); | |
| } | |
| return queryLog; | |
| } | |
| // Wraps .streamingQuery(), captures errors and re-throws with current stack. | |
| // | |
| // Note: This function is less flexible than .execute() as it only returns a | |
| // promise which must be unwrapped before the QueryResult may be accessed. | |
| async query(sqlQuery: string, tag?: string): Promise<QueryResult> { | |
| const queryLog = this.logQueryStart(sqlQuery, tag); | |
| try { | |
| const result = createQueryResult({query: sqlQuery, tag}); | |
| this.streamingQuery(result, sqlQuery, tag); | |
| const resolvedResult = await result; | |
| queryLog.success = true; | |
| queryLog.elapsedTimeMs = resolvedResult.elapsedTimeMs(); | |
| return resolvedResult; | |
| } catch (e) { | |
| // Replace the error's stack trace with the one from here | |
| // Note: It seems only V8 can trace the stack up the promise chain, so its | |
| // likely this stack won't be useful on !V8. | |
| // See | |
| // https://docs.google.com/document/d/13Sy_kBIJGP0XT34V1CV3nkWya4TwYx9L3Yv45LdGB6Q | |
| captureStackTrace(e); | |
| queryLog.success = false; | |
| throw e; | |
| } | |
| } | |
| async tryQuery(sql: string, tag?: string): Promise<Result<QueryResult>> { | |
| try { | |
| const result = await this.query(sql, tag); | |
| return okResult(result); | |
| } catch (error) { | |
| const msg = 'message' in error ? `${error.message}` : `${error}`; | |
| return errResult(msg); | |
| } | |
| } | |
| enableMetatrace(categories?: protos.MetatraceCategories) { | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_ENABLE_METATRACE; | |
| if ( | |
| categories !== undefined && | |
| categories !== protos.MetatraceCategories.NONE | |
| ) { | |
| rpc.enableMetatraceArgs = new protos.EnableMetatraceArgs(); | |
| rpc.enableMetatraceArgs.categories = categories; | |
| } | |
| this.rpcSendRequest(rpc); | |
| } | |
| stopAndGetMetatrace(): Promise<protos.DisableAndReadMetatraceResult> { | |
| // If we are already finalising a metatrace, ignore the request. | |
| if (this.pendingReadMetatrace) { | |
| return Promise.reject(new Error('Already finalising a metatrace')); | |
| } | |
| const result = defer<protos.DisableAndReadMetatraceResult>(); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_DISABLE_AND_READ_METATRACE; | |
| this.pendingReadMetatrace = result; | |
| this.rpcSendRequest(rpc); | |
| return result; | |
| } | |
| registerSqlPackages(pkg: { | |
| name: string; | |
| modules: ReadonlyArray<{name: string; sql: string}>; | |
| }): Promise<void> { | |
| if (this.pendingRegisterSqlPackage) { | |
| return Promise.reject(new Error('Already registering SQL package')); | |
| } | |
| const result = defer<void>(); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_REGISTER_SQL_PACKAGE; | |
| const args = (rpc.registerSqlPackageArgs = | |
| new protos.RegisterSqlPackageArgs()); | |
| args.packageName = pkg.name; | |
| args.modules = [...pkg.modules]; | |
| args.allowOverride = true; | |
| this.pendingRegisterSqlPackage = result; | |
| this.rpcSendRequest(rpc); | |
| return result; | |
| } | |
| createSummarizer( | |
| summarizerId: string, | |
| ): Promise<protos.CreateSummarizerResult> { | |
| if (this.pendingCreateSummarizer) { | |
| return Promise.reject(new Error('Already creating summarizer')); | |
| } | |
| const result = defer<protos.CreateSummarizerResult>(); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_CREATE_SUMMARIZER; | |
| rpc.createSummarizerArgs = new protos.CreateSummarizerArgs({ | |
| summarizerId, | |
| }); | |
| this.pendingCreateSummarizer = result; | |
| this.rpcSendRequest(rpc); | |
| return result; | |
| } | |
| updateSummarizerSpec( | |
| summarizerId: string, | |
| spec: protos.TraceSummarySpec, | |
| ): Promise<protos.UpdateSummarizerSpecResult> { | |
| if (this.pendingUpdateSummarizerSpec) { | |
| return Promise.reject(new Error('Already updating summarizer spec')); | |
| } | |
| const result = defer<protos.UpdateSummarizerSpecResult>(); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_UPDATE_SUMMARIZER_SPEC; | |
| const args = (rpc.updateSummarizerSpecArgs = | |
| new protos.UpdateSummarizerSpecArgs()); | |
| args.summarizerId = summarizerId; | |
| args.spec = spec; | |
| this.pendingUpdateSummarizerSpec = result; | |
| this.rpcSendRequest(rpc); | |
| return result; | |
| } | |
| querySummarizer( | |
| summarizerId: string, | |
| queryId: string, | |
| ): Promise<protos.QuerySummarizerResult> { | |
| if (this.pendingQuerySummarizer) { | |
| return Promise.reject(new Error('Already querying summarizer')); | |
| } | |
| const result = defer<protos.QuerySummarizerResult>(); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_QUERY_SUMMARIZER; | |
| const args = (rpc.querySummarizerArgs = new protos.QuerySummarizerArgs()); | |
| args.summarizerId = summarizerId; | |
| args.queryId = queryId; | |
| this.pendingQuerySummarizer = result; | |
| this.rpcSendRequest(rpc); | |
| return result; | |
| } | |
| destroySummarizer( | |
| summarizerId: string, | |
| ): Promise<protos.DestroySummarizerResult> { | |
| if (this.pendingDestroySummarizer) { | |
| return Promise.reject(new Error('Already destroying summarizer')); | |
| } | |
| const result = defer<protos.DestroySummarizerResult>(); | |
| const rpc = protos.TraceProcessorRpc.create(); | |
| rpc.request = TPM.TPM_DESTROY_SUMMARIZER; | |
| const args = (rpc.destroySummarizerArgs = | |
| new protos.DestroySummarizerArgs()); | |
| args.summarizerId = summarizerId; | |
| this.pendingDestroySummarizer = result; | |
| this.rpcSendRequest(rpc); | |
| return result; | |
| } | |
| // Marshals the TraceProcessorRpc request arguments and sends the request | |
| // to the concrete Engine (Wasm or HTTP). | |
| private rpcSendRequest(rpc: protos.TraceProcessorRpc) { | |
| rpc.seq = this.txSeqId++; | |
| // Each message is wrapped in a TraceProcessorRpcStream to add the varint | |
| // preamble with the size, which allows tokenization on the other end. | |
| const outerProto = protos.TraceProcessorRpcStream.create(); | |
| outerProto.msg.push(rpc); | |
| const buf = protos.TraceProcessorRpcStream.encode(outerProto).finish(); | |
| ++this._numRequestsPending; | |
| this.rpcSendRequestBytes(buf); | |
| } | |
| get engineId(): string { | |
| return this.id; | |
| } | |
| get numRequestsPending(): number { | |
| return this._numRequestsPending; | |
| } | |
| getProxy(tag: string): EngineProxy { | |
| return new EngineProxy(this, tag); | |
| } | |
| protected fail(reason: string) { | |
| this._failed = reason; | |
| throw new Error(reason); | |
| } | |
| get failed(): string | undefined { | |
| return this._failed; | |
| } | |
| abstract [Symbol.dispose](): void; | |
| } | |
| // Lightweight engine proxy which annotates all queries with a tag | |
| export class EngineProxy implements Engine, Disposable { | |
| private engine: EngineBase; | |
| private disposed = false; | |
| private tag: string; | |
| get queryLog() { | |
| return this.engine.queryLog; | |
| } | |
| clearQueryLog(): void { | |
| this.engine.clearQueryLog(); | |
| } | |
| constructor(engine: EngineBase, tag: string) { | |
| this.engine = engine; | |
| this.tag = tag; | |
| } | |
| async query(query: string): Promise<QueryResult> { | |
| if (this.disposed) { | |
| // If we are disposed (the trace was closed), return an empty QueryResult | |
| // that will never see any data or EOF. We can't do otherwise or it will | |
| // cause crashes to code calling firstRow() and expecting data. | |
| return createQueryResult({query, tag: this.tag}); | |
| } | |
| return await this.engine.query(query, this.tag); | |
| } | |
| async tryQuery(query: string): Promise<Result<QueryResult>> { | |
| if (this.disposed) { | |
| return errResult(`EngineProxy ${this.tag} was disposed`); | |
| } | |
| return await this.engine.tryQuery(query); | |
| } | |
| async computeMetric( | |
| metrics: string[], | |
| format: 'json' | 'prototext' | 'proto', | |
| ): Promise<string | Uint8Array> { | |
| if (this.disposed) { | |
| return defer<string>(); // Return a promise that will hang forever. | |
| } | |
| return this.engine.computeMetric(metrics, format); | |
| } | |
| summarizeTrace( | |
| summarySpecs: protos.TraceSummarySpec[] | string[], | |
| metricIds: string[] | undefined, | |
| metadataId: string | undefined, | |
| format: 'prototext' | 'proto', | |
| ): Promise<protos.TraceSummaryResult> { | |
| return this.engine.summarizeTrace( | |
| summarySpecs, | |
| metricIds, | |
| metadataId, | |
| format, | |
| ); | |
| } | |
| enableMetatrace(categories?: protos.MetatraceCategories): void { | |
| this.engine.enableMetatrace(categories); | |
| } | |
| stopAndGetMetatrace(): Promise<protos.DisableAndReadMetatraceResult> { | |
| return this.engine.stopAndGetMetatrace(); | |
| } | |
| createSummarizer( | |
| summarizerId: string, | |
| ): Promise<protos.CreateSummarizerResult> { | |
| return this.engine.createSummarizer(summarizerId); | |
| } | |
| updateSummarizerSpec( | |
| summarizerId: string, | |
| spec: protos.TraceSummarySpec, | |
| ): Promise<protos.UpdateSummarizerSpecResult> { | |
| return this.engine.updateSummarizerSpec(summarizerId, spec); | |
| } | |
| querySummarizer( | |
| summarizerId: string, | |
| queryId: string, | |
| ): Promise<protos.QuerySummarizerResult> { | |
| return this.engine.querySummarizer(summarizerId, queryId); | |
| } | |
| destroySummarizer( | |
| summarizerId: string, | |
| ): Promise<protos.DestroySummarizerResult> { | |
| return this.engine.destroySummarizer(summarizerId); | |
| } | |
| get engineId(): string { | |
| return this.engine.id; | |
| } | |
| getProxy(tag: string): EngineProxy { | |
| return this.engine.getProxy(`${this.tag}/${tag}`); | |
| } | |
| get numRequestsPending() { | |
| return this.engine.numRequestsPending; | |
| } | |
| get mode() { | |
| return this.engine.mode; | |
| } | |
| get failed() { | |
| return this.engine.failed; | |
| } | |
| [Symbol.dispose]() { | |
| this.disposed = true; | |
| } | |
| } | |
| // Capture stack trace and attach to the given error object | |
| function captureStackTrace(e: Error): void { | |
| const stack = new Error().stack; | |
| if ('captureStackTrace' in Error) { | |
| // V8 specific | |
| Error.captureStackTrace(e, captureStackTrace); | |
| } else { | |
| // Generic | |
| Object.defineProperty(e, 'stack', { | |
| value: stack, | |
| writable: true, | |
| configurable: true, | |
| }); | |
| } | |
| } | |
| // A convenience interface to inject the App in Mithril components. | |
| export interface EngineAttrs { | |
| engine: Engine; | |
| } |