| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Preprofile handles pprof files. |
| // |
| // Usage: |
| // |
| // go tool preprofile [-v] [-o output] [-i (pprof)input] |
| // |
| // |
| |
| package main |
| |
| import ( |
| "bufio" |
| "flag" |
| "fmt" |
| "internal/profile" |
| "log" |
| "os" |
| "path/filepath" |
| "strconv" |
| ) |
| |
| // The current Go Compiler consumes significantly long compilation time when the PGO |
| // is enabled. To optimize the existing flow and reduce build time of multiple Go |
| // services, we create a standalone tool, PGO preprocessor, to extract information |
| // from collected profiling files and to cache the WeightedCallGraph in one time |
| // fashion. By adding the new tool to the Go compiler, it will reduce the time |
| // of repeated profiling file parsing and avoid WeightedCallGraph reconstruction |
| // in current Go Compiler. |
| // The format of the pre-processed output is as follows. |
| // |
| // Header |
| // caller_name |
| // callee_name |
| // "call site offset" "caller's start line number" "flat" "cum" "call edge weight" |
| // ... |
| // caller_name |
| // callee_name |
| // "call site offset" "caller's start line number" "flat" "cum" "call edge weight" |
| |
| func usage() { |
| fmt.Fprintf(os.Stderr, "MUST have (pprof) input file \n") |
| fmt.Fprintf(os.Stderr, "usage: go tool preprofile [-v] [-o output] [-i (pprof)input] \n\n") |
| flag.PrintDefaults() |
| os.Exit(2) |
| } |
| |
| type NodeMapKey struct { |
| CallerName string |
| CalleeName string |
| CallSiteOffset int // Line offset from function start line. |
| CallStartLine int // Start line of the function. Can be 0 which means missing. |
| } |
| |
| type Weights struct { |
| NFlat int64 |
| NCum int64 |
| EWeight int64 |
| } |
| |
| func readPprofFile(profileFile string, outputFile string, verbose bool) bool { |
| // open the pprof profile file |
| f, err := os.Open(profileFile) |
| if err != nil { |
| log.Fatal("failed to open file " + profileFile) |
| return false |
| } |
| defer f.Close() |
| p, err := profile.Parse(f) |
| if err != nil { |
| log.Fatal("failed to Parse profile file.") |
| return false |
| } |
| |
| if len(p.Sample) == 0 { |
| // We accept empty profiles, but there is nothing to do. |
| return false |
| } |
| |
| valueIndex := -1 |
| for i, s := range p.SampleType { |
| // Samples count is the raw data collected, and CPU nanoseconds is just |
| // a scaled version of it, so either one we can find is fine. |
| if (s.Type == "samples" && s.Unit == "count") || |
| (s.Type == "cpu" && s.Unit == "nanoseconds") { |
| valueIndex = i |
| break |
| } |
| } |
| |
| if valueIndex == -1 { |
| log.Fatal("failed to find CPU samples count or CPU nanoseconds value-types in profile.") |
| return false |
| } |
| |
| // The processing here is equivalent to cmd/compile/internal/pgo.createNamedEdgeMap. |
| g := profile.NewGraph(p, &profile.Options{ |
| SampleValue: func(v []int64) int64 { return v[valueIndex] }, |
| }) |
| |
| nFlat := make(map[string]int64) |
| nCum := make(map[string]int64) |
| |
| // Accummulate weights for the same node. |
| for _, n := range g.Nodes { |
| canonicalName := n.Info.Name |
| nFlat[canonicalName] += n.FlatValue() |
| nCum[canonicalName] += n.CumValue() |
| } |
| |
| TotalNodeWeight := int64(0) |
| TotalEdgeWeight := int64(0) |
| |
| NodeMap := make(map[NodeMapKey]*Weights) |
| NodeWeightMap := make(map[string]int64) |
| |
| for _, n := range g.Nodes { |
| TotalNodeWeight += n.FlatValue() |
| canonicalName := n.Info.Name |
| // Create the key to the nodeMapKey. |
| nodeinfo := NodeMapKey{ |
| CallerName: canonicalName, |
| CallSiteOffset: n.Info.Lineno - n.Info.StartLine, |
| CallStartLine: n.Info.StartLine, |
| } |
| |
| if nodeinfo.CallStartLine == 0 { |
| if verbose { |
| log.Println("[PGO] warning: " + canonicalName + " relative line number is missing from the profile") |
| } |
| } |
| |
| for _, e := range n.Out { |
| TotalEdgeWeight += e.WeightValue() |
| nodeinfo.CalleeName = e.Dest.Info.Name |
| if w, ok := NodeMap[nodeinfo]; ok { |
| w.EWeight += e.WeightValue() |
| } else { |
| weights := new(Weights) |
| weights.NFlat = nFlat[canonicalName] |
| weights.NCum = nCum[canonicalName] |
| weights.EWeight = e.WeightValue() |
| NodeMap[nodeinfo] = weights |
| } |
| } |
| } |
| |
| for _, n := range g.Nodes { |
| lineno := fmt.Sprintf("%v", n.Info.Lineno) |
| canonicalName := n.Info.Name + "-" + lineno |
| if _, ok := (NodeWeightMap)[canonicalName]; ok { |
| (NodeWeightMap)[canonicalName] += n.CumValue() |
| } else { |
| (NodeWeightMap)[canonicalName] = n.CumValue() |
| } |
| } |
| |
| var fNodeMap *os.File |
| if outputFile == "" { |
| fNodeMap = os.Stdout |
| } else { |
| dirPath := filepath.Dir(outputFile) |
| _, err := os.Stat(dirPath) |
| if err != nil { |
| log.Fatal("Directory does not exist: ", dirPath) |
| } |
| base := filepath.Base(outputFile) |
| outputFile = filepath.Join(dirPath, base) |
| |
| // write out NodeMap to a file |
| fNodeMap, err = os.Create(outputFile) |
| if err != nil { |
| log.Fatal("Error creating output file:", err) |
| return false |
| } |
| |
| defer fNodeMap.Close() // Close the file when done writing |
| } |
| |
| w := bufio.NewWriter(fNodeMap) |
| w.WriteString("GO PREPROFILE V1\n") |
| count := 1 |
| separator := " " |
| for key, element := range NodeMap { |
| line := key.CallerName + "\n" |
| w.WriteString(line) |
| line = key.CalleeName + "\n" |
| w.WriteString(line) |
| line = strconv.Itoa(key.CallSiteOffset) |
| line = line + separator + strconv.Itoa(key.CallStartLine) |
| line = line + separator + strconv.FormatInt(element.NFlat, 10) |
| line = line + separator + strconv.FormatInt(element.NCum, 10) |
| line = line + separator + strconv.FormatInt(element.EWeight, 10) + "\n" |
| w.WriteString(line) |
| w.Flush() |
| count += 1 |
| } |
| |
| if TotalNodeWeight == 0 || TotalEdgeWeight == 0 { |
| return false |
| } |
| |
| return true |
| } |
| |
| var dumpCode = flag.String("o", "", "dump output file ") |
| var input = flag.String("i", "", "input pprof file ") |
| var verbose = flag.Bool("v", false, "verbose log") |
| |
| func main() { |
| log.SetFlags(0) |
| log.SetPrefix("preprofile: ") |
| |
| flag.Usage = usage |
| flag.Parse() |
| if *input == "" { |
| usage() |
| } else { |
| readPprofFile(*input, *dumpCode, *verbose) |
| } |
| } |