From bb68a685253f77fdf49475cb963ee1995c93657f Mon Sep 17 00:00:00 2001 From: Gus Eggert Date: Tue, 12 Apr 2022 11:58:03 -0400 Subject: [PATCH] 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 --- bin/collect-profiles.sh | 53 ------- cmd/ipfs/debug.go | 4 +- core/commands/profile.go | 217 +++++++------------------- core/commands/root.go | 2 +- core/commands/version.go | 26 +--- profile/goroutines.go | 27 ++++ profile/profile.go | 268 +++++++++++++++++++++++++++++++++ profile/profile_test.go | 172 +++++++++++++++++++++ test/sharness/t0152-profile.sh | 28 +++- version.go | 25 +++ 10 files changed, 574 insertions(+), 248 deletions(-) delete mode 100755 bin/collect-profiles.sh create mode 100644 profile/goroutines.go create mode 100644 profile/profile.go create mode 100644 profile/profile_test.go diff --git a/bin/collect-profiles.sh b/bin/collect-profiles.sh deleted file mode 100755 index 25032a4655b..00000000000 --- a/bin/collect-profiles.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -# collect-profiles.sh -# -# Collects go profile information from a running `ipfs` daemon. -# Creates an archive including the profiles, profile graph svgs, -# ...and where available, a copy of the `ipfs` binary on the PATH. -# -# Please run this script and attach the profile archive it creates -# when reporting bugs at https://github.com/ipfs/go-ipfs/issues - -set -euo pipefail -IFS=$'\n\t' - -SOURCE_URL="${1:-http://127.0.0.1:5001}" -tmpdir=$(mktemp -d) -export PPROF_TMPDIR="$tmpdir" -pushd "$tmpdir" > /dev/null - -if command -v ipfs > /dev/null 2>&1; then - cp "$(command -v ipfs)" ipfs -fi - -echo Collecting goroutine stacks -curl -s -o goroutines.stacks "$SOURCE_URL"'/debug/pprof/goroutine?debug=2' - -curl -s -o goroutines.stacks.full "$SOURCE_URL"'/debug/stack' - -echo Collecting goroutine profile -go tool pprof -symbolize=remote -svg -output goroutine.svg "$SOURCE_URL/debug/pprof/goroutine" - -echo Collecting heap profile -go tool pprof -symbolize=remote -svg -output heap.svg "$SOURCE_URL/debug/pprof/heap" - -echo "Collecting cpu profile (~30s)" -go tool pprof -symbolize=remote -svg -output cpu.svg "$SOURCE_URL/debug/pprof/profile" - -echo "Enabling mutex profiling" -curl -X POST "$SOURCE_URL"'/debug/pprof-mutex/?fraction=4' - -echo "Waiting for mutex data to be updated (30s)" -sleep 30 -curl -s -o mutex.txt "$SOURCE_URL"'/debug/pprof/mutex?debug=2' -go tool pprof -symbolize=remote -svg -output mutex.svg "$SOURCE_URL/debug/pprof/mutex" - -echo "Disabling mutex profiling" -curl -X POST "$SOURCE_URL"'/debug/pprof-mutex/?fraction=0' - -OUTPUT_NAME=ipfs-profile-$(uname -n)-$(date +'%Y-%m-%dT%H:%M:%S%z').tar.gz -echo "Creating $OUTPUT_NAME" -popd > /dev/null -tar czf "./$OUTPUT_NAME" -C "$tmpdir" . -rm -rf "$tmpdir" diff --git a/cmd/ipfs/debug.go b/cmd/ipfs/debug.go index d3fa90940b9..5eeb590e2c5 100644 --- a/cmd/ipfs/debug.go +++ b/cmd/ipfs/debug.go @@ -3,13 +3,13 @@ package main import ( "net/http" - "github.com/ipfs/go-ipfs/core/commands" + "github.com/ipfs/go-ipfs/profile" ) func init() { http.HandleFunc("/debug/stack", func(w http.ResponseWriter, _ *http.Request) { - _ = commands.WriteAllGoroutineStacks(w) + _ = profile.WriteAllGoroutineStacks(w) }, ) } diff --git a/core/commands/profile.go b/core/commands/profile.go index f92c3169a7c..a5567fc814b 100644 --- a/core/commands/profile.go +++ b/core/commands/profile.go @@ -2,18 +2,15 @@ package commands import ( "archive/zip" - "context" - "encoding/json" "fmt" "io" "os" - "runtime" - "runtime/pprof" "strings" "time" cmds "github.com/ipfs/go-ipfs-cmds" "github.com/ipfs/go-ipfs/core/commands/e" + "github.com/ipfs/go-ipfs/profile" ) // time format that works in filenames on windows. @@ -23,22 +20,27 @@ type profileResult struct { File string } -const cpuProfileTimeOption = "cpu-profile-time" +const ( + collectorsOptionName = "collectors" + profileTimeOption = "profile-time" + mutexProfileFractionOption = "mutex-profile-fraction" + blockProfileRateOption = "block-profile-rate" +) var sysProfileCmd = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "Collect a performance profile for debugging.", ShortDescription: ` -Collects cpu, heap, and goroutine profiles from a running go-ipfs daemon -into a single zip file. To aid in debugging, this command also attempts to -include a copy of the running go-ipfs binary. +Collects profiles from a running go-ipfs daemon into a single zip file. +To aid in debugging, this command also attempts to include a copy of +the running go-ipfs binary. `, LongDescription: ` -Collects cpu, heap, and goroutine profiles from a running go-ipfs daemon -into a single zipfile. To aid in debugging, this command also attempts to -include a copy of the running go-ipfs binary. +Collects profiles from a running go-ipfs daemon into a single zipfile. +To aid in debugging, this command also attempts to include a copy of +the running go-ipfs binary. -Profile's can be examined using 'go tool pprof', some tips can be found at +Profiles can be examined using 'go tool pprof', some tips can be found at https://github.com/ipfs/go-ipfs/blob/master/docs/debug-guide.md. Privacy Notice: @@ -48,6 +50,8 @@ The output file includes: - A list of running goroutines. - A CPU profile. - A heap profile. +- A mutex profile. +- A block profile. - Your copy of go-ipfs. - The output of 'ipfs version --all'. @@ -68,19 +72,51 @@ However, it could reveal: }, NoLocal: true, Options: []cmds.Option{ - cmds.StringOption(outputOptionName, "o", "The path where the output should be stored."), - cmds.StringOption(cpuProfileTimeOption, "The amount of time spent profiling CPU usage.").WithDefault("30s"), + cmds.StringOption(outputOptionName, "o", "The path where the output .zip should be stored. Default: ./ipfs-profile-[timestamp].zip"), + cmds.DelimitedStringsOption(",", collectorsOptionName, "The list of collectors to use for collecting diagnostic data."). + WithDefault([]string{ + profile.CollectorGoroutinesStack, + profile.CollectorGoroutinesPprof, + profile.CollectorVersion, + profile.CollectorHeap, + profile.CollectorBin, + profile.CollectorCPU, + profile.CollectorMutex, + profile.CollectorBlock, + }), + cmds.StringOption(profileTimeOption, "The amount of time spent profiling. If this is set to 0, then sampling profiles are skipped.").WithDefault("30s"), + cmds.IntOption(mutexProfileFractionOption, "The fraction 1/n of mutex contention events that are reported in the mutex profile.").WithDefault(4), + cmds.StringOption(blockProfileRateOption, "The duration to wait between sampling goroutine-blocking events for the blocking profile.").WithDefault("1ms"), }, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { - cpuProfileTimeStr, _ := req.Options[cpuProfileTimeOption].(string) - cpuProfileTime, err := time.ParseDuration(cpuProfileTimeStr) + collectors := req.Options[collectorsOptionName].([]string) + + profileTimeStr, _ := req.Options[profileTimeOption].(string) + profileTime, err := time.ParseDuration(profileTimeStr) if err != nil { - return fmt.Errorf("failed to parse CPU profile duration %q: %w", cpuProfileTimeStr, err) + return fmt.Errorf("failed to parse profile duration %q: %w", profileTimeStr, err) } + blockProfileRateStr, _ := req.Options[blockProfileRateOption].(string) + blockProfileRate, err := time.ParseDuration(blockProfileRateStr) + if err != nil { + return fmt.Errorf("failed to parse block profile rate %q: %w", blockProfileRateStr, err) + } + + mutexProfileFraction, _ := req.Options[mutexProfileFractionOption].(int) + r, w := io.Pipe() + go func() { - _ = w.CloseWithError(writeProfiles(req.Context, cpuProfileTime, w)) + archive := zip.NewWriter(w) + err = profile.WriteProfiles(req.Context, archive, profile.Options{ + Collectors: collectors, + ProfileDuration: profileTime, + MutexProfileFraction: mutexProfileFraction, + BlockProfileRate: blockProfileRate, + }) + archive.Close() + _ = w.CloseWithError(err) }() return res.Emit(r) }, @@ -120,148 +156,3 @@ However, it could reveal: }), }, } - -func WriteAllGoroutineStacks(w io.Writer) error { - // this is based on pprof.writeGoroutineStacks, and removes the 64 MB limit - buf := make([]byte, 1<<20) - for i := 0; ; i++ { - n := runtime.Stack(buf, true) - if n < len(buf) { - buf = buf[:n] - break - } - // if len(buf) >= 64<<20 { - // // Filled 64 MB - stop there. - // break - // } - buf = make([]byte, 2*len(buf)) - } - _, err := w.Write(buf) - return err -} - -func writeProfiles(ctx context.Context, cpuProfileTime time.Duration, w io.Writer) error { - archive := zip.NewWriter(w) - - // Take some profiles. - type profile struct { - name string - file string - debug int - } - - profiles := []profile{{ - name: "goroutine", - file: "goroutines.stacks", - debug: 2, - }, { - name: "goroutine", - file: "goroutines.pprof", - }, { - name: "heap", - file: "heap.pprof", - }} - - { - out, err := archive.Create("goroutines-all.stacks") - if err != nil { - return err - } - err = WriteAllGoroutineStacks(out) - if err != nil { - return err - } - } - - for _, profile := range profiles { - prof := pprof.Lookup(profile.name) - out, err := archive.Create(profile.file) - if err != nil { - return err - } - err = prof.WriteTo(out, profile.debug) - if err != nil { - return err - } - } - - // Take a CPU profile. - if cpuProfileTime != 0 { - out, err := archive.Create("cpu.pprof") - if err != nil { - return err - } - - err = writeCPUProfile(ctx, cpuProfileTime, out) - if err != nil { - return err - } - } - - // Collect version info - // I'd use diag sysinfo, but that includes some more sensitive information - // (GOPATH, etc.). - { - out, err := archive.Create("version.json") - if err != nil { - return err - } - - err = json.NewEncoder(out).Encode(getVersionInfo()) - if err != nil { - return err - } - } - - // Collect binary - if fi, err := openIPFSBinary(); err == nil { - fname := "ipfs" - if runtime.GOOS == "windows" { - fname += ".exe" - } - - out, err := archive.Create(fname) - if err != nil { - return err - } - - _, err = io.Copy(out, fi) - _ = fi.Close() - if err != nil { - return err - } - } - return archive.Close() -} - -func writeCPUProfile(ctx context.Context, d time.Duration, w io.Writer) error { - if err := pprof.StartCPUProfile(w); err != nil { - return err - } - defer pprof.StopCPUProfile() - - timer := time.NewTimer(d) - defer timer.Stop() - - select { - case <-timer.C: - case <-ctx.Done(): - return ctx.Err() - } - return nil -} - -func openIPFSBinary() (*os.File, error) { - if runtime.GOOS == "linux" { - pid := os.Getpid() - fi, err := os.Open(fmt.Sprintf("/proc/%d/exe", pid)) - if err == nil { - return fi, nil - } - } - path, err := os.Executable() - if err != nil { - return nil, err - } - return os.Open(path) -} diff --git a/core/commands/root.go b/core/commands/root.go index 64b9b42fbf0..3fe31a8587a 100644 --- a/core/commands/root.go +++ b/core/commands/root.go @@ -67,13 +67,13 @@ NETWORK COMMANDS swarm Manage connections to the p2p network dht Query the DHT for values or peers ping Measure the latency of a connection - diag Print diagnostics bitswap Inspect bitswap state pubsub Send and receive messages via pubsub TOOL COMMANDS config Manage configuration version Show IPFS version information + diag Generate diagnostic reports update Download and apply go-ipfs updates commands List all available commands log Manage and show logs of running daemon diff --git a/core/commands/version.go b/core/commands/version.go index ecdda594b2d..70686da24dc 100644 --- a/core/commands/version.go +++ b/core/commands/version.go @@ -4,23 +4,13 @@ import ( "errors" "fmt" "io" - "runtime" "runtime/debug" version "github.com/ipfs/go-ipfs" - fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo" cmds "github.com/ipfs/go-ipfs-cmds" ) -type VersionOutput struct { - Version string - Commit string - Repo string - System string - Golang string -} - const ( versionNumberOptionName = "number" versionCommitOptionName = "commit" @@ -28,16 +18,6 @@ const ( versionAllOptionName = "all" ) -func getVersionInfo() *VersionOutput { - return &VersionOutput{ - Version: version.CurrentVersionNumber, - Commit: version.CurrentCommit, - Repo: fmt.Sprint(fsrepo.RepoVersion), - System: runtime.GOARCH + "/" + runtime.GOOS, //TODO: Precise version here - Golang: runtime.Version(), - } -} - var VersionCmd = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "Show IPFS version information.", @@ -56,10 +36,10 @@ var VersionCmd = &cmds.Command{ // must be permitted to run before init Extra: CreateCmdExtras(SetDoesNotUseRepo(true), SetDoesNotUseConfigAsInput(true)), Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { - return cmds.EmitOnce(res, getVersionInfo()) + return cmds.EmitOnce(res, version.GetVersionInfo()) }, Encoders: cmds.EncoderMap{ - cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, version *VersionOutput) error { + cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, version *version.VersionInfo) error { all, _ := req.Options[versionAllOptionName].(bool) if all { ver := version.Version @@ -95,7 +75,7 @@ var VersionCmd = &cmds.Command{ return nil }), }, - Type: VersionOutput{}, + Type: version.VersionInfo{}, } type Dependency struct { diff --git a/profile/goroutines.go b/profile/goroutines.go new file mode 100644 index 00000000000..be227af488d --- /dev/null +++ b/profile/goroutines.go @@ -0,0 +1,27 @@ +package profile + +import ( + "io" + "runtime" +) + +// WriteAllGoroutineStacks writes a stack trace to the given writer. +// This is distinct from the Go-provided method because it does not truncate after 64 MB. +func WriteAllGoroutineStacks(w io.Writer) error { + // this is based on pprof.writeGoroutineStacks, and removes the 64 MB limit + buf := make([]byte, 1<<20) + for i := 0; ; i++ { + n := runtime.Stack(buf, true) + if n < len(buf) { + buf = buf[:n] + break + } + // if len(buf) >= 64<<20 { + // // Filled 64 MB - stop there. + // break + // } + buf = make([]byte, 2*len(buf)) + } + _, err := w.Write(buf) + return err +} diff --git a/profile/profile.go b/profile/profile.go new file mode 100644 index 00000000000..06cd4a7ba33 --- /dev/null +++ b/profile/profile.go @@ -0,0 +1,268 @@ +package profile + +import ( + "archive/zip" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "os" + "runtime" + "runtime/pprof" + "sync" + "time" + + version "github.com/ipfs/go-ipfs" + "github.com/ipfs/go-log" +) + +const ( + CollectorGoroutinesStack = "goroutines-stack" + CollectorGoroutinesPprof = "goroutines-pprof" + CollectorVersion = "version" + CollectorHeap = "heap" + CollectorBin = "bin" + CollectorCPU = "cpu" + CollectorMutex = "mutex" + CollectorBlock = "block" +) + +var ( + logger = log.Logger("profile") + goos = runtime.GOOS +) + +type collector struct { + outputFile string + isExecutable bool + collectFunc func(ctx context.Context, opts Options, writer io.Writer) error + enabledFunc func(opts Options) bool +} + +func (p *collector) outputFileName() string { + fName := p.outputFile + if p.isExecutable { + if goos == "windows" { + fName += ".exe" + } + } + return fName +} + +var collectors = map[string]collector{ + CollectorGoroutinesStack: { + outputFile: "goroutines.stacks", + collectFunc: goroutineStacksText, + enabledFunc: func(opts Options) bool { return true }, + }, + CollectorGoroutinesPprof: { + outputFile: "goroutines.pprof", + collectFunc: goroutineStacksProto, + enabledFunc: func(opts Options) bool { return true }, + }, + CollectorVersion: { + outputFile: "version.json", + collectFunc: versionInfo, + enabledFunc: func(opts Options) bool { return true }, + }, + CollectorHeap: { + outputFile: "heap.pprof", + collectFunc: heapProfile, + enabledFunc: func(opts Options) bool { return true }, + }, + CollectorBin: { + outputFile: "ipfs", + isExecutable: true, + collectFunc: binary, + enabledFunc: func(opts Options) bool { return true }, + }, + CollectorCPU: { + outputFile: "cpu.pprof", + collectFunc: profileCPU, + enabledFunc: func(opts Options) bool { return opts.ProfileDuration > 0 }, + }, + CollectorMutex: { + outputFile: "mutex.pprof", + collectFunc: mutexProfile, + enabledFunc: func(opts Options) bool { return opts.ProfileDuration > 0 && opts.MutexProfileFraction > 0 }, + }, + CollectorBlock: { + outputFile: "block.pprof", + collectFunc: blockProfile, + enabledFunc: func(opts Options) bool { return opts.ProfileDuration > 0 && opts.BlockProfileRate > 0 }, + }, +} + +type Options struct { + Collectors []string + ProfileDuration time.Duration + MutexProfileFraction int + BlockProfileRate time.Duration +} + +func WriteProfiles(ctx context.Context, archive *zip.Writer, opts Options) error { + p := profiler{ + archive: archive, + opts: opts, + } + return p.runProfile(ctx) +} + +// profiler runs the collectors concurrently and writes the results to the zip archive. +type profiler struct { + archive *zip.Writer + opts Options +} + +func (p *profiler) runProfile(ctx context.Context) error { + type profileResult struct { + fName string + buf *bytes.Buffer + err error + } + + ctx, cancelFn := context.WithCancel(ctx) + defer cancelFn() + + var collectorsToRun []collector + for _, name := range p.opts.Collectors { + c, ok := collectors[name] + if !ok { + return fmt.Errorf("unknown collector '%s'", name) + } + collectorsToRun = append(collectorsToRun, c) + } + + results := make(chan profileResult, len(p.opts.Collectors)) + wg := sync.WaitGroup{} + for _, c := range collectorsToRun { + if !c.enabledFunc(p.opts) { + continue + } + + fName := c.outputFileName() + + wg.Add(1) + go func(c collector) { + defer wg.Done() + logger.Infow("collecting profile", "File", fName) + defer logger.Infow("profile done", "File", fName) + b := bytes.Buffer{} + err := c.collectFunc(ctx, p.opts, &b) + if err != nil { + select { + case results <- profileResult{err: fmt.Errorf("generating profile data for %q: %w", fName, err)}: + case <-ctx.Done(): + return + } + } + select { + case results <- profileResult{buf: &b, fName: fName}: + case <-ctx.Done(): + } + }(c) + } + go func() { + wg.Wait() + close(results) + }() + + for res := range results { + if res.err != nil { + return res.err + } + out, err := p.archive.Create(res.fName) + if err != nil { + return fmt.Errorf("creating output file %q: %w", res.fName, err) + } + _, err = io.Copy(out, res.buf) + if err != nil { + return fmt.Errorf("compressing result %q: %w", res.fName, err) + } + } + + return nil +} + +func goroutineStacksText(ctx context.Context, _ Options, w io.Writer) error { + return WriteAllGoroutineStacks(w) +} + +func goroutineStacksProto(ctx context.Context, _ Options, w io.Writer) error { + return pprof.Lookup("goroutine").WriteTo(w, 0) +} + +func heapProfile(ctx context.Context, _ Options, w io.Writer) error { + return pprof.Lookup("heap").WriteTo(w, 0) +} + +func versionInfo(ctx context.Context, _ Options, w io.Writer) error { + return json.NewEncoder(w).Encode(version.GetVersionInfo()) +} + +func binary(ctx context.Context, _ Options, w io.Writer) error { + var ( + path string + err error + ) + if goos == "linux" { + pid := os.Getpid() + path = fmt.Sprintf("/proc/%d/exe", pid) + } else { + path, err = os.Executable() + if err != nil { + return fmt.Errorf("finding binary path: %w", err) + } + } + fi, err := os.Open(path) + if err != nil { + return fmt.Errorf("opening binary %q: %w", path, err) + } + _, err = io.Copy(w, fi) + _ = fi.Close() + if err != nil { + return fmt.Errorf("copying binary %q: %w", path, err) + } + return nil +} + +func mutexProfile(ctx context.Context, opts Options, w io.Writer) error { + prev := runtime.SetMutexProfileFraction(opts.MutexProfileFraction) + defer runtime.SetMutexProfileFraction(prev) + err := waitOrCancel(ctx, opts.ProfileDuration) + if err != nil { + return err + } + return pprof.Lookup("mutex").WriteTo(w, 2) +} + +func blockProfile(ctx context.Context, opts Options, w io.Writer) error { + runtime.SetBlockProfileRate(int(opts.BlockProfileRate.Nanoseconds())) + defer runtime.SetBlockProfileRate(0) + err := waitOrCancel(ctx, opts.ProfileDuration) + if err != nil { + return err + } + return pprof.Lookup("block").WriteTo(w, 2) +} + +func profileCPU(ctx context.Context, opts Options, w io.Writer) error { + err := pprof.StartCPUProfile(w) + if err != nil { + return err + } + defer pprof.StopCPUProfile() + return waitOrCancel(ctx, opts.ProfileDuration) +} + +func waitOrCancel(ctx context.Context, d time.Duration) error { + timer := time.NewTimer(d) + defer timer.Stop() + select { + case <-timer.C: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/profile/profile_test.go b/profile/profile_test.go new file mode 100644 index 00000000000..8da00d0181a --- /dev/null +++ b/profile/profile_test.go @@ -0,0 +1,172 @@ +package profile + +import ( + "archive/zip" + "bytes" + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProfiler(t *testing.T) { + allCollectors := []string{ + CollectorGoroutinesStack, + CollectorGoroutinesPprof, + CollectorVersion, + CollectorHeap, + CollectorBin, + CollectorCPU, + CollectorMutex, + CollectorBlock, + } + + cases := []struct { + name string + opts Options + goos string + + expectFiles []string + }{ + { + name: "happy case", + opts: Options{ + Collectors: allCollectors, + ProfileDuration: 1 * time.Millisecond, + MutexProfileFraction: 4, + BlockProfileRate: 50 * time.Nanosecond, + }, + expectFiles: []string{ + "goroutines.stacks", + "goroutines.pprof", + "version.json", + "heap.pprof", + "ipfs", + "cpu.pprof", + "mutex.pprof", + "block.pprof", + }, + }, + { + name: "windows", + opts: Options{ + Collectors: allCollectors, + ProfileDuration: 1 * time.Millisecond, + MutexProfileFraction: 4, + BlockProfileRate: 50 * time.Nanosecond, + }, + goos: "windows", + expectFiles: []string{ + "goroutines.stacks", + "goroutines.pprof", + "version.json", + "heap.pprof", + "ipfs.exe", + "cpu.pprof", + "mutex.pprof", + "block.pprof", + }, + }, + { + name: "sampling profiling disabled", + opts: Options{ + Collectors: allCollectors, + MutexProfileFraction: 4, + BlockProfileRate: 50 * time.Nanosecond, + }, + expectFiles: []string{ + "goroutines.stacks", + "goroutines.pprof", + "version.json", + "heap.pprof", + "ipfs", + }, + }, + { + name: "Mutex profiling disabled", + opts: Options{ + Collectors: allCollectors, + ProfileDuration: 1 * time.Millisecond, + BlockProfileRate: 50 * time.Nanosecond, + }, + expectFiles: []string{ + "goroutines.stacks", + "goroutines.pprof", + "version.json", + "heap.pprof", + "ipfs", + "cpu.pprof", + "block.pprof", + }, + }, + { + name: "block profiling disabled", + opts: Options{ + Collectors: allCollectors, + ProfileDuration: 1 * time.Millisecond, + MutexProfileFraction: 4, + BlockProfileRate: 0, + }, + expectFiles: []string{ + "goroutines.stacks", + "goroutines.pprof", + "version.json", + "heap.pprof", + "ipfs", + "cpu.pprof", + "mutex.pprof", + }, + }, + { + name: "single collector", + opts: Options{ + Collectors: []string{CollectorVersion}, + ProfileDuration: 1 * time.Millisecond, + MutexProfileFraction: 4, + BlockProfileRate: 0, + }, + expectFiles: []string{ + "version.json", + }, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if c.goos != "" { + oldGOOS := goos + goos = c.goos + defer func() { goos = oldGOOS }() + } + + buf := &bytes.Buffer{} + archive := zip.NewWriter(buf) + err := WriteProfiles(context.Background(), archive, c.opts) + require.NoError(t, err) + + err = archive.Close() + require.NoError(t, err) + + zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) + require.NoError(t, err) + + for _, f := range zr.File { + logger.Info("zip file: ", f.Name) + } + + require.Equal(t, len(c.expectFiles), len(zr.File)) + + for _, expectedFile := range c.expectFiles { + func() { + f, err := zr.Open(expectedFile) + require.NoError(t, err) + defer f.Close() + fi, err := f.Stat() + require.NoError(t, err) + assert.NotZero(t, fi.Size()) + }() + } + }) + } +} diff --git a/test/sharness/t0152-profile.sh b/test/sharness/t0152-profile.sh index 13b40517b8d..0bc328d2ed2 100755 --- a/test/sharness/t0152-profile.sh +++ b/test/sharness/t0152-profile.sh @@ -16,8 +16,8 @@ test_expect_success "profiling requires a running daemon" ' test_launch_ipfs_daemon -test_expect_success "test profiling (without CPU)" ' - ipfs diag profile --cpu-profile-time=0 > cmd_out +test_expect_success "test profiling (without sampling)" ' + ipfs diag profile --profile-time=0 > cmd_out ' test_expect_success "filename shows up in output" ' @@ -29,12 +29,17 @@ test_expect_success "profile file created" ' ' test_expect_success "test profiling with -o" ' - ipfs diag profile --cpu-profile-time=1s -o test-profile.zip + ipfs diag profile --profile-time=1s -o test-profile.zip ' test_expect_success "test that test-profile.zip exists" ' test -e test-profile.zip ' + +test_expect_success "test profiling with specific collectors" ' + ipfs diag profile --collectors version,goroutines-stack -o test-profile-small.zip +' + test_kill_ipfs_daemon if ! test_have_prereq UNZIP; then @@ -42,7 +47,8 @@ if ! test_have_prereq UNZIP; then fi test_expect_success "unpack profiles" ' - unzip -d profiles test-profile.zip + unzip -d profiles test-profile.zip && + unzip -d profiles-small test-profile-small.zip ' test_expect_success "cpu profile is valid" ' @@ -57,12 +63,22 @@ test_expect_success "goroutines profile is valid" ' go tool pprof -top profiles/ipfs "profiles/goroutines.pprof" | grep -q "Type: goroutine" ' +test_expect_success "mutex profile is valid" ' + go tool pprof -top profiles/ipfs "profiles/mutex.pprof" | grep -q "Type: delay" +' + +test_expect_success "block profile is valid" ' + go tool pprof -top profiles/ipfs "profiles/block.pprof" | grep -q "Type: delay" +' + test_expect_success "goroutines stacktrace is valid" ' grep -q "goroutine" "profiles/goroutines.stacks" ' -test_expect_success "full goroutines stacktrace is valid" ' - grep -q "goroutine" "profiles/goroutines-all.stacks" +test_expect_success "the small profile only contains the requested data" ' + find profiles-small -type f | sort > actual && + echo -e "profiles-small/goroutines.stacks\nprofiles-small/version.json" > expected && + test_cmp expected actual ' test_done diff --git a/version.go b/version.go index c29491a2745..0018a056ecc 100644 --- a/version.go +++ b/version.go @@ -1,5 +1,12 @@ package ipfs +import ( + "fmt" + "runtime" + + "github.com/ipfs/go-ipfs/repo/fsrepo" +) + // CurrentCommit is the current git commit, this is set as a ldflag in the Makefile var CurrentCommit string @@ -27,3 +34,21 @@ var userAgentSuffix string func SetUserAgentSuffix(suffix string) { userAgentSuffix = suffix } + +type VersionInfo struct { + Version string + Commit string + Repo string + System string + Golang string +} + +func GetVersionInfo() *VersionInfo { + return &VersionInfo{ + Version: CurrentVersionNumber, + Commit: CurrentCommit, + Repo: fmt.Sprint(fsrepo.RepoVersion), + System: runtime.GOARCH + "/" + runtime.GOOS, //TODO: Precise version here + Golang: runtime.Version(), + } +}