diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1d564fd6..73b56b78 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -160,6 +160,7 @@ jobs: run: | go test -fuzz=FuzzFind -fuzztime=30s ./pkg/acor go test -fuzz=FuzzAdd -fuzztime=30s ./pkg/acor + go test -fuzz=FuzzScanLeftmostParity -fuzztime=30s ./pkg/acor - name: Run build run: make build - name: Verify documentation examples compile diff --git a/Makefile b/Makefile index f1ba2c2e..2498128f 100644 --- a/Makefile +++ b/Makefile @@ -88,6 +88,7 @@ lint-fix: fuzz: @go test -fuzz=FuzzFind -fuzztime=30s ./pkg/acor @go test -fuzz=FuzzAdd -fuzztime=30s ./pkg/acor + @go test -fuzz=FuzzScanLeftmostParity -fuzztime=30s ./pkg/acor # Timing evidence for docs/content/reference/benchmarks.md. Set # ACOR_INTEGRATION_ADDR to also run the real-server benchmarks; without it only @@ -108,3 +109,8 @@ bench-module: race: @go test -race ./... @cd server && go test -race ./... + +# Full V3 matrix on a disposable endpoint; results include environment and seed. +.PHONY: bench-v3 +bench-v3: + @sh scripts/benchmark-v3.sh diff --git a/README.md b/README.md index 4225dc33..ace8859d 100644 --- a/README.md +++ b/README.md @@ -103,3 +103,13 @@ promises. Package signatures are on ## License [Apache License 2.0](LICENSE) — Copyright 2016-2026 Sungkyu Yoo + +### Large versioned dictionaries + +Use `OpenVersioned` for V3 snapshots, atomic expected-version updates and background +engine replacement. V1/V2 APIs remain compatible. See the [V3 guide](docs/content/reference/versioned.md) +and [performance report](docs/content/reference/versioned-performance.md). + +R2 reuses unchanged downloaded buckets and reduces sparse-engine build memory. +R3 adds bounded `Scan`, `MaskText`, and `ReplaceText` with original byte/rune spans; +see [text processing](docs/content/reference/text-processing.md). diff --git a/api/v1-audit.txt b/api/v1-audit.txt index f7e20a11..eee82915 100644 --- a/api/v1-audit.txt +++ b/api/v1-audit.txt @@ -185,3 +185,121 @@ var ErrRedisRingAddrs ok client.go:69 when ring mode has no shard address var ErrRedisSentinelAddrs ok client.go:60 when sentinel mode has no addresses var ErrSuggestRequiresRedis ok redis_backed_ops.go:160,164 — both suggest and suggestIndex in preset mode, as documented var ErrV1ReadOnly ok v1_ops.go:52,59 refuse unconditionally in production; the writable path is a test-only fixture (test_helpers_test.go:117), so the doc holds for real callers + +field DictionaryDiff.Added []string ok versioned_snapshot.go:268 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field DictionaryDiff.Removed []string ok versioned_snapshot.go:268 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field DictionaryDiff.Retained []string ok versioned_snapshot.go:268 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field DictionaryDiff.Version Version ok versioned_snapshot.go:268 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field DictionaryPage.Keywords []string ok versioned_snapshot.go:162 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field DictionaryPage.NextCursor string ok versioned_snapshot.go:162 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field DictionaryPage.Version Version ok versioned_snapshot.go:162 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field PruneResult.Chunks int ok versioned_prune.go:15 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field PruneResult.Generations int ok versioned_prune.go:15 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field V2CopyOptions.RejectEmpty bool ok versioned_write.go:273 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field V2CopyResult.Checksum string ok versioned_write.go:265 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field V2CopyResult.Count int ok versioned_write.go:265 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field V2CopyResult.SourceVersion string ok versioned_write.go:265 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field V2CopyResult.Write *WriteResult ok versioned_write.go:265 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedOptions.CaseSensitive bool ok versioned.go:58 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedOptions.LeaseDuration time.Duration ok versioned.go:58 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedOptions.LeaseRefresh time.Duration ok versioned.go:58 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedOptions.PollInterval time.Duration ok versioned.go:58 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedOptions.Preset Preset ok versioned.go:58 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedOptions.Redis AhoCorasickArgs ok versioned.go:58 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedStatus.ActiveVersion Version ok versioned.go:73 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedStatus.BuildDuration time.Duration ok versioned.go:73 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedStatus.BuildStarted time.Time ok versioned.go:73 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedStatus.Building bool ok versioned.go:73 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedStatus.LastError string ok versioned.go:73 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field VersionedStatus.ServingVersion Version ok versioned.go:73 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field WriteResult.Added int ok versioned.go:89 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field WriteResult.OperationID string ok versioned.go:89 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field WriteResult.PreviousVersion Version ok versioned.go:89 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field WriteResult.Removed int ok versioned.go:89 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field WriteResult.Version Version ok versioned.go:89 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +func OpenVersioned(ctx context.Context, opts *VersionedOptions) (*VersionedCollection, error) ok versioned.go:158 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*Snapshot) Close(ctx context.Context) error ok versioned_snapshot.go:158 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*Snapshot) Count() int ok versioned_snapshot.go:155 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*Snapshot) Diff(ctx context.Context, target []string) (*DictionaryDiff, error) ok versioned_snapshot.go:276 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*Snapshot) List(ctx context.Context, cursor string, limit int) (*DictionaryPage, error) ok versioned_snapshot.go:176 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*Snapshot) Version() Version ok versioned_snapshot.go:152 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) Add(ctx context.Context, expected Version, word string) (*WriteResult, error) ok versioned_write.go:54 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) AddMany(ctx context.Context, expected Version, words []string) (*WriteResult, error) ok versioned_write.go:65 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) Close() error ok versioned.go:370 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) Contains(ctx context.Context, text string) (bool, error) ok versioned_search.go:83 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) CopyV2(ctx context.Context, source string, expected Version, opts *V2CopyOptions) (*V2CopyResult, error) ok versioned_write.go:281 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) Find(ctx context.Context, text string) ([]string, error) ok versioned_search.go:47 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) FindBatch(ctx context.Context, texts []string) ([][]string, error) ok versioned_search.go:119 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) FindIndex(ctx context.Context, text string) (map[string][]int, error) ok versioned_search.go:56 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) FindIndexParallel(ctx context.Context, text string, opts *ParallelOptions) (map[string][]int, error) ok versioned_search.go:101 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) FindMatches(ctx context.Context, text string, opts *MatchOptions) ([]Match, error) ok versioned_search.go:74 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) FindParallel(ctx context.Context, text string, opts *ParallelOptions) ([]string, error) ok versioned_search.go:92 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) FindSet(ctx context.Context, text string) ([]string, error) ok versioned_search.go:65 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) FindStream(ctx context.Context, r io.Reader, onMatch func(Match) bool) error ok versioned_search.go:110 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) Prune(ctx context.Context) (*PruneResult, error) ok versioned_prune.go:32 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) Remove(ctx context.Context, expected Version, word string) (*WriteResult, error) ok versioned_write.go:59 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) RemoveMany(ctx context.Context, expected Version, words []string) (*WriteResult, error) ok versioned_write.go:70 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) Replace(ctx context.Context, expected Version, words []string) (*WriteResult, error) ok versioned_write.go:49 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) ResolveOperation(ctx context.Context, id string) (*WriteResult, error) ok versioned_write.go:245 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) Snapshot(ctx context.Context) (*Snapshot, error) ok versioned_snapshot.go:138 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) Status() VersionedStatus ok versioned.go:219 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +method (*VersionedCollection) WaitForVersion(ctx context.Context, version Version) error ok versioned.go:341 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type DictionaryDiff struct ok versioned_snapshot.go:268 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type DictionaryPage struct ok versioned_snapshot.go:162 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type PruneResult struct ok versioned_prune.go:15 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type Snapshot struct ok versioned_snapshot.go:131 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type V2CopyOptions struct ok versioned_write.go:273 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type V2CopyResult struct ok versioned_write.go:265 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type Version string ok versioned.go:24 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type VersionedCollection struct ok versioned.go:119 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type VersionedOptions struct ok versioned.go:58 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type VersionedStatus struct ok versioned.go:73 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +type WriteResult struct ok versioned.go:89 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +var ErrCasePolicy ok versioned.go:47 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +var ErrCommitUnknown ok versioned.go:51 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +var ErrInvalidVersion ok versioned.go:49 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +var ErrLeaseExpired ok versioned.go:43 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +var ErrMaintenance ok versioned.go:45 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +var ErrVersionedClosed ok versioned.go:41 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +var ErrVersionedCorrupt ok versioned.go:53 V3 contract reviewed against the implementation; lifecycle, parity, leases and fault scenarios are in versioned_test.go:1 and versioned_failure_test.go:1 +field RewriteOptions.MaxCandidates int ok rewrite.go:17 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field RewriteOptions.MaxInputBytes int ok rewrite.go:17 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field RewriteOptions.MaxMatches int ok rewrite.go:17 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field RewriteOptions.MaxOutputBytes int ok rewrite.go:17 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field RewriteOptions.WholeWord bool ok rewrite.go:17 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field RewriteOptions.WordRune func(rune) bool ok rewrite.go:17 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field RewriteResult.Matches []SourceMatch ok rewrite.go:28 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field RewriteResult.Text string ok rewrite.go:28 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field ScanOptions.Kind MatchKind ok scan.go:48 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field ScanOptions.MaxCandidates int ok scan.go:48 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field ScanOptions.MaxInputBytes int ok scan.go:48 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field ScanOptions.MaxMatches int ok scan.go:48 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field ScanOptions.WholeWord bool ok scan.go:48 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field ScanOptions.WordRune func(rune) bool ok scan.go:48 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field ScanResult.Matches []SourceMatch ok scan.go:63 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field ScanResult.Truncated bool ok scan.go:63 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field SourceMatch.ByteEnd int ok scan.go:36 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field SourceMatch.ByteStart int ok scan.go:36 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field SourceMatch.End int ok scan.go:36 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field SourceMatch.Keyword string ok scan.go:36 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field SourceMatch.Start int ok scan.go:36 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field SourceMatch.Text string ok scan.go:36 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field VersionedOptions.RefreshDebounce time.Duration ok versioned.go:58 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field VersionedStatus.CompletedBuilds uint64 ok versioned.go:73 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field VersionedStatus.DownloadedBuckets int ok versioned.go:73 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +field VersionedStatus.ReusedBuckets int ok versioned.go:73 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +method (*AhoCorasick) MaskText(ctx context.Context, text string, mask rune, opts *RewriteOptions) (*RewriteResult, error) ok rewrite.go:41 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +method (*AhoCorasick) ReplaceText(ctx context.Context, text, replacement string, opts *RewriteOptions) (*RewriteResult, error) ok rewrite.go:35 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +method (*AhoCorasick) Scan(ctx context.Context, text string, opts *ScanOptions) (*ScanResult, error) ok scan.go:70 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +method (*VersionedCollection) MaskText(ctx context.Context, text string, mask rune, opts *RewriteOptions) (*RewriteResult, error) ok rewrite.go:58 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +method (*VersionedCollection) ReplaceText(ctx context.Context, text, replacement string, opts *RewriteOptions) (*RewriteResult, error) ok rewrite.go:49 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +method (*VersionedCollection) Scan(ctx context.Context, text string, opts *ScanOptions) (*ScanResult, error) ok scan.go:83 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +type RewriteOptions struct ok rewrite.go:17 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +type RewriteResult struct ok rewrite.go:28 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +type ScanOptions struct ok scan.go:48 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +type ScanResult struct ok scan.go:63 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +type SourceMatch struct ok scan.go:36 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +var ErrInputLimit ok scan.go:23 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +var ErrMatchLimit ok scan.go:27 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +var ErrOutputLimit ok scan.go:29 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 +var ErrScanWorkLimit ok scan.go:25 additive R2/R3 contract; source-span and limit parity verified in scan_test.go:1, cache and coalescing in versioned_reuse_test.go:1 diff --git a/api/v1.txt b/api/v1.txt index c7923865..7ef9a34d 100644 --- a/api/v1.txt +++ b/api/v1.txt @@ -55,6 +55,13 @@ field CacheStats.PresetPollFailures uint64 field CacheStats.PresetReloadFailures uint64 field CacheStats.RebuildDuration time.Duration field CacheStats.Rebuilds uint64 +field DictionaryDiff.Added []string +field DictionaryDiff.Removed []string +field DictionaryDiff.Retained []string +field DictionaryDiff.Version Version +field DictionaryPage.Keywords []string +field DictionaryPage.NextCursor string +field DictionaryPage.Version Version field KeywordError.Error error field KeywordError.Keyword string field Match.End int @@ -89,13 +96,64 @@ field ParallelOptions.Boundary ChunkBoundary field ParallelOptions.ChunkSize int field ParallelOptions.Overlap int field ParallelOptions.Workers int +field PruneResult.Chunks int +field PruneResult.Generations int field RedisError.Err error field RedisError.Key string field RedisError.Op string +field RewriteOptions.MaxCandidates int +field RewriteOptions.MaxInputBytes int +field RewriteOptions.MaxMatches int +field RewriteOptions.MaxOutputBytes int +field RewriteOptions.WholeWord bool +field RewriteOptions.WordRune func(rune) bool +field RewriteResult.Matches []SourceMatch +field RewriteResult.Text string +field ScanOptions.Kind MatchKind +field ScanOptions.MaxCandidates int +field ScanOptions.MaxInputBytes int +field ScanOptions.MaxMatches int +field ScanOptions.WholeWord bool +field ScanOptions.WordRune func(rune) bool +field ScanResult.Matches []SourceMatch +field ScanResult.Truncated bool +field SourceMatch.ByteEnd int +field SourceMatch.ByteStart int +field SourceMatch.End int +field SourceMatch.Keyword string +field SourceMatch.Start int +field SourceMatch.Text string +field V2CopyOptions.RejectEmpty bool +field V2CopyResult.Checksum string +field V2CopyResult.Count int +field V2CopyResult.SourceVersion string +field V2CopyResult.Write *WriteResult +field VersionedOptions.CaseSensitive bool +field VersionedOptions.LeaseDuration time.Duration +field VersionedOptions.LeaseRefresh time.Duration +field VersionedOptions.PollInterval time.Duration +field VersionedOptions.Preset Preset +field VersionedOptions.Redis AhoCorasickArgs +field VersionedOptions.RefreshDebounce time.Duration +field VersionedStatus.ActiveVersion Version +field VersionedStatus.BuildDuration time.Duration +field VersionedStatus.BuildStarted time.Time +field VersionedStatus.Building bool +field VersionedStatus.CompletedBuilds uint64 +field VersionedStatus.DownloadedBuckets int +field VersionedStatus.LastError string +field VersionedStatus.ReusedBuckets int +field VersionedStatus.ServingVersion Version +field WriteResult.Added int +field WriteResult.OperationID string +field WriteResult.PreviousVersion Version +field WriteResult.Removed int +field WriteResult.Version Version func Create(args *AhoCorasickArgs) (*AhoCorasick, error) func CreateContext(ctx context.Context, args *AhoCorasickArgs) (*AhoCorasick, error) func DefaultMigrationOptions() *MigrationOptions func DefaultParallelOptions() *ParallelOptions +func OpenVersioned(ctx context.Context, opts *VersionedOptions) (*VersionedCollection, error) method (*AhoCorasick) Add(keyword string) (int, error) method (*AhoCorasick) AddContext(ctx context.Context, keyword string) (int, error) method (*AhoCorasick) AddMany(keywords []string, opts *BatchOptions) (*BatchResult, error) @@ -126,12 +184,15 @@ method (*AhoCorasick) Flush() error method (*AhoCorasick) FlushContext(ctx context.Context) error method (*AhoCorasick) Info() (*AhoCorasickInfo, error) method (*AhoCorasick) InfoContext(ctx context.Context) (*AhoCorasickInfo, error) +method (*AhoCorasick) MaskText(ctx context.Context, text string, mask rune, opts *RewriteOptions) (*RewriteResult, error) method (*AhoCorasick) MigrateV1ToV2(opts *MigrationOptions) (*MigrationResult, error) method (*AhoCorasick) Remove(keyword string) (int, error) method (*AhoCorasick) RemoveContext(ctx context.Context, keyword string) (int, error) method (*AhoCorasick) RemoveMany(keywords []string, opts *BatchOptions) (*BatchResult, error) method (*AhoCorasick) RemoveManyContext(ctx context.Context, keywords []string, opts *BatchOptions) (*BatchResult, error) +method (*AhoCorasick) ReplaceText(ctx context.Context, text, replacement string, opts *RewriteOptions) (*RewriteResult, error) method (*AhoCorasick) RollbackToV1() error +method (*AhoCorasick) Scan(ctx context.Context, text string, opts *ScanOptions) (*ScanResult, error) method (*AhoCorasick) SchemaVersion() int method (*AhoCorasick) Suggest(input string) ([]string, error) method (*AhoCorasick) SuggestContext(ctx context.Context, input string) ([]string, error) @@ -142,6 +203,35 @@ method (*OperationError) Error() string method (*OperationError) Unwrap() error method (*RedisError) Error() string method (*RedisError) Unwrap() error +method (*Snapshot) Close(ctx context.Context) error +method (*Snapshot) Count() int +method (*Snapshot) Diff(ctx context.Context, target []string) (*DictionaryDiff, error) +method (*Snapshot) List(ctx context.Context, cursor string, limit int) (*DictionaryPage, error) +method (*Snapshot) Version() Version +method (*VersionedCollection) Add(ctx context.Context, expected Version, word string) (*WriteResult, error) +method (*VersionedCollection) AddMany(ctx context.Context, expected Version, words []string) (*WriteResult, error) +method (*VersionedCollection) Close() error +method (*VersionedCollection) Contains(ctx context.Context, text string) (bool, error) +method (*VersionedCollection) CopyV2(ctx context.Context, source string, expected Version, opts *V2CopyOptions) (*V2CopyResult, error) +method (*VersionedCollection) Find(ctx context.Context, text string) ([]string, error) +method (*VersionedCollection) FindBatch(ctx context.Context, texts []string) ([][]string, error) +method (*VersionedCollection) FindIndex(ctx context.Context, text string) (map[string][]int, error) +method (*VersionedCollection) FindIndexParallel(ctx context.Context, text string, opts *ParallelOptions) (map[string][]int, error) +method (*VersionedCollection) FindMatches(ctx context.Context, text string, opts *MatchOptions) ([]Match, error) +method (*VersionedCollection) FindParallel(ctx context.Context, text string, opts *ParallelOptions) ([]string, error) +method (*VersionedCollection) FindSet(ctx context.Context, text string) ([]string, error) +method (*VersionedCollection) FindStream(ctx context.Context, r io.Reader, onMatch func(Match) bool) error +method (*VersionedCollection) MaskText(ctx context.Context, text string, mask rune, opts *RewriteOptions) (*RewriteResult, error) +method (*VersionedCollection) Prune(ctx context.Context) (*PruneResult, error) +method (*VersionedCollection) Remove(ctx context.Context, expected Version, word string) (*WriteResult, error) +method (*VersionedCollection) RemoveMany(ctx context.Context, expected Version, words []string) (*WriteResult, error) +method (*VersionedCollection) Replace(ctx context.Context, expected Version, words []string) (*WriteResult, error) +method (*VersionedCollection) ReplaceText(ctx context.Context, text, replacement string, opts *RewriteOptions) (*RewriteResult, error) +method (*VersionedCollection) ResolveOperation(ctx context.Context, id string) (*WriteResult, error) +method (*VersionedCollection) Scan(ctx context.Context, text string, opts *ScanOptions) (*ScanResult, error) +method (*VersionedCollection) Snapshot(ctx context.Context) (*Snapshot, error) +method (*VersionedCollection) Status() VersionedStatus +method (*VersionedCollection) WaitForVersion(ctx context.Context, version Version) error method (Preset) String() string method Logger.Printf(format string, v ...interface{}) method Logger.Println(v ...interface{}) @@ -153,6 +243,8 @@ type BatchOptions struct type BatchResult struct type CacheStats struct type ChunkBoundary int +type DictionaryDiff struct +type DictionaryPage struct type KeywordError struct type Logger interface type Match struct @@ -163,18 +255,40 @@ type MigrationResult struct type OperationError struct type ParallelOptions struct type Preset int +type PruneResult struct type RedisError struct +type RewriteOptions struct +type RewriteResult struct +type ScanOptions struct +type ScanResult struct +type Snapshot struct +type SourceMatch struct +type V2CopyOptions struct +type V2CopyResult struct +type Version string +type VersionedCollection struct +type VersionedOptions struct +type VersionedStatus struct +type WriteResult struct var ErrAlreadyV2 var ErrCacheRequiresV2 var ErrCacheWithPreset +var ErrCasePolicy +var ErrCommitUnknown var ErrConcurrencyConflict var ErrEmptyKeyword +var ErrInputLimit var ErrInvalidChunkSize var ErrInvalidName +var ErrInvalidVersion +var ErrLeaseExpired +var ErrMaintenance +var ErrMatchLimit var ErrMigrationInProg var ErrMigrationRequiresRedis var ErrNilArgs var ErrNoDataToMigrate +var ErrOutputLimit var ErrPresetRequiresRedis var ErrPresetRequiresV2 var ErrRedisAddrs @@ -183,5 +297,8 @@ var ErrRedisClusterDB var ErrRedisConflictingTopology var ErrRedisRingAddrs var ErrRedisSentinelAddrs +var ErrScanWorkLimit var ErrSuggestRequiresRedis var ErrV1ReadOnly +var ErrVersionedClosed +var ErrVersionedCorrupt diff --git a/benchmarks/results/r2-r3-20260906.json b/benchmarks/results/r2-r3-20260906.json new file mode 100644 index 00000000..292675d0 --- /dev/null +++ b/benchmarks/results/r2-r3-20260906.json @@ -0,0 +1,5526 @@ +{ + "date": "2026-09-06", + "baseline": "v3-20260906.json", + "host": "Apple M4, 10 logical CPUs, 16 GiB RAM, macOS 26.5.2 (Darwin 25.5.0), arm64", + "go": "1.26.7", + "redis": "8.10.1", + "valkey": "9.1.2, source-built, prefetch-batch-max-size=0", + "configuration": "Standalone localhost TCP, persistence disabled, MemoryEfficient preset, 50ms polling, default 5 minute leases; two runs per size/distribution, separate Go process per run. R2 default RefreshDebounce=20ms; same R1 TestVersionedScale workload.", + "caveats": [ + "Developer workstation, not an isolated performance lab; no latency or memory guarantee.", + "Server-wide memory and network counters include protocol, polling, engine downloads, script cache and retained generations.", + "Prune benchmark advances generation registry timestamps past the retention horizon; it does not wait 24 hours.", + "Default-prefetch local Valkey 9.1.2 exited with SIGSEGV during exploratory measurement; successful Valkey results disable prefetch." + ], + "source_sha256": { + "internal/engine/build_context.go": "0a9a33eb82725e915d79d3ed5a3613c1d6a983ab1e516b9e9dee36f6fc5bbfd5", + "internal/engine/build_context_test.go": "0c407a301a88e909ccbde794327342d93588ffedd5b17d900e242b2581f79489", + "internal/engine/coder.go": "282aa94da1d6b6f72aa22087da1caae3b871e2249bfd395174599314e8896711", + "internal/engine/coder_test.go": "436a9431876003378dc4e9ab0a994f943a742d00441271230cb35a8dfad733f1", + "internal/engine/engine.go": "627093707446c6338d3381115830793bd89056fb518824955eebfa8b9417691a", + "internal/engine/engine_banded_dfa.go": "43bac4bae59dafac223d0604a1b893e72c82663b01169aacb97ec01571bd58c2", + "internal/engine/engine_bench_test.go": "efc9c75e0885ecc4746f8abca239ddd626497a5e689c242a0682337480043257", + "internal/engine/engine_bloom.go": "e9208d182ada943d262bbff285fa33aa3c0a15f9beb4f2adf5c0526a8fdf40ba", + "internal/engine/engine_dat.go": "a3dd3d3d225757e652d8e44547483b6e61ca2901563c42be77a520c08d407f56", + "internal/engine/engine_flat.go": "9e91caa902b080a63d7038a7f13c6ec45c3278dabc35bb7282538f7a68d961e4", + "internal/engine/engine_handle.go": "7ccbb55c199bc9a0dd58567e41dfb01c38491e495abb19148ac427ca7163c76e", + "internal/engine/engine_map.go": "540f131ac903e47741105fa51a74af0af13fdeaaeb8a0971da6296975876be4c", + "internal/engine/engine_matches_test.go": "49cdfa8a81827c7527aaf541066345f5b36876f378d005c4571a27a94b132e99", + "internal/engine/engine_output.go": "e9f6aa84b8f8f8b935af0826eb911f082a01b76118332cc35fca3bbf77826889", + "internal/engine/engine_output_test.go": "935fe9559f4cfcc837624b39de477bfb7fcbd1b4f7e15d1baa635f3784b4e9c7", + "internal/engine/engine_presets.go": "a068f943e2e72f8eebf85b320d1b9ebf916b05914bbc2c146542b8eddd5c8019", + "internal/engine/engine_test.go": "9fd9e0200c0f92f73a8fd63aeb301c853f21fa246621faffe1a0808a4f96da5b", + "pkg/acor/rewrite.go": "000966244bdfb62565d5ee265d2187daad109e0c81b0b93716eed71e0b978bf0", + "pkg/acor/scan.go": "cd9f3a663937403412429316e85c25a35e9a7e843dff8099626c215e6aff631a", + "pkg/acor/scan_test.go": "7a4b04a6eb5e4f7088a6104034ae59faf2db9c0cf55aeb8ef609f888cb007a19", + "pkg/acor/versioned.go": "be342cb376cffe70c47f76dab8b5e44dcca755d39e6004e65988b0f28aeeb84b", + "pkg/acor/versioned_failure_test.go": "de3e220c7ed5b20c27aaec77a6a2855eb002407f5cae687eecb6178dff63069b", + "pkg/acor/versioned_million_test.go": "83f03bfa7029d7966917a358477b3fdedae24a1b0355d49794d47cbf92107e5f", + "pkg/acor/versioned_prune.go": "80afd3a69d5b416a554aa77bcd6e8534ee56aadcbec2b4254da6a28fae2a0b6b", + "pkg/acor/versioned_reuse.go": "ad18d950053af49c12966364ae4ee1cc86b1259fd030f34072832d6bd509e5a1", + "pkg/acor/versioned_reuse_test.go": "e37fb221c3ae604bf48cb3bf31f6bf22a35b5414205982a507abee5c864aea23", + "pkg/acor/versioned_scale_test.go": "ddbc0e213c3b7a47d3d5ed88542de19cbadcb186d62b0f11ba93a538cbd44895", + "pkg/acor/versioned_search.go": "9d1ba589f381d274e1f5ac461b47b574ac00929de4343f39c9fd47668dfe0b10", + "pkg/acor/versioned_snapshot.go": "bad1796954fb058b77bccdc0996a3c04c359d36cc728efbcc22b4422031ad47f", + "pkg/acor/versioned_test.go": "cf84d3f7ebd06735397bbd598f761c3cb9c5e37f40d5bc74118e68a6882ad126", + "pkg/acor/versioned_write.go": "1f8afde415e110d48dd50ace4849d5c93acdc6c78491d8e9d562d53e32f76040" + }, + "million_safety": { + "redis": "=== RUN TestVersionedMillionSafety\n--- PASS: TestVersionedMillionSafety (3.20s)\nPASS\n", + "valkey": "=== RUN TestVersionedMillionSafety\n--- PASS: TestVersionedMillionSafety (2.98s)\nPASS\n" + }, + "runs": [ + { + "backend": "redis", + "n": 10000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 266.957, + "kind": "diverse", + "max_rss_bytes": 35094528, + "n": 10000, + "operation": "initial_load", + "ready_ms": 507.192, + "received_bytes": 1014839, + "redis_bytes": 3787072, + "search_p50_ns": 334, + "search_p95_ns": 791, + "search_p99_ns": 3709, + "search_samples": 505, + "sent_bytes": 5031514 + }, + { + "commit_ms": 204.42, + "kind": "diverse", + "max_rss_bytes": 36175872, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 204.475, + "received_bytes": 835310, + "redis_bytes": 3788560, + "search_p50_ns": 1167, + "search_p95_ns": 2083, + "search_p99_ns": 2750, + "search_samples": 204, + "sent_bytes": 622208 + }, + { + "commit_ms": 5.516, + "kind": "diverse", + "max_rss_bytes": 42483712, + "n": 10000, + "operation": "add_1", + "ready_ms": 35.636, + "received_bytes": 1286620, + "redis_bytes": 5080240, + "search_p50_ns": 2166, + "search_p95_ns": 7291, + "search_p99_ns": 8500, + "search_samples": 32, + "sent_bytes": 648180 + }, + { + "commit_ms": 5.796, + "kind": "diverse", + "max_rss_bytes": 47562752, + "n": 10000, + "operation": "remove_1", + "ready_ms": 25.977, + "received_bytes": 1286620, + "redis_bytes": 5736544, + "search_p50_ns": 4042, + "search_p95_ns": 9000, + "search_p99_ns": 9000, + "search_samples": 21, + "sent_bytes": 648159 + }, + { + "commit_ms": 119.534, + "kind": "diverse", + "max_rss_bytes": 47710208, + "n": 10000, + "operation": "add_1000", + "ready_ms": 189.666, + "received_bytes": 1404355, + "redis_bytes": 6837664, + "search_p50_ns": 1416, + "search_p95_ns": 1917, + "search_p99_ns": 5417, + "search_samples": 189, + "sent_bytes": 1859056 + }, + { + "commit_ms": 125.054, + "kind": "diverse", + "max_rss_bytes": 55656448, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 195.142, + "received_bytes": 1404164, + "redis_bytes": 7492128, + "search_p50_ns": 1417, + "search_p95_ns": 3125, + "search_p99_ns": 10041, + "search_samples": 191, + "sent_bytes": 1763731 + }, + { + "commit_ms": 18.657, + "kind": "diverse", + "max_rss_bytes": 56049664, + "n": 10000, + "operation": "add_100", + "ready_ms": 38.816, + "received_bytes": 1299598, + "redis_bytes": 8187584, + "search_p50_ns": 1500, + "search_p95_ns": 6958, + "search_p99_ns": 7584, + "search_samples": 34, + "sent_bytes": 782693 + }, + { + "commit_ms": 17.618, + "kind": "diverse", + "max_rss_bytes": 56082432, + "n": 10000, + "operation": "remove_100", + "ready_ms": 57.772, + "received_bytes": 1299639, + "redis_bytes": 8843552, + "search_p50_ns": 1667, + "search_p95_ns": 7625, + "search_p99_ns": 14917, + "search_samples": 52, + "sent_bytes": 771811 + }, + { + "commit_ms": 515.853, + "kind": "diverse", + "max_rss_bytes": 61030400, + "n": 10000, + "operation": "full_replace", + "ready_ms": 786.066, + "received_bytes": 1706076, + "redis_bytes": 10455280, + "search_p50_ns": 1416, + "search_p95_ns": 2417, + "search_p99_ns": 6834, + "search_samples": 781, + "sent_bytes": 5708382 + } + ], + "startup_ms": 236.304, + "prune": { + "ms": 17.543, + "generations": 8, + "chunks": 4716, + "redis_bytes": 3864000 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658841157222 uptime_in_seconds:63 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10273945 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 10000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 340.302, + "kind": "diverse", + "max_rss_bytes": 35946496, + "n": 10000, + "operation": "initial_load", + "ready_ms": 680.434, + "received_bytes": 1015010, + "redis_bytes": 3810864, + "search_p50_ns": 500, + "search_p95_ns": 1667, + "search_p99_ns": 5542, + "search_samples": 676, + "sent_bytes": 5031716 + }, + { + "commit_ms": 268.732, + "kind": "diverse", + "max_rss_bytes": 36700160, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 268.8, + "received_bytes": 835395, + "redis_bytes": 3812352, + "search_p50_ns": 1584, + "search_p95_ns": 2083, + "search_p99_ns": 2750, + "search_samples": 263, + "sent_bytes": 622309 + }, + { + "commit_ms": 6.89, + "kind": "diverse", + "max_rss_bytes": 42942464, + "n": 10000, + "operation": "add_1", + "ready_ms": 37.104, + "received_bytes": 1286633, + "redis_bytes": 5104000, + "search_p50_ns": 2084, + "search_p95_ns": 8917, + "search_p99_ns": 14833, + "search_samples": 33, + "sent_bytes": 648180 + }, + { + "commit_ms": 6.255, + "kind": "diverse", + "max_rss_bytes": 45088768, + "n": 10000, + "operation": "remove_1", + "ready_ms": 26.371, + "received_bytes": 1286638, + "redis_bytes": 5762384, + "search_p50_ns": 2292, + "search_p95_ns": 13292, + "search_p99_ns": 13292, + "search_samples": 20, + "sent_bytes": 648159 + }, + { + "commit_ms": 129.131, + "kind": "diverse", + "max_rss_bytes": 48283648, + "n": 10000, + "operation": "add_1000", + "ready_ms": 219.234, + "received_bytes": 1404446, + "redis_bytes": 6861408, + "search_p50_ns": 1541, + "search_p95_ns": 3417, + "search_p99_ns": 6541, + "search_samples": 213, + "sent_bytes": 1859157 + }, + { + "commit_ms": 128.101, + "kind": "diverse", + "max_rss_bytes": 48316416, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 208.205, + "received_bytes": 1404183, + "redis_bytes": 7513824, + "search_p50_ns": 1583, + "search_p95_ns": 5333, + "search_p99_ns": 7625, + "search_samples": 204, + "sent_bytes": 1763731 + }, + { + "commit_ms": 19.288, + "kind": "diverse", + "max_rss_bytes": 52527104, + "n": 10000, + "operation": "add_100", + "ready_ms": 49.415, + "received_bytes": 1299689, + "redis_bytes": 8209456, + "search_p50_ns": 1750, + "search_p95_ns": 7166, + "search_p99_ns": 15958, + "search_samples": 44, + "sent_bytes": 782794 + }, + { + "commit_ms": 18.778, + "kind": "diverse", + "max_rss_bytes": 52559872, + "n": 10000, + "operation": "remove_100", + "ready_ms": 48.954, + "received_bytes": 1299585, + "redis_bytes": 8865408, + "search_p50_ns": 1750, + "search_p95_ns": 6250, + "search_p99_ns": 9916, + "search_samples": 43, + "sent_bytes": 771710 + }, + { + "commit_ms": 517.525, + "kind": "diverse", + "max_rss_bytes": 52822016, + "n": 10000, + "operation": "full_replace", + "ready_ms": 787.681, + "received_bytes": 1706022, + "redis_bytes": 10477584, + "search_p50_ns": 1417, + "search_p95_ns": 1958, + "search_p99_ns": 6583, + "search_samples": 783, + "sent_bytes": 5708281 + } + ], + "startup_ms": 316.903, + "prune": { + "ms": 18.635, + "generations": 8, + "chunks": 4716, + "redis_bytes": 3886928 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658985099989 uptime_in_seconds:207 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10274089 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 10000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 280.066, + "kind": "korean", + "max_rss_bytes": 46776320, + "n": 10000, + "operation": "initial_load", + "ready_ms": 550.367, + "received_bytes": 1221143, + "redis_bytes": 3989808, + "search_p50_ns": 584, + "search_p95_ns": 2166, + "search_p99_ns": 9375, + "search_samples": 548, + "sent_bytes": 5273746 + }, + { + "commit_ms": 234.813, + "kind": "korean", + "max_rss_bytes": 47546368, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 234.874, + "received_bytes": 1041404, + "redis_bytes": 3991296, + "search_p50_ns": 1750, + "search_p95_ns": 2500, + "search_p99_ns": 3083, + "search_samples": 230, + "sent_bytes": 627520 + }, + { + "commit_ms": 5.232, + "kind": "korean", + "max_rss_bytes": 56295424, + "n": 10000, + "operation": "add_1", + "ready_ms": 45.325, + "received_bytes": 1294906, + "redis_bytes": 5282944, + "search_p50_ns": 2958, + "search_p95_ns": 11625, + "search_p99_ns": 22375, + "search_samples": 41, + "sent_bytes": 652390 + }, + { + "commit_ms": 5.281, + "kind": "korean", + "max_rss_bytes": 63913984, + "n": 10000, + "operation": "remove_1", + "ready_ms": 25.384, + "received_bytes": 1294839, + "redis_bytes": 5941312, + "search_p50_ns": 2750, + "search_p95_ns": 8291, + "search_p99_ns": 8291, + "search_samples": 21, + "sent_bytes": 652269 + }, + { + "commit_ms": 119.311, + "kind": "korean", + "max_rss_bytes": 69664768, + "n": 10000, + "operation": "add_1000", + "ready_ms": 199.414, + "received_bytes": 1500093, + "redis_bytes": 7089312, + "search_p50_ns": 1875, + "search_p95_ns": 6334, + "search_p99_ns": 8541, + "search_samples": 195, + "sent_bytes": 1906214 + }, + { + "commit_ms": 118.485, + "kind": "korean", + "max_rss_bytes": 74317824, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 188.57, + "received_bytes": 1499874, + "redis_bytes": 7741440, + "search_p50_ns": 1833, + "search_p95_ns": 6583, + "search_p99_ns": 11333, + "search_samples": 184, + "sent_bytes": 1803266 + }, + { + "commit_ms": 17.547, + "kind": "korean", + "max_rss_bytes": 74366976, + "n": 10000, + "operation": "add_100", + "ready_ms": 37.703, + "received_bytes": 1317738, + "redis_bytes": 8442592, + "search_p50_ns": 1875, + "search_p95_ns": 8167, + "search_p99_ns": 9083, + "search_samples": 32, + "sent_bytes": 791657 + }, + { + "commit_ms": 17.981, + "kind": "korean", + "max_rss_bytes": 74399744, + "n": 10000, + "operation": "remove_100", + "ready_ms": 58.173, + "received_bytes": 1317774, + "redis_bytes": 9098576, + "search_p50_ns": 2125, + "search_p95_ns": 8917, + "search_p99_ns": 10375, + "search_samples": 53, + "sent_bytes": 779694 + }, + { + "commit_ms": 504.539, + "kind": "korean", + "max_rss_bytes": 76431360, + "n": 10000, + "operation": "full_replace", + "ready_ms": 774.691, + "received_bytes": 2112864, + "redis_bytes": 10926320, + "search_p50_ns": 1709, + "search_p95_ns": 2542, + "search_p99_ns": 9125, + "search_samples": 770, + "sent_bytes": 5906529 + } + ], + "startup_ms": 240.01, + "prune": { + "ms": 16.73, + "generations": 8, + "chunks": 4748, + "redis_bytes": 4078480 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658843702987 uptime_in_seconds:65 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10273947 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 10000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 288.942, + "kind": "korean", + "max_rss_bytes": 44728320, + "n": 10000, + "operation": "initial_load", + "ready_ms": 559.042, + "received_bytes": 1221164, + "redis_bytes": 4013872, + "search_p50_ns": 667, + "search_p95_ns": 1333, + "search_p99_ns": 5625, + "search_samples": 556, + "sent_bytes": 5273746 + }, + { + "commit_ms": 258.338, + "kind": "korean", + "max_rss_bytes": 48840704, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 258.408, + "received_bytes": 1041566, + "redis_bytes": 4015376, + "search_p50_ns": 1916, + "search_p95_ns": 2417, + "search_p99_ns": 3792, + "search_samples": 254, + "sent_bytes": 627722 + }, + { + "commit_ms": 6.258, + "kind": "korean", + "max_rss_bytes": 62996480, + "n": 10000, + "operation": "add_1", + "ready_ms": 36.35, + "received_bytes": 1294852, + "redis_bytes": 5307040, + "search_p50_ns": 2583, + "search_p95_ns": 9417, + "search_p99_ns": 27791, + "search_samples": 30, + "sent_bytes": 652289 + }, + { + "commit_ms": 6.128, + "kind": "korean", + "max_rss_bytes": 68157440, + "n": 10000, + "operation": "remove_1", + "ready_ms": 36.218, + "received_bytes": 1294857, + "redis_bytes": 5963360, + "search_p50_ns": 6500, + "search_p95_ns": 16208, + "search_p99_ns": 22625, + "search_samples": 30, + "sent_bytes": 652269 + }, + { + "commit_ms": 129.17, + "kind": "korean", + "max_rss_bytes": 74104832, + "n": 10000, + "operation": "add_1000", + "ready_ms": 219.27, + "received_bytes": 1500182, + "redis_bytes": 7113760, + "search_p50_ns": 1833, + "search_p95_ns": 3417, + "search_p99_ns": 8834, + "search_samples": 212, + "sent_bytes": 1906315 + }, + { + "commit_ms": 124.693, + "kind": "korean", + "max_rss_bytes": 74153984, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 214.848, + "received_bytes": 1499892, + "redis_bytes": 7765840, + "search_p50_ns": 1875, + "search_p95_ns": 3459, + "search_p99_ns": 8250, + "search_samples": 211, + "sent_bytes": 1803266 + }, + { + "commit_ms": 19.543, + "kind": "korean", + "max_rss_bytes": 77381632, + "n": 10000, + "operation": "add_100", + "ready_ms": 69.689, + "received_bytes": 1317828, + "redis_bytes": 8466944, + "search_p50_ns": 4333, + "search_p95_ns": 14667, + "search_p99_ns": 19084, + "search_samples": 64, + "sent_bytes": 791758 + }, + { + "commit_ms": 31.699, + "kind": "korean", + "max_rss_bytes": 77398016, + "n": 10000, + "operation": "remove_100", + "ready_ms": 81.942, + "received_bytes": 1317792, + "redis_bytes": 9122944, + "search_p50_ns": 5125, + "search_p95_ns": 11542, + "search_p99_ns": 22834, + "search_samples": 77, + "sent_bytes": 779694 + }, + { + "commit_ms": 779.995, + "kind": "korean", + "max_rss_bytes": 84066304, + "n": 10000, + "operation": "full_replace", + "ready_ms": 1150.181, + "received_bytes": 2113242, + "redis_bytes": 10949088, + "search_p50_ns": 2500, + "search_p95_ns": 6708, + "search_p99_ns": 13291, + "search_samples": 1139, + "sent_bytes": 5907034 + } + ], + "startup_ms": 269.046, + "prune": { + "ms": 24.478, + "generations": 8, + "chunks": 4748, + "redis_bytes": 4102304 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658988058620 uptime_in_seconds:210 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10274091 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 10000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 280.105, + "kind": "shared", + "max_rss_bytes": 26165248, + "n": 10000, + "operation": "initial_load", + "ready_ms": 540.175, + "received_bytes": 1108539, + "redis_bytes": 3691696, + "search_p50_ns": 416, + "search_p95_ns": 1417, + "search_p99_ns": 3708, + "search_samples": 539, + "sent_bytes": 5147742 + }, + { + "commit_ms": 232.972, + "kind": "shared", + "max_rss_bytes": 28114944, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 233.035, + "received_bytes": 928902, + "redis_bytes": 3693184, + "search_p50_ns": 1375, + "search_p95_ns": 2250, + "search_p99_ns": 5542, + "search_samples": 232, + "sent_bytes": 625528 + }, + { + "commit_ms": 5.68, + "kind": "shared", + "max_rss_bytes": 30490624, + "n": 10000, + "operation": "add_1", + "ready_ms": 35.836, + "received_bytes": 1291652, + "redis_bytes": 4986784, + "search_p50_ns": 1834, + "search_p95_ns": 6417, + "search_p99_ns": 6875, + "search_samples": 34, + "sent_bytes": 650768 + }, + { + "commit_ms": 5.431, + "kind": "shared", + "max_rss_bytes": 32243712, + "n": 10000, + "operation": "remove_1", + "ready_ms": 25.555, + "received_bytes": 1291585, + "redis_bytes": 5643120, + "search_p50_ns": 2042, + "search_p95_ns": 7500, + "search_p99_ns": 8708, + "search_samples": 23, + "sent_bytes": 650647 + }, + { + "commit_ms": 118.801, + "kind": "shared", + "max_rss_bytes": 33816576, + "n": 10000, + "operation": "add_1000", + "ready_ms": 208.976, + "received_bytes": 1453182, + "redis_bytes": 6762448, + "search_p50_ns": 1459, + "search_p95_ns": 6667, + "search_p99_ns": 9917, + "search_samples": 203, + "sent_bytes": 1881483 + }, + { + "commit_ms": 117.107, + "kind": "shared", + "max_rss_bytes": 34947072, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 197.198, + "received_bytes": 1452913, + "redis_bytes": 7415056, + "search_p50_ns": 1500, + "search_p95_ns": 6375, + "search_p99_ns": 12083, + "search_samples": 195, + "sent_bytes": 1764301 + }, + { + "commit_ms": 17.878, + "kind": "shared", + "max_rss_bytes": 35618816, + "n": 10000, + "operation": "add_100", + "ready_ms": 47.996, + "received_bytes": 1309879, + "redis_bytes": 8113664, + "search_p50_ns": 1541, + "search_p95_ns": 6708, + "search_p99_ns": 6833, + "search_samples": 44, + "sent_bytes": 787895 + }, + { + "commit_ms": 18.343, + "kind": "shared", + "max_rss_bytes": 35635200, + "n": 10000, + "operation": "remove_100", + "ready_ms": 58.459, + "received_bytes": 1309776, + "redis_bytes": 8769664, + "search_p50_ns": 1750, + "search_p95_ns": 6584, + "search_p99_ns": 7125, + "search_samples": 55, + "sent_bytes": 776826 + }, + { + "commit_ms": 498.445, + "kind": "shared", + "max_rss_bytes": 35913728, + "n": 10000, + "operation": "full_replace", + "ready_ms": 758.563, + "received_bytes": 1886739, + "redis_bytes": 10468432, + "search_p50_ns": 1334, + "search_p95_ns": 2000, + "search_p99_ns": 6875, + "search_samples": 752, + "sent_bytes": 5769565 + } + ], + "startup_ms": 234.238, + "prune": { + "ms": 16.681, + "generations": 8, + "chunks": 4736, + "redis_bytes": 3925536 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658838548994 uptime_in_seconds:60 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10273942 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 10000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 435.785, + "kind": "shared", + "max_rss_bytes": 26165248, + "n": 10000, + "operation": "initial_load", + "ready_ms": 826.01, + "received_bytes": 1108830, + "redis_bytes": 3895200, + "search_p50_ns": 625, + "search_p95_ns": 1500, + "search_p99_ns": 4583, + "search_samples": 821, + "sent_bytes": 5148045 + }, + { + "commit_ms": 272.664, + "kind": "shared", + "max_rss_bytes": 27869184, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 272.734, + "received_bytes": 929001, + "redis_bytes": 3893584, + "search_p50_ns": 1542, + "search_p95_ns": 2125, + "search_p99_ns": 7000, + "search_samples": 268, + "sent_bytes": 625629 + }, + { + "commit_ms": 7.123, + "kind": "shared", + "max_rss_bytes": 31014912, + "n": 10000, + "operation": "add_1", + "ready_ms": 27.221, + "received_bytes": 1291608, + "redis_bytes": 5185264, + "search_p50_ns": 2084, + "search_p95_ns": 6959, + "search_p99_ns": 7542, + "search_samples": 24, + "sent_bytes": 650667 + }, + { + "commit_ms": 6.502, + "kind": "shared", + "max_rss_bytes": 31588352, + "n": 10000, + "operation": "remove_1", + "ready_ms": 36.651, + "received_bytes": 1291608, + "redis_bytes": 5841584, + "search_p50_ns": 1792, + "search_p95_ns": 6375, + "search_p99_ns": 6917, + "search_samples": 30, + "sent_bytes": 650647 + }, + { + "commit_ms": 129.192, + "kind": "shared", + "max_rss_bytes": 32456704, + "n": 10000, + "operation": "add_1000", + "ready_ms": 209.312, + "received_bytes": 1453277, + "redis_bytes": 6962928, + "search_p50_ns": 1458, + "search_p95_ns": 2625, + "search_p99_ns": 6167, + "search_samples": 203, + "sent_bytes": 1881584 + }, + { + "commit_ms": 122.836, + "kind": "shared", + "max_rss_bytes": 32718848, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 203.034, + "received_bytes": 1452938, + "redis_bytes": 7615536, + "search_p50_ns": 1541, + "search_p95_ns": 5625, + "search_p99_ns": 7084, + "search_samples": 197, + "sent_bytes": 1764301 + }, + { + "commit_ms": 19.095, + "kind": "shared", + "max_rss_bytes": 33521664, + "n": 10000, + "operation": "add_100", + "ready_ms": 49.194, + "received_bytes": 1309904, + "redis_bytes": 8313888, + "search_p50_ns": 1625, + "search_p95_ns": 6750, + "search_p99_ns": 8375, + "search_samples": 45, + "sent_bytes": 787895 + }, + { + "commit_ms": 18.921, + "kind": "shared", + "max_rss_bytes": 33947648, + "n": 10000, + "operation": "remove_100", + "ready_ms": 49.118, + "received_bytes": 1309796, + "redis_bytes": 8969872, + "search_p50_ns": 1583, + "search_p95_ns": 6416, + "search_p99_ns": 7500, + "search_samples": 44, + "sent_bytes": 776826 + }, + { + "commit_ms": 509.235, + "kind": "shared", + "max_rss_bytes": 36569088, + "n": 10000, + "operation": "full_replace", + "ready_ms": 779.357, + "received_bytes": 1886764, + "redis_bytes": 10670416, + "search_p50_ns": 1333, + "search_p95_ns": 2208, + "search_p99_ns": 6167, + "search_samples": 773, + "sent_bytes": 5769565 + } + ], + "startup_ms": 326.513, + "prune": { + "ms": 22.056, + "generations": 8, + "chunks": 4736, + "redis_bytes": 3976208 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658981987265 uptime_in_seconds:203 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10274085 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 325.38, + "kind": "diverse", + "max_rss_bytes": 126959616, + "n": 100000, + "operation": "initial_load", + "ready_ms": 685.491, + "received_bytes": 2515082, + "redis_bytes": 5539904, + "search_p50_ns": 375, + "search_p95_ns": 1625, + "search_p99_ns": 9166, + "search_samples": 682, + "sent_bytes": 6948061 + }, + { + "commit_ms": 295.109, + "kind": "diverse", + "max_rss_bytes": 128548864, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 295.169, + "received_bytes": 2334058, + "redis_bytes": 5541344, + "search_p50_ns": 1625, + "search_p95_ns": 2250, + "search_p99_ns": 4084, + "search_samples": 282, + "sent_bytes": 683996 + }, + { + "commit_ms": 5.538, + "kind": "diverse", + "max_rss_bytes": 191823872, + "n": 100000, + "operation": "add_1", + "ready_ms": 95.676, + "received_bytes": 1390495, + "redis_bytes": 6999248, + "search_p50_ns": 2208, + "search_p95_ns": 8083, + "search_p99_ns": 40083, + "search_samples": 86, + "sent_bytes": 700114 + }, + { + "commit_ms": 5.591, + "kind": "diverse", + "max_rss_bytes": 213237760, + "n": 100000, + "operation": "remove_1", + "ready_ms": 75.756, + "received_bytes": 1390501, + "redis_bytes": 7704688, + "search_p50_ns": 1916, + "search_p95_ns": 7709, + "search_p99_ns": 66334, + "search_samples": 62, + "sent_bytes": 700094 + }, + { + "commit_ms": 129.577, + "kind": "diverse", + "max_rss_bytes": 213319680, + "n": 100000, + "operation": "add_1000", + "ready_ms": 269.67, + "received_bytes": 2123409, + "redis_bytes": 8415920, + "search_p50_ns": 1667, + "search_p95_ns": 3250, + "search_p99_ns": 7708, + "search_samples": 260, + "sent_bytes": 2225130 + }, + { + "commit_ms": 129.908, + "kind": "diverse", + "max_rss_bytes": 219119616, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 280.098, + "received_bytes": 2123337, + "redis_bytes": 9802368, + "search_p50_ns": 1791, + "search_p95_ns": 6875, + "search_p99_ns": 11500, + "search_samples": 272, + "sent_bytes": 2202029 + }, + { + "commit_ms": 130.155, + "kind": "diverse", + "max_rss_bytes": 235585536, + "n": 100000, + "operation": "add_1000", + "ready_ms": 280.243, + "received_bytes": 2123410, + "redis_bytes": 10474448, + "search_p50_ns": 1667, + "search_p95_ns": 7125, + "search_p99_ns": 10500, + "search_samples": 272, + "sent_bytes": 2225130 + }, + { + "commit_ms": 128.845, + "kind": "diverse", + "max_rss_bytes": 235585536, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 269.038, + "received_bytes": 2123341, + "redis_bytes": 11179888, + "search_p50_ns": 1708, + "search_p95_ns": 7708, + "search_p99_ns": 19375, + "search_samples": 259, + "sent_bytes": 2202029 + }, + { + "commit_ms": 601.067, + "kind": "diverse", + "max_rss_bytes": 274235392, + "n": 100000, + "operation": "full_replace", + "ready_ms": 971.173, + "received_bytes": 4880621, + "redis_bytes": 14710848, + "search_p50_ns": 1584, + "search_p95_ns": 2750, + "search_p99_ns": 9750, + "search_samples": 956, + "sent_bytes": 7828565 + } + ], + "startup_ms": 329.453, + "prune": { + "ms": 17.823, + "generations": 8, + "chunks": 4987, + "redis_bytes": 5735600 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658849906082 uptime_in_seconds:71 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10273953 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 394.512, + "kind": "diverse", + "max_rss_bytes": 126877696, + "n": 100000, + "operation": "initial_load", + "ready_ms": 794.647, + "received_bytes": 2515163, + "redis_bytes": 5565504, + "search_p50_ns": 417, + "search_p95_ns": 1125, + "search_p99_ns": 8583, + "search_samples": 791, + "sent_bytes": 6948162 + }, + { + "commit_ms": 410.88, + "kind": "diverse", + "max_rss_bytes": 128417792, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 410.97, + "received_bytes": 2334209, + "redis_bytes": 5566960, + "search_p50_ns": 2542, + "search_p95_ns": 9958, + "search_p99_ns": 14541, + "search_samples": 393, + "sent_bytes": 684198 + }, + { + "commit_ms": 9.29, + "kind": "diverse", + "max_rss_bytes": 187990016, + "n": 100000, + "operation": "add_1", + "ready_ms": 129.496, + "received_bytes": 1390502, + "redis_bytes": 7023808, + "search_p50_ns": 5542, + "search_p95_ns": 14875, + "search_p99_ns": 18500, + "search_samples": 111, + "sent_bytes": 700114 + }, + { + "commit_ms": 9.495, + "kind": "diverse", + "max_rss_bytes": 202866688, + "n": 100000, + "operation": "remove_1", + "ready_ms": 129.697, + "received_bytes": 1390508, + "redis_bytes": 7026848, + "search_p50_ns": 4584, + "search_p95_ns": 14750, + "search_p99_ns": 18959, + "search_samples": 112, + "sent_bytes": 700094 + }, + { + "commit_ms": 256.753, + "kind": "diverse", + "max_rss_bytes": 202866688, + "n": 100000, + "operation": "add_1000", + "ready_ms": 506.993, + "received_bytes": 2123557, + "redis_bytes": 8441184, + "search_p50_ns": 3083, + "search_p95_ns": 11667, + "search_p99_ns": 23541, + "search_samples": 488, + "sent_bytes": 2225332 + }, + { + "commit_ms": 255.51, + "kind": "diverse", + "max_rss_bytes": 242188288, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 485.729, + "received_bytes": 2123559, + "redis_bytes": 9144576, + "search_p50_ns": 3083, + "search_p95_ns": 11292, + "search_p99_ns": 21000, + "search_samples": 466, + "sent_bytes": 2202332 + }, + { + "commit_ms": 203.653, + "kind": "diverse", + "max_rss_bytes": 246611968, + "n": 100000, + "operation": "add_1000", + "ready_ms": 393.851, + "received_bytes": 2123415, + "redis_bytes": 10498672, + "search_p50_ns": 2417, + "search_p95_ns": 3542, + "search_p99_ns": 14875, + "search_samples": 376, + "sent_bytes": 2225130 + }, + { + "commit_ms": 176.062, + "kind": "diverse", + "max_rss_bytes": 246628352, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 356.165, + "received_bytes": 2123419, + "redis_bytes": 10520096, + "search_p50_ns": 2250, + "search_p95_ns": 5208, + "search_p99_ns": 11667, + "search_samples": 342, + "sent_bytes": 2202130 + }, + { + "commit_ms": 715.711, + "kind": "diverse", + "max_rss_bytes": 291209216, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1245.872, + "received_bytes": 4880774, + "redis_bytes": 14734720, + "search_p50_ns": 2042, + "search_p95_ns": 7917, + "search_p99_ns": 15458, + "search_samples": 1225, + "sent_bytes": 7828767 + } + ], + "startup_ms": 364.114, + "prune": { + "ms": 28.67, + "generations": 8, + "chunks": 4987, + "redis_bytes": 5760352 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658996186797 uptime_in_seconds:218 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10274100 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 347.358, + "kind": "korean", + "max_rss_bytes": 211042304, + "n": 100000, + "operation": "initial_load", + "ready_ms": 737.47, + "received_bytes": 4516121, + "redis_bytes": 7705184, + "search_p50_ns": 625, + "search_p95_ns": 2208, + "search_p99_ns": 12625, + "search_samples": 733, + "sent_bytes": 8949098 + }, + { + "commit_ms": 319.342, + "kind": "korean", + "max_rss_bytes": 213450752, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 319.401, + "received_bytes": 4335096, + "redis_bytes": 7708688, + "search_p50_ns": 2000, + "search_p95_ns": 2875, + "search_p99_ns": 7916, + "search_samples": 307, + "sent_bytes": 683996 + }, + { + "commit_ms": 5.544, + "kind": "korean", + "max_rss_bytes": 356139008, + "n": 100000, + "operation": "add_1", + "ready_ms": 115.698, + "received_bytes": 1391687, + "redis_bytes": 9168352, + "search_p50_ns": 2625, + "search_p95_ns": 9917, + "search_p99_ns": 15083, + "search_samples": 103, + "sent_bytes": 700709 + }, + { + "commit_ms": 5.638, + "kind": "korean", + "max_rss_bytes": 361267200, + "n": 100000, + "operation": "remove_1", + "ready_ms": 105.818, + "received_bytes": 1391689, + "redis_bytes": 9873840, + "search_p50_ns": 2708, + "search_p95_ns": 12542, + "search_p99_ns": 33958, + "search_samples": 100, + "sent_bytes": 700689 + }, + { + "commit_ms": 140.191, + "kind": "korean", + "max_rss_bytes": 361332736, + "n": 100000, + "operation": "add_1000", + "ready_ms": 320.305, + "received_bytes": 2998258, + "redis_bytes": 11070352, + "search_p50_ns": 2166, + "search_p95_ns": 8250, + "search_p99_ns": 15750, + "search_samples": 311, + "sent_bytes": 2662556 + }, + { + "commit_ms": 133.875, + "kind": "korean", + "max_rss_bytes": 361349120, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 314.058, + "received_bytes": 2998254, + "redis_bytes": 11772080, + "search_p50_ns": 2208, + "search_p95_ns": 8208, + "search_p99_ns": 15000, + "search_samples": 303, + "sent_bytes": 2639549 + }, + { + "commit_ms": 135.349, + "kind": "korean", + "max_rss_bytes": 361381888, + "n": 100000, + "operation": "add_1000", + "ready_ms": 315.458, + "received_bytes": 2998187, + "redis_bytes": 12445392, + "search_p50_ns": 2209, + "search_p95_ns": 8584, + "search_p99_ns": 13667, + "search_samples": 311, + "sent_bytes": 2662455 + }, + { + "commit_ms": 152.668, + "kind": "korean", + "max_rss_bytes": 361431040, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 322.85, + "received_bytes": 2998260, + "redis_bytes": 13834864, + "search_p50_ns": 2208, + "search_p95_ns": 8792, + "search_p99_ns": 18291, + "search_samples": 311, + "sent_bytes": 2639549 + }, + { + "commit_ms": 636.695, + "kind": "korean", + "max_rss_bytes": 415154176, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1046.85, + "received_bytes": 8883025, + "redis_bytes": 19572560, + "search_p50_ns": 2083, + "search_p95_ns": 3625, + "search_p99_ns": 9041, + "search_samples": 1026, + "sent_bytes": 9829925 + } + ], + "startup_ms": 374.114, + "prune": { + "ms": 18.153, + "generations": 8, + "chunks": 4987, + "redis_bytes": 7946704 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658853787346 uptime_in_seconds:75 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10273957 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 536.605, + "kind": "korean", + "max_rss_bytes": 207732736, + "n": 100000, + "operation": "initial_load", + "ready_ms": 1056.783, + "received_bytes": 4516416, + "redis_bytes": 7731472, + "search_p50_ns": 875, + "search_p95_ns": 2458, + "search_p99_ns": 9208, + "search_samples": 1056, + "sent_bytes": 8949502 + }, + { + "commit_ms": 405.201, + "kind": "korean", + "max_rss_bytes": 230293504, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 405.272, + "received_bytes": 4335247, + "redis_bytes": 7734976, + "search_p50_ns": 2291, + "search_p95_ns": 4125, + "search_p99_ns": 6667, + "search_samples": 390, + "sent_bytes": 684198 + }, + { + "commit_ms": 7.881, + "kind": "korean", + "max_rss_bytes": 342425600, + "n": 100000, + "operation": "add_1", + "ready_ms": 148.075, + "received_bytes": 1391692, + "redis_bytes": 9194688, + "search_p50_ns": 2792, + "search_p95_ns": 12125, + "search_p99_ns": 23542, + "search_samples": 139, + "sent_bytes": 700709 + }, + { + "commit_ms": 7.707, + "kind": "korean", + "max_rss_bytes": 385925120, + "n": 100000, + "operation": "remove_1", + "ready_ms": 147.898, + "received_bytes": 1391698, + "redis_bytes": 9216128, + "search_p50_ns": 3875, + "search_p95_ns": 13333, + "search_p99_ns": 16083, + "search_samples": 138, + "sent_bytes": 700689 + }, + { + "commit_ms": 192.397, + "kind": "korean", + "max_rss_bytes": 391069696, + "n": 100000, + "operation": "add_1000", + "ready_ms": 402.558, + "received_bytes": 2998260, + "redis_bytes": 11096448, + "search_p50_ns": 2750, + "search_p95_ns": 9875, + "search_p99_ns": 18458, + "search_samples": 384, + "sent_bytes": 2662556 + }, + { + "commit_ms": 179.108, + "kind": "korean", + "max_rss_bytes": 418873344, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 419.225, + "received_bytes": 2998264, + "redis_bytes": 11798112, + "search_p50_ns": 2708, + "search_p95_ns": 11625, + "search_p99_ns": 16584, + "search_samples": 413, + "sent_bytes": 2639549 + }, + { + "commit_ms": 288.337, + "kind": "korean", + "max_rss_bytes": 418906112, + "n": 100000, + "operation": "add_1000", + "ready_ms": 628.524, + "received_bytes": 2998477, + "redis_bytes": 12468304, + "search_p50_ns": 5667, + "search_p95_ns": 13333, + "search_p99_ns": 20292, + "search_samples": 607, + "sent_bytes": 2662859 + }, + { + "commit_ms": 285, + "kind": "korean", + "max_rss_bytes": 418922496, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 595.177, + "received_bytes": 2998410, + "redis_bytes": 13173760, + "search_p50_ns": 4333, + "search_p95_ns": 14333, + "search_p99_ns": 24375, + "search_samples": 568, + "sent_bytes": 2639751 + }, + { + "commit_ms": 996.028, + "kind": "korean", + "max_rss_bytes": 474284032, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1616.206, + "received_bytes": 8883534, + "redis_bytes": 19573936, + "search_p50_ns": 3125, + "search_p95_ns": 7084, + "search_p99_ns": 15125, + "search_samples": 1595, + "sent_bytes": 9830632 + } + ], + "startup_ms": 617.747, + "prune": { + "ms": 30.187, + "generations": 8, + "chunks": 4987, + "redis_bytes": 7970208 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788659001385446 uptime_in_seconds:223 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10274105 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 319.486, + "kind": "shared", + "max_rss_bytes": 58294272, + "n": 100000, + "operation": "initial_load", + "ready_ms": 639.614, + "received_bytes": 3415090, + "redis_bytes": 6517776, + "search_p50_ns": 375, + "search_p95_ns": 1416, + "search_p99_ns": 3750, + "search_samples": 634, + "sent_bytes": 7848071 + }, + { + "commit_ms": 293.914, + "kind": "shared", + "max_rss_bytes": 72024064, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 293.975, + "received_bytes": 3234066, + "redis_bytes": 6519008, + "search_p50_ns": 1666, + "search_p95_ns": 2417, + "search_p99_ns": 8458, + "search_samples": 286, + "sent_bytes": 683996 + }, + { + "commit_ms": 5.688, + "kind": "shared", + "max_rss_bytes": 91357184, + "n": 100000, + "operation": "add_1", + "ready_ms": 55.864, + "received_bytes": 1390748, + "redis_bytes": 7974960, + "search_p50_ns": 2083, + "search_p95_ns": 10500, + "search_p99_ns": 13792, + "search_samples": 51, + "sent_bytes": 700241 + }, + { + "commit_ms": 5.676, + "kind": "shared", + "max_rss_bytes": 97419264, + "n": 100000, + "operation": "remove_1", + "ready_ms": 45.834, + "received_bytes": 1390753, + "redis_bytes": 8683008, + "search_p50_ns": 2000, + "search_p95_ns": 10334, + "search_p99_ns": 13833, + "search_samples": 34, + "sent_bytes": 700221 + }, + { + "commit_ms": 133.352, + "kind": "shared", + "max_rss_bytes": 97697792, + "n": 100000, + "operation": "add_1000", + "ready_ms": 243.464, + "received_bytes": 2517361, + "redis_bytes": 10300224, + "search_p50_ns": 1791, + "search_p95_ns": 3209, + "search_p99_ns": 7917, + "search_samples": 232, + "sent_bytes": 2422042 + }, + { + "commit_ms": 132.639, + "kind": "shared", + "max_rss_bytes": 97730560, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 242.793, + "received_bytes": 2517365, + "redis_bytes": 11001264, + "search_p50_ns": 1791, + "search_p95_ns": 7542, + "search_p99_ns": 9542, + "search_samples": 234, + "sent_bytes": 2399041 + }, + { + "commit_ms": 132.148, + "kind": "shared", + "max_rss_bytes": 97763328, + "n": 100000, + "operation": "add_1000", + "ready_ms": 252.242, + "received_bytes": 2517366, + "redis_bytes": 11672640, + "search_p50_ns": 1792, + "search_p95_ns": 7458, + "search_p99_ns": 12250, + "search_samples": 247, + "sent_bytes": 2422042 + }, + { + "commit_ms": 132.154, + "kind": "shared", + "max_rss_bytes": 101761024, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 242.251, + "received_bytes": 2517366, + "redis_bytes": 12380656, + "search_p50_ns": 1792, + "search_p95_ns": 7291, + "search_p99_ns": 8375, + "search_samples": 236, + "sent_bytes": 2399041 + }, + { + "commit_ms": 602.973, + "kind": "shared", + "max_rss_bytes": 132972544, + "n": 100000, + "operation": "full_replace", + "ready_ms": 923.124, + "received_bytes": 6680615, + "redis_bytes": 16893296, + "search_p50_ns": 1709, + "search_p95_ns": 2375, + "search_p99_ns": 7458, + "search_samples": 918, + "sent_bytes": 8728516 + } + ], + "startup_ms": 304.226, + "prune": { + "ms": 17.862, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6719680 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658846334029 uptime_in_seconds:68 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10273950 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 413.938, + "kind": "shared", + "max_rss_bytes": 57475072, + "n": 100000, + "operation": "initial_load", + "ready_ms": 794.1, + "received_bytes": 3415246, + "redis_bytes": 6542672, + "search_p50_ns": 500, + "search_p95_ns": 1209, + "search_p99_ns": 5500, + "search_samples": 787, + "sent_bytes": 7848273 + }, + { + "commit_ms": 384.481, + "kind": "shared", + "max_rss_bytes": 73154560, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 384.565, + "received_bytes": 3234147, + "redis_bytes": 6543664, + "search_p50_ns": 2084, + "search_p95_ns": 7333, + "search_p99_ns": 15666, + "search_samples": 373, + "sent_bytes": 684097 + }, + { + "commit_ms": 8.346, + "kind": "shared", + "max_rss_bytes": 90963968, + "n": 100000, + "operation": "add_1", + "ready_ms": 58.477, + "received_bytes": 1390756, + "redis_bytes": 7999616, + "search_p50_ns": 2375, + "search_p95_ns": 5000, + "search_p99_ns": 11791, + "search_samples": 49, + "sent_bytes": 700241 + }, + { + "commit_ms": 7.803, + "kind": "shared", + "max_rss_bytes": 97845248, + "n": 100000, + "operation": "remove_1", + "ready_ms": 68.002, + "received_bytes": 1390761, + "redis_bytes": 8707632, + "search_p50_ns": 2250, + "search_p95_ns": 6084, + "search_p99_ns": 65584, + "search_samples": 59, + "sent_bytes": 700221 + }, + { + "commit_ms": 161.607, + "kind": "shared", + "max_rss_bytes": 98402304, + "n": 100000, + "operation": "add_1000", + "ready_ms": 321.718, + "received_bytes": 2517441, + "redis_bytes": 10305200, + "search_p50_ns": 2084, + "search_p95_ns": 8042, + "search_p99_ns": 16958, + "search_samples": 300, + "sent_bytes": 2422143 + }, + { + "commit_ms": 240.387, + "kind": "shared", + "max_rss_bytes": 98451456, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 400.552, + "received_bytes": 2517516, + "redis_bytes": 11025712, + "search_p50_ns": 2667, + "search_p95_ns": 8834, + "search_p99_ns": 16792, + "search_samples": 395, + "sent_bytes": 2399243 + }, + { + "commit_ms": 182.868, + "kind": "shared", + "max_rss_bytes": 98959360, + "n": 100000, + "operation": "add_1000", + "ready_ms": 363.103, + "received_bytes": 2517444, + "redis_bytes": 11013808, + "search_p50_ns": 2416, + "search_p95_ns": 7250, + "search_p99_ns": 14042, + "search_samples": 354, + "sent_bytes": 2422143 + }, + { + "commit_ms": 222.368, + "kind": "shared", + "max_rss_bytes": 98959360, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 392.539, + "received_bytes": 2517515, + "redis_bytes": 11719264, + "search_p50_ns": 2792, + "search_p95_ns": 11917, + "search_p99_ns": 20000, + "search_samples": 376, + "sent_bytes": 2399243 + }, + { + "commit_ms": 946.701, + "kind": "shared", + "max_rss_bytes": 155435008, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1386.894, + "received_bytes": 6681124, + "redis_bytes": 16915680, + "search_p50_ns": 2542, + "search_p95_ns": 3709, + "search_p99_ns": 15833, + "search_samples": 1364, + "sent_bytes": 8729223 + } + ], + "startup_ms": 349.273, + "prune": { + "ms": 23.551, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6743536 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658991278648 uptime_in_seconds:213 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10274095 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 758.888, + "kind": "diverse", + "max_rss_bytes": 993394688, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 2079.072, + "received_bytes": 16923938, + "redis_bytes": 21323344, + "search_p50_ns": 417, + "search_p95_ns": 1541, + "search_p99_ns": 6375, + "search_samples": 2039, + "sent_bytes": 21357165 + }, + { + "commit_ms": 734.026, + "kind": "diverse", + "max_rss_bytes": 1001078784, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 734.089, + "received_bytes": 16742911, + "redis_bytes": 21327904, + "search_p50_ns": 1875, + "search_p95_ns": 3208, + "search_p99_ns": 6166, + "search_samples": 718, + "sent_bytes": 684905 + }, + { + "commit_ms": 5.635, + "kind": "diverse", + "max_rss_bytes": 1414021120, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1105.788, + "received_bytes": 1405232, + "redis_bytes": 22103296, + "search_p50_ns": 2292, + "search_p95_ns": 10459, + "search_p99_ns": 20417, + "search_samples": 1066, + "sent_bytes": 707477 + }, + { + "commit_ms": 6.149, + "kind": "diverse", + "max_rss_bytes": 1961033728, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1016.247, + "received_bytes": 1405236, + "redis_bytes": 22808768, + "search_p50_ns": 2209, + "search_p95_ns": 5750, + "search_p99_ns": 15875, + "search_samples": 949, + "sent_bytes": 707457 + }, + { + "commit_ms": 227.279, + "kind": "diverse", + "max_rss_bytes": 2199289856, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1317.391, + "received_bytes": 8390164, + "redis_bytes": 27624560, + "search_p50_ns": 2125, + "search_p95_ns": 4791, + "search_p99_ns": 12250, + "search_samples": 1259, + "sent_bytes": 5358571 + }, + { + "commit_ms": 258.939, + "kind": "diverse", + "max_rss_bytes": 2199289856, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 2289.317, + "received_bytes": 8390238, + "redis_bytes": 28325808, + "search_p50_ns": 3959, + "search_p95_ns": 19500, + "search_p99_ns": 61750, + "search_samples": 2022, + "sent_bytes": 5335672 + }, + { + "commit_ms": 2611.763, + "kind": "diverse", + "max_rss_bytes": 2199289856, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 4081.919, + "received_bytes": 30943921, + "redis_bytes": 46838848, + "search_p50_ns": 3791, + "search_p95_ns": 19375, + "search_p99_ns": 54666, + "search_samples": 3709, + "sent_bytes": 20422296 + }, + { + "commit_ms": 935.392, + "kind": "diverse", + "max_rss_bytes": 2199289856, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2375.511, + "received_bytes": 30941554, + "redis_bytes": 47535024, + "search_p50_ns": 2166, + "search_p95_ns": 10000, + "search_p99_ns": 17792, + "search_samples": 2311, + "sent_bytes": 20178949 + }, + { + "commit_ms": 1367.377, + "kind": "diverse", + "max_rss_bytes": 2199289856, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 3058.137, + "received_bytes": 35498026, + "redis_bytes": 69894624, + "search_p50_ns": 2167, + "search_p95_ns": 12209, + "search_p99_ns": 20625, + "search_samples": 2961, + "sent_bytes": 24038174 + } + ], + "startup_ms": 1322.347, + "prune": { + "ms": 37.289, + "generations": 8, + "chunks": 8726, + "redis_bytes": 24221680 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658871055886 uptime_in_seconds:93 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10273974 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 767.112, + "kind": "diverse", + "max_rss_bytes": 1003716608, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 2137.194, + "received_bytes": 16923947, + "redis_bytes": 21346560, + "search_p50_ns": 416, + "search_p95_ns": 1542, + "search_p99_ns": 6333, + "search_samples": 2062, + "sent_bytes": 21357165 + }, + { + "commit_ms": 989.067, + "kind": "diverse", + "max_rss_bytes": 1207074816, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 989.164, + "received_bytes": 16743278, + "redis_bytes": 21348032, + "search_p50_ns": 2583, + "search_p95_ns": 10083, + "search_p99_ns": 17334, + "search_samples": 970, + "sent_bytes": 685410 + }, + { + "commit_ms": 9.436, + "kind": "diverse", + "max_rss_bytes": 1656127488, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1469.593, + "received_bytes": 1405238, + "redis_bytes": 22123440, + "search_p50_ns": 2708, + "search_p95_ns": 10167, + "search_p99_ns": 23334, + "search_samples": 1401, + "sent_bytes": 707477 + }, + { + "commit_ms": 9.204, + "kind": "diverse", + "max_rss_bytes": 1949024256, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1329.361, + "received_bytes": 1405236, + "redis_bytes": 22804320, + "search_p50_ns": 2750, + "search_p95_ns": 9875, + "search_p99_ns": 19792, + "search_samples": 1269, + "sent_bytes": 707457 + }, + { + "commit_ms": 238.205, + "kind": "diverse", + "max_rss_bytes": 2019000320, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1458.312, + "received_bytes": 8390239, + "redis_bytes": 27644560, + "search_p50_ns": 2208, + "search_p95_ns": 4166, + "search_p99_ns": 9584, + "search_samples": 1421, + "sent_bytes": 5358672 + }, + { + "commit_ms": 195.463, + "kind": "diverse", + "max_rss_bytes": 2019295232, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1165.586, + "received_bytes": 8390106, + "redis_bytes": 28349920, + "search_p50_ns": 2083, + "search_p95_ns": 4042, + "search_p99_ns": 10334, + "search_samples": 1119, + "sent_bytes": 5335470 + }, + { + "commit_ms": 790.355, + "kind": "diverse", + "max_rss_bytes": 2039201792, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2110.468, + "received_bytes": 30941344, + "redis_bytes": 46817312, + "search_p50_ns": 1916, + "search_p95_ns": 2875, + "search_p99_ns": 7458, + "search_samples": 2064, + "sent_bytes": 20418646 + }, + { + "commit_ms": 790.511, + "kind": "diverse", + "max_rss_bytes": 2041528320, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2230.641, + "received_bytes": 30941344, + "redis_bytes": 47506304, + "search_p50_ns": 2000, + "search_p95_ns": 3125, + "search_p99_ns": 7500, + "search_samples": 2189, + "sent_bytes": 20178646 + }, + { + "commit_ms": 1594.95, + "kind": "diverse", + "max_rss_bytes": 2204205056, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 3955.125, + "received_bytes": 35498472, + "redis_bytes": 69897552, + "search_p50_ns": 3000, + "search_p95_ns": 9708, + "search_p99_ns": 18667, + "search_samples": 3859, + "sent_bytes": 24038794 + } + ], + "startup_ms": 1342.022, + "prune": { + "ms": 35.189, + "generations": 8, + "chunks": 8726, + "redis_bytes": 24225664 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788659027750973 uptime_in_seconds:249 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10274131 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 991.239, + "kind": "korean", + "max_rss_bytes": 1393262592, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 2561.343, + "received_bytes": 36924360, + "redis_bytes": 44666512, + "search_p50_ns": 666, + "search_p95_ns": 1667, + "search_p99_ns": 6542, + "search_samples": 2503, + "sent_bytes": 41357737 + }, + { + "commit_ms": 977.772, + "kind": "korean", + "max_rss_bytes": 1403092992, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 977.834, + "received_bytes": 36743339, + "redis_bytes": 44679232, + "search_p50_ns": 2333, + "search_p95_ns": 3500, + "search_p99_ns": 10750, + "search_samples": 956, + "sent_bytes": 685410 + }, + { + "commit_ms": 5.764, + "kind": "korean", + "max_rss_bytes": 2169962496, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1215.932, + "received_bytes": 1414872, + "redis_bytes": 45460768, + "search_p50_ns": 3541, + "search_p95_ns": 14083, + "search_p99_ns": 20583, + "search_samples": 1154, + "sent_bytes": 712297 + }, + { + "commit_ms": 6.758, + "kind": "korean", + "max_rss_bytes": 2170028032, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1066.968, + "received_bytes": 1414878, + "redis_bytes": 46150864, + "search_p50_ns": 2625, + "search_p95_ns": 9750, + "search_p99_ns": 18833, + "search_samples": 1034, + "sent_bytes": 712277 + }, + { + "commit_ms": 245.153, + "kind": "korean", + "max_rss_bytes": 2170044416, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1375.246, + "received_bytes": 17126562, + "redis_bytes": 56077712, + "search_p50_ns": 2500, + "search_p95_ns": 4750, + "search_p99_ns": 10916, + "search_samples": 1331, + "sent_bytes": 9726769 + }, + { + "commit_ms": 269.012, + "kind": "korean", + "max_rss_bytes": 2170044416, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1459.195, + "received_bytes": 17126643, + "redis_bytes": 56779072, + "search_p50_ns": 2542, + "search_p95_ns": 9542, + "search_p99_ns": 15792, + "search_samples": 1419, + "sent_bytes": 9703870 + }, + { + "commit_ms": 1061.958, + "kind": "korean", + "max_rss_bytes": 2181824512, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2822.16, + "received_bytes": 67460054, + "redis_bytes": 96343008, + "search_p50_ns": 2208, + "search_p95_ns": 8041, + "search_p99_ns": 21166, + "search_samples": 2753, + "sent_bytes": 38678337 + }, + { + "commit_ms": 1007.17, + "kind": "korean", + "max_rss_bytes": 2609741824, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2567.274, + "received_bytes": 67459979, + "redis_bytes": 97047200, + "search_p50_ns": 2458, + "search_p95_ns": 5500, + "search_p99_ns": 12416, + "search_samples": 2523, + "sent_bytes": 38438215 + }, + { + "commit_ms": 1487.384, + "kind": "korean", + "max_rss_bytes": 2627895296, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 3237.504, + "received_bytes": 75498856, + "redis_bytes": 141176480, + "search_p50_ns": 2417, + "search_p95_ns": 5083, + "search_p99_ns": 14667, + "search_samples": 3175, + "sent_bytes": 44039057 + } + ], + "startup_ms": 1510.889, + "prune": { + "ms": 52.739, + "generations": 8, + "chunks": 8726, + "redis_bytes": 45987376 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658891143728 uptime_in_seconds:113 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10273995 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 1008.525, + "kind": "korean", + "max_rss_bytes": 1302478848, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 2618.653, + "received_bytes": 36924368, + "redis_bytes": 44667024, + "search_p50_ns": 667, + "search_p95_ns": 1875, + "search_p99_ns": 7375, + "search_samples": 2540, + "sent_bytes": 41357737 + }, + { + "commit_ms": 1087.28, + "kind": "korean", + "max_rss_bytes": 1438679040, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 1087.343, + "received_bytes": 36743418, + "redis_bytes": 44679744, + "search_p50_ns": 2167, + "search_p95_ns": 3750, + "search_p99_ns": 9708, + "search_samples": 1070, + "sent_bytes": 685511 + }, + { + "commit_ms": 6.265, + "kind": "korean", + "max_rss_bytes": 2183331840, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1316.372, + "received_bytes": 1414951, + "redis_bytes": 45461328, + "search_p50_ns": 2542, + "search_p95_ns": 11709, + "search_p99_ns": 22625, + "search_samples": 1262, + "sent_bytes": 712398 + }, + { + "commit_ms": 6.883, + "kind": "korean", + "max_rss_bytes": 2203697152, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1407.078, + "received_bytes": 1414884, + "redis_bytes": 46130944, + "search_p50_ns": 3208, + "search_p95_ns": 10084, + "search_p99_ns": 20834, + "search_samples": 1345, + "sent_bytes": 712277 + }, + { + "commit_ms": 292.835, + "kind": "korean", + "max_rss_bytes": 2336768000, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1903.078, + "received_bytes": 17126705, + "redis_bytes": 56049792, + "search_p50_ns": 2959, + "search_p95_ns": 6041, + "search_p99_ns": 15833, + "search_samples": 1843, + "sent_bytes": 9726971 + }, + { + "commit_ms": 251.164, + "kind": "korean", + "max_rss_bytes": 2403123200, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1431.251, + "received_bytes": 17126646, + "redis_bytes": 56766512, + "search_p50_ns": 2417, + "search_p95_ns": 4500, + "search_p99_ns": 11042, + "search_samples": 1385, + "sent_bytes": 9703870 + }, + { + "commit_ms": 994.385, + "kind": "korean", + "max_rss_bytes": 2437103616, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2644.5, + "received_bytes": 67459988, + "redis_bytes": 96344656, + "search_p50_ns": 2333, + "search_p95_ns": 6500, + "search_p99_ns": 14666, + "search_samples": 2580, + "sent_bytes": 38678236 + }, + { + "commit_ms": 1333.822, + "kind": "korean", + "max_rss_bytes": 2501443584, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 3314.049, + "received_bytes": 67460420, + "redis_bytes": 97033504, + "search_p50_ns": 2875, + "search_p95_ns": 5375, + "search_p99_ns": 15125, + "search_samples": 3259, + "sent_bytes": 38438821 + }, + { + "commit_ms": 1686.152, + "kind": "korean", + "max_rss_bytes": 2505539584, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 3916.249, + "received_bytes": 75499147, + "redis_bytes": 141192784, + "search_p50_ns": 2708, + "search_p95_ns": 7833, + "search_p99_ns": 18083, + "search_samples": 3818, + "sent_bytes": 44039461 + } + ], + "startup_ms": 1542.771, + "prune": { + "ms": 46.981, + "generations": 8, + "chunks": 8726, + "redis_bytes": 46002032 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788659046617025 uptime_in_seconds:268 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10274150 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 647.956, + "kind": "shared", + "max_rss_bytes": 363380736, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 1608.177, + "received_bytes": 25923787, + "redis_bytes": 30884176, + "search_p50_ns": 417, + "search_p95_ns": 1167, + "search_p99_ns": 4167, + "search_samples": 1574, + "sent_bytes": 30356963 + }, + { + "commit_ms": 639.919, + "kind": "shared", + "max_rss_bytes": 486031360, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 639.982, + "received_bytes": 25742761, + "redis_bytes": 30888688, + "search_p50_ns": 2000, + "search_p95_ns": 3000, + "search_p99_ns": 8834, + "search_samples": 612, + "sent_bytes": 684703 + }, + { + "commit_ms": 5.741, + "kind": "shared", + "max_rss_bytes": 690749440, + "n": 1000000, + "operation": "add_1", + "ready_ms": 545.928, + "received_bytes": 1410351, + "redis_bytes": 31667136, + "search_p50_ns": 2167, + "search_p95_ns": 6042, + "search_p99_ns": 11834, + "search_samples": 524, + "sent_bytes": 710040 + }, + { + "commit_ms": 6.228, + "kind": "shared", + "max_rss_bytes": 755138560, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 526.387, + "received_bytes": 1410357, + "redis_bytes": 32372592, + "search_p50_ns": 2292, + "search_p95_ns": 4417, + "search_p99_ns": 12167, + "search_samples": 491, + "sent_bytes": 710020 + }, + { + "commit_ms": 218.503, + "kind": "shared", + "max_rss_bytes": 790364160, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 948.651, + "received_bytes": 12312585, + "redis_bytes": 39278400, + "search_p50_ns": 2125, + "search_p95_ns": 6750, + "search_p99_ns": 14459, + "search_samples": 939, + "sent_bytes": 7319781 + }, + { + "commit_ms": 213.89, + "kind": "shared", + "max_rss_bytes": 790364160, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 814.086, + "received_bytes": 12312585, + "redis_bytes": 39979968, + "search_p50_ns": 2084, + "search_p95_ns": 3500, + "search_p99_ns": 9625, + "search_samples": 790, + "sent_bytes": 7296781 + }, + { + "commit_ms": 883.135, + "kind": "shared", + "max_rss_bytes": 792313856, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 1823.232, + "received_bytes": 47387299, + "redis_bytes": 67071600, + "search_p50_ns": 2041, + "search_p95_ns": 2875, + "search_p99_ns": 9916, + "search_samples": 1812, + "sent_bytes": 28641693 + }, + { + "commit_ms": 887.99, + "kind": "shared", + "max_rss_bytes": 896811008, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 1958.19, + "received_bytes": 47387373, + "redis_bytes": 67768240, + "search_p50_ns": 2209, + "search_p95_ns": 9583, + "search_p99_ns": 17583, + "search_samples": 1923, + "sent_bytes": 28401794 + }, + { + "commit_ms": 1172.482, + "kind": "shared", + "max_rss_bytes": 1081868288, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 2382.604, + "received_bytes": 53497881, + "redis_bytes": 98859504, + "search_p50_ns": 2166, + "search_p95_ns": 8917, + "search_p99_ns": 20208, + "search_samples": 2335, + "sent_bytes": 33037972 + } + ], + "startup_ms": 947.489, + "prune": { + "ms": 29.455, + "generations": 8, + "chunks": 8726, + "redis_bytes": 32908128 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788658858157550 uptime_in_seconds:80 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10273962 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 1025.244, + "kind": "shared", + "max_rss_bytes": 356630528, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 2575.381, + "received_bytes": 25924300, + "redis_bytes": 30905104, + "search_p50_ns": 625, + "search_p95_ns": 2417, + "search_p99_ns": 5708, + "search_samples": 2525, + "sent_bytes": 30357670 + }, + { + "commit_ms": 1043.142, + "kind": "shared", + "max_rss_bytes": 477839360, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 1043.229, + "received_bytes": 25743345, + "redis_bytes": 30912704, + "search_p50_ns": 3084, + "search_p95_ns": 6542, + "search_p99_ns": 15250, + "search_samples": 1036, + "sent_bytes": 685511 + }, + { + "commit_ms": 9.451, + "kind": "shared", + "max_rss_bytes": 612745216, + "n": 1000000, + "operation": "add_1", + "ready_ms": 799.598, + "received_bytes": 1410359, + "redis_bytes": 31691152, + "search_p50_ns": 3083, + "search_p95_ns": 8042, + "search_p99_ns": 24250, + "search_samples": 779, + "sent_bytes": 710040 + }, + { + "commit_ms": 10.054, + "kind": "shared", + "max_rss_bytes": 727547904, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 760.214, + "received_bytes": 1410365, + "redis_bytes": 32389488, + "search_p50_ns": 2958, + "search_p95_ns": 9375, + "search_p99_ns": 21708, + "search_samples": 710, + "sent_bytes": 710020 + }, + { + "commit_ms": 332.706, + "kind": "shared", + "max_rss_bytes": 812990464, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1222.907, + "received_bytes": 12312806, + "redis_bytes": 39301872, + "search_p50_ns": 2792, + "search_p95_ns": 6459, + "search_p99_ns": 18917, + "search_samples": 1198, + "sent_bytes": 7320084 + }, + { + "commit_ms": 313.928, + "kind": "shared", + "max_rss_bytes": 813039616, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1264.174, + "received_bytes": 12312738, + "redis_bytes": 40003408, + "search_p50_ns": 3041, + "search_p95_ns": 8833, + "search_p99_ns": 19750, + "search_samples": 1243, + "sent_bytes": 7296983 + }, + { + "commit_ms": 1428, + "kind": "shared", + "max_rss_bytes": 814563328, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2928.175, + "received_bytes": 47388099, + "redis_bytes": 67094352, + "search_p50_ns": 2959, + "search_p95_ns": 6791, + "search_p99_ns": 16375, + "search_samples": 2905, + "sent_bytes": 28642804 + }, + { + "commit_ms": 1222.56, + "kind": "shared", + "max_rss_bytes": 814628864, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 3312.882, + "received_bytes": 47387812, + "redis_bytes": 67790976, + "search_p50_ns": 4334, + "search_p95_ns": 14042, + "search_p99_ns": 23209, + "search_samples": 3207, + "sent_bytes": 28402400 + }, + { + "commit_ms": 1671.371, + "kind": "shared", + "max_rss_bytes": 1169260544, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 3021.505, + "received_bytes": 53498541, + "redis_bytes": 98901696, + "search_p50_ns": 2792, + "search_p95_ns": 4791, + "search_p99_ns": 15542, + "search_samples": 2952, + "sent_bytes": 33038895 + } + ], + "startup_ms": 1726.999, + "prune": { + "ms": 34.58, + "generations": 8, + "chunks": 8726, + "redis_bytes": 32952240 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:92942 process_supervised:no run_id:d078a837c2fd6335d36e7c6f1c2bed05facf5802 tcp_port:16389 server_time_usec:1788659007962633 uptime_in_seconds:229 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10274111 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 291.001, + "kind": "diverse", + "max_rss_bytes": 35192832, + "n": 10000, + "operation": "initial_load", + "ready_ms": 561.152, + "received_bytes": 1014588, + "redis_bytes": 4094080, + "search_p50_ns": 458, + "search_p95_ns": 1833, + "search_p99_ns": 5250, + "search_samples": 561, + "sent_bytes": 5031615 + }, + { + "commit_ms": 231.982, + "kind": "diverse", + "max_rss_bytes": 35864576, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 232.042, + "received_bytes": 834989, + "redis_bytes": 4095520, + "search_p50_ns": 1416, + "search_p95_ns": 2041, + "search_p99_ns": 5542, + "search_samples": 231, + "sent_bytes": 622208 + }, + { + "commit_ms": 5.017, + "kind": "diverse", + "max_rss_bytes": 44744704, + "n": 10000, + "operation": "add_1", + "ready_ms": 25.135, + "received_bytes": 1286299, + "redis_bytes": 5387056, + "search_p50_ns": 1833, + "search_p95_ns": 6625, + "search_p99_ns": 7375, + "search_samples": 24, + "sent_bytes": 648180 + }, + { + "commit_ms": 4.914, + "kind": "diverse", + "max_rss_bytes": 48234496, + "n": 10000, + "operation": "remove_1", + "ready_ms": 35.136, + "received_bytes": 1286299, + "redis_bytes": 6045456, + "search_p50_ns": 1916, + "search_p95_ns": 6708, + "search_p99_ns": 6709, + "search_samples": 34, + "sent_bytes": 648159 + }, + { + "commit_ms": 116.045, + "kind": "diverse", + "max_rss_bytes": 50708480, + "n": 10000, + "operation": "add_1000", + "ready_ms": 206.161, + "received_bytes": 1404102, + "redis_bytes": 7014064, + "search_p50_ns": 1417, + "search_p95_ns": 6041, + "search_p99_ns": 8291, + "search_samples": 206, + "sent_bytes": 1859157 + }, + { + "commit_ms": 118.174, + "kind": "diverse", + "max_rss_bytes": 50757632, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 198.265, + "received_bytes": 1403844, + "redis_bytes": 7670416, + "search_p50_ns": 1500, + "search_p95_ns": 5792, + "search_p99_ns": 15292, + "search_samples": 194, + "sent_bytes": 1763731 + }, + { + "commit_ms": 17.83, + "kind": "diverse", + "max_rss_bytes": 51183616, + "n": 10000, + "operation": "add_100", + "ready_ms": 58.018, + "received_bytes": 1299350, + "redis_bytes": 8366432, + "search_p50_ns": 1708, + "search_p95_ns": 9375, + "search_p99_ns": 12417, + "search_samples": 57, + "sent_bytes": 782794 + }, + { + "commit_ms": 18.379, + "kind": "diverse", + "max_rss_bytes": 51183616, + "n": 10000, + "operation": "remove_100", + "ready_ms": 58.502, + "received_bytes": 1299318, + "redis_bytes": 9022848, + "search_p50_ns": 1625, + "search_p95_ns": 8041, + "search_p99_ns": 11042, + "search_samples": 58, + "sent_bytes": 771811 + }, + { + "commit_ms": 462.081, + "kind": "diverse", + "max_rss_bytes": 55394304, + "n": 10000, + "operation": "full_replace", + "ready_ms": 692.151, + "received_bytes": 1705682, + "redis_bytes": 10565696, + "search_p50_ns": 1250, + "search_p95_ns": 2250, + "search_p99_ns": 9541, + "search_samples": 691, + "sent_bytes": 5708281 + } + ], + "startup_ms": 240.472, + "prune": { + "ms": 15.403, + "generations": 8, + "chunks": 4716, + "redis_bytes": 4024848 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788658913264132 uptime_in_seconds:135 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274017 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 262.483, + "kind": "diverse", + "max_rss_bytes": 33062912, + "n": 10000, + "operation": "initial_load", + "ready_ms": 502.62, + "received_bytes": 1014536, + "redis_bytes": 4121280, + "search_p50_ns": 375, + "search_p95_ns": 958, + "search_p99_ns": 2417, + "search_samples": 502, + "sent_bytes": 5031514 + }, + { + "commit_ms": 228.566, + "kind": "diverse", + "max_rss_bytes": 36306944, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 228.625, + "received_bytes": 835004, + "redis_bytes": 4122720, + "search_p50_ns": 1417, + "search_p95_ns": 2000, + "search_p99_ns": 5166, + "search_samples": 228, + "sent_bytes": 622208 + }, + { + "commit_ms": 5.251, + "kind": "diverse", + "max_rss_bytes": 44138496, + "n": 10000, + "operation": "add_1", + "ready_ms": 25.36, + "received_bytes": 1286314, + "redis_bytes": 5414256, + "search_p50_ns": 2167, + "search_p95_ns": 7208, + "search_p99_ns": 7209, + "search_samples": 25, + "sent_bytes": 648180 + }, + { + "commit_ms": 5.525, + "kind": "diverse", + "max_rss_bytes": 48726016, + "n": 10000, + "operation": "remove_1", + "ready_ms": 35.676, + "received_bytes": 1286309, + "redis_bytes": 6070544, + "search_p50_ns": 1792, + "search_p95_ns": 7791, + "search_p99_ns": 8667, + "search_samples": 35, + "sent_bytes": 648159 + }, + { + "commit_ms": 117.285, + "kind": "diverse", + "max_rss_bytes": 48922624, + "n": 10000, + "operation": "add_1000", + "ready_ms": 207.393, + "received_bytes": 1404122, + "redis_bytes": 7041808, + "search_p50_ns": 1458, + "search_p95_ns": 5958, + "search_p99_ns": 7625, + "search_samples": 207, + "sent_bytes": 1859157 + }, + { + "commit_ms": 115.704, + "kind": "diverse", + "max_rss_bytes": 51675136, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 195.882, + "received_bytes": 1403859, + "redis_bytes": 7718592, + "search_p50_ns": 1500, + "search_p95_ns": 6250, + "search_p99_ns": 8542, + "search_samples": 190, + "sent_bytes": 1763731 + }, + { + "commit_ms": 17.512, + "kind": "diverse", + "max_rss_bytes": 51675136, + "n": 10000, + "operation": "add_100", + "ready_ms": 37.707, + "received_bytes": 1299292, + "redis_bytes": 8415616, + "search_p50_ns": 1666, + "search_p95_ns": 9667, + "search_p99_ns": 18125, + "search_samples": 35, + "sent_bytes": 782693 + }, + { + "commit_ms": 16.644, + "kind": "diverse", + "max_rss_bytes": 56049664, + "n": 10000, + "operation": "remove_100", + "ready_ms": 56.79, + "received_bytes": 1299333, + "redis_bytes": 9706848, + "search_p50_ns": 1583, + "search_p95_ns": 6875, + "search_p99_ns": 10333, + "search_samples": 54, + "sent_bytes": 771811 + }, + { + "commit_ms": 483.259, + "kind": "diverse", + "max_rss_bytes": 56311808, + "n": 10000, + "operation": "full_replace", + "ready_ms": 733.362, + "received_bytes": 1705698, + "redis_bytes": 10614784, + "search_p50_ns": 1375, + "search_p95_ns": 2166, + "search_p99_ns": 5708, + "search_samples": 733, + "sent_bytes": 5708281 + } + ], + "startup_ms": 232.076, + "prune": { + "ms": 16.958, + "generations": 8, + "chunks": 4716, + "redis_bytes": 4070560 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788659071154377 uptime_in_seconds:293 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274175 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 268.508, + "kind": "korean", + "max_rss_bytes": 45957120, + "n": 10000, + "operation": "initial_load", + "ready_ms": 578.613, + "received_bytes": 1220747, + "redis_bytes": 4296384, + "search_p50_ns": 625, + "search_p95_ns": 3708, + "search_p99_ns": 7583, + "search_samples": 578, + "sent_bytes": 5273645 + }, + { + "commit_ms": 202.826, + "kind": "korean", + "max_rss_bytes": 46776320, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 202.881, + "received_bytes": 1041088, + "redis_bytes": 4297824, + "search_p50_ns": 1458, + "search_p95_ns": 2375, + "search_p99_ns": 5291, + "search_samples": 202, + "sent_bytes": 627520 + }, + { + "commit_ms": 4.779, + "kind": "korean", + "max_rss_bytes": 69337088, + "n": 10000, + "operation": "add_1", + "ready_ms": 34.94, + "received_bytes": 1294518, + "redis_bytes": 5589392, + "search_p50_ns": 2292, + "search_p95_ns": 6625, + "search_p99_ns": 12708, + "search_samples": 34, + "sent_bytes": 652289 + }, + { + "commit_ms": 4.865, + "kind": "korean", + "max_rss_bytes": 75235328, + "n": 10000, + "operation": "remove_1", + "ready_ms": 35.037, + "received_bytes": 1294518, + "redis_bytes": 6245680, + "search_p50_ns": 2333, + "search_p95_ns": 4583, + "search_p99_ns": 8958, + "search_samples": 31, + "sent_bytes": 652269 + }, + { + "commit_ms": 102.11, + "kind": "korean", + "max_rss_bytes": 75350016, + "n": 10000, + "operation": "add_1000", + "ready_ms": 162.187, + "received_bytes": 1499772, + "redis_bytes": 7267488, + "search_p50_ns": 1459, + "search_p95_ns": 2292, + "search_p99_ns": 9875, + "search_samples": 161, + "sent_bytes": 1906214 + }, + { + "commit_ms": 101.616, + "kind": "korean", + "max_rss_bytes": 75366400, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 181.81, + "received_bytes": 1499548, + "redis_bytes": 7923776, + "search_p50_ns": 1500, + "search_p95_ns": 5459, + "search_p99_ns": 29250, + "search_samples": 181, + "sent_bytes": 1803266 + }, + { + "commit_ms": 18.862, + "kind": "korean", + "max_rss_bytes": 79773696, + "n": 10000, + "operation": "add_100", + "ready_ms": 59.096, + "received_bytes": 1317489, + "redis_bytes": 8625056, + "search_p50_ns": 1916, + "search_p95_ns": 11250, + "search_p99_ns": 12166, + "search_samples": 59, + "sent_bytes": 791758 + }, + { + "commit_ms": 17.055, + "kind": "korean", + "max_rss_bytes": 79806464, + "n": 10000, + "operation": "remove_100", + "ready_ms": 47.169, + "received_bytes": 1317381, + "redis_bytes": 9281456, + "search_p50_ns": 2458, + "search_p95_ns": 9917, + "search_p99_ns": 12583, + "search_samples": 45, + "sent_bytes": 779593 + }, + { + "commit_ms": 526.152, + "kind": "korean", + "max_rss_bytes": 86802432, + "n": 10000, + "operation": "full_replace", + "ready_ms": 796.223, + "received_bytes": 2112542, + "redis_bytes": 11061104, + "search_p50_ns": 1667, + "search_p95_ns": 6708, + "search_p99_ns": 13625, + "search_samples": 790, + "sent_bytes": 5906529 + } + ], + "startup_ms": 265.111, + "prune": { + "ms": 16.623, + "generations": 8, + "chunks": 4748, + "redis_bytes": 4261392 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788658915828378 uptime_in_seconds:137 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274019 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 267.243, + "kind": "korean", + "max_rss_bytes": 44679168, + "n": 10000, + "operation": "initial_load", + "ready_ms": 597.333, + "received_bytes": 1220762, + "redis_bytes": 4340064, + "search_p50_ns": 791, + "search_p95_ns": 3375, + "search_p99_ns": 9667, + "search_samples": 597, + "sent_bytes": 5273645 + }, + { + "commit_ms": 295.968, + "kind": "korean", + "max_rss_bytes": 45465600, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 296.041, + "received_bytes": 1041247, + "redis_bytes": 4321024, + "search_p50_ns": 2125, + "search_p95_ns": 2916, + "search_p99_ns": 4916, + "search_samples": 295, + "sent_bytes": 627722 + }, + { + "commit_ms": 6.061, + "kind": "korean", + "max_rss_bytes": 67010560, + "n": 10000, + "operation": "add_1", + "ready_ms": 36.152, + "received_bytes": 1294533, + "redis_bytes": 5612592, + "search_p50_ns": 2541, + "search_p95_ns": 10375, + "search_p99_ns": 14084, + "search_samples": 36, + "sent_bytes": 652289 + }, + { + "commit_ms": 6.026, + "kind": "korean", + "max_rss_bytes": 69976064, + "n": 10000, + "operation": "remove_1", + "ready_ms": 36.169, + "received_bytes": 1294533, + "redis_bytes": 6268880, + "search_p50_ns": 2625, + "search_p95_ns": 9542, + "search_p99_ns": 9625, + "search_samples": 32, + "sent_bytes": 652269 + }, + { + "commit_ms": 135.809, + "kind": "korean", + "max_rss_bytes": 70008832, + "n": 10000, + "operation": "add_1000", + "ready_ms": 236.002, + "received_bytes": 1499858, + "redis_bytes": 7290624, + "search_p50_ns": 2000, + "search_p95_ns": 8083, + "search_p99_ns": 10875, + "search_samples": 235, + "sent_bytes": 1906315 + }, + { + "commit_ms": 126.112, + "kind": "korean", + "max_rss_bytes": 71335936, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 206.195, + "received_bytes": 1499568, + "redis_bytes": 7946912, + "search_p50_ns": 1917, + "search_p95_ns": 4042, + "search_p99_ns": 8250, + "search_samples": 203, + "sent_bytes": 1803266 + }, + { + "commit_ms": 18.847, + "kind": "korean", + "max_rss_bytes": 71352320, + "n": 10000, + "operation": "add_100", + "ready_ms": 69.025, + "received_bytes": 1317504, + "redis_bytes": 8648208, + "search_p50_ns": 2250, + "search_p95_ns": 8708, + "search_p99_ns": 13917, + "search_samples": 63, + "sent_bytes": 791758 + }, + { + "commit_ms": 20.228, + "kind": "korean", + "max_rss_bytes": 74301440, + "n": 10000, + "operation": "remove_100", + "ready_ms": 60.339, + "received_bytes": 1317468, + "redis_bytes": 9304624, + "search_p50_ns": 2125, + "search_p95_ns": 4125, + "search_p99_ns": 11708, + "search_samples": 59, + "sent_bytes": 779694 + }, + { + "commit_ms": 921.169, + "kind": "korean", + "max_rss_bytes": 83509248, + "n": 10000, + "operation": "full_replace", + "ready_ms": 1551.318, + "received_bytes": 2113134, + "redis_bytes": 11064736, + "search_p50_ns": 3334, + "search_p95_ns": 9917, + "search_p99_ns": 16042, + "search_samples": 1533, + "sent_bytes": 5907337 + } + ], + "startup_ms": 350.683, + "prune": { + "ms": 40.956, + "generations": 8, + "chunks": 4748, + "redis_bytes": 4265456 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788659073674683 uptime_in_seconds:295 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274177 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 275.727, + "kind": "shared", + "max_rss_bytes": 25968640, + "n": 10000, + "operation": "initial_load", + "ready_ms": 535.915, + "received_bytes": 1108228, + "redis_bytes": 4000528, + "search_p50_ns": 375, + "search_p95_ns": 1041, + "search_p99_ns": 3375, + "search_samples": 535, + "sent_bytes": 5147742 + }, + { + "commit_ms": 235.267, + "kind": "shared", + "max_rss_bytes": 29032448, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 235.341, + "received_bytes": 928580, + "redis_bytes": 4001968, + "search_p50_ns": 1375, + "search_p95_ns": 3208, + "search_p99_ns": 6833, + "search_samples": 235, + "sent_bytes": 625528 + }, + { + "commit_ms": 6.17, + "kind": "shared", + "max_rss_bytes": 30081024, + "n": 10000, + "operation": "add_1", + "ready_ms": 36.271, + "received_bytes": 1291332, + "redis_bytes": 5293584, + "search_p50_ns": 1750, + "search_p95_ns": 6292, + "search_p99_ns": 6333, + "search_samples": 33, + "sent_bytes": 650768 + }, + { + "commit_ms": 5.119, + "kind": "shared", + "max_rss_bytes": 31752192, + "n": 10000, + "operation": "remove_1", + "ready_ms": 25.204, + "received_bytes": 1291260, + "redis_bytes": 5951920, + "search_p50_ns": 1834, + "search_p95_ns": 7708, + "search_p99_ns": 8917, + "search_samples": 25, + "sent_bytes": 650647 + }, + { + "commit_ms": 117.58, + "kind": "shared", + "max_rss_bytes": 32309248, + "n": 10000, + "operation": "add_1000", + "ready_ms": 207.726, + "received_bytes": 1452863, + "redis_bytes": 6941648, + "search_p50_ns": 1375, + "search_p95_ns": 6167, + "search_p99_ns": 7458, + "search_samples": 207, + "sent_bytes": 1881483 + }, + { + "commit_ms": 115.292, + "kind": "shared", + "max_rss_bytes": 34209792, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 195.408, + "received_bytes": 1452592, + "redis_bytes": 7597936, + "search_p50_ns": 1458, + "search_p95_ns": 6042, + "search_p99_ns": 7625, + "search_samples": 194, + "sent_bytes": 1764301 + }, + { + "commit_ms": 17.63, + "kind": "shared", + "max_rss_bytes": 34291712, + "n": 10000, + "operation": "add_100", + "ready_ms": 57.802, + "received_bytes": 1309558, + "redis_bytes": 8296384, + "search_p50_ns": 1500, + "search_p95_ns": 6542, + "search_p99_ns": 7583, + "search_samples": 56, + "sent_bytes": 787895 + }, + { + "commit_ms": 18.033, + "kind": "shared", + "max_rss_bytes": 34357248, + "n": 10000, + "operation": "remove_100", + "ready_ms": 48.197, + "received_bytes": 1309527, + "redis_bytes": 8952736, + "search_p50_ns": 1583, + "search_p95_ns": 6583, + "search_p99_ns": 6708, + "search_samples": 47, + "sent_bytes": 776927 + }, + { + "commit_ms": 495.86, + "kind": "shared", + "max_rss_bytes": 35815424, + "n": 10000, + "operation": "full_replace", + "ready_ms": 756.032, + "received_bytes": 1886419, + "redis_bytes": 10582864, + "search_p50_ns": 1375, + "search_p95_ns": 1958, + "search_p99_ns": 6125, + "search_samples": 756, + "sent_bytes": 5769565 + } + ], + "startup_ms": 234.324, + "prune": { + "ms": 16.625, + "generations": 8, + "chunks": 4736, + "redis_bytes": 4088336 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788658910666062 uptime_in_seconds:132 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274014 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 297.349, + "kind": "shared", + "max_rss_bytes": 26116096, + "n": 10000, + "operation": "initial_load", + "ready_ms": 577.471, + "received_bytes": 1108275, + "redis_bytes": 4200224, + "search_p50_ns": 417, + "search_p95_ns": 1250, + "search_p99_ns": 2542, + "search_samples": 577, + "sent_bytes": 5147742 + }, + { + "commit_ms": 239.665, + "kind": "shared", + "max_rss_bytes": 28393472, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 239.726, + "received_bytes": 928677, + "redis_bytes": 4201664, + "search_p50_ns": 1416, + "search_p95_ns": 2000, + "search_p99_ns": 2958, + "search_samples": 239, + "sent_bytes": 625629 + }, + { + "commit_ms": 5.921, + "kind": "shared", + "max_rss_bytes": 31408128, + "n": 10000, + "operation": "add_1", + "ready_ms": 36.162, + "received_bytes": 1291284, + "redis_bytes": 5495328, + "search_p50_ns": 1875, + "search_p95_ns": 7708, + "search_p99_ns": 8291, + "search_samples": 32, + "sent_bytes": 650667 + }, + { + "commit_ms": 5.573, + "kind": "shared", + "max_rss_bytes": 33144832, + "n": 10000, + "operation": "remove_1", + "ready_ms": 25.708, + "received_bytes": 1291288, + "redis_bytes": 6151680, + "search_p50_ns": 1708, + "search_p95_ns": 6209, + "search_p99_ns": 6209, + "search_samples": 20, + "sent_bytes": 650647 + }, + { + "commit_ms": 117.211, + "kind": "shared", + "max_rss_bytes": 33341440, + "n": 10000, + "operation": "add_1000", + "ready_ms": 197.303, + "received_bytes": 1452885, + "redis_bytes": 7140512, + "search_p50_ns": 1375, + "search_p95_ns": 5917, + "search_p99_ns": 12125, + "search_samples": 195, + "sent_bytes": 1881483 + }, + { + "commit_ms": 115.566, + "kind": "shared", + "max_rss_bytes": 33390592, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 195.709, + "received_bytes": 1452614, + "redis_bytes": 7817344, + "search_p50_ns": 1458, + "search_p95_ns": 5375, + "search_p99_ns": 8084, + "search_samples": 193, + "sent_bytes": 1764301 + }, + { + "commit_ms": 17.776, + "kind": "shared", + "max_rss_bytes": 34242560, + "n": 10000, + "operation": "add_100", + "ready_ms": 57.972, + "received_bytes": 1309580, + "redis_bytes": 8515856, + "search_p50_ns": 1500, + "search_p95_ns": 6625, + "search_p99_ns": 6958, + "search_samples": 54, + "sent_bytes": 787895 + }, + { + "commit_ms": 17.978, + "kind": "shared", + "max_rss_bytes": 34471936, + "n": 10000, + "operation": "remove_100", + "ready_ms": 48.152, + "received_bytes": 1309476, + "redis_bytes": 9173248, + "search_p50_ns": 1625, + "search_p95_ns": 7834, + "search_p99_ns": 7958, + "search_samples": 41, + "sent_bytes": 776826 + }, + { + "commit_ms": 494.689, + "kind": "shared", + "max_rss_bytes": 37797888, + "n": 10000, + "operation": "full_replace", + "ready_ms": 754.872, + "received_bytes": 1886438, + "redis_bytes": 10804608, + "search_p50_ns": 1417, + "search_p95_ns": 2625, + "search_p99_ns": 8500, + "search_samples": 752, + "sent_bytes": 5769565 + } + ], + "startup_ms": 239.889, + "prune": { + "ms": 16.992, + "generations": 8, + "chunks": 4736, + "redis_bytes": 4165760 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788659068508214 uptime_in_seconds:290 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274172 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 326.208, + "kind": "diverse", + "max_rss_bytes": 124698624, + "n": 100000, + "operation": "initial_load", + "ready_ms": 676.303, + "received_bytes": 2514760, + "redis_bytes": 5787936, + "search_p50_ns": 375, + "search_p95_ns": 1125, + "search_p99_ns": 5417, + "search_samples": 675, + "sent_bytes": 6948061 + }, + { + "commit_ms": 295.34, + "kind": "diverse", + "max_rss_bytes": 126402560, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 295.407, + "received_bytes": 2333660, + "redis_bytes": 5786304, + "search_p50_ns": 1666, + "search_p95_ns": 2125, + "search_p99_ns": 6791, + "search_samples": 282, + "sent_bytes": 683895 + }, + { + "commit_ms": 5.053, + "kind": "diverse", + "max_rss_bytes": 191905792, + "n": 100000, + "operation": "add_1", + "ready_ms": 95.168, + "received_bytes": 1390241, + "redis_bytes": 7178640, + "search_p50_ns": 2167, + "search_p95_ns": 8000, + "search_p99_ns": 22666, + "search_samples": 89, + "sent_bytes": 700215 + }, + { + "commit_ms": 5.165, + "kind": "diverse", + "max_rss_bytes": 212615168, + "n": 100000, + "operation": "remove_1", + "ready_ms": 85.269, + "received_bytes": 1390176, + "redis_bytes": 7884080, + "search_p50_ns": 2083, + "search_p95_ns": 8708, + "search_p99_ns": 13584, + "search_samples": 75, + "sent_bytes": 700094 + }, + { + "commit_ms": 131.109, + "kind": "diverse", + "max_rss_bytes": 230408192, + "n": 100000, + "operation": "add_1000", + "ready_ms": 281.196, + "received_bytes": 2123085, + "redis_bytes": 8595264, + "search_p50_ns": 1708, + "search_p95_ns": 6416, + "search_p99_ns": 8125, + "search_samples": 275, + "sent_bytes": 2225130 + }, + { + "commit_ms": 128.65, + "kind": "diverse", + "max_rss_bytes": 252608512, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 268.796, + "received_bytes": 2123011, + "redis_bytes": 9300704, + "search_p50_ns": 1750, + "search_p95_ns": 6375, + "search_p99_ns": 17750, + "search_samples": 267, + "sent_bytes": 2202029 + }, + { + "commit_ms": 129.049, + "kind": "diverse", + "max_rss_bytes": 252608512, + "n": 100000, + "operation": "add_1000", + "ready_ms": 279.169, + "received_bytes": 2123077, + "redis_bytes": 10690192, + "search_p50_ns": 1709, + "search_p95_ns": 6125, + "search_p99_ns": 11708, + "search_samples": 270, + "sent_bytes": 2225130 + }, + { + "commit_ms": 127.841, + "kind": "diverse", + "max_rss_bytes": 252624896, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 268.019, + "received_bytes": 2123017, + "redis_bytes": 11416176, + "search_p50_ns": 1792, + "search_p95_ns": 7083, + "search_p99_ns": 18625, + "search_samples": 261, + "sent_bytes": 2202029 + }, + { + "commit_ms": 598.747, + "kind": "diverse", + "max_rss_bytes": 261816320, + "n": 100000, + "operation": "full_replace", + "ready_ms": 958.864, + "received_bytes": 4880301, + "redis_bytes": 14843376, + "search_p50_ns": 1667, + "search_p95_ns": 2625, + "search_p99_ns": 7209, + "search_samples": 957, + "sent_bytes": 7828565 + } + ], + "startup_ms": 328.331, + "prune": { + "ms": 17.833, + "generations": 8, + "chunks": 4987, + "redis_bytes": 5975568 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788658922071848 uptime_in_seconds:144 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274026 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 478.071, + "kind": "diverse", + "max_rss_bytes": 132677632, + "n": 100000, + "operation": "initial_load", + "ready_ms": 998.178, + "received_bytes": 2514983, + "redis_bytes": 5810688, + "search_p50_ns": 459, + "search_p95_ns": 2250, + "search_p99_ns": 6209, + "search_samples": 998, + "sent_bytes": 6948364 + }, + { + "commit_ms": 426.113, + "kind": "diverse", + "max_rss_bytes": 134103040, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 426.205, + "received_bytes": 2333956, + "redis_bytes": 5809056, + "search_p50_ns": 2083, + "search_p95_ns": 3208, + "search_p99_ns": 11791, + "search_samples": 426, + "sent_bytes": 684299 + }, + { + "commit_ms": 7.419, + "kind": "diverse", + "max_rss_bytes": 191905792, + "n": 100000, + "operation": "add_1", + "ready_ms": 107.559, + "received_bytes": 1390175, + "redis_bytes": 7201408, + "search_p50_ns": 2042, + "search_p95_ns": 7750, + "search_p99_ns": 121625, + "search_samples": 97, + "sent_bytes": 700114 + }, + { + "commit_ms": 7.092, + "kind": "diverse", + "max_rss_bytes": 212647936, + "n": 100000, + "operation": "remove_1", + "ready_ms": 97.201, + "received_bytes": 1390182, + "redis_bytes": 7906928, + "search_p50_ns": 2167, + "search_p95_ns": 5750, + "search_p99_ns": 13500, + "search_samples": 87, + "sent_bytes": 700094 + }, + { + "commit_ms": 189.427, + "kind": "diverse", + "max_rss_bytes": 230424576, + "n": 100000, + "operation": "add_1000", + "ready_ms": 369.603, + "received_bytes": 2123163, + "redis_bytes": 9322016, + "search_p50_ns": 2083, + "search_p95_ns": 5000, + "search_p99_ns": 10541, + "search_samples": 360, + "sent_bytes": 2225231 + }, + { + "commit_ms": 183.576, + "kind": "diverse", + "max_rss_bytes": 230457344, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 363.743, + "received_bytes": 2123092, + "redis_bytes": 9344464, + "search_p50_ns": 2167, + "search_p95_ns": 7708, + "search_p99_ns": 18958, + "search_samples": 354, + "sent_bytes": 2202130 + }, + { + "commit_ms": 181.269, + "kind": "diverse", + "max_rss_bytes": 230473728, + "n": 100000, + "operation": "add_1000", + "ready_ms": 351.403, + "received_bytes": 2123091, + "redis_bytes": 10051952, + "search_p50_ns": 2084, + "search_p95_ns": 3667, + "search_p99_ns": 13541, + "search_samples": 343, + "sent_bytes": 2225130 + }, + { + "commit_ms": 184.152, + "kind": "diverse", + "max_rss_bytes": 230490112, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 354.273, + "received_bytes": 2123093, + "redis_bytes": 10757472, + "search_p50_ns": 2166, + "search_p95_ns": 4125, + "search_p99_ns": 15625, + "search_samples": 347, + "sent_bytes": 2202130 + }, + { + "commit_ms": 857.491, + "kind": "diverse", + "max_rss_bytes": 262029312, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1347.664, + "received_bytes": 4880666, + "redis_bytes": 14869872, + "search_p50_ns": 2083, + "search_p95_ns": 3583, + "search_p99_ns": 13875, + "search_samples": 1342, + "sent_bytes": 7829070 + } + ], + "startup_ms": 481.457, + "prune": { + "ms": 24.085, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6002768 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788659083047245 uptime_in_seconds:305 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274187 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 348.734, + "kind": "korean", + "max_rss_bytes": 223215616, + "n": 100000, + "operation": "initial_load", + "ready_ms": 738.905, + "received_bytes": 4515793, + "redis_bytes": 7971760, + "search_p50_ns": 625, + "search_p95_ns": 1625, + "search_p99_ns": 6541, + "search_samples": 736, + "sent_bytes": 8949098 + }, + { + "commit_ms": 321.823, + "kind": "korean", + "max_rss_bytes": 224952320, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 321.888, + "received_bytes": 4334770, + "redis_bytes": 7956304, + "search_p50_ns": 2000, + "search_p95_ns": 2625, + "search_p99_ns": 4708, + "search_samples": 305, + "sent_bytes": 683996 + }, + { + "commit_ms": 5.166, + "kind": "korean", + "max_rss_bytes": 334626816, + "n": 100000, + "operation": "add_1", + "ready_ms": 115.266, + "received_bytes": 1391361, + "redis_bytes": 9347408, + "search_p50_ns": 2625, + "search_p95_ns": 11959, + "search_p99_ns": 18500, + "search_samples": 112, + "sent_bytes": 700709 + }, + { + "commit_ms": 5.231, + "kind": "korean", + "max_rss_bytes": 334675968, + "n": 100000, + "operation": "remove_1", + "ready_ms": 105.345, + "received_bytes": 1391369, + "redis_bytes": 10052864, + "search_p50_ns": 2583, + "search_p95_ns": 9708, + "search_p99_ns": 24709, + "search_samples": 92, + "sent_bytes": 700689 + }, + { + "commit_ms": 139.394, + "kind": "korean", + "max_rss_bytes": 334708736, + "n": 100000, + "operation": "add_1000", + "ready_ms": 299.507, + "received_bytes": 2997863, + "redis_bytes": 11932144, + "search_p50_ns": 2125, + "search_p95_ns": 5375, + "search_p99_ns": 19667, + "search_samples": 296, + "sent_bytes": 2662455 + }, + { + "commit_ms": 132.867, + "kind": "korean", + "max_rss_bytes": 350355456, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 293.084, + "received_bytes": 2997864, + "redis_bytes": 12637584, + "search_p50_ns": 2167, + "search_p95_ns": 5167, + "search_p99_ns": 18667, + "search_samples": 289, + "sent_bytes": 2639448 + }, + { + "commit_ms": 132.851, + "kind": "korean", + "max_rss_bytes": 393805824, + "n": 100000, + "operation": "add_1000", + "ready_ms": 303.06, + "received_bytes": 2997865, + "redis_bytes": 13343024, + "search_p50_ns": 2208, + "search_p95_ns": 8292, + "search_p99_ns": 11375, + "search_samples": 299, + "sent_bytes": 2662455 + }, + { + "commit_ms": 132.815, + "kind": "korean", + "max_rss_bytes": 393854976, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 293.036, + "received_bytes": 2997864, + "redis_bytes": 14048576, + "search_p50_ns": 2125, + "search_p95_ns": 7709, + "search_p99_ns": 10167, + "search_samples": 279, + "sent_bytes": 2639448 + }, + { + "commit_ms": 625.729, + "kind": "korean", + "max_rss_bytes": 404865024, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1035.89, + "received_bytes": 8882700, + "redis_bytes": 19681696, + "search_p50_ns": 2041, + "search_p95_ns": 4792, + "search_p99_ns": 10292, + "search_samples": 1011, + "sent_bytes": 9829925 + } + ], + "startup_ms": 370.41, + "prune": { + "ms": 17.215, + "generations": 8, + "chunks": 4987, + "redis_bytes": 8162608 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788658925932933 uptime_in_seconds:147 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274029 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 498.574, + "kind": "korean", + "max_rss_bytes": 211271680, + "n": 100000, + "operation": "initial_load", + "ready_ms": 1078.747, + "received_bytes": 4516017, + "redis_bytes": 7993968, + "search_p50_ns": 833, + "search_p95_ns": 3291, + "search_p99_ns": 9167, + "search_samples": 1072, + "sent_bytes": 8949401 + }, + { + "commit_ms": 1421.617, + "kind": "korean", + "max_rss_bytes": 231374848, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 1421.787, + "received_bytes": 4336432, + "redis_bytes": 7979088, + "search_p50_ns": 7333, + "search_p95_ns": 21250, + "search_p99_ns": 37917, + "search_samples": 1180, + "sent_bytes": 686319 + }, + { + "commit_ms": 13.87, + "kind": "korean", + "max_rss_bytes": 365035520, + "n": 100000, + "operation": "add_1", + "ready_ms": 261.192, + "received_bytes": 1391366, + "redis_bytes": 8686112, + "search_p50_ns": 5833, + "search_p95_ns": 22709, + "search_p99_ns": 49500, + "search_samples": 222, + "sent_bytes": 700709 + }, + { + "commit_ms": 17.374, + "kind": "korean", + "max_rss_bytes": 370163712, + "n": 100000, + "operation": "remove_1", + "ready_ms": 247.612, + "received_bytes": 1391444, + "redis_bytes": 9391616, + "search_p50_ns": 5084, + "search_p95_ns": 22750, + "search_p99_ns": 27291, + "search_samples": 214, + "sent_bytes": 700790 + }, + { + "commit_ms": 322.61, + "kind": "korean", + "max_rss_bytes": 370262016, + "n": 100000, + "operation": "add_1000", + "ready_ms": 702.84, + "received_bytes": 2998153, + "redis_bytes": 11293344, + "search_p50_ns": 4000, + "search_p95_ns": 16542, + "search_p99_ns": 22833, + "search_samples": 612, + "sent_bytes": 2662859 + }, + { + "commit_ms": 258.658, + "kind": "korean", + "max_rss_bytes": 370294784, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 518.853, + "received_bytes": 2998084, + "redis_bytes": 12000832, + "search_p50_ns": 3542, + "search_p95_ns": 15917, + "search_p99_ns": 31167, + "search_samples": 499, + "sent_bytes": 2639751 + }, + { + "commit_ms": 216.22, + "kind": "korean", + "max_rss_bytes": 396771328, + "n": 100000, + "operation": "add_1000", + "ready_ms": 466.374, + "received_bytes": 2998085, + "redis_bytes": 12706288, + "search_p50_ns": 3083, + "search_p95_ns": 7500, + "search_p99_ns": 16042, + "search_samples": 447, + "sent_bytes": 2662758 + }, + { + "commit_ms": 192.274, + "kind": "korean", + "max_rss_bytes": 396771328, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 392.407, + "received_bytes": 2997941, + "redis_bytes": 13407248, + "search_p50_ns": 2792, + "search_p95_ns": 4833, + "search_p99_ns": 12708, + "search_samples": 382, + "sent_bytes": 2639549 + }, + { + "commit_ms": 683.732, + "kind": "korean", + "max_rss_bytes": 488308736, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1093.913, + "received_bytes": 8882777, + "redis_bytes": 19724096, + "search_p50_ns": 2084, + "search_p95_ns": 3042, + "search_p99_ns": 7208, + "search_samples": 1076, + "sent_bytes": 9830026 + } + ], + "startup_ms": 555.572, + "prune": { + "ms": 18.442, + "generations": 8, + "chunks": 4987, + "redis_bytes": 8206448 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788659088429745 uptime_in_seconds:310 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274192 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 315.537, + "kind": "shared", + "max_rss_bytes": 59654144, + "n": 100000, + "operation": "initial_load", + "ready_ms": 635.681, + "received_bytes": 3414762, + "redis_bytes": 6761552, + "search_p50_ns": 375, + "search_p95_ns": 1166, + "search_p99_ns": 3708, + "search_samples": 635, + "sent_bytes": 7848071 + }, + { + "commit_ms": 294.754, + "kind": "shared", + "max_rss_bytes": 73515008, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 294.818, + "received_bytes": 3233668, + "redis_bytes": 6762480, + "search_p50_ns": 1667, + "search_p95_ns": 2542, + "search_p99_ns": 7000, + "search_samples": 294, + "sent_bytes": 683895 + }, + { + "commit_ms": 5.158, + "kind": "shared", + "max_rss_bytes": 88424448, + "n": 100000, + "operation": "add_1", + "ready_ms": 55.256, + "received_bytes": 1390493, + "redis_bytes": 8155472, + "search_p50_ns": 1875, + "search_p95_ns": 8417, + "search_p99_ns": 8542, + "search_samples": 49, + "sent_bytes": 700342 + }, + { + "commit_ms": 5.584, + "kind": "shared", + "max_rss_bytes": 88637440, + "n": 100000, + "operation": "remove_1", + "ready_ms": 55.725, + "received_bytes": 1390430, + "redis_bytes": 8860912, + "search_p50_ns": 2042, + "search_p95_ns": 8416, + "search_p99_ns": 18208, + "search_samples": 46, + "sent_bytes": 700221 + }, + { + "commit_ms": 132.681, + "kind": "shared", + "max_rss_bytes": 93192192, + "n": 100000, + "operation": "add_1000", + "ready_ms": 232.851, + "received_bytes": 2517037, + "redis_bytes": 10476832, + "search_p50_ns": 1708, + "search_p95_ns": 5583, + "search_p99_ns": 8125, + "search_samples": 232, + "sent_bytes": 2422042 + }, + { + "commit_ms": 131.196, + "kind": "shared", + "max_rss_bytes": 97812480, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 241.304, + "received_bytes": 2517040, + "redis_bytes": 11182384, + "search_p50_ns": 1833, + "search_p95_ns": 6250, + "search_p99_ns": 8542, + "search_samples": 240, + "sent_bytes": 2399041 + }, + { + "commit_ms": 130.524, + "kind": "shared", + "max_rss_bytes": 103481344, + "n": 100000, + "operation": "add_1000", + "ready_ms": 240.673, + "received_bytes": 2517041, + "redis_bytes": 11908304, + "search_p50_ns": 1708, + "search_p95_ns": 6750, + "search_p99_ns": 8292, + "search_samples": 235, + "sent_bytes": 2422042 + }, + { + "commit_ms": 131.102, + "kind": "shared", + "max_rss_bytes": 103694336, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 241.206, + "received_bytes": 2517113, + "redis_bytes": 12614880, + "search_p50_ns": 1709, + "search_p95_ns": 7959, + "search_p99_ns": 9917, + "search_samples": 237, + "sent_bytes": 2399142 + }, + { + "commit_ms": 593.876, + "kind": "shared", + "max_rss_bytes": 131563520, + "n": 100000, + "operation": "full_replace", + "ready_ms": 924.021, + "received_bytes": 6680363, + "redis_bytes": 17026352, + "search_p50_ns": 1709, + "search_p95_ns": 2458, + "search_p99_ns": 7750, + "search_samples": 923, + "sent_bytes": 8728617 + } + ], + "startup_ms": 298.815, + "prune": { + "ms": 18.793, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6961504 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788658918526026 uptime_in_seconds:140 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274022 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 600.558, + "kind": "shared", + "max_rss_bytes": 57982976, + "n": 100000, + "operation": "initial_load", + "ready_ms": 1160.754, + "received_bytes": 3415137, + "redis_bytes": 6808608, + "search_p50_ns": 583, + "search_p95_ns": 2666, + "search_p99_ns": 6542, + "search_samples": 1156, + "sent_bytes": 7848576 + }, + { + "commit_ms": 463.914, + "kind": "shared", + "max_rss_bytes": 71909376, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 464.013, + "received_bytes": 3233965, + "redis_bytes": 6787008, + "search_p50_ns": 2333, + "search_p95_ns": 7792, + "search_p99_ns": 19792, + "search_samples": 451, + "sent_bytes": 684299 + }, + { + "commit_ms": 9.226, + "kind": "shared", + "max_rss_bytes": 87965696, + "n": 100000, + "operation": "add_1", + "ready_ms": 79.367, + "received_bytes": 1390429, + "redis_bytes": 8200016, + "search_p50_ns": 2708, + "search_p95_ns": 10791, + "search_p99_ns": 12208, + "search_samples": 69, + "sent_bytes": 700241 + }, + { + "commit_ms": 8.344, + "kind": "shared", + "max_rss_bytes": 95731712, + "n": 100000, + "operation": "remove_1", + "ready_ms": 68.505, + "received_bytes": 1390436, + "redis_bytes": 8905456, + "search_p50_ns": 2625, + "search_p95_ns": 5042, + "search_p99_ns": 16709, + "search_samples": 68, + "sent_bytes": 700221 + }, + { + "commit_ms": 200.38, + "kind": "shared", + "max_rss_bytes": 96157696, + "n": 100000, + "operation": "add_1000", + "ready_ms": 360.506, + "received_bytes": 2517188, + "redis_bytes": 10521520, + "search_p50_ns": 2208, + "search_p95_ns": 8750, + "search_p99_ns": 17458, + "search_samples": 355, + "sent_bytes": 2422244 + }, + { + "commit_ms": 198.457, + "kind": "shared", + "max_rss_bytes": 103464960, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 338.64, + "received_bytes": 2517120, + "redis_bytes": 10545488, + "search_p50_ns": 2291, + "search_p95_ns": 5500, + "search_p99_ns": 14500, + "search_samples": 331, + "sent_bytes": 2399142 + }, + { + "commit_ms": 186.966, + "kind": "shared", + "max_rss_bytes": 103694336, + "n": 100000, + "operation": "add_1000", + "ready_ms": 347.152, + "received_bytes": 2517191, + "redis_bytes": 11251008, + "search_p50_ns": 2250, + "search_p95_ns": 5333, + "search_p99_ns": 15125, + "search_samples": 330, + "sent_bytes": 2422244 + }, + { + "commit_ms": 181.299, + "kind": "shared", + "max_rss_bytes": 103727104, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 331.446, + "received_bytes": 2517191, + "redis_bytes": 12640544, + "search_p50_ns": 2125, + "search_p95_ns": 7375, + "search_p99_ns": 17750, + "search_samples": 331, + "sent_bytes": 2399243 + }, + { + "commit_ms": 878.832, + "kind": "shared", + "max_rss_bytes": 142082048, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1349.058, + "received_bytes": 6680730, + "redis_bytes": 17049072, + "search_p50_ns": 2167, + "search_p95_ns": 4333, + "search_p99_ns": 13959, + "search_samples": 1347, + "sent_bytes": 8729122 + } + ], + "startup_ms": 485.416, + "prune": { + "ms": 25.31, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6985616 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788659077545962 uptime_in_seconds:299 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274181 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 861.48, + "kind": "diverse", + "max_rss_bytes": 971292672, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 2191.677, + "received_bytes": 16923755, + "redis_bytes": 21565328, + "search_p50_ns": 459, + "search_p95_ns": 2333, + "search_p99_ns": 6459, + "search_samples": 2173, + "sent_bytes": 21357367 + }, + { + "commit_ms": 753.774, + "kind": "diverse", + "max_rss_bytes": 1204928512, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 753.835, + "received_bytes": 16742585, + "redis_bytes": 21565744, + "search_p50_ns": 1917, + "search_p95_ns": 7875, + "search_p99_ns": 16958, + "search_samples": 733, + "sent_bytes": 684905 + }, + { + "commit_ms": 5.188, + "kind": "diverse", + "max_rss_bytes": 1611399168, + "n": 1000000, + "operation": "add_1", + "ready_ms": 915.294, + "received_bytes": 1404905, + "redis_bytes": 22300160, + "search_p50_ns": 2042, + "search_p95_ns": 6250, + "search_p99_ns": 14084, + "search_samples": 863, + "sent_bytes": 707477 + }, + { + "commit_ms": 5.987, + "kind": "diverse", + "max_rss_bytes": 1799258112, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1116.146, + "received_bytes": 1404912, + "redis_bytes": 23005616, + "search_p50_ns": 2167, + "search_p95_ns": 12416, + "search_p99_ns": 20833, + "search_samples": 1043, + "sent_bytes": 707457 + }, + { + "commit_ms": 207.876, + "kind": "diverse", + "max_rss_bytes": 2064400384, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1288.068, + "received_bytes": 8389835, + "redis_bytes": 27824880, + "search_p50_ns": 1792, + "search_p95_ns": 5916, + "search_p99_ns": 20125, + "search_samples": 1244, + "sent_bytes": 5358571 + }, + { + "commit_ms": 212.35, + "kind": "diverse", + "max_rss_bytes": 2121990144, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1382.495, + "received_bytes": 8389841, + "redis_bytes": 28530384, + "search_p50_ns": 2209, + "search_p95_ns": 8209, + "search_p99_ns": 18875, + "search_samples": 1354, + "sent_bytes": 5335571 + }, + { + "commit_ms": 806.628, + "kind": "diverse", + "max_rss_bytes": 2213920768, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2136.786, + "received_bytes": 30940938, + "redis_bytes": 46936896, + "search_p50_ns": 1917, + "search_p95_ns": 3500, + "search_p99_ns": 10417, + "search_samples": 2091, + "sent_bytes": 20418545 + }, + { + "commit_ms": 757.343, + "kind": "diverse", + "max_rss_bytes": 2221867008, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2017.45, + "received_bytes": 30940938, + "redis_bytes": 47642480, + "search_p50_ns": 2000, + "search_p95_ns": 2834, + "search_p99_ns": 8708, + "search_samples": 1971, + "sent_bytes": 20178545 + }, + { + "commit_ms": 1100.067, + "kind": "diverse", + "max_rss_bytes": 2352578560, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 2770.181, + "received_bytes": 35497411, + "redis_bytes": 70076768, + "search_p50_ns": 2000, + "search_p95_ns": 4166, + "search_p99_ns": 12958, + "search_samples": 2680, + "sent_bytes": 24037770 + } + ], + "startup_ms": 1278.257, + "prune": { + "ms": 33.221, + "generations": 8, + "chunks": 8726, + "redis_bytes": 24385856 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788658942600050 uptime_in_seconds:164 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274046 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 1197.087, + "kind": "diverse", + "max_rss_bytes": 942292992, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 3697.418, + "received_bytes": 16924261, + "redis_bytes": 21591840, + "search_p50_ns": 625, + "search_p95_ns": 2875, + "search_p99_ns": 7042, + "search_samples": 3456, + "sent_bytes": 21358074 + }, + { + "commit_ms": 802.625, + "kind": "diverse", + "max_rss_bytes": 997523456, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 802.703, + "received_bytes": 16742660, + "redis_bytes": 21593280, + "search_p50_ns": 2000, + "search_p95_ns": 2958, + "search_p99_ns": 6833, + "search_samples": 786, + "sent_bytes": 685006 + }, + { + "commit_ms": 6.254, + "kind": "diverse", + "max_rss_bytes": 1550237696, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1266.367, + "received_bytes": 1404908, + "redis_bytes": 22323648, + "search_p50_ns": 2209, + "search_p95_ns": 4250, + "search_p99_ns": 14750, + "search_samples": 1223, + "sent_bytes": 707477 + }, + { + "commit_ms": 5.916, + "kind": "diverse", + "max_rss_bytes": 1769521152, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 926.129, + "received_bytes": 1404914, + "redis_bytes": 23004512, + "search_p50_ns": 2000, + "search_p95_ns": 4042, + "search_p99_ns": 12375, + "search_samples": 893, + "sent_bytes": 707457 + }, + { + "commit_ms": 189.506, + "kind": "diverse", + "max_rss_bytes": 1970487296, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1209.635, + "received_bytes": 8389772, + "redis_bytes": 27855024, + "search_p50_ns": 1917, + "search_p95_ns": 3542, + "search_p99_ns": 8917, + "search_samples": 1158, + "sent_bytes": 5358470 + }, + { + "commit_ms": 193.153, + "kind": "diverse", + "max_rss_bytes": 2134245376, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1193.228, + "received_bytes": 8389850, + "redis_bytes": 28556368, + "search_p50_ns": 2000, + "search_p95_ns": 3750, + "search_p99_ns": 9292, + "search_samples": 1159, + "sent_bytes": 5335571 + }, + { + "commit_ms": 791.145, + "kind": "diverse", + "max_rss_bytes": 2134245376, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2191.232, + "received_bytes": 30941014, + "redis_bytes": 46958768, + "search_p50_ns": 1917, + "search_p95_ns": 3125, + "search_p99_ns": 9209, + "search_samples": 2139, + "sent_bytes": 20418646 + }, + { + "commit_ms": 801.492, + "kind": "diverse", + "max_rss_bytes": 2134245376, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2441.613, + "received_bytes": 30941020, + "redis_bytes": 47657168, + "search_p50_ns": 2167, + "search_p95_ns": 3708, + "search_p99_ns": 10208, + "search_samples": 2387, + "sent_bytes": 20178646 + }, + { + "commit_ms": 1297.731, + "kind": "diverse", + "max_rss_bytes": 2164850688, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 3607.939, + "received_bytes": 35497705, + "redis_bytes": 70089472, + "search_p50_ns": 2208, + "search_p95_ns": 7000, + "search_p99_ns": 24083, + "search_samples": 3501, + "sent_bytes": 24038174 + } + ], + "startup_ms": 1702.949, + "prune": { + "ms": 50.631, + "generations": 8, + "chunks": 8726, + "redis_bytes": 24401392 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788659110170714 uptime_in_seconds:332 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274214 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 1159.456, + "kind": "korean", + "max_rss_bytes": 1095090176, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 3199.629, + "received_bytes": 36924248, + "redis_bytes": 44881920, + "search_p50_ns": 833, + "search_p95_ns": 3083, + "search_p99_ns": 8834, + "search_samples": 3163, + "sent_bytes": 41358040 + }, + { + "commit_ms": 1087.769, + "kind": "korean", + "max_rss_bytes": 1212006400, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 1087.833, + "received_bytes": 36743157, + "redis_bytes": 44897696, + "search_p50_ns": 2458, + "search_p95_ns": 3708, + "search_p99_ns": 11166, + "search_samples": 1064, + "sent_bytes": 685612 + }, + { + "commit_ms": 5.923, + "kind": "korean", + "max_rss_bytes": 1946435584, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1206.13, + "received_bytes": 1414546, + "redis_bytes": 45598320, + "search_p50_ns": 2458, + "search_p95_ns": 5500, + "search_p99_ns": 14083, + "search_samples": 1151, + "sent_bytes": 712297 + }, + { + "commit_ms": 5.844, + "kind": "korean", + "max_rss_bytes": 2228387840, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1386.111, + "received_bytes": 1414552, + "redis_bytes": 46304848, + "search_p50_ns": 2792, + "search_p95_ns": 12125, + "search_p99_ns": 29834, + "search_samples": 1320, + "sent_bytes": 712277 + }, + { + "commit_ms": 258.629, + "kind": "korean", + "max_rss_bytes": 2630696960, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1598.804, + "received_bytes": 17126305, + "redis_bytes": 56216560, + "search_p50_ns": 2417, + "search_p95_ns": 4541, + "search_p99_ns": 10875, + "search_samples": 1533, + "sent_bytes": 9726870 + }, + { + "commit_ms": 241.107, + "kind": "korean", + "max_rss_bytes": 2630696960, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1361.212, + "received_bytes": 17126314, + "redis_bytes": 56922112, + "search_p50_ns": 2417, + "search_p95_ns": 4292, + "search_p99_ns": 9417, + "search_samples": 1326, + "sent_bytes": 9703870 + }, + { + "commit_ms": 981.123, + "kind": "korean", + "max_rss_bytes": 2633367552, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2701.219, + "received_bytes": 67459654, + "redis_bytes": 96436752, + "search_p50_ns": 2334, + "search_p95_ns": 3875, + "search_p99_ns": 11000, + "search_samples": 2666, + "sent_bytes": 38678236 + }, + { + "commit_ms": 961.333, + "kind": "korean", + "max_rss_bytes": 2681012224, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2721.428, + "received_bytes": 67459584, + "redis_bytes": 97159680, + "search_p50_ns": 2416, + "search_p95_ns": 3875, + "search_p99_ns": 10917, + "search_samples": 2672, + "sent_bytes": 38438114 + }, + { + "commit_ms": 1466.054, + "kind": "korean", + "max_rss_bytes": 2683748352, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 4166.147, + "received_bytes": 75498388, + "redis_bytes": 141385568, + "search_p50_ns": 3250, + "search_p95_ns": 14417, + "search_p99_ns": 25125, + "search_samples": 4077, + "sent_bytes": 44038855 + } + ], + "startup_ms": 2389.762, + "prune": { + "ms": 58.645, + "generations": 8, + "chunks": 8726, + "redis_bytes": 46181040 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788658959148541 uptime_in_seconds:181 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274063 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 1097.191, + "kind": "korean", + "max_rss_bytes": 1111441408, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 3197.306, + "received_bytes": 36924184, + "redis_bytes": 44908624, + "search_p50_ns": 833, + "search_p95_ns": 3250, + "search_p99_ns": 7667, + "search_samples": 3146, + "sent_bytes": 41357939 + }, + { + "commit_ms": 1306.658, + "kind": "korean", + "max_rss_bytes": 1512521728, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 1306.759, + "received_bytes": 36743453, + "redis_bytes": 44924400, + "search_p50_ns": 3750, + "search_p95_ns": 12959, + "search_p99_ns": 19375, + "search_samples": 1289, + "sent_bytes": 686016 + }, + { + "commit_ms": 8.928, + "kind": "korean", + "max_rss_bytes": 2079014912, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1599.191, + "received_bytes": 1414554, + "redis_bytes": 45625040, + "search_p50_ns": 3000, + "search_p95_ns": 5500, + "search_p99_ns": 12958, + "search_samples": 1557, + "sent_bytes": 712297 + }, + { + "commit_ms": 6.147, + "kind": "korean", + "max_rss_bytes": 2323234816, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1196.331, + "received_bytes": 1414558, + "redis_bytes": 46331504, + "search_p50_ns": 2500, + "search_p95_ns": 6125, + "search_p99_ns": 17417, + "search_samples": 1124, + "sent_bytes": 712277 + }, + { + "commit_ms": 249.758, + "kind": "korean", + "max_rss_bytes": 2503458816, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1449.845, + "received_bytes": 17126315, + "redis_bytes": 56255168, + "search_p50_ns": 2459, + "search_p95_ns": 4583, + "search_p99_ns": 10625, + "search_samples": 1418, + "sent_bytes": 9726870 + }, + { + "commit_ms": 241.165, + "kind": "korean", + "max_rss_bytes": 2503458816, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1461.252, + "received_bytes": 17126322, + "redis_bytes": 56971936, + "search_p50_ns": 2500, + "search_p95_ns": 7084, + "search_p99_ns": 13375, + "search_samples": 1423, + "sent_bytes": 9703870 + }, + { + "commit_ms": 1036.157, + "kind": "korean", + "max_rss_bytes": 2503458816, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2636.24, + "received_bytes": 67459662, + "redis_bytes": 96495136, + "search_p50_ns": 2375, + "search_p95_ns": 4167, + "search_p99_ns": 10875, + "search_samples": 2591, + "sent_bytes": 38678236 + }, + { + "commit_ms": 977.339, + "kind": "korean", + "max_rss_bytes": 2503458816, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 3037.448, + "received_bytes": 67459661, + "redis_bytes": 97200656, + "search_p50_ns": 2375, + "search_p95_ns": 11000, + "search_p99_ns": 20625, + "search_samples": 2923, + "sent_bytes": 38438215 + }, + { + "commit_ms": 1913.918, + "kind": "korean", + "max_rss_bytes": 2503458816, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 4924.151, + "received_bytes": 75499112, + "redis_bytes": 141363600, + "search_p50_ns": 3084, + "search_p95_ns": 13375, + "search_p99_ns": 29500, + "search_samples": 4717, + "sent_bytes": 44039879 + } + ], + "startup_ms": 1812.156, + "prune": { + "ms": 83.11, + "generations": 8, + "chunks": 8726, + "redis_bytes": 46155200 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788659129974952 uptime_in_seconds:351 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274233 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 624.794, + "kind": "shared", + "max_rss_bytes": 390004736, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 1594.945, + "received_bytes": 25923460, + "redis_bytes": 31122272, + "search_p50_ns": 416, + "search_p95_ns": 1250, + "search_p99_ns": 6583, + "search_samples": 1579, + "sent_bytes": 30356963 + }, + { + "commit_ms": 620.365, + "kind": "shared", + "max_rss_bytes": 476184576, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 620.423, + "received_bytes": 25742434, + "redis_bytes": 31129856, + "search_p50_ns": 1958, + "search_p95_ns": 3042, + "search_p99_ns": 7291, + "search_samples": 613, + "sent_bytes": 684703 + }, + { + "commit_ms": 5.282, + "kind": "shared", + "max_rss_bytes": 648298496, + "n": 1000000, + "operation": "add_1", + "ready_ms": 545.39, + "received_bytes": 1410025, + "redis_bytes": 31863264, + "search_p50_ns": 2083, + "search_p95_ns": 4708, + "search_p99_ns": 13833, + "search_samples": 537, + "sent_bytes": 710040 + }, + { + "commit_ms": 5.799, + "kind": "shared", + "max_rss_bytes": 710524928, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 546.052, + "received_bytes": 1410032, + "redis_bytes": 32568768, + "search_p50_ns": 2208, + "search_p95_ns": 8833, + "search_p99_ns": 14500, + "search_samples": 511, + "sent_bytes": 710020 + }, + { + "commit_ms": 212.454, + "kind": "shared", + "max_rss_bytes": 726253568, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 822.606, + "received_bytes": 12312262, + "redis_bytes": 39476064, + "search_p50_ns": 2000, + "search_p95_ns": 3583, + "search_p99_ns": 10083, + "search_samples": 805, + "sent_bytes": 7319781 + }, + { + "commit_ms": 209.353, + "kind": "shared", + "max_rss_bytes": 771964928, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 839.467, + "received_bytes": 12312261, + "redis_bytes": 40181568, + "search_p50_ns": 2083, + "search_p95_ns": 3875, + "search_p99_ns": 11000, + "search_samples": 819, + "sent_bytes": 7296781 + }, + { + "commit_ms": 871.11, + "kind": "shared", + "max_rss_bytes": 781910016, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 1801.203, + "received_bytes": 47387046, + "redis_bytes": 67213856, + "search_p50_ns": 2041, + "search_p95_ns": 3083, + "search_p99_ns": 11125, + "search_samples": 1768, + "sent_bytes": 28641794 + }, + { + "commit_ms": 849.876, + "kind": "shared", + "max_rss_bytes": 857964544, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 1780.065, + "received_bytes": 47386904, + "redis_bytes": 67919360, + "search_p50_ns": 2125, + "search_p95_ns": 2958, + "search_p99_ns": 8750, + "search_samples": 1749, + "sent_bytes": 28401592 + }, + { + "commit_ms": 1047.109, + "kind": "shared", + "max_rss_bytes": 1242595328, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 2207.193, + "received_bytes": 53497335, + "redis_bytes": 99067408, + "search_p50_ns": 2042, + "search_p95_ns": 3709, + "search_p99_ns": 10208, + "search_samples": 2147, + "sent_bytes": 33037669 + } + ], + "startup_ms": 954.829, + "prune": { + "ms": 32.579, + "generations": 8, + "chunks": 8726, + "redis_bytes": 33100960 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788658930194342 uptime_in_seconds:152 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274034 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 624.018, + "kind": "shared", + "max_rss_bytes": 388218880, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 1584.173, + "received_bytes": 25923468, + "redis_bytes": 31147376, + "search_p50_ns": 417, + "search_p95_ns": 1375, + "search_p99_ns": 5500, + "search_samples": 1559, + "sent_bytes": 30356963 + }, + { + "commit_ms": 619.173, + "kind": "shared", + "max_rss_bytes": 532480000, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 619.235, + "received_bytes": 25742372, + "redis_bytes": 31154960, + "search_p50_ns": 2041, + "search_p95_ns": 2708, + "search_p99_ns": 11291, + "search_samples": 615, + "sent_bytes": 684602 + }, + { + "commit_ms": 5.288, + "kind": "shared", + "max_rss_bytes": 637353984, + "n": 1000000, + "operation": "add_1", + "ready_ms": 525.394, + "received_bytes": 1410034, + "redis_bytes": 31888352, + "search_p50_ns": 2125, + "search_p95_ns": 5541, + "search_p99_ns": 12292, + "search_samples": 517, + "sent_bytes": 710040 + }, + { + "commit_ms": 5.895, + "kind": "shared", + "max_rss_bytes": 702316544, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 526.108, + "received_bytes": 1410041, + "redis_bytes": 32593792, + "search_p50_ns": 2250, + "search_p95_ns": 7042, + "search_p99_ns": 14875, + "search_samples": 495, + "sent_bytes": 710020 + }, + { + "commit_ms": 250.701, + "kind": "shared", + "max_rss_bytes": 703299584, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1250.986, + "received_bytes": 12312343, + "redis_bytes": 39500560, + "search_p50_ns": 3500, + "search_p95_ns": 12583, + "search_p99_ns": 20000, + "search_samples": 1222, + "sent_bytes": 7319882 + }, + { + "commit_ms": 297.164, + "kind": "shared", + "max_rss_bytes": 786825216, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1137.278, + "received_bytes": 12312414, + "redis_bytes": 40198896, + "search_p50_ns": 2500, + "search_p95_ns": 3708, + "search_p99_ns": 9500, + "search_samples": 1116, + "sent_bytes": 7296983 + }, + { + "commit_ms": 908.539, + "kind": "shared", + "max_rss_bytes": 797065216, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 1888.669, + "received_bytes": 47387055, + "redis_bytes": 67236032, + "search_p50_ns": 2042, + "search_p95_ns": 2833, + "search_p99_ns": 8792, + "search_samples": 1872, + "sent_bytes": 28641794 + }, + { + "commit_ms": 1095.52, + "kind": "shared", + "max_rss_bytes": 801865728, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2285.687, + "received_bytes": 47387273, + "redis_bytes": 67921152, + "search_p50_ns": 2542, + "search_p95_ns": 8583, + "search_p99_ns": 16625, + "search_samples": 2261, + "sent_bytes": 28402097 + }, + { + "commit_ms": 1402.667, + "kind": "shared", + "max_rss_bytes": 1123762176, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 2672.854, + "received_bytes": 53497779, + "redis_bytes": 99106608, + "search_p50_ns": 2500, + "search_p95_ns": 6709, + "search_p99_ns": 15916, + "search_samples": 2617, + "sent_bytes": 33038275 + } + ], + "startup_ms": 948.085, + "prune": { + "ms": 33.217, + "generations": 8, + "chunks": 8726, + "redis_bytes": 33141440 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:92945 process_supervised:no run_id:7df2e94555fd97f6003bd800740fc6cab3115d03 tcp_port:16390 server_time_usec:1788659096030483 uptime_in_seconds:318 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10274200 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + } + ] +} diff --git a/benchmarks/results/v3-20260906-valkey-crash.txt b/benchmarks/results/v3-20260906-valkey-crash.txt new file mode 100644 index 00000000..cd4c0e31 --- /dev/null +++ b/benchmarks/results/v3-20260906-valkey-crash.txt @@ -0,0 +1,24 @@ +Exploratory run: local source build, default prefetch, Valkey 9.1.2. +The final successful matrix uses prefetch-batch-max-size=0. + +=== VALKEY BUG REPORT START: Cut & paste starting from here === +36656:M 06 Sep 2026 02:57:50.531 # valkey 9.1.2 crashed by signal: 11, si_code: 2 +36656:M 06 Sep 2026 02:57:50.531 # Accessing address: 0x222c6c6c756e3a22 +36656:M 06 Sep 2026 02:57:50.531 # Crashed running the instruction at: 0x104e047ec + +------ STACK TRACE ------ +EIP: +0 valkey-server 0x0000000104e047ec hashtableIncrementalFindStep + 200 + +Backtrace: +0 libsystem_platform.dylib 0x000000018cd2b744 _sigtramp + 56 +1 valkey-server 0x0000000104e4fe30 prefetchCommandQueueKeys + 476 +2 valkey-server 0x0000000104e4fe30 prefetchCommandQueueKeys + 476 +3 valkey-server 0x0000000104e4e25c processInputBuffer + 660 +4 valkey-server 0x0000000104e4d7b8 readQueryFromClient + 176 +5 valkey-server 0x0000000104edf68c connSocketEventHandler + 180 +6 valkey-server 0x0000000104d7e790 aeProcessEvents + 364 +7 valkey-server 0x0000000104edcb60 main + 3348 +8 dyld 0x000000018c963e00 start + 6992 + +------ STACK TRACE DONE ------ diff --git a/benchmarks/results/v3-20260906.json b/benchmarks/results/v3-20260906.json new file mode 100644 index 00000000..2e584326 --- /dev/null +++ b/benchmarks/results/v3-20260906.json @@ -0,0 +1,5503 @@ +{ + "date": "2026-09-06", + "host": "Apple M4, 10 logical CPUs, 16 GiB RAM, macOS 26.5.2 (Darwin 25.5.0), arm64", + "go": "1.26.7", + "redis": "8.10.1", + "valkey": "9.1.2, source-built, prefetch-batch-max-size=0", + "configuration": "Standalone localhost TCP, persistence disabled, MemoryEfficient preset, 50ms polling, default 5 minute leases; two runs per size/distribution, separate Go process per run.", + "caveats": [ + "Developer workstation, not an isolated performance lab; no latency or memory guarantee.", + "Server-wide memory and network counters include protocol, polling, engine downloads, script cache and retained generations.", + "Prune benchmark advances generation registry timestamps past the retention horizon; it does not wait 24 hours.", + "Default-prefetch local Valkey 9.1.2 exited with SIGSEGV during exploratory measurement; successful Valkey results disable prefetch." + ], + "source_sha256": { + "pkg/acor/versioned.go": "f96993b16da7f22e24f9411a84f929d126a52a87eef3b6d7d95dfd70bdf58e02", + "pkg/acor/versioned_failure_test.go": "de3e220c7ed5b20c27aaec77a6a2855eb002407f5cae687eecb6178dff63069b", + "pkg/acor/versioned_million_test.go": "83f03bfa7029d7966917a358477b3fdedae24a1b0355d49794d47cbf92107e5f", + "pkg/acor/versioned_prune.go": "80afd3a69d5b416a554aa77bcd6e8534ee56aadcbec2b4254da6a28fae2a0b6b", + "pkg/acor/versioned_scale_test.go": "ddbc0e213c3b7a47d3d5ed88542de19cbadcb186d62b0f11ba93a538cbd44895", + "pkg/acor/versioned_search.go": "9d1ba589f381d274e1f5ac461b47b574ac00929de4343f39c9fd47668dfe0b10", + "pkg/acor/versioned_snapshot.go": "bad1796954fb058b77bccdc0996a3c04c359d36cc728efbcc22b4422031ad47f", + "pkg/acor/versioned_test.go": "cf84d3f7ebd06735397bbd598f761c3cb9c5e37f40d5bc74118e68a6882ad126", + "pkg/acor/versioned_write.go": "1f8afde415e110d48dd50ace4849d5c93acdc6c78491d8e9d562d53e32f76040" + }, + "runs": [ + { + "backend": "redis", + "n": 10000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 321.832, + "kind": "diverse", + "max_rss_bytes": 40910848, + "n": 10000, + "operation": "initial_load", + "ready_ms": 612.161, + "received_bytes": 1015247, + "redis_bytes": 3789984, + "search_p50_ns": 708, + "search_p95_ns": 2500, + "search_p99_ns": 5917, + "search_samples": 609, + "sent_bytes": 5032809 + }, + { + "commit_ms": 234.542, + "kind": "diverse", + "max_rss_bytes": 49938432, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 234.614, + "received_bytes": 835486, + "redis_bytes": 3811952, + "search_p50_ns": 1625, + "search_p95_ns": 2083, + "search_p99_ns": 3041, + "search_samples": 231, + "sent_bytes": 622410 + }, + { + "commit_ms": 5.131, + "kind": "diverse", + "max_rss_bytes": 57966592, + "n": 10000, + "operation": "add_1", + "ready_ms": 245.231, + "received_bytes": 1476548, + "redis_bytes": 4470816, + "search_p50_ns": 1625, + "search_p95_ns": 2333, + "search_p99_ns": 8541, + "search_samples": 240, + "sent_bytes": 1267356 + }, + { + "commit_ms": 5.342, + "kind": "diverse", + "max_rss_bytes": 71335936, + "n": 10000, + "operation": "remove_1", + "ready_ms": 255.521, + "received_bytes": 1476548, + "redis_bytes": 5106672, + "search_p50_ns": 1666, + "search_p95_ns": 2458, + "search_p99_ns": 7875, + "search_samples": 251, + "sent_bytes": 1267335 + }, + { + "commit_ms": 119.813, + "kind": "diverse", + "max_rss_bytes": 76873728, + "n": 10000, + "operation": "add_1000", + "ready_ms": 370.11, + "received_bytes": 1553290, + "redis_bytes": 6206288, + "search_p50_ns": 1709, + "search_p95_ns": 2125, + "search_p99_ns": 7833, + "search_samples": 365, + "sent_bytes": 2341614 + }, + { + "commit_ms": 118.851, + "kind": "diverse", + "max_rss_bytes": 77643776, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 369.136, + "received_bytes": 1553093, + "redis_bytes": 6878896, + "search_p50_ns": 1625, + "search_p95_ns": 2125, + "search_p99_ns": 7292, + "search_samples": 369, + "sent_bytes": 2246289 + }, + { + "commit_ms": 17.903, + "kind": "diverse", + "max_rss_bytes": 78348288, + "n": 10000, + "operation": "add_100", + "ready_ms": 268.214, + "received_bytes": 1484877, + "redis_bytes": 7554224, + "search_p50_ns": 1625, + "search_p95_ns": 2458, + "search_p99_ns": 9125, + "search_samples": 263, + "sent_bytes": 1386763 + }, + { + "commit_ms": 17.778, + "kind": "diverse", + "max_rss_bytes": 78364672, + "n": 10000, + "operation": "remove_100", + "ready_ms": 268.044, + "received_bytes": 1484845, + "redis_bytes": 8212240, + "search_p50_ns": 1709, + "search_p95_ns": 2333, + "search_p99_ns": 7625, + "search_samples": 264, + "sent_bytes": 1375780 + }, + { + "commit_ms": 499.841, + "kind": "diverse", + "max_rss_bytes": 78430208, + "n": 10000, + "operation": "full_replace", + "ready_ms": 750.129, + "received_bytes": 1706186, + "redis_bytes": 10461024, + "search_p50_ns": 1666, + "search_p95_ns": 2292, + "search_p99_ns": 6167, + "search_samples": 747, + "sent_bytes": 5709273 + } + ], + "startup_ms": 250.023, + "prune": { + "ms": 16.566, + "generations": 8, + "chunks": 4716, + "redis_bytes": 3892432 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788631841789072 uptime_in_seconds:1133 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10246945 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 10000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 264.452, + "kind": "diverse", + "max_rss_bytes": 40517632, + "n": 10000, + "operation": "initial_load", + "ready_ms": 514.64, + "received_bytes": 1015101, + "redis_bytes": 3792240, + "search_p50_ns": 334, + "search_p95_ns": 834, + "search_p99_ns": 3541, + "search_samples": 512, + "sent_bytes": 5032607 + }, + { + "commit_ms": 234.783, + "kind": "diverse", + "max_rss_bytes": 48381952, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 234.863, + "received_bytes": 835487, + "redis_bytes": 3814208, + "search_p50_ns": 1625, + "search_p95_ns": 2250, + "search_p99_ns": 5125, + "search_samples": 231, + "sent_bytes": 622410 + }, + { + "commit_ms": 5.256, + "kind": "diverse", + "max_rss_bytes": 56492032, + "n": 10000, + "operation": "add_1", + "ready_ms": 245.401, + "received_bytes": 1476549, + "redis_bytes": 4473056, + "search_p50_ns": 1625, + "search_p95_ns": 2208, + "search_p99_ns": 8416, + "search_samples": 241, + "sent_bytes": 1267356 + }, + { + "commit_ms": 5.34, + "kind": "diverse", + "max_rss_bytes": 73957376, + "n": 10000, + "operation": "remove_1", + "ready_ms": 315.485, + "received_bytes": 1476549, + "redis_bytes": 5108896, + "search_p50_ns": 3583, + "search_p95_ns": 11042, + "search_p99_ns": 13958, + "search_samples": 311, + "sent_bytes": 1267335 + }, + { + "commit_ms": 159.768, + "kind": "diverse", + "max_rss_bytes": 76267520, + "n": 10000, + "operation": "add_1000", + "ready_ms": 420.064, + "received_bytes": 1553506, + "redis_bytes": 6208416, + "search_p50_ns": 2083, + "search_p95_ns": 10375, + "search_p99_ns": 13208, + "search_samples": 415, + "sent_bytes": 2341917 + }, + { + "commit_ms": 120.979, + "kind": "diverse", + "max_rss_bytes": 77037568, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 371.26, + "received_bytes": 1553027, + "redis_bytes": 6881040, + "search_p50_ns": 1750, + "search_p95_ns": 3708, + "search_p99_ns": 10542, + "search_samples": 368, + "sent_bytes": 2246188 + }, + { + "commit_ms": 17.677, + "kind": "diverse", + "max_rss_bytes": 77185024, + "n": 10000, + "operation": "add_100", + "ready_ms": 257.911, + "received_bytes": 1484878, + "redis_bytes": 7556352, + "search_p50_ns": 1667, + "search_p95_ns": 2792, + "search_p99_ns": 10042, + "search_samples": 252, + "sent_bytes": 1386763 + }, + { + "commit_ms": 17.297, + "kind": "diverse", + "max_rss_bytes": 77791232, + "n": 10000, + "operation": "remove_100", + "ready_ms": 257.459, + "received_bytes": 1484846, + "redis_bytes": 8234848, + "search_p50_ns": 1667, + "search_p95_ns": 2542, + "search_p99_ns": 8750, + "search_samples": 252, + "sent_bytes": 1375780 + }, + { + "commit_ms": 500.716, + "kind": "diverse", + "max_rss_bytes": 80101376, + "n": 10000, + "operation": "full_replace", + "ready_ms": 750.997, + "received_bytes": 1706188, + "redis_bytes": 10481888, + "search_p50_ns": 1667, + "search_p95_ns": 2125, + "search_p99_ns": 3666, + "search_samples": 747, + "sent_bytes": 5709273 + } + ], + "startup_ms": 245.075, + "prune": { + "ms": 16.584, + "generations": 8, + "chunks": 4716, + "redis_bytes": 3890672 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788632071897195 uptime_in_seconds:1363 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10247175 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 10000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 267.687, + "kind": "korean", + "max_rss_bytes": 63143936, + "n": 10000, + "operation": "initial_load", + "ready_ms": 527.946, + "received_bytes": 1221328, + "redis_bytes": 3995056, + "search_p50_ns": 584, + "search_p95_ns": 1042, + "search_p99_ns": 9084, + "search_samples": 523, + "sent_bytes": 5274738 + }, + { + "commit_ms": 238.046, + "kind": "korean", + "max_rss_bytes": 71024640, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 238.134, + "received_bytes": 1041580, + "redis_bytes": 4017008, + "search_p50_ns": 2166, + "search_p95_ns": 2625, + "search_p99_ns": 3459, + "search_samples": 234, + "sent_bytes": 627722 + }, + { + "commit_ms": 5.472, + "kind": "korean", + "max_rss_bytes": 100859904, + "n": 10000, + "operation": "add_1", + "ready_ms": 275.704, + "received_bytes": 1686823, + "redis_bytes": 4675808, + "search_p50_ns": 2250, + "search_p95_ns": 9250, + "search_p99_ns": 11375, + "search_samples": 270, + "sent_bytes": 1276878 + }, + { + "commit_ms": 5.999, + "kind": "korean", + "max_rss_bytes": 133660672, + "n": 10000, + "operation": "remove_1", + "ready_ms": 326.262, + "received_bytes": 1686828, + "redis_bytes": 5311648, + "search_p50_ns": 3833, + "search_p95_ns": 15833, + "search_p99_ns": 23042, + "search_samples": 318, + "sent_bytes": 1276858 + }, + { + "commit_ms": 144.167, + "kind": "korean", + "max_rss_bytes": 133906432, + "n": 10000, + "operation": "add_1000", + "ready_ms": 424.297, + "received_bytes": 1807924, + "redis_bytes": 6461024, + "search_p50_ns": 2333, + "search_p95_ns": 11458, + "search_p99_ns": 18000, + "search_samples": 417, + "sent_bytes": 2395549 + }, + { + "commit_ms": 118.596, + "kind": "korean", + "max_rss_bytes": 134217728, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 378.807, + "received_bytes": 1807417, + "redis_bytes": 7133936, + "search_p50_ns": 2125, + "search_p95_ns": 3292, + "search_p99_ns": 8375, + "search_samples": 373, + "sent_bytes": 2292197 + }, + { + "commit_ms": 18.108, + "kind": "korean", + "max_rss_bytes": 134332416, + "n": 10000, + "operation": "add_100", + "ready_ms": 278.258, + "received_bytes": 1700106, + "redis_bytes": 7835280, + "search_p50_ns": 2125, + "search_p95_ns": 3042, + "search_p99_ns": 7333, + "search_samples": 274, + "sent_bytes": 1401205 + }, + { + "commit_ms": 17.855, + "kind": "korean", + "max_rss_bytes": 134348800, + "n": 10000, + "operation": "remove_100", + "ready_ms": 278.168, + "received_bytes": 1700070, + "redis_bytes": 8470864, + "search_p50_ns": 2166, + "search_p95_ns": 3333, + "search_p99_ns": 11083, + "search_samples": 273, + "sent_bytes": 1389141 + }, + { + "commit_ms": 505.03, + "kind": "korean", + "max_rss_bytes": 148684800, + "n": 10000, + "operation": "full_replace", + "ready_ms": 775.221, + "received_bytes": 2112970, + "redis_bytes": 10955456, + "search_p50_ns": 2083, + "search_p95_ns": 2750, + "search_p99_ns": 7750, + "search_samples": 765, + "sent_bytes": 5907420 + } + ], + "startup_ms": 262.265, + "prune": { + "ms": 16.563, + "generations": 8, + "chunks": 4748, + "redis_bytes": 4106848 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788631845682701 uptime_in_seconds:1137 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10246949 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 10000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 267.687, + "kind": "korean", + "max_rss_bytes": 64241664, + "n": 10000, + "operation": "initial_load", + "ready_ms": 527.952, + "received_bytes": 1221331, + "redis_bytes": 4015040, + "search_p50_ns": 583, + "search_p95_ns": 1125, + "search_p99_ns": 3458, + "search_samples": 524, + "sent_bytes": 5274738 + }, + { + "commit_ms": 238.755, + "kind": "korean", + "max_rss_bytes": 81592320, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 238.837, + "received_bytes": 1041586, + "redis_bytes": 4016528, + "search_p50_ns": 2042, + "search_p95_ns": 2625, + "search_p99_ns": 4125, + "search_samples": 234, + "sent_bytes": 627722 + }, + { + "commit_ms": 5.149, + "kind": "korean", + "max_rss_bytes": 106971136, + "n": 10000, + "operation": "add_1", + "ready_ms": 265.249, + "received_bytes": 1686757, + "redis_bytes": 4675392, + "search_p50_ns": 2083, + "search_p95_ns": 2834, + "search_p99_ns": 9667, + "search_samples": 259, + "sent_bytes": 1276777 + }, + { + "commit_ms": 5.721, + "kind": "korean", + "max_rss_bytes": 111132672, + "n": 10000, + "operation": "remove_1", + "ready_ms": 265.989, + "received_bytes": 1686757, + "redis_bytes": 5311248, + "search_p50_ns": 2042, + "search_p95_ns": 3000, + "search_p99_ns": 9709, + "search_samples": 257, + "sent_bytes": 1276757 + }, + { + "commit_ms": 120.047, + "kind": "korean", + "max_rss_bytes": 120946688, + "n": 10000, + "operation": "add_1000", + "ready_ms": 390.284, + "received_bytes": 1807709, + "redis_bytes": 6459744, + "search_p50_ns": 2250, + "search_p95_ns": 3625, + "search_p99_ns": 8875, + "search_samples": 382, + "sent_bytes": 2395246 + }, + { + "commit_ms": 118.033, + "kind": "korean", + "max_rss_bytes": 121225216, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 378.225, + "received_bytes": 1807490, + "redis_bytes": 7132624, + "search_p50_ns": 2208, + "search_p95_ns": 2875, + "search_p99_ns": 8041, + "search_samples": 373, + "sent_bytes": 2292298 + }, + { + "commit_ms": 17.754, + "kind": "korean", + "max_rss_bytes": 125042688, + "n": 10000, + "operation": "add_100", + "ready_ms": 278.036, + "received_bytes": 1700107, + "redis_bytes": 7833664, + "search_p50_ns": 2125, + "search_p95_ns": 3042, + "search_p99_ns": 10666, + "search_samples": 272, + "sent_bytes": 1401205 + }, + { + "commit_ms": 17.724, + "kind": "korean", + "max_rss_bytes": 128188416, + "n": 10000, + "operation": "remove_100", + "ready_ms": 278.007, + "received_bytes": 1700071, + "redis_bytes": 8489728, + "search_p50_ns": 2125, + "search_p95_ns": 3000, + "search_p99_ns": 8166, + "search_samples": 272, + "sent_bytes": 1389141 + }, + { + "commit_ms": 504.592, + "kind": "korean", + "max_rss_bytes": 152158208, + "n": 10000, + "operation": "full_replace", + "ready_ms": 824.816, + "received_bytes": 2112976, + "redis_bytes": 10950672, + "search_p50_ns": 2209, + "search_p95_ns": 9667, + "search_p99_ns": 13750, + "search_samples": 817, + "sent_bytes": 5907420 + } + ], + "startup_ms": 261.182, + "prune": { + "ms": 20.055, + "generations": 8, + "chunks": 4748, + "redis_bytes": 4104416 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788632075780905 uptime_in_seconds:1367 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10247179 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 10000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 280.268, + "kind": "shared", + "max_rss_bytes": 25346048, + "n": 10000, + "operation": "initial_load", + "ready_ms": 520.371, + "received_bytes": 1108770, + "redis_bytes": 3870976, + "search_p50_ns": 375, + "search_p95_ns": 791, + "search_p99_ns": 3666, + "search_samples": 516, + "sent_bytes": 5148734 + }, + { + "commit_ms": 270.102, + "kind": "shared", + "max_rss_bytes": 26755072, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 270.179, + "received_bytes": 929087, + "redis_bytes": 3892896, + "search_p50_ns": 1500, + "search_p95_ns": 9833, + "search_p99_ns": 14500, + "search_samples": 269, + "sent_bytes": 625730 + }, + { + "commit_ms": 5.814, + "kind": "shared", + "max_rss_bytes": 30195712, + "n": 10000, + "operation": "add_1", + "ready_ms": 216.075, + "received_bytes": 1572709, + "redis_bytes": 4531184, + "search_p50_ns": 1292, + "search_p95_ns": 2250, + "search_p99_ns": 7458, + "search_samples": 212, + "sent_bytes": 1273264 + }, + { + "commit_ms": 5.85, + "kind": "shared", + "max_rss_bytes": 33390592, + "n": 10000, + "operation": "remove_1", + "ready_ms": 326.196, + "received_bytes": 1572708, + "redis_bytes": 5208960, + "search_p50_ns": 1875, + "search_p95_ns": 10000, + "search_p99_ns": 14959, + "search_samples": 316, + "sent_bytes": 1273244 + }, + { + "commit_ms": 130.524, + "kind": "shared", + "max_rss_bytes": 33521664, + "n": 10000, + "operation": "add_1000", + "ready_ms": 370.706, + "received_bytes": 1672787, + "redis_bytes": 6311168, + "search_p50_ns": 1458, + "search_p95_ns": 6417, + "search_p99_ns": 10750, + "search_samples": 361, + "sent_bytes": 2370883 + }, + { + "commit_ms": 189.113, + "kind": "shared", + "max_rss_bytes": 34373632, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 699.786, + "received_bytes": 1672515, + "redis_bytes": 6963072, + "search_p50_ns": 2292, + "search_p95_ns": 10958, + "search_p99_ns": 24917, + "search_samples": 645, + "sent_bytes": 2253701 + }, + { + "commit_ms": 82.44, + "kind": "shared", + "max_rss_bytes": 34406400, + "n": 10000, + "operation": "add_100", + "ready_ms": 742.724, + "received_bytes": 1583728, + "redis_bytes": 7681760, + "search_p50_ns": 4833, + "search_p95_ns": 11417, + "search_p99_ns": 16875, + "search_samples": 667, + "sent_bytes": 1395386 + }, + { + "commit_ms": 34.356, + "kind": "shared", + "max_rss_bytes": 34406400, + "n": 10000, + "operation": "remove_100", + "ready_ms": 324.539, + "received_bytes": 1583695, + "redis_bytes": 8317216, + "search_p50_ns": 2291, + "search_p95_ns": 8209, + "search_p99_ns": 11583, + "search_samples": 320, + "sent_bytes": 1384418 + }, + { + "commit_ms": 591.947, + "kind": "shared", + "max_rss_bytes": 35012608, + "n": 10000, + "operation": "full_replace", + "ready_ms": 832.289, + "received_bytes": 1887143, + "redis_bytes": 10652688, + "search_p50_ns": 1583, + "search_p95_ns": 6459, + "search_p99_ns": 12458, + "search_samples": 823, + "sent_bytes": 5770860 + } + ], + "startup_ms": 233.725, + "prune": { + "ms": 16.856, + "generations": 8, + "chunks": 4736, + "redis_bytes": 3979424 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788631836956466 uptime_in_seconds:1128 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10246940 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 10000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 286.381, + "kind": "shared", + "max_rss_bytes": 26820608, + "n": 10000, + "operation": "initial_load", + "ready_ms": 526.544, + "received_bytes": 1108842, + "redis_bytes": 3871280, + "search_p50_ns": 459, + "search_p95_ns": 1959, + "search_p99_ns": 4167, + "search_samples": 524, + "sent_bytes": 5148835 + }, + { + "commit_ms": 232.417, + "kind": "shared", + "max_rss_bytes": 27607040, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 232.503, + "received_bytes": 929087, + "redis_bytes": 3893200, + "search_p50_ns": 1500, + "search_p95_ns": 2083, + "search_p99_ns": 4042, + "search_samples": 232, + "sent_bytes": 625730 + }, + { + "commit_ms": 5.62, + "kind": "shared", + "max_rss_bytes": 30883840, + "n": 10000, + "operation": "add_1", + "ready_ms": 245.849, + "received_bytes": 1572637, + "redis_bytes": 4551968, + "search_p50_ns": 1584, + "search_p95_ns": 2083, + "search_p99_ns": 6792, + "search_samples": 241, + "sent_bytes": 1273163 + }, + { + "commit_ms": 5.491, + "kind": "shared", + "max_rss_bytes": 32669696, + "n": 10000, + "operation": "remove_1", + "ready_ms": 245.673, + "received_bytes": 1572641, + "redis_bytes": 5208272, + "search_p50_ns": 1667, + "search_p95_ns": 2375, + "search_p99_ns": 3917, + "search_samples": 243, + "sent_bytes": 1273143 + }, + { + "commit_ms": 119.774, + "kind": "shared", + "max_rss_bytes": 32833536, + "n": 10000, + "operation": "add_1000", + "ready_ms": 370.028, + "received_bytes": 1672642, + "redis_bytes": 6308176, + "search_p50_ns": 1584, + "search_p95_ns": 2083, + "search_p99_ns": 6083, + "search_samples": 363, + "sent_bytes": 2370681 + }, + { + "commit_ms": 117.542, + "kind": "shared", + "max_rss_bytes": 32931840, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 357.774, + "received_bytes": 1672299, + "redis_bytes": 6961120, + "search_p50_ns": 1500, + "search_p95_ns": 2000, + "search_p99_ns": 7292, + "search_samples": 353, + "sent_bytes": 2253398 + }, + { + "commit_ms": 18.287, + "kind": "shared", + "max_rss_bytes": 35012608, + "n": 10000, + "operation": "add_100", + "ready_ms": 258.452, + "received_bytes": 1583651, + "redis_bytes": 7680192, + "search_p50_ns": 1666, + "search_p95_ns": 2083, + "search_p99_ns": 4084, + "search_samples": 255, + "sent_bytes": 1395285 + }, + { + "commit_ms": 17.646, + "kind": "shared", + "max_rss_bytes": 35045376, + "n": 10000, + "operation": "remove_100", + "ready_ms": 257.873, + "received_bytes": 1583624, + "redis_bytes": 8336096, + "search_p50_ns": 1666, + "search_p95_ns": 2042, + "search_p99_ns": 5584, + "search_samples": 253, + "sent_bytes": 1384317 + }, + { + "commit_ms": 500.956, + "kind": "shared", + "max_rss_bytes": 35094528, + "n": 10000, + "operation": "full_replace", + "ready_ms": 741.246, + "received_bytes": 1886929, + "redis_bytes": 10673952, + "search_p50_ns": 1625, + "search_p95_ns": 2042, + "search_p99_ns": 3291, + "search_samples": 739, + "sent_bytes": 5770557 + } + ], + "startup_ms": 238.742, + "prune": { + "ms": 16.928, + "generations": 8, + "chunks": 4736, + "redis_bytes": 3978592 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788632068151606 uptime_in_seconds:1360 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10247172 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 323.513, + "kind": "diverse", + "max_rss_bytes": 174194688, + "n": 100000, + "operation": "initial_load", + "ready_ms": 743.72, + "received_bytes": 2515332, + "redis_bytes": 5544528, + "search_p50_ns": 375, + "search_p95_ns": 1042, + "search_p99_ns": 9167, + "search_samples": 740, + "sent_bytes": 6949154 + }, + { + "commit_ms": 300.95, + "kind": "diverse", + "max_rss_bytes": 228048896, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 301.015, + "received_bytes": 2334151, + "redis_bytes": 5566448, + "search_p50_ns": 1916, + "search_p95_ns": 2459, + "search_p99_ns": 6833, + "search_samples": 286, + "sent_bytes": 684097 + }, + { + "commit_ms": 5.651, + "kind": "diverse", + "max_rss_bytes": 291995648, + "n": 100000, + "operation": "add_1", + "ready_ms": 415.897, + "received_bytes": 3027075, + "redis_bytes": 6319840, + "search_p50_ns": 1958, + "search_p95_ns": 2833, + "search_p99_ns": 10334, + "search_samples": 396, + "sent_bytes": 1380876 + }, + { + "commit_ms": 5.567, + "kind": "diverse", + "max_rss_bytes": 416661504, + "n": 100000, + "operation": "remove_1", + "ready_ms": 415.786, + "received_bytes": 3027152, + "redis_bytes": 7026320, + "search_p50_ns": 2083, + "search_p95_ns": 3084, + "search_p99_ns": 9083, + "search_samples": 392, + "sent_bytes": 1380957 + }, + { + "commit_ms": 131.14, + "kind": "diverse", + "max_rss_bytes": 417054720, + "n": 100000, + "operation": "add_1000", + "ready_ms": 541.259, + "received_bytes": 3406875, + "redis_bytes": 8421232, + "search_p50_ns": 2125, + "search_p95_ns": 3375, + "search_p99_ns": 12208, + "search_samples": 521, + "sent_bytes": 2758217 + }, + { + "commit_ms": 130.372, + "kind": "diverse", + "max_rss_bytes": 417153024, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 550.55, + "received_bytes": 3406804, + "redis_bytes": 9145200, + "search_p50_ns": 2083, + "search_p95_ns": 3666, + "search_p99_ns": 15375, + "search_samples": 529, + "sent_bytes": 2735116 + }, + { + "commit_ms": 130.103, + "kind": "diverse", + "max_rss_bytes": 417267712, + "n": 100000, + "operation": "add_1000", + "ready_ms": 540.236, + "received_bytes": 3406875, + "redis_bytes": 9794896, + "search_p50_ns": 2000, + "search_p95_ns": 2916, + "search_p99_ns": 6917, + "search_samples": 518, + "sent_bytes": 2758217 + }, + { + "commit_ms": 129.016, + "kind": "diverse", + "max_rss_bytes": 431538176, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 539.284, + "received_bytes": 3406876, + "redis_bytes": 10500320, + "search_p50_ns": 2083, + "search_p95_ns": 3000, + "search_p99_ns": 9125, + "search_samples": 519, + "sent_bytes": 2735217 + }, + { + "commit_ms": 601.197, + "kind": "diverse", + "max_rss_bytes": 460767232, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1081.325, + "received_bytes": 4880725, + "redis_bytes": 14736992, + "search_p50_ns": 2083, + "search_p95_ns": 2792, + "search_p99_ns": 10208, + "search_samples": 1046, + "sent_bytes": 7829456 + } + ], + "startup_ms": 414.148, + "prune": { + "ms": 20.185, + "generations": 8, + "chunks": 4987, + "redis_bytes": 5766752 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788631854724157 uptime_in_seconds:1146 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10246958 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 322.67, + "kind": "diverse", + "max_rss_bytes": 161497088, + "n": 100000, + "operation": "initial_load", + "ready_ms": 742.884, + "received_bytes": 2515331, + "redis_bytes": 5564656, + "search_p50_ns": 375, + "search_p95_ns": 1125, + "search_p99_ns": 8041, + "search_samples": 739, + "sent_bytes": 6949154 + }, + { + "commit_ms": 299.112, + "kind": "diverse", + "max_rss_bytes": 200867840, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 299.196, + "received_bytes": 2334153, + "redis_bytes": 5566096, + "search_p50_ns": 1959, + "search_p95_ns": 2375, + "search_p99_ns": 3833, + "search_samples": 288, + "sent_bytes": 684097 + }, + { + "commit_ms": 5.704, + "kind": "diverse", + "max_rss_bytes": 290045952, + "n": 100000, + "operation": "add_1", + "ready_ms": 416.013, + "received_bytes": 3027078, + "redis_bytes": 6319520, + "search_p50_ns": 2000, + "search_p95_ns": 4625, + "search_p99_ns": 10875, + "search_samples": 393, + "sent_bytes": 1380876 + }, + { + "commit_ms": 5.443, + "kind": "diverse", + "max_rss_bytes": 393117696, + "n": 100000, + "operation": "remove_1", + "ready_ms": 415.607, + "received_bytes": 3027149, + "redis_bytes": 7026000, + "search_p50_ns": 2042, + "search_p95_ns": 3416, + "search_p99_ns": 10500, + "search_samples": 394, + "sent_bytes": 1380957 + }, + { + "commit_ms": 130.397, + "kind": "diverse", + "max_rss_bytes": 403963904, + "n": 100000, + "operation": "add_1000", + "ready_ms": 540.57, + "received_bytes": 3406806, + "redis_bytes": 8421824, + "search_p50_ns": 2125, + "search_p95_ns": 2709, + "search_p99_ns": 8208, + "search_samples": 535, + "sent_bytes": 2758116 + }, + { + "commit_ms": 128.684, + "kind": "diverse", + "max_rss_bytes": 406650880, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 538.899, + "received_bytes": 3406950, + "redis_bytes": 9125408, + "search_p50_ns": 2000, + "search_p95_ns": 3000, + "search_p99_ns": 10292, + "search_samples": 519, + "sent_bytes": 2735318 + }, + { + "commit_ms": 128.811, + "kind": "diverse", + "max_rss_bytes": 416940032, + "n": 100000, + "operation": "add_1000", + "ready_ms": 539.146, + "received_bytes": 3406878, + "redis_bytes": 9816048, + "search_p50_ns": 2083, + "search_p95_ns": 2792, + "search_p99_ns": 8833, + "search_samples": 518, + "sent_bytes": 2758217 + }, + { + "commit_ms": 128.946, + "kind": "diverse", + "max_rss_bytes": 426147840, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 539.293, + "received_bytes": 3406873, + "redis_bytes": 10521536, + "search_p50_ns": 2083, + "search_p95_ns": 2708, + "search_p99_ns": 8000, + "search_samples": 533, + "sent_bytes": 2735217 + }, + { + "commit_ms": 600.943, + "kind": "diverse", + "max_rss_bytes": 462536704, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1081.24, + "received_bytes": 4880799, + "redis_bytes": 14713184, + "search_p50_ns": 2000, + "search_p95_ns": 3208, + "search_p99_ns": 10000, + "search_samples": 1048, + "sent_bytes": 7829557 + } + ], + "startup_ms": 415.611, + "prune": { + "ms": 21.295, + "generations": 8, + "chunks": 4987, + "redis_bytes": 5759200 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788632084816624 uptime_in_seconds:1376 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10247188 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 355.225, + "kind": "korean", + "max_rss_bytes": 328433664, + "n": 100000, + "operation": "initial_load", + "ready_ms": 935.361, + "received_bytes": 4516362, + "redis_bytes": 7731776, + "search_p50_ns": 625, + "search_p95_ns": 1833, + "search_p99_ns": 10458, + "search_samples": 928, + "sent_bytes": 8950191 + }, + { + "commit_ms": 324.346, + "kind": "korean", + "max_rss_bytes": 370376704, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 324.409, + "received_bytes": 4335259, + "redis_bytes": 7736320, + "search_p50_ns": 2292, + "search_p95_ns": 3084, + "search_p99_ns": 3875, + "search_samples": 324, + "sent_bytes": 684198 + }, + { + "commit_ms": 5.786, + "kind": "korean", + "max_rss_bytes": 583532544, + "n": 100000, + "operation": "add_1", + "ready_ms": 576.126, + "received_bytes": 5028705, + "redis_bytes": 8508928, + "search_p50_ns": 2500, + "search_p95_ns": 4667, + "search_p99_ns": 12000, + "search_samples": 556, + "sent_bytes": 1381471 + }, + { + "commit_ms": 6.117, + "kind": "korean", + "max_rss_bytes": 755367936, + "n": 100000, + "operation": "remove_1", + "ready_ms": 576.245, + "received_bytes": 5028854, + "redis_bytes": 9193920, + "search_p50_ns": 2500, + "search_p95_ns": 4542, + "search_p99_ns": 12584, + "search_samples": 549, + "sent_bytes": 1381653 + }, + { + "commit_ms": 141.986, + "kind": "korean", + "max_rss_bytes": 899366912, + "n": 100000, + "operation": "add_1000", + "ready_ms": 702.248, + "received_bytes": 5845336, + "redis_bytes": 11077872, + "search_p50_ns": 2583, + "search_p95_ns": 3583, + "search_p99_ns": 12375, + "search_samples": 679, + "sent_bytes": 3195643 + }, + { + "commit_ms": 143.777, + "kind": "korean", + "max_rss_bytes": 899973120, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 694.074, + "received_bytes": 5845340, + "redis_bytes": 11779296, + "search_p50_ns": 2625, + "search_p95_ns": 4625, + "search_p99_ns": 13958, + "search_samples": 668, + "sent_bytes": 3172636 + }, + { + "commit_ms": 143.153, + "kind": "korean", + "max_rss_bytes": 900628480, + "n": 100000, + "operation": "add_1000", + "ready_ms": 693.264, + "received_bytes": 5845197, + "redis_bytes": 12470176, + "search_p50_ns": 2666, + "search_p95_ns": 4000, + "search_p99_ns": 10500, + "search_samples": 668, + "sent_bytes": 3195441 + }, + { + "commit_ms": 143.022, + "kind": "korean", + "max_rss_bytes": 954548224, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 703.229, + "received_bytes": 5845486, + "redis_bytes": 13175664, + "search_p50_ns": 2583, + "search_p95_ns": 3709, + "search_p99_ns": 9291, + "search_samples": 681, + "sent_bytes": 3172838 + }, + { + "commit_ms": 637.59, + "kind": "korean", + "max_rss_bytes": 987152384, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1237.801, + "received_bytes": 8883194, + "redis_bytes": 19590144, + "search_p50_ns": 2667, + "search_p95_ns": 3500, + "search_p99_ns": 8416, + "search_samples": 1196, + "sent_bytes": 9830917 + } + ], + "startup_ms": 575.616, + "prune": { + "ms": 20.805, + "generations": 8, + "chunks": 4987, + "redis_bytes": 7962256 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788631860605143 uptime_in_seconds:1152 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10246964 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 395.672, + "kind": "korean", + "max_rss_bytes": 330465280, + "n": 100000, + "operation": "initial_load", + "ready_ms": 985.907, + "received_bytes": 4516442, + "redis_bytes": 7712192, + "search_p50_ns": 708, + "search_p95_ns": 3000, + "search_p99_ns": 9167, + "search_samples": 981, + "sent_bytes": 8950292 + }, + { + "commit_ms": 318.534, + "kind": "korean", + "max_rss_bytes": 384073728, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 318.618, + "received_bytes": 4335189, + "redis_bytes": 7734624, + "search_p50_ns": 2459, + "search_p95_ns": 3209, + "search_p99_ns": 5417, + "search_samples": 303, + "sent_bytes": 684097 + }, + { + "commit_ms": 5.524, + "kind": "korean", + "max_rss_bytes": 590397440, + "n": 100000, + "operation": "add_1", + "ready_ms": 575.727, + "received_bytes": 5028707, + "redis_bytes": 8491328, + "search_p50_ns": 2500, + "search_p95_ns": 5291, + "search_p99_ns": 14917, + "search_samples": 548, + "sent_bytes": 1381471 + }, + { + "commit_ms": 5.626, + "kind": "korean", + "max_rss_bytes": 913309696, + "n": 100000, + "operation": "remove_1", + "ready_ms": 575.902, + "received_bytes": 5028784, + "redis_bytes": 9198816, + "search_p50_ns": 2542, + "search_p95_ns": 4500, + "search_p99_ns": 11542, + "search_samples": 550, + "sent_bytes": 1381552 + }, + { + "commit_ms": 142.966, + "kind": "korean", + "max_rss_bytes": 916865024, + "n": 100000, + "operation": "add_1000", + "ready_ms": 683.08, + "received_bytes": 5845339, + "redis_bytes": 11099632, + "search_p50_ns": 2583, + "search_p95_ns": 3500, + "search_p99_ns": 6084, + "search_samples": 675, + "sent_bytes": 3195643 + }, + { + "commit_ms": 143.009, + "kind": "korean", + "max_rss_bytes": 917176320, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 703.286, + "received_bytes": 5845271, + "redis_bytes": 11801040, + "search_p50_ns": 2583, + "search_p95_ns": 3625, + "search_p99_ns": 12125, + "search_samples": 693, + "sent_bytes": 3172535 + }, + { + "commit_ms": 143.285, + "kind": "korean", + "max_rss_bytes": 934379520, + "n": 100000, + "operation": "add_1000", + "ready_ms": 693.423, + "received_bytes": 5845343, + "redis_bytes": 12471488, + "search_p50_ns": 2541, + "search_p95_ns": 3333, + "search_p99_ns": 5542, + "search_samples": 686, + "sent_bytes": 3195643 + }, + { + "commit_ms": 141.227, + "kind": "korean", + "max_rss_bytes": 934412288, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 691.39, + "received_bytes": 5845272, + "redis_bytes": 13176960, + "search_p50_ns": 2625, + "search_p95_ns": 3584, + "search_p99_ns": 9833, + "search_samples": 674, + "sent_bytes": 3172535 + }, + { + "commit_ms": 640.847, + "kind": "korean", + "max_rss_bytes": 949862400, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1251.158, + "received_bytes": 8883191, + "redis_bytes": 19576704, + "search_p50_ns": 2625, + "search_p95_ns": 3459, + "search_p99_ns": 8834, + "search_samples": 1227, + "sent_bytes": 9830917 + } + ], + "startup_ms": 563.244, + "prune": { + "ms": 20.88, + "generations": 8, + "chunks": 4987, + "redis_bytes": 7973264 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788632090675004 uptime_in_seconds:1382 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10247194 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 317.108, + "kind": "shared", + "max_rss_bytes": 60915712, + "n": 100000, + "operation": "initial_load", + "ready_ms": 637.245, + "received_bytes": 3415267, + "redis_bytes": 6524272, + "search_p50_ns": 375, + "search_p95_ns": 1333, + "search_p99_ns": 9292, + "search_samples": 636, + "sent_bytes": 7849063 + }, + { + "commit_ms": 296.909, + "kind": "shared", + "max_rss_bytes": 76185600, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 296.991, + "received_bytes": 3234161, + "redis_bytes": 6545712, + "search_p50_ns": 1875, + "search_p95_ns": 2667, + "search_p99_ns": 9959, + "search_samples": 285, + "sent_bytes": 684097 + }, + { + "commit_ms": 5.643, + "kind": "shared", + "max_rss_bytes": 99598336, + "n": 100000, + "operation": "add_1", + "ready_ms": 325.85, + "received_bytes": 3927284, + "redis_bytes": 7320304, + "search_p50_ns": 1958, + "search_p95_ns": 3250, + "search_p99_ns": 11083, + "search_samples": 313, + "sent_bytes": 1381104 + }, + { + "commit_ms": 5.53, + "kind": "shared", + "max_rss_bytes": 122175488, + "n": 100000, + "operation": "remove_1", + "ready_ms": 325.729, + "received_bytes": 3927290, + "redis_bytes": 8006304, + "search_p50_ns": 2041, + "search_p95_ns": 4625, + "search_p99_ns": 9917, + "search_samples": 311, + "sent_bytes": 1381084 + }, + { + "commit_ms": 133.515, + "kind": "shared", + "max_rss_bytes": 122437632, + "n": 100000, + "operation": "add_1000", + "ready_ms": 443.762, + "received_bytes": 4503899, + "redis_bytes": 9625072, + "search_p50_ns": 1958, + "search_p95_ns": 2625, + "search_p99_ns": 8792, + "search_samples": 435, + "sent_bytes": 2955230 + }, + { + "commit_ms": 131.394, + "kind": "shared", + "max_rss_bytes": 123748352, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 451.547, + "received_bytes": 4503900, + "redis_bytes": 10325856, + "search_p50_ns": 1916, + "search_p95_ns": 2750, + "search_p99_ns": 9916, + "search_samples": 443, + "sent_bytes": 2932229 + }, + { + "commit_ms": 131.088, + "kind": "shared", + "max_rss_bytes": 123781120, + "n": 100000, + "operation": "add_1000", + "ready_ms": 451.24, + "received_bytes": 4503902, + "redis_bytes": 10996016, + "search_p50_ns": 1958, + "search_p95_ns": 2708, + "search_p99_ns": 9000, + "search_samples": 438, + "sent_bytes": 2955230 + }, + { + "commit_ms": 130.032, + "kind": "shared", + "max_rss_bytes": 123797504, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 450.262, + "received_bytes": 4503902, + "redis_bytes": 11701456, + "search_p50_ns": 1875, + "search_p95_ns": 2791, + "search_p99_ns": 8459, + "search_samples": 444, + "sent_bytes": 2932229 + }, + { + "commit_ms": 601.006, + "kind": "shared", + "max_rss_bytes": 151650304, + "n": 100000, + "operation": "full_replace", + "ready_ms": 971.306, + "received_bytes": 6680788, + "redis_bytes": 16920432, + "search_p50_ns": 2000, + "search_p95_ns": 2666, + "search_p99_ns": 7250, + "search_samples": 950, + "sent_bytes": 8729508 + } + ], + "startup_ms": 312.302, + "prune": { + "ms": 17.702, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6746384 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788631849726514 uptime_in_seconds:1141 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10246953 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 100000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 371.883, + "kind": "shared", + "max_rss_bytes": 61079552, + "n": 100000, + "operation": "initial_load", + "ready_ms": 692.206, + "received_bytes": 3415345, + "redis_bytes": 6544624, + "search_p50_ns": 541, + "search_p95_ns": 2375, + "search_p99_ns": 11667, + "search_samples": 686, + "sent_bytes": 7849164 + }, + { + "commit_ms": 293.647, + "kind": "shared", + "max_rss_bytes": 75399168, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 293.737, + "received_bytes": 3234162, + "redis_bytes": 6543808, + "search_p50_ns": 1917, + "search_p95_ns": 2333, + "search_p99_ns": 7834, + "search_samples": 284, + "sent_bytes": 684097 + }, + { + "commit_ms": 5.437, + "kind": "shared", + "max_rss_bytes": 91979776, + "n": 100000, + "operation": "add_1", + "ready_ms": 325.619, + "received_bytes": 3927285, + "redis_bytes": 7317040, + "search_p50_ns": 2042, + "search_p95_ns": 3042, + "search_p99_ns": 11958, + "search_samples": 315, + "sent_bytes": 1381104 + }, + { + "commit_ms": 5.519, + "kind": "shared", + "max_rss_bytes": 108953600, + "n": 100000, + "operation": "remove_1", + "ready_ms": 325.749, + "received_bytes": 3927219, + "redis_bytes": 8004624, + "search_p50_ns": 2083, + "search_p95_ns": 3750, + "search_p99_ns": 9333, + "search_samples": 320, + "sent_bytes": 1380983 + }, + { + "commit_ms": 131.88, + "kind": "shared", + "max_rss_bytes": 114507776, + "n": 100000, + "operation": "add_1000", + "ready_ms": 452.189, + "received_bytes": 4503901, + "redis_bytes": 9643520, + "search_p50_ns": 2041, + "search_p95_ns": 2667, + "search_p99_ns": 9583, + "search_samples": 445, + "sent_bytes": 2955230 + }, + { + "commit_ms": 132.103, + "kind": "shared", + "max_rss_bytes": 114507776, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 442.18, + "received_bytes": 4503968, + "redis_bytes": 10323872, + "search_p50_ns": 2041, + "search_p95_ns": 2541, + "search_p99_ns": 8833, + "search_samples": 438, + "sent_bytes": 2932330 + }, + { + "commit_ms": 132.669, + "kind": "shared", + "max_rss_bytes": 114540544, + "n": 100000, + "operation": "add_1000", + "ready_ms": 442.801, + "received_bytes": 4503898, + "redis_bytes": 11014480, + "search_p50_ns": 1875, + "search_p95_ns": 2375, + "search_p99_ns": 3333, + "search_samples": 426, + "sent_bytes": 2955230 + }, + { + "commit_ms": 131.617, + "kind": "shared", + "max_rss_bytes": 114556928, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 451.809, + "received_bytes": 4503832, + "redis_bytes": 11699456, + "search_p50_ns": 1959, + "search_p95_ns": 3459, + "search_p99_ns": 9708, + "search_samples": 446, + "sent_bytes": 2932128 + }, + { + "commit_ms": 600.44, + "kind": "shared", + "max_rss_bytes": 143147008, + "n": 100000, + "operation": "full_replace", + "ready_ms": 960.594, + "received_bytes": 6680860, + "redis_bytes": 16918832, + "search_p50_ns": 1958, + "search_p95_ns": 2916, + "search_p99_ns": 9708, + "search_samples": 938, + "sent_bytes": 8729609 + } + ], + "startup_ms": 323.433, + "prune": { + "ms": 18.086, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6752928 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788632079775576 uptime_in_seconds:1371 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10247183 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 753.62, + "kind": "diverse", + "max_rss_bytes": 1551908864, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 3033.831, + "received_bytes": 16924110, + "redis_bytes": 21330208, + "search_p50_ns": 416, + "search_p95_ns": 1167, + "search_p99_ns": 10834, + "search_samples": 2951, + "sent_bytes": 21358157 + }, + { + "commit_ms": 744.694, + "kind": "diverse", + "max_rss_bytes": 2072363008, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 744.779, + "received_bytes": 16743001, + "redis_bytes": 21349040, + "search_p50_ns": 2167, + "search_p95_ns": 4875, + "search_p99_ns": 13333, + "search_samples": 730, + "sent_bytes": 685006 + }, + { + "commit_ms": 5.562, + "kind": "diverse", + "max_rss_bytes": 2702393344, + "n": 1000000, + "operation": "add_1", + "ready_ms": 2295.796, + "received_bytes": 17442640, + "redis_bytes": 22120416, + "search_p50_ns": 2375, + "search_p95_ns": 3833, + "search_p99_ns": 11750, + "search_samples": 2209, + "sent_bytes": 1388239 + }, + { + "commit_ms": 6.269, + "kind": "diverse", + "max_rss_bytes": 3812982784, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 2656.438, + "received_bytes": 17442701, + "redis_bytes": 22801280, + "search_p50_ns": 2625, + "search_p95_ns": 11291, + "search_p99_ns": 19500, + "search_samples": 2585, + "sent_bytes": 1388320 + }, + { + "commit_ms": 201.009, + "kind": "diverse", + "max_rss_bytes": 3812982784, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 2701.248, + "received_bytes": 20948472, + "redis_bytes": 27643776, + "search_p50_ns": 2458, + "search_p95_ns": 4166, + "search_p99_ns": 11916, + "search_samples": 2650, + "sent_bytes": 5891658 + }, + { + "commit_ms": 203.468, + "kind": "diverse", + "max_rss_bytes": 3812982784, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 2523.661, + "received_bytes": 20948474, + "redis_bytes": 28351232, + "search_p50_ns": 2333, + "search_p95_ns": 3833, + "search_p99_ns": 11584, + "search_samples": 2448, + "sent_bytes": 5868658 + }, + { + "commit_ms": 816.184, + "kind": "diverse", + "max_rss_bytes": 3812982784, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 3376.311, + "received_bytes": 32338767, + "redis_bytes": 46799968, + "search_p50_ns": 2500, + "search_p95_ns": 4459, + "search_p99_ns": 12667, + "search_samples": 3319, + "sent_bytes": 20478914 + }, + { + "commit_ms": 814.534, + "kind": "diverse", + "max_rss_bytes": 3812982784, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 3064.675, + "received_bytes": 32338698, + "redis_bytes": 47537536, + "search_p50_ns": 2416, + "search_p95_ns": 3834, + "search_p99_ns": 10958, + "search_samples": 2986, + "sent_bytes": 20238813 + }, + { + "commit_ms": 1160.955, + "kind": "diverse", + "max_rss_bytes": 3812982784, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 4341.119, + "received_bytes": 35497909, + "redis_bytes": 69849904, + "search_p50_ns": 2500, + "search_p95_ns": 4708, + "search_p99_ns": 12208, + "search_samples": 4239, + "sent_bytes": 24038790 + } + ], + "startup_ms": 2245.741, + "prune": { + "ms": 46.822, + "generations": 8, + "chunks": 8726, + "redis_bytes": 24175760 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788631884586258 uptime_in_seconds:1176 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10246988 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 748.579, + "kind": "diverse", + "max_rss_bytes": 1453654016, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 3068.736, + "received_bytes": 16924112, + "redis_bytes": 21349840, + "search_p50_ns": 417, + "search_p95_ns": 2208, + "search_p99_ns": 8625, + "search_samples": 3000, + "sent_bytes": 21358157 + }, + { + "commit_ms": 741.088, + "kind": "diverse", + "max_rss_bytes": 2074640384, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 741.172, + "received_bytes": 16743003, + "redis_bytes": 21350256, + "search_p50_ns": 2208, + "search_p95_ns": 4583, + "search_p99_ns": 10917, + "search_samples": 719, + "sent_bytes": 685006 + }, + { + "commit_ms": 5.617, + "kind": "diverse", + "max_rss_bytes": 2608218112, + "n": 1000000, + "operation": "add_1", + "ready_ms": 2275.841, + "received_bytes": 17442715, + "redis_bytes": 22105200, + "search_p50_ns": 2333, + "search_p95_ns": 4250, + "search_p99_ns": 10209, + "search_samples": 2235, + "sent_bytes": 1388340 + }, + { + "commit_ms": 6.564, + "kind": "diverse", + "max_rss_bytes": 2785705984, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 2326.801, + "received_bytes": 17442703, + "redis_bytes": 22810656, + "search_p50_ns": 2333, + "search_p95_ns": 3750, + "search_p99_ns": 10625, + "search_samples": 2279, + "sent_bytes": 1388320 + }, + { + "commit_ms": 209.463, + "kind": "diverse", + "max_rss_bytes": 3025223680, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 2709.667, + "received_bytes": 20948475, + "redis_bytes": 27632144, + "search_p50_ns": 2542, + "search_p95_ns": 9709, + "search_p99_ns": 16875, + "search_samples": 2666, + "sent_bytes": 5891658 + }, + { + "commit_ms": 197.087, + "kind": "diverse", + "max_rss_bytes": 3069837312, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 2377.19, + "received_bytes": 20948475, + "redis_bytes": 28349872, + "search_p50_ns": 2542, + "search_p95_ns": 3791, + "search_p99_ns": 7750, + "search_samples": 2319, + "sent_bytes": 5868658 + }, + { + "commit_ms": 788.138, + "kind": "diverse", + "max_rss_bytes": 3197550592, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 3008.248, + "received_bytes": 32338691, + "redis_bytes": 46819408, + "search_p50_ns": 2500, + "search_p95_ns": 3625, + "search_p99_ns": 5958, + "search_samples": 2960, + "sent_bytes": 20478799 + }, + { + "commit_ms": 785.686, + "kind": "diverse", + "max_rss_bytes": 3217342464, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2955.858, + "received_bytes": 32338548, + "redis_bytes": 47508848, + "search_p50_ns": 2459, + "search_p95_ns": 3375, + "search_p99_ns": 5625, + "search_samples": 2913, + "sent_bytes": 20238611 + }, + { + "commit_ms": 1166.083, + "kind": "diverse", + "max_rss_bytes": 3366191104, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 3976.26, + "received_bytes": 35497904, + "redis_bytes": 69869312, + "search_p50_ns": 2625, + "search_p95_ns": 6292, + "search_p99_ns": 14792, + "search_samples": 3897, + "sent_bytes": 24038776 + } + ], + "startup_ms": 2229.481, + "prune": { + "ms": 33.25, + "generations": 8, + "chunks": 8726, + "redis_bytes": 24196144 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=diverse seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788632114818488 uptime_in_seconds:1406 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10247218 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 984.254, + "kind": "korean", + "max_rss_bytes": 2142683136, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 3964.391, + "received_bytes": 36924603, + "redis_bytes": 44648144, + "search_p50_ns": 666, + "search_p95_ns": 1542, + "search_p99_ns": 10375, + "search_samples": 3918, + "sent_bytes": 41358830 + }, + { + "commit_ms": 1047.947, + "kind": "korean", + "max_rss_bytes": 2418950144, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 1048.032, + "received_bytes": 36743499, + "redis_bytes": 44660864, + "search_p50_ns": 2750, + "search_p95_ns": 4417, + "search_p99_ns": 7584, + "search_samples": 1021, + "sent_bytes": 685612 + }, + { + "commit_ms": 5.982, + "kind": "korean", + "max_rss_bytes": 3601170432, + "n": 1000000, + "operation": "add_1", + "ready_ms": 2956.22, + "received_bytes": 37447526, + "redis_bytes": 45427040, + "search_p50_ns": 3125, + "search_p95_ns": 4875, + "search_p99_ns": 13417, + "search_samples": 2893, + "sent_bytes": 1393059 + }, + { + "commit_ms": 6.381, + "kind": "korean", + "max_rss_bytes": 3772743680, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 3636.578, + "received_bytes": 37447527, + "redis_bytes": 46117104, + "search_p50_ns": 3083, + "search_p95_ns": 6750, + "search_p99_ns": 17500, + "search_samples": 3511, + "sent_bytes": 1393053 + }, + { + "commit_ms": 283.998, + "kind": "korean", + "max_rss_bytes": 3910615040, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 3554.247, + "received_bytes": 45316885, + "redis_bytes": 56035136, + "search_p50_ns": 3084, + "search_p95_ns": 6458, + "search_p99_ns": 14541, + "search_samples": 3485, + "sent_bytes": 10260072 + }, + { + "commit_ms": 264.379, + "kind": "korean", + "max_rss_bytes": 4011196416, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 3514.733, + "received_bytes": 45316819, + "redis_bytes": 56748160, + "search_p50_ns": 3125, + "search_p95_ns": 5334, + "search_p99_ns": 13084, + "search_samples": 3450, + "sent_bytes": 10236971 + }, + { + "commit_ms": 1072.456, + "kind": "korean", + "max_rss_bytes": 4011196416, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 4632.648, + "received_bytes": 70598382, + "redis_bytes": 96324432, + "search_p50_ns": 3083, + "search_p95_ns": 5417, + "search_p99_ns": 16958, + "search_samples": 4477, + "sent_bytes": 38738605 + }, + { + "commit_ms": 1080.613, + "kind": "korean", + "max_rss_bytes": 4011196416, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 5130.809, + "received_bytes": 70598450, + "redis_bytes": 97013600, + "search_p50_ns": 3083, + "search_p95_ns": 13042, + "search_p99_ns": 28458, + "search_samples": 4914, + "sent_bytes": 38498685 + }, + { + "commit_ms": 1678.83, + "kind": "korean", + "max_rss_bytes": 4011196416, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 7429.152, + "received_bytes": 75499151, + "redis_bytes": 141136752, + "search_p50_ns": 3000, + "search_p95_ns": 13000, + "search_p99_ns": 31375, + "search_samples": 6854, + "sent_bytes": 44040251 + } + ], + "startup_ms": 2977.541, + "prune": { + "ms": 78.846, + "generations": 8, + "chunks": 8726, + "redis_bytes": 45946224 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788631912323427 uptime_in_seconds:1204 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10247016 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 988.152, + "kind": "korean", + "max_rss_bytes": 1960181760, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 3888.28, + "received_bytes": 36924533, + "redis_bytes": 44645696, + "search_p50_ns": 667, + "search_p95_ns": 1500, + "search_p99_ns": 7542, + "search_samples": 3815, + "sent_bytes": 41358729 + }, + { + "commit_ms": 1062.929, + "kind": "korean", + "max_rss_bytes": 2723446784, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 1063.019, + "received_bytes": 36743501, + "redis_bytes": 44681952, + "search_p50_ns": 2625, + "search_p95_ns": 5250, + "search_p99_ns": 12416, + "search_samples": 1046, + "sent_bytes": 685612 + }, + { + "commit_ms": 5.679, + "kind": "korean", + "max_rss_bytes": 2992078848, + "n": 1000000, + "operation": "add_1", + "ready_ms": 3165.95, + "received_bytes": 37447608, + "redis_bytes": 45448128, + "search_p50_ns": 3000, + "search_p95_ns": 12750, + "search_p99_ns": 21041, + "search_samples": 3118, + "sent_bytes": 1393174 + }, + { + "commit_ms": 7.2, + "kind": "korean", + "max_rss_bytes": 3235594240, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 3547.311, + "received_bytes": 37447597, + "redis_bytes": 46117712, + "search_p50_ns": 3125, + "search_p95_ns": 12292, + "search_p99_ns": 21625, + "search_samples": 3472, + "sent_bytes": 1393154 + }, + { + "commit_ms": 270.297, + "kind": "korean", + "max_rss_bytes": 3235594240, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 4480.463, + "received_bytes": 45316887, + "redis_bytes": 56029392, + "search_p50_ns": 2917, + "search_p95_ns": 12875, + "search_p99_ns": 25333, + "search_samples": 4288, + "sent_bytes": 10260072 + }, + { + "commit_ms": 269.19, + "kind": "korean", + "max_rss_bytes": 3247095808, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 4509.287, + "received_bytes": 45316960, + "redis_bytes": 56746512, + "search_p50_ns": 3125, + "search_p95_ns": 8500, + "search_p99_ns": 26375, + "search_samples": 4114, + "sent_bytes": 10237173 + }, + { + "commit_ms": 1196.313, + "kind": "korean", + "max_rss_bytes": 3247095808, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 6106.441, + "received_bytes": 70598524, + "redis_bytes": 96311712, + "search_p50_ns": 2792, + "search_p95_ns": 13792, + "search_p99_ns": 28083, + "search_samples": 5793, + "sent_bytes": 38738807 + }, + { + "commit_ms": 1115.386, + "kind": "korean", + "max_rss_bytes": 3247095808, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 5715.564, + "received_bytes": 70598458, + "redis_bytes": 97000864, + "search_p50_ns": 3125, + "search_p95_ns": 11500, + "search_p99_ns": 27708, + "search_samples": 5217, + "sent_bytes": 38498699 + }, + { + "commit_ms": 1853.722, + "kind": "korean", + "max_rss_bytes": 3247095808, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 7714.14, + "received_bytes": 75499514, + "redis_bytes": 141137664, + "search_p50_ns": 3041, + "search_p95_ns": 12375, + "search_p99_ns": 30084, + "search_samples": 7146, + "sent_bytes": 44040770 + } + ], + "startup_ms": 2881.431, + "prune": { + "ms": 80.338, + "generations": 8, + "chunks": 8726, + "redis_bytes": 45965968 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=korean seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788632141210093 uptime_in_seconds:1433 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10247245 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 637.202, + "kind": "shared", + "max_rss_bytes": 449544192, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 1837.321, + "received_bytes": 25923960, + "redis_bytes": 30889456, + "search_p50_ns": 416, + "search_p95_ns": 1125, + "search_p99_ns": 7375, + "search_samples": 1789, + "sent_bytes": 30357955 + }, + { + "commit_ms": 608.145, + "kind": "shared", + "max_rss_bytes": 532889600, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 608.216, + "received_bytes": 25742780, + "redis_bytes": 30914432, + "search_p50_ns": 2208, + "search_p95_ns": 3167, + "search_p99_ns": 11584, + "search_samples": 574, + "sent_bytes": 684703 + }, + { + "commit_ms": 5.759, + "kind": "shared", + "max_rss_bytes": 697991168, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1206.02, + "received_bytes": 26445270, + "redis_bytes": 31672416, + "search_p50_ns": 2334, + "search_p95_ns": 3500, + "search_p99_ns": 13750, + "search_samples": 1164, + "sent_bytes": 1390903 + }, + { + "commit_ms": 6.295, + "kind": "shared", + "max_rss_bytes": 928808960, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1206.45, + "received_bytes": 26445276, + "redis_bytes": 32377872, + "search_p50_ns": 2291, + "search_p95_ns": 3375, + "search_p99_ns": 11625, + "search_samples": 1170, + "sent_bytes": 1390883 + }, + { + "commit_ms": 215.836, + "kind": "shared", + "max_rss_bytes": 945504256, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1406.124, + "received_bytes": 31909767, + "redis_bytes": 39283424, + "search_p50_ns": 2250, + "search_p95_ns": 3459, + "search_p99_ns": 11500, + "search_samples": 1363, + "sent_bytes": 7852969 + }, + { + "commit_ms": 214.655, + "kind": "shared", + "max_rss_bytes": 996376576, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1394.912, + "received_bytes": 31909768, + "redis_bytes": 39997824, + "search_p50_ns": 2333, + "search_p95_ns": 3333, + "search_p99_ns": 9750, + "search_samples": 1352, + "sent_bytes": 7829969 + }, + { + "commit_ms": 872.128, + "kind": "shared", + "max_rss_bytes": 997507072, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2082.284, + "received_bytes": 49561863, + "redis_bytes": 67082352, + "search_p50_ns": 2458, + "search_p95_ns": 3416, + "search_p99_ns": 10084, + "search_samples": 2051, + "sent_bytes": 28702048 + }, + { + "commit_ms": 870.317, + "kind": "shared", + "max_rss_bytes": 997588992, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2050.505, + "received_bytes": 49561721, + "redis_bytes": 67785296, + "search_p50_ns": 2375, + "search_p95_ns": 3167, + "search_p99_ns": 7000, + "search_samples": 2033, + "sent_bytes": 28461846 + }, + { + "commit_ms": 1067.41, + "kind": "shared", + "max_rss_bytes": 1289256960, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 2877.575, + "received_bytes": 53497836, + "redis_bytes": 98893120, + "search_p50_ns": 2333, + "search_p95_ns": 3500, + "search_p99_ns": 12041, + "search_samples": 2784, + "sent_bytes": 33038661 + } + ], + "startup_ms": 1188.255, + "prune": { + "ms": 33.476, + "generations": 8, + "chunks": 8726, + "redis_bytes": 32943424 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788631868036382 uptime_in_seconds:1160 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10246972 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "redis", + "n": 1000000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 629.653, + "kind": "shared", + "max_rss_bytes": 487243776, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 1809.969, + "received_bytes": 25923885, + "redis_bytes": 30905248, + "search_p50_ns": 417, + "search_p95_ns": 958, + "search_p99_ns": 5958, + "search_samples": 1772, + "sent_bytes": 30357854 + }, + { + "commit_ms": 610.081, + "kind": "shared", + "max_rss_bytes": 551960576, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 610.16, + "received_bytes": 25742782, + "redis_bytes": 30912832, + "search_p50_ns": 2209, + "search_p95_ns": 2958, + "search_p99_ns": 11000, + "search_samples": 587, + "sent_bytes": 684703 + }, + { + "commit_ms": 5.73, + "kind": "shared", + "max_rss_bytes": 765067264, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1196.033, + "received_bytes": 26445201, + "redis_bytes": 31670832, + "search_p50_ns": 2292, + "search_p95_ns": 3583, + "search_p99_ns": 10500, + "search_samples": 1151, + "sent_bytes": 1390802 + }, + { + "commit_ms": 6.352, + "kind": "shared", + "max_rss_bytes": 964411392, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1186.528, + "received_bytes": 26445273, + "redis_bytes": 32376272, + "search_p50_ns": 2375, + "search_p95_ns": 3250, + "search_p99_ns": 9792, + "search_samples": 1145, + "sent_bytes": 1390883 + }, + { + "commit_ms": 215.674, + "kind": "shared", + "max_rss_bytes": 985874432, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1395.915, + "received_bytes": 31909697, + "redis_bytes": 39295312, + "search_p50_ns": 2292, + "search_p95_ns": 3500, + "search_p99_ns": 10500, + "search_samples": 1350, + "sent_bytes": 7852868 + }, + { + "commit_ms": 215.694, + "kind": "shared", + "max_rss_bytes": 1002602496, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1576, + "received_bytes": 31909698, + "redis_bytes": 39975968, + "search_p50_ns": 2625, + "search_p95_ns": 9583, + "search_p99_ns": 17167, + "search_samples": 1541, + "sent_bytes": 7829868 + }, + { + "commit_ms": 870.56, + "kind": "shared", + "max_rss_bytes": 1002995712, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2070.742, + "received_bytes": 49561793, + "redis_bytes": 67081536, + "search_p50_ns": 2334, + "search_p95_ns": 3125, + "search_p99_ns": 7958, + "search_samples": 2051, + "sent_bytes": 28701947 + }, + { + "commit_ms": 873.025, + "kind": "shared", + "max_rss_bytes": 1003028480, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2063.277, + "received_bytes": 49561717, + "redis_bytes": 67784496, + "search_p50_ns": 2333, + "search_p95_ns": 3084, + "search_p99_ns": 7708, + "search_samples": 2015, + "sent_bytes": 28461846 + }, + { + "commit_ms": 1066.502, + "kind": "shared", + "max_rss_bytes": 1285505024, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 2856.697, + "received_bytes": 53497839, + "redis_bytes": 98869536, + "search_p50_ns": 2333, + "search_p95_ns": 3458, + "search_p99_ns": 9750, + "search_samples": 2799, + "sent_bytes": 33038661 + } + ], + "startup_ms": 1232.694, + "prune": { + "ms": 32.382, + "generations": 8, + "chunks": 8726, + "redis_bytes": 32941104 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=shared seed=20260906 server=# Server redis_version:8.10.1 redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:81870544b30d700a redis_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue atomicvar_api:c11-builtin gcc_version:4.2.1 process_id:18977 process_supervised:no run_id:d56dd79067479e69d227fbcbbd602046694f8b84 tcp_port:16389 server_time_usec:1788632098131214 uptime_in_seconds:1390 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:10247202 executable:/Users/lukas/Workspace/acor/redis-server config_file: io_threads_active:0 listener0:name=tcp,bind=127.0.0.1,port=16389", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 265.345, + "kind": "diverse", + "max_rss_bytes": 39239680, + "n": 10000, + "operation": "initial_load", + "ready_ms": 515.499, + "received_bytes": 1014768, + "redis_bytes": 4119664, + "search_p50_ns": 334, + "search_p95_ns": 875, + "search_p99_ns": 5375, + "search_samples": 515, + "sent_bytes": 5032607 + }, + { + "commit_ms": 247.506, + "kind": "diverse", + "max_rss_bytes": 47857664, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 247.607, + "received_bytes": 835156, + "redis_bytes": 4121104, + "search_p50_ns": 1542, + "search_p95_ns": 2292, + "search_p99_ns": 4083, + "search_samples": 244, + "sent_bytes": 622410 + }, + { + "commit_ms": 4.751, + "kind": "diverse", + "max_rss_bytes": 58458112, + "n": 10000, + "operation": "add_1", + "ready_ms": 255.047, + "received_bytes": 1476218, + "redis_bytes": 4759408, + "search_p50_ns": 1667, + "search_p95_ns": 2167, + "search_p99_ns": 4833, + "search_samples": 252, + "sent_bytes": 1267356 + }, + { + "commit_ms": 4.706, + "kind": "diverse", + "max_rss_bytes": 78053376, + "n": 10000, + "operation": "remove_1", + "ready_ms": 255.054, + "received_bytes": 1476289, + "redis_bytes": 5437136, + "search_p50_ns": 1666, + "search_p95_ns": 2125, + "search_p99_ns": 8666, + "search_samples": 252, + "sent_bytes": 1267436 + }, + { + "commit_ms": 123.813, + "kind": "diverse", + "max_rss_bytes": 78643200, + "n": 10000, + "operation": "add_1000", + "ready_ms": 383.941, + "received_bytes": 1552960, + "redis_bytes": 6406464, + "search_p50_ns": 1708, + "search_p95_ns": 2083, + "search_p99_ns": 3000, + "search_samples": 380, + "sent_bytes": 2341614 + }, + { + "commit_ms": 121.308, + "kind": "diverse", + "max_rss_bytes": 78823424, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 371.469, + "received_bytes": 1552768, + "redis_bytes": 7064800, + "search_p50_ns": 1709, + "search_p95_ns": 2167, + "search_p99_ns": 5083, + "search_samples": 369, + "sent_bytes": 2246289 + }, + { + "commit_ms": 17.774, + "kind": "diverse", + "max_rss_bytes": 79577088, + "n": 10000, + "operation": "add_100", + "ready_ms": 268.091, + "received_bytes": 1484547, + "redis_bytes": 7780944, + "search_p50_ns": 1750, + "search_p95_ns": 2583, + "search_p99_ns": 8833, + "search_samples": 261, + "sent_bytes": 1386763 + }, + { + "commit_ms": 17.29, + "kind": "diverse", + "max_rss_bytes": 79970304, + "n": 10000, + "operation": "remove_100", + "ready_ms": 257.426, + "received_bytes": 1484515, + "redis_bytes": 8416896, + "search_p50_ns": 1667, + "search_p95_ns": 5000, + "search_p99_ns": 12250, + "search_samples": 251, + "sent_bytes": 1375780 + }, + { + "commit_ms": 500.453, + "kind": "diverse", + "max_rss_bytes": 80134144, + "n": 10000, + "operation": "full_replace", + "ready_ms": 750.631, + "received_bytes": 1705857, + "redis_bytes": 10594576, + "search_p50_ns": 1584, + "search_p95_ns": 2125, + "search_p99_ns": 3375, + "search_samples": 749, + "sent_bytes": 5709273 + } + ], + "startup_ms": 246.91, + "prune": { + "ms": 16.285, + "generations": 8, + "chunks": 4716, + "redis_bytes": 4072944 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788631955690213 uptime_in_seconds:576 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247059 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 263.289, + "kind": "diverse", + "max_rss_bytes": 40402944, + "n": 10000, + "operation": "initial_load", + "ready_ms": 513.432, + "received_bytes": 1014769, + "redis_bytes": 4098144, + "search_p50_ns": 334, + "search_p95_ns": 917, + "search_p99_ns": 8083, + "search_samples": 513, + "sent_bytes": 5032607 + }, + { + "commit_ms": 231.348, + "kind": "diverse", + "max_rss_bytes": 48955392, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 231.452, + "received_bytes": 835151, + "redis_bytes": 4120048, + "search_p50_ns": 1625, + "search_p95_ns": 2667, + "search_p99_ns": 5458, + "search_samples": 231, + "sent_bytes": 622410 + }, + { + "commit_ms": 4.713, + "kind": "diverse", + "max_rss_bytes": 57982976, + "n": 10000, + "operation": "add_1", + "ready_ms": 254.963, + "received_bytes": 1476213, + "redis_bytes": 4758416, + "search_p50_ns": 1666, + "search_p95_ns": 2417, + "search_p99_ns": 7625, + "search_samples": 251, + "sent_bytes": 1267356 + }, + { + "commit_ms": 4.895, + "kind": "diverse", + "max_rss_bytes": 77873152, + "n": 10000, + "operation": "remove_1", + "ready_ms": 255.207, + "received_bytes": 1476218, + "redis_bytes": 5436144, + "search_p50_ns": 1500, + "search_p95_ns": 3375, + "search_p99_ns": 8125, + "search_samples": 253, + "sent_bytes": 1267335 + }, + { + "commit_ms": 118.846, + "kind": "diverse", + "max_rss_bytes": 78675968, + "n": 10000, + "operation": "add_1000", + "ready_ms": 369.123, + "received_bytes": 1553033, + "redis_bytes": 6405424, + "search_p50_ns": 1708, + "search_p95_ns": 2208, + "search_p99_ns": 9417, + "search_samples": 368, + "sent_bytes": 2341715 + }, + { + "commit_ms": 117.627, + "kind": "diverse", + "max_rss_bytes": 78708736, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 367.818, + "received_bytes": 1552841, + "redis_bytes": 7063760, + "search_p50_ns": 1625, + "search_p95_ns": 2125, + "search_p99_ns": 8625, + "search_samples": 367, + "sent_bytes": 2246390 + }, + { + "commit_ms": 17.008, + "kind": "diverse", + "max_rss_bytes": 78807040, + "n": 10000, + "operation": "add_100", + "ready_ms": 267.247, + "received_bytes": 1484542, + "redis_bytes": 7759232, + "search_p50_ns": 1750, + "search_p95_ns": 2792, + "search_p99_ns": 7333, + "search_samples": 264, + "sent_bytes": 1386763 + }, + { + "commit_ms": 17.105, + "kind": "diverse", + "max_rss_bytes": 79151104, + "n": 10000, + "operation": "remove_100", + "ready_ms": 267.278, + "received_bytes": 1484588, + "redis_bytes": 8415584, + "search_p50_ns": 1708, + "search_p95_ns": 2417, + "search_p99_ns": 7875, + "search_samples": 263, + "sent_bytes": 1375881 + }, + { + "commit_ms": 494.845, + "kind": "diverse", + "max_rss_bytes": 79298560, + "n": 10000, + "operation": "full_replace", + "ready_ms": 745.114, + "received_bytes": 1705930, + "redis_bytes": 10594320, + "search_p50_ns": 1708, + "search_p95_ns": 2292, + "search_p99_ns": 7167, + "search_samples": 738, + "sent_bytes": 5709374 + } + ], + "startup_ms": 244.037, + "prune": { + "ms": 15.688, + "generations": 8, + "chunks": 4716, + "redis_bytes": 4073552 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788632188945427 uptime_in_seconds:809 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247292 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 267.754, + "kind": "korean", + "max_rss_bytes": 65634304, + "n": 10000, + "operation": "initial_load", + "ready_ms": 528.143, + "received_bytes": 1220996, + "redis_bytes": 4301104, + "search_p50_ns": 625, + "search_p95_ns": 1166, + "search_p99_ns": 8334, + "search_samples": 528, + "sent_bytes": 5274738 + }, + { + "commit_ms": 237.446, + "kind": "korean", + "max_rss_bytes": 73547776, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 237.509, + "received_bytes": 1041255, + "redis_bytes": 4323008, + "search_p50_ns": 2083, + "search_p95_ns": 2542, + "search_p99_ns": 2750, + "search_samples": 237, + "sent_bytes": 627722 + }, + { + "commit_ms": 5.034, + "kind": "korean", + "max_rss_bytes": 99123200, + "n": 10000, + "operation": "add_1", + "ready_ms": 265.273, + "received_bytes": 1686426, + "redis_bytes": 4961392, + "search_p50_ns": 2167, + "search_p95_ns": 3042, + "search_p99_ns": 9250, + "search_samples": 261, + "sent_bytes": 1276777 + }, + { + "commit_ms": 5.032, + "kind": "korean", + "max_rss_bytes": 118194176, + "n": 10000, + "operation": "remove_1", + "ready_ms": 265.232, + "received_bytes": 1686419, + "redis_bytes": 5639168, + "search_p50_ns": 2250, + "search_p95_ns": 3333, + "search_p99_ns": 9791, + "search_samples": 264, + "sent_bytes": 1276757 + }, + { + "commit_ms": 125.552, + "kind": "korean", + "max_rss_bytes": 134987776, + "n": 10000, + "operation": "add_1000", + "ready_ms": 395.789, + "received_bytes": 1807451, + "redis_bytes": 6658576, + "search_p50_ns": 1917, + "search_p95_ns": 7333, + "search_p99_ns": 12291, + "search_samples": 392, + "sent_bytes": 2395347 + }, + { + "commit_ms": 112.357, + "kind": "korean", + "max_rss_bytes": 138854400, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 362.501, + "received_bytes": 1807159, + "redis_bytes": 7314864, + "search_p50_ns": 1917, + "search_p95_ns": 2875, + "search_p99_ns": 4458, + "search_samples": 357, + "sent_bytes": 2292298 + }, + { + "commit_ms": 18.427, + "kind": "korean", + "max_rss_bytes": 139051008, + "n": 10000, + "operation": "add_100", + "ready_ms": 278.611, + "received_bytes": 1699776, + "redis_bytes": 8018480, + "search_p50_ns": 2042, + "search_p95_ns": 3167, + "search_p99_ns": 14250, + "search_samples": 274, + "sent_bytes": 1401205 + }, + { + "commit_ms": 17.611, + "kind": "korean", + "max_rss_bytes": 139051008, + "n": 10000, + "operation": "remove_100", + "ready_ms": 287.823, + "received_bytes": 1699884, + "redis_bytes": 8674832, + "search_p50_ns": 2125, + "search_p95_ns": 3000, + "search_p99_ns": 6791, + "search_samples": 280, + "sent_bytes": 1389343 + }, + { + "commit_ms": 517.051, + "kind": "korean", + "max_rss_bytes": 139132928, + "n": 10000, + "operation": "full_replace", + "ready_ms": 787.239, + "received_bytes": 2112717, + "redis_bytes": 11068240, + "search_p50_ns": 2125, + "search_p95_ns": 2875, + "search_p99_ns": 7083, + "search_samples": 787, + "sent_bytes": 5907521 + } + ], + "startup_ms": 260.401, + "prune": { + "ms": 16.401, + "generations": 8, + "chunks": 4748, + "redis_bytes": 4267440 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788631959521252 uptime_in_seconds:580 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247063 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 266.024, + "kind": "korean", + "max_rss_bytes": 63127552, + "n": 10000, + "operation": "initial_load", + "ready_ms": 526.282, + "received_bytes": 1220992, + "redis_bytes": 4321968, + "search_p50_ns": 583, + "search_p95_ns": 1000, + "search_p99_ns": 9375, + "search_samples": 524, + "sent_bytes": 5274738 + }, + { + "commit_ms": 238.83, + "kind": "korean", + "max_rss_bytes": 80691200, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 238.906, + "received_bytes": 1041256, + "redis_bytes": 4323408, + "search_p50_ns": 2208, + "search_p95_ns": 3125, + "search_p99_ns": 4208, + "search_samples": 238, + "sent_bytes": 627722 + }, + { + "commit_ms": 5.196, + "kind": "korean", + "max_rss_bytes": 106774528, + "n": 10000, + "operation": "add_1", + "ready_ms": 265.34, + "received_bytes": 1686427, + "redis_bytes": 4961744, + "search_p50_ns": 2208, + "search_p95_ns": 2958, + "search_p99_ns": 9917, + "search_samples": 261, + "sent_bytes": 1276777 + }, + { + "commit_ms": 5.013, + "kind": "korean", + "max_rss_bytes": 114130944, + "n": 10000, + "operation": "remove_1", + "ready_ms": 265.253, + "received_bytes": 1686426, + "redis_bytes": 5639472, + "search_p50_ns": 2125, + "search_p95_ns": 3000, + "search_p99_ns": 10083, + "search_samples": 261, + "sent_bytes": 1276757 + }, + { + "commit_ms": 116.952, + "kind": "korean", + "max_rss_bytes": 117211136, + "n": 10000, + "operation": "add_1000", + "ready_ms": 387.233, + "received_bytes": 1807452, + "redis_bytes": 6659088, + "search_p50_ns": 2167, + "search_p95_ns": 2958, + "search_p99_ns": 10333, + "search_samples": 382, + "sent_bytes": 2395347 + }, + { + "commit_ms": 116.978, + "kind": "korean", + "max_rss_bytes": 118456320, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 377.273, + "received_bytes": 1807160, + "redis_bytes": 7317424, + "search_p50_ns": 2125, + "search_p95_ns": 2791, + "search_p99_ns": 9125, + "search_samples": 372, + "sent_bytes": 2292298 + }, + { + "commit_ms": 17.149, + "kind": "korean", + "max_rss_bytes": 119996416, + "n": 10000, + "operation": "add_100", + "ready_ms": 277.251, + "received_bytes": 1699777, + "redis_bytes": 8039408, + "search_p50_ns": 2208, + "search_p95_ns": 2917, + "search_p99_ns": 6125, + "search_samples": 276, + "sent_bytes": 1401205 + }, + { + "commit_ms": 17.244, + "kind": "korean", + "max_rss_bytes": 126763008, + "n": 10000, + "operation": "remove_100", + "ready_ms": 277.365, + "received_bytes": 1699813, + "redis_bytes": 8675424, + "search_p50_ns": 2083, + "search_p95_ns": 3083, + "search_p99_ns": 8500, + "search_samples": 272, + "sent_bytes": 1389242 + }, + { + "commit_ms": 502.406, + "kind": "korean", + "max_rss_bytes": 149897216, + "n": 10000, + "operation": "full_replace", + "ready_ms": 762.623, + "received_bytes": 2112646, + "redis_bytes": 11067376, + "search_p50_ns": 2084, + "search_p95_ns": 2666, + "search_p99_ns": 3458, + "search_samples": 760, + "sent_bytes": 5907420 + } + ], + "startup_ms": 259.078, + "prune": { + "ms": 15.823, + "generations": 8, + "chunks": 4748, + "redis_bytes": 4287664 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788632192728261 uptime_in_seconds:813 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247296 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 272.483, + "kind": "shared", + "max_rss_bytes": 26001408, + "n": 10000, + "operation": "initial_load", + "ready_ms": 512.67, + "received_bytes": 1108437, + "redis_bytes": 4199344, + "search_p50_ns": 375, + "search_p95_ns": 791, + "search_p99_ns": 2500, + "search_samples": 512, + "sent_bytes": 5148734 + }, + { + "commit_ms": 234.619, + "kind": "shared", + "max_rss_bytes": 26886144, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 234.703, + "received_bytes": 928759, + "redis_bytes": 4200768, + "search_p50_ns": 1625, + "search_p95_ns": 2083, + "search_p99_ns": 2792, + "search_samples": 233, + "sent_bytes": 625730 + }, + { + "commit_ms": 5.097, + "kind": "shared", + "max_rss_bytes": 29949952, + "n": 10000, + "operation": "add_1", + "ready_ms": 245.249, + "received_bytes": 1572308, + "redis_bytes": 4839008, + "search_p50_ns": 1625, + "search_p95_ns": 2041, + "search_p99_ns": 4875, + "search_samples": 243, + "sent_bytes": 1273163 + }, + { + "commit_ms": 5.161, + "kind": "shared", + "max_rss_bytes": 32522240, + "n": 10000, + "operation": "remove_1", + "ready_ms": 245.294, + "received_bytes": 1572310, + "redis_bytes": 5537264, + "search_p50_ns": 1625, + "search_p95_ns": 2125, + "search_p99_ns": 6209, + "search_samples": 245, + "sent_bytes": 1273143 + }, + { + "commit_ms": 118.064, + "kind": "shared", + "max_rss_bytes": 32768000, + "n": 10000, + "operation": "add_1000", + "ready_ms": 348.271, + "received_bytes": 1672313, + "redis_bytes": 6529808, + "search_p50_ns": 1541, + "search_p95_ns": 1916, + "search_p99_ns": 2292, + "search_samples": 348, + "sent_bytes": 2370681 + }, + { + "commit_ms": 112.659, + "kind": "shared", + "max_rss_bytes": 33603584, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 342.916, + "received_bytes": 1671969, + "redis_bytes": 7186096, + "search_p50_ns": 1459, + "search_p95_ns": 2000, + "search_p99_ns": 5334, + "search_samples": 341, + "sent_bytes": 2253398 + }, + { + "commit_ms": 17.31, + "kind": "shared", + "max_rss_bytes": 33603584, + "n": 10000, + "operation": "add_100", + "ready_ms": 247.453, + "received_bytes": 1583321, + "redis_bytes": 7884544, + "search_p50_ns": 1458, + "search_p95_ns": 1917, + "search_p99_ns": 4333, + "search_samples": 245, + "sent_bytes": 1395285 + }, + { + "commit_ms": 17.197, + "kind": "shared", + "max_rss_bytes": 34848768, + "n": 10000, + "operation": "remove_100", + "ready_ms": 247.308, + "received_bytes": 1583222, + "redis_bytes": 8540960, + "search_p50_ns": 1541, + "search_p95_ns": 2125, + "search_p99_ns": 6000, + "search_samples": 245, + "sent_bytes": 1384216 + }, + { + "commit_ms": 470.708, + "kind": "shared", + "max_rss_bytes": 35078144, + "n": 10000, + "operation": "full_replace", + "ready_ms": 700.951, + "received_bytes": 1886599, + "redis_bytes": 10807760, + "search_p50_ns": 1500, + "search_p95_ns": 2000, + "search_p99_ns": 6417, + "search_samples": 700, + "sent_bytes": 5770557 + } + ], + "startup_ms": 238.835, + "prune": { + "ms": 16.197, + "generations": 8, + "chunks": 4736, + "redis_bytes": 4161360 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788631952057534 uptime_in_seconds:573 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247056 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 10000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 273.228, + "kind": "shared", + "max_rss_bytes": 25346048, + "n": 10000, + "operation": "initial_load", + "ready_ms": 513.337, + "received_bytes": 1108438, + "redis_bytes": 4178672, + "search_p50_ns": 416, + "search_p95_ns": 959, + "search_p99_ns": 4167, + "search_samples": 511, + "sent_bytes": 5148734 + }, + { + "commit_ms": 233.771, + "kind": "shared", + "max_rss_bytes": 27295744, + "n": 10000, + "operation": "identical_replace", + "ready_ms": 233.851, + "received_bytes": 928761, + "redis_bytes": 4200576, + "search_p50_ns": 1583, + "search_p95_ns": 1917, + "search_p99_ns": 2583, + "search_samples": 233, + "sent_bytes": 625730 + }, + { + "commit_ms": 5.423, + "kind": "shared", + "max_rss_bytes": 29999104, + "n": 10000, + "operation": "add_1", + "ready_ms": 255.585, + "received_bytes": 1572311, + "redis_bytes": 4859280, + "search_p50_ns": 1584, + "search_p95_ns": 6417, + "search_p99_ns": 8833, + "search_samples": 252, + "sent_bytes": 1273163 + }, + { + "commit_ms": 5.023, + "kind": "shared", + "max_rss_bytes": 32358400, + "n": 10000, + "operation": "remove_1", + "ready_ms": 245.259, + "received_bytes": 1572383, + "redis_bytes": 5495184, + "search_p50_ns": 1666, + "search_p95_ns": 2167, + "search_p99_ns": 4500, + "search_samples": 243, + "sent_bytes": 1273244 + }, + { + "commit_ms": 118.446, + "kind": "shared", + "max_rss_bytes": 33783808, + "n": 10000, + "operation": "add_1000", + "ready_ms": 369.25, + "received_bytes": 1672313, + "redis_bytes": 6528224, + "search_p50_ns": 1625, + "search_p95_ns": 2250, + "search_p99_ns": 8625, + "search_samples": 369, + "sent_bytes": 2370681 + }, + { + "commit_ms": 116.206, + "kind": "shared", + "max_rss_bytes": 34160640, + "n": 10000, + "operation": "remove_1000", + "ready_ms": 356.295, + "received_bytes": 1672114, + "redis_bytes": 7166096, + "search_p50_ns": 1708, + "search_p95_ns": 2250, + "search_p99_ns": 3625, + "search_samples": 354, + "sent_bytes": 2253600 + }, + { + "commit_ms": 17.531, + "kind": "shared", + "max_rss_bytes": 35274752, + "n": 10000, + "operation": "add_100", + "ready_ms": 257.691, + "received_bytes": 1583399, + "redis_bytes": 7864176, + "search_p50_ns": 1666, + "search_p95_ns": 2083, + "search_p99_ns": 6375, + "search_samples": 257, + "sent_bytes": 1395386 + }, + { + "commit_ms": 16.007, + "kind": "shared", + "max_rss_bytes": 35323904, + "n": 10000, + "operation": "remove_100", + "ready_ms": 226.103, + "received_bytes": 1583079, + "redis_bytes": 8541072, + "search_p50_ns": 1500, + "search_p95_ns": 2083, + "search_p99_ns": 4500, + "search_samples": 223, + "sent_bytes": 1384014 + }, + { + "commit_ms": 499.408, + "kind": "shared", + "max_rss_bytes": 35389440, + "n": 10000, + "operation": "full_replace", + "ready_ms": 739.589, + "received_bytes": 1886744, + "redis_bytes": 10788208, + "search_p50_ns": 1625, + "search_p95_ns": 2125, + "search_p99_ns": 4083, + "search_samples": 739, + "sent_bytes": 5770759 + } + ], + "startup_ms": 238.136, + "prune": { + "ms": 16.157, + "generations": 8, + "chunks": 4736, + "redis_bytes": 4141824 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=10000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788632185241080 uptime_in_seconds:806 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247289 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 320.803, + "kind": "diverse", + "max_rss_bytes": 164855808, + "n": 100000, + "operation": "initial_load", + "ready_ms": 741.103, + "received_bytes": 2514923, + "redis_bytes": 5809424, + "search_p50_ns": 375, + "search_p95_ns": 1084, + "search_p99_ns": 8334, + "search_samples": 736, + "sent_bytes": 6949053 + }, + { + "commit_ms": 296.648, + "kind": "diverse", + "max_rss_bytes": 187138048, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 296.728, + "received_bytes": 2333820, + "redis_bytes": 5810864, + "search_p50_ns": 1916, + "search_p95_ns": 2625, + "search_p99_ns": 5416, + "search_samples": 283, + "sent_bytes": 684097 + }, + { + "commit_ms": 5.497, + "kind": "diverse", + "max_rss_bytes": 293650432, + "n": 100000, + "operation": "add_1", + "ready_ms": 425.736, + "received_bytes": 3026816, + "redis_bytes": 6520224, + "search_p50_ns": 2000, + "search_p95_ns": 4209, + "search_p99_ns": 14375, + "search_samples": 423, + "sent_bytes": 1380977 + }, + { + "commit_ms": 5.245, + "kind": "diverse", + "max_rss_bytes": 337395712, + "n": 100000, + "operation": "remove_1", + "ready_ms": 435.389, + "received_bytes": 3026745, + "redis_bytes": 7225664, + "search_p50_ns": 2000, + "search_p95_ns": 3667, + "search_p99_ns": 10375, + "search_samples": 429, + "sent_bytes": 1380856 + }, + { + "commit_ms": 130.467, + "kind": "diverse", + "max_rss_bytes": 373637120, + "n": 100000, + "operation": "add_1000", + "ready_ms": 560.681, + "received_bytes": 3406546, + "redis_bytes": 8644528, + "search_p50_ns": 2000, + "search_p95_ns": 2792, + "search_p99_ns": 10917, + "search_samples": 545, + "sent_bytes": 2758217 + }, + { + "commit_ms": 127.864, + "kind": "diverse", + "max_rss_bytes": 407355392, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 538.131, + "received_bytes": 3406546, + "redis_bytes": 9329504, + "search_p50_ns": 2125, + "search_p95_ns": 2917, + "search_p99_ns": 9208, + "search_samples": 516, + "sent_bytes": 2735217 + }, + { + "commit_ms": 127.907, + "kind": "diverse", + "max_rss_bytes": 421085184, + "n": 100000, + "operation": "add_1000", + "ready_ms": 548.237, + "received_bytes": 3406539, + "redis_bytes": 10034944, + "search_p50_ns": 2000, + "search_p95_ns": 3333, + "search_p99_ns": 10583, + "search_samples": 541, + "sent_bytes": 2758217 + }, + { + "commit_ms": 128.743, + "kind": "diverse", + "max_rss_bytes": 421134336, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 539.068, + "received_bytes": 3406548, + "redis_bytes": 10760912, + "search_p50_ns": 2084, + "search_p95_ns": 3000, + "search_p99_ns": 9292, + "search_samples": 516, + "sent_bytes": 2735217 + }, + { + "commit_ms": 598.554, + "kind": "diverse", + "max_rss_bytes": 457326592, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1068.773, + "received_bytes": 4880395, + "redis_bytes": 14851744, + "search_p50_ns": 1917, + "search_p95_ns": 2833, + "search_p99_ns": 10958, + "search_samples": 1049, + "sent_bytes": 7829456 + } + ], + "startup_ms": 414.022, + "prune": { + "ms": 20.536, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6003552 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788631968473797 uptime_in_seconds:589 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247072 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 326.509, + "kind": "diverse", + "max_rss_bytes": 161906688, + "n": 100000, + "operation": "initial_load", + "ready_ms": 736.702, + "received_bytes": 2514925, + "redis_bytes": 5808928, + "search_p50_ns": 416, + "search_p95_ns": 1375, + "search_p99_ns": 4916, + "search_samples": 736, + "sent_bytes": 6949053 + }, + { + "commit_ms": 306.112, + "kind": "diverse", + "max_rss_bytes": 218234880, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 306.202, + "received_bytes": 2333821, + "redis_bytes": 5810368, + "search_p50_ns": 2000, + "search_p95_ns": 4875, + "search_p99_ns": 11208, + "search_samples": 306, + "sent_bytes": 684097 + }, + { + "commit_ms": 5.129, + "kind": "diverse", + "max_rss_bytes": 290832384, + "n": 100000, + "operation": "add_1", + "ready_ms": 425.233, + "received_bytes": 3026745, + "redis_bytes": 6518720, + "search_p50_ns": 1916, + "search_p95_ns": 3584, + "search_p99_ns": 10500, + "search_samples": 405, + "sent_bytes": 1380876 + }, + { + "commit_ms": 5.221, + "kind": "diverse", + "max_rss_bytes": 417415168, + "n": 100000, + "operation": "remove_1", + "ready_ms": 415.34, + "received_bytes": 3026751, + "redis_bytes": 7224160, + "search_p50_ns": 2042, + "search_p95_ns": 3333, + "search_p99_ns": 12125, + "search_samples": 399, + "sent_bytes": 1380856 + }, + { + "commit_ms": 129.748, + "kind": "diverse", + "max_rss_bytes": 418775040, + "n": 100000, + "operation": "add_1000", + "ready_ms": 540.029, + "received_bytes": 3406547, + "redis_bytes": 8620656, + "search_p50_ns": 2000, + "search_p95_ns": 2709, + "search_p99_ns": 8625, + "search_samples": 536, + "sent_bytes": 2758217 + }, + { + "commit_ms": 128.114, + "kind": "diverse", + "max_rss_bytes": 431144960, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 538.254, + "received_bytes": 3406547, + "redis_bytes": 9305696, + "search_p50_ns": 2000, + "search_p95_ns": 2625, + "search_p99_ns": 8625, + "search_samples": 519, + "sent_bytes": 2735217 + }, + { + "commit_ms": 128.094, + "kind": "diverse", + "max_rss_bytes": 431177728, + "n": 100000, + "operation": "add_1000", + "ready_ms": 538.267, + "received_bytes": 3406545, + "redis_bytes": 10053168, + "search_p50_ns": 2167, + "search_p95_ns": 3458, + "search_p99_ns": 11125, + "search_samples": 519, + "sent_bytes": 2758217 + }, + { + "commit_ms": 128.451, + "kind": "diverse", + "max_rss_bytes": 431308800, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 538.602, + "received_bytes": 3406621, + "redis_bytes": 10740320, + "search_p50_ns": 2042, + "search_p95_ns": 2875, + "search_p99_ns": 8458, + "search_samples": 521, + "sent_bytes": 2735318 + }, + { + "commit_ms": 594.563, + "kind": "diverse", + "max_rss_bytes": 448102400, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1054.815, + "received_bytes": 4880396, + "redis_bytes": 14868912, + "search_p50_ns": 1917, + "search_p95_ns": 2542, + "search_p99_ns": 6541, + "search_samples": 1039, + "sent_bytes": 7829456 + } + ], + "startup_ms": 416.688, + "prune": { + "ms": 20.199, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6002304 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788632201652690 uptime_in_seconds:822 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247305 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 350.746, + "kind": "korean", + "max_rss_bytes": 346112000, + "n": 100000, + "operation": "initial_load", + "ready_ms": 921.007, + "received_bytes": 4516105, + "redis_bytes": 7953312, + "search_p50_ns": 625, + "search_p95_ns": 1416, + "search_p99_ns": 10250, + "search_samples": 912, + "sent_bytes": 8950292 + }, + { + "commit_ms": 322.032, + "kind": "korean", + "max_rss_bytes": 383516672, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 322.115, + "received_bytes": 4334928, + "redis_bytes": 7976752, + "search_p50_ns": 2291, + "search_p95_ns": 3417, + "search_p99_ns": 4833, + "search_samples": 307, + "sent_bytes": 684198 + }, + { + "commit_ms": 5.257, + "kind": "korean", + "max_rss_bytes": 613482496, + "n": 100000, + "operation": "add_1", + "ready_ms": 585.372, + "received_bytes": 5028518, + "redis_bytes": 8685904, + "search_p50_ns": 2542, + "search_p95_ns": 4459, + "search_p99_ns": 15875, + "search_samples": 561, + "sent_bytes": 1381673 + }, + { + "commit_ms": 5.703, + "kind": "korean", + "max_rss_bytes": 829538304, + "n": 100000, + "operation": "remove_1", + "ready_ms": 585.962, + "received_bytes": 5028452, + "redis_bytes": 9394944, + "search_p50_ns": 2583, + "search_p95_ns": 4208, + "search_p99_ns": 9708, + "search_samples": 562, + "sent_bytes": 1381552 + }, + { + "commit_ms": 141.348, + "kind": "korean", + "max_rss_bytes": 858947584, + "n": 100000, + "operation": "add_1000", + "ready_ms": 701.501, + "received_bytes": 5845008, + "redis_bytes": 11275936, + "search_p50_ns": 2708, + "search_p95_ns": 3834, + "search_p99_ns": 15125, + "search_samples": 683, + "sent_bytes": 3195643 + }, + { + "commit_ms": 142.408, + "kind": "korean", + "max_rss_bytes": 877117440, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 692.567, + "received_bytes": 5845011, + "redis_bytes": 11981440, + "search_p50_ns": 2708, + "search_p95_ns": 3917, + "search_p99_ns": 11209, + "search_samples": 670, + "sent_bytes": 3172636 + }, + { + "commit_ms": 139.768, + "kind": "korean", + "max_rss_bytes": 877281280, + "n": 100000, + "operation": "add_1000", + "ready_ms": 690.094, + "received_bytes": 5845012, + "redis_bytes": 12707424, + "search_p50_ns": 2750, + "search_p95_ns": 3875, + "search_p99_ns": 8125, + "search_samples": 666, + "sent_bytes": 3195643 + }, + { + "commit_ms": 141.685, + "kind": "korean", + "max_rss_bytes": 904364032, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 731.928, + "received_bytes": 5845006, + "redis_bytes": 13412928, + "search_p50_ns": 2584, + "search_p95_ns": 4792, + "search_p99_ns": 15375, + "search_samples": 707, + "sent_bytes": 3172636 + }, + { + "commit_ms": 618.082, + "kind": "korean", + "max_rss_bytes": 950353920, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1238.288, + "received_bytes": 8882786, + "redis_bytes": 19701008, + "search_p50_ns": 2583, + "search_p95_ns": 3666, + "search_p99_ns": 11750, + "search_samples": 1201, + "sent_bytes": 9830816 + } + ], + "startup_ms": 571.22, + "prune": { + "ms": 20.995, + "generations": 8, + "chunks": 4987, + "redis_bytes": 8205280 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788631974378230 uptime_in_seconds:595 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247078 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 351.293, + "kind": "korean", + "max_rss_bytes": 359645184, + "n": 100000, + "operation": "initial_load", + "ready_ms": 921.413, + "received_bytes": 4516035, + "redis_bytes": 7975392, + "search_p50_ns": 666, + "search_p95_ns": 1333, + "search_p99_ns": 10042, + "search_samples": 917, + "sent_bytes": 8950191 + }, + { + "commit_ms": 318.834, + "kind": "korean", + "max_rss_bytes": 383057920, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 318.9, + "received_bytes": 4334930, + "redis_bytes": 7977584, + "search_p50_ns": 2416, + "search_p95_ns": 3167, + "search_p99_ns": 4959, + "search_samples": 303, + "sent_bytes": 684198 + }, + { + "commit_ms": 5.22, + "kind": "korean", + "max_rss_bytes": 617693184, + "n": 100000, + "operation": "add_1", + "ready_ms": 565.327, + "received_bytes": 5028376, + "redis_bytes": 8709968, + "search_p50_ns": 2625, + "search_p95_ns": 3709, + "search_p99_ns": 13750, + "search_samples": 542, + "sent_bytes": 1381471 + }, + { + "commit_ms": 5.569, + "kind": "korean", + "max_rss_bytes": 870907904, + "n": 100000, + "operation": "remove_1", + "ready_ms": 575.792, + "received_bytes": 5028526, + "redis_bytes": 9394944, + "search_p50_ns": 2542, + "search_p95_ns": 4375, + "search_p99_ns": 12292, + "search_samples": 551, + "sent_bytes": 1381653 + }, + { + "commit_ms": 143.687, + "kind": "korean", + "max_rss_bytes": 888274944, + "n": 100000, + "operation": "add_1000", + "ready_ms": 683.966, + "received_bytes": 5845010, + "redis_bytes": 11278992, + "search_p50_ns": 2500, + "search_p95_ns": 3459, + "search_p99_ns": 10792, + "search_samples": 662, + "sent_bytes": 3195643 + }, + { + "commit_ms": 140.94, + "kind": "korean", + "max_rss_bytes": 888815616, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 701.23, + "received_bytes": 5845084, + "redis_bytes": 11984496, + "search_p50_ns": 2500, + "search_p95_ns": 3750, + "search_p99_ns": 12042, + "search_samples": 682, + "sent_bytes": 3172737 + }, + { + "commit_ms": 142.695, + "kind": "korean", + "max_rss_bytes": 911212544, + "n": 100000, + "operation": "add_1000", + "ready_ms": 692.974, + "received_bytes": 5845014, + "redis_bytes": 12689952, + "search_p50_ns": 2542, + "search_p95_ns": 3875, + "search_p99_ns": 10458, + "search_samples": 668, + "sent_bytes": 3195643 + }, + { + "commit_ms": 141.492, + "kind": "korean", + "max_rss_bytes": 911228928, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 691.666, + "received_bytes": 5845086, + "redis_bytes": 13395456, + "search_p50_ns": 2625, + "search_p95_ns": 3500, + "search_p99_ns": 8417, + "search_samples": 670, + "sent_bytes": 3172737 + }, + { + "commit_ms": 637.92, + "kind": "korean", + "max_rss_bytes": 911261696, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1248.23, + "received_bytes": 8882866, + "redis_bytes": 19703216, + "search_p50_ns": 2625, + "search_p95_ns": 3750, + "search_p99_ns": 11958, + "search_samples": 1215, + "sent_bytes": 9830917 + } + ], + "startup_ms": 571.534, + "prune": { + "ms": 21.404, + "generations": 8, + "chunks": 4987, + "redis_bytes": 8206224 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788632207495699 uptime_in_seconds:828 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247311 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 321.566, + "kind": "shared", + "max_rss_bytes": 63651840, + "n": 100000, + "operation": "initial_load", + "ready_ms": 631.795, + "received_bytes": 3414933, + "redis_bytes": 6788416, + "search_p50_ns": 375, + "search_p95_ns": 916, + "search_p99_ns": 8667, + "search_samples": 629, + "sent_bytes": 7849063 + }, + { + "commit_ms": 294.989, + "kind": "shared", + "max_rss_bytes": 76578816, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 295.07, + "received_bytes": 3233830, + "redis_bytes": 6789344, + "search_p50_ns": 1833, + "search_p95_ns": 2250, + "search_p99_ns": 9750, + "search_samples": 283, + "sent_bytes": 684097 + }, + { + "commit_ms": 5.048, + "kind": "shared", + "max_rss_bytes": 99385344, + "n": 100000, + "operation": "add_1", + "ready_ms": 325.217, + "received_bytes": 3927025, + "redis_bytes": 7478960, + "search_p50_ns": 1833, + "search_p95_ns": 2625, + "search_p99_ns": 7792, + "search_samples": 314, + "sent_bytes": 1381205 + }, + { + "commit_ms": 5.074, + "kind": "shared", + "max_rss_bytes": 119619584, + "n": 100000, + "operation": "remove_1", + "ready_ms": 325.255, + "received_bytes": 3926960, + "redis_bytes": 8206944, + "search_p50_ns": 2000, + "search_p95_ns": 2875, + "search_p99_ns": 10459, + "search_samples": 316, + "sent_bytes": 1381084 + }, + { + "commit_ms": 130.169, + "kind": "shared", + "max_rss_bytes": 119832576, + "n": 100000, + "operation": "add_1000", + "ready_ms": 450.269, + "received_bytes": 4503636, + "redis_bytes": 9823984, + "search_p50_ns": 1958, + "search_p95_ns": 2583, + "search_p99_ns": 10208, + "search_samples": 447, + "sent_bytes": 2955331 + }, + { + "commit_ms": 129.902, + "kind": "shared", + "max_rss_bytes": 121978880, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 450.238, + "received_bytes": 4503565, + "redis_bytes": 10529488, + "search_p50_ns": 1875, + "search_p95_ns": 2542, + "search_p99_ns": 9666, + "search_samples": 442, + "sent_bytes": 2932229 + }, + { + "commit_ms": 131.101, + "kind": "shared", + "max_rss_bytes": 122421248, + "n": 100000, + "operation": "add_1000", + "ready_ms": 451.244, + "received_bytes": 4503573, + "redis_bytes": 11255408, + "search_p50_ns": 1875, + "search_p95_ns": 2334, + "search_p99_ns": 4791, + "search_samples": 438, + "sent_bytes": 2955230 + }, + { + "commit_ms": 131.499, + "kind": "shared", + "max_rss_bytes": 122470400, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 451.717, + "received_bytes": 4503573, + "redis_bytes": 11960912, + "search_p50_ns": 1834, + "search_p95_ns": 2833, + "search_p99_ns": 15625, + "search_samples": 444, + "sent_bytes": 2932229 + }, + { + "commit_ms": 600.545, + "kind": "shared", + "max_rss_bytes": 157253632, + "n": 100000, + "operation": "full_replace", + "ready_ms": 970.765, + "received_bytes": 6680458, + "redis_bytes": 17051728, + "search_p50_ns": 1917, + "search_p95_ns": 2500, + "search_p99_ns": 6000, + "search_samples": 967, + "sent_bytes": 8729508 + } + ], + "startup_ms": 317.516, + "prune": { + "ms": 17.767, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6985952 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788631963470410 uptime_in_seconds:584 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247067 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 100000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 318.394, + "kind": "shared", + "max_rss_bytes": 64176128, + "n": 100000, + "operation": "initial_load", + "ready_ms": 638.566, + "received_bytes": 3414935, + "redis_bytes": 6766208, + "search_p50_ns": 416, + "search_p95_ns": 916, + "search_p99_ns": 6500, + "search_samples": 636, + "sent_bytes": 7849063 + }, + { + "commit_ms": 292.082, + "kind": "shared", + "max_rss_bytes": 74432512, + "n": 100000, + "operation": "identical_replace", + "ready_ms": 292.169, + "received_bytes": 3233831, + "redis_bytes": 6787664, + "search_p50_ns": 1875, + "search_p95_ns": 2750, + "search_p99_ns": 8125, + "search_samples": 283, + "sent_bytes": 684097 + }, + { + "commit_ms": 4.992, + "kind": "shared", + "max_rss_bytes": 97157120, + "n": 100000, + "operation": "add_1", + "ready_ms": 315.279, + "received_bytes": 3926882, + "redis_bytes": 7497728, + "search_p50_ns": 1958, + "search_p95_ns": 2542, + "search_p99_ns": 9708, + "search_samples": 307, + "sent_bytes": 1381003 + }, + { + "commit_ms": 5.124, + "kind": "shared", + "max_rss_bytes": 114311168, + "n": 100000, + "operation": "remove_1", + "ready_ms": 315.281, + "received_bytes": 3926889, + "redis_bytes": 8203184, + "search_p50_ns": 2000, + "search_p95_ns": 2750, + "search_p99_ns": 8625, + "search_samples": 308, + "sent_bytes": 1380983 + }, + { + "commit_ms": 131.334, + "kind": "shared", + "max_rss_bytes": 115982336, + "n": 100000, + "operation": "add_1000", + "ready_ms": 451.508, + "received_bytes": 4503571, + "redis_bytes": 9821600, + "search_p50_ns": 1958, + "search_p95_ns": 2750, + "search_p99_ns": 8292, + "search_samples": 447, + "sent_bytes": 2955230 + }, + { + "commit_ms": 130.22, + "kind": "shared", + "max_rss_bytes": 119357440, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 450.361, + "received_bytes": 4503571, + "redis_bytes": 10527248, + "search_p50_ns": 1917, + "search_p95_ns": 3125, + "search_p99_ns": 9584, + "search_samples": 443, + "sent_bytes": 2932229 + }, + { + "commit_ms": 129.445, + "kind": "shared", + "max_rss_bytes": 119537664, + "n": 100000, + "operation": "add_1000", + "ready_ms": 449.626, + "received_bytes": 4503574, + "redis_bytes": 11252944, + "search_p50_ns": 1875, + "search_p95_ns": 2542, + "search_p99_ns": 8833, + "search_samples": 445, + "sent_bytes": 2955230 + }, + { + "commit_ms": 129.461, + "kind": "shared", + "max_rss_bytes": 120815616, + "n": 100000, + "operation": "remove_1000", + "ready_ms": 449.657, + "received_bytes": 4503574, + "redis_bytes": 11958448, + "search_p50_ns": 1916, + "search_p95_ns": 2417, + "search_p99_ns": 6084, + "search_samples": 444, + "sent_bytes": 2932229 + }, + { + "commit_ms": 619.987, + "kind": "shared", + "max_rss_bytes": 145375232, + "n": 100000, + "operation": "full_replace", + "ready_ms": 1010.227, + "received_bytes": 6680459, + "redis_bytes": 17049408, + "search_p50_ns": 1959, + "search_p95_ns": 6875, + "search_p99_ns": 12125, + "search_samples": 992, + "sent_bytes": 8729508 + } + ], + "startup_ms": 309.907, + "prune": { + "ms": 18.043, + "generations": 8, + "chunks": 4987, + "redis_bytes": 6984944 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=100000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788632196642096 uptime_in_seconds:817 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247300 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "diverse", + "repeat": 1, + "measurements": [ + { + "commit_ms": 738.905, + "kind": "diverse", + "max_rss_bytes": 1457078272, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 3059.251, + "received_bytes": 16923776, + "redis_bytes": 21614288, + "search_p50_ns": 417, + "search_p95_ns": 1458, + "search_p99_ns": 8125, + "search_samples": 3022, + "sent_bytes": 21358157 + }, + { + "commit_ms": 862.872, + "kind": "diverse", + "max_rss_bytes": 1995898880, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 862.955, + "received_bytes": 16742810, + "redis_bytes": 21574768, + "search_p50_ns": 2167, + "search_p95_ns": 6625, + "search_p99_ns": 14959, + "search_samples": 830, + "sent_bytes": 685208 + }, + { + "commit_ms": 5.09, + "kind": "diverse", + "max_rss_bytes": 2734850048, + "n": 1000000, + "operation": "add_1", + "ready_ms": 2395.245, + "received_bytes": 17442376, + "redis_bytes": 22280528, + "search_p50_ns": 2292, + "search_p95_ns": 3750, + "search_p99_ns": 9458, + "search_samples": 2352, + "sent_bytes": 1388340 + }, + { + "commit_ms": 6.125, + "kind": "diverse", + "max_rss_bytes": 2818457600, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 2256.262, + "received_bytes": 17442293, + "redis_bytes": 23006448, + "search_p50_ns": 2625, + "search_p95_ns": 4209, + "search_p99_ns": 12542, + "search_samples": 2210, + "sent_bytes": 1388219 + }, + { + "commit_ms": 277.159, + "kind": "diverse", + "max_rss_bytes": 3023994880, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 2597.355, + "received_bytes": 20948288, + "redis_bytes": 27827952, + "search_p50_ns": 2667, + "search_p95_ns": 10125, + "search_p99_ns": 17666, + "search_samples": 2546, + "sent_bytes": 5891860 + }, + { + "commit_ms": 194.568, + "kind": "diverse", + "max_rss_bytes": 3059630080, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 2644.768, + "received_bytes": 20948144, + "redis_bytes": 28533440, + "search_p50_ns": 2667, + "search_p95_ns": 4500, + "search_p99_ns": 11417, + "search_samples": 2555, + "sent_bytes": 5868658 + }, + { + "commit_ms": 812.698, + "kind": "diverse", + "max_rss_bytes": 3243819008, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 3542.92, + "received_bytes": 32338431, + "redis_bytes": 46930752, + "search_p50_ns": 2500, + "search_p95_ns": 4792, + "search_p99_ns": 16000, + "search_samples": 3382, + "sent_bytes": 20478914 + }, + { + "commit_ms": 821.375, + "kind": "diverse", + "max_rss_bytes": 3243819008, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 3641.49, + "received_bytes": 32338430, + "redis_bytes": 47636240, + "search_p50_ns": 2541, + "search_p95_ns": 5125, + "search_p99_ns": 16584, + "search_samples": 3565, + "sent_bytes": 20238914 + }, + { + "commit_ms": 1221.247, + "kind": "diverse", + "max_rss_bytes": 3243819008, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 4331.371, + "received_bytes": 35497720, + "redis_bytes": 70065264, + "search_p50_ns": 2709, + "search_p95_ns": 5458, + "search_p99_ns": 14375, + "search_samples": 4253, + "sent_bytes": 24038992 + } + ], + "startup_ms": 2505.186, + "prune": { + "ms": 56.856, + "generations": 8, + "chunks": 8726, + "redis_bytes": 24380096 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788631998465145 uptime_in_seconds:619 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247102 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "diverse", + "repeat": 2, + "measurements": [ + { + "commit_ms": 734.368, + "kind": "diverse", + "max_rss_bytes": 1508491264, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 2984.508, + "received_bytes": 16923779, + "redis_bytes": 21574128, + "search_p50_ns": 417, + "search_p95_ns": 1250, + "search_p99_ns": 9083, + "search_samples": 2936, + "sent_bytes": 21358157 + }, + { + "commit_ms": 733.371, + "kind": "diverse", + "max_rss_bytes": 1855160320, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 733.607, + "received_bytes": 16742673, + "redis_bytes": 21596032, + "search_p50_ns": 2292, + "search_p95_ns": 6958, + "search_p99_ns": 15542, + "search_samples": 701, + "sent_bytes": 685006 + }, + { + "commit_ms": 7.326, + "kind": "diverse", + "max_rss_bytes": 2527821824, + "n": 1000000, + "operation": "add_1", + "ready_ms": 2327.684, + "received_bytes": 17442455, + "redis_bytes": 22301840, + "search_p50_ns": 2459, + "search_p95_ns": 8500, + "search_p99_ns": 15917, + "search_samples": 2241, + "sent_bytes": 1388441 + }, + { + "commit_ms": 5.585, + "kind": "diverse", + "max_rss_bytes": 3439378432, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 2725.807, + "received_bytes": 17442378, + "redis_bytes": 23001136, + "search_p50_ns": 2500, + "search_p95_ns": 7542, + "search_p99_ns": 17583, + "search_samples": 2647, + "sent_bytes": 1388320 + }, + { + "commit_ms": 202.034, + "kind": "diverse", + "max_rss_bytes": 3501604864, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 2422.234, + "received_bytes": 20948218, + "redis_bytes": 27823264, + "search_p50_ns": 2417, + "search_p95_ns": 3708, + "search_p99_ns": 11375, + "search_samples": 2366, + "sent_bytes": 5891759 + }, + { + "commit_ms": 203.626, + "kind": "diverse", + "max_rss_bytes": 3616915456, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 2423.766, + "received_bytes": 20948001, + "redis_bytes": 28534848, + "search_p50_ns": 2500, + "search_p95_ns": 3833, + "search_p99_ns": 9042, + "search_samples": 2364, + "sent_bytes": 5868456 + }, + { + "commit_ms": 798.814, + "kind": "diverse", + "max_rss_bytes": 3809525760, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 3029.124, + "received_bytes": 32338439, + "redis_bytes": 46913456, + "search_p50_ns": 2542, + "search_p95_ns": 3750, + "search_p99_ns": 9084, + "search_samples": 2965, + "sent_bytes": 20478914 + }, + { + "commit_ms": 773.754, + "kind": "diverse", + "max_rss_bytes": 3810623488, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 3034.026, + "received_bytes": 32338295, + "redis_bytes": 47626128, + "search_p50_ns": 2416, + "search_p95_ns": 3709, + "search_p99_ns": 9042, + "search_samples": 2958, + "sent_bytes": 20238712 + }, + { + "commit_ms": 1124.201, + "kind": "diverse", + "max_rss_bytes": 4080975872, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 4084.326, + "received_bytes": 35497501, + "redis_bytes": 70070640, + "search_p50_ns": 2500, + "search_p95_ns": 3958, + "search_p99_ns": 12333, + "search_samples": 3982, + "sent_bytes": 24038675 + } + ], + "startup_ms": 2212.592, + "prune": { + "ms": 38.879, + "generations": 8, + "chunks": 8726, + "redis_bytes": 24390160 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=diverse seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788632232271499 uptime_in_seconds:853 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247336 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "korean", + "repeat": 1, + "measurements": [ + { + "commit_ms": 960.252, + "kind": "korean", + "max_rss_bytes": 2121646080, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 3870.392, + "received_bytes": 36924198, + "redis_bytes": 44892720, + "search_p50_ns": 666, + "search_p95_ns": 1375, + "search_p99_ns": 9875, + "search_samples": 3787, + "sent_bytes": 41358729 + }, + { + "commit_ms": 922.133, + "kind": "korean", + "max_rss_bytes": 2318909440, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 922.215, + "received_bytes": 36743024, + "redis_bytes": 44925888, + "search_p50_ns": 2792, + "search_p95_ns": 3792, + "search_p99_ns": 5375, + "search_samples": 898, + "sent_bytes": 685410 + }, + { + "commit_ms": 5.448, + "kind": "korean", + "max_rss_bytes": 3446194176, + "n": 1000000, + "operation": "add_1", + "ready_ms": 3395.608, + "received_bytes": 37447202, + "redis_bytes": 45606032, + "search_p50_ns": 3041, + "search_p95_ns": 5291, + "search_p99_ns": 14250, + "search_samples": 3188, + "sent_bytes": 1393073 + }, + { + "commit_ms": 6.168, + "kind": "korean", + "max_rss_bytes": 3446194176, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 4676.268, + "received_bytes": 37447328, + "redis_bytes": 46296304, + "search_p50_ns": 3000, + "search_p95_ns": 8791, + "search_p99_ns": 24583, + "search_samples": 4310, + "sent_bytes": 1393255 + }, + { + "commit_ms": 264.566, + "kind": "korean", + "max_rss_bytes": 3446194176, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 4384.78, + "received_bytes": 45316484, + "redis_bytes": 56221888, + "search_p50_ns": 3000, + "search_p95_ns": 7167, + "search_p99_ns": 18916, + "search_samples": 4182, + "sent_bytes": 10259971 + }, + { + "commit_ms": 306.761, + "kind": "korean", + "max_rss_bytes": 3550429184, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 3687.015, + "received_bytes": 45316629, + "redis_bytes": 56912048, + "search_p50_ns": 3041, + "search_p95_ns": 7334, + "search_p99_ns": 16750, + "search_samples": 3578, + "sent_bytes": 10237173 + }, + { + "commit_ms": 1076.289, + "kind": "korean", + "max_rss_bytes": 3617865728, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 4486.444, + "received_bytes": 70598047, + "redis_bytes": 96459072, + "search_p50_ns": 3084, + "search_p95_ns": 5417, + "search_p99_ns": 14917, + "search_samples": 4318, + "sent_bytes": 38738605 + }, + { + "commit_ms": 1035.599, + "kind": "korean", + "max_rss_bytes": 3617865728, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 5085.835, + "received_bytes": 70597975, + "redis_bytes": 97144112, + "search_p50_ns": 3125, + "search_p95_ns": 7625, + "search_p99_ns": 20959, + "search_samples": 4837, + "sent_bytes": 38498483 + }, + { + "commit_ms": 2056.258, + "kind": "korean", + "max_rss_bytes": 3617865728, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 6776.403, + "received_bytes": 75499404, + "redis_bytes": 141332304, + "search_p50_ns": 3042, + "search_p95_ns": 14000, + "search_p99_ns": 29917, + "search_samples": 6410, + "sent_bytes": 44041073 + } + ], + "startup_ms": 2881.158, + "prune": { + "ms": 66.244, + "generations": 8, + "chunks": 8726, + "redis_bytes": 46126736 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788632027083305 uptime_in_seconds:648 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247131 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "korean", + "repeat": 2, + "measurements": [ + { + "commit_ms": 963.739, + "kind": "korean", + "max_rss_bytes": 2033680384, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 3864.04, + "received_bytes": 36924200, + "redis_bytes": 44911520, + "search_p50_ns": 667, + "search_p95_ns": 1542, + "search_p99_ns": 7250, + "search_samples": 3807, + "sent_bytes": 41358729 + }, + { + "commit_ms": 936.869, + "kind": "korean", + "max_rss_bytes": 2337980416, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 936.957, + "received_bytes": 36743026, + "redis_bytes": 44924272, + "search_p50_ns": 2708, + "search_p95_ns": 3916, + "search_p99_ns": 5209, + "search_samples": 915, + "sent_bytes": 685410 + }, + { + "commit_ms": 5.425, + "kind": "korean", + "max_rss_bytes": 3473637376, + "n": 1000000, + "operation": "add_1", + "ready_ms": 2895.618, + "received_bytes": 37447197, + "redis_bytes": 45604544, + "search_p50_ns": 2875, + "search_p95_ns": 4667, + "search_p99_ns": 12500, + "search_samples": 2818, + "sent_bytes": 1393059 + }, + { + "commit_ms": 5.781, + "kind": "korean", + "max_rss_bytes": 3964043264, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 3676.095, + "received_bytes": 37447271, + "redis_bytes": 46294704, + "search_p50_ns": 2917, + "search_p95_ns": 8375, + "search_p99_ns": 18750, + "search_samples": 3505, + "sent_bytes": 1393154 + }, + { + "commit_ms": 259.68, + "kind": "korean", + "max_rss_bytes": 3965304832, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 4029.838, + "received_bytes": 45316558, + "redis_bytes": 56222368, + "search_p50_ns": 3000, + "search_p95_ns": 10708, + "search_p99_ns": 20750, + "search_samples": 3902, + "sent_bytes": 10260072 + }, + { + "commit_ms": 254.059, + "kind": "korean", + "max_rss_bytes": 3965304832, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 4084.23, + "received_bytes": 45316487, + "redis_bytes": 56912496, + "search_p50_ns": 3125, + "search_p95_ns": 6500, + "search_p99_ns": 16084, + "search_samples": 3951, + "sent_bytes": 10236971 + }, + { + "commit_ms": 1045.204, + "kind": "korean", + "max_rss_bytes": 3965304832, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 3975.313, + "received_bytes": 70598042, + "redis_bytes": 96454880, + "search_p50_ns": 2917, + "search_p95_ns": 4334, + "search_p99_ns": 10458, + "search_samples": 3891, + "sent_bytes": 38738591 + }, + { + "commit_ms": 1020.023, + "kind": "korean", + "max_rss_bytes": 3965304832, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 4850.243, + "received_bytes": 70597988, + "redis_bytes": 97145008, + "search_p50_ns": 3000, + "search_p95_ns": 6833, + "search_p99_ns": 21834, + "search_samples": 4511, + "sent_bytes": 38498497 + }, + { + "commit_ms": 1770.864, + "kind": "korean", + "max_rss_bytes": 3965304832, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 7311.176, + "received_bytes": 75498968, + "redis_bytes": 141331328, + "search_p50_ns": 3041, + "search_p95_ns": 11625, + "search_p99_ns": 25042, + "search_samples": 6729, + "sent_bytes": 44040453 + } + ], + "startup_ms": 2854.197, + "prune": { + "ms": 55.469, + "generations": 8, + "chunks": 8726, + "redis_bytes": 46125920 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=korean seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788632258982741 uptime_in_seconds:879 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247362 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "shared", + "repeat": 1, + "measurements": [ + { + "commit_ms": 629.881, + "kind": "shared", + "max_rss_bytes": 453967872, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 1840.172, + "received_bytes": 25923555, + "redis_bytes": 31170592, + "search_p50_ns": 417, + "search_p95_ns": 1208, + "search_p99_ns": 7375, + "search_samples": 1801, + "sent_bytes": 30357854 + }, + { + "commit_ms": 611.082, + "kind": "shared", + "max_rss_bytes": 541360128, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 611.17, + "received_bytes": 25742449, + "redis_bytes": 31154624, + "search_p50_ns": 2250, + "search_p95_ns": 3125, + "search_p99_ns": 11542, + "search_samples": 584, + "sent_bytes": 684703 + }, + { + "commit_ms": 5.216, + "kind": "shared", + "max_rss_bytes": 689995776, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1205.356, + "received_bytes": 26444867, + "redis_bytes": 31847072, + "search_p50_ns": 2333, + "search_p95_ns": 4125, + "search_p99_ns": 13667, + "search_samples": 1174, + "sent_bytes": 1390802 + }, + { + "commit_ms": 5.821, + "kind": "shared", + "max_rss_bytes": 906919936, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1206.157, + "received_bytes": 26444941, + "redis_bytes": 32573040, + "search_p50_ns": 2375, + "search_p95_ns": 4375, + "search_p99_ns": 12750, + "search_samples": 1174, + "sent_bytes": 1390883 + }, + { + "commit_ms": 213.322, + "kind": "shared", + "max_rss_bytes": 979124224, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1403.445, + "received_bytes": 31909510, + "redis_bytes": 39493056, + "search_p50_ns": 2459, + "search_p95_ns": 3584, + "search_p99_ns": 10917, + "search_samples": 1365, + "sent_bytes": 7853070 + }, + { + "commit_ms": 218.579, + "kind": "shared", + "max_rss_bytes": 979402752, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1418.86, + "received_bytes": 31909366, + "redis_bytes": 40198560, + "search_p50_ns": 2375, + "search_p95_ns": 3542, + "search_p99_ns": 12334, + "search_samples": 1374, + "sent_bytes": 7829868 + }, + { + "commit_ms": 864.479, + "kind": "shared", + "max_rss_bytes": 981073920, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2084.678, + "received_bytes": 49561455, + "redis_bytes": 67230192, + "search_p50_ns": 2292, + "search_p95_ns": 3042, + "search_p99_ns": 8417, + "search_samples": 2046, + "sent_bytes": 28701947 + }, + { + "commit_ms": 851.561, + "kind": "shared", + "max_rss_bytes": 981172224, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2091.812, + "received_bytes": 49561390, + "redis_bytes": 67935760, + "search_p50_ns": 2416, + "search_p95_ns": 3291, + "search_p99_ns": 8833, + "search_samples": 2041, + "sent_bytes": 28461846 + }, + { + "commit_ms": 1048.994, + "kind": "shared", + "max_rss_bytes": 1393803264, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 2879.122, + "received_bytes": 53497506, + "redis_bytes": 99105696, + "search_p50_ns": 2375, + "search_p95_ns": 3333, + "search_p99_ns": 9500, + "search_samples": 2807, + "sent_bytes": 33038661 + } + ], + "startup_ms": 1193.936, + "prune": { + "ms": 32.686, + "generations": 8, + "chunks": 8726, + "redis_bytes": 33140080 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788631981835544 uptime_in_seconds:602 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247085 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + }, + { + "backend": "valkey", + "n": 1000000, + "kind": "shared", + "repeat": 2, + "measurements": [ + { + "commit_ms": 658.414, + "kind": "shared", + "max_rss_bytes": 488030208, + "n": 1000000, + "operation": "initial_load", + "ready_ms": 1828.575, + "received_bytes": 25923629, + "redis_bytes": 31172880, + "search_p50_ns": 458, + "search_p95_ns": 1584, + "search_p99_ns": 6292, + "search_samples": 1800, + "sent_bytes": 30357955 + }, + { + "commit_ms": 617.976, + "kind": "shared", + "max_rss_bytes": 550600704, + "n": 1000000, + "operation": "identical_replace", + "ready_ms": 618.055, + "received_bytes": 25742452, + "redis_bytes": 31156912, + "search_p50_ns": 2166, + "search_p95_ns": 6833, + "search_p99_ns": 18500, + "search_samples": 593, + "sent_bytes": 684703 + }, + { + "commit_ms": 5.296, + "kind": "shared", + "max_rss_bytes": 693108736, + "n": 1000000, + "operation": "add_1", + "ready_ms": 1225.421, + "received_bytes": 26444941, + "redis_bytes": 31890304, + "search_p50_ns": 2292, + "search_p95_ns": 3833, + "search_p99_ns": 12167, + "search_samples": 1190, + "sent_bytes": 1390903 + }, + { + "commit_ms": 5.867, + "kind": "shared", + "max_rss_bytes": 887275520, + "n": 1000000, + "operation": "remove_1", + "ready_ms": 1266.149, + "received_bytes": 26444948, + "redis_bytes": 32568112, + "search_p50_ns": 2375, + "search_p95_ns": 8250, + "search_p99_ns": 14958, + "search_samples": 1246, + "sent_bytes": 1390883 + }, + { + "commit_ms": 220.275, + "kind": "shared", + "max_rss_bytes": 900349952, + "n": 1000000, + "operation": "add_1000", + "ready_ms": 1410.388, + "received_bytes": 31909584, + "redis_bytes": 39481952, + "search_p50_ns": 2292, + "search_p95_ns": 4542, + "search_p99_ns": 13000, + "search_samples": 1379, + "sent_bytes": 7853171 + }, + { + "commit_ms": 209.174, + "kind": "shared", + "max_rss_bytes": 1003896832, + "n": 1000000, + "operation": "remove_1000", + "ready_ms": 1669.276, + "received_bytes": 31909368, + "redis_bytes": 40207856, + "search_p50_ns": 2291, + "search_p95_ns": 8084, + "search_p99_ns": 15125, + "search_samples": 1618, + "sent_bytes": 7829868 + }, + { + "commit_ms": 849.195, + "kind": "shared", + "max_rss_bytes": 1005289472, + "n": 1000000, + "operation": "add_10000", + "ready_ms": 2049.316, + "received_bytes": 49561391, + "redis_bytes": 67231472, + "search_p50_ns": 2333, + "search_p95_ns": 3042, + "search_p99_ns": 10042, + "search_samples": 2009, + "sent_bytes": 28701846 + }, + { + "commit_ms": 854.582, + "kind": "shared", + "max_rss_bytes": 1005404160, + "n": 1000000, + "operation": "remove_10000", + "ready_ms": 2034.8, + "received_bytes": 49561393, + "redis_bytes": 67916512, + "search_p50_ns": 2292, + "search_p95_ns": 3208, + "search_p99_ns": 7666, + "search_samples": 1986, + "sent_bytes": 28461846 + }, + { + "commit_ms": 1048.23, + "kind": "shared", + "max_rss_bytes": 1296793600, + "n": 1000000, + "operation": "full_replace", + "ready_ms": 2858.38, + "received_bytes": 53497509, + "redis_bytes": 99078336, + "search_p50_ns": 2375, + "search_p95_ns": 3625, + "search_p99_ns": 10333, + "search_samples": 2786, + "sent_bytes": 33038661 + } + ], + "startup_ms": 1531.781, + "prune": { + "ms": 32.555, + "generations": 8, + "chunks": 8726, + "redis_bytes": 33133952 + }, + "environment": "versioned_scale_test.go:70: environment go=go1.26.7 os=darwin arch=arm64 cpus=10 n=1000000 kind=shared seed=20260906 server=# Server redis_version:7.2.4 server_name:valkey valkey_version:9.1.2 valkey_release_stage:ga redis_git_sha1:00000000 redis_git_dirty:1 redis_build_id:97cfd53c10d1713 server_mode:standalone os:Darwin 25.5.0 arm64 arch_bits:64 monotonic_clock:ARM CNTVCT @ 24 ticks/us multiplexing_api:kqueue gcc_version:4.2.1 process_id:76607 process_supervised:no run_id:ecf3a7f64fa848d82f4952d6a705c8b5dabad016 tcp_port:16390 server_time_usec:1788632214877275 uptime_in_seconds:835 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:10247318 executable:/tmp/valkey-9.1.2/src/valkey-server config_file: io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,port=16390", + "passed": true + } + ], + "million_safety": { + "redis": "=== RUN TestVersionedMillionSafety\n--- PASS: TestVersionedMillionSafety (3.92s)\nPASS\n", + "valkey": "=== RUN TestVersionedMillionSafety\n--- PASS: TestVersionedMillionSafety (3.95s)\nPASS\n" + } +} diff --git a/changes/unreleased/20260906-versioned-dictionaries.yaml b/changes/unreleased/20260906-versioned-dictionaries.yaml new file mode 100644 index 00000000..bcd27c77 --- /dev/null +++ b/changes/unreleased/20260906-versioned-dictionaries.yaml @@ -0,0 +1,5 @@ +kind: Added +body: "Add opt-in V3 versioned dictionaries with atomic batch updates, snapshots, background engine refresh, bucket reuse, V2 migration and dictionary CLI tools; add bounded scanning, masking and replacement with original text positions." +time: 2026-09-06T21:00:00+09:00 +custom: + Issue: "244" diff --git a/cmd/acor/dictionary.go b/cmd/acor/dictionary.go new file mode 100644 index 00000000..854c7336 --- /dev/null +++ b/cmd/acor/dictionary.go @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + + "github.com/skyoo2003/acor/pkg/acor" +) + +const dictionaryCopyV2 = "copy-v2" +const dictionaryReplace = "replace" +const dictionaryDiff = "diff" +const dictionaryList = "list" +const dictionaryPageSize = 1000 + +type dictionaryConfig struct { + command string + expected string + allowEmpty bool + cursor string + limit int + source string + sensitive bool + words []string +} + +func dispatchDictionary(connection *acor.AhoCorasickArgs, opts *commandOptions, args []string, stdin io.Reader, stdout, stderr io.Writer) int { + if opts.dryRun || opts.keepOldKeys { + _, _ = fmt.Fprintln(stderr, "migration flags do not apply to dictionary") + return exitCodeUsage + } + return runDictionary(connection, args, stdin, stdout, stderr) +} + +func parseDictionary(args []string, stdin io.Reader, stderr io.Writer) (*dictionaryConfig, error) { + if len(args) == 0 { + return nil, errors.New("dictionary requires list, diff, replace, status, copy-v2 or prune") + } + c := &dictionaryConfig{command: args[0]} + fs := flag.NewFlagSet("dictionary "+args[0], flag.ContinueOnError) + fs.SetOutput(stderr) + fs.StringVar(&c.expected, "expected-version", "", "required opaque version for replace/copy-v2") + fs.BoolVar(&c.allowEmpty, "allow-empty", false, "allow an empty replacement") + fs.StringVar(&c.cursor, "cursor", "", "page cursor (version must still be active)") + fs.IntVar(&c.limit, "limit", dictionaryPageSize, "page size") + fs.StringVar(&c.source, "source", "", "source V2 collection name") + fs.BoolVar(&c.sensitive, "case-sensitive", false, "persisted case policy") + if err := fs.Parse(args[1:]); err != nil { + return nil, err + } + if fs.NArg() != 0 { + return nil, errors.New("dictionary input is a JSON string array on stdin") + } + if err := c.validate(); err != nil { + return nil, err + } + if c.command == dictionaryReplace || c.command == dictionaryDiff { + if err := c.readInput(stdin); err != nil { + return nil, err + } + } + return c, nil +} +func (c *dictionaryConfig) validate() error { + switch c.command { + case dictionaryList, dictionaryDiff, dictionaryReplace, "status", dictionaryCopyV2, "prune": + default: + return errors.New("unknown dictionary command") + } + if (c.command == dictionaryReplace || c.command == dictionaryCopyV2) && c.expected == "" { + return errors.New("--expected-version is required") + } + if c.command == dictionaryCopyV2 && c.source == "" { + return errors.New("--source is required") + } + if c.limit <= 0 { + return errors.New("--limit must be positive") + } + return nil +} +func (c *dictionaryConfig) readInput(stdin io.Reader) error { + decoder := json.NewDecoder(stdin) + if err := decoder.Decode(&c.words); err != nil { + return err + } + var extra interface{} + if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) { + return errors.New("expected one JSON array") + } + if c.command == dictionaryReplace && len(c.words) == 0 && !c.allowEmpty { + return errors.New("empty replacement requires --allow-empty") + } + return nil +} +func runDictionary(connection *acor.AhoCorasickArgs, args []string, stdin io.Reader, stdout, stderr io.Writer) int { + c, err := parseDictionary(args, stdin, stderr) + if err != nil { + _, _ = fmt.Fprintln(stderr, err) + return exitCodeUsage + } + ctx := context.Background() + v, err := acor.OpenVersioned(ctx, &acor.VersionedOptions{Redis: *connection, CaseSensitive: c.sensitive}) + if err != nil { + _, _ = fmt.Fprintln(stderr, err) + return 1 + } + defer func() { _ = v.Close() }() + result, err := c.execute(ctx, v) + if err != nil { + if result != nil { + _ = json.NewEncoder(stdout).Encode(result) + } + _, _ = fmt.Fprintln(stderr, err) + return 1 + } + if err = json.NewEncoder(stdout).Encode(result); err != nil { + _, _ = fmt.Fprintln(stderr, err) + return 1 + } + return 0 +} +func (c *dictionaryConfig) execute(ctx context.Context, v *acor.VersionedCollection) (interface{}, error) { + switch c.command { + case "status": + return v.Status(), nil + case dictionaryReplace: + return v.Replace(ctx, acor.Version(c.expected), c.words) + case "prune": + return v.Prune(ctx) + case dictionaryCopyV2: + return v.CopyV2(ctx, c.source, acor.Version(c.expected), &acor.V2CopyOptions{RejectEmpty: !c.allowEmpty}) + case dictionaryList, dictionaryDiff: + s, err := v.Snapshot(ctx) + if err != nil { + return nil, err + } + defer func() { _ = s.Close(ctx) }() + if c.command == dictionaryList { + return s.List(ctx, c.cursor, c.limit) + } + return s.Diff(ctx, c.words) + } + return nil, errors.New("unknown dictionary command") +} diff --git a/cmd/acor/dictionary_test.go b/cmd/acor/dictionary_test.go new file mode 100644 index 00000000..4702e533 --- /dev/null +++ b/cmd/acor/dictionary_test.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/alicebob/miniredis/v2" + + "github.com/skyoo2003/acor/pkg/acor" +) + +func TestDictionaryCLI(t *testing.T) { + server := miniredis.RunT(t) + invoke := func(input string, args ...string) []byte { + t.Helper() + all := append([]string{"-addr", server.Addr(), "-name", "cli-v3", "dictionary"}, args...) + var out, stderr bytes.Buffer + if code := runWithInput(all, strings.NewReader(input), &out, &stderr, createService); code != 0 { + t.Fatalf("code=%d err=%s", code, stderr.String()) + } + return out.Bytes() + } + var status acor.VersionedStatus + if err := json.Unmarshal(invoke("", "status"), &status); err != nil { + t.Fatal(err) + } + var written acor.WriteResult + if err := json.Unmarshal(invoke(`["hello","한국"]`, "replace", "--expected-version", string(status.ServingVersion)), &written); err != nil { + t.Fatal(err) + } + var page acor.DictionaryPage + if err := json.Unmarshal(invoke("", "list", "--limit", "1"), &page); err != nil || len(page.Keywords) != 1 || page.NextCursor == "" { + t.Fatal(page, err) + } + invoke("", "list", "--cursor", page.NextCursor) + invoke(`["hello"]`, "diff") + invoke("", "prune") + invoke(`[]`, "replace", "--allow-empty", "--expected-version", string(written.Version)) +} +func TestDictionaryCLIGuards(t *testing.T) { + cases := [][]string{ + {}, {"bogus"}, {"replace"}, {"replace", "--expected-version", "token"}, {"copy-v2", "--expected-version", "token"}, {"list", "--limit", "0"}} + for _, args := range cases { + var stderr bytes.Buffer + if _, err := parseDictionary(args, strings.NewReader(`[]`), &stderr); err == nil { + t.Fatal("accepted", args) + } + } + for _, input := range []string{`["a"] []`, `[1]`, `no`} { + var stderr bytes.Buffer + if _, err := parseDictionary([]string{"diff"}, strings.NewReader(input), &stderr); err == nil { + t.Fatal("accepted", input) + } + } +} diff --git a/cmd/acor/main.go b/cmd/acor/main.go index 5f0dc5f2..f0f898cc 100644 --- a/cmd/acor/main.go +++ b/cmd/acor/main.go @@ -42,6 +42,7 @@ Commands: migrate [options] migrate-rollback schema-version + dictionary list|diff|replace|status|copy-v2|prune [options] version Options: @@ -219,6 +220,9 @@ func runWithInput(args []string, stdin io.Reader, stdout, stderr io.Writer, } command := remaining[0] + if command == "dictionary" { + return dispatchDictionary(config, commandOpts, remaining[1:], stdin, stdout, stderr) + } runner, argMode, err := commandHandler(command) if err != nil { _, _ = fmt.Fprintln(stderr, err.Error()) diff --git a/cmd/acor/main_test.go b/cmd/acor/main_test.go index f56910d8..484f54da 100644 --- a/cmd/acor/main_test.go +++ b/cmd/acor/main_test.go @@ -1136,7 +1136,7 @@ func TestUsageTextListsEveryCommand(t *testing.T) { if len(fields) == 0 { continue } - if _, ok := commandSpecs[fields[0]]; !ok { + if _, ok := commandSpecs[fields[0]]; !ok && fields[0] != "dictionary" { t.Errorf("usage text advertises %q, which has no commandSpecs entry", fields[0]) } } diff --git a/docs/content/cli/commands.md b/docs/content/cli/commands.md index 9fa42d3d..a87cd7f1 100644 --- a/docs/content/cli/commands.md +++ b/docs/content/cli/commands.md @@ -84,3 +84,11 @@ The trade-offs behind each preset are in ## Navigation ← [CLI](../) | [Extending](../../extending/) → + +## Versioned dictionaries + +Use `acor -name new-v3-name dictionary list|diff|replace|status|copy-v2|prune`. +Replacement and copying require `--expected-version`; empty replacements and +empty V2 sources require `--allow-empty`. Diff and replace read a JSON string +array from stdin. See the [V3 guide](../../reference/versioned/) for pagination, +case policy, cutover and command examples. diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md index 9157cedf..5eeca1d0 100644 --- a/docs/content/reference/api.md +++ b/docs/content/reference/api.md @@ -473,3 +473,17 @@ positions. See [parallel matching](../../guides/parallel-matching/). `CacheStats.PresetReloadFailures` and `CacheStats.PresetPollFailures` are cumulative `uint64` counters. Shared reload failures count once per job; cancellations are excluded. They remain zero in other modes. See [Preset refresh](../../guides/redis-backed-engine/#invalidation-safety). + +## Versioned dictionaries + +`OpenVersioned(ctx, *VersionedOptions)` opens a separate V3 collection. Its +`VersionedCollection` API provides leased snapshots, paginated list/diff, expected-version +replace/add/remove, asynchronous engine refresh, operation receipts, V2 copying +and explicit pruning. See [the V3 API guide](../versioned/) for contracts and a +compilable example; V3 uses the existing module version and search semantics. + +## Bounded source-position APIs + +`Scan`, `MaskText`, and `ReplaceText` are available on AhoCorasick and +VersionedCollection with explicit contexts. See [bounded text processing](../text-processing/) +for ScanOptions, RewriteOptions, original byte/rune spans and error contracts. diff --git a/docs/content/reference/r2-r3-performance.md b/docs/content/reference/r2-r3-performance.md new file mode 100644 index 00000000..6d107987 --- /dev/null +++ b/docs/content/reference/r2-r3-performance.md @@ -0,0 +1,122 @@ +--- +title: "R2/R3 verification and R1 comparison" +description: "Incremental download, engine memory and bounded source-text API evidence." +--- + +The R2/R3 implementation completed 36 real-server runs on 2026-09-06 using the +same R1 workload: Redis/Valkey × 10,000/100,000/1,000,000 keywords × shared/diverse/ +Korean distributions × two repetitions. The R1 measurements remain unchanged in +`benchmarks/results/v3-20260906.json`. New measurements, source hashes and safety +results are in `benchmarks/results/r2-r3-20260906.json`. + +Environment: Apple M4, 10 logical CPUs, 16 GiB RAM, macOS 26.5.2, Go 1.26.7, +Redis 8.10.1 and source-built Valkey 9.1.2. Both are standalone localhost TCP, +with RDB/AOF disabled. The preset remains MemoryEfficient; polling remains 50 ms +for measurement. R2 adds the default fixed 20 ms refresh debounce. Each run uses +a fresh Go process. These are developer-workstation measurements with possible +background host activity, not latency guarantees or confidence intervals. + +Valkey uses `prefetch-batch-max-size 0`, as in the successful R1 baseline. The +local default-prefetch build previously crashed; this work does not establish +support for that configuration or diagnose its root cause. See the +[R1 report](../versioned-performance/) for source checksum and crash evidence. + +## R2 changes + +- The installed engine keeps verified immutable keyword slices and its manifest. + Unchanged buckets share those slices; only changed nonempty buckets download + chunk data. Candidate caches are installed with the engine only after success. +- MemoryEfficient builds consume the bucket sequence directly, avoiding the + flattened full dictionary slice and an additional keyword-set map. Nodes keep + their only child inline; a map is allocated only when the node branches. BFS + uses a compact integer queue. The first-rune Bloom filter sizes itself from + distinct first runes rather than total keyword count. +- Builds check cancellation in insertion, failure-link construction and table + filling for all three presets. Cancellation discards the private candidate; + no detached builder goroutine continues after return. Allocations, rune-count + calls and sorting finish before the next checkpoint. +- A fixed debounce window merges burst notifications. New events cannot extend + that window; an in-flight build completes under ordinary writes, preventing + repeated cancellation from starving refresh. Close cancels its build context. +- Status reports the last successful build's DownloadedBuckets/ReusedBuckets and + cumulative CompletedBuilds. Redis schema, expected-version writes, receipt + resolution, leases and pruning semantics are unchanged. + +## Million-keyword comparison + +Ranges below are the minimum–maximum of two runs. Ready is write preparation, +commit, refresh detection and engine construction combined. RSS is the process +high-water mark across the full run, including all update scenarios. Network +counters are server-wide and include protocol, polling and engine downloads; +they do not isolate only keyword payloads. + +| Server | Distribution | Add 1 ready: R1 → R2 (s) | Received bytes reduction for add 1 | Peak RSS: R1 → R2 (GiB) | +|---|---|---:|---:|---:| +| redis | shared | 1.20–1.21 → 0.55–0.80 | 94.7% | 1.20–1.20 → 1.01–1.09 | +| redis | diverse | 2.28–2.30 → 1.11–1.47 | 91.9% | 3.14–3.55 → 2.05–2.05 | +| redis | korean | 2.96–3.17 → 1.22–1.32 | 96.2% | 3.02–3.74 → 2.33–2.45 | +| valkey | shared | 1.21–1.23 → 0.53–0.55 | 94.7% | 1.21–1.30 → 1.05–1.16 | +| valkey | diverse | 2.33–2.40 → 0.92–1.27 | 91.9% | 3.02–3.80 → 2.02–2.19 | +| valkey | korean | 2.90–3.40 → 1.21–1.60 | 96.2% | 3.37–3.69 → 2.33–2.50 | + +All six million-keyword server/distribution combinations reduced single-change +received bytes and peak RSS in these runs. A full engine is still rebuilt after +a change; this is not an incremental automaton or a constant-memory design. +Larger changes touch more buckets and therefore reuse less downloaded data. + +| Server | Distribution | Add 1,000 ready: R1 → R2 (s) | Add 1% ready: R1 → R2 (s) | Full replace ready: R1 → R2 (s) | +|---|---|---:|---:|---:| +| redis | shared | 1.40–1.41 → 0.95–1.22 | 2.07–2.08 → 1.82–2.93 | 2.86–2.88 → 2.38–3.02 | +| redis | diverse | 2.70–2.71 → 1.32–1.46 | 3.01–3.38 → 2.11–4.08 | 3.98–4.34 → 3.06–3.96 | +| redis | korean | 3.55–4.48 → 1.38–1.90 | 4.63–6.11 → 2.64–2.82 | 7.43–7.71 → 3.24–3.92 | +| valkey | shared | 1.40–1.41 → 0.82–1.25 | 2.05–2.08 → 1.80–1.89 | 2.86–2.88 → 2.21–2.67 | +| valkey | diverse | 2.42–2.60 → 1.21–1.29 | 3.03–3.54 → 2.14–2.19 | 4.08–4.33 → 2.77–3.61 | +| valkey | korean | 4.03–4.38 → 1.45–1.60 | 3.98–4.49 → 2.64–2.70 | 6.78–7.31 → 4.17–4.92 | + +Improvement is not uniform: some Redis large-change repetitions overlap or +exceed the R1 timing range. Two repetitions do not establish a universal speedup. + +The JSON also records 10,000/100,000-entry results, initial loading/startup, +identical replacement, removals, search p50/p95/p99 while refreshing, Redis +memory and pruning. The same seed 20260906 and generation functions are used. +Prune measurements simulate the retention horizon by aging generation registry +scores; they do not wait 24 hours. Separate million-entry safety tests passed +against both servers after the matrix, with no concurrent workload on a measured +endpoint. + +## R3 contracts and verification + +`Scan`, `MaskText` and `ReplaceText` are additive on AhoCorasick and +VersionedCollection. See [bounded text processing](../text-processing/) for the +complete defaults and error behavior. Existing unlimited APIs are unchanged. + +| Area | Evidence | +|---|---| +| Original positions | All presets match existing FindMatches for overlapping and leftmost-longest results; original byte slices survive Korean, emoji, `İ` case folding and malformed UTF-8 input | +| Result limits | At most MaxMatches are retained; Truncated requires an additional eligible result | +| Work/input bounds | Oversize input and excess raw candidates fail explicitly, including candidates later filtered by word boundaries/overlap | +| Atomic rewrite | Match, output and work exhaustion return no partial rewrite; empty literal replacement deletes; masks preserve original rune count, including NUL masks | +| Overlap | A bounded pending-start heap selects leftmost-longest matches without collecting all raw matches | +| Fuzz | FuzzScanLeftmostParity completed 30 seconds and 819,033 executions against the existing leftmost-longest implementation | +| Refresh | Verified bucket reuse, failed-candidate cache preservation and burst coalescing tests pass | +| Cancellation | All three presets preserve the previous engine on cancellation during sequence ingestion and CPU construction; unrelated panics are not swallowed | +| Safety | Both real servers pass the million-entry conflict, pinned paging and lease-safe pruning scenario | + +Resource bounds are per call. Scratch input indexing is proportional to the +bounded input size, and the pending window is bounded by input/longest-keyword +length and candidate count. A caller's custom WordRune function controls its own +resource use. R3 does not claim a wall-clock bound on callbacks or allocation. + +Root and server race tests, root/server lint, all-module vet, benchmark-module +tests, API snapshot/audit and documentation compilation pass. Legacy Find/Add +fuzz targets are rerun for 30 seconds each. No HTTP/gRPC surface or storage +schema migration is introduced by R2/R3. + +## Reproduce + +Run the unchanged `scripts/benchmark-v3.sh` against each disposable endpoint with +`ACOR_V3_SCALE_REPEATS=2` and separate output directories. See the +[R1 reproduction instructions](../versioned-performance/#reproduce) for the exact +environment and dictionary distributions. The R2/R3 defaults select the new +engine and refresh behavior automatically; existing V1/V2 public contracts remain +covered by the regression suite. diff --git a/docs/content/reference/text-processing.md b/docs/content/reference/text-processing.md new file mode 100644 index 00000000..ebfee5b5 --- /dev/null +++ b/docs/content/reference/text-processing.md @@ -0,0 +1,116 @@ +--- +title: "Bounded search, masking and replacement" +description: "Original byte and rune positions with explicit work and output limits." +--- + +R3 adds `Scan`, `MaskText`, and `ReplaceText` to both AhoCorasick and +VersionedCollection. All take a context. Existing Find/FindMatches APIs retain +their existing unlimited result and position contracts. V3 calls hold one serving +engine for the entire scan or rewrite, even if a refresh finishes concurrently. + +## Original positions and bounded search + +`Scan(ctx, text, *ScanOptions)` returns SourceMatch entries containing the normalized +Keyword, the original Text substring, and half-open Start/End rune and +ByteStart/ByteEnd byte offsets into the original input. Unicode case folding can +change byte lengths: `İSTANBUL` matches `istanbul`, but its byte span still selects +the original spelling. No normalization of the original output text occurs. +Invalid UTF-8 input bytes are decoded as RuneError for matching, while reported +byte slices retain those exact original bytes. + +Zero-valued limits select these defaults; negative limits are rejected. Construct +ScanOptions and RewriteOptions using named fields for forward compatibility. + +| Option | Default | Behavior at limit | +|---|---:|---| +| MaxInputBytes | 1 MiB | ErrInputLimit before loading/scanning the engine | +| MaxMatches | 1,000 | At most this many entries; Truncated when another eligible match is observed | +| MaxCandidates | 100,000 | ErrScanWorkLimit when another raw automaton match is encountered | + +Kind defaults to MatchKindOverlapping. MatchKindLeftmostLongest selects the +leftmost available start, then the longest keyword there, and discards overlaps. +The implementation keeps the longest candidate at each pending start until the +engine's longest keyword makes the decision safe. It does not collect all raw +matches and sort them. Scratch memory is bounded by input length plus the pending +start window and retained result count; it is not constant memory. Input indexing +uses rune and byte-offset arrays. A low result limit alone does not replace the +input-byte or candidate-work limit. + +WholeWord and WordRune have the same normalized-rune boundary semantics as +MatchOptions. Letters, digits, combining marks and underscore are word characters +by default. Scripts without spaces may require an application-specific WordRune. +Candidate counting happens before whole-word and overlap filtering, so a dense +rejected-match workload cannot bypass the work budget. Custom WordRune code runs +in the caller's goroutine; its own resource use is the caller's responsibility. + +Input or candidate exhaustion and cancellation return an error with no result. +Truncated concerns the eligible result count only. A caller checking safety or +completeness must handle both errors and Truncated; neither means a clean input. +The context is checked during input indexing, traversal and match handling. + +## Atomic masking and literal replacement + +`ReplaceText(ctx, text, replacement, *RewriteOptions)` inserts the literal +replacement for each selected non-overlapping leftmost-longest match. It does not +interpret regular expressions, expand captures, or search replacement text again. +An empty replacement deletes matched spans. + +`MaskText(ctx, text, maskRune, *RewriteOptions)` writes one mask rune for every +matched original rune. The result preserves rune count, not necessarily byte count. +Any valid Unicode rune, including NUL, is accepted. Invalid runes are rejected. +Both APIs leave unmatched original bytes untouched. + +RewriteOptions shares the three scan limits above and adds MaxOutputBytes, which +defaults to 4 MiB. WholeWord and WordRune are also available. Overlap selection is +always leftmost-longest. Exceeding MaxMatches produces ErrMatchLimit; exceeding the +output bound produces ErrOutputLimit. Every error returns no RewriteResult, so a +caller cannot accidentally consume a partially masked document. All output sizes +are checked before allocating the output buffer. + +RewriteResult contains Text and the SourceMatch entries used for the rewrite. +Their offsets always refer to the **input**, even when replacement changes output +length. Match Text substrings can retain the original input string in memory. + + +```go +package main + +import ( + "context" + "log" + + "github.com/skyoo2003/acor/pkg/acor" +) + +func main() { + ctx := context.Background() + dictionary, err := acor.OpenVersioned(ctx, &acor.VersionedOptions{ + Redis: acor.AhoCorasickArgs{Addr: "localhost:6379", Name: "text-v3"}, + }) + if err != nil { log.Fatal(err) } + defer dictionary.Close() + + snapshot, err := dictionary.Snapshot(ctx) + if err != nil { log.Fatal(err) } + expected := snapshot.Version() + snapshot.Close(ctx) + write, err := dictionary.Replace(ctx, expected, []string{"한국", "한국어", "istanbul"}) + if err != nil { log.Fatal(err) } + if err := dictionary.WaitForVersion(ctx, write.Version); err != nil { log.Fatal(err) } + + found, err := dictionary.Scan(ctx, "한국어 İSTANBUL", &acor.ScanOptions{ + Kind: acor.MatchKindLeftmostLongest, MaxMatches: 10, + }) + if err != nil { log.Fatal(err) } + if found.Truncated { log.Fatal("incomplete result") } + for _, match := range found.Matches { + log.Printf("%q at input bytes [%d,%d)", match.Text, match.ByteStart, match.ByteEnd) + } + masked, err := dictionary.MaskText(ctx, "한국어 İSTANBUL", '*', nil) + if err != nil { log.Fatal(err) } + log.Print(masked.Text) // *** ******** +} +``` + +See the [R2/R3 report](../r2-r3-performance/) for parity, resource-bound and +cancellation evidence and the same-environment R1 comparison. diff --git a/docs/content/reference/versioned-performance.md b/docs/content/reference/versioned-performance.md new file mode 100644 index 00000000..7bd257ed --- /dev/null +++ b/docs/content/reference/versioned-performance.md @@ -0,0 +1,169 @@ +--- +title: "V3 performance and acceptance report" +description: "Reproducible million-keyword measurements on Redis and Valkey." +--- + +This is the archived R1 baseline. See [R2/R3 comparison](../r2-r3-performance/) +for the subsequent implementation and measurements. + +Measured on 2026-09-06. The final matrix completed **36 runs**: Redis and Valkey, +three dictionary sizes, three deterministic distributions, two repetitions each. +The separate million-entry safety scenario passed on both configured servers. +These are measurements of one environment, not throughput or memory guarantees. + +## Environment and methodology + +- Apple M4, 10 logical CPUs, 16 GiB RAM; macOS 26.5.2, Darwin 25.5.0, arm64. +- Go 1.26.7; Redis 8.10.1; source-built Valkey 9.1.2. +- Standalone TCP on localhost; RDB snapshots and AOF disabled. No production + durability or cross-node network overhead is included. +- MemoryEfficient engine; 50 ms version polling for the measurement (the library + default is 30 seconds); five-minute leases renewed every minute. +- Separate Go process per size/distribution/repetition; two complete repetitions. + This was a developer workstation, with some development checks running on the + host. Treat the ranges as baseline observations, not isolated microbenchmark + confidence intervals. Each server ran one scale workload at a time. +- Fixed seed 20260906. Shared: `shared-prefix-%08d`. Diverse: a random 12-bit hex + prefix plus a unique eight-digit index. Korean: `한국어-%04x-키워드-%08d`, using a + random 16-bit prefix. Full replacement appends `-x` to every keyword. + +**Valkey qualification:** an exploratory run of the local Valkey 9.1.2 build with +its default prefetch setting exited with SIGSEGV in `hashtableIncrementalFindStep` +via `prefetchCommandQueueKeys`. The final successful Valkey matrix used +`prefetch-batch-max-size 0`. This report does not establish support for that local +build's default configuration. The stack excerpt is preserved in +`benchmarks/results/v3-20260906-valkey-crash.txt`; the library returned an error +when the server disappeared. The root cause has not been established. + +The Valkey source archive SHA-256 was +`19c23908e7d57e8d91ef85b41f5646307582f10f4f0fb999bbf89ed24ec9c983` +(tag 9.1.2), built with `make -j4 valkey-server`. Prefetch was disabled using its +existing configuration option, without modifying server source. + +## Initial load and startup + +All table cells show the minimum–maximum across two runs. Load-ready is elapsed +from Replace to the local engine serving its version. Startup opens a new +collection instance against the existing dictionary and builds its first engine. +Peak RSS is the process lifetime high-water mark across the entire run, including +subsequent changes, rather than only initial load. + +| Server | Keywords | Distribution | Load-ready (s) | Startup (s) | Peak RSS (GiB) | +|---|---:|---|---:|---:|---:| +| redis | 10,000 | shared | 0.52–0.53 | 0.23–0.24 | 0.03–0.03 | +| redis | 10,000 | diverse | 0.51–0.61 | 0.25–0.25 | 0.07–0.07 | +| redis | 10,000 | korean | 0.53–0.53 | 0.26–0.26 | 0.14–0.14 | +| redis | 100,000 | shared | 0.64–0.69 | 0.31–0.32 | 0.13–0.14 | +| redis | 100,000 | diverse | 0.74–0.74 | 0.41–0.42 | 0.43–0.43 | +| redis | 100,000 | korean | 0.94–0.99 | 0.56–0.58 | 0.88–0.92 | +| redis | 1,000,000 | shared | 1.81–1.84 | 1.19–1.23 | 1.20–1.20 | +| redis | 1,000,000 | diverse | 3.03–3.07 | 2.23–2.25 | 3.14–3.55 | +| redis | 1,000,000 | korean | 3.89–3.96 | 2.88–2.98 | 3.02–3.74 | +| valkey | 10,000 | shared | 0.51–0.51 | 0.24–0.24 | 0.03–0.03 | +| valkey | 10,000 | diverse | 0.51–0.52 | 0.24–0.25 | 0.07–0.07 | +| valkey | 10,000 | korean | 0.53–0.53 | 0.26–0.26 | 0.13–0.14 | +| valkey | 100,000 | shared | 0.63–0.64 | 0.31–0.32 | 0.14–0.15 | +| valkey | 100,000 | diverse | 0.74–0.74 | 0.41–0.42 | 0.42–0.43 | +| valkey | 100,000 | korean | 0.92–0.92 | 0.57–0.57 | 0.85–0.89 | +| valkey | 1,000,000 | shared | 1.83–1.84 | 1.19–1.53 | 1.21–1.30 | +| valkey | 1,000,000 | diverse | 2.98–3.06 | 2.21–2.51 | 3.02–3.80 | +| valkey | 1,000,000 | korean | 3.86–3.87 | 2.85–2.88 | 3.37–3.69 | + +## Million-keyword changes + +Commit below includes normalization, bucket reads/preparation, manifest storage, +and final pointer commit. Ready also includes notification/polling and engine +rebuilding. Their difference is the calling instance's serving-version lag; +fleet-wide simultaneous activation is not asserted. Unchanged buckets are reused, +but every changed generation still rebuilds its entire local engine. + +| Server | Distribution | Add 1 commit / ready (s) | Add 1,000 ready (s) | Add 1% ready (s) | Full replace ready (s) | +|---|---|---:|---:|---:|---:| +| redis | shared | 0.006–0.006 / 1.20–1.21 | 1.40–1.41 | 2.07–2.08 | 2.86–2.88 | +| redis | diverse | 0.006–0.006 / 2.28–2.30 | 2.70–2.71 | 3.01–3.38 | 3.98–4.34 | +| redis | korean | 0.006–0.006 / 2.96–3.17 | 3.55–4.48 | 4.63–6.11 | 7.43–7.71 | +| valkey | shared | 0.005–0.005 / 1.21–1.23 | 1.40–1.41 | 2.05–2.08 | 2.86–2.88 | +| valkey | diverse | 0.005–0.007 / 2.33–2.40 | 2.42–2.60 | 3.03–3.54 | 4.08–4.33 | +| valkey | korean | 0.005–0.005 / 2.90–3.40 | 4.03–4.38 | 3.98–4.49 | 6.78–7.31 | + +The JSON report also includes removals of 1, 1,000 and 1%, and identical Replace. +At 100,000 entries, 1,000 and 1% are the same count and are measured as two +successive add/remove cycles. Identical Replace still reads and compares buckets; +it does not create a generation or rebuild the engine. + +## Search during replacement and retained storage + +Search sampling uses a short string assembled from three dictionary keywords, at +approximately one-millisecond intervals while a write and its engine refresh run. +The timer includes that small string construction and the Find call. The old +engine continues serving until replacement; initial load starts with an empty +engine. These figures are not a general text-search throughput benchmark. The +following cells summarize full replacement at one million keywords. + +| Server | Distribution | Search p50 / p95 / p99 (µs) | Redis before prune (MiB) | Redis after prune (MiB) | +|---|---|---:|---:|---:| +| redis | shared | 2.33–2.33 / 3.46–3.50 / 9.75–12.04 | 94.29–94.31 | 31.42–31.42 | +| redis | diverse | 2.50–2.62 / 4.71–6.29 / 12.21–14.79 | 66.61–66.63 | 23.06–23.08 | +| redis | korean | 3.00–3.04 / 12.38–13.00 / 30.08–31.38 | 134.60–134.60 | 43.82–43.84 | +| valkey | shared | 2.38–2.38 / 3.33–3.62 / 9.50–10.33 | 94.49–94.51 | 31.60–31.60 | +| valkey | diverse | 2.50–2.71 / 3.96–5.46 / 12.33–14.38 | 66.82–66.82 | 23.25–23.26 | +| valkey | korean | 3.04–3.04 / 11.62–14.00 / 25.04–29.92 | 134.78–134.78 | 43.99–43.99 | + +Redis memory is server-wide `used_memory`, including server overhead, receipts, +script cache, and retained manifests/chunks. The pruning measurement artificially +ages inactive generation registry timestamps beyond 24 hours; the active +generation remains protected. Actual retention is tested separately. Network byte +counters in the JSON include both writes and engine downloads/polling, as well as +protocol and measurement overhead; they are not just payload keyword bytes. + +## Acceptance evidence + +- The 36 scale runs completed initial loading, reopening, all paginated entries, + naive-reference search comparison, no-op/full replacements, 1/1,000/1% additions + and removals, WaitForVersion and explicit pruning. +- `TestVersionedMillionSafety` passed on both configured real servers: one of two + writers using the same million-entry expected version won; the other conflicted. + A pinned million-entry snapshot remained readable across commit and Prune, + contained no competing-generation keywords, and was collected only after close. +- Unit/race tests compare existing V2 engine search results with V3 for Korean, + multilingual, nested/overlapping, case-sensitive and stream-boundary cases. + Concurrent batch scans preserve a single engine generation. +- Fault injection covers chunk preparation errors, abandoned prepared generations, + lost commit responses and delayed retries, dropped Pub/Sub, failed engine loads, + reconnect, lease expiry, and a stale pruning fence. These are deterministic + simulations; they are not a production failover or forced-OS-process-kill test. +- Chunk-write instrumentation confirms an identical dictionary writes no chunks + and a single change writes only its affected bucket. The final Lua commit only + checks fixed keys, writes a receipt/marker and swaps the pointer; it does not + parse a manifest or iterate keywords. +- Root and server race suites, root/server lint, all-module vet, benchmark-module + tests, API snapshot/audit, documentation compilation, unchanged module manifests + and license attribution passed. FuzzFind and FuzzAdd ran for 30 seconds each; + V3 normalization fuzzing ran for 10 seconds. No production acor function remained + at zero coverage in the normal test suite. + +No real Cluster/Sentinel failover matrix or production persistence recovery was +run for V3 here. All collection keys use one hash tag and the existing connection +implementations; do not interpret standalone measurements as multi-node results. +R2 incremental download reuse and build-memory/cancellation improvements, and R3 +result limits/masking/replacement, remain follow-up roadmap work. + +## Reproduce + +Use a disposable server with the same persistence, preset and polling settings. +From the repository root: + +```sh +ACOR_V3_SCALE_ADDR=127.0.0.1:6379 \ +ACOR_V3_SCALE_OUTPUT=/tmp/acor-v3-redis \ +ACOR_V3_SCALE_REPEATS=2 make bench-v3 +``` + +Repeat against Valkey with `prefetch-batch-max-size 0`, using a separate output +directory. `scripts/benchmark-v3.sh` compiles one test binary and starts a fresh +process for every size/distribution, then runs the million-entry safety scenario. +It removes only its uniquely named collection keys, never FLUSHDB. Do not run +another workload on the same endpoint during measurements: Redis memory/network +counters are server-wide. The raw operation summaries, environment strings, +startup/prune measurements and source hashes for this report are in +`benchmarks/results/v3-20260906.json`. diff --git a/docs/content/reference/versioned.md b/docs/content/reference/versioned.md new file mode 100644 index 00000000..4bdf50e9 --- /dev/null +++ b/docs/content/reference/versioned.md @@ -0,0 +1,177 @@ +--- +title: "Versioned dictionaries (V3)" +description: "Large dictionary snapshots, atomic changes, and background search refresh." +--- + +V3 is an opt-in storage format in the existing Go module. Use `OpenVersioned` +and a new collection name. Existing `Create`, V1/V2 keys, and their error and +update contracts remain unchanged. V3 does not provide Suggest or a server API. + + +```go +package main + +import ( + "context" + "log" + + "github.com/skyoo2003/acor/pkg/acor" +) + +func main() { + ctx := context.Background() + dictionary, err := acor.OpenVersioned(ctx, &acor.VersionedOptions{ + Redis: acor.AhoCorasickArgs{Addr: "localhost:6379", Name: "filter-v3"}, + }) + if err != nil { log.Fatal(err) } + defer dictionary.Close() + + snapshot, err := dictionary.Snapshot(ctx) + if err != nil { log.Fatal(err) } + expected := snapshot.Version() + snapshot.Close(ctx) + + result, err := dictionary.Replace(ctx, expected, []string{"한국", "hello"}) + if err != nil { log.Fatal(err) } + if err := dictionary.WaitForVersion(ctx, result.Version); err != nil { log.Fatal(err) } + matches, err := dictionary.Find(ctx, "HELLO 한국") + if err != nil { log.Fatal(err) } + log.Print(matches) +} +``` + +## API contracts + +All I/O methods take a context. `Status`, `Snapshot.Version`, and `Snapshot.Count` +read local state. `Close` on the collection stops its background work; snapshot +`Close(ctx)` releases its lease. Initial loading fails the open operation. + +`Snapshot.List(ctx, cursor, limit)` returns up to a positive limit, ordered by +bucket number then lexical keyword order. Pass an empty cursor to start and stop +when `NextCursor` is empty. Keep the same snapshot for a complete traversal: +a cursor is bound to the generation. `Snapshot.Diff(ctx, target)` returns sorted +Added, Removed, and Retained entries without writing. Diff materializes the +comparison in memory. Count is the normalized, deduplicated keyword count. + +`Replace`, `Add`, `Remove`, `AddMany`, and `RemoveMany` require an expected Version. +Treat versions as opaque equality tokens, never ordered numbers or strings. +Concurrent writers using the same expected version cannot overwrite each other; +use `errors.Is(err, acor.ErrConcurrencyConflict)`. Batch changes are atomic. +Replace accepts an empty target. Reapplying an identical dictionary keeps the +version and does not publish invalidation. Keywords are trimmed and, unless +CaseSensitive was set at creation, lowercased. Blank keywords and invalid UTF-8 +fail the entire input. Duplicate normalized inputs are removed. The stored case +policy must match every subsequent open. + +A successful write means the Redis commit completed. The calling instance and +other instances may still search the previous engine. Use `WaitForVersion` to +wait for that commit or a later one; it honors cancellation. Status exposes the +observed active version, serving version, build start/duration, and recent error. +A failed refresh preserves the old serving engine. Polling defaults to 30 seconds, +with Pub/Sub accelerating discovery. A fixed 20 ms debounce window (RefreshDebounce) merges bursts before each +background build. Incoming events do not extend the window. The worker finishes +its current build, then loads the newest observed target. Every search, including a batch, +parallel scan, or stream, uses one engine. V3 reuses existing rune positions, +case folding, overlap and word-boundary semantics. + +`Find`, `FindIndex`, `FindMatches`, `FindSet`, `Contains`, `FindStream`, +`FindBatch`, `FindParallel`, and `FindIndexParallel` are available with explicit +contexts. Streaming keeps state across reader boundaries. The existing parallel +options still apply; use AutoOverlap for dictionary-length boundary protection. + +## Storage and failure handling + +SHA-256's first 12 bits select one of 4,096 fixed buckets. Each bucket is sorted, +deduplicated, and split into JSON string-array chunks of at most 1 MiB including +JSON escaping and delimiters. An oversized individual keyword gets its own chunk. +Content-addressed immutable chunks, generation manifests, metadata, and the active +pointer use separate keys. All keys share a name-digest Redis hash tag, hence one +Cluster slot. This does not distribute one collection across multiple nodes. +Connection options reuse existing standalone, Sentinel, Cluster and Ring clients. +Only connection fields and Name from VersionedOptions.Redis are used. + +Delta writes download and prepare affected buckets only; full replacements compare +all buckets and reuse unchanged ones. Local engine refresh reuses verified immutable keyword slices for unchanged +buckets and downloads changed buckets only. The previous cache remains usable +when a candidate fails. Full local engine rebuilding still requires memory for +the old engine, retained keyword slices, and the new engine. The default +preset is MemoryEfficient; Speed and Balanced are also selectable. No constant +memory or unmeasured latency guarantee is made. + +The final Lua commit checks the expected pointer, preparation lease and maintenance +lock, stores a receipt, and changes the pointer. It does not parse the manifest or +iterate keywords. Prepared data is unreachable until committed. Storage durability +and failover behavior still depend on the Redis/Valkey persistence configuration. + +On `ErrCommitUnknown`, retain the returned WriteResult.OperationID and use +`ResolveOperation(ctx, id)`. A found receipt is authoritative success. A missing +receipt is **not** proof that an in-flight request cannot commit later. Do not +blindly reapply ambiguous writes. Operation receipts and committed-version markers +are retained indefinitely, including no-op receipts, so they grow with write count. + +## Leases and explicit pruning + +Snapshots, builds and preparation writers use server-time leases (five minutes, +renewed every minute by default). Lease checks fail explicitly after expiration; +renewal cannot resurrect an expired handle. Close snapshots promptly. Existing +local searches need no Redis lease once their engine is built. + +`Prune(ctx)` retains the active generation, generations prepared or committed +within 24 hours, and generations protected by valid reader leases. It deletes +unreferenced chunks and unretained manifests in batches of at most 64. It also +collects abandoned preparation data. Receipts are not deleted. + +Prune acquires a monotonically increasing maintenance fence only when no valid +writer lease remains. New writers and snapshots temporarily return ErrMaintenance; +searches continue. Each deletion checks and extends lock ownership atomically. +The maintenance lock expires after 30 seconds if its process dies. An expired +pruner cannot delete after a successor takes ownership. Preparation itself is +fenced so an expired writer cannot create unregistered data during cleanup. +Large retention sets are enumerated in memory; there is no automatic collector. +R2 builds check cancellation throughout trie insertion, failure-link construction, +and table filling. Close cancels the active build; initialization honors its own +context. Cancellation discards a candidate without publishing it or leaving a +detached builder goroutine. Individual allocation, string length and sort +operations complete before the next checkpoint. Ordinary new commits do not +cancel an active build, avoiding starvation under continuous writes. + +## V2 cutover + +1. Use a **different** V3 name. Migrate V1 to V2 through the existing migration + first. `CopyV2(ctx, sourceName, expected, nil)` reads the V2 version and keywords in + one HMGET and replaces the V3 target. It reports source version, normalized + count and SHA-256 checksum of the sorted JSON keyword array. +2. For rehearsal, copy and compare count, checksum and representative search + results, including case-sensitive, Korean and overlapping matches. Set the + V3 case policy to the source application's policy (V2 does not persist it). +3. Stop V2 writes for the final copy. Verify its source version, count, checksum + and search results, then change application configuration to the new V3 name. + There is no automatic dual write or simultaneous fleet engine switch. +4. After V3 writes begin, switching configuration back to V2 loses those changes. + Export and apply the V3 changes back to V2 before rollback; a simple name change + is insufficient. Keep V2 untouched until the migration has been accepted. + +## CLI + +Global connection/name flags precede `dictionary`; its flags follow the subcommand. +Input to diff/replace is one JSON string array on stdin. Empty copy-v2 sources +also require `--allow-empty`; the guard runs before replacing the target. Status and other results +are JSON. List is one page per invocation; concurrent generation changes reject a +previous page cursor. Library callers should retain a Snapshot for a long export. + +```sh +acor -name filter-v3 dictionary status +acor -name filter-v3 dictionary list --limit 1000 +printf '["한국","hello"]' | acor -name filter-v3 dictionary diff +printf '["한국","hello"]' | acor -name filter-v3 dictionary replace --expected-version TOKEN +printf '[]' | acor -name filter-v3 dictionary replace --expected-version TOKEN --allow-empty +acor -name filter-v3 dictionary copy-v2 --source filter-v2 --expected-version TOKEN +acor -name filter-v3 dictionary prune +``` + +R1 validation and measured resource costs are recorded in +[the reproducible V3 performance report](../versioned-performance/). R2 adds incremental download reuse, bounded coalescing, cancellable builds, and +more compact sparse-engine nodes. R3 adds bounded source-position search and +atomic masking/replacement; see [bounded text processing](../text-processing/) and +[the R2/R3 verification report](../r2-r3-performance/). R1 measurements remain +archived as the baseline; none of these reports are performance guarantees. diff --git a/internal/engine/build_context.go b/internal/engine/build_context.go new file mode 100644 index 00000000..956c2f13 --- /dev/null +++ b/internal/engine/build_context.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 + +package engine + +import ( + "context" + "iter" + "unicode/utf8" +) + +type buildCanceled struct{ err error } +type buildGuard struct { + ctx context.Context + steps uint32 +} + +func (g *buildGuard) check() { + if g == nil { + return + } + g.steps++ + if g.steps%1024 == 0 { + g.now() + } +} +func (g *buildGuard) now() { + if err := g.ctx.Err(); err != nil { + panic(buildCanceled{err}) + } +} + +// BuildSequenceContext builds a fresh engine transactionally from a unique +// keyword sequence. Cancellation never publishes a partial engine or leaves a +// detached builder goroutine running. Allocation and sorting remain synchronous. +func (e *Engine) BuildSequenceContext(ctx context.Context, words iter.Seq[string], count int) (err error) { + if contextErr := ctx.Err(); contextErr != nil { + return contextErr + } + defer func() { + if value := recover(); value != nil { + if canceled, ok := value.(buildCanceled); ok { + err = canceled.err + } else { + panic(value) + } + } + }() + next := New(e.preset) + guard := &buildGuard{ctx: ctx} + switch impl := next.impl.(type) { + case *memEfficientEngine: + impl.guard = guard + case *speedEngine: + impl.guard = guard + case *balancedEngine: + impl.banded.dat.guard = guard + } + maxRunes := 0 + sequence := func(yield func(string) bool) { + for word := range words { + guard.check() + maxRunes = max(maxRunes, utf8.RuneCountInString(word)) + if !yield(word) { + return + } + } + } + if impl, ok := next.impl.(*memEfficientEngine); ok { + impl.buildFromSequence(sequence, count) + impl.guard = nil + } else { + set := make(map[string]struct{}, count) + for word := range sequence { + set[word] = struct{}{} + } + next.impl.buildFromKeywords(set) + switch impl := next.impl.(type) { + case *speedEngine: + impl.guard = nil + case *balancedEngine: + impl.banded.dat.guard = nil + } + } + guard.now() + next.maxKeywordRunes = maxRunes + *e = *next + return nil +} diff --git a/internal/engine/build_context_test.go b/internal/engine/build_context_test.go new file mode 100644 index 00000000..ab054a0f --- /dev/null +++ b/internal/engine/build_context_test.go @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 + +package engine + +import ( + "context" + "errors" + "fmt" + "iter" + "maps" + "reflect" + "testing" +) + +func TestBuildSequenceContextParity(t *testing.T) { + words := map[string]struct{}{"he": {}, "she": {}, "hers": {}, "한국": {}, "한국어": {}, "aa": {}, "aaa": {}} + for _, preset := range []Preset{PresetSpeed, PresetBalanced, PresetMemoryEfficient} { + t.Run(preset.String(), func(t *testing.T) { + old := New(preset) + old.Build(words) + next := New(preset) + if err := next.BuildSequenceContext(context.Background(), maps.Keys(words), len(words)); err != nil { + t.Fatal(err) + } + text := "she hers 한국어 aaaa" + if !reflect.DeepEqual(old.FindIndex(text), next.FindIndex(text)) { + t.Fatal("context builder changed matches") + } + if next.MaxKeywordRunes() != old.MaxKeywordRunes() { + t.Fatal("max length mismatch") + } + }) + } +} +func TestBuildSequenceCancellationPreservesEngine(t *testing.T) { + for _, preset := range []Preset{PresetSpeed, PresetBalanced, PresetMemoryEfficient} { + t.Run(preset.String(), func(t *testing.T) { + e := New(preset) + e.Build(map[string]struct{}{"old": {}}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + var words iter.Seq[string] = func(yield func(string) bool) { + for i := range 10000 { + if i == 1500 { + cancel() + } + if !yield(fmt.Sprintf("new-%08d", i)) { + return + } + } + } + if err := e.BuildSequenceContext(ctx, words, 10000); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + if !e.Contains("old") || e.Contains("new-00000001") { + t.Fatal("partially built engine published") + } + }) + } +} + +// countContext cancels in CPU build phases after sequence ingestion finishes. +// It makes the cancellation checkpoint coverage deterministic without sleeps. +type countContext struct { + context.Context + checks int + remaining int +} + +func (c *countContext) Err() error { + c.checks++ + if c.checks >= c.remaining { + return context.Canceled + } + return nil +} +func TestBuildSequenceCPUPhaseCancellation(t *testing.T) { + words := make(map[string]struct{}) + for i := range 1000 { + words[fmt.Sprintf("long-shared-prefix-%08d", i)] = struct{}{} + } + for _, preset := range []Preset{PresetSpeed, PresetBalanced, PresetMemoryEfficient} { + ctx := &countContext{Context: context.Background(), remaining: 8} + e := New(preset) + e.Build(map[string]struct{}{"old": {}}) + if err := e.BuildSequenceContext(ctx, maps.Keys(words), len(words)); !errors.Is(err, context.Canceled) { + t.Fatal(preset, err) + } + if !e.Contains("old") { + t.Fatal("lost previous engine") + } + } +} +func TestBuildSequenceDoesNotSwallowPanics(t *testing.T) { + defer func() { + if recover() != "caller panic" { + t.Fatal("unexpected panic recovery") + } + }() + e := New(PresetMemoryEfficient) + _ = e.BuildSequenceContext(context.Background(), func(func(string) bool) { panic("caller panic") }, 0) +} diff --git a/internal/engine/engine_banded_dfa.go b/internal/engine/engine_banded_dfa.go index be89b4ce..a382a8ac 100644 --- a/internal/engine/engine_banded_dfa.go +++ b/internal/engine/engine_banded_dfa.go @@ -69,6 +69,7 @@ func (bd *bandedDFA) buildDFABand() { alphaSize := len(dat.runes) bd.bandOff = make([]int32, dat.size) for s := range bd.bandOff { + dat.guard.check() bd.bandOff[s] = bandNotBanded } @@ -87,11 +88,13 @@ func (bd *bandedDFA) buildDFABand() { bd.band = make([]int32, rows*alphaSize) off := int32(0) for s := datRootPos; s < dat.size; s++ { + dat.guard.check() if !inBand(dat, s, bd.bandDepth) { continue } bd.bandOff[s] = off for ai := range dat.runes { + dat.guard.check() next := dat.followFailByCode(s, ai) bd.band[off+int32(ai)] = packState(next, next < len(dat.hasOutput) && dat.hasOutput[next]) //nolint:gosec // G115: ai < alphaSize, bounded above. } @@ -116,6 +119,7 @@ func inBand(dat *doubleArrayTrie, s, bandDepth int) bool { func bandRows(dat *doubleArrayTrie, bandDepth int) int { rows := 0 for s := datRootPos; s < dat.size; s++ { + dat.guard.check() if inBand(dat, s, bandDepth) { rows++ } diff --git a/internal/engine/engine_bloom.go b/internal/engine/engine_bloom.go index b9e676ca..e35f6886 100644 --- a/internal/engine/engine_bloom.go +++ b/internal/engine/engine_bloom.go @@ -4,7 +4,6 @@ package engine import ( "math" - "unicode/utf8" ) // bloomFilter is a space-efficient probabilistic data structure for testing @@ -73,18 +72,6 @@ func (bf *bloomFilter) skipAtRoot(atRoot bool, r rune) bool { return bf != nil && atRoot && !bf.mightContain(r) } -// buildFirstRuneBloom builds a Bloom filter of the first rune of each keyword, -// used as a root-state pre-filter to skip characters that cannot start a match. -func buildFirstRuneBloom(keywords map[string]struct{}) *bloomFilter { - bf := newBloomFilter(len(keywords), 0.01) - for kw := range keywords { - if r, size := utf8.DecodeRuneInString(kw); size > 0 { - bf.add(r) - } - } - return bf -} - func (bf *bloomFilter) hashPair(r rune) (hash1, hash2 uint64) { //nolint:gosec var h1 uint64 = 14695981039346656037 h1 ^= uint64(r) //nolint:gosec diff --git a/internal/engine/engine_dat.go b/internal/engine/engine_dat.go index cc9dcc41..c2bfb6fa 100644 --- a/internal/engine/engine_dat.go +++ b/internal/engine/engine_dat.go @@ -11,6 +11,7 @@ import "slices" // Position 0 is unused (sentinel); root is at position 1. This avoids the // ambiguity where check[pos]=0 could mean either "empty" or "parent is state 0". type doubleArrayTrie struct { + guard *buildGuard // base, check and fail are int32 rather than int: three arrays cost 12 bytes per // state instead of 24. Overflowing int32 would need over 8 GiB of double-array, // so the allocation fails first. @@ -70,6 +71,7 @@ func (dat *doubleArrayTrie) expand() { func (dat *doubleArrayTrie) ensureCapacity(needed int) { for needed >= dat.cap { + dat.guard.check() dat.expand() } } @@ -88,12 +90,14 @@ func (dat *doubleArrayTrie) buildFromKeywords(keywords map[string]struct{}) { // runeSet := make(map[rune]struct{}) for kw := range keywords { + dat.guard.check() for _, ch := range kw { runeSet[ch] = struct{}{} } } dat.runes = make([]rune, 0, len(runeSet)) for r := range runeSet { + dat.guard.check() dat.runes = append(dat.runes, r) } slices.Sort(dat.runes) @@ -105,6 +109,7 @@ func (dat *doubleArrayTrie) buildFromKeywords(keywords map[string]struct{}) { // tmpChildren[0] = make(map[rune]int) for kw := range keywords { + dat.guard.check() // An empty keyword would end at the root, where own == 0 already means "no // keyword". The public API rejects empty keywords; skipping keeps this trie // correct on its own. @@ -113,6 +118,7 @@ func (dat *doubleArrayTrie) buildFromKeywords(keywords map[string]struct{}) { // } cur := 0 for _, ch := range kw { + dat.guard.check() if _, ok := tmpChildren[cur][ch]; !ok { if tmpChildren[cur] == nil { tmpChildren[cur] = make(map[rune]int) @@ -139,6 +145,7 @@ func (dat *doubleArrayTrie) buildFromKeywords(keywords map[string]struct{}) { // queue = append(queue, 0) for len(queue) > 0 { + dat.guard.check() parent := queue[0] queue = queue[1:] @@ -149,6 +156,7 @@ func (dat *doubleArrayTrie) buildFromKeywords(keywords map[string]struct{}) { // codes := make([]int, 0, len(children)) for ch := range children { + dat.guard.check() codes = append(codes, dat.index[ch]) } @@ -156,6 +164,7 @@ func (dat *doubleArrayTrie) buildFromKeywords(keywords map[string]struct{}) { // dat.base[datPos[parent]] = int32(base) //nolint:gosec // G115: see the int32 note on the fields. for ch, childID := range children { + dat.guard.check() code := dat.index[ch] pos := base + code dat.ensureCapacity(pos + 1) @@ -169,6 +178,7 @@ func (dat *doubleArrayTrie) buildFromKeywords(keywords map[string]struct{}) { // if kw, ok := tmpOwn[childID]; ok && kw != "" { for pos >= len(dat.out.own) { + dat.guard.check() dat.out.own = append(dat.out.own, 0) } dat.out.own[pos] = dat.out.assignID(kw) @@ -195,6 +205,7 @@ func (dat *doubleArrayTrie) buildFromKeywords(keywords map[string]struct{}) { // dat.out.outLink = make([]int32, dat.size) for s := range dat.out.outLink { + dat.guard.check() dat.out.outLink[s] = outNone } dat.computeFailLinks() @@ -202,6 +213,7 @@ func (dat *doubleArrayTrie) buildFromKeywords(keywords map[string]struct{}) { // // Derived after the fail links, which fill the output chain. dat.hasOutput = make([]bool, dat.size) for s := 0; s < dat.size; s++ { + dat.guard.check() dat.hasOutput[s] = dat.out.own[s] != 0 || dat.out.outLink[s] != outNone } } @@ -212,6 +224,7 @@ func (dat *doubleArrayTrie) findBase(codes []int) int { } minCode := codes[0] for _, c := range codes[1:] { + dat.guard.check() if c < minCode { minCode = c } @@ -219,8 +232,10 @@ func (dat *doubleArrayTrie) findBase(codes []int) int { // Start from a base that places minCode at position datRootPos+1 (skip sentinel). for base := (datRootPos + 1) - minCode; ; base++ { + dat.guard.check() conflict := false for _, code := range codes { + dat.guard.check() pos := base + code if pos >= dat.cap { dat.expand() @@ -245,6 +260,7 @@ func (dat *doubleArrayTrie) computeFailLinks() { // Root's direct children: check[pos] == datRootPos. for i := datRootPos + 1; i < dat.size; i++ { + dat.guard.check() if dat.check[i] == datRootPos { dat.fail[i] = datRootPos queue = append(queue, i) @@ -253,10 +269,12 @@ func (dat *doubleArrayTrie) computeFailLinks() { dat.fail[datRootPos] = datRootPos for len(queue) > 0 { + dat.guard.check() state := queue[0] queue = queue[1:] for code := range dat.runes { + dat.guard.check() next := dat.gotoStateByCode(state, code) if next == 0 { continue diff --git a/internal/engine/engine_flat.go b/internal/engine/engine_flat.go index 0c26f020..21d0a471 100644 --- a/internal/engine/engine_flat.go +++ b/internal/engine/engine_flat.go @@ -26,6 +26,7 @@ var _ matchEngine = (*speedEngine)(nil) // speedEngine implements matchEngine using a flat array trie with Full DFA // transitions and compact alphabet mapping. Used by PresetSpeed. type speedEngine struct { + guard *buildGuard // dfa is the transition table flattened to state*alphaSize+alphabetIndex. A // [][]int cost the scan a slice-header load and two bounds checks per // character, where one flat []int32 costs a single indexed load. Each entry @@ -58,12 +59,14 @@ func (e *speedEngine) buildFromKeywords(keywords map[string]struct{}) { //nolint runeSet := make(map[rune]struct{}) for kw := range keywords { + e.guard.check() for _, ch := range kw { runeSet[ch] = struct{}{} } } e.alphabet = make([]rune, 0, len(runeSet)) for r := range runeSet { + e.guard.check() e.alphabet = append(e.alphabet, r) } slices.Sort(e.alphabet) @@ -74,11 +77,13 @@ func (e *speedEngine) buildFromKeywords(keywords map[string]struct{}) { //nolint } sortedKw := make([]string, 0, len(keywords)) for kw := range keywords { + e.guard.check() sortedKw = append(sortedKw, kw) } slices.Sort(sortedKw) outs := outputs{} for _, kw := range sortedKw { + e.guard.check() // An empty keyword would end at the root, where ownID == 0 already means // "no keyword". The public API rejects empty keywords; skipping keeps this // engine correct on its own. @@ -87,6 +92,7 @@ func (e *speedEngine) buildFromKeywords(keywords map[string]struct{}) { //nolint } state := 0 for _, ch := range kw { + e.guard.check() child, ok := nodes[state].gotoMap[ch] if !ok { child = len(nodes) @@ -119,6 +125,7 @@ func (e *speedEngine) buildFromKeywords(keywords map[string]struct{}) { //nolint child int }, 0, len(gotoMap)) for ch, child := range gotoMap { + e.guard.check() pairs = append(pairs, struct { ch rune child int @@ -134,16 +141,19 @@ func (e *speedEngine) buildFromKeywords(keywords map[string]struct{}) { //nolint } for _, pair := range sortedChildren(nodes[0].gotoMap) { + e.guard.check() nodes[pair.child].fail = 0 queue = append(queue, pair.child) } for len(queue) > 0 { + e.guard.check() state := queue[0] queue = queue[1:] bfsOrder = append(bfsOrder, state) for _, pair := range sortedChildren(nodes[state].gotoMap) { + e.guard.check() ch := pair.ch child := pair.child queue = append(queue, child) @@ -154,6 +164,7 @@ func (e *speedEngine) buildFromKeywords(keywords map[string]struct{}) { //nolint // fail link at itself (e.g. keywords {a,aa,aaa}), corrupting the DFA. fail := nodes[state].fail for fail != 0 { + e.guard.check() if _, ok := nodes[fail].gotoMap[ch]; ok { break } @@ -179,6 +190,7 @@ func (e *speedEngine) buildFromKeywords(keywords map[string]struct{}) { //nolint outs.own = make([]int32, numStates) outs.outLink = make([]int32, numStates) for i := range nodes { + e.guard.check() outs.own[i] = nodes[i].ownID outs.outLink[i] = nodes[i].outLink } @@ -194,6 +206,7 @@ func (e *speedEngine) buildFromKeywords(keywords map[string]struct{}) { //nolint } for ai, r := range e.alphabet { + e.guard.check() if child, ok := nodes[0].gotoMap[r]; ok { e.dfa[ai] = enc(child) } else { @@ -207,9 +220,11 @@ func (e *speedEngine) buildFromKeywords(keywords map[string]struct{}) { //nolint // so a fail link can point to a higher-id row that is still all zeros, which // silently drops matches. for _, s := range bfsOrder { + e.guard.check() row := s * alphaSize failRow := nodes[s].fail * alphaSize for ai, r := range e.alphabet { + e.guard.check() if child, ok := nodes[s].gotoMap[r]; ok { e.dfa[row+ai] = enc(child) } else { @@ -223,6 +238,7 @@ func (e *speedEngine) buildFromKeywords(keywords map[string]struct{}) { //nolint // so a rebuild from shorter keywords must report the shorter depth. e.trieDepth = 0 for i := range nodes { + e.guard.check() if nodes[i].depth > e.trieDepth { e.trieDepth = nodes[i].depth } diff --git a/internal/engine/engine_handle.go b/internal/engine/engine_handle.go index e046cf00..1740ce55 100644 --- a/internal/engine/engine_handle.go +++ b/internal/engine/engine_handle.go @@ -25,13 +25,14 @@ const findResultHint = 8 // package can build and query the automaton without depending on the concrete // engine types (which stay unexported). type Engine struct { + preset Preset impl matchEngine maxKeywordRunes int } // New returns an Engine backed by the implementation selected for preset. func New(preset Preset) *Engine { - return &Engine{impl: newMatchEngine(preset)} + return &Engine{impl: newMatchEngine(preset), preset: preset} } // Build (re)constructs the automaton from the given keyword set. diff --git a/internal/engine/engine_map.go b/internal/engine/engine_map.go index e55704a0..59e84037 100644 --- a/internal/engine/engine_map.go +++ b/internal/engine/engine_map.go @@ -2,11 +2,19 @@ package engine +import ( + "iter" + "maps" + "unicode/utf8" +) + // mapNode is a trie node using Go maps for children (sparse representation). type mapNode struct { - children map[rune]int - fail int - depth int + children map[rune]int + singleRune rune + singleChild int + fail int + depth int } // mapTrie is a trie backed by a slice of mapNodes. @@ -23,6 +31,7 @@ var _ matchEngine = (*memEfficientEngine)(nil) // memEfficientEngine implements matchEngine using a map-based sparse trie // with standard NFA (failure links). Used by PresetMemoryEfficient. type memEfficientEngine struct { + guard *buildGuard trie mapTrie bloom *bloomFilter } @@ -32,29 +41,36 @@ func newMemEfficientEngine() *memEfficientEngine { } func (e *memEfficientEngine) buildFromKeywords(keywords map[string]struct{}) { + e.buildFromSequence(maps.Keys(keywords), len(keywords)) +} +func (e *memEfficientEngine) buildFromSequence(keywords iter.Seq[string], count int) { trie := mapTrie{ nodes: []mapNode{ - {children: make(map[rune]int), depth: 0}, + {depth: 0}, }, - out: outputs{own: []int32{0}, outLink: []int32{outNone}}, + out: outputs{own: []int32{0}, outLink: []int32{outNone}, keywords: make([]string, 1, count+1), runeLens: make([]int32, 1, count+1)}, } + firstRunes := make(map[rune]struct{}) for kw := range keywords { + e.guard.check() // An empty keyword would end at the root, where own == 0 already means "no // keyword". The public API rejects empty keywords; skipping keeps this engine // correct on its own. if kw == "" { continue } + r, _ := utf8.DecodeRuneInString(kw) + firstRunes[r] = struct{}{} state := 0 for _, ch := range kw { - child, ok := trie.nodes[state].children[ch] + e.guard.check() + child, ok := trie.nodes[state].next(ch) if !ok { child = len(trie.nodes) - trie.nodes[state].children[ch] = child + trie.nodes[state].addChild(ch, child) trie.nodes = append(trie.nodes, mapNode{ - children: make(map[rune]int), - depth: trie.nodes[state].depth + 1, + depth: trie.nodes[state].depth + 1, }) trie.out.own = append(trie.out.own, 0) trie.out.outLink = append(trie.out.outLink, outNone) @@ -64,36 +80,32 @@ func (e *memEfficientEngine) buildFromKeywords(keywords map[string]struct{}) { trie.out.own[state] = trie.out.assign(trie.out.own[state], kw) } - type queueEntry struct { - ch rune - state int - } - queue := make([]queueEntry, 0) - for ch, child := range trie.nodes[0].children { - trie.nodes[child].fail = 0 - queue = append(queue, queueEntry{ch, child}) + queue := make([]int, 0, len(trie.nodes)) + for _, child := range trie.nodes[0].edges() { + queue = append(queue, child) } - for len(queue) > 0 { - entry := queue[0] - queue = queue[1:] + for head := 0; head < len(queue); head++ { + e.guard.check() + state := queue[head] - for ch, child := range trie.nodes[entry.state].children { - queue = append(queue, queueEntry{ch, child}) + for ch, child := range trie.nodes[state].edges() { + queue = append(queue, child) // Walk failure links to the deepest state that has a `ch` child, then // apply goto(fail, ch) exactly once below. Assigning inside the loop // and re-applying after would double-apply goto and can point a state's // fail link at itself (e.g. keywords {a,aa,aaa}), causing find to loop // forever following that self-referential fail link. - fail := trie.nodes[entry.state].fail + fail := trie.nodes[state].fail for fail != 0 { - if _, ok := trie.nodes[fail].children[ch]; ok { + e.guard.check() + if _, ok := trie.nodes[fail].next(ch); ok { break } fail = trie.nodes[fail].fail } - if next, ok := trie.nodes[fail].children[ch]; ok { + if next, ok := trie.nodes[fail].next(ch); ok { fail = next } @@ -110,7 +122,11 @@ func (e *memEfficientEngine) buildFromKeywords(keywords map[string]struct{}) { } e.trie = trie - e.bloom = buildFirstRuneBloom(keywords) + e.bloom = newBloomFilter(len(firstRunes), 0.01) + for r := range firstRunes { + e.guard.check() + e.bloom.add(r) + } } func (e *memEfficientEngine) find(text string) []string { @@ -127,7 +143,7 @@ func (e *memEfficientEngine) find(text string) []string { } for { - if next, ok := e.trie.nodes[state].children[ch]; ok { + if next, ok := e.trie.nodes[state].next(ch); ok { state = next break } @@ -159,7 +175,7 @@ func (e *memEfficientEngine) findSet(text string) []string { } for { - if next, ok := e.trie.nodes[state].children[ch]; ok { + if next, ok := e.trie.nodes[state].next(ch); ok { state = next break } @@ -191,7 +207,7 @@ func (e *memEfficientEngine) findIndex(text string) map[string][]int { } for { - if next, ok := e.trie.nodes[state].children[ch]; ok { + if next, ok := e.trie.nodes[state].next(ch); ok { state = next break } @@ -225,7 +241,7 @@ func (e *memEfficientEngine) matchString(text string, emit func(keyword string, } for { - if nx, ok := e.trie.nodes[state].children[ch]; ok { + if nx, ok := e.trie.nodes[state].next(ch); ok { state = nx break } @@ -261,7 +277,7 @@ func (e *memEfficientEngine) matchStream(next func() (rune, bool), emit func(key } for { - if nx, ok := e.trie.nodes[state].children[ch]; ok { + if nx, ok := e.trie.nodes[state].next(ch); ok { state = nx break } @@ -301,7 +317,10 @@ func trieMaxDepth(nodes []mapNode) int { func (e *memEfficientEngine) estimateMemory() int64 { var size int64 for _, n := range e.trie.nodes { - size += int64(24 + 16 + 16) // children hdr, fail, depth + size += int64(40) // map pointer, inline edge, fail and depth + if n.children != nil { + size += 48 + } for range n.children { size += 24 } @@ -312,3 +331,41 @@ func (e *memEfficientEngine) estimateMemory() int64 { } return size } + +// A majority of large-dictionary states have one child. Keep that edge inline; +// allocate a map only when a state branches. Published nodes are immutable. +func (n *mapNode) next(ch rune) (int, bool) { + if n.children != nil { + child, ok := n.children[ch] + return child, ok + } + return n.singleChild, n.singleChild != 0 && n.singleRune == ch +} +func (n *mapNode) addChild(ch rune, child int) { + if n.children != nil { + n.children[ch] = child + return + } + if n.singleChild == 0 { + n.singleRune = ch + n.singleChild = child + return + } + n.children = map[rune]int{n.singleRune: n.singleChild, ch: child} + n.singleChild = 0 +} +func (n *mapNode) edges() iter.Seq2[rune, int] { + return func(yield func(rune, int) bool) { + if n.children != nil { + for ch, child := range n.children { + if !yield(ch, child) { + return + } + } + return + } + if n.singleChild != 0 { + yield(n.singleRune, n.singleChild) + } + } +} diff --git a/pkg/acor/rewrite.go b/pkg/acor/rewrite.go new file mode 100644 index 00000000..2b8e7bac --- /dev/null +++ b/pkg/acor/rewrite.go @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "errors" + "strings" + "unicode/utf8" +) + +const defaultRewriteBytes = 4 << 20 + +// RewriteOptions bounds an atomic leftmost-longest rewrite. Zero input, match +// and candidate limits use ScanOptions defaults; zero output limit means 4 MiB. +// Any exhausted limit returns an error without a partially rewritten result. +type RewriteOptions struct { + MaxInputBytes int + MaxMatches int + MaxCandidates int + MaxOutputBytes int + WholeWord bool + WordRune func(rune) bool +} + +// RewriteResult carries the transformed text and the original input spans that +// were changed. Match offsets always refer to the input, not the resulting text. +type RewriteResult struct { + Text string + Matches []SourceMatch +} + +// ReplaceText replaces each non-overlapping leftmost-longest match with literal +// replacement text. Replacement text is not searched recursively. +func (ac *AhoCorasick) ReplaceText(ctx context.Context, text, replacement string, opts *RewriteOptions) (*RewriteResult, error) { + return ac.rewrite(ctx, text, replacement, 0, false, opts) +} + +// MaskText replaces each matched original rune with mask, preserving rune count. +// Overlaps are resolved leftmost-longest. Invalid mask runes are rejected. +func (ac *AhoCorasick) MaskText(ctx context.Context, text string, mask rune, opts *RewriteOptions) (*RewriteResult, error) { + if !utf8.ValidRune(mask) { + return nil, errors.New("acor: invalid mask rune") + } + return ac.rewrite(ctx, text, "", mask, true, opts) +} + +// ReplaceText rewrites using one serving engine; positions refer to the input. +func (v *VersionedCollection) ReplaceText(ctx context.Context, text, replacement string, opts *RewriteOptions) (*RewriteResult, error) { + ac, err := v.search(ctx) + if err != nil { + return nil, err + } + return ac.ReplaceText(ctx, text, replacement, opts) +} + +// MaskText masks using one serving engine and one mask rune per input rune. +func (v *VersionedCollection) MaskText(ctx context.Context, text string, mask rune, opts *RewriteOptions) (*RewriteResult, error) { + ac, err := v.search(ctx) + if err != nil { + return nil, err + } + return ac.MaskText(ctx, text, mask, opts) +} +func (ac *AhoCorasick) rewrite(ctx context.Context, text, replacement string, mask rune, masking bool, opts *RewriteOptions) (*RewriteResult, error) { + var o RewriteOptions + if opts != nil { + o = *opts + } + if o.MaxOutputBytes < 0 { + return nil, errors.New("acor: negative output limit") + } + if o.MaxOutputBytes == 0 { + o.MaxOutputBytes = defaultRewriteBytes + } + scan, err := ac.Scan(ctx, text, &ScanOptions{MaxInputBytes: o.MaxInputBytes, MaxMatches: o.MaxMatches, MaxCandidates: o.MaxCandidates, + Kind: MatchKindLeftmostLongest, WholeWord: o.WholeWord, WordRune: o.WordRune}) + if err != nil { + return nil, err + } + if scan.Truncated { + return nil, ErrMatchLimit + } + return renderRewrite(ctx, text, replacement, mask, masking, o.MaxOutputBytes, scan.Matches) +} +func renderRewrite(ctx context.Context, text, replacement string, mask rune, masking bool, limit int, matches []SourceMatch) (*RewriteResult, error) { + total, cursor := 0, 0 + for _, m := range matches { + if err := ctx.Err(); err != nil { + return nil, err + } + unchanged := m.ByteStart - cursor + if unchanged > limit-total { + return nil, ErrOutputLimit + } + total += unchanged + size := len(replacement) + if masking { + width := utf8.RuneLen(mask) + count := m.End - m.Start + if count > (limit-total)/width { + return nil, ErrOutputLimit + } + size = count * width + } + if size > limit-total { + return nil, ErrOutputLimit + } + total += size + cursor = m.ByteEnd + } + if len(text)-cursor > limit-total { + return nil, ErrOutputLimit + } + total += len(text) - cursor + var out strings.Builder + out.Grow(total) + cursor = 0 + for _, m := range matches { + out.WriteString(text[cursor:m.ByteStart]) + if !masking { + out.WriteString(replacement) + } else { + for i := m.Start; i < m.End; i++ { + if i%scanContextStride == 0 { + if err := ctx.Err(); err != nil { + return nil, err + } + } + out.WriteRune(mask) + } + } + cursor = m.ByteEnd + } + out.WriteString(text[cursor:]) + if err := ctx.Err(); err != nil { + return nil, err + } + return &RewriteResult{Text: out.String(), Matches: matches}, nil +} diff --git a/pkg/acor/scan.go b/pkg/acor/scan.go new file mode 100644 index 00000000..f993f96a --- /dev/null +++ b/pkg/acor/scan.go @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "errors" + "unicode" + + matchengine "github.com/skyoo2003/acor/internal/engine" +) + +const ( + defaultScanMatches = 1000 + defaultScanInputBytes = 1 << 20 + defaultScanCandidates = 100000 + scanContextStride = 1024 + sourceHeapArity = 2 +) + +var ( + // ErrInputLimit means the input exceeds the requested byte bound. + ErrInputLimit = errors.New("acor: input byte limit exceeded") + // ErrScanWorkLimit means the raw automaton match budget was exhausted. + ErrScanWorkLimit = errors.New("acor: candidate match limit exceeded") + // ErrMatchLimit means a rewrite would exceed its match limit. No partial rewrite is returned. + ErrMatchLimit = errors.New("acor: rewrite match limit exceeded") + // ErrOutputLimit means the rewritten text would exceed its byte bound. + ErrOutputLimit = errors.New("acor: output byte limit exceeded") +) + +// SourceMatch identifies a match in the original input, even when Unicode case +// folding changes byte lengths. Start/End are rune offsets; ByteStart/ByteEnd are +// byte offsets. Both intervals are half-open. Text is the original substring; +// Keyword is the normalized dictionary entry. Text can retain the input string. +type SourceMatch struct { + Keyword string + Text string + Start int + End int + ByteStart int + ByteEnd int +} + +// ScanOptions bounds input, result storage and raw automaton output work. +// Zero limits select defaults (1 MiB input, 1,000 matches, 100,000 candidates); +// negative limits are rejected. Bounds are per call, not per worker. +type ScanOptions struct { + MaxInputBytes int + MaxMatches int + MaxCandidates int + // Kind defaults to overlapping. LeftmostLongest selects non-overlapping matches + // by earliest start, then longest length, without first collecting all matches. + Kind MatchKind + WholeWord bool + // WordRune follows MatchOptions' normalized-rune boundary semantics. + WordRune func(rune) bool +} + +// ScanResult contains at most MaxMatches matches. Truncated is true only when +// another eligible match was found. Input/work exhaustion returns an error and +// no result; callers must not mistake an incomplete scan for a clean document. +type ScanResult struct { + Matches []SourceMatch + Truncated bool +} + +// Scan searches with explicit resource bounds and original byte/rune positions. +// Existing Find/FindMatches remain unlimited and retain their existing contracts. +func (ac *AhoCorasick) Scan(ctx context.Context, text string, opts *ScanOptions) (*ScanResult, error) { + o, err := scanOptions(ctx, text, opts) + if err != nil { + return nil, err + } + eng, err := ac.ops.loadEngine(ctx) + if err != nil { + return nil, err + } + return scanSource(ctx, eng, text, ac.caseSensitive, o) +} + +// Scan searches one serving engine with bounded results and original positions. +func (v *VersionedCollection) Scan(ctx context.Context, text string, opts *ScanOptions) (*ScanResult, error) { + ac, err := v.search(ctx) + if err != nil { + return nil, err + } + return ac.Scan(ctx, text, opts) +} +func scanOptions(ctx context.Context, text string, opts *ScanOptions) (ScanOptions, error) { + if err := ctx.Err(); err != nil { + return ScanOptions{}, err + } + var o ScanOptions + if opts != nil { + o = *opts + } + if o.MaxInputBytes < 0 || o.MaxMatches < 0 || o.MaxCandidates < 0 { + return o, errors.New("acor: negative scan limit") + } + if o.Kind != MatchKindOverlapping && o.Kind != MatchKindLeftmostLongest { + return o, errors.New("acor: invalid scan match kind") + } + if o.MaxInputBytes == 0 { + o.MaxInputBytes = defaultScanInputBytes + } + if o.MaxMatches == 0 { + o.MaxMatches = defaultScanMatches + } + if o.MaxCandidates == 0 { + o.MaxCandidates = defaultScanCandidates + } + if len(text) > o.MaxInputBytes { + return o, ErrInputLimit + } + if o.WordRune == nil { + o.WordRune = isWordRune + } + return o, nil +} + +type sourceScanner struct { + ctx context.Context + text string + runes []rune + offsets []int + sensitive bool + opts ScanOptions + result ScanResult + pending sourceWindow + consumed int + candidates int + longest int + stopped bool + err error +} + +func scanSource(ctx context.Context, eng *matchengine.Engine, text string, sensitive bool, o ScanOptions) (*ScanResult, error) { + s := sourceScanner{ctx: ctx, text: text, sensitive: sensitive, opts: o, longest: eng.MaxKeywordRunes(), result: ScanResult{Matches: []SourceMatch{}}} + for offset, r := range text { + if len(s.runes)%scanContextStride == 0 { + if err := ctx.Err(); err != nil { + return nil, err + } + } + s.runes = append(s.runes, r) + s.offsets = append(s.offsets, offset) + } + s.offsets = append(s.offsets, len(text)) + eng.Stream(s.next, s.emit) + if err := ctx.Err(); err != nil { + return nil, err + } + if s.err != nil { + return nil, s.err + } + if !s.stopped { + s.flush(true) + } + if s.err != nil { + return nil, s.err + } + return &s.result, nil +} +func (s *sourceScanner) normalized(r rune) rune { + if s.sensitive { + return r + } + return unicode.ToLower(r) +} +func (s *sourceScanner) next() (rune, bool) { + if s.err = s.ctx.Err(); s.err != nil { + return 0, false + } + if s.opts.Kind == MatchKindLeftmostLongest { + s.flush(false) + } + if s.stopped || s.consumed == len(s.runes) { + return 0, false + } + r := s.normalized(s.runes[s.consumed]) + s.consumed++ + return r, true +} +func (s *sourceScanner) emit(keyword string, start, end int) bool { + if s.err = s.ctx.Err(); s.err != nil { + return false + } + s.candidates++ + if s.candidates > s.opts.MaxCandidates { + s.err = ErrScanWorkLimit + return false + } + if s.opts.WholeWord && !s.wholeWord(start, end) { + return true + } + m := SourceMatch{Keyword: keyword, Start: start, End: end, ByteStart: s.offsets[start], ByteEnd: s.offsets[end]} + m.Text = s.text[m.ByteStart:m.ByteEnd] + if s.opts.Kind == MatchKindLeftmostLongest { + s.pending.add(m) + return true + } + return s.append(m) +} +func (s *sourceScanner) wholeWord(start, end int) bool { + return (start == 0 || !s.opts.WordRune(s.normalized(s.runes[start-1]))) && + (end == len(s.runes) || !s.opts.WordRune(s.normalized(s.runes[end]))) +} +func (s *sourceScanner) append(m SourceMatch) bool { + if len(s.result.Matches) == s.opts.MaxMatches { + s.result.Truncated = true + s.stopped = true + return false + } + s.result.Matches = append(s.result.Matches, m) + return true +} +func (s *sourceScanner) flush(final bool) { + for len(s.pending.starts) > 0 && !s.stopped { + if s.err = s.ctx.Err(); s.err != nil { + s.stopped = true + return + } + start := s.pending.starts[0] + if !final && s.consumed-start < s.longest { + return + } + m := s.pending.pop() + if m.Start < s.pending.end { + continue + } + if !s.append(m) { + return + } + s.pending.end = m.End + } +} + +// sourceWindow keeps one best candidate per start position and a min-heap of +// starts. Memory is O(min(input runes, longest keyword)), not raw match count. +type sourceWindow struct { + best map[int]SourceMatch + starts []int + end int +} + +func (w *sourceWindow) add(m SourceMatch) { + if m.Start < w.end { + return + } + if w.best == nil { + w.best = make(map[int]SourceMatch) + } + if old, ok := w.best[m.Start]; ok { + if m.End > old.End { + w.best[m.Start] = m + } + return + } + w.best[m.Start] = m + w.starts = append(w.starts, m.Start) + i := len(w.starts) - 1 + for i > 0 { + parent := (i - 1) / sourceHeapArity + if w.starts[parent] <= w.starts[i] { + break + } + w.starts[parent], w.starts[i] = w.starts[i], w.starts[parent] + i = parent + } +} +func (w *sourceWindow) pop() SourceMatch { + start := w.starts[0] + m := w.best[start] + delete(w.best, start) + last := len(w.starts) - 1 + w.starts[0] = w.starts[last] + w.starts = w.starts[:last] + for i := 0; i < len(w.starts); { + child := i*sourceHeapArity + 1 + if child >= len(w.starts) { + break + } + if child+1 < len(w.starts) && w.starts[child+1] < w.starts[child] { + child++ + } + if w.starts[i] <= w.starts[child] { + break + } + w.starts[i], w.starts[child] = w.starts[child], w.starts[i] + i = child + } + return m +} diff --git a/pkg/acor/scan_test.go b/pkg/acor/scan_test.go new file mode 100644 index 00000000..1908dd23 --- /dev/null +++ b/pkg/acor/scan_test.go @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "errors" + "reflect" + "strings" + "testing" + "unicode/utf8" + + "github.com/alicebob/miniredis/v2" + + matchengine "github.com/skyoo2003/acor/internal/engine" +) + +func sourceTestAC(words []string, preset Preset, sensitive bool) *AhoCorasick { + set := make(map[string]struct{}) + for _, w := range words { + set[normalizeText(w, sensitive)] = struct{}{} + } + e := matchengine.New(enginePreset(preset)) + e.Build(set) + return &AhoCorasick{ctx: context.Background(), caseSensitive: sensitive, ops: &v3SearchOps{engine: e, sensitive: sensitive}} +} +func TestScanOriginalUnicodeSpans(t *testing.T) { + text := "A İSTANBUL 한국어 café 😀 abc" + words := []string{"istanbul", "한국", "한국어", "café", "😀", "ab", "abc", "bc"} + for _, preset := range []Preset{PresetSpeed, PresetBalanced, PresetMemoryEfficient} { + ac := sourceTestAC(words, preset, false) + for _, kind := range []MatchKind{MatchKindOverlapping, MatchKindLeftmostLongest} { + result, err := ac.Scan(context.Background(), text, &ScanOptions{Kind: kind}) + if err != nil { + t.Fatal(err) + } + expected, err := ac.FindMatches(text, &MatchOptions{Kind: kind}) + if err != nil { + t.Fatal(err) + } + projected := make([]Match, 0, len(result.Matches)) + for _, m := range result.Matches { + if m.Text != text[m.ByteStart:m.ByteEnd] || m.Text != string([]rune(text)[m.Start:m.End]) { + t.Fatal("bad original positions", m) + } + projected = append(projected, Match{Keyword: m.Keyword, Start: m.Start, End: m.End}) + } + if result.Truncated || !reflect.DeepEqual(projected, expected) { + t.Fatal(preset, kind, projected, expected) + } + } + } +} + +//nolint:gocyclo // A single fixture exercises independent bounded-scan contracts. +func TestScanLimitsAndWordBoundaries(t *testing.T) { + ac := sourceTestAC([]string{"a", "aa", "aaa", "class"}, PresetMemoryEfficient, false) + ctx := context.Background() + for _, kind := range []MatchKind{MatchKindOverlapping, MatchKindLeftmostLongest} { + r, err := ac.Scan(ctx, "aaaa aaaa", &ScanOptions{MaxMatches: 1, Kind: kind}) + if err != nil || !r.Truncated || len(r.Matches) != 1 { + t.Fatal(r, err) + } + } + r, err := ac.Scan(ctx, "a", &ScanOptions{MaxMatches: 1}) + if err != nil || r.Truncated { + t.Fatal(r, err) + } + if _, err = ac.Scan(ctx, "aaa", &ScanOptions{MaxInputBytes: 2}); !errors.Is(err, ErrInputLimit) { + t.Fatal(err) + } + if _, err = ac.Scan(ctx, "aaaaaa", &ScanOptions{MaxCandidates: 2, Kind: MatchKindLeftmostLongest}); !errors.Is(err, ErrScanWorkLimit) { + t.Fatal(err) + } + r, err = ac.Scan(ctx, "classic class!", &ScanOptions{WholeWord: true}) + if err != nil || len(r.Matches) != 1 || r.Matches[0].Text != "class" { + t.Fatal(r, err) + } + r, err = ac.Scan(ctx, "classic", &ScanOptions{WholeWord: true, WordRune: func(rune) bool { return false }}) + if err != nil || len(r.Matches) == 0 { + t.Fatal(r, err) + } + for _, opts := range []*ScanOptions{{MaxMatches: -1}, {MaxCandidates: -1}, {MaxInputBytes: -1}, {Kind: MatchKind(99)}} { + if _, err = ac.Scan(ctx, "a", opts); err == nil { + t.Fatal("invalid option accepted", opts) + } + } + canceled, cancel := context.WithCancel(ctx) + cancel() + if _, err = ac.Scan(canceled, "a", nil); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } +} + +//nolint:gocyclo // Rewrite invariants share the same multilingual source spans. +func TestRewriteAtomicAndOriginalOffsets(t *testing.T) { + ac := sourceTestAC([]string{"한국", "한국어", "istanbul", "ab", "abc", "bc"}, PresetMemoryEfficient, false) + ctx := context.Background() + text := "한국어 İSTANBUL abc!" + r, err := ac.ReplaceText(ctx, text, "[x]", nil) + if err != nil || r.Text != "[x] [x] [x]!" { + t.Fatal(r, err) + } + for _, m := range r.Matches { + if m.Text != text[m.ByteStart:m.ByteEnd] { + t.Fatal(m) + } + } + masked, err := ac.MaskText(ctx, text, '●', nil) + if err != nil || masked.Text != "●●● ●●●●●●●● ●●●!" { + t.Fatal(masked, err) + } + if utf8.RuneCountInString(masked.Text) != utf8.RuneCountInString(text) { + t.Fatal("mask changed rune count") + } + nul, err := ac.MaskText(ctx, "abc", 0, nil) + if err != nil || nul.Text != "\x00\x00\x00" { + t.Fatal(nul, err) + } + if _, err = ac.MaskText(ctx, text, -1, nil); err == nil { + t.Fatal("invalid rune accepted") + } + if result, e := ac.ReplaceText(ctx, text, "x", &RewriteOptions{MaxMatches: 1}); !errors.Is(e, ErrMatchLimit) || result != nil { + t.Fatal(result, e) + } + if result, e := ac.MaskText(ctx, text, '●', &RewriteOptions{MaxOutputBytes: 2}); !errors.Is(e, ErrOutputLimit) || result != nil { + t.Fatal(result, e) + } + if _, err = ac.ReplaceText(ctx, "abc", strings.Repeat("x", 100), &RewriteOptions{MaxOutputBytes: 10}); !errors.Is(err, ErrOutputLimit) { + t.Fatal(err) + } + if _, err = ac.ReplaceText(ctx, "clean text", "x", &RewriteOptions{MaxOutputBytes: 1}); !errors.Is(err, ErrOutputLimit) { + t.Fatal(err) + } + deleted, err := ac.ReplaceText(ctx, "abc abc", "", nil) + if err != nil || deleted.Text != " " { + t.Fatal(deleted, err) + } +} +func TestVersionedScanAndRewrite(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "source-api") + r, err := v.Replace(ctx, v.Status().ServingVersion, []string{"한국", "hello"}) + if err != nil { + t.Fatal(err) + } + waitV3(t, v, r.Version) + if found, e := v.Scan(ctx, "HELLO 한국", nil); e != nil || len(found.Matches) != 2 { + t.Fatal(found, e) + } + if rewritten, e := v.ReplaceText(ctx, "HELLO 한국", "ok", nil); e != nil || rewritten.Text != "ok ok" { + t.Fatal(rewritten, e) + } + if masked, e := v.MaskText(ctx, "HELLO 한국", '*', nil); e != nil || masked.Text != "***** **" { + t.Fatal(masked, e) + } +} +func FuzzScanLeftmostParity(f *testing.F) { + f.Add("ababa 한국어 İSTANBUL", "aba", "ba") + f.Add("aaaaaa", "a", "aaa") + f.Fuzz(func(t *testing.T, text, a, b string) { + if len(text) > 512 || len(a) > 32 || len(b) > 32 || !utf8.ValidString(a) || !utf8.ValidString(b) { + return + } + ac := sourceTestAC([]string{a, b}, PresetMemoryEfficient, false) + got, err := ac.Scan(context.Background(), text, &ScanOptions{Kind: MatchKindLeftmostLongest, MaxCandidates: 10000}) + if err != nil { + t.Fatal(err) + } + expected, err := ac.FindMatches(text, &MatchOptions{Kind: MatchKindLeftmostLongest}) + if err != nil { + t.Fatal(err) + } + projected := make([]Match, 0, len(got.Matches)) + for _, m := range got.Matches { + projected = append(projected, Match{Keyword: m.Keyword, Start: m.Start, End: m.End}) + if m.Text != text[m.ByteStart:m.ByteEnd] { + t.Fatal(m) + } + } + if !reflect.DeepEqual(projected, expected) { + t.Fatal(text, a, b, projected, expected) + } + }) +} + +func TestScanSensitiveAndMalformedUTF8(t *testing.T) { + ctx := context.Background() + ac := sourceTestAC([]string{"A", "a", "�"}, PresetMemoryEfficient, true) + text := "A a \xff" + result, err := ac.Scan(ctx, text, nil) + if err != nil || len(result.Matches) != 3 { + t.Fatal(result, err) + } + for _, m := range result.Matches { + if m.Text != text[m.ByteStart:m.ByteEnd] { + t.Fatal(m) + } + } + if result.Matches[0].Keyword != "A" || result.Matches[2].Text != "\xff" { + t.Fatal(result) + } +} diff --git a/pkg/acor/versioned.go b/pkg/acor/versioned.go new file mode 100644 index 00000000..67b08651 --- /dev/null +++ b/pkg/acor/versioned.go @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strings" + "sync" + "sync/atomic" + "time" + + redis "github.com/redis/go-redis/v9" + + matchengine "github.com/skyoo2003/acor/internal/engine" +) + +// Version is an opaque, collection-bound token. Only equality is meaningful. +type Version string + +const ( + v3BucketCount = 4096 + v3IDLength = 32 + v3PollDefault = 30 * time.Second + v3LeaseDefault = 5 * time.Minute + v3WaitPoll = 10 * time.Millisecond + v3CleanupTimeout = 5 * time.Second + v3PairLength = 2 + v3MillisPerSecond = 1000 + v3PageHint = 1024 + v3DebounceDefault = 20 * time.Millisecond +) + +var ( + // ErrVersionedClosed means the collection or snapshot was closed. + ErrVersionedClosed = errors.New("acor: versioned handle closed") + // ErrLeaseExpired means a generation can no longer be safely read. + ErrLeaseExpired = errors.New("acor: generation lease expired") + // ErrMaintenance means Prune currently excludes new readers and writers. + ErrMaintenance = errors.New("acor: collection maintenance in progress") + // ErrCasePolicy means the stored case policy differs from the requested one. + ErrCasePolicy = errors.New("acor: collection case policy mismatch") + // ErrInvalidVersion means a token is malformed or belongs to another collection. + ErrInvalidVersion = errors.New("acor: invalid collection version") + // ErrCommitUnknown means the commit response was lost; inspect OperationID using ResolveOperation. + ErrCommitUnknown = errors.New("acor: commit outcome unknown") + // ErrVersionedCorrupt means immutable stored data failed validation. + ErrVersionedCorrupt = errors.New("acor: corrupt versioned data") +) + +// VersionedOptions configures V3. Redis supplies connection fields and Name only; +// legacy schema, cache and engine options in Redis are ignored. +type VersionedOptions struct { + Redis AhoCorasickArgs + CaseSensitive bool + Preset Preset + // PollInterval defaults to 30 seconds; negative values are invalid. + PollInterval time.Duration + // RefreshDebounce coalesces bursts before a build, default 20ms. Negative values are invalid. + // The fixed window is not extended by incoming events, so continuous writes cannot starve refresh. + RefreshDebounce time.Duration + // LeaseDuration defaults to five minutes; LeaseRefresh defaults to one minute. + LeaseDuration time.Duration + LeaseRefresh time.Duration +} + +// VersionedStatus describes locally observed storage and engine state. +type VersionedStatus struct { + ActiveVersion Version + ServingVersion Version + Building bool + BuildStarted time.Time + BuildDuration time.Duration + LastError string + // DownloadedBuckets and ReusedBuckets describe the most recent successful build. + DownloadedBuckets int + ReusedBuckets int + // CompletedBuilds counts successful engines installed by this instance. + CompletedBuilds uint64 +} + +// WriteResult describes an atomic commit. OperationID remains available on an +// ambiguous commit error; never automatically reapply that operation. +type WriteResult struct { + PreviousVersion Version + Version Version + OperationID string + Added int + Removed int +} + +type v3Manifest struct { + Version Version + Sequence uint64 + Buckets [v3BucketCount]v3Bucket + Count int +} +type v3Bucket struct { + Chunks []string + Count int + Checksum string +} +type v3Engine struct { + engine *matchengine.Engine + version Version + sequence uint64 + manifest *v3Manifest + buckets *[v3BucketCount][]string +} + +// VersionedCollection owns a V3 dictionary and a background engine refresher. +// Writes commit to Redis before local search changes. Use WaitForVersion when +// read-after-write behavior is required. Close releases its Redis connection. +type VersionedCollection struct { + client redis.UniversalClient + opts VersionedOptions + prefix string + id string + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + closed atomic.Bool + current atomic.Pointer[v3Engine] + refreshMu sync.Mutex + mu sync.Mutex + status VersionedStatus + leases map[*v3Lease]struct{} + wake chan struct{} +} + +func v3ID() string { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + panic(err) + } + return hex.EncodeToString(b[:]) +} +func v3Hash(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) } +func (v *VersionedCollection) key(s string) string { return v.prefix + s } +func (v *VersionedCollection) valid(t Version) bool { + s := string(t) + return strings.HasPrefix(s, v.id+".") && len(s) == len(v.id)+1+v3IDLength +} +func (v *VersionedCollection) check(ctx context.Context) error { + if v.closed.Load() { + return ErrVersionedClosed + } + return ctx.Err() +} + +// OpenVersioned creates or opens a V3 collection and builds its first engine. +// ctx bounds initialization, not the returned collection's lifetime. +func OpenVersioned(ctx context.Context, opts *VersionedOptions) (*VersionedCollection, error) { + o, err := versionedOptions(opts) + if err != nil { + return nil, err + } + // A digest hash tag prevents braces in user names from splitting Cluster slots. + client, err := newRedisClient(&o.Redis) + if err != nil { + return nil, err + } + bg, cancel := context.WithCancel(context.Background()) + v := &VersionedCollection{ + client: client, opts: o, prefix: "{acor-v3-" + v3Hash([]byte(o.Redis.Name)) + "}:", + ctx: bg, cancel: cancel, leases: make(map[*v3Lease]struct{}), wake: make(chan struct{}, 1), + } + if err = v.initialize(ctx); err != nil { + cancel() + _ = client.Close() + return nil, err + } + v.wg.Add(1) + go v.renewLoop() + if err = v.refresh(ctx); err != nil { + _ = v.Close() + return nil, err + } + v.wg.Add(1) + go v.refreshLoop() + return v, nil +} + +func (v *VersionedCollection) initialize(ctx context.Context) error { + id := v3ID() + token := Version(id + "." + v3ID()) + m := v3Manifest{Version: token, Sequence: 1} + data, _ := json.Marshal(m) + const script = ` + if redis.call('EXISTS',KEYS[1])==0 then + redis.call('HSET',KEYS[1],'id',ARGV[1],'case',ARGV[2]) + redis.call('SET',KEYS[2],ARGV[3]) + redis.call('SET',KEYS[3],ARGV[4]) + redis.call('SET',KEYS[5],'1') + local t=redis.call('TIME'); redis.call('ZADD',KEYS[4],t[1],ARGV[3]) + end + return redis.call('HMGET',KEYS[1],'id','case')` + keys := []string{v.key("meta"), v.key("active"), v.key("gen:" + string(token)), v.key("generations"), v.key("committed:" + string(token))} + r, err := v.client.Eval(ctx, script, keys, id, fmt.Sprint(v.opts.CaseSensitive), string(token), data).Slice() + if err != nil { + return err + } + if len(r) != v3PairLength || r[0] == nil { + return ErrVersionedCorrupt + } + v.id = fmt.Sprint(r[0]) + if fmt.Sprint(r[1]) != fmt.Sprint(v.opts.CaseSensitive) { + return ErrCasePolicy + } + return nil +} + +// Status returns local observations without Redis I/O. +func (v *VersionedCollection) Status() VersionedStatus { + v.mu.Lock() + defer v.mu.Unlock() + return v.status +} +func (v *VersionedCollection) signal() { + select { + case v.wake <- struct{}{}: + default: + } +} + +func (v *VersionedCollection) refresh(ctx context.Context) error { + v.refreshMu.Lock() + defer v.refreshMu.Unlock() + if err := v.check(ctx); err != nil { + return err + } + active, err := v.client.Get(ctx, v.key("active")).Result() + if err != nil { + return err + } + v.mu.Lock() + v.status.ActiveVersion = Version(active) + v.mu.Unlock() + if e := v.current.Load(); e != nil && e.version == Version(active) { + return nil + } + s, err := v.Snapshot(ctx) + if err != nil { + return err + } + defer func() { _ = s.Close(ctx) }() + v.mu.Lock() + v.status.ActiveVersion = s.Version() + v.mu.Unlock() + if e := v.current.Load(); e != nil && e.version == s.Version() { + return nil + } + start := time.Now() + v.mu.Lock() + v.status.Building = true + v.status.BuildStarted = start + v.mu.Unlock() + defer func() { + v.mu.Lock() + v.status.Building = false + v.status.BuildDuration = time.Since(start) + v.mu.Unlock() + }() + buckets, downloaded, reused, err := v.engineBuckets(ctx, s) + if err != nil { + return err + } + e := matchengine.New(enginePreset(v.opts.Preset)) + if buildErr := e.BuildSequenceContext(ctx, bucketSequence(buckets), s.Count()); buildErr != nil { + return buildErr + } + if err := s.lease.check(ctx); err != nil { + return err + } + if err := v.check(ctx); err != nil { + return err + } + v.installEngine(s, e, buckets, downloaded, reused) + return nil +} +func (v *VersionedCollection) installEngine(s *Snapshot, e *matchengine.Engine, buckets *[v3BucketCount][]string, downloaded, reused int) { + v.current.Store(&v3Engine{engine: e, version: s.Version(), sequence: s.manifest.Sequence, manifest: s.manifest, buckets: buckets}) + v.mu.Lock() + v.status.ServingVersion = s.Version() + v.status.LastError = "" + v.status.DownloadedBuckets = downloaded + v.status.ReusedBuckets = reused + v.status.CompletedBuilds++ + v.mu.Unlock() +} +func (v *VersionedCollection) refreshLoop() { + defer v.wg.Done() + sub := v.client.Subscribe(v.ctx, v.key("events")) + defer func() { _ = sub.Close() }() + ch := sub.Channel() + ticker := time.NewTicker(v.opts.PollInterval) + defer ticker.Stop() + for { + select { + case <-v.ctx.Done(): + return + case <-ticker.C: + case <-v.wake: + case _, ok := <-ch: + if !ok { + ch = nil + } + } + timer := time.NewTimer(v.opts.RefreshDebounce) + select { + case <-v.ctx.Done(): + timer.Stop() + return + case <-timer.C: + } + // Drain only events already buffered. A fixed bound prevents a busy publisher + // from turning this into an unbounded trailing-edge debounce. + pending := len(ch) + for range pending { + <-ch + } + select { + case <-v.wake: + default: + } + if err := v.refresh(v.ctx); err != nil && v.ctx.Err() == nil { + v.mu.Lock() + v.status.LastError = err.Error() + v.mu.Unlock() + } + } +} + +// WaitForVersion waits for an engine at the given committed version or a later +// commit. Committed-version receipts survive generation pruning. +func (v *VersionedCollection) WaitForVersion(ctx context.Context, version Version) error { + if !v.valid(version) { + return ErrInvalidVersion + } + sequence, err := v.client.Get(ctx, v.key("committed:"+string(version))).Uint64() + if err != nil { + return err + } + v.signal() + t := time.NewTicker(v3WaitPoll) + defer t.Stop() + for { + if err := v.check(ctx); err != nil { + return err + } + if e := v.current.Load(); e != nil && e.sequence >= sequence { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-v.ctx.Done(): + return ErrVersionedClosed + case <-t.C: + } + } +} + +// Close stops background work and invalidates all owned snapshots. +func (v *VersionedCollection) Close() error { + if v.closed.Swap(true) { + return nil + } + v.cancel() + v.wg.Wait() + v.current.Store(nil) + v.mu.Lock() + leases := make([]*v3Lease, 0, len(v.leases)) + for l := range v.leases { + leases = append(leases, l) + } + v.mu.Unlock() + ctx, cancel := context.WithTimeout(context.Background(), v3CleanupTimeout) + defer cancel() + for _, l := range leases { + _ = l.close(ctx) + } + return v.client.Close() +} + +func (v *VersionedCollection) manifest(ctx context.Context, t Version) (*v3Manifest, error) { + if !v.valid(t) { + return nil, ErrInvalidVersion + } + b, err := v.client.Get(ctx, v.key("gen:"+string(t))).Bytes() + if err != nil { + return nil, err + } + var m v3Manifest + if json.Unmarshal(b, &m) != nil || m.Version != t || m.Sequence == 0 { + return nil, ErrVersionedCorrupt + } + count := 0 + for _, bucket := range &m.Buckets { + if bucket.Count < 0 || bucket.Count > m.Count-count { + return nil, ErrVersionedCorrupt + } + count += bucket.Count + } + if count != m.Count { + return nil, ErrVersionedCorrupt + } + return &m, nil +} + +func versionedOptions(opts *VersionedOptions) (VersionedOptions, error) { + if opts == nil { + return VersionedOptions{}, errors.New("acor: VersionedOptions required") + } + o := *opts + if strings.TrimSpace(o.Redis.Name) == "" { + return VersionedOptions{}, errors.New("acor: collection name required") + } + if o.PollInterval == 0 { + o.PollInterval = v3PollDefault + } + if o.LeaseDuration == 0 { + o.LeaseDuration = v3LeaseDefault + } + if o.LeaseRefresh == 0 { + o.LeaseRefresh = time.Minute + } + if o.RefreshDebounce == 0 { + o.RefreshDebounce = v3DebounceDefault + } + if o.RefreshDebounce < 0 { + return VersionedOptions{}, errors.New("acor: invalid refresh debounce") + } + if o.PollInterval < 0 || o.LeaseRefresh <= 0 || o.LeaseDuration <= o.LeaseRefresh { + return VersionedOptions{}, errors.New("acor: invalid refresh intervals") + } + if o.Preset == PresetNone { + o.Preset = PresetMemoryEfficient + } + if o.Preset < PresetSpeed || o.Preset > PresetMemoryEfficient { + return VersionedOptions{}, errors.New("acor: invalid preset") + } + return o, nil +} diff --git a/pkg/acor/versioned_failure_test.go b/pkg/acor/versioned_failure_test.go new file mode 100644 index 00000000..07115205 --- /dev/null +++ b/pkg/acor/versioned_failure_test.go @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "encoding/json" + "errors" + "net" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + redis "github.com/redis/go-redis/v9" +) + +type v3FaultHook struct { + dropCommit atomic.Bool + failChunks atomic.Bool + suppressPublish bool + chunksWritten atomic.Int64 +} + +func (h *v3FaultHook) DialHook(next redis.DialHook) redis.DialHook { + return func(ctx context.Context, network, addr string) (net.Conn, error) { return next(ctx, network, addr) } +} +func (h *v3FaultHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { + return next +} +func (h *v3FaultHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { + return func(ctx context.Context, cmd redis.Cmder) error { + args := cmd.Args() + if cmd.Name() == "get" && strings.Contains(args[1].(string), ":chunk:") && h.failChunks.Load() { + return errors.New("injected chunk read failure") + } + commit := cmd.Name() == "eval" && args[1] == v3CommitScript + if cmd.Name() == "eval" && args[1] == v3StageScript && strings.Contains(args[5].(string), ":chunk:") { + h.chunksWritten.Add(1) + } + if commit && h.suppressPublish { + args[1] = strings.ReplaceAll(v3CommitScript, "redis.call('PUBLISH',KEYS[6],ARGV[3])", "-- deliberately dropped publication") + } + err := next(ctx, cmd) + if commit && err == nil && h.dropCommit.Swap(false) { + return errors.New("injected lost commit response") + } + return err + } +} +func TestVersionedLostCommitReceiptAndReuse(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "receipt") + hook := &v3FaultHook{} + v.client.AddHook(hook) + hook.dropCommit.Store(true) + r, err := v.Add(ctx, v.Status().ServingVersion, "first") + if !errors.Is(err, ErrCommitUnknown) || r == nil { + t.Fatal(r, err) + } + receipt, err := v.ResolveOperation(ctx, r.OperationID) + if err != nil || receipt.Version != r.Version { + t.Fatal(receipt, err) + } + waitV3(t, v, receipt.Version) + next, err := v.Add(ctx, receipt.Version, "second") + if err != nil { + t.Fatal(err) + } + waitV3(t, v, next.Version) + // A delayed transport retry must return the old receipt, not reapply it. + recovered, err := v.commit(ctx, &v3Lease{member: "expired"}, receipt, 2) + if err != nil || recovered.Version != receipt.Version { + t.Fatal(recovered, err) + } + if active := v.client.Get(ctx, v.key("active")).Val(); active != string(next.Version) { + t.Fatal(active) + } + before := hook.chunksWritten.Load() + if _, err = v.Replace(ctx, next.Version, []string{"first", "second"}); err != nil { + t.Fatal(err) + } + if hook.chunksWritten.Load() != before { + t.Fatal("identical dictionary rewrote chunks") + } + before = hook.chunksWritten.Load() + if _, err = v.Add(ctx, next.Version, "third"); err != nil { + t.Fatal(err) + } + if hook.chunksWritten.Load() != before+1 { + t.Fatal("single addition rewrote unaffected buckets") + } +} +func TestVersionedPollingAndBuildFailure(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + writer := openV3Test(t, server, "poll") + reader := openV3Test(t, server, "poll") + writer.client.AddHook(&v3FaultHook{suppressPublish: true}) + readHook := &v3FaultHook{} + reader.client.AddHook(readHook) + initial := reader.Status().ServingVersion + readHook.failChunks.Store(true) + r, err := writer.Add(ctx, initial, "hello") + if err != nil { + t.Fatal(err) + } + deadline := time.Now().Add(5 * time.Second) + for reader.Status().LastError == "" && time.Now().Before(deadline) { + time.Sleep(time.Millisecond) + } + status := reader.Status() + if status.LastError == "" || status.ServingVersion != initial { + t.Fatal(status) + } + if found, e := reader.Find(ctx, "hello"); e != nil || len(found) != 0 { + t.Fatal(found, e) + } + readHook.failChunks.Store(false) + waitV3(t, reader, r.Version) + if found, e := reader.Contains(ctx, "hello"); e != nil || !found { + t.Fatal(found, e) + } + if _, e := reader.FindSet(ctx, "hello hello"); e != nil { + t.Fatal(e) + } + if _, e := reader.FindBatch(ctx, []string{"hello", "missing"}); e != nil { + t.Fatal(e) + } + if _, e := reader.FindParallel(ctx, "hello hello", &ParallelOptions{ChunkSize: 3, AutoOverlap: true}); e != nil { + t.Fatal(e) + } + if _, e := reader.Remove(ctx, r.Version, "hello"); e != nil { + t.Fatal(e) + } +} + +//nolint:gocyclo // A copied generation supplies the context and cursor rejection cases. +func TestVersionedCopyV2AndCancellation(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "target") + old, err := Create(&AhoCorasickArgs{Addr: server.Addr(), Name: "source"}) + if err != nil { + t.Fatal(err) + } + defer old.Close() + if _, err = old.AddMany([]string{"한국", "HELLO"}, nil); err != nil { + t.Fatal(err) + } + copied, err := v.CopyV2(ctx, "source", v.Status().ServingVersion, nil) + if err != nil || copied.Count != 2 || copied.Checksum == "" || copied.SourceVersion == "" { + t.Fatal(copied, err) + } + waitV3(t, v, copied.Write.Version) + canceled, cancel := context.WithCancel(ctx) + cancel() + if _, err = v.Snapshot(canceled); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + if _, err = OpenVersioned(canceled, &VersionedOptions{Redis: AhoCorasickArgs{Addr: server.Addr(), Name: "cancel"}}); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + s, err := v.Snapshot(ctx) + if err != nil { + t.Fatal(err) + } + defer s.Close(ctx) + if _, err = s.List(ctx, "invalid", 1); !errors.Is(err, ErrInvalidVersion) { + t.Fatal(err) + } + if _, err = s.List(ctx, "", 0); err == nil { + t.Fatal("invalid limit accepted") + } + p, err := s.List(ctx, "", 1) + if err != nil { + t.Fatal(err) + } + r, err := v.Add(ctx, s.Version(), "another") + if err != nil { + t.Fatal(err) + } + waitV3(t, v, r.Version) + newer, err := v.Snapshot(ctx) + if err != nil { + t.Fatal(err) + } + defer newer.Close(ctx) + if _, err = newer.List(ctx, p.NextCursor, 1); !errors.Is(err, ErrInvalidVersion) { + t.Fatal(err) + } +} +func TestVersionedCorruptionAndOrphanRecovery(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "orphan") + initial := v.Status().ServingVersion + l, _, err := v.acquire(ctx, initial, true) + if err != nil { + t.Fatal(err) + } + b, err := v.stageBucket(ctx, l, []string{"orphan"}) + if err != nil { + t.Fatal(err) + } + // Simulate process death after all immutable data was prepared, before commit. + orphan := v3Manifest{Version: Version(v.id + "." + v3ID()), Sequence: 2, Count: 1} + orphan.Buckets[v3BucketNumber("orphan")] = b + data, _ := json.Marshal(orphan) + if err = v.stage(ctx, l, "gen:"+string(orphan.Version), "generations", string(orphan.Version), data); err != nil { + t.Fatal(err) + } + _ = l.close(ctx) + v.client.ZAdd(ctx, v.key("generations"), redis.Z{Member: string(orphan.Version), Score: 1}) + if _, err = v.Prune(ctx); err != nil { + t.Fatal(err) + } + if v.client.Exists(ctx, v.key("chunk:"+b.Chunks[0])).Val() != 0 { + t.Fatal("orphan chunk survived") + } + if v.client.Get(ctx, v.key("active")).Val() != string(initial) { + t.Fatal("orphan became active") + } + r, err := v.Add(ctx, initial, "good") + if err != nil { + t.Fatal(err) + } + waitV3(t, v, r.Version) + s, err := v.Snapshot(ctx) + if err != nil { + t.Fatal(err) + } + defer s.Close(ctx) + h := s.manifest.Buckets[v3BucketNumber("good")].Chunks[0] + v.client.Set(ctx, v.key("chunk:"+h), `["bad"]`, 0) + if _, err = s.List(ctx, "", 10); !errors.Is(err, ErrVersionedCorrupt) { + t.Fatal(err) + } +} + +func FuzzVersionedNormalization(f *testing.F) { + f.Add(" 한글 ", "ABC") + f.Add("", "x") + f.Fuzz(func(t *testing.T, a, b string) { + normalized, err := v3Normalize([]string{a, b, a}, false) + if err != nil { + return + } + again, e := v3Normalize(normalized, false) + if e != nil || strings.Join(normalized, "|") != strings.Join(again, "|") { + t.Fatal("normalization is not idempotent") + } + for _, w := range normalized { + if bucket := v3BucketNumber(w); bucket < 0 || bucket >= v3BucketCount { + t.Fatal(bucket) + } + } + }) +} + +func TestVersionedReconnect(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "reconnect") + r, err := v.Add(ctx, v.Status().ServingVersion, "before") + if err != nil { + t.Fatal(err) + } + waitV3(t, v, r.Version) + server.Close() + if found, e := v.Contains(ctx, "before"); e != nil || !found { + t.Fatal(found, e) + } + if err = server.Restart(); err != nil { + t.Fatal(err) + } + next, err := v.Add(ctx, r.Version, "after") + if err != nil { + t.Fatal(err) + } + waitV3(t, v, next.Version) + if found, e := v.Contains(ctx, "after"); e != nil || !found { + t.Fatal(found, e) + } +} + +func TestVersionedSearchGenerationConsistency(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "consistent") + old := []string{"old-a", "old-b"} + newer := []string{"new-a", "new-b"} + r, err := v.Replace(ctx, v.Status().ServingVersion, old) + if err != nil { + t.Fatal(err) + } + waitV3(t, v, r.Version) + done := make(chan struct{}) + outcome := make(chan error, 1) + go func() { + defer close(outcome) + for { + select { + case <-done: + return + default: + } + batch, e := v.FindBatch(ctx, []string{"old-a old-b new-a new-b", "old-a old-b new-a new-b"}) + if e != nil { + outcome <- e + return + } + if len(batch) != 2 || len(batch[0]) != 2 || strings.Join(batch[0], "|") != strings.Join(batch[1], "|") { + outcome <- errors.New("search batch mixed generations") + return + } + } + }() + for i := range 20 { + target := old + if i%2 == 0 { + target = newer + } + r, err = v.Replace(ctx, r.Version, target) + if err != nil { + close(done) + <-outcome + t.Fatal(err) + } + } + close(done) + if e := <-outcome; e != nil { + t.Fatal(e) + } + waitV3(t, v, r.Version) +} +func TestVersionedSensitiveAndEmptyCopy(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v, err := OpenVersioned(ctx, &VersionedOptions{Redis: AhoCorasickArgs{Addr: server.Addr(), Name: "sensitive"}, CaseSensitive: true}) + if err != nil { + t.Fatal(err) + } + defer v.Close() + r, err := v.Replace(ctx, v.Status().ServingVersion, []string{"Hello", "hello"}) + if err != nil || r.Added != 2 { + t.Fatal(r, err) + } + waitV3(t, v, r.Version) + found, err := v.Find(ctx, "HELLO Hello") + if err != nil || len(found) != 1 || found[0] != "Hello" { + t.Fatal(found, err) + } + old, err := Create(&AhoCorasickArgs{Addr: server.Addr(), Name: "empty-v2"}) + if err != nil { + t.Fatal(err) + } + defer old.Close() + if _, err = v.CopyV2(ctx, "empty-v2", r.Version, &V2CopyOptions{RejectEmpty: true}); err == nil { + t.Fatal("empty copy accepted") + } + copied, err := v.CopyV2(ctx, "empty-v2", r.Version, nil) + if err != nil || copied.Count != 0 { + t.Fatal(copied, err) + } +} diff --git a/pkg/acor/versioned_million_test.go b/pkg/acor/versioned_million_test.go new file mode 100644 index 00000000..01beec37 --- /dev/null +++ b/pkg/acor/versioned_million_test.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "errors" + "os" + "strings" + "sync" + "testing" + + redis "github.com/redis/go-redis/v9" +) + +// TestVersionedMillionSafety verifies pinned paging, conflicting commits and +// lease-safe pruning at one million entries on the supplied real endpoint. +// +//nolint:gocyclo,funlen // The acceptance scenario retains one million-entry snapshot throughout. +func TestVersionedMillionSafety(t *testing.T) { + addr := os.Getenv("ACOR_V3_SCALE_ADDR") + if addr == "" { + t.Skip("requires real disposable server") + } + ctx := context.Background() + v, err := OpenVersioned(ctx, &VersionedOptions{Redis: AhoCorasickArgs{Addr: addr, Name: "million-safety-" + v3ID()}}) + if err != nil { + t.Fatal(err) + } + defer v.Close() + defer func() { + var cursor uint64 + for { + keys, next, e := v.client.Scan(ctx, cursor, v.prefix+"*", 256).Result() + if e != nil { + return + } + if len(keys) > 0 { + v.client.Del(ctx, keys...) + } + cursor = next + if cursor == 0 { + break + } + } + }() + words := v3ScaleWords(1000000, "shared") + r, err := v.Replace(ctx, v.Status().ServingVersion, words) + if err != nil { + t.Fatal(err) + } + waitV3(t, v, r.Version) + s, err := v.Snapshot(ctx) + if err != nil { + t.Fatal(err) + } + defer s.Close(ctx) + var wg sync.WaitGroup + outcomes := make(chan error, 2) + for _, w := range []string{"competitor-a", "competitor-b"} { + wg.Go(func() { _, e := v.Add(ctx, r.Version, w); outcomes <- e }) + } + wg.Wait() + close(outcomes) + success, conflict := 0, 0 + for e := range outcomes { + if e == nil { + success++ + } else if errors.Is(e, ErrConcurrencyConflict) { + conflict++ + } else { + t.Fatal(e) + } + } + if success != 1 || conflict != 1 { + t.Fatal(success, conflict) + } + active, err := v.Snapshot(ctx) + if err != nil { + t.Fatal(err) + } + waitV3(t, v, active.Version()) + _ = active.Close(ctx) + gens, _ := v.client.ZRange(ctx, v.key("generations"), 0, -1).Result() + for _, g := range gens { + v.client.ZAdd(ctx, v.key("generations"), redis.Z{Score: 1, Member: g}) + } + if _, err = v.Prune(ctx); err != nil { + t.Fatal(err) + } + count := 0 + cursor := "" + for { + p, e := s.List(ctx, cursor, 4096) + if e != nil { + t.Fatal(e) + } + for _, word := range p.Keywords { + if !strings.HasPrefix(word, "shared-prefix-") { + t.Fatal("mixed snapshot", word) + } + } + count += len(p.Keywords) + cursor = p.NextCursor + if cursor == "" { + break + } + } + if count != len(words) { + t.Fatal(count) + } + _ = s.Close(ctx) + if _, err = v.Prune(ctx); err != nil { + t.Fatal(err) + } + if _, err = v.manifest(ctx, r.Version); !errors.Is(err, redis.Nil) { + t.Fatal("unleased old generation retained", err) + } + if found, e := v.Contains(ctx, words[0]); e != nil || !found { + t.Fatal(found, e) + } +} diff --git a/pkg/acor/versioned_prune.go b/pkg/acor/versioned_prune.go new file mode 100644 index 00000000..458890b4 --- /dev/null +++ b/pkg/acor/versioned_prune.go @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "fmt" + "strings" + + redis "github.com/redis/go-redis/v9" +) + +// PruneResult counts deleted immutable records. Operation receipts are retained +// indefinitely so delayed commit reconciliation remains possible. +type PruneResult struct { + Generations int + Chunks int +} + +const v3MaintenanceTTL = 30000 +const v3PruneBatch = 64 +const v3RetentionSeconds = 24 * 60 * 60 + +const v3FenceScript = ` + if redis.call('GET',KEYS[1])~=ARGV[1] then return 0 end + redis.call('PEXPIRE',KEYS[1],ARGV[2]); return 1` + +// Prune explicitly collects unreachable data, retaining the active generation, +// generations prepared in the last 24 hours, and all valid reader leases. +// It excludes writers and new snapshots while allowing local searches and lease +// renewals. Each bounded deletion checks a monotonic fencing token atomically. +func (v *VersionedCollection) Prune(ctx context.Context) (*PruneResult, error) { + if err := v.check(ctx); err != nil { + return nil, err + } + const acquire = v3Now + ` + redis.call('ZREMRANGEBYSCORE',KEYS[2],'-inf',now) + redis.call('ZREMRANGEBYSCORE',KEYS[4],'-inf',now) + if redis.call('EXISTS',KEYS[1])==1 or redis.call('ZCARD',KEYS[2])>0 then return {} end + redis.call('INCR',KEYS[3]); local token=redis.call('GET',KEYS[3]) + redis.call('SET',KEYS[1],token,'PX',ARGV[1]) + return {token,tostring(math.floor(now/1000))}` + r, err := v.client.Eval(ctx, acquire, []string{v.key("maintenance"), v.key("writers"), v.key("fence"), v.key("leases")}, v3MaintenanceTTL).StringSlice() + if err != nil { + return nil, err + } + if len(r) != v3PairLength { + return nil, ErrMaintenance + } + token := r[0] + defer func() { + cleanup, cancel := context.WithTimeout(context.Background(), v3CleanupTimeout) + defer cancel() + const release = `if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) end return 0` + _ = v.client.Eval(cleanup, release, []string{v.key("maintenance")}, token).Err() + }() + var now int64 + if _, err = fmt.Sscan(r[1], &now); err != nil { + return nil, err + } + return v.pruneLocked(ctx, token, now) +} +func (v *VersionedCollection) fence(ctx context.Context, token string) error { + n, err := v.client.Eval(ctx, v3FenceScript, []string{v.key("maintenance")}, token, v3MaintenanceTTL).Int() + if err != nil { + return err + } + if n != 1 { + return ErrMaintenance + } + return nil +} +func (v *VersionedCollection) pruneLocked(ctx context.Context, token string, now int64) (*PruneResult, error) { + keep, gens, err := v.pruneKeep(ctx, now) + if err != nil { + return nil, err + } + refs := make(map[string]bool) + for generation := range keep { + if fenceErr := v.fence(ctx, token); fenceErr != nil { + return nil, fenceErr + } + m, readErr := v.manifest(ctx, Version(generation)) + if readErr != nil { + return nil, readErr + } + for _, b := range &m.Buckets { + for _, h := range b.Chunks { + refs[h] = true + } + } + } + chunks, err := v.client.ZRange(ctx, v.key("chunks"), 0, -1).Result() + if err != nil { + return nil, err + } + deadChunks := make([]string, 0) + for _, h := range chunks { + if !refs[h] { + deadChunks = append(deadChunks, h) + } + } + deadGens := make([]string, 0) + for _, g := range gens { + name := fmt.Sprint(g.Member) + if !keep[name] { + deadGens = append(deadGens, name) + } + } + result := &PruneResult{} + result.Chunks, err = v.pruneDelete(ctx, token, "chunks", "chunk:", deadChunks) + if err != nil { + return result, err + } + result.Generations, err = v.pruneDelete(ctx, token, "generations", "gen:", deadGens) + return result, err +} +func (v *VersionedCollection) pruneDelete(ctx context.Context, token, registry, prefix string, ids []string) (int, error) { + const script = ` + if redis.call('GET',KEYS[1])~=ARGV[1] then return -1 end + redis.call('PEXPIRE',KEYS[1],ARGV[2]) + for i=3,#KEYS do redis.call('DEL',KEYS[i]); redis.call('ZREM',KEYS[2],ARGV[i]) end + return #KEYS-2` + count := 0 + for len(ids) > 0 { + n := min(v3PruneBatch, len(ids)) + keys := []string{v.key("maintenance"), v.key(registry)} + args := []interface{}{token, v3MaintenanceTTL} + for _, id := range ids[:n] { + keys = append(keys, v.key(prefix+id)) + args = append(args, id) + } + deleted, err := v.client.Eval(ctx, script, keys, args...).Int() + if err != nil { + return count, err + } + if deleted < 0 { + return count, ErrMaintenance + } + count += deleted + ids = ids[n:] + } + return count, nil +} + +func (v *VersionedCollection) pruneKeep(ctx context.Context, now int64) (map[string]bool, []redis.Z, error) { + active, err := v.client.Get(ctx, v.key("active")).Result() + if err != nil { + return nil, nil, err + } + keep := map[string]bool{active: true} + leases, err := v.client.ZRangeWithScores(ctx, v.key("leases"), 0, -1).Result() + if err != nil { + return nil, nil, err + } + for _, l := range leases { + if l.Score > float64(now*v3MillisPerSecond) { + generation, _, _ := strings.Cut(fmt.Sprint(l.Member), "/") + keep[generation] = true + } + } + gens, err := v.client.ZRangeWithScores(ctx, v.key("generations"), 0, -1).Result() + if err != nil { + return nil, nil, err + } + for _, g := range gens { + if g.Score >= float64(now-v3RetentionSeconds) { + keep[fmt.Sprint(g.Member)] = true + } + } + return keep, gens, nil +} diff --git a/pkg/acor/versioned_reuse.go b/pkg/acor/versioned_reuse.go new file mode 100644 index 00000000..7185bbe2 --- /dev/null +++ b/pkg/acor/versioned_reuse.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "iter" + "slices" +) + +// Cached words have already passed chunk and bucket checksums. They are ordinary +// local immutable strings, so reusing them never reads a pruned Redis generation. +// Only the newly installed generation is kept; failed candidates are discarded. +func (v *VersionedCollection) engineBuckets(ctx context.Context, s *Snapshot) (next *[v3BucketCount][]string, downloaded, reused int, err error) { + next = new([v3BucketCount][]string) + previous := v.current.Load() + + for i, b := range &s.manifest.Buckets { + if err := ctx.Err(); err != nil { + return nil, 0, 0, err + } + if previous != nil && previous.buckets != nil && sameBucket(previous.manifest.Buckets[i], b) { + next[i] = previous.buckets[i] + reused++ + continue + } + words, err := v.bucket(ctx, b) + if err != nil { + return nil, 0, 0, err + } + next[i] = words + if b.Count > 0 { + downloaded++ + } + } + return next, downloaded, reused, nil +} +func sameBucket(a, b v3Bucket) bool { + return a.Count == b.Count && a.Checksum == b.Checksum && slices.Equal(a.Chunks, b.Chunks) +} +func bucketSequence(buckets *[v3BucketCount][]string) iter.Seq[string] { + return func(yield func(string) bool) { + for _, words := range buckets { + for _, word := range words { + if !yield(word) { + return + } + } + } + } +} diff --git a/pkg/acor/versioned_reuse_test.go b/pkg/acor/versioned_reuse_test.go new file mode 100644 index 00000000..364dcce6 --- /dev/null +++ b/pkg/acor/versioned_reuse_test.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" +) + +func TestVersionedReusesVerifiedBuckets(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "reuse-r2") + words := v3ScaleWords(10000, "shared") + r, err := v.Replace(ctx, v.Status().ServingVersion, words) + if err != nil { + t.Fatal(err) + } + waitV3(t, v, r.Version) + old := v.current.Load() + next, err := v.Add(ctx, r.Version, "one-new-keyword") + if err != nil { + t.Fatal(err) + } + waitV3(t, v, next.Version) + status := v.Status() + if status.DownloadedBuckets != 1 || status.ReusedBuckets != v3BucketCount-1 { + t.Fatal(status) + } + current := v.current.Load() + for i, words := range old.buckets { + if i == v3BucketNumber("one-new-keyword") || len(words) == 0 { + continue + } + if &words[0] != ¤t.buckets[i][0] { + t.Fatal("unchanged bucket copied", i) + } + } + // A failed download must not replace the last successful cache or engine. + writer := openV3Test(t, server, "reuse-r2") + fault := &v3FaultHook{} + v.client.AddHook(fault) + fault.failChunks.Store(true) + changed, err := writer.Add(ctx, next.Version, "next-new-keyword") + if err != nil { + t.Fatal(err) + } + deadline := time.Now().Add(time.Second) + for v.Status().LastError == "" && time.Now().Before(deadline) { + time.Sleep(time.Millisecond) + } + if v.current.Load() != current { + t.Fatal("failed refresh discarded serving cache") + } + fault.failChunks.Store(false) + waitV3(t, v, changed.Version) +} +func TestVersionedDebounceCoalescesWithoutStarvation(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + opts := &VersionedOptions{Redis: AhoCorasickArgs{Addr: server.Addr(), Name: "coalesce-r2"}, RefreshDebounce: 200 * time.Millisecond, PollInterval: time.Hour} + reader, err := OpenVersioned(ctx, opts) + if err != nil { + t.Fatal(err) + } + defer reader.Close() + writer := openV3Test(t, server, "coalesce-r2") + expected := reader.Status().ServingVersion + for i := range 10 { + r, e := writer.Add(ctx, expected, fmt.Sprint(i)) + if e != nil { + t.Fatal(e) + } + expected = r.Version + } + waitV3(t, reader, expected) + if builds := reader.Status().CompletedBuilds; builds >= 11 { + t.Fatal("every intermediate generation was built", builds) + } + // Waiting on a skipped version is valid once a later generation is serving. + if err = reader.WaitForVersion(ctx, writer.Status().ServingVersion); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/acor/versioned_scale_test.go b/pkg/acor/versioned_scale_test.go new file mode 100644 index 00000000..c349bcff --- /dev/null +++ b/pkg/acor/versioned_scale_test.go @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "encoding/json" + "fmt" + "math/rand" + "os" + "runtime" + "slices" + "strconv" + "strings" + "sync" + "syscall" + "testing" + "time" + + redis "github.com/redis/go-redis/v9" +) + +// TestVersionedScale is opt-in and must use a disposable real Redis/Valkey +// server. The deterministic seed and workload are recorded with each result. +// Run each size/workload in a separate process for independent maximum RSS. +// +//nolint:gocyclo,funlen // Opt-in end-to-end measurement keeps operation ordering explicit. +func TestVersionedScale(t *testing.T) { + addr := os.Getenv("ACOR_V3_SCALE_ADDR") + if addr == "" { + t.Skip("set ACOR_V3_SCALE_ADDR to a disposable real server") + } + n, err := strconv.Atoi(os.Getenv("ACOR_V3_SCALE_N")) + if err != nil || n < 1 { + t.Fatal("ACOR_V3_SCALE_N must be positive") + } + kind := os.Getenv("ACOR_V3_SCALE_KIND") + if kind == "" { + kind = "shared" + } + ctx := context.Background() + name := "scale-" + v3ID() + opts := &VersionedOptions{Redis: AhoCorasickArgs{Addr: addr, Name: name}, PollInterval: 50 * time.Millisecond} + v, err := OpenVersioned(ctx, opts) + if err != nil { + t.Fatal(err) + } + defer func() { _ = v.Close() }() + // Delete only this test's keys, never FLUSHDB on a supplied endpoint. + defer func() { + c, _ := newRedisClient(&opts.Redis) + defer c.Close() + var cursor uint64 + for { + keys, next, e := c.Scan(ctx, cursor, v.prefix+"*", 256).Result() + if e != nil { + return + } + if len(keys) > 0 { + c.Del(ctx, keys...) + } + cursor = next + if cursor == 0 { + break + } + } + }() + words := v3ScaleWords(n, kind) + serverInfo, _ := v.client.Info(ctx, "server").Result() + t.Logf("environment go=%s os=%s arch=%s cpus=%d n=%d kind=%s seed=20260906 server=%s", + runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.NumCPU(), n, kind, strings.ReplaceAll(serverInfo, "\r\n", " ")) + measure := func(label string, write func() (*WriteResult, error)) *WriteResult { + t.Helper() + before := v3ServerMetrics(ctx, v.client) + done := make(chan struct{}) + var wg sync.WaitGroup + var latencies []int64 + wg.Go(func() { + ticker := time.NewTicker(time.Millisecond) + defer ticker.Stop() + for { + select { + case <-done: + return + case <-ticker.C: + start := time.Now() + _, e := v.Find(ctx, words[0]+" "+words[n/2]+" "+words[n-1]) + if e != nil { + return + } + latencies = append(latencies, time.Since(start).Nanoseconds()) + } + } + }) + start := time.Now() + r, e := write() + commit := time.Since(start) + if e == nil { + e = v.WaitForVersion(ctx, r.Version) + } + ready := time.Since(start) + close(done) + wg.Wait() + if e != nil { + t.Fatal(label, e) + } + after := v3ServerMetrics(ctx, v.client) + slices.Sort(latencies) + percentile := func(p int) int64 { + if len(latencies) == 0 { + return 0 + } + return latencies[(len(latencies)-1)*p/100] + } + var usage syscall.Rusage + _ = syscall.Getrusage(syscall.RUSAGE_SELF, &usage) + rss := usage.Maxrss + if runtime.GOOS != "darwin" { + rss *= 1024 + } + record := map[string]interface{}{"operation": label, + "n": n, + "kind": kind, + "commit_ms": float64(commit.Microseconds()) / 1000, + "ready_ms": float64(ready.Microseconds()) / 1000, + "max_rss_bytes": rss, + "redis_bytes": after["used_memory"], + "sent_bytes": after["total_net_input_bytes"] - before["total_net_input_bytes"], + "received_bytes": after["total_net_output_bytes"] - before["total_net_output_bytes"], + "search_samples": len(latencies), + "search_p50_ns": percentile(50), + "search_p95_ns": percentile(95), + "search_p99_ns": percentile(99)} + data, _ := json.Marshal(record) + t.Log(string(data)) + return r + } + r := measure("initial_load", func() (*WriteResult, error) { return v.Replace(ctx, v.Status().ServingVersion, words) }) + // Reopening builds from stored keywords; search equivalence is checked below. + _ = v.Close() + runtime.GC() + started := time.Now() + v, err = OpenVersioned(ctx, opts) + if err != nil { + t.Fatal(err) + } + t.Logf("startup_ms=%.3f", float64(time.Since(started).Microseconds())/1000) + snapshot, err := v.Snapshot(ctx) + if err != nil { + t.Fatal(err) + } + count := 0 + cursor := "" + for { + page, e := snapshot.List(ctx, cursor, 4096) + if e != nil { + t.Fatal(e) + } + count += len(page.Keywords) + cursor = page.NextCursor + if cursor == "" { + break + } + } + _ = snapshot.Close(ctx) + if count != n { + t.Fatalf("count=%d want=%d", count, n) + } + text := words[0] + " " + words[n/2] + " " + words[n-1] + got, err := v.FindSet(ctx, text) + if err != nil { + t.Fatal(err) + } + expected := make([]string, 0) + for _, w := range words { + if strings.Contains(text, w) { + expected = append(expected, w) + } + } + slices.Sort(got) + slices.Sort(expected) + if !slices.Equal(got, expected) { + t.Fatal("search differs from naive reference") + } + measure("identical_replace", func() (*WriteResult, error) { return v.Replace(ctx, r.Version, words) }) + for _, changes := range []int{1, 1000, max(1, n/100)} { + added := make([]string, changes) + for i := range added { + added[i] = fmt.Sprintf("change-%d-%08d", changes, i) + } + r = measure(fmt.Sprintf("add_%d", changes), func() (*WriteResult, error) { return v.AddMany(ctx, r.Version, added) }) + r = measure(fmt.Sprintf("remove_%d", changes), func() (*WriteResult, error) { return v.RemoveMany(ctx, r.Version, added) }) + } + target := make([]string, len(words)) + for i, w := range words { + target[i] = w + "-x" + } + r = measure("full_replace", func() (*WriteResult, error) { return v.Replace(ctx, r.Version, target) }) + // Simulate passage of the retention horizon, keeping the active generation. + gens, _ := v.client.ZRange(ctx, v.key("generations"), 0, -1).Result() + for _, g := range gens { + if g != string(r.Version) { + v.client.ZAdd(ctx, v.key("generations"), redis.Z{Score: 1, Member: g}) + } + } + start := time.Now() + pruned, err := v.Prune(ctx) + if err != nil { + t.Fatal(err) + } + t.Logf("prune_ms=%.3f result=%+v redis_after=%d", float64(time.Since(start).Microseconds())/1000, pruned, v3ServerMetrics(ctx, v.client)["used_memory"]) +} +func v3ScaleWords(n int, kind string) []string { + rng := rand.New(rand.NewSource(20260906)) + words := make([]string, n) + for i := range words { + switch kind { + case "shared": + words[i] = fmt.Sprintf("shared-prefix-%08d", i) + case "diverse": + words[i] = fmt.Sprintf("%04x-%08d", rng.Uint32()&0xfff, i) + case "korean": + words[i] = fmt.Sprintf("한국어-%04x-키워드-%08d", rng.Uint32()&0xffff, i) + default: + panic("unknown scale workload") + } + } + return words +} +func v3ServerMetrics(ctx context.Context, client redis.UniversalClient) map[string]int64 { + info, _ := client.Info(ctx, "memory", "stats").Result() + out := make(map[string]int64) + for _, line := range strings.Split(info, "\n") { + k, value, ok := strings.Cut(strings.TrimSpace(line), ":") + if ok { + out[k], _ = strconv.ParseInt(value, 10, 64) + } + } + return out +} diff --git a/pkg/acor/versioned_search.go b/pkg/acor/versioned_search.go new file mode 100644 index 00000000..0ad2ba4f --- /dev/null +++ b/pkg/acor/versioned_search.go @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "io" + + matchengine "github.com/skyoo2003/acor/internal/engine" +) + +// v3SearchOps only backs private, read-only AhoCorasick adapters. No mutation or +// suggestion entry point is exported through VersionedCollection. +type v3SearchOps struct { + operations + engine *matchengine.Engine + sensitive bool +} + +func (o *v3SearchOps) loadEngine(ctx context.Context) (*matchengine.Engine, error) { + return o.engine, ctx.Err() +} +func (o *v3SearchOps) find(ctx context.Context, text string) ([]string, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + return o.engine.Find(normalizeText(text, o.sensitive)), nil +} +func (o *v3SearchOps) findIndex(ctx context.Context, text string) (map[string][]int, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + return o.engine.FindIndex(normalizeText(text, o.sensitive)), nil +} +func (v *VersionedCollection) search(ctx context.Context) (*AhoCorasick, error) { + if err := v.check(ctx); err != nil { + return nil, err + } + e := v.current.Load() + if e == nil { + return nil, ErrVersionedClosed + } + return &AhoCorasick{ctx: ctx, caseSensitive: v.opts.CaseSensitive, ops: &v3SearchOps{engine: e.engine, sensitive: v.opts.CaseSensitive}}, nil +} + +// Find uses one serving engine and the existing AhoCorasick Find semantics. +func (v *VersionedCollection) Find(ctx context.Context, text string) ([]string, error) { + ac, err := v.search(ctx) + if err != nil { + return nil, err + } + return ac.FindContext(ctx, text) +} + +// FindIndex uses one serving engine and the existing AhoCorasick FindIndex semantics. +func (v *VersionedCollection) FindIndex(ctx context.Context, text string) (map[string][]int, error) { + ac, err := v.search(ctx) + if err != nil { + return nil, err + } + return ac.FindIndexContext(ctx, text) +} + +// FindSet uses one serving engine and the existing AhoCorasick FindSet semantics. +func (v *VersionedCollection) FindSet(ctx context.Context, text string) ([]string, error) { + ac, err := v.search(ctx) + if err != nil { + return nil, err + } + return ac.FindSetContext(ctx, text) +} + +// FindMatches uses one serving engine and the existing AhoCorasick FindMatches semantics. +func (v *VersionedCollection) FindMatches(ctx context.Context, text string, opts *MatchOptions) ([]Match, error) { + ac, err := v.search(ctx) + if err != nil { + return nil, err + } + return ac.FindMatchesContext(ctx, text, opts) +} + +// Contains uses one serving engine and the existing AhoCorasick Contains semantics. +func (v *VersionedCollection) Contains(ctx context.Context, text string) (bool, error) { + ac, err := v.search(ctx) + if err != nil { + return false, err + } + return ac.ContainsContext(ctx, text) +} + +// FindParallel uses one serving engine and the existing AhoCorasick FindParallel semantics. +func (v *VersionedCollection) FindParallel(ctx context.Context, text string, opts *ParallelOptions) ([]string, error) { + ac, err := v.search(ctx) + if err != nil { + return nil, err + } + return ac.FindParallelContext(ctx, text, opts) +} + +// FindIndexParallel uses one serving engine and the existing AhoCorasick FindIndexParallel semantics. +func (v *VersionedCollection) FindIndexParallel(ctx context.Context, text string, opts *ParallelOptions) (map[string][]int, error) { + ac, err := v.search(ctx) + if err != nil { + return nil, err + } + return ac.FindIndexParallelContext(ctx, text, opts) +} + +// FindStream scans the entire stream with one engine, preserving chunk boundaries. +func (v *VersionedCollection) FindStream(ctx context.Context, r io.Reader, onMatch func(Match) bool) error { + ac, err := v.search(ctx) + if err != nil { + return err + } + return ac.FindStreamContext(ctx, r, onMatch) +} + +// FindBatch scans every input against one serving engine, preserving input order. +func (v *VersionedCollection) FindBatch(ctx context.Context, texts []string) ([][]string, error) { + ac, err := v.search(ctx) + if err != nil { + return nil, err + } + out := make([][]string, len(texts)) + for i, text := range texts { + out[i], err = ac.FindContext(ctx, text) + if err != nil { + return nil, err + } + } + return out, nil +} diff --git a/pkg/acor/versioned_snapshot.go b/pkg/acor/versioned_snapshot.go new file mode 100644 index 00000000..e4d2891c --- /dev/null +++ b/pkg/acor/versioned_snapshot.go @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "slices" + "strconv" + "strings" + "sync/atomic" + "time" + "unicode/utf8" +) + +type v3Lease struct { + owner *VersionedCollection + member string + set string + expired atomic.Bool + closed atomic.Bool +} + +// Redis time is authoritative for leases, retention, and fencing. +const v3Now = `local t=redis.call('TIME'); local now=t[1]*1000+math.floor(t[2]/1000); ` + +func (v *VersionedCollection) acquire(ctx context.Context, expected Version, writer bool) (*v3Lease, Version, error) { + if err := v.check(ctx); err != nil { + return nil, "", err + } + set := "leases" + if writer { + set = "writers" + } + const script = v3Now + ` + if redis.call('EXISTS',KEYS[1])==1 then return {'maintenance'} end + local active=redis.call('GET',KEYS[2]) + if ARGV[1]~='' and active~=ARGV[1] then return {'conflict'} end + local member=active..'/'..ARGV[2] + redis.call('ZADD',KEYS[3],now+ARGV[3],member) + return {active,member}` + keys := []string{v.key("maintenance"), v.key("active"), v.key(set)} + r, err := v.client.Eval(ctx, script, keys, string(expected), v3ID(), v.opts.LeaseDuration.Milliseconds()).StringSlice() + if err != nil { + return nil, "", err + } + if len(r) == 1 { + if r[0] == "maintenance" { + return nil, "", ErrMaintenance + } + return nil, "", ErrConcurrencyConflict + } + l := &v3Lease{owner: v, member: r[1], set: set} + v.mu.Lock() + if v.closed.Load() { + v.mu.Unlock() + _ = l.close(ctx) + return nil, "", ErrVersionedClosed + } + v.leases[l] = struct{}{} + v.mu.Unlock() + return l, Version(r[0]), nil +} +func (l *v3Lease) check(ctx context.Context) error { + if l.closed.Load() { + return ErrVersionedClosed + } + if l.expired.Load() { + return ErrLeaseExpired + } + if err := l.owner.check(ctx); err != nil { + return err + } + const script = v3Now + `local e=redis.call('ZSCORE',KEYS[1],ARGV[1]); if not e or tonumber(e)<=now then return 0 end; return 1` + ok, err := l.owner.client.Eval(ctx, script, []string{l.owner.key(l.set)}, l.member).Int() + if err != nil { + return err + } + if ok == 0 { + l.expired.Store(true) + return ErrLeaseExpired + } + return nil +} +func (l *v3Lease) renew(ctx context.Context) { + if l.expired.Load() { + return + } + const script = v3Now + `local e=redis.call('ZSCORE',KEYS[1],ARGV[1]); + if not e or tonumber(e)<=now then return 0 end; + redis.call('ZADD',KEYS[1],now+ARGV[2],ARGV[1]); return 1` + ok, err := l.owner.client.Eval(ctx, script, []string{l.owner.key(l.set)}, l.member, l.owner.opts.LeaseDuration.Milliseconds()).Int() + if err == nil && ok == 0 { + l.expired.Store(true) + } +} +func (l *v3Lease) close(ctx context.Context) error { + l.closed.Store(true) + l.expired.Store(true) + l.owner.mu.Lock() + delete(l.owner.leases, l) + l.owner.mu.Unlock() + return l.owner.client.ZRem(ctx, l.owner.key(l.set), l.member).Err() +} +func (v *VersionedCollection) renewLoop() { + defer v.wg.Done() + t := time.NewTicker(v.opts.LeaseRefresh) + defer t.Stop() + for { + select { + case <-v.ctx.Done(): + return + case <-t.C: + v.mu.Lock() + ls := make([]*v3Lease, 0, len(v.leases)) + for l := range v.leases { + ls = append(ls, l) + } + v.mu.Unlock() + for _, l := range ls { + l.renew(v.ctx) + } + } + } +} + +// Snapshot is a leased, immutable dictionary view. Close it after use. Its +// cursors are bound to its version and must not be interpreted by callers. +type Snapshot struct { + owner *VersionedCollection + manifest *v3Manifest + lease *v3Lease +} + +// Snapshot pins the active generation before reading its manifest. +func (v *VersionedCollection) Snapshot(ctx context.Context) (*Snapshot, error) { + l, t, err := v.acquire(ctx, "", false) + if err != nil { + return nil, err + } + m, err := v.manifest(ctx, t) + if err != nil { + _ = l.close(ctx) + return nil, err + } + return &Snapshot{owner: v, manifest: m, lease: l}, nil +} + +// Version returns the pinned opaque token. +func (s *Snapshot) Version() Version { return s.manifest.Version } + +// Count returns the pinned keyword count without I/O. +func (s *Snapshot) Count() int { return s.manifest.Count } + +// Close releases the generation lease. Subsequent reads fail. +func (s *Snapshot) Close(ctx context.Context) error { return s.lease.close(ctx) } + +// DictionaryPage lists keywords in bucket number, then lexical order. An empty +// NextCursor marks the end; an empty input cursor starts the snapshot. +type DictionaryPage struct { + Keywords []string + NextCursor string + Version Version +} + +type v3Cursor struct { + Version Version + Bucket int + Offset int +} + +// List reads at most limit entries; limit must be positive. It does not load the +// entire dictionary. A large individual keyword can exceed the chunk target. +func (s *Snapshot) List(ctx context.Context, cursor string, limit int) (*DictionaryPage, error) { + if limit <= 0 { + return nil, fmt.Errorf("acor: page limit must be positive") + } + if err := s.lease.check(ctx); err != nil { + return nil, err + } + c, err := s.cursor(cursor) + if err != nil { + return nil, err + } + p := &DictionaryPage{Version: s.Version(), Keywords: make([]string, 0, min(limit, v3PageHint))} + for c.Bucket < v3BucketCount { + words, err := s.owner.bucket(ctx, s.manifest.Buckets[c.Bucket]) + if err != nil { + return nil, err + } + if c.Offset > len(words) { + return nil, ErrInvalidVersion + } + n := min(limit-len(p.Keywords), len(words)-c.Offset) + p.Keywords = append(p.Keywords, words[c.Offset:c.Offset+n]...) + c.Offset += n + if c.Offset == len(words) { + c.Bucket++ + c.Offset = 0 + } + if len(p.Keywords) == limit { + break + } + } + // Skip empty buckets so the final nonempty page has no continuation. + for c.Bucket < v3BucketCount && s.manifest.Buckets[c.Bucket].Count == 0 { + c.Bucket++ + } + if c.Bucket < v3BucketCount { + b, _ := json.Marshal(c) + p.NextCursor = base64.RawURLEncoding.EncodeToString(b) + } + if err := s.lease.check(ctx); err != nil { + return nil, err + } + return p, nil +} +func (v *VersionedCollection) bucket(ctx context.Context, b v3Bucket) ([]string, error) { + words := make([]string, 0, b.Count) + if b.Count == 0 { + if len(b.Chunks) != 0 { + return nil, ErrVersionedCorrupt + } + return words, nil + } + for _, h := range b.Chunks { + data, err := v.client.Get(ctx, v.key("chunk:"+h)).Bytes() + if err != nil { + return nil, err + } + if v3Hash(data) != h { + return nil, ErrVersionedCorrupt + } + var part []string + if json.Unmarshal(data, &part) != nil { + return nil, ErrVersionedCorrupt + } + words = append(words, part...) + } + data, _ := json.Marshal(words) + if len(words) != b.Count || v3Hash(data) != b.Checksum || !slices.IsSorted(words) { + return nil, ErrVersionedCorrupt + } + return words, nil +} +func (s *Snapshot) all(ctx context.Context) ([]string, error) { + if err := s.lease.check(ctx); err != nil { + return nil, err + } + words := make([]string, 0, s.Count()) + for _, b := range &s.manifest.Buckets { + w, err := s.owner.bucket(ctx, b) + if err != nil { + return nil, err + } + words = append(words, w...) + } + if err := s.lease.check(ctx); err != nil { + return nil, err + } + return words, nil +} + +// DictionaryDiff compares a target with a pinned snapshot without writing. +// Each slice is sorted lexically after normalization and deduplication. +type DictionaryDiff struct { + Added []string + Removed []string + Retained []string + Version Version +} + +// Diff reports additions, removals and retained keywords for a full replacement. +func (s *Snapshot) Diff(ctx context.Context, target []string) (*DictionaryDiff, error) { + normalized, err := v3Normalize(target, s.owner.opts.CaseSensitive) + if err != nil { + return nil, err + } + words, err := s.all(ctx) + if err != nil { + return nil, err + } + old := make(map[string]struct{}, len(words)) + for _, w := range words { + old[w] = struct{}{} + } + d := &DictionaryDiff{Version: s.Version(), Added: []string{}, Removed: []string{}, Retained: []string{}} + for _, w := range normalized { + if _, ok := old[w]; ok { + d.Retained = append(d.Retained, w) + delete(old, w) + } else { + d.Added = append(d.Added, w) + } + } + for w := range old { + d.Removed = append(d.Removed, w) + } + slices.Sort(d.Removed) + return d, nil +} +func v3Normalize(words []string, sensitive bool) ([]string, error) { + out := make([]string, 0, len(words)) + for _, w := range words { + if !utf8.ValidString(w) { + return nil, fmt.Errorf("acor: keyword is not valid UTF-8") + } + w = strings.TrimSpace(w) + if !sensitive { + w = strings.ToLower(w) + } + if w == "" { + return nil, ErrEmptyKeyword + } + out = append(out, w) + } + slices.Sort(out) + return slices.Compact(out), nil +} +func v3BucketNumber(w string) int { + h := v3Hash([]byte(w)) + n, _ := strconv.ParseUint(h[:3], 16, 12) + return int(n) +} + +func (s *Snapshot) cursor(cursor string) (v3Cursor, error) { + c := v3Cursor{Version: s.Version()} + if cursor != "" { + b, err := base64.RawURLEncoding.DecodeString(cursor) + if err != nil || json.Unmarshal(b, &c) != nil || c.Version != s.Version() || c.Bucket < 0 || c.Bucket >= v3BucketCount || c.Offset < 0 { + return c, ErrInvalidVersion + } + } + return c, nil +} diff --git a/pkg/acor/versioned_test.go b/pkg/acor/versioned_test.go new file mode 100644 index 00000000..65f04bf4 --- /dev/null +++ b/pkg/acor/versioned_test.go @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "errors" + "fmt" + "reflect" + "slices" + "strings" + "sync" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + redis "github.com/redis/go-redis/v9" +) + +func openV3Test(t *testing.T, server *miniredis.Miniredis, name string) *VersionedCollection { + t.Helper() + v, err := OpenVersioned(context.Background(), &VersionedOptions{Redis: AhoCorasickArgs{Addr: server.Addr(), Name: name}, PollInterval: 20 * time.Millisecond}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = v.Close() }) + return v +} +func waitV3(t *testing.T, v *VersionedCollection, version Version) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := v.WaitForVersion(ctx, version); err != nil { + t.Fatal(err) + } +} + +//nolint:gocyclo,funlen // One lifecycle follows the same version through successive assertions. +func TestVersionedLifecycle(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "dictionary") + initial := v.Status().ServingVersion + r, err := v.Replace(ctx, initial, []string{" He ", "she", "hers", "한국", "한국어", "CAFÉ", "café"}) + if err != nil { + t.Fatal(err) + } + if r.Added != 6 || r.Removed != 0 { + t.Fatal(r) + } + waitV3(t, v, r.Version) + s, err := v.Snapshot(ctx) + if err != nil { + t.Fatal(err) + } + defer s.Close(ctx) + var words []string + cursor := "" + for { + p, e := s.List(ctx, cursor, 2) + if e != nil { + t.Fatal(e) + } + words = append(words, p.Keywords...) + cursor = p.NextCursor + if cursor == "" { + break + } + } + if len(words) != 6 { + t.Fatal(words) + } + d, err := s.Diff(ctx, []string{"he", "new"}) + if err != nil || !reflect.DeepEqual(d.Added, []string{"new"}) || len(d.Removed) != 5 { + t.Fatal(d, err) + } + same, err := v.Replace(ctx, r.Version, words) + if err != nil || same.Version != r.Version { + t.Fatal(same, err) + } + if _, err = v.AddMany(ctx, r.Version, []string{"valid", " "}); !errors.Is(err, ErrEmptyKeyword) { + t.Fatal(err) + } + r2, err := v.RemoveMany(ctx, r.Version, []string{"he", "she", "missing"}) + if err != nil || r2.Removed != 2 { + t.Fatal(r2, err) + } + if _, err = v.Add(ctx, r.Version, "conflict"); !errors.Is(err, ErrConcurrencyConflict) { + t.Fatal(err) + } + pinned, err := s.all(ctx) + if err != nil || !reflect.DeepEqual(words, pinned) { + t.Fatal(pinned, err) + } + receipt, err := v.ResolveOperation(ctx, r2.OperationID) + if err != nil || *receipt != *r2 { + t.Fatal(receipt, err) + } + waitV3(t, v, r2.Version) + if err = v.WaitForVersion(ctx, r.Version); err != nil { + t.Fatal(err) + } + empty, err := v.Replace(ctx, r2.Version, nil) + if err != nil { + t.Fatal(err) + } + waitV3(t, v, empty.Version) + found, err := v.Find(ctx, "she 한국") + if err != nil || len(found) != 0 { + t.Fatal(found, err) + } +} +func TestVersionedSearchParity(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "v3") + words := []string{"he", "she", "hers", "한국", "한국어", "café", "ab", "abc", "bc"} + legacy, err := Create(&AhoCorasickArgs{Addr: server.Addr(), Name: "v2", Preset: PresetMemoryEfficient}) + if err != nil { + t.Fatal(err) + } + defer legacy.Close() + if _, err = legacy.AddMany(words, nil); err != nil { + t.Fatal(err) + } + r, err := v.Replace(ctx, v.Status().ServingVersion, words) + if err != nil { + t.Fatal(err) + } + waitV3(t, v, r.Version) + text := "SHE hers 한국어 CAFÉ abc abc" + a, err := v.Find(ctx, text) + if err != nil { + t.Fatal(err) + } + b, err := legacy.Find(text) + if err != nil || !reflect.DeepEqual(a, b) { + t.Fatal(a, b, err) + } + ai, _ := v.FindIndex(ctx, text) + bi, _ := legacy.FindIndex(text) + if !reflect.DeepEqual(ai, bi) { + t.Fatal(ai, bi) + } + for _, o := range []*MatchOptions{nil, {WholeWord: true}, {Kind: MatchKindLeftmostLongest}} { + am, _ := v.FindMatches(ctx, text, o) + bm, _ := legacy.FindMatches(text, o) + if !reflect.DeepEqual(am, bm) { + t.Fatal(am, bm) + } + } + var streamed []Match + if err = v.FindStream(ctx, strings.NewReader(strings.Repeat(text, 300)), func(m Match) bool { streamed = append(streamed, m); return true }); err != nil { + t.Fatal(err) + } + expected, _ := v.FindMatches(ctx, strings.Repeat(text, 300), nil) + if !reflect.DeepEqual(streamed, expected) { + t.Fatal("stream mismatch") + } + opts := &ParallelOptions{Workers: 3, ChunkSize: 3, AutoOverlap: true} + ap, _ := v.FindIndexParallel(ctx, text, opts) + bp, _ := legacy.FindIndexParallel(text, opts) + if !reflect.DeepEqual(ap, bp) { + t.Fatal(ap, bp) + } +} +func TestVersionedConcurrentWriters(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "race") + other := openV3Test(t, server, "race") + expected := v.Status().ServingVersion + var wg sync.WaitGroup + results := make(chan error, 2) + for i, c := range []*VersionedCollection{v, other} { + wg.Go(func() { _, err := c.Add(ctx, expected, fmt.Sprint(i)); results <- err }) + } + wg.Wait() + close(results) + successes, conflicts := 0, 0 + for err := range results { + if err == nil { + successes++ + } else if errors.Is(err, ErrConcurrencyConflict) { + conflicts++ + } else { + t.Fatal(err) + } + } + if successes != 1 || conflicts != 1 { + t.Fatal(successes, conflicts) + } + s, err := other.Snapshot(ctx) + if err != nil { + t.Fatal(err) + } + defer s.Close(ctx) + waitV3(t, v, s.Version()) + waitV3(t, other, s.Version()) +} + +//nolint:gocyclo // Retention assertions share a pinned generation and its successor. +func TestVersionedReuseLeasePrune(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "prune") + r, err := v.Replace(ctx, v.Status().ServingVersion, []string{"one", "two", "three"}) + if err != nil { + t.Fatal(err) + } + waitV3(t, v, r.Version) + s, err := v.Snapshot(ctx) + if err != nil { + t.Fatal(err) + } + r2, err := v.Add(ctx, r.Version, "four") + if err != nil { + t.Fatal(err) + } + waitV3(t, v, r2.Version) + next, err := v.manifest(ctx, r2.Version) + if err != nil { + t.Fatal(err) + } + changed := v3BucketNumber("four") + for i, b := range s.manifest.Buckets { + if i != changed && !reflect.DeepEqual(b, next.Buckets[i]) { + t.Fatal("unchanged bucket rewritten", i) + } + } + // Age all generations without expiring the lease under test. + gens, _ := v.client.ZRange(ctx, v.key("generations"), 0, -1).Result() + for _, g := range gens { + v.client.ZAdd(ctx, v.key("generations"), redis.Z{Score: 1, Member: g}) + } + if _, err = v.Prune(ctx); err != nil { + t.Fatal(err) + } + if _, err = s.all(ctx); err != nil { + t.Fatal("leased snapshot pruned", err) + } + _ = s.Close(ctx) + result, err := v.Prune(ctx) + if err != nil || result.Generations == 0 { + t.Fatal(result, err) + } + if _, err = v.manifest(ctx, r.Version); !errors.Is(err, redis.Nil) { + t.Fatal(err) + } + if err = v.WaitForVersion(ctx, r.Version); err != nil { + t.Fatal(err) + } + expired, err := v.Snapshot(ctx) + if err != nil { + t.Fatal(err) + } + v.client.ZAdd(ctx, v.key("leases"), redis.Z{Score: 1, Member: expired.lease.member}) + expired.lease.renew(ctx) + if _, err = expired.List(ctx, "", 1); !errors.Is(err, ErrLeaseExpired) { + t.Fatal(err) + } +} +func TestVersionedFencingAndPreparationFailure(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "failure") + initial := v.Status().ServingVersion + l, _, err := v.acquire(ctx, initial, true) + if err != nil { + t.Fatal(err) + } + if _, err = v.Prune(ctx); !errors.Is(err, ErrMaintenance) { + t.Fatal(err) + } + v.client.ZAdd(ctx, v.key("writers"), redis.Z{Score: 1, Member: l.member}) + if err = v.stage(ctx, l, "chunk:orphan", "chunks", "orphan", []byte("[]")); !errors.Is(err, ErrLeaseExpired) { + t.Fatal(err) + } + if v.client.Exists(ctx, v.key("chunk:orphan")).Val() != 0 { + t.Fatal("expired writer recorded data") + } + _ = l.close(ctx) + v.client.Set(ctx, v.key("maintenance"), "new-owner", time.Minute) + if _, err = v.pruneDelete(ctx, "old-owner", "generations", "gen:", []string{string(initial)}); !errors.Is(err, ErrMaintenance) { + t.Fatal(err) + } + if _, err = v.manifest(ctx, initial); err != nil { + t.Fatal(err) + } + v.client.Del(ctx, v.key("maintenance")) + // Wrong-type registry fails staging after a chunk may have been prepared; + // publication must still leave the active generation untouched. + v.client.Set(ctx, v.key("chunks"), "wrong-type", 0) + if _, err = v.Add(ctx, initial, "new"); err == nil { + t.Fatal("expected failure") + } + if active := v.client.Get(ctx, v.key("active")).Val(); active != string(initial) { + t.Fatal(active) + } +} +func TestVersionedCaseAndForeignTokens(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "case") + _, err := OpenVersioned(ctx, &VersionedOptions{Redis: AhoCorasickArgs{Addr: server.Addr(), Name: "case"}, CaseSensitive: true}) + if !errors.Is(err, ErrCasePolicy) { + t.Fatal(err) + } + other := openV3Test(t, server, "other") + if _, err = v.Replace(ctx, other.Status().ServingVersion, nil); !errors.Is(err, ErrInvalidVersion) { + t.Fatal(err) + } + _ = v.Close() + if _, err = v.Find(ctx, "a"); !errors.Is(err, ErrVersionedClosed) { + t.Fatal(err) + } +} +func TestVersionedChunkLimit(t *testing.T) { + ctx := context.Background() + server := miniredis.RunT(t) + v := openV3Test(t, server, "chunks") + l, _, err := v.acquire(ctx, v.Status().ServingVersion, true) + if err != nil { + t.Fatal(err) + } + defer l.close(ctx) + words := []string{"a", strings.Repeat("b", v3ChunkBytes), strings.Repeat("c", v3ChunkBytes/2), strings.Repeat("d", v3ChunkBytes/2)} + b, err := v.stageBucket(ctx, l, words) + if err != nil { + t.Fatal(err) + } + got, err := v.bucket(ctx, b) + if err != nil || !slices.Equal(got, words) || len(b.Chunks) != 4 { + t.Fatal(len(b.Chunks), err) + } +} diff --git a/pkg/acor/versioned_write.go b/pkg/acor/versioned_write.go new file mode 100644 index 00000000..db2d7f13 --- /dev/null +++ b/pkg/acor/versioned_write.go @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: Apache-2.0 + +package acor + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "slices" + "strings" + + redis "github.com/redis/go-redis/v9" +) + +const v3ChunkBytes = 1 << 20 +const v3Add = "add" + +// Every preparation write is fenced, including writes by an expired process +// resuming after Prune. Registration and content creation are one atomic step. +const v3StageScript = v3Now + ` + if redis.call('EXISTS',KEYS[1])==1 then return 0 end + local e=redis.call('ZSCORE',KEYS[2],ARGV[1]) + if not e or tonumber(e)<=now then return 0 end + redis.call('ZADD',KEYS[4],'NX',math.floor(now/1000),ARGV[3]) + redis.call('SET',KEYS[3],ARGV[2],'NX') + return 1` + +// The commit does not decode manifests or traverse dictionary data. An operation +// receipt makes transport-level retries idempotent, even after another commit. +const v3CommitScript = v3Now + ` + local receipt=redis.call('GET',KEYS[5]); if receipt then return receipt end + if redis.call('EXISTS',KEYS[1])==1 then return 'maintenance' end + local e=redis.call('ZSCORE',KEYS[2],ARGV[1]) + if not e or tonumber(e)<=now then return 'expired' end + if redis.call('GET',KEYS[3])~=ARGV[2] then return 'conflict' end + if redis.call('EXISTS',KEYS[4])==0 then return 'missing' end + redis.call('SET',KEYS[5],ARGV[4]) + if ARGV[2]~=ARGV[3] then + redis.call('SET',KEYS[3],ARGV[3]) + redis.call('SET',KEYS[7],ARGV[5]) + redis.call('ZADD',KEYS[8],math.floor(now/1000),ARGV[3]) + redis.call('PUBLISH',KEYS[6],ARGV[3]) + end + return ARGV[4]` + +// Replace atomically replaces the dictionary against a mandatory expected +// version. An empty target deletes all keywords; an identical target is a no-op. +func (v *VersionedCollection) Replace(ctx context.Context, expected Version, words []string) (*WriteResult, error) { + return v.change(ctx, expected, words, "replace") +} + +// Add atomically adds a normalized keyword against expected. +func (v *VersionedCollection) Add(ctx context.Context, expected Version, word string) (*WriteResult, error) { + return v.AddMany(ctx, expected, []string{word}) +} + +// Remove atomically removes a normalized keyword against expected. +func (v *VersionedCollection) Remove(ctx context.Context, expected Version, word string) (*WriteResult, error) { + return v.RemoveMany(ctx, expected, []string{word}) +} + +// AddMany adds all normalized keywords or none. Only affected buckets are read +// and rewritten. Duplicate and already-present keywords are no-ops. +func (v *VersionedCollection) AddMany(ctx context.Context, expected Version, words []string) (*WriteResult, error) { + return v.change(ctx, expected, words, v3Add) +} + +// RemoveMany removes all normalized keywords or none; absent entries are no-ops. +func (v *VersionedCollection) RemoveMany(ctx context.Context, expected Version, words []string) (*WriteResult, error) { + return v.change(ctx, expected, words, "remove") +} +func (v *VersionedCollection) change(ctx context.Context, expected Version, words []string, mode string) (*WriteResult, error) { + if !v.valid(expected) { + return nil, ErrInvalidVersion + } + normalized, err := v3Normalize(words, v.opts.CaseSensitive) + if err != nil { + return nil, err + } + l, _, err := v.acquire(ctx, expected, true) + if err != nil { + return nil, err + } + defer func() { _ = l.close(ctx) }() + old, err := v.manifest(ctx, expected) + if err != nil { + return nil, err + } + next := *old + result := &WriteResult{PreviousVersion: expected, Version: expected, OperationID: v3ID()} + var buckets [v3BucketCount][]string + for _, w := range normalized { + b := v3BucketNumber(w) + buckets[b] = append(buckets[b], w) + } + for i, input := range &buckets { + if mode != "replace" && len(input) == 0 { + continue + } + before, readErr := v.bucket(ctx, old.Buckets[i]) + if readErr != nil { + return nil, readErr + } + after, added, removed := v3Apply(before, input, mode) + if added == 0 && removed == 0 { + continue + } + result.Added += added + result.Removed += removed + b, stageErr := v.stageBucket(ctx, l, after) + if stageErr != nil { + return nil, stageErr + } + next.Buckets[i] = b + } + return v.prepareCommit(ctx, l, &next, result) +} +func (v *VersionedCollection) prepareCommit(ctx context.Context, l *v3Lease, next *v3Manifest, result *WriteResult) (*WriteResult, error) { + if result.Added != 0 || result.Removed != 0 { + next.Version = Version(v.id + "." + v3ID()) + next.Sequence++ + next.Count += result.Added - result.Removed + result.Version = next.Version + data, _ := json.Marshal(next) + if err := v.stage(ctx, l, "gen:"+string(next.Version), "generations", string(next.Version), data); err != nil { + return nil, err + } + } + return v.commit(ctx, l, result, next.Sequence) +} + +func (v *VersionedCollection) commit(ctx context.Context, l *v3Lease, result *WriteResult, sequence uint64) (*WriteResult, error) { + data, _ := json.Marshal(result) + keys := []string{v.key("maintenance"), v.key("writers"), v.key("active"), v.key("gen:" + string(result.Version)), + v.key("op:" + result.OperationID), v.key("events"), v.key("committed:" + string(result.Version)), v.key("generations")} + raw, err := v.client.Eval(ctx, v3CommitScript, keys, l.member, string(result.PreviousVersion), string(result.Version), data, sequence).Text() + if err != nil { + return result, fmt.Errorf("%w: %w", ErrCommitUnknown, err) + } + switch raw { + case "conflict": + return nil, ErrConcurrencyConflict + case "maintenance": + return nil, ErrMaintenance + case "expired": + return nil, ErrLeaseExpired + case "missing": + return nil, ErrVersionedCorrupt + } + var committed WriteResult + if json.Unmarshal([]byte(raw), &committed) != nil { + return result, ErrCommitUnknown + } + v.signal() + return &committed, nil +} +func v3Apply(before, input []string, mode string) (after []string, added, removed int) { + set := make(map[string]struct{}, len(before)+len(input)) + for _, w := range before { + set[w] = struct{}{} + } + if mode == "replace" { + for _, w := range input { + if _, ok := set[w]; !ok { + added++ + } else { + delete(set, w) + } + } + return input, added, len(set) + } + for _, w := range input { + _, ok := set[w] + if mode == v3Add && !ok { + set[w] = struct{}{} + added++ + } + if mode == "remove" && ok { + delete(set, w) + removed++ + } + } + after = make([]string, 0, len(set)) + for w := range set { + after = append(after, w) + } + slices.Sort(after) + return after, added, removed +} +func (v *VersionedCollection) stage(ctx context.Context, l *v3Lease, key, registry, id string, data []byte) error { + ok, err := v.client.Eval(ctx, v3StageScript, []string{v.key("maintenance"), v.key("writers"), v.key(key), v.key(registry)}, l.member, data, id).Int() + if err != nil { + return err + } + if ok != 1 { + return ErrLeaseExpired + } + return nil +} +func (v *VersionedCollection) stageBucket(ctx context.Context, l *v3Lease, words []string) (v3Bucket, error) { + b := v3Bucket{Count: len(words)} + if len(words) == 0 { + return b, nil + } + data, _ := json.Marshal(words) + b.Checksum = v3Hash(data) + // Size includes JSON quotes, escaping, commas and brackets. Oversize single + // keywords form independent chunks and cannot make neighboring chunks exceed 1 MiB. + start, size := 0, 2 + flush := func(end int) error { + part, _ := json.Marshal(words[start:end]) + h := v3Hash(part) + if err := v.stage(ctx, l, "chunk:"+h, "chunks", h, part); err != nil { + return err + } + b.Chunks = append(b.Chunks, h) + start = end + size = 2 + return nil + } + for i, w := range words { + encoded, _ := json.Marshal(w) + n := len(encoded) + if i > start { + n++ + } + if size+n > v3ChunkBytes && i > start { + if err := flush(i); err != nil { + return b, err + } + n = len(encoded) + } + size += n + } + if err := flush(len(words)); err != nil { + return b, err + } + return b, nil +} + +// ResolveOperation retrieves a durable successful commit receipt. redis.Nil +// means no receipt was observed, not proof that an in-flight commit cannot still +// succeed. The caller must not automatically reapply an ambiguous operation. +func (v *VersionedCollection) ResolveOperation(ctx context.Context, id string) (*WriteResult, error) { + if err := v.check(ctx); err != nil { + return nil, err + } + if len(id) != v3IDLength { + return nil, errors.New("acor: invalid operation ID") + } + raw, err := v.client.Get(ctx, v.key("op:"+id)).Bytes() + if err != nil { + return nil, err + } + var r WriteResult + if json.Unmarshal(raw, &r) != nil || !v.valid(r.Version) { + return nil, ErrVersionedCorrupt + } + return &r, nil +} + +// V2CopyResult records the exact V2 version read and normalized target checksum. +// Freeze V2 writers before the final copy and application cutover. +type V2CopyResult struct { + SourceVersion string + Count int + Checksum string + Write *WriteResult +} + +// V2CopyOptions controls the optional empty-source guard. A nil option allows an empty source. +type V2CopyOptions struct { + // RejectEmpty fails before writing when the normalized source contains no keywords. + RejectEmpty bool +} + +// CopyV2 atomically reads V2 keywords and version from a differently named source +// on the same Redis connection and replaces this collection against expected. +// V1 sources must first use the existing V1-to-V2 migration. +func (v *VersionedCollection) CopyV2(ctx context.Context, source string, expected Version, opts *V2CopyOptions) (*V2CopyResult, error) { + if source == v.opts.Redis.Name || strings.TrimSpace(source) == "" { + return nil, errors.New("acor: copy requires a distinct nonempty V2 name") + } + raw, err := v.client.HMGet(ctx, trieKey(source), fieldVersion, fieldKeywords).Result() + if err != nil { + return nil, err + } + if raw[0] == nil || raw[1] == nil { + return nil, redis.Nil + } + var words []string + if json.Unmarshal([]byte(fmt.Sprint(raw[1])), &words) != nil { + return nil, ErrVersionedCorrupt + } + normalized, err := v3Normalize(words, v.opts.CaseSensitive) + if err != nil { + return nil, err + } + if opts != nil && opts.RejectEmpty && len(normalized) == 0 { + return nil, errors.New("acor: empty V2 source rejected") + } + data, _ := json.Marshal(normalized) + result := &V2CopyResult{SourceVersion: fmt.Sprint(raw[0]), Count: len(normalized), Checksum: v3Hash(data)} + result.Write, err = v.Replace(ctx, expected, normalized) + return result, err +} diff --git a/scripts/benchmark-v3.sh b/scripts/benchmark-v3.sh new file mode 100644 index 00000000..91157872 --- /dev/null +++ b/scripts/benchmark-v3.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# Run against one disposable Redis/Valkey endpoint at a time. No FLUSHDB is used. +set -eu +: "${ACOR_V3_SCALE_ADDR:?set ACOR_V3_SCALE_ADDR to a disposable real server}" +repeats=${ACOR_V3_SCALE_REPEATS:-2} +output=${ACOR_V3_SCALE_OUTPUT:-/tmp/acor-v3-results} +mkdir -p "$output" +binary=$(mktemp /tmp/acor-v3-test.XXXXXX) +trap 'rm -f "$binary"' EXIT HUP INT TERM +go test -c ./pkg/acor -o "$binary" +repeat=1 +while [ "$repeat" -le "$repeats" ]; do + for count in 10000 100000 1000000; do + for kind in shared diverse korean; do + ACOR_V3_SCALE_N=$count ACOR_V3_SCALE_KIND=$kind \ + "$binary" -test.run='^TestVersionedScale$' -test.v -test.timeout=15m \ + > "$output/$count-$kind-$repeat.txt" 2>&1 + echo "completed $count $kind repeat $repeat" + done + done + repeat=$((repeat + 1)) +done +"$binary" -test.run='^TestVersionedMillionSafety$' -test.v -test.timeout=15m \ + > "$output/million-safety.txt" 2>&1