Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# APP-4154: Audit macOS ObjC allocations for leaks
|
||||
|
||||
## Problem
|
||||
|
||||
We pass ObjC objects (`NSString`, `SentryUser`, `NSMutableArray`, ...) into Cocoa APIs from both Rust and our own non-ARC `.m` translation units. These objects aren't tracked by Rust's memory system, and PR #560 (APP-4104) caught a large leak on the Cocoa-Sentry breadcrumb path because the string allocations weren't autoreleased inside a pool. The same pattern can occur anywhere we bridge into Cocoa.
|
||||
|
||||
This spec covers the follow-up audit APP-4104 explicitly flagged: walk every NSString production site (Rust) and every `alloc]`/`new]`/`copy]`/`mutableCopy]` in every non-ARC `.m` file, classify each call site, and fix the leaked ones. The audit ships as multiple small, semantically-related PRs so each slice can be reviewed independently.
|
||||
|
||||
## Scope
|
||||
|
||||
**Rust files** that produce or pass ObjC objects (via `make_nsstring`, `NSString::alloc`, or `msg_send![class!(X), alloc]`) across `app/src/` and `crates/warpui*`. See `nsstring_checklist.md` for the full row set.
|
||||
|
||||
**Non-ARC ObjC translation units** (compiled via `cc::Build` without `-fobjc-arc`):
|
||||
- `app/src/platform/mac/objc/`: `app_bundle.m`, `crash_reporting.m`, `services.m`
|
||||
- `crates/warpui/src/platform/mac/objc/`: `alert.m`, `app.m`, `fullscreen_queue.m`, `host_view.m`, `hotkey.m`, `keycode.m`, `menus.m`, `notifications/notifications.m`, `reachability.m`, `window.m`, `window_blur.m`
|
||||
|
||||
**Out of scope:**
|
||||
- `app/DockTilePlugin/WarpDockTilePlugin.m` (ARC, per `app/DockTilePlugin/Makefile:5`).
|
||||
- The cross-platform `sentry` Rust crate and `app/src/crash_reporting/sentry_minidump.rs` (no ObjC).
|
||||
- Switching non-ARC files to ARC wholesale.
|
||||
- Non-macOS targets.
|
||||
|
||||
## Ambient autorelease pools
|
||||
|
||||
`make_nsstring` (`crates/warpui/src/platform/mac/mod.rs:34`) autoreleases its NSString, so it only works if an `NSAutoreleasePool` is active for the calling scope. Several contexts create one for us:
|
||||
|
||||
- **AppKit main event loop** drains a pool around each event dispatch (delegate callbacks, menu selectors, key/mouse events, timer callbacks).
|
||||
- **GCD blocks** drain a pool around each block invocation (see comment at `crates/warpui/src/platform/mac/objc/reachability.m:75-79`).
|
||||
- **`NSThread` detaches** and ObjC methods wrapped in `@autoreleasepool { ... }` inherit the same guarantee.
|
||||
|
||||
Contexts that do **not** provide an ambient pool:
|
||||
|
||||
- **Rust-spawned threads** (`std::thread::spawn`, Tokio workers, `async_channel` recv loops).
|
||||
- **Sentry callbacks** like `before_breadcrumb` (runs on whichever thread emitted the breadcrumb).
|
||||
- **`lazy_static` / `OnceCell` initializers** triggered from a non-AppKit thread.
|
||||
- **Early `main`** before the AppKit event loop starts.
|
||||
|
||||
## Decision rule for each row
|
||||
|
||||
The audit does not unconditionally wrap every call site in a pool; redundant pools add a per-call push/pop on hot AppKit-event paths. For each row, pick one of:
|
||||
|
||||
1. **`ambient`** — no change. Use when the call is provably reached only from an AppKit event handler or GCD block, and the scope creates a small bounded number of autoreleased temporaries.
|
||||
2. **`local-pool`** — add `NSAutoreleasePool::new(nil)` / `pool.drain()` (Rust) or `@autoreleasepool { ... }` (ObjC). Use when the call can originate from a Rust-spawned thread, Sentry callback, early init, unknown origin (safe default), or when the scope accumulates many temporaries (e.g. `Window::open` at `crates/warpui/src/platform/mac/window.rs:496`).
|
||||
3. **`autorelease-helper`** — replace retained `NSString::alloc(nil).init_str(...)` with `make_nsstring`, or swap `[[Class alloc] init]` for a convenience constructor that returns an autoreleased instance (`[NSMutableArray array]`, `[NSString stringWith...]`). Typically combined with (1) or (2).
|
||||
4. **`explicit-release`** — pair an `alloc]`/`init]` with a matching `[obj release]` after last use. Use when the object is consumed synchronously and the lifetime is obvious. This is the shape of `recordBreadcrumb` in `crash_reporting.m`.
|
||||
|
||||
Default when uncertain about thread origin: (2). Nested pools are correct and cheap.
|
||||
|
||||
`ambient` vs `local-pool` is a perf/peak-memory call, not correctness: an ambient AppKit-event pool is sufficient to prevent leaks on a path that runs on that thread, but a nested `local-pool` drains earlier and bounds peak memory at the cost of a per-call push/pop. Prefer `ambient` only when adding a nested pool has measurable per-call overhead on a hot path; otherwise default to `local-pool`.
|
||||
|
||||
### `hot/cold` classification
|
||||
|
||||
Each row must be marked `hot` or `cold` so the `ambient`/`local-pool` trade-off above can be applied consistently. Treat any call that runs on a per-frame, per-keystroke, per-mouse-move, per-log-event, or otherwise recurring path as `hot`. Treat one-shot init code, user-initiated rare actions (menu click, file picker, settings change), and error/alert paths as `cold`. If in doubt, mark `hot` — the consequence is at worst an unnecessary pool push/pop, which is safer than unbounded peak memory.
|
||||
|
||||
## How to use the checklists
|
||||
|
||||
`nsstring_checklist.md` and `objc_checklist.md` list every call site we care about, grouped by the PR batch that will ship its fix. Each row has the form:
|
||||
|
||||
```
|
||||
- [ ] path/file.ext:line — function/symbol — disposition — thread-origin — hot/cold — strategy — action
|
||||
```
|
||||
|
||||
The trailing columns start as TODOs. When you pick up a batch:
|
||||
|
||||
1. Walk the call graph one level up for each row to determine thread origin and hot/cold classification.
|
||||
2. Fill in the disposition, thread-origin, hot/cold, and strategy columns with your finding.
|
||||
3. Apply the fix per the chosen strategy.
|
||||
4. Tick the row.
|
||||
5. Validate your slice: `cargo fmt` + `cargo check -p warp --bin warp --features gui,cocoa_sentry`.
|
||||
6. Open a PR against `lucie/app-4154-prep` (stacked). Keep the diff under ~200 lines; split by file if needed.
|
||||
|
||||
The checklist files are edited in-place by each batch PR (each PR only touches its own rows, avoiding conflicts). The orchestrator runs `./script/presubmit` once all batches have merged, and re-runs the greps at the top of each checklist to prove completeness.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo fmt` and `cargo check -p warp --bin warp --features gui,cocoa_sentry` on macOS, matching PR #560.
|
||||
- Xcode Instruments Leaks template: rerun the breadcrumb-hammer repro from #560 plus a short session exercising touched UI paths (window open/close, menu open, clipboard, appearance change, file picker). No new `Warp`-owned frames should appear.
|
||||
- Manually fire a Sentry event and confirm tags, user id, and breadcrumbs still round-trip.
|
||||
- `./script/presubmit` before the final merge.
|
||||
|
||||
## References
|
||||
|
||||
- PR #560 / APP-4104: `specs/APP-4104/TECH.md`.
|
||||
- `make_nsstring` helper: `crates/warpui/src/platform/mac/mod.rs:34`.
|
||||
- Reference-correct patterns: `app/src/crash_reporting/mac.rs:60-78` (`forward_breadcrumb`, pooled), `app/src/platform/mac/objc/crash_reporting.m:76-82` (`recordBreadcrumb`, explicit release), `app/src/platform/mac/objc/services.m:28-50` (`@autoreleasepool` block), `crates/warpui/src/platform/mac/objc/window.m:703-746` (mixed autorelease + explicit release).
|
||||
@@ -0,0 +1,107 @@
|
||||
# APP-4154 Phase 1 — NSString checklist
|
||||
|
||||
Every Rust call site that produces or passes an NSString into Cocoa. Each batch agent fills in the `disposition`, `thread-origin`, `hot/cold`, `strategy`, and `action` columns for their rows, applies the fix, and ticks the row. See `TECH.md` for the decision rule.
|
||||
|
||||
## Reproducible greps
|
||||
|
||||
```
|
||||
rg -n 'make_nsstring\b' -g '*.rs'
|
||||
rg -n 'NSString::alloc' -g '*.rs'
|
||||
```
|
||||
|
||||
Ignore these (not call sites):
|
||||
- `crates/warpui/src/platform/mac/mod.rs:34` — definition of `make_nsstring` itself. Excluded because the body is a one-liner that always returns an autoreleased NSString; the leak potential is at its callers, not the definition.
|
||||
- `use ... make_nsstring` import lines.
|
||||
Audited even though it's a definition, not a call:
|
||||
- `crates/warpui_extras/src/user_preferences/user_defaults.rs:88-89` — local `util::make_nsstring` helper definition. Unlike the warpui helper, this one returns a retained `StrongPtr`-wrapped NSString; the definition itself is the correctness point, so it's listed in batch 1.D.
|
||||
|
||||
## Row format
|
||||
|
||||
```
|
||||
- [ ] path:line — function — disposition (retained|autoreleased|?) — thread-origin (appkit-event|gcd-block|rust-thread|unknown|?) — hot/cold — strategy (ambient|local-pool|autorelease-helper|explicit-release|?) — action
|
||||
```
|
||||
|
||||
## Batch 1.A — `sentry-nsstring`
|
||||
|
||||
Files: `app/src/crash_reporting/mac.rs`. Reference pattern (`forward_breadcrumb`, already pooled) kept for consistency.
|
||||
|
||||
- [x] app/src/crash_reporting/mac.rs:28 — `init_cocoa_sentry` — autoreleased — rust-thread (early init from `init_sentry` before AppKit loop) — cold (once per session) — local-pool — wrapped body in `NSAutoreleasePool::new(nil)` / `pool.drain()`
|
||||
- [x] app/src/crash_reporting/mac.rs:30 — `init_cocoa_sentry` — autoreleased — rust-thread — cold — local-pool — covered by the same pool
|
||||
- [x] app/src/crash_reporting/mac.rs:31 — `init_cocoa_sentry` — autoreleased — rust-thread — cold — local-pool — covered by the same pool
|
||||
- [x] app/src/crash_reporting/mac.rs:55 — `set_user_id` — autoreleased — rust-thread (invoked from `set_optional_user_information` on auth state changes and init) — cold — local-pool — wrapped body in `NSAutoreleasePool::new(nil)` / `pool.drain()`
|
||||
- [x] app/src/crash_reporting/mac.rs:71 — `forward_breadcrumb` — autoreleased — rust-thread (Sentry `before_breadcrumb`) — hot — local-pool — already pooled (post-#560), confirmed no-op
|
||||
- [x] app/src/crash_reporting/mac.rs:72 — `forward_breadcrumb` — autoreleased — rust-thread — hot — local-pool — already pooled, confirmed no-op
|
||||
- [x] app/src/crash_reporting/mac.rs:73 — `forward_breadcrumb` — autoreleased — rust-thread — hot — local-pool — already pooled, confirmed no-op
|
||||
- [x] app/src/crash_reporting/mac.rs:82 — `set_tag` (key) — autoreleased — rust-thread (called from `init_cocoa_sentry` loop and `set_tag` wrapper in `mod.rs`) — cold — local-pool — wrapped body in `NSAutoreleasePool::new(nil)` / `pool.drain()`
|
||||
- [x] app/src/crash_reporting/mac.rs:82 — `set_tag` (value) — autoreleased — rust-thread — cold — local-pool — covered by the same pool
|
||||
|
||||
## Batch 1.B — `app-ffi-nsstring`
|
||||
|
||||
Files: `app/src/app_services/mac.rs`, `app/src/appearance.rs`, `app/src/util/file/external_editor/mac.rs`. `app/src/settings_view/appearance_page.rs` and `app/src/lib.rs` were dropped from this batch's scope: the rg invocations at the top of this file show no matches there, and a zero-hit re-grep is sufficient to prove completeness — no rows needed.
|
||||
|
||||
- [x] app/src/app_services/mac.rs:27 — `warp_services_provider_custom_url_scheme` — autoreleased — appkit-event (called from `services.m` inside an `@autoreleasepool` on the NSServices dispatch path) — cold — autorelease-helper — replaced the raw `NSString::alloc(nil).init_str(...).autorelease()` with `make_nsstring(...)`; the ambient ObjC pool owns the returned string.
|
||||
- [x] app/src/appearance.rs:222 — `AppearanceManager::set_app_icon` (plugin_name) — autoreleased — mixed (startup from `lib.rs:1204`, settings/autoupdate completion callbacks) — cold — local-pool — wrapped the `unsafe { … }` body in `NSAutoreleasePool::new(nil)` held by an `AutoreleasePoolGuard` RAII wrapper whose `Drop` impl sends `drain`, so the pool is released on every exit path (early `return`, normal fall-through, or an unexpected panic from an intermediate `msg_send!`).
|
||||
- [x] app/src/appearance.rs:233 — `AppearanceManager::set_app_icon` (image_name) — autoreleased — mixed (see above) — cold — local-pool — covered by the same `AutoreleasePoolGuard` as plugin_name.
|
||||
- [x] app/src/appearance.rs:234 — `AppearanceManager::set_app_icon` (extension) — autoreleased — mixed (see above) — cold — local-pool — covered by the same `AutoreleasePoolGuard` as plugin_name.
|
||||
- [x] app/src/util/file/external_editor/mac.rs:357 — `default_app_to_open_path` / `to_nsstring` helper — was retained (leaked, never released) — main-thread, UI action (`open_file_path_with_line_and_col`) — cold — autorelease-helper + local-pool — swapped to `make_nsstring`, wrapped the body in `NSAutoreleasePool::new(nil) … pool.drain()`, and changed the return type from `Option<&'static str>` to `Option<String>` so the UTF-8 bytes are copied out before the pool drains (the previous `'static` cast was a lie whose only safety net was the leak it caused).
|
||||
|
||||
Before ticking, agent 1.B must re-run the rg invocations at the top of this checklist across the whole workspace and confirm no new hits have landed since this scaffolding was written. Add any new rows that appear.
|
||||
|
||||
## Batch 1.C — `warpui-platform-nsstring`
|
||||
|
||||
Files: `crates/warpui/src/platform/mac/{app.rs, clipboard.rs, delegate.rs, menus.rs, window.rs, keycode.rs}`. If the batch diff exceeds ~200 lines, split by file.
|
||||
|
||||
- [x] crates/warpui/src/platform/mac/app.rs:81 — `create_native_platform_modal` — autoreleased — appkit-event (show_native_platform_modal via AppContext) — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/app.rs:82 — `create_native_platform_modal` — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/app.rs:84 — `create_native_platform_modal` — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/clipboard.rs:43 — `pasteboard_type_for_image_mime_type` — retained — appkit-event (copy action on main thread) — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/clipboard.rs:49 — `Clipboard::write` (plain text) — retained — appkit-event — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/clipboard.rs:55 — `Clipboard::write` (html) — retained — appkit-event — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/clipboard.rs:142 — `read_image_data_from_pasteboard` (public.png) — retained — appkit-event — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/clipboard.rs:143 — `read_image_data_from_pasteboard` (public.jpeg) — retained — appkit-event — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/clipboard.rs:144 — `read_image_data_from_pasteboard` (public.gif) — retained — appkit-event — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/clipboard.rs:145 — `read_image_data_from_pasteboard` (public.webp) — retained — appkit-event — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/clipboard.rs:146 — `read_image_data_from_pasteboard` (public.svg-image) — retained — appkit-event — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/clipboard.rs:147 — `read_image_data_from_pasteboard` (com.compuserve.gif) — retained — appkit-event — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/delegate.rs:257 — `application_bundle_info` — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/delegate.rs:267 — `application_bundle_info` — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/delegate.rs:343 — `send_desktop_notification` (title) — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/delegate.rs:344 — `send_desktop_notification` (body) — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/delegate.rs:345 — `send_desktop_notification` (data) — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/delegate.rs:423 — `microphone_access_state` — retained — appkit-event — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/keycode.rs:50 — `Keycode::keycodes_from_key_name` (`charToKeyCodes` wrapper) — autoreleased — appkit-event (register/unregister global shortcut via AppContext) — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/menus.rs:215 — `resolve_key_equivalent` (empty key_equivalent) — autoreleased — appkit-event (menu item update) — hot — local-pool — covered by pool wrapping `apply_changes` body (hot: AppKit menu validation per open/shortcut)
|
||||
- [x] crates/warpui/src/platform/mac/menus.rs:219 — `resolve_key_equivalent` (special char key equivalent) — autoreleased — appkit-event — hot — local-pool — covered by pool wrapping `apply_changes` body
|
||||
- [x] crates/warpui/src/platform/mac/menus.rs:220 — `resolve_key_equivalent` (literal key equivalent) — autoreleased — appkit-event — hot — local-pool — covered by pool wrapping `apply_changes` body
|
||||
- [x] crates/warpui/src/platform/mac/menus.rs:240 — `apply_changes` (setTitle) — autoreleased — appkit-event — hot — local-pool — wrapped `apply_changes` body in NSAutoreleasePool
|
||||
- [x] crates/warpui/src/platform/mac/menus.rs:265 — `make_submenu` (delegated menu title) — autoreleased — appkit-event (menu rebuild) — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/menus.rs:296 — `make_menu_item` standard-action title — autoreleased — appkit-event (menu rebuild) — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/menus.rs:298 — `make_menu_item` standard-action key equivalent — retained — appkit-event — cold — autorelease-helper — switched to `make_nsstring`
|
||||
- [x] crates/warpui/src/platform/mac/menus.rs:313 — `make_top_level_menu_item` (top-level menu title) — autoreleased — appkit-event (app startup / menubar rebuild) — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:711 — `Window::open_url` — autoreleased — appkit-event (delegate call from AppContext) — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:718 — `Window::open_file_path` — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:726 — `Window::open_file_path_in_explorer` — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:735 — `Window::open_file_picker` (file type mapping) — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:754 — `Window::open_save_file_picker` (default_directory) — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:755 — `Window::open_save_file_picker` (default_directory fallback) — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:758 — `Window::open_save_file_picker` (default_filename) — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:759 — `Window::open_save_file_picker` (default_filename fallback) — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:803 — `Window::set_accessibility_contents` (value) — autoreleased — appkit-event (fires per user action when VoiceOver is enabled) — hot — local-pool — wrapped `Window::set_accessibility_contents` body in NSAutoreleasePool
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:804 — `Window::set_accessibility_contents` (help) — autoreleased — appkit-event — hot — local-pool — covered by pool wrapping `Window::set_accessibility_contents` body
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:805 — `Window::set_accessibility_contents` (role) — autoreleased — appkit-event — hot — local-pool — covered by pool wrapping `Window::set_accessibility_contents` body
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:893 — `Window::set_window_title` — autoreleased — appkit-event — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/window.rs:1230 — `warp_get_accessibility_contents` (C-unwind) — autoreleased — appkit-event (AppKit accessibility callback) — hot — ambient — no-op; local-pool not applicable because the autoreleased NSString is the return value and must outlive this scope
|
||||
|
||||
## Batch 1.D — `warpui-extras-nsstring`
|
||||
|
||||
Files: `crates/warpui_extras/src/user_preferences/user_defaults.rs`.
|
||||
|
||||
This batch also owns the adjacent `msg_send![class!(NSUserDefaults), alloc]` site on line 39 (even though it's Phase-2 by category), because editing lines 39 and 40 from separate PRs would conflict on merge.
|
||||
|
||||
- [x] crates/warpui_extras/src/user_preferences/user_defaults.rs:39 — `UserDefaultsPreferencesStorage::user_defaults` — retained (chained into `initWithSuiteName:` on line 42 and wrapped in `StrongPtr::new`) — rust-thread (startup) — cold — explicit-release — no-op: `alloc` → `initWithSuiteName:` → `StrongPtr::new` takes ownership of the +1 retain; drop releases (Phase 2 row, owned here to avoid adjacency conflicts)
|
||||
- [x] crates/warpui_extras/src/user_preferences/user_defaults.rs:40 — `UserDefaultsPreferencesStorage::user_defaults` — retained (local `util::make_nsstring` returns `StrongPtr`) — rust-thread (startup) — cold — explicit-release — no-op: `StrongPtr` drop at end of scope releases the retained NSString
|
||||
- [x] crates/warpui_extras/src/user_preferences/user_defaults.rs:53 — `UserPreferences::write_value` (key) — retained (local `util::make_nsstring` returns `StrongPtr`) — rust-thread (settings writes) — cold — explicit-release — no-op: `StrongPtr` drop at end of scope releases
|
||||
- [x] crates/warpui_extras/src/user_preferences/user_defaults.rs:54 — `UserPreferences::write_value` (value) — retained (local `util::make_nsstring` returns `StrongPtr`) — rust-thread (settings writes) — cold — explicit-release — no-op: `StrongPtr` drop at end of scope releases
|
||||
- [x] crates/warpui_extras/src/user_preferences/user_defaults.rs:63 — `UserPreferences::read_value` (key) — retained (local `util::make_nsstring` returns `StrongPtr`) — rust-thread (settings reads) — cold — explicit-release — no-op: `StrongPtr` drop at end of scope releases
|
||||
- [x] crates/warpui_extras/src/user_preferences/user_defaults.rs:77 — `UserPreferences::remove_value` (key) — retained (local `util::make_nsstring` returns `StrongPtr`) — rust-thread (settings removes) — cold — explicit-release — no-op: `StrongPtr` drop at end of scope releases
|
||||
- [x] crates/warpui_extras/src/user_preferences/user_defaults.rs:89 — `util::make_nsstring` body (`NSString::alloc(nil).init_str(...)`) — retained (wrapped in `StrongPtr::new`) — n/a (helper) — n/a — explicit-release — no-op: `NSString::alloc(nil).init_str(...)` returns a +1 retained object; `StrongPtr::new` takes ownership without additional retain, and its `Drop` impl sends `release`, balancing the alloc/init
|
||||
@@ -0,0 +1,97 @@
|
||||
# APP-4154 Phase 2 — Other ObjC objects checklist
|
||||
|
||||
Every ownership-producing message send in a non-ARC `.m` file, plus every Rust-side `msg_send![class!(X), alloc]` retained allocation. Each batch agent fills in the trailing columns, applies the fix, and ticks the row. See `TECH.md` for the decision rule.
|
||||
|
||||
## Reproducible greps
|
||||
|
||||
```
|
||||
rg -n '\balloc\]|\bnew\]|\bcopy\]|\bmutableCopy\]' -g '*.m' -g '*.mm'
|
||||
rg -n 'msg_send!\[class!\([A-Za-z_]+\), alloc\]' -g '*.rs'
|
||||
```
|
||||
|
||||
Ignore (not leaks):
|
||||
- `[super dealloc]` matches (`crates/warpui/src/platform/mac/objc/menus.m:25`).
|
||||
- `app/DockTilePlugin/WarpDockTilePlugin.m` — compiled with `-fobjc-arc`.
|
||||
- Definitions of trait-style alloc helpers (e.g. `unsafe fn alloc(...) -> id { msg_send![class!(NSAlert), alloc] }` in `crates/warpui/src/platform/mac/app.rs:46` when it's a helper; audit the callers instead).
|
||||
|
||||
## Row format
|
||||
|
||||
```
|
||||
- [ ] path:line — function — disposition (released|autoreleased|leaked|stored|?) — thread-origin — hot/cold — strategy — action
|
||||
```
|
||||
|
||||
## Batch 2.A — `sentry-objc`
|
||||
|
||||
Files: `app/src/platform/mac/objc/crash_reporting.m`.
|
||||
|
||||
- [x] app/src/platform/mac/objc/crash_reporting.m:21 — `setUser` — leaked (`[[SentryUser alloc] init]` never released) — appkit-main (called via `set_optional_user_information` on `AppContext`) — cold (auth login/logout) — explicit-release — added `[user release]` after `[SentrySDK setUser:user]`
|
||||
- [x] app/src/platform/mac/objc/crash_reporting.m:76 — `recordBreadcrumb` — released (post-#560 `[crumb release]` on line 82) — rust-thread (`forward_breadcrumb`, any Rust thread; caller wraps in `NSAutoreleasePool`) — hot — explicit-release — no-op (already correct)
|
||||
|
||||
## Batch 2.B — `app-objc-misc`
|
||||
|
||||
Files: `app/src/platform/mac/objc/{app_bundle.m, services.m}`.
|
||||
|
||||
`app_bundle.m` has no `alloc]`/`new]`/`copy]`/`mutableCopy]` hits — confirmed via re-grep, N/A (no rows to file).
|
||||
|
||||
NB: the `@autoreleasepool { ... }` around this function body drains autoreleased temporaries but does NOT balance `[[X alloc] init]`'s +1 retain. The rows below are retained-and-leaked until the enclosing scope exits; they need `autorelease-helper` (swap to `[NSMutableArray array]` etc.) or `explicit-release`, not `ambient`.
|
||||
|
||||
- [x] app/src/platform/mac/objc/services.m:30 — `forFilesFromPasteboard:performAction:` — retained (+1 from alloc/init, `@autoreleasepool` does not drain) and leaked prior to fix — appkit-main (Services dispatch) — cold — autorelease-helper — replaced with `[NSMutableArray array]`
|
||||
- [x] app/src/platform/mac/objc/services.m:35 — `forFilesFromPasteboard:performAction:` — retained (+1 from alloc/init) and leaked prior to fix — appkit-main — cold — autorelease-helper — replaced with `[NSMutableArray array]`
|
||||
- [x] app/src/platform/mac/objc/services.m:37 — `forFilesFromPasteboard:performAction:` — retained (+1 from alloc/init; no empty-init convenience ctor) and leaked prior to fix — appkit-main — cold — autorelease-helper — wrapped with `autorelease`
|
||||
- [x] app/src/platform/mac/objc/services.m:42 — `forFilesFromPasteboard:performAction:` — retained (+1 from alloc/init) and leaked prior to fix — appkit-main — cold — autorelease-helper — replaced with `[NSMutableArray array]`
|
||||
- [x] app/src/platform/mac/objc/services.m:58 — `warp_register_services_provider` — retained (bare `[WarpServicesProvider alloc]` without `init`) and leaked prior to fix; `setServicesProvider:` adds its own retain per Apple docs — appkit-main pre-event-loop (called from Rust `app_services::mac::init`) — cold (one-shot) — explicit-release — added `init`, paired with `[provider release]` after `setServicesProvider:`
|
||||
|
||||
## Batch 2.C — `warpui-windowing-objc`
|
||||
|
||||
Files: `crates/warpui/src/platform/mac/objc/{app.m, host_view.m, window.m, window_blur.m, fullscreen_queue.m, keycode.m}`. `window_blur.m` confirmed to have no `alloc]`/`new]`/`copy]`/`mutableCopy]` matches (CoreFoundation `CFBundleCreate`/`CFStringCreateWithCString` are already balanced by `CFRelease`). N/A.
|
||||
|
||||
- [x] crates/warpui/src/platform/mac/objc/app.m:65 — `registerGlobalHotkey` — leaked (`setObject:forKey:` retains, but alloc+init +1 was never balanced) — appkit-main — cold — autorelease-helper — added `autorelease` so `_hotKeys` holds the only reference
|
||||
- [x] crates/warpui/src/platform/mac/objc/app.m:194 — `-[WarpDelegate init]` — stored (module-level `_hotKeys` held for app lifetime; WarpDelegate is itself deliberately leaked singleton per `get_warp_app`) — appkit-main — cold — ambient — no-op, intentional singleton
|
||||
- [x] crates/warpui/src/platform/mac/objc/app.m:488 — `get_warp_app` — stored (comment on line 483 states the delegate is deliberately leaked; guarded by `dispatch_once`) — appkit-main — cold — ambient — no-op, intentional singleton
|
||||
- [x] crates/warpui/src/platform/mac/objc/app.m:501 — `make_delegated_menu` — autoreleased — appkit-main — cold — autorelease-helper — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/app.m:509 — `make_services_menu_item` — leaked (`NSApp.servicesMenu` setter retains; alloc+init +1 was never balanced) — appkit-main — cold — autorelease-helper — added `autorelease`
|
||||
- [x] crates/warpui/src/platform/mac/objc/app.m:512 — `make_services_menu_item` — leaked (returned from factory; caller stores `submenu` which retains) — appkit-main — cold — autorelease-helper — added `autorelease` so the factory matches the rest of the menu-factory conventions in this file
|
||||
- [x] crates/warpui/src/platform/mac/objc/app.m:524 — `make_warp_custom_menu_item` — autoreleased — appkit-main — cold — autorelease-helper — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/app.m:527 — `make_warp_custom_menu_item` — autoreleased — appkit-main — cold — autorelease-helper — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/host_view.m:281 — `-[WarpHostView initWithFrame:...]` — stored (`markedText` ivar, released in `dealloc`) — appkit-main — cold — explicit-release — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/host_view.m:282 — `-[WarpHostView initWithFrame:...]` — leaked (`textToInsert` ivar was not released in `dealloc`) — appkit-main — cold — explicit-release — added `[textToInsert release]` to `-dealloc`
|
||||
- [x] crates/warpui/src/platform/mac/objc/host_view.m:423 — `-insertText:replacementRange:` — released (explicit `[characters release]` at line 445) — appkit-event — hot — explicit-release — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/host_view.m:470 — `-setMarkedText:...` — stored (`markedText` ivar; previous value released at line 468, final release in `dealloc`) — appkit-event — hot — explicit-release — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/host_view.m:472 — `-setMarkedText:...` — stored (same pattern as :470) — appkit-event — hot — explicit-release — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:37 — `-enqueueFullscreenTransition` — stored (module-level `fullscreenManager` via `dispatch_once`, intentional singleton) — appkit-main — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:499 — `+[WarpWindow createWithContentRect:...]` — retained and returned per the `create` naming convention (caller owns); Rust `Window::open` stores the resulting `id` as `native_window` and AppKit releases it via `releasedWhenClosed = YES` — appkit-main — cold — ambient — no-op, documented ownership transfer
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:663 — `+[WarpPanel createWithContentRect:...]` — same as :499 (ownership transferred to Rust caller) — appkit-main — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:689 — `create_warp_nspanel` — released (manually balanced by `[pool release]` at line 714/719 post-edit) — appkit-main — cold — local-pool — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:693 — `create_warp_nspanel` — stored (module-level `windowOrderForTests` via `dispatch_once`, intentional singleton for integration tests) — appkit-main — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:703 — `create_warp_nspanel` — autoreleased — appkit-main — cold — autorelease-helper — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:708 — `create_warp_nspanel` — leaked (`NSWindow.delegate` is weak; the +1 retain count was never balanced so the delegate outlived every window open) — appkit-main — cold — stored — tied delegate lifetime to window via `objc_setAssociatedObject` + released caller's +1
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:721 — `create_warp_nswindow` — released (manually balanced by `[pool release]` at line 746/753 post-edit) — appkit-main — cold — local-pool — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:725 — `create_warp_nswindow` — stored (same as :693) — appkit-main — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:735 — `create_warp_nswindow` — autoreleased — appkit-main — cold — autorelease-helper — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/window.m:740 — `create_warp_nswindow` — leaked (same root cause as :708) — appkit-main — cold — stored — fixed alongside :708 with `objc_setAssociatedObject`
|
||||
- [x] crates/warpui/src/platform/mac/objc/fullscreen_queue.m:17 — `-[FullscreenWindowManager init]` — stored (ivar on `fullscreenManager` singleton which is itself intentionally leaked for app lifetime) — appkit-main — cold — ambient — no-op
|
||||
- [x] crates/warpui/src/platform/mac/objc/keycode.m:163 — `charToKeyCodes` — stored (module-level `keycodeDict` cache, intentional singleton built lazily on first call) — rust-thread? — cold — ambient — no-op, singleton cache
|
||||
- [x] crates/warpui/src/platform/mac/objc/keycode.m:193 — `charToKeyCodes` — leaked (`setObject:forKey:` retains; alloc+init +1 was never balanced) — rust-thread? — cold — autorelease-helper — added `autorelease`
|
||||
- [x] crates/warpui/src/platform/mac/objc/keycode.m:201 — `charToKeyCodes` — leaked (same pattern as :193) — rust-thread? — cold — autorelease-helper — added `autorelease`
|
||||
|
||||
## Batch 2.D — `warpui-chrome-objc`
|
||||
|
||||
Files: `crates/warpui/src/platform/mac/objc/{alert.m, menus.m, notifications/notifications.m, reachability.m, hotkey.m}`. `alert.m`, `menus.m` (beyond `[super dealloc]`), and `hotkey.m` currently have no `alloc]` matches; agent confirms.
|
||||
|
||||
Confirmed via `rg -n 'alloc\]|\bnew\]|\bcopy\]|\bmutableCopy\]'` on each file in the working tree: `alert.m`, `hotkey.m` → no matches (N/A). `menus.m` → only `[super dealloc]` at line 25 (N/A).
|
||||
|
||||
- [x] crates/warpui/src/platform/mac/objc/notifications/notifications.m:55 — `sendNotificationWithErrorHandler` completion block — leaked (alloc/init `UNMutableNotificationContent` never released) — gcd-block (UNUserNotificationCenter completion handler) — cold (per user-triggered notification) — autorelease-helper — added inline `autorelease` on the alloc/init expression
|
||||
- [x] crates/warpui/src/platform/mac/objc/reachability.m:93 — `+reachabilityWithHostname:` — autoreleased — appkit-main (via `warp_app_will_finish_launching` → `setReachabilityListener`) — cold (once per app lifetime) — autorelease-helper — added `autorelease` so the factory matches Cocoa naming conventions; caller in `app.m:394` now `retain`s and `-[WarpDelegate dealloc]` calls `stopNotifier` (to break the `reachabilityObject = self` retain cycle set up by `-startNotifier`) followed by `release`.
|
||||
- [x] crates/warpui/src/platform/mac/objc/reachability.m:105 — `+reachabilityWithAddress:` — autoreleased — n/a (dead path today; only reached via `reachabilityForInternetConnection` / `reachabilityForLocalWiFi` / `reachabilityWithURL`, none of which are called in the current tree) — cold — autorelease-helper — added `autorelease` alongside :93 for consistency; no caller updates required because the path is unused today.
|
||||
|
||||
## Batch 2.E — `rust-msg-send-alloc`
|
||||
|
||||
Rust-side `msg_send![class!(X), alloc]` sites that retain without autoreleasing. These require explicit balance or switching to an autoreleased helper.
|
||||
|
||||
`crates/warpui_extras/src/user_preferences/user_defaults.rs:39` is adjacent to NSString lines audited by batch 1.D; to avoid merge conflicts it's owned by batch 1.D in `nsstring_checklist.md`, not this file.
|
||||
|
||||
- [x] crates/warpui/src/platform/mac/app.rs:46 — `NSAlert::alloc` trait impl — autoreleased (by caller) — appkit-main — cold — autorelease-helper — no-op: caller at :80 `create_native_platform_modal` wraps the chain in `NSAlert::autorelease(NSAlert::init(NSAlert::alloc(nil)))`, and the caller's callers (`show_native_platform_modal` in `delegate.rs:375`) run on the AppKit main thread where an ambient pool exists
|
||||
- [x] crates/warpui/src/platform/mac/app.rs:187 — `App::run` — retained (chained into `initWithBytes_length_`) — appkit-main — cold — ambient — no-op: `App::run` is a one-shot called from `main`; `NSAutoreleasePool::new(nil)` at :178 spans the entire NSApp run loop and drains at :210 on app shutdown. The icon data is consumed synchronously by the `NSImage` init at :192, and the resulting image is retained by `NSApp` via `setApplicationIconImage:` at :206. Any residual retain is reclaimed at process exit.
|
||||
- [x] crates/warpui/src/platform/mac/app.rs:192 — `App::run` — retained (chained into `initWithData_`) — appkit-main — cold — ambient — no-op: same scope as :187. The produced image is handed off to `NSApp` via `setApplicationIconImage:` (NSApp retains) at :206, and the outer pool at :178 covers the call; one-shot at startup, reclaimed on process exit.
|
||||
- [x] crates/warpui/src/platform/mac/clipboard.rs:68 — `<impl Clipboard for Clipboard>::write` — leaked (chained `alloc].initWithBytes_length_` never balanced; pasteboard retains its own copy) — appkit-main — cold (user-initiated copy action, not a tight loop) — explicit-release — added `msg_send![data, release]` after `setData:forType:` to balance the `+1` from `[NSData alloc]`; pasteboard retain keeps the data alive for consumers
|
||||
- [x] app/src/appearance.rs:234 — `AppearanceManager::set_app_icon` — leaked (chained `alloc].initWithContentsOfFile:` never balanced) — mixed (appkit-main from settings UI + `ctx.spawn` continuation after autoupdate + app init in `lib.rs:1204`) — cold (fires on icon change, app start, after updates) — explicit-release — added `msg_send![image, release]` after the final `noteFileSystemChanged:`; `setApplicationIconImage:` and `setIcon:forFile:options:` both retain the image, and `initWithContentsOfFile:` releases the `alloc` on failure, so the nil-check early return needs no additional release
|
||||
Reference in New Issue
Block a user