929 lines
31 KiB
C
929 lines
31 KiB
C
|
|
/**
|
||
|
|
* @file perf.c
|
||
|
|
* @brief Stress and timing benchmarks for every subsystem that does not need a renderer.
|
||
|
|
*
|
||
|
|
* These are not correctness tests. Each one drives a hot path -- the pool scans,
|
||
|
|
* the registry, the physics sweep, the per-actor update, the geometry helpers,
|
||
|
|
* the JSON accessors -- hard enough that its per-operation cost is measurable,
|
||
|
|
* and reports what that cost is. The companion suite in `tests/perf_render.c`
|
||
|
|
* covers the paths that need a renderer.
|
||
|
|
*
|
||
|
|
* The pools are the reason this matters. Every acquire is a linear scan of a
|
||
|
|
* fixed array looking for a zero reference count, so cost grows with how full
|
||
|
|
* the pool is, not with how much is being asked for. Several benchmarks below
|
||
|
|
* are therefore run twice: once against an empty pool, which is the number a
|
||
|
|
* casual reading of the code predicts, and once against a nearly full one,
|
||
|
|
* which is the number a game in its fifth minute actually gets.
|
||
|
|
*
|
||
|
|
* Everything here runs headless under the dummy drivers, and every benchmark
|
||
|
|
* rebuilds the state it needs rather than inheriting whatever the previous one
|
||
|
|
* left behind.
|
||
|
|
*
|
||
|
|
* @note SDL's log output is redirected to a sink that discards it. The library
|
||
|
|
* logs on paths this suite hammers -- akgl_actor_initialize logs every
|
||
|
|
* spawn -- and timing a write to a terminal or a CTest capture file
|
||
|
|
* measures the machine's I/O, not libakgl. What the *formatting* costs is
|
||
|
|
* measured deliberately, by the pair of actor-spawn benchmarks.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <SDL3/SDL.h>
|
||
|
|
#include <string.h>
|
||
|
|
#include <jansson.h>
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#include <akgl/error.h>
|
||
|
|
#include <akgl/game.h>
|
||
|
|
#include <akgl/heap.h>
|
||
|
|
#include <akgl/registry.h>
|
||
|
|
#include <akgl/actor.h>
|
||
|
|
#include <akgl/character.h>
|
||
|
|
#include <akgl/sprite.h>
|
||
|
|
#include <akgl/physics.h>
|
||
|
|
#include <akgl/staticstring.h>
|
||
|
|
#include <akgl/util.h>
|
||
|
|
#include <akgl/json_helpers.h>
|
||
|
|
#include <akgl/tilemap.h>
|
||
|
|
|
||
|
|
#include "benchutil.h"
|
||
|
|
#include "testutil.h"
|
||
|
|
|
||
|
|
/** @brief How many actors the frame-shaped benchmarks put on the heap. */
|
||
|
|
#define BENCH_ACTOR_COUNT AKGL_MAX_HEAP_ACTOR
|
||
|
|
/** @brief The JSON document the accessor benchmarks read. */
|
||
|
|
#define BENCH_JSON_FIXTURE "assets/snippets/test_json_helpers.json"
|
||
|
|
/** @brief A path that exists relative to the CTest working directory, for akgl_path_relative. */
|
||
|
|
#define BENCH_PATH_FIXTURE "assets/testcharacter.json"
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Print the library's fixed memory footprint.
|
||
|
|
*
|
||
|
|
* Not a benchmark, but it belongs in the same report: libakgl does not call
|
||
|
|
* `malloc`, so every one of these arrays exists from process start whether the
|
||
|
|
* game uses one slot or all of them, and several of the timings above are
|
||
|
|
* explained entirely by their size. The string pool is a megabyte of `char`
|
||
|
|
* arrays, and an akgl_Tilemap is dominated by 16 layers of 512x512 `int` cells
|
||
|
|
* and 16 tilesets of 65536 offset pairs -- which is why zeroing one costs what
|
||
|
|
* it costs, and why the header warns against putting one on the stack.
|
||
|
|
*/
|
||
|
|
static void bench_report_footprint(void)
|
||
|
|
{
|
||
|
|
size_t total = 0;
|
||
|
|
|
||
|
|
total = sizeof(HEAP_ACTOR) + sizeof(HEAP_SPRITE) + sizeof(HEAP_SPRITESHEET) +
|
||
|
|
sizeof(HEAP_CHARACTER) + sizeof(HEAP_STRING);
|
||
|
|
|
||
|
|
printf("\n");
|
||
|
|
printf("static footprint, fixed at compile time:\n");
|
||
|
|
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||
|
|
"HEAP_ACTOR", AKGL_MAX_HEAP_ACTOR, sizeof(akgl_Actor), sizeof(HEAP_ACTOR));
|
||
|
|
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||
|
|
"HEAP_SPRITE", AKGL_MAX_HEAP_SPRITE, sizeof(akgl_Sprite), sizeof(HEAP_SPRITE));
|
||
|
|
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||
|
|
"HEAP_SPRITESHEET", AKGL_MAX_HEAP_SPRITESHEET, sizeof(akgl_SpriteSheet), sizeof(HEAP_SPRITESHEET));
|
||
|
|
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||
|
|
"HEAP_CHARACTER", AKGL_MAX_HEAP_CHARACTER, sizeof(akgl_Character), sizeof(HEAP_CHARACTER));
|
||
|
|
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||
|
|
"HEAP_STRING", AKGL_MAX_HEAP_STRING, sizeof(akgl_String), sizeof(HEAP_STRING));
|
||
|
|
printf(" %-24s %5s %7s %10zu bytes\n", "pools, total", "", "", total);
|
||
|
|
printf(" %-24s %5d x %7s = %10zu bytes\n",
|
||
|
|
"akgl_Tilemap", 1, "", sizeof(akgl_Tilemap));
|
||
|
|
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||
|
|
" of which layers", AKGL_TILEMAP_MAX_LAYERS, sizeof(akgl_TilemapLayer),
|
||
|
|
(sizeof(akgl_TilemapLayer) * AKGL_TILEMAP_MAX_LAYERS));
|
||
|
|
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||
|
|
" of which tilesets", AKGL_TILEMAP_MAX_TILESETS, sizeof(akgl_Tileset),
|
||
|
|
(sizeof(akgl_Tileset) * AKGL_TILEMAP_MAX_TILESETS));
|
||
|
|
fflush(stdout);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Swallow SDL's log output.
|
||
|
|
*
|
||
|
|
* Installed for the whole run. See the note in the file comment: the library
|
||
|
|
* logs on paths this suite calls hundreds of thousands of times, and the cost of
|
||
|
|
* writing that out is not a property of libakgl.
|
||
|
|
*/
|
||
|
|
static void bench_discard_log(void *userdata, int category, SDL_LogPriority priority, const char *message)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Claim a spritesheet slot without loading an image into it.
|
||
|
|
*
|
||
|
|
* akgl_spritesheet_initialize uploads a texture and so needs a renderer, which
|
||
|
|
* this suite deliberately does not have. Nothing here draws, and the only thing
|
||
|
|
* that reads the sheet is akgl_sprite_initialize storing the pointer, so an
|
||
|
|
* empty claimed slot is enough.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_make_sheet(akgl_SpriteSheet **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||
|
|
PASS(errctx, akgl_heap_next_spritesheet(dest));
|
||
|
|
memset(*dest, 0x00, sizeof(akgl_SpriteSheet));
|
||
|
|
(*dest)->refcount += 1;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Build a sprite on a freshly claimed sheet and publish it in the sprite registry.
|
||
|
|
*
|
||
|
|
* @param dest Receives the sprite. Required.
|
||
|
|
* @param name Registry key. Copied at a fixed #AKGL_SPRITE_MAX_NAME_LENGTH
|
||
|
|
* bytes by akgl_sprite_initialize, so it is staged through a buffer
|
||
|
|
* of exactly that size first.
|
||
|
|
* @param speed Nanoseconds one frame is held. 0 makes every akgl_actor_update
|
||
|
|
* advance the animation, which is the expensive path.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_make_sprite(akgl_Sprite **dest, char *name, uint32_t speed)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akgl_SpriteSheet *sheet = NULL;
|
||
|
|
char namebuf[AKGL_SPRITE_MAX_NAME_LENGTH];
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||
|
|
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
|
||
|
|
|
||
|
|
memset(&namebuf, 0x00, sizeof(namebuf));
|
||
|
|
strncpy((char *)&namebuf, name, AKGL_SPRITE_MAX_NAME_LENGTH - 1);
|
||
|
|
|
||
|
|
PASS(errctx, bench_make_sheet(&sheet));
|
||
|
|
PASS(errctx, akgl_heap_next_sprite(dest));
|
||
|
|
PASS(errctx, akgl_sprite_initialize(*dest, (char *)&namebuf, sheet));
|
||
|
|
(*dest)->frames = 3;
|
||
|
|
(*dest)->frameids[0] = 0;
|
||
|
|
(*dest)->frameids[1] = 1;
|
||
|
|
(*dest)->frameids[2] = 2;
|
||
|
|
(*dest)->width = 48;
|
||
|
|
(*dest)->height = 48;
|
||
|
|
(*dest)->speed = speed;
|
||
|
|
(*dest)->loop = true;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Build a character with one sprite mapped to the two states these benchmarks use.
|
||
|
|
*
|
||
|
|
* State 0 is what akgl_actor_initialize leaves an actor in and what
|
||
|
|
* akgl_actor_automatic_face leaves it in while it is standing still;
|
||
|
|
* `MOVING_RIGHT | FACE_RIGHT` is what bench_fill_actor_pool puts it in and what
|
||
|
|
* the face logic settles on from there. Mapping both keeps the update path off
|
||
|
|
* the missing-sprite branch, which is measured deliberately elsewhere rather
|
||
|
|
* than by accident here.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_make_character(akgl_Character **dest, char *name, uint32_t spritespeed)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akgl_Sprite *sprite = NULL;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||
|
|
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_next_character(dest));
|
||
|
|
PASS(errctx, akgl_character_initialize(*dest, name));
|
||
|
|
(*dest)->sx = 120.0;
|
||
|
|
(*dest)->sy = 120.0;
|
||
|
|
(*dest)->sz = 0.0;
|
||
|
|
(*dest)->ax = 40.0;
|
||
|
|
(*dest)->ay = 40.0;
|
||
|
|
(*dest)->speedtime = 16;
|
||
|
|
|
||
|
|
PASS(errctx, bench_make_sprite(&sprite, name, spritespeed));
|
||
|
|
PASS(errctx, (*dest)->sprite_add(*dest, sprite, 0));
|
||
|
|
PASS(errctx,
|
||
|
|
(*dest)->sprite_add(
|
||
|
|
*dest,
|
||
|
|
sprite,
|
||
|
|
(AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT))
|
||
|
|
);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Populate the actor pool with @p count live actors bound to @p basechar.
|
||
|
|
*
|
||
|
|
* The heap and the actor registry are rebuilt first, so this is the state every
|
||
|
|
* frame-shaped benchmark starts from rather than something it inherits.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_fill_actor_pool(akgl_Character *basechar, int count)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akgl_Actor *actor = NULL;
|
||
|
|
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "basechar");
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_init_actor());
|
||
|
|
PASS(errctx, akgl_registry_init_actor());
|
||
|
|
|
||
|
|
for ( i = 0; i < count; i++ ) {
|
||
|
|
memset(&name, 0x00, sizeof(name));
|
||
|
|
snprintf((char *)&name, AKGL_ACTOR_MAX_NAME_LENGTH, "benchactor%d", i);
|
||
|
|
PASS(errctx, akgl_heap_next_actor(&actor));
|
||
|
|
PASS(errctx, akgl_actor_initialize(actor, (char *)&name));
|
||
|
|
PASS(errctx, akgl_actor_set_character(actor, basechar->name));
|
||
|
|
actor->visible = true;
|
||
|
|
actor->layer = 0;
|
||
|
|
actor->x = (float32_t)(i * 13);
|
||
|
|
actor->y = (float32_t)(i * 7);
|
||
|
|
actor->sx = 120.0;
|
||
|
|
actor->sy = 120.0;
|
||
|
|
actor->ax = 40.0;
|
||
|
|
actor->ay = 40.0;
|
||
|
|
AKGL_BITMASK_ADD(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time a claim from an empty actor pool against one from a nearly full pool.
|
||
|
|
*
|
||
|
|
* The pool scan stops at the first slot with a zero reference count, so an empty
|
||
|
|
* pool answers from index 0 and a pool with one slot left answers from index
|
||
|
|
* #AKGL_MAX_HEAP_ACTOR - 1. The ratio between these two numbers *is* the cost of
|
||
|
|
* the linear-scan allocator.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_heap_actor_claim(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_Actor *actor = NULL;
|
||
|
|
int count = bench_iterations(500000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
PASS(errctx, akgl_heap_init_actor());
|
||
|
|
bench_start("heap_next_actor, empty pool", "call", 100.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
for ( i = 0; i < (AKGL_MAX_HEAP_ACTOR - 1); i++ ) {
|
||
|
|
HEAP_ACTOR[i].refcount = 1;
|
||
|
|
}
|
||
|
|
bench_start("heap_next_actor, one slot left", "call", 400.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
PASS(errctx, akgl_heap_init_actor());
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time the string pool, which is the one that hurts.
|
||
|
|
*
|
||
|
|
* Every entry is #AKGL_MAX_STRING_LENGTH bytes, so 256 of them is a megabyte and
|
||
|
|
* the scan for a free slot touches one reference count every 4 KiB -- a cache
|
||
|
|
* miss per candidate. Release then wipes the whole 4 KiB whether the string held
|
||
|
|
* a path or a single character. Three numbers come out of this: the claim on an
|
||
|
|
* empty pool, the claim with one slot left, and the release on its own.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_heap_string(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_String *str = NULL;
|
||
|
|
int count = bench_iterations(100000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
|
||
|
|
// Claim and release together: the scratch-buffer idiom the library uses
|
||
|
|
// everywhere, and the only string benchmark whose cost a caller can
|
||
|
|
// actually observe from outside.
|
||
|
|
bench_start("heap string claim + release cycle", "cycle", 500.0);
|
||
|
|
for ( i = 0; i < count; i++ ) {
|
||
|
|
inner = akgl_heap_next_string(&str);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
inner = akgl_heap_release_string(str);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
// The claim on its own. The reference the claim takes is dropped by hand
|
||
|
|
// rather than through release, so the 4 KiB wipe stays out of this one.
|
||
|
|
bench_start("heap_next_string, empty pool", "call", 100.0);
|
||
|
|
for ( i = 0; i < count; i++ ) {
|
||
|
|
inner = akgl_heap_next_string(&str);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
str->refcount = 0;
|
||
|
|
}
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
// The same claim with 255 of the 256 slots taken: a megabyte walked, one
|
||
|
|
// reference count read per 4 KiB page.
|
||
|
|
for ( i = 0; i < (AKGL_MAX_HEAP_STRING - 1); i++ ) {
|
||
|
|
HEAP_STRING[i].refcount = 1;
|
||
|
|
}
|
||
|
|
bench_start("heap_next_string, one slot left", "call", 2500.0);
|
||
|
|
for ( i = 0; i < count; i++ ) {
|
||
|
|
inner = akgl_heap_next_string(&str);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
str->refcount = 0;
|
||
|
|
}
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
|
||
|
|
// Release on its own: one 4 KiB memset per call, whatever the string held.
|
||
|
|
PASS(errctx, akgl_heap_next_string(&str));
|
||
|
|
bench_start("heap_release_string, 4 KiB wipe", "call", 500.0);
|
||
|
|
for ( i = 0; i < count; i++ ) {
|
||
|
|
str->refcount = 1;
|
||
|
|
inner = akgl_heap_release_string(str);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time a full reset of every pool.
|
||
|
|
*
|
||
|
|
* akgl_heap_init zeroes all five arrays -- better than a megabyte, dominated by
|
||
|
|
* the string pool. A game pays this once at startup and again on every call to
|
||
|
|
* akgl_game_init; a level transition that calls it per level pays it per level.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_heap_init(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
int count = bench_iterations(2000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("heap_init, all five pools", "call", 450000.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_heap_init());
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time an actor spawn, with and without the log line the library writes.
|
||
|
|
*
|
||
|
|
* A spawn is a pool claim, a `memset` of the whole actor, a registry insert, and
|
||
|
|
* an `SDL_Log`. The second run raises the log priority so SDL returns before it
|
||
|
|
* formats anything; the gap between the two numbers is what the log line costs
|
||
|
|
* every caller, including the ones that never read it.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_actor_spawn(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_Actor *actor = NULL;
|
||
|
|
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||
|
|
int count = bench_iterations(20000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
memset(&name, 0x00, sizeof(name));
|
||
|
|
strncpy((char *)&name, "benchspawn", AKGL_ACTOR_MAX_NAME_LENGTH - 1);
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
PASS(errctx, akgl_heap_init_actor());
|
||
|
|
PASS(errctx, akgl_registry_init_actor());
|
||
|
|
|
||
|
|
bench_start("actor spawn + release, library logging on", "actor", 2100.0);
|
||
|
|
for ( i = 0; i < count; i++ ) {
|
||
|
|
inner = akgl_heap_next_actor(&actor);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
inner = akgl_actor_initialize(actor, (char *)&name);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
inner = akgl_heap_release_actor(actor);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
SDL_SetLogPriorities(SDL_LOG_PRIORITY_CRITICAL);
|
||
|
|
bench_start("actor spawn + release, logging suppressed", "actor", 1700.0);
|
||
|
|
for ( i = 0; i < count; i++ ) {
|
||
|
|
inner = akgl_heap_next_actor(&actor);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
inner = akgl_actor_initialize(actor, (char *)&name);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
inner = akgl_heap_release_actor(actor);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
bench_stop(count);
|
||
|
|
SDL_SetLogPriorities(SDL_LOG_PRIORITY_INFO);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time a registry lookup with the actor registry full.
|
||
|
|
*
|
||
|
|
* akgl_actor_set_character is the library's own name-to-pointer lookup: an SDL
|
||
|
|
* property fetch against a registry holding #AKGL_MAX_HEAP_ACTOR entries, plus
|
||
|
|
* the four field copies that follow it. Every actor built from a tilemap object
|
||
|
|
* layer pays it once.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_registry_lookup(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_Character *basechar = NULL;
|
||
|
|
akgl_Actor *actor = NULL;
|
||
|
|
int count = bench_iterations(200000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
PASS(errctx, akgl_registry_init());
|
||
|
|
PASS(errctx, bench_make_character(&basechar, "benchlookupchar", 0));
|
||
|
|
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
|
||
|
|
actor = &HEAP_ACTOR[0];
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("actor_set_character, registry lookup", "call", 400.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_actor_set_character(actor, basechar->name));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time the property store a game reads its configuration out of.
|
||
|
|
*
|
||
|
|
* akgl_get_property copies a fixed #AKGL_MAX_STRING_LENGTH bytes out of the
|
||
|
|
* property store regardless of how long the value is, so reading `"0.0"` moves
|
||
|
|
* 4 KiB. akgl_physics_init_arcade makes six of these calls.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_properties(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_String *value = NULL;
|
||
|
|
int count = bench_iterations(200000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
PASS(errctx, akgl_registry_init_properties());
|
||
|
|
PASS(errctx, akgl_set_property("bench.property", "a short value"));
|
||
|
|
PASS(errctx, akgl_heap_next_string(&value));
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("set_property", "call", 1000.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_set_property("bench.property", "a short value"));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
bench_start("get_property, 4 KiB copy", "call", 900.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_get_property("bench.property", &value, "unset"));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
PASS(errctx, akgl_heap_release_string(value));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time the state-to-sprite lookup that runs twice per actor per frame.
|
||
|
|
*
|
||
|
|
* akgl_character_sprite_get renders the state bitmask to decimal with `SDL_itoa`
|
||
|
|
* and looks the result up as a property string. akgl_actor_update calls it, and
|
||
|
|
* so does akgl_actor_render, so a 64-actor frame makes 128 of these calls before
|
||
|
|
* anything is drawn.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_character_sprite_get(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_Character *basechar = NULL;
|
||
|
|
akgl_Sprite *sprite = NULL;
|
||
|
|
int count = bench_iterations(200000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
PASS(errctx, akgl_registry_init());
|
||
|
|
PASS(errctx, bench_make_character(&basechar, "benchspritechar", 0));
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("character_sprite_get, state to sprite", "call", 400.0);
|
||
|
|
BENCH_LOOP(inner, i, count, basechar->sprite_get(basechar, 0, &sprite));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time one actor's logic update, with the animation advancing every call.
|
||
|
|
*
|
||
|
|
* The sprite's frame time is 0, so every update takes the branch that changes
|
||
|
|
* frame -- the expensive path, and the one a fast animation actually takes. An
|
||
|
|
* update is a face recalculation, a sprite lookup, a clock read, and the frame
|
||
|
|
* arithmetic.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_actor_update(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_Character *basechar = NULL;
|
||
|
|
akgl_Actor *actor = NULL;
|
||
|
|
int count = bench_iterations(200000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
PASS(errctx, akgl_registry_init());
|
||
|
|
PASS(errctx, bench_make_character(&basechar, "benchupdatechar", 0));
|
||
|
|
PASS(errctx, bench_fill_actor_pool(basechar, 1));
|
||
|
|
actor = &HEAP_ACTOR[0];
|
||
|
|
AKGL_BITMASK_CLEAR(actor->state);
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("actor_update, animation advancing", "actor", 700.0);
|
||
|
|
BENCH_LOOP(inner, i, count, actor->updatefunc(actor));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
// An actor in a state its character has no sprite for is not an error
|
||
|
|
// the library refuses -- akgl_actor_update handles AKERR_KEY and carries
|
||
|
|
// on, and akgl_actor_render logs it and draws nothing. It is a normal
|
||
|
|
// condition on a partly authored character, and it is worth knowing what
|
||
|
|
// one costs, because raising, formatting, and handling a context is far
|
||
|
|
// more work than the update it replaces.
|
||
|
|
AKGL_BITMASK_ADD(actor->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||
|
|
bench_start("actor_update, no sprite for state", "actor", 6200.0);
|
||
|
|
BENCH_LOOP(inner, i, count, actor->updatefunc(actor));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
AKGL_BITMASK_CLEAR(actor->state);
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time the physics sweep over a full actor pool, and over an empty one.
|
||
|
|
*
|
||
|
|
* akgl_physics_simulate walks all #AKGL_MAX_HEAP_ACTOR slots whether or not
|
||
|
|
* anything is in them, so the empty-pool number is the floor every frame pays.
|
||
|
|
* The full-pool number is one frame of movement for 64 actors: movement logic,
|
||
|
|
* gravity, drag, clamp, and integrate, per actor.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_physics_simulate(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_Character *basechar = NULL;
|
||
|
|
akgl_PhysicsBackend backend;
|
||
|
|
int count = bench_iterations(20000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
PASS(errctx, akgl_registry_init());
|
||
|
|
PASS(errctx, akgl_registry_init_properties());
|
||
|
|
PASS(errctx, bench_make_character(&basechar, "benchphysicschar", 0));
|
||
|
|
|
||
|
|
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||
|
|
PASS(errctx, akgl_physics_init_arcade(&backend));
|
||
|
|
backend.gravity_y = 9.8;
|
||
|
|
backend.drag_x = 0.1;
|
||
|
|
backend.drag_y = 0.1;
|
||
|
|
|
||
|
|
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("physics_simulate, 64 live actors", "frame", 12500.0);
|
||
|
|
BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_init_actor());
|
||
|
|
PASS(errctx, akgl_registry_init_actor());
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("physics_simulate, empty pool", "frame", 650.0);
|
||
|
|
BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time the logic half of a frame: update every actor, then simulate.
|
||
|
|
*
|
||
|
|
* This is what a host's main loop does before it draws anything, and it is the
|
||
|
|
* number to subtract from a 16.6 ms frame to find out what is left for drawing.
|
||
|
|
* Note that it is *not* what akgl_game_update does: that one runs the update
|
||
|
|
* loop once per tilemap layer, which is measured in the render suite.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_logic_frame(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_Character *basechar = NULL;
|
||
|
|
akgl_Actor *actor = NULL;
|
||
|
|
akgl_PhysicsBackend backend;
|
||
|
|
int count = bench_iterations(5000);
|
||
|
|
int i = 0;
|
||
|
|
int j = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
PASS(errctx, akgl_registry_init());
|
||
|
|
PASS(errctx, akgl_registry_init_properties());
|
||
|
|
PASS(errctx, bench_make_character(&basechar, "benchframechar", 0));
|
||
|
|
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||
|
|
PASS(errctx, akgl_physics_init_arcade(&backend));
|
||
|
|
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("logic frame, 64 actors updated + simulated", "frame", 60000.0);
|
||
|
|
for ( i = 0; i < count; i++ ) {
|
||
|
|
for ( j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
|
||
|
|
actor = &HEAP_ACTOR[j];
|
||
|
|
if ( actor->refcount == 0 ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
inner = actor->updatefunc(actor);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
inner = backend.simulate(&backend, NULL);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time the geometry helpers, including the all-pairs sweep a caller has to write.
|
||
|
|
*
|
||
|
|
* akgl_collide_rectangles tests eight corners and returns at the first hit, so
|
||
|
|
* an overlap is cheap and a miss is the full eight. The third benchmark is the
|
||
|
|
* broad phase the library does not provide: 64 actors is 2016 pairs, and a
|
||
|
|
* caller doing collision at all does that every frame.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_geometry(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
RectanglePoints points;
|
||
|
|
SDL_FRect rects[BENCH_ACTOR_COUNT];
|
||
|
|
SDL_FRect overlapping = { .x = 8.0, .y = 8.0, .w = 32.0, .h = 32.0 };
|
||
|
|
SDL_FRect disjoint = { .x = 900.0, .y = 900.0, .w = 32.0, .h = 32.0 };
|
||
|
|
SDL_FRect subject = { .x = 0.0, .y = 0.0, .w = 32.0, .h = 32.0 };
|
||
|
|
bool collide = false;
|
||
|
|
int count = bench_iterations(500000);
|
||
|
|
int sweeps = bench_iterations(2000);
|
||
|
|
int pairs = 0;
|
||
|
|
int i = 0;
|
||
|
|
int j = 0;
|
||
|
|
int k = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
|
||
|
|
rects[i].x = (float32_t)(i * 24);
|
||
|
|
rects[i].y = (float32_t)(i * 18);
|
||
|
|
rects[i].w = 32.0;
|
||
|
|
rects[i].h = 32.0;
|
||
|
|
}
|
||
|
|
pairs = (BENCH_ACTOR_COUNT * (BENCH_ACTOR_COUNT - 1)) / 2;
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("rectangle_points", "call", 100.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_rectangle_points(&points, &subject));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
bench_start("collide_rectangles, overlapping", "call", 300.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &overlapping, &collide));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
bench_start("collide_rectangles, disjoint", "call", 600.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &disjoint, &collide));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
bench_start("all-pairs collision sweep, 64 actors", "sweep", 1200000.0);
|
||
|
|
for ( k = 0; k < sweeps; k++ ) {
|
||
|
|
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
|
||
|
|
for ( j = i + 1; j < BENCH_ACTOR_COUNT; j++ ) {
|
||
|
|
inner = akgl_collide_rectangles(&rects[i], &rects[j], &collide);
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ( inner != NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
bench_stop(sweeps);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
printf("all-pairs collision sweep covers %d pairs per sweep\n", pairs);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time the pooled-string operations.
|
||
|
|
*
|
||
|
|
* Both of these move #AKGL_MAX_STRING_LENGTH bytes by default -- a copy with a
|
||
|
|
* count of 0 means "all of it" -- so the cost is the same for a filename and for
|
||
|
|
* a single character.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_strings(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_String *src = NULL;
|
||
|
|
akgl_String *dst = NULL;
|
||
|
|
int count = bench_iterations(200000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
PASS(errctx, akgl_heap_next_string(&src));
|
||
|
|
PASS(errctx, akgl_heap_next_string(&dst));
|
||
|
|
PASS(errctx, akgl_string_initialize(src, "assets/testcharacter.json"));
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("string_initialize", "call", 350.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_string_initialize(dst, "assets/testcharacter.json"));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
bench_start("string_copy, full length", "call", 350.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_string_copy(src, dst, 0));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
PASS(errctx, akgl_heap_release_string(src));
|
||
|
|
PASS(errctx, akgl_heap_release_string(dst));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time parsing a document and reading values back out of it.
|
||
|
|
*
|
||
|
|
* The parse is what an asset load pays per file. The accessors are what the
|
||
|
|
* tilemap and character loaders pay per field, and every one of them that
|
||
|
|
* returns a string claims and fills a 4 KiB pooled string.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_json(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_String *path = NULL;
|
||
|
|
akgl_String *value = NULL;
|
||
|
|
json_t *doc = NULL;
|
||
|
|
json_error_t jsonerr;
|
||
|
|
int number = 0;
|
||
|
|
int loads = bench_iterations(2000);
|
||
|
|
int count = bench_iterations(200000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
PASS(errctx, akgl_heap_next_string(&path));
|
||
|
|
PASS(errctx, akgl_heap_next_string(&value));
|
||
|
|
snprintf((char *)&path->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), BENCH_JSON_FIXTURE);
|
||
|
|
|
||
|
|
doc = json_load_file((char *)&path->data, 0, &jsonerr);
|
||
|
|
FAIL_ZERO_RETURN(errctx, doc, AKERR_IO, "Unable to load %s: %s", (char *)&path->data, jsonerr.text);
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("json_load_file, small document", "load", 120000.0);
|
||
|
|
for ( i = 0; i < loads; i++ ) {
|
||
|
|
json_t *scratch = json_load_file((char *)&path->data, 0, &jsonerr);
|
||
|
|
if ( scratch == NULL ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
json_decref(scratch);
|
||
|
|
}
|
||
|
|
bench_stop(loads);
|
||
|
|
|
||
|
|
bench_start("get_json_string_value", "call", 450.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_get_json_string_value(doc, "name", &value));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
|
||
|
|
bench_start("get_json_integer_value", "call", 150.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_get_json_integer_value(doc, "count", &number));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
json_decref(doc);
|
||
|
|
PASS(errctx, akgl_heap_release_string(path));
|
||
|
|
PASS(errctx, akgl_heap_release_string(value));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Time path resolution, which every asset reference goes through.
|
||
|
|
*
|
||
|
|
* akgl_path_relative calls `realpath(3)`, so this is a syscall and a walk of the
|
||
|
|
* filesystem, per reference, at load time. A tilemap naming twenty tilesets and
|
||
|
|
* sprites pays it twenty times.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *bench_path_relative(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akerr_ErrorContext *inner = NULL;
|
||
|
|
akgl_String *dst = NULL;
|
||
|
|
int count = bench_iterations(20000);
|
||
|
|
int i = 0;
|
||
|
|
int rep = 0;
|
||
|
|
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
PASS(errctx, akgl_heap_next_string(&dst));
|
||
|
|
|
||
|
|
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||
|
|
bench_start("path_relative, realpath on an existing file", "call", 36000.0);
|
||
|
|
BENCH_LOOP(inner, i, count, akgl_path_relative(".", BENCH_PATH_FIXTURE, dst));
|
||
|
|
bench_stop(count);
|
||
|
|
PASS(errctx, inner);
|
||
|
|
}
|
||
|
|
PASS(errctx, akgl_heap_release_string(dst));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(void)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||
|
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||
|
|
SDL_SetLogOutputFunction(bench_discard_log, NULL);
|
||
|
|
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, akgl_error_init());
|
||
|
|
CATCH(errctx, akgl_heap_init());
|
||
|
|
CATCH(errctx, akgl_registry_init());
|
||
|
|
CATCH(errctx, akgl_registry_init_properties());
|
||
|
|
|
||
|
|
CATCH(errctx, bench_heap_actor_claim());
|
||
|
|
CATCH(errctx, bench_heap_string());
|
||
|
|
CATCH(errctx, bench_heap_init());
|
||
|
|
CATCH(errctx, bench_actor_spawn());
|
||
|
|
CATCH(errctx, bench_registry_lookup());
|
||
|
|
CATCH(errctx, bench_properties());
|
||
|
|
CATCH(errctx, bench_character_sprite_get());
|
||
|
|
CATCH(errctx, bench_actor_update());
|
||
|
|
CATCH(errctx, bench_physics_simulate());
|
||
|
|
CATCH(errctx, bench_logic_frame());
|
||
|
|
CATCH(errctx, bench_geometry());
|
||
|
|
CATCH(errctx, bench_strings());
|
||
|
|
CATCH(errctx, bench_json());
|
||
|
|
CATCH(errctx, bench_path_relative());
|
||
|
|
|
||
|
|
bench_report_footprint();
|
||
|
|
BENCH_REPORT_BREAK(errctx);
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH_NORETURN(errctx);
|
||
|
|
}
|