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 {NUM} from '../../trace_processor/query_result';
17import {Slice} from '../../public/track';
18import {
19  BaseSliceTrack,
20  OnSliceClickArgs,
21} from '../../components/tracks/base_slice_track';
22import {NAMED_ROW, NamedRow} from '../../components/tracks/named_slice_track';
23import {getColorForSample} from '../../components/colorizer';
24import {
25  ProfileType,
26  TrackEventDetails,
27  TrackEventSelection,
28} from '../../public/selection';
29import {assertExists} from '../../base/logging';
30import {
31  metricsFromTableOrSubquery,
32  QueryFlamegraph,
33} from '../../components/query_flamegraph';
34import {DetailsShell} from '../../widgets/details_shell';
35import {Timestamp} from '../../components/widgets/timestamp';
36import {time} from '../../base/time';
37import {TrackEventDetailsPanel} from '../../public/details_panel';
38import {Flamegraph, FLAMEGRAPH_STATE_SCHEMA} from '../../widgets/flamegraph';
39import {Trace} from '../../public/trace';
40
41interface PerfSampleRow extends NamedRow {
42  callsiteId: number;
43}
44
45abstract class BasePerfSamplesProfileTrack extends BaseSliceTrack<
46  Slice,
47  PerfSampleRow
48> {
49  constructor(trace: Trace, uri: string) {
50    super(trace, uri);
51  }
52
53  protected getRowSpec(): PerfSampleRow {
54    return {...NAMED_ROW, callsiteId: NUM};
55  }
56
57  protected rowToSlice(row: PerfSampleRow): Slice {
58    const baseSlice = super.rowToSliceBase(row);
59    const name = assertExists(row.name);
60    const colorScheme = getColorForSample(row.callsiteId);
61    return {...baseSlice, title: name, colorScheme};
62  }
63
64  onUpdatedSlices(slices: Slice[]) {
65    for (const slice of slices) {
66      slice.isHighlighted = slice === this.hoveredSlice;
67    }
68  }
69
70  onSliceClick(args: OnSliceClickArgs<Slice>): void {
71    // TODO(stevegolton): Perhaps we could just move this to BaseSliceTrack?
72    this.trace.selection.selectTrackEvent(this.uri, args.slice.id);
73  }
74}
75
76export class ProcessPerfSamplesProfileTrack extends BasePerfSamplesProfileTrack {
77  constructor(
78    trace: Trace,
79    uri: string,
80    private readonly upid: number,
81  ) {
82    super(trace, uri);
83  }
84
85  getSqlSource(): string {
86    return `
87      select
88        p.id,
89        ts,
90        0 as dur,
91        0 as depth,
92        'Perf Sample' as name,
93        callsite_id as callsiteId
94      from perf_sample p
95      join thread using (utid)
96      where upid = ${this.upid} and callsite_id is not null
97      order by ts
98    `;
99  }
100
101  async getSelectionDetails(
102    id: number,
103  ): Promise<TrackEventDetails | undefined> {
104    const details = await super.getSelectionDetails(id);
105    if (details === undefined) return undefined;
106    return {
107      ...details,
108      upid: this.upid,
109      profileType: ProfileType.PERF_SAMPLE,
110    };
111  }
112
113  detailsPanel(sel: TrackEventSelection) {
114    const upid = assertExists(sel.upid);
115    const ts = sel.ts;
116
117    const metrics = metricsFromTableOrSubquery(
118      `
119        (
120          select
121            id,
122            parent_id as parentId,
123            name,
124            mapping_name,
125            source_file,
126            cast(line_number AS text) as line_number,
127            self_count
128          from _callstacks_for_callsites!((
129            select p.callsite_id
130            from perf_sample p
131            join thread t using (utid)
132            where p.ts >= ${ts}
133              and p.ts <= ${ts}
134              and t.upid = ${upid}
135          ))
136        )
137      `,
138      [
139        {
140          name: 'Perf Samples',
141          unit: '',
142          columnName: 'self_count',
143        },
144      ],
145      'include perfetto module linux.perf.samples',
146      [{name: 'mapping_name', displayName: 'Mapping'}],
147      [
148        {
149          name: 'source_file',
150          displayName: 'Source File',
151          mergeAggregation: 'ONE_OR_NULL',
152        },
153        {
154          name: 'line_number',
155          displayName: 'Line Number',
156          mergeAggregation: 'ONE_OR_NULL',
157        },
158      ],
159    );
160    const serialization = {
161      schema: FLAMEGRAPH_STATE_SCHEMA,
162      state: Flamegraph.createDefaultState(metrics),
163    };
164    const flamegraph = new QueryFlamegraph(this.trace, metrics, serialization);
165    return {
166      render: () => renderDetailsPanel(flamegraph, ts),
167      serialization,
168    };
169  }
170}
171
172export class ThreadPerfSamplesProfileTrack extends BasePerfSamplesProfileTrack {
173  constructor(
174    trace: Trace,
175    uri: string,
176    private readonly utid: number,
177  ) {
178    super(trace, uri);
179  }
180
181  getSqlSource(): string {
182    return `
183      select
184        p.id,
185        ts,
186        0 as dur,
187        0 as depth,
188        'Perf Sample' as name,
189        callsite_id as callsiteId
190      from perf_sample p
191      where utid = ${this.utid} and callsite_id is not null
192      order by ts
193    `;
194  }
195
196  async getSelectionDetails(
197    id: number,
198  ): Promise<TrackEventDetails | undefined> {
199    const details = await super.getSelectionDetails(id);
200    if (details === undefined) return undefined;
201    return {
202      ...details,
203      utid: this.utid,
204      profileType: ProfileType.PERF_SAMPLE,
205    };
206  }
207
208  detailsPanel(sel: TrackEventSelection): TrackEventDetailsPanel {
209    const utid = assertExists(sel.utid);
210    const ts = sel.ts;
211
212    const metrics = metricsFromTableOrSubquery(
213      `
214        (
215          select
216            id,
217            parent_id as parentId,
218            name,
219            mapping_name,
220            source_file,
221            cast(line_number AS text) as line_number,
222            self_count
223          from _callstacks_for_callsites!((
224            select p.callsite_id
225            from perf_sample p
226            where p.ts >= ${ts}
227              and p.ts <= ${ts}
228              and p.utid = ${utid}
229          ))
230        )
231      `,
232      [
233        {
234          name: 'Perf Samples',
235          unit: '',
236          columnName: 'self_count',
237        },
238      ],
239      'include perfetto module linux.perf.samples',
240      [{name: 'mapping_name', displayName: 'Mapping'}],
241      [
242        {
243          name: 'source_file',
244          displayName: 'Source File',
245          mergeAggregation: 'ONE_OR_NULL',
246        },
247        {
248          name: 'line_number',
249          displayName: 'Line Number',
250          mergeAggregation: 'ONE_OR_NULL',
251        },
252      ],
253    );
254    const serialization = {
255      schema: FLAMEGRAPH_STATE_SCHEMA,
256      state: Flamegraph.createDefaultState(metrics),
257    };
258    const flamegraph = new QueryFlamegraph(this.trace, metrics, serialization);
259    return {
260      render: () => renderDetailsPanel(flamegraph, ts),
261      serialization,
262    };
263  }
264}
265
266function renderDetailsPanel(flamegraph: QueryFlamegraph, ts: time) {
267  return m(
268    '.flamegraph-profile',
269    m(
270      DetailsShell,
271      {
272        fillParent: true,
273        title: m('.title', 'Perf Samples'),
274        description: [],
275        buttons: [
276          m(
277            'div.time',
278            `First timestamp: `,
279            m(Timestamp, {
280              ts,
281            }),
282          ),
283          m(
284            'div.time',
285            `Last timestamp: `,
286            m(Timestamp, {
287              ts,
288            }),
289          ),
290        ],
291      },
292      flamegraph.render(),
293    ),
294  );
295}
296