modernc.org/ql@v1.4.7/performance_test.go (about) 1 package ql_test 2 3 import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "math/rand" 8 "os" 9 "strconv" 10 "sync/atomic" 11 "testing" 12 "time" 13 14 _ "modernc.org/ql/driver" // driver 15 ) 16 17 func TestPerformance(t *testing.T) { 18 if os.Getenv("PERF") == "" { 19 t.Skip("set PERF=1 to run this test") 20 } 21 22 var ( 23 seed = envInt(t, "SEED", 123) // rng seed 24 insertCount = envInt(t, "INSERT_COUNT", 10000) // INSERT overall 25 batchSize = envInt(t, "BATCH_SIZE", 128) // INSERT per transaction 26 userCount = envInt(t, "USER_COUNT", 8) // unique object users 27 idCount = envInt(t, "ID_COUNT", 64) // unique object IDs 28 kindCount = envInt(t, "KIND_COUNT", 4) // unique object kinds 29 ) 30 31 rand.Seed(int64(seed)) 32 33 d, err := newDatabase(t) 34 if err != nil { 35 t.Fatal(err) 36 } 37 38 keys := make([]key, insertCount) 39 for i := range keys { 40 keys[i] = key{ 41 user: fmt.Sprintf("user_%d", rand.Intn(userCount)), 42 id: fmt.Sprintf("id_%d", rand.Intn(idCount)), 43 kind: fmt.Sprintf("kind_%d", rand.Intn(kindCount)), 44 } 45 } 46 47 var ( 48 begin = time.Now() 49 ctx = context.Background() 50 objects = map[key]object{} 51 batch = map[key]object{} 52 sequence = uint64(1) 53 readCount = uint64(0) 54 writeCount = uint64(0) 55 deleteCount = uint64(0) 56 ) 57 58 for _, k := range keys { 59 now := time.Now() 60 61 o := objects[k] 62 { 63 o.sequence = sequence 64 sequence++ 65 66 o.accessCount.Valid = true 67 o.accessCount.Int64 = o.accessCount.Int64 + 1 68 o.lastAccessAt.Valid = true 69 o.lastAccessAt.Time = now 70 71 r := rand.Float64() 72 switch { 73 case r < 0.20: 74 readCount++ 75 o.readCount.Valid = true 76 o.readCount.Int64 = o.readCount.Int64 + 1 77 o.lastReadAt.Valid = true 78 o.lastReadAt.Time = now 79 80 case r < 0.95: 81 writeCount++ 82 o.size.Valid = true 83 o.size.Int64 = 100 + int64(rand.Intn(100)) 84 o.writeCount.Valid = true 85 o.writeCount.Int64 = o.writeCount.Int64 + 1 86 o.lastWriteAt.Valid = true 87 o.lastWriteAt.Time = now 88 89 default: 90 deleteCount++ 91 o.size.Valid = false 92 o.size.Int64 = 0 93 o.deleteCount.Valid = true 94 o.deleteCount.Int64 = o.deleteCount.Int64 + 1 95 o.lastDeleteAt.Valid = true 96 o.lastDeleteAt.Time = now 97 } 98 } 99 objects[k] = o 100 batch[k] = o 101 102 if len(batch) >= batchSize { 103 if err := d.insertBatch(ctx, batch); err != nil { 104 t.Fatal(err) 105 } 106 batch = map[key]object{} 107 } 108 } 109 110 if len(batch) >= 0 { 111 if err := d.insertBatch(ctx, batch); err != nil { 112 t.Fatal(err) 113 } 114 } 115 116 t.Logf("%d INSERT(s) in %s", insertCount, time.Since(begin)) 117 t.Logf("%d reads, %d writes, %d deletes", readCount, writeCount, deleteCount) 118 t.Logf("final object count %d, row count %d", len(objects), d.selectCount(ctx)) 119 } 120 121 // 122 // 123 // 124 125 type key struct { 126 user string 127 id string 128 kind string 129 } 130 131 type object struct { 132 sequence uint64 133 size sql.NullInt64 134 accessCount sql.NullInt64 135 lastAccessAt sql.NullTime 136 readCount sql.NullInt64 137 lastReadAt sql.NullTime 138 writeCount sql.NullInt64 139 lastWriteAt sql.NullTime 140 deleteCount sql.NullInt64 141 lastDeleteAt sql.NullTime 142 } 143 144 // 145 // 146 // 147 148 type database struct { 149 t *testing.T 150 db *sql.DB 151 ins *sql.Stmt 152 upd *sql.Stmt 153 sel *sql.Stmt 154 } 155 156 func newDatabase(t *testing.T) (_ *database, err error) { 157 db, err := sql.Open("ql-mem", randomDSN()) 158 if err != nil { 159 return nil, fmt.Errorf("during Open: %w", err) 160 } 161 162 defer func() { 163 if err != nil { 164 db.Close() 165 } 166 }() 167 168 tx, err := db.Begin() 169 if err != nil { 170 return nil, fmt.Errorf("during Begin: %w", err) 171 } 172 173 if _, err := tx.Exec(` 174 CREATE TABLE objects ( 175 user string NOT NULL, 176 id string NOT NULL, 177 kind string NOT NULL, 178 insert_at time NOT NULL DEFAULT timeIn(now(), "UTC"), 179 operations int64 NOT NULL DEFAULT 0, 180 sequence int64 NOT NULL DEFAULT 0, 181 size int64, 182 access_count int64, 183 last_access_at time, 184 read_count int64, 185 last_read_at time, 186 write_count int64, 187 last_write_at time, 188 delete_count int64, 189 last_delete_at time, 190 ); 191 `); err != nil { 192 return nil, fmt.Errorf("during CREATE TABLE: %w", err) 193 } 194 195 for _, s := range []string{ 196 "CREATE UNIQUE INDEX index_key ON objects (user, id, kind);", 197 "CREATE INDEX index_access ON objects (last_access_at);", 198 "CREATE INDEX index_size ON objects (size);", 199 "CREATE INDEX index_write ON objects (last_write_at);", 200 "CREATE INDEX index_delete ON objects (last_delete_at);", 201 } { 202 if _, err := tx.Exec(s); err != nil { 203 return nil, fmt.Errorf("during CREATE INDEX: %w", err) 204 } 205 } 206 207 if err := tx.Commit(); err != nil { 208 return nil, fmt.Errorf("during Commit: %w", err) 209 } 210 211 ins, err := db.Prepare(` 212 INSERT INTO objects 213 IF NOT EXISTS 214 (user, id, kind) 215 VALUES 216 ($1, $2, $3); 217 `) 218 if err != nil { 219 return nil, fmt.Errorf("during PREPARE for INSERT: %w", err) 220 } 221 222 upd, err := db.Prepare(` 223 UPDATE objects 224 SET 225 operations = 1 + operations, 226 sequence = $1, 227 size = $2, 228 access_count = $3, 229 last_access_at= $4, 230 read_count = $5, 231 last_read_at = $6, 232 write_count = $7, 233 last_write_at = $8, 234 delete_count = $9, 235 last_delete_at= $10, 236 WHERE 237 user == $11 238 AND id == $12 239 AND kind == $13 240 AND $1 > sequence; 241 `) 242 if err != nil { 243 return nil, fmt.Errorf("during PREPARE for UPDATE: %w", err) 244 } 245 246 sel, err := db.Prepare(` 247 SELECT 248 user, 249 id, 250 kind, 251 insert_at, 252 operations, 253 sequence, 254 size, 255 access_count, 256 last_access_at, 257 read_count, 258 last_read_at, 259 write_count, 260 last_write_at, 261 delete_count, 262 last_delete_at, 263 FROM objects 264 WHERE user == $1 AND id == $2 AND kind == $3; 265 `) 266 if err != nil { 267 return nil, fmt.Errorf("during PREPARE for SELECT: %w", err) 268 } 269 270 return &database{ 271 t: t, 272 db: db, 273 ins: ins, 274 upd: upd, 275 sel: sel, 276 }, nil 277 } 278 279 func (d *database) selectCount(ctx context.Context) int { 280 var n int 281 d.db.QueryRowContext(ctx, `SELECT count() FROM objects;`).Scan(&n) 282 return n 283 } 284 285 func (d *database) insertBatch(ctx context.Context, batch map[key]object) (err error) { 286 if len(batch) <= 0 { 287 return nil 288 } 289 290 defer func(begin time.Time) { 291 var ( 292 rowCount = d.selectCount(ctx) 293 perBatch = perUnit(time.Since(begin), 1).Truncate(time.Microsecond) 294 perRecord = perUnit(perBatch, len(batch)).Truncate(time.Microsecond) 295 perRecordRow = perUnit(perRecord, rowCount) 296 ) 297 d.t.Helper() 298 d.t.Logf( 299 "batch size %4d · row count %5d · per batch %10s · per record %10s · per record-row %10s · error %v", 300 len(batch), rowCount, perBatch, perRecord, perRecordRow, err, 301 ) 302 }(time.Now()) 303 304 tx, err := d.db.BeginTx(ctx, &sql.TxOptions{}) 305 if err != nil { 306 return err 307 } 308 309 defer func() { 310 if err == nil { 311 err = tx.Commit() 312 } else { 313 err = fmt.Errorf("%w (rollback err=%v)", err, tx.Rollback()) 314 } 315 }() 316 317 var ( 318 ins = tx.Stmt(d.ins) 319 upd = tx.Stmt(d.upd) 320 ) 321 for k, o := range batch { 322 if _, err := ins.Exec( 323 k.user, 324 k.id, 325 k.kind, 326 ); err != nil { 327 return fmt.Errorf("INSERT: %w", err) 328 } 329 if _, err := upd.Exec( 330 o.sequence, // $1 331 o.size, // $2 332 o.accessCount, // $3 333 o.lastAccessAt, // $4 334 o.readCount, // $5 335 o.lastReadAt, // $6 336 o.writeCount, // $7 337 o.lastWriteAt, // $8 338 o.deleteCount, // $9 339 o.lastDeleteAt, // $10 340 k.user, // $11 341 k.id, // $12 342 k.kind, // $13 343 ); err != nil { 344 return fmt.Errorf("UPDATE: %w", err) 345 } 346 } 347 348 return nil 349 } 350 351 // 352 // 353 // 354 355 var unique uint64 356 357 func randomDSN() string { 358 cnt := atomic.AddUint64(&unique, 1) 359 rng := rand.New(rand.NewSource(time.Now().UnixNano())) 360 buf := make([]byte, 32) 361 n, _ := rng.Read(buf) 362 return fmt.Sprintf("memory://%d-%x", cnt, buf[:n]) 363 } 364 365 func perUnit(d time.Duration, n int) time.Duration { 366 if n == 0 { 367 n = 1 368 } 369 return d / time.Duration(n) 370 } 371 372 func envInt(t *testing.T, key string, def int) int { 373 t.Helper() 374 i, err := strconv.Atoi(os.Getenv(key)) 375 if err != nil { 376 i = def 377 } 378 t.Logf("%s=%d", key, i) 379 return i 380 }