github.com/ice-blockchain/go/src@v0.0.0-20240403114104-1564d284e521/runtime/mgcpacer_test.go (about) 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 }