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 <noreply@anthropic.com>
This commit is contained in:
Denis Gomes
2026-10-25 18:29:30 -04:00
co-authored by Claude Opus 5.5
parent 5699e7abe6
commit 7f6a3a4e46
3 changed files with 44 additions and 16 deletions
Generated
+32 -8
View File
@@ -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",
+6 -4
View File
@@ -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"
+6 -4
View File
@@ -850,10 +850,12 @@ fn files_outside_dirs(files: Vec<std::path::PathBuf>, dirs: &[std::path::PathBuf
#[cfg(target_os = "macos")]
fn font_manager_files() -> Vec<std::path::PathBuf> {
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()
}