github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/pacer_test.go (about)

     1  // Copyright 2019 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  	"fmt"
     9  	"math"
    10  	"math/rand"
    11  	"slices"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestDeletionPacer(t *testing.T) {
    19  	const MB = 1 << 20
    20  	const GB = 1 << 30
    21  	testCases := []struct {
    22  		freeBytes     uint64
    23  		obsoleteBytes uint64
    24  		liveBytes     uint64
    25  		// history of deletion reporting; first value in the pair is the time,
    26  		// second value is the deleted bytes. The time of pacing is the same as the
    27  		// last time in the history.
    28  		history [][2]int64
    29  		// expected pacing rate in MB/s.
    30  		expected float64
    31  	}{
    32  		{
    33  			freeBytes:     160 * GB,
    34  			obsoleteBytes: 1 * MB,
    35  			liveBytes:     160 * MB,
    36  			expected:      100.0,
    37  		},
    38  		// As freeBytes is 2GB below the free space threshold, rate should be
    39  		// increased by 204.8MB/s.
    40  		{
    41  			freeBytes:     14 * GB,
    42  			obsoleteBytes: 1 * MB,
    43  			liveBytes:     100 * MB,
    44  			expected:      304.8,
    45  		},
    46  		// As freeBytes is 10GB below the free space threshold, rate should be
    47  		// increased to by 1GB/s.
    48  		{
    49  			freeBytes:     6 * GB,
    50  			obsoleteBytes: 1 * MB,
    51  			liveBytes:     100 * MB,
    52  			expected:      1124.0,
    53  		},
    54  		// obsoleteBytesRatio is 50%. We need to delete 30GB within 5 minutes.
    55  		{
    56  			freeBytes:     500 * GB,
    57  			obsoleteBytes: 50 * GB,
    58  			liveBytes:     100 * GB,
    59  			expected:      202.4,
    60  		},
    61  		// When obsolete ratio unknown, there should be no throttling.
    62  		{
    63  			freeBytes:     500 * GB,
    64  			obsoleteBytes: 0,
    65  			liveBytes:     0,
    66  			expected:      math.Inf(1),
    67  		},
    68  		// History shows 200MB/sec deletions on average over last 5 minutes.
    69  		{
    70  			freeBytes:     160 * GB,
    71  			obsoleteBytes: 1 * MB,
    72  			liveBytes:     160 * MB,
    73  			history:       [][2]int64{{0, 5 * 60 * 200 * MB}},
    74  			expected:      200.0,
    75  		},
    76  		// History shows 200MB/sec deletions on average over last 5 minutes and
    77  		// freeBytes is 10GB below the threshold.
    78  		{
    79  			freeBytes:     6 * GB,
    80  			obsoleteBytes: 1 * MB,
    81  			liveBytes:     160 * MB,
    82  			history:       [][2]int64{{0, 5 * 60 * 200 * MB}},
    83  			expected:      1224.0,
    84  		},
    85  		// History shows 200MB/sec deletions on average over last 5 minutes and
    86  		// obsoleteBytesRatio is 50%.
    87  		{
    88  			freeBytes:     500 * GB,
    89  			obsoleteBytes: 50 * GB,
    90  			liveBytes:     100 * GB,
    91  			history:       [][2]int64{{0, 5 * 60 * 200 * MB}},
    92  			expected:      302.4,
    93  		},
    94  		// History shows 1000MB/sec deletions on average over last 5 minutes.
    95  		{
    96  			freeBytes:     160 * GB,
    97  			obsoleteBytes: 1 * MB,
    98  			liveBytes:     160 * MB,
    99  			history:       [][2]int64{{0, 60 * 1000 * MB}, {3 * 60, 60 * 4 * 1000 * MB}, {4 * 60, 0}},
   100  			expected:      1000.0,
   101  		},
   102  		// First entry in history is too old, it should be discarded.
   103  		{
   104  			freeBytes:     160 * GB,
   105  			obsoleteBytes: 1 * MB,
   106  			liveBytes:     160 * MB,
   107  			history:       [][2]int64{{0, 10 * 60 * 10000 * MB}, {3 * 60, 4 * 60 * 200 * MB}, {7 * 60, 1 * 60 * 200 * MB}},
   108  			expected:      200.0,
   109  		},
   110  	}
   111  	for tcIdx, tc := range testCases {
   112  		t.Run(fmt.Sprintf("%d", tcIdx), func(t *testing.T) {
   113  			getInfo := func() deletionPacerInfo {
   114  				return deletionPacerInfo{
   115  					freeBytes:     tc.freeBytes,
   116  					liveBytes:     tc.liveBytes,
   117  					obsoleteBytes: tc.obsoleteBytes,
   118  				}
   119  			}
   120  			start := time.Now()
   121  			last := start
   122  			pacer := newDeletionPacer(start, 100*MB, getInfo)
   123  			for _, h := range tc.history {
   124  				last = start.Add(time.Second * time.Duration(h[0]))
   125  				pacer.ReportDeletion(last, uint64(h[1]))
   126  			}
   127  			result := 1.0 / pacer.PacingDelay(last, 1*MB)
   128  			require.InDelta(t, tc.expected, result, 1e-7)
   129  		})
   130  	}
   131  }
   132  
   133  // TestDeletionPacerHistory tests the history helper by crosschecking Sum()
   134  // against a naive implementation.
   135  func TestDeletionPacerHistory(t *testing.T) {
   136  	type event struct {
   137  		time time.Time
   138  		// If report is 0, this event is a Sum(). Otherwise it is an Add().
   139  		report int64
   140  	}
   141  	numEvents := 1 + rand.Intn(200)
   142  	timeframe := time.Duration(1+rand.Intn(60*100)) * time.Second
   143  	events := make([]event, numEvents)
   144  	startTime := time.Now()
   145  	for i := range events {
   146  		events[i].time = startTime.Add(time.Duration(rand.Int63n(int64(timeframe))))
   147  		if rand.Intn(3) == 0 {
   148  			events[i].report = 0
   149  		} else {
   150  			events[i].report = int64(rand.Intn(100000))
   151  		}
   152  	}
   153  	slices.SortFunc(events, func(a, b event) int { return a.time.Compare(b.time) })
   154  
   155  	var h history
   156  	h.Init(startTime, timeframe)
   157  
   158  	// partialSums[i] := SUM_j<i events[j].report
   159  	partialSums := make([]int64, len(events)+1)
   160  	for i := range events {
   161  		partialSums[i+1] = partialSums[i] + events[i].report
   162  	}
   163  
   164  	for i, e := range events {
   165  		if e.report != 0 {
   166  			h.Add(e.time, e.report)
   167  			continue
   168  		}
   169  
   170  		result := h.Sum(e.time)
   171  
   172  		// getIdx returns the largest event index <= i that is before the cutoff
   173  		// time.
   174  		getIdx := func(cutoff time.Time) int {
   175  			for j := i; j >= 0; j-- {
   176  				if events[j].time.Before(cutoff) {
   177  					return j
   178  				}
   179  			}
   180  			return -1
   181  		}
   182  
   183  		// Sum all report values in the last timeframe, and see if recent events
   184  		// (allowing 1% error in the cutoff time) match the result.
   185  		a := getIdx(e.time.Add(-timeframe * (historyEpochs + 1) / historyEpochs))
   186  		b := getIdx(e.time.Add(-timeframe * (historyEpochs - 1) / historyEpochs))
   187  		found := false
   188  		for j := a; j <= b; j++ {
   189  			if partialSums[i+1]-partialSums[j+1] == result {
   190  				found = true
   191  				break
   192  			}
   193  		}
   194  		if !found {
   195  			t.Fatalf("incorrect Sum() result %d; %v", result, events[a+1:i+1])
   196  		}
   197  	}
   198  }