blob: 763513a1d63b0970a0d79e7ecfe817c0f152e3d7 [file] [edit]
// 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 {
LONG_NULL,
NUM_NULL,
STR_NULL,
} from '../../../trace_processor/query_result';
import {Icon} from '../../../widgets/icon';
import {Tooltip} from '../../../widgets/tooltip';
import {Section} from '../../../widgets/section';
import {Card} from '../../../widgets/card';
import {GridLayout} from '../../../widgets/grid_layout';
import {EmptyState} from '../../../widgets/empty_state';
import {Callout} from '../../../widgets/callout';
import {Intent} from '../../../widgets/common';
import type {Trace} from '../../../public/trace';
import type {duration} from '../../../base/time';
import {formatDuration} from '../../../components/time_utils';
import type {TabKey} from '../utils';
import {formatFileSize} from '../../../base/file_utils';
export interface OverviewData {
// Status counts
importErrors: number;
traceErrors: number;
dataLosses: number;
uiLoadingErrorCount: number;
// Metrics
traceSizeBytes?: bigint;
traceTypes: string[];
uuid?: string;
durationNs?: duration;
schedDurationNs?: duration;
// System information
androidBuildFingerprint?: string;
systemName?: string;
systemMachine?: string;
systemRelease?: string;
// Multi-trace/machine counts
traceCount: number;
machineCount: number;
}
export async function loadOverviewData(trace: Trace): Promise<OverviewData> {
// Load everything in a single query
const result = await trace.engine.query(`
SELECT
-- Status card counts
(SELECT IFNULL(sum(value), 0) FROM stats WHERE severity = 'error' AND source = 'analysis') as import_errors,
(SELECT IFNULL(sum(value), 0) FROM stats WHERE severity = 'error' AND source = 'trace') as trace_errors,
(SELECT IFNULL(sum(value), 0) FROM stats WHERE severity = 'data_loss') as data_losses,
-- Metrics
extract_metadata('trace_size_bytes') as trace_size_bytes,
extract_metadata('tracing_disabled_ns') -
extract_metadata('tracing_started_ns') as duration_ns,
(SELECT max(ts) - min(ts) FROM sched) as sched_duration_ns,
-- System info
extract_metadata('system_name') as system_name,
extract_metadata('system_release') as system_release,
extract_metadata('system_machine') as system_machine,
extract_metadata('android_build_fingerprint') as android_build_fingerprint,
(SELECT COUNT(DISTINCT trace_id) FROM metadata WHERE trace_id IS NOT NULL) as trace_count,
(SELECT COUNT(DISTINCT machine_id) FROM metadata WHERE machine_id IS NOT NULL) as machine_count;
`);
const row = result.firstRow({
import_errors: NUM_NULL,
trace_errors: NUM_NULL,
data_losses: NUM_NULL,
trace_size_bytes: LONG_NULL,
duration_ns: LONG_NULL,
sched_duration_ns: LONG_NULL,
system_name: STR_NULL,
system_release: STR_NULL,
system_machine: STR_NULL,
android_build_fingerprint: STR_NULL,
trace_count: NUM_NULL,
machine_count: NUM_NULL,
});
return {
importErrors: row.import_errors ?? 0,
traceErrors: row.trace_errors ?? 0,
dataLosses: row.data_losses ?? 0,
uiLoadingErrorCount: trace.loadingErrors.length,
traceSizeBytes: row.trace_size_bytes ?? undefined,
traceTypes: trace.traceInfo.traceTypes,
uuid: trace.traceInfo.uuid,
durationNs: row.duration_ns ?? undefined,
schedDurationNs: row.sched_duration_ns ?? undefined,
androidBuildFingerprint: row.android_build_fingerprint ?? undefined,
systemName: row.system_name ?? undefined,
systemMachine: row.system_machine ?? undefined,
systemRelease: row.system_release ?? undefined,
traceCount: row.trace_count ?? 0,
machineCount: row.machine_count ?? 0,
};
}
export interface OverviewTabAttrs {
trace: Trace;
data: OverviewData;
onTabChange(key: TabKey): void;
}
interface StatusCardConfig {
title: string;
count: number;
severity: 'success' | 'danger' | 'warning';
icon: string;
helpText: string;
targetTab: TabKey;
}
interface MetricCardConfig {
label: string;
value: string | undefined;
help?: string;
wide?: boolean;
}
export class OverviewTab implements m.ClassComponent<OverviewTabAttrs> {
view({attrs}: m.CVnode<OverviewTabAttrs>) {
return m(
'.pf-trace-info-page__tab-content',
attrs.data.traceCount > 1 &&
m(
Callout,
{
icon: 'layers',
intent: Intent.Primary,
className: 'pf-trace-info-page__banner',
},
'This session contains multiple traces. See the "Traces" tab for details.',
),
attrs.data.machineCount > 1 &&
m(
Callout,
{
icon: 'computer',
intent: Intent.Primary,
className: 'pf-trace-info-page__banner',
},
'This session contains data from multiple machines. See the "Machines" tab for details.',
),
this.renderCardSection(
'Trace Health',
'Summary of errors, warnings, and data quality indicators',
createStatusCards(attrs.data).map((card) =>
renderStatusCard(attrs, card),
),
),
this.renderCardSection(
'Trace Overview',
'Key metadata and properties of the trace file',
createTraceMetrics(attrs.trace, attrs.data).map((metric) =>
renderMetricCard(metric),
),
),
this.renderCardSection(
'System Information',
'Operating system and hardware details from the traced device',
createSystemInfoMetrics(attrs.data).map((metric) =>
renderMetricCard(metric),
),
{
emptyState: {
icon: 'computer',
title: 'No system information available',
},
},
),
);
}
private renderCardSection(
title: string,
subtitle: string,
cards: m.Children[],
options?: {emptyState?: {icon: string; title: string}},
): m.Children {
const filteredCards = cards.filter(Boolean);
return m(
Section,
{title, subtitle},
filteredCards.length === 0 && options?.emptyState
? m(EmptyState, options.emptyState)
: m(GridLayout, {}, ...filteredCards),
);
}
}
function renderStatusCard(
attrs: OverviewTabAttrs,
{title, count, severity, icon, helpText, targetTab}: StatusCardConfig,
): m.Children {
const isClickable = count > 0;
const className = `pf-trace-info-page__status-card pf-trace-info-page__status-card--${severity}${
isClickable ? ' pf-trace-info-page__status-card--clickable' : ''
}`;
return m(
Card,
{
className,
onclick: isClickable ? () => attrs.onTabChange(targetTab) : undefined,
},
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,
m(
Tooltip,
{
trigger: m(Icon, {
icon: 'help_outline',
className: 'pf-trace-info-page__help-icon',
}),
},
helpText,
),
),
m('.pf-trace-info-page__status-value', count),
),
isClickable &&
m(
'.pf-trace-info-page__status-link',
m(Icon, {
icon: 'arrow_forward',
className: 'pf-trace-info-page__status-link-icon',
}),
),
),
);
}
function renderMetricCard({
label,
value,
help,
wide,
}: MetricCardConfig): m.Children {
if (value === undefined) {
return null;
}
const className = wide
? 'pf-trace-info-page__metric-card pf-trace-info-page__metric-card--wide'
: 'pf-trace-info-page__metric-card';
return m(
Card,
{className},
m(
'.pf-trace-info-page__metric-content',
m(
'.pf-trace-info-page__metric-label',
label,
help &&
m(
Tooltip,
{
trigger: m(Icon, {
icon: 'help_outline',
className: 'pf-trace-info-page__help-icon',
}),
},
help,
),
),
m('.pf-trace-info-page__metric-value', value),
),
);
}
function createStatusCards(data: OverviewData): StatusCardConfig[] {
const statusCards: StatusCardConfig[] = [
{
title: 'Import Errors',
count: data.importErrors,
severity: data.importErrors === 0 ? 'success' : 'danger',
icon: data.importErrors === 0 ? 'check_circle' : 'error',
helpText:
'Errors encountered during trace import by the trace processor. These may indicate missing or corrupted data.',
targetTab: data.importErrors > 0 ? 'import_errors' : 'stats',
},
{
title: 'Trace Errors',
count: data.traceErrors,
severity: data.traceErrors === 0 ? 'success' : 'danger',
icon: data.traceErrors === 0 ? 'check_circle' : 'error',
helpText:
'Errors that occurred during trace recording. These indicate problems during data collection.',
targetTab: data.traceErrors > 0 ? 'trace_errors' : 'stats',
},
{
title: 'Data Losses',
count: data.dataLosses,
severity: data.dataLosses === 0 ? 'success' : 'warning',
icon: data.dataLosses === 0 ? 'check_circle' : 'warning',
helpText:
'Events that were dropped during trace recording due to buffer overflow or other issues.',
targetTab: data.dataLosses > 0 ? 'data_losses' : 'stats',
},
];
// Optional UI loading errors card - only show if there are errors
if (data.uiLoadingErrorCount > 0) {
statusCards.push({
title: 'UI Loading Errors',
count: data.uiLoadingErrorCount,
severity: 'danger',
icon: 'error',
helpText:
'Errors that occurred in the UI while loading or processing the trace.',
targetTab: 'ui_loading_errors',
});
}
return statusCards;
}
function createTraceMetrics(
trace: Trace,
data: OverviewData,
): MetricCardConfig[] {
return [
{
label: 'Trace Size',
value:
data.traceSizeBytes !== undefined
? formatFileSize(Number(data.traceSizeBytes))
: undefined,
help: 'Total size of the trace file on disk',
},
{
label: 'Trace Type',
value:
data.traceTypes.length > 0 ? data.traceTypes.join(', ') : 'Unknown',
help: 'Format of the trace file (proto, json, etc.)',
},
{
label: 'Recording Duration',
value:
data.durationNs !== undefined
? formatDuration(trace, data.durationNs)
: undefined,
help: 'Time between tracing_started and tracing_disabled',
},
{
label: 'Scheduling Duration',
value:
data.schedDurationNs !== undefined
? formatDuration(trace, data.schedDurationNs)
: undefined,
help: 'Duration from first to last scheduling event (max(ts) - min(ts) from sched)',
},
{
label: 'Trace UUID',
value: data.uuid,
help:
data.traceCount > 1
? 'Session-wide identifier. Individual UUIDs are in the "Traces" tab.'
: 'Unique identifier for this trace session',
wide: true,
},
];
}
function createSystemInfoMetrics(data: OverviewData): MetricCardConfig[] {
return [
{
label: 'Android Fingerprint',
value: data.androidBuildFingerprint,
help: 'Unique build identifier for Android devices',
wide: true,
},
{
label: 'Operating System',
value: data.systemName,
help: 'Operating system name from uname',
},
{
label: 'Architecture',
value: data.systemMachine,
help: 'System architecture (e.g., x86_64, aarch64)',
},
{
label: 'Kernel Release',
value: data.systemRelease,
help: 'Kernel version string',
wide: true,
},
];
}