diff --git a/Android.bp b/Android.bp index 291e4d276c0..a52cf1b34d8 100644 --- a/Android.bp +++ b/Android.bp @@ -19164,7 +19164,6 @@ filegroup { filegroup { name: "perfetto_src_trace_processor_perfetto_sql_stdlib_viz_viz", srcs: [ - "src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql", "src/trace_processor/perfetto_sql/stdlib/viz/slices.sql", "src/trace_processor/perfetto_sql/stdlib/viz/summary/counters.sql", "src/trace_processor/perfetto_sql/stdlib/viz/summary/processes.sql", diff --git a/BUILD b/BUILD index 42722780d04..41550a42215 100644 --- a/BUILD +++ b/BUILD @@ -4287,7 +4287,6 @@ perfetto_filegroup( name = "src_trace_processor_perfetto_sql_stdlib_viz_viz", srcs = [ ":src_trace_processor_perfetto_sql_stdlib_viz_summary_summary", - "src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql", "src/trace_processor/perfetto_sql/stdlib/viz/slices.sql", "src/trace_processor/perfetto_sql/stdlib/viz/threads.sql", "src/trace_processor/perfetto_sql/stdlib/viz/track_event_callstacks.sql", diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/viz/BUILD.gn index bc5b891ccdc..df5ec95aad2 100644 --- a/src/trace_processor/perfetto_sql/stdlib/viz/BUILD.gn +++ b/src/trace_processor/perfetto_sql/stdlib/viz/BUILD.gn @@ -16,7 +16,6 @@ import("../../../../../gn/perfetto_sql.gni") perfetto_sql_source_set("viz") { sources = [ - "flamegraph.sql", "slices.sql", "threads.sql", "track_event_callstacks.sql", diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql b/src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql deleted file mode 100644 index 70c6a809ad5..00000000000 --- a/src/trace_processor/perfetto_sql/stdlib/viz/flamegraph.sql +++ /dev/null @@ -1,408 +0,0 @@ --- --- Copyright 2024 The Android Open Source Project --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- https://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - --- sqlformat file off --- The case sensitivity of this file matters so don't format it which --- changes sensitivity. - -INCLUDE PERFETTO MODULE graphs.scan; - -INCLUDE PERFETTO MODULE std.metasql.unparenthesize; - -CREATE PERFETTO MACRO _viz_flamegraph_hash_coalesce(col ColumnName) -RETURNS Expr -AS IFNULL($col, 0); - --- For each frame in |tab|, returns a row containing the result of running --- all the filtering operations over that frame's name. -CREATE PERFETTO MACRO _viz_flamegraph_prepare_filter( - tab TableOrSubquery, - show_stack Expr, - hide_stack Expr, - show_from_frame Expr, - hide_frame Expr, - pivot Expr, - impossible_stack_bits Expr, - grouping ColumnNameList -) -RETURNS TableOrSubquery -AS ( - SELECT - *, - IIF($hide_stack, $impossible_stack_bits, $show_stack) AS stackBits, - $show_from_frame As showFromFrameBits, - $hide_frame = 0 AS showFrame, - $pivot AS isPivot, - HASH( - name, - __intrinsic_token_apply!(_viz_flamegraph_hash_coalesce, $grouping) - ) AS groupingHash - FROM $tab - ORDER BY id -); - --- Walks the forest from root to leaf and performs the following operations: --- 1) removes frames which were filtered out --- 2) make any pivot nodes become the roots --- 3) computes whether the stack as a whole should be retained or not -CREATE PERFETTO MACRO _viz_flamegraph_filter_frames( - source TableOrSubquery, - show_from_frame_bits Expr -) -RETURNS TableOrSubquery -AS ( - WITH edges AS ( - SELECT parentId AS source_node_id, id AS dest_node_id - FROM $source - WHERE parentId IS NOT NULL - ), - inits AS ( - SELECT - id, - IIF( - showFrame AND showFromFrameBits = $show_from_frame_bits, - id, - NULL - ) AS filteredId, - NULL AS filteredParentId, - NULL AS filteredUnpivotedParentId, - IIF( - showFrame, - showFromFrameBits, - 0 - ) AS showFromFrameBits, - IIF( - showFrame AND showFromFrameBits = $show_from_frame_bits, - stackBits, - 0 - ) AS stackBits - FROM $source - WHERE parentId IS NULL - ) - SELECT - g.filteredId AS id, - g.filteredParentId AS parentId, - g.filteredUnpivotedParentId AS unpivotedParentId, - g.stackBits, - SUM(t.value) AS value - FROM _graph_scan!( - edges, - inits, - (filteredId, filteredParentId, filteredUnpivotedParentId, showFromFrameBits, stackBits), - ( - SELECT - t.id, - IIF( - x.showFrame AND (t.showFromFrameBits | x.showFromFrameBits) = $show_from_frame_bits, - t.id, - t.filteredId - ) AS filteredId, - IIF( - x.showFrame AND (t.showFromFrameBits | x.showFromFrameBits) = $show_from_frame_bits, - IIF(x.isPivot, NULL, t.filteredId), - t.filteredParentId - ) AS filteredParentId, - IIF( - x.showFrame AND (t.showFromFrameBits | x.showFromFrameBits) = $show_from_frame_bits, - t.filteredId, - t.filteredParentId - ) AS filteredUnpivotedParentId, - IIF( - x.showFrame, - (t.showFromFrameBits | x.showFromFrameBits), - t.showFromFrameBits - ) AS showFromFrameBits, - IIF( - x.showFrame AND (t.showFromFrameBits | x.showFromFrameBits) = $show_from_frame_bits, - (t.stackBits | x.stackBits), - t.stackBits - ) AS stackBits - FROM $table t - JOIN $source x USING (id) - ) - ) g - JOIN $source t USING (id) - WHERE filteredId IS NOT NULL - GROUP BY filteredId - ORDER BY filteredId -); - --- Walks the forest from leaves to root and does the following: --- 1) removes nodes whose stacks are filtered out --- 2) computes the cumulative value for each node (i.e. the sum of the self --- value of the node and all descendants). -CREATE PERFETTO MACRO _viz_flamegraph_accumulate( - filtered TableOrSubquery, - showStackBits Expr -) -RETURNS TableOrSubquery -AS ( - WITH edges AS ( - SELECT id AS source_node_id, parentId AS dest_node_id - FROM $filtered - WHERE parentId IS NOT NULL - ), inits AS ( - SELECT f.id, f.value AS cumulativeValue - FROM $filtered f - LEFT JOIN $filtered c ON c.parentId = f.id - WHERE c.id IS NULL AND f.stackBits = $showStackBits - ) - SELECT id, cumulativeValue - FROM _graph_aggregating_scan!( - edges, - inits, - (cumulativeValue), - ( - SELECT - x.id, - x.childValue + IIF( - t.stackBits = $showStackBits, - t.value, - 0 - ) AS cumulativeValue - FROM ( - SELECT id, SUM(cumulativeValue) AS childValue - FROM $table - GROUP BY id - ) x - JOIN $filtered t USING (id) - ) - ) - ORDER BY id -); - -CREATE PERFETTO MACRO _viz_flamegraph_s_prefix(col ColumnName) -RETURNS Expr -AS s.$col; - --- Propogates the cumulative value of the pivot nodes to the roots --- and computes the "fingerprint" of the path. -CREATE PERFETTO MACRO _viz_flamegraph_upwards_hash( - source TableOrSubquery, - filtered TableOrSubquery, - accumulated TableOrSubquery, - grouping ColumnNameList, - grouped ColumnNameList -) -RETURNS TableOrSubquery -AS ( - WITH edges AS ( - SELECT id AS source_node_id, unpivotedParentId AS dest_node_id - FROM $filtered - WHERE unpivotedParentId IS NOT NULL - ), - inits AS ( - SELECT - f.id, - HASH(-1, s.groupingHash) AS hash, - NULL AS parentHash, - -1 AS depth, - a.cumulativeValue - FROM $filtered f - JOIN $source s USING (id) - JOIN $accumulated a USING (id) - WHERE s.isPivot AND a.cumulativeValue > 0 - ) - SELECT - g.id, - g.hash, - g.parentHash, - g.depth, - s.name, - __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouping), - __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouped), - f.value, - g.cumulativeValue - FROM _graph_scan!( - edges, - inits, - (hash, parentHash, depth, cumulativeValue), - ( - SELECT - t.id, - HASH(t.hash, x.groupingHash) AS hash, - t.hash AS parentHash, - t.depth - 1 AS depth, - t.cumulativeValue - FROM $table t - JOIN $source x USING (id) - ) - ) g - JOIN $source s USING (id) - JOIN $filtered f USING (id) -); - --- Computes the "fingerprint" of the path by walking from the laves --- to the root. -CREATE PERFETTO MACRO _viz_flamegraph_downwards_hash( - source TableOrSubquery, - filtered TableOrSubquery, - accumulated TableOrSubquery, - grouping ColumnNameList, - grouped ColumnNameList, - showDownward Expr -) -RETURNS TableOrSubquery -AS ( - WITH - edges AS ( - SELECT parentId AS source_node_id, id AS dest_node_id - FROM $filtered - WHERE parentId IS NOT NULL - ), - inits AS ( - SELECT - f.id, - HASH(1, s.groupingHash) AS hash, - NULL AS parentHash, - 1 AS depth - FROM $filtered f - JOIN $source s USING (id) - WHERE f.parentId IS NULL AND $showDownward - ) - SELECT - g.id, - g.hash, - g.parentHash, - g.depth, - s.name, - __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouping), - __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouped), - f.value, - a.cumulativeValue - FROM _graph_scan!( - edges, - inits, - (hash, parentHash, depth), - ( - SELECT - t.id, - HASH(t.hash, x.groupingHash) AS hash, - t.hash AS parentHash, - t.depth + 1 AS depth - FROM $table t - JOIN $source x USING (id) - ) - ) g - JOIN $source s USING (id) - JOIN $filtered f USING (id) - JOIN $accumulated a USING (id) - ORDER BY hash -); - --- Converts a table of hashes and paretn hashes into ids and parent --- ids, grouping all hashes together. -CREATE PERFETTO MACRO _viz_flamegraph_merge_hashes( - hashed TableOrSubquery, - grouping ColumnNameList, - grouped_agged_exprs ColumnNameList -) -RETURNS TableOrSubquery -AS ( - SELECT - _auto_id AS id, - ( - SELECT p._auto_id - FROM $hashed p - WHERE p.hash = c.parentHash - LIMIT 1 - ) AS parentId, - depth, - name, - -- The grouping columns should be passed through as-is because the - -- hash took them into account: we would not merged any nodes where - -- the grouping columns were different. - metasql_unparenthesize_exprlist!($grouping), - metasql_unparenthesize_exprlist!($grouped_agged_exprs), - SUM(value) AS value, - SUM(cumulativeValue) AS cumulativeValue - FROM $hashed c - GROUP BY hash -); - --- Performs a "layout" of nodes in the flamegraph relative to their --- siblings. -CREATE PERFETTO MACRO _viz_flamegraph_local_layout(merged TableOrSubquery) -RETURNS TableOrSubquery -AS ( - WITH partial_layout AS ( - SELECT - id, - cumulativeValue, - SUM(cumulativeValue) OVER win AS xEnd - FROM $merged - WHERE cumulativeValue > 0 - WINDOW win AS ( - PARTITION BY parentId, depth - ORDER BY cumulativeValue DESC - ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - ) - ) - SELECT id, xEnd - cumulativeValue as xStart, xEnd - FROM partial_layout - ORDER BY id -); - --- Walks the graph from root to leaf, propogating the layout of --- parents to their children. -CREATE PERFETTO MACRO _viz_flamegraph_global_layout( - merged TableOrSubquery, - layout TableOrSubquery, - grouping ColumnNameList, - grouped ColumnNameList -) -RETURNS TableOrSubquery -AS ( - WITH edges AS ( - SELECT parentId AS source_node_id, id AS dest_node_id - FROM $merged - WHERE parentId IS NOT NULL - ), - inits AS ( - SELECT h.id, 1 AS rootDistance, l.xStart, l.xEnd - FROM $merged h - JOIN $layout l USING (id) - WHERE h.parentId IS NULL - ) - SELECT - s.id, - IFNULL(s.parentId, -1) AS parentId, - IIF(s.name = '', 'unknown', s.name) AS name, - __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouping), - __intrinsic_token_apply!(_viz_flamegraph_s_prefix, $grouped), - s.value AS selfValue, - s.cumulativeValue, - p.cumulativeValue AS parentCumulativeValue, - s.depth, - g.xStart, - g.xEnd - FROM _graph_scan!( - edges, - inits, - (rootDistance, xStart, xEnd), - ( - SELECT - t.id, - t.rootDistance + 1 as rootDistance, - t.xStart + w.xStart AS xStart, - t.xStart + w.xEnd AS xEnd - FROM $table t - JOIN $layout w USING (id) - ) - ) g - JOIN $merged s USING (id) - LEFT JOIN $merged p ON s.parentId = p.id - ORDER BY rootDistance, xStart -); diff --git a/ui/src/components/query_flamegraph.ts b/ui/src/components/query_flamegraph.ts index e9cf33a95e6..019935ce344 100644 --- a/ui/src/components/query_flamegraph.ts +++ b/ui/src/components/query_flamegraph.ts @@ -16,11 +16,10 @@ import m from 'mithril'; import {AsyncLimiter} from '../base/async_limiter'; import {AsyncDisposableStack} from '../base/disposable_stack'; import {ensureExists} from '../base/assert'; -import {uuidv4Sql} from '../base/uuid'; import type {Engine} from '../trace_processor/engine'; import { - createPerfettoIndex, - createPerfettoTable, + createVirtualTable, + type DisposableSqlEntity, } from '../trace_processor/sql_utils'; import { NUM, @@ -35,7 +34,6 @@ import { type FlamegraphPropertyDefinition, type FlamegraphQueryData, type FlamegraphState, - type FlamegraphView, type FlamegraphOptionalAction, type FlamegraphOptionalMarker, } from '../widgets/flamegraph'; @@ -191,6 +189,12 @@ interface QueryFlamegraphAttrs { readonly onStateChange: (state: FlamegraphState) => void; } +interface FlamegraphTable { + readonly metric: QueryFlamegraphMetric; + readonly table: DisposableSqlEntity; + readonly unfilteredCumulativeValue: number; +} + // A Perfetto UI component which wraps the `Flamegraph` widget and fetches the // data for the widget by querying an `Engine`. export class QueryFlamegraph implements AsyncDisposable { @@ -199,6 +203,7 @@ export class QueryFlamegraph implements AsyncDisposable { private readonly dependencies: ReadonlyArray< SharedAsyncDisposable >; + private readonly flamegraphTables: FlamegraphTable[] = []; private lastAttrs?: QueryFlamegraphAttrs; private monitor = new Monitor([ () => this.lastAttrs?.metrics, @@ -213,6 +218,9 @@ export class QueryFlamegraph implements AsyncDisposable { } async [Symbol.asyncDispose](): Promise { + for (const flamegraph of this.flamegraphTables) { + await flamegraph.table[Symbol.asyncDispose](); + } for (const dependency of this.dependencies ?? []) { await dependency[Symbol.asyncDispose]?.(); } @@ -266,254 +274,137 @@ export class QueryFlamegraph implements AsyncDisposable { } trash.use(dependency.clone()); } - this.data = await computeFlamegraphTree(engine, metric, state); + const flamegraph = await this.getFlamegraphTable(metric); + this.data = await computeFlamegraphTree(engine, flamegraph, state); + }); + } + + private async getFlamegraphTable( + metric: QueryFlamegraphMetric, + ): Promise { + const cached = this.flamegraphTables.find( + (entry) => entry.metric === metric, + ); + if (cached) { + return cached; + } + if (metric.dependencySql !== undefined) { + await this.trace.engine.query(metric.dependencySql); + } + const properties = [ + ...(metric.unaggregatableProperties ?? []), + ...(metric.aggregatableProperties ?? []), + ]; + const sourceColumns = [ + 's.id', + 's.parentId as parent_id', + 's.name', + 's.value', + ...properties.map((property) => `s.${property.name}`), + ]; + const table = await createVirtualTable({ + engine: this.trace.engine, + using: `__intrinsic_flamegraph(( + select ${sourceColumns.join(', ')} + from (${metric.statement}) s + ))`, }); + try { + const result = await this.trace.engine.query(` + select cumulative_value + from ${table.name}(__intrinsic_flamegraph_config( + 'value', 'value', + 'view', 'TOP_DOWN' + )) + where __intrinsic_flamegraph_find(_tree_id, 'SUPER_ROOT') + `); + const flamegraph = { + metric, + table, + unfilteredCumulativeValue: result.firstRow({ + cumulative_value: NUM, + }).cumulative_value, + }; + this.flamegraphTables.push(flamegraph); + return flamegraph; + } catch (error) { + await table[Symbol.asyncDispose](); + throw error; + } } } async function computeFlamegraphTree( engine: Engine, - { - dependencySql, - statement, + flamegraph: FlamegraphTable, + {filters, view}: FlamegraphState, +): Promise { + const { unaggregatableProperties, aggregatableProperties, optionalNodeActions, optionalRootActions, optionalMarker, - }: QueryFlamegraphMetric, - {filters, view}: FlamegraphState, -): Promise { - const showStack = filters - .filter((x) => x.kind === 'SHOW_STACK') - .map((x) => x.filter); - const hideStack = filters - .filter((x) => x.kind === 'HIDE_STACK') - .map((x) => x.filter); - const hideFrame = filters - .filter((x) => x.kind === 'HIDE_FRAME') - .map((x) => x.filter); - - // Pivot also essentially acts as a "show stack" filter so treat it like one. - const showStackAndPivot = [...showStack]; - if (view.kind === 'PIVOT') { - showStackAndPivot.push(view.pivot); - } - + } = flamegraph.metric; const agg = aggregatableProperties ?? []; const aggCols = agg.map((x) => x.name); const unagg = unaggregatableProperties ?? []; const unaggCols = unagg.map((x) => x.name); - - const matchingColumns = ['name', ...unaggCols]; - // Bare filters are case-insensitive literals. `/…/` is a case-sensitive - // regex and `/…/i` is a case-insensitive regex. - const matchExpr = (filter: string) => { - const regex = parseUserFilterRegex(filter); - return matchingColumns.map( - (column) => - `regexp(${sqliteString(regex.pattern)}, IFNULL(${column}, ''), ${sqliteString(regex.flags)})`, - ); - }; - - const showStackFilter = - showStackAndPivot.length === 0 - ? '0' - : showStackAndPivot - .map((x, i) => `((${matchExpr(x).join(' OR ')}) << ${i})`) - .join(' | '); - const showStackBits = (1 << showStackAndPivot.length) - 1; - - const hideStackFilter = - hideStack.length === 0 - ? 'false' - : hideStack - .map((x) => matchExpr(x)) - .flat() - .join(' OR '); - - const showFromFrameFilter = - view.kind === 'FROM_FRAME' ? matchExpr(view.pattern).join(' OR ') : 'false'; - const showFromFrameBits = view.kind === 'FROM_FRAME' ? 1 : 0; - - const hideFrameFilter = - hideFrame.length === 0 - ? 'false' - : hideFrame - .map((x) => matchExpr(x)) - .flat() - .join(' OR '); - - const pivotFilter = getPivotFilter(view, matchExpr); - const nodeActions = optionalNodeActions ?? []; const rootActions = optionalRootActions ?? []; - const groupingColumns = `(${(unaggCols.length === 0 ? ['groupingColumn'] : unaggCols).join()})`; - const groupedColumns = `(${(aggCols.length === 0 ? ['groupedColumn'] : aggCols).join()})`; - - if (dependencySql !== undefined) { - await engine.query(dependencySql); + // Convert the UI syntax into finished patterns and explicit flags before + // passing them to the operator. + const configArgs = [`'view', ${sqliteString(view.kind)}`, `'value', 'value'`]; + if (view.kind === 'PIVOT' || view.kind === 'FROM_FRAME') { + const filter = view.kind === 'PIVOT' ? view.pivot : view.pattern; + const regex = parseUserFilterRegex(filter); + configArgs.push( + `'view_pattern', ${sqliteString(regex.pattern)}, ` + + sqliteString(regex.flags), + ); } - await engine.query(`include perfetto module viz.flamegraph;`); - - const uuid = uuidv4Sql(); - await using disposable = new AsyncDisposableStack(); - - disposable.use( - await createPerfettoTable({ - engine, - name: `_flamegraph_materialized_statement_${uuid}`, - as: statement, - }), - ); - disposable.use( - await createPerfettoIndex({ - engine, - name: `_flamegraph_materialized_statement_${uuid}_index`, - on: `_flamegraph_materialized_statement_${uuid}(parentId)`, - }), - ); - - // TODO(lalitm): this doesn't need to be called unless we have - // a non-empty set of filters. - disposable.use( - await createPerfettoTable({ - engine, - name: `_flamegraph_source_${uuid}`, - as: ` - select * - from _viz_flamegraph_prepare_filter!( - ( - select - s.id, - s.parentId, - s.name, - s.value, - ${(unaggCols.length === 0 - ? [`'' as groupingColumn`] - : unaggCols.map((x) => `s.${x}`) - ).join()}, - ${(aggCols.length === 0 - ? [`'' as groupedColumn`] - : aggCols.map((x) => `s.${x}`) - ).join()} - from _flamegraph_materialized_statement_${uuid} s - ), - (${showStackFilter}), - (${hideStackFilter}), - (${showFromFrameFilter}), - (${hideFrameFilter}), - (${pivotFilter}), - ${1 << showStackAndPivot.length}, - ${groupingColumns} - ) - `, - }), - ); - // TODO(lalitm): this doesn't need to be called unless we have - // a non-empty set of filters. - disposable.use( - await createPerfettoTable({ - engine, - name: `_flamegraph_filtered_${uuid}`, - as: ` - select * - from _viz_flamegraph_filter_frames!( - _flamegraph_source_${uuid}, - ${showFromFrameBits} - ) - `, - }), - ); - disposable.use( - await createPerfettoIndex({ - engine, - name: `_flamegraph_filtered_${uuid}_index`, - on: `_flamegraph_filtered_${uuid}(parentId)`, - }), - ); - disposable.use( - await createPerfettoTable({ - engine, - name: `_flamegraph_accumulated_${uuid}`, - as: ` - select * - from _viz_flamegraph_accumulate!( - _flamegraph_filtered_${uuid}, - ${showStackBits} - ) - `, - }), - ); - disposable.use( - await createPerfettoTable({ - engine, - name: `_flamegraph_hash_${uuid}`, - as: ` - select * - from _viz_flamegraph_downwards_hash!( - _flamegraph_source_${uuid}, - _flamegraph_filtered_${uuid}, - _flamegraph_accumulated_${uuid}, - ${groupingColumns}, - ${groupedColumns}, - ${view.kind === 'BOTTOM_UP' ? 'FALSE' : 'TRUE'} - ) - union all - select * - from _viz_flamegraph_upwards_hash!( - _flamegraph_source_${uuid}, - _flamegraph_filtered_${uuid}, - _flamegraph_accumulated_${uuid}, - ${groupingColumns}, - ${groupedColumns} - ) - order by hash - `, - }), - ); - disposable.use( - await createPerfettoTable({ - engine, - name: `_flamegraph_merged_${uuid}`, - as: ` - select * - from _viz_flamegraph_merge_hashes!( - _flamegraph_hash_${uuid}, - ${groupingColumns}, - ${computeGroupedAggExprs(agg)} - ) - `, - }), - ); - disposable.use( - await createPerfettoIndex({ - engine, - name: `_flamegraph_merged_${uuid}_index`, - on: `_flamegraph_merged_${uuid}(parentId)`, - }), - ); - disposable.use( - await createPerfettoTable({ - engine, - name: `_flamegraph_layout_${uuid}`, - as: ` - select * - from _viz_flamegraph_local_layout!( - _flamegraph_merged_${uuid} - ); - `, - }), - ); + for (const filter of filters) { + if (filter.kind !== 'OPTIONS') { + const regex = parseUserFilterRegex(filter.filter); + configArgs.push( + `'filter', ${sqliteString(filter.kind)}, ` + + `${sqliteString(regex.pattern)}, ${sqliteString(regex.flags)}`, + ); + } + } + for (const column of unaggCols) { + configArgs.push(`'grouping', ${sqliteString(column)}`); + } + for (const column of agg) { + configArgs.push( + `'aggregate', ${sqliteString(column.mergeAggregation)}, ` + + `${sqliteString(column.name)}, ${sqliteString(column.name)}`, + ); + } + + const outputColumns = [ + '_tree_id as id', + 'IFNULL(_tree_parent_id, -1) as parentId', + 'depth', + `IIF(IFNULL(name, '') = '', 'unknown', name) as name`, + 'self_value as selfValue', + 'cumulative_value as cumulativeValue', + 'parent_cumulative_value as parentCumulativeValue', + 'x_start as xStart', + 'x_end as xEnd', + ...unaggCols, + ...aggCols, + ]; + + // The operator emits rows pre-ordered for rendering with layout geometry + // attached; nodes with no cumulative value are invisible and skipped. const res = await engine.query(` - select * - from _viz_flamegraph_global_layout!( - _flamegraph_merged_${uuid}, - _flamegraph_layout_${uuid}, - ${groupingColumns}, - ${groupedColumns} + select ${outputColumns.join(', ')} + from ${flamegraph.table.name}( + __intrinsic_flamegraph_config(${configArgs.join(', ')}) ) + where cumulative_value > 0 `); const it = res.iter({ @@ -594,51 +485,14 @@ async function computeFlamegraphTree( minDepth = Math.min(minDepth, it.depth); maxDepth = Math.max(maxDepth, it.depth); } - const sumQuery = await engine.query( - `select sum(value) v from _flamegraph_source_${uuid}`, - ); - const unfilteredCumulativeValue = sumQuery.firstRow({v: NUM_NULL}).v ?? 0; return { nodes, allRootsCumulativeValue: view.kind === 'BOTTOM_UP' ? negativeRootsValue : postiveRootsValue, - unfilteredCumulativeValue, + unfilteredCumulativeValue: flamegraph.unfilteredCumulativeValue, minDepth, maxDepth, nodeActions, rootActions, }; } - -function getPivotFilter( - view: FlamegraphView, - makeFilterExpr: (x: string) => string[], -) { - if (view.kind === 'PIVOT') { - return makeFilterExpr(view.pivot).join(' OR '); - } - if (view.kind === 'BOTTOM_UP') { - return 'value > 0'; - } - return '0'; -} - -function computeGroupedAggExprs(agg: ReadonlyArray) { - const aggFor = (x: AggQueryFlamegraphColumn) => { - switch (x.mergeAggregation) { - case 'ONE_OR_SUMMARY': - return ` - ${x.name} || IIF( - COUNT(DISTINCT ${x.name}) = 1, - '', - ' ' || ' and ' || cast_string!(COUNT(DISTINCT ${x.name})) || ' others' - ) AS ${x.name} - `; - case 'SUM': - return `SUM(${x.name}) AS ${x.name}`; - case 'CONCAT_WITH_COMMA': - return `GROUP_CONCAT(${x.name}, ',') AS ${x.name}`; - } - }; - return `(${agg.length === 0 ? 'groupedColumn' : agg.map((x) => aggFor(x)).join(',')})`; -}