Skip to content

Trim redundant work from the request hot path - #10

Merged
lpgauth merged 4 commits into
socket-backendfrom
hot-path-opts
Aug 23, 2026
Merged

lpgauth merged 4 commits into
socket-backendfrom
hot-path-opts

Conversation

@lpgauth

@lpgauth lpgauth commented Aug 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

eprof under load (64 keepalive connections) showed the parse path running twice per request: after each response the connection loop re-parsed the leftover buffer even when it was empty, costing a full parse attempt and a binary:split on an empty binary every time. This skips straight back to recv when the buffer is empty.

Also:

  • The header split re-scanned the parsed line list for Content-Length / Transfer-Encoding; the check now happens inside the split loop, one pass instead of two.
  • The per-request GC heap check (process_info(self(), total_heap_size) + threshold from Invoke garbage collection to avoid dirty gc #8) is removed entirely.
  • Responses with binary bodies compute Content-Length with byte_size instead of iolist_size, and empty bodies use a constant <<"0">>.

Per-request effect (eprof, before -> after): whitecap_protocol:request/3 2 -> 1 calls, binary:split 4 -> 3, process_info gone; whitecap's share of profile time drops from ~25% to ~19%, the rest being socket syscalls. Localhost throughput is unchanged (the bench is syscall-bound), so this is a CPU reduction, not a latency win.

Stacked on #9.

Profiling with eprof showed the parse path running twice per request:
after each response the loop re-parsed the leftover buffer even when
it was empty. Skip straight back to recv in that case. The header
split also re-scanned the line list for Content-Length; fold that
check into the split loop instead.

The per-request GC heap check (process_info + threshold) is removed
entirely, and responses with binary bodies size the Content-Length
with byte_size instead of iolist_size.

Together this cuts whitecap's share of profile time from ~25% to ~19%
(the rest is socket syscalls). Localhost throughput is unchanged; the
bench is syscall-bound.
With the JIT and module-wide type propagation these no longer have
a measurable effect: parse-loop and end-to-end benchmarks are within
noise with and without them, while the beam is 12% larger and stack
traces lose inlined frames.
socket:send flattens iolists with list_to_binary on every call, and
whitecap responses are always iolists. Convert them to iovecs and
write them with a single writev-style sendv NIF call instead.
{otp, select_read} lands in OTP 28.0, not 27.3 as the workflow
claimed. Drop OTP 27 from the matrix and run dialyzer on every
matrix entry.
@lpgauth
lpgauth merged commit da1b4ec into socket-backend Aug 23, 2026
2 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.

1 participant