pax_global_header00006660000000000000000000000064140466136320014517gustar00rootroot0000000000000052 comment=7c5d76b2c0980d726b44dd969b35d35d84ccea59 profile-0.1.1/000077500000000000000000000000001404661363200131565ustar00rootroot00000000000000profile-0.1.1/.github/000077500000000000000000000000001404661363200145165ustar00rootroot00000000000000profile-0.1.1/.github/workflows/000077500000000000000000000000001404661363200165535ustar00rootroot00000000000000profile-0.1.1/.github/workflows/ci.yml000066400000000000000000000026311404661363200176730ustar00rootroot00000000000000name: ci permissions: contents: read on: push: branches: - main pull_request: jobs: test: strategy: matrix: go-version: [1.16.x] platform: [ubuntu-latest, macos-latest, windows-latest] include: - go-version: 1.15.x platform: ubuntu-latest runs-on: ${{ matrix.platform }} steps: - name: Install Go uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8 # v2.1.3 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 with: persist-credentials: false - name: Build run: go build ./... - name: Test run: go test -cover ./... lint: runs-on: ubuntu-latest steps: - name: Configure Go Environment run: | echo GOPATH=${{ runner.workspace }} >> $GITHUB_ENV echo ${{ runner.workspace }}/bin >> $GITHUB_PATH - name: Checkout code uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 with: persist-credentials: false - name: Bootstrap run: ./script/bootstrap - name: Lint run: ./script/lint - name: Generate run: ./script/generate - name: Git Status run: | git diff test -z "$(git status --porcelain)" profile-0.1.1/.golangci.yml000066400000000000000000000016051404661363200155440ustar00rootroot00000000000000linters: disable-all: true enable: - asciicheck - deadcode - depguard - dupl - errcheck - errorlint - exhaustive - exportloopref - forbidigo - forcetypeassert - gci - gocognit - gocritic - gocyclo - godot - godox - gofumpt - goimports - golint - goprintffuncname - gosec - gosimple - govet - importas - ineffassign - lll - makezero - misspell - nakedret - nestif - nilerr - predeclared - revive - staticcheck - structcheck - stylecheck - thelper - typecheck - unconvert - unparam - unused - varcheck - wastedassign - whitespace linters-settings: depguard: list-type: whitelist packages: - github.com/mmcloughlin/profile lll: line-length: 140 tab-width: 4 issues: exclude-use-default: false profile-0.1.1/LICENSE000066400000000000000000000030261404661363200141640ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2013, Dave Cheney Copyright (c) 2021, Michael McLoughlin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. profile-0.1.1/README.md000066400000000000000000000115751404661363200144460ustar00rootroot00000000000000# profile [![Go Reference](https://pkg.go.dev/badge/github.com/mmcloughlin/profile.svg)](https://pkg.go.dev/github.com/mmcloughlin/profile) Simple profiling for Go. * Easy management of Go's built-in [profiling](https://golang.org/pkg/runtime/pprof) and [tracing](https://golang.org/pkg/runtime/trace) * Based on the widely-used [`pkg/profile`](https://github.com/pkg/profile): mostly-compatible API * Supports generating multiple profiles at once * Configurable with [idiomatic flags](#flags): `-cpuprofile`, `-memprofile`, ... just like `go test` * Configurable by [environment variable](#environment): key-value interface like `GODEBUG` ## Install ``` go get github.com/mmcloughlin/profile ``` ## Usage Enabling profiling in your application is as simple as one line at the top of your main function. [embedmd]:# (internal/example/basic/main.go go /import/ /^}/) ```go import "github.com/mmcloughlin/profile" func main() { defer profile.Start().Stop() // ... } ``` This will write a CPU profile to the current directory. Generate multiple profiles by passing options to the `Start` function. [embedmd]:# (internal/example/multi/main.go go /defer.*/) ```go defer profile.Start(profile.CPUProfile, profile.MemProfile).Stop() ``` Profiles can also be configured by the user via [flags](#flags) or [environment variable](#environment), as demonstrated in the examples below. ### Flags The following example shows how to configure `profile` via flags with multiple available profile types. [embedmd]:# (internal/example/flags/main.go /func main/ /^}/) ```go func main() { log.SetPrefix("example: ") log.SetFlags(0) // Setup profiler. p := profile.New( profile.CPUProfile, profile.MemProfile, profile.TraceProfile, ) // Configure flags. n := flag.Int("n", 1000000, "sum the integers 1 to `n`") p.SetFlags(flag.CommandLine) flag.Parse() // Start profiler. defer p.Start().Stop() // Sum 1 to n. sum := 0 for i := 1; i <= *n; i++ { sum += i } log.Printf("sum: %d", sum) } ``` See the registered flags: [embedmd]:# (internal/example/flags/help.err) ```err Usage of example: -cpuprofile file write a cpu profile to file -memprofile file write an allocation profile to file -memprofilerate rate set memory allocation profiling rate (see runtime.MemProfileRate) -n n sum the integers 1 to n (default 1000000) -trace file write an execution trace to file ``` Profile the application with the following flags: [embedmd]:# (internal/example/flags/run.sh sh /.*cpuprofile.*/) ```sh example -n 1000000000 -cpuprofile cpu.out -memprofile mem.out ``` We'll see additional logging in the output, as well as the profiles `cpu.out` and `mem.out` written on exit. [embedmd]:# (internal/example/flags/run.err) ```err example: cpu profile: started example: mem profile: started example: sum: 500000000500000000 example: cpu profile: stopped example: mem profile: stopped ``` ### Environment For a user-facing tool you may not want to expose profiling options via flags. The `profile` package also offers configuration by environment variable, similar to the `GODEBUG` option offered by the Go runtime. [embedmd]:# (internal/example/env/main.go go /.*Setup.*/ /.*Stop.*/) ```go // Setup profiler. defer profile.Start( profile.AllProfiles, profile.ConfigEnvVar("PROFILE"), ).Stop() ``` Now you can enable profiling with an environment variable, as follows: [embedmd]:# (internal/example/env/run.sh sh /.*cpuprofile.*/) ```sh PROFILE=cpuprofile=cpu.out,memprofile=mem.out example -n 1000000000 ``` The output will be just the same as for the previous flags example. Set the environment variable to `help` to get help on available options: [embedmd]:# (internal/example/env/help.sh) ```sh PROFILE=help example ``` In this case you'll see: [embedmd]:# (internal/example/env/help.err) ```err blockprofile=file write a goroutine blocking profile to file blockprofilerate=rate set blocking profile rate (see runtime.SetBlockProfileRate) cpuprofile=file write a cpu profile to file goroutineprofile=file write a running goroutine profile to file memprofile=file write an allocation profile to file memprofilerate=rate set memory allocation profiling rate (see runtime.MemProfileRate) mutexprofile=string write a mutex contention profile to the named file after execution mutexprofilefraction=int if >= 0, calls runtime.SetMutexProfileFraction() threadcreateprofile=file write a thread creation profile to file trace=file write an execution trace to file ``` ## Thanks Thank you to [Dave Cheney](https://dave.cheney.net/) and [contributors](https://github.com/pkg/profile/graphs/contributors) for the excellent [`pkg/profile`](https://github.com/pkg/profile) package, which provided the inspiration and basis for this work. ## License `profile` is available under the [BSD 3-Clause License](LICENSE). The license retains the copyright notice from [`pkg/profile`](https://github.com/pkg/profile). profile-0.1.1/go.mod000066400000000000000000000000571404661363200142660ustar00rootroot00000000000000module github.com/mmcloughlin/profile go 1.15 profile-0.1.1/internal/000077500000000000000000000000001404661363200147725ustar00rootroot00000000000000profile-0.1.1/internal/example/000077500000000000000000000000001404661363200164255ustar00rootroot00000000000000profile-0.1.1/internal/example/basic/000077500000000000000000000000001404661363200175065ustar00rootroot00000000000000profile-0.1.1/internal/example/basic/log.go000066400000000000000000000001311404661363200206110ustar00rootroot00000000000000package main import "log" func init() { log.SetPrefix("example: ") log.SetFlags(0) } profile-0.1.1/internal/example/basic/main.go000066400000000000000000000001551404661363200207620ustar00rootroot00000000000000package main import "github.com/mmcloughlin/profile" func main() { defer profile.Start().Stop() // ... } profile-0.1.1/internal/example/basic/run.err000066400000000000000000000000741404661363200210250ustar00rootroot00000000000000example: cpu profile: started example: cpu profile: stopped profile-0.1.1/internal/example/basic/run.out000066400000000000000000000000001404661363200210310ustar00rootroot00000000000000profile-0.1.1/internal/example/basic/run.sh000066400000000000000000000000261404661363200206440ustar00rootroot00000000000000example rm cpu.pprof profile-0.1.1/internal/example/env/000077500000000000000000000000001404661363200172155ustar00rootroot00000000000000profile-0.1.1/internal/example/env/help.err000066400000000000000000000012351404661363200206600ustar00rootroot00000000000000blockprofile=file write a goroutine blocking profile to file blockprofilerate=rate set blocking profile rate (see runtime.SetBlockProfileRate) cpuprofile=file write a cpu profile to file goroutineprofile=file write a running goroutine profile to file memprofile=file write an allocation profile to file memprofilerate=rate set memory allocation profiling rate (see runtime.MemProfileRate) mutexprofile=string write a mutex contention profile to the named file after execution mutexprofilefraction=int if >= 0, calls runtime.SetMutexProfileFraction() threadcreateprofile=file write a thread creation profile to file trace=file write an execution trace to file profile-0.1.1/internal/example/env/help.out000066400000000000000000000000001404661363200206640ustar00rootroot00000000000000profile-0.1.1/internal/example/env/help.sh000066400000000000000000000000251404661363200204760ustar00rootroot00000000000000PROFILE=help example profile-0.1.1/internal/example/env/main.go000066400000000000000000000006671404661363200205010ustar00rootroot00000000000000package main import ( "flag" "log" "github.com/mmcloughlin/profile" ) func main() { log.SetPrefix("example: ") log.SetFlags(0) // Configure flags. n := flag.Int("n", 1000000, "sum the integers 1 to `n`") flag.Parse() // Setup profiler. defer profile.Start( profile.AllProfiles, profile.ConfigEnvVar("PROFILE"), ).Stop() // Sum 1 to n. sum := 0 for i := 1; i <= *n; i++ { sum += i } log.Printf("sum: %d", sum) } profile-0.1.1/internal/example/env/run.err000066400000000000000000000002311404661363200205270ustar00rootroot00000000000000example: cpu profile: started example: mem profile: started example: sum: 500000000500000000 example: cpu profile: stopped example: mem profile: stopped profile-0.1.1/internal/example/env/run.out000066400000000000000000000000001404661363200205400ustar00rootroot00000000000000profile-0.1.1/internal/example/env/run.sh000066400000000000000000000001301404661363200203470ustar00rootroot00000000000000PROFILE=cpuprofile=cpu.out,memprofile=mem.out example -n 1000000000 rm cpu.out mem.out profile-0.1.1/internal/example/flags/000077500000000000000000000000001404661363200175215ustar00rootroot00000000000000profile-0.1.1/internal/example/flags/help.err000066400000000000000000000005121404661363200211610ustar00rootroot00000000000000Usage of example: -cpuprofile file write a cpu profile to file -memprofile file write an allocation profile to file -memprofilerate rate set memory allocation profiling rate (see runtime.MemProfileRate) -n n sum the integers 1 to n (default 1000000) -trace file write an execution trace to file profile-0.1.1/internal/example/flags/help.out000066400000000000000000000000001404661363200211700ustar00rootroot00000000000000profile-0.1.1/internal/example/flags/help.sh000066400000000000000000000000131404661363200207770ustar00rootroot00000000000000example -h profile-0.1.1/internal/example/flags/main.go000066400000000000000000000010021404661363200207650ustar00rootroot00000000000000package main import ( "flag" "log" "github.com/mmcloughlin/profile" ) func main() { log.SetPrefix("example: ") log.SetFlags(0) // Setup profiler. p := profile.New( profile.CPUProfile, profile.MemProfile, profile.TraceProfile, ) // Configure flags. n := flag.Int("n", 1000000, "sum the integers 1 to `n`") p.SetFlags(flag.CommandLine) flag.Parse() // Start profiler. defer p.Start().Stop() // Sum 1 to n. sum := 0 for i := 1; i <= *n; i++ { sum += i } log.Printf("sum: %d", sum) } profile-0.1.1/internal/example/flags/run.err000066400000000000000000000002311404661363200210330ustar00rootroot00000000000000example: cpu profile: started example: mem profile: started example: sum: 500000000500000000 example: cpu profile: stopped example: mem profile: stopped profile-0.1.1/internal/example/flags/run.out000066400000000000000000000000001404661363200210440ustar00rootroot00000000000000profile-0.1.1/internal/example/flags/run.sh000066400000000000000000000001221404661363200206540ustar00rootroot00000000000000example -n 1000000000 -cpuprofile cpu.out -memprofile mem.out rm cpu.out mem.out profile-0.1.1/internal/example/multi/000077500000000000000000000000001404661363200175575ustar00rootroot00000000000000profile-0.1.1/internal/example/multi/log.go000066400000000000000000000001311404661363200206620ustar00rootroot00000000000000package main import "log" func init() { log.SetPrefix("example: ") log.SetFlags(0) } profile-0.1.1/internal/example/multi/main.go000066400000000000000000000002231404661363200210270ustar00rootroot00000000000000package main import "github.com/mmcloughlin/profile" func main() { defer profile.Start(profile.CPUProfile, profile.MemProfile).Stop() // ... } profile-0.1.1/internal/example/multi/run.err000066400000000000000000000001701404661363200210730ustar00rootroot00000000000000example: cpu profile: started example: mem profile: started example: cpu profile: stopped example: mem profile: stopped profile-0.1.1/internal/example/multi/run.out000066400000000000000000000000001404661363200211020ustar00rootroot00000000000000profile-0.1.1/internal/example/multi/run.sh000066400000000000000000000000401404661363200207110ustar00rootroot00000000000000example rm cpu.pprof mem.pprof profile-0.1.1/method.go000066400000000000000000000164651404661363200150010ustar00rootroot00000000000000package profile import ( "flag" "fmt" "io" "os" "runtime" "runtime/pprof" "runtime/trace" ) // AllProfiles enables all profiling types. Running all profiles at once is // generally not a good idea, so it's recommended that this option is combined // with some configuration mechanism, via flags or otherwise. func AllProfiles(p *Profile) { p.Configure( CPUProfile, MemProfile, GoroutineProfile, ThreadcreationProfile, BlockProfile, MutexProfile, TraceProfile, ) } type method interface { Name() string SetFlags(f *flag.FlagSet) Enabled() bool Start() error Stop() error } // CPUProfile enables cpu profiling. func CPUProfile(p *Profile) { p.addmethod(&cpu{ filename: "cpu.pprof", }) } type cpu struct { filename string f io.WriteCloser } func (cpu) Name() string { return "cpu" } func (c *cpu) SetFlags(f *flag.FlagSet) { // Reference: https://github.com/golang/go/blob/303b194c6daf319f88e56d8ece56d924044f65a8/src/testing/testing.go#L292 // // cpuProfile = flag.String("test.cpuprofile", "", "write a cpu profile to `file`") // f.StringVar(&c.filename, "cpuprofile", "", "write a cpu profile to `file`") } func (c *cpu) Enabled() bool { return c.filename != "" } func (c *cpu) Start() error { // Open output file. f, err := os.Create(c.filename) if err != nil { return err } // Start profile. if err := pprof.StartCPUProfile(f); err != nil { _ = f.Close() // best effort: ignore error since we already have one return err } c.f = f return nil } func (c *cpu) Stop() error { pprof.StopCPUProfile() return c.f.Close() } // MemProfile enables memory profiling. func MemProfile(p *Profile) { p.addmethod(&mem{ filename: "mem.pprof", }) } type mem struct { filename string rate int prevrate int } func (mem) Name() string { return "mem" } func (m *mem) SetFlags(f *flag.FlagSet) { // Reference: https://github.com/golang/go/blob/303b194c6daf319f88e56d8ece56d924044f65a8/src/testing/testing.go#L290-L291 // // memProfile = flag.String("test.memprofile", "", "write an allocation profile to `file`") // memProfileRate = flag.Int("test.memprofilerate", 0, "set memory allocation profiling `rate` (see runtime.MemProfileRate)") // f.StringVar(&m.filename, "memprofile", "", "write an allocation profile to `file`") f.IntVar(&m.rate, "memprofilerate", 0, "set memory allocation profiling `rate` (see runtime.MemProfileRate)") } func (m *mem) Enabled() bool { return m.filename != "" } func (m *mem) Start() error { m.prevrate = runtime.MemProfileRate if m.rate > 0 { runtime.MemProfileRate = m.rate } return nil } func (m *mem) Stop() error { // Materialize all statistics. runtime.GC() // Write to file. err := writeprofile("allocs", m.filename) // Restore profile rate. runtime.MemProfileRate = m.prevrate return err } // GoroutineProfile enables goroutine profiling. func GoroutineProfile(p *Profile) { p.addmethod(&lookup{ name: "goroutine", long: "running goroutine", filename: "goroutine.pprof", }) } // ThreadcreationProfile enables thread creation profiling. func ThreadcreationProfile(p *Profile) { p.addmethod(&lookup{ name: "threadcreate", long: "thread creation", filename: "threadcreate.pprof", }) } type lookup struct { name string long string filename string } func (l *lookup) Name() string { return l.name } func (l *lookup) SetFlags(f *flag.FlagSet) { f.StringVar(&l.filename, l.name+"profile", "", "write a "+l.long+" profile to `file`") } func (l *lookup) Enabled() bool { return l.filename != "" } func (l *lookup) Start() error { return nil } func (l *lookup) Stop() error { return writeprofile(l.name, l.filename) } // BlockProfile enables block (contention) profiling. func BlockProfile(p *Profile) { p.addmethod(&block{ filename: "block.pprof", rate: 1, }) } type block struct { filename string rate int } func (block) Name() string { return "block" } func (b *block) SetFlags(f *flag.FlagSet) { // Reference: https://github.com/golang/go/blob/303b194c6daf319f88e56d8ece56d924044f65a8/src/testing/testing.go#L293-L294 // // blockProfile = flag.String("test.blockprofile", "", "write a goroutine blocking profile to `file`") // blockProfileRate = flag.Int("test.blockprofilerate", 1, "set blocking profile `rate` (see runtime.SetBlockProfileRate)") // f.StringVar(&b.filename, "blockprofile", "", "write a goroutine blocking profile to `file`") f.IntVar(&b.rate, "blockprofilerate", 1, "set blocking profile `rate` (see runtime.SetBlockProfileRate)") } func (b *block) Enabled() bool { return b.filename != "" && b.rate > 0 } func (b *block) Start() error { runtime.SetBlockProfileRate(b.rate) return nil } func (b *block) Stop() error { // Write to file. err := writeprofile("block", b.filename) // Disable block profiling. runtime.SetBlockProfileRate(0) return err } // MutexProfile enables mutex profiling. func MutexProfile(p *Profile) { p.addmethod(&mutex{ filename: "mutex.pprof", rate: 1, }) } type mutex struct { filename string rate int } func (mutex) Name() string { return "mutex" } func (m *mutex) SetFlags(f *flag.FlagSet) { // Reference: https://github.com/golang/go/blob/303b194c6daf319f88e56d8ece56d924044f65a8/src/testing/testing.go#L295-L296 // // mutexProfile = flag.String("test.mutexprofile", "", "write a mutex contention profile to the named file after execution") // mutexProfileFraction = flag.Int("test.mutexprofilefraction", 1, "if >= 0, calls runtime.SetMutexProfileFraction()") // f.StringVar(&m.filename, "mutexprofile", "", "write a mutex contention profile to the named file after execution") f.IntVar(&m.rate, "mutexprofilefraction", 1, "if >= 0, calls runtime.SetMutexProfileFraction()") } func (m *mutex) Enabled() bool { return m.filename != "" && m.rate > 0 } func (m *mutex) Start() error { runtime.SetMutexProfileFraction(m.rate) return nil } func (m *mutex) Stop() error { // Write to file. err := writeprofile("mutex", m.filename) // Disable mutex profiling. runtime.SetMutexProfileFraction(0) return err } // TraceProfile enables execution tracing. func TraceProfile(p *Profile) { p.addmethod(&tracer{ filename: "trace.out", }) } type tracer struct { filename string f io.WriteCloser } func (tracer) Name() string { return "trace" } func (t *tracer) SetFlags(f *flag.FlagSet) { // Reference: https://github.com/golang/go/blob/303b194c6daf319f88e56d8ece56d924044f65a8/src/testing/testing.go#L298 // // traceFile = flag.String("test.trace", "", "write an execution trace to `file`") // f.StringVar(&t.filename, "trace", "", "write an execution trace to `file`") } func (t *tracer) Enabled() bool { return t.filename != "" } func (t *tracer) Start() error { // Open output file. f, err := os.Create(t.filename) if err != nil { return err } // Start trace. if err := trace.Start(f); err != nil { _ = f.Close() // best effort: ignore error since we already have one return err } t.f = f return nil } func (t *tracer) Stop() error { trace.Stop() return t.f.Close() } func writeprofile(name, filename string) (err error) { // Lookup profile. p := pprof.Lookup(name) if p == nil { return fmt.Errorf("unknown profile %q", name) } // Open file. f, err := os.Create(filename) if err != nil { return err } defer func() { if errc := f.Close(); err == nil && errc != nil { err = errc } }() // Write. return p.WriteTo(f, 0) } profile-0.1.1/profile.go000066400000000000000000000075011404661363200151500ustar00rootroot00000000000000// Package profile provides simple profiling for Go applications. package profile import ( "flag" "fmt" "io/ioutil" "log" "os" "os/signal" "strings" ) // Profile represents a profiling session. type Profile struct { methods []method log func(string, ...interface{}) noshutdownhook bool envvar string running []method } // New creates a new profiling session configured with the given options. func New(options ...func(*Profile)) *Profile { p := &Profile{ log: log.Printf, } p.Configure(options...) return p } // Start a new profiling session with the given options. func Start(options ...func(*Profile)) *Profile { return New(options...).Start() } // Configure applies the given options to this profiling session. func (p *Profile) Configure(options ...func(*Profile)) { for _, option := range options { option(p) } } // WithLogger configures informational messages to be logged to the given // logger. Defaults to the standard library global logger. func WithLogger(l *log.Logger) func(p *Profile) { return func(p *Profile) { p.log = l.Printf } } // Quiet suppresses logging. func Quiet(p *Profile) { p.Configure(WithLogger(log.New(ioutil.Discard, "", 0))) } // NoShutdownHook controls whether the profiling session should shutdown on // interrupt. Programs with more sophisticated signal handling should use this // option to disable the default shutdown handler, and ensure the profile Stop() // method is called during shutdown. func NoShutdownHook(p *Profile) { p.noshutdownhook = true } // ConfigEnvVar specifies an environment variable to configure profiles from. func ConfigEnvVar(key string) func(*Profile) { return func(p *Profile) { p.envvar = key } } func (p *Profile) addmethod(m method) { p.methods = append(p.methods, m) } func (p *Profile) setdefaults() { if len(p.methods) == 0 { p.Configure(CPUProfile) } } // SetFlags registers flags to configure this profiling session. This should be // called after all options have been applied. func (p *Profile) SetFlags(f *flag.FlagSet) { p.setdefaults() for _, m := range p.methods { m.SetFlags(f) } } // config configures profiles based on a GODEBUG-like configuration string. func (p *Profile) config(cfg string) { // Convert config string into equivalent command-line arguments and parse them. args := []string{} for _, arg := range strings.Split(cfg, ",") { args = append(args, "-"+arg) } // Register flags on a custom flagset. Register custom usage function that // will output flags in a format closer to the expected format of the // configuration string. f := flag.NewFlagSet("", flag.ExitOnError) p.SetFlags(f) f.Usage = func() { f.VisitAll(func(opt *flag.Flag) { value, usage := flag.UnquoteUsage(opt) fmt.Fprintf(f.Output(), "%s=%s\n\t%s\n", opt.Name, value, usage) }) } // Parse. Discard error because ExitOnError ensures it's handled internally. _ = f.Parse(args) } // Start profiling. func (p *Profile) Start() *Profile { // Set defaults. p.setdefaults() // Optionally configure via environment variable. if p.envvar != "" { p.config(os.Getenv(p.envvar)) } // Start methods. for _, m := range p.methods { if !m.Enabled() { continue } if err := m.Start(); err != nil { p.log("%s profile: error starting: %v", m.Name(), err) continue } p.log("%s profile: started", m.Name()) p.running = append(p.running, m) } // Shutdown hook. if !p.noshutdownhook { go func() { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) s := <-c p.log("caught %v: stopping profiles", s) p.Stop() os.Exit(0) }() } return p } // Stop profiling. func (p *Profile) Stop() { for _, m := range p.running { if err := m.Stop(); err != nil { p.log("%s profile: error stopping: %v", m.Name(), err) } else { p.log("%s profile: stopped", m.Name()) } } p.running = nil } profile-0.1.1/profile_test.go000066400000000000000000000142571404661363200162150ustar00rootroot00000000000000package profile_test import ( "flag" "io" "io/ioutil" "log" "os" "reflect" "sort" "testing" "github.com/mmcloughlin/profile" ) func TestFlagsConfiguration(t *testing.T) { cases := []struct { Name string Options []func(*profile.Profile) Args []string ParseError bool Files []string }{ // Each method on its own. { Name: "cpu", Options: []func(*profile.Profile){profile.CPUProfile}, Args: []string{"-cpuprofile=cpu.out"}, Files: []string{"cpu.out"}, }, { Name: "mem", Options: []func(*profile.Profile){profile.MemProfile}, Args: []string{"-memprofile=mem.out"}, Files: []string{"mem.out"}, }, { Name: "goroutine", Options: []func(*profile.Profile){profile.GoroutineProfile}, Args: []string{"-goroutineprofile=goroutine.out"}, Files: []string{"goroutine.out"}, }, { Name: "threadcreateprofile", Options: []func(*profile.Profile){profile.ThreadcreationProfile}, Args: []string{"-threadcreateprofile=threadcreate.out"}, Files: []string{"threadcreate.out"}, }, { Name: "block", Options: []func(*profile.Profile){profile.BlockProfile}, Args: []string{"-blockprofile=block.out"}, Files: []string{"block.out"}, }, { Name: "mutex", Options: []func(*profile.Profile){profile.MutexProfile}, Args: []string{"-mutexprofile=mutex.out"}, Files: []string{"mutex.out"}, }, { Name: "trace", Options: []func(*profile.Profile){profile.TraceProfile}, Args: []string{"-trace=trace.out"}, Files: []string{"trace.out"}, }, // Defaults: when no options are provided. { Name: "default_noargs", }, { Name: "default_cpu", Args: []string{"-cpuprofile=cpu.out"}, Files: []string{"cpu.out"}, }, // Multi-mode profiling. { Name: "multi_enable_both", Options: []func(*profile.Profile){profile.CPUProfile, profile.MemProfile}, Args: []string{"-cpuprofile=cpu.out", "-memprofile=mem.out"}, Files: []string{"cpu.out", "mem.out"}, }, { Name: "multi_enable_one", Options: []func(*profile.Profile){profile.CPUProfile, profile.MemProfile}, Args: []string{"-memprofile=mem.out"}, Files: []string{"mem.out"}, }, // All. { Name: "all", Options: []func(*profile.Profile){profile.AllProfiles}, Args: []string{ "-cpuprofile=cpu.out", "-memprofile=mem.out", "-goroutineprofile=goroutine.out", "-threadcreateprofile=threadcreate.out", "-blockprofile=block.out", "-mutexprofile=mutex.out", "-trace=trace.out", }, Files: []string{ "cpu.out", "mem.out", "goroutine.out", "threadcreate.out", "block.out", "mutex.out", "trace.out", }, }, } for _, c := range cases { c := c // scopelint t.Run(c.Name, func(t *testing.T) { dir := t.TempDir() Chdir(t, dir) // Initialize. p := profile.New(c.Options...) p.Configure(profile.WithLogger(Logger(t))) // Configure via flags. f := flag.NewFlagSet("profile", flag.ContinueOnError) p.SetFlags(f) err := f.Parse(c.Args) if (err != nil) != c.ParseError { t.Logf("expected parse error: %v", c.ParseError) t.Logf("got: %v", err) t.FailNow() } // Run. p.Start().Stop() // Confirm we have the files we expect. AssertDirContains(t, dir, c.Files) }) } } func TestEnvConfiguration(t *testing.T) { dir := t.TempDir() Chdir(t, dir) // Set the environment variable for this test. key := "PROFILE" Setenv(t, key, "cpuprofile=cpu.out,memprofile=mem.out") // Run profiler. profile.Start( profile.AllProfiles, profile.ConfigEnvVar(key), profile.WithLogger(Logger(t)), ).Stop() // Verify we have what we expect. AssertDirContains(t, dir, []string{"cpu.out", "mem.out"}) } // TestEnvConfigurationEmpty is a regression test for the case where a // configuration environment variable is specified but it's empty or unset. In // this case no profilers should be run. func TestEnvConfigurationEmpty(t *testing.T) { dir := t.TempDir() Chdir(t, dir) // Use a key that should not be set. Bail in the absurdly unlikely case that // it is set. key := "PROFILE_VARIABLE_EMPTY" if os.Getenv(key) != "" { t.FailNow() } // Run profiler. profile.Start( profile.AllProfiles, profile.ConfigEnvVar(key), profile.WithLogger(Logger(t)), ).Stop() // Verify the directory is empty. AssertDirContains(t, dir, nil) } // AssertDirContains asserts that dir contains non-empty files called filenames, // and nothing else. func AssertDirContains(t *testing.T, dir string, filenames []string) { t.Helper() entries, err := ioutil.ReadDir(dir) if err != nil { t.Fatal(err) } var got []string for _, entry := range entries { if !entry.Mode().IsRegular() { t.Errorf("%s is not regular file", entry.Name()) } if entry.Size() == 0 { t.Errorf("file %v is empty", entry.Name()) } got = append(got, entry.Name()) } sort.Strings(got) sort.Strings(filenames) if !reflect.DeepEqual(got, filenames) { t.Logf("expect: %v", filenames) t.Logf(" got: %v", got) t.Error("unexpected file output") } } // Chdir changes into a given directory for the duration of a test. func Chdir(t *testing.T, dir string) { t.Helper() wd, err := os.Getwd() if err != nil { t.Fatal(err) } if err := os.Chdir(dir); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.Chdir(wd); err != nil { t.Fatal(err) } }) } // Setenv sets an environment variable for the duration of a test. func Setenv(t *testing.T, key, value string) { t.Helper() prev, ok := os.LookupEnv(key) t.Cleanup(func() { if ok { if err := os.Setenv(key, prev); err != nil { t.Fatal(err) } } else { if err := os.Unsetenv(key); err != nil { t.Fatal(err) } } }) if err := os.Setenv(key, value); err != nil { t.Fatal(err) } } // Logger builds a logger that writes to the test object. func Logger(tb testing.TB) *log.Logger { tb.Helper() return log.New(Writer(tb), "test: ", 0) } type writer struct { tb testing.TB } // Writer builds a writer that logs all writes to the test object. func Writer(tb testing.TB) io.Writer { tb.Helper() return writer{tb} } func (w writer) Write(p []byte) (n int, err error) { w.tb.Log(string(p)) return len(p), nil } profile-0.1.1/script/000077500000000000000000000000001404661363200144625ustar00rootroot00000000000000profile-0.1.1/script/bootstrap000077500000000000000000000004141404661363200164240ustar00rootroot00000000000000#!/usr/bin/env bash set -exuo pipefail # Install golangci-lint curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b "${GOPATH}/bin" v1.39.0 # Install tools. go install -modfile=script/tools.mod \ github.com/campoy/embedmd profile-0.1.1/script/generate000077500000000000000000000006701404661363200162050ustar00rootroot00000000000000#!/usr/bin/env bash set -exuo pipefail # Generate example output. for example in ./internal/example/*; do # Compile. bindir=$(mktemp -d) go build -o "${bindir}/example" "${example}" for script in ${example}/*.sh; do root="${script%.sh}" PATH="${bindir}:${PATH}" bash "${script}" > "${root}.out" 2> "${root}.err" done # Cleanup. rm -r "${bindir}" done # Generate README. embedmd -w README.md profile-0.1.1/script/lint000077500000000000000000000002631404661363200153570ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail args=() if [[ "${GITHUB_ACTIONS:-false}" == "true" ]]; then args+=("--out-format" "github-actions") fi golangci-lint run "${args[@]}" profile-0.1.1/script/tools.mod000066400000000000000000000001451404661363200163230ustar00rootroot00000000000000module github.com/mmcloughlin/profile go 1.15 require github.com/campoy/embedmd v1.0.0 // indirect profile-0.1.1/script/tools.sum000066400000000000000000000005321404661363200163500ustar00rootroot00000000000000github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=