Files
tutti/tools/scripts/agent-gui-scroll-performance-scenario.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

445 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
evaluate,
finishRendererScenario,
markRenderer,
startRendererScenario,
waitForEvaluation
} from "./agent-gui-performance-helpers.mjs";
import {
prepareVirtualizedTranscript,
prepareVirtualizedTranscriptSnapshot
} from "./agent-gui-layout-performance-scenarios.mjs";
import { scenarioSummary } from "./agent-gui-performance-snapshot-helpers.mjs";
const markers = {
start: "tutti-perf:virtualized-scroll-locator:start",
scrolled: "tutti-perf:virtualized-scroll-locator:scrolled-observed",
stable: "tutti-perf:virtualized-scroll-locator:stable-observed",
end: "tutti-perf:virtualized-scroll-locator:end"
};
const scrollDurationMs = 10_000;
const minimumScrollDurationMs = 9_000;
const minimumViewportTravel = 8;
const forbiddenProfileFunctions = [
"EditorView",
"hasSelection",
"selectionToDOM"
];
const diagnosticProfileFunctions = ["captureScrollAnchor", "updateStateInner"];
export const virtualizedScrollLocatorScenario = {
id: "virtualized-scroll-locator",
markers,
milestones: [
{
key: "scrolled",
label: "monotonic transcript scroll completed",
marker: markers.scrolled
},
{
key: "stable",
label: "locator selection stable",
marker: markers.stable
}
],
profileFunctionNames: [
...forbiddenProfileFunctions,
...diagnosticProfileFunctions
],
prepareSnapshot(context) {
return prepareVirtualizedTranscriptSnapshot(context, {
richTextFixture: true
});
},
prepare(context, options) {
return prepareVirtualizedTranscript(
context,
options,
"virtualized-scroll-locator"
);
},
execute: executeVirtualizedScrollLocator,
describe(prepared) {
return `${prepared.sessionID}; ${prepared.turnCount} persisted turns; ${scrollDurationMs / 1_000}s monotonic upward scroll`;
},
summarize(prepared, result) {
return scenarioSummary(
[
{ name: "transcript virtualized", passed: prepared.virtualized },
{
name: "rich-text fixture has at least four messages",
passed: prepared.richTextMessageCount >= 4
},
{
name: "rich-text fixture has three mentions per message",
passed: prepared.richTextMentionsPerMessage === 3
},
{
name: "rich-text fixture has eight paragraphs per message",
passed: prepared.richTextParagraphsPerMessage === 8
},
{ name: "timeline scrolled upward", passed: result.moved },
{
name: "timeline traveled at least eight viewports",
passed: result.travel >= result.viewportHeight * minimumViewportTravel
},
{
name: "continuous scroll lasted at least nine seconds",
passed: result.elapsedMs >= minimumScrollDurationMs
},
{
name: "historical transcript created no interactive editor",
passed: result.maxHistoricalInteractiveNodes === 0
},
{
name: "mounted window exercised static rich text",
passed: result.maxStaticRichTextNodes > 0
},
{
name: "mounted window exercised mention chips",
passed: result.maxMountedMentions > 0
},
{ name: "locator selected a message", passed: result.selected },
{
name: "locator selection matches viewport turn",
passed: result.matchesViewportTurn
},
{
name: "locator selection never reversed",
passed: result.reversedTransitions === 0
},
{
name: "locator selection never returned",
passed: result.returnedTransitions === 0
},
{ name: "locator settled", passed: result.stable }
],
[
{ label: "Session", value: prepared.sessionID },
{ label: "Persisted turns", value: String(prepared.turnCount) },
{
label: "Rich-text fixture",
value: `${prepared.richTextMessageCount} messages × ${prepared.richTextParagraphsPerMessage} paragraphs × ${prepared.richTextMentionsPerMessage} mentions`
},
{
label: "Scroll duration",
value: `${Math.round(result.elapsedMs)} ms over ${result.frameCount} frames`
},
{
label: "Scroll range",
value: `${Math.round(result.startScrollTop)} → ${Math.round(result.endScrollTop)}`
},
{
label: "Viewport travel",
value: `${(result.travel / result.viewportHeight).toFixed(1)}×`
},
{
label: "Max mounted static rich text / mentions",
value: `${result.maxStaticRichTextNodes} / ${result.maxMountedMentions}`
},
{
label: "Selected indexes",
value: result.selectedIndexes.join(" → ") || "none"
},
{
label: "Selected index / scrollTop",
value:
result.selectedSnapshots
.map(
(snapshot) =>
`${snapshot.index}@${Math.round(snapshot.scrollTop)}`
)
.join(" → ") || "none"
},
{
label: "Expected final index",
value: String(result.expectedSelectedIndex)
},
{
label: "Initial virtual turns",
value: result.startVirtualTurnKeys.join(", ") || "none"
},
{
label: "Final virtual turns",
value: result.endVirtualTurnKeys.join(", ") || "none"
},
{
label: "Reversed transitions",
value: String(result.reversedTransitions)
},
{
label: "Returned transitions",
value: String(result.returnedTransitions)
}
],
"a ten-second monotonic upward scroll traverses at least eight viewport heights, keeps historical content non-editable, produces a non-increasing locator index sequence, then reaches five identical settled snapshots"
);
},
assessTrace(summary) {
const profileSamples = summary.cpuProfile.functionSamples;
return {
assertions: [
{
name: "CPU profile samples > 0",
passed: summary.cpuProfile.sampleCount > 0
},
{
name: "scroll EventDispatch count >= 300",
passed: (summary.inputEventTiming.scroll?.count ?? 0) >= 300
},
{
name: "max scroll EventDispatch <= 50 ms",
passed: (summary.inputEventTiming.scroll?.maxMs ?? 0) <= 50
},
{
name: "scroll EventDispatch inclusive <= 1200 ms",
passed:
(summary.inputEventTiming.scroll?.totalInclusiveMs ?? 0) <= 1_200
},
{
name: "Layout inclusive <= 500 ms",
passed: summary.timing.layoutInclusiveMs <= 500
},
{
name: "UpdateLayoutTree inclusive <= 1000 ms",
passed: summary.timing.updateLayoutTreeInclusiveMs <= 1_000
},
...forbiddenProfileFunctions.map((functionName) => ({
name: `${functionName} CPU samples = 0`,
passed: (profileSamples[functionName] ?? 0) === 0
}))
],
details: [
{
label: "Scroll EventDispatch",
value: `${summary.inputEventTiming.scroll?.count ?? 0} events; ${summary.inputEventTiming.scroll?.totalInclusiveMs ?? 0} ms inclusive; ${summary.inputEventTiming.scroll?.maxMs ?? 0} ms max`
},
{
label: "Layout / UpdateLayoutTree",
value: `${summary.timing.layoutInclusiveMs} / ${summary.timing.updateLayoutTreeInclusiveMs} ms inclusive`
},
{
label: "captureScrollAnchor CPU samples",
value: String(profileSamples.captureScrollAnchor ?? 0)
},
{
label: "updateStateInner CPU samples",
value: String(profileSamples.updateStateInner ?? 0)
}
]
};
}
};
async function executeVirtualizedScrollLocator(context, _prepared, options) {
const { pageClient } = context;
await evaluate(
pageClient,
`(() => {
const timeline = document.querySelector('[data-testid="agent-gui-timeline"]');
const locator = document.querySelector('[data-testid="agent-message-locator"]');
if (!(timeline instanceof HTMLElement)) throw new Error('timeline is unavailable');
if (!(locator instanceof HTMLElement)) throw new Error('message locator is unavailable');
const state = window.__tuttiPerfVirtualizedScrollLocator = {
maxHistoricalInteractiveNodes: 0,
maxMountedMentions: 0,
maxStaticRichTextNodes: 0,
selectedIndexes: [],
selectedSnapshots: [],
stablePolls: 0,
startVirtualTurnKeys: [...document.querySelectorAll('[data-agent-transcript-virtual-turn]')]
.map((element) => element.getAttribute('data-agent-transcript-virtual-turn') ?? '')
.filter(Boolean)
};
const transcriptRoot = document.querySelector('[data-agent-transcript-virtualized="true"]');
if (!(transcriptRoot instanceof HTMLElement)) {
throw new Error('virtualized transcript is unavailable');
}
const recordTranscriptMetrics = () => {
state.maxHistoricalInteractiveNodes = Math.max(
state.maxHistoricalInteractiveNodes,
transcriptRoot.querySelectorAll('[contenteditable="true"], [role="textbox"]').length
);
state.maxMountedMentions = Math.max(
state.maxMountedMentions,
transcriptRoot.querySelectorAll('[data-agent-file-mention="true"]').length
);
state.maxStaticRichTextNodes = Math.max(
state.maxStaticRichTextNodes,
transcriptRoot.querySelectorAll('.ProseMirror').length
);
};
const readSelectedIndex = () => [...locator.querySelectorAll('.agent-gui-message-locator__tick')]
.findIndex((tick) => tick.dataset.selected === 'true');
const readExpectedSelectedIndex = () => {
const viewportCenter = timeline.getBoundingClientRect().top + timeline.clientHeight / 2;
const virtualTurns = [...document.querySelectorAll('[data-agent-transcript-virtual-turn][data-index]')];
let viewportTurnIndex = -1;
let viewportTurnDistance = Number.POSITIVE_INFINITY;
for (const turn of virtualTurns) {
const rect = turn.getBoundingClientRect();
const distance = Math.abs(rect.top + rect.height / 2 - viewportCenter);
if (distance < viewportTurnDistance) {
viewportTurnDistance = distance;
viewportTurnIndex = Number(turn.getAttribute('data-index'));
}
}
const ticks = [...locator.querySelectorAll('.agent-gui-message-locator__tick')];
let expectedIndex = ticks.length > 0 ? 0 : -1;
ticks.forEach((tick, index) => {
const turnGroupIndex = Number(tick.getAttribute('data-agent-message-locator-turn-group-index'));
if (turnGroupIndex <= viewportTurnIndex) expectedIndex = index;
});
return expectedIndex;
};
const recordSelectedIndex = () => {
const index = readSelectedIndex();
if (index >= 0 && state.selectedIndexes.at(-1) !== index) {
state.selectedIndexes.push(index);
state.selectedSnapshots.push({
index,
scrollTop: timeline.scrollTop
});
}
return index;
};
state.readSelectedIndex = readSelectedIndex;
state.readExpectedSelectedIndex = readExpectedSelectedIndex;
state.recordSelectedIndex = recordSelectedIndex;
state.recordTranscriptMetrics = recordTranscriptMetrics;
state.observer = new MutationObserver(recordSelectedIndex);
state.observer.observe(locator, {
attributeFilter: ['data-selected'],
attributes: true,
subtree: true
});
state.transcriptObserver = new MutationObserver(recordTranscriptMetrics);
state.transcriptObserver.observe(transcriptRoot, {
attributes: true,
childList: true,
subtree: true
});
recordSelectedIndex();
recordTranscriptMetrics();
return true;
})()`
);
await startRendererScenario(pageClient, markers.start);
const scroll = await evaluate(
pageClient,
`new Promise((resolve) => {
const timeline = document.querySelector('[data-testid="agent-gui-timeline"]');
const state = window.__tuttiPerfVirtualizedScrollLocator;
if (!(timeline instanceof HTMLElement) || !state) {
throw new Error('virtualized scroll fixture is unavailable');
}
const startScrollTop = timeline.scrollTop;
const viewportHeight = timeline.clientHeight;
const targetScrollTop = 0;
timeline.dispatchEvent(new WheelEvent('wheel', { deltaY: -100 }));
const startedAt = performance.now();
let frame = 0;
const step = () => {
frame += 1;
const elapsedMs = performance.now() - startedAt;
const progress = Math.min(elapsedMs / ${scrollDurationMs}, 1);
if (frame % 8 === 0) {
timeline.dispatchEvent(new WheelEvent('wheel', { deltaY: -100 }));
}
timeline.scrollTop =
startScrollTop + (targetScrollTop - startScrollTop) * progress;
state.recordTranscriptMetrics();
if (progress < 1) {
requestAnimationFrame(step);
return;
}
requestAnimationFrame(() => resolve({
elapsedMs: performance.now() - startedAt,
startScrollTop,
endScrollTop: timeline.scrollTop,
frameCount: frame,
viewportHeight
}));
};
requestAnimationFrame(step);
})`,
true
);
await markRenderer(pageClient, markers.scrolled);
const settled = await waitForEvaluation(
pageClient,
`(() => {
const state = window.__tuttiPerfVirtualizedScrollLocator;
if (!state) return { ready: false };
const selectedIndex = state.recordSelectedIndex();
if (selectedIndex === state.lastPolledIndex) {
state.stablePolls += 1;
} else {
state.stablePolls = 0;
}
state.lastPolledIndex = selectedIndex;
return {
ready: selectedIndex >= 0 && state.stablePolls >= 5,
expectedSelectedIndex: state.readExpectedSelectedIndex(),
selectedIndex,
selectedIndexes: [...state.selectedIndexes],
selectedSnapshots: [...state.selectedSnapshots]
};
})()`,
options.timeoutMs,
"stable virtualized locator selection",
100
);
await markRenderer(pageClient, markers.stable);
const result = await evaluate(
pageClient,
`(() => {
const state = window.__tuttiPerfVirtualizedScrollLocator;
const selectedIndexes = [...(state?.selectedIndexes ?? [])];
const selectedSnapshots = [...(state?.selectedSnapshots ?? [])];
state?.observer?.disconnect();
state?.transcriptObserver?.disconnect();
let reversedTransitions = 0;
let returnedTransitions = 0;
for (let index = 1; index < selectedIndexes.length; index += 1) {
if (selectedIndexes[index] > selectedIndexes[index - 1]) {
reversedTransitions += 1;
}
if (
index >= 2 &&
selectedIndexes[index] === selectedIndexes[index - 2] &&
selectedIndexes[index] !== selectedIndexes[index - 1]
) {
returnedTransitions += 1;
}
}
return {
endVirtualTurnKeys: [...document.querySelectorAll('[data-agent-transcript-virtual-turn]')]
.map((element) => element.getAttribute('data-agent-transcript-virtual-turn') ?? '')
.filter(Boolean),
reversedTransitions,
returnedTransitions,
selectedIndexes,
selectedSnapshots,
startVirtualTurnKeys: [...(state?.startVirtualTurnKeys ?? [])],
maxHistoricalInteractiveNodes: state?.maxHistoricalInteractiveNodes ?? 0,
maxMountedMentions: state?.maxMountedMentions ?? 0,
maxStaticRichTextNodes: state?.maxStaticRichTextNodes ?? 0
};
})()`
);
await finishRendererScenario(pageClient, markers.end);
return {
...result,
...scroll,
endScrollTop: scroll.endScrollTop,
travel: scroll.startScrollTop - scroll.endScrollTop,
moved: scroll.endScrollTop < scroll.startScrollTop,
selected: settled.selectedIndex >= 0,
expectedSelectedIndex: settled.expectedSelectedIndex,
matchesViewportTurn:
settled.selectedIndex === settled.expectedSelectedIndex,
stable: settled.ready
};
}