Skip to content

Commit e36d078

Browse files
manifest: improve Annotator interface with generics
Refactors `manifest.Annotator` to use generics and a simplified API. This eliminates the need to perform pointer manipulation and unsafe typecasting when defining a new Annotator. The goal of this change is to improve the `Annotator` interface while not changing any existing behavior. `BenchmarkNumFilesAnnotator` shows roughly the same performance as master when compared to the equivalent implementation of `orderStatistic`: ``` pkg: github.com/cockroachdb/pebble/internal/manifest │ old │ new │ │ sec/op │ sec/op vs base │ NumFilesAnnotator-10 1.635µ ± 1% 1.572µ ± 7% ~ (p=0.065 n=6) │ old │ new │ │ B/op │ B/op vs base │ NumFilesAnnotator-10 536.0 ± 0% 536.0 ± 0% ~ (p=1.000 n=6) ¹ ¹ all samples are equal │ old │ new │ │ allocs/op │ allocs/op vs base │ NumFilesAnnotator-10 7.000 ± 0% 7.000 ± 0% ~ (p=1.000 n=6) ¹ ¹ all samples are equal ```
1 parent 1cedb60 commit e36d078

File tree

11 files changed

+436
-536
lines changed

11 files changed

+436
-536
lines changed

compaction_picker.go

Lines changed: 62 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -639,39 +639,13 @@ func compensatedSize(f *fileMetadata) uint64 {
639639
return f.Size + fileCompensation(f)
640640
}
641641

642-
// compensatedSizeAnnotator implements manifest.Annotator, annotating B-Tree
643-
// nodes with the sum of the files' compensated sizes. Its annotation type is
644-
// a *uint64. Compensated sizes may change once a table's stats are loaded
645-
// asynchronously, so its values are marked as cacheable only if a file's
646-
// stats have been loaded.
647-
type compensatedSizeAnnotator struct {
648-
}
649-
650-
var _ manifest.Annotator = compensatedSizeAnnotator{}
651-
652-
func (a compensatedSizeAnnotator) Zero(dst interface{}) interface{} {
653-
if dst == nil {
654-
return new(uint64)
655-
}
656-
v := dst.(*uint64)
657-
*v = 0
658-
return v
659-
}
660-
661-
func (a compensatedSizeAnnotator) Accumulate(
662-
f *fileMetadata, dst interface{},
663-
) (v interface{}, cacheOK bool) {
664-
vptr := dst.(*uint64)
665-
*vptr = *vptr + compensatedSize(f)
666-
return vptr, f.StatsValid()
667-
}
668-
669-
func (a compensatedSizeAnnotator) Merge(src interface{}, dst interface{}) interface{} {
670-
srcV := src.(*uint64)
671-
dstV := dst.(*uint64)
672-
*dstV = *dstV + *srcV
673-
return dstV
674-
}
642+
// compensatedSizeAnnotator is a manifest.Annotator that annotates B-Tree
643+
// nodes with the sum of the files' compensated sizes. Compensated sizes may
644+
// change once a table's stats are loaded asynchronously, so its values are
645+
// marked as cacheable only if a file's stats have been loaded.
646+
var compensatedSizeAnnotator = manifest.SumAnnotator(func(f *fileMetadata) (uint64, bool) {
647+
return compensatedSize(f), f.StatsValid()
648+
})
675649

676650
// totalCompensatedSize computes the compensated size over a file metadata
677651
// iterator. Note that this function is linear in the files available to the
@@ -912,10 +886,6 @@ func calculateSizeAdjust(inProgressCompactions []compactionInfo) [numLevels]leve
912886
return sizeAdjust
913887
}
914888

915-
func levelCompensatedSize(lm manifest.LevelMetadata) uint64 {
916-
return *lm.Annotation(compensatedSizeAnnotator{}).(*uint64)
917-
}
918-
919889
func (p *compactionPickerByScore) calculateLevelScores(
920890
inProgressCompactions []compactionInfo,
921891
) [numLevels]candidateLevelInfo {
@@ -932,7 +902,7 @@ func (p *compactionPickerByScore) calculateLevelScores(
932902
}
933903
sizeAdjust := calculateSizeAdjust(inProgressCompactions)
934904
for level := 1; level < numLevels; level++ {
935-
compensatedLevelSize := levelCompensatedSize(p.vers.Levels[level]) + sizeAdjust[level].compensated()
905+
compensatedLevelSize := *compensatedSizeAnnotator.LevelAnnotation(p.vers.Levels[level]) + sizeAdjust[level].compensated()
936906
scores[level].compensatedScore = float64(compensatedLevelSize) / float64(p.levelMaxBytes[level])
937907
scores[level].uncompensatedScore = float64(p.vers.Levels[level].Size()+sizeAdjust[level].actual()) / float64(p.levelMaxBytes[level])
938908
}
@@ -1393,109 +1363,56 @@ func (p *compactionPickerByScore) addScoresToPickedCompactionMetrics(
13931363
}
13941364
}
13951365

1396-
// elisionOnlyAnnotator implements the manifest.Annotator interface,
1397-
// annotating B-Tree nodes with the *fileMetadata of a file meeting the
1398-
// obsolete keys criteria for an elision-only compaction within the subtree.
1399-
// If multiple files meet the criteria, it chooses whichever file has the
1400-
// lowest LargestSeqNum. The lowest LargestSeqNum file will be the first
1401-
// eligible for an elision-only compaction once snapshots less than or equal
1402-
// to its LargestSeqNum are closed.
1403-
type elisionOnlyAnnotator struct{}
1404-
1405-
var _ manifest.Annotator = elisionOnlyAnnotator{}
1406-
1407-
func (a elisionOnlyAnnotator) Zero(interface{}) interface{} {
1408-
return nil
1409-
}
1410-
1411-
func (a elisionOnlyAnnotator) Accumulate(f *fileMetadata, dst interface{}) (interface{}, bool) {
1412-
if f.IsCompacting() {
1413-
return dst, true
1414-
}
1415-
if !f.StatsValid() {
1416-
return dst, false
1417-
}
1418-
// Bottommost files are large and not worthwhile to compact just
1419-
// to remove a few tombstones. Consider a file ineligible if its
1420-
// own range deletions delete less than 10% of its data and its
1421-
// deletion tombstones make up less than 10% of its entries.
1422-
//
1423-
// TODO(jackson): This does not account for duplicate user keys
1424-
// which may be collapsed. Ideally, we would have 'obsolete keys'
1425-
// statistics that would include tombstones, the keys that are
1426-
// dropped by tombstones and duplicated user keys. See #847.
1427-
//
1428-
// Note that tables that contain exclusively range keys (i.e. no point keys,
1429-
// `NumEntries` and `RangeDeletionsBytesEstimate` are both zero) are excluded
1430-
// from elision-only compactions.
1431-
// TODO(travers): Consider an alternative heuristic for elision of range-keys.
1432-
if f.Stats.RangeDeletionsBytesEstimate*10 < f.Size &&
1433-
f.Stats.NumDeletions*10 <= f.Stats.NumEntries {
1434-
return dst, true
1435-
}
1436-
if dst == nil {
1437-
return f, true
1438-
} else if dstV := dst.(*fileMetadata); dstV.LargestSeqNum > f.LargestSeqNum {
1439-
return f, true
1440-
}
1441-
return dst, true
1442-
}
1443-
1444-
func (a elisionOnlyAnnotator) Merge(v interface{}, accum interface{}) interface{} {
1445-
if v == nil {
1446-
return accum
1447-
}
1448-
// If we haven't accumulated an eligible file yet, or f's LargestSeqNum is
1449-
// less than the accumulated file's, use f.
1450-
if accum == nil {
1451-
return v
1452-
}
1453-
f := v.(*fileMetadata)
1454-
accumV := accum.(*fileMetadata)
1455-
if accumV == nil || accumV.LargestSeqNum > f.LargestSeqNum {
1456-
return f
1457-
}
1458-
return accumV
1459-
}
1460-
1461-
// markedForCompactionAnnotator implements the manifest.Annotator interface,
1462-
// annotating B-Tree nodes with the *fileMetadata of a file that is marked for
1463-
// compaction within the subtree. If multiple files meet the criteria, it
1464-
// chooses whichever file has the lowest LargestSeqNum.
1465-
type markedForCompactionAnnotator struct{}
1466-
1467-
var _ manifest.Annotator = markedForCompactionAnnotator{}
1468-
1469-
func (a markedForCompactionAnnotator) Zero(interface{}) interface{} {
1470-
return nil
1471-
}
1472-
1473-
func (a markedForCompactionAnnotator) Accumulate(
1474-
f *fileMetadata, dst interface{},
1475-
) (interface{}, bool) {
1476-
if !f.MarkedForCompaction {
1477-
// Not marked for compaction; return dst.
1478-
return dst, true
1479-
}
1480-
return markedMergeHelper(f, dst)
1481-
}
1482-
1483-
func (a markedForCompactionAnnotator) Merge(v interface{}, accum interface{}) interface{} {
1484-
if v == nil {
1485-
return accum
1486-
}
1487-
accum, _ = markedMergeHelper(v.(*fileMetadata), accum)
1488-
return accum
1489-
}
1490-
1491-
// REQUIRES: f is non-nil, and f.MarkedForCompaction=true.
1492-
func markedMergeHelper(f *fileMetadata, dst interface{}) (interface{}, bool) {
1493-
if dst == nil {
1494-
return f, true
1495-
} else if dstV := dst.(*fileMetadata); dstV.LargestSeqNum > f.LargestSeqNum {
1496-
return f, true
1497-
}
1498-
return dst, true
1366+
// elisionOnlyAnnotator is a manifest.Annotator that annotates B-Tree
1367+
// nodes with the *fileMetadata of a file meeting the obsolete keys criteria
1368+
// for an elision-only compaction within the subtree. If multiple files meet
1369+
// the criteria, it chooses whichever file has the lowest LargestSeqNum. The
1370+
// lowest LargestSeqNum file will be the first eligible for an elision-only
1371+
// compaction once snapshots less than or equal to its LargestSeqNum are closed.
1372+
var elisionOnlyAnnotator = &manifest.Annotator[fileMetadata]{
1373+
Aggregator: manifest.PickFileAggregator{
1374+
Filter: func(f *fileMetadata) (eligible bool, cacheOK bool) {
1375+
if f.IsCompacting() {
1376+
return false, true
1377+
}
1378+
if !f.StatsValid() {
1379+
return false, false
1380+
}
1381+
// Bottommost files are large and not worthwhile to compact just
1382+
// to remove a few tombstones. Consider a file eligible only if
1383+
// either its own range deletions delete at least 10% of its data or
1384+
// its deletion tombstones make at least 10% of its entries.
1385+
//
1386+
// TODO(jackson): This does not account for duplicate user keys
1387+
// which may be collapsed. Ideally, we would have 'obsolete keys'
1388+
// statistics that would include tombstones, the keys that are
1389+
// dropped by tombstones and duplicated user keys. See #847.
1390+
//
1391+
// Note that tables that contain exclusively range keys (i.e. no point keys,
1392+
// `NumEntries` and `RangeDeletionsBytesEstimate` are both zero) are excluded
1393+
// from elision-only compactions.
1394+
// TODO(travers): Consider an alternative heuristic for elision of range-keys.
1395+
return f.Stats.RangeDeletionsBytesEstimate*10 >= f.Size || f.Stats.NumDeletions*10 > f.Stats.NumEntries, true
1396+
},
1397+
Compare: func(f1 *fileMetadata, f2 *fileMetadata) bool {
1398+
return f1.LargestSeqNum < f2.LargestSeqNum
1399+
},
1400+
},
1401+
}
1402+
1403+
// markedForCompactionAnnotator is a manifest.Annotator that annotates B-Tree
1404+
// nodes with the *fileMetadata of a file that is marked for compaction
1405+
// within the subtree. If multiple files meet the criteria, it chooses
1406+
// whichever file has the lowest LargestSeqNum.
1407+
var markedForCompactionAnnotator = &manifest.Annotator[fileMetadata]{
1408+
Aggregator: manifest.PickFileAggregator{
1409+
Filter: func(f *fileMetadata) (eligible bool, cacheOK bool) {
1410+
return f.MarkedForCompaction, true
1411+
},
1412+
Compare: func(f1 *fileMetadata, f2 *fileMetadata) bool {
1413+
return f1.LargestSeqNum < f2.LargestSeqNum
1414+
},
1415+
},
14991416
}
15001417

15011418
// pickElisionOnlyCompaction looks for compactions of sstables in the
@@ -1506,11 +1423,10 @@ func (p *compactionPickerByScore) pickElisionOnlyCompaction(
15061423
if p.opts.private.disableElisionOnlyCompactions {
15071424
return nil
15081425
}
1509-
v := p.vers.Levels[numLevels-1].Annotation(elisionOnlyAnnotator{})
1510-
if v == nil {
1426+
candidate := elisionOnlyAnnotator.LevelAnnotation(p.vers.Levels[numLevels-1])
1427+
if candidate == nil {
15111428
return nil
15121429
}
1513-
candidate := v.(*fileMetadata)
15141430
if candidate.IsCompacting() || candidate.LargestSeqNum >= env.earliestSnapshotSeqNum {
15151431
return nil
15161432
}
@@ -1542,12 +1458,11 @@ func (p *compactionPickerByScore) pickElisionOnlyCompaction(
15421458
// the input level.
15431459
func (p *compactionPickerByScore) pickRewriteCompaction(env compactionEnv) (pc *pickedCompaction) {
15441460
for l := numLevels - 1; l >= 0; l-- {
1545-
v := p.vers.Levels[l].Annotation(markedForCompactionAnnotator{})
1546-
if v == nil {
1461+
candidate := markedForCompactionAnnotator.LevelAnnotation(p.vers.Levels[l])
1462+
if candidate == nil {
15471463
// Try the next level.
15481464
continue
15491465
}
1550-
candidate := v.(*fileMetadata)
15511466
if candidate.IsCompacting() {
15521467
// Try the next level.
15531468
continue

compaction_picker_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ func TestCompactionPickerL0(t *testing.T) {
563563
}
564564
f.MarkedForCompaction = true
565565
picker.vers.Stats.MarkedForCompaction++
566-
picker.vers.Levels[l].InvalidateAnnotation(markedForCompactionAnnotator{})
566+
markedForCompactionAnnotator.InvalidateLevelAnnotation(picker.vers.Levels[l])
567567
return fmt.Sprintf("marked L%d.%s", l, f.FileNum)
568568
}
569569
}

compaction_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2504,7 +2504,7 @@ func TestMarkedForCompaction(t *testing.T) {
25042504
}
25052505
f.MarkedForCompaction = true
25062506
vers.Stats.MarkedForCompaction++
2507-
vers.Levels[l].InvalidateAnnotation(markedForCompactionAnnotator{})
2507+
markedForCompactionAnnotator.InvalidateLevelAnnotation(vers.Levels[l])
25082508
return fmt.Sprintf("marked L%d.%s", l, f.FileNum)
25092509
}
25102510
}

db.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,8 +1995,8 @@ func (d *DB) Metrics() *Metrics {
19951995
metrics.private.optionsFileSize = d.optionsFileSize
19961996

19971997
// TODO(jackson): Consider making these metrics optional.
1998-
metrics.Keys.RangeKeySetsCount = countRangeKeySetFragments(vers)
1999-
metrics.Keys.TombstoneCount = countTombstones(vers)
1998+
metrics.Keys.RangeKeySetsCount = *rangeKeySetsAnnotator.MultiLevelAnnotation(vers.RangeKeyLevels[:])
1999+
metrics.Keys.TombstoneCount = *tombstonesAnnotator.MultiLevelAnnotation(vers.Levels[:])
20002000

20012001
d.mu.versions.logLock()
20022002
metrics.private.manifestFileSize = uint64(d.mu.versions.manifest.Size())
@@ -2014,12 +2014,12 @@ func (d *DB) Metrics() *Metrics {
20142014
metrics.Flush.NumInProgress = 1
20152015
}
20162016
for i := 0; i < numLevels; i++ {
2017-
metrics.Levels[i].Additional.ValueBlocksSize = valueBlocksSizeForLevel(vers, i)
2018-
unknown, snappy, none, zstd := compressionTypesForLevel(vers, i)
2019-
metrics.Table.CompressedCountUnknown += int64(unknown)
2020-
metrics.Table.CompressedCountSnappy += int64(snappy)
2021-
metrics.Table.CompressedCountZstd += int64(zstd)
2022-
metrics.Table.CompressedCountNone += int64(none)
2017+
metrics.Levels[i].Additional.ValueBlocksSize = *valueBlockSizeAnnotator.LevelAnnotation(vers.Levels[i])
2018+
compressionTypes := compressionTypeAnnotator.LevelAnnotation(vers.Levels[i])
2019+
metrics.Table.CompressedCountUnknown += int64(compressionTypes.unknown)
2020+
metrics.Table.CompressedCountSnappy += int64(compressionTypes.snappy)
2021+
metrics.Table.CompressedCountZstd += int64(compressionTypes.zstd)
2022+
metrics.Table.CompressedCountNone += int64(compressionTypes.none)
20232023
}
20242024

20252025
d.mu.Unlock()

format_major_version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ func (d *DB) markFilesLocked(findFn findFilesFunc) error {
517517
// annotations will be out of date. Clear the compaction-picking
518518
// annotation, so that it's recomputed the next time the compaction
519519
// picker looks for a file marked for compaction.
520-
vers.Levels[l].InvalidateAnnotation(markedForCompactionAnnotator{})
520+
markedForCompactionAnnotator.InvalidateLevelAnnotation(vers.Levels[l])
521521
}
522522

523523
// The 'marked-for-compaction' bit is persisted in the MANIFEST file

0 commit comments

Comments
 (0)