feat(lang): load only the resident prefix; split the pack ceiling in two - #83
Merged
Conversation
The 32,768 B LANG_FILE_MAX charged web translations against RAM they never used: @webdict is ~60% of a pack by bytes and is served from flash by GET /api/lang, yet the loader malloc'd the whole file and then excised the blob into a second buffer — peaking at file + resident (~45 KB) to keep ~13 KB. With es-ES at 32,045 B (723 B left), every new web string was being priced against that single ceiling. loadLangFile() now locates the @webdict marker by streaming the file through a 256 B stack chunk, mallocs only the resident prefix, and records the blob's byte range for the web handler exactly as before — the length formula (fsize - bodyStart) is byte-for-byte what the excision used to serve. The blob must be the file's suffix; a pack with sections after it is rejected, and tools/check_lang_packs.py enforces the same order at build time. The excise-and-rebase pass is deleted: the hole never loads at all. Two ceilings replace the one: LANG_RESIDENT_MAX = 16384 the malloc that lives for the whole uptime LANG_FILE_MAX = 49152 flash-side envelope for the whole file es-ES moves from 97% of one ceiling to resident 13,251 B (80%) + file 32,045 B (65%): ~17 KB now available for web translations, ~3 KB for resident ones. Load peak drops from a ~45 KB transient to 13.3 KB. Flash cost: -8 B. Validated on the rig over OTA (image b66379ab): /api/lang byte-identical to the pre-flash baseline with the unchanged pack (18,785 B); a 36,545 B inflated es-ES — which the old ceiling rejects whole, reverting the UI to English — loads with es-ES active and serves its 23,285 B body byte-for-byte; original pack restored and re-verified identical.
check_lang_packs.py reads LANG_FILE_MAX and LANG_RESIDENT_MAX from the parser source so the gate cannot drift from the device rule, measures the resident prefix exactly as loadLangFile() mallocs it, fails a pack whose sections continue past @webdict, and keeps the 95% warning band on both numbers. tools/test_lang_gate.py provokes each new failure mode on synthetic packs mutated from the real es-ES — a gate that never fired is indistinguishable from one that cannot fire — plus an A-vs-A control on the shipped packs: 8/8.
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves the es-ES ceiling squeeze that was blocking any new UI string (#60's hidden cost): the pack sat at 32,045 B of a 32,768 B ceiling — 723 B left — while ~60% of those bytes (@webdict, 18.8 KB) never touch the RAM the ceiling exists to protect.
What changed
loadLangFile()reads only the resident prefix. The @webdict marker is located by streaming the file through a 256 B stack chunk; the malloc covers just the sections the firmware actually keeps (@DICT/@LOGCODES/@TRL/@HELP/@license). The blob's byte range is recorded forGET /api/langexactly as before — the new length formula is byte-for-byte what the old excision served. The excise-and-rebase pass (two live buffers, ~45 KB transient peak) is deleted outright; peak load heap is now 13.3 KB instead of ~45 KB.Two ceilings replace the one, each guarding what it actually bounds:
Contract: @webdict must be the file's suffix (both shipped packs already are). The device rejects a pack with sections after it, and
check_lang_packs.pyenforces the same rule at build time — plus both ceilings, read from the parser source so the gate can't drift.tools/test_lang_gate.pyprovokes every new failure mode on synthetic packs (8/8) with an A-vs-A control on the shipped packs.Deploy order note: a pack over 32,768 B needs this firmware first — the old loader rejects it whole and the UI silently reverts to English. The shipped packs are unchanged and work on both firmwares.
Validation on the rig (over OTA, image
b66379ab)/api/lang= 18,785 B, langCode es-ES./api/langbyte-identical to baseline.Gates:
pio runrelease + test both SUCCESS (flash delta −8 B),check_lang_packs.pyOK on both packs,test_lang_gate.py8/8. The one release warning (-Wcommentin WebManager_Files.cpp) predates this branch and its fix ships in #82.Unblocks: error-states and
aria-labeli18n from #60's pick-list, and any future translation growth up to ~33 KB of @webdict per pack.