// Task 20: Performance benchmarks
//
// Measures key operations against target latencies:
// 1. SHM header write/read (target: < 0.001ms)
// 2. Object entry write (256) (target: < 0.1ms)
// 3. Transform hash (FNV-1a) (target: < 0.001ms per hash)
// 4. Blob write 10K verts (target: < 2ms)
// 5. Page table map 1K tiles (target: < 0.5ms)
// 6. Page table lookup 1K (target: < 0.1ms)
// 7. Tile cache alloc+evict (target: < 0.5ms for 1K ops)
// 8. Feedback parse 1K entries (target: < 0.5ms)
// 9. Bootstrap 100 objects (target: < 500ms)
// 10. Invalidation cascade (target: < 1ms)
// 11. Coordinate conversion 10K (target: < 0.1ms)
//
// Compile: g++ -std=c++17 -O2 -I../src test/test_benchmarks.cpp -o test/test_benchmarks -lrt && ./test/test_benchmarks
#include "../src/page_table.h"
#include "../src/shm_protocol.h"
#include "../src/tile_cache.h"
#include "../src/tile_request_queue.h"
#include <cassert>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
using Clock = std::chrono::high_resolution_clock;
static double elapsed_ms(Clock::time_point start, Clock::time_point end) {
return std::chrono::duration<double, std::milli>(end - start).count();
}
static void report(const char *name, double ms, double target_ms) {
const char *status = (ms <= target_ms) ? "OK" : "SLOW";
printf(" %-40s %8.3f ms (target < %.3f) [%s]\n", name, ms, target_ms, status);
}
// ── FNV-1a hash (matches blender_bridge.cpp) ─────────────────────────────────
static uint64_t fnv1a_hash(const float *data, int count) {
uint64_t hash = 14695981039346656037ULL;
const uint8_t *bytes = reinterpret_cast<const uint8_t *>(data);
for (int i = 0; i < count * 4; i++) {
hash ^= bytes[i];
hash *= 1099511628211ULL;
}
return hash;
}
// ── Coordinate conversion (matches blender_bridge.h) ─────────────────────────
static void godot_to_blender(const float *in, float *out) {
out[0] = in[0];
out[1] = -in[2];
out[2] = in[1];
}
static void blender_to_godot(const float *in, float *out) {
out[0] = in[0];
out[1] = in[2];
out[2] = -in[1];
}
// ── Benchmark 1: SHM header ops ─────────────────────────────────────────────
void bench_shm_header() {
ShmRegion shm;
assert(shm.create("/bench_shm", 1024 * 1024));
uint8_t *base = static_cast<uint8_t *>(shm.ptr());
const int ITERS = 100000;
auto start = Clock::now();
for (int i = 0; i < ITERS; i++) {
BridgeHeader h = {};
memcpy(h.magic, "BGOD", 4);
h.version = 1;
h.sequence = i;
h.object_count = 100;
h.frame_number = i;
write_header(base, h);
// Read back
BridgeHeader h2 = read_header(base);
volatile uint64_t seq = h2.sequence;
volatile uint32_t cnt = h2.object_count;
(void)seq;
(void)cnt;
}
auto end = Clock::now();
double ms = elapsed_ms(start, end) / ITERS;
report("SHM header write+read (per op)", ms, 0.001);
shm.destroy();
}
// ── Benchmark 2: Object entry write ──────────────────────────────────────────
void bench_object_entries() {
ShmRegion shm;
assert(shm.create("/bench_entries", 1024 * 1024));
uint8_t *base = static_cast<uint8_t *>(shm.ptr());
const int COUNT = 256;
auto start = Clock::now();
for (int iter = 0; iter < 1000; iter++) {
for (int i = 0; i < COUNT; i++) {
ObjectEntry e = {};
e.object_id = i + 1;
e.object_type = OBJ_MESH;
e.update_type = UPDATE_TRANSFORM;
e.data_offset = data_blob_offset(COUNT) + i * 64;
e.data_size = 64;
e.sequence = iter;
write_object_entry(base, i, e);
}
}
auto end = Clock::now();
double ms = elapsed_ms(start, end) / 1000.0;
report("256 object entries write (per batch)", ms, 0.1);
shm.destroy();
}
// ── Benchmark 3: Transform hash ──────────────────────────────────────────────
void bench_transform_hash() {
float xform[12]; // 3×4 matrix
for (int i = 0; i < 12; i++)
xform[i] = (float)i * 0.1f;
const int ITERS = 1000000;
volatile uint64_t sink = 0;
auto start = Clock::now();
for (int i = 0; i < ITERS; i++) {
xform[0] = (float)i * 0.001f; // Vary input
sink = fnv1a_hash(xform, 12);
}
auto end = Clock::now();
double ms = elapsed_ms(start, end) / ITERS;
report("FNV-1a transform hash (per hash)", ms, 0.001);
(void)sink;
}
// ── Benchmark 4: Blob write 10K vertices ─────────────────────────────────────
void bench_blob_write() {
ShmRegion shm;
assert(shm.create("/bench_blob", 4 * 1024 * 1024));
const int VERTS = 10000;
const int FLOATS = VERTS * 3; // positions only
float *data = new float[FLOATS];
for (int i = 0; i < FLOATS; i++)
data[i] = (float)i * 0.01f;
uint32_t offset = data_blob_offset(256);
uint8_t *base = static_cast<uint8_t *>(shm.ptr());
const int ITERS = 1000;
auto start = Clock::now();
for (int i = 0; i < ITERS; i++) {
memcpy(base + offset, data, FLOATS * sizeof(float));
}
auto end = Clock::now();
double ms = elapsed_ms(start, end) / ITERS;
report("Blob write 10K verts (per write)", ms, 2.0);
delete[] data;
shm.destroy();
}
// ── Benchmark 5: Page table map 1K tiles ─────────────────────────────────────
void bench_page_table_map() {
PageTable pt;
pt.init(8, 128); // 8 mips, 128×128 base = 16K tiles at mip 0
const int COUNT = 1024;
auto start = Clock::now();
for (int iter = 0; iter < 100; iter++) {
for (int i = 0; i < COUNT; i++) {
int x = i % 128;
int y = i / 128;
pt.map(0, x, y, { (uint8_t)(i & 0xFF), (uint8_t)((i >> 8) & 0xFF) });
}
}
auto end = Clock::now();
double ms = elapsed_ms(start, end) / 100.0;
report("Page table map 1K tiles (per batch)", ms, 0.5);
}
// ── Benchmark 6: Page table lookup 1K ────────────────────────────────────────
void bench_page_table_lookup() {
PageTable pt;
pt.init(8, 128);
// Pre-fill
for (int i = 0; i < 1024; i++) {
pt.map(0, i % 128, i / 128, { (uint8_t)(i & 0xFF), (uint8_t)((i >> 8) & 0xFF) });
}
const int COUNT = 1024;
volatile int valid_count = 0;
auto start = Clock::now();
for (int iter = 0; iter < 10000; iter++) {
int vc = 0;
for (int i = 0; i < COUNT; i++) {
auto r = pt.lookup(0, i % 128, i / 128);
if (r.valid)
vc++;
}
valid_count = vc;
}
auto end = Clock::now();
double ms = elapsed_ms(start, end) / 10000.0;
report("Page table lookup 1K (per batch)", ms, 0.1);
(void)valid_count;
}
// ── Benchmark 7: Tile cache alloc+evict ──────────────────────────────────────
void bench_tile_cache() {
TileCache cache;
cache.init(32, 32); // 1024 slots
// Pre-fill to force evictions
for (int i = 0; i < 1024; i++) {
TileId t = { 1, i % 32, i / 32, 0 };
cache.alloc(t, 1);
}
const int OPS = 1024;
auto start = Clock::now();
for (int iter = 0; iter < 100; iter++) {
for (int i = 0; i < OPS; i++) {
TileId t = { 2 + iter, i % 32, i / 32, 0 };
cache.alloc(t, 100 + iter);
}
}
auto end = Clock::now();
double ms = elapsed_ms(start, end) / 100.0;
report("Tile cache 1K alloc+evict (per batch)", ms, 0.5);
}
// ── Benchmark 8: Feedback parse ──────────────────────────────────────────────
void bench_feedback_parse() {
const int COUNT = 1024;
uint32_t feedback[COUNT];
for (int i = 0; i < COUNT; i++) {
feedback[i] = pack_feedback(1, i % 64, i / 64, i % 8);
}
TileRequestQueue queue;
auto start = Clock::now();
for (int iter = 0; iter < 1000; iter++) {
queue.clear();
queue.add_from_feedback(feedback, COUNT);
}
auto end = Clock::now();
double ms = elapsed_ms(start, end) / 1000.0;
report("Feedback parse 1K entries (per batch)", ms, 0.5);
}
// ── Benchmark 9: Bootstrap 100 objects ───────────────────────────────────────
void bench_bootstrap() {
ShmRegion shm;
assert(shm.create("/bench_boot", 16 * 1024 * 1024));
uint8_t *base = static_cast<uint8_t *>(shm.ptr());
const int OBJ_COUNT = 100;
// Simulate bootstrap: write header, 100 entries, and mesh blobs
auto start = Clock::now();
BridgeHeader h = {};
memcpy(h.magic, "BGOD", 4);
h.version = 1;
h.direction = 0;
h.sequence = 1;
h.object_count = OBJ_COUNT;
write_header(base, h);
uint32_t blob_offset = data_blob_offset(256);
for (int i = 0; i < OBJ_COUNT; i++) {
ObjectEntry e = {};
e.object_id = i + 1;
e.object_type = OBJ_MESH;
e.update_type = UPDATE_FULL;
e.data_offset = blob_offset;
// Simulate a mesh with 1000 verts (transform + vert count + positions + normals)
float xform[16] = {};
for (int j = 0; j < 16; j++)
xform[j] = (float)(i * 16 + j) * 0.1f;
memcpy(base + blob_offset, xform, 64);
blob_offset += 64;
uint32_t vert_count = 1000;
memcpy(base + blob_offset, &vert_count, 4);
blob_offset += 4;
// Positions (1000 × 3 floats)
for (int v = 0; v < 3000; v++) {
float val = (float)v * 0.01f;
memcpy(base + blob_offset, &val, 4);
blob_offset += 4;
}
// Normals (1000 × 3 floats)
for (int v = 0; v < 3000; v++) {
float val = (float)v * 0.001f;
memcpy(base + blob_offset, &val, 4);
blob_offset += 4;
}
e.data_size = blob_offset - e.data_offset;
write_object_entry(base, i, e);
}
auto end = Clock::now();
double ms = elapsed_ms(start, end);
report("Bootstrap 100 objects (1K verts each)", ms, 500.0);
shm.destroy();
}
// ── Benchmark 10: Invalidation cascade ───────────────────────────────────────
void bench_invalidation() {
PageTable pt;
pt.init(8, 128); // 8 mips, 128×128
// Fill all mip 0 tiles
for (int y = 0; y < 128; y++) {
for (int x = 0; x < 128; x++) {
pt.map(0, x, y, { (uint8_t)(x & 0xFF), (uint8_t)(y & 0xFF) });
}
}
// Fill mip 1 (64×64)
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) {
pt.map(1, x, y, { (uint8_t)(x & 0xFF), (uint8_t)(y & 0xFF) });
}
}
auto start = Clock::now();
for (int iter = 0; iter < 1000; iter++) {
// Re-fill the invalidated region each iteration
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
pt.map(2, x, y, { (uint8_t)x, (uint8_t)y });
}
}
// Invalidate a 4×4 region at mip 2 — cascades to mip 1 (8×8) and mip 0 (16×16)
pt.invalidate_region(2, 0, 0, 4, 4);
}
auto end = Clock::now();
double ms = elapsed_ms(start, end) / 1000.0;
report("Invalidate 4x4 region + cascade (per op)", ms, 1.0);
}
// ── Benchmark 11: Coordinate conversion ──────────────────────────────────────
void bench_coord_conversion() {
const int COUNT = 10000;
float vecs[COUNT * 3];
float out[COUNT * 3];
for (int i = 0; i < COUNT * 3; i++)
vecs[i] = (float)i * 0.01f;
auto start = Clock::now();
for (int iter = 0; iter < 1000; iter++) {
for (int i = 0; i < COUNT; i++) {
godot_to_blender(vecs + i * 3, out + i * 3);
}
}
auto end = Clock::now();
double ms = elapsed_ms(start, end) / 1000.0;
report("Coord convert 10K verts (per batch)", ms, 0.1);
}
// ── Main ─────────────────────────────────────────────────────────────────────
int main() {
printf("=== Performance Benchmarks ===\n");
printf(" (compiled with -O2, results may vary)\n\n");
bench_shm_header();
bench_object_entries();
bench_transform_hash();
bench_blob_write();
bench_page_table_map();
bench_page_table_lookup();
bench_tile_cache();
bench_feedback_parse();
bench_bootstrap();
bench_invalidation();
bench_coord_conversion();
printf("\nDone.\n");
return 0;
}