From 2f6c3ed4e2bfa1d4c680cdfa903c7cabc38ff7ab Mon Sep 17 00:00:00 2001 From: Dawid Malecki Date: Mon, 3 Aug 2026 02:28:44 -0700 Subject: [PATCH] Decrease amount of requests send to Maven repos (#57765) Summary: The Ruby scripts that resolve prebuilt artifacts (ReactNativeCore, ReactNativeDependencies, hermes-engine) re-resolve their tarball URLs on every call site and every podspec evaluation during a single pod install. Each resolution re-issues the same requests to the Maven repositories, so identical requests are made many times per install. This PR adds a shared cache in `ReactNativePodsUtils` for RNCore/RNDeps and a self-contained one in `hermes-utils.rb`. ## Changelog: [IOS] [CHANGED] - Cache Maven repository requests (artifact existence probes, nightly metadata) during pod install to avoid re-issuing identical requests on every podspec evaluation Test Plan: I've added logs locally and inspected number of cache hits and misses from the single `pod install` in rn-tester: ``` RCT_USE_PREBUILT_RNCORE=1 RCT_TESTONLY_RNCORE_VERSION=0.81.0 RCT_USE_RN_DEP=1 RCT_DEPS_VERSION=0.81.0 bundle exec pod install 2>&1 | tee /tmp/pod.log ``` ``` [Cache] [bench] MISS probe (requesting): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-dependencies-debug.tar.gz [Cache] [bench] HIT probe (request saved): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-dependencies-debug.tar.gz [Cache] [bench] HIT probe (request saved): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-dependencies-debug.tar.gz [Cache] [bench] MISS probe (requesting): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-dependencies-release.tar.gz [Cache] [bench] MISS probe (requesting): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-core-debug.tar.gz [Cache] [bench] HIT probe (request saved): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-core-debug.tar.gz [Cache] [bench] MISS probe (requesting): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-core-release.tar.gz [Cache] [bench] HIT probe (request saved): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-core-debug.tar.gz [Cache] [bench] HIT probe (request saved): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-dependencies-debug.tar.gz [Cache] [bench] HIT probe (request saved): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-dependencies-debug.tar.gz [Cache] [bench] HIT probe (request saved): https://repo.reactnative.dev/maven2/com/facebook/react/react-native-artifacts/0.81.0/react-native-artifacts-0.81.0-reactnative-dependencies-release.tar.gz [Hermes] [bench] MISS probe (requesting): https://repo.reactnative.dev/maven2/com/facebook/hermes/hermes-ios/260318099.0.1/hermes-ios-260318099.0.1-hermes-ios-debug.tar.gz [Hermes] [bench] HIT probe (request saved): https://repo.reactnative.dev/maven2/com/facebook/hermes/hermes-ios/260318099.0.1/hermes-ios-260318099.0.1-hermes-ios-debug.tar.gz [Hermes] [bench] HIT probe (request saved): https://repo.reactnative.dev/maven2/com/facebook/hermes/hermes-ios/260318099.0.1/hermes-ios-260318099.0.1-hermes-ios-debug.tar.gz [Hermes] [bench] MISS probe (requesting): https://repo.reactnative.dev/maven2/com/facebook/hermes/hermes-ios/260318099.0.1/hermes-ios-260318099.0.1-hermes-ios-release.tar.gz ``` Differential Revision: D114341998 Pulled By: coado --- .../react-native/scripts/cocoapods/rncore.rb | 10 ++--- .../scripts/cocoapods/rndependencies.rb | 10 ++--- .../react-native/scripts/cocoapods/utils.rb | 42 +++++++++++++++++++ .../sdks/hermes-engine/hermes-utils.rb | 27 +++++++++--- 4 files changed, 74 insertions(+), 15 deletions(-) diff --git a/packages/react-native/scripts/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index 4a62c7808f9..e56ce29b8a1 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -371,7 +371,7 @@ def self.nightly_tarball_url(version, configuration, dsyms = false) artefact_name = "reactnative-core-#{dsyms ? "dSYM-" : ""}#{configuration ? configuration : "debug"}.tar.gz" xml_url = "https://central.sonatype.com/repository/maven-snapshots/com/facebook/react/#{artefact_coordinate}/#{version}-SNAPSHOT/maven-metadata.xml" - response = Net::HTTP.get_response(URI(xml_url)) + response = ReactNativePodsUtils.memoized_get_response(xml_url) if response.is_a?(Net::HTTPSuccess) xml = REXML::Document.new(response.body) timestamp = xml.elements['metadata/versioning/snapshot/timestamp'].text @@ -476,11 +476,11 @@ def self.artifacts_dir() return File.join(Pod::Config.instance.project_pods_root, "ReactNativeCore-artifacts") end - # This function checks that ReactNativeCore artifact exists on the maven repo + # This function checks that ReactNativeCore artifact exists on the maven repo. + # The probe is memoized, so repeated podspec evaluations in one `pod install` + # don't re-request the same URL. def self.artifact_exists(tarball_url) - # -L is used to follow redirects, useful for the nightlies - # I also needed to wrap the url in quotes to avoid escaping & and ?. - return (`curl -o /dev/null --silent -Iw '%{http_code}' -L "#{tarball_url}"` == "200") + return ReactNativePodsUtils.artifact_exists?(tarball_url) end def self.rncore_log(message, level = :info) diff --git a/packages/react-native/scripts/cocoapods/rndependencies.rb b/packages/react-native/scripts/cocoapods/rndependencies.rb index 6843ed27c0b..963290e5fdc 100644 --- a/packages/react-native/scripts/cocoapods/rndependencies.rb +++ b/packages/react-native/scripts/cocoapods/rndependencies.rb @@ -248,7 +248,7 @@ def self.nightly_tarball_url(version, build_type) artifact_name = "reactnative-dependencies-#{build_type.to_s}.tar.gz" xml_url = "https://central.sonatype.com/repository/maven-snapshots/com/facebook/react/#{artifact_coordinate}/#{version}-SNAPSHOT/maven-metadata.xml" - response = Net::HTTP.get_response(URI(xml_url)) + response = ReactNativePodsUtils.memoized_get_response(xml_url) if response.is_a?(Net::HTTPSuccess) xml = REXML::Document.new(response.body) timestamp = xml.elements['metadata/versioning/snapshot/timestamp'].text @@ -378,11 +378,11 @@ def self.artifacts_dir() return File.join(Pod::Config.instance.project_pods_root, "ReactNativeDependencies-artifacts") end - # This function checks that ReactNativeDependencies artifact exists on the maven repo + # This function checks that ReactNativeDependencies artifact exists on the maven repo. + # The probe is memoized, so repeated podspec evaluations in one `pod install` + # don't re-request the same URL. def self.artifact_exists(tarball_url) - # -L is used to follow redirects, useful for the nightlies - # I also needed to wrap the url in quotes to avoid escaping & and ?. - return (`curl -o /dev/null --silent -Iw '%{http_code}' -L "#{tarball_url}"` == "200") + return ReactNativePodsUtils.artifact_exists?(tarball_url) end def self.rndeps_log(message, level = :info) diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index 307c4210c4c..60dcde24f15 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -6,6 +6,7 @@ require 'shellwords' require 'digest' require 'uri' +require 'net/http' require_relative "./helpers.rb" require_relative "./jsengine.rb" @@ -757,6 +758,47 @@ def self.resolve_use_frameworks(spec, header_mappings_dir: nil, module_name: nil end end + # ============================ # + # Network request memoization # + # ============================ # + # CocoaPods evaluates the prebuilt podspecs several times during a single + # `pod install`, and every evaluation re-resolves the artifact URLs from + # scratch: existence probes against the mirror/Maven Central and nightly + # metadata lookups. The answers should not change within one install, so + # each request is issued at most once per process and then served from + # these in-memory caches. + @@artifact_exists_cache = {} + @@get_response_cache = {} + + # Memoized existence probe (HTTP HEAD) for a prebuilt artifact URL. + # Only conclusive answers are cached. If curl never got an HTTP status + # (DNS failure, no route, ...) the probe is left uncached so that a + # transient hiccup doesn't permanently mark the artifact as missing. + def self.artifact_exists?(tarball_url) + unless @@artifact_exists_cache.key?(tarball_url) + # -L is used to follow redirects, useful for the nightlies + # The url is wrapped in quotes to avoid escaping & and ?. + http_code = `curl -o /dev/null --silent -Iw '%{http_code}' -L "#{tarball_url}"` + return false if !$?.success? || http_code == "000" + @@artifact_exists_cache[tarball_url] = (http_code == "200") + end + return @@artifact_exists_cache[tarball_url] + end + + # Memoized HTTP GET for small metadata lookups (Maven snapshot metadata). + # Returns the Net::HTTPResponse. Only successful responses are cached: + # raised network errors propagate uncached, and non-2xx responses (a + # transient 5xx, a 404) are returned without being stored, so a later + # call within the same process can retry. + def self.memoized_get_response(url) + unless @@get_response_cache.key?(url) + response = Net::HTTP.get_response(URI(url)) + return response unless response.is_a?(Net::HTTPSuccess) + @@get_response_cache[url] = response + end + return @@get_response_cache[url] + end + # ==================== # # Shared download cache # # ==================== # diff --git a/packages/react-native/sdks/hermes-engine/hermes-utils.rb b/packages/react-native/sdks/hermes-engine/hermes-utils.rb index 8af5123e99d..71ef0481bbd 100644 --- a/packages/react-native/sdks/hermes-engine/hermes-utils.rb +++ b/packages/react-native/sdks/hermes-engine/hermes-utils.rb @@ -10,6 +10,14 @@ MAVEN_CENTRAL_REPOSITORY = "https://repo1.maven.org/maven2" REACT_NATIVE_MAVEN_MIRROR_REPOSITORY = "https://repo.reactnative.dev/maven2" +# Memoized results of requests to the Maven repositories (mirror or central). +# hermes-engine.podspec is evaluated several times during a single +# `pod install`, and every evaluation re-resolves the artifact source from +# scratch; without memoization that re-issues identical artifact existence +# probes. The answers should not change within one install, so each request +# is issued at most once per process. +HERMES_ARTIFACT_EXISTS_CACHE = {} + module HermesEngineSourceType LOCAL_PREBUILT_TARBALL = :local_prebuilt_tarball DOWNLOAD_PREBUILD_RELEASE_TARBALL = :download_prebuild_release_tarball @@ -339,14 +347,23 @@ def resolve_url_redirects(url) # This function checks that Hermes artifact exists. # As of now it should check it on the Maven repo. +# The probe is memoized, so repeated podspec evaluations in one `pod install` +# don't re-request the same URL. Only conclusive answers are cached: if curl +# never got an HTTP status (DNS failure, no route, ...) the probe is left +# uncached so a transient hiccup doesn't permanently mark the artifact as +# missing. # # Parameters -# - version: the version of React Native -# - build_type: debug or release +# - tarball_url: the URL of the Hermes artifact to probe def hermes_artifact_exists(tarball_url) - # -L is used to follow redirects, useful for the nightlies - # I also needed to wrap the url in quotes to avoid escaping & and ?. - return (`curl -o /dev/null --silent -Iw '%{http_code}' -L "#{tarball_url}"` == "200") + unless HERMES_ARTIFACT_EXISTS_CACHE.key?(tarball_url) + # -L is used to follow redirects, useful for the nightlies + # I also needed to wrap the url in quotes to avoid escaping & and ?. + http_code = `curl -o /dev/null --silent -Iw '%{http_code}' -L "#{tarball_url}"` + return false if !$?.success? || http_code == "000" + HERMES_ARTIFACT_EXISTS_CACHE[tarball_url] = (http_code == "200") + end + return HERMES_ARTIFACT_EXISTS_CACHE[tarball_url] end def hermes_log(message, level = :warning)