StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! Memory-behavior repros for APP-4154 batch 1.C (strato_ui-platform-nsstring). |
| 2 | //! |
| 3 | //! Covers two kinds of fix in `menus.rs`: |
| 4 | //! |
| 5 | //! 1. Retain → autorelease conversion (line 298, `make_menu_item` standard |
| 6 | //! action): the key-equivalent NSString used to be `NSString::alloc(nil) |
| 7 | //! .init_str(...)`, which returns a +1 retained reference. The PR uses |
| 8 | //! `make_nsstring`, which autoreleases. Covered by |
| 9 | //! [`make_menu_item_standard_action_memory_behavior`]. |
| 10 | //! |
| 11 | //! 2. Local `NSAutoreleasePool` wrapper around `apply_changes` body. The |
| 12 | //! NSString temporaries produced by `make_nsstring(name)` and inside |
| 13 | //! `resolve_key_equivalent` used to go into whatever ambient pool AppKit |
| 14 | //! had set up (or leak if called from a Rust thread with no active pool). |
| 15 | //! The PR drains them per call. Covered by |
| 16 | //! [`apply_changes_local_pool_memory_behavior`], which deliberately runs |
| 17 | //! WITHOUT any outer `NSAutoreleasePool` so the local-pool drain is the |
| 18 | //! only thing that can release the temporaries. |
| 19 | use cocoa::appkit::NSMenuItem; |
| 20 | use cocoa::base::nil; |
| 21 | use cocoa::foundation::NSAutoreleasePool; |
| 22 | use objc::runtime::Object; |
| 23 | use objc::{msg_send, sel, sel_impl}; |
| 24 | use strato_ui_core::actions::StandardAction; |
| 25 | use strato_ui_core::keymap::Keystroke; |
| 26 | use strato_ui_core::platform::menu::{MenuItem, MenuItemPropertyChanges}; |
| 27 | |
| 28 | use super::{apply_changes, make_menu_item}; |
| 29 | |
| 30 | /// How many outer pool cycles for the retain → autorelease test. |
| 31 | const MENU_ITEM_OUTER: usize = 40; |
| 32 | /// Inner iterations per outer cycle. Each one allocates one NSMenuItem plus |
| 33 | /// (on master) one retained NSString for the key equivalent. |
| 34 | const MENU_ITEM_INNER: usize = 10_000; |
| 35 | |
| 36 | /// Driver for the local-pool wrapper test. `apply_changes` creates a handful |
| 37 | /// of NSString temporaries per call; without an outer pool, master accumulates |
| 38 | /// them all, while the branch drains them per iteration. |
| 39 | const APPLY_CHANGES_ITERS: usize = 200_000; |
| 40 | |
| 41 | /// Reproduces the per-call NSString leak fixed by switching the key-equivalent |
| 42 | /// argument to `make_nsstring` on line 298. Each outer cycle gets its own |
| 43 | /// autorelease pool; the branch reclaims everything on drain, master keeps the |
| 44 | /// retained key-equivalent strings alive. |
| 45 | #[test] |
| 46 | fn make_menu_item_standard_action_memory_behavior() { |
| 47 | unsafe { |
| 48 | for _ in 0..MENU_ITEM_OUTER { |
| 49 | let pool = NSAutoreleasePool::new(nil); |
| 50 | for _ in 0..MENU_ITEM_INNER { |
| 51 | // `Quit` has a non-empty key equivalent ("q"); `Close Window` |
| 52 | // has an empty one. Mix the two so we cover both branches. |
| 53 | let _ = make_menu_item(MenuItem::Standard(StandardAction::Quit)); |
| 54 | let _ = make_menu_item(MenuItem::Standard(StandardAction::Close)); |
| 55 | } |
| 56 | pool.drain(); |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | /// Reproduces the accumulation that the `apply_changes` local pool prevents. |
| 62 | /// Note the deliberate absence of an outer `NSAutoreleasePool` — this is what |
| 63 | /// makes the local-pool wrapper observable. |
| 64 | #[test] |
| 65 | fn apply_changes_local_pool_memory_behavior() { |
| 66 | unsafe { |
| 67 | // Hold a single menu item for the entire loop so that the only growth |
| 68 | // we measure is the NSString temporaries inside `apply_changes`, not |
| 69 | // the menu item objects themselves. |
| 70 | let outer_pool = NSAutoreleasePool::new(nil); |
| 71 | let item: *mut Object = msg_send![NSMenuItem::alloc(nil), init]; |
| 72 | // Retain so we can freely drain the outer pool after constructing it. |
| 73 | let _: *mut Object = msg_send![item, retain]; |
| 74 | outer_pool.drain(); |
| 75 | |
| 76 | for _ in 0..APPLY_CHANGES_ITERS { |
| 77 | let changes = MenuItemPropertyChanges { |
| 78 | name: Some("Warp Menu Item".to_string()), |
| 79 | keystroke: Some(Some(Keystroke { |
| 80 | cmd: true, |
| 81 | key: "k".to_string(), |
| 82 | ..Default::default() |
| 83 | })), |
| 84 | disabled: Some(false), |
| 85 | checked: Some(false), |
| 86 | submenu: None, |
| 87 | }; |
| 88 | apply_changes(changes, item); |
| 89 | } |
| 90 | |
| 91 | // Balance the manual retain above. |
| 92 | let _: () = msg_send![item, release]; |
| 93 | } |
| 94 | } |
| 95 |