Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 14 additions & 0 deletions admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ curl localhost:9002/admin/run_command -H 'Content-Type: application/json' -d '{"
curl localhost:9002/admin/run_command -H 'Content-Type: application/json' -d '{"commandName": "set-config", "data": {"profiler-trigger": "1m"}}'
```

### Ledger service profiler
The standalone ledger service (`cmd/ledger`) exposes the same profiler configuration through its admin server (default port `9003`). It also registers the `net/http/pprof` endpoints on the admin server for on-demand profiling.

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.

nit: cmd/ledger/README.md documents the admin commands and flags but was not updated with the new profiler flags and the get-config/set-config commands.


```
# Enable the auto-profiler
curl localhost:9003/admin/run_command -H 'Content-Type: application/json' -d '{"commandName": "set-config", "data": {"profiler-enabled": true}}'

# Trigger a profile run
curl localhost:9003/admin/run_command -H 'Content-Type: application/json' -d '{"commandName": "set-config", "data": {"profiler-trigger": "1m"}}'

# Get a heap profile via pprof
curl -o heap.prof localhost:9003/debug/pprof/heap
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

### Set a stop height
```
curl localhost:9002/admin/run_command -H 'Content-Type: application/json' -d '{"commandName": "stop-at-height", "data": { "height": 1111, "crash": false }}'
Expand Down
61 changes: 59 additions & 2 deletions cmd/ledger/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/http/pprof"

"github.com/rs/zerolog"
"go.uber.org/atomic"

"github.com/onflow/flow-go/module/updatable_configs"
)

// adminRequest represents the JSON request body for admin commands.
Expand All @@ -29,19 +32,29 @@ type adminResponse struct {
type adminHandler struct {
logger zerolog.Logger
triggerCheckpoint *atomic.Bool
configManager *updatable_configs.Manager
commands []string
}

// newAdminHandler creates a new admin HTTP handler.
func newAdminHandler(logger zerolog.Logger, triggerCheckpoint *atomic.Bool) http.Handler {
func newAdminHandler(logger zerolog.Logger, triggerCheckpoint *atomic.Bool, configManager *updatable_configs.Manager) http.Handler {
h := &adminHandler{
logger: logger.With().Str("component", "admin").Logger(),
triggerCheckpoint: triggerCheckpoint,
commands: []string{"ping", "list-commands", "trigger-checkpoint"},
configManager: configManager,
commands: []string{"ping", "list-commands", "trigger-checkpoint", "get-config", "set-config"},
}

mux := http.NewServeMux()
mux.HandleFunc("/admin/run_command", h.handleCommand)

// Register pprof handlers for profiling (CPU, heap, goroutine, etc.)
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

return mux
}

Expand Down Expand Up @@ -78,6 +91,50 @@ func (h *adminHandler) handleCommand(w http.ResponseWriter, r *http.Request) {
result = "checkpoint already triggered"
}

case "get-config":
var configName string
if err := json.Unmarshal(req.Data, &configName); err != nil {
h.writeError(w, http.StatusBadRequest, fmt.Sprintf("get-config data must be a string config name: %v", err))
return
}
field, ok := h.configManager.GetField(configName)
if !ok {
h.writeError(w, http.StatusBadRequest, fmt.Sprintf("unknown config field: %s", configName))
return
}
result = field.Get()

case "set-config":
var data map[string]any
if err := json.Unmarshal(req.Data, &data); err != nil {
h.writeError(w, http.StatusBadRequest, fmt.Sprintf("set-config data must be a JSON object: %v", err))
return
}
if len(data) != 1 {
h.writeError(w, http.StatusBadRequest, fmt.Sprintf("set-config data must have exactly one entry, got %d", len(data)))
return
}
var configName string
var configValue any
for k, v := range data {
configName = k
configValue = v
}
field, ok := h.configManager.GetField(configName)
if !ok {
h.writeError(w, http.StatusBadRequest, fmt.Sprintf("unknown config field: %s", configName))
return
}
oldValue := field.Get()
if err := field.Set(configValue); err != 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.

Field.Set can fail with non-validation errors, e.g. TriggerRun returns "profiling is already in progress", which is server state, not a bad request. Check updatable_configs.IsValidationError(err) and return 400 only for validation errors, 500 otherwise, matching admin/commands/common/set_config.go.

h.writeError(w, http.StatusBadRequest, fmt.Sprintf("failed to set config %s: %v", configName, err))
return
}
result = map[string]any{
"oldValue": oldValue,
"newValue": configValue,
}

default:
h.writeError(w, http.StatusBadRequest, fmt.Sprintf("unknown command: %s", req.CommandName))
return
Expand Down
74 changes: 73 additions & 1 deletion cmd/ledger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
Expand All @@ -23,6 +24,8 @@ import (
"github.com/onflow/flow-go/ledger/remote"
"github.com/onflow/flow-go/module/irrecoverable"
"github.com/onflow/flow-go/module/metrics"
"github.com/onflow/flow-go/module/profiler"
"github.com/onflow/flow-go/module/updatable_configs"
)

var (
Expand All @@ -37,6 +40,12 @@ var (
logLevel = flag.String("loglevel", "info", "Log level (panic, fatal, error, warn, info, debug)")
maxRequestSize = flag.Uint("max-request-size", 1<<30, "Maximum request message size in bytes (default: 1 GiB)")
maxResponseSize = flag.Uint("max-response-size", 1<<30, "Maximum response message size in bytes (default: 1 GiB)")

profilerEnabled = flag.Bool("profiler-enabled", false, "Whether to enable the auto-profiler")
profilerDir = flag.String("profiler-dir", "profiler", "Directory to create auto-profiler profiles")
profilerInterval = flag.Duration("profiler-interval", 15*time.Minute, "Interval between auto-profiler runs")
profilerDuration = flag.Duration("profiler-duration", 10*time.Second, "Duration of each auto-profiler run")
profileUploaderEnabled = flag.Bool("profile-uploader-enabled", false, "Whether to upload profiles to a remote uploader (disabled for ledger service)")

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.

nit: this flag's only effect is a warning that it is ignored.

)

func main() {
Expand All @@ -60,6 +69,68 @@ func main() {
Str("service", "ledger").
Logger()

// Initialize updatable config manager and auto-profiler.
// The profiler is configured via admin get-config/set-config commands.
configManager := updatable_configs.NewManager()

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.

nit: profiler.New creates the profile directory and starts a goroutine before the required-address validation below can fatal. Move this block after that validation so misconfigured invocations have no side effects.

profilerConfig := profiler.ProfilerConfig{
Enabled: *profilerEnabled,
UploaderEnabled: false, // ledger service does not support remote profile upload
Dir: *profilerDir,
Interval: *profilerInterval,
Duration: *profilerDuration,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
if *profileUploaderEnabled {
logger.Warn().Msg("profile-uploader-enabled is not supported by the ledger service, ignoring")
}

autoProfiler, err := profiler.New(logger, &profiler.NoopUploader{}, profilerConfig)
if err != nil {
logger.Fatal().Err(err).Msg("failed to create auto-profiler")
}

err = configManager.RegisterBoolConfig("profiler-enabled", autoProfiler.Enabled, autoProfiler.SetEnabled)
if err != nil {
logger.Fatal().Err(err).Msg("failed to register profiler-enabled config")
}
err = configManager.RegisterDurationConfig(
"profiler-trigger",
func() time.Duration { return profilerConfig.Duration },
func(d time.Duration) error { return autoProfiler.TriggerRun(d) },
)
if err != nil {
logger.Fatal().Err(err).Msg("failed to register profiler-trigger config")
}
err = configManager.RegisterUintConfig(
"profiler-set-mem-profile-rate",
func() uint { return uint(runtime.MemProfileRate) },
func(r uint) error { runtime.MemProfileRate = int(r); return nil },
)
if err != nil {
logger.Fatal().Err(err).Msg("failed to register profiler-set-mem-profile-rate config")
}
currentBlockRate := new(uint)
err = configManager.RegisterUintConfig(
"profiler-set-block-profile-rate",
func() uint { return *currentBlockRate },
func(r uint) error { currentBlockRate = &r; runtime.SetBlockProfileRate(int(r)); return nil },
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if err != nil {
logger.Fatal().Err(err).Msg("failed to register profiler-set-block-profile-rate config")
}
err = configManager.RegisterUintConfig(
"profiler-set-mutex-profile-fraction",
func() uint { return uint(runtime.SetMutexProfileFraction(-1)) },
func(r uint) error { _ = runtime.SetMutexProfileFraction(int(r)); return nil },
)
if err != nil {
logger.Fatal().Err(err).Msg("failed to register profiler-set-mutex-profile-fraction config")
}

go func() {
<-autoProfiler.Ready()
logger.Info().Bool("enabled", autoProfiler.Enabled()).Msg("auto-profiler ready")
}()

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.

nit: autoProfiler.Done() is never awaited at shutdown, so an in-flight profile run is truncated at exit. The goroutine is also unnecessary: with no readiness checks, Ready() closes almost immediately.


// Validate that at least one address is provided
if *ledgerServiceTCP == "" && *ledgerServiceSocket == "" {
logger.Fatal().Msg("at least one of --ledger-service-tcp or --ledger-service-socket must be provided")
Expand All @@ -72,6 +143,7 @@ func main() {
Str("admin_addr", *adminAddr).
Uint("metrics_port", *metricsPort).
Int("mtrie_cache_size", *mtrieCacheSize).
Bool("profiler_enabled", *profilerEnabled).
Msg("starting ledger service")

// Create trigger for manual checkpointing (used by admin command)
Expand Down Expand Up @@ -229,7 +301,7 @@ func main() {
// This is a lightweight HTTP-only server (no gRPC proxy layer)
var adminServer *http.Server
if *adminAddr != "" {
adminHandler := newAdminHandler(logger, triggerCheckpointOnNextSegmentFinish)
adminHandler := newAdminHandler(logger, triggerCheckpointOnNextSegmentFinish, configManager)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
adminServer = &http.Server{
Addr: *adminAddr,
Handler: adminHandler,
Expand Down
Loading