// 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-.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 #include #include #include #include #include #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; }