From 7f6a3a4e46922d737b8053c7cd5968a02a558ae3 Mon Sep 17 00:00:00 2001 From: Denis Gomes <6731020+denisg0mes@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:53:16 -0400 Subject: [PATCH] M7: font manager scan: drain Core Text's autoreleased objects; core-text 22 Review on #1309 pointed out that core-text 20.1's get_descriptors retains the array it gets under the create rule a second time, so every scan kept one array of font descriptors alive. Checking that turned up a larger leak: Core Text autoreleases objects while it builds the collection, and the scan runs where no autorelease pool is active (the app's background scan thread, the CLI, the MCP server), so nothing ever drained them. Measured with `figcraft-cli run` of N `text.rescanFonts` steps on macOS 15 (peak memory footprint): | | 10 rescans | 200 rescans | |---|---|---| | main, without this PR | 221 MB | 220 MB | | this PR before the fix | 228 MB | 796 MB | | with the fix | 226 MB | 225 MB | A standalone Swift loop over the same Core Text calls grows from 9 MB to 759 MB over 200 scans without a pool and stays at 20 MB inside one, so the pool is what matters; the core-text bump removes the over-retain on top. - The query runs inside objc2::rc::autoreleasepool. objc2 0.6 is already in the lockfile (winit's), and the function is safe, so the workspace's unsafe_code = "forbid" still holds. - core-text goes to =22.0.0, the first release that takes ownership of the array correctly. It brings core-foundation 0.10, core-graphics 0.25 and core-graphics-types 0.2 beside the older versions other crates use. 22.0 moved framework linking behind a `link` feature that is on by default, so with default features off it is named explicitly. A rescan takes about 0.2 s instead of 0.08 s on that Mac (266 more font files are indexed); it only runs when asked for. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 40 +++++++++++++++++++++++++++++++-------- crates/text/Cargo.toml | 10 ++++++---- crates/text/src/fontdb.rs | 10 ++++++---- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac37cf4c..474a1ca3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,7 +855,20 @@ checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "core-graphics-types", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", "foreign-types", "libc", ] @@ -872,17 +885,27 @@ dependencies = [ ] [[package]] -name = "core-text" -version = "20.1.0" +name = "core-graphics-types" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "core-foundation", - "core-graphics", - "foreign-types", + "bitflags 2.13.2", + "core-foundation 0.10.1", "libc", ] +[[package]] +name = "core-text" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333dab512ce710ca2d08574c373d246dbeac8b22769e47da4c0e72730ce442b7" +dependencies = [ + "core-foundation 0.10.1", + "core-graphics 0.25.0", + "foreign-types", +] + [[package]] name = "core_detect" version = "1.0.0" @@ -1796,6 +1819,7 @@ dependencies = [ "figcraft-geom", "harfrust 0.13.3", "log", + "objc2 0.6.5", "png", "skrifa 0.47.0", "unicode-bidi", @@ -5992,7 +6016,7 @@ dependencies = [ "cfg_aliases", "concurrent-queue", "core-foundation 0.9.4", - "core-graphics", + "core-graphics 0.23.2", "cursor-icon", "dpi", "js-sys", diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index d1b983af..5322ca58 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -21,8 +21,10 @@ log = { workspace = true } # Fonts that font managers (RightFont, FontBase, Typeface) activate without copying them into a font # folder are known only to Core Text. Its safe wrappers list every available font's file (Core Text -# needs unsafe FFI, which the workspace forbids). Its dependencies (core-foundation 0.9, -# core-graphics 0.23, foreign-types 0.5, libc) are already in the lockfile. Pinned exactly: review -# the diff before bumping. +# needs unsafe FFI, which the workspace forbids). 22.0 is the first release whose +# `get_descriptors` takes ownership of the array correctly (20.x and 21.x leak it on every scan); +# it brings core-foundation 0.10, core-graphics 0.25 and core-graphics-types 0.2 beside the older +# versions other crates use. Pinned exactly: review the diff before bumping. [target.'cfg(target_os = "macos")'.dependencies] -core-text = { version = "=20.1.0", default-features = false } +core-text = { version = "=22.0.0", default-features = false, features = ["link"] } +objc2 = "0.6" diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index 30319bc1..6fa096ae 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -850,10 +850,12 @@ fn files_outside_dirs(files: Vec, dirs: &[std::path::PathBuf #[cfg(target_os = "macos")] fn font_manager_files() -> Vec { std::panic::catch_unwind(|| { - core_text::font_collection::create_for_all_families() - .get_descriptors() - .map(|descriptors| descriptors.iter().filter_map(|descriptor| descriptor.font_path()).collect()) - .unwrap_or_default() + objc2::rc::autoreleasepool(|_| { + core_text::font_collection::create_for_all_families() + .get_descriptors() + .map(|descriptors| descriptors.iter().filter_map(|descriptor| descriptor.font_path()).collect()) + .unwrap_or_default() + }) }) .unwrap_or_default() }