Generate snapshots in parallel (#56184)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/56184

Changelog: [Internal]

Updates the snapshot generator script to run generation steps for all snapshots in parallel instead of in sequence, significantly improving the generation time.

Reviewed By: cortinico

Differential Revision: D97714334

fbshipit-source-id: b643dd7bb2fa4f3f9db1495bc7a44c0069a25274
This commit is contained in:
Jakub Piasecki
2026-03-24 02:34:59 -07:00
committed by meta-codesync[bot]
parent 6b764a2ebd
commit 32f1dcd804
4 changed files with 117 additions and 56 deletions
@@ -74,7 +74,7 @@ PROJECT_ICON =
# entered, it will be relative to the location where Doxygen was started. If
# left blank the current directory will be used.
OUTPUT_DIRECTORY = api
OUTPUT_DIRECTORY = ${OUTPUT_DIR}
# If the CREATE_SUBDIRS tag is set to YES then Doxygen will create up to 4096
# sub-directories (in 2 levels) under the output directory of each output format
@@ -74,7 +74,7 @@ PROJECT_ICON =
# entered, it will be relative to the location where Doxygen was started. If
# left blank the current directory will be used.
OUTPUT_DIRECTORY = api
OUTPUT_DIRECTORY = ${OUTPUT_DIR}
# If the CREATE_SUBDIRS tag is set to YES then Doxygen will create up to 4096
# sub-directories (in 2 levels) under the output directory of each output format
+96 -43
View File
@@ -12,11 +12,12 @@ Usage:
"""
import argparse
import concurrent.futures
import os
import shutil
import subprocess
import sys
import tempfile
import traceback
from .config import ApiViewSnapshotConfig, parse_config_file
from .doxygen import get_doxygen_bin, run_doxygen
@@ -36,13 +37,20 @@ def run_command(
if result.returncode != 0:
if verbose:
print(f"{label} finished with error: {result.stderr}")
sys.exit(1)
raise RuntimeError(
f"{label} finished with error (exit code {result.returncode})"
)
elif verbose:
print(f"{label} finished successfully")
return result
def build_codegen(platform: str, verbose: bool = False) -> str:
def build_codegen(
platform: str,
verbose: bool = False,
output_path: str = "./api/codegen",
label: str = "",
) -> str:
react_native_dir = os.path.join(get_react_native_dir(), "packages", "react-native")
run_command(
@@ -52,17 +60,21 @@ def build_codegen(platform: str, verbose: bool = False) -> str:
"--path",
"./",
"--outputPath",
"./api/codegen",
output_path,
"--targetPlatform",
platform,
"--forceOutputPath",
],
label="Codegen",
label=f"[{label}] Codegen" if label else "Codegen",
verbose=verbose,
cwd=react_native_dir,
capture_output=True,
text=True,
)
return os.path.join(react_native_dir, "api", "codegen")
if os.path.isabs(output_path):
return output_path
return os.path.join(react_native_dir, output_path)
def build_snapshot_for_view(
@@ -75,21 +87,29 @@ def build_snapshot_for_view(
codegen_platform: str | None = None,
verbose: bool = True,
input_filter: str = None,
work_dir: str | None = None,
) -> str:
if verbose:
print(f"Generating API view: {api_view}")
print(f"[{api_view}] Generating API view")
api_dir = os.path.join(react_native_dir, "api")
if os.path.exists(api_dir):
if verbose:
print(" Deleting existing output directory")
shutil.rmtree(api_dir)
include_directories = list(include_directories)
if work_dir is None:
work_dir = os.path.join(react_native_dir, "api")
if codegen_platform is not None:
codegen_dir = build_codegen(codegen_platform, verbose=verbose)
codegen_output = os.path.join(work_dir, "codegen")
codegen_dir = build_codegen(
codegen_platform,
verbose=verbose,
output_path=codegen_output,
label=api_view,
)
include_directories.append(codegen_dir)
elif verbose:
print(" Skipping codegen")
print(f"[{api_view}] Skipping codegen")
config_file = f".doxygen.config.{api_view}.generated"
run_doxygen(
working_dir=react_native_dir,
@@ -98,12 +118,15 @@ def build_snapshot_for_view(
definitions=definitions,
input_filter=input_filter,
verbose=verbose,
output_dir=work_dir,
config_file=config_file,
label=api_view,
)
if verbose:
print(" Building snapshot")
print(f"[{api_view}] Building snapshot")
snapshot = build_snapshot(os.path.join(api_dir, "xml"))
snapshot = build_snapshot(os.path.join(work_dir, "xml"))
snapshot_string = snapshot.to_string()
output_file = os.path.join(output_dir, f"{api_view}Cxx.api")
@@ -126,36 +149,66 @@ def build_snapshots(
is_test: bool = False,
) -> None:
if not is_test:
for config in snapshot_configs:
if view_filter and config.snapshot_name != view_filter:
continue
configs_to_build = [
config
for config in snapshot_configs
if not view_filter or config.snapshot_name == view_filter
]
build_snapshot_for_view(
api_view=config.snapshot_name,
react_native_dir=react_native_dir,
include_directories=config.inputs,
exclude_patterns=config.exclude_patterns,
definitions=config.definitions,
output_dir=output_dir,
codegen_platform=config.codegen_platform,
verbose=verbose,
input_filter=input_filter if config.input_filter else None,
)
with tempfile.TemporaryDirectory(prefix="cxx-api-") as parent_tmp:
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {}
for config in configs_to_build:
work_dir = os.path.join(parent_tmp, config.snapshot_name)
os.makedirs(work_dir, exist_ok=True)
future = executor.submit(
build_snapshot_for_view,
api_view=config.snapshot_name,
react_native_dir=react_native_dir,
include_directories=config.inputs,
exclude_patterns=config.exclude_patterns,
definitions=config.definitions,
output_dir=output_dir,
codegen_platform=config.codegen_platform,
verbose=verbose,
input_filter=input_filter if config.input_filter else None,
work_dir=work_dir,
)
futures[future] = config.snapshot_name
errors = []
for future in concurrent.futures.as_completed(futures):
view_name = futures[future]
try:
future.result()
except Exception as e:
errors.append((view_name, e))
if verbose:
print(
f"[{view_name}] Error generating:\n"
f"{traceback.format_exc()}"
)
if errors:
failed_views = ", ".join(name for name, _ in errors)
raise RuntimeError(f"Failed to generate snapshots: {failed_views}")
else:
snapshot = build_snapshot_for_view(
api_view="Test",
react_native_dir=react_native_dir,
include_directories=[],
exclude_patterns=[],
definitions={},
output_dir=output_dir,
codegen_platform=None,
verbose=verbose,
input_filter=input_filter,
)
with tempfile.TemporaryDirectory(prefix="cxx-api-test-") as work_dir:
snapshot = build_snapshot_for_view(
api_view="Test",
react_native_dir=react_native_dir,
include_directories=[],
exclude_patterns=[],
definitions={},
output_dir=output_dir,
codegen_platform=None,
verbose=verbose,
input_filter=input_filter,
work_dir=work_dir,
)
if verbose:
print(snapshot)
if verbose:
print(snapshot)
def get_default_snapshot_dir() -> str:
+19 -11
View File
@@ -9,7 +9,6 @@ Doxygen configuration and execution utilities.
import os
import subprocess
import sys
_DOXYGEN_CONFIG_FILE = ".doxygen.config.generated"
@@ -24,6 +23,8 @@ def build_doxygen_config(
exclude_patterns: list[str] = None,
definitions: dict[str, str | int] = None,
input_filter: str = None,
output_dir: str = "api",
config_file: str = _DOXYGEN_CONFIG_FILE,
) -> None:
if include_directories is None:
include_directories = []
@@ -54,9 +55,10 @@ def build_doxygen_config(
.replace("${EXCLUDE_PATTERNS}", exclude_patterns_str)
.replace("${PREDEFINED}", definitions_str)
.replace("${DOXYGEN_INPUT_FILTER}", input_filter_str)
.replace("${OUTPUT_DIR}", output_dir)
)
with open(os.path.join(directory, _DOXYGEN_CONFIG_FILE), "w") as f:
with open(os.path.join(directory, config_file), "w") as f:
f.write(config)
@@ -67,10 +69,14 @@ def run_doxygen(
definitions: dict[str, str | int],
input_filter: str = None,
verbose: bool = True,
output_dir: str = "api",
config_file: str = _DOXYGEN_CONFIG_FILE,
label: str = "",
) -> None:
"""Generate Doxygen config, run Doxygen, and clean up the config file."""
prefix = f"[{label}] " if label else ""
if verbose:
print(" Generating Doxygen config file")
print(f"{prefix}Generating Doxygen config file")
build_doxygen_config(
working_dir,
@@ -78,17 +84,19 @@ def run_doxygen(
exclude_patterns=exclude_patterns,
definitions=definitions,
input_filter=input_filter,
output_dir=output_dir,
config_file=config_file,
)
if verbose:
print(" Running Doxygen")
print(f"{prefix}Running Doxygen")
if input_filter:
print(f" Using input filter: {input_filter}")
print(f"{prefix}Using input filter: {input_filter}")
doxygen_bin = get_doxygen_bin()
result = subprocess.run(
[doxygen_bin, _DOXYGEN_CONFIG_FILE],
[doxygen_bin, config_file],
cwd=working_dir,
capture_output=True,
text=True,
@@ -96,11 +104,11 @@ def run_doxygen(
if result.returncode != 0:
if verbose:
print(f" Doxygen finished with error: {result.stderr}")
sys.exit(1)
print(f"{prefix}Doxygen finished with error: {result.stderr}")
raise RuntimeError(f"Doxygen finished with error: {result.stderr}")
elif verbose:
print(" Doxygen finished successfully")
print(f"{prefix}Doxygen finished successfully")
if verbose:
print(" Deleting Doxygen config file")
os.remove(os.path.join(working_dir, _DOXYGEN_CONFIG_FILE))
print(f"{prefix}Deleting Doxygen config file")
os.remove(os.path.join(working_dir, config_file))