feat(debug): enable pprof memory profiling in dev/debug mode (#518)

This commit is contained in:
Carlos Florêncio
2026-04-01 23:21:29 +01:00
committed by GitHub
parent fdeef51865
commit b07356b29b
8 changed files with 86 additions and 6 deletions
+10 -4
View File
@@ -25,7 +25,7 @@ BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || powershell -c
LDFLAGS += -X main.Version=$(VERSION) -X main.Commit=$(COMMIT) -X main.BuildTime=$(BUILD_TIME)
.PHONY: all build build-all build-agentctl build-agentctl-linux build-mock-agent clean run dev test test-e2e test-sprites-e2e lint fmt vet help
.PHONY: all build build-all build-agentctl build-agentctl-linux build-mock-agent clean run dev start-debug test test-e2e test-sprites-e2e lint fmt vet help
## Default target
all: build
@@ -71,10 +71,15 @@ run: build
@echo "Running $(BINARY_NAME)..."
./$(BUILD_DIR)/$(BINARY_NAME)
## Run in development mode
## Run in development mode (with pprof enabled)
dev: build
@echo "Running $(BINARY_NAME) in development mode..."
@exec ./$(BUILD_DIR)/$(BINARY_NAME)
@KANDEV_DEBUG_PPROF_ENABLED=true exec ./$(BUILD_DIR)/$(BINARY_NAME)
## Run in debug mode (pprof + debug logging)
start-debug: build
@echo "Running $(BINARY_NAME) in debug mode (pprof + debug logging)..."
@KANDEV_DEBUG_PPROF_ENABLED=true KANDEV_LOG_LEVEL=debug exec ./$(BUILD_DIR)/$(BINARY_NAME)
## Run tests
test:
@@ -149,7 +154,8 @@ help:
@echo " build-mock-agent Build the mock-agent binary for testing"
@echo " clean Remove build artifacts"
@echo " run Build and run the application"
@echo " dev Run in development mode with a local database"
@echo " dev Run in development mode (pprof enabled)"
@echo " start-debug Run in debug mode (pprof + debug logging)"
@echo " test Run tests"
@echo " test-e2e Run E2E adapter tests (real agents, costs money)"
@echo " test-coverage Run tests with coverage report"
+6
View File
@@ -431,6 +431,7 @@ type routeParams struct {
secretStore secrets.SecretStore
mcpConfigSvc *mcpconfig.Service
webInternalURL string
pprofEnabled bool
log *logger.Logger
}
@@ -590,6 +591,11 @@ func registerMCPAndDebugRoutes(
debughandlers.RegisterRoutes(p.router, p.log)
p.log.Debug("Registered Debug handlers (HTTP)")
if p.pprofEnabled {
debughandlers.RegisterPprofRoutes(p.router, p.log)
debughandlers.RegisterMemoryRoute(p.router, p.log)
}
}
// runGracefulShutdown gracefully stops all services and runs cleanups.
+1
View File
@@ -446,6 +446,7 @@ func buildHTTPServer(
secretStore: repos.Secrets,
mcpConfigSvc: mcpconfig.NewService(repos.AgentSettings),
webInternalURL: cfg.Server.WebInternalURL,
pprofEnabled: cfg.Debug.PprofEnabled,
log: log,
})
@@ -27,6 +27,7 @@ type Config struct {
RepositoryDiscovery RepositoryDiscoveryConfig `mapstructure:"repositoryDiscovery"`
Worktree WorktreeConfig `mapstructure:"worktree"`
RepoClone RepoCloneConfig `mapstructure:"repoClone"`
Debug DebugConfig `mapstructure:"debug"`
}
// ResolvedHomeDir returns the Kandev home directory (~/.kandev for local dev).
@@ -153,6 +154,13 @@ type RepoCloneConfig struct {
BasePath string `mapstructure:"basePath"` // Base directory for cloned repos (default: ~/.kandev/repos)
}
// DebugConfig holds debug/profiling configuration.
type DebugConfig struct {
// PprofEnabled enables pprof endpoints at /debug/pprof/ and /api/v1/debug/memory.
// Controlled via KANDEV_DEBUG_PPROF_ENABLED env var. Default: false.
PprofEnabled bool `mapstructure:"pprofEnabled"`
}
// AgentConfig holds agent runtime configuration.
// Note: Runtime selection is now per-task based on executor type, not global.
// The Standalone runtime (agentctl) always runs as a core service.
@@ -282,6 +290,9 @@ func setDefaults(v *viper.Viper) {
// RepoClone defaults
v.SetDefault("repoClone.basePath", "")
// Debug defaults
v.SetDefault("debug.pprofEnabled", false)
}
// DefaultDockerHost returns the platform-appropriate Docker socket path.
@@ -338,6 +349,7 @@ func LoadWithPath(configPath string) (*Config, error) {
_ = v.BindEnv("dataDir", "KANDEV_DATA_DIR")
_ = v.BindEnv("logging.level", "KANDEV_LOG_LEVEL")
_ = v.BindEnv("events.namespace", "KANDEV_EVENTS_NAMESPACE")
_ = v.BindEnv("debug.pprofEnabled", "KANDEV_DEBUG_PPROF_ENABLED")
// Configure config file
v.SetConfigName("config")
+54
View File
@@ -0,0 +1,54 @@
package debug
import (
"net/http"
"net/http/pprof"
"runtime"
"github.com/gin-gonic/gin"
"github.com/kandev/kandev/internal/common/logger"
)
// RegisterPprofRoutes mounts the standard net/http/pprof handlers on the Gin router
// at /debug/pprof/. These endpoints are only registered when debug.pprofEnabled is true.
func RegisterPprofRoutes(router *gin.Engine, log *logger.Logger) {
g := router.Group("/debug/pprof")
g.GET("/", gin.WrapF(pprof.Index))
g.GET("/cmdline", gin.WrapF(pprof.Cmdline))
g.GET("/profile", gin.WrapF(pprof.Profile))
g.GET("/symbol", gin.WrapF(pprof.Symbol))
g.POST("/symbol", gin.WrapF(pprof.Symbol))
g.GET("/trace", gin.WrapF(pprof.Trace))
for _, name := range []string{"allocs", "block", "goroutine", "heap", "mutex", "threadcreate"} {
g.GET("/"+name, gin.WrapH(pprof.Handler(name)))
}
log.Info("pprof endpoints registered at /debug/pprof/")
}
// RegisterMemoryRoute registers GET /api/v1/debug/memory which returns a JSON summary
// of runtime memory statistics. Useful for quick health checks without pulling a full
// pprof heap profile.
func RegisterMemoryRoute(router *gin.Engine, log *logger.Logger) {
router.GET("/api/v1/debug/memory", handleMemoryStats())
log.Info("Memory stats endpoint registered at /api/v1/debug/memory")
}
func handleMemoryStats() gin.HandlerFunc {
return func(c *gin.Context) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
c.JSON(http.StatusOK, gin.H{
"heap_alloc_mb": float64(m.HeapAlloc) / (1024 * 1024),
"heap_inuse_mb": float64(m.HeapInuse) / (1024 * 1024),
"heap_sys_mb": float64(m.HeapSys) / (1024 * 1024),
"heap_objects": m.HeapObjects,
"goroutines": runtime.NumGoroutine(),
"num_gc": m.NumGC,
"sys_mb": float64(m.Sys) / (1024 * 1024),
"stack_inuse_mb": float64(m.StackInuse) / (1024 * 1024),
})
}
}
+1
View File
@@ -26,6 +26,7 @@ export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Pr
const extra: Record<string, string> = {
KANDEV_DATABASE_PATH: dbPath,
KANDEV_MOCK_AGENT: process.env.KANDEV_MOCK_AGENT || "true",
KANDEV_DEBUG_PPROF_ENABLED: "true",
};
const backendEnv = buildBackendEnv({ ports, extra });
const webEnv = buildWebEnv({ ports, debug: true });
+1 -1
View File
@@ -187,7 +187,7 @@ async function prepareReleaseBundle({
KANDEV_AGENT_STANDALONE_PORT: String(agentctlPort),
KANDEV_DATABASE_PATH: dbPath,
KANDEV_LOG_LEVEL: logLevel,
...(debug ? { KANDEV_DEBUG_AGENT_MESSAGES: "true" } : {}),
...(debug ? { KANDEV_DEBUG_AGENT_MESSAGES: "true", KANDEV_DEBUG_PPROF_ENABLED: "true" } : {}),
};
const webEnv: NodeJS.ProcessEnv = {
+1 -1
View File
@@ -118,7 +118,7 @@ export async function runStart({
logLevel,
extra: {
KANDEV_DATABASE_PATH: dbPath,
...(debug ? { KANDEV_DEBUG_AGENT_MESSAGES: "true" } : {}),
...(debug ? { KANDEV_DEBUG_AGENT_MESSAGES: "true", KANDEV_DEBUG_PPROF_ENABLED: "true" } : {}),
},
});
const webEnv = buildWebEnv({ ports, production: true, debug });