Skip to content

perf(rpc): introduce progressive trace caching - #4080

Open
danielntmd wants to merge 2 commits into
danielntmd/progressive-trace-primitivesfrom
danielntmd/progressive-trace-integration
Open

danielntmd wants to merge 2 commits into
danielntmd/progressive-trace-primitivesfrom
danielntmd/progressive-trace-integration

Conversation

@danielntmd

@danielntmd danielntmd commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

User description

Summary

TraceTransaction currently executes every transaction in a finalized block on a cache miss, even when the requested transaction appears near the beginning.

Workflow

This PR caches successful tracing progress from the beginning of each block (a prefix). Later requests reconstruct the state at the cached point and execute only the remaining transactions through the requested target (the missing suffix). Only one request extends a block at a time, while different blocks can be traced concurrently.

lookup(block, target)
├─ target cached → return, even if an extension is active
├─ extension active
│ ├─ completed → retry lookup
│ └─ caller cancelled → return cancellation error
└─ target missing → acquire lease, reconstruct state, execute missing suffix
├─ success → combine prefix + suffix, publish, wake waiters
└─ failure or panic → preserve record, release lease, wake waiters

Benchmarks

Cold starknet_traceTransaction at 100 VUs

Broad corpus sample size: 1,000
Default size: 200

Block corpus Target Throughput Δ Baseline avg PR avg Latency Δ CPU/request Δ
Broad Beginning +626.67% 7,433.0 ms 1,000.2 ms -86.54% -86.72%
Broad Middle +86.93% 7,512.1 ms 4,013.8 ms -46.57% -46.82%
Broad End -1.87% 7,486.8 ms 7,628.2 ms +1.89% +1.80%
1 transaction Beginning +8.38% 724.5 ms 696.5 ms -3.87% -2.45%
1 transaction Middle -1.03% 710.7 ms 721.1 ms +1.47% +1.04%
1 transaction End -4.00% 681.6 ms 730.0 ms +7.10% +5.61%
10 transactions Beginning +608.77% 7,095.1 ms 805.0 ms -88.65% -86.72%
10 transactions Middle +88.07% 7,100.6 ms 3,693.2 ms -47.99% -47.61%
10 transactions End -0.28% 7,185.9 ms 7,213.5 ms +0.38% +0.63%
>20 transactions Beginning +1,557.25% 14,864.3 ms 778.8 ms -94.76% -94.57%
>20 transactions Middle +86.93% 14,855.5 ms 8,088.9 ms -45.55% -45.37%
>20 transactions End +0.30% 14,971.4 ms 14,973.1 ms +0.01% -0.74%

Interleaved traceTransaction -> traceBlockTransactions

Sample size: 200

Block corpus VUs Throughput Δ Baseline avg PR avg Latency Δ CPU/pair Δ
Broad 1 -4.18% 449.8 ms 469.4 ms +4.36% +5.42%
Broad 50 -2.91% 3,380.7 ms 3,531.7 ms +4.46% +2.11%
1 transaction 1 -0.82% 46.5 ms 47.0 ms +0.93% +1.39%
1 transaction 50 -7.33% 392.5 ms 412.7 ms +5.15% +4.29%
10 transactions 1 -2.93% 591.6 ms 609.4 ms +3.01% +3.81%
10 transactions 50 -0.26% 4,264.5 ms 4,418.0 ms +3.60% +1.09%
>20 transactions 1 -0.65% 1,086.6 ms 1,093.7 ms +0.65% +1.14%
>20 transactions 50 +1.67% 8,646.3 ms 8,971.4 ms +3.76% +0.67%

PR Type

Enhancement, Tests


Description

  • Introduce progressive trace caching in RPC to reuse successful tracing prefixes and execute only the missing transaction suffixes.

  • Implement state resumption and execution error offsetting for partial block tracing.

  • Integrate targeted subset trace resolutions in RPC versions 8, 9, and 10 trace handlers.

  • Add comprehensive test suites for caching behaviors, wait cancellations, and error offsets.


File Walkthrough

Relevant files
Tests
shared_trace_test.go
Update shared trace tests and add progressive tracing checks

rpc/shared_trace_test.go

  • Update mock trace execution expectations to accommodate trace
    extensions and initial reads.
  • Add TestProgressiveTraceEachVersionExtends to test progressive tracing
    behaviors across RPC versions 8, 9, and 10.
+106/-9 
progressive_trace_test.go
Add test suite for progressive trace caching in v10           

rpc/v10/progressive_trace_test.go

  • Add comprehensive tests for progressive trace executions covering
    error offset retentions.
  • Validate wait cancellation, malformed result omissions, and initial
    reads replay interactions.
  • Test checkpoint read failures and target identity before execution.
+383/-0 
trace_test.go
Fix mock trace initialization in v10 trace tests                 

rpc/v10/trace_test.go

  • Fix mock setups by initializing StateDiff to avoid nil pointer issues
    during tests.
+2/-2     
Enhancement
trace.go
Integrate progressive trace caching in v10 handlers           

rpc/v10/trace.go

  • Update traceTransactionsWithState to offset VM execution errors using
    errorIndexOffset.
  • Enhance traceFinalisedBlock to accept a target transaction and
    conditionally skip previously cached trace prefixes.
  • Integrate tracecache.PlanRange to combine pre-cached trace prefixes
    with new execution results.
+35/-4   
trace.go
Integrate progressive trace caching in v8 handlers             

rpc/v8/trace.go

  • Adapt traceBlockTransactions to leverage progressive trace cache and
    target-specific resolutions.
  • Forward tracecache.Range details and errorIndexOffset to support
    targeted state resumptions and combined execution outputs.
+59/-9   
trace.go
Integrate progressive trace caching in v9 handlers             

rpc/v9/trace.go

  • Refactor traceFinalisedBlock to match partial traces against specific
    target queries.
  • Apply plan.ResumeState for targeted state recovery and combine cached
    outcomes with VM suffix runs.
  • Route errorIndexOffset across trace execution paths to properly adjust
    potential transaction errors.
+41/-5   

@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.46%. Comparing base (5132f3f) to head (b5bbbac).

Files with missing lines Patch % Lines
rpc/v8/trace.go 82.92% 7 Missing ⚠️
rpc/v9/trace.go 75.86% 7 Missing ⚠️
rpc/v10/trace.go 92.30% 2 Missing ⚠️
Additional details and impacted files
@@                             Coverage Diff                             @@
##           danielntmd/progressive-trace-primitives    #4080      +/-   ##
===========================================================================
+ Coverage                                    79.43%   79.46%   +0.03%     
===========================================================================
  Files                                          470      470              
  Lines                                        36212    36293      +81     
===========================================================================
+ Hits                                         28765    28841      +76     
- Misses                                        7438     7443       +5     
  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 changed the title perf(rpc): resume shared trace prefixes in RPC workflows perf(rpc): introduce progressive trace caching Sep 15, 2026
@danielntmd
danielntmd force-pushed the danielntmd/progressive-trace-integration branch from 441385b to b5bbbac Compare September 15, 2026 22:47
@danielntmd
danielntmd marked this pull request as ready for review September 15, 2026 22:47
@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Review in progress

  • Gather context (PR body, diff, CLAUDE.md)
  • Review rpc/v8/trace.go, rpc/v9/trace.go, rpc/v10/trace.go progressive trace logic
  • Review concurrency safety of lease/wake mechanism
  • Review test coverage (shared_trace_test.go, progressive_trace_test.go)
  • Post inline comments for any issues found
  • Post final summary

View job run

@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
🏅 Score: 62
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Nil plan dereference

traceBlockTransactionWithVM unconditionally calls plan.ResumeState guarded by if plan != nil, but later transactions = transactions[plan.Start:plan.End] and tracecache.OffsetExecutionError(err, plan.Start) and traceTransactionsWithState(..., plan.Start) are called without checking plan != nil first for the pending-block path where plan remains nil (isPending branch skips plan assignment). This will panic with a nil pointer dereference when tracing a pending block, since plan is only set if !isPending.

func (h *Handler) traceBlockTransactionWithVM(block *core.Block, plan *tracecache.Range) (
	*tracecache.BlockTrace, http.Header, *jsonrpc.Error,
) {
	httpHeader := defaultExecutionHeader()
	transactions := block.Transactions
	if plan != nil {
		transactions = transactions[plan.Start:plan.End]
	}
	state, closer, err := h.bcReader.StateAtBlockHash(block.ParentHash)
	if err != nil {
		return nil, httpHeader, rpccore.ErrBlockNotFound
	}
	defer h.callAndLogErr(closer, "Failed to close state in traceBlockTransactions")

	var (
		headState       core.StateReader
		headStateCloser blockchain.StateCloser
	)

	headState, headStateCloser, err = h.bcReader.HeadState()
	if err != nil {
		return nil, httpHeader, jsonrpc.Err(jsonrpc.InternalError, err.Error())
	}
	defer h.callAndLogErr(headStateCloser, "Failed to close head state in traceBlockTransactions")

	if plan != nil {
		state, err = plan.ResumeState(state, headState, block.Number)
		if err != nil {
			return nil, httpHeader, jsonrpc.Err(jsonrpc.InternalError, err.Error())
		}
	}

	var classes []core.ClassDefinition
	paidFeesOnL1 := []*felt.Felt{}

	for _, transaction := range transactions {
		switch tx := transaction.(type) {
		case *core.DeclareTransaction:
			class, stateErr := headState.Class(tx.ClassHash)
			if stateErr != nil {
				return nil, httpHeader, jsonrpc.Err(jsonrpc.InternalError, stateErr.Error())
			}
			classes = append(classes, class.Class)
		case *core.L1HandlerTransaction:
			// TODO (granza): use real L1 message fee.
			paidFeesOnL1 = append(paidFeesOnL1, &felt.One)
		}
	}

	blockHashToBeRevealed, err := h.getRevealedBlockHash(block.Number)
	if err != nil {
		return nil, httpHeader, rpccore.ErrInternal.CloneWithData(err)
	}

	header := block.Header
	blockInfo := vm.BlockInfo{
		Header:                header,
		BlockHashToBeRevealed: blockHashToBeRevealed,
	}

	executionResult, err := h.vm.Trace(transactions, classes, paidFeesOnL1,
		&blockInfo, state, vm.TraceOptions{})

	if plan != nil {
		err = tracecache.OffsetExecutionError(err, plan.Start)
	}
Nil plan dereference on feeder path

In traceFinalisedBlock, plan is only computed inside the else branch (non-feeder path). When fetchFromFeederGW is true, plan stays nil, but the code after the if/else calls plan.Combine(traces) only inside the else block, so that part is fine, but traceBlockWithVM unconditionally dereferences plan.Start/plan.End — this function is only called from the else branch so it should be fine. However, verify that plan cannot be nil in traceBlockWithVM since it directly does transactions[plan.Start:plan.End] without a nil check, differing from v8/v10 pending-block handling; if any future caller passes nil plan this will panic. Confirm all call sites always supply a non-nil plan.

// traceBlockWithVM traces a block using the local VM.
func (h *Handler) traceBlockWithVM(
	header *core.Header,
	transactions []core.Transaction,
	plan *tracecache.Range,
) (*tracecache.BlockTrace, http.Header, *jsonrpc.Error) {
	transactions = transactions[plan.Start:plan.End]
Unconditional plan dereference

traceBlockWithVM in rpc/v10 does transactions[plan.Start:plan.End] without checking plan for nil, unlike v8's pending-block path which passes a nil plan. If traceBlockWithVM is ever invoked with a nil plan (e.g. future refactor or missed initialization), this will panic. Confirm plan is always non-nil for all call sites in this file.

func (h *Handler) traceBlockWithVM(
	header *core.Header,
	transactions []core.Transaction,
	returnInitialReads bool,
	plan *tracecache.Range,
) (*tracecache.BlockTrace, http.Header, *jsonrpc.Error) {
	transactions = transactions[plan.Start:plan.End]

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