perf(cache): bound the response cache by bytes rather than entry count - #255
Merged
Conversation
A DNS response spans tens of bytes to tens of kilobytes, so `max_capacity` counting entries left the memory a full cache occupies decided by whatever traffic it happened to see. A run of large DNSSEC or TXT answers grew it without limit, which is the wrong thing to leave open on the small hosts noadd targets. Give moka a weigher. An entry is charged its response, its domain, its offsets table, and ENTRY_OVERHEAD_BYTES for the part that does not vary -- moka's node, the Arc header and the CacheKey struct. That constant is measured rather than estimated: cache_memory_bench now reports it on every run (364.6 bytes against the 365 in the code), so a change to the entry's shape surfaces as the constant drifting rather than as a cache quietly holding more than it was told to. The default is 5 MiB, which at the ~499 bytes an entry measures on ordinary traffic holds roughly the 10,000 entries the count-based bound allowed. This is a ceiling, not a smaller cache. `new` is renamed `with_capacity_bytes` on purpose: the argument changed meaning, and every one of the 33 call sites would otherwise have kept compiling while saying something else. Test call sites take a deliberately roomy bound -- none of them ever meant to exercise eviction. Covered by two tests in cache_test.rs comparing how many entries survive a fixed cap at two response sizes; both were observed failing without the weigher (60 of 60 and 400 of 400 survived) and passing with it. Cache-hit throughput is unchanged, as expected -- the weigher runs on insert and eviction, not on a hit: 4.23M, 4.24M, 4.35M qps over three cache_hit_bench runs, inside the 4.08-4.97M spread measured on the parent commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #255 +/- ##
=======================================
Coverage 90.92% 90.92%
=======================================
Files 30 30
Lines 10763 10768 +5
=======================================
+ Hits 9786 9791 +5
Misses 977 977 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
Third and last of the changes prompted by Cloudflare's DNS cache memory write-up, after #252 and #254. Not one of its five techniques — this is its premise: per-entry footprint is a quantity you manage, and you cannot manage it if the bound is in the wrong unit.
The problem
max_capacity(10_000)counted entries. A DNS response spans tens of bytes to tens of kilobytes, so the memory a full cache occupied was decided by whatever traffic it happened to see. A run of large DNSSEC or TXT answers grew it without limit — the wrong thing to leave open on the small hosts noadd targets, and the same concern that already drove themimallocchoice.The change
An entry is charged its response, its domain, its offsets table, and
ENTRY_OVERHEAD_BYTESfor the part that does not vary — moka's node, theArcheader and theCacheKeystruct:That constant is measured, not estimated.
cache_memory_benchgained a line that reports it, and it currently reads 364.6 against the 365 in the code — so a change to the entry's shape shows up as the constant drifting rather than as a cache quietly holding more than it was told to.The default is 5 MiB, which at the ~499 bytes an entry measures on ordinary traffic holds roughly the 10,000 entries the old bound allowed. This is a ceiling, not a smaller cache.
new→with_capacity_bytesThe argument changed meaning, so keeping the name would have left all 33 call sites compiling while saying something else. Renaming makes the compiler find every one. Test call sites take a deliberately roomy bound — none of them ever meant to exercise eviction, and I checked that none depends on it.
Tests
Two in
cache_test.rs, both comparing how many of N entries survive a fixed cap at two response sizes — asserting the property, not a number, since exactly which entries moka keeps is its business:capacity_is_measured_in_bytes_not_entries— 60 tiny responses all survive a 100 KB cap; 60 × 8 KB responses do not.a_large_response_displaces_more_than_a_small_one— a 20-byte response is worth several 2 KB ones.Both observed failing without the weigher (
60 of 60 survived,400 small against 400 large) and passing with it.Throughput
Unchanged, as expected — the weigher runs on insert and eviction, never on a hit: 4.23M, 4.24M, 4.35M qps over three
cache_hit_benchruns, inside the 4.08–4.97M spread measured on the parent commit.Not done here
No CLI flag or runtime setting for the figure; it stays a constant in
main.rs, as the entry count was. Exposing it is a reasonable follow-up but a separate one — the ceiling existing at all is the change here.🤖 Generated with Claude Code