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

Full log


View this message in rfc822 format

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: 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));
> +          }
>   	}
>       }
> 
> 
> 
> 
> 




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.