Skip to content

Commit dd4ff4b

Browse files
authored
Add additional benchmarks for TypeID (#447)
## Summary Adds some additional benchmarks to the existing ones we had in typeid. ## How was it tested? Ran the benchmarks. ## Community Contribution License All community contributions in this pull request are licensed to the project maintainers under the terms of the [Apache 2 license](https://www.apache.org/licenses/LICENSE-2.0). By creating this pull request I represent that I have the right to license the contributions to the project maintainers under the Apache 2 license. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Chores** - Enhanced repository maintenance with refined ignore patterns and updated dependency management for improved consistency. - **Tests** - Expanded performance benchmarks to assess encoding efficiency, memory usage under various workloads, and parallel ID generation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent b8c05c2 commit dd4ff4b

File tree

2 files changed

+265
-0
lines changed

2 files changed

+265
-0
lines changed

.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Benchmark results
2+
benchmark_results/
3+
4+
# Go specific
5+
*.prof
6+
*.test
7+
*.out
8+
/vendor/
9+
10+
# OS specific
11+
.DS_Store
12+
Thumbs.db
13+
14+
# Editor files
15+
.idea/
16+
.vscode/
17+
*.swp
18+
*.swo
19+
*~
20+
21+
# Build artifacts
22+
bin/
23+
dist/

bench_test.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package typeid_test
33

44
import (
55
"fmt"
6+
"math/rand"
7+
"runtime"
68
"testing"
79

810
"github.com/gofrs/uuid/v5"
911
"go.jetify.com/typeid"
12+
"go.jetify.com/typeid/base32"
1013
)
1114

1215
func BenchmarkNew(b *testing.B) {
@@ -28,6 +31,25 @@ func BenchmarkNew(b *testing.B) {
2831
uuid.NewV7()
2932
}
3033
})
34+
// Add benchmark for different prefix lengths
35+
b.Run("prefix=short", func(b *testing.B) {
36+
b.ReportAllocs()
37+
for i := 0; i < b.N; i++ {
38+
typeid.WithPrefix("s")
39+
}
40+
})
41+
b.Run("prefix=medium", func(b *testing.B) {
42+
b.ReportAllocs()
43+
for i := 0; i < b.N; i++ {
44+
typeid.WithPrefix("medium")
45+
}
46+
})
47+
b.Run("prefix=long", func(b *testing.B) {
48+
b.ReportAllocs()
49+
for i := 0; i < b.N; i++ {
50+
typeid.WithPrefix("thisislongprefix")
51+
}
52+
})
3153
}
3254

3355
func BenchmarkString(b *testing.B) {
@@ -349,6 +371,226 @@ func BenchmarkEncodeDecode(b *testing.B) {
349371
}
350372
}
351373

374+
// Benchmark Base32 operations directly
375+
func BenchmarkBase32(b *testing.B) {
376+
b.Run("encode", func(b *testing.B) {
377+
uid := uuid.Must(uuid.NewV7())
378+
var bytes [16]byte
379+
copy(bytes[:], uid.Bytes())
380+
b.ReportAllocs()
381+
b.ResetTimer()
382+
for i := 0; i < b.N; i++ {
383+
_ = base32.Encode(bytes)
384+
}
385+
})
386+
b.Run("decode", func(b *testing.B) {
387+
uid := uuid.Must(uuid.NewV7())
388+
var bytes [16]byte
389+
copy(bytes[:], uid.Bytes())
390+
encoded := base32.Encode(bytes)
391+
b.ReportAllocs()
392+
b.ResetTimer()
393+
for i := 0; i < b.N; i++ {
394+
_, _ = base32.Decode(encoded)
395+
}
396+
})
397+
}
398+
399+
// Benchmark memory usage with different batch sizes
400+
func BenchmarkMemoryUsage(b *testing.B) {
401+
benchSizes := []int{100, 1000, 10000}
402+
for _, size := range benchSizes {
403+
size := size // capture range variable
404+
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
405+
// Pre-allocate a slice to avoid measuring slice growth
406+
ids := make([]typeid.AnyID, size)
407+
b.Cleanup(func() {
408+
// Clear the slice to help GC
409+
for i := range ids {
410+
ids[i] = typeid.AnyID{}
411+
}
412+
ids = nil
413+
})
414+
415+
b.ReportAllocs()
416+
b.SetBytes(int64(size * 16)) // Each UUID is 16 bytes
417+
b.ResetTimer()
418+
419+
for i := 0; i < b.N; i++ {
420+
for j := range ids {
421+
ids[j] = typeid.Must(typeid.WithPrefix("prefix"))
422+
}
423+
// Force memory pressure to ensure GC behavior is measured
424+
runtime.GC()
425+
}
426+
})
427+
}
428+
}
429+
430+
// Benchmark validation
431+
func BenchmarkValidation(b *testing.B) {
432+
validIDs := make([]string, 100)
433+
invalidIDs := make([]string, 100)
434+
435+
for i := range validIDs {
436+
validIDs[i] = typeid.Must(typeid.WithPrefix("prefix")).String()
437+
if i < len(invalidIDs) {
438+
// Create definitely invalid IDs by:
439+
// 1. Using invalid prefix characters
440+
// 2. Wrong separator
441+
// 3. Invalid base32 characters
442+
switch i % 3 {
443+
case 0:
444+
// Invalid prefix (contains number)
445+
invalidIDs[i] = "prefix1_01h2xcejqtf2nbrexx3vqjhp41"
446+
case 1:
447+
// Wrong separator (using . instead of _)
448+
invalidIDs[i] = "prefix.01h2xcejqtf2nbrexx3vqjhp41"
449+
case 2:
450+
// Invalid base32 character in suffix (using 'u' which isn't in the alphabet)
451+
invalidIDs[i] = "prefix_u1h2xcejqtf2nbrexx3vqjhp41"
452+
}
453+
}
454+
}
455+
456+
b.Run("valid", func(b *testing.B) {
457+
b.ReportAllocs()
458+
b.ResetTimer()
459+
460+
for i := 0; i < b.N; i++ {
461+
// Test all valid IDs in each iteration
462+
for _, id := range validIDs {
463+
_, err := typeid.FromString(id)
464+
if err != nil {
465+
b.Fatalf("Expected valid ID to pass validation: %v", err)
466+
}
467+
}
468+
}
469+
})
470+
471+
b.Run("invalid", func(b *testing.B) {
472+
b.ReportAllocs()
473+
b.ResetTimer()
474+
475+
for i := 0; i < b.N; i++ {
476+
// Test all invalid IDs in each iteration
477+
for _, id := range invalidIDs {
478+
_, err := typeid.FromString(id)
479+
if err == nil {
480+
b.Fatalf("Expected invalid ID to fail validation for ID: %s", id)
481+
}
482+
}
483+
}
484+
})
485+
}
486+
487+
// Benchmark parallel ID generation
488+
func BenchmarkParallelGeneration(b *testing.B) {
489+
benchCases := []struct {
490+
name string
491+
procs int
492+
batchSize int
493+
}{
494+
{"procs=4_batch=100", 4, 100},
495+
{"procs=8_batch=100", runtime.GOMAXPROCS(0) * 2, 100},
496+
{"procs=4_batch=1000", 4, 1000},
497+
{"procs=8_batch=1000", runtime.GOMAXPROCS(0) * 2, 1000},
498+
}
499+
500+
for _, bc := range benchCases {
501+
bc := bc // capture range variable
502+
b.Run(bc.name, func(b *testing.B) {
503+
// Pre-allocate a slice of prefixes for each processor
504+
prefixes := make([]string, bc.procs)
505+
for i := range prefixes {
506+
// Use valid prefixes with only [a-z_]
507+
prefixes[i] = fmt.Sprintf("prefix_%c", 'a'+i)
508+
}
509+
510+
b.SetParallelism(bc.procs)
511+
b.ReportAllocs()
512+
b.ResetTimer()
513+
514+
b.RunParallel(func(pb *testing.PB) {
515+
// Pre-allocate slice with capacity
516+
ids := make([]typeid.AnyID, 0, bc.batchSize)
517+
// Keep track of which prefix we're using
518+
prefixIdx := 0
519+
520+
for pb.Next() {
521+
// Clear the slice but keep capacity
522+
ids = ids[:0]
523+
524+
// Use all prefixes in each iteration
525+
for j := 0; j < bc.batchSize; j++ {
526+
// Cycle through prefixes
527+
prefix := prefixes[prefixIdx%len(prefixes)]
528+
prefixIdx++
529+
ids = append(ids, typeid.Must(typeid.WithPrefix(prefix)))
530+
}
531+
}
532+
})
533+
})
534+
}
535+
}
536+
537+
// Benchmark mixed operations to simulate real-world usage patterns
538+
func BenchmarkMixedOperations(b *testing.B) {
539+
// Pre-generate all operations and test data
540+
type op struct {
541+
kind string
542+
id typeid.AnyID // For operations that need an existing ID
543+
}
544+
545+
// Create a slice of operations in the ratio we want to test
546+
numOps := 100 // Number of operations to pre-generate
547+
ops := make([]op, numOps)
548+
ids := make([]typeid.AnyID, numOps/2) // Pre-generate some IDs for operations that need them
549+
550+
for i := range ids {
551+
ids[i] = typeid.Must(typeid.WithPrefix("prefix"))
552+
}
553+
554+
// Initialize random number generator
555+
src := rand.NewSource(1234)
556+
rnd := rand.New(src)
557+
558+
// Generate operations in desired ratios
559+
for i := range ops {
560+
r := rnd.Float32()
561+
switch {
562+
case r < 0.1: // 10% new IDs
563+
ops[i] = op{kind: "create"}
564+
case r < 0.4: // 30% toString
565+
ops[i] = op{kind: "toString", id: ids[rnd.Intn(len(ids))]}
566+
case r < 0.7: // 30% parse
567+
ops[i] = op{kind: "parse", id: ids[rnd.Intn(len(ids))]}
568+
default: // 30% validate
569+
ops[i] = op{kind: "validate", id: ids[rnd.Intn(len(ids))]}
570+
}
571+
}
572+
573+
b.ReportAllocs()
574+
b.ResetTimer()
575+
576+
// Ensure we go through all operations at least once
577+
for i := 0; i < b.N; i++ {
578+
// Do all operations once per iteration
579+
for _, op := range ops {
580+
switch op.kind {
581+
case "create":
582+
_ = typeid.Must(typeid.WithPrefix("prefix"))
583+
case "toString":
584+
_ = op.id.String()
585+
case "parse":
586+
_, _ = typeid.FromString(op.id.String())
587+
case "validate":
588+
_, _ = typeid.FromString(op.id.String())
589+
}
590+
}
591+
}
592+
}
593+
352594
// TODO: define these in a shared file if we're gonna use in several tests.
353595

354596
type TestPrefix struct{}

0 commit comments

Comments
 (0)