diff --git a/logging/hiperiso-log/Makefile b/logging/hiperiso-log/Makefile new file mode 100644 index 0000000..9006c7a --- /dev/null +++ b/logging/hiperiso-log/Makefile @@ -0,0 +1,16 @@ +CC = gcc +CFLAGS = -Wall -Wextra -O2 -std=c11 +LDFLAGS = -static + +OBJS = main.o trace_parser.o serial_parser.o report.o + +hiperiso-log: $(OBJS) + $(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS) + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJS) hiperiso-log + +.PHONY: clean diff --git a/logging/hiperiso-log/hiperiso-log b/logging/hiperiso-log/hiperiso-log new file mode 100755 index 0000000..99baea5 Binary files /dev/null and b/logging/hiperiso-log/hiperiso-log differ diff --git a/logging/hiperiso-log/main.c b/logging/hiperiso-log/main.c new file mode 100644 index 0000000..48f2fa6 --- /dev/null +++ b/logging/hiperiso-log/main.c @@ -0,0 +1,119 @@ +/* + * main.c - CLI entry point for hiperiso-log. + * + * Usage: + * hiperiso-log analyze + * hiperiso-log trace [--format json|csv|text] + * hiperiso-log serial [--stages] + */ + +#include "trace_parser.h" +#include "serial_parser.h" +#include "report.h" + +#include +#include +#include + +static void usage(const char *prog) +{ + fprintf(stderr, "Usage: %s analyze \n", prog); + fprintf(stderr, " %s trace [--format json|csv|text]\n", prog); + fprintf(stderr, " %s serial [--stages]\n", prog); +} + +static int cmd_analyze(const char *log_dir) +{ + if (report_generate(log_dir) != 0) { + fprintf(stderr, "Error: failed to generate report for '%s'\n", log_dir); + return 1; + } + + printf("Report generated successfully in %s/\n", log_dir); + printf(" report.json - machine-readable analysis\n"); + printf(" report.txt - human-readable summary\n"); + return 0; +} + +static int cmd_trace(const char *trace_file, const char *format) +{ + trace_summary_t summary; + if (trace_parse_file(trace_file, &summary) != 0) { + fprintf(stderr, "Error: failed to parse trace file '%s'\n", trace_file); + return 1; + } + + if (strcmp(format, "json") == 0) + trace_print_json(&summary, stdout); + else if (strcmp(format, "csv") == 0) + trace_print_csv(&summary, stdout); + else + trace_print_text(&summary, stdout); + + trace_free_summary(&summary); + return 0; +} + +static int cmd_serial(const char *serial_file, int show_stages) +{ + serial_summary_t summary; + if (serial_parse_file(serial_file, &summary) != 0) { + fprintf(stderr, "Error: failed to parse serial log '%s'\n", serial_file); + return 1; + } + + if (show_stages) + serial_print_stages(&summary, stdout); + else + serial_print_text(&summary, stdout); + + serial_free_summary(&summary); + return 0; +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + usage(argv[0]); + return 1; + } + + if (strcmp(argv[1], "analyze") == 0) { + if (argc < 3) { + usage(argv[0]); + return 1; + } + return cmd_analyze(argv[2]); + } + + if (strcmp(argv[1], "trace") == 0) { + if (argc < 3) { + usage(argv[0]); + return 1; + } + const char *format = "text"; + for (int i = 3; i < argc; i++) { + if (strcmp(argv[i], "--format") == 0 && i + 1 < argc) { + format = argv[i + 1]; + i++; + } + } + return cmd_trace(argv[2], format); + } + + if (strcmp(argv[1], "serial") == 0) { + if (argc < 3) { + usage(argv[0]); + return 1; + } + int show_stages = 0; + for (int i = 3; i < argc; i++) { + if (strcmp(argv[i], "--stages") == 0) + show_stages = 1; + } + return cmd_serial(argv[2], show_stages); + } + + usage(argv[0]); + return 1; +} diff --git a/logging/hiperiso-log/report.c b/logging/hiperiso-log/report.c new file mode 100644 index 0000000..b56d01a --- /dev/null +++ b/logging/hiperiso-log/report.c @@ -0,0 +1,636 @@ +/* + * report.c - Generate analysis reports from boot logs. + * + * Reads serial.log, trace.bin, qemu.cmdline, and env.txt from a log + * directory, then writes report.json and report.txt into the same directory. + * + * Also derives a structured "analysis" (boot result, failure domain, graphics + * hints) from the serial console and writes a flat analysis.meta sidecar + * (KEY=VALUE) so the initramfs can fold derived fields into the canonical + * session manifest without a JSON parser. + */ + +#include "report.h" +#include "trace_parser.h" +#include "serial_parser.h" + +#include +#include +#include +#include + +#define MAX_PATH 4096 +#define MAX_CMDLINE 4096 +#define MAX_ENV_ENTRIES 64 +#define MAX_ENV_KEY 128 +#define MAX_ENV_VALUE 512 +#define MAX_LINEBUF (MAX_ENV_KEY + MAX_ENV_VALUE + 64) + +typedef struct { + char key[MAX_ENV_KEY]; + char value[MAX_ENV_VALUE]; +} env_entry_t; + +/* ── Derived analysis (Redbear telemetry) ───────────────────── */ +typedef struct { + const char *boot_result; /* "success" | "failure" | "incomplete" | "no_serial" */ + const char *failure_domain; /* "none" | "firmware" | "bootloader" | "kernel" | + * "hardware" | "userspace" | "unknown" */ + int reached_login; /* saw login/complete stage */ + int kernel_panic_count; /* error lines containing "panic" */ + int graphics_drm_seen; /* any DRM/KMS line observed */ + char graphics_drivers[256]; /* comma-separated unique driver names found */ + char graphics_resolution[64]; /* first WxH token found, or "" */ +} analysis_t; + +static void trim_trailing_nl(char *s) +{ + size_t len = strlen(s); + while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r')) + s[--len] = '\0'; +} + +static int read_env_file(const char *path, env_entry_t *entries, + int max_entries, int *count) +{ + FILE *f = fopen(path, "r"); + if (!f) + return -1; + + *count = 0; + char buf[MAX_LINEBUF]; + + while (*count < max_entries && fgets(buf, sizeof(buf), f)) { + char *colon = strchr(buf, ':'); + if (!colon) + continue; + *colon = '\0'; + + char *value = colon + 1; + while (*value == ' ' || *value == '\t') + value++; + trim_trailing_nl(value); + + if (strlen(buf) == 0) + continue; + + strncpy(entries[*count].key, buf, MAX_ENV_KEY - 1); + entries[*count].key[MAX_ENV_KEY - 1] = '\0'; + strncpy(entries[*count].value, value, MAX_ENV_VALUE - 1); + entries[*count].value[MAX_ENV_VALUE - 1] = '\0'; + (*count)++; + } + + fclose(f); + return 0; +} + +static int read_cmdline(const char *path, char *cmdline, size_t maxlen) +{ + cmdline[0] = '\0'; + + FILE *f = fopen(path, "r"); + if (!f) + return -1; + + if (!fgets(cmdline, (int)maxlen, f)) { + fclose(f); + return -1; + } + + fclose(f); + trim_trailing_nl(cmdline); + return 0; +} + +static void json_escape(FILE *out, const char *str) +{ + fputc('"', out); + while (*str) { + unsigned char ch = (unsigned char)*str; + switch (ch) { + case '"': fputs("\\\"", out); break; + case '\\': fputs("\\\\", out); break; + case '\n': fputs("\\n", out); break; + case '\r': fputs("\\r", out); break; + case '\t': fputs("\\t", out); break; + default: + if (ch < 0x20) + fprintf(out, "\\u%04x", ch); + else + fputc((int)ch, out); + } + str++; + } + fputc('"', out); +} + +static void build_path(char *dst, size_t dstsz, + const char *dir, const char *file) +{ + snprintf(dst, dstsz, "%s/%s", dir, file); +} + +/* ── Analysis derivation ────────────────────────────────────── */ + +static const char *stage_to_domain(boot_stage_t s) +{ + switch (s) { + case STAGE_FIRMWARE: return "firmware"; + case STAGE_BOOTLOADER: return "bootloader"; + case STAGE_KERNEL_INIT: return "kernel"; + case STAGE_HARDWARE_INIT: return "hardware"; + case STAGE_USERSPACE: return "userspace"; + default: return "unknown"; + } +} + +/* Scan a single text line for a WxH resolution token (e.g. "1024x768"). + * Returns 1 and fills out[] on success, 0 if none found. */ +static int extract_resolution(const char *s, char *out, size_t outsz) +{ + for (const char *p = s; *p; p++) { + if (*p < '0' || *p > '9') + continue; + const char *wstart = p; + while (*p >= '0' && *p <= '9') + p++; + int wlen = (int)(p - wstart); + if (wlen < 3 || wlen > 4) + continue; + if (*p != 'x' && *p != 'X') + continue; + p++; + const char *hstart = p; + while (*p >= '0' && *p <= '9') + p++; + int hlen = (int)(p - hstart); + if (hlen < 3 || hlen > 4) + continue; + snprintf(out, outsz, "%.*sx%.*s", wlen, wstart, hlen, hstart); + return 1; + } + return 0; +} + +/* GPU driver names we can detect from serial console text. These are + * matched as substrings; only names literally present are reported. */ +static const char *known_gpu_drivers[] = { + "i915", "amdgpu", "radeon", "nouveau", "nvidia", + "virtio_gpu", "virtio-gpu", "cirrus", "vmwgfx", + "mgag200", "simpledrm", "ast", "panfrost", NULL +}; + +static void scan_graphics(const serial_summary_t *s, analysis_t *a) +{ + a->graphics_drivers[0] = '\0'; + a->graphics_resolution[0] = '\0'; + a->graphics_drm_seen = 0; + + int first = 1; + for (serial_line_t *sl = s->lines; sl; sl = sl->next) { + const char *t = sl->text; + if (!t || !*t) + continue; + + if (strstr(t, "[drm]") || strstr(t, "[DRM]") || + strstr(t, "drm_kms_helper") || strstr(t, "DRM:") || + strstr(t, "fb:")) + a->graphics_drm_seen = 1; + + for (int i = 0; known_gpu_drivers[i]; i++) { + if (strstr(t, known_gpu_drivers[i]) && + !strstr(a->graphics_drivers, known_gpu_drivers[i])) { + if (!first) + strncat(a->graphics_drivers, ",", + sizeof(a->graphics_drivers) - + strlen(a->graphics_drivers) - 1); + strncat(a->graphics_drivers, known_gpu_drivers[i], + sizeof(a->graphics_drivers) - + strlen(a->graphics_drivers) - 1); + first = 0; + } + } + + if (a->graphics_resolution[0] == '\0') + extract_resolution(t, a->graphics_resolution, + sizeof(a->graphics_resolution)); + } +} + +static void derive_analysis(const serial_summary_t *s, int has_serial, + analysis_t *a) +{ + memset(a, 0, sizeof(*a)); + a->boot_result = "no_serial"; + a->failure_domain = "unknown"; + a->kernel_panic_count = 0; + + if (!has_serial || !s) + return; + + for (int i = 0; i < s->error_count; i++) { + if (strstr(s->error_messages[i], "panic") || + strstr(s->error_messages[i], "Panic")) + a->kernel_panic_count++; + } + + a->reached_login = (s->lines_per_stage[STAGE_LOGIN] > 0 || + s->lines_per_stage[STAGE_COMPLETE] > 0); + + /* Highest stage with observed lines (excluding error/unknown). */ + boot_stage_t progressed = STAGE_UNKNOWN; + for (int i = STAGE_FIRMWARE; i < STAGE_ERROR; i++) { + if (s->lines_per_stage[i] > 0) + progressed = (boot_stage_t)i; + } + + if (a->kernel_panic_count > 0) { + a->boot_result = "failure"; + a->failure_domain = (progressed != STAGE_UNKNOWN) + ? stage_to_domain(progressed) : "unknown"; + } else if (a->reached_login) { + a->boot_result = (s->error_count > 0) ? "success_with_warnings" : "success"; + a->failure_domain = "none"; + } else if (s->error_count > 0) { + a->boot_result = "failure"; + a->failure_domain = (progressed != STAGE_UNKNOWN) + ? stage_to_domain(progressed) : "unknown"; + } else { + a->boot_result = "incomplete"; + a->failure_domain = (progressed != STAGE_UNKNOWN) + ? stage_to_domain(progressed) : "unknown"; + } + + scan_graphics(s, a); +} + +/* ── analysis.meta flat sidecar (consumed by initramfs init) ── */ + +static int write_analysis_meta(const char *path, const analysis_t *a, + const serial_summary_t *serial, int has_serial, + const trace_summary_t *trace, int has_trace) +{ + FILE *out = fopen(path, "w"); + if (!out) + return -1; + + const char *stage = "unknown"; + if (has_serial && serial->final_stage >= 0 && + serial->final_stage < STAGE_COUNT) + stage = boot_stage_names[serial->final_stage]; + + /* stages_reached: comma-separated list of stages that had activity */ + char stages_reached[384] = ""; + if (has_serial) { + int first = 1; + for (int i = STAGE_FIRMWARE; i <= STAGE_COMPLETE; i++) { + if (serial->lines_per_stage[i] > 0) { + if (!first) { + strncat(stages_reached, ",", + sizeof(stages_reached) - strlen(stages_reached) - 1); + } + strncat(stages_reached, boot_stage_names[i], + sizeof(stages_reached) - strlen(stages_reached) - 1); + first = 0; + } + } + } + + /* trace_duration_ms: from trace timestamps, already parsed */ + uint64_t trace_duration_ms = 0; + if (has_trace && trace->last_timestamp > trace->first_timestamp) + trace_duration_ms = (trace->last_timestamp - trace->first_timestamp) / 1000000ULL; + + fprintf(out, "BOOT_RESULT=%s\n", a->boot_result); + fprintf(out, "FAILURE_DOMAIN=%s\n", a->failure_domain); + fprintf(out, "BOOT_STAGE_FINAL=%s\n", stage); + fprintf(out, "STAGES_REACHED=%s\n", stages_reached); + fprintf(out, "REACHED_LOGIN=%d\n", a->reached_login ? 1 : 0); + fprintf(out, "KERNEL_PANIC_COUNT=%d\n", a->kernel_panic_count); + fprintf(out, "ERROR_COUNT=%d\n", has_serial ? serial->error_count : 0); + fprintf(out, "HAS_SERIAL=%d\n", has_serial ? 1 : 0); + fprintf(out, "HAS_TRACE=%d\n", has_trace ? 1 : 0); + fprintf(out, "BOOT_DURATION_MS=%" PRIu64 "\n", + has_serial ? serial->boot_duration_ms : (uint64_t)0); + fprintf(out, "TRACE_DURATION_MS=%" PRIu64 "\n", trace_duration_ms); + fprintf(out, "GRAPHICS_DRIVERS=%s\n", a->graphics_drivers); + fprintf(out, "GRAPHICS_RESOLUTION=%s\n", a->graphics_resolution); + fprintf(out, "GRAPHICS_DRM_SEEN=%d\n", a->graphics_drm_seen ? 1 : 0); + + fclose(out); + return 0; +} + +/* ── report.json ────────────────────────────────────────────── */ + +static int write_report_json(const char *path, + const env_entry_t *env, int env_count, + const char *cmdline, + const serial_summary_t *serial, int has_serial, + const trace_summary_t *trace, int has_trace, + const analysis_t *analysis) +{ + FILE *out = fopen(path, "w"); + if (!out) + return -1; + + fprintf(out, "{\n"); + + fprintf(out, " \"environment\": {"); + for (int i = 0; i < env_count; i++) { + if (i > 0) + fprintf(out, ","); + fprintf(out, "\n "); + json_escape(out, env[i].key); + fprintf(out, ": "); + json_escape(out, env[i].value); + } + fprintf(out, env_count > 0 ? "\n },\n" : "},\n"); + + fprintf(out, " \"qemu_cmdline\": "); + json_escape(out, cmdline); + fprintf(out, ",\n"); + + const char *stage = "unknown"; + if (has_serial) { + if (serial->final_stage >= 0 && serial->final_stage < STAGE_COUNT) + stage = boot_stage_names[serial->final_stage]; + } + fprintf(out, " \"boot_stage\": "); + json_escape(out, stage); + fprintf(out, ",\n"); + + fprintf(out, " \"boot_duration_ms\": %" PRIu64 ",\n", + has_serial ? serial->boot_duration_ms : (uint64_t)0); + + fprintf(out, " \"errors\": ["); + for (int i = 0; has_serial && i < serial->error_count; i++) { + if (i > 0) + fprintf(out, ", "); + json_escape(out, serial->error_messages[i]); + } + fprintf(out, "],\n"); + + if (has_serial) { + fprintf(out, " \"serial_summary\": {\n"); + fprintf(out, " \"total_lines\": %zu,\n", serial->total_lines); + for (int i = 0; i < STAGE_COUNT; i++) { + fprintf(out, " \"lines_%s\": %zu%s\n", + boot_stage_names[i], + serial->lines_per_stage[i], + i < STAGE_COUNT - 1 ? "," : ""); + } + fprintf(out, " },\n"); + } else { + fprintf(out, " \"serial_summary\": null,\n"); + } + + if (has_trace) { + uint64_t trace_dur_ms = 0; + if (trace->last_timestamp > trace->first_timestamp) + trace_dur_ms = (trace->last_timestamp - trace->first_timestamp) / 1000000ULL; + + fprintf(out, " \"trace_summary\": {\n"); + fprintf(out, " \"total_events\": %" PRIu64 ",\n", trace->total_events); + fprintf(out, " \"disk_reads\": %" PRIu64 ",\n", trace->total_disk_reads); + fprintf(out, " \"disk_writes\": %" PRIu64 ",\n", trace->total_disk_writes); + fprintf(out, " \"serial_writes\": %" PRIu64 ",\n", trace->total_serial_writes); + fprintf(out, " \"first_timestamp_ns\": %" PRIu64 ",\n", trace->first_timestamp); + fprintf(out, " \"last_timestamp_ns\": %" PRIu64 ",\n", trace->last_timestamp); + fprintf(out, " \"duration_ms\": %" PRIu64 "\n", trace_dur_ms); + fprintf(out, " },\n"); + } else { + fprintf(out, " \"trace_summary\": null,\n"); + } + + /* Derived analysis block (additive, always present). */ + fprintf(out, " \"analysis\": {\n"); + fprintf(out, " \"boot_result\": "); + json_escape(out, analysis->boot_result); + fprintf(out, ",\n"); + fprintf(out, " \"failure_domain\": "); + json_escape(out, analysis->failure_domain); + fprintf(out, ",\n"); + fprintf(out, " \"boot_stage_final\": "); + json_escape(out, stage); + fprintf(out, ",\n"); + fprintf(out, " \"stages_reached\": ["); + if (has_serial) { + int first_stg = 1; + for (int i = STAGE_FIRMWARE; i <= STAGE_COMPLETE; i++) { + if (serial->lines_per_stage[i] > 0) { + if (!first_stg) + fprintf(out, ", "); + json_escape(out, boot_stage_names[i]); + first_stg = 0; + } + } + } + fprintf(out, "],\n"); + fprintf(out, " \"reached_login\": %s,\n", + analysis->reached_login ? "true" : "false"); + fprintf(out, " \"kernel_panic_count\": %d,\n", + analysis->kernel_panic_count); + fprintf(out, " \"error_count\": %d,\n", + has_serial ? serial->error_count : 0); + fprintf(out, " \"has_serial\": %s,\n", has_serial ? "true" : "false"); + fprintf(out, " \"has_trace\": %s,\n", has_trace ? "true" : "false"); + fprintf(out, " \"boot_duration_ms\": %" PRIu64 ",\n", + has_serial ? serial->boot_duration_ms : (uint64_t)0); + { + uint64_t tdm = 0; + if (has_trace && trace->last_timestamp > trace->first_timestamp) + tdm = (trace->last_timestamp - trace->first_timestamp) / 1000000ULL; + fprintf(out, " \"trace_duration_ms\": %" PRIu64 ",\n", tdm); + } + fprintf(out, " \"graphics\": {\n"); + fprintf(out, " \"drm_seen\": %s,\n", + analysis->graphics_drm_seen ? "true" : "false"); + fprintf(out, " \"drivers\": ["); + { + const char *p = analysis->graphics_drivers; + int first_drv = 1; + while (*p) { + const char *comma = strchr(p, ','); + size_t len = comma ? (size_t)(comma - p) : strlen(p); + if (len > 0) { + if (!first_drv) + fprintf(out, ", "); + fprintf(out, "\"%.*s\"", (int)len, p); + first_drv = 0; + } + if (!comma) + break; + p = comma + 1; + } + } + fprintf(out, "],\n"); + fprintf(out, " \"resolution\": "); + json_escape(out, analysis->graphics_resolution); + fprintf(out, "\n"); + fprintf(out, " }\n"); + fprintf(out, " }\n"); + + fprintf(out, "}\n"); + fclose(out); + return 0; +} + +/* ── report.txt ─────────────────────────────────────────────── */ + +static int write_report_txt(const char *path, + const env_entry_t *env, int env_count, + const char *cmdline, + const serial_summary_t *serial, int has_serial, + const trace_summary_t *trace, int has_trace, + const analysis_t *analysis) +{ + FILE *out = fopen(path, "w"); + if (!out) + return -1; + + fprintf(out, "========================================\n"); + fprintf(out, " hiperiso Boot Analysis Report\n"); + fprintf(out, "========================================\n\n"); + + fprintf(out, "--- Environment ---\n"); + for (int i = 0; i < env_count; i++) + fprintf(out, " %-20s %s\n", env[i].key, env[i].value); + if (env_count == 0) + fprintf(out, " (not available)\n"); + fprintf(out, "\n"); + + fprintf(out, "--- QEMU Command Line ---\n"); + if (cmdline[0]) + fprintf(out, " %s\n", cmdline); + else + fprintf(out, " (not available)\n"); + fprintf(out, "\n"); + + if (has_serial) { + const char *stage = (serial->final_stage >= 0 && + serial->final_stage < STAGE_COUNT) + ? boot_stage_names[serial->final_stage] + : "unknown"; + + fprintf(out, "--- Serial Console Summary ---\n"); + fprintf(out, " Total lines: %zu\n", serial->total_lines); + fprintf(out, " Final boot stage: %s\n", stage); + fprintf(out, " Boot duration: %" PRIu64 " ms\n", + serial->boot_duration_ms); + fprintf(out, "\n Lines per stage:\n"); + for (int i = 0; i < STAGE_COUNT; i++) + fprintf(out, " %-20s %zu\n", + boot_stage_names[i], serial->lines_per_stage[i]); + + if (serial->error_count > 0) { + fprintf(out, "\n Errors (%d):\n", serial->error_count); + for (int i = 0; i < serial->error_count; i++) + fprintf(out, " %s\n", serial->error_messages[i]); + } + fprintf(out, "\n"); + } else { + fprintf(out, "--- Serial Console: NOT AVAILABLE ---\n\n"); + } + + if (has_trace) { + fprintf(out, "--- Trace Summary ---\n"); + trace_print_text(trace, out); + } else { + fprintf(out, "--- Trace: NOT AVAILABLE ---\n"); + } + + fprintf(out, "\n--- Derived Analysis ---\n"); + fprintf(out, " Boot result: %s\n", analysis->boot_result); + fprintf(out, " Failure domain: %s\n", analysis->failure_domain); + if (has_serial) { + fprintf(out, " Stage progression: "); + int first_stg = 1; + for (int i = STAGE_FIRMWARE; i <= STAGE_COMPLETE; i++) { + if (serial->lines_per_stage[i] > 0) { + fprintf(out, "%s%s", + first_stg ? "" : " \xe2\x86\x92 ", + boot_stage_names[i]); + first_stg = 0; + } + } + fprintf(out, "\n"); + } + fprintf(out, " Reached login: %s\n", + analysis->reached_login ? "yes" : "no"); + fprintf(out, " Kernel panics: %d\n", analysis->kernel_panic_count); + fprintf(out, " Error lines: %d\n", + has_serial ? serial->error_count : 0); + fprintf(out, " Graphics DRM seen: %s\n", + analysis->graphics_drm_seen ? "yes" : "no"); + if (analysis->graphics_drivers[0]) + fprintf(out, " Graphics drivers: %s\n", analysis->graphics_drivers); + if (analysis->graphics_resolution[0]) + fprintf(out, " Graphics res: %s\n", analysis->graphics_resolution); + + fprintf(out, "\n========================================\n"); + fprintf(out, " End of Report\n"); + fprintf(out, "========================================\n"); + + fclose(out); + return 0; +} + +int report_generate(const char *log_dir) +{ + if (!log_dir) + return -1; + + char path[MAX_PATH]; + env_entry_t env[MAX_ENV_ENTRIES]; + int env_count = 0; + char cmdline[MAX_CMDLINE]; + cmdline[0] = '\0'; + + build_path(path, sizeof(path), log_dir, "env.txt"); + read_env_file(path, env, MAX_ENV_ENTRIES, &env_count); + + build_path(path, sizeof(path), log_dir, "qemu.cmdline"); + read_cmdline(path, cmdline, sizeof(cmdline)); + + serial_summary_t serial; + memset(&serial, 0, sizeof(serial)); + build_path(path, sizeof(path), log_dir, "serial.log"); + int has_serial = (serial_parse_file(path, &serial) == 0); + + trace_summary_t trace; + memset(&trace, 0, sizeof(trace)); + build_path(path, sizeof(path), log_dir, "trace.bin"); + int has_trace = (trace_parse_file(path, &trace) == 0); + + analysis_t analysis; + derive_analysis(has_serial ? &serial : NULL, has_serial, &analysis); + + int rc = 0; + + build_path(path, sizeof(path), log_dir, "report.json"); + if (write_report_json(path, env, env_count, cmdline, + &serial, has_serial, &trace, has_trace, + &analysis) != 0) + rc = -1; + + build_path(path, sizeof(path), log_dir, "report.txt"); + if (write_report_txt(path, env, env_count, cmdline, + &serial, has_serial, &trace, has_trace, + &analysis) != 0) + rc = -1; + + /* Flat sidecar for the initramfs to fold into the session manifest. */ + build_path(path, sizeof(path), log_dir, "analysis.meta"); + if (write_analysis_meta(path, &analysis, &serial, has_serial, + &trace, has_trace) != 0) + rc = -1; + + if (has_serial) + serial_free_summary(&serial); + if (has_trace) + trace_free_summary(&trace); + + return rc; +} diff --git a/logging/hiperiso-log/report.h b/logging/hiperiso-log/report.h new file mode 100644 index 0000000..fbf1c46 --- /dev/null +++ b/logging/hiperiso-log/report.h @@ -0,0 +1,24 @@ +/* + * report.h - Generate analysis reports from boot logs. + * + * Reads serial.log, trace.bin, qemu.cmdline, and env.txt from a log + * directory, then writes report.json and report.txt into the same directory. + */ + +#ifndef REPORT_H +#define REPORT_H + +/* + * Generate report.json and report.txt inside . + * + * Input files (all optional — missing files are handled gracefully): + * /env.txt Key: Value lines + * /qemu.cmdline Single-line QEMU command + * /serial.log Serial console text + * /trace.bin QEMU simpletrace binary + * + * Returns 0 on success, -1 if the report files could not be written. + */ +int report_generate(const char *log_dir); + +#endif /* REPORT_H */ diff --git a/logging/hiperiso-log/serial_parser.c b/logging/hiperiso-log/serial_parser.c new file mode 100644 index 0000000..daf0333 --- /dev/null +++ b/logging/hiperiso-log/serial_parser.c @@ -0,0 +1,304 @@ +/* + * serial_parser.c - Parse serial console logs and classify boot stages. + * + * Reads the text captured by QEMU's -serial file:serial.log and classifies + * each line into a boot stage (firmware → bootloader → kernel → hardware → + * userspace → login). Kernel timestamps in [X.XXXXXX] prefix are used to + * estimate boot duration. + */ + +#include "serial_parser.h" + +#include +#include +#include +#include + +const char *boot_stage_names[STAGE_COUNT] = { + "unknown", + "firmware", + "bootloader", + "kernel_init", + "hardware_init", + "userspace", + "login", + "complete", + "error" +}; + +static int is_benign_warning(const char *line) +{ + if (strstr(line, "ACPI:") || strstr(line, "ACPI Error:") || + strstr(line, "acpi")) + return 1; + + if (strstr(line, "[drm:") || strstr(line, "drm:") || + strstr(line, "DRM:")) + return 1; + + if (strstr(line, "scheduling while atomic") || + strstr(line, "warning:")) + return 1; + + if (strstr(line, "can't find") || strstr(line, "not found.") || + strstr(line, "unknown symbol")) + return 1; + + return 0; +} + +static boot_stage_t classify_line(const char *line) +{ + if (strstr(line, "kernel panic") || strstr(line, "Kernel panic") || + strstr(line, "Kernel Panic") || strstr(line, "not syncing.")) + return STAGE_ERROR; + + if ((strstr(line, "BUG:") || strstr(line, "Oops:") || + strstr(line, "Call Trace")) && !is_benign_warning(line)) + return STAGE_ERROR; + + if (strstr(line, "BdsDxe") || strstr(line, "OVMF") || + strstr(line, "UEFI") || strstr(line, "SEC:") || + strstr(line, "PEI:") || strstr(line, "DXE:")) + return STAGE_FIRMWARE; + + if (strstr(line, "grub") || strstr(line, "GRUB") || + strstr(line, "GNU GRUB") || strstr(line, "systemd-boot")) + return STAGE_BOOTLOADER; + + if (strstr(line, "Linux version") || strstr(line, "Booting Linux")) + return STAGE_KERNEL_INIT; + + if (strstr(line, "login:") || strstr(line, "Login") || + strstr(line, "Welcome")) + return STAGE_LOGIN; + + if (strstr(line, "boot finished") || strstr(line, "Boot complete") || + strstr(line, "System finished") || strstr(line, "Reached target")) + return STAGE_COMPLETE; + + if (strstr(line, "systemd") || strstr(line, "Starting") || + strstr(line, "Mounted") || strstr(line, "Started")) + return STAGE_USERSPACE; + + if (strstr(line, "[drm]") || strstr(line, "usb") || strstr(line, "USB") || + strstr(line, "pci") || strstr(line, "PCI") || + strstr(line, "ahci") || strstr(line, "AHCI") || + strstr(line, "SCSI") || strstr(line, "scsi") || + strstr(line, "ata") || strstr(line, "ATA") || + strstr(line, "virtio")|| strstr(line, "VIRTIO")|| + strstr(line, "input:")|| strstr(line, "eth") || + strstr(line, "acpi") || strstr(line, "ACPI")) + return STAGE_HARDWARE_INIT; + + return STAGE_UNKNOWN; +} + +static int extract_timestamp(const char *line, double *ts) +{ + const char *p = strchr(line, '['); + if (!p) + return -1; + p++; + + char *end; + double val = strtod(p, &end); + if (end == p) + return -1; + if (*end != ']') + return -1; + + *ts = val; + return 0; +} + +static char *read_line(FILE *f) +{ + size_t cap = 256; + size_t len = 0; + char *buf = malloc(cap); + if (!buf) + return NULL; + + int c; + while ((c = fgetc(f)) != EOF) { + if (c == '\n') + break; + if (len + 1 >= cap) { + size_t newcap = cap * 2; + char *tmp = realloc(buf, newcap); + if (!tmp) { + free(buf); + return NULL; + } + buf = tmp; + cap = newcap; + } + buf[len++] = (char)c; + } + + if (len == 0 && c == EOF) { + free(buf); + return NULL; + } + + buf[len] = '\0'; + if (len > 0 && buf[len - 1] == '\r') + buf[len - 1] = '\0'; + + return buf; +} + +int serial_parse_file(const char *path, serial_summary_t *summary) +{ + if (!path || !summary) + return -1; + + FILE *f = fopen(path, "r"); + if (!f) + return -1; + + memset(summary, 0, sizeof(*summary)); + summary->final_stage = STAGE_UNKNOWN; + + serial_line_t *tail = NULL; + double first_ts = -1.0; + double last_ts = -1.0; + + for (;;) { + char *text = read_line(f); + if (!text) + break; + if (text[0] == '\0') { + free(text); + continue; + } + + serial_line_t *sl = calloc(1, sizeof(serial_line_t)); + if (!sl) { + free(text); + break; + } + + sl->text = text; + sl->stage = classify_line(text); + + if (!summary->lines) + summary->lines = sl; + else + tail->next = sl; + tail = sl; + summary->total_lines++; + + if (sl->stage >= 0 && sl->stage < STAGE_COUNT) + summary->lines_per_stage[sl->stage]++; + + if (sl->stage != STAGE_UNKNOWN) { + if (sl->stage == STAGE_ERROR) { + summary->final_stage = STAGE_ERROR; + } else if (sl->stage > summary->final_stage) { + summary->final_stage = sl->stage; + } + } + + if (sl->stage == STAGE_ERROR && summary->error_count < 8) { + strncpy(summary->error_messages[summary->error_count], + text, 255); + summary->error_messages[summary->error_count][255] = '\0'; + summary->error_count++; + } + + double ts; + if (extract_timestamp(text, &ts) == 0) { + if (first_ts < 0) + first_ts = ts; + last_ts = ts; + } + } + + fclose(f); + + if (first_ts >= 0 && last_ts >= first_ts) + summary->boot_duration_ms = (uint64_t)((last_ts - first_ts) * 1000.0); + + return 0; +} + +void serial_free_summary(serial_summary_t *summary) +{ + if (!summary) + return; + + serial_line_t *sl = summary->lines; + while (sl) { + serial_line_t *next = sl->next; + free(sl->text); + free(sl); + sl = next; + } + summary->lines = NULL; + summary->total_lines = 0; +} + +void serial_print_stages(const serial_summary_t *summary, FILE *out) +{ + const char *final = (summary->final_stage >= 0 && + summary->final_stage < STAGE_COUNT) + ? boot_stage_names[summary->final_stage] + : "unknown"; + + fprintf(out, "=== Boot Stages ===\n\n"); + fprintf(out, "Final stage: %s\n\n", final); + + fprintf(out, "Lines per stage:\n"); + for (int i = 0; i < STAGE_COUNT; i++) { + fprintf(out, " %-20s %5zu%s\n", + boot_stage_names[i], + summary->lines_per_stage[i], + summary->lines_per_stage[i] > 0 ? " <--" : ""); + } + + if (summary->boot_duration_ms > 0) { + fprintf(out, "\nBoot duration: %" PRIu64 " ms (%.2f s)\n", + summary->boot_duration_ms, + summary->boot_duration_ms / 1000.0); + } + + if (summary->error_count > 0) { + fprintf(out, "\nErrors (%d):\n", summary->error_count); + for (int i = 0; i < summary->error_count; i++) + fprintf(out, " %s\n", summary->error_messages[i]); + } +} + +void serial_print_text(const serial_summary_t *summary, FILE *out) +{ + fprintf(out, "=== Serial Console Summary ===\n"); + fprintf(out, "Total lines: %zu\n", summary->total_lines); + + const char *final = (summary->final_stage >= 0 && + summary->final_stage < STAGE_COUNT) + ? boot_stage_names[summary->final_stage] + : "unknown"; + fprintf(out, "Final boot stage: %s\n", final); + + if (summary->boot_duration_ms > 0) { + fprintf(out, "Boot duration: %" PRIu64 " ms\n", + summary->boot_duration_ms); + } + + fprintf(out, "\nLines per stage:\n"); + for (int i = 0; i < STAGE_COUNT; i++) { + if (summary->lines_per_stage[i] > 0) { + fprintf(out, " %-20s %zu\n", + boot_stage_names[i], + summary->lines_per_stage[i]); + } + } + + if (summary->error_count > 0) { + fprintf(out, "\nErrors (%d):\n", summary->error_count); + for (int i = 0; i < summary->error_count; i++) + fprintf(out, " %s\n", summary->error_messages[i]); + } +} diff --git a/logging/hiperiso-log/serial_parser.h b/logging/hiperiso-log/serial_parser.h new file mode 100644 index 0000000..410117b --- /dev/null +++ b/logging/hiperiso-log/serial_parser.h @@ -0,0 +1,66 @@ +/* + * serial_parser.h - Parse serial.log text files and identify boot stages. + * + * Reads the QEMU serial console capture and classifies each line into a + * boot stage (firmware → bootloader → kernel → hardware → userspace → login). + */ + +#ifndef SERIAL_PARSER_H +#define SERIAL_PARSER_H + +#include +#include +#include + +/* Boot progression stages (ordered by typical sequence). */ +typedef enum { + STAGE_UNKNOWN = 0, + STAGE_FIRMWARE, /* UEFI / OVMF initialization */ + STAGE_BOOTLOADER, /* GRUB / systemd-boot */ + STAGE_KERNEL_INIT, /* "Linux version", "Booting Linux" */ + STAGE_HARDWARE_INIT, /* hardware detection messages */ + STAGE_USERSPACE, /* systemd / init starting */ + STAGE_LOGIN, /* login prompt */ + STAGE_COMPLETE, /* boot finished */ + STAGE_ERROR, /* panic, oops, BUG */ + STAGE_COUNT /* sentinel — number of stages */ +} boot_stage_t; + +/* Human-readable names for each stage (indexed by boot_stage_t). */ +extern const char *boot_stage_names[STAGE_COUNT]; + +/* A single serial-log line (node in a singly-linked list). */ +typedef struct serial_line { + char *text; /* malloc'd line content (no trailing \n) */ + boot_stage_t stage; + struct serial_line *next; +} serial_line_t; + +/* Aggregate statistics over all parsed lines. */ +typedef struct serial_summary { + serial_line_t *lines; /* linked-list head */ + size_t total_lines; + size_t lines_per_stage[STAGE_COUNT]; + boot_stage_t final_stage; /* highest stage reached (or STAGE_ERROR) */ + char error_messages[8][256]; /* up to 8 error lines */ + int error_count; + uint64_t boot_duration_ms; /* estimated from [X.XXXXXX] timestamps */ +} serial_summary_t; + +/* + * Parse a serial console log. + * Returns 0 on success, -1 on error (file not found, read failure). + * Caller must call serial_free_summary() to release memory. + */ +int serial_parse_file(const char *path, serial_summary_t *summary); + +/* Free all dynamically-allocated memory in *summary. */ +void serial_free_summary(serial_summary_t *summary); + +/* Print per-stage line counts and boot duration. */ +void serial_print_stages(const serial_summary_t *summary, FILE *out); + +/* Print a human-readable summary. */ +void serial_print_text(const serial_summary_t *summary, FILE *out); + +#endif /* SERIAL_PARSER_H */ diff --git a/logging/hiperiso-log/simpletrace.h b/logging/hiperiso-log/simpletrace.h new file mode 100644 index 0000000..ad7a1cc --- /dev/null +++ b/logging/hiperiso-log/simpletrace.h @@ -0,0 +1,68 @@ +/* + * simpletrace.h - QEMU simpletrace binary format constants. + * + * Based on the actual QEMU source: scripts/simpletrace.py and trace/simple.c. + * The format is NOT documented as stable — trace files must be parsed with + * the matching simpletrace implementation. + * + * All fields are stored in native byte-order (little-endian on x86_64). + * + * File layout: + * + * File header (24 bytes, Python struct '=QQQ'): + * uint64_t header_event_id = TRACE_HEADER_EVENT_ID (sentinel) + * uint64_t header_magic = TRACE_HEADER_MAGIC + * uint64_t log_version = TRACE_HEADER_VERSION + * + * Records — each begins with a uint64 discriminator ("rectype"): + * + * rectype == TRACE_RECORD_TYPE_MAPPING (0): + * uint64_t event_id + * uint32_t name_length + * char name[name_length] (no NUL terminator on disk) + * + * rectype == TRACE_RECORD_TYPE_EVENT (1): + * uint64_t event_id + * uint64_t timestamp_ns + * uint32_t record_length (total: 24 + args payload length) + * uint32_t record_pid + * uint8_t args[record_length - 24] + */ + +#ifndef SIMPLETRACE_H +#define SIMPLETRACE_H + +#include + +/* --- File header constants --- */ + +/* First uint64 of the file header — identifies this as the header record. */ +#define TRACE_HEADER_EVENT_ID 0xffffffffffffffffULL + +/* Magic value in the second uint64 of the file header. */ +#define TRACE_HEADER_MAGIC 0xf2b177cb0aa429b4ULL + +/* Supported log format versions (0, 2, 3, 4 have existed; 4 is current). */ +#define TRACE_HEADER_VERSION 4 + +/* Pseudo event-id used when QEMU drops events due to a full ring buffer. */ +#define TRACE_DROPPED_EVENT_ID 0xfffffffffffffffeULL + +/* --- Record type discriminators (first uint64 of each record) --- */ + +#define TRACE_RECORD_TYPE_MAPPING 0 +#define TRACE_RECORD_TYPE_EVENT 1 + +/* --- Field / header sizes --- */ + +/* File header: 3 x uint64 = 24 bytes. */ +#define TRACE_LOG_HEADER_LEN 24 + +/* Event-record body after the rectype uint64 (Python '=QQII'): 24 bytes. + * uint64 event_id + uint64 timestamp_ns + uint32 record_length + uint32 pid */ +#define TRACE_REC_HEADER_LEN 24 + +/* Record-type discriminator field. */ +#define TRACE_REC_TYPE_LEN 8 + +#endif /* SIMPLETRACE_H */ diff --git a/logging/hiperiso-log/trace_parser.c b/logging/hiperiso-log/trace_parser.c new file mode 100644 index 0000000..d5be20f --- /dev/null +++ b/logging/hiperiso-log/trace_parser.c @@ -0,0 +1,364 @@ +/* + * trace_parser.c - Parse QEMU simpletrace binary files. + * + * Format reference: QEMU scripts/simpletrace.py (log format version 4). + * All multi-byte fields are little-endian (native on x86_64). + */ + +#include "trace_parser.h" +#include "simpletrace.h" + +#include +#include +#include +#include + +#define MAX_EVENTS_IN_LIST 1000000 + +static const char *known_event_names[] = { + "serial_read", + "serial_write", + "ide_sector_read", + "cd_read_sector", + "cd_read_sector_sync", + "blk_co_preadv", + "virtio_blk_handle_read", + "cpu_in", + "cpu_out", + "pci_cfg_read", + "pci_cfg_write", + "ide_ioport_read", + "ide_ioport_write", + "memory_region_ops_read", + "memory_region_ops_write", + NULL +}; + +static int is_disk_read(const char *name) +{ + return strcmp(name, "ide_sector_read") == 0 || + strcmp(name, "cd_read_sector") == 0 || + strcmp(name, "cd_read_sector_sync") == 0 || + strcmp(name, "blk_co_preadv") == 0 || + strcmp(name, "virtio_blk_handle_read") == 0; +} + +static int is_disk_write(const char *name) +{ + return strcmp(name, "blk_co_pwritev") == 0 || + strcmp(name, "virtio_blk_handle_write") == 0 || + strcmp(name, "ide_sector_write") == 0; +} + +static int read_u64(FILE *f, uint64_t *val) +{ + uint8_t b[8]; + if (fread(b, 1, 8, f) != 8) + return -1; + *val = (uint64_t)b[0] + | ((uint64_t)b[1] << 8) + | ((uint64_t)b[2] << 16) + | ((uint64_t)b[3] << 24) + | ((uint64_t)b[4] << 32) + | ((uint64_t)b[5] << 40) + | ((uint64_t)b[6] << 48) + | ((uint64_t)b[7] << 56); + return 0; +} + +static int read_u32(FILE *f, uint32_t *val) +{ + uint8_t b[4]; + if (fread(b, 1, 4, f) != 4) + return -1; + *val = (uint32_t)b[0] + | ((uint32_t)b[1] << 8) + | ((uint32_t)b[2] << 16) + | ((uint32_t)b[3] << 24); + return 0; +} + +static void init_known_names(trace_summary_t *summary) +{ + for (int i = 0; known_event_names[i] != NULL && i < TRACE_MAX_EVENT_ID; i++) { + size_t len = strlen(known_event_names[i]); + if (len >= 64) + len = 63; + memcpy(summary->event_names[i], known_event_names[i], len); + summary->event_names[i][len] = '\0'; + } +} + +static void resolve_name(trace_summary_t *summary, trace_event_t *ev) +{ + if (ev->event_id < TRACE_MAX_EVENT_ID && summary->event_names[ev->event_id][0]) { + strncpy(ev->name, summary->event_names[ev->event_id], 63); + ev->name[63] = '\0'; + } else { + snprintf(ev->name, sizeof(ev->name), "event_%" PRIu64, ev->event_id); + } +} + +static void update_stats(trace_summary_t *summary, const trace_event_t *ev) +{ + summary->total_events++; + if (ev->event_id < TRACE_MAX_EVENT_ID) + summary->events_by_id[ev->event_id]++; + + if (summary->first_timestamp == 0 || ev->timestamp_ns < summary->first_timestamp) + summary->first_timestamp = ev->timestamp_ns; + if (ev->timestamp_ns > summary->last_timestamp) + summary->last_timestamp = ev->timestamp_ns; + + if (is_disk_read(ev->name)) + summary->total_disk_reads++; + else if (is_disk_write(ev->name)) + summary->total_disk_writes++; + if (strcmp(ev->name, "serial_write") == 0) + summary->total_serial_writes++; +} + +static void handle_mapping(FILE *f, trace_summary_t *summary) +{ + uint64_t event_id; + uint32_t name_len; + + if (read_u64(f, &event_id) != 0) + return; + if (read_u32(f, &name_len) != 0) + return; + + if (name_len == 0 || name_len > 4096) { + if (name_len > 0) + fseek(f, (long)name_len, SEEK_CUR); + return; + } + + char namebuf[64]; + size_t copylen = name_len < 63 ? name_len : 63; + + if (copylen <= sizeof(namebuf) - 1) { + if (fread(namebuf, 1, copylen, f) != copylen) + return; + namebuf[copylen] = '\0'; + + if (event_id < TRACE_MAX_EVENT_ID) { + memcpy(summary->event_names[event_id], namebuf, copylen + 1); + } + + if (name_len > copylen) { + if (fseek(f, (long)(name_len - copylen), SEEK_CUR) != 0) + return; + } + } else { + if (fseek(f, (long)name_len, SEEK_CUR) != 0) + return; + } +} + +static int handle_event(FILE *f, trace_summary_t *summary, trace_event_t **tail) +{ + uint64_t event_id, timestamp_ns; + uint32_t record_length, record_pid; + + if (read_u64(f, &event_id) != 0) return -1; + if (read_u64(f, ×tamp_ns) != 0) return -1; + if (read_u32(f, &record_length) != 0) return -1; + if (read_u32(f, &record_pid) != 0) return -1; + + uint32_t args_len = 0; + if (record_length >= TRACE_REC_HEADER_LEN) + args_len = record_length - TRACE_REC_HEADER_LEN; + + uint8_t *args = NULL; + if (args_len > 0) { + args = malloc(args_len); + if (!args) + return -1; + if (fread(args, 1, args_len, f) != args_len) { + free(args); + return -1; + } + } + + trace_event_t *ev = calloc(1, sizeof(trace_event_t)); + if (!ev) { + free(args); + return -1; + } + + ev->event_id = event_id; + ev->timestamp_ns = timestamp_ns; + ev->pid = record_pid; + ev->args_len = args_len; + ev->args = args; + + resolve_name(summary, ev); + update_stats(summary, ev); + + if (!summary->events) + summary->events = ev; + else + (*tail)->next = ev; + *tail = ev; + summary->event_count++; + + return 0; +} + +int trace_parse_file(const char *path, trace_summary_t *summary) +{ + if (!path || !summary) + return -1; + + FILE *f = fopen(path, "rb"); + if (!f) + return -1; + + memset(summary, 0, sizeof(*summary)); + init_known_names(summary); + + uint64_t hdr_event_id, hdr_magic, hdr_version; + if (read_u64(f, &hdr_event_id) != 0 || + read_u64(f, &hdr_magic) != 0 || + read_u64(f, &hdr_version) != 0) { + fclose(f); + return -1; + } + + if (hdr_event_id != TRACE_HEADER_EVENT_ID || + hdr_magic != TRACE_HEADER_MAGIC) { + fclose(f); + return -1; + } + + trace_event_t *tail = NULL; + + for (;;) { + if (summary->event_count >= MAX_EVENTS_IN_LIST) { + uint64_t rectype; + while (read_u64(f, &rectype) == 0) { + if (rectype == TRACE_RECORD_TYPE_MAPPING) { + handle_mapping(f, summary); + } else { + uint64_t event_id, timestamp_ns; + uint32_t record_length, record_pid; + if (read_u64(f, &event_id) != 0) break; + if (read_u64(f, ×tamp_ns) != 0) break; + if (read_u32(f, &record_length) != 0) break; + if (read_u32(f, &record_pid) != 0) break; + summary->total_events++; + if (event_id < TRACE_MAX_EVENT_ID) + summary->events_by_id[event_id]++; + if (timestamp_ns > summary->last_timestamp) + summary->last_timestamp = timestamp_ns; + uint32_t al = 0; + if (record_length >= TRACE_REC_HEADER_LEN) + al = record_length - TRACE_REC_HEADER_LEN; + if (al > 0) + fseek(f, (long)al, SEEK_CUR); + } + } + break; + } + + uint64_t rectype; + if (read_u64(f, &rectype) != 0) + break; + + if (rectype == TRACE_RECORD_TYPE_MAPPING) { + handle_mapping(f, summary); + } else if (rectype == TRACE_RECORD_TYPE_EVENT) { + if (handle_event(f, summary, &tail) != 0) + break; + } else { + break; + } + } + + fclose(f); + return 0; +} + +void trace_free_summary(trace_summary_t *summary) +{ + if (!summary) + return; + + trace_event_t *ev = summary->events; + while (ev) { + trace_event_t *next = ev->next; + free(ev->args); + free(ev); + ev = next; + } + summary->events = NULL; + summary->event_count = 0; +} + +void trace_print_text(const trace_summary_t *summary, FILE *out) +{ + fprintf(out, "=== Trace Summary ===\n"); + fprintf(out, "Total events: %" PRIu64 "\n", summary->total_events); + fprintf(out, "Events in list: %zu\n", summary->event_count); + fprintf(out, "First timestamp: %" PRIu64 " ns\n", summary->first_timestamp); + fprintf(out, "Last timestamp: %" PRIu64 " ns\n", summary->last_timestamp); + + if (summary->last_timestamp > summary->first_timestamp) { + double dur_ms = (double)(summary->last_timestamp - summary->first_timestamp) / 1000000.0; + fprintf(out, "Duration: %.3f ms\n", dur_ms); + } + + fprintf(out, "\nI/O Counts:\n"); + fprintf(out, " Disk reads: %" PRIu64 "\n", summary->total_disk_reads); + fprintf(out, " Disk writes: %" PRIu64 "\n", summary->total_disk_writes); + fprintf(out, " Serial writes: %" PRIu64 "\n", summary->total_serial_writes); + + fprintf(out, "\n--- Events by ID ---\n"); + for (int i = 0; i < TRACE_MAX_EVENT_ID; i++) { + if (summary->events_by_id[i] > 0) { + const char *nm = summary->event_names[i][0] + ? summary->event_names[i] : "(unknown)"; + fprintf(out, " [%3d] %-40s %" PRIu64 "\n", i, nm, summary->events_by_id[i]); + } + } +} + +void trace_print_json(const trace_summary_t *summary, FILE *out) +{ + fprintf(out, "{\n"); + fprintf(out, " \"total_events\": %" PRIu64 ",\n", summary->total_events); + fprintf(out, " \"event_count\": %zu,\n", summary->event_count); + fprintf(out, " \"first_timestamp_ns\": %" PRIu64 ",\n", summary->first_timestamp); + fprintf(out, " \"last_timestamp_ns\": %" PRIu64 ",\n", summary->last_timestamp); + fprintf(out, " \"disk_reads\": %" PRIu64 ",\n", summary->total_disk_reads); + fprintf(out, " \"disk_writes\": %" PRIu64 ",\n", summary->total_disk_writes); + fprintf(out, " \"serial_writes\": %" PRIu64 ",\n", summary->total_serial_writes); + + fprintf(out, " \"events\": ["); + int first = 1; + for (int i = 0; i < TRACE_MAX_EVENT_ID; i++) { + if (summary->events_by_id[i] > 0) { + const char *nm = summary->event_names[i][0] + ? summary->event_names[i] : "unknown"; + fprintf(out, "%s\n {\"id\": %d, \"name\": \"%s\", \"count\": %" PRIu64 "}", + first ? "" : ",", i, nm, summary->events_by_id[i]); + first = 0; + } + } + fprintf(out, first ? "]\n" : "\n ]\n"); + + fprintf(out, "}\n"); +} + +void trace_print_csv(const trace_summary_t *summary, FILE *out) +{ + fprintf(out, "event_id,event_name,count\n"); + for (int i = 0; i < TRACE_MAX_EVENT_ID; i++) { + if (summary->events_by_id[i] > 0) { + const char *nm = summary->event_names[i][0] + ? summary->event_names[i] : "unknown"; + fprintf(out, "%d,%s,%" PRIu64 "\n", i, nm, summary->events_by_id[i]); + } + } +} diff --git a/logging/hiperiso-log/trace_parser.h b/logging/hiperiso-log/trace_parser.h new file mode 100644 index 0000000..0a1654b --- /dev/null +++ b/logging/hiperiso-log/trace_parser.h @@ -0,0 +1,59 @@ +/* + * trace_parser.h - Parse QEMU simpletrace .bin files. + * + * Provides structured access to trace events, summary statistics, and + * formatted output (text / JSON / CSV). + */ + +#ifndef TRACE_PARSER_H +#define TRACE_PARSER_H + +#include +#include +#include + +/* Maximum number of distinct event IDs we track (QEMU uses sequential IDs). */ +#define TRACE_MAX_EVENT_ID 256 + +/* A single parsed trace event (node in a singly-linked list). */ +typedef struct trace_event { + uint64_t event_id; + uint64_t timestamp_ns; + uint64_t pid; + uint32_t args_len; + uint8_t *args; /* raw argument bytes (malloc'd, may be NULL) */ + char name[64]; /* resolved name, or "event_" fallback */ + struct trace_event *next; +} trace_event_t; + +/* Aggregate statistics over all parsed events. */ +typedef struct trace_summary { + uint64_t total_events; + uint64_t events_by_id[TRACE_MAX_EVENT_ID]; /* count per event_id */ + char event_names[TRACE_MAX_EVENT_ID][64]; /* name mapping */ + uint64_t first_timestamp; + uint64_t last_timestamp; + uint64_t total_disk_reads; + uint64_t total_disk_writes; + uint64_t total_serial_writes; + trace_event_t *events; /* linked-list head (all parsed events) */ + size_t event_count; /* number of nodes in the list */ +} trace_summary_t; + +/* + * Parse a QEMU simpletrace binary file. + * Returns 0 on success (even if the file contained 0 events), -1 on error + * (file not found, bad magic, read failure). + * Caller must call trace_free_summary() to release memory. + */ +int trace_parse_file(const char *path, trace_summary_t *summary); + +/* Free all dynamically-allocated memory in *summary. */ +void trace_free_summary(trace_summary_t *summary); + +/* Output helpers — write formatted summaries to *out. */ +void trace_print_text(const trace_summary_t *summary, FILE *out); +void trace_print_json(const trace_summary_t *summary, FILE *out); +void trace_print_csv(const trace_summary_t *summary, FILE *out); + +#endif /* TRACE_PARSER_H */ diff --git a/logging/trace-detailed.events b/logging/trace-detailed.events new file mode 100644 index 0000000..94741e6 --- /dev/null +++ b/logging/trace-detailed.events @@ -0,0 +1,18 @@ +# hiperiso trace events — Tier 2 (detailed) +# Standard set plus PCI config-space access, port I/O, and AHCI details. +# Used by: qemu -trace events=trace-detailed.events +serial_read +serial_write +cd_read_sector +cd_read_sector_sync +blk_co_preadv +blk_co_pwritev +ahci_command_complete +cpu_in +cpu_out +pci_cfg_read +pci_cfg_write +ahci_port_read +ahci_port_write +ahci_irq_raise +ahci_irq_lower diff --git a/logging/trace-full.events b/logging/trace-full.events new file mode 100644 index 0000000..2860c3e --- /dev/null +++ b/logging/trace-full.events @@ -0,0 +1,21 @@ +# hiperiso trace events — Tier 3 (full) +# Detailed set plus every memory-mapped I/O access. Very high overhead +# (EPT violation per access) — debug only, expect 10-100x boot slowdown. +# Used by: qemu -trace events=trace-full.events +serial_read +serial_write +cd_read_sector +cd_read_sector_sync +blk_co_preadv +blk_co_pwritev +ahci_command_complete +cpu_in +cpu_out +pci_cfg_read +pci_cfg_write +ahci_port_read +ahci_port_write +ahci_irq_raise +ahci_irq_lower +memory_region_ops_read +memory_region_ops_write diff --git a/logging/trace-standard.events b/logging/trace-standard.events new file mode 100644 index 0000000..5adfd74 --- /dev/null +++ b/logging/trace-standard.events @@ -0,0 +1,10 @@ +# hiperiso trace events — Tier 1 (standard) +# Always-on: serial console capture + basic disk/CD-ROM I/O. +# Used by: qemu -trace events=trace-standard.events +serial_read +serial_write +cd_read_sector +cd_read_sector_sync +blk_co_preadv +blk_co_pwritev +ahci_command_complete