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
27 changes: 25 additions & 2 deletions common/maps/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,27 @@ import (
// Cache is a simple thread safe cache backed by a map.
type Cache[K comparable, T any] struct {
m map[K]T
opts CacheOptions
hasBeenInitialized bool
sync.RWMutex
}

// NewCache creates a new Cache.
// CacheOptions are the options for the Cache.
type CacheOptions struct {
// If set, the cache will not grow beyond this size.
Size uint64
}

var defaultCacheOptions = CacheOptions{}

// NewCache creates a new Cache with default options.
func NewCache[K comparable, T any]() *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T)}
return &Cache[K, T]{m: make(map[K]T), opts: defaultCacheOptions}
}

// NewCacheWithOptions creates a new Cache with the given options.
func NewCacheWithOptions[K comparable, T any](opts CacheOptions) *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T), opts: opts}
}

// Delete deletes the given key from the cache.
Expand Down Expand Up @@ -65,6 +79,7 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
if err != nil {
return v, err
}
c.clearIfNeeded()
c.m[key] = v
return v, nil
}
Expand Down Expand Up @@ -127,7 +142,15 @@ func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
}
}

func (c *Cache[K, T]) clearIfNeeded() {
if c.opts.Size > 0 && uint64(len(c.m)) >= c.opts.Size {
// clear the map
clear(c.m)
}
}

func (c *Cache[K, T]) set(key K, value T) {
c.clearIfNeeded()
c.m[key] = value
}

Expand Down
57 changes: 57 additions & 0 deletions common/maps/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// 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 maps

import (
"testing"

qt "github.com/frankban/quicktest"
)

func TestCacheSize(t *testing.T) {
c := qt.New(t)

cache := NewCacheWithOptions[string, string](CacheOptions{Size: 10})

for i := 0; i < 30; i++ {
cache.Set(string(rune('a'+i)), "value")
}

c.Assert(len(cache.m), qt.Equals, 10)

for i := 20; i < 50; i++ {
cache.GetOrCreate(string(rune('a'+i)), func() (string, error) {
return "value", nil
})
}

c.Assert(len(cache.m), qt.Equals, 10)

for i := 100; i < 200; i++ {
cache.SetIfAbsent(string(rune('a'+i)), "value")
}

c.Assert(len(cache.m), qt.Equals, 10)

cache.InitAndGet("foo", func(
get func(key string) (string, bool), set func(key string, value string),
) error {
for i := 50; i < 100; i++ {
set(string(rune('a'+i)), "value")
}
return nil
})

c.Assert(len(cache.m), qt.Equals, 10)
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ require (
github.com/yuin/goldmark-emoji v1.0.6
go.uber.org/automaxprocs v1.5.3
gocloud.dev v0.43.0
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
golang.org/x/image v0.30.0
golang.org/x/mod v0.27.0
golang.org/x/net v0.43.0
Expand Down Expand Up @@ -190,4 +190,4 @@ require (
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
)

go 1.24
go 1.24.0
9 changes: 3 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,10 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M=
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
Expand Down Expand Up @@ -583,8 +580,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
Expand Down
29 changes: 28 additions & 1 deletion tpl/collections/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"context"
"errors"
"fmt"
"math/rand"
"math/rand/v2"
"reflect"
"strings"
"time"
Expand All @@ -41,9 +41,12 @@ func New(deps *deps.Deps) *Namespace {
}
loc := langs.GetLocation(language)

dCache := maps.NewCacheWithOptions[dKey, []int](maps.CacheOptions{Size: 100})

return &Namespace{
loc: loc,
sortComp: compare.New(loc, true),
dCache: dCache,
deps: deps,
}
}
Expand All @@ -52,6 +55,7 @@ func New(deps *deps.Deps) *Namespace {
type Namespace struct {
loc *time.Location
sortComp *compare.Namespace
dCache *maps.Cache[dKey, []int]
deps *deps.Deps
}

Expand Down Expand Up @@ -520,6 +524,29 @@ func (ns *Namespace) Slice(args ...any) any {
return collections.Slice(args...)
}

type dKey struct {
seed uint64
n int
hi int
}

// D returns a slice of n unique random numbers in the range [0, hi) using the provded seed,
// using J. S. Vitter's Method D for sequential random sampling, from Vitter, J.S.
// - An Efficient Algorithm for Sequential Random Sampling - ACM Trans. Math. Software 11 (1985), 37-57.
// See https://getkerf.wordpress.com/2016/03/30/the-best-algorithm-no-one-knows-about/
func (ns *Namespace) D(seed, n, hi any) []int {
key := dKey{seed: cast.ToUint64(seed), n: cast.ToInt(n), hi: cast.ToInt(hi)}
v, _ := ns.dCache.GetOrCreate(key, func() ([]int, error) {
prng := rand.New(rand.NewPCG(key.seed, 0))
result := make([]int, 0, key.n)
_d(prng, key.n, key.hi, func(i int) {
result = append(result, i)
})
return result, nil
})
return v
}

type intersector struct {
r reflect.Value
seen map[any]bool
Expand Down
29 changes: 29 additions & 0 deletions tpl/collections/collections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,35 @@ func TestUniq(t *testing.T) {
}
}

func TestD(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := newNs()

c.Assert(ns.D(42, 5, 100), qt.DeepEquals, []int{24, 34, 66, 82, 96})
c.Assert(ns.D(31, 5, 100), qt.DeepEquals, []int{12, 37, 38, 69, 98})
}

func BenchmarkD2(b *testing.B) {
ns := newNs()

runBenchmark := func(seed, n, max int) {
name := fmt.Sprintf("n=%d,max=%d", n, max)
b.Run(name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
ns.D(seed, n, max)
}
})
}

runBenchmark(32, 5, 100)
runBenchmark(32, 50, 1000)
runBenchmark(32, 10, 10000)
runBenchmark(32, 500, 10000)
runBenchmark(32, 10, 500000)
runBenchmark(32, 5000, 500000)
}

func (x *TstX) TstRp() string {
return "r" + x.A
}
Expand Down
149 changes: 149 additions & 0 deletions tpl/collections/vitter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// This is just a temporary fork of https://github.com/josharian/vitter (ISC License, https://github.com/josharian/vitter/blob/main/LICENSE)
//
// This file will be removed once https://github.com/josharian/vitter/issues/1 is resolved.

package collections

import (
"math"
"math/rand/v2"
)

// https://getkerf.wordpress.com/2016/03/30/the-best-algorithm-no-one-knows-about/

// Copyright Kevin Lawler, released under ISC License

// _d generates an in-order uniform random sample of size 'want' from the range [0, max) using the provided PRNG.
//
// Parameters:
// - prng: random number generator
// - want: number of samples to select
// - max: upper bound of the range [0, max) from which to sample
// - choose: callback function invoked with each selected index in ascending order
//
// If the parameters are invalid (want < 0 or want > max), no samples are selected.
//
// Vitter, J.S. - An Efficient Algorithm for Sequential Random Sampling - ACM Trans. Math. Software 11 (1985), 37-57.
func _d(prng *rand.Rand, want, max int, choose func(n int)) {
if want <= 0 || want > max {
return
}
// POTENTIAL_OPTIMIZATION_POINT: Christian Neukirchen points out we can replace exp(log(x)*y) by pow(x,y)
// POTENTIAL_OPTIMIZATION_POINT: Vitter paper points out an exponentially distributed random var can provide speed ups
// 'a' is space allocated for the hand
// 'n' is the size of the hand
// 'N' is the upper bound on the random card values
j := -1
qu1 := -want + 1 + max
const negalphainv = -13 // threshold parameter from Vitter's paper for algorithm selection
threshold := -negalphainv * want

wantf := float64(want)
maxf := float64(max)
ninv := 1.0 / wantf
var nmin1inv float64
Vprime := math.Exp(math.Log(prng.Float64()) * ninv)

qu1real := -wantf + 1.0 + maxf
var U, X, y1, y2, top, bottom, negSreal float64

for want > 1 && threshold < max {
var S int

nmin1inv = 1.0 / (-1.0 + wantf)

for {
for {
X = maxf * (-Vprime + 1.0)
S = int(math.Floor(X))

if S < qu1 {
break
}

Vprime = math.Exp(math.Log(prng.Float64()) * ninv)
}

U = prng.Float64()
negSreal = float64(-S)
y1 = math.Exp(math.Log(U*maxf/qu1real) * nmin1inv)
Vprime = y1 * (-X/maxf + 1.0) * (qu1real / (negSreal + qu1real))

if Vprime <= 1.0 {
break
}

y2 = 1.0
top = -1.0 + maxf
var limit int

if -1+want > S {
bottom = -wantf + maxf
limit = -S + max
} else {
bottom = -1.0 + negSreal + maxf
limit = qu1
}

for t := max - 1; t >= limit; t-- {
y2 = (y2 * top) / bottom
top--
bottom--
}

if maxf/(-X+maxf) >= y1*math.Exp(math.Log(y2)*nmin1inv) {
Vprime = math.Exp(math.Log(prng.Float64()) * nmin1inv)
break
}

Vprime = math.Exp(math.Log(prng.Float64()) * ninv)
}

j += S + 1

choose(j)

max = -S + (-1 + max)
maxf = negSreal + (-1.0 + maxf)
want--
wantf--
ninv = nmin1inv

qu1 = -S + qu1
qu1real = negSreal + qu1real

threshold += negalphainv
}

if want > 1 {
methodA(prng, want, max, j, choose) // if i>0 then n has been decremented
} else {
S := int(math.Floor(float64(max) * Vprime))

j += S + 1

choose(j)
}
}

// methodA is the simpler fallback algorithm used when Algorithm D's optimizations are not beneficial.
func methodA(prng *rand.Rand, want, max int, j int, choose func(n int)) {
for want >= 2 {
j++
V := prng.Float64()
quot := float64(max-want) / float64(max)
for quot > V {
j++
max--
quot *= float64(max - want)
quot /= float64(max)
}
choose(j)
max--
want--
}

S := int(math.Floor(float64(max) * prng.Float64()))
j += S + 1
choose(j)
}
Loading
Loading