Raise MemoryHigh to 150M to fit chardet's lazily-loaded tables - #427
Merged
Conversation
The 96M ceiling was set against a ~57M working set measured before httplint started running a charset check over response content. That check calls chardet, whose first invocation lazily loads 352 bigram models of 64K each -- a one-off +46M of RSS, taking the daemon to ~103M and straight back over MemoryHigh into reclaim throttling. Production peaks clustered at 104-117M across every crash, with and without --debug, which is what that arithmetic predicts. Concurrency is not the problem: measured at ~0.14M per in-flight request, near enough independent of body size, since content streams through the linter rather than accumulating. Twenty concurrent 5M fetches cost 2.6M in total. 150M leaves headroom for a few hundred concurrent requests over the ~103M floor; 192M hard cap still catches a runaway. Also corrects the comment's claim that --debug costs ~45M of tracemalloc overhead. That figure was never measured; it was chardet's 46M being misattributed. A crash with --debug off peaked at 107.8M, which is the base plus chardet and no tracemalloc at all.
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.
Follow-up to #425, which fixed two watchdog-stall causes but set
MemoryHighfrom a working-set figure that turned out to be incomplete. Production kept tripping the watchdog afterwards, most recently on Jul 29 with--debugoff.What was actually consuming the memory
Measured, not inferred. One 14 KB
text/html; charset=utf-8fetch through redbot's ownHttpResource:+46 MB, permanent, from a single small text response. In isolation it's chardet's first
detect()call:19.5 → 65.6 MB, matching exactly. Subsequent detects add nothing.The path is httplint's
verify_charset, which callschardet.detect(sample)for anytext/*with a charset parameter (or+json). chardet then lazily loads 352 bigram models of 64 KB each — 22.0 MB of live tables, plus ~24 MB of decompression the allocator never returns. That 22.0 MB matches thechardet/__init__.py:83line that had been sitting at the top of every production memory dump.So the real floor is ~57M base + 46M chardet ≈ 103M, against a
MemoryHighof 96M. The kernel throttles in reclaim, the loop parks for tens of seconds, the watchdog fires. Production peaks clustered at 104–117M across every crash — with and without--debug— which is what that arithmetic predicts.Concurrency is not a factor
Measured with chardet pre-warmed, so only per-request cost shows:
~0.14 MB per in-flight request, near enough independent of body size. Twenty concurrent 5 MB fetches — 100 MB streamed — cost 2.6 MB total. Bodies genuinely stream:
feed_contenthashes and discards, redbot's sampler caps at 8 KB, httplint's at 8192 bytes.The change
MemoryHigh96M → 150M (~47M of headroom over the ~103M floor, enough for several hundred concurrent requests),MemoryMax128M → 192M so a genuine runaway is still caught. The comment now records the measured breakdown and where each number came from.It also corrects a claim I put in that comment in #425 — that
--debugcosts ~45M of tracemalloc overhead. That figure was never measured; it was chardet's 46M misattributed. The Jul 29 crash had--debugoff and still peaked at 107.8M.For reviewers
make testpassing says nothing about it. The unit file is still not syntax-checked —systemd-analyze verifyisn't available on the dev machine.memory.eventsshould stop incrementinghigh; if it doesn't, the floor is higher than I've measured and this needs revisiting.include_encodings=[...]restricted to three encodings gave no reduction at all — chardet's@functools.cached loader pulls in every model before filtering. There's no lazy-per-encoding path in 7.4.3. One untested idea: ~24M of the 46M is freed-but-unreturned decompression memory, somalloc_trim(0)via ctypes might reclaim it on glibc. I couldn't test that on macOS and am not proposing it here.Related
Filed mnot/httplint#155 for a separate bug found while measuring:
verify_charsetreports a falseCHARSET_UNDECODABLEwhen a multi-byte character straddles the 8192-bytecontent_sampleboundary, because the sample is truncated at a fixed byte count and then decoded witherrors="strict".🤖 Written by Claude Opus 4.8 in Claude Code. @mnot directed the investigation throughout, supplied every production journal excerpt and cgroup reading, proposed the 150M figure, and asked for the concurrency evaluation that informs it. Several earlier hypotheses in this investigation — a large-body memory spike, chardet being CPU-bound, tracemalloc being the dominant footprint — were wrong and were discarded once measured; the numbers above come from instrumented runs whose output is quoted verbatim. Not yet verified against the production host.