From 1ccbd90e16e78201753992dacefdd318988f4649 Mon Sep 17 00:00:00 2001 From: Nathaniel Kofalt Date: Thu, 16 May 2024 08:46:46 -0500 Subject: [PATCH] Add note about performance & associated unit tests --- .github/workflows/build.yml | 2 +- memoize_test.go | 55 ++++++++++++++++++++++++++++++++++--- readme.md | 13 +++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29e1999..094e572 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,4 +18,4 @@ jobs: - run: go build -v - - run: go test -count 1 ./... + - run: go test -race -count 10 diff --git a/memoize_test.go b/memoize_test.go index f700b07..cd8754d 100644 --- a/memoize_test.go +++ b/memoize_test.go @@ -2,6 +2,8 @@ package memoize import ( "errors" + "sync" + "sync/atomic" "testing" "time" @@ -84,8 +86,7 @@ func (t *F) TestFailure() { t.So(cached, ShouldBeTrue) } -// TestBasicGenerics adopts the code from readme.md into a simple test case -// but using generics. +// TestBasicGenerics adopts the code from readme.md into a simple test case but using generics. func (t *F) TestBasicGenerics() { expensiveCalls := 0 @@ -116,8 +117,7 @@ func (t *F) TestBasicGenerics() { t.So(cached, ShouldBeFalse) } -// TestFailureGenerics checks that failed function values are not cached -// when using generics. +// TestFailureGenerics checks that failed function values are not cached when using generics. func (t *F) TestFailureGenerics() { calls := 0 @@ -152,3 +152,50 @@ func (t *F) TestFailureGenerics() { t.So(result, ShouldEqual, 2) t.So(cached, ShouldBeTrue) } + +// TestConcurrency runs 10,000 goroutines of ~10ms tasks and ensures at least 99.9% deduplication occurs. +func (t *F) TestConcurrency() { + var counter atomic.Int64 + var wg sync.WaitGroup + + expensive := func() (int64, error) { + time.Sleep(10 * time.Millisecond) + return counter.Add(1), nil + } + + cache := NewMemoizer(90*time.Second, 10*time.Minute) + + wg.Add(1000) + for range 1000 { + go func() { + Call(cache, "key1", expensive) + wg.Done() + }() + } + + wg.Wait() + t.So(counter.Load(), ShouldBeLessThanOrEqualTo, 10) +} + +// TestTrivialConcurrency hammers 10,000 trivial goroutines and ensures at least 95% deduplication occurs. +func (t *F) TestTrivialConcurrency() { + var counter atomic.Int64 + var wg sync.WaitGroup + + expensive := func() (int64, error) { + return counter.Add(1), nil + } + + cache := NewMemoizer(90*time.Second, 10*time.Minute) + + wg.Add(10000) + for range 10000 { + go func() { + Call(cache, "key1", expensive) + wg.Done() + }() + } + + wg.Wait() + t.So(counter.Load(), ShouldBeLessThanOrEqualTo, 500) +} diff --git a/readme.md b/readme.md index 43db044..d0352c9 100644 --- a/readme.md +++ b/readme.md @@ -81,3 +81,16 @@ result, err, cached = memoize.Call(cache, "key1", expensive) // This uses a new cache key, so expensive is called again result, err, cached = memoize.Call(cache, "key2", expensive) ``` + +### Note about performance + +Go-memoize is extremely fast, but does not guarantee 100% deduplication.
+This is an intentional trade-off, because the goal of a memoizer is to increase performance!
+Most users do not need to worry about this. + +A memoizer is best suited for functions that take a few milliseconds or greater.
+Examples: checking disk, make an HTTP call, calculating expensive values...
+For these use cases you can expect deduplication of 99.9% or greater, confirmed by unit tests. + +If you have a very tiny function that only takes nanoseconds, you may see a few extra calls.
+See issue #7 for more information.