Skip to content

Commit 707cd0f

Browse files
authored
Replace per-iter conan info subprocess with cached /search API (#2098)
## Summary Library-builder hot path was shelling out to `conan info -r ceserver .` once per build combination to derive the conan package_id. Replaced with a single `GET /v1/conans/{lib}/{ver}/{lib}/{ver}/search` cached on the builder instance, with client-side settings matching. Pattern ported from `compiler-explorer/lib/buildenvsetup/ceconan.ts` (@partouf's existing rewrite on the Node.js side). Match rules - headeronly / cshared compiler wildcards, libcxx and arch wildcards under those, stdver always-match - extracted as module-level helpers (`match_conan_settings`, `conan_search_url`, `build_target_settings` in `library_builder.py`) and shared across all four builder classes (c++, rust, fortran, go) to avoid drift. Refs #1342. ## Performance Measured locally with conan 1.66 against the live conan proxy: | | per-iteration cost | |---|---| | old (`conan info` subprocess) | ~335ms | | new (cached `/search` + dict scan) | ~0.5ms after a one-off ~1s search | | iterations | old | new | speedup | |--:|--:|--:|--:| | 20 | 6.7s | 1.0s | 7x | | 40 | 13.4s | 1.0s | 13x | | 200 | 67s | 1.1s | 60x | End-to-end timing on a real lin-builder is the natural follow-up; couldn't run that locally because CE customises conan's `settings.yml` with non-standard values (e.g. `compiler.version=g141`, the `flagcollection` setting) that only exist on the builder bootstrap. ## Subtle bits - **`set_as_uploaded` ordering.** The new search endpoint can only see packages already on the remote, so the original ordering (`get_conan_hash` -> conditional `upload_builds`) would have crashed every first-time upload with `RuntimeError: Error determining conan hash`. Reordered to `get_build_annotations` -> conditional `upload_builds` -> `get_conan_hash`. `upload_builds` invalidates `_possible_builds`, so the post-upload `get_conan_hash` sees the fresh search response. `test_set_as_uploaded_first_time_does_not_raise` is the regression test for this path; it fails on the un-reordered version. - **TS port faithfulness.** Match rules check the candidate's specific key (e.g. `candidate.get("compiler.version") in ("headeronly", "cshared")`) rather than a single up-front predicate, mirroring the TS implementation. Worth comparing to `ceconan.ts:249-275` if reviewing the matcher. - **URL encoding** uses `urllib.parse.quote(s, safe="")` to match `encodeURIComponent` semantics for lib/version path segments. - **Cache-invalidation point.** All four `upload_builds` set `self._possible_builds = None` after a successful upload, so the next `get_conan_hash` in the same builder instance refetches. ## Out of scope - TTL on `hasFailedBefore` (the trunk-poisoning aspect of #1342). Separate, server-side concern. - Consolidating `resil_get` / HTTP-retry into a shared helper across the four builders. Pre-existing drift, not made worse by this PR. ## Test plan - [x] `make test` (636 passed, 1 skipped) - [x] `make static-checks` clean - [x] Two passes of code review by an LLM agent - [x] Live validation against `conan.compiler-explorer.com`: real boost_bin/clang_barry hash returned by the matcher resolves to a real annotation; libcxx mismatch returns None; eigen/3.4.0 headeronly matches across compiler/libcxx/arch but rejects on os mismatch - [x] Regression test for fresh-upload path verified to fail when the ordering fix is reverted - [ ] End-to-end timing on a real lin-builder (follow-up; needs CE's customised conan settings)
1 parent 24ecd1b commit 707cd0f

8 files changed

Lines changed: 666 additions & 128 deletions

bin/lib/fortran_library_builder.py

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from lib.amazon_properties import get_properties_compilers_and_libraries, get_specific_library_version_details
2626
from lib.installation_context import FetchFailure, PostFailure
2727
from lib.library_build_config import LibraryBuildConfig
28+
from lib.library_builder import PossibleBuilds, build_target_settings, fetch_possible_builds, find_matching_package_id
2829
from lib.library_platform import LibraryPlatform
2930
from lib.staging import StagingDir
3031

@@ -46,7 +47,6 @@
4647
_supports_x86: dict[str, Any] = defaultdict(lambda: [])
4748

4849
GITCOMMITHASH_RE = re.compile(r"^(\w*)\s.*")
49-
CONANINFOHASH_RE = re.compile(r"\s+ID:\s(\w*)")
5050

5151

5252
def _quote(string: str) -> str:
@@ -98,7 +98,7 @@ def __init__(
9898
self.needs_uploading = 0
9999
self.libid = self.libname # TODO: CE libid might be different from yaml libname
100100
self.conanserverproxy_token = None
101-
self._conan_hash_cache: dict[str, str | None] = {}
101+
self._possible_builds: PossibleBuilds | None = None
102102
self._annotations_cache: dict[str, dict] = {}
103103
self.http_session = requests.Session()
104104

@@ -450,25 +450,21 @@ def makebuildhash(self, compiler, options, toolchain, buildos, buildtype, arch,
450450

451451
return compiler + "_" + hasher.hexdigest()
452452

453-
def get_conan_hash(self, buildfolder: str) -> str | None:
454-
if buildfolder in self._conan_hash_cache:
455-
self.logger.debug(f"Using cached conan hash for {buildfolder}")
456-
return self._conan_hash_cache[buildfolder]
457-
458-
if not self.install_context.dry_run:
459-
self.logger.debug(["conan", "info", "."] + self.current_buildparameters)
460-
conaninfo = subprocess.check_output(
461-
["conan", "info", "-r", "ceserver", "."] + self.current_buildparameters, cwd=buildfolder
462-
).decode("utf-8", "ignore")
463-
self.logger.debug(conaninfo)
464-
match = CONANINFOHASH_RE.search(conaninfo, re.MULTILINE)
465-
if match:
466-
result = match[1]
467-
self._conan_hash_cache[buildfolder] = result
468-
return result
453+
def _get_possible_builds(self) -> PossibleBuilds:
454+
if self._possible_builds is None:
455+
self._possible_builds = fetch_possible_builds(
456+
self.libname,
457+
self.target_name,
458+
lambda url: self.http_session.get(url, timeout=_TIMEOUT),
459+
self.logger,
460+
)
461+
return self._possible_builds
469462

470-
self._conan_hash_cache[buildfolder] = None
471-
return None
463+
def get_conan_hash(self, buildfolder: str) -> str | None:
464+
if self.install_context.dry_run:
465+
return None
466+
target = build_target_settings(self.current_buildparameters_obj)
467+
return find_matching_package_id(self._get_possible_builds(), target)
472468

473469
def conanproxy_login(self):
474470
url = f"{conanserver_url}/login"
@@ -581,15 +577,19 @@ def is_already_uploaded(self, buildfolder):
581577
return False
582578

583579
def set_as_uploaded(self, buildfolder):
580+
# We need the conan package_id (a deterministic SHA from compiler+version+libcxx+arch+...)
581+
# to PUT annotations against /annotations/{lib}/{ver}/{package_id}. get_conan_hash now
582+
# derives the id by querying the server's /search index, which only lists packages
583+
# already on the server -- so for a freshly built package, we must upload first.
584+
annotations = self.get_build_annotations(buildfolder)
585+
if "commithash" not in annotations:
586+
self.upload_builds()
587+
584588
conanhash = self.get_conan_hash(buildfolder)
585589
if conanhash is None:
586590
raise RuntimeError(f"Error determining conan hash in {buildfolder}")
587591

588-
self.logger.info(f"commithash: {conanhash}")
589-
590-
annotations = self.get_build_annotations(buildfolder)
591-
if "commithash" not in annotations:
592-
self.upload_builds()
592+
self.logger.info(f"conanhash: {conanhash}")
593593
annotations["commithash"] = self.get_commit_hash()
594594

595595
self.logger.info(annotations)
@@ -705,6 +705,7 @@ def upload_builds(self):
705705
self.logger.debug("Clearing cache to speed up next upload")
706706
subprocess.check_call(["conan", "remove", "-f", f"{self.libname}/{self.target_name}"])
707707
self.needs_uploading = 0
708+
self._possible_builds = None
708709

709710
def get_compiler_type(self, compiler):
710711
compilerType = ""

bin/lib/go_library_builder.py

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import json
1212
import logging
1313
import os
14-
import re
1514
import shutil
1615
import subprocess
1716
from collections import defaultdict
@@ -29,6 +28,7 @@
2928
from lib.golang_stdlib import go_supports_trimpath
3029
from lib.installation_context import InstallationContext, PostFailure
3130
from lib.library_build_config import LibraryBuildConfig
31+
from lib.library_builder import PossibleBuilds, build_target_settings, fetch_possible_builds, find_matching_package_id
3232
from lib.library_platform import LibraryPlatform
3333
from lib.staging import StagingDir
3434

@@ -77,8 +77,6 @@ def get_build_method(compiler_id: str) -> str:
7777
# Cache for compiler properties
7878
_propsandlibs: dict[str, Any] = defaultdict(lambda: [])
7979

80-
CONANINFOHASH_RE = re.compile(r"\s+ID:\s(\w*)")
81-
8280

8381
def clear_properties_cache() -> None:
8482
"""Clear the compiler properties cache. Used for testing."""
@@ -133,7 +131,7 @@ def __init__(
133131
# Prefix with 'go_' to avoid Conan namespace collisions with other languages
134132
self.libid = f"go_{self.libname}"
135133
self.conanserverproxy_token: str | None = None
136-
self._conan_hash_cache: dict[str, str | None] = {}
134+
self._possible_builds: PossibleBuilds | None = None
137135
self._annotations_cache: dict[str, dict] = {}
138136
self.http_session = requests.Session()
139137

@@ -412,28 +410,22 @@ def writeconanfile(self, buildfolder: Path) -> None:
412410
f.write(' self.copy("module_sources/*", dst=".", keep_path=True)\n')
413411
f.write(' self.copy("metadata.json", dst=".", keep_path=False)\n')
414412

415-
def get_conan_hash(self, buildfolder: Path) -> str | None:
416-
"""Query Conan for package hash."""
417-
if str(buildfolder) in self._conan_hash_cache:
418-
return self._conan_hash_cache[str(buildfolder)]
413+
def _get_possible_builds(self) -> PossibleBuilds:
414+
if self._possible_builds is None:
415+
self._possible_builds = fetch_possible_builds(
416+
self.libid,
417+
self.target_name,
418+
lambda url: self.http_session.get(url, timeout=_TIMEOUT),
419+
self.logger,
420+
)
421+
return self._possible_builds
419422

420-
if not self.install_context.dry_run:
421-
try:
422-
conaninfo = subprocess.check_output(
423-
["conan", "info", "-r", "ceserver", "."] + self.current_buildparameters,
424-
cwd=buildfolder,
425-
timeout=_TIMEOUT,
426-
).decode("utf-8", "ignore")
427-
match = CONANINFOHASH_RE.search(conaninfo)
428-
if match:
429-
result = match[1]
430-
self._conan_hash_cache[str(buildfolder)] = result
431-
return result
432-
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
433-
self.logger.debug("Conan info failed: %s", e)
434-
435-
self._conan_hash_cache[str(buildfolder)] = None
436-
return None
423+
def get_conan_hash(self, buildfolder: Path) -> str | None:
424+
"""Find the conan package_id matching the current build parameters, via the proxy search API."""
425+
if self.install_context.dry_run:
426+
return None
427+
target = build_target_settings(self.current_buildparameters_obj)
428+
return find_matching_package_id(self._get_possible_builds(), target)
437429

438430
def resil_post(self, url: str, json_data: str, headers: dict | None = None) -> requests.Response | dict:
439431
"""Resilient POST request with retries."""
@@ -557,15 +549,19 @@ def is_already_uploaded(self, buildfolder: Path) -> bool:
557549

558550
def set_as_uploaded(self, buildfolder: Path, build_method: str) -> None:
559551
"""Mark build as uploaded in Conan server."""
552+
# We need the conan package_id (a deterministic SHA from compiler+version+libcxx+arch+...)
553+
# to PUT annotations against /annotations/{lib}/{ver}/{package_id}. get_conan_hash now
554+
# derives the id by querying the server's /search index, which only lists packages
555+
# already on the server -- so for a freshly built package, we must upload first.
556+
annotations = self.get_build_annotations(buildfolder)
557+
if "commithash" not in annotations:
558+
self.upload_builds()
559+
560560
conanhash = self.get_conan_hash(buildfolder)
561561
if conanhash is None:
562562
raise RuntimeError(f"Error determining conan hash in {buildfolder}")
563563

564564
self.logger.info("conanhash: %s", conanhash)
565-
566-
annotations = self.get_build_annotations(buildfolder)
567-
if "commithash" not in annotations:
568-
self.upload_builds()
569565
annotations["commithash"] = self.target_name
570566
annotations["build_method"] = build_method
571567

@@ -606,6 +602,7 @@ def upload_builds(self) -> None:
606602
self.logger.debug("Clearing cache to speed up next upload")
607603
subprocess.check_call(["conan", "remove", "-f", f"{self.libid}/{self.target_name}"])
608604
self.needs_uploading = 0
605+
self._possible_builds = None
609606

610607
def build_cleanup(self, buildfolder: Path) -> None:
611608
"""Clean up build folder."""

0 commit comments

Comments
 (0)