github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/replay/replay.go (about)

     1  // Copyright 2022 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 replay implements collection and replaying of compaction benchmarking
     6  // workloads. A workload is a collection of flushed and ingested sstables, along
     7  // with the corresponding manifests describing the order and grouping with which
     8  // they were applied. Replaying a workload flushes and ingests the same keys and
     9  // sstables to reproduce the write workload for the purpose of evaluating
    10  // compaction heuristics.
    11  package replay
    12  
    13  import (
    14  	"context"
    15  	"encoding/binary"
    16  	"fmt"
    17  	"io"
    18  	"os"
    19  	"sort"
    20  	"strings"
    21  	"sync"
    22  	"sync/atomic"
    23  	"time"
    24  
    25  	"github.com/cockroachdb/errors"
    26  	"github.com/cockroachdb/pebble"
    27  	"github.com/cockroachdb/pebble/internal/base"
    28  	"github.com/cockroachdb/pebble/internal/bytealloc"
    29  	"github.com/cockroachdb/pebble/internal/manifest"
    30  	"github.com/cockroachdb/pebble/internal/rangedel"
    31  	"github.com/cockroachdb/pebble/internal/rangekey"
    32  	"github.com/cockroachdb/pebble/record"
    33  	"github.com/cockroachdb/pebble/sstable"
    34  	"github.com/cockroachdb/pebble/vfs"
    35  	"golang.org/x/perf/benchfmt"
    36  	"golang.org/x/sync/errgroup"
    37  )
    38  
    39  // A Pacer paces replay of a workload, determining when to apply the next
    40  // incoming write.
    41  type Pacer interface {
    42  	pace(r *Runner, step workloadStep) time.Duration
    43  }
    44  
    45  // computeReadAmp calculates the read amplification from a manifest.Version
    46  func computeReadAmp(v *manifest.Version) int {
    47  	refRAmp := v.L0Sublevels.ReadAmplification()
    48  	for _, lvl := range v.Levels[1:] {
    49  		if !lvl.Empty() {
    50  			refRAmp++
    51  		}
    52  	}
    53  	return refRAmp
    54  }
    55  
    56  // waitForReadAmpLE is a common function used by PaceByReferenceReadAmp and
    57  // PaceByFixedReadAmp to wait on the dbMetricsNotifier condition variable if the
    58  // read amplification observed is greater than the specified target (refRAmp).
    59  func waitForReadAmpLE(r *Runner, rAmp int) {
    60  	r.dbMetricsCond.L.Lock()
    61  	m := r.dbMetrics
    62  	ra := m.ReadAmp()
    63  	for ra > rAmp {
    64  		r.dbMetricsCond.Wait()
    65  		ra = r.dbMetrics.ReadAmp()
    66  	}
    67  	r.dbMetricsCond.L.Unlock()
    68  }
    69  
    70  // Unpaced implements Pacer by applying each new write as soon as possible. It
    71  // may be useful for examining performance under high read amplification.
    72  type Unpaced struct{}
    73  
    74  func (Unpaced) pace(*Runner, workloadStep) (d time.Duration) { return }
    75  
    76  // PaceByReferenceReadAmp implements Pacer by applying each new write following
    77  // the collected workloads read amplification.
    78  type PaceByReferenceReadAmp struct{}
    79  
    80  func (PaceByReferenceReadAmp) pace(r *Runner, w workloadStep) time.Duration {
    81  	startTime := time.Now()
    82  	refRAmp := computeReadAmp(w.pv)
    83  	waitForReadAmpLE(r, refRAmp)
    84  	return time.Since(startTime)
    85  }
    86  
    87  // PaceByFixedReadAmp implements Pacer by applying each new write following a
    88  // fixed read amplification.
    89  type PaceByFixedReadAmp int
    90  
    91  func (pra PaceByFixedReadAmp) pace(r *Runner, _ workloadStep) time.Duration {
    92  	startTime := time.Now()
    93  	waitForReadAmpLE(r, int(pra))
    94  	return time.Since(startTime)
    95  }
    96  
    97  // Metrics holds the various statistics on a replay run and its performance.
    98  type Metrics struct {
    99  	CompactionCounts struct {
   100  		Total       int64
   101  		Default     int64
   102  		DeleteOnly  int64
   103  		ElisionOnly int64
   104  		Move        int64
   105  		Read        int64
   106  		Rewrite     int64
   107  		MultiLevel  int64
   108  	}
   109  	EstimatedDebt SampledMetric
   110  	Final         *pebble.Metrics
   111  	Ingest        struct {
   112  		BytesIntoL0 uint64
   113  		// BytesWeightedByLevel is calculated as the number of bytes ingested
   114  		// into a level multiplied by the level's distance from the bottommost
   115  		// level (L6), summed across all levels. It can be used to guage how
   116  		// effective heuristics are at ingesting files into lower levels, saving
   117  		// write amplification.
   118  		BytesWeightedByLevel uint64
   119  	}
   120  	// PaceDuration is the time waiting for the pacer to allow the workload to
   121  	// continue.
   122  	PaceDuration time.Duration
   123  	ReadAmp      SampledMetric
   124  	// QuiesceDuration is the time between completing application of the workload and
   125  	// compactions quiescing.
   126  	QuiesceDuration time.Duration
   127  	TombstoneCount  SampledMetric
   128  	// TotalSize holds the total size of the database, sampled after each
   129  	// workload step.
   130  	TotalSize           SampledMetric
   131  	TotalWriteAmp       float64
   132  	WorkloadDuration    time.Duration
   133  	WriteBytes          uint64
   134  	WriteStalls         map[string]int
   135  	WriteStallsDuration map[string]time.Duration
   136  	WriteThroughput     SampledMetric
   137  }
   138  
   139  // Plot holds an ascii plot and its name.
   140  type Plot struct {
   141  	Name string
   142  	Plot string
   143  }
   144  
   145  // Plots returns a slice of ascii plots describing metrics change over time.
   146  func (m *Metrics) Plots(width, height int) []Plot {
   147  	const scaleMB = 1.0 / float64(1<<20)
   148  	return []Plot{
   149  		{Name: "Write throughput (MB/s)", Plot: m.WriteThroughput.PlotIncreasingPerSec(width, height, scaleMB)},
   150  		{Name: "Estimated compaction debt (MB)", Plot: m.EstimatedDebt.Plot(width, height, scaleMB)},
   151  		{Name: "Total database size (MB)", Plot: m.TotalSize.Plot(width, height, scaleMB)},
   152  		{Name: "ReadAmp", Plot: m.ReadAmp.Plot(width, height, 1.0)},
   153  	}
   154  }
   155  
   156  // WriteBenchmarkString writes the metrics in the form of a series of
   157  // 'Benchmark' lines understandable by benchstat.
   158  func (m *Metrics) WriteBenchmarkString(name string, w io.Writer) error {
   159  	type benchmarkSection struct {
   160  		label  string
   161  		values []benchfmt.Value
   162  	}
   163  	groups := []benchmarkSection{
   164  		{label: "CompactionCounts", values: []benchfmt.Value{
   165  			{Value: float64(m.CompactionCounts.Total), Unit: "compactions"},
   166  			{Value: float64(m.CompactionCounts.Default), Unit: "default"},
   167  			{Value: float64(m.CompactionCounts.DeleteOnly), Unit: "delete"},
   168  			{Value: float64(m.CompactionCounts.ElisionOnly), Unit: "elision"},
   169  			{Value: float64(m.CompactionCounts.Move), Unit: "move"},
   170  			{Value: float64(m.CompactionCounts.Read), Unit: "read"},
   171  			{Value: float64(m.CompactionCounts.Rewrite), Unit: "rewrite"},
   172  			{Value: float64(m.CompactionCounts.MultiLevel), Unit: "multilevel"},
   173  		}},
   174  		// Total database sizes sampled after every workload step and
   175  		// compaction. This can be used to evaluate the relative LSM space
   176  		// amplification between runs of the same workload. Calculating the true
   177  		// space amplification continuously is prohibitvely expensive (it
   178  		// requires totally compacting a copy of the LSM).
   179  		{label: "DatabaseSize/mean", values: []benchfmt.Value{
   180  			{Value: m.TotalSize.Mean(), Unit: "bytes"},
   181  		}},
   182  		{label: "DatabaseSize/max", values: []benchfmt.Value{
   183  			{Value: float64(m.TotalSize.Max()), Unit: "bytes"},
   184  		}},
   185  		// Time applying the workload and time waiting for compactions to
   186  		// quiesce after the workload has completed.
   187  		{label: "DurationWorkload", values: []benchfmt.Value{
   188  			{Value: m.WorkloadDuration.Seconds(), Unit: "sec/op"},
   189  		}},
   190  		{label: "DurationQuiescing", values: []benchfmt.Value{
   191  			{Value: m.QuiesceDuration.Seconds(), Unit: "sec/op"},
   192  		}},
   193  		{label: "DurationPaceDelay", values: []benchfmt.Value{
   194  			{Value: m.PaceDuration.Seconds(), Unit: "sec/op"},
   195  		}},
   196  		// Estimated compaction debt, sampled after every workload step and
   197  		// compaction.
   198  		{label: "EstimatedDebt/mean", values: []benchfmt.Value{
   199  			{Value: m.EstimatedDebt.Mean(), Unit: "bytes"},
   200  		}},
   201  		{label: "EstimatedDebt/max", values: []benchfmt.Value{
   202  			{Value: float64(m.EstimatedDebt.Max()), Unit: "bytes"},
   203  		}},
   204  		{label: "FlushUtilization", values: []benchfmt.Value{
   205  			{Value: m.Final.Flush.WriteThroughput.Utilization(), Unit: "util"},
   206  		}},
   207  		{label: "IngestedIntoL0", values: []benchfmt.Value{
   208  			{Value: float64(m.Ingest.BytesIntoL0), Unit: "bytes"},
   209  		}},
   210  		{label: "IngestWeightedByLevel", values: []benchfmt.Value{
   211  			{Value: float64(m.Ingest.BytesWeightedByLevel), Unit: "bytes"},
   212  		}},
   213  		{label: "ReadAmp/mean", values: []benchfmt.Value{
   214  			{Value: m.ReadAmp.Mean(), Unit: "files"},
   215  		}},
   216  		{label: "ReadAmp/max", values: []benchfmt.Value{
   217  			{Value: float64(m.ReadAmp.Max()), Unit: "files"},
   218  		}},
   219  		{label: "TombstoneCount/mean", values: []benchfmt.Value{
   220  			{Value: m.TombstoneCount.Mean(), Unit: "tombstones"},
   221  		}},
   222  		{label: "TombstoneCount/max", values: []benchfmt.Value{
   223  			{Value: float64(m.TombstoneCount.Max()), Unit: "tombstones"},
   224  		}},
   225  		{label: "Throughput", values: []benchfmt.Value{
   226  			{Value: float64(m.WriteBytes) / (m.WorkloadDuration + m.QuiesceDuration).Seconds(), Unit: "B/s"},
   227  		}},
   228  		{label: "WriteAmp", values: []benchfmt.Value{
   229  			{Value: float64(m.TotalWriteAmp), Unit: "wamp"},
   230  		}},
   231  	}
   232  
   233  	for _, reason := range []string{"L0", "memtable"} {
   234  		groups = append(groups, benchmarkSection{
   235  			label: fmt.Sprintf("WriteStall/%s", reason),
   236  			values: []benchfmt.Value{
   237  				{Value: float64(m.WriteStalls[reason]), Unit: "stalls"},
   238  				{Value: float64(m.WriteStallsDuration[reason].Seconds()), Unit: "stallsec/op"},
   239  			},
   240  		})
   241  	}
   242  
   243  	bw := benchfmt.NewWriter(w)
   244  	for _, grp := range groups {
   245  		err := bw.Write(&benchfmt.Result{
   246  			Name:   benchfmt.Name(fmt.Sprintf("BenchmarkReplay/%s/%s", name, grp.label)),
   247  			Iters:  1,
   248  			Values: grp.values,
   249  		})
   250  		if err != nil {
   251  			return err
   252  		}
   253  	}
   254  	return nil
   255  }
   256  
   257  // Runner runs a captured workload against a test database, collecting
   258  // metrics on performance.
   259  type Runner struct {
   260  	RunDir        string
   261  	WorkloadFS    vfs.FS
   262  	WorkloadPath  string
   263  	Pacer         Pacer
   264  	Opts          *pebble.Options
   265  	MaxWriteBytes uint64
   266  
   267  	// Internal state.
   268  
   269  	d *pebble.DB
   270  	// dbMetrics and dbMetricsCond work in unison to update the metrics and
   271  	// notify (broadcast) to any waiting clients that metrics have been updated.
   272  	dbMetrics     *pebble.Metrics
   273  	dbMetricsCond sync.Cond
   274  	cancel        func()
   275  	err           atomic.Value
   276  	errgroup      *errgroup.Group
   277  	readerOpts    sstable.ReaderOptions
   278  	stagingDir    string
   279  	steps         chan workloadStep
   280  	stepsApplied  chan workloadStep
   281  
   282  	metrics struct {
   283  		estimatedDebt    SampledMetric
   284  		quiesceDuration  time.Duration
   285  		readAmp          SampledMetric
   286  		tombstoneCount   SampledMetric
   287  		totalSize        SampledMetric
   288  		paceDurationNano atomic.Uint64
   289  		workloadDuration time.Duration
   290  		writeBytes       atomic.Uint64
   291  		writeThroughput  SampledMetric
   292  	}
   293  	writeStallMetrics struct {
   294  		sync.Mutex
   295  		countByReason    map[string]int
   296  		durationByReason map[string]time.Duration
   297  	}
   298  	// compactionMu holds state for tracking the number of compactions
   299  	// started and completed and waking waiting goroutines when a new compaction
   300  	// completes. See nextCompactionCompletes.
   301  	compactionMu struct {
   302  		sync.Mutex
   303  		ch        chan struct{}
   304  		started   int64
   305  		completed int64
   306  	}
   307  	workload struct {
   308  		manifests []string
   309  		// manifest{Idx,Off} record the starting position of the workload
   310  		// relative to the initial database state.
   311  		manifestIdx int
   312  		manifestOff int64
   313  		// sstables records the set of captured workload sstables by file num.
   314  		sstables map[base.FileNum]struct{}
   315  	}
   316  }
   317  
   318  // Run begins executing the workload and returns.
   319  //
   320  // The workload application will respect the provided context's cancellation.
   321  func (r *Runner) Run(ctx context.Context) error {
   322  	// Find the workload start relative to the RunDir's existing database state.
   323  	// A prefix of the workload's manifest edits are expected to have already
   324  	// been applied to the checkpointed existing database state.
   325  	var err error
   326  	r.workload.manifests, r.workload.sstables, err = findWorkloadFiles(r.WorkloadPath, r.WorkloadFS)
   327  	if err != nil {
   328  		return err
   329  	}
   330  	r.workload.manifestIdx, r.workload.manifestOff, err = findManifestStart(r.RunDir, r.Opts.FS, r.workload.manifests)
   331  	if err != nil {
   332  		return err
   333  	}
   334  
   335  	// Set up a staging dir for files that will be ingested.
   336  	r.stagingDir = r.Opts.FS.PathJoin(r.RunDir, "staging")
   337  	if err := r.Opts.FS.MkdirAll(r.stagingDir, os.ModePerm); err != nil {
   338  		return err
   339  	}
   340  
   341  	r.dbMetricsCond = sync.Cond{
   342  		L: &sync.Mutex{},
   343  	}
   344  
   345  	// Extend the user-provided Options with extensions necessary for replay
   346  	// mechanics.
   347  	r.compactionMu.ch = make(chan struct{})
   348  	r.Opts.AddEventListener(r.eventListener())
   349  	r.writeStallMetrics.countByReason = make(map[string]int)
   350  	r.writeStallMetrics.durationByReason = make(map[string]time.Duration)
   351  	r.Opts.EnsureDefaults()
   352  	r.readerOpts = r.Opts.MakeReaderOptions()
   353  	r.Opts.DisableWAL = true
   354  	r.d, err = pebble.Open(r.RunDir, r.Opts)
   355  	if err != nil {
   356  		return err
   357  	}
   358  
   359  	r.dbMetrics = r.d.Metrics()
   360  
   361  	// Use a buffered channel to allow the prepareWorkloadSteps to read ahead,
   362  	// buffering up to cap(r.steps) steps ahead of the current applied state.
   363  	// Flushes need to be buffered and ingested sstables need to be copied, so
   364  	// pipelining this preparation makes it more likely the step will be ready
   365  	// to apply when the pacer decides to apply it.
   366  	r.steps = make(chan workloadStep, 5)
   367  	r.stepsApplied = make(chan workloadStep, 5)
   368  
   369  	ctx, r.cancel = context.WithCancel(ctx)
   370  	r.errgroup, ctx = errgroup.WithContext(ctx)
   371  	r.errgroup.Go(func() error { return r.prepareWorkloadSteps(ctx) })
   372  	r.errgroup.Go(func() error { return r.applyWorkloadSteps(ctx) })
   373  	r.errgroup.Go(func() error { return r.refreshMetrics(ctx) })
   374  	return nil
   375  }
   376  
   377  // refreshMetrics runs in its own goroutine, collecting metrics from the Pebble
   378  // instance whenever a) a workload step completes, or b) a compaction completes.
   379  // The Pacer implementations that pace based on read-amplification rely on these
   380  // refreshed metrics to decide when to allow the workload to proceed.
   381  func (r *Runner) refreshMetrics(ctx context.Context) error {
   382  	startAt := time.Now()
   383  	var workloadExhausted bool
   384  	var workloadExhaustedAt time.Time
   385  	stepsApplied := r.stepsApplied
   386  	compactionCount, alreadyCompleted, compactionCh := r.nextCompactionCompletes(0)
   387  	for {
   388  		if !alreadyCompleted {
   389  			select {
   390  			case <-ctx.Done():
   391  				return ctx.Err()
   392  			case <-compactionCh:
   393  				// Fall through to refreshing dbMetrics.
   394  			case _, ok := <-stepsApplied:
   395  				if !ok {
   396  					workloadExhausted = true
   397  					workloadExhaustedAt = time.Now()
   398  					// Set the [stepsApplied] channel to nil so that we'll never
   399  					// hit this case again, and we don't busy loop.
   400  					stepsApplied = nil
   401  					// Record the replay time.
   402  					r.metrics.workloadDuration = workloadExhaustedAt.Sub(startAt)
   403  				}
   404  				// Fall through to refreshing dbMetrics.
   405  			}
   406  		}
   407  
   408  		m := r.d.Metrics()
   409  		r.dbMetricsCond.L.Lock()
   410  		r.dbMetrics = m
   411  		r.dbMetricsCond.Broadcast()
   412  		r.dbMetricsCond.L.Unlock()
   413  
   414  		// Collect sample metrics. These metrics are calculated by sampling
   415  		// every time we collect metrics.
   416  		r.metrics.readAmp.record(int64(m.ReadAmp()))
   417  		r.metrics.estimatedDebt.record(int64(m.Compact.EstimatedDebt))
   418  		r.metrics.tombstoneCount.record(int64(m.Keys.TombstoneCount))
   419  		r.metrics.totalSize.record(int64(m.DiskSpaceUsage()))
   420  		r.metrics.writeThroughput.record(int64(r.metrics.writeBytes.Load()))
   421  
   422  		compactionCount, alreadyCompleted, compactionCh = r.nextCompactionCompletes(compactionCount)
   423  		// Consider whether replaying is complete. There are two necessary
   424  		// conditions:
   425  		//
   426  		//   1. The workload must be exhausted.
   427  		//   2. Compactions must have quiesced.
   428  		//
   429  		// The first condition is simple. The replay tool is responsible for
   430  		// applying the workload. The goroutine responsible for applying the
   431  		// workload closes the `stepsApplied` channel after the last step has
   432  		// been applied, and we'll flip `workloadExhausted` to true.
   433  		//
   434  		// The second condition is tricky. The replay tool doesn't control
   435  		// compactions and doesn't have visibility into whether the compaction
   436  		// picker is about to schedule a new compaction. We can tell when
   437  		// compactions are in progress or may be immeninent (eg, flushes in
   438  		// progress). If it appears that compactions have quiesced, pause for a
   439  		// fixed duration to see if a new one is scheduled. If not, consider
   440  		// compactions quiesced.
   441  		if workloadExhausted && !alreadyCompleted && r.compactionsAppearQuiesced(m) {
   442  			select {
   443  			case <-compactionCh:
   444  				// A new compaction just finished; compactions have not
   445  				// quiesced.
   446  				continue
   447  			case <-time.After(time.Second):
   448  				// No compactions completed. If it still looks like they've
   449  				// quiesced according to the metrics, consider them quiesced.
   450  				if r.compactionsAppearQuiesced(r.d.Metrics()) {
   451  					r.metrics.quiesceDuration = time.Since(workloadExhaustedAt)
   452  					return nil
   453  				}
   454  			}
   455  		}
   456  	}
   457  }
   458  
   459  // compactionsAppearQuiesced returns true if the database may have quiesced, and
   460  // there likely won't be additional compactions scheduled. Detecting quiescence
   461  // is a bit fraught: The various signals that Pebble makes available are
   462  // adjusted at different points in the compaction lifecycle, and database
   463  // mutexes are dropped and acquired between them. This makes it difficult to
   464  // reliably identify when compactions quiesce.
   465  //
   466  // For example, our call to DB.Metrics() may acquire the DB.mu mutex when a
   467  // compaction has just successfully completed, but before it's managed to
   468  // schedule the next compaction (DB.mu is dropped while it attempts to acquire
   469  // the manifest lock).
   470  func (r *Runner) compactionsAppearQuiesced(m *pebble.Metrics) bool {
   471  	r.compactionMu.Lock()
   472  	defer r.compactionMu.Unlock()
   473  	if m.Flush.NumInProgress > 0 {
   474  		return false
   475  	} else if m.Compact.NumInProgress > 0 && r.compactionMu.started != r.compactionMu.completed {
   476  		return false
   477  	}
   478  	return true
   479  }
   480  
   481  // nextCompactionCompletes may be used to be notified when new compactions
   482  // complete. The caller is responsible for holding on to a monotonically
   483  // increasing count representing the number of compactions that have been
   484  // observed, beginning at zero.
   485  //
   486  // The caller passes their current count as an argument. If a new compaction has
   487  // already completed since their provided count, nextCompactionCompletes returns
   488  // the new count and a true boolean return value. If a new compaction has not
   489  // yet completed, it returns a channel that will be closed when the next
   490  // compaction completes. This scheme allows the caller to select{...},
   491  // performing some action on every compaction completion.
   492  func (r *Runner) nextCompactionCompletes(
   493  	lastObserved int64,
   494  ) (count int64, alreadyOccurred bool, ch chan struct{}) {
   495  	r.compactionMu.Lock()
   496  	defer r.compactionMu.Unlock()
   497  
   498  	if lastObserved < r.compactionMu.completed {
   499  		// There has already been another compaction since the last one observed
   500  		// by this caller. Return immediately.
   501  		return r.compactionMu.completed, true, nil
   502  	}
   503  
   504  	// The last observed compaction is still the most recent compaction.
   505  	// Return a channel that the caller can wait on to be notified when the
   506  	// next compaction occurs.
   507  	if r.compactionMu.ch == nil {
   508  		r.compactionMu.ch = make(chan struct{})
   509  	}
   510  	return lastObserved, false, r.compactionMu.ch
   511  }
   512  
   513  // Wait waits for the workload replay to complete. Wait returns once the entire
   514  // workload has been replayed, and compactions have quiesced.
   515  func (r *Runner) Wait() (Metrics, error) {
   516  	err := r.errgroup.Wait()
   517  	if storedErr := r.err.Load(); storedErr != nil {
   518  		err = storedErr.(error)
   519  	}
   520  	pm := r.d.Metrics()
   521  	total := pm.Total()
   522  	var ingestBytesWeighted uint64
   523  	for l := 0; l < len(pm.Levels); l++ {
   524  		ingestBytesWeighted += pm.Levels[l].BytesIngested * uint64(len(pm.Levels)-l-1)
   525  	}
   526  
   527  	m := Metrics{
   528  		Final:               pm,
   529  		EstimatedDebt:       r.metrics.estimatedDebt,
   530  		PaceDuration:        time.Duration(r.metrics.paceDurationNano.Load()),
   531  		ReadAmp:             r.metrics.readAmp,
   532  		QuiesceDuration:     r.metrics.quiesceDuration,
   533  		TombstoneCount:      r.metrics.tombstoneCount,
   534  		TotalSize:           r.metrics.totalSize,
   535  		TotalWriteAmp:       total.WriteAmp(),
   536  		WorkloadDuration:    r.metrics.workloadDuration,
   537  		WriteBytes:          r.metrics.writeBytes.Load(),
   538  		WriteStalls:         make(map[string]int),
   539  		WriteStallsDuration: make(map[string]time.Duration),
   540  		WriteThroughput:     r.metrics.writeThroughput,
   541  	}
   542  
   543  	r.writeStallMetrics.Lock()
   544  	for reason, count := range r.writeStallMetrics.countByReason {
   545  		m.WriteStalls[reason] = count
   546  	}
   547  	for reason, duration := range r.writeStallMetrics.durationByReason {
   548  		m.WriteStallsDuration[reason] = duration
   549  	}
   550  	r.writeStallMetrics.Unlock()
   551  	m.CompactionCounts.Total = pm.Compact.Count
   552  	m.CompactionCounts.Default = pm.Compact.DefaultCount
   553  	m.CompactionCounts.DeleteOnly = pm.Compact.DeleteOnlyCount
   554  	m.CompactionCounts.ElisionOnly = pm.Compact.ElisionOnlyCount
   555  	m.CompactionCounts.Move = pm.Compact.MoveCount
   556  	m.CompactionCounts.Read = pm.Compact.ReadCount
   557  	m.CompactionCounts.Rewrite = pm.Compact.RewriteCount
   558  	m.CompactionCounts.MultiLevel = pm.Compact.MultiLevelCount
   559  	m.Ingest.BytesIntoL0 = pm.Levels[0].BytesIngested
   560  	m.Ingest.BytesWeightedByLevel = ingestBytesWeighted
   561  	return m, err
   562  }
   563  
   564  // Close closes remaining open resources, including the database. It must be
   565  // called after Wait.
   566  func (r *Runner) Close() error {
   567  	return r.d.Close()
   568  }
   569  
   570  // A workloadStep describes a single manifest edit in the workload. It may be a
   571  // flush or ingest that should be applied to the test database, or it may be a
   572  // compaction that is surfaced to allow the replay logic to compare against the
   573  // state of the database at workload collection time.
   574  type workloadStep struct {
   575  	kind stepKind
   576  	ve   manifest.VersionEdit
   577  	// a Version describing the state of the LSM *before* the workload was
   578  	// collected.
   579  	pv *manifest.Version
   580  	// a Version describing the state of the LSM when the workload was
   581  	// collected.
   582  	v *manifest.Version
   583  	// non-nil for flushStepKind
   584  	flushBatch           *pebble.Batch
   585  	tablesToIngest       []string
   586  	cumulativeWriteBytes uint64
   587  }
   588  
   589  type stepKind uint8
   590  
   591  const (
   592  	flushStepKind stepKind = iota
   593  	ingestStepKind
   594  	compactionStepKind
   595  )
   596  
   597  // eventListener returns a Pebble EventListener that is installed on the replay
   598  // database so that the replay runner has access to internal Pebble events.
   599  func (r *Runner) eventListener() pebble.EventListener {
   600  	var writeStallBegin time.Time
   601  	var writeStallReason string
   602  	l := pebble.EventListener{
   603  		BackgroundError: func(err error) {
   604  			r.err.Store(err)
   605  			r.cancel()
   606  		},
   607  		WriteStallBegin: func(info pebble.WriteStallBeginInfo) {
   608  			r.writeStallMetrics.Lock()
   609  			defer r.writeStallMetrics.Unlock()
   610  			writeStallReason = info.Reason
   611  			// Take just the first word of the reason.
   612  			if j := strings.IndexByte(writeStallReason, ' '); j != -1 {
   613  				writeStallReason = writeStallReason[:j]
   614  			}
   615  			switch writeStallReason {
   616  			case "L0", "memtable":
   617  				r.writeStallMetrics.countByReason[writeStallReason]++
   618  			default:
   619  				panic(fmt.Sprintf("unrecognized write stall reason %q", info.Reason))
   620  			}
   621  			writeStallBegin = time.Now()
   622  		},
   623  		WriteStallEnd: func() {
   624  			r.writeStallMetrics.Lock()
   625  			defer r.writeStallMetrics.Unlock()
   626  			r.writeStallMetrics.durationByReason[writeStallReason] += time.Since(writeStallBegin)
   627  		},
   628  		CompactionBegin: func(_ pebble.CompactionInfo) {
   629  			r.compactionMu.Lock()
   630  			defer r.compactionMu.Unlock()
   631  			r.compactionMu.started++
   632  		},
   633  		CompactionEnd: func(_ pebble.CompactionInfo) {
   634  			// Keep track of the number of compactions that complete and notify
   635  			// anyone waiting for a compaction to complete. See the function
   636  			// nextCompactionCompletes for the corresponding receiver side.
   637  			r.compactionMu.Lock()
   638  			defer r.compactionMu.Unlock()
   639  			r.compactionMu.completed++
   640  			if r.compactionMu.ch != nil {
   641  				// Signal that a compaction has completed.
   642  				close(r.compactionMu.ch)
   643  				r.compactionMu.ch = nil
   644  			}
   645  		},
   646  	}
   647  	l.EnsureDefaults(nil)
   648  	return l
   649  }
   650  
   651  // applyWorkloadSteps runs in its own goroutine, reading workload steps off the
   652  // r.steps channel and applying them to the test database.
   653  func (r *Runner) applyWorkloadSteps(ctx context.Context) error {
   654  	for {
   655  		var ok bool
   656  		var step workloadStep
   657  		select {
   658  		case <-ctx.Done():
   659  			return ctx.Err()
   660  		case step, ok = <-r.steps:
   661  			if !ok {
   662  				// Exhausted the workload. Exit.
   663  				close(r.stepsApplied)
   664  				return nil
   665  			}
   666  		}
   667  
   668  		paceDur := r.Pacer.pace(r, step)
   669  		r.metrics.paceDurationNano.Add(uint64(paceDur))
   670  
   671  		switch step.kind {
   672  		case flushStepKind:
   673  			if err := step.flushBatch.Commit(&pebble.WriteOptions{Sync: false}); err != nil {
   674  				return err
   675  			}
   676  			_, err := r.d.AsyncFlush()
   677  			if err != nil {
   678  				return err
   679  			}
   680  			r.metrics.writeBytes.Store(step.cumulativeWriteBytes)
   681  			r.stepsApplied <- step
   682  		case ingestStepKind:
   683  			if err := r.d.Ingest(step.tablesToIngest); err != nil {
   684  				return err
   685  			}
   686  			r.metrics.writeBytes.Store(step.cumulativeWriteBytes)
   687  			r.stepsApplied <- step
   688  		case compactionStepKind:
   689  			// No-op.
   690  			// TODO(jackson): Should we elide this earlier?
   691  		default:
   692  			panic("unreachable")
   693  		}
   694  	}
   695  }
   696  
   697  // prepareWorkloadSteps runs in its own goroutine, reading the workload
   698  // manifests in order to reconstruct the workload and prepare each step to be
   699  // applied. It sends each workload step to the r.steps channel.
   700  func (r *Runner) prepareWorkloadSteps(ctx context.Context) error {
   701  	defer func() { close(r.steps) }()
   702  
   703  	idx := r.workload.manifestIdx
   704  
   705  	var cumulativeWriteBytes uint64
   706  	var flushBufs flushBuffers
   707  	var v *manifest.Version
   708  	var previousVersion *manifest.Version
   709  	var bve manifest.BulkVersionEdit
   710  	bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata)
   711  	applyVE := func(ve *manifest.VersionEdit) error {
   712  		return bve.Accumulate(ve)
   713  	}
   714  	currentVersion := func() (*manifest.Version, error) {
   715  		var err error
   716  		v, err = bve.Apply(v,
   717  			r.Opts.Comparer.Compare,
   718  			r.Opts.Comparer.FormatKey,
   719  			r.Opts.FlushSplitBytes,
   720  			r.Opts.Experimental.ReadCompactionRate,
   721  			nil, /* zombies */
   722  			manifest.ProhibitSplitUserKeys)
   723  		bve = manifest.BulkVersionEdit{AddedByFileNum: bve.AddedByFileNum}
   724  		return v, err
   725  	}
   726  
   727  	for ; idx < len(r.workload.manifests); idx++ {
   728  		if r.MaxWriteBytes != 0 && cumulativeWriteBytes > r.MaxWriteBytes {
   729  			break
   730  		}
   731  
   732  		err := func() error {
   733  			manifestName := r.workload.manifests[idx]
   734  			f, err := r.WorkloadFS.Open(r.WorkloadFS.PathJoin(r.WorkloadPath, manifestName))
   735  			if err != nil {
   736  				return err
   737  			}
   738  			defer f.Close()
   739  
   740  			rr := record.NewReader(f, 0 /* logNum */)
   741  			// A manifest's first record always holds the initial version state.
   742  			// If this is the first manifest we're examining, we load it in
   743  			// order to seed `metas` with the file metadata of the existing
   744  			// files. Otherwise, we can skip it because we already know all the
   745  			// file metadatas up to this point.
   746  			rec, err := rr.Next()
   747  			if err != nil {
   748  				return err
   749  			}
   750  			if idx == r.workload.manifestIdx {
   751  				var ve manifest.VersionEdit
   752  				if err := ve.Decode(rec); err != nil {
   753  					return err
   754  				}
   755  				if err := applyVE(&ve); err != nil {
   756  					return err
   757  				}
   758  			}
   759  
   760  			// Read the remaining of the manifests version edits, one-by-one.
   761  			for {
   762  				rec, err := rr.Next()
   763  				if err == io.EOF || record.IsInvalidRecord(err) {
   764  					break
   765  				} else if err != nil {
   766  					return err
   767  				}
   768  				var ve manifest.VersionEdit
   769  				if err = ve.Decode(rec); err == io.EOF || record.IsInvalidRecord(err) {
   770  					break
   771  				} else if err != nil {
   772  					return err
   773  				}
   774  				if err := applyVE(&ve); err != nil {
   775  					return err
   776  				}
   777  				if idx == r.workload.manifestIdx && rr.Offset() <= r.workload.manifestOff {
   778  					// The record rec began at an offset strictly less than
   779  					// rr.Offset(), which means it's strictly less than
   780  					// r.workload.manifestOff, and we should skip it.
   781  					continue
   782  				}
   783  				if len(ve.NewFiles) == 0 && len(ve.DeletedFiles) == 0 {
   784  					// Skip WAL rotations and other events that don't affect the
   785  					// files of the LSM.
   786  					continue
   787  				}
   788  
   789  				s := workloadStep{ve: ve}
   790  				if len(ve.DeletedFiles) > 0 {
   791  					// If a version edit deletes files, we assume it's a compaction.
   792  					s.kind = compactionStepKind
   793  				} else {
   794  					// Default to ingest. If any files have unequal
   795  					// smallest,largest sequence numbers, we'll update this to a
   796  					// flush.
   797  					s.kind = ingestStepKind
   798  				}
   799  				var newFiles []base.DiskFileNum
   800  				for _, nf := range ve.NewFiles {
   801  					newFiles = append(newFiles, nf.Meta.FileBacking.DiskFileNum)
   802  					if s.kind == ingestStepKind && (nf.Meta.SmallestSeqNum != nf.Meta.LargestSeqNum || nf.Level != 0) {
   803  						s.kind = flushStepKind
   804  					}
   805  				}
   806  				// Add the current reference *Version to the step. This provides
   807  				// access to, for example, the read-amplification of the
   808  				// database at this point when the workload was collected. This
   809  				// can be useful for pacing.
   810  				if s.v, err = currentVersion(); err != nil {
   811  					return err
   812  				}
   813  				// On the first time through, we set the previous version to the current
   814  				// version otherwise we set it to the actual previous version.
   815  				if previousVersion == nil {
   816  					previousVersion = s.v
   817  				}
   818  				s.pv = previousVersion
   819  				previousVersion = s.v
   820  
   821  				// It's possible that the workload collector captured this
   822  				// version edit, but wasn't able to collect all of the
   823  				// corresponding sstables before being terminated.
   824  				if s.kind == flushStepKind || s.kind == ingestStepKind {
   825  					for _, fileNum := range newFiles {
   826  						if _, ok := r.workload.sstables[fileNum.FileNum()]; !ok {
   827  							// TODO(jackson,leon): This isn't exactly an error
   828  							// condition. Give this more thought; do we want to
   829  							// require graceful exiting of workload collection,
   830  							// such that the last version edit must have had its
   831  							// corresponding sstables collected?
   832  							return errors.Newf("sstable %s not found", fileNum)
   833  						}
   834  					}
   835  				}
   836  
   837  				switch s.kind {
   838  				case flushStepKind:
   839  					// Load all of the flushed sstables' keys into a batch.
   840  					s.flushBatch = r.d.NewBatch()
   841  					if err := loadFlushedSSTableKeys(s.flushBatch, r.WorkloadFS, r.WorkloadPath, newFiles, r.readerOpts, &flushBufs); err != nil {
   842  						return errors.Wrapf(err, "flush in %q at offset %d", manifestName, rr.Offset())
   843  					}
   844  					cumulativeWriteBytes += uint64(s.flushBatch.Len())
   845  				case ingestStepKind:
   846  					// Copy the ingested sstables into a staging area within the
   847  					// run dir. This is necessary for two reasons:
   848  					//  a) Ingest will remove the source file, and we don't want
   849  					//     to mutate the workload.
   850  					//  b) If the workload stored on another volume, Ingest
   851  					//     would need to fall back to copying the file since
   852  					//     it's not possible to link across volumes. The true
   853  					//     workload likely linked the file. Staging the file
   854  					//     ahead of time ensures that we're able to Link the
   855  					//     file like the original workload did.
   856  					for _, fileNum := range newFiles {
   857  						src := base.MakeFilepath(r.WorkloadFS, r.WorkloadPath, base.FileTypeTable, fileNum)
   858  						dst := base.MakeFilepath(r.Opts.FS, r.stagingDir, base.FileTypeTable, fileNum)
   859  						if err := vfs.CopyAcrossFS(r.WorkloadFS, src, r.Opts.FS, dst); err != nil {
   860  							return errors.Wrapf(err, "ingest in %q at offset %d", manifestName, rr.Offset())
   861  						}
   862  						finfo, err := r.Opts.FS.Stat(dst)
   863  						if err != nil {
   864  							return errors.Wrapf(err, "stating %q", dst)
   865  						}
   866  						cumulativeWriteBytes += uint64(finfo.Size())
   867  						s.tablesToIngest = append(s.tablesToIngest, dst)
   868  					}
   869  				case compactionStepKind:
   870  					// Nothing to do.
   871  				}
   872  				s.cumulativeWriteBytes = cumulativeWriteBytes
   873  
   874  				select {
   875  				case <-ctx.Done():
   876  					return ctx.Err()
   877  				case r.steps <- s:
   878  				}
   879  
   880  				if r.MaxWriteBytes != 0 && cumulativeWriteBytes > r.MaxWriteBytes {
   881  					break
   882  				}
   883  			}
   884  			return nil
   885  		}()
   886  		if err != nil {
   887  			return err
   888  		}
   889  	}
   890  	return nil
   891  }
   892  
   893  // findWorkloadFiles finds all manifests and tables in the provided path on fs.
   894  func findWorkloadFiles(
   895  	path string, fs vfs.FS,
   896  ) (manifests []string, sstables map[base.FileNum]struct{}, err error) {
   897  	dirents, err := fs.List(path)
   898  	if err != nil {
   899  		return nil, nil, err
   900  	}
   901  	sstables = make(map[base.FileNum]struct{})
   902  	for _, dirent := range dirents {
   903  		typ, fileNum, ok := base.ParseFilename(fs, dirent)
   904  		if !ok {
   905  			continue
   906  		}
   907  		switch typ {
   908  		case base.FileTypeManifest:
   909  			manifests = append(manifests, dirent)
   910  		case base.FileTypeTable:
   911  			sstables[fileNum.FileNum()] = struct{}{}
   912  		}
   913  	}
   914  	if len(manifests) == 0 {
   915  		return nil, nil, errors.Newf("no manifests found")
   916  	}
   917  	sort.Strings(manifests)
   918  	return manifests, sstables, err
   919  }
   920  
   921  // findManifestStart takes a database directory and FS containing the initial
   922  // database state that a workload will be run against, and a list of a workloads
   923  // manifests. It examines the database's current manifest to determine where
   924  // workload replay should begin, so as to not duplicate already-applied version
   925  // edits.
   926  //
   927  // It returns the index of the starting manifest, and the database's current
   928  // offset within the manifest.
   929  func findManifestStart(
   930  	dbDir string, dbFS vfs.FS, manifests []string,
   931  ) (index int, offset int64, err error) {
   932  	// Identify the database's current manifest.
   933  	dbDesc, err := pebble.Peek(dbDir, dbFS)
   934  	if err != nil {
   935  		return 0, 0, err
   936  	}
   937  	dbManifest := dbFS.PathBase(dbDesc.ManifestFilename)
   938  	// If there is no initial database state, begin workload replay from the
   939  	// beginning of the first manifest.
   940  	if !dbDesc.Exists {
   941  		return 0, 0, nil
   942  	}
   943  	for index = 0; index < len(manifests); index++ {
   944  		if manifests[index] == dbManifest {
   945  			break
   946  		}
   947  	}
   948  	if index == len(manifests) {
   949  		// The initial database state has a manifest that does not appear within
   950  		// the workload's set of manifests. This is possible if we began
   951  		// recording the workload at the same time as a manifest rotation, but
   952  		// more likely we're applying a workload to a different initial database
   953  		// state than the one from which the workload was collected. Either way,
   954  		// start from the beginning of the first manifest.
   955  		return 0, 0, nil
   956  	}
   957  	// Find the initial database's offset within the manifest.
   958  	info, err := dbFS.Stat(dbFS.PathJoin(dbDir, dbManifest))
   959  	if err != nil {
   960  		return 0, 0, err
   961  	}
   962  	return index, info.Size(), nil
   963  }
   964  
   965  // loadFlushedSSTableKeys copies keys from the sstables specified by `fileNums`
   966  // in the directory specified by `path` into the provided the batch. Keys are
   967  // applied to the batch in the order dictated by their sequence numbers within
   968  // the sstables, ensuring the relative relationship between sequence numbers is
   969  // maintained.
   970  //
   971  // Preserving the relative relationship between sequence numbers is not strictly
   972  // necessary, but it ensures we accurately exercise some microoptimizations (eg,
   973  // detecting user key changes by descending trailer). There may be additional
   974  // dependencies on sequence numbers in the future.
   975  func loadFlushedSSTableKeys(
   976  	b *pebble.Batch,
   977  	fs vfs.FS,
   978  	path string,
   979  	fileNums []base.DiskFileNum,
   980  	readOpts sstable.ReaderOptions,
   981  	bufs *flushBuffers,
   982  ) error {
   983  	// Load all the keys across all the sstables.
   984  	for _, fileNum := range fileNums {
   985  		if err := func() error {
   986  			filePath := base.MakeFilepath(fs, path, base.FileTypeTable, fileNum)
   987  			f, err := fs.Open(filePath)
   988  			if err != nil {
   989  				return err
   990  			}
   991  			readable, err := sstable.NewSimpleReadable(f)
   992  			if err != nil {
   993  				f.Close()
   994  				return err
   995  			}
   996  			r, err := sstable.NewReader(readable, readOpts)
   997  			if err != nil {
   998  				return err
   999  			}
  1000  			defer r.Close()
  1001  
  1002  			// Load all the point keys.
  1003  			iter, err := r.NewIter(nil, nil)
  1004  			if err != nil {
  1005  				return err
  1006  			}
  1007  			defer iter.Close()
  1008  			for k, lv := iter.First(); k != nil; k, lv = iter.Next() {
  1009  				var key flushedKey
  1010  				key.Trailer = k.Trailer
  1011  				bufs.alloc, key.UserKey = bufs.alloc.Copy(k.UserKey)
  1012  				if v, callerOwned, err := lv.Value(nil); err != nil {
  1013  					return err
  1014  				} else if callerOwned {
  1015  					key.value = v
  1016  				} else {
  1017  					bufs.alloc, key.value = bufs.alloc.Copy(v)
  1018  				}
  1019  				bufs.keys = append(bufs.keys, key)
  1020  			}
  1021  
  1022  			// Load all the range tombstones.
  1023  			if iter, err := r.NewRawRangeDelIter(); err != nil {
  1024  				return err
  1025  			} else if iter != nil {
  1026  				defer iter.Close()
  1027  				for s := iter.First(); s != nil; s = iter.Next() {
  1028  					if err := rangedel.Encode(s, func(k base.InternalKey, v []byte) error {
  1029  						var key flushedKey
  1030  						key.Trailer = k.Trailer
  1031  						bufs.alloc, key.UserKey = bufs.alloc.Copy(k.UserKey)
  1032  						bufs.alloc, key.value = bufs.alloc.Copy(v)
  1033  						bufs.keys = append(bufs.keys, key)
  1034  						return nil
  1035  					}); err != nil {
  1036  						return err
  1037  					}
  1038  				}
  1039  			}
  1040  
  1041  			// Load all the range keys.
  1042  			if iter, err := r.NewRawRangeKeyIter(); err != nil {
  1043  				return err
  1044  			} else if iter != nil {
  1045  				defer iter.Close()
  1046  				for s := iter.First(); s != nil; s = iter.Next() {
  1047  					if err := rangekey.Encode(s, func(k base.InternalKey, v []byte) error {
  1048  						var key flushedKey
  1049  						key.Trailer = k.Trailer
  1050  						bufs.alloc, key.UserKey = bufs.alloc.Copy(k.UserKey)
  1051  						bufs.alloc, key.value = bufs.alloc.Copy(v)
  1052  						bufs.keys = append(bufs.keys, key)
  1053  						return nil
  1054  					}); err != nil {
  1055  						return err
  1056  					}
  1057  				}
  1058  			}
  1059  			return nil
  1060  		}(); err != nil {
  1061  			return err
  1062  		}
  1063  	}
  1064  
  1065  	// Sort the flushed keys by their sequence numbers so that we can apply them
  1066  	// to the batch in the same order, maintaining the relative relationship
  1067  	// between keys.
  1068  	// NB: We use a stable sort so that keys corresponding to span fragments
  1069  	// (eg, range tombstones and range keys) have a deterministic ordering for
  1070  	// testing.
  1071  	sort.Stable(bufs.keys)
  1072  
  1073  	// Add the keys to the batch in the order they were committed when the
  1074  	// workload was captured.
  1075  	for i := 0; i < len(bufs.keys); i++ {
  1076  		var err error
  1077  		switch bufs.keys[i].Kind() {
  1078  		case base.InternalKeyKindDelete:
  1079  			err = b.Delete(bufs.keys[i].UserKey, nil)
  1080  		case base.InternalKeyKindDeleteSized:
  1081  			v, _ := binary.Uvarint(bufs.keys[i].value)
  1082  			// Batch.DeleteSized takes just the length of the value being
  1083  			// deleted and adds the key's length to derive the overall entry
  1084  			// size of the value being deleted. This has already been done to
  1085  			// the key we're reading from the sstable, so we must subtract the
  1086  			// key length from the encoded value before calling b.DeleteSized,
  1087  			// which will again add the key length before encoding.
  1088  			err = b.DeleteSized(bufs.keys[i].UserKey, uint32(v-uint64(len(bufs.keys[i].UserKey))), nil)
  1089  		case base.InternalKeyKindSet, base.InternalKeyKindSetWithDelete:
  1090  			err = b.Set(bufs.keys[i].UserKey, bufs.keys[i].value, nil)
  1091  		case base.InternalKeyKindMerge:
  1092  			err = b.Merge(bufs.keys[i].UserKey, bufs.keys[i].value, nil)
  1093  		case base.InternalKeyKindSingleDelete:
  1094  			err = b.SingleDelete(bufs.keys[i].UserKey, nil)
  1095  		case base.InternalKeyKindRangeDelete:
  1096  			err = b.DeleteRange(bufs.keys[i].UserKey, bufs.keys[i].value, nil)
  1097  		case base.InternalKeyKindRangeKeySet, base.InternalKeyKindRangeKeyUnset, base.InternalKeyKindRangeKeyDelete:
  1098  			s, err := rangekey.Decode(bufs.keys[i].InternalKey, bufs.keys[i].value, nil)
  1099  			if err != nil {
  1100  				return err
  1101  			}
  1102  			if len(s.Keys) != 1 {
  1103  				return errors.Newf("range key span unexpectedly contains %d keys", len(s.Keys))
  1104  			}
  1105  			switch bufs.keys[i].Kind() {
  1106  			case base.InternalKeyKindRangeKeySet:
  1107  				err = b.RangeKeySet(s.Start, s.End, s.Keys[0].Suffix, s.Keys[0].Value, nil)
  1108  			case base.InternalKeyKindRangeKeyUnset:
  1109  				err = b.RangeKeyUnset(s.Start, s.End, s.Keys[0].Suffix, nil)
  1110  			case base.InternalKeyKindRangeKeyDelete:
  1111  				err = b.RangeKeyDelete(s.Start, s.End, nil)
  1112  			default:
  1113  				err = errors.Newf("unexpected key kind %q", bufs.keys[i].Kind())
  1114  			}
  1115  			if err != nil {
  1116  				return err
  1117  			}
  1118  		default:
  1119  			err = errors.Newf("unexpected key kind %q", bufs.keys[i].Kind())
  1120  		}
  1121  		if err != nil {
  1122  			return err
  1123  		}
  1124  	}
  1125  
  1126  	// Done with the flushBuffers. Reset.
  1127  	bufs.keys = bufs.keys[:0]
  1128  	return nil
  1129  }
  1130  
  1131  type flushBuffers struct {
  1132  	keys  flushedKeysByTrailer
  1133  	alloc bytealloc.A
  1134  }
  1135  
  1136  type flushedKeysByTrailer []flushedKey
  1137  
  1138  func (s flushedKeysByTrailer) Len() int           { return len(s) }
  1139  func (s flushedKeysByTrailer) Less(i, j int) bool { return s[i].Trailer < s[j].Trailer }
  1140  func (s flushedKeysByTrailer) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
  1141  
  1142  type flushedKey struct {
  1143  	base.InternalKey
  1144  	value []byte
  1145  }