xref: /aosp_15_r20/external/perfetto/ui/src/components/query_flamegraph.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 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
15import m from 'mithril';
16import {AsyncLimiter} from '../base/async_limiter';
17import {AsyncDisposableStack} from '../base/disposable_stack';
18import {assertExists} from '../base/logging';
19import {Monitor} from '../base/monitor';
20import {uuidv4Sql} from '../base/uuid';
21import {Engine} from '../trace_processor/engine';
22import {
23  createPerfettoIndex,
24  createPerfettoTable,
25} from '../trace_processor/sql_utils';
26import {
27  NUM,
28  NUM_NULL,
29  STR,
30  STR_NULL,
31  UNKNOWN,
32} from '../trace_processor/query_result';
33import {
34  Flamegraph,
35  FlamegraphQueryData,
36  FlamegraphState,
37  FlamegraphView,
38} from '../widgets/flamegraph';
39import {Trace} from '../public/trace';
40
41export interface QueryFlamegraphColumn {
42  // The name of the column in SQL.
43  readonly name: string;
44
45  // The human readable name describing the contents of the column.
46  readonly displayName: string;
47}
48
49export interface AggQueryFlamegraphColumn extends QueryFlamegraphColumn {
50  // The aggregation to be run when nodes are merged together in the flamegraph.
51  //
52  // TODO(lalitm): consider adding extra functions here (e.g. a top 5 or similar).
53  readonly mergeAggregation: 'ONE_OR_NULL' | 'SUM';
54}
55
56export interface QueryFlamegraphMetric {
57  // The human readable name of the metric: will be shown to the user to change
58  // between metrics.
59  readonly name: string;
60
61  // The human readable SI-style unit of `selfValue`. Values will be shown to
62  // the user suffixed with this.
63  readonly unit: string;
64
65  // SQL statement which need to be run in preparation for being able to execute
66  // `statement`.
67  readonly dependencySql?: string;
68
69  // A single SQL statement which returns the columns `id`, `parentId`, `name`
70  // `selfValue`, all columns specified by `unaggregatableProperties` and
71  // `aggregatableProperties`.
72  readonly statement: string;
73
74  // Additional contextual columns containing data which should not be merged
75  // between sibling nodes, even if they have the same name.
76  //
77  // Examples include the mapping that a name comes from, the heap graph root
78  // type etc.
79  //
80  // Note: the name is always unaggregatable and should not be specified here.
81  readonly unaggregatableProperties?: ReadonlyArray<QueryFlamegraphColumn>;
82
83  // Additional contextual columns containing data which will be displayed to
84  // the user if there is no merging. If there is merging, currently the value
85  // will not be shown.
86  //
87  // Examples include the source file and line number.
88  readonly aggregatableProperties?: ReadonlyArray<AggQueryFlamegraphColumn>;
89}
90
91export interface QueryFlamegraphState {
92  state: FlamegraphState;
93}
94
95// Given a table and columns on those table (corresponding to metrics),
96// returns an array of `QueryFlamegraphMetric` structs which can be passed
97// in QueryFlamegraph's attrs.
98//
99// `tableOrSubquery` should have the columns `id`, `parentId`, `name` and all
100// columns specified by `tableMetrics[].name`, `unaggregatableProperties` and
101// `aggregatableProperties`.
102export function metricsFromTableOrSubquery(
103  tableOrSubquery: string,
104  tableMetrics: ReadonlyArray<{name: string; unit: string; columnName: string}>,
105  dependencySql?: string,
106  unaggregatableProperties?: ReadonlyArray<QueryFlamegraphColumn>,
107  aggregatableProperties?: ReadonlyArray<AggQueryFlamegraphColumn>,
108): QueryFlamegraphMetric[] {
109  const metrics = [];
110  for (const {name, unit, columnName} of tableMetrics) {
111    metrics.push({
112      name,
113      unit,
114      dependencySql,
115      statement: `
116        select *, ${columnName} as value
117        from ${tableOrSubquery}
118      `,
119      unaggregatableProperties,
120      aggregatableProperties,
121    });
122  }
123  return metrics;
124}
125
126// A Perfetto UI component which wraps the `Flamegraph` widget and fetches the
127// data for the widget by querying an `Engine`.
128export class QueryFlamegraph {
129  private data?: FlamegraphQueryData;
130  private readonly selMonitor = new Monitor([() => this.state.state]);
131  private readonly queryLimiter = new AsyncLimiter();
132
133  constructor(
134    private readonly trace: Trace,
135    private readonly metrics: ReadonlyArray<QueryFlamegraphMetric>,
136    private state: QueryFlamegraphState,
137  ) {}
138
139  render() {
140    if (this.selMonitor.ifStateChanged()) {
141      const metric = assertExists(
142        this.metrics.find(
143          (x) => this.state.state.selectedMetricName === x.name,
144        ),
145      );
146      const engine = this.trace.engine;
147      const state = this.state;
148      this.data = undefined;
149      this.queryLimiter.schedule(async () => {
150        this.data = undefined;
151        this.data = await computeFlamegraphTree(engine, metric, state.state);
152      });
153    }
154    return m(Flamegraph, {
155      metrics: this.metrics,
156      data: this.data,
157      state: this.state.state,
158      onStateChange: (state) => {
159        this.state.state = state;
160        this.trace.scheduleFullRedraw();
161      },
162    });
163  }
164}
165
166async function computeFlamegraphTree(
167  engine: Engine,
168  {
169    dependencySql,
170    statement,
171    unaggregatableProperties,
172    aggregatableProperties,
173  }: QueryFlamegraphMetric,
174  {filters, view}: FlamegraphState,
175): Promise<FlamegraphQueryData> {
176  const showStack = filters
177    .filter((x) => x.kind === 'SHOW_STACK')
178    .map((x) => x.filter);
179  const hideStack = filters
180    .filter((x) => x.kind === 'HIDE_STACK')
181    .map((x) => x.filter);
182  const showFromFrame = filters
183    .filter((x) => x.kind === 'SHOW_FROM_FRAME')
184    .map((x) => x.filter);
185  const hideFrame = filters
186    .filter((x) => x.kind === 'HIDE_FRAME')
187    .map((x) => x.filter);
188
189  // Pivot also essentially acts as a "show stack" filter so treat it like one.
190  const showStackAndPivot = [...showStack];
191  if (view.kind === 'PIVOT') {
192    showStackAndPivot.push(view.pivot);
193  }
194
195  const showStackFilter =
196    showStackAndPivot.length === 0
197      ? '0'
198      : showStackAndPivot
199          .map(
200            (x, i) => `((name like '${makeSqlFilter(x)}' escape '\\') << ${i})`,
201          )
202          .join(' | ');
203  const showStackBits = (1 << showStackAndPivot.length) - 1;
204
205  const hideStackFilter =
206    hideStack.length === 0
207      ? 'false'
208      : hideStack
209          .map((x) => `name like '${makeSqlFilter(x)}' escape '\\'`)
210          .join(' OR ');
211
212  const showFromFrameFilter =
213    showFromFrame.length === 0
214      ? '0'
215      : showFromFrame
216          .map(
217            (x, i) => `((name like '${makeSqlFilter(x)}' escape '\\') << ${i})`,
218          )
219          .join(' | ');
220  const showFromFrameBits = (1 << showFromFrame.length) - 1;
221
222  const hideFrameFilter =
223    hideFrame.length === 0
224      ? 'false'
225      : hideFrame
226          .map((x) => `name like '${makeSqlFilter(x)}' escape '\\'`)
227          .join(' OR ');
228
229  const pivotFilter = getPivotFilter(view);
230
231  const unagg = unaggregatableProperties ?? [];
232  const unaggCols = unagg.map((x) => x.name);
233
234  const agg = aggregatableProperties ?? [];
235  const aggCols = agg.map((x) => x.name);
236
237  const groupingColumns = `(${(unaggCols.length === 0 ? ['groupingColumn'] : unaggCols).join()})`;
238  const groupedColumns = `(${(aggCols.length === 0 ? ['groupedColumn'] : aggCols).join()})`;
239
240  if (dependencySql !== undefined) {
241    await engine.query(dependencySql);
242  }
243  await engine.query(`include perfetto module viz.flamegraph;`);
244
245  const uuid = uuidv4Sql();
246  await using disposable = new AsyncDisposableStack();
247
248  disposable.use(
249    await createPerfettoTable(
250      engine,
251      `_flamegraph_materialized_statement_${uuid}`,
252      statement,
253    ),
254  );
255  disposable.use(
256    await createPerfettoIndex(
257      engine,
258      `_flamegraph_materialized_statement_${uuid}_index`,
259      `_flamegraph_materialized_statement_${uuid}(parentId)`,
260    ),
261  );
262
263  // TODO(lalitm): this doesn't need to be called unless we have
264  // a non-empty set of filters.
265  disposable.use(
266    await createPerfettoTable(
267      engine,
268      `_flamegraph_source_${uuid}`,
269      `
270        select *
271        from _viz_flamegraph_prepare_filter!(
272          (
273            select
274              s.id,
275              s.parentId,
276              s.name,
277              s.value,
278              ${(unaggCols.length === 0
279                ? [`'' as groupingColumn`]
280                : unaggCols.map((x) => `s.${x}`)
281              ).join()},
282              ${(aggCols.length === 0
283                ? [`'' as groupedColumn`]
284                : aggCols.map((x) => `s.${x}`)
285              ).join()}
286            from _flamegraph_materialized_statement_${uuid} s
287          ),
288          (${showStackFilter}),
289          (${hideStackFilter}),
290          (${showFromFrameFilter}),
291          (${hideFrameFilter}),
292          (${pivotFilter}),
293          ${1 << showStackAndPivot.length},
294          ${groupingColumns}
295        )
296      `,
297    ),
298  );
299  // TODO(lalitm): this doesn't need to be called unless we have
300  // a non-empty set of filters.
301  disposable.use(
302    await createPerfettoTable(
303      engine,
304      `_flamegraph_filtered_${uuid}`,
305      `
306        select *
307        from _viz_flamegraph_filter_frames!(
308          _flamegraph_source_${uuid},
309          ${showFromFrameBits}
310        )
311      `,
312    ),
313  );
314  disposable.use(
315    await createPerfettoTable(
316      engine,
317      `_flamegraph_accumulated_${uuid}`,
318      `
319        select *
320        from _viz_flamegraph_accumulate!(
321          _flamegraph_filtered_${uuid},
322          ${showStackBits}
323        )
324      `,
325    ),
326  );
327  disposable.use(
328    await createPerfettoTable(
329      engine,
330      `_flamegraph_hash_${uuid}`,
331      `
332        select *
333        from _viz_flamegraph_downwards_hash!(
334          _flamegraph_source_${uuid},
335          _flamegraph_filtered_${uuid},
336          _flamegraph_accumulated_${uuid},
337          ${groupingColumns},
338          ${groupedColumns},
339          ${view.kind === 'BOTTOM_UP' ? 'FALSE' : 'TRUE'}
340        )
341        union all
342        select *
343        from _viz_flamegraph_upwards_hash!(
344          _flamegraph_source_${uuid},
345          _flamegraph_filtered_${uuid},
346          _flamegraph_accumulated_${uuid},
347          ${groupingColumns},
348          ${groupedColumns}
349        )
350        order by hash
351      `,
352    ),
353  );
354  disposable.use(
355    await createPerfettoTable(
356      engine,
357      `_flamegraph_merged_${uuid}`,
358      `
359        select *
360        from _viz_flamegraph_merge_hashes!(
361          _flamegraph_hash_${uuid},
362          ${groupingColumns},
363          ${computeGroupedAggExprs(agg)}
364        )
365      `,
366    ),
367  );
368  disposable.use(
369    await createPerfettoTable(
370      engine,
371      `_flamegraph_layout_${uuid}`,
372      `
373        select *
374        from _viz_flamegraph_local_layout!(
375          _flamegraph_merged_${uuid}
376        );
377      `,
378    ),
379  );
380  const res = await engine.query(`
381    select *
382    from _viz_flamegraph_global_layout!(
383      _flamegraph_merged_${uuid},
384      _flamegraph_layout_${uuid},
385      ${groupingColumns},
386      ${groupedColumns}
387    )
388  `);
389
390  const it = res.iter({
391    id: NUM,
392    parentId: NUM,
393    depth: NUM,
394    name: STR,
395    selfValue: NUM,
396    cumulativeValue: NUM,
397    parentCumulativeValue: NUM_NULL,
398    xStart: NUM,
399    xEnd: NUM,
400    ...Object.fromEntries(unaggCols.map((m) => [m, STR_NULL])),
401    ...Object.fromEntries(aggCols.map((m) => [m, UNKNOWN])),
402  });
403  let postiveRootsValue = 0;
404  let negativeRootsValue = 0;
405  let minDepth = 0;
406  let maxDepth = 0;
407  const nodes = [];
408  for (; it.valid(); it.next()) {
409    const properties = new Map<string, string>();
410    for (const a of [...agg, ...unagg]) {
411      const r = it.get(a.name);
412      if (r !== null) {
413        properties.set(a.displayName, r as string);
414      }
415    }
416    nodes.push({
417      id: it.id,
418      parentId: it.parentId,
419      depth: it.depth,
420      name: it.name,
421      selfValue: it.selfValue,
422      cumulativeValue: it.cumulativeValue,
423      parentCumulativeValue: it.parentCumulativeValue ?? undefined,
424      xStart: it.xStart,
425      xEnd: it.xEnd,
426      properties,
427    });
428    if (it.depth === 1) {
429      postiveRootsValue += it.cumulativeValue;
430    } else if (it.depth === -1) {
431      negativeRootsValue += it.cumulativeValue;
432    }
433    minDepth = Math.min(minDepth, it.depth);
434    maxDepth = Math.max(maxDepth, it.depth);
435  }
436  const sumQuery = await engine.query(
437    `select sum(value) v from _flamegraph_source_${uuid}`,
438  );
439  const unfilteredCumulativeValue = sumQuery.firstRow({v: NUM_NULL}).v ?? 0;
440  return {
441    nodes,
442    allRootsCumulativeValue:
443      view.kind === 'BOTTOM_UP' ? negativeRootsValue : postiveRootsValue,
444    unfilteredCumulativeValue,
445    minDepth,
446    maxDepth,
447  };
448}
449
450function makeSqlFilter(x: string) {
451  if (x.startsWith('^') && x.endsWith('$')) {
452    return x.slice(1, -1);
453  }
454  return `%${x}%`;
455}
456
457function getPivotFilter(view: FlamegraphView) {
458  if (view.kind === 'PIVOT') {
459    return `name like '${makeSqlFilter(view.pivot)}'`;
460  }
461  if (view.kind === 'BOTTOM_UP') {
462    return 'value > 0';
463  }
464  return '0';
465}
466
467function computeGroupedAggExprs(agg: ReadonlyArray<AggQueryFlamegraphColumn>) {
468  const aggFor = (x: AggQueryFlamegraphColumn) => {
469    switch (x.mergeAggregation) {
470      case 'ONE_OR_NULL':
471        return `IIF(COUNT() = 1, ${x.name}, NULL) AS ${x.name}`;
472      case 'SUM':
473        return `SUM(${x.name}) AS ${x.name}`;
474    }
475  };
476  return `(${agg.length === 0 ? 'groupedColumn' : agg.map((x) => aggFor(x)).join(',')})`;
477}
478