OverviewFilesCommitsIssuesDocsReleasesPeers

blender-bridge / test / test_benchmarks.cpp

403 lines · cpp ref:
// 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;
}