blob: e3823abb86c3343d2cda7ce2ae3899a631bb70ca [file]
// Copyright (C) 2026 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 {Box} from '../../widgets/box';
import {Button} from '../../widgets/button';
import {PopupPosition} from '../../widgets/popup';
import {Tooltip} from '../../widgets/tooltip';
import {Duration} from '../../base/time';
import {BigtraceAsyncDataSource} from '../query/bigtrace_async_data_source';
import {
formatCompact,
queryStore,
statusDisplayLabel,
TERMINAL_STATUSES,
} from '../query/query_store';
import type {BigTraceEditorTab} from './query_tabs_state';
// "<1s" for sub-500ms runs so the user sees the query actually ran.
export function formatDurationS(ms: number): string {
if (ms < 500) return '<1s';
return Duration.format(Duration.fromMillis(Math.round(ms / 1000) * 1000));
}
export function renderStatusBox(tab: BigTraceEditorTab): m.Children {
if (!tab.materialize || !tab.queryUuid) return false;
const isTerminal =
tab.execution?.status !== undefined &&
TERMINAL_STATUSES.has(tab.execution.status);
const processedRows = tab.execution?.processedRows ?? 0;
const hasNewData = !isTerminal && processedRows > tab.lastProcessedRows;
let durationMs = 0;
if (
isTerminal &&
tab.execution?.endTime !== undefined &&
tab.execution?.startTime !== undefined
) {
durationMs = tab.execution.endTime - tab.execution.startTime;
} else if (!isTerminal) {
const start =
tab.execution?.startTime !== undefined
? tab.execution.startTime
: tab.clientStartTime;
if (start !== undefined) {
durationMs = Date.now() - start;
}
}
const status = tab.execution?.status ?? 'UNKNOWN';
const processedTraces = tab.execution?.processedTraces ?? 0;
const totalTraces = tab.execution?.totalTraces ?? 0;
const durationStr = formatDurationS(durationMs);
const leftGroup = m(
'.pf-bt-status-bar-group',
m(
'div.pf-bt-status-bar-refresh',
m(Button, {
icon: 'refresh',
title: hasNewData
? 'New data available. Click to refresh.'
: 'Refresh data',
onclick: () => refreshAsyncStatus(tab),
}),
hasNewData &&
m('span.pf-bt-status-bar-notif', {
'aria-label': 'New data available',
}),
),
m(
'span.pf-bt-status-bar-pill',
{className: `pf-bt-status-${status.toLowerCase().replace(/_/g, '-')}`},
statusDisplayLabel(status),
),
m(
'span.pf-bt-status-bar-duration',
m('span.pf-bt-status-bar-duration-value', durationStr),
),
);
const rowsStatClasses = [
'pf-bt-status-bar-stat',
'pf-bt-status-bar-stat--rows',
processedRows === 0 && 'pf-bt-status-bar-stat--empty',
]
.filter(Boolean)
.join(' ');
const rightGroupContent = m(
'.pf-bt-status-bar-group',
m(
'span.pf-bt-status-bar-stat.pf-bt-status-bar-stat--traces',
m('span.pf-bt-status-bar-stat-label', 'Traces:'),
m(
'span.pf-bt-status-bar-stat-value',
{
title:
`${processedTraces.toLocaleString()} of ` +
`${totalTraces.toLocaleString()}` +
(!isTerminal
? ' — numerator lags the poll (≤3s); denominator is exact.'
: ''),
},
formatCompact(processedTraces),
),
renderInlineProgressBar(processedTraces, totalTraces, !isTerminal),
),
m(
'span',
{className: rowsStatClasses},
m('span.pf-bt-status-bar-stat-label', 'Rows:'),
m(
'span.pf-bt-status-bar-stat-value',
{
title: `${processedRows.toLocaleString()} of result limit ${tab.limit.toLocaleString()}`,
},
formatCompact(processedRows),
),
renderInlineProgressBar(processedRows, tab.limit, !isTerminal),
),
);
const rightGroup = !isTerminal
? m(
Tooltip,
{
trigger: rightGroupContent,
position: PopupPosition.Top,
},
m(
'.pf-bt-status-bar-progress-tooltip',
m('div', `Traces: ${formatCompact(processedTraces)}`),
m('div', `Rows: ${formatCompact(processedRows)}`),
),
)
: rightGroupContent;
return m(
Box,
{
className: isTerminal
? 'pf-bt-status-bar'
: 'pf-bt-status-bar pf-bt-status-bar--running',
},
leftGroup,
rightGroup,
);
}
function renderInlineProgressBar(
done: number,
total: number,
live: boolean,
): m.Children {
if (!live) return null;
if (total <= 0) return null;
const pct = Math.max(0, Math.min(100, (done / total) * 100));
return m(
'span.pf-bt-inline-progress',
m('span.pf-bt-inline-progress-fill', {
style: {width: `${pct}%`},
}),
);
}
async function refreshAsyncStatus(tab: BigTraceEditorTab): Promise<void> {
if (!tab.queryUuid) return;
try {
const status = await tab.queryClient?.getStatus(
tab.queryUuid,
tab.lifecycle.signal,
);
if (status) {
queryStore.update(tab.queryUuid, {
processedRows: status.processedRows ?? 0,
processedTraces: status.processedTraces ?? 0,
totalTraces: status.totalTraces ?? 0,
status: status.status ?? 'N/A',
});
}
} catch (e) {
console.error('Failed to fetch query status on refresh:', e);
}
if (tab.dataSource instanceof BigtraceAsyncDataSource) {
tab.dataSource.refresh();
tab.lastProcessedRows = tab.execution?.processedRows ?? 0;
}
m.redraw();
}