blob: 6bcaee502cdc6d07c4ad9891bbd041625b6c7dfe [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 {assertExists} from '../../base/assert';
import type {Trace} from '../../public/trace';
import type {QueryLog} from '../../trace_processor/engine';
import {Button} from '../../widgets/button';
import {Chip} from '../../widgets/chip';
import {Intent} from '../../widgets/common';
import {CopyToClipboardButton} from '../../widgets/copy_to_clipboard_button';
import {DetailsShell} from '../../widgets/details_shell';
function formatMillis(millis: number) {
return millis.toFixed(1);
}
interface QueryLogEntryAttrs {
readonly queryLog: QueryLog;
}
class QueryLogRow implements m.ClassComponent<QueryLogEntryAttrs> {
private isOpen = false;
view({attrs}: m.Vnode<QueryLogEntryAttrs>) {
const {queryLog: ql} = attrs;
assertExists(ql.query);
const fullQuery = ql.query.trim();
const isOpen = this.isOpen;
const toggle = () => {
this.isOpen = !this.isOpen;
};
const statusChip =
ql.success === undefined
? m(Chip, {label: 'Running', intent: Intent.Warning, compact: true})
: ql.success
? m(Chip, {label: 'Completed', intent: Intent.Success, compact: true})
: m(Chip, {label: 'Failed', intent: Intent.Danger, compact: true});
return m(
'tr.pf-query-log-row',
{class: isOpen ? 'pf-expanded' : ''},
m('td.pf-query-log-num', formatMillis(ql.startTime)),
m(
'td.pf-query-log-num',
ql.elapsedTimeMs === undefined ? '...' : formatMillis(ql.elapsedTimeMs),
),
m('td', ql.tag),
m('td', statusChip),
m(
'td.pf-query-log-toggle',
m(Button, {
icon: isOpen ? 'expand_more' : 'chevron_right',
compact: true,
onclick: toggle,
}),
),
m(
'td.pf-query-log-query',
{
title: isOpen ? undefined : fullQuery,
onclick: toggle,
},
isOpen
? m(
'.pf-query-log-expanded-body',
m(
'.pf-query-log-expanded-toolbar',
{onclick: (e: Event) => e.stopPropagation()},
m(CopyToClipboardButton, {
textToCopy: fullQuery,
label: 'Copy',
compact: true,
}),
),
m('span.pf-query-log-full', fullQuery),
)
: m('span.pf-query-log-snippet', fullQuery.replace(/\s+/g, ' ')),
),
);
}
}
export interface QueryTabAttrs {
readonly trace: Trace;
}
const PAGE_SIZE = 100;
export class QueryTab implements m.ClassComponent<QueryTabAttrs> {
private visibleCount = PAGE_SIZE;
view({attrs}: m.Vnode<QueryTabAttrs>) {
const {trace} = attrs;
// Show the logs in reverse order
const queryLog = Array.from(trace.engine.queryLog).reverse();
const visible = queryLog.slice(0, this.visibleCount);
const remaining = queryLog.length - visible.length;
return m(
DetailsShell,
{
title: 'Query Log',
description: `${queryLog.length} ${queryLog.length === 1 ? 'entry' : 'entries'}`,
buttons: m(Button, {
label: 'Clear',
icon: 'delete',
disabled: queryLog.length === 0,
onclick: () => trace.engine.clearQueryLog(),
}),
},
m(
'.pf-query-log',
m(
'table.pf-query-log-table',
m(
'thead',
m(
'tr',
m('th.pf-query-log-num', 'Start (ms)'),
m('th.pf-query-log-num', 'Duration (ms)'),
m('th', 'Tag'),
m('th', 'Status'),
m('th'),
m('th', 'Query'),
),
),
m(
'tbody',
visible.map((ql) => m(QueryLogRow, {queryLog: ql})),
),
),
remaining > 0 &&
m(
'.pf-query-log-show-more',
m(Button, {
label: `Show ${Math.min(PAGE_SIZE, remaining)} more (${remaining} remaining)`,
onclick: () => {
this.visibleCount += PAGE_SIZE;
},
}),
),
),
);
}
}