| // Copyright (C) 2025 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 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 type {Engine} from '../../../trace_processor/engine'; |
| import {NUM_NULL, UNKNOWN} from '../../../trace_processor/query_result'; |
| import {Button} from '../../../widgets/button'; |
| import {Section} from '../../../widgets/section'; |
| import {Icon} from '../../../widgets/icon'; |
| import {Tooltip} from '../../../widgets/tooltip'; |
| import {statsSpec, type StatsSectionRow} from '../utils'; |
| |
| // Trace metadata row spec and type |
| const traceMetadataRowSpec = { |
| name: UNKNOWN, |
| value: UNKNOWN, |
| machineId: NUM_NULL, |
| traceId: NUM_NULL, |
| }; |
| type TraceMetadataRow = typeof traceMetadataRowSpec; |
| |
| export interface StatsData { |
| traceMetadata: TraceMetadataRow[]; |
| allStats: StatsSectionRow[]; |
| isMultiTrace: boolean; |
| isMultiMachine: boolean; |
| } |
| |
| export async function loadStatsData(engine: Engine): Promise<StatsData> { |
| // Load all stats |
| const allStatsResult = await engine.query(` |
| select |
| name, |
| value, |
| cast(ifnull(idx, '') as text) as idx, |
| description, |
| severity, |
| source, |
| machine_id as machineId, |
| trace_id as traceId |
| from stats |
| order by trace_id, machine_id, name, idx |
| `); |
| const allStats: StatsSectionRow[] = []; |
| for (const iter = allStatsResult.iter(statsSpec); iter.valid(); iter.next()) { |
| allStats.push({ |
| name: iter.name, |
| value: iter.value, |
| description: iter.description, |
| idx: iter.idx, |
| severity: iter.severity, |
| source: iter.source, |
| machineId: iter.machineId, |
| traceId: iter.traceId, |
| }); |
| } |
| |
| // Load trace metadata |
| const traceMetadataResult = await engine.query(` |
| with metadata_with_priorities as ( |
| select |
| name, |
| ifnull(str_value, cast(int_value as text)) as value, |
| machine_id, |
| trace_id, |
| 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, |
| machine_id as machineId, |
| trace_id as traceId |
| from metadata_with_priorities |
| order by |
| trace_id, |
| machine_id, |
| priority desc, |
| name |
| `); |
| |
| // Load trace and machine info |
| const traceIds = new Set<number>(); |
| const machineIds = new Set<number>(); |
| |
| const traceMetadata: TraceMetadataRow[] = []; |
| for ( |
| const iter = traceMetadataResult.iter(traceMetadataRowSpec); |
| iter.valid(); |
| iter.next() |
| ) { |
| if (iter.machineId !== null) { |
| machineIds.add(iter.machineId); |
| } |
| if (iter.traceId !== null) { |
| traceIds.add(iter.traceId); |
| } |
| traceMetadata.push({ |
| name: iter.name, |
| value: iter.value, |
| machineId: iter.machineId, |
| traceId: iter.traceId, |
| }); |
| } |
| |
| // Ensure every machine_id / trace_id observed in the stats rows is |
| // covered when deciding whether to show the per-machine/per-trace |
| // columns — the metadata table alone may not have seen all of them. |
| for (const stat of allStats) { |
| if (stat.machineId !== null) { |
| machineIds.add(stat.machineId); |
| } |
| if (stat.traceId !== null) { |
| traceIds.add(stat.traceId); |
| } |
| } |
| |
| return { |
| traceMetadata, |
| allStats, |
| isMultiTrace: traceIds.size > 1, |
| isMultiMachine: machineIds.size > 1, |
| }; |
| } |
| |
| export interface StatsTabAttrs { |
| data: StatsData; |
| } |
| |
| export class StatsTab implements m.ClassComponent<StatsTabAttrs> { |
| view({attrs}: m.CVnode<StatsTabAttrs>) { |
| return m( |
| '.pf-trace-info-page__tab-content', |
| m( |
| Section, |
| { |
| title: 'Trace Metadata', |
| subtitle: 'All metadata key-value pairs recorded in the trace', |
| }, |
| m(TraceMetadata, { |
| data: attrs.data.traceMetadata, |
| isMultiTrace: attrs.data.isMultiTrace, |
| isMultiMachine: attrs.data.isMultiMachine, |
| }), |
| ), |
| m( |
| Section, |
| { |
| title: 'Statistics', |
| subtitle: |
| 'Complete dump of all trace statistics including errors, data losses, and debugging info', |
| }, |
| m(StatsSection, { |
| data: attrs.data.allStats, |
| isMultiTrace: attrs.data.isMultiTrace, |
| isMultiMachine: attrs.data.isMultiMachine, |
| }), |
| ), |
| ); |
| } |
| } |
| |
| // Trace Metadata Section |
| interface TraceMetadataAttrs { |
| data: TraceMetadataRow[]; |
| isMultiTrace: boolean; |
| isMultiMachine: boolean; |
| } |
| |
| class TraceMetadata implements m.ClassComponent<TraceMetadataAttrs> { |
| view({attrs}: m.CVnode<TraceMetadataAttrs>) { |
| const tableRows = attrs.data.map((row) => { |
| const columns = []; |
| if (attrs.isMultiTrace) { |
| columns.push( |
| m( |
| 'td.pf-trace-info-page__stats-table-cell', |
| row.traceId === null ? '-' : row.traceId, |
| ), |
| ); |
| } |
| if (attrs.isMultiMachine) { |
| columns.push( |
| m( |
| 'td.pf-trace-info-page__stats-table-cell', |
| row.machineId === null ? '-' : row.machineId, |
| ), |
| ); |
| } |
| columns.push( |
| m( |
| 'td.pf-trace-info-page__stats-table-cell.pf-trace-info-page__stats-table-cell--name', |
| `${row.name}`, |
| ), |
| ); |
| columns.push( |
| m('td.pf-trace-info-page__stats-table-cell', `${row.value}`), |
| ); |
| return m('tr.pf-trace-info-page__stats-table-row', columns); |
| }); |
| |
| const headerCols = []; |
| if (attrs.isMultiTrace) { |
| headerCols.push( |
| m('td.pf-trace-info-page__stats-table-head-cell', 'Trace'), |
| ); |
| } |
| if (attrs.isMultiMachine) { |
| headerCols.push( |
| m('td.pf-trace-info-page__stats-table-head-cell', 'Machine'), |
| ); |
| } |
| headerCols.push(m('td.pf-trace-info-page__stats-table-head-cell', 'Name')); |
| headerCols.push(m('td.pf-trace-info-page__stats-table-head-cell', 'Value')); |
| |
| return m( |
| 'section.pf-trace-info-page__stats-section', |
| m( |
| 'table.pf-trace-info-page__stats-table', |
| m('thead', m('tr', headerCols)), |
| m('tbody', tableRows), |
| ), |
| ); |
| } |
| } |
| |
| // Stats Section |
| interface StatsSectionAttrs { |
| data: StatsSectionRow[]; |
| isMultiTrace: boolean; |
| isMultiMachine: boolean; |
| } |
| |
| class StatsSection implements m.ClassComponent<StatsSectionAttrs> { |
| private hideZeroValues = true; |
| |
| view({attrs}: m.CVnode<StatsSectionAttrs>) { |
| const data = attrs.data; |
| if (data === undefined || data.length === 0) { |
| return m(''); |
| } |
| |
| const filtered = this.hideZeroValues |
| ? data.filter((row) => row.value !== 0 && row.value !== null) |
| : data; |
| |
| const tableRows = filtered.map((row) => { |
| const help = []; |
| if (Boolean(row.description)) { |
| help.push( |
| m( |
| Tooltip, |
| { |
| trigger: m(Icon, { |
| icon: 'help_outline', |
| className: 'pf-trace-info-page__help-icon', |
| }), |
| }, |
| `${row.description}`, |
| ), |
| ); |
| } |
| const idx = row.idx !== '' ? `[${row.idx}]` : ''; |
| const cells = []; |
| if (attrs.isMultiTrace) { |
| cells.push( |
| m( |
| 'td.pf-trace-info-page__stats-table-cell', |
| row.traceId === null ? '-' : row.traceId, |
| ), |
| ); |
| } |
| if (attrs.isMultiMachine) { |
| cells.push( |
| m( |
| 'td.pf-trace-info-page__stats-table-cell', |
| row.machineId === null ? '-' : row.machineId, |
| ), |
| ); |
| } |
| cells.push( |
| m( |
| 'td.pf-trace-info-page__stats-table-cell.pf-trace-info-page__stats-table-cell--name', |
| `${row.name}${idx}`, |
| help, |
| ), |
| m('td.pf-trace-info-page__stats-table-cell', `${row.value}`), |
| m( |
| 'td.pf-trace-info-page__stats-table-cell', |
| `${row.severity} (${row.source})`, |
| ), |
| ); |
| return m('tr.pf-trace-info-page__stats-table-row', cells); |
| }); |
| |
| const headerCols = []; |
| if (attrs.isMultiTrace) { |
| headerCols.push( |
| m('td.pf-trace-info-page__stats-table-head-cell', 'Trace'), |
| ); |
| } |
| if (attrs.isMultiMachine) { |
| headerCols.push( |
| m('td.pf-trace-info-page__stats-table-head-cell', 'Machine'), |
| ); |
| } |
| headerCols.push( |
| m('td.pf-trace-info-page__stats-table-head-cell', 'Name'), |
| m('td.pf-trace-info-page__stats-table-head-cell', 'Value'), |
| m('td.pf-trace-info-page__stats-table-head-cell', 'Type'), |
| ); |
| |
| return m( |
| 'section.pf-trace-info-page__stats-section', |
| m(Button, { |
| label: this.hideZeroValues ? 'Show zero values' : 'Hide zero values', |
| icon: this.hideZeroValues ? 'visibility' : 'visibility_off', |
| onclick: () => { |
| this.hideZeroValues = !this.hideZeroValues; |
| }, |
| }), |
| m( |
| 'table.pf-trace-info-page__stats-table', |
| m('thead', m('tr', headerCols)), |
| m('tbody', tableRows), |
| ), |
| ); |
| } |
| } |