Files
UnrealEngine/Engine/Source/Programs/AutoRTFMTests/Private/WriteLogTests.cpp
2025-05-18 13:04:45 +08:00

456 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "WriteLog.h"
#include "Catch2Includes.h"
#include <algorithm>
#include <cstddef>
#include <numeric>
#include <vector>
TEST_CASE("FWriteLog")
{
std::mt19937 Rand(0x1234);
AutoRTFM::FWriteLog WriteLog;
// Returns a vector with the given size filled with random bytes
auto RandomBuffer = [&Rand](size_t Size) -> std::vector<std::byte>
{
std::vector<std::byte> Out(Size);
for (size_t I = 0; I < Size; I++)
{
Out[I] = static_cast<std::byte>(Rand() & 0xff);
}
return Out;
};
// Returns a std::vector<size_t> with NumEntries elements all equal to
// EntrySize
auto FixedSize = [](size_t EntrySize, size_t NumEntries)
{
return std::vector<size_t>(NumEntries, EntrySize);
};
// Returns a std::vector<size_t> with NumEntries elements with random values
// between 1..AutoRTFM::FWriteLogEntry::MaxSize
auto RandomSize = [&Rand](size_t NumEntries)
{
std::vector<size_t> EntrySizes(NumEntries);
for (size_t& EntrySize : EntrySizes)
{
EntrySize = std::max<size_t>(1, Rand() & AutoRTFM::FWriteLogEntry::MaxSize);
}
return EntrySizes;
};
// The order in which to add data to the write log.
enum class EEntryOrder
{
Forwards, // Sequentially forward
Backwards, // Sequentially backwards
Random, // Random order
};
auto CheckWithOrder = [&](std::vector<size_t> EntrySizes, EEntryOrder EntryOrder)
{
// Total number of write entries to test
const size_t NumEntries = EntrySizes.size();
// Total number of bytes to add to the write log
const size_t TotalBufferSize = std::reduce(EntrySizes.begin(), EntrySizes.end());
// Buffer of random data
std::vector<std::byte> Data = RandomBuffer(TotalBufferSize);
// An interval in Data
struct FDataSpan
{
size_t Offset;
size_t Size;
};
// Produce a list of data spans on Data. These will be added to the write log in order.
std::vector<FDataSpan> DataSpans(NumEntries);
{
size_t Offset = 0;
for (size_t I = 0; I < NumEntries; I++)
{
FDataSpan& DataSpan = DataSpans[I];
DataSpan.Offset = Offset;
DataSpan.Size = EntrySizes[I];
Offset += EntrySizes[I];
}
// Shuffle the spans
switch (EntryOrder)
{
case EEntryOrder::Forwards:
break; // Already forwards
case EEntryOrder::Backwards:
std::reverse(std::begin(DataSpans), std::end(DataSpans));
break;
case EEntryOrder::Random:
std::shuffle(std::begin(DataSpans), std::end(DataSpans), Rand);
break;
}
}
// Populate the write log and Entries with the expected write log entries
std::vector<AutoRTFM::FWriteLogEntry> Entries;
for (FDataSpan& DataSpan : DataSpans)
{
AutoRTFM::FWriteLogEntry Entry;
Entry.Data = &Data[DataSpan.Offset];
Entry.Size = DataSpan.Size;
Entry.LogicalAddress = reinterpret_cast<std::byte*>(static_cast<uintptr_t>(0x1234000 + DataSpan.Offset));
Entry.bNoMemoryValidation = (static_cast<int>(Entry.Data[0]) & 0x10) != 0;
Entries.push_back(Entry);
WriteLog.Push(Entry);
}
REQUIRE(!WriteLog.IsEmpty());
REQUIRE(WriteLog.Num() <= NumEntries); // WriteLog may append back-to-back entries.
REQUIRE(WriteLog.TotalSize() == Data.size());
SECTION("Forwards iterator")
{
size_t EntryIndex = 0;
for (AutoRTFM::FWriteLogEntry Got : WriteLog)
{
size_t Offset = 0;
while (Offset < Got.Size)
{
const AutoRTFM::FWriteLogEntry& Expect = Entries[EntryIndex];
REQUIRE(Got.LogicalAddress + Offset == Expect.LogicalAddress);
REQUIRE(Got.bNoMemoryValidation == Expect.bNoMemoryValidation);
REQUIRE(Expect.Size + Offset <= Got.Size);
REQUIRE(memcmp(Got.Data + Offset, Expect.Data, Expect.Size) == 0);
EntryIndex++;
Offset += Expect.Size;
}
REQUIRE(Offset == Got.Size);
}
REQUIRE(EntryIndex == Entries.size());
}
SECTION("Reverse iterator")
{
size_t EntryIndex = Entries.size() - 1;
for (AutoRTFM::FWriteLog::ReverseIterator It = WriteLog.rbegin(); It != WriteLog.rend(); ++It)
{
const AutoRTFM::FWriteLogEntry Got = *It;
size_t Offset = Got.Size;
while (Offset > 0)
{
const AutoRTFM::FWriteLogEntry& Expect = Entries[EntryIndex];
Offset -= Expect.Size;
REQUIRE(Offset < Got.Size);
REQUIRE(Got.LogicalAddress + Offset == Expect.LogicalAddress);
REQUIRE(Got.bNoMemoryValidation == Expect.bNoMemoryValidation);
REQUIRE(Expect.Size + Offset <= Got.Size);
REQUIRE(memcmp(Got.Data + Offset, Expect.Data, Expect.Size) == 0);
EntryIndex--;
}
}
REQUIRE(EntryIndex + 1 == 0);
}
SECTION("Reset")
{
WriteLog.Reset();
REQUIRE(WriteLog.IsEmpty());
REQUIRE(WriteLog.Num() == 0u);
REQUIRE(WriteLog.TotalSize() == 0u);
}
};
auto Check = [&](std::vector<size_t> EntrySizes)
{
SECTION("Forwards")
{
CheckWithOrder(std::move(EntrySizes), EEntryOrder::Forwards);
}
SECTION("Backwards")
{
CheckWithOrder(std::move(EntrySizes), EEntryOrder::Backwards);
}
SECTION("Random")
{
CheckWithOrder(std::move(EntrySizes), EEntryOrder::Random);
}
};
SECTION("Empty")
{
REQUIRE(WriteLog.IsEmpty());
REQUIRE(WriteLog.Num() == 0u);
REQUIRE(WriteLog.TotalSize() == 0u);
}
SECTION("EntrySize: 1, NumEntries: 32")
{
Check(FixedSize(/* EntrySize */ 1, /* NumEntries */ 32));
}
SECTION("EntrySize: 32, NumEntries: 65536")
{
Check(FixedSize(/* EntrySize */ 32, /* NumEntries */ 65536));
}
SECTION("EntrySize: 1024, NumEntries: 32")
{
Check(FixedSize(/* EntrySize */ 1024, /* NumEntries */ 32));
}
SECTION("EntrySize: FWriteLogEntry::MaxSize-1, NumEntries: 32")
{
Check(FixedSize(/* EntrySize */ AutoRTFM::FWriteLogEntry::MaxSize-1, /* NumEntries */ 32));
}
SECTION("EntrySize: FWriteLogEntry::MaxSize, NumEntries: 32")
{
Check(FixedSize(/* EntrySize */ AutoRTFM::FWriteLogEntry::MaxSize, /* NumEntries */ 32));
}
SECTION("EntrySize: random, NumEntries: 32")
{
Check(RandomSize(/* NumEntries */ 32));
}
}
TEST_CASE("FWriteLog.Hash")
{
struct FMemorySpan
{
std::byte* Data;
size_t Size;
};
// Populates Buffer with some pseudo-random data.
auto FillBuffer = [](FMemorySpan Buffer)
{
uint32_t Value = 976187;
for (size_t Offset = 0; Offset < Buffer.Size; Offset++)
{
Value = ((Value >> 3) * 921563) ^ ((Value << 1) * 743917);
Buffer.Data[Offset] = std::byte(Value & 255);
}
};
// Returns a FWriteLog populated with write log records of size WriteSize,
// and with a mis-alignment of AlignmentOffset. Records will have at least
// one byte between them.
// Writes is populated with all the memory spans for the write records.
// Gaps is populated with all the memory spans between the write records.
auto CreateWriteLog = [](
FMemorySpan Buffer,
size_t WriteSize,
size_t AlignmentOffset,
std::vector<FMemorySpan>& Writes,
std::vector<FMemorySpan>& Gaps)
{
AutoRTFM::FWriteLog WriteLog;
size_t Offset = AlignmentOffset;
while (Offset + WriteSize < Buffer.Size)
{
AutoRTFM::FWriteLogEntry Entry{};
Entry.LogicalAddress = Buffer.Data + Offset;
Entry.Data = Entry.LogicalAddress;
Entry.Size = WriteSize;
WriteLog.Push(Entry);
Writes.push_back(FMemorySpan{Buffer.Data + Offset, WriteSize});
Offset += WriteSize;
const size_t GapStart = Offset;
if (AlignmentOffset == 0)
{
// Round up offset to a multiple of WriteSize, but ensuring
// there's a gap of at least one byte between write log records.
// This ensures that records are not folded together.
Offset = AutoRTFM::RoundUp(Offset + 1, WriteSize);
}
else
{
// Round up offset to a multiple of WriteSize, then add the
// alignment offset to make the offset misaligned.
Offset = AutoRTFM::RoundUp(Offset, WriteSize) + AlignmentOffset;
}
const size_t GapEnd = std::min(Offset, Buffer.Size);
Gaps.push_back(FMemorySpan{Buffer.Data + GapStart, GapEnd - GapStart});
}
return WriteLog;
};
// Tests
{
constexpr size_t BufferSize = 1 << 13; // 8KiB
auto BufferData = std::unique_ptr<std::byte[]>(new std::byte[BufferSize]);
REQUIRE((reinterpret_cast<uintptr_t>(BufferData.get()) & 15) == 0);
FMemorySpan Buffer{BufferData.get(), BufferSize};
FillBuffer(Buffer);
auto Check = [&](size_t WriteSize, size_t AlignmentOffset)
{
std::vector<FMemorySpan> Writes, Gaps;
AutoRTFM::FWriteLog WriteLog = CreateWriteLog(Buffer, WriteSize, AlignmentOffset, Writes, Gaps);
const AutoRTFM::FWriteLog::FHash OriginalHash = WriteLog.Hash(WriteLog.Num());
constexpr size_t NumChecks = 100;
size_t NumChanged = 0;
for (size_t I = 0; I < NumChecks; I++)
{
// Pick a random write.
FMemorySpan& Write = Writes[(I * 16831) % Writes.size()];
// Pick a random byte within the write, change it and check the hash changes.
size_t Offset = (I * 838483) % Write.Size;
const std::byte OriginalByte = Write.Data[Offset];
Write.Data[Offset] = ~Write.Data[Offset];
const AutoRTFM::FWriteLog::FHash NewHash = WriteLog.Hash(WriteLog.Num());
NumChanged += (OriginalHash != NewHash) ? 1 : 0;
Write.Data[Offset] = OriginalByte;
}
// Expect at least 95% of the byte changes to have affected the hash.
REQUIRE(NumChanged > (NumChecks * 95 / 100));
REQUIRE(OriginalHash == WriteLog.Hash(WriteLog.Num()));
// Modify all the padding bytes between write records. These should
// not affect the hash.
for (FMemorySpan& Gap : Gaps)
{
for (size_t I = 0; I < Gap.Size; I++)
{
Gap.Data[I] = ~Gap.Data[I];
}
}
REQUIRE(OriginalHash == WriteLog.Hash(WriteLog.Num()));
};
Check(/* WriteSize */ 1, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 2, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 2, /* AlignmentOffset */ 1);
Check(/* WriteSize */ 3, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 3, /* AlignmentOffset */ 1);
Check(/* WriteSize */ 3, /* AlignmentOffset */ 2);
Check(/* WriteSize */ 4, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 4, /* AlignmentOffset */ 1);
Check(/* WriteSize */ 4, /* AlignmentOffset */ 2);
Check(/* WriteSize */ 4, /* AlignmentOffset */ 3);
Check(/* WriteSize */ 5, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 5, /* AlignmentOffset */ 1);
Check(/* WriteSize */ 5, /* AlignmentOffset */ 3);
Check(/* WriteSize */ 5, /* AlignmentOffset */ 4);
Check(/* WriteSize */ 6, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 6, /* AlignmentOffset */ 2);
Check(/* WriteSize */ 6, /* AlignmentOffset */ 3);
Check(/* WriteSize */ 6, /* AlignmentOffset */ 5);
Check(/* WriteSize */ 7, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 7, /* AlignmentOffset */ 1);
Check(/* WriteSize */ 7, /* AlignmentOffset */ 4);
Check(/* WriteSize */ 7, /* AlignmentOffset */ 6);
Check(/* WriteSize */ 8, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 8, /* AlignmentOffset */ 2);
Check(/* WriteSize */ 8, /* AlignmentOffset */ 3);
Check(/* WriteSize */ 8, /* AlignmentOffset */ 7);
Check(/* WriteSize */ 9, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 9, /* AlignmentOffset */ 1);
Check(/* WriteSize */ 9, /* AlignmentOffset */ 5);
Check(/* WriteSize */ 9, /* AlignmentOffset */ 8);
Check(/* WriteSize */ 10, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 10, /* AlignmentOffset */ 3);
Check(/* WriteSize */ 10, /* AlignmentOffset */ 5);
Check(/* WriteSize */ 10, /* AlignmentOffset */ 9);
Check(/* WriteSize */ 15, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 15, /* AlignmentOffset */ 6);
Check(/* WriteSize */ 15, /* AlignmentOffset */ 9);
Check(/* WriteSize */ 15, /* AlignmentOffset */ 14);
Check(/* WriteSize */ 16, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 16, /* AlignmentOffset */ 8);
Check(/* WriteSize */ 16, /* AlignmentOffset */ 9);
Check(/* WriteSize */ 16, /* AlignmentOffset */ 15);
Check(/* WriteSize */ 32, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 32, /* AlignmentOffset */ 8);
Check(/* WriteSize */ 32, /* AlignmentOffset */ 16);
Check(/* WriteSize */ 32, /* AlignmentOffset */ 31);
Check(/* WriteSize */ 64, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 64, /* AlignmentOffset */ 7);
Check(/* WriteSize */ 64, /* AlignmentOffset */ 30);
Check(/* WriteSize */ 64, /* AlignmentOffset */ 63);
Check(/* WriteSize */ 100, /* AlignmentOffset */ 0);
Check(/* WriteSize */ 100, /* AlignmentOffset */ 1);
Check(/* WriteSize */ 100, /* AlignmentOffset */ 10);
Check(/* WriteSize */ 100, /* AlignmentOffset */ 99);
}
{
constexpr size_t BufferSize = 1 << 20; // 1MiB
auto BufferData = std::unique_ptr<std::byte[]>(new std::byte[BufferSize]);
REQUIRE((reinterpret_cast<uintptr_t>(BufferData.get()) & 15) == 0);
FMemorySpan Buffer{BufferData.get(), BufferSize};
FillBuffer(Buffer);
auto Benchmark = [&](Catch::Benchmark::Chronometer Meter, size_t WriteSize, size_t AlignmentOffset)
{
std::vector<FMemorySpan> Writes, Gaps;
AutoRTFM::FWriteLog WriteLog = CreateWriteLog(Buffer, WriteSize, AlignmentOffset, Writes, Gaps);
Meter.measure([&]
{
return WriteLog.Hash(WriteLog.Num());
});
};
SECTION("Aligned")
{
#define BENCH(WRITE_SIZE) \
BENCHMARK_ADVANCED("WriteSize: " #WRITE_SIZE)(Catch::Benchmark::Chronometer Meter) \
{ \
Benchmark(Meter, /* WriteSize */ WRITE_SIZE, /* AlignmentOffset */ 0); \
};
BENCH(1);
BENCH(2);
BENCH(3);
BENCH(4);
BENCH(5);
BENCH(6);
BENCH(7);
BENCH(8);
BENCH(9);
BENCH(10);
BENCH(15);
BENCH(16);
BENCH(32);
BENCH(64);
BENCH(128);
#undef BENCH
}
SECTION("Unaligned")
{
#define BENCH(WRITE_SIZE) \
BENCHMARK_ADVANCED("WriteSize: " #WRITE_SIZE)(Catch::Benchmark::Chronometer Meter) \
{ \
Benchmark(Meter, /* WriteSize */ WRITE_SIZE, /* AlignmentOffset */ 1); \
};
BENCH(1);
BENCH(2);
BENCH(3);
BENCH(4);
BENCH(5);
BENCH(6);
BENCH(7);
BENCH(8);
BENCH(9);
BENCH(10);
BENCH(15);
BENCH(16);
BENCH(32);
BENCH(64);
BENCH(128);
#undef BENCH
}
}
}