diff --git a/docs/agent-stream-performance.md b/docs/agent-stream-performance.md
index 4f6dc6de2e..f0d5e2cb12 100644
--- a/docs/agent-stream-performance.md
+++ b/docs/agent-stream-performance.md
@@ -39,7 +39,7 @@ So arrival sets a _target_ and the reveal rate is derived from the backlog inste
memo boundary each coalesced tick re-rendered every mounted row (about 50 on a phone, 100–250 ms of
JS per tick). `layoutStream` keeps a layout item's identity when nothing about it changed,
`useRevisedHistoryRows` hands a fresh item identity to rows whose tool-call group, expanded state,
- or breakpoint changed, and `HistoryStreamRow` memoizes on both. Every viewport runs its history
+ or breakpoint changed, and `StreamRow` memoizes on both. Every viewport runs its history
through that hook; the web viewport once skipped it and history hosts of a live tool group went
stale. A new field on `StreamLayoutItem` must be added to `areLayoutItemsEquivalent`, or sharing
silently stops.
diff --git a/packages/app/src/components/markdown/renderer.test.ts b/packages/app/src/components/markdown/renderer.test.ts
index a3d4224b05..4f4dcccf2a 100644
--- a/packages/app/src/components/markdown/renderer.test.ts
+++ b/packages/app/src/components/markdown/renderer.test.ts
@@ -403,12 +403,34 @@ it("reserves pending image geometry until decode succeeds and collapses only con
);
});
-it("normalizes a large HTML table without repeated prefix scans", () => {
+it("normalizes a large HTML table with linear bounded-source slicing", () => {
const cell = "cell".repeat(8);
- const source = `
${`| ${cell} | ${cell} |
`.repeat(8000)}
`;
- const started = performance.now();
- const rendered = normalizeMarkdownHtml(source);
- expect(performance.now() - started).toBeLessThan(3000);
- expect(rendered.split("\n\n")).toHaveLength(8000);
- expect(rendered.startsWith(`${cell} ${cell}`)).toBe(true);
-}, 20000);
+ const measure = (rows: number) => {
+ const source = `${`| ${cell} | ${cell} |
`.repeat(rows)}
`;
+ const originalSlice = String.prototype.slice;
+ let inspectedCharacters = 0;
+ // Observe real slices without changing their results. Repeated source.slice(0, offset)
+ // prefix scans grow quadratically; inspecting only each new gap grows linearly.
+ // eslint-disable-next-line no-extend-native -- Test-only observation, restored in finally.
+ String.prototype.slice = function (start, end) {
+ const result = originalSlice.call(this, start, end);
+ if (String(this) === source && end !== undefined) {
+ inspectedCharacters += result.length;
+ }
+ return result;
+ };
+ let rendered: string;
+ try {
+ rendered = normalizeMarkdownHtml(source);
+ } finally {
+ // eslint-disable-next-line no-extend-native -- Restore the observed built-in unchanged.
+ String.prototype.slice = originalSlice;
+ }
+ expect(rendered.split("\n\n")).toHaveLength(rows);
+ expect(rendered.startsWith(`${cell} ${cell}`)).toBe(true);
+ return inspectedCharacters;
+ };
+ const baseline = measure(200);
+ expect(baseline).toBeGreaterThan(0);
+ expect(measure(800)).toBeLessThanOrEqual(baseline * 4);
+});