diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 9b6c45267654..60d7c0dcdb46 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -927,7 +927,10 @@ jobs: env: HOSTED_COMPILE_SECONDS: ${{ steps.hosted-compile.outputs.seconds }} HOSTED_COMPILE_OUTCOME: ${{ steps.hosted-compile.outcome }} - SEED_DISTANCE: ${{ steps.seed-derived-data.outputs.seed_distance || steps.prefer-seed.outputs.seed_distance }} + # Report only the seed that adopt actually installed. prefer-seed's + # distance describes a candidate and can remain set when adoption + # fails or is skipped. + SEED_DISTANCE: ${{ steps.seed-derived-data.outputs.seed_distance }} SEED_SECONDS: ${{ steps.seed-derived-data.outputs.seconds }} run: | set -euo pipefail diff --git a/scripts/ci/build_metrics.py b/scripts/ci/build_metrics.py index 2ba998e21277..77a1c2a04ffb 100644 --- a/scripts/ci/build_metrics.py +++ b/scripts/ci/build_metrics.py @@ -33,6 +33,14 @@ r"^\s*(.+?)(?:\s+\(\d+\s+tasks?\)\s+\|)?\s+" r"([0-9]+(?:\.[0-9]+)?) seconds\s*$" ) +CACHE_PLUGIN_SETTING_RE = re.compile( + r"\bCOMPILATION_CACHE_ENABLE_PLUGIN\s*=\s*YES\b", + re.IGNORECASE, +) +CACHE_REMOTE_SETTING_RE = re.compile( + r"\bCOMPILATION_CACHE_REMOTE_SERVICE_PATH\s*=\s*\S+", + re.IGNORECASE, +) CACHE_VALUES = {"Cache hit": "hit", "Cache miss": "miss"} @@ -140,6 +148,15 @@ def parse_log(path: Path) -> dict[str, object]: "swift_compile_events": sum(swift_compile_by_target.values()), "swift_emit_module_events": sum(swift_emit_by_target.values()), "cacheable_tasks": cache["hit"] + cache["miss"], + # The command's build-settings echo is the only portable evidence of + # whether Xcode was given the fleet plugin/socket. Keep the value + # categorical; never copy the socket path into telemetry. + "cache_backend": ( + "fleet" + if CACHE_PLUGIN_SETTING_RE.search("\n".join(lines)) + and CACHE_REMOTE_SETTING_RE.search("\n".join(lines)) + else "local" + ), "timing_summary_seconds": timing_summary(lines), "targets": targets, } @@ -162,6 +179,7 @@ def aggregate(schemes: list[dict[str, object]]) -> dict[str, object]: return { **dict(totals), + "cache_backend": "fleet" if any(scheme.get("cache_backend") == "fleet" for scheme in schemes) else "local", "timing_summary_seconds": dict(sorted(timing.items())), "targets": { target: dict(values) @@ -233,8 +251,10 @@ def build_receipt( "hit_rate": round(hits / cacheable, 6) if cacheable else None, "seed_distance": seed_distance, "compile_seconds": compile_phase, + "compile_wall_seconds": compile_seconds, "fetch_seconds": fetch_seconds, "link_seconds": link_phase, + "cache_backend": aggregate_values.get("cache_backend", "local"), } return { "schema_version": 1, diff --git a/tests/test_build_metrics.py b/tests/test_build_metrics.py index 88ecd51506b0..db18b4eb5c1c 100644 --- a/tests/test_build_metrics.py +++ b/tests/test_build_metrics.py @@ -4,6 +4,9 @@ import importlib.util import json from pathlib import Path +import os +import subprocess +import sys import tempfile import unittest @@ -56,6 +59,14 @@ ** BUILD SUCCEEDED ** """ +FLEET_CACHE_LOG = """COMPILATION_CACHE_ENABLE_PLUGIN = YES +COMPILATION_CACHE_REMOTE_SERVICE_PATH = /Users/Shared/cmux-build-fleet/xcode/fleet-cas.sock +Build Timing Summary +CompileSwiftSources (2 tasks) | 3.000 seconds +Ld (1 task) | 0.500 seconds +** BUILD SUCCEEDED ** +""" + class BuildMetricsTests(unittest.TestCase): def test_parse_log_attributes_cache_and_swift_work(self): @@ -156,6 +167,11 @@ def test_ci_wires_advisory_receipt_and_timing_summary(self): self.assertIn("steps.hosted-compile.outcome != 'skipped'", workflow) self.assertIn("--compile-outcome \"$HOSTED_COMPILE_OUTCOME\"", workflow) self.assertIn("--host-telemetry \"$RUNNER_TEMP/glaeda-compile-telemetry.json\"", workflow) + self.assertIn("SEED_DISTANCE: ${{ steps.seed-derived-data.outputs.seed_distance }}", workflow) + self.assertNotIn( + "SEED_DISTANCE: ${{ steps.seed-derived-data.outputs.seed_distance || steps.prefer-seed.outputs.seed_distance }}", + workflow, + ) self.assertIn("steps.build-metrics.outcome == 'success'", workflow) self.assertIn("continue-on-error: true", workflow) @@ -179,10 +195,56 @@ def test_receipt_discovers_logs_and_activity_metadata(self): self.assertEqual(receipt["compiler_cache"]["cacheable_tasks"], 3) self.assertEqual(receipt["compiler_cache"]["compile_seconds"], 12.5) self.assertEqual(receipt["compiler_cache"]["link_seconds"], 2.25) + self.assertEqual(receipt["compiler_cache"]["compile_wall_seconds"], 42.5) + self.assertEqual(receipt["compiler_cache"]["cache_backend"], "local") self.assertEqual(receipt["derived_data_log_count"], 1) self.assertEqual(receipt["activity_logs"], [{"name": "one.xcactivitylog", "bytes": 3}]) json.dumps(receipt) + def test_receipt_identifies_fleet_cache_backend_without_emitting_paths(self): + with tempfile.TemporaryDirectory() as directory: + derived = Path(directory) + (derived / "cmux-build.log").write_text(FLEET_CACHE_LOG) + + receipt = build_metrics.build_receipt(derived, 9.25, compile_outcome="success") + + self.assertEqual(receipt["compiler_cache"]["cache_backend"], "fleet") + self.assertEqual(receipt["compiler_cache"]["compile_wall_seconds"], 9.25) + self.assertNotIn("fleet-cas.sock", json.dumps(receipt["compiler_cache"])) + + def test_host_sidecar_carries_wall_time_and_backend(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + derived = root / "derived" + derived.mkdir() + (derived / "cmux-build.log").write_text(FLEET_CACHE_LOG) + output = root / "receipt.json" + host = root / "host.json" + env = {**os.environ, "GITHUB_RUN_ID": "123", "GITHUB_RUN_ATTEMPT": "2"} + subprocess.run( + [ + sys.executable, + str(SCRIPT), + str(derived), + "--output", + str(output), + "--compile-seconds", + "9.25", + "--compile-outcome", + "success", + "--host-telemetry", + str(host), + ], + check=True, + env=env, + ) + sidecar = json.loads(host.read_text()) + + self.assertEqual(sidecar["compile_wall_seconds"], 9.25) + self.assertEqual(sidecar["cache_backend"], "fleet") + self.assertEqual(sidecar["run_id"], "123") + self.assertEqual(sidecar["run_attempt"], "2") + if __name__ == "__main__": unittest.main()