# PRIV5 save-performance follow-up PR https://github.com/chhoumann/PodNotes/pull/479. Branch `fix/redaction-followups`. Head `3d1a3cf85b987c22becadc7b35ecc8c7b10660a0`. Current master `8449c620c0f5c8f844a41883ad324096ef25fabb`. Rejected head `1ed75ad1e5362bc8d2cf25adde26743e8a7fa6c2`. The recurring save regression is removed. The final head meets the no-slower-than-master target within the observed variation on the verifier's unchanged large library. The head's median is lower, but shared-host noise limits a claim about the size of that improvement. Primary final-head artifacts are under `post-478/` in the evidence archive. ## Final production Obsidian comparison Linux x64, Node 22.23.2, Obsidian app and installer 1.14.4. Each version used its production bundle and the isolated runner. Five fresh-vault phases per version retained seven saves after one warmup, giving 35 saves per version. Each phase also retained five Settings exports. Values below are medians and full ranges in milliseconds. The timer measurement is the maximum interval of a 16 ms main-thread timer during each save. | Version | Save | Maximum timer gap | Settings export | | --- | ---: | ---: | ---: | | master `8449c620` | 230.0 (190.4 to 346.3) | 119.1 (91.9 to 178.9) | 29.2 (25.2 to 41.3) | | rejected `1ed75ad` | 505.8 (419.8 to 1340.5) | 363.9 (304.9 to 914.4) | 73.9 (55.1 to 454.2) | | fixed `3d1a3cf8` | 207.5 (181.2 to 247.5) | 113.0 (91.2 to 141.0) | 25.1 (20.6 to 42.1) | Every timed save changed `skipBackwardLength`, awaited the real `saveSettingsStrict`, then read back the expected value. Each wrote 5,585,824 bytes. Each export completed a real adapter write, contained 51 feeds and wrote 2,846,039 bytes. The final benchmark documents are byte-identical across all 15 phases. Public data.json, notes and exported settings also match. Export envelope timestamps are excluded from parity. The actual `npm run count:writes` recorder measured a separate normal session on every version and phase. Three UI edits to Skip back (s), followed by a no-op flush and save, produced exactly three attempted and completed writes. There were zero failed or inflight writes. Every recorder result has `complete: true` and `pluginUnchanged: true`. This is a large-library workload, not a typical-user measurement. It contains 200 distinct saved episodes, 721 episode snapshots, 2,000 playback records and 50 public feeds, plus one private reference. URLs use the local fixture server, so the private host also appears in public URLs. The separate source benchmark uses ten private hosts. These distributions differ, so source timings are not an exact decomposition of the live save. The 12-core host is shared. All samples remain, including the slow first rejected-head phase. The order was master, rejected, fixed, fixed, rejected, master, master, rejected, fixed, fixed, rejected, master, master, rejected, fixed. The means of the five phase medians differ by -23.66 ms for fixed minus master. An exact two-sided permutation check gives p=0.095. This noise check does not prove equivalence or a precise speedup. No other unit's process was stopped. Raw measurements, loads and bundle hashes are in `post-478/comparison-final.json`, `post-478/live-summary.json` and `post-478/live/*/live-performance.json`. `post-478/phase-statistics.py` reproduces the phase comparison. ## Repeated work and cache lifetime On the original base, separate instrumentation found six traversals per settled save. This fix removed the redundant read-document traversal when repair changed no JSON value, leaving five. A repair that changes a value still gets a complete redaction pass. Both local snapshots around the disk-read await and the final write guard remain. Master then advanced with #480. Its unchanged-disk shortcut independently removes the read-side work when the saved document matches the writer's baseline. Instrumentation after that merge finds three traversals on the normal benchmark save. The original 3,538-character description was scanned 2,460 times per save. It is now visited 1,230 times but scanned only three times, once per traversal. Expensive scans fall from 219,740 to 1,722. Of that reduction, six to five traversals belongs to this PR; five to three on an unchanged disk belongs to #480. The scan count includes both effects. The instrumentation is excluded from reported timing samples. The full-string memo is a local `Map` created after private-context discovery. It stores changed and unchanged results and disappears after the traversal. Another call cannot reuse results from an earlier address map, placeholder name or discovered record. A cross-call cache was skipped because the measured target is met and persistent invalidation, eviction and retained private-bearing input would add state. The conservative candidate filter precedes decoding and matching. Ampersands admit entity spellings, percent signs admit encoding, and non-ASCII code units admit Unicode normalization. Backslashes, brackets and at signs admit unusual authorities. A literal scheme-and-digit test admits numeric authorities through HTTP, HTTPS, feed and podcast prefixes. Other covered candidates retain a case-insensitive known hostname. `filter-proof.md` and the committed tests cover these branches. False positives cost work; no covered form is intentionally excluded. The source profile was collected separately. Most sampled work in the encode wrapper is JSON serialization and garbage collection after the scan was reduced. Normal encode-plus-JSON medians were 20.5 ms on the original master and 20.3 ms on the fixed source, with overlapping ranges. The codec and redactor source used by that profile is unchanged by the later merges. This is not a profile of the Obsidian scheduler. See the root `benchmark-results.json`, `benchmark-head.cpuprofile` and `profile-hotspots.json`. ## Correctness and wrong alternatives All 70 public corpus entries and nine transcript fixtures preserve their bytes on final head and current master. The malformed mixed-case `&aMp;` under-redaction regression is fixed with a narrow amp lookup. Other named entities retain their exact-case HTML semantics. The unreachable uppercase AMP table entry is removed. All 35 original wrong alternatives fail named assertions. Fifteen filter, memo-lifetime and restoration variants also fail. Two more variants, always rescanning the read document and treating an equal eager copy as a changed repair, fail the five-traversal save test. There are 52 named mutation failures in total. The export-only M24 mutant still fails persistence while its export assertion passes. No loader failure or missing pattern is counted as a kill. The complete suite was repeated after #480 at `1018f302`. `post-480/mutations/results.json` contains 51 failures and `post-480/export-only-mutation.log` contains M24. Every production file those variants mutate is byte-identical on final head, as recorded in `post-478/unchanged-mutation-source.json`. The Transcript panel merge changes none of them. The final unit suite and runtime gate test the combined artifact. The numeric-backslash case prevents the known-host branch from hiding a missing backslash fallback. E1 and E2 are optional controls that remove the filter or memo. They are not counted as killed. Instrumentation and the production comparison verify their performance effect. The restored-private-entry test catches omitting a required read pass. The equal eager-copy test verifies five traversals, one persisted write and no sentinel on that repair path. ## Acceptance and size All eight final local gates pass under Node 22.23.2. Commands were `npm run lint`, `npm run format:check`, `npm run typecheck`, `npm run build`, `npm run test`, `npm run test:tooling`, `npm run docs:build` and `npm run size`. There are 2,686 passing unit tests in 124 files and 13 passing tooling tests. Complete logs and exit codes are under `post-478/gate-*.log` and `post-478/gates.json`. `CI=true npm run test:e2e` passed once on a fresh vault with no filter. All 199 tests in 27 files passed on Obsidian 1.14.4, Linux x64, with no unexpected renderer errors. The data.json, Settings export and note sentinel counts are 0/0/0. The unsubscribed note contains neither the private sentinel nor an empty Resume label. The isolated instance is stopped and cleanup reports no remaining PIDs. The complete runtime log is `post-478/full-e2e.log`. The runner deletes its temporary Vitest JSON, so `post-478/full-e2e-log-summary.json` is explicitly derived from that log. `post-478/runtime-result.md` records commands, version, sentinel counts and cleanup. The test count is 199 after the master merges. The final bundle is 323,538 raw bytes and 102,654 gzip bytes at level 9 under Node 22.23.2. This is +1,898 raw and +817 gzip versus current master. The 103,000-byte gate arrived with #478 and leaves 346 gzip bytes. SHA-256 is `07a459c518e76c3dc9055372a9beb199c1301ab96e9ba917d6d28b3cd2414d29`, matching all five final-head timed builds and the acceptance build. The performance patch alone added 289 raw and 101 gzip bytes on the original base. The final bundle is 9,678 raw and 2,890 gzip bytes above rejected head because it also contains the two intervening master merges. Do not attribute that whole increase to redaction. The final marginal ablations are in `post-478/size-attribution.log`. Their gzip costs are 303 for entity decoding and source offsets, 12 for feed/podcast wrappers, 4 for HTTP, 75 for scheme-less addresses, 60 for encoded query values, 34 for sentence periods, 16 for the Resume line, 37 for safe placeholder names, 50 for the candidate filter and 19 for the traversal memo. These costs are not additive. The shared six-entity table saves 34 gzip bytes. No requested alias class was dropped. ## Evidence history and reproduction The original comparison against `8603ccfc` remains at the archive root. It measured save medians of 235.0 ms on master, 437.5 ms on rejected head and 206.9 ms on the fix before the master merges, with 35 retained saves per version. Its full report is `performance-report-pre-merges.md`. `post-480/` contains the repeated comparison and checks after the persistence merge. `post-478/` is authoritative for the final combined bundle. An initial post-480 setup attempt lacked the copied public corpus file. It is retained under `post-480/setup-missing-corpus/` and is excluded from published comparisons because the suite failed. The benchmark filter does not run private-note journeys. Older private-note artifacts copied into benchmark phase folders are excluded from the archive. The copy step was removed from the driver. Use `post-478/final-private-notes/` for final privacy evidence. To repeat the final comparison, copy `post-478/runtime-probes.test.ts` to `tests/e2e/zz-v479.test.ts` and its `fixed-port-fixtures.ts` to `tests/e2e/journey/zz-v479-fixtures.ts`. With no source writer or isolated instance active, run `post-478/compare.py` with `PRIV5_BENCH_PHASES` naming the desired phases and `PRIV5_BENCH_FILTER='V479|PRIV5 normal'`. The scripts use this bound worktree's absolute path. The driver swaps changed files to the pinned versions, rebuilds each one, runs the probes and restores bytes in `finally`. Remove both scratch tests before acceptance checks. Never run the driver alongside mutation tests. `post-478/summarize-live.py` checks saved values, byte counts, write counts and parity before printing the table. `post-480/instrument.py` and `summarize-passes.py` reproduce the separate pass count. `post-480/implementation-mutate.mjs` uses `all-correctness-mutants.json`; M24 has its own script. The size-ablation script builds scratch exports without editing the checkout. No data schema, public API or URI changes belong to this PR. Saved templates are not migrated. Published docs remain unchanged. Issue #452, unsupported double encoding and the verifier's Unicode-prefixed scheme-less-host caveat remain follow-ups. The coordinator's delta verification is pending. ## Decision and dispatch record Model the Domain kept the memo inside a traversal's fixed address map. Boundary Discipline preserved disk protection, both local snapshots and the final write guard. Sequence Work into Verifiable Units kept measurement, mutation checks and runtime proof separate. Explain the Number required completed writes, output parity, separate instrumentation, repeated phases and retained noisy samples. The decision log is `decisions.tsv`. The how step ran on `claudeAgent/claude-opus-5-5`. Design exploration ran on `codex_pool/gpt-6-astra`. The `claudeAgent/claude-fable-5-1` exploration stopped without an artifact; the parent supplied the scoped alternative. An independent `claudeAgent/claude-opus-5-5` judge selected it. Implementation ran on `claudeAgent/claude-opus-5-5`. The parent reviewed the diff, added two coverage tests and ran measurements and mutation checks in-thread. Both final runtime validations ran on `claudeAgent/claude-opus-5-5`, before and after the master merges.