Merge pull request #2258 from pikasTech/fix/2255-workbench-render-budget
fix: bound workbench trace rendering
This commit is contained in:
@@ -9,14 +9,22 @@ import { traceDisplayRows, type TraceEventRow } from "../../../../../tools/src/h
|
|||||||
|
|
||||||
const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string; autoExpanded?: boolean | null }>();
|
const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string; autoExpanded?: boolean | null }>();
|
||||||
|
|
||||||
|
const TRACE_RENDER_ROW_WINDOW = 120;
|
||||||
const listRef = ref<HTMLOListElement | null>(null);
|
const listRef = ref<HTMLOListElement | null>(null);
|
||||||
const expanded = ref(false);
|
const expanded = ref(false);
|
||||||
const { following, keepBottomAfterUpdate, onScroll, scrollToBottom } = useBottomFollowScroll(listRef, { threshold: 24 });
|
const { following, keepBottomAfterUpdate, onScroll, scrollToBottom } = useBottomFollowScroll(listRef, { threshold: 24 });
|
||||||
|
|
||||||
const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
|
const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
|
||||||
const eventCount = computed(() => props.trace?.eventCount ?? events.value.length);
|
const eventCount = computed(() => props.trace?.eventCount ?? events.value.length);
|
||||||
const rawReadableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[], { formatClock: formatDisplayClock }));
|
const rawReadableRows = computed(() => expanded.value ? traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[], { formatClock: formatDisplayClock }) : []);
|
||||||
const readableRows = computed(() => rawReadableRows.value);
|
const readableRows = computed(() => rawReadableRows.value);
|
||||||
|
const hiddenRowCount = computed(() => Math.max(0, readableRows.value.length - TRACE_RENDER_ROW_WINDOW));
|
||||||
|
const renderedRows = computed(() => hiddenRowCount.value > 0 ? readableRows.value.slice(-TRACE_RENDER_ROW_WINDOW) : readableRows.value);
|
||||||
|
const traceSummaryLabel = computed(() => {
|
||||||
|
const count = eventCount.value;
|
||||||
|
if (count <= 0) return String(props.trace?.status ?? "").trim() || "pending";
|
||||||
|
return `${count} events`;
|
||||||
|
});
|
||||||
const projectionDiagnosticLabel = computed(() => {
|
const projectionDiagnosticLabel = computed(() => {
|
||||||
const projection = props.trace?.projection ?? null;
|
const projection = props.trace?.projection ?? null;
|
||||||
const health = projection?.projectionHealth ?? props.trace?.projectionHealth;
|
const health = projection?.projectionHealth ?? props.trace?.projectionHealth;
|
||||||
@@ -128,14 +136,16 @@ function firstNonEmptyString(...values: unknown[]): string | null {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section v-if="trace" class="trace-timeline" :data-trace-id="trace.traceId || undefined" :data-status="trace.status" :data-projection-health="trace.projection?.projectionHealth || trace.projectionHealth || undefined" :data-following="following ? 'true' : 'false'">
|
<section v-if="trace" class="trace-timeline" :data-trace-id="trace.traceId || undefined" :data-status="trace.status" :data-projection-health="trace.projection?.projectionHealth || trace.projectionHealth || undefined" :data-following="following ? 'true' : 'false'">
|
||||||
<details class="trace-disclosure" :open="expanded" @toggle="onDisclosureToggle">
|
<details class="trace-disclosure" :open="expanded" :data-event-count="eventCount" :data-readable-row-count="readableRows.length" :data-rendered-row-count="renderedRows.length" :data-row-windowed="hiddenRowCount > 0 ? 'true' : 'false'" @toggle="onDisclosureToggle">
|
||||||
<summary class="trace-disclosure-summary" aria-label="折叠运行记录">
|
<summary class="trace-disclosure-summary" aria-label="折叠运行记录">
|
||||||
<span class="trace-disclosure-caret" aria-hidden="true" />
|
<span class="trace-disclosure-caret" aria-hidden="true" />
|
||||||
<span class="trace-disclosure-label">运行记录</span>
|
<span class="trace-disclosure-label">运行记录</span>
|
||||||
|
<span class="trace-disclosure-count">{{ traceSummaryLabel }}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="trace-disclosure-body">
|
<div v-if="expanded" class="trace-disclosure-body">
|
||||||
<ol ref="listRef" @scroll="onScroll">
|
<p v-if="hiddenRowCount > 0" class="trace-window-notice">显示最近 {{ renderedRows.length }} / {{ readableRows.length }} 行</p>
|
||||||
<li v-for="(row, index) in readableRows" :key="rowKey(row, index)" class="trace-render-row" data-testid="trace-render-row" :data-trace-id="trace.traceId || undefined" :data-projected-seq="row.seq ?? undefined" :data-event-kind="row.rowId" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'" :data-row-id="row.rowId" :aria-posinset="row.seq ?? index + 1">
|
<ol v-if="renderedRows.length > 0" ref="listRef" @scroll="onScroll">
|
||||||
|
<li v-for="(row, index) in renderedRows" :key="rowKey(row, index)" class="trace-render-row" data-testid="trace-render-row" :data-trace-id="trace.traceId || undefined" :data-projected-seq="row.seq ?? undefined" :data-event-kind="row.rowId" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'" :data-row-id="row.rowId" :aria-posinset="row.seq ?? hiddenRowCount + index + 1">
|
||||||
<details v-if="rowIsTool(row)" class="trace-tool-details">
|
<details v-if="rowIsTool(row)" class="trace-tool-details">
|
||||||
<summary>
|
<summary>
|
||||||
<code>{{ row.header }}</code>
|
<code>{{ row.header }}</code>
|
||||||
|
|||||||
@@ -34,10 +34,8 @@ const traceTimelinePolicy = computed(() => workbenchTraceTimelinePolicy());
|
|||||||
const scrollSignature = computed(() => workbench.activeMessages.map((message) => [
|
const scrollSignature = computed(() => workbench.activeMessages.map((message) => [
|
||||||
message.id,
|
message.id,
|
||||||
message.status,
|
message.status,
|
||||||
visibleMessageText(message).length,
|
visibleMessageText(message).length
|
||||||
message.updatedAt ?? "",
|
].join(":")).join("|"));
|
||||||
message.runnerTrace?.eventCount ?? message.runnerTrace?.events?.length ?? 0
|
|
||||||
].join(":")));
|
|
||||||
|
|
||||||
watch(scrollSignature, async () => {
|
watch(scrollSignature, async () => {
|
||||||
await keepBottomAfterUpdate();
|
await keepBottomAfterUpdate();
|
||||||
|
|||||||
@@ -5,14 +5,34 @@ import { firstNonEmptyString } from "@/utils";
|
|||||||
|
|
||||||
marked.setOptions({ async: false, breaks: true, gfm: true });
|
marked.setOptions({ async: false, breaks: true, gfm: true });
|
||||||
|
|
||||||
|
const MARKDOWN_RENDER_CACHE_LIMIT = 160;
|
||||||
|
const markdownRenderCache = new Map<string, string>();
|
||||||
|
|
||||||
export function renderSafeMarkdown(source: string | null | undefined): string {
|
export function renderSafeMarkdown(source: string | null | undefined): string {
|
||||||
const text = source?.trim();
|
const text = source?.trim();
|
||||||
if (!text) return "";
|
if (!text) return "";
|
||||||
|
const cached = markdownRenderCache.get(text);
|
||||||
|
if (cached !== undefined) {
|
||||||
|
markdownRenderCache.delete(text);
|
||||||
|
markdownRenderCache.set(text, cached);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
const rendered = marked.parse(text) as string;
|
const rendered = marked.parse(text) as string;
|
||||||
return sanitizeHtml(rendered, {
|
const html = sanitizeHtml(rendered, {
|
||||||
ADD_ATTR: ["target", "rel"],
|
ADD_ATTR: ["target", "rel"],
|
||||||
USE_PROFILES: { html: true }
|
USE_PROFILES: { html: true }
|
||||||
});
|
});
|
||||||
|
rememberMarkdownRender(text, html);
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rememberMarkdownRender(source: string, html: string): void {
|
||||||
|
markdownRenderCache.set(source, html);
|
||||||
|
while (markdownRenderCache.size > MARKDOWN_RENDER_CACHE_LIMIT) {
|
||||||
|
const oldest = markdownRenderCache.keys().next().value;
|
||||||
|
if (typeof oldest !== "string") break;
|
||||||
|
markdownRenderCache.delete(oldest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HtmlSanitizeConfig {
|
interface HtmlSanitizeConfig {
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import { createWorkbenchServerState, reduceWorkbenchServerState, selectActiveMes
|
|||||||
|
|
||||||
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1_800_000;
|
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1_800_000;
|
||||||
const DEFAULT_GATEWAY_TIMEOUT_MS = 120_000;
|
const DEFAULT_GATEWAY_TIMEOUT_MS = 120_000;
|
||||||
const TRACE_HYDRATION_PAGE_LIMIT = 100;
|
const TRACE_HYDRATION_PAGE_LIMIT = 50;
|
||||||
const TRACE_HYDRATION_MAX_PAGES = 60;
|
const TRACE_HYDRATION_MAX_PAGES = 4;
|
||||||
const TRACE_HYDRATION_MAX_ATTEMPTS = 3;
|
const TRACE_HYDRATION_MAX_ATTEMPTS = 3;
|
||||||
const TRACE_HYDRATION_RETRY_DELAY_MS = 700;
|
const TRACE_HYDRATION_RETRY_DELAY_MS = 700;
|
||||||
const TRACE_HYDRATION_AUTO_QUEUE_LIMIT = 12;
|
const TRACE_HYDRATION_AUTO_QUEUE_LIMIT = 4;
|
||||||
const TRACE_HYDRATION_BACKGROUND_CONCURRENCY = 2;
|
const TRACE_HYDRATION_BACKGROUND_CONCURRENCY = 1;
|
||||||
const TRACE_HYDRATION_BACKGROUND_DELAY_MS = 150;
|
const TRACE_HYDRATION_BACKGROUND_DELAY_MS = 400;
|
||||||
const SESSION_LIST_PAGE_LIMIT = 20;
|
const SESSION_LIST_PAGE_LIMIT = 20;
|
||||||
const WORKBENCH_READ_HYDRATION_CONCURRENCY = 3;
|
const WORKBENCH_READ_HYDRATION_CONCURRENCY = 3;
|
||||||
const WORKBENCH_READ_FAILURE_COOLDOWN_MS = 5_000;
|
const WORKBENCH_READ_FAILURE_COOLDOWN_MS = 5_000;
|
||||||
|
|||||||
@@ -1391,6 +1391,13 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trace-disclosure-count {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.trace-disclosure-body {
|
.trace-disclosure-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
@@ -1535,6 +1542,18 @@
|
|||||||
color: #92400e;
|
color: #92400e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trace-window-notice {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid #d8e1eb;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: white;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 5px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
.trace-empty {
|
.trace-empty {
|
||||||
border: 1px solid #d8e1eb;
|
border: 1px solid #d8e1eb;
|
||||||
background: white;
|
background: white;
|
||||||
|
|||||||
Reference in New Issue
Block a user