feat(web): shared rows swipe to Remove me, and have their menu again #1041

Open
kayg wants to merge 4 commits from fix/shared-row-swipes into dev
Owner

Swipe actions on every list (kayg 11-08): shared rows had neither a swipe nor a menu. They were drawn outside RowActions, which also dropped their right-click/hold menu.

Change

  • Shared rows are RowActions of a new kind, shared:
    • the menu is the shared-note menu: Open, Open in focus, Remove me;
    • swipe right-to-left shows Remove me (danger; a long swipe runs it and the row leaves the list);
    • they're not multi-selected together with your own notes.
  • The swipe action registers from the lazy sharing-sync module, which has always loaded before a shared row can exist. The first screen only gains the kind/menu props on the existing RowActions call.
  • Spec: a new test, a shared row has row actions: its menu, and a swipe to Remove me (wide screens, trackpad swipe). It checks the menu (no Pin/Delete), swipes, presses Remove me in the tray, and the row leaves the list.

Gates (head c226c4da4)

gate result
svelte-check / web unit 0 errors / pass
bundle gate pass. first_js 223.681 vs dev 223.653 (+0.03 KB, the props on one RowActions call; inside build noise)
admin-sharing ×3 admin-chromium-1440 16 · admin-chromium-390 13 (+3 skipped: the swipe test is wide-only) · admin-webkit-1440 16 · admin-webkit-390 13 (+3 skipped)
swipe-rows, select-rows, preload, lazy-ui, lazy-offline, list-chrome, list-fit, switching chromium-1440 58 · chromium-390 60 · webkit-1440 57 · webkit-390 57 passed, 0 failed

Nit for review: "Remove me" wraps to two lines in the tray button (screenshot). Keeping the menu's wording; say if you'd rather the tray read "Remove".

🤖 Generated with Claude Code

Swipe actions on every list (kayg 11-08): shared rows had **neither a swipe nor a menu**. They were drawn outside `RowActions`, which also dropped their right-click/hold menu. ## Change - Shared rows are `RowActions` of a new kind, `shared`: - the menu is the shared-note menu: Open, Open in focus, Remove me; - swipe right-to-left shows **Remove me** (danger; a long swipe runs it and the row leaves the list); - they're not multi-selected together with your own notes. - The swipe action registers from the lazy `sharing-sync` module, which has always loaded before a shared row can exist. The first screen only gains the `kind`/`menu` props on the existing `RowActions` call. - Spec: a new test, `a shared row has row actions: its menu, and a swipe to Remove me` (wide screens, trackpad swipe). It checks the menu (no Pin/Delete), swipes, presses Remove me in the tray, and the row leaves the list. ## Gates (head `c226c4da4`) | gate | result | |---|---| | svelte-check / web unit | 0 errors / pass | | bundle gate | pass. first_js 223.681 vs dev 223.653 (+0.03 KB, the props on one `RowActions` call; inside build noise) | | admin-sharing ×3 | admin-chromium-1440 16 · admin-chromium-390 13 (+3 skipped: the swipe test is wide-only) · admin-webkit-1440 16 · admin-webkit-390 13 (+3 skipped) | | swipe-rows, select-rows, preload, lazy-ui, lazy-offline, list-chrome, list-fit, switching | chromium-1440 58 · chromium-390 60 · webkit-1440 57 · webkit-390 57 passed, 0 failed | Nit for review: "Remove me" wraps to two lines in the tray button (screenshot). Keeping the menu's wording; say if you'd rather the tray read "Remove". 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Shared rows were drawn without RowActions, so they had no swipe and no
menu at all. They are now RowActions of kind "shared": the menu is the
shared-note menu, the swipe action registers from the lazy sharing-sync
module (nothing on the first screen), and they are not picked together
with your own notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(web): shared-row actions in their own test, ending with Remove me from the swipe tray
Some checks are pending
CI / server (pull_request) Waiting to run
CI / rust (pull_request) Waiting to run
CI / web (pull_request) Waiting to run
Markdown extraction contract / markdown-native-contract (pull_request) Waiting to run
Markdown extraction contract / markdown-editor-contract (pull_request) Waiting to run
c226c4da49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Shared row swiped (Chromium 1440, 2×):

shared-row-swipe-1440-dark.png

shared-row-swipe-1440-light.png

Shared row swiped (Chromium 1440, 2×): shared-row-swipe-1440-dark.png<br><img src="https://git.kayg.org/attachments/1888eeb2-4f40-4eb2-8530-e8b7ab8d3ad8" width="600"> shared-row-swipe-1440-light.png<br><img src="https://git.kayg.org/attachments/da7d5db4-5344-41ae-b37a-10a676c3f803" width="600">
Author
Owner

Test lane result for #1041, head f0ccc37ed (rebased onto dev 9438918f3; the icon-only rule commit 66477a1fe content is in, as c7000fdd9).

Seed script: scripts/sharing-sim-seed.sh is on the PR with its usage and what it makes documented in its header. The std runner's testSwipe and SIM_UDID were already in ~/Developer/calternal-sim and are in its README, so nothing was added.

iOS Simulator (iPhone 18 Pro, iOS 27, Safari, own simulator under the std lock):

  • Shared with me list, light + dark: ok.
  • Row swiped open: icon-only strip (trash icon, no label), light + dark: ok.
  • Long swipe: the row runs Remove me and leaves, list goes to "1 note", light + dark: ok.
  • No Undo toast: "Remove me" only hides the note (notes.hide-shared; the share stays), and the code has no Undo for it. I saw none in the sim and none in the code. If an Undo is wanted, that is a small follow-up.
  • The row ⋯ menu could not be reached by touch in the sim (a zero-travel long press just opens the note). It is covered by the Playwright spec (Open, Open in focus, Remove me). The list header ⋯ is shown (light).
  • Seen once in the sim: opening the edit-share note showed "Could not save" on the toast. This is a --dev-user seed server over plain http with a localhost forward, and may well be that setup, not this PR. Not investigated further.

Gates (dev-02, built from the PR head, one ~/bin/slot each):

  • admin-sharing.spec ×2 (--repeat-each=2) on admin-chromium-1440/390 and admin-webkit-1440/390: 37 passed, 4 skipped, 0 failed.
  • swipe-rows, swipe-guard, tasks-swipe on chromium-1440/390 and webkit-1440/390: 113 passed, 31 skipped, 0 failed.
  • Shots (E2E_SHOTS, 2x) of admin-sharing, light and dark, on the four admin projects: 19 passed each.
  • Not run: separate 430 and 800 widths (there is no project at those widths; only 390 and 1440 exist). The desktop row-swipe shot exists at 1440 only (the spec drives a two-finger scroll, which is desktop).

Shots: sim-* are the iPhone sim, the rest are Playwright (Chromium and WebKit share file names, so WebKit's overwrite Chromium's).

shared-list-1440-dark.png

shared-list-1440-light.png

shared-list-390-dark.png

shared-list-390-light.png

shared-menu-1440-dark.png

shared-menu-1440-light.png

shared-menu-390-dark.png

shared-menu-390-light.png

shared-row-swipe-1440-dark.png

shared-row-swipe-1440-light.png

sim-list-dark.png

sim-list-light.png

sim-list-menu-light.png

sim-removed-dark.png

sim-removed-light.png

sim-swipe-open-dark.png

sim-swipe-open-light.png

**Test lane result for #1041, head `f0ccc37ed`** (rebased onto dev `9438918f3`; the icon-only rule commit `66477a1fe` content is in, as `c7000fdd9`). **Seed script:** `scripts/sharing-sim-seed.sh` is on the PR with its usage and what it makes documented in its header. The std runner's `testSwipe` and `SIM_UDID` were already in `~/Developer/calternal-sim` and are in its README, so nothing was added. **iOS Simulator (iPhone 18 Pro, iOS 27, Safari, own simulator under the std lock):** - Shared with me list, light + dark: ok. - Row swiped open: icon-only strip (trash icon, no label), light + dark: ok. - Long swipe: the row runs Remove me and leaves, list goes to "1 note", light + dark: ok. - **No Undo toast:** "Remove me" only hides the note (`notes.hide-shared`; the share stays), and the code has no Undo for it. I saw none in the sim and none in the code. If an Undo is wanted, that is a small follow-up. - The row ⋯ menu could not be reached by touch in the sim (a zero-travel long press just opens the note). It is covered by the Playwright spec (Open, Open in focus, Remove me). The list header ⋯ is shown (light). - Seen once in the sim: opening the edit-share note showed "Could not save" on the toast. This is a `--dev-user` seed server over plain http with a localhost forward, and may well be that setup, not this PR. Not investigated further. **Gates (dev-02, built from the PR head, one `~/bin/slot` each):** - `admin-sharing.spec` ×2 (`--repeat-each=2`) on admin-chromium-1440/390 and admin-webkit-1440/390: 37 passed, 4 skipped, 0 failed. - `swipe-rows`, `swipe-guard`, `tasks-swipe` on chromium-1440/390 and webkit-1440/390: 113 passed, 31 skipped, 0 failed. - Shots (`E2E_SHOTS`, 2x) of `admin-sharing`, light and dark, on the four admin projects: 19 passed each. - Not run: separate 430 and 800 widths (there is no project at those widths; only 390 and 1440 exist). The desktop row-swipe shot exists at 1440 only (the spec drives a two-finger scroll, which is desktop). Shots: sim-* are the iPhone sim, the rest are Playwright (Chromium and WebKit share file names, so WebKit's overwrite Chromium's). shared-list-1440-dark.png<br><img src="https://git.kayg.org/attachments/ce56ee5d-7f27-4c30-b96b-14d5b808356c" width="300"> shared-list-1440-light.png<br><img src="https://git.kayg.org/attachments/fe95d3c9-6500-48aa-9cf1-d4b6d921b13d" width="300"> shared-list-390-dark.png<br><img src="https://git.kayg.org/attachments/16a7d5b8-da48-4dd5-8b38-04b50866365c" width="300"> shared-list-390-light.png<br><img src="https://git.kayg.org/attachments/077999a8-a165-4afe-a9d6-115a0d5185a1" width="300"> shared-menu-1440-dark.png<br><img src="https://git.kayg.org/attachments/c37d1e3c-376c-4a3e-a9f8-d7704bf5161e" width="300"> shared-menu-1440-light.png<br><img src="https://git.kayg.org/attachments/66d6bad9-1529-4fe0-93f8-56c9aa5284c5" width="300"> shared-menu-390-dark.png<br><img src="https://git.kayg.org/attachments/2ff37942-f16c-47c8-8f57-e77b41ff5061" width="300"> shared-menu-390-light.png<br><img src="https://git.kayg.org/attachments/000e7b50-9042-4895-b352-d4ef672956b1" width="300"> shared-row-swipe-1440-dark.png<br><img src="https://git.kayg.org/attachments/308ea090-2d10-4c98-b49a-48fadbfa65df" width="300"> shared-row-swipe-1440-light.png<br><img src="https://git.kayg.org/attachments/4c493bd7-80f0-4ae5-b016-b8ba0fd95ecf" width="300"> sim-list-dark.png<br><img src="https://git.kayg.org/attachments/410e42bb-9900-433f-9586-99a6627e64a3" width="300"> sim-list-light.png<br><img src="https://git.kayg.org/attachments/ab6a4db6-b809-4740-bb6f-28d26e9096d2" width="300"> sim-list-menu-light.png<br><img src="https://git.kayg.org/attachments/1bede882-080b-4fc2-a4c7-b3a76fc5c3bd" width="300"> sim-removed-dark.png<br><img src="https://git.kayg.org/attachments/e137d94a-d9bb-43cc-b439-8dcf9f486d9e" width="300"> sim-removed-light.png<br><img src="https://git.kayg.org/attachments/eb9c5277-8c13-4663-a268-e72e937de13a" width="300"> sim-swipe-open-dark.png<br><img src="https://git.kayg.org/attachments/2fb839c7-7c18-42da-b81b-480f756209bf" width="300"> sim-swipe-open-light.png<br><img src="https://git.kayg.org/attachments/f57e7f54-2c7f-47d5-b85d-4f718462fe22" width="300">
Author
Owner

Launch safety for recipients: "Could not save" was a --dev-user artefact, not a bug on dev.

New spec in admin-sharing (a recipient who can edit is saved: no "Could not save", the words reach the owner live and survive a reload, commit d4577e908): the owner has the note open; a recipient with Can edit types, the page reaches data-save="saved" with no refusal toast, the owner's open editor shows the words without a reload, the server's copy (notes.get) has them, and after the recipient reloads they are still there with no refusal.

Run on plain dev dd6eebd43 (no PR code), real sign-in on the admin projects' https server, --repeat-each=3 on admin-chromium-1440, admin-chromium-390, admin-webkit-1440 and admin-webkit-390: 13 passed (12 runs plus admin-setup), 0 failed.

The earlier "Could not save" (iPhone sim, seed script) reproduces only on a --dev-user server over plain http: the recipient's room join is answered forbidden, because a recipient's write needs an installation and a --dev-user session has none (ensure_share_controls_applied). A real sign-in carries one. So recipients are not affected on a real server; the seed script is only good for looking at the list and swipes, not for editing as the recipient.

**Launch safety for recipients: "Could not save" was a `--dev-user` artefact, not a bug on dev.** New spec in `admin-sharing` (*a recipient who can edit is saved: no "Could not save", the words reach the owner live and survive a reload*, commit `d4577e908`): the owner has the note open; a recipient with Can edit types, the page reaches `data-save="saved"` with no refusal toast, the owner's open editor shows the words without a reload, the server's copy (`notes.get`) has them, and after the recipient reloads they are still there with no refusal. Run on plain dev `dd6eebd43` (no PR code), real sign-in on the admin projects' https server, `--repeat-each=3` on admin-chromium-1440, admin-chromium-390, admin-webkit-1440 and admin-webkit-390: **13 passed (12 runs plus admin-setup), 0 failed.** The earlier "Could not save" (iPhone sim, seed script) reproduces only on a `--dev-user` server over plain http: the recipient's room join is answered `forbidden`, because a recipient's write needs an installation and a `--dev-user` session has none (`ensure_share_controls_applied`). A real sign-in carries one. So recipients are not affected on a real server; the seed script is only good for looking at the list and swipes, not for editing as the recipient.
Author
Owner

#1041 rebased onto dev dd6eebd43; gates for the Undo of "Remove me", head d4577e908 (dev-01, x86_64; the server binary is dev's, since the PR has no Rust).

Undo: leaveShared shows a toast "Removed “…” from Shared with me" with Undo, which calls notes.hide-shared {share_id, hidden: false} (the exact reverse of the hide) and reads the list again. Spec a long swipe on a shared row runs Remove me, and Undo puts the row back: long swipe -> row gone -> toast has Undo -> Undo -> row back -> still there after a reload.

Gate Result
swipe-rows, swipe-guard, tasks-swipe on chromium-1440, chromium-390, webkit-1440, webkit-390 113 passed, 31 skipped, 0 failed
admin-sharing --repeat-each=2 on the 4 admin projects (includes the new Undo spec, desktop projects, and the recipient-edit spec) 56 passed, 8 skipped, 1 failed (see below)
contrast.spec (project contrast) 9 passed
admin-contrast on the 4 admin projects 7 passed, 2 failed (see below)
svelte-check 0 errors, 18 warnings (unchanged)
vitest 22 files, 223 tests passed

Two failures, neither from this PR, per the flaky rule:

  • admin-sharing a shared note catches up after its recipient is offline, admin-webkit-1440: the editor was empty for 5 s once. Alone, --repeat-each=10 passes 10/10 on this PR head and 10/10 on dev dd6eebd43.
  • admin-contrast accent buttons are at full strength, admin-webkit-1440: hover() on the invite sheet's Copy chip waits "visible and stable" until the 30 s timeout. This one depends on host load: PR head 3 failed of 21 and dev 0 of 21 in two runs at different loads, then an interleaved run with the host at load 14–28 (a daily train and other agents' jobs were using dev-01): PR 2 failed of 11, dev 4 failed of 11. Dev fails the same way, so it is not this PR; I stopped the comparison after the first pair to stop adding load. It deserves a stability issue (the Copy chip's hover target keeps moving).

Not done: the phone Undo toast has not been looked at in the iPhone Simulator (the earlier sim shots are from before Undo existed); the perf VMs and the first-paint budget were not run for this push (the earlier push moved the budgets by +0.2 for the press chunk; the Undo adds one undoable import to the lazy sharing module).

**#1041 rebased onto dev `dd6eebd43`; gates for the Undo of "Remove me", head `d4577e908`** (dev-01, x86_64; the server binary is dev's, since the PR has no Rust). Undo: `leaveShared` shows a toast "Removed “…” from Shared with me" with Undo, which calls `notes.hide-shared {share_id, hidden: false}` (the exact reverse of the hide) and reads the list again. Spec *a long swipe on a shared row runs Remove me, and Undo puts the row back*: long swipe -> row gone -> toast has Undo -> Undo -> row back -> still there after a reload. | Gate | Result | |---|---| | `swipe-rows`, `swipe-guard`, `tasks-swipe` on chromium-1440, chromium-390, webkit-1440, webkit-390 | 113 passed, 31 skipped, 0 failed | | `admin-sharing` `--repeat-each=2` on the 4 admin projects (includes the new Undo spec, desktop projects, and the recipient-edit spec) | 56 passed, 8 skipped, 1 failed (see below) | | `contrast.spec` (project `contrast`) | 9 passed | | `admin-contrast` on the 4 admin projects | 7 passed, 2 failed (see below) | | svelte-check | 0 errors, 18 warnings (unchanged) | | vitest | 22 files, 223 tests passed | **Two failures, neither from this PR, per the flaky rule:** - `admin-sharing` *a shared note catches up after its recipient is offline*, admin-webkit-1440: the editor was empty for 5 s once. Alone, `--repeat-each=10` passes 10/10 on this PR head and 10/10 on dev `dd6eebd43`. - `admin-contrast` *accent buttons are at full strength*, admin-webkit-1440: `hover()` on the invite sheet's Copy chip waits "visible and stable" until the 30 s timeout. This one depends on host load: PR head 3 failed of 21 and dev 0 of 21 in two runs at different loads, then an interleaved run with the host at load 14–28 (a daily train and other agents' jobs were using dev-01): PR 2 failed of 11, dev **4 failed of 11**. Dev fails the same way, so it is not this PR; I stopped the comparison after the first pair to stop adding load. It deserves a stability issue (the Copy chip's hover target keeps moving). **Not done:** the phone Undo toast has not been looked at in the iPhone Simulator (the earlier sim shots are from before Undo existed); the perf VMs and the first-paint budget were not run for this push (the earlier push moved the budgets by +0.2 for the press chunk; the Undo adds one `undoable` import to the lazy sharing module).
Author
Owner

#1041 status (head d4577e908): 2 of the 4 required checks are done, 2 are not. Stopped on a usage limit; not READY yet.

Done

  1. iPhone 18 Pro simulator, Safari (std), light and dark: long swipe on "Packing list" runs Remove me, the row leaves ("1 note"), the toast "Removed “Packing list” from Shared with me" with Undo shows, I tapped Undo, and "Packing list" came back ("2 notes"). The toast sits above the Search/+ bar (its bottom is about 60 px above the bar at 3x) and far above Safari's own bar and the home indicator, so nothing is covered. One observation, no code change: the message wraps onto two lines at 402 pt; a shorter text ("Removed “Packing list”") would fit on one.
  2. Flake issue filed: #1057, the admin-contrast Copy-chip hover stability, with the load-correlated numbers (dev fails 4/11 at load 21; the PR 2/11 at load 14).

Not done
2. First-paint and bundle gate, PR vs dev: the perf/web.sh --bundle-only --gate run for the PR head and for its base dd6eebd43 was queued behind other jobs on dev-01 and had not produced numbers. I stopped it. No KB figures yet, so no statement about growth.
3. Perf (Notes list and shared list p50/p95, perf VM, ~/bin/perf-turn, ABAB ×3): not started on the VM. The PR changes no Rust; I wrote and smoke-tested a server-side probe (notes.list limit 50 and 500, notes.shared-with-me; 2000 notes, 500 shared) but no release binaries were built or relayed and no perf turn was taken, so there are no perf numbers.

Left alone, and released: the simulator, its forward, the seed server and its data are deleted; no perf locks, holds or turns were taken; the dev-01 job is stopped. dev is now at 4274dafb4 (the PR is based on dd6eebd43); a rebase is still needed before merge.

sim-dark-toast.png

sim-dark-undo.png

sim-light-toast.png

sim-light-undo.png

**#1041 status (head `d4577e908`): 2 of the 4 required checks are done, 2 are not. Stopped on a usage limit; not READY yet.** **Done** 1. **iPhone 18 Pro simulator, Safari (std), light and dark:** long swipe on "Packing list" runs Remove me, the row leaves ("1 note"), the toast "Removed “Packing list” from Shared with me" with Undo shows, I tapped Undo, and "Packing list" came back ("2 notes"). The toast sits above the Search/+ bar (its bottom is about 60 px above the bar at 3x) and far above Safari's own bar and the home indicator, so nothing is covered. One observation, no code change: the message wraps onto two lines at 402 pt; a shorter text ("Removed “Packing list”") would fit on one. 4. **Flake issue filed:** #1057, the `admin-contrast` Copy-chip hover stability, with the load-correlated numbers (dev fails 4/11 at load 21; the PR 2/11 at load 14). **Not done** 2. **First-paint and bundle gate, PR vs dev:** the `perf/web.sh --bundle-only --gate` run for the PR head and for its base `dd6eebd43` was queued behind other jobs on dev-01 and had not produced numbers. I stopped it. No KB figures yet, so no statement about growth. 3. **Perf (Notes list and shared list p50/p95, perf VM, `~/bin/perf-turn`, ABAB ×3):** not started on the VM. The PR changes no Rust; I wrote and smoke-tested a server-side probe (notes.list limit 50 and 500, notes.shared-with-me; 2000 notes, 500 shared) but no release binaries were built or relayed and no perf turn was taken, so there are no perf numbers. Left alone, and released: the simulator, its forward, the seed server and its data are deleted; no perf locks, holds or turns were taken; the dev-01 job is stopped. `dev` is now at `4274dafb4` (the PR is based on `dd6eebd43`); a rebase is still needed before merge. sim-dark-toast.png<br><img src="https://git.kayg.org/attachments/183c240a-8dfe-41d8-b803-720e39d83e17" width="260"> sim-dark-undo.png<br><img src="https://git.kayg.org/attachments/5a601b5e-ca25-4717-9b08-350a1bfa0b39" width="260"> sim-light-toast.png<br><img src="https://git.kayg.org/attachments/b962b18b-89f2-4d79-8399-8a727586e11d" width="260"> sim-light-undo.png<br><img src="https://git.kayg.org/attachments/688ed820-7faa-4b6d-b5d0-b4f652d1b52e" width="260">
Author
Owner

test lane update, rebased and pushed: head 5a7fde10e5c18e7d5489fb462ffec5d7566461b6, base dev 4274dafb46f03ca23fea1fb76929bb13716af64e.

size gate is not green, so this PR cannot be READY. both builds ran on dev-01 through ~/bin/slot nix develop -c bash -c 'PERF_OUT=<scratch>/<pr|dev>-bundle PERF_WORK=<scratch>/<pr|dev>-work bash perf/web.sh --bundle-only --gate' with frozen dependencies.

KB below means KiB (bytes / 1024), brotli quality 11, matching the repo gate:

metric dev KiB PR KiB delta bytes delta KiB
first-paint JS 222.5458984375 222.7490234375 +208 +0.203125
first-paint CSS 17.8212890625 17.8271484375 +6 +0.005859375
all app JS 483.388671875 483.9560546875 +581 +0.5673828125
all app CSS 52.1220703125 52.1279296875 +6 +0.005859375
total named JS (including plugins) 521.22265625 521.9111328125 +705 +0.6884765625

PR gate exit 1: web.bundle.chunk.index: 222.7490234375 KiB > 222.71. dev gate exit 0. notable lazy chunk changes: press 7856 -> 8034 bytes (+178); sharing-sync 753 -> 972 (+219). budgets have not been changed by this lane. the branch already carries the earlier press 7.7 -> 7.9 and total 537.55 -> 537.75 increase; this is reported, not treated as approval for growth under the current brief. any growth needs kayg's OK.

functional reruns and release binary/fixture preparation are in progress. recovered the previous smoke-tested /tmp/list-probe.py; will run Notes limit 50/500 and Shared with me through plain perf-turn, with a dev-vs-dev control and ABAB x3. no merge will be performed.

test lane update, rebased and pushed: head `5a7fde10e5c18e7d5489fb462ffec5d7566461b6`, base dev `4274dafb46f03ca23fea1fb76929bb13716af64e`. size gate is **not green**, so this PR cannot be READY. both builds ran on dev-01 through `~/bin/slot nix develop -c bash -c 'PERF_OUT=<scratch>/<pr|dev>-bundle PERF_WORK=<scratch>/<pr|dev>-work bash perf/web.sh --bundle-only --gate'` with frozen dependencies. KB below means KiB (bytes / 1024), brotli quality 11, matching the repo gate: | metric | dev KiB | PR KiB | delta bytes | delta KiB | |---|---:|---:|---:|---:| | first-paint JS | 222.5458984375 | 222.7490234375 | +208 | +0.203125 | | first-paint CSS | 17.8212890625 | 17.8271484375 | +6 | +0.005859375 | | all app JS | 483.388671875 | 483.9560546875 | +581 | +0.5673828125 | | all app CSS | 52.1220703125 | 52.1279296875 | +6 | +0.005859375 | | total named JS (including plugins) | 521.22265625 | 521.9111328125 | +705 | +0.6884765625 | PR gate exit 1: `web.bundle.chunk.index`: 222.7490234375 KiB > 222.71. dev gate exit 0. notable lazy chunk changes: `press` 7856 -> 8034 bytes (+178); `sharing-sync` 753 -> 972 (+219). budgets have not been changed by this lane. the branch already carries the earlier `press` 7.7 -> 7.9 and total 537.55 -> 537.75 increase; this is reported, not treated as approval for growth under the current brief. any growth needs kayg's OK. functional reruns and release binary/fixture preparation are in progress. recovered the previous smoke-tested `/tmp/list-probe.py`; will run Notes limit 50/500 and Shared with me through plain `perf-turn`, with a dev-vs-dev control and ABAB x3. no merge will be performed.
Author
Owner

perf results for head 5a7fde10e5c18e7d5489fb462ffec5d7566461b6 vs dev 4274dafb46f03ca23fea1fb76929bb13716af64e.

completed the same-session dev-vs-dev control, then ABAB ×3 (12 comparison runs, 6 per label), on perf-01 through plain ~/bin/perf-turn shared-row-1041 timeout 4800 bash <scratch>/run.sh. FIFO waited behind wal1051; no queue jumping. perf-01 and perf-02 locks were held together only during measurement, and both are now released. measurement ended 2026-11-10 21:22:49 IST.

backend source identity was checked with git diff --quiet origin/dev -- crates vendor Cargo.toml Cargo.lock .cargo rust-toolchain.toml. it exits 0. therefore both labels intentionally use the same current-dev release executable, built on dev-01 under ~/bin/slot nix develop -c bash -c 'CARGO_TARGET_DIR=<scratch>/target cargo build --release --locked -j4 -p calternal'. binary SHA256 verified on dev-01, control host, and perf-01: 12cfdfdebabd891f3cc014bc36bcaf3922861d7a623541c3da12c05f12d00194. staged fixture+probe tar SHA256: eef5ed099c5529886e8c999e12dd10e304006108131f7e00a3edc98ba87d9319.

fixture: 2,000 owner notes; 500 shared read-only with Friend; 2,000 Friend notes. each run copies the same stopped fixture, serves as Friend, settles for 3 seconds, validates result counts (50/500/500), warms each action 30 times, then records 300 persistent-loopback HTTP requests per action. percentiles use sorted sample index int(q*n); the summary below is the median of the six per-run percentiles, not a pooled percentile. timing includes HTTP, server read and JSON decoding; it does not measure browser rendering or the whole Undo interaction.

read percentile dev ms PR ms delta ms delta %
notes.list:{"limit": 50} p50 0.4990 0.4945 -0.0045 -0.90%
notes.list:{"limit": 50} p95 0.5825 0.5505 -0.0320 -5.49%
notes.list:{"limit": 500} p50 2.7640 2.7620 -0.0020 -0.07%
notes.list:{"limit": 500} p95 3.1190 3.1750 +0.0560 +1.80%
notes.shared-with-me:{} p50 149.0070 149.4530 +0.4460 +0.30%
notes.shared-with-me:{} p95 160.2490 160.9965 +0.7475 +0.47%
dev-vs-dev control A p50 / p95 ms B p50 / p95 ms
notes.list:{"limit": 50} 0.499 / 0.592 0.489 / 0.540
notes.list:{"limit": 500} 2.756 / 2.933 2.762 / 2.953
notes.shared-with-me:{} 150.260 / 164.727 149.262 / 161.059

all deltas are reported, including positive ones. no latency allowance or budget was changed. this is a comparison of identical backend code, so it does not establish a PR-specific backend slowdown or speedup. under the strict all-green rule, these numbers are not presented as a blanket green no-regression claim. the bundle/first-paint gate is already red (comment #89640).

probe repair: the recovered probe used fixed port 18931 and its setup connected to an older smoke server on dev-01. that failed setup sent sample seed writes to the old smoke fixture, then failed on sharing.grant; none of it was measured or reused. the older process was left untouched. the new throwaway probe selects a free port, checks its own process during startup, checks result counts, and stops its own servers in finally. a small fixture smoke passed before the full fresh fixture was staged. one smoke assertion initially used notes rather than the API's items key; corrected before the full fixture and all perf measurements. no application source changed.

exact raw perf results (14 runs)
{"label": "control-dev-A", "notes.list:{\"limit\": 50}": {"p50": 0.499, "p95": 0.592, "p99": 0.686}, "notes.list:{\"limit\": 500}": {"p50": 2.756, "p95": 2.933, "p99": 3.083}, "notes.shared-with-me:{}": {"p50": 150.26, "p95": 164.727, "p99": 172.109}}
{"label": "control-dev-B", "notes.list:{\"limit\": 50}": {"p50": 0.489, "p95": 0.54, "p99": 0.643}, "notes.list:{\"limit\": 500}": {"p50": 2.762, "p95": 2.953, "p99": 3.359}, "notes.shared-with-me:{}": {"p50": 149.262, "p95": 161.059, "p99": 167.314}}
{"label": "dev-1", "notes.list:{\"limit\": 50}": {"p50": 0.522, "p95": 0.632, "p99": 1.03}, "notes.list:{\"limit\": 500}": {"p50": 2.853, "p95": 3.533, "p99": 4.03}, "notes.shared-with-me:{}": {"p50": 148.88, "p95": 160.046, "p99": 172.556}}
{"label": "pr-1", "notes.list:{\"limit\": 50}": {"p50": 0.487, "p95": 0.541, "p99": 0.599}, "notes.list:{\"limit\": 500}": {"p50": 2.795, "p95": 3.33, "p99": 3.693}, "notes.shared-with-me:{}": {"p50": 150.473, "p95": 163.724, "p99": 172.167}}
{"label": "dev-2", "notes.list:{\"limit\": 50}": {"p50": 0.497, "p95": 0.532, "p99": 0.593}, "notes.list:{\"limit\": 500}": {"p50": 2.767, "p95": 3.108, "p99": 3.618}, "notes.shared-with-me:{}": {"p50": 149.134, "p95": 161.916, "p99": 173.915}}
{"label": "pr-2", "notes.list:{\"limit\": 50}": {"p50": 0.482, "p95": 0.535, "p99": 0.57}, "notes.list:{\"limit\": 500}": {"p50": 2.741, "p95": 2.917, "p99": 3.143}, "notes.shared-with-me:{}": {"p50": 149.504, "p95": 162.847, "p99": 184.351}}
{"label": "dev-3", "notes.list:{\"limit\": 50}": {"p50": 0.486, "p95": 0.572, "p99": 0.697}, "notes.list:{\"limit\": 500}": {"p50": 2.761, "p95": 3.13, "p99": 3.351}, "notes.shared-with-me:{}": {"p50": 148.78, "p95": 158.931, "p99": 166.757}}
{"label": "pr-3", "notes.list:{\"limit\": 50}": {"p50": 0.5, "p95": 0.557, "p99": 0.658}, "notes.list:{\"limit\": 500}": {"p50": 2.761, "p95": 2.967, "p99": 3.508}, "notes.shared-with-me:{}": {"p50": 149.402, "p95": 161.225, "p99": 167.167}}
{"label": "dev-4", "notes.list:{\"limit\": 50}": {"p50": 0.508, "p95": 0.639, "p99": 0.764}, "notes.list:{\"limit\": 500}": {"p50": 2.761, "p95": 3.082, "p99": 3.484}, "notes.shared-with-me:{}": {"p50": 148.724, "p95": 160.257, "p99": 167.239}}
{"label": "pr-4", "notes.list:{\"limit\": 50}": {"p50": 0.492, "p95": 0.558, "p99": 0.636}, "notes.list:{\"limit\": 500}": {"p50": 2.89, "p95": 3.57, "p99": 4.149}, "notes.shared-with-me:{}": {"p50": 148.268, "p95": 159.092, "p99": 166.113}}
{"label": "dev-5", "notes.list:{\"limit\": 50}": {"p50": 0.501, "p95": 0.593, "p99": 0.659}, "notes.list:{\"limit\": 500}": {"p50": 2.761, "p95": 2.964, "p99": 3.092}, "notes.shared-with-me:{}": {"p50": 150.972, "p95": 162.646, "p99": 171.547}}
{"label": "pr-5", "notes.list:{\"limit\": 50}": {"p50": 0.497, "p95": 0.544, "p99": 0.637}, "notes.list:{\"limit\": 500}": {"p50": 2.743, "p95": 3.195, "p99": 3.89}, "notes.shared-with-me:{}": {"p50": 148.867, "p95": 159.484, "p99": 162.834}}
{"label": "dev-6", "notes.list:{\"limit\": 50}": {"p50": 0.486, "p95": 0.536, "p99": 0.577}, "notes.list:{\"limit\": 500}": {"p50": 2.78, "p95": 3.406, "p99": 3.71}, "notes.shared-with-me:{}": {"p50": 149.136, "p95": 160.241, "p99": 166.291}}
{"label": "pr-6", "notes.list:{\"limit\": 50}": {"p50": 0.497, "p95": 0.618, "p99": 0.709}, "notes.list:{\"limit\": 500}": {"p50": 2.763, "p95": 3.155, "p99": 5.124}, "notes.shared-with-me:{}": {"p50": 149.797, "p95": 160.768, "p99": 166.158}}
throwaway probe used for the run
#!/usr/bin/env python3
"""List read-path probe for #1041 (perf VM, no browser): p50/p95/p99 of notes.list and notes.shared-with-me.

usage: list-probe.py fixture <bin> <fixture-dir>            builds the data once (Owner with N notes, 500 shared with Friend, Friend with N notes)
       list-probe.py run <bin> <fixture-dir> <label> <out.json>   copies the fixture, serves as Friend, measures, prints one JSON line

Server side only: the PR changes no Rust, so this shows the reads the Undo's re-read (loadShared) uses are unchanged.
"""
import json, os, re, shutil, subprocess, sys, tempfile, time, http.client, uuid, socket

N, SHARED, REPS, WARM = int(os.environ.get('PROBE_N', 2000)), int(os.environ.get('PROBE_SHARED', 500)), int(os.environ.get('PROBE_REPS', 300)), 30
with socket.socket() as port_picker:
    port_picker.bind(('127.0.0.1', 0))
    PORT = port_picker.getsockname()[1]
LIVE = []

def cli(b, *a):
    return subprocess.run([b, *a], capture_output=True, text=True).stdout

def serve(b, data, user):
    p = subprocess.Popen([b, 'serve', '--create', '--listen', f'127.0.0.1:{PORT}', '--data', data, '--dev-user', user, '--origin', f'http://localhost:{PORT}'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    LIVE.append(p)
    for _ in range(300):
        if p.poll() is not None: raise RuntimeError('own server exited during startup')
        try:
            c = http.client.HTTPConnection('127.0.0.1', PORT, timeout=5); c.request('GET', '/healthz'); response = c.getresponse(); response.read();
            if response.status == 200: return p, c
        except Exception: time.sleep(0.1)
    p.kill(); raise SystemExit('server did not start')

def act(c, name, body, mutate=True):
    h = {'content-type': 'application/json'}
    if mutate: h['idempotency-key'] = str(uuid.uuid4())
    c.request('POST', f'/api/v1/actions/{name}', None if body == {} else json.dumps(body), h)
    r = c.getresponse(); d = r.read()
    if r.status != 200: raise SystemExit(f'{name} -> {r.status} {d[:200]!r}')
    return json.loads(d)['result']

def fixture(b, d):
    os.makedirs(d, exist_ok=True)
    ids = {}
    for who in ('Owner', 'Friend'):
        ids[who] = re.search(r'/users/([0-9a-f-]{36})', cli(b, 'user', 'create', who, '--data', d)).group(1)
    p, c = serve(b, d, ids['Owner'])
    notes = [act(c, 'notes.create', {'title': f'Owner note {i}', 'text': f'# Owner note {i}\n\nwords {i}\n'})['id'] for i in range(N)]
    for nid in notes[:SHARED]:
        act(c, 'sharing.grant', {'note_id': nid, 'person_id': ids['Friend'], 'permission': 'read'})
    p.terminate(); p.wait()
    p, c = serve(b, d, ids['Friend'])
    for i in range(N): act(c, 'notes.create', {'title': f'Friend note {i}', 'text': f'# Friend note {i}\n\nwords {i}\n'})
    p.terminate(); p.wait()
    json.dump(ids, open(os.path.join(d, 'ids.json'), 'w'))
    print('fixture ready', d)

def pct(v, q): v = sorted(v); return v[min(len(v) - 1, int(q * len(v)))]

def run(b, fx, label, out):
    d = tempfile.mkdtemp(prefix='probe-', dir=os.path.dirname(fx.rstrip('/')) or '.')
    try:
        shutil.copytree(fx, d + '/data', symlinks=True, ignore=shutil.ignore_patterns('*.sock'))
        ids = json.load(open(d + '/data/ids.json'))
        p, c = serve(b, d + '/data', ids['Friend'])
        time.sleep(3)  # let the index settle
        res = {'label': label}
        for name, body in (('notes.list', {'limit': 50}), ('notes.list', {'limit': 500}), ('notes.shared-with-me', {})):
            result = act(c, name, body, False)
            key = 'shared' if name == 'notes.shared-with-me' else 'items'
            expected = SHARED if key == 'shared' else min(N, body['limit'])
            assert len(result[key]) == expected, (name, len(result[key]), expected)
            for _ in range(WARM): act(c, name, body, False)
            ts = []
            for _ in range(REPS):
                t = time.perf_counter(); act(c, name, body, False); ts.append((time.perf_counter() - t) * 1000)
            res[f'{name}:{json.dumps(body)}'] = {'p50': round(pct(ts, .5), 3), 'p95': round(pct(ts, .95), 3), 'p99': round(pct(ts, .99), 3)}
        p.terminate(); p.wait()
        print(json.dumps(res)); open(out, 'a').write(json.dumps(res) + '\n')
    finally:
        shutil.rmtree(d, ignore_errors=True)

if __name__ == '__main__':
    m = sys.argv[1]
    try:
        if m == 'fixture': fixture(sys.argv[2], sys.argv[3])
        else: run(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
    finally:
        for process in LIVE:
            if process.poll() is None:
                process.terminate()
                try: process.wait(timeout=20)
                except subprocess.TimeoutExpired: process.kill(); process.wait()
perf results for head `5a7fde10e5c18e7d5489fb462ffec5d7566461b6` vs dev `4274dafb46f03ca23fea1fb76929bb13716af64e`. completed the same-session dev-vs-dev control, then **ABAB ×3** (12 comparison runs, 6 per label), on perf-01 through plain `~/bin/perf-turn shared-row-1041 timeout 4800 bash <scratch>/run.sh`. FIFO waited behind `wal1051`; no queue jumping. perf-01 and perf-02 locks were held together only during measurement, and both are now released. measurement ended 2026-11-10 21:22:49 IST. backend source identity was checked with `git diff --quiet origin/dev -- crates vendor Cargo.toml Cargo.lock .cargo rust-toolchain.toml`. it exits 0. therefore both labels intentionally use the **same current-dev release executable**, built on dev-01 under `~/bin/slot nix develop -c bash -c 'CARGO_TARGET_DIR=<scratch>/target cargo build --release --locked -j4 -p calternal'`. binary SHA256 verified on dev-01, control host, and perf-01: `12cfdfdebabd891f3cc014bc36bcaf3922861d7a623541c3da12c05f12d00194`. staged fixture+probe tar SHA256: `eef5ed099c5529886e8c999e12dd10e304006108131f7e00a3edc98ba87d9319`. fixture: 2,000 owner notes; 500 shared read-only with Friend; 2,000 Friend notes. each run copies the same stopped fixture, serves as Friend, settles for 3 seconds, validates result counts (50/500/500), warms each action 30 times, then records 300 persistent-loopback HTTP requests per action. percentiles use sorted sample index `int(q*n)`; the summary below is the median of the six per-run percentiles, not a pooled percentile. timing includes HTTP, server read and JSON decoding; it does not measure browser rendering or the whole Undo interaction. | read | percentile | dev ms | PR ms | delta ms | delta % | |---|---|---:|---:|---:|---:| | `notes.list:{"limit": 50}` | p50 | 0.4990 | 0.4945 | -0.0045 | -0.90% | | `notes.list:{"limit": 50}` | p95 | 0.5825 | 0.5505 | -0.0320 | -5.49% | | `notes.list:{"limit": 500}` | p50 | 2.7640 | 2.7620 | -0.0020 | -0.07% | | `notes.list:{"limit": 500}` | p95 | 3.1190 | 3.1750 | +0.0560 | +1.80% | | `notes.shared-with-me:{}` | p50 | 149.0070 | 149.4530 | +0.4460 | +0.30% | | `notes.shared-with-me:{}` | p95 | 160.2490 | 160.9965 | +0.7475 | +0.47% | | dev-vs-dev control | A p50 / p95 ms | B p50 / p95 ms | |---|---:|---:| | `notes.list:{"limit": 50}` | 0.499 / 0.592 | 0.489 / 0.540 | | `notes.list:{"limit": 500}` | 2.756 / 2.933 | 2.762 / 2.953 | | `notes.shared-with-me:{}` | 150.260 / 164.727 | 149.262 / 161.059 | all deltas are reported, including positive ones. no latency allowance or budget was changed. this is a comparison of identical backend code, so it does not establish a PR-specific backend slowdown or speedup. under the strict all-green rule, these numbers are not presented as a blanket green no-regression claim. the bundle/first-paint gate is already red (comment #89640). probe repair: the recovered probe used fixed port 18931 and its setup connected to an older smoke server on dev-01. that failed setup sent sample seed writes to the old smoke fixture, then failed on `sharing.grant`; none of it was measured or reused. the older process was left untouched. the new throwaway probe selects a free port, checks its own process during startup, checks result counts, and stops its own servers in `finally`. a small fixture smoke passed before the full fresh fixture was staged. one smoke assertion initially used `notes` rather than the API's `items` key; corrected before the full fixture and all perf measurements. no application source changed. <details><summary>exact raw perf results (14 runs)</summary> ```jsonl {"label": "control-dev-A", "notes.list:{\"limit\": 50}": {"p50": 0.499, "p95": 0.592, "p99": 0.686}, "notes.list:{\"limit\": 500}": {"p50": 2.756, "p95": 2.933, "p99": 3.083}, "notes.shared-with-me:{}": {"p50": 150.26, "p95": 164.727, "p99": 172.109}} {"label": "control-dev-B", "notes.list:{\"limit\": 50}": {"p50": 0.489, "p95": 0.54, "p99": 0.643}, "notes.list:{\"limit\": 500}": {"p50": 2.762, "p95": 2.953, "p99": 3.359}, "notes.shared-with-me:{}": {"p50": 149.262, "p95": 161.059, "p99": 167.314}} {"label": "dev-1", "notes.list:{\"limit\": 50}": {"p50": 0.522, "p95": 0.632, "p99": 1.03}, "notes.list:{\"limit\": 500}": {"p50": 2.853, "p95": 3.533, "p99": 4.03}, "notes.shared-with-me:{}": {"p50": 148.88, "p95": 160.046, "p99": 172.556}} {"label": "pr-1", "notes.list:{\"limit\": 50}": {"p50": 0.487, "p95": 0.541, "p99": 0.599}, "notes.list:{\"limit\": 500}": {"p50": 2.795, "p95": 3.33, "p99": 3.693}, "notes.shared-with-me:{}": {"p50": 150.473, "p95": 163.724, "p99": 172.167}} {"label": "dev-2", "notes.list:{\"limit\": 50}": {"p50": 0.497, "p95": 0.532, "p99": 0.593}, "notes.list:{\"limit\": 500}": {"p50": 2.767, "p95": 3.108, "p99": 3.618}, "notes.shared-with-me:{}": {"p50": 149.134, "p95": 161.916, "p99": 173.915}} {"label": "pr-2", "notes.list:{\"limit\": 50}": {"p50": 0.482, "p95": 0.535, "p99": 0.57}, "notes.list:{\"limit\": 500}": {"p50": 2.741, "p95": 2.917, "p99": 3.143}, "notes.shared-with-me:{}": {"p50": 149.504, "p95": 162.847, "p99": 184.351}} {"label": "dev-3", "notes.list:{\"limit\": 50}": {"p50": 0.486, "p95": 0.572, "p99": 0.697}, "notes.list:{\"limit\": 500}": {"p50": 2.761, "p95": 3.13, "p99": 3.351}, "notes.shared-with-me:{}": {"p50": 148.78, "p95": 158.931, "p99": 166.757}} {"label": "pr-3", "notes.list:{\"limit\": 50}": {"p50": 0.5, "p95": 0.557, "p99": 0.658}, "notes.list:{\"limit\": 500}": {"p50": 2.761, "p95": 2.967, "p99": 3.508}, "notes.shared-with-me:{}": {"p50": 149.402, "p95": 161.225, "p99": 167.167}} {"label": "dev-4", "notes.list:{\"limit\": 50}": {"p50": 0.508, "p95": 0.639, "p99": 0.764}, "notes.list:{\"limit\": 500}": {"p50": 2.761, "p95": 3.082, "p99": 3.484}, "notes.shared-with-me:{}": {"p50": 148.724, "p95": 160.257, "p99": 167.239}} {"label": "pr-4", "notes.list:{\"limit\": 50}": {"p50": 0.492, "p95": 0.558, "p99": 0.636}, "notes.list:{\"limit\": 500}": {"p50": 2.89, "p95": 3.57, "p99": 4.149}, "notes.shared-with-me:{}": {"p50": 148.268, "p95": 159.092, "p99": 166.113}} {"label": "dev-5", "notes.list:{\"limit\": 50}": {"p50": 0.501, "p95": 0.593, "p99": 0.659}, "notes.list:{\"limit\": 500}": {"p50": 2.761, "p95": 2.964, "p99": 3.092}, "notes.shared-with-me:{}": {"p50": 150.972, "p95": 162.646, "p99": 171.547}} {"label": "pr-5", "notes.list:{\"limit\": 50}": {"p50": 0.497, "p95": 0.544, "p99": 0.637}, "notes.list:{\"limit\": 500}": {"p50": 2.743, "p95": 3.195, "p99": 3.89}, "notes.shared-with-me:{}": {"p50": 148.867, "p95": 159.484, "p99": 162.834}} {"label": "dev-6", "notes.list:{\"limit\": 50}": {"p50": 0.486, "p95": 0.536, "p99": 0.577}, "notes.list:{\"limit\": 500}": {"p50": 2.78, "p95": 3.406, "p99": 3.71}, "notes.shared-with-me:{}": {"p50": 149.136, "p95": 160.241, "p99": 166.291}} {"label": "pr-6", "notes.list:{\"limit\": 50}": {"p50": 0.497, "p95": 0.618, "p99": 0.709}, "notes.list:{\"limit\": 500}": {"p50": 2.763, "p95": 3.155, "p99": 5.124}, "notes.shared-with-me:{}": {"p50": 149.797, "p95": 160.768, "p99": 166.158}} ``` </details> <details><summary>throwaway probe used for the run</summary> ```python #!/usr/bin/env python3 """List read-path probe for #1041 (perf VM, no browser): p50/p95/p99 of notes.list and notes.shared-with-me. usage: list-probe.py fixture <bin> <fixture-dir> builds the data once (Owner with N notes, 500 shared with Friend, Friend with N notes) list-probe.py run <bin> <fixture-dir> <label> <out.json> copies the fixture, serves as Friend, measures, prints one JSON line Server side only: the PR changes no Rust, so this shows the reads the Undo's re-read (loadShared) uses are unchanged. """ import json, os, re, shutil, subprocess, sys, tempfile, time, http.client, uuid, socket N, SHARED, REPS, WARM = int(os.environ.get('PROBE_N', 2000)), int(os.environ.get('PROBE_SHARED', 500)), int(os.environ.get('PROBE_REPS', 300)), 30 with socket.socket() as port_picker: port_picker.bind(('127.0.0.1', 0)) PORT = port_picker.getsockname()[1] LIVE = [] def cli(b, *a): return subprocess.run([b, *a], capture_output=True, text=True).stdout def serve(b, data, user): p = subprocess.Popen([b, 'serve', '--create', '--listen', f'127.0.0.1:{PORT}', '--data', data, '--dev-user', user, '--origin', f'http://localhost:{PORT}'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) LIVE.append(p) for _ in range(300): if p.poll() is not None: raise RuntimeError('own server exited during startup') try: c = http.client.HTTPConnection('127.0.0.1', PORT, timeout=5); c.request('GET', '/healthz'); response = c.getresponse(); response.read(); if response.status == 200: return p, c except Exception: time.sleep(0.1) p.kill(); raise SystemExit('server did not start') def act(c, name, body, mutate=True): h = {'content-type': 'application/json'} if mutate: h['idempotency-key'] = str(uuid.uuid4()) c.request('POST', f'/api/v1/actions/{name}', None if body == {} else json.dumps(body), h) r = c.getresponse(); d = r.read() if r.status != 200: raise SystemExit(f'{name} -> {r.status} {d[:200]!r}') return json.loads(d)['result'] def fixture(b, d): os.makedirs(d, exist_ok=True) ids = {} for who in ('Owner', 'Friend'): ids[who] = re.search(r'/users/([0-9a-f-]{36})', cli(b, 'user', 'create', who, '--data', d)).group(1) p, c = serve(b, d, ids['Owner']) notes = [act(c, 'notes.create', {'title': f'Owner note {i}', 'text': f'# Owner note {i}\n\nwords {i}\n'})['id'] for i in range(N)] for nid in notes[:SHARED]: act(c, 'sharing.grant', {'note_id': nid, 'person_id': ids['Friend'], 'permission': 'read'}) p.terminate(); p.wait() p, c = serve(b, d, ids['Friend']) for i in range(N): act(c, 'notes.create', {'title': f'Friend note {i}', 'text': f'# Friend note {i}\n\nwords {i}\n'}) p.terminate(); p.wait() json.dump(ids, open(os.path.join(d, 'ids.json'), 'w')) print('fixture ready', d) def pct(v, q): v = sorted(v); return v[min(len(v) - 1, int(q * len(v)))] def run(b, fx, label, out): d = tempfile.mkdtemp(prefix='probe-', dir=os.path.dirname(fx.rstrip('/')) or '.') try: shutil.copytree(fx, d + '/data', symlinks=True, ignore=shutil.ignore_patterns('*.sock')) ids = json.load(open(d + '/data/ids.json')) p, c = serve(b, d + '/data', ids['Friend']) time.sleep(3) # let the index settle res = {'label': label} for name, body in (('notes.list', {'limit': 50}), ('notes.list', {'limit': 500}), ('notes.shared-with-me', {})): result = act(c, name, body, False) key = 'shared' if name == 'notes.shared-with-me' else 'items' expected = SHARED if key == 'shared' else min(N, body['limit']) assert len(result[key]) == expected, (name, len(result[key]), expected) for _ in range(WARM): act(c, name, body, False) ts = [] for _ in range(REPS): t = time.perf_counter(); act(c, name, body, False); ts.append((time.perf_counter() - t) * 1000) res[f'{name}:{json.dumps(body)}'] = {'p50': round(pct(ts, .5), 3), 'p95': round(pct(ts, .95), 3), 'p99': round(pct(ts, .99), 3)} p.terminate(); p.wait() print(json.dumps(res)); open(out, 'a').write(json.dumps(res) + '\n') finally: shutil.rmtree(d, ignore_errors=True) if __name__ == '__main__': m = sys.argv[1] try: if m == 'fixture': fixture(sys.argv[2], sys.argv[3]) else: run(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]) finally: for process in LIVE: if process.poll() is None: process.terminate() try: process.wait(timeout=20) except subprocess.TimeoutExpired: process.kill(); process.wait() ``` </details>
Author
Owner

rebase follow-up: dev advanced during the first gate run, from 4274dafb4 to 581a40460ff9d035d83ff42899be065e2c42fa31 (#1061 keyboard-motion fixes). rebased again and pushed head f83dc6bb413d6b22f2125b7ac05efbd218bb4fe3.

the old PR commit raising press and total JS budgets conflicted with current dev's approved Contacts budget change. to follow the current test brief (do not raise budgets), dropped the old budget-raising commit. git diff --quiet origin/dev -- perf/budgets.json now exits 0: PR budgets exactly equal current dev. no application behavior was changed by this lane.

all gates on the first rebased head 5a7fde10e passed except sizes: admin-sharing (repeat 2) 60 passed / 8 skipped, swipe-rows + swipe-guard + tasks-swipe 113 passed / 31 skipped, svelte-check 0 errors / 18 warnings, vitest 24 files / 238 tests passed. those functional results are superseded by fresh frontend reruns now in progress for f83dc6bb4 against 581a40460 (all requested specs, both engines, both widths; one repeat, as requested).

completed perf evidence is in comment #89764. git diff --quiet 4274dafb4 581a40460 -- crates vendor Cargo.toml Cargo.lock .cargo rust-toolchain.toml also exits 0, and the PR has no backend delta. the existing identical-backend, same-session ABAB x3 experiment therefore applies to both updated labels; no second perf turn is needed for the frontend-only rebase. both perf locks/units have been released and large fixtures/binaries on perf-01 and the relay host removed. fresh first-paint and bundle numbers will replace #89640's numbers in the final gate table.

rebase follow-up: dev advanced during the first gate run, from `4274dafb4` to `581a40460ff9d035d83ff42899be065e2c42fa31` (#1061 keyboard-motion fixes). rebased again and pushed head **`f83dc6bb413d6b22f2125b7ac05efbd218bb4fe3`**. the old PR commit raising `press` and total JS budgets conflicted with current dev's approved Contacts budget change. to follow the current test brief (do not raise budgets), dropped the old budget-raising commit. `git diff --quiet origin/dev -- perf/budgets.json` now exits 0: PR budgets exactly equal current dev. no application behavior was changed by this lane. all gates on the first rebased head `5a7fde10e` passed except sizes: admin-sharing (repeat 2) **60 passed / 8 skipped**, swipe-rows + swipe-guard + tasks-swipe **113 passed / 31 skipped**, svelte-check **0 errors / 18 warnings**, vitest **24 files / 238 tests passed**. those functional results are superseded by fresh frontend reruns now in progress for `f83dc6bb4` against `581a40460` (all requested specs, both engines, both widths; one repeat, as requested). completed perf evidence is in [comment #89764](https://git.kayg.org/kayg/calternal/pulls/1041#issuecomment-89764). `git diff --quiet 4274dafb4 581a40460 -- crates vendor Cargo.toml Cargo.lock .cargo rust-toolchain.toml` also exits 0, and the PR has no backend delta. the existing identical-backend, same-session ABAB x3 experiment therefore applies to both updated labels; no second perf turn is needed for the frontend-only rebase. both perf locks/units have been released and large fixtures/binaries on perf-01 and the relay host removed. fresh first-paint and bundle numbers will replace #89640's numbers in the final gate table.
Some checks are pending
CI / server (pull_request) Waiting to run
CI / rust (pull_request) Waiting to run
CI / web (pull_request) Waiting to run
Markdown extraction contract / markdown-native-contract (pull_request) Waiting to run
Markdown extraction contract / markdown-editor-contract (pull_request) Waiting to run
This pull request is broken due to missing fork information.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/shared-row-swipes:fix/shared-row-swipes
git switch fix/shared-row-swipes

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch dev
git merge --no-ff fix/shared-row-swipes
git switch fix/shared-row-swipes
git rebase dev
git switch dev
git merge --ff-only fix/shared-row-swipes
git switch fix/shared-row-swipes
git rebase dev
git switch dev
git merge --no-ff fix/shared-row-swipes
git switch dev
git merge --squash fix/shared-row-swipes
git switch dev
git merge --ff-only fix/shared-row-swipes
git switch dev
git merge fix/shared-row-swipes
git push origin dev
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!1041
No description provided.