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 }