Skip to content

Commit e069587

Browse files
jinlin-bayareaprattmic
authored andcommitted
cmd/preprofile: Implement a tool to preprocess the PGO profile.
It fixes the issue #65220. It also includes https://go.dev/cl/557458 from Michael. Change-Id: Ic6109e1b6a9045459ff4a54dea11cbfe732b01e6 Reviewed-on: https://go-review.googlesource.com/c/go/+/557918 Reviewed-by: Michael Pratt <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Cherry Mui <[email protected]>
1 parent 5c0d092 commit e069587

File tree

6 files changed

+448
-44
lines changed

6 files changed

+448
-44
lines changed

src/cmd/compile/internal/base/flag.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ type CmdFlags struct {
124124
TraceProfile string "help:\"write an execution trace to `file`\""
125125
TrimPath string "help:\"remove `prefix` from recorded source file paths\""
126126
WB bool "help:\"enable write barrier\"" // TODO: remove
127-
PgoProfile string "help:\"read profile from `file`\""
127+
PgoProfile string "help:\"read profile or pre-process profile from `file`\""
128128
ErrorURL bool "help:\"print explanatory URL with error message if applicable\""
129129

130130
// Configuration derived from flags; not a flag itself.

src/cmd/compile/internal/pgo/irgraph.go

Lines changed: 171 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,19 @@
4141
package pgo
4242

4343
import (
44+
"bufio"
4445
"cmd/compile/internal/base"
4546
"cmd/compile/internal/ir"
46-
"cmd/compile/internal/pgo/internal/graph"
4747
"cmd/compile/internal/typecheck"
4848
"cmd/compile/internal/types"
4949
"errors"
5050
"fmt"
5151
"internal/profile"
52+
"io/ioutil"
5253
"os"
5354
"sort"
55+
"strconv"
56+
"strings"
5457
)
5558

5659
// IRGraph is a call graph with nodes pointing to IRs of functions and edges
@@ -105,6 +108,7 @@ type NamedCallEdge struct {
105108
CallerName string
106109
CalleeName string
107110
CallSiteOffset int // Line offset from function start line.
111+
CallStartLine int // Start line of the function. Can be 0 which means missing.
108112
}
109113

110114
// NamedEdgeMap contains all unique call edges in the profile and their
@@ -139,8 +143,47 @@ type Profile struct {
139143
WeightedCG *IRGraph
140144
}
141145

142-
// New generates a profile-graph from the profile.
146+
var wantHdr = "GO PREPROFILE V1\n"
147+
148+
func isPreProfileFile(filename string) (bool, error) {
149+
content, err := ioutil.ReadFile(filename)
150+
if err != nil {
151+
return false, err
152+
}
153+
154+
/* check the header */
155+
fileContent := string(content)
156+
if strings.HasPrefix(fileContent, wantHdr) {
157+
return true, nil
158+
}
159+
return false, nil
160+
}
161+
162+
// New generates a profile-graph from the profile or pre-processed profile.
143163
func New(profileFile string) (*Profile, error) {
164+
var profile *Profile
165+
var err error
166+
isPreProf, err := isPreProfileFile(profileFile)
167+
if err != nil {
168+
return nil, fmt.Errorf("error opening profile: %w", err)
169+
}
170+
if !isPreProf {
171+
profile, err = processProto(profileFile)
172+
if err != nil {
173+
return nil, fmt.Errorf("error processing pprof PGO profile: %w", err)
174+
}
175+
} else {
176+
profile, err = processPreprof(profileFile)
177+
if err != nil {
178+
return nil, fmt.Errorf("error processing preprocessed PGO profile: %w", err)
179+
}
180+
}
181+
return profile, nil
182+
183+
}
184+
185+
// processProto generates a profile-graph from the profile.
186+
func processProto(profileFile string) (*Profile, error) {
144187
f, err := os.Open(profileFile)
145188
if err != nil {
146189
return nil, fmt.Errorf("error opening profile: %w", err)
@@ -175,7 +218,7 @@ func New(profileFile string) (*Profile, error) {
175218
return nil, fmt.Errorf(`profile does not contain a sample index with value/type "samples/count" or cpu/nanoseconds"`)
176219
}
177220

178-
g := graph.NewGraph(p, &graph.Options{
221+
g := profile.NewGraph(p, &profile.Options{
179222
SampleValue: func(v []int64) int64 { return v[valueIndex] },
180223
})
181224

@@ -198,11 +241,134 @@ func New(profileFile string) (*Profile, error) {
198241
}, nil
199242
}
200243

244+
// processPreprof generates a profile-graph from the pre-procesed profile.
245+
func processPreprof(preprofileFile string) (*Profile, error) {
246+
namedEdgeMap, totalWeight, err := createNamedEdgeMapFromPreprocess(preprofileFile)
247+
if err != nil {
248+
return nil, err
249+
}
250+
251+
if totalWeight == 0 {
252+
return nil, nil // accept but ignore profile with no samples.
253+
}
254+
255+
// Create package-level call graph with weights from profile and IR.
256+
wg := createIRGraph(namedEdgeMap)
257+
258+
return &Profile{
259+
TotalWeight: totalWeight,
260+
NamedEdgeMap: namedEdgeMap,
261+
WeightedCG: wg,
262+
}, nil
263+
}
264+
265+
func postProcessNamedEdgeMap(weight map[NamedCallEdge]int64, weightVal int64) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
266+
if weightVal == 0 {
267+
return NamedEdgeMap{}, 0, nil // accept but ignore profile with no samples.
268+
}
269+
byWeight := make([]NamedCallEdge, 0, len(weight))
270+
for namedEdge := range weight {
271+
byWeight = append(byWeight, namedEdge)
272+
}
273+
sort.Slice(byWeight, func(i, j int) bool {
274+
ei, ej := byWeight[i], byWeight[j]
275+
if wi, wj := weight[ei], weight[ej]; wi != wj {
276+
return wi > wj // want larger weight first
277+
}
278+
// same weight, order by name/line number
279+
if ei.CallerName != ej.CallerName {
280+
return ei.CallerName < ej.CallerName
281+
}
282+
if ei.CalleeName != ej.CalleeName {
283+
return ei.CalleeName < ej.CalleeName
284+
}
285+
return ei.CallSiteOffset < ej.CallSiteOffset
286+
})
287+
288+
edgeMap = NamedEdgeMap{
289+
Weight: weight,
290+
ByWeight: byWeight,
291+
}
292+
293+
totalWeight = weightVal
294+
295+
return edgeMap, totalWeight, nil
296+
}
297+
298+
// restore NodeMap information from a preprocessed profile.
299+
// The reader can refer to the format of preprocessed profile in cmd/preprofile/main.go.
300+
func createNamedEdgeMapFromPreprocess(preprofileFile string) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
301+
readFile, err := os.Open(preprofileFile)
302+
if err != nil {
303+
return NamedEdgeMap{}, 0, fmt.Errorf("error opening preprocessed profile: %w", err)
304+
}
305+
defer readFile.Close()
306+
307+
fileScanner := bufio.NewScanner(readFile)
308+
fileScanner.Split(bufio.ScanLines)
309+
weight := make(map[NamedCallEdge]int64)
310+
311+
if !fileScanner.Scan() {
312+
if err := fileScanner.Err(); err != nil {
313+
return NamedEdgeMap{}, 0, fmt.Errorf("error reading preprocessed profile: %w", err)
314+
}
315+
return NamedEdgeMap{}, 0, fmt.Errorf("preprocessed profile missing header")
316+
}
317+
if gotHdr := fileScanner.Text() + "\n"; gotHdr != wantHdr {
318+
return NamedEdgeMap{}, 0, fmt.Errorf("preprocessed profile malformed header; got %q want %q", gotHdr, wantHdr)
319+
}
320+
321+
for fileScanner.Scan() {
322+
readStr := fileScanner.Text()
323+
324+
callerName := readStr
325+
326+
if !fileScanner.Scan() {
327+
if err := fileScanner.Err(); err != nil {
328+
return NamedEdgeMap{}, 0, fmt.Errorf("error reading preprocessed profile: %w", err)
329+
}
330+
return NamedEdgeMap{}, 0, fmt.Errorf("preprocessed profile entry missing callee")
331+
}
332+
calleeName := fileScanner.Text()
333+
334+
if !fileScanner.Scan() {
335+
if err := fileScanner.Err(); err != nil {
336+
return NamedEdgeMap{}, 0, fmt.Errorf("error reading preprocessed profile: %w", err)
337+
}
338+
return NamedEdgeMap{}, 0, fmt.Errorf("preprocessed profile entry missing weight")
339+
}
340+
readStr = fileScanner.Text()
341+
342+
split := strings.Split(readStr, " ")
343+
344+
if len(split) != 5 {
345+
return NamedEdgeMap{}, 0, fmt.Errorf("preprocessed profile entry got %v want 5 fields", split)
346+
}
347+
348+
co, _ := strconv.Atoi(split[0])
349+
cs, _ := strconv.Atoi(split[1])
350+
351+
namedEdge := NamedCallEdge{
352+
CallerName: callerName,
353+
CallSiteOffset: co - cs,
354+
}
355+
356+
namedEdge.CalleeName = calleeName
357+
EWeight, _ := strconv.ParseInt(split[4], 10, 64)
358+
359+
weight[namedEdge] += EWeight
360+
totalWeight += EWeight
361+
}
362+
363+
return postProcessNamedEdgeMap(weight, totalWeight)
364+
365+
}
366+
201367
// createNamedEdgeMap builds a map of callsite-callee edge weights from the
202368
// profile-graph.
203369
//
204370
// Caller should ignore the profile if totalWeight == 0.
205-
func createNamedEdgeMap(g *graph.Graph) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
371+
func createNamedEdgeMap(g *profile.Graph) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
206372
seenStartLine := false
207373

208374
// Process graph and build various node and edge maps which will
@@ -226,42 +392,13 @@ func createNamedEdgeMap(g *graph.Graph) (edgeMap NamedEdgeMap, totalWeight int64
226392
}
227393
}
228394

229-
if totalWeight == 0 {
230-
return NamedEdgeMap{}, 0, nil // accept but ignore profile with no samples.
231-
}
232-
233395
if !seenStartLine {
234396
// TODO(prattmic): If Function.start_line is missing we could
235397
// fall back to using absolute line numbers, which is better
236398
// than nothing.
237399
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)")
238400
}
239-
240-
byWeight := make([]NamedCallEdge, 0, len(weight))
241-
for namedEdge := range weight {
242-
byWeight = append(byWeight, namedEdge)
243-
}
244-
sort.Slice(byWeight, func(i, j int) bool {
245-
ei, ej := byWeight[i], byWeight[j]
246-
if wi, wj := weight[ei], weight[ej]; wi != wj {
247-
return wi > wj // want larger weight first
248-
}
249-
// same weight, order by name/line number
250-
if ei.CallerName != ej.CallerName {
251-
return ei.CallerName < ej.CallerName
252-
}
253-
if ei.CalleeName != ej.CalleeName {
254-
return ei.CalleeName < ej.CalleeName
255-
}
256-
return ei.CallSiteOffset < ej.CallSiteOffset
257-
})
258-
259-
edgeMap = NamedEdgeMap{
260-
Weight: weight,
261-
ByWeight: byWeight,
262-
}
263-
264-
return edgeMap, totalWeight, nil
401+
return postProcessNamedEdgeMap(weight, totalWeight)
265402
}
266403

267404
// initializeIRGraph builds the IRGraph by visiting all the ir.Func in decl list

src/cmd/compile/internal/test/pgo_inl_test.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ go 1.19
4343
}
4444

4545
// testPGOIntendedInlining tests that specific functions are inlined.
46-
func testPGOIntendedInlining(t *testing.T, dir string) {
46+
func testPGOIntendedInlining(t *testing.T, dir string, preprocessed ...bool) {
47+
defaultPGOPackValue := false
48+
if len(preprocessed) > 0 {
49+
defaultPGOPackValue = preprocessed[0]
50+
}
51+
4752
testenv.MustHaveGoRun(t)
4853
t.Parallel()
4954

@@ -86,7 +91,12 @@ func testPGOIntendedInlining(t *testing.T, dir string) {
8691

8792
// Build the test with the profile. Use a smaller threshold to test.
8893
// TODO: maybe adjust the test to work with default threshold.
89-
pprof := filepath.Join(dir, "inline_hot.pprof")
94+
var pprof string
95+
if defaultPGOPackValue == false {
96+
pprof = filepath.Join(dir, "inline_hot.pprof")
97+
} else {
98+
pprof = filepath.Join(dir, "inline_hot.pprof.node_map")
99+
}
90100
gcflag := fmt.Sprintf("-m -m -pgoprofile=%s -d=pgoinlinebudget=160,pgoinlinecdfthreshold=90", pprof)
91101
out := buildPGOInliningTest(t, dir, gcflag)
92102

@@ -164,6 +174,27 @@ func TestPGOIntendedInlining(t *testing.T) {
164174
testPGOIntendedInlining(t, dir)
165175
}
166176

177+
// TestPGOIntendedInlining tests that specific functions are inlined when PGO
178+
// is applied to the exact source that was profiled.
179+
func TestPGOPreprocessInlining(t *testing.T) {
180+
wd, err := os.Getwd()
181+
if err != nil {
182+
t.Fatalf("error getting wd: %v", err)
183+
}
184+
srcDir := filepath.Join(wd, "testdata/pgo/inline")
185+
186+
// Copy the module to a scratch location so we can add a go.mod.
187+
dir := t.TempDir()
188+
189+
for _, file := range []string{"inline_hot.go", "inline_hot_test.go", "inline_hot.pprof.node_map"} {
190+
if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
191+
t.Fatalf("error copying %s: %v", file, err)
192+
}
193+
}
194+
195+
testPGOIntendedInlining(t, dir, true)
196+
}
197+
167198
// TestPGOIntendedInlining tests that specific functions are inlined when PGO
168199
// is applied to the modified source.
169200
func TestPGOIntendedInliningShiftedLines(t *testing.T) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
GO PREPROFILE V1
2+
example.com/pgo/inline.benchmarkB
3+
example.com/pgo/inline.A
4+
18 17 0 1 1
5+
example.com/pgo/inline.(*BS).NS
6+
example.com/pgo/inline.T
7+
13 53 124 129 2
8+
example.com/pgo/inline.(*BS).NS
9+
example.com/pgo/inline.T
10+
8 53 124 129 3
11+
example.com/pgo/inline.A
12+
example.com/pgo/inline.(*BS).NS
13+
7 74 1 130 129

0 commit comments

Comments
 (0)