|
| 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