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