joeylib2/tools/diffUberPerf/diffUberPerf.c

251 lines
8.7 KiB
C

// 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] <reference-log> <test-log>
//
// 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 <inttypes.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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] <reference-log> <test-log>\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;
}