Skip to content

Commit b7705e0

Browse files
authored
Merge pull request #185 from multiversx/pprof-and-shutdown
Pprof and shutdown
2 parents 6d12db1 + 7f0959c commit b7705e0

File tree

2 files changed

+96
-3
lines changed

2 files changed

+96
-3
lines changed

cmd/chainsimulator/flags.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ var (
147147
Name: "fetch-configs-and-close",
148148
Usage: "This flag is used to specify to fetch all configs and close the chain simulator after",
149149
}
150+
enableProfiling = cli.BoolFlag{
151+
Name: "enable-profiling",
152+
Usage: "Boolean option for enabling CPU profiling. If set, CPU profile will be saved to a file.",
153+
}
150154
)
151155

152156
func applyFlags(ctx *cli.Context, cfg *config.Config) {

cmd/chainsimulator/main.go

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ package main
33
import (
44
"errors"
55
"fmt"
6+
"net/http"
67
"os"
78
"os/signal"
89
"runtime/debug"
10+
"runtime/pprof"
911
"strconv"
1012
"strings"
1113
"syscall"
1214
"time"
1315

16+
"github.com/gin-gonic/gin"
1417
"github.com/multiversx/mx-chain-core-go/core"
1518
"github.com/multiversx/mx-chain-core-go/core/check"
1619
"github.com/multiversx/mx-chain-core-go/core/closing"
@@ -86,6 +89,7 @@ func main() {
8689
skipConfigsDownload,
8790
fetchConfigsAndClose,
8891
pathWhereToSaveLogs,
92+
enableProfiling,
8993
}
9094

9195
app.Authors = []cli.Author{
@@ -177,6 +181,23 @@ func startChainSimulator(ctx *cli.Context) error {
177181
return err
178182
}
179183

184+
// CPU profiling setup - only if enable-profiling flag is set
185+
var profileFile *os.File
186+
profilingEnabled := ctx.GlobalBool(enableProfiling.Name)
187+
if profilingEnabled {
188+
pathLogsSave := ctx.GlobalString(pathWhereToSaveLogs.Name)
189+
profileFile, err = startCPUProfiling(pathLogsSave, startTimeUnix)
190+
if err != nil {
191+
return fmt.Errorf("%w while starting CPU profiling", err)
192+
}
193+
194+
// Ensure pprof is stopped and file is synced/closed even on early exits
195+
defer func() {
196+
log.Info("stopping CPU profile (defer)")
197+
stopCPUProfiling(profileFile)
198+
}()
199+
}
200+
180201
var alterConfigsError error
181202
argsChainSimulator := chainSimulator.ArgsChainSimulator{
182203
BypassTxSignatureCheck: bypassTxsSignature,
@@ -277,7 +298,28 @@ func startChainSimulator(ctx *cli.Context) error {
277298
return err
278299
}
279300

280-
err = endpointsProc.ExtendProxyServer(proxyInstance.GetHttpServer())
301+
// Create a channel for programmatic shutdown
302+
shutdownChan := make(chan struct{})
303+
304+
// Add a shutdown endpoint before extending the proxy server
305+
httpServer := proxyInstance.GetHttpServer()
306+
ginEngine, ok := httpServer.Handler.(*gin.Engine)
307+
if !ok {
308+
return fmt.Errorf("cannot cast httpServer.Handler to gin.Engine")
309+
}
310+
311+
ginEngine.POST("/simulator/shutdown", func(c *gin.Context) {
312+
log.Info("shutdown requested via HTTP endpoint")
313+
c.JSON(http.StatusOK, gin.H{"message": "shutdown initiated"})
314+
315+
// Trigger shutdown in a goroutine to allow the response to be sent
316+
go func() {
317+
time.Sleep(100 * time.Millisecond)
318+
close(shutdownChan)
319+
}()
320+
})
321+
322+
err = endpointsProc.ExtendProxyServer(httpServer)
281323
if err != nil {
282324
return err
283325
}
@@ -289,9 +331,19 @@ func startChainSimulator(ctx *cli.Context) error {
289331

290332
interrupt := make(chan os.Signal, 1)
291333
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
292-
<-interrupt
293334

294-
log.Info("close")
335+
// Wait for either signal or programmatic shutdown
336+
select {
337+
case sig := <-interrupt:
338+
log.Info("close", "signal", sig)
339+
case <-shutdownChan:
340+
log.Info("close", "trigger", "HTTP shutdown endpoint")
341+
}
342+
343+
// Stop CPU profiling FIRST and flush to disk (only if profiling is enabled)
344+
if profilingEnabled {
345+
stopCPUProfiling(profileFile)
346+
}
295347

296348
generator.Close()
297349

@@ -306,6 +358,43 @@ func startChainSimulator(ctx *cli.Context) error {
306358
return nil
307359
}
308360

361+
func startCPUProfiling(pathLogsSave string, startTimeUnix int64) (*os.File, error) {
362+
timestampMilisecond := time.Unix(startTimeUnix, 0).UnixNano() / 1000000
363+
cpuProfilePath := fmt.Sprintf("%s/cpu-%d.pprof", pathLogsSave, timestampMilisecond)
364+
365+
profileFile, err := os.Create(cpuProfilePath)
366+
if err != nil {
367+
return nil, fmt.Errorf("could not create CPU profile: %w", err)
368+
}
369+
370+
if err := pprof.StartCPUProfile(profileFile); err != nil {
371+
_ = profileFile.Close()
372+
return nil, fmt.Errorf("could not start CPU profile: %w", err)
373+
}
374+
375+
log.Info("CPU profiling started", "path", cpuProfilePath)
376+
return profileFile, nil
377+
}
378+
379+
func stopCPUProfiling(profileFile *os.File) {
380+
if profileFile == nil {
381+
return
382+
}
383+
384+
log.Info("stopping CPU profile")
385+
pprof.StopCPUProfile()
386+
387+
if err := profileFile.Sync(); err != nil {
388+
log.Error("error syncing CPU profile file", "err", err)
389+
}
390+
391+
if err := profileFile.Close(); err != nil {
392+
log.Error("error closing CPU profile file", "err", err)
393+
} else {
394+
log.Info("CPU profile file closed successfully")
395+
}
396+
}
397+
309398
func initializeLogger(ctx *cli.Context, cfg config.Config) (closing.Closer, error) {
310399
logLevelFlagValue := ctx.GlobalString(logLevel.Name)
311400
err := logger.SetLogLevel(logLevelFlagValue)

0 commit comments

Comments
 (0)