Files
elowdb-go/examples/perf/main.go
2026-04-07 15:04:38 +06:00

329 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Пример комплексного performance-теста LineDb.
// Запуск:
//
// go run ./examples/perf/main.go
//
// Сценарий:
// 1. Вставка 5000 записей, каждая ~1800 символов.
// 2. N выборок ReadByFilter по индексируемому полю, замер среднего времени.
// 3. 100 случайных удалений, замер среднего времени.
// 4. Замеры памяти процесса через runtime.MemStats.
package main
import (
"fmt"
"log"
"math/rand"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"direct-dev.ru/gitea/GiteaAdmin/elowdb-go"
)
const (
recordsCount = 3000
payloadSize = 1024
readOps = 2000
updateOps = 500
deleteOps = 500
collectionName = "perf_items"
baseDBDir = "./data/perf-benchmark"
allocSizeEstimate = 2048
)
func main() {
rand.Seed(time.Now().UnixNano())
if err := os.MkdirAll(baseDBDir, 0755); err != nil {
log.Fatalf("mkdir: %v", err)
}
fmt.Printf("LineDB perf сравнение: без индекса vs с индексом\n")
fmt.Printf("records=%d payload~%d readOps=%d updateOps=%d deleteOps=%d allocSize=%d\n\n",
recordsCount, payloadSize, readOps, updateOps, deleteOps, allocSizeEstimate)
noIdx := runScenario(false)
runtime.GC()
withIdx := runScenario(true)
fmt.Printf("\n=== Сводка (рядом) ===\n")
fmt.Printf("%-26s | %-18s | %-18s\n", "Метрика", "Без индекса", "С индексом")
fmt.Printf("%-26s | %-18v | %-18v\n", "Insert total", noIdx.insertTotal, withIdx.insertTotal)
fmt.Printf("%-26s | %-18v | %-18v\n", "Index build", noIdx.indexBuildTotal, withIdx.indexBuildTotal)
fmt.Printf("%-26s | %-18v | %-18v\n", "ReadByFilter avg", noIdx.readAvg, withIdx.readAvg)
fmt.Printf("%-26s | %-18v | %-18v\n", "Update avg", noIdx.updateAvg, withIdx.updateAvg)
fmt.Printf("%-26s | %-18v | %-18v\n", "Delete avg", noIdx.deleteAvg, withIdx.deleteAvg)
fmt.Printf("%-26s | %-18d | %-18d\n", "Final records", noIdx.finalCount, withIdx.finalCount)
fmt.Printf("%-26s | %-18.2f | %-18.2f\n", "Mem Alloc (MB)", noIdx.memAllocMB, withIdx.memAllocMB)
fmt.Printf("%-26s | %-18.2f | %-18.2f\n", "Mem Sys (MB)", noIdx.memSysMB, withIdx.memSysMB)
fmt.Printf("%-26s | %-18d | %-18d\n", "NumGC", noIdx.numGC, withIdx.numGC)
fmt.Printf("\nData directories:\n no-index: %s\n with-index:%s\n", noIdx.dbDir, withIdx.dbDir)
}
type scenarioResult struct {
dbDir string
insertTotal time.Duration
indexBuildTotal time.Duration
readAvg time.Duration
updateAvg time.Duration
deleteAvg time.Duration
finalCount int
memAllocMB float64
memSysMB float64
numGC uint32
}
func runScenario(useIndex bool) scenarioResult {
label := "no-index"
if useIndex {
label = "with-index"
}
dbDir := filepath.Join(baseDBDir, label)
_ = os.RemoveAll(dbDir)
var store linedb.IndexStore
lineOpts := linedb.LineDbOptions{}
collOpts := linedb.JSONLFileOptions{
CollectionName: collectionName,
AllocSize: allocSizeEstimate,
}
if useIndex {
// Индексируем поля id и index (по ним идут фильтры в тесте)
collOpts.IndexedFields = []string{"id", "index"}
store = linedb.NewInMemoryIndexStore()
lineOpts.IndexStore = store
}
initOptions := &linedb.LineDbInitOptions{
CacheSize: 1000,
CacheTTL: time.Minute,
DBFolder: dbDir,
Collections: []linedb.JSONLFileOptions{
collOpts,
},
}
db := linedb.NewLineDb(&lineOpts)
if err := db.Init(true, initOptions); err != nil {
log.Fatalf("[%s] Init failed: %v", label, err)
}
defer db.Close()
fmt.Printf("=== %s ===\n", label)
printMem("Before insert")
fmt.Printf("1) Insert %d records...\n", recordsCount)
start := time.Now()
if err := insertRecords(db); err != nil {
log.Fatalf("[%s] insertRecords failed: %v", label, err)
}
insertDur := time.Since(start)
fmt.Printf(" Total insert time: %v, per record: %v\n", insertDur, insertDur/time.Duration(recordsCount))
// Индекс строится внутри Write при DoIndexing: true (точечная индексация)
printMem("After insert")
all, err := db.Read(collectionName, linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatalf("[%s] Read after insert failed: %v", label, err)
}
fmt.Printf(" Records in collection: %d\n", len(all))
ids := collectIDs(all)
indexVals := collectFieldValues(all, "index")
if len(ids) == 0 || len(indexVals) == 0 {
log.Fatalf("[%s] No IDs or index values collected, cannot continue", label)
}
fmt.Printf("\n2) Random ReadByFilter of %d ops (field=index)...\n", readOps)
readAvg := benchmarkReads(db, indexVals, readOps)
fmt.Printf(" Average ReadByFilter time: %v\n", readAvg)
fmt.Printf("\n3) Random update of %d records...\n", updateOps)
avgUpdate := benchmarkUpdates(db, ids, updateOps)
fmt.Printf(" Average update time: %v\n", avgUpdate)
printMem("After updates")
fmt.Printf("\n4) Random delete of %d records...\n", deleteOps)
avgDelete := benchmarkDeletes(db, ids, deleteOps)
fmt.Printf(" Average delete time: %v\n", avgDelete)
printMem("After deletes")
final, err := db.Read(collectionName, linedb.LineDbAdapterOptions{})
if err != nil {
log.Fatalf("[%s] Final read failed: %v", label, err)
}
fmt.Printf("\nFinal records in collection: %d\n", len(final))
mem := memSnapshot()
return scenarioResult{
dbDir: dbDir,
insertTotal: insertDur,
indexBuildTotal: 0, // индекс строится точечно при Write с DoIndexing
readAvg: readAvg,
updateAvg: avgUpdate,
deleteAvg: avgDelete,
finalCount: len(final),
memAllocMB: mem.allocMB,
memSysMB: mem.sysMB,
numGC: mem.numGC,
}
}
func insertRecords(db *linedb.LineDb) error {
// Сгенерируем базовый payload
base := strings.Repeat("X", payloadSize)
batch := make([]any, 0, 100)
for i := 0; i < recordsCount; i++ {
rec := map[string]any{
"id": i + 1,
"index": i,
"payload": base,
"created": time.Now().Format(time.RFC3339Nano),
}
batch = append(batch, rec)
if len(batch) >= cap(batch) {
if err := db.Write(batch, collectionName, linedb.LineDbAdapterOptions{DoIndexing: true}); err != nil {
return err
}
batch = batch[:0]
}
}
if len(batch) > 0 {
if err := db.Write(batch, collectionName, linedb.LineDbAdapterOptions{DoIndexing: true}); err != nil {
return err
}
}
return nil
}
func collectIDs(all []any) []any {
ids := make([]any, 0, len(all))
for _, r := range all {
if m, ok := r.(map[string]any); ok {
if id, ok := m["id"]; ok {
ids = append(ids, id)
}
}
}
return ids
}
func collectFieldValues(all []any, field string) []any {
vals := make([]any, 0, len(all))
for _, r := range all {
if m, ok := r.(map[string]any); ok {
if v, ok := m[field]; ok {
vals = append(vals, v)
}
}
}
return vals
}
func benchmarkReads(db *linedb.LineDb, values []any, ops int) time.Duration {
if len(values) == 0 || ops == 0 {
return 0
}
var total time.Duration
for i := 0; i < ops; i++ {
v := values[rand.Intn(len(values))]
start := time.Now()
_, err := db.ReadByFilter(map[string]any{"index": v}, collectionName, linedb.LineDbAdapterOptions{})
dur := time.Since(start)
if err != nil {
log.Printf("ReadByFilter error (index=%v): %v", v, err)
continue
}
total += dur
}
return total / time.Duration(ops)
}
func benchmarkUpdates(db *linedb.LineDb, ids []any, ops int) time.Duration {
if len(ids) == 0 {
return 0
}
var total time.Duration
for i := 0; i < ops; i++ {
id := ids[rand.Intn(len(ids))]
update := map[string]any{
"updatedAt": time.Now().Format(time.RFC3339Nano),
}
start := time.Now()
_, err := db.Update(update, collectionName, map[string]any{"id": id}, linedb.LineDbAdapterOptions{})
dur := time.Since(start)
if err != nil {
log.Printf("Update error (id=%v): %v", id, err)
continue
}
total += dur
}
if ops == 0 {
return 0
}
return total / time.Duration(ops)
}
func benchmarkDeletes(db *linedb.LineDb, ids []any, ops int) time.Duration {
if len(ids) == 0 {
return 0
}
var total time.Duration
used := make(map[int]struct{})
for i := 0; i < ops; i++ {
// чтобы не удалять по одному и тому же id постоянно, постараемся брать разные индексы
var idx int
for tries := 0; tries < 10; tries++ {
idx = rand.Intn(len(ids))
if _, ok := used[idx]; !ok {
break
}
}
used[idx] = struct{}{}
id := ids[idx]
start := time.Now()
_, err := db.Delete(map[string]any{"id": id}, collectionName, linedb.LineDbAdapterOptions{})
dur := time.Since(start)
if err != nil {
log.Printf("Delete error (id=%v): %v", id, err)
continue
}
total += dur
}
if ops == 0 {
return 0
}
return total / time.Duration(ops)
}
func printMem(label string) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("\n=== %s ===\n", label)
fmt.Printf(" Alloc: %.2f MB\n", float64(m.Alloc)/1024.0/1024.0)
fmt.Printf(" TotalAlloc: %.2f MB\n", float64(m.TotalAlloc)/1024.0/1024.0)
fmt.Printf(" Sys: %.2f MB\n", float64(m.Sys)/1024.0/1024.0)
fmt.Printf(" NumGC: %d\n", m.NumGC)
}
type memSnap struct {
allocMB float64
sysMB float64
numGC uint32
}
func memSnapshot() memSnap {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return memSnap{
allocMB: float64(m.Alloc) / 1024.0 / 1024.0,
sysMB: float64(m.Sys) / 1024.0 / 1024.0,
numGC: m.NumGC,
}
}