Source file
src/runtime/mgcpacer_test.go
1
2
3
4
5 package runtime_test
6
7 import (
8 "fmt"
9 "internal/goexperiment"
10 "math"
11 "math/rand"
12 . "runtime"
13 "testing"
14 "time"
15 )
16
17 func TestGcPacer(t *testing.T) {
18 t.Parallel()
19
20 const initialHeapBytes = 256 << 10
21 for _, e := range []*gcExecTest{
22 {
23
24
25 name: "Steady",
26 gcPercent: 100,
27 globalsBytes: 32 << 10,
28 nCores: 8,
29 allocRate: constant(33.0),
30 scanRate: constant(1024.0),
31 growthRate: constant(2.0).sum(ramp(-1.0, 12)),
32 scannableFrac: constant(1.0),
33 stackBytes: constant(8192),
34 length: 50,
35 checker: func(t *testing.T, c []gcCycleResult) {
36 n := len(c)
37 if n >= 25 {
38 if goexperiment.PacerRedesign {
39
40
41 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
42 }
43
44
45 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
46 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
47 }
48 },
49 },
50 {
51
52 name: "SteadyBigStacks",
53 gcPercent: 100,
54 globalsBytes: 32 << 10,
55 nCores: 8,
56 allocRate: constant(132.0),
57 scanRate: constant(1024.0),
58 growthRate: constant(2.0).sum(ramp(-1.0, 12)),
59 scannableFrac: constant(1.0),
60 stackBytes: constant(2048).sum(ramp(128<<20, 8)),
61 length: 50,
62 checker: func(t *testing.T, c []gcCycleResult) {
63
64
65 n := len(c)
66 if n >= 25 {
67 if goexperiment.PacerRedesign {
68
69
70 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
71 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
72 }
73
74
75 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
76 }
77 },
78 },
79 {
80
81 name: "SteadyBigGlobals",
82 gcPercent: 100,
83 globalsBytes: 128 << 20,
84 nCores: 8,
85 allocRate: constant(132.0),
86 scanRate: constant(1024.0),
87 growthRate: constant(2.0).sum(ramp(-1.0, 12)),
88 scannableFrac: constant(1.0),
89 stackBytes: constant(8192),
90 length: 50,
91 checker: func(t *testing.T, c []gcCycleResult) {
92
93
94 n := len(c)
95 if n >= 25 {
96 if goexperiment.PacerRedesign {
97
98
99 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
100 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
101 }
102
103
104 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
105 }
106 },
107 },
108 {
109
110 name: "StepAlloc",
111 gcPercent: 100,
112 globalsBytes: 32 << 10,
113 nCores: 8,
114 allocRate: constant(33.0).sum(ramp(66.0, 1).delay(50)),
115 scanRate: constant(1024.0),
116 growthRate: constant(2.0).sum(ramp(-1.0, 12)),
117 scannableFrac: constant(1.0),
118 stackBytes: constant(8192),
119 length: 100,
120 checker: func(t *testing.T, c []gcCycleResult) {
121 n := len(c)
122 if (n >= 25 && n < 50) || n >= 75 {
123
124
125 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
126 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
127 }
128 },
129 },
130 {
131
132 name: "HeavyStepAlloc",
133 gcPercent: 100,
134 globalsBytes: 32 << 10,
135 nCores: 8,
136 allocRate: constant(33).sum(ramp(330, 1).delay(50)),
137 scanRate: constant(1024.0),
138 growthRate: constant(2.0).sum(ramp(-1.0, 12)),
139 scannableFrac: constant(1.0),
140 stackBytes: constant(8192),
141 length: 100,
142 checker: func(t *testing.T, c []gcCycleResult) {
143 n := len(c)
144 if (n >= 25 && n < 50) || n >= 75 {
145
146
147 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
148 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
149 }
150 },
151 },
152 {
153
154 name: "StepScannableFrac",
155 gcPercent: 100,
156 globalsBytes: 32 << 10,
157 nCores: 8,
158 allocRate: constant(128.0),
159 scanRate: constant(1024.0),
160 growthRate: constant(2.0).sum(ramp(-1.0, 12)),
161 scannableFrac: constant(0.2).sum(unit(0.5).delay(50)),
162 stackBytes: constant(8192),
163 length: 100,
164 checker: func(t *testing.T, c []gcCycleResult) {
165 n := len(c)
166 if (n >= 25 && n < 50) || n >= 75 {
167
168
169 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
170 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
171 }
172 },
173 },
174 {
175
176
177
178 name: "HighGOGC",
179 gcPercent: 1500,
180 globalsBytes: 32 << 10,
181 nCores: 8,
182 allocRate: random(7, 0x53).offset(165),
183 scanRate: constant(1024.0),
184 growthRate: constant(2.0).sum(ramp(-1.0, 12), random(0.01, 0x1), unit(14).delay(25)),
185 scannableFrac: constant(1.0),
186 stackBytes: constant(8192),
187 length: 50,
188 checker: func(t *testing.T, c []gcCycleResult) {
189 n := len(c)
190 if goexperiment.PacerRedesign && n > 12 {
191 if n == 26 {
192
193
194
195 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.90, 15)
196 } else {
197
198
199
200
201
202
203 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.90, 1.05)
204 }
205
206
207
208
209
210 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, GCGoalUtilization+0.03)
211 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.03)
212 }
213 },
214 },
215 {
216
217
218 name: "OscAlloc",
219 gcPercent: 100,
220 globalsBytes: 32 << 10,
221 nCores: 8,
222 allocRate: oscillate(13, 0, 8).offset(67),
223 scanRate: constant(1024.0),
224 growthRate: constant(2.0).sum(ramp(-1.0, 12)),
225 scannableFrac: constant(1.0),
226 stackBytes: constant(8192),
227 length: 50,
228 checker: func(t *testing.T, c []gcCycleResult) {
229 n := len(c)
230 if n > 12 {
231
232
233
234 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
235 if goexperiment.PacerRedesign {
236 assertInRange(t, "GC utilization", c[n-1].gcUtilization, 0.25, 0.3)
237 } else {
238
239 assertInRange(t, "GC utilization", c[n-1].gcUtilization, 0.25, 0.4)
240 }
241 }
242 },
243 },
244 {
245
246 name: "JitterAlloc",
247 gcPercent: 100,
248 globalsBytes: 32 << 10,
249 nCores: 8,
250 allocRate: random(13, 0xf).offset(132),
251 scanRate: constant(1024.0),
252 growthRate: constant(2.0).sum(ramp(-1.0, 12), random(0.01, 0xe)),
253 scannableFrac: constant(1.0),
254 stackBytes: constant(8192),
255 length: 50,
256 checker: func(t *testing.T, c []gcCycleResult) {
257 n := len(c)
258 if n > 12 {
259
260
261
262 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
263 if goexperiment.PacerRedesign {
264 assertInRange(t, "GC utilization", c[n-1].gcUtilization, 0.25, 0.3)
265 } else {
266
267 assertInRange(t, "GC utilization", c[n-1].gcUtilization, 0.25, 0.4)
268 }
269 }
270 },
271 },
272 {
273
274
275 name: "HeavyJitterAlloc",
276 gcPercent: 100,
277 globalsBytes: 32 << 10,
278 nCores: 8,
279 allocRate: random(33.0, 0x0).offset(330),
280 scanRate: constant(1024.0),
281 growthRate: constant(2.0).sum(ramp(-1.0, 12), random(0.01, 0x152)),
282 scannableFrac: constant(1.0),
283 stackBytes: constant(8192),
284 length: 50,
285 checker: func(t *testing.T, c []gcCycleResult) {
286 n := len(c)
287 if n > 13 {
288
289
290
291
292 assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
293
294
295 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.05)
296 if goexperiment.PacerRedesign {
297 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[11].gcUtilization, 0.05)
298 } else {
299
300 assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[11].gcUtilization, 0.07)
301 }
302 }
303 },
304 },
305
306
307
308
309
310
311 } {
312 e := e
313 t.Run(e.name, func(t *testing.T) {
314 t.Parallel()
315
316 c := NewGCController(e.gcPercent)
317 var bytesAllocatedBlackLast int64
318 results := make([]gcCycleResult, 0, e.length)
319 for i := 0; i < e.length; i++ {
320 cycle := e.next()
321 c.StartCycle(cycle.stackBytes, e.globalsBytes, cycle.scannableFrac, e.nCores)
322
323
324 const (
325 revisePeriod = 500 * time.Microsecond
326 rateConv = 1024 * float64(revisePeriod) / float64(time.Millisecond)
327 )
328 var nextHeapMarked int64
329 if i == 0 {
330 nextHeapMarked = initialHeapBytes
331 } else {
332 nextHeapMarked = int64(float64(int64(c.HeapMarked())-bytesAllocatedBlackLast) * cycle.growthRate)
333 }
334 globalsScanWorkLeft := int64(e.globalsBytes)
335 stackScanWorkLeft := int64(cycle.stackBytes)
336 heapScanWorkLeft := int64(float64(nextHeapMarked) * cycle.scannableFrac)
337 doWork := func(work int64) (int64, int64, int64) {
338 var deltas [3]int64
339
340
341 for i, workLeft := range []*int64{&globalsScanWorkLeft, &stackScanWorkLeft, &heapScanWorkLeft} {
342 if *workLeft == 0 {
343 continue
344 }
345 if *workLeft > work {
346 deltas[i] += work
347 *workLeft -= work
348 work = 0
349 break
350 } else {
351 deltas[i] += *workLeft
352 work -= *workLeft
353 *workLeft = 0
354 }
355 }
356 return deltas[0], deltas[1], deltas[2]
357 }
358 var (
359 gcDuration int64
360 assistTime int64
361 bytesAllocatedBlack int64
362 )
363 for heapScanWorkLeft+stackScanWorkLeft+globalsScanWorkLeft > 0 {
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402 assistRatio := c.AssistWorkPerByte()
403 utilization := assistRatio * cycle.allocRate / (assistRatio*cycle.allocRate + cycle.scanRate)
404 if utilization < GCBackgroundUtilization {
405 utilization = GCBackgroundUtilization
406 }
407
408
409 bytesScanned := int64(cycle.scanRate * rateConv * float64(e.nCores) * utilization)
410 bytesAllocated := int64(cycle.allocRate * rateConv * float64(e.nCores) * (1 - utilization))
411
412
413 globalsScanned, stackScanned, heapScanned := doWork(bytesScanned)
414
415
416
417
418 actualElapsed := revisePeriod
419 actualAllocated := bytesAllocated
420 if actualScanned := globalsScanned + stackScanned + heapScanned; actualScanned < bytesScanned {
421
422
423 actualElapsed = time.Duration(float64(actualScanned) * float64(revisePeriod) / (cycle.scanRate * rateConv * float64(e.nCores) * utilization))
424 actualAllocated = int64(cycle.allocRate * rateConv * float64(actualElapsed) / float64(revisePeriod) * float64(e.nCores) * (1 - utilization))
425 }
426
427
428 c.Revise(GCControllerReviseDelta{
429 HeapLive: actualAllocated,
430 HeapScan: int64(float64(actualAllocated) * cycle.scannableFrac),
431 HeapScanWork: heapScanned,
432 StackScanWork: stackScanned,
433 GlobalsScanWork: globalsScanned,
434 })
435
436
437 assistTime += int64(float64(actualElapsed) * float64(e.nCores) * (utilization - GCBackgroundUtilization))
438 gcDuration += int64(actualElapsed)
439 bytesAllocatedBlack += actualAllocated
440 }
441
442
443 result := gcCycleResult{
444 cycle: i + 1,
445 heapLive: c.HeapMarked(),
446 heapScannable: int64(float64(int64(c.HeapMarked())-bytesAllocatedBlackLast) * cycle.scannableFrac),
447 heapTrigger: c.Trigger(),
448 heapPeak: c.HeapLive(),
449 heapGoal: c.HeapGoal(),
450 gcUtilization: float64(assistTime)/(float64(gcDuration)*float64(e.nCores)) + GCBackgroundUtilization,
451 }
452 t.Log("GC", result.String())
453 results = append(results, result)
454
455
456 e.check(t, results)
457
458 c.EndCycle(uint64(nextHeapMarked+bytesAllocatedBlack), assistTime, gcDuration, e.nCores)
459
460 bytesAllocatedBlackLast = bytesAllocatedBlack
461 }
462 })
463 }
464 }
465
466 type gcExecTest struct {
467 name string
468
469 gcPercent int
470 globalsBytes uint64
471 nCores int
472
473 allocRate float64Stream
474 scanRate float64Stream
475 growthRate float64Stream
476 scannableFrac float64Stream
477 stackBytes float64Stream
478 length int
479
480 checker func(*testing.T, []gcCycleResult)
481 }
482
483
484
485 const minRate = 0.0001
486
487 func (e *gcExecTest) next() gcCycle {
488 return gcCycle{
489 allocRate: e.allocRate.min(minRate)(),
490 scanRate: e.scanRate.min(minRate)(),
491 growthRate: e.growthRate.min(minRate)(),
492 scannableFrac: e.scannableFrac.limit(0, 1)(),
493 stackBytes: uint64(e.stackBytes.quantize(2048).min(0)()),
494 }
495 }
496
497 func (e *gcExecTest) check(t *testing.T, results []gcCycleResult) {
498 t.Helper()
499
500
501 n := len(results)
502 switch n {
503 case 0:
504 t.Fatal("no results passed to check")
505 return
506 case 1:
507 if results[0].cycle != 1 {
508 t.Error("first cycle has incorrect number")
509 }
510 default:
511 if results[n-1].cycle != results[n-2].cycle+1 {
512 t.Error("cycle numbers out of order")
513 }
514 }
515 if u := results[n-1].gcUtilization; u < 0 || u > 1 {
516 t.Fatal("GC utilization not within acceptable bounds")
517 }
518 if s := results[n-1].heapScannable; s < 0 {
519 t.Fatal("heapScannable is negative")
520 }
521 if e.checker == nil {
522 t.Fatal("test-specific checker is missing")
523 }
524
525
526 e.checker(t, results)
527 }
528
529 type gcCycle struct {
530 allocRate float64
531 scanRate float64
532 growthRate float64
533 scannableFrac float64
534 stackBytes uint64
535 }
536
537 type gcCycleResult struct {
538 cycle int
539
540
541 heapLive uint64
542 heapTrigger uint64
543 heapGoal uint64
544 heapPeak uint64
545
546
547
548
549 heapScannable int64
550 gcUtilization float64
551 }
552
553 func (r *gcCycleResult) goalRatio() float64 {
554 return float64(r.heapPeak) / float64(r.heapGoal)
555 }
556
557 func (r *gcCycleResult) String() string {
558 return fmt.Sprintf("%d %2.1f%% %d->%d->%d (goal: %d)", r.cycle, r.gcUtilization*100, r.heapLive, r.heapTrigger, r.heapPeak, r.heapGoal)
559 }
560
561 func assertInEpsilon(t *testing.T, name string, a, b, epsilon float64) {
562 t.Helper()
563 assertInRange(t, name, a, b-epsilon, b+epsilon)
564 }
565
566 func assertInRange(t *testing.T, name string, a, min, max float64) {
567 t.Helper()
568 if a < min || a > max {
569 t.Errorf("%s not in range (%f, %f): %f", name, min, max, a)
570 }
571 }
572
573
574
575 type float64Stream func() float64
576
577
578 func constant(c float64) float64Stream {
579 return func() float64 {
580 return c
581 }
582 }
583
584
585
586
587
588 func unit(amp float64) float64Stream {
589 dropped := false
590 return func() float64 {
591 if dropped {
592 return 0
593 }
594 dropped = true
595 return amp
596 }
597 }
598
599
600
601 func oscillate(amp, phase float64, period int) float64Stream {
602 var cycle int
603 return func() float64 {
604 p := float64(cycle)/float64(period)*2*math.Pi + phase
605 cycle++
606 if cycle == period {
607 cycle = 0
608 }
609 return math.Sin(p) * amp
610 }
611 }
612
613
614
615 func ramp(height float64, length int) float64Stream {
616 var cycle int
617 return func() float64 {
618 h := height * float64(cycle) / float64(length)
619 if cycle < length {
620 cycle++
621 }
622 return h
623 }
624 }
625
626
627
628 func random(amp float64, seed int64) float64Stream {
629 r := rand.New(rand.NewSource(seed))
630 return func() float64 {
631 return ((r.Float64() - 0.5) * 2) * amp
632 }
633 }
634
635
636
637 func (f float64Stream) delay(cycles int) float64Stream {
638 zeroes := 0
639 return func() float64 {
640 if zeroes < cycles {
641 zeroes++
642 return 0
643 }
644 return f()
645 }
646 }
647
648
649
650 func (f float64Stream) scale(amt float64) float64Stream {
651 return func() float64 {
652 return f() * amt
653 }
654 }
655
656
657
658 func (f float64Stream) offset(amt float64) float64Stream {
659 return func() float64 {
660 old := f()
661 return old + amt
662 }
663 }
664
665
666
667 func (f float64Stream) sum(fs ...float64Stream) float64Stream {
668 return func() float64 {
669 sum := f()
670 for _, s := range fs {
671 sum += s()
672 }
673 return sum
674 }
675 }
676
677
678
679 func (f float64Stream) quantize(mult float64) float64Stream {
680 return func() float64 {
681 r := f() / mult
682 if r < 0 {
683 return math.Ceil(r) * mult
684 }
685 return math.Floor(r) * mult
686 }
687 }
688
689
690
691 func (f float64Stream) min(min float64) float64Stream {
692 return func() float64 {
693 return math.Max(min, f())
694 }
695 }
696
697
698
699 func (f float64Stream) max(max float64) float64Stream {
700 return func() float64 {
701 return math.Min(max, f())
702 }
703 }
704
705
706
707 func (f float64Stream) limit(min, max float64) float64Stream {
708 return func() float64 {
709 v := f()
710 if v < min {
711 v = min
712 } else if v > max {
713 v = max
714 }
715 return v
716 }
717 }
718
719 func FuzzPIController(f *testing.F) {
720 isNormal := func(x float64) bool {
721 return !math.IsInf(x, 0) && !math.IsNaN(x)
722 }
723 isPositive := func(x float64) bool {
724 return isNormal(x) && x > 0
725 }
726
727
728
729 f.Add(0.3375, 3.2e6, 1e9, 0.001, 1000.0, 0.01)
730 f.Add(0.9, 4.0, 1000.0, -1000.0, 1000.0, 0.84)
731 f.Fuzz(func(t *testing.T, kp, ti, tt, min, max, setPoint float64) {
732
733
734
735
736
737
738 if !isPositive(kp) || !isPositive(ti) || !isPositive(tt) {
739 return
740 }
741 if !isNormal(min) || !isNormal(max) || min > max {
742 return
743 }
744
745 rs := rand.New(rand.NewSource(800))
746 randFloat64 := func() float64 {
747 return math.Float64frombits(rs.Uint64())
748 }
749 p := NewPIController(kp, ti, tt, min, max)
750 state := float64(0)
751 for i := 0; i < 100; i++ {
752 input := randFloat64()
753
754
755 var ok bool
756 state, ok = p.Next(input, setPoint, 1.0)
757 if !isNormal(state) {
758 t.Fatalf("got NaN or Inf result from controller: %f %v", state, ok)
759 }
760 }
761 })
762 }
763
View as plain text