Make an empty TP instance available to unit tests
diff --git a/include/perfetto/trace_processor/basic_types.h b/include/perfetto/trace_processor/basic_types.h index a960e15..be2a8ac 100644 --- a/include/perfetto/trace_processor/basic_types.h +++ b/include/perfetto/trace_processor/basic_types.h
@@ -215,6 +215,18 @@ // When provided, these descriptors allow trace processor to parse custom // protobuf messages that are not compiled into Perfetto std::vector<std::string> extra_parsing_descriptors; + + // When set to true, trace processor starts with a *bare* PerfettoSQL engine: + // only the core PerfettoSQL language (CREATE PERFETTO TABLE/FUNCTION/..., + // runtime functions) is available. None of the built-in trace tables (slice, + // thread, ...), stdlib packages, prelude, metrics or the trace_bounds table + // are created. This is intended for tests and tools that want a blank engine + // to define their own schema without name collisions or setup cost. + // + // Trace ingestion (Parse/NotifyEndOfFile) is NOT supported in this mode: the + // parsers rely on the built-in tables which are absent. Use it only to run + // SQL against tables you create yourself. + bool bare_sql_engine = false; }; // Represents a dynamically typed value returned by SQL.
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto index fe41e90..79f7537 100644 --- a/protos/perfetto/trace_processor/trace_processor.proto +++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -387,6 +387,12 @@ // A list of serialized FileDescriptorSet protos. repeated bytes extra_parsing_descriptors = 7; + + // When true, starts a bare PerfettoSQL engine: only the core language is + // available, with no built-in tables, stdlib, prelude or metrics. Trace + // ingestion is unsupported in this mode; intended for running SQL against + // tables created by the caller. + optional bool bare_sql_engine = 8; } message RegisterSqlPackageArgs {
diff --git a/python/perfetto/trace_processor/trace_processor.descriptor b/python/perfetto/trace_processor/trace_processor.descriptor index 16d356a..7d9e63f 100644 --- a/python/perfetto/trace_processor/trace_processor.descriptor +++ b/python/perfetto/trace_processor/trace_processor.descriptor Binary files differ
diff --git a/src/trace_processor/rpc/rpc.cc b/src/trace_processor/rpc/rpc.cc index ffb3445..b3d56d8 100644 --- a/src/trace_processor/rpc/rpc.cc +++ b/src/trace_processor/rpc/rpc.cc
@@ -543,6 +543,9 @@ config.analyze_trace_proto_content = reset_trace_processor_args.analyze_trace_proto_content(); } + if (reset_trace_processor_args.has_bare_sql_engine()) { + config.bare_sql_engine = reset_trace_processor_args.bare_sql_engine(); + } if (reset_trace_processor_args.has_ftrace_drop_until_all_cpus_valid()) { config.soft_drop_ftrace_data_before = reset_trace_processor_args.ftrace_drop_until_all_cpus_valid()
diff --git a/src/trace_processor/shell/common_flags.cc b/src/trace_processor/shell/common_flags.cc index 54259a2..ddb2291 100644 --- a/src/trace_processor/shell/common_flags.cc +++ b/src/trace_processor/shell/common_flags.cc
@@ -118,6 +118,10 @@ flags.push_back(BoolFlag("extra-checks", '\0', "Enables additional SQL error checks.", &opts->extra_checks)); + flags.push_back(BoolFlag( + "bare-sql-engine", '\0', + "Starts with a bare PerfettoSQL engine (no built-in tables/stdlib).", + &opts->bare_sql_engine)); flags.push_back( {/*long_name=*/"add-sql-package", /*short_name=*/'\0', /*has_arg=*/true, /*arg_name=*/"PATH[@PKG]", @@ -303,6 +307,10 @@ config.enable_extra_checks = true; } + if (opts.bare_sql_engine) { + config.bare_sql_engine = true; + } + return config; }
diff --git a/src/trace_processor/shell/common_flags.h b/src/trace_processor/shell/common_flags.h index 660022c..afba927 100644 --- a/src/trace_processor/shell/common_flags.h +++ b/src/trace_processor/shell/common_flags.h
@@ -51,6 +51,7 @@ bool dev = false; std::vector<std::string> dev_flags; bool extra_checks = false; + bool bare_sql_engine = false; std::vector<std::string> sql_package_paths; std::vector<std::string> override_sql_package_paths;
diff --git a/src/trace_processor/shell/query_subcommand.cc b/src/trace_processor/shell/query_subcommand.cc index 525d160..a169b3c 100644 --- a/src/trace_processor/shell/query_subcommand.cc +++ b/src/trace_processor/shell/query_subcommand.cc
@@ -203,8 +203,12 @@ auto config = BuildConfig(*ctx.global, ctx.platform); ASSIGN_OR_RETURN(auto tp, SetupTraceProcessor(*ctx.global, config, ctx.platform)); - ASSIGN_OR_RETURN(auto t_load, - LoadTraceFile(tp.get(), ctx.platform, trace_file)); + // Bare mode has no trace to ingest (and the built-in tables the load path + // relies on don't exist), so skip loading entirely. + base::TimeNanos t_load{}; + if (!ctx.global->bare_sql_engine) { + ASSIGN_OR_RETURN(t_load, LoadTraceFile(tp.get(), ctx.platform, trace_file)); + } if (!query_file_.empty()) { if (!base::ReadFile(query_file_, &sql)) {
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc index f13d781..8411647 100644 --- a/src/trace_processor/trace_processor_impl.cc +++ b/src/trace_processor/trace_processor_impl.cc
@@ -529,7 +529,7 @@ bool skip_all_sql = std::find(config_.skip_builtin_metric_paths.begin(), config_.skip_builtin_metric_paths.end(), "") != config_.skip_builtin_metric_paths.end(); - if (!skip_all_sql) { + if (!skip_all_sql && !config_.bare_sql_engine) { // Wrap the per-metric INSERTs in one transaction; otherwise SQLite // implicitly commits after each statement. PerfettoSqlConnection::Transaction txn(engine_.get()); @@ -612,14 +612,21 @@ } } - // Stage 4: prepare the connection for queries. - IncludeAfterEofPrelude(engine_.get()); + // Stage 4: prepare the connection for queries. In bare mode there is no + // prelude registered, so skip the include (and leave the engine untouched). + if (!config_.bare_sql_engine) { + IncludeAfterEofPrelude(engine_.get()); + } sqlite_objects_post_prelude_ = engine_->SqliteRegisteredObjectCount(); return base::OkStatus(); } void TraceProcessorImpl::CacheBoundsAndBuildTable() { + // Bare mode has no trace_bounds table (and no engine objects to back it). + if (config_.bare_sql_engine) { + return; + } uint64_t mutations = AggregatePluginBoundsMutationCount(plugins_); if (mutations == bounds_tables_mutations_) { return; @@ -980,6 +987,13 @@ auto connection = PerfettoSqlConnection::CreateConnectionToNewDatabase( storage->mutable_string_pool(), config.enable_extra_checks); + // Bare mode: return a connection with only the core PerfettoSQL language + // (registered by the PerfettoSqlConnection constructor). Skip built-in + // tables, stdlib packages, prelude, metrics and the trace_bounds table. + if (config.bare_sql_engine) { + return connection; + } + PerfettoSqlConnection::Initializer init; init.static_tables.reserve(plugin_dataframes.size()); for (const auto& df : plugin_dataframes) {
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc index 56b48b1..9aa0981 100644 --- a/src/trace_processor/trace_processor_shell.cc +++ b/src/trace_processor/trace_processor_shell.cc
@@ -129,6 +129,7 @@ bool dev = false; std::vector<std::string> dev_flags; bool extra_checks = false; + bool bare_sql_engine = false; std::string export_file_path; std::string perf_file_path; bool wide = false; @@ -320,6 +321,12 @@ --extra-checks Enables additional checks which can catch more SQL errors, but which incur additional runtime overhead. + --bare-sql-engine Starts with a bare PerfettoSQL engine: + only the core language is available, with + no built-in tables, stdlib, prelude or + metrics. Trace ingestion is unsupported; + intended for running SQL against tables + you create yourself. -e, --export FILE Export the contents of trace processor into an SQLite database after running any metrics or queries specified. @@ -422,6 +429,7 @@ OPT_DEV, OPT_DEV_FLAG, OPT_EXTRA_CHECKS, + OPT_BARE_SQL_ENGINE, OPT_ANALYZE_TRACE_PROTO_CONTENT, OPT_CROP_TRACK_EVENTS, OPT_REGISTER_FILES_DIR, @@ -475,6 +483,7 @@ {"dev", no_argument, nullptr, OPT_DEV}, {"dev-flag", required_argument, nullptr, OPT_DEV_FLAG}, {"extra-checks", no_argument, nullptr, OPT_EXTRA_CHECKS}, + {"bare-sql-engine", no_argument, nullptr, OPT_BARE_SQL_ENGINE}, {"export", required_argument, nullptr, 'e'}, {"perf-file", required_argument, nullptr, 'p'}, {"wide", no_argument, nullptr, 'W'}, @@ -615,6 +624,11 @@ continue; } + if (option == OPT_BARE_SQL_ENGINE) { + command_line_options.bare_sql_engine = true; + continue; + } + if (option == OPT_ADD_SQL_PACKAGE) { command_line_options.sql_package_paths.emplace_back(optarg); continue; @@ -937,6 +951,8 @@ } if (options.extra_checks) args.emplace_back("--extra-checks"); + if (options.bare_sql_engine) + args.emplace_back("--bare-sql-engine"); for (const auto& p : options.sql_package_paths) { args.emplace_back("--add-sql-package"); args.emplace_back(p);
diff --git a/ui/run-unittests b/ui/run-unittests index 3e81748..362f05d 100755 --- a/ui/run-unittests +++ b/ui/run-unittests
@@ -14,4 +14,4 @@ # limitations under the License. UI_DIR="$(cd -P ${BASH_SOURCE[0]%/*}; pwd)" -$UI_DIR/node $UI_DIR/build.mjs --only-wasm-memory64 --run-unittests "$@" +$UI_DIR/node $UI_DIR/build.mjs --run-unittests "$@"
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts index a9845a8..9218c0d 100644 --- a/ui/src/trace_processor/engine.ts +++ b/ui/src/trace_processor/engine.ts
@@ -39,13 +39,17 @@ // 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; + readonly tokenizeOnly?: boolean; + readonly cropTrackEvents?: boolean; + readonly ingestFtraceInRawTable?: boolean; + readonly analyzeTraceProtoContent?: boolean; + readonly ftraceDropUntilAllCpusValid?: boolean; + readonly extraParsingDescriptors?: ReadonlyArray<Uint8Array>; + readonly forceFullSort?: boolean; + // When true, starts a bare PerfettoSQL engine: only the core language, with no + // built-in tables, stdlib, prelude or metrics. Trace ingestion is unsupported + // in this mode; intended for running SQL against caller-created tables. + readonly bareSqlEngine?: boolean; } const QUERY_LOG_BUFFER_SIZE = 1024; @@ -427,15 +431,18 @@ // 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> { + resetTraceProcessor(config: TraceProcessorConfig): Promise<void> { + const { + tokenizeOnly = false, + cropTrackEvents = false, + ingestFtraceInRawTable = false, + analyzeTraceProtoContent = false, + ftraceDropUntilAllCpusValid = false, + extraParsingDescriptors, + forceFullSort = false, + bareSqlEngine = false, + } = config; + const asyncRes = defer<void>(); this.pendingResetTraceProcessors.push(asyncRes); const rpc = protos.TraceProcessorRpc.create(); @@ -460,6 +467,7 @@ args.extraParsingDescriptors = extraParsingDescriptors ? [...extraParsingDescriptors] : []; + args.bareSqlEngine = bareSqlEngine; this.rpcSendRequest(rpc); return asyncRes; }
diff --git a/ui/src/trace_processor/node_wasm_engine_for_testing.ts b/ui/src/trace_processor/node_wasm_engine_for_testing.ts new file mode 100644 index 0000000..b9d2dd7 --- /dev/null +++ b/ui/src/trace_processor/node_wasm_engine_for_testing.ts
@@ -0,0 +1,159 @@ +// Copyright (C) 2026 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. + +// A minimal EngineBase implementation that hosts the trace_processor WASM +// module directly in the current (Node) process — no Web Worker, no +// MessagePort. Intended only for vitest unit tests that want to run real +// PerfettoSQL queries against a real engine. +// +// The browser path (wasm_engine_proxy.ts + engine/wasm_bridge.ts) keeps the +// WASM on a worker and bridges I/O over a MessagePort for thread isolation. +// Here we drop that layer: the JS<>WASM RPC is synchronous, so we copy request +// bytes into the module's HEAPU8 and read the reply back inline via the same +// addFunction() callback the bridge uses. +// +// We target the 32-bit module so this works on Node versions whose V8 does not +// support the memory64 proposal. wasm_modules' TraceProcessor32 resolves via the +// trace_processor_32_stub indirection; vitest.config.mjs aliases that stub to +// the real ../gen/trace_processor module (matching the production vite alias). + +import * as fs from 'fs'; +import * as path from 'path'; + +// Type-only: the 32-bit stub is typed `never`, so we borrow the (identical) +// factory type from the memory64 declarations for the module/return types. +import type initTraceProcessorMem64 from '../gen/trace_processor_memory64'; +import {TraceProcessor32} from './wasm_modules'; +import {EngineBase} from './engine'; + +type InitTraceProcessor = typeof initTraceProcessorMem64; + +// Must match REQ_BUF_SIZE in engine/wasm_bridge.ts: the C++ side allocates a +// buffer of this size for incoming request bytes. +const REQ_BUF_SIZE = 32 * 1024 * 1024; + +// Converts a 32-bit wasm pointer into a positive number usable as a HEAPU8 +// offset. memory32 pointers come across as JS numbers, but pointers above 2GB +// arrive negative (the uint32 is reinterpreted as int32), so force unsigned. +// See WasmBridge.wasmPtrCast in engine/wasm_bridge.ts. +function wasmPtr32(val: number): number { + return val >>> 0; +} + +type WasmModule = Awaited<ReturnType<InitTraceProcessor>>; + +// Resolves the path to the 32-bit trace_processor.wasm. Assumes it has been +// built; if not, reading it later will fail loudly. The gen trace_processor.js +// factory is a build artifact whose real location is <outDir>/ui/tsc/gen; the +// matching .wasm lives in <outDir>/wasm. +export function locateTraceProcessorWasm(): string { + // cwd differs by entry point: ui/ when running vitest directly (src/gen is a + // symlink into the build output), or <outDir> under ui/run-unittests (gen + // lives at ui/tsc/gen). Resolve whichever exists, then derive the sibling. + const realGenJs = [ + path.resolve(process.cwd(), 'src/gen/trace_processor.js'), + path.resolve(process.cwd(), 'ui/tsc/gen/trace_processor.js'), + ].reduce<string | undefined>((found, p) => { + if (found) return found; + try { + return fs.realpathSync(p); + } catch { + return undefined; + } + }, undefined); + if (!realGenJs) { + throw new Error('trace_processor.js not found; build the UI wasm first.'); + } + // <outDir>/ui/tsc/gen/*.js -> <outDir>/wasm/trace_processor.wasm + return path.resolve( + path.dirname(realGenJs), + '../../../wasm/trace_processor.wasm', + ); +} + +export class NodeWasmEngine extends EngineBase implements Disposable { + readonly mode = 'WASM'; + readonly id = 'node-wasm'; + private module!: WasmModule; + private reqBufferAddr = 0; + + private constructor() { + super(); + } + + static async create(wasmPath: string): Promise<NodeWasmEngine> { + const engine = new NodeWasmEngine(); + await engine.init(wasmPath); + return engine; + } + + private async init(wasmPath: string): Promise<void> { + const file = fs.readFileSync(wasmPath); + // Hand the bytes directly to Emscripten so it doesn't try to fetch() them. + const wasmBinary = file.buffer.slice( + file.byteOffset, + file.byteOffset + file.byteLength, + ) as ArrayBuffer; + // TraceProcessor32 is the vite-aliased real 32-bit factory at runtime, but + // typed `never` (the stub); cast to the shared factory type. + const init = TraceProcessor32 as unknown as InitTraceProcessor; + const module = await init({ + locateFile: (s: string) => s, + print: (line: string) => console.log(line), + printErr: (line: string) => console.warn(line), + onRuntimeInitialized: () => {}, + wasmBinary, + }); + const fn = module.addFunction(this.onReply.bind(this), 'vpi'); + this.reqBufferAddr = wasmPtr32( + module.ccall( + 'trace_processor_rpc_init', + /* return=*/ 'pointer', + /* args=*/ ['pointer', 'number'], + [fn, REQ_BUF_SIZE], + ), + ); + this.module = module; + } + + // Called by the C++ code (while inside trace_processor_on_rpc_request) with a + // pointer/size into HEAPU8 holding the response bytes. + private onReply(heapPtr: number, size: number) { + const addr = wasmPtr32(heapPtr); + const data = this.module.HEAPU8.slice(addr, addr + size); + super.onRpcResponseBytes(data); + } + + rpcSendRequestBytes(data: Uint8Array): void { + // Mirrors WasmBridge.onMessage: split into REQ_BUF_SIZE-sized writes since + // the RPC channel is byte-oriented and handles arbitrary fragmentation. + let wrSize = 0; + while (wrSize < data.length) { + const sliceLen = Math.min(data.length - wrSize, REQ_BUF_SIZE); + this.module.HEAPU8.set( + data.subarray(wrSize, wrSize + sliceLen), + this.reqBufferAddr, + ); + wrSize += sliceLen; + this.module.ccall( + 'trace_processor_on_rpc_request', + 'void', + ['number'], + [sliceLen], + ); + } + } + + [Symbol.dispose]() {} +}
diff --git a/ui/src/trace_processor/node_wasm_engine_unittest.ts b/ui/src/trace_processor/node_wasm_engine_unittest.ts new file mode 100644 index 0000000..586c40e --- /dev/null +++ b/ui/src/trace_processor/node_wasm_engine_unittest.ts
@@ -0,0 +1,86 @@ +// Copyright (C) 2026 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. + +// Runs the real trace_processor WASM in-process (via NodeWasmEngine) using the +// bare_sql_engine config, then defines a minimal "slice" table and runs queries +// against it. This exercises the full JS<>WASM RPC path without loading a trace. +// +// The test needs the trace_processor.wasm artifact to be built (e.g. via +// ui/build.mjs or `ninja -C out/ui trace_processor_wasm`). If it isn't present +// the whole suite is skipped rather than failing. + +import {NUM, STR} from './query_result'; +import { + NodeWasmEngine, + locateTraceProcessorWasm, +} from './node_wasm_engine_for_testing'; +import {createPerfettoTable} from './sql_utils'; + +const wasmPath = locateTraceProcessorWasm(); + +describe('NodeWasmEngine (bare SQL engine)', () => { + let engine: NodeWasmEngine; + + beforeAll(async () => { + engine = await NodeWasmEngine.create(wasmPath); + await engine.resetTraceProcessor({bareSqlEngine: true}); + }); + + afterAll(() => { + engine?.[Symbol.dispose](); + }); + + test('built-in slice table is absent in bare mode', async () => { + const res = await engine.tryQuery('SELECT count(*) FROM slice'); + expect(res.ok).toBe(false); + }); + + test('can create a slice table and query it', async () => { + await createPerfettoTable({ + engine, + name: 'slice', + as: [ + {id: 1, ts: 100n, dur: 10, name: 'foo'}, + {id: 2, ts: 200n, dur: 50, name: 'bar'}, + {id: 3, ts: 300n, dur: 5, name: 'foo'}, + ], + }); + + const agg = await engine.query( + 'SELECT count(*) AS cnt, sum(dur) AS total_dur FROM slice', + ); + const aggRow = agg.firstRow({cnt: NUM, total_dur: NUM}); + expect(aggRow.cnt).toBe(3); + expect(aggRow.total_dur).toBe(65); + + const filtered = await engine.query( + "SELECT count(*) AS cnt FROM slice WHERE name = 'foo'", + ); + expect(filtered.firstRow({cnt: NUM}).cnt).toBe(2); + + const longest = await engine.query( + 'SELECT name FROM slice ORDER BY dur DESC LIMIT 1', + ); + expect(longest.firstRow({name: STR}).name).toBe('bar'); + }); + + test('can iterate multiple rows', async () => { + const result = await engine.query('SELECT id, name FROM slice ORDER BY id'); + const names: string[] = []; + for (const it = result.iter({id: NUM, name: STR}); it.valid(); it.next()) { + names.push(it.name); + } + expect(names).toEqual(['foo', 'bar', 'foo']); + }); +});
diff --git a/ui/src/trace_processor/sql_utils.ts b/ui/src/trace_processor/sql_utils.ts index 931b2d2..c6a0da3 100644 --- a/ui/src/trace_processor/sql_utils.ts +++ b/ui/src/trace_processor/sql_utils.ts
@@ -153,17 +153,50 @@ type CreateTableArgs = { readonly engine: Engine; - readonly as: string; + readonly as: string | readonly Record<string, SqlValue>[]; readonly name?: string; }; /** + * Builds a SELECT statement that materializes the given rows inline, e.g. for + * use as a subquery or to define a table without a source trace. + * + * Each row becomes `SELECT <value> AS <col>, ...` and the rows are combined + * with `UNION ALL`, so the result is a self-contained query yielding exactly + * those rows. Column names are taken from the first row; every row is expected + * to have the same keys. Values are rendered via sqlValueToSqliteString(). + * + * Example: [{id: 1, name: 'a'}, {id: 2, name: 'b'}] produces + * SELECT 1 AS id, 'a' AS name UNION ALL SELECT 2 AS id, 'b' AS name + * + * @param rows The rows to materialize; must be non-empty. + * @returns The combined SELECT ... UNION ALL ... query string. + */ +export function rowsToSelectStatement( + rows: readonly Record<string, SqlValue>[], +) { + const firstRow = rows[0]; + const columnNames = Object.keys(firstRow); + const queryRows: string[] = []; + for (const row of rows) { + const queryCols: string[] = []; + for (const key of columnNames) { + const value = row[key]; + queryCols.push(`${sqlValueToSqliteString(value)} AS ${key}`); + } + queryRows.push(`SELECT ${queryCols.join(', ')}`); + } + + return queryRows.join(' UNION ALL '); +} + +/** * Asynchronously creates a "perfetto" SQL table using the given engine and * returns a disposable object to handle its cleanup. * * @param args The arguments for creating the table. * @param args.engine The database engine to execute the query. - * @param args.as The SQL expression to define the table. + * @param args.as The SQL expression to define the table or an array of row-like objects. * @param args.name The name of the table to be created. * @returns An AsyncDisposable which drops the created table when disposed. */ @@ -171,7 +204,13 @@ args: CreateTableArgs, ): Promise<DisposableSqlEntity> { const {engine, as, name = makeTempName()} = args; - await engine.query(`CREATE PERFETTO TABLE ${name} AS ${as}`); + let asQuery: string; + if (typeof as === 'string') { + asQuery = as; + } else { + asQuery = rowsToSelectStatement(as); + } + await engine.query(`CREATE PERFETTO TABLE ${name} AS ${asQuery}`); return createDisposableSqlEntity(engine, name, 'TABLE'); }
diff --git a/ui/src/trace_processor/sql_utils_unittest.ts b/ui/src/trace_processor/sql_utils_unittest.ts index 31b006e..ce9e84b 100644 --- a/ui/src/trace_processor/sql_utils_unittest.ts +++ b/ui/src/trace_processor/sql_utils_unittest.ts
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {constraintsToQuerySuffix} from './sql_utils'; +import {constraintsToQuerySuffix, rowsToSelectStatement} from './sql_utils'; // Clean up repeated whitespaces to allow for easier testing. function normalize(s: string): string { @@ -82,3 +82,36 @@ ), ).toEqual(''); }); + +describe('createTableQuery', () => { + test('single row', () => { + const query = rowsToSelectStatement([ + {foo: 123, bar: 'bar', baz: 123n, qux: null}, + ]); + expect(query).toBe( + "SELECT 123 AS foo, 'bar' AS bar, 123 AS baz, NULL AS qux", + ); + }); + + test('multiple rows', () => { + const query = rowsToSelectStatement([ + {foo: 123, bar: 'bar', baz: 123n, qux: null}, + {foo: 456, bar: 'baz', baz: 456n, qux: null}, + ]); + expect(query).toBe( + "SELECT 123 AS foo, 'bar' AS bar, 123 AS baz, NULL AS qux " + + "UNION ALL SELECT 456 AS foo, 'baz' AS bar, 456 AS baz, NULL AS qux", + ); + }); + + test('out of order columns', () => { + const query = rowsToSelectStatement([ + {foo: 123, bar: 'bar', baz: 123n, qux: null}, + {bar: 'baz', baz: 456n, qux: null, foo: 456}, + ]); + expect(query).toBe( + "SELECT 123 AS foo, 'bar' AS bar, 123 AS baz, NULL AS qux " + + "UNION ALL SELECT 456 AS foo, 'baz' AS bar, 456 AS baz, NULL AS qux", + ); + }); +});
diff --git a/ui/vite.config.mjs b/ui/vite.config.mjs index fee64aa..743e7f8 100644 --- a/ui/vite.config.mjs +++ b/ui/vite.config.mjs
@@ -251,7 +251,7 @@ }); } -function pluginGenRelativeImports() { +export function pluginGenRelativeImports() { return { name: 'perfetto:gen-relative-imports', enforce: 'pre', @@ -274,7 +274,7 @@ // `export default`. In build mode @rollup/plugin-commonjs handles this; in // dev we transform the source: run the UMD body with a synthesised // `module`/`exports`, then re-export `module.exports.default` as ESM default. -function pluginGenWasmGlueEsm() { +export function pluginGenWasmGlueEsm() { return { name: 'perfetto:gen-wasm-glue-esm', enforce: 'pre',
diff --git a/ui/vitest.config.mjs b/ui/vitest.config.mjs index 7a4df35..b9dc865 100644 --- a/ui/vitest.config.mjs +++ b/ui/vitest.config.mjs
@@ -24,6 +24,8 @@ import { pluginPerfettoPluginBarrels, pluginPerfettoVersion, + pluginGenRelativeImports, + pluginGenWasmGlueEsm, } from './vite.config.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -39,7 +41,22 @@ // Synthesised modules under ui/src/virtual (version, plugin barrels). pluginPerfettoVersion(), pluginPerfettoPluginBarrels(), + // Allow tests to import the UMD wasm glue under src/gen (e.g. the node + // trace_processor engine shim used by *_unittest.ts running real wasm). + pluginGenRelativeImports(), + pluginGenWasmGlueEsm(), ], + resolve: { + alias: [ + // Mirror the production vite alias: resolve the trace_processor_32_stub + // indirection to the real 32-bit gen module, so tests that use the WASM + // engine load the 32-bit build (works on Node without memory64 support). + { + find: /.*\/trace_processor_32_stub$/, + replacement: path.join(__dirname, 'src/gen/trace_processor'), + }, + ], + }, define: { 'process.env.NODE_ENV': JSON.stringify('test'), },