joeylib2/tools/uberPerfTable/uberPerfTable.c

354 lines
12 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// uberPerfTable.c - UBER's speed on every port as one Markdown table,
// against a reference port.
//
// uberPerfTable [-h] [--ref NAME] [--out FILE] [--repo DIR]
//
// Reads the UBER log each port's scripts/bench-<port>.sh leaves under the
// repo's build/ (gPorts) and renders a table with a row for each op. The
// reference port's column (default the IIgs, the perf floor) shows its
// ops/sec; every other port's shows its ops/sec and that as a percentage
// of the reference's, in bold when under 100% (the cells worth attention).
// A port whose log is missing is headed "(no log)" with "-" in its cells;
// where the reference has no number for an op its cell says "pending" and
// the others show their plain ops/sec. Rows follow the reference's op order
// (UBER's own), then the ops only other ports ran. Against the IIgs a note
// on its full-screen ceiling follows the table. Values are those of each
// log's last run (toolUberLog.h). From the repo root:
//
// build/tools/uberPerfTable > PERF.md
// build/tools/uberPerfTable --ref dos
//
// --ref names the reference port (iigs, amiga, atarist, or dos); --out
// writes the table to FILE instead of stdout; --repo names the repo whose
// build/ is read, by default the one this tool was built from (its
// source's, __FILE__). Each option's value follows it or an '='.
//
// Exit: 0 the table is written, 1 the --out file cannot be written, 2 a
// usage error, a missing reference log, or no op in any log.
#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "toolFile.h"
#include "toolUberLog.h"
#define TOOL_NAME "uberPerfTable"
#define OK_EXIT 0
#define WRITE_EXIT 1
#define USAGE_EXIT 2
#define HELP_EXIT 0
#define PERCENT 100.0
#define PATH_BYTES 4096u
// The repo is this many directories up from this source file.
#define SOURCE_DEPTH 3u
#define USAGE_TEXT "usage: " TOOL_NAME " [-h] [--ref NAME] [--out FILE] [--repo DIR]\n"
// The note under a table against the IIgs: the IIgs is a 2.8 MHz 65816
// moving a 32000-byte SHR screen, so a full-screen byte-moving op has a
// hard ceiling, and a number above it is a measurement artifact.
#define CEILING_NOTE \
"\n" \
"> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves\n" \
"> 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both\n" \
"> 3 cyc/byte) / 2.8 MHz ~= 34 ms ~= 29 ops/sec maximum. So\n" \
"> `jlSurfaceClear`, `jlFillRect 320x200`, and `jlStagePresent full`\n" \
"> cannot legitimately exceed ~29; a higher reading means the benchmark\n" \
"> clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt-\n" \
"> driven GetTick, so an op that holds `SEI` while it runs can over-\n" \
"> report its ops/sec. Treat full-screen IIgs numbers above ~29 as\n" \
"> inflated and cross-check against the cycle model.\n"
// The ports, in the table's column order.
typedef enum PortE {
PORT_IIGS = 0,
PORT_AMIGA,
PORT_ATARIST,
PORT_DOS,
PORT_COUNT
} PortE;
typedef struct PortT {
const char *key;
const char *label;
const char *log;
} PortT;
// The command line as read.
typedef struct OptionsT {
PortE ref;
const char *out;
const char *repo;
} OptionsT;
static const PortT gPorts[PORT_COUNT] = {
{"iigs", "IIgs", "build/iigs/bin/joeylog.txt"},
{"amiga", "Amiga", "build/amiga/bin/joeylog.txt"},
{"atarist", "Atari ST", "build/atarist/bin/joeylog.txt"},
{"dos", "DOS", "build/dos/bin/JOEYLOG.TXT"}
};
#define REF_DEFAULT PORT_IIGS
#define CEILING_PORT PORT_IIGS
static void argsRead(int argc, char **argv, OptionsT *options);
static void cellPrint(FILE *f, const ToolUberOpT *port, const ToolUberOpT *ref);
static void helpPrint(void) __attribute__((noreturn));
static bool optionValue(int argc, char **argv, int *a, const char *name, const char **value);
static void portsPrint(FILE *f);
static void tablePrint(FILE *f, PortE ref, const ToolUberLogT *logs, const bool *loaded, const ToolUberOpT **ordered, size_t count);
static void usageFail(const char *problem, const char *arg) __attribute__((noreturn));
static int writeFail(const char *path);
// The command line: -h or --help, --ref, --out, and --repo (each value in
// the next argument or after an '=').
static void argsRead(int argc, char **argv, OptionsT *options) {
const char *value;
size_t p;
int a;
options->ref = REF_DEFAULT;
options->out = NULL;
options->repo = NULL;
for (a = 1; a < argc; a++) {
if (strcmp(argv[a], "-h") == 0 || strcmp(argv[a], "--help") == 0) {
helpPrint();
} else if (optionValue(argc, argv, &a, "--ref", &value)) {
for (p = 0; p < PORT_COUNT && strcmp(gPorts[p].key, value) != 0; p++) {
}
if (p == PORT_COUNT) {
fprintf(stderr, "%s: --ref takes ", TOOL_NAME);
portsPrint(stderr);
fprintf(stderr, ", not '%s'\n", value);
exit(USAGE_EXIT);
}
options->ref = (PortE)p;
} else if (optionValue(argc, argv, &a, "--out", &value)) {
options->out = value;
} else if (optionValue(argc, argv, &a, "--repo", &value)) {
options->repo = value;
} else {
usageFail("is not an option", argv[a]);
}
}
}
// A port's cell: its ops/sec and the percentage of the reference's, bold
// under 100%; "-" when it has no number for the op.
static void cellPrint(FILE *f, const ToolUberOpT *port, const ToolUberOpT *ref) {
const char *bold;
double pct;
if (port == NULL) {
fputs("-", f);
return;
}
if (ref == NULL || ref->opsPerSec == 0u) {
fprintf(f, "%" PRIu64, port->opsPerSec);
return;
}
pct = (double)port->opsPerSec / (double)ref->opsPerSec * PERCENT;
bold = pct < PERCENT ? "**" : "";
fprintf(f, "%s%" PRIu64 " (%.0f%%)%s", bold, port->opsPerSec, pct, bold);
}
static void helpPrint(void) {
fputs(USAGE_TEXT, stdout);
fputs("\nRender a cross-port UBER perf table in Markdown.\n\noptions:\n", stdout);
fputs(" -h, --help show this help message and exit\n", stdout);
fputs(" --ref NAME Reference port: ", stdout);
portsPrint(stdout);
printf(" (default: %s)\n", gPorts[REF_DEFAULT].key);
fputs(" --out FILE Write table to this file instead of stdout\n", stdout);
fputs(" --repo DIR Repo root (default: the one this tool was built from)\n", stdout);
exit(HELP_EXIT);
}
// argv[*a] is the option name with its value after an '=' or in the next
// argument (*a then moves past it): its value; false when it is not that
// option.
static bool optionValue(int argc, char **argv, int *a, const char *name, const char **value) {
size_t len;
len = strlen(name);
if (strncmp(argv[*a], name, len) != 0) {
return false;
}
if (argv[*a][len] == '=') {
*value = argv[*a] + len + 1;
return true;
}
if (argv[*a][len] != '\0') {
return false;
}
if (*a + 1 >= argc) {
usageFail("needs a value", argv[*a]);
}
*a += 1;
*value = argv[*a];
return true;
}
// The ports' keys: "iigs, amiga, atarist, or dos".
static void portsPrint(FILE *f) {
size_t p;
for (p = 0; p < PORT_COUNT; p++) {
fprintf(f, "%s%s", p == 0u ? "" : (p + 1u == PORT_COUNT ? ", or " : ", "), gPorts[p].key);
}
}
static void tablePrint(FILE *f, PortE ref, const ToolUberLogT *logs, const bool *loaded, const ToolUberOpT **ordered, size_t count) {
const ToolUberOpT *refOp;
const ToolUberOpT *op;
const char *key;
size_t p;
size_t i;
fputs("| Op", f);
for (p = 0; p < PORT_COUNT; p++) {
fprintf(f, " | %s%s (ops/sec", gPorts[p].label, loaded[p] ? "" : " (no log)");
if (p != ref) {
fputs(", % of ", f);
for (key = gPorts[ref].key; *key != '\0'; key++) {
fputc(toupper((unsigned char)*key), f);
}
}
fputc(')', f);
}
fputs(" |\n| ---", f);
for (p = 0; p < PORT_COUNT; p++) {
fputs(" | ---", f);
}
fputs(" |\n", f);
for (i = 0; i < count; i++) {
fputs("| ", f);
toolUberNamePrint(f, ordered[i]);
refOp = toolUberFind(&logs[ref], ordered[i]->name, ordered[i]->nameLen);
for (p = 0; p < PORT_COUNT; p++) {
op = toolUberFind(&logs[p], ordered[i]->name, ordered[i]->nameLen);
fputs(" | ", f);
if (p != ref) {
cellPrint(f, op, refOp);
} else if (op != NULL) {
fprintf(f, "%" PRIu64, op->opsPerSec);
} else {
fputs("pending", f);
}
}
fputs(" |\n", f);
}
if (ref == CEILING_PORT) {
fputs(CEILING_NOTE, f);
fputc('\n', f);
}
}
// A command-line argument that cannot be taken: say why, then the usage.
static void usageFail(const char *problem, const char *arg) {
fprintf(stderr, "%s: '%s' %s\n", TOOL_NAME, arg, problem);
fputs(USAGE_TEXT, stderr);
exit(USAGE_EXIT);
}
// The --out file cannot be written.
static int writeFail(const char *path) {
fprintf(stderr, "%s: cannot write %s: %s\n", TOOL_NAME, path, strerror(errno));
return WRITE_EXIT;
}
int main(int argc, char **argv) {
OptionsT options;
ToolUberLogT logs[PORT_COUNT];
bool loaded[PORT_COUNT];
char paths[PORT_COUNT][PATH_BYTES];
char builtFrom[PATH_BYTES];
const ToolUberOpT **ordered;
const ToolUberOpT *op;
const char *repo;
char *slash;
FILE *f;
size_t total;
size_t count;
size_t known;
size_t p;
size_t i;
toolFileName(TOOL_NAME);
argsRead(argc, argv, &options);
repo = options.repo;
if (repo == NULL) {
snprintf(builtFrom, sizeof(builtFrom), "%s", __FILE__);
for (i = 0; i < SOURCE_DEPTH; i++) {
slash = strrchr(builtFrom, '/');
if (slash == NULL) {
toolFail("cannot find the repo from %s; give --repo", __FILE__);
}
*slash = '\0';
}
repo = builtFrom;
}
total = 0u;
for (p = 0; p < PORT_COUNT; p++) {
if (snprintf(paths[p], PATH_BYTES, "%s/%s", repo, gPorts[p].log) >= (int)PATH_BYTES) {
toolFail("--repo %s is too long", repo);
}
loaded[p] = toolUberLoad(paths[p], TOOL_UBER_PERF, &logs[p]);
total += logs[p].count;
}
if (!loaded[options.ref]) {
fprintf(stderr, "%s: reference log missing: %s (run UBER on %s first, then retry)\n", TOOL_NAME, paths[options.ref], gPorts[options.ref].key);
return USAGE_EXIT;
}
// The reference's ops in its order, then those it lacks, in the order
// the ports first give them.
ordered = (const ToolUberOpT **)calloc(total + 1u, sizeof(const ToolUberOpT *));
if (ordered == NULL) {
toolFail("out of memory");
}
count = 0u;
for (i = 0; i < logs[options.ref].count; i++) {
ordered[count++] = &logs[options.ref].ops[i];
}
for (p = 0; p < PORT_COUNT; p++) {
for (i = 0; i < logs[p].count; i++) {
op = &logs[p].ops[i];
for (known = 0u; known < count && (ordered[known]->nameLen != op->nameLen || memcmp(ordered[known]->name, op->name, op->nameLen) != 0); known++) {
}
if (known == count) {
ordered[count++] = op;
}
}
}
if (count == 0u) {
fprintf(stderr, "%s: no UBER ops in any log\n", TOOL_NAME);
return USAGE_EXIT;
}
f = stdout;
if (options.out != NULL) {
f = fopen(options.out, "w");
if (f == NULL) {
return writeFail(options.out);
}
}
tablePrint(f, options.ref, logs, loaded, ordered, count);
if (f != stdout && fclose(f) != 0) {
return writeFail(options.out);
}
return OK_EXIT;
}