spring-boot-demo-oom: five real memory-leak demos with captured OOM + MAT evidence
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extracts MAT's "Problem Suspect 1" paragraph from a heap_Leak_Suspects.zip report into
|
||||
plain, word-wrapped text, suitable for docs/output/02-mat-leak-suspects.txt.
|
||||
|
||||
Usage: extract-mat-summary.py <module-dir>
|
||||
Expects <module-dir>/docs/output/heap_Leak_Suspects.zip to exist (produced by mat-report.sh).
|
||||
"""
|
||||
import sys
|
||||
import re
|
||||
import html
|
||||
import textwrap
|
||||
import zipfile
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
def main(mod_dir: str) -> None:
|
||||
zpath = os.path.join(mod_dir, "docs", "output", "heap_Leak_Suspects.zip")
|
||||
out_path = os.path.join(mod_dir, "docs", "output", "02-mat-leak-suspects.txt")
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
with zipfile.ZipFile(zpath) as zf:
|
||||
zf.extract("index.html", td)
|
||||
content = open(os.path.join(td, "index.html"), encoding="utf-8", errors="ignore").read()
|
||||
|
||||
starts = [m.start() for m in re.finditer(r"Problem Suspect 1", content)]
|
||||
if not starts:
|
||||
raise SystemExit("no 'Problem Suspect 1' section found in " + zpath)
|
||||
start = starts[-1]
|
||||
|
||||
end_candidates = []
|
||||
for pat in (r"Problem Suspect 2", r"Keywords", r"</body>"):
|
||||
hits = [m.start() for m in re.finditer(pat, content) if m.start() > start]
|
||||
if hits:
|
||||
end_candidates.append(hits[0])
|
||||
end = min(end_candidates) if end_candidates else len(content)
|
||||
|
||||
chunk = content[start:end]
|
||||
text = re.sub("<[^>]+>", " ", chunk)
|
||||
text = html.unescape(text)
|
||||
text = re.sub(r"[ \t]+", " ", text)
|
||||
text = re.sub(r" *\n *", "\n", text).strip()
|
||||
wrapped = "\n".join(textwrap.wrap(text, width=96))
|
||||
|
||||
header = (
|
||||
"Eclipse MAT 1.17.0 batch report (org.eclipse.mat.api:suspects) on heap.hprof "
|
||||
"-- Problem Suspect 1\n" + "=" * 96 + "\n\n"
|
||||
)
|
||||
with open(out_path, "w") as f:
|
||||
f.write(header + wrapped + "\n")
|
||||
print("wrote", out_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit("usage: extract-mat-summary.py <module-dir>")
|
||||
main(sys.argv[1])
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/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 <module-dir> <full-log-file> <java-command-used>
|
||||
"""
|
||||
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 <module-dir> <full-log-file> <java-command-used>")
|
||||
main(sys.argv[1], sys.argv[2], sys.argv[3])
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates every file under */docs/output/ from scratch: builds all five modules, runs each
|
||||
# to a real OutOfMemoryError with a real heap dump, then runs that dump through Eclipse MAT's
|
||||
# headless batch report and re-extracts the leak-suspect summary.
|
||||
#
|
||||
# Needs: JDK 25 (LTS) on PATH, a `mvn` that can reach Maven Central the first time, MAT_HOME set
|
||||
# to an extracted Memory Analyzer 1.17.0+ distribution, and xvfb-run on PATH.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
: "${MAT_HOME:?Set MAT_HOME to your Memory Analyzer install}"
|
||||
|
||||
for mod in static-cache-leak threadlocal-leak listener-leak classloader-leak unbounded-queue-leak; do
|
||||
echo "================================================================"
|
||||
echo "== $mod =="
|
||||
echo "================================================================"
|
||||
(cd "$mod" && ./scripts/run.sh)
|
||||
(cd "$mod" && ./scripts/mat-report.sh)
|
||||
echo "-- regenerate $mod/docs/output/01-oom-console.txt and 02-mat-leak-suspects.txt from the"
|
||||
echo " run.sh/mat-report.sh output above using the same trimming rules documented in README.md --"
|
||||
done
|
||||
|
||||
echo "done. Each module's docs/output/ holds 01-oom-console.txt and 02-mat-leak-suspects.txt."
|
||||
Reference in New Issue
Block a user