Skip to content

Commit bb68a68

Browse files
guseggertlidel
andauthored
feat: port collect-profiles.sh to 'ipfs diag profile' (#8786)
* feat: add block profiling to collect-profiles.sh * feat: add more profiles to 'ipfs diag profile' This adds mutex and block profiles, and brings the command up-to-par with 'collect-profiles.sh', so that we can remove it. Profiles are also now collected concurrently, which improves the runtime from (profile_time * num_profiles) to just (profile_time). Note that this has a backwards-incompatible change, removing --cpu-profile-time in favor of the more general --profile-time, which covers all sampling profiles. * docs(cli): ipfs diag profile * add CLI flag to select specific diag collectors Co-authored-by: Marcin Rataj <[email protected]>
1 parent ca4a3ed commit bb68a68

File tree

10 files changed

+574
-248
lines changed

10 files changed

+574
-248
lines changed

bin/collect-profiles.sh

-53
This file was deleted.

cmd/ipfs/debug.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ package main
33
import (
44
"net/http"
55

6-
"github.com/ipfs/go-ipfs/core/commands"
6+
"github.com/ipfs/go-ipfs/profile"
77
)
88

99
func init() {
1010
http.HandleFunc("/debug/stack",
1111
func(w http.ResponseWriter, _ *http.Request) {
12-
_ = commands.WriteAllGoroutineStacks(w)
12+
_ = profile.WriteAllGoroutineStacks(w)
1313
},
1414
)
1515
}

core/commands/profile.go

+54-163
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@ package commands
22

33
import (
44
"archive/zip"
5-
"context"
6-
"encoding/json"
75
"fmt"
86
"io"
97
"os"
10-
"runtime"
11-
"runtime/pprof"
128
"strings"
139
"time"
1410

1511
cmds "github.com/ipfs/go-ipfs-cmds"
1612
"github.com/ipfs/go-ipfs/core/commands/e"
13+
"github.com/ipfs/go-ipfs/profile"
1714
)
1815

1916
// time format that works in filenames on windows.
@@ -23,22 +20,27 @@ type profileResult struct {
2320
File string
2421
}
2522

26-
const cpuProfileTimeOption = "cpu-profile-time"
23+
const (
24+
collectorsOptionName = "collectors"
25+
profileTimeOption = "profile-time"
26+
mutexProfileFractionOption = "mutex-profile-fraction"
27+
blockProfileRateOption = "block-profile-rate"
28+
)
2729

2830
var sysProfileCmd = &cmds.Command{
2931
Helptext: cmds.HelpText{
3032
Tagline: "Collect a performance profile for debugging.",
3133
ShortDescription: `
32-
Collects cpu, heap, and goroutine profiles from a running go-ipfs daemon
33-
into a single zip file. To aid in debugging, this command also attempts to
34-
include a copy of the running go-ipfs binary.
34+
Collects profiles from a running go-ipfs daemon into a single zip file.
35+
To aid in debugging, this command also attempts to include a copy of
36+
the running go-ipfs binary.
3537
`,
3638
LongDescription: `
37-
Collects cpu, heap, and goroutine profiles from a running go-ipfs daemon
38-
into a single zipfile. To aid in debugging, this command also attempts to
39-
include a copy of the running go-ipfs binary.
39+
Collects profiles from a running go-ipfs daemon into a single zipfile.
40+
To aid in debugging, this command also attempts to include a copy of
41+
the running go-ipfs binary.
4042
41-
Profile's can be examined using 'go tool pprof', some tips can be found at
43+
Profiles can be examined using 'go tool pprof', some tips can be found at
4244
https://github.com/ipfs/go-ipfs/blob/master/docs/debug-guide.md.
4345
4446
Privacy Notice:
@@ -48,6 +50,8 @@ The output file includes:
4850
- A list of running goroutines.
4951
- A CPU profile.
5052
- A heap profile.
53+
- A mutex profile.
54+
- A block profile.
5155
- Your copy of go-ipfs.
5256
- The output of 'ipfs version --all'.
5357
@@ -68,19 +72,51 @@ However, it could reveal:
6872
},
6973
NoLocal: true,
7074
Options: []cmds.Option{
71-
cmds.StringOption(outputOptionName, "o", "The path where the output should be stored."),
72-
cmds.StringOption(cpuProfileTimeOption, "The amount of time spent profiling CPU usage.").WithDefault("30s"),
75+
cmds.StringOption(outputOptionName, "o", "The path where the output .zip should be stored. Default: ./ipfs-profile-[timestamp].zip"),
76+
cmds.DelimitedStringsOption(",", collectorsOptionName, "The list of collectors to use for collecting diagnostic data.").
77+
WithDefault([]string{
78+
profile.CollectorGoroutinesStack,
79+
profile.CollectorGoroutinesPprof,
80+
profile.CollectorVersion,
81+
profile.CollectorHeap,
82+
profile.CollectorBin,
83+
profile.CollectorCPU,
84+
profile.CollectorMutex,
85+
profile.CollectorBlock,
86+
}),
87+
cmds.StringOption(profileTimeOption, "The amount of time spent profiling. If this is set to 0, then sampling profiles are skipped.").WithDefault("30s"),
88+
cmds.IntOption(mutexProfileFractionOption, "The fraction 1/n of mutex contention events that are reported in the mutex profile.").WithDefault(4),
89+
cmds.StringOption(blockProfileRateOption, "The duration to wait between sampling goroutine-blocking events for the blocking profile.").WithDefault("1ms"),
7390
},
7491
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
75-
cpuProfileTimeStr, _ := req.Options[cpuProfileTimeOption].(string)
76-
cpuProfileTime, err := time.ParseDuration(cpuProfileTimeStr)
92+
collectors := req.Options[collectorsOptionName].([]string)
93+
94+
profileTimeStr, _ := req.Options[profileTimeOption].(string)
95+
profileTime, err := time.ParseDuration(profileTimeStr)
7796
if err != nil {
78-
return fmt.Errorf("failed to parse CPU profile duration %q: %w", cpuProfileTimeStr, err)
97+
return fmt.Errorf("failed to parse profile duration %q: %w", profileTimeStr, err)
7998
}
8099

100+
blockProfileRateStr, _ := req.Options[blockProfileRateOption].(string)
101+
blockProfileRate, err := time.ParseDuration(blockProfileRateStr)
102+
if err != nil {
103+
return fmt.Errorf("failed to parse block profile rate %q: %w", blockProfileRateStr, err)
104+
}
105+
106+
mutexProfileFraction, _ := req.Options[mutexProfileFractionOption].(int)
107+
81108
r, w := io.Pipe()
109+
82110
go func() {
83-
_ = w.CloseWithError(writeProfiles(req.Context, cpuProfileTime, w))
111+
archive := zip.NewWriter(w)
112+
err = profile.WriteProfiles(req.Context, archive, profile.Options{
113+
Collectors: collectors,
114+
ProfileDuration: profileTime,
115+
MutexProfileFraction: mutexProfileFraction,
116+
BlockProfileRate: blockProfileRate,
117+
})
118+
archive.Close()
119+
_ = w.CloseWithError(err)
84120
}()
85121
return res.Emit(r)
86122
},
@@ -120,148 +156,3 @@ However, it could reveal:
120156
}),
121157
},
122158
}
123-
124-
func WriteAllGoroutineStacks(w io.Writer) error {
125-
// this is based on pprof.writeGoroutineStacks, and removes the 64 MB limit
126-
buf := make([]byte, 1<<20)
127-
for i := 0; ; i++ {
128-
n := runtime.Stack(buf, true)
129-
if n < len(buf) {
130-
buf = buf[:n]
131-
break
132-
}
133-
// if len(buf) >= 64<<20 {
134-
// // Filled 64 MB - stop there.
135-
// break
136-
// }
137-
buf = make([]byte, 2*len(buf))
138-
}
139-
_, err := w.Write(buf)
140-
return err
141-
}
142-
143-
func writeProfiles(ctx context.Context, cpuProfileTime time.Duration, w io.Writer) error {
144-
archive := zip.NewWriter(w)
145-
146-
// Take some profiles.
147-
type profile struct {
148-
name string
149-
file string
150-
debug int
151-
}
152-
153-
profiles := []profile{{
154-
name: "goroutine",
155-
file: "goroutines.stacks",
156-
debug: 2,
157-
}, {
158-
name: "goroutine",
159-
file: "goroutines.pprof",
160-
}, {
161-
name: "heap",
162-
file: "heap.pprof",
163-
}}
164-
165-
{
166-
out, err := archive.Create("goroutines-all.stacks")
167-
if err != nil {
168-
return err
169-
}
170-
err = WriteAllGoroutineStacks(out)
171-
if err != nil {
172-
return err
173-
}
174-
}
175-
176-
for _, profile := range profiles {
177-
prof := pprof.Lookup(profile.name)
178-
out, err := archive.Create(profile.file)
179-
if err != nil {
180-
return err
181-
}
182-
err = prof.WriteTo(out, profile.debug)
183-
if err != nil {
184-
return err
185-
}
186-
}
187-
188-
// Take a CPU profile.
189-
if cpuProfileTime != 0 {
190-
out, err := archive.Create("cpu.pprof")
191-
if err != nil {
192-
return err
193-
}
194-
195-
err = writeCPUProfile(ctx, cpuProfileTime, out)
196-
if err != nil {
197-
return err
198-
}
199-
}
200-
201-
// Collect version info
202-
// I'd use diag sysinfo, but that includes some more sensitive information
203-
// (GOPATH, etc.).
204-
{
205-
out, err := archive.Create("version.json")
206-
if err != nil {
207-
return err
208-
}
209-
210-
err = json.NewEncoder(out).Encode(getVersionInfo())
211-
if err != nil {
212-
return err
213-
}
214-
}
215-
216-
// Collect binary
217-
if fi, err := openIPFSBinary(); err == nil {
218-
fname := "ipfs"
219-
if runtime.GOOS == "windows" {
220-
fname += ".exe"
221-
}
222-
223-
out, err := archive.Create(fname)
224-
if err != nil {
225-
return err
226-
}
227-
228-
_, err = io.Copy(out, fi)
229-
_ = fi.Close()
230-
if err != nil {
231-
return err
232-
}
233-
}
234-
return archive.Close()
235-
}
236-
237-
func writeCPUProfile(ctx context.Context, d time.Duration, w io.Writer) error {
238-
if err := pprof.StartCPUProfile(w); err != nil {
239-
return err
240-
}
241-
defer pprof.StopCPUProfile()
242-
243-
timer := time.NewTimer(d)
244-
defer timer.Stop()
245-
246-
select {
247-
case <-timer.C:
248-
case <-ctx.Done():
249-
return ctx.Err()
250-
}
251-
return nil
252-
}
253-
254-
func openIPFSBinary() (*os.File, error) {
255-
if runtime.GOOS == "linux" {
256-
pid := os.Getpid()
257-
fi, err := os.Open(fmt.Sprintf("/proc/%d/exe", pid))
258-
if err == nil {
259-
return fi, nil
260-
}
261-
}
262-
path, err := os.Executable()
263-
if err != nil {
264-
return nil, err
265-
}
266-
return os.Open(path)
267-
}

core/commands/root.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ NETWORK COMMANDS
6767
swarm Manage connections to the p2p network
6868
dht Query the DHT for values or peers
6969
ping Measure the latency of a connection
70-
diag Print diagnostics
7170
bitswap Inspect bitswap state
7271
pubsub Send and receive messages via pubsub
7372
7473
TOOL COMMANDS
7574
config Manage configuration
7675
version Show IPFS version information
76+
diag Generate diagnostic reports
7777
update Download and apply go-ipfs updates
7878
commands List all available commands
7979
log Manage and show logs of running daemon

0 commit comments

Comments
 (0)