Skip to content

Commit e78e2e1

Browse files
committed
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.
1 parent 737062c commit e78e2e1

File tree

9 files changed

+498
-258
lines changed

9 files changed

+498
-258
lines changed

bin/collect-profiles.sh

Lines changed: 0 additions & 64 deletions
This file was deleted.

cmd/ipfs/debug.go

Lines changed: 2 additions & 2 deletions
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

Lines changed: 38 additions & 162 deletions
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,26 @@ type profileResult struct {
2320
File string
2421
}
2522

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

2829
var sysProfileCmd = &cmds.Command{
2930
Helptext: cmds.HelpText{
3031
Tagline: "Collect a performance profile for debugging.",
3132
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.
33+
Collects profiles from a running go-ipfs daemon into a single zip file.
34+
To aid in debugging, this command also attempts to include a copy of
35+
the running go-ipfs binary.
3536
`,
3637
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.
38+
Collects profiles from a running go-ipfs daemon into a single zipfile.
39+
To aid in debugging, this command also attempts to include a copy of
40+
the running go-ipfs binary.
4041
41-
Profile's can be examined using 'go tool pprof', some tips can be found at
42+
Profiles can be examined using 'go tool pprof', some tips can be found at
4243
https://github.com/ipfs/go-ipfs/blob/master/docs/debug-guide.md.
4344
4445
Privacy Notice:
@@ -48,6 +49,8 @@ The output file includes:
4849
- A list of running goroutines.
4950
- A CPU profile.
5051
- A heap profile.
52+
- A mutex profile.
53+
- A block profile.
5154
- Your copy of go-ipfs.
5255
- The output of 'ipfs version --all'.
5356
@@ -69,18 +72,36 @@ However, it could reveal:
6972
NoLocal: true,
7073
Options: []cmds.Option{
7174
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(profileTimeOption, "The amount of time spent profiling. If this is set to 0, then sampling profiles are skipped.").WithDefault("30s"),
76+
cmds.IntOption(mutexProfileFractionOption, "The fraction 1/n of mutex contention events that are reported in the mutex profile.").WithDefault(4),
77+
cmds.StringOption(blockProfileRateOption, "The duration to wait between sampling goroutine-blocking events for the blocking profile.").WithDefault("1ms"),
7378
},
7479
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
75-
cpuProfileTimeStr, _ := req.Options[cpuProfileTimeOption].(string)
76-
cpuProfileTime, err := time.ParseDuration(cpuProfileTimeStr)
80+
profileTimeStr, _ := req.Options[profileTimeOption].(string)
81+
profileTime, err := time.ParseDuration(profileTimeStr)
82+
if err != nil {
83+
return fmt.Errorf("failed to parse profile duration %q: %w", profileTimeStr, err)
84+
}
85+
86+
blockProfileRateStr, _ := req.Options[blockProfileRateOption].(string)
87+
blockProfileRate, err := time.ParseDuration(blockProfileRateStr)
7788
if err != nil {
78-
return fmt.Errorf("failed to parse CPU profile duration %q: %w", cpuProfileTimeStr, err)
89+
return fmt.Errorf("failed to parse block profile rate %q: %w", blockProfileRateStr, err)
7990
}
8091

92+
mutexProfileFraction, _ := req.Options[mutexProfileFractionOption].(int)
93+
8194
r, w := io.Pipe()
95+
8296
go func() {
83-
_ = w.CloseWithError(writeProfiles(req.Context, cpuProfileTime, w))
97+
archive := zip.NewWriter(w)
98+
err = profile.WriteProfiles(req.Context, archive, profile.Options{
99+
ProfileDuration: profileTime,
100+
MutexProfileFraction: mutexProfileFraction,
101+
BlockProfileRate: blockProfileRate,
102+
})
103+
archive.Close()
104+
_ = w.CloseWithError(err)
84105
}()
85106
return res.Emit(r)
86107
},
@@ -120,148 +141,3 @@ However, it could reveal:
120141
}),
121142
},
122143
}
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/version.go

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,20 @@ import (
44
"errors"
55
"fmt"
66
"io"
7-
"runtime"
87
"runtime/debug"
98

109
version "github.com/ipfs/go-ipfs"
11-
fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
1210

1311
cmds "github.com/ipfs/go-ipfs-cmds"
1412
)
1513

16-
type VersionOutput struct {
17-
Version string
18-
Commit string
19-
Repo string
20-
System string
21-
Golang string
22-
}
23-
2414
const (
2515
versionNumberOptionName = "number"
2616
versionCommitOptionName = "commit"
2717
versionRepoOptionName = "repo"
2818
versionAllOptionName = "all"
2919
)
3020

31-
func getVersionInfo() *VersionOutput {
32-
return &VersionOutput{
33-
Version: version.CurrentVersionNumber,
34-
Commit: version.CurrentCommit,
35-
Repo: fmt.Sprint(fsrepo.RepoVersion),
36-
System: runtime.GOARCH + "/" + runtime.GOOS, //TODO: Precise version here
37-
Golang: runtime.Version(),
38-
}
39-
}
40-
4121
var VersionCmd = &cmds.Command{
4222
Helptext: cmds.HelpText{
4323
Tagline: "Show IPFS version information.",
@@ -56,10 +36,10 @@ var VersionCmd = &cmds.Command{
5636
// must be permitted to run before init
5737
Extra: CreateCmdExtras(SetDoesNotUseRepo(true), SetDoesNotUseConfigAsInput(true)),
5838
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
59-
return cmds.EmitOnce(res, getVersionInfo())
39+
return cmds.EmitOnce(res, version.GetVersionInfo())
6040
},
6141
Encoders: cmds.EncoderMap{
62-
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, version *VersionOutput) error {
42+
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, version *version.VersionInfo) error {
6343
all, _ := req.Options[versionAllOptionName].(bool)
6444
if all {
6545
ver := version.Version
@@ -95,7 +75,7 @@ var VersionCmd = &cmds.Command{
9575
return nil
9676
}),
9777
},
98-
Type: VersionOutput{},
78+
Type: version.VersionInfo{},
9979
}
10080

10181
type Dependency struct {

0 commit comments

Comments
 (0)