Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
fmt
fmt-check
bench
bench-regression
coverage
tidy
tidy-check
Expand Down Expand Up @@ -324,3 +325,36 @@ jobs:
distribution: goreleaser
version: v2.7.0
args: check

benchstat-regression-guard:
name: Benchmark regression guard
runs-on: ubuntu-latest
# CAVEAT: ubuntu-latest is a shared GitHub-hosted runner. Shared
# runners exhibit ±5-15% variance between runs for nanosecond-scale
# benchmarks, which can fire the guard on pure jitter. If this job
# flakes repeatedly, options are (a) move to a dedicated runner,
# (b) raise the time/op threshold (keeping allocs/op strict since
# allocation counts are deterministic), or (c) make the job
# advisory rather than blocking. See CONTRIBUTING.md §Performance
# baseline for the policy.
#
# Skip on Dependabot-authored PRs: dependency bumps cannot
# legitimately move in-tree benchmarks, so variance there is noise.
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-go@v6.4.0
with:
go-version: "1.26"
cache: true
- name: Install benchstat
run: go install golang.org/x/perf/cmd/benchstat@v0.0.0-20260409210113-8e83ce0f7b1c
- name: Run make bench-regression
run: make bench-regression
- name: Attach benchstat report
if: always()
uses: actions/upload-artifact@v7.0.1
with:
name: benchstat-report
path: bench-regression.txt
if-no-files-found: ignore
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ profile.cov
# GoReleaser snapshot output
dist/

# bench-regression intermediates (bench.txt is the committed baseline)
bench-regression.txt
current.txt

# Dependency directories (remove the comment below to include it)
# vendor/

Expand Down
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ fmt-check: ## Fail if any Go file is unformatted
bench: ## Run benchmarks
$(GO) test -bench=. -benchmem -run=^$$ $(PKG)

.PHONY: bench-regression
bench-regression: ## Compare this tree against bench.txt and fail on regression
@if ! command -v benchstat >/dev/null 2>&1; then \
echo "benchstat not installed. Install with: go install golang.org/x/perf/cmd/benchstat@latest"; \
exit 2; \
fi
@echo "Running benchmarks at count=5 (fresh samples for benchstat)..."
@$(GO) test -bench=. -benchmem -run=^$$ -count=5 $(PKG) > current.txt
@echo "Comparing against committed baseline (bench.txt) via benchstat..."
@benchstat bench.txt current.txt | tee bench-regression.txt
@./scripts/check-bench-regression.sh bench-regression.txt

.PHONY: coverage
coverage: ## Generate coverage profile and HTML report for the library
$(GO) test -race -coverprofile=$(COVER_OUT) -covermode=atomic .
Expand Down
173 changes: 173 additions & 0 deletions bench.txt

Large diffs are not rendered by default.

219 changes: 219 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// Copyright 2026 AxonOps Limited.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package syncmap_test

import (
"fmt"
"sort"

"github.com/axonops/syncmap"
)

// Range, Keys, Values, and Map make no order guarantee. Every Example
// that iterates over the map sorts its output before printing so the
// // Output: blocks are deterministic under `go test`.

func ExampleSyncMap() {
var m syncmap.SyncMap[string, int]

m.Store("hits", 1)
m.Store("misses", 0)

v, ok := m.Load("hits")
fmt.Println(v, ok)
// Output: 1 true
}

func ExampleSyncMap_Load() {
var m syncmap.SyncMap[string, int]
m.Store("answer", 42)

v, ok := m.Load("answer")
fmt.Println(v, ok)

v, ok = m.Load("missing")
fmt.Println(v, ok)
// Output:
// 42 true
// 0 false
}

func ExampleSyncMap_Store() {
var m syncmap.SyncMap[string, string]
m.Store("env", "prod")
m.Store("env", "staging") // overwrites

v, _ := m.Load("env")
fmt.Println(v)
// Output: staging
}

func ExampleSyncMap_LoadOrStore() {
var m syncmap.SyncMap[string, int]

v1, loaded1 := m.LoadOrStore("k", 1)
v2, loaded2 := m.LoadOrStore("k", 2)

fmt.Println(v1, loaded1)
fmt.Println(v2, loaded2)
// Output:
// 1 false
// 1 true
}

func ExampleSyncMap_LoadAndDelete() {
var m syncmap.SyncMap[string, int]
m.Store("k", 7)

v, loaded := m.LoadAndDelete("k")
fmt.Println(v, loaded)

v, loaded = m.LoadAndDelete("k")
fmt.Println(v, loaded)
// Output:
// 7 true
// 0 false
}

func ExampleSyncMap_Delete() {
var m syncmap.SyncMap[string, int]
m.Store("k", 1)
m.Delete("k")

_, ok := m.Load("k")
fmt.Println(ok)
// Output: false
}

func ExampleSyncMap_Swap() {
var m syncmap.SyncMap[string, int]

previous, loaded := m.Swap("k", 1)
fmt.Println(previous, loaded)

previous, loaded = m.Swap("k", 2)
fmt.Println(previous, loaded)
// Output:
// 0 false
// 1 true
}

func ExampleSyncMap_Clear() {
var m syncmap.SyncMap[string, int]
m.Store("a", 1)
m.Store("b", 2)

m.Clear()
fmt.Println(m.Len())
// Output: 0
}

func ExampleSyncMap_Range() {
var m syncmap.SyncMap[string, int]
m.Store("a", 1)
m.Store("b", 2)
m.Store("c", 3)

var keys []string
m.Range(func(k string, v int) bool {
keys = append(keys, fmt.Sprintf("%s=%d", k, v))
return true
})
sort.Strings(keys)
for _, entry := range keys {
fmt.Println(entry)
}
// Output:
// a=1
// b=2
// c=3
}

func ExampleSyncMap_Len() {
var m syncmap.SyncMap[string, int]
m.Store("a", 1)
m.Store("b", 2)
fmt.Println(m.Len())
// Output: 2
}

func ExampleSyncMap_Map() {
var m syncmap.SyncMap[string, int]
m.Store("a", 1)
m.Store("b", 2)

snap := m.Map()
keys := make([]string, 0, len(snap))
for k := range snap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s=%d\n", k, snap[k])
}
// Output:
// a=1
// b=2
}

func ExampleSyncMap_Keys() {
var m syncmap.SyncMap[string, int]
m.Store("a", 1)
m.Store("b", 2)

keys := m.Keys()
sort.Strings(keys)
fmt.Println(keys)
// Output: [a b]
}

func ExampleSyncMap_Values() {
var m syncmap.SyncMap[string, int]
m.Store("a", 1)
m.Store("b", 2)

values := m.Values()
sort.Ints(values)
fmt.Println(values)
// Output: [1 2]
}

func ExampleCompareAndSwap() {
var m syncmap.SyncMap[string, int]
m.Store("k", 1)

swapped := syncmap.CompareAndSwap(&m, "k", 1, 2)
fmt.Println(swapped)

swapped = syncmap.CompareAndSwap(&m, "k", 1, 3)
fmt.Println(swapped)
// Output:
// true
// false
}

func ExampleCompareAndDelete() {
var m syncmap.SyncMap[string, int]
m.Store("k", 1)

deleted := syncmap.CompareAndDelete(&m, "k", 2)
fmt.Println(deleted)

deleted = syncmap.CompareAndDelete(&m, "k", 1)
fmt.Println(deleted)
// Output:
// false
// true
}
Loading