|  | // Copyright (C) 2020 The Android Open Source Project | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  |  | 
|  | import m from 'mithril'; | 
|  |  | 
|  | import {QueryResponse, runQuery} from '../common/queries'; | 
|  | import {raf} from '../core/raf_scheduler'; | 
|  | import {EngineProxy} from '../trace_processor/engine'; | 
|  |  | 
|  | import {globals} from './globals'; | 
|  | import {createPage} from './pages'; | 
|  |  | 
|  |  | 
|  | interface StatsSectionAttrs { | 
|  | title: string; | 
|  | subTitle: string; | 
|  | sqlConstraints: string; | 
|  | cssClass: string; | 
|  | queryId: string; | 
|  | } | 
|  |  | 
|  | function getEngine(name: string): EngineProxy|undefined { | 
|  | const currentEngine = globals.getCurrentEngine(); | 
|  | if (currentEngine === undefined) return undefined; | 
|  | const engineId = currentEngine.id; | 
|  | return globals.engines.get(engineId)?.getProxy(name); | 
|  | } | 
|  |  | 
|  | // Generic class that generate a <section> + <table> from the stats table. | 
|  | // The caller defines the query constraint, title and styling. | 
|  | // Used for errors, data losses and debugging sections. | 
|  | class StatsSection implements m.ClassComponent<StatsSectionAttrs> { | 
|  | private queryResponse?: QueryResponse; | 
|  |  | 
|  | constructor({attrs}: m.CVnode<StatsSectionAttrs>) { | 
|  | const engine = getEngine('StatsSection'); | 
|  | if (engine === undefined) { | 
|  | return; | 
|  | } | 
|  | const query = `select name, value, cast(ifnull(idx, '') as text) as idx, | 
|  | description, severity, source from stats | 
|  | where ${attrs.sqlConstraints || '1=1'} | 
|  | order by name, idx`; | 
|  | runQuery(query, engine).then((resp: QueryResponse) => { | 
|  | this.queryResponse = resp; | 
|  | raf.scheduleFullRedraw(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | view({attrs}: m.CVnode<StatsSectionAttrs>) { | 
|  | const resp = this.queryResponse; | 
|  | if (resp === undefined || resp.totalRowCount === 0) { | 
|  | return m(''); | 
|  | } | 
|  | if (resp.error) throw new Error(resp.error); | 
|  |  | 
|  | const tableRows = []; | 
|  | for (const row of resp.rows) { | 
|  | const help = []; | 
|  | if (row.description) { | 
|  | help.push(m('i.material-icons.contextual-help', 'help_outline')); | 
|  | } | 
|  | const idx = row.idx !== '' ? `[${row.idx}]` : ''; | 
|  | tableRows.push(m( | 
|  | 'tr', | 
|  | m('td.name', {title: row.description}, `${row.name}${idx}`, help), | 
|  | m('td', `${row.value}`), | 
|  | m('td', `${row.severity} (${row.source})`), | 
|  | )); | 
|  | } | 
|  |  | 
|  | return m( | 
|  | `section${attrs.cssClass}`, | 
|  | m('h2', attrs.title), | 
|  | m('h3', attrs.subTitle), | 
|  | m( | 
|  | 'table', | 
|  | m('thead', | 
|  | m('tr', m('td', 'Name'), m('td', 'Value'), m('td', 'Type'))), | 
|  | m('tbody', tableRows), | 
|  | ), | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | class MetricErrors implements m.ClassComponent { | 
|  | view() { | 
|  | if (!globals.metricError) return; | 
|  | return m( | 
|  | `section.errors`, | 
|  | m('h2', `Metric Errors`), | 
|  | m('h3', `One or more metrics were not computed successfully:`), | 
|  | m('div.metric-error', globals.metricError)); | 
|  | } | 
|  | } | 
|  |  | 
|  | class TraceMetadata implements m.ClassComponent { | 
|  | private queryResponse?: QueryResponse; | 
|  |  | 
|  | constructor() { | 
|  | const engine = getEngine('StatsSection'); | 
|  | if (engine === undefined) { | 
|  | return; | 
|  | } | 
|  | const query = `with | 
|  | metadata_with_priorities as (select | 
|  | name, ifnull(str_value, cast(int_value as text)) as value, | 
|  | name in ( | 
|  | "trace_size_bytes", | 
|  | "cr-os-arch", | 
|  | "cr-os-name", | 
|  | "cr-os-version", | 
|  | "cr-physical-memory", | 
|  | "cr-product-version", | 
|  | "cr-hardware-class" | 
|  | ) as priority | 
|  | from metadata | 
|  | ) | 
|  | select name, value | 
|  | from metadata_with_priorities | 
|  | order by priority desc, name`; | 
|  | runQuery(query, engine).then((resp: QueryResponse) => { | 
|  | this.queryResponse = resp; | 
|  | raf.scheduleFullRedraw(); | 
|  | }); | 
|  | } | 
|  |  | 
|  |  | 
|  | view() { | 
|  | const resp = this.queryResponse; | 
|  | if (resp === undefined || resp.totalRowCount === 0) { | 
|  | return m(''); | 
|  | } | 
|  |  | 
|  | const tableRows = []; | 
|  | for (const row of resp.rows) { | 
|  | tableRows.push(m( | 
|  | 'tr', | 
|  | m('td.name', `${row.name}`), | 
|  | m('td', `${row.value}`), | 
|  | )); | 
|  | } | 
|  |  | 
|  | return m( | 
|  | 'section', | 
|  | m('h2', 'System info and metadata'), | 
|  | m( | 
|  | 'table', | 
|  | m('thead', m('tr', m('td', 'Name'), m('td', 'Value'))), | 
|  | m('tbody', tableRows), | 
|  | ), | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | class AndroidGameInterventionList implements m.ClassComponent { | 
|  | private queryResponse?: QueryResponse; | 
|  |  | 
|  | constructor() { | 
|  | const engine = getEngine('StatsSection'); | 
|  | if (engine === undefined) { | 
|  | return; | 
|  | } | 
|  | const query = `select | 
|  | package_name, | 
|  | uid, | 
|  | current_mode, | 
|  | standard_mode_supported, | 
|  | standard_mode_downscale, | 
|  | standard_mode_use_angle, | 
|  | standard_mode_fps, | 
|  | perf_mode_supported, | 
|  | perf_mode_downscale, | 
|  | perf_mode_use_angle, | 
|  | perf_mode_fps, | 
|  | battery_mode_supported, | 
|  | battery_mode_downscale, | 
|  | battery_mode_use_angle, | 
|  | battery_mode_fps | 
|  | from android_game_intervention_list`; | 
|  | runQuery(query, engine).then((resp: QueryResponse) => { | 
|  | this.queryResponse = resp; | 
|  | raf.scheduleFullRedraw(); | 
|  | }); | 
|  | } | 
|  |  | 
|  |  | 
|  | view() { | 
|  | const resp = this.queryResponse; | 
|  | if (resp === undefined || resp.totalRowCount === 0) { | 
|  | return m(''); | 
|  | } | 
|  |  | 
|  | const tableRows = []; | 
|  | let standardInterventions = ''; | 
|  | let perfInterventions = ''; | 
|  | let batteryInterventions = ''; | 
|  | for (const row of resp.rows) { | 
|  | if (row.standard_mode_supported) { | 
|  | standardInterventions = | 
|  | `angle=${row.standard_mode_use_angle},downscale=${ | 
|  | row.standard_mode_downscale},fps=${row.standard_mode_fps}`; | 
|  | } else { | 
|  | standardInterventions = 'Not supported'; | 
|  | } | 
|  |  | 
|  | if (row.perf_mode_supported) { | 
|  | perfInterventions = `angle=${row.perf_mode_use_angle},downscale=${ | 
|  | row.perf_mode_downscale},fps=${row.perf_mode_fps}`; | 
|  | } else { | 
|  | perfInterventions = 'Not supported'; | 
|  | } | 
|  |  | 
|  | if (row.battery_mode_supported) { | 
|  | batteryInterventions = `angle=${row.battery_mode_use_angle},downscale=${ | 
|  | row.battery_mode_downscale},fps=${row.battery_mode_fps}`; | 
|  | } else { | 
|  | batteryInterventions = 'Not supported'; | 
|  | } | 
|  | // Game mode numbers are defined in | 
|  | // https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/app/GameManager.java;l=68 | 
|  | if (row.current_mode === 1) { | 
|  | row.current_mode = 'Standard'; | 
|  | } else if (row.current_mode === 2) { | 
|  | row.current_mode = 'Performance'; | 
|  | } else if (row.current_mode === 3) { | 
|  | row.current_mode = 'Battery'; | 
|  | } | 
|  | tableRows.push(m( | 
|  | 'tr', | 
|  | m('td.name', `${row.package_name}`), | 
|  | m('td', `${row.current_mode}`), | 
|  | m('td', standardInterventions), | 
|  | m('td', perfInterventions), | 
|  | m('td', batteryInterventions), | 
|  | )); | 
|  | } | 
|  |  | 
|  | return m( | 
|  | 'section', | 
|  | m('h2', 'Game Intervention List'), | 
|  | m( | 
|  | 'table', | 
|  | m('thead', | 
|  | m( | 
|  | 'tr', | 
|  | m('td', 'Name'), | 
|  | m('td', 'Current mode'), | 
|  | m('td', 'Standard mode interventions'), | 
|  | m('td', 'Performance mode interventions'), | 
|  | m('td', 'Battery mode interventions'), | 
|  | )), | 
|  | m('tbody', tableRows), | 
|  | ), | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | class PackageList implements m.ClassComponent { | 
|  | private queryResponse?: QueryResponse; | 
|  |  | 
|  | constructor() { | 
|  | const engine = getEngine('StatsSection'); | 
|  | if (engine === undefined) { | 
|  | return; | 
|  | } | 
|  | const query = `select package_name, version_code, debuggable, | 
|  | profileable_from_shell from package_list`; | 
|  | runQuery(query, engine).then((resp: QueryResponse) => { | 
|  | this.queryResponse = resp; | 
|  | raf.scheduleFullRedraw(); | 
|  | }); | 
|  | } | 
|  |  | 
|  |  | 
|  | view() { | 
|  | const resp = this.queryResponse; | 
|  | if (resp === undefined || resp.totalRowCount === 0) { | 
|  | return m(''); | 
|  | } | 
|  |  | 
|  | const tableRows = []; | 
|  | for (const row of resp.rows) { | 
|  | tableRows.push(m( | 
|  | 'tr', | 
|  | m('td.name', `${row.package_name}`), | 
|  | m('td', `${row.version_code}`), | 
|  | m('td', | 
|  | `${row.debuggable ? 'debuggable' : ''} ${ | 
|  | row.profileable_from_shell ? 'profileable' : ''}`), | 
|  | )); | 
|  | } | 
|  |  | 
|  | return m( | 
|  | 'section', | 
|  | m('h2', 'Package list'), | 
|  | m( | 
|  | 'table', | 
|  | m('thead', | 
|  | m('tr', | 
|  | m('td', 'Name'), | 
|  | m('td', 'Version code'), | 
|  | m('td', 'Flags'))), | 
|  | m('tbody', tableRows), | 
|  | ), | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | export const TraceInfoPage = createPage({ | 
|  | view() { | 
|  | return m( | 
|  | '.trace-info-page', | 
|  | m(MetricErrors), | 
|  | m(StatsSection, { | 
|  | queryId: 'info_errors', | 
|  | title: 'Import errors', | 
|  | cssClass: '.errors', | 
|  | subTitle: | 
|  | `The following errors have been encountered while importing the | 
|  | trace. These errors are usually non-fatal but indicate that one | 
|  | or more tracks might be missing or showing erroneous data.`, | 
|  | sqlConstraints: `severity = 'error' and value > 0`, | 
|  |  | 
|  | }), | 
|  | m(StatsSection, { | 
|  | queryId: 'info_data_losses', | 
|  | title: 'Data losses', | 
|  | cssClass: '.errors', | 
|  | subTitle: | 
|  | `These counters are collected at trace recording time. The trace | 
|  | data for one or more data sources was dropped and hence some | 
|  | track contents will be incomplete.`, | 
|  | sqlConstraints: `severity = 'data_loss' and value > 0`, | 
|  | }), | 
|  | m(TraceMetadata), | 
|  | m(PackageList), | 
|  | m(AndroidGameInterventionList), | 
|  | m(StatsSection, { | 
|  | queryId: 'info_all', | 
|  | title: 'Debugging stats', | 
|  | cssClass: '', | 
|  | subTitle: `Debugging statistics such as trace buffer usage and metrics | 
|  | coming from the TraceProcessor importer stages.`, | 
|  | sqlConstraints: '', | 
|  |  | 
|  | }), | 
|  | ); | 
|  | }, | 
|  | }); |