cmd/compile,cmd/preprofile: move logic to shared common package

The processing performed in cmd/preprofile is a simple version of the
same initial processing performed by cmd/compile/internal/pgo. Refactor
this processing into the new IR-independent cmd/internal/pgo package.

Now cmd/preprofile and cmd/compile run the same code for initial
processing of a pprof profile, guaranteeing that they always stay in
sync.

Since it is now trivial, this CL makes one change to the serialization
format: the entries are ordered by weight. This allows us to avoid
sorting ByWeight on deserialization.

Impact on PGO parsing when compiling cmd/compile with PGO:

* Without preprocessing: PGO parsing ~13.7% of CPU time
* With preprocessing (unsorted): ~2.9% of CPU time (sorting ~1.7%)
* With preprocessing (sorted): ~1.3% of CPU time

The remaining 1.3% of CPU time approximately breaks down as:

* ~0.5% parsing the preprocessed profile
* ~0.7% building weighted IR call graph
  * ~0.5% walking function IR to find direct calls
  * ~0.2% performing lookups for indirect calls targets

For #58102.

Change-Id: Iaba425ea30b063ca195fb2f7b29342961c8a64c2
Reviewed-on: https://go-review.googlesource.com/c/go/+/569337
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
Michael Pratt 2024-03-04 13:29:39 -05:00 committed by Gopher Robot
parent 2860e01853
commit 63deaf00ea
16 changed files with 691 additions and 449 deletions

View File

@ -7,10 +7,11 @@ package devirtualize
import (
"cmd/compile/internal/base"
"cmd/compile/internal/ir"
"cmd/compile/internal/pgo"
pgoir "cmd/compile/internal/pgo"
"cmd/compile/internal/typecheck"
"cmd/compile/internal/types"
"cmd/internal/obj"
"cmd/internal/pgo"
"cmd/internal/src"
"testing"
)
@ -32,32 +33,32 @@ func makePos(b *src.PosBase, line, col uint) src.XPos {
}
type profileBuilder struct {
p *pgo.Profile
p *pgoir.Profile
}
func newProfileBuilder() *profileBuilder {
// findHotConcreteCallee only uses pgo.Profile.WeightedCG, so we're
// findHotConcreteCallee only uses pgoir.Profile.WeightedCG, so we're
// going to take a shortcut and only construct that.
return &profileBuilder{
p: &pgo.Profile{
WeightedCG: &pgo.IRGraph{
IRNodes: make(map[string]*pgo.IRNode),
p: &pgoir.Profile{
WeightedCG: &pgoir.IRGraph{
IRNodes: make(map[string]*pgoir.IRNode),
},
},
}
}
// Profile returns the constructed profile.
func (p *profileBuilder) Profile() *pgo.Profile {
func (p *profileBuilder) Profile() *pgoir.Profile {
return p.p
}
// NewNode creates a new IRNode and adds it to the profile.
//
// fn may be nil, in which case the node will set LinkerSymbolName.
func (p *profileBuilder) NewNode(name string, fn *ir.Func) *pgo.IRNode {
n := &pgo.IRNode{
OutEdges: make(map[pgo.NamedCallEdge]*pgo.IREdge),
func (p *profileBuilder) NewNode(name string, fn *ir.Func) *pgoir.IRNode {
n := &pgoir.IRNode{
OutEdges: make(map[pgo.NamedCallEdge]*pgoir.IREdge),
}
if fn != nil {
n.AST = fn
@ -69,13 +70,13 @@ func (p *profileBuilder) NewNode(name string, fn *ir.Func) *pgo.IRNode {
}
// Add a new call edge from caller to callee.
func addEdge(caller, callee *pgo.IRNode, offset int, weight int64) {
func addEdge(caller, callee *pgoir.IRNode, offset int, weight int64) {
namedEdge := pgo.NamedCallEdge{
CallerName: caller.Name(),
CalleeName: callee.Name(),
CallSiteOffset: offset,
}
irEdge := &pgo.IREdge{
irEdge := &pgoir.IREdge{
Src: caller,
Dst: callee,
CallSiteOffset: offset,

View File

@ -36,10 +36,11 @@ import (
"cmd/compile/internal/inline/inlheur"
"cmd/compile/internal/ir"
"cmd/compile/internal/logopt"
"cmd/compile/internal/pgo"
pgoir "cmd/compile/internal/pgo"
"cmd/compile/internal/typecheck"
"cmd/compile/internal/types"
"cmd/internal/obj"
"cmd/internal/pgo"
)
// Inlining budget parameters, gathered in one place
@ -58,11 +59,11 @@ const (
var (
// List of all hot callee nodes.
// TODO(prattmic): Make this non-global.
candHotCalleeMap = make(map[*pgo.IRNode]struct{})
candHotCalleeMap = make(map[*pgoir.IRNode]struct{})
// List of all hot call sites. CallSiteInfo.Callee is always nil.
// TODO(prattmic): Make this non-global.
candHotEdgeMap = make(map[pgo.CallSiteInfo]struct{})
candHotEdgeMap = make(map[pgoir.CallSiteInfo]struct{})
// Threshold in percentage for hot callsite inlining.
inlineHotCallSiteThresholdPercent float64
@ -78,7 +79,7 @@ var (
)
// PGOInlinePrologue records the hot callsites from ir-graph.
func PGOInlinePrologue(p *pgo.Profile) {
func PGOInlinePrologue(p *pgoir.Profile) {
if base.Debug.PGOInlineCDFThreshold != "" {
if s, err := strconv.ParseFloat(base.Debug.PGOInlineCDFThreshold, 64); err == nil && s >= 0 && s <= 100 {
inlineCDFHotCallSiteThresholdPercent = s
@ -103,7 +104,7 @@ func PGOInlinePrologue(p *pgo.Profile) {
}
// mark hot call sites
if caller := p.WeightedCG.IRNodes[n.CallerName]; caller != nil && caller.AST != nil {
csi := pgo.CallSiteInfo{LineOffset: n.CallSiteOffset, Caller: caller.AST}
csi := pgoir.CallSiteInfo{LineOffset: n.CallSiteOffset, Caller: caller.AST}
candHotEdgeMap[csi] = struct{}{}
}
}
@ -120,7 +121,7 @@ func PGOInlinePrologue(p *pgo.Profile) {
// (currently only used in debug prints) (in case of equal weights,
// comparing with the threshold may not accurately reflect which nodes are
// considered hot).
func hotNodesFromCDF(p *pgo.Profile) (float64, []pgo.NamedCallEdge) {
func hotNodesFromCDF(p *pgoir.Profile) (float64, []pgo.NamedCallEdge) {
cum := int64(0)
for i, n := range p.NamedEdgeMap.ByWeight {
w := p.NamedEdgeMap.Weight[n]
@ -136,7 +137,7 @@ func hotNodesFromCDF(p *pgo.Profile) (float64, []pgo.NamedCallEdge) {
}
// CanInlineFuncs computes whether a batch of functions are inlinable.
func CanInlineFuncs(funcs []*ir.Func, profile *pgo.Profile) {
func CanInlineFuncs(funcs []*ir.Func, profile *pgoir.Profile) {
if profile != nil {
PGOInlinePrologue(profile)
}
@ -224,7 +225,7 @@ func GarbageCollectUnreferencedHiddenClosures() {
// possibility that a call to the function might have its score
// adjusted downwards. If 'verbose' is set, then print a remark where
// we boost the budget due to PGO.
func inlineBudget(fn *ir.Func, profile *pgo.Profile, relaxed bool, verbose bool) int32 {
func inlineBudget(fn *ir.Func, profile *pgoir.Profile, relaxed bool, verbose bool) int32 {
// Update the budget for profile-guided inlining.
budget := int32(inlineMaxBudget)
if profile != nil {
@ -246,7 +247,7 @@ func inlineBudget(fn *ir.Func, profile *pgo.Profile, relaxed bool, verbose bool)
// CanInline determines whether fn is inlineable.
// If so, CanInline saves copies of fn.Body and fn.Dcl in fn.Inl.
// fn and fn.Body will already have been typechecked.
func CanInline(fn *ir.Func, profile *pgo.Profile) {
func CanInline(fn *ir.Func, profile *pgoir.Profile) {
if fn.Nname == nil {
base.Fatalf("CanInline no nname %+v", fn)
}
@ -451,7 +452,7 @@ type hairyVisitor struct {
extraCallCost int32
usedLocals ir.NameSet
do func(ir.Node) bool
profile *pgo.Profile
profile *pgoir.Profile
}
func (v *hairyVisitor) tooHairy(fn *ir.Func) bool {
@ -768,7 +769,7 @@ func IsBigFunc(fn *ir.Func) bool {
// TryInlineCall returns an inlined call expression for call, or nil
// if inlining is not possible.
func TryInlineCall(callerfn *ir.Func, call *ir.CallExpr, bigCaller bool, profile *pgo.Profile) *ir.InlinedCallExpr {
func TryInlineCall(callerfn *ir.Func, call *ir.CallExpr, bigCaller bool, profile *pgoir.Profile) *ir.InlinedCallExpr {
if base.Flag.LowerL == 0 {
return nil
}
@ -804,7 +805,7 @@ func TryInlineCall(callerfn *ir.Func, call *ir.CallExpr, bigCaller bool, profile
// inlCallee takes a function-typed expression and returns the underlying function ONAME
// that it refers to if statically known. Otherwise, it returns nil.
func inlCallee(caller *ir.Func, fn ir.Node, profile *pgo.Profile) (res *ir.Func) {
func inlCallee(caller *ir.Func, fn ir.Node, profile *pgoir.Profile) (res *ir.Func) {
fn = ir.StaticValue(fn)
switch fn.Op() {
case ir.OMETHEXPR:
@ -877,8 +878,8 @@ func inlineCostOK(n *ir.CallExpr, caller, callee *ir.Func, bigCaller bool) (bool
// We'll also allow inlining of hot functions below inlineHotMaxBudget,
// but only in small functions.
lineOffset := pgo.NodeLineOffset(n, caller)
csi := pgo.CallSiteInfo{LineOffset: lineOffset, Caller: caller}
lineOffset := pgoir.NodeLineOffset(n, caller)
csi := pgoir.CallSiteInfo{LineOffset: lineOffset, Caller: caller}
if _, ok := candHotEdgeMap[csi]; !ok {
// Cold
return false, maxCost, metric
@ -1188,9 +1189,9 @@ func isAtomicCoverageCounterUpdate(cn *ir.CallExpr) bool {
return v
}
func PostProcessCallSites(profile *pgo.Profile) {
func PostProcessCallSites(profile *pgoir.Profile) {
if base.Debug.DumpInlCallSiteScores != 0 {
budgetCallback := func(fn *ir.Func, prof *pgo.Profile) (int32, bool) {
budgetCallback := func(fn *ir.Func, prof *pgoir.Profile) (int32, bool) {
v := inlineBudget(fn, prof, false, false)
return v, v == inlineHotMaxBudget
}
@ -1198,7 +1199,7 @@ func PostProcessCallSites(profile *pgo.Profile) {
}
}
func analyzeFuncProps(fn *ir.Func, p *pgo.Profile) {
func analyzeFuncProps(fn *ir.Func, p *pgoir.Profile) {
canInline := func(fn *ir.Func) { CanInline(fn, p) }
budgetForFunc := func(fn *ir.Func) int32 {
return inlineBudget(fn, p, true, false)

View File

@ -46,14 +46,9 @@ import (
"cmd/compile/internal/ir"
"cmd/compile/internal/typecheck"
"cmd/compile/internal/types"
"errors"
"cmd/internal/pgo"
"fmt"
"internal/profile"
"io"
"os"
"sort"
"strconv"
"strings"
)
// IRGraph is a call graph with nodes pointing to IRs of functions and edges
@ -82,7 +77,7 @@ type IRNode struct {
// Set of out-edges in the callgraph. The map uniquely identifies each
// edge based on the callsite and callee, for fast lookup.
OutEdges map[NamedCallEdge]*IREdge
OutEdges map[pgo.NamedCallEdge]*IREdge
}
// Name returns the symbol name of this function.
@ -102,23 +97,6 @@ type IREdge struct {
CallSiteOffset int // Line offset from function start line.
}
// NamedCallEdge identifies a call edge by linker symbol names and call site
// offset.
type NamedCallEdge struct {
CallerName string
CalleeName string
CallSiteOffset int // Line offset from function start line.
}
// NamedEdgeMap contains all unique call edges in the profile and their
// edge weight.
type NamedEdgeMap struct {
Weight map[NamedCallEdge]int64
// ByWeight lists all keys in Weight, sorted by edge weight.
ByWeight []NamedCallEdge
}
// CallSiteInfo captures call-site information and its caller/callee.
type CallSiteInfo struct {
LineOffset int // Line offset from function start line.
@ -129,33 +107,14 @@ type CallSiteInfo struct {
// Profile contains the processed PGO profile and weighted call graph used for
// PGO optimizations.
type Profile struct {
// Aggregated edge weights across the profile. This helps us determine
// the percentage threshold for hot/cold partitioning.
TotalWeight int64
// NamedEdgeMap contains all unique call edges in the profile and their
// edge weight.
NamedEdgeMap NamedEdgeMap
// Profile is the base data from the raw profile, without IR attribution.
*pgo.Profile
// WeightedCG represents the IRGraph built from profile, which we will
// update as part of inlining.
WeightedCG *IRGraph
}
var wantHdr = "GO PREPROFILE V1\n"
func isPreProfileFile(r *bufio.Reader) (bool, error) {
hdr, err := r.Peek(len(wantHdr))
if err == io.EOF {
// Empty file.
return false, nil
} else if err != nil {
return false, fmt.Errorf("error reading profile header: %w", err)
}
return string(hdr) == wantHdr, nil
}
// New generates a profile-graph from the profile or pre-processed profile.
func New(profileFile string) (*Profile, error) {
f, err := os.Open(profileFile)
@ -163,240 +122,42 @@ func New(profileFile string) (*Profile, error) {
return nil, fmt.Errorf("error opening profile: %w", err)
}
defer f.Close()
r := bufio.NewReader(f)
isPreProf, err := isPreProfileFile(r)
isSerialized, err := pgo.IsSerialized(r)
if err != nil {
return nil, fmt.Errorf("error processing profile header: %w", err)
}
if isPreProf {
profile, err := processPreprof(r)
var base *pgo.Profile
if isSerialized {
base, err = pgo.FromSerialized(r)
if err != nil {
return nil, fmt.Errorf("error processing preprocessed PGO profile: %w", err)
return nil, fmt.Errorf("error processing serialized PGO profile: %w", err)
}
return profile, nil
}
profile, err := processProto(r)
if err != nil {
return nil, fmt.Errorf("error processing pprof PGO profile: %w", err)
}
return profile, nil
}
// processProto generates a profile-graph from the profile.
func processProto(r io.Reader) (*Profile, error) {
p, err := profile.Parse(r)
if errors.Is(err, profile.ErrNoData) {
// Treat a completely empty file the same as a profile with no
// samples: nothing to do.
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("error parsing profile: %w", err)
}
if len(p.Sample) == 0 {
// We accept empty profiles, but there is nothing to do.
return nil, nil
}
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
} else {
base, err = pgo.FromPProf(r)
if err != nil {
return nil, fmt.Errorf("error processing pprof PGO profile: %w", err)
}
}
if valueIndex == -1 {
return nil, fmt.Errorf(`profile does not contain a sample index with value/type "samples/count" or cpu/nanoseconds"`)
}
g := profile.NewGraph(p, &profile.Options{
SampleValue: func(v []int64) int64 { return v[valueIndex] },
})
namedEdgeMap, totalWeight, err := createNamedEdgeMap(g)
if err != nil {
return nil, err
}
if totalWeight == 0 {
if base.TotalWeight == 0 {
return nil, nil // accept but ignore profile with no samples.
}
// Create package-level call graph with weights from profile and IR.
wg := createIRGraph(namedEdgeMap)
wg := createIRGraph(base.NamedEdgeMap)
return &Profile{
TotalWeight: totalWeight,
NamedEdgeMap: namedEdgeMap,
WeightedCG: wg,
Profile: base,
WeightedCG: wg,
}, nil
}
// processPreprof generates a profile-graph from the pre-processed profile.
func processPreprof(r io.Reader) (*Profile, error) {
namedEdgeMap, totalWeight, err := createNamedEdgeMapFromPreprocess(r)
if err != nil {
return nil, err
}
if totalWeight == 0 {
return nil, nil // accept but ignore profile with no samples.
}
// Create package-level call graph with weights from profile and IR.
wg := createIRGraph(namedEdgeMap)
return &Profile{
TotalWeight: totalWeight,
NamedEdgeMap: namedEdgeMap,
WeightedCG: wg,
}, nil
}
func postProcessNamedEdgeMap(weight map[NamedCallEdge]int64, weightVal int64) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
if weightVal == 0 {
return NamedEdgeMap{}, 0, nil // accept but ignore profile with no samples.
}
byWeight := make([]NamedCallEdge, 0, len(weight))
for namedEdge := range weight {
byWeight = append(byWeight, namedEdge)
}
sort.Slice(byWeight, func(i, j int) bool {
ei, ej := byWeight[i], byWeight[j]
if wi, wj := weight[ei], weight[ej]; wi != wj {
return wi > wj // want larger weight first
}
// same weight, order by name/line number
if ei.CallerName != ej.CallerName {
return ei.CallerName < ej.CallerName
}
if ei.CalleeName != ej.CalleeName {
return ei.CalleeName < ej.CalleeName
}
return ei.CallSiteOffset < ej.CallSiteOffset
})
edgeMap = NamedEdgeMap{
Weight: weight,
ByWeight: byWeight,
}
totalWeight = weightVal
return edgeMap, totalWeight, nil
}
// restore NodeMap information from a preprocessed profile.
// The reader can refer to the format of preprocessed profile in cmd/preprofile/main.go.
func createNamedEdgeMapFromPreprocess(r io.Reader) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
fileScanner := bufio.NewScanner(r)
fileScanner.Split(bufio.ScanLines)
weight := make(map[NamedCallEdge]int64)
if !fileScanner.Scan() {
if err := fileScanner.Err(); err != nil {
return NamedEdgeMap{}, 0, fmt.Errorf("error reading preprocessed profile: %w", err)
}
return NamedEdgeMap{}, 0, fmt.Errorf("preprocessed profile missing header")
}
if gotHdr := fileScanner.Text() + "\n"; gotHdr != wantHdr {
return NamedEdgeMap{}, 0, fmt.Errorf("preprocessed profile malformed header; got %q want %q", gotHdr, wantHdr)
}
for fileScanner.Scan() {
readStr := fileScanner.Text()
callerName := readStr
if !fileScanner.Scan() {
if err := fileScanner.Err(); err != nil {
return NamedEdgeMap{}, 0, fmt.Errorf("error reading preprocessed profile: %w", err)
}
return NamedEdgeMap{}, 0, fmt.Errorf("preprocessed profile entry missing callee")
}
calleeName := fileScanner.Text()
if !fileScanner.Scan() {
if err := fileScanner.Err(); err != nil {
return NamedEdgeMap{}, 0, fmt.Errorf("error reading preprocessed profile: %w", err)
}
return NamedEdgeMap{}, 0, fmt.Errorf("preprocessed profile entry missing weight")
}
readStr = fileScanner.Text()
split := strings.Split(readStr, " ")
if len(split) != 2 {
return NamedEdgeMap{}, 0, fmt.Errorf("preprocessed profile entry got %v want 2 fields", split)
}
co, _ := strconv.Atoi(split[0])
namedEdge := NamedCallEdge{
CallerName: callerName,
CalleeName: calleeName,
CallSiteOffset: co,
}
EWeight, _ := strconv.ParseInt(split[1], 10, 64)
weight[namedEdge] += EWeight
totalWeight += EWeight
}
return postProcessNamedEdgeMap(weight, totalWeight)
}
// createNamedEdgeMap builds a map of callsite-callee edge weights from the
// profile-graph.
//
// Caller should ignore the profile if totalWeight == 0.
func createNamedEdgeMap(g *profile.Graph) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
seenStartLine := false
// Process graph and build various node and edge maps which will
// be consumed by AST walk.
weight := make(map[NamedCallEdge]int64)
for _, n := range g.Nodes {
seenStartLine = seenStartLine || n.Info.StartLine != 0
canonicalName := n.Info.Name
// Create the key to the nodeMapKey.
namedEdge := NamedCallEdge{
CallerName: canonicalName,
CallSiteOffset: n.Info.Lineno - n.Info.StartLine,
}
for _, e := range n.Out {
totalWeight += e.WeightValue()
namedEdge.CalleeName = e.Dest.Info.Name
// Create new entry or increment existing entry.
weight[namedEdge] += e.WeightValue()
}
}
if !seenStartLine {
// TODO(prattmic): If Function.start_line is missing we could
// fall back to using absolute line numbers, which is better
// than nothing.
return NamedEdgeMap{}, 0, fmt.Errorf("profile missing Function.start_line data (Go version of profiled application too old? Go 1.20+ automatically adds this to profiles)")
}
return postProcessNamedEdgeMap(weight, totalWeight)
}
// initializeIRGraph builds the IRGraph by visiting all the ir.Func in decl list
// of a package.
func createIRGraph(namedEdgeMap NamedEdgeMap) *IRGraph {
func createIRGraph(namedEdgeMap pgo.NamedEdgeMap) *IRGraph {
g := &IRGraph{
IRNodes: make(map[string]*IRNode),
}
@ -425,7 +186,7 @@ func createIRGraph(namedEdgeMap NamedEdgeMap) *IRGraph {
// visitIR traverses the body of each ir.Func adds edges to g from ir.Func to
// any called function in the body.
func visitIR(fn *ir.Func, namedEdgeMap NamedEdgeMap, g *IRGraph) {
func visitIR(fn *ir.Func, namedEdgeMap pgo.NamedEdgeMap, g *IRGraph) {
name := ir.LinkFuncName(fn)
node, ok := g.IRNodes[name]
if !ok {
@ -442,7 +203,7 @@ func visitIR(fn *ir.Func, namedEdgeMap NamedEdgeMap, g *IRGraph) {
// createIRGraphEdge traverses the nodes in the body of ir.Func and adds edges
// between the callernode which points to the ir.Func and the nodes in the
// body.
func createIRGraphEdge(fn *ir.Func, callernode *IRNode, name string, namedEdgeMap NamedEdgeMap, g *IRGraph) {
func createIRGraphEdge(fn *ir.Func, callernode *IRNode, name string, namedEdgeMap pgo.NamedEdgeMap, g *IRGraph) {
ir.VisitList(fn.Body, func(n ir.Node) {
switch n.Op() {
case ir.OCALLFUNC:
@ -471,7 +232,7 @@ func NodeLineOffset(n ir.Node, fn *ir.Func) int {
// addIREdge adds an edge between caller and new node that points to `callee`
// based on the profile-graph and NodeMap.
func addIREdge(callerNode *IRNode, callerName string, call ir.Node, callee *ir.Func, namedEdgeMap NamedEdgeMap, g *IRGraph) {
func addIREdge(callerNode *IRNode, callerName string, call ir.Node, callee *ir.Func, namedEdgeMap pgo.NamedEdgeMap, g *IRGraph) {
calleeName := ir.LinkFuncName(callee)
calleeNode, ok := g.IRNodes[calleeName]
if !ok {
@ -481,7 +242,7 @@ func addIREdge(callerNode *IRNode, callerName string, call ir.Node, callee *ir.F
g.IRNodes[calleeName] = calleeNode
}
namedEdge := NamedCallEdge{
namedEdge := pgo.NamedCallEdge{
CallerName: callerName,
CalleeName: calleeName,
CallSiteOffset: NodeLineOffset(call, callerNode.AST),
@ -496,7 +257,7 @@ func addIREdge(callerNode *IRNode, callerName string, call ir.Node, callee *ir.F
}
if callerNode.OutEdges == nil {
callerNode.OutEdges = make(map[NamedCallEdge]*IREdge)
callerNode.OutEdges = make(map[pgo.NamedCallEdge]*IREdge)
}
callerNode.OutEdges[namedEdge] = edge
}
@ -519,7 +280,7 @@ var LookupFunc = func(fullName string) (*ir.Func, error) {
// TODO(prattmic): Devirtualization runs before inlining, so we can't devirtualize
// calls inside inlined call bodies. If we did add that, we'd need edges from
// inlined bodies as well.
func addIndirectEdges(g *IRGraph, namedEdgeMap NamedEdgeMap) {
func addIndirectEdges(g *IRGraph, namedEdgeMap pgo.NamedEdgeMap) {
// g.IRNodes is populated with the set of functions in the local
// package build by VisitIR. We want to filter for local functions
// below, but we also add unknown callees to IRNodes as we go. So make
@ -616,17 +377,12 @@ func addIndirectEdges(g *IRGraph, namedEdgeMap NamedEdgeMap) {
}
if callerNode.OutEdges == nil {
callerNode.OutEdges = make(map[NamedCallEdge]*IREdge)
callerNode.OutEdges = make(map[pgo.NamedCallEdge]*IREdge)
}
callerNode.OutEdges[key] = edge
}
}
// WeightInPercentage converts profile weights to a percentage.
func WeightInPercentage(value int64, total int64) float64 {
return (float64(value) / float64(total)) * 100
}
// PrintWeightedCallGraphDOT prints IRGraph in DOT format.
func (p *Profile) PrintWeightedCallGraphDOT(edgeThreshold float64) {
fmt.Printf("\ndigraph G {\n")
@ -688,7 +444,7 @@ func (p *Profile) PrintWeightedCallGraphDOT(edgeThreshold float64) {
style = "dashed"
}
color := "black"
edgepercent := WeightInPercentage(e.Weight, p.TotalWeight)
edgepercent := pgo.WeightInPercentage(e.Weight, p.TotalWeight)
if edgepercent > edgeThreshold {
color = "red"
}

View File

@ -1,52 +1,52 @@
GO PREPROFILE V1
example.com/pgo/devirtualize.ExerciseFuncClosure
example.com/pgo/devirtualize/mult%2epkg.MultClosure.func1
18 93
example.com/pgo/devirtualize.ExerciseIface
example.com/pgo/devirtualize/mult%2epkg.NegMult.Multiply
49 4
example.com/pgo/devirtualize.ExerciseFuncConcrete
example.com/pgo/devirtualize.AddFn
48 103
example.com/pgo/devirtualize.ExerciseFuncField
example.com/pgo/devirtualize/mult%2epkg.NegMultFn
23 8
example.com/pgo/devirtualize.ExerciseFuncField
example.com/pgo/devirtualize/mult%2epkg.MultFn
23 94
example.com/pgo/devirtualize.ExerciseIface
example.com/pgo/devirtualize/mult%2epkg.Mult.Multiply
49 40
example.com/pgo/devirtualize.ExerciseIface
example.com/pgo/devirtualize.Add.Add
49 55
example.com/pgo/devirtualize.ExerciseFuncConcrete
example.com/pgo/devirtualize/mult%2epkg.NegMultFn
48 8
example.com/pgo/devirtualize.ExerciseFuncClosure
example.com/pgo/devirtualize/mult%2epkg.NegMultClosure.func1
18 10
example.com/pgo/devirtualize.ExerciseIface
example.com/pgo/devirtualize.Sub.Add
49 7
example.com/pgo/devirtualize.ExerciseFuncField
example.com/pgo/devirtualize.AddFn
23 101
example.com/pgo/devirtualize.ExerciseFuncField
example.com/pgo/devirtualize.SubFn
23 12
example.com/pgo/devirtualize.BenchmarkDevirtFuncConcrete
example.com/pgo/devirtualize.ExerciseFuncConcrete
1 2
example.com/pgo/devirtualize.ExerciseFuncConcrete
example.com/pgo/devirtualize/mult%2epkg.MultFn
48 91
example.com/pgo/devirtualize.ExerciseFuncConcrete
example.com/pgo/devirtualize.SubFn
48 5
23 94
example.com/pgo/devirtualize.ExerciseFuncClosure
example.com/pgo/devirtualize/mult%2epkg.MultClosure.func1
18 93
example.com/pgo/devirtualize.ExerciseFuncClosure
example.com/pgo/devirtualize.Add.Add
18 92
example.com/pgo/devirtualize.ExerciseFuncConcrete
example.com/pgo/devirtualize/mult%2epkg.MultFn
48 91
example.com/pgo/devirtualize.ExerciseIface
example.com/pgo/devirtualize.Add.Add
49 55
example.com/pgo/devirtualize.ExerciseIface
example.com/pgo/devirtualize/mult%2epkg.Mult.Multiply
49 40
example.com/pgo/devirtualize.ExerciseFuncClosure
example.com/pgo/devirtualize.Sub.Add
18 14
example.com/pgo/devirtualize.ExerciseFuncField
example.com/pgo/devirtualize.SubFn
23 12
example.com/pgo/devirtualize.ExerciseFuncClosure
example.com/pgo/devirtualize/mult%2epkg.NegMultClosure.func1
18 10
example.com/pgo/devirtualize.ExerciseFuncConcrete
example.com/pgo/devirtualize/mult%2epkg.NegMultFn
48 8
example.com/pgo/devirtualize.ExerciseFuncField
example.com/pgo/devirtualize/mult%2epkg.NegMultFn
23 8
example.com/pgo/devirtualize.ExerciseIface
example.com/pgo/devirtualize.Sub.Add
49 7
example.com/pgo/devirtualize.ExerciseFuncConcrete
example.com/pgo/devirtualize.SubFn
48 5
example.com/pgo/devirtualize.ExerciseIface
example.com/pgo/devirtualize/mult%2epkg.NegMult.Multiply
49 4
example.com/pgo/devirtualize.BenchmarkDevirtFuncConcrete
example.com/pgo/devirtualize.ExerciseFuncConcrete
1 2

View File

@ -1,13 +1,13 @@
GO PREPROFILE V1
example.com/pgo/inline.benchmarkB
example.com/pgo/inline.A
18 1
example.com/pgo/inline.(*BS).NS
7 129
example.com/pgo/inline.(*BS).NS
example.com/pgo/inline.T
8 3
example.com/pgo/inline.(*BS).NS
example.com/pgo/inline.T
13 2
example.com/pgo/inline.benchmarkB
example.com/pgo/inline.A
example.com/pgo/inline.(*BS).NS
7 129
18 1

View File

@ -47,6 +47,7 @@ var bootstrapDirs = []string{
"cmd/internal/notsha256",
"cmd/internal/obj/...",
"cmd/internal/objabi",
"cmd/internal/pgo",
"cmd/internal/pkgpath",
"cmd/internal/quoted",
"cmd/internal/src",
@ -316,7 +317,7 @@ func bootstrapFixImports(srcFile string) string {
continue
}
if strings.HasPrefix(line, `import "`) || strings.HasPrefix(line, `import . "`) ||
inBlock && (strings.HasPrefix(line, "\t\"") || strings.HasPrefix(line, "\t. \"") || strings.HasPrefix(line, "\texec \"") || strings.HasPrefix(line, "\trtabi \"")) {
inBlock && (strings.HasPrefix(line, "\t\"") || strings.HasPrefix(line, "\t. \"") || strings.HasPrefix(line, "\texec \"") || strings.HasPrefix(line, "\trtabi \"") || strings.HasPrefix(line, "\tpgoir \"")) {
line = strings.Replace(line, `"cmd/`, `"bootstrap/cmd/`, -1)
for _, dir := range bootstrapDirs {
if strings.HasPrefix(dir, "cmd/") {

View File

@ -0,0 +1,102 @@
// Copyright 2024 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.
package pgo
import (
"bufio"
"fmt"
"io"
"strings"
"strconv"
)
// IsSerialized returns true if r is a serialized Profile.
//
// IsSerialized only peeks at r, so seeking back after calling is not
// necessary.
func IsSerialized(r *bufio.Reader) (bool, error) {
hdr, err := r.Peek(len(serializationHeader))
if err == io.EOF {
// Empty file.
return false, nil
} else if err != nil {
return false, fmt.Errorf("error reading profile header: %w", err)
}
return string(hdr) == serializationHeader, nil
}
// FromSerialized parses a profile from serialization output of Profile.WriteTo.
func FromSerialized(r io.Reader) (*Profile, error) {
d := emptyProfile()
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines)
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading preprocessed profile: %w", err)
}
return nil, fmt.Errorf("preprocessed profile missing header")
}
if gotHdr := scanner.Text() + "\n"; gotHdr != serializationHeader {
return nil, fmt.Errorf("preprocessed profile malformed header; got %q want %q", gotHdr, serializationHeader)
}
for scanner.Scan() {
readStr := scanner.Text()
callerName := readStr
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading preprocessed profile: %w", err)
}
return nil, fmt.Errorf("preprocessed profile entry missing callee")
}
calleeName := scanner.Text()
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading preprocessed profile: %w", err)
}
return nil, fmt.Errorf("preprocessed profile entry missing weight")
}
readStr = scanner.Text()
split := strings.Split(readStr, " ")
if len(split) != 2 {
return nil, fmt.Errorf("preprocessed profile entry got %v want 2 fields", split)
}
co, err := strconv.Atoi(split[0])
if err != nil {
return nil, fmt.Errorf("preprocessed profile error processing call line: %w", err)
}
edge := NamedCallEdge{
CallerName: callerName,
CalleeName: calleeName,
CallSiteOffset: co,
}
weight, err := strconv.ParseInt(split[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("preprocessed profile error processing call weight: %w", err)
}
if _, ok := d.NamedEdgeMap.Weight[edge]; ok {
return nil, fmt.Errorf("preprocessed profile contains duplicate edge %+v", edge)
}
d.NamedEdgeMap.ByWeight = append(d.NamedEdgeMap.ByWeight, edge) // N.B. serialization is ordered.
d.NamedEdgeMap.Weight[edge] += weight
d.TotalWeight += weight
}
return d, nil
}

View File

@ -0,0 +1,55 @@
// Copyright 2024 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.
// Package pgo contains the compiler-agnostic portions of PGO profile handling.
// Notably, parsing pprof profiles and serializing/deserializing from a custom
// intermediate representation.
package pgo
// Profile contains the processed data from the PGO profile.
type Profile struct {
// TotalWeight is the aggregated edge weights across the profile. This
// helps us determine the percentage threshold for hot/cold
// partitioning.
TotalWeight int64
// NamedEdgeMap contains all unique call edges in the profile and their
// edge weight.
NamedEdgeMap NamedEdgeMap
}
// NamedCallEdge identifies a call edge by linker symbol names and call site
// offset.
type NamedCallEdge struct {
CallerName string
CalleeName string
CallSiteOffset int // Line offset from function start line.
}
// NamedEdgeMap contains all unique call edges in the profile and their
// edge weight.
type NamedEdgeMap struct {
Weight map[NamedCallEdge]int64
// ByWeight lists all keys in Weight, sorted by edge weight from
// highest to lowest.
ByWeight []NamedCallEdge
}
func emptyProfile() *Profile {
// Initialize empty maps/slices for easier use without a requiring a
// nil check.
return &Profile{
NamedEdgeMap: NamedEdgeMap{
ByWeight: make([]NamedCallEdge, 0),
Weight: make(map[NamedCallEdge]int64),
},
}
}
// WeightInPercentage converts profile weights to a percentage.
func WeightInPercentage(value int64, total int64) float64 {
return (float64(value) / float64(total)) * 100
}

View File

@ -0,0 +1,140 @@
// Copyright 2024 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.
// Package pgo contains the compiler-agnostic portions of PGO profile handling.
// Notably, parsing pprof profiles and serializing/deserializing from a custom
// intermediate representation.
package pgo
import (
"errors"
"fmt"
"internal/profile"
"io"
"sort"
)
// FromPProf parses Profile from a pprof profile.
func FromPProf(r io.Reader) (*Profile, error) {
p, err := profile.Parse(r)
if errors.Is(err, profile.ErrNoData) {
// Treat a completely empty file the same as a profile with no
// samples: nothing to do.
return emptyProfile(), nil
} else if err != nil {
return nil, fmt.Errorf("error parsing profile: %w", err)
}
if len(p.Sample) == 0 {
// We accept empty profiles, but there is nothing to do.
return emptyProfile(), nil
}
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 {
return nil, fmt.Errorf(`profile does not contain a sample index with value/type "samples/count" or cpu/nanoseconds"`)
}
g := profile.NewGraph(p, &profile.Options{
SampleValue: func(v []int64) int64 { return v[valueIndex] },
})
namedEdgeMap, totalWeight, err := createNamedEdgeMap(g)
if err != nil {
return nil, err
}
if totalWeight == 0 {
return emptyProfile(), nil // accept but ignore profile with no samples.
}
return &Profile{
TotalWeight: totalWeight,
NamedEdgeMap: namedEdgeMap,
}, nil
}
// createNamedEdgeMap builds a map of callsite-callee edge weights from the
// profile-graph.
//
// Caller should ignore the profile if totalWeight == 0.
func createNamedEdgeMap(g *profile.Graph) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
seenStartLine := false
// Process graph and build various node and edge maps which will
// be consumed by AST walk.
weight := make(map[NamedCallEdge]int64)
for _, n := range g.Nodes {
seenStartLine = seenStartLine || n.Info.StartLine != 0
canonicalName := n.Info.Name
// Create the key to the nodeMapKey.
namedEdge := NamedCallEdge{
CallerName: canonicalName,
CallSiteOffset: n.Info.Lineno - n.Info.StartLine,
}
for _, e := range n.Out {
totalWeight += e.WeightValue()
namedEdge.CalleeName = e.Dest.Info.Name
// Create new entry or increment existing entry.
weight[namedEdge] += e.WeightValue()
}
}
if !seenStartLine {
// TODO(prattmic): If Function.start_line is missing we could
// fall back to using absolute line numbers, which is better
// than nothing.
return NamedEdgeMap{}, 0, fmt.Errorf("profile missing Function.start_line data (Go version of profiled application too old? Go 1.20+ automatically adds this to profiles)")
}
return postProcessNamedEdgeMap(weight, totalWeight)
}
func sortByWeight(edges []NamedCallEdge, weight map[NamedCallEdge]int64) {
sort.Slice(edges, func(i, j int) bool {
ei, ej := edges[i], edges[j]
if wi, wj := weight[ei], weight[ej]; wi != wj {
return wi > wj // want larger weight first
}
// same weight, order by name/line number
if ei.CallerName != ej.CallerName {
return ei.CallerName < ej.CallerName
}
if ei.CalleeName != ej.CalleeName {
return ei.CalleeName < ej.CalleeName
}
return ei.CallSiteOffset < ej.CallSiteOffset
})
}
func postProcessNamedEdgeMap(weight map[NamedCallEdge]int64, weightVal int64) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
if weightVal == 0 {
return NamedEdgeMap{}, 0, nil // accept but ignore profile with no samples.
}
byWeight := make([]NamedCallEdge, 0, len(weight))
for namedEdge := range weight {
byWeight = append(byWeight, namedEdge)
}
sortByWeight(byWeight, weight)
edgeMap = NamedEdgeMap{
Weight: weight,
ByWeight: byWeight,
}
totalWeight = weightVal
return edgeMap, totalWeight, nil
}

View File

@ -0,0 +1,79 @@
// Copyright 2024 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.
package pgo
import (
"bufio"
"fmt"
"io"
)
// Serialization of a Profile allows go tool preprofile to construct the edge
// map only once (rather than once per compile process). The compiler processes
// then parse the pre-processed data directly from the serialized format.
//
// The format of the serialized output is as follows.
//
// GO PREPROFILE V1
// caller_name
// callee_name
// "call site offset" "call edge weight"
// ...
// caller_name
// callee_name
// "call site offset" "call edge weight"
//
// Entries are sorted by "call edge weight", from highest to lowest.
const serializationHeader = "GO PREPROFILE V1\n"
// WriteTo writes a serialized representation of Profile to w.
//
// FromSerialized can parse the format back to Profile.
//
// WriteTo implements io.WriterTo.Write.
func (d *Profile) WriteTo(w io.Writer) (int64, error) {
bw := bufio.NewWriter(w)
var written int64
// Header
n, err := bw.WriteString(serializationHeader)
written += int64(n)
if err != nil {
return written, err
}
for _, edge := range d.NamedEdgeMap.ByWeight {
weight := d.NamedEdgeMap.Weight[edge]
n, err = fmt.Fprintln(bw, edge.CallerName)
written += int64(n)
if err != nil {
return written, err
}
n, err = fmt.Fprintln(bw, edge.CalleeName)
written += int64(n)
if err != nil {
return written, err
}
n, err = fmt.Fprintf(bw, "%d %d\n", edge.CallSiteOffset, weight)
written += int64(n)
if err != nil {
return written, err
}
}
if err := bw.Flush(); err != nil {
return written, err
}
// No need to serialize TotalWeight, it can be trivially recomputed
// during parsing.
return written, nil
}

View File

@ -0,0 +1,190 @@
// Copyright 2024 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.
package pgo
import (
"bytes"
"encoding/binary"
"fmt"
"reflect"
"strings"
"testing"
)
// equal returns an error if got and want are not equal.
func equal(got, want *Profile) error {
if got.TotalWeight != want.TotalWeight {
return fmt.Errorf("got.TotalWeight %d != want.TotalWeight %d", got.TotalWeight, want.TotalWeight)
}
if !reflect.DeepEqual(got.NamedEdgeMap.ByWeight, want.NamedEdgeMap.ByWeight) {
return fmt.Errorf("got.NamedEdgeMap.ByWeight != want.NamedEdgeMap.ByWeight\ngot = %+v\nwant = %+v", got.NamedEdgeMap.ByWeight, want.NamedEdgeMap.ByWeight)
}
if !reflect.DeepEqual(got.NamedEdgeMap.Weight, want.NamedEdgeMap.Weight) {
return fmt.Errorf("got.NamedEdgeMap.Weight != want.NamedEdgeMap.Weight\ngot = %+v\nwant = %+v", got.NamedEdgeMap.Weight, want.NamedEdgeMap.Weight)
}
return nil
}
func testRoundTrip(t *testing.T, d *Profile) []byte {
var buf bytes.Buffer
n, err := d.WriteTo(&buf)
if err != nil {
t.Fatalf("WriteTo got err %v want nil", err)
}
if n != int64(buf.Len()) {
t.Errorf("WriteTo got n %d want %d", n, int64(buf.Len()))
}
b := buf.Bytes()
got, err := FromSerialized(&buf)
if err != nil {
t.Fatalf("processSerialized got err %v want nil", err)
}
if err := equal(got, d); err != nil {
t.Errorf("processSerialized output does not match input: %v", err)
}
return b
}
func TestEmpty(t *testing.T) {
d := emptyProfile()
b := testRoundTrip(t, d)
// Contents should consist of only a header.
if string(b) != serializationHeader {
t.Errorf("WriteTo got %q want %q", string(b), serializationHeader)
}
}
func TestRoundTrip(t *testing.T) {
d := &Profile{
TotalWeight: 3,
NamedEdgeMap: NamedEdgeMap{
ByWeight: []NamedCallEdge{
{
CallerName: "a",
CalleeName: "b",
CallSiteOffset: 14,
},
{
CallerName: "c",
CalleeName: "d",
CallSiteOffset: 15,
},
},
Weight: map[NamedCallEdge]int64{
{
CallerName: "a",
CalleeName: "b",
CallSiteOffset: 14,
}: 2,
{
CallerName: "c",
CalleeName: "d",
CallSiteOffset: 15,
}: 1,
},
},
}
testRoundTrip(t, d)
}
func constructFuzzProfile(t *testing.T, b []byte) *Profile {
// The fuzzer can't construct an arbitrary structure, so instead we
// consume bytes from b to act as our edge data.
r := bytes.NewReader(b)
consumeString := func() (string, bool) {
// First byte: how many bytes to read for this string? We only
// use a byte to avoid making humongous strings.
length, err := r.ReadByte()
if err != nil {
return "", false
}
if length == 0 {
return "", false
}
b := make([]byte, length)
_, err = r.Read(b)
if err != nil {
return "", false
}
return string(b), true
}
consumeInt64 := func() (int64, bool) {
b := make([]byte, 8)
_, err := r.Read(b)
if err != nil {
return 0, false
}
return int64(binary.LittleEndian.Uint64(b)), true
}
d := emptyProfile()
for {
caller, ok := consumeString()
if !ok {
break
}
if strings.ContainsAny(caller, " \r\n") {
t.Skip("caller contains space or newline")
}
callee, ok := consumeString()
if !ok {
break
}
if strings.ContainsAny(callee, " \r\n") {
t.Skip("callee contains space or newline")
}
line, ok := consumeInt64()
if !ok {
break
}
weight, ok := consumeInt64()
if !ok {
break
}
edge := NamedCallEdge{
CallerName: caller,
CalleeName: callee,
CallSiteOffset: int(line),
}
if _, ok := d.NamedEdgeMap.Weight[edge]; ok {
t.Skip("duplicate edge")
}
d.NamedEdgeMap.Weight[edge] = weight
d.TotalWeight += weight
}
byWeight := make([]NamedCallEdge, 0, len(d.NamedEdgeMap.Weight))
for namedEdge := range d.NamedEdgeMap.Weight {
byWeight = append(byWeight, namedEdge)
}
sortByWeight(byWeight, d.NamedEdgeMap.Weight)
d.NamedEdgeMap.ByWeight = byWeight
return d
}
func FuzzRoundTrip(f *testing.F) {
f.Add([]byte("")) // empty profile
f.Fuzz(func(t *testing.T, b []byte) {
d := constructFuzzProfile(t, b)
testRoundTrip(t, d)
})
}

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd00000000\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd\xfd0")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\x00\x040000000000000")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\b00000000\x01\n000000000")

View File

@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\x010\x01\r000000000")

View File

@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Preprofile handles pprof files.
// Preprofile creates an intermediate representation of a pprof profile for use
// during PGO in the compiler. This transformation depends only on the profile
// itself and is thus wasteful to perform in every invocation of the compiler.
//
// Usage:
//
@ -14,32 +16,13 @@ package main
import (
"bufio"
"cmd/internal/pgo"
"flag"
"fmt"
"internal/profile"
"log"
"os"
"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" "call edge weight"
// ...
// caller_name
// callee_name
// "call site offset" "call edge weight"
func usage() {
fmt.Fprintf(os.Stderr, "usage: go tool preprofile [-v] [-o output] -i input\n\n")
flag.PrintDefaults()
@ -49,109 +32,35 @@ func usage() {
var (
output = flag.String("o", "", "output file path")
input = flag.String("i", "", "input pprof file path")
verbose = flag.Bool("v", false, "enable verbose logging")
)
type NodeMapKey struct {
CallerName string
CalleeName string
CallSiteOffset int // Line offset from function start line.
}
func preprocess(profileFile string, outputFile string, verbose bool) error {
// open the pprof profile file
func preprocess(profileFile string, outputFile string) error {
f, err := os.Open(profileFile)
if err != nil {
return fmt.Errorf("error opening profile: %w", err)
}
defer f.Close()
p, err := profile.Parse(f)
r := bufio.NewReader(f)
d, err := pgo.FromPProf(r)
if err != nil {
return fmt.Errorf("error parsing profile: %w", err)
}
if len(p.Sample) == 0 {
// We accept empty profiles, but there is nothing to do.
//
// TODO(prattmic): write an "empty" preprocessed file.
return nil
}
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 {
return fmt.Errorf("failed to find CPU samples count or CPU nanoseconds value-types in profile.")
}
// 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] },
})
TotalEdgeWeight := int64(0)
NodeMap := make(map[NodeMapKey]int64)
for _, n := range g.Nodes {
canonicalName := n.Info.Name
// Create the key to the nodeMapKey.
nodeinfo := NodeMapKey{
CallerName: canonicalName,
CallSiteOffset: n.Info.Lineno - n.Info.StartLine,
}
if n.Info.StartLine == 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 += e.WeightValue()
} else {
w = e.WeightValue()
NodeMap[nodeinfo] = w
}
}
}
var fNodeMap *os.File
var out *os.File
if outputFile == "" {
fNodeMap = os.Stdout
out = os.Stdout
} else {
fNodeMap, err = os.Create(outputFile)
out, err = os.Create(outputFile)
if err != nil {
return fmt.Errorf("Error creating output file: %w", err)
return fmt.Errorf("error creating output file: %w", err)
}
defer fNodeMap.Close()
defer out.Close()
}
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.FormatInt(element, 10) + "\n"
w.WriteString(line)
w.Flush()
count += 1
w := bufio.NewWriter(out)
if _, err := d.WriteTo(w); err != nil {
return fmt.Errorf("error writing output file: %w", err)
}
return nil
@ -168,7 +77,7 @@ func main() {
usage()
}
if err := preprocess(*input, *output, *verbose); err != nil {
if err := preprocess(*input, *output); err != nil {
log.Fatal(err)
}
}