xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.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 {Time, duration, time} from '../../base/time';
16import {Engine} from '../../trace_processor/engine';
17import {Trace} from '../../public/trace';
18import {
19  LONG,
20  LONG_NULL,
21  NUM,
22  NUM_NULL,
23} from '../../trace_processor/query_result';
24import {TrackEventDetailsPanel} from '../../public/details_panel';
25import m from 'mithril';
26import {DetailsShell} from '../../widgets/details_shell';
27import {GridLayout} from '../../widgets/grid_layout';
28import {Section} from '../../widgets/section';
29import {Tree, TreeNode} from '../../widgets/tree';
30import {Timestamp} from '../../components/widgets/timestamp';
31import {DurationWidget} from '../../components/widgets/duration';
32import {TrackEventSelection} from '../../public/selection';
33import {hasArgs, renderArguments} from '../../components/details/slice_args';
34import {asArgSetId} from '../../components/sql_utils/core_types';
35import {Arg, getArgs} from '../../components/sql_utils/args';
36
37interface CounterDetails {
38  // The "left" timestamp of the counter sample T(N)
39  ts: time;
40
41  // The delta between this sample and the next one's timestamps T(N+1) - T(N)
42  duration: duration;
43
44  // The value of the counter sample F(N)
45  value: number;
46
47  // The delta between this sample's value and the previous one F(N) - F(N-1)
48  delta: number;
49
50  args?: Arg[];
51}
52
53export class CounterDetailsPanel implements TrackEventDetailsPanel {
54  private readonly trace: Trace;
55  private readonly engine: Engine;
56  private readonly trackId: number;
57  private readonly rootTable: string;
58  private readonly trackName: string;
59  private counterDetails?: CounterDetails;
60
61  constructor(
62    trace: Trace,
63    trackId: number,
64    trackName: string,
65    rootTable = 'counter',
66  ) {
67    this.trace = trace;
68    this.engine = trace.engine;
69    this.trackId = trackId;
70    this.trackName = trackName;
71    this.rootTable = rootTable;
72  }
73
74  async load({eventId}: TrackEventSelection) {
75    this.counterDetails = await loadCounterDetails(
76      this.engine,
77      this.trackId,
78      eventId,
79      this.rootTable,
80    );
81  }
82
83  render() {
84    const counterInfo = this.counterDetails;
85    if (counterInfo) {
86      const args =
87        hasArgs(counterInfo.args) &&
88        m(
89          Section,
90          {title: 'Arguments'},
91          m(Tree, renderArguments(this.trace, counterInfo.args)),
92        );
93
94      return m(
95        DetailsShell,
96        {title: 'Counter', description: `${this.trackName}`},
97        m(
98          GridLayout,
99          m(
100            Section,
101            {title: 'Properties'},
102            m(
103              Tree,
104              m(TreeNode, {left: 'Name', right: `${this.trackName}`}),
105              m(TreeNode, {
106                left: 'Start time',
107                right: m(Timestamp, {ts: counterInfo.ts}),
108              }),
109              m(TreeNode, {
110                left: 'Value',
111                right: `${counterInfo.value.toLocaleString()}`,
112              }),
113              m(TreeNode, {
114                left: 'Delta',
115                right: `${counterInfo.delta.toLocaleString()}`,
116              }),
117              m(TreeNode, {
118                left: 'Duration',
119                right: m(DurationWidget, {dur: counterInfo.duration}),
120              }),
121            ),
122          ),
123          args,
124        ),
125      );
126    } else {
127      return m(DetailsShell, {title: 'Counter', description: 'Loading...'});
128    }
129  }
130
131  isLoading(): boolean {
132    return this.counterDetails === undefined;
133  }
134}
135
136async function loadCounterDetails(
137  engine: Engine,
138  trackId: number,
139  id: number,
140  rootTable: string,
141): Promise<CounterDetails> {
142  const query = `
143    WITH CTE AS (
144      SELECT
145        id,
146        ts as leftTs,
147        value,
148        LAG(value) OVER (ORDER BY ts) AS prevValue,
149        LEAD(ts) OVER (ORDER BY ts) AS rightTs,
150        arg_set_id AS argSetId
151      FROM ${rootTable}
152      WHERE track_id = ${trackId}
153    )
154    SELECT * FROM CTE WHERE id = ${id}
155  `;
156
157  const counter = await engine.query(query);
158  const row = counter.iter({
159    value: NUM,
160    prevValue: NUM_NULL,
161    leftTs: LONG,
162    rightTs: LONG_NULL,
163    argSetId: NUM_NULL,
164  });
165  const value = row.value;
166  const leftTs = Time.fromRaw(row.leftTs);
167  const rightTs = row.rightTs !== null ? Time.fromRaw(row.rightTs) : leftTs;
168  const prevValue = row.prevValue !== null ? row.prevValue : value;
169
170  const delta = value - prevValue;
171  const duration = rightTs - leftTs;
172  const argSetId = row.argSetId;
173  const args =
174    argSetId == null ? undefined : await getArgs(engine, asArgSetId(argSetId));
175  return {ts: leftTs, value, delta, duration, args};
176}
177