GNU bug report logs - #73563
[Ben Simms] Performance bottleneck in ns_draw_fringe_bitmap

Previous Next

Package: emacs;

Reported by: Stefan Kangas <stefankangas <at> gmail.com>

Date: Mon, 30 Sep 2024 08:03:02 UTC

Severity: normal

To reply to this bug, email your comments to 73563 AT debbugs.gnu.org.

Toggle the display of automated, internal messages from the tracker.

View this report as an mbox folder, status mbox, maintainer mbox


Report forwarded to ben <at> bensimms.moe, bug-gnu-emacs <at> gnu.org:
bug#73563; Package emacs. (Mon, 30 Sep 2024 08:03:02 GMT) Full text and rfc822 format available.

Acknowledgement sent to Stefan Kangas <stefankangas <at> gmail.com>:
New bug report received and forwarded. Copy sent to ben <at> bensimms.moe, bug-gnu-emacs <at> gnu.org. (Mon, 30 Sep 2024 08:03:02 GMT) Full text and rfc822 format available.

Message #5 received at submit <at> debbugs.gnu.org (full text, mbox):

From: Stefan Kangas <stefankangas <at> gmail.com>
To: bug-gnu-emacs <at> gnu.org
Subject: [Ben Simms] Performance bottleneck in ns_draw_fringe_bitmap
Date: Mon, 30 Sep 2024 07:21:03 +0000
[Message part 1 (text/plain, inline)]
I'm forwarding this to the bug tracker so that we don't lose track of
it.

Original message:
https://lists.gnu.org/r/emacs-devel/2024-06/msg00900.html

-------------------- Start of forwarded message --------------------
From: Ben Simms <ben <at> bensimms.moe>
Date: Wed, 26 Jun 2024 13:56:43 +0200
Subject: Performance bottleneck in ns_draw_fringe_bitmap
To: emacs-devel <at> gnu.org
[Message part 2 (text/plain, attachment)]
[Message part 3 (text/html, attachment)]

Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#73563; Package emacs. (Mon, 30 Sep 2024 12:28:02 GMT) Full text and rfc822 format available.

Message #8 received at 73563 <at> debbugs.gnu.org (full text, mbox):

From: Eli Zaretskii <eliz <at> gnu.org>
To: Stefan Kangas <stefankangas <at> gmail.com>
Cc: ben <at> bensimms.moe, 73563 <at> debbugs.gnu.org
Subject: Re: bug#73563: [Ben Simms] Performance bottleneck in
 ns_draw_fringe_bitmap
Date: Mon, 30 Sep 2024 15:26:31 +0300
> Cc: Ben Simms <ben <at> bensimms.moe>
> From: Stefan Kangas <stefankangas <at> gmail.com>
> Date: Mon, 30 Sep 2024 07:21:03 +0000
> 
> Hi all, I recently started using Emacs (ns) HEAD on an ARM macos sonoma
> system.
> 
> I've noticed that ns_draw_fringe_bitmap is a fairly large performance sink
> when using pixel scrolling (to the point of 99% of cpu time being inside this
> function, with Emacs drawing at approx 5Hz). The slowness here isn't as
> obvious when not pixel scrolling, presumably because Emacs never tries to
> redraw at 60+Hz otherwise.
> 
> I have performed some profiling and discovered that in my observed worse case
> situation, of the 99% of cpu time spent in ns_draw_fringe_bitmap, approx 50%
> is spent in [NSBezierPath copy], and approx 30% in [NSBezierPath fill].

According to the posted profile, ns_draw_fringe_bitmap takes much less
than 99% of CPU time, somewhere around 10%.  What am I missing?




Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#73563; Package emacs. (Tue, 13 May 2025 11:40:01 GMT) Full text and rfc822 format available.

Message #11 received at submit <at> debbugs.gnu.org (full text, mbox):

From: Jordan Ellis Coppard <jc+o.emacs <at> wz.ht>
To: Emacs Bugs <bug-gnu-emacs <at> gnu.org>
Cc: Ben Simms <ben <at> bensimms.moe>
Subject: bug#73563: [Ben Simms] Performance bottleneck in ns_draw_fringe_bitmap
Date: Tue, 13 May 2025 20:37:55 +0900
Howdy,


I've been using the following patch (bottom of this email) until my most 
recent rebuild of Emacs from master. Since the recent commit fixing 
stipple drawing the patch can be reduced to just: 
https://github.com/emacs-mirror/emacs/commit/7f2efe6503fdce2a4e552c14802644a05b581bc7 
(link to a diff authored by Ben Simms).

I've now noticed again extreme slowdown when fringe bitmaps are used. It 
appears to be proportional to the number of bits set in the bitmap. An 
'empty' bitmap (all zeroes) doesn't yield any noticeable slowdown, a 
bitmap with 1 bit set yields a little, with 2 its noticeable and with 
all 8 bits set Emacs takes up to 1 second to respond to any input at all.

This has been the case since at least August of last year. I can get a 
minimal reproduction but it has happened on completely different 
configurations with the only common denominator being the NS build (on 
ARM-based macOS), and the use of fringe bitmaps.

Below is trace with update_frame as the root context. Captured with 
Instruments.app (which uses dtrace and more under the hood IIRC).

ns_draw_fringe_bitmap takes up 94% of the time for the short 
reproduction this trace records. The deepest the trace goes is to 
CG::Path::recalculate_bounding_box() which eats 80% of time. So instead 
of a cached value the bounding box is being recalculated every time 
which appears very expensive, but beyond that I have close to zero 
experience with macOS' graphics stack.

I've just recompiled Emacs against the same commit of my recent master 
build (648453c04d9b91d96452b930c0c948b0b39b5dc0) except now with the 
patch applied (since the stipple changes are merged it's just the 
smallest subset: 
https://github.com/emacs-mirror/emacs/commit/7f2efe6503fdce2a4e552c14802644a05b581bc7) 
and once again fringe performance is back to being buttery smooth.


(trace also here in-case formatting gets borked:
trace:

3.07 s  100.0%	0 s	               update_frame
3.06 s  100.0%	0 s	                update_window_tree
3.06 s  100.0%	1.00 ms	                 update_window
2.89 s  94.2%	0 s	                  gui_update_window_end
2.88 s  94.1%	0 s	                   draw_window_fringes
2.88 s  93.9%	0 s	                    draw_fringe_bitmap
2.88 s  93.9%	0 s	                     draw_fringe_bitmap_1
2.88 s  93.9%	0 s	                      ns_draw_fringe_bitmap
2.72 s  88.7%	0 s	                       -[NSBezierPath copyWithZone:]
2.72 s  88.7%	0 s	                        -[NSBezierPath _appendToPath:]
2.72 s  88.7%	0 s	                         -[NSBezierPath 
_enumeratePathElementsUsingBlock:]
2.72 s  88.7%	0 s	                          CGPathApplyWithBlock2
2.72 s  88.7%	5.00 ms	                           CG::Path::apply(void 
(CGPathElementType, CGPoint const*, bool*) block_pointer) const
2.71 s  88.5%	30.00 ms	 
__CGPathApplyWithBlock2_block_invoke
2.68 s  87.4%	11.00 ms	                             __49-[NSBezierPath 
_enumeratePathElementsUsingBlock:]_block_invoke
2.48 s  80.8%	1.00 ms	 
-[NSBezierPath(NSBezierPathDevicePrimitives) _deviceMoveToPoint:]
2.47 s  80.6%	2.00 ms	                               CGPathMoveToPoint
2.46 s  80.3%	2.46 s	 
CG::Path::recalculate_bounding_box()
5.00 ms   0.2%	4.00 ms	 
CG::Path::move_to_point(CGPoint const&, CGAffineTransform const*)
1.00 ms   0.0%	1.00 ms	 
CG::Path::convert_to_huge()
2.00 ms   0.1%	2.00 ms	                                (anonymous 
namespace)::transform_is_valid(CGAffineTransform const*)
1.00 ms   0.0%	1.00 ms	 
CG::Path::convert_to_huge()
1.00 ms   0.0%	1.00 ms	 
CGFloatValidateWithLog
1.00 ms   0.0%	1.00 ms	 
DYLD-STUB$$CGPathMoveToPoint
1.00 ms   0.0%	1.00 ms	                               -[NSBezierPath 
_cgPath]
1.00 ms   0.0%	1.00 ms	                               objc_msgSend
1.00 ms   0.0%	1.00 ms	                               (anonymous 
namespace)::transform_is_valid(CGAffineTransform const*)
67.00 ms   2.2%	7.00 ms	 
-[NSBezierPath(NSBezierPathDevicePrimitives) _deviceLineToPoint:]
40.00 ms   1.3%	5.00 ms	 
-[NSBezierPath(NSBezierPathDevicePrimitives) _deviceClosePath]
39.00 ms   1.3%	2.00 ms	                              -[NSBezierPath 
lineToPoint:]
14.00 ms   0.5%	14.00 ms	                              objc_msgSend
6.00 ms   0.2%	6.00 ms	                              -[NSBezierPath _cgPath]
5.00 ms   0.2%	5.00 ms	                              CGPathAddLineToPoint
4.00 ms   0.1%	4.00 ms	                              __30-[NSBezierPath 
_appendToPath:]_block_invoke
4.00 ms   0.1%	4.00 ms	 
objc_msgSend$lineToPoint:
2.00 ms   0.1%	2.00 ms	                              CGPathIsEmpty
2.00 ms   0.1%	2.00 ms	 
objc_msgSend$moveToPoint:
2.00 ms   0.1%	2.00 ms	 
CG::Path::close_subpath()
1.00 ms   0.0%	1.00 ms	 
objc_msgSend$_deviceLineToPoint:
1.00 ms   0.0%	1.00 ms	                              CGPathGetCurrentPoint
1.00 ms   0.0%	1.00 ms	                              -[NSBezierPath 
moveToPoint:]
1.00 ms   0.0%	1.00 ms	 
objc_msgSend$_deviceClosePath
1.00 ms   0.0%	1.00 ms	                              objc_msgSend$closePath
1.00 ms   0.0%	1.00 ms	 
objc_msgSend$_deviceMoveToPoint:
3.00 ms   0.1%	3.00 ms	                             -[NSBezierPath 
lineToPoint:]
1.00 ms   0.0%	1.00 ms	 
-[NSBezierPath(NSBezierPathDevicePrimitives) _deviceLineToPoint:]
2.00 ms   0.1%	2.00 ms	                            __49-[NSBezierPath 
_enumeratePathElementsUsingBlock:]_block_invoke
1.00 ms   0.0%	1.00 ms	                        objc_msgSend$setFlatness:
104.00 ms   3.4%	0 s	                       -[NSBezierPath fill]
42.00 ms   1.4%	0 s	                       -[NSBezierPath 
transformUsingAffineTransform:]
7.00 ms   0.2%	0 s	                       NSRectFill
2.00 ms   0.1%	0 s	                       NSColorSetWithFillAndStroke
1.00 ms   0.0%	1.00 ms	                       CGContextClipToRect
1.00 ms   0.0%	0 s	                       -[NSBezierPath dealloc]
1.00 ms   0.0%	1.00 ms	                       CGGStateSetCompositeOperation
1.00 ms   0.0%	1.00 ms	                      lookup_named_face
5.00 ms   0.2%	4.00 ms	                    set_buffer_internal_2
1.00 ms   0.0%	0 s	                   unblock_input
1.00 ms   0.0%	0 s	                   ns_draw_window_cursor
121.00 ms   3.9%	0 s	                  ns_scroll_run
51.00 ms   1.7%	1.00 ms	                  update_window_line
4.00 ms   0.1%	4.00 ms	                  row_equal_p
1.00 ms   0.0%	1.00 ms	                  xwidget_end_redisplay
1.00 ms   0.0%	0 s	                ns_update_end



Patch:

--- src/nsimage.m.orig
+++ src/nsimage.m
@@ -28,6 +28,7 @@ Updated by Christian Limpach (chris <at> nice.ch)
 /* This should be the first include, as it may set up #defines affecting
    interpretation of even the system includes.  */
 #include <config.h>
+#include <CoreGraphics/CoreGraphics.h>

 #include "lisp.h"
 #include "dispextern.h"
@@ -510,10 +511,20 @@ - (void) setAlphaAtX: (int) x Y: (int) y to: 
(unsigned char) a
 }

 /* Returns a pattern color, which is cached here.  */
-- (NSColor *)stippleMask
+- (CGImageRef)stippleMask
 {
-  if (stippleMask == nil)
-      stippleMask = [[NSColor colorWithPatternImage: self] retain];
+  if (stippleMask == nil) {
+    CGDataProviderRef provider = CGDataProviderCreateWithData (NULL, 
[bmRep bitmapData],
+                                                             [self 
sizeInBytes], NULL);
+    id mask = (id)CGImageMaskCreate(
+                                          [self size].width,
+                                          [self size].height,
+                                          8, 8, [self size].width,
+                                          provider, NULL, 0);
+
+    CGDataProviderRelease(provider);
+    stippleMask = (CGImageRef)[mask retain];
+  }
   return stippleMask;
 }

--- src/nsterm.h.orig
+++ src/nsterm.h
@@ -670,7 +670,7 @@ enum ns_return_frame_mode
 {
   NSBitmapImageRep *bmRep; /* used for accessing pixel data */
   unsigned char *pixmapData[5]; /* shortcut to access pixel data */
-  NSColor *stippleMask;
+  CGImageRef stippleMask;
 @public
   NSAffineTransform *transform;
   BOOL smoothing;
@@ -687,8 +687,8 @@ enum ns_return_frame_mode
                green: (unsigned char)g blue: (unsigned char)b
               alpha:(unsigned char)a;
 - (void)setAlphaAtX: (int)x Y: (int)y to: (unsigned char)a;
-- (NSColor *)stippleMask;
+- (CGImageRef)stippleMask;
 - (Lisp_Object)getMetadata;
 - (BOOL)setFrame: (unsigned int) index;
 - (void)setTransform: (double[3][3]) m;

--- src/nsterm.m.orig
+++ src/nsterm.m
@@ -2903,22 +2903,24 @@ Hide the window (X11 semantics)
 static void
 ns_define_fringe_bitmap (int which, unsigned short *bits, int h, int w)
 {
-  NSBezierPath *p = [NSBezierPath bezierPath];
-
   if (!fringe_bmp)
     fringe_bmp = [[NSMutableDictionary alloc] initWithCapacity:25];

-  [p moveToPoint:NSMakePoint (0, 0)];

-  for (int y = 0 ; y < h ; y++)
-    for (int x = 0 ; x < w ; x++)
-      {
-        bool bit = bits[y] & (1 << (w - x - 1));
-        if (bit)
-          [p appendBezierPathWithRect:NSMakeRect (x, y, 1, 1)];
-      }
+  for (int i = 0; i < h; i++)
+    bits[i] = ~bits[i];
+
+  CGDataProviderRef provider = CGDataProviderCreateWithData (NULL, bits,
+					   sizeof (unsigned short) * h, NULL);
+  if (provider) {
+    id p = (id)CGImageMaskCreate (w, h, 1, 1,
+                 sizeof (unsigned short),
+                 provider, NULL, 0);
+    CGDataProviderRelease (provider);
+
+    [fringe_bmp setObject:p forKey:[NSNumber numberWithInt:which]];
+  }

-  [fringe_bmp setObject:p forKey:[NSNumber numberWithInt:which]];
 }


@@ -2981,26 +2983,29 @@ Hide the window (X11 semantics)
       NSRectFill (clearRect);
     }

-  NSBezierPath *bmp = [fringe_bmp objectForKey:[NSNumber 
numberWithInt:p->which]];
+  CGImageRef bmp = (CGImageRef)[fringe_bmp objectForKey:[NSNumber 
numberWithInt:p->which]];

   if (bmp == nil
       && p->which < max_used_fringe_bitmap)
     {
       gui_define_fringe_bitmap (f, p->which);
-      bmp = [fringe_bmp objectForKey: [NSNumber numberWithInt: p->which]];
+      bmp = (CGImageRef)[fringe_bmp objectForKey: [NSNumber 
numberWithInt: p->which]];
     }

   if (bmp)
     {
-      NSAffineTransform *transform = [NSAffineTransform transform];
-      NSColor *bm_color;
+      CGRect bounds = CGRectMake (p->x, p->y - p->dh,
+			   CGImageGetWidth (bmp), CGImageGetHeight (bmp));
+
+      NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
+      [ctx saveGraphicsState];
+      CGContextRef context = [ctx CGContext];

-      /* Because the image is defined at (0, 0) we need to take a copy
-         and then transform that copy to the new origin.  */
-      bmp = [bmp copy];
-      [transform translateXBy:p->x yBy:p->y - p->dh];
-      [bmp transformUsingAffineTransform:transform];
+      CGContextTranslateCTM (context,
+			     CGRectGetMinX (bounds), CGRectGetMaxY (bounds));
+      CGContextScaleCTM (context, 1, -1);

+      NSColor *bm_color;
       if (!p->cursor_p)
         bm_color = [NSColor colorWithUnsignedLong:face->foreground];
       else if (p->overlay_p)
@@ -3009,9 +3014,10 @@ Hide the window (X11 semantics)
         bm_color = f->output_data.ns->cursor_color;

       [bm_color set];
-      [bmp fill];
+      bounds.origin = CGPointZero;
+      CGContextDrawImage (context, bounds, bmp);

-      [bmp release];
+      [[NSGraphicsContext currentContext] restoreGraphicsState];
     }
   ns_unfocus (f);
 }
@@ -3273,11 +3279,10 @@ Note that CURSOR_WIDTH is meaningful only for 
(h)bar cursors.
 ns_draw_underwave (struct glyph_string *s, EmacsCGFloat width, 
EmacsCGFloat x)
 {
   int wave_height = 3, wave_length = 2;
-  int y, dx, dy, odd, xmax;
-  NSPoint a, b;
+  int y, dx, dy, xmax;
   NSRect waveClip;

-  dx = wave_length;
+  dx = wave_length * 2;
   dy = wave_height - 1;
   y =  s->ybase - wave_height + 3;
   xmax = x + width;
@@ -3287,25 +3292,24 @@ Note that CURSOR_WIDTH is meaningful only for 
(h)bar cursors.
   [[NSGraphicsContext currentContext] saveGraphicsState];
   NSRectClip (waveClip);

-  /* Draw the waves */
-  a.x = x - ((int)(x) % dx) + (EmacsCGFloat) 0.5;
-  b.x = a.x + dx;
-  odd = (int)(a.x/dx) % 2;
-  a.y = b.y = y + 0.5;
+  float ax = x - ((int)(x) % dx);
+  float ay = y + wave_height / 2.0;

-  if (odd)
-    a.y += dy;
-  else
-    b.y += dy;
+  NSBezierPath *path = [[NSBezierPath alloc] init];
+  [path moveToPoint: (NSPoint){ ax, ay }];
+
+  NSPoint stepOne = { dx, 0 };
+  NSPoint controlOne = { 0.5 * dx, dy };
+  NSPoint controlTwo = { 0.5 * dx, -dy };

-  while (a.x <= xmax)
+  while (ax <= xmax)
     {
-      [NSBezierPath strokeLineFromPoint:a toPoint:b];
-      a.x = b.x, a.y = b.y;
-      b.x += dx, b.y = y + 0.5 + odd*dy;
-      odd = !odd;
+      [path relativeCurveToPoint:stepOne controlPoint1:controlOne 
controlPoint2:controlTwo];
+      ax += dx;
     }

+  [path stroke];
+
   /* Restore previous clipping rectangle(s) */
   [[NSGraphicsContext currentContext] restoreGraphicsState];
 }
@@ -3825,10 +3829,35 @@ Function modeled after x_draw_glyph_string_box ().
       int box_line_width = max (s->face->box_horizontal_line_width, 0);

       if (s->stippled_p)
-	{
-	  struct ns_display_info *dpyinfo = FRAME_DISPLAY_INFO (s->f);
-	  [[dpyinfo->bitmaps[face->stipple-1].img stippleMask] set];
-	  goto fill;
+        {
+          [[NSColor colorWithUnsignedLong:face->background] set];
+          r = NSMakeRect (s->x, s->y + box_line_width,
+                          s->background_width,
+                          s->height - 2 * box_line_width);
+          NSRectFill (r);
+          s->background_filled_p = 1;
+
+          struct ns_display_info *dpyinfo = FRAME_DISPLAY_INFO (s->f);
+          CGImageRef mask =
+            [dpyinfo->bitmaps[face->stipple - 1].img stippleMask];
+
+          CGRect bounds = CGRectMake (s->x, s->y + box_line_width,
+                                      s->background_width,
+                                      s->height - 2 * box_line_width);
+          NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
+          [ctx saveGraphicsState];
+          CGContextRef context = [ctx CGContext];
+
+          CGContextClipToRect (context, bounds);
+
+          CGContextScaleCTM (context, 1, -1);
+          [[NSColor colorWithUnsignedLong:face->foreground] set];
+
+          CGRect imageSize = CGRectMake (0, 0, CGImageGetWidth (mask),
+                                         CGImageGetHeight (mask));
+
+          CGContextDrawTiledImage (context, imageSize, mask);
+          [[NSGraphicsContext currentContext] restoreGraphicsState];
 	}
       else if (FONT_HEIGHT (s->font) < s->height - 2 * box_line_width
 	       /* When xdisp.c ignores FONT_HEIGHT, we cannot trust font
@@ -3851,7 +3880,6 @@ Function modeled after x_draw_glyph_string_box ().
 	  else
 	    [FRAME_CURSOR_COLOR (s->f) set];

-	fill:
 	  r = NSMakeRect (s->x, s->y + box_line_width,
 			  s->background_width,
 			  s->height - 2 * box_line_width);
@@ -4175,12 +4203,42 @@ Function modeled after x_draw_glyph_string_box ().
 	  dpyinfo = FRAME_DISPLAY_INFO (s->f);
 	  if (s->hl == DRAW_CURSOR)
 	    [FRAME_CURSOR_COLOR (s->f) set];
-	  else if (s->stippled_p)
-	    [[dpyinfo->bitmaps[s->face->stipple - 1].img stippleMask] set];
-	  else
+	  else if (s->stippled_p) {
+              [[NSColor colorWithUnsignedLong:s->face->background]
+                set];
+              NSRectFill (
+                NSMakeRect (x, s->y, background_width, s->height));
+
+              CGImageRef mask =
+                [dpyinfo->bitmaps[s->face->stipple - 1]
+                    .img stippleMask];
+
+              CGRect bounds
+                = CGRectMake (s->x, s->y, s->background_width,
+                              s->height);
+
+              NSGraphicsContext *ctx =
+                [NSGraphicsContext currentContext];
+              [ctx saveGraphicsState];
+              CGContextRef context = [ctx CGContext];
+              CGContextClipToRect(context, bounds);
+              CGContextScaleCTM (context, 1, -1);
+              [[NSColor colorWithUnsignedLong:s->face->foreground]
+                set];
+
+              CGRect imageSize
+                = CGRectMake (0, 0, CGImageGetWidth (mask),
+                              CGImageGetHeight (mask));
+
+              CGContextDrawTiledImage (context, imageSize, mask);
+
+              [[NSGraphicsContext currentContext]
+		restoreGraphicsState];
+	    }
+	  else {
 	    [[NSColor colorWithUnsignedLong: s->face->background] set];
-
-	  NSRectFill (NSMakeRect (x, s->y, background_width, s->height));
+            NSRectFill (NSMakeRect (x, s->y, background_width, s->height));
+          }
 	}
     }





Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#73563; Package emacs. (Sat, 17 May 2025 09:27:01 GMT) Full text and rfc822 format available.

Message #14 received at 73563 <at> debbugs.gnu.org (full text, mbox):

From: Eli Zaretskii <eliz <at> gnu.org>
To: Jordan Ellis Coppard <jc+o.emacs <at> wz.ht>,
 Alan Third <alan <at> idiocy.org>, Gerd Möllmann
 <gerd.moellmann <at> gmail.com> 
Cc: ben <at> bensimms.moe, 73563 <at> debbugs.gnu.org
Subject: Re: bug#73563: [Ben Simms] Performance bottleneck in
 ns_draw_fringe_bitmap
Date: Sat, 17 May 2025 12:26:26 +0300
> Cc: Ben Simms <ben <at> bensimms.moe>
> Date: Tue, 13 May 2025 20:37:55 +0900
> From:  Jordan Ellis Coppard via "Bug reports for GNU Emacs,
>  the Swiss army knife of text editors" <bug-gnu-emacs <at> gnu.org>
> 
> Howdy,
> 
> 
> I've been using the following patch (bottom of this email) until my most 
> recent rebuild of Emacs from master. Since the recent commit fixing 
> stipple drawing the patch can be reduced to just: 
> https://github.com/emacs-mirror/emacs/commit/7f2efe6503fdce2a4e552c14802644a05b581bc7 
> (link to a diff authored by Ben Simms).
> 
> I've now noticed again extreme slowdown when fringe bitmaps are used. It 
> appears to be proportional to the number of bits set in the bitmap. An 
> 'empty' bitmap (all zeroes) doesn't yield any noticeable slowdown, a 
> bitmap with 1 bit set yields a little, with 2 its noticeable and with 
> all 8 bits set Emacs takes up to 1 second to respond to any input at all.
> 
> This has been the case since at least August of last year. I can get a 
> minimal reproduction but it has happened on completely different 
> configurations with the only common denominator being the NS build (on 
> ARM-based macOS), and the use of fringe bitmaps.
> 
> Below is trace with update_frame as the root context. Captured with 
> Instruments.app (which uses dtrace and more under the hood IIRC).
> 
> ns_draw_fringe_bitmap takes up 94% of the time for the short 
> reproduction this trace records. The deepest the trace goes is to 
> CG::Path::recalculate_bounding_box() which eats 80% of time. So instead 
> of a cached value the bounding box is being recalculated every time 
> which appears very expensive, but beyond that I have close to zero 
> experience with macOS' graphics stack.
> 
> I've just recompiled Emacs against the same commit of my recent master 
> build (648453c04d9b91d96452b930c0c948b0b39b5dc0) except now with the 
> patch applied (since the stipple changes are merged it's just the 
> smallest subset: 
> https://github.com/emacs-mirror/emacs/commit/7f2efe6503fdce2a4e552c14802644a05b581bc7) 
> and once again fringe performance is back to being buttery smooth.

Alan and Gerd, any comments or suggestions?

> (trace also here in-case formatting gets borked:
> trace:
> 
> 3.07 s  100.0%	0 s	               update_frame
> 3.06 s  100.0%	0 s	                update_window_tree
> 3.06 s  100.0%	1.00 ms	                 update_window
> 2.89 s  94.2%	0 s	                  gui_update_window_end
> 2.88 s  94.1%	0 s	                   draw_window_fringes
> 2.88 s  93.9%	0 s	                    draw_fringe_bitmap
> 2.88 s  93.9%	0 s	                     draw_fringe_bitmap_1
> 2.88 s  93.9%	0 s	                      ns_draw_fringe_bitmap
> 2.72 s  88.7%	0 s	                       -[NSBezierPath copyWithZone:]
> 2.72 s  88.7%	0 s	                        -[NSBezierPath _appendToPath:]
> 2.72 s  88.7%	0 s	                         -[NSBezierPath 
> _enumeratePathElementsUsingBlock:]
> 2.72 s  88.7%	0 s	                          CGPathApplyWithBlock2
> 2.72 s  88.7%	5.00 ms	                           CG::Path::apply(void 
> (CGPathElementType, CGPoint const*, bool*) block_pointer) const
> 2.71 s  88.5%	30.00 ms	 
> __CGPathApplyWithBlock2_block_invoke
> 2.68 s  87.4%	11.00 ms	                             __49-[NSBezierPath 
> _enumeratePathElementsUsingBlock:]_block_invoke
> 2.48 s  80.8%	1.00 ms	 
> -[NSBezierPath(NSBezierPathDevicePrimitives) _deviceMoveToPoint:]
> 2.47 s  80.6%	2.00 ms	                               CGPathMoveToPoint
> 2.46 s  80.3%	2.46 s	 
> CG::Path::recalculate_bounding_box()
> 5.00 ms   0.2%	4.00 ms	 
> CG::Path::move_to_point(CGPoint const&, CGAffineTransform const*)
> 1.00 ms   0.0%	1.00 ms	 
> CG::Path::convert_to_huge()
> 2.00 ms   0.1%	2.00 ms	                                (anonymous 
> namespace)::transform_is_valid(CGAffineTransform const*)
> 1.00 ms   0.0%	1.00 ms	 
> CG::Path::convert_to_huge()
> 1.00 ms   0.0%	1.00 ms	 
> CGFloatValidateWithLog
> 1.00 ms   0.0%	1.00 ms	 
> DYLD-STUB$$CGPathMoveToPoint
> 1.00 ms   0.0%	1.00 ms	                               -[NSBezierPath 
> _cgPath]
> 1.00 ms   0.0%	1.00 ms	                               objc_msgSend
> 1.00 ms   0.0%	1.00 ms	                               (anonymous 
> namespace)::transform_is_valid(CGAffineTransform const*)
> 67.00 ms   2.2%	7.00 ms	 
> -[NSBezierPath(NSBezierPathDevicePrimitives) _deviceLineToPoint:]
> 40.00 ms   1.3%	5.00 ms	 
> -[NSBezierPath(NSBezierPathDevicePrimitives) _deviceClosePath]
> 39.00 ms   1.3%	2.00 ms	                              -[NSBezierPath 
> lineToPoint:]
> 14.00 ms   0.5%	14.00 ms	                              objc_msgSend
> 6.00 ms   0.2%	6.00 ms	                              -[NSBezierPath _cgPath]
> 5.00 ms   0.2%	5.00 ms	                              CGPathAddLineToPoint
> 4.00 ms   0.1%	4.00 ms	                              __30-[NSBezierPath 
> _appendToPath:]_block_invoke
> 4.00 ms   0.1%	4.00 ms	 
> objc_msgSend$lineToPoint:
> 2.00 ms   0.1%	2.00 ms	                              CGPathIsEmpty
> 2.00 ms   0.1%	2.00 ms	 
> objc_msgSend$moveToPoint:
> 2.00 ms   0.1%	2.00 ms	 
> CG::Path::close_subpath()
> 1.00 ms   0.0%	1.00 ms	 
> objc_msgSend$_deviceLineToPoint:
> 1.00 ms   0.0%	1.00 ms	                              CGPathGetCurrentPoint
> 1.00 ms   0.0%	1.00 ms	                              -[NSBezierPath 
> moveToPoint:]
> 1.00 ms   0.0%	1.00 ms	 
> objc_msgSend$_deviceClosePath
> 1.00 ms   0.0%	1.00 ms	                              objc_msgSend$closePath
> 1.00 ms   0.0%	1.00 ms	 
> objc_msgSend$_deviceMoveToPoint:
> 3.00 ms   0.1%	3.00 ms	                             -[NSBezierPath 
> lineToPoint:]
> 1.00 ms   0.0%	1.00 ms	 
> -[NSBezierPath(NSBezierPathDevicePrimitives) _deviceLineToPoint:]
> 2.00 ms   0.1%	2.00 ms	                            __49-[NSBezierPath 
> _enumeratePathElementsUsingBlock:]_block_invoke
> 1.00 ms   0.0%	1.00 ms	                        objc_msgSend$setFlatness:
> 104.00 ms   3.4%	0 s	                       -[NSBezierPath fill]
> 42.00 ms   1.4%	0 s	                       -[NSBezierPath 
> transformUsingAffineTransform:]
> 7.00 ms   0.2%	0 s	                       NSRectFill
> 2.00 ms   0.1%	0 s	                       NSColorSetWithFillAndStroke
> 1.00 ms   0.0%	1.00 ms	                       CGContextClipToRect
> 1.00 ms   0.0%	0 s	                       -[NSBezierPath dealloc]
> 1.00 ms   0.0%	1.00 ms	                       CGGStateSetCompositeOperation
> 1.00 ms   0.0%	1.00 ms	                      lookup_named_face
> 5.00 ms   0.2%	4.00 ms	                    set_buffer_internal_2
> 1.00 ms   0.0%	0 s	                   unblock_input
> 1.00 ms   0.0%	0 s	                   ns_draw_window_cursor
> 121.00 ms   3.9%	0 s	                  ns_scroll_run
> 51.00 ms   1.7%	1.00 ms	                  update_window_line
> 4.00 ms   0.1%	4.00 ms	                  row_equal_p
> 1.00 ms   0.0%	1.00 ms	                  xwidget_end_redisplay
> 1.00 ms   0.0%	0 s	                ns_update_end
> 
> 
> 
> Patch:
> 
> --- src/nsimage.m.orig
> +++ src/nsimage.m
> @@ -28,6 +28,7 @@ Updated by Christian Limpach (chris <at> nice.ch)
>   /* This should be the first include, as it may set up #defines affecting
>      interpretation of even the system includes.  */
>   #include <config.h>
> +#include <CoreGraphics/CoreGraphics.h>
> 
>   #include "lisp.h"
>   #include "dispextern.h"
> @@ -510,10 +511,20 @@ - (void) setAlphaAtX: (int) x Y: (int) y to: 
> (unsigned char) a
>   }
> 
>   /* Returns a pattern color, which is cached here.  */
> -- (NSColor *)stippleMask
> +- (CGImageRef)stippleMask
>   {
> -  if (stippleMask == nil)
> -      stippleMask = [[NSColor colorWithPatternImage: self] retain];
> +  if (stippleMask == nil) {
> +    CGDataProviderRef provider = CGDataProviderCreateWithData (NULL, 
> [bmRep bitmapData],
> +                                                             [self 
> sizeInBytes], NULL);
> +    id mask = (id)CGImageMaskCreate(
> +                                          [self size].width,
> +                                          [self size].height,
> +                                          8, 8, [self size].width,
> +                                          provider, NULL, 0);
> +
> +    CGDataProviderRelease(provider);
> +    stippleMask = (CGImageRef)[mask retain];
> +  }
>     return stippleMask;
>   }
> 
> --- src/nsterm.h.orig
> +++ src/nsterm.h
> @@ -670,7 +670,7 @@ enum ns_return_frame_mode
>   {
>     NSBitmapImageRep *bmRep; /* used for accessing pixel data */
>     unsigned char *pixmapData[5]; /* shortcut to access pixel data */
> -  NSColor *stippleMask;
> +  CGImageRef stippleMask;
>   @public
>     NSAffineTransform *transform;
>     BOOL smoothing;
> @@ -687,8 +687,8 @@ enum ns_return_frame_mode
>                  green: (unsigned char)g blue: (unsigned char)b
>                 alpha:(unsigned char)a;
>   - (void)setAlphaAtX: (int)x Y: (int)y to: (unsigned char)a;
> -- (NSColor *)stippleMask;
> +- (CGImageRef)stippleMask;
>   - (Lisp_Object)getMetadata;
>   - (BOOL)setFrame: (unsigned int) index;
>   - (void)setTransform: (double[3][3]) m;
> 
> --- src/nsterm.m.orig
> +++ src/nsterm.m
> @@ -2903,22 +2903,24 @@ Hide the window (X11 semantics)
>   static void
>   ns_define_fringe_bitmap (int which, unsigned short *bits, int h, int w)
>   {
> -  NSBezierPath *p = [NSBezierPath bezierPath];
> -
>     if (!fringe_bmp)
>       fringe_bmp = [[NSMutableDictionary alloc] initWithCapacity:25];
> 
> -  [p moveToPoint:NSMakePoint (0, 0)];
> 
> -  for (int y = 0 ; y < h ; y++)
> -    for (int x = 0 ; x < w ; x++)
> -      {
> -        bool bit = bits[y] & (1 << (w - x - 1));
> -        if (bit)
> -          [p appendBezierPathWithRect:NSMakeRect (x, y, 1, 1)];
> -      }
> +  for (int i = 0; i < h; i++)
> +    bits[i] = ~bits[i];
> +
> +  CGDataProviderRef provider = CGDataProviderCreateWithData (NULL, bits,
> +					   sizeof (unsigned short) * h, NULL);
> +  if (provider) {
> +    id p = (id)CGImageMaskCreate (w, h, 1, 1,
> +                 sizeof (unsigned short),
> +                 provider, NULL, 0);
> +    CGDataProviderRelease (provider);
> +
> +    [fringe_bmp setObject:p forKey:[NSNumber numberWithInt:which]];
> +  }
> 
> -  [fringe_bmp setObject:p forKey:[NSNumber numberWithInt:which]];
>   }
> 
> 
> @@ -2981,26 +2983,29 @@ Hide the window (X11 semantics)
>         NSRectFill (clearRect);
>       }
> 
> -  NSBezierPath *bmp = [fringe_bmp objectForKey:[NSNumber 
> numberWithInt:p->which]];
> +  CGImageRef bmp = (CGImageRef)[fringe_bmp objectForKey:[NSNumber 
> numberWithInt:p->which]];
> 
>     if (bmp == nil
>         && p->which < max_used_fringe_bitmap)
>       {
>         gui_define_fringe_bitmap (f, p->which);
> -      bmp = [fringe_bmp objectForKey: [NSNumber numberWithInt: p->which]];
> +      bmp = (CGImageRef)[fringe_bmp objectForKey: [NSNumber 
> numberWithInt: p->which]];
>       }
> 
>     if (bmp)
>       {
> -      NSAffineTransform *transform = [NSAffineTransform transform];
> -      NSColor *bm_color;
> +      CGRect bounds = CGRectMake (p->x, p->y - p->dh,
> +			   CGImageGetWidth (bmp), CGImageGetHeight (bmp));
> +
> +      NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
> +      [ctx saveGraphicsState];
> +      CGContextRef context = [ctx CGContext];
> 
> -      /* Because the image is defined at (0, 0) we need to take a copy
> -         and then transform that copy to the new origin.  */
> -      bmp = [bmp copy];
> -      [transform translateXBy:p->x yBy:p->y - p->dh];
> -      [bmp transformUsingAffineTransform:transform];
> +      CGContextTranslateCTM (context,
> +			     CGRectGetMinX (bounds), CGRectGetMaxY (bounds));
> +      CGContextScaleCTM (context, 1, -1);
> 
> +      NSColor *bm_color;
>         if (!p->cursor_p)
>           bm_color = [NSColor colorWithUnsignedLong:face->foreground];
>         else if (p->overlay_p)
> @@ -3009,9 +3014,10 @@ Hide the window (X11 semantics)
>           bm_color = f->output_data.ns->cursor_color;
> 
>         [bm_color set];
> -      [bmp fill];
> +      bounds.origin = CGPointZero;
> +      CGContextDrawImage (context, bounds, bmp);
> 
> -      [bmp release];
> +      [[NSGraphicsContext currentContext] restoreGraphicsState];
>       }
>     ns_unfocus (f);
>   }
> @@ -3273,11 +3279,10 @@ Note that CURSOR_WIDTH is meaningful only for 
> (h)bar cursors.
>   ns_draw_underwave (struct glyph_string *s, EmacsCGFloat width, 
> EmacsCGFloat x)
>   {
>     int wave_height = 3, wave_length = 2;
> -  int y, dx, dy, odd, xmax;
> -  NSPoint a, b;
> +  int y, dx, dy, xmax;
>     NSRect waveClip;
> 
> -  dx = wave_length;
> +  dx = wave_length * 2;
>     dy = wave_height - 1;
>     y =  s->ybase - wave_height + 3;
>     xmax = x + width;
> @@ -3287,25 +3292,24 @@ Note that CURSOR_WIDTH is meaningful only for 
> (h)bar cursors.
>     [[NSGraphicsContext currentContext] saveGraphicsState];
>     NSRectClip (waveClip);
> 
> -  /* Draw the waves */
> -  a.x = x - ((int)(x) % dx) + (EmacsCGFloat) 0.5;
> -  b.x = a.x + dx;
> -  odd = (int)(a.x/dx) % 2;
> -  a.y = b.y = y + 0.5;
> +  float ax = x - ((int)(x) % dx);
> +  float ay = y + wave_height / 2.0;
> 
> -  if (odd)
> -    a.y += dy;
> -  else
> -    b.y += dy;
> +  NSBezierPath *path = [[NSBezierPath alloc] init];
> +  [path moveToPoint: (NSPoint){ ax, ay }];
> +
> +  NSPoint stepOne = { dx, 0 };
> +  NSPoint controlOne = { 0.5 * dx, dy };
> +  NSPoint controlTwo = { 0.5 * dx, -dy };
> 
> -  while (a.x <= xmax)
> +  while (ax <= xmax)
>       {
> -      [NSBezierPath strokeLineFromPoint:a toPoint:b];
> -      a.x = b.x, a.y = b.y;
> -      b.x += dx, b.y = y + 0.5 + odd*dy;
> -      odd = !odd;
> +      [path relativeCurveToPoint:stepOne controlPoint1:controlOne 
> controlPoint2:controlTwo];
> +      ax += dx;
>       }
> 
> +  [path stroke];
> +
>     /* Restore previous clipping rectangle(s) */
>     [[NSGraphicsContext currentContext] restoreGraphicsState];
>   }
> @@ -3825,10 +3829,35 @@ Function modeled after x_draw_glyph_string_box ().
>         int box_line_width = max (s->face->box_horizontal_line_width, 0);
> 
>         if (s->stippled_p)
> -	{
> -	  struct ns_display_info *dpyinfo = FRAME_DISPLAY_INFO (s->f);
> -	  [[dpyinfo->bitmaps[face->stipple-1].img stippleMask] set];
> -	  goto fill;
> +        {
> +          [[NSColor colorWithUnsignedLong:face->background] set];
> +          r = NSMakeRect (s->x, s->y + box_line_width,
> +                          s->background_width,
> +                          s->height - 2 * box_line_width);
> +          NSRectFill (r);
> +          s->background_filled_p = 1;
> +
> +          struct ns_display_info *dpyinfo = FRAME_DISPLAY_INFO (s->f);
> +          CGImageRef mask =
> +            [dpyinfo->bitmaps[face->stipple - 1].img stippleMask];
> +
> +          CGRect bounds = CGRectMake (s->x, s->y + box_line_width,
> +                                      s->background_width,
> +                                      s->height - 2 * box_line_width);
> +          NSGraphicsContext *ctx = [NSGraphicsContext currentContext];
> +          [ctx saveGraphicsState];
> +          CGContextRef context = [ctx CGContext];
> +
> +          CGContextClipToRect (context, bounds);
> +
> +          CGContextScaleCTM (context, 1, -1);
> +          [[NSColor colorWithUnsignedLong:face->foreground] set];
> +
> +          CGRect imageSize = CGRectMake (0, 0, CGImageGetWidth (mask),
> +                                         CGImageGetHeight (mask));
> +
> +          CGContextDrawTiledImage (context, imageSize, mask);
> +          [[NSGraphicsContext currentContext] restoreGraphicsState];
>   	}
>         else if (FONT_HEIGHT (s->font) < s->height - 2 * box_line_width
>   	       /* When xdisp.c ignores FONT_HEIGHT, we cannot trust font
> @@ -3851,7 +3880,6 @@ Function modeled after x_draw_glyph_string_box ().
>   	  else
>   	    [FRAME_CURSOR_COLOR (s->f) set];
> 
> -	fill:
>   	  r = NSMakeRect (s->x, s->y + box_line_width,
>   			  s->background_width,
>   			  s->height - 2 * box_line_width);
> @@ -4175,12 +4203,42 @@ Function modeled after x_draw_glyph_string_box ().
>   	  dpyinfo = FRAME_DISPLAY_INFO (s->f);
>   	  if (s->hl == DRAW_CURSOR)
>   	    [FRAME_CURSOR_COLOR (s->f) set];
> -	  else if (s->stippled_p)
> -	    [[dpyinfo->bitmaps[s->face->stipple - 1].img stippleMask] set];
> -	  else
> +	  else if (s->stippled_p) {
> +              [[NSColor colorWithUnsignedLong:s->face->background]
> +                set];
> +              NSRectFill (
> +                NSMakeRect (x, s->y, background_width, s->height));
> +
> +              CGImageRef mask =
> +                [dpyinfo->bitmaps[s->face->stipple - 1]
> +                    .img stippleMask];
> +
> +              CGRect bounds
> +                = CGRectMake (s->x, s->y, s->background_width,
> +                              s->height);
> +
> +              NSGraphicsContext *ctx =
> +                [NSGraphicsContext currentContext];
> +              [ctx saveGraphicsState];
> +              CGContextRef context = [ctx CGContext];
> +              CGContextClipToRect(context, bounds);
> +              CGContextScaleCTM (context, 1, -1);
> +              [[NSColor colorWithUnsignedLong:s->face->foreground]
> +                set];
> +
> +              CGRect imageSize
> +                = CGRectMake (0, 0, CGImageGetWidth (mask),
> +                              CGImageGetHeight (mask));
> +
> +              CGContextDrawTiledImage (context, imageSize, mask);
> +
> +              [[NSGraphicsContext currentContext]
> +		restoreGraphicsState];
> +	    }
> +	  else {
>   	    [[NSColor colorWithUnsignedLong: s->face->background] set];
> -
> -	  NSRectFill (NSMakeRect (x, s->y, background_width, s->height));
> +            NSRectFill (NSMakeRect (x, s->y, background_width, s->height));
> +          }
>   	}
>       }
> 
> 
> 
> 
> 




Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#73563; Package emacs. (Sat, 17 May 2025 09:46:02 GMT) Full text and rfc822 format available.

Message #17 received at 73563 <at> debbugs.gnu.org (full text, mbox):

From: Gerd Möllmann <gerd.moellmann <at> gmail.com>
To: Eli Zaretskii <eliz <at> gnu.org>
Cc: ben <at> bensimms.moe, Alan Third <alan <at> idiocy.org>, 73563 <at> debbugs.gnu.org,
 Jordan Ellis Coppard <jc+o.emacs <at> wz.ht>
Subject: Re: bug#73563: [Ben Simms] Performance bottleneck in
 ns_draw_fringe_bitmap
Date: Sat, 17 May 2025 11:45:33 +0200
Eli Zaretskii <eliz <at> gnu.org> writes:

> Alan and Gerd, any comments or suggestions?

That goes beyond my macOS graphics knowledge I'm afraid.




Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#73563; Package emacs. (Sat, 17 May 2025 11:25:01 GMT) Full text and rfc822 format available.

Message #20 received at 73563 <at> debbugs.gnu.org (full text, mbox):

From: Alan Third <alan <at> idiocy.org>
To: Eli Zaretskii <eliz <at> gnu.org>
Cc: Gerd Möllmann <gerd.moellmann <at> gmail.com>,
 ben <at> bensimms.moe, 73563 <at> debbugs.gnu.org,
 Jordan Ellis Coppard <jc+o.emacs <at> wz.ht>
Subject: Re: bug#73563: [Ben Simms] Performance bottleneck in
 ns_draw_fringe_bitmap
Date: Sat, 17 May 2025 12:23:53 +0100
On Sat, May 17, 2025 at 12:26:26PM +0300, Eli Zaretskii wrote:
> > Cc: Ben Simms <ben <at> bensimms.moe>
> > Date: Tue, 13 May 2025 20:37:55 +0900
> > From:  Jordan Ellis Coppard via "Bug reports for GNU Emacs,
> >  the Swiss army knife of text editors" <bug-gnu-emacs <at> gnu.org>
> > 
> > Howdy,
> > 
> > 
> > I've been using the following patch (bottom of this email) until my most 
> > recent rebuild of Emacs from master. Since the recent commit fixing 
> > stipple drawing the patch can be reduced to just: 
> > https://github.com/emacs-mirror/emacs/commit/7f2efe6503fdce2a4e552c14802644a05b581bc7 
> > (link to a diff authored by Ben Simms).
> > 
> > I've now noticed again extreme slowdown when fringe bitmaps are used. It 
> > appears to be proportional to the number of bits set in the bitmap. An 
> > 'empty' bitmap (all zeroes) doesn't yield any noticeable slowdown, a 
> > bitmap with 1 bit set yields a little, with 2 its noticeable and with 
> > all 8 bits set Emacs takes up to 1 second to respond to any input at all.
> > 
> > This has been the case since at least August of last year. I can get a 
> > minimal reproduction but it has happened on completely different 
> > configurations with the only common denominator being the NS build (on 
> > ARM-based macOS), and the use of fringe bitmaps.
> > 
> > Below is trace with update_frame as the root context. Captured with 
> > Instruments.app (which uses dtrace and more under the hood IIRC).
> > 
> > ns_draw_fringe_bitmap takes up 94% of the time for the short 
> > reproduction this trace records. The deepest the trace goes is to 
> > CG::Path::recalculate_bounding_box() which eats 80% of time. So instead 
> > of a cached value the bounding box is being recalculated every time 
> > which appears very expensive, but beyond that I have close to zero 
> > experience with macOS' graphics stack.
> > 
> > I've just recompiled Emacs against the same commit of my recent master 
> > build (648453c04d9b91d96452b930c0c948b0b39b5dc0) except now with the 
> > patch applied (since the stipple changes are merged it's just the 
> > smallest subset: 
> > https://github.com/emacs-mirror/emacs/commit/7f2efe6503fdce2a4e552c14802644a05b581bc7) 
> > and once again fringe performance is back to being buttery smooth.
> 
> Alan and Gerd, any comments or suggestions?

I've not looked at it closely, but I will point out that AFAIK GNUStep
does not support Core Graphics, so any function or struct starting
with "CG" needs an alternative method in place.

-- 
Alan Third




Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#73563; Package emacs. (Sat, 24 May 2025 16:12:02 GMT) Full text and rfc822 format available.

Message #23 received at 73563 <at> debbugs.gnu.org (full text, mbox):

From: Alan Third <alan <at> idiocy.org>
To: Jordan Ellis Coppard <jc+o.emacs <at> wz.ht>
Cc: Ben Simms <ben <at> bensimms.moe>, 73563 <at> debbugs.gnu.org
Subject: Re: bug#73563: [Ben Simms] Performance bottleneck in
 ns_draw_fringe_bitmap
Date: Sat, 24 May 2025 17:10:55 +0100
[Message part 1 (text/plain, inline)]
On Tue, May 13, 2025 at 08:37:55PM +0900, Jordan Ellis Coppard wrote:
> Howdy,
> 
> 
> I've been using the following patch (bottom of this email) until my most
> recent rebuild of Emacs from master. Since the recent commit fixing stipple
> drawing the patch can be reduced to just: https://github.com/emacs-mirror/emacs/commit/7f2efe6503fdce2a4e552c14802644a05b581bc7
> (link to a diff authored by Ben Simms).
> 
> I've now noticed again extreme slowdown when fringe bitmaps are used. It
> appears to be proportional to the number of bits set in the bitmap. An
> 'empty' bitmap (all zeroes) doesn't yield any noticeable slowdown, a bitmap
> with 1 bit set yields a little, with 2 its noticeable and with all 8 bits
> set Emacs takes up to 1 second to respond to any input at all.
> 
> This has been the case since at least August of last year. I can get a
> minimal reproduction but it has happened on completely different
> configurations with the only common denominator being the NS build (on
> ARM-based macOS), and the use of fringe bitmaps.

I have no idea what's going on, but it looks from the trace like the
problem is in the transform to move it to the final position, or the
copy... Which are both odd as to my mind they should be fast
processes.

Since we can't really use Core Graphics code in the NS port, I've
tried improving the bitmap tracing a little. I have my doubts it will
improve things much, but can you please try it and see how it
compares? I tried it here and the difference between all three
versions of the code is within 0.2 of a second, and totally
inconsistent. I couldn't vouch for any of them being consistently
faster.


-- 
Alan Third
[0001-Simplified-NS-fringe-vectors.patch (text/x-diff, attachment)]

Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#73563; Package emacs. (Tue, 03 Jun 2025 08:27:03 GMT) Full text and rfc822 format available.

Message #26 received at 73563 <at> debbugs.gnu.org (full text, mbox):

From: Jordan Ellis Coppard <jc+o.emacs <at> wz.ht>
To: Alan Third <alan <at> idiocy.org>, 73563 <at> debbugs.gnu.org,
 Ben Simms <ben <at> bensimms.moe>
Subject: Re: bug#73563: [Ben Simms] Performance bottleneck in
 ns_draw_fringe_bitmap
Date: Tue, 3 Jun 2025 17:25:40 +0900
On 25/5/2025 1:10 am, Alan Third wrote:
> Since we can't really use Core Graphics code in the NS port, I've
> tried improving the bitmap tracing a little. I have my doubts it will
> improve things much, but can you please try it and see how it
> compares? I tried it here and the difference between all three
> versions of the code is within 0.2 of a second, and totally
> inconsistent. I couldn't vouch for any of them being consistently
> faster.

Your patch appears to have fixed it. I can now add 8-bit fringe bitmaps 
and scroll a buffer with no noticeable slowdown. Unpatched Emacs will 
slow down to a crawl, probably only redisplaying 1-3 times a second, Ben 
Simms' patch looks equally as performant as this one however Ben's seems 
to break with dape-mode which arranges the Emacs frame into a bunch of 
windows etc. Yours, Alan, seems to not bork Emacs display under 
dape-mode but there are tiny 1-pixel artifacts in the margin (only in 
dape-mode). Dape-mode seems to be using Emacs' features that break when 
combined with fringes as if I turn fringes off Emacs displays normally 
thereafter.

I tried writing a benchmark for this (here: 
https://gist.github.com/tsujp/79d743ca23555c679f5a9cb667ddc3be) but even 
on the version of Emacs which was incredibly laggy the results all came 
out within ~0.2 seconds of each other.

It appeared to lag the most when the margin was approaching the end of, 
or start of, the top/bottom of a buffer and while it still lagged a lot 
otherwise it was a tiny bit less so (it felt like).

I can provide screenshots and/or screen recordings of the lag in all 3 
variants (vanilla Emacs, Ben's patch, and your patch Alan) since this 
seems like quite a nasty thing to demonstrate.


/Jordan





Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#73563; Package emacs. (Tue, 03 Jun 2025 21:08:01 GMT) Full text and rfc822 format available.

Message #29 received at 73563 <at> debbugs.gnu.org (full text, mbox):

From: Alan Third <alan <at> idiocy.org>
To: Jordan Ellis Coppard <jc+o.emacs <at> wz.ht>
Cc: Ben Simms <ben <at> bensimms.moe>, 73563 <at> debbugs.gnu.org
Subject: Re: bug#73563: [Ben Simms] Performance bottleneck in
 ns_draw_fringe_bitmap
Date: Tue, 3 Jun 2025 22:07:01 +0100
On Tue, Jun 03, 2025 at 05:25:40PM +0900, Jordan Ellis Coppard wrote:
> On 25/5/2025 1:10 am, Alan Third wrote:
> > Since we can't really use Core Graphics code in the NS port, I've
> > tried improving the bitmap tracing a little. I have my doubts it will
> > improve things much, but can you please try it and see how it
> > compares? I tried it here and the difference between all three
> > versions of the code is within 0.2 of a second, and totally
> > inconsistent. I couldn't vouch for any of them being consistently
> > faster.
> 
> Your patch appears to have fixed it. I can now add 8-bit fringe bitmaps and
> scroll a buffer with no noticeable slowdown.

Thanks for testing!

> Unpatched Emacs will slow down to a crawl, probably only
> redisplaying 1-3 times a second, Ben Simms' patch looks equally as
> performant as this one however Ben's seems to break with dape-mode
> which arranges the Emacs frame into a bunch of windows etc.

Hmm, I'm surprised to hear that as I didn't think Ben's patch would
have touched anything that should break that.

> Yours, Alan, seems to not bork Emacs display under dape-mode but
> there are tiny 1-pixel artifacts in the margin (only in dape-mode).

Can you send a screenshot? That sounds odd.

-- 
Alan Third




Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#73563; Package emacs. (Fri, 06 Jun 2025 10:03:02 GMT) Full text and rfc822 format available.

Message #32 received at 73563 <at> debbugs.gnu.org (full text, mbox):

From: Jordan Ellis Coppard <jc+o.emacs <at> wz.ht>
To: Alan Third <alan <at> idiocy.org>, 73563 <at> debbugs.gnu.org,
 Ben Simms <ben <at> bensimms.moe>
Subject: Re: bug#73563: [Ben Simms] Performance bottleneck in
 ns_draw_fringe_bitmap
Date: Fri, 6 Jun 2025 19:01:45 +0900
On 4/6/2025 6:07 am, Alan Third wrote:
> Can you send a screenshot? That sounds odd.

I forgot to check that on emacs -Q and it appears to not be present 
there. In my config I change the size of the fringes a bit, however that 
might mean your patch doesn't account for that somehow. I'm still bisecting.

I've decided to record all 3 Emacs variants in-play here:

(1) Vanilla, unpatched Emacs.
(2) Emacs patched with Ben Simms' fringe changes.
(3) Emacs patched with yours (Alan Third's) fringe changes.

I've uploaded the three recordings to YouTube. All are emacs -Q. Note in 
1's case the extreme lag when scrolling the *test fringe bench* buffer 
and that in 2 and 3's case there is no such lag.

In all cases I start scrolling by holding down the arrow keys up/down 
(for simplicity, in actuality I am using C-n or a key set to run 
(next-line) and so forth) and then I scroll a bit with the trackpad.

I _think_ Ben's might be a bit more performant but at this point it 
feels like splitting hairs especially since there doesn't appear (yet) 
to be a concrete way to benchmark this without human interaction which 
is concerning (I think you and others would agree a way to get concrete 
data here would be better for long term regression testing).

Videos in respective order:
(1) https://www.youtube.com/watch?v=G8T0S1m-mLs
(2) https://www.youtube.com/watch?v=-9Ro3myXztM
(3) https://www.youtube.com/watch?v=CYwAfPBt8us

Here are four screenshots in the same respective order as the videos in 
terms of Emacs version. Unpatched Emacs with dape-mode shows no 
corruption, Ben's patch shows the frame and all it's windows borked 
(when it should look like 1, 3, or 4), your patch shows dape-mode 
without corruption, and the fourth screenshot which is Emacs (3) but 
with my dotfiles loaded shows the small 1-pixel-ish corruption in the 
margin (look at the fringe for lines 1-15). That corruption only appears 
when I scroll the buffer down and up. Whereas Ben's corruption appears 
immediately and indefinitely unless fringe-mode is toggled off.

Screenshots (direct links):
(1) https://i.imgur.com/BpXQxcj.png
(2) https://i.imgur.com/Nd3YPda.png
(3) https://i.imgur.com/gjPmMc2.png
(4) https://i.imgur.com/X2ybi2Q.png

Album link: https://imgur.com/a/0oReqr2

The window configurations in the Emacs frame (except for the fourth) are 
all exactly the same, screenshots taken at the same point in time:

M-x dape
M-x dape-breakpoint-toggle
and then 'r' in the dape console to start debugging.

I forgot to disable macOS' shadow-adding behaviour when capturing the 
screenshot.

Also there's a tangential thing I noticed. My benchmark takes ~18 
seconds to run under emacs -Q but (for all 3 Emacsen) only ~2.7 seconds 
when using my config 
(https://github.com/tsujp/dotfiles/tree/master/.config/emacs). As you 
can see from the fourth screenshot I disable native scroll bars, toolbar 
mode and various other things. Such a huge increase in performance there 
is.. perhaps concerning too.


/Jordan




This bug report was last modified 14 days ago.

Previous Next


GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997,2003 nCipher Corporation Ltd, 1994-97 Ian Jackson.