Refactoring loadtrace Change-Id: I8cea647f332cb95b24de6df3f3339a429a1ff616
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts index 93c6eab..eae35f5 100644 --- a/ui/src/core/app_impl.ts +++ b/ui/src/core/app_impl.ts
@@ -24,11 +24,16 @@ import {Setting} from '../public/settings'; import {TraceStream} from '../public/stream'; import {DurationPrecision, TimestampFormat} from '../public/timeline'; -import {NewEngineMode} from '../trace_processor/engine'; +import {EngineBase, NewEngineMode} from '../trace_processor/engine'; import {AnalyticsInternal, initAnalytics} from './analytics_impl'; -import {CommandInvocation, CommandManagerImpl, Macro} from './command_manager'; +import { + CommandInvocation, + CommandManagerImpl, + Macro, + parseUrlCommands, +} from './command_manager'; import {featureFlags} from './feature_flags'; -import {loadTrace} from './load_trace'; +import {loadTrace, RawTrace} from './load_trace'; import {OmniboxManagerImpl} from './omnibox_manager'; import {PageManagerImpl} from './page_manager'; import {PerfManager} from './perf_manager'; @@ -43,6 +48,14 @@ import {TaskTrackerImpl} from '../frontend/task_tracker/task_tracker'; import {Embedder} from './embedder/embedder'; import {createEmbedder} from './embedder/create_embedder'; +import { + deserializeAppStatePhase1, + deserializeAppStatePhase2, +} from './state_serialization'; +import {cacheTrace} from './cache_manager'; +import {HighPrecisionTimeSpan} from '../base/high_precision_time_span'; +import {base64Decode} from '../base/string_utils'; +import {uuidv4} from '../base/uuid'; export type OpenTraceArrayBufArgs = Omit< Omit<TraceArrayBufferSource, 'type'>, @@ -163,6 +176,14 @@ this.pages = new PageManagerImpl(this.analytics); } + renderPageForCurrentRoute() { + if (this.trace) { + return this.trace.pages.renderPageForCurrentRoute(); + } else { + return this.pages.renderPageForCurrentRoute(); + } + } + setActiveTrace(trace: TraceImpl) { this.closeCurrentTrace(); this._activeTrace = trace; @@ -195,7 +216,10 @@ } get trace(): TraceImpl | undefined { - return this._activeTrace; + // Parse the doc out of the location hash + const currentRoute = Router.parseFragment(location.hash); + const docUuid = currentRoute.args.doc as string; + return this.traces.get(docUuid); } get raf(): Raf { @@ -235,26 +259,15 @@ return this.openTrace({type: 'HTTP_RPC'}); } - private async openTrace(src: TraceSource): Promise<TraceImpl> { - if (src.type === 'ARRAY_BUFFER' && src.buffer instanceof Uint8Array) { - // Even though the type of `buffer` is ArrayBuffer, it's possible to - // accidentally pass a Uint8Array here, because the interface of - // Uint8Array is compatible with ArrayBuffer. That can cause subtle bugs - // in TraceStream when creating chunks out of it (see b/390473162). - // So if we get a Uint8Array in input, convert it into an actual - // ArrayBuffer, as various parts of the codebase assume that this is a - // pure ArrayBuffer, and not a logical view of it with a byteOffset > 0. - if ( - src.buffer.byteOffset === 0 && - src.buffer.byteLength === src.buffer.buffer.byteLength - ) { - src = {...src, buffer: src.buffer.buffer}; - } else { - src = {...src, buffer: src.buffer.slice().buffer}; - } - } + // A map of loaded traces by document key + traces = new Map<string, TraceImpl>(); + private async openTrace(src: TraceSource): Promise<TraceImpl> { const result = defer<TraceImpl>(); + const documentUuid = uuidv4(); + + // Update the URL bar to the new document ID + document.location.hash = '#!/?doc=' + documentUuid; // Rationale for asyncLimiter: openTrace takes several seconds and involves // a long sequence of async tasks (e.g. invoking plugins' onLoad()). These @@ -269,6 +282,14 @@ this.closeCurrentTrace(); this.isLoadingTrace = true; try { + const extraParsingDescriptors: Uint8Array[] = []; + for (const b64Str of await this.protoDescriptors()) { + extraParsingDescriptors.push(base64Decode(b64Str)); + } + + const useHttpIfAvailable = + this.httpRpc.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE'; + // loadTrace() in trace_loader.ts will do the following: // - Create a new engine. // - Pump the data from the TraceSource into the engine. @@ -276,13 +297,36 @@ // - Call AppImpl.setActiveTrace(TraceImpl) // - Continue with the trace loading logic (track decider, plugins, etc) // - Resolve the promise when everything is done. - const trace = await loadTrace(this, src); + const rawTrace = await loadTrace(src, { + useHttpIfAvailable, + extraParsingDescriptors, + }); + + // Try to cache the trace bytes if possible + const cached = await cacheTrace(src, rawTrace.uuid); + + const trace = new TraceImpl(this, rawTrace.engine, { + source: src, + cached: cached, + uuid: rawTrace.uuid, + start: rawTrace.traceSpan.start, + end: rawTrace.traceSpan.end, + tzOffMin: rawTrace.tzOffMin, + unixOffset: rawTrace.unixOffset, + traceTypes: rawTrace.traceTypes, + hasFtrace: rawTrace.hasFtrace, + traceTitle: rawTrace.traceTitle, + traceUrl: rawTrace.traceUrl, + downloadable: rawTrace.downloadable, + importErrors: rawTrace.importErrors, + }); + + this.setActiveTrace(trace); + await this.initializeTrace(rawTrace, trace, src); this.omnibox.reset(/* focus= */ false); - // loadTrace() internally will call setActiveTrace() and change our - // _currentTrace in the middle of its ececution. We cannot wait for - // loadTrace to be finished before setting it because some internal - // implementation details of loadTrace() rely on that trace to be current - // to work properly (mainly the router hash uuid). + + this.traces.set(documentUuid, trace); + result.resolve(trace); } catch (error) { result.reject(error); @@ -334,4 +378,139 @@ const macrosArray = await Promise.all(this._macrosPromises); return macrosArray.flat(); } + + async initializeTrace( + rawTrace: RawTrace, + trace: TraceImpl, + traceSource: TraceSource, + ): Promise<TraceImpl> { + const engine = rawTrace.engine; + + await this.installSqlPackages(engine); + + if (traceSource.serializedAppState !== undefined) { + deserializeAppStatePhase1(traceSource.serializedAppState, trace); + } + + // Initialize the plugins (call onTraceLoad() on all plugins) + await this.plugins.onTraceLoad(trace, (id) => { + this.updateStatus(`Running plugin: ${id}`); + }); + + // Decide which tabls to show on startup + this.decideTabs(trace); + + // Load the minimap after pluigns have had a chance to register their loaders + this.updateStatus(`Loading minimap`); + await trace.minimap.load(rawTrace.traceSpan.start, rawTrace.traceSpan.end); + + // // Trace Processor doesn't support the reliable range feature for JSON + // // traces. + // if (!hasJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get()) { + // const reliableRangeStart = await computeTraceReliableRangeStart(engine); + // if (reliableRangeStart > 0) { + // trace.notes.addNote({ + // timestamp: reliableRangeStart, + // color: '#ff0000', + // text: 'Reliable Range Start', + // }); + // } + // } + + // notify() will await that all listeners' promises have resolved. + await trace.onTraceReady.notify(); + + if (traceSource.serializedAppState !== undefined) { + // Wait that plugins have completed their actions and then proceed with + // the final phase of app state restore. + // TODO(primiano): this can probably be removed once we refactor tracks + // to be URI based and can deal with non-existing URIs. + deserializeAppStatePhase2(traceSource.serializedAppState, trace); + } + + // Update the timeline to show the reliable range of the trace + trace.timeline.setVisibleWindow( + HighPrecisionTimeSpan.fromTime( + rawTrace.reliableRange.start, + rawTrace.reliableRange.end, + ), + ); + + // Execute startup commands as the final step - simulates user actions + // after the trace is fully loaded and any saved state has been restored. + // This ensures startup commands see the complete, final state of the trace. + await this.runStartupCommands(trace); + + return trace; + } + + private async runStartupCommands(trace: TraceImpl) { + // CRITICAL ORDER: URL commands MUST execute before settings commands! + // This ordering has subtle but important implications: + // - URL commands are trace-specific and should establish initial state + // - Settings commands are user preferences that should override URL defaults + // - Changing this order could break trace sharing and user customization + // DO NOT REORDER without understanding the full impact! + const urlCommands = + parseUrlCommands(this.initialRouteArgs.startupCommands) ?? []; + const settingsCommands = this.startupCommandsSetting.get(); + + // Combine URL and settings commands - runtime allowlist checking will handle filtering + const allStartupCommands = [...urlCommands, ...settingsCommands]; + const enforceAllowlist = this.enforceStartupCommandAllowlistSetting.get(); + + if (allStartupCommands.length > 0) { + this.updateStatus('Running startup commands'); + using _ = trace.omnibox.disablePrompts(); + + // Execute startup commands in trace context after everything is ready. + // This simulates user actions taken after trace load is complete, + // including any saved app state restoration. At this point: + // - All plugins have loaded and registered their commands + // - Trace data is fully accessible + // - UI state has been restored from any saved workspace + // - Commands can safely query trace data and modify UI state + // Set allowlist checking during startup if enforcement enabled + if (enforceAllowlist) { + this.commands.setExecutingStartupCommands(true); + } + + try { + for (const command of allStartupCommands) { + try { + // Execute through proxy to access both global and trace-specific + // commands. + await this.commands.runCommand(command.id, ...command.args); + } catch (error) { + // TODO(stevegolton): Add a mechanism to notify users of startup + // command errors. This will involve creating a notification UX + // similar to VSCode where there are popups on the bottom right + // of the UI. + console.warn(`Startup command ${command.id} failed:`, error); + } + } + } finally { + // Always restore default (allow all) behavior when done + this.commands.setExecutingStartupCommands(false); + } + } + } + + private updateStatus(message: string) { + this.omnibox.showStatusMessage(message, 0); + } + + private async installSqlPackages(engine: EngineBase) { + const sqlPackages = await this.sqlPackages(); + for (const pkg of sqlPackages) { + await engine.registerSqlPackages(pkg); + } + } + + private decideTabs(trace: TraceImpl) { + // Show the list of default tabs, but don't make them active! + for (const tabUri of trace.tabs.defaultTabs) { + trace.tabs.showTab(tabUri); + } + } }
diff --git a/ui/src/core/load_trace.ts b/ui/src/core/load_trace.ts index 0c9258a..7583b5c 100644 --- a/ui/src/core/load_trace.ts +++ b/ui/src/core/load_trace.ts
@@ -13,13 +13,15 @@ // limitations under the License. import {assertExists, assertTrue} from '../base/assert'; +import {sha1} from '../base/hash'; import {time, Time, TimeSpan} from '../base/time'; -import {cacheTrace} from './cache_manager'; import { - getEnabledMetatracingCategories, - isMetatracingEnabled, -} from './metatracing'; -import {featureFlags} from './feature_flags'; + TraceBufferStream, + TraceFileStream, + TraceHttpStream, + TraceMultipleFilesStream, +} from '../core/trace_stream'; +import {TraceStream} from '../public/stream'; import {Engine, EngineBase} from '../trace_processor/engine'; import {HttpRpcEngine} from '../trace_processor/http_rpc_engine'; import { @@ -30,27 +32,13 @@ STR, } from '../trace_processor/query_result'; import {WasmEngineProxy} from '../trace_processor/wasm_engine_proxy'; +import {featureFlags} from './feature_flags'; import { - TraceBufferStream, - TraceFileStream, - TraceHttpStream, - TraceMultipleFilesStream, -} from '../core/trace_stream'; -import {TraceStream} from '../public/stream'; -import { - deserializeAppStatePhase1, - deserializeAppStatePhase2, -} from './state_serialization'; -import {AppImpl} from './app_impl'; + getEnabledMetatracingCategories, + isMetatracingEnabled, +} from './metatracing'; import {raf} from './raf_scheduler'; -import {TraceImpl} from './trace_impl'; import {TraceSource} from './trace_source'; -import {Router} from '../core/router'; -import {TraceInfoImpl} from './trace_info_impl'; -import {base64Decode} from '../base/string_utils'; -import {parseUrlCommands} from './command_manager'; -import {HighPrecisionTimeSpan} from '../base/high_precision_time_span'; -import {sha1} from '../base/hash'; const ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG = featureFlags.register({ id: 'enableChromeReliableRangeZoom', @@ -101,54 +89,125 @@ defaultValue: false, }); -// TODO(stevegolton): Move this into some global "SQL extensions" file and -// ensure it's only run once. -async function defineMaxLayoutDepthSqlFunction(engine: Engine): Promise<void> { - await engine.query(` - create perfetto function __max_layout_depth(track_count INT, track_ids STRING) - returns INT AS - select iif( - $track_count = 1, - ( - select max_depth - from _slice_track_summary - where id = cast($track_ids AS int) - ), - ( - select max(layout_depth) - from experimental_slice_layout($track_ids) - ) - ); - `); +export interface RawTrace extends Disposable { + readonly engine: EngineBase; + readonly uuid: string; + readonly traceSpan: TimeSpan; + readonly reliableRange: TimeSpan; + readonly tzOffMin: number; + readonly unixOffset: time; + readonly traceTypes: string[]; + readonly hasFtrace: boolean; + readonly traceTitle: string; + readonly traceUrl: string; + readonly downloadable: boolean; + readonly importErrors: number; +} + +interface TraceLoadArgs { + readonly useHttpIfAvailable?: boolean; + readonly extraParsingDescriptors?: readonly Uint8Array[]; } let lastEngineId = 0; +// Loads a trace from a given source and returns the loaded trace object export async function loadTrace( - app: AppImpl, traceSource: TraceSource, -): Promise<TraceImpl> { - updateStatus(app, 'Opening trace'); + opts: TraceLoadArgs = {}, +): Promise<RawTrace> { + const {useHttpIfAvailable = false, extraParsingDescriptors = []} = opts; + + traceSource = maybeFixBuffers(traceSource); const engineId = `${++lastEngineId}`; - const engine = await createEngine(app, engineId); - return await loadTraceIntoEngine(app, traceSource, engine); + const engine = await createEngine( + engineId, + useHttpIfAvailable, + extraParsingDescriptors, + ); + engine.onResponseReceived = function () { + raf.scheduleFullRedraw(); + }; + + await engine.resetTraceProcessor({ + tokenizeOnly: false, + cropTrackEvents: CROP_TRACK_EVENTS_FLAG.get(), + ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(), + analyzeTraceProtoContent: ANALYZE_TRACE_PROTO_CONTENT_FLAG.get(), + ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(), + forceFullSort: FORCE_FULL_SORT_FLAG.get(), + }); + + await loadTraceIntoEngine(engine, traceSource); + + const uuid = await getTraceUuid(engine); + const traceTypes = await getTraceTypes(engine); + const traceSpan = await getTraceSpan(engine); + const hasJsonTrace = traceTypes.includes('json'); + const reliableRange = await computeVisibleTime( + engine, + traceSpan, + hasJsonTrace, + ); + const tzOffMin = await getTzOffset(engine); + const unixOffset = await getUnixEpochOffset(engine); + const hasFtrace = await getHasFtrace(engine); + const {traceTitle, traceUrl} = getTraceTitleAndUrl(traceSource); + const downloadable = isDownloadable(traceSource); + const importErrors = await getTraceErrors(engine); + await includeSummaryTables(engine); + await defineMaxLayoutDepthSqlFunction(engine); + + return { + engine, + uuid, + traceSpan, + reliableRange, + tzOffMin, + unixOffset, + traceTypes, + hasFtrace, + traceTitle, + traceUrl, + downloadable, + importErrors, + [Symbol.dispose]() { + console.log(`Disposing trace: ${uuid}`); + engine[Symbol.dispose](); + }, + }; +} + +function maybeFixBuffers(src: TraceSource) { + if (src.type === 'ARRAY_BUFFER' && src.buffer instanceof Uint8Array) { + // Even though the type of `buffer` is ArrayBuffer, it's possible to + // accidentally pass a Uint8Array here, because the interface of + // Uint8Array is compatible with ArrayBuffer. That can cause subtle bugs + // in TraceStream when creating chunks out of it (see b/390473162). + // So if we get a Uint8Array in input, convert it into an actual + // ArrayBuffer, as various parts of the codebase assume that this is a + // pure ArrayBuffer, and not a logical view of it with a byteOffset > 0. + const u8 = src.buffer as unknown as Uint8Array; + if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength) { + src = {...src, buffer: u8.buffer as ArrayBuffer}; + } else { + src = {...src, buffer: u8.slice().buffer as ArrayBuffer}; + } + } + return src; } async function createEngine( - app: AppImpl, engineId: string, + useHttpEngineIfAvailable: boolean, + extraParsingDescriptors: readonly Uint8Array[] = [], ): Promise<EngineBase> { // Check if there is any instance of the trace_processor_shell running in // HTTP RPC mode (i.e. trace_processor_shell -D). - let useRpc = false; - if (app.httpRpc.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') { - useRpc = (await HttpRpcEngine.checkConnection()).connected; - } + const useRpc = + useHttpEngineIfAvailable && + (await HttpRpcEngine.checkConnection()).connected; - const descriptorBlobs: Uint8Array[] = []; - for (const b64Str of await app.protoDescriptors()) { - descriptorBlobs.push(base64Decode(b64Str)); - } let engine; if (useRpc) { console.log('Opening trace using native accelerator over HTTP+RPC'); @@ -162,7 +221,7 @@ ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(), analyzeTraceProtoContent: ANALYZE_TRACE_PROTO_CONTENT_FLAG.get(), ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(), - extraParsingDescriptors: descriptorBlobs, + extraParsingDescriptors, forceFullSort: FORCE_FULL_SORT_FLAG.get(), }); } @@ -175,27 +234,10 @@ } async function loadTraceIntoEngine( - app: AppImpl, - traceSource: TraceSource, engine: EngineBase, -): Promise<TraceImpl> { - let traceStream: TraceStream | undefined; - const serializedAppState = traceSource.serializedAppState; - if (traceSource.type === 'FILE') { - traceStream = new TraceFileStream(traceSource.file); - } else if (traceSource.type === 'ARRAY_BUFFER') { - traceStream = new TraceBufferStream(traceSource.buffer); - } else if (traceSource.type === 'URL') { - traceStream = new TraceHttpStream(traceSource.url); - } else if (traceSource.type === 'STREAM') { - traceStream = traceSource.stream; - } else if (traceSource.type === 'HTTP_RPC') { - traceStream = undefined; - } else if (traceSource.type === 'MULTIPLE_FILES') { - traceStream = new TraceMultipleFilesStream(traceSource.files); - } else { - throw new Error(`Unknown source: ${JSON.stringify(traceSource)}`); - } + traceSource: TraceSource, +): Promise<void> { + const traceStream = createStreamFromSource(traceSource); // |traceStream| can be undefined in the case when we are using the external // HTTP+RPC endpoint and the trace processor instance has already loaded @@ -216,7 +258,7 @@ status += `${Math.round(res.bytesRead / 1e6)} MB`; } status += ` - ${Math.ceil(res.bytesRead / elapsed / 1e6)} MB/s`; - updateStatus(app, status); + console.log(status); if (res.eof) break; } await engine.notifyEof(); @@ -224,188 +266,107 @@ assertTrue(engine instanceof HttpRpcEngine); await engine.restoreInitialTables(); } - - for (const p of await app.sqlPackages()) { - await engine.registerSqlPackages(p); - } - - const traceDetails = await getTraceInfo(engine, app, traceSource); - const trace = new TraceImpl(app, engine, traceDetails); - app.setActiveTrace(trace); - - const hasJsonTrace = traceDetails.traceTypes.includes('json'); - - const visibleTimeSpan = await computeVisibleTime( - traceDetails.start, - traceDetails.end, - hasJsonTrace, - engine, - ); - - const newViewport = HighPrecisionTimeSpan.fromTime( - visibleTimeSpan.start, - visibleTimeSpan.end, - ); - trace.timeline.setVisibleWindow(newViewport); - - const cacheUuid = traceDetails.cached ? traceDetails.uuid : ''; - Router.navigate(`#!/viewer?local_cache_key=${cacheUuid}`); - - // Make sure the helper views are available before we start adding tracks. - await includeSummaryTables(trace); - - await defineMaxLayoutDepthSqlFunction(engine); - - if (serializedAppState !== undefined) { - deserializeAppStatePhase1(serializedAppState, trace); - } - - await app.plugins.onTraceLoad(trace, (id) => { - updateStatus(app, `Running plugin: ${id}`); - }); - - decideTabs(trace); - - updateStatus(app, `Loading minimap`); - await trace.minimap.load(traceDetails.start, traceDetails.end); - - // Trace Processor doesn't support the reliable range feature for JSON - // traces. - if (!hasJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get()) { - const reliableRangeStart = await computeTraceReliableRangeStart(engine); - if (reliableRangeStart > 0) { - trace.notes.addNote({ - timestamp: reliableRangeStart, - color: '#ff0000', - text: 'Reliable Range Start', - }); - } - } - - // notify() will await that all listeners' promises have resolved. - await trace.onTraceReady.notify(); - - if (serializedAppState !== undefined) { - // Wait that plugins have completed their actions and then proceed with - // the final phase of app state restore. - // TODO(primiano): this can probably be removed once we refactor tracks - // to be URI based and can deal with non-existing URIs. - deserializeAppStatePhase2(serializedAppState, trace); - } - - // Execute startup commands as the final step - simulates user actions - // after the trace is fully loaded and any saved state has been restored. - // This ensures startup commands see the complete, final state of the trace. - - // CRITICAL ORDER: URL commands MUST execute before settings commands! - // This ordering has subtle but important implications: - // - URL commands are trace-specific and should establish initial state - // - Settings commands are user preferences that should override URL defaults - // - Changing this order could break trace sharing and user customization - // DO NOT REORDER without understanding the full impact! - const urlCommands = - parseUrlCommands(app.initialRouteArgs.startupCommands) ?? []; - const settingsCommands = app.startupCommandsSetting.get(); - - // Combine URL and settings commands - runtime allowlist checking will handle filtering - const allStartupCommands = [...urlCommands, ...settingsCommands]; - const enforceAllowlist = app.enforceStartupCommandAllowlistSetting.get(); - - if (allStartupCommands.length > 0) { - updateStatus(app, 'Running startup commands'); - using _ = trace.omnibox.disablePrompts(); - - // Execute startup commands in trace context after everything is ready. - // This simulates user actions taken after trace load is complete, - // including any saved app state restoration. At this point: - // - All plugins have loaded and registered their commands - // - Trace data is fully accessible - // - UI state has been restored from any saved workspace - // - Commands can safely query trace data and modify UI state - - // Set allowlist checking during startup if enforcement enabled - if (enforceAllowlist) { - app.commands.setExecutingStartupCommands(true); - } - - try { - for (const command of allStartupCommands) { - try { - // Execute through proxy to access both global and trace-specific - // commands. - await app.commands.runCommand(command.id, ...command.args); - } catch (error) { - // TODO(stevegolton): Add a mechanism to notify users of startup - // command errors. This will involve creating a notification UX - // similar to VSCode where there are popups on the bottom right - // of the UI. - console.warn(`Startup command ${command.id} failed:`, error); - } - } - } finally { - // Always restore default (allow all) behavior when done - app.commands.setExecutingStartupCommands(false); - } - } - - return trace; } -function decideTabs(trace: TraceImpl) { - // Show the list of default tabs, but don't make them active! - for (const tabUri of trace.tabs.defaultTabs) { - trace.tabs.showTab(tabUri); +function createStreamFromSource( + traceSource: TraceSource, +): TraceStream | undefined { + if (traceSource.type === 'FILE') { + return new TraceFileStream(traceSource.file); + } else if (traceSource.type === 'ARRAY_BUFFER') { + return new TraceBufferStream(traceSource.buffer); + } else if (traceSource.type === 'URL') { + return new TraceHttpStream(traceSource.url); + } else if (traceSource.type === 'STREAM') { + return traceSource.stream; + } else if (traceSource.type === 'HTTP_RPC') { + return undefined; + } else if (traceSource.type === 'MULTIPLE_FILES') { + return new TraceMultipleFilesStream(traceSource.files); + } else { + throw new Error(`Unknown source: ${JSON.stringify(traceSource)}`); } } -async function includeSummaryTables(trace: TraceImpl) { - const engine = trace.engine; - updateStatus(trace, 'Creating slice summaries'); - await engine.query(`include perfetto module viz.summary.slices;`); - - updateStatus(trace, 'Creating counter summaries'); - await engine.query(`include perfetto module viz.summary.counters;`); - - updateStatus(trace, 'Creating thread summaries'); - await engine.query(`include perfetto module viz.summary.threads;`); - - updateStatus(trace, 'Creating processes summaries'); - await engine.query(`include perfetto module viz.summary.processes;`); -} - -function updateStatus(traceOrApp: TraceImpl | AppImpl, msg: string): void { - const showUntilDismissed = 0; - traceOrApp.omnibox.showStatusMessage(msg, showUntilDismissed); -} - -async function computeFtraceBounds(engine: Engine): Promise<TimeSpan | null> { - const result = await engine.query(` - SELECT min(ts) as start, max(ts) as end FROM ftrace_event; +// TODO(sashwinbalaji): Move session UUID generation to TraceProcessor. +// getTraceUuid is a temporary measure to ensure multi-trace sessions have a +// unique cache key. This prevents collisions where a multi-trace session (e.g. +// a ZIP) would otherwise reuse the cache entry of its first component trace if +// that trace was previously opened individually. +async function getTraceUuid(engine: Engine): Promise<string> { + // Each trace in the session contributes to the global cache key. To maintain + // stable identifiers, we use the following priority: + // 1. Per-trace UUID: e.g. from a TraceUuid packet. + // 2. Global session UUID: ONLY used if no trace in the entire session has a + // specific UUID. + // 3. Trace ID + Type: e.g. '1-perf'. The last-resort fallback. + const uuidRes = await engine.query(` + INCLUDE PERFETTO MODULE std.traceinfo.trace; + SELECT DISTINCT + coalesce( + trace_uuid, + iif( + (SELECT COUNT(trace_uuid) FROM _metadata_by_trace) = 0, + extract_metadata('trace_uuid'), + NULL + ), + trace_id || '-' || trace_type + ) AS uuid + FROM _metadata_by_trace `); - const {start, end} = result.firstRow({start: LONG_NULL, end: LONG_NULL}); - if (start !== null && end !== null) { - return new TimeSpan(Time.fromRaw(start), Time.fromRaw(end)); + const uuids: string[] = []; + for ( + const itUuid = uuidRes.iter({uuid: STR}); + itUuid.valid(); + itUuid.next() + ) { + uuids.push(itUuid.uuid); } - return null; + + if (uuids.length === 0) return ''; + if (uuids.length === 1) return uuids[0]; + const sortedUuids = [...uuids].sort(); + return await sha1(sortedUuids.join(';')); } -async function computeTraceReliableRangeStart(engine: Engine): Promise<time> { - const result = - await engine.query(`SELECT RUN_METRIC('chrome/chrome_reliable_range.sql'); - SELECT start FROM chrome_reliable_range`); - const bounds = result.firstRow({start: LONG}); - return Time.fromRaw(bounds.start); +async function getTraceTypes(engine: Engine): Promise<string[]> { + const result = await engine.query(` + INCLUDE PERFETTO MODULE std.traceinfo.trace; + SELECT DISTINCT + trace_type AS str_value + FROM _metadata_by_trace + `); + + const traceTypes: string[] = []; + const it = result.iter({str_value: STR}); + for (; it.valid(); it.next()) { + traceTypes.push(it.str_value); + } + return traceTypes; +} + +async function getTraceSpan(engine: Engine): Promise<TimeSpan> { + const result = await engine.query(` + SELECT + start_ts AS startTs, + end_ts AS endTs + FROM trace_bounds + `); + const bounds = result.firstRow({ + startTs: LONG, + endTs: LONG, + }); + return new TimeSpan(Time.fromRaw(bounds.startTs), Time.fromRaw(bounds.endTs)); } async function computeVisibleTime( - traceStart: time, - traceEnd: time, - isJsonTrace: boolean, engine: Engine, + traceSpan: TimeSpan, + isJsonTrace: boolean, ): Promise<TimeSpan> { // initialise visible time to the trace time bounds - let visibleStart = traceStart; - let visibleEnd = traceEnd; + let visibleStart = traceSpan.start; + let visibleEnd = traceSpan.end; // compare start and end with metadata computed by the trace processor const mdTime = await getTracingMetadataTimeBounds(engine); @@ -431,37 +392,81 @@ return new TimeSpan(visibleStart, visibleEnd); } -// TODO(sashwinbalaji): Move session UUID generation to TraceProcessor. -// computeGlobalUuid is a temporary measure to ensure multi-trace sessions -// have a unique cache key. This prevents collisions where a multi-trace -// session (e.g. a ZIP) would otherwise reuse the cache entry of its first -// component trace if that trace was previously opened individually. -async function computeGlobalUuid(uuids: string[]): Promise<string> { - if (uuids.length === 0) return ''; - if (uuids.length === 1) return uuids[0]; - const sortedUuids = [...uuids].sort(); - return await sha1(sortedUuids.join(';')); +async function getTracingMetadataTimeBounds(engine: Engine): Promise<TimeSpan> { + const queryRes = await engine.query(` + SELECT + name, + int_value AS intValue + FROM metadata + WHERE + name = 'tracing_started_ns' OR + name = 'tracing_disabled_ns' OR + name = 'all_data_source_started_ns' + `); + let startBound = Time.MIN; + let endBound = Time.MAX; + const it = queryRes.iter({name: STR, intValue: LONG_NULL}); + for (; it.valid(); it.next()) { + const columnName = it.name; + const timestamp = it.intValue; + if (timestamp === null) continue; + if (columnName === 'tracing_disabled_ns') { + endBound = Time.min(endBound, Time.fromRaw(timestamp)); + } else { + startBound = Time.max(startBound, Time.fromRaw(timestamp)); + } + } + + return new TimeSpan(startBound, endBound); } -async function getTraceInfo( - engine: Engine, - app: AppImpl, - traceSource: TraceSource, -): Promise<TraceInfoImpl> { - const traceTime = await getTraceTimeBounds(engine); +async function computeTraceReliableRangeStart(engine: Engine): Promise<time> { + const result = await engine.query(` + SELECT RUN_METRIC('chrome/chrome_reliable_range.sql'); + SELECT start + FROM chrome_reliable_range + `); + const bounds = result.firstRow({start: LONG}); + return Time.fromRaw(bounds.start); +} +async function computeFtraceBounds(engine: Engine): Promise<TimeSpan | null> { + const result = await engine.query(` + SELECT + MIN(ts) AS start, + MAX(ts) AS end + FROM ftrace_event + `); + const {start, end} = result.firstRow({start: LONG_NULL, end: LONG_NULL}); + if (start !== null && end !== null) { + return new TimeSpan(Time.fromRaw(start), Time.fromRaw(end)); + } + return null; +} + +async function getTzOffset(engine: Engine): Promise<number> { + // The max() is so the query returns NULL if the tz info doesn't exist. + const result = await engine.query(` + SELECT MAX(int_value) AS tzOffMin + FROM metadata + WHERE name = 'timezone_off_mins' + `); + return result.firstRow({tzOffMin: NUM_NULL}).tzOffMin ?? 0; +} + +async function getUnixEpochOffset(engine: Engine): Promise<time> { // Find the first REALTIME or REALTIME_COARSE clock snapshot. // Prioritize REALTIME over REALTIME_COARSE. - const query = `select - ts, - clock_value as clockValue, - clock_name as clockName - from clock_snapshot - where - snapshot_id = 0 AND - clock_name in ('REALTIME', 'REALTIME_COARSE') - `; - const result = await engine.query(query); + const result = await engine.query(` + SELECT + ts, + clock_value AS clockValue, + clock_name AS clockName + FROM clock_snapshot + WHERE + snapshot_id = 0 AND + clock_name IN ('REALTIME', 'REALTIME_COARSE') + `); const it = result.iter({ ts: LONG, clockValue: LONG, @@ -494,17 +499,25 @@ } } - // The max() is so the query returns NULL if the tz info doesn't exist. - const queryTz = `select max(int_value) as tzOffMin from metadata - where name = 'timezone_off_mins'`; - const resTz = await assertExists(engine).query(queryTz); - const tzOffMin = resTz.firstRow({tzOffMin: NUM_NULL}).tzOffMin ?? 0; - // This is the offset between the unix epoch and ts in the ts domain. // I.e. the value of ts at the time of the unix epoch - usually some large // negative value. - const unixOffset = Time.sub(snapshot.ts, snapshot.clockValue); + return Time.sub(snapshot.ts, snapshot.clockValue); +} +async function getHasFtrace(engine: Engine): Promise<boolean> { + const result = await engine.query(` + SELECT * + FROM ftrace_event + LIMIT 1 + `); + return result.numRows() > 0; +} + +function getTraceTitleAndUrl(traceSource: TraceSource): { + traceTitle: string; + traceUrl: string; +} { let traceTitle = ''; let traceUrl = ''; switch (traceSource.type) { @@ -531,117 +544,50 @@ break; } - const traceTypes = await getTraceTypes(engine); + return {traceTitle, traceUrl}; +} - const hasFtrace = - (await engine.query(`select * from ftrace_event limit 1`)).numRows() > 0; - - // Each trace in the session contributes to the global cache key. To maintain - // stable identifiers, we use the following priority: - // 1. Per-trace UUID: e.g. from a TraceUuid packet. - // 2. Global session UUID: ONLY used if no trace in the entire session has a - // specific UUID. - // 3. Trace ID + Type: e.g. '1-perf'. The last-resort fallback. - const uuidRes = await engine.query(` - INCLUDE PERFETTO MODULE std.traceinfo.trace; - SELECT DISTINCT - coalesce( - trace_uuid, - iif( - (SELECT COUNT(trace_uuid) FROM _metadata_by_trace) = 0, - extract_metadata('trace_uuid'), - NULL - ), - trace_id || '-' || trace_type - ) AS uuid - FROM _metadata_by_trace - `); - const uuids: string[] = []; - for ( - const itUuid = uuidRes.iter({uuid: STR}); - itUuid.valid(); - itUuid.next() - ) { - uuids.push(itUuid.uuid); - } - const uuid = await computeGlobalUuid(uuids); - - updateStatus(app, 'Caching trace...'); - const cached = await cacheTrace(traceSource, uuid); - - const downloadable = +function isDownloadable(traceSource: TraceSource): boolean { + return ( (traceSource.type === 'ARRAY_BUFFER' && !traceSource.localOnly) || traceSource.type === 'FILE' || - traceSource.type === 'URL'; - - return { - ...traceTime, - traceTitle, - traceUrl, - tzOffMin, - unixOffset, - importErrors: await getTraceErrors(engine), - source: traceSource, - traceTypes, - hasFtrace, - uuid, - cached, - downloadable, - }; -} - -async function getTraceTypes(engine: Engine): Promise<string[]> { - const result = await engine.query(` - INCLUDE PERFETTO MODULE std.traceinfo.trace; - select distinct trace_type as str_value - from _metadata_by_trace - `); - - const traceTypes: string[] = []; - const it = result.iter({str_value: STR}); - for (; it.valid(); it.next()) { - traceTypes.push(it.str_value); - } - return traceTypes; -} - -async function getTraceTimeBounds(engine: Engine): Promise<TimeSpan> { - const result = await engine.query( - `select start_ts as startTs, end_ts as endTs from trace_bounds`, + traceSource.type === 'URL' ); - const bounds = result.firstRow({ - startTs: LONG, - endTs: LONG, - }); - return new TimeSpan(Time.fromRaw(bounds.startTs), Time.fromRaw(bounds.endTs)); } async function getTraceErrors(engine: Engine): Promise<number> { - const sql = `SELECT sum(value) as errs FROM stats WHERE severity != 'info'`; - const result = await engine.query(sql); + const result = await engine.query(` + SELECT SUM(value) AS errs + FROM stats + WHERE severity != 'info' + `); return result.firstRow({errs: NUM}).errs; } -async function getTracingMetadataTimeBounds(engine: Engine): Promise<TimeSpan> { - const queryRes = await engine.query(`select - name, - int_value as intValue - from metadata - where name = 'tracing_started_ns' or name = 'tracing_disabled_ns' - or name = 'all_data_source_started_ns'`); - let startBound = Time.MIN; - let endBound = Time.MAX; - const it = queryRes.iter({name: STR, intValue: LONG_NULL}); - for (; it.valid(); it.next()) { - const columnName = it.name; - const timestamp = it.intValue; - if (timestamp === null) continue; - if (columnName === 'tracing_disabled_ns') { - endBound = Time.min(endBound, Time.fromRaw(timestamp)); - } else { - startBound = Time.max(startBound, Time.fromRaw(timestamp)); - } - } +async function includeSummaryTables(engine: Engine): Promise<void> { + await engine.query(`INCLUDE PERFETTO MODULE viz.summary.slices;`); + await engine.query(`INCLUDE PERFETTO MODULE viz.summary.counters;`); + await engine.query(`INCLUDE PERFETTO MODULE viz.summary.threads;`); + await engine.query(`INCLUDE PERFETTO MODULE viz.summary.processes;`); +} - return new TimeSpan(startBound, endBound); +// TODO(stevegolton): Move this into some global "SQL extensions" file and +// ensure it's only run once. +async function defineMaxLayoutDepthSqlFunction(engine: Engine): Promise<void> { + await engine.query(` + CREATE PERFETTO FUNCTION __max_layout_depth(track_count INT, track_ids STRING) + RETURNS INT AS + SELECT iif( + $track_count = 1, + ( + SELECT max_depth + FROM _slice_track_summary + WHERE id = CAST($track_ids AS INT) + ), + ( + SELECT MAX(layout_depth) + FROM experimental_slice_layout($track_ids) + ) + ); + `); }
diff --git a/ui/src/core/page_manager.ts b/ui/src/core/page_manager.ts index 3b2ab95..3806cec 100644 --- a/ui/src/core/page_manager.ts +++ b/ui/src/core/page_manager.ts
@@ -45,6 +45,8 @@ renderPageForCurrentRoute(): m.Children { const route = Router.parseFragment(location.hash); + console.log('Rendering page for route', route); + // Log page changes to analytics if (this.currentPage !== route.page) { this.currentPage = route.page;
diff --git a/ui/src/core/router.ts b/ui/src/core/router.ts index 581718e..b4fbe45 100644 --- a/ui/src/core/router.ts +++ b/ui/src/core/router.ts
@@ -49,6 +49,8 @@ args: RouteArgs; } +type RouteHandler = (route: Route, oldRoute?: Route) => void; + // This router does two things: // 1) Maps fragment paths (#!/page/subpage) to Mithril components. // The route map is passed to the ctor and is later used when calling the @@ -74,12 +76,13 @@ // frontend/index.ts calls maybeOpenTraceFromRoute() + redraw here. // This event is decoupled for testing and to avoid circular deps. - onRouteChanged: (route: Route) => void = () => {}; + onRouteChanged?: RouteHandler; constructor() { - window.onhashchange = (e: HashChangeEvent) => this.onHashChange(e); - const route = Router.parseUrl(window.location.href); - this.onRouteChanged(route); + window.onhashchange = (e: HashChangeEvent) => { + console.log('hashchange', e); + m.redraw(); + }; } private onHashChange(e: HashChangeEvent) { @@ -121,7 +124,7 @@ return; } - this.onRouteChanged(newRoute); + this.onRouteChanged?.(newRoute, oldRoute); } static getCurrentRoute(): Route {
diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts index dd35678..b8e4ea3 100644 --- a/ui/src/core/trace_impl.ts +++ b/ui/src/core/trace_impl.ts
@@ -80,6 +80,7 @@ readonly loadingErrors: string[] = []; readonly app: AppImpl; readonly store = createStore<Record<string, unknown>>({}); + readonly pages: PageManager; // Do we need this? readonly pluginSerializableState = createStore<{[key: string]: {}}>({}); @@ -140,47 +141,48 @@ // avoid ending up with duplicate commands. this.commandMgrProxy = createProxy(app.commands, { registerCommand: (cmd: Command) => { - const disposable = app.commands.registerCommand(cmd); - this.trash.use(disposable); - return disposable; + // const disposable = app.commands.registerCommand(cmd); + // this.trash.use(disposable); + // return disposable; }, registerMacro: (macro, source) => { - const disposable = app.commands.registerMacro(macro, source); - this.trash.use(disposable); - return disposable; + // const disposable = app.commands.registerMacro(macro, source); + // this.trash.use(disposable); + // return disposable; }, }); // Likewise, remove all trace-scoped sidebar entries when the trace unloads. this.sidebarProxy = createProxy(app.sidebar, { addMenuItem: (menuItem: SidebarMenuItem) => { - const disposable = app.sidebar.addMenuItem(menuItem); - this.trash.use(disposable); - return disposable; + // const disposable = app.sidebar.addMenuItem(menuItem); + // this.trash.use(disposable); + // return disposable; }, }); this.pageMgrProxy = createProxy(app.pages, { registerPage: (pageHandler: PageHandler) => { - const disposable = app.pages.registerPage(pageHandler); - this.trash.use(disposable); - return disposable; + // const disposable = app.pages.registerPage(pageHandler); + // this.trash.use(disposable); + // return disposable; }, }); + this.pages = new PageManagerImpl(app.analytics); this.settingsProxy = createProxy(app.settings, { register: <T>(setting: SettingDescriptor<T>) => { - const disposable = app.settings.register(setting); - this.trash.use(disposable); - return disposable; + // const disposable = app.settings.register(setting); + // this.trash.use(disposable); + // return disposable; }, }); this.omniboxProxy = createProxy(app.omnibox, { registerMode: (descriptor: OmniboxModeDescriptor) => { - const disposable = app.omnibox.registerMode(descriptor); - this.trash.use(disposable); - return disposable; + // const disposable = app.omnibox.registerMode(descriptor); + // this.trash.use(disposable); + // return disposable; }, }); } @@ -266,10 +268,6 @@ return this.sidebarProxy; } - get pages(): PageManager { - return this.pageMgrProxy; - } - get omnibox(): OmniboxManagerImpl { return this.omniboxProxy; }
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts index 42aeae2..083aa54 100644 --- a/ui/src/frontend/index.ts +++ b/ui/src/frontend/index.ts
@@ -78,7 +78,7 @@ // ├─► main() ───────────────────────────────────────────────────────┐ // │ ├─ Setup CSP │ // │ ├─ Init settings & app │ -// │ ├─ Start CSS load (async) ──────┐ │ +// │ ├─ Start CSS load (async) ───────┐ │ // │ ├─ Setup error handlers │ │ // │ └─ Register window.onload ───────┼──────────┐ │ // │ │ │ │ @@ -127,6 +127,29 @@ } }); maybeOpenTraceFromRoute(route); + + // What we want to do here is to check if the hash route has actually changed + // and if it has then schedule a load of that trace. + + // The only problem is that we might be already loading this trace right now + // if the user changes pages mid-load, we just haven't loaded the uuid into + // the URL bar yet. + + // So we need to be very careful to avoid queueing up another load of the same + // trace which will start immediately after the first one finishes and cause a + // reload of the same trace. + + // We could just flat out refuse to load a trace while another one is loading, + // but that isn't very freiendly. + + // Ideally, whenever the uuid in the hash changes, we just chuck away the previously loading trace (whatever it was) and start loading a new one immeidately. Then, when the trace is loaded enough to know its uuid, we put that in the url. + + // if there was no url in the bar last time then we'll reload the trace, that's the only problem. + + // The only thing I can thnink of is to reserve a special cache entry in the url bar for the pending trace. So when we want to load a trace we put the trace content (trace source) in a special reserved 'cache' and put local_cache_key=pending in the URL. + // Then, when the url bar chanegs and this function is called, we look in that special area and react to it by starting to load the trace. + // Then when the trace has loaded enough to find out the uuid, we put that in the url bar and clear the pending cache entry. We see that change, but this code will chech against the current UUID of the current trace, see it's the same and ignore the change. + // What happens if we start loading multiple pending traces? We can just see the fact that there is already a pending trace and refuse to start loading another one until the first one has finished loading. } function setupContentSecurityPolicy() { @@ -195,7 +218,60 @@ setupContentSecurityPolicy(); initAssets(); - // Create settings Manager + initAppObject(); + + // Secure a reference to the app instance. + const app = AppImpl.instance; + + // Load the css. The load is asynchronous and the CSS is not ready by the time + // appendChild returns. + const cssLoadPromise = loadCss(); + + // Fonts can take a long time to load, and we want to avoid showing the UI + // with fallback fonts. To avoid that, we add a 'pf-fonts-loading' class to + // the body until either the fonts are ready or 15s have passed (whichever + // happens first). + setFontsLoadingClass(); + + // Load the script to detect if this is a Googler (see comments on globals.ts). + // This registers macros, SQL packages, and proto descriptors. + tryLoadIsInternalUserScript(app); + + // Route errors to both the UI bugreport dialog and Analytics (if enabled). + addErrorHandler(maybeShowErrorDialog); + addErrorHandler((e) => app.analytics.logError(e)); + + // Add Error handlers for JS error and for uncaught exceptions in promises. + window.addEventListener('error', (e) => reportError(e)); + window.addEventListener('unhandledrejection', (e) => reportError(e)); + + // Put debug variables in the global scope for better debugging. + registerDebugGlobals(); + + // Prevent pinch zoom. + document.body.addEventListener( + 'wheel', + (e: MouseEvent) => { + if (e.ctrlKey) e.preventDefault(); + }, + {passive: false}, + ); + + cssLoadPromise.then(() => onCssLoaded(app)); + + (window as {} as IdleDetectorWindow).waitForPerfettoIdle = (ms?: number) => { + return new IdleDetector().waitForPerfettoIdle(ms); + }; + + // Keep at the end. Potentially it calls into the next stage (onWindowLoaded). + if (document.readyState === 'complete') { + onWindowLoaded(app); + } else { + window.addEventListener('load', () => onWindowLoaded(app)); + } +} + +function initAppObject() { const settingsManager = new SettingsManagerImpl( new LocalStorage(PERFETTO_SETTINGS_STORAGE_KEY), ); @@ -297,9 +373,9 @@ startupCommandsSetting, enforceStartupCommandAllowlistSetting, }); +} - // Load the css. The load is asynchronous and the CSS is not ready by the time - // appendChild returns. +function loadCss() { const cssLoadPromise = defer<void>(); const css = document.createElement('link'); css.rel = 'stylesheet'; @@ -310,50 +386,15 @@ if (favicon instanceof HTMLLinkElement) { favicon.href = assetSrc('assets/favicon.png'); } - document.body.classList.add('pf-fonts-loading'); document.head.append(css); + return cssLoadPromise; +} +function setFontsLoadingClass() { + document.body.classList.add('pf-fonts-loading'); Promise.race([document.fonts.ready, sleepMs(15000)]).then(() => { document.body.classList.remove('pf-fonts-loading'); }); - - // Load the script to detect if this is a Googler (see comments on globals.ts). - // This registers macros, SQL packages, and proto descriptors. - const app = AppImpl.instance; - tryLoadIsInternalUserScript(app); - - // Route errors to both the UI bugreport dialog and Analytics (if enabled). - addErrorHandler(maybeShowErrorDialog); - addErrorHandler((e) => app.analytics.logError(e)); - - // Add Error handlers for JS error and for uncaught exceptions in promises. - window.addEventListener('error', (e) => reportError(e)); - window.addEventListener('unhandledrejection', (e) => reportError(e)); - - // Put debug variables in the global scope for better debugging. - registerDebugGlobals(); - - // Prevent pinch zoom. - document.body.addEventListener( - 'wheel', - (e: MouseEvent) => { - if (e.ctrlKey) e.preventDefault(); - }, - {passive: false}, - ); - - cssLoadPromise.then(() => onCssLoaded(app)); - - (window as {} as IdleDetectorWindow).waitForPerfettoIdle = (ms?: number) => { - return new IdleDetector().waitForPerfettoIdle(ms); - }; - - // Keep at the end. Potentially it calls into the next stage (onWindowLoaded). - if (document.readyState === 'complete') { - onWindowLoaded(); - } else { - window.addEventListener('load', () => onWindowLoaded()); - } } function onCssLoaded(app: AppImpl) { @@ -488,9 +529,9 @@ // This function is called only later after all the sub-resources (fonts, // images) have been loaded. -function onWindowLoaded() { +function onWindowLoaded(app: AppImpl) { // These two functions cause large network fetches and are not load bearing. - AppImpl.instance.serviceWorkerController.install(); + app.serviceWorkerController.install(); warmupWasmWorker(); }
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts index 5252df7..3521df2 100644 --- a/ui/src/frontend/ui_main.ts +++ b/ui/src/frontend/ui_main.ts
@@ -23,6 +23,10 @@ import {renderStatusBar} from './statusbar'; import {taskTracker} from './task_tracker'; import {Topbar} from './topbar'; +import {Router} from '../core/router'; +import {Gate} from '../base/mithril_utils'; +import {RouteArgs} from '../public/route_schema'; +import {HomePage} from './home_page'; const showStatusBarFlag = featureFlags.register({ id: 'Enable status bar', @@ -33,6 +37,19 @@ // via --title (e.g. to distinguish multiple dev server instances). const APP_TITLE = document.title || 'Perfetto UI'; +interface RouteAttrs { + readonly route: string; + readonly content: (subpage: string, args: RouteArgs) => m.Children; +} + +const Route: m.Component<RouteAttrs> = { + view({attrs}: m.CVnode<RouteAttrs>): m.Children { + const route = Router.parseFragment(location.hash); + const content = attrs.content(route.subpage, route.args); + return m(Gate, {open: route.page === attrs.route}, content); + }, +}; + // This components gets destroyed and recreated every time the current trace // changes. Note that in the beginning the current trace is undefined. export class UiMain implements m.ClassComponent { @@ -66,7 +83,7 @@ className: 'pf-ui-main__loading', state: isSomethingLoading ? 'indeterminate' : 'none', }), - m('.pf-ui-main__page-container', app.pages.renderPageForCurrentRoute()), + m('.pf-ui-main__page-container'), m(CookieConsent), maybeRenderFullscreenModalDialog(), showStatusBarFlag.get() && renderStatusBar(app),