github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/tool/manifest.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 tool 6 7 import ( 8 "cmp" 9 "fmt" 10 "io" 11 "slices" 12 "time" 13 14 "github.com/cockroachdb/pebble" 15 "github.com/cockroachdb/pebble/internal/base" 16 "github.com/cockroachdb/pebble/internal/humanize" 17 "github.com/cockroachdb/pebble/internal/manifest" 18 "github.com/cockroachdb/pebble/record" 19 "github.com/cockroachdb/pebble/sstable" 20 "github.com/spf13/cobra" 21 ) 22 23 // manifestT implements manifest-level tools, including both configuration 24 // state and the commands themselves. 25 type manifestT struct { 26 Root *cobra.Command 27 Dump *cobra.Command 28 Summarize *cobra.Command 29 Check *cobra.Command 30 31 opts *pebble.Options 32 comparers sstable.Comparers 33 fmtKey keyFormatter 34 verbose bool 35 36 filterStart key 37 filterEnd key 38 39 summarizeDur time.Duration 40 } 41 42 func newManifest(opts *pebble.Options, comparers sstable.Comparers) *manifestT { 43 m := &manifestT{ 44 opts: opts, 45 comparers: comparers, 46 summarizeDur: time.Hour, 47 } 48 m.fmtKey.mustSet("quoted") 49 50 m.Root = &cobra.Command{ 51 Use: "manifest", 52 Short: "manifest introspection tools", 53 } 54 55 // Add dump command 56 m.Dump = &cobra.Command{ 57 Use: "dump <manifest-files>", 58 Short: "print manifest contents", 59 Long: ` 60 Print the contents of the MANIFEST files. 61 `, 62 Args: cobra.MinimumNArgs(1), 63 Run: m.runDump, 64 } 65 m.Dump.Flags().Var(&m.fmtKey, "key", "key formatter") 66 m.Dump.Flags().Var(&m.filterStart, "filter-start", "start key filters out all version edits that only reference sstables containing keys strictly before the given key") 67 m.Dump.Flags().Var(&m.filterEnd, "filter-end", "end key filters out all version edits that only reference sstables containing keys at or strictly after the given key") 68 m.Root.AddCommand(m.Dump) 69 m.Root.PersistentFlags().BoolVarP(&m.verbose, "verbose", "v", false, "verbose output") 70 71 // Add summarize command 72 m.Summarize = &cobra.Command{ 73 Use: "summarize <manifest-files>", 74 Short: "summarize manifest contents", 75 Long: ` 76 Summarize the edits to the MANIFEST files over time. 77 `, 78 Args: cobra.MinimumNArgs(1), 79 Run: m.runSummarize, 80 } 81 m.Root.AddCommand(m.Summarize) 82 m.Summarize.Flags().DurationVar( 83 &m.summarizeDur, "dur", time.Hour, "bucket duration as a Go duration string (eg, '1h', '15m')") 84 85 // Add check command 86 m.Check = &cobra.Command{ 87 Use: "check <manifest-files>", 88 Short: "check manifest contents", 89 Long: ` 90 Check the contents of the MANIFEST files. 91 `, 92 Args: cobra.MinimumNArgs(1), 93 Run: m.runCheck, 94 } 95 m.Root.AddCommand(m.Check) 96 m.Check.Flags().Var( 97 &m.fmtKey, "key", "key formatter") 98 99 return m 100 } 101 102 func (m *manifestT) printLevels(cmp base.Compare, stdout io.Writer, v *manifest.Version) { 103 for level := range v.Levels { 104 if level == 0 && len(v.L0SublevelFiles) > 0 && !v.Levels[level].Empty() { 105 for sublevel := len(v.L0SublevelFiles) - 1; sublevel >= 0; sublevel-- { 106 fmt.Fprintf(stdout, "--- L0.%d ---\n", sublevel) 107 v.L0SublevelFiles[sublevel].Each(func(f *manifest.FileMetadata) { 108 if !anyOverlapFile(cmp, f, m.filterStart, m.filterEnd) { 109 return 110 } 111 fmt.Fprintf(stdout, " %s:%d", f.FileNum, f.Size) 112 formatSeqNumRange(stdout, f.SmallestSeqNum, f.LargestSeqNum) 113 formatKeyRange(stdout, m.fmtKey, &f.Smallest, &f.Largest) 114 if f.Virtual { 115 fmt.Fprintf(stdout, "(virtual:backingNum=%s)", f.FileBacking.DiskFileNum) 116 } 117 fmt.Fprintf(stdout, "\n") 118 }) 119 } 120 continue 121 } 122 fmt.Fprintf(stdout, "--- L%d ---\n", level) 123 iter := v.Levels[level].Iter() 124 for f := iter.First(); f != nil; f = iter.Next() { 125 if !anyOverlapFile(cmp, f, m.filterStart, m.filterEnd) { 126 continue 127 } 128 fmt.Fprintf(stdout, " %s:%d", f.FileNum, f.Size) 129 formatSeqNumRange(stdout, f.SmallestSeqNum, f.LargestSeqNum) 130 formatKeyRange(stdout, m.fmtKey, &f.Smallest, &f.Largest) 131 if f.Virtual { 132 fmt.Fprintf(stdout, "(virtual:backingNum=%s)", f.FileBacking.DiskFileNum) 133 } 134 fmt.Fprintf(stdout, "\n") 135 } 136 } 137 } 138 139 func (m *manifestT) runDump(cmd *cobra.Command, args []string) { 140 stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() 141 for _, arg := range args { 142 func() { 143 f, err := m.opts.FS.Open(arg) 144 if err != nil { 145 fmt.Fprintf(stderr, "%s\n", err) 146 return 147 } 148 defer f.Close() 149 150 fmt.Fprintf(stdout, "%s\n", arg) 151 152 var bve manifest.BulkVersionEdit 153 bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata) 154 var comparer *base.Comparer 155 var editIdx int 156 rr := record.NewReader(f, 0 /* logNum */) 157 for { 158 offset := rr.Offset() 159 r, err := rr.Next() 160 if err != nil { 161 fmt.Fprintf(stdout, "%s\n", err) 162 break 163 } 164 165 var ve manifest.VersionEdit 166 err = ve.Decode(r) 167 if err != nil { 168 fmt.Fprintf(stdout, "%s\n", err) 169 break 170 } 171 if err := bve.Accumulate(&ve); err != nil { 172 fmt.Fprintf(stdout, "%s\n", err) 173 break 174 } 175 176 if comparer != nil && !anyOverlap(comparer.Compare, &ve, m.filterStart, m.filterEnd) { 177 continue 178 } 179 180 empty := true 181 fmt.Fprintf(stdout, "%d/%d\n", offset, editIdx) 182 if ve.ComparerName != "" { 183 empty = false 184 fmt.Fprintf(stdout, " comparer: %s", ve.ComparerName) 185 comparer = m.comparers[ve.ComparerName] 186 if comparer == nil { 187 fmt.Fprintf(stdout, " (unknown)") 188 } 189 fmt.Fprintf(stdout, "\n") 190 m.fmtKey.setForComparer(ve.ComparerName, m.comparers) 191 } 192 if ve.MinUnflushedLogNum != 0 { 193 empty = false 194 fmt.Fprintf(stdout, " log-num: %d\n", ve.MinUnflushedLogNum) 195 } 196 if ve.ObsoletePrevLogNum != 0 { 197 empty = false 198 fmt.Fprintf(stdout, " prev-log-num: %d\n", ve.ObsoletePrevLogNum) 199 } 200 if ve.NextFileNum != 0 { 201 empty = false 202 fmt.Fprintf(stdout, " next-file-num: %d\n", ve.NextFileNum) 203 } 204 if ve.LastSeqNum != 0 { 205 empty = false 206 fmt.Fprintf(stdout, " last-seq-num: %d\n", ve.LastSeqNum) 207 } 208 entries := make([]manifest.DeletedFileEntry, 0, len(ve.DeletedFiles)) 209 for df := range ve.DeletedFiles { 210 empty = false 211 entries = append(entries, df) 212 } 213 slices.SortFunc(entries, func(a, b manifest.DeletedFileEntry) int { 214 if v := cmp.Compare(a.Level, b.Level); v != 0 { 215 return v 216 } 217 return cmp.Compare(a.FileNum, b.FileNum) 218 }) 219 for _, df := range entries { 220 fmt.Fprintf(stdout, " deleted: L%d %s\n", df.Level, df.FileNum) 221 } 222 for _, nf := range ve.NewFiles { 223 empty = false 224 fmt.Fprintf(stdout, " added: L%d %s:%d", 225 nf.Level, nf.Meta.FileNum, nf.Meta.Size) 226 formatSeqNumRange(stdout, nf.Meta.SmallestSeqNum, nf.Meta.LargestSeqNum) 227 formatKeyRange(stdout, m.fmtKey, &nf.Meta.Smallest, &nf.Meta.Largest) 228 if nf.Meta.CreationTime != 0 { 229 fmt.Fprintf(stdout, " (%s)", 230 time.Unix(nf.Meta.CreationTime, 0).UTC().Format(time.RFC3339)) 231 } 232 fmt.Fprintf(stdout, "\n") 233 } 234 if empty { 235 // NB: An empty version edit can happen if we log a version edit with 236 // a zero field. RocksDB does this with a version edit that contains 237 // `LogNum == 0`. 238 fmt.Fprintf(stdout, " <empty>\n") 239 } 240 editIdx++ 241 } 242 243 if comparer != nil { 244 v, err := bve.Apply( 245 nil /* version */, comparer.Compare, m.fmtKey.fn, 0, 246 m.opts.Experimental.ReadCompactionRate, 247 nil /* zombies */, manifest.AllowSplitUserKeys, 248 ) 249 if err != nil { 250 fmt.Fprintf(stdout, "%s\n", err) 251 return 252 } 253 m.printLevels(comparer.Compare, stdout, v) 254 } 255 }() 256 } 257 } 258 259 func anyOverlap(cmp base.Compare, ve *manifest.VersionEdit, start, end key) bool { 260 if start == nil && end == nil { 261 return true 262 } 263 for _, df := range ve.DeletedFiles { 264 if anyOverlapFile(cmp, df, start, end) { 265 return true 266 } 267 } 268 for _, nf := range ve.NewFiles { 269 if anyOverlapFile(cmp, nf.Meta, start, end) { 270 return true 271 } 272 } 273 return false 274 } 275 276 func anyOverlapFile(cmp base.Compare, f *manifest.FileMetadata, start, end key) bool { 277 if f == nil { 278 return true 279 } 280 if start != nil { 281 if v := cmp(f.Largest.UserKey, start); v < 0 { 282 return false 283 } else if f.Largest.IsExclusiveSentinel() && v == 0 { 284 return false 285 } 286 } 287 if end != nil && cmp(f.Smallest.UserKey, end) >= 0 { 288 return false 289 } 290 return true 291 } 292 293 func (m *manifestT) runSummarize(cmd *cobra.Command, args []string) { 294 for _, arg := range args { 295 err := m.runSummarizeOne(cmd.OutOrStdout(), arg) 296 if err != nil { 297 fmt.Fprintf(cmd.OutOrStderr(), "%s\n", err) 298 } 299 } 300 } 301 302 func (m *manifestT) runSummarizeOne(stdout io.Writer, arg string) error { 303 f, err := m.opts.FS.Open(arg) 304 if err != nil { 305 return err 306 } 307 defer f.Close() 308 fmt.Fprintf(stdout, "%s\n", arg) 309 310 type summaryBucket struct { 311 bytesAdded [manifest.NumLevels]uint64 312 bytesCompactOut [manifest.NumLevels]uint64 313 } 314 var ( 315 bve manifest.BulkVersionEdit 316 newestOverall time.Time 317 oldestOverall time.Time // oldest after initial version edit 318 buckets = map[time.Time]*summaryBucket{} 319 metadatas = map[base.FileNum]*manifest.FileMetadata{} 320 ) 321 bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata) 322 rr := record.NewReader(f, 0 /* logNum */) 323 for i := 0; ; i++ { 324 r, err := rr.Next() 325 if err == io.EOF { 326 break 327 } else if err != nil { 328 return err 329 } 330 331 var ve manifest.VersionEdit 332 err = ve.Decode(r) 333 if err != nil { 334 return err 335 } 336 if err := bve.Accumulate(&ve); err != nil { 337 return err 338 } 339 340 veNewest, veOldest := newestOverall, newestOverall 341 for _, nf := range ve.NewFiles { 342 _, seen := metadatas[nf.Meta.FileNum] 343 metadatas[nf.Meta.FileNum] = nf.Meta 344 if nf.Meta.CreationTime == 0 { 345 continue 346 } 347 348 t := time.Unix(nf.Meta.CreationTime, 0).UTC() 349 if veNewest.Before(t) { 350 veNewest = t 351 } 352 // Only update the oldest if we haven't already seen this 353 // file; it might've been moved in which case the sstable's 354 // creation time is from when it was originally created. 355 if veOldest.After(t) && !seen { 356 veOldest = t 357 } 358 } 359 // Ratchet up the most recent timestamp we've seen. 360 if newestOverall.Before(veNewest) { 361 newestOverall = veNewest 362 } 363 364 if i == 0 || newestOverall.IsZero() { 365 continue 366 } 367 // Update oldestOverall once, when we encounter the first version edit 368 // at index >= 1. It should be approximately the start time of the 369 // manifest. 370 if !newestOverall.IsZero() && oldestOverall.IsZero() { 371 oldestOverall = newestOverall 372 } 373 374 bucketKey := newestOverall.Truncate(m.summarizeDur) 375 b := buckets[bucketKey] 376 if b == nil { 377 b = &summaryBucket{} 378 buckets[bucketKey] = b 379 } 380 381 // Increase `bytesAdded` for any version edits that only add files. 382 // These are either flushes or ingests. 383 if len(ve.NewFiles) > 0 && len(ve.DeletedFiles) == 0 { 384 for _, nf := range ve.NewFiles { 385 b.bytesAdded[nf.Level] += nf.Meta.Size 386 } 387 continue 388 } 389 390 // Increase `bytesCompactOut` for the input level of any compactions 391 // that remove bytes from a level (excluding intra-L0 compactions). 392 // compactions. 393 destLevel := -1 394 if len(ve.NewFiles) > 0 { 395 destLevel = ve.NewFiles[0].Level 396 } 397 for dfe := range ve.DeletedFiles { 398 if dfe.Level != destLevel { 399 b.bytesCompactOut[dfe.Level] += metadatas[dfe.FileNum].Size 400 } 401 } 402 } 403 404 formatUint64 := func(v uint64, _ time.Duration) string { 405 if v == 0 { 406 return "." 407 } 408 return humanize.Bytes.Uint64(v).String() 409 } 410 formatRate := func(v uint64, dur time.Duration) string { 411 if v == 0 { 412 return "." 413 } 414 secs := dur.Seconds() 415 if secs == 0 { 416 secs = 1 417 } 418 return humanize.Bytes.Uint64(uint64(float64(v)/secs)).String() + "/s" 419 } 420 421 if newestOverall.IsZero() { 422 fmt.Fprintf(stdout, "(no timestamps)\n") 423 } else { 424 // NB: bt begins unaligned with the bucket duration (m.summarizeDur), 425 // but after the first bucket will always be aligned. 426 for bi, bt := 0, oldestOverall; !bt.After(newestOverall); bi, bt = bi+1, bt.Truncate(m.summarizeDur).Add(m.summarizeDur) { 427 // Truncate the start time to calculate the bucket key, and 428 // retrieve the appropriate bucket. 429 bk := bt.Truncate(m.summarizeDur) 430 var bucket summaryBucket 431 if buckets[bk] != nil { 432 bucket = *buckets[bk] 433 } 434 435 if bi%10 == 0 { 436 fmt.Fprintf(stdout, " ") 437 fmt.Fprintf(stdout, "_______L0_______L1_______L2_______L3_______L4_______L5_______L6_____TOTAL\n") 438 } 439 fmt.Fprintf(stdout, "%s\n", bt.Format(time.RFC3339)) 440 441 // Compute the bucket duration. It may < `m.summarizeDur` if this is 442 // the first or last bucket. 443 bucketEnd := bt.Truncate(m.summarizeDur).Add(m.summarizeDur) 444 if bucketEnd.After(newestOverall) { 445 bucketEnd = newestOverall 446 } 447 dur := bucketEnd.Sub(bt) 448 449 stats := []struct { 450 label string 451 format func(uint64, time.Duration) string 452 vals [manifest.NumLevels]uint64 453 }{ 454 {"Ingest+Flush", formatUint64, bucket.bytesAdded}, 455 {"Ingest+Flush", formatRate, bucket.bytesAdded}, 456 {"Compact (out)", formatUint64, bucket.bytesCompactOut}, 457 {"Compact (out)", formatRate, bucket.bytesCompactOut}, 458 } 459 for _, stat := range stats { 460 var sum uint64 461 for _, v := range stat.vals { 462 sum += v 463 } 464 fmt.Fprintf(stdout, "%20s %8s %8s %8s %8s %8s %8s %8s %8s\n", 465 stat.label, 466 stat.format(stat.vals[0], dur), 467 stat.format(stat.vals[1], dur), 468 stat.format(stat.vals[2], dur), 469 stat.format(stat.vals[3], dur), 470 stat.format(stat.vals[4], dur), 471 stat.format(stat.vals[5], dur), 472 stat.format(stat.vals[6], dur), 473 stat.format(sum, dur)) 474 } 475 } 476 fmt.Fprintf(stdout, "%s\n", newestOverall.Format(time.RFC3339)) 477 } 478 479 dur := newestOverall.Sub(oldestOverall) 480 fmt.Fprintf(stdout, "---\n") 481 fmt.Fprintf(stdout, "Estimated start time: %s\n", oldestOverall.Format(time.RFC3339)) 482 fmt.Fprintf(stdout, "Estimated end time: %s\n", newestOverall.Format(time.RFC3339)) 483 fmt.Fprintf(stdout, "Estimated duration: %s\n", dur.String()) 484 485 return nil 486 } 487 488 func (m *manifestT) runCheck(cmd *cobra.Command, args []string) { 489 stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr() 490 ok := true 491 for _, arg := range args { 492 func() { 493 f, err := m.opts.FS.Open(arg) 494 if err != nil { 495 fmt.Fprintf(stderr, "%s\n", err) 496 ok = false 497 return 498 } 499 defer f.Close() 500 501 var v *manifest.Version 502 var cmp *base.Comparer 503 rr := record.NewReader(f, 0 /* logNum */) 504 // Contains the FileMetadata needed by BulkVersionEdit.Apply. 505 // It accumulates the additions since later edits contain 506 // deletions of earlier added files. 507 addedByFileNum := make(map[base.FileNum]*manifest.FileMetadata) 508 for { 509 offset := rr.Offset() 510 r, err := rr.Next() 511 if err != nil { 512 if err == io.EOF { 513 break 514 } 515 fmt.Fprintf(stdout, "%s: offset: %d err: %s\n", arg, offset, err) 516 ok = false 517 break 518 } 519 520 var ve manifest.VersionEdit 521 err = ve.Decode(r) 522 if err != nil { 523 fmt.Fprintf(stdout, "%s: offset: %d err: %s\n", arg, offset, err) 524 ok = false 525 break 526 } 527 var bve manifest.BulkVersionEdit 528 bve.AddedByFileNum = addedByFileNum 529 if err := bve.Accumulate(&ve); err != nil { 530 fmt.Fprintf(stderr, "%s\n", err) 531 ok = false 532 return 533 } 534 535 empty := true 536 if ve.ComparerName != "" { 537 empty = false 538 cmp = m.comparers[ve.ComparerName] 539 if cmp == nil { 540 fmt.Fprintf(stdout, "%s: offset: %d comparer %s not found", 541 arg, offset, ve.ComparerName) 542 ok = false 543 break 544 } 545 m.fmtKey.setForComparer(ve.ComparerName, m.comparers) 546 } 547 empty = empty && ve.MinUnflushedLogNum == 0 && ve.ObsoletePrevLogNum == 0 && 548 ve.LastSeqNum == 0 && len(ve.DeletedFiles) == 0 && 549 len(ve.NewFiles) == 0 550 if empty { 551 continue 552 } 553 // TODO(sbhola): add option to Apply that reports all errors instead of 554 // one error. 555 newv, err := bve.Apply(v, cmp.Compare, m.fmtKey.fn, 0, m.opts.Experimental.ReadCompactionRate, nil /* zombies */, manifest.AllowSplitUserKeys) 556 if err != nil { 557 fmt.Fprintf(stdout, "%s: offset: %d err: %s\n", 558 arg, offset, err) 559 fmt.Fprintf(stdout, "Version state before failed Apply\n") 560 m.printLevels(cmp.Compare, stdout, v) 561 fmt.Fprintf(stdout, "Version edit that failed\n") 562 for df := range ve.DeletedFiles { 563 fmt.Fprintf(stdout, " deleted: L%d %s\n", df.Level, df.FileNum) 564 } 565 for _, nf := range ve.NewFiles { 566 fmt.Fprintf(stdout, " added: L%d %s:%d", 567 nf.Level, nf.Meta.FileNum, nf.Meta.Size) 568 formatSeqNumRange(stdout, nf.Meta.SmallestSeqNum, nf.Meta.LargestSeqNum) 569 formatKeyRange(stdout, m.fmtKey, &nf.Meta.Smallest, &nf.Meta.Largest) 570 fmt.Fprintf(stdout, "\n") 571 } 572 ok = false 573 break 574 } 575 v = newv 576 } 577 }() 578 } 579 if ok { 580 fmt.Fprintf(stdout, "OK\n") 581 } 582 }