Skip to content

Conversation

@tac0turtle
Copy link
Contributor

@tac0turtle tac0turtle commented Jan 2, 2026

Overview

Tune badgerdb for our write heavy usecase, this is a small change i dont believe we need to go beyond this

closes #2605

@github-actions
Copy link
Contributor

github-actions bot commented Jan 2, 2026

The latest Buf updates on your PR. Results from workflow CI / buf-check (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped✅ passed✅ passedJan 2, 2026, 4:18 PM

@claude
Copy link
Contributor

claude bot commented Jan 2, 2026

Claude finished @tac0turtle's task in 2m 38s —— View job


Code Review Complete

I've reviewed the BadgerDB optimization and benchmarking tool changes. Overall, this is a well-implemented performance improvement with good documentation and a useful benchmarking tool. Here's my detailed feedback:


Summary

Strengths:

  • Well-reasoned BadgerDB tuning for append-heavy workloads
  • Clean abstraction with BadgerOptions() function
  • Comprehensive benchmarking tool with good UX
  • Excellent documentation updates

Areas for Improvement:

  • Minor concurrency consideration for compactor count
  • Benchmark tool could benefit from additional validation and features
  • Consider adding unit tests for the new options function

1. BadgerDB Options Tuning (pkg/store/badger_options.go)

Positives ✅

Well-documented rationale: Each option change includes clear comments explaining the "why" behind the tuning:

  • WithDetectConflicts(false) - Appropriate for single-writer scenarios
  • WithNumLevelZeroTables(10) and WithNumLevelZeroTablesStall(20) - Good for bursty write patterns
  • Dynamic compactor count based on CPU cores - Smart resource utilization

Clean API design: The BadgerOptions() function provides a clear, reusable interface.

Suggestions 💡

  1. Add CPU core validation: pkg/store/badger_options.go:29-36

    func compactorCount() int {
        count := runtime.NumCPU()
        if count < 4 {
            return 4
        }
        if count > 8 {
            return 8
        }
        return count
    }

    Issue: runtime.NumCPU() can theoretically return 0 or negative values in edge cases (container limits, cgroup restrictions).

    Recommendation:

    func compactorCount() int {
        count := runtime.NumCPU()
        if count <= 0 {
            count = 1 // Defensive fallback
        }
        if count < 4 {
            return 4
        }
        if count > 8 {
            return 8
        }
        return count
    }
  2. Consider making options configurable: pkg/store/badger_options.go:11-24

    The hardcoded values (10, 20, 4-8 compactors) work well as defaults, but consider whether users might benefit from environment variables or config overrides for:

    • Systems with unusual hardware profiles
    • Development vs production tuning
    • A/B testing different configurations

    Not required now, but worth considering for future iterations if users report needing custom tuning.

  3. Add package-level documentation:

    Consider adding a package comment at the top of badger_options.go:

    // Package store provides tuned BadgerDB configurations optimized for
    // ev-node's append-heavy blockchain data persistence patterns.

2. Integration Changes (pkg/store/kv.go)

Positives ✅

Consistent application: The changes correctly apply BadgerOptions() to both:

  • Production storage: NewDefaultKVStore() (line 21)
  • Test storage: NewTestInMemoryKVStore() (lines 59-60)

Backwards compatible: The API surface remains unchanged - this is purely an internal optimization.

Suggestions 💡

  1. In-memory test isolation: pkg/store/kv.go:58-62

    func NewTestInMemoryKVStore() (ds.Batching, error) {
        inMemoryOptions := BadgerOptions()
        inMemoryOptions.Options = inMemoryOptions.Options.WithInMemory(true)
        return badger4.NewDatastore("", inMemoryOptions)
    }

    Consideration: Test environments might benefit from different tuning than production. For example:

    • Tests might not need 4-8 compactors (could use fewer for faster cleanup)
    • L0 table counts could be lower for smaller test datasets

    Recommendation: Consider a separate TestBadgerOptions() function if test performance becomes an issue, but the current approach is fine for now.


3. Benchmarking Tool (tools/db-bench/main.go)

Positives ✅

Excellent structure:

  • Clean separation of concerns (config parsing, profile execution, result display)
  • Deterministic seeding (rand.NewSource(1)) for reproducible benchmarks
  • Good CLI flag design with sensible defaults
  • Proper cleanup with defer patterns

Good UX features:

  • Human-readable byte formatting
  • Clear result output
  • Support for comparing multiple profiles
  • Temp directory management with -keep flag

Critical Issues 🔴

None! The code is well-written.

Suggestions 💡

  1. Add context cancellation support: tools/db-bench/main.go:137-209

    The benchmark can run for a long time with large -bytes values. Consider adding signal handling for graceful shutdown:

    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer cancel()

    This would allow users to Ctrl+C cleanly and still get partial results.

  2. Validate overwrite logic: tools/db-bench/main.go:148-154

    keyIndex := keysWritten
    if overwriteEvery > 0 && i%overwriteEvery == 0 && keysWritten > 0 {
        keyIndex = i % keysWritten
    } else {
        keysWritten++
    }

    Issue: When i%overwriteEvery == 0 and i == 0, this skips the first write. Consider:

    if overwriteEvery > 0 && i > 0 && i%overwriteEvery == 0 && keysWritten > 0 {

    Impact: Minor - only affects the very first write in the benchmark.

  3. Add progress indicators: For long-running benchmarks, consider printing progress:

    if i > 0 && i%(totalWrites/10) == 0 {
        fmt.Printf("Progress: %d%%\n", (i*100)/totalWrites)
    }
  4. Error handling enhancement: tools/db-bench/main.go:183-186

    if err := kv.Sync(ctx, ds.NewKey("/")); err != nil {
        _ = kv.Close()
        return result{}, fmt.Errorf("sync failed: %w", err)
    }

    The kv.Close() error is silently discarded. Consider:

    if err := kv.Sync(ctx, ds.NewKey("/")); err != nil {
        closeErr := kv.Close()
        return result{}, fmt.Errorf("sync failed: %w (close: %v)", err, closeErr)
    }
  5. Add read benchmarks: Consider adding a read phase after writes to measure:

    • Sequential read performance
    • Random read performance
    • Range query performance

    This would give a more complete picture of the tuning impact.

  6. Memory profiling option: Add a -memprofile or -cpuprofile flag to enable Go's pprof profiling during benchmarks.


4. Security Review 🔒

No security concerns identified.

  • No credential handling
  • No network operations
  • No user input parsing vulnerabilities
  • Proper path handling with filepath package
  • Appropriate file permissions (0o755)

5. Performance & Resource Efficiency ⚡

Positives ✅

Smart tuning decisions:

  • Disabling conflict detection removes unnecessary overhead for single-writer scenarios
  • Increased L0 tables reduce compaction frequency during write bursts
  • CPU-scaled compactor count optimizes for available resources

Benchmark design:

  • Batching writes (default 1000) is realistic for production patterns
  • Deterministic data generation avoids randomization overhead
  • Proper sync and close ensure accurate timing

Considerations 💭

  1. Compactor count ceiling: The 8-compactor maximum is conservative. Consider:

    • High-core-count servers (32+, 64+ cores) might benefit from more compactors
    • Monitor in production to see if compaction becomes a bottleneck
    • The current cap is safe and prevents over-subscription
  2. Memory implications:

    • More L0 tables = higher memory usage during compaction
    • The 10/20 split is reasonable but should be monitored
    • Consider documenting expected memory overhead

6. Testing & Reliability 🧪

Missing Test Coverage

Recommendation: Add unit tests for BadgerOptions():

func TestBadgerOptions(t *testing.T) {
    opts := BadgerOptions()
    
    // Verify key options are set
    require.NotNil(t, opts)
    // Note: Options are private, so testing behavior might require
    // integration tests or checking if defaults are modified correctly
}

func TestCompactorCount(t *testing.T) {
    // This would require refactoring compactorCount to be exported
    // or testing it indirectly through BadgerOptions behavior
}

Integration test suggestion: Add a test that:

  1. Creates a store with BadgerOptions()
  2. Performs a write burst (simulating block production)
  3. Verifies write latency stays within acceptable bounds

7. Documentation 📚

Positives ✅

README updates are excellent:

  • Clear guidance on using the benchmark tool
  • Proper flag documentation
  • Good examples

Code comments are clear and purposeful.

Suggestions 💡

  1. Add benchmark interpretation guide: In tools/db-bench/README.md, add a section:

    ## Interpreting Results
    
    - **Throughput (MB/s)**: Raw write speed; higher is better
    - **Writes/sec**: Operations per second; higher is better  
    - **DB size**: On-disk footprint; compare compression efficiency
    
    Expect evnode profile to show 15-30% higher throughput for append-heavy
    workloads due to reduced compaction overhead.
  2. Document when to use defaults: In pkg/store/README.md, add:

    ## Performance Tuning
    
    The default `BadgerOptions()` are optimized for:
    - Append-heavy write patterns (block/transaction storage)
    - Single-writer scenarios (sequencer node)
    - Periodic overwrites (state updates)
    
    For read-heavy or multi-writer use cases, consider custom options.

8. Code Quality & Architecture 🏗️

Positives ✅

Excellent Go idioms:

  • Proper error wrapping with %w
  • Clean function signatures
  • Good naming conventions
  • Appropriate use of defer

Modularity:

  • BadgerOptions() is properly extracted and reusable
  • Benchmark profiles are extensible
  • Configuration is well-structured

Minor Style Notes

  1. Export profile struct? tools/db-bench/main.go:32-35

    Currently profile is private. If you want external packages to add custom profiles, consider:

    type Profile struct {
        Name string
        Open func(path string) (ds.Batching, error)
    }
  2. Constant extraction: tools/db-bench/main.go:118

    rng := rand.New(rand.NewSource(1)) // Deterministic seed

    Consider:

    const benchmarkSeed = 1 // Deterministic seed for reproducible benchmarks

9. Recommended Action Items

High Priority

  • Add defensive check for runtime.NumCPU() <= 0 in compactorCount()

Medium Priority

  • Add unit tests for BadgerOptions() function
  • Fix first-write skip in overwrite logic (i > 0 check)
  • Add progress indicators for long-running benchmarks

Low Priority (Future Enhancements)

  • Add context cancellation to benchmark tool
  • Add read performance benchmarks
  • Consider configurable tuning parameters
  • Add memory/CPU profiling flags
  • Expand documentation with interpretation guide

Conclusion

This PR represents a high-quality performance improvement with excellent supporting tooling. The BadgerDB tuning is well-reasoned and appropriate for ev-node's workload. The benchmark tool is production-ready with minor room for enhancement.

Approval recommendation:Approve with minor suggestions

The critical functionality is sound. The suggestions above are optimizations and safeguards that can be addressed in follow-up PRs if desired.


@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @tac0turtle, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the performance of the BadgerDB key-value store by introducing a set of finely-tuned default options tailored for the ev-node's specific write workload. To support validation and comparison, a new benchmarking utility has also been added, allowing developers to assess the impact of these optimizations.

Highlights

  • BadgerDB Option Tuning: Introduced a new BadgerOptions() function in pkg/store/badger_options.go to provide a set of tuned configurations for BadgerDB, specifically optimized for ev-node's append-heavy write patterns with periodic overwrites.
  • Configuration Details: The tuning includes disabling conflict detection, increasing NumLevelZeroTables to 10 and NumLevelZeroTablesStall to 20 to handle bursty writes, and dynamically scaling NumCompactors based on CPU count (between 4 and 8).
  • Integration of Tuned Options: The NewDefaultKVStore and NewTestInMemoryKVStore functions in pkg/store/kv.go were updated to utilize these new BadgerOptions() by default.
  • New Benchmarking Tool: A new command-line tool, db-bench, has been added under tools/db-bench. This tool allows for local benchmarking of BadgerDB performance, comparing the ev-node tuned options against Badger's default settings.
  • Documentation Updates: The pkg/store/README.md was updated to mention the tuned Badger options and the existence of the db-bench tool for validation. A new tools/db-bench/README.md was also added to document the usage of the benchmarking tool.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces performance tuning for BadgerDB by providing a set of default options tailored for the application's write patterns. It also adds a new benchmark tool, db-bench, to validate these performance improvements. The changes are well-structured and the new options are correctly applied. The benchmark tool is a valuable addition. I have provided a couple of suggestions to improve the implementation of the benchmark tool for better error handling and code clarity.

for _, p := range selected {
profileDir := filepath.Join(baseDir, p.name)
if cfg.reset {
_ = os.RemoveAll(profileDir)
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The error returned by os.RemoveAll is ignored. If removing the directory fails (e.g., due to permissions), the benchmark might run with old data or fail at a later stage, leading to confusing results. It would be better to handle this error, for instance by logging a warning to the user.

Suggested change
_ = os.RemoveAll(profileDir)
if err := os.RemoveAll(profileDir); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to remove profile directory %s: %v\n", profileDir, err)
}

Comment on lines +132 to +144
kv, err := p.open(dir)
if err != nil {
return result{}, fmt.Errorf("failed to open db: %w", err)
}

ctx := context.Background()
start := time.Now()

batch, err := kv.Batch(ctx)
if err != nil {
_ = kv.Close()
return result{}, fmt.Errorf("failed to create batch: %w", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The manual calls to kv.Close() on various execution paths are repetitive and can be simplified using a defer statement. This ensures the database connection is always closed, making the code cleaner and more robust.

This suggestion adds defer kv.Close() and cleans up the first error handling block. Please also remove the other manual kv.Close() calls throughout this function for consistency (on lines 157, 164, 169, 178, 184, and the block on 188-190).

Suggested change
kv, err := p.open(dir)
if err != nil {
return result{}, fmt.Errorf("failed to open db: %w", err)
}
ctx := context.Background()
start := time.Now()
batch, err := kv.Batch(ctx)
if err != nil {
_ = kv.Close()
return result{}, fmt.Errorf("failed to create batch: %w", err)
}
kv, err := p.open(dir)
if err != nil {
return result{}, fmt.Errorf("failed to open db: %w", err)
}
defer kv.Close()
ctx := context.Background()
start := time.Now()
batch, err := kv.Batch(ctx)
if err != nil {
return result{}, fmt.Errorf("failed to create batch: %w", err)
}

@codecov
Copy link

codecov bot commented Jan 2, 2026

Codecov Report

❌ Patch coverage is 7.10383% with 170 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.66%. Comparing base (42228e2) to head (b54eaf3).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
tools/db-bench/main.go 0.00% 166 Missing ⚠️
pkg/store/badger_options.go 71.42% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2950      +/-   ##
==========================================
- Coverage   59.15%   57.66%   -1.49%     
==========================================
  Files          90       92       +2     
  Lines        8632     8900     +268     
==========================================
+ Hits         5106     5132      +26     
- Misses       2944     3179     +235     
- Partials      582      589       +7     
Flag Coverage Δ
combined 57.66% <7.10%> (-1.49%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

perf: configure db options

2 participants