Document runtime exclusions and verify offline transfer savings
This commit is contained in:
@@ -335,3 +335,19 @@ No product behavior changes are involved in these measurement corrections.
|
||||
Keep browser/build/test processes idle during full timing measurements. WebKit's
|
||||
zero long-task field means unavailable, not zero work. Autoplay-blocked/media-ready
|
||||
null results do not establish playback performance or iPhone audio continuity.
|
||||
|
||||
## Runtime manifest hygiene
|
||||
|
||||
`server/asset-manifest.js` excludes unit tests (`*.test.js`, `*.test.mjs`),
|
||||
Markdown, text licences/data (`*.txt`), C build sources, `*.entry.js` build
|
||||
entries, `/admin.html`, and files below any `perf/` directory from the runtime
|
||||
manifest and both build-tag modes. They remain directly servable with the usual
|
||||
hash and cache headers. If a matching file is genuinely needed at runtime, add
|
||||
its exact root-relative path to the optional `include` array in
|
||||
`frontend/assets.json`; group declarations referencing excluded files fail at boot
|
||||
unless explicitly included.
|
||||
|
||||
The offline completeness harness compares cache contents with the runtime
|
||||
manifest dynamically; it must not hard-code the old 151-file inventory. See
|
||||
`plans/manifest-hygiene-report.md` and `perf/results/hygiene-*.json` for the
|
||||
before/after measurements and compatibility checks.
|
||||
|
||||
2893
perf/results/hygiene-after.json
Normal file
2893
perf/results/hygiene-after.json
Normal file
File diff suppressed because it is too large
Load Diff
260
perf/results/hygiene-assets.json
Normal file
260
perf/results/hygiene-assets.json
Normal file
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"before": {
|
||||
"count": 151,
|
||||
"rawBytes": 2044337,
|
||||
"manifestBytes": 15601
|
||||
},
|
||||
"after": {
|
||||
"count": 102,
|
||||
"rawBytes": 1807059,
|
||||
"manifestBytes": 12551
|
||||
},
|
||||
"excluded": [
|
||||
{
|
||||
"path": "/admin.html",
|
||||
"rawBytes": 102005,
|
||||
"brBytes": 24050
|
||||
},
|
||||
{
|
||||
"path": "/app-seams.test.js",
|
||||
"rawBytes": 9786,
|
||||
"brBytes": 2457
|
||||
},
|
||||
{
|
||||
"path": "/asset-sync-core.test.js",
|
||||
"rawBytes": 9430,
|
||||
"brBytes": 2525
|
||||
},
|
||||
{
|
||||
"path": "/asset-worker.test.js",
|
||||
"rawBytes": 8219,
|
||||
"brBytes": 1984
|
||||
},
|
||||
{
|
||||
"path": "/async-guard.test.js",
|
||||
"rawBytes": 2543,
|
||||
"brBytes": 765
|
||||
},
|
||||
{
|
||||
"path": "/audio-continuity.test.js",
|
||||
"rawBytes": 2899,
|
||||
"brBytes": 813
|
||||
},
|
||||
{
|
||||
"path": "/bento-hub.test.js",
|
||||
"rawBytes": 868,
|
||||
"brBytes": 283
|
||||
},
|
||||
{
|
||||
"path": "/car-mode.test.js",
|
||||
"rawBytes": 2556,
|
||||
"brBytes": 973
|
||||
},
|
||||
{
|
||||
"path": "/direct-protocol.test.js",
|
||||
"rawBytes": 1279,
|
||||
"brBytes": 521
|
||||
},
|
||||
{
|
||||
"path": "/direct-stream.test.js",
|
||||
"rawBytes": 891,
|
||||
"brBytes": 390
|
||||
},
|
||||
{
|
||||
"path": "/download-errors.test.js",
|
||||
"rawBytes": 1783,
|
||||
"brBytes": 566
|
||||
},
|
||||
{
|
||||
"path": "/downloads-page.test.js",
|
||||
"rawBytes": 2453,
|
||||
"brBytes": 699
|
||||
},
|
||||
{
|
||||
"path": "/downloads.test.js",
|
||||
"rawBytes": 1608,
|
||||
"brBytes": 639
|
||||
},
|
||||
{
|
||||
"path": "/eq-core.test.js",
|
||||
"rawBytes": 1055,
|
||||
"brBytes": 428
|
||||
},
|
||||
{
|
||||
"path": "/export.test.js",
|
||||
"rawBytes": 521,
|
||||
"brBytes": 259
|
||||
},
|
||||
{
|
||||
"path": "/f7-layout.test.js",
|
||||
"rawBytes": 1400,
|
||||
"brBytes": 380
|
||||
},
|
||||
{
|
||||
"path": "/fonts/PlusJakartaSans-OFL.txt",
|
||||
"rawBytes": 4402,
|
||||
"brBytes": 1428
|
||||
},
|
||||
{
|
||||
"path": "/fullscreen-orientation.test.js",
|
||||
"rawBytes": 1278,
|
||||
"brBytes": 404
|
||||
},
|
||||
{
|
||||
"path": "/glass-controls.test.js",
|
||||
"rawBytes": 1177,
|
||||
"brBytes": 412
|
||||
},
|
||||
{
|
||||
"path": "/glass-panel-layout.test.js",
|
||||
"rawBytes": 782,
|
||||
"brBytes": 259
|
||||
},
|
||||
{
|
||||
"path": "/lazy.test.js",
|
||||
"rawBytes": 7117,
|
||||
"brBytes": 2030
|
||||
},
|
||||
{
|
||||
"path": "/loudness.test.js",
|
||||
"rawBytes": 1626,
|
||||
"brBytes": 706
|
||||
},
|
||||
{
|
||||
"path": "/lower-third.test.js",
|
||||
"rawBytes": 845,
|
||||
"brBytes": 338
|
||||
},
|
||||
{
|
||||
"path": "/lyrics-core.test.js",
|
||||
"rawBytes": 4996,
|
||||
"brBytes": 1430
|
||||
},
|
||||
{
|
||||
"path": "/lyrics-display.test.js",
|
||||
"rawBytes": 494,
|
||||
"brBytes": 244
|
||||
},
|
||||
{
|
||||
"path": "/lyrics-window.test.js",
|
||||
"rawBytes": 617,
|
||||
"brBytes": 264
|
||||
},
|
||||
{
|
||||
"path": "/midi.test.js",
|
||||
"rawBytes": 987,
|
||||
"brBytes": 348
|
||||
},
|
||||
{
|
||||
"path": "/p2p-core.test.js",
|
||||
"rawBytes": 2116,
|
||||
"brBytes": 675
|
||||
},
|
||||
{
|
||||
"path": "/party-dj.test.js",
|
||||
"rawBytes": 550,
|
||||
"brBytes": 234
|
||||
},
|
||||
{
|
||||
"path": "/piano-core.test.js",
|
||||
"rawBytes": 2102,
|
||||
"brBytes": 731
|
||||
},
|
||||
{
|
||||
"path": "/prewarm-next.test.js",
|
||||
"rawBytes": 1268,
|
||||
"brBytes": 434
|
||||
},
|
||||
{
|
||||
"path": "/related-videos.test.js",
|
||||
"rawBytes": 1514,
|
||||
"brBytes": 556
|
||||
},
|
||||
{
|
||||
"path": "/resume-core.test.js",
|
||||
"rawBytes": 1531,
|
||||
"brBytes": 484
|
||||
},
|
||||
{
|
||||
"path": "/saved-page.test.js",
|
||||
"rawBytes": 553,
|
||||
"brBytes": 224
|
||||
},
|
||||
{
|
||||
"path": "/server-backfill.test.js",
|
||||
"rawBytes": 2118,
|
||||
"brBytes": 683
|
||||
},
|
||||
{
|
||||
"path": "/setlist-import.test.js",
|
||||
"rawBytes": 1134,
|
||||
"brBytes": 508
|
||||
},
|
||||
{
|
||||
"path": "/settings-sections.test.js",
|
||||
"rawBytes": 1209,
|
||||
"brBytes": 405
|
||||
},
|
||||
{
|
||||
"path": "/sha256.test.js",
|
||||
"rawBytes": 3251,
|
||||
"brBytes": 1153
|
||||
},
|
||||
{
|
||||
"path": "/shell-consistency.test.js",
|
||||
"rawBytes": 5265,
|
||||
"brBytes": 1507
|
||||
},
|
||||
{
|
||||
"path": "/stats-core.test.js",
|
||||
"rawBytes": 3165,
|
||||
"brBytes": 929
|
||||
},
|
||||
{
|
||||
"path": "/sw-update.test.js",
|
||||
"rawBytes": 17825,
|
||||
"brBytes": 4099
|
||||
},
|
||||
{
|
||||
"path": "/sw.test.js",
|
||||
"rawBytes": 6219,
|
||||
"brBytes": 1849
|
||||
},
|
||||
{
|
||||
"path": "/vendor/framework7-swipe.entry.js",
|
||||
"rawBytes": 1040,
|
||||
"brBytes": 290
|
||||
},
|
||||
{
|
||||
"path": "/video-edit.test.js",
|
||||
"rawBytes": 3396,
|
||||
"brBytes": 855
|
||||
},
|
||||
{
|
||||
"path": "/visible-timer.test.js",
|
||||
"rawBytes": 2827,
|
||||
"brBytes": 687
|
||||
},
|
||||
{
|
||||
"path": "/vocal-reducer.test.js",
|
||||
"rawBytes": 648,
|
||||
"brBytes": 235
|
||||
},
|
||||
{
|
||||
"path": "/wasm/loudness.c",
|
||||
"rawBytes": 2132,
|
||||
"brBytes": 861
|
||||
},
|
||||
{
|
||||
"path": "/wasm/sha256.c",
|
||||
"rawBytes": 2428,
|
||||
"brBytes": 1064
|
||||
},
|
||||
{
|
||||
"path": "/worker-imports.test.js",
|
||||
"rawBytes": 1370,
|
||||
"brBytes": 636
|
||||
}
|
||||
],
|
||||
"excludedBrBytes": 64694
|
||||
}
|
||||
5013
perf/results/hygiene-before-rollback-offline.json
Normal file
5013
perf/results/hygiene-before-rollback-offline.json
Normal file
File diff suppressed because it is too large
Load Diff
3383
perf/results/hygiene-before.json
Normal file
3383
perf/results/hygiene-before.json
Normal file
File diff suppressed because it is too large
Load Diff
41
perf/results/hygiene-migration.json
Normal file
41
perf/results/hygiene-migration.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"commit": "b77938a",
|
||||
"results": [
|
||||
{
|
||||
"browser": "chromium",
|
||||
"oldTag": "1175f1a1d2c1",
|
||||
"newTag": "604f8f8d8a68",
|
||||
"banners": 1,
|
||||
"updateReloads": 1,
|
||||
"cachedFiles": 124,
|
||||
"durationMs": 26801,
|
||||
"wireBytes": 638444,
|
||||
"offline": true,
|
||||
"legacyURLs": 21,
|
||||
"profile": "lossy",
|
||||
"userInitiatedOnly": true,
|
||||
"playbackGuard": true,
|
||||
"evictionRepair": true,
|
||||
"incrementalRefresh": true,
|
||||
"offlineTransportErrors": 0
|
||||
},
|
||||
{
|
||||
"browser": "webkit",
|
||||
"oldTag": "1175f1a1d2c1",
|
||||
"newTag": "604f8f8d8a68",
|
||||
"banners": 1,
|
||||
"updateReloads": 1,
|
||||
"cachedFiles": 124,
|
||||
"durationMs": 24322,
|
||||
"wireBytes": 743954,
|
||||
"offline": true,
|
||||
"legacyURLs": 21,
|
||||
"profile": "lossy",
|
||||
"userInitiatedOnly": true,
|
||||
"playbackGuard": true,
|
||||
"evictionRepair": true,
|
||||
"incrementalRefresh": true,
|
||||
"offlineTransportErrors": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
5836
perf/results/hygiene-offline.json
Normal file
5836
perf/results/hygiene-offline.json
Normal file
File diff suppressed because it is too large
Load Diff
4817
perf/results/hygiene-rollback-offline.json
Normal file
4817
perf/results/hygiene-rollback-offline.json
Normal file
File diff suppressed because it is too large
Load Diff
150
plans/manifest-hygiene-report.md
Normal file
150
plans/manifest-hygiene-report.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Runtime manifest hygiene
|
||||
|
||||
Based on main/offline-fix tip `0b8e398`; offline-fix was already merged when this
|
||||
worktree was created. Implementation: `dd09f66`. No frontend runtime, loading,
|
||||
service-worker, playback or layout code changed.
|
||||
|
||||
## Policy and delivery
|
||||
|
||||
`server/asset-manifest.js` now selects runtime inputs separately from delivery
|
||||
metadata. It excludes `*.test.js`, `*.test.mjs`, `*.md`, `*.txt`, `*.c`,
|
||||
`*.entry.js`, `/admin.html`, and all files beneath a `perf/` directory. The
|
||||
optional `assets.json` `include` array accepts exact root-relative existing file
|
||||
paths to override this policy. An explicitly declared group cannot silently lose
|
||||
an excluded dependency: boot rejects it unless the path is included.
|
||||
|
||||
The 49 excluded files here are 44 unit tests, admin HTML, the Plus Jakarta Sans
|
||||
font licence, two WASM C build sources, and the Framework7 source build entry.
|
||||
WASM binaries, fonts, icons, workers and the Framework7 runtime bundle remain.
|
||||
|
||||
Excluded files remain directly servable, with the existing content hash, ETag,
|
||||
compression and cache rules: plain requests revalidate (font-folder requests
|
||||
keep their 30-day cache rule), matching `?v` is immutable, and stale `?v` is
|
||||
no-store in per-file mode. Delivery hashes are computed separately; they never
|
||||
enter `manifest.files` or build identity. `ASSET_HASHING=0` retains single-tag
|
||||
stamping and its legacy header matrix, but its tag also uses only runtime
|
||||
inputs. `ASSET_SYNC=0` still selects the legacy worker.
|
||||
|
||||
Fresh installs cache only runtime assets. On an incremental upgrade, excluded
|
||||
entries from the previous manifest may survive for one build under existing
|
||||
N-1 retention, then are pruned as N-2. No new excluded entries are downloaded.
|
||||
This preserves retention for already-open pages instead of changing worker
|
||||
pruning or contracts/pinning.
|
||||
|
||||
## Inventory
|
||||
|
||||
Measured from the untouched before snapshot and the fixed frontend using the
|
||||
respective real server manifest modules. Counts include stamped index and SW.
|
||||
|
||||
| Metric | Before | After | Reduction |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Manifest files | 151 | 102 | 49 (32.5%) |
|
||||
| Summed manifest file sizes | 2,044,337 B | 1,807,059 B | 237,278 B |
|
||||
| JSON manifest body | 15,601 B | 12,551 B | 3,050 B |
|
||||
|
||||
Excluded source bodies total 237,278 B, or 64,694 B using per-file Brotli
|
||||
compression. Actual install/update measurements below include protocol and
|
||||
manifest/index effects; compression estimates are not substituted for those runs.
|
||||
Full inventory: `perf/results/hygiene-assets.json`.
|
||||
|
||||
## Verification
|
||||
|
||||
Commands:
|
||||
|
||||
```sh
|
||||
node --test frontend/*.test.js
|
||||
cd server && bun install && bun run test
|
||||
node perf/offline-complete.mjs --browser all --conditions --out perf/results/hygiene-offline.json
|
||||
node perf/migration.mjs --browser all --profile lossy --out perf/results/hygiene-migration.json
|
||||
node perf/offline-complete.mjs --browser all --legacy --out perf/results/hygiene-rollback-offline.json
|
||||
```
|
||||
|
||||
The server test command used a worktree-local Python venv on PATH with yt-dlp
|
||||
installed so both environment-sensitive worker tests ran successfully.
|
||||
Frontend: 209 passed, zero failures. Server: 186 passed, zero failures.
|
||||
Manifest/static/shell tests cover excluded-file identity invariance in both hash
|
||||
modes, app.js identity changes, explicit inclusion, group validation, direct
|
||||
bodies/hash/header behavior, SPA delivery and rollback flags.
|
||||
|
||||
Offline completeness: Chromium and WebKit each report manifest count 102,
|
||||
missing `[]`, failures `[]`, across all four layouts and lazy features. Page
|
||||
closure/interrupted completion, worker restart/re-registration, Save-Data, two
|
||||
tabs and online-event resumption pass. Linux WebKit uses same-context worker
|
||||
re-registration because persistent CacheStorage restart is broken there; its
|
||||
offline proxy blocks transport because native setOffline breaks cached navigation.
|
||||
Native OPFS-dependent video-edit behavior cannot be exercised in Linux WebKit.
|
||||
|
||||
Lossy legacy migration passes on both engines: one banner, one explicit-user
|
||||
reload, offline launch, playback guard, legacy URLs, eviction repair and
|
||||
incremental refresh. WebKit records four tolerated offline API transport errors,
|
||||
with the offline assertions passing. Migration caches include retained N-1
|
||||
entries, so their raw key count is deliberately greater than manifest count.
|
||||
|
||||
## Rollback limitation found during extra verification
|
||||
|
||||
An additional `--legacy` offline-completeness run disables both hashing and sync.
|
||||
It fails on both the untouched base and fixed build: the legacy SHELL omits
|
||||
`piano-engine.mjs`, so its direct offline request returns 503 across layouts.
|
||||
The legacy shell also intentionally does not cache its own `sw.js`; the harness
|
||||
therefore marks that inventory entry missing too, without a corresponding offline
|
||||
UI failure. Before: 151 manifest entries / 51 missing; after: 102 / 2 missing.
|
||||
The other 49 before-only misses are precisely the excluded non-runtime files.
|
||||
Normal sync-mode completeness is 102/102 with no failures. Header/injection
|
||||
rollback tests pass; this change neither fixes nor worsens the existing legacy
|
||||
piano coverage gap. Results: `hygiene-before-rollback-offline.json` and
|
||||
`hygiene-rollback-offline.json`.
|
||||
|
||||
## Reduced performance measurements
|
||||
|
||||
Three runs per scenario/engine, LTE. Before used an isolated untouched snapshot
|
||||
of `0b8e398` inside this worktree; after used the fixed server. No external
|
||||
worktree was modified. Cold wire totals include page requests and install/completion
|
||||
fetches; these are not claimed to be worker-only bytes. Chromium uses CDP LTE
|
||||
throttling; WebKit uses the LTE proxy.
|
||||
|
||||
```sh
|
||||
node perf/.tmp/before/perf/baseline.mjs --runs 3 --browser all --profile lte --scenario all --out perf/results/hygiene-before.json
|
||||
node perf/baseline.mjs --runs 3 --browser all --profile lte --scenario all --out perf/results/hygiene-after.json
|
||||
```
|
||||
|
||||
All values below are actual medians; bytes are wire bytes.
|
||||
|
||||
| Engine | Scenario | Before B | After B | Before ms | After ms |
|
||||
| --- | --- | ---: | ---: | ---: | ---: |
|
||||
| chromium | Cold visit + install | 728,266 | 660,390 | 7101 | 7090 |
|
||||
| chromium | Warm reload | 8,132 | 8,159 | 647 | 445 |
|
||||
| chromium | Offline reload | 0 | 0 | 285 | 209 |
|
||||
| chromium | Core app.js edit | 126,358 | 125,729 | 1008 | 753 |
|
||||
| chromium | theme-glass.css edit | 44,027 | 43,434 | 1600 | 1372 |
|
||||
| chromium | Presenter edit + idle fill | 42,065 | 41,436 | 1392 | 1376 |
|
||||
| webkit | Cold visit + install | 1,189,813 | 1,111,241 | 2321 | 2310 |
|
||||
| webkit | Warm reload | 0 | 0 | 92 | 89 |
|
||||
| webkit | Offline reload | 0 | 0 | 100 | 88 |
|
||||
| webkit | Core app.js edit | 148,709 | 147,955 | 2013 | 2013 |
|
||||
| webkit | theme-glass.css edit | 57,732 | 56,976 | 1479 | 1466 |
|
||||
| webkit | Presenter edit + idle fill | 55,382 | 54,625 | 1494 | 1557 |
|
||||
|
||||
Cold requests fall 165 → 116 (Chromium) and 209 → 160 (WebKit), exactly 49
|
||||
fewer each. Cold wire savings are 67,876 B (9.3%) and 78,572 B (6.6%).
|
||||
Ordinary runtime updates already shared unchanged files before this fix, so
|
||||
their savings are mainly smaller manifest metadata: approximately 0.6–0.8 KB.
|
||||
Test-only edits now leave the tag unchanged and trigger no build update.
|
||||
Milliseconds for cold/warm/offline are boot-done; update milliseconds are completion
|
||||
duration. These three-run timing differences are descriptive, not a performance
|
||||
claim. Warm Chromium API traffic varies independently of the asset set.
|
||||
|
||||
Real output excerpts (after):
|
||||
|
||||
```text
|
||||
run 3/3... done (660390 bytes, 116 reqs, boot=7090ms)
|
||||
run 3/3... done (1111241 bytes, 160 reqs, boot=2310ms)
|
||||
run 3/3... done (147955 bytes, 5 reqs, dur=2014ms)
|
||||
run 3/3... done (56976 bytes, 9 reqs, dur=1483ms)
|
||||
```
|
||||
|
||||
## Real iPhone checks
|
||||
|
||||
Confirm installed-PWA offline launch and feature/layout first use after update;
|
||||
completion after suspension, storage pressure/OPFS sharing, and old excluded
|
||||
cache entries disappearing after the subsequent build. Linux automation cannot
|
||||
prove iOS worker lifetime, quota eviction or native OPFS behavior.
|
||||
@@ -55,7 +55,6 @@ export function createAssetManifest(publicDir = './public', { hashing = true, bu
|
||||
else {
|
||||
const content = readFileSync(path);
|
||||
bytes.set(url, content);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +90,7 @@ export function createAssetManifest(publicDir = './public', { hashing = true, bu
|
||||
const swSource = bytes.get('/sw.js')?.toString() ?? null;
|
||||
// Derived build metadata cannot be an input to its own hash. Canonicalize
|
||||
// the index meta and SW injected tag, then publish hashes of the final bytes.
|
||||
// All original bytes (including index/SW source) remain inputs via source hashes.
|
||||
// All runtime source bytes (including index/SW source) remain inputs via source hashes.
|
||||
let canonicalIndex = source === null ? null : stampIndex(embedAssets(source, files, groups, '__BUILD_TAG__', hashing), files, { hashing });
|
||||
const canonical = { assetSync, files: { ...files }, groups, contracts };
|
||||
if (canonicalIndex !== null && hashing) canonical.files['/index.html'] = { ...files['/index.html'], h: assetHash(canonicalIndex), s: Buffer.byteLength(canonicalIndex), source: files['/index.html'].h };
|
||||
|
||||
@@ -94,7 +94,7 @@ test('non-runtime files stay deliverable but never affect manifest or build iden
|
||||
for (const hashing of [true, false]) {
|
||||
const first = createAssetManifest(dir, { hashing });
|
||||
mkdirSync(join(dir, 'perf'), { recursive: true });
|
||||
for (const name of ['app.test.js', 'app.test.mjs', 'README.md', 'license.txt', 'admin.html', 'perf/fixture.js']) {
|
||||
for (const name of ['app.test.js', 'app.test.mjs', 'README.md', 'license.txt', 'admin.html', 'perf/fixture.js', 'sha256.c', 'framework.entry.js']) {
|
||||
writeFileSync(join(dir, name), 'non-runtime');
|
||||
const next = createAssetManifest(dir, { hashing });
|
||||
expect(next.manifest.buildTag).toBe(first.manifest.buildTag);
|
||||
|
||||
Reference in New Issue
Block a user