Skip to content

Load Sparkle without PyObjC's class scan (~150 MB less memory on macOS) - #2551

Open
ThomasWaldmann wants to merge 1 commit into
borgbase:masterfrom
ThomasWaldmann:fix-memory-retention
Open

Load Sparkle without PyObjC's class scan (~150 MB less memory on macOS)#2551
ThomasWaldmann wants to merge 1 commit into
borgbase:masterfrom
ThomasWaldmann:fix-memory-retention

Conversation

@ThomasWaldmann

Copy link
Copy Markdown
Collaborator

Description

get_updater() loads the Sparkle framework with objc.loadBundle('Sparkle', globals(), bundle_path). PyObjC's default scan_classes=True does not just register Sparkle: it calls objc_getClassList(), realizes every Objective-C class in the process (~67,000 on macOS 15 once Qt's Cocoa plugin and the system frameworks are loaded) and wraps each one as a Python class that is kept in this module's globals for the lifetime of the app. That costs ~150 MB of physical footprint and only happens in the frozen app bundle (the updater is only set up when sys.frozen), which is why the shipped Vorta.app idles at ~220–250 MB while a venv install of the same code idles at ~80–120 MB.

This PR loads only the framework (scan_classes=False) and looks up the one class Vorta uses explicitly with objc.lookUpClass('SUUpdater'). Nothing else changes: SUUpdater was the only bare name taken from the scan (hence the old # noqa: F821), other Sparkle classes remain reachable through objc.lookUpClass, and objects returned from Sparkle are wrapped lazily by PyObjC either way.

Related Issue

Motivation and Context

Vorta is a tray-resident app, so its idle footprint is what users see all day. Measured on the shipped 0.11.5 bundle right after launch (scratch $HOME, no backup run yet): 218 MB with the main window hidden, 250 MB with it shown; heap reported 66,870 realized ObjC classes and 71,919 Python objects (94 MB) allocated by the class scan. A pip install of the same version on the same machine: 91 MB / 120 MB. A 150-cycle borg create run showed no per-backup growth, so the class scan is the dominant cost.

How Has This Been Tested?

  • New unit test tests/unit/test_updater.py (macOS only): asserts objc.loadBundle is called with scan_classes=False and SUUpdater is obtained via objc.lookUpClass.

  • pytest tests/unit: 265 passed, 7 skipped. ruff check / ruff format --check clean.

  • Two app bundles built with PyInstaller from this tree (macOS 15.7.9, Apple M3, Python 3.11, PyQt6 6.6.1), launched under a scratch $HOME with the main window hidden and measured 35 s after launch:

    physical footprint realized ObjC classes
    master (ebf7571) 228.8 MB 66,928
    this branch 78.6 MB 2,322
  • Isolated check with PyObjC 12 and the real Sparkle.framework: the default scan costs +62 MB and adds 15,559 names to the module, of which 57 come from Sparkle; scan_classes=False + lookUpClass costs +1 MB and returns the same class.

Side note for maintainers: building from master needed the spec's datas to collect assets/UI recursively (the dialogs' .ui files moved into subdirectories), otherwise the bundle fails at startup on assets/UI/dialogs/exception.ui. That is unrelated to this change and not included here.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have read the CONTRIBUTING guide.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

I provide my contribution under the terms of the license of this repository and I affirm the Developer Certificate of Origin.

🤖 Generated with Claude Code

…mory

`objc.loadBundle('Sparkle', ...)` defaults to `scan_classes=True`, which wraps
every Objective-C class in the process (~67,000 on macOS 15) as a Python class
and keeps them in this module's globals for the lifetime of the app. Only the
frozen app bundle takes this path, which is why the shipped Vorta.app idles at
~220-250 MB while a venv install of the same code idles at ~80-120 MB.

Load only the framework and look up `SUUpdater` explicitly instead.

Measured on macOS 15.7 (M3) with a bundle built from this tree, main window
hidden, 35 s after launch: physical footprint 228.8 MB -> 78.6 MB, realized
ObjC classes 66,928 -> 2,322.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ThomasWaldmann

Copy link
Copy Markdown
Collaborator Author

@m3nu Have a look!

Looks like Claude Fable 5.1 found a nice tweak to reduce memory usage significantly.

Also, it found some other issues while doing that, see the other PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant