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