mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 18:52:39 +00:00
* perf(app): stop re-walking the timeline on every streamed delta Every coalesced delta rebuilds the stream render model with the same tail. The model re-sliced the tail at the history-window start on every call, so the rendered tail was a fresh array per delta and every WeakMap keyed on it (ordering, history split, turn timing, the layout history cache, row revision) missed each time. Cache the slice by tail identity and window start. Turn timing then still walked the whole rendered tail per delta and allocated a new assistant-timing Map, which the layout memo depends on. Split the walk: the tail is walked once per tail identity and the head is walked from the carried open turn per delta. The Map keeps its identity while the head adds no completed turn. Adds identity-contract tests and a vitest bench for the per-delta pipeline. 200 deltas over a 2,000-item timeline, mean per run: default mounted window 5.68ms -> 2.82ms, history fully revealed 16.95ms -> 1.81ms. Fixes #4816 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(app): keep the stream view's per-delta work bounded by the head The chat outline concatenated tail and head, reduced over the result in the render body, and rebuilt jumpToPrompt on every delta even when the outline was disabled, which is every native viewport. Memoize the tail's contribution, scan only the head per delta, do nothing while disabled, make jumpToPrompt a stable event so the rail's memo holds, and republish the active prompt only when the index or reading position changes. The view built a Set of every mounted id plus the live head per delta for the outline's pending-jump check; key it on mounted history only and treat live head rows as always mounted. Split plugin projection so a head change does not re-project the tail, memoize the history-window boundary lookup, and read the turn presentation's submission flag with some() instead of allocating a filtered array. Refs #4816 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(app): confirm full history before measuring streaming --------- Co-authored-by: Ivan Malison <IvanMalison@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
287 lines
8.8 KiB
TypeScript
287 lines
8.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { StreamItem } from "@/types/stream";
|
|
import { projectPluginTimelineItems } from "@/plugins/timeline/projection";
|
|
import { findMountedWindowStart } from "./history-window";
|
|
import { buildAgentStreamRenderModel } from "./model";
|
|
|
|
function createTimestamp(seed: number): Date {
|
|
return new Date(`2026-01-01T00:00:${seed.toString().padStart(2, "0")}.000Z`);
|
|
}
|
|
|
|
function userMessage(id: string, seed: number): StreamItem {
|
|
return {
|
|
kind: "user_message",
|
|
id,
|
|
text: id,
|
|
timestamp: createTimestamp(seed),
|
|
};
|
|
}
|
|
|
|
function assistantMessage(id: string, seed: number): StreamItem {
|
|
return {
|
|
kind: "assistant_message",
|
|
id,
|
|
text: id,
|
|
timestamp: createTimestamp(seed),
|
|
};
|
|
}
|
|
|
|
describe("buildAgentStreamRenderModel", () => {
|
|
it("projects a bounded recent turn-aligned history window on every platform", () => {
|
|
const tail = [
|
|
userMessage("u1", 1),
|
|
assistantMessage("a1", 2),
|
|
userMessage("u2", 3),
|
|
assistantMessage("a2", 4),
|
|
userMessage("u3", 5),
|
|
assistantMessage("a3", 6),
|
|
];
|
|
|
|
const model = buildAgentStreamRenderModel({
|
|
isTurnActive: false,
|
|
activeTurnStartedAt: null,
|
|
tail,
|
|
head: [],
|
|
platform: "native",
|
|
isMobileBreakpoint: false,
|
|
historyStart: 2,
|
|
});
|
|
|
|
expect(model.history.map((item) => item.id)).toEqual(["a3", "u3", "a2", "u2"]);
|
|
expect(model.segments.historyVirtualized).toHaveLength(0);
|
|
});
|
|
|
|
it("derives timing only for the rendered history window", () => {
|
|
const tail = [
|
|
userMessage("hidden-u", 1),
|
|
assistantMessage("hidden-a", 2),
|
|
userMessage("visible-u", 3),
|
|
assistantMessage("visible-a", 4),
|
|
];
|
|
|
|
const model = buildAgentStreamRenderModel({
|
|
isTurnActive: false,
|
|
activeTurnStartedAt: null,
|
|
tail,
|
|
head: [],
|
|
platform: "web",
|
|
isMobileBreakpoint: false,
|
|
historyStart: 2,
|
|
});
|
|
|
|
expect(model.turnTiming.byAssistantId.has("hidden-a")).toBe(false);
|
|
expect(model.turnTiming.byAssistantId.get("visible-a")).toEqual({
|
|
completedAt: tail[3]?.timestamp,
|
|
durationMs: 1000,
|
|
});
|
|
});
|
|
|
|
it("keeps the mounted boundary stable when a transformer filters an earlier item", () => {
|
|
const tail = [
|
|
userMessage("filtered", 1),
|
|
assistantMessage("hidden", 2),
|
|
userMessage("visible-u", 3),
|
|
assistantMessage("visible-a", 4),
|
|
];
|
|
|
|
const projectedTail = projectPluginTimelineItems(tail, ({ sourceId }) =>
|
|
sourceId === "filtered" ? [] : undefined,
|
|
);
|
|
const historyStart = findMountedWindowStart({ items: projectedTail, minMountedCount: 2 });
|
|
const model = buildAgentStreamRenderModel({
|
|
isTurnActive: false,
|
|
activeTurnStartedAt: null,
|
|
tail: projectedTail,
|
|
head: [],
|
|
platform: "native",
|
|
isMobileBreakpoint: false,
|
|
historyStart,
|
|
});
|
|
|
|
expect(model.history.map((item) => item.id)).toEqual(["visible-a", "visible-u"]);
|
|
});
|
|
|
|
it("keeps head separate from committed history on desktop web", () => {
|
|
const tail: StreamItem[] = [];
|
|
for (let index = 0; index < 60; index += 1) {
|
|
const seed = index * 2;
|
|
tail.push(userMessage(`u${index}`, seed + 1));
|
|
tail.push(assistantMessage(`a${index}`, seed + 2));
|
|
}
|
|
const head = [assistantMessage("live-a", 121)];
|
|
|
|
const model = buildAgentStreamRenderModel({
|
|
isTurnActive: true,
|
|
activeTurnStartedAt: tail.at(-2)?.timestamp ?? null,
|
|
tail,
|
|
head,
|
|
platform: "web",
|
|
isMobileBreakpoint: false,
|
|
});
|
|
|
|
expect(model.segments.historyVirtualized.length).toBeGreaterThan(0);
|
|
expect(model.segments.historyMounted.length).toBeGreaterThan(0);
|
|
expect(model.segments.liveHead.map((item) => item.id)).toEqual(["live-a"]);
|
|
expect(model.history).not.toContain(head[0]);
|
|
});
|
|
|
|
it("keeps the full committed tail mounted on mobile web", () => {
|
|
const tail = [userMessage("u1", 1), assistantMessage("a1", 2)];
|
|
const head = [assistantMessage("live-a", 3)];
|
|
|
|
const model = buildAgentStreamRenderModel({
|
|
isTurnActive: true,
|
|
activeTurnStartedAt: tail[0]?.timestamp ?? null,
|
|
tail,
|
|
head,
|
|
platform: "web",
|
|
isMobileBreakpoint: true,
|
|
});
|
|
|
|
expect(model.segments.historyVirtualized).toHaveLength(0);
|
|
expect(model.segments.historyMounted).toBe(tail);
|
|
expect(model.segments.liveHead).toBe(head);
|
|
});
|
|
|
|
it("reuses ordered committed history when only the live head changes", () => {
|
|
const tail = [userMessage("u1", 1), assistantMessage("a1", 2)];
|
|
const firstHead = [assistantMessage("live-a", 3)];
|
|
const secondHead = [assistantMessage("live-b", 4)];
|
|
|
|
const first = buildAgentStreamRenderModel({
|
|
isTurnActive: true,
|
|
activeTurnStartedAt: tail[0]?.timestamp ?? null,
|
|
tail,
|
|
head: firstHead,
|
|
platform: "native",
|
|
isMobileBreakpoint: false,
|
|
});
|
|
const second = buildAgentStreamRenderModel({
|
|
isTurnActive: true,
|
|
activeTurnStartedAt: tail[0]?.timestamp ?? null,
|
|
tail,
|
|
head: secondHead,
|
|
platform: "native",
|
|
isMobileBreakpoint: false,
|
|
});
|
|
|
|
expect(first.history).toBe(second.history);
|
|
expect(first.segments.historyMounted).toBe(second.segments.historyMounted);
|
|
expect(second.segments.liveHead.map((item) => item.id)).toEqual(["live-b"]);
|
|
});
|
|
|
|
it("derives running turn timing across committed history and live head", () => {
|
|
const tail = [userMessage("u1", 1)];
|
|
const head = [assistantMessage("live-a", 4)];
|
|
|
|
const model = buildAgentStreamRenderModel({
|
|
isTurnActive: true,
|
|
activeTurnStartedAt: tail[0]?.timestamp ?? null,
|
|
tail,
|
|
head,
|
|
platform: "web",
|
|
isMobileBreakpoint: false,
|
|
});
|
|
|
|
expect(model.turnTiming.runningStartedAt).toBe(tail[0]?.timestamp);
|
|
expect(model.turnTiming.byAssistantId.has("live-a")).toBe(false);
|
|
});
|
|
|
|
it("maps completed turn timing to assistant ids across committed history and live head", () => {
|
|
const tail = [userMessage("u1", 1)];
|
|
const head = [assistantMessage("live-a", 4)];
|
|
|
|
const model = buildAgentStreamRenderModel({
|
|
isTurnActive: false,
|
|
activeTurnStartedAt: null,
|
|
tail,
|
|
head,
|
|
platform: "web",
|
|
isMobileBreakpoint: false,
|
|
});
|
|
|
|
expect(model.turnTiming.runningStartedAt).toBe(null);
|
|
expect(model.turnTiming.byAssistantId.get("live-a")).toEqual({
|
|
completedAt: head[0]?.timestamp,
|
|
durationMs: 3000,
|
|
});
|
|
});
|
|
|
|
it("derives the same timing for native inverted rendering", () => {
|
|
const tail = [userMessage("u1", 1), assistantMessage("a1", 4)];
|
|
|
|
const model = buildAgentStreamRenderModel({
|
|
isTurnActive: false,
|
|
activeTurnStartedAt: null,
|
|
tail,
|
|
head: [],
|
|
platform: "native",
|
|
isMobileBreakpoint: false,
|
|
});
|
|
|
|
expect(model.segments.historyMounted.map((item) => item.id)).toEqual(["a1", "u1"]);
|
|
expect(model.turnTiming.byAssistantId.get("a1")).toEqual({
|
|
completedAt: tail[1]?.timestamp,
|
|
durationMs: 3000,
|
|
});
|
|
});
|
|
|
|
it("does not create completed timing for adjacent user messages", () => {
|
|
const tail = [userMessage("u1", 1), userMessage("u2", 4)];
|
|
|
|
const model = buildAgentStreamRenderModel({
|
|
isTurnActive: false,
|
|
activeTurnStartedAt: null,
|
|
tail,
|
|
head: [],
|
|
platform: "web",
|
|
isMobileBreakpoint: false,
|
|
});
|
|
|
|
expect(model.turnTiming.byAssistantId.size).toBe(0);
|
|
});
|
|
|
|
it.each(["web", "native"] as const)(
|
|
"reuses rendered history and timing across live-head updates inside a history window on %s",
|
|
(platform) => {
|
|
const at = (seed: number) => new Date(Date.UTC(2026, 0, 1, 0, 0, seed));
|
|
const tail: StreamItem[] = [];
|
|
for (let index = 0; index < 40; index += 1) {
|
|
tail.push(
|
|
{ ...userMessage(`u${index}`, 0), timestamp: at(index * 2) },
|
|
{ ...assistantMessage(`a${index}`, 0), timestamp: at(index * 2 + 1) },
|
|
);
|
|
}
|
|
const live = (text: string): StreamItem => ({
|
|
kind: "assistant_message",
|
|
id: "live",
|
|
text,
|
|
timestamp: at(81),
|
|
});
|
|
const build = (head: StreamItem[]) =>
|
|
buildAgentStreamRenderModel({
|
|
isTurnActive: true,
|
|
activeTurnStartedAt: tail.at(-2)?.timestamp ?? null,
|
|
tail,
|
|
head,
|
|
platform,
|
|
isMobileBreakpoint: false,
|
|
historyStart: 60,
|
|
});
|
|
|
|
const first = build([live("live")]);
|
|
const second = build([live("live grows")]);
|
|
|
|
expect(second.history).toBe(first.history);
|
|
expect(second.segments.historyMounted).toBe(first.segments.historyMounted);
|
|
expect(second.turnTiming.byAssistantId).toBe(first.turnTiming.byAssistantId);
|
|
expect(first.history).toHaveLength(20);
|
|
expect(first.turnTiming.byAssistantId.has("a29")).toBe(false);
|
|
expect(first.turnTiming.byAssistantId.get("a38")).toEqual({
|
|
completedAt: tail[77]?.timestamp,
|
|
durationMs: 1000,
|
|
});
|
|
},
|
|
);
|
|
});
|