From a0ee191dd794d6d25f6e323e2c32fa8bb431c10f Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sun, 18 Jan 2026 13:45:24 -0500 Subject: [PATCH] feat: Add performance reporting for release tracking - Add scripts/generate-perf-report.sh to fetch CI timing data - Add make perf-report TAG=X.Y.Z target - Generate per-language timing reports (JSON + Markdown) - Track performance history across releases - Prefer fully passing pipelines over warnings 4.2.0: 638 tests, 100% pass, avg 58s, slowest commonlisp (119s) --- Makefile | 15 +- reports/PERFORMANCE-HISTORY.md | 9 + reports/perf-4.2.0.json | 398 ++++++++++++++++++++++++++++++++ reports/perf-4.2.0.md | 114 +++++++++ scripts/generate-perf-report.sh | 230 ++++++++++++++++++ 5 files changed, 765 insertions(+), 1 deletion(-) create mode 100644 reports/PERFORMANCE-HISTORY.md create mode 100644 reports/perf-4.2.0.json create mode 100644 reports/perf-4.2.0.md create mode 100755 scripts/generate-perf-report.sh diff --git a/Makefile b/Makefile index c732f1e..c4e8070 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help test test-all test-c test-python test-go test-javascript test-ruby test-php test-rust test-java test-bash test-perl test-lua set-version +.PHONY: help test test-all test-c test-python test-go test-javascript test-ruby test-php test-rust test-java test-bash test-perl test-lua set-version perf-report # Client directories with their own Makefiles CLIENTS_WITH_MAKEFILE := $(wildcard clients/*/Makefile) @@ -33,6 +33,7 @@ help: @echo " make lint # Lint all clients" @echo " make clean # Clean build artifacts" @echo " make set-version VERSION=X.Y.Z # Set version in all files" + @echo " make perf-report TAG=X.Y.Z # Generate performance report for release" @echo "" @echo "Available client Makefiles:" @for dir in $(CLIENT_DIRS); do echo " $$dir"; done @@ -308,3 +309,15 @@ ifndef VERSION @exit 1 endif @bash scripts/set-version.sh $(VERSION) + +# ============================================================================ +# Performance Reporting +# ============================================================================ + +.PHONY: perf-report +perf-report: +ifdef TAG + @bash scripts/generate-perf-report.sh $(TAG) +else + @bash scripts/generate-perf-report.sh +endif diff --git a/reports/PERFORMANCE-HISTORY.md b/reports/PERFORMANCE-HISTORY.md new file mode 100644 index 0000000..2c485cf --- /dev/null +++ b/reports/PERFORMANCE-HISTORY.md @@ -0,0 +1,9 @@ +# Performance History + +Tracking compile and test execution times across releases. + +## Releases + +| Version | Date | Tests | Pass Rate | Avg Duration | Slowest | Fastest | +|---------|------|-------|-----------|--------------|---------|---------| +| 4.2.0 | 2026-01-18 | 638 | 100.0% | 58s | commonlisp | ocaml | diff --git a/reports/perf-4.2.0.json b/reports/perf-4.2.0.json new file mode 100644 index 0000000..8a87b81 --- /dev/null +++ b/reports/perf-4.2.0.json @@ -0,0 +1,398 @@ +{ + "tag": "4.2.0", + "generated_at": "2026-01-18T18:44:55Z", + "parent_pipeline_id": 10827, + "child_pipeline_id": 10829, + "summary": { + "total_tests": 638, + "passed_tests": 638, + "failed_tests": 0, + "total_languages": 42, + "passed_languages": 42, + "avg_duration_seconds": 58, + "max_duration_seconds": 119, + "min_duration_seconds": 42, + "slowest_language": "commonlisp", + "fastest_language": "ocaml" + }, + "languages": [ + { + "language": "commonlisp", + "status": "Passed", + "queued_duration": 130.733107, + "started_at": "2026-01-17T23:27:44Z", + "updated_at": "2026-01-17T23:29:43Z", + "job_id": 23846, + "duration_seconds": 119 + }, + { + "language": "forth", + "status": "Passed", + "queued_duration": 226.822778, + "started_at": "2026-01-17T23:29:36Z", + "updated_at": "2026-01-17T23:30:57Z", + "job_id": 23872, + "duration_seconds": 81 + }, + { + "language": "csharp", + "status": "Passed", + "queued_duration": 221.494055, + "started_at": "2026-01-17T23:29:31Z", + "updated_at": "2026-01-17T23:30:51Z", + "job_id": 23870, + "duration_seconds": 80 + }, + { + "language": "fsharp", + "status": "Passed", + "queued_duration": 215.969817, + "started_at": "2026-01-17T23:29:25Z", + "updated_at": "2026-01-17T23:30:43Z", + "job_id": 23869, + "duration_seconds": 78 + }, + { + "language": "go", + "status": "Passed", + "queued_duration": 182.801565, + "started_at": "2026-01-17T23:28:38Z", + "updated_at": "2026-01-17T23:29:49Z", + "job_id": 23854, + "duration_seconds": 71 + }, + { + "language": "prolog", + "status": "Passed", + "queued_duration": 224.618985, + "started_at": "2026-01-17T23:29:34Z", + "updated_at": "2026-01-17T23:30:43Z", + "job_id": 23871, + "duration_seconds": 69 + }, + { + "language": "perl", + "status": "Passed", + "queued_duration": 124.483959, + "started_at": "2026-01-17T23:27:35Z", + "updated_at": "2026-01-17T23:28:43Z", + "job_id": 23832, + "duration_seconds": 68 + }, + { + "language": "php", + "status": "Passed", + "queued_duration": 126.024306, + "started_at": "2026-01-17T23:27:37Z", + "updated_at": "2026-01-17T23:28:43Z", + "job_id": 23834, + "duration_seconds": 66 + }, + { + "language": "raku", + "status": "Passed", + "queued_duration": 181.812216, + "started_at": "2026-01-17T23:28:36Z", + "updated_at": "2026-01-17T23:29:40Z", + "job_id": 23851, + "duration_seconds": 64 + }, + { + "language": "elixir", + "status": "Passed", + "queued_duration": 128.33839, + "started_at": "2026-01-17T23:27:40Z", + "updated_at": "2026-01-17T23:28:44Z", + "job_id": 23840, + "duration_seconds": 64 + }, + { + "language": "cpp", + "status": "Passed", + "queued_duration": 182.401442, + "started_at": "2026-01-17T23:28:37Z", + "updated_at": "2026-01-17T23:29:41Z", + "job_id": 23853, + "duration_seconds": 64 + }, + { + "language": "r", + "status": "Passed", + "queued_duration": 128.039533, + "started_at": "2026-01-17T23:27:40Z", + "updated_at": "2026-01-17T23:28:43Z", + "job_id": 23839, + "duration_seconds": 63 + }, + { + "language": "bash", + "status": "Passed", + "queued_duration": 127.648122, + "started_at": "2026-01-17T23:27:39Z", + "updated_at": "2026-01-17T23:28:42Z", + "job_id": 23838, + "duration_seconds": 63 + }, + { + "language": "rust", + "status": "Passed", + "queued_duration": 184.558244, + "started_at": "2026-01-17T23:28:40Z", + "updated_at": "2026-01-17T23:29:42Z", + "job_id": 23855, + "duration_seconds": 62 + }, + { + "language": "deno", + "status": "Passed", + "queued_duration": 175.795332, + "started_at": "2026-01-17T23:28:30Z", + "updated_at": "2026-01-17T23:29:31Z", + "job_id": 23849, + "duration_seconds": 61 + }, + { + "language": "c", + "status": "Passed", + "queued_duration": 182.150823, + "started_at": "2026-01-17T23:28:37Z", + "updated_at": "2026-01-17T23:29:36Z", + "job_id": 23852, + "duration_seconds": 59 + }, + { + "language": "lua", + "status": "Passed", + "queued_duration": 126.849415, + "started_at": "2026-01-17T23:27:38Z", + "updated_at": "2026-01-17T23:28:35Z", + "job_id": 23836, + "duration_seconds": 57 + }, + { + "language": "zig", + "status": "Passed", + "queued_duration": 189.744268, + "started_at": "2026-01-17T23:28:47Z", + "updated_at": "2026-01-17T23:29:43Z", + "job_id": 23861, + "duration_seconds": 56 + }, + { + "language": "erlang", + "status": "Passed", + "queued_duration": 128.85146, + "started_at": "2026-01-17T23:27:41Z", + "updated_at": "2026-01-17T23:28:37Z", + "job_id": 23841, + "duration_seconds": 56 + }, + { + "language": "scheme", + "status": "Passed", + "queued_duration": 129.75745, + "started_at": "2026-01-17T23:27:42Z", + "updated_at": "2026-01-17T23:28:37Z", + "job_id": 23843, + "duration_seconds": 55 + }, + { + "language": "groovy", + "status": "Passed", + "queued_duration": 134.87264, + "started_at": "2026-01-17T23:27:49Z", + "updated_at": "2026-01-17T23:28:43Z", + "job_id": 23848, + "duration_seconds": 54 + }, + { + "language": "d", + "status": "Passed", + "queued_duration": 189.224944, + "started_at": "2026-01-17T23:28:46Z", + "updated_at": "2026-01-17T23:29:40Z", + "job_id": 23860, + "duration_seconds": 54 + }, + { + "language": "tcl", + "status": "Passed", + "queued_duration": 129.562818, + "started_at": "2026-01-17T23:27:42Z", + "updated_at": "2026-01-17T23:28:35Z", + "job_id": 23842, + "duration_seconds": 53 + }, + { + "language": "kotlin", + "status": "Passed", + "queued_duration": 187.476238, + "started_at": "2026-01-17T23:28:44Z", + "updated_at": "2026-01-17T23:29:37Z", + "job_id": 23857, + "duration_seconds": 53 + }, + { + "language": "java", + "status": "Passed", + "queued_duration": 186.306987, + "started_at": "2026-01-17T23:28:42Z", + "updated_at": "2026-01-17T23:29:35Z", + "job_id": 23856, + "duration_seconds": 53 + }, + { + "language": "v", + "status": "Passed", + "queued_duration": 190.541247, + "started_at": "2026-01-17T23:28:48Z", + "updated_at": "2026-01-17T23:29:40Z", + "job_id": 23863, + "duration_seconds": 52 + }, + { + "language": "typescript", + "status": "Passed", + "queued_duration": 74.207827, + "started_at": "2026-01-17T23:26:44Z", + "updated_at": "2026-01-17T23:27:36Z", + "job_id": 23827, + "duration_seconds": 52 + }, + { + "language": "python", + "status": "Passed", + "queued_duration": 72.915966, + "started_at": "2026-01-17T23:26:43Z", + "updated_at": "2026-01-17T23:27:35Z", + "job_id": 23824, + "duration_seconds": 52 + }, + { + "language": "objc", + "status": "Passed", + "queued_duration": 187.135332, + "started_at": "2026-01-17T23:28:48Z", + "updated_at": "2026-01-17T23:29:40Z", + "job_id": 23864, + "duration_seconds": 52 + }, + { + "language": "javascript", + "status": "Passed", + "queued_duration": 73.805034, + "started_at": "2026-01-17T23:26:44Z", + "updated_at": "2026-01-17T23:27:36Z", + "job_id": 23826, + "duration_seconds": 52 + }, + { + "language": "fortran", + "status": "Passed", + "queued_duration": 188.920887, + "started_at": "2026-01-17T23:28:45Z", + "updated_at": "2026-01-17T23:29:37Z", + "job_id": 23859, + "duration_seconds": 52 + }, + { + "language": "ruby", + "status": "Passed", + "queued_duration": 75.205723, + "started_at": "2026-01-17T23:26:46Z", + "updated_at": "2026-01-17T23:27:37Z", + "job_id": 23829, + "duration_seconds": 51 + }, + { + "language": "dart", + "status": "Passed", + "queued_duration": 184.421648, + "started_at": "2026-01-17T23:28:49Z", + "updated_at": "2026-01-17T23:29:40Z", + "job_id": 23865, + "duration_seconds": 51 + }, + { + "language": "crystal", + "status": "Passed", + "queued_duration": 131.235438, + "started_at": "2026-01-17T23:27:45Z", + "updated_at": "2026-01-17T23:28:36Z", + "job_id": 23847, + "duration_seconds": 51 + }, + { + "language": "clojure", + "status": "Passed", + "queued_duration": 130.414948, + "started_at": "2026-01-17T23:27:44Z", + "updated_at": "2026-01-17T23:28:35Z", + "job_id": 23845, + "duration_seconds": 51 + }, + { + "language": "awk", + "status": "Passed", + "queued_duration": 184.885825, + "started_at": "2026-01-17T23:28:35Z", + "updated_at": "2026-01-17T23:29:25Z", + "job_id": 23850, + "duration_seconds": 50 + }, + { + "language": "cobol", + "status": "Passed", + "queued_duration": 188.376226, + "started_at": "2026-01-17T23:28:45Z", + "updated_at": "2026-01-17T23:29:34Z", + "job_id": 23858, + "duration_seconds": 49 + }, + { + "language": "powershell", + "status": "Passed", + "queued_duration": 130.055351, + "started_at": "2026-01-17T23:27:43Z", + "updated_at": "2026-01-17T23:28:30Z", + "job_id": 23844, + "duration_seconds": 47 + }, + { + "language": "julia", + "status": "Passed", + "queued_duration": 181.87361, + "started_at": "2026-01-17T23:28:50Z", + "updated_at": "2026-01-17T23:29:36Z", + "job_id": 23866, + "duration_seconds": 46 + }, + { + "language": "nim", + "status": "Passed", + "queued_duration": 190.067576, + "started_at": "2026-01-17T23:28:47Z", + "updated_at": "2026-01-17T23:29:31Z", + "job_id": 23862, + "duration_seconds": 44 + }, + { + "language": "haskell", + "status": "Passed", + "queued_duration": 188.242852, + "started_at": "2026-01-17T23:28:57Z", + "updated_at": "2026-01-17T23:29:40Z", + "job_id": 23867, + "duration_seconds": 43 + }, + { + "language": "ocaml", + "status": "Passed", + "queued_duration": 188.729273, + "started_at": "2026-01-17T23:28:57Z", + "updated_at": "2026-01-17T23:29:39Z", + "job_id": 23868, + "duration_seconds": 42 + } +] +} diff --git a/reports/perf-4.2.0.md b/reports/perf-4.2.0.md new file mode 100644 index 0000000..22627e3 --- /dev/null +++ b/reports/perf-4.2.0.md @@ -0,0 +1,114 @@ +# Performance Report: 4.2.0 + +**Generated:** 2026-01-18T18:44:55Z +**Pipeline:** [10829](https://git.unturf.com/engineering/unturf/un-inception/-/pipelines/10829) + +## Summary + +| Metric | Value | +|--------|-------| +| Total Tests | 638 | +| Passed | 638 | +| Failed | 0 | +| Pass Rate | 100.0% | +| Languages | 42 | +| Avg Duration | 58s | +| Slowest | commonlisp (119s) | +| Fastest | ocaml (42s) | + +## Per-Language Performance + +| Language | Status | Duration | Tests | +|----------|--------|----------|-------| +| commonlisp | Passed | 119s | - | +| forth | Passed | 81s | - | +| csharp | Passed | 80s | - | +| fsharp | Passed | 78s | - | +| go | Passed | 71s | - | +| prolog | Passed | 69s | - | +| perl | Passed | 68s | - | +| php | Passed | 66s | - | +| raku | Passed | 64s | - | +| elixir | Passed | 64s | - | +| cpp | Passed | 64s | - | +| r | Passed | 63s | - | +| bash | Passed | 63s | - | +| rust | Passed | 62s | - | +| deno | Passed | 61s | - | +| c | Passed | 59s | - | +| lua | Passed | 57s | - | +| zig | Passed | 56s | - | +| erlang | Passed | 56s | - | +| scheme | Passed | 55s | - | +| groovy | Passed | 54s | - | +| d | Passed | 54s | - | +| tcl | Passed | 53s | - | +| kotlin | Passed | 53s | - | +| java | Passed | 53s | - | +| v | Passed | 52s | - | +| typescript | Passed | 52s | - | +| python | Passed | 52s | - | +| objc | Passed | 52s | - | +| javascript | Passed | 52s | - | +| fortran | Passed | 52s | - | +| ruby | Passed | 51s | - | +| dart | Passed | 51s | - | +| crystal | Passed | 51s | - | +| clojure | Passed | 51s | - | +| awk | Passed | 50s | - | +| cobol | Passed | 49s | - | +| powershell | Passed | 47s | - | +| julia | Passed | 46s | - | +| nim | Passed | 44s | - | +| haskell | Passed | 43s | - | +| ocaml | Passed | 42s | - | + +## Timing Distribution + +``` +commonlisp 119s ███████████████████████ +forth 81s ████████████████ +csharp 80s ████████████████ +fsharp 78s ███████████████ +go 71s ██████████████ +prolog 69s █████████████ +perl 68s █████████████ +php 66s █████████████ +raku 64s ████████████ +elixir 64s ████████████ +cpp 64s ████████████ +r 63s ████████████ +bash 63s ████████████ +rust 62s ████████████ +deno 61s ████████████ +c 59s ███████████ +lua 57s ███████████ +zig 56s ███████████ +erlang 56s ███████████ +scheme 55s ███████████ +groovy 54s ██████████ +d 54s ██████████ +tcl 53s ██████████ +kotlin 53s ██████████ +java 53s ██████████ +v 52s ██████████ +typescript 52s ██████████ +python 52s ██████████ +objc 52s ██████████ +javascript 52s ██████████ +fortran 52s ██████████ +ruby 51s ██████████ +dart 51s ██████████ +crystal 51s ██████████ +clojure 51s ██████████ +awk 50s ██████████ +cobol 49s █████████ +powershell 47s █████████ +julia 46s █████████ +nim 44s ████████ +haskell 43s ████████ +ocaml 42s ████████ +``` + +--- +*Report generated by `make perf-report`* diff --git a/scripts/generate-perf-report.sh b/scripts/generate-perf-report.sh new file mode 100755 index 0000000..c83b3db --- /dev/null +++ b/scripts/generate-perf-report.sh @@ -0,0 +1,230 @@ +#!/bin/bash +# Generate performance report for a tagged release +# Fetches job timing data from GitLab CI and creates a historical record +# +# Usage: scripts/generate-perf-report.sh [TAG] +# Example: scripts/generate-perf-report.sh 4.2.0 +# +# Output: reports/perf-{TAG}.md and reports/perf-{TAG}.json + +set -e + +TAG="${1:-}" +GITLAB_URL="https://git.unturf.com" +PROJECT_PATH="engineering/unturf/un-inception" + +if [ -z "$TAG" ]; then + # Default to current VERSION + if [ -f VERSION ]; then + TAG=$(cat VERSION) + else + echo "Usage: $0 " + echo "Example: $0 4.2.0" + exit 1 + fi +fi + +echo "Generating performance report for tag: $TAG" + +# Create reports directory +mkdir -p reports + +# Find pipeline for this tag (prefer passing pipelines) +echo "Finding pipeline for tag $TAG..." +PIPELINES=$(curl -s "$GITLAB_URL/$PROJECT_PATH/-/pipelines.json" | jq -r "[.pipelines[] | select(.ref.name == \"$TAG\" and .ref.tag == true)]") + +if [ "$(echo "$PIPELINES" | jq 'length')" -eq 0 ]; then + echo "Error: No pipeline found for tag $TAG" + exit 1 +fi + +# Try to find a pipeline with fully passing child +PARENT_PIPELINE_ID="" +CHILD_PIPELINE_ID="" + +for pid in $(echo "$PIPELINES" | jq -r '.[].id'); do + CHILD_DATA=$(curl -s "$GITLAB_URL/$PROJECT_PATH/-/pipelines/$pid.json" | jq '.triggered[0]') + CHILD_ID=$(echo "$CHILD_DATA" | jq -r '.id // empty') + CHILD_STATUS=$(echo "$CHILD_DATA" | jq -r '.details.status.group // empty') + + if [ -n "$CHILD_ID" ]; then + PARENT_PIPELINE_ID="$pid" + CHILD_PIPELINE_ID="$CHILD_ID" + # Prefer "success" over "success-with-warnings" + if [ "$CHILD_STATUS" = "success" ]; then + echo "Found fully passing pipeline: $pid -> $CHILD_ID" + break + fi + fi +done + +if [ -z "$CHILD_PIPELINE_ID" ]; then + echo "Error: No child pipeline found for tag $TAG" + exit 1 +fi + +echo "Using parent pipeline: $PARENT_PIPELINE_ID" +echo "Using child pipeline: $CHILD_PIPELINE_ID" + +if [ -z "$CHILD_PIPELINE_ID" ] || [ "$CHILD_PIPELINE_ID" = "null" ]; then + echo "Error: No child pipeline found" + exit 1 +fi + +echo "Found child pipeline: $CHILD_PIPELINE_ID" + +# Get all test jobs +echo "Fetching job data..." +JOBS_JSON=$(curl -s "$GITLAB_URL/$PROJECT_PATH/-/pipelines/$CHILD_PIPELINE_ID/stage.json?stage=test" | jq '.latest_statuses') + +# Get test report summary +TEST_REPORT=$(curl -s "$GITLAB_URL/$PROJECT_PATH/-/pipelines/$CHILD_PIPELINE_ID/test_report.json") +TOTAL_TESTS=$(echo "$TEST_REPORT" | jq -r '.total_count // 0') +PASSED_TESTS=$(echo "$TEST_REPORT" | jq -r '.success_count // 0') +FAILED_TESTS=$(echo "$TEST_REPORT" | jq -r '.failed_count // 0') + +# Parse job timing data +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +REPORT_DATE=$(date -u +"%Y-%m-%d") + +# Create JSON report with per-language timing +echo "Parsing timing data..." + +# Build JSON array of language performance +# Note: We strip milliseconds from timestamps for jq compatibility +PERF_JSON=$(echo "$JOBS_JSON" | jq -r ' + [.[] | + select(.name | startswith("test: [")) | + { + language: (.name | gsub("test: \\[|\\]"; "")), + status: .status.text, + queued_duration: (.queued_duration // 0), + started_at: (.started_at // "" | gsub("\\.[0-9]+Z$"; "Z")), + updated_at: (.updated_at // "" | gsub("\\.[0-9]+Z$"; "Z")), + job_id: .id + } + ] | + map(. + { + duration_seconds: ( + if .started_at != "" and .updated_at != "" then + (((.updated_at | fromdateiso8601) - (.started_at | fromdateiso8601)) | floor) + else 0 end + ) + }) | + sort_by(.duration_seconds) | reverse +') + +# Calculate stats +TOTAL_LANGUAGES=$(echo "$PERF_JSON" | jq 'length') +PASSED_LANGUAGES=$(echo "$PERF_JSON" | jq '[.[] | select(.status == "Passed")] | length') +AVG_DURATION=$(echo "$PERF_JSON" | jq '[.[].duration_seconds] | add / length | floor') +MAX_DURATION=$(echo "$PERF_JSON" | jq '[.[].duration_seconds] | max') +MIN_DURATION=$(echo "$PERF_JSON" | jq '[.[].duration_seconds] | min') +SLOWEST_LANG=$(echo "$PERF_JSON" | jq -r '.[0].language') +FASTEST_LANG=$(echo "$PERF_JSON" | jq -r '.[-1].language') + +# Write JSON report +cat > "reports/perf-$TAG.json" << EOF +{ + "tag": "$TAG", + "generated_at": "$TIMESTAMP", + "parent_pipeline_id": $PARENT_PIPELINE_ID, + "child_pipeline_id": $CHILD_PIPELINE_ID, + "summary": { + "total_tests": $TOTAL_TESTS, + "passed_tests": $PASSED_TESTS, + "failed_tests": $FAILED_TESTS, + "total_languages": $TOTAL_LANGUAGES, + "passed_languages": $PASSED_LANGUAGES, + "avg_duration_seconds": $AVG_DURATION, + "max_duration_seconds": $MAX_DURATION, + "min_duration_seconds": $MIN_DURATION, + "slowest_language": "$SLOWEST_LANG", + "fastest_language": "$FASTEST_LANG" + }, + "languages": $PERF_JSON +} +EOF + +echo " ✓ reports/perf-$TAG.json" + +# Write Markdown report +cat > "reports/perf-$TAG.md" << EOF +# Performance Report: $TAG + +**Generated:** $TIMESTAMP +**Pipeline:** [$CHILD_PIPELINE_ID]($GITLAB_URL/$PROJECT_PATH/-/pipelines/$CHILD_PIPELINE_ID) + +## Summary + +| Metric | Value | +|--------|-------| +| Total Tests | $TOTAL_TESTS | +| Passed | $PASSED_TESTS | +| Failed | $FAILED_TESTS | +| Pass Rate | $(echo "scale=1; $PASSED_TESTS * 100 / $TOTAL_TESTS" | bc)% | +| Languages | $TOTAL_LANGUAGES | +| Avg Duration | ${AVG_DURATION}s | +| Slowest | $SLOWEST_LANG (${MAX_DURATION}s) | +| Fastest | $FASTEST_LANG (${MIN_DURATION}s) | + +## Per-Language Performance + +| Language | Status | Duration | Tests | +|----------|--------|----------|-------| +EOF + +# Add per-language rows +echo "$PERF_JSON" | jq -r '.[] | "| \(.language) | \(.status) | \(.duration_seconds)s | - |"' >> "reports/perf-$TAG.md" + +cat >> "reports/perf-$TAG.md" << EOF + +## Timing Distribution + +\`\`\` +EOF + +# Add a simple ASCII histogram +echo "$PERF_JSON" | jq -r '.[] | "\(.language)|\(.duration_seconds)"' | while IFS='|' read -r lang dur; do + bar="" + bars=$((dur / 5)) + for ((i=0; i> "reports/perf-$TAG.md" +done + +cat >> "reports/perf-$TAG.md" << EOF +\`\`\` + +--- +*Report generated by \`make perf-report\`* +EOF + +echo " ✓ reports/perf-$TAG.md" + +# Update or create historical index +if [ ! -f "reports/PERFORMANCE-HISTORY.md" ]; then + cat > "reports/PERFORMANCE-HISTORY.md" << EOF +# Performance History + +Tracking compile and test execution times across releases. + +## Releases + +| Version | Date | Tests | Pass Rate | Avg Duration | Slowest | Fastest | +|---------|------|-------|-----------|--------------|---------|---------| +EOF +fi + +# Add this release to history (avoid duplicates) +if ! grep -q "| $TAG |" "reports/PERFORMANCE-HISTORY.md"; then + echo "| $TAG | $REPORT_DATE | $TOTAL_TESTS | $(echo "scale=1; $PASSED_TESTS * 100 / $TOTAL_TESTS" | bc)% | ${AVG_DURATION}s | $SLOWEST_LANG | $FASTEST_LANG |" >> "reports/PERFORMANCE-HISTORY.md" + echo " ✓ Updated reports/PERFORMANCE-HISTORY.md" +fi + +echo "" +echo "Done! Performance report for $TAG:" +echo " - reports/perf-$TAG.json" +echo " - reports/perf-$TAG.md" +echo " - reports/PERFORMANCE-HISTORY.md" +echo "" +echo "To commit: git add reports/ && git commit -m 'perf: Add performance report for $TAG'"