From 684aeb3314932bb49f49b2b200b5a35776a60d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 10 Oct 2025 15:02:15 -0700 Subject: [PATCH] add support for collecting computation profiles --- cmd/emulator/start/start.go | 2 ++ emulator/blockchain.go | 53 +++++++++++++++++++++++++++----- emulator/coverage_report_test.go | 4 +-- emulator/emulator.go | 6 ++++ emulator/mocks/emulator.go | 26 ++++++++++++++++ server/server.go | 9 ++++++ server/utils/emulator.go | 39 ++++++++++++++++++++--- 7 files changed, 124 insertions(+), 15 deletions(-) diff --git a/cmd/emulator/start/start.go b/cmd/emulator/start/start.go index e25faef4..7caa5105 100644 --- a/cmd/emulator/start/start.go +++ b/cmd/emulator/start/start.go @@ -74,6 +74,7 @@ type Config struct { RedisURL string `default:"" flag:"redis-url" info:"redis-server URL for persisting redis storage backend ( redis://[[username:]password@]host[:port][/database] ) "` SqliteURL string `default:"" flag:"sqlite-url" info:"sqlite db URL for persisting sqlite storage backend "` CoverageReportingEnabled bool `default:"false" flag:"coverage-reporting" info:"enable Cadence code coverage reporting"` + ComputationProfilingEnabled bool `default:"false" flag:"computation-profiling" info:"enable Cadence computation profiling"` LegacyContractUpgradeEnabled bool `default:"false" flag:"legacy-upgrade" info:"enable Cadence legacy contract upgrade"` StartBlockHeight uint64 `default:"0" flag:"start-block-height" info:"block height to start the emulator at. only valid when forking Mainnet or Testnet"` RPCHost string `default:"" flag:"rpc-host" info:"rpc host to query when forking Mainnet or Testnet"` @@ -217,6 +218,7 @@ func Cmd(config StartConfig) *cobra.Command { ContractRemovalEnabled: conf.ContractRemovalEnabled, SqliteURL: conf.SqliteURL, CoverageReportingEnabled: conf.CoverageReportingEnabled, + ComputationProfilingEnabled: conf.ComputationProfilingEnabled, StartBlockHeight: conf.StartBlockHeight, RPCHost: conf.RPCHost, CheckpointPath: conf.CheckpointPath, diff --git a/emulator/blockchain.go b/emulator/blockchain.go index 41425191..910aa5e2 100644 --- a/emulator/blockchain.go +++ b/emulator/blockchain.go @@ -315,6 +315,15 @@ func WithComputationReporting(enabled bool) Option { } } +// WithComputationProfile injects a ComputationProfile to collect coverage information. +// +// The default is nil. +func WithComputationProfile(computationProfile *runtime.ComputationProfile) Option { + return func(c *config) { + c.ComputationProfile = computationProfile + } +} + func WithScheduledTransactions(enabled bool) Option { return func(c *config) { c.ScheduledTransactionsEnabled = enabled @@ -402,6 +411,7 @@ type config struct { TransactionValidationEnabled bool ChainID flowgo.ChainID CoverageReport *runtime.CoverageReport + ComputationProfile *runtime.ComputationProfile AutoMine bool Contracts []ContractDescription ComputationReportingEnabled bool @@ -627,17 +637,29 @@ var _ environment.EntropyProvider = &blockHashEntropyProvider{} func configureFVM(blockchain *Blockchain, conf config, blocks *blocks) (*fvm.VirtualMachine, fvm.Context, error) { vm := fvm.NewVirtualMachine() - cadenceLogger := conf.Logger.Hook(CadenceHook{MainLogger: &conf.ServerLogger}).Level(zerolog.DebugLevel) + cadenceLogger := conf.Logger. + Hook(CadenceHook{ + MainLogger: &conf.ServerLogger, + }). + Level(zerolog.DebugLevel) + + if conf.ExecutionEffortWeights != nil && + conf.ComputationProfile != nil { + + conf.ComputationProfile. + WithComputationWeights(conf.ExecutionEffortWeights) + } runtimeConfig := runtime.Config{ - Debugger: blockchain.debugger, - CoverageReport: conf.CoverageReport, + Debugger: blockchain.debugger, + CoverageReport: conf.CoverageReport, + ComputationProfile: conf.ComputationProfile, } rt := runtime.NewRuntime(runtimeConfig) customRuntimePool := reusableRuntime.NewCustomReusableCadenceRuntimePool( 1, runtimeConfig, - func(config runtime.Config) runtime.Runtime { + func(_ runtime.Config) runtime.Runtime { return rt }, ) @@ -776,6 +798,10 @@ func bootstrapLedger( fvm.ProcedureOutput, error, ) { + if conf.ComputationProfile != nil { + conf.ComputationProfile.Reset() + } + accountKey := conf.GetServiceKey().AccountKey() publicKey, _ := crypto.DecodePublicKey( accountKey.SigAlgo, @@ -808,7 +834,12 @@ func bootstrapLedger( return executionSnapshot, output, nil } -func configureBootstrapProcedure(conf config, flowAccountKey flowgo.AccountPublicKey, supply cadence.UFix64) *fvm.BootstrapProcedure { +func configureBootstrapProcedure( + conf config, + flowAccountKey flowgo.AccountPublicKey, + supply cadence.UFix64, +) *fvm.BootstrapProcedure { + options := make([]fvm.BootstrapProcedureOption, 0) options = append(options, fvm.WithInitialTokenSupply(supply), @@ -1712,12 +1743,20 @@ func (b *Blockchain) CoverageReport() *runtime.CoverageReport { return b.conf.CoverageReport } +func (b *Blockchain) ResetCoverageReport() { + b.conf.CoverageReport.Reset() +} + func (b *Blockchain) ComputationReport() *ComputationReport { return b.computationReport } -func (b *Blockchain) ResetCoverageReport() { - b.conf.CoverageReport.Reset() +func (b *Blockchain) ComputationProfile() *runtime.ComputationProfile { + return b.conf.ComputationProfile +} + +func (b *Blockchain) ResetComputationProfile() { + b.conf.ComputationProfile.Reset() } func (b *Blockchain) GetTransactionsByBlockID(blockID flowgo.Identifier) ([]*flowgo.TransactionBody, error) { diff --git a/emulator/coverage_report_test.go b/emulator/coverage_report_test.go index 3391ed77..1104287f 100644 --- a/emulator/coverage_report_test.go +++ b/emulator/coverage_report_test.go @@ -80,10 +80,8 @@ func TestCoverageReport(t *testing.T) { require.NoError(t, err) AssertTransactionSucceeded(t, txResult) - address, err := common.HexToAddress(counterAddress.Hex()) - require.NoError(t, err) location := common.AddressLocation{ - Address: address, + Address: common.MustBytesToAddress(counterAddress.Bytes()), Name: "Counting", } coverage := coverageReport.Coverage[location] diff --git a/emulator/emulator.go b/emulator/emulator.go index 6d9083c2..b9e45168 100644 --- a/emulator/emulator.go +++ b/emulator/emulator.go @@ -104,6 +104,11 @@ type ComputationReportCapable interface { ComputationReport() *ComputationReport } +type ComputationProfileCapable interface { + ComputationProfile() *runtime.ComputationProfile + ResetComputationProfile() +} + type DebuggingCapable interface { StartDebugger() *interpreter.Debugger EndDebugging() @@ -182,6 +187,7 @@ type Emulator interface { CoverageReportCapable ComputationReportCapable + ComputationProfileCapable DebuggingCapable SnapshotCapable RollbackCapable diff --git a/emulator/mocks/emulator.go b/emulator/mocks/emulator.go index f809631e..6212854b 100644 --- a/emulator/mocks/emulator.go +++ b/emulator/mocks/emulator.go @@ -75,6 +75,20 @@ func (mr *MockEmulatorMockRecorder) CommitBlock() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommitBlock", reflect.TypeOf((*MockEmulator)(nil).CommitBlock)) } +// ComputationProfile mocks base method. +func (m *MockEmulator) ComputationProfile() *runtime.ComputationProfile { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ComputationProfile") + ret0, _ := ret[0].(*runtime.ComputationProfile) + return ret0 +} + +// ComputationProfile indicates an expected call of ComputationProfile. +func (mr *MockEmulatorMockRecorder) ComputationProfile() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ComputationProfile", reflect.TypeOf((*MockEmulator)(nil).ComputationProfile)) +} + // ComputationReport mocks base method. func (m *MockEmulator) ComputationReport() *emulator.ComputationReport { m.ctrl.T.Helper() @@ -555,6 +569,18 @@ func (mr *MockEmulatorMockRecorder) Ping() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockEmulator)(nil).Ping)) } +// ResetComputationProfile mocks base method. +func (m *MockEmulator) ResetComputationProfile() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ResetComputationProfile") +} + +// ResetComputationProfile indicates an expected call of ResetComputationProfile. +func (mr *MockEmulatorMockRecorder) ResetComputationProfile() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetComputationProfile", reflect.TypeOf((*MockEmulator)(nil).ResetComputationProfile)) +} + // ResetCoverageReport mocks base method. func (m *MockEmulator) ResetCoverageReport() { m.ctrl.T.Helper() diff --git a/server/server.go b/server/server.go index e30ba4cb..10cd32ad 100644 --- a/server/server.go +++ b/server/server.go @@ -137,6 +137,8 @@ type Config struct { SqliteURL string // CoverageReportingEnabled enables/disables Cadence code coverage reporting. CoverageReportingEnabled bool + // ComputationProfilingEnabled enables/disables Cadence computation profiling. + ComputationProfilingEnabled bool // RPCHost is the address of the access node to use when using a forked network. RPCHost string // StartBlockHeight is the height at which to start the emulator. @@ -463,6 +465,13 @@ func configureBlockchain(logger *zerolog.Logger, conf *Config, store storage.Sto ) } + if conf.ComputationProfilingEnabled { + options = append( + options, + emulator.WithComputationProfile(runtime.NewComputationProfile()), + ) + } + if conf.ComputationReportingEnabled { options = append( options, diff --git a/server/utils/emulator.go b/server/utils/emulator.go index f9103ba7..d0a58b1f 100644 --- a/server/utils/emulator.go +++ b/server/utils/emulator.go @@ -25,7 +25,9 @@ import ( "strconv" "github.com/gorilla/mux" + "github.com/onflow/cadence/runtime" flowgo "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-emulator/adapters" "github.com/onflow/flow-emulator/emulator" ) @@ -64,6 +66,9 @@ func NewEmulatorAPIServer(emulator emulator.Emulator, adapter *adapters.AccessAd router.HandleFunc("/emulator/codeCoverage", r.CodeCoverage).Methods("GET") router.HandleFunc("/emulator/codeCoverage/reset", r.ResetCodeCoverage).Methods("PUT") + router.HandleFunc("/emulator/computationProfile", r.ComputationProfile).Methods("GET") + router.HandleFunc("/emulator/computationProfile/reset", r.ResetComputationProfile).Methods("PUT") + router.HandleFunc("/emulator/computationReport", r.ComputationReport).Methods("GET") return r @@ -86,7 +91,7 @@ func (m EmulatorAPIServer) Config(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write(s) } -func (m EmulatorAPIServer) CommitBlock(w http.ResponseWriter, r *http.Request) { +func (m EmulatorAPIServer) CommitBlock(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, err := m.emulator.CommitBlock() if err != nil { @@ -230,7 +235,7 @@ func (m EmulatorAPIServer) SnapshotCreate(w http.ResponseWriter, r *http.Request m.latestBlockResponse(name, w) } -func (m EmulatorAPIServer) CodeCoverage(w http.ResponseWriter, r *http.Request) { +func (m EmulatorAPIServer) CodeCoverage(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(m.emulator.CoverageReport()) @@ -240,7 +245,13 @@ func (m EmulatorAPIServer) CodeCoverage(w http.ResponseWriter, r *http.Request) } } -func (m EmulatorAPIServer) ComputationReport(w http.ResponseWriter, r *http.Request) { +func (m EmulatorAPIServer) ResetCodeCoverage(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + m.emulator.ResetCoverageReport() + w.WriteHeader(http.StatusOK) +} + +func (m EmulatorAPIServer) ComputationReport(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(m.emulator.ComputationReport()) @@ -250,9 +261,27 @@ func (m EmulatorAPIServer) ComputationReport(w http.ResponseWriter, r *http.Requ } } -func (m EmulatorAPIServer) ResetCodeCoverage(w http.ResponseWriter, r *http.Request) { +func (m EmulatorAPIServer) ComputationProfile(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/gzip") + + computationProfile := m.emulator.ComputationProfile() + + pprofProfile, err := runtime.NewPProfExporter(computationProfile).Export() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = pprofProfile.Write(w) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func (m EmulatorAPIServer) ResetComputationProfile(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") - m.emulator.ResetCoverageReport() + m.emulator.ResetComputationProfile() w.WriteHeader(http.StatusOK) }