354 lines
12 KiB
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;
|
|
}
|