Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .github/workflows/multi_arch_build_portable_linux_artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,35 @@ jobs:

- name: Build stage
run: |
cmake --build "${BUILD_DIR}" --target stage-${STAGE_NAME} therock-artifacts -- -k 0
mkdir -p "${BUILD_DIR}/logs"
set -o pipefail
cmake --build "${BUILD_DIR}" --target stage-${STAGE_NAME} therock-artifacts -- -k 0 2>&1 | tee "${BUILD_DIR}/logs/build_stage.log"

# Preserve project-level timings for the ASAN dashboard. Ninja records
# completed targets even when a later target fails, so this also captures
# useful partial data on failed builds.
- name: Capture project build timings
if: always()
run: |
if [[ -f "${BUILD_DIR}/.ninja_log" ]]; then
python3 build_tools/analyze_build_times.py \
--build-dir "${BUILD_DIR}" \
--json-output "${BUILD_DIR}/logs/asan_build_metrics.json" \
--stage "${STAGE_NAME}" \
--architectures "${AMDGPU_FAMILIES}" \
--conclusion "${{ job.status }}"
else
echo "Ninja timing log was not created; no project timing artifact to upload."
fi

- name: Upload project build timings
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: asan-build-metrics-${{ inputs.stage_name }}-${{ inputs.amdgpu_family || 'shared' }}
path: ${{ env.BUILD_DIR }}/logs/asan_build_metrics.json
if-no-files-found: ignore
retention-days: 90

- name: Generate failed log summaries
if: ${{ !cancelled() }}
Expand Down
60 changes: 60 additions & 0 deletions build_tools/analyze_build_times.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"""

import argparse
import json
import os
import re
import sys
Expand Down Expand Up @@ -332,6 +333,26 @@ def calculate_wall_time(tasks: List[Task]) -> int:
return max_end - min_start


def find_failed_components(build_dir: Path) -> List[str]:
"""Identify project names from Ninja's explicit FAILED target lines."""
log_path = build_dir / "logs" / "build_stage.log"
if not log_path.exists():
return []
build_prefix = str(build_dir.resolve()).replace("\\", "/").rstrip("/") + "/"
failed = set()
for line in log_path.read_text(encoding="utf-8", errors="replace").splitlines():
match = re.search(r"\bFAILED:\s*(.+)$", line)
if not match:
continue
output = match.group(1).strip().replace("\\", "/")
if output.startswith(build_prefix):
output = output[len(build_prefix) :]
name, _category, _phase = parse_output_path(output)
if name:
failed.add(name)
return sorted(failed)


def format_time_human(ms: int) -> str:
"""Format milliseconds to human readable time (e.g., 1h 23.5m or 45.32 min)."""
minutes = ms / 60000
Expand Down Expand Up @@ -479,6 +500,14 @@ def main():
"--build-dir", type=Path, required=True, help="Path to build directory"
)
parser.add_argument("--output", type=Path, help="Path to output HTML file")
parser.add_argument(
"--json-output", type=Path, help="Write machine-readable project phase timings"
)
parser.add_argument("--stage", help="Build stage name for machine-readable output")
parser.add_argument(
"--architectures", help="Architecture family or families for this stage"
)
parser.add_argument("--conclusion", help="GitHub job conclusion at capture time")
args = parser.parse_args()

ninja_log = args.build_dir / ".ninja_log"
Expand All @@ -489,6 +518,37 @@ def main():
tasks = parse_ninja_log(ninja_log)
projects = analyze_tasks(tasks, args.build_dir)

if args.json_output:
records = []
for category, components in projects.items():
for name, phases in components.items():
for phase, milliseconds in phases.items():
if milliseconds > 0:
records.append(
{
"component": name,
"category": category,
"phase": phase,
"milliseconds": milliseconds,
}
)
args.json_output.parent.mkdir(parents=True, exist_ok=True)
args.json_output.write_text(
json.dumps(
{
"schemaVersion": 1,
"stage": args.stage,
"architectures": args.architectures or "shared",
"conclusion": args.conclusion or "unknown",
"failedComponents": find_failed_components(args.build_dir),
"wallMilliseconds": calculate_wall_time(tasks),
"projects": records,
},
indent=2,
)
+ "\n"
)

output_file = args.output or args.build_dir / "logs" / "build_observability.html"
output_file.parent.mkdir(parents=True, exist_ok=True)
generate_report(projects, tasks, output_file, args.build_dir)
Expand Down
Loading