Skip to content

Commit 5602cc7

Browse files
committed
Merge bitcoin/bitcoin#16981: Improve runtime performance of --reindex
db92989 Faster -reindex by initially deserializing only headers (Larry Ruane) c72de99 util: add CBufferedFile::SkipTo() to move ahead in the stream (Larry Ruane) 48a6890 Add LoadExternalBlockFile() benchmark (Larry Ruane) Pull request description: ### Background During the first part of reindexing, `LoadExternalBlockFile()` sequentially reads raw blocks from the `blocks/blk00nnn.dat` files (rather than receiving them from peers, as with initial block download) and eventually adds all of them to the block index. When an individual block is initially read, it can't be immediately added unless all its ancestors have been added, which is rare (only about 8% of the time), because the blocks are not sorted by height. When the block can't be immediately added to the block index, its disk location is saved in a map so it can be added later. When its parent is later added to the block index, `LoadExternalBlockFile()` reads and deserializes the block from disk a second time and adds it to the block index. Most blocks (92%) get deserialized twice. ### This PR During the initial read, it's rarely useful to deserialize the entire block; only the header is needed to determine if the block can be added to the block index immediately. This change to `LoadExternalBlockFile()` initially deserializes only a block's header, then deserializes the entire block only if it can be added immediately. This reduces reindex time on mainnet by 7 hours on a Raspberry Pi, which translates to around a 25% reduction in the first part of reindexing (adding blocks to the index), and about a 6% reduction in overall reindex time. Summary: The performance gain is the result of deserializing each block only once, except its header which is deserialized twice, but the header is only 80 bytes. ACKs for top commit: andrewtoth: ACK db92989 achow101: ACK db92989 aureleoules: ACK db92989 - minor changes and new benchmark since last review theStack: re-ACK db92989 stickies-v: re-ACK db92989 Tree-SHA512: 5a5377192c11edb5b662e18f511c9beb8f250bc88aeadf2f404c92c3232a7617bade50477ebf16c0602b9bd3b68306d3ee7615de58acfd8cae664d28bb7b0136
2 parents 547a963 + db92989 commit 5602cc7

File tree

6 files changed

+238
-37
lines changed

6 files changed

+238
-37
lines changed

src/Makefile.bench.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ bench_bench_bitcoin_SOURCES = \
3232
bench/examples.cpp \
3333
bench/gcs_filter.cpp \
3434
bench/hashpadding.cpp \
35+
bench/load_external.cpp \
3536
bench/lockedpool.cpp \
3637
bench/logging.cpp \
3738
bench/mempool_eviction.cpp \

src/bench/load_external.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2022 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or https://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <bench/bench.h>
6+
#include <bench/data.h>
7+
#include <chainparams.h>
8+
#include <test/util/setup_common.h>
9+
#include <validation.h>
10+
11+
/**
12+
* The LoadExternalBlockFile() function is used during -reindex and -loadblock.
13+
*
14+
* Create a test file that's similar to a datadir/blocks/blk?????.dat file,
15+
* It contains around 134 copies of the same block (typical size of real block files).
16+
* For each block in the file, LoadExternalBlockFile() won't find its parent,
17+
* and so will skip the block. (In the real system, it will re-read the block
18+
* from disk later when it encounters its parent.)
19+
*
20+
* This benchmark measures the performance of deserializing the block (or just
21+
* its header, beginning with PR 16981).
22+
*/
23+
static void LoadExternalBlockFile(benchmark::Bench& bench)
24+
{
25+
const auto testing_setup{MakeNoLogFileContext<const TestingSetup>(CBaseChainParams::MAIN)};
26+
27+
// Create a single block as in the blocks files (magic bytes, block size,
28+
// block data) as a stream object.
29+
const fs::path blkfile{testing_setup.get()->m_path_root / "blk.dat"};
30+
CDataStream ss(SER_DISK, 0);
31+
auto params{testing_setup->m_node.chainman->GetParams()};
32+
ss << params.MessageStart();
33+
ss << static_cast<uint32_t>(benchmark::data::block413567.size());
34+
// We can't use the streaming serialization (ss << benchmark::data::block413567)
35+
// because that first writes a compact size.
36+
ss.write(MakeByteSpan(benchmark::data::block413567));
37+
38+
// Create the test file.
39+
{
40+
// "wb+" is "binary, O_RDWR | O_CREAT | O_TRUNC".
41+
FILE* file{fsbridge::fopen(blkfile, "wb+")};
42+
// Make the test block file about 128 MB in length.
43+
for (size_t i = 0; i < node::MAX_BLOCKFILE_SIZE / ss.size(); ++i) {
44+
if (fwrite(ss.data(), 1, ss.size(), file) != ss.size()) {
45+
throw std::runtime_error("write to test file failed\n");
46+
}
47+
}
48+
fclose(file);
49+
}
50+
51+
Chainstate& chainstate{testing_setup->m_node.chainman->ActiveChainstate()};
52+
std::multimap<uint256, FlatFilePos> blocks_with_unknown_parent;
53+
FlatFilePos pos;
54+
bench.run([&] {
55+
// "rb" is "binary, O_RDONLY", positioned to the start of the file.
56+
// The file will be closed by LoadExternalBlockFile().
57+
FILE* file{fsbridge::fopen(blkfile, "rb")};
58+
chainstate.LoadExternalBlockFile(file, &pos, &blocks_with_unknown_parent);
59+
});
60+
fs::remove(blkfile);
61+
}
62+
63+
BENCHMARK(LoadExternalBlockFile, benchmark::PriorityLevel::HIGH);

src/streams.h

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,6 @@ class CBufferedFile
612612
uint64_t nRewind; //!< how many bytes we guarantee to rewind
613613
std::vector<std::byte> vchBuf; //!< the buffer
614614

615-
protected:
616615
//! read data from the source to fill the buffer
617616
bool Fill() {
618617
unsigned int pos = nSrcPos % vchBuf.size();
@@ -630,6 +629,28 @@ class CBufferedFile
630629
return true;
631630
}
632631

632+
//! Advance the stream's read pointer (m_read_pos) by up to 'length' bytes,
633+
//! filling the buffer from the file so that at least one byte is available.
634+
//! Return a pointer to the available buffer data and the number of bytes
635+
//! (which may be less than the requested length) that may be accessed
636+
//! beginning at that pointer.
637+
std::pair<std::byte*, size_t> AdvanceStream(size_t length)
638+
{
639+
assert(m_read_pos <= nSrcPos);
640+
if (m_read_pos + length > nReadLimit) {
641+
throw std::ios_base::failure("Attempt to position past buffer limit");
642+
}
643+
// If there are no bytes available, read from the file.
644+
if (m_read_pos == nSrcPos && length > 0) Fill();
645+
646+
size_t buffer_offset{static_cast<size_t>(m_read_pos % vchBuf.size())};
647+
size_t buffer_available{static_cast<size_t>(vchBuf.size() - buffer_offset)};
648+
size_t bytes_until_source_pos{static_cast<size_t>(nSrcPos - m_read_pos)};
649+
size_t advance{std::min({length, buffer_available, bytes_until_source_pos})};
650+
m_read_pos += advance;
651+
return std::make_pair(&vchBuf[buffer_offset], advance);
652+
}
653+
633654
public:
634655
CBufferedFile(FILE* fileIn, uint64_t nBufSize, uint64_t nRewindIn, int nTypeIn, int nVersionIn)
635656
: nType(nTypeIn), nVersion(nVersionIn), nSrcPos(0), m_read_pos(0), nReadLimit(std::numeric_limits<uint64_t>::max()), nRewind(nRewindIn), vchBuf(nBufSize, std::byte{0})
@@ -667,24 +688,21 @@ class CBufferedFile
667688
//! read a number of bytes
668689
void read(Span<std::byte> dst)
669690
{
670-
if (dst.size() + m_read_pos > nReadLimit) {
671-
throw std::ios_base::failure("Read attempted past buffer limit");
672-
}
673691
while (dst.size() > 0) {
674-
if (m_read_pos == nSrcPos)
675-
Fill();
676-
unsigned int pos = m_read_pos % vchBuf.size();
677-
size_t nNow = dst.size();
678-
if (nNow + pos > vchBuf.size())
679-
nNow = vchBuf.size() - pos;
680-
if (nNow + m_read_pos > nSrcPos)
681-
nNow = nSrcPos - m_read_pos;
682-
memcpy(dst.data(), &vchBuf[pos], nNow);
683-
m_read_pos += nNow;
684-
dst = dst.subspan(nNow);
692+
auto [buffer_pointer, length]{AdvanceStream(dst.size())};
693+
memcpy(dst.data(), buffer_pointer, length);
694+
dst = dst.subspan(length);
685695
}
686696
}
687697

698+
//! Move the read position ahead in the stream to the given position.
699+
//! Use SetPos() to back up in the stream, not SkipTo().
700+
void SkipTo(const uint64_t file_pos)
701+
{
702+
assert(file_pos >= m_read_pos);
703+
while (m_read_pos < file_pos) AdvanceStream(file_pos - m_read_pos);
704+
}
705+
688706
//! return the current reading position
689707
uint64_t GetPos() const {
690708
return m_read_pos;

src/test/streams_tests.cpp

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file)
253253
BOOST_CHECK(false);
254254
} catch (const std::exception& e) {
255255
BOOST_CHECK(strstr(e.what(),
256-
"Read attempted past buffer limit") != nullptr);
256+
"Attempt to position past buffer limit") != nullptr);
257257
}
258258
// The default argument removes the limit completely.
259259
BOOST_CHECK(bf.SetLimit());
@@ -322,14 +322,63 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file)
322322
BOOST_CHECK(!bf.SetPos(0));
323323
// But we should now be positioned at least as far back as allowed
324324
// by the rewind window (relative to our farthest read position, 40).
325-
BOOST_CHECK(bf.GetPos() <= 30);
325+
BOOST_CHECK(bf.GetPos() <= 30U);
326326

327327
// We can explicitly close the file, or the destructor will do it.
328328
bf.fclose();
329329

330330
fs::remove(streams_test_filename);
331331
}
332332

333+
BOOST_AUTO_TEST_CASE(streams_buffered_file_skip)
334+
{
335+
fs::path streams_test_filename = m_args.GetDataDirBase() / "streams_test_tmp";
336+
FILE* file = fsbridge::fopen(streams_test_filename, "w+b");
337+
// The value at each offset is the byte offset (e.g. byte 1 in the file has the value 0x01).
338+
for (uint8_t j = 0; j < 40; ++j) {
339+
fwrite(&j, 1, 1, file);
340+
}
341+
rewind(file);
342+
343+
// The buffer is 25 bytes, allow rewinding 10 bytes.
344+
CBufferedFile bf(file, 25, 10, 222, 333);
345+
346+
uint8_t i;
347+
// This is like bf >> (7-byte-variable), in that it will cause data
348+
// to be read from the file into memory, but it's not copied to us.
349+
bf.SkipTo(7);
350+
BOOST_CHECK_EQUAL(bf.GetPos(), 7U);
351+
bf >> i;
352+
BOOST_CHECK_EQUAL(i, 7);
353+
354+
// The bytes in the buffer up to offset 7 are valid and can be read.
355+
BOOST_CHECK(bf.SetPos(0));
356+
bf >> i;
357+
BOOST_CHECK_EQUAL(i, 0);
358+
bf >> i;
359+
BOOST_CHECK_EQUAL(i, 1);
360+
361+
bf.SkipTo(11);
362+
bf >> i;
363+
BOOST_CHECK_EQUAL(i, 11);
364+
365+
// SkipTo() honors the transfer limit; we can't position beyond the limit.
366+
bf.SetLimit(13);
367+
try {
368+
bf.SkipTo(14);
369+
BOOST_CHECK(false);
370+
} catch (const std::exception& e) {
371+
BOOST_CHECK(strstr(e.what(), "Attempt to position past buffer limit") != nullptr);
372+
}
373+
374+
// We can position exactly to the transfer limit.
375+
bf.SkipTo(13);
376+
BOOST_CHECK_EQUAL(bf.GetPos(), 13U);
377+
378+
bf.fclose();
379+
fs::remove(streams_test_filename);
380+
}
381+
333382
BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
334383
{
335384
// Make this test deterministic.
@@ -361,7 +410,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
361410
// sizes; the boundaries of the objects can interact arbitrarily
362411
// with the CBufferFile's internal buffer. These first three
363412
// cases simulate objects of various sizes (1, 2, 5 bytes).
364-
switch (InsecureRandRange(5)) {
413+
switch (InsecureRandRange(6)) {
365414
case 0: {
366415
uint8_t a[1];
367416
if (currentPos + 1 > fileSize)
@@ -399,6 +448,16 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
399448
break;
400449
}
401450
case 3: {
451+
// SkipTo is similar to the "read" cases above, except
452+
// we don't receive the data.
453+
size_t skip_length{static_cast<size_t>(InsecureRandRange(5))};
454+
if (currentPos + skip_length > fileSize) continue;
455+
bf.SetLimit(currentPos + skip_length);
456+
bf.SkipTo(currentPos + skip_length);
457+
currentPos += skip_length;
458+
break;
459+
}
460+
case 4: {
402461
// Find a byte value (that is at or ahead of the current position).
403462
size_t find = currentPos + InsecureRandRange(8);
404463
if (find >= fileSize)
@@ -415,7 +474,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
415474
currentPos++;
416475
break;
417476
}
418-
case 4: {
477+
case 5: {
419478
size_t requestPos = InsecureRandRange(maxPos + 4);
420479
bool okay = bf.SetPos(requestPos);
421480
// The new position may differ from the requested position

src/validation.cpp

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4389,6 +4389,8 @@ void Chainstate::LoadExternalBlockFile(
43894389
try {
43904390
// This takes over fileIn and calls fclose() on it in the CBufferedFile destructor
43914391
CBufferedFile blkdat(fileIn, 2*MAX_BLOCK_SERIALIZED_SIZE, MAX_BLOCK_SERIALIZED_SIZE+8, SER_DISK, CLIENT_VERSION);
4392+
// nRewind indicates where to resume scanning in case something goes wrong,
4393+
// such as a block fails to deserialize.
43924394
uint64_t nRewind = blkdat.GetPos();
43934395
while (!blkdat.eof()) {
43944396
if (ShutdownRequested()) return;
@@ -4412,42 +4414,50 @@ void Chainstate::LoadExternalBlockFile(
44124414
continue;
44134415
} catch (const std::exception&) {
44144416
// no valid block header found; don't complain
4417+
// (this happens at the end of every blk.dat file)
44154418
break;
44164419
}
44174420
try {
4418-
// read block
4419-
uint64_t nBlockPos = blkdat.GetPos();
4421+
// read block header
4422+
const uint64_t nBlockPos{blkdat.GetPos()};
44204423
if (dbp)
44214424
dbp->nPos = nBlockPos;
44224425
blkdat.SetLimit(nBlockPos + nSize);
4423-
std::shared_ptr<CBlock> pblock = std::make_shared<CBlock>();
4424-
CBlock& block = *pblock;
4425-
blkdat >> block;
4426-
nRewind = blkdat.GetPos();
4427-
4428-
uint256 hash = block.GetHash();
4426+
CBlockHeader header;
4427+
blkdat >> header;
4428+
const uint256 hash{header.GetHash()};
4429+
// Skip the rest of this block (this may read from disk into memory); position to the marker before the
4430+
// next block, but it's still possible to rewind to the start of the current block (without a disk read).
4431+
nRewind = nBlockPos + nSize;
4432+
blkdat.SkipTo(nRewind);
44294433
{
44304434
LOCK(cs_main);
44314435
// detect out of order blocks, and store them for later
4432-
if (hash != params.GetConsensus().hashGenesisBlock && !m_blockman.LookupBlockIndex(block.hashPrevBlock)) {
4436+
if (hash != params.GetConsensus().hashGenesisBlock && !m_blockman.LookupBlockIndex(header.hashPrevBlock)) {
44334437
LogPrint(BCLog::REINDEX, "%s: Out of order block %s, parent %s not known\n", __func__, hash.ToString(),
4434-
block.hashPrevBlock.ToString());
4438+
header.hashPrevBlock.ToString());
44354439
if (dbp && blocks_with_unknown_parent) {
4436-
blocks_with_unknown_parent->emplace(block.hashPrevBlock, *dbp);
4440+
blocks_with_unknown_parent->emplace(header.hashPrevBlock, *dbp);
44374441
}
44384442
continue;
44394443
}
44404444

44414445
// process in case the block isn't known yet
44424446
const CBlockIndex* pindex = m_blockman.LookupBlockIndex(hash);
44434447
if (!pindex || (pindex->nStatus & BLOCK_HAVE_DATA) == 0) {
4444-
BlockValidationState state;
4445-
if (AcceptBlock(pblock, state, nullptr, true, dbp, nullptr, true)) {
4446-
nLoaded++;
4447-
}
4448-
if (state.IsError()) {
4449-
break;
4450-
}
4448+
// This block can be processed immediately; rewind to its start, read and deserialize it.
4449+
blkdat.SetPos(nBlockPos);
4450+
std::shared_ptr<CBlock> pblock{std::make_shared<CBlock>()};
4451+
blkdat >> *pblock;
4452+
nRewind = blkdat.GetPos();
4453+
4454+
BlockValidationState state;
4455+
if (AcceptBlock(pblock, state, nullptr, true, dbp, nullptr, true)) {
4456+
nLoaded++;
4457+
}
4458+
if (state.IsError()) {
4459+
break;
4460+
}
44514461
} else if (hash != params.GetConsensus().hashGenesisBlock && pindex->nHeight % 1000 == 0) {
44524462
LogPrint(BCLog::REINDEX, "Block Import: already had block %s at height %d\n", hash.ToString(), pindex->nHeight);
44534463
}

0 commit comments

Comments
 (0)