@cryptotaxi247 / kubo / commits / d52d18302

feat: add an "ipfs diag profile" command

This should replace the "collect-profiles.sh" script and allow users to easily collect profiles. At the moment, it just dumps all profiles into a single zip file. It does this server-side so it's easy fetch them with curl. In the future, it would be nice to add: 1. Progress indicators (cpu profiles take 30 seconds). 2. An option to specify which profiles to collect. But we can handle that later. Unfortunately, this command doesn't produce symbolized svgs as I didn't want to depend on having a local go compiler.

Steven Allen committed Jul 21, 2021 at 14:12 UTC d52d183020842bebac788842e4f72d238609517d
5 files changed +283 -27
core/commands/diag.go
+3 -2
@@ -10,7 +10,8 @@ var DiagCmd = &cmds.Command{
10 },
11
12 Subcommands: map[string]*cmds.Command{
13 - "sys": sysDiagCmd,
14 - "cmds": ActiveReqsCmd,
13 + "sys": sysDiagCmd,
14 + "cmds": ActiveReqsCmd,
15 + "profile": sysProfileCmd,
16 },
17 }
core/commands/profile.go new
+205
@@ -0,0 +1,205 @@
1 +package commands
2 +
3 +import (
4 + "archive/zip"
5 + "context"
6 + "encoding/json"
7 + "fmt"
8 + "io"
9 + "os"
10 + "runtime"
11 + "runtime/pprof"
12 + "strings"
13 + "time"
14 +
15 + cmds "github.com/ipfs/go-ipfs-cmds"
16 + "github.com/ipfs/go-ipfs/core"
17 + "github.com/ipfs/go-ipfs/core/commands/cmdenv"
18 + "github.com/ipfs/go-ipfs/core/commands/e"
19 +)
20 +
21 +// time format that works in filenames on windows.
22 +var timeFormat = strings.ReplaceAll(time.RFC3339, ":", "_")
23 +
24 +type profileResult struct {
25 + File string
26 +}
27 +
28 +const cpuProfileTimeOption = "cpu-profile-time"
29 +
30 +var sysProfileCmd = &cmds.Command{
31 + NoLocal: true,
32 + Options: []cmds.Option{
33 + cmds.StringOption(outputOptionName, "o", "The path where the output should be stored."),
34 + cmds.StringOption(cpuProfileTimeOption, "The amount of time spent profiling CPU usage.").WithDefault("30s"),
35 + },
36 + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
37 + cpuProfileTimeStr, _ := req.Options[cpuProfileTimeOption].(string)
38 + cpuProfileTime, err := time.ParseDuration(cpuProfileTimeStr)
39 + if err != nil {
40 + return fmt.Errorf("failed to parse CPU profile duration %q: %w", cpuProfileTimeStr, err)
41 + }
42 +
43 + nd, err := cmdenv.GetNode(env)
44 + if err != nil {
45 + return err
46 + }
47 +
48 + r, w := io.Pipe()
49 + go func() {
50 + _ = w.CloseWithError(writeProfiles(req.Context, nd, cpuProfileTime, w))
51 + }()
52 + return res.Emit(r)
53 + },
54 + PostRun: cmds.PostRunMap{
55 + cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error {
56 + v, err := res.Next()
57 + if err != nil {
58 + return err
59 + }
60 +
61 + outReader, ok := v.(io.Reader)
62 + if !ok {
63 + return e.New(e.TypeErr(outReader, v))
64 + }
65 +
66 + outPath, _ := res.Request().Options[outputOptionName].(string)
67 + if outPath == "" {
68 + outPath = "ipfs-profile-" + time.Now().Format(timeFormat) + ".zip"
69 + }
70 + fi, err := os.Create(outPath)
71 + if err != nil {
72 + return err
73 + }
74 + defer fi.Close()
75 +
76 + _, err = io.Copy(fi, outReader)
77 + if err != nil {
78 + return err
79 + }
80 + return re.Emit(&profileResult{File: outPath})
81 + },
82 + },
83 + Encoders: cmds.EncoderMap{
84 + cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *profileResult) error {
85 + fmt.Fprintf(w, "Wrote profiles to: %s\n", out.File)
86 + return nil
87 + }),
88 + },
89 +}
90 +
91 +func writeProfiles(ctx context.Context, nd *core.IpfsNode, cpuProfileTime time.Duration, w io.Writer) error {
92 + archive := zip.NewWriter(w)
93 +
94 + // Take some profiles.
95 + type profile struct {
96 + name string
97 + file string
98 + debug int
99 + }
100 +
101 + profiles := []profile{{
102 + name: "goroutine",
103 + file: "goroutines.stacks",
104 + debug: 2,
105 + }, {
106 + name: "goroutine",
107 + file: "goroutines.pprof",
108 + }, {
109 + name: "heap",
110 + file: "heap.pprof",
111 + }}
112 +
113 + for _, profile := range profiles {
114 + prof := pprof.Lookup(profile.name)
115 + out, err := archive.Create(profile.file)
116 + if err != nil {
117 + return err
118 + }
119 + err = prof.WriteTo(out, profile.debug)
120 + if err != nil {
121 + return err
122 + }
123 + }
124 +
125 + // Take a CPU profile.
126 + if cpuProfileTime != 0 {
127 + out, err := archive.Create("cpu.pprof")
128 + if err != nil {
129 + return err
130 + }
131 +
132 + err = writeCPUProfile(ctx, cpuProfileTime, out)
133 + if err != nil {
134 + return err
135 + }
136 + }
137 +
138 + // Collect info
139 + {
140 + out, err := archive.Create("sysinfo.json")
141 + if err != nil {
142 + return err
143 + }
144 + info, err := getInfo(nd)
145 + if err != nil {
146 + return err
147 + }
148 + err = json.NewEncoder(out).Encode(info)
149 + if err != nil {
150 + return err
151 + }
152 + }
153 +
154 + // Collect binary
155 + if fi, err := openIPFSBinary(); err == nil {
156 + fname := "ipfs"
157 + if runtime.GOOS == "windows" {
158 + fname += ".exe"
159 + }
160 +
161 + out, err := archive.Create(fname)
162 + if err != nil {
163 + return err
164 + }
165 +
166 + _, err = io.Copy(out, fi)
167 + _ = fi.Close()
168 + if err != nil {
169 + return err
170 + }
171 + }
172 + return archive.Close()
173 +}
174 +
175 +func writeCPUProfile(ctx context.Context, d time.Duration, w io.Writer) error {
176 + if err := pprof.StartCPUProfile(w); err != nil {
177 + return err
178 + }
179 + defer pprof.StopCPUProfile()
180 +
181 + timer := time.NewTimer(d)
182 + defer timer.Stop()
183 +
184 + select {
185 + case <-timer.C:
186 + case <-ctx.Done():
187 + return ctx.Err()
188 + }
189 + return nil
190 +}
191 +
192 +func openIPFSBinary() (*os.File, error) {
193 + if runtime.GOOS == "linux" {
194 + pid := os.Getpid()
195 + fi, err := os.Open(fmt.Sprintf("/proc/%d/exe", pid))
196 + if err == nil {
197 + return fi, nil
198 + }
199 + }
200 + path, err := os.Executable()
201 + if err != nil {
202 + return nil, err
203 + }
204 + return os.Open(path)
205 +}
core/commands/sysdiag.go
+33 -23
@@ -6,6 +6,7 @@ import (
6 "runtime"
7
8 version "github.com/ipfs/go-ipfs"
9 + "github.com/ipfs/go-ipfs/core"
10 cmdenv "github.com/ipfs/go-ipfs/core/commands/cmdenv"
11
12 cmds "github.com/ipfs/go-ipfs-cmds"
@@ -21,40 +22,49 @@ Prints out information about your computer to aid in easier debugging.
22 `,
23 },
24 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
24 - info := make(map[string]interface{})
25 - err := runtimeInfo(info)
25 + nd, err := cmdenv.GetNode(env)
26 if err != nil {
27 return err
28 }
29
30 - err = envVarInfo(info)
30 + info, err := getInfo(nd)
31 if err != nil {
32 return err
33 }
34 + return cmds.EmitOnce(res, info)
35 + },
36 +}
37
35 - err = diskSpaceInfo(info)
36 - if err != nil {
37 - return err
38 - }
38 +func getInfo(nd *core.IpfsNode) (map[string]interface{}, error) {
39 + info := make(map[string]interface{})
40 + err := runtimeInfo(info)
41 + if err != nil {
42 + return nil, err
43 + }
44
40 - err = memInfo(info)
41 - if err != nil {
42 - return err
43 - }
44 - nd, err := cmdenv.GetNode(env)
45 - if err != nil {
46 - return err
47 - }
45 + err = envVarInfo(info)
46 + if err != nil {
47 + return nil, err
48 + }
49
49 - err = netInfo(nd.IsOnline, info)
50 - if err != nil {
51 - return err
52 - }
50 + err = diskSpaceInfo(info)
51 + if err != nil {
52 + return nil, err
53 + }
54
54 - info["ipfs_version"] = version.CurrentVersionNumber
55 - info["ipfs_commit"] = version.CurrentCommit
56 - return cmds.EmitOnce(res, info)
57 - },
55 + err = memInfo(info)
56 + if err != nil {
57 + return nil, err
58 + }
59 +
60 + err = netInfo(nd.IsOnline, info)
61 + if err != nil {
62 + return nil, err
63 + }
64 +
65 + info["ipfs_version"] = version.CurrentVersionNumber
66 + info["ipfs_commit"] = version.CurrentCommit
67 + return info, nil
68 }
69
70 func runtimeInfo(out map[string]interface{}) error {
docs/debug-guide.md
+2 -2
@@ -15,8 +15,8 @@ When you see ipfs doing something (using lots of CPU, memory, or otherwise
15 being weird), the first thing you want to do is gather all the relevant
16 profiling information.
17
18 -There's a script (`bin/collect-profiles.sh`) that will do this for you and
19 -bundle the results up into a tarball, ready to be attached to a bug report.
18 +There's a command (`ipfs diag profile`) that will do this for you and
19 +bundle the results up into a zip file, ready to be attached to a bug report.
20
21 If you feel intrepid, you can dump this information and investigate it yourself:
22
test/sharness/t0152-profile.sh new
+40
@@ -0,0 +1,40 @@
1 +#!/usr/bin/env bash
2 +#
3 +# Copyright (c) 2016 Jeromy Johnson
4 +# MIT Licensed; see the LICENSE file in this repository.
5 +#
6 +
7 +test_description="Test profile collection"
8 +
9 +. lib/test-lib.sh
10 +
11 +test_init_ipfs
12 +
13 +test_expect_success "profiling requires a running daemon" '
14 + test_must_fail ipfs diag profile
15 +'
16 +
17 +test_launch_ipfs_daemon
18 +
19 +test_expect_success "test profiling" '
20 + ipfs diag profile --cpu-profile-time=1s > cmd_out
21 +'
22 +
23 +test_expect_success "filename shows up in output" '
24 + grep -q "ipfs-profile" cmd_out > /dev/null
25 +'
26 +
27 +test_expect_success "profile file created" '
28 + test -e "$(sed -n -e "s/.*\(ipfs-profile.*\.zip\)/\1/p" cmd_out)"
29 +'
30 +
31 +test_expect_success "test profiling with -o (without CPU profiling)" '
32 + ipfs diag profile --cpu-profile-time=0 -o test-profile.zip
33 +'
34 +
35 +test_expect_success "test that test-profile.zip exists" '
36 + test -e test-profile.zip
37 +'
38 +
39 +test_kill_ipfs_daemon
40 +test_done