├── vendor ├── gopkg.in │ └── urfave │ │ └── cli.v1 │ │ ├── .gitignore │ │ ├── appveyor.yml │ │ ├── cli.go │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── category.go │ │ ├── funcs.go │ │ ├── errors.go │ │ ├── runtests │ │ ├── command.go │ │ ├── help.go │ │ └── CHANGELOG.md └── github.com │ ├── dustin │ └── go-humanize │ │ ├── .gitignore │ │ ├── humanize.go │ │ ├── .travis.yml │ │ ├── ordinals.go │ │ ├── ftoa.go │ │ ├── big.go │ │ ├── commaf.go │ │ ├── LICENSE │ │ ├── README.markdown │ │ ├── comma.go │ │ ├── si.go │ │ ├── bytes.go │ │ ├── times.go │ │ ├── bigbytes.go │ │ └── number.go │ └── geckoboard │ └── cli-table │ ├── .gitignore │ ├── circle.yml │ ├── README.md │ └── table.go ├── Godeps ├── Readme └── Godeps.json ├── .gitignore ├── profiler ├── sink.go ├── sink │ ├── discard_test.go │ ├── discard.go │ ├── file_test.go │ └── file.go ├── goid_test.go ├── goid.go ├── profiler_test.go ├── profiler.go ├── profile_test.go └── profile.go ├── cmd ├── display_format.go ├── padded_writer_test.go ├── display_format_test.go ├── display_unit.go ├── table_column_test.go ├── display_unit_test.go ├── padded_writer.go ├── table_column.go ├── profile_test.go ├── print.go ├── print_test.go └── profile.go ├── circle.yml ├── Makefile ├── LICENSE ├── CONTRIBUTING.md ├── tools ├── callgraph_test.go ├── injector.go ├── callgraph.go ├── ssa.go ├── ast.go ├── ast_test.go ├── injector_test.go ├── ssa_test.go ├── package_test.go └── package.go └── main.go /vendor/gopkg.in/urfave/cli.v1/.gitignore: -------------------------------------------------------------------------------- 1 | *.coverprofile 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/.gitignore: -------------------------------------------------------------------------------- 1 | #* 2 | *.[568] 3 | *.a 4 | *~ 5 | [568].out 6 | _* 7 | -------------------------------------------------------------------------------- /Godeps/Readme: -------------------------------------------------------------------------------- 1 | This directory tree is generated automatically by godep. 2 | 3 | Please do not edit. 4 | 5 | See https://github.com/tools/godep for more information. 6 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/humanize.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package humanize converts boring ugly numbers to human-friendly strings and back. 3 | 4 | Durations can be turned into strings such as "3 days ago", numbers 5 | representing sizes like 82854982 into useful strings like, "83MB" or 6 | "79MiB" (whichever you prefer). 7 | */ 8 | package humanize 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /vendor/github.com/geckoboard/cli-table/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /profiler/sink.go: -------------------------------------------------------------------------------- 1 | package profiler 2 | 3 | // Sink defines an interface for processing profile entries emitted by the profiler. 4 | type Sink interface { 5 | // Initialize the sink input channel with the specified buffer capacity. 6 | Open(inputBufferSize int) error 7 | 8 | // Shutdown the sink. 9 | Close() error 10 | 11 | // Get a channel for piping profile entries to the sink. 12 | Input() chan<- *Profile 13 | } 14 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.3.3 5 | - 1.5.4 6 | - 1.6.2 7 | - tip 8 | matrix: 9 | allow_failures: 10 | - go: tip 11 | fast_finish: true 12 | install: 13 | - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). 14 | script: 15 | - go get -t -v ./... 16 | - diff -u <(echo -n) <(gofmt -d -s .) 17 | - go tool vet . 18 | - go test -v -race ./... 19 | -------------------------------------------------------------------------------- /vendor/github.com/geckoboard/cli-table/circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | PATH: ~/.local/bin:$PATH 4 | test: 5 | pre: 6 | - go get github.com/mattn/goveralls 7 | - cd $HOME/.go_project/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME && go vet ./... 8 | override: 9 | - go test -v -cover -race -coverprofile=/home/ubuntu/coverage.out 10 | post: 11 | - /home/ubuntu/.go_workspace/bin/goveralls -coverprofile=/home/ubuntu/coverage.out -service=circle-ci -repotoken=$COVERALLS_TOKEN 12 | -------------------------------------------------------------------------------- /cmd/display_format.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type displayFormat uint8 9 | 10 | const ( 11 | displayTime displayFormat = iota 12 | displayPercent 13 | ) 14 | 15 | func parseDisplayFormat(val string) (displayFormat, error) { 16 | trimmed := strings.TrimSpace(val) 17 | switch trimmed { 18 | case "time": 19 | return displayTime, nil 20 | case "percent": 21 | return displayPercent, nil 22 | } 23 | 24 | return 0, fmt.Errorf("unsupported display format %q", trimmed) 25 | } 26 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/ordinals.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import "strconv" 4 | 5 | // Ordinal gives you the input number in a rank/ordinal format. 6 | // 7 | // Ordinal(3) -> 3rd 8 | func Ordinal(x int) string { 9 | suffix := "th" 10 | switch x % 10 { 11 | case 1: 12 | if x%100 != 11 { 13 | suffix = "st" 14 | } 15 | case 2: 16 | if x%100 != 12 { 17 | suffix = "nd" 18 | } 19 | case 3: 20 | if x%100 != 13 { 21 | suffix = "rd" 22 | } 23 | } 24 | return strconv.Itoa(x) + suffix 25 | } 26 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | PATH: ~/.local/bin:$PATH 4 | test: 5 | pre: 6 | - go get github.com/mattn/goveralls 7 | - cd $HOME/.go_project/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME && go vet ./... 8 | override: 9 | - cd $HOME/.go_project/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME && make test-ci-with-combined-coverage 10 | - /home/ubuntu/.go_workspace/bin/goveralls -coverprofile=/home/ubuntu/profile.cov -service=circle-ci -repotoken=$COVERALLS_TOKEN 11 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/ftoa.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import "strconv" 4 | 5 | func stripTrailingZeros(s string) string { 6 | offset := len(s) - 1 7 | for offset > 0 { 8 | if s[offset] == '.' { 9 | offset-- 10 | break 11 | } 12 | if s[offset] != '0' { 13 | break 14 | } 15 | offset-- 16 | } 17 | return s[:offset+1] 18 | } 19 | 20 | // Ftoa converts a float to a string with no trailing zeros. 21 | func Ftoa(num float64) string { 22 | return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64)) 23 | } 24 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | os: Windows Server 2012 R2 4 | 5 | clone_folder: c:\gopath\src\github.com\urfave\cli 6 | 7 | environment: 8 | GOPATH: C:\gopath 9 | GOVERSION: 1.6 10 | PYTHON: C:\Python27-x64 11 | PYTHON_VERSION: 2.7.x 12 | PYTHON_ARCH: 64 13 | GFMXR_DEBUG: 1 14 | 15 | install: 16 | - set PATH=%GOPATH%\bin;C:\go\bin;%PATH% 17 | - go version 18 | - go env 19 | - go get github.com/urfave/gfmxr/... 20 | - go get -v -t ./... 21 | 22 | build_script: 23 | - python runtests vet 24 | - python runtests test 25 | - python runtests gfmxr 26 | -------------------------------------------------------------------------------- /profiler/sink/discard_test.go: -------------------------------------------------------------------------------- 1 | package sink 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/geckoboard/prism/profiler" 7 | ) 8 | 9 | func TestDiscardSink(t *testing.T) { 10 | s := NewDiscardSink() 11 | err := s.Open(1) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | numEntries := 5 17 | for i := 0; i < numEntries; i++ { 18 | s.Input() <- &profiler.Profile{} 19 | } 20 | 21 | err = s.Close() 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | numDiscarded := s.(*discardSink).numDiscarded 27 | if numDiscarded != numEntries { 28 | t.Errorf("expected sink discarded entry count to be %d; got %d", numEntries, numDiscarded) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/geckoboard/prism", 3 | "GoVersion": "go1.7", 4 | "GodepVersion": "v74", 5 | "Deps": [ 6 | { 7 | "ImportPath": "github.com/dustin/go-humanize", 8 | "Rev": "7a41df006ff9af79a29f0ffa9c5f21fbe6314a2d" 9 | }, 10 | { 11 | "ImportPath": "github.com/geckoboard/cli-table", 12 | "Rev": "ba78d7928542412052e800bbe238b66085289778" 13 | }, 14 | { 15 | "ImportPath": "golang.org/x/crypto/ssh/terminal", 16 | "Rev": "346896d57731cb5670b36c6178fc5519f3225980" 17 | }, 18 | { 19 | "ImportPath": "gopkg.in/urfave/cli.v1", 20 | "Comment": "v1.18.1", 21 | "Rev": "a14d7d367bc02b1f57d88de97926727f2d936387" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=prism 2 | 3 | SHELL = /bin/bash 4 | 5 | .PHONY: all test test-ci 6 | 7 | all: test 8 | 9 | test: 10 | go test `go list ./... | grep -v "vendor"` 11 | 12 | test-ci: 13 | go get -f -u github.com/jstemmer/go-junit-report 14 | go test -v -race `go list ./... | grep -v "vendor"` | tee >(go-junit-report -package-name $(NAME) > $$CIRCLE_TEST_REPORTS/golang.xml); test $${PIPESTATUS[0]} -eq 0 15 | 16 | test-ci-with-combined-coverage: 17 | echo 'mode: count' > /home/ubuntu/profile.cov ; go list ./... | grep -v "vendor" | xargs -i sh -c 'echo > /home/ubuntu/tmp.cov && go test -v -covermode=count -coverprofile=/home/ubuntu/tmp.cov {} && tail -n +2 /home/ubuntu/tmp.cov >> /home/ubuntu/profile.cov' 18 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/cli.go: -------------------------------------------------------------------------------- 1 | // Package cli provides a minimal framework for creating and organizing command line 2 | // Go applications. cli is designed to be easy to understand and write, the most simple 3 | // cli application can be written as follows: 4 | // func main() { 5 | // cli.NewApp().Run(os.Args) 6 | // } 7 | // 8 | // Of course this application does not do much, so let's make this an actual application: 9 | // func main() { 10 | // app := cli.NewApp() 11 | // app.Name = "greet" 12 | // app.Usage = "say a greeting" 13 | // app.Action = func(c *cli.Context) error { 14 | // println("Greetings") 15 | // } 16 | // 17 | // app.Run(os.Args) 18 | // } 19 | package cli 20 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/big.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "math/big" 5 | ) 6 | 7 | // order of magnitude (to a max order) 8 | func oomm(n, b *big.Int, maxmag int) (float64, int) { 9 | mag := 0 10 | m := &big.Int{} 11 | for n.Cmp(b) >= 0 { 12 | n.DivMod(n, b, m) 13 | mag++ 14 | if mag == maxmag && maxmag >= 0 { 15 | break 16 | } 17 | } 18 | return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag 19 | } 20 | 21 | // total order of magnitude 22 | // (same as above, but with no upper limit) 23 | func oom(n, b *big.Int) (float64, int) { 24 | mag := 0 25 | m := &big.Int{} 26 | for n.Cmp(b) >= 0 { 27 | n.DivMod(n, b, m) 28 | mag++ 29 | } 30 | return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag 31 | } 32 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | 9 | go: 10 | - 1.2.2 11 | - 1.3.3 12 | - 1.4 13 | - 1.5.4 14 | - 1.6.2 15 | - master 16 | 17 | matrix: 18 | allow_failures: 19 | - go: master 20 | include: 21 | - go: 1.6.2 22 | os: osx 23 | - go: 1.1.2 24 | install: go get -v . 25 | before_script: echo skipping gfmxr on $TRAVIS_GO_VERSION 26 | script: 27 | - ./runtests vet 28 | - ./runtests test 29 | 30 | before_script: 31 | - go get github.com/urfave/gfmxr/... 32 | - if [ ! -f node_modules/.bin/markdown-toc ] ; then 33 | npm install markdown-toc ; 34 | fi 35 | 36 | script: 37 | - ./runtests vet 38 | - ./runtests test 39 | - ./runtests gfmxr 40 | - ./runtests toc 41 | -------------------------------------------------------------------------------- /cmd/padded_writer_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestPaddedWriterWithoutColor(t *testing.T) { 9 | var buf bytes.Buffer 10 | 11 | pw := newPaddedWriter(&buf, "> >", "") 12 | pw.Write([]byte("line 1\nline 2")) 13 | pw.Flush() 14 | 15 | expOutput := "> >line 1\n> >line 2\n" 16 | output := buf.String() 17 | if output != expOutput { 18 | t.Fatalf("expected padded writer output to be %q; got %q", expOutput, output) 19 | } 20 | } 21 | 22 | func TestPaddedWriterWithColor(t *testing.T) { 23 | var buf bytes.Buffer 24 | 25 | pw := newPaddedWriter(&buf, "> >", "\033[41m") 26 | pw.Write([]byte("line 1\nline 2")) 27 | pw.Flush() 28 | 29 | expOutput := "> >\033[41mline 1\n\033[0m> >\033[41mline 2\n\033[0m" 30 | output := buf.String() 31 | if output != expOutput { 32 | t.Fatalf("expected padded writer output to be %q; got %q", expOutput, output) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/commaf.go: -------------------------------------------------------------------------------- 1 | // +build go1.6 2 | 3 | package humanize 4 | 5 | import ( 6 | "bytes" 7 | "math/big" 8 | "strings" 9 | ) 10 | 11 | // BigCommaf produces a string form of the given big.Float in base 10 12 | // with commas after every three orders of magnitude. 13 | func BigCommaf(v *big.Float) string { 14 | buf := &bytes.Buffer{} 15 | if v.Sign() < 0 { 16 | buf.Write([]byte{'-'}) 17 | v.Abs(v) 18 | } 19 | 20 | comma := []byte{','} 21 | 22 | parts := strings.Split(v.Text('f', -1), ".") 23 | pos := 0 24 | if len(parts[0])%3 != 0 { 25 | pos += len(parts[0]) % 3 26 | buf.WriteString(parts[0][:pos]) 27 | buf.Write(comma) 28 | } 29 | for ; pos < len(parts[0]); pos += 3 { 30 | buf.WriteString(parts[0][pos : pos+3]) 31 | buf.Write(comma) 32 | } 33 | buf.Truncate(buf.Len() - 1) 34 | 35 | if len(parts) > 1 { 36 | buf.Write([]byte{'.'}) 37 | buf.WriteString(parts[1]) 38 | } 39 | return buf.String() 40 | } 41 | -------------------------------------------------------------------------------- /cmd/display_format_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestParseDisplayFormat(t *testing.T) { 9 | specs := []struct { 10 | input string 11 | expOutput displayFormat 12 | expError error 13 | }{ 14 | {" time", displayTime, nil}, 15 | {"percent ", displayPercent, nil}, 16 | {"something-else ", displayFormat(0), errors.New(`unsupported display format "something-else"`)}, 17 | } 18 | 19 | for specIndex, spec := range specs { 20 | out, err := parseDisplayFormat(spec.input) 21 | if spec.expError != nil || err != nil { 22 | if spec.expError != nil && err == nil || spec.expError == nil && err != nil || spec.expError.Error() != err.Error() { 23 | t.Errorf("[spec %d] expected error %v; got %v", specIndex, spec.expError, err) 24 | continue 25 | } 26 | } 27 | 28 | if out != spec.expOutput { 29 | t.Errorf("[spec %d] expected output %d; got %d", specIndex, spec.expOutput, out) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Geckoboard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jeremy Saenz & Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2008 Dustin Sallings 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | 22 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/category.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | // CommandCategories is a slice of *CommandCategory. 4 | type CommandCategories []*CommandCategory 5 | 6 | // CommandCategory is a category containing commands. 7 | type CommandCategory struct { 8 | Name string 9 | Commands Commands 10 | } 11 | 12 | func (c CommandCategories) Less(i, j int) bool { 13 | return c[i].Name < c[j].Name 14 | } 15 | 16 | func (c CommandCategories) Len() int { 17 | return len(c) 18 | } 19 | 20 | func (c CommandCategories) Swap(i, j int) { 21 | c[i], c[j] = c[j], c[i] 22 | } 23 | 24 | // AddCommand adds a command to a category. 25 | func (c CommandCategories) AddCommand(category string, command Command) CommandCategories { 26 | for _, commandCategory := range c { 27 | if commandCategory.Name == category { 28 | commandCategory.Commands = append(commandCategory.Commands, command) 29 | return c 30 | } 31 | } 32 | return append(c, &CommandCategory{Name: category, Commands: []Command{command}}) 33 | } 34 | 35 | // VisibleCommands returns a slice of the Commands with Hidden=false 36 | func (c *CommandCategory) VisibleCommands() []Command { 37 | ret := []Command{} 38 | for _, command := range c.Commands { 39 | if !command.Hidden { 40 | ret = append(ret, command) 41 | } 42 | } 43 | return ret 44 | } 45 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/funcs.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | // BashCompleteFunc is an action to execute when the bash-completion flag is set 4 | type BashCompleteFunc func(*Context) 5 | 6 | // BeforeFunc is an action to execute before any subcommands are run, but after 7 | // the context is ready if a non-nil error is returned, no subcommands are run 8 | type BeforeFunc func(*Context) error 9 | 10 | // AfterFunc is an action to execute after any subcommands are run, but after the 11 | // subcommand has finished it is run even if Action() panics 12 | type AfterFunc func(*Context) error 13 | 14 | // ActionFunc is the action to execute when no subcommands are specified 15 | type ActionFunc func(*Context) error 16 | 17 | // CommandNotFoundFunc is executed if the proper command cannot be found 18 | type CommandNotFoundFunc func(*Context, string) 19 | 20 | // OnUsageErrorFunc is executed if an usage error occurs. This is useful for displaying 21 | // customized usage error messages. This function is able to replace the 22 | // original error messages. If this function is not set, the "Incorrect usage" 23 | // is displayed and the execution is interrupted. 24 | type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error 25 | 26 | // FlagStringFunc is used by the help generation to display a flag, which is 27 | // expected to be a single line. 28 | type FlagStringFunc func(Flag) string 29 | -------------------------------------------------------------------------------- /profiler/sink/discard.go: -------------------------------------------------------------------------------- 1 | package sink 2 | 3 | import "github.com/geckoboard/prism/profiler" 4 | 5 | type discardSink struct { 6 | sigChan chan struct{} 7 | inputChan chan *profiler.Profile 8 | numDiscarded int 9 | } 10 | 11 | // NewDiscardSink creates a profile entry sink instance which discards all 12 | // incoming profile entries. 13 | func NewDiscardSink() profiler.Sink { 14 | return &discardSink{ 15 | sigChan: make(chan struct{}, 0), 16 | } 17 | } 18 | 19 | // Initialize the sink. 20 | func (s *discardSink) Open(inputBufferSize int) error { 21 | s.inputChan = make(chan *profiler.Profile, inputBufferSize) 22 | 23 | // start worker and wait for ready signal 24 | go s.worker() 25 | <-s.sigChan 26 | return nil 27 | } 28 | 29 | // Shutdown the sink. 30 | func (s *discardSink) Close() error { 31 | // Signal worker to exit and wait for confirmation 32 | close(s.inputChan) 33 | <-s.sigChan 34 | close(s.sigChan) 35 | return nil 36 | } 37 | 38 | // Get a channel for piping profile entries to the sink. 39 | func (s *discardSink) Input() chan<- *profiler.Profile { 40 | return s.inputChan 41 | } 42 | 43 | func (s *discardSink) worker() { 44 | // Signal that worker has started 45 | s.sigChan <- struct{}{} 46 | defer func() { 47 | // Signal that we have stopped 48 | s.sigChan <- struct{}{} 49 | }() 50 | 51 | for { 52 | _, sinkOpen := <-s.inputChan 53 | if !sinkOpen { 54 | return 55 | } 56 | s.numDiscarded++ 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Prism is Open Source! Feel free to contribute! 4 | 5 | Here are a few ideas on things you could be working on: 6 | 7 | - [ ] generate [flamegraphs](http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html) from the captured profile data. 8 | - [ ] create a sink to send profile data to [statsd](https://github.com/etsy/statsd) or other similar systems. 9 | - [ ] add support for converting profile data into [pprof's format](https://github.com/google/pprof). 10 | 11 | ## Getting Started 12 | 13 | - Make sure you have a [GitHub Account](https://github.com/signup/free). 14 | - Make sure you have [Go 1.5+](https://golang.org/dl/) installed on your system. 15 | - Make sure you have [Git](http://git-scm.com/) installed on your system. 16 | - [Fork](https://help.github.com/articles/fork-a-repo) the [repository](https://github.com/geckoboard/prism) on GitHub. 17 | 18 | ## Making Changes 19 | 20 | - [Create a branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository) for your changes. 21 | - [Commit your code](http://git-scm.com/book/en/Git-Basics-Recording-Changes-to-the-Repository) for each logical change (see [tips for creating better commit messages](http://robots.thoughtbot.com/5-useful-tips-for-a-better-commit-message)). 22 | - [Push your change](https://help.github.com/articles/pushing-to-a-remote) to your fork. 23 | - [Create a Pull Request](https://help.github.com/articles/creating-a-pull-request) on GitHub for your change. 24 | 25 | ## Unit tests 26 | 27 | Please make sure you submit unit-tests together with your pull request. 28 | -------------------------------------------------------------------------------- /tools/callgraph_test.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestCallgraphGeneration(t *testing.T) { 9 | wsDir, pkgDir, pkgName := mockPackage(t) 10 | defer os.RemoveAll(wsDir) 11 | 12 | candidates, err := ssaCandidates(pkgDir, pkgName, wsDir) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | mainFqName := pkgName + "/main" 18 | mainFn := candidates[mainFqName] 19 | if mainFn == nil { 20 | t.Fatal("error looking up SSA representation of main()") 21 | } 22 | 23 | target := &ProfileTarget{ 24 | QualifiedName: mainFqName, 25 | PkgPrefix: pkgName, 26 | ssaFunc: mainFn, 27 | } 28 | 29 | graphNodes := target.CallGraph() 30 | expGraphNodeNames := []string{ 31 | pkgName + "/main", 32 | pkgName + "/DoStuff", 33 | pkgName + "/A.DoStuff", 34 | } 35 | if len(graphNodes) != len(expGraphNodeNames) { 36 | t.Fatalf("expected callgraph from main() to have %d nodes; got %d", len(expGraphNodeNames), len(graphNodes)) 37 | } 38 | 39 | for depth, node := range graphNodes { 40 | if node.Depth != depth { 41 | t.Errorf("node depth mismatch; expected %d; got %d", depth, node.Depth) 42 | } 43 | if node.Name != expGraphNodeNames[depth] { 44 | t.Errorf("expected node at depth %d to have name %q; got %q", depth, expGraphNodeNames[depth], node.Name) 45 | } 46 | } 47 | } 48 | 49 | func TestCallgraphGenerationWithNilSSA(t *testing.T) { 50 | target := &ProfileTarget{ 51 | QualifiedName: "mock/main", 52 | PkgPrefix: "mock", 53 | } 54 | 55 | graphNodes := target.CallGraph() 56 | expNodes := 1 57 | if len(graphNodes) != expNodes { 58 | t.Fatalf("expected callgraph from main() to have %d nodes; got %d", expNodes, len(graphNodes)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /profiler/sink/file_test.go: -------------------------------------------------------------------------------- 1 | package sink 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "sync" 10 | "testing" 11 | 12 | "github.com/geckoboard/prism/profiler" 13 | ) 14 | 15 | func TestFileSink(t *testing.T) { 16 | tmpDir, err := ioutil.TempDir("", "prism-test") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | defer os.RemoveAll(tmpDir) 21 | 22 | s := NewFileSink(tmpDir) 23 | err = s.Open(0) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | numEntries := 10 29 | 30 | var wg sync.WaitGroup 31 | wg.Add(numEntries) 32 | for i := 0; i < numEntries; i++ { 33 | go func(i int) { 34 | defer wg.Done() 35 | // This gets incorrectly flagged as a data-race but 36 | // in reality no race exists as we are sending data to the channel 37 | s.Input() <- &profiler.Profile{ 38 | Target: &profiler.CallMetrics{ 39 | FnName: fmt.Sprintf(`foo.bar/baz\boo.%d`, i), 40 | }, 41 | } 42 | }(i) 43 | } 44 | 45 | wg.Wait() 46 | 47 | err = s.Close() 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | fileList, err := filepath.Glob(tmpDir + "/*.json") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if len(fileList) != numEntries { 57 | t.Errorf("expected number of written files to be %d; got %d", numEntries, len(fileList)) 58 | } 59 | 60 | for _, fpath := range fileList { 61 | fname := strings.TrimSuffix(fpath[len(tmpDir)+1:], ".json") 62 | if !strings.HasPrefix(fname, profilePrefix) { 63 | t.Errorf("[%s] expected prefix to be %q", fname, profilePrefix) 64 | } 65 | 66 | badCharIndex := strings.IndexAny(fname, `./\`) 67 | if badCharIndex != -1 { 68 | t.Errorf("[%s] found invalid char %q at index %d", fname, fname[badCharIndex:badCharIndex+1], badCharIndex) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cmd/display_unit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/dustin/go-humanize" 9 | ) 10 | 11 | type displayUnit uint8 12 | 13 | const ( 14 | displayUnitAuto displayUnit = iota 15 | displayUnitMs 16 | displayUnitUs 17 | displayUnitNs 18 | ) 19 | 20 | // Convert a time.Duration into a floating point value representing the 21 | // duration value in the unit specified by du. 22 | func (du displayUnit) Convert(t time.Duration) float64 { 23 | switch du { 24 | case displayUnitMs: 25 | return float64(t.Nanoseconds()) / 1.0e6 26 | case displayUnitUs: 27 | return float64(t.Nanoseconds()) / 1.0e3 28 | default: 29 | return float64(t.Nanoseconds()) 30 | } 31 | } 32 | 33 | // Format a time unit as a string. 34 | func (du displayUnit) Format(val float64) string { 35 | switch du { 36 | case displayUnitMs: 37 | return humanize.FormatFloat("#,###.##", val) + " ms" 38 | case displayUnitUs: 39 | return humanize.FormatFloat("#,###.##", val) + " us" 40 | default: 41 | return humanize.Comma(int64(val)) + " ns" 42 | } 43 | } 44 | 45 | // DetectTimeUnit returns the time unit best representing the given time.Duration. 46 | func detectTimeUnit(t time.Duration) displayUnit { 47 | ns := t.Nanoseconds() 48 | if ns >= 1e6 { 49 | return displayUnitMs 50 | } else if ns >= 1e3 { 51 | return displayUnitUs 52 | } 53 | return displayUnitNs 54 | } 55 | 56 | func parseDisplayUnit(val string) (displayUnit, error) { 57 | trimmed := strings.TrimSpace(val) 58 | switch trimmed { 59 | case "auto": 60 | return displayUnitAuto, nil 61 | case "us": 62 | return displayUnitUs, nil 63 | case "ms": 64 | return displayUnitMs, nil 65 | case "ns": 66 | return displayUnitNs, nil 67 | } 68 | 69 | return 0, fmt.Errorf("unsupported display unit %q", trimmed) 70 | } 71 | -------------------------------------------------------------------------------- /profiler/goid_test.go: -------------------------------------------------------------------------------- 1 | package profiler 2 | 3 | import ( 4 | "runtime" 5 | "strconv" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestThreadID(t *testing.T) { 11 | var buf = make([]byte, 64) 12 | runtime.Stack(buf, false) 13 | tokens := strings.Split(string(buf), " ") 14 | expID, err := strconv.ParseUint(tokens[1], 10, 64) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | tid := threadID() 20 | 21 | if tid != expID { 22 | t.Fatalf("expected threadID() to return %d; got %d", expID, tid) 23 | } 24 | } 25 | 26 | func TestParseBase10UintBytes(t *testing.T) { 27 | specs := []struct { 28 | Bits int 29 | Input []byte 30 | ExpVal uint64 31 | ExpErr error 32 | }{ 33 | {64, []byte{'1', '2', '3', '4', '5'}, 12345, nil}, 34 | {64, []byte{'A', '2', '3', '4', '5'}, 0, strconv.ErrSyntax}, 35 | {64, []byte{}, 0, strconv.ErrSyntax}, 36 | {64, []byte{'9', '6', '7', '6', '9', '7', '6', '7', '3', '3', '9', '7', '3', '5', '9', '5', '6', '0', '0', '0'}, 1<<64 - 1, strconv.ErrRange}, 37 | {32, []byte{'9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', '6', '7', '6', '9', '7', '6', '7', '3', '3', '9', '7', '3', '5', '9', '5', '6', '0', '0', '0'}, 1<<64 - 1, strconv.ErrRange}, 38 | } 39 | 40 | for specIndex, spec := range specs { 41 | val, err := parseBase10UintBytes(spec.Input, spec.Bits) 42 | if spec.ExpErr == nil && err != nil { 43 | t.Errorf("[spec %d] error parsing value: %v", specIndex, err) 44 | } 45 | 46 | if spec.ExpErr != nil && (err == nil || !strings.Contains(err.Error(), spec.ExpErr.Error())) { 47 | t.Errorf("[spec %d] expected parse error %q; got %v", specIndex, spec.ExpErr.Error(), err) 48 | } 49 | 50 | if val != spec.ExpVal { 51 | t.Errorf("[spec %d] expected parsed value to be %d; got %d", specIndex, spec.ExpVal, val) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cmd/table_column_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestParseTableColumnList(t *testing.T) { 9 | colNamesToHeaderNames := map[string]string{ 10 | "total": "total", 11 | "min": "min", 12 | "max": "max", 13 | "mean": "mean", 14 | "median": "median", 15 | "invocations": "invoc", 16 | "p50": "p50", 17 | "p75": "p75", 18 | "p90": "p90", 19 | "p99": "p99", 20 | "stddev": "stddev", 21 | } 22 | 23 | for colName, expHeader := range colNamesToHeaderNames { 24 | colTypes, err := parseTableColumList(colName) 25 | if err != nil { 26 | t.Errorf("error parsing col list %q: %v", colName, err) 27 | continue 28 | } 29 | 30 | if len(colTypes) != 1 { 31 | t.Errorf("expected parsed column type list %q to contain 1 entry; got %d", colName, len(colTypes)) 32 | continue 33 | } 34 | 35 | if colTypes[0].Header() != expHeader { 36 | t.Errorf("expected header for column %q to be %q; got %q", colName, expHeader, colTypes[0].Header()) 37 | } 38 | } 39 | } 40 | 41 | func TestParseTableColumnListError(t *testing.T) { 42 | _, err := parseTableColumList("total, unknown") 43 | expError := fmt.Sprintf(`unsupported column name "unknown"; supported column names are: %s`, SupportedColumnNames()) 44 | if err == nil || err.Error() != expError { 45 | t.Fatalf("expected to get error %q; got %v", expError, err) 46 | } 47 | } 48 | 49 | func TestColumnTypePanicForUnknownType(t *testing.T) { 50 | defer func() { 51 | if err := recover(); err == nil { 52 | t.Fatal("expected Header() to panic") 53 | } 54 | }() 55 | 56 | unknownType := numTableColumns 57 | if unknownType.Name() != "" { 58 | t.Fatal("expected to get an empty string when calling Name() on an unknown table column type") 59 | } 60 | unknownType.Header() 61 | } 62 | -------------------------------------------------------------------------------- /profiler/goid.go: -------------------------------------------------------------------------------- 1 | package profiler 2 | 3 | import ( 4 | "bytes" 5 | "runtime" 6 | "strconv" 7 | "sync" 8 | ) 9 | 10 | const ( 11 | base10CutOff = (1<<64 - 1) / 11 12 | ) 13 | 14 | var ( 15 | goRoutinePrefix = []byte("goroutine ") 16 | ) 17 | 18 | // Implementation copied by https://github.com/tylerb/gls/blob/2ef09cd25215bcab07d95380475175ba9a9fdc40/gotrack.go 19 | var stackBufPool = sync.Pool{ 20 | New: func() interface{} { 21 | buf := make([]byte, 64) 22 | return &buf 23 | }, 24 | } 25 | 26 | // Detect the current goroutine id. 27 | func threadID() uint64 { 28 | bp := stackBufPool.Get().(*[]byte) 29 | defer stackBufPool.Put(bp) 30 | b := *bp 31 | b = b[:runtime.Stack(b, false)] 32 | // Parse the 4707 out of "goroutine 4707 [" 33 | b = bytes.TrimPrefix(b, goRoutinePrefix) 34 | i := bytes.IndexByte(b, ' ') 35 | if i < 0 { 36 | panic("threadID(): [BUG] missing space at goRoutinePrefix") 37 | } 38 | b = b[:i] 39 | n, err := parseBase10UintBytes(b, 64) 40 | if err != nil { 41 | panic("threadID(): [BUG] failed to parse goroutine ID") 42 | } 43 | return n 44 | } 45 | 46 | // parseUintBytes works like strconv.ParseUint with base=10, but using a []byte. 47 | func parseBase10UintBytes(s []byte, bitSize int) (n uint64, err error) { 48 | if len(s) == 0 { 49 | err = strconv.ErrSyntax 50 | return n, &strconv.NumError{Func: "ParseUint", Num: string(s), Err: err} 51 | } 52 | 53 | n = 0 54 | var maxVal uint64 = 1<= base10CutOff { 69 | n = 1<<64 - 1 70 | err = strconv.ErrRange 71 | return n, &strconv.NumError{Func: "parseBase10UintBytes", Num: string(s), Err: err} 72 | } 73 | n *= 10 74 | 75 | n1 := n + uint64(v) 76 | if n1 < n || n1 > maxVal { 77 | // n+v overflows 78 | n = 1<<64 - 1 79 | err = strconv.ErrRange 80 | return n, &strconv.NumError{Func: "parseBase10UintBytes", Num: string(s), Err: err} 81 | } 82 | n = n1 83 | } 84 | 85 | return n, nil 86 | } 87 | -------------------------------------------------------------------------------- /cmd/display_unit_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestParseDisplayUnit(t *testing.T) { 10 | specs := []struct { 11 | input string 12 | expOutput displayUnit 13 | expError error 14 | }{ 15 | {" auto", displayUnitAuto, nil}, 16 | {" ms ", displayUnitMs, nil}, 17 | {" us", displayUnitUs, nil}, 18 | {"ns", displayUnitNs, nil}, 19 | {"something-else ", displayUnit(0), errors.New(`unsupported display unit "something-else"`)}, 20 | } 21 | 22 | for specIndex, spec := range specs { 23 | out, err := parseDisplayUnit(spec.input) 24 | if spec.expError != nil || err != nil { 25 | if spec.expError != nil && err == nil || spec.expError == nil && err != nil || spec.expError.Error() != err.Error() { 26 | t.Errorf("[spec %d] expected error %v; got %v", specIndex, spec.expError, err) 27 | continue 28 | } 29 | } 30 | 31 | if out != spec.expOutput { 32 | t.Errorf("[spec %d] expected output %d; got %d", specIndex, spec.expOutput, out) 33 | } 34 | } 35 | } 36 | 37 | func TestFormatDisplayUnit(t *testing.T) { 38 | specs := []struct { 39 | in float64 40 | unit displayUnit 41 | expOut string 42 | }{ 43 | {1234, displayUnitNs, "1,234 ns"}, 44 | {1.1, displayUnitNs, "1 ns"}, 45 | {123456, displayUnitUs, "123,456.00 us"}, 46 | {1234567, displayUnitUs, "1,234,567.00 us"}, 47 | {0.001, displayUnitUs, "0.00 us"}, 48 | {1234567, displayUnitMs, "1,234,567.00 ms"}, 49 | } 50 | 51 | for specIndex, spec := range specs { 52 | out := spec.unit.Format(spec.in) 53 | if out != spec.expOut { 54 | t.Errorf("[spec %d] expected formatted value to be %q; got %q", specIndex, spec.expOut, out) 55 | } 56 | } 57 | } 58 | 59 | func TestDetectDisplayUnit(t *testing.T) { 60 | specs := []struct { 61 | in time.Duration 62 | expUnit displayUnit 63 | }{ 64 | {10 * time.Nanosecond, displayUnitNs}, 65 | {1000 * time.Nanosecond, displayUnitUs}, 66 | {1000000 * time.Nanosecond, displayUnitMs}, 67 | } 68 | 69 | for specIndex, spec := range specs { 70 | unit := detectTimeUnit(spec.in) 71 | if unit != spec.expUnit { 72 | t.Errorf("[spec %d] expected detected unit to be %v; got %v", specIndex, spec.expUnit, unit) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /vendor/github.com/geckoboard/cli-table/README.md: -------------------------------------------------------------------------------- 1 | # cli-table 2 | [![CircleCI](https://circleci.com/gh/geckoboard/cli-table.svg?style=shield)](https://circleci.com/gh/geckoboard/cli-table) 3 | [![Coverage Status](https://coveralls.io/repos/github/geckoboard/cli-table/badge.svg?branch=master)](https://coveralls.io/github/geckoboard/cli-table?branch=master) 4 | [![GoDoc](https://godoc.org/github.com/geckoboard/cli-table?status.svg)](https://godoc.org/github.com/geckoboard/cli-table) 5 | 6 | Fancy ASCII tables for your CLI with full ANSI support. 7 | 8 | The cli-table package provides a simple API for rendering ASCII tables. 9 | Its main features are: 10 | - headers and header groups; header groups may span multiple header columns. 11 | - user-selectable cell padding. 12 | - per-column and per header group alignment selection (left, center, right). 13 | - ANSI support. ANSI characters are properly handled and do not mess up width calculations. 14 | - tables are rendered to any output sink implementing [io.Writer](https://golang.org/pkg/io/#Writer) 15 | 16 | ## Getting started 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "os" 23 | 24 | "github.com/geckoboard/cli-table" 25 | ) 26 | 27 | func main() { 28 | t := table.New(3) 29 | 30 | // Set headers 31 | t.SetHeader(0, "left", table.AlignLeft) 32 | t.SetHeader(1, "center", table.AlignCenter) 33 | t.SetHeader(2, "right", table.AlignRight) 34 | 35 | // Optionally define header groups 36 | t.AddHeaderGroup(2, "left group", table.AlignCenter) 37 | t.AddHeaderGroup(1, "right group", table.AlignRight) 38 | 39 | // Append single row 40 | t.Append([]string{"1", "2", "3"}) 41 | 42 | // Or append a bunch of rows which may contain ANSI characters 43 | t.Append( 44 | []string{"1", "2", "3"}, 45 | []string{"\033[33mfour\033[0m", "five", "six"}, 46 | ) 47 | 48 | // Render table and strip out ANSI characters 49 | t.Write(os.Stdout, table.StripAnsi) 50 | 51 | // If your terminal does support ANSI characters you can use: 52 | // t.Write(os.Stdout, table.PreserveAnsi) 53 | } 54 | ``` 55 | 56 | Running this example would render the following output: 57 | 58 | ``` 59 | +---------------+---------------+ 60 | | left group | right group | 61 | +---------------+---------------+ 62 | | left | center | right | 63 | +------+--------+---------------+ 64 | | 1 | 2 | 3 | 65 | | 1 | 2 | 3 | 66 | | four | five | six | 67 | +------+--------+---------------+ 68 | ``` 69 | 70 | -------------------------------------------------------------------------------- /cmd/padded_writer.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // padded writer wraps an io.Writer and inserts a customizable padding to the 9 | // beginning of every line. It buffers incoming data and flushes it whenever 10 | // a new line is encountered or the writer is manually flushed. 11 | type paddedWriter struct { 12 | w io.Writer 13 | buf *bytes.Buffer 14 | padPrefix []byte 15 | padSuffix []byte 16 | } 17 | 18 | // Wrap a io.Writer with a writer that prepends pad to the beginning of each line. 19 | // An optional color argument containing an ANSI escape code may be specified to 20 | // colorize output for color terminals. 21 | func newPaddedWriter(w io.Writer, pad, color string) *paddedWriter { 22 | pw := &paddedWriter{ 23 | w: w, 24 | buf: new(bytes.Buffer), 25 | padPrefix: []byte(pad), 26 | } 27 | 28 | if color != "" { 29 | pw.padPrefix = append(pw.padPrefix, []byte(color)...) 30 | pw.padSuffix = []byte("\033[0m") 31 | } 32 | 33 | return pw 34 | } 35 | 36 | // Implements io.Writer. 37 | func (pw *paddedWriter) Write(data []byte) (int, error) { 38 | if len(data) == 0 { 39 | return 0, nil 40 | } 41 | 42 | var err error 43 | var lStart, lEnd int 44 | for _, b := range data { 45 | lEnd++ 46 | 47 | if b != '\n' { 48 | continue 49 | } 50 | 51 | // We hit a line feed. Append data block to our buffer 52 | _, err = pw.buf.Write(data[lStart:lEnd]) 53 | if err != nil { 54 | return 0, err 55 | } 56 | 57 | // Flush buffer 58 | pw.Flush() 59 | 60 | // Reset block indices for next block 61 | lStart = lEnd 62 | lEnd = lStart 63 | } 64 | 65 | // Append any pending bytes. 66 | if lEnd > lStart { 67 | _, err = pw.buf.Write(data[lStart:lEnd]) 68 | if err != nil { 69 | return 0, err 70 | } 71 | } 72 | 73 | return len(data), nil 74 | } 75 | 76 | // Flush buffered line. 77 | func (pw *paddedWriter) Flush() { 78 | if pw.buf.Len() == 0 { 79 | return 80 | } 81 | 82 | // If last character is not a line feed append one 83 | if pw.buf.Bytes()[pw.buf.Len()-1] != '\n' { 84 | pw.buf.WriteByte('\n') 85 | } 86 | 87 | // Write padding 88 | _, err := pw.w.Write(pw.padPrefix) 89 | if err != nil { 90 | return 91 | } 92 | 93 | // Write buffered data and suffix 94 | pw.w.Write(pw.buf.Bytes()) 95 | if pw.padSuffix != nil { 96 | pw.w.Write(pw.padSuffix) 97 | } 98 | pw.buf.Reset() 99 | } 100 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/README.markdown: -------------------------------------------------------------------------------- 1 | # Humane Units [![Build Status](https://travis-ci.org/dustin/go-humanize.svg?branch=master)](https://travis-ci.org/dustin/go-humanize) [![GoDoc](https://godoc.org/github.com/dustin/go-humanize?status.svg)](https://godoc.org/github.com/dustin/go-humanize) 2 | 3 | Just a few functions for helping humanize times and sizes. 4 | 5 | `go get` it as `github.com/dustin/go-humanize`, import it as 6 | `"github.com/dustin/go-humanize"`, use it as `humanize` 7 | 8 | See [godoc](https://godoc.org/github.com/dustin/go-humanize) for 9 | complete documentation. 10 | 11 | ## Sizes 12 | 13 | This lets you take numbers like `82854982` and convert them to useful 14 | strings like, `83MB` or `79MiB` (whichever you prefer). 15 | 16 | Example: 17 | 18 | ```go 19 | fmt.Printf("That file is %s.", humanize.Bytes(82854982)) 20 | ``` 21 | 22 | ## Times 23 | 24 | This lets you take a `time.Time` and spit it out in relative terms. 25 | For example, `12 seconds ago` or `3 days from now`. 26 | 27 | Example: 28 | 29 | ```go 30 | fmt.Printf("This was touched %s", humanize.Time(someTimeInstance)) 31 | ``` 32 | 33 | Thanks to Kyle Lemons for the time implementation from an IRC 34 | conversation one day. It's pretty neat. 35 | 36 | ## Ordinals 37 | 38 | From a [mailing list discussion][odisc] where a user wanted to be able 39 | to label ordinals. 40 | 41 | 0 -> 0th 42 | 1 -> 1st 43 | 2 -> 2nd 44 | 3 -> 3rd 45 | 4 -> 4th 46 | [...] 47 | 48 | Example: 49 | 50 | ```go 51 | fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) 52 | ``` 53 | 54 | ## Commas 55 | 56 | Want to shove commas into numbers? Be my guest. 57 | 58 | 0 -> 0 59 | 100 -> 100 60 | 1000 -> 1,000 61 | 1000000000 -> 1,000,000,000 62 | -100000 -> -100,000 63 | 64 | Example: 65 | 66 | ```go 67 | fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) 68 | ``` 69 | 70 | ## Ftoa 71 | 72 | Nicer float64 formatter that removes trailing zeros. 73 | 74 | ```go 75 | fmt.Printf("%f", 2.24) // 2.240000 76 | fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24 77 | fmt.Printf("%f", 2.0) // 2.000000 78 | fmt.Printf("%s", humanize.Ftoa(2.0)) // 2 79 | ``` 80 | 81 | ## SI notation 82 | 83 | Format numbers with [SI notation][sinotation]. 84 | 85 | Example: 86 | 87 | ```go 88 | humanize.SI(0.00000000223, "M") // 2.23nM 89 | ``` 90 | 91 | [odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion 92 | [sinotation]: http://en.wikipedia.org/wiki/Metric_prefix 93 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/errors.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // OsExiter is the function used when the app exits. If not set defaults to os.Exit. 11 | var OsExiter = os.Exit 12 | 13 | // ErrWriter is used to write errors to the user. This can be anything 14 | // implementing the io.Writer interface and defaults to os.Stderr. 15 | var ErrWriter io.Writer = os.Stderr 16 | 17 | // MultiError is an error that wraps multiple errors. 18 | type MultiError struct { 19 | Errors []error 20 | } 21 | 22 | // NewMultiError creates a new MultiError. Pass in one or more errors. 23 | func NewMultiError(err ...error) MultiError { 24 | return MultiError{Errors: err} 25 | } 26 | 27 | // Error implents the error interface. 28 | func (m MultiError) Error() string { 29 | errs := make([]string, len(m.Errors)) 30 | for i, err := range m.Errors { 31 | errs[i] = err.Error() 32 | } 33 | 34 | return strings.Join(errs, "\n") 35 | } 36 | 37 | // ExitCoder is the interface checked by `App` and `Command` for a custom exit 38 | // code 39 | type ExitCoder interface { 40 | error 41 | ExitCode() int 42 | } 43 | 44 | // ExitError fulfills both the builtin `error` interface and `ExitCoder` 45 | type ExitError struct { 46 | exitCode int 47 | message string 48 | } 49 | 50 | // NewExitError makes a new *ExitError 51 | func NewExitError(message string, exitCode int) *ExitError { 52 | return &ExitError{ 53 | exitCode: exitCode, 54 | message: message, 55 | } 56 | } 57 | 58 | // Error returns the string message, fulfilling the interface required by 59 | // `error` 60 | func (ee *ExitError) Error() string { 61 | return ee.message 62 | } 63 | 64 | // ExitCode returns the exit code, fulfilling the interface required by 65 | // `ExitCoder` 66 | func (ee *ExitError) ExitCode() int { 67 | return ee.exitCode 68 | } 69 | 70 | // HandleExitCoder checks if the error fulfills the ExitCoder interface, and if 71 | // so prints the error to stderr (if it is non-empty) and calls OsExiter with the 72 | // given exit code. If the given error is a MultiError, then this func is 73 | // called on all members of the Errors slice. 74 | func HandleExitCoder(err error) { 75 | if err == nil { 76 | return 77 | } 78 | 79 | if exitErr, ok := err.(ExitCoder); ok { 80 | if err.Error() != "" { 81 | fmt.Fprintln(ErrWriter, err) 82 | } 83 | OsExiter(exitErr.ExitCode()) 84 | return 85 | } 86 | 87 | if multiErr, ok := err.(MultiError); ok { 88 | for _, merr := range multiErr.Errors { 89 | HandleExitCoder(merr) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tools/injector.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | ) 8 | 9 | var ( 10 | profilerImports = []string{"prismProfiler github.com/geckoboard/prism/profiler"} 11 | sinkImports = []string{"prismSink github.com/geckoboard/prism/profiler/sink"} 12 | ) 13 | 14 | // InjectProfilerBootstrap returns a PatchFunc that injects our profiler init code the main function of the target package. 15 | func InjectProfilerBootstrap(profileDir, profileLabel string) PatchFunc { 16 | return func(cgNode *CallGraphNode, fnDeclNode *ast.BlockStmt) (modifiedAST bool, extraImports []string) { 17 | imports := append(profilerImports, sinkImports...) 18 | fnDeclNode.List = append( 19 | []ast.Stmt{ 20 | &ast.ExprStmt{ 21 | X: &ast.BasicLit{ 22 | ValuePos: token.NoPos, 23 | Kind: token.STRING, 24 | Value: fmt.Sprintf("prismProfiler.Init(prismSink.NewFileSink(%q), %q)", profileDir, profileLabel), 25 | }, 26 | }, 27 | &ast.ExprStmt{ 28 | X: &ast.BasicLit{ 29 | ValuePos: token.NoPos, 30 | Kind: token.STRING, 31 | Value: `defer prismProfiler.Shutdown()`, 32 | }, 33 | }, 34 | }, 35 | fnDeclNode.List..., 36 | ) 37 | 38 | return true, imports 39 | } 40 | } 41 | 42 | // InjectProfiler returns a PatchFunc that injects our profiler instrumentation code in all 43 | // functions that are reachable from the profile targets that the user specified. 44 | func InjectProfiler() PatchFunc { 45 | return func(cgNode *CallGraphNode, fnDeclNode *ast.BlockStmt) (modifiedAST bool, extraImports []string) { 46 | enterFn, leaveFn := profileFnName(cgNode.Depth) 47 | 48 | // Append our instrumentation calls to the top of the function 49 | fnDeclNode.List = append( 50 | []ast.Stmt{ 51 | &ast.ExprStmt{ 52 | X: &ast.BasicLit{ 53 | ValuePos: token.NoPos, 54 | Kind: token.STRING, 55 | Value: fmt.Sprintf(`prismProfiler.%s("%s")`, enterFn, cgNode.Name), 56 | }, 57 | }, 58 | &ast.ExprStmt{ 59 | X: &ast.BasicLit{ 60 | ValuePos: token.NoPos, 61 | Kind: token.STRING, 62 | Value: fmt.Sprintf(`defer prismProfiler.%s()`, leaveFn), 63 | }, 64 | }, 65 | }, 66 | fnDeclNode.List..., 67 | ) 68 | 69 | return true, profilerImports 70 | } 71 | } 72 | 73 | // Return the appropriate profiler enter/exit function names depending on whether 74 | // a profile target is a user-specified target (depth=0) or a target discovered 75 | // by analyzing the callgraph from a user-specified target. 76 | func profileFnName(depth int) (enterFn, leaveFn string) { 77 | if depth == 0 { 78 | return "BeginProfile", "EndProfile" 79 | } 80 | 81 | return "Enter", "Leave" 82 | } 83 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/comma.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "bytes" 5 | "math" 6 | "math/big" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Comma produces a string form of the given number in base 10 with 12 | // commas after every three orders of magnitude. 13 | // 14 | // e.g. Comma(834142) -> 834,142 15 | func Comma(v int64) string { 16 | sign := "" 17 | 18 | // minin64 can't be negated to a usable value, so it has to be special cased. 19 | if v == math.MinInt64 { 20 | return "-9,223,372,036,854,775,808" 21 | } 22 | 23 | if v < 0 { 24 | sign = "-" 25 | v = 0 - v 26 | } 27 | 28 | parts := []string{"", "", "", "", "", "", ""} 29 | j := len(parts) - 1 30 | 31 | for v > 999 { 32 | parts[j] = strconv.FormatInt(v%1000, 10) 33 | switch len(parts[j]) { 34 | case 2: 35 | parts[j] = "0" + parts[j] 36 | case 1: 37 | parts[j] = "00" + parts[j] 38 | } 39 | v = v / 1000 40 | j-- 41 | } 42 | parts[j] = strconv.Itoa(int(v)) 43 | return sign + strings.Join(parts[j:], ",") 44 | } 45 | 46 | // Commaf produces a string form of the given number in base 10 with 47 | // commas after every three orders of magnitude. 48 | // 49 | // e.g. Commaf(834142.32) -> 834,142.32 50 | func Commaf(v float64) string { 51 | buf := &bytes.Buffer{} 52 | if v < 0 { 53 | buf.Write([]byte{'-'}) 54 | v = 0 - v 55 | } 56 | 57 | comma := []byte{','} 58 | 59 | parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".") 60 | pos := 0 61 | if len(parts[0])%3 != 0 { 62 | pos += len(parts[0]) % 3 63 | buf.WriteString(parts[0][:pos]) 64 | buf.Write(comma) 65 | } 66 | for ; pos < len(parts[0]); pos += 3 { 67 | buf.WriteString(parts[0][pos : pos+3]) 68 | buf.Write(comma) 69 | } 70 | buf.Truncate(buf.Len() - 1) 71 | 72 | if len(parts) > 1 { 73 | buf.Write([]byte{'.'}) 74 | buf.WriteString(parts[1]) 75 | } 76 | return buf.String() 77 | } 78 | 79 | // BigComma produces a string form of the given big.Int in base 10 80 | // with commas after every three orders of magnitude. 81 | func BigComma(b *big.Int) string { 82 | sign := "" 83 | if b.Sign() < 0 { 84 | sign = "-" 85 | b.Abs(b) 86 | } 87 | 88 | athousand := big.NewInt(1000) 89 | c := (&big.Int{}).Set(b) 90 | _, m := oom(c, athousand) 91 | parts := make([]string, m+1) 92 | j := len(parts) - 1 93 | 94 | mod := &big.Int{} 95 | for b.Cmp(athousand) >= 0 { 96 | b.DivMod(b, athousand, mod) 97 | parts[j] = strconv.FormatInt(mod.Int64(), 10) 98 | switch len(parts[j]) { 99 | case 2: 100 | parts[j] = "0" + parts[j] 101 | case 1: 102 | parts[j] = "00" + parts[j] 103 | } 104 | j-- 105 | } 106 | parts[j] = strconv.Itoa(int(b.Int64())) 107 | return sign + strings.Join(parts[j:], ",") 108 | } 109 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/runtests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import argparse 5 | import os 6 | import sys 7 | import tempfile 8 | 9 | from subprocess import check_call, check_output 10 | 11 | 12 | PACKAGE_NAME = os.environ.get( 13 | 'CLI_PACKAGE_NAME', 'github.com/urfave/cli' 14 | ) 15 | 16 | 17 | def main(sysargs=sys.argv[:]): 18 | targets = { 19 | 'vet': _vet, 20 | 'test': _test, 21 | 'gfmxr': _gfmxr, 22 | 'toc': _toc, 23 | } 24 | 25 | parser = argparse.ArgumentParser() 26 | parser.add_argument( 27 | 'target', nargs='?', choices=tuple(targets.keys()), default='test' 28 | ) 29 | args = parser.parse_args(sysargs[1:]) 30 | 31 | targets[args.target]() 32 | return 0 33 | 34 | 35 | def _test(): 36 | if check_output('go version'.split()).split()[2] < 'go1.2': 37 | _run('go test -v .'.split()) 38 | return 39 | 40 | coverprofiles = [] 41 | for subpackage in ['', 'altsrc']: 42 | coverprofile = 'cli.coverprofile' 43 | if subpackage != '': 44 | coverprofile = '{}.coverprofile'.format(subpackage) 45 | 46 | coverprofiles.append(coverprofile) 47 | 48 | _run('go test -v'.split() + [ 49 | '-coverprofile={}'.format(coverprofile), 50 | ('{}/{}'.format(PACKAGE_NAME, subpackage)).rstrip('/') 51 | ]) 52 | 53 | combined_name = _combine_coverprofiles(coverprofiles) 54 | _run('go tool cover -func={}'.format(combined_name).split()) 55 | os.remove(combined_name) 56 | 57 | 58 | def _gfmxr(): 59 | _run(['gfmxr', '-c', str(_gfmxr_count()), '-s', 'README.md']) 60 | 61 | 62 | def _vet(): 63 | _run('go vet ./...'.split()) 64 | 65 | 66 | def _toc(): 67 | _run(['node_modules/.bin/markdown-toc', '-i', 'README.md']) 68 | _run(['git', 'diff', '--quiet']) 69 | 70 | 71 | def _run(command): 72 | print('runtests: {}'.format(' '.join(command)), file=sys.stderr) 73 | check_call(command) 74 | 75 | 76 | def _gfmxr_count(): 77 | with open('README.md') as infile: 78 | lines = infile.read().splitlines() 79 | return len(filter(_is_go_runnable, lines)) 80 | 81 | 82 | def _is_go_runnable(line): 83 | return line.startswith('package main') 84 | 85 | 86 | def _combine_coverprofiles(coverprofiles): 87 | combined = tempfile.NamedTemporaryFile( 88 | suffix='.coverprofile', delete=False 89 | ) 90 | combined.write('mode: set\n') 91 | 92 | for coverprofile in coverprofiles: 93 | with open(coverprofile, 'r') as infile: 94 | for line in infile.readlines(): 95 | if not line.startswith('mode: '): 96 | combined.write(line) 97 | 98 | combined.flush() 99 | name = combined.name 100 | combined.close() 101 | return name 102 | 103 | 104 | if __name__ == '__main__': 105 | sys.exit(main()) 106 | -------------------------------------------------------------------------------- /cmd/table_column.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // A typed value to indicate which table columns should be included in the output. 10 | type tableColumnType int 11 | 12 | const ( 13 | tableColTotal tableColumnType = iota 14 | tableColMin 15 | tableColMax 16 | tableColMean 17 | tableColMedian 18 | tableColInvocations 19 | tableColP50 20 | tableColP75 21 | tableColP90 22 | tableColP99 23 | tableColStdDev 24 | // a sentinel value allowing us to iterate all valid table column types 25 | numTableColumns 26 | ) 27 | 28 | var ( 29 | tableColSplitRegex = regexp.MustCompile(`\s*,\s*`) 30 | tableColTypeToName = map[tableColumnType]string{ 31 | tableColTotal: "total", 32 | tableColMin: "min", 33 | tableColMax: "max", 34 | tableColMean: "mean", 35 | tableColMedian: "median", 36 | tableColInvocations: "invocations", 37 | tableColP50: "p50", 38 | tableColP75: "p75", 39 | tableColP90: "p90", 40 | tableColP99: "p99", 41 | tableColStdDev: "stddev", 42 | } 43 | ) 44 | 45 | // Header returns the table header description for this column type. 46 | func (dc tableColumnType) Header() string { 47 | switch dc { 48 | case tableColTotal: 49 | return "total" 50 | case tableColMin: 51 | return "min" 52 | case tableColMax: 53 | return "max" 54 | case tableColMean: 55 | return "mean" 56 | case tableColMedian: 57 | return "median" 58 | case tableColInvocations: 59 | return "invoc" 60 | case tableColP50: 61 | return "p50" 62 | case tableColP75: 63 | return "p75" 64 | case tableColP90: 65 | return "p90" 66 | case tableColP99: 67 | return "p99" 68 | case tableColStdDev: 69 | return "stddev" 70 | } 71 | panic("unsupported column type") 72 | } 73 | 74 | // Name returns a string representation of this column's type. 75 | func (dc tableColumnType) Name() string { 76 | return tableColTypeToName[dc] 77 | } 78 | 79 | // Parse a comma delimited set of column types. 80 | func parseTableColumList(list string) ([]tableColumnType, error) { 81 | cols := make([]tableColumnType, 0) 82 | for _, colName := range tableColSplitRegex.Split(list, -1) { 83 | found := false 84 | for colType, colTypeName := range tableColTypeToName { 85 | if colName == colTypeName { 86 | cols = append(cols, colType) 87 | found = true 88 | break 89 | } 90 | } 91 | 92 | if !found { 93 | return nil, fmt.Errorf("unsupported column name %q; supported column names are: %s", colName, SupportedColumnNames()) 94 | } 95 | } 96 | 97 | return cols, nil 98 | } 99 | 100 | // SupportedColumnNames returns back a string will all supported metric column names. 101 | func SupportedColumnNames() string { 102 | set := make([]string, numTableColumns) 103 | for i := 0; i < int(numTableColumns); i++ { 104 | set[i] = tableColumnType(i).Name() 105 | } 106 | 107 | return strings.Join(set, ", ") 108 | } 109 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/si.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | ) 9 | 10 | var siPrefixTable = map[float64]string{ 11 | -24: "y", // yocto 12 | -21: "z", // zepto 13 | -18: "a", // atto 14 | -15: "f", // femto 15 | -12: "p", // pico 16 | -9: "n", // nano 17 | -6: "µ", // micro 18 | -3: "m", // milli 19 | 0: "", 20 | 3: "k", // kilo 21 | 6: "M", // mega 22 | 9: "G", // giga 23 | 12: "T", // tera 24 | 15: "P", // peta 25 | 18: "E", // exa 26 | 21: "Z", // zetta 27 | 24: "Y", // yotta 28 | } 29 | 30 | var revSIPrefixTable = revfmap(siPrefixTable) 31 | 32 | // revfmap reverses the map and precomputes the power multiplier 33 | func revfmap(in map[float64]string) map[string]float64 { 34 | rv := map[string]float64{} 35 | for k, v := range in { 36 | rv[v] = math.Pow(10, k) 37 | } 38 | return rv 39 | } 40 | 41 | var riParseRegex *regexp.Regexp 42 | 43 | func init() { 44 | ri := `^([\-0-9.]+)\s?([` 45 | for _, v := range siPrefixTable { 46 | ri += v 47 | } 48 | ri += `]?)(.*)` 49 | 50 | riParseRegex = regexp.MustCompile(ri) 51 | } 52 | 53 | // ComputeSI finds the most appropriate SI prefix for the given number 54 | // and returns the prefix along with the value adjusted to be within 55 | // that prefix. 56 | // 57 | // See also: SI, ParseSI. 58 | // 59 | // e.g. ComputeSI(2.2345e-12) -> (2.2345, "p") 60 | func ComputeSI(input float64) (float64, string) { 61 | if input == 0 { 62 | return 0, "" 63 | } 64 | mag := math.Abs(input) 65 | exponent := math.Floor(logn(mag, 10)) 66 | exponent = math.Floor(exponent/3) * 3 67 | 68 | value := mag / math.Pow(10, exponent) 69 | 70 | // Handle special case where value is exactly 1000.0 71 | // Should return 1M instead of 1000k 72 | if value == 1000.0 { 73 | exponent += 3 74 | value = mag / math.Pow(10, exponent) 75 | } 76 | 77 | value = math.Copysign(value, input) 78 | 79 | prefix := siPrefixTable[exponent] 80 | return value, prefix 81 | } 82 | 83 | // SI returns a string with default formatting. 84 | // 85 | // SI uses Ftoa to format float value, removing trailing zeros. 86 | // 87 | // See also: ComputeSI, ParseSI. 88 | // 89 | // e.g. SI(1000000, B) -> 1MB 90 | // e.g. SI(2.2345e-12, "F") -> 2.2345pF 91 | func SI(input float64, unit string) string { 92 | value, prefix := ComputeSI(input) 93 | return Ftoa(value) + " " + prefix + unit 94 | } 95 | 96 | var errInvalid = errors.New("invalid input") 97 | 98 | // ParseSI parses an SI string back into the number and unit. 99 | // 100 | // See also: SI, ComputeSI. 101 | // 102 | // e.g. ParseSI(2.2345pF) -> (2.2345e-12, "F", nil) 103 | func ParseSI(input string) (float64, string, error) { 104 | found := riParseRegex.FindStringSubmatch(input) 105 | if len(found) != 4 { 106 | return 0, "", errInvalid 107 | } 108 | mag := revSIPrefixTable[found[2]] 109 | unit := found[3] 110 | 111 | base, err := strconv.ParseFloat(found[1], 64) 112 | return base * mag, unit, err 113 | } 114 | -------------------------------------------------------------------------------- /profiler/sink/file.go: -------------------------------------------------------------------------------- 1 | package sink 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | 10 | "github.com/geckoboard/prism/profiler" 11 | ) 12 | 13 | var ( 14 | profilePrefix = "profile-" 15 | badCharRegex = regexp.MustCompile(`[\./\\]`) 16 | ) 17 | 18 | type fileSink struct { 19 | outputDir string 20 | sigChan chan struct{} 21 | inputChan chan *profiler.Profile 22 | } 23 | 24 | // NewFileSink creates a new profile entry sink instance which stores profiles 25 | // to disk at the folder specified by outputDir. 26 | func NewFileSink(outputDir string) profiler.Sink { 27 | return &fileSink{ 28 | outputDir: outputDir, 29 | sigChan: make(chan struct{}, 0), 30 | } 31 | } 32 | 33 | // Initialize the sink. 34 | func (s *fileSink) Open(inputBufferSize int) error { 35 | // Ensure that ouptut folder exists 36 | err := os.MkdirAll(s.outputDir, os.ModeDir|os.ModePerm) 37 | if err != nil { 38 | return err 39 | } 40 | fmt.Fprintf(os.Stderr, "profiler: saving profiles to %s\n", s.outputDir) 41 | 42 | s.inputChan = make(chan *profiler.Profile, inputBufferSize) 43 | 44 | // start worker and wait for ready signal 45 | go s.worker() 46 | <-s.sigChan 47 | return nil 48 | } 49 | 50 | // Shutdown the sink. 51 | func (s *fileSink) Close() error { 52 | // Signal worker to exit and wait for confirmation 53 | close(s.inputChan) 54 | <-s.sigChan 55 | close(s.sigChan) 56 | return nil 57 | } 58 | 59 | // Get a channel for piping profile entries to the sink. 60 | func (s *fileSink) Input() chan<- *profiler.Profile { 61 | return s.inputChan 62 | } 63 | 64 | func (s *fileSink) worker() { 65 | // Signal that worker has started 66 | s.sigChan <- struct{}{} 67 | defer func() { 68 | // Signal that we have stopped 69 | s.sigChan <- struct{}{} 70 | }() 71 | 72 | for { 73 | profile, sinkOpen := <-s.inputChan 74 | if !sinkOpen { 75 | return 76 | } 77 | 78 | fpath := outputFile(s.outputDir, profile, "json") 79 | f, err := os.Create(fpath) 80 | if err != nil { 81 | fmt.Fprintf(os.Stderr, "profiler: could not create output file %q due to %s; dropping profile\n", fpath, err.Error()) 82 | continue 83 | } 84 | 85 | data, err := json.Marshal(profile) 86 | if err != nil { 87 | fmt.Fprintf(os.Stderr, "profiler: error marshalling profile: %s; dropping profile\n", err.Error()) 88 | continue 89 | } 90 | f.Write(data) 91 | f.Close() 92 | } 93 | } 94 | 95 | // Construct the path to a profile file for this entry. This function will 96 | // also pass the path through filepath.Clean to ensure that the proper slashes 97 | // are used depending on the host OS. 98 | func outputFile(outputDir string, profile *profiler.Profile, extension string) string { 99 | return filepath.Clean( 100 | fmt.Sprintf( 101 | "%s/%s%s-%d-%d.%s", 102 | outputDir, 103 | profilePrefix, 104 | badCharRegex.ReplaceAllString(profile.Target.FnName, "_"), 105 | profile.CreatedAt.UnixNano(), 106 | profile.ID, 107 | extension, 108 | ), 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /tools/callgraph.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "strings" 5 | 6 | "golang.org/x/tools/go/callgraph" 7 | "golang.org/x/tools/go/callgraph/rta" 8 | "golang.org/x/tools/go/ssa" 9 | ) 10 | 11 | // CallGraphNode models a section of the callgraph that is reachable through 12 | // a Target root node via one or more hops. 13 | type CallGraphNode struct { 14 | // A fully qualified function name reachable through a ProfileTarget. 15 | Name string 16 | 17 | // Number of hops from the callgraph entrypoint (root). 18 | Depth int 19 | } 20 | 21 | // CallGraph is a slice of callgraph nodes obtained by performing 22 | // Rapid Type Analysis (RTA) on a ProfileTarget. 23 | type CallGraph []*CallGraphNode 24 | 25 | // ProfileTarget encapsulates the SSA representation of a function that serves 26 | // as an entrypoint for applying profiler instrumentation code to itself and 27 | // any functions reachable through it. 28 | type ProfileTarget struct { 29 | // The fully qualified function name for the target. 30 | QualifiedName string 31 | 32 | // The fully qualified package name for the analyzed go package. 33 | PkgPrefix string 34 | 35 | // The SSA representation of the target. We rely on this to perform 36 | // RTA analysis so we can discover any reachable functions from this endpoint 37 | ssaFunc *ssa.Function 38 | } 39 | 40 | // CallGraph constructs a callgraph containing the list of qualified function names in 41 | // the project package and its sub-packages that are reachable via a call to 42 | // the profile target. 43 | // 44 | // The discovery of any functions reachable by the endpoint is facilitated by 45 | // the use of Rapid Type Analysis (RTA). 46 | // 47 | // The discovery algorithm only considers functions whose FQN begins with the 48 | // processed root package name. This includes any vendored dependencies. 49 | func (pt *ProfileTarget) CallGraph() CallGraph { 50 | cg := make(CallGraph, 0) 51 | if pt.ssaFunc == nil { 52 | return append(cg, &CallGraphNode{ 53 | Name: pt.QualifiedName, 54 | }) 55 | } 56 | 57 | var visitFn func(node *callgraph.Node, depth int) 58 | calleeCache := make(map[string]struct{}, 0) 59 | visitFn = func(node *callgraph.Node, depth int) { 60 | target := ssaQualifiedFuncName(node.Func) 61 | 62 | if !includeInGraph(target, pt.PkgPrefix) { 63 | return 64 | } 65 | 66 | // Watch out for callgraph loops; if we have already visited 67 | // this edge bail out 68 | if _, exists := calleeCache[target]; exists { 69 | return 70 | } 71 | calleeCache[target] = struct{}{} 72 | 73 | cg = append(cg, &CallGraphNode{ 74 | Name: target, 75 | Depth: depth, 76 | }) 77 | 78 | // Visit edges 79 | for _, outEdge := range node.Out { 80 | visitFn(outEdge.Callee, depth+1) 81 | } 82 | } 83 | 84 | // Build and traverse RTA graph starting at entrypoint. 85 | rtaRes := rta.Analyze([]*ssa.Function{pt.ssaFunc}, true) 86 | visitFn(rtaRes.CallGraph.Root, 0) 87 | 88 | return cg 89 | } 90 | 91 | // Check if target can be include in callgraph. 92 | func includeInGraph(target string, pkgPrefix string) bool { 93 | return strings.HasPrefix(target, pkgPrefix) 94 | } 95 | -------------------------------------------------------------------------------- /profiler/profiler_test.go: -------------------------------------------------------------------------------- 1 | package profiler 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestProfiler(t *testing.T) { 10 | nestedCallName := "func2" 11 | 12 | sink := newBufferedSink() 13 | Init(sink, "profiler-test") 14 | 15 | BeginProfile("func1") 16 | <-time.After(5 * time.Millisecond) 17 | 18 | // Invoke EndProfile/Enter/Leave from a different go-routine. These 19 | // calls should be ignored as there is no active profile running in 20 | // the go-routine. 21 | var wg sync.WaitGroup 22 | wg.Add(2) 23 | for depth := 0; depth < 2; depth++ { 24 | go func(depth int) { 25 | defer wg.Done() 26 | if depth == 0 { 27 | // Calling EndProfile should not interfere with the 28 | // profile data collected by the other goroutine 29 | defer EndProfile() 30 | } else { 31 | Enter(nestedCallName) 32 | defer Leave() 33 | } 34 | }(depth) 35 | } 36 | 37 | Enter(nestedCallName) 38 | <-time.After(10 * time.Millisecond) 39 | Leave() 40 | 41 | wg.Wait() 42 | EndProfile() 43 | 44 | // Shutdown and flush sink 45 | Shutdown() 46 | 47 | expEntries := 1 48 | if len(sink.buffer) != expEntries { 49 | t.Fatalf("expected sink to capture %d entries; got %d", expEntries, len(sink.buffer)) 50 | } 51 | 52 | profile := sink.buffer[0] 53 | 54 | expID := threadID() 55 | if profile.ID != expID { 56 | t.Fatalf("expected profile ID to be %d; got %d", expID, profile.ID) 57 | } 58 | 59 | expInvocations := 1 60 | if len(profile.Target.NestedCalls) != expInvocations { 61 | t.Fatalf("expected profile target func1 to capture %d unique nested calls; got %d", expInvocations, len(profile.Target.NestedCalls)) 62 | } 63 | 64 | nestedCall := profile.Target.NestedCalls[0] 65 | if nestedCall.FnName != nestedCallName { 66 | t.Fatalf("expected nested call name to be %q; got %q", nestedCallName, nestedCall.FnName) 67 | } 68 | 69 | if nestedCall.Invocations != expInvocations { 70 | t.Fatalf("expected nested call %q to be invoked %d times; got %d", nestedCallName, expInvocations, nestedCall.Invocations) 71 | } 72 | } 73 | 74 | type bufferedSink struct { 75 | sigChan chan struct{} 76 | inputChan chan *Profile 77 | buffer []*Profile 78 | } 79 | 80 | func newBufferedSink() *bufferedSink { 81 | return &bufferedSink{ 82 | sigChan: make(chan struct{}, 0), 83 | buffer: make([]*Profile, 0), 84 | } 85 | } 86 | 87 | func (s *bufferedSink) Open(_ int) error { 88 | s.inputChan = make(chan *Profile, 0) 89 | go s.worker() 90 | <-s.sigChan 91 | return nil 92 | } 93 | 94 | // Shutdown the sink. 95 | func (s *bufferedSink) Close() error { 96 | // Signal worker to exit and wait for confirmation 97 | close(s.inputChan) 98 | <-s.sigChan 99 | close(s.sigChan) 100 | return nil 101 | } 102 | 103 | // Get a channel for piping profile entries to the sink. 104 | func (s *bufferedSink) Input() chan<- *Profile { 105 | return s.inputChan 106 | } 107 | 108 | func (s *bufferedSink) worker() { 109 | // Signal that worker has started 110 | s.sigChan <- struct{}{} 111 | defer func() { 112 | // Signal that we have stopped 113 | s.sigChan <- struct{}{} 114 | }() 115 | 116 | for { 117 | profile, sinkOpen := <-s.inputChan 118 | if !sinkOpen { 119 | return 120 | } 121 | s.buffer = append(s.buffer, profile) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/bytes.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | // IEC Sizes. 12 | // kibis of bits 13 | const ( 14 | Byte = 1 << (iota * 10) 15 | KiByte 16 | MiByte 17 | GiByte 18 | TiByte 19 | PiByte 20 | EiByte 21 | ) 22 | 23 | // SI Sizes. 24 | const ( 25 | IByte = 1 26 | KByte = IByte * 1000 27 | MByte = KByte * 1000 28 | GByte = MByte * 1000 29 | TByte = GByte * 1000 30 | PByte = TByte * 1000 31 | EByte = PByte * 1000 32 | ) 33 | 34 | var bytesSizeTable = map[string]uint64{ 35 | "b": Byte, 36 | "kib": KiByte, 37 | "kb": KByte, 38 | "mib": MiByte, 39 | "mb": MByte, 40 | "gib": GiByte, 41 | "gb": GByte, 42 | "tib": TiByte, 43 | "tb": TByte, 44 | "pib": PiByte, 45 | "pb": PByte, 46 | "eib": EiByte, 47 | "eb": EByte, 48 | // Without suffix 49 | "": Byte, 50 | "ki": KiByte, 51 | "k": KByte, 52 | "mi": MiByte, 53 | "m": MByte, 54 | "gi": GiByte, 55 | "g": GByte, 56 | "ti": TiByte, 57 | "t": TByte, 58 | "pi": PiByte, 59 | "p": PByte, 60 | "ei": EiByte, 61 | "e": EByte, 62 | } 63 | 64 | func logn(n, b float64) float64 { 65 | return math.Log(n) / math.Log(b) 66 | } 67 | 68 | func humanateBytes(s uint64, base float64, sizes []string) string { 69 | if s < 10 { 70 | return fmt.Sprintf("%d B", s) 71 | } 72 | e := math.Floor(logn(float64(s), base)) 73 | suffix := sizes[int(e)] 74 | val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10 75 | f := "%.0f %s" 76 | if val < 10 { 77 | f = "%.1f %s" 78 | } 79 | 80 | return fmt.Sprintf(f, val, suffix) 81 | } 82 | 83 | // Bytes produces a human readable representation of an SI size. 84 | // 85 | // See also: ParseBytes. 86 | // 87 | // Bytes(82854982) -> 83MB 88 | func Bytes(s uint64) string { 89 | sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"} 90 | return humanateBytes(s, 1000, sizes) 91 | } 92 | 93 | // IBytes produces a human readable representation of an IEC size. 94 | // 95 | // See also: ParseBytes. 96 | // 97 | // IBytes(82854982) -> 79MiB 98 | func IBytes(s uint64) string { 99 | sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} 100 | return humanateBytes(s, 1024, sizes) 101 | } 102 | 103 | // ParseBytes parses a string representation of bytes into the number 104 | // of bytes it represents. 105 | // 106 | // See Also: Bytes, IBytes. 107 | // 108 | // ParseBytes("42MB") -> 42000000, nil 109 | // ParseBytes("42mib") -> 44040192, nil 110 | func ParseBytes(s string) (uint64, error) { 111 | lastDigit := 0 112 | hasComma := false 113 | for _, r := range s { 114 | if !(unicode.IsDigit(r) || r == '.' || r == ',') { 115 | break 116 | } 117 | if r == ',' { 118 | hasComma = true 119 | } 120 | lastDigit++ 121 | } 122 | 123 | num := s[:lastDigit] 124 | if hasComma { 125 | num = strings.Replace(num, ",", "", -1) 126 | } 127 | 128 | f, err := strconv.ParseFloat(num, 64) 129 | if err != nil { 130 | return 0, err 131 | } 132 | 133 | extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) 134 | if m, ok := bytesSizeTable[extra]; ok { 135 | f *= float64(m) 136 | if f >= math.MaxUint64 { 137 | return 0, fmt.Errorf("too large: %v", s) 138 | } 139 | return uint64(f), nil 140 | } 141 | 142 | return 0, fmt.Errorf("unhandled size name: %v", extra) 143 | } 144 | -------------------------------------------------------------------------------- /tools/ssa.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "path/filepath" 7 | "strings" 8 | 9 | "golang.org/x/tools/go/loader" 10 | "golang.org/x/tools/go/ssa" 11 | "golang.org/x/tools/go/ssa/ssautil" 12 | ) 13 | 14 | // Collect the set of go files that comprise a package and its included 15 | // sub-packages and create a static single-assignment representation 16 | // of the source code. 17 | // 18 | // Process the list of SSA function representations exposed by the program and 19 | // select the entries that can be used as injector targets. An entry is considered 20 | // to be a valid target if its fully qualified name starts with the supplied 21 | // package name. 22 | // 23 | // The function maps valid entries to their fully qualified names and returns them as a map. 24 | func ssaCandidates(pathToPackage, fqPkgPrefix, goPath string) (map[string]*ssa.Function, error) { 25 | // Fetch all package-wide go files and pass them to a loader 26 | goFiles, err := filepath.Glob(fmt.Sprintf("%s*.go", pathToPackage)) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | var conf loader.Config 32 | conf.CreateFromFilenames(fqPkgPrefix, goFiles...) 33 | conf.Build = &build.Default 34 | conf.Build.GOPATH = goPath 35 | conf.Cwd = pathToPackage 36 | loadedProg, err := conf.Load() 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | // Convert to SSA format 42 | ssaProg := ssautil.CreateProgram(loadedProg, ssa.BuilderMode(0)) 43 | ssaProg.Build() 44 | 45 | // Build candidate map 46 | candidates := make(map[string]*ssa.Function, 0) 47 | for ssaFn := range ssautil.AllFunctions(ssaProg) { 48 | target := ssaQualifiedFuncName(ssaFn) 49 | if strings.HasPrefix(string(target), fqPkgPrefix) { 50 | candidates[target] = ssaFn 51 | } 52 | } 53 | return candidates, nil 54 | } 55 | 56 | // Generate fully qualified name for SSA function representation that includes 57 | // the name of the package. This is achieved by invoking the String() method on 58 | // the supplied SSA function and manipulating its output. 59 | func ssaQualifiedFuncName(fn *ssa.Function) string { 60 | // Normalize fn.String() output by removing parenthesis and star operator 61 | normalized := stripCharRegex.ReplaceAllString(fn.String(), "") 62 | 63 | // The normalized string representation of the function concatenates 64 | // the package name and the function name (incl. receiver) with a dot. 65 | // We need to replace that with a '/' 66 | if fn.Pkg != nil { 67 | pkgName := strings.TrimPrefix(fn.Pkg.String(), "package ") 68 | pkgLen := len(pkgName) 69 | normalized = normalized[0:pkgLen] + "/" + normalized[pkgLen+1:] 70 | } 71 | return normalized 72 | } 73 | 74 | // Construct fully qualified package name from a file path by stripping the 75 | // go workspace location from its absolute path representation. 76 | func qualifiedPkgName(pathToPackage string) (string, error) { 77 | absPackageDir, err := filepath.Abs(filepath.Dir(pathToPackage)) 78 | if err != nil { 79 | return "", err 80 | } 81 | 82 | skipLen := strings.Index(absPackageDir, "/src/") + 5 83 | return absPackageDir[skipLen:], nil 84 | } 85 | 86 | // Get the go workspace location from an absolute package path. 87 | func packageWorkspace(pathToPackage string) (string, error) { 88 | absPackageDir, err := filepath.Abs(filepath.Dir(pathToPackage)) 89 | if err != nil { 90 | return "", err 91 | } 92 | 93 | skipLen := strings.Index(absPackageDir, "/src/") 94 | return absPackageDir[:skipLen], nil 95 | } 96 | -------------------------------------------------------------------------------- /tools/ast.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "bytes" 5 | "go/ast" 6 | "strings" 7 | 8 | "golang.org/x/tools/go/ast/astutil" 9 | ) 10 | 11 | // A visitor for AST nodes representing functions. 12 | type funcVisitor struct { 13 | // The file we are currently visiting. 14 | parsedFile *parsedGoFile 15 | 16 | // Additional package imports that need to be injected to the file. 17 | // Modeled as a map to filter duplicates. 18 | extraImports map[string]struct{} 19 | 20 | // The patch function to apply to matching targets. 21 | patchFn PatchFunc 22 | 23 | // The unique list of functions that we need to hook indexed by FQN. 24 | uniqueTargetMap map[string]*CallGraphNode 25 | 26 | // Flag indicating whether the AST was modified. 27 | modifiedAST bool 28 | 29 | // The number of successfully applied patches. 30 | patchCount int 31 | } 32 | 33 | // Create a new function node visitor. 34 | func newFuncVisitor(uniqueTargetMap map[string]*CallGraphNode, patchFn PatchFunc) *funcVisitor { 35 | return &funcVisitor{ 36 | patchFn: patchFn, 37 | uniqueTargetMap: uniqueTargetMap, 38 | } 39 | } 40 | 41 | // Apply the visitor to a parsedFile and return a flag indicating whether the AST was modified. 42 | func (v *funcVisitor) Process(parsedFile *parsedGoFile) (modifiedAST bool, patchCount int) { 43 | // Reset visitor state 44 | v.parsedFile = parsedFile 45 | v.extraImports = make(map[string]struct{}, 0) 46 | v.modifiedAST = false 47 | v.patchCount = 0 48 | ast.Walk(v, parsedFile.astFile) 49 | 50 | if len(v.extraImports) != 0 { 51 | for pkgName := range v.extraImports { 52 | tokens := strings.Fields(pkgName) 53 | if len(tokens) > 1 { 54 | astutil.AddNamedImport(parsedFile.fset, parsedFile.astFile, tokens[0], tokens[1]) 55 | } else { 56 | astutil.AddImport(parsedFile.fset, parsedFile.astFile, tokens[0]) 57 | } 58 | } 59 | v.modifiedAST = true 60 | } 61 | 62 | return v.modifiedAST, v.patchCount 63 | } 64 | 65 | // Implements ast.Visitor. Recursively looks for AST nodes that correspond to our 66 | // targets and applies a PatchFunc. 67 | func (v *funcVisitor) Visit(node ast.Node) ast.Visitor { 68 | // We are only interested in function nodes 69 | fnDecl, isFnNode := node.(*ast.FuncDecl) 70 | if !isFnNode { 71 | return v 72 | } 73 | 74 | // Ignore forward function declarations 75 | if fnDecl.Body == nil { 76 | return nil 77 | } 78 | 79 | // Check if we need to hook this function 80 | fqName := qualifiedNodeName(fnDecl, v.parsedFile.pkgName) 81 | cgNode, isTarget := v.uniqueTargetMap[fqName] 82 | if !isTarget { 83 | return nil 84 | } 85 | 86 | modified, extraImports := v.patchFn(cgNode, fnDecl.Body) 87 | if modified { 88 | v.modifiedAST = true 89 | v.patchCount++ 90 | } 91 | if extraImports != nil { 92 | for _, name := range extraImports { 93 | v.extraImports[name] = struct{}{} 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | 100 | // Returns the fully qualified name for function declaration given its AST node. 101 | func qualifiedNodeName(fnDecl *ast.FuncDecl, pkgName string) string { 102 | buf := bytes.NewBufferString(pkgName) 103 | buf.WriteByte('/') 104 | 105 | // Examine receiver 106 | if fnDecl.Recv != nil { 107 | for _, rcvField := range fnDecl.Recv.List { 108 | // We only care for identifiers and star expressions 109 | switch rcvType := rcvField.Type.(type) { 110 | case *ast.StarExpr: // e.g (b *Bar) 111 | buf.WriteString(rcvType.X.(*ast.Ident).Name) 112 | buf.WriteByte('.') 113 | case *ast.Ident: 114 | buf.WriteString(rcvType.Name) 115 | buf.WriteByte('.') 116 | } 117 | } 118 | } 119 | 120 | // Finally append fn name 121 | buf.WriteString(fnDecl.Name.Name) 122 | return buf.String() 123 | } 124 | -------------------------------------------------------------------------------- /tools/ast_test.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | "testing" 8 | ) 9 | 10 | func TestQualifiedCodeName(t *testing.T) { 11 | parsedFile := mockParsedGoFile(t) 12 | 13 | specs := []struct { 14 | FnName string 15 | ExpQualifiedName string 16 | }{ 17 | {"NoReceiver", parsedFile.pkgName + "/NoReceiver"}, 18 | {"Receiver", parsedFile.pkgName + "/MyFoo.Receiver"}, 19 | {"PtrReceiver", parsedFile.pkgName + "/MyFoo.PtrReceiver"}, 20 | } 21 | 22 | for specIndex, spec := range specs { 23 | var fnDecl *ast.FuncDecl 24 | for _, decl := range parsedFile.astFile.Decls { 25 | if d, isFnDecl := decl.(*ast.FuncDecl); isFnDecl { 26 | if d.Name.Name == spec.FnName { 27 | fnDecl = d 28 | break 29 | } 30 | } 31 | } 32 | 33 | if fnDecl == nil { 34 | t.Errorf("[spec %d] could not lookup declaration of %q in test file", specIndex, spec.FnName) 35 | continue 36 | } 37 | 38 | qualifiedName := qualifiedNodeName(fnDecl, parsedFile.pkgName) 39 | if qualifiedName != spec.ExpQualifiedName { 40 | t.Errorf("[spec %d] expected to get qualified name %q; got %q", specIndex, spec.ExpQualifiedName, qualifiedName) 41 | } 42 | } 43 | } 44 | 45 | func TestFuncVisitorImport(t *testing.T) { 46 | parsedFile := mockParsedGoFile(t) 47 | targetMap := map[string]*CallGraphNode{ 48 | parsedFile.pkgName + "/NoReceiver": &CallGraphNode{ 49 | Name: "NoReceiver", 50 | }, 51 | } 52 | visitor := newFuncVisitor( 53 | targetMap, 54 | func(_ *CallGraphNode, _ *ast.BlockStmt) (modifiedAST bool, extraImports []string) { 55 | return true, []string{ 56 | "github.com/foo/bar", 57 | "namedImport github.com/foo/baz", 58 | } 59 | }, 60 | ) 61 | 62 | modifiedAST, patchCount := visitor.Process(parsedFile) 63 | if !modifiedAST { 64 | t.Fatal("expected func visitor to modify the file AST") 65 | } 66 | 67 | expPatchCount := 1 68 | if patchCount != expPatchCount { 69 | t.Fatalf("expected patchCount to be %d; got %d", expPatchCount, patchCount) 70 | } 71 | 72 | expImportCount := 2 73 | if len(parsedFile.astFile.Imports) != expImportCount { 74 | t.Fatalf("expected import count to be %d; got %d", expImportCount, len(parsedFile.astFile.Imports)) 75 | } 76 | 77 | for importIndex, importDecl := range parsedFile.astFile.Imports { 78 | switch importDecl.Path.Value { 79 | case `"github.com/foo/bar"`: 80 | if importDecl.Name != nil { 81 | t.Errorf("[import %d] expected import %q to have nil name", importIndex, importDecl.Path.Value) 82 | } 83 | case `"github.com/foo/baz"`: 84 | if importDecl.Name == nil { 85 | t.Errorf("[import %d] expected named import %q to have non-nil name", importIndex, importDecl.Path.Value) 86 | } else if importDecl.Name.Name != "namedImport" { 87 | t.Errorf("[import %d] expected named import %q to be named as %q; got %q", importIndex, importDecl.Path.Value, "namedImport", importDecl.Name.Name) 88 | } 89 | default: 90 | t.Errorf("[import %d] unexpected import %q", importIndex, importDecl.Path.Value) 91 | } 92 | } 93 | } 94 | 95 | func mockParsedGoFile(t *testing.T) *parsedGoFile { 96 | filePath := "test.go" 97 | fqPkgName := "github.com/geckoboard/test" 98 | 99 | src := ` 100 | package foo 101 | 102 | type MyFoo struct{} 103 | 104 | // Forward declaration 105 | func NoReceiver() 106 | 107 | func NoReceiver(){} 108 | func (f MyFoo) Receiver(){} 109 | func (f *MyFoo) PtrReceiver(arg int){} 110 | ` 111 | 112 | fset := token.NewFileSet() 113 | astFile, err := parser.ParseFile(fset, filePath, src, 0) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | 118 | return &parsedGoFile{ 119 | pkgName: fqPkgName, 120 | filePath: filePath, 121 | fset: fset, 122 | astFile: astFile, 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/times.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sort" 7 | "time" 8 | ) 9 | 10 | // Seconds-based time units 11 | const ( 12 | Day = 24 * time.Hour 13 | Week = 7 * Day 14 | Month = 30 * Day 15 | Year = 12 * Month 16 | LongTime = 37 * Year 17 | ) 18 | 19 | // Time formats a time into a relative string. 20 | // 21 | // Time(someT) -> "3 weeks ago" 22 | func Time(then time.Time) string { 23 | return RelTime(then, time.Now(), "ago", "from now") 24 | } 25 | 26 | // A RelTimeMagnitude struct contains a relative time point at which 27 | // the relative format of time will switch to a new format string. A 28 | // slice of these in ascending order by their "D" field is passed to 29 | // CustomRelTime to format durations. 30 | // 31 | // The Format field is a string that may contain a "%s" which will be 32 | // replaced with the appropriate signed label (e.g. "ago" or "from 33 | // now") and a "%d" that will be replaced by the quantity. 34 | // 35 | // The DivBy field is the amount of time the time difference must be 36 | // divided by in order to display correctly. 37 | // 38 | // e.g. if D is 2*time.Minute and you want to display "%d minutes %s" 39 | // DivBy should be time.Minute so whatever the duration is will be 40 | // expressed in minutes. 41 | type RelTimeMagnitude struct { 42 | D time.Duration 43 | Format string 44 | DivBy time.Duration 45 | } 46 | 47 | var defaultMagnitudes = []RelTimeMagnitude{ 48 | {time.Second, "now", time.Second}, 49 | {2 * time.Second, "1 second %s", 1}, 50 | {time.Minute, "%d seconds %s", time.Second}, 51 | {2 * time.Minute, "1 minute %s", 1}, 52 | {time.Hour, "%d minutes %s", time.Minute}, 53 | {2 * time.Hour, "1 hour %s", 1}, 54 | {Day, "%d hours %s", time.Hour}, 55 | {2 * Day, "1 day %s", 1}, 56 | {Week, "%d days %s", Day}, 57 | {2 * Week, "1 week %s", 1}, 58 | {Month, "%d weeks %s", Week}, 59 | {2 * Month, "1 month %s", 1}, 60 | {Year, "%d months %s", Month}, 61 | {18 * Month, "1 year %s", 1}, 62 | {2 * Year, "2 years %s", 1}, 63 | {LongTime, "%d years %s", Year}, 64 | {math.MaxInt64, "a long while %s", 1}, 65 | } 66 | 67 | // RelTime formats a time into a relative string. 68 | // 69 | // It takes two times and two labels. In addition to the generic time 70 | // delta string (e.g. 5 minutes), the labels are used applied so that 71 | // the label corresponding to the smaller time is applied. 72 | // 73 | // RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier" 74 | func RelTime(a, b time.Time, albl, blbl string) string { 75 | return CustomRelTime(a, b, albl, blbl, defaultMagnitudes) 76 | } 77 | 78 | // CustomRelTime formats a time into a relative string. 79 | // 80 | // It takes two times two labels and a table of relative time formats. 81 | // In addition to the generic time delta string (e.g. 5 minutes), the 82 | // labels are used applied so that the label corresponding to the 83 | // smaller time is applied. 84 | func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string { 85 | lbl := albl 86 | diff := b.Sub(a) 87 | 88 | if a.After(b) { 89 | lbl = blbl 90 | diff = a.Sub(b) 91 | } 92 | 93 | n := sort.Search(len(magnitudes), func(i int) bool { 94 | return magnitudes[i].D >= diff 95 | }) 96 | 97 | if n >= len(magnitudes) { 98 | n = len(magnitudes) - 1 99 | } 100 | mag := magnitudes[n] 101 | args := []interface{}{} 102 | escaped := false 103 | for _, ch := range mag.Format { 104 | if escaped { 105 | switch ch { 106 | case 's': 107 | args = append(args, lbl) 108 | case 'd': 109 | args = append(args, diff/mag.DivBy) 110 | } 111 | escaped = false 112 | } else { 113 | escaped = ch == '%' 114 | } 115 | } 116 | return fmt.Sprintf(mag.Format, args...) 117 | } 118 | -------------------------------------------------------------------------------- /cmd/profile_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "strings" 11 | "testing" 12 | 13 | "gopkg.in/urfave/cli.v1" 14 | ) 15 | 16 | func TestProfile(t *testing.T) { 17 | wsDir, pkgDir, pkgName := mockPackageWithVendoredDeps(t, true) 18 | defer os.RemoveAll(wsDir) 19 | 20 | // Mock args 21 | set := flag.NewFlagSet("test", 0) 22 | set.String("profile-dir", wsDir, "") 23 | set.String("build-cmd", "go build -o artifact", "") 24 | set.String("run-cmd", "./artifact", "") 25 | set.Bool("no-ansi", true, "") 26 | set.Parse([]string{pkgDir}) 27 | targets := cli.StringSlice{pkgName + "/main"} 28 | targetFlag := &cli.StringSliceFlag{ 29 | Name: "profile-target", 30 | Value: &targets, 31 | } 32 | targetFlag.Apply(set) 33 | ctx := cli.NewContext(nil, set, nil) 34 | 35 | // Redirect stdout and stderr 36 | stdOut := os.Stdout 37 | stdErr := os.Stderr 38 | pRead, pWrite, err := os.Pipe() 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | os.Stdout = pWrite 43 | os.Stderr = pWrite 44 | 45 | // Restore stdout/err incase of a panic 46 | defer func() { 47 | os.Stdout = stdOut 48 | os.Stderr = stdErr 49 | }() 50 | 51 | // Profile package and capture output 52 | err = ProfileProject(ctx) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | // Drain pipe and restore stdout 58 | var buf bytes.Buffer 59 | pWrite.Close() 60 | io.Copy(&buf, pRead) 61 | pRead.Close() 62 | os.Stdout = stdOut 63 | os.Stderr = stdErr 64 | 65 | outputLines := strings.Split(strings.Trim(buf.String(), "\n"), "\n") 66 | expLines := 5 67 | if len(outputLines) != expLines { 68 | t.Fatalf("expected profile cmd output to emit %d output lines; got %d", expLines, len(outputLines)) 69 | } 70 | 71 | specs := []struct { 72 | Line int 73 | ExpText string 74 | }{ 75 | {1, "profile: updated 1 files and applied 4 patches"}, 76 | {2, "profile: building patched project (go build -o artifact)"}, 77 | {3, "profile: running patched project (./artifact)"}, 78 | {4, fmt.Sprintf("profile: [run] > profiler: saving profiles to %s", wsDir)}, 79 | } 80 | 81 | for _, spec := range specs { 82 | if outputLines[spec.Line] != spec.ExpText { 83 | t.Errorf("[output line %d] expected text to match %q; got %q", spec.Line, spec.ExpText, outputLines[spec.Line]) 84 | } 85 | } 86 | } 87 | 88 | func mockPackageWithVendoredDeps(t *testing.T, useGodeps bool) (workspaceDir, pkgDir, pkgName string) { 89 | var otherPkgName string 90 | pkgName = "prism-mock" 91 | 92 | if useGodeps { 93 | otherPkgName = pkgName + "/Godeps/_workspace/other/pkg" 94 | } else { 95 | otherPkgName = pkgName + "/vendor/other/pkg" 96 | } 97 | 98 | pkgData := map[string]string{ 99 | otherPkgName: ` 100 | package other 101 | 102 | func DoStuff(){ 103 | } 104 | `, 105 | pkgName: ` 106 | package main 107 | 108 | import other "` + otherPkgName + `" 109 | 110 | type A struct { 111 | } 112 | 113 | func(a *A) DoStuff(){ 114 | // Call to vendored dep 115 | other.DoStuff() 116 | } 117 | 118 | func DoStuff(){ 119 | a := &A{} 120 | a.DoStuff() 121 | 122 | // The callgraph generator should not visit this function a second time 123 | a.DoStuff() 124 | } 125 | 126 | func main(){ 127 | DoStuff() 128 | } 129 | `, 130 | } 131 | 132 | workspaceDir, err := ioutil.TempDir("", "prism-test") 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | 137 | for name, src := range pkgData { 138 | dir := workspaceDir + "/src/" + name + "/" 139 | err = os.MkdirAll(dir, os.ModeDir|os.ModePerm) 140 | if err != nil { 141 | os.RemoveAll(workspaceDir) 142 | t.Fatalf("error creating workspace folder for package %q: %s", name, err) 143 | } 144 | 145 | err = ioutil.WriteFile(dir+"src.go", []byte(src), os.ModePerm) 146 | if err != nil { 147 | os.RemoveAll(workspaceDir) 148 | t.Fatalf("error creating package contents for package %q: %s", name, err) 149 | } 150 | } 151 | 152 | pkgDir = workspaceDir + "/src/" + pkgName + "/" 153 | return workspaceDir, pkgDir, pkgName 154 | } 155 | -------------------------------------------------------------------------------- /tools/injector_test.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "testing" 7 | ) 8 | 9 | func TestInjectProfilerBootstrap(t *testing.T) { 10 | profileDir := "/tmp/foo" 11 | profileLabel := "label" 12 | injectFn := InjectProfilerBootstrap(profileDir, profileLabel) 13 | 14 | cgNode := &CallGraphNode{ 15 | Name: "main", 16 | Depth: 0, 17 | } 18 | 19 | stmt := &ast.BlockStmt{ 20 | List: make([]ast.Stmt, 0), 21 | } 22 | 23 | modifiedAST, extraImports := injectFn(cgNode, stmt) 24 | 25 | if !modifiedAST { 26 | t.Fatal("expected injector to modify the AST") 27 | } 28 | 29 | expImports := append(profilerImports, sinkImports...) 30 | if !importsMatch(extraImports, expImports) { 31 | t.Fatalf("injector did not return the expected imports; got %v", extraImports) 32 | } 33 | 34 | expStmtCount := 2 35 | if len(stmt.List) != expStmtCount { 36 | t.Fatalf("expected injector to append %d statements; got %d", expStmtCount, len(stmt.List)) 37 | } 38 | 39 | expStmts := []string{ 40 | fmt.Sprintf("prismProfiler.Init(prismSink.NewFileSink(%q), %q)", profileDir, profileLabel), 41 | "defer prismProfiler.Shutdown()", 42 | } 43 | for stmtIndex, expStmt := range expStmts { 44 | expr, err := extractExpr(stmt.List[stmtIndex]) 45 | if err != nil { 46 | t.Errorf("[stmt %d] : %v", stmtIndex, err) 47 | continue 48 | } 49 | 50 | if expr != expStmt { 51 | t.Errorf("[stmt %d] expected expression to be %q; got %q", stmtIndex, expStmt, expr) 52 | } 53 | } 54 | } 55 | 56 | func TestInjectProfiler(t *testing.T) { 57 | injectFn := InjectProfiler() 58 | 59 | cgNode := &CallGraphNode{ 60 | Name: "DoStuff", 61 | Depth: 1, 62 | } 63 | 64 | stmt := &ast.BlockStmt{ 65 | List: make([]ast.Stmt, 0), 66 | } 67 | 68 | modifiedAST, extraImports := injectFn(cgNode, stmt) 69 | 70 | if !modifiedAST { 71 | t.Fatal("expected injector to modify the AST") 72 | } 73 | 74 | expImports := profilerImports 75 | if !importsMatch(extraImports, expImports) { 76 | t.Fatalf("injector did not return the expected imports; got %v", extraImports) 77 | } 78 | 79 | expStmtCount := 2 80 | if len(stmt.List) != expStmtCount { 81 | t.Fatalf("expected injector to append %d statements; got %d", expStmtCount, len(stmt.List)) 82 | } 83 | 84 | expStmts := []string{ 85 | fmt.Sprintf("prismProfiler.Enter(%q)", cgNode.Name), 86 | "defer prismProfiler.Leave()", 87 | } 88 | for stmtIndex, expStmt := range expStmts { 89 | expr, err := extractExpr(stmt.List[stmtIndex]) 90 | if err != nil { 91 | t.Errorf("[stmt %d] : %v", stmtIndex, err) 92 | continue 93 | } 94 | 95 | if expr != expStmt { 96 | t.Errorf("[stmt %d] expected expression to be %q; got %q", stmtIndex, expStmt, expr) 97 | } 98 | } 99 | } 100 | 101 | func TestProfileFnSelection(t *testing.T) { 102 | specs := []struct { 103 | Depth int 104 | ExpEnterFn string 105 | ExpLeaveFn string 106 | }{ 107 | {0, "BeginProfile", "EndProfile"}, 108 | {1, "Enter", "Leave"}, 109 | {2, "Enter", "Leave"}, 110 | } 111 | 112 | for specIndex, spec := range specs { 113 | enterFn, leaveFn := profileFnName(spec.Depth) 114 | if enterFn != spec.ExpEnterFn { 115 | t.Errorf("[spec %d] expected enter fn to be %q; got %q", specIndex, spec.ExpEnterFn, enterFn) 116 | continue 117 | } 118 | if leaveFn != spec.ExpLeaveFn { 119 | t.Errorf("[spec %d] expected leave fn to be %q; got %q", specIndex, spec.ExpLeaveFn, leaveFn) 120 | continue 121 | } 122 | } 123 | } 124 | 125 | func extractExpr(stmt ast.Stmt) (string, error) { 126 | if exprStmt, isExpr := stmt.(*ast.ExprStmt); isExpr { 127 | if basicLitStmt, isLit := exprStmt.X.(*ast.BasicLit); isLit { 128 | return basicLitStmt.Value, nil 129 | } 130 | } 131 | 132 | return "", fmt.Errorf("statement does not contain a basic literal node") 133 | } 134 | 135 | func importsMatch(input, expected []string) bool { 136 | if len(input) != len(expected) { 137 | return false 138 | } 139 | 140 | for _, expImport := range expected { 141 | found := false 142 | for _, test := range input { 143 | if test == expImport { 144 | found = true 145 | break 146 | } 147 | } 148 | 149 | if !found { 150 | return false 151 | } 152 | } 153 | 154 | return true 155 | } 156 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/user" 7 | 8 | "github.com/geckoboard/prism/cmd" 9 | "gopkg.in/urfave/cli.v1" 10 | ) 11 | 12 | func main() { 13 | app := cli.NewApp() 14 | app.Name = "prism" 15 | app.Usage = "profiler injector and analysis tool" 16 | app.Version = "0.0.1" 17 | app.Commands = []cli.Command{ 18 | { 19 | Name: "profile", 20 | Usage: "clone go project and inject profiler", 21 | Description: `Create a temp copy of a go project, attach profiler, build and run it.`, 22 | ArgsUsage: "path_to_project", 23 | 24 | Action: cmd.ProfileProject, 25 | Flags: []cli.Flag{ 26 | cli.StringFlag{ 27 | Name: "build-cmd", 28 | Value: "", 29 | Usage: "project build command", 30 | }, 31 | cli.StringFlag{ 32 | Name: "run-cmd", 33 | Value: `find . -d 1 -type f -name *\.go ! -name *_test\.go -exec go run {} +`, 34 | Usage: "project run command", 35 | }, 36 | cli.StringFlag{ 37 | Name: "output-dir, o", 38 | Value: os.TempDir(), 39 | Usage: "path for storing patched project version", 40 | }, 41 | cli.BoolFlag{ 42 | Name: "preserve-output", 43 | Usage: "preserve patched project post build", 44 | }, 45 | cli.StringSliceFlag{ 46 | Name: "profile-target, t", 47 | Value: &cli.StringSlice{}, 48 | Usage: "fully qualified function name to profile", 49 | }, 50 | cli.StringFlag{ 51 | Name: "profile-dir", 52 | Usage: "specify the output dir for captured profiles", 53 | Value: defaultOutputDir(), 54 | }, 55 | cli.StringFlag{ 56 | Name: "profile-label", 57 | Usage: `specify a label to be attached to captured profiles and displayed when using the "print" or "diff" commands`, 58 | }, 59 | cli.StringSliceFlag{ 60 | Name: "profile-vendored-pkg", 61 | Usage: "inject profile hooks to any vendored packages matching this regex. If left unspecified, no vendored packages will be hooked", 62 | Value: &cli.StringSlice{}, 63 | }, 64 | cli.BoolFlag{ 65 | Name: "no-ansi", 66 | Usage: "disable ansi output", 67 | }, 68 | }, 69 | }, 70 | { 71 | Name: "print", 72 | Usage: "pretty-print profile", 73 | Description: ``, 74 | ArgsUsage: "profile", 75 | Action: cmd.PrintProfile, 76 | Flags: []cli.Flag{ 77 | cli.StringFlag{ 78 | Name: "display-columns, dc", 79 | Value: "total,min,mean,max,invocations", 80 | Usage: fmt.Sprintf("columns to include in the output; supported options: %s", cmd.SupportedColumnNames()), 81 | }, 82 | cli.StringFlag{ 83 | Name: "display-format, df", 84 | Value: "time", 85 | Usage: "set the format for the output columns containing time values; supported options: time, percent", 86 | }, 87 | cli.StringFlag{ 88 | Name: "display-unit, du", 89 | Value: "ms", 90 | Usage: "set the unit for the output columns containing time values; supported options: auto, ms, us, ns", 91 | }, 92 | cli.Float64Flag{ 93 | Name: "display-threshold", 94 | Value: 0.0, 95 | Usage: "only show measurements for entries whose time exceeds the threshold. Unit is the same as --display-unit unless --display-format is set to percent in which case the threshold is applied to the percent value", 96 | }, 97 | cli.BoolFlag{ 98 | Name: "no-ansi", 99 | Usage: "disable ansi output", 100 | }, 101 | }, 102 | }, 103 | { 104 | Name: "diff", 105 | Usage: "visually compare profiles", 106 | Description: ``, 107 | ArgsUsage: "profile1 profile2 [...profile_n]", 108 | Action: cmd.DiffProfiles, 109 | Flags: []cli.Flag{ 110 | cli.StringFlag{ 111 | Name: "display-columns,dc", 112 | Value: "total,min,mean,max,invocations", 113 | Usage: fmt.Sprintf("columns to include in the diff output; supported options: %s", cmd.SupportedColumnNames()), 114 | }, 115 | cli.StringFlag{ 116 | Name: "display-unit, du", 117 | Value: "ms", 118 | Usage: "set the unit for the output columns containing time values; supported options: auto, ms, us, ns", 119 | }, 120 | cli.Float64Flag{ 121 | Name: "display-threshold", 122 | Value: 0.0, 123 | Usage: "only show measurements for entries whose delta time exceeds the threshold. Unit is the same as --display-unit", 124 | }, 125 | cli.BoolFlag{ 126 | Name: "no-ansi", 127 | Usage: "disable ansi output", 128 | }, 129 | }, 130 | }, 131 | } 132 | 133 | err := app.Run(os.Args) 134 | if err != nil { 135 | fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) 136 | os.Exit(1) 137 | } 138 | } 139 | 140 | // Get default output dir for profiles. 141 | func defaultOutputDir() string { 142 | usr, err := user.Current() 143 | if err != nil { 144 | panic(err) 145 | } 146 | return usr.HomeDir + "/prism" 147 | } 148 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/bigbytes.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | var ( 11 | bigIECExp = big.NewInt(1024) 12 | 13 | // BigByte is one byte in bit.Ints 14 | BigByte = big.NewInt(1) 15 | // BigKiByte is 1,024 bytes in bit.Ints 16 | BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp) 17 | // BigMiByte is 1,024 k bytes in bit.Ints 18 | BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp) 19 | // BigGiByte is 1,024 m bytes in bit.Ints 20 | BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp) 21 | // BigTiByte is 1,024 g bytes in bit.Ints 22 | BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp) 23 | // BigPiByte is 1,024 t bytes in bit.Ints 24 | BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp) 25 | // BigEiByte is 1,024 p bytes in bit.Ints 26 | BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp) 27 | // BigZiByte is 1,024 e bytes in bit.Ints 28 | BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp) 29 | // BigYiByte is 1,024 z bytes in bit.Ints 30 | BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp) 31 | ) 32 | 33 | var ( 34 | bigSIExp = big.NewInt(1000) 35 | 36 | // BigSIByte is one SI byte in big.Ints 37 | BigSIByte = big.NewInt(1) 38 | // BigKByte is 1,000 SI bytes in big.Ints 39 | BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp) 40 | // BigMByte is 1,000 SI k bytes in big.Ints 41 | BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp) 42 | // BigGByte is 1,000 SI m bytes in big.Ints 43 | BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp) 44 | // BigTByte is 1,000 SI g bytes in big.Ints 45 | BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp) 46 | // BigPByte is 1,000 SI t bytes in big.Ints 47 | BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp) 48 | // BigEByte is 1,000 SI p bytes in big.Ints 49 | BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp) 50 | // BigZByte is 1,000 SI e bytes in big.Ints 51 | BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp) 52 | // BigYByte is 1,000 SI z bytes in big.Ints 53 | BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp) 54 | ) 55 | 56 | var bigBytesSizeTable = map[string]*big.Int{ 57 | "b": BigByte, 58 | "kib": BigKiByte, 59 | "kb": BigKByte, 60 | "mib": BigMiByte, 61 | "mb": BigMByte, 62 | "gib": BigGiByte, 63 | "gb": BigGByte, 64 | "tib": BigTiByte, 65 | "tb": BigTByte, 66 | "pib": BigPiByte, 67 | "pb": BigPByte, 68 | "eib": BigEiByte, 69 | "eb": BigEByte, 70 | "zib": BigZiByte, 71 | "zb": BigZByte, 72 | "yib": BigYiByte, 73 | "yb": BigYByte, 74 | // Without suffix 75 | "": BigByte, 76 | "ki": BigKiByte, 77 | "k": BigKByte, 78 | "mi": BigMiByte, 79 | "m": BigMByte, 80 | "gi": BigGiByte, 81 | "g": BigGByte, 82 | "ti": BigTiByte, 83 | "t": BigTByte, 84 | "pi": BigPiByte, 85 | "p": BigPByte, 86 | "ei": BigEiByte, 87 | "e": BigEByte, 88 | "z": BigZByte, 89 | "zi": BigZiByte, 90 | "y": BigYByte, 91 | "yi": BigYiByte, 92 | } 93 | 94 | var ten = big.NewInt(10) 95 | 96 | func humanateBigBytes(s, base *big.Int, sizes []string) string { 97 | if s.Cmp(ten) < 0 { 98 | return fmt.Sprintf("%d B", s) 99 | } 100 | c := (&big.Int{}).Set(s) 101 | val, mag := oomm(c, base, len(sizes)-1) 102 | suffix := sizes[mag] 103 | f := "%.0f %s" 104 | if val < 10 { 105 | f = "%.1f %s" 106 | } 107 | 108 | return fmt.Sprintf(f, val, suffix) 109 | 110 | } 111 | 112 | // BigBytes produces a human readable representation of an SI size. 113 | // 114 | // See also: ParseBigBytes. 115 | // 116 | // BigBytes(82854982) -> 83MB 117 | func BigBytes(s *big.Int) string { 118 | sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} 119 | return humanateBigBytes(s, bigSIExp, sizes) 120 | } 121 | 122 | // BigIBytes produces a human readable representation of an IEC size. 123 | // 124 | // See also: ParseBigBytes. 125 | // 126 | // BigIBytes(82854982) -> 79MiB 127 | func BigIBytes(s *big.Int) string { 128 | sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} 129 | return humanateBigBytes(s, bigIECExp, sizes) 130 | } 131 | 132 | // ParseBigBytes parses a string representation of bytes into the number 133 | // of bytes it represents. 134 | // 135 | // See also: BigBytes, BigIBytes. 136 | // 137 | // ParseBigBytes("42MB") -> 42000000, nil 138 | // ParseBigBytes("42mib") -> 44040192, nil 139 | func ParseBigBytes(s string) (*big.Int, error) { 140 | lastDigit := 0 141 | hasComma := false 142 | for _, r := range s { 143 | if !(unicode.IsDigit(r) || r == '.' || r == ',') { 144 | break 145 | } 146 | if r == ',' { 147 | hasComma = true 148 | } 149 | lastDigit++ 150 | } 151 | 152 | num := s[:lastDigit] 153 | if hasComma { 154 | num = strings.Replace(num, ",", "", -1) 155 | } 156 | 157 | val := &big.Rat{} 158 | _, err := fmt.Sscanf(num, "%f", val) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) 164 | if m, ok := bigBytesSizeTable[extra]; ok { 165 | mv := (&big.Rat{}).SetInt(m) 166 | val.Mul(val, mv) 167 | rv := &big.Int{} 168 | rv.Div(val.Num(), val.Denom()) 169 | return rv, nil 170 | } 171 | 172 | return nil, fmt.Errorf("unhandled size name: %v", extra) 173 | } 174 | -------------------------------------------------------------------------------- /vendor/github.com/dustin/go-humanize/number.go: -------------------------------------------------------------------------------- 1 | package humanize 2 | 3 | /* 4 | Slightly adapted from the source to fit go-humanize. 5 | 6 | Author: https://github.com/gorhill 7 | Source: https://gist.github.com/gorhill/5285193 8 | 9 | */ 10 | 11 | import ( 12 | "math" 13 | "strconv" 14 | ) 15 | 16 | var ( 17 | renderFloatPrecisionMultipliers = [...]float64{ 18 | 1, 19 | 10, 20 | 100, 21 | 1000, 22 | 10000, 23 | 100000, 24 | 1000000, 25 | 10000000, 26 | 100000000, 27 | 1000000000, 28 | } 29 | 30 | renderFloatPrecisionRounders = [...]float64{ 31 | 0.5, 32 | 0.05, 33 | 0.005, 34 | 0.0005, 35 | 0.00005, 36 | 0.000005, 37 | 0.0000005, 38 | 0.00000005, 39 | 0.000000005, 40 | 0.0000000005, 41 | } 42 | ) 43 | 44 | // FormatFloat produces a formatted number as string based on the following user-specified criteria: 45 | // * thousands separator 46 | // * decimal separator 47 | // * decimal precision 48 | // 49 | // Usage: s := RenderFloat(format, n) 50 | // The format parameter tells how to render the number n. 51 | // 52 | // See examples: http://play.golang.org/p/LXc1Ddm1lJ 53 | // 54 | // Examples of format strings, given n = 12345.6789: 55 | // "#,###.##" => "12,345.67" 56 | // "#,###." => "12,345" 57 | // "#,###" => "12345,678" 58 | // "#\u202F###,##" => "12 345,68" 59 | // "#.###,###### => 12.345,678900 60 | // "" (aka default format) => 12,345.67 61 | // 62 | // The highest precision allowed is 9 digits after the decimal symbol. 63 | // There is also a version for integer number, FormatInteger(), 64 | // which is convenient for calls within template. 65 | func FormatFloat(format string, n float64) string { 66 | // Special cases: 67 | // NaN = "NaN" 68 | // +Inf = "+Infinity" 69 | // -Inf = "-Infinity" 70 | if math.IsNaN(n) { 71 | return "NaN" 72 | } 73 | if n > math.MaxFloat64 { 74 | return "Infinity" 75 | } 76 | if n < -math.MaxFloat64 { 77 | return "-Infinity" 78 | } 79 | 80 | // default format 81 | precision := 2 82 | decimalStr := "." 83 | thousandStr := "," 84 | positiveStr := "" 85 | negativeStr := "-" 86 | 87 | if len(format) > 0 { 88 | format := []rune(format) 89 | 90 | // If there is an explicit format directive, 91 | // then default values are these: 92 | precision = 9 93 | thousandStr = "" 94 | 95 | // collect indices of meaningful formatting directives 96 | formatIndx := []int{} 97 | for i, char := range format { 98 | if char != '#' && char != '0' { 99 | formatIndx = append(formatIndx, i) 100 | } 101 | } 102 | 103 | if len(formatIndx) > 0 { 104 | // Directive at index 0: 105 | // Must be a '+' 106 | // Raise an error if not the case 107 | // index: 0123456789 108 | // +0.000,000 109 | // +000,000.0 110 | // +0000.00 111 | // +0000 112 | if formatIndx[0] == 0 { 113 | if format[formatIndx[0]] != '+' { 114 | panic("RenderFloat(): invalid positive sign directive") 115 | } 116 | positiveStr = "+" 117 | formatIndx = formatIndx[1:] 118 | } 119 | 120 | // Two directives: 121 | // First is thousands separator 122 | // Raise an error if not followed by 3-digit 123 | // 0123456789 124 | // 0.000,000 125 | // 000,000.00 126 | if len(formatIndx) == 2 { 127 | if (formatIndx[1] - formatIndx[0]) != 4 { 128 | panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers") 129 | } 130 | thousandStr = string(format[formatIndx[0]]) 131 | formatIndx = formatIndx[1:] 132 | } 133 | 134 | // One directive: 135 | // Directive is decimal separator 136 | // The number of digit-specifier following the separator indicates wanted precision 137 | // 0123456789 138 | // 0.00 139 | // 000,0000 140 | if len(formatIndx) == 1 { 141 | decimalStr = string(format[formatIndx[0]]) 142 | precision = len(format) - formatIndx[0] - 1 143 | } 144 | } 145 | } 146 | 147 | // generate sign part 148 | var signStr string 149 | if n >= 0.000000001 { 150 | signStr = positiveStr 151 | } else if n <= -0.000000001 { 152 | signStr = negativeStr 153 | n = -n 154 | } else { 155 | signStr = "" 156 | n = 0.0 157 | } 158 | 159 | // split number into integer and fractional parts 160 | intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision]) 161 | 162 | // generate integer part string 163 | intStr := strconv.FormatInt(int64(intf), 10) 164 | 165 | // add thousand separator if required 166 | if len(thousandStr) > 0 { 167 | for i := len(intStr); i > 3; { 168 | i -= 3 169 | intStr = intStr[:i] + thousandStr + intStr[i:] 170 | } 171 | } 172 | 173 | // no fractional part, we can leave now 174 | if precision == 0 { 175 | return signStr + intStr 176 | } 177 | 178 | // generate fractional part 179 | fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision])) 180 | // may need padding 181 | if len(fracStr) < precision { 182 | fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr 183 | } 184 | 185 | return signStr + intStr + decimalStr + fracStr 186 | } 187 | 188 | // FormatInteger produces a formatted number as string. 189 | // See FormatFloat. 190 | func FormatInteger(format string, n int) string { 191 | return FormatFloat(format, float64(n)) 192 | } 193 | -------------------------------------------------------------------------------- /tools/ssa_test.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "runtime" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestQualifiedPkgName(t *testing.T) { 12 | _, pathToTestFile, _, ok := runtime.Caller(0) 13 | if !ok { 14 | t.Fatal("could not get the path of current test file") 15 | } 16 | 17 | specs := []string{ 18 | pathToTestFile, 19 | "./", // relative path 20 | } 21 | 22 | expPkgName := "github.com/geckoboard/prism/tools" 23 | for specIndex, spec := range specs { 24 | pkgName, err := qualifiedPkgName(spec) 25 | if err != nil { 26 | t.Fatalf("[spec %d] %s", specIndex, err) 27 | } 28 | 29 | if pkgName != expPkgName { 30 | t.Fatalf("[spec %d] expected qualified package name to be %q; got %q", specIndex, expPkgName, pkgName) 31 | } 32 | } 33 | } 34 | 35 | func TestPackageWorkspace(t *testing.T) { 36 | _, pathToTestFile, _, ok := runtime.Caller(0) 37 | if !ok { 38 | t.Fatal("could not get the path of current test file") 39 | } 40 | 41 | specs := []string{ 42 | pathToTestFile, 43 | "./", // relative path 44 | } 45 | 46 | expWorkspace := pathToTestFile[:strings.Index(pathToTestFile, "src/github")-1] 47 | for specIndex, spec := range specs { 48 | workspace, err := packageWorkspace(spec) 49 | if err != nil { 50 | t.Fatalf("[spec %d] %s", specIndex, err) 51 | } 52 | 53 | if workspace != expWorkspace { 54 | t.Fatalf("[spec %d] expected workspace path to be %q; got %q", specIndex, expWorkspace, workspace) 55 | } 56 | } 57 | } 58 | 59 | func TestSSACandidates(t *testing.T) { 60 | wsDir, pkgDir, pkgName := mockPackage(t) 61 | defer os.RemoveAll(wsDir) 62 | 63 | candidates, err := ssaCandidates(pkgDir, pkgName, wsDir) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | // We expect one candidate for each defined function/method + 1 for the init() function 69 | expCandidates := 4 70 | if len(candidates) != expCandidates { 71 | t.Fatalf("expected to get back %d SSA function candidates; got %d", expCandidates, len(candidates)) 72 | } 73 | 74 | validFqNames := map[string]struct{}{ 75 | pkgName + "/A.DoStuff": struct{}{}, 76 | pkgName + "/DoStuff": struct{}{}, 77 | pkgName + "/main": struct{}{}, 78 | pkgName + "/init": struct{}{}, 79 | } 80 | for fqName := range candidates { 81 | if _, valid := validFqNames[fqName]; !valid { 82 | t.Errorf("unexpected fq name %q", fqName) 83 | } 84 | } 85 | } 86 | 87 | func mockPackage(t *testing.T) (workspaceDir, pkgDir, pkgName string) { 88 | pkgName = "prism-mock" 89 | otherPkgName := "other" 90 | pkgData := map[string]string{ 91 | otherPkgName: ` 92 | package other 93 | 94 | func DoStuff(){ 95 | } 96 | `, 97 | pkgName: ` 98 | package main 99 | 100 | import other "` + otherPkgName + `" 101 | 102 | type A struct { 103 | } 104 | 105 | func(a *A) DoStuff(){ 106 | // Call to external package; should not be returned as an SSA candidate 107 | other.DoStuff() 108 | } 109 | 110 | func DoStuff(){ 111 | a := &A{} 112 | a.DoStuff() 113 | 114 | // The callgraph generator should not visit this function a second time 115 | a.DoStuff() 116 | } 117 | 118 | func main(){ 119 | DoStuff() 120 | } 121 | `, 122 | } 123 | 124 | workspaceDir, err := ioutil.TempDir("", "prism-test") 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | 129 | for name, src := range pkgData { 130 | dir := workspaceDir + "/src/" + name + "/" 131 | err = os.MkdirAll(dir, os.ModeDir|os.ModePerm) 132 | if err != nil { 133 | os.RemoveAll(workspaceDir) 134 | t.Fatalf("error creating workspace folder for package %q: %s", name, err) 135 | } 136 | 137 | err = ioutil.WriteFile(dir+"src.go", []byte(src), os.ModePerm) 138 | if err != nil { 139 | os.RemoveAll(workspaceDir) 140 | t.Fatalf("error creating package contents for package %q: %s", name, err) 141 | } 142 | } 143 | 144 | pkgDir = workspaceDir + "/src/" + pkgName + "/" 145 | return workspaceDir, pkgDir, pkgName 146 | } 147 | 148 | func mockPackageWithVendoredDeps(t *testing.T, useGodeps bool) (workspaceDir, pkgDir, pkgName string) { 149 | var otherPkgName string 150 | pkgName = "prism-mock" 151 | 152 | if useGodeps { 153 | otherPkgName = pkgName + "/Godeps/_workspace/other/pkg" 154 | } else { 155 | otherPkgName = pkgName + "/vendor/other/pkg" 156 | } 157 | 158 | pkgData := map[string]string{ 159 | otherPkgName: ` 160 | package other 161 | 162 | func DoStuff(){ 163 | } 164 | `, 165 | pkgName: ` 166 | package main 167 | 168 | import other "` + otherPkgName + `" 169 | 170 | type A struct { 171 | } 172 | 173 | func(a *A) DoStuff(){ 174 | // Call to vendored dep 175 | other.DoStuff() 176 | } 177 | 178 | func DoStuff(){ 179 | a := &A{} 180 | a.DoStuff() 181 | 182 | // The callgraph generator should not visit this function a second time 183 | a.DoStuff() 184 | } 185 | 186 | func main(){ 187 | DoStuff() 188 | } 189 | `, 190 | } 191 | 192 | workspaceDir, err := ioutil.TempDir("", "prism-test") 193 | if err != nil { 194 | t.Fatal(err) 195 | } 196 | 197 | for name, src := range pkgData { 198 | dir := workspaceDir + "/src/" + name + "/" 199 | err = os.MkdirAll(dir, os.ModeDir|os.ModePerm) 200 | if err != nil { 201 | os.RemoveAll(workspaceDir) 202 | t.Fatalf("error creating workspace folder for package %q: %s", name, err) 203 | } 204 | 205 | err = ioutil.WriteFile(dir+"src.go", []byte(src), os.ModePerm) 206 | if err != nil { 207 | os.RemoveAll(workspaceDir) 208 | t.Fatalf("error creating package contents for package %q: %s", name, err) 209 | } 210 | } 211 | 212 | pkgDir = workspaceDir + "/src/" + pkgName + "/" 213 | return workspaceDir, pkgDir, pkgName 214 | } 215 | -------------------------------------------------------------------------------- /cmd/print.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "golang.org/x/crypto/ssh/terminal" 11 | 12 | "github.com/geckoboard/cli-table" 13 | "github.com/geckoboard/prism/profiler" 14 | "gopkg.in/urfave/cli.v1" 15 | ) 16 | 17 | var ( 18 | errNoProfile = errors.New(`"print" requires a profile argument`) 19 | errNoPrintColumnsSpecified = errors.New("no table columns specified for printing profile") 20 | ) 21 | 22 | // PrintProfile displays a captured profile in tabular form. 23 | func PrintProfile(ctx *cli.Context) error { 24 | var err error 25 | 26 | args := ctx.Args() 27 | if len(args) != 1 { 28 | return errNoProfile 29 | } 30 | 31 | pp := &profilePrinter{} 32 | 33 | pp.format, err = parseDisplayFormat(ctx.String("display-format")) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | pp.unit, err = parseDisplayUnit(ctx.String("display-unit")) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | pp.columns, err = parseTableColumList(ctx.String("display-columns")) 44 | if err != nil { 45 | return err 46 | } 47 | if len(pp.columns) == 0 { 48 | return errNoPrintColumnsSpecified 49 | } 50 | 51 | pp.clipThreshold = ctx.Float64("display-threshold") 52 | 53 | profile, err := loadProfile(args[0]) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | profTable := pp.Tabularize(profile) 59 | 60 | // If stdout is not a terminal we need to strip ANSI characters 61 | filter := table.StripAnsi 62 | if terminal.IsTerminal(int(os.Stdout.Fd())) && !ctx.Bool("no-ansi") { 63 | filter = table.PreserveAnsi 64 | } 65 | profTable.Write(os.Stdout, filter) 66 | 67 | return nil 68 | } 69 | 70 | // profilePrinter generates a tabulated output of the captured profile details. 71 | type profilePrinter struct { 72 | format displayFormat 73 | unit displayUnit 74 | columns []tableColumnType 75 | clipThreshold float64 76 | } 77 | 78 | // Create a table with profile details. 79 | func (pp *profilePrinter) Tabularize(profile *profiler.Profile) *table.Table { 80 | if pp.unit == displayUnitAuto { 81 | pp.unit = pp.detectTimeUnit(profile.Target) 82 | } 83 | 84 | t := table.New(len(pp.columns) + 1) 85 | t.SetPadding(1) 86 | 87 | // Setup headers and alignment settings 88 | if profile.Label != "" { 89 | t.SetHeader(0, fmt.Sprintf("%s - call stack", profile.Label), table.AlignLeft) 90 | } else { 91 | t.SetHeader(0, "call stack", table.AlignLeft) 92 | } 93 | for dIndex, dType := range pp.columns { 94 | t.SetHeader(dIndex+1, dType.Header(), table.AlignRight) 95 | } 96 | 97 | // Populate rows 98 | pp.appendRow(0, profile.Target, profile.Target, t) 99 | 100 | return t 101 | } 102 | 103 | // Append a row with call metrics and recursively process nested profile entries. 104 | func (pp *profilePrinter) appendRow(depth int, rootMetrics, rowMetrics *profiler.CallMetrics, t *table.Table) { 105 | row := make([]string, len(pp.columns)+1) 106 | 107 | // Fill in call 108 | call := strings.Repeat("| ", depth) 109 | if len(rowMetrics.NestedCalls) == 0 { 110 | call += "- " 111 | } else { 112 | call += "+ " 113 | } 114 | row[0] = call + rowMetrics.FnName 115 | 116 | baseIndex := 1 117 | for dIndex, dType := range pp.columns { 118 | row[baseIndex+dIndex] = pp.fmtEntry(rootMetrics, rowMetrics, dType) 119 | } 120 | t.Append(row) 121 | 122 | // Emit table rows for nested calls 123 | for _, childMetrics := range rowMetrics.NestedCalls { 124 | pp.appendRow(depth+1, rootMetrics, childMetrics, t) 125 | } 126 | } 127 | 128 | // detectTimeUnit iterates through the list of displayable metrics and tries to 129 | // figure out best displayUnit that can represent all displayable values. 130 | func (pp *profilePrinter) detectTimeUnit(metrics *profiler.CallMetrics) displayUnit { 131 | var val time.Duration 132 | var unit displayUnit = displayUnitMs 133 | for _, dType := range pp.columns { 134 | switch dType { 135 | case tableColTotal: 136 | val = metrics.TotalTime 137 | case tableColMin: 138 | val = metrics.MinTime 139 | case tableColMax: 140 | val = metrics.MaxTime 141 | case tableColMean: 142 | val = metrics.MeanTime 143 | case tableColMedian: 144 | val = metrics.MedianTime 145 | case tableColP50: 146 | val = metrics.P50Time 147 | case tableColP75: 148 | val = metrics.P75Time 149 | case tableColP90: 150 | val = metrics.P90Time 151 | case tableColP99: 152 | val = metrics.P99Time 153 | default: 154 | continue 155 | } 156 | 157 | dUnit := detectTimeUnit(val) 158 | if dUnit > unit { 159 | unit = dUnit 160 | } 161 | } 162 | 163 | // Process nested measurements 164 | for _, childMetrics := range metrics.NestedCalls { 165 | dUnit := pp.detectTimeUnit(childMetrics) 166 | if dUnit > unit { 167 | unit = dUnit 168 | } 169 | } 170 | 171 | return unit 172 | } 173 | 174 | // Format metric entry. An empty string will be returned if the entry is of 175 | // time.Duration type and its value is less than the specified threshold. 176 | func (pp *profilePrinter) fmtEntry(rootMetrics, metrics *profiler.CallMetrics, metricType tableColumnType) string { 177 | var val, rootVal time.Duration 178 | 179 | switch metricType { 180 | case tableColInvocations: 181 | return fmt.Sprintf("%d", metrics.Invocations) 182 | case tableColStdDev: 183 | return fmt.Sprintf("%3.3f", metrics.StdDev) 184 | case tableColTotal: 185 | val = metrics.TotalTime 186 | rootVal = rootMetrics.TotalTime 187 | case tableColMin: 188 | val = metrics.MinTime 189 | rootVal = rootMetrics.MinTime 190 | case tableColMax: 191 | val = metrics.MaxTime 192 | rootVal = rootMetrics.MaxTime 193 | case tableColMean: 194 | val = metrics.MeanTime 195 | rootVal = rootMetrics.MeanTime 196 | case tableColMedian: 197 | val = metrics.MedianTime 198 | rootVal = rootMetrics.MedianTime 199 | case tableColP50: 200 | val = metrics.P50Time 201 | rootVal = rootMetrics.P50Time 202 | case tableColP75: 203 | val = metrics.P75Time 204 | rootVal = rootMetrics.P75Time 205 | case tableColP90: 206 | val = metrics.P90Time 207 | rootVal = rootMetrics.P90Time 208 | case tableColP99: 209 | val = metrics.P99Time 210 | rootVal = rootMetrics.P99Time 211 | } 212 | 213 | // Convert value to the proper unit 214 | rootTime := pp.unit.Convert(rootVal) 215 | entryTime := pp.unit.Convert(val) 216 | 217 | switch pp.format { 218 | case displayTime: 219 | if entryTime < pp.clipThreshold { 220 | return "" 221 | } 222 | return pp.unit.Format(entryTime) 223 | default: 224 | percent := 0.0 225 | if rootTime != 0.0 { 226 | percent = 100.0 * entryTime / rootTime 227 | } 228 | if percent < pp.clipThreshold { 229 | return "" 230 | } 231 | return fmt.Sprintf("%2.1f%%", percent) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /profiler/profiler.go: -------------------------------------------------------------------------------- 1 | package profiler 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | const ( 10 | defaultSinkBufferSize = 100 11 | numCalibrationCalls = 10000000 12 | ) 13 | 14 | var ( 15 | // A mutex for protecting access to the activeProfiles map. 16 | profileMutex sync.Mutex 17 | 18 | // A label to be applied to generated profiles. 19 | profileLabel string 20 | 21 | // We maintain a dedicated call stack for each profiled goroutine. Each 22 | // map entry points to the currently entered function scope. 23 | activeProfiles map[uint64]*fnCall 24 | 25 | // A sink for emitted profile entries. 26 | outputSink Sink 27 | 28 | // Function call invokation overhead; calculated by calibrate() and triggered by init() 29 | timeNowOverhead, timeSinceOverhead, deferredFnOverhead, fnCallOverhead time.Duration 30 | ) 31 | 32 | func init() { 33 | calibrate() 34 | } 35 | 36 | // callibrate attempts to estimate the mean overhead for invoking time.Now(), time.Since(), 37 | // as well as the mean time spent in function guard code (stack setup, pushing/popping 38 | // registers, dealing with deferred calls e.t.c). 39 | // 40 | // Runtime overhead is generally in the nanosecond range but we need to 41 | // properly account for it when calculating the total time spent inside a 42 | // profiled function as it tends to skew our timing calculations when the profiled 43 | // function is invoked a large number of times. 44 | // 45 | // To calculate an estimate, we time N executions of each function and then just 46 | // calculate the mean execution time. As outliers may skew our results, using 47 | // the median execution time would be better but calculating it is not a trivial 48 | // operation due to the space and timing accurace required. 49 | func calibrate() { 50 | // Benchmark function call time. We use a switch statement to ensure that 51 | // the compiler will not inline this function (see https://github.com/golang/go/issues/12312) 52 | fnCallBench := func(i int) { 53 | switch i { 54 | } 55 | } 56 | tick := time.Now() 57 | for i := 0; i < numCalibrationCalls; i++ { 58 | fnCallBench(i) 59 | } 60 | fnCallOverhead = time.Since(tick) / time.Duration(numCalibrationCalls) 61 | 62 | // Benchmark deferred function call time 63 | deferBench := func(i int) { 64 | defer func() { fnCallBench(i) }() 65 | } 66 | tick = time.Now() 67 | for i := 0; i < numCalibrationCalls; i++ { 68 | deferBench(i) 69 | } 70 | deferredFnOverhead = time.Since(tick) / time.Duration(numCalibrationCalls) 71 | 72 | // Benchmark time.Now() 73 | tick = time.Now() 74 | for i := 0; i < numCalibrationCalls; i++ { 75 | time.Now() 76 | } 77 | timeNowOverhead = fnCallOverhead + time.Since(tick)/time.Duration(numCalibrationCalls) 78 | 79 | // Benchmark time.Since() 80 | tick = time.Now() 81 | for i := 0; i < numCalibrationCalls; i++ { 82 | time.Since(tick) 83 | } 84 | timeSinceOverhead = fnCallOverhead + time.Since(tick)/time.Duration(numCalibrationCalls) 85 | } 86 | 87 | // Init handles the initialization of the prism profiler. This method must be 88 | // called before invoking any other method from this package. 89 | func Init(sink Sink, capturedProfileLabel string) { 90 | err := sink.Open(defaultSinkBufferSize) 91 | if err != nil { 92 | err = fmt.Errorf("profiler: error initializing sink: %s", err) 93 | panic(err) 94 | } 95 | 96 | outputSink = sink 97 | activeProfiles = make(map[uint64]*fnCall, 0) 98 | profileLabel = capturedProfileLabel 99 | } 100 | 101 | // Shutdown waits for shippers to fully dequeue any buffered profiles and shuts 102 | // them down. This method should be called by main() before the program exits 103 | // to ensure that no profile data is lost if the program executes too fast. 104 | func Shutdown() { 105 | err := outputSink.Close() 106 | if err != nil { 107 | err = fmt.Errorf("profiler: error shutting downg sink: %s", err) 108 | panic(err) 109 | } 110 | } 111 | 112 | // BeginProfile creates a new profile. 113 | func BeginProfile(rootFnName string) { 114 | tick := time.Now() 115 | 116 | tid := threadID() 117 | 118 | rootCall := makeFnCall(rootFnName) 119 | rootCall.enteredAt = tick 120 | 121 | profileMutex.Lock() 122 | activeProfiles[tid] = rootCall 123 | profileMutex.Unlock() 124 | 125 | rootCall.profilerOverhead += timeNowOverhead + timeSinceOverhead + fnCallOverhead + time.Since(tick) 126 | } 127 | 128 | // EndProfile finalizes and ships a currently active profile. 129 | func EndProfile() { 130 | tick := time.Now() 131 | tid := threadID() 132 | 133 | profileMutex.Lock() 134 | rootCall := activeProfiles[tid] 135 | if rootCall == nil { 136 | // No active profile for this threadID; skip 137 | profileMutex.Unlock() 138 | return 139 | } 140 | 141 | delete(activeProfiles, tid) 142 | profileMutex.Unlock() 143 | 144 | // Generate profile; 145 | rootCall.exitedAt = time.Now() 146 | rootCall.profilerOverhead += 2*timeNowOverhead + timeSinceOverhead + deferredFnOverhead + time.Since(tick) 147 | profile := genProfile(tid, profileLabel, rootCall) 148 | rootCall.free() 149 | 150 | // Ship profile 151 | outputSink.Input() <- profile 152 | } 153 | 154 | // Enter adds a new nested function call to the profile linked to the current go-routine ID. 155 | func Enter(fnName string) { 156 | tick := time.Now() 157 | tid := threadID() 158 | 159 | profileMutex.Lock() 160 | parentCall := activeProfiles[tid] 161 | if parentCall == nil { 162 | // No active profile for this threadID; skip 163 | profileMutex.Unlock() 164 | return 165 | } 166 | 167 | call := makeFnCall(fnName) 168 | call.enteredAt = tick 169 | parentCall.nestCall(call) 170 | 171 | activeProfiles[tid] = call 172 | profileMutex.Unlock() 173 | 174 | // Update overhead estimate 175 | call.profilerOverhead += timeNowOverhead + timeSinceOverhead + fnCallOverhead + time.Since(tick) 176 | } 177 | 178 | // Leave exits the current function in the profile linked to the current go-routine ID. 179 | func Leave() { 180 | tick := time.Now() 181 | tid := threadID() 182 | 183 | profileMutex.Lock() 184 | call := activeProfiles[tid] 185 | if call == nil { 186 | // No active profile for this threadID; skip 187 | profileMutex.Unlock() 188 | return 189 | } 190 | 191 | if call.parent == nil { 192 | profileMutex.Unlock() 193 | panic(fmt.Sprintf("profiler: [BUG] attempted to exit an active profile (tid %d)", tid)) 194 | } 195 | 196 | // Exit current scope 197 | activeProfiles[tid] = call.parent 198 | profileMutex.Unlock() 199 | 200 | // Update exit timestamp and overhead estimate for the parent. We also add in 201 | // an extra fnCallOverhead to account for the pointer dereferencing code for 202 | // updating the parent's overhead 203 | call.exitedAt = time.Now() 204 | call.profilerOverhead += 2*timeNowOverhead + timeSinceOverhead + deferredFnOverhead + 2*fnCallOverhead + time.Since(tick) 205 | call.parent.profilerOverhead += call.profilerOverhead 206 | } 207 | -------------------------------------------------------------------------------- /cmd/print_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "io" 7 | "os" 8 | "testing" 9 | 10 | "gopkg.in/urfave/cli.v1" 11 | ) 12 | 13 | func TestLoadProfileErrors(t *testing.T) { 14 | expErr := `unrecognized profile extension ".yml" for "foo.yml"; only json profiles are currently supported` 15 | _, err := loadProfile("foo.yml") 16 | if err == nil || err.Error() != expErr { 17 | t.Fatalf("expected to get error %q; got %v", expErr, err) 18 | } 19 | 20 | _, err = loadProfile("no-such-file.json") 21 | if err == nil { 22 | t.Fatal("expected to get an error") 23 | } 24 | } 25 | 26 | func TestPrintWithProfileLabel(t *testing.T) { 27 | profileDir, profileFiles := mockProfiles(t, true) 28 | defer os.RemoveAll(profileDir) 29 | 30 | // Mock args 31 | set := flag.NewFlagSet("test", 0) 32 | set.String("display-columns", SupportedColumnNames(), "") 33 | set.String("display-format", "time", "") 34 | set.String("display-unit", "ms", "") 35 | set.Float64("display-threshold", 11.0, "") 36 | set.Parse(profileFiles[0:1]) 37 | ctx := cli.NewContext(nil, set, nil) 38 | 39 | // Redirect stdout 40 | stdOut := os.Stdout 41 | pRead, pWrite, err := os.Pipe() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | os.Stdout = pWrite 46 | 47 | // Restore stdout incase of a panic 48 | defer func() { 49 | os.Stdout = stdOut 50 | }() 51 | 52 | // Run diff and capture output 53 | err = PrintProfile(ctx) 54 | if err != nil { 55 | os.Stdout = stdOut 56 | t.Fatal(err) 57 | } 58 | 59 | // Drain pipe and restore stdout 60 | var buf bytes.Buffer 61 | pWrite.Close() 62 | io.Copy(&buf, pRead) 63 | pRead.Close() 64 | os.Stdout = stdOut 65 | 66 | output := buf.String() 67 | expOutput := `+-------------------------+-----------+-----------+-----------+-----------+-----------+-------+-----------+-----------+-----------+-----------+--------+ 68 | | With Label - call stack | total | min | max | mean | median | invoc | p50 | p75 | p90 | p99 | stddev | 69 | +-------------------------+-----------+-----------+-----------+-----------+-----------+-------+-----------+-----------+-----------+-----------+--------+ 70 | | + main | 120.00 ms | 120.00 ms | 120.00 ms | 120.00 ms | 120.00 ms | 1 | 120.00 ms | 120.00 ms | 120.00 ms | 120.00 ms | 0.000 | 71 | | | - foo | 120.00 ms | | 110.00 ms | 60.00 ms | 60.00 ms | 2 | | | | 120.00 ms | 70.711 | 72 | +-------------------------+-----------+-----------+-----------+-----------+-----------+-------+-----------+-----------+-----------+-----------+--------+ 73 | ` 74 | 75 | if expOutput != output { 76 | t.Fatalf("tabularized print output mismatch; expected:\n%s\n\ngot:\n%s", expOutput, output) 77 | } 78 | } 79 | 80 | func TestPrintWithoutProfileLabel(t *testing.T) { 81 | profileDir, profileFiles := mockProfiles(t, false) 82 | defer os.RemoveAll(profileDir) 83 | 84 | // Mock args 85 | set := flag.NewFlagSet("test", 0) 86 | set.String("display-columns", SupportedColumnNames(), "") 87 | set.String("display-format", "time", "") 88 | set.String("display-unit", "ms", "") 89 | set.Float64("display-threshold", 0.0, "") 90 | set.Parse(profileFiles[1:]) 91 | ctx := cli.NewContext(nil, set, nil) 92 | 93 | // Redirect stdout 94 | stdOut := os.Stdout 95 | pRead, pWrite, err := os.Pipe() 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | os.Stdout = pWrite 100 | 101 | // Restore stdout incase of a panic 102 | defer func() { 103 | os.Stdout = stdOut 104 | }() 105 | 106 | // Run diff and capture output 107 | err = PrintProfile(ctx) 108 | if err != nil { 109 | os.Stdout = stdOut 110 | t.Fatal(err) 111 | } 112 | 113 | // Drain pipe and restore stdout 114 | var buf bytes.Buffer 115 | pWrite.Close() 116 | io.Copy(&buf, pRead) 117 | pRead.Close() 118 | os.Stdout = stdOut 119 | 120 | output := buf.String() 121 | expOutput := `+------------+----------+----------+----------+----------+----------+-------+----------+----------+----------+----------+--------+ 122 | | call stack | total | min | max | mean | median | invoc | p50 | p75 | p90 | p99 | stddev | 123 | +------------+----------+----------+----------+----------+----------+-------+----------+----------+----------+----------+--------+ 124 | | + main | 10.00 ms | 10.00 ms | 10.00 ms | 10.00 ms | 10.00 ms | 1 | 10.00 ms | 10.00 ms | 10.00 ms | 10.00 ms | 0.000 | 125 | | | - foo | 10.00 ms | 4.00 ms | 6.00 ms | 5.00 ms | 5.00 ms | 2 | 4.00 ms | 4.00 ms | 4.00 ms | 6.00 ms | 1.414 | 126 | +------------+----------+----------+----------+----------+----------+-------+----------+----------+----------+----------+--------+ 127 | ` 128 | 129 | if expOutput != output { 130 | t.Fatalf("tabularized print output mismatch; expected:\n%s\n\ngot:\n%s", expOutput, output) 131 | } 132 | } 133 | 134 | func TestPrintWithoutProfileLabelAndPercentOutput(t *testing.T) { 135 | profileDir, profileFiles := mockProfiles(t, false) 136 | defer os.RemoveAll(profileDir) 137 | 138 | // Mock args 139 | set := flag.NewFlagSet("test", 0) 140 | set.String("display-columns", SupportedColumnNames(), "") 141 | set.String("display-format", "percent", "") 142 | set.String("display-unit", "auto", "") 143 | set.Float64("display-threshold", 41.0, "") 144 | set.Parse(profileFiles[1:]) 145 | ctx := cli.NewContext(nil, set, nil) 146 | 147 | // Redirect stdout 148 | stdOut := os.Stdout 149 | pRead, pWrite, err := os.Pipe() 150 | if err != nil { 151 | t.Fatal(err) 152 | } 153 | os.Stdout = pWrite 154 | 155 | // Restore stdout incase of a panic 156 | defer func() { 157 | os.Stdout = stdOut 158 | }() 159 | 160 | // Run diff and capture output 161 | err = PrintProfile(ctx) 162 | if err != nil { 163 | os.Stdout = stdOut 164 | t.Fatal(err) 165 | } 166 | 167 | // Drain pipe and restore stdout 168 | var buf bytes.Buffer 169 | pWrite.Close() 170 | io.Copy(&buf, pRead) 171 | pRead.Close() 172 | os.Stdout = stdOut 173 | 174 | output := buf.String() 175 | expOutput := `+------------+--------+--------+--------+--------+--------+-------+--------+--------+--------+--------+--------+ 176 | | call stack | total | min | max | mean | median | invoc | p50 | p75 | p90 | p99 | stddev | 177 | +------------+--------+--------+--------+--------+--------+-------+--------+--------+--------+--------+--------+ 178 | | + main | 100.0% | 100.0% | 100.0% | 100.0% | 100.0% | 1 | 100.0% | 100.0% | 100.0% | 100.0% | 0.000 | 179 | | | - foo | 100.0% | | 60.0% | 50.0% | 50.0% | 2 | | | | 60.0% | 1.414 | 180 | +------------+--------+--------+--------+--------+--------+-------+--------+--------+--------+--------+--------+ 181 | ` 182 | 183 | if expOutput != output { 184 | t.Fatalf("tabularized print output mismatch; expected:\n%s\n\ngot:\n%s", expOutput, output) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /tools/package_test.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "os" 7 | "runtime" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestNewGoPackage(t *testing.T) { 13 | wsDir, pkgDir, pkgName := mockPackage(t) 14 | defer os.RemoveAll(wsDir) 15 | 16 | pkg, err := NewGoPackage(pkgDir) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | if pkg.pathToPackage != pkgDir { 22 | t.Fatalf("expected pathToPackage to be %q; got %q", pkgDir, pkg.pathToPackage) 23 | } 24 | 25 | if pkg.PkgPrefix != pkgName { 26 | t.Fatalf("expected PkgPrefix to be %q; got %q", pkgName, pkg.PkgPrefix) 27 | } 28 | 29 | separator := ':' 30 | if runtime.GOOS == "windows" { 31 | separator = ';' 32 | } 33 | 34 | expGOPATH := fmt.Sprintf("%s%c%s", wsDir, separator, os.Getenv("GOPATH")) 35 | if pkg.GOPATH != expGOPATH { 36 | t.Fatalf("expected adjusted GOPATH to be %q; got %q", expGOPATH, pkg.GOPATH) 37 | } 38 | } 39 | 40 | func TestFindTarget(t *testing.T) { 41 | wsDir, pkgDir, pkgName := mockPackage(t) 42 | defer os.RemoveAll(wsDir) 43 | 44 | pkg, err := NewGoPackage(pkgDir) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | patchTargets := []string{ 50 | pkgName + "/main", 51 | pkgName + "/DoStuff", 52 | pkgName + "/A.DoStuff", 53 | } 54 | targetList, err := pkg.Find(patchTargets...) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | if len(targetList) != len(patchTargets) { 60 | t.Fatalf("expected to get back %d targets; got %d", len(patchTargets), len(targetList)) 61 | } 62 | } 63 | 64 | func TestFindMissingTarget(t *testing.T) { 65 | wsDir, pkgDir, pkgName := mockPackage(t) 66 | defer os.RemoveAll(wsDir) 67 | 68 | pkg, err := NewGoPackage(pkgDir) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | invalidTarget := pkgName + "/missing.target" 74 | expError := fmt.Sprintf("GoPackage.Find: no match for profile target %q", invalidTarget) 75 | _, err = pkg.Find(invalidTarget) 76 | if err == nil || err.Error() != expError { 77 | t.Fatalf("expected to get error %q; got: %v", expError, err) 78 | } 79 | } 80 | 81 | func TestPatchPackageExcludingDeps(t *testing.T) { 82 | wsDir, pkgDir, pkgName := mockPackage(t) 83 | defer os.RemoveAll(wsDir) 84 | 85 | pkg, err := NewGoPackage(pkgDir) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | targetList, err := pkg.Find(pkgName + "/main") 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | if len(targetList) != 1 { 96 | t.Fatal("error looking up patch target for main()") 97 | } 98 | 99 | vendorPkgRegex := []string{} 100 | dummyPatchCmd := PatchCmd{ 101 | Targets: targetList, 102 | PatchFn: func(_ *CallGraphNode, _ *ast.BlockStmt) (modifiedAST bool, extraImports []string) { 103 | return true, nil 104 | }, 105 | } 106 | updatedFiles, patchCount, err := pkg.Patch(vendorPkgRegex, dummyPatchCmd) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | 111 | expUpdatedFiles := 1 112 | if updatedFiles != expUpdatedFiles { 113 | t.Fatalf("expected Patch() to update %d files; got %d", expUpdatedFiles, updatedFiles) 114 | } 115 | 116 | // pkg.Patch will modify main and the 2 call targets in its callgraph 117 | expPatchCount := 3 118 | if patchCount != expPatchCount { 119 | t.Fatalf("expected Patch() to apply %d patches; got %d", expPatchCount, patchCount) 120 | } 121 | } 122 | 123 | func TestPatchPackageIncludingGodeps(t *testing.T) { 124 | wsDir, pkgDir, pkgName := mockPackageWithVendoredDeps(t, true) 125 | defer os.RemoveAll(wsDir) 126 | 127 | pkg, err := NewGoPackage(pkgDir) 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | 132 | targetList, err := pkg.Find(pkgName + "/main") 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | 137 | if len(targetList) != 1 { 138 | t.Fatal("error looking up patch target for main()") 139 | } 140 | 141 | vendorPkgRegex := []string{"other/pkg"} 142 | dummyPatchCmd := PatchCmd{ 143 | Targets: targetList, 144 | PatchFn: func(_ *CallGraphNode, _ *ast.BlockStmt) (modifiedAST bool, extraImports []string) { 145 | return true, nil 146 | }, 147 | } 148 | updatedFiles, patchCount, err := pkg.Patch(vendorPkgRegex, dummyPatchCmd) 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | 153 | expUpdatedFiles := 2 154 | if updatedFiles != expUpdatedFiles { 155 | t.Fatalf("expected Patch() to update %d files; got %d", expUpdatedFiles, updatedFiles) 156 | } 157 | 158 | // pkg.Patch will modify main and the 2 call targets in its callgraph + 1 function in the vendored package 159 | expPatchCount := 4 160 | if patchCount != expPatchCount { 161 | t.Fatalf("expected Patch() to apply %d patches; got %d", expPatchCount, patchCount) 162 | } 163 | } 164 | 165 | func TestPatchPackageIncludingVendorDeps(t *testing.T) { 166 | wsDir, pkgDir, pkgName := mockPackageWithVendoredDeps(t, false) 167 | defer os.RemoveAll(wsDir) 168 | 169 | pkg, err := NewGoPackage(pkgDir) 170 | if err != nil { 171 | t.Fatal(err) 172 | } 173 | 174 | targetList, err := pkg.Find(pkgName + "/main") 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | 179 | if len(targetList) != 1 { 180 | t.Fatal("error looking up patch target for main()") 181 | } 182 | 183 | vendorPkgRegex := []string{"other/pkg"} 184 | dummyPatchCmd := PatchCmd{ 185 | Targets: targetList, 186 | PatchFn: func(_ *CallGraphNode, _ *ast.BlockStmt) (modifiedAST bool, extraImports []string) { 187 | return true, nil 188 | }, 189 | } 190 | updatedFiles, patchCount, err := pkg.Patch(vendorPkgRegex, dummyPatchCmd) 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | 195 | expUpdatedFiles := 2 196 | if updatedFiles != expUpdatedFiles { 197 | t.Fatalf("expected Patch() to update %d files; got %d", expUpdatedFiles, updatedFiles) 198 | } 199 | 200 | // pkg.Patch will modify main and the 2 call targets in its callgraph + 1 function in the vendored package 201 | expPatchCount := 4 202 | if patchCount != expPatchCount { 203 | t.Fatalf("expected Patch() to apply %d patches; got %d", expPatchCount, patchCount) 204 | } 205 | } 206 | 207 | func TestPatchPackageWithInvalidVendorRegex(t *testing.T) { 208 | wsDir, pkgDir, pkgName := mockPackageWithVendoredDeps(t, true) 209 | defer os.RemoveAll(wsDir) 210 | 211 | pkg, err := NewGoPackage(pkgDir) 212 | if err != nil { 213 | t.Fatal(err) 214 | } 215 | 216 | targetList, err := pkg.Find(pkgName + "/main") 217 | if err != nil { 218 | t.Fatal(err) 219 | } 220 | 221 | if len(targetList) != 1 { 222 | t.Fatal("error looking up patch target for main()") 223 | } 224 | 225 | vendorPkgRegex := []string{"other/pkg *****"} 226 | dummyPatchCmd := PatchCmd{ 227 | Targets: targetList, 228 | PatchFn: func(_ *CallGraphNode, _ *ast.BlockStmt) (modifiedAST bool, extraImports []string) { 229 | return true, nil 230 | }, 231 | } 232 | _, _, err = pkg.Patch(vendorPkgRegex, dummyPatchCmd) 233 | if err == nil || !strings.Contains(err.Error(), "could not compile regex") { 234 | t.Fatalf("expected to get a regex compilation error; got %v", err) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/command.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | // Command is a subcommand for a cli.App. 11 | type Command struct { 12 | // The name of the command 13 | Name string 14 | // short name of the command. Typically one character (deprecated, use `Aliases`) 15 | ShortName string 16 | // A list of aliases for the command 17 | Aliases []string 18 | // A short description of the usage of this command 19 | Usage string 20 | // Custom text to show on USAGE section of help 21 | UsageText string 22 | // A longer explanation of how the command works 23 | Description string 24 | // A short description of the arguments of this command 25 | ArgsUsage string 26 | // The category the command is part of 27 | Category string 28 | // The function to call when checking for bash command completions 29 | BashComplete BashCompleteFunc 30 | // An action to execute before any sub-subcommands are run, but after the context is ready 31 | // If a non-nil error is returned, no sub-subcommands are run 32 | Before BeforeFunc 33 | // An action to execute after any subcommands are run, but after the subcommand has finished 34 | // It is run even if Action() panics 35 | After AfterFunc 36 | // The function to call when this command is invoked 37 | Action interface{} 38 | // TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind 39 | // of deprecation period has passed, maybe? 40 | 41 | // Execute this function if a usage error occurs. 42 | OnUsageError OnUsageErrorFunc 43 | // List of child commands 44 | Subcommands Commands 45 | // List of flags to parse 46 | Flags []Flag 47 | // Treat all flags as normal arguments if true 48 | SkipFlagParsing bool 49 | // Boolean to hide built-in help command 50 | HideHelp bool 51 | // Boolean to hide this command from help or completion 52 | Hidden bool 53 | 54 | // Full name of command for help, defaults to full command name, including parent commands. 55 | HelpName string 56 | commandNamePath []string 57 | } 58 | 59 | // FullName returns the full name of the command. 60 | // For subcommands this ensures that parent commands are part of the command path 61 | func (c Command) FullName() string { 62 | if c.commandNamePath == nil { 63 | return c.Name 64 | } 65 | return strings.Join(c.commandNamePath, " ") 66 | } 67 | 68 | // Commands is a slice of Command 69 | type Commands []Command 70 | 71 | // Run invokes the command given the context, parses ctx.Args() to generate command-specific flags 72 | func (c Command) Run(ctx *Context) (err error) { 73 | if len(c.Subcommands) > 0 { 74 | return c.startApp(ctx) 75 | } 76 | 77 | if !c.HideHelp && (HelpFlag != BoolFlag{}) { 78 | // append help to flags 79 | c.Flags = append( 80 | c.Flags, 81 | HelpFlag, 82 | ) 83 | } 84 | 85 | if ctx.App.EnableBashCompletion { 86 | c.Flags = append(c.Flags, BashCompletionFlag) 87 | } 88 | 89 | set := flagSet(c.Name, c.Flags) 90 | set.SetOutput(ioutil.Discard) 91 | 92 | if !c.SkipFlagParsing { 93 | firstFlagIndex := -1 94 | terminatorIndex := -1 95 | for index, arg := range ctx.Args() { 96 | if arg == "--" { 97 | terminatorIndex = index 98 | break 99 | } else if arg == "-" { 100 | // Do nothing. A dash alone is not really a flag. 101 | continue 102 | } else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 { 103 | firstFlagIndex = index 104 | } 105 | } 106 | 107 | if firstFlagIndex > -1 { 108 | args := ctx.Args() 109 | regularArgs := make([]string, len(args[1:firstFlagIndex])) 110 | copy(regularArgs, args[1:firstFlagIndex]) 111 | 112 | var flagArgs []string 113 | if terminatorIndex > -1 { 114 | flagArgs = args[firstFlagIndex:terminatorIndex] 115 | regularArgs = append(regularArgs, args[terminatorIndex:]...) 116 | } else { 117 | flagArgs = args[firstFlagIndex:] 118 | } 119 | 120 | err = set.Parse(append(flagArgs, regularArgs...)) 121 | } else { 122 | err = set.Parse(ctx.Args().Tail()) 123 | } 124 | } else { 125 | if c.SkipFlagParsing { 126 | err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...)) 127 | } 128 | } 129 | 130 | if err != nil { 131 | if c.OnUsageError != nil { 132 | err := c.OnUsageError(ctx, err, false) 133 | HandleExitCoder(err) 134 | return err 135 | } 136 | fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") 137 | fmt.Fprintln(ctx.App.Writer) 138 | ShowCommandHelp(ctx, c.Name) 139 | return err 140 | } 141 | 142 | nerr := normalizeFlags(c.Flags, set) 143 | if nerr != nil { 144 | fmt.Fprintln(ctx.App.Writer, nerr) 145 | fmt.Fprintln(ctx.App.Writer) 146 | ShowCommandHelp(ctx, c.Name) 147 | return nerr 148 | } 149 | 150 | context := NewContext(ctx.App, set, ctx) 151 | 152 | if checkCommandCompletions(context, c.Name) { 153 | return nil 154 | } 155 | 156 | if checkCommandHelp(context, c.Name) { 157 | return nil 158 | } 159 | 160 | if c.After != nil { 161 | defer func() { 162 | afterErr := c.After(context) 163 | if afterErr != nil { 164 | HandleExitCoder(err) 165 | if err != nil { 166 | err = NewMultiError(err, afterErr) 167 | } else { 168 | err = afterErr 169 | } 170 | } 171 | }() 172 | } 173 | 174 | if c.Before != nil { 175 | err = c.Before(context) 176 | if err != nil { 177 | fmt.Fprintln(ctx.App.Writer, err) 178 | fmt.Fprintln(ctx.App.Writer) 179 | ShowCommandHelp(ctx, c.Name) 180 | HandleExitCoder(err) 181 | return err 182 | } 183 | } 184 | 185 | context.Command = c 186 | err = HandleAction(c.Action, context) 187 | 188 | if err != nil { 189 | HandleExitCoder(err) 190 | } 191 | return err 192 | } 193 | 194 | // Names returns the names including short names and aliases. 195 | func (c Command) Names() []string { 196 | names := []string{c.Name} 197 | 198 | if c.ShortName != "" { 199 | names = append(names, c.ShortName) 200 | } 201 | 202 | return append(names, c.Aliases...) 203 | } 204 | 205 | // HasName returns true if Command.Name or Command.ShortName matches given name 206 | func (c Command) HasName(name string) bool { 207 | for _, n := range c.Names() { 208 | if n == name { 209 | return true 210 | } 211 | } 212 | return false 213 | } 214 | 215 | func (c Command) startApp(ctx *Context) error { 216 | app := NewApp() 217 | app.Metadata = ctx.App.Metadata 218 | // set the name and usage 219 | app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) 220 | if c.HelpName == "" { 221 | app.HelpName = c.HelpName 222 | } else { 223 | app.HelpName = app.Name 224 | } 225 | 226 | if c.Description != "" { 227 | app.Usage = c.Description 228 | } else { 229 | app.Usage = c.Usage 230 | } 231 | 232 | // set CommandNotFound 233 | app.CommandNotFound = ctx.App.CommandNotFound 234 | 235 | // set the flags and commands 236 | app.Commands = c.Subcommands 237 | app.Flags = c.Flags 238 | app.HideHelp = c.HideHelp 239 | 240 | app.Version = ctx.App.Version 241 | app.HideVersion = ctx.App.HideVersion 242 | app.Compiled = ctx.App.Compiled 243 | app.Author = ctx.App.Author 244 | app.Email = ctx.App.Email 245 | app.Writer = ctx.App.Writer 246 | 247 | app.categories = CommandCategories{} 248 | for _, command := range c.Subcommands { 249 | app.categories = app.categories.AddCommand(command.Category, command) 250 | } 251 | 252 | sort.Sort(app.categories) 253 | 254 | // bash completion 255 | app.EnableBashCompletion = ctx.App.EnableBashCompletion 256 | if c.BashComplete != nil { 257 | app.BashComplete = c.BashComplete 258 | } 259 | 260 | // set the actions 261 | app.Before = c.Before 262 | app.After = c.After 263 | if c.Action != nil { 264 | app.Action = c.Action 265 | } else { 266 | app.Action = helpSubcommand.Action 267 | } 268 | 269 | for index, cc := range app.Commands { 270 | app.Commands[index].commandNamePath = []string{c.Name, cc.Name} 271 | } 272 | 273 | return app.RunAsSubcommand(ctx) 274 | } 275 | 276 | // VisibleFlags returns a slice of the Flags with Hidden=false 277 | func (c Command) VisibleFlags() []Flag { 278 | return visibleFlags(c.Flags) 279 | } 280 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/help.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | "text/tabwriter" 9 | "text/template" 10 | ) 11 | 12 | // AppHelpTemplate is the text template for the Default help topic. 13 | // cli.go uses text/template to render templates. You can 14 | // render custom help text by setting this variable. 15 | var AppHelpTemplate = `NAME: 16 | {{.Name}} - {{.Usage}} 17 | 18 | USAGE: 19 | {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} 20 | {{if .Version}}{{if not .HideVersion}} 21 | VERSION: 22 | {{.Version}} 23 | {{end}}{{end}}{{if len .Authors}} 24 | AUTHOR(S): 25 | {{range .Authors}}{{.}}{{end}} 26 | {{end}}{{if .VisibleCommands}} 27 | COMMANDS:{{range .VisibleCategories}}{{if .Name}} 28 | {{.Name}}:{{end}}{{range .VisibleCommands}} 29 | {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}} 30 | {{end}}{{end}}{{if .VisibleFlags}} 31 | GLOBAL OPTIONS: 32 | {{range .VisibleFlags}}{{.}} 33 | {{end}}{{end}}{{if .Copyright}} 34 | COPYRIGHT: 35 | {{.Copyright}} 36 | {{end}} 37 | ` 38 | 39 | // CommandHelpTemplate is the text template for the command help topic. 40 | // cli.go uses text/template to render templates. You can 41 | // render custom help text by setting this variable. 42 | var CommandHelpTemplate = `NAME: 43 | {{.HelpName}} - {{.Usage}} 44 | 45 | USAGE: 46 | {{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}} 47 | 48 | CATEGORY: 49 | {{.Category}}{{end}}{{if .Description}} 50 | 51 | DESCRIPTION: 52 | {{.Description}}{{end}}{{if .VisibleFlags}} 53 | 54 | OPTIONS: 55 | {{range .VisibleFlags}}{{.}} 56 | {{end}}{{end}} 57 | ` 58 | 59 | // SubcommandHelpTemplate is the text template for the subcommand help topic. 60 | // cli.go uses text/template to render templates. You can 61 | // render custom help text by setting this variable. 62 | var SubcommandHelpTemplate = `NAME: 63 | {{.HelpName}} - {{.Usage}} 64 | 65 | USAGE: 66 | {{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} 67 | 68 | COMMANDS:{{range .VisibleCategories}}{{if .Name}} 69 | {{.Name}}:{{end}}{{range .VisibleCommands}} 70 | {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}} 71 | {{end}}{{if .VisibleFlags}} 72 | OPTIONS: 73 | {{range .VisibleFlags}}{{.}} 74 | {{end}}{{end}} 75 | ` 76 | 77 | var helpCommand = Command{ 78 | Name: "help", 79 | Aliases: []string{"h"}, 80 | Usage: "Shows a list of commands or help for one command", 81 | ArgsUsage: "[command]", 82 | Action: func(c *Context) error { 83 | args := c.Args() 84 | if args.Present() { 85 | return ShowCommandHelp(c, args.First()) 86 | } 87 | 88 | ShowAppHelp(c) 89 | return nil 90 | }, 91 | } 92 | 93 | var helpSubcommand = Command{ 94 | Name: "help", 95 | Aliases: []string{"h"}, 96 | Usage: "Shows a list of commands or help for one command", 97 | ArgsUsage: "[command]", 98 | Action: func(c *Context) error { 99 | args := c.Args() 100 | if args.Present() { 101 | return ShowCommandHelp(c, args.First()) 102 | } 103 | 104 | return ShowSubcommandHelp(c) 105 | }, 106 | } 107 | 108 | // Prints help for the App or Command 109 | type helpPrinter func(w io.Writer, templ string, data interface{}) 110 | 111 | // HelpPrinter is a function that writes the help output. If not set a default 112 | // is used. The function signature is: 113 | // func(w io.Writer, templ string, data interface{}) 114 | var HelpPrinter helpPrinter = printHelp 115 | 116 | // VersionPrinter prints the version for the App 117 | var VersionPrinter = printVersion 118 | 119 | // ShowAppHelp is an action that displays the help. 120 | func ShowAppHelp(c *Context) error { 121 | HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) 122 | return nil 123 | } 124 | 125 | // DefaultAppComplete prints the list of subcommands as the default app completion method 126 | func DefaultAppComplete(c *Context) { 127 | for _, command := range c.App.Commands { 128 | if command.Hidden { 129 | continue 130 | } 131 | for _, name := range command.Names() { 132 | fmt.Fprintln(c.App.Writer, name) 133 | } 134 | } 135 | } 136 | 137 | // ShowCommandHelp prints help for the given command 138 | func ShowCommandHelp(ctx *Context, command string) error { 139 | // show the subcommand help for a command with subcommands 140 | if command == "" { 141 | HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) 142 | return nil 143 | } 144 | 145 | for _, c := range ctx.App.Commands { 146 | if c.HasName(command) { 147 | HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) 148 | return nil 149 | } 150 | } 151 | 152 | if ctx.App.CommandNotFound == nil { 153 | return NewExitError(fmt.Sprintf("No help topic for '%v'", command), 3) 154 | } 155 | 156 | ctx.App.CommandNotFound(ctx, command) 157 | return nil 158 | } 159 | 160 | // ShowSubcommandHelp prints help for the given subcommand 161 | func ShowSubcommandHelp(c *Context) error { 162 | return ShowCommandHelp(c, c.Command.Name) 163 | } 164 | 165 | // ShowVersion prints the version number of the App 166 | func ShowVersion(c *Context) { 167 | VersionPrinter(c) 168 | } 169 | 170 | func printVersion(c *Context) { 171 | fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) 172 | } 173 | 174 | // ShowCompletions prints the lists of commands within a given context 175 | func ShowCompletions(c *Context) { 176 | a := c.App 177 | if a != nil && a.BashComplete != nil { 178 | a.BashComplete(c) 179 | } 180 | } 181 | 182 | // ShowCommandCompletions prints the custom completions for a given command 183 | func ShowCommandCompletions(ctx *Context, command string) { 184 | c := ctx.App.Command(command) 185 | if c != nil && c.BashComplete != nil { 186 | c.BashComplete(ctx) 187 | } 188 | } 189 | 190 | func printHelp(out io.Writer, templ string, data interface{}) { 191 | funcMap := template.FuncMap{ 192 | "join": strings.Join, 193 | } 194 | 195 | w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) 196 | t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) 197 | err := t.Execute(w, data) 198 | if err != nil { 199 | // If the writer is closed, t.Execute will fail, and there's nothing 200 | // we can do to recover. 201 | if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" { 202 | fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err) 203 | } 204 | return 205 | } 206 | w.Flush() 207 | } 208 | 209 | func checkVersion(c *Context) bool { 210 | found := false 211 | if VersionFlag.Name != "" { 212 | eachName(VersionFlag.Name, func(name string) { 213 | if c.GlobalBool(name) || c.Bool(name) { 214 | found = true 215 | } 216 | }) 217 | } 218 | return found 219 | } 220 | 221 | func checkHelp(c *Context) bool { 222 | found := false 223 | if HelpFlag.Name != "" { 224 | eachName(HelpFlag.Name, func(name string) { 225 | if c.GlobalBool(name) || c.Bool(name) { 226 | found = true 227 | } 228 | }) 229 | } 230 | return found 231 | } 232 | 233 | func checkCommandHelp(c *Context, name string) bool { 234 | if c.Bool("h") || c.Bool("help") { 235 | ShowCommandHelp(c, name) 236 | return true 237 | } 238 | 239 | return false 240 | } 241 | 242 | func checkSubcommandHelp(c *Context) bool { 243 | if c.GlobalBool("h") || c.GlobalBool("help") { 244 | ShowSubcommandHelp(c) 245 | return true 246 | } 247 | 248 | return false 249 | } 250 | 251 | func checkCompletions(c *Context) bool { 252 | if (c.GlobalBool(BashCompletionFlag.Name) || c.Bool(BashCompletionFlag.Name)) && c.App.EnableBashCompletion { 253 | ShowCompletions(c) 254 | return true 255 | } 256 | 257 | return false 258 | } 259 | 260 | func checkCommandCompletions(c *Context, name string) bool { 261 | if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { 262 | ShowCommandCompletions(c, name) 263 | return true 264 | } 265 | 266 | return false 267 | } 268 | -------------------------------------------------------------------------------- /vendor/github.com/geckoboard/cli-table/table.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "regexp" 9 | "strings" 10 | "unicode/utf8" 11 | ) 12 | 13 | // Alignment represents the supported cell content alignment modes. 14 | type Alignment uint8 15 | 16 | const ( 17 | AlignLeft Alignment = iota 18 | AlignCenter 19 | AlignRight 20 | ) 21 | 22 | // CharacterFilter defines the character filter modes supported by the table writer. 23 | type CharacterFilter uint8 24 | 25 | const ( 26 | PreserveAnsi CharacterFilter = iota 27 | StripAnsi 28 | ) 29 | 30 | var ( 31 | ansiEscapeRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`) 32 | tableColSplitRegex = regexp.MustCompile(`\s*,\s*`) 33 | ) 34 | 35 | // Header groups are used to define headers that span multiple columns. 36 | type headerGroup struct { 37 | header string 38 | alignment Alignment 39 | colSpan int 40 | } 41 | 42 | // A table that can be rendered in a terminal. 43 | type Table struct { 44 | headers []string 45 | headerGroups []headerGroup 46 | rows [][]string 47 | alignments []Alignment 48 | padding int 49 | } 50 | 51 | // Create a new empty table with the specified number of columns. 52 | func New(columns int) *Table { 53 | return &Table{ 54 | headers: make([]string, columns), 55 | headerGroups: make([]headerGroup, 0), 56 | rows: make([][]string, 0), 57 | alignments: make([]Alignment, columns), 58 | } 59 | } 60 | 61 | // Set cell padding for cell contents. If a negative padding is specified, a 62 | // padding value of 0 will be forced. 63 | func (t *Table) SetPadding(padding int) { 64 | if padding < 0 { 65 | padding = 0 66 | } 67 | 68 | t.padding = padding 69 | } 70 | 71 | // Set header title and column alignment settings. Column indices are 0-based. 72 | func (t *Table) SetHeader(col int, title string, alignment Alignment) error { 73 | if col < 0 || col > len(t.headers)-1 { 74 | return fmt.Errorf("index out of range while attempting to set table header for column %d", col) 75 | } 76 | 77 | t.headers[col] = title 78 | t.alignments[col] = alignment 79 | return nil 80 | } 81 | 82 | // Add a super-group for a set of header columns. If the requested colSpan exceeds 83 | // the number of available un-grouped header columns this method returns an error. 84 | func (t *Table) AddHeaderGroup(colSpan int, title string, alignment Alignment) error { 85 | groupedCols := 0 86 | for _, hg := range t.headerGroups { 87 | groupedCols += hg.colSpan 88 | } 89 | 90 | colCount := len(t.headers) 91 | if groupedCols+colSpan > colCount { 92 | return fmt.Errorf("requested header group colspan %d exceeds the available columns for grouping %d/%d", colSpan, groupedCols, colCount) 93 | } 94 | 95 | t.headerGroups = append(t.headerGroups, headerGroup{ 96 | header: title, 97 | colSpan: colSpan, 98 | alignment: alignment, 99 | }) 100 | return nil 101 | } 102 | 103 | // Append one or more rows to the table. 104 | func (t *Table) Append(rows ...[]string) error { 105 | colCount := len(t.headers) 106 | 107 | for rowIndex, row := range rows { 108 | if len(row) != colCount { 109 | return fmt.Errorf("inconsistent number of colums for row %d; expected %d but got %d", rowIndex, colCount, len(row)) 110 | } 111 | } 112 | 113 | t.rows = append(t.rows, rows...) 114 | return nil 115 | } 116 | 117 | // Render table to an io.Writer. The charFilter parameter can be used to 118 | // either preserve or strip ANSI characters from the output. 119 | func (t *Table) Write(to io.Writer, charFilter CharacterFilter) { 120 | stripAnsiChars := charFilter == StripAnsi 121 | w := bufio.NewWriter(to) 122 | padding := strings.Repeat(" ", t.padding) 123 | 124 | // Calculate col widths and use them to calculate group heading widths 125 | colWidths := t.colWidths() 126 | 127 | // Render header groups if defined 128 | if len(t.headerGroups) > 0 { 129 | var groupWidths []int 130 | groupWidths, colWidths = t.groupWidths(colWidths) 131 | hLine := t.hLine(groupWidths) 132 | 133 | w.WriteString(hLine) 134 | w.WriteByte('|') 135 | for hgIndex, hg := range t.headerGroups { 136 | w.WriteString(padding) 137 | w.WriteString(t.align(hg.header, hg.alignment, groupWidths[hgIndex], stripAnsiChars)) 138 | w.WriteString(padding) 139 | w.WriteByte('|') 140 | } 141 | w.WriteString("\n") 142 | w.WriteString(hLine) 143 | } 144 | 145 | // Render headers 146 | hLine := t.hLine(colWidths) 147 | if len(t.headerGroups) == 0 { 148 | w.WriteString(hLine) 149 | } 150 | w.WriteByte('|') 151 | for colIndex, h := range t.headers { 152 | w.WriteString(padding) 153 | w.WriteString(t.align(h, t.alignments[colIndex], colWidths[colIndex], stripAnsiChars)) 154 | w.WriteString(padding) 155 | w.WriteByte('|') 156 | } 157 | w.WriteString("\n") 158 | w.WriteString(hLine) 159 | 160 | // Render rows 161 | for _, row := range t.rows { 162 | w.WriteByte('|') 163 | for colIndex, c := range row { 164 | w.WriteString(padding) 165 | w.WriteString(t.align(c, t.alignments[colIndex], colWidths[colIndex], stripAnsiChars)) 166 | w.WriteString(padding) 167 | w.WriteByte('|') 168 | } 169 | w.WriteString("\n") 170 | } 171 | 172 | // Render footer line if the table is not empty 173 | if len(t.rows) > 0 { 174 | w.WriteString(hLine) 175 | } 176 | 177 | w.Flush() 178 | } 179 | 180 | // Generate horizontal line. 181 | func (t *Table) hLine(colWidths []int) string { 182 | buf := bytes.NewBufferString("") 183 | 184 | buf.WriteByte('+') 185 | for _, colWidth := range colWidths { 186 | buf.WriteString(strings.Repeat("-", colWidth+2*t.padding)) 187 | buf.WriteByte('+') 188 | } 189 | buf.WriteString("\n") 190 | 191 | return buf.String() 192 | } 193 | 194 | // Pad and align input string. 195 | func (t *Table) align(val string, align Alignment, maxWidth int, stripAnsiChars bool) string { 196 | var vLen int 197 | 198 | if stripAnsiChars { 199 | val = ansiEscapeRegex.ReplaceAllString(val, "") 200 | vLen = utf8.RuneCountInString(val) 201 | } else { 202 | vLen = measure(val) 203 | } 204 | 205 | switch align { 206 | case AlignLeft: 207 | return val + strings.Repeat(" ", maxWidth-vLen) 208 | case AlignRight: 209 | return strings.Repeat(" ", maxWidth-vLen) + val 210 | default: 211 | lPad := (maxWidth - vLen) / 2 212 | return strings.Repeat(" ", lPad) + val + strings.Repeat(" ", maxWidth-lPad-vLen) 213 | } 214 | } 215 | 216 | // Calculate max width for each column. 217 | func (t *Table) colWidths() []int { 218 | colWidths := make([]int, len(t.headers)) 219 | for colIndex, h := range t.headers { 220 | maxWidth := utf8.RuneCountInString(h) 221 | for _, row := range t.rows { 222 | cellWidth := measure(row[colIndex]) 223 | if cellWidth > maxWidth { 224 | maxWidth = cellWidth 225 | } 226 | } 227 | 228 | colWidths[colIndex] = maxWidth 229 | } 230 | return colWidths 231 | } 232 | 233 | // Calculate max width for each header group. If a group header's width exceeds 234 | // the total width of the grouped columns, they will be automatically expanded 235 | // to preserve alignment with the group header. 236 | func (t *Table) groupWidths(colWidths []int) (groupWidths []int, adjustedColWidths []int) { 237 | adjustedColWidths = append([]int{}, colWidths...) 238 | groupWidths = make([]int, len(t.headerGroups)) 239 | 240 | groupStartCol := 0 241 | for groupIndex, group := range t.headerGroups { 242 | // Calculate group width based on the grouped columns 243 | groupWidth := 0 244 | for ci := groupStartCol; ci < groupStartCol+group.colSpan; ci++ { 245 | groupWidth += colWidths[ci] 246 | } 247 | 248 | // Include separators and padding for inner columns to width 249 | if group.colSpan > 1 { 250 | groupWidth += (group.colSpan - 1) * (1 + 2*t.padding) 251 | } 252 | 253 | // Calculate group width based on padding and group title. If its 254 | // greater than the calculated groupWidth, append the extra space to the last group col 255 | contentWidth := 2*t.padding + utf8.RuneCountInString(group.header) 256 | if contentWidth > groupWidth { 257 | adjustedColWidths[groupStartCol+group.colSpan-1] += contentWidth - groupWidth 258 | groupWidth = contentWidth 259 | } 260 | 261 | groupWidths[groupIndex] = groupWidth 262 | groupStartCol += group.colSpan 263 | } 264 | return groupWidths, adjustedColWidths 265 | } 266 | 267 | // Measure string length excluding any Ansi color escape codes. 268 | func measure(val string) int { 269 | return utf8.RuneCountInString(ansiEscapeRegex.ReplaceAllString(val, "")) 270 | } 271 | -------------------------------------------------------------------------------- /profiler/profile_test.go: -------------------------------------------------------------------------------- 1 | package profiler 2 | 3 | import ( 4 | "encoding/json" 5 | "math" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestGroupMetrics(t *testing.T) { 11 | fnName := "testFn" 12 | numMetrics := 100 13 | 14 | metrics := metricsList{} 15 | for i := 0; i < numMetrics; i++ { 16 | metrics = append( 17 | metrics, 18 | &CallMetrics{ 19 | FnName: fnName, 20 | TotalTime: time.Duration(i) * time.Millisecond, 21 | }, 22 | ) 23 | } 24 | 25 | groupedMetric := metrics.aggregate() 26 | expValues := map[string]time.Duration{ 27 | // This is an arithmetic series so S = n/2 * (a_0 + a_n) 28 | "total_time": time.Duration(numMetrics/2) * (metrics[0].TotalTime + metrics[numMetrics-1].TotalTime), 29 | // 30 | "min_time": 0, 31 | "max_time": metrics[numMetrics-1].TotalTime, 32 | // 33 | "mean_time": metrics[0].TotalTime + metrics[numMetrics-1].TotalTime/2, 34 | "median_time": (metrics[(numMetrics/2)-1].TotalTime + metrics[numMetrics/2].TotalTime) / 2, 35 | // 36 | "p50_time": metrics[50-1].TotalTime, 37 | "p75_time": metrics[75-1].TotalTime, 38 | "p90_time": metrics[90-1].TotalTime, 39 | "p99_time": metrics[99-1].TotalTime, 40 | } 41 | 42 | dataDump, _ := json.Marshal(groupedMetric) 43 | var dataMap map[string]time.Duration 44 | json.Unmarshal(dataDump, &dataMap) 45 | for k, expVal := range expValues { 46 | if dataMap[k] != expVal { 47 | t.Errorf("expected group metric key %q to be %v; got %v", k, expVal, dataMap[k]) 48 | } 49 | } 50 | 51 | // arithmetic series stddev: |d| * sqrt( (n-1)(n+1) / 12 ) 52 | expStdDev := float64(time.Millisecond) * math.Sqrt(float64((numMetrics-1)*(numMetrics+1))/12.0) 53 | if groupedMetric.StdDev != expStdDev { 54 | t.Errorf("expected stddev to be %f; got %f", expStdDev, groupedMetric.StdDev) 55 | } 56 | 57 | // append one more entry to calc median time with odd number of metrics 58 | metrics = append(metrics, &CallMetrics{TotalTime: time.Duration(numMetrics) * time.Millisecond}) 59 | groupedMetric = metrics.aggregate() 60 | expMedian := metrics[len(metrics)/2].TotalTime 61 | if groupedMetric.MedianTime != expMedian { 62 | t.Errorf("expected median time with odd number of metrics to be %v; got %v", expMedian, groupedMetric.MedianTime) 63 | } 64 | } 65 | 66 | func TestGenProfile(t *testing.T) { 67 | tick := time.Now() 68 | numNestedCalls := 10 69 | timeInRoot := 5 * time.Millisecond 70 | rootOverhead := 5 * time.Nanosecond 71 | nestedCallOverhead := 2 * time.Nanosecond 72 | 73 | root := &fnCall{ 74 | fnName: "func1", 75 | enteredAt: tick, 76 | profilerOverhead: rootOverhead, 77 | nestedCalls: make([]*fnCall, 0), 78 | } 79 | tick = tick.Add(rootOverhead + timeInRoot) 80 | 81 | var totalTimeInNestedCalls time.Duration 82 | for i := 0; i < numNestedCalls; i++ { 83 | timeInFunc := time.Duration(i+1) * time.Millisecond 84 | totalTimeInNestedCalls += timeInFunc 85 | exitedAt := tick.Add(timeInFunc + nestedCallOverhead) 86 | 87 | nestedCall := makeFnCall("func2") 88 | nestedCall.enteredAt = tick 89 | nestedCall.exitedAt = exitedAt 90 | nestedCall.profilerOverhead = nestedCallOverhead 91 | root.profilerOverhead += nestedCallOverhead 92 | root.nestCall(nestedCall) 93 | 94 | if nestedCall.parent != root { 95 | t.Fatal("expected nested call parent to be the root call after a call to nestCall()") 96 | } 97 | 98 | tick = exitedAt 99 | } 100 | root.exitedAt = tick.Add(root.profilerOverhead) 101 | root.profilerOverhead += root.profilerOverhead 102 | 103 | expLabel := "test-profile" 104 | expID := threadID() 105 | profile := genProfile(expID, expLabel, root) 106 | 107 | if profile.Label != expLabel { 108 | t.Fatalf("expected profile label to be %q; got %q", expLabel, profile.Label) 109 | } 110 | 111 | if profile.ID != expID { 112 | t.Fatalf("expected profile ID to be %d; got %d", expID, profile.ID) 113 | } 114 | 115 | if profile.CreatedAt != root.enteredAt { 116 | t.Fatal("expected profile creation timestamp to match the entry timestamp for the target func") 117 | } 118 | 119 | expRootTotalTime := root.exitedAt.Sub(root.enteredAt) - root.profilerOverhead 120 | if profile.Target.TotalTime != expRootTotalTime { 121 | t.Fatalf("expected func1 total time (sans any overhead) to be %d; got %d", expRootTotalTime, profile.Target.TotalTime) 122 | } 123 | 124 | expRootTotalTime = timeInRoot + totalTimeInNestedCalls 125 | if profile.Target.TotalTime != expRootTotalTime { 126 | t.Fatalf("expected func1 total time (sans any overhead) to be %d; got %d", expRootTotalTime, profile.Target.TotalTime) 127 | } 128 | 129 | nestedCalls := root.nestedCalls 130 | root.free() 131 | if root.nestedCalls != nil { 132 | t.Fatal("expected calling free() on the root fnCall to free the nestedCalls list") 133 | } 134 | for callIndex, nestedCall := range nestedCalls { 135 | if nestedCall.nestedCalls != nil { 136 | t.Errorf("[nested call %d] expected calling free() to set nestedCalls to nil", callIndex) 137 | } 138 | } 139 | } 140 | 141 | func TestCallGrouping(t *testing.T) { 142 | // Call graph models the following flow: 143 | // 144 | // C--F 145 | // B--| 146 | // | D 147 | // A--| 148 | // | C 149 | // B--| 150 | // E 151 | // 152 | graph := &fnCall{ 153 | fnName: "A", 154 | nestedCalls: []*fnCall{ 155 | &fnCall{ 156 | fnName: "B", 157 | nestedCalls: []*fnCall{ 158 | &fnCall{ 159 | fnName: "C", 160 | nestedCalls: []*fnCall{ 161 | &fnCall{ 162 | fnName: "F", 163 | nestedCalls: []*fnCall{}, 164 | }, 165 | }, 166 | }, 167 | &fnCall{ 168 | fnName: "D", 169 | nestedCalls: []*fnCall{}, 170 | }, 171 | }, 172 | }, 173 | &fnCall{ 174 | fnName: "B", 175 | nestedCalls: []*fnCall{ 176 | &fnCall{ 177 | fnName: "C", 178 | nestedCalls: []*fnCall{}, 179 | }, 180 | &fnCall{ 181 | fnName: "E", 182 | nestedCalls: []*fnCall{}, 183 | }, 184 | }, 185 | }, 186 | }, 187 | } 188 | 189 | // Setup parent pointers 190 | var fillParents func(fn *fnCall) 191 | fillParents = func(fn *fnCall) { 192 | for _, child := range fn.nestedCalls { 193 | child.parent = fn 194 | fillParents(child) 195 | } 196 | } 197 | fillParents(graph) 198 | 199 | // Group calls 200 | cgt := &callGroupTree{ 201 | levels: make([]*callGroups, 0), 202 | } 203 | 204 | cgt.insert(0, graph) 205 | cgt.linkGroups() 206 | 207 | // After grouping and linking we expect the processed tree to look like: 208 | // 209 | // [A] -- [B, B] --|- [C, C] -- [F] 210 | // |- [D] 211 | // |- [E] 212 | testGroup := cgt.levels[0].groups[0] 213 | if len(testGroup.calls) != 1 || testGroup.calls[0].fnName != "A" { 214 | t.Fatal(`expected call group at level 0 to contain 1 call entry of type "A"`) 215 | } 216 | if len(testGroup.nestedGroups) != 1 { 217 | t.Fatal(`expected call group at level 0 to contain 1 nested group`) 218 | } 219 | 220 | testGroup = testGroup.nestedGroups[0] 221 | if len(testGroup.calls) != 2 || testGroup.calls[0].fnName != "B" { 222 | t.Fatal(`expected call group at level 1 to contain 2 call entries of type "B"`) 223 | } 224 | if len(testGroup.nestedGroups) != 3 { 225 | t.Fatal(`expected call group at level 1 to contain 3 nested group`) 226 | } 227 | 228 | testSubGroup := testGroup.nestedGroups[0] 229 | if len(testSubGroup.calls) != 2 || testSubGroup.calls[0].fnName != "C" { 230 | t.Fatal(`expected call group 0 at level 2 to contain 2 call entries of type "C"`) 231 | } 232 | if len(testSubGroup.nestedGroups) != 1 { 233 | t.Fatal(`expected call group 0 at level 2 to contain 1 nested group`) 234 | } 235 | 236 | testSubGroup = testSubGroup.nestedGroups[0] 237 | if len(testSubGroup.calls) != 1 || testSubGroup.calls[0].fnName != "F" { 238 | t.Fatal(`expected call group at level 3 to contain 1 call entry of type "F"`) 239 | } 240 | if len(testSubGroup.nestedGroups) != 0 { 241 | t.Fatal(`expected call group at level 3 to contain 0 nested groups`) 242 | } 243 | 244 | testSubGroup = testGroup.nestedGroups[1] 245 | if len(testSubGroup.calls) != 1 || testSubGroup.calls[0].fnName != "D" { 246 | t.Fatal(`expected call group 1 at level 2 to contain 1 call entry of type "D"`) 247 | } 248 | if len(testSubGroup.nestedGroups) != 0 { 249 | t.Fatal(`expected call group 1 at level 2 to contain 0 nested groups`) 250 | } 251 | 252 | testSubGroup = testGroup.nestedGroups[2] 253 | if len(testSubGroup.calls) != 1 || testSubGroup.calls[0].fnName != "E" { 254 | t.Fatal(`expected call group 2 at level 2 to contain 1 call entry of type "E"`) 255 | } 256 | if len(testSubGroup.nestedGroups) != 0 { 257 | t.Fatal(`expected call group 2 at level 2 to contain 0 nested groups`) 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /cmd/profile.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "os/signal" 12 | "path/filepath" 13 | "regexp" 14 | "strings" 15 | "syscall" 16 | 17 | "github.com/geckoboard/prism/profiler" 18 | "github.com/geckoboard/prism/tools" 19 | "gopkg.in/urfave/cli.v1" 20 | ) 21 | 22 | var ( 23 | errMissingPathToProject = errors.New("missing path_to_project argument") 24 | errNoProfileTargets = errors.New("no profile targets specified") 25 | errMissingRunCmd = errors.New("run-cmd not specified") 26 | 27 | tokenizeRegex = regexp.MustCompile("'.+?'|\".+?\"|\\S+") 28 | ) 29 | 30 | // ProfileProject clones a go package, injects profile hooks, builds and runs 31 | // the project to collect profiling information. 32 | func ProfileProject(ctx *cli.Context) error { 33 | args := ctx.Args() 34 | if len(args) != 1 { 35 | return errMissingPathToProject 36 | } 37 | 38 | profileFuncs := ctx.StringSlice("profile-target") 39 | if len(profileFuncs) == 0 { 40 | return errNoProfileTargets 41 | } 42 | 43 | runCmd := ctx.String("run-cmd") 44 | if runCmd == "" { 45 | return errMissingRunCmd 46 | } 47 | 48 | if !strings.HasSuffix("/", args[0]) { 49 | args[0] += "/" 50 | } 51 | absProjPath, err := filepath.Abs(filepath.Dir(args[0])) 52 | if err != nil { 53 | return err 54 | } 55 | absProjPath += "/" 56 | 57 | // Clone project 58 | tmpDir, tmpAbsProjPath, err := cloneProject(absProjPath, ctx.String("output-dir")) 59 | if err != nil { 60 | return err 61 | } 62 | if !ctx.Bool("preserve-output") { 63 | defer deleteClonedProject(tmpDir) 64 | } 65 | 66 | // Analyze project 67 | goPackage, err := tools.NewGoPackage(tmpAbsProjPath) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | // Select profile targets 73 | profileTargets, err := goPackage.Find(profileFuncs...) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | // Inject profiler hooks and bootstrap code to main() 79 | bootstrapTargets := []tools.ProfileTarget{ 80 | tools.ProfileTarget{ 81 | QualifiedName: goPackage.PkgPrefix + "/main", 82 | PkgPrefix: goPackage.PkgPrefix, 83 | }, 84 | } 85 | updatedFiles, patchCount, err := goPackage.Patch( 86 | ctx.StringSlice("profile-vendored-pkg"), 87 | tools.PatchCmd{Targets: profileTargets, PatchFn: tools.InjectProfiler()}, 88 | tools.PatchCmd{Targets: bootstrapTargets, PatchFn: tools.InjectProfilerBootstrap(ctx.String("profile-dir"), ctx.String("profile-label"))}, 89 | ) 90 | if err != nil { 91 | return err 92 | } 93 | fmt.Printf("profile: updated %d files and applied %d patches\n", updatedFiles, patchCount) 94 | 95 | // Handle build step if a build command is specified 96 | buildCmd := ctx.String("build-cmd") 97 | if buildCmd != "" { 98 | err = buildProject(goPackage.GOPATH, tmpAbsProjPath, buildCmd, ctx.Bool("no-ansi")) 99 | if err != nil { 100 | return err 101 | } 102 | } 103 | 104 | return runProject(goPackage.GOPATH, tmpAbsProjPath, runCmd, ctx.Bool("no-ansi")) 105 | } 106 | 107 | // Clone project and return path to the cloned project. 108 | func cloneProject(absProjPath, dest string) (tmpDir, tmpAbsProjPath string, err error) { 109 | skipLen := strings.Index(absProjPath, "/src/") 110 | 111 | tmpDir, err = ioutil.TempDir(dest, "prism-") 112 | if err != nil { 113 | return "", "", err 114 | } 115 | 116 | fmt.Printf("profile: copying project to %s\n", tmpDir) 117 | 118 | err = filepath.Walk(absProjPath, func(path string, info os.FileInfo, err error) error { 119 | if err != nil { 120 | return err 121 | } 122 | 123 | dstPath := tmpDir + path[skipLen:] 124 | 125 | if info.IsDir() { 126 | return os.MkdirAll(dstPath, info.Mode()) 127 | } else if !info.Mode().IsRegular() { 128 | fmt.Printf("profile: [WARNING] skipping non-regular file %s\n", path) 129 | return nil 130 | } 131 | 132 | // Copy file 133 | fSrc, err := os.Open(path) 134 | if err != nil { 135 | return err 136 | } 137 | defer fSrc.Close() 138 | fDst, err := os.Create(dstPath) 139 | if err != nil { 140 | return err 141 | } 142 | defer fDst.Close() 143 | _, err = io.Copy(fDst, fSrc) 144 | return err 145 | }) 146 | 147 | if err != nil { 148 | deleteClonedProject(tmpDir) 149 | return "", "", err 150 | } 151 | 152 | return tmpDir, 153 | tmpDir + absProjPath[skipLen:], 154 | nil 155 | } 156 | 157 | // Update GOPATH so that the workspace containing the cloned package is included 158 | // first. This ensures that go will pick up subpackages from the cloned folder. 159 | func overrideGoPath(adjustedGoPath string) []string { 160 | env := os.Environ() 161 | for index, envVar := range env { 162 | if strings.HasPrefix(envVar, "GOPATH=") { 163 | env[index] = "GOPATH=" + adjustedGoPath 164 | break 165 | } 166 | } 167 | 168 | return env 169 | } 170 | 171 | // Build patched project copy. 172 | func buildProject(adjustedGoPath, tmpAbsProjPath, buildCmd string, stripAnsi bool) error { 173 | fmt.Printf("profile: building patched project (%s)\n", buildCmd) 174 | 175 | color := "\033[32m" 176 | if stripAnsi { 177 | color = "" 178 | } 179 | 180 | // Setup buffered output writers 181 | stdout := newPaddedWriter(os.Stdout, "profile: [build] > ", color) 182 | stderr := newPaddedWriter(os.Stderr, "profile: [build] > ", color) 183 | 184 | // Setup the build command and set up its cwd and env overrides 185 | var execCmd *exec.Cmd 186 | tokens := tokenizeArgs(buildCmd) 187 | if len(tokens) > 1 { 188 | execCmd = exec.Command(tokens[0], tokens[1:]...) 189 | } else { 190 | execCmd = exec.Command(tokens[0]) 191 | } 192 | execCmd.Dir = tmpAbsProjPath 193 | execCmd.Env = overrideGoPath(adjustedGoPath) 194 | execCmd.Stdin = os.Stdin 195 | execCmd.Stdout = stdout 196 | execCmd.Stderr = stderr 197 | err := execCmd.Run() 198 | 199 | // Flush writers 200 | stdout.Flush() 201 | stderr.Flush() 202 | 203 | if err != nil { 204 | return fmt.Errorf("profile: build failed: %s", err.Error()) 205 | } 206 | 207 | return nil 208 | } 209 | 210 | // Run patched project to collect profiler data. 211 | func runProject(adjustedGoPath, tmpAbsProjPath, runCmd string, stripAnsi bool) error { 212 | fmt.Printf("profile: running patched project (%s)\n", runCmd) 213 | 214 | color := "\033[32m" 215 | if stripAnsi { 216 | color = "" 217 | } 218 | 219 | // Setup buffered output writers 220 | stdout := newPaddedWriter(os.Stdout, "profile: [run] > ", color) 221 | stderr := newPaddedWriter(os.Stderr, "profile: [run] > ", color) 222 | 223 | // Setup the run command and set up its cwd and env overrides 224 | var execCmd *exec.Cmd 225 | tokens := tokenizeArgs(runCmd) 226 | if len(tokens) > 1 { 227 | execCmd = exec.Command(tokens[0], tokens[1:]...) 228 | } else { 229 | execCmd = exec.Command(tokens[0]) 230 | } 231 | execCmd.Dir = tmpAbsProjPath 232 | execCmd.Env = overrideGoPath(adjustedGoPath) 233 | execCmd.Stdin = os.Stdin 234 | execCmd.Stdout = stdout 235 | execCmd.Stderr = stderr 236 | // start a signal handler and forward signals to process: 237 | sigChan := make(chan os.Signal, 1) 238 | defer close(sigChan) 239 | signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 240 | gotSignal := false 241 | go func() { 242 | s := <-sigChan 243 | gotSignal = true 244 | if execCmd.Process != nil { 245 | execCmd.Process.Signal(s) 246 | } 247 | }() 248 | err := execCmd.Run() 249 | 250 | // Flush writers 251 | stdout.Flush() 252 | stderr.Flush() 253 | 254 | if err != nil && !gotSignal { 255 | return fmt.Errorf("profile: run failed: %s", err.Error()) 256 | } 257 | 258 | if gotSignal { 259 | fmt.Printf("profile: patched process execution interrupted by signal\n") 260 | } 261 | 262 | return nil 263 | } 264 | 265 | // Delete temp project copy. 266 | func deleteClonedProject(path string) { 267 | os.RemoveAll(path) 268 | } 269 | 270 | // Split args into tokens using whitespace as the delimiter. This function 271 | // behaves similar to strings.Fields but also preseves quoted sections. 272 | func tokenizeArgs(args string) []string { 273 | return tokenizeRegex.FindAllString(args, -1) 274 | } 275 | 276 | // loadProfile reads a profile from disk. 277 | func loadProfile(file string) (*profiler.Profile, error) { 278 | if !strings.HasSuffix(file, ".json") { 279 | return nil, fmt.Errorf( 280 | "unrecognized profile extension %q for %q; only json profiles are currently supported", 281 | filepath.Ext(file), 282 | file, 283 | ) 284 | } 285 | 286 | f, err := os.Open(file) 287 | if err != nil { 288 | return nil, err 289 | } 290 | defer f.Close() 291 | 292 | data, err := ioutil.ReadAll(f) 293 | if err != nil { 294 | return nil, err 295 | } 296 | 297 | var profile *profiler.Profile 298 | err = json.Unmarshal(data, &profile) 299 | return profile, err 300 | } 301 | -------------------------------------------------------------------------------- /tools/package.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/printer" 8 | "go/token" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "runtime" 13 | "strings" 14 | 15 | "golang.org/x/tools/go/ssa" 16 | ) 17 | 18 | var ( 19 | stripCharRegex = regexp.MustCompile(`[()*]`) 20 | ) 21 | 22 | // PatchFunc is a function used to modify the AST for a go function matching a profile target. The 23 | // function should return a flag to indicate whether the AST of the function was modified 24 | // and a list of additional package imports to be injected into the file where the target 25 | // is defined. 26 | // 27 | // The method is passed a callgraph node instance and the AST node that corresponds to its body. 28 | type PatchFunc func(cgNode *CallGraphNode, fnDeclNode *ast.BlockStmt) (modifiedAST bool, extraImports []string) 29 | 30 | // PatchCmd groups together a list of targets and a patch function to apply to them. 31 | type PatchCmd struct { 32 | // The slice of targets to hook. 33 | Targets []ProfileTarget 34 | 35 | // The patch function to apply to functions matching the target. 36 | PatchFn PatchFunc 37 | } 38 | 39 | // Represents the contents of a parsed go file. 40 | type parsedGoFile struct { 41 | // Path to the file. 42 | filePath string 43 | 44 | // The package name for this file. 45 | pkgName string 46 | 47 | // The set of tokens and AST for the parsed file. 48 | fset *token.FileSet 49 | astFile *ast.File 50 | } 51 | 52 | // GoPackage contains the SSA analysis output performed on a package and all 53 | // packages it imports. 54 | type GoPackage struct { 55 | // The path to the project. 56 | pathToPackage string 57 | 58 | // The fully qualified package name for the base package. 59 | PkgPrefix string 60 | 61 | // A map of FQ function names to their SSA representation. This map 62 | // only contains functions that can be used as profile injection points. 63 | ssaFuncCandidates map[string]*ssa.Function 64 | 65 | // The GOPATH for loading package dependencies. We intentionally override it 66 | // so that the workspace path where this package's sources exist is included first. 67 | GOPATH string 68 | } 69 | 70 | // NewGoPackage analyzes all go files in pathToPackage as well as any other packages that are 71 | // referenced by them and constructs a static single-assignment representation of 72 | // the underlying code. 73 | func NewGoPackage(pathToPackage string) (*GoPackage, error) { 74 | // Detect FQN for project base package 75 | fqPkgPrefix, err := qualifiedPkgName(pathToPackage) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | adjustedGoPath, err := adjustGoPath(pathToPackage) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | candidates, err := ssaCandidates(pathToPackage, fqPkgPrefix, adjustedGoPath) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return &GoPackage{ 91 | pathToPackage: pathToPackage, 92 | PkgPrefix: fqPkgPrefix, 93 | ssaFuncCandidates: candidates, 94 | GOPATH: adjustedGoPath, 95 | }, nil 96 | } 97 | 98 | // Find will lookup a list of fully qualified profile targets inside the parsed package sources. 99 | // These targets serve as the entrypoint for injecting profiler hooks to any 100 | // function reachable by that entrypoint. 101 | // 102 | // Injector targets for functions without receivers are constructed by 103 | // concatenating the fully qualified package name where the function is defined, 104 | // a '/' character and the function name. 105 | // 106 | // If the function uses a receiver, the target name is constructed by 107 | // concatenating the fully qualified package name, a '/' character, the 108 | // function receiver's name, a '.' character and finally the function name. 109 | // 110 | // Here is an example of constructing injector targets for a set of functions 111 | // defined in package: "github.com/geckoboard/foo" 112 | // 113 | // package foo 114 | // 115 | // func MoreStuff(){} 116 | // 117 | // type Foo struct{} 118 | // func (f *Foo) DoStuff(){} 119 | // 120 | // The injector targets for the two functions are defined as: 121 | // github.com/geckoboard/foo/MoreStuff 122 | // github.com/geckoboard/foo/Foo.DoStuff 123 | func (pkg *GoPackage) Find(targetList ...string) ([]ProfileTarget, error) { 124 | profileTargets := make([]ProfileTarget, len(targetList)) 125 | var entrypointSSA *ssa.Function 126 | for targetIndex, target := range targetList { 127 | entrypointSSA = nil 128 | for candidate, ssaFn := range pkg.ssaFuncCandidates { 129 | if candidate == target { 130 | entrypointSSA = ssaFn 131 | break 132 | } 133 | } 134 | 135 | if entrypointSSA == nil { 136 | return nil, fmt.Errorf("GoPackage.Find: no match for profile target %q", target) 137 | } 138 | 139 | profileTargets[targetIndex] = ProfileTarget{ 140 | QualifiedName: target, 141 | PkgPrefix: pkg.PkgPrefix, 142 | ssaFunc: entrypointSSA, 143 | } 144 | } 145 | 146 | return profileTargets, nil 147 | } 148 | 149 | // Patch iterates the list of go source files that comprise this package and any folder 150 | // defined inside it and applies the patch function to AST entries matching the given 151 | // list of targets. 152 | // 153 | // This function will automatically overwrite any files that are modified by the 154 | // given patch function. 155 | func (pkg *GoPackage) Patch(vendorPkgRegex []string, patchCmds ...PatchCmd) (updatedFiles int, patchCount int, err error) { 156 | // Parse package sources 157 | parsedFiles, err := parsePackageSources(pkg.pathToPackage, vendorPkgRegex) 158 | if err != nil { 159 | return 0, 0, err 160 | } 161 | 162 | // Expand the callgraph of hook targets and generate a visitor for each patch cmd 163 | visitors := make([]*funcVisitor, len(patchCmds)) 164 | for cmdIndex, cmd := range patchCmds { 165 | visitors[cmdIndex] = newFuncVisitor(uniqueTargetMap(cmd.Targets), cmd.PatchFn) 166 | } 167 | 168 | totalPatchCount := 0 169 | visitorPatchCount := 0 170 | visitorModifiedAST := false 171 | for _, parsedFile := range parsedFiles { 172 | modifiedAST := false 173 | for _, visitor := range visitors { 174 | visitorModifiedAST, visitorPatchCount = visitor.Process(parsedFile) 175 | modifiedAST = visitorModifiedAST || modifiedAST 176 | totalPatchCount += visitorPatchCount 177 | } 178 | 179 | // If the file was updated write it back to disk 180 | if modifiedAST { 181 | f, err := os.Create(parsedFile.filePath) 182 | if err != nil { 183 | return 0, 0, err 184 | } 185 | printer.Fprint(f, parsedFile.fset, parsedFile.astFile) 186 | f.Close() 187 | updatedFiles++ 188 | } 189 | } 190 | 191 | return updatedFiles, totalPatchCount, err 192 | } 193 | 194 | // For each profile target, discover all reachable functions in its callgraph and 195 | // generate a map where keys are the FQ name of each callgraph node and values 196 | // are the callgraph nodes. 197 | func uniqueTargetMap(targets []ProfileTarget) map[string]*CallGraphNode { 198 | uniqueTargets := make(map[string]*CallGraphNode, 0) 199 | for _, target := range targets { 200 | cg := target.CallGraph() 201 | for _, cgNode := range cg { 202 | uniqueTargets[cgNode.Name] = cgNode 203 | } 204 | } 205 | 206 | return uniqueTargets 207 | } 208 | 209 | // Recursively scan pathToPackage and create an AST for any non-test go files 210 | // that are found. 211 | func parsePackageSources(pathToPackage string, vendorPkgRegex []string) ([]*parsedGoFile, error) { 212 | var err error 213 | pkgRegexes := make([]*regexp.Regexp, len(vendorPkgRegex)) 214 | for index, regex := range vendorPkgRegex { 215 | pkgRegexes[index], err = regexp.Compile(regex) 216 | if err != nil { 217 | return nil, fmt.Errorf("GoPackage.Patch: could not compile regex for profile-vendored-pkg arg %q: %s", regex, err) 218 | } 219 | } 220 | 221 | parsedFiles := make([]*parsedGoFile, 0) 222 | buildAST := func(path string, info os.FileInfo, err error) error { 223 | if err != nil { 224 | return err 225 | } 226 | 227 | // Skip dirs, non-go and go test files 228 | if info.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { 229 | return nil 230 | } 231 | 232 | // If this is a vendored dependency we should skip it unless it matches one of 233 | // the user-defined vendor package regexes 234 | if isVendoredDep(path) { 235 | keep := false 236 | for _, r := range pkgRegexes { 237 | if r.MatchString(path) { 238 | keep = true 239 | break 240 | } 241 | } 242 | if !keep { 243 | return nil 244 | } 245 | } 246 | 247 | fset := token.NewFileSet() 248 | f, err := parser.ParseFile(fset, path, nil, parser.ParseComments) 249 | if err != nil { 250 | return fmt.Errorf("in: could not parse %s; %v", path, err) 251 | } 252 | 253 | pkgName, err := qualifiedPkgName(path) 254 | if err != nil { 255 | return err 256 | } 257 | 258 | parsedFiles = append(parsedFiles, &parsedGoFile{ 259 | pkgName: pkgName, 260 | filePath: path, 261 | fset: fset, 262 | astFile: f, 263 | }) 264 | 265 | return nil 266 | } 267 | 268 | // Parse package files 269 | err = filepath.Walk(pathToPackage, buildAST) 270 | if err != nil { 271 | return nil, err 272 | } 273 | 274 | return parsedFiles, nil 275 | } 276 | 277 | // Check if the given path points to a vendored dependency. 278 | func isVendoredDep(path string) bool { 279 | return strings.Contains(path, "/Godeps/") || strings.Contains(path, "/vendor/") 280 | } 281 | 282 | // In order for go to correctly pick up any patched nested packages 283 | // instead of the original ones we need to override GOPATH so that 284 | // the workspace where the copied files reside is included first. 285 | func adjustGoPath(pathToPackage string) (string, error) { 286 | separator := ':' 287 | if runtime.GOOS == "windows" { 288 | separator = ';' 289 | } 290 | 291 | workspaceDir, err := packageWorkspace(pathToPackage) 292 | if err != nil { 293 | return "", err 294 | } 295 | 296 | return fmt.Sprintf( 297 | "%s%c%s", 298 | workspaceDir, 299 | separator, 300 | os.Getenv("GOPATH"), 301 | ), nil 302 | } 303 | -------------------------------------------------------------------------------- /profiler/profile.go: -------------------------------------------------------------------------------- 1 | package profiler 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // Profile wraps the processed metrics for a particular execution of a prism-hooked target. 11 | type Profile struct { 12 | ID uint64 `json:"-"` 13 | CreatedAt time.Time `json:"-"` 14 | 15 | Label string `json:"label"` 16 | Target *CallMetrics `json:"target"` 17 | } 18 | 19 | type metricsList []*CallMetrics 20 | 21 | func (p metricsList) Len() int { return len(p) } 22 | func (p metricsList) Less(i, j int) bool { return p[i].TotalTime < p[j].TotalTime } 23 | func (p metricsList) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 24 | 25 | // aggregate iterates all CallMetrics in the list and returns a composite CallMetric 26 | // representing the Group. When the list length is > 1 the returned metric 27 | // will also contain statistics for mean/median/p50/p75/p90/p99 and sttdev. 28 | func (p metricsList) aggregate() *CallMetrics { 29 | cm := &CallMetrics{ 30 | FnName: p[0].FnName, 31 | NestedCalls: make([]*CallMetrics, 0), 32 | 33 | MinTime: time.Duration(math.MaxInt64), 34 | MaxTime: time.Duration(math.MinInt64), 35 | Invocations: len(p), 36 | } 37 | 38 | // Pre-sort metrics so we can calculate the percentiles and the median 39 | if cm.Invocations > 0 { 40 | sort.Sort(p) 41 | 42 | // Calc min/max/total and pXX values 43 | callCountF := float64(len(p)) 44 | p50 := int(math.Ceil(callCountF*.5)) - 1 45 | p75 := int(math.Ceil(callCountF*.75)) - 1 46 | p90 := int(math.Ceil(callCountF*.90)) - 1 47 | p99 := int(math.Ceil(callCountF*.99)) - 1 48 | 49 | cm.MinTime = p[0].TotalTime 50 | cm.MaxTime = p[len(p)-1].TotalTime 51 | cm.P50Time = p[p50].TotalTime 52 | cm.P75Time = p[p75].TotalTime 53 | cm.P90Time = p[p90].TotalTime 54 | cm.P99Time = p[p99].TotalTime 55 | 56 | for _, metric := range p { 57 | cm.TotalTime += metric.TotalTime 58 | } 59 | } 60 | 61 | // Calc mean 62 | cm.MeanTime = cm.TotalTime / time.Duration(cm.Invocations) 63 | 64 | // Calc stddev = Sqrt( 1 / N * Sum_i( (total_i - mean)^2 ) ) 65 | for _, metric := range p { 66 | cm.StdDev += math.Pow(float64(metric.TotalTime-cm.MeanTime), 2.0) 67 | } 68 | cm.StdDev = math.Sqrt(cm.StdDev / float64(cm.Invocations)) 69 | 70 | // Calc median 71 | if cm.Invocations%2 == 0 { 72 | cm.MedianTime = (p[(cm.Invocations/2-1)].TotalTime + p[cm.Invocations/2].TotalTime) / 2 73 | } else { 74 | cm.MedianTime = p[cm.Invocations/2].TotalTime 75 | } 76 | 77 | return cm 78 | } 79 | 80 | // CallMetrics encapsulates all collected metrics about a function call that is 81 | // reachable by a profile target. 82 | type CallMetrics struct { 83 | FnName string `json:"fn"` 84 | 85 | // Total time spent in this call. 86 | TotalTime time.Duration `json:"total_time"` 87 | 88 | // Min and max time. 89 | MinTime time.Duration `json:"min_time"` 90 | MaxTime time.Duration `json:"max_time"` 91 | 92 | // Mean and median time. 93 | MeanTime time.Duration `json:"mean_time"` 94 | MedianTime time.Duration `json:"median_time"` 95 | 96 | // Percentiles. 97 | P50Time time.Duration `json:"p50_time"` 98 | P75Time time.Duration `json:"p75_time"` 99 | P90Time time.Duration `json:"p90_time"` 100 | P99Time time.Duration `json:"p99_time"` 101 | 102 | // Std of time valus. 103 | StdDev float64 `json:"std_dev"` 104 | 105 | // The number of times a scope was entered by the same parent function call. 106 | Invocations int `json:"invocations"` 107 | 108 | NestedCalls []*CallMetrics `json:"calls"` 109 | } 110 | 111 | type fnCall struct { 112 | fnName string 113 | 114 | // Time of entry/exit for this call. 115 | enteredAt time.Time 116 | exitedAt time.Time 117 | 118 | // The overhead introduced by the profiler hooks. 119 | profilerOverhead time.Duration 120 | 121 | // The ordered list of calls originating from this call's scope. 122 | nestedCalls []*fnCall 123 | 124 | // The call via which this call was reached. 125 | parent *fnCall 126 | 127 | // The call group index this call belongs to. This field is populated 128 | // by the aggregateMetrics() call. 129 | callGroupIndex int 130 | } 131 | 132 | var callPool = sync.Pool{ 133 | New: func() interface{} { 134 | return &fnCall{} 135 | }, 136 | } 137 | 138 | // Allocate and initialize a new fnCall. 139 | func makeFnCall(fnName string) *fnCall { 140 | call := callPool.Get().(*fnCall) 141 | call.fnName = fnName 142 | call.profilerOverhead = 0 143 | call.nestedCalls = make([]*fnCall, 0) 144 | call.parent = nil 145 | 146 | return call 147 | } 148 | 149 | // Perform a DFS on the fnCall tree releasing fnCall entries back to the call pool. 150 | func (fn *fnCall) free() { 151 | for _, child := range fn.nestedCalls { 152 | child.free() 153 | } 154 | 155 | fn.nestedCalls = nil 156 | callPool.Put(fn) 157 | } 158 | 159 | // Append a fnCall instance to the set of nested calls. 160 | func (fn *fnCall) nestCall(call *fnCall) { 161 | call.parent = fn 162 | fn.nestedCalls = append(fn.nestedCalls, call) 163 | } 164 | 165 | type callGroup struct { 166 | calls []*fnCall 167 | nestedGroups []*callGroup 168 | } 169 | 170 | // callGroups groups fnCall sibling nodes at a particular fnCall tree depth by 171 | // the composite key (fnCall.fnName, fnCall.parent.fnName) 172 | type callGroups struct { 173 | nameToGroupIndex map[string]int 174 | groups []*callGroup 175 | } 176 | 177 | // callGroupTree allows us to group together fnCall nodes belonging to similar 178 | // execution paths so we can properly extract metrics for function scopes with 179 | // multiple invocations. For more details see the docs on makeCallGroupTree() 180 | type callGroupTree struct { 181 | levels []*callGroups 182 | } 183 | 184 | // aggregateMetrics takes as input a fnCall tree and emits a tree of aggregated 185 | // CallMetrics. To illustrate how this works lets examine the following scenario: 186 | // 187 | // fnCall tree depth: 188 | // 0 1 2 189 | // -------- 190 | // 191 | // C1 192 | // B1--| 193 | // | D1 194 | // A--| 195 | // | C2 196 | // B2--| 197 | // E1 198 | // 199 | // In this captured profile, A calls B twice (e.g. using a for loop) and B 200 | // randomly calls 2 functions of the set [C, D, E]. Due to the way we capture 201 | // our profiling data, each node in the graph is only aware of its direct 202 | // children so the A->B2 branch is not aware of the A->B1 branch. In this scenario, 203 | // C is actually invoked twice via B so we need to group these two paths together 204 | // so we can calculate min/max/pXX stats for C. 205 | // 206 | // This function transforms the initial call tree using a 3-pass algorithm. The 207 | // first pass performs a DFS on the tree grouping together similar (= with same name) 208 | // sibling nodes at each level that are reached by a similar (= with same name) 209 | // parent. 210 | // 211 | // The second pass performs a DFS on the output of the first pass 212 | // nesting groups on level i under a i-1 level group when a i_th level group 213 | // node has a parent belonging to the i-1_th group. 214 | // 215 | // After running the 2nd pass of the algorithm, we end up with a tree that looks 216 | // like ([] denotes a list of grouped nodes): 217 | // 218 | // |- [C1, C2] 219 | // [A] -|- [B1, B2] -|- [D1] 220 | // |- [E1] 221 | // 222 | // The 3rd pass performs another DFS on the output of the second pass emitting 223 | // an aggregated CallMetric for each set of grouped nodes yielding the final 224 | // output (each item now being a CallMetric instance): 225 | // |- C 226 | // A -|- B -|- D 227 | // |- E 228 | func aggregateMetrics(rootFnCall *fnCall) *CallMetrics { 229 | cgt := &callGroupTree{ 230 | levels: make([]*callGroups, 0), 231 | } 232 | 233 | cgt.insert(0, rootFnCall) 234 | cgt.linkGroups() 235 | 236 | // Run a DFS and group metrics starting at the top-level group which 237 | // only contains the rootFnCall node 238 | return cgt.groupMetrics(cgt.levels[0].groups[0]) 239 | } 240 | 241 | // insert will recursively insert a call node and its children to the callGroup 242 | // tree. At each tree level, nodes will be grouped with their sibling nodes using 243 | // the composite key (fnCall.fnName, fnCall.parent.fnName) 244 | func (t *callGroupTree) insert(depth int, call *fnCall) { 245 | if len(t.levels) < depth+1 { 246 | t.levels = append(t.levels, &callGroups{ 247 | nameToGroupIndex: make(map[string]int, 0), 248 | groups: make([]*callGroup, 0), 249 | }) 250 | } 251 | 252 | level := t.levels[depth] 253 | 254 | // Construct composite grouping key 255 | var groupKey string 256 | if call.parent != nil { 257 | groupKey = call.parent.fnName + "," 258 | } 259 | groupKey += call.fnName 260 | 261 | groupIndex, exists := level.nameToGroupIndex[groupKey] 262 | if !exists { 263 | groupIndex = len(level.groups) 264 | level.groups = append(level.groups, &callGroup{ 265 | calls: make([]*fnCall, 0), 266 | nestedGroups: make([]*callGroup, 0), 267 | }) 268 | level.nameToGroupIndex[groupKey] = groupIndex 269 | } 270 | 271 | call.callGroupIndex = groupIndex 272 | level.groups[groupIndex].calls = append(level.groups[groupIndex].calls, call) 273 | 274 | // Recursively process nested calls 275 | for _, nestedCall := range call.nestedCalls { 276 | t.insert(depth+1, nestedCall) 277 | } 278 | } 279 | 280 | // linkGroups connects groups at level i with groups at level i+1 when a 281 | // direct path exists between the nodes in the two groups. 282 | func (t *callGroupTree) linkGroups() { 283 | numLevels := len(t.levels) 284 | for levelNum := numLevels - 2; levelNum >= 0; levelNum-- { 285 | for levelGroupIndex, levelGroup := range t.levels[levelNum].groups { 286 | for _, subLevelGroup := range t.levels[levelNum+1].groups { 287 | for _, call := range subLevelGroup.calls { 288 | // A direct path exists from a node in levelGroup to 289 | // a node in subLevelGroup; link them together 290 | if call.parent.callGroupIndex == levelGroupIndex { 291 | levelGroup.nestedGroups = append(levelGroup.nestedGroups, subLevelGroup) 292 | break 293 | } 294 | } 295 | } 296 | } 297 | } 298 | } 299 | 300 | // groupMetrics generates a CallMetrics instance summarizing the individual 301 | // CallMetrics for each fnCall instance in the given callGroup. 302 | func (t *callGroupTree) groupMetrics(cg *callGroup) *CallMetrics { 303 | groupCallMetrics := make(metricsList, len(cg.calls)) 304 | for callIndex, call := range cg.calls { 305 | // Our overhead calculation codes uses the mean fn call overhead 306 | // estimated by a calibration loop. This may cause the estimated total 307 | // time to become negative due to jitter so we need to ensure we track 308 | // at least 1ns of total time 309 | totalTime := call.exitedAt.Sub(call.enteredAt) - call.profilerOverhead 310 | if totalTime <= 0 { 311 | totalTime = 1 * time.Nanosecond 312 | } 313 | 314 | groupCallMetrics[callIndex] = &CallMetrics{ 315 | FnName: call.fnName, 316 | TotalTime: totalTime, 317 | } 318 | } 319 | cm := groupCallMetrics.aggregate() 320 | 321 | // Iterate nested groups and append one aggregated metric per group 322 | for _, nestedGroup := range cg.nestedGroups { 323 | groupMetric := t.groupMetrics(nestedGroup) 324 | cm.NestedCalls = append(cm.NestedCalls, groupMetric) 325 | } 326 | 327 | return cm 328 | } 329 | 330 | // genProfile post-processes the data captured by the profiler into a Profile 331 | // instance consisting of a tree structure of CallMetrics instances. 332 | func genProfile(ID uint64, label string, rootFnCall *fnCall) *Profile { 333 | return &Profile{ 334 | ID: ID, 335 | CreatedAt: rootFnCall.enteredAt, 336 | Label: label, 337 | Target: aggregateMetrics(rootFnCall), 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /vendor/gopkg.in/urfave/cli.v1/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | **ATTN**: This project uses [semantic versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | 7 | ## [1.18.1] - 2016-08-28 8 | ### Fixed 9 | - Removed deprecation warnings to STDERR to avoid them leaking to the end-user (backported) 10 | 11 | ## [1.18.0] - 2016-06-27 12 | ### Added 13 | - `./runtests` test runner with coverage tracking by default 14 | - testing on OS X 15 | - testing on Windows 16 | - `UintFlag`, `Uint64Flag`, and `Int64Flag` types and supporting code 17 | 18 | ### Changed 19 | - Use spaces for alignment in help/usage output instead of tabs, making the 20 | output alignment consistent regardless of tab width 21 | 22 | ### Fixed 23 | - Printing of command aliases in help text 24 | - Printing of visible flags for both struct and struct pointer flags 25 | - Display the `help` subcommand when using `CommandCategories` 26 | - No longer swallows `panic`s that occur within the `Action`s themselves when 27 | detecting the signature of the `Action` field 28 | 29 | ## [1.17.1] - 2016-08-28 30 | ### Fixed 31 | - Removed deprecation warnings to STDERR to avoid them leaking to the end-user 32 | 33 | ## [1.17.0] - 2016-05-09 34 | ### Added 35 | - Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc` 36 | - `context.GlobalBoolT` was added as an analogue to `context.GlobalBool` 37 | - Support for hiding commands by setting `Hidden: true` -- this will hide the 38 | commands in help output 39 | 40 | ### Changed 41 | - `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer 42 | quoted in help text output. 43 | - All flag types now include `(default: {value})` strings following usage when a 44 | default value can be (reasonably) detected. 45 | - `IntSliceFlag` and `StringSliceFlag` usage strings are now more consistent 46 | with non-slice flag types 47 | - Apps now exit with a code of 3 if an unknown subcommand is specified 48 | (previously they printed "No help topic for...", but still exited 0. This 49 | makes it easier to script around apps built using `cli` since they can trust 50 | that a 0 exit code indicated a successful execution. 51 | - cleanups based on [Go Report Card 52 | feedback](https://goreportcard.com/report/github.com/urfave/cli) 53 | 54 | ## [1.16.1] - 2016-08-28 55 | ### Fixed 56 | - Removed deprecation warnings to STDERR to avoid them leaking to the end-user 57 | 58 | ## [1.16.0] - 2016-05-02 59 | ### Added 60 | - `Hidden` field on all flag struct types to omit from generated help text 61 | 62 | ### Changed 63 | - `BashCompletionFlag` (`--enable-bash-completion`) is now omitted from 64 | generated help text via the `Hidden` field 65 | 66 | ### Fixed 67 | - handling of error values in `HandleAction` and `HandleExitCoder` 68 | 69 | ## [1.15.0] - 2016-04-30 70 | ### Added 71 | - This file! 72 | - Support for placeholders in flag usage strings 73 | - `App.Metadata` map for arbitrary data/state management 74 | - `Set` and `GlobalSet` methods on `*cli.Context` for altering values after 75 | parsing. 76 | - Support for nested lookup of dot-delimited keys in structures loaded from 77 | YAML. 78 | 79 | ### Changed 80 | - The `App.Action` and `Command.Action` now prefer a return signature of 81 | `func(*cli.Context) error`, as defined by `cli.ActionFunc`. If a non-nil 82 | `error` is returned, there may be two outcomes: 83 | - If the error fulfills `cli.ExitCoder`, then `os.Exit` will be called 84 | automatically 85 | - Else the error is bubbled up and returned from `App.Run` 86 | - Specifying an `Action` with the legacy return signature of 87 | `func(*cli.Context)` will produce a deprecation message to stderr 88 | - Specifying an `Action` that is not a `func` type will produce a non-zero exit 89 | from `App.Run` 90 | - Specifying an `Action` func that has an invalid (input) signature will 91 | produce a non-zero exit from `App.Run` 92 | 93 | ### Deprecated 94 | - 95 | `cli.App.RunAndExitOnError`, which should now be done by returning an error 96 | that fulfills `cli.ExitCoder` to `cli.App.Run`. 97 | - the legacy signature for 98 | `cli.App.Action` of `func(*cli.Context)`, which should now have a return 99 | signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. 100 | 101 | ### Fixed 102 | - Added missing `*cli.Context.GlobalFloat64` method 103 | 104 | ## [1.14.0] - 2016-04-03 (backfilled 2016-04-25) 105 | ### Added 106 | - Codebeat badge 107 | - Support for categorization via `CategorizedHelp` and `Categories` on app. 108 | 109 | ### Changed 110 | - Use `filepath.Base` instead of `path.Base` in `Name` and `HelpName`. 111 | 112 | ### Fixed 113 | - Ensure version is not shown in help text when `HideVersion` set. 114 | 115 | ## [1.13.0] - 2016-03-06 (backfilled 2016-04-25) 116 | ### Added 117 | - YAML file input support. 118 | - `NArg` method on context. 119 | 120 | ## [1.12.0] - 2016-02-17 (backfilled 2016-04-25) 121 | ### Added 122 | - Custom usage error handling. 123 | - Custom text support in `USAGE` section of help output. 124 | - Improved help messages for empty strings. 125 | - AppVeyor CI configuration. 126 | 127 | ### Changed 128 | - Removed `panic` from default help printer func. 129 | - De-duping and optimizations. 130 | 131 | ### Fixed 132 | - Correctly handle `Before`/`After` at command level when no subcommands. 133 | - Case of literal `-` argument causing flag reordering. 134 | - Environment variable hints on Windows. 135 | - Docs updates. 136 | 137 | ## [1.11.1] - 2015-12-21 (backfilled 2016-04-25) 138 | ### Changed 139 | - Use `path.Base` in `Name` and `HelpName` 140 | - Export `GetName` on flag types. 141 | 142 | ### Fixed 143 | - Flag parsing when skipping is enabled. 144 | - Test output cleanup. 145 | - Move completion check to account for empty input case. 146 | 147 | ## [1.11.0] - 2015-11-15 (backfilled 2016-04-25) 148 | ### Added 149 | - Destination scan support for flags. 150 | - Testing against `tip` in Travis CI config. 151 | 152 | ### Changed 153 | - Go version in Travis CI config. 154 | 155 | ### Fixed 156 | - Removed redundant tests. 157 | - Use correct example naming in tests. 158 | 159 | ## [1.10.2] - 2015-10-29 (backfilled 2016-04-25) 160 | ### Fixed 161 | - Remove unused var in bash completion. 162 | 163 | ## [1.10.1] - 2015-10-21 (backfilled 2016-04-25) 164 | ### Added 165 | - Coverage and reference logos in README. 166 | 167 | ### Fixed 168 | - Use specified values in help and version parsing. 169 | - Only display app version and help message once. 170 | 171 | ## [1.10.0] - 2015-10-06 (backfilled 2016-04-25) 172 | ### Added 173 | - More tests for existing functionality. 174 | - `ArgsUsage` at app and command level for help text flexibility. 175 | 176 | ### Fixed 177 | - Honor `HideHelp` and `HideVersion` in `App.Run`. 178 | - Remove juvenile word from README. 179 | 180 | ## [1.9.0] - 2015-09-08 (backfilled 2016-04-25) 181 | ### Added 182 | - `FullName` on command with accompanying help output update. 183 | - Set default `$PROG` in bash completion. 184 | 185 | ### Changed 186 | - Docs formatting. 187 | 188 | ### Fixed 189 | - Removed self-referential imports in tests. 190 | 191 | ## [1.8.0] - 2015-06-30 (backfilled 2016-04-25) 192 | ### Added 193 | - Support for `Copyright` at app level. 194 | - `Parent` func at context level to walk up context lineage. 195 | 196 | ### Fixed 197 | - Global flag processing at top level. 198 | 199 | ## [1.7.1] - 2015-06-11 (backfilled 2016-04-25) 200 | ### Added 201 | - Aggregate errors from `Before`/`After` funcs. 202 | - Doc comments on flag structs. 203 | - Include non-global flags when checking version and help. 204 | - Travis CI config updates. 205 | 206 | ### Fixed 207 | - Ensure slice type flags have non-nil values. 208 | - Collect global flags from the full command hierarchy. 209 | - Docs prose. 210 | 211 | ## [1.7.0] - 2015-05-03 (backfilled 2016-04-25) 212 | ### Changed 213 | - `HelpPrinter` signature includes output writer. 214 | 215 | ### Fixed 216 | - Specify go 1.1+ in docs. 217 | - Set `Writer` when running command as app. 218 | 219 | ## [1.6.0] - 2015-03-23 (backfilled 2016-04-25) 220 | ### Added 221 | - Multiple author support. 222 | - `NumFlags` at context level. 223 | - `Aliases` at command level. 224 | 225 | ### Deprecated 226 | - `ShortName` at command level. 227 | 228 | ### Fixed 229 | - Subcommand help output. 230 | - Backward compatible support for deprecated `Author` and `Email` fields. 231 | - Docs regarding `Names`/`Aliases`. 232 | 233 | ## [1.5.0] - 2015-02-20 (backfilled 2016-04-25) 234 | ### Added 235 | - `After` hook func support at app and command level. 236 | 237 | ### Fixed 238 | - Use parsed context when running command as subcommand. 239 | - Docs prose. 240 | 241 | ## [1.4.1] - 2015-01-09 (backfilled 2016-04-25) 242 | ### Added 243 | - Support for hiding `-h / --help` flags, but not `help` subcommand. 244 | - Stop flag parsing after `--`. 245 | 246 | ### Fixed 247 | - Help text for generic flags to specify single value. 248 | - Use double quotes in output for defaults. 249 | - Use `ParseInt` instead of `ParseUint` for int environment var values. 250 | - Use `0` as base when parsing int environment var values. 251 | 252 | ## [1.4.0] - 2014-12-12 (backfilled 2016-04-25) 253 | ### Added 254 | - Support for environment variable lookup "cascade". 255 | - Support for `Stdout` on app for output redirection. 256 | 257 | ### Fixed 258 | - Print command help instead of app help in `ShowCommandHelp`. 259 | 260 | ## [1.3.1] - 2014-11-13 (backfilled 2016-04-25) 261 | ### Added 262 | - Docs and example code updates. 263 | 264 | ### Changed 265 | - Default `-v / --version` flag made optional. 266 | 267 | ## [1.3.0] - 2014-08-10 (backfilled 2016-04-25) 268 | ### Added 269 | - `FlagNames` at context level. 270 | - Exposed `VersionPrinter` var for more control over version output. 271 | - Zsh completion hook. 272 | - `AUTHOR` section in default app help template. 273 | - Contribution guidelines. 274 | - `DurationFlag` type. 275 | 276 | ## [1.2.0] - 2014-08-02 277 | ### Added 278 | - Support for environment variable defaults on flags plus tests. 279 | 280 | ## [1.1.0] - 2014-07-15 281 | ### Added 282 | - Bash completion. 283 | - Optional hiding of built-in help command. 284 | - Optional skipping of flag parsing at command level. 285 | - `Author`, `Email`, and `Compiled` metadata on app. 286 | - `Before` hook func support at app and command level. 287 | - `CommandNotFound` func support at app level. 288 | - Command reference available on context. 289 | - `GenericFlag` type. 290 | - `Float64Flag` type. 291 | - `BoolTFlag` type. 292 | - `IsSet` flag helper on context. 293 | - More flag lookup funcs at context level. 294 | - More tests & docs. 295 | 296 | ### Changed 297 | - Help template updates to account for presence/absence of flags. 298 | - Separated subcommand help template. 299 | - Exposed `HelpPrinter` var for more control over help output. 300 | 301 | ## [1.0.0] - 2013-11-01 302 | ### Added 303 | - `help` flag in default app flag set and each command flag set. 304 | - Custom handling of argument parsing errors. 305 | - Command lookup by name at app level. 306 | - `StringSliceFlag` type and supporting `StringSlice` type. 307 | - `IntSliceFlag` type and supporting `IntSlice` type. 308 | - Slice type flag lookups by name at context level. 309 | - Export of app and command help functions. 310 | - More tests & docs. 311 | 312 | ## 0.1.0 - 2013-07-22 313 | ### Added 314 | - Initial implementation. 315 | 316 | [Unreleased]: https://github.com/urfave/cli/compare/v1.18.0...HEAD 317 | [1.18.0]: https://github.com/urfave/cli/compare/v1.17.0...v1.18.0 318 | [1.17.0]: https://github.com/urfave/cli/compare/v1.16.0...v1.17.0 319 | [1.16.0]: https://github.com/urfave/cli/compare/v1.15.0...v1.16.0 320 | [1.15.0]: https://github.com/urfave/cli/compare/v1.14.0...v1.15.0 321 | [1.14.0]: https://github.com/urfave/cli/compare/v1.13.0...v1.14.0 322 | [1.13.0]: https://github.com/urfave/cli/compare/v1.12.0...v1.13.0 323 | [1.12.0]: https://github.com/urfave/cli/compare/v1.11.1...v1.12.0 324 | [1.11.1]: https://github.com/urfave/cli/compare/v1.11.0...v1.11.1 325 | [1.11.0]: https://github.com/urfave/cli/compare/v1.10.2...v1.11.0 326 | [1.10.2]: https://github.com/urfave/cli/compare/v1.10.1...v1.10.2 327 | [1.10.1]: https://github.com/urfave/cli/compare/v1.10.0...v1.10.1 328 | [1.10.0]: https://github.com/urfave/cli/compare/v1.9.0...v1.10.0 329 | [1.9.0]: https://github.com/urfave/cli/compare/v1.8.0...v1.9.0 330 | [1.8.0]: https://github.com/urfave/cli/compare/v1.7.1...v1.8.0 331 | [1.7.1]: https://github.com/urfave/cli/compare/v1.7.0...v1.7.1 332 | [1.7.0]: https://github.com/urfave/cli/compare/v1.6.0...v1.7.0 333 | [1.6.0]: https://github.com/urfave/cli/compare/v1.5.0...v1.6.0 334 | [1.5.0]: https://github.com/urfave/cli/compare/v1.4.1...v1.5.0 335 | [1.4.1]: https://github.com/urfave/cli/compare/v1.4.0...v1.4.1 336 | [1.4.0]: https://github.com/urfave/cli/compare/v1.3.1...v1.4.0 337 | [1.3.1]: https://github.com/urfave/cli/compare/v1.3.0...v1.3.1 338 | [1.3.0]: https://github.com/urfave/cli/compare/v1.2.0...v1.3.0 339 | [1.2.0]: https://github.com/urfave/cli/compare/v1.1.0...v1.2.0 340 | [1.1.0]: https://github.com/urfave/cli/compare/v1.0.0...v1.1.0 341 | [1.0.0]: https://github.com/urfave/cli/compare/v0.1.0...v1.0.0 342 | --------------------------------------------------------------------------------