joeylib2/tools/common/toolUberLog.c

289 lines
8.3 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// toolUberLog.c - UBER's log for the JoeyLib host tools (toolUberLog.h).
#define _DEFAULT_SOURCE
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include "toolFile.h"
#include "toolUberLog.h"
#define TIMED_PREFIX "UBER:"
#define CHECK_PREFIX "UBER-CHK:"
#define CHECK_NAME "chk:"
// What follows an op's colon, as a pattern: PAT_SPACE is a run of
// whitespace, PAT_DIGITS a run of digits, PAT_OPS the run of digits that is
// the op's ops/sec, PAT_HASH the run of hex digits that is its hash, and
// any other character itself.
#define PAT_SPACE ' '
#define PAT_DIGITS '#'
#define PAT_OPS '@'
#define PAT_HASH '%'
#define TIMED_PATTERN " # iters / # frames = @ ops/sec"
#define HASH_PATTERN TIMED_PATTERN " | hash=%"
#define CHECK_PATTERN " hash=%"
// The op is a whitespace character and at least one more before its colon.
#define OP_MIN_BYTES 2
#define DECIMAL 10u
// len bytes at text, not NUL-terminated.
typedef struct SpanT {
const char *text;
size_t len;
} SpanT;
// Where a line's op, ops/sec, and hash are.
typedef struct MatchT {
SpanT op;
SpanT opsPerSec;
SpanT hash;
} MatchT;
static uint64_t decimalValue(SpanT digits);
static void hashSet(ToolUberOpT *op, SpanT hash);
static bool matchAt(SpanT line, size_t at, const char *pattern, MatchT *m);
static bool matchLine(SpanT line, const char *prefix, const char *pattern, MatchT *m);
static size_t matchRun(SpanT line, size_t at, int (*isWanted)(int));
static size_t opIndex(const ToolUberLogT *log, const char *name, size_t nameLen);
static ToolUberOpT *opSet(ToolUberLogT *log, const char *prefix, SpanT name);
// A run of digits' value, held at UINT64_MAX past it.
static uint64_t decimalValue(SpanT digits) {
uint64_t value;
uint64_t digit;
size_t i;
value = 0u;
for (i = 0; i < digits.len; i++) {
digit = (uint64_t)(digits.text[i] - '0');
if (value > (UINT64_MAX - digit) / DECIMAL) {
return UINT64_MAX;
}
value = value * DECIMAL + digit;
}
return value;
}
// An op's hash, upper case, in place of any it had.
static void hashSet(ToolUberOpT *op, SpanT hash) {
size_t i;
free(op->hash);
op->hash = (char *)malloc(hash.len + 1u);
if (op->hash == NULL) {
toolFail("out of memory");
}
for (i = 0; i < hash.len; i++) {
op->hash[i] = (char)toupper((unsigned char)hash.text[i]);
}
op->hash[hash.len] = '\0';
}
// The line from at (just past a prefix) as an op, its colon, and pattern.
static bool matchAt(SpanT line, size_t at, const char *pattern, MatchT *m) {
const char *colon;
const char *p;
size_t run;
colon = (const char *)memchr(line.text + at, ':', line.len - at);
if (colon == NULL || colon - (line.text + at) < OP_MIN_BYTES || !isspace((unsigned char)line.text[at])) {
return false;
}
m->op.text = line.text + at;
m->op.len = (size_t)(colon - m->op.text);
while (m->op.len > 0u && isspace((unsigned char)m->op.text[0])) {
m->op.text++;
m->op.len--;
}
while (m->op.len > 0u && isspace((unsigned char)m->op.text[m->op.len - 1u])) {
m->op.len--;
}
at = (size_t)(colon - line.text) + 1u;
for (p = pattern; *p != '\0'; p++) {
switch (*p) {
case PAT_SPACE:
run = matchRun(line, at, isspace);
break;
case PAT_DIGITS:
case PAT_OPS:
run = matchRun(line, at, isdigit);
break;
case PAT_HASH:
run = matchRun(line, at, isxdigit);
break;
default:
run = (at < line.len && line.text[at] == *p) ? 1u : 0u;
break;
}
if (run == 0u) {
return false;
}
if (*p == PAT_OPS) {
m->opsPerSec.text = line.text + at;
m->opsPerSec.len = run;
} else if (*p == PAT_HASH) {
m->hash.text = line.text + at;
m->hash.len = run;
}
at += run;
}
return true;
}
// The first place in the line where prefix, an op, and pattern fit.
static bool matchLine(SpanT line, const char *prefix, const char *pattern, MatchT *m) {
size_t prefixLen;
size_t at;
prefixLen = strlen(prefix);
for (at = 0; at + prefixLen <= line.len; at++) {
if (memcmp(line.text + at, prefix, prefixLen) == 0 && matchAt(line, at + prefixLen, pattern, m)) {
return true;
}
}
return false;
}
// The bytes of the run of characters isWanted takes from at.
static size_t matchRun(SpanT line, size_t at, int (*isWanted)(int)) {
size_t run;
run = 0u;
while (at + run < line.len && isWanted((unsigned char)line.text[at + run])) {
run++;
}
return run;
}
// The index of the op of that name, or the count when there is none.
static size_t opIndex(const ToolUberLogT *log, const char *name, size_t nameLen) {
size_t i;
for (i = 0; i < log->count; i++) {
if (log->ops[i].nameLen == nameLen && memcmp(log->ops[i].name, name, nameLen) == 0) {
break;
}
}
return i;
}
// The op named prefix and name: the one the log has, or a new one after
// the rest.
static ToolUberOpT *opSet(ToolUberLogT *log, const char *prefix, SpanT name) {
ToolUberOpT *op;
char *full;
size_t prefixLen;
size_t len;
size_t index;
prefixLen = strlen(prefix);
len = prefixLen + name.len;
full = (char *)malloc(len + 1u);
if (full == NULL) {
toolFail("out of memory");
}
memcpy(full, prefix, prefixLen);
memcpy(full + prefixLen, name.text, name.len);
full[len] = '\0';
index = opIndex(log, full, len);
if (index < log->count) {
free(full);
return &log->ops[index];
}
if (log->count == log->cap) {
log->cap = log->cap * 2u + 1u;
log->ops = (ToolUberOpT *)realloc(log->ops, log->cap * sizeof(ToolUberOpT));
if (log->ops == NULL) {
toolFail("out of memory");
}
}
op = &log->ops[log->count++];
memset(op, 0, sizeof(*op));
op->name = full;
op->nameLen = len;
return op;
}
const ToolUberOpT *toolUberFind(const ToolUberLogT *log, const char *name, size_t nameLen) {
size_t index;
index = opIndex(log, name, nameLen);
return index < log->count ? &log->ops[index] : NULL;
}
bool toolUberLoad(const char *path, ToolUberReadE what, ToolUberLogT *log) {
ToolUberOpT *op;
MatchT m;
SpanT line;
FILE *f;
char *text;
size_t cap;
ssize_t got;
bool ok;
int err;
memset(log, 0, sizeof(*log));
f = fopen(path, "rb");
if (f == NULL) {
return false;
}
text = NULL;
cap = 0u;
while ((got = getline(&text, &cap, f)) >= 0) {
line.text = text;
line.len = (size_t)got;
if (line.len > 0u && line.text[line.len - 1u] == '\n') {
line.len--;
}
if (line.len > 0u && line.text[line.len - 1u] == '\r') {
line.len--;
}
if (what == TOOL_UBER_PERF) {
if (matchLine(line, TIMED_PREFIX, TIMED_PATTERN, &m)) {
op = opSet(log, "", m.op);
op->opsPerSec = decimalValue(m.opsPerSec);
}
} else if (matchLine(line, TIMED_PREFIX, HASH_PATTERN, &m)) {
hashSet(opSet(log, "", m.op), m.hash);
} else if (matchLine(line, CHECK_PREFIX, CHECK_PATTERN, &m)) {
hashSet(opSet(log, CHECK_NAME, m.op), m.hash);
}
}
err = errno;
ok = !ferror(f);
free(text);
fclose(f);
errno = err;
return ok;
}
void toolUberNamePrint(FILE *f, const ToolUberOpT *op) {
fwrite(op->name, 1u, op->nameLen, f);
}
bool toolUberRead(const char *toolName, const char *path, ToolUberReadE what, ToolUberLogT *log) {
if (toolUberLoad(path, what, log)) {
return true;
}
fprintf(stderr, "%s: cannot read %s: %s\n", toolName, path, strerror(errno));
return false;
}