From 52bb24f7ce07d5a7f4ea07285471a402fea5e561 Mon Sep 17 00:00:00 2001 From: gignat Date: Tue, 6 Oct 2026 07:54:59 +0000 Subject: [PATCH 1/6] ui: build BreakdownTracks counters with INTERVAL FLATTEN Replace the interval_self_intersect! segments table (one row per atomic segment and live interval, joined back on id) with one INTERVAL FLATTEN table per hierarchy level, partitioned by that level's breakdown columns. A node's counter is then a filter on its level's table; FLATTEN emits nothing over uncovered time, so the per-node query appends a 0 at the end of each run of segments to make the counter drop there. Intervals with dur = 0 are dropped (dur > 0 instead of >= 0): the macro counted them as 0, FLATTEN would count them over a zero-width segment. With that filter the counter step functions are identical to before. Measured on android_startup_real's slice table with 5 breakdown columns (314k rows): 32.5 s / 35.5M rows for the macro vs 0.6 s / 0.94M rows for the six FLATTEN tables. Enabled with PERFETTO PRAGMA pipelines = 1 at the start of the table-building batch. --- ui/src/components/tracks/breakdown_tracks.ts | 129 +++++++++++-------- 1 file changed, 72 insertions(+), 57 deletions(-) diff --git a/ui/src/components/tracks/breakdown_tracks.ts b/ui/src/components/tracks/breakdown_tracks.ts index c06aec42a10..3430e061254 100644 --- a/ui/src/components/tracks/breakdown_tracks.ts +++ b/ui/src/components/tracks/breakdown_tracks.ts @@ -162,10 +162,10 @@ interface Filter { * slice columns -> slice tracks * pivot columns -> slice tracks (sourced via an optional join) * - * All counters are driven by ONE shared `interval_self_intersect` segments - * table built once per instance, so the expensive overlap computation is paid - * a single time instead of once per node. The per-node counter query is then a - * cheap GROUP BY over those segments, and the counter renderers are lazy + * All counters are driven by per-level `INTERVAL FLATTEN` segment tables built + * once per instance, so the expensive overlap computation is paid once per + * hierarchy level instead of once per node. The per-node counter query is then + * a cheap filter over its level's segments, and the counter renderers are lazy * (CounterTrack.create), so the work scales with what the user views rather * than with the size of the hierarchy. */ @@ -180,14 +180,14 @@ export class BreakdownTracks { private readonly sliceJoinClause: string; private readonly pivotJoinClause: string; - // The three tables built once per BreakdownTracks instance (see buildTables); + // The tables built once per BreakdownTracks instance (see buildTables); // each name gets a UUID suffix so independent grids — e.g. the binder server // and client trees — never collide on a table name: // intervals one row per source interval (no joins). - // segments interval_self_intersect output: the atomic overlap segments. + // segments one per hierarchy level: INTERVAL FLATTEN PER k0..k(i-1). // projected source rows + slice/pivot joins (may fan out 1:N) + ts/dur. private readonly intervalsTableName: string; - private readonly segmentsTableName: string; + private readonly segmentsTableNames: readonly string[]; private readonly projectedTableName: string; // The breakdown / slice / pivot columns the caller supplies are arbitrary SQL @@ -216,17 +216,21 @@ export class BreakdownTracks { const unique = uuidv4().replace(/-/g, '_'); this.intervalsTableName = `_breakdown_intervals_${unique}`; - this.segmentsTableName = `_breakdown_segments_${unique}`; this.projectedTableName = `_breakdown_projected_${unique}`; this.aggColNames = props.aggregation.columns.map((_, i) => `k${i}`); this.sliceColNames = (props.slice?.columns ?? []).map((_, i) => `s${i}`); this.pivotColNames = (props.pivots?.columns ?? []).map((_, i) => `p${i}`); - this.modulesClause = [ - ...(props.modules ?? []).map((m) => `INCLUDE PERFETTO MODULE ${m};`), - 'INCLUDE PERFETTO MODULE intervals.intersect;', - ].join('\n'); + // Level i is partitioned by the first i breakdown columns; level 0 is the + // root (no partition). + this.segmentsTableNames = [...Array(this.aggColNames.length + 1)].map( + (_, i) => `_breakdown_segments_${unique}_${i}`, + ); + + this.modulesClause = (props.modules ?? []) + .map((m) => `INCLUDE PERFETTO MODULE ${m};`) + .join('\n'); this.sliceJoinClause = props.slice?.joins ? this.getJoinClause(props.slice.joins) @@ -236,33 +240,35 @@ export class BreakdownTracks { : ''; } - // The aggregate evaluated over each atomic segment's active (non-end-marker) - // intervals. COUNT counts them; MAX/SUM reduce the denormalized agg_value. - // End-markers map to NULL / 0 so they contribute nothing and the counter - // naturally drops where intervals end. MAX maps end-markers to NULL (rather - // than a sentinel like 0) precisely because SQL aggregates skip NULL: a - // numeric sentinel could wrongly win the MAX when every active value is lower - // (e.g. all negative). - private aggValueExpr(): string { + // The INTERVAL FLATTEN aggregate evaluated over each atomic segment's active + // intervals. + private aggExpr(): string { switch (this.props.aggregationType) { case BreakdownTrackAggType.MAX: - return 'MAX(IIF(interval_ends_at_ts = FALSE, agg_value, NULL))'; + return 'MAX(agg_value)'; case BreakdownTrackAggType.SUM: - return 'SUM(IIF(interval_ends_at_ts = FALSE, agg_value, 0))'; + return 'SUM(agg_value)'; case BreakdownTrackAggType.COUNT: - return 'SUM(IIF(interval_ends_at_ts = FALSE, 1, 0))'; + return 'COUNT(*)'; } assertUnreachable(this.props.aggregationType); } - private getAggregationQuery(filtersClause: string): string { - // One row per atomic segment, aggregating that segment's active intervals. - // Filters are raw equality on the pre-projected k* columns. + private getAggregationQuery(filtersClause: string, level: number): string { + // One row per atomic segment of this node, read from its level's segments + // table; filters are raw equality on the pre-projected k* columns. FLATTEN + // emits nothing where no interval is active, so a 0 is added at the end of + // every run of segments to make the counter drop there. return ` - SELECT MIN(ts) AS ts, ${this.aggValueExpr()} AS value - FROM ${this.segmentsTableName} - ${filtersClause} - GROUP BY group_id + WITH s AS ( + SELECT ts, dur, value, LEAD(ts) OVER (ORDER BY ts) AS next_ts + FROM ${this.segmentsTableNames[level]} + ${filtersClause} + ) + SELECT ts, value FROM s + UNION ALL + SELECT ts + dur AS ts, 0 AS value FROM s + WHERE next_ts IS NULL OR next_ts > ts + dur ORDER BY ts `; } @@ -297,14 +303,13 @@ export class BreakdownTracks { ]; } - // Builds the three tables that drive every track: + // Builds the tables that drive every track: // _breakdown_intervals: one row per source interval (id, ts, dur, the // breakdown columns, [agg_value]) from the aggregation table ONLY — no // slice/pivot joins, so the overlap count is never inflated by a 1:N join. - // _breakdown_segments: per-(atomic segment, active interval) rows from - // interval_self_intersect over the intervals, with the breakdown columns - // and agg_value inline so per-node counter queries are a plain GROUP BY - // with no JOIN back. + // _breakdown_segments_: INTERVAL FLATTEN of the intervals partitioned by + // the first i breakdown columns: one row per atomic segment with its + // aggregate, so per-node counter queries are a plain filter on level i. // _breakdown_projected: one row per source row WITH the slice/pivot joins // applied (so it may fan out 1:N) plus the slice/pivot ts/dur. Drives the // hierarchy enumeration and the slice/pivot tracks. @@ -323,19 +328,28 @@ export class BreakdownTracks { ...(hasValue ? [`${agg.valueCol} AS agg_value`] : []), ].join(', '); - const denormCols = [ - ...this.aggColNames.map((n) => `i.${n}`), - ...(hasValue ? ['i.agg_value'] : []), - ].join(', '); - // Drop intervals that can't carry a count: a NULL id (e.g. binder_reply_id - // on oneway transactions) or a negative dur (dur = -1 marks an incomplete - // slice). ROW_NUMBER ids are never NULL, so the id check is only emitted - // when a real id column was supplied. + // on oneway transactions), a negative dur (dur = -1 marks an incomplete + // slice) or a zero dur (an instant, which FLATTEN would otherwise count + // over a zero-width segment). ROW_NUMBER ids are never NULL, so the id + // check is only emitted when a real id column was supplied. const idCheck = this.props.sliceIdColumn ? `${this.props.sliceIdColumn} IS NOT NULL AND ` : ''; + const segmentsTables = this.segmentsTableNames + .map((name, level) => { + const per = + level === 0 + ? '' + : `PER ${this.aggColNames.slice(0, level).join(', ')} `; + return ` + CREATE PERFETTO TABLE ${name} AS + FROM ${this.intervalsTableName} + |> INTERVAL FLATTEN ${per}AGGREGATE ${this.aggExpr()} AS value;`; + }) + .join('\n'); + // The projection applies the slice/pivot joins, so a plain id column would // be ambiguous if a join table shares the name — e.g. the binder breakdown // tables also carry binder_reply_id, which the client perspective uses as @@ -354,17 +368,14 @@ export class BreakdownTracks { ].join(', '); await this.props.trace.engine.query(` + ${this.modulesClause} + PERFETTO PRAGMA pipelines = 1; + CREATE PERFETTO TABLE ${this.intervalsTableName} AS SELECT ${intervalCols} FROM ${agg.tableName} - WHERE ${idCheck}${aggTs} IS NOT NULL AND ${aggDur} >= 0; - - CREATE PERFETTO TABLE ${this.segmentsTableName} AS - SELECT iss.ts, iss.group_id, iss.interval_ends_at_ts, ${denormCols} - FROM interval_self_intersect!(( - SELECT id, ts, dur FROM ${this.intervalsTableName} - )) iss - JOIN ${this.intervalsTableName} i USING(id); + WHERE ${idCheck}${aggTs} IS NOT NULL AND ${aggDur} > 0; + ${segmentsTables} CREATE PERFETTO TABLE ${this.projectedTableName} AS SELECT ${projectedCols} @@ -375,7 +386,6 @@ export class BreakdownTracks { } async createTracks(): Promise { - await this.props.trace.engine.query(this.modulesClause); await this.buildTables(); const rootTrackNode = await this.createCounterTrackNode( @@ -555,10 +565,12 @@ export class BreakdownTracks { private async getCounterTrackSortOrder( filtersClause: string, + level: number, ): Promise { - const aggregationQuery = this.getAggregationQuery(filtersClause); const result = await this.props.trace.engine.query(` - SELECT MAX(value) as max_value FROM (${aggregationQuery}) + SELECT MAX(value) as max_value + FROM ${this.segmentsTableNames[level]} + ${filtersClause} `); const maxValue = result.firstRow({max_value: NUM_NULL}).max_value; return maxValue === null ? 0 : maxValue; @@ -568,11 +580,14 @@ export class BreakdownTracks { name: string, newFilters: Filter[], ): Promise { + // A counter node's filters are exactly the breakdown columns above it, so + // its level in the hierarchy is the number of filters. + const level = newFilters.length; return this.createTrackNode( name, newFilters, (uri, filtersClause) => - // Lazy: getAggregationQuery is a cheap GROUP BY over the shared segments + // Lazy: getAggregationQuery is a cheap filter over the level's segments // table, and CounterTrack's useData fires it only on render — nothing is // materialized per node at trace load. CounterTrack.create({ @@ -580,10 +595,10 @@ export class BreakdownTracks { uri, sqlSource: ` SELECT ts, value - FROM (${this.getAggregationQuery(filtersClause)}) + FROM (${this.getAggregationQuery(filtersClause, level)}) `, }), - (filtersClause) => this.getCounterTrackSortOrder(filtersClause), + (filtersClause) => this.getCounterTrackSortOrder(filtersClause, level), ); } From 15a3d62af101dabcb06356c746e1470ac0e96075 Mon Sep 17 00:00:00 2001 From: gignat Date: Tue, 6 Oct 2026 08:54:09 +0000 Subject: [PATCH 2/6] ui: sort binder breakdown counters by peak Children of each binder counter are now ordered by their peak concurrent transaction count, busiest first (ties stay alphabetical), like the memory breakdowns already do. The ordering costs one GROUP BY over the next level's INTERVAL FLATTEN table per expand, a few ms at most. --- ui/src/plugins/com.android.AndroidBinderViz/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/plugins/com.android.AndroidBinderViz/index.ts b/ui/src/plugins/com.android.AndroidBinderViz/index.ts index 3b423f38f62..7ac96490202 100644 --- a/ui/src/plugins/com.android.AndroidBinderViz/index.ts +++ b/ui/src/plugins/com.android.AndroidBinderViz/index.ts @@ -87,7 +87,7 @@ export default class implements PerfettoPlugin { durCol: `${oppositePerspective}_dur`, }, sliceIdColumn: sliceIdColumn, - sortTracks: false, + sortTracks: true, detailsPanel: (trace: Trace) => new BinderSliceDetailsPanel(trace), }); From 3135d986f4350c98332e91d4703e83cfe98c397f Mon Sep 17 00:00:00 2001 From: gignat Date: Tue, 6 Oct 2026 13:46:02 +0000 Subject: [PATCH 3/6] ui: tighten BreakdownTracks intervals for INTERVAL FLATTEN Drop the id column from the intervals table: FLATTEN keeps only ts, dur and the PER columns, and nothing else reads the table, so the column (and the ROW_NUMBER window pass behind it when no sliceIdColumn is set) was dead. Filter ts >= 0 instead of IS NOT NULL: FLATTEN rejects a negative ts, which would have failed the whole batch and with it every track of the instance. Document that valueCol must be an integer column, as FLATTEN only sums integers. --- ui/src/components/tracks/breakdown_tracks.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/ui/src/components/tracks/breakdown_tracks.ts b/ui/src/components/tracks/breakdown_tracks.ts index 3430e061254..067597c4401 100644 --- a/ui/src/components/tracks/breakdown_tracks.ts +++ b/ui/src/components/tracks/breakdown_tracks.ts @@ -54,7 +54,7 @@ interface BreakdownTrackSqlInfo { /** * This is the value that should be displayed in the * aggregation counter track. Required for MAX / SUM aggregation; ignored for - * COUNT. + * COUNT. Must be an integer column. */ valueCol?: string; /** @@ -304,7 +304,7 @@ export class BreakdownTracks { } // Builds the tables that drive every track: - // _breakdown_intervals: one row per source interval (id, ts, dur, the + // _breakdown_intervals: one row per source interval (ts, dur, the // breakdown columns, [agg_value]) from the aggregation table ONLY — no // slice/pivot joins, so the overlap count is never inflated by a 1:N join. // _breakdown_segments_: INTERVAL FLATTEN of the intervals partitioned by @@ -317,11 +317,9 @@ export class BreakdownTracks { const agg = this.props.aggregation; const aggTs = agg.tsCol ?? 'ts'; const aggDur = agg.durCol ?? 'dur'; - const idExpr = this.props.sliceIdColumn ?? 'ROW_NUMBER() OVER ()'; const hasValue = agg.valueCol !== undefined; const intervalCols = [ - `${idExpr} AS id`, `${aggTs} AS ts`, `${aggDur} AS dur`, ...agg.columns.map((col, i) => `${col} AS k${i}`), @@ -329,10 +327,11 @@ export class BreakdownTracks { ].join(', '); // Drop intervals that can't carry a count: a NULL id (e.g. binder_reply_id - // on oneway transactions), a negative dur (dur = -1 marks an incomplete - // slice) or a zero dur (an instant, which FLATTEN would otherwise count - // over a zero-width segment). ROW_NUMBER ids are never NULL, so the id - // check is only emitted when a real id column was supplied. + // on oneway transactions), a NULL or negative ts (FLATTEN rejects ts < 0), + // a negative dur (dur = -1 marks an incomplete slice) or a zero dur (an + // instant, which FLATTEN would otherwise count over a zero-width segment). + // ROW_NUMBER ids are never NULL, so the id check is only emitted when a + // real id column was supplied. const idCheck = this.props.sliceIdColumn ? `${this.props.sliceIdColumn} IS NOT NULL AND ` : ''; @@ -354,8 +353,7 @@ export class BreakdownTracks { // be ambiguous if a join table shares the name — e.g. the binder breakdown // tables also carry binder_reply_id, which the client perspective uses as // its id. Qualify it with the base table. The ROW_NUMBER fallback needs no - // qualification and can't be ambiguous. (The intervals table has no joins, - // so it keeps the unqualified idExpr.) + // qualification and can't be ambiguous. const projectedIdExpr = this.props.sliceIdColumn ? `${agg.tableName}.${this.props.sliceIdColumn}` : 'ROW_NUMBER() OVER ()'; @@ -374,7 +372,7 @@ export class BreakdownTracks { CREATE PERFETTO TABLE ${this.intervalsTableName} AS SELECT ${intervalCols} FROM ${agg.tableName} - WHERE ${idCheck}${aggTs} IS NOT NULL AND ${aggDur} > 0; + WHERE ${idCheck}${aggTs} >= 0 AND ${aggDur} > 0; ${segmentsTables} CREATE PERFETTO TABLE ${this.projectedTableName} AS From f6ec085e857f9cc4bf18d0a8cf709bbc4d90bb01 Mon Sep 17 00:00:00 2001 From: gignat Date: Tue, 6 Oct 2026 16:39:46 +0000 Subject: [PATCH 4/6] ui: enable PerfettoSQL pipelines once per trace Run PERFETTO PRAGMA pipelines = 1 in loadTrace, after notifyEof / restoreInitialTables, instead of inside BreakdownTracks. The flag is per connection and restoreInitialTables rebuilds the connection, so this is the first point where it sticks for both the Wasm and the HTTP RPC engine. Any plugin can now use pipe syntax without setting it. --- ui/src/components/tracks/breakdown_tracks.ts | 1 - ui/src/core/load_trace.ts | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/components/tracks/breakdown_tracks.ts b/ui/src/components/tracks/breakdown_tracks.ts index 067597c4401..b9eb027a64c 100644 --- a/ui/src/components/tracks/breakdown_tracks.ts +++ b/ui/src/components/tracks/breakdown_tracks.ts @@ -367,7 +367,6 @@ export class BreakdownTracks { await this.props.trace.engine.query(` ${this.modulesClause} - PERFETTO PRAGMA pipelines = 1; CREATE PERFETTO TABLE ${this.intervalsTableName} AS SELECT ${intervalCols} diff --git a/ui/src/core/load_trace.ts b/ui/src/core/load_trace.ts index 2d742eb5c64..4ba71eeb10d 100644 --- a/ui/src/core/load_trace.ts +++ b/ui/src/core/load_trace.ts @@ -238,6 +238,10 @@ async function loadTraceIntoEngine( await engine.restoreInitialTables(); } + // Set once the connection is in its final state: restoreInitialTables + // rebuilds it, which resets the flag. + await engine.query('PERFETTO PRAGMA pipelines = 1;'); + for (const p of await app.sqlPackages()) { await engine.registerSqlPackages(p); } From bc9bd142fd93251225d9955b7ef382fb9bc436a2 Mon Sep 17 00:00:00 2001 From: gignat Date: Tue, 6 Oct 2026 20:16:20 +0000 Subject: [PATCH 5/6] ui: build all BreakdownTracks levels with one INTERVAL FLATTEN Replace the per-level segments tables with a single one: the intervals are replicated once per hierarchy level with a level column (deeper breakdown columns NULL) and flattened once with level as the first PER column, so every level's segments come out of one pass. A counter node pins its level as a plain filter next to its breakdown columns. --- ui/src/components/tracks/breakdown_tracks.ts | 102 +++++++++++-------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/ui/src/components/tracks/breakdown_tracks.ts b/ui/src/components/tracks/breakdown_tracks.ts index b9eb027a64c..223567a9e02 100644 --- a/ui/src/components/tracks/breakdown_tracks.ts +++ b/ui/src/components/tracks/breakdown_tracks.ts @@ -162,10 +162,11 @@ interface Filter { * slice columns -> slice tracks * pivot columns -> slice tracks (sourced via an optional join) * - * All counters are driven by per-level `INTERVAL FLATTEN` segment tables built - * once per instance, so the expensive overlap computation is paid once per - * hierarchy level instead of once per node. The per-node counter query is then - * a cheap filter over its level's segments, and the counter renderers are lazy + * All counters are driven by ONE shared `INTERVAL FLATTEN` segments table built + * once per instance, holding the atomic overlap segments of every hierarchy + * level, so the expensive overlap computation is paid a single time instead of + * once per node. The per-node counter query is then a cheap filter over those + * segments, and the counter renderers are lazy * (CounterTrack.create), so the work scales with what the user views rather * than with the size of the hierarchy. */ @@ -184,10 +185,11 @@ export class BreakdownTracks { // each name gets a UUID suffix so independent grids — e.g. the binder server // and client trees — never collide on a table name: // intervals one row per source interval (no joins). - // segments one per hierarchy level: INTERVAL FLATTEN PER k0..k(i-1). + // segments INTERVAL FLATTEN output: the atomic overlap segments of every + // level, tagged with their level. // projected source rows + slice/pivot joins (may fan out 1:N) + ts/dur. private readonly intervalsTableName: string; - private readonly segmentsTableNames: readonly string[]; + private readonly segmentsTableName: string; private readonly projectedTableName: string; // The breakdown / slice / pivot columns the caller supplies are arbitrary SQL @@ -216,18 +218,13 @@ export class BreakdownTracks { const unique = uuidv4().replace(/-/g, '_'); this.intervalsTableName = `_breakdown_intervals_${unique}`; + this.segmentsTableName = `_breakdown_segments_${unique}`; this.projectedTableName = `_breakdown_projected_${unique}`; this.aggColNames = props.aggregation.columns.map((_, i) => `k${i}`); this.sliceColNames = (props.slice?.columns ?? []).map((_, i) => `s${i}`); this.pivotColNames = (props.pivots?.columns ?? []).map((_, i) => `p${i}`); - // Level i is partitioned by the first i breakdown columns; level 0 is the - // root (no partition). - this.segmentsTableNames = [...Array(this.aggColNames.length + 1)].map( - (_, i) => `_breakdown_segments_${unique}_${i}`, - ); - this.modulesClause = (props.modules ?? []) .map((m) => `INCLUDE PERFETTO MODULE ${m};`) .join('\n'); @@ -254,15 +251,15 @@ export class BreakdownTracks { assertUnreachable(this.props.aggregationType); } - private getAggregationQuery(filtersClause: string, level: number): string { - // One row per atomic segment of this node, read from its level's segments - // table; filters are raw equality on the pre-projected k* columns. FLATTEN - // emits nothing where no interval is active, so a 0 is added at the end of - // every run of segments to make the counter drop there. + private getAggregationQuery(filtersClause: string): string { + // One row per atomic segment of this node; filters are raw equality on the + // level and the pre-projected k* columns. FLATTEN emits nothing where no + // interval is active, so a 0 is added at the end of every run of segments + // to make the counter drop there. return ` WITH s AS ( SELECT ts, dur, value, LEAD(ts) OVER (ORDER BY ts) AS next_ts - FROM ${this.segmentsTableNames[level]} + FROM ${this.segmentsTableName} ${filtersClause} ) SELECT ts, value FROM s @@ -307,9 +304,10 @@ export class BreakdownTracks { // _breakdown_intervals: one row per source interval (ts, dur, the // breakdown columns, [agg_value]) from the aggregation table ONLY — no // slice/pivot joins, so the overlap count is never inflated by a 1:N join. - // _breakdown_segments_: INTERVAL FLATTEN of the intervals partitioned by - // the first i breakdown columns: one row per atomic segment with its - // aggregate, so per-node counter queries are a plain filter on level i. + // _breakdown_segments: INTERVAL FLATTEN over the intervals, once per + // hierarchy level (level i partitioned by the first i breakdown columns, + // level 0 the unpartitioned root): one row per atomic segment with its + // level and aggregate, so per-node counter queries are a plain filter. // _breakdown_projected: one row per source row WITH the slice/pivot joins // applied (so it may fan out 1:N) plus the slice/pivot ts/dur. Drives the // hierarchy enumeration and the slice/pivot tracks. @@ -336,18 +334,25 @@ export class BreakdownTracks { ? `${this.props.sliceIdColumn} IS NOT NULL AND ` : ''; - const segmentsTables = this.segmentsTableNames - .map((name, level) => { - const per = - level === 0 - ? '' - : `PER ${this.aggColNames.slice(0, level).join(', ')} `; - return ` - CREATE PERFETTO TABLE ${name} AS - FROM ${this.intervalsTableName} - |> INTERVAL FLATTEN ${per}AGGREGATE ${this.aggExpr()} AS value;`; - }) - .join('\n'); + // One FLATTEN input row per interval and level, keyed by the level and the + // breakdown columns above it (the deeper ones NULL). `level` must be a PER + // column: a real key can itself be NULL, so without it level 0 and level 1 + // with a NULL k0 would share a group. With it, one FLATTEN computes every + // level's segments in a single pass. + const levelRows = Array.from( + {length: this.aggColNames.length + 1}, + (_, level) => { + const cols = [ + `${level} AS level`, + 'ts', + 'dur', + ...this.aggColNames.map((k, i) => (i < level ? k : `NULL AS ${k}`)), + ...(hasValue ? ['agg_value'] : []), + ]; + return `SELECT ${cols.join(', ')} FROM ${this.intervalsTableName}`; + }, + ).join('\n UNION ALL '); + const perCols = ['level', ...this.aggColNames].join(', '); // The projection applies the slice/pivot joins, so a plain id column would // be ambiguous if a join table shares the name — e.g. the binder breakdown @@ -372,7 +377,12 @@ export class BreakdownTracks { SELECT ${intervalCols} FROM ${agg.tableName} WHERE ${idCheck}${aggTs} >= 0 AND ${aggDur} > 0; - ${segmentsTables} + + CREATE PERFETTO TABLE ${this.segmentsTableName} AS + FROM ( + ${levelRows} + ) + |> INTERVAL FLATTEN PER ${perCols} AGGREGATE ${this.aggExpr()} AS value; CREATE PERFETTO TABLE ${this.projectedTableName} AS SELECT ${projectedCols} @@ -562,11 +572,10 @@ export class BreakdownTracks { private async getCounterTrackSortOrder( filtersClause: string, - level: number, ): Promise { const result = await this.props.trace.engine.query(` SELECT MAX(value) as max_value - FROM ${this.segmentsTableNames[level]} + FROM ${this.segmentsTableName} ${filtersClause} `); const maxValue = result.firstRow({max_value: NUM_NULL}).max_value; @@ -578,13 +587,17 @@ export class BreakdownTracks { newFilters: Filter[], ): Promise { // A counter node's filters are exactly the breakdown columns above it, so - // its level in the hierarchy is the number of filters. - const level = newFilters.length; + // its level in the hierarchy is the number of filters; the level selects + // its segments in the shared segments table. + const filters: Filter[] = [ + {columnName: 'level', value: `${newFilters.length}`}, + ...newFilters, + ]; return this.createTrackNode( name, - newFilters, + filters, (uri, filtersClause) => - // Lazy: getAggregationQuery is a cheap filter over the level's segments + // Lazy: getAggregationQuery is a cheap filter over the shared segments // table, and CounterTrack's useData fires it only on render — nothing is // materialized per node at trace load. CounterTrack.create({ @@ -592,10 +605,11 @@ export class BreakdownTracks { uri, sqlSource: ` SELECT ts, value - FROM (${this.getAggregationQuery(filtersClause, level)}) + FROM (${this.getAggregationQuery(filtersClause)}) `, }), - (filtersClause) => this.getCounterTrackSortOrder(filtersClause, level), + (filtersClause) => this.getCounterTrackSortOrder(filtersClause), + newFilters.length === 0 ? this.props.description : undefined, ); } @@ -604,6 +618,7 @@ export class BreakdownTracks { filters: Filter[], createTrack: (uri: string, filtersClause: string) => TrackRenderer, getSortOrder?: (filterClause: string) => Promise, + description?: string, ): Promise { const filtersClause = filters.length > 0 ? `\nWHERE ${buildFilterSqlClause(filters)}` : ''; @@ -614,8 +629,7 @@ export class BreakdownTracks { this.props.trace.tracks.registerTrack({ uri, renderer, - ...(filters.length === 0 && - this.props.description && {description: this.props.description}), + ...(description && {description}), }); let sortOrder: number | undefined; From 3814a1fd45c3275b87e92a06989166d53303708e Mon Sep 17 00:00:00 2001 From: gignat Date: Tue, 6 Oct 2026 21:08:53 +0000 Subject: [PATCH 6/6] ui: index BreakdownTracks segments on level and breakdown columns Every per-node counter query filters on level plus the breakdown columns above the node. With all levels in one table a plain scan touches every level's segments, so index on exactly those columns. --- ui/src/components/tracks/breakdown_tracks.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/src/components/tracks/breakdown_tracks.ts b/ui/src/components/tracks/breakdown_tracks.ts index 223567a9e02..b4564a1da53 100644 --- a/ui/src/components/tracks/breakdown_tracks.ts +++ b/ui/src/components/tracks/breakdown_tracks.ts @@ -384,6 +384,11 @@ export class BreakdownTracks { ) |> INTERVAL FLATTEN PER ${perCols} AGGREGATE ${this.aggExpr()} AS value; + -- Per-node queries filter on exactly these columns; without the index + -- each one scans every level's segments. + CREATE PERFETTO INDEX ${this.segmentsTableName}_idx + ON ${this.segmentsTableName}(${perCols}); + CREATE PERFETTO TABLE ${this.projectedTableName} AS SELECT ${projectedCols} FROM ${agg.tableName}