Phase 0: profiling scaffolding for DOD refactor
Adds a tiny zone-based profiler (prof.{h,cpp}) enabled via KOBO_PROF=1.
Instruments run_game with zones for myship.move, enemies.move (split into
realize and move_pass), hit_structure, and hit_bolt. Adds KOBO_STRESS=N
to keep the enemy pool topped up with rocks for stable per-frame timing.
Why: the entity/render pipeline refactor in BENCH.md needs a comparable
baseline before any layout changes. Without a stress workload the natural
gameplay enemy count is too noisy to compare across commits.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
aa8387e9ec
commit
869056c5d2
54
BENCH.md
Normal file
54
BENCH.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Kobo Deluxe DOD Refactor — Benchmark Scoreboard
|
||||
|
||||
Each row is one phase of the refactor (see
|
||||
`~/.claude/plans/can-you-explain-the-noble-wilkes.md`). Numbers are median
|
||||
ns/frame from `KOBO_PROF=1 KOBO_STRESS=1500 ./build/kobodl`, ~1000 frames,
|
||||
release build (`-O2 -g`).
|
||||
|
||||
## How to capture a baseline
|
||||
|
||||
```sh
|
||||
cd build
|
||||
cmake --build . -j
|
||||
KOBO_PROF=1 KOBO_STRESS=1500 ./kobodl
|
||||
# play through a stage; profile dumps every 600 frames to stderr.
|
||||
# Redirect stderr to a file if you want to keep it:
|
||||
KOBO_PROF=1 KOBO_STRESS=1500 ./kobodl 2>prof.log
|
||||
```
|
||||
|
||||
The profiler dumps a table per zone with `calls`, `total_ms`, `ns/call`.
|
||||
Record the `ns/call` for these zones (they are the moving parts the refactor
|
||||
targets):
|
||||
|
||||
| Zone | What it measures |
|
||||
|-----------------------|-----------------------------------------------------|
|
||||
| `run_game` | Full game tick |
|
||||
| `myship.move` | Player + bolt update |
|
||||
| `enemies.move` | Realize pass + move pass combined |
|
||||
| `enemies.realize` | Just the realize-reserved pass |
|
||||
| `enemies.move_pass` | Just the per-enemy move pass (the hot loop) |
|
||||
| `hit_structure` | Player vs map + bolt vs map |
|
||||
| `hit_bolt` | Bolt-vs-enemy scan (called from inside enemy move) |
|
||||
|
||||
## Scoreboard
|
||||
|
||||
| Phase | run_game | myship.move | enemies.move_pass | hit_bolt | Notes |
|
||||
|------------------------------|---------:|------------:|------------------:|---------:|-------|
|
||||
| 0 — baseline | ? | ? | ? | ? | TBD — user to capture |
|
||||
| 1 — hot/cold split | | | | | |
|
||||
| 2 — SoA pool | | | | | |
|
||||
| 3 — bucket dispatch | | | | | |
|
||||
| 4 — flipped bolt collision | | | | | |
|
||||
| 5 — render decouple | | | | | |
|
||||
| 6 — SIMD integrator (opt.) | | | | | |
|
||||
|
||||
## Notes
|
||||
|
||||
- `KOBO_STRESS=N` keeps the pool topped up with rocks; N=1500 sits well
|
||||
below ENEMY_MAX=2048 so the spawner doesn't thrash on full-pool checks.
|
||||
- The stress spawner uses `pubrand`, which is acceptable for benchmarking
|
||||
but will *not* produce identical demos across builds — that's fine, we
|
||||
only care about steady-state cost.
|
||||
- For a stable run, start the game, get into a stage, then let it sit for
|
||||
~1000 frames before reading numbers. The first dump (frame 600) will
|
||||
include startup costs and should be discarded.
|
||||
@ -160,7 +160,7 @@ set(KOBO_SOURCES
|
||||
myship.cpp radar.cpp random.cpp scenes.cpp score.cpp screen.cpp
|
||||
filemap.cpp prefs.cpp cfgform.cpp options.cpp gamestate.cpp
|
||||
states.cpp form.cpp cfgparse.cpp game.cpp kobo.cpp logger.c
|
||||
dashboard.cpp sound.cpp
|
||||
dashboard.cpp sound.cpp prof.cpp
|
||||
)
|
||||
|
||||
add_executable(kobodl ${KOBO_SOURCES})
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
#include "enemies.h"
|
||||
#include "random.h"
|
||||
#include "radar.h"
|
||||
#include "prof.h"
|
||||
|
||||
_enemy _enemies::enemy[ENEMY_MAX];
|
||||
_enemy *_enemies::enemy_max;
|
||||
@ -99,15 +100,21 @@ int _enemies::init()
|
||||
void _enemies::move()
|
||||
{
|
||||
_enemy *enemyp;
|
||||
{
|
||||
PROF_ZONE("enemies.realize");
|
||||
/* realize reserved enemies */
|
||||
for(enemyp = enemy; enemyp < enemy + ENEMY_MAX; enemyp++)
|
||||
{
|
||||
if(enemyp->realize())
|
||||
enemy_max = enemyp;
|
||||
}
|
||||
}
|
||||
{
|
||||
PROF_ZONE("enemies.move_pass");
|
||||
for(enemyp = enemy; enemyp <= enemy_max; enemyp++)
|
||||
enemyp->move();
|
||||
}
|
||||
}
|
||||
|
||||
void _enemies::move_intro()
|
||||
{
|
||||
|
||||
3
kobo.cpp
3
kobo.cpp
@ -61,6 +61,7 @@ extern "C" {
|
||||
#include "options.h"
|
||||
#include "myship.h"
|
||||
#include "enemies.h"
|
||||
#include "prof.h"
|
||||
|
||||
#define MAX_FPS_RESULTS 64
|
||||
|
||||
@ -2054,6 +2055,8 @@ int main(int argc, char *argv[])
|
||||
signal(SIGTERM, breakhandler);
|
||||
signal(SIGINT, breakhandler);
|
||||
|
||||
prof_init();
|
||||
|
||||
SDL_Init(0);
|
||||
|
||||
if(main_init())
|
||||
|
||||
44
manage.cpp
44
manage.cpp
@ -50,9 +50,24 @@
|
||||
#include "states.h"
|
||||
#include "audio.h"
|
||||
#include "random.h"
|
||||
#include "prof.h"
|
||||
|
||||
#define GIGA 1000000000
|
||||
|
||||
// Stress-test spawner: keeps the enemy pool topped up with rocks so frame
|
||||
// timing is stable. Enabled by KOBO_STRESS=N (target population, 0 = off).
|
||||
static int kobo_stress_target = -1;
|
||||
static int kobo_stress_init(void)
|
||||
{
|
||||
if (kobo_stress_target >= 0) return kobo_stress_target;
|
||||
const char *e = getenv("KOBO_STRESS");
|
||||
kobo_stress_target = (e && *e) ? atoi(e) : 0;
|
||||
if (kobo_stress_target > 0)
|
||||
fprintf(stderr, "[stress] target population=%d\n", kobo_stress_target);
|
||||
return kobo_stress_target;
|
||||
}
|
||||
static void kobo_stress_step(void);
|
||||
|
||||
int _manage::blank = 0;
|
||||
int _manage::next_state_out;
|
||||
int _manage::next_state_next;
|
||||
@ -556,6 +571,7 @@ void _manage::run_pause()
|
||||
|
||||
void _manage::run_game()
|
||||
{
|
||||
PROF_ZONE("run_game");
|
||||
put_health();
|
||||
put_temp();
|
||||
|
||||
@ -583,11 +599,33 @@ void _manage::run_game()
|
||||
}
|
||||
}
|
||||
|
||||
myship.move();
|
||||
enemies.move();
|
||||
myship.hit_structure();
|
||||
if(kobo_stress_init() > 0)
|
||||
kobo_stress_step();
|
||||
|
||||
{ PROF_ZONE("myship.move"); myship.move(); }
|
||||
{ PROF_ZONE("enemies.move"); enemies.move(); }
|
||||
{ PROF_ZONE("hit_structure"); myship.hit_structure(); }
|
||||
update();
|
||||
++hi.playtime;
|
||||
prof_frame_tick();
|
||||
}
|
||||
|
||||
// Top up the enemy pool with rocks until we hit the target population.
|
||||
// Rocks are inert enough to give a stable per-frame integration cost.
|
||||
static void kobo_stress_step(void)
|
||||
{
|
||||
extern const enemy_kind rock;
|
||||
int target = kobo_stress_target;
|
||||
if (target <= 0) return;
|
||||
// Spawn at most a few per frame to avoid burst allocation spikes.
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
int px = (pubrand.get() & (WORLD_SIZEX - 1));
|
||||
int py = (pubrand.get() & (WORLD_SIZEY - 1));
|
||||
int h = (int)(pubrand.get() & 0x7f) - 64;
|
||||
int v = (int)(pubrand.get() & 0x7f) - 64;
|
||||
// enemies.make returns 0 on success, non-zero when pool is full.
|
||||
if (enemies.make(&rock, px, py, h, v) != 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
#include "manage.h"
|
||||
#include "random.h"
|
||||
#include "sound.h"
|
||||
#include "prof.h"
|
||||
|
||||
#define WING_GUN_OFFSET 4
|
||||
|
||||
@ -384,6 +385,7 @@ int _myship::hit_structure()
|
||||
|
||||
int _myship::hit_bolt(int ex, int ey, int hitsize, int health)
|
||||
{
|
||||
PROF_ZONE("hit_bolt");
|
||||
int dmg = 0;
|
||||
int i;
|
||||
for(i = 0; i < MAX_BOLTS; i++)
|
||||
|
||||
41
prof.cpp
Normal file
41
prof.cpp
Normal file
@ -0,0 +1,41 @@
|
||||
#include "prof.h"
|
||||
|
||||
prof_zone_t prof_zones[PROF_MAX_ZONES];
|
||||
int prof_nzones = 0;
|
||||
int prof_enabled = 0;
|
||||
uint64_t prof_frame_count = 0;
|
||||
|
||||
extern "C" void prof_init(void)
|
||||
{
|
||||
const char *e = getenv("KOBO_PROF");
|
||||
prof_enabled = (e && *e && *e != '0') ? 1 : 0;
|
||||
if (prof_enabled)
|
||||
fprintf(stderr, "[prof] enabled (dump every %d frames)\n",
|
||||
PROF_DUMP_EVERY_FRAMES);
|
||||
}
|
||||
|
||||
extern "C" void prof_dump(FILE *f)
|
||||
{
|
||||
if (!prof_enabled) return;
|
||||
fprintf(f, "[prof] frame=%llu zones=%d\n",
|
||||
(unsigned long long)prof_frame_count, prof_nzones);
|
||||
for (int i = 0; i < prof_nzones; ++i) {
|
||||
prof_zone_t *z = &prof_zones[i];
|
||||
double ns_per_call = z->calls ? (double)z->total_ns / (double)z->calls : 0.0;
|
||||
double total_ms = (double)z->total_ns / 1.0e6;
|
||||
fprintf(f, " %-28s calls=%-10llu total_ms=%-10.3f ns/call=%.1f\n",
|
||||
z->name,
|
||||
(unsigned long long)z->calls,
|
||||
total_ms,
|
||||
ns_per_call);
|
||||
}
|
||||
fflush(f);
|
||||
}
|
||||
|
||||
extern "C" void prof_frame_tick(void)
|
||||
{
|
||||
if (!prof_enabled) return;
|
||||
++prof_frame_count;
|
||||
if (prof_frame_count % PROF_DUMP_EVERY_FRAMES == 0)
|
||||
prof_dump(stderr);
|
||||
}
|
||||
94
prof.h
Normal file
94
prof.h
Normal file
@ -0,0 +1,94 @@
|
||||
// Lightweight zone-based profiler for measuring the DOD refactor.
|
||||
// Enabled at runtime via KOBO_PROF=1 in the environment.
|
||||
// See plans: .claude/plans/can-you-explain-the-noble-wilkes.md (Phase 0).
|
||||
|
||||
#ifndef KOBO_PROF_H
|
||||
#define KOBO_PROF_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PROF_MAX_ZONES 32
|
||||
#define PROF_DUMP_EVERY_FRAMES 600
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint64_t calls;
|
||||
uint64_t total_ns;
|
||||
} prof_zone_t;
|
||||
|
||||
extern prof_zone_t prof_zones[PROF_MAX_ZONES];
|
||||
extern int prof_nzones;
|
||||
extern int prof_enabled;
|
||||
extern uint64_t prof_frame_count;
|
||||
|
||||
static inline uint64_t prof_now_ns(void)
|
||||
{
|
||||
#if defined(__APPLE__) || defined(__linux__)
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Returns the index of the zone with the given name, creating it on first use.
|
||||
// Names must be string literals (we compare by pointer for speed).
|
||||
static inline int prof_zone_id(const char *name)
|
||||
{
|
||||
for (int i = 0; i < prof_nzones; ++i)
|
||||
if (prof_zones[i].name == name)
|
||||
return i;
|
||||
if (prof_nzones >= PROF_MAX_ZONES)
|
||||
return PROF_MAX_ZONES - 1;
|
||||
int id = prof_nzones++;
|
||||
prof_zones[id].name = name;
|
||||
prof_zones[id].calls = 0;
|
||||
prof_zones[id].total_ns = 0;
|
||||
return id;
|
||||
}
|
||||
|
||||
static inline void prof_accumulate(int id, uint64_t ns)
|
||||
{
|
||||
prof_zones[id].calls += 1;
|
||||
prof_zones[id].total_ns += ns;
|
||||
}
|
||||
|
||||
void prof_init(void);
|
||||
void prof_frame_tick(void);
|
||||
void prof_dump(FILE *f);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
// RAII helper for C++ scoping.
|
||||
struct ProfScope {
|
||||
int id;
|
||||
uint64_t t0;
|
||||
inline ProfScope(int zone_id) : id(zone_id), t0(prof_enabled ? prof_now_ns() : 0) {}
|
||||
inline ~ProfScope() {
|
||||
if (prof_enabled) prof_accumulate(id, prof_now_ns() - t0);
|
||||
}
|
||||
};
|
||||
|
||||
// Pastes the line number to make the static unique per use-site.
|
||||
#define PROF_ZONE_CONCAT2(a,b) a##b
|
||||
#define PROF_ZONE_CONCAT(a,b) PROF_ZONE_CONCAT2(a,b)
|
||||
#define PROF_ZONE(name) \
|
||||
static int PROF_ZONE_CONCAT(_prof_zid_, __LINE__) = -1; \
|
||||
if (PROF_ZONE_CONCAT(_prof_zid_, __LINE__) < 0) \
|
||||
PROF_ZONE_CONCAT(_prof_zid_, __LINE__) = prof_zone_id(name); \
|
||||
ProfScope PROF_ZONE_CONCAT(_prof_scope_, __LINE__)( \
|
||||
PROF_ZONE_CONCAT(_prof_zid_, __LINE__))
|
||||
|
||||
#endif
|
||||
|
||||
#endif // KOBO_PROF_H
|
||||
Loading…
x
Reference in New Issue
Block a user