feat(cccl): integrate missing CCCL directories — python/, ci/, .agent/, docs/, test/
Sparse-checkout from NVIDIA/cccl main branch to complete cccl_upstream: Added: - python/cuda_cccl/ (226 files) — Python bindings for device-level algorithms Critical for muh toolchain: cuda.compute.reduce_into, scan, radix_sort, etc. Includes 204 .py files with full test coverage for all 27 algorithms - ci/ (163 files) — Build/test infrastructure build_cub.sh, test_cub.sh, build_and_test_targets.sh, matrix.yaml Directly maps to our [INFRA-CI] and [INFRA-BUILD] items - .agent/skills/ (7 files) — NVIDIA's own agent skills for CCCL cccl-style/SKILL.md, cccl-test/SKILL.md, sass-diff/SKILL.md - docs/ (491 files) — Official CCCL documentation CI references, CMake guides, Python compute docs, libcudacxx PTX docs - test/ (12 files) — Top-level integration tests (cuda_smoke, stdpar) - Root configs: .clang-format, .clang-tidy, CONTRIBUTING.md, pyproject.toml - CLAUDE.md symlink → AGENTS.md (NVIDIA's standard) cccl_upstream now mirrors full NVIDIA/cccl structure: Before: 42M (cub + thrust + libcudacxx + cudax + c + examples + benchmarks) After: 53M (+python +ci +docs +.agent +test +configs) This completes the CCCL base needed for: - [muh-bench] items: ci/util/build_and_test_targets.sh for targeted builds - [CCCL-verify] items: python/cuda_cccl/tests/ as reference implementations - [CCCL-test] items: ci/test_cub.sh, ci/test_thrust.sh - Agent workflow: .agent/skills/ for consistent style and test patterns
This commit is contained in:
77
cccl_upstream/ci/compile_time/README.md
Normal file
77
cccl_upstream/ci/compile_time/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Compile-time benchmark CI contracts
|
||||
|
||||
The compile-time benchmark CI flow is configured from `ci/matrix.yaml` under
|
||||
`compile_time.pull_request`.
|
||||
|
||||
## Matrix schema
|
||||
|
||||
Each config is a GitHub Actions matrix entry:
|
||||
|
||||
```yaml
|
||||
compile_time:
|
||||
pull_request:
|
||||
- id: public-headers-gcc13
|
||||
name: Public headers compile-time bench
|
||||
gpu: rtx2080
|
||||
launch_args: "--cuda 13.3 --host gcc13"
|
||||
baseline_ref: origin/main
|
||||
preset: all-dev
|
||||
targets:
|
||||
- cub.headers.base
|
||||
args: "-arch native"
|
||||
slices:
|
||||
- id: total-compilation
|
||||
title: TU total compilation
|
||||
filter: total-compilation
|
||||
timing: inclusive
|
||||
sort: total
|
||||
top: 15
|
||||
threshold: 0.001
|
||||
```
|
||||
|
||||
Required config fields are `id`, `name`, `gpu`, `launch_args`,
|
||||
`baseline_ref`, `preset`, `targets`, and `slices`. `args`, `comment`, and
|
||||
`artifact_retention_days` are optional.
|
||||
|
||||
Required slice fields are `id`, `title`, `filter`, `timing`, `sort`, `top`, and
|
||||
`threshold`. Slice `children` may be used to group nested report sections in the
|
||||
PR comment. Empty slice sections are omitted recursively by the renderer unless
|
||||
the summary manifest carries warnings for that slice.
|
||||
|
||||
`ci/compile_time/parse_matrix.py ci/matrix.yaml --workflow pull_request` emits
|
||||
the GitHub Actions matrix JSON. Missing or empty `compile_time.pull_request`
|
||||
emits `{"include":[]}`.
|
||||
|
||||
In baseline comparisons, `threshold` is measured against the total selected
|
||||
inclusive/exclusive impact across all matched traces. The per-side reports still
|
||||
use `sort` for their own top-N ordering; comparison worse/better tables always
|
||||
rank by total impact so a change repeated across many traces is not hidden by a
|
||||
larger single-trace movement.
|
||||
|
||||
## Report contract
|
||||
|
||||
`summarize_events.py --slices <json>` writes per-slice CSVs under
|
||||
`event_reports/<slice-id>/` and writes a normalized `event_reports/summary.json`
|
||||
manifest. The manifest is the renderer contract; CSVs are human artifacts.
|
||||
Configured slices that match no events, have no matching trace files, or have no
|
||||
comparable event keys record warnings in the manifest so reporting failures are
|
||||
not presented as ordinary no-regression results.
|
||||
|
||||
In comparison mode, the wrapper preserves:
|
||||
|
||||
- current raw traces: `compile_time/raw_traces`
|
||||
- baseline raw traces: `compile_time/baseline_raw_traces`
|
||||
- Perfetto copies: `compile_time/perfetto_traces/current` and
|
||||
`compile_time/perfetto_traces/baseline`
|
||||
|
||||
## PR comments
|
||||
|
||||
`render_pr_comment.py` reads `summary.json`, config metadata, and an artifacts
|
||||
URL, then writes the sticky PR comment body. Regressions and improvements are
|
||||
rendered in separate `<details>` blocks and are never mixed in one table.
|
||||
Warnings are rendered separately and keep their slice visible even when there
|
||||
are no regression/improvement rows.
|
||||
|
||||
The reusable workflow uses the sticky-comment header
|
||||
`compile-time-bench-<config-id>` with `hide_and_recreate: true`, so previous
|
||||
comments for the same config are archived as outdated.
|
||||
333
cccl_upstream/ci/compile_time/analytics.ipynb
Normal file
333
cccl_upstream/ci/compile_time/analytics.ipynb
Normal file
@@ -0,0 +1,333 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "903704f7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Compile-Time Analytics\n",
|
||||
"\n",
|
||||
"This notebook helps you:\n",
|
||||
"\n",
|
||||
"1. Run `ci/build_compile_time_bench.sh` from the repo root.\n",
|
||||
"2. Load and inspect an all-header processing CSV.\n",
|
||||
"3. Explore high-impact headers by TU coverage and average processing time.\n",
|
||||
"4. Build combined ranking scores to identify optimization targets.\n",
|
||||
"\n",
|
||||
"Expected CSV columns include:\n",
|
||||
"- `header_path`\n",
|
||||
"- `include_tu_count`\n",
|
||||
"- `avg_process_time_s`\n",
|
||||
"- `total_process_time_s`\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ad58db28",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install pandas matplotlib plotly nbformat"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "16ba6808",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"import shlex\n",
|
||||
"import sys\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"pd.set_option(\"display.max_colwidth\", 160)\n",
|
||||
"pd.set_option(\"display.width\", 200)\n",
|
||||
"pd.set_option(\"display.max_columns\", 20)\n",
|
||||
"\n",
|
||||
"REPO_ROOT = Path.cwd()\n",
|
||||
"while REPO_ROOT != REPO_ROOT.parent and not (REPO_ROOT / \".git\").exists():\n",
|
||||
" REPO_ROOT = REPO_ROOT.parent\n",
|
||||
"\n",
|
||||
"if not (REPO_ROOT / \".git\").exists():\n",
|
||||
" raise RuntimeError(\n",
|
||||
" \"Could not locate repo root (.git). Start notebook from inside the CCCL repo.\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"print(f\"Repo root: {REPO_ROOT}\")\n",
|
||||
"print(f\"Python executable: {sys.executable}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "373652c8",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# --- Run build_compile_time_bench.sh (file-processing mode) ---\n",
|
||||
"import subprocess\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"if \"REPO_ROOT\" not in globals():\n",
|
||||
" REPO_ROOT = Path.cwd()\n",
|
||||
" while REPO_ROOT != REPO_ROOT.parent and not (REPO_ROOT / \".git\").exists():\n",
|
||||
" REPO_ROOT = REPO_ROOT.parent\n",
|
||||
"\n",
|
||||
"output_csv = Path(os.environ.get(\"COMPILE_TIME_CSV\", \"/tmp/compile_time.csv\"))\n",
|
||||
"cmd = [\n",
|
||||
" \"bash\",\n",
|
||||
" str(REPO_ROOT / \"ci\" / \"build_compile_time_bench.sh\"),\n",
|
||||
" *shlex.split(os.environ.get(\"COMPILE_TIME_BUILD_ARGS\", \"\")),\n",
|
||||
" \"--\",\n",
|
||||
" \"-f\",\n",
|
||||
" \"file-processing\",\n",
|
||||
" \"-e\",\n",
|
||||
" \"-n\",\n",
|
||||
" os.environ.get(\"COMPILE_TIME_TOP_N\", \"5000\"),\n",
|
||||
" \"--sort\",\n",
|
||||
" \"total\",\n",
|
||||
" \"--output-csv\",\n",
|
||||
" str(output_csv),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"print(\"Command:\")\n",
|
||||
"print(\" \" + \" \".join(shlex.quote(x) for x in cmd))\n",
|
||||
"\n",
|
||||
"subprocess.run(cmd, cwd=REPO_ROOT, check=True)\n",
|
||||
"print(f\"\\nWrote: {output_csv}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "12ad4c5c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# --- Load CSV ---\n",
|
||||
"csv_path = Path(os.environ.get(\"COMPILE_TIME_CSV\", \"/tmp/compile_time.csv\"))\n",
|
||||
"if not csv_path.exists():\n",
|
||||
" raise FileNotFoundError(f\"Missing CSV: {csv_path}. Run the script first.\")\n",
|
||||
"\n",
|
||||
"df = pd.read_csv(csv_path)\n",
|
||||
"\n",
|
||||
"event_summary_cols = {\n",
|
||||
" \"event_name\",\n",
|
||||
" \"event_key\",\n",
|
||||
" \"root_tu_count\",\n",
|
||||
" \"selected_avg_per_root_tu_s\",\n",
|
||||
" \"selected_total_s\",\n",
|
||||
"}\n",
|
||||
"required_cols = [\n",
|
||||
" \"header_path\",\n",
|
||||
" \"include_tu_count\",\n",
|
||||
" \"avg_process_time_s\",\n",
|
||||
" \"total_process_time_s\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"if event_summary_cols.issubset(df.columns):\n",
|
||||
" df[\"header_path\"] = df[\"event_key\"]\n",
|
||||
" df[\"include_tu_count\"] = df[\"root_tu_count\"]\n",
|
||||
" if (\n",
|
||||
" \"avg_inclusive_per_root_tu_s\" in df.columns\n",
|
||||
" and \"total_inclusive_s\" in df.columns\n",
|
||||
" ):\n",
|
||||
" df[\"avg_process_time_s\"] = df[\"avg_inclusive_per_root_tu_s\"]\n",
|
||||
" df[\"total_process_time_s\"] = df[\"total_inclusive_s\"]\n",
|
||||
" else:\n",
|
||||
" df[\"avg_process_time_s\"] = df[\"selected_avg_per_root_tu_s\"]\n",
|
||||
" df[\"total_process_time_s\"] = df[\"selected_total_s\"]\n",
|
||||
"\n",
|
||||
"missing = [c for c in required_cols if c not in df.columns]\n",
|
||||
"if missing:\n",
|
||||
" raise ValueError(\n",
|
||||
" f\"CSV is not all-header processing output. Missing columns: {missing}. \"\n",
|
||||
" \"Run the script cell above to regenerate /tmp/compile_time.csv.\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"for col in [\"include_tu_count\", \"avg_process_time_s\", \"total_process_time_s\"]:\n",
|
||||
" df[col] = pd.to_numeric(df[col], errors=\"coerce\").fillna(0)\n",
|
||||
"\n",
|
||||
"print(f\"Rows: {len(df):,}\")\n",
|
||||
"print(f\"Columns: {list(df.columns)}\")\n",
|
||||
"df.head(5)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "1a9b36a8",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# --- Quick top-N views ---\n",
|
||||
"TOP_N = 5\n",
|
||||
"\n",
|
||||
"print(\"Top by avg_process_time_s\")\n",
|
||||
"display(\n",
|
||||
" df.nlargest(TOP_N, \"avg_process_time_s\")[\n",
|
||||
" [\n",
|
||||
" \"header_path\",\n",
|
||||
" \"avg_process_time_s\",\n",
|
||||
" \"include_tu_count\",\n",
|
||||
" \"total_process_time_s\",\n",
|
||||
" ]\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"\\nTop by include_tu_count\")\n",
|
||||
"display(\n",
|
||||
" df.nlargest(TOP_N, \"include_tu_count\")[\n",
|
||||
" [\n",
|
||||
" \"header_path\",\n",
|
||||
" \"include_tu_count\",\n",
|
||||
" \"avg_process_time_s\",\n",
|
||||
" \"total_process_time_s\",\n",
|
||||
" ]\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"\\nTop by impact score (include_tu_count * avg_process_time_s)\")\n",
|
||||
"df_score = df.copy()\n",
|
||||
"df_score[\"impact_score\"] = df_score[\"include_tu_count\"] * df_score[\"avg_process_time_s\"]\n",
|
||||
"display(\n",
|
||||
" df_score.nlargest(TOP_N, \"impact_score\")[\n",
|
||||
" [\n",
|
||||
" \"header_path\",\n",
|
||||
" \"impact_score\",\n",
|
||||
" \"include_tu_count\",\n",
|
||||
" \"avg_process_time_s\",\n",
|
||||
" \"total_process_time_s\",\n",
|
||||
" ]\n",
|
||||
" ]\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "174eabd4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### How to read this plot\n",
|
||||
"\n",
|
||||
"- Each point is one header seen in all-mode profiling.\n",
|
||||
"- **X axis (`include_tu_count`)**: how many generated public-header TUs include this header at least once.\n",
|
||||
"- **Y axis (`avg_process_time_s`)**: average time spent processing that header per including TU.\n",
|
||||
"- Headers near the **upper-right** are usually the best optimization candidates because they are both widespread and expensive per include."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "9ddc0725",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# --- Interactive scatter (hover shows header name) ---\n",
|
||||
"\n",
|
||||
"import plotly.express as px\n",
|
||||
"\n",
|
||||
"plot_df = df.copy()\n",
|
||||
"\n",
|
||||
"total_headers = len(plot_df)\n",
|
||||
"total_public_headers = int(plot_df[\"include_tu_count\"].max())\n",
|
||||
"\n",
|
||||
"fig = px.scatter(\n",
|
||||
" plot_df,\n",
|
||||
" x=\"include_tu_count\",\n",
|
||||
" y=\"avg_process_time_s\",\n",
|
||||
" hover_name=\"header_path\",\n",
|
||||
" hover_data={\n",
|
||||
" \"include_tu_count\": True,\n",
|
||||
" \"avg_process_time_s\": \":.6f\",\n",
|
||||
" \"total_process_time_s\": \":.3f\",\n",
|
||||
" \"header_path\": False,\n",
|
||||
" },\n",
|
||||
" opacity=0.6,\n",
|
||||
" title=(\n",
|
||||
" f\"Header include count vs avg processing time ({total_headers:,} total headers; \"\n",
|
||||
" f\"coverage measured across {total_public_headers} public headers)\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"fig.update_layout(\n",
|
||||
" xaxis_title=(\n",
|
||||
" \"TU coverage: number of public headers that include this header \"\n",
|
||||
" f\"(out of {total_public_headers})\"\n",
|
||||
" ),\n",
|
||||
" yaxis_title=\"Average processing time per including TU (seconds)\",\n",
|
||||
" height=900,\n",
|
||||
")\n",
|
||||
"fig.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "f429b934",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# --- Optional: run ctadvisor and parse expensive headers ---\n",
|
||||
"run_ctadvisor = os.environ.get(\"COMPILE_TIME_RUN_CTADVISOR\") == \"1\"\n",
|
||||
"CTADVISOR_ENTRIES = 10\n",
|
||||
"CTADVISOR_THREADS = os.cpu_count() or 8\n",
|
||||
"\n",
|
||||
"trace_root = (\n",
|
||||
" REPO_ROOT\n",
|
||||
" / \"build\"\n",
|
||||
" / os.environ.get(\"CCCL_BUILD_INFIX\", \"cuda13.1-gcc14\")\n",
|
||||
" / os.environ.get(\"CCCL_COMPILE_TIME_PRESET\", \"all-dev\")\n",
|
||||
" / \"compile_time\"\n",
|
||||
" / \"raw_traces\"\n",
|
||||
")\n",
|
||||
"ctadvisor_cmd = [\n",
|
||||
" \"ctadvisor\",\n",
|
||||
" \"--trace-file-path\",\n",
|
||||
" str(trace_root),\n",
|
||||
" \"--header-advisor-entries\",\n",
|
||||
" str(CTADVISOR_ENTRIES),\n",
|
||||
" \"--thread-number\",\n",
|
||||
" str(CTADVISOR_THREADS),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"if run_ctadvisor:\n",
|
||||
" print(\"Command:\")\n",
|
||||
" print(\" \" + \" \".join(shlex.quote(x) for x in ctadvisor_cmd))\n",
|
||||
"\n",
|
||||
" result = subprocess.run(\n",
|
||||
" ctadvisor_cmd, cwd=REPO_ROOT, check=True, capture_output=True, text=True\n",
|
||||
" )\n",
|
||||
" print(result.stdout)\n",
|
||||
"else:\n",
|
||||
" print(\"Skipping ctadvisor. Set COMPILE_TIME_RUN_CTADVISOR=1 to run it.\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "cccl",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
272
cccl_upstream/ci/compile_time/parse_matrix.py
Executable file
272
cccl_upstream/ci/compile_time/parse_matrix.py
Executable file
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ModuleNotFoundError:
|
||||
yaml = None
|
||||
YAML_ERROR_TYPES: tuple[type[BaseException], ...] = ()
|
||||
else:
|
||||
YAML_ERROR_TYPES = (yaml.YAMLError,)
|
||||
|
||||
ID_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]*$")
|
||||
TIMINGS = {"inclusive", "exclusive"}
|
||||
SORTS = {"total", "avg", "avg-root-tu", "max"}
|
||||
|
||||
|
||||
def die(message: str) -> None:
|
||||
print(f"error: {message}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
def require_mapping(value: Any, where: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
die(f"{where} must be a mapping")
|
||||
return value
|
||||
|
||||
|
||||
def require_field(mapping: dict[str, Any], field: str, where: str) -> Any:
|
||||
if field not in mapping:
|
||||
die(f"{where} is missing required field '{field}'")
|
||||
return mapping[field]
|
||||
|
||||
|
||||
def require_string(value: Any, where: str, *, nonempty: bool = True) -> str:
|
||||
if not isinstance(value, str):
|
||||
die(f"{where} must be a string")
|
||||
if nonempty and not value:
|
||||
die(f"{where} must be non-empty")
|
||||
return value
|
||||
|
||||
|
||||
def require_id(value: Any, where: str) -> str:
|
||||
text = require_string(value, where)
|
||||
if not ID_RE.fullmatch(text):
|
||||
die(f"{where} must match {ID_RE.pattern}")
|
||||
return text
|
||||
|
||||
|
||||
def require_string_list(value: Any, where: str) -> list[str]:
|
||||
if not isinstance(value, list) or not value:
|
||||
die(f"{where} must be a non-empty list")
|
||||
strings: list[str] = []
|
||||
for index, item in enumerate(value):
|
||||
strings.append(require_string(item, f"{where}[{index}]"))
|
||||
return strings
|
||||
|
||||
|
||||
def require_bool(value: Any, where: str) -> bool:
|
||||
if not isinstance(value, bool):
|
||||
die(f"{where} must be a boolean")
|
||||
return value
|
||||
|
||||
|
||||
def require_positive_int(value: Any, where: str) -> int:
|
||||
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
||||
die(f"{where} must be a positive integer")
|
||||
return value
|
||||
|
||||
|
||||
def validate_slice(
|
||||
slice_data: Any,
|
||||
*,
|
||||
where: str,
|
||||
seen_ids: set[str],
|
||||
) -> dict[str, Any]:
|
||||
data = require_mapping(slice_data, where)
|
||||
slice_id = require_id(require_field(data, "id", where), f"{where}.id")
|
||||
if slice_id in seen_ids:
|
||||
die(f"duplicate slice id '{slice_id}' in {where}")
|
||||
seen_ids.add(slice_id)
|
||||
|
||||
title = require_string(require_field(data, "title", where), f"{where}.title")
|
||||
filter_name = require_string(
|
||||
require_field(data, "filter", where), f"{where}.filter"
|
||||
)
|
||||
timing = require_string(require_field(data, "timing", where), f"{where}.timing")
|
||||
if timing not in TIMINGS:
|
||||
die(f"{where}.timing must be one of {sorted(TIMINGS)}")
|
||||
sort = require_string(require_field(data, "sort", where), f"{where}.sort")
|
||||
if sort not in SORTS:
|
||||
die(f"{where}.sort must be one of {sorted(SORTS)}")
|
||||
|
||||
top = require_field(data, "top", where)
|
||||
if isinstance(top, bool) or not isinstance(top, int) or top <= 0:
|
||||
die(f"{where}.top must be a positive integer")
|
||||
threshold = require_field(data, "threshold", where)
|
||||
if (
|
||||
isinstance(threshold, bool)
|
||||
or not isinstance(threshold, (int, float))
|
||||
or threshold < 0
|
||||
):
|
||||
die(f"{where}.threshold must be a non-negative number")
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"id": slice_id,
|
||||
"title": title,
|
||||
"filter": filter_name,
|
||||
"timing": timing,
|
||||
"sort": sort,
|
||||
"top": top,
|
||||
"threshold": threshold,
|
||||
}
|
||||
for optional in ("scope_filter", "exclusive_scope"):
|
||||
if optional in data:
|
||||
result[optional] = require_string(
|
||||
data[optional], f"{where}.{optional}", nonempty=False
|
||||
)
|
||||
|
||||
children = data.get("children", [])
|
||||
if not isinstance(children, list):
|
||||
die(f"{where}.children must be a list")
|
||||
if children:
|
||||
result["children"] = [
|
||||
validate_slice(
|
||||
child,
|
||||
where=f"{where}.children[{index}]",
|
||||
seen_ids=seen_ids,
|
||||
)
|
||||
for index, child in enumerate(children)
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
def validate_config(
|
||||
config_data: Any, *, where: str, seen_ids: set[str]
|
||||
) -> dict[str, Any]:
|
||||
data = require_mapping(config_data, where)
|
||||
config_id = require_id(require_field(data, "id", where), f"{where}.id")
|
||||
if config_id in seen_ids:
|
||||
die(f"duplicate compile_time config id '{config_id}'")
|
||||
seen_ids.add(config_id)
|
||||
|
||||
targets = require_string_list(
|
||||
require_field(data, "targets", where), f"{where}.targets"
|
||||
)
|
||||
slices = require_field(data, "slices", where)
|
||||
if not isinstance(slices, list) or not slices:
|
||||
die(f"{where}.slices must be a non-empty list")
|
||||
|
||||
slice_ids: set[str] = set()
|
||||
normalized_slices = [
|
||||
validate_slice(
|
||||
slice_data,
|
||||
where=f"{where}.slices[{index}]",
|
||||
seen_ids=slice_ids,
|
||||
)
|
||||
for index, slice_data in enumerate(slices)
|
||||
]
|
||||
|
||||
return {
|
||||
"id": config_id,
|
||||
"name": require_string(require_field(data, "name", where), f"{where}.name"),
|
||||
"gpu": require_string(require_field(data, "gpu", where), f"{where}.gpu"),
|
||||
"launch_args": require_string(
|
||||
require_field(data, "launch_args", where), f"{where}.launch_args"
|
||||
),
|
||||
"baseline_ref": require_string(
|
||||
require_field(data, "baseline_ref", where), f"{where}.baseline_ref"
|
||||
),
|
||||
"preset": require_string(
|
||||
require_field(data, "preset", where), f"{where}.preset"
|
||||
),
|
||||
"targets": targets,
|
||||
"args": require_string(data.get("args", ""), f"{where}.args", nonempty=False),
|
||||
"comment": require_bool(data.get("comment", True), f"{where}.comment"),
|
||||
"artifact_retention_days": require_positive_int(
|
||||
data.get("artifact_retention_days", 14),
|
||||
f"{where}.artifact_retention_days",
|
||||
),
|
||||
"slices": normalized_slices,
|
||||
}
|
||||
|
||||
|
||||
def matrix_entry(config: dict[str, Any]) -> dict[str, Any]:
|
||||
config_id = config["id"]
|
||||
return {
|
||||
"id": config_id,
|
||||
"name": config["name"],
|
||||
"gpu": config["gpu"],
|
||||
"launch_args": config["launch_args"],
|
||||
"baseline_ref": config["baseline_ref"],
|
||||
"preset": config["preset"],
|
||||
"targets_json": json.dumps(config["targets"], separators=(",", ":")),
|
||||
"args": config["args"],
|
||||
"slices_json": json.dumps({"slices": config["slices"]}, separators=(",", ":")),
|
||||
"comment": str(config["comment"]).lower(),
|
||||
"artifact_retention_days": config["artifact_retention_days"],
|
||||
"comment_header": f"compile-time-bench-{config_id}",
|
||||
}
|
||||
|
||||
|
||||
def parse_matrix(path: Path, workflow: str) -> dict[str, Any]:
|
||||
try:
|
||||
if yaml is not None:
|
||||
with path.open(encoding="utf-8") as f:
|
||||
matrix = yaml.safe_load(f) or {}
|
||||
else:
|
||||
completed = subprocess.run(
|
||||
["yq", "-o=json", ".", path.as_posix()],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
matrix = json.loads(completed.stdout or "{}")
|
||||
except OSError as e:
|
||||
die(f"failed to read {path}: {e}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
die(f"failed to parse {path} with yq: {e.stderr.strip()}")
|
||||
except json.JSONDecodeError as e:
|
||||
die(f"failed to decode {path} as JSON: {e}")
|
||||
except YAML_ERROR_TYPES as e:
|
||||
die(f"failed to parse {path}: {e}")
|
||||
|
||||
compile_time = matrix.get("compile_time")
|
||||
if compile_time is None:
|
||||
return {"include": []}
|
||||
compile_time = require_mapping(compile_time, "compile_time")
|
||||
configs = compile_time.get(workflow, [])
|
||||
if configs is None:
|
||||
configs = []
|
||||
if not isinstance(configs, list):
|
||||
die(f"compile_time.{workflow} must be a list")
|
||||
if not configs:
|
||||
return {"include": []}
|
||||
|
||||
seen_ids: set[str] = set()
|
||||
return {
|
||||
"include": [
|
||||
matrix_entry(
|
||||
validate_config(
|
||||
config,
|
||||
where=f"compile_time.{workflow}[{index}]",
|
||||
seen_ids=seen_ids,
|
||||
)
|
||||
)
|
||||
for index, config in enumerate(configs)
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Parse ci/matrix.yaml compile_time entries for GitHub Actions."
|
||||
)
|
||||
parser.add_argument("matrix_yaml", type=Path)
|
||||
parser.add_argument("--workflow", default="pull_request")
|
||||
args = parser.parse_args()
|
||||
|
||||
json.dump(parse_matrix(args.matrix_yaml, args.workflow), sys.stdout)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
167
cccl_upstream/ci/compile_time/prepare_traces.py
Executable file
167
cccl_upstream/ci/compile_time/prepare_traces.py
Executable file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
DETAIL_EVENT_NAMES = {
|
||||
"Code Generation Function",
|
||||
"CodeGen Function",
|
||||
"ExecuteCompiler",
|
||||
"Frontend",
|
||||
"Instantiating Template Class",
|
||||
"Instantiating Template Function",
|
||||
"InstantiateClass",
|
||||
"InstantiateFunction",
|
||||
"OptFunction",
|
||||
"ParseClass",
|
||||
"PerformPendingInstantiations",
|
||||
"Processing Header File",
|
||||
"RunPass",
|
||||
"Scanning Function Body",
|
||||
"Source",
|
||||
}
|
||||
|
||||
DETAIL_PREFIXES_TO_STRIP = (
|
||||
"libcudacxx/include/",
|
||||
"cudax/include/",
|
||||
"c/parallel/include/",
|
||||
)
|
||||
|
||||
DETAIL_PREFIXES_TO_COLLAPSE = (
|
||||
("cub/cub/", "cub/"),
|
||||
("thrust/thrust/", "thrust/"),
|
||||
)
|
||||
|
||||
|
||||
def normalize_detail(detail: str, repo_root: Path) -> str:
|
||||
detail_path = Path(detail)
|
||||
if detail_path.is_absolute():
|
||||
try:
|
||||
rel = detail_path.resolve(strict=False).relative_to(repo_root)
|
||||
detail = rel.as_posix()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
for prefix in DETAIL_PREFIXES_TO_STRIP:
|
||||
if detail.startswith(prefix):
|
||||
detail = detail[len(prefix) :]
|
||||
break
|
||||
|
||||
for prefix, replacement in DETAIL_PREFIXES_TO_COLLAPSE:
|
||||
if detail.startswith(prefix):
|
||||
detail = replacement + detail[len(prefix) :]
|
||||
break
|
||||
|
||||
return detail
|
||||
|
||||
|
||||
def display_detail(detail: str, repo_root: Path, max_detail_len: int | None) -> str:
|
||||
detail = normalize_detail(detail, repo_root)
|
||||
if (
|
||||
max_detail_len is not None
|
||||
and max_detail_len > 0
|
||||
and len(detail) > max_detail_len
|
||||
):
|
||||
return detail[: max_detail_len - 1] + "..."
|
||||
return detail
|
||||
|
||||
|
||||
def rewrite_event_name(
|
||||
event: dict, repo_root: Path, max_detail_len: int | None
|
||||
) -> bool:
|
||||
name = event.get("name")
|
||||
if name not in DETAIL_EVENT_NAMES:
|
||||
return False
|
||||
|
||||
args = event.get("args")
|
||||
if not isinstance(args, dict):
|
||||
return False
|
||||
|
||||
detail = args.get("detail")
|
||||
if not detail:
|
||||
return False
|
||||
|
||||
args.setdefault("original_name", name)
|
||||
event["name"] = f"{name}: {display_detail(str(detail), repo_root, max_detail_len)}"
|
||||
return True
|
||||
|
||||
|
||||
def prepare_trace(
|
||||
input_path: Path, output_path: Path, repo_root: Path, max_detail_len: int | None
|
||||
) -> int:
|
||||
with input_path.open(encoding="utf-8") as f:
|
||||
trace = json.load(f)
|
||||
|
||||
rewritten = 0
|
||||
for event in trace.get("traceEvents", []):
|
||||
if rewrite_event_name(event, repo_root, max_detail_len):
|
||||
rewritten += 1
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(trace, f, separators=(",", ":"))
|
||||
|
||||
return rewritten
|
||||
|
||||
|
||||
def iter_input_traces(input_path: Path) -> list[Path]:
|
||||
if input_path.is_file():
|
||||
return [input_path]
|
||||
return sorted(input_path.rglob("*.json"))
|
||||
|
||||
|
||||
def output_path_for(input_trace: Path, input_root: Path, output_path: Path) -> Path:
|
||||
if input_root.is_file():
|
||||
if output_path.is_dir() or not output_path.suffix:
|
||||
return output_path / f"{input_trace.stem}.perfetto.json"
|
||||
return output_path
|
||||
|
||||
rel = input_trace.relative_to(input_root)
|
||||
return output_path / rel.parent / f"{rel.stem}.perfetto.json"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Prepare NVCC device-time-trace JSON files for Perfetto by promoting args.detail into event names."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input", required=True, type=Path, help="Input trace JSON file or directory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", required=True, type=Path, help="Output trace JSON file or directory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repo-root", default=Path(__file__).resolve().parents[2], type=Path
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-detail-len",
|
||||
default=0,
|
||||
type=int,
|
||||
help="Truncate promoted detail text to this many characters; 0 keeps full details",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
input_path = args.input.resolve(strict=False)
|
||||
output_path = args.output.resolve(strict=False)
|
||||
repo_root = args.repo_root.resolve(strict=False)
|
||||
max_detail_len = args.max_detail_len if args.max_detail_len > 0 else None
|
||||
|
||||
traces = iter_input_traces(input_path)
|
||||
if not traces:
|
||||
raise SystemExit(f"no JSON traces found under {args.input}")
|
||||
|
||||
total_rewritten = 0
|
||||
for trace_path in traces:
|
||||
total_rewritten += prepare_trace(
|
||||
trace_path,
|
||||
output_path_for(trace_path, input_path, output_path),
|
||||
repo_root,
|
||||
max_detail_len,
|
||||
)
|
||||
|
||||
print(f"prepared {len(traces)} trace(s); renamed {total_rewritten} event(s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
246
cccl_upstream/ci/compile_time/render_pr_comment.py
Executable file
246
cccl_upstream/ci/compile_time/render_pr_comment.py
Executable file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
with path.open(encoding="utf-8") as f:
|
||||
payload = json.load(f)
|
||||
if not isinstance(payload, dict):
|
||||
raise SystemExit(f"{path} must contain a JSON object")
|
||||
return payload
|
||||
|
||||
|
||||
def md_escape(value: object) -> str:
|
||||
text = str(value)
|
||||
return (
|
||||
text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("|", "\\|")
|
||||
.replace("\n", " ")
|
||||
)
|
||||
|
||||
|
||||
def md_code_span(value: object) -> str:
|
||||
text = str(value).replace("\n", " ")
|
||||
max_backtick_run = 0
|
||||
current_backtick_run = 0
|
||||
for char in text:
|
||||
if char == "`":
|
||||
current_backtick_run += 1
|
||||
max_backtick_run = max(max_backtick_run, current_backtick_run)
|
||||
else:
|
||||
current_backtick_run = 0
|
||||
delimiter = "`" * (max_backtick_run + 1)
|
||||
if text.startswith("`") or text.endswith("`"):
|
||||
text = f" {text} "
|
||||
return f"{delimiter}{text}{delimiter}"
|
||||
|
||||
|
||||
def render_event_name(row: dict[str, Any]) -> str:
|
||||
event_name = row.get("event_name", "")
|
||||
event_key = row.get("event_key", "")
|
||||
if event_key:
|
||||
return f"{md_escape(event_name)}: {md_code_span(event_key)}"
|
||||
return md_escape(event_name)
|
||||
|
||||
|
||||
def render_rows(rows: list[dict[str, Any]], *, direction: str) -> str:
|
||||
delta_heading = (
|
||||
"Regression impact" if direction == "worse" else "Improvement impact"
|
||||
)
|
||||
lines = [
|
||||
f"| Rank | {delta_heading} | Selected Δ | Baseline | Current | Event | Matched traces |",
|
||||
"| ---: | ---: | ---: | ---: | ---: | --- | ---: |",
|
||||
]
|
||||
for row in rows:
|
||||
lines.append(
|
||||
"| {rank} | `{impact}` | `{selected_delta}` | `{baseline}` | `{current}` | {event} | {traces} |".format(
|
||||
rank=md_escape(row.get("rank", "")),
|
||||
impact=md_escape(row.get("impact_magnitude_s", "")),
|
||||
selected_delta=md_escape(row.get("selected_delta_s", "")),
|
||||
baseline=md_escape(row.get("baseline_selected_s", "")),
|
||||
current=md_escape(row.get("current_selected_s", "")),
|
||||
event=render_event_name(row),
|
||||
traces=md_escape(row.get("matched_trace_count", "")),
|
||||
)
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_direction_details(
|
||||
slice_title: str,
|
||||
direction: str,
|
||||
rows: list[dict[str, Any]],
|
||||
) -> str:
|
||||
if not rows:
|
||||
return ""
|
||||
label = "Regressions" if direction == "worse" else "Improvements"
|
||||
icon = "🔴" if direction == "worse" else "🟢"
|
||||
return "\n".join(
|
||||
[
|
||||
"<details>",
|
||||
f"<summary><strong>{icon} {md_escape(slice_title)} — {label}</strong></summary>",
|
||||
"",
|
||||
render_rows(rows, direction=direction),
|
||||
"",
|
||||
"</details>",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def render_warning_details(slice_title: str, warnings: list[Any]) -> str:
|
||||
if not warnings:
|
||||
return ""
|
||||
lines = [
|
||||
"<details open>",
|
||||
f"<summary><strong>⚠️ {md_escape(slice_title)} — Warnings</strong></summary>",
|
||||
"",
|
||||
]
|
||||
lines.extend(f"- {md_escape(warning)}" for warning in warnings)
|
||||
lines.extend(["", "</details>"])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_slice(slice_data: dict[str, Any], *, level: int = 3) -> str:
|
||||
comparison = slice_data.get("comparison", {})
|
||||
worse_rows = comparison.get("worse", {}).get("rows", [])
|
||||
better_rows = comparison.get("better", {}).get("rows", [])
|
||||
warnings = slice_data.get("warnings", [])
|
||||
child_sections = [
|
||||
rendered
|
||||
for child in slice_data.get("children", [])
|
||||
if (rendered := render_slice(child, level=level + 1))
|
||||
]
|
||||
direct_sections = [
|
||||
section
|
||||
for section in (
|
||||
render_warning_details(slice_data.get("title", "Slice"), warnings),
|
||||
render_direction_details(
|
||||
slice_data.get("title", "Slice"), "worse", worse_rows
|
||||
),
|
||||
render_direction_details(
|
||||
slice_data.get("title", "Slice"), "better", better_rows
|
||||
),
|
||||
)
|
||||
if section
|
||||
]
|
||||
if not direct_sections and not child_sections:
|
||||
return ""
|
||||
|
||||
heading_prefix = "#" * min(level, 6)
|
||||
subtitle = (
|
||||
f"`-f {slice_data.get('filter', '')}` "
|
||||
f"`{slice_data.get('timing', '')}` "
|
||||
f"`--sort {slice_data.get('sort', '')}`"
|
||||
)
|
||||
lines = [
|
||||
f"{heading_prefix} {md_escape(slice_data.get('title', 'Slice'))}",
|
||||
"",
|
||||
subtitle,
|
||||
"",
|
||||
]
|
||||
lines.extend(join_sections(direct_sections))
|
||||
if child_sections:
|
||||
lines.extend(["", *join_sections(child_sections)])
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def join_sections(sections: list[str]) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for section in sections:
|
||||
if lines:
|
||||
lines.append("")
|
||||
lines.append(section)
|
||||
return lines
|
||||
|
||||
|
||||
def count_rows(slice_data: dict[str, Any], direction: str) -> int:
|
||||
comparison = slice_data.get("comparison", {})
|
||||
total = len(comparison.get(direction, {}).get("rows", []))
|
||||
return total + sum(
|
||||
count_rows(child, direction) for child in slice_data.get("children", [])
|
||||
)
|
||||
|
||||
|
||||
def count_warnings(slice_data: dict[str, Any]) -> int:
|
||||
return len(slice_data.get("warnings", [])) + sum(
|
||||
count_warnings(child) for child in slice_data.get("children", [])
|
||||
)
|
||||
|
||||
|
||||
def render_comment(
|
||||
summary: dict[str, Any],
|
||||
config: dict[str, Any],
|
||||
*,
|
||||
artifacts_url: str,
|
||||
) -> str:
|
||||
config_id = str(config["id"])
|
||||
slices = summary.get("slices", [])
|
||||
sections = [
|
||||
section for slice_data in slices if (section := render_slice(slice_data))
|
||||
]
|
||||
worse_count = sum(count_rows(slice_data, "worse") for slice_data in slices)
|
||||
better_count = sum(count_rows(slice_data, "better") for slice_data in slices)
|
||||
warning_count = sum(count_warnings(slice_data) for slice_data in slices)
|
||||
result = (
|
||||
f"**Result:** {worse_count} regression row(s), "
|
||||
f"{better_count} improvement row(s) above threshold."
|
||||
)
|
||||
if warning_count:
|
||||
result += f" {warning_count} warning(s)."
|
||||
|
||||
lines = [
|
||||
f"<!-- cccl-compile-time-bench: {md_escape(config_id)} -->",
|
||||
f"## ⏱️ CCCL compile-time benchmark comparison: {md_escape(config.get('name', config_id))}",
|
||||
"",
|
||||
result,
|
||||
"",
|
||||
"| Run | Value |",
|
||||
"| --- | --- |",
|
||||
f"| Config | {md_code_span(config_id)} |",
|
||||
f"| Baseline | {md_code_span(config.get('baseline_ref', ''))} |",
|
||||
f"| Preset | {md_code_span(config.get('preset', ''))} |",
|
||||
f"| Targets | {md_code_span(', '.join(config.get('targets', [])))} |",
|
||||
f"| GPU / launch args | {md_code_span(config.get('gpu', ''))} / {md_code_span(config.get('launch_args', ''))} |",
|
||||
"",
|
||||
f"**Artifacts:** [reports and traces]({artifacts_url})",
|
||||
"",
|
||||
]
|
||||
if sections:
|
||||
lines.extend(join_sections(sections))
|
||||
else:
|
||||
lines.append(
|
||||
"No compile-time benchmark changes exceeded the configured thresholds."
|
||||
)
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render a GitHub PR comment from compile-time report JSON."
|
||||
)
|
||||
parser.add_argument("--summary", type=Path, required=True)
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument("--artifacts-url", required=True)
|
||||
parser.add_argument("-o", "--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
comment = render_comment(
|
||||
load_json(args.summary),
|
||||
load_json(args.config),
|
||||
artifacts_url=args.artifacts_url,
|
||||
)
|
||||
if args.output:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(comment, encoding="utf-8")
|
||||
else:
|
||||
print(comment, end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1718
cccl_upstream/ci/compile_time/summarize_events.py
Executable file
1718
cccl_upstream/ci/compile_time/summarize_events.py
Executable file
File diff suppressed because it is too large
Load Diff
147
cccl_upstream/ci/compile_time/summarize_tus.py
Executable file
147
cccl_upstream/ci/compile_time/summarize_tus.py
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
GENERATED_TU_MARKER = "/headers/"
|
||||
GENERATED_TU_SOURCE_SUFFIXES = (".cu", ".cpp", ".cxx", ".cc", ".c")
|
||||
PREPROCESSED_TU_SUFFIX = ".cpp4.ii"
|
||||
PREPROCESSED_TU_SUFFIXES = (".cpp4.ii", ".ii")
|
||||
|
||||
|
||||
def strip_generated_tu_suffix(path_text: str) -> str:
|
||||
for suffix in GENERATED_TU_SOURCE_SUFFIXES:
|
||||
if path_text.endswith(suffix):
|
||||
return path_text[: -len(suffix)]
|
||||
return path_text
|
||||
|
||||
|
||||
def generated_tu_input(tu: Path) -> str:
|
||||
parts = tu.as_posix().split(GENERATED_TU_MARKER, 1)
|
||||
if len(parts) != 2:
|
||||
return tu.as_posix()
|
||||
|
||||
rel = parts[1].split("/", 1)
|
||||
if len(rel) != 2:
|
||||
return tu.as_posix()
|
||||
|
||||
return strip_generated_tu_suffix(rel[1])
|
||||
|
||||
|
||||
def find_preprocessed_tus(build_dir: Path) -> list[Path]:
|
||||
return sorted(
|
||||
{
|
||||
path
|
||||
for suffix in PREPROCESSED_TU_SUFFIXES
|
||||
for path in build_dir.glob(f"**/headers/**/*{suffix}")
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def tu_source_for_preprocessed_tu(pp_path: Path) -> Path:
|
||||
pp_text = pp_path.as_posix()
|
||||
for suffix in PREPROCESSED_TU_SUFFIXES:
|
||||
if pp_text.endswith(suffix):
|
||||
return Path(pp_text[: -len(suffix)])
|
||||
return pp_path.with_suffix("")
|
||||
|
||||
|
||||
def run_cloc(preprocessed_tus: list[Path], processes: int) -> dict[str, int]:
|
||||
if not preprocessed_tus:
|
||||
return {}
|
||||
|
||||
command = [
|
||||
"cloc",
|
||||
"--csv",
|
||||
"--by-file",
|
||||
"--skip-uniqueness",
|
||||
"--processes",
|
||||
str(processes),
|
||||
"--force-lang=C++,ii",
|
||||
*[path.as_posix() for path in preprocessed_tus],
|
||||
]
|
||||
result = subprocess.run(command, check=True, capture_output=True, text=True)
|
||||
|
||||
loc_by_file: dict[str, int] = {}
|
||||
reader = csv.reader(result.stdout.splitlines())
|
||||
for row in reader:
|
||||
if len(row) < 5 or row[1] == "filename":
|
||||
continue
|
||||
try:
|
||||
loc_by_file[row[1]] = int(row[4])
|
||||
except ValueError:
|
||||
continue
|
||||
return loc_by_file
|
||||
|
||||
|
||||
def write_summary(
|
||||
output_csv: Path,
|
||||
preprocessed_tus: list[Path],
|
||||
loc_by_file: dict[str, int],
|
||||
) -> None:
|
||||
output_csv.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output_csv.open("w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(
|
||||
f,
|
||||
fieldnames=[
|
||||
"tu_input",
|
||||
"transitive_loc",
|
||||
"tu_source",
|
||||
"preprocessed_tu",
|
||||
],
|
||||
)
|
||||
writer.writeheader()
|
||||
for pp_path in preprocessed_tus:
|
||||
tu_path = tu_source_for_preprocessed_tu(pp_path)
|
||||
writer.writerow(
|
||||
{
|
||||
"tu_input": generated_tu_input(tu_path),
|
||||
"transitive_loc": loc_by_file.get(pp_path.as_posix(), 0),
|
||||
"tu_source": tu_path.as_posix(),
|
||||
"preprocessed_tu": pp_path.as_posix(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Summarize generated TU inputs and preprocessed LOC."
|
||||
)
|
||||
parser.add_argument("--build-dir", required=True, type=Path)
|
||||
parser.add_argument("--output-csv", required=True, type=Path)
|
||||
parser.add_argument(
|
||||
"--cloc-processes",
|
||||
type=int,
|
||||
default=0,
|
||||
help="cloc process count; 0 uses nproc --all --ignore=2 when available",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
build_dir = args.build_dir.resolve(strict=False)
|
||||
preprocessed_tus = find_preprocessed_tus(build_dir)
|
||||
if not preprocessed_tus:
|
||||
raise SystemExit(f"no preprocessed generated TUs found under {build_dir}")
|
||||
|
||||
processes = args.cloc_processes
|
||||
if processes <= 0:
|
||||
try:
|
||||
processes = int(
|
||||
subprocess.check_output(
|
||||
["nproc", "--all", "--ignore=2"], text=True
|
||||
).strip()
|
||||
)
|
||||
except (subprocess.SubprocessError, ValueError):
|
||||
processes = 1
|
||||
|
||||
write_summary(
|
||||
output_csv=args.output_csv,
|
||||
preprocessed_tus=preprocessed_tus,
|
||||
loc_by_file=run_cloc(preprocessed_tus, processes),
|
||||
)
|
||||
print(f"wrote {len(preprocessed_tus)} generated TU row(s) to {args.output_csv}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1731
cccl_upstream/ci/compile_time/test_summarize_events.py
Normal file
1731
cccl_upstream/ci/compile_time/test_summarize_events.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user