CI: Improve RTLMeter PR report

- If any simulated cycle counts mismatch, report them and fail the job
- Ignore "Elapsed" and "CPU" samples less than 30 seconds - too noisy
- Widen acceptable ranges on "Elapsed" and "Memory"
- Add badge legend
This commit is contained in:
Geza Lore 2026-06-08 19:50:33 +01:00
parent 96336395d6
commit 698c82bbcd
3 changed files with 72 additions and 31 deletions

View File

@ -343,14 +343,17 @@ jobs:
path: verilator path: verilator
- name: Create report - name: Create report
id: report
working-directory: verilator working-directory: verilator
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
ln -s ../rtlmeter rtlmeter ln -s ../rtlmeter rtlmeter
gh repo set-default ${{ github.repository }} gh repo set-default ${{ github.repository }}
# Compare to last successful scheduled run # Create run report - save status to fail job if the script did
ci/ci-rtlmeter-report.bash ${{ github.run_id }} ${{ github.sha }} ${{ needs.combine-results.outputs.run-tags }} STATUS=0
ci/ci-rtlmeter-report.bash ${{ github.run_id }} ${{ github.sha }} ${{ needs.combine-results.outputs.run-tags }} || STATUS=$?
echo "status=$STATUS" >> "$GITHUB_OUTPUT"
# Create the report artifact # Create the report artifact
mkdir ../report-artifact mkdir ../report-artifact
mv rtlmeter-report/report ../report-artifact/ mv rtlmeter-report/report ../report-artifact/
@ -372,6 +375,9 @@ jobs:
path: notification-artifact path: notification-artifact
name: pr-notification name: pr-notification
- name: Report status
run: exit ${{ steps.report.outputs.status }}
# Create GitHub issue for failed scheduled jobs # Create GitHub issue for failed scheduled jobs
# This should always be the last job (we want an issue if anything breaks) # This should always be the last job (we want an issue if anything breaks)
create-issue: create-issue:

View File

@ -82,8 +82,9 @@ for r in $RUNS; do
awk -v RS= -v prefix=$TMP_DIR/$r-frag '{print > sprintf("%s-execute-%02d.txt" ,prefix,NR)}' $TMP_DIR/execute-$r.txt awk -v RS= -v prefix=$TMP_DIR/$r-frag '{print > sprintf("%s-execute-%02d.txt" ,prefix,NR)}' $TMP_DIR/execute-$r.txt
done done
# Create summary # Create summary, suppress failure, but save the status reported to pass back.
venv/bin/python3 $SCRIPT_DIR/ci-rtlmeter-report.py ${SUMMARY_ARGS[@]} > $TMP_DIR/summary.txt STATUS=0
venv/bin/python3 $SCRIPT_DIR/ci-rtlmeter-report.py ${SUMMARY_ARGS[@]} > $TMP_DIR/summary.txt || STATUS=$?
# Print it # Print it
cat $TMP_DIR/summary.txt cat $TMP_DIR/summary.txt
@ -158,3 +159,5 @@ $(cat ${TMP_DIR}/body.html)
</html> </html>
INDEX_TEMPLATE INDEX_TEMPLATE
exit $STATUS

View File

@ -33,21 +33,29 @@ def printTable(table: List[List[str]], **kwargs) -> None:
# fmt: off # fmt: off
stepMetric = ( stepMetric = (
("verilate", "elapsed"), # Step, Metric, Status Brackets
("verilate", "memory"), ("execute", "speed", ("", 0.96, "⚠️", 0.98, "", 1.02, "💡", 1.04, "")),
("verilate", "cpu"), ("execute", "memory", ("", 0.90, "⚠️", 0.95, "", 1.05, "💡", 1.10, "")),
("cppbuild", "elapsed"), ("verilate", "cpu", ("", 0.96, "⚠️", 0.98, "", 1.02, "💡", 1.04, "")),
("cppbuild", "memory"), ("verilate", "memory", ("", 0.90, "⚠️", 0.95, "", 1.05, "💡", 1.10, "")),
("cppbuild", "cpu"), ("cppbuild", "cpu", ("", 0.96, "⚠️", 0.98, "", 1.02, "💡", 1.04, "")),
("cppbuild", "codeSize"), ("cppbuild", "memory", ("", 0.90, "⚠️", 0.95, "", 1.05, "💡", 1.10, "")),
("execute", "speed"), ("cppbuild", "codeSize",("", 0.96, "⚠️", 0.98, "", 1.02, "💡", 1.04, "")),
("execute", "clocks"), ("cppbuild", "elapsed", ("", 0.70, "⚠️", 0.85, "", 1.15, "💡", 1.30, "")),
("execute", "memory"),
("execute", "cpu"),
) )
badgeLegend = [
("", "Likely regression"),
("⚠️", "Possible regression"),
("", "Within acceptable range"),
("💡", "Possible improvement"),
("", "Likely improvement"),
]
# fmt: on # fmt: on
changedCycles = []
table = [] table = []
badgesToExplain = set()
for ref, cmp in zip(sys.argv[1::2], sys.argv[2::2]): for ref, cmp in zip(sys.argv[1::2], sys.argv[2::2]):
with open(ref, "r", encoding="utf-8") as f: with open(ref, "r", encoding="utf-8") as f:
@ -57,7 +65,17 @@ for ref, cmp in zip(sys.argv[1::2], sys.argv[2::2]):
if table: if table:
table.append(tabulate.SEPARATING_LINE) table.append(tabulate.SEPARATING_LINE)
runName = ref_json["runName"] runName = ref_json["runName"]
for step, metric in stepMetric:
# Check simulated cycles match - it's ok to crash if this entry does not exist
for entry in cmp_json["execute"]["clocks"]["table"]:
case, _, _, (refCycles, _), (newCycles, _), _, _ = entry
refCycles = int(refCycles)
newCycles = int(newCycles)
if refCycles != newCycles:
changedCycles.append([runName, case, refCycles, newCycles])
# Check metrics
for step, metric, brackets in stepMetric:
if step not in cmp_json: if step not in cmp_json:
continue continue
data = cmp_json[step] data = cmp_json[step]
@ -68,7 +86,11 @@ for ref, cmp in zip(sys.argv[1::2], sys.argv[2::2]):
maxGain = float("-inf") maxGain = float("-inf")
meanGain = 1 meanGain = 1
count = 0 count = 0
for _, _, _, _, _, g, _ in data["table"]: for _, _, _, (a, _), (b, _), g, _ in data["table"]:
# for wall clock and CPU time, ignore small values that just add noise
if metric == "elapsed" or metric == "cpu":
if a < 30 or b < 30:
continue
minGain = min(minGain, g) minGain = min(minGain, g)
maxGain = max(maxGain, g) maxGain = max(maxGain, g)
meanGain *= g meanGain *= g
@ -77,29 +99,39 @@ for ref, cmp in zip(sys.argv[1::2], sys.argv[2::2]):
continue continue
meanGain = meanGain**(1 / count) meanGain = meanGain**(1 / count)
if metric == "clocks": status = brackets[0]
# Clock cycles should match exactly for limit, badge in zip(brackets[1::2], brackets[2::2]):
status = "" if minGain != 1 or maxGain != 1 else "" if meanGain >= limit:
else: status = badge
# Otherwise use some arbitrary brackets badgesToExplain.add(status)
status = ""
if (meanGain > 0.95):
status = "⚠️"
if (meanGain > 0.98):
status = ""
if (meanGain > 1.02):
status = "💡"
if (meanGain > 1.05):
status = ""
table.append([ table.append([
runName, step, ref_json["metrics"][metric]["header"], f"{meanGain:.2f}x {status} ", runName, step, ref_json["metrics"][metric]["header"], f"{meanGain:.2f}x {status} ",
f"{minGain:.2f}x", f"{maxGain:.2f}x", f"{count}" f"{minGain:.2f}x", f"{maxGain:.2f}x", f"{count}"
]) ])
# Print changed cycles if any
if changedCycles:
print("❌ simulated cycles changed (must be fixed):")
printTable(changedCycles,
headers=("Run", "Case", "Old Cycles", "New Cycles"),
colalign=("left", "left", "right", "right"),
disable_numparse=True)
print()
# Print results
printTable( printTable(
table, table,
headers=("Run", "Step", "Metric", "Improvement", "Min", "Max", "Samples"), headers=("Run", "Step", "Metric", "Improvement", "Min", "Max", "Samples"),
colalign=("left", "left", "left", "right", "right", "right", "right"), colalign=("left", "left", "left", "right", "right", "right", "right"),
disable_numparse=True, disable_numparse=True,
) )
# Explain badges
print()
for badge, legend in badgeLegend:
if badge in badgesToExplain:
print(f" {badge} : {legend}")
# Fail job if status changed
sys.exit(0 if not changedCycles else 1)