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