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
28 changes: 28 additions & 0 deletions cmd/mithril/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ var (

paramArenaSizeMB uint64
borrowedAccountArenaSize uint64
persistProgramCache bool

rpcPort int
)
Expand Down Expand Up @@ -414,6 +415,13 @@ func initConfigAndBindFlags(cmd *cobra.Command) error {
sbpf.UsePool = config.GetBool("development.use_pool")
}

// Read persist_program_cache setting (try tuning.*, fallback to development.*)
if config.IsSet("tuning.persist_program_cache") {
persistProgramCache = config.GetBool("tuning.persist_program_cache")
} else if config.IsSet("development.persist_program_cache") {
persistProgramCache = config.GetBool("development.persist_program_cache")
}

return nil
}

Expand Down Expand Up @@ -610,6 +618,11 @@ func runVerifyRange(c *cobra.Command, args []string) {

mlog.Log.Infof("initializing caches")
accountsDb.InitCaches()
if persistProgramCache {
if err := accountsDb.LoadProgramCache(); err != nil {
mlog.Log.Infof("warning: failed to load program cache: %v", err)
}
}

metricsWriter, metricsWriterCleanup, err := createBufWriter(metricsPath)
if err != nil {
Expand Down Expand Up @@ -638,6 +651,11 @@ func runVerifyRange(c *cobra.Command, args []string) {

replay.ReplayBlocks(ctx, accountsDb, accountsDbDir, manifest, uint64(startSlot), uint64(endSlot), rpcEndpoints[0], ledgerPath, int(txParallelism), false, false, dbgOpts, metricsWriter, rpcServer)
mlog.Log.Infof("done replaying, closing DB")
if persistProgramCache {
if err := accountsDb.SaveProgramCache(); err != nil {
mlog.Log.Infof("warning: failed to save program cache: %v", err)
}
}
accountsDb.CloseDb()
}

Expand Down Expand Up @@ -781,6 +799,11 @@ func runLive(c *cobra.Command, args []string) {

mlog.Log.Infof("initializing caches")
accountsDb.InitCaches()
if persistProgramCache {
if err := accountsDb.LoadProgramCache(); err != nil {
mlog.Log.Infof("warning: failed to load program cache: %v", err)
}
}

metricsWriter, metricsWriterCleanup, err := createBufWriter(metricsPath)
if err != nil {
Expand Down Expand Up @@ -809,6 +832,11 @@ func runLive(c *cobra.Command, args []string) {

replay.ReplayBlocks(ctx, accountsDb, accountsPath, manifest, uint64(startSlot), liveEndSlot, rpcEndpoints[0], ledgerPath, int(txParallelism), true, useOvercast, dbgOpts, metricsWriter, rpcServer)
mlog.Log.Infof("done replaying, closing DB")
if persistProgramCache {
if err := accountsDb.SaveProgramCache(); err != nil {
mlog.Log.Infof("warning: failed to save program cache: %v", err)
}
}
accountsDb.CloseDb()
}

Expand Down
5 changes: 5 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ name = "mithril"
# Enable/disable pool allocator for slices
use_pool = true

# Persist compiled program cache across restarts
# When enabled, compiled SBPF programs are saved to disk on shutdown
# and reloaded on startup, speeding up replay warmup.
persist_program_cache = false

# [tuning.pprof] - CPU/Memory Profiling
[tuning.pprof]
# Port to serve HTTP pprof endpoint (-1 to disable)
Expand Down
2 changes: 1 addition & 1 deletion pkg/accountsdb/accountsdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func (accountsDb *AccountsDb) CloseDb() {

func (accountsDb *AccountsDb) InitCaches() {
var err error
accountsDb.VoteAcctCache, err = otter.MustBuilder[solana.PublicKey, *accounts.Account](5000).
accountsDb.VoteAcctCache, err = otter.MustBuilder[solana.PublicKey, *accounts.Account](2000).
Cost(func(key solana.PublicKey, acct *accounts.Account) uint32 {
return 1
}).
Expand Down
161 changes: 161 additions & 0 deletions pkg/accountsdb/program_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package accountsdb

import (
"encoding/gob"
"fmt"
"os"
"path/filepath"

"github.com/Overclock-Validator/mithril/pkg/mlog"
"github.com/Overclock-Validator/mithril/pkg/sbpf"
"github.com/Overclock-Validator/mithril/pkg/sbpf/sbpfver"
"github.com/gagliardetto/solana-go"
)

const programCacheFilename = "program_cache.gob"

// SerializableProgram is a gob-encodable version of sbpf.Program
type SerializableProgram struct {
RO []byte
Text []uint64 // sbpf.Slot is uint64
TextVA uint64
Entrypoint uint64
Funcs map[uint32]int64
SbpfVersion uint32
}

// SerializableProgramCacheEntry is a gob-encodable version of ProgramCacheEntry
type SerializableProgramCacheEntry struct {
Program SerializableProgram
DeploymentSlot uint64
}

// SerializableProgramCache holds all cache entries for serialization
type SerializableProgramCache struct {
Entries map[solana.PublicKey]SerializableProgramCacheEntry
}

func init() {
// Register types for gob encoding
gob.Register(SerializableProgramCache{})
gob.Register(solana.PublicKey{})
}

// toSerializable converts a ProgramCacheEntry to a serializable form
func toSerializable(entry *ProgramCacheEntry) SerializableProgramCacheEntry {
prog := entry.Program
text := make([]uint64, len(prog.Text))
for i, slot := range prog.Text {
text[i] = uint64(slot)
}

return SerializableProgramCacheEntry{
Program: SerializableProgram{
RO: prog.RO,
Text: text,
TextVA: prog.TextVA,
Entrypoint: prog.Entrypoint,
Funcs: prog.Funcs,
SbpfVersion: prog.SbpfVersion.Version,
},
DeploymentSlot: entry.DeploymentSlot,
}
}

// fromSerializable converts a serializable entry back to ProgramCacheEntry
func fromSerializable(entry SerializableProgramCacheEntry) *ProgramCacheEntry {
text := make([]sbpf.Slot, len(entry.Program.Text))
for i, slot := range entry.Program.Text {
text[i] = sbpf.Slot(slot)
}

return &ProgramCacheEntry{
Program: &sbpf.Program{
RO: entry.Program.RO,
Text: text,
TextVA: entry.Program.TextVA,
Entrypoint: entry.Program.Entrypoint,
Funcs: entry.Program.Funcs,
SbpfVersion: sbpfver.SbpfVersion{Version: entry.Program.SbpfVersion},
},
DeploymentSlot: entry.DeploymentSlot,
}
}

// SaveProgramCache saves the program cache to disk
func (accountsDb *AccountsDb) SaveProgramCache() error {
cacheFile := filepath.Join(filepath.Dir(accountsDb.AcctsDir), programCacheFilename)

// Collect all entries from the otter cache
cache := SerializableProgramCache{
Entries: make(map[solana.PublicKey]SerializableProgramCacheEntry),
}

// Iterate over the cache - otter doesn't have a Range method,
// so we need to track keys separately or use a different approach
// For now, we'll save what we can access
accountsDb.ProgramCache.Range(func(key solana.PublicKey, entry *ProgramCacheEntry) bool {
cache.Entries[key] = toSerializable(entry)
return true
})

if len(cache.Entries) == 0 {
mlog.Log.Debugf("program cache is empty, skipping save")
return nil
}

file, err := os.Create(cacheFile)
if err != nil {
return fmt.Errorf("failed to create program cache file: %w", err)
}
defer file.Close()

encoder := gob.NewEncoder(file)
if err := encoder.Encode(cache); err != nil {
return fmt.Errorf("failed to encode program cache: %w", err)
}

mlog.Log.Infof("saved %d programs to cache file %s", len(cache.Entries), cacheFile)
return nil
}

// LoadProgramCache loads the program cache from disk
func (accountsDb *AccountsDb) LoadProgramCache() error {
cacheFile := filepath.Join(filepath.Dir(accountsDb.AcctsDir), programCacheFilename)

file, err := os.Open(cacheFile)
if err != nil {
if os.IsNotExist(err) {
mlog.Log.Debugf("no program cache file found at %s", cacheFile)
return nil
}
return fmt.Errorf("failed to open program cache file: %w", err)
}
defer file.Close()

var cache SerializableProgramCache
decoder := gob.NewDecoder(file)
if err := decoder.Decode(&cache); err != nil {
mlog.Log.Infof("failed to decode program cache (may be from incompatible version), starting fresh: %v", err)
return nil
}

loaded := 0
for key, entry := range cache.Entries {
accountsDb.ProgramCache.Set(key, fromSerializable(entry))
loaded++
}

mlog.Log.Infof("loaded %d programs from cache file %s", loaded, cacheFile)
return nil
}

// ClearProgramCacheFile removes the program cache file from disk
func (accountsDb *AccountsDb) ClearProgramCacheFile() error {
cacheFile := filepath.Join(filepath.Dir(accountsDb.AcctsDir), programCacheFilename)
err := os.Remove(cacheFile)
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type DevelopmentConfig struct {
ParamArenaSizeMB uint64 `toml:"param_arena_size_mb" mapstructure:"param_arena_size_mb"` // was: param-arena-size-mb
BorrowedAccountArenaSize uint64 `toml:"borrowed_account_arena_size" mapstructure:"borrowed_account_arena_size"` // was: borrowed-account-arena-size
UsePool bool `toml:"use_pool" mapstructure:"use_pool"` // was: use-pool
PersistProgramCache bool `toml:"persist_program_cache" mapstructure:"persist_program_cache"` // Persist compiled program cache across restarts
Pprof PprofConfig `toml:"pprof" mapstructure:"pprof"`
Debug DebugConfig `toml:"debug" mapstructure:"debug"`
}
Expand Down