Skip to content

fix: use hashlink::LruCache in SizeTrackingLruCache#5922

Merged
hanabi1224 merged 8 commits intomainfrom
hm/hashlink-lru
Aug 13, 2025
Merged

fix: use hashlink::LruCache in SizeTrackingLruCache#5922
hanabi1224 merged 8 commits intomainfrom
hm/hashlink-lru

Conversation

@hanabi1224
Copy link
Contributor

@hanabi1224 hanabi1224 commented Aug 11, 2025

Summary of changes

Changes introduced in this pull request:

  • use hashlink::LruCache in SizeTrackingLruCache to reduce memory usage
  • use hashbrown::HashMap in ZstdFrameCache to track the index size accurately
  • add benchmarks for lru::LruCache and hashlink::LruCache
     Running benches/lru.rs (target/release/deps/lru-8442215fe6c9570d)
Gnuplot not found, using plotters backend
LRU/lru::LruCache::push time:   [203.49 µs 204.77 µs 205.90 µs]
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) low mild
LRU/lru::LruCache::get  time:   [53.115 µs 54.036 µs 55.057 µs]
LRU/hashlink::LruCache::insert
                        time:   [241.97 µs 243.43 µs 244.81 µs]
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild
LRU/hashlink::LruCache::get
                        time:   [47.782 µs 48.787 µs 49.794 µs]
Found 5 outliers among 100 measurements (5.00%)
  5 (5.00%) high mild

Reference issue to close (if applicable)

Closes

Other information and links

Change checklist

  • I have performed a self-review of my own code,
  • I have made corresponding changes to the documentation. All new code adheres to the team's documentation standards,
  • I have added tests that prove my fix is effective or that my feature works (if possible),
  • I have made sure the CHANGELOG is up-to-date. All user-facing changes should be reflected in this document.

Summary by CodeRabbit

  • Performance
    • Improved cache efficiency and memory tracking by switching to a different LRU backend and a more compact hash map implementation.
  • Documentation
    • Added bench run instructions and minor formatting for benchmarks.
  • Refactor
    • Reworked internal caching and index handling and updated related APIs without changing external behavior.
  • Chores
    • Aligned and updated workspace dependencies for consistency.
  • Tests
    • Updated tests to reflect the internal cache and index changes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 11, 2025

Walkthrough

Replaces lru with hashlink and ahash::HashMap with hashbrown::HashMap in CAR cache paths; updates SizeTrackingLruCache API (push/insert/capacity), ZstdFrameCache::put signature and index handling (shrink_to_fit, CidWrapper::from); aligns workspace Cargo.toml deps; adds bench docs.

Changes

Cohort / File(s) Summary
Workspace dependencies
Cargo.toml
Add hashbrown dependency and hashlink workspace dependency; extend get-size2 features to include hashbrown; root crates reference hashlink = { workspace = true }; remove lru from dependencies.
LRU implementation swap
src/utils/cache/lru.rs
Replace lru::LruCache with hashlink::LruCache; initialize bounded or unbounded cache based on optional capacity; change API usage to insert/remove_lru/capacity; SizeTrackingLruCache::push signature changed from -> Option<(K,V)> to -> Option<V>.
CAR cache & frame index map
src/db/car/mod.rs, src/db/car/forest.rs
Replace ahash::HashMap with hashbrown::HashMap<CidWrapper, Vec<u8>>; use CidWrapper::from(...) for lookups; ZstdFrameCache::put now accepts mut hashbrown::HashMap and calls shrink_to_fit(); adjust LRU insertion/eviction handling and current-size accounting; tests updated to use hashbrown::HashMap.
Benchmarks docs
benches/car-index.rs, benches/example-benchmark.rs
Insert/format file-level documentation showing how to run bench(s); purely documentation edits.
Tooling config
.clippy.toml
Update disallowed-methods entries to reference hashlink::LruCache variants (remove unbounded lru entries, map unbounded_with_hasherhashlink::LruCache::new_unbounded).

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ForestCar
  participant ZstdFrameCache
  participant LruCache

  Caller->>ForestCar: get(cid)
  ForestCar->>ZstdFrameCache: get(offset, key)
  ZstdFrameCache->>LruCache: get_cloned(key)
  alt cache hit
    LruCache-->>ZstdFrameCache: Some(index map)
    ZstdFrameCache-->>ForestCar: index map (hashbrown)
    ForestCar->>ForestCar: lookup using CidWrapper::from(cid)
    ForestCar-->>Caller: Some(Vec<u8>) or None
  else cache miss
    LruCache-->>ZstdFrameCache: None
    ForestCar->>ForestCar: build index (hashbrown::HashMap)
    ForestCar->>ZstdFrameCache: put(offset, key, mut index)
    ZstdFrameCache->>LruCache: insert(key, index)
    ZstdFrameCache->>ZstdFrameCache: update current_size (shrink_to_fit, adjust for prev entry)
    ForestCar-->>Caller: Some(Vec<u8>) or None
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • elmattic
  • LesnyRumcajs
  • akaladarshi

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 145e145 and d81d645.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • .clippy.toml (1 hunks)
  • Cargo.toml (2 hunks)
  • src/db/car/mod.rs (4 hunks)
  • src/utils/cache/lru.rs (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Cargo.toml
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-17T15:21:40.753Z
Learnt from: hanabi1224
PR: ChainSafe/forest#5841
File: src/utils/get_size/mod.rs:10-10
Timestamp: 2025-07-17T15:21:40.753Z
Learning: The get_size2 crate's GetSize trait provides default implementations: get_stack_size() uses std::mem::size_of, get_heap_size() returns 0, and get_size() returns their sum. An empty impl like `impl GetSize for MyType {}` is valid and uses these defaults, making it suitable for simple wrapper types that don't allocate heap memory.

Applied to files:

  • src/utils/cache/lru.rs
📚 Learning: 2025-08-04T13:36:22.993Z
Learnt from: hanabi1224
PR: ChainSafe/forest#5835
File: src/chain/tests.rs:58-76
Timestamp: 2025-08-04T13:36:22.993Z
Learning: In the Forest codebase, `Vec<u8>` can be used as an `AsyncWrite` implementation in test contexts. The user confirmed that tests using `&mut Vec<u8>` with `export` and `export_v2` functions compile and pass both locally and on CI.

Applied to files:

  • src/db/car/mod.rs
📚 Learning: 2025-08-08T12:10:45.218Z
Learnt from: hanabi1224
PR: ChainSafe/forest#5867
File: src/ipld/util.rs:553-558
Timestamp: 2025-08-08T12:10:45.218Z
Learning: Forest project targets Rust stable >=1.89; features stabilized in 1.88 like let-chains are acceptable in this codebase.

Applied to files:

  • src/db/car/mod.rs
🧬 Code Graph Analysis (1)
src/utils/cache/lru.rs (2)
src/beacon/drand.rs (1)
  • new (251-269)
src/db/car/mod.rs (1)
  • new (72-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (go)
  • GitHub Check: Build Ubuntu
  • GitHub Check: Build MacOS
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: All lint checks
  • GitHub Check: tests
  • GitHub Check: tests-release
🔇 Additional comments (13)
.clippy.toml (1)

48-57: Correct migration from lru to hashlink in disallowed methods.

The update properly replaces the lru crate methods with their hashlink equivalents while maintaining the same security reasoning. The method signatures align with the hashlink::LruCache API.

src/utils/cache/lru.rs (7)

16-16: LGTM - Clean import migration.

The switch from lru::LruCache to hashlink::LruCache is correct and aligns with the PR objectives to reduce memory usage.


133-134: API method name update for consistency.

The method call correctly updates from the lru crate's API to hashlink's contains_key method.


152-154: Correct method migration for LRU removal.

The change from pop_lru() to remove_lru() correctly follows the hashlink API. The return type remains Option<(K, V)>, maintaining backward compatibility for this method.


160-162: Capacity method update aligns with hashlink API.

The change from cap().get() to capacity() correctly reflects the hashlink::LruCache API where capacity() directly returns a usize.


219-219: Metrics capacity reporting updated correctly.

The capacity metric now correctly calls self.cap() which internally calls capacity() on the hashlink cache, maintaining consistent reporting.


70-75: Confirm hashlink::LruCache capacity semantics align with previous LRU implementation

Although we’ve correctly wired up

capacity
  .map(From::from)          // NonZeroUsize → usize
  .map(LruCache::new)       // bounded by number of entries
  .unwrap_or_else(LruCache::new_unbounded),

we need to be sure that LruCache::new(capacity) in hashlink uses entry count (not bytes) as its limit and that eviction behavior matches our prior LRU. Please:

• Review src/utils/cache/lru.rs in the new_inner function (around lines 70–75) to confirm that the hashlink API’s notion of “capacity” is indeed the max number of items.
• Add or update a unit test that drives the cache just past its capacity and asserts that the oldest entry is evicted.
• Optionally skim the hashlink docs to validate there are no surprises (e.g., per-entry cost weighting).


124-126: No breaking change in push return handling
We searched the codebase for any destructuring of push(...) into (K, V) and found none. All callers treat the returned Option as the value V (or ignore it), so switching from Option<(K, V)> to Option<V> is safe and requires no further updates.

src/db/car/mod.rs (5)

60-62: Good documentation and hashbrown migration rationale.

The inline comment clearly explains why hashbrown::HashMap is chosen over other HashMap implementations - for accurate GetSize implementation via allocation_size. This is a well-reasoned technical decision.


87-87: Consistent CidWrapper usage for key lookups.

The change to CidWrapper::from(cid) aligns with the new hashbrown::HashMap<CidWrapper, Vec> type and maintains consistency with the updated key handling pattern.


91-98: Memory optimization with shrink_to_fit call.

The addition of index.shrink_to_fit() before insertion is a good memory optimization that reduces unnecessary allocation overhead. This aligns well with the overall goal of reducing memory usage.


107-107: Correct handling of push method return type change.

The code correctly adapts to the new push method signature that returns Option<V> instead of Option<(K, V)>. The variable name prev_entry accurately reflects that only the previous value is returned.


157-159: Test helper updated for hashbrown consistency.

The test helper function correctly switches to hashbrown::HashMap::default() and updates the return type to match the new hashbrown-based implementation.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch hm/hashlink-lru

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@hanabi1224 hanabi1224 marked this pull request as ready for review August 11, 2025 16:03
@hanabi1224 hanabi1224 requested a review from a team as a code owner August 11, 2025 16:03
@hanabi1224 hanabi1224 requested review from LesnyRumcajs, akaladarshi and elmattic and removed request for a team August 11, 2025 16:03
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
benches/lru.rs (5)

21-21: Prefer NonZeroUsize::new(...) over try_into().unwrap() for clarity

More idiomatic and self-documenting when constructing the lru::LruCache capacity.

Apply this diff:

-                let mut c = lru::LruCache::new(LRU_CAPACITY.try_into().unwrap());
+                let mut c = lru::LruCache::new(std::num::NonZeroUsize::new(LRU_CAPACITY).unwrap());
-            let mut c = lru::LruCache::new(LRU_CAPACITY.try_into().unwrap());
+            let mut c = lru::LruCache::new(std::num::NonZeroUsize::new(LRU_CAPACITY).unwrap());

Also applies to: 28-28


19-26: Isolate insertion cost using iter_batched to avoid including cache construction time

Separating setup from the measured body yields a cleaner comparison of push/insert throughput.

Apply this diff:

-        .bench_function("lru::LruCache::push", |b| {
-            b.iter(|| {
-                let mut c = lru::LruCache::new(LRU_CAPACITY.try_into().unwrap());
-                for (k, v) in black_box(&input).iter() {
-                    c.push(k, v);
-                }
-            })
-        })
+        .bench_function("lru::LruCache::push", |b| {
+            b.iter_batched(
+                || lru::LruCache::new(std::num::NonZeroUsize::new(LRU_CAPACITY).unwrap()),
+                |mut c| {
+                    for (k, v) in black_box(&input).iter() {
+                        c.push(k, v);
+                    }
+                    black_box(c)
+                },
+                criterion::BatchSize::SmallInput,
+            )
+        })

Outside these lines, add BatchSize to the imports:

-use criterion::{Criterion, criterion_group, criterion_main};
+use criterion::{Criterion, criterion_group, criterion_main, BatchSize};

38-45: Mirror insertion-bench structure for hashlink with iter_batched

Aligns both implementations’ measurement methodology and reduces noise from cache construction.

Apply this diff:

-        .bench_function("hashlink::LruCache::insert", |b| {
-            b.iter(|| {
-                let mut c = hashlink::LruCache::new(LRU_CAPACITY);
-                for (k, v) in black_box(&input).iter() {
-                    c.insert(k, v);
-                }
-            })
-        })
+        .bench_function("hashlink::LruCache::insert", |b| {
+            b.iter_batched(
+                || hashlink::LruCache::new(LRU_CAPACITY),
+                |mut c| {
+                    for (k, v) in black_box(&input).iter() {
+                        c.insert(k, v);
+                    }
+                    black_box(c)
+                },
+                criterion::BatchSize::SmallInput,
+            )
+        })

22-24: Be explicit about benchmarking reference types vs. owned types

Both insertion benches use &usize/&String entries (borrowing from input). This is fine but may under-represent costs (e.g., hashing of owned String, moves). Consider adding an additional benchmark variant that inserts owned usize/String to present a fuller picture.

For example, add parallel benches using cloned values:

// Example snippet outside provided ranges (add as a new bench):
group.bench_function("lru::LruCache::push_owned", |b| {
    b.iter_batched(
        || lru::LruCache::new(std::num::NonZeroUsize::new(LRU_CAPACITY).unwrap()),
        |mut c| {
            for (k, v) in black_box(&input).iter().cloned() {
                c.push(k, v);
            }
            black_box(c)
        },
        BatchSize::SmallInput,
    )
});

Also applies to: 41-43


60-69: Minor: gen_input can be tightened up slightly

Style-only. Using extend with an array is a tad cleaner; performance is identical here.

Apply this diff:

-    for i in 0..LRU_CAPACITY {
-        v.push((i, format!("{i}")));
-        v.push((i, format!("{i}")));
-    }
+    for i in 0..LRU_CAPACITY {
+        v.extend([(i, format!("{i}")), (i, format!("{i}"))]);
+    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a23fb32 and a5907a8.

📒 Files selected for processing (4)
  • Cargo.toml (3 hunks)
  • benches/car-index.rs (1 hunks)
  • benches/example-benchmark.rs (1 hunks)
  • benches/lru.rs (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • benches/example-benchmark.rs
  • benches/car-index.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • Cargo.toml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: tests-release
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: tests
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: Build Ubuntu
  • GitHub Check: Build MacOS
  • GitHub Check: All lint checks
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (go)
🔇 Additional comments (1)
benches/lru.rs (1)

4-6: Nice touch: runnable doc snippet

The inline console snippet makes it clear how to run the bench. Good developer experience.

LesnyRumcajs
LesnyRumcajs previously approved these changes Aug 12, 2025
LesnyRumcajs
LesnyRumcajs previously approved these changes Aug 12, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/db/car/mod.rs (1)

93-100: Consider the performance impact of shrink_to_fit().

The addition of index.shrink_to_fit() at line 99 will reduce memory usage by releasing excess capacity, which aligns with the PR's memory optimization goals. However, this operation can trigger memory reallocation and copying, which may impact performance when called frequently.

Consider whether this should be conditional based on the excess capacity ratio to avoid frequent reallocations for small excesses.

Example optimization to avoid frequent reallocations:

-        index.shrink_to_fit();
+        // Only shrink if we have significant excess capacity (e.g., > 25%)
+        if index.capacity() > index.len() * 5 / 4 {
+            index.shrink_to_fit();
+        }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9a5a10 and 145e145.

📒 Files selected for processing (1)
  • src/db/car/mod.rs (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-04T13:36:22.993Z
Learnt from: hanabi1224
PR: ChainSafe/forest#5835
File: src/chain/tests.rs:58-76
Timestamp: 2025-08-04T13:36:22.993Z
Learning: In the Forest codebase, `Vec<u8>` can be used as an `AsyncWrite` implementation in test contexts. The user confirmed that tests using `&mut Vec<u8>` with `export` and `export_v2` functions compile and pass both locally and on CI.

Applied to files:

  • src/db/car/mod.rs
📚 Learning: 2025-08-08T12:10:45.218Z
Learnt from: hanabi1224
PR: ChainSafe/forest#5867
File: src/ipld/util.rs:553-558
Timestamp: 2025-08-08T12:10:45.218Z
Learning: Forest project targets Rust stable >=1.89; features stabilized in 1.88 like let-chains are acceptable in this codebase.

Applied to files:

  • src/db/car/mod.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: All lint checks
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: Build Ubuntu
  • GitHub Check: Build MacOS
  • GitHub Check: tests
  • GitHub Check: tests-release
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (go)
🔇 Additional comments (6)
src/db/car/mod.rs (6)

60-62: LGTM! Good documentation for the HashMap type change.

The switch to hashbrown::HashMap with its accurate GetSize implementation via allocation_size is well documented and aligns with the PR's goal of tracking memory usage more accurately.


89-89: LGTM! Consistent use of CidWrapper constructor.

The change from cid.into() to CidWrapper::from(cid) is consistent with the codebase's pattern and provides better clarity about the type conversion.


109-117: LGTM! Correct handling of LRU cache replacement.

The logic correctly handles both the case where an entry is replaced (lines 110-113) and where a new entry is added (lines 115-116). The size tracking properly accounts for the difference when replacing an existing entry.


143-156: Good test coverage for cache size tracking.

The test effectively validates that the cache size tracking remains accurate across multiple insertions and replacements. Testing both new insertions and replacements of existing keys ensures the size accounting logic is correct.


159-169: LGTM! Test helper updated correctly.

The gen_index function has been properly updated to use hashbrown::HashMap, maintaining consistency with the production code changes.


118-127: Eviction loop memory ordering under concurrency
We verified that all accesses to current_size in src/db/car/mod.rs—including the eviction loop at lines 118–127 and the insert paths—use Ordering::Relaxed for both load and RMW operations. While these atomics are safe and lock-free, relaxed ordering provides no inter-thread ordering guarantees. Under high contention, the loop’s load may see stale values, leading to over- or under-eviction relative to max_size.

If your correctness model requires strict enforcement of the size bound under concurrent inserts:

  • Switch the eviction loop’s load to Ordering::Acquire, and use Ordering::AcqRel for all fetch_add/fetch_sub.
  • Or serialize eviction and size updates behind a mutex.

Otherwise—if eventual consistency around max_size is acceptable—the current implementation remains functionally correct.

elmattic
elmattic previously approved these changes Aug 12, 2025
@elmattic elmattic added this pull request to the merge queue Aug 12, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Aug 12, 2025
@hanabi1224 hanabi1224 dismissed stale reviews from elmattic and LesnyRumcajs via d81d645 August 12, 2025 14:33
@hanabi1224 hanabi1224 enabled auto-merge August 13, 2025 08:15
@hanabi1224 hanabi1224 requested a review from elmattic August 13, 2025 08:18
@hanabi1224 hanabi1224 added this pull request to the merge queue Aug 13, 2025
Merged via the queue into main with commit cc5c4c1 Aug 13, 2025
43 checks passed
@hanabi1224 hanabi1224 deleted the hm/hashlink-lru branch August 13, 2025 08:46
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.

4 participants