1// Copyright (C) 2020 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 {ColumnDef, Sorting, ThreadStateExtra} from '../../public/aggregation';
16import {AreaSelection} from '../../public/selection';
17import {Engine} from '../../trace_processor/engine';
18import {
19  LONG,
20  NUM,
21  NUM_NULL,
22  STR,
23  STR_NULL,
24} from '../../trace_processor/query_result';
25import {AreaSelectionAggregator} from '../../public/selection';
26import {UnionDataset} from '../../trace_processor/dataset';
27import {translateState} from '../../components/sql_utils/thread_state';
28import {TrackDescriptor} from '../../public/track';
29
30export class ThreadStateSelectionAggregator implements AreaSelectionAggregator {
31  readonly id = 'thread_state_aggregation';
32
33  async createAggregateView(engine: Engine, area: AreaSelection) {
34    const dataset = this.getDatasetFromTracks(area.tracks);
35    if (dataset === undefined) return false;
36
37    await engine.query(`
38      create or replace perfetto table ${this.id} as
39      select
40        process.name as process_name,
41        process.pid,
42        thread.name as thread_name,
43        thread.tid,
44        tstate.state || ',' || ifnull(tstate.io_wait, 'NULL') as concat_state,
45        sum(tstate.dur) AS total_dur,
46        sum(tstate.dur) / count() as avg_dur,
47        count() as occurrences
48      from (${dataset.query()}) tstate
49      join thread using (utid)
50      left join process using (upid)
51      where
52        ts + dur > ${area.start}
53        and ts < ${area.end}
54      group by utid, concat_state
55    `);
56
57    return true;
58  }
59
60  async getExtra(
61    engine: Engine,
62    area: AreaSelection,
63  ): Promise<ThreadStateExtra | void> {
64    const dataset = this.getDatasetFromTracks(area.tracks);
65    if (dataset === undefined) return;
66
67    const query = `
68      select
69        state,
70        io_wait as ioWait,
71        sum(dur) as totalDur
72      from (${dataset.query()}) tstate
73      join thread using (utid)
74      where tstate.ts + tstate.dur > ${area.start}
75        and tstate.ts < ${area.end}
76      group by state, io_wait
77    `;
78    const result = await engine.query(query);
79
80    const it = result.iter({
81      state: STR_NULL,
82      ioWait: NUM_NULL,
83      totalDur: NUM,
84    });
85
86    let totalMs = 0;
87    const values = new Float64Array(result.numRows());
88    const states = [];
89    for (let i = 0; it.valid(); ++i, it.next()) {
90      const state = it.state == null ? undefined : it.state;
91      const ioWait = it.ioWait === null ? undefined : it.ioWait > 0;
92      states.push(translateState(state, ioWait));
93      const ms = it.totalDur / 1000000;
94      values[i] = ms;
95      totalMs += ms;
96    }
97    return {
98      kind: 'THREAD_STATE',
99      states,
100      values,
101      totalMs,
102    };
103  }
104
105  getColumnDefinitions(): ColumnDef[] {
106    return [
107      {
108        title: 'Process',
109        kind: 'STRING',
110        columnConstructor: Uint16Array,
111        columnId: 'process_name',
112      },
113      {
114        title: 'PID',
115        kind: 'NUMBER',
116        columnConstructor: Uint16Array,
117        columnId: 'pid',
118      },
119      {
120        title: 'Thread',
121        kind: 'STRING',
122        columnConstructor: Uint16Array,
123        columnId: 'thread_name',
124      },
125      {
126        title: 'TID',
127        kind: 'NUMBER',
128        columnConstructor: Uint16Array,
129        columnId: 'tid',
130      },
131      {
132        title: 'State',
133        kind: 'STATE',
134        columnConstructor: Uint16Array,
135        columnId: 'concat_state',
136      },
137      {
138        title: 'Wall duration (ms)',
139        kind: 'TIMESTAMP_NS',
140        columnConstructor: Float64Array,
141        columnId: 'total_dur',
142        sum: true,
143      },
144      {
145        title: 'Avg Wall duration (ms)',
146        kind: 'TIMESTAMP_NS',
147        columnConstructor: Float64Array,
148        columnId: 'avg_dur',
149      },
150      {
151        title: 'Occurrences',
152        kind: 'NUMBER',
153        columnConstructor: Uint16Array,
154        columnId: 'occurrences',
155        sum: true,
156      },
157    ];
158  }
159
160  getTabName() {
161    return 'Thread States';
162  }
163
164  getDefaultSorting(): Sorting {
165    return {column: 'total_dur', direction: 'DESC'};
166  }
167
168  // Creates an optimized dataset containing the thread state events within a
169  // given list of tracks, or returns undefined if no compatible tracks are
170  // present in the list.
171  private getDatasetFromTracks(tracks: ReadonlyArray<TrackDescriptor>) {
172    const desiredSchema = {
173      dur: LONG,
174      io_wait: NUM_NULL,
175      state: STR,
176      utid: NUM,
177    };
178    const validDatasets = tracks
179      .map((track) => track.track.getDataset?.())
180      .filter((ds) => ds !== undefined)
181      .filter((ds) => ds.implements(desiredSchema));
182    if (validDatasets.length === 0) {
183      return undefined;
184    }
185    return new UnionDataset(validDatasets).optimize();
186  }
187}
188