package main import ( "fmt" "math/rand" "os" "runtime" "sync" "sync/atomic" "time" "igit.com/xbase/raft/db" ) const ( TotalKeys = 100000 DataDir = "bench_db_data" ) func main() { // Scenario: Key Query Performance and Correctness fmt.Println("==================================================") fmt.Println("SCENARIO: Key Query Performance (Flat Array Index)") fmt.Println("==================================================") // Value Index Disabled runBenchmark(false) } func runBenchmark(enableValueIndex bool) { // Clean up previous run os.RemoveAll(DataDir) fmt.Printf("Initializing DB Engine in %s (ValueIndex=%v)...\n", DataDir, enableValueIndex) startMem := getMemUsage() e, err := db.NewEngine(DataDir, db.WithValueIndex(enableValueIndex)) if err != nil { panic(err) } defer e.Close() defer os.RemoveAll(DataDir) // Cleanup after run // 1. Bulk Insert 100k keys with varied segments (1 to 5) fmt.Println("\n--- Phase 1: Bulk Insert 100k Keys (Varied Segments) ---") keys := make([]string, TotalKeys) start := time.Now() // Generate keys with 5 patterns // Pattern 1 (Len 1): item. (20k) // Pattern 2 (Len 2): user..profile (20k) // Pattern 3 (Len 3): team..user. (20k) -> 200 users per team // Pattern 4 (Len 4): region..zone..node. (20k) -> 100 zones, 200 nodes // Pattern 5 (Len 5): a.b.c.d. (20k) for i := 0; i < TotalKeys; i++ { group := i % 5 id := i / 5 switch group { case 0: // Len 1: item. // But to test prefix well, let's group them slightly? // Actually "item.1" is 2 segments if split by dot. // User said "one to 5 segments". // Let's treat "." as separator. // "root" is 1 segment. keys[i] = fmt.Sprintf("root%d", id) case 1: // Len 2: user. keys[i] = fmt.Sprintf("user.%d", id) case 2: // Len 3: group..member. // group.0.member.0 ... group.0.member.199 keys[i] = fmt.Sprintf("group.%d.member.%d", id%100, id) case 3: // Len 4: app..ver..config. keys[i] = fmt.Sprintf("app.%d.ver.%d.config.%d", id%10, id%10, id) case 4: // Len 5: log.2023.01.01. keys[i] = fmt.Sprintf("log.2023.01.01.%d", id) } } var wg sync.WaitGroup workers := 10 chunkSize := TotalKeys / workers var insertOps int64 for w := 0; w < workers; w++ { wg.Add(1) go func(id int) { defer wg.Done() base := id * chunkSize for i := 0; i < chunkSize; i++ { idx := base + i if idx >= TotalKeys { continue } // Small value val := "v" if err := e.Set(keys[idx], val, uint64(idx)); err != nil { panic(err) } atomic.AddInt64(&insertOps, 1) } }(w) } wg.Wait() duration := time.Since(start) qps := float64(TotalKeys) / duration.Seconds() currentMem := getMemUsage() printStats("Insert", TotalKeys, duration, qps, getFileSize(DataDir+"/values.data"), currentMem - startMem) // 2. Query Consistency Verification fmt.Println("\n--- Phase 2: Query Consistency Verification ---") verifyQuery(e, "Exact Match (Len 1)", `key = "root0"`, 1) verifyQuery(e, "Exact Match (Len 2)", `key = "user.0"`, 1) verifyQuery(e, "Prefix Scan (Len 3)", `key like "group.0.member.*"`, 200) // 20000 items / 5 types = 4000 items. id%100 -> 100 groups. 4000/100 = 40? Wait. // Total items in group 2: 20,000. // id goes from 0 to 19999. // id%100 goes 0..99. // So each group (0..99) appears 20000/100 = 200 times. // So "group.0.member.*" should have 200 matches. Correct. verifyQuery(e, "Prefix Scan (Len 4)", `key like "app.0.ver.0.config.*"`, 2000) // Group 3 items: 20000. // Key format: app..ver..config. // Condition: id%10 == 0. // Since id goes 0..19999, exactly 1/10 of ids satisfy (id%10 == 0). // Count = 20000 / 10 = 2000. verifyQuery(e, "Prefix Scan (Len 5)", `key like "log.2023.*"`, 20000) // All items in group 4 // 3. Query Performance Test (Prefix) fmt.Println("\n--- Phase 3: Query Performance (Prefix Scan) ---") start = time.Now() qCount := 10000 wg = sync.WaitGroup{} chunkSize = qCount / workers for w := 0; w < workers; w++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < chunkSize; i++ { // Query overlapping prefixes to stress test binary search + scan // "group.50.member.*" -> 200 items scan _, _ = e.Query(`key like "group.50.member.*"`) } }() } wg.Wait() duration = time.Since(start) qps = float64(qCount) / duration.Seconds() printStats("Query(Prefix)", qCount, duration, qps, 0, 0) // Final Memory Report runtime.GC() finalMem := getMemUsage() fmt.Printf("\n[Final Stats] Total Keys: %d | Memory Usage: %.2f MB\n", TotalKeys, float64(finalMem-startMem)/1024/1024) } func verifyQuery(e *db.Engine, name, sql string, expected int) { start := time.Now() res, err := e.Query(sql) if err != nil { fmt.Printf("FAIL: %s - Error: %v\n", name, err) return } duration := time.Since(start) if len(res) == expected { fmt.Printf("PASS: %-25s | Count: %5d | Time: %v\n", name, len(res), duration) } else { fmt.Printf("FAIL: %-25s | Expected: %d, Got: %d\n", name, expected, len(res)) } } func randomString(n int) string { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" b := make([]byte, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } func getFileSize(path string) int64 { fi, err := os.Stat(path) if err != nil { return 0 } return fi.Size() } func getMemUsage() uint64 { var m runtime.MemStats runtime.ReadMemStats(&m) return m.Alloc } func printStats(op string, count int, d time.Duration, qps float64, size int64, memDelta uint64) { sizeStr := "" if size > 0 { sizeStr = fmt.Sprintf(" | Disk: %.2f MB", float64(size)/1024/1024) } memStr := "" if memDelta > 0 { memStr = fmt.Sprintf(" | Mem+: %.2f MB", float64(memDelta)/1024/1024) } fmt.Printf("%-15s: %6d ops in %7v | QPS: %8.0f%s%s\n", op, count, d, qps, sizeStr, memStr) }