Gate a build on a leak threshold¶
The problem¶
You run a soak test or a UI test and capture a heap dump at the end. You want the build to fail when one object keeps too much of the heap alive, or when a class that should not grow has grown, instead of finding out from a user.
The solution¶
heaptrail writes the result as JSON, and jq -e turns a condition on that JSON into an exit status: 0 if the expression is true, 1 if it is false.
Fail when one suspect retains too much¶
heaptrail -i heap.hprof --leak-suspects --exclude-soft-weak --top 1 --json --json-out reports/suspects.json
jq -e '.suspects[0].heap_share_pct < 30' reports/suspects.json
In this dump the biggest suspect keeps 26.2 % of the reachable heap, so a threshold of 30 passes:
A stricter threshold of 20 fails, and jq -e exits with status 1:
The same shape works on bytes: jq -e '.suspects[0].retained_bytes < 50000000'. A percentage follows the size of the heap, and bytes follow the app. Pick the one that matches what you want to catch.
Fail when a class grew¶
Compare the dump with a baseline you keep, and test the delta of the classes you care about. Ask for many rows so the class is certainly in the list:
heaptrail --diff-from before.hprof --diff-to after.hprof --top 100000 --json --json-out reports/diff.json
jq -e '(map(select(.class_name == "com.nexio.tv.domain.model.MetaPreview")) | first) as $c | $c.delta_count <= 1000' reports/diff.json
Here MetaPreview shrank between the two dumps, so the gate passes:
Swap the two dumps, so the same change reads as growth of 450,170 instances, and it fails:
heaptrail --diff-from after.hprof --diff-to before.hprof --top 100000 --json --json-out reports/diff.json
jq -e '(map(select(.class_name == "com.nexio.tv.domain.model.MetaPreview")) | first) as $c | $c.delta_count <= 1000' reports/diff.json
If the class is not in the report at all, first returns null, and in jq null <= 1000 is true, so the gate passes without saying so. Make that explicit by naming the default: first // {delta_count: 0}. For a test such as >= 1000, a missing class would fail instead, so decide what "missing" should mean before you rely on it.
A script that keeps the two kinds of failure apart¶
A gate that fails the build must not confuse "the threshold was crossed" with "heaptrail could not read the dump". The exit statuses differ, so check each:
#!/usr/bin/env bash
set -uo pipefail
heaptrail -i heap.hprof --leak-suspects --exclude-soft-weak --top 1 \
--json --json-out reports/suspects.json > reports/suspects.txt
status=$?
if [ "$status" -ne 0 ]; then
echo "heaptrail failed with status $status" >&2 # 1 error, 2 bad arguments, 101 panic
tail -n 5 reports/suspects.txt >&2
exit 2
fi
if ! jq -e '.suspects[0].heap_share_pct < 30' reports/suspects.json > /dev/null; then
echo "A single object retains more than 30 % of the heap" >&2
cat reports/suspects.txt
exit 1
fi
The text report is kept in reports/suspects.txt, so a failed gate leaves the evidence in the job log. Upload reports/ as a build artifact, and do not upload the dump unless you have decided to. See
Handle dumps safely.
How it works¶
--json writes the same numbers as the text report. jq -e evaluates a condition and sets the exit status from the result. That is all a gate needs: the numbers, a comparison, and an exit status. The comparison
lives in your pipeline, where it is simple to change, and not in heaptrail.
Variations¶
- Gate on a percentage and on bytes. Combine them:
jq -e '.suspects[0].heap_share_pct < 30 and .suspects[0].retained_bytes < 50000000'. - Gate on the dominating class.
jq -e '.suspects[0].accumulating_class != "com.example.Cache"'fails when a known cache is the biggest thing in the heap. - Gate a series.
jq -e '.monotonic_growth | length < 5'with--diff-seriesfails when too many classes only grow. - Obfuscated builds. Add
--mapping, so the class names in your conditions are real. - Avoid noisy baselines. A heap changes between runs, so leave a margin, and compare against a dump from the same build.
Gotchas¶
- A runner needs the RAM.
--leak-suspectskeeps the whole graph in memory, about five to seven and a half times the dump size. Plan the runner for that. See Performance. - Garbage can rank. On a JVM, use
jmap -dump:liveso an unreachable object does not become the suspect. See Leak suspects. - The threshold is yours. A cache that is meant to hold 30 % of the heap will trip a 30 % gate every time. Gate on what should be small.
jqis not part of heaptrail. Install it in the job.
Related¶
- JSON output explains the files.
- Errors and exit codes lists every status.
- Leak suspects explains the numbers you are gating on.