| // 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. |
| // 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 type {Engine} from '../../trace_processor/engine'; |
| import {NUM_NULL, STR} from '../../trace_processor/query_result'; |
| import {Icon} from '../../widgets/icon'; |
| import {Tooltip} from '../../widgets/tooltip'; |
| import {Card} from '../../widgets/card'; |
| import {Grid, GridCell, GridHeaderCell} from '../../widgets/grid'; |
| |
| // All possible tab keys - single source of truth |
| export const ALL_TAB_KEYS = [ |
| 'overview', |
| 'config', |
| 'android', |
| 'traces', |
| 'machines', |
| 'import_errors', |
| 'trace_errors', |
| 'data_losses', |
| 'ui_loading_errors', |
| 'stats', |
| ] as const; |
| |
| // Derive the TabKey type from the array |
| export type TabKey = (typeof ALL_TAB_KEYS)[number]; |
| |
| // Type guard to check if a string is a valid TabKey |
| export function isValidTabKey(key: string): key is TabKey { |
| return ALL_TAB_KEYS.includes(key as TabKey); |
| } |
| |
| // Shared stats spec and type |
| export const statsSpec = { |
| name: STR, |
| value: NUM_NULL, |
| description: STR, |
| idx: STR, |
| severity: STR, |
| source: STR, |
| machineId: NUM_NULL, |
| traceId: NUM_NULL, |
| }; |
| |
| export type StatsSectionRow = typeof statsSpec; |
| |
| export interface TraceInfo { |
| readonly id: number; |
| readonly traceIndex: number; |
| } |
| |
| export async function getTraceInfos( |
| engine: Engine, |
| ): Promise<Map<number, TraceInfo>> { |
| const result = await engine.query(` |
| select id |
| from __intrinsic_trace_file |
| where is_container = 0 |
| order by id |
| `); |
| |
| const map = new Map<number, TraceInfo>(); |
| let traceIndex = 0; |
| for (const it = result.iter({id: NUM_NULL}); it.valid(); it.next()) { |
| const id = it.id; |
| if (id === null) continue; |
| map.set(id, { |
| id, |
| traceIndex: traceIndex++, |
| }); |
| } |
| return map; |
| } |
| |
| // Generic error category interface |
| export interface ErrorCategory { |
| name: string; |
| description: string; |
| totalCount: number; |
| entries: StatsSectionRow[]; |
| } |
| |
| // Load stats with a custom WHERE clause |
| export async function loadStatsWithFilter( |
| engine: Engine, |
| whereClause: string, |
| ): Promise<StatsSectionRow[]> { |
| const result = 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 |
| where ${whereClause} |
| order by trace_id, machine_id, name, idx |
| `); |
| |
| const stats: StatsSectionRow[] = []; |
| for (const iter = result.iter(statsSpec); iter.valid(); iter.next()) { |
| stats.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, |
| }); |
| } |
| |
| return stats; |
| } |
| |
| // Group stats by category (name) |
| export function groupByCategory(stats: StatsSectionRow[]): ErrorCategory[] { |
| const categoryMap = new Map<string, ErrorCategory>(); |
| for (const stat of stats) { |
| const existing = categoryMap.get(stat.name); |
| if (existing) { |
| existing.totalCount += stat.value ?? 0; |
| existing.entries.push(stat); |
| } else { |
| categoryMap.set(stat.name, { |
| name: stat.name, |
| description: stat.description, |
| totalCount: stat.value ?? 0, |
| entries: [stat], |
| }); |
| } |
| } |
| return Array.from(categoryMap.values()); |
| } |
| |
| // Render an error category card |
| export function renderErrorCategoryCard( |
| category: ErrorCategory, |
| severity: 'danger' | 'warning', |
| icon: string, |
| ): m.Children { |
| const scrollToSection = () => { |
| const targetId = categoryToId(category.name); |
| const element = document.getElementById(targetId); |
| if (element) { |
| element.scrollIntoView({behavior: 'smooth', block: 'start'}); |
| } |
| }; |
| |
| return m( |
| Card, |
| { |
| className: `pf-trace-info-page__status-card pf-trace-info-page__status-card--${severity} pf-trace-info-page__status-card--clickable`, |
| onclick: scrollToSection, |
| }, |
| m( |
| '.pf-trace-info-page__status-card-main', |
| m(Icon, { |
| icon, |
| className: 'pf-trace-info-page__status-icon', |
| filled: true, |
| }), |
| m( |
| '.pf-trace-info-page__status-content', |
| m( |
| '.pf-trace-info-page__status-title', |
| {title: category.name}, |
| category.name, |
| category.description && |
| m( |
| Tooltip, |
| { |
| trigger: m(Icon, { |
| icon: 'help_outline', |
| className: 'pf-trace-info-page__help-icon', |
| }), |
| }, |
| category.description, |
| ), |
| ), |
| m('.pf-trace-info-page__status-value', category.totalCount), |
| ), |
| m( |
| '.pf-trace-info-page__status-link', |
| m(Icon, { |
| icon: 'arrow_downward', |
| className: 'pf-trace-info-page__status-link-icon', |
| }), |
| ), |
| ), |
| ); |
| } |
| |
| // Helper to generate a safe HTML ID from a category name |
| function categoryToId(categoryName: string): string { |
| return `category-${categoryName.replace(/[^a-zA-Z0-9_-]/g, '_')}`; |
| } |
| |
| // Render a category section with detailed breakdown |
| export function renderCategorySection( |
| category: ErrorCategory, |
| options?: { |
| className?: string; |
| isMultiTrace?: boolean; |
| isMultiMachine?: boolean; |
| }, |
| ): m.Children { |
| const isMultiTrace = options?.isMultiTrace ?? false; |
| const isMultiMachine = options?.isMultiMachine ?? false; |
| const columns = []; |
| if (isMultiTrace) { |
| columns.push({key: 'trace_id', header: m(GridHeaderCell, 'Trace')}); |
| } |
| if (isMultiMachine) { |
| columns.push({key: 'machine_id', header: m(GridHeaderCell, 'Machine')}); |
| } |
| columns.push( |
| {key: 'idx', header: m(GridHeaderCell, 'Index')}, |
| {key: 'value', header: m(GridHeaderCell, 'Count')}, |
| ); |
| return m( |
| '', |
| m('h3', {id: categoryToId(category.name)}, category.name), |
| category.description && m('p', category.description), |
| m(Grid, { |
| columns, |
| rowData: category.entries.map((row) => { |
| const cells = []; |
| if (isMultiTrace) { |
| cells.push(m(GridCell, row.traceId !== null ? row.traceId : '-')); |
| } |
| if (isMultiMachine) { |
| cells.push(m(GridCell, row.machineId !== null ? row.machineId : '-')); |
| } |
| cells.push( |
| m(GridCell, row.idx !== '' ? row.idx : '-'), |
| m(GridCell, row.value !== null ? row.value : '-'), |
| ); |
| return cells; |
| }), |
| className: options?.className, |
| }), |
| ); |
| } |