Skip to content

Commit 9d91401

Browse files
committed
internal/trace: implement MutatorUtilizationV2
This change adds a new MutatorUtilization for traces for Go 1.22+. To facilitate testing, it also generates a short trace with the gc-stress.go test program (shortening its duration to 10ms) and adds it to the tests for the internal/trace/v2 package. Notably, we make sure this trace has a GCMarkAssistActive event to test that codepath. For #63960. For #60773. Change-Id: I2e61f545988677be716818e2a08641c54c4c201f Reviewed-on: https://go-review.googlesource.com/c/go/+/540256 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Michael Pratt <[email protected]>
1 parent 43ffe2a commit 9d91401

File tree

4 files changed

+2775
-38
lines changed

4 files changed

+2775
-38
lines changed

src/go/build/deps_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -608,9 +608,6 @@ var depsRules = `
608608
FMT
609609
< internal/diff, internal/txtar;
610610
611-
FMT, container/heap, math/rand
612-
< internal/trace;
613-
614611
# v2 execution trace parser.
615612
FMT
616613
< internal/trace/v2/event;
@@ -633,6 +630,9 @@ var depsRules = `
633630
regexp, internal/txtar, internal/trace/v2, internal/trace/v2/raw
634631
< internal/trace/v2/internal/testgen/go122;
635632
633+
FMT, container/heap, math/rand, internal/trace/v2
634+
< internal/trace;
635+
636636
# Coverage.
637637
FMT, crypto/md5, encoding/binary, regexp, sort, text/tabwriter, unsafe,
638638
internal/coverage, internal/coverage/uleb128

src/internal/trace/gc.go

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package trace
66

77
import (
88
"container/heap"
9+
tracev2 "internal/trace/v2"
10+
"io"
911
"math"
1012
"sort"
1113
"strings"
@@ -202,6 +204,268 @@ func MutatorUtilization(events []*Event, flags UtilFlags) [][]MutatorUtil {
202204
return out
203205
}
204206

207+
// MutatorUtilizationV2 returns a set of mutator utilization functions
208+
// for the given v2 trace, passed as an io.Reader. Each function will
209+
// always end with 0 utilization. The bounds of each function are implicit
210+
// in the first and last event; outside of these bounds each function is
211+
// undefined.
212+
//
213+
// If the UtilPerProc flag is not given, this always returns a single
214+
// utilization function. Otherwise, it returns one function per P.
215+
func MutatorUtilizationV2(trace io.Reader, flags UtilFlags) ([][]MutatorUtil, error) {
216+
// Create a reader.
217+
r, err := tracev2.NewReader(trace)
218+
if err != nil {
219+
return nil, err
220+
}
221+
222+
// Set up a bunch of analysis state.
223+
type perP struct {
224+
// gc > 0 indicates that GC is active on this P.
225+
gc int
226+
// series the logical series number for this P. This
227+
// is necessary because Ps may be removed and then
228+
// re-added, and then the new P needs a new series.
229+
series int
230+
}
231+
type procsCount struct {
232+
// time at which procs changed.
233+
time int64
234+
// n is the number of procs at that point.
235+
n int
236+
}
237+
out := [][]MutatorUtil{}
238+
stw := 0
239+
ps := []perP{}
240+
inGC := make(map[tracev2.GoID]bool)
241+
states := make(map[tracev2.GoID]tracev2.GoState)
242+
bgMark := make(map[tracev2.GoID]bool)
243+
procs := []procsCount{}
244+
seenSync := false
245+
246+
// Helpers.
247+
handleSTW := func(r tracev2.Range) bool {
248+
return flags&UtilSTW != 0 && isGCSTW(r)
249+
}
250+
handleMarkAssist := func(r tracev2.Range) bool {
251+
return flags&UtilAssist != 0 && isGCMarkAssist(r)
252+
}
253+
handleSweep := func(r tracev2.Range) bool {
254+
return flags&UtilSweep != 0 && isGCSweep(r)
255+
}
256+
257+
// Iterate through the trace, tracking mutator utilization.
258+
var lastEv tracev2.Event
259+
for {
260+
// Read a single event.
261+
ev, err := r.ReadEvent()
262+
if err == io.EOF {
263+
break
264+
}
265+
if err != nil {
266+
return nil, err
267+
}
268+
lastEv = ev
269+
270+
// Process the event.
271+
switch ev.Kind() {
272+
case tracev2.EventSync:
273+
seenSync = true
274+
case tracev2.EventMetric:
275+
m := ev.Metric()
276+
if m.Name != "/sched/gomaxprocs:threads" {
277+
break
278+
}
279+
gomaxprocs := int(m.Value.Uint64())
280+
if len(ps) > gomaxprocs {
281+
if flags&UtilPerProc != 0 {
282+
// End each P's series.
283+
for _, p := range ps[gomaxprocs:] {
284+
out[p.series] = addUtil(out[p.series], MutatorUtil{int64(ev.Time()), 0})
285+
}
286+
}
287+
ps = ps[:gomaxprocs]
288+
}
289+
for len(ps) < gomaxprocs {
290+
// Start new P's series.
291+
series := 0
292+
if flags&UtilPerProc != 0 || len(out) == 0 {
293+
series = len(out)
294+
out = append(out, []MutatorUtil{{int64(ev.Time()), 1}})
295+
}
296+
ps = append(ps, perP{series: series})
297+
}
298+
if len(procs) == 0 || gomaxprocs != procs[len(procs)-1].n {
299+
procs = append(procs, procsCount{time: int64(ev.Time()), n: gomaxprocs})
300+
}
301+
}
302+
if len(ps) == 0 {
303+
// We can't start doing any analysis until we see what GOMAXPROCS is.
304+
// It will show up very early in the trace, but we need to be robust to
305+
// something else being emitted beforehand.
306+
continue
307+
}
308+
309+
switch ev.Kind() {
310+
case tracev2.EventRangeActive:
311+
if seenSync {
312+
// If we've seen a sync, then we can be sure we're not finding out about
313+
// something late; we have complete information after that point, and these
314+
// active events will just be redundant.
315+
break
316+
}
317+
// This range is active back to the start of the trace. We're failing to account
318+
// for this since we just found out about it now. Fix up the mutator utilization.
319+
//
320+
// N.B. A trace can't start during a STW, so we don't handle it here.
321+
r := ev.Range()
322+
switch {
323+
case handleMarkAssist(r):
324+
if !states[ev.Goroutine()].Executing() {
325+
// If the goroutine isn't executing, then the fact that it was in mark
326+
// assist doesn't actually count.
327+
break
328+
}
329+
// This G has been in a mark assist *and running on its P* since the start
330+
// of the trace.
331+
fallthrough
332+
case handleSweep(r):
333+
// This P has been in sweep (or mark assist, from above) in the start of the trace.
334+
//
335+
// We don't need to do anything if UtilPerProc is set. If we get an event like
336+
// this for a running P, it must show up the first time a P is mentioned. Therefore,
337+
// this P won't actually have any MutatorUtils on its list yet.
338+
//
339+
// However, if UtilPerProc isn't set, then we probably have data from other procs
340+
// and from previous events. We need to fix that up.
341+
if flags&UtilPerProc != 0 {
342+
break
343+
}
344+
// Subtract out 1/gomaxprocs mutator utilization for all time periods
345+
// from the beginning of the trace until now.
346+
mi, pi := 0, 0
347+
for mi < len(out[0]) {
348+
if pi < len(procs)-1 && procs[pi+1].time < out[0][mi].Time {
349+
pi++
350+
continue
351+
}
352+
out[0][mi].Util -= float64(1) / float64(procs[pi].n)
353+
if out[0][mi].Util < 0 {
354+
out[0][mi].Util = 0
355+
}
356+
mi++
357+
}
358+
}
359+
// After accounting for the portion we missed, this just acts like the
360+
// beginning of a new range.
361+
fallthrough
362+
case tracev2.EventRangeBegin:
363+
r := ev.Range()
364+
if handleSTW(r) {
365+
stw++
366+
} else if handleSweep(r) {
367+
ps[ev.Proc()].gc++
368+
} else if handleMarkAssist(r) {
369+
ps[ev.Proc()].gc++
370+
if g := r.Scope.Goroutine(); g != tracev2.NoGoroutine {
371+
inGC[g] = true
372+
}
373+
}
374+
case tracev2.EventRangeEnd:
375+
r := ev.Range()
376+
if handleSTW(r) {
377+
stw--
378+
} else if handleSweep(r) {
379+
ps[ev.Proc()].gc--
380+
} else if handleMarkAssist(r) {
381+
ps[ev.Proc()].gc--
382+
if g := r.Scope.Goroutine(); g != tracev2.NoGoroutine {
383+
delete(inGC, g)
384+
}
385+
}
386+
case tracev2.EventStateTransition:
387+
st := ev.StateTransition()
388+
if st.Resource.Kind != tracev2.ResourceGoroutine {
389+
break
390+
}
391+
old, new := st.Goroutine()
392+
g := st.Resource.Goroutine()
393+
if inGC[g] || bgMark[g] {
394+
if !old.Executing() && new.Executing() {
395+
// Started running while doing GC things.
396+
ps[ev.Proc()].gc++
397+
} else if old.Executing() && !new.Executing() {
398+
// Stopped running while doing GC things.
399+
ps[ev.Proc()].gc--
400+
}
401+
}
402+
states[g] = new
403+
case tracev2.EventLabel:
404+
l := ev.Label()
405+
if flags&UtilBackground != 0 && strings.HasPrefix(l.Label, "GC ") && l.Label != "GC (idle)" {
406+
// Background mark worker.
407+
//
408+
// If we're in per-proc mode, we don't
409+
// count dedicated workers because
410+
// they kick all of the goroutines off
411+
// that P, so don't directly
412+
// contribute to goroutine latency.
413+
if !(flags&UtilPerProc != 0 && l.Label == "GC (dedicated)") {
414+
bgMark[ev.Goroutine()] = true
415+
ps[ev.Proc()].gc++
416+
}
417+
}
418+
}
419+
420+
if flags&UtilPerProc == 0 {
421+
// Compute the current average utilization.
422+
if len(ps) == 0 {
423+
continue
424+
}
425+
gcPs := 0
426+
if stw > 0 {
427+
gcPs = len(ps)
428+
} else {
429+
for i := range ps {
430+
if ps[i].gc > 0 {
431+
gcPs++
432+
}
433+
}
434+
}
435+
mu := MutatorUtil{int64(ev.Time()), 1 - float64(gcPs)/float64(len(ps))}
436+
437+
// Record the utilization change. (Since
438+
// len(ps) == len(out), we know len(out) > 0.)
439+
out[0] = addUtil(out[0], mu)
440+
} else {
441+
// Check for per-P utilization changes.
442+
for i := range ps {
443+
p := &ps[i]
444+
util := 1.0
445+
if stw > 0 || p.gc > 0 {
446+
util = 0.0
447+
}
448+
out[p.series] = addUtil(out[p.series], MutatorUtil{int64(ev.Time()), util})
449+
}
450+
}
451+
}
452+
453+
// No events in the stream.
454+
if lastEv.Kind() == tracev2.EventBad {
455+
return nil, nil
456+
}
457+
458+
// Add final 0 utilization event to any remaining series. This
459+
// is important to mark the end of the trace. The exact value
460+
// shouldn't matter since no window should extend beyond this,
461+
// but using 0 is symmetric with the start of the trace.
462+
mu := MutatorUtil{int64(lastEv.Time()), 0}
463+
for i := range ps {
464+
out[ps[i].series] = addUtil(out[ps[i].series], mu)
465+
}
466+
return out, nil
467+
}
468+
205469
func addUtil(util []MutatorUtil, mu MutatorUtil) []MutatorUtil {
206470
if len(util) > 0 {
207471
if mu.Util == util[len(util)-1].Util {
@@ -824,3 +1088,15 @@ func (in *integrator) next(time int64) int64 {
8241088
}
8251089
return 1<<63 - 1
8261090
}
1091+
1092+
func isGCSTW(r tracev2.Range) bool {
1093+
return strings.HasPrefix(r.Name, "stop-the-world") && strings.Contains(r.Name, "GC")
1094+
}
1095+
1096+
func isGCMarkAssist(r tracev2.Range) bool {
1097+
return r.Name == "GC mark assist"
1098+
}
1099+
1100+
func isGCSweep(r tracev2.Range) bool {
1101+
return r.Name == "GC incremental sweep"
1102+
}

0 commit comments

Comments
 (0)