xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.TraceInfoPage/trace_info_page.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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 m from 'mithril';
16import {Engine, EngineAttrs} from '../../trace_processor/engine';
17import {QueryResult, UNKNOWN} from '../../trace_processor/query_result';
18import {assertExists} from '../../base/logging';
19import {TraceAttrs} from '../../public/trace';
20import {PageWithTraceAttrs} from '../../public/page';
21
22/**
23 * Extracts and copies fields from a source object based on the keys present in
24 * a spec object, effectively creating a new object that includes only the
25 * fields that are present in the spec object.
26 *
27 * @template S - A type representing the spec object, a subset of T.
28 * @template T - A type representing the source object, a superset of S.
29 *
30 * @param {T} source - The source object containing the full set of properties.
31 * @param {S} spec - The specification object whose keys determine which fields
32 * should be extracted from the source object.
33 *
34 * @returns {S} A new object containing only the fields from the source object
35 * that are also present in the specification object.
36 *
37 * @example
38 * const fullObject = { foo: 123, bar: '123', baz: true };
39 * const spec = { foo: 0, bar: '' };
40 * const result = pickFields(fullObject, spec);
41 * console.log(result); // Output: { foo: 123, bar: '123' }
42 */
43function pickFields<S extends Record<string, unknown>, T extends S>(
44  source: T,
45  spec: S,
46): S {
47  const result: Record<string, unknown> = {};
48  for (const key of Object.keys(spec)) {
49    result[key] = source[key];
50  }
51  return result as S;
52}
53
54interface StatsSectionAttrs {
55  engine: Engine;
56  title: string;
57  subTitle: string;
58  sqlConstraints: string;
59  cssClass: string;
60  queryId: string;
61}
62
63const statsSpec = {
64  name: UNKNOWN,
65  value: UNKNOWN,
66  description: UNKNOWN,
67  idx: UNKNOWN,
68  severity: UNKNOWN,
69  source: UNKNOWN,
70};
71
72type StatsSectionRow = typeof statsSpec;
73
74// Generic class that generate a <section> + <table> from the stats table.
75// The caller defines the query constraint, title and styling.
76// Used for errors, data losses and debugging sections.
77class StatsSection implements m.ClassComponent<StatsSectionAttrs> {
78  private data?: StatsSectionRow[];
79
80  constructor({attrs}: m.CVnode<StatsSectionAttrs>) {
81    const engine = attrs.engine;
82    if (engine === undefined) {
83      return;
84    }
85    const query = `
86      select
87        name,
88        value,
89        cast(ifnull(idx, '') as text) as idx,
90        description,
91        severity,
92        source from stats
93      where ${attrs.sqlConstraints || '1=1'}
94      order by name, idx
95    `;
96
97    engine.query(query).then((resp) => {
98      const data: StatsSectionRow[] = [];
99      const it = resp.iter(statsSpec);
100      for (; it.valid(); it.next()) {
101        data.push(pickFields(it, statsSpec));
102      }
103      this.data = data;
104    });
105  }
106
107  view({attrs}: m.CVnode<StatsSectionAttrs>) {
108    const data = this.data;
109    if (data === undefined || data.length === 0) {
110      return m('');
111    }
112
113    const tableRows = data.map((row) => {
114      const help = [];
115      if (Boolean(row.description)) {
116        help.push(m('i.material-icons.contextual-help', 'help_outline'));
117      }
118      const idx = row.idx !== '' ? `[${row.idx}]` : '';
119      return m(
120        'tr',
121        m('td.name', {title: row.description}, `${row.name}${idx}`, help),
122        m('td', `${row.value}`),
123        m('td', `${row.severity} (${row.source})`),
124      );
125    });
126
127    return m(
128      `section${attrs.cssClass}`,
129      m('h2', attrs.title),
130      m('h3', attrs.subTitle),
131      m(
132        'table',
133        m('thead', m('tr', m('td', 'Name'), m('td', 'Value'), m('td', 'Type'))),
134        m('tbody', tableRows),
135      ),
136    );
137  }
138}
139
140class LoadingErrors implements m.ClassComponent<TraceAttrs> {
141  view({attrs}: m.CVnode<TraceAttrs>) {
142    const errors = attrs.trace.loadingErrors;
143    if (errors.length === 0) return;
144    return m(
145      `section.errors`,
146      m('h2', `Loading errors`),
147      m('h3', `The following errors were encountered while loading the trace:`),
148      m('pre.metric-error', errors.join('\n')),
149    );
150  }
151}
152
153const traceMetadataRowSpec = {name: UNKNOWN, value: UNKNOWN};
154
155type TraceMetadataRow = typeof traceMetadataRowSpec;
156
157class TraceMetadata implements m.ClassComponent<EngineAttrs> {
158  private data?: TraceMetadataRow[];
159
160  oncreate({attrs}: m.CVnodeDOM<EngineAttrs>) {
161    const engine = attrs.engine;
162    const query = `
163      with metadata_with_priorities as (
164        select
165          name,
166          ifnull(str_value, cast(int_value as text)) as value,
167          name in (
168            "trace_size_bytes",
169            "cr-os-arch",
170            "cr-os-name",
171            "cr-os-version",
172            "cr-physical-memory",
173            "cr-product-version",
174            "cr-hardware-class"
175          ) as priority
176        from metadata
177      )
178      select
179        name,
180        value
181      from metadata_with_priorities
182      order by
183        priority desc,
184        name
185    `;
186
187    engine.query(query).then((resp: QueryResult) => {
188      const tableRows: TraceMetadataRow[] = [];
189      const it = resp.iter(traceMetadataRowSpec);
190      for (; it.valid(); it.next()) {
191        tableRows.push(pickFields(it, traceMetadataRowSpec));
192      }
193      this.data = tableRows;
194    });
195  }
196
197  view() {
198    const data = this.data;
199    if (data === undefined || data.length === 0) {
200      return m('');
201    }
202
203    const tableRows = data.map((row) => {
204      return m('tr', m('td.name', `${row.name}`), m('td', `${row.value}`));
205    });
206
207    return m(
208      'section',
209      m('h2', 'System info and metadata'),
210      m(
211        'table',
212        m('thead', m('tr', m('td', 'Name'), m('td', 'Value'))),
213        m('tbody', tableRows),
214      ),
215    );
216  }
217}
218
219const androidGameInterventionRowSpec = {
220  package_name: UNKNOWN,
221  uid: UNKNOWN,
222  current_mode: UNKNOWN,
223  standard_mode_supported: UNKNOWN,
224  standard_mode_downscale: UNKNOWN,
225  standard_mode_use_angle: UNKNOWN,
226  standard_mode_fps: UNKNOWN,
227  perf_mode_supported: UNKNOWN,
228  perf_mode_downscale: UNKNOWN,
229  perf_mode_use_angle: UNKNOWN,
230  perf_mode_fps: UNKNOWN,
231  battery_mode_supported: UNKNOWN,
232  battery_mode_downscale: UNKNOWN,
233  battery_mode_use_angle: UNKNOWN,
234  battery_mode_fps: UNKNOWN,
235};
236
237type AndroidGameInterventionRow = typeof androidGameInterventionRowSpec;
238
239class AndroidGameInterventionList implements m.ClassComponent<EngineAttrs> {
240  private data?: AndroidGameInterventionRow[];
241
242  oncreate({attrs}: m.CVnodeDOM<EngineAttrs>) {
243    const engine = attrs.engine;
244    const query = `
245      select
246        package_name,
247        uid,
248        current_mode,
249        standard_mode_supported,
250        standard_mode_downscale,
251        standard_mode_use_angle,
252        standard_mode_fps,
253        perf_mode_supported,
254        perf_mode_downscale,
255        perf_mode_use_angle,
256        perf_mode_fps,
257        battery_mode_supported,
258        battery_mode_downscale,
259        battery_mode_use_angle,
260        battery_mode_fps
261      from android_game_intervention_list
262    `;
263
264    engine.query(query).then((resp) => {
265      const data: AndroidGameInterventionRow[] = [];
266      const it = resp.iter(androidGameInterventionRowSpec);
267      for (; it.valid(); it.next()) {
268        data.push(pickFields(it, androidGameInterventionRowSpec));
269      }
270      this.data = data;
271    });
272  }
273
274  view() {
275    const data = this.data;
276    if (data === undefined || data.length === 0) {
277      return m('');
278    }
279
280    const tableRows = [];
281    let standardInterventions = '';
282    let perfInterventions = '';
283    let batteryInterventions = '';
284
285    for (const row of data) {
286      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
287      if (row.standard_mode_supported) {
288        standardInterventions = `angle=${row.standard_mode_use_angle},downscale=${row.standard_mode_downscale},fps=${row.standard_mode_fps}`;
289      } else {
290        standardInterventions = 'Not supported';
291      }
292
293      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
294      if (row.perf_mode_supported) {
295        perfInterventions = `angle=${row.perf_mode_use_angle},downscale=${row.perf_mode_downscale},fps=${row.perf_mode_fps}`;
296      } else {
297        perfInterventions = 'Not supported';
298      }
299
300      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
301      if (row.battery_mode_supported) {
302        batteryInterventions = `angle=${row.battery_mode_use_angle},downscale=${row.battery_mode_downscale},fps=${row.battery_mode_fps}`;
303      } else {
304        batteryInterventions = 'Not supported';
305      }
306      // Game mode numbers are defined in
307      // https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/app/GameManager.java;l=68
308      if (row.current_mode === 1) {
309        row.current_mode = 'Standard';
310      } else if (row.current_mode === 2) {
311        row.current_mode = 'Performance';
312      } else if (row.current_mode === 3) {
313        row.current_mode = 'Battery';
314      }
315      tableRows.push(
316        m(
317          'tr',
318          m('td.name', `${row.package_name}`),
319          m('td', `${row.current_mode}`),
320          m('td', standardInterventions),
321          m('td', perfInterventions),
322          m('td', batteryInterventions),
323        ),
324      );
325    }
326
327    return m(
328      'section',
329      m('h2', 'Game Intervention List'),
330      m(
331        'table',
332        m(
333          'thead',
334          m(
335            'tr',
336            m('td', 'Name'),
337            m('td', 'Current mode'),
338            m('td', 'Standard mode interventions'),
339            m('td', 'Performance mode interventions'),
340            m('td', 'Battery mode interventions'),
341          ),
342        ),
343        m('tbody', tableRows),
344      ),
345    );
346  }
347}
348
349const packageDataSpec = {
350  packageName: UNKNOWN,
351  versionCode: UNKNOWN,
352  debuggable: UNKNOWN,
353  profileableFromShell: UNKNOWN,
354};
355
356type PackageData = typeof packageDataSpec;
357
358class PackageListSection implements m.ClassComponent<EngineAttrs> {
359  private packageList?: PackageData[];
360
361  oncreate({attrs}: m.CVnodeDOM<EngineAttrs>) {
362    const engine = attrs.engine;
363    this.loadData(engine);
364  }
365
366  private async loadData(engine: Engine): Promise<void> {
367    const query = `
368      select
369        package_name as packageName,
370        version_code as versionCode,
371        debuggable,
372        profileable_from_shell as profileableFromShell
373      from package_list
374    `;
375
376    const packageList: PackageData[] = [];
377    const result = await engine.query(query);
378    const it = result.iter(packageDataSpec);
379    for (; it.valid(); it.next()) {
380      packageList.push(pickFields(it, packageDataSpec));
381    }
382
383    this.packageList = packageList;
384  }
385
386  view() {
387    const packageList = this.packageList;
388    if (packageList === undefined || packageList.length === 0) {
389      return undefined;
390    }
391
392    const tableRows = packageList.map((it) => {
393      return m(
394        'tr',
395        m('td.name', `${it.packageName}`),
396        m('td', `${it.versionCode}`),
397        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
398        m(
399          'td',
400          `${it.debuggable ? 'debuggable' : ''} ${
401            it.profileableFromShell ? 'profileable' : ''
402          }`,
403        ),
404        /* eslint-enable */
405      );
406    });
407
408    return m(
409      'section',
410      m('h2', 'Package list'),
411      m(
412        'table',
413        m(
414          'thead',
415          m('tr', m('td', 'Name'), m('td', 'Version code'), m('td', 'Flags')),
416        ),
417        m('tbody', tableRows),
418      ),
419    );
420  }
421}
422
423export class TraceInfoPage implements m.ClassComponent<PageWithTraceAttrs> {
424  private engine?: Engine;
425
426  oninit({attrs}: m.CVnode<PageWithTraceAttrs>) {
427    this.engine = attrs.trace.engine.getProxy('TraceInfoPage');
428  }
429
430  view({attrs}: m.CVnode<PageWithTraceAttrs>) {
431    const engine = assertExists(this.engine);
432    return m(
433      '.trace-info-page',
434      m(LoadingErrors, {trace: attrs.trace}),
435      m(StatsSection, {
436        engine,
437        queryId: 'info_errors',
438        title: 'Import errors',
439        cssClass: '.errors',
440        subTitle: `The following errors have been encountered while importing
441               the trace. These errors are usually non-fatal but indicate that
442               one or more tracks might be missing or showing erroneous data.`,
443        sqlConstraints: `severity = 'error' and value > 0`,
444      }),
445      m(StatsSection, {
446        engine,
447        queryId: 'info_data_losses',
448        title: 'Data losses',
449        cssClass: '.errors',
450        subTitle: `These counters are collected at trace recording time. The
451               trace data for one or more data sources was dropped and hence
452               some track contents will be incomplete.`,
453        sqlConstraints: `severity = 'data_loss' and value > 0`,
454      }),
455      m(TraceMetadata, {engine}),
456      m(PackageListSection, {engine}),
457      m(AndroidGameInterventionList, {engine}),
458      m(StatsSection, {
459        engine,
460        queryId: 'info_all',
461        title: 'Debugging stats',
462        cssClass: '',
463        subTitle: `Debugging statistics such as trace buffer usage and metrics
464                     coming from the TraceProcessor importer stages.`,
465        sqlConstraints: '',
466      }),
467    );
468  }
469}
470