From 180ab8f765e18561eaa99c2f7af3627b1f5e2f9c Mon Sep 17 00:00:00 2001 From: Jamie <2119834+jamieQ@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:36:47 -0500 Subject: [PATCH] perf(size): Add compact Swift demangling rollout Update CwlDemangle to v1.2.0 and allow the compact JSON summary output to be enabled with a runtime option. Keep full JSON as the default during rollout. --- scripts/deps | 8 ++++---- sentry-options/schemas/launchpad/schema.json | 5 +++++ src/launchpad/artifact_processor.py | 5 ++++- src/launchpad/size/analyzers/apple.py | 8 +++++++- src/launchpad/size/symbols/partitioner.py | 6 ++++-- src/launchpad/size/symbols/swift_aggregator.py | 6 +++--- src/launchpad/utils/apple/cwl_demangle.py | 13 ++++--------- tests/integration/test_cwl_demangle.py | 10 ++++++++-- tests/unit/artifacts/test_artifact_processor.py | 13 ++++++++++++- 9 files changed, 51 insertions(+), 23 deletions(-) diff --git a/scripts/deps b/scripts/deps index 897d7eb8..fa6ab575 100755 --- a/scripts/deps +++ b/scripts/deps @@ -163,8 +163,8 @@ DEPS = [ ), Dep( "cwl-demangle", - "https://github.com/getsentry/CwlDemangle/releases/download/v1.1.0/cwl-demangle-linux-x86_64", - "e15449f24a73c184c05640100ec4e0c1e9c05c3542b497e9243eb4c3c1d607dd", + "https://github.com/getsentry/CwlDemangle/releases/download/v1.2.0/cwl-demangle-linux-x86_64", + "f3c543536c1c35d3c61e8f88cd96daa0f45a1145f123b6d6227fa3598464a262", architecture="x86_64", system="linux", binaries=[ @@ -173,8 +173,8 @@ DEPS = [ ), Dep( "cwl-demangle", - "https://github.com/getsentry/CwlDemangle/releases/download/v1.1.0/cwl-demangle-macos-arm64", - "a91dee08c3794fbea2009bfe52262b25df6d4c486539e85fd906aca4fa9e9d7f", + "https://github.com/getsentry/CwlDemangle/releases/download/v1.2.0/cwl-demangle-macos-arm64", + "8de11faa6582db54f7a8c2c72c63252e4f3b6b59fdbba6f785efe97f3ddcba71", architecture="aarch64", system="darwin", binaries=[ diff --git a/sentry-options/schemas/launchpad/schema.json b/sentry-options/schemas/launchpad/schema.json index 3e9cb7c3..9a394167 100644 --- a/sentry-options/schemas/launchpad/schema.json +++ b/sentry-options/schemas/launchpad/schema.json @@ -14,6 +14,11 @@ "type": "integer", "default": 4, "description": "Number of worker processes for Apple per-binary analysis. 0 runs binaries in-process, one at a time." + }, + "size.swift_demangling.json_summary.enabled": { + "type": "boolean", + "default": false, + "description": "Use the compact JSON summary output from cwl-demangle." } } } diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index 95e7d1c8..39f8c9b4 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -278,7 +278,10 @@ def _create_analyzer(self, artifact: AndroidArtifact | AppleArtifact) -> Android if isinstance(artifact, AndroidArtifact): return AndroidAnalyzer() elif isinstance(artifact, AppleArtifact): - return AppleAppAnalyzer(binary_analysis_workers=get_option("size.binary_analysis.workers", 4)) + return AppleAppAnalyzer( + binary_analysis_workers=get_option("size.binary_analysis.workers", 4), + use_json_summary=get_option("size.swift_demangling.json_summary.enabled", False), + ) else: raise ValueError(f"Unknown artifact kind {artifact}") diff --git a/src/launchpad/size/analyzers/apple.py b/src/launchpad/size/analyzers/apple.py index 22dc44ad..5f5dda0e 100644 --- a/src/launchpad/size/analyzers/apple.py +++ b/src/launchpad/size/analyzers/apple.py @@ -128,6 +128,7 @@ def __init__( skip_image_analysis: bool = False, skip_insights: bool = False, binary_analysis_workers: int = _DEFAULT_BINARY_ANALYSIS_WORKERS, + use_json_summary: bool = False, ) -> None: """Initialize the Apple analyzer. @@ -140,9 +141,11 @@ def __init__( skip_image_analysis: Skip image analysis for faster processing skip_insights: Skip insights generation for faster analysis binary_analysis_workers: Processes for per-binary analysis; 0 runs binaries in-process + use_json_summary: Use compact JSON output when demangling Swift symbols """ self.working_dir = working_dir self.binary_analysis_workers = binary_analysis_workers + self.use_json_summary = use_json_summary self.skip_swift_metadata = skip_swift_metadata self.skip_symbols = skip_symbols self.skip_component_analysis = skip_component_analysis @@ -630,7 +633,10 @@ def _analyze_binary( dsym_index = dsym_arch_map[arch_slice.arch_name] dsym_binary = dwarf_fat_binary.at(dsym_index) slice_symbol_sizes = MachOSymbolSizes(dsym_binary).get_symbol_sizes() - arch_slice.symbol_info = SymbolInfo.from_symbol_sizes(symbol_sizes=slice_symbol_sizes) + arch_slice.symbol_info = SymbolInfo.from_symbol_sizes( + symbol_sizes=slice_symbol_sizes, + use_json_summary=self.use_json_summary, + ) logger.debug( f"Symbolicated {arch_slice.arch_name} slice with {len(slice_symbol_sizes)} symbols" ) diff --git a/src/launchpad/size/symbols/partitioner.py b/src/launchpad/size/symbols/partitioner.py index 150df910..c5fc0f50 100644 --- a/src/launchpad/size/symbols/partitioner.py +++ b/src/launchpad/size/symbols/partitioner.py @@ -87,7 +87,7 @@ def _is_compiler_generated(mangled_name: str) -> bool: @classmethod @sentry_sdk.trace - def from_symbol_sizes(cls, symbol_sizes: List[SymbolSize]) -> "SymbolInfo": + def from_symbol_sizes(cls, symbol_sizes: List[SymbolSize], use_json_summary: bool = False) -> "SymbolInfo": with sentry_sdk.start_span(op="partition_symbols", description="Partition symbols by language"): swift_symbols = SwiftSymbolList() objc_symbols = ObjCSymbolList() @@ -108,7 +108,9 @@ def from_symbol_sizes(cls, symbol_sizes: List[SymbolSize]) -> "SymbolInfo": other_symbols.append(symbol) # Aggregate each partition - swift_type_groups = SwiftSymbolTypeAggregator().aggregate_symbols(swift_symbols) + swift_type_groups = SwiftSymbolTypeAggregator(use_json_summary=use_json_summary).aggregate_symbols( + swift_symbols + ) objc_type_groups = ObjCSymbolTypeAggregator().aggregate_symbols(objc_symbols) cpp_type_groups = CppSymbolTypeAggregator().aggregate_symbols(cpp_symbols) diff --git a/src/launchpad/size/symbols/swift_aggregator.py b/src/launchpad/size/symbols/swift_aggregator.py index 3c5d3fe4..bfb7660e 100644 --- a/src/launchpad/size/symbols/swift_aggregator.py +++ b/src/launchpad/size/symbols/swift_aggregator.py @@ -22,8 +22,8 @@ class SwiftModuleType(NamedTuple): class SwiftSymbolTypeAggregator: """Aggregates symbols by their module/type after demangling.""" - def __init__(self) -> None: - self.demangler = CwlDemangler() + def __init__(self, use_json_summary: bool = False) -> None: + self.demangler = CwlDemangler(use_json_summary=use_json_summary) @staticmethod def is_swift_symbol(mangled_name: str) -> bool: @@ -56,7 +56,7 @@ def aggregate_symbols(self, symbol_sizes: SwiftSymbolList) -> list[SwiftSymbolTy if demangled_result: # Use module and type from demangled result module = demangled_result.module or "Unattributed" - type_name = demangled_result.typeName or demangled_result.type or "Unattributed" + type_name = demangled_result.typeName or "Unattributed" else: # Fallback for symbols that couldn't be demangled module = "Unattributed" diff --git a/src/launchpad/utils/apple/cwl_demangle.py b/src/launchpad/utils/apple/cwl_demangle.py index ddebe786..bcbc3614 100644 --- a/src/launchpad/utils/apple/cwl_demangle.py +++ b/src/launchpad/utils/apple/cwl_demangle.py @@ -25,13 +25,9 @@ class CwlDemangleResult: """Result from cwl-demangle tool parsing.""" - name: str - type: str - identifier: str module: str testName: List[str] typeName: str - description: str mangled: str @@ -42,6 +38,7 @@ def __init__( self, is_type: bool = False, continue_on_error: bool = True, + use_json_summary: bool = False, ): """ Initialize the CwlDemangler. @@ -49,11 +46,13 @@ def __init__( Args: is_type: Whether to treat inputs as types rather than symbols continue_on_error: Whether to continue processing on errors + use_json_summary: Whether to request compact JSON output """ self.is_type = is_type self.queue: List[str] = [] self.continue_on_error = continue_on_error self.uuid = str(uuid.uuid4()) + self.json_output_flag = "--json-summary" if use_json_summary else "--json" # Disable parallel processing if LAUNCHPAD_NO_PARALLEL_DEMANGLE=true env_disable = os.environ.get("LAUNCHPAD_NO_PARALLEL_DEMANGLE", "").lower() == "true" @@ -153,7 +152,7 @@ def _demangle_chunk(self, chunk: List[str], chunk_idx: int) -> Dict[str, CwlDema "batch", "--input", temp_file.name, - "--json", + self.json_output_flag, ] if self.is_type: @@ -183,13 +182,9 @@ def _demangle_chunk(self, chunk: List[str], chunk_idx: int) -> Dict[str, CwlDema mangled = symbol_result.get("mangled", "") if mangled in chunk_set: demangle_result = CwlDemangleResult( - name=symbol_result["name"], - type=symbol_result["type"], - identifier=symbol_result["identifier"], module=symbol_result["module"], testName=symbol_result["testName"], typeName=symbol_result["typeName"], - description=symbol_result["description"], mangled=mangled, ) results[mangled] = demangle_result diff --git a/tests/integration/test_cwl_demangle.py b/tests/integration/test_cwl_demangle.py index aea29523..f67a766a 100644 --- a/tests/integration/test_cwl_demangle.py +++ b/tests/integration/test_cwl_demangle.py @@ -2,6 +2,8 @@ from unittest import mock +import pytest + from launchpad.utils.apple.cwl_demangle import CwlDemangler, CwlDemangleResult @@ -28,9 +30,10 @@ def test_demangle_all_empty_queue(self): result = demangler.demangle_all() assert result == {} - def test_demangle_all_success(self): + @pytest.mark.parametrize("use_json_summary", [False, True]) + def test_demangle_all_success(self, use_json_summary: bool): """Test successful demangling with real cwl-demangle.""" - demangler = CwlDemangler() + demangler = CwlDemangler(use_json_summary=use_json_summary) demangler.add_name( "_$s6Sentry0A14OnDemandReplayC8addFrame33_70FE3B80E922CEF5576FF378226AFAE1LL5image9forScreenySo7UIImageC_SSSgtF" ) @@ -59,6 +62,9 @@ def test_demangle_all_success(self): first_result.mangled == "_$s6Sentry0A14OnDemandReplayC8addFrame33_70FE3B80E922CEF5576FF378226AFAE1LL5image9forScreenySo7UIImageC_SSSgtF" ) + assert first_result.module == "Sentry" + assert first_result.typeName == "SentryOnDemandReplay" + assert first_result.testName == ["Sentry", "SentryOnDemandReplay", "addFrame(image,forScreen)"] second_result = result[ "_$s6Sentry0A18UserFeedbackWidgetC18RootViewControllerC6config6buttonAeA0abC13ConfigurationC_AA0abcd6ButtonF0Ctcfc" diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index 2e8002cf..12d3393d 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -655,7 +655,7 @@ def test_env_and_option_are_unioned_and_deduped(self, monkeypatch): assert get_service_config().projects_to_skip == ["shared", "env-only", "opt-only"] -class TestCreateAnalyzerBinaryAnalysisWorkers: +class TestCreateAppleAnalyzerOptions: def test_apple_analyzer_uses_schema_default(self): processor = ArtifactProcessor(Mock(), Mock(), Mock()) analyzer = processor._create_analyzer(Mock(spec=AppleArtifact)) @@ -666,3 +666,14 @@ def test_apple_analyzer_uses_option_value(self): with override_options("launchpad", {"size.binary_analysis.workers": 0}): analyzer = processor._create_analyzer(Mock(spec=AppleArtifact)) assert analyzer.binary_analysis_workers == 0 + + def test_json_summary_uses_schema_default(self): + processor = ArtifactProcessor(Mock(), Mock(), Mock()) + analyzer = processor._create_analyzer(Mock(spec=AppleArtifact)) + assert analyzer.use_json_summary is False + + def test_json_summary_uses_option_value(self): + processor = ArtifactProcessor(Mock(), Mock(), Mock()) + with override_options("launchpad", {"size.swift_demangling.json_summary.enabled": True}): + analyzer = processor._create_analyzer(Mock(spec=AppleArtifact)) + assert analyzer.use_json_summary is True