Skip to content

Commit

Permalink
Add note about performance & associated unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kofalt committed May 16, 2024
1 parent 9e5eb99 commit 1ccbd90
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ jobs:

- run: go build -v

- run: go test -count 1 ./...
- run: go test -race -count 10
55 changes: 51 additions & 4 deletions memoize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package memoize

import (
"errors"
"sync"
"sync/atomic"
"testing"
"time"

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
13 changes: 13 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br/>
This is an intentional trade-off, because the goal of a memoizer is to increase performance!<br/>
Most users do not need to worry about this.

A memoizer is best suited for functions that take a few milliseconds or greater.<br/>
Examples: checking disk, make an HTTP call, calculating expensive values...<br/>
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.<br/>
See issue #7 for more information.

0 comments on commit 1ccbd90

Please sign in to comment.