diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index a1cb4fe..b7562f9 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,37 +1,14 @@ { "ImportPath": "github.com/uber/go-torch", - "GoVersion": "go1.4.1", + "GoVersion": "go1.5", "Packages": [ "./..." ], "Deps": [ { - "ImportPath": "github.com/Sirupsen/logrus", - "Comment": "v0.8.2-2-g6ba91e2", - "Rev": "6ba91e24c498b49d0363c723e9e2ab2b5b8fd012" - }, - { - "ImportPath": "github.com/awalterschulze/gographviz", - "Rev": "7c3cf72121515513fad9d6c9a090db2aa4f47143" - }, - { - "ImportPath": "github.com/codegangsta/cli", - "Comment": "1.2.0-87-g8ce64f1", - "Rev": "8ce64f19ff08029a69d11b7615c9b591245450ad" - }, - { - "ImportPath": "github.com/stretchr/objx", - "Rev": "cbeaeb16a013161a98496fad62933b1d21786672" - }, - { - "ImportPath": "github.com/stretchr/testify/assert", - "Comment": "v1.0-17-g089c718", - "Rev": "089c7181b8c728499929ff09b62d3fdd8df8adff" - }, - { - "ImportPath": "github.com/stretchr/testify/mock", - "Comment": "v1.0-17-g089c718", - "Rev": "089c7181b8c728499929ff09b62d3fdd8df8adff" + "ImportPath": "github.com/jessevdk/go-flags", + "Comment": "v1-297-g1b89bf7", + "Rev": "1b89bf73cd2c3a911d7b2a279ab085c4a18cf539" } ] } diff --git a/graph/graph.go b/graph/graph.go deleted file mode 100644 index e8b8caf..0000000 --- a/graph/graph.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2015 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// Package graph transforms a DOT graph text file into the representation -// expected by the visualization package. -// -// The graph is a directed acyclic graph where nodes represent functions and -// directed edges represent how many times a function calls another. -package graph - -import ( - "bytes" - "errors" - "fmt" - "strconv" - "strings" - - log "github.com/Sirupsen/logrus" - ggv "github.com/awalterschulze/gographviz" - "github.com/awalterschulze/gographviz/parser" -) - -var errNoActivity = errors.New("Your application is not doing anything right now. Please try again.") - -// Grapher handles transforming a DOT graph byte array into the -// representation expected by the visualization package. -type Grapher interface { - GraphAsText([]byte) (string, error) -} - -type defaultGrapher struct { - searcher - collectionGetter -} - -type searchArgs struct { - root string - path []ggv.Edge - nodeToOutEdges map[string][]*ggv.Edge - nameToNodes map[string]*ggv.Node - buffer *bytes.Buffer - statusMap map[string]discoveryStatus -} - -type searcher interface { - dfs(args searchArgs) -} - -type defaultSearcher struct { - pathStringer -} - -type collectionGetter interface { - generateNodeToOutEdges(*ggv.Graph) map[string][]*ggv.Edge - getInDegreeZeroNodes(*ggv.Graph) []string -} - -type defaultCollectionGetter struct{} - -type pathStringer interface { - pathAsString([]ggv.Edge, map[string]*ggv.Node) string -} - -type defaultPathStringer struct{} - -// Marking nodes during depth-first search is a standard way of detecting cycles. -// A node is undiscovered before it has been discovered, onstack when it is on the recursion stack, -// and discovered when all of its neighbors have been traversed. A edge terminating at a onstack -// node implies a back edge, which also implies a cycle -// (see: https://en.wikipedia.org/wiki/Cycle_(graph_theory)#Cycle_detection). -type discoveryStatus int - -const ( - undiscovered discoveryStatus = iota - onstack - discovered -) - -// NewGrapher returns a default grapher struct with default attributes -func NewGrapher() Grapher { - return &defaultGrapher{ - searcher: newSearcher(), - collectionGetter: new(defaultCollectionGetter), - } -} - -// newSearcher returns a default searcher struct with a default pathStringer -func newSearcher() *defaultSearcher { - return &defaultSearcher{ - pathStringer: new(defaultPathStringer), - } -} - -// GraphAsText is the standard implementation of Grapher -func (g *defaultGrapher) GraphAsText(dotText []byte) (string, error) { - graphAst, err := parser.ParseBytes(dotText) - if err != nil { - return "", err - } - dag := ggv.NewGraph() // A directed acyclic graph - ggv.Analyse(graphAst, dag) - - if len(dag.Edges.Edges) == 0 { - return "", errNoActivity - } - nodeToOutEdges := g.collectionGetter.generateNodeToOutEdges(dag) - inDegreeZeroNodes := g.collectionGetter.getInDegreeZeroNodes(dag) - nameToNodes := dag.Nodes.Lookup - - buffer := new(bytes.Buffer) - statusMap := make(map[string]discoveryStatus) - - for _, root := range inDegreeZeroNodes { - g.searcher.dfs(searchArgs{ - root: root, - path: nil, - nodeToOutEdges: nodeToOutEdges, - nameToNodes: nameToNodes, - buffer: buffer, - statusMap: statusMap, - }) - } - - return buffer.String(), nil -} - -// generateNodeToOutEdges takes a graph and generates a mapping of nodes to -// edges originating from nodes. -func (c *defaultCollectionGetter) generateNodeToOutEdges(dag *ggv.Graph) map[string][]*ggv.Edge { - nodeToOutEdges := make(map[string][]*ggv.Edge) - for _, edge := range dag.Edges.Edges { - nodeToOutEdges[edge.Src] = append(nodeToOutEdges[edge.Src], edge) - } - return nodeToOutEdges -} - -// getInDegreeZeroNodes takes a graph and returns a list of nodes with -// in-degree of 0. In other words, no edges terminate at these nodes. -func (c *defaultCollectionGetter) getInDegreeZeroNodes(dag *ggv.Graph) []string { - var inDegreeZeroNodes []string - nodeToInDegree := make(map[string]int) - for _, edge := range dag.Edges.Edges { - dst := edge.Dst - nodeToInDegree[dst]++ - } - for _, node := range dag.Nodes.Nodes { - // @HACK This is a hack to fix a bug with gographviz where a cluster - // 'L' is being parsed as a node. This just checks that all node names - // begin with N. - correctPrefix := strings.HasPrefix(node.Name, "N") - if correctPrefix && nodeToInDegree[node.Name] == 0 { - inDegreeZeroNodes = append(inDegreeZeroNodes, node.Name) - } - } - return inDegreeZeroNodes -} - -// dfs performs a depth-first search traversal of the graph starting from a -// given root node. When a node with no outgoing edges is reached, the path -// taken to that node is written to a buffer. -func (s *defaultSearcher) dfs(args searchArgs) { - outEdges := args.nodeToOutEdges[args.root] - if args.statusMap[args.root] == onstack { - log.Warn("The input call graph contains a cycle. This can't be represented in a " + - "flame graph, so this path will be ignored. For your record, the ignored path " + - "is:\n" + strings.TrimSpace(s.pathStringer.pathAsString(args.path, args.nameToNodes))) - return - } - if len(outEdges) == 0 { - args.buffer.WriteString(s.pathStringer.pathAsString(args.path, args.nameToNodes)) - args.statusMap[args.root] = discovered - return - } - args.statusMap[args.root] = onstack - for _, edge := range outEdges { - s.dfs(searchArgs{ - root: edge.Dst, - path: append(args.path, *edge), - nodeToOutEdges: args.nodeToOutEdges, - nameToNodes: args.nameToNodes, - buffer: args.buffer, - statusMap: args.statusMap, - }) - } - args.statusMap[args.root] = discovered -} - -// pathAsString takes a path and a mapping of node names to node structs and -// generates the string representation of the path expected by the -// visualization package. -func (p *defaultPathStringer) pathAsString(path []ggv.Edge, nameToNodes map[string]*ggv.Node) string { - var ( - pathBuffer bytes.Buffer - weightSum int - ) - for _, edge := range path { - // If the function call represented by the edge happened very rarely, - // the edge's weight will not be recorded. The edge's label will always - // be recorded. - if weightStr, ok := edge.Attrs["weight"]; ok { - weight, err := strconv.Atoi(weightStr) - if err != nil { // This should never happen - log.Panic(err) - } - weightSum += weight - } - functionLabel := getFormattedFunctionLabel(nameToNodes[edge.Src]) - pathBuffer.WriteString(functionLabel + ";") - } - if len(path) >= 1 { - lastEdge := path[len(path)-1] - lastFunctionLabel := getFormattedFunctionLabel(nameToNodes[lastEdge.Dst]) - pathBuffer.WriteString(lastFunctionLabel + " ") - } - pathBuffer.WriteString(fmt.Sprint(weightSum)) - pathBuffer.WriteString("\n") - - return pathBuffer.String() -} - -// getFormattedFunctionLabel takes a node and returns a formatted function -// label. -func getFormattedFunctionLabel(node *ggv.Node) string { - label := node.Attrs["tooltip"] - label = strings.Replace(label, `\n`, " ", -1) - label = strings.Replace(label, `"`, "", -1) - return label -} diff --git a/graph/graph_test.go b/graph/graph_test.go deleted file mode 100644 index 233ddde..0000000 --- a/graph/graph_test.go +++ /dev/null @@ -1,550 +0,0 @@ -// Copyright (c) 2015 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package graph - -import ( - "bytes" - "testing" - - ggv "github.com/awalterschulze/gographviz" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestPathAsString(t *testing.T) { - g := testGraphWithTooltipAndWeight() - - eMap := g.Edges.SrcToDsts - path := []ggv.Edge{*eMap["N1"]["N2"], *eMap["N2"]["N3"], *eMap["N3"]["N4"]} - - pathString := new(defaultPathStringer).pathAsString(path, g.Nodes.Lookup) - - assert.Equal(t, "function1;function2;function3;function4 9\n", pathString) -} - -func TestPathAsStringWithEmptyPath(t *testing.T) { - path := []ggv.Edge{} - - pathString := new(defaultPathStringer).pathAsString(path, map[string]*ggv.Node{}) - assert.Equal(t, "0\n", pathString) -} - -func TestPathAsStringWithNoWeightEdges(t *testing.T) { - g := testGraphWithTooltip() - - eMap := g.Edges.SrcToDsts - path := []ggv.Edge{*eMap["N1"]["N2"], *eMap["N2"]["N3"], *eMap["N3"]["N4"]} - - pathString := new(defaultPathStringer).pathAsString(path, g.Nodes.Lookup) - - assert.Equal(t, "function1;function2;function3;function4 0\n", pathString) -} - -func TestDFS(t *testing.T) { - g := testSingleRootGraph() - eMap := g.Edges.SrcToDsts - - nodeToOutEdges := map[string][]*ggv.Edge{ - "N1": {eMap["N1"]["N2"], eMap["N1"]["N3"], eMap["N1"]["N4"]}, - "N2": {eMap["N2"]["N3"]}, - "N4": {eMap["N4"]["N3"]}, - } - - buffer := new(bytes.Buffer) - mockPathStringer := new(mockPathStringer) - anythingType := mock.AnythingOfType("map[string]*gographviz.Node") - pathOne := []ggv.Edge{*eMap["N1"]["N2"], *eMap["N2"]["N3"]} - pathTwo := []ggv.Edge{*eMap["N1"]["N3"]} - pathThree := []ggv.Edge{*eMap["N1"]["N4"], *eMap["N4"]["N3"]} - - mockPathStringer.On("pathAsString", pathOne, anythingType).Return("N1;N2;N3 3\n").Once() - mockPathStringer.On("pathAsString", pathTwo, anythingType).Return("N1;N3 2\n").Once() - mockPathStringer.On("pathAsString", pathThree, anythingType).Return("N1;N4;N3 8\n").Once() - - searcherWithTestStringer := &defaultSearcher{ - pathStringer: mockPathStringer, - } - searcherWithTestStringer.dfs(searchArgs{ - root: "N1", - path: []ggv.Edge{}, - nodeToOutEdges: nodeToOutEdges, - nameToNodes: g.Nodes.Lookup, - buffer: buffer, - statusMap: make(map[string]discoveryStatus), - }) - - correctOutput := "N1;N2;N3 3\nN1;N3 2\nN1;N4;N3 8\n" - actualOutput := buffer.String() - - assert.Equal(t, correctOutput, actualOutput) - mockPathStringer.AssertExpectations(t) -} - -func TestDFSAlmostEmptyGraph(t *testing.T) { - g := ggv.NewGraph() - g.SetName("G") - g.AddNode("G", "N1", nil) - g.SetDir(true) - - nodeToOutEdges := map[string][]*ggv.Edge{} - buffer := new(bytes.Buffer) - - mockPathStringer := new(mockPathStringer) - anythingType := mock.AnythingOfType("map[string]*gographviz.Node") - - mockPathStringer.On("pathAsString", []ggv.Edge{}, anythingType).Return("").Once() - - searcherWithTestStringer := &defaultSearcher{ - pathStringer: mockPathStringer, - } - searcherWithTestStringer.dfs(searchArgs{ - root: "N1", - path: []ggv.Edge{}, - nodeToOutEdges: nodeToOutEdges, - nameToNodes: g.Nodes.Lookup, - buffer: buffer, - statusMap: make(map[string]discoveryStatus), - }) - - correctOutput := "" - actualOutput := buffer.String() - - assert.Equal(t, correctOutput, actualOutput) - mockPathStringer.AssertExpectations(t) -} - -func TestDFSMultipleRootsLeaves(t *testing.T) { - g := testMultiRootGraph() - - eMap := g.Edges.SrcToDsts - - nodeToOutEdges := map[string][]*ggv.Edge{ - "N1": {eMap["N1"]["N2"], eMap["N1"]["N3"]}, - "N4": {eMap["N4"]["N5"], eMap["N4"]["N6"]}, - "N6": {eMap["N6"]["N5"]}, - } - - buffer := new(bytes.Buffer) - mockPathStringer := new(mockPathStringer) - anythingType := mock.AnythingOfType("map[string]*gographviz.Node") - pathOne := []ggv.Edge{*eMap["N1"]["N2"]} - pathTwo := []ggv.Edge{*eMap["N1"]["N3"]} - pathThree := []ggv.Edge{*eMap["N4"]["N5"]} - pathFour := []ggv.Edge{*eMap["N4"]["N6"], *eMap["N6"]["N5"]} - - mockPathStringer.On("pathAsString", pathOne, anythingType).Return("N1;N2 3\n").Once() - mockPathStringer.On("pathAsString", pathTwo, anythingType).Return("N1;N3 2\n").Once() - mockPathStringer.On("pathAsString", pathThree, anythingType).Return("N4;N5 8\n").Once() - mockPathStringer.On("pathAsString", pathFour, anythingType).Return("N4;N6;N5 7\n").Once() - - searcherWithTestStringer := &defaultSearcher{ - pathStringer: mockPathStringer, - } - - searcherWithTestStringer.dfs(searchArgs{ - root: "N1", - path: []ggv.Edge{}, - nodeToOutEdges: nodeToOutEdges, - nameToNodes: g.Nodes.Lookup, - buffer: buffer, - statusMap: make(map[string]discoveryStatus), - }) - searcherWithTestStringer.dfs(searchArgs{ - root: "N4", - path: []ggv.Edge{}, - nodeToOutEdges: nodeToOutEdges, - nameToNodes: g.Nodes.Lookup, - buffer: buffer, - statusMap: make(map[string]discoveryStatus), - }) - - correctOutput := "N1;N2 3\nN1;N3 2\nN4;N5 8\nN4;N6;N5 7\n" - actualOutput := buffer.String() - - assert.Equal(t, correctOutput, actualOutput) - mockPathStringer.AssertExpectations(t) -} - -func TestDFSCyclicGraph(t *testing.T) { - g := testGraphWithCycles() - eMap := g.Edges.SrcToDsts - - nodeToOutEdges := map[string][]*ggv.Edge{ - "N1": {eMap["N1"]["N2"], eMap["N1"]["N4"]}, - "N2": {eMap["N2"]["N3"], eMap["N2"]["N4"]}, - "N3": {eMap["N3"]["N4"], eMap["N3"]["N5"]}, - "N5": {eMap["N5"]["N2"]}, - } - - buffer := new(bytes.Buffer) - mockPathStringer := new(mockPathStringer) - anythingType := mock.AnythingOfType("map[string]*gographviz.Node") - pathOne := []ggv.Edge{*eMap["N1"]["N2"], *eMap["N2"]["N3"], *eMap["N3"]["N4"]} - pathTwo := []ggv.Edge{*eMap["N1"]["N4"]} - pathThree := []ggv.Edge{*eMap["N1"]["N2"], *eMap["N2"]["N4"]} - - cycleOne := []ggv.Edge{*eMap["N1"]["N2"], *eMap["N2"]["N3"], *eMap["N3"]["N5"], - *eMap["N5"]["N2"]} - - mockPathStringer.On("pathAsString", pathOne, anythingType).Return("N1;N2;N3;N4 4\n").Once() - mockPathStringer.On("pathAsString", pathTwo, anythingType).Return("N1;N4 2\n").Once() - mockPathStringer.On("pathAsString", pathThree, anythingType).Return("N2;N4 2\n").Once() - - mockPathStringer.On("pathAsString", cycleOne, anythingType).Return("should not include\n").Once() - - searcherWithTestStringer := &defaultSearcher{ - pathStringer: mockPathStringer, - } - searcherWithTestStringer.dfs(searchArgs{ - root: "N1", - path: []ggv.Edge{}, - nodeToOutEdges: nodeToOutEdges, - nameToNodes: g.Nodes.Lookup, - buffer: buffer, - statusMap: make(map[string]discoveryStatus), - }) - - correctOutput := "N1;N2;N3;N4 4\nN2;N4 2\nN1;N4 2\n" - actualOutput := buffer.String() - - assert.Equal(t, correctOutput, actualOutput) - mockPathStringer.AssertExpectations(t) -} - -func TestGetInDegreeZeroNodes(t *testing.T) { - g := testMultiRootGraph() - - correctInDegreeZeroNodes := []string{"N1", "N4"} - actualInDegreeZeroNodes := new(defaultCollectionGetter).getInDegreeZeroNodes(g) - assert.Equal(t, correctInDegreeZeroNodes, actualInDegreeZeroNodes) -} - -func TestGetInDegreeZeroNodesEmptyGraph(t *testing.T) { - g := ggv.NewGraph() - g.SetName("G") - g.SetDir(true) - - var correctInDegreeZeroNodes []string - actualInDegreeZeroNodes := new(defaultCollectionGetter).getInDegreeZeroNodes(g) - assert.Equal(t, correctInDegreeZeroNodes, actualInDegreeZeroNodes) -} - -func TestGetInDegreeZeroNodesIgnoreClusterNodes(t *testing.T) { - g := testGraphWithClusterNodes() - - correctInDegreeZeroNodes := []string{"N1"} - actualInDegreeZeroNodes := new(defaultCollectionGetter).getInDegreeZeroNodes(g) - assert.Equal(t, correctInDegreeZeroNodes, actualInDegreeZeroNodes) -} - -func TestGenerateNodeToOutEdges(t *testing.T) { - g := testMultiRootGraph() - - eMap := g.Edges.SrcToDsts - - correctNodeToOutEdges := map[string][]*ggv.Edge{ - "N1": {eMap["N1"]["N2"], eMap["N1"]["N3"]}, - "N4": {eMap["N4"]["N5"], eMap["N4"]["N6"]}, - "N6": {eMap["N6"]["N5"]}, - } - actualNodeToOutEdges := new(defaultCollectionGetter).generateNodeToOutEdges(g) - assert.Equal(t, correctNodeToOutEdges, actualNodeToOutEdges) -} - -func TestGenerateNodeToOutEdgesEmptyGraph(t *testing.T) { - g := ggv.NewGraph() - g.SetName("G") - g.SetDir(true) - - correctNodeToOutEdges := make(map[string][]*ggv.Edge) - actualNodeToOutEdges := new(defaultCollectionGetter).generateNodeToOutEdges(g) - assert.Equal(t, correctNodeToOutEdges, actualNodeToOutEdges) -} - -func TestGraphAsText(t *testing.T) { - mockSearcher := new(mockSearcher) - mockCollectionGetter := new(mockCollectionGetter) - grapher := &defaultGrapher{ - searcher: mockSearcher, - collectionGetter: mockCollectionGetter, - } - - graphAsTextInput := []byte(`digraph "unnamed" { - node [style=filled fillcolor="#f8f8f8"] - N1 [tooltip="N1"] - N2 [tooltip="N2"] - N3 [tooltip="N3"] - N4 [tooltip="N4"] - N5 [tooltip="N5"] - N6 [tooltip="N6"] - N1 -> N2 [weight=1] - N1 -> N3 [weight=2] - N4 -> N5 [weight=1] - N4 -> N6 [weight=4] - N6 -> N5 [weight=4] - }`) - - fakeWriteToBuffer := func(args mock.Arguments) { - searchArgs := args.Get(0).(searchArgs) - if searchArgs.root == "N1" { - searchArgs.buffer.WriteString("N1;N2 1\nN1;N3 2\n") - } else { - searchArgs.buffer.WriteString("N4;N5 1\nN4;N6;N5 8\n") - } - } - - mockSearcher.On("dfs", mock.AnythingOfType("searchArgs")).Return().Run(fakeWriteToBuffer).Twice() - mockCollectionGetter.On("generateNodeToOutEdges", - mock.AnythingOfType("*gographviz.Graph")).Return(nil).Once() // We can return nil since the mock dfs will ignore this - mockCollectionGetter.On("getInDegreeZeroNodes", - mock.AnythingOfType("*gographviz.Graph")).Return([]string{"N1", "N4"}).Once() - - correctGraphAsText := "N1;N2 1\nN1;N3 2\nN4;N5 1\nN4;N6;N5 8\n" - - actualGraphAsText, err := grapher.GraphAsText(graphAsTextInput) - assert.NoError(t, err) - assert.Equal(t, correctGraphAsText, actualGraphAsText) - mockSearcher.AssertExpectations(t) -} - -func TestNewGrapher(t *testing.T) { - assert.NotNil(t, NewGrapher()) -} - -func TestNewSearcher(t *testing.T) { - assert.NotNil(t, newSearcher()) -} - -// The returned graph, represented in ascii: -// +----+ +----+ -// | N2 | <-- | N1 | -// +----+ +----+ -// | -// | -// v -// +----+ -// | N3 | -// +----+ -// +----+ -// | N4 | -+ -// +----+ | -// | | -// | | -// v | -// +----+ | -// | N6 | | -// +----+ | -// | | -// | | -// v | -// +----+ | -// | N5 | <+ -// +----+ -func testMultiRootGraph() *ggv.Graph { - g := ggv.NewGraph() - g.SetName("G") - g.SetDir(true) - g.AddNode("G", "N1", nil) - g.AddNode("G", "N2", nil) - g.AddNode("G", "N3", nil) - g.AddNode("G", "N4", nil) - g.AddNode("G", "N5", nil) - g.AddNode("G", "N6", nil) - g.AddEdge("N1", "N2", true, nil) - g.AddEdge("N1", "N3", true, nil) - g.AddEdge("N4", "N5", true, nil) - g.AddEdge("N4", "N6", true, nil) - g.AddEdge("N6", "N5", true, nil) - return g -} - -// The returned graph, represented in ascii: -// +----+ +----+ -// +> | N4 | <-- | N1 | -// | +----+ +----+ -// | ^ | -// | | | -// | | v -// | | +----+ -// | +------- | N2 | <+ -// | +----+ | -// | | | -// | | | -// | v | -// | +----+ | -// +------------ | N3 | | -// +----+ | -// | | -// | | -// v | -// +----+ | -// | N5 | -+ -// +----+ -func testGraphWithCycles() *ggv.Graph { - g := ggv.NewGraph() - g.SetName("G") - g.SetDir(true) - g.AddNode("G", "N1", nil) - g.AddNode("G", "N2", nil) - g.AddNode("G", "N3", nil) - g.AddNode("G", "N4", nil) - g.AddNode("G", "N5", nil) - g.AddEdge("N1", "N2", true, nil) - g.AddEdge("N1", "N4", true, nil) - g.AddEdge("N2", "N3", true, nil) - g.AddEdge("N2", "N4", true, nil) - g.AddEdge("N3", "N4", true, nil) - g.AddEdge("N3", "N5", true, nil) - g.AddEdge("N5", "N2", true, nil) - return g -} - -// The returned graph, represented in ascii: -// +----+ -// | N1 | -+ -// +----+ | -// | | -// | | -// v | -// +----+ | -// | N2 | | -// +----+ | -// | | -// | | -// v | -// +----+ | -// | N3 | | -// +----+ | -// | | -// | | -// v | -// +----+ | -// | N4 | <+ -// +----+ -func testGraphWithTooltipAndWeight() *ggv.Graph { - g := ggv.NewGraph() - g.SetName("G") - g.SetDir(true) - g.AddNode("G", "N1", map[string]string{"tooltip": "function1"}) - g.AddNode("G", "N2", map[string]string{"tooltip": "function2"}) - g.AddNode("G", "N3", map[string]string{"tooltip": "function3"}) - g.AddNode("G", "N4", map[string]string{"tooltip": "function4"}) - g.AddEdge("N1", "N2", true, map[string]string{"weight": "5"}) - g.AddEdge("N2", "N3", true, map[string]string{"weight": "2"}) - g.AddEdge("N3", "N4", true, map[string]string{"weight": "2"}) - g.AddEdge("N1", "N4", true, map[string]string{"weight": "1"}) - return g -} - -// The returned graph, represented in ascii: -// +----+ -// | N1 | -+ -// +----+ | -// | | -// | | -// v | -// +----+ | -// | N2 | | -// +----+ | -// | | -// | | -// v | -// +----+ | -// | N3 | | -// +----+ | -// | | -// | | -// v | -// +----+ | -// | N4 | <+ -// +----+ -func testGraphWithTooltip() *ggv.Graph { - g := ggv.NewGraph() - g.SetName("G") - g.SetDir(true) - g.AddNode("G", "N1", map[string]string{"tooltip": "function1"}) - g.AddNode("G", "N2", map[string]string{"tooltip": "function2"}) - g.AddNode("G", "N3", map[string]string{"tooltip": "function3"}) - g.AddNode("G", "N4", map[string]string{"tooltip": "function4"}) - g.AddEdge("N1", "N2", true, nil) - g.AddEdge("N2", "N3", true, nil) - g.AddEdge("N3", "N4", true, nil) - g.AddEdge("N1", "N4", true, nil) - return g -} - -// The returned graph, represented in ascii: -// +----+ +----+ -// | N4 | <-- | N1 | -+ -// +----+ +----+ | -// | | | -// | | | -// | v | -// | +----+ | -// | | N2 | | -// | +----+ | -// | | | -// | | | -// | v | -// | +----+ | -// +------> | N3 | <+ -// +----+ -func testSingleRootGraph() *ggv.Graph { - g := ggv.NewGraph() - g.SetName("G") - g.SetDir(true) - g.AddNode("G", "N1", nil) - g.AddNode("G", "N2", nil) - g.AddNode("G", "N3", nil) - g.AddNode("G", "N4", nil) - g.AddEdge("N1", "N2", true, nil) - g.AddEdge("N2", "N3", true, nil) - g.AddEdge("N4", "N3", true, nil) - g.AddEdge("N1", "N4", true, nil) - g.AddEdge("N1", "N3", true, nil) - return g -} - -// The returned graph, represented in ascii: -// +----------+ -// | Ignoreme | -// +----------+ -// +----+ +----------+ -// | N2 | <-- | N1 | -// +----+ +----------+ -// | -// | -// v -// +----------+ -// | N3 | -// +----------+ -func testGraphWithClusterNodes() *ggv.Graph { - g := ggv.NewGraph() - g.SetName("G") - g.SetDir(true) - g.AddNode("G", "N1", nil) - g.AddNode("G", "N2", nil) - g.AddNode("G", "N3", nil) - g.AddNode("G", "Ignore me!", nil) - g.AddEdge("N1", "N2", true, nil) - g.AddEdge("N1", "N3", true, nil) - return g -} diff --git a/main.go b/main.go index dc5a107..d1e02cf 100644 --- a/main.go +++ b/main.go @@ -23,225 +23,96 @@ package main import ( - "bytes" - "errors" "fmt" - "net/url" + "io/ioutil" + "log" "os" - "os/exec" - "regexp" "strings" - log "github.com/Sirupsen/logrus" - "github.com/codegangsta/cli" + "github.com/uber/go-torch/pprof" + "github.com/uber/go-torch/renderer" - "github.com/uber/go-torch/graph" - "github.com/uber/go-torch/visualization" + gflags "github.com/jessevdk/go-flags" ) -type torcher struct { - commander +// options are the parameters for go-torch. +type options struct { + PProfOptions pprof.Options + File string `short:"f" long:"file" default:"torch.svg" description:"Output file name (must be .svg)"` + Print bool `short:"p" long:"print" description:"Print the generated svg to stdout instead of writing to file"` + Raw bool `short:"r" long:"raw" description:"Print the raw call graph output to stdout instead of creating a flame graph; use with Brendan Gregg's flame graph perl script (see https://github.com/brendangregg/FlameGraph)"` } -type commander interface { - goTorchCommand(*cli.Context) -} - -type defaultCommander struct { - validator validator - pprofer pprofer - grapher graph.Grapher - visualizer visualization.Visualizer -} - -type validator interface { - validateArgument(string, string, string) error -} - -type defaultValidator struct{} - -type osWrapper interface { - cmdOutput(*exec.Cmd) ([]byte, error) -} - -type defaultOSWrapper struct{} - -type pprofer interface { - runPprofCommand(args ...string) ([]byte, error) -} - -type defaultPprofer struct { - osWrapper -} - -// newTorcher returns a torcher struct with a default commander -func newTorcher() *torcher { - return &torcher{ - commander: newCommander(), +// main is the entry point of the application +func main() { + log.SetFlags(log.Ltime) + if err := runWithArgs(os.Args...); err != nil { + log.Fatalf("Failed: %v", err) } } -// newCommander returns a default commander struct with default attributes -func newCommander() commander { - return &defaultCommander{ - validator: new(defaultValidator), - pprofer: newPprofer(), - grapher: graph.NewGrapher(), - visualizer: visualization.NewVisualizer(), +func runWithArgs(args ...string) error { + opts := &options{} + if _, err := gflags.ParseArgs(opts, args); err != nil { + if flagErr, ok := err.(*gflags.Error); ok && flagErr.Type == gflags.ErrHelp { + os.Exit(0) + } + return fmt.Errorf("could not parse options: %v", err) } -} - -func newPprofer() pprofer { - return &defaultPprofer{ - osWrapper: new(defaultOSWrapper), + if err := validateOptions(opts); err != nil { + return fmt.Errorf("invalid options: %v", err) } -} -// main is the entry point of the application -func main() { - t := newTorcher() - t.createAndRunApp() -} - -// createAndRunApp configures and runs a cli.App -func (t *torcher) createAndRunApp() { - app := cli.NewApp() - app.Name = "go-torch" - app.Usage = "go-torch collects stack traces of a Go application and synthesizes them into into a flame graph" - app.Version = "0.5" - app.Authors = []cli.Author{{Name: "Ben Sandler", Email: "bens@uber.com"}} - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "url, u", - Value: "http://localhost:8080", - Usage: "base url of your Go program", - }, - cli.StringFlag{ - Name: "suffix, s", - Value: "/debug/pprof/profile", - Usage: "url path of pprof profile", - }, - cli.StringFlag{ - Name: "binaryinput, b", - Value: "", - Usage: "file path of raw binary profile; alternative to having go-torch query pprof endpoint " + - "(binary profile is anything accepted by https://golang.org/cmd/pprof)", - }, - cli.StringFlag{ - Name: "binaryname", - Value: "", - Usage: "file path of the binary that the binaryinput is for, used for pprof inputs", - }, - cli.IntFlag{ - Name: "time, t", - Value: 30, - Usage: "time in seconds to profile for", - }, - cli.StringFlag{ - Name: "file, f", - Value: "torch.svg", - Usage: "ouput file name (must be .svg)", - }, - cli.BoolFlag{ - Name: "print, p", - Usage: "print the generated svg to stdout instead of writing to file", - }, - cli.BoolFlag{ - Name: "raw, r", - Usage: "print the raw call graph output to stdout instead of creating a flame graph; " + - "use with Brendan Gregg's flame graph perl script (see https://github.com/brendangregg/FlameGraph)", - }, - } - app.Action = t.commander.goTorchCommand - app.Run(os.Args) + return runWithOptions(opts) } -// goTorchCommand executes the 'go-torch' command. -func (com *defaultCommander) goTorchCommand(c *cli.Context) { - outputFile := c.String("file") - binaryName := c.String("binaryname") - binaryInput := c.String("binaryinput") - time := c.Int("time") - stdout := c.Bool("print") - raw := c.Bool("raw") - - err := com.validator.validateArgument(outputFile, `\w+\.svg`, "Output file name must be .svg") +func runWithOptions(opts *options) error { + pprofRawOutput, err := pprof.GetRaw(opts.PProfOptions) if err != nil { - log.Fatal(err) - } - - log.Info("Profiling ...") - - var pprofArgs []string - if binaryInput != "" { - if binaryName != "" { - pprofArgs = append(pprofArgs, binaryName) - } - pprofArgs = append(pprofArgs, binaryInput) - } else { - u, err := url.Parse(c.String("url")) - if err != nil { - log.Fatal(err) - } - u.Path = c.String("suffix") - pprofArgs = []string{"-seconds", fmt.Sprint(time), u.String()} + return fmt.Errorf("could not get raw output from pprof: %v", err) } - out, err := com.pprofer.runPprofCommand(pprofArgs...) + callStacks, err := pprof.ParseRaw(pprofRawOutput) if err != nil { - log.Fatal(err) + return fmt.Errorf("could not parse raw pprof output: %v", err) } - flamegraphInput, err := com.grapher.GraphAsText(out) + + flameInput, err := renderer.ToFlameInput(callStacks) if err != nil { - log.Fatal(err) - } - flamegraphInput = strings.TrimSpace(flamegraphInput) - if raw { - fmt.Println(flamegraphInput) - log.Info("raw call graph output been printed to stdout") - return + return fmt.Errorf("could not convert stacks to flamegraph input: %v", err) } - if err := com.visualizer.GenerateFlameGraph(flamegraphInput, outputFile, stdout); err != nil { - log.Fatal(err) - } -} -// runPprofCommand runs the `go tool pprof` command to profile an application. -// It returns the output of the underlying command. -func (p *defaultPprofer) runPprofCommand(args ...string) ([]byte, error) { - allArgs := []string{"tool", "pprof", "-dot", "-lines"} - allArgs = append(allArgs, args...) + if opts.Raw { + log.Print("Printing raw flamegraph input to stdout") + fmt.Printf("%s", flameInput) + return nil + } - var buf bytes.Buffer - cmd := exec.Command("go", allArgs...) - cmd.Stderr = &buf - out, err := p.osWrapper.cmdOutput(cmd) + flameGraph, err := renderer.GenerateFlameGraph(flameInput) if err != nil { - return nil, err + return fmt.Errorf("could not generate flame graph: %v", err) } - // @HACK because 'go tool pprof' doesn't exit on errors with nonzero status codes. - // Ironically, this means that Go's own os/exec package does not detect its errors. - // See issue here https://github.com/golang/go/issues/11510 - if len(out) == 0 { - errText := buf.String() - return nil, errors.New("pprof returned an error. Here is the raw STDERR output:\n" + errText) + if opts.Print { + log.Print("Printing svg to stdout") + fmt.Printf("%s", flameGraph) + return nil } - return out, nil -} + log.Printf("Writing svg to %v", opts.File) + if err := ioutil.WriteFile(opts.File, flameGraph, 0666); err != nil { + return fmt.Errorf("could not write output file: %v", err) + } -// cmdOutput is a tiny wrapper around cmd.Output to enable test mocking -func (w *defaultOSWrapper) cmdOutput(cmd *exec.Cmd) ([]byte, error) { - return cmd.Output() + return nil } -// validateArgument validates a given command line argument with regex. If the -// argument does not match the expected format, this function returns an error. -func (v *defaultValidator) validateArgument(argument, regex, errorMessage string) error { - match, _ := regexp.MatchString(regex, argument) - if !match { - return errors.New(errorMessage) +func validateOptions(opts *options) error { + if opts.File != "" && !strings.HasSuffix(opts.File, ".svg") { + return fmt.Errorf("output file must end in .svg") + } + if opts.PProfOptions.TimeSeconds < 1 { + return fmt.Errorf("seconds must be an integer greater than 0") } return nil } diff --git a/main_test.go b/main_test.go index a62d00b..32977de 100644 --- a/main_test.go +++ b/main_test.go @@ -21,294 +21,173 @@ package main import ( - "errors" + "bufio" + "io/ioutil" + "os" + "path/filepath" + "strings" "testing" - "github.com/codegangsta/cli" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" + gflags "github.com/jessevdk/go-flags" ) -func TestCreateAndRunApp(t *testing.T) { - mockCommander := new(mockCommander) - torcher := &torcher{ - commander: mockCommander, - } +const testPProfInputFile = "./pprof/testdata/pprof.1.pb.gz" - var validateContext = func(args mock.Arguments) { - context := args.Get(0).(*cli.Context) - assert.NotNil(t, context) - assert.Equal(t, "go-torch", context.App.Name) +func getDefaultOptions() *options { + opts := &options{} + if _, err := gflags.ParseArgs(opts, nil); err != nil { + panic(err) } - mockCommander.On("goTorchCommand", mock.AnythingOfType("*cli.Context")).Return().Run(validateContext).Once() - - torcher.createAndRunApp() + opts.PProfOptions.BinaryFile = testPProfInputFile + return opts } -func TestCreateAndRunAppDefaultValues(t *testing.T) { - mockCommander := new(mockCommander) - torcher := &torcher{ - commander: mockCommander, +func TestBadArgs(t *testing.T) { + err := runWithArgs("-t", "asd") + if err == nil { + t.Fatalf("expected run with bad arguments to fail") } - validateDefaults := func(args mock.Arguments) { - context := args.Get(0).(*cli.Context) - assert.Equal(t, 30, context.Int("time")) - assert.Equal(t, "http://localhost:8080", context.String("url")) - assert.Equal(t, "/debug/pprof/profile", context.String("suffix")) - assert.Equal(t, "torch.svg", context.String("file")) - assert.Equal(t, "", context.String("binaryinput")) - assert.Equal(t, "", context.String("binaryname")) - assert.Equal(t, false, context.Bool("print")) - assert.Equal(t, false, context.Bool("raw")) - assert.Equal(t, 10, len(context.App.Flags)) + expectedSubstr := []string{ + "could not parse options", + "invalid argument", } - mockCommander.On("goTorchCommand", mock.AnythingOfType( - "*cli.Context")).Return().Run(validateDefaults) - - torcher.createAndRunApp() -} - -func testGoTorchCommand(t *testing.T, url string) { - mockValidator := new(mockValidator) - mockPprofer := new(mockPprofer) - mockGrapher := new(mockGrapher) - mockVisualizer := new(mockVisualizer) - commander := &defaultCommander{ - validator: mockValidator, - pprofer: mockPprofer, - grapher: mockGrapher, - visualizer: mockVisualizer, + for _, substr := range expectedSubstr { + if !strings.Contains(err.Error(), substr) { + t.Errorf("error is missing message: %v", substr) + } } - - samplePprofOutput := []byte("out") - - mockValidator.On("validateArgument", "torch.svg", `\w+\.svg`, - "Output file name must be .svg").Return(nil).Once() - mockPprofer.On("runPprofCommand", []string{"-seconds", "30", "http://localhost/hi"}).Return(samplePprofOutput, nil).Once() - mockGrapher.On("GraphAsText", samplePprofOutput).Return("1;2;3 3", nil).Once() - mockVisualizer.On("GenerateFlameGraph", "1;2;3 3", "torch.svg", false).Return(nil).Once() - - createSampleContext(commander, url) - - mockValidator.AssertExpectations(t) - mockPprofer.AssertExpectations(t) - mockGrapher.AssertExpectations(t) - mockVisualizer.AssertExpectations(t) } -func TestGoTorchCommand(t *testing.T) { - testGoTorchCommand(t, "http://localhost") - - // Trailing slash in url should still work. - testGoTorchCommand(t, "http://localhost/") +func TestMain(t *testing.T) { + os.Args = []string{"--raw", "--binaryinput", testPProfInputFile} + main() + // Test should not fatal. } -func TestGoTorchCommandRawOutput(t *testing.T) { - mockValidator := new(mockValidator) - mockPprofer := new(mockPprofer) - mockGrapher := new(mockGrapher) - mockVisualizer := new(mockVisualizer) - commander := &defaultCommander{ - validator: mockValidator, - pprofer: mockPprofer, - grapher: mockGrapher, - visualizer: mockVisualizer, +func TestInvalidOptions(t *testing.T) { + tests := []struct { + args []string + errorMessage string + }{ + { + args: []string{"--file", "bad.jpg"}, + errorMessage: "must end in .svg", + }, + { + args: []string{"-t", "0"}, + errorMessage: "seconds must be an integer greater than 0", + }, } - samplePprofOutput := []byte("out") - mockValidator.On("validateArgument", "torch.svg", `\w+\.svg`, - "Output file name must be .svg").Return(nil).Once() - mockPprofer.On("runPprofCommand", []string{"-seconds", "30", "http://localhost/hi"}).Return(samplePprofOutput, nil).Once() - mockGrapher.On("GraphAsText", samplePprofOutput).Return("1;2;3 3", nil).Once() - - createSampleContextForRaw(commander) + for _, tt := range tests { + err := runWithArgs(tt.args...) + if err == nil { + t.Errorf("Expected error when running with: %v", tt.args) + continue + } - mockValidator.AssertExpectations(t) - mockPprofer.AssertExpectations(t) - mockGrapher.AssertExpectations(t) - mockVisualizer.AssertExpectations(t) // ensure that mockVisualizer was never called -} - -func TestGoTorchCommandBinaryInput(t *testing.T) { - mockValidator := new(mockValidator) - mockPprofer := new(mockPprofer) - mockGrapher := new(mockGrapher) - mockVisualizer := new(mockVisualizer) - commander := &defaultCommander{ - validator: mockValidator, - pprofer: mockPprofer, - grapher: mockGrapher, - visualizer: mockVisualizer, + if !strings.Contains(err.Error(), tt.errorMessage) { + t.Errorf("Error missing message, got %v want message %v", err.Error(), tt.errorMessage) + } } - - samplePprofOutput := []byte("out") - mockValidator.On("validateArgument", "torch.svg", `\w+\.svg`, - "Output file name must be .svg").Return(nil).Once() - mockPprofer.On("runPprofCommand", []string{"/path/to/binary/file", "/path/to/binary/input"}).Return(samplePprofOutput, nil).Once() - mockGrapher.On("GraphAsText", samplePprofOutput).Return("1;2;3 3", nil).Once() - mockVisualizer.On("GenerateFlameGraph", "1;2;3 3", "torch.svg", false).Return(nil).Once() - - createSampleContextForBinaryInput(commander) - - mockValidator.AssertExpectations(t) - mockPprofer.AssertExpectations(t) - mockGrapher.AssertExpectations(t) - mockVisualizer.AssertExpectations(t) } -func TestValidateArgumentFail(t *testing.T) { - validator := new(defaultValidator) - assert.Error(t, validator.validateArgument("bad bad", `\w+\.svg`, "Message")) -} +func TestRunRaw(t *testing.T) { + opts := getDefaultOptions() + opts.Raw = true -func TestValidateArgumentPass(t *testing.T) { - assert.NotPanics(t, func() { - new(defaultValidator).validateArgument("good.svg", `\w+\.svg`, "Message") - }) -} - -func TestRunPprofCommand(t *testing.T) { - mockOSWrapper := new(mockOSWrapper) - pprofer := defaultPprofer{ - osWrapper: mockOSWrapper, + if err := runWithOptions(opts); err != nil { + t.Fatalf("Run with Raw failed: %v", err) } - - mockOSWrapper.On("cmdOutput", mock.AnythingOfType("*exec.Cmd")).Return([]byte("output"), nil).Once() - - sampleArgs := []string{"-seconds", "15", "http://localhost:8080"} - out, err := pprofer.runPprofCommand(sampleArgs...) - - assert.Equal(t, []byte("output"), out) - assert.NoError(t, err) - mockOSWrapper.AssertExpectations(t) } -func TestRunPprofCommandUnderlyingError(t *testing.T) { - mockOSWrapper := new(mockOSWrapper) - pprofer := defaultPprofer{ - osWrapper: mockOSWrapper, +func getTempFilename(t *testing.T, suffix string) string { + f, err := ioutil.TempFile("", "") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) } - mockOSWrapper.On("cmdOutput", mock.AnythingOfType("*exec.Cmd")).Return(nil, errors.New("pprof underlying error")).Once() - - sampleArgs := []string{"-seconds", "15", "http://localhost:8080"} - out, err := pprofer.runPprofCommand(sampleArgs...) - - assert.Equal(t, 0, len(out)) - assert.Error(t, err) - mockOSWrapper.AssertExpectations(t) + defer f.Close() + return f.Name() + suffix +} + +func TestRunFile(t *testing.T) { + opts := getDefaultOptions() + opts.File = getTempFilename(t, ".svg") + + withScriptsInPath(t, func() { + if err := runWithOptions(opts); err != nil { + t.Fatalf("Run with Print failed: %v", err) + } + + f, err := os.Open(opts.File) + if err != nil { + t.Errorf("Failed to open output file: %v", err) + } + defer f.Close() + + // Our fake flamegraph scripts just add script names to the output. + reader := bufio.NewReader(f) + line1, err := reader.ReadString('\n') + if err != nil { + t.Errorf("Failed to read line 1 in output file: %v", err) + } + if !strings.Contains(line1, "flamegraph.pl") { + t.Errorf("Output file has not been processed by flame graph scripts") + } + }) } -// 'go tool pprof' doesn't exit on errors with nonzero status codes. This test -// ensures that go-torch will detect undrlying errors despite the pprof bug. -// See pprof issue here https://github.com/golang/go/issues/11510 -func TestRunPprofCommandHandlePprofErrorBug(t *testing.T) { - mockOSWrapper := new(mockOSWrapper) - pprofer := defaultPprofer{ - osWrapper: mockOSWrapper, - } - - mockOSWrapper.On("cmdOutput", mock.AnythingOfType("*exec.Cmd")).Return([]byte{}, nil).Once() - - sampleArgs := []string{"-seconds", "15", "http://localhost:8080"} - out, err := pprofer.runPprofCommand(sampleArgs...) +func TestRunBadFile(t *testing.T) { + opts := getDefaultOptions() + opts.File = "/dev/zero/invalid/file" - assert.Equal(t, 0, len(out)) - assert.Error(t, err) - mockOSWrapper.AssertExpectations(t) + withScriptsInPath(t, func() { + if err := runWithOptions(opts); err == nil { + t.Fatalf("Run with bad file expected to fail") + } + }) } -func TestNewTorcher(t *testing.T) { - assert.NotNil(t, newTorcher()) -} +func TestRunPrint(t *testing.T) { + opts := getDefaultOptions() + opts.Print = true -func TestNewCommander(t *testing.T) { - assert.NotNil(t, newCommander()) + withScriptsInPath(t, func() { + if err := runWithOptions(opts); err != nil { + t.Fatalf("Run with Print failed: %v", err) + } + // TODO(prashantv): Verify that output is printed to stdout. + }) } -func createSampleContext(commander *defaultCommander, url string) { - app := cli.NewApp() - app.Name = "go-torch" - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "url, u", - Value: url, - }, - cli.StringFlag{ - Name: "suffix, s", - Value: "/hi", - }, - cli.IntFlag{ - Name: "time, t", - Value: 30, - }, - cli.StringFlag{ - Name: "file, f", - Value: "torch.svg", - }, +// scriptsPath is used to cache the fake scripts if we've already created it. +var scriptsPath string + +func withScriptsInPath(t *testing.T, f func()) { + oldPath := os.Getenv("PATH") + defer os.Setenv("PATH", oldPath) + + // Create a temporary directory with fake flamegraph scripts if we haven't already. + if scriptsPath == "" { + var err error + scriptsPath, err = ioutil.TempDir("", "go-torch-scripts") + if err != nil { + t.Fatalf("Failed to create temporary scripts dir: %v", err) + } + + // Create scripts in this path. + const scriptContents = `#!/bin/sh + echo $0 + cat + ` + scriptFile := filepath.Join(scriptsPath, "flamegraph.pl") + if err := ioutil.WriteFile(scriptFile, []byte(scriptContents), 0777); err != nil { + t.Errorf("Failed to create script %v: %v", scriptFile, err) + } } - app.Action = commander.goTorchCommand - app.Run([]string{"go-torch"}) -} -func createSampleContextForRaw(commander *defaultCommander) { - app := cli.NewApp() - app.Name = "go-torch" - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "url, u", - Value: "http://localhost", - }, - cli.StringFlag{ - Name: "suffix, s", - Value: "/hi", - }, - cli.IntFlag{ - Name: "time, t", - Value: 30, - }, - cli.StringFlag{ - Name: "file, f", - Value: "torch.svg", - }, - cli.BoolTFlag{ - Name: "raw, r", - }, - } - app.Action = commander.goTorchCommand - app.Run([]string{"go-torch"}) -} - -func createSampleContextForBinaryInput(commander *defaultCommander) { - app := cli.NewApp() - app.Name = "go-torch" - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "url, u", - Value: "http://localhost", - }, - cli.StringFlag{ - Name: "suffix, s", - Value: "/hi", - }, - cli.StringFlag{ - Name: "binaryinput, b", - Value: "/path/to/binary/input", - }, - cli.StringFlag{ - Name: "binaryname", - Value: "/path/to/binary/file", - }, - cli.IntFlag{ - Name: "time, t", - Value: 30, - }, - cli.StringFlag{ - Name: "file, f", - Value: "torch.svg", - }, - } - app.Action = commander.goTorchCommand - app.Run([]string{"go-torch"}) + os.Setenv("PATH", scriptsPath+":"+oldPath) + f() } diff --git a/mocks.go b/mocks.go deleted file mode 100644 index 9f178a7..0000000 --- a/mocks.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) 2015 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package main - -import ( - "os/exec" - - "github.com/codegangsta/cli" - "github.com/stretchr/testify/mock" -) - -type mockVisualizer struct { - mock.Mock -} - -func (m *mockVisualizer) GenerateFlameGraph(_a0 string, _a1 string, _a2 bool) error { - ret := m.Called(_a0, _a1, _a2) - - r0 := ret.Error(0) - - return r0 -} - -type mockGrapher struct { - mock.Mock -} - -func (m *mockGrapher) GraphAsText(_a0 []byte) (string, error) { - ret := m.Called(_a0) - - r0 := ret.Get(0).(string) - r1 := ret.Error(1) - - return r0, r1 -} - -type mockCommander struct { - mock.Mock -} - -func (m *mockCommander) goTorchCommand(_a0 *cli.Context) { - m.Called(_a0) -} - -type mockValidator struct { - mock.Mock -} - -func (m *mockValidator) validateArgument(_a0 string, _a1 string, _a2 string) error { - ret := m.Called(_a0, _a1, _a2) - - r0 := ret.Error(0) - - return r0 -} - -type mockPprofer struct { - mock.Mock -} - -type mockOSWrapper struct { - mock.Mock -} - -func (m *mockOSWrapper) cmdOutput(_a0 *exec.Cmd) ([]byte, error) { - ret := m.Called(_a0) - - var r0 []byte - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - r1 := ret.Error(1) - - return r0, r1 -} - -func (m *mockPprofer) runPprofCommand(args ...string) ([]byte, error) { - ret := m.Called(args) - - var r0 []byte - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - r1 := ret.Error(1) - - return r0, r1 -} diff --git a/pprof/parser.go b/pprof/parser.go new file mode 100644 index 0000000..c0f222b --- /dev/null +++ b/pprof/parser.go @@ -0,0 +1,233 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package pprof + +import ( + "bufio" + "bytes" + "fmt" + "io" + "regexp" + "strconv" + "strings" + "time" + + "github.com/uber/go-torch/stack" +) + +type readState int + +const ( + ignore readState = iota + samplesHeader + samples + locations + mappings +) + +// funcID is the ID of a given Location in the pprof raw output. +type funcID int + +type rawParser struct { + // err is the first error encountered by the parser. + err error + + state readState + funcNames map[funcID]string + records []*stackRecord +} + +// ParseRaw parses the raw pprof output and returns call stacks. +func ParseRaw(input []byte) ([]*stack.Sample, error) { + parser := newRawParser() + if err := parser.parse(input); err != nil { + return nil, err + } + + return parser.toSamples(), nil +} + +func newRawParser() *rawParser { + return &rawParser{ + funcNames: make(map[funcID]string), + } +} + +func (p *rawParser) parse(input []byte) error { + reader := bufio.NewReader(bytes.NewReader(input)) + + for { + line, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF { + if p.state < locations { + p.setError(fmt.Errorf("parser ended before processing locations, state: %v", p.state)) + } + break + } + return err + } + + p.processLine(strings.TrimSpace(line)) + } + + return p.err +} + +func (p *rawParser) setError(err error) { + if p.err != nil { + return + } + p.err = err +} + +func (p *rawParser) processLine(line string) { + switch p.state { + case ignore: + if strings.HasPrefix(line, "Samples") { + p.state = samplesHeader + return + } + case samplesHeader: + p.state = samples + case samples: + if strings.HasPrefix(line, "Locations") { + p.state = locations + return + } + p.addSample(line) + case locations: + if strings.HasPrefix(line, "Mappings") { + p.state = mappings + return + } + p.addLocation(line) + case mappings: + // Nothing to process. + } +} + +// toSamples aggregates stack sample counts and returns a list of unique stack samples. +func (p *rawParser) toSamples() []*stack.Sample { + samples := make(map[string]*stack.Sample) + for _, r := range p.records { + funcNames := r.funcNames(p.funcNames) + funcKey := strings.Join(funcNames, ";") + + if sample, ok := samples[funcKey]; ok { + sample.Count += r.samples + continue + } + + samples[funcKey] = &stack.Sample{ + Funcs: funcNames, + Count: r.samples, + } + } + + samplesList := make([]*stack.Sample, 0, len(samples)) + for _, s := range samples { + samplesList = append(samplesList, s) + } + + return samplesList +} + +// addLocation parses a location that looks like: +// 292: 0x49dee1 github.com/uber/tchannel/golang.(*Frame).ReadIn :0 s=0 +// and creates a mapping from funcID to function name. +func (p *rawParser) addLocation(line string) { + parts := splitBySpace(line) + if len(parts) < 3 { + p.setError(fmt.Errorf("malformed location line: %v", line)) + return + } + funcID := p.toFuncID(strings.TrimSuffix(parts[0], ":")) + p.funcNames[funcID] = parts[2] +} + +type stackRecord struct { + samples int + duration time.Duration + stack []funcID +} + +// addSample parses a sample that looks like: +// 1 10000000: 1 2 3 4 +// and creates a stackRecord for it. +func (p *rawParser) addSample(line string) { + // Parse a sample which looks like: + parts := splitBySpace(line) + if len(parts) < 3 { + p.setError(fmt.Errorf("malformed sample line: %v", line)) + return + } + + record := &stackRecord{ + samples: p.parseInt(parts[0]), + duration: time.Duration(p.parseInt(strings.TrimSuffix(parts[1], ":"))), + } + for _, fIDStr := range parts[2:] { + record.stack = append(record.stack, p.toFuncID(fIDStr)) + } + + p.records = append(p.records, record) +} +func getFunctionName(funcNames map[funcID]string, funcID funcID) string { + if funcName, ok := funcNames[funcID]; ok { + return funcName + } + return fmt.Sprintf("missing-function-%v", funcID) +} + +// funcNames returns the function names for this stack sample. +// It returns in parent first order. +func (r *stackRecord) funcNames(funcNames map[funcID]string) []string { + var names []string + for i := len(r.stack) - 1; i >= 0; i-- { + funcID := r.stack[i] + names = append(names, getFunctionName(funcNames, funcID)) + } + return names +} + +// parseInt converts a string to an int. It stores any errors using setError. +func (p *rawParser) parseInt(s string) int { + v, err := strconv.Atoi(s) + if err != nil { + p.setError(err) + return 0 + } + + return v +} + +// toFuncID converts a string like "8" to a funcID. +func (p *rawParser) toFuncID(s string) funcID { + return funcID(p.parseInt(s)) +} + +var spaceSplitter = regexp.MustCompile(`\s+`) + +// splitBySpace splits values separated by 1 or more spaces. +func splitBySpace(s string) []string { + return spaceSplitter.Split(strings.TrimSpace(s), -1) +} diff --git a/pprof/parser_test.go b/pprof/parser_test.go new file mode 100644 index 0000000..5c7f30b --- /dev/null +++ b/pprof/parser_test.go @@ -0,0 +1,222 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package pprof + +import ( + "io/ioutil" + "reflect" + "strings" + "testing" + "time" + + "github.com/uber/go-torch/stack" +) + +func parseTestRawData(t *testing.T) ([]byte, *rawParser) { + rawBytes, err := ioutil.ReadFile("testdata/pprof.raw.txt") + if err != nil { + t.Fatalf("Failed to read testdata/pprof.raw.txt: %v", err) + } + + parser := newRawParser() + if err := parser.parse(rawBytes); err != nil { + t.Fatalf("Parse failed: %v", err) + } + + return rawBytes, parser +} + +func TestParse(t *testing.T) { + _, parser := parseTestRawData(t) + + // line 7 - 249 are stack records in the test file. + const expectedNumRecords = 242 + if len(parser.records) != expectedNumRecords { + t.Errorf("Failed to parse all records, got %v records, expected %v", + len(parser.records), expectedNumRecords) + } + expectedRecords := map[int]*stackRecord{ + 0: &stackRecord{1, time.Duration(10000000), []funcID{1, 2, 2, 2, 3, 3, 2, 2, 3, 3, 2, 2, 2, 3, 3, 3, 2, 3, 2, 3, 2, 2, 3, 2, 2, 3, 4, 5, 6}}, + 18: &stackRecord{1, time.Duration(10000000), []funcID{14, 2, 2, 3, 2, 2, 3, 2, 2, 3, 3, 3, 2, 2, 2, 3, 3, 2, 3, 3, 3, 3, 3, 2, 4, 5, 6}}, + 45: &stackRecord{12, time.Duration(120000000), []funcID{23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34}}, + } + for recordNum, expected := range expectedRecords { + if got := parser.records[recordNum]; !reflect.DeepEqual(got, expected) { + t.Errorf("Unexpected record for %v:\n got %#v\n want %#v", recordNum, got, expected) + } + } + + // line 250 - 290 are locations (or funcID mappings) + const expectedFuncIDs = 41 + if len(parser.funcNames) != expectedFuncIDs { + t.Errorf("Failed to parse func ID mappings, got %v records, expected %v", + len(parser.funcNames), expectedFuncIDs) + } + knownMappings := map[funcID]string{ + 1: "main.fib", + 20: "main.fib", + 34: "runtime.morestack", + } + for funcID, expected := range knownMappings { + if got := parser.funcNames[funcID]; got != expected { + t.Errorf("Unexpected mapping for %v: got %v, want %v", funcID, got, expected) + } + } +} + +func TestParseRawValid(t *testing.T) { + rawBytes, _ := parseTestRawData(t) + got, err := ParseRaw(rawBytes) + if err != nil { + t.Fatalf("ParseRaw failed: %v", err) + } + + if expected := 18; len(got) != expected { + t.Errorf("Expected %v unique stack samples, got %v", expected, got) + } +} + +func TestParseMissingLocation(t *testing.T) { + contents := `Samples: + samples/count cpu/nanoseconds + 2 10000000: 1 2 + Locations: + 1: 0xaaaaa funcName :0 s=0 +` + out, err := ParseRaw([]byte(contents)) + if err != nil { + t.Fatalf("Missing location should not cause an error, got %v", err) + } + + expected := []*stack.Sample{{ + Funcs: []string{"missing-function-2", "funcName"}, + Count: 2, + }} + if !reflect.DeepEqual(out, expected) { + t.Errorf("Missing function call stack should contain missing-function-2\n got %+v\n want %+v", expected, out) + } +} + +func testParseRawBad(t *testing.T, errorReason, errorSubstr, contents string) { + _, err := ParseRaw([]byte(contents)) + if err == nil { + t.Errorf("Bad %v should cause error while parsing:%s", errorReason, contents) + return + } + + if !strings.Contains(err.Error(), errorSubstr) { + t.Errorf("Bad %v error should contain %q, got %v", errorReason, errorSubstr, err) + } +} + +// Test data for validating that bad input is handled. +const ( + sampleCount = "2" + sampleTime = "10000000" + funcIDLocation = "3" + funcIDSample = "4" + simpleTemplate = ` +Samples: +samples/count cpu/nanoseconds + 2 10000000: 4 5 6 +Locations: + 3: 0xaaaaa funcName :0 s=0 +` +) + +func TestParseRawBadFuncID(t *testing.T) { + { + contents := strings.Replace(simpleTemplate, funcIDSample, "?sample?", -1) + testParseRawBad(t, "funcID in sample", "strconv.ParseInt", contents) + } + + { + contents := strings.Replace(simpleTemplate, funcIDLocation, "?location?", -1) + testParseRawBad(t, "funcID in location", "strconv.ParseInt", contents) + } +} + +func TestParseRawBadSample(t *testing.T) { + { + contents := strings.Replace(simpleTemplate, sampleCount, "??", -1) + testParseRawBad(t, "sample count", "strconv.ParseInt", contents) + } + + { + contents := strings.Replace(simpleTemplate, sampleTime, "??", -1) + testParseRawBad(t, "sample duration", "strconv.ParseInt", contents) + } +} + +func TestParseRawBadMultipleErrors(t *testing.T) { + contents := strings.Replace(simpleTemplate, sampleCount, "?s?", -1) + contents = strings.Replace(contents, sampleTime, "?t?", -1) + testParseRawBad(t, "sample duration", `strconv.ParseInt: parsing "?s?"`, contents) +} + +func TestParseRawBadMalformedSample(t *testing.T) { + contents := ` +Samples: +samples/count cpu/nanoseconds + 1 +Locations: + 3: 0xaaaaa funcName :0 s=0 +` + testParseRawBad(t, "malformed sample line", "malformed sample", contents) +} + +func TestParseRawBadMalformedLocation(t *testing.T) { + contents := ` +Samples: +samples/count cpu/nanoseconds + 1 10000: 2 +Locations: + 3 +` + testParseRawBad(t, "malformed location line", "malformed location", contents) +} + +func TestParseRawBadNoLocations(t *testing.T) { + contents := ` +Samples: +samples/count cpu/nanoseconds + 1 10000: 2 +` + testParseRawBad(t, "no locations", "parser ended before processing locations", contents) +} + +func TestSplitBySpace(t *testing.T) { + tests := []struct { + s string + expected []string + }{ + {"", []string{""}}, + {"test", []string{"test"}}, + {"1 2", []string{"1", "2"}}, + {"1 2 3 4 ", []string{"1", "2", "3", "4"}}, + } + + for _, tt := range tests { + if got := splitBySpace(tt.s); !reflect.DeepEqual(got, tt.expected) { + t.Errorf("splitBySpace(%v) failed:\n got %#v\n want %#v", tt.s, got, tt.expected) + } + } +} diff --git a/pprof/pprof.go b/pprof/pprof.go new file mode 100644 index 0000000..86c6b7d --- /dev/null +++ b/pprof/pprof.go @@ -0,0 +1,90 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package pprof + +import ( + "bytes" + "fmt" + "net/url" + "os/exec" +) + +// Options are parameters for pprof. +type Options struct { + BaseURL string `short:"u" long:"url" default:"http://localhost:8080" description:"Base URL of your Go program"` + URLSuffix string `short:"s" long:"suffix" default:"/debug/pprof/profile" description:"URL path of pprof profile"` + BinaryFile string `short:"b" long:"binaryinput" description:"file path of previously saved binary profile. (binary profile is anything accepted by https://golang.org/cmd/pprof)"` + BinaryName string `long:"binaryname" description:"file path of the binary that the binaryinput is for, used for pprof inputs"` + TimeSeconds int `short:"t" long:"time" default:"30" description:"Duration to profile for"` +} + +// GetRaw returns the raw output from pprof for the given options. +func GetRaw(opts Options) ([]byte, error) { + args, err := getArgs(opts) + if err != nil { + return nil, err + } + + return runPProf(args...) +} + +// getArgs gets the arguments to run pprof with for a given set of Options. +func getArgs(opts Options) ([]string, error) { + var pprofArgs []string + if opts.BinaryFile != "" { + if opts.BinaryName != "" { + pprofArgs = append(pprofArgs, opts.BinaryName) + } + pprofArgs = append(pprofArgs, opts.BinaryFile) + } else { + u, err := url.Parse(opts.BaseURL) + if err != nil { + return nil, fmt.Errorf("failed to parse URL: %v", err) + } + + u.Path = opts.URLSuffix + pprofArgs = append(pprofArgs, "-seconds", fmt.Sprint(opts.TimeSeconds), u.String()) + } + + return pprofArgs, nil +} + +func runPProf(args ...string) ([]byte, error) { + allArgs := []string{"tool", "pprof", "-raw"} + allArgs = append(allArgs, args...) + + var buf bytes.Buffer + cmd := exec.Command("go", allArgs...) + cmd.Stderr = &buf + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("pprof error: %v\nSTDERR:\n%s", err, buf.Bytes()) + } + + // @HACK because 'go tool pprof' doesn't exit on errors with nonzero status codes. + // Ironically, this means that Go's own os/exec package does not detect its errors. + // See issue here https://github.com/golang/go/issues/11510 + if len(out) == 0 { + return nil, fmt.Errorf("pprof error:\n%s", buf.Bytes()) + } + + return out, nil +} diff --git a/pprof/pprof_test.go b/pprof/pprof_test.go new file mode 100644 index 0000000..88110c0 --- /dev/null +++ b/pprof/pprof_test.go @@ -0,0 +1,149 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package pprof + +import ( + "bytes" + "reflect" + "testing" +) + +func TestGetArgs(t *testing.T) { + tests := []struct { + opts Options + expected []string + wantErr bool + }{ + { + opts: Options{ + BaseURL: "http://localhost:1234", + URLSuffix: "/path/to/profile", + TimeSeconds: 5, + }, + expected: []string{"-seconds", "5", "http://localhost:1234/path/to/profile"}, + }, + { + opts: Options{ + BaseURL: "http://localhost:1234/", + URLSuffix: "/path/to/profile", + TimeSeconds: 5, + }, + expected: []string{"-seconds", "5", "http://localhost:1234/path/to/profile"}, + }, + { + opts: Options{ + BaseURL: "http://localhost:1234/test", + URLSuffix: "/path/to/profile", + TimeSeconds: 5, + }, + expected: []string{"-seconds", "5", "http://localhost:1234/path/to/profile"}, + }, + { + opts: Options{ + BinaryFile: "/path/to/binaryfile", + BaseURL: "http://localhost:1234", + URLSuffix: "/profile", + TimeSeconds: 5}, + expected: []string{"/path/to/binaryfile"}, + }, + { + opts: Options{ + BinaryFile: "/path/to/binaryfile", + BinaryName: "/path/to/binaryname", + BaseURL: "http://localhost:1234", + URLSuffix: "/profile", + TimeSeconds: 5}, + expected: []string{"/path/to/binaryname", "/path/to/binaryfile"}, + }, + { + opts: Options{ + BaseURL: "%-0", // this makes url.Parse fail. + URLSuffix: "/profile", + TimeSeconds: 5, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + got, err := getArgs(tt.opts) + if (err != nil) != tt.wantErr { + t.Errorf("wantErr %v got error: %v", tt.wantErr, err) + continue + } + if err != nil { + continue + } + + if !reflect.DeepEqual(tt.expected, got) { + t.Errorf("got incorrect args for %v:\n got %v\n want %v", tt.opts, got, tt.expected) + } + } +} + +func TestRunPProfUnknownFlag(t *testing.T) { + if _, err := runPProf("-unknownFlag"); err == nil { + t.Fatalf("expected error for unknown flag") + } +} + +func TestRunPProfMissingFile(t *testing.T) { + if _, err := runPProf("unknown-file"); err == nil { + t.Fatalf("expected error for unknown file") + } +} + +func TestRunPProfInvalidURL(t *testing.T) { + if _, err := runPProf("http://127.0.0.1:999/profile"); err == nil { + t.Fatalf("expected error for unknown file") + } +} + +func TestGetPProfRawBadURL(t *testing.T) { + opts := Options{ + BaseURL: "%-0", + } + if _, err := GetRaw(opts); err == nil { + t.Error("expected bad BaseURL to fail") + } +} + +func TestGetPProfRawSuccess(t *testing.T) { + opts := Options{ + BinaryFile: "testdata/pprof.1.pb.gz", + } + raw, err := GetRaw(opts) + if err != nil { + t.Fatalf("getPProfRaw failed: %v", err) + } + + expectedSubstrings := []string{ + "Duration: 3s", + "Samples", + "Locations", + "main.fib", + } + for _, substr := range expectedSubstrings { + if !bytes.Contains(raw, []byte(substr)) { + t.Errorf("pprof raw output missing expected string: %s\ngot:\n%s", substr, raw) + } + } +} diff --git a/pprof/testdata/pprof.1.pb.gz b/pprof/testdata/pprof.1.pb.gz new file mode 100644 index 0000000..2e6f117 Binary files /dev/null and b/pprof/testdata/pprof.1.pb.gz differ diff --git a/pprof/testdata/pprof.raw.txt b/pprof/testdata/pprof.raw.txt new file mode 100644 index 0000000..5170190 --- /dev/null +++ b/pprof/testdata/pprof.raw.txt @@ -0,0 +1,291 @@ +PeriodType: cpu nanoseconds +Period: 10000000 +Time: 2015-09-10 13:53:30.696637683 -0700 PDT +Duration: 3s +Samples: +samples/count cpu/nanoseconds + 1 10000000: 1 2 2 2 3 3 2 2 3 3 2 2 2 3 3 3 2 3 2 3 2 2 3 2 2 3 4 5 6 + 1 10000000: 7 2 3 3 3 3 3 3 3 2 2 3 3 3 2 3 3 3 3 3 3 3 3 2 3 3 3 3 2 3 3 2 4 5 6 + 1 10000000: 8 2 2 3 3 3 2 3 3 3 3 3 2 3 3 3 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 4 5 6 + 1 10000000: 9 3 2 2 2 3 2 3 3 2 3 2 3 2 3 3 2 3 3 2 3 2 3 3 3 2 4 5 6 + 1 10000000: 10 2 3 3 3 3 3 3 3 2 3 2 2 3 3 3 3 2 3 2 3 2 2 3 3 3 2 4 5 6 + 1 10000000: 1 3 3 3 3 2 3 3 3 3 2 3 3 2 3 3 2 3 2 2 2 2 3 2 3 4 5 6 + 1 10000000: 1 2 2 2 2 3 2 2 3 2 2 3 2 3 2 2 3 3 3 2 3 2 3 3 3 3 4 5 6 + 1 10000000: 10 3 2 3 3 2 3 2 3 3 2 3 3 2 2 2 3 3 3 3 3 3 3 3 3 3 3 2 3 3 3 2 4 5 6 + 1 10000000: 11 3 2 3 3 3 3 2 2 3 3 3 2 2 3 3 3 2 3 2 3 3 2 3 3 3 2 4 5 6 + 1 10000000: 12 3 3 2 2 2 2 3 3 2 3 2 2 2 2 2 2 3 3 3 3 2 3 3 2 4 5 6 + 1 10000000: 10 3 3 3 2 3 2 2 3 2 3 2 3 3 3 2 3 2 2 2 3 3 3 3 3 3 3 3 2 3 2 4 5 6 + 1 10000000: 11 3 2 3 2 2 2 2 3 2 3 2 3 3 3 2 3 2 3 3 3 3 3 3 2 2 4 5 6 + 1 10000000: 13 3 3 3 3 3 2 3 3 3 3 2 3 3 3 2 3 2 2 3 3 3 3 3 3 3 2 3 3 2 2 2 4 5 6 + 1 10000000: 14 3 3 3 3 2 2 3 2 2 3 3 3 3 2 2 3 3 3 3 3 3 3 3 3 3 2 2 3 3 2 3 4 5 6 + 1 10000000: 11 3 3 3 3 3 3 2 3 2 3 3 2 2 3 3 3 3 3 3 3 3 3 3 3 2 3 3 3 3 3 3 3 3 4 5 6 + 1 10000000: 15 3 2 3 3 3 3 3 3 3 3 3 2 2 3 3 3 2 3 3 2 2 2 2 3 3 3 3 3 3 2 3 4 5 6 + 1 10000000: 13 3 3 3 3 3 3 3 3 3 3 3 2 3 2 3 3 3 3 2 2 3 3 3 3 2 3 3 3 2 3 3 3 3 3 4 5 6 + 1 10000000: 14 2 2 3 2 3 3 3 3 3 3 3 3 3 3 3 3 2 3 3 2 2 3 2 3 3 3 3 3 3 3 3 3 3 4 5 6 + 1 10000000: 14 2 2 3 2 2 3 2 2 3 3 3 2 2 2 3 3 2 3 3 3 3 3 2 4 5 6 + 1 10000000: 16 3 2 3 2 2 3 2 3 2 3 3 2 3 2 2 3 3 3 2 2 3 3 2 2 2 2 4 5 6 + 1 10000000: 1 2 3 2 3 2 3 3 3 2 3 3 2 2 3 3 2 2 3 2 2 3 3 3 2 2 4 5 6 + 1 10000000: 14 3 3 3 2 2 3 2 3 3 3 3 3 3 2 3 3 3 3 3 3 3 2 2 3 3 2 3 3 3 3 3 4 5 6 + 1 10000000: 13 3 3 2 2 2 3 3 3 3 3 2 3 2 3 2 2 2 3 3 3 3 2 2 3 3 2 2 4 5 6 + 1 10000000: 17 3 2 3 3 3 3 2 3 3 3 3 3 3 3 2 3 3 3 2 3 3 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 5 6 + 1 10000000: 7 2 3 2 3 3 2 2 3 2 3 3 3 3 3 3 3 3 3 3 3 3 2 3 2 3 2 3 2 3 3 3 3 4 5 6 + 1 10000000: 14 2 3 3 3 3 3 2 2 2 2 3 2 3 2 2 3 3 2 3 3 3 2 3 3 3 4 5 6 + 1 10000000: 11 3 3 3 2 3 3 3 3 2 3 3 3 2 2 2 3 2 3 2 2 2 3 2 3 2 3 3 4 5 6 + 1 10000000: 7 2 3 3 3 3 3 2 3 2 2 3 3 2 3 2 3 3 3 3 2 3 3 2 3 3 3 3 3 2 3 3 4 5 6 + 1 10000000: 11 3 3 3 3 2 2 3 3 3 3 3 3 2 2 3 3 3 3 2 3 3 3 3 3 2 3 2 2 3 3 3 3 4 5 6 + 1 10000000: 14 2 3 3 2 2 3 2 3 2 3 3 2 2 3 2 3 3 3 2 3 2 3 2 3 2 4 5 6 + 1 10000000: 18 3 2 2 2 3 2 3 2 2 2 2 3 2 3 3 2 2 3 2 2 3 3 2 3 3 3 4 5 6 + 1 10000000: 13 3 3 3 3 3 3 3 3 2 3 2 3 3 3 3 3 3 2 3 2 3 2 3 3 3 3 2 3 3 3 3 4 5 6 + 1 10000000: 10 2 2 3 3 3 3 3 2 2 3 2 3 3 2 3 3 2 3 3 2 2 2 3 3 3 2 3 3 3 2 3 4 5 6 + 1 10000000: 14 3 2 3 3 2 3 2 2 3 3 2 2 2 3 3 2 2 3 3 3 3 2 3 3 3 3 3 4 5 6 + 1 10000000: 13 2 3 3 3 3 3 3 2 3 3 2 2 3 3 3 2 3 3 3 2 2 2 3 3 3 3 2 3 2 3 3 3 4 5 6 + 1 10000000: 11 3 2 2 3 3 3 2 3 3 3 2 2 2 3 3 2 2 3 3 3 2 2 3 2 3 4 5 6 + 1 10000000: 19 3 2 3 2 2 3 2 3 3 3 3 3 3 2 2 3 3 3 3 3 2 3 3 3 3 2 3 2 3 2 3 4 5 6 + 1 10000000: 20 3 2 3 2 3 2 3 2 3 3 2 2 2 3 2 3 2 3 2 3 3 3 3 3 2 3 3 2 3 4 5 6 + 1 10000000: 7 2 3 3 3 3 3 3 3 2 2 3 2 2 3 3 3 2 3 3 2 3 2 3 2 3 2 3 3 3 4 5 6 + 1 10000000: 21 3 2 2 3 3 3 2 2 2 2 3 3 2 2 2 2 3 2 3 2 2 4 5 6 + 1 10000000: 22 3 3 3 2 3 3 3 3 2 3 3 2 3 2 3 3 3 3 3 3 2 3 3 2 3 2 2 2 2 3 4 5 6 + 1 10000000: 17 2 2 2 2 3 3 3 3 3 2 3 2 3 3 2 3 3 2 3 3 3 3 3 3 3 3 3 3 3 2 4 5 6 + 1 10000000: 9 2 2 3 2 2 3 2 3 2 2 3 3 3 3 3 3 3 3 2 3 3 3 3 3 2 2 2 2 3 4 5 6 + 1 10000000: 7 2 3 2 3 2 3 3 3 2 3 2 3 3 3 2 2 3 3 2 3 2 3 3 2 3 3 2 3 3 4 5 6 + 1 10000000: 1 2 2 3 2 2 3 2 3 2 3 3 3 3 3 3 3 3 3 3 3 2 2 3 2 2 3 2 3 3 4 5 6 + 12 120000000: 23 24 25 26 27 28 29 30 31 32 33 34 + 1 10000000: 13 2 3 2 3 3 2 3 3 3 2 2 2 3 3 3 3 2 3 3 3 3 3 3 2 2 2 3 2 3 4 5 6 + 1 10000000: 35 3 3 2 3 2 3 2 3 2 3 2 3 2 3 2 2 2 3 3 2 3 3 3 3 3 3 3 3 3 3 4 5 6 + 1 10000000: 13 3 3 3 3 3 2 3 3 3 2 2 3 3 3 3 2 3 2 3 3 3 2 2 2 2 3 3 3 3 3 3 3 3 4 5 6 + 1 10000000: 1 3 3 3 3 2 3 3 3 3 3 3 2 3 2 2 3 3 3 3 2 2 2 3 3 3 3 2 3 2 3 4 5 6 + 1 10000000: 9 2 3 3 3 3 3 3 2 3 3 3 2 3 2 2 3 3 3 3 2 3 2 3 3 3 3 3 3 2 2 3 4 5 6 + 1 10000000: 17 3 2 3 3 3 3 2 2 3 3 3 3 3 3 3 3 2 3 3 3 3 3 3 2 3 2 3 2 2 3 3 3 4 5 6 + 1 10000000: 16 3 3 2 3 2 3 3 3 3 3 3 3 3 2 3 3 3 3 2 3 3 2 3 3 3 3 3 3 3 2 3 2 3 4 5 6 + 1 10000000: 14 2 3 3 2 3 3 3 3 3 3 3 3 2 3 3 2 2 2 3 3 3 2 3 3 2 3 3 3 3 3 3 3 4 5 6 + 1 10000000: 36 3 3 3 3 3 3 3 2 3 3 3 3 2 2 3 3 2 3 3 3 2 3 3 3 2 3 3 2 3 2 3 3 4 5 6 + 1 10000000: 37 3 3 3 3 3 2 3 3 3 3 3 3 2 3 2 2 2 3 2 3 2 3 3 3 3 3 2 3 2 3 2 4 5 6 + 1 10000000: 8 2 3 3 3 3 3 2 3 3 3 2 3 3 3 3 3 3 3 2 2 2 3 2 3 2 3 3 2 3 2 3 4 5 6 + 1 10000000: 13 2 3 3 3 3 3 3 3 3 3 3 3 2 3 2 2 3 3 3 3 2 3 3 3 2 3 2 3 2 3 3 3 2 4 5 6 + 1 10000000: 36 3 3 3 2 3 3 2 3 3 3 3 3 3 3 3 3 3 3 3 2 2 3 3 2 3 3 3 3 3 3 2 3 3 3 3 4 5 6 + 1 10000000: 9 3 3 3 3 2 3 2 3 3 2 2 3 3 2 3 2 2 3 2 3 3 3 3 3 2 3 2 2 2 4 5 6 + 1 10000000: 9 2 2 2 3 2 2 2 2 3 3 3 3 2 3 2 3 3 3 3 3 3 2 3 3 2 2 2 3 3 4 5 6 + 1 10000000: 9 2 2 3 3 3 3 3 3 2 2 2 2 3 3 3 3 2 2 2 3 3 3 3 3 2 3 4 5 6 + 1 10000000: 14 2 2 3 3 3 3 3 3 3 3 2 3 2 3 2 3 3 3 3 3 3 3 3 2 2 2 3 2 2 4 5 6 + 1 10000000: 15 3 2 2 3 3 3 2 3 2 3 3 2 3 3 3 2 2 3 3 3 3 3 2 2 3 2 3 3 2 4 5 6 + 1 10000000: 21 2 3 2 3 3 3 2 3 2 3 3 3 2 2 3 2 3 3 3 3 2 3 2 4 5 6 + 1 10000000: 21 3 3 3 2 2 3 3 3 2 3 3 3 2 2 3 3 2 3 3 3 3 2 2 2 4 5 6 + 1 10000000: 14 2 2 3 2 2 2 2 2 3 3 3 3 3 2 2 2 2 3 3 3 3 2 3 3 4 5 6 + 1 10000000: 11 2 2 3 3 3 3 3 3 2 3 3 3 3 3 2 3 3 3 3 3 2 3 2 3 3 4 5 6 + 1 10000000: 9 2 2 3 3 2 3 3 3 2 2 3 2 3 2 2 2 2 3 2 3 3 3 3 3 2 3 3 3 2 4 5 6 + 1 10000000: 14 2 3 2 3 3 3 3 2 2 2 3 3 2 3 3 3 3 3 3 3 2 2 3 2 3 4 5 6 + 1 10000000: 11 2 2 3 2 3 2 3 3 3 2 3 2 3 3 3 3 2 3 2 3 3 2 2 3 3 2 4 5 6 + 1 10000000: 14 3 2 2 3 3 3 3 3 3 2 3 3 3 3 3 3 3 2 3 3 2 3 2 3 3 2 2 4 5 6 + 1 10000000: 9 3 2 2 3 3 3 3 2 2 3 2 2 2 3 2 3 2 2 3 3 3 3 3 3 2 2 2 4 5 6 + 1 10000000: 13 3 3 3 3 3 3 3 3 2 3 2 2 2 3 2 2 3 3 2 2 2 3 3 2 2 3 2 4 5 6 + 1 10000000: 9 2 2 3 3 3 3 2 3 2 2 3 3 3 3 2 3 2 3 3 3 2 3 3 3 3 2 2 4 5 6 + 1 10000000: 11 3 2 2 3 3 2 3 2 3 3 2 3 3 2 2 2 3 2 4 5 6 + 1 10000000: 13 3 3 3 3 3 3 3 2 3 3 3 2 2 2 3 3 3 2 3 2 3 3 2 2 2 2 3 3 2 4 5 6 + 1 10000000: 13 2 3 3 3 2 2 3 2 3 3 3 3 2 3 2 3 2 3 2 3 3 2 3 2 2 3 2 3 2 4 5 6 + 1 10000000: 9 2 2 2 3 3 3 2 3 3 3 2 2 3 3 2 3 3 3 3 3 3 2 2 3 3 3 3 3 3 3 3 2 3 4 5 6 + 1 10000000: 14 3 3 3 2 3 3 3 2 2 3 3 3 3 3 3 2 2 3 3 2 2 3 3 3 2 2 3 3 3 3 3 4 5 6 + 1 10000000: 13 3 3 3 3 3 3 3 3 3 3 3 3 2 3 3 2 3 3 3 3 3 3 3 2 2 3 3 3 3 3 2 3 3 3 4 5 6 + 1 10000000: 37 2 3 3 3 2 3 3 2 2 3 3 3 3 3 2 3 2 3 2 3 3 2 3 3 3 2 3 3 3 2 3 3 4 5 6 + 1 10000000: 13 2 3 2 3 3 3 2 3 3 3 3 2 3 3 2 3 3 3 3 3 2 2 2 3 2 3 3 3 2 2 3 3 4 5 6 + 1 10000000: 9 2 3 3 3 3 3 3 2 3 3 3 2 2 2 2 3 2 2 3 3 3 3 3 3 3 2 2 3 3 3 3 3 4 5 6 + 1 10000000: 38 3 3 3 3 3 2 3 3 3 2 2 3 3 2 3 3 3 3 3 2 3 3 3 3 2 3 3 3 2 2 3 3 4 5 6 + 1 10000000: 19 3 2 3 2 3 3 3 3 3 3 2 3 2 2 3 2 2 3 2 3 3 3 2 2 3 3 3 3 2 3 4 5 6 + 1 10000000: 37 3 3 3 2 3 3 2 3 2 2 3 2 3 2 3 3 2 2 3 3 3 3 2 2 2 2 3 3 3 3 4 5 6 + 1 10000000: 9 3 2 3 3 3 3 3 2 3 2 3 3 3 3 2 3 3 2 3 3 2 3 3 3 3 3 3 2 2 3 3 2 3 4 5 6 + 1 10000000: 13 3 3 3 2 2 3 3 3 3 3 2 3 3 3 3 3 2 2 2 3 3 3 2 3 3 3 3 2 2 3 2 4 5 6 + 1 10000000: 39 3 2 2 3 2 2 3 3 3 3 2 3 3 3 3 3 3 2 2 3 3 2 3 2 2 3 3 2 3 3 4 5 6 + 1 10000000: 13 3 3 3 3 3 2 3 3 2 3 2 2 2 3 2 3 3 3 3 2 3 3 2 3 3 2 3 3 3 3 2 3 4 5 6 + 1 10000000: 9 2 2 3 2 3 3 3 3 3 3 3 3 3 3 2 3 2 3 3 2 3 2 3 2 3 3 2 2 3 2 3 3 4 5 6 + 1 10000000: 8 2 3 2 3 3 2 3 2 3 3 2 3 3 3 2 3 2 2 2 3 3 3 3 3 3 3 3 2 2 3 3 4 5 6 + 1 10000000: 10 2 2 2 3 3 2 3 3 3 3 2 3 3 2 3 3 2 3 2 3 2 3 2 2 3 3 3 3 3 3 3 4 5 6 + 1 10000000: 13 2 3 3 3 3 3 3 2 3 3 3 2 2 3 3 3 3 2 2 2 3 2 3 3 3 3 3 2 2 3 3 3 4 5 6 + 1 10000000: 9 2 2 3 3 2 3 3 3 3 3 2 3 3 3 3 2 2 3 3 2 3 2 3 2 3 2 3 3 2 4 5 6 + 1 10000000: 11 3 3 3 3 3 3 2 2 2 2 2 3 3 3 3 2 2 2 3 3 2 2 3 3 4 5 6 + 1 10000000: 11 2 3 2 2 3 3 3 3 3 2 3 2 3 3 2 2 2 2 3 3 2 3 3 3 3 2 4 5 6 + 1 10000000: 1 2 3 3 3 3 3 3 2 2 2 2 3 3 2 2 3 2 2 3 2 3 2 2 2 2 4 5 6 + 1 10000000: 11 3 2 2 3 3 3 2 2 2 2 3 2 3 2 2 3 3 3 2 3 3 2 3 2 2 4 5 6 + 1 10000000: 14 2 3 2 2 2 3 2 2 3 3 3 3 2 3 3 3 2 2 3 3 3 2 3 4 5 6 + 1 10000000: 10 2 2 2 2 2 2 3 2 2 3 3 3 3 3 3 3 3 2 3 3 3 3 2 2 3 2 4 5 6 + 1 10000000: 11 3 2 2 2 2 3 2 3 2 3 3 2 3 3 3 3 2 3 2 2 3 2 3 2 3 3 4 5 6 + 1 10000000: 9 2 2 2 3 3 2 3 3 3 2 2 2 3 3 3 3 3 2 3 3 3 3 3 3 3 2 3 4 5 6 + 1 10000000: 11 3 2 2 3 2 2 3 3 3 3 3 2 2 3 3 2 3 3 3 2 3 3 3 2 3 3 3 4 5 6 + 1 10000000: 1 2 2 3 2 3 3 3 3 3 3 2 3 3 2 3 2 3 3 3 3 2 2 2 3 2 3 4 5 6 + 1 10000000: 21 2 3 2 3 2 3 3 3 3 3 3 3 2 3 2 2 2 2 3 2 3 3 3 2 2 4 5 6 + 1 10000000: 7 3 3 3 3 3 3 2 3 3 2 3 2 2 2 3 3 2 3 2 2 3 2 2 2 4 5 6 + 1 10000000: 14 3 2 2 3 2 3 3 3 3 2 3 2 2 2 3 2 3 3 2 3 3 2 2 3 3 4 5 6 + 1 10000000: 21 3 3 3 2 2 3 3 3 3 3 2 2 2 2 2 3 3 2 2 2 3 3 3 2 3 2 3 4 5 6 + 1 10000000: 15 3 2 2 3 2 3 3 2 2 3 3 3 3 3 3 3 2 2 3 3 2 2 2 2 3 3 3 4 5 6 + 1 10000000: 7 3 2 2 3 3 3 3 2 3 3 3 3 3 3 2 3 3 3 3 3 3 2 3 3 3 2 3 4 5 6 + 1 10000000: 21 2 2 2 2 2 3 3 2 2 3 2 2 3 2 3 3 3 2 2 3 3 3 2 3 3 2 3 4 5 6 + 1 10000000: 8 2 3 3 2 3 2 2 3 2 3 3 2 2 3 3 2 2 2 3 3 2 3 2 2 2 3 3 4 5 6 + 1 10000000: 11 3 2 3 3 2 2 2 2 2 3 3 3 3 2 2 3 3 2 2 3 3 2 3 3 3 3 4 5 6 + 1 10000000: 13 3 3 2 2 3 2 3 2 2 2 3 2 3 3 2 3 3 2 2 3 3 3 2 2 3 3 3 4 5 6 + 1 10000000: 21 2 3 2 2 2 3 2 3 3 3 2 2 2 2 3 2 3 3 2 2 3 3 2 3 2 3 3 4 5 6 + 1 10000000: 22 3 2 2 3 2 2 3 3 2 3 3 3 3 2 3 3 3 2 2 3 3 3 3 3 3 2 3 2 3 4 5 6 + 1 10000000: 38 3 3 2 3 2 3 2 3 3 2 2 2 2 3 3 3 2 3 2 2 3 3 3 2 2 3 3 3 3 4 5 6 + 1 10000000: 38 3 2 3 2 3 3 3 3 2 3 3 3 3 2 3 3 2 3 2 2 3 2 2 2 2 2 3 3 3 4 5 6 + 1 10000000: 37 3 3 3 3 2 2 2 3 3 2 3 3 3 2 3 2 2 3 3 2 3 2 3 3 2 2 2 3 3 4 5 6 + 1 10000000: 10 3 3 3 3 3 3 3 3 3 2 3 2 3 3 3 3 3 2 3 2 3 3 2 2 2 3 3 3 3 3 2 2 3 4 5 6 + 1 10000000: 10 3 3 3 3 2 3 3 3 2 2 3 2 3 3 2 3 3 3 3 3 2 3 3 2 3 2 3 3 3 3 3 3 3 3 4 5 6 + 1 10000000: 13 2 3 3 3 3 3 3 2 2 3 3 2 3 3 3 3 3 2 3 2 2 3 3 3 2 2 3 2 2 3 3 4 5 6 + 1 10000000: 35 2 3 3 2 2 3 3 3 3 2 3 3 3 3 2 3 3 3 3 3 3 3 2 3 2 2 3 2 3 2 3 4 5 6 + 1 10000000: 10 2 3 2 3 3 3 3 3 3 3 3 3 2 2 3 3 3 2 3 3 3 3 2 3 2 3 2 2 3 3 2 3 4 5 6 + 1 10000000: 15 2 2 2 3 3 3 3 2 2 2 3 3 2 3 3 3 3 3 3 3 3 3 3 3 2 3 3 3 3 3 3 4 5 6 + 1 10000000: 16 2 3 3 3 3 2 3 2 3 3 3 2 3 3 2 3 3 2 2 3 2 2 3 3 2 3 3 3 3 3 4 5 6 + 1 10000000: 20 3 3 3 3 3 2 3 2 2 2 2 3 3 3 3 2 3 2 3 3 2 3 3 2 3 3 3 3 2 3 3 4 5 6 + 1 10000000: 14 3 3 2 3 3 3 2 3 3 2 3 2 3 3 3 3 3 3 2 3 3 3 3 3 3 2 3 2 2 3 2 3 4 5 6 + 1 10000000: 16 3 3 3 3 2 3 2 3 2 2 2 3 2 2 3 3 3 2 3 2 3 3 2 3 2 3 3 3 3 3 4 5 6 + 1 10000000: 12 3 3 3 2 3 2 2 3 2 3 2 3 3 3 3 3 3 3 3 3 2 3 3 3 3 2 2 2 3 3 3 3 4 5 6 + 1 10000000: 10 3 3 3 2 3 3 3 3 2 3 3 2 3 3 3 3 3 2 3 3 2 3 2 2 2 2 3 3 2 3 4 5 6 + 1 10000000: 21 3 3 2 2 3 3 2 2 3 3 3 3 3 3 3 3 3 3 2 3 2 3 2 2 3 2 3 3 3 3 4 5 6 + 1 10000000: 9 2 2 2 3 2 3 3 3 3 2 2 2 3 3 3 2 3 3 2 3 3 2 2 3 3 3 3 3 3 2 3 4 5 6 + 1 10000000: 14 3 3 3 3 2 3 3 3 3 2 3 2 2 3 2 3 3 3 2 3 3 3 3 3 3 2 3 3 2 3 4 5 6 + 1 10000000: 1 2 2 2 2 2 3 2 3 2 3 3 2 3 3 2 3 3 3 3 3 2 3 3 2 2 3 4 5 6 + 1 10000000: 1 2 2 2 2 3 3 2 3 3 2 2 3 3 3 3 3 2 3 2 2 3 2 3 2 3 4 5 6 + 1 10000000: 12 3 2 2 2 3 3 3 2 2 3 3 2 3 2 3 2 3 2 3 3 3 3 3 2 3 4 5 6 + 1 10000000: 11 3 3 2 3 2 3 3 3 2 3 3 2 2 3 2 3 2 2 2 2 3 3 3 3 2 3 4 5 6 + 1 10000000: 7 3 2 2 3 2 3 3 3 3 3 2 2 2 3 3 2 3 3 3 3 3 2 2 3 3 2 4 5 6 + 1 10000000: 14 2 2 2 2 2 3 3 3 2 2 3 3 2 2 3 3 2 3 3 2 2 2 3 4 5 6 + 1 10000000: 9 3 3 3 3 2 2 3 2 2 2 2 2 3 2 2 2 3 2 3 2 3 3 2 3 2 4 5 6 + 1 10000000: 14 2 3 3 2 2 3 2 3 2 3 3 3 3 2 3 3 2 3 3 2 2 3 3 2 2 2 4 5 6 + 1 10000000: 14 3 3 2 2 3 2 3 2 2 2 3 2 3 3 3 3 2 3 3 3 3 2 3 2 3 2 2 4 5 6 + 1 10000000: 12 2 3 3 3 2 2 3 3 3 3 3 3 3 2 3 2 3 3 3 3 2 3 3 2 3 3 3 4 5 6 + 1 10000000: 12 2 3 3 2 3 2 3 2 3 3 2 3 3 3 3 2 2 3 3 3 3 2 3 2 2 3 3 4 5 6 + 1 10000000: 11 3 2 3 2 2 2 3 2 2 3 2 3 2 2 2 3 3 2 3 2 3 2 3 3 3 4 5 6 + 1 10000000: 21 2 2 2 3 2 2 3 3 3 2 2 3 3 2 3 3 3 3 3 2 2 3 3 2 3 3 4 5 6 + 1 10000000: 14 2 2 2 3 3 2 3 3 2 3 3 2 3 3 3 2 3 3 2 2 3 2 2 3 4 5 6 + 1 10000000: 40 2 2 3 2 2 2 2 2 3 2 2 2 3 2 3 3 3 3 3 3 2 2 2 3 2 4 5 6 + 1 10000000: 9 3 2 2 2 3 2 3 3 3 3 3 3 2 2 2 3 3 3 3 3 2 3 3 3 3 2 3 4 5 6 + 1 10000000: 11 3 2 2 3 2 2 3 2 3 3 2 3 3 3 2 3 2 3 3 2 2 2 3 3 3 2 3 4 5 6 + 1 10000000: 11 2 3 2 3 3 2 3 3 3 3 3 2 3 3 2 3 3 2 2 3 3 2 3 2 3 2 3 4 5 6 + 1 10000000: 14 3 3 2 2 3 3 2 3 3 3 3 3 3 2 2 2 3 3 3 2 3 2 3 3 3 2 3 4 5 6 + 1 10000000: 9 2 3 3 2 3 2 3 3 2 3 3 2 2 2 2 2 2 2 3 3 3 3 2 3 3 3 3 4 5 6 + 1 10000000: 10 3 3 2 3 2 2 2 3 2 2 2 2 3 2 2 3 2 3 2 3 2 2 2 3 3 3 4 5 6 + 1 10000000: 16 3 2 3 2 3 2 2 2 2 3 2 2 3 2 2 2 2 2 2 2 2 2 3 3 4 5 6 + 1 10000000: 10 2 2 2 3 3 3 2 3 3 3 2 3 2 2 2 3 3 2 2 3 2 2 2 3 3 3 3 4 5 6 + 1 10000000: 12 3 3 3 2 3 3 2 2 3 2 2 2 2 2 2 3 3 3 2 3 2 2 2 3 2 3 3 4 5 6 + 1 10000000: 38 3 3 2 3 2 2 3 2 2 3 2 2 3 3 3 3 3 3 2 3 3 2 3 2 3 3 3 2 3 4 5 6 + 1 10000000: 38 3 3 3 3 3 3 2 2 2 2 2 2 2 2 3 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 5 6 + 1 10000000: 9 2 2 3 2 3 3 3 3 3 3 2 3 3 2 3 3 2 3 2 2 3 3 3 2 2 2 2 2 4 5 6 + 1 10000000: 9 2 2 3 3 3 2 3 3 2 2 2 2 3 3 3 2 3 2 2 3 3 2 3 3 2 3 3 3 4 5 6 + 1 10000000: 10 2 3 3 3 2 3 3 3 2 3 3 2 2 2 3 2 3 2 3 3 3 3 3 2 2 3 2 3 4 5 6 + 1 10000000: 21 3 3 3 2 3 2 3 3 3 2 2 3 3 3 3 3 3 2 3 3 3 3 3 3 2 3 2 2 4 5 6 + 1 10000000: 21 3 3 3 3 3 2 3 3 3 3 3 2 2 3 3 3 2 2 2 3 2 3 3 3 2 3 3 2 4 5 6 + 1 10000000: 21 2 3 2 2 3 3 3 3 3 3 3 3 2 2 3 3 3 2 3 3 3 2 3 3 2 3 3 2 4 5 6 + 1 10000000: 21 3 3 2 2 3 3 3 2 2 3 2 3 3 2 3 3 2 3 3 2 3 2 3 3 3 3 3 3 4 5 6 + 1 10000000: 21 3 2 2 2 3 3 3 3 3 3 2 2 3 2 3 2 2 3 3 3 2 2 3 3 3 3 3 3 4 5 6 + 1 10000000: 11 3 3 2 3 3 3 3 2 2 3 3 2 3 3 3 2 3 3 3 3 2 3 3 2 2 3 3 3 4 5 6 + 1 10000000: 13 3 3 2 3 3 3 3 3 2 3 3 3 3 3 3 3 3 2 2 3 3 3 2 3 2 2 2 3 3 3 3 3 4 5 6 + 1 10000000: 16 2 2 2 3 3 3 3 3 2 3 3 3 3 3 3 3 2 2 2 3 2 2 2 3 3 3 2 3 3 3 4 5 6 + 1 10000000: 36 3 2 3 2 2 2 3 3 3 3 3 2 3 2 3 3 2 3 2 2 2 3 3 3 3 3 3 3 3 3 4 5 6 + 1 10000000: 39 2 2 2 2 3 3 3 3 3 3 3 2 3 3 3 2 3 3 3 2 3 3 2 3 3 3 3 2 2 4 5 6 + 1 10000000: 11 3 2 3 2 3 3 3 2 2 3 3 3 2 3 3 2 3 2 3 3 3 3 2 3 3 3 2 3 3 3 4 5 6 + 1 10000000: 39 2 3 3 3 2 3 2 3 3 3 3 2 3 2 3 2 2 2 2 3 3 3 3 3 3 3 2 2 3 4 5 6 + 1 10000000: 38 3 3 2 2 2 3 3 2 3 3 2 3 2 3 2 3 3 2 2 3 2 3 3 2 2 3 4 5 6 + 1 10000000: 40 2 2 3 3 3 3 2 3 2 3 2 3 3 2 3 3 3 2 3 2 3 3 3 2 2 2 2 3 3 4 5 6 + 1 10000000: 14 3 2 3 2 2 3 3 2 2 2 3 3 2 2 3 3 3 3 3 3 2 3 2 3 3 2 4 5 6 + 1 10000000: 21 2 2 2 3 3 2 3 2 2 2 3 3 3 2 3 3 3 2 3 2 3 2 2 4 5 6 + 1 10000000: 21 2 3 2 2 2 3 2 3 2 2 2 3 3 2 2 2 2 3 2 2 2 3 2 3 4 5 6 + 1 10000000: 11 3 3 3 3 3 3 3 2 3 3 3 2 3 2 2 3 2 2 2 2 3 3 4 5 6 + 1 10000000: 11 3 2 3 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 2 3 3 2 4 5 6 + 1 10000000: 10 2 3 2 2 3 2 3 3 3 2 3 3 3 2 3 3 3 3 3 2 3 3 2 2 3 3 3 3 4 5 6 + 1 10000000: 8 3 3 2 3 3 2 3 2 3 2 3 3 3 3 3 2 2 3 2 3 2 3 2 3 2 2 3 2 4 5 6 + 1 10000000: 11 3 3 3 2 2 3 2 3 2 3 2 3 3 2 3 3 3 3 3 3 3 2 3 3 3 2 2 3 4 5 6 + 1 10000000: 11 3 3 3 2 2 2 3 3 3 3 3 3 3 3 2 3 2 2 3 3 3 3 3 3 3 3 3 2 4 5 6 + 1 10000000: 20 3 2 3 3 3 2 2 3 3 3 2 3 3 3 3 2 2 2 3 3 2 3 3 2 3 2 3 2 4 5 6 + 1 10000000: 13 3 2 2 3 3 2 3 2 2 3 2 3 2 2 2 3 3 3 2 3 2 3 3 2 3 3 3 3 4 5 6 + 1 10000000: 13 3 3 2 3 3 2 2 3 3 3 2 3 2 3 3 2 3 3 3 3 3 3 3 2 2 3 3 3 4 5 6 + 1 10000000: 13 3 3 3 3 2 3 3 2 3 2 3 2 2 2 2 2 2 3 2 3 2 2 3 3 3 2 3 3 4 5 6 + 1 10000000: 21 3 2 3 3 3 3 2 3 3 3 2 3 3 2 3 3 3 2 3 2 3 2 3 3 3 2 3 2 4 5 6 + 1 10000000: 21 2 2 2 3 2 3 3 3 3 3 3 2 3 2 2 3 2 3 2 3 3 3 3 3 2 3 3 3 4 5 6 + 1 10000000: 1 2 3 3 3 2 3 2 3 3 2 3 3 2 2 3 2 3 3 3 2 3 2 3 2 2 3 3 3 4 5 6 + 1 10000000: 35 2 3 3 3 3 2 3 2 2 2 2 3 3 2 2 3 3 2 2 3 3 3 3 3 3 3 2 2 4 5 6 + 1 10000000: 21 3 3 3 3 3 3 3 2 3 3 3 2 3 2 2 3 3 3 2 2 3 3 3 3 3 2 2 3 4 5 6 + 1 10000000: 14 3 3 3 3 2 3 2 3 3 2 3 3 3 3 3 2 2 3 2 2 2 3 3 3 3 3 3 2 4 5 6 + 1 10000000: 7 3 3 2 3 3 2 3 2 3 3 3 2 3 3 3 3 2 3 3 3 3 3 2 2 3 3 3 3 4 5 6 + 1 10000000: 12 2 3 2 2 3 3 3 3 3 2 3 2 2 3 2 3 3 2 3 3 2 3 3 2 3 2 2 3 4 5 6 + 1 10000000: 12 2 3 3 3 3 3 3 3 2 3 3 3 2 2 2 2 3 3 2 3 2 2 3 3 3 3 3 2 4 5 6 + 1 10000000: 40 2 3 3 3 3 2 3 3 2 2 3 2 2 3 3 2 3 2 2 3 3 2 2 3 3 3 3 2 4 5 6 + 1 10000000: 21 3 3 3 2 3 3 3 3 3 3 3 3 2 3 3 2 2 3 2 3 3 3 2 2 2 3 3 3 4 5 6 + 1 10000000: 14 3 3 3 2 2 3 2 3 3 3 2 2 3 3 3 2 3 2 2 2 3 3 3 2 3 3 3 3 4 5 6 + 1 10000000: 11 2 2 3 3 3 3 3 3 2 3 3 2 2 3 3 3 3 2 3 3 3 2 3 3 2 3 3 2 3 4 5 6 + 1 10000000: 20 3 2 3 2 3 3 3 3 3 3 3 2 3 3 2 2 3 2 2 2 4 5 6 + 1 10000000: 9 2 3 3 3 3 3 2 3 3 3 3 2 2 3 3 3 2 3 2 3 2 2 2 2 3 3 3 3 3 3 4 5 6 + 1 10000000: 41 2 2 2 3 3 2 2 3 2 3 3 3 3 3 3 3 2 3 2 3 2 3 3 3 2 2 3 3 3 2 4 5 6 + 1 10000000: 21 3 3 3 2 3 3 3 3 2 3 3 3 3 3 2 3 2 2 3 3 2 3 3 3 3 3 3 2 3 3 4 5 6 + 1 10000000: 36 3 2 3 3 2 3 2 2 3 3 3 3 2 3 3 3 2 3 3 3 2 3 3 2 2 3 3 3 2 3 4 5 6 + 1 10000000: 10 3 2 2 3 2 2 2 3 2 3 2 3 3 3 3 3 3 2 2 3 3 3 3 3 2 2 2 3 3 4 5 6 + 1 10000000: 13 2 3 3 2 3 3 3 2 3 2 3 2 2 2 3 3 2 2 3 3 3 3 2 3 3 3 2 3 2 3 4 5 6 + 1 10000000: 8 2 3 3 2 3 3 2 3 3 2 3 2 2 2 2 3 3 2 2 3 3 3 3 2 3 2 2 3 4 5 6 + 1 10000000: 10 2 3 3 3 3 3 2 2 2 3 3 2 3 2 3 3 2 2 3 3 3 3 2 3 3 3 2 2 4 5 6 + 1 10000000: 13 3 2 2 3 3 2 2 3 2 3 2 3 3 2 2 2 3 2 3 3 3 3 3 3 2 2 2 3 4 5 6 + 1 10000000: 11 3 3 2 2 2 3 2 3 3 2 3 2 3 3 2 3 3 2 3 3 3 2 3 3 3 3 3 3 4 5 6 + 1 10000000: 1 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 3 2 2 2 3 2 3 2 2 3 3 2 3 4 5 6 + 1 10000000: 38 3 3 2 3 2 2 2 3 2 3 3 2 3 3 2 2 3 3 3 3 2 2 3 3 2 3 3 3 4 5 6 + 1 10000000: 11 3 3 3 3 3 2 3 3 3 3 3 2 3 2 2 3 3 3 3 3 2 3 3 2 3 3 3 3 2 4 5 6 + 1 10000000: 13 3 3 2 2 3 3 3 2 3 2 3 2 2 2 2 2 3 2 3 2 2 3 2 3 3 2 3 3 4 5 6 + 1 10000000: 11 3 3 3 2 2 2 2 3 3 3 3 2 3 2 3 2 3 2 3 2 3 3 2 3 3 2 2 3 3 4 5 6 + 1 10000000: 21 3 3 2 3 3 3 3 3 2 2 3 3 3 3 2 3 3 3 2 3 2 3 2 3 3 2 3 2 4 5 6 + 1 10000000: 7 3 2 3 2 3 3 3 2 3 3 3 3 2 3 2 3 2 2 3 3 3 3 2 3 3 2 2 3 4 5 6 + 1 10000000: 11 3 2 3 2 2 3 2 2 2 3 3 3 3 2 2 3 3 3 2 3 3 2 2 3 3 3 3 3 3 4 5 6 + 1 10000000: 11 2 2 2 3 3 2 3 2 3 3 2 2 3 3 3 3 3 3 3 3 3 2 2 3 3 3 3 2 3 4 5 6 + 1 10000000: 14 3 3 3 2 2 2 2 2 2 3 3 3 2 3 2 3 3 3 3 3 3 3 3 3 2 3 2 2 4 5 6 + 1 10000000: 21 2 2 3 3 3 3 2 3 2 2 2 2 3 2 3 3 3 3 3 3 2 2 2 3 3 3 2 3 3 4 5 6 + 1 10000000: 13 2 3 3 3 3 3 2 3 2 3 3 3 3 2 2 3 3 3 3 3 3 2 3 2 2 3 2 2 2 3 4 5 6 + 1 10000000: 13 2 3 3 2 2 3 2 2 2 3 2 3 3 2 3 2 2 3 2 2 3 3 3 3 3 3 3 2 3 3 4 5 6 + 1 10000000: 10 2 2 3 2 3 3 2 2 3 2 3 3 2 2 3 3 3 3 2 3 3 3 2 3 3 2 2 2 3 4 5 6 + 1 10000000: 8 2 3 3 3 3 2 3 2 3 3 3 2 3 3 3 2 3 3 2 2 2 2 3 2 3 2 2 3 3 4 5 6 + 1 10000000: 9 2 2 2 3 3 2 2 3 2 3 3 3 2 3 3 2 2 3 2 2 3 3 2 3 3 3 3 2 3 3 4 5 6 + 1 10000000: 7 3 3 2 3 3 2 3 3 2 3 3 3 3 3 2 3 3 2 3 2 2 2 3 3 3 3 3 3 3 2 4 5 6 + 1 10000000: 7 3 3 3 3 3 3 3 2 3 3 3 3 2 3 3 3 2 3 3 3 2 3 3 2 3 2 3 3 2 3 4 5 6 + 1 10000000: 7 2 3 3 3 3 3 3 3 3 3 2 3 3 3 2 2 3 2 3 3 3 3 3 3 3 2 3 2 3 3 4 5 6 + 1 10000000: 36 3 3 3 2 2 3 3 3 2 3 3 3 2 3 2 3 3 2 2 3 3 3 3 3 2 2 3 2 2 4 5 6 + 1 10000000: 7 3 3 3 3 3 3 3 3 2 3 3 3 3 3 3 3 2 2 2 2 2 2 2 3 2 2 3 3 3 4 5 6 + 1 10000000: 11 3 2 2 3 3 2 3 2 3 3 3 2 2 3 3 2 2 3 3 2 3 3 2 3 2 3 3 3 3 4 5 6 + 1 10000000: 21 3 2 3 3 3 2 2 3 2 3 2 3 3 3 3 3 2 3 2 3 3 3 3 2 3 2 3 3 3 4 5 6 + 1 10000000: 10 3 3 3 3 2 3 3 2 3 2 2 3 2 2 3 3 3 2 3 3 2 3 2 2 2 2 3 2 3 4 5 6 + 1 10000000: 21 2 3 3 3 3 3 3 3 2 2 3 3 3 3 3 3 2 3 2 3 3 3 3 2 2 3 3 3 3 3 4 5 6 + 1 10000000: 35 3 3 3 3 2 2 3 3 3 3 3 3 3 3 3 2 3 3 2 3 2 3 2 3 3 2 3 2 3 3 4 5 6 +Locations + 1: 0x206f main.fib :0 s=0 + 2: 0x2096 main.fib :0 s=0 + 3: 0x207a main.fib :0 s=0 + 4: 0x2134 main.main :0 s=0 + 5: 0x2df2f runtime.main :0 s=0 + 6: 0x5da90 runtime.goexit :0 s=0 + 7: 0x2085 main.fib :0 s=0 + 8: 0x2049 main.fib :0 s=0 + 9: 0x2040 main.fib :0 s=0 + 10: 0x204f main.fib :0 s=0 + 11: 0x2080 main.fib :0 s=0 + 12: 0x20a9 main.fib :0 s=0 + 13: 0x2058 main.fib :0 s=0 + 14: 0x20a4 main.fib :0 s=0 + 15: 0x20a1 main.fib :0 s=0 + 16: 0x2097 main.fib :0 s=0 + 17: 0x208a main.fib :0 s=0 + 18: 0x2072 main.fib :0 s=0 + 19: 0x206b main.fib :0 s=0 + 20: 0x2053 main.fib :0 s=0 + 21: 0x209c main.fib :0 s=0 + 22: 0x2092 main.fib :0 s=0 + 23: 0x5eecb runtime.mach_semaphore_signal :0 s=0 + 24: 0x29bef runtime.mach_semrelease :0 s=0 + 25: 0x28f29 runtime.semawakeup :0 s=0 + 26: 0xefae runtime.notewakeup :0 s=0 + 27: 0x32109 runtime.startm :0 s=0 + 28: 0x32468 runtime.wakep :0 s=0 + 29: 0x332ef runtime.resetspinning :0 s=0 + 30: 0x3374d runtime.schedule :0 s=0 + 31: 0x33b09 runtime.goschedImpl :0 s=0 + 32: 0x33ba1 runtime.gopreempt_m :0 s=0 + 33: 0x44511 runtime.newstack :0 s=0 + 34: 0x5b4fe runtime.morestack :0 s=0 + 35: 0x208e main.fib :0 s=0 + 36: 0x206c main.fib :0 s=0 + 37: 0x205e main.fib :0 s=0 + 38: 0x2076 main.fib :0 s=0 + 39: 0x207b main.fib :0 s=0 + 40: 0x20ad main.fib :0 s=0 + 41: 0x2067 main.fib :0 s=0 +Mappings diff --git a/renderer/flamegraph.go b/renderer/flamegraph.go new file mode 100644 index 0000000..655d867 --- /dev/null +++ b/renderer/flamegraph.go @@ -0,0 +1,77 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package renderer + +import ( + "bytes" + "errors" + "os" + "os/exec" +) + +var errNoPerlScript = errors.New("Cannot find flamegraph scripts in the PATH or current " + + "directory. You can download the script at https://github.com/brendangregg/FlameGraph. " + + "These scripts should be added to your PATH or in the directory where go-torch is executed. " + + "Alternatively, you can run go-torch with the --raw flag.") + +var ( + stackCollapseScripts = []string{"stackcollapse.pl", "./stackcollapse.pl", "./FlameGraph/stackcollapse.pl"} + flameGraphScripts = []string{"flamegraph.pl", "./flamegraph.pl", "./FlameGraph/flamegraph.pl", "flame-graph-gen"} +) + +// findInPath returns the first path that is found in PATH. +func findInPath(paths []string) string { + for _, v := range paths { + if path, err := exec.LookPath(v); err == nil { + return path + } + } + return "" +} + +// runScript runs scriptName with the given arguments, and stdin set to inData. +// It returns the stdout on success. +func runScript(scriptName string, args []string, inData []byte) ([]byte, error) { + cmd := exec.Command(scriptName, args...) + cmd.Stdin = bytes.NewReader(inData) + cmd.Stderr = os.Stderr + return cmd.Output() +} + +// CollapseStacks runs the flamegraph's collapse stacks script. +func CollapseStacks(stacks []byte) ([]byte, error) { + stackCollapse := findInPath(stackCollapseScripts) + if stackCollapse == "" { + return nil, errNoPerlScript + } + + return runScript(stackCollapse, nil, stacks) +} + +// GenerateFlameGraph runs the flamegraph script to generate a flame graph SVG. +func GenerateFlameGraph(graphInput []byte) ([]byte, error) { + flameGraph := findInPath(flameGraphScripts) + if flameGraph == "" { + return nil, errNoPerlScript + } + + return runScript(flameGraph, nil, graphInput) +} diff --git a/renderer/flamegraph_test.go b/renderer/flamegraph_test.go new file mode 100644 index 0000000..8b43d03 --- /dev/null +++ b/renderer/flamegraph_test.go @@ -0,0 +1,128 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package renderer + +import ( + "os" + "path/filepath" + "testing" +) + +const testData = "1 2 3 4 5\n" + +func TestFindInPatch(t *testing.T) { + const realCmd1 = "ls" + const realCmd2 = "cat" + const fakeCmd1 = "should-not-find-this" + const fakeCmd2 = "not-going-to-exist" + + tests := []struct { + paths []string + expected string + }{ + { + paths: []string{}, + }, + { + paths: []string{realCmd1}, + expected: realCmd1, + }, + { + paths: []string{fakeCmd1, realCmd1}, + expected: realCmd1, + }, + { + paths: []string{fakeCmd1, realCmd1, fakeCmd2, realCmd2}, + expected: realCmd1, + }, + } + + for _, tt := range tests { + got := findInPath(tt.paths) + var gotFile string + if got != "" { + gotFile = filepath.Base(got) + } + if gotFile != tt.expected { + t.Errorf("findInPaths(%v) got %v, want %v", tt.paths, gotFile, tt.expected) + } + + // Verify that the returned path exists. + if got != "" { + _, err := os.Stat(got) + if err != nil { + t.Errorf("returned path %v failed to stat: %v", got, err) + + } + } + } +} + +func TestRunScriptNoInput(t *testing.T) { + out, err := runScript("echo", []string{"1", "2", "3"}, nil) + if err != nil { + t.Fatalf("run echo failed: %v", err) + } + + const want = "1 2 3\n" + if string(out) != want { + t.Errorf("Got unexpected output:\n got %v\n want %v", string(out), want) + } +} + +type scriptFn func([]byte) ([]byte, error) + +func testScriptFound(t *testing.T, sliceToStub []string, f scriptFn) { + // Stub out the scripts that it looks at for the test + origVal := sliceToStub[0] + sliceToStub[0] = "cat" + defer func() { sliceToStub[0] = origVal }() + + out, err := f([]byte(testData)) + if err != nil { + t.Fatalf("Failed to run script: %v", err) + } + + if string(out) != testData { + t.Errorf("Got unexpected output:\n got %v\n want %v", string(out), testData) + } +} + +func testScriptNotFound(t *testing.T, sliceToStub *[]string, f scriptFn) { + origVal := *sliceToStub + *sliceToStub = []string{} + defer func() { *sliceToStub = origVal }() + + _, err := f([]byte(testData)) + if err != errNoPerlScript { + t.Errorf("Unexpected error:\n got %v\n want %v", err, errNoPerlScript) + } +} + +func TestCollapseStacks(t *testing.T) { + testScriptFound(t, stackCollapseScripts, CollapseStacks) + testScriptNotFound(t, &stackCollapseScripts, CollapseStacks) +} + +func TestGenerateFlameGraph(t *testing.T) { + testScriptFound(t, flameGraphScripts, GenerateFlameGraph) + testScriptNotFound(t, &flameGraphScripts, GenerateFlameGraph) +} diff --git a/graph/mocks.go b/renderer/renderer.go similarity index 56% rename from graph/mocks.go rename to renderer/renderer.go index 4ee7a91..3a280d1 100644 --- a/graph/mocks.go +++ b/renderer/renderer.go @@ -18,54 +18,30 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -package graph +package renderer import ( - ggv "github.com/awalterschulze/gographviz" - "github.com/stretchr/testify/mock" -) - -type mockSearcher struct { - mock.Mock -} - -func (m *mockSearcher) dfs(args searchArgs) { - m.Called(args) -} + "bytes" + "fmt" + "io" + "strings" -type mockCollectionGetter struct { - mock.Mock -} - -func (m *mockCollectionGetter) generateNodeToOutEdges(_a0 *ggv.Graph) map[string][]*ggv.Edge { - ret := m.Called(_a0) - - var r0 map[string][]*ggv.Edge - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string][]*ggv.Edge) - } - - return r0 -} -func (m *mockCollectionGetter) getInDegreeZeroNodes(_a0 *ggv.Graph) []string { - ret := m.Called(_a0) + "github.com/uber/go-torch/stack" +) - var r0 []string - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) +// ToFlameInput convers the given stack samples to flame graph input. +func ToFlameInput(samples []*stack.Sample) ([]byte, error) { + buf := &bytes.Buffer{} + for _, s := range samples { + if err := renderSample(buf, s); err != nil { + return nil, err + } } - - return r0 + return buf.Bytes(), nil } -type mockPathStringer struct { - mock.Mock -} - -func (m *mockPathStringer) pathAsString(_a0 []ggv.Edge, _a1 map[string]*ggv.Node) string { - ret := m.Called(_a0, _a1) - - r0 := ret.Get(0).(string) - - return r0 +// renderSample renders a single stack sample as flame graph input. +func renderSample(w io.Writer, s *stack.Sample) error { + _, err := fmt.Fprintf(w, "%s %v\n", strings.Join(s.Funcs, ";"), s.Count) + return err } diff --git a/visualization/mocks.go b/renderer/renderer_test.go similarity index 57% rename from visualization/mocks.go rename to renderer/renderer_test.go index 4c5ee03..34b4db3 100644 --- a/visualization/mocks.go +++ b/renderer/renderer_test.go @@ -18,58 +18,29 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -package visualization +package renderer import ( - "os/exec" + "reflect" + "testing" - "github.com/stretchr/testify/mock" + "github.com/uber/go-torch/stack" ) -type mockOSWrapper struct { - mock.Mock -} - -func (m *mockOSWrapper) execLookPath(_a0 string) (string, error) { - println(_a0) - ret := m.Called(_a0) - - r0 := ret.Get(0).(string) - r1 := ret.Error(1) - - return r0, r1 -} -func (m *mockOSWrapper) cmdOutput(_a0 *exec.Cmd) ([]byte, error) { - ret := m.Called(_a0) - - var r0 []byte - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) +func TestToFlameInput(t *testing.T) { + samples := []*stack.Sample{ + {Funcs: []string{"func1", "func2"}, Count: 10}, + {Funcs: []string{"func3"}, Count: 8}, + {Funcs: []string{"func4", "func5", "func6"}, Count: 3}, } - r1 := ret.Error(1) - - return r0, r1 -} + expected := "func1;func2 10\nfunc3 8\nfunc4;func5;func6 3\n" -type mockExecutor struct { - mock.Mock -} - -func (m *mockExecutor) createFile(_a0 string, _a1 []byte) error { - ret := m.Called(_a0, _a1) - - r0 := ret.Error(0) - - return r0 -} -func (m *mockExecutor) runPerlScript(_a0 string) ([]byte, error) { - ret := m.Called(_a0) - - var r0 []byte - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) + out, err := ToFlameInput(samples) + if err != nil { + t.Fatalf("ToFlameInput failed: %v", err) } - r1 := ret.Error(1) - return r0, r1 + if !reflect.DeepEqual(expected, string(out)) { + t.Errorf("ToFlameInput failed:\n got %s\n want %s", out, expected) + } } diff --git a/stack/sample.go b/stack/sample.go new file mode 100644 index 0000000..048f63c --- /dev/null +++ b/stack/sample.go @@ -0,0 +1,28 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package stack + +// Sample represents the sample count for a specific call stack. +type Sample struct { + // Funcs is parent first. + Funcs []string + Count int +} diff --git a/visualization/visualization.go b/visualization/visualization.go deleted file mode 100644 index 2343ba5..0000000 --- a/visualization/visualization.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2015 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// Package visualization handles the generation of the -// flame graph visualization. -package visualization - -import ( - "errors" - "fmt" - "os" - "os/exec" - "strings" - - log "github.com/Sirupsen/logrus" -) - -var errNoPerlScript = errors.New("Cannot find flamegraph script in the PATH or current " + - "directory. You can download the script at https://github.com/brendangregg/FlameGraph. " + - "Alternatively, you can run go-torch with the --raw flag.") - -// Visualizer takes a graph in the format specified at -// https://github.com/brendangregg/FlameGraph and creates a svg flame graph -// using Brendan Gregg's flame graph perl script -type Visualizer interface { - GenerateFlameGraph(string, string, bool) error -} - -type defaultVisualizer struct { - executor -} - -type osWrapper interface { - execLookPath(string) (string, error) - cmdOutput(*exec.Cmd) ([]byte, error) -} - -type defaultOSWrapper struct{} - -type executor interface { - createFile(string, []byte) error - runPerlScript(string) ([]byte, error) -} - -type defaultExecutor struct { - osWrapper -} - -func newExecutor() executor { - return &defaultExecutor{ - osWrapper: new(defaultOSWrapper), - } -} - -// NewVisualizer returns a visualizer struct with default fileCreator -func NewVisualizer() Visualizer { - return &defaultVisualizer{ - executor: newExecutor(), - } -} - -// GenerateFlameGraph is the standard implementation of Visualizer -func (v *defaultVisualizer) GenerateFlameGraph(graphInput, outputFilePath string, stdout bool) error { - out, err := v.executor.runPerlScript(graphInput) - if err != nil { - return err - } - if stdout { - fmt.Println(string(out)) - log.Info("flame graph has been printed to stdout") - return nil - } - if err = v.executor.createFile(outputFilePath, out); err != nil { - return err - } - log.Info("flame graph has been created as " + outputFilePath) - - return nil -} - -// runPerlScript checks whether the flamegraph script exists in the PATH or current directory and -// then executes it with the graphInput. -func (e *defaultExecutor) runPerlScript(graphInput string) ([]byte, error) { - cwd, err := os.Getwd() - if err != nil { - return nil, err - } - possibilities := []string{"flamegraph.pl", cwd + "/flamegraph.pl", "flame-graph-gen"} - perlScript := "" - for _, path := range possibilities { - perlScript, err = e.osWrapper.execLookPath(path) - // found a valid script - if err == nil { - break - } - } - if err != nil { - return nil, errNoPerlScript - } - cmd := exec.Command(perlScript, os.Stdin.Name()) - cmd.Stdin = strings.NewReader(graphInput) - out, err := e.osWrapper.cmdOutput(cmd) - return out, err -} - -// execLookPath is a tiny wrapper around exec.LookPath to enable test mocking -func (w *defaultOSWrapper) execLookPath(path string) (fullPath string, err error) { - return exec.LookPath(path) -} - -// cmdOutput is a tiny wrapper around cmd.Output to enable test mocking -func (w *defaultOSWrapper) cmdOutput(cmd *exec.Cmd) ([]byte, error) { - return cmd.Output() -} - -// createFile creates a file at a given path with given contents. If a file -// already exists at the path, it will be overwritten and replaced. -func (e *defaultExecutor) createFile(filePath string, fileContents []byte) error { - os.Remove(filePath) - file, err := os.Create(filePath) - if err != nil { - return err - } - defer file.Close() - - _, err = file.Write(fileContents) - if err != nil { - return err - } - return nil -} diff --git a/visualization/visualization_test.go b/visualization/visualization_test.go deleted file mode 100644 index aed1316..0000000 --- a/visualization/visualization_test.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) 2015 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package visualization - -import ( - "errors" - "io/ioutil" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestCreateFile(t *testing.T) { - new(defaultExecutor).createFile(".text.svg", []byte("the contents")) - - // teardown - defer os.Remove(".text.svg") - - actualContents, err := ioutil.ReadFile(".text.svg") - assert.NoError(t, err) - assert.Equal(t, "the contents", string(actualContents)) -} - -func TestCreateFileOverwriteExisting(t *testing.T) { - new(defaultExecutor).createFile(".text.svg", []byte("delete me")) - new(defaultExecutor).createFile(".text.svg", []byte("correct answer")) - - // teardown - defer os.Remove(".text.svg") - - actualContents, err := ioutil.ReadFile(".text.svg") - assert.NoError(t, err) - assert.Equal(t, "correct answer", string(actualContents)) -} - -func TestGenerateFlameGraph(t *testing.T) { - mockExecutor := new(mockExecutor) - visualizer := defaultVisualizer{ - executor: mockExecutor, - } - - graphInput := "N4;N5 1\nN4;N6;N5 8\n" - - mockExecutor.On("runPerlScript", graphInput).Return([]byte(""), nil).Once() - mockExecutor.On("createFile", ".text.svg", mock.AnythingOfType("[]uint8")).Return(nil).Once() - - visualizer.GenerateFlameGraph(graphInput, ".text.svg", false) - - mockExecutor.AssertExpectations(t) -} - -func TestGenerateFlameGraphPrintsToStdout(t *testing.T) { - mockExecutor := new(mockExecutor) - visualizer := defaultVisualizer{ - executor: mockExecutor, - } - graphInput := "N4;N5 1\nN4;N6;N5 8\n" - mockExecutor.On("runPerlScript", graphInput).Return([]byte(""), nil).Once() - visualizer.GenerateFlameGraph(graphInput, ".text.svg", true) - - mockExecutor.AssertNotCalled(t, "createFile") - mockExecutor.AssertExpectations(t) -} - -// Underlying errors can occur in runPerlScript(). This test ensures that errors -// like a missing flamegraph.pl script or malformed input are propagated. -func TestGenerateFlameGraphExecError(t *testing.T) { - mockExecutor := new(mockExecutor) - visualizer := defaultVisualizer{ - executor: mockExecutor, - } - mockExecutor.On("runPerlScript", "").Return(nil, errors.New("bad input")).Once() - - err := visualizer.GenerateFlameGraph("", ".text.svg", false) - assert.Error(t, err) - mockExecutor.AssertNotCalled(t, "createFile") - mockExecutor.AssertExpectations(t) -} - -func TestRunPerlScriptDoesExist(t *testing.T) { - mockOSWrapper := new(mockOSWrapper) - executor := defaultExecutor{ - osWrapper: mockOSWrapper, - } - cwd, err := os.Getwd() - if err != nil { - t.Fatal(err.Error()) - } - mockOSWrapper.On("execLookPath", "flamegraph.pl").Return("", errors.New("DNE")).Once() - mockOSWrapper.On("execLookPath", cwd+"/flamegraph.pl").Return("", errors.New("DNE")).Once() - mockOSWrapper.On("execLookPath", "flame-graph-gen").Return("/somepath/flame-graph-gen", nil).Once() - - mockOSWrapper.On("cmdOutput", mock.AnythingOfType("*exec.Cmd")).Return([]byte("output"), nil).Once() - - out, err := executor.runPerlScript("some graph input") - - assert.Equal(t, []byte("output"), out) - assert.NoError(t, err) - mockOSWrapper.AssertExpectations(t) -} - -func TestRunPerlScriptDoesNotExist(t *testing.T) { - mockOSWrapper := new(mockOSWrapper) - executor := defaultExecutor{ - osWrapper: mockOSWrapper, - } - cwd, err := os.Getwd() - if err != nil { - t.Fatal(err.Error()) - } - mockOSWrapper.On("execLookPath", "flamegraph.pl").Return("", errors.New("DNE")).Once() - mockOSWrapper.On("execLookPath", cwd+"/flamegraph.pl").Return("", errors.New("DNE")).Once() - mockOSWrapper.On("execLookPath", "flame-graph-gen").Return("", errors.New("DNE")).Once() - - out, err := executor.runPerlScript("some graph input") - - assert.Equal(t, 0, len(out)) - assert.Error(t, err) - mockOSWrapper.AssertExpectations(t) -} - -// Smoke test the NewVisualizer method -func TestNewVisualizer(t *testing.T) { - assert.NotNil(t, NewVisualizer()) -}