github.com/fff-chain/go-fff@v0.0.0-20220726032732-1c84420b8a99/core/rawdb/database.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package rawdb 18 19 import ( 20 "bytes" 21 "errors" 22 "fmt" 23 "math/big" 24 "os" 25 "sync/atomic" 26 "time" 27 28 "github.com/olekukonko/tablewriter" 29 30 "github.com/fff-chain/go-fff/common" 31 "github.com/fff-chain/go-fff/ethdb" 32 "github.com/fff-chain/go-fff/ethdb/leveldb" 33 "github.com/fff-chain/go-fff/ethdb/memorydb" 34 "github.com/fff-chain/go-fff/log" 35 ) 36 37 // freezerdb is a database wrapper that enabled freezer data retrievals. 38 type freezerdb struct { 39 ethdb.KeyValueStore 40 ethdb.AncientStore 41 diffStore ethdb.KeyValueStore 42 } 43 44 // Close implements io.Closer, closing both the fast key-value store as well as 45 // the slow ancient tables. 46 func (frdb *freezerdb) Close() error { 47 var errs []error 48 if err := frdb.AncientStore.Close(); err != nil { 49 errs = append(errs, err) 50 } 51 if err := frdb.KeyValueStore.Close(); err != nil { 52 errs = append(errs, err) 53 } 54 if frdb.diffStore != nil { 55 if err := frdb.diffStore.Close(); err != nil { 56 errs = append(errs, err) 57 } 58 } 59 if len(errs) != 0 { 60 return fmt.Errorf("%v", errs) 61 } 62 return nil 63 } 64 65 func (frdb *freezerdb) DiffStore() ethdb.KeyValueStore { 66 return frdb.diffStore 67 } 68 69 func (frdb *freezerdb) SetDiffStore(diff ethdb.KeyValueStore) { 70 if frdb.diffStore != nil { 71 frdb.diffStore.Close() 72 } 73 frdb.diffStore = diff 74 } 75 76 // Freeze is a helper method used for external testing to trigger and block until 77 // a freeze cycle completes, without having to sleep for a minute to trigger the 78 // automatic background run. 79 func (frdb *freezerdb) Freeze(threshold uint64) error { 80 if frdb.AncientStore.(*freezer).readonly { 81 return errReadOnly 82 } 83 // Set the freezer threshold to a temporary value 84 defer func(old uint64) { 85 atomic.StoreUint64(&frdb.AncientStore.(*freezer).threshold, old) 86 }(atomic.LoadUint64(&frdb.AncientStore.(*freezer).threshold)) 87 atomic.StoreUint64(&frdb.AncientStore.(*freezer).threshold, threshold) 88 89 // Trigger a freeze cycle and block until it's done 90 trigger := make(chan struct{}, 1) 91 frdb.AncientStore.(*freezer).trigger <- trigger 92 <-trigger 93 return nil 94 } 95 96 // nofreezedb is a database wrapper that disables freezer data retrievals. 97 type nofreezedb struct { 98 ethdb.KeyValueStore 99 diffStore ethdb.KeyValueStore 100 } 101 102 // HasAncient returns an error as we don't have a backing chain freezer. 103 func (db *nofreezedb) HasAncient(kind string, number uint64) (bool, error) { 104 return false, errNotSupported 105 } 106 107 // Ancient returns an error as we don't have a backing chain freezer. 108 func (db *nofreezedb) Ancient(kind string, number uint64) ([]byte, error) { 109 return nil, errNotSupported 110 } 111 112 // Ancients returns an error as we don't have a backing chain freezer. 113 func (db *nofreezedb) Ancients() (uint64, error) { 114 return 0, errNotSupported 115 } 116 117 // Ancients returns an error as we don't have a backing chain freezer. 118 func (db *nofreezedb) ItemAmountInAncient() (uint64, error) { 119 return 0, errNotSupported 120 } 121 122 // AncientSize returns an error as we don't have a backing chain freezer. 123 func (db *nofreezedb) AncientSize(kind string) (uint64, error) { 124 return 0, errNotSupported 125 } 126 127 // AppendAncient returns an error as we don't have a backing chain freezer. 128 func (db *nofreezedb) AppendAncient(number uint64, hash, header, body, receipts, td []byte) error { 129 return errNotSupported 130 } 131 132 // TruncateAncients returns an error as we don't have a backing chain freezer. 133 func (db *nofreezedb) TruncateAncients(items uint64) error { 134 return errNotSupported 135 } 136 137 // Sync returns an error as we don't have a backing chain freezer. 138 func (db *nofreezedb) Sync() error { 139 return errNotSupported 140 } 141 142 func (db *nofreezedb) DiffStore() ethdb.KeyValueStore { 143 return db.diffStore 144 } 145 146 func (db *nofreezedb) SetDiffStore(diff ethdb.KeyValueStore) { 147 db.diffStore = diff 148 } 149 150 func (db *nofreezedb) AncientOffSet() uint64 { 151 return 0 152 } 153 154 // NewDatabase creates a high level database on top of a given key-value data 155 // store without a freezer moving immutable chain segments into cold storage. 156 func NewDatabase(db ethdb.KeyValueStore) ethdb.Database { 157 return &nofreezedb{ 158 KeyValueStore: db, 159 } 160 } 161 162 func ReadOffSetOfCurrentAncientFreezer(db ethdb.KeyValueReader) uint64 { 163 offset, _ := db.Get(offSetOfCurrentAncientFreezer) 164 if offset == nil { 165 return 0 166 } 167 return new(big.Int).SetBytes(offset).Uint64() 168 } 169 170 func ReadOffSetOfLastAncientFreezer(db ethdb.KeyValueReader) uint64 { 171 offset, _ := db.Get(offSetOfLastAncientFreezer) 172 if offset == nil { 173 return 0 174 } 175 return new(big.Int).SetBytes(offset).Uint64() 176 } 177 178 func WriteOffSetOfCurrentAncientFreezer(db ethdb.KeyValueWriter, offset uint64) { 179 if err := db.Put(offSetOfCurrentAncientFreezer, new(big.Int).SetUint64(offset).Bytes()); err != nil { 180 log.Crit("Failed to store offSetOfAncientFreezer", "err", err) 181 } 182 } 183 func WriteOffSetOfLastAncientFreezer(db ethdb.KeyValueWriter, offset uint64) { 184 if err := db.Put(offSetOfLastAncientFreezer, new(big.Int).SetUint64(offset).Bytes()); err != nil { 185 log.Crit("Failed to store offSetOfAncientFreezer", "err", err) 186 } 187 } 188 189 // NewFreezerDb only create a freezer without statedb. 190 func NewFreezerDb(db ethdb.KeyValueStore, frz, namespace string, readonly bool, newOffSet uint64) (*freezer, error) { 191 // Create the idle freezer instance, this operation should be atomic to avoid mismatch between offset and acientDB. 192 frdb, err := newFreezer(frz, namespace, readonly) 193 if err != nil { 194 return nil, err 195 } 196 frdb.offset = newOffSet 197 frdb.frozen += newOffSet 198 return frdb, nil 199 } 200 201 // NewDatabaseWithFreezer creates a high level database on top of a given key- 202 // value data store with a freezer moving immutable chain segments into cold 203 // storage. 204 func NewDatabaseWithFreezer(db ethdb.KeyValueStore, freezer string, namespace string, readonly, disableFreeze, isLastOffset bool) (ethdb.Database, error) { 205 // Create the idle freezer instance 206 frdb, err := newFreezer(freezer, namespace, readonly) 207 if err != nil { 208 return nil, err 209 } 210 211 var offset uint64 212 // The offset of ancientDB should be handled differently in different scenarios. 213 if isLastOffset { 214 offset = ReadOffSetOfLastAncientFreezer(db) 215 } else { 216 offset = ReadOffSetOfCurrentAncientFreezer(db) 217 } 218 219 frdb.offset = offset 220 221 // Some blocks in ancientDB may have already been frozen and been pruned, so adding the offset to 222 // reprensent the absolute number of blocks already frozen. 223 frdb.frozen += offset 224 225 // Since the freezer can be stored separately from the user's key-value database, 226 // there's a fairly high probability that the user requests invalid combinations 227 // of the freezer and database. Ensure that we don't shoot ourselves in the foot 228 // by serving up conflicting data, leading to both datastores getting corrupted. 229 // 230 // - If both the freezer and key-value store is empty (no genesis), we just 231 // initialized a new empty freezer, so everything's fine. 232 // - If the key-value store is empty, but the freezer is not, we need to make 233 // sure the user's genesis matches the freezer. That will be checked in the 234 // blockchain, since we don't have the genesis block here (nor should we at 235 // this point care, the key-value/freezer combo is valid). 236 // - If neither the key-value store nor the freezer is empty, cross validate 237 // the genesis hashes to make sure they are compatible. If they are, also 238 // ensure that there's no gap between the freezer and sunsequently leveldb. 239 // - If the key-value store is not empty, but the freezer is we might just be 240 // upgrading to the freezer release, or we might have had a small chain and 241 // not frozen anything yet. Ensure that no blocks are missing yet from the 242 // key-value store, since that would mean we already had an old freezer. 243 244 // If the genesis hash is empty, we have a new key-value store, so nothing to 245 // validate in this method. If, however, the genesis hash is not nil, compare 246 // it to the freezer content. 247 // Only to check the followings when offset equal to 0, otherwise the block number 248 // in ancientdb did not start with 0, no genesis block in ancientdb as well. 249 250 if kvgenesis, _ := db.Get(headerHashKey(0)); offset == 0 && len(kvgenesis) > 0 { 251 if frozen, _ := frdb.Ancients(); frozen > 0 { 252 // If the freezer already contains something, ensure that the genesis blocks 253 // match, otherwise we might mix up freezers across chains and destroy both 254 // the freezer and the key-value store. 255 frgenesis, err := frdb.Ancient(freezerHashTable, 0) 256 if err != nil { 257 return nil, fmt.Errorf("failed to retrieve genesis from ancient %v", err) 258 } else if !bytes.Equal(kvgenesis, frgenesis) { 259 return nil, fmt.Errorf("genesis mismatch: %#x (leveldb) != %#x (ancients)", kvgenesis, frgenesis) 260 } 261 // Key-value store and freezer belong to the same network. Ensure that they 262 // are contiguous, otherwise we might end up with a non-functional freezer. 263 if kvhash, _ := db.Get(headerHashKey(frozen)); len(kvhash) == 0 { 264 // Subsequent header after the freezer limit is missing from the database. 265 // Reject startup is the database has a more recent head. 266 if *ReadHeaderNumber(db, ReadHeadHeaderHash(db)) > frozen-1 { 267 return nil, fmt.Errorf("gap (#%d) in the chain between ancients and leveldb", frozen) 268 } 269 // Database contains only older data than the freezer, this happens if the 270 // state was wiped and reinited from an existing freezer. 271 } 272 // Otherwise, key-value store continues where the freezer left off, all is fine. 273 // We might have duplicate blocks (crash after freezer write but before key-value 274 // store deletion, but that's fine). 275 } else { 276 // If the freezer is empty, ensure nothing was moved yet from the key-value 277 // store, otherwise we'll end up missing data. We check block #1 to decide 278 // if we froze anything previously or not, but do take care of databases with 279 // only the genesis block. 280 if ReadHeadHeaderHash(db) != common.BytesToHash(kvgenesis) { 281 // Key-value store contains more data than the genesis block, make sure we 282 // didn't freeze anything yet. 283 if kvblob, _ := db.Get(headerHashKey(1)); len(kvblob) == 0 { 284 return nil, errors.New("ancient chain segments already extracted, please set --datadir.ancient to the correct path") 285 } 286 // Block #1 is still in the database, we're allowed to init a new feezer 287 } 288 // Otherwise, the head header is still the genesis, we're allowed to init a new 289 // feezer. 290 } 291 } 292 293 // Freezer is consistent with the key-value database, permit combining the two 294 if !disableFreeze && !frdb.readonly { 295 go frdb.freeze(db) 296 } 297 return &freezerdb{ 298 KeyValueStore: db, 299 AncientStore: frdb, 300 }, nil 301 } 302 303 // NewMemoryDatabase creates an ephemeral in-memory key-value database without a 304 // freezer moving immutable chain segments into cold storage. 305 func NewMemoryDatabase() ethdb.Database { 306 return NewDatabase(memorydb.New()) 307 } 308 309 // NewMemoryDatabaseWithCap creates an ephemeral in-memory key-value database 310 // with an initial starting capacity, but without a freezer moving immutable 311 // chain segments into cold storage. 312 func NewMemoryDatabaseWithCap(size int) ethdb.Database { 313 return NewDatabase(memorydb.NewWithCap(size)) 314 } 315 316 // NewLevelDBDatabase creates a persistent key-value database without a freezer 317 // moving immutable chain segments into cold storage. 318 func NewLevelDBDatabase(file string, cache int, handles int, namespace string, readonly bool) (ethdb.Database, error) { 319 db, err := leveldb.New(file, cache, handles, namespace, readonly) 320 if err != nil { 321 return nil, err 322 } 323 return NewDatabase(db), nil 324 } 325 326 // NewLevelDBDatabaseWithFreezer creates a persistent key-value database with a 327 // freezer moving immutable chain segments into cold storage. 328 func NewLevelDBDatabaseWithFreezer(file string, cache int, handles int, freezer string, namespace string, readonly, disableFreeze, isLastOffset bool) (ethdb.Database, error) { 329 kvdb, err := leveldb.New(file, cache, handles, namespace, readonly) 330 if err != nil { 331 return nil, err 332 } 333 frdb, err := NewDatabaseWithFreezer(kvdb, freezer, namespace, readonly, disableFreeze, isLastOffset) 334 if err != nil { 335 kvdb.Close() 336 return nil, err 337 } 338 return frdb, nil 339 } 340 341 type counter uint64 342 343 func (c counter) String() string { 344 return fmt.Sprintf("%d", c) 345 } 346 347 func (c counter) Percentage(current uint64) string { 348 return fmt.Sprintf("%d", current*100/uint64(c)) 349 } 350 351 // stat stores sizes and count for a parameter 352 type stat struct { 353 size common.StorageSize 354 count counter 355 } 356 357 // Add size to the stat and increase the counter by 1 358 func (s *stat) Add(size common.StorageSize) { 359 s.size += size 360 s.count++ 361 } 362 363 func (s *stat) Size() string { 364 return s.size.String() 365 } 366 367 func (s *stat) Count() string { 368 return s.count.String() 369 } 370 func AncientInspect(db ethdb.Database) error { 371 offset := counter(ReadOffSetOfCurrentAncientFreezer(db)) 372 // Get number of ancient rows inside the freezer. 373 ancients := counter(0) 374 if count, err := db.ItemAmountInAncient(); err != nil { 375 log.Error("failed to get the items amount in ancientDB", "err", err) 376 return err 377 } else { 378 ancients = counter(count) 379 } 380 var endNumber counter 381 if offset+ancients <= 0 { 382 endNumber = 0 383 } else { 384 endNumber = offset + ancients - 1 385 } 386 stats := [][]string{ 387 {"Offset/StartBlockNumber", "Offset/StartBlockNumber of ancientDB", offset.String()}, 388 {"Amount of remained items in AncientStore", "Remaining items of ancientDB", ancients.String()}, 389 {"The last BlockNumber within ancientDB", "The last BlockNumber", endNumber.String()}, 390 } 391 table := tablewriter.NewWriter(os.Stdout) 392 table.SetHeader([]string{"Database", "Category", "Items"}) 393 table.SetFooter([]string{"", "AncientStore information", ""}) 394 table.AppendBulk(stats) 395 table.Render() 396 397 return nil 398 } 399 400 // InspectDatabase traverses the entire database and checks the size 401 // of all different categories of data. 402 func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { 403 it := db.NewIterator(keyPrefix, keyStart) 404 defer it.Release() 405 406 var ( 407 count int64 408 start = time.Now() 409 logged = time.Now() 410 411 // Key-value store statistics 412 headers stat 413 bodies stat 414 receipts stat 415 tds stat 416 numHashPairings stat 417 hashNumPairings stat 418 tries stat 419 codes stat 420 txLookups stat 421 accountSnaps stat 422 storageSnaps stat 423 preimages stat 424 bloomBits stat 425 cliqueSnaps stat 426 parliaSnaps stat 427 428 // Ancient store statistics 429 ancientHeadersSize common.StorageSize 430 ancientBodiesSize common.StorageSize 431 ancientReceiptsSize common.StorageSize 432 ancientTdsSize common.StorageSize 433 ancientHashesSize common.StorageSize 434 435 // Les statistic 436 chtTrieNodes stat 437 bloomTrieNodes stat 438 439 // Meta- and unaccounted data 440 metadata stat 441 unaccounted stat 442 shutdownInfo stat 443 444 // Totals 445 total common.StorageSize 446 ) 447 // Inspect key-value database first. 448 for it.Next() { 449 var ( 450 key = it.Key() 451 size = common.StorageSize(len(key) + len(it.Value())) 452 ) 453 total += size 454 switch { 455 case bytes.HasPrefix(key, headerPrefix) && len(key) == (len(headerPrefix)+8+common.HashLength): 456 headers.Add(size) 457 case bytes.HasPrefix(key, blockBodyPrefix) && len(key) == (len(blockBodyPrefix)+8+common.HashLength): 458 bodies.Add(size) 459 case bytes.HasPrefix(key, blockReceiptsPrefix) && len(key) == (len(blockReceiptsPrefix)+8+common.HashLength): 460 receipts.Add(size) 461 case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix): 462 tds.Add(size) 463 case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix): 464 numHashPairings.Add(size) 465 case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength): 466 hashNumPairings.Add(size) 467 case len(key) == common.HashLength: 468 tries.Add(size) 469 case bytes.HasPrefix(key, CodePrefix) && len(key) == len(CodePrefix)+common.HashLength: 470 codes.Add(size) 471 case bytes.HasPrefix(key, txLookupPrefix) && len(key) == (len(txLookupPrefix)+common.HashLength): 472 txLookups.Add(size) 473 case bytes.HasPrefix(key, SnapshotAccountPrefix) && len(key) == (len(SnapshotAccountPrefix)+common.HashLength): 474 accountSnaps.Add(size) 475 case bytes.HasPrefix(key, SnapshotStoragePrefix) && len(key) == (len(SnapshotStoragePrefix)+2*common.HashLength): 476 storageSnaps.Add(size) 477 case bytes.HasPrefix(key, preimagePrefix) && len(key) == (len(preimagePrefix)+common.HashLength): 478 preimages.Add(size) 479 case bytes.HasPrefix(key, bloomBitsPrefix) && len(key) == (len(bloomBitsPrefix)+10+common.HashLength): 480 bloomBits.Add(size) 481 case bytes.HasPrefix(key, BloomBitsIndexPrefix): 482 bloomBits.Add(size) 483 case bytes.HasPrefix(key, []byte("clique-")) && len(key) == 7+common.HashLength: 484 cliqueSnaps.Add(size) 485 case bytes.HasPrefix(key, []byte("parlia-")) && len(key) == 7+common.HashLength: 486 parliaSnaps.Add(size) 487 488 case bytes.HasPrefix(key, []byte("cht-")) || 489 bytes.HasPrefix(key, []byte("chtIndexV2-")) || 490 bytes.HasPrefix(key, []byte("chtRootV2-")): // Canonical hash trie 491 chtTrieNodes.Add(size) 492 case bytes.HasPrefix(key, []byte("blt-")) || 493 bytes.HasPrefix(key, []byte("bltIndex-")) || 494 bytes.HasPrefix(key, []byte("bltRoot-")): // Bloomtrie sub 495 bloomTrieNodes.Add(size) 496 case bytes.Equal(key, uncleanShutdownKey): 497 shutdownInfo.Add(size) 498 default: 499 var accounted bool 500 for _, meta := range [][]byte{ 501 databaseVersionKey, headHeaderKey, headBlockKey, headFastBlockKey, lastPivotKey, 502 fastTrieProgressKey, snapshotDisabledKey, snapshotRootKey, snapshotJournalKey, 503 snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey, 504 uncleanShutdownKey, badBlockKey, 505 } { 506 if bytes.Equal(key, meta) { 507 metadata.Add(size) 508 accounted = true 509 break 510 } 511 } 512 if !accounted { 513 unaccounted.Add(size) 514 } 515 } 516 count++ 517 if count%1000 == 0 && time.Since(logged) > 8*time.Second { 518 log.Info("Inspecting database", "count", count, "elapsed", common.PrettyDuration(time.Since(start))) 519 logged = time.Now() 520 } 521 } 522 // Inspect append-only file store then. 523 ancientSizes := []*common.StorageSize{&ancientHeadersSize, &ancientBodiesSize, &ancientReceiptsSize, &ancientHashesSize, &ancientTdsSize} 524 for i, category := range []string{freezerHeaderTable, freezerBodiesTable, freezerReceiptTable, freezerHashTable, freezerDifficultyTable} { 525 if size, err := db.AncientSize(category); err == nil { 526 *ancientSizes[i] += common.StorageSize(size) 527 total += common.StorageSize(size) 528 } 529 } 530 // Get number of ancient rows inside the freezer 531 ancients := counter(0) 532 if count, err := db.ItemAmountInAncient(); err == nil { 533 ancients = counter(count) 534 } 535 536 // Display the database statistic. 537 stats := [][]string{ 538 {"Key-Value store", "Headers", headers.Size(), headers.Count()}, 539 {"Key-Value store", "Bodies", bodies.Size(), bodies.Count()}, 540 {"Key-Value store", "Receipt lists", receipts.Size(), receipts.Count()}, 541 {"Key-Value store", "Difficulties", tds.Size(), tds.Count()}, 542 {"Key-Value store", "Block number->hash", numHashPairings.Size(), numHashPairings.Count()}, 543 {"Key-Value store", "Block hash->number", hashNumPairings.Size(), hashNumPairings.Count()}, 544 {"Key-Value store", "Transaction index", txLookups.Size(), txLookups.Count()}, 545 {"Key-Value store", "Bloombit index", bloomBits.Size(), bloomBits.Count()}, 546 {"Key-Value store", "Contract codes", codes.Size(), codes.Count()}, 547 {"Key-Value store", "Trie nodes", tries.Size(), tries.Count()}, 548 {"Key-Value store", "Trie preimages", preimages.Size(), preimages.Count()}, 549 {"Key-Value store", "Account snapshot", accountSnaps.Size(), accountSnaps.Count()}, 550 {"Key-Value store", "Storage snapshot", storageSnaps.Size(), storageSnaps.Count()}, 551 {"Key-Value store", "Clique snapshots", cliqueSnaps.Size(), cliqueSnaps.Count()}, 552 {"Key-Value store", "Parlia snapshots", parliaSnaps.Size(), parliaSnaps.Count()}, 553 {"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()}, 554 {"Key-Value store", "Shutdown metadata", shutdownInfo.Size(), shutdownInfo.Count()}, 555 {"Ancient store", "Headers", ancientHeadersSize.String(), ancients.String()}, 556 {"Ancient store", "Bodies", ancientBodiesSize.String(), ancients.String()}, 557 {"Ancient store", "Receipt lists", ancientReceiptsSize.String(), ancients.String()}, 558 {"Ancient store", "Difficulties", ancientTdsSize.String(), ancients.String()}, 559 {"Ancient store", "Block number->hash", ancientHashesSize.String(), ancients.String()}, 560 {"Light client", "CHT trie nodes", chtTrieNodes.Size(), chtTrieNodes.Count()}, 561 {"Light client", "Bloom trie nodes", bloomTrieNodes.Size(), bloomTrieNodes.Count()}, 562 } 563 table := tablewriter.NewWriter(os.Stdout) 564 table.SetHeader([]string{"Database", "Category", "Size", "Items"}) 565 table.SetFooter([]string{"", "Total", total.String(), " "}) 566 table.AppendBulk(stats) 567 table.Render() 568 569 if unaccounted.size > 0 { 570 log.Error("Database contains unaccounted data", "size", unaccounted.size, "count", unaccounted.count) 571 } 572 573 return nil 574 }