perf(desktop): skip unused continuation rail measurements

Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
This commit is contained in:
Brain
2026-09-20 15:27:51 -06:00
parent 497e369e75
commit 564a7da84e
4 changed files with 282 additions and 2 deletions

View File

@@ -145,6 +145,7 @@ export default defineConfig({
"**/inbox-refactor-screenshots.spec.ts",
"**/inbox-title-overlap.spec.ts",
"**/message-author-overlap.spec.ts",
"**/message-rail-measurement.spec.ts",
"**/buzz-theme-screenshots.spec.ts",
"**/appearance-previews.spec.ts",
"**/channel-sort.spec.ts",

View File

@@ -219,10 +219,13 @@ export const MessageRow = React.memo(
// takes no layout space of its own. Measure its rendered footprint and
// reserve it in the message-header row (see `headerNode`) so a long author
// name ellipsizes before the rail instead of painting underneath it. The
// reservation is unconditional — the rail appears on hover AND
// focus-within, and padding must not reflow the header mid-interaction.
// reservation is unconditional for displayed headers — the rail appears
// on hover AND focus-within, so padding must not reflow mid-interaction.
// Continuations have no header consuming the width; keep their controls
// mounted without measuring an unused reservation.
const articleRef = React.useRef<HTMLElement | null>(null);
const actionRailMeasureRef = useMeasuredCssVariable({
enabled: !isDisplayedAsContinuation,
cssVariable: "--message-action-rail-width",
dimension: "inline",
resetValue: "0px",

View File

@@ -0,0 +1,188 @@
import { expect, test } from "@playwright/test";
import type { QueryClient } from "@tanstack/react-query";
import type { ChannelWindowStore } from "../../src/features/messages/lib/channelWindowStore";
import type { RelayEvent } from "../../src/shared/api/types";
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
type ProbeWindow = Window & {
__RAIL_PROBES__: { reads: string[]; active: (id: string) => number };
};
// Exercise the production MessageRow caller, not only the hook's enabled option.
// Query-cache transitions below intentionally isolate presentation from send/relay behavior.
test("rail measurement follows displayed headers, including pending transitions", async ({
page,
}) => {
await page.addInitScript(() => {
const active = new Map<ResizeObserver, Set<Element>>();
const reads: string[] = [];
const NativeObserver = ResizeObserver;
const originalRect = Element.prototype.getBoundingClientRect;
Element.prototype.getBoundingClientRect = function () {
const id = this.getAttribute("data-testid");
if (id?.startsWith("message-action-bar-")) reads.push(id);
return originalRect.call(this);
};
window.ResizeObserver = class extends NativeObserver {
observe(target: Element, options?: ResizeObserverOptions) {
const targets = active.get(this) ?? new Set<Element>();
targets.add(target);
active.set(this, targets);
return super.observe(target, options);
}
unobserve(target: Element) {
active.get(this)?.delete(target);
return super.unobserve(target);
}
disconnect() {
active.delete(this);
return super.disconnect();
}
};
(window as ProbeWindow).__RAIL_PROBES__ = {
reads,
active: (id: string) =>
[...active.values()]
.flatMap((targets) => [...targets])
.filter(
(el) =>
el.getAttribute("data-testid") === `message-action-bar-${id}`,
).length,
};
});
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect
.poll(() =>
page.evaluate(() =>
window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
channelName: "general",
}),
),
)
.toBe(true);
const { first, second, channelId } = await page.evaluate((pubkey) => {
const emit = window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
if (!emit) throw new Error("Mock message emitter unavailable");
const timestamp = Math.floor(Date.now() / 1000) + 1000;
const first = emit({
channelName: "general",
content: "Rail header fixture",
pubkey,
createdAt: timestamp,
});
const second = emit({
channelName: "general",
content: "Rail continuation fixture",
pubkey,
createdAt: timestamp + 1,
});
const channelId = first.tags.find((tag) => tag[0] === "h")?.[1];
if (!channelId) throw new Error("Emitted message has no channel");
return {
first: first.id,
second: second.id,
channelId,
};
}, TEST_IDENTITIES.alice.pubkey);
const firstRow = page.locator(`[data-message-id="${first}"]`);
const secondRow = page.locator(`[data-message-id="${second}"]`);
await expect(firstRow.getByTestId("message-header")).toHaveCount(1);
await expect(secondRow.getByTestId("message-body")).toHaveText(
"Rail continuation fixture",
);
await expect(secondRow.getByTestId("message-header")).toHaveCount(0);
const active = (id: string) =>
page.evaluate(
(id) => (window as ProbeWindow).__RAIL_PROBES__.active(id),
id,
);
await expect.poll(() => active(first)).toBe(1);
expect(await active(second)).toBe(0);
expect(
await page.evaluate(
(id) =>
(window as ProbeWindow).__RAIL_PROBES__.reads.filter(
(x: string) => x === `message-action-bar-${id}`,
).length,
second,
),
).toBe(0);
const setPending = async (pending: boolean) => {
await page.evaluate(
({ id, channelId, pending }) => {
const client = window.__BUZZ_E2E_QUERY_CLIENT__ as
| QueryClient
| undefined;
if (!client) throw new Error("Query client unavailable");
const update = (event: RelayEvent) =>
event.id === id ? { ...event, pending } : event;
client.setQueryData<RelayEvent[]>(
["channel-messages", channelId],
(events) => events?.map(update),
);
client.setQueryData<ChannelWindowStore>(
["channel-window", channelId],
(store) =>
store
? {
...store,
pages: store.pages.map((page) => ({
...page,
rows: page.rows.map((row) => ({
...row,
event: update(row.event),
})),
})),
liveOverlay: store.liveOverlay.map(update),
}
: store,
);
},
{ id: second, channelId, pending },
);
};
// A pending continuation still shows a header: bare isContinuation is wrong.
await setPending(true);
await expect(secondRow.getByTestId("message-header")).toHaveCount(1);
await expect(secondRow.getByTestId("message-send-status")).toHaveText(
"Sending…",
);
await expect.poll(() => active(second)).toBe(1);
const railWidth = () =>
secondRow.evaluate(
(el) =>
Number.parseFloat(
getComputedStyle(el).getPropertyValue("--message-action-rail-width"),
) || 0,
);
await expect.poll(railWidth).toBeGreaterThan(0);
const rail = secondRow.getByTestId(`message-action-bar-${second}`);
const measuredBeforeResize = await railWidth();
await rail.evaluate((element, width) => {
element.style.width = `${width}px`;
}, measuredBeforeResize + 40);
await expect.poll(railWidth).toBe(measuredBeforeResize + 40);
await rail.evaluate((element) => element.style.removeProperty("width"));
await expect.poll(railWidth).toBe(measuredBeforeResize);
await page.setViewportSize({ width: 900, height: 700 });
await expect.poll(railWidth).toBeGreaterThan(0);
await setPending(false);
await expect(secondRow.getByTestId("message-header")).toHaveCount(0);
await expect.poll(() => active(second)).toBe(0);
expect(await railWidth()).toBe(0);
// Controls stay mounted and reveal on hover/focus without measuring unused width.
await secondRow.hover();
await expect(rail).toHaveCSS("opacity", "1");
await page.mouse.move(0, 0);
await rail.getByRole("button").first().focus();
await expect(rail).toHaveCSS("opacity", "1");
await setPending(true);
await expect.poll(() => active(second)).toBe(1);
await page.getByTestId("channel-random").click();
await expect(secondRow).toHaveCount(0);
await expect.poll(() => active(second)).toBe(0);
});

View File

@@ -0,0 +1,88 @@
# Warm channel switching: unused action-rail measurement
## Scope
Skip action-rail width measurement only when `MessageRow` has no displayed
header: `enabled: !isDisplayedAsContinuation`. The existing predicate is
`isContinuation && !message.pending`, so pending messages retain measurement.
The existing hook owns observer cleanup, reset, and re-enable.
Action controls remain mounted. Header reservation on hover/keyboard focus,
loading UI, corners, retention, scrolling, subscription freshness reads, and
transport are unchanged. No new scheduler, cache, or lifecycle owner.
This is a modest rendering improvement, **not a resolution of multi-second
native startup, channel switching, or relay waits**.
## Measurement
Baseline: `497e369e75cfa2ed8caeb1204e7233f30c6a7f90`.
Candidate `MessageRow.tsx` SHA-256:
`5ef36df5cfad76887c5a6b1a5ecf0c653e77019067d598b4e15f58f97ce86f27`.
Chromium 148.0.7778.96, production-optimized Vite e2e builds, mock Tauri/relay,
1280x800. Three alternating baseline/candidate context pairs per CPU rate;
two measured warm re-entries per scenario per context: **six samples per cell**.
Date fixed, native timers/rAF preserved; CPU profiling ran separately.
| Scenario | CPU | Visible newest message, median | Max long task, median |
|---|---:|---:|---:|
| Plain, alternating authors | 1x | 88.6 -> 92.2 ms | 0 -> 0 ms |
| Rich, grouped messages | 1x | 178.4 -> 167.4 ms | 105.5 -> 87.0 ms |
| Plain, alternating authors | 4x | 396.0 -> 402.6 ms | 159.5 -> 161.0 ms |
| Rich, grouped messages | 4x | 766.4 -> 689.0 ms | 448.5 -> 373.5 ms |
Rich readiness improved in each of the three pair medians at both rates.
Plain rows show no consistent benefit. This is about 11ms unthrottled in the
rich fixture; 4x throttling is a stress comparison, not a hardware model.
Readiness requires the correct newest message body above the composer, visible
within the timeline, for two native animation frames. One further second counts
deferred work. Mounted rows remain 50 plain / 60 rich. No fresh Markdown parses
occurred; the existing cache already serves warm switches. The intentional
subscription-handoff freshness read remains.
The build-only rail experiment and actual edited-source build contain 547
byte-identical files. Raw samples, source/bundle hash manifest, diagnostic CPU
profiles and harness are retained in the local evidence package
`RESEARCH/OLD_BUZZ_CHANNEL_SWITCH_PROFILE_2026_09_20/` in the agent workspace.
These measurements do not establish native/WebKit paint or live-relay latency.
A smaller initial retention window was rejected because deferred plain-row work
increased. Disabling corners diagnostically left most cost intact; a global
scheduler was not justified. Neither experiment is included in this patch.
## Regression evidence
`desktop/tests/e2e/message-rail-measurement.spec.ts` instruments the real
production caller: displayed header gets an observer; continuation gets neither
an observer nor an initial width read; pending transitions enable/disable/reset
and re-enable it; changing rail width by 40px must update the reservation exactly
and removing that width must restore it. Navigation cleans up. Hover/focus still
reveals continuation controls. Cache-driven pending transitions test presentation,
not send/relay lifecycle; visible controls do not prove every action executes.
The original production build fails at the continuation observer assertion
(expected zero, got one). The candidate passes. The regression is registered in
the regular smoke project, not only a temporary test configuration.
Validation on the candidate:
- Standard command below builds the actual source and passes all four checks in
the three complete spec files. No retries.
- 6,531 desktop unit tests pass through the existing pre-push desktop-test hook.
- Desktop lint/static checks pass with existing warnings; TypeScript passes.
- Production protected-feature artifact matrix build passes.
- Independent read-only review found no production correctness blocker. Smoke
registration and exact resize assertion were strengthened after review.
```bash
. ./bin/activate-hermit
cd desktop
pnpm test:e2e:smoke message-rail-measurement.spec.ts \
message-author-overlap.spec.ts message-feedback-snapshots.spec.ts
```
Full repository CI, complete browser smoke, native/WebKit behavior, and release
readiness are not certified by these checks. No live app was restarted, cache
changed, or relay deployed for this work.