diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 582213e587..15dfd228bc 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -103,6 +103,7 @@ var ( utils.TransactionHistoryFlag, utils.StateHistoryFlag, utils.PathDBSyncFlag, + utils.JournalFileFlag, utils.LightServeFlag, // deprecated utils.LightIngressFlag, // deprecated utils.LightEgressFlag, // deprecated diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index bec7929026..f92836593c 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -365,6 +365,12 @@ var ( Value: false, Category: flags.StateCategory, } + JournalFileFlag = &cli.BoolFlag{ + Name: "journalfile", + Usage: "Enable using journal file to store the TrieJournal instead of KVDB in pbss (default = false)", + Value: false, + Category: flags.StateCategory, + } StateHistoryFlag = &cli.Uint64Flag{ Name: "history.state", Usage: "Number of recent blocks to retain state history for (default = 90,000 blocks, 0 = entire chain)", @@ -1962,6 +1968,10 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.IsSet(PathDBSyncFlag.Name) { cfg.PathSyncFlush = true } + if ctx.IsSet(JournalFileFlag.Name) { + cfg.JournalFileEnabled = true + } + if ctx.String(GCModeFlag.Name) == "archive" && cfg.TransactionHistory != 0 { cfg.TransactionHistory = 0 log.Warn("Disabled transaction unindexing for archive node") diff --git a/core/blockchain.go b/core/blockchain.go index 1ec98a09b4..09c93bedc3 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -165,6 +165,7 @@ type CacheConfig struct { StateHistory uint64 // Number of blocks from head whose state histories are reserved. StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top PathSyncFlush bool // Whether sync flush the trienodebuffer of pathdb to disk. + JournalFilePath string SnapshotNoBuild bool // Whether the background generation is allowed SnapshotWait bool // Wait for snapshot construction on startup. TODO(karalabe): This is a dirty hack for testing, nuke it @@ -184,10 +185,11 @@ func (c *CacheConfig) triedbConfig() *triedb.Config { } if c.StateScheme == rawdb.PathScheme { config.PathDB = &pathdb.Config{ - SyncFlush: c.PathSyncFlush, - StateHistory: c.StateHistory, - CleanCacheSize: c.TrieCleanLimit * 1024 * 1024, - DirtyCacheSize: c.TrieDirtyLimit * 1024 * 1024, + SyncFlush: c.PathSyncFlush, + StateHistory: c.StateHistory, + CleanCacheSize: c.TrieCleanLimit * 1024 * 1024, + DirtyCacheSize: c.TrieDirtyLimit * 1024 * 1024, + JournalFilePath: c.JournalFilePath, } } return config diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 0fe10da8b9..eead20c5eb 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -26,14 +26,13 @@ import ( "strings" "time" - "github.com/olekukonko/tablewriter" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/leveldb" "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/ethdb/pebble" "github.com/ethereum/go-ethereum/log" + "github.com/olekukonko/tablewriter" ) // freezerdb is a database wrapper that enables freezer data retrievals. diff --git a/eth/backend.go b/eth/backend.go index 9078cce1d4..c864ea96c7 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -69,6 +69,8 @@ import ( const ( ChainDBNamespace = "eth/db/chaindata/" + JournalFileName = "trie.journal" + ChainData = "chaindata" ) // Config contains the configuration options of the ETH protocol. @@ -137,7 +139,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { } // Assemble the Ethereum object - chainDb, err := stack.OpenAndMergeDatabase("chaindata", config.DatabaseCache, config.DatabaseHandles, + chainDb, err := stack.OpenAndMergeDatabase(ChainData, config.DatabaseCache, config.DatabaseHandles, config.DatabaseFreezer, config.DatabaseDiff, ChainDBNamespace, false, config.PersistDiff, config.PruneAncientData) if err != nil { return nil, err @@ -250,6 +252,18 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { rawdb.WriteDatabaseVersion(chainDb, core.BlockChainVersion) } } + var ( + journalFilePath string + path string + ) + if config.JournalFileEnabled { + if stack.IsSeparatedDB() { + path = ChainData + "/state" + } else { + path = ChainData + } + journalFilePath = stack.ResolvePath(path) + "/" + JournalFileName + } var ( vmConfig = vm.Config{ EnablePreimageRecording: config.EnablePreimageRecording, @@ -267,6 +281,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { StateHistory: config.StateHistory, StateScheme: config.StateScheme, PathSyncFlush: config.PathSyncFlush, + JournalFilePath: journalFilePath, } ) bcOps := make([]core.BlockChainOption, 0) diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 95a311914e..7878daa3d0 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -117,8 +117,9 @@ type Config struct { // State scheme represents the scheme used to store ethereum states and trie // nodes on top. It can be 'hash', 'path', or none which means use the scheme // consistent with persistent state. - StateScheme string `toml:",omitempty"` // State scheme used to store ethereum state and merkle trie nodes on top - PathSyncFlush bool `toml:",omitempty"` // State scheme used to store ethereum state and merkle trie nodes on top + StateScheme string `toml:",omitempty"` // State scheme used to store ethereum state and merkle trie nodes on top + PathSyncFlush bool `toml:",omitempty"` // State scheme used to store ethereum state and merkle trie nodes on top + JournalFileEnabled bool // Whether the TrieJournal is stored using journal file // RequiredBlocks is a set of block number -> hash mappings which must be in the // canonical chain of all remote peers. Setting the option makes geth verify the diff --git a/node/node.go b/node/node.go index bd102d32fb..dd220c9abe 100644 --- a/node/node.go +++ b/node/node.go @@ -77,6 +77,8 @@ const ( const chainDataHandlesPercentage = 80 +const StateDBNamespace = "eth/db/statedata/" + // New creates a new P2P node, ready for protocol registration. func New(conf *Config) (*Node, error) { // Copy config and resolve the datadir so future changes to the current @@ -791,7 +793,7 @@ func (n *Node) OpenAndMergeDatabase(name string, cache, handles int, freezer, di // Open the separated state database if the state directory exists if n.IsSeparatedDB() { // Allocate half of the handles and cache to this separate state data database - statediskdb, err = n.OpenDatabaseWithFreezer(name+"/state", cache/2, chainDataHandles/2, "", "eth/db/statedata/", readonly, false, false, pruneAncientData) + statediskdb, err = n.OpenDatabaseWithFreezer(name+"/state", cache/2, chainDataHandles/2, "", StateDBNamespace, readonly, false, false, pruneAncientData) if err != nil { return nil, err } diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index 247e684928..2afd14b1a5 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "io" + "os" "sort" "strconv" "sync" @@ -96,12 +97,13 @@ type layer interface { // Config contains the settings for database. type Config struct { - SyncFlush bool // Flag of trienodebuffer sync flush cache to disk - StateHistory uint64 // Number of recent blocks to maintain state history for - CleanCacheSize int // Maximum memory allowance (in bytes) for caching clean nodes - DirtyCacheSize int // Maximum memory allowance (in bytes) for caching dirty nodes - ReadOnly bool // Flag whether the database is opened in read only mode. - NoTries bool + SyncFlush bool // Flag of trienodebuffer sync flush cache to disk + StateHistory uint64 // Number of recent blocks to maintain state history for + CleanCacheSize int // Maximum memory allowance (in bytes) for caching clean nodes + DirtyCacheSize int // Maximum memory allowance (in bytes) for caching dirty nodes + ReadOnly bool // Flag whether the database is opened in read only mode. + NoTries bool + JournalFilePath string } // sanitize checks the provided user configurations and changes anything that's @@ -316,7 +318,7 @@ func (db *Database) Enable(root common.Hash) error { // Drop the stale state journal in persistent database and // reset the persistent state id back to zero. batch := db.diskdb.NewBatch() - rawdb.DeleteTrieJournal(batch) + db.DeleteTrieJournal(batch) rawdb.WritePersistentStateID(batch, 0) if err := batch.Write(); err != nil { return err @@ -380,7 +382,7 @@ func (db *Database) Recover(root common.Hash, loader triestate.TrieLoader) error // disk layer won't be accessible from outside. db.tree.reset(dl) } - rawdb.DeleteTrieJournal(db.diskdb) + db.DeleteTrieJournal(db.diskdb) _, err := truncateFromHead(db.diskdb, db.freezer, dl.stateID()) if err != nil { return err @@ -524,3 +526,20 @@ func (db *Database) GetAllRooHash() [][]string { data = append(data, []string{"-1", db.tree.bottom().rootHash().String()}) return data } + +func (db *Database) DeleteTrieJournal(writer ethdb.KeyValueWriter) error { + filePath := db.config.JournalFilePath + if len(filePath) == 0 { + rawdb.DeleteTrieJournal(writer) + } else { + _, err := os.Stat(filePath) + if os.IsNotExist(err) { + return err + } + errRemove := os.Remove(filePath) + if errRemove != nil { + log.Crit("Failed to remove tries journal", "journal path", filePath, "err", err) + } + } + return nil +} diff --git a/triedb/pathdb/journal.go b/triedb/pathdb/journal.go index 1100e03ba3..a1d17b720c 100644 --- a/triedb/pathdb/journal.go +++ b/triedb/pathdb/journal.go @@ -18,15 +18,19 @@ package pathdb import ( "bytes" + "crypto/sha256" "errors" "fmt" "io" + "io/fs" + "os" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie/trienode" @@ -70,13 +74,134 @@ type journalStorage struct { Slots [][]byte } +type JournalWriter interface { + io.Writer + + Close() + Size() uint64 +} + +type JournalReader interface { + io.Reader + Close() +} + +type JournalFileWriter struct { + file *os.File +} + +type JournalFileReader struct { + file *os.File +} + +type JournalKVWriter struct { + journalBuf bytes.Buffer + diskdb ethdb.Database +} + +type JournalKVReader struct { + journalBuf *bytes.Buffer +} + +// Write appends b directly to the encoder output. +func (fw *JournalFileWriter) Write(b []byte) (int, error) { + return fw.file.Write(b) +} + +func (fw *JournalFileWriter) Close() { + fw.file.Close() +} + +func (fw *JournalFileWriter) Size() uint64 { + if fw.file == nil { + return 0 + } + fileInfo, err := fw.file.Stat() + if err != nil { + log.Crit("Failed to stat journal", "err", err) + } + return uint64(fileInfo.Size()) +} + +func (kw *JournalKVWriter) Write(b []byte) (int, error) { + return kw.journalBuf.Write(b) +} + +func (kw *JournalKVWriter) Close() { + rawdb.WriteTrieJournal(kw.diskdb, kw.journalBuf.Bytes()) + kw.journalBuf.Reset() +} + +func (kw *JournalKVWriter) Size() uint64 { + return uint64(kw.journalBuf.Len()) +} + +func (fr *JournalFileReader) Read(p []byte) (n int, err error) { + return fr.file.Read(p) +} + +func (fr *JournalFileReader) Close() { + fr.file.Close() +} + +func (kr *JournalKVReader) Read(p []byte) (n int, err error) { + return kr.journalBuf.Read(p) +} + +func (kr *JournalKVReader) Close() { +} + +func newJournalWriter(file string, db ethdb.Database) JournalWriter { + if len(file) == 0 { + return &JournalKVWriter{ + diskdb: db, + } + } else { + fd, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return nil + } + return &JournalFileWriter{ + file: fd, + } + } +} + +func newJournalReader(file string, db ethdb.Database) (JournalReader, error) { + if len(file) == 0 { + journal := rawdb.ReadTrieJournal(db) + if len(journal) == 0 { + return nil, errMissJournal + } + return &JournalKVReader{ + journalBuf: bytes.NewBuffer(journal), + }, nil + } else { + fd, err := os.Open(file) + if errors.Is(err, fs.ErrNotExist) { + return nil, errMissJournal + } + if err != nil { + return nil, err + } + return &JournalFileReader{ + file: fd, + }, nil + } +} + // loadJournal tries to parse the layer journal from the disk. func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) { - journal := rawdb.ReadTrieJournal(db.diskdb) - if len(journal) == 0 { - return nil, errMissJournal + start := time.Now() + reader, err := newJournalReader(db.config.JournalFilePath, db.diskdb) + + if err != nil { + return nil, err } - r := rlp.NewStream(bytes.NewReader(journal), 0) + if reader != nil { + defer reader.Close() + } + r := rlp.NewStream(reader, 0) // Firstly, resolve the first element as the journal version version, err := r.Uint64() @@ -108,7 +233,7 @@ func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) { if err != nil { return nil, err } - log.Debug("Loaded layer journal", "diskroot", diskRoot, "diffhead", head.rootHash()) + log.Info("Loaded layer journal", "diskroot", diskRoot, "diffhead", head.rootHash(), "elapsed", common.PrettyDuration(time.Since(start))) return head, nil } @@ -138,14 +263,25 @@ func (db *Database) loadLayers() layer { func (db *Database) loadDiskLayer(r *rlp.Stream) (layer, error) { // Resolve disk layer root var root common.Hash - if err := r.Decode(&root); err != nil { + var length uint64 + if err := r.Decode(&length); err != nil { + return nil, fmt.Errorf("load disk length: %v", err) + } + + var journalEncodedBuff []byte + if err := r.Decode(&journalEncodedBuff); err != nil { + return nil, fmt.Errorf("load disk journal: %v", err) + } + + journalBuf := rlp.NewStream(bytes.NewReader(journalEncodedBuff), 0) + if err := journalBuf.Decode(&root); err != nil { return nil, fmt.Errorf("load disk root: %v", err) } // Resolve the state id of disk layer, it can be different // with the persistent id tracked in disk, the id distance // is the number of transitions aggregated in disk layer. var id uint64 - if err := r.Decode(&id); err != nil { + if err := journalBuf.Decode(&id); err != nil { return nil, fmt.Errorf("load state id: %v", err) } stored := rawdb.ReadPersistentStateID(db.diskdb) @@ -154,7 +290,7 @@ func (db *Database) loadDiskLayer(r *rlp.Stream) (layer, error) { } // Resolve nodes cached in node buffer var encoded []journalNodes - if err := r.Decode(&encoded); err != nil { + if err := journalBuf.Decode(&encoded); err != nil { return nil, fmt.Errorf("load disk nodes: %v", err) } nodes := make(map[common.Hash]map[string]*trienode.Node) @@ -169,6 +305,17 @@ func (db *Database) loadDiskLayer(r *rlp.Stream) (layer, error) { } nodes[entry.Owner] = subset } + + var shaSum [32]byte + if err := r.Decode(&shaSum); err != nil { + return nil, fmt.Errorf("load shasum: %v", err) + } + + expectSum := sha256.Sum256(journalEncodedBuff) + if shaSum != expectSum { + return nil, fmt.Errorf("expect shaSum: %v, real:%v", expectSum, shaSum) + } + // Calculate the internal state transitions by id difference. base := newDiskLayer(root, id, db, nil, NewTrieNodeBuffer(db.config.SyncFlush, db.bufferSize, nodes, id-stored)) return base, nil @@ -179,7 +326,22 @@ func (db *Database) loadDiskLayer(r *rlp.Stream) (layer, error) { func (db *Database) loadDiffLayer(parent layer, r *rlp.Stream) (layer, error) { // Read the next diff journal entry var root common.Hash - if err := r.Decode(&root); err != nil { + var length uint64 + if err := r.Decode(&length); err != nil { + // The first read may fail with EOF, marking the end of the journal + if err == io.EOF { + return parent, nil + } + return nil, fmt.Errorf("load disk length : %v", err) + } + var journalEncodedBuff []byte + if err := r.Decode(&journalEncodedBuff); err != nil { + return nil, fmt.Errorf("load disk journal buffer: %v", err) + } + + journalBuf := rlp.NewStream(bytes.NewReader(journalEncodedBuff), 0) + + if err := journalBuf.Decode(&root); err != nil { // The first read may fail with EOF, marking the end of the journal if err == io.EOF { return parent, nil @@ -187,12 +349,12 @@ func (db *Database) loadDiffLayer(parent layer, r *rlp.Stream) (layer, error) { return nil, fmt.Errorf("load diff root: %v", err) } var block uint64 - if err := r.Decode(&block); err != nil { + if err := journalBuf.Decode(&block); err != nil { return nil, fmt.Errorf("load block number: %v", err) } // Read in-memory trie nodes from journal var encoded []journalNodes - if err := r.Decode(&encoded); err != nil { + if err := journalBuf.Decode(&encoded); err != nil { return nil, fmt.Errorf("load diff nodes: %v", err) } nodes := make(map[common.Hash]map[string]*trienode.Node) @@ -215,13 +377,13 @@ func (db *Database) loadDiffLayer(parent layer, r *rlp.Stream) (layer, error) { storages = make(map[common.Address]map[common.Hash][]byte) incomplete = make(map[common.Address]struct{}) ) - if err := r.Decode(&jaccounts); err != nil { + if err := journalBuf.Decode(&jaccounts); err != nil { return nil, fmt.Errorf("load diff accounts: %v", err) } for i, addr := range jaccounts.Addresses { accounts[addr] = jaccounts.Accounts[i] } - if err := r.Decode(&jstorages); err != nil { + if err := journalBuf.Decode(&jstorages); err != nil { return nil, fmt.Errorf("load diff storages: %v", err) } for _, entry := range jstorages { @@ -238,6 +400,17 @@ func (db *Database) loadDiffLayer(parent layer, r *rlp.Stream) (layer, error) { } storages[entry.Account] = set } + var shaSum [32]byte + if err := r.Decode(&shaSum); err != nil { + return nil, fmt.Errorf("load shasum: %v", err) + } + + expectSum := sha256.Sum256(journalEncodedBuff) + if shaSum != expectSum { + return nil, fmt.Errorf("expect shaSum: %v, real:%v", expectSum, shaSum) + } + log.Debug("Loaded diff layer journal", "root", root, "parent", parent.rootHash(), "id", parent.stateID()+1, "block", block) + return db.loadDiffLayer(newDiffLayer(parent, root, parent.stateID()+1, block, nodes, triestate.New(accounts, storages, incomplete)), r) } @@ -247,16 +420,19 @@ func (dl *diskLayer) journal(w io.Writer) error { dl.lock.RLock() defer dl.lock.RUnlock() + // Create a buffer to store encoded data + journalBuf := new(bytes.Buffer) + // Ensure the layer didn't get stale if dl.stale { return errSnapshotStale } // Step one, write the disk root into the journal. - if err := rlp.Encode(w, dl.root); err != nil { + if err := rlp.Encode(journalBuf, dl.root); err != nil { return err } // Step two, write the corresponding state id into the journal - if err := rlp.Encode(w, dl.id); err != nil { + if err := rlp.Encode(journalBuf, dl.id); err != nil { return err } // Step three, write all unwritten nodes into the journal @@ -269,10 +445,22 @@ func (dl *diskLayer) journal(w io.Writer) error { } nodes = append(nodes, entry) } - if err := rlp.Encode(w, nodes); err != nil { + if err := rlp.Encode(journalBuf, nodes); err != nil { + return err + } + + // Store the journal buf into w and calculate checksum + if err := rlp.Encode(w, uint64(journalBuf.Len())); err != nil { return err } - log.Debug("Journaled pathdb disk layer", "root", dl.root, "nodes", len(bufferNodes)) + shasum := sha256.Sum256(journalBuf.Bytes()) + if err := rlp.Encode(w, journalBuf.Bytes()); err != nil { + return err + } + if err := rlp.Encode(w, shasum); err != nil { + return err + } + log.Info("Journaled pathdb disk layer", "root", dl.root, "nodes", len(bufferNodes)) return nil } @@ -286,11 +474,13 @@ func (dl *diffLayer) journal(w io.Writer) error { if err := dl.parent.journal(w); err != nil { return err } + // Create a buffer to store encoded data + journalBuf := new(bytes.Buffer) // Everything below was journaled, persist this layer too - if err := rlp.Encode(w, dl.root); err != nil { + if err := rlp.Encode(journalBuf, dl.root); err != nil { return err } - if err := rlp.Encode(w, dl.block); err != nil { + if err := rlp.Encode(journalBuf, dl.block); err != nil { return err } // Write the accumulated trie nodes into buffer @@ -302,7 +492,7 @@ func (dl *diffLayer) journal(w io.Writer) error { } nodes = append(nodes, entry) } - if err := rlp.Encode(w, nodes); err != nil { + if err := rlp.Encode(journalBuf, nodes); err != nil { return err } // Write the accumulated state changes into buffer @@ -311,7 +501,7 @@ func (dl *diffLayer) journal(w io.Writer) error { jacct.Addresses = append(jacct.Addresses, addr) jacct.Accounts = append(jacct.Accounts, account) } - if err := rlp.Encode(w, jacct); err != nil { + if err := rlp.Encode(journalBuf, jacct); err != nil { return err } storage := make([]journalStorage, 0, len(dl.states.Storages)) @@ -326,10 +516,22 @@ func (dl *diffLayer) journal(w io.Writer) error { } storage = append(storage, entry) } - if err := rlp.Encode(w, storage); err != nil { + if err := rlp.Encode(journalBuf, storage); err != nil { + return err + } + + // Store the journal buf into w and calculate checksum + if err := rlp.Encode(w, uint64(journalBuf.Len())); err != nil { return err } - log.Debug("Journaled pathdb diff layer", "root", dl.root, "parent", dl.parent.rootHash(), "id", dl.stateID(), "block", dl.block, "nodes", len(dl.nodes)) + shasum := sha256.Sum256(journalBuf.Bytes()) + if err := rlp.Encode(w, journalBuf.Bytes()); err != nil { + return err + } + if err := rlp.Encode(w, shasum); err != nil { + return err + } + log.Info("Journaled pathdb diff layer", "root", dl.root, "parent", dl.parent.rootHash(), "id", dl.stateID(), "block", dl.block, "nodes", len(dl.nodes)) return nil } @@ -362,7 +564,10 @@ func (db *Database) Journal(root common.Hash) error { return errDatabaseReadOnly } // Firstly write out the metadata of journal - journal := new(bytes.Buffer) + db.DeleteTrieJournal(db.diskdb) + journal := newJournalWriter(db.config.JournalFilePath, db.diskdb) + defer journal.Close() + if err := rlp.Encode(journal, journalVersion); err != nil { return err } @@ -381,10 +586,10 @@ func (db *Database) Journal(root common.Hash) error { return err } // Store the journal into the database and return - rawdb.WriteTrieJournal(db.diskdb, journal.Bytes()) + journalSize := journal.Size() // Set the db in read only mode to reject all following mutations db.readOnly = true - log.Info("Persisted dirty state to disk", "size", common.StorageSize(journal.Len()), "elapsed", common.PrettyDuration(time.Since(start))) + log.Info("Persisted dirty state to disk", "size", common.StorageSize(journalSize), "elapsed", common.PrettyDuration(time.Since(start))) return nil }