Skip to content

Commit dc7656b

Browse files
committed
core/web: support pprof for loop plugins
1 parent 513d33d commit dc7656b

27 files changed

Lines changed: 401 additions & 85 deletions

File tree

.changeset/slow-deer-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink": patch
3+
---
4+
5+
Expanded `admin profile` to collect PPROF profiles from LOOP Plugins. Added `-vitals` flag for more granular profiling.

core/cmd/admin_commands.go

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package cmd
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/json"
67
"errors"
78
"fmt"
89
"io"
910
"net/http"
1011
"os"
1112
"path/filepath"
13+
"slices"
1214
"strings"
1315
"sync"
1416
"time"
@@ -20,6 +22,7 @@ import (
2022

2123
"github.com/smartcontractkit/chainlink/v2/core/sessions"
2224
"github.com/smartcontractkit/chainlink/v2/core/utils"
25+
"github.com/smartcontractkit/chainlink/v2/core/web"
2326
"github.com/smartcontractkit/chainlink/v2/core/web/presenters"
2427
)
2528

@@ -65,6 +68,10 @@ func initAdminSubCmds(s *Shell) []cli.Command {
6568
Usage: "output directory of the captured profile",
6669
Value: "/tmp/",
6770
},
71+
cli.StringSliceFlag{
72+
Name: "vitals, v",
73+
Usage: "vitals to collect, can be specified multiple times. Options: 'allocs', 'block', 'cmdline', 'goroutine', 'heap', 'mutex', 'profile', 'threadcreate', 'trace'",
74+
},
6875
},
6976
},
7077
{
@@ -319,16 +326,13 @@ func (s *Shell) Status(c *cli.Context) error {
319326
// Profile will collect pprof metrics and store them in a folder.
320327
func (s *Shell) Profile(c *cli.Context) error {
321328
ctx := s.ctx()
322-
seconds := c.Uint("seconds")
329+
seconds := c.Int("seconds")
323330
baseDir := c.String("output_dir")
324331

325332
genDir := filepath.Join(baseDir, "debuginfo-"+time.Now().Format(time.RFC3339))
326333

327-
if err := os.Mkdir(genDir, 0o755); err != nil {
328-
return s.errorOut(err)
329-
}
330-
var wgPprof sync.WaitGroup
331-
vitals := []string{
334+
vitals := c.StringSlice("vitals")
335+
allVitals := []string{
332336
"allocs", // A sampling of all past memory allocations
333337
"block", // Stack traces that led to blocking on synchronization primitives
334338
"cmdline", // The command line invocation of the current program
@@ -339,15 +343,100 @@ func (s *Shell) Profile(c *cli.Context) error {
339343
"threadcreate", // Stack traces that led to the creation of new OS threads
340344
"trace", // A trace of execution of the current program.
341345
}
342-
wgPprof.Add(len(vitals))
343-
s.Logger.Infof("Collecting profiles: %v", vitals)
346+
if len(vitals) == 0 {
347+
vitals = slices.Clone(allVitals)
348+
} else if slices.ContainsFunc(vitals, func(s string) bool { return !slices.Contains(allVitals, s) }) {
349+
return fmt.Errorf("invalid vitals: must be from the set: %v", allVitals)
350+
}
351+
352+
plugins, err := s.discoverPlugins(ctx)
353+
if err != nil {
354+
return s.errorOut(err)
355+
}
356+
var names []string
357+
for _, group := range plugins {
358+
if name := group.Labels[web.LabelMetaPluginName]; name != "" {
359+
names = append(names, name)
360+
}
361+
}
362+
363+
if len(names) == 0 {
364+
s.Logger.Infof("Collecting profiles: %v", vitals)
365+
} else {
366+
s.Logger.Infof("Collecting profiles from host and %d plugins: %v", len(names), vitals)
367+
}
344368
s.Logger.Infof("writing debug info to %s", genDir)
345369

370+
var wg sync.WaitGroup
371+
errs := make([]error, len(names)+1)
372+
wg.Add(len(names) + 1)
373+
go func() {
374+
defer wg.Done()
375+
errs[0] = s.profile(ctx, genDir, "", vitals, seconds)
376+
}()
377+
for i, name := range names {
378+
go func() {
379+
defer wg.Done()
380+
errs[i] = s.profile(ctx, genDir, name, vitals, seconds)
381+
}()
382+
}
383+
wg.Wait()
384+
385+
err = errors.Join(errs...)
386+
if err != nil {
387+
return s.errorOut(err)
388+
}
389+
return nil
390+
}
391+
func (s *Shell) discoverPlugins(ctx context.Context) (
392+
got []struct {
393+
Targets []string `yaml:"targets"`
394+
Labels map[string]string `yaml:"labels"`
395+
},
396+
err error,
397+
) {
398+
resp, err := s.HTTP.Get(ctx, "/discovery")
399+
if err != nil {
400+
return
401+
}
402+
defer func() {
403+
if resp.Body != nil {
404+
resp.Body.Close()
405+
}
406+
}()
407+
data, err := io.ReadAll(resp.Body)
408+
if err != nil {
409+
return
410+
}
411+
412+
if err = json.Unmarshal(data, &got); err != nil {
413+
s.Logger.Errorf("failed to unmarshal discovery response: %s", string(data))
414+
return
415+
}
416+
return
417+
}
418+
419+
func (s *Shell) profile(ctx context.Context, genDir string, name string, vitals []string, seconds int) error {
420+
lggr := s.Logger
421+
path := "/v2"
422+
if name != "" {
423+
genDir = filepath.Join(genDir, "plugins", name)
424+
path += "/plugins/" + name
425+
lggr = lggr.With("plugin", name)
426+
}
427+
if err := os.MkdirAll(genDir, 0o755); err != nil {
428+
return fmt.Errorf("failed to create directory: %w", err)
429+
}
430+
346431
errs := make(chan error, len(vitals))
432+
var wgPprof sync.WaitGroup
433+
wgPprof.Add(len(vitals))
347434
for _, vt := range vitals {
348-
go func(vt string) {
435+
go func(ctx context.Context, vt string) {
349436
defer wgPprof.Done()
350-
uri := fmt.Sprintf("/v2/debug/pprof/%s?seconds=%d", vt, seconds)
437+
ctx, cancel := context.WithTimeout(ctx, time.Duration(max(seconds, 0)+web.PPROFOverheadSeconds)*time.Second)
438+
defer cancel()
439+
uri := fmt.Sprintf(path+"/debug/pprof/%s?seconds=%d", vt, seconds)
351440
resp, err := s.HTTP.Get(ctx, uri)
352441
if err != nil {
353442
errs <- fmt.Errorf("error collecting %s: %w", vt, err)
@@ -403,12 +492,12 @@ func (s *Shell) Profile(c *cli.Context) error {
403492
errs <- fmt.Errorf("error closing file for %s: %w", vt, err)
404493
return
405494
}
406-
}(vt)
495+
}(ctx, vt)
407496
}
408497
wgPprof.Wait()
409498
close(errs)
410-
// Atmost one err is emitted per vital.
411-
s.Logger.Infof("collected %d/%d profiles", len(vitals)-len(errs), len(vitals))
499+
// At most one err is emitted per vital.
500+
lggr.Infof("collected %d/%d profiles", len(vitals)-len(errs), len(vitals))
412501
if len(errs) > 0 {
413502
var merr error
414503
for err := range errs {

core/cmd/shell_local.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ func initLocalSubCmds(s *Shell, safe bool) []cli.Command {
148148
Usage: "output directory of the captured profile",
149149
Value: "/tmp/",
150150
},
151+
cli.StringSliceFlag{
152+
Name: "vitals, v",
153+
Usage: "vitals to collect, can be specified multiple times. Options: 'allocs', 'block', 'cmdline', 'goroutine', 'heap', 'mutex', 'profile', 'threadcreate', 'trace'",
154+
},
151155
},
152156
Hidden: true,
153157
Before: func(_ *cli.Context) error {

core/scripts/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ require (
512512
github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260217210647-11c42009ec1f // indirect
513513
github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.10.0 // indirect
514514
github.com/smartcontractkit/chainlink-testing-framework/parrot v0.6.2 // indirect
515-
github.com/smartcontractkit/chainlink-ton v0.0.0-20260218110243-cd2592187c66 // indirect
515+
github.com/smartcontractkit/chainlink-ton v0.0.0-20260218144352-f8d460be6125 // indirect
516516
github.com/smartcontractkit/chainlink-ton/deployment v0.0.0-20260218110243-cd2592187c66 // indirect
517517
github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260218133534-cbd44da2856b // indirect
518518
github.com/smartcontractkit/cre-sdk-go v1.3.0 // indirect

core/scripts/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,8 +1690,8 @@ github.com/smartcontractkit/chainlink-testing-framework/parrot v0.6.2 h1:cWUHB6Q
16901690
github.com/smartcontractkit/chainlink-testing-framework/parrot v0.6.2/go.mod h1:Z4K5VJLjsfqIIaBcZ1Sfccxu0xsCxBjPa6zF+5gtQaM=
16911691
github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.3 h1:TZ0Yk+vjAJpoWnfsPdftWkq/NwZTrk734a/H4RHKnY8=
16921692
github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.3/go.mod h1:kHYJnZUqiPF7/xN5273prV+srrLJkS77GbBXHLKQpx0=
1693-
github.com/smartcontractkit/chainlink-ton v0.0.0-20260218110243-cd2592187c66 h1:6l+kMm8s9V3cOxAeMpzfvj7CgiscKAPUudTf9KpgKNs=
1694-
github.com/smartcontractkit/chainlink-ton v0.0.0-20260218110243-cd2592187c66/go.mod h1:Sy0O2HOmKJ+m0CpvCwye3CF8VwldBBFpqFhKai+p8/g=
1693+
github.com/smartcontractkit/chainlink-ton v0.0.0-20260218144352-f8d460be6125 h1:ptVEMET7sCJg6ToNkDwiqu3PqtceTHTlePQZxCeYHMg=
1694+
github.com/smartcontractkit/chainlink-ton v0.0.0-20260218144352-f8d460be6125/go.mod h1:FDDjLuc4vrfclu3JHkMaREg0XZz7Lw1MK47Z4jJ4U5Q=
16951695
github.com/smartcontractkit/chainlink-ton/deployment v0.0.0-20260218110243-cd2592187c66 h1:PDsujAkEXtdfpArKAlQs9OsBmmyAZpv6d7jOjxs6uJM=
16961696
github.com/smartcontractkit/chainlink-ton/deployment v0.0.0-20260218110243-cd2592187c66/go.mod h1:GNtjS/cwOMowWXLq9HFtY8JlZ98lK9UHt1bEuaev52c=
16971697
github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260218133534-cbd44da2856b h1:0XLtETkgkzwnEgUIIgyO/oydkUpzDVVuuFLf6aBeNPg=

0 commit comments

Comments
 (0)