github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/compaction_picker_test.go (about) 1 // Copyright 2018 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 pebble 6 7 import ( 8 "bytes" 9 "fmt" 10 "math" 11 "sort" 12 "strconv" 13 "strings" 14 "sync" 15 "testing" 16 "time" 17 18 "github.com/cockroachdb/datadriven" 19 "github.com/cockroachdb/errors" 20 "github.com/cockroachdb/pebble/internal/base" 21 "github.com/cockroachdb/pebble/internal/humanize" 22 "github.com/cockroachdb/pebble/internal/manifest" 23 "github.com/cockroachdb/pebble/internal/testkeys" 24 "github.com/cockroachdb/pebble/vfs" 25 "github.com/stretchr/testify/require" 26 ) 27 28 func loadVersion(t *testing.T, d *datadriven.TestData) (*version, *Options, string) { 29 var sizes [numLevels]int64 30 opts := &Options{} 31 opts.testingRandomized(t) 32 opts.EnsureDefaults() 33 34 if len(d.CmdArgs) != 1 { 35 return nil, nil, fmt.Sprintf("%s expects 1 argument", d.Cmd) 36 } 37 var err error 38 opts.LBaseMaxBytes, err = strconv.ParseInt(d.CmdArgs[0].Key, 10, 64) 39 if err != nil { 40 return nil, nil, err.Error() 41 } 42 43 var files [numLevels][]*fileMetadata 44 if len(d.Input) > 0 { 45 // Parse each line as 46 // 47 // <level>: <size> [compensation] 48 // 49 // Creating sstables within the level whose file sizes total to `size` 50 // and whose compensated file sizes total to `size`+`compensation`. If 51 // size is sufficiently large, only one single file is created. See 52 // the TODO below. 53 for _, data := range strings.Split(d.Input, "\n") { 54 parts := strings.Split(data, " ") 55 parts[0] = strings.TrimSuffix(strings.TrimSpace(parts[0]), ":") 56 if len(parts) < 2 { 57 return nil, nil, fmt.Sprintf("malformed test:\n%s", d.Input) 58 } 59 level, err := strconv.Atoi(parts[0]) 60 if err != nil { 61 return nil, nil, err.Error() 62 } 63 if files[level] != nil { 64 return nil, nil, fmt.Sprintf("level %d already filled", level) 65 } 66 size, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 64) 67 if err != nil { 68 return nil, nil, err.Error() 69 } 70 var compensation uint64 71 if len(parts) == 3 { 72 compensation, err = strconv.ParseUint(strings.TrimSpace(parts[2]), 10, 64) 73 if err != nil { 74 return nil, nil, err.Error() 75 } 76 } 77 78 var lastFile *fileMetadata 79 for i := uint64(1); sizes[level] < int64(size); i++ { 80 var key InternalKey 81 if level == 0 { 82 // For L0, make `size` overlapping files. 83 key = base.MakeInternalKey([]byte(fmt.Sprintf("%04d", 1)), i, InternalKeyKindSet) 84 } else { 85 key = base.MakeInternalKey([]byte(fmt.Sprintf("%04d", i)), i, InternalKeyKindSet) 86 } 87 m := (&fileMetadata{ 88 FileNum: base.FileNum(uint64(level)*100_000 + i), 89 SmallestSeqNum: key.SeqNum(), 90 LargestSeqNum: key.SeqNum(), 91 Size: 1, 92 Stats: manifest.TableStats{ 93 RangeDeletionsBytesEstimate: 0, 94 }, 95 }).ExtendPointKeyBounds(opts.Comparer.Compare, key, key) 96 m.InitPhysicalBacking() 97 m.StatsMarkValid() 98 lastFile = m 99 if size >= 100 { 100 // If the requested size of the level is very large only add a single 101 // file in order to avoid massive blow-up in the number of files in 102 // the Version. 103 // 104 // TODO(peter): There is tension between the testing in 105 // TestCompactionPickerLevelMaxBytes and 106 // TestCompactionPickerTargetLevel. Clean this up somehow. 107 m.Size = size 108 if level != 0 { 109 endKey := base.MakeInternalKey([]byte(fmt.Sprintf("%04d", size)), i, InternalKeyKindSet) 110 m.ExtendPointKeyBounds(opts.Comparer.Compare, key, endKey) 111 } 112 } 113 files[level] = append(files[level], m) 114 sizes[level] += int64(m.Size) 115 } 116 // Let all the compensation be due to the last file. 117 if lastFile != nil && compensation > 0 { 118 lastFile.Stats.RangeDeletionsBytesEstimate = compensation 119 } 120 } 121 } 122 123 vers := newVersion(opts, files) 124 return vers, opts, "" 125 } 126 127 func TestCompactionPickerByScoreLevelMaxBytes(t *testing.T) { 128 datadriven.RunTest(t, "testdata/compaction_picker_level_max_bytes", 129 func(t *testing.T, d *datadriven.TestData) string { 130 switch d.Cmd { 131 case "init": 132 vers, opts, errMsg := loadVersion(t, d) 133 if errMsg != "" { 134 return errMsg 135 } 136 137 p, ok := newCompactionPicker(vers, opts, nil).(*compactionPickerByScore) 138 require.True(t, ok) 139 var buf bytes.Buffer 140 for level := p.getBaseLevel(); level < numLevels; level++ { 141 fmt.Fprintf(&buf, "%d: %d\n", level, p.levelMaxBytes[level]) 142 } 143 return buf.String() 144 145 default: 146 return fmt.Sprintf("unknown command: %s", d.Cmd) 147 } 148 }) 149 } 150 151 func TestCompactionPickerTargetLevel(t *testing.T) { 152 var vers *version 153 var opts *Options 154 var pickerByScore *compactionPickerByScore 155 156 parseInProgress := func(vals []string) ([]compactionInfo, error) { 157 var levels []int 158 for _, s := range vals { 159 l, err := strconv.ParseInt(s, 10, 8) 160 if err != nil { 161 return nil, err 162 } 163 levels = append(levels, int(l)) 164 } 165 if len(levels)%2 != 0 { 166 return nil, errors.New("odd number of levels with ongoing compactions") 167 } 168 var inProgress []compactionInfo 169 for i := 0; i < len(levels); i += 2 { 170 inProgress = append(inProgress, compactionInfo{ 171 inputs: []compactionLevel{ 172 {level: levels[i]}, 173 {level: levels[i+1]}, 174 }, 175 outputLevel: levels[i+1], 176 }) 177 } 178 return inProgress, nil 179 } 180 181 resetCompacting := func() { 182 for _, files := range vers.Levels { 183 files.Slice().Each(func(f *fileMetadata) { 184 f.CompactionState = manifest.CompactionStateNotCompacting 185 }) 186 } 187 } 188 189 datadriven.RunTest(t, "testdata/compaction_picker_target_level", 190 func(t *testing.T, d *datadriven.TestData) string { 191 switch d.Cmd { 192 case "init": 193 // loadVersion expects a single datadriven argument that it 194 // sets as Options.LBaseMaxBytes. It parses the input as 195 // newline-separated levels, specifying the level's file size 196 // and optionally additional compensation to be added during 197 // compensated file size calculations. Eg: 198 // 199 // init <LBaseMaxBytes> 200 // <level>: <size> [compensation] 201 // <level>: <size> [compensation] 202 var errMsg string 203 vers, opts, errMsg = loadVersion(t, d) 204 if errMsg != "" { 205 return errMsg 206 } 207 return runVersionFileSizes(vers) 208 case "init_cp": 209 resetCompacting() 210 211 var inProgress []compactionInfo 212 if arg, ok := d.Arg("ongoing"); ok { 213 var err error 214 inProgress, err = parseInProgress(arg.Vals) 215 if err != nil { 216 return err.Error() 217 } 218 } 219 220 p := newCompactionPicker(vers, opts, inProgress) 221 var ok bool 222 pickerByScore, ok = p.(*compactionPickerByScore) 223 require.True(t, ok) 224 return fmt.Sprintf("base: %d", pickerByScore.baseLevel) 225 case "queue": 226 var b strings.Builder 227 var inProgress []compactionInfo 228 for { 229 env := compactionEnv{ 230 diskAvailBytes: math.MaxUint64, 231 earliestUnflushedSeqNum: InternalKeySeqNumMax, 232 inProgressCompactions: inProgress, 233 } 234 pc := pickerByScore.pickAuto(env) 235 if pc == nil { 236 break 237 } 238 fmt.Fprintf(&b, "L%d->L%d: %.1f\n", pc.startLevel.level, pc.outputLevel.level, pc.score) 239 inProgress = append(inProgress, compactionInfo{ 240 inputs: pc.inputs, 241 outputLevel: pc.outputLevel.level, 242 smallest: pc.smallest, 243 largest: pc.largest, 244 }) 245 if pc.outputLevel.level == 0 { 246 // Once we pick one L0->L0 compaction, we'll keep on doing so 247 // because the test isn't marking files as Compacting. 248 break 249 } 250 for _, cl := range pc.inputs { 251 cl.files.Each(func(f *fileMetadata) { 252 f.CompactionState = manifest.CompactionStateCompacting 253 fmt.Fprintf(&b, " %s marked as compacting\n", f) 254 }) 255 } 256 } 257 258 resetCompacting() 259 return b.String() 260 case "pick": 261 resetCompacting() 262 263 var inProgress []compactionInfo 264 if len(d.CmdArgs) == 1 { 265 arg := d.CmdArgs[0] 266 if arg.Key != "ongoing" { 267 return "unknown arg: " + arg.Key 268 } 269 var err error 270 inProgress, err = parseInProgress(arg.Vals) 271 if err != nil { 272 return err.Error() 273 } 274 } 275 276 // Mark files as compacting for each in-progress compaction. 277 for i := range inProgress { 278 c := &inProgress[i] 279 for j, cl := range c.inputs { 280 iter := vers.Levels[cl.level].Iter() 281 for f := iter.First(); f != nil; f = iter.Next() { 282 if !f.IsCompacting() { 283 f.CompactionState = manifest.CompactionStateCompacting 284 c.inputs[j].files = iter.Take().Slice() 285 break 286 } 287 } 288 } 289 if c.inputs[0].level == 0 && c.outputLevel != 0 { 290 // L0->Lbase: mark all of Lbase as compacting. 291 c.inputs[1].files = vers.Levels[c.outputLevel].Slice() 292 for _, in := range c.inputs { 293 in.files.Each(func(f *fileMetadata) { 294 f.CompactionState = manifest.CompactionStateCompacting 295 }) 296 } 297 } 298 } 299 300 var b strings.Builder 301 fmt.Fprintf(&b, "Initial state before pick:\n%s", runVersionFileSizes(vers)) 302 pc := pickerByScore.pickAuto(compactionEnv{ 303 earliestUnflushedSeqNum: InternalKeySeqNumMax, 304 inProgressCompactions: inProgress, 305 }) 306 if pc != nil { 307 fmt.Fprintf(&b, "Picked: L%d->L%d: %0.1f\n", pc.startLevel.level, pc.outputLevel.level, pc.score) 308 } 309 if pc == nil { 310 fmt.Fprintln(&b, "Picked: no compaction") 311 } 312 return b.String() 313 case "pick_manual": 314 var startLevel int 315 var start, end string 316 d.MaybeScanArgs(t, "level", &startLevel) 317 d.MaybeScanArgs(t, "start", &start) 318 d.MaybeScanArgs(t, "end", &end) 319 320 iStart := base.MakeInternalKey([]byte(start), InternalKeySeqNumMax, InternalKeyKindMax) 321 iEnd := base.MakeInternalKey([]byte(end), 0, 0) 322 manual := &manualCompaction{ 323 done: make(chan error, 1), 324 level: startLevel, 325 start: iStart.UserKey, 326 end: iEnd.UserKey, 327 } 328 329 pc, retryLater := pickManualCompaction( 330 pickerByScore.vers, 331 opts, 332 compactionEnv{ 333 earliestUnflushedSeqNum: InternalKeySeqNumMax, 334 }, 335 pickerByScore.getBaseLevel(), 336 manual) 337 if pc == nil { 338 return fmt.Sprintf("nil, retryLater = %v", retryLater) 339 } 340 341 return fmt.Sprintf("L%d->L%d, retryLater = %v", pc.startLevel.level, pc.outputLevel.level, retryLater) 342 default: 343 return fmt.Sprintf("unknown command: %s", d.Cmd) 344 } 345 }) 346 } 347 348 func TestCompactionPickerEstimatedCompactionDebt(t *testing.T) { 349 datadriven.RunTest(t, "testdata/compaction_picker_estimated_debt", 350 func(t *testing.T, d *datadriven.TestData) string { 351 switch d.Cmd { 352 case "init": 353 vers, opts, errMsg := loadVersion(t, d) 354 if errMsg != "" { 355 return errMsg 356 } 357 opts.MemTableSize = 1000 358 359 p := newCompactionPicker(vers, opts, nil) 360 return fmt.Sprintf("%d\n", p.estimatedCompactionDebt(0)) 361 362 default: 363 return fmt.Sprintf("unknown command: %s", d.Cmd) 364 } 365 }) 366 } 367 368 func TestCompactionPickerL0(t *testing.T) { 369 opts := (*Options)(nil).EnsureDefaults() 370 opts.Experimental.L0CompactionConcurrency = 1 371 372 parseMeta := func(s string) (*fileMetadata, error) { 373 parts := strings.Split(s, ":") 374 fileNum, err := strconv.Atoi(parts[0]) 375 if err != nil { 376 return nil, err 377 } 378 fields := strings.Fields(parts[1]) 379 parts = strings.Split(fields[0], "-") 380 if len(parts) != 2 { 381 return nil, errors.Errorf("malformed table spec: %s", s) 382 } 383 m := (&fileMetadata{ 384 FileNum: base.FileNum(fileNum), 385 }).ExtendPointKeyBounds( 386 opts.Comparer.Compare, 387 base.ParseInternalKey(strings.TrimSpace(parts[0])), 388 base.ParseInternalKey(strings.TrimSpace(parts[1])), 389 ) 390 m.SmallestSeqNum = m.Smallest.SeqNum() 391 m.LargestSeqNum = m.Largest.SeqNum() 392 m.InitPhysicalBacking() 393 return m, nil 394 } 395 396 var picker *compactionPickerByScore 397 var inProgressCompactions []compactionInfo 398 var pc *pickedCompaction 399 400 datadriven.RunTest(t, "testdata/compaction_picker_L0", func(t *testing.T, td *datadriven.TestData) string { 401 switch td.Cmd { 402 case "define": 403 fileMetas := [manifest.NumLevels][]*fileMetadata{} 404 baseLevel := manifest.NumLevels - 1 405 level := 0 406 var err error 407 lines := strings.Split(td.Input, "\n") 408 var compactionLines []string 409 410 for len(lines) > 0 { 411 data := strings.TrimSpace(lines[0]) 412 lines = lines[1:] 413 switch data { 414 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 415 level, err = strconv.Atoi(data[1:]) 416 if err != nil { 417 return err.Error() 418 } 419 case "compactions": 420 compactionLines, lines = lines, nil 421 default: 422 meta, err := parseMeta(data) 423 if err != nil { 424 return err.Error() 425 } 426 if level != 0 && level < baseLevel { 427 baseLevel = level 428 } 429 fileMetas[level] = append(fileMetas[level], meta) 430 } 431 } 432 433 // Parse in-progress compactions in the form of: 434 // L0 000001 -> L2 000005 435 inProgressCompactions = nil 436 for len(compactionLines) > 0 { 437 parts := strings.Fields(compactionLines[0]) 438 compactionLines = compactionLines[1:] 439 440 var level int 441 var info compactionInfo 442 first := true 443 compactionFiles := map[int][]*fileMetadata{} 444 for _, p := range parts { 445 switch p { 446 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 447 var err error 448 level, err = strconv.Atoi(p[1:]) 449 if err != nil { 450 return err.Error() 451 } 452 if len(info.inputs) > 0 && info.inputs[len(info.inputs)-1].level == level { 453 // eg, L0 -> L0 compaction or L6 -> L6 compaction 454 continue 455 } 456 if info.outputLevel < level { 457 info.outputLevel = level 458 } 459 info.inputs = append(info.inputs, compactionLevel{level: level}) 460 case "->": 461 continue 462 default: 463 fileNum, err := strconv.Atoi(p) 464 if err != nil { 465 return err.Error() 466 } 467 var compactFile *fileMetadata 468 for _, m := range fileMetas[level] { 469 if m.FileNum == FileNum(fileNum) { 470 compactFile = m 471 } 472 } 473 if compactFile == nil { 474 return fmt.Sprintf("cannot find compaction file %s", FileNum(fileNum)) 475 } 476 compactFile.CompactionState = manifest.CompactionStateCompacting 477 if first || base.InternalCompare(DefaultComparer.Compare, info.largest, compactFile.Largest) < 0 { 478 info.largest = compactFile.Largest 479 } 480 if first || base.InternalCompare(DefaultComparer.Compare, info.smallest, compactFile.Smallest) > 0 { 481 info.smallest = compactFile.Smallest 482 } 483 first = false 484 compactionFiles[level] = append(compactionFiles[level], compactFile) 485 } 486 } 487 for i, cl := range info.inputs { 488 files := compactionFiles[cl.level] 489 info.inputs[i].files = manifest.NewLevelSliceSeqSorted(files) 490 // Mark as intra-L0 compacting if the compaction is 491 // L0 -> L0. 492 if info.outputLevel == 0 { 493 for _, f := range files { 494 f.IsIntraL0Compacting = true 495 } 496 } 497 } 498 inProgressCompactions = append(inProgressCompactions, info) 499 } 500 501 version := newVersion(opts, fileMetas) 502 version.L0Sublevels.InitCompactingFileInfo(inProgressL0Compactions(inProgressCompactions)) 503 vs := &versionSet{ 504 opts: opts, 505 cmp: DefaultComparer.Compare, 506 cmpName: DefaultComparer.Name, 507 } 508 vs.versions.Init(nil) 509 vs.append(version) 510 picker = &compactionPickerByScore{ 511 opts: opts, 512 vers: version, 513 baseLevel: baseLevel, 514 } 515 vs.picker = picker 516 picker.initLevelMaxBytes(inProgressCompactions) 517 518 var buf bytes.Buffer 519 fmt.Fprint(&buf, version.String()) 520 if len(inProgressCompactions) > 0 { 521 fmt.Fprintln(&buf, "compactions") 522 for _, c := range inProgressCompactions { 523 fmt.Fprintf(&buf, " %s\n", c.String()) 524 } 525 } 526 return buf.String() 527 case "pick-auto": 528 td.MaybeScanArgs(t, "l0_compaction_threshold", &opts.L0CompactionThreshold) 529 td.MaybeScanArgs(t, "l0_compaction_file_threshold", &opts.L0CompactionFileThreshold) 530 531 pc = picker.pickAuto(compactionEnv{ 532 diskAvailBytes: math.MaxUint64, 533 earliestUnflushedSeqNum: math.MaxUint64, 534 inProgressCompactions: inProgressCompactions, 535 }) 536 var result strings.Builder 537 if pc != nil { 538 checkClone(t, pc) 539 c := newCompaction(pc, opts, time.Now(), nil /* provider */) 540 fmt.Fprintf(&result, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) 541 fmt.Fprintf(&result, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) 542 if !pc.outputLevel.files.Empty() { 543 fmt.Fprintf(&result, "L%d: %s\n", pc.outputLevel.level, fileNums(pc.outputLevel.files)) 544 } 545 if !c.grandparents.Empty() { 546 fmt.Fprintf(&result, "grandparents: %s\n", fileNums(c.grandparents)) 547 } 548 } else { 549 return "nil" 550 } 551 return result.String() 552 case "mark-for-compaction": 553 var fileNum uint64 554 td.ScanArgs(t, "file", &fileNum) 555 for l, lm := range picker.vers.Levels { 556 iter := lm.Iter() 557 for f := iter.First(); f != nil; f = iter.Next() { 558 if f.FileNum != base.FileNum(fileNum) { 559 continue 560 } 561 f.MarkedForCompaction = true 562 picker.vers.Stats.MarkedForCompaction++ 563 picker.vers.Levels[l].InvalidateAnnotation(markedForCompactionAnnotator{}) 564 return fmt.Sprintf("marked L%d.%s", l, f.FileNum) 565 } 566 } 567 return "not-found" 568 case "max-output-file-size": 569 if pc == nil { 570 return "no compaction" 571 } 572 return fmt.Sprintf("%d", pc.maxOutputFileSize) 573 case "max-overlap-bytes": 574 if pc == nil { 575 return "no compaction" 576 } 577 return fmt.Sprintf("%d", pc.maxOverlapBytes) 578 } 579 return fmt.Sprintf("unrecognized command: %s", td.Cmd) 580 }) 581 } 582 583 func TestCompactionPickerConcurrency(t *testing.T) { 584 opts := (*Options)(nil).EnsureDefaults() 585 opts.Experimental.L0CompactionConcurrency = 1 586 587 parseMeta := func(s string) (*fileMetadata, error) { 588 parts := strings.Split(s, ":") 589 fileNum, err := strconv.Atoi(parts[0]) 590 if err != nil { 591 return nil, err 592 } 593 fields := strings.Fields(parts[1]) 594 parts = strings.Split(fields[0], "-") 595 if len(parts) != 2 { 596 return nil, errors.Errorf("malformed table spec: %s", s) 597 } 598 m := (&fileMetadata{ 599 FileNum: base.FileNum(fileNum), 600 Size: 1028, 601 }).ExtendPointKeyBounds( 602 opts.Comparer.Compare, 603 base.ParseInternalKey(strings.TrimSpace(parts[0])), 604 base.ParseInternalKey(strings.TrimSpace(parts[1])), 605 ) 606 m.InitPhysicalBacking() 607 for _, p := range fields[1:] { 608 if strings.HasPrefix(p, "size=") { 609 v, err := strconv.Atoi(strings.TrimPrefix(p, "size=")) 610 if err != nil { 611 return nil, err 612 } 613 m.Size = uint64(v) 614 } 615 } 616 m.SmallestSeqNum = m.Smallest.SeqNum() 617 m.LargestSeqNum = m.Largest.SeqNum() 618 return m, nil 619 } 620 621 var picker *compactionPickerByScore 622 var inProgressCompactions []compactionInfo 623 624 datadriven.RunTest(t, "testdata/compaction_picker_concurrency", func(t *testing.T, td *datadriven.TestData) string { 625 switch td.Cmd { 626 case "define": 627 fileMetas := [manifest.NumLevels][]*fileMetadata{} 628 level := 0 629 var err error 630 lines := strings.Split(td.Input, "\n") 631 var compactionLines []string 632 633 for len(lines) > 0 { 634 data := strings.TrimSpace(lines[0]) 635 lines = lines[1:] 636 switch data { 637 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 638 level, err = strconv.Atoi(data[1:]) 639 if err != nil { 640 return err.Error() 641 } 642 case "compactions": 643 compactionLines, lines = lines, nil 644 default: 645 meta, err := parseMeta(data) 646 if err != nil { 647 return err.Error() 648 } 649 fileMetas[level] = append(fileMetas[level], meta) 650 } 651 } 652 653 // Parse in-progress compactions in the form of: 654 // L0 000001 -> L2 000005 655 inProgressCompactions = nil 656 for len(compactionLines) > 0 { 657 parts := strings.Fields(compactionLines[0]) 658 compactionLines = compactionLines[1:] 659 660 var level int 661 var info compactionInfo 662 first := true 663 compactionFiles := map[int][]*fileMetadata{} 664 for _, p := range parts { 665 switch p { 666 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 667 var err error 668 level, err = strconv.Atoi(p[1:]) 669 if err != nil { 670 return err.Error() 671 } 672 if len(info.inputs) > 0 && info.inputs[len(info.inputs)-1].level == level { 673 // eg, L0 -> L0 compaction or L6 -> L6 compaction 674 continue 675 } 676 if info.outputLevel < level { 677 info.outputLevel = level 678 } 679 info.inputs = append(info.inputs, compactionLevel{level: level}) 680 case "->": 681 continue 682 default: 683 fileNum, err := strconv.Atoi(p) 684 if err != nil { 685 return err.Error() 686 } 687 var compactFile *fileMetadata 688 for _, m := range fileMetas[level] { 689 if m.FileNum == FileNum(fileNum) { 690 compactFile = m 691 } 692 } 693 if compactFile == nil { 694 return fmt.Sprintf("cannot find compaction file %s", FileNum(fileNum)) 695 } 696 compactFile.CompactionState = manifest.CompactionStateCompacting 697 if first || base.InternalCompare(DefaultComparer.Compare, info.largest, compactFile.Largest) < 0 { 698 info.largest = compactFile.Largest 699 } 700 if first || base.InternalCompare(DefaultComparer.Compare, info.smallest, compactFile.Smallest) > 0 { 701 info.smallest = compactFile.Smallest 702 } 703 first = false 704 compactionFiles[level] = append(compactionFiles[level], compactFile) 705 } 706 } 707 for i, cl := range info.inputs { 708 files := compactionFiles[cl.level] 709 if cl.level == 0 { 710 info.inputs[i].files = manifest.NewLevelSliceSeqSorted(files) 711 } else { 712 info.inputs[i].files = manifest.NewLevelSliceKeySorted(DefaultComparer.Compare, files) 713 } 714 // Mark as intra-L0 compacting if the compaction is 715 // L0 -> L0. 716 if info.outputLevel == 0 { 717 for _, f := range files { 718 f.IsIntraL0Compacting = true 719 } 720 } 721 } 722 inProgressCompactions = append(inProgressCompactions, info) 723 } 724 725 version := newVersion(opts, fileMetas) 726 version.L0Sublevels.InitCompactingFileInfo(inProgressL0Compactions(inProgressCompactions)) 727 vs := &versionSet{ 728 opts: opts, 729 cmp: DefaultComparer.Compare, 730 cmpName: DefaultComparer.Name, 731 } 732 vs.versions.Init(nil) 733 vs.append(version) 734 735 picker = newCompactionPicker(version, opts, inProgressCompactions).(*compactionPickerByScore) 736 vs.picker = picker 737 738 var buf bytes.Buffer 739 fmt.Fprint(&buf, version.String()) 740 if len(inProgressCompactions) > 0 { 741 fmt.Fprintln(&buf, "compactions") 742 for _, c := range inProgressCompactions { 743 fmt.Fprintf(&buf, " %s\n", c.String()) 744 } 745 } 746 return buf.String() 747 748 case "pick-auto": 749 td.MaybeScanArgs(t, "l0_compaction_threshold", &opts.L0CompactionThreshold) 750 td.MaybeScanArgs(t, "l0_compaction_concurrency", &opts.Experimental.L0CompactionConcurrency) 751 td.MaybeScanArgs(t, "compaction_debt_concurrency", &opts.Experimental.CompactionDebtConcurrency) 752 753 pc := picker.pickAuto(compactionEnv{ 754 earliestUnflushedSeqNum: math.MaxUint64, 755 inProgressCompactions: inProgressCompactions, 756 }) 757 var result strings.Builder 758 if pc != nil { 759 c := newCompaction(pc, opts, time.Now(), nil /* provider */) 760 fmt.Fprintf(&result, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) 761 fmt.Fprintf(&result, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) 762 if !pc.outputLevel.files.Empty() { 763 fmt.Fprintf(&result, "L%d: %s\n", pc.outputLevel.level, fileNums(pc.outputLevel.files)) 764 } 765 if !c.grandparents.Empty() { 766 fmt.Fprintf(&result, "grandparents: %s\n", fileNums(c.grandparents)) 767 } 768 } else { 769 return "nil" 770 } 771 return result.String() 772 } 773 return fmt.Sprintf("unrecognized command: %s", td.Cmd) 774 }) 775 } 776 777 func TestCompactionPickerPickReadTriggered(t *testing.T) { 778 opts := (*Options)(nil).EnsureDefaults() 779 var picker *compactionPickerByScore 780 var rcList readCompactionQueue 781 var vers *version 782 783 parseMeta := func(s string) (*fileMetadata, error) { 784 parts := strings.Split(s, ":") 785 fileNum, err := strconv.Atoi(parts[0]) 786 if err != nil { 787 return nil, err 788 } 789 fields := strings.Fields(parts[1]) 790 parts = strings.Split(fields[0], "-") 791 if len(parts) != 2 { 792 return nil, errors.Errorf("malformed table spec: %s. usage: <file-num>:start.SET.1-end.SET.2", s) 793 } 794 m := (&fileMetadata{ 795 FileNum: base.FileNum(fileNum), 796 Size: 1028, 797 }).ExtendPointKeyBounds( 798 opts.Comparer.Compare, 799 base.ParseInternalKey(strings.TrimSpace(parts[0])), 800 base.ParseInternalKey(strings.TrimSpace(parts[1])), 801 ) 802 m.InitPhysicalBacking() 803 for _, p := range fields[1:] { 804 if strings.HasPrefix(p, "size=") { 805 v, err := strconv.Atoi(strings.TrimPrefix(p, "size=")) 806 if err != nil { 807 return nil, err 808 } 809 m.Size = uint64(v) 810 } 811 } 812 m.SmallestSeqNum = m.Smallest.SeqNum() 813 m.LargestSeqNum = m.Largest.SeqNum() 814 return m, nil 815 } 816 817 datadriven.RunTest(t, "testdata/compaction_picker_read_triggered", func(t *testing.T, td *datadriven.TestData) string { 818 switch td.Cmd { 819 case "define": 820 rcList = readCompactionQueue{} 821 fileMetas := [manifest.NumLevels][]*fileMetadata{} 822 level := 0 823 var err error 824 lines := strings.Split(td.Input, "\n") 825 826 for len(lines) > 0 { 827 data := strings.TrimSpace(lines[0]) 828 lines = lines[1:] 829 switch data { 830 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 831 level, err = strconv.Atoi(data[1:]) 832 if err != nil { 833 return err.Error() 834 } 835 default: 836 meta, err := parseMeta(data) 837 if err != nil { 838 return err.Error() 839 } 840 fileMetas[level] = append(fileMetas[level], meta) 841 } 842 } 843 844 vers = newVersion(opts, fileMetas) 845 vs := &versionSet{ 846 opts: opts, 847 cmp: DefaultComparer.Compare, 848 cmpName: DefaultComparer.Name, 849 } 850 vs.versions.Init(nil) 851 vs.append(vers) 852 var inProgressCompactions []compactionInfo 853 picker = newCompactionPicker(vers, opts, inProgressCompactions).(*compactionPickerByScore) 854 vs.picker = picker 855 856 var buf bytes.Buffer 857 fmt.Fprint(&buf, vers.String()) 858 return buf.String() 859 860 case "add-read-compaction": 861 for _, line := range strings.Split(td.Input, "\n") { 862 if line == "" { 863 continue 864 } 865 parts := strings.Split(line, " ") 866 if len(parts) != 3 { 867 return "error: malformed data for add-read-compaction. usage: <level>: <start>-<end> <filenum>" 868 } 869 if l, err := strconv.Atoi(parts[0][:1]); err == nil { 870 keys := strings.Split(parts[1], "-") 871 fileNum, _ := strconv.Atoi(parts[2]) 872 873 rc := readCompaction{ 874 level: l, 875 start: []byte(keys[0]), 876 end: []byte(keys[1]), 877 fileNum: base.FileNum(fileNum), 878 } 879 rcList.add(&rc, DefaultComparer.Compare) 880 } else { 881 return err.Error() 882 } 883 } 884 return "" 885 886 case "show-read-compactions": 887 var sb strings.Builder 888 if rcList.size == 0 { 889 sb.WriteString("(none)") 890 } 891 for i := 0; i < rcList.size; i++ { 892 rc := rcList.at(i) 893 sb.WriteString(fmt.Sprintf("(level: %d, start: %s, end: %s)\n", rc.level, string(rc.start), string(rc.end))) 894 } 895 return sb.String() 896 897 case "pick-auto": 898 pc := picker.pickAuto(compactionEnv{ 899 earliestUnflushedSeqNum: math.MaxUint64, 900 readCompactionEnv: readCompactionEnv{ 901 readCompactions: &rcList, 902 flushing: false, 903 }, 904 }) 905 var result strings.Builder 906 if pc != nil { 907 fmt.Fprintf(&result, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) 908 fmt.Fprintf(&result, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) 909 if !pc.outputLevel.files.Empty() { 910 fmt.Fprintf(&result, "L%d: %s\n", pc.outputLevel.level, fileNums(pc.outputLevel.files)) 911 } 912 } else { 913 return "nil" 914 } 915 return result.String() 916 } 917 return fmt.Sprintf("unrecognized command: %s", td.Cmd) 918 }) 919 } 920 921 type alwaysMultiLevel struct{} 922 923 func (d alwaysMultiLevel) pick( 924 pcOrig *pickedCompaction, opts *Options, diskAvailBytes uint64, 925 ) *pickedCompaction { 926 pcMulti := pcOrig.clone() 927 if !pcMulti.setupMultiLevelCandidate(opts, diskAvailBytes) { 928 return pcOrig 929 } 930 return pcMulti 931 } 932 933 func (d alwaysMultiLevel) allowL0() bool { return false } 934 func (d alwaysMultiLevel) String() string { return "always" } 935 936 func TestPickedCompactionSetupInputs(t *testing.T) { 937 opts := &Options{} 938 opts.EnsureDefaults() 939 940 parseMeta := func(s string) *fileMetadata { 941 parts := strings.Split(strings.TrimSpace(s), " ") 942 var fileSize uint64 943 var compacting bool 944 for _, part := range parts { 945 switch { 946 case part == "compacting": 947 compacting = true 948 case strings.HasPrefix(part, "size="): 949 v, err := strconv.ParseUint(strings.TrimPrefix(part, "size="), 10, 64) 950 require.NoError(t, err) 951 fileSize = v 952 } 953 } 954 tableParts := strings.Split(parts[0], "-") 955 if len(tableParts) != 2 { 956 t.Fatalf("malformed table spec: %s", s) 957 } 958 state := manifest.CompactionStateNotCompacting 959 if compacting { 960 state = manifest.CompactionStateCompacting 961 } 962 m := (&fileMetadata{ 963 CompactionState: state, 964 Size: fileSize, 965 }).ExtendPointKeyBounds( 966 opts.Comparer.Compare, 967 base.ParseInternalKey(strings.TrimSpace(tableParts[0])), 968 base.ParseInternalKey(strings.TrimSpace(tableParts[1])), 969 ) 970 m.SmallestSeqNum = m.Smallest.SeqNum() 971 m.LargestSeqNum = m.Largest.SeqNum() 972 m.InitPhysicalBacking() 973 return m 974 } 975 976 setupInputTest := func(t *testing.T, d *datadriven.TestData) string { 977 switch d.Cmd { 978 case "setup-inputs": 979 var availBytes uint64 = math.MaxUint64 980 var maxLevelBytes [7]int64 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[:2] { 1006 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 1007 levelArgs := strings.Fields(data) 1008 level, err := strconv.Atoi(levelArgs[0][1:]) 1009 if err != nil { 1010 return err.Error() 1011 } 1012 currentLevel = level 1013 if len(levelArgs) > 1 { 1014 maxSizeArg := strings.Replace(levelArgs[1], "max-size=", "", 1) 1015 maxSize, err := strconv.ParseInt(maxSizeArg, 10, 64) 1016 if err != nil { 1017 return err.Error() 1018 } 1019 maxLevelBytes[level] = maxSize 1020 } else { 1021 maxLevelBytes[level] = math.MaxInt64 1022 } 1023 if pc.startLevel.level == -1 { 1024 pc.startLevel.level = level 1025 1026 } else if pc.outputLevel.level == -1 { 1027 if pc.startLevel.level >= level { 1028 return fmt.Sprintf("startLevel=%d >= outputLevel=%d\n", pc.startLevel.level, level) 1029 } 1030 pc.outputLevel.level = level 1031 } 1032 default: 1033 meta := parseMeta(data) 1034 meta.FileNum = fileNum 1035 fileNum++ 1036 files[currentLevel] = append(files[currentLevel], meta) 1037 } 1038 } 1039 1040 if pc.outputLevel.level == -1 { 1041 pc.outputLevel.level = pc.startLevel.level + 1 1042 } 1043 pc.version = newVersion(opts, files) 1044 pc.startLevel.files = pc.version.Overlaps(pc.startLevel.level, pc.cmp, 1045 []byte(args[0].String()), []byte(args[1].String()), false /* exclusiveEnd */) 1046 1047 var isCompacting bool 1048 if !pc.setupInputs(opts, availBytes, pc.startLevel) { 1049 isCompacting = true 1050 } 1051 origPC := pc 1052 pc = pc.maybeAddLevel(opts, availBytes) 1053 // If pc points to a new pickedCompaction, a new multi level compaction 1054 // was initialized. 1055 initMultiLevel := pc != origPC 1056 checkClone(t, pc) 1057 var buf bytes.Buffer 1058 for _, cl := range pc.inputs { 1059 if cl.files.Empty() { 1060 continue 1061 } 1062 1063 fmt.Fprintf(&buf, "L%d\n", cl.level) 1064 cl.files.Each(func(f *fileMetadata) { 1065 fmt.Fprintf(&buf, " %s\n", f) 1066 }) 1067 } 1068 if isCompacting { 1069 fmt.Fprintf(&buf, "is-compacting\n") 1070 } 1071 1072 if initMultiLevel { 1073 extraLevel := pc.extraLevels[0].level 1074 fmt.Fprintf(&buf, "init-multi-level(%d,%d,%d)\n", pc.startLevel.level, extraLevel, 1075 pc.outputLevel.level) 1076 fmt.Fprintf(&buf, "Original WriteAmp %.2f; ML WriteAmp %.2f\n", origPC.predictedWriteAmp(), pc.predictedWriteAmp()) 1077 fmt.Fprintf(&buf, "Original OverlappingRatio %.2f; ML OverlappingRatio %.2f\n", origPC.overlappingRatio(), pc.overlappingRatio()) 1078 } 1079 return buf.String() 1080 1081 default: 1082 return fmt.Sprintf("unknown command: %s", d.Cmd) 1083 } 1084 } 1085 1086 t.Logf("Test basic setup inputs behavior without multi level compactions") 1087 opts.Experimental.MultiLevelCompactionHeuristic = NoMultiLevel{} 1088 datadriven.RunTest(t, "testdata/compaction_setup_inputs", 1089 setupInputTest) 1090 1091 t.Logf("Turning multi level compaction on") 1092 opts.Experimental.MultiLevelCompactionHeuristic = alwaysMultiLevel{} 1093 datadriven.RunTest(t, "testdata/compaction_setup_inputs_multilevel_dummy", 1094 setupInputTest) 1095 1096 t.Logf("Try Write-Amp Heuristic") 1097 opts.Experimental.MultiLevelCompactionHeuristic = WriteAmpHeuristic{} 1098 datadriven.RunTest(t, "testdata/compaction_setup_inputs_multilevel_write_amp", 1099 setupInputTest) 1100 } 1101 1102 func TestPickedCompactionExpandInputs(t *testing.T) { 1103 opts := &Options{} 1104 opts.EnsureDefaults() 1105 cmp := DefaultComparer.Compare 1106 var files []*fileMetadata 1107 1108 parseMeta := func(s string) *fileMetadata { 1109 parts := strings.Split(s, "-") 1110 if len(parts) != 2 { 1111 t.Fatalf("malformed table spec: %s", s) 1112 } 1113 m := (&fileMetadata{}).ExtendPointKeyBounds( 1114 opts.Comparer.Compare, 1115 base.ParseInternalKey(parts[0]), 1116 base.ParseInternalKey(parts[1]), 1117 ) 1118 m.InitPhysicalBacking() 1119 return m 1120 } 1121 1122 datadriven.RunTest(t, "testdata/compaction_expand_inputs", 1123 func(t *testing.T, d *datadriven.TestData) string { 1124 switch d.Cmd { 1125 case "define": 1126 files = nil 1127 if len(d.Input) == 0 { 1128 return "" 1129 } 1130 for _, data := range strings.Split(d.Input, "\n") { 1131 meta := parseMeta(data) 1132 meta.FileNum = FileNum(len(files)) 1133 files = append(files, meta) 1134 } 1135 manifest.SortBySmallest(files, cmp) 1136 return "" 1137 1138 case "expand-inputs": 1139 pc := &pickedCompaction{ 1140 cmp: cmp, 1141 inputs: []compactionLevel{{level: 1}}, 1142 } 1143 pc.startLevel = &pc.inputs[0] 1144 1145 var filesLevelled [numLevels][]*fileMetadata 1146 filesLevelled[pc.startLevel.level] = files 1147 pc.version = newVersion(opts, filesLevelled) 1148 1149 if len(d.CmdArgs) != 1 { 1150 return fmt.Sprintf("%s expects 1 argument", d.Cmd) 1151 } 1152 index, err := strconv.ParseInt(d.CmdArgs[0].String(), 10, 64) 1153 if err != nil { 1154 return err.Error() 1155 } 1156 1157 // Advance the iterator to position `index`. 1158 iter := pc.version.Levels[pc.startLevel.level].Iter() 1159 _ = iter.First() 1160 for i := int64(0); i < index; i++ { 1161 _ = iter.Next() 1162 } 1163 1164 inputs, _ := expandToAtomicUnit(cmp, iter.Take().Slice(), true /* disableIsCompacting */) 1165 1166 var buf bytes.Buffer 1167 inputs.Each(func(f *fileMetadata) { 1168 fmt.Fprintf(&buf, "%d: %s-%s\n", f.FileNum, f.Smallest, f.Largest) 1169 }) 1170 return buf.String() 1171 1172 default: 1173 return fmt.Sprintf("unknown command: %s", d.Cmd) 1174 } 1175 }) 1176 } 1177 1178 func TestCompactionOutputFileSize(t *testing.T) { 1179 opts := (*Options)(nil).EnsureDefaults() 1180 var picker *compactionPickerByScore 1181 var vers *version 1182 1183 parseMeta := func(s string) (*fileMetadata, error) { 1184 parts := strings.Split(s, ":") 1185 fileNum, err := strconv.Atoi(parts[0]) 1186 if err != nil { 1187 return nil, err 1188 } 1189 fields := strings.Fields(parts[1]) 1190 parts = strings.Split(fields[0], "-") 1191 if len(parts) != 2 { 1192 return nil, errors.Errorf("malformed table spec: %s. usage: <file-num>:start.SET.1-end.SET.2", s) 1193 } 1194 m := (&fileMetadata{ 1195 FileNum: base.FileNum(fileNum), 1196 Size: 1028, 1197 }).ExtendPointKeyBounds( 1198 opts.Comparer.Compare, 1199 base.ParseInternalKey(strings.TrimSpace(parts[0])), 1200 base.ParseInternalKey(strings.TrimSpace(parts[1])), 1201 ) 1202 m.InitPhysicalBacking() 1203 for _, p := range fields[1:] { 1204 if strings.HasPrefix(p, "size=") { 1205 v, err := strconv.Atoi(strings.TrimPrefix(p, "size=")) 1206 if err != nil { 1207 return nil, err 1208 } 1209 m.Size = uint64(v) 1210 } 1211 if strings.HasPrefix(p, "range-deletions-bytes-estimate=") { 1212 v, err := strconv.Atoi(strings.TrimPrefix(p, "range-deletions-bytes-estimate=")) 1213 if err != nil { 1214 return nil, err 1215 } 1216 m.Stats.RangeDeletionsBytesEstimate = uint64(v) 1217 m.Stats.NumDeletions = 1 // At least one range del responsible for the deletion bytes. 1218 m.StatsMarkValid() 1219 } 1220 } 1221 m.SmallestSeqNum = m.Smallest.SeqNum() 1222 m.LargestSeqNum = m.Largest.SeqNum() 1223 return m, nil 1224 } 1225 1226 datadriven.RunTest(t, "testdata/compaction_output_file_size", func(t *testing.T, td *datadriven.TestData) string { 1227 switch td.Cmd { 1228 case "define": 1229 fileMetas := [manifest.NumLevels][]*fileMetadata{} 1230 level := 0 1231 var err error 1232 lines := strings.Split(td.Input, "\n") 1233 1234 for len(lines) > 0 { 1235 data := strings.TrimSpace(lines[0]) 1236 lines = lines[1:] 1237 switch data { 1238 case "L0", "L1", "L2", "L3", "L4", "L5", "L6": 1239 level, err = strconv.Atoi(data[1:]) 1240 if err != nil { 1241 return err.Error() 1242 } 1243 default: 1244 meta, err := parseMeta(data) 1245 if err != nil { 1246 return err.Error() 1247 } 1248 fileMetas[level] = append(fileMetas[level], meta) 1249 } 1250 } 1251 1252 vers = newVersion(opts, fileMetas) 1253 vs := &versionSet{ 1254 opts: opts, 1255 cmp: DefaultComparer.Compare, 1256 cmpName: DefaultComparer.Name, 1257 } 1258 vs.versions.Init(nil) 1259 vs.append(vers) 1260 var inProgressCompactions []compactionInfo 1261 picker = newCompactionPicker(vers, opts, inProgressCompactions).(*compactionPickerByScore) 1262 vs.picker = picker 1263 1264 var buf bytes.Buffer 1265 fmt.Fprint(&buf, vers.String()) 1266 return buf.String() 1267 1268 case "pick-auto": 1269 pc := picker.pickAuto(compactionEnv{ 1270 earliestUnflushedSeqNum: math.MaxUint64, 1271 earliestSnapshotSeqNum: math.MaxUint64, 1272 }) 1273 var buf bytes.Buffer 1274 if pc != nil { 1275 fmt.Fprintf(&buf, "L%d -> L%d\n", pc.startLevel.level, pc.outputLevel.level) 1276 fmt.Fprintf(&buf, "L%d: %s\n", pc.startLevel.level, fileNums(pc.startLevel.files)) 1277 fmt.Fprintf(&buf, "maxOutputFileSize: %d\n", pc.maxOutputFileSize) 1278 } else { 1279 return "nil" 1280 } 1281 return buf.String() 1282 1283 default: 1284 return fmt.Sprintf("unrecognized command: %s", td.Cmd) 1285 } 1286 }) 1287 } 1288 1289 func TestCompactionPickerCompensatedSize(t *testing.T) { 1290 testCases := []struct { 1291 size uint64 1292 pointDelEstimateBytes uint64 1293 rangeDelEstimateBytes uint64 1294 wantBytes uint64 1295 }{ 1296 { 1297 size: 100, 1298 pointDelEstimateBytes: 0, 1299 rangeDelEstimateBytes: 0, 1300 wantBytes: 100, 1301 }, 1302 { 1303 size: 100, 1304 pointDelEstimateBytes: 10, 1305 rangeDelEstimateBytes: 0, 1306 wantBytes: 100 + 10, 1307 }, 1308 { 1309 size: 100, 1310 pointDelEstimateBytes: 10, 1311 rangeDelEstimateBytes: 5, 1312 wantBytes: 100 + 10 + 5, 1313 }, 1314 } 1315 1316 for _, tc := range testCases { 1317 t.Run("", func(t *testing.T) { 1318 f := &fileMetadata{Size: tc.size} 1319 f.InitPhysicalBacking() 1320 f.Stats.PointDeletionsBytesEstimate = tc.pointDelEstimateBytes 1321 f.Stats.RangeDeletionsBytesEstimate = tc.rangeDelEstimateBytes 1322 gotBytes := compensatedSize(f) 1323 require.Equal(t, tc.wantBytes, gotBytes) 1324 }) 1325 } 1326 } 1327 1328 func TestCompactionPickerPickFile(t *testing.T) { 1329 fs := vfs.NewMem() 1330 opts := &Options{ 1331 Comparer: testkeys.Comparer, 1332 FormatMajorVersion: FormatNewest, 1333 FS: fs, 1334 } 1335 1336 d, err := Open("", opts) 1337 require.NoError(t, err) 1338 defer func() { 1339 if d != nil { 1340 require.NoError(t, d.Close()) 1341 } 1342 }() 1343 1344 datadriven.RunTest(t, "testdata/compaction_picker_pick_file", func(t *testing.T, td *datadriven.TestData) string { 1345 switch td.Cmd { 1346 case "define": 1347 require.NoError(t, d.Close()) 1348 1349 d, err = runDBDefineCmd(td, opts) 1350 if err != nil { 1351 return err.Error() 1352 } 1353 d.mu.Lock() 1354 s := d.mu.versions.currentVersion().String() 1355 d.mu.Unlock() 1356 return s 1357 1358 case "file-sizes": 1359 return runTableFileSizesCmd(td, d) 1360 1361 case "pick-file": 1362 s := strings.TrimPrefix(td.CmdArgs[0].String(), "L") 1363 level, err := strconv.Atoi(s) 1364 if err != nil { 1365 return fmt.Sprintf("unable to parse arg %q as level", td.CmdArgs[0].String()) 1366 } 1367 if level == 0 { 1368 panic("L0 picking unimplemented") 1369 } 1370 d.mu.Lock() 1371 defer d.mu.Unlock() 1372 1373 // Use maybeScheduleCompactionPicker to take care of all of the 1374 // initialization of the compaction-picking environment, but never 1375 // pick a compaction; just call pickFile using the user-provided 1376 // level. 1377 var lf manifest.LevelFile 1378 var ok bool 1379 d.maybeScheduleCompactionPicker(func(untypedPicker compactionPicker, env compactionEnv) *pickedCompaction { 1380 p := untypedPicker.(*compactionPickerByScore) 1381 lf, ok = pickCompactionSeedFile(p.vers, opts, level, level+1, env.earliestSnapshotSeqNum) 1382 return nil 1383 }) 1384 if !ok { 1385 return "(none)" 1386 } 1387 return lf.FileMetadata.String() 1388 1389 default: 1390 return fmt.Sprintf("unknown command: %s", td.Cmd) 1391 } 1392 }) 1393 } 1394 1395 type pausableCleaner struct { 1396 mu sync.Mutex 1397 cond sync.Cond 1398 paused bool 1399 cleaner Cleaner 1400 } 1401 1402 func (c *pausableCleaner) Clean(fs vfs.FS, fileType base.FileType, path string) error { 1403 c.mu.Lock() 1404 defer c.mu.Unlock() 1405 for c.paused { 1406 c.cond.Wait() 1407 } 1408 return c.cleaner.Clean(fs, fileType, path) 1409 } 1410 1411 func (c *pausableCleaner) pause() { 1412 c.mu.Lock() 1413 defer c.mu.Unlock() 1414 c.paused = true 1415 } 1416 1417 func (c *pausableCleaner) resume() { 1418 c.mu.Lock() 1419 defer c.mu.Unlock() 1420 c.paused = false 1421 c.cond.Broadcast() 1422 } 1423 1424 func TestCompactionPickerScores(t *testing.T) { 1425 fs := vfs.NewMem() 1426 cleaner := pausableCleaner{cleaner: DeleteCleaner{}} 1427 cleaner.cond.L = &cleaner.mu 1428 opts := &Options{ 1429 Cleaner: &cleaner, 1430 Comparer: testkeys.Comparer, 1431 DisableAutomaticCompactions: true, 1432 FormatMajorVersion: FormatNewest, 1433 FS: fs, 1434 } 1435 1436 d, err := Open("", opts) 1437 require.NoError(t, err) 1438 defer func() { 1439 if d != nil { 1440 cleaner.resume() 1441 require.NoError(t, closeAllSnapshots(d)) 1442 require.NoError(t, d.Close()) 1443 } 1444 }() 1445 1446 var buf bytes.Buffer 1447 datadriven.RunTest(t, "testdata/compaction_picker_scores", func(t *testing.T, td *datadriven.TestData) string { 1448 switch td.Cmd { 1449 case "define": 1450 require.NoError(t, closeAllSnapshots(d)) 1451 require.NoError(t, d.Close()) 1452 1453 if td.HasArg("pause-cleaning") { 1454 cleaner.pause() 1455 } 1456 1457 d, err = runDBDefineCmd(td, opts) 1458 if err != nil { 1459 return err.Error() 1460 } 1461 d.mu.Lock() 1462 s := d.mu.versions.currentVersion().String() 1463 d.mu.Unlock() 1464 return s 1465 1466 case "disable-table-stats": 1467 d.mu.Lock() 1468 d.opts.private.disableTableStats = true 1469 d.mu.Unlock() 1470 return "" 1471 1472 case "enable-table-stats": 1473 d.mu.Lock() 1474 d.opts.private.disableTableStats = false 1475 d.maybeCollectTableStatsLocked() 1476 d.mu.Unlock() 1477 return "" 1478 1479 case "resume-cleaning": 1480 cleaner.resume() 1481 return "" 1482 1483 case "ingest": 1484 if err = runBuildCmd(td, d, d.opts.FS); err != nil { 1485 return err.Error() 1486 } 1487 if err = runIngestCmd(td, d, d.opts.FS); err != nil { 1488 return err.Error() 1489 } 1490 d.mu.Lock() 1491 s := d.mu.versions.currentVersion().String() 1492 d.mu.Unlock() 1493 return s 1494 1495 case "lsm": 1496 return runLSMCmd(td, d) 1497 1498 case "maybe-compact": 1499 buf.Reset() 1500 d.mu.Lock() 1501 d.opts.DisableAutomaticCompactions = false 1502 d.maybeScheduleCompaction() 1503 fmt.Fprintf(&buf, "%d compactions in progress:", d.mu.compact.compactingCount) 1504 for c := range d.mu.compact.inProgress { 1505 fmt.Fprintf(&buf, "\n%s", c) 1506 } 1507 d.opts.DisableAutomaticCompactions = true 1508 d.mu.Unlock() 1509 return buf.String() 1510 1511 case "scores": 1512 waitFor := "completion" 1513 td.MaybeScanArgs(t, "wait-for-compaction", &waitFor) 1514 1515 // Wait for any running compactions to complete before calculating 1516 // scores. Otherwise, the output of this command is 1517 // nondeterministic. 1518 switch waitFor { 1519 case "completion": 1520 d.mu.Lock() 1521 for d.mu.compact.compactingCount > 0 { 1522 d.mu.compact.cond.Wait() 1523 } 1524 d.mu.Unlock() 1525 case "version-edit": 1526 func() { 1527 for { 1528 d.mu.Lock() 1529 wait := len(d.mu.compact.inProgress) > 0 1530 for c := range d.mu.compact.inProgress { 1531 wait = wait && !c.versionEditApplied 1532 } 1533 d.mu.Unlock() 1534 if !wait { 1535 return 1536 } 1537 // d.mu.compact.cond isn't notified until the compaction 1538 // is removed from inProgress, so we need to just sleep 1539 // and check again soon. 1540 time.Sleep(10 * time.Millisecond) 1541 } 1542 }() 1543 default: 1544 panic(fmt.Sprintf("unrecognized `wait-for-compaction` value: %q", waitFor)) 1545 } 1546 1547 buf.Reset() 1548 fmt.Fprintf(&buf, "L Size Score\n") 1549 for l, lm := range d.Metrics().Levels { 1550 if l < numLevels-1 { 1551 fmt.Fprintf(&buf, "L%-3d\t%-7s%.1f\n", l, humanize.Bytes.Int64(lm.Size), lm.Score) 1552 } else { 1553 fmt.Fprintf(&buf, "L%-3d\t%-7s-\n", l, humanize.Bytes.Int64(lm.Size)) 1554 } 1555 } 1556 return buf.String() 1557 1558 case "wait-pending-table-stats": 1559 return runTableStatsCmd(td, d) 1560 1561 default: 1562 return fmt.Sprintf("unknown command: %s", td.Cmd) 1563 } 1564 }) 1565 } 1566 1567 func fileNums(files manifest.LevelSlice) string { 1568 var ss []string 1569 files.Each(func(f *fileMetadata) { 1570 ss = append(ss, f.FileNum.String()) 1571 }) 1572 sort.Strings(ss) 1573 return strings.Join(ss, ",") 1574 } 1575 1576 func checkClone(t *testing.T, pc *pickedCompaction) { 1577 pcClone := pc.clone() 1578 require.Equal(t, pc.String(), pcClone.String()) 1579 1580 // ensure all input files are in new address 1581 for i := range pc.inputs { 1582 // Len could be zero if setup inputs rejected a level 1583 if pc.inputs[i].files.Len() > 0 { 1584 require.NotEqual(t, &pc.inputs[i], &pcClone.inputs[i]) 1585 } 1586 } 1587 for i := range pc.startLevel.l0SublevelInfo { 1588 if pc.startLevel.l0SublevelInfo[i].Len() > 0 { 1589 require.NotEqual(t, &pc.startLevel.l0SublevelInfo[i], &pcClone.startLevel.l0SublevelInfo[i]) 1590 } 1591 } 1592 }