mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
perf(desktop): skip unused continuation rail measurements
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
188
desktop/tests/e2e/message-rail-measurement.spec.ts
Normal file
188
desktop/tests/e2e/message-rail-measurement.spec.ts
Normal 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);
|
||||
});
|
||||
88
perf/CHANNEL_ACTION_RAIL.md
Normal file
88
perf/CHANNEL_ACTION_RAIL.md
Normal 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.
|
||||
Reference in New Issue
Block a user