github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/internal/manifest/l0_sublevels_test.go (about) 1 // Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 // of this source code is governed by a BSD-style license that can be found in 3 // the LICENSE file. 4 5 package manifest 6 7 import ( 8 "bytes" 9 "fmt" 10 "io" 11 "math" 12 "os" 13 "sort" 14 "strconv" 15 "strings" 16 "testing" 17 "time" 18 19 "github.com/cockroachdb/datadriven" 20 "github.com/cockroachdb/pebble/internal/base" 21 "github.com/cockroachdb/pebble/internal/testkeys" 22 "github.com/cockroachdb/pebble/record" 23 "github.com/stretchr/testify/require" 24 "golang.org/x/exp/rand" 25 ) 26 27 func readManifest(filename string) (*Version, error) { 28 f, err := os.Open(filename) 29 if err != nil { 30 return nil, err 31 } 32 defer f.Close() 33 rr := record.NewReader(f, 0 /* logNum */) 34 var v *Version 35 addedByFileNum := make(map[base.FileNum]*FileMetadata) 36 for { 37 r, err := rr.Next() 38 if err == io.EOF { 39 break 40 } 41 if err != nil { 42 return nil, err 43 } 44 var ve VersionEdit 45 if err = ve.Decode(r); err != nil { 46 return nil, err 47 } 48 var bve BulkVersionEdit 49 bve.AddedByFileNum = addedByFileNum 50 if err := bve.Accumulate(&ve); err != nil { 51 return nil, err 52 } 53 if v, err = bve.Apply(v, base.DefaultComparer.Compare, base.DefaultFormatter, 10<<20, 32000, nil, ProhibitSplitUserKeys); err != nil { 54 return nil, err 55 } 56 } 57 return v, nil 58 } 59 60 func visualizeSublevels( 61 s *L0Sublevels, compactionFiles bitSet, otherLevels [][]*FileMetadata, 62 ) string { 63 var buf strings.Builder 64 if compactionFiles == nil { 65 compactionFiles = newBitSet(s.levelMetadata.Len()) 66 } 67 largestChar := byte('a') 68 printLevel := func(files []*FileMetadata, level string, isL0 bool) { 69 lastChar := byte('a') 70 fmt.Fprintf(&buf, "L%s:", level) 71 for i := 0; i < 5-len(level); i++ { 72 buf.WriteByte(' ') 73 } 74 for j, f := range files { 75 for lastChar < f.Smallest.UserKey[0] { 76 buf.WriteString(" ") 77 lastChar++ 78 } 79 buf.WriteByte(f.Smallest.UserKey[0]) 80 middleChar := byte('-') 81 if isL0 { 82 if compactionFiles[f.L0Index] { 83 middleChar = '+' 84 } else if f.IsCompacting() { 85 if f.IsIntraL0Compacting { 86 middleChar = '^' 87 } else { 88 middleChar = 'v' 89 } 90 } 91 } else if f.IsCompacting() { 92 middleChar = '=' 93 } 94 if largestChar < f.Largest.UserKey[0] { 95 largestChar = f.Largest.UserKey[0] 96 } 97 if f.Smallest.UserKey[0] == f.Largest.UserKey[0] { 98 buf.WriteByte(f.Largest.UserKey[0]) 99 if compactionFiles[f.L0Index] { 100 buf.WriteByte('+') 101 } else if j < len(files)-1 { 102 buf.WriteByte(' ') 103 } 104 lastChar++ 105 continue 106 } 107 buf.WriteByte(middleChar) 108 buf.WriteByte(middleChar) 109 lastChar++ 110 for lastChar < f.Largest.UserKey[0] { 111 buf.WriteByte(middleChar) 112 buf.WriteByte(middleChar) 113 buf.WriteByte(middleChar) 114 lastChar++ 115 } 116 if f.Largest.IsExclusiveSentinel() && 117 j < len(files)-1 && files[j+1].Smallest.UserKey[0] == f.Largest.UserKey[0] { 118 // This case happens where two successive files have 119 // matching end/start user keys but where the left-side file 120 // has the sentinel key as its end key trailer. In this case 121 // we print the sstables as: 122 // 123 // a------d------g 124 // 125 continue 126 } 127 buf.WriteByte(middleChar) 128 buf.WriteByte(f.Largest.UserKey[0]) 129 if j < len(files)-1 { 130 buf.WriteByte(' ') 131 } 132 lastChar++ 133 } 134 fmt.Fprintf(&buf, "\n") 135 } 136 for i := len(s.levelFiles) - 1; i >= 0; i-- { 137 printLevel(s.levelFiles[i], fmt.Sprintf("0.%d", i), true) 138 } 139 for i := range otherLevels { 140 if len(otherLevels[i]) == 0 { 141 continue 142 } 143 printLevel(otherLevels[i], strconv.Itoa(i+1), false) 144 } 145 buf.WriteString(" ") 146 for b := byte('a'); b <= largestChar; b++ { 147 buf.WriteByte(b) 148 buf.WriteByte(b) 149 if b < largestChar { 150 buf.WriteByte(' ') 151 } 152 } 153 buf.WriteByte('\n') 154 return buf.String() 155 } 156 157 func TestL0Sublevels(t *testing.T) { 158 parseMeta := func(s string) (*FileMetadata, error) { 159 parts := strings.Split(s, ":") 160 if len(parts) != 2 { 161 t.Fatalf("malformed table spec: %s", s) 162 } 163 fileNum, err := strconv.Atoi(strings.TrimSpace(parts[0])) 164 if err != nil { 165 return nil, err 166 } 167 fields := strings.Fields(parts[1]) 168 keyRange := strings.Split(strings.TrimSpace(fields[0]), "-") 169 m := (&FileMetadata{}).ExtendPointKeyBounds( 170 base.DefaultComparer.Compare, 171 base.ParseInternalKey(strings.TrimSpace(keyRange[0])), 172 base.ParseInternalKey(strings.TrimSpace(keyRange[1])), 173 ) 174 m.SmallestSeqNum = m.Smallest.SeqNum() 175 m.LargestSeqNum = m.Largest.SeqNum() 176 if m.Largest.IsExclusiveSentinel() { 177 m.LargestSeqNum = m.SmallestSeqNum 178 } 179 m.FileNum = base.FileNum(fileNum) 180 m.Size = uint64(256) 181 m.InitPhysicalBacking() 182 if len(fields) > 1 { 183 for _, field := range fields[1:] { 184 parts := strings.Split(field, "=") 185 switch parts[0] { 186 case "base_compacting": 187 m.IsIntraL0Compacting = false 188 m.CompactionState = CompactionStateCompacting 189 case "intra_l0_compacting": 190 m.IsIntraL0Compacting = true 191 m.CompactionState = CompactionStateCompacting 192 case "compacting": 193 m.CompactionState = CompactionStateCompacting 194 case "size": 195 sizeInt, err := strconv.Atoi(parts[1]) 196 if err != nil { 197 return nil, err 198 } 199 m.Size = uint64(sizeInt) 200 } 201 } 202 } 203 204 return m, nil 205 } 206 207 var err error 208 var fileMetas [NumLevels][]*FileMetadata 209 var explicitSublevels [][]*FileMetadata 210 var activeCompactions []L0Compaction 211 var sublevels *L0Sublevels 212 baseLevel := NumLevels - 1 213 214 datadriven.RunTest(t, "testdata/l0_sublevels", func(t *testing.T, td *datadriven.TestData) string { 215 pickBaseCompaction := false 216 level := 0 217 addL0FilesOpt := false 218 switch td.Cmd { 219 case "add-l0-files": 220 addL0FilesOpt = true 221 level = 0 222 fallthrough 223 case "define": 224 if !addL0FilesOpt { 225 fileMetas = [NumLevels][]*FileMetadata{} 226 baseLevel = NumLevels - 1 227 activeCompactions = nil 228 } 229 explicitSublevels = [][]*FileMetadata{} 230 sublevel := -1 231 addedL0Files := make([]*FileMetadata, 0) 232 for _, data := range strings.Split(td.Input, "\n") { 233 data = strings.TrimSpace(data) 234 switch data[:2] { 235 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 236 level, err = strconv.Atoi(data[1:2]) 237 if err != nil { 238 return err.Error() 239 } 240 if level == 0 && len(data) > 3 { 241 // Sublevel was specified. 242 sublevel, err = strconv.Atoi(data[3:]) 243 if err != nil { 244 return err.Error() 245 } 246 } else { 247 sublevel = -1 248 } 249 default: 250 meta, err := parseMeta(data) 251 if err != nil { 252 return err.Error() 253 } 254 if level != 0 && level < baseLevel { 255 baseLevel = level 256 } 257 fileMetas[level] = append(fileMetas[level], meta) 258 if level == 0 { 259 addedL0Files = append(addedL0Files, meta) 260 } 261 if sublevel != -1 { 262 for len(explicitSublevels) <= sublevel { 263 explicitSublevels = append(explicitSublevels, []*FileMetadata{}) 264 } 265 explicitSublevels[sublevel] = append(explicitSublevels[sublevel], meta) 266 } 267 } 268 } 269 270 flushSplitMaxBytes := 64 271 initialize := true 272 for _, arg := range td.CmdArgs { 273 switch arg.Key { 274 case "flush_split_max_bytes": 275 flushSplitMaxBytes, err = strconv.Atoi(arg.Vals[0]) 276 if err != nil { 277 t.Fatal(err) 278 } 279 case "no_initialize": 280 // This case is for use with explicitly-specified sublevels 281 // only. 282 initialize = false 283 } 284 } 285 SortBySeqNum(fileMetas[0]) 286 for i := 1; i < NumLevels; i++ { 287 SortBySmallest(fileMetas[i], base.DefaultComparer.Compare) 288 } 289 290 levelMetadata := makeLevelMetadata(base.DefaultComparer.Compare, 0, fileMetas[0]) 291 if initialize { 292 if addL0FilesOpt { 293 SortBySeqNum(addedL0Files) 294 sublevels, err = sublevels.AddL0Files(addedL0Files, int64(flushSplitMaxBytes), &levelMetadata) 295 // Check if the output matches a full initialization. 296 sublevels2, _ := NewL0Sublevels(&levelMetadata, base.DefaultComparer.Compare, base.DefaultFormatter, int64(flushSplitMaxBytes)) 297 if sublevels != nil && sublevels2 != nil { 298 require.Equal(t, sublevels.flushSplitUserKeys, sublevels2.flushSplitUserKeys) 299 require.Equal(t, sublevels.levelFiles, sublevels2.levelFiles) 300 } 301 } else { 302 sublevels, err = NewL0Sublevels( 303 &levelMetadata, 304 base.DefaultComparer.Compare, 305 base.DefaultFormatter, 306 int64(flushSplitMaxBytes)) 307 } 308 if err != nil { 309 return err.Error() 310 } 311 sublevels.InitCompactingFileInfo(nil) 312 } else { 313 // This case is for use with explicitly-specified sublevels 314 // only. 315 sublevels = &L0Sublevels{ 316 levelFiles: explicitSublevels, 317 cmp: base.DefaultComparer.Compare, 318 formatKey: base.DefaultFormatter, 319 levelMetadata: &levelMetadata, 320 } 321 for _, files := range explicitSublevels { 322 sublevels.Levels = append(sublevels.Levels, NewLevelSliceSpecificOrder(files)) 323 } 324 } 325 326 if err != nil { 327 t.Fatal(err) 328 } 329 330 var builder strings.Builder 331 builder.WriteString(sublevels.describe(true)) 332 builder.WriteString(visualizeSublevels(sublevels, nil, fileMetas[1:])) 333 return builder.String() 334 case "pick-base-compaction": 335 pickBaseCompaction = true 336 fallthrough 337 case "pick-intra-l0-compaction": 338 minCompactionDepth := 3 339 earliestUnflushedSeqNum := uint64(math.MaxUint64) 340 for _, arg := range td.CmdArgs { 341 switch arg.Key { 342 case "min_depth": 343 minCompactionDepth, err = strconv.Atoi(arg.Vals[0]) 344 if err != nil { 345 t.Fatal(err) 346 } 347 case "earliest_unflushed_seqnum": 348 eusnInt, err := strconv.Atoi(arg.Vals[0]) 349 if err != nil { 350 t.Fatal(err) 351 } 352 earliestUnflushedSeqNum = uint64(eusnInt) 353 } 354 } 355 356 var lcf *L0CompactionFiles 357 if pickBaseCompaction { 358 baseFiles := NewLevelSliceKeySorted(base.DefaultComparer.Compare, fileMetas[baseLevel]) 359 lcf, err = sublevels.PickBaseCompaction(minCompactionDepth, baseFiles) 360 if err == nil && lcf != nil { 361 // Try to extend the base compaction into a more rectangular 362 // shape, using the smallest/largest keys of the files before 363 // and after overlapping base files. This mimics the logic 364 // the compactor is expected to implement. 365 baseFiles := fileMetas[baseLevel] 366 firstFile := sort.Search(len(baseFiles), func(i int) bool { 367 return sublevels.cmp(baseFiles[i].Largest.UserKey, sublevels.orderedIntervals[lcf.minIntervalIndex].startKey.key) >= 0 368 }) 369 lastFile := sort.Search(len(baseFiles), func(i int) bool { 370 return sublevels.cmp(baseFiles[i].Smallest.UserKey, sublevels.orderedIntervals[lcf.maxIntervalIndex+1].startKey.key) >= 0 371 }) 372 startKey := base.InvalidInternalKey 373 endKey := base.InvalidInternalKey 374 if firstFile > 0 { 375 startKey = baseFiles[firstFile-1].Largest 376 } 377 if lastFile < len(baseFiles) { 378 endKey = baseFiles[lastFile].Smallest 379 } 380 sublevels.ExtendL0ForBaseCompactionTo( 381 startKey, 382 endKey, 383 lcf) 384 } 385 } else { 386 lcf, err = sublevels.PickIntraL0Compaction(earliestUnflushedSeqNum, minCompactionDepth) 387 } 388 if err != nil { 389 return fmt.Sprintf("error: %s", err.Error()) 390 } 391 if lcf == nil { 392 return "no compaction picked" 393 } 394 var builder strings.Builder 395 builder.WriteString(fmt.Sprintf("compaction picked with stack depth reduction %d\n", lcf.seedIntervalStackDepthReduction)) 396 for i, file := range lcf.Files { 397 builder.WriteString(file.FileNum.String()) 398 if i < len(lcf.Files)-1 { 399 builder.WriteByte(',') 400 } 401 } 402 startKey := sublevels.orderedIntervals[lcf.seedInterval].startKey 403 endKey := sublevels.orderedIntervals[lcf.seedInterval+1].startKey 404 builder.WriteString(fmt.Sprintf("\nseed interval: %s-%s\n", startKey.key, endKey.key)) 405 builder.WriteString(visualizeSublevels(sublevels, lcf.FilesIncluded, fileMetas[1:])) 406 407 return builder.String() 408 case "read-amp": 409 return strconv.Itoa(sublevels.ReadAmplification()) 410 case "in-use-key-ranges": 411 var buf bytes.Buffer 412 for _, data := range strings.Split(strings.TrimSpace(td.Input), "\n") { 413 keyRange := strings.Split(strings.TrimSpace(data), "-") 414 smallest := []byte(strings.TrimSpace(keyRange[0])) 415 largest := []byte(strings.TrimSpace(keyRange[1])) 416 417 keyRanges := sublevels.InUseKeyRanges(smallest, largest) 418 for i, r := range keyRanges { 419 fmt.Fprintf(&buf, "%s-%s", sublevels.formatKey(r.Start), sublevels.formatKey(r.End)) 420 if i < len(keyRanges)-1 { 421 fmt.Fprint(&buf, ", ") 422 } 423 } 424 if len(keyRanges) == 0 { 425 fmt.Fprint(&buf, ".") 426 } 427 fmt.Fprintln(&buf) 428 } 429 return buf.String() 430 case "flush-split-keys": 431 var builder strings.Builder 432 builder.WriteString("flush user split keys: ") 433 flushSplitKeys := sublevels.FlushSplitKeys() 434 for i, key := range flushSplitKeys { 435 builder.Write(key) 436 if i < len(flushSplitKeys)-1 { 437 builder.WriteString(", ") 438 } 439 } 440 if len(flushSplitKeys) == 0 { 441 builder.WriteString("none") 442 } 443 return builder.String() 444 case "max-depth-after-ongoing-compactions": 445 return strconv.Itoa(sublevels.MaxDepthAfterOngoingCompactions()) 446 case "l0-check-ordering": 447 for sublevel, files := range sublevels.levelFiles { 448 slice := NewLevelSliceSpecificOrder(files) 449 err := CheckOrdering(base.DefaultComparer.Compare, base.DefaultFormatter, 450 L0Sublevel(sublevel), slice.Iter(), ProhibitSplitUserKeys) 451 if err != nil { 452 return err.Error() 453 } 454 } 455 return "OK" 456 case "update-state-for-compaction": 457 var fileNums []base.FileNum 458 for _, arg := range td.CmdArgs { 459 switch arg.Key { 460 case "files": 461 for _, val := range arg.Vals { 462 fileNum, err := strconv.ParseUint(val, 10, 64) 463 if err != nil { 464 return err.Error() 465 } 466 fileNums = append(fileNums, base.FileNum(fileNum)) 467 } 468 } 469 } 470 files := make([]*FileMetadata, 0, len(fileNums)) 471 for _, num := range fileNums { 472 for _, f := range fileMetas[0] { 473 if f.FileNum == num { 474 f.CompactionState = CompactionStateCompacting 475 files = append(files, f) 476 break 477 } 478 } 479 } 480 slice := NewLevelSliceSeqSorted(files) 481 sm, la := KeyRange(base.DefaultComparer.Compare, slice.Iter()) 482 activeCompactions = append(activeCompactions, L0Compaction{Smallest: sm, Largest: la}) 483 if err := sublevels.UpdateStateForStartedCompaction([]LevelSlice{slice}, true); err != nil { 484 return err.Error() 485 } 486 return "OK" 487 case "describe": 488 var builder strings.Builder 489 builder.WriteString(sublevels.describe(true)) 490 builder.WriteString(visualizeSublevels(sublevels, nil, fileMetas[1:])) 491 return builder.String() 492 } 493 return fmt.Sprintf("unrecognized command: %s", td.Cmd) 494 }) 495 } 496 497 func TestAddL0FilesEquivalence(t *testing.T) { 498 seed := uint64(time.Now().UnixNano()) 499 rng := rand.New(rand.NewSource(seed)) 500 t.Logf("seed: %d", seed) 501 502 var inUseKeys [][]byte 503 const keyReusePct = 0.15 504 var fileMetas []*FileMetadata 505 var s, s2 *L0Sublevels 506 keySpace := testkeys.Alpha(8) 507 508 flushSplitMaxBytes := rng.Int63n(1 << 20) 509 510 // The outer loop runs once for each version edit. The inner loop(s) run 511 // once for each file, or each file bound. 512 for i := 0; i < 100; i++ { 513 var filesToAdd []*FileMetadata 514 numFiles := 1 + rng.Intn(9) 515 keys := make([][]byte, 0, 2*numFiles) 516 for j := 0; j < 2*numFiles; j++ { 517 if rng.Float64() <= keyReusePct && len(inUseKeys) > 0 { 518 keys = append(keys, inUseKeys[rng.Intn(len(inUseKeys))]) 519 } else { 520 newKey := testkeys.Key(keySpace, rng.Int63n(keySpace.Count())) 521 inUseKeys = append(inUseKeys, newKey) 522 keys = append(keys, newKey) 523 } 524 } 525 sort.Slice(keys, func(i, j int) bool { 526 return bytes.Compare(keys[i], keys[j]) < 0 527 }) 528 for j := 0; j < numFiles; j++ { 529 startKey := keys[j*2] 530 endKey := keys[j*2+1] 531 if bytes.Equal(startKey, endKey) { 532 continue 533 } 534 meta := (&FileMetadata{ 535 FileNum: base.FileNum(i*10 + j + 1), 536 Size: rng.Uint64n(1 << 20), 537 SmallestSeqNum: uint64(2*i + 1), 538 LargestSeqNum: uint64(2*i + 2), 539 }).ExtendPointKeyBounds( 540 base.DefaultComparer.Compare, 541 base.MakeInternalKey(startKey, uint64(2*i+1), base.InternalKeyKindSet), 542 base.MakeRangeDeleteSentinelKey(endKey), 543 ) 544 meta.InitPhysicalBacking() 545 fileMetas = append(fileMetas, meta) 546 filesToAdd = append(filesToAdd, meta) 547 } 548 if len(filesToAdd) == 0 { 549 continue 550 } 551 552 levelMetadata := makeLevelMetadata(testkeys.Comparer.Compare, 0, fileMetas) 553 var err error 554 555 if s2 == nil { 556 s2, err = NewL0Sublevels(&levelMetadata, testkeys.Comparer.Compare, testkeys.Comparer.FormatKey, flushSplitMaxBytes) 557 require.NoError(t, err) 558 } else { 559 // AddL0Files relies on the indices in FileMetadatas pointing to that of 560 // the previous L0Sublevels. So it must be called before NewL0Sublevels; 561 // calling it the other way around results in out-of-bounds panics. 562 SortBySeqNum(filesToAdd) 563 s2, err = s2.AddL0Files(filesToAdd, flushSplitMaxBytes, &levelMetadata) 564 require.NoError(t, err) 565 } 566 567 s, err = NewL0Sublevels(&levelMetadata, testkeys.Comparer.Compare, testkeys.Comparer.FormatKey, flushSplitMaxBytes) 568 require.NoError(t, err) 569 570 // Check for equivalence. 571 require.Equal(t, s.flushSplitUserKeys, s2.flushSplitUserKeys) 572 require.Equal(t, s.orderedIntervals, s2.orderedIntervals) 573 require.Equal(t, s.levelFiles, s2.levelFiles) 574 } 575 } 576 577 func BenchmarkManifestApplyWithL0Sublevels(b *testing.B) { 578 b.ResetTimer() 579 for n := 0; n < b.N; n++ { 580 v, err := readManifest("testdata/MANIFEST_import") 581 require.NotNil(b, v) 582 require.NoError(b, err) 583 } 584 } 585 586 func BenchmarkL0SublevelsInit(b *testing.B) { 587 v, err := readManifest("testdata/MANIFEST_import") 588 if err != nil { 589 b.Fatal(err) 590 } 591 b.ResetTimer() 592 for n := 0; n < b.N; n++ { 593 sl, err := NewL0Sublevels(&v.Levels[0], 594 base.DefaultComparer.Compare, base.DefaultFormatter, 5<<20) 595 require.NoError(b, err) 596 if sl == nil { 597 b.Fatal("expected non-nil L0Sublevels to be generated") 598 } 599 } 600 } 601 602 func BenchmarkL0SublevelsInitAndPick(b *testing.B) { 603 v, err := readManifest("testdata/MANIFEST_import") 604 if err != nil { 605 b.Fatal(err) 606 } 607 b.ResetTimer() 608 for n := 0; n < b.N; n++ { 609 sl, err := NewL0Sublevels(&v.Levels[0], 610 base.DefaultComparer.Compare, base.DefaultFormatter, 5<<20) 611 require.NoError(b, err) 612 if sl == nil { 613 b.Fatal("expected non-nil L0Sublevels to be generated") 614 } 615 c, err := sl.PickBaseCompaction(2, LevelSlice{}) 616 require.NoError(b, err) 617 if c == nil { 618 b.Fatal("expected non-nil compaction to be generated") 619 } 620 } 621 }