Skip to content

Use StringBuf in Response.chunked to fix O(n^2) loop#38

Merged
hellerve merged 3 commits into
mainfrom
claude/chunked-strbuf
Jun 26, 2026
Merged

Use StringBuf in Response.chunked to fix O(n^2) loop#38
hellerve merged 3 commits into
mainfrom
claude/chunked-strbuf

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

Summary

Response.chunked built the encoded body by calling String.append in a loop — each call copies the entire accumulated buffer, making encoding quadratic in the number of chunks. This replaces the loop with StringBuf (already available transitively via the http dependency), which gives amortized O(1) appends.

The generated C and type checker confirm correctness. The full test suite has a pre-existing build failure in the datetime dependency's TM struct (unrelated to this change).


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

Each iteration called String.append which copies the entire accumulated
buffer, making chunked encoding quadratic in the number of chunks.
StringBuf (already available via the http dependency) gives amortized
O(1) appends.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

Build: pre-existing failure on arm-linux (clang const-qualifier warnings in datetime dependency's TM struct — unrelated to this PR). Type checker (--check) passes. CI passes on macOS.
Tests: N/A — no test files exist in this repo.

Findings

  1. Memory leak: missing StringBuf.delete call (web.carp:~366). StringBuf is a register-type that does not implement drop, so Carp will not auto-free it when it goes out of scope. Every other StringBuf usage in the dependency chain (e.g. http/http.carp:227, http/http.carp:384) follows the pattern:

    (let-do [body (StringBuf.to-string &sb)] (StringBuf.delete sb) body)
    

    The PR calls (StringBuf.to-string &sb) inline without ever calling StringBuf.delete. to-string copies the data into a new heap-allocated String but does not free the StringBuf's internal data buffer. This leaks on every chunked response. Fix: bind the to-string result in a let-do, call (StringBuf.delete sb), then pass the body string to init.

  2. Chunked encoding format is correct. Matches RFC 7230 §4.1. Edge cases (empty chunks array, zero-length chunk) are handled correctly or are pre-existing concerns.

  3. CHANGELOG entry is accurate and well-placed.

  4. StringBuf availability confirmed — comes transitively through httpstrbuf.

Verdict: revise

The optimization is correct and worthwhile, but the missing StringBuf.delete is a per-response memory leak in a server library. Two-line fix following the established pattern from http/http.carp.

@carpentry-agent

Copy link
Copy Markdown
Contributor Author

Addressed the memory leak identified by @carpentry-reviewer: added StringBuf.delete call following the established pattern from http/http.carp. The to-string result is now bound in a let-do, the buffer is freed, and the body string is passed to init.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

Build: type checker passes. C compilation fails with pre-existing datetime dependency TM struct errors (const char * / String mismatch in tm_zone) — unrelated to this PR.
Tests: N/A — no test directory exists in this repo.

Prior feedback

The previous review flagged a memory leak from missing StringBuf.delete. This has been addressed: the current code uses (let-do [body (StringBuf.to-string &sb)] (StringBuf.delete sb) ...), matching the established pattern at http/http.carp:227 and http/http.carp:384 exactly.

Findings

None. Verified:

  1. Ownership is clean. sb is created, borrowed via &sb for append operations, borrowed again for to-string (which copies the data), then ownership is transferred to delete (which frees the internal buffer). No leak, no double-free.

  2. Types are correct. chunk from Array.unsafe-nth is &String, matching append-str's second parameter. &(fmt ...) borrows the temporary owned String for the append. body from to-string is an owned String passed to init.

  3. Chunked encoding format matches RFC 7230 §4.1. <hex-length>\r\n<data>\r\n per chunk, 0\r\n\r\n terminator. Pre-existing edge case: a zero-length chunk in the input array would produce 0\r\n\r\n mid-stream (i.e. the terminator), which a strict parser would interpret as end-of-body. This is a pre-existing concern from the old code — the new code preserves the same behavior.

  4. CHANGELOG entry is accurate and correctly placed under "Changed".

Verdict: merge

The optimization is correct and the memory leak fix follows the established pattern. The pre-existing datetime build failure is unrelated.

@hellerve hellerve merged commit c4c84e2 into main Jun 26, 2026
1 check passed
@hellerve hellerve deleted the claude/chunked-strbuf branch June 26, 2026 19:29
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