...

Source file src/runtime/mgcpacer_test.go

Documentation: runtime

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package runtime_test
     6  
     7  import (
     8  	"fmt"
     9  	"math"
    10  	"math/rand"
    11  	. "runtime"
    12  	"testing"
    13  	"time"
    14  )
    15  
    16  func TestGcPacer(t *testing.T) {
    17  	t.Parallel()
    18  
    19  	const initialHeapBytes = 256 << 10
    20  	for _, e := range []*gcExecTest{
    21  		{
    22  			// The most basic test case: a steady-state heap.
    23  			// Growth to an O(MiB) heap, then constant heap size, alloc/scan rates.
    24  			name:          "Steady",
    25  			gcPercent:     100,
    26  			memoryLimit:   math.MaxInt64,
    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  					// At this alloc/scan rate, the pacer should be extremely close to the goal utilization.
    39  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
    40  
    41  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles.
    42  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
    43  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
    44  				}
    45  			},
    46  		},
    47  		{
    48  			// Same as the steady-state case, but lots of stacks to scan relative to the heap size.
    49  			name:          "SteadyBigStacks",
    50  			gcPercent:     100,
    51  			memoryLimit:   math.MaxInt64,
    52  			globalsBytes:  32 << 10,
    53  			nCores:        8,
    54  			allocRate:     constant(132.0),
    55  			scanRate:      constant(1024.0),
    56  			growthRate:    constant(2.0).sum(ramp(-1.0, 12)),
    57  			scannableFrac: constant(1.0),
    58  			stackBytes:    constant(2048).sum(ramp(128<<20, 8)),
    59  			length:        50,
    60  			checker: func(t *testing.T, c []gcCycleResult) {
    61  				// Check the same conditions as the steady-state case, except the old pacer can't
    62  				// really handle this well, so don't check the goal ratio for it.
    63  				n := len(c)
    64  				if n >= 25 {
    65  					// For the pacer redesign, assert something even stronger: at this alloc/scan rate,
    66  					// it should be extremely close to the goal utilization.
    67  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
    68  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
    69  
    70  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles.
    71  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
    72  				}
    73  			},
    74  		},
    75  		{
    76  			// Same as the steady-state case, but lots of globals to scan relative to the heap size.
    77  			name:          "SteadyBigGlobals",
    78  			gcPercent:     100,
    79  			memoryLimit:   math.MaxInt64,
    80  			globalsBytes:  128 << 20,
    81  			nCores:        8,
    82  			allocRate:     constant(132.0),
    83  			scanRate:      constant(1024.0),
    84  			growthRate:    constant(2.0).sum(ramp(-1.0, 12)),
    85  			scannableFrac: constant(1.0),
    86  			stackBytes:    constant(8192),
    87  			length:        50,
    88  			checker: func(t *testing.T, c []gcCycleResult) {
    89  				// Check the same conditions as the steady-state case, except the old pacer can't
    90  				// really handle this well, so don't check the goal ratio for it.
    91  				n := len(c)
    92  				if n >= 25 {
    93  					// For the pacer redesign, assert something even stronger: at this alloc/scan rate,
    94  					// it should be extremely close to the goal utilization.
    95  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
    96  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
    97  
    98  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles.
    99  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
   100  				}
   101  			},
   102  		},
   103  		{
   104  			// This tests the GC pacer's response to a small change in allocation rate.
   105  			name:          "StepAlloc",
   106  			gcPercent:     100,
   107  			memoryLimit:   math.MaxInt64,
   108  			globalsBytes:  32 << 10,
   109  			nCores:        8,
   110  			allocRate:     constant(33.0).sum(ramp(66.0, 1).delay(50)),
   111  			scanRate:      constant(1024.0),
   112  			growthRate:    constant(2.0).sum(ramp(-1.0, 12)),
   113  			scannableFrac: constant(1.0),
   114  			stackBytes:    constant(8192),
   115  			length:        100,
   116  			checker: func(t *testing.T, c []gcCycleResult) {
   117  				n := len(c)
   118  				if (n >= 25 && n < 50) || n >= 75 {
   119  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles
   120  					// and then is able to settle again after a significant jump in allocation rate.
   121  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
   122  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
   123  				}
   124  			},
   125  		},
   126  		{
   127  			// This tests the GC pacer's response to a large change in allocation rate.
   128  			name:          "HeavyStepAlloc",
   129  			gcPercent:     100,
   130  			memoryLimit:   math.MaxInt64,
   131  			globalsBytes:  32 << 10,
   132  			nCores:        8,
   133  			allocRate:     constant(33).sum(ramp(330, 1).delay(50)),
   134  			scanRate:      constant(1024.0),
   135  			growthRate:    constant(2.0).sum(ramp(-1.0, 12)),
   136  			scannableFrac: constant(1.0),
   137  			stackBytes:    constant(8192),
   138  			length:        100,
   139  			checker: func(t *testing.T, c []gcCycleResult) {
   140  				n := len(c)
   141  				if (n >= 25 && n < 50) || n >= 75 {
   142  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles
   143  					// and then is able to settle again after a significant jump in allocation rate.
   144  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
   145  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
   146  				}
   147  			},
   148  		},
   149  		{
   150  			// This tests the GC pacer's response to a change in the fraction of the scannable heap.
   151  			name:          "StepScannableFrac",
   152  			gcPercent:     100,
   153  			memoryLimit:   math.MaxInt64,
   154  			globalsBytes:  32 << 10,
   155  			nCores:        8,
   156  			allocRate:     constant(128.0),
   157  			scanRate:      constant(1024.0),
   158  			growthRate:    constant(2.0).sum(ramp(-1.0, 12)),
   159  			scannableFrac: constant(0.2).sum(unit(0.5).delay(50)),
   160  			stackBytes:    constant(8192),
   161  			length:        100,
   162  			checker: func(t *testing.T, c []gcCycleResult) {
   163  				n := len(c)
   164  				if (n >= 25 && n < 50) || n >= 75 {
   165  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles
   166  					// and then is able to settle again after a significant jump in allocation rate.
   167  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
   168  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
   169  				}
   170  			},
   171  		},
   172  		{
   173  			// Tests the pacer for a high GOGC value with a large heap growth happening
   174  			// in the middle. The purpose of the large heap growth is to check if GC
   175  			// utilization ends up sensitive
   176  			name:          "HighGOGC",
   177  			gcPercent:     1500,
   178  			memoryLimit:   math.MaxInt64,
   179  			globalsBytes:  32 << 10,
   180  			nCores:        8,
   181  			allocRate:     random(7, 0x53).offset(165),
   182  			scanRate:      constant(1024.0),
   183  			growthRate:    constant(2.0).sum(ramp(-1.0, 12), random(0.01, 0x1), unit(14).delay(25)),
   184  			scannableFrac: constant(1.0),
   185  			stackBytes:    constant(8192),
   186  			length:        50,
   187  			checker: func(t *testing.T, c []gcCycleResult) {
   188  				n := len(c)
   189  				if n > 12 {
   190  					if n == 26 {
   191  						// In the 26th cycle there's a heap growth. Overshoot is expected to maintain
   192  						// a stable utilization, but we should *never* overshoot more than GOGC of
   193  						// the next cycle.
   194  						assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.90, 15)
   195  					} else {
   196  						// Give a wider goal range here. With such a high GOGC value we're going to be
   197  						// forced to undershoot.
   198  						//
   199  						// TODO(mknyszek): Instead of placing a 0.95 limit on the trigger, make the limit
   200  						// based on absolute bytes, that's based somewhat in how the minimum heap size
   201  						// is determined.
   202  						assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.90, 1.05)
   203  					}
   204  
   205  					// Ensure utilization remains stable despite a growth in live heap size
   206  					// at GC #25. This test fails prior to the GC pacer redesign.
   207  					//
   208  					// Because GOGC is so large, we should also be really close to the goal utilization.
   209  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, GCGoalUtilization+0.03)
   210  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.03)
   211  				}
   212  			},
   213  		},
   214  		{
   215  			// This test makes sure that in the face of a varying (in this case, oscillating) allocation
   216  			// rate, the pacer does a reasonably good job of staying abreast of the changes.
   217  			name:          "OscAlloc",
   218  			gcPercent:     100,
   219  			memoryLimit:   math.MaxInt64,
   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  					// After the 12th GC, the heap will stop growing. Now, just make sure that:
   232  					// 1. Utilization isn't varying _too_ much, and
   233  					// 2. The pacer is mostly keeping up with the goal.
   234  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
   235  					assertInRange(t, "GC utilization", c[n-1].gcUtilization, 0.25, 0.3)
   236  				}
   237  			},
   238  		},
   239  		{
   240  			// This test is the same as OscAlloc, but instead of oscillating, the allocation rate is jittery.
   241  			name:          "JitterAlloc",
   242  			gcPercent:     100,
   243  			memoryLimit:   math.MaxInt64,
   244  			globalsBytes:  32 << 10,
   245  			nCores:        8,
   246  			allocRate:     random(13, 0xf).offset(132),
   247  			scanRate:      constant(1024.0),
   248  			growthRate:    constant(2.0).sum(ramp(-1.0, 12), random(0.01, 0xe)),
   249  			scannableFrac: constant(1.0),
   250  			stackBytes:    constant(8192),
   251  			length:        50,
   252  			checker: func(t *testing.T, c []gcCycleResult) {
   253  				n := len(c)
   254  				if n > 12 {
   255  					// After the 12th GC, the heap will stop growing. Now, just make sure that:
   256  					// 1. Utilization isn't varying _too_ much, and
   257  					// 2. The pacer is mostly keeping up with the goal.
   258  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.025)
   259  					assertInRange(t, "GC utilization", c[n-1].gcUtilization, 0.25, 0.275)
   260  				}
   261  			},
   262  		},
   263  		{
   264  			// This test is the same as JitterAlloc, but with a much higher allocation rate.
   265  			// The jitter is proportionally the same.
   266  			name:          "HeavyJitterAlloc",
   267  			gcPercent:     100,
   268  			memoryLimit:   math.MaxInt64,
   269  			globalsBytes:  32 << 10,
   270  			nCores:        8,
   271  			allocRate:     random(33.0, 0x0).offset(330),
   272  			scanRate:      constant(1024.0),
   273  			growthRate:    constant(2.0).sum(ramp(-1.0, 12), random(0.01, 0x152)),
   274  			scannableFrac: constant(1.0),
   275  			stackBytes:    constant(8192),
   276  			length:        50,
   277  			checker: func(t *testing.T, c []gcCycleResult) {
   278  				n := len(c)
   279  				if n > 13 {
   280  					// After the 12th GC, the heap will stop growing. Now, just make sure that:
   281  					// 1. Utilization isn't varying _too_ much, and
   282  					// 2. The pacer is mostly keeping up with the goal.
   283  					// We start at the 13th here because we want to use the 12th as a reference.
   284  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
   285  					// Unlike the other tests, GC utilization here will vary more and tend higher.
   286  					// Just make sure it's not going too crazy.
   287  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.05)
   288  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[11].gcUtilization, 0.05)
   289  				}
   290  			},
   291  		},
   292  		{
   293  			// This test sets a slow allocation rate and a small heap (close to the minimum heap size)
   294  			// to try to minimize the difference between the trigger and the goal.
   295  			name:          "SmallHeapSlowAlloc",
   296  			gcPercent:     100,
   297  			memoryLimit:   math.MaxInt64,
   298  			globalsBytes:  32 << 10,
   299  			nCores:        8,
   300  			allocRate:     constant(1.0),
   301  			scanRate:      constant(2048.0),
   302  			growthRate:    constant(2.0).sum(ramp(-1.0, 3)),
   303  			scannableFrac: constant(0.01),
   304  			stackBytes:    constant(8192),
   305  			length:        50,
   306  			checker: func(t *testing.T, c []gcCycleResult) {
   307  				n := len(c)
   308  				if n > 4 {
   309  					// After the 4th GC, the heap will stop growing.
   310  					// First, let's make sure we're finishing near the goal, with some extra
   311  					// room because we're probably going to be triggering early.
   312  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.925, 1.025)
   313  					// Next, let's make sure there's some minimum distance between the goal
   314  					// and the trigger. It should be proportional to the runway (hence the
   315  					// trigger ratio check, instead of a check against the runway).
   316  					assertInRange(t, "trigger ratio", c[n-1].triggerRatio(), 0.925, 0.975)
   317  				}
   318  				if n > 25 {
   319  					// Double-check that GC utilization looks OK.
   320  
   321  					// At this alloc/scan rate, the pacer should be extremely close to the goal utilization.
   322  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
   323  					// Make sure GC utilization has mostly levelled off.
   324  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.05)
   325  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[11].gcUtilization, 0.05)
   326  				}
   327  			},
   328  		},
   329  		{
   330  			// This test sets a slow allocation rate and a medium heap (around 10x the min heap size)
   331  			// to try to minimize the difference between the trigger and the goal.
   332  			name:          "MediumHeapSlowAlloc",
   333  			gcPercent:     100,
   334  			memoryLimit:   math.MaxInt64,
   335  			globalsBytes:  32 << 10,
   336  			nCores:        8,
   337  			allocRate:     constant(1.0),
   338  			scanRate:      constant(2048.0),
   339  			growthRate:    constant(2.0).sum(ramp(-1.0, 8)),
   340  			scannableFrac: constant(0.01),
   341  			stackBytes:    constant(8192),
   342  			length:        50,
   343  			checker: func(t *testing.T, c []gcCycleResult) {
   344  				n := len(c)
   345  				if n > 9 {
   346  					// After the 4th GC, the heap will stop growing.
   347  					// First, let's make sure we're finishing near the goal, with some extra
   348  					// room because we're probably going to be triggering early.
   349  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.925, 1.025)
   350  					// Next, let's make sure there's some minimum distance between the goal
   351  					// and the trigger. It should be proportional to the runway (hence the
   352  					// trigger ratio check, instead of a check against the runway).
   353  					assertInRange(t, "trigger ratio", c[n-1].triggerRatio(), 0.925, 0.975)
   354  				}
   355  				if n > 25 {
   356  					// Double-check that GC utilization looks OK.
   357  
   358  					// At this alloc/scan rate, the pacer should be extremely close to the goal utilization.
   359  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
   360  					// Make sure GC utilization has mostly levelled off.
   361  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.05)
   362  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[11].gcUtilization, 0.05)
   363  				}
   364  			},
   365  		},
   366  		{
   367  			// This test sets a slow allocation rate and a large heap to try to minimize the
   368  			// difference between the trigger and the goal.
   369  			name:          "LargeHeapSlowAlloc",
   370  			gcPercent:     100,
   371  			memoryLimit:   math.MaxInt64,
   372  			globalsBytes:  32 << 10,
   373  			nCores:        8,
   374  			allocRate:     constant(1.0),
   375  			scanRate:      constant(2048.0),
   376  			growthRate:    constant(4.0).sum(ramp(-3.0, 12)),
   377  			scannableFrac: constant(0.01),
   378  			stackBytes:    constant(8192),
   379  			length:        50,
   380  			checker: func(t *testing.T, c []gcCycleResult) {
   381  				n := len(c)
   382  				if n > 13 {
   383  					// After the 4th GC, the heap will stop growing.
   384  					// First, let's make sure we're finishing near the goal.
   385  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
   386  					// Next, let's make sure there's some minimum distance between the goal
   387  					// and the trigger. It should be around the default minimum heap size.
   388  					assertInRange(t, "runway", c[n-1].runway(), DefaultHeapMinimum-64<<10, DefaultHeapMinimum+64<<10)
   389  				}
   390  				if n > 25 {
   391  					// Double-check that GC utilization looks OK.
   392  
   393  					// At this alloc/scan rate, the pacer should be extremely close to the goal utilization.
   394  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
   395  					// Make sure GC utilization has mostly levelled off.
   396  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.05)
   397  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[11].gcUtilization, 0.05)
   398  				}
   399  			},
   400  		},
   401  		{
   402  			// The most basic test case with a memory limit: a steady-state heap.
   403  			// Growth to an O(MiB) heap, then constant heap size, alloc/scan rates.
   404  			// Provide a lot of room for the limit. Essentially, this should behave just like
   405  			// the "Steady" test. Note that we don't simulate non-heap overheads, so the
   406  			// memory limit and the heap limit are identical.
   407  			name:          "SteadyMemoryLimit",
   408  			gcPercent:     100,
   409  			memoryLimit:   512 << 20,
   410  			globalsBytes:  32 << 10,
   411  			nCores:        8,
   412  			allocRate:     constant(33.0),
   413  			scanRate:      constant(1024.0),
   414  			growthRate:    constant(2.0).sum(ramp(-1.0, 12)),
   415  			scannableFrac: constant(1.0),
   416  			stackBytes:    constant(8192),
   417  			length:        50,
   418  			checker: func(t *testing.T, c []gcCycleResult) {
   419  				n := len(c)
   420  				if peak := c[n-1].heapPeak; peak >= applyMemoryLimitHeapGoalHeadroom(512<<20) {
   421  					t.Errorf("peak heap size reaches heap limit: %d", peak)
   422  				}
   423  				if n >= 25 {
   424  					// At this alloc/scan rate, the pacer should be extremely close to the goal utilization.
   425  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
   426  
   427  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles.
   428  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
   429  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
   430  				}
   431  			},
   432  		},
   433  		{
   434  			// This is the same as the previous test, but gcPercent = -1, so the heap *should* grow
   435  			// all the way to the peak.
   436  			name:          "SteadyMemoryLimitNoGCPercent",
   437  			gcPercent:     -1,
   438  			memoryLimit:   512 << 20,
   439  			globalsBytes:  32 << 10,
   440  			nCores:        8,
   441  			allocRate:     constant(33.0),
   442  			scanRate:      constant(1024.0),
   443  			growthRate:    constant(2.0).sum(ramp(-1.0, 12)),
   444  			scannableFrac: constant(1.0),
   445  			stackBytes:    constant(8192),
   446  			length:        50,
   447  			checker: func(t *testing.T, c []gcCycleResult) {
   448  				n := len(c)
   449  				if goal := c[n-1].heapGoal; goal != applyMemoryLimitHeapGoalHeadroom(512<<20) {
   450  					t.Errorf("heap goal is not the heap limit: %d", goal)
   451  				}
   452  				if n >= 25 {
   453  					// At this alloc/scan rate, the pacer should be extremely close to the goal utilization.
   454  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
   455  
   456  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles.
   457  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
   458  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
   459  				}
   460  			},
   461  		},
   462  		{
   463  			// This test ensures that the pacer doesn't fall over even when the live heap exceeds
   464  			// the memory limit. It also makes sure GC utilization actually rises to push back.
   465  			name:          "ExceedMemoryLimit",
   466  			gcPercent:     100,
   467  			memoryLimit:   512 << 20,
   468  			globalsBytes:  32 << 10,
   469  			nCores:        8,
   470  			allocRate:     constant(33.0),
   471  			scanRate:      constant(1024.0),
   472  			growthRate:    constant(3.5).sum(ramp(-2.5, 12)),
   473  			scannableFrac: constant(1.0),
   474  			stackBytes:    constant(8192),
   475  			length:        50,
   476  			checker: func(t *testing.T, c []gcCycleResult) {
   477  				n := len(c)
   478  				if n > 12 {
   479  					// We're way over the memory limit, so we want to make sure our goal is set
   480  					// as low as it possibly can be.
   481  					if goal, live := c[n-1].heapGoal, c[n-1].heapLive; goal != live {
   482  						t.Errorf("heap goal is not equal to live heap: %d != %d", goal, live)
   483  					}
   484  				}
   485  				if n >= 25 {
   486  					// Due to memory pressure, we should scale to 100% GC CPU utilization.
   487  					// Note that in practice this won't actually happen because of the CPU limiter,
   488  					// but it's not the pacer's job to limit CPU usage.
   489  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, 1.0, 0.005)
   490  
   491  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles.
   492  					// In this case, that just means it's not wavering around a whole bunch.
   493  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
   494  				}
   495  			},
   496  		},
   497  		{
   498  			// Same as the previous test, but with gcPercent = -1.
   499  			name:          "ExceedMemoryLimitNoGCPercent",
   500  			gcPercent:     -1,
   501  			memoryLimit:   512 << 20,
   502  			globalsBytes:  32 << 10,
   503  			nCores:        8,
   504  			allocRate:     constant(33.0),
   505  			scanRate:      constant(1024.0),
   506  			growthRate:    constant(3.5).sum(ramp(-2.5, 12)),
   507  			scannableFrac: constant(1.0),
   508  			stackBytes:    constant(8192),
   509  			length:        50,
   510  			checker: func(t *testing.T, c []gcCycleResult) {
   511  				n := len(c)
   512  				if n < 10 {
   513  					if goal := c[n-1].heapGoal; goal != applyMemoryLimitHeapGoalHeadroom(512<<20) {
   514  						t.Errorf("heap goal is not the heap limit: %d", goal)
   515  					}
   516  				}
   517  				if n > 12 {
   518  					// We're way over the memory limit, so we want to make sure our goal is set
   519  					// as low as it possibly can be.
   520  					if goal, live := c[n-1].heapGoal, c[n-1].heapLive; goal != live {
   521  						t.Errorf("heap goal is not equal to live heap: %d != %d", goal, live)
   522  					}
   523  				}
   524  				if n >= 25 {
   525  					// Due to memory pressure, we should scale to 100% GC CPU utilization.
   526  					// Note that in practice this won't actually happen because of the CPU limiter,
   527  					// but it's not the pacer's job to limit CPU usage.
   528  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, 1.0, 0.005)
   529  
   530  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles.
   531  					// In this case, that just means it's not wavering around a whole bunch.
   532  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
   533  				}
   534  			},
   535  		},
   536  		{
   537  			// This test ensures that the pacer maintains the memory limit as the heap grows.
   538  			name:          "MaintainMemoryLimit",
   539  			gcPercent:     100,
   540  			memoryLimit:   512 << 20,
   541  			globalsBytes:  32 << 10,
   542  			nCores:        8,
   543  			allocRate:     constant(33.0),
   544  			scanRate:      constant(1024.0),
   545  			growthRate:    constant(3.0).sum(ramp(-2.0, 12)),
   546  			scannableFrac: constant(1.0),
   547  			stackBytes:    constant(8192),
   548  			length:        50,
   549  			checker: func(t *testing.T, c []gcCycleResult) {
   550  				n := len(c)
   551  				if n > 12 {
   552  					// We're trying to saturate the memory limit.
   553  					if goal := c[n-1].heapGoal; goal != applyMemoryLimitHeapGoalHeadroom(512<<20) {
   554  						t.Errorf("heap goal is not the heap limit: %d", goal)
   555  					}
   556  				}
   557  				if n >= 25 {
   558  					// At this alloc/scan rate, the pacer should be extremely close to the goal utilization,
   559  					// even with the additional memory pressure.
   560  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
   561  
   562  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles and
   563  					// that it's meeting its goal.
   564  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
   565  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
   566  				}
   567  			},
   568  		},
   569  		{
   570  			// Same as the previous test, but with gcPercent = -1.
   571  			name:          "MaintainMemoryLimitNoGCPercent",
   572  			gcPercent:     -1,
   573  			memoryLimit:   512 << 20,
   574  			globalsBytes:  32 << 10,
   575  			nCores:        8,
   576  			allocRate:     constant(33.0),
   577  			scanRate:      constant(1024.0),
   578  			growthRate:    constant(3.0).sum(ramp(-2.0, 12)),
   579  			scannableFrac: constant(1.0),
   580  			stackBytes:    constant(8192),
   581  			length:        50,
   582  			checker: func(t *testing.T, c []gcCycleResult) {
   583  				n := len(c)
   584  				if goal := c[n-1].heapGoal; goal != applyMemoryLimitHeapGoalHeadroom(512<<20) {
   585  					t.Errorf("heap goal is not the heap limit: %d", goal)
   586  				}
   587  				if n >= 25 {
   588  					// At this alloc/scan rate, the pacer should be extremely close to the goal utilization,
   589  					// even with the additional memory pressure.
   590  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, GCGoalUtilization, 0.005)
   591  
   592  					// Make sure the pacer settles into a non-degenerate state in at least 25 GC cycles and
   593  					// that it's meeting its goal.
   594  					assertInEpsilon(t, "GC utilization", c[n-1].gcUtilization, c[n-2].gcUtilization, 0.005)
   595  					assertInRange(t, "goal ratio", c[n-1].goalRatio(), 0.95, 1.05)
   596  				}
   597  			},
   598  		},
   599  		// TODO(mknyszek): Write a test that exercises the pacer's hard goal.
   600  		// This is difficult in the idealized model this testing framework places
   601  		// the pacer in, because the calculated overshoot is directly proportional
   602  		// to the runway for the case of the expected work.
   603  		// However, it is still possible to trigger this case if something exceptional
   604  		// happens between calls to revise; the framework just doesn't support this yet.
   605  	} {
   606  		e := e
   607  		t.Run(e.name, func(t *testing.T) {
   608  			t.Parallel()
   609  
   610  			c := NewGCController(e.gcPercent, e.memoryLimit)
   611  			var bytesAllocatedBlackLast int64
   612  			results := make([]gcCycleResult, 0, e.length)
   613  			for i := 0; i < e.length; i++ {
   614  				cycle := e.next()
   615  				c.StartCycle(cycle.stackBytes, e.globalsBytes, cycle.scannableFrac, e.nCores)
   616  
   617  				// Update pacer incrementally as we complete scan work.
   618  				const (
   619  					revisePeriod = 500 * time.Microsecond
   620  					rateConv     = 1024 * float64(revisePeriod) / float64(time.Millisecond)
   621  				)
   622  				var nextHeapMarked int64
   623  				if i == 0 {
   624  					nextHeapMarked = initialHeapBytes
   625  				} else {
   626  					nextHeapMarked = int64(float64(int64(c.HeapMarked())-bytesAllocatedBlackLast) * cycle.growthRate)
   627  				}
   628  				globalsScanWorkLeft := int64(e.globalsBytes)
   629  				stackScanWorkLeft := int64(cycle.stackBytes)
   630  				heapScanWorkLeft := int64(float64(nextHeapMarked) * cycle.scannableFrac)
   631  				doWork := func(work int64) (int64, int64, int64) {
   632  					var deltas [3]int64
   633  
   634  					// Do globals work first, then stacks, then heap.
   635  					for i, workLeft := range []*int64{&globalsScanWorkLeft, &stackScanWorkLeft, &heapScanWorkLeft} {
   636  						if *workLeft == 0 {
   637  							continue
   638  						}
   639  						if *workLeft > work {
   640  							deltas[i] += work
   641  							*workLeft -= work
   642  							work = 0
   643  							break
   644  						} else {
   645  							deltas[i] += *workLeft
   646  							work -= *workLeft
   647  							*workLeft = 0
   648  						}
   649  					}
   650  					return deltas[0], deltas[1], deltas[2]
   651  				}
   652  				var (
   653  					gcDuration          int64
   654  					assistTime          int64
   655  					bytesAllocatedBlack int64
   656  				)
   657  				for heapScanWorkLeft+stackScanWorkLeft+globalsScanWorkLeft > 0 {
   658  					// Simulate GC assist pacing.
   659  					//
   660  					// Note that this is an idealized view of the GC assist pacing
   661  					// mechanism.
   662  
   663  					// From the assist ratio and the alloc and scan rates, we can idealize what
   664  					// the GC CPU utilization looks like.
   665  					//
   666  					// We start with assistRatio = (bytes of scan work) / (bytes of runway) (by definition).
   667  					//
   668  					// Over revisePeriod, we can also calculate how many bytes are scanned and
   669  					// allocated, given some GC CPU utilization u:
   670  					//
   671  					//     bytesScanned   = scanRate  * rateConv * nCores * u
   672  					//     bytesAllocated = allocRate * rateConv * nCores * (1 - u)
   673  					//
   674  					// During revisePeriod, assistRatio is kept constant, and GC assists kick in to
   675  					// maintain it. Specifically, they act to prevent too many bytes being allocated
   676  					// compared to how many bytes are scanned. It directly defines the ratio of
   677  					// bytesScanned to bytesAllocated over this period, hence:
   678  					//
   679  					//     assistRatio = bytesScanned / bytesAllocated
   680  					//
   681  					// From this, we can solve for utilization, because everything else has already
   682  					// been determined:
   683  					//
   684  					//     assistRatio = (scanRate * rateConv * nCores * u) / (allocRate * rateConv * nCores * (1 - u))
   685  					//     assistRatio = (scanRate * u) / (allocRate * (1 - u))
   686  					//     assistRatio * allocRate * (1-u) = scanRate * u
   687  					//     assistRatio * allocRate - assistRatio * allocRate * u = scanRate * u
   688  					//     assistRatio * allocRate = assistRatio * allocRate * u + scanRate * u
   689  					//     assistRatio * allocRate = (assistRatio * allocRate + scanRate) * u
   690  					//     u = (assistRatio * allocRate) / (assistRatio * allocRate + scanRate)
   691  					//
   692  					// Note that this may give a utilization that is _less_ than GCBackgroundUtilization,
   693  					// which isn't possible in practice because of dedicated workers. Thus, this case
   694  					// must be interpreted as GC assists not kicking in at all, and just round up. All
   695  					// downstream values will then have this accounted for.
   696  					assistRatio := c.AssistWorkPerByte()
   697  					utilization := assistRatio * cycle.allocRate / (assistRatio*cycle.allocRate + cycle.scanRate)
   698  					if utilization < GCBackgroundUtilization {
   699  						utilization = GCBackgroundUtilization
   700  					}
   701  
   702  					// Knowing the utilization, calculate bytesScanned and bytesAllocated.
   703  					bytesScanned := int64(cycle.scanRate * rateConv * float64(e.nCores) * utilization)
   704  					bytesAllocated := int64(cycle.allocRate * rateConv * float64(e.nCores) * (1 - utilization))
   705  
   706  					// Subtract work from our model.
   707  					globalsScanned, stackScanned, heapScanned := doWork(bytesScanned)
   708  
   709  					// doWork may not use all of bytesScanned.
   710  					// In this case, the GC actually ends sometime in this period.
   711  					// Let's figure out when, exactly, and adjust bytesAllocated too.
   712  					actualElapsed := revisePeriod
   713  					actualAllocated := bytesAllocated
   714  					if actualScanned := globalsScanned + stackScanned + heapScanned; actualScanned < bytesScanned {
   715  						// actualScanned = scanRate * rateConv * (t / revisePeriod) * nCores * u
   716  						// => t = actualScanned * revisePeriod / (scanRate * rateConv * nCores * u)
   717  						actualElapsed = time.Duration(float64(actualScanned) * float64(revisePeriod) / (cycle.scanRate * rateConv * float64(e.nCores) * utilization))
   718  						actualAllocated = int64(cycle.allocRate * rateConv * float64(actualElapsed) / float64(revisePeriod) * float64(e.nCores) * (1 - utilization))
   719  					}
   720  
   721  					// Ask the pacer to revise.
   722  					c.Revise(GCControllerReviseDelta{
   723  						HeapLive:        actualAllocated,
   724  						HeapScan:        int64(float64(actualAllocated) * cycle.scannableFrac),
   725  						HeapScanWork:    heapScanned,
   726  						StackScanWork:   stackScanned,
   727  						GlobalsScanWork: globalsScanned,
   728  					})
   729  
   730  					// Accumulate variables.
   731  					assistTime += int64(float64(actualElapsed) * float64(e.nCores) * (utilization - GCBackgroundUtilization))
   732  					gcDuration += int64(actualElapsed)
   733  					bytesAllocatedBlack += actualAllocated
   734  				}
   735  
   736  				// Put together the results, log them, and concatenate them.
   737  				result := gcCycleResult{
   738  					cycle:         i + 1,
   739  					heapLive:      c.HeapMarked(),
   740  					heapScannable: int64(float64(int64(c.HeapMarked())-bytesAllocatedBlackLast) * cycle.scannableFrac),
   741  					heapTrigger:   c.Triggered(),
   742  					heapPeak:      c.HeapLive(),
   743  					heapGoal:      c.HeapGoal(),
   744  					gcUtilization: float64(assistTime)/(float64(gcDuration)*float64(e.nCores)) + GCBackgroundUtilization,
   745  				}
   746  				t.Log("GC", result.String())
   747  				results = append(results, result)
   748  
   749  				// Run the checker for this test.
   750  				e.check(t, results)
   751  
   752  				c.EndCycle(uint64(nextHeapMarked+bytesAllocatedBlack), assistTime, gcDuration, e.nCores)
   753  
   754  				bytesAllocatedBlackLast = bytesAllocatedBlack
   755  			}
   756  		})
   757  	}
   758  }
   759  
   760  type gcExecTest struct {
   761  	name string
   762  
   763  	gcPercent    int
   764  	memoryLimit  int64
   765  	globalsBytes uint64
   766  	nCores       int
   767  
   768  	allocRate     float64Stream // > 0, KiB / cpu-ms
   769  	scanRate      float64Stream // > 0, KiB / cpu-ms
   770  	growthRate    float64Stream // > 0
   771  	scannableFrac float64Stream // Clamped to [0, 1]
   772  	stackBytes    float64Stream // Multiple of 2048.
   773  	length        int
   774  
   775  	checker func(*testing.T, []gcCycleResult)
   776  }
   777  
   778  // minRate is an arbitrary minimum for allocRate, scanRate, and growthRate.
   779  // These values just cannot be zero.
   780  const minRate = 0.0001
   781  
   782  func (e *gcExecTest) next() gcCycle {
   783  	return gcCycle{
   784  		allocRate:     e.allocRate.min(minRate)(),
   785  		scanRate:      e.scanRate.min(minRate)(),
   786  		growthRate:    e.growthRate.min(minRate)(),
   787  		scannableFrac: e.scannableFrac.limit(0, 1)(),
   788  		stackBytes:    uint64(e.stackBytes.quantize(2048).min(0)()),
   789  	}
   790  }
   791  
   792  func (e *gcExecTest) check(t *testing.T, results []gcCycleResult) {
   793  	t.Helper()
   794  
   795  	// Do some basic general checks first.
   796  	n := len(results)
   797  	switch n {
   798  	case 0:
   799  		t.Fatal("no results passed to check")
   800  		return
   801  	case 1:
   802  		if results[0].cycle != 1 {
   803  			t.Error("first cycle has incorrect number")
   804  		}
   805  	default:
   806  		if results[n-1].cycle != results[n-2].cycle+1 {
   807  			t.Error("cycle numbers out of order")
   808  		}
   809  	}
   810  	if u := results[n-1].gcUtilization; u < 0 || u > 1 {
   811  		t.Fatal("GC utilization not within acceptable bounds")
   812  	}
   813  	if s := results[n-1].heapScannable; s < 0 {
   814  		t.Fatal("heapScannable is negative")
   815  	}
   816  	if e.checker == nil {
   817  		t.Fatal("test-specific checker is missing")
   818  	}
   819  
   820  	// Run the test-specific checker.
   821  	e.checker(t, results)
   822  }
   823  
   824  type gcCycle struct {
   825  	allocRate     float64
   826  	scanRate      float64
   827  	growthRate    float64
   828  	scannableFrac float64
   829  	stackBytes    uint64
   830  }
   831  
   832  type gcCycleResult struct {
   833  	cycle int
   834  
   835  	// These come directly from the pacer, so uint64.
   836  	heapLive    uint64
   837  	heapTrigger uint64
   838  	heapGoal    uint64
   839  	heapPeak    uint64
   840  
   841  	// These are produced by the simulation, so int64 and
   842  	// float64 are more appropriate, so that we can check for
   843  	// bad states in the simulation.
   844  	heapScannable int64
   845  	gcUtilization float64
   846  }
   847  
   848  func (r *gcCycleResult) goalRatio() float64 {
   849  	return float64(r.heapPeak) / float64(r.heapGoal)
   850  }
   851  
   852  func (r *gcCycleResult) runway() float64 {
   853  	return float64(r.heapGoal - r.heapTrigger)
   854  }
   855  
   856  func (r *gcCycleResult) triggerRatio() float64 {
   857  	return float64(r.heapTrigger-r.heapLive) / float64(r.heapGoal-r.heapLive)
   858  }
   859  
   860  func (r *gcCycleResult) String() string {
   861  	return fmt.Sprintf("%d %2.1f%% %d->%d->%d (goal: %d)", r.cycle, r.gcUtilization*100, r.heapLive, r.heapTrigger, r.heapPeak, r.heapGoal)
   862  }
   863  
   864  func assertInEpsilon(t *testing.T, name string, a, b, epsilon float64) {
   865  	t.Helper()
   866  	assertInRange(t, name, a, b-epsilon, b+epsilon)
   867  }
   868  
   869  func assertInRange(t *testing.T, name string, a, min, max float64) {
   870  	t.Helper()
   871  	if a < min || a > max {
   872  		t.Errorf("%s not in range (%f, %f): %f", name, min, max, a)
   873  	}
   874  }
   875  
   876  // float64Stream is a function that generates an infinite stream of
   877  // float64 values when called repeatedly.
   878  type float64Stream func() float64
   879  
   880  // constant returns a stream that generates the value c.
   881  func constant(c float64) float64Stream {
   882  	return func() float64 {
   883  		return c
   884  	}
   885  }
   886  
   887  // unit returns a stream that generates a single peak with
   888  // amplitude amp, followed by zeroes.
   889  //
   890  // In another manner of speaking, this is the Kronecker delta.
   891  func unit(amp float64) float64Stream {
   892  	dropped := false
   893  	return func() float64 {
   894  		if dropped {
   895  			return 0
   896  		}
   897  		dropped = true
   898  		return amp
   899  	}
   900  }
   901  
   902  // oscillate returns a stream that oscillates sinusoidally
   903  // with the given amplitude, phase, and period.
   904  func oscillate(amp, phase float64, period int) float64Stream {
   905  	var cycle int
   906  	return func() float64 {
   907  		p := float64(cycle)/float64(period)*2*math.Pi + phase
   908  		cycle++
   909  		if cycle == period {
   910  			cycle = 0
   911  		}
   912  		return math.Sin(p) * amp
   913  	}
   914  }
   915  
   916  // ramp returns a stream that moves from zero to height
   917  // over the course of length steps.
   918  func ramp(height float64, length int) float64Stream {
   919  	var cycle int
   920  	return func() float64 {
   921  		h := height * float64(cycle) / float64(length)
   922  		if cycle < length {
   923  			cycle++
   924  		}
   925  		return h
   926  	}
   927  }
   928  
   929  // random returns a stream that generates random numbers
   930  // between -amp and amp.
   931  func random(amp float64, seed int64) float64Stream {
   932  	r := rand.New(rand.NewSource(seed))
   933  	return func() float64 {
   934  		return ((r.Float64() - 0.5) * 2) * amp
   935  	}
   936  }
   937  
   938  // delay returns a new stream which is a buffered version
   939  // of f: it returns zero for cycles steps, followed by f.
   940  func (f float64Stream) delay(cycles int) float64Stream {
   941  	zeroes := 0
   942  	return func() float64 {
   943  		if zeroes < cycles {
   944  			zeroes++
   945  			return 0
   946  		}
   947  		return f()
   948  	}
   949  }
   950  
   951  // scale returns a new stream that is f, but attenuated by a
   952  // constant factor.
   953  func (f float64Stream) scale(amt float64) float64Stream {
   954  	return func() float64 {
   955  		return f() * amt
   956  	}
   957  }
   958  
   959  // offset returns a new stream that is f but offset by amt
   960  // at each step.
   961  func (f float64Stream) offset(amt float64) float64Stream {
   962  	return func() float64 {
   963  		old := f()
   964  		return old + amt
   965  	}
   966  }
   967  
   968  // sum returns a new stream that is the sum of all input streams
   969  // at each step.
   970  func (f float64Stream) sum(fs ...float64Stream) float64Stream {
   971  	return func() float64 {
   972  		sum := f()
   973  		for _, s := range fs {
   974  			sum += s()
   975  		}
   976  		return sum
   977  	}
   978  }
   979  
   980  // quantize returns a new stream that rounds f to a multiple
   981  // of mult at each step.
   982  func (f float64Stream) quantize(mult float64) float64Stream {
   983  	return func() float64 {
   984  		r := f() / mult
   985  		if r < 0 {
   986  			return math.Ceil(r) * mult
   987  		}
   988  		return math.Floor(r) * mult
   989  	}
   990  }
   991  
   992  // min returns a new stream that replaces all values produced
   993  // by f lower than min with min.
   994  func (f float64Stream) min(min float64) float64Stream {
   995  	return func() float64 {
   996  		return math.Max(min, f())
   997  	}
   998  }
   999  
  1000  // max returns a new stream that replaces all values produced
  1001  // by f higher than max with max.
  1002  func (f float64Stream) max(max float64) float64Stream {
  1003  	return func() float64 {
  1004  		return math.Min(max, f())
  1005  	}
  1006  }
  1007  
  1008  // limit returns a new stream that replaces all values produced
  1009  // by f lower than min with min and higher than max with max.
  1010  func (f float64Stream) limit(min, max float64) float64Stream {
  1011  	return func() float64 {
  1012  		v := f()
  1013  		if v < min {
  1014  			v = min
  1015  		} else if v > max {
  1016  			v = max
  1017  		}
  1018  		return v
  1019  	}
  1020  }
  1021  
  1022  func applyMemoryLimitHeapGoalHeadroom(goal uint64) uint64 {
  1023  	headroom := goal / 100 * MemoryLimitHeapGoalHeadroomPercent
  1024  	if headroom < MemoryLimitMinHeapGoalHeadroom {
  1025  		headroom = MemoryLimitMinHeapGoalHeadroom
  1026  	}
  1027  	if goal < headroom || goal-headroom < headroom {
  1028  		goal = headroom
  1029  	} else {
  1030  		goal -= headroom
  1031  	}
  1032  	return goal
  1033  }
  1034  
  1035  func TestIdleMarkWorkerCount(t *testing.T) {
  1036  	const workers = 10
  1037  	c := NewGCController(100, math.MaxInt64)
  1038  	c.SetMaxIdleMarkWorkers(workers)
  1039  	for i := 0; i < workers; i++ {
  1040  		if !c.NeedIdleMarkWorker() {
  1041  			t.Fatalf("expected to need idle mark workers: i=%d", i)
  1042  		}
  1043  		if !c.AddIdleMarkWorker() {
  1044  			t.Fatalf("expected to be able to add an idle mark worker: i=%d", i)
  1045  		}
  1046  	}
  1047  	if c.NeedIdleMarkWorker() {
  1048  		t.Fatalf("expected to not need idle mark workers")
  1049  	}
  1050  	if c.AddIdleMarkWorker() {
  1051  		t.Fatalf("expected to not be able to add an idle mark worker")
  1052  	}
  1053  	for i := 0; i < workers; i++ {
  1054  		c.RemoveIdleMarkWorker()
  1055  		if !c.NeedIdleMarkWorker() {
  1056  			t.Fatalf("expected to need idle mark workers after removal: i=%d", i)
  1057  		}
  1058  	}
  1059  	for i := 0; i < workers-1; i++ {
  1060  		if !c.AddIdleMarkWorker() {
  1061  			t.Fatalf("expected to be able to add idle mark workers after adding again: i=%d", i)
  1062  		}
  1063  	}
  1064  	for i := 0; i < 10; i++ {
  1065  		if !c.AddIdleMarkWorker() {
  1066  			t.Fatalf("expected to be able to add idle mark workers interleaved: i=%d", i)
  1067  		}
  1068  		if c.AddIdleMarkWorker() {
  1069  			t.Fatalf("expected to not be able to add idle mark workers interleaved: i=%d", i)
  1070  		}
  1071  		c.RemoveIdleMarkWorker()
  1072  	}
  1073  	// Support the max being below the count.
  1074  	c.SetMaxIdleMarkWorkers(0)
  1075  	if c.NeedIdleMarkWorker() {
  1076  		t.Fatalf("expected to not need idle mark workers after capacity set to 0")
  1077  	}
  1078  	if c.AddIdleMarkWorker() {
  1079  		t.Fatalf("expected to not be able to add idle mark workers after capacity set to 0")
  1080  	}
  1081  	for i := 0; i < workers-1; i++ {
  1082  		c.RemoveIdleMarkWorker()
  1083  	}
  1084  	if c.NeedIdleMarkWorker() {
  1085  		t.Fatalf("expected to not need idle mark workers after capacity set to 0")
  1086  	}
  1087  	if c.AddIdleMarkWorker() {
  1088  		t.Fatalf("expected to not be able to add idle mark workers after capacity set to 0")
  1089  	}
  1090  	c.SetMaxIdleMarkWorkers(1)
  1091  	if !c.NeedIdleMarkWorker() {
  1092  		t.Fatalf("expected to need idle mark workers after capacity set to 1")
  1093  	}
  1094  	if !c.AddIdleMarkWorker() {
  1095  		t.Fatalf("expected to be able to add idle mark workers after capacity set to 1")
  1096  	}
  1097  }
  1098  

View as plain text