github.com/cockroachdb/pebble@v1.1.2/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 "math/rand" 11 "runtime" 12 "strconv" 13 "strings" 14 "sync" 15 "testing" 16 "time" 17 18 "github.com/cockroachdb/datadriven" 19 "github.com/cockroachdb/pebble/internal/humanize" 20 "github.com/cockroachdb/pebble/internal/testkeys" 21 "github.com/cockroachdb/pebble/vfs" 22 "github.com/cockroachdb/redact" 23 "github.com/stretchr/testify/require" 24 ) 25 26 func exampleMetrics() Metrics { 27 var m Metrics 28 m.BlockCache.Size = 1 29 m.BlockCache.Count = 2 30 m.BlockCache.Hits = 3 31 m.BlockCache.Misses = 4 32 m.Compact.Count = 5 33 m.Compact.DefaultCount = 27 34 m.Compact.DeleteOnlyCount = 28 35 m.Compact.ElisionOnlyCount = 29 36 m.Compact.MoveCount = 30 37 m.Compact.ReadCount = 31 38 m.Compact.RewriteCount = 32 39 m.Compact.MultiLevelCount = 33 40 m.Compact.EstimatedDebt = 6 41 m.Compact.InProgressBytes = 7 42 m.Compact.NumInProgress = 2 43 m.Flush.Count = 8 44 m.Flush.AsIngestBytes = 34 45 m.Flush.AsIngestTableCount = 35 46 m.Flush.AsIngestCount = 36 47 m.Filter.Hits = 9 48 m.Filter.Misses = 10 49 m.MemTable.Size = 11 50 m.MemTable.Count = 12 51 m.MemTable.ZombieSize = 13 52 m.MemTable.ZombieCount = 14 53 m.Snapshots.Count = 4 54 m.Snapshots.EarliestSeqNum = 1024 55 m.Table.ZombieSize = 15 56 m.Table.BackingTableCount = 1 57 m.Table.BackingTableSize = 2 << 20 58 m.Table.ZombieCount = 16 59 m.TableCache.Size = 17 60 m.TableCache.Count = 18 61 m.TableCache.Hits = 19 62 m.TableCache.Misses = 20 63 m.TableIters = 21 64 m.WAL.Files = 22 65 m.WAL.ObsoleteFiles = 23 66 m.WAL.Size = 24 67 m.WAL.BytesIn = 25 68 m.WAL.BytesWritten = 26 69 m.Ingest.Count = 27 70 71 for i := range m.Levels { 72 l := &m.Levels[i] 73 base := uint64((i + 1) * 100) 74 l.Sublevels = int32(i + 1) 75 l.NumFiles = int64(base) + 1 76 l.NumVirtualFiles = uint64(base) + 1 77 l.VirtualSize = base + 3 78 l.Size = int64(base) + 2 79 l.Score = float64(base) + 3 80 l.BytesIn = base + 4 81 l.BytesIngested = base + 4 82 l.BytesMoved = base + 6 83 l.BytesRead = base + 7 84 l.BytesCompacted = base + 8 85 l.BytesFlushed = base + 9 86 l.TablesCompacted = base + 10 87 l.TablesFlushed = base + 11 88 l.TablesIngested = base + 12 89 l.TablesMoved = base + 13 90 l.MultiLevel.BytesInTop = base + 4 91 l.MultiLevel.BytesIn = base + 4 92 l.MultiLevel.BytesRead = base + 4 93 } 94 return m 95 } 96 97 func TestMetrics(t *testing.T) { 98 if runtime.GOARCH == "386" { 99 t.Skip("skipped on 32-bit due to slightly varied output") 100 } 101 opts := &Options{ 102 Comparer: testkeys.Comparer, 103 FormatMajorVersion: FormatNewest, 104 FS: vfs.NewMem(), 105 L0CompactionThreshold: 8, 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 iter, _ := d.NewIter(nil) 240 // Some iterators (eg. levelIter) do not instantiate the underlying 241 // iterator until the first positioning call. Position the iterator 242 // so that levelIters will have loaded an sstable. 243 iter.First() 244 iters[name] = iter 245 return "" 246 247 case "metrics": 248 // The asynchronous loading of table stats can change metrics, so 249 // wait for all the tables' stats to be loaded. 250 d.mu.Lock() 251 d.waitTableStats() 252 d.mu.Unlock() 253 254 return d.Metrics().StringForTests() 255 256 case "metrics-value": 257 // metrics-value confirms the value of a given metric. Note that there 258 // are some metrics which aren't deterministic and behave differently 259 // for invariant/non-invariant builds. An example of this is cache 260 // hit rates. Under invariant builds, the excising code will try 261 // to create iterators and confirm that the virtual sstable bounds 262 // are accurate. Reads on these iterators will change the cache hit 263 // rates. 264 lines := strings.Split(td.Input, "\n") 265 m := d.Metrics() 266 // TODO(bananabrick): Use reflection to pull the values associated 267 // with the metrics fields. 268 var buf bytes.Buffer 269 for i := range lines { 270 line := lines[i] 271 if line == "num-backing" { 272 buf.WriteString(fmt.Sprintf("%d\n", m.Table.BackingTableCount)) 273 } else if line == "backing-size" { 274 buf.WriteString(fmt.Sprintf("%s\n", humanize.Bytes.Uint64(m.Table.BackingTableSize))) 275 } else if line == "virtual-size" { 276 buf.WriteString(fmt.Sprintf("%s\n", humanize.Bytes.Uint64(m.VirtualSize()))) 277 } else if strings.HasPrefix(line, "num-virtual") { 278 splits := strings.Split(line, " ") 279 if len(splits) == 1 { 280 buf.WriteString(fmt.Sprintf("%d\n", m.NumVirtual())) 281 continue 282 } 283 // Level is specified. 284 l, err := strconv.Atoi(splits[1]) 285 if err != nil { 286 panic(err) 287 } 288 if l >= numLevels { 289 panic(fmt.Sprintf("invalid level %d", l)) 290 } 291 buf.WriteString(fmt.Sprintf("%d\n", m.Levels[l].NumVirtualFiles)) 292 } else { 293 panic(fmt.Sprintf("invalid field: %s", line)) 294 } 295 } 296 return buf.String() 297 298 case "disk-usage": 299 return humanize.Bytes.Uint64(d.Metrics().DiskSpaceUsage()).String() 300 301 case "additional-metrics": 302 // The asynchronous loading of table stats can change metrics, so 303 // wait for all the tables' stats to be loaded. 304 d.mu.Lock() 305 d.waitTableStats() 306 d.mu.Unlock() 307 308 m := d.Metrics() 309 var b strings.Builder 310 fmt.Fprintf(&b, "block bytes written:\n") 311 fmt.Fprintf(&b, " __level___data-block__value-block\n") 312 for i := range m.Levels { 313 fmt.Fprintf(&b, "%7d ", i) 314 fmt.Fprintf(&b, "%12s %12s\n", 315 humanize.Bytes.Uint64(m.Levels[i].Additional.BytesWrittenDataBlocks), 316 humanize.Bytes.Uint64(m.Levels[i].Additional.BytesWrittenValueBlocks)) 317 } 318 return b.String() 319 320 default: 321 return fmt.Sprintf("unknown command: %s", td.Cmd) 322 } 323 }) 324 } 325 326 func TestMetricsWAmpDisableWAL(t *testing.T) { 327 d, err := Open("", &Options{FS: vfs.NewMem(), DisableWAL: true}) 328 require.NoError(t, err) 329 ks := testkeys.Alpha(2) 330 wo := WriteOptions{Sync: false} 331 for i := 0; i < 5; i++ { 332 v := []byte(strconv.Itoa(i)) 333 for j := int64(0); j < ks.Count(); j++ { 334 require.NoError(t, d.Set(testkeys.Key(ks, j), v, &wo)) 335 } 336 require.NoError(t, d.Flush()) 337 require.NoError(t, d.Compact([]byte("a"), []byte("z"), false /* parallelize */)) 338 } 339 m := d.Metrics() 340 tot := m.Total() 341 require.Greater(t, tot.WriteAmp(), 1.0) 342 require.NoError(t, d.Close()) 343 } 344 345 // TestMetricsWALBytesWrittenMonotonicity tests that the 346 // Metrics.WAL.BytesWritten metric is always nondecreasing. 347 // It's a regression test for issue #3505. 348 func TestMetricsWALBytesWrittenMonotonicity(t *testing.T) { 349 fs := vfs.NewMem() 350 d, err := Open("", &Options{ 351 FS: fs, 352 // Use a tiny memtable size so that we get frequent flushes. While a 353 // flush is in-progress or completing, the WAL bytes written should 354 // remain nondecreasing. 355 MemTableSize: 1 << 20, /* 20 KiB */ 356 }) 357 require.NoError(t, err) 358 359 stopCh := make(chan struct{}) 360 361 ks := testkeys.Alpha(3) 362 var wg sync.WaitGroup 363 const concurrentWriters = 5 364 wg.Add(concurrentWriters) 365 for w := 0; w < concurrentWriters; w++ { 366 go func() { 367 defer wg.Done() 368 data := make([]byte, 1<<10) // 1 KiB 369 rng := rand.New(rand.NewSource(time.Now().UnixNano())) 370 _, err := rng.Read(data) 371 require.NoError(t, err) 372 373 buf := make([]byte, ks.MaxLen()) 374 for i := 0; ; i++ { 375 select { 376 case <-stopCh: 377 return 378 default: 379 } 380 n := testkeys.WriteKey(buf, ks, int64(i)%ks.Count()) 381 require.NoError(t, d.Set(buf[:n], data, NoSync)) 382 } 383 }() 384 } 385 386 func() { 387 defer func() { close(stopCh) }() 388 abort := time.After(time.Second) 389 var prevWALBytesWritten uint64 390 for { 391 select { 392 case <-abort: 393 return 394 default: 395 } 396 397 m := d.Metrics() 398 if m.WAL.BytesWritten < prevWALBytesWritten { 399 t.Fatalf("WAL bytes written decreased: %d -> %d", prevWALBytesWritten, m.WAL.BytesWritten) 400 } 401 prevWALBytesWritten = m.WAL.BytesWritten 402 } 403 }() 404 wg.Wait() 405 }