benchmark.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. package main
  2. import (
  3. "fmt"
  4. "math/rand"
  5. "os"
  6. "sync"
  7. "sync/atomic"
  8. "time"
  9. "igit.com/xbase/raft/db"
  10. )
  11. const (
  12. TotalKeys = 100000
  13. UpdateCount = 10000
  14. DeleteCount = 10000
  15. QueryCount = 100000
  16. DataDir = "bench_db_data"
  17. ValueBaseSize = 32 // Base size for values
  18. )
  19. func main() {
  20. // Clean up previous run
  21. os.RemoveAll(DataDir)
  22. fmt.Printf("Initializing DB Engine in %s...\n", DataDir)
  23. e, err := db.NewEngine(DataDir)
  24. if err != nil {
  25. panic(err)
  26. }
  27. defer e.Close()
  28. // 1. Bulk Insert 100k keys
  29. fmt.Println("\n--- Phase 1: Bulk Insert 100k Keys ---")
  30. keys := make([]string, TotalKeys)
  31. start := time.Now()
  32. // Pre-generate keys to avoid benchmark overhead
  33. for i := 0; i < TotalKeys; i++ {
  34. keys[i] = fmt.Sprintf("bench.key.%d", i)
  35. }
  36. var wg sync.WaitGroup
  37. workers := 10 // Concurrent workers
  38. chunkSize := TotalKeys / workers
  39. var insertOps int64
  40. for w := 0; w < workers; w++ {
  41. wg.Add(1)
  42. go func(id int) {
  43. defer wg.Done()
  44. base := id * chunkSize
  45. for i := 0; i < chunkSize; i++ {
  46. idx := base + i
  47. // Random value string of approx ValueBaseSize
  48. // Using consistent size initially to test reuse later
  49. val := fmt.Sprintf("value-data-%d-%s", idx, randomString(15))
  50. if err := e.Set(keys[idx], val, uint64(idx)); err != nil {
  51. panic(err)
  52. }
  53. atomic.AddInt64(&insertOps, 1)
  54. }
  55. }(w)
  56. }
  57. wg.Wait()
  58. duration := time.Since(start)
  59. qps := float64(TotalKeys) / duration.Seconds()
  60. printStats("Insert", TotalKeys, duration, qps, getFileSize(DataDir+"/values.data"))
  61. // 2. Update 10k Keys (Mixed Strategy)
  62. // Strategy:
  63. // - 50% Longer: Triggers Append + Mark Old Deleted (Old slots become Free)
  64. // - 50% Shorter: Triggers In-Place Update (No FreeList change)
  65. // But wait, with FreeList, the "Longer" updates will generate Free slots.
  66. // Can we verify if subsequent inserts reuse them?
  67. fmt.Println("\n--- Phase 2: Update 10k Keys (50% Long, 50% Short) ---")
  68. start = time.Now()
  69. updateKeys := keys[:UpdateCount]
  70. wg = sync.WaitGroup{}
  71. chunkSize = UpdateCount / workers
  72. for w := 0; w < workers; w++ {
  73. wg.Add(1)
  74. go func(id int) {
  75. defer wg.Done()
  76. base := id * chunkSize
  77. for i := 0; i < chunkSize; i++ {
  78. idx := base + i
  79. key := updateKeys[idx]
  80. var val string
  81. if idx%2 == 0 {
  82. // Longer value (Trigger Append, Release Old Slot to FreeList)
  83. // Old cap was likely 32 or 48. New cap will be larger.
  84. val = fmt.Sprintf("updated-long-value-%d-%s-padding-padding", idx, randomString(40))
  85. } else {
  86. // Shorter value (Trigger In-Place)
  87. val = "short"
  88. }
  89. if err := e.Set(key, val, uint64(TotalKeys+idx)); err != nil {
  90. panic(err)
  91. }
  92. }
  93. }(w)
  94. }
  95. wg.Wait()
  96. duration = time.Since(start)
  97. qps = float64(UpdateCount) / duration.Seconds()
  98. printStats("Update", UpdateCount, duration, qps, getFileSize(DataDir+"/values.data"))
  99. // 3. Insert New Data to Test Reuse
  100. // We generated ~5000 free slots (from the Long updates) in Phase 2.
  101. // Let's insert 5000 new keys with size matching the old slots (approx 32-48 bytes).
  102. // If reuse works, file size should NOT increase significantly.
  103. fmt.Println("\n--- Phase 3: Insert 5k New Keys (Test Reuse) ---")
  104. reuseKeysCount := 5000
  105. start = time.Now()
  106. for i := 0; i < reuseKeysCount; i++ {
  107. key := fmt.Sprintf("reuse.key.%d", i)
  108. // Length matches initial inserts (approx 30-40 bytes)
  109. val := fmt.Sprintf("new-value-reuse-%d-%s", i, randomString(15))
  110. if err := e.Set(key, val, uint64(TotalKeys+UpdateCount+i)); err != nil {
  111. panic(err)
  112. }
  113. }
  114. duration = time.Since(start)
  115. qps = float64(reuseKeysCount) / duration.Seconds()
  116. printStats("InsertReuse", reuseKeysCount, duration, qps, getFileSize(DataDir+"/values.data"))
  117. // 4. Delete 10k Keys
  118. fmt.Println("\n--- Phase 4: Delete 10k Keys (Simulated via Update) ---")
  119. // Using "DELETED_MARKER" which is short, so it will be In-Place update.
  120. // This won't test FreeList populating, but In-Place logic.
  121. // To test FreeList populating from Delete, we need a real Delete() or Update to smaller size that frees extra space?
  122. // But our storage doesn't split blocks.
  123. // So let's stick to standard benchmark.
  124. deleteKeys := keys[UpdateCount : UpdateCount+DeleteCount]
  125. start = time.Now()
  126. wg = sync.WaitGroup{}
  127. chunkSize = DeleteCount / workers
  128. for w := 0; w < workers; w++ {
  129. wg.Add(1)
  130. go func(id int) {
  131. defer wg.Done()
  132. base := id * chunkSize
  133. for i := 0; i < chunkSize; i++ {
  134. idx := base + i
  135. key := deleteKeys[idx]
  136. if err := e.Set(key, "DELETED_MARKER", uint64(TotalKeys+UpdateCount+reuseKeysCount+idx)); err != nil {
  137. panic(err)
  138. }
  139. }
  140. }(w)
  141. }
  142. wg.Wait()
  143. duration = time.Since(start)
  144. qps = float64(DeleteCount) / duration.Seconds()
  145. printStats("Delete", DeleteCount, duration, qps, getFileSize(DataDir+"/values.data"))
  146. // 5. Verification
  147. fmt.Println("\n--- Phase 5: Verification (Full Scan) ---")
  148. errors := 0
  149. // 1. Check Updated Keys (0-9999)
  150. for i := 0; i < UpdateCount; i++ {
  151. val, ok := e.Get(keys[i])
  152. if !ok {
  153. fmt.Printf("Error: Key %s not found\n", keys[i])
  154. errors++
  155. continue
  156. }
  157. if i%2 == 0 {
  158. // Should be long
  159. if len(val) < 40 { // Our long value is > 40 chars
  160. fmt.Printf("Error: Key %s should be long, got: %s\n", keys[i], val)
  161. errors++
  162. }
  163. } else {
  164. // Should be "short"
  165. if val != "short" {
  166. fmt.Printf("Error: Key %s should be 'short', got: %s\n", keys[i], val)
  167. errors++
  168. }
  169. }
  170. }
  171. // 2. Check Deleted Keys (10000-19999)
  172. for i := UpdateCount; i < UpdateCount+DeleteCount; i++ {
  173. val, ok := e.Get(keys[i])
  174. if !ok {
  175. fmt.Printf("Error: Key %s not found\n", keys[i])
  176. errors++
  177. continue
  178. }
  179. if val != "DELETED_MARKER" {
  180. fmt.Printf("Error: Key %s should be deleted, got: %s\n", keys[i], val)
  181. errors++
  182. }
  183. }
  184. // 3. Check Original Keys (20000-99999)
  185. for i := UpdateCount + DeleteCount; i < TotalKeys; i++ {
  186. val, ok := e.Get(keys[i])
  187. if !ok {
  188. fmt.Printf("Error: Key %s not found\n", keys[i])
  189. errors++
  190. continue
  191. }
  192. // Original values start with "value-data-"
  193. if len(val) < 10 || val[:11] != "value-data-" {
  194. fmt.Printf("Error: Key %s should be original, got: %s\n", keys[i], val)
  195. errors++
  196. }
  197. }
  198. // 4. Check Reused Keys (Extra 5000)
  199. for i := 0; i < 5000; i++ { // reuseKeysCount was hardcoded as 5000 inside main
  200. key := fmt.Sprintf("reuse.key.%d", i)
  201. val, ok := e.Get(key)
  202. if !ok {
  203. fmt.Printf("Error: Reuse Key %s not found\n", key)
  204. errors++
  205. continue
  206. }
  207. if len(val) < 10 || val[:16] != "new-value-reuse-" {
  208. fmt.Printf("Error: Reuse Key %s mismatch, got: %s\n", key, val)
  209. errors++
  210. }
  211. }
  212. if errors == 0 {
  213. fmt.Println("Integrity Check: PASS (All keys verified successfully)")
  214. } else {
  215. fmt.Printf("Integrity Check: FAIL (%d errors found)\n", errors)
  216. }
  217. }
  218. func randomString(n int) string {
  219. const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  220. b := make([]byte, n)
  221. for i := range b {
  222. b[i] = letters[rand.Intn(len(letters))]
  223. }
  224. return string(b)
  225. }
  226. func getFileSize(path string) int64 {
  227. fi, err := os.Stat(path)
  228. if err != nil {
  229. return 0
  230. }
  231. return fi.Size()
  232. }
  233. func printStats(op string, count int, d time.Duration, qps float64, size int64) {
  234. sizeStr := ""
  235. if size > 0 {
  236. sizeStr = fmt.Sprintf(" | DB Size: %.2f MB", float64(size)/1024/1024)
  237. }
  238. fmt.Printf("%s: %d ops in %v | QPS: %.0f%s\n", op, count, d, qps, sizeStr)
  239. }