pkg/display/graph/flamegraph/flamegraph.go (125 lines of code) (raw):

// Licensed to Apache Software Foundation (ASF) under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Apache Software Foundation (ASF) licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package flamegraph import ( "bytes" "fmt" "html/template" "os" "path/filepath" "time" "github.com/apache/skywalking-cli/internal/logger" api "skywalking.apache.org/repo/goapi/query" "github.com/urfave/cli/v2" ) func DisplayByTrace(ctx *cli.Context, analysis api.ProfileAnalyzation) error { trees := make([]*ProfilingDataTree, 0) for _, tree := range analysis.Trees { elements := make([]ProfilingDataStackElement, 0) for _, e := range tree.Elements { elements = append(elements, &traceStackElementAdapter{e}) } trees = append(trees, &ProfilingDataTree{Elements: elements}) } return display(ctx, trees) } func DisplayByEBPF(ctx *cli.Context, analysis *api.EBPFProfilingAnalyzation) error { trees := make([]*ProfilingDataTree, 0) for _, tree := range analysis.Trees { elements := make([]ProfilingDataStackElement, 0) for _, e := range tree.Elements { elements = append(elements, &eBPFStackElementAdapter{e}) } trees = append(trees, &ProfilingDataTree{Elements: elements}) } return display(ctx, trees) } func display(_ *cli.Context, trees []*ProfilingDataTree) error { if len(trees) == 0 { return fmt.Errorf("could not find the analysis data") } // generate flame graph file path for write flameGraphPath, err := generateFlameGraphFile() if err != nil { return err } // build data data := make(map[string]interface{}) elements, maxDepth := buildFlameGraphElements(trees) data["elements"] = elements data["maxDepth"] = maxDepth + 1 data["canvasHeight"] = maxDepth*16 + 30 // render template return renderFlameGraphAndWrite(flameGraphPath, data) } func renderFlameGraphAndWrite(path string, data map[string]interface{}) error { // render template var b bytes.Buffer tmpl, err := template.New("flameGraphTemplate").Parse(flameGraphHTML) if err != nil { return fmt.Errorf("failed to parse flame graph template: %v", err) } if err = tmpl.Execute(&b, data); err != nil { return fmt.Errorf("failed to render flame graph: %v", err) } // write to file file, err := os.Create(path) if err != nil { return fmt.Errorf("could not create the flame graph file, %v", err) } _, err = file.Write(b.Bytes()) if err != nil { return fmt.Errorf("could not write the flame graph to file, %v", err) } logger.Log.Infof("success write the flame graph to: %s", path) return nil } func generateFlameGraphFile() (string, error) { wd, err := os.Getwd() if err != nil { return "", fmt.Errorf("could not get current directory: %v", err) } flameGraphPath := filepath.Join(wd, buildFileName()) return flameGraphPath, nil } func buildFileName() string { return fmt.Sprintf("flame_graph_%d.html", time.Now().Unix()) } func buildFlameGraphElements(trees []*ProfilingDataTree) (result []*StackElement, maxDepth int64) { // adding the root element root := &StackElement{Symbol: "all"} result = append(result, root) // build elements var left int64 for _, t := range trees { result, left = buildFlameGraphChildElements(result, t.Elements, nil, left) } // calculate the root total count for _, r := range result { if r.ParentID == "0" { root.Count += r.Count } if maxDepth < r.Depth { maxDepth = r.Depth } } return result, maxDepth } func buildFlameGraphChildElements(renderElements []*StackElement, dataElements []ProfilingDataStackElement, parent *StackElement, relativeLeft int64) (result []*StackElement, left int64) { parentID := "0" var depth int64 = 1 left = relativeLeft if parent != nil { parentID = parent.ID depth = parent.Depth + 1 left += parent.Left } es := findProfilingElementByParentID(dataElements, parentID) for _, element := range es { current := GenerateStackElementByProfilingData(element, depth, left) renderElements = append(renderElements, current) left += element.DumpCount() renderElements, _ = buildFlameGraphChildElements(renderElements, dataElements, current, relativeLeft) } return renderElements, left } func findProfilingElementByParentID(elements []ProfilingDataStackElement, parentID string) []ProfilingDataStackElement { children := make([]ProfilingDataStackElement, 0) for _, e := range elements { if e.ParentID() == parentID { children = append(children, e) } } return children }