6

I currently have an NSView that draws a grid pattern (essentially a guide of horizontal and vertical lines) with the idea being that a user can change the spacing of the grid and the color of the grid.

The purpose of the grid is to act as a guideline for the user when lining up objects. Everything works just fine with one exception. When I resize the NSWindow by dragging the resize handle, if my grid spacing is particularly small (say 10 pixels). the drag resize becomes lethargic in nature.

My drawRect code for the grid is as follows:

-(void)drawRect:(NSRect)dirtyRect {

    NSRect thisViewSize = [self bounds];

    // Set the line color

    [[NSColor colorWithDeviceRed:0 
                           green:(255/255.0) 
                            blue:(255/255.0) 
                           alpha:1] set];

    // Draw the vertical lines first

    NSBezierPath * verticalLinePath = [NSBezierPath bezierPath];

    int gridWidth = thisViewSize.size.width;
    int gridHeight = thisViewSize.size.height;

    int i;

    while (i < gridWidth)
    {
        i = i + [self currentSpacing];

        NSPoint startPoint = {i,0};
        NSPoint endPoint = {i, gridHeight};

        [verticalLinePath setLineWidth:1];
        [verticalLinePath moveToPoint:startPoint];
        [verticalLinePath lineToPoint:endPoint];
        [verticalLinePath stroke];
    }

    // Draw the horizontal lines

    NSBezierPath * horizontalLinePath = [NSBezierPath bezierPath];

    i = 0;

    while (i < gridHeight)
    {
        i = i + [self currentSpacing];

        NSPoint startPoint = {0,i};
        NSPoint endPoint = {gridWidth, i};

        [horizontalLinePath setLineWidth:1];
        [horizontalLinePath moveToPoint:startPoint];
        [horizontalLinePath lineToPoint:endPoint];

        [horizontalLinePath stroke];
    }
}

I suspect this is entirely to do with the way that I am drawing the grid and am open to suggestions on how I might better go about it.

I can see where the inefficiency is coming in, drag-resizing the NSWindow is constantly calling the drawRect in this view as it resizes, and the closer the grid, the more calculations per pixel drag of the parent window.

I was thinking of hiding the view on the resize of the window, but it doesn't feel as dynamic. I want the user experience to be very smooth without any perceived delay or flickering.

Does anyone have any ideas on a better or more efficient method to drawing the grid?

All help, as always, very much appreciated.

Hooligancat
  • 3,588
  • 1
  • 37
  • 55

3 Answers3

13

You've inadvertently introduced a Schlemiel into your algorithm. Every time you call moveToPoint and lineToPoint in your loops, you are actually adding more lines to the same path, all of which will be drawn every time you call stroke on that path.

This means that you are drawing one line the first time through, two lines the second time through, three lines the third time, etc...

A quick fix would be to use a new path each time through the loop simply perform the stroke after the loop (with thanks to Jason Coco for the idea):

path = [NSBezierPath path];
while (...)
{
    ...

    [path setLineWidth:1];
    [path moveToPoint:startPoint];  
    [path lineToPoint:endPoint];
}
[path stroke];

Update: Another approach would be to avoid creating that NSBezierPath altogether, and just use the strokeLineFromPoint:toPoint: class method:

[NSBezierPath setDefaultLineWidth:1];
while (...)
{
    ...
    [NSBezierPath strokeLineFromPoint:startPoint toPoint:endPoint];
}

Update #2: I did some basic benchmarking on the approaches so far. I'm using a window sized 800x600 pixels, a grid spacing of ten pixels, and I'm having cocoa redraw the window a thousand times, scaling from 800x600 to 900x700 and back again. Running on my 2GHz Core Duo Intel MacBook, I see the following times:

Original method posted in question:  206.53 seconds  
Calling stroke after the loops:       16.68 seconds  
New path each time through the loop:  16.68 seconds  
Using strokeLineFromPoint:toPoint:    16.68 seconds  

This means that the slowdown was entirely caused by the repetition, and that any of the several micro-improvements do very little to actually speed things up. This shouldn't be much of a surprise, since the actual drawing of pixels on-screen is (almost always) far more processor-intensive than simple loops and mathematical operations.

Lessons to be learned:

  1. Hidden Schlemiels can really slow things down.
  2. Always profile your code before doing unnecessary optimization
e.James
  • 116,942
  • 41
  • 177
  • 214
  • 1
    Since it's stroking the path and not actually generating the path that is inefficient, why not just leave the code the way it is but move the stroke to outside the loop? – Jason Coco Apr 26 '10 at 23:10
  • @Jason Coco: That's a good idea. I'd be curious which of the two methods is faster after that change. – e.James Apr 26 '10 at 23:52
  • I'll try both and let you know... Thanks! Stay tuned for an update – Hooligancat Apr 27 '10 at 00:06
  • @e.James - looks like the path stroke was the culprit. Your alternate solution (using `strokeLineFromPoint:toPoint:`) was marginally, but only marginally, slower than moving the path stroke to outside the loop. For the scale that I am using this works perfectly. Thanks to you and @Jason Coco for your help. – Hooligancat Apr 27 '10 at 00:38
  • You're more than welcome. Great question, by the way. I ran some benchmarks just to see how it would come out. See results in my answer. Cheers! – e.James Apr 27 '10 at 00:40
  • If you want to increase the drawing speed even further, simply using `NSRectFill()` to draw the lines will be even faster than `+strokeLineFromPoint:toPoint:`. – Rob Keniger Apr 27 '10 at 06:15
  • @e.James - thanks for the test results. I think the key here is, as you point out, the hidden repetition. These are the gotchas that one could spend hours trying to figure out. I'm pretty religious about unit testing each individual area before I fully commit it to my application. Imagine having a whole host of drawing going on, doing a resize and then trying to figure out exactly which piece was slowing me down. That would be a nightmare. Thanks again! – Hooligancat Apr 27 '10 at 19:22
  • @Rob Keniger - I will have to give your code a run through as well at some point to see if I can get further gains. At this point the performance is easily acceptable in my app, but I'm always interested in making things more efficient at some point - especially as the app grows and more complex operations add to the performance issues. – Hooligancat Apr 27 '10 at 19:23
0

You should run Instruments Cpu Sampler to determine where most of the time is being spent and then optimized based on that info. If it's the stroke, put it outside the loop. If it's drawing the path, try offloading the rendering to the gpu. See if CALayer can help.

lucius
  • 8,665
  • 3
  • 34
  • 41
0

Maybe to late for the party, however someone could find this helpful. Recently, I needed a custom components for a customer, in order to recreate a grid resizable overlay UIView. The following should to the work, without issues even with very little dimensions.

The code is for iPhone (UIView), but it can be ported to NSView very quickly.

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextClearRect(context, rect);
    CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);

    //corners
    CGContextSetLineWidth(context, 5.0);
    CGContextMoveToPoint(context, 0, 0);
    CGContextAddLineToPoint(context, 15, 0);
    CGContextMoveToPoint(context, 0, 0);
    CGContextAddLineToPoint(context, 0, 15);
    CGContextMoveToPoint(context, rect.size.width, 0);
    CGContextAddLineToPoint(context, rect.size.width-15, 0);
    CGContextMoveToPoint(context, rect.size.width, 0);
    CGContextAddLineToPoint(context, rect.size.width, 15);
    CGContextMoveToPoint(context, 0, rect.size.height);
    CGContextAddLineToPoint(context, 15, rect.size.height);
    CGContextMoveToPoint(context, 0, rect.size.height);
    CGContextAddLineToPoint(context, 0, rect.size.height-15);
    CGContextMoveToPoint(context, rect.size.width, rect.size.height);
    CGContextAddLineToPoint(context, rect.size.width-15, rect.size.height);
    CGContextMoveToPoint(context, rect.size.width, rect.size.height);
    CGContextAddLineToPoint(context, rect.size.width, rect.size.height-15);
    CGContextStrokePath(context);


    //border
    CGFloat correctRatio = 2.0;
    CGContextSetLineWidth(context, correctRatio);
    CGContextAddRect(context, rect);
    CGContextStrokePath(context);

    //grid
    CGContextSetLineWidth(context, 0.5);
    for (int i=0; i<4; i++) {
        //vertical
        CGPoint aPoint = CGPointMake(i*(rect.size.width/4), 0.0);
        CGContextMoveToPoint(context, aPoint.x, aPoint.y);
        CGContextAddLineToPoint(context,aPoint.x, rect.size.height);
        CGContextStrokePath(context);

        //horizontal
        aPoint = CGPointMake(0.0, i*(rect.size.height/4));
        CGContextMoveToPoint(context, aPoint.x, aPoint.y);
        CGContextAddLineToPoint(context,rect.size.width, aPoint.y);
        CGContextStrokePath(context);
    }

}
valvoline
  • 7,737
  • 3
  • 47
  • 52