#!/usr/bin/env python3 """Trims a full `java ... -jar target/x.jar` run log down to the part worth committing as docs/output/01-oom-console.txt: a handful of progress lines leading up to the real OutOfMemoryError, the "Dumping heap" / "Heap dump file created" lines, and (if present) one real OutOfMemoryError stack trace thrown from application code on a worker thread. Usage: extract-oom-console.py """ import sys import os def main(mod_dir: str, log_path: str, command: str) -> None: lines = open(log_path, encoding="utf-8", errors="replace").read().splitlines() # drop the sandbox's own JAVA_TOOL_OPTIONS proxy banner -- environment noise, not output lines = [l for l in lines if not l.startswith("Picked up JAVA_TOOL_OPTIONS")] first_oom = next((i for i, l in enumerate(lines) if "OutOfMemoryError: Java heap space" in l), None) if first_oom is None: raise SystemExit("no OutOfMemoryError found in " + log_path) start = max(0, first_oom - 5) # The dump-triggering OOM line itself, plus "Dumping heap to ..." right after it if present. core = [lines[first_oom]] for k in range(first_oom + 1, min(first_oom + 4, len(lines))): if lines[k].startswith("Dumping heap to"): core.append(lines[k]) break # "Heap dump file created" can appear much later in the raw log, because writing a ~150-300 # MB dump takes a second or two during which OTHER worker threads keep printing progress # lines concurrently. Rather than include that whole noisy, interleaved stretch, skip straight # to that one line wherever it is. created_line = next( (l for l in lines[first_oom + 1:] if l.startswith("Heap dump file created")), None ) if created_line: core.append(created_line) end = first_oom + 1 # "extra" below scans forward from here, independent of core # If a *second*, worker-thread OutOfMemoryError with a real stack trace follows shortly # after (common when more than one thread hits the wall around the same time), keep the # cleanest such trace -- it is often more informative than the dump-triggering one alone. # With several worker threads printing at once, most candidates have another thread's # progress line spliced into the middle of the stack trace; rather than include that # interleaving (confusing to a reader) or paper over it (no longer verbatim), look at every # candidate in the window and keep whichever has the longest *uninterrupted* run of real # "\tat " frames immediately after its "Exception in thread" line, truncating there. best = [] search_end = min(end + 300, len(lines)) for i in range(end, search_end): line = lines[i] if 'Exception in thread "' in line and "OutOfMemoryError" in line: trace = [line] j = i + 1 while j < len(lines) and lines[j].startswith("\tat "): trace.append(lines[j]) j += 1 if len(trace) > len(best): best = trace extra = best out_lines = [f"$ {command}", ""] + lines[start:first_oom] + core + ([""] + extra if extra else []) out_path = os.path.join(mod_dir, "docs", "output", "01-oom-console.txt") with open(out_path, "w") as f: f.write("\n".join(out_lines) + "\n") print("wrote", out_path) if __name__ == "__main__": if len(sys.argv) != 4: raise SystemExit("usage: extract-oom-console.py ") main(sys.argv[1], sys.argv[2], sys.argv[3])