CI: Show summary tables in 'pr: rtlmeter' results

This commit is contained in:
Geza Lore 2026-06-06 10:19:21 +01:00
parent 680ef8dda9
commit 067cd6c9c6
10 changed files with 428 additions and 156 deletions

View File

@ -182,7 +182,7 @@ jobs:
uses: actions/upload-artifact@v7
with:
path: repo/notification
name: coverage-pr-notification
name: pr-notification
# Create GitHub issue for failed scheduled jobs
# This should always be the last job (we want an issue if anything breaks)

View File

@ -10,7 +10,7 @@ on:
paths: ["ci/**", ".github/workflows"]
workflow_dispatch:
workflow_run:
workflows: ["Code coverage"]
workflows: ["Code coverage", "RTLMeter"]
types: [completed]
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
@ -35,7 +35,7 @@ jobs:
name: Build content
runs-on: ubuntu-24.04
outputs:
coverage-pr-run-ids: ${{ steps.build.outputs.coverage-pr-run-ids }}
pr-run-ids: ${{ steps.build.outputs.pr-run-ids }}
steps:
- name: Checkout
uses: actions/checkout@v6
@ -83,5 +83,5 @@ jobs:
- name: Comment on PR
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
COVERAGE_PR_RUN_IDS: ${{ needs.build.outputs.coverage-pr-run-ids }}
PR_RUN_IDS: ${{ needs.build.outputs.pr-run-ids }}
run: bash -x ./ci/ci-pages-notify.bash

View File

@ -1,48 +0,0 @@
---
# DESCRIPTION: Github actions config
# This name is key to badges in README.rst, so we use the name build
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
name: RTLMeter PR results
on:
workflow_run:
workflows: [RTLMeter]
types: [completed]
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }}
permissions:
actions: read
pull-requests: write
steps:
- name: Download report
uses: actions/download-artifact@v8
with:
name: rtlmeter-pr-results
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download PR number
uses: actions/download-artifact@v8
with:
name: pr-number
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
# Use the Verilator CI app to post the comment
- name: Generate access token
id: generate-token
uses: actions/create-github-app-token@v3.2.0
with:
app-id: ${{ vars.VERILATOR_CI_ID }}
private-key: ${{ secrets.VERILATOR_CI_KEY }}
permission-pull-requests: write
- name: Comment on PR
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |-
ls -la
cat report.txt
gh pr --repo ${{ github.repository }} comment $(cat pr-number.txt) --body-file report.txt

View File

@ -286,106 +286,45 @@ jobs:
with:
repository: "verilator/rtlmeter"
path: rtlmeter
- name: Setup RTLMeter venv
working-directory: rtlmeter
run: make venv
- name: Download combined results
uses: actions/download-artifact@v8
- name: Checkout Verilator
uses: actions/checkout@v6
with:
pattern: all-results-*
path: all-results
merge-multiple: true
- name: Get scheduled run info
id: scheduled-info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ID=$(gh run --repo ${{ github.repository }} list --workflow RTLMeter --event schedule --status success --limit 1 --json databaseId --jq ".[0].databaseId")
echo "id=$ID" >> $GITHUB_OUTPUT
URL=$(gh run --repo ${{ github.repository }} view $ID --json url --jq ".url")
echo "url=$URL" >> $GITHUB_OUTPUT
NUM=$(gh run --repo ${{ github.repository }} view $ID --json number --jq ".number")
echo "num=$NUM" >> $GITHUB_OUTPUT
DATE=$(gh run --repo ${{ github.repository }} view $ID --json createdAt --jq ".createdAt")
echo "date=$DATE" >> $GITHUB_OUTPUT
- name: Download scheduled run results
uses: actions/download-artifact@v8
with:
name: published-results
path: nightly-results
run-id: ${{ steps.scheduled-info.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Compare results
working-directory: rtlmeter
run: |
for tag in gcc clang gcc-hier; do
ADATA=../nightly-results/all-results-${tag}.json
BDATA=../all-results/all-results-${tag}.json
touch ../verilate-${tag}.txt
touch ../execute-${tag}.txt
touch ../cppbuild-${tag}.txt
if [[ ! -e $ADATA ]]; then
continue
fi
./rtlmeter compare --cases '* !Example:* !*:hello' --steps "verilate" --metrics "elapsed memory" $ADATA $BDATA > ../verilate-${tag}.txt
cat ../verilate-${tag}.txt
./rtlmeter compare --cases '* !Example:* !*:hello' --steps "execute" --metrics "speed memory elapsed" $ADATA $BDATA > ../execute-${tag}.txt
cat ../execute-${tag}.txt
./rtlmeter compare --cases '* !Example:* !*:hello' --steps "cppbuild" --metrics "elapsed memory cpu codeSize" $ADATA $BDATA > ../cppbuild-${tag}.txt
cat ../cppbuild-${tag}.txt
done
path: verilator
- name: Create report
working-directory: verilator
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -x
NUM=$(gh run --repo ${{ github.repository }} view ${{ github.run_id }} --json number --jq ".number")
URL=$(gh run --repo ${{ github.repository }} view ${{ github.run_id }} --json url --jq ".url")
echo -n "Performance metrics for PR workflow [#$NUM]($URL) (B) compared to scheduled run" > report.txt
echo -n " [#${{ steps.scheduled-info.outputs.num }}](${{ steps.scheduled-info.outputs.url }}) (A)" >> report.txt
echo " from ${{ steps.scheduled-info.outputs.date }}" >> report.txt
for tag in gcc clang gcc-hier; do
echo "" >> report.txt
if [[ $tag == "gcc" ]]; then
echo "<details open>" >> report.txt
else
echo "<details>" >> report.txt
fi
echo -n "<summary><strong><em>" >> report.txt
jq -rj ".[0].runName" all-results/all-results-${tag}.json >> report.txt
echo "</em></strong></summary>" >> report.txt
awk -v RS= -v tag=${tag} '{print > sprintf("frag-%02d-verilate-%s.txt",NR,tag)}' verilate-${tag}.txt
awk -v RS= -v tag=${tag} '{print > sprintf("frag-%02d-execute-%s.txt" ,NR,tag)}' execute-${tag}.txt
awk -v RS= -v tag=${tag} '{print > sprintf("frag-$02d-cppbuild-%s.txt",NR,tag)}' cppbuild-${tag}.txt
for f in $(ls -1 frag-*-verilate-${tag}.txt | sort) $(ls -1 frag-*-execute-${tag}.txt | sort) $(ls -1 frag-*-cppbuild-${tag}.txt | sort); do
if [[ $f == frag-01-verilate-${tag}.txt || $f == frag-01-execute-${tag}.txt ]]; then
echo "<details open>" >> report.txt
else
echo "<details>" >> report.txt
fi
echo -n "<summary>" >> report.txt
head -n 1 $f | tr -d '\n' >> report.txt
echo "</summary>" >> report.txt
echo '<pre>' >> report.txt
tail -n +2 $f >> report.txt
echo '</pre>' >> report.txt
echo "</details>" >> report.txt
done
echo "</details>" >> report.txt
done
cat report.txt
ln -s ../rtlmeter rtlmeter
gh repo set-default ${{ github.repository }}
# Compare to last successful scheduled run
REF_ID=$(gh run list --workflow RTLMeter --event schedule --status success --limit 1 --json databaseId --jq ".[0].databaseId")
NEW_ID=${{ github.run_id }}
ci/ci-rtlmeter-pr-report.bash $REF_ID $NEW_ID gcc clang gcc-hier
mv rtlmeter-pr-report/report.txt ../
cat ../report.txt
# Generate notification comment content
mkdir -p ../notification
mv rtlmeter-pr-report/notification.txt ../notification/body.txt
echo ${{ github.event.number }} > ../notification/pr-number.txt
- name: Upload report
uses: actions/upload-artifact@v7
with:
path: report.txt
name: rtlmeter-pr-results
- name: Save PR number
run: echo ${{ github.event.number }} > pr-number.txt
- name: Upload PR number
name: rtlmeter-pr-report
- name: Upload notification
uses: actions/upload-artifact@v7
with:
path: pr-number.txt
name: pr-number
path: notification
name: pr-notification
# Create GitHub issue for failed scheduled jobs
# This should always be the last job (we want an issue if anything breaks)

View File

@ -535,6 +535,7 @@ PY_PROGRAMS = \
# Python files, subject to format but not lint
PY_FILES = \
$(PY_PROGRAMS) \
ci/*.py \
test_regress/t/*.py \
# Python files, test_regress tests

View File

@ -4,7 +4,7 @@
# SPDX-FileCopyrightText: 2025 Geza Lore
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
# Notify PRs via comment that their coverage reports are available
# Notify PRs via comment that their workflow reports are available
# Get the current repo URL - might differ on a fork
readonly REPO_URL=$(gh repo view --json url --jq .url)
@ -13,7 +13,7 @@ readonly REPO_URL=$(gh repo view --json url --jq .url)
ARTIFACTS_ROOT=artifacts
mkdir -p ${ARTIFACTS_ROOT}
for RUN_ID in ${COVERAGE_PR_RUN_IDS//,/ }; do
for RUN_ID in ${PR_RUN_IDS//,/ }; do
echo "@@@ Processing run ${RUN_ID}"
# Create workflow artifacts directory
@ -21,7 +21,7 @@ for RUN_ID in ${COVERAGE_PR_RUN_IDS//,/ }; do
mkdir -p ${ARTIFACTS_DIR}
# Download artifact of this run, if exists
gh run download ${RUN_ID} --name coverage-pr-notification --dir ${ARTIFACTS_DIR} || true
gh run download ${RUN_ID} --name pr-notification --dir ${ARTIFACTS_DIR} || true
ls -lsha ${ARTIFACTS_DIR}
# Move on if no notification is required
@ -35,7 +35,7 @@ for RUN_ID in ${COVERAGE_PR_RUN_IDS//,/ }; do
gh pr comment $(cat ${ARTIFACTS_DIR}/pr-number.txt) --body-file ${ARTIFACTS_DIR}/body.txt
# Get the artifact ID
ARTIFACT_ID=$(gh api "repos/{owner}/{repo}/actions/runs/${RUN_ID}/artifacts" --jq '.artifacts[] | select(.name == "coverage-pr-notification") | .id')
ARTIFACT_ID=$(gh api "repos/{owner}/{repo}/actions/runs/${RUN_ID}/artifacts" --jq '.artifacts[] | select(.name == "pr-notification") | .id')
# Delete it, so we only notify once
gh api --method DELETE "repos/{owner}/{repo}/actions/artifacts/${ARTIFACT_ID}"

View File

@ -5,8 +5,7 @@
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
# This scipt build the content of the GitHub Pages for the repository.
# Currently this only hosts code coverage reports, but it would be possible to
# add any other contents to the page in parallel here.
# Currently this hosts code coverage reports, and PR RTLMeter detailed reports.
# Developer note: You should be able to run this script in your local checkout
# if you have GitHub CLI (command 'gh') setup, authenticated ('gh auth login'),
@ -25,6 +24,9 @@ if [[ -z "$GITHUB_OUTPUT" ]]; then
GITHUB_OUTPUT=github-output.txt
fi
# Run IDs of PR jobs processed
PR_RUN_IDS=""
# Populates ${PAGES_ROOT}/coverage-reports
compile_coverage_reports() {
# We will process all runs up to and including this date. This is chosen to be
@ -53,9 +55,6 @@ compile_coverage_reports() {
local CONTENTS=contents.tmp
echo > ${CONTENTS}
# Run IDs of PR jobs processed
local PR_RUN_IDS=""
# Iterate over all unique event types that triggered the workflows
for EVENT in $(jq -r 'map(.event) | sort | unique | .[]' completedRuns.json); do
echo "@@@ Processing '${EVENT}' runs"
@ -73,11 +72,7 @@ compile_coverage_reports() {
# Record run ID of PR job
if [[ $EVENT == "pull_request" ]]; then
if [[ -z "$PR_RUN_IDS" ]]; then
PR_RUN_IDS="$RUN_ID"
else
PR_RUN_IDS="$PR_RUN_IDS,$RUN_ID"
fi
PR_RUN_IDS="$PR_RUN_IDS $RUN_ID"
fi
# Create workflow artifacts directory
@ -148,7 +143,7 @@ CONTENTS_TEMPLATE
<body>
$(cat ${CONTENTS})
<h4>Assembled $(date --iso-8601=minutes --utc)</h1>
<h4>Assembled $(date --iso-8601=minutes --utc)</h4>
<body>
</html>
@ -156,12 +151,159 @@ INDEX_TEMPLATE
# Report size
du -shc ${COVERAGE_ROOT}/*
}
# Set output
echo "coverage-pr-run-ids=${PR_RUN_IDS}" >> $GITHUB_OUTPUT
# Populates ${PAGES_ROOT}/rtlmeter-reports
compile_rtlmeter_reports() {
# We will process all runs up to and including this date. This is chosen to be
# slightly less than the artifact retention period for simplicity.
local OLDEST=$(date --date="28 days ago" --iso-8601=date)
# Gather all RTLMeter workflow runs within the time window
gh run list -w rtlmeter.yml --limit 1000 --created ">=${OLDEST}" --json "databaseId,event,status,conclusion,createdAt,number" > recentRuns.json
echo @@@ Recent runs:
jq "." recentRuns.json
# Select completd runs that were not cancelled or skipped, sort by descending run number
jq 'sort_by(-.number) | map(select(.status == "completed" and (.conclusion == "success" or .conclusion == "failure")))' recentRuns.json > completedRuns.json
echo @@@ Completed with success or failure:
jq "." completedRuns.json
# Create artifacts root directory
local ARTIFACTS_ROOT=artifacts
mkdir -p ${ARTIFACTS_ROOT}
# Create rtlmeter reports root directory
local RTLMETER_ROOT=${PAGES_ROOT}/rtlmeter-reports
mkdir -p ${RTLMETER_ROOT}
# Create index page contents fragment
local CONTENTS=contents.tmp
echo > ${CONTENTS}
# Iterate over all unique event types that triggered the workflows
for EVENT in $(jq -r 'map(.event) | sort | unique | .[]' completedRuns.json); do
echo "@@@ Processing '${EVENT}' runs"
# Emit section header if a report exists with this event type
EMIT_SECTION_HEADER=1
# For each worfklow run that was triggered by this event type
for RUN_ID in $(jq ".[] | select(.event == \"${EVENT}\") |.databaseId" completedRuns.json); do
echo "@@@ Processing run ${RUN_ID}"
# Extract the info of this run
jq ".[] | select(.databaseId == $RUN_ID)" completedRuns.json > workflow.json
jq "." workflow.json
# Record run ID of PR job
if [[ $EVENT == "pull_request" ]]; then
PR_RUN_IDS="$PR_RUN_IDS $RUN_ID"
fi
# Create workflow artifacts directory
local ARTIFACTS_DIR=${ARTIFACTS_ROOT}/${RUN_ID}
mkdir -p ${ARTIFACTS_DIR}
# Download artifacts of this run, if exists
gh run download ${RUN_ID} --name rtlmeter-pr-report --dir ${ARTIFACTS_DIR} || true
ls -lsha ${ARTIFACTS_DIR}
# Move on if no RTLMeter report is available
if [ ! -f ${ARTIFACTS_DIR}/report.txt ]; then
echo "No RTLMeter report found"
continue
fi
echo "RTLMeter report found"
# Emit section header
if [[ -n $EMIT_SECTION_HEADER ]]; then
unset EMIT_SECTION_HEADER
echo "<h4>RTLMeter reports for '${EVENT}' runs:</h4>" >> ${CONTENTS}
fi
# Extract run metadata
local WORKFLOW_CREATED=$(jq -r '.createdAt' workflow.json)
local WOFKRLOW_NUMBER=$(jq -r '.number' workflow.json)
# Wrap the report into an HTML page. The report content is already HTML
# (produced by ci-rtlmeter-pr-report.bash), so we just embed it in the
# page body.
cat > ${RTLMETER_ROOT}/${RUN_ID}.html <<REPORT_TEMPLATE
<html>
<head>
<title>Verilator RTLMeter report #${WOFKRLOW_NUMBER}</title>
<style>
body {
font-family: courier, serif;
background-color: #f3f3f3;
a {
color: #008fd7;
}
}
</style>
</head>
<body>
$(cat ${ARTIFACTS_DIR}/report.txt)
</body>
</html>
REPORT_TEMPLATE
# Add index page content
cat >> ${CONTENTS} <<CONTENTS_TEMPLATE
Run <a href="${RUN_ID}.html">#${WOFKRLOW_NUMBER}</a>
| GitHub: <a href="${REPO_URL}/actions/runs/${RUN_ID}">${RUN_ID}</a>
| started at: ${WORKFLOW_CREATED}
CONTENTS_TEMPLATE
echo "<br>" >> ${CONTENTS}
done
# Section break
if [[ -z "$EMIT_SECTION_HEADER" ]]; then
echo "<hr>" >> ${CONTENTS}
fi
done
# Write rtlmeter report index.html
cat > ${RTLMETER_ROOT}/index.html <<INDEX_TEMPLATE
<html>
<head>
<title>Verilator CI RTLMeter reports</title>
<style>
body {
font-family: courier, serif;
background-color: #f3f3f3;
a {
color: #008fd7;
}
}
</style>
</head>
<body>
$(cat ${CONTENTS})
<h4>Assembled $(date --iso-8601=minutes --utc)</h4>
<body>
</html>
INDEX_TEMPLATE
# Report size
du -shc ${RTLMETER_ROOT}/*
}
# Compilie coverage reports
compile_coverage_reports;
# Compile RTLMeter reports
compile_rtlmeter_reports;
# You can build any other content here to be put under ${PAGES_ROOT}
# Set output to all affected unique PR run IDs (comma separated, no trailing comma)
IDS=$(echo ${PR_RUN_IDS} | xargs -n1 | sort -u | paste -sd,)
echo "pr-run-ids=${IDS}" >> $GITHUB_OUTPUT

135
ci/ci-rtlmeter-pr-report.bash Executable file
View File

@ -0,0 +1,135 @@
#!/usr/bin/env bash
# DESCRIPTION: Verilator: CI script for 'rtlmeter.yml' PR results
#
# SPDX-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
# This scipt builds the content of the response comment posten on PRs
# at the end of RTLMeter runs.
# Developer note: You should be able to run this script in your local checkout
# if you have GitHub CLI (command 'gh') setup, authenticated ('gh auth login'),
# and have set a default repository ('gh repo set-default').
# Trace when running in the CI
[ "$GITHUB_ACTIONS" != "true" ] || set -x
# Arguments:
# 1. reference run ID
# 2. new run ID
# rest: run tags
REF_ID=$1; shift
NEW_ID=$1; shift
RUNS="$@"
# $VERILATOR_CHECKOUT/ci directory
SCRIPT_DIR=$(readlink -f $(dirname ${BASH_SOURCE[0]}))
# Move into a temporary directory
rm -rf rtlmeter-pr-report
mkdir rtlmeter-pr-report
pushd rtlmeter-pr-report &> /dev/null
TMP_DIR=$(readlink -f .)
# Artifacts to download
DOWNLOAD_ARTIFACTS=""
for r in $RUNS; do
DOWNLOAD_ARTIFACTS="$DOWNLOAD_ARTIFACTS --name all-results-$r"
done
# Download reference run results
mkdir ref
REF_DIR=$(readlink -f ref)
gh run download ${REF_ID} $DOWNLOAD_ARTIFACTS --dir $REF_DIR
mv $REF_DIR/*/*.json $REF_DIR/
find $REF_DIR -mindepth 1 -type d -delete
# Download current run results
mkdir new
NEW_DIR=$(readlink -f new)
gh run download ${NEW_ID} $DOWNLOAD_ARTIFACTS --dir $NEW_DIR
mv $NEW_DIR/*/*.json $NEW_DIR/
find $NEW_DIR -mindepth 1 -type d -delete
# Get Some metadata about the runs
REF_URL=$(gh run view $REF_ID --json url --jq ".url")
REF_NUM=$(gh run view $REF_ID --json number --jq ".number")
REF_DATE=$(gh run view $REF_ID --json createdAt --jq ".createdAt")
NEW_URL=$(gh run view $NEW_ID --json url --jq ".url")
NEW_NUM=$(gh run view $NEW_ID --json number --jq ".number")
# Go back to root directory
popd &> /dev/null
# Go to RTLMeter directory
cd rtlmeter
# Compare results
SUMMARY_ARGS=()
for r in $RUNS; do
CMP_JSON=$TMP_DIR/cmp-$r.json
# Gather args for summary script
SUMMARY_ARGS+=($REF_DIR/all-results-$r.json $CMP_JSON)
# Create JSON
./rtlmeter compare --format json --steps "*" --metrics "*" \
$REF_DIR/all-results-$r.json $NEW_DIR/all-results-$r.json > $CMP_JSON
# Also create detailed tables
./rtlmeter compare --format ascii --steps 'verilate' --metrics '* !system !user' $REF_DIR/all-results-$r.json $NEW_DIR/all-results-$r.json > $TMP_DIR/verilate-$r.txt
./rtlmeter compare --format ascii --steps 'cppbuild' --metrics '* !system !user' $REF_DIR/all-results-$r.json $NEW_DIR/all-results-$r.json > $TMP_DIR/cppbuild-$r.txt
./rtlmeter compare --format ascii --steps 'execute' --metrics '* !system !user' $REF_DIR/all-results-$r.json $NEW_DIR/all-results-$r.json > $TMP_DIR/execute-$r.txt
# Chop them at new lines, into one table per file
awk -v RS= -v prefix=$TMP_DIR/$r-frag '{print > sprintf("%s-verilate-%02d.txt",prefix,NR)}' $TMP_DIR/verilate-$r.txt
awk -v RS= -v prefix=$TMP_DIR/$r-frag '{print > sprintf("%s-cppbuild-%02d.txt",prefix,NR)}' $TMP_DIR/cppbuild-$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
# Create summary
venv/bin/python3 $SCRIPT_DIR/ci-rtlmeter-pr-report.py ${SUMMARY_ARGS[@]} > $TMP_DIR/summary.txt
# Print it
cat $TMP_DIR/summary.txt
# Create notification comment content
NOTIFICATION=$TMP_DIR/notification.txt
cat > $NOTIFICATION <<NOTIFICATION_TEMPLATE
Performance metrics for PR workflow [#$NEW_NUM]($NEW_URL) (B)
compared to scheduled run [#$REF_NUM]($REF_URL) (A) from $REF_DATE
<details open>
<summary><strong>Summary of all runs</strong></summary>
<pre>
$(cat $TMP_DIR/summary.txt)
</pre>
</details>
Blah Blah Blah
NOTIFICATION_TEMPLATE
# Create detailed report
REPORT=$TMP_DIR/report.txt
cat > $REPORT <<SUMMARY_TEMPLATE
Preamble Preamble
<details open>
<summary><strong>Summary of all runs</strong></summary>
<pre>
$(cat $TMP_DIR/summary.txt)
</pre>
</details>
SUMMARY_TEMPLATE
echo "<details>" >> $REPORT
echo " <summary><strong>Detailed results</strong></summary>" >> $REPORT
for r in $RUNS; do
RUN_NAME=$(jq -rj ".[0].runName" $REF_DIR/all-results-$r.json)
echo " <details>" >> $REPORT
echo " <summary><strong><em>$RUN_NAME</em></strong></summary>" >> $REPORT
for f in $(ls -1 $TMP_DIR/$r-frag-verilate-*.txt | sort) \
$(ls -1 $TMP_DIR/$r-frag-cppbuild-*.txt | sort) \
$(ls -1 $TMP_DIR/$r-frag-execute-*.txt | sort); do
cat >> $REPORT <<DETAIL_TAMPLATE
<details>
<summary>$(head -n 1 $f | tr -d '\n')</summary>
<pre>
$(tail -n +2 $f)
</pre>
</details>
DETAIL_TAMPLATE
done
echo " </details>" >> $REPORT
done
echo "</details>" >> $REPORT

103
ci/ci-rtlmeter-pr-report.py Normal file
View File

@ -0,0 +1,103 @@
# DESCRIPTION: Verilator: CI script for 'rtlmeter.yml' PR results
#
# SPDX-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
###############################################################################
# This script runs with the venv of **RTLMeter**
###############################################################################
import sys
import json
import tabulate
from typing import Final, List
tabulate.PRESERVE_WHITESPACE = True
tabulate.MIN_PADDING = 0
_ASCII_TABLE_FORMAT: Final = tabulate.TableFormat(
lineabove=tabulate.Line("╒═", "", "═╤═", "═╕"),
linebelowheader=tabulate.Line("╞═", "", "═╪═", "═╡"),
linebelow=tabulate.Line("╘═", "", "═╧═", "═╛"),
headerrow=tabulate.DataRow("", "", ""),
datarow=tabulate.DataRow("", "", ""),
linebetweenrows=None,
padding=0,
with_header_hide=None,
)
def printTable(table: List[List[str]], **kwargs) -> None:
print(tabulate.tabulate(table, tablefmt=_ASCII_TABLE_FORMAT, **kwargs))
# fmt: off
stepMetric = (
("verilate", "elapsed"),
("verilate", "memory"),
("verilate", "cpu"),
("cppbuild", "elapsed"),
("cppbuild", "memory"),
("cppbuild", "cpu"),
("cppbuild", "codeSize"),
("execute", "speed"),
("execute", "clocks"),
("execute", "memory"),
("execute", "cpu"),
)
# fmt: on
table = []
for ref, cmp in zip(sys.argv[1::2], sys.argv[2::2]):
with open(ref, "r", encoding="utf-8") as f:
ref_json = json.load(f)[0]
with open(cmp, "r", encoding="utf-8") as f:
cmp_json = json.load(f)
if table:
table.append(tabulate.SEPARATING_LINE)
runName = ref_json["runName"]
for step, metric in stepMetric:
if step not in cmp_json:
continue
data = cmp_json[step]
if metric not in data:
continue
data = data[metric]
minGain = float("inf")
maxGain = float("-inf")
meanGain = 1
count = 0
for _, _, _, _, _, g, _ in data["table"]:
minGain = min(minGain, g)
maxGain = max(maxGain, g)
meanGain *= g
count += 1
meanGain = meanGain**(1 / count)
if metric == "clocks":
# Clock cycles should match exactly
status = "" if minGain != 1 or maxGain != 1 else ""
else:
# Otherwise use some arbitrary brackets
status = ""
if (meanGain > 0.95):
status = "⚠️"
if (meanGain > 0.98):
status = ""
if (meanGain > 1.02):
status = "💡"
if (meanGain > 1.05):
status = ""
table.append([
runName, step, ref_json["metrics"][metric]["header"], f"{meanGain:.2f}x {status} ",
f"{minGain:.2f}x", f"{maxGain:.2f}x"
])
printTable(
table,
headers=("Run", "Step", "Metric", "Improvement", "Min", "Max"),
colalign=("left", "left", "left", "right", "right", "right"),
disable_numparse=True,
)

View File

@ -13,7 +13,7 @@ test.scenarios('dist')
Tabs_Exempt_Re = r'(\.out$)|(/fstcpp)|(Makefile)|(\.mk$)|(\.mk\.in$)|test_regress/t/t_preproc\.v|install-sh'
Unicode_Exempt_Re = r'(Changes$|CONTRIBUTORS$|LICENSES?|contributors.rst$|spelling.txt$)'
Unicode_Exempt_Re = r'(Changes$|CONTRIBUTORS$|LICENSES?|contributors.rst$|spelling.txt$|ci-rtlmeter-pr-report.py)'
def get_source_files():