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

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

I'm attaching 6 patches (for easy review/scrutiny/integration).
Those patches that I marked as tested was used by me for >1 week.

* render_block_small.patch (significant impact, tested):

This is similar patch to previous one, but it's minimal. It's crucial
to avoid "blink" redraws.
Still breaks smooth resizing, but frame-resize-pixelwise set to true
makes it unnoticable.

* cache_all_font_results.patch (significant impact, requires testing):

This one prevents huge leak. I was trying to debug emacs-mac version (which
has very deep traces for allocations, more 256 deep, which is limit for
Instrumentations).

That led to full malloc logging and discovery that macfont listings were
allocating plenty of font-related data. Some of it it never cleared.

I didn't test it and worry that caching "null" result might introduce
some bugs, but it was hundreds of megabytes of allocations on -Q instance.

* dont_flush_cg.patch (medium impact, tested):

Based on documentation of CGContextFlush it should seldom be used, as
it's much better to give OS possibility to decide when to flush. It
prevents non-needed renders (and allocations etc.)

* misc_releases.patch (small-mid impact, tested):

This one is based on Rudolf Adamkovič patch with some minor extra
releases and adjustments
(like releasing an object before setting it to nil ;))

* ns_native_api_dict.patch (small-mid impact, needs testing):

A small leak, but modified function had ~600_000 allocations after very
short test runs. Seems like it wasted a lot of power, so I decided it
should be checking dictionary hydrated at start and queried later
instead of checking during runtime.

I believe that high NSString allocation amount was indirectly caused by
using autorelease pools.

* nullify_frame.patch (small impact, tested):

releases reference to a frame, as I've found traces in which resources
were left behind, small leak.

---

Fingers crossed that patches go through ;-)

diff --git a/src/nsterm.m b/src/nsterm.m
index b006b4d5dd..1510f74b8b 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -6839,6 +6839,9 @@
 
   if (fs_state == FULLSCREEN_BOTH)
     [nonfs_window release];
+  /* Don't reuse inner - cleanup is robust enough */
+  emacsframe = nil;
+
   [super dealloc];
 }
diff --git a/src/nsimage.m b/src/nsimage.m
index 8f1653d085..de2ae8aecf 100644
--- a/src/nsimage.m
+++ b/src/nsimage.m
@@ -56,9 +56,51 @@
 
    ========================================================================== */
 
+
+static CFDictionaryRef image_api_dict;
+extern void
+ns_image_init (void)
+{
+  if(image_api_dict) return;
+  NSArray *imageTypes = [NSImage imageTypes];
+Lisp_Object keys[]
+  = { Qnative_image,
+      Qjpeg,
+      Qpng,
+      Qgif,
+      Qtiff,
+#ifndef HAVE_RSVG
+      Qsvg,
+#endif
+#ifndef HAVE_WEBP
+      Qwebp,
+#endif
+      Qheic
+};
+bool values[]
+  = { YES,
+      [imageTypes containsObject:@"public.jpeg"],
+      [imageTypes containsObject:@"public.png"],
+      [imageTypes containsObject:@"com.compuserve.gif"],
+      [imageTypes containsObject:@"public.tiff"],
+#ifndef HAVE_RSVG
+      [imageTypes containsObject:@"public.svg-image"],
+#endif
+#ifndef HAVE_WEBP
+  [imageTypes containsObject:  @"org.webmproject.webp"],
+#endif
+  [imageTypes containsObject:  @"public.heic"],
+};
+ image_api_dict = CFDictionaryCreate(NULL, (void *) keys, ( void *) values, 50, NULL,NULL);
+
+}
+
 bool
 ns_can_use_native_image_api (Lisp_Object type)
 {
+#ifdef NS_IMPL_COCOA
+  return (bool) (CFDictionaryGetValue(image_api_dict, type) || NO);
+#endif
   NSString *imageType = @"unknown";
   NSArray *types;
 
@@ -67,32 +109,6 @@
   if (EQ (type, Qnative_image))
     return YES;
 
-#ifdef NS_IMPL_COCOA
-  /* Work out the UTI of the image type.  */
-  if (EQ (type, Qjpeg))
-    imageType = @"public.jpeg";
-  else if (EQ (type, Qpng))
-    imageType = @"public.png";
-  else if (EQ (type, Qgif))
-    imageType = @"com.compuserve.gif";
-  else if (EQ (type, Qtiff))
-    imageType = @"public.tiff";
-#ifndef HAVE_RSVG
-  else if (EQ (type, Qsvg))
-    imageType = @"public.svg-image";
-#endif
-#ifndef HAVE_WEBP
-  else if (EQ (type, Qwebp))
-    imageType = @"org.webmproject.webp";
-#endif
-  else if (EQ (type, Qheic))
-    imageType = @"public.heic";
-
-  /* NSImage also supports a host of other types such as PDF and BMP,
-     but we don't yet support these in image.c.  */
-
-  types = [NSImage imageTypes];
-#else
   /* Work out the image type.  */
   if (EQ (type, Qjpeg))
     imageType = @"jpeg";
@@ -104,7 +120,6 @@
     imageType = @"tiff";
 
   types = [NSImage imageFileTypes];
-#endif
 
   /* Check if the type is supported on this system.  */
   if ([types indexOfObject:imageType] != NSNotFound)
diff --git a/src/nsterm.h b/src/nsterm.h
index d9d16ffabd..342a38a126 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -1200,6 +1200,7 @@
 
 /* From nsimage.m, needed in image.c.  */
 struct image;
+extern void ns_image_init(void);
 extern bool ns_can_use_native_image_api (Lisp_Object type);
 extern void *ns_image_from_XBM (char *bits, int width, int height,
                                 unsigned long fg, unsigned long bg);
diff --git a/src/nsterm.m b/src/nsterm.m
index b006b4d5dd..c539832756 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -5787,6 +5787,7 @@
       pthread_mutex_init (&select_mutex, NULL);
     }
 
+  ns_image_init();
   ns_pending_files = [[NSMutableArray alloc] init];
   ns_pending_service_names = [[NSMutableArray alloc] init];
   ns_pending_service_args = [[NSMutableArray alloc] init];
diff --git a/src/nsterm.m b/src/nsterm.m
index b006b4d5dd..cd9ee9ebfe 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;

@@ -4956,8 +4975,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 +5119,9 @@
   block_input ();
   ns_init_events (&event);

+  block_render();
   [NSApp run];
+  unblock_render();

   ns_finish_events ();
   if (nr > 0 && readfds)
@@ -9084,7 +9106,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
diff --git a/src/macfont.m b/src/macfont.m
index 2a0b9aa255..cef4cf80ff 100644
--- a/src/macfont.m
+++ b/src/macfont.m
@@ -1152,9 +1152,10 @@
 	    CTFontDescriptorCopyAttribute (desc, kCTFontFamilyNameAttribute);
 	  CFRelease (desc);
 	}
-      macfont_set_family_cache (symbol, result);
+
       CFRelease (pat_desc);
     }
-
+  /* It's possible that we don't have have kCTFontFamilyNameAttribute for symbol, but still cache result */
+  macfont_set_family_cache (symbol, result);
   return result;
 }
diff --git a/src/nsterm.m b/src/nsterm.m
index b006b4d5dd..057d876a35 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -9033,7 +9033,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;
@@ -10994,7 +10993,6 @@
   if (!context)
     return;

-  CGContextFlush (context);
   CGContextRelease (context);
   context = NULL;
diff --git a/src/nsterm.m b/src/nsterm.m
index b006b4d5dd..f1ea98b107 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -1637,6 +1637,7 @@

   [[view window] close];
   [view removeFromSuperview];
+  [view release];

   xfree (f->output_data.ns);
   f->output_data.ns = NULL;
@@ -6839,6 +6840,11 @@

   if (fs_state == FULLSCREEN_BOTH)
     [nonfs_window release];
+
+  /* Release layer and menu */
+  EmacsLayer *layer = (EmacsLayer *)[self layer];
+  [layer release];
+  [[self menu] release];
   [super dealloc];
 }

@@ -9588,8 +9594,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)

Best,
Przemysław Alexander Kamiński