Subject: bug#79023: 30.1.90; Suspicion of memory leak on internal_redisplay (MacOS)

From: Przemysław Alexander Kamiński<[email-masked]>
Date: Mon, 04 Aug 2025 20:10:03 +0200

I'm attaching the patch that looks promising (from local instrumentation):

Changes:
- render_block mechanism when doing "inner" run loops
- don't CGContextFlush manually (documentation recommends to leave
  window renderer to handle that)
- some small fixes in releases (toolbar)
- niling Emacs' frame struct on window dealloc - it should be garbagged
  but it seems something is still attached to the ref and cannot be GCed

Known issues:
- Can't get resizing as it's chicken-and-egg problem - drag redraws and
redraw reads on socket (which blocks redraws)

Other remarks:
- Increasing number of buffers in "double buffering" prevents leaks and
  memory allocation, but I didn't include it in the patch (e.g. not
  array of 2, but array of 4 or 6)

Best,
Przemysław Alexander Kamiński

diff --git a/ns.patch b/ns.patch
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/nsterm.m b/src/nsterm.m
index 30a35bdd5b..544ec409a3 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -78,6 +78,25 @@
 static EmacsMenu *mainMenu;
 #endif
 
+static volatile int render_blocked = 0;
+INLINE void
+block_render (void)
+{
+  render_blocked++;
+}
+INLINE bool
+render_blocked_p (void)
+{
+  return render_blocked > 0;
+}
+INLINE void
+unblock_render (void)
+{
+  render_blocked--;
+}
+
+
+
 /* The last known monitor attributes list.  */
 static Lisp_Object last_known_monitors;
 
@@ -1637,6 +1656,7 @@
 
   [[view window] close];
   [view removeFromSuperview];
+  [view release];
 
   xfree (f->output_data.ns);
   f->output_data.ns = NULL;
@@ -4956,8 +4976,9 @@
              to ourself, otherwise [NXApp run] will never exit.  */
           send_appdefined = YES;
           ns_send_appdefined (-1);
-
+          block_render();
           [NSApp run];
+          unblock_render();
         }
 
       nevents = n_emacs_events_pending;
@@ -5099,7 +5120,9 @@
   block_input ();
   ns_init_events (&event);
 
+  block_render();
   [NSApp run];
+  unblock_render();
 
   ns_finish_events ();
   if (nr > 0 && readfds)
@@ -6839,6 +6862,13 @@
 
   if (fs_state == FULLSCREEN_BOTH)
     [nonfs_window release];
+
+  // Don't reuse inner frame as on NS cleanup isn't complete
+  emacsframe = nil;
+
+  EmacsLayer *layer = (EmacsLayer *)[self layer];
+  [layer release];
+  [[self menu] release];
   [super dealloc];
 }
 
@@ -9033,7 +9063,6 @@
 
 #if defined (NS_IMPL_COCOA) && MAC_OS_X_VERSION_MIN_REQUIRED >= 101400
   CGContextRef context = [(EmacsLayer *)[self layer] getContext];
-  CGContextFlush (context);
 
   double scale = [[self window] backingScaleFactor];
   int bpp = CGBitmapContextGetBitsPerPixel (context) / 8;
@@ -9084,7 +9113,7 @@
    appears to be safe to call redisplay here.  */
 - (void)layoutSublayersOfLayer:(CALayer *)layer
 {
-  if (!redisplaying_p && FRAME_GARBAGED_P (emacsframe))
+  if (!redisplaying_p && FRAME_GARBAGED_P (emacsframe) && !render_blocked_p())
     {
       /* If there is IO going on when redisplay is run here Emacs
          crashes.  I think it's because this code will always be run
@@ -9588,8 +9617,9 @@
   NSTRACE ("[EmacsWindow dealloc]");
 
   /* We need to release the toolbar ourselves.  */
+  [[self toolbar] release];
   [self setToolbar: nil];
-  [[self toolbar] release];
+
 
   /* Also the last button press event .  */
   if (last_drag_event)
@@ -10994,7 +11024,6 @@
   if (!context)
     return;
 
-  CGContextFlush (context);
   CGContextRelease (context);
   context = NULL;