github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/metrics_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  	"bytes"
     9  	"fmt"
    10  	"strconv"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/cockroachdb/datadriven"
    15  	"github.com/cockroachdb/pebble/internal/cache"
    16  	"github.com/cockroachdb/pebble/internal/humanize"
    17  	"github.com/cockroachdb/pebble/internal/testkeys"
    18  	"github.com/cockroachdb/pebble/sstable"
    19  	"github.com/cockroachdb/pebble/vfs"
    20  	"github.com/cockroachdb/redact"
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  func exampleMetrics() Metrics {
    25  	var m Metrics
    26  	m.BlockCache.Size = 1
    27  	m.BlockCache.Count = 2
    28  	m.BlockCache.Hits = 3
    29  	m.BlockCache.Misses = 4
    30  	m.Compact.Count = 5
    31  	m.Compact.DefaultCount = 27
    32  	m.Compact.DeleteOnlyCount = 28
    33  	m.Compact.ElisionOnlyCount = 29
    34  	m.Compact.MoveCount = 30
    35  	m.Compact.ReadCount = 31
    36  	m.Compact.RewriteCount = 32
    37  	m.Compact.MultiLevelCount = 33
    38  	m.Compact.EstimatedDebt = 6
    39  	m.Compact.InProgressBytes = 7
    40  	m.Compact.NumInProgress = 2
    41  	m.Flush.Count = 8
    42  	m.Flush.AsIngestBytes = 34
    43  	m.Flush.AsIngestTableCount = 35
    44  	m.Flush.AsIngestCount = 36
    45  	m.Filter.Hits = 9
    46  	m.Filter.Misses = 10
    47  	m.MemTable.Size = 11
    48  	m.MemTable.Count = 12
    49  	m.MemTable.ZombieSize = 13
    50  	m.MemTable.ZombieCount = 14
    51  	m.Snapshots.Count = 4
    52  	m.Snapshots.EarliestSeqNum = 1024
    53  	m.Table.ZombieSize = 15
    54  	m.Table.BackingTableCount = 1
    55  	m.Table.BackingTableSize = 2 << 20
    56  	m.Table.ZombieCount = 16
    57  	m.TableCache.Size = 17
    58  	m.TableCache.Count = 18
    59  	m.TableCache.Hits = 19
    60  	m.TableCache.Misses = 20
    61  	m.TableIters = 21
    62  	m.WAL.Files = 22
    63  	m.WAL.ObsoleteFiles = 23
    64  	m.WAL.Size = 24
    65  	m.WAL.BytesIn = 25
    66  	m.WAL.BytesWritten = 26
    67  	m.Ingest.Count = 27
    68  
    69  	for i := range m.Levels {
    70  		l := &m.Levels[i]
    71  		base := uint64((i + 1) * 100)
    72  		l.Sublevels = int32(i + 1)
    73  		l.NumFiles = int64(base) + 1
    74  		l.NumVirtualFiles = uint64(base) + 1
    75  		l.VirtualSize = base + 3
    76  		l.Size = int64(base) + 2
    77  		l.Score = float64(base) + 3
    78  		l.BytesIn = base + 4
    79  		l.BytesIngested = base + 4
    80  		l.BytesMoved = base + 6
    81  		l.BytesRead = base + 7
    82  		l.BytesCompacted = base + 8
    83  		l.BytesFlushed = base + 9
    84  		l.TablesCompacted = base + 10
    85  		l.TablesFlushed = base + 11
    86  		l.TablesIngested = base + 12
    87  		l.TablesMoved = base + 13
    88  		l.MultiLevel.BytesInTop = base + 4
    89  		l.MultiLevel.BytesIn = base + 4
    90  		l.MultiLevel.BytesRead = base + 4
    91  	}
    92  	return m
    93  }
    94  
    95  func TestMetrics(t *testing.T) {
    96  	c := cache.New(cacheDefaultSize)
    97  	defer c.Unref()
    98  	opts := &Options{
    99  		Cache:                 c,
   100  		Comparer:              testkeys.Comparer,
   101  		FormatMajorVersion:    FormatNewest,
   102  		FS:                    vfs.NewMem(),
   103  		L0CompactionThreshold: 8,
   104  		// Large value for determinism.
   105  		MaxOpenFiles: 10000,
   106  	}
   107  	opts.Experimental.EnableValueBlocks = func() bool { return true }
   108  	opts.Levels = append(opts.Levels, LevelOptions{TargetFileSize: 50})
   109  
   110  	// Prevent foreground flushes and compactions from triggering asynchronous
   111  	// follow-up compactions. This avoids asynchronously-scheduled work from
   112  	// interfering with the expected metrics output and reduces test flakiness.
   113  	opts.DisableAutomaticCompactions = true
   114  
   115  	// Increase the threshold for memtable stalls to allow for more flushable
   116  	// ingests.
   117  	opts.MemTableStopWritesThreshold = 4
   118  
   119  	d, err := Open("", opts)
   120  	require.NoError(t, err)
   121  	defer func() {
   122  		require.NoError(t, d.Close())
   123  	}()
   124  
   125  	iters := make(map[string]*Iterator)
   126  	defer func() {
   127  		for _, i := range iters {
   128  			require.NoError(t, i.Close())
   129  		}
   130  	}()
   131  
   132  	datadriven.RunTest(t, "testdata/metrics", func(t *testing.T, td *datadriven.TestData) string {
   133  		switch td.Cmd {
   134  		case "example":
   135  			m := exampleMetrics()
   136  			res := m.String()
   137  
   138  			// Nothing in the metrics should be redacted.
   139  			redacted := string(redact.Sprintf("%s", &m).Redact())
   140  			if redacted != res {
   141  				td.Fatalf(t, "redacted metrics don't match\nunredacted:\n%s\nredacted:%s\n", res, redacted)
   142  			}
   143  			return res
   144  
   145  		case "batch":
   146  			b := d.NewBatch()
   147  			if err := runBatchDefineCmd(td, b); err != nil {
   148  				return err.Error()
   149  			}
   150  			b.Commit(nil)
   151  			return ""
   152  
   153  		case "build":
   154  			if err := runBuildCmd(td, d, d.opts.FS); err != nil {
   155  				return err.Error()
   156  			}
   157  			return ""
   158  
   159  		case "compact":
   160  			if err := runCompactCmd(td, d); err != nil {
   161  				return err.Error()
   162  			}
   163  
   164  			d.mu.Lock()
   165  			s := d.mu.versions.currentVersion().String()
   166  			d.mu.Unlock()
   167  			return s
   168  
   169  		case "delay-flush":
   170  			d.mu.Lock()
   171  			defer d.mu.Unlock()
   172  			switch td.Input {
   173  			case "enable":
   174  				d.mu.compact.flushing = true
   175  			case "disable":
   176  				d.mu.compact.flushing = false
   177  			default:
   178  				return fmt.Sprintf("unknown directive %q (expected 'enable'/'disable')", td.Input)
   179  			}
   180  			return ""
   181  
   182  		case "flush":
   183  			if err := d.Flush(); err != nil {
   184  				return err.Error()
   185  			}
   186  
   187  			d.mu.Lock()
   188  			s := d.mu.versions.currentVersion().String()
   189  			d.mu.Unlock()
   190  			return s
   191  
   192  		case "ingest":
   193  			if err := runIngestCmd(td, d, d.opts.FS); err != nil {
   194  				return err.Error()
   195  			}
   196  			return ""
   197  
   198  		case "lsm":
   199  			d.mu.Lock()
   200  			s := d.mu.versions.currentVersion().String()
   201  			d.mu.Unlock()
   202  			return s
   203  
   204  		case "ingest-and-excise":
   205  			if err := runIngestAndExciseCmd(td, d, d.opts.FS); err != nil {
   206  				return err.Error()
   207  			}
   208  			return ""
   209  
   210  		case "iter-close":
   211  			if len(td.CmdArgs) != 1 {
   212  				return "iter-close <name>"
   213  			}
   214  			name := td.CmdArgs[0].String()
   215  			if iter := iters[name]; iter != nil {
   216  				if err := iter.Close(); err != nil {
   217  					return err.Error()
   218  				}
   219  				delete(iters, name)
   220  			} else {
   221  				return fmt.Sprintf("%s: not found", name)
   222  			}
   223  
   224  			// The deletion of obsolete files happens asynchronously when an iterator
   225  			// is closed. Wait for the obsolete tables to be deleted.
   226  			d.cleanupManager.Wait()
   227  			return ""
   228  
   229  		case "iter-new":
   230  			if len(td.CmdArgs) < 1 {
   231  				return "iter-new <name>"
   232  			}
   233  			name := td.CmdArgs[0].String()
   234  			if iter := iters[name]; iter != nil {
   235  				if err := iter.Close(); err != nil {
   236  					return err.Error()
   237  				}
   238  			}
   239  			var categoryAndQoS sstable.CategoryAndQoS
   240  			if td.HasArg("category") {
   241  				var s string
   242  				td.ScanArgs(t, "category", &s)
   243  				categoryAndQoS.Category = sstable.Category(s)
   244  			}
   245  			if td.HasArg("qos") {
   246  				var qos string
   247  				td.ScanArgs(t, "qos", &qos)
   248  				categoryAndQoS.QoSLevel = sstable.StringToQoSForTesting(qos)
   249  			}
   250  			iter, _ := d.NewIter(&IterOptions{CategoryAndQoS: categoryAndQoS})
   251  			// Some iterators (eg. levelIter) do not instantiate the underlying
   252  			// iterator until the first positioning call. Position the iterator
   253  			// so that levelIters will have loaded an sstable.
   254  			iter.First()
   255  			iters[name] = iter
   256  			return ""
   257  
   258  		case "metrics":
   259  			// The asynchronous loading of table stats can change metrics, so
   260  			// wait for all the tables' stats to be loaded.
   261  			d.mu.Lock()
   262  			d.waitTableStats()
   263  			d.mu.Unlock()
   264  
   265  			m := d.Metrics()
   266  			if td.HasArg("zero-cache-hits-misses") {
   267  				// Avoid non-determinism.
   268  				m.TableCache.Hits = 0
   269  				m.TableCache.Misses = 0
   270  				m.BlockCache.Hits = 0
   271  				m.BlockCache.Misses = 0
   272  				// Empirically, the unknown stats are also non-deterministic.
   273  				if len(m.CategoryStats) > 0 && m.CategoryStats[0].Category == "_unknown" {
   274  					m.CategoryStats[0].CategoryStats = sstable.CategoryStats{}
   275  				}
   276  			}
   277  			var buf strings.Builder
   278  			fmt.Fprintf(&buf, "%s", m.StringForTests())
   279  			if len(m.CategoryStats) > 0 {
   280  				fmt.Fprintf(&buf, "Iter category stats:\n")
   281  				for _, stats := range m.CategoryStats {
   282  					fmt.Fprintf(&buf, "%20s, %11s: %+v\n", stats.Category,
   283  						redact.StringWithoutMarkers(stats.QoSLevel), stats.CategoryStats)
   284  				}
   285  			}
   286  			return buf.String()
   287  
   288  		case "metrics-value":
   289  			// metrics-value confirms the value of a given metric. Note that there
   290  			// are some metrics which aren't deterministic and behave differently
   291  			// for invariant/non-invariant builds. An example of this is cache
   292  			// hit rates. Under invariant builds, the excising code will try
   293  			// to create iterators and confirm that the virtual sstable bounds
   294  			// are accurate. Reads on these iterators will change the cache hit
   295  			// rates.
   296  			lines := strings.Split(td.Input, "\n")
   297  			m := d.Metrics()
   298  			// TODO(bananabrick): Use reflection to pull the values associated
   299  			// with the metrics fields.
   300  			var buf bytes.Buffer
   301  			for i := range lines {
   302  				line := lines[i]
   303  				if line == "num-backing" {
   304  					buf.WriteString(fmt.Sprintf("%d\n", m.Table.BackingTableCount))
   305  				} else if line == "backing-size" {
   306  					buf.WriteString(fmt.Sprintf("%s\n", humanize.Bytes.Uint64(m.Table.BackingTableSize)))
   307  				} else if line == "virtual-size" {
   308  					buf.WriteString(fmt.Sprintf("%s\n", humanize.Bytes.Uint64(m.VirtualSize())))
   309  				} else if strings.HasPrefix(line, "num-virtual") {
   310  					splits := strings.Split(line, " ")
   311  					if len(splits) == 1 {
   312  						buf.WriteString(fmt.Sprintf("%d\n", m.NumVirtual()))
   313  						continue
   314  					}
   315  					// Level is specified.
   316  					l, err := strconv.Atoi(splits[1])
   317  					if err != nil {
   318  						panic(err)
   319  					}
   320  					if l >= numLevels {
   321  						panic(fmt.Sprintf("invalid level %d", l))
   322  					}
   323  					buf.WriteString(fmt.Sprintf("%d\n", m.Levels[l].NumVirtualFiles))
   324  				} else {
   325  					panic(fmt.Sprintf("invalid field: %s", line))
   326  				}
   327  			}
   328  			return buf.String()
   329  
   330  		case "disk-usage":
   331  			return humanize.Bytes.Uint64(d.Metrics().DiskSpaceUsage()).String()
   332  
   333  		case "additional-metrics":
   334  			// The asynchronous loading of table stats can change metrics, so
   335  			// wait for all the tables' stats to be loaded.
   336  			d.mu.Lock()
   337  			d.waitTableStats()
   338  			d.mu.Unlock()
   339  
   340  			m := d.Metrics()
   341  			var b strings.Builder
   342  			fmt.Fprintf(&b, "block bytes written:\n")
   343  			fmt.Fprintf(&b, " __level___data-block__value-block\n")
   344  			for i := range m.Levels {
   345  				fmt.Fprintf(&b, "%7d ", i)
   346  				fmt.Fprintf(&b, "%12s %12s\n",
   347  					humanize.Bytes.Uint64(m.Levels[i].Additional.BytesWrittenDataBlocks),
   348  					humanize.Bytes.Uint64(m.Levels[i].Additional.BytesWrittenValueBlocks))
   349  			}
   350  			return b.String()
   351  
   352  		default:
   353  			return fmt.Sprintf("unknown command: %s", td.Cmd)
   354  		}
   355  	})
   356  }
   357  
   358  func TestMetricsWAmpDisableWAL(t *testing.T) {
   359  	d, err := Open("", &Options{FS: vfs.NewMem(), DisableWAL: true})
   360  	require.NoError(t, err)
   361  	ks := testkeys.Alpha(2)
   362  	wo := WriteOptions{Sync: false}
   363  	for i := 0; i < 5; i++ {
   364  		v := []byte(strconv.Itoa(i))
   365  		for j := int64(0); j < ks.Count(); j++ {
   366  			require.NoError(t, d.Set(testkeys.Key(ks, j), v, &wo))
   367  		}
   368  		require.NoError(t, d.Flush())
   369  		require.NoError(t, d.Compact([]byte("a"), []byte("z"), false /* parallelize */))
   370  	}
   371  	m := d.Metrics()
   372  	tot := m.Total()
   373  	require.Greater(t, tot.WriteAmp(), 1.0)
   374  	require.NoError(t, d.Close())
   375  }