github.com/zuoyebang/bitalostable@v1.0.1-0.20240229032404-e3b99a834294/compaction_picker_test.go (about) 1 // Copyright 2018 The LevelDB-Go and Pebble and Bitalostored 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 bitalostable 6 7 import ( 8 "bytes" 9 "fmt" 10 "math" 11 "sort" 12 "strconv" 13 "strings" 14 "testing" 15 16 "github.com/cockroachdb/errors" 17 "github.com/stretchr/testify/require" 18 "github.com/zuoyebang/bitalostable/internal/base" 19 "github.com/zuoyebang/bitalostable/internal/datadriven" 20 "github.com/zuoyebang/bitalostable/internal/manifest" 21 ) 22 23 func loadVersion(d *datadriven.TestData) (*version, *Options, [numLevels]int64, string) { 24 var sizes [numLevels]int64 25 opts := &Options{} 26 opts.EnsureDefaults() 27 28 if len(d.CmdArgs) != 1 { 29 return nil, nil, sizes, fmt.Sprintf("%s expects 1 argument", d.Cmd) 30 } 31 var err error 32 opts.LBaseMaxBytes, err = strconv.ParseInt(d.CmdArgs[0].Key, 10, 64) 33 if err != nil { 34 return nil, nil, sizes, err.Error() 35 } 36 37 var files [numLevels][]*fileMetadata 38 if len(d.Input) > 0 { 39 for _, data := range strings.Split(d.Input, "\n") { 40 parts := strings.Split(data, " ") 41 parts[0] = strings.TrimSuffix(strings.TrimSpace(parts[0]), ":") 42 if len(parts) < 2 { 43 return nil, nil, sizes, fmt.Sprintf("malformed test:\n%s", d.Input) 44 } 45 level, err := strconv.Atoi(parts[0]) 46 if err != nil { 47 return nil, nil, sizes, err.Error() 48 } 49 if files[level] != nil { 50 return nil, nil, sizes, fmt.Sprintf("level %d already filled", level) 51 } 52 size, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 64) 53 if err != nil { 54 return nil, nil, sizes, err.Error() 55 } 56 for i := uint64(1); sizes[level] < int64(size); i++ { 57 var key InternalKey 58 if level == 0 { 59 // For L0, make `size` overlapping files. 60 key = base.MakeInternalKey([]byte(fmt.Sprintf("%04d", 1)), i, InternalKeyKindSet) 61 } else { 62 key = base.MakeInternalKey([]byte(fmt.Sprintf("%04d", i)), i, InternalKeyKindSet) 63 } 64 m := (&fileMetadata{ 65 SmallestSeqNum: key.SeqNum(), 66 LargestSeqNum: key.SeqNum(), 67 Size: 1, 68 }).ExtendPointKeyBounds(opts.Comparer.Compare, key, key) 69 if size >= 100 { 70 // If the requested size of the level is very large only add a single 71 // file in order to avoid massive blow-up in the number of files in 72 // the Version. 73 // 74 // TODO(peter): There is tension between the testing in 75 // TestCompactionPickerLevelMaxBytes and 76 // TestCompactionPickerTargetLevel. Clean this up somehow. 77 m.Size = size 78 } 79 files[level] = append(files[level], m) 80 sizes[level] += int64(m.Size) 81 } 82 } 83 } 84 85 vers := newVersion(opts, files) 86 return vers, opts, sizes, "" 87 } 88 89 func diskAvailBytesInf() uint64 { 90 return math.MaxUint64 91 } 92 93 func TestCompactionPickerByScoreLevelMaxBytes(t *testing.T) { 94 datadriven.RunTest(t, "testdata/compaction_picker_level_max_bytes", 95 func(d *datadriven.TestData) string { 96 switch d.Cmd { 97 case "init": 98 vers, opts, sizes, errMsg := loadVersion(d) 99 if errMsg != "" { 100 return errMsg 101 } 102 103 p, ok := newCompactionPicker(vers, opts, nil, sizes, diskAvailBytesInf).(*compactionPickerByScore) 104 require.True(t, ok) 105 var buf bytes.Buffer 106 for level := p.getBaseLevel(); level < numLevels; level++ { 107 fmt.Fprintf(&buf, "%d: %d\n", level, p.levelMaxBytes[level]) 108 } 109 return buf.String() 110 111 default: 112 return fmt.Sprintf("unknown command: %s", d.Cmd) 113 } 114 }) 115 } 116 117 func TestCompactionPickerTargetLevel(t *testing.T) { 118 var vers *version 119 var opts *Options 120 var sizes [numLevels]int64 121 var pickerByScore *compactionPickerByScore 122 123 parseInProgress := func(vals []string) ([]compactionInfo, error) { 124 var levels []int 125 for _, s := range vals { 126 l, err := strconv.ParseInt(s, 10, 8) 127 if err != nil { 128 return nil, err 129 } 130 levels = append(levels, int(l)) 131 } 132 if len(levels)%2 != 0 { 133 return nil, errors.New("odd number of levels with ongoing compactions") 134 } 135 var inProgress []compactionInfo 136 for i := 0; i < len(levels); i += 2 { 137 inProgress = append(inProgress, compactionInfo{ 138 inputs: []compactionLevel{ 139 {level: levels[i]}, 140 {level: levels[i+1]}, 141 }, 142 outputLevel: levels[i+1], 143 }) 144 } 145 return inProgress, nil 146 } 147 148 resetCompacting := func() { 149 for _, files := range vers.Levels { 150 files.Slice().Each(func(f *fileMetadata) { 151 f.CompactionState = manifest.CompactionStateNotCompacting 152 }) 153 } 154 } 155 156 datadriven.RunTest(t, "testdata/compaction_picker_target_level", 157 func(d *datadriven.TestData) string { 158 switch d.Cmd { 159 case "init": 160 var errMsg string 161 vers, opts, sizes, errMsg = loadVersion(d) 162 if errMsg != "" { 163 return errMsg 164 } 165 return "" 166 case "init_cp": 167 resetCompacting() 168 169 var inProgress []compactionInfo 170 if len(d.CmdArgs) == 1 { 171 arg := d.CmdArgs[0] 172 if arg.Key != "ongoing" { 173 return "unknown arg: " + arg.Key 174 } 175 var err error 176 inProgress, err = parseInProgress(arg.Vals) 177 if err != nil { 178 return err.Error() 179 } 180 } 181 182 p := newCompactionPicker(vers, opts, inProgress, sizes, diskAvailBytesInf) 183 var ok bool 184 pickerByScore, ok = p.(*compactionPickerByScore) 185 require.True(t, ok) 186 return fmt.Sprintf("base: %d", pickerByScore.baseLevel) 187 case "queue": 188 var b strings.Builder 189 var inProgress []compactionInfo 190 for { 191 env := compactionEnv{ 192 earliestUnflushedSeqNum: InternalKeySeqNumMax, 193 inProgressCompactions: inProgress, 194 } 195 pc := pickerByScore.pickAuto(env) 196 if pc == nil { 197 break 198 } 199 fmt.Fprintf(&b, "L%d->L%d: %.1f\n", pc.startLevel.level, pc.outputLevel.level, pc.score) 200 inProgress = append(inProgress, compactionInfo{ 201 inputs: pc.inputs, 202 outputLevel: pc.outputLevel.level, 203 }) 204 if pc.outputLevel.level == 0 { 205 // Once we pick one L0->L0 compaction, we'll keep on doing so 206 // because the test isn't marking files as Compacting. 207 break 208 } 209 for _, cl := range pc.inputs { 210 cl.files.Each(func(f *fileMetadata) { 211 f.CompactionState = manifest.CompactionStateCompacting 212 }) 213 } 214 } 215 216 resetCompacting() 217 return b.String() 218 case "pick": 219 resetCompacting() 220 221 var inProgress []compactionInfo 222 if len(d.CmdArgs) == 1 { 223 arg := d.CmdArgs[0] 224 if arg.Key != "ongoing" { 225 return "unknown arg: " + arg.Key 226 } 227 var err error 228 inProgress, err = parseInProgress(arg.Vals) 229 if err != nil { 230 return err.Error() 231 } 232 } 233 234 // Mark files as compacting for each in-progress compaction. 235 for i := range inProgress { 236 c := &inProgress[i] 237 iter := vers.Levels[c.inputs[0].level].Iter() 238 for f := iter.First(); f != nil; f = iter.Next() { 239 if !f.IsCompacting() { 240 f.CompactionState = manifest.CompactionStateCompacting 241 c.inputs[0].files = iter.Take().Slice() 242 break 243 } 244 } 245 246 switch { 247 case c.inputs[0].level == 0 && c.outputLevel != 0: 248 // L0->Lbase: mark all of Lbase as compacting. 249 c.inputs[1].files = vers.Levels[c.outputLevel].Slice() 250 for _, in := range c.inputs { 251 in.files.Each(func(f *fileMetadata) { 252 f.CompactionState = manifest.CompactionStateCompacting 253 }) 254 } 255 case c.inputs[0].level != c.outputLevel: 256 // Ln->Ln+1: mark 1 file in Ln+1 as compacting. 257 iter := vers.Levels[c.outputLevel].Iter() 258 for f := iter.First(); f != nil; f = iter.Next() { 259 if !f.IsCompacting() { 260 f.CompactionState = manifest.CompactionStateCompacting 261 c.inputs[1].files = iter.Take().Slice() 262 break 263 } 264 } 265 } 266 } 267 268 pc := pickerByScore.pickAuto(compactionEnv{ 269 earliestUnflushedSeqNum: InternalKeySeqNumMax, 270 inProgressCompactions: inProgress, 271 }) 272 if pc == nil { 273 return "no compaction" 274 } 275 return fmt.Sprintf("L%d->L%d: %0.1f", pc.startLevel.level, pc.outputLevel.level, pc.score) 276 case "pick_manual": 277 startLevel := 0 278 start := "" 279 end := "" 280 281 if len(d.CmdArgs) > 0 { 282 for _, arg := range d.CmdArgs { 283 switch arg.Key { 284 case "level": 285 startLevel64, err := strconv.ParseInt(arg.Vals[0], 10, 64) 286 if err != nil { 287 return err.Error() 288 } 289 startLevel = int(startLevel64) 290 case "start": 291 start = arg.Vals[0] 292 case "end": 293 end = arg.Vals[0] 294 default: 295 return "unknown arg: " + arg.Key 296 } 297 } 298 } 299 300 iStart := base.MakeInternalKey([]byte(start), InternalKeySeqNumMax, InternalKeyKindMax) 301 iEnd := base.MakeInternalKey([]byte(end), 0, 0) 302 manual := &manualCompaction{ 303 done: make(chan error, 1), 304 level: startLevel, 305 start: iStart.UserKey, 306 end: iEnd.UserKey, 307 } 308 309 pc, retryLater := pickerByScore.pickManual(compactionEnv{ 310 earliestUnflushedSeqNum: InternalKeySeqNumMax, 311 }, manual) 312 if pc == nil { 313 return fmt.Sprintf("nil, retryLater = %v", retryLater) 314 } 315 316 return fmt.Sprintf("L%d->L%d, retryLater = %v", pc.startLevel.level, pc.outputLevel.level, retryLater) 317 default: 318 return fmt.Sprintf("unknown command: %s", d.Cmd) 319 } 320 }) 321 } 322 323 func TestCompactionPickerEstimatedCompactionDebt(t *testing.T) { 324 datadriven.RunTest(t, "testdata/compaction_picker_estimated_debt", 325 func(d *datadriven.TestData) string { 326 switch d.Cmd { 327 case "init": 328 vers, opts, sizes, errMsg := loadVersion(d) 329 if errMsg != "" { 330 return errMsg 331 } 332 opts.MemTableSize = 1000 333 334 p := newCompactionPicker(vers, opts, nil, sizes, diskAvailBytesInf) 335 return fmt.Sprintf("%d\n", p.estimatedCompactionDebt(0)) 336 337 default: 338 return fmt.Sprintf("unknown command: %s", d.Cmd) 339 } 340 }) 341 } 342 343 func TestCompactionPickerL0(t *testing.T) { 344 opts := (*Options)(nil).EnsureDefaults() 345 opts.Experimental.L0CompactionConcurrency = 1 346 347 parseMeta := func(s string) (*fileMetadata, error) { 348 parts := strings.Split(s, ":") 349 fileNum, err := strconv.Atoi(parts[0]) 350 if err != nil { 351 return nil, err 352 } 353 fields := strings.Fields(parts[1]) 354 parts = strings.Split(fields[0], "-") 355 if len(parts) != 2 { 356 return nil, errors.Errorf("malformed table spec: %s", s) 357 } 358 m := (&fileMetadata{ 359 FileNum: base.FileNum(fileNum), 360 }).ExtendPointKeyBounds( 361 opts.Comparer.Compare, 362 base.ParseInternalKey(strings.TrimSpace(parts[0])), 363 base.ParseInternalKey(strings.TrimSpace(parts[1])), 364 ) 365 m.SmallestSeqNum = m.Smallest.SeqNum() 366 m.LargestSeqNum = m.Largest.SeqNum() 367 return m, nil 368 } 369 370 var picker *compactionPickerByScore 371 var inProgressCompactions []compactionInfo 372 var pc *pickedCompaction 373 374 datadriven.RunTest(t, "testdata/compaction_picker_L0", func(td *datadriven.TestData) string { 375 switch td.Cmd { 376 case "define": 377 fileMetas := [manifest.NumLevels][]*fileMetadata{} 378 baseLevel := manifest.NumLevels - 1 379 level := 0 380 var err error 381 lines := strings.Split(td.Input, "\n") 382 var compactionLines []string 383 384 for len(lines) > 0 { 385 data := strings.TrimSpace(lines[0]) 386 lines = lines[1:] 387 switch data { 388 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 389 level, err = strconv.Atoi(data[1:]) 390 if err != nil { 391 return err.Error() 392 } 393 case "compactions": 394 compactionLines, lines = lines, nil 395 default: 396 meta, err := parseMeta(data) 397 if err != nil { 398 return err.Error() 399 } 400 if level != 0 && level < baseLevel { 401 baseLevel = level 402 } 403 fileMetas[level] = append(fileMetas[level], meta) 404 } 405 } 406 407 // Parse in-progress compactions in the form of: 408 // L0 000001 -> L2 000005 409 inProgressCompactions = nil 410 for len(compactionLines) > 0 { 411 parts := strings.Fields(compactionLines[0]) 412 compactionLines = compactionLines[1:] 413 414 var level int 415 var info compactionInfo 416 first := true 417 compactionFiles := map[int][]*fileMetadata{} 418 for _, p := range parts { 419 switch p { 420 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 421 var err error 422 level, err = strconv.Atoi(p[1:]) 423 if err != nil { 424 return err.Error() 425 } 426 if len(info.inputs) > 0 && info.inputs[len(info.inputs)-1].level == level { 427 // eg, L0 -> L0 compaction or L6 -> L6 compaction 428 continue 429 } 430 if info.outputLevel < level { 431 info.outputLevel = level 432 } 433 info.inputs = append(info.inputs, compactionLevel{level: level}) 434 case "->": 435 continue 436 default: 437 fileNum, err := strconv.Atoi(p) 438 if err != nil { 439 return err.Error() 440 } 441 var compactFile *fileMetadata 442 for _, m := range fileMetas[level] { 443 if m.FileNum == FileNum(fileNum) { 444 compactFile = m 445 } 446 } 447 if compactFile == nil { 448 return fmt.Sprintf("cannot find compaction file %s", FileNum(fileNum)) 449 } 450 compactFile.CompactionState = manifest.CompactionStateCompacting 451 if first || base.InternalCompare(DefaultComparer.Compare, info.largest, compactFile.Largest) < 0 { 452 info.largest = compactFile.Largest 453 } 454 if first || base.InternalCompare(DefaultComparer.Compare, info.smallest, compactFile.Smallest) > 0 { 455 info.smallest = compactFile.Smallest 456 } 457 first = false 458 compactionFiles[level] = append(compactionFiles[level], compactFile) 459 } 460 } 461 for i, cl := range info.inputs { 462 files := compactionFiles[cl.level] 463 info.inputs[i].files = manifest.NewLevelSliceSeqSorted(files) 464 // Mark as intra-L0 compacting if the compaction is 465 // L0 -> L0. 466 if info.outputLevel == 0 { 467 for _, f := range files { 468 f.IsIntraL0Compacting = true 469 } 470 } 471 } 472 inProgressCompactions = append(inProgressCompactions, info) 473 } 474 475 version := newVersion(opts, fileMetas) 476 version.L0Sublevels.InitCompactingFileInfo(inProgressL0Compactions(inProgressCompactions)) 477 vs := &versionSet{ 478 opts: opts, 479 cmp: DefaultComparer.Compare, 480 cmpName: DefaultComparer.Name, 481 } 482 vs.versions.Init(nil) 483 vs.append(version) 484 picker = &compactionPickerByScore{ 485 opts: opts, 486 vers: version, 487 baseLevel: baseLevel, 488 diskAvailBytes: diskAvailBytesInf, 489 } 490 vs.picker = picker 491 for l := 0; l < len(picker.levelSizes); l++ { 492 version.Levels[l].Slice().Each(func(m *fileMetadata) { 493 picker.levelSizes[l] += int64(m.Size) 494 }) 495 } 496 picker.initLevelMaxBytes(inProgressCompactions) 497 498 var buf bytes.Buffer 499 fmt.Fprint(&buf, version.String()) 500 if len(inProgressCompactions) > 0 { 501 fmt.Fprintln(&buf, "compactions") 502 for _, c := range inProgressCompactions { 503 fmt.Fprintf(&buf, " %s\n", c.String()) 504 } 505 } 506 return buf.String() 507 case "pick-auto": 508 for _, arg := range td.CmdArgs { 509 var err error 510 switch arg.Key { 511 case "l0_compaction_threshold": 512 opts.L0CompactionThreshold, err = strconv.Atoi(arg.Vals[0]) 513 if err != nil { 514 return err.Error() 515 } 516 case "l0_compaction_file_threshold": 517 opts.L0CompactionFileThreshold, err = strconv.Atoi(arg.Vals[0]) 518 if err != nil { 519 return err.Error() 520 } 521 } 522 } 523 524 pc = picker.pickAuto(compactionEnv{ 525 earliestUnflushedSeqNum: math.MaxUint64, 526 inProgressCompactions: inProgressCompactions, 527 }) 528 var result strings.Builder 529 if pc != nil { 530 c := newCompaction(pc, opts) 531 fmt.Fprintf(&result, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) 532 fmt.Fprintf(&result, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) 533 if !pc.outputLevel.files.Empty() { 534 fmt.Fprintf(&result, "L%d: %s\n", pc.outputLevel.level, fileNums(pc.outputLevel.files)) 535 } 536 if !c.grandparents.Empty() { 537 fmt.Fprintf(&result, "grandparents: %s\n", fileNums(c.grandparents)) 538 } 539 } else { 540 return "nil" 541 } 542 return result.String() 543 case "mark-for-compaction": 544 var fileNum uint64 545 td.ScanArgs(t, "file", &fileNum) 546 for l, lm := range picker.vers.Levels { 547 iter := lm.Iter() 548 for f := iter.First(); f != nil; f = iter.Next() { 549 if f.FileNum != base.FileNum(fileNum) { 550 continue 551 } 552 f.MarkedForCompaction = true 553 picker.vers.Stats.MarkedForCompaction++ 554 picker.vers.Levels[l].InvalidateAnnotation(markedForCompactionAnnotator{}) 555 return fmt.Sprintf("marked L%d.%s", l, f.FileNum) 556 } 557 } 558 return "not-found" 559 case "max-output-file-size": 560 if pc == nil { 561 return "no compaction" 562 } 563 return fmt.Sprintf("%d", pc.maxOutputFileSize) 564 case "max-overlap-bytes": 565 if pc == nil { 566 return "no compaction" 567 } 568 return fmt.Sprintf("%d", pc.maxOverlapBytes) 569 } 570 return fmt.Sprintf("unrecognized command: %s", td.Cmd) 571 }) 572 } 573 574 func TestCompactionPickerConcurrency(t *testing.T) { 575 opts := (*Options)(nil).EnsureDefaults() 576 opts.Experimental.L0CompactionConcurrency = 1 577 578 parseMeta := func(s string) (*fileMetadata, error) { 579 parts := strings.Split(s, ":") 580 fileNum, err := strconv.Atoi(parts[0]) 581 if err != nil { 582 return nil, err 583 } 584 fields := strings.Fields(parts[1]) 585 parts = strings.Split(fields[0], "-") 586 if len(parts) != 2 { 587 return nil, errors.Errorf("malformed table spec: %s", s) 588 } 589 m := (&fileMetadata{ 590 FileNum: base.FileNum(fileNum), 591 Size: 1028, 592 }).ExtendPointKeyBounds( 593 opts.Comparer.Compare, 594 base.ParseInternalKey(strings.TrimSpace(parts[0])), 595 base.ParseInternalKey(strings.TrimSpace(parts[1])), 596 ) 597 for _, p := range fields[1:] { 598 if strings.HasPrefix(p, "size=") { 599 v, err := strconv.Atoi(strings.TrimPrefix(p, "size=")) 600 if err != nil { 601 return nil, err 602 } 603 m.Size = uint64(v) 604 } 605 } 606 m.SmallestSeqNum = m.Smallest.SeqNum() 607 m.LargestSeqNum = m.Largest.SeqNum() 608 return m, nil 609 } 610 611 var picker *compactionPickerByScore 612 var inProgressCompactions []compactionInfo 613 614 datadriven.RunTest(t, "testdata/compaction_picker_concurrency", func(td *datadriven.TestData) string { 615 switch td.Cmd { 616 case "define": 617 fileMetas := [manifest.NumLevels][]*fileMetadata{} 618 level := 0 619 var err error 620 lines := strings.Split(td.Input, "\n") 621 var compactionLines []string 622 623 for len(lines) > 0 { 624 data := strings.TrimSpace(lines[0]) 625 lines = lines[1:] 626 switch data { 627 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 628 level, err = strconv.Atoi(data[1:]) 629 if err != nil { 630 return err.Error() 631 } 632 case "compactions": 633 compactionLines, lines = lines, nil 634 default: 635 meta, err := parseMeta(data) 636 if err != nil { 637 return err.Error() 638 } 639 fileMetas[level] = append(fileMetas[level], meta) 640 } 641 } 642 643 // Parse in-progress compactions in the form of: 644 // L0 000001 -> L2 000005 645 inProgressCompactions = nil 646 for len(compactionLines) > 0 { 647 parts := strings.Fields(compactionLines[0]) 648 compactionLines = compactionLines[1:] 649 650 var level int 651 var info compactionInfo 652 first := true 653 compactionFiles := map[int][]*fileMetadata{} 654 for _, p := range parts { 655 switch p { 656 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 657 var err error 658 level, err = strconv.Atoi(p[1:]) 659 if err != nil { 660 return err.Error() 661 } 662 if len(info.inputs) > 0 && info.inputs[len(info.inputs)-1].level == level { 663 // eg, L0 -> L0 compaction or L6 -> L6 compaction 664 continue 665 } 666 if info.outputLevel < level { 667 info.outputLevel = level 668 } 669 info.inputs = append(info.inputs, compactionLevel{level: level}) 670 case "->": 671 continue 672 default: 673 fileNum, err := strconv.Atoi(p) 674 if err != nil { 675 return err.Error() 676 } 677 var compactFile *fileMetadata 678 for _, m := range fileMetas[level] { 679 if m.FileNum == FileNum(fileNum) { 680 compactFile = m 681 } 682 } 683 if compactFile == nil { 684 return fmt.Sprintf("cannot find compaction file %s", FileNum(fileNum)) 685 } 686 compactFile.CompactionState = manifest.CompactionStateCompacting 687 if first || base.InternalCompare(DefaultComparer.Compare, info.largest, compactFile.Largest) < 0 { 688 info.largest = compactFile.Largest 689 } 690 if first || base.InternalCompare(DefaultComparer.Compare, info.smallest, compactFile.Smallest) > 0 { 691 info.smallest = compactFile.Smallest 692 } 693 first = false 694 compactionFiles[level] = append(compactionFiles[level], compactFile) 695 } 696 } 697 for i, cl := range info.inputs { 698 files := compactionFiles[cl.level] 699 if cl.level == 0 { 700 info.inputs[i].files = manifest.NewLevelSliceSeqSorted(files) 701 } else { 702 info.inputs[i].files = manifest.NewLevelSliceKeySorted(DefaultComparer.Compare, files) 703 } 704 // Mark as intra-L0 compacting if the compaction is 705 // L0 -> L0. 706 if info.outputLevel == 0 { 707 for _, f := range files { 708 f.IsIntraL0Compacting = true 709 } 710 } 711 } 712 inProgressCompactions = append(inProgressCompactions, info) 713 } 714 715 version := newVersion(opts, fileMetas) 716 version.L0Sublevels.InitCompactingFileInfo(inProgressL0Compactions(inProgressCompactions)) 717 vs := &versionSet{ 718 opts: opts, 719 cmp: DefaultComparer.Compare, 720 cmpName: DefaultComparer.Name, 721 } 722 vs.versions.Init(nil) 723 vs.append(version) 724 var sizes [numLevels]int64 725 for l := 0; l < len(sizes); l++ { 726 version.Levels[l].Slice().Each(func(m *fileMetadata) { 727 sizes[l] += int64(m.Size) 728 }) 729 } 730 picker = newCompactionPicker(version, opts, inProgressCompactions, sizes, diskAvailBytesInf).(*compactionPickerByScore) 731 vs.picker = picker 732 733 var buf bytes.Buffer 734 fmt.Fprint(&buf, version.String()) 735 if len(inProgressCompactions) > 0 { 736 fmt.Fprintln(&buf, "compactions") 737 for _, c := range inProgressCompactions { 738 fmt.Fprintf(&buf, " %s\n", c.String()) 739 } 740 } 741 return buf.String() 742 743 case "pick-auto": 744 for _, arg := range td.CmdArgs { 745 var err error 746 switch arg.Key { 747 case "l0_compaction_threshold": 748 opts.L0CompactionThreshold, err = strconv.Atoi(arg.Vals[0]) 749 if err != nil { 750 return err.Error() 751 } 752 case "l0_compaction_concurrency": 753 opts.Experimental.L0CompactionConcurrency, err = strconv.Atoi(arg.Vals[0]) 754 if err != nil { 755 return err.Error() 756 } 757 case "compaction_debt_concurrency": 758 opts.Experimental.CompactionDebtConcurrency, err = strconv.Atoi(arg.Vals[0]) 759 if err != nil { 760 return err.Error() 761 } 762 } 763 } 764 765 pc := picker.pickAuto(compactionEnv{ 766 earliestUnflushedSeqNum: math.MaxUint64, 767 inProgressCompactions: inProgressCompactions, 768 }) 769 var result strings.Builder 770 if pc != nil { 771 c := newCompaction(pc, opts) 772 fmt.Fprintf(&result, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) 773 fmt.Fprintf(&result, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) 774 if !pc.outputLevel.files.Empty() { 775 fmt.Fprintf(&result, "L%d: %s\n", pc.outputLevel.level, fileNums(pc.outputLevel.files)) 776 } 777 if !c.grandparents.Empty() { 778 fmt.Fprintf(&result, "grandparents: %s\n", fileNums(c.grandparents)) 779 } 780 } else { 781 return "nil" 782 } 783 return result.String() 784 } 785 return fmt.Sprintf("unrecognized command: %s", td.Cmd) 786 }) 787 } 788 789 func TestCompactionPickerPickReadTriggered(t *testing.T) { 790 opts := (*Options)(nil).EnsureDefaults() 791 var picker *compactionPickerByScore 792 var rcList readCompactionQueue 793 var vers *version 794 795 parseMeta := func(s string) (*fileMetadata, error) { 796 parts := strings.Split(s, ":") 797 fileNum, err := strconv.Atoi(parts[0]) 798 if err != nil { 799 return nil, err 800 } 801 fields := strings.Fields(parts[1]) 802 parts = strings.Split(fields[0], "-") 803 if len(parts) != 2 { 804 return nil, errors.Errorf("malformed table spec: %s. usage: <file-num>:start.SET.1-end.SET.2", s) 805 } 806 m := (&fileMetadata{ 807 FileNum: base.FileNum(fileNum), 808 Size: 1028, 809 }).ExtendPointKeyBounds( 810 opts.Comparer.Compare, 811 base.ParseInternalKey(strings.TrimSpace(parts[0])), 812 base.ParseInternalKey(strings.TrimSpace(parts[1])), 813 ) 814 for _, p := range fields[1:] { 815 if strings.HasPrefix(p, "size=") { 816 v, err := strconv.Atoi(strings.TrimPrefix(p, "size=")) 817 if err != nil { 818 return nil, err 819 } 820 m.Size = uint64(v) 821 } 822 } 823 m.SmallestSeqNum = m.Smallest.SeqNum() 824 m.LargestSeqNum = m.Largest.SeqNum() 825 return m, nil 826 } 827 828 datadriven.RunTest(t, "testdata/compaction_picker_read_triggered", func(td *datadriven.TestData) string { 829 switch td.Cmd { 830 case "define": 831 rcList = readCompactionQueue{} 832 fileMetas := [manifest.NumLevels][]*fileMetadata{} 833 level := 0 834 var err error 835 lines := strings.Split(td.Input, "\n") 836 837 for len(lines) > 0 { 838 data := strings.TrimSpace(lines[0]) 839 lines = lines[1:] 840 switch data { 841 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 842 level, err = strconv.Atoi(data[1:]) 843 if err != nil { 844 return err.Error() 845 } 846 default: 847 meta, err := parseMeta(data) 848 if err != nil { 849 return err.Error() 850 } 851 fileMetas[level] = append(fileMetas[level], meta) 852 } 853 } 854 855 vers = newVersion(opts, fileMetas) 856 vs := &versionSet{ 857 opts: opts, 858 cmp: DefaultComparer.Compare, 859 cmpName: DefaultComparer.Name, 860 } 861 vs.versions.Init(nil) 862 vs.append(vers) 863 var sizes [numLevels]int64 864 for l := 0; l < len(sizes); l++ { 865 vers.Levels[l].Slice().Each(func(m *fileMetadata) { 866 sizes[l] += int64(m.Size) 867 }) 868 } 869 var inProgressCompactions []compactionInfo 870 picker = newCompactionPicker(vers, opts, inProgressCompactions, sizes, diskAvailBytesInf).(*compactionPickerByScore) 871 vs.picker = picker 872 873 var buf bytes.Buffer 874 fmt.Fprint(&buf, vers.String()) 875 return buf.String() 876 877 case "add-read-compaction": 878 for _, line := range strings.Split(td.Input, "\n") { 879 if line == "" { 880 continue 881 } 882 parts := strings.Split(line, " ") 883 if len(parts) != 3 { 884 return "error: malformed data for add-read-compaction. usage: <level>: <start>-<end> <filenum>" 885 } 886 if l, err := strconv.Atoi(parts[0][:1]); err == nil { 887 keys := strings.Split(parts[1], "-") 888 fileNum, _ := strconv.Atoi(parts[2]) 889 890 rc := readCompaction{ 891 level: l, 892 start: []byte(keys[0]), 893 end: []byte(keys[1]), 894 fileNum: base.FileNum(fileNum), 895 } 896 rcList.add(&rc, DefaultComparer.Compare) 897 } else { 898 return err.Error() 899 } 900 } 901 return "" 902 903 case "show-read-compactions": 904 var sb strings.Builder 905 if rcList.size == 0 { 906 sb.WriteString("(none)") 907 } 908 for i := 0; i < rcList.size; i++ { 909 rc := rcList.at(i) 910 sb.WriteString(fmt.Sprintf("(level: %d, start: %s, end: %s)\n", rc.level, string(rc.start), string(rc.end))) 911 } 912 return sb.String() 913 914 case "pick-auto": 915 pc := picker.pickAuto(compactionEnv{ 916 earliestUnflushedSeqNum: math.MaxUint64, 917 readCompactionEnv: readCompactionEnv{ 918 readCompactions: &rcList, 919 flushing: false, 920 }, 921 }) 922 var result strings.Builder 923 if pc != nil { 924 fmt.Fprintf(&result, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) 925 fmt.Fprintf(&result, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) 926 if !pc.outputLevel.files.Empty() { 927 fmt.Fprintf(&result, "L%d: %s\n", pc.outputLevel.level, fileNums(pc.outputLevel.files)) 928 } 929 } else { 930 return "nil" 931 } 932 return result.String() 933 } 934 return fmt.Sprintf("unrecognized command: %s", td.Cmd) 935 }) 936 } 937 938 func TestPickedCompactionSetupInputs(t *testing.T) { 939 opts := &Options{} 940 opts.EnsureDefaults() 941 parseMeta := func(s string) *fileMetadata { 942 parts := strings.Split(strings.TrimSpace(s), " ") 943 var fileSize uint64 944 var compacting bool 945 for _, part := range parts { 946 switch { 947 case part == "compacting": 948 compacting = true 949 case strings.HasPrefix(part, "size="): 950 v, err := strconv.ParseUint(strings.TrimPrefix(part, "size="), 10, 64) 951 require.NoError(t, err) 952 fileSize = v 953 } 954 } 955 tableParts := strings.Split(parts[0], "-") 956 if len(tableParts) != 2 { 957 t.Fatalf("malformed table spec: %s", s) 958 } 959 state := manifest.CompactionStateNotCompacting 960 if compacting { 961 state = manifest.CompactionStateCompacting 962 } 963 m := (&fileMetadata{ 964 CompactionState: state, 965 Size: fileSize, 966 }).ExtendPointKeyBounds( 967 opts.Comparer.Compare, 968 base.ParseInternalKey(strings.TrimSpace(tableParts[0])), 969 base.ParseInternalKey(strings.TrimSpace(tableParts[1])), 970 ) 971 m.SmallestSeqNum = m.Smallest.SeqNum() 972 m.LargestSeqNum = m.Largest.SeqNum() 973 return m 974 } 975 976 datadriven.RunTest(t, "testdata/compaction_setup_inputs", 977 func(d *datadriven.TestData) string { 978 switch d.Cmd { 979 case "setup-inputs": 980 var availBytes uint64 = math.MaxUint64 981 args := d.CmdArgs 982 983 if len(args) > 0 && args[0].Key == "avail-bytes" { 984 require.Equal(t, 1, len(args[0].Vals)) 985 var err error 986 availBytes, err = strconv.ParseUint(args[0].Vals[0], 10, 64) 987 require.NoError(t, err) 988 args = args[1:] 989 } 990 991 if len(args) != 2 { 992 return "setup-inputs [avail-bytes=XXX] <start> <end>" 993 } 994 995 pc := &pickedCompaction{ 996 cmp: DefaultComparer.Compare, 997 inputs: []compactionLevel{{level: -1}, {level: -1}}, 998 } 999 pc.startLevel, pc.outputLevel = &pc.inputs[0], &pc.inputs[1] 1000 var currentLevel int 1001 var files [numLevels][]*fileMetadata 1002 fileNum := FileNum(1) 1003 1004 for _, data := range strings.Split(d.Input, "\n") { 1005 switch data { 1006 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 1007 level, err := strconv.Atoi(data[1:]) 1008 if err != nil { 1009 return err.Error() 1010 } 1011 if pc.startLevel.level == -1 { 1012 pc.startLevel.level = level 1013 currentLevel = level 1014 } else if pc.outputLevel.level == -1 { 1015 if pc.startLevel.level >= level { 1016 return fmt.Sprintf("startLevel=%d >= outputLevel=%d\n", pc.startLevel.level, level) 1017 } 1018 pc.outputLevel.level = level 1019 currentLevel = level 1020 } else { 1021 return "outputLevel already set\n" 1022 } 1023 1024 default: 1025 meta := parseMeta(data) 1026 meta.FileNum = fileNum 1027 fileNum++ 1028 files[currentLevel] = append(files[currentLevel], meta) 1029 } 1030 } 1031 1032 if pc.outputLevel.level == -1 { 1033 pc.outputLevel.level = pc.startLevel.level + 1 1034 } 1035 pc.version = newVersion(opts, files) 1036 pc.startLevel.files = pc.version.Overlaps(pc.startLevel.level, pc.cmp, 1037 []byte(args[0].String()), []byte(args[1].String()), false /* exclusiveEnd */) 1038 1039 var isCompacting bool 1040 if !pc.setupInputs(opts, availBytes, pc.startLevel) { 1041 isCompacting = true 1042 } 1043 1044 var buf bytes.Buffer 1045 for _, cl := range pc.inputs { 1046 if cl.files.Empty() { 1047 continue 1048 } 1049 1050 fmt.Fprintf(&buf, "L%d\n", cl.level) 1051 cl.files.Each(func(f *fileMetadata) { 1052 fmt.Fprintf(&buf, " %s\n", f) 1053 }) 1054 } 1055 if isCompacting { 1056 fmt.Fprintf(&buf, "is-compacting") 1057 } 1058 return buf.String() 1059 1060 default: 1061 return fmt.Sprintf("unknown command: %s", d.Cmd) 1062 } 1063 }) 1064 } 1065 1066 func TestPickedCompactionExpandInputs(t *testing.T) { 1067 opts := &Options{} 1068 opts.EnsureDefaults() 1069 cmp := DefaultComparer.Compare 1070 var files []*fileMetadata 1071 1072 parseMeta := func(s string) *fileMetadata { 1073 parts := strings.Split(s, "-") 1074 if len(parts) != 2 { 1075 t.Fatalf("malformed table spec: %s", s) 1076 } 1077 m := (&fileMetadata{}).ExtendPointKeyBounds( 1078 opts.Comparer.Compare, 1079 base.ParseInternalKey(parts[0]), 1080 base.ParseInternalKey(parts[1]), 1081 ) 1082 return m 1083 } 1084 1085 datadriven.RunTest(t, "testdata/compaction_expand_inputs", 1086 func(d *datadriven.TestData) string { 1087 switch d.Cmd { 1088 case "define": 1089 files = nil 1090 if len(d.Input) == 0 { 1091 return "" 1092 } 1093 for _, data := range strings.Split(d.Input, "\n") { 1094 meta := parseMeta(data) 1095 meta.FileNum = FileNum(len(files)) 1096 files = append(files, meta) 1097 } 1098 manifest.SortBySmallest(files, cmp) 1099 return "" 1100 1101 case "expand-inputs": 1102 pc := &pickedCompaction{ 1103 cmp: cmp, 1104 inputs: []compactionLevel{{level: 1}}, 1105 } 1106 pc.startLevel = &pc.inputs[0] 1107 1108 var filesLevelled [numLevels][]*fileMetadata 1109 filesLevelled[pc.startLevel.level] = files 1110 pc.version = newVersion(opts, filesLevelled) 1111 1112 if len(d.CmdArgs) != 1 { 1113 return fmt.Sprintf("%s expects 1 argument", d.Cmd) 1114 } 1115 index, err := strconv.ParseInt(d.CmdArgs[0].String(), 10, 64) 1116 if err != nil { 1117 return err.Error() 1118 } 1119 1120 // Advance the iterator to position `index`. 1121 iter := pc.version.Levels[pc.startLevel.level].Iter() 1122 _ = iter.First() 1123 for i := int64(0); i < index; i++ { 1124 _ = iter.Next() 1125 } 1126 1127 inputs, _ := expandToAtomicUnit(cmp, iter.Take().Slice(), true /* disableIsCompacting */) 1128 1129 var buf bytes.Buffer 1130 inputs.Each(func(f *fileMetadata) { 1131 fmt.Fprintf(&buf, "%d: %s-%s\n", f.FileNum, f.Smallest, f.Largest) 1132 }) 1133 return buf.String() 1134 1135 default: 1136 return fmt.Sprintf("unknown command: %s", d.Cmd) 1137 } 1138 }) 1139 } 1140 1141 func TestCompactionOutputFileSize(t *testing.T) { 1142 opts := (*Options)(nil).EnsureDefaults() 1143 var picker *compactionPickerByScore 1144 var vers *version 1145 1146 parseMeta := func(s string) (*fileMetadata, error) { 1147 parts := strings.Split(s, ":") 1148 fileNum, err := strconv.Atoi(parts[0]) 1149 if err != nil { 1150 return nil, err 1151 } 1152 fields := strings.Fields(parts[1]) 1153 parts = strings.Split(fields[0], "-") 1154 if len(parts) != 2 { 1155 return nil, errors.Errorf("malformed table spec: %s. usage: <file-num>:start.SET.1-end.SET.2", s) 1156 } 1157 m := (&fileMetadata{ 1158 FileNum: base.FileNum(fileNum), 1159 Size: 1028, 1160 }).ExtendPointKeyBounds( 1161 opts.Comparer.Compare, 1162 base.ParseInternalKey(strings.TrimSpace(parts[0])), 1163 base.ParseInternalKey(strings.TrimSpace(parts[1])), 1164 ) 1165 for _, p := range fields[1:] { 1166 if strings.HasPrefix(p, "size=") { 1167 v, err := strconv.Atoi(strings.TrimPrefix(p, "size=")) 1168 if err != nil { 1169 return nil, err 1170 } 1171 m.Size = uint64(v) 1172 } 1173 if strings.HasPrefix(p, "range-deletions-bytes-estimate=") { 1174 v, err := strconv.Atoi(strings.TrimPrefix(p, "range-deletions-bytes-estimate=")) 1175 if err != nil { 1176 return nil, err 1177 } 1178 m.Stats.RangeDeletionsBytesEstimate = uint64(v) 1179 m.Stats.NumDeletions = 1 // At least one range del responsible for the deletion bytes. 1180 m.StatsMarkValid() 1181 } 1182 } 1183 m.SmallestSeqNum = m.Smallest.SeqNum() 1184 m.LargestSeqNum = m.Largest.SeqNum() 1185 return m, nil 1186 } 1187 1188 datadriven.RunTest(t, "testdata/compaction_output_file_size", func(td *datadriven.TestData) string { 1189 switch td.Cmd { 1190 case "define": 1191 fileMetas := [manifest.NumLevels][]*fileMetadata{} 1192 level := 0 1193 var err error 1194 lines := strings.Split(td.Input, "\n") 1195 1196 for len(lines) > 0 { 1197 data := strings.TrimSpace(lines[0]) 1198 lines = lines[1:] 1199 switch data { 1200 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 1201 level, err = strconv.Atoi(data[1:]) 1202 if err != nil { 1203 return err.Error() 1204 } 1205 default: 1206 meta, err := parseMeta(data) 1207 if err != nil { 1208 return err.Error() 1209 } 1210 fileMetas[level] = append(fileMetas[level], meta) 1211 } 1212 } 1213 1214 vers = newVersion(opts, fileMetas) 1215 vs := &versionSet{ 1216 opts: opts, 1217 cmp: DefaultComparer.Compare, 1218 cmpName: DefaultComparer.Name, 1219 } 1220 vs.versions.Init(nil) 1221 vs.append(vers) 1222 var sizes [numLevels]int64 1223 for l := 0; l < len(sizes); l++ { 1224 slice := vers.Levels[l].Slice() 1225 sizes[l] = int64(slice.SizeSum()) 1226 } 1227 var inProgressCompactions []compactionInfo 1228 picker = newCompactionPicker(vers, opts, inProgressCompactions, sizes, diskAvailBytesInf).(*compactionPickerByScore) 1229 vs.picker = picker 1230 1231 var buf bytes.Buffer 1232 fmt.Fprint(&buf, vers.String()) 1233 return buf.String() 1234 1235 case "pick-auto": 1236 pc := picker.pickAuto(compactionEnv{ 1237 earliestUnflushedSeqNum: math.MaxUint64, 1238 earliestSnapshotSeqNum: math.MaxUint64, 1239 }) 1240 var buf bytes.Buffer 1241 if pc != nil { 1242 fmt.Fprintf(&buf, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) 1243 fmt.Fprintf(&buf, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) 1244 fmt.Fprintf(&buf, "maxOutputFileSize: %d\n", pc.maxOutputFileSize) 1245 } else { 1246 return "nil" 1247 } 1248 return buf.String() 1249 1250 default: 1251 return fmt.Sprintf("unrecognized command: %s", td.Cmd) 1252 } 1253 }) 1254 } 1255 1256 func fileNums(files manifest.LevelSlice) string { 1257 var ss []string 1258 files.Each(func(f *fileMetadata) { 1259 ss = append(ss, f.FileNum.String()) 1260 }) 1261 sort.Strings(ss) 1262 return strings.Join(ss, ",") 1263 }