Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions rpc/v10/adapt_trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,18 +247,11 @@
}
}

// adaptVMInitialReads requires non-nil VM output; callers decide how missing reads are handled.
func adaptVMInitialReads(vmInitialReads *vm.InitialReads) InitialReads {
if vmInitialReads == nil {
return InitialReads{
Storage: []StorageEntry{},
Nonces: []NonceEntry{},
ClassHashes: []ClassHashEntry{},
DeclaredContracts: []DeclaredContractEntry{},
}
}

storage := make([]StorageEntry, len(vmInitialReads.Storage))
for i, s := range vmInitialReads.Storage {
for i := range vmInitialReads.Storage {
s := &vmInitialReads.Storage[i]
storage[i] = StorageEntry{
ContractAddress: s.ContractAddress,
Key: s.Key,
Expand All @@ -267,23 +260,26 @@
}

nonces := make([]NonceEntry, len(vmInitialReads.Nonces))
for i, n := range vmInitialReads.Nonces {
for i := range vmInitialReads.Nonces {
n := &vmInitialReads.Nonces[i]

Check warning on line 264 in rpc/v10/adapt_trace.go

View check run for this annotation

Codecov / codecov/patch

rpc/v10/adapt_trace.go#L264

Added line #L264 was not covered by tests
nonces[i] = NonceEntry{
ContractAddress: n.ContractAddress,
Nonce: n.Nonce,
}
}

classHashes := make([]ClassHashEntry, len(vmInitialReads.ClassHashes))
for i, ch := range vmInitialReads.ClassHashes {
for i := range vmInitialReads.ClassHashes {
ch := &vmInitialReads.ClassHashes[i]

Check warning on line 273 in rpc/v10/adapt_trace.go

View check run for this annotation

Codecov / codecov/patch

rpc/v10/adapt_trace.go#L273

Added line #L273 was not covered by tests
classHashes[i] = ClassHashEntry{
ContractAddress: ch.ContractAddress,
ClassHash: ch.ClassHash,
}
}

declaredContracts := make([]DeclaredContractEntry, len(vmInitialReads.DeclaredContracts))
for i, dc := range vmInitialReads.DeclaredContracts {
for i := range vmInitialReads.DeclaredContracts {
dc := &vmInitialReads.DeclaredContracts[i]

Check warning on line 282 in rpc/v10/adapt_trace.go

View check run for this annotation

Codecov / codecov/patch

rpc/v10/adapt_trace.go#L282

Added line #L282 was not covered by tests
declaredContracts[i] = DeclaredContractEntry{
ClassHash: dc.ClassHash,
IsDeclared: dc.IsDeclared,
Expand Down
13 changes: 4 additions & 9 deletions rpc/v10/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/NethermindEth/juno/blockchain"
"github.com/NethermindEth/juno/clients/feeder"
"github.com/NethermindEth/juno/core"
"github.com/NethermindEth/juno/core/felt"
"github.com/NethermindEth/juno/core/pending"
"github.com/NethermindEth/juno/feed"
"github.com/NethermindEth/juno/jsonrpc"
Expand All @@ -20,7 +19,6 @@ import (
"github.com/NethermindEth/juno/starknet/compiler"
"github.com/NethermindEth/juno/sync"
"github.com/NethermindEth/juno/utils/log"
"github.com/NethermindEth/juno/utils/lru"
"github.com/NethermindEth/juno/vm"
"github.com/sourcegraph/conc"
)
Expand All @@ -43,8 +41,8 @@ type Handler struct {
idgen func() string
subscriptions stdsync.Map // map[string]*subscription

blockTraceCache *lru.Cache[felt.Felt, TraceBlockTransactionsResponse]
// todo(rdr): Can this cache be genericified and can it be applied to the `blockTraceCache`
blockTraceCache *blockTraceCache
// submittedTransactionsCache is a TTL membership set, unlike the coordinated block trace LRU.
submittedTransactionsCache *rpccore.TransactionCache

filterLimit uint
Expand Down Expand Up @@ -84,11 +82,8 @@ func New(
preConfirmedFeed: feed.New[*pending.PreConfirmed](),
l1Heads: feed.New[*core.L1Head](),

blockTraceCache: lru.New[
felt.Felt,
TraceBlockTransactionsResponse,
](rpccore.TraceCacheSize),
filterLimit: math.MaxUint,
blockTraceCache: newBlockTraceCache(rpccore.TraceCacheSize),
filterLimit: math.MaxUint,
}
}

Expand Down
9 changes: 9 additions & 0 deletions rpc/v10/simulation.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ type InitialReads struct {
DeclaredContracts []DeclaredContractEntry `json:"declared_contracts"`
}

func emptyInitialReads() *InitialReads {
return &InitialReads{
Storage: []StorageEntry{},
Nonces: []NonceEntry{},
ClassHashes: []ClassHashEntry{},
DeclaredContracts: []DeclaredContractEntry{},
}
}
Comment on lines +163 to +170

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not return by value? (reduce the extra alloc)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, being used only twice, you could consider inlining it. Up to you!

@danielntmd danielntmd Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why not return by value? (reduce the extra alloc)

rpc/v10/simulation.go:121

type TraceBlockTransactionsResponse struct {
      Traces       []TracedBlockTransaction `json:"traces"`
      InitialReads *InitialReads            `json:"initial_reads"`
}

InitialReads is a pointer, we could change the field to a value, but then we would need to track absence separately since we currently use nil.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can return by value and still treated as a pointer via the use of new(...).

Wherever you call emptyInitialReads() will become new(emptyInitialReads())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here are the suggested code shapes and their respective escaped diagnostics and benchmarks.

// Current:
func emptyInitialReads() *InitialReads {
    return &InitialReads{ /* four empty slices */ }
}

response.InitialReads = emptyInitialReads()
// Proposed value-returning helper:
func emptyInitialReads() InitialReads {
    return InitialReads{ /* same four empty slices */ }
}

// Explicit address:
reads := emptyInitialReads()
response.InitialReads = &reads
// Or the proposed new(...) construction:
response.InitialReads = new(emptyInitialReads())
Shape B/op allocs/op
Current pointer return 96 1
Value return, then &reads 96 1
Value return, then new(...) 96 1

The problem is that both constructions escape through the response returned by traceFinalisedBlock, so returning by value and wrapping it in new retains allocation. We can avoid this allocation for the empty block path when no reads are requested by leaving InitialReads as nil, as such:

response := TraceBlockTransactionsResponse{
    Traces: []TracedBlockTransaction{},
}
if returnInitialReads {
    response.InitialReads = emptyInitialReads()
}
return response, defaultExecutionHeader(), nil

This removes the allocation for unflagged empty-block requests while preserving the external response format. The requested-reads path still allocates, as it did before this PR.


type BroadcastedTransactionInputs = rpccore.LimitSlice[
BroadcastedTransaction,
rpccore.SimulationLimit,
Expand Down
Loading
Loading