Skip to content

Commit b7ae16e

Browse files
jinlin-bayareaprattmic
authored andcommitted
cmd/preprofile: Add preprocess tool to pre-parse the profile file.
The pgo compilation time is very long if the profile file is large. We added a preprocess tool to pre-parse profile file in order to expedite the compile time. Change-Id: I6f50bbd01f242448e2463607a9b63483c6ca9a12 Reviewed-on: https://go-review.googlesource.com/c/go/+/529738 Reviewed-by: Michael Pratt <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Cherry Mui <[email protected]>
1 parent 6e7aee5 commit b7ae16e

File tree

6 files changed

+450
-44
lines changed

6 files changed

+450
-44
lines changed

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

+1-1
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

+173-34
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,20 @@
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"
49+
"cmd/internal/bio"
4950
"errors"
5051
"fmt"
5152
"internal/profile"
53+
"log"
5254
"os"
5355
"sort"
56+
"strconv"
57+
"strings"
5458
)
5559

5660
// IRGraph is a call graph with nodes pointing to IRs of functions and edges
@@ -105,6 +109,7 @@ type NamedCallEdge struct {
105109
CallerName string
106110
CalleeName string
107111
CallSiteOffset int // Line offset from function start line.
112+
CallStartLine int // Start line of the function. Can be 0 which means missing.
108113
}
109114

110115
// NamedEdgeMap contains all unique call edges in the profile and their
@@ -139,8 +144,52 @@ type Profile struct {
139144
WeightedCG *IRGraph
140145
}
141146

142-
// New generates a profile-graph from the profile.
147+
var wantHdr = "GO PREPROFILE V1\n"
148+
149+
func isPreProfileFile(filename string) (bool, error) {
150+
file, err := bio.Open(filename)
151+
if err != nil {
152+
return false, err
153+
}
154+
defer file.Close()
155+
156+
/* check the header */
157+
line, err := file.ReadString('\n')
158+
if err != nil {
159+
return false, err
160+
}
161+
162+
if wantHdr == line {
163+
return true, nil
164+
}
165+
return false, nil
166+
}
167+
168+
// New generates a profile-graph from the profile or pre-processed profile.
143169
func New(profileFile string) (*Profile, error) {
170+
var profile *Profile
171+
var err error
172+
isPreProf, err := isPreProfileFile(profileFile)
173+
if err != nil {
174+
return nil, fmt.Errorf("error opening profile: %w", err)
175+
}
176+
if !isPreProf {
177+
profile, err = processProto(profileFile)
178+
if err != nil {
179+
log.Fatalf("%s: PGO error: %v", profileFile, err)
180+
}
181+
} else {
182+
profile, err = processPreprof(profileFile)
183+
if err != nil {
184+
log.Fatalf("%s: Preprocessed PGO error: %v", profileFile, err)
185+
}
186+
}
187+
return profile, nil
188+
189+
}
190+
191+
// processProto generates a profile-graph from the profile.
192+
func processProto(profileFile string) (*Profile, error) {
144193
f, err := os.Open(profileFile)
145194
if err != nil {
146195
return nil, fmt.Errorf("error opening profile: %w", err)
@@ -175,7 +224,7 @@ func New(profileFile string) (*Profile, error) {
175224
return nil, fmt.Errorf(`profile does not contain a sample index with value/type "samples/count" or cpu/nanoseconds"`)
176225
}
177226

178-
g := graph.NewGraph(p, &graph.Options{
227+
g := profile.NewGraph(p, &profile.Options{
179228
SampleValue: func(v []int64) int64 { return v[valueIndex] },
180229
})
181230

@@ -198,11 +247,130 @@ func New(profileFile string) (*Profile, error) {
198247
}, nil
199248
}
200249

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

208376
// Process graph and build various node and edge maps which will
@@ -226,42 +394,13 @@ func createNamedEdgeMap(g *graph.Graph) (edgeMap NamedEdgeMap, totalWeight int64
226394
}
227395
}
228396

229-
if totalWeight == 0 {
230-
return NamedEdgeMap{}, 0, nil // accept but ignore profile with no samples.
231-
}
232-
233397
if !seenStartLine {
234398
// TODO(prattmic): If Function.start_line is missing we could
235399
// fall back to using absolute line numbers, which is better
236400
// than nothing.
237401
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)")
238402
}
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
403+
return postProcessNamedEdgeMap(weight, totalWeight)
265404
}
266405

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

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

+33-2
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) {
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)