// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // diffUberPerf.c - compare two UBER logs (joeylog.txt) op by op, by speed // (ops/sec). // // diffUberPerf [--threshold N] // // diffUberHashes' sibling: that one compares pixels, this one finds the // biggest speed gaps against the reference (the IIgs, the perf floor every // other port must match or beat), to aim asm and algorithm work at them. // // Prints a table of every op, ref and test ops/sec and their ratio // (test/ref), sorted so the worst gaps come first: ops MISSING from the // test log, then those below the threshold (FAIL), then the rest (ok), each // by ratio and then name; ops only the test log has follow as EXTRA. The // threshold (default 1.0, parity; --threshold N or --threshold=N, anywhere // on the line) is the ratio an op must reach; 0.8 is the looser acceptance // of a port's first planar pass. An op the reference ran at 0 ops/sec has // the ratio inf when the test ran it at all, else 1. Values are those of // each log's last run (toolUberLog.h). // // Exit: 0 every op at or above the threshold, 1 at least one below it or // missing, 2 a usage error, a bad threshold, or a log that cannot be read // or has no UBER lines. #include #include #include #include #include #include "toolFile.h" #include "toolUberLog.h" #define TOOL_NAME "diffUberPerf" #define THRESHOLD_FLAG "--threshold" #define THRESHOLD_DEFAULT 1.0 #define LOG_COUNT 2u #define PASS_EXIT 0 #define BELOW_EXIT 1 #define USAGE_EXIT 2 // The table's columns: the op (as wide as the widest name, at least the // heading), then ref, test, and ratio right-aligned. #define OP_HEADING "op" #define NUMBER_WIDTH 10 #define RATIO_WIDTH 7 #define STATUS_RULE "------" #define COLUMN_GAP " " #define EXTRA_NAME "EXTRA" // Room for an ops/sec count or a ratio (at most 2^64 to two places) as text. #define TEXT_BYTES 32u // A row's standing, in the order the table sorts them. typedef enum StatusE { STATUS_MISSING = 0, STATUS_FAIL, STATUS_OK } StatusE; // A reference op's row: the test log's op (NULL when it has none) and the // ratio (0 for a missing op). typedef struct RowT { const ToolUberOpT *ref; const ToolUberOpT *test; double ratio; StatusE status; } RowT; static const char *const gStatusNames[] = {"MISSING", "FAIL", "ok"}; static int nameCompare(const ToolUberOpT *a, const ToolUberOpT *b); static size_t nameWidest(const ToolUberLogT *log, size_t width); static int rowCompare(const void *a, const void *b); static void rowPrint(const ToolUberOpT *op, const char *refText, const char *testText, const char *ratioText, const char *status, size_t opWidth); static void rulePrint(size_t width); static int usage(void); // Op names in byte order, a name before the longer ones it starts. static int nameCompare(const ToolUberOpT *a, const ToolUberOpT *b) { int order; order = memcmp(a->name, b->name, a->nameLen < b->nameLen ? a->nameLen : b->nameLen); if (order != 0) { return order; } return (a->nameLen > b->nameLen) - (a->nameLen < b->nameLen); } // The widest of width and the log's op names. static size_t nameWidest(const ToolUberLogT *log, size_t width) { size_t i; for (i = 0; i < log->count; i++) { width = log->ops[i].nameLen > width ? log->ops[i].nameLen : width; } return width; } // The table's order: by standing, then ratio, then name. static int rowCompare(const void *a, const void *b) { const RowT *x; const RowT *y; x = (const RowT *)a; y = (const RowT *)b; if (x->status != y->status) { return x->status < y->status ? -1 : 1; } if (x->ratio != y->ratio) { return x->ratio < y->ratio ? -1 : 1; } return nameCompare(x->ref, y->ref); } static void rowPrint(const ToolUberOpT *op, const char *refText, const char *testText, const char *ratioText, const char *status, size_t opWidth) { toolUberNamePrint(stdout, op); printf("%*s" COLUMN_GAP "%*s" COLUMN_GAP "%*s" COLUMN_GAP "%*s" COLUMN_GAP "%s\n", (int)(opWidth - op->nameLen), "", NUMBER_WIDTH, refText, NUMBER_WIDTH, testText, RATIO_WIDTH, ratioText, status); } // A column's rule: width dashes, then the gap to the next column. static void rulePrint(size_t width) { size_t i; for (i = 0; i < width; i++) { putchar('-'); } fputs(COLUMN_GAP, stdout); } static int usage(void) { fprintf(stderr, "usage: %s [%s N] \n", TOOL_NAME, THRESHOLD_FLAG); return USAGE_EXIT; } int main(int argc, char **argv) { ToolUberLogT ref; ToolUberLogT test; RowT *rows; const char *logs[LOG_COUNT]; const char *value; char *end; char refText[TEXT_BYTES]; char testText[TEXT_BYTES]; char ratioText[TEXT_BYTES]; double threshold; size_t logCount; size_t flagLen; size_t opWidth; size_t fails; size_t i; int a; toolFileName(TOOL_NAME); threshold = THRESHOLD_DEFAULT; logCount = 0u; flagLen = strlen(THRESHOLD_FLAG); for (a = 1; a < argc; a++) { if (strncmp(argv[a], THRESHOLD_FLAG, flagLen) == 0 && (argv[a][flagLen] == '\0' || argv[a][flagLen] == '=')) { if (argv[a][flagLen] == '=') { value = argv[a] + flagLen + 1; } else if (a + 1 < argc) { value = argv[++a]; } else { return usage(); } threshold = strtod(value, &end); if (end == value || *end != '\0') { fprintf(stderr, "%s: bad threshold '%s'\n", TOOL_NAME, value); return USAGE_EXIT; } } else if (argv[a][0] == '-' || logCount == LOG_COUNT) { return usage(); } else { logs[logCount++] = argv[a]; } } if (logCount != LOG_COUNT) { return usage(); } if (!toolUberRead(TOOL_NAME, logs[0], TOOL_UBER_PERF, &ref) || !toolUberRead(TOOL_NAME, logs[1], TOOL_UBER_PERF, &test)) { return USAGE_EXIT; } if (ref.count == 0u) { fprintf(stderr, "%s: no UBER lines in %s\n", TOOL_NAME, logs[0]); return USAGE_EXIT; } if (test.count == 0u) { fprintf(stderr, "%s: no UBER lines in %s\n", TOOL_NAME, logs[1]); return USAGE_EXIT; } rows = (RowT *)calloc(ref.count, sizeof(RowT)); if (rows == NULL) { toolFail("out of memory"); } for (i = 0; i < ref.count; i++) { rows[i].ref = &ref.ops[i]; rows[i].test = toolUberFind(&test, ref.ops[i].name, ref.ops[i].nameLen); if (rows[i].test == NULL) { rows[i].status = STATUS_MISSING; continue; } if (ref.ops[i].opsPerSec == 0u) { rows[i].ratio = rows[i].test->opsPerSec > 0u ? INFINITY : 1.0; } else { rows[i].ratio = (double)rows[i].test->opsPerSec / (double)ref.ops[i].opsPerSec; } rows[i].status = rows[i].ratio >= threshold ? STATUS_OK : STATUS_FAIL; } qsort(rows, ref.count, sizeof(RowT), rowCompare); opWidth = nameWidest(&test, nameWidest(&ref, strlen(OP_HEADING))); printf("%-*s" COLUMN_GAP "%*s" COLUMN_GAP "%*s" COLUMN_GAP "%*s" COLUMN_GAP "status\n", (int)opWidth, OP_HEADING, NUMBER_WIDTH, "ref", NUMBER_WIDTH, "test", RATIO_WIDTH, "ratio"); rulePrint(opWidth); rulePrint(NUMBER_WIDTH); rulePrint(NUMBER_WIDTH); rulePrint(RATIO_WIDTH); puts(STATUS_RULE); fails = 0u; for (i = 0; i < ref.count; i++) { snprintf(refText, sizeof(refText), "%" PRIu64, rows[i].ref->opsPerSec); testText[0] = '\0'; ratioText[0] = '\0'; if (rows[i].test != NULL) { snprintf(testText, sizeof(testText), "%" PRIu64, rows[i].test->opsPerSec); snprintf(ratioText, sizeof(ratioText), "%.2fx", rows[i].ratio); } rowPrint(rows[i].ref, refText, testText, ratioText, gStatusNames[rows[i].status], opWidth); if (rows[i].status != STATUS_OK) { fails++; } } for (i = 0; i < test.count; i++) { if (toolUberFind(&ref, test.ops[i].name, test.ops[i].nameLen) == NULL) { snprintf(testText, sizeof(testText), "%" PRIu64, test.ops[i].opsPerSec); rowPrint(&test.ops[i], "", testText, "", EXTRA_NAME, opWidth); } } printf("\nthreshold: %.2fx (%zu ops compared, %zu below threshold)\n", threshold, ref.count, fails); return fails > 0u ? BELOW_EXIT : PASS_EXIT; }