github.com/searKing/golang/go@v1.2.117/exp/sync/fixedpool_test.go (about) 1 // Copyright 2022 The searKing Author. 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 sync_test 6 7 import ( 8 "fmt" 9 "path" 10 "path/filepath" 11 "runtime" 12 "runtime/debug" 13 "sort" 14 "strconv" 15 "sync/atomic" 16 "testing" 17 "time" 18 19 sync_ "github.com/searKing/golang/go/exp/sync" 20 runtime_ "github.com/searKing/golang/go/runtime" 21 ) 22 23 func fixGC() { 24 // TODO: Fix #45315 and remove this extra call. 25 // 26 // Unfortunately, it's possible for the sweep termination condition 27 // to flap, so with just one runtime.GC call, a freed object could be 28 // missed, leading this test to fail. A second call reduces the chance 29 // of this happening to zero, because sweeping actually has to finish 30 // to move on to the next GC, during which nothing will happen. 31 // 32 // See https://github.com/golang/go/issues/46500 and 33 // https://github.com/golang/go/issues/45315 for more details. 34 runtime.GOMAXPROCS(1) 35 } 36 func caller() string { 37 function, file, line := runtime_.GetCallerFuncFileLine(3) 38 return fmt.Sprintf("%s() %s:%d", path.Base(function), filepath.Base(file), line) 39 } 40 41 func testFixedPoolLenAndCap[E any](t *testing.T, p *sync_.FixedPool[E], l, c int) { 42 gotLen := p.Len() 43 gotCap := p.Cap() 44 if (gotLen != l && c >= 0) || (gotCap != c && c >= 0) { 45 t.Fatalf("%s, got %d|%d; want %d|%d", caller(), gotLen, gotCap, l, c) 46 } 47 } 48 49 func TestNewFixedPool(t *testing.T) { 50 fixGC() 51 52 // disable GC so we can control when it happens. 53 defer debug.SetGCPercent(debug.SetGCPercent(-1)) 54 var i int 55 f := func() string { 56 defer func() { i++ }() 57 return strconv.Itoa(i) 58 } 59 var p = sync_.NewFixedPool[string](f, 2) 60 testFixedPoolLenAndCap(t, p, 2, 2) 61 if g := p.TryGet(); g == nil || g.Value != "0" { 62 t.Fatalf("got %#v; want 0", g) 63 } 64 testFixedPoolLenAndCap(t, p, 1, 2) 65 p.Emplace("a") 66 testFixedPoolLenAndCap(t, p, 2, 3) 67 p.Emplace("b") 68 testFixedPoolLenAndCap(t, p, 3, 4) 69 if g := p.TryGet(); g == nil || g.Value != "1" { 70 t.Fatalf("got %#v; want 1", g) 71 } 72 if g := p.Get(); g == nil || g.Value != "a" { 73 t.Fatalf("got %#v; want a", g) 74 } 75 testFixedPoolLenAndCap(t, p, 1, 4) 76 if g := p.Get(); g.Value != "b" { 77 t.Fatalf("got %#v; want b", g) 78 } 79 testFixedPoolLenAndCap(t, p, 0, 4) 80 if g := p.TryGet(); g != nil { 81 t.Fatalf("got %#v; want nil", g) 82 } 83 // After one GC, the victim cache should keep them alive. 84 runtime.GC() 85 // drop all the items taken by Get and not be referenced by any 86 testFixedPoolLenAndCap(t, p, 2, 2) 87 // A second GC should drop the victim cache. 88 runtime.GC() 89 testFixedPoolLenAndCap(t, p, 2, 2) 90 91 // Put in a large number of items, so they spill into 92 // stealable space. 93 n := 100 94 for i := 0; i < n; i++ { 95 p.Emplace("c") 96 testFixedPoolLenAndCap(t, p, i+1+2, i+1+2) 97 } 98 testFixedPoolLenAndCap(t, p, 102, 102) 99 for i := 0; i < n; i++ { 100 if g := p.Get(); g == nil { 101 t.Fatalf("got empty") 102 } 103 } 104 testFixedPoolLenAndCap(t, p, 2, 102) 105 if g := p.TryGet(); g == nil { 106 t.Fatalf("got empty") 107 } 108 testFixedPoolLenAndCap(t, p, 1, 102) 109 // After one GC, the victim cache should keep them alive. 110 runtime.GC() 111 // drop all the items taken by Get and not be referenced by any 112 testFixedPoolLenAndCap(t, p, 3, 3) 113 // A second GC should drop the victim cache. 114 runtime.GC() 115 testFixedPoolLenAndCap(t, p, 2, 2) 116 } 117 func TestNewCachePool(t *testing.T) { 118 fixGC() 119 // disable GC so we can control when it happens. 120 defer debug.SetGCPercent(debug.SetGCPercent(-1)) 121 var p = sync_.NewCachedPool[string](nil) 122 if p.TryGet() != nil { 123 t.Fatal("expected empty") 124 } 125 p.Emplace("a") 126 testFixedPoolLenAndCap(t, p, 1, 1) 127 p.Emplace("b") 128 testFixedPoolLenAndCap(t, p, 2, 2) 129 if g := p.Get(); g.Value != "a" { 130 t.Fatalf("got %#v; want a", g) 131 } 132 testFixedPoolLenAndCap(t, p, 1, 2) 133 if g := p.Get(); g.Value != "b" { 134 t.Fatalf("got %#v; want b", g) 135 } 136 testFixedPoolLenAndCap(t, p, 0, 2) 137 if g := p.TryGet(); g != nil { 138 t.Fatalf("got %#v; want nil", g) 139 } 140 testFixedPoolLenAndCap(t, p, 0, 2) 141 142 // Put in a large number of items, so they spill into 143 // stealable space. 144 n := 100 145 for i := 0; i < n; i++ { 146 p.Emplace("c") 147 testFixedPoolLenAndCap(t, p, i+1, i+1+2) 148 } 149 testFixedPoolLenAndCap(t, p, 100, 102) 150 for i := 0; i < n; i++ { 151 if g := p.Get(); g.Value != "c" { 152 t.Fatalf("got %#v; want a", g) 153 } 154 } 155 testFixedPoolLenAndCap(t, p, 0, 102) 156 if g := p.TryGet(); g != nil { 157 t.Fatalf("got %#v; want nil", g) 158 } 159 testFixedPoolLenAndCap(t, p, 0, 102) 160 } 161 162 func TestNewTempPool(t *testing.T) { 163 fixGC() 164 // disable GC so we can control when it happens. 165 defer debug.SetGCPercent(debug.SetGCPercent(-1)) 166 var p = sync_.NewTempPool[string](nil) 167 if p.TryGet() != nil { 168 t.Fatal("expected empty") 169 } 170 testFixedPoolLenAndCap(t, p, 0, 0) 171 p.Emplace("a") 172 173 testFixedPoolLenAndCap(t, p, 1, 1) 174 p.Emplace("b") 175 testFixedPoolLenAndCap(t, p, 2, 2) 176 if g := p.Get(); g.Value != "a" { 177 t.Fatalf("got %#v; want a", g) 178 } 179 testFixedPoolLenAndCap(t, p, 1, 2) 180 if g := p.Get(); g.Value != "b" { 181 t.Fatalf("got %#v; want b", g) 182 } 183 testFixedPoolLenAndCap(t, p, 0, 2) 184 185 // Put in a large number of items, so they spill into 186 // stealable space. 187 for i := 0; i < 100; i++ { 188 p.Emplace("c") 189 testFixedPoolLenAndCap(t, p, i+1, i+1+2) 190 } 191 testFixedPoolLenAndCap(t, p, 100, 102) 192 // After one GC, the victim cache should keep them alive. 193 runtime.GC() 194 // drop all the items taken by Get and not be referenced by any 195 testFixedPoolLenAndCap(t, p, 100, 100) 196 if g := p.Get(); g.Value != "c" { 197 t.Fatalf("got %#v; want c after GC", g) 198 } 199 testFixedPoolLenAndCap(t, p, 99, 100) 200 // A second GC should drop the victim cache. 201 runtime.GC() 202 testFixedPoolLenAndCap(t, p, 0, 0) 203 if g := p.TryGet(); g != nil { 204 t.Fatalf("got %#v; want nil after second GC", g) 205 } 206 testFixedPoolLenAndCap(t, p, 0, 0) 207 } 208 209 func TestFixedPoolNilNew(t *testing.T) { 210 fixGC() 211 // disable GC so we can control when it happens. 212 defer debug.SetGCPercent(debug.SetGCPercent(-1)) 213 const LEN = 10 214 const CAP = 20 215 var p = (&sync_.FixedPool[string]{ 216 New: nil, 217 MinResidentSize: 0, 218 MaxResidentSize: LEN, 219 MaxCapacity: CAP, 220 }).Init() 221 222 testFixedPoolLenAndCap(t, p, 0, 0) 223 224 if p.TryGet() != nil { 225 t.Fatal("expected empty") 226 } 227 p.Emplace("a") 228 testFixedPoolLenAndCap(t, p, 1, 1) 229 p.Emplace("b") 230 testFixedPoolLenAndCap(t, p, 2, 2) 231 if g := p.Get(); g.Value != "a" { 232 t.Fatalf("got %#v; want a", g) 233 } 234 testFixedPoolLenAndCap(t, p, 1, 2) 235 if g := p.Get(); g.Value != "b" { 236 t.Fatalf("got %#v; want b", g) 237 } 238 testFixedPoolLenAndCap(t, p, 0, 2) 239 if g := p.TryGet(); g != nil { 240 t.Fatalf("got %#v; want nil", g) 241 } 242 testFixedPoolLenAndCap(t, p, 0, 2) 243 244 // Put in a large number of items, so they spill into 245 // stealable space. 246 for i := 0; i < 100; i++ { 247 p.Emplace("c") 248 testFixedPoolLenAndCap(t, p, i+1, i+1+2) 249 } 250 testFixedPoolLenAndCap(t, p, 100, 102) 251 // After one GC, the victim cache should keep them alive. 252 runtime.GC() 253 testFixedPoolLenAndCap(t, p, 100, 100) 254 if g := p.Get(); g.Value != "c" { 255 t.Fatalf("got %#v; want c after GC", g) 256 } 257 testFixedPoolLenAndCap(t, p, 99, 100) 258 // A second GC should drop the victim cache, try put into local first. 259 runtime.GC() 260 testFixedPoolLenAndCap(t, p, LEN, LEN) 261 262 // drain keep-alive cache 263 for i := 0; i < LEN; i++ { 264 if g := p.Get(); g == nil || g.Value != "c" { 265 t.Fatalf("#%d: got %#v; want c after GC", i, g) 266 } 267 } 268 if g := p.TryGet(); g != nil { 269 t.Fatalf("got %#v; want nil after second GC", g) 270 } 271 testFixedPoolLenAndCap(t, p, 0, LEN) 272 // After one GC, the victim cache should keep them alive. 273 // After one GC, the got object will be GC, as no reference 274 runtime.GC() 275 testFixedPoolLenAndCap(t, p, LEN, LEN) 276 // A second GC should drop the victim cache, try put into local first. 277 runtime.GC() 278 testFixedPoolLenAndCap(t, p, LEN, LEN) 279 } 280 281 func TestFixedPoolNew(t *testing.T) { 282 fixGC() 283 // disable GC so we can control when it happens. 284 defer debug.SetGCPercent(debug.SetGCPercent(-1)) 285 286 const MinLen = 2 287 const LEN = 10 288 const CAP = 20 289 i := 0 290 var p = (&sync_.FixedPool[int]{ 291 New: func() int { 292 i++ 293 return i 294 }, 295 MinResidentSize: MinLen, 296 MaxResidentSize: LEN, 297 MaxCapacity: CAP, 298 }).Init() 299 testFixedPoolLenAndCap(t, p, MinLen, MinLen) 300 301 if v := p.Get(); v.Value != 1 { 302 t.Fatalf("got %v; want 1", v.Value) 303 } 304 if v := p.Get(); v.Value != 2 { 305 t.Fatalf("got %v; want 2", v.Value) 306 } 307 308 p.Emplace(42) 309 if v := p.Get(); v.Value != 42 { 310 t.Fatalf("got %v; want 42", v) 311 } 312 313 if v := p.Get(); v.Value != 3 { 314 t.Fatalf("got %v; want 3", v) 315 } 316 } 317 318 func TestFixedPoolGCRetryPut(t *testing.T) { 319 // disable GC so we can control when it happens. 320 defer debug.SetGCPercent(debug.SetGCPercent(-1)) 321 const LEN = 1 322 const CAP = 2 323 var p = (&sync_.FixedPool[string]{ 324 New: nil, 325 MinResidentSize: 0, 326 MaxResidentSize: LEN, 327 MaxCapacity: CAP, 328 }).Init() 329 330 testFixedPoolLenAndCap(t, p, 0, 0) 331 332 if p.TryGet() != nil { 333 t.Fatal("expected empty") 334 } 335 336 // Put in a large number of items, so they spill into 337 // stealable space. 338 var N = 4 339 for i := 0; i < N; i++ { 340 p.Emplace(strconv.Itoa(i)) 341 testFixedPoolLenAndCap(t, p, i+1, i+1) 342 } 343 testFixedPoolLenAndCap(t, p, N, N) 344 // After one GC, the victim cache should keep them alive. 345 runtime.GC() 346 testFixedPoolLenAndCap(t, p, N, N) 347 if g := p.Get(); g.Value != "0" { 348 t.Fatalf("got %#v; want c after GC", g) 349 } 350 testFixedPoolLenAndCap(t, p, N-1, N) 351 // A second GC should drop the victim cache, try put into local first. 352 runtime.GC() 353 testFixedPoolLenAndCap(t, p, LEN, LEN) 354 355 // drain keep-alive cache 356 for i := 1; i < LEN+1; i++ { 357 if g := p.Get(); g == nil { 358 t.Fatalf("#%d: got nil; want %q after GC", i, strconv.Itoa(i)) 359 } 360 } 361 testFixedPoolLenAndCap(t, p, 0, LEN) 362 { 363 if g := p.TryGet(); g != nil { 364 t.Fatalf("got %#v; want nil after second GC", g.Value) 365 } 366 } 367 testFixedPoolLenAndCap(t, p, 0, LEN) 368 // After one GC, the victim cache should keep them alive. 369 runtime.GC() 370 testFixedPoolLenAndCap(t, p, LEN, LEN) 371 // A second GC should drop the victim cache, try put into local first. 372 runtime.GC() 373 testFixedPoolLenAndCap(t, p, LEN, LEN) 374 { 375 if g := p.TryGet(); g == nil { 376 t.Fatalf("got nil; want %q after GC", g.Value) 377 } 378 } 379 testFixedPoolLenAndCap(t, p, 0, LEN) 380 } 381 382 func TestFixedPoolGCReFillLocal(t *testing.T) { 383 // disable GC so we can control when it happens. 384 defer debug.SetGCPercent(debug.SetGCPercent(-1)) 385 const LEN = 1 386 const CAP = 2 387 var p = (&sync_.FixedPool[string]{ 388 New: nil, 389 MinResidentSize: 0, 390 MaxResidentSize: LEN, 391 MaxCapacity: CAP, 392 }).Init() 393 // Put in a large number of items, so they spill into 394 // stealable space. 395 for i := 0; i < CAP*2; i++ { 396 p.Emplace(strconv.Itoa(i)) 397 } 398 testFixedPoolLenAndCap(t, p, CAP*2, CAP*2) 399 400 // drain all cache 401 for i := 0; i < 2*CAP; i++ { 402 g := p.Get() 403 if i < LEN { 404 if g == nil || g.Value != strconv.Itoa(i) { 405 t.Fatalf("#%d: got %#v; want %q after GC", i, g, strconv.Itoa(i)) 406 } 407 } else { 408 if g == nil { 409 t.Fatalf("#%d: got %#v; want %q after GC", i, g, strconv.Itoa(i)) 410 } 411 } 412 testFixedPoolLenAndCap(t, p, CAP*2-i-1, CAP*2) 413 } 414 testFixedPoolLenAndCap(t, p, 0, CAP*2) 415 if g := p.TryGet(); g != nil { 416 t.Fatalf("got %#v; want nil after second GC", g) 417 } 418 testFixedPoolLenAndCap(t, p, 0, CAP*2) 419 420 // After one GC, the victim cache should keep them alive. 421 runtime.GC() 422 testFixedPoolLenAndCap(t, p, LEN, LEN) 423 // A second GC should drop the victim cache, try put into local first. 424 runtime.GC() 425 testFixedPoolLenAndCap(t, p, LEN, LEN) 426 427 // drain all cache 428 for i := 0; i < LEN; i++ { 429 g := p.Get() 430 if g == nil { 431 t.Fatalf("#%d: got nil; want not nil after GC", i) 432 } 433 } 434 testFixedPoolLenAndCap(t, p, 0, LEN) 435 if g := p.TryGet(); g != nil { 436 t.Fatalf("got %#v; want nil after second GC", g) 437 } 438 testFixedPoolLenAndCap(t, p, 0, LEN) 439 } 440 441 // Test that Pool does not hold pointers to previously cached resources. 442 func TestFixedPoolGC(t *testing.T) { 443 testFixedPool(t, true) 444 } 445 446 // Test that Pool releases resources on GC. 447 func TestFixedPoolRelease(t *testing.T) { 448 testFixedPool(t, false) 449 } 450 451 func testFixedPool(t *testing.T, drain bool) { 452 var p sync_.FixedPool[*string] 453 const N = 100 454 loop: 455 for try := 0; try < 3; try++ { 456 if try == 1 && testing.Short() { 457 testFixedPoolLenAndCap(t, &p, 0, 0) 458 break 459 } 460 var fin, fin1 uint32 461 for i := 0; i < N; i++ { 462 v := new(string) 463 runtime.SetFinalizer(v, func(vv *string) { 464 atomic.AddUint32(&fin, 1) 465 }) 466 p.Emplace(v) 467 } 468 if drain { 469 for i := 0; i < N; i++ { 470 p.Get() 471 } 472 } 473 for i := 0; i < 5; i++ { 474 runtime.GC() 475 time.Sleep(time.Duration(i*100+10) * time.Millisecond) 476 // 1 pointer can remain on stack or elsewhere 477 if fin1 = atomic.LoadUint32(&fin); fin1 >= N-1 { 478 continue loop 479 } 480 } 481 t.Fatalf("only %v out of %v resources are finalized on try %v", fin1, N, try) 482 } 483 } 484 485 func TestFixedPoolStress(t *testing.T) { 486 const P = 10 487 N := int(1e6) 488 if testing.Short() { 489 N /= 100 490 } 491 var p sync_.FixedPool[any] 492 done := make(chan bool) 493 for i := 0; i < P; i++ { 494 go func() { 495 var v any = 0 496 for j := 0; j < N; j++ { 497 if v == nil { 498 v = 0 499 } 500 p.Emplace(v) 501 e := p.Get() 502 if e != nil && e.Value != 0 { 503 t.Errorf("expect 0, got %v", v) 504 break 505 } 506 } 507 done <- true 508 }() 509 } 510 for i := 0; i < P; i++ { 511 <-done 512 } 513 } 514 515 func BenchmarkFixedPool(b *testing.B) { 516 var p sync_.FixedPool[int] 517 b.RunParallel(func(pb *testing.PB) { 518 for pb.Next() { 519 p.Emplace(1) 520 p.Get() 521 } 522 }) 523 } 524 525 func BenchmarkPoolOverflow(b *testing.B) { 526 var p sync_.FixedPool[int] 527 b.RunParallel(func(pb *testing.PB) { 528 for pb.Next() { 529 for b := 0; b < 100; b++ { 530 p.Emplace(1) 531 } 532 for b := 0; b < 100; b++ { 533 p.Get() 534 } 535 } 536 }) 537 } 538 539 // Simulate object starvation in order to force Ps to steal items 540 // from other Ps. 541 func BenchmarkPoolStarvation(b *testing.B) { 542 var p sync_.FixedPool[int] 543 count := 100 544 // Reduce number of putted items by 33 %. It creates items starvation 545 // that force P-local storage to steal items from other Ps. 546 countStarved := count - int(float32(count)*0.33) 547 b.RunParallel(func(pb *testing.PB) { 548 for pb.Next() { 549 for b := 0; b < countStarved; b++ { 550 p.Emplace(1) 551 } 552 for b := 0; b < count; b++ { 553 p.Get() 554 } 555 } 556 }) 557 } 558 559 var globalSink any 560 561 func BenchmarkPoolSTW(b *testing.B) { 562 // Take control of GC. 563 defer debug.SetGCPercent(debug.SetGCPercent(-1)) 564 565 var mstats runtime.MemStats 566 var pauses []uint64 567 568 var p sync_.FixedPool[any] 569 for i := 0; i < b.N; i++ { 570 // Put a large number of items into a pool. 571 const N = 100000 572 var item any = 42 573 for i := 0; i < N; i++ { 574 p.Emplace(item) 575 } 576 // Do a GC. 577 runtime.GC() 578 // Record pause time. 579 runtime.ReadMemStats(&mstats) 580 pauses = append(pauses, mstats.PauseNs[(mstats.NumGC+255)%256]) 581 } 582 583 // Get pause time stats. 584 sort.Slice(pauses, func(i, j int) bool { return pauses[i] < pauses[j] }) 585 var total uint64 586 for _, ns := range pauses { 587 total += ns 588 } 589 // ns/op for this benchmark is average STW time. 590 b.ReportMetric(float64(total)/float64(b.N), "ns/op") 591 b.ReportMetric(float64(pauses[len(pauses)*95/100]), "p95-ns/STW") 592 b.ReportMetric(float64(pauses[len(pauses)*50/100]), "p50-ns/STW") 593 } 594 595 func BenchmarkPoolExpensiveNew(b *testing.B) { 596 // Populate a pool with items that are expensive to construct 597 // to stress pool cleanup and subsequent reconstruction. 598 599 // Create a ballast so the GC has a non-zero heap size and 600 // runs at reasonable times. 601 globalSink = make([]byte, 8<<20) 602 defer func() { globalSink = nil }() 603 604 // Create a pool that's "expensive" to fill. 605 var p sync_.FixedPool[any] 606 var nNew uint64 607 p.New = func() any { 608 atomic.AddUint64(&nNew, 1) 609 time.Sleep(time.Millisecond) 610 return 42 611 } 612 var mstats1, mstats2 runtime.MemStats 613 runtime.ReadMemStats(&mstats1) 614 b.RunParallel(func(pb *testing.PB) { 615 // Simulate 100X the number of goroutines having items 616 // checked out from the Pool simultaneously. 617 items := make([]*sync_.FixedPoolElement[any], 100) 618 var sink []byte 619 for pb.Next() { 620 // Stress the pool. 621 for i := range items { 622 items[i] = p.Get() 623 // Simulate doing some work with this 624 // item checked out. 625 sink = make([]byte, 32<<10) 626 } 627 for i, v := range items { 628 p.Put(v) 629 items[i] = nil 630 } 631 } 632 _ = sink 633 }) 634 runtime.ReadMemStats(&mstats2) 635 636 b.ReportMetric(float64(mstats2.NumGC-mstats1.NumGC)/float64(b.N), "GCs/op") 637 b.ReportMetric(float64(nNew)/float64(b.N), "New/op") 638 }