Skip to content

Commit b1a8648

Browse files
bench: add missing crash-recovery benchmark source (#41)
* feat: First phase of hardening and recovering vtx file in case of a crash. Implement a durable_file class that syncs and flushes to hard disk, file_sink uses now the durable_file stead of a ffstream. Added checksum to each chunk to validate chunks separetly * feat : vtx crash recovery, create a .recovery file with only chunks, repairs the replay from the .recovery, it includes a test forcing a crash * feat : recovery fixes * feat : recovery replay completed * test: crash test recovery * test: crash test recovery * Feat/writer crash recovery (#39) (#40) * feat: First phase of hardening and recovering vtx file in case of a crash. Implement a durable_file class that syncs and flushes to hard disk, file_sink uses now the durable_file stead of a ffstream. Added checksum to each chunk to validate chunks separetly * feat : vtx crash recovery, create a .recovery file with only chunks, repairs the replay from the .recovery, it includes a test forcing a crash * feat : recovery fixes * feat : recovery replay completed * test: crash test recovery * test: crash test recovery
1 parent b0bfb75 commit b1a8648

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// VTX SDK -- crash-recovery durability-tier benchmarks.
2+
//
3+
// Scenarios (same recording, three sink configurations)
4+
// BM_WriterDurabilityTier/journal_off no .recovery sidecar (pre-feature baseline)
5+
// BM_WriterDurabilityTier/flush_only journal on, fflush per operation
6+
// (process-crash safe)
7+
// BM_WriterDurabilityTier/fsync journal on, fsync per operation -- the
8+
// DEFAULT (power-loss safe)
9+
//
10+
// Quantifies what the crash-recovery defaults cost on the writer hot path: with
11+
// the journal enabled every recorded frame is serialized twice (once into its F
12+
// record, once into its chunk) and, in fsync mode, hits physical media per frame.
13+
// items_per_second is frames/sec end-to-end (writer create + record + Stop).
14+
15+
#include "vtx_schema_generated.h" // complete fbsvtx types before the policy header
16+
#include "vtx_schema.pb.h"
17+
18+
#include "vtx/common/vtx_logger.h"
19+
#include "vtx/common/vtx_types.h"
20+
#include "vtx/reader/core/vtx_reader_facade.h"
21+
#include "vtx/writer/core/vtx_replay_recovery.h"
22+
#include "vtx/writer/core/vtx_writer_facade.h" // brings SchemaRegistry/SchemaSanitizer for writer.h
23+
#include "vtx/writer/core/writer.h"
24+
#include "vtx/writer/policies/formatters/flatbuffers_vtx_policy.h"
25+
#include "vtx/writer/policies/sinks/file_sink.h"
26+
27+
#include <benchmark/benchmark.h>
28+
29+
#include <filesystem>
30+
#include <string>
31+
32+
namespace {
33+
34+
constexpr int kFramesPerIteration = 200;
35+
36+
std::string TempOutputPath() {
37+
return (std::filesystem::temp_directory_path() / "vtx_bench_recovery_out.vtx").string();
38+
}
39+
40+
std::string ArenaSchemaPath() {
41+
return (std::filesystem::path(VTX_BENCH_FIXTURES_DIR).parent_path().parent_path() / "samples" / "content" /
42+
"writer" / "arena" / "arena_schema.json")
43+
.string();
44+
}
45+
46+
struct SilenceDebugLogsOnce {
47+
SilenceDebugLogsOnce() { VTX::Logger::Instance().SetDebugEnabled(false); }
48+
};
49+
const SilenceDebugLogsOnce silence_recovery_bench_logs_once {};
50+
51+
} // namespace
52+
53+
// arg0: enable_recovery_journal, arg1: durable_writes
54+
static void BM_WriterDurabilityTier(benchmark::State& state) {
55+
using RawWriter = VTX::ReplayWriter<VTX::ChunkedFileSink<VTX::FlatBuffersVtxPolicy>>;
56+
const std::string schema_path = ArenaSchemaPath();
57+
const std::string out_path = TempOutputPath();
58+
const bool journal = state.range(0) != 0;
59+
const bool durable = state.range(1) != 0;
60+
61+
for (auto _ : state) {
62+
RawWriter::Config config;
63+
config.sink_config.filename = out_path;
64+
config.sink_config.header_config.replay_name = "BenchRecovery";
65+
config.sink_config.enable_recovery_journal = journal;
66+
config.sink_config.durable_writes = durable;
67+
config.schema_json_path = schema_path;
68+
config.default_fps = 60.0f;
69+
config.chunker_config.max_frames = 100;
70+
71+
RawWriter writer(config);
72+
for (int i = 0; i < kFramesPerIteration; ++i) {
73+
VTX::Frame frame;
74+
auto& bucket = frame.CreateBucket("entity");
75+
VTX::PropertyContainer entity;
76+
entity.entity_type_id = 0;
77+
entity.float_properties.push_back(static_cast<float>(i) * 1.5f);
78+
bucket.unique_ids.push_back("player_" + std::to_string(i % 10));
79+
bucket.entities.push_back(std::move(entity));
80+
81+
VTX::GameTime::GameTimeRegister game_time;
82+
game_time.game_time = static_cast<float>(i) / 60.0f;
83+
writer.RecordFrame(frame, game_time);
84+
}
85+
writer.Stop();
86+
}
87+
88+
state.SetItemsProcessed(state.iterations() * kFramesPerIteration);
89+
std::filesystem::remove(out_path);
90+
std::filesystem::remove(out_path + ".recovery");
91+
}
92+
BENCHMARK(BM_WriterDurabilityTier)
93+
->Unit(benchmark::kMillisecond)
94+
->Args({0, 1})
95+
->ArgNames({"journal", "fsync"})
96+
->Args({1, 0})
97+
->Args({1, 1});
98+
99+
// ---------------------------------------------------------------------------
100+
// Read cost of a recovered file's tail: pending frames are re-appended by
101+
// RepairReplayFile as ONE-FRAME chunks, so a recovery with a large in-flight
102+
// batch yields many tiny chunks. This pair quantifies the sequential-read
103+
// penalty versus an identically-sized cleanly-written file (100-frame chunks).
104+
// For hot-path use, transcode a salvaged file by re-recording it (see docs).
105+
// ---------------------------------------------------------------------------
106+
107+
namespace {
108+
109+
constexpr int kReadFrames = 300;
110+
111+
void RecordFramesInto(VTX::ReplayWriter<VTX::ChunkedFileSink<VTX::FlatBuffersVtxPolicy>>& writer, int frames) {
112+
for (int i = 0; i < frames; ++i) {
113+
VTX::Frame frame;
114+
auto& bucket = frame.CreateBucket("entity");
115+
VTX::PropertyContainer entity;
116+
entity.entity_type_id = 0;
117+
entity.float_properties.push_back(static_cast<float>(i) * 1.5f);
118+
bucket.unique_ids.push_back("player_" + std::to_string(i % 10));
119+
bucket.entities.push_back(std::move(entity));
120+
VTX::GameTime::GameTimeRegister game_time;
121+
game_time.game_time = static_cast<float>(i) / 60.0f;
122+
writer.RecordFrame(frame, game_time);
123+
}
124+
}
125+
126+
// arg: pending frames at crash time (0 = clean Stop, all 100-frame chunks).
127+
std::string MakeReadSubject(int pending) {
128+
using RawWriter = VTX::ReplayWriter<VTX::ChunkedFileSink<VTX::FlatBuffersVtxPolicy>>;
129+
const std::string path =
130+
(std::filesystem::temp_directory_path() / ("vtx_bench_recovery_read_" + std::to_string(pending) + ".vtx"))
131+
.string();
132+
RawWriter::Config config;
133+
config.sink_config.filename = path;
134+
config.sink_config.header_config.replay_name = "BenchRecoveryRead";
135+
config.schema_json_path = ArenaSchemaPath();
136+
config.default_fps = 60.0f;
137+
config.chunker_config.max_frames = 100;
138+
{
139+
RawWriter writer(config);
140+
RecordFramesInto(writer, kReadFrames - pending);
141+
writer.Flush(); // committed portion lands in 100-frame chunks
142+
RecordFramesInto(writer, pending);
143+
if (pending == 0)
144+
writer.Stop();
145+
// else: dropped without Stop -> `pending` in-flight frames
146+
}
147+
if (pending > 0) {
148+
const auto rr = VTX::RepairReplayFile(path);
149+
if (!rr.ok() || rr.recovered_frames != kReadFrames)
150+
return {};
151+
}
152+
return path;
153+
}
154+
155+
} // namespace
156+
157+
static void BM_ReaderRecoveredTail(benchmark::State& state) {
158+
const int pending = static_cast<int>(state.range(0));
159+
const std::string path = MakeReadSubject(pending);
160+
if (path.empty()) {
161+
state.SkipWithError("failed to prepare read subject");
162+
return;
163+
}
164+
165+
for (auto _ : state) {
166+
auto ctx = VTX::OpenReplayFile(path);
167+
if (!ctx) {
168+
state.SkipWithError("open failed");
169+
break;
170+
}
171+
ctx->WaitUntilReady();
172+
for (int i = 0; i < kReadFrames; ++i)
173+
benchmark::DoNotOptimize(ctx->GetFrameSync(i));
174+
}
175+
176+
state.SetItemsProcessed(state.iterations() * kReadFrames);
177+
std::filesystem::remove(path);
178+
}
179+
BENCHMARK(BM_ReaderRecoveredTail)
180+
->Unit(benchmark::kMillisecond)
181+
->Arg(0) // clean file: 3 chunks of 100
182+
->Arg(200) // recovered: 1 chunk of 100 + 200 one-frame chunks
183+
->ArgName("pending");

0 commit comments

Comments
 (0)