Skip to content

perf(rpc): share neutral traces across RPC versions - #4067

Open
danielntmd wants to merge 2 commits into
danielntmd/feeder-trace-adaptersfrom
danielntmd/shared-trace-cache-integration
Open

danielntmd wants to merge 2 commits into
danielntmd/feeder-trace-adaptersfrom
danielntmd/shared-trace-cache-integration

Conversation

@danielntmd

@danielntmd danielntmd commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

User description

This PR wires the neutral trace cache from PR #4065 across RPC v8, v9, and v10

  • Refactors version-specific adaptation into helpers
  • Adds integration tests for cache sharing and version-specific behavior.

Benchmark

Cross-Version Trace Cache

Answers the question: Does sharing one block-trace execution across concurrent v8/v9/v10 block and transaction requests improve performance, and what overhead per-request response adaptation introduces on cache hits?

Setup

Request Set:
- 50% traceBlockTransactions, 50% traceTransaction
- Equal distribution across v8, v9, and v10

Working set:
- 32 measured blocks, plus one separate startup-warmup block
- Each sampled row is run 3 times (mean taken)

Iterations per sample:
- Cold: 32 burst iterations, each sending N concurrent RPC requests
- Warm: 393,216 iterations, each sending one RPC request

Cold Trace Workload Matrix

Each "burst" targets one initially uncached block B and one fixed transaction T inside B.

Request RPC v8 RPC v9 RPC v10
Block trace traceBlockTransactions(B) traceBlockTransactions(B) traceBlockTransactions(B)
Transaction trace traceTransaction(T) traceTransaction(T) traceTransaction(T)

The concurrency controls the copies of each request.

Concurrency Copies of each table cell Requests per burst
6 1 6
24 4 24
96 16 96
192 32 192

Example:

  1. Send the burst for block B1 and transaction T1; wait for all responses.
  2. Send the burst for block B2 and transaction T2; wait for all responses.
  3. Continue through 32 distinct blocks.

Warm Cache Workload Matrix

Concurrency Virtual users Total RPC requests Average requests per VU
6 6 393,216 65,536
24 24 393,216 16,384
96 96 393,216 4,096
192 192 393,216 2,048

Cross-Version Trace Cache:

Cache Concurrency Throughput p50 latency p95 latency CPU time Peak RSS
Cold 6 +12.5% -7.4% -5.5% -81.9% -23.8%
Cold 24 +244.9% -56.0% -52.4% -95.3% -32.8%
Cold 96 +1106.3% -85.2% -82.3% -98.7% -32.3%
Cold 192 +2016.9% -91.9% -90.8% -99.3% -30.8%
Warm 6 -0.8% +1.6% +2.4% +1.7% -18.3%
Warm 24 -0.9% +0.5% +2.8% +1.2% -23.8%
Warm 96 -0.9% +2.4% +1.4% +1.9% -24.3%
Warm 192 ~0.0% +2.0% +2.7% +0.2% -23.8%

Concurrent Cache-Hit Benchmark

Answers the question: “How much does serializing cache access through a single mutex cost as concurrency increases, and is that an acceptable tradeoff for a simpler design?

  • p50 samples remained below 0.2 µs at every tested concurrency.
  • Overhead from minimal design choice is negligible.

Setup

  • 1 second measuring throughput, 1 separate second sampling latency
  • 3x per sample (mean taken)

Concurrent Cache-Hit Benchmark:

Keys Workers Throughput (M hits/s) p95 (µs) p99 (µs)
1 1 45.94 → 58.44 0.033 → 0.030 0.034 → 0.031
1 6 107.70 → 19.09 0.043 → 0.390 1.081 → 3.576
1 24 58.87 → 14.89 0.921 → 1.734 9.153 → 47.968
1 192 57.15 → 10.96 1.103 → 7.836 157.440 → 519.347
32 1 35.73 → 45.81 0.039 → 0.033 0.042 → 0.035
32 6 96.90 → 11.66 0.074 → 2.348 1.418 → 19.604
32 24 47.51 → 13.09 1.440 → 5.234 17.268 → 64.260
32 192 35.22 → 6.76 3.986 → 183.766 215.974 → 679.776

PR Type

Enhancement, Tests


Description

  • Wire a shared tracecache across RPC v8, v9, and v10 handlers to reuse neutral block-trace executions.

  • Refactor trace adaptation logic into version-specific helpers (adaptCachedTrace, adaptCachedTraces).

  • Replace lru.Cache with tracecache.Cache for coordinating block trace queries and caching results.

  • Add comprehensive integration tests covering shared cache concurrency, version shapes, and failure handling.


File Walkthrough

Relevant files
Enhancement
10 files
handlers.go
Inject shared trace cache across RPC handlers                       
+6/-3     
adapt_trace.go
Add helpers to adapt cached traces to v10 types                   
+55/-4   
handlers.go
Update v10 handler to use `tracecache.Cache`                         
+13/-7   
trace.go
Refactor v10 trace fetching to use `tracecache` mechanisms
+60/-129
adapters.go
Add helpers to adapt cached traces to v8 types                     
+32/-0   
handlers.go
Update v8 handler to use `tracecache.Cache`                           
+13/-6   
trace.go
Refactor v8 trace fetching to use `tracecache` mechanisms
+39/-77 
adapters.go
Add helpers to adapt cached traces to v9 types                     
+32/-0   
handlers.go
Update v9 handler to use `tracecache.Cache`                           
+13/-7   
trace.go
Refactor v9 trace fetching to use `tracecache` mechanisms
+41/-83 
Tests
5 files
shared_trace_test.go
Add integration tests for shared trace cache                         
+468/-0 
trace_adaptation_test.go
Add tests for trace adaptation allocations                             
+119/-0 
trace_test.go
Update v10 tests to reflect new trace response structures
+6/-1     
trace_test.go
Update v8 tests to reflect new trace response structures 
+1/-1     
trace_test.go
Update v9 tests to reflect new trace response structures 
+1/-1     

@danielntmd
danielntmd added this pull request to stack #4068 September 14, 2026 07:49
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.53439% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.43%. Comparing base (d7b9154) to head (52db996).

Files with missing lines Patch % Lines
rpc/v10/trace.go 80.85% 9 Missing ⚠️
rpc/v10/adapt_trace.go 91.42% 3 Missing ⚠️
rpc/v8/trace.go 93.10% 2 Missing ⚠️
rpc/v9/trace.go 93.33% 2 Missing ⚠️
Additional details and impacted files
@@                         Coverage Diff                          @@
##           danielntmd/feeder-trace-adapters    #4067      +/-   ##
====================================================================
+ Coverage                             79.38%   79.43%   +0.04%     
====================================================================
  Files                                   469      469              
  Lines                                 36088    36097       +9     
====================================================================
+ Hits                                  28648    28672      +24     
+ Misses                                 7431     7416      -15     
  Partials                                  9        9              

☔ View full report in Codecov by Harness.
📢 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@danielntmd
danielntmd force-pushed the danielntmd/shared-trace-cache-integration branch from 6182767 to 52db996 Compare September 15, 2026 07:25
@danielntmd
danielntmd marked this pull request as ready for review September 15, 2026 07:36
@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Reviewing PR #4067

  • Gather context (diff, base branch, changed files)
  • Read repo CLAUDE.md for conventions
  • Review new/changed files for correctness, concurrency, error handling
  • Review tests for coverage/quality
  • Post inline comments for specific issues
  • Post final summary grouped by severity

@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
⚠️ Risk level: Medium
📂 Priority files

  • rpc/v10/trace.go
  • rpc/v8/trace.go
  • rpc/v9/trace.go
  • rpc/handlers.go
🏅 Score: 72
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Lease lifecycle

traceFinalisedBlock and traceBlockTransactions (v8) call defer lease.Abort() right after acquiring a lease, and then explicitly call lease.Publish(...) on success paths before returning. Since the deferred Abort() still executes after Publish() on every return path, correctness depends entirely on Lease.Abort() being a safe no-op once Publish() has already been called. If that invariant does not hold (e.g. it re-releases waiters or double-frees internal state), concurrent v8/v9/v10 callers waiting on the same block hash could see corrupted or duplicate cache entries. This is not verifiable from the diff alone since tracecache.Lease internals aren't shown, but it is the central correctness assumption of the whole shared-cache design.

func (h *Handler) traceFinalisedBlock(
	ctx context.Context,
	header *core.Header,
	returnInitialReads bool,
) (*tracecache.BlockTrace, http.Header, *jsonrpc.Error) {
	cached, lease, err := h.blockTraceCache.Acquire(
		ctx, header.Hash, func(b *tracecache.BlockTrace) bool {
			return b.Covers(returnInitialReads)
		},
	)
	if err != nil {
		return nil, defaultExecutionHeader(), rpccore.ErrUnexpectedError.CloneWithData(err.Error())
	}
	if lease == nil {
		return cached, defaultExecutionHeader(), nil
	}
	defer lease.Abort()

	fetchFromFeederGW, err := shouldFetchTracesFromFeederGateway(header, h.bcReader.Network())
	if err != nil {
		return nil,
			defaultExecutionHeader(),
			rpccore.ErrUnexpectedError.CloneWithData(err.Error())
	}

	if fetchFromFeederGW {
		traces, rpcErr := h.fetchTracesFromFeederGateway(ctx, header)
		if rpcErr != nil {
			return nil, defaultExecutionHeader(), rpcErr
		}

		lease.Publish(traces)
		return traces, defaultExecutionHeader(), nil
	}

	transactions, err := h.bcReader.TransactionsByBlockNumber(header.Number)
	if err != nil {
		if errors.Is(err, db.ErrKeyNotFound) {
			return nil, defaultExecutionHeader(), rpccore.ErrBlockNotFound
		}

		return nil,
			defaultExecutionHeader(),
			rpccore.ErrInternal.CloneWithData(err)
	}

	response, httpHeader, rpcErr := h.traceBlockWithVM(header, transactions, returnInitialReads)
	if rpcErr != nil {
		return nil, httpHeader, rpcErr
	}
	lease.Publish(response)

	return response, httpHeader, nil
}
Duplicated adaptation logic

adaptCachedTrace and adaptCachedTraces are duplicated nearly verbatim across rpc/v8/adapters.go, rpc/v9/adapters.go, and rpc/v10/adapt_trace.go. Any future fix to gas/resource mapping logic must be applied in three places, increasing the risk of the versions drifting out of sync.

func adaptCachedTrace(source *tracecache.TransactionTrace, result *TransactionTrace) {
	if feeder := source.FeederTrace(); feeder != nil {
		*result = adaptFeederTransactionTrace(TransactionType(source.Type), feeder)
	} else {
		*result = AdaptVMTransactionTrace(source.VMTrace())
	}
	if result.ExecutionResources == nil {
		result.ExecutionResources = new(ExecutionResources)
	}
	*result.ExecutionResources = ExecutionResources{
		InnerExecutionResources: InnerExecutionResources{
			L1Gas: source.Gas.L1Gas,
			L2Gas: source.Gas.L2Gas,
		},
		L1DataGas: source.Gas.L1DataGas,
	}
}

func adaptCachedTraces(result *tracecache.BlockTrace) []TracedBlockTransaction {
	traces := make([]TracedBlockTransaction, len(result.Traces))
	roots := make([]TransactionTrace, len(result.Traces))
	for i := range result.Traces {
		adaptCachedTrace(&result.Traces[i], &roots[i])
		traces[i] = TracedBlockTransaction{
			TraceRoot:       &roots[i],
			TransactionHash: &result.Traces[i].Hash,
		}
	}
	return traces
}

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