Skip to content

feat(coord): consolidate H2 recv/JSON buffers into one dynamically-sized window - #75

Merged
fudio101 merged 2 commits into
mainfrom
feat/issue-38-h2-dynamic-window
Aug 19, 2026
Merged

feat(coord): consolidate H2 recv/JSON buffers into one dynamically-sized window#75
fudio101 merged 2 commits into
mainfrom
feat/issue-38-h2-dynamic-window

Conversation

@fudio101

@fudio101 fudio101 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Ports piece (a) of djorr5/microlink's 67b230b2 — dynamic H2 RX window sizing — tracked as issue #38 in FORK_PRS.md row 19. Piece (b) (ip4_route_src_hook) is unrelated and untouched, still tracked separately as #39.

  • do_fetch_peers() previously allocated two fixed 512KB PSRAM buffers: an H2 receive buffer and a separate JSON parse buffer. Merged into one buffer — the extracted MapResponse JSON is now compacted in place via memmove instead of copied into a second buffer, halving peak footprint (~1MB → ~512KB at defaults, less under heap pressure).
  • The buffer size is chosen at connect time by new choose_h2_rx_window_size() (ml_coord.c): largest free block across SPIRAM/internal heap, minus a 32KB safety margin, clamped to [64KB, CONFIG_ML_H2_BUFFER_SIZE_KB]. Stored in a new ml->h2_rx_window_size field, threaded through ml_h2_build_preface()'s signature and do_h2_preface()'s window-update math.
  • CONFIG_ML_JSON_BUFFER_SIZE_KB removed — the merged buffer only needs one size knob now. CLAUDE.md/README.md updated to match.

Adapted, not a literal port: kept this fork's existing frame_buf scratch-then-copy pattern in the initial H2 receive loop, rather than reading noise_recv() directly into the shrinking destination window as the source commit does. This fork's noise_recv() returns -1 without draining the ciphertext off the socket when a frame doesn't fit the caller's buffer — reading straight into a near-full window would risk desyncing the coordination stream. The scratch-buffer indirection avoids that regardless of remaining space.

Credit to djorr5/microlink for the original fix.

Test plan

  • Full clean pio run -e lilygo-t-display-s3 against zen-clock (a real downstream consumer, LilyGo T-Display-S3/ESP32-S3) with idf_component.yml's override_path pointed at this branch's working tree — bootloader + firmware built and linked with zero errors. dependencies.lock confirmed the build resolved fugo101/microlink from the local path, not the registry.
  • Flashed to real hardware and confirmed working against a real tailnet (husky-firefighter.ts.net). Boot log through do_fetch_peers():
    H2 rx window: 128KB (largest free block 6144KB, ceiling 128KB)
    H2 preface sent (61 bytes, conn window=128KB)
    H2 END_STREAM detected after 13 Noise frames (22KB, 1189ms)
    Accumulated 22KB of H2 data from Noise frames (1189ms)
    MapResponse JSON: 22592 bytes
    MapResponse top-level fields: ... Peers (array, size=5)
    
    No truncation warnings, no crashes. All 5 real tailnet peers then completed WireGuard handshakes (direct + DERP) normally, confirming the consolidated buffer + in-place memmove compaction didn't corrupt the accumulated H2/JSON data across a real 13-frame, 22KB MapResponse.
  • The adaptive clamp-under-heap-pressure branch specifically (window shrinking below the compiled ceiling) was not exercised — this board's free PSRAM (6144KB largest block) is far above its 128KB ceiling, so the window always resolved to the full ceiling. Would need a genuinely RAM-constrained board or artificial heap pressure to observe the clamp actually engaging.

Closes #38

🤖 Generated with Claude Code

nguyenndt-qualgo and others added 2 commits August 19, 2026 22:51
…zed window

do_fetch_peers() previously allocated two fixed 512KB PSRAM buffers (H2
receive + a separate JSON parse buffer). Merge them into a single buffer,
compacting the extracted MapResponse JSON in place via memmove instead of
copying into a second buffer -- halves peak footprint (~1MB -> ~512KB at
defaults). The buffer's size is now also clamped at connect time to the
largest actually-free heap block (min 64KB, never above the configured
ML_H2_BUFFER_SIZE_KB ceiling), via new choose_h2_rx_window_size().

CONFIG_ML_JSON_BUFFER_SIZE_KB is removed -- the merged buffer only needs
one size knob.

Adapted from djorr5/microlink's `67b230b2` piece (a) (dynamic H2 RX window
sizing) -- piece (b), `ip4_route_src_hook`, is unrelated and untouched
(tracked separately as #39). Kept this fork's existing frame_buf
scratch-then-copy pattern in the initial receive loop rather than reading
noise_recv() straight into the shrinking window, since this fork's
noise_recv() doesn't drain the ciphertext off the socket when a frame
doesn't fit the destination buffer -- reading directly into a
near-full window risked desyncing the coordination stream.

Verified with a from-clean `pio run` against zen-clock (a real downstream
consumer, LilyGo T-Display-S3) using idf_component.yml's override_path
pointed at this working tree: full firmware build + link succeeded. The
adaptive clamp-under-heap-pressure path itself wasn't exercised on
hardware since that board has ample free PSRAM.

Closes #38

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Board became available mid-session -- re-flashed zen-clock (LilyGo
T-Display-S3) against a real tailnet and captured the actual boot log
through do_fetch_peers(): H2 rx window computed correctly (128KB, clamped
to the configured ceiling since free heap was ample), a real 22KB/13-frame
MapResponse reassembled and JSON-compacted in place without truncation or
corruption, and all 5 real tailnet peers completed WireGuard handshakes
normally afterward. Upgrades the earlier compile-only verification note.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@fudio101
fudio101 merged commit 802944c into main Aug 19, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dynamic H2 RX window sizing for constrained RAM (mined from djorr5/microlink)

2 participants