Skip to content

Commit ca3afcc

Browse files
cdamusjfaltermeiermartin-fleck-atwpaul-samsungdfriederich
committed
Support for hosting Perfetto in a Theia Electron application
- enable generating .d.ts files. Some types had to be exported that previously weren't in order for TypeScript to be able to correctly resolve its generated type definitions - prevent loading sidebar and other unnecessary parts of UI - add other global flags to externally control Perfetto functionality - support loading the trace database from a stream provided by the host application (such as from a remote Theia backend) - optionally copy the Perfetto CSS and WASM Engine in the build for bundling in an Electron application - adjust routing to work with applications that use hashes already (such as in Theia for for opening workspaces) - introduce routing that uses url search params and keeps existing values for the hash that are not params - if a new route has no page/subpage at all, don't reacting to the route change - allow unsafe inline CSS via configuration option (required for Theia) - allow to control cache keys because chromium only allows to cache http-family protocols. In Electron we have file:// however, so we may use a prefix to fake this. - allow file CORS origins for Electron-based applications - ensure we preserve empty strings in URL creation - support opening multiple traces in same Electron application by making the port customizable on a per-AppImpl basis (using the window URL is impractical in a Theia application) - support multiple instantiation of Perfetto. Add an optional build step to copy the transpiled code into the package so that it can be installed multiple times in an application without sharing one copy via symlink (and also because Theia's yarn build breaks the symlink when copying the package into the node_modules/ directory) - make @types/mithril a dev dependency - remove unused dependencies from package.json (#65) - explicitly define which location TS @types should be read from. Otherwise the tsc compiler possibly finds the types in a parent's folder node_modules/@types directory, which then causes build failures in the host Theia/Electron application, as those files are not ignored like the local node_modules is - support opening Perfetto pages such as the Trace Info page in separate subtrees of the DOM (in a Theia application, in different widget-tabs) via a new optional ViewOpener configured in the AppImpl. Employ this view opener in the Errors (info) button in the topbar - Scope key handling to the viewer page so that it doesn't get key events from other UI elements in the host Theia/Electron application - current script is null also in a Theia application, not just tests - fix macro redef warning in trace processor build for macOS 15.4 SDK - fill holes in painting the overview timeline panel (evident in Theia colour themes) - allow embedding of the Perfetto UI via an explicit CSS class on its main element in addition to the default DOM nesting position - implement a means to suppress the built-in handling of trace file drop - add to the context a prop suppressing the dialog that prompts to open a trace already loaded in the trace_processor_shell, for applications that manage this externally - provide API to remove all registered error handlers. An embedding application often has its own error handling so provide an API for such application to remove all registered error handlers and then it can supply its own - return showHelp's showModal promise. This allows a host application to track whether the help dialog is (still) showing Co-authored-by: Johannes Faltermeier <jfaltermeier@eclipsesource.com> Co-authored-by: Martin Fleck <mfleck@eclipsesource.com> Co-authored-by: Warren Paul <w.paul@samsung.com> Co-authored-by: dfriederich <109323095+dfriederich@users.noreply.github.com> Co-authored-by: Andrew Levans <121060410+ALevansSamsung@users.noreply.github.com> Signed-off-by: Christian W. Damus <cdamus.ext@eclipsesource.com>
1 parent 6d3ea2e commit ca3afcc

File tree

137 files changed

+1443
-1033
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

137 files changed

+1443
-1033
lines changed

ui/src/base/adapter.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (C) 2025 The Android Open Source Project
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/** A function that can _adapt_ some object to another type. */
16+
export type Adapter<T extends object> = (obj: object) => T | undefined;
17+
18+
/**
19+
* An adapter can adapt objects to types identified by adapter keys,
20+
* being either
21+
* - a class for object types, or
22+
* - a symbol for interface types
23+
*/
24+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25+
export type AdapterKey = symbol | ClassType<any>;
26+
27+
export type ClassType<T extends object> = Function & { prototype: T };
28+
29+
class AdapterRegistry {
30+
private readonly adapters = new Map<AdapterKey, Adapter<object>[]>();
31+
32+
/** Register an adapter for some class type. */
33+
register<T extends object>(adapter: Adapter<T>, forType: ClassType<T>): this;
34+
/** Register an adapter for some interface type identified by a symbol. */
35+
register<T extends object>(adapter: Adapter<T>, forType: symbol): this;
36+
register<T extends object>(adapter: Adapter<T>, forKey: AdapterKey): this {
37+
let adapters = this.adapters.get(forKey);
38+
if (adapters === undefined) {
39+
adapters = [];
40+
this.adapters.set(forKey, adapters);
41+
}
42+
adapters.push(adapter);
43+
return this;
44+
}
45+
46+
/** Adapt the given object to some class type. */
47+
adapt<T extends object>(obj: object, toType: ClassType<T>): T | undefined;
48+
/** Adapt the given object to some interface type identified by a symbol. */
49+
adapt<T extends object>(obj: object, toType: symbol): T | undefined;
50+
adapt<T extends object>(obj: object, toKey: AdapterKey): T | undefined {
51+
const adapters = this.adapters.get(toKey) ?? [];
52+
if (typeof toKey === 'symbol') {
53+
for (const adapter of adapters) {
54+
const result = adapter(obj);
55+
if (result !== undefined) {
56+
return result as T;
57+
}
58+
}
59+
} else {
60+
for (const adapter of adapters) {
61+
const result = adapter(obj);
62+
if (result instanceof toKey) {
63+
return result as T;
64+
}
65+
}
66+
}
67+
return undefined;
68+
}
69+
}
70+
71+
export const adapterRegistry = new AdapterRegistry();
72+
73+
/** Adapt the given object to some class type. */
74+
export function adapt<T extends object>(obj: object, toType: ClassType<T>): T | undefined;
75+
/** Adapt the given object to some interface type identified by a symbol. */
76+
export function adapt<T extends object>(obj: object, toType: symbol): T | undefined;
77+
export function adapt<T extends object>(obj: object, toKey: AdapterKey): T | undefined {
78+
return (typeof toKey === 'symbol') ?
79+
adapterRegistry.adapt(obj, toKey) :
80+
adapterRegistry.adapt(obj, toKey);
81+
}

ui/src/base/assets.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ let rootUrl = '';
1919
/**
2020
* This function must be called once while bootstrapping in a direct script
2121
* context (i.e. not a promise or callback). Typically frontend/index.ts.
22+
* @param hostApplicationRelative relative path of the app directory when integrated
23+
* in some host application
2224
*/
23-
export function initAssets() {
24-
rootUrl = getServingRoot();
25+
export function initAssets(hostApplicationRelative?: string) {
26+
rootUrl = getServingRoot(hostApplicationRelative);
2527
}
2628

2729
/**

ui/src/base/http_utils.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import {IntegrationContext} from '../core/integration_context';
1615
import {assertTrue} from './logging';
1716

1817
export function fetchWithTimeout(
@@ -72,17 +71,19 @@ export function fetchWithProgress(
7271
* NOTE: this function can only be called from synchronous contexts. It will
7372
* fail if called in timer handlers or async continuations (e.g. after an await)
7473
* Use assetSrc(relPath) which caches it on startup.
74+
* @param hostApplicationRelative relative path of the app directory when integrated
75+
* in some host application
7576
* @returns the directory where the app is served from, e.g. 'v46.0-a2082649b'
7677
*/
77-
export function getServingRoot() {
78+
export function getServingRoot(hostApplicationRelative?: string) {
7879
// Works out the root directory where the content should be served from
7980
// e.g. `http://origin/v1.2.3/`.
8081
const script = document.currentScript as HTMLScriptElement;
8182

8283
if (script === null) {
8384
// Can be null in tests or embedding application.
84-
assertTrue(typeof jest !== 'undefined' || IntegrationContext.instance !== undefined);
85-
return '';
85+
assertTrue(typeof jest !== 'undefined' || hostApplicationRelative !== undefined);
86+
return hostApplicationRelative ?? '';
8687
}
8788

8889
let root = script.src;

ui/src/base/logging.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import {App} from '../public/app';
1516
import {VERSION} from '../gen/perfetto_version';
1617
import {exists} from './utils';
1718

@@ -21,6 +22,7 @@ export interface ErrorStackEntry {
2122
location: string; // e.g. frontend_bundle.js:12:3
2223
}
2324
export interface ErrorDetails {
25+
app: App;
2426
errType: ErrorType;
2527
message: string; // Uncaught StoreError: No such subtree: tracks,1374,state
2628
stack: ErrorStackEntry[];

ui/src/components/aggregation_panel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export class AggregationPanel
131131
return m(
132132
'.time-range',
133133
'Selected range: ',
134-
m(DurationWidget, {dur: duration}),
134+
m(DurationWidget, {trace: this.trace, dur: duration}),
135135
);
136136
}
137137

ui/src/components/details/add_ephemeral_tab.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,15 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414
import {uuidv4} from '../../base/uuid';
15-
import {AppImpl} from '../../core/app_impl';
1615
import {Tab} from '../../public/tab';
16+
import {Trace} from '../..//public/trace';
1717

18-
// TODO(primiano): this method should take a Trace parameter (or probably
19-
// shouldn't exist at all in favour of some helper in the Trace object).
20-
export function addEphemeralTab(uriPrefix: string, tab: Tab): void {
18+
// TODO(primiano): this method probably shouldn't exist at all in favour
19+
// of some helper in the Trace object.
20+
export function addEphemeralTab(trace: Trace, uriPrefix: string, tab: Tab): void {
2121
const uri = `${uriPrefix}#${uuidv4()}`;
2222

23-
const tabManager = AppImpl.instance.trace?.tabs;
24-
if (tabManager === undefined) return;
23+
const tabManager = trace.tabs;
2524
tabManager.registerTab({
2625
uri,
2726
content: tab,

ui/src/components/details/slice_details.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,33 +79,34 @@ export function renderDetails(
7979
}),
8080
m(TreeNode, {
8181
left: 'Start time',
82-
right: m(Timestamp, {ts: slice.ts}),
82+
right: m(Timestamp, {trace, ts: slice.ts}),
8383
}),
8484
exists(slice.absTime) &&
8585
m(TreeNode, {left: 'Absolute Time', right: slice.absTime}),
8686
m(
8787
TreeNode,
8888
{
8989
left: 'Duration',
90-
right: m(DurationWidget, {dur: slice.dur}),
90+
right: m(DurationWidget, {trace, dur: slice.dur}),
9191
},
9292
exists(durationBreakdown) &&
9393
slice.dur > 0 &&
9494
m(BreakdownByThreadStateTreeNode, {
95+
trace,
9596
data: durationBreakdown,
9697
dur: slice.dur,
9798
}),
9899
),
99-
renderThreadDuration(slice),
100+
renderThreadDuration(slice, trace),
100101
slice.thread &&
101102
m(TreeNode, {
102103
left: 'Thread',
103-
right: renderThreadRef(slice.thread),
104+
right: renderThreadRef(trace, slice.thread),
104105
}),
105106
slice.process &&
106107
m(TreeNode, {
107108
left: 'Process',
108-
right: renderProcessRef(slice.process),
109+
right: renderProcessRef(trace, slice.process),
109110
}),
110111
slice.process &&
111112
exists(slice.process.uid) &&
@@ -133,7 +134,7 @@ export function renderDetails(
133134
);
134135
}
135136

136-
function renderThreadDuration(sliceInfo: SliceDetails) {
137+
function renderThreadDuration(sliceInfo: SliceDetails, trace: Trace) {
137138
if (exists(sliceInfo.threadTs) && exists(sliceInfo.threadDur)) {
138139
// If we have valid thread duration, also display a percentage of
139140
// |threadDur| compared to |dur|.
@@ -143,7 +144,7 @@ function renderThreadDuration(sliceInfo: SliceDetails) {
143144
return m(TreeNode, {
144145
left: 'Thread duration',
145146
right: [
146-
m(DurationWidget, {dur: sliceInfo.threadDur}),
147+
m(DurationWidget, {trace, dur: sliceInfo.threadDur}),
147148
threadDurFractionSuffix,
148149
],
149150
});

ui/src/components/details/sql_table_tab.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function addLegacyTableTab(
5757
}
5858

5959
function addSqlTableTabWithState(state: SqlTableState) {
60-
addEphemeralTab('sqlTable', new LegacySqlTableTab(state));
60+
addEphemeralTab(state.trace, 'sqlTable', new LegacySqlTableTab(state));
6161
}
6262

6363
class LegacySqlTableTab implements Tab {
@@ -163,7 +163,7 @@ class LegacySqlTableTab implements Tab {
163163
...chartAttrs,
164164
},
165165
],
166-
addChart: (chart) => addChartTab(chart),
166+
addChart: (chart) => addChartTab(this.state.trace, chart),
167167
}),
168168
m(MenuItem, {
169169
label: 'Pivot',
@@ -251,11 +251,13 @@ class LegacySqlTableTab implements Tab {
251251
),
252252
this.selected.kind === 'table' &&
253253
m(SqlTable, {
254+
trace: this.state.trace,
254255
state: this.selected.state,
255256
addColumnMenuItems: this.tableMenuItems.bind(this),
256257
}),
257258
this.selected.kind === 'pivot' &&
258259
m(PivotTable, {
260+
trace: this.state.trace,
259261
state: this.selected.state,
260262
extraRowButton: (node) =>
261263
// Do not show any buttons for root as it doesn't have any filters anyway.

ui/src/components/details/thread_slice_details_tab.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
255255
}
256256

257257
private renderRhs(trace: Trace, slice: SliceDetails): m.Children {
258-
const precFlows = this.renderPrecedingFlows(slice);
259-
const followingFlows = this.renderFollowingFlows(slice);
258+
const precFlows = this.renderPrecedingFlows(trace, slice);
259+
const followingFlows = this.renderFollowingFlows(trace, slice);
260260
const args =
261261
hasArgs(slice.args) &&
262262
m(
@@ -272,7 +272,7 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
272272
}
273273
}
274274

275-
private renderPrecedingFlows(slice: SliceDetails): m.Children {
275+
private renderPrecedingFlows(trace: Trace, slice: SliceDetails): m.Children {
276276
const flows = this.trace.flows.connectedFlows;
277277
const inFlows = flows.filter(({end}) => end.sliceId === slice.id);
278278

@@ -290,6 +290,7 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
290290
title: 'Slice',
291291
render: (flow: Flow) =>
292292
m(SliceRef, {
293+
trace,
293294
id: asSliceSqlId(flow.begin.sliceId),
294295
name:
295296
flow.begin.sliceChromeCustomName ?? flow.begin.sliceName,
@@ -299,6 +300,7 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
299300
title: 'Delay',
300301
render: (flow: Flow) =>
301302
m(DurationWidget, {
303+
trace,
302304
dur: flow.end.sliceStartTs - flow.begin.sliceEndTs,
303305
}),
304306
},
@@ -316,7 +318,7 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
316318
}
317319
}
318320

319-
private renderFollowingFlows(slice: SliceDetails): m.Children {
321+
private renderFollowingFlows(trace: Trace, slice: SliceDetails): m.Children {
320322
const flows = this.trace.flows.connectedFlows;
321323
const outFlows = flows.filter(({begin}) => begin.sliceId === slice.id);
322324

@@ -334,6 +336,7 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
334336
title: 'Slice',
335337
render: (flow: Flow) =>
336338
m(SliceRef, {
339+
trace,
337340
id: asSliceSqlId(flow.end.sliceId),
338341
name: flow.end.sliceChromeCustomName ?? flow.end.sliceName,
339342
}),
@@ -342,6 +345,7 @@ export class ThreadSliceDetailsPanel implements TrackEventDetailsPanel {
342345
title: 'Delay',
343346
render: (flow: Flow) =>
344347
m(DurationWidget, {
348+
trace,
345349
dur: flow.end.sliceStartTs - flow.begin.sliceEndTs,
346350
}),
347351
},

ui/src/components/details/thread_state.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import m from 'mithril';
1616
import {duration, TimeSpan} from '../../base/time';
17+
import {Trace} from '../../public/trace';
1718
import {Engine} from '../../trace_processor/engine';
1819
import {
1920
LONG,
@@ -116,30 +117,31 @@ export async function breakDownIntervalByThreadState(
116117
};
117118
}
118119

119-
function renderChildren(node: Node, totalDur: duration): m.Child[] {
120+
function renderChildren(trace: Trace, node: Node, totalDur: duration): m.Child[] {
120121
const res = Array.from(node.children.entries()).map(([name, child]) =>
121-
renderNode(child, name, totalDur),
122+
renderNode(trace, child, name, totalDur),
122123
);
123124
return res;
124125
}
125126

126-
function renderNode(node: Node, name: string, totalDur: duration): m.Child {
127+
function renderNode(trace: Trace, node: Node, name: string, totalDur: duration): m.Child {
127128
const durPercent = (100 * Number(node.dur)) / Number(totalDur);
128129
return m(
129130
TreeNode,
130131
{
131132
left: name,
132133
right: [
133-
m(DurationWidget, {dur: node.dur}),
134+
m(DurationWidget, {trace, dur: node.dur}),
134135
` (${durPercent.toFixed(2)}%)`,
135136
],
136137
startsCollapsed: node.startsCollapsed,
137138
},
138-
renderChildren(node, totalDur),
139+
renderChildren(trace, node, totalDur),
139140
);
140141
}
141142

142143
interface BreakdownByThreadStateTreeNodeAttrs {
144+
trace: Trace,
143145
dur: duration;
144146
data: BreakdownByThreadState;
145147
}
@@ -149,6 +151,6 @@ export class BreakdownByThreadStateTreeNode
149151
implements m.ClassComponent<BreakdownByThreadStateTreeNodeAttrs>
150152
{
151153
view({attrs}: m.Vnode<BreakdownByThreadStateTreeNodeAttrs>): m.Child[] {
152-
return renderChildren(attrs.data.root, attrs.dur);
154+
return renderChildren(attrs.trace, attrs.data.root, attrs.dur);
153155
}
154156
}

0 commit comments

Comments
 (0)