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