NSView -drawRect optimization

  • Hi-

    I have just deployed my first Cocoa app which is a scheduling program
    used by one user in my company. I'm very proud of it as a first app
    but I want to make it a little better.

    It has a subclass of NSView where I draw all of the "orderSteps"
    which are the pieces of work that are required to complete all the
    orders on our shop floor. Here is a screenshot of the view in its
    window:
    http://special-lite.com/satellite/scheduleWindow.png

    The user clicks and drags all those little rectangles around in order
    to optimize the production. They "bump" each other out of the way.
    They limit each others' motion when there is no more room to bump,
    etc. There are 226 rectangles on the screenshot, but during busy
    times there might be 500-700.

    My only concern with it is the frames per second draw speed of the
    view (it's all done in -drawRect). I have done pretty good testing to
    remove individual segments of code from the loop in order to see what
    is taking the time:

    - As developed, it draws at about 5.9 frames per second on my 2.66
    ghz Mac Pro (user is on a 24" iMac, almost as fast)
    - Removing the orderStep draw loop completely gives me about 25 fps

    Here is my loop that draws each orderStep on the view. This loop gets
    called once for each workCenter that you see on the left side of the
    view.

    http://objc.pastebin.com/m4fe1a694

    In the code are comments that tell how many fps were gained for each
    fragment of code that I commented out. All the fragments do not add
    up to the 19fps difference so I can assume that my math is horrible
    or that the addition of each fragment makes other things take longer
    in some way. One thing I know for sure is that -addToolTipRect takes
    forever!

    If anyone likes looking at newbie code for stupid things, now is your
    chance. I'll happily take any tips, comments, or even derision. My
    biggest fear is to hear "There's nothing you can do about it--stuff
    takes time" to which I would respond, "Then how does Blizzard do it!?
    I know they are drawing more than 226 rectangles!"

    Thank you
  • Paul Bruneau wrote:
    > If anyone likes looking at newbie code for stupid things, now is your
    > chance. I'll happily take any tips, comments, or even derision. My
    > biggest fear is to hear "There's nothing you can do about it--stuff
    > takes time" to which I would respond, "Then how does Blizzard do it!?
    > I know they are drawing more than 226 rectangles!"
    At Blizzard, we render graphics with OpenGL, which is going to be a lot
    faster than anything you can do on the CPU :)
    I haven't looked at your code, but have you experimented with
    CoreAnimation at all? This would let you leverage the GPU for your
    drawing as well, which might help.
  • On Feb 12, 2008, at 2:02 PM, Paul Bruneau wrote:

    >
    > My only concern with it is the frames per second draw speed of the
    > view (it's all done in -drawRect). I have done pretty good testing
    > to remove individual segments of code from the loop in order to see
    > what is taking the time:

    Hi Paul,

    It's not clear how you are measuring the fps, but a common cause of
    slow drawing is that your app triggers immediate rendering too often,
    which will be throttled by coalesced updates.  See http://
    developer.apple.com/technotes/tn2005/tn2133.html for more information.

    To determine if you are being bitten by this, launch Quartz Debug and
    turn on Disable Beam Synchronization under the Tools menu.  If your
    fps improves substantially, that's the culprit.

    -Peter
  • Hi Paul,

    Here are a couple of suggestions:

    1) Create an OrderStepCell class, which represents the visible display
    of a single OrderStep. Have it have a pointer to the OrderStep and the
    rect that it draws in (i.e. it caches your OSRect value).

    2) Only update that drawing rect for the cell when the OrderStep is
    moved. Or update them all when your scale changes (the
    pixelsPerSecond) or when enough time passes (1.0 / pixelsPerSecond = #
    of seconds before the display changes by a single pixel). Use an
    NSTimer for the latter.

    3) When a cell moves, scale changes, or time passes, call -
    updateTrackingAreas. Implement -updateTrackingAreas to
    removeAllTooltips, and then create a tooltip for each order step.
    Cocoa will also call -updateTrackingAreas whenever the view's geometry
    changes such that tracking areas / cursor rects / tooltips need to be
    updated. See the NSView documentation.

    4) As a result of these changes, you should be able to remove the
    computation of off-line time and the tooltip creation from your
    drawing loop. You also shouldn't need to constantly redisplay - only
    do it when your timer goes off or when order steps are being moved.

    In short, most of your slowness here isn't really the drawing, it is
    that you are doing a lot of computation during drawing that ought to
    be done much less often.

    Hope this helps!
    - Greg
  • Hi Paul,

    I think there's plenty you can do to optimize drawing here.

    I haven't analysed your code to the last line, but it looks as if you
    are drawing every rect regardless. The fastest drawing is none at
    all, so the first thing would be to only draw what needs to be drawn.
    You can check this in a variety of ways. The simplest is to check if
    the rect to be drawn intersects the <rect> parameter to -drawRect: -
    if not, skip it. (Use the NSIntersectsRect utility).

    For this to help, you also only need to call for a refresh only of
    those rects that have actually changed when you drag one. Instead of
    calling a -setNeedsDisplay: on the view, call setNeedDisplayInrect:
    for each rect that moves. (This part of the code you haven't posted
    so you might be doing this already).

    The simple intersection test might not gain you all that much if a
    lot of rects move at a time, since Cocoa will form the union of all
    those rects and pass that to -drawRect:, resulting in refreshing more
    than necessary. So the next step is to test your rects using -
    needsToDrawRect: which is more fine-grained and will eliminate all
    rects that haven't changed at all from being drawn.

    If setting the tooltip rects is slow, don't do it unless you have to.
    The tooltip rects for all static rects doesn't need to change at all,
    and for the rest only when the mouse is released, so maybe you could
    move this step to the mouseUp: method. Keep track of the rects that
    you changed when dragging, then update this list's tooltips on mouse
    up. Keep this step out of the drawing loop altogether. I think this
    will work since presumably tooltips don't need to be displayed while
    dragging, only when you hover over an item - so the stale tooltip
    rects during a drag won't affect you.

    These steps should give you a dramatic boost in performance. If not,
    we can think again.

    -------
    S.O.S.

    On 13/02/2008, at 9:02 AM, Paul Bruneau wrote:

    > Hi-
    >
    > I have just deployed my first Cocoa app which is a scheduling
    > program used by one user in my company. I'm very proud of it as a
    > first app but I want to make it a little better.
    >
    > It has a subclass of NSView where I draw all of the "orderSteps"
    > which are the pieces of work that are required to complete all the
    > orders on our shop floor. Here is a screenshot of the view in its
    > window:
    > http://special-lite.com/satellite/scheduleWindow.png
    >
    > The user clicks and drags all those little rectangles around in
    > order to optimize the production. They "bump" each other out of the
    > way. They limit each others' motion when there is no more room to
    > bump, etc. There are 226 rectangles on the screenshot, but during
    > busy times there might be 500-700.
    >
    > My only concern with it is the frames per second draw speed of the
    > view (it's all done in -drawRect). I have done pretty good testing
    > to remove individual segments of code from the loop in order to see
    > what is taking the time:
    >
    > - As developed, it draws at about 5.9 frames per second on my 2.66
    > ghz Mac Pro (user is on a 24" iMac, almost as fast)
    > - Removing the orderStep draw loop completely gives me about 25 fps
    >
    > Here is my loop that draws each orderStep on the view. This loop
    > gets called once for each workCenter that you see on the left side
    > of the view.
    >
    > http://objc.pastebin.com/m4fe1a694
    >
    > In the code are comments that tell how many fps were gained for
    > each fragment of code that I commented out. All the fragments do
    > not add up to the 19fps difference so I can assume that my math is
    > horrible or that the addition of each fragment makes other things
    > take longer in some way. One thing I know for sure is that -
    > addToolTipRect takes forever!
    >
    > If anyone likes looking at newbie code for stupid things, now is
    > your chance. I'll happily take any tips, comments, or even
    > derision. My biggest fear is to hear "There's nothing you can do
    > about it--stuff takes time" to which I would respond, "Then how
    > does Blizzard do it!? I know they are drawing more than 226
    > rectangles!"
    >
    > Thank you
  • Your method of determining where drawing time is being spent is
    probably effective, but it's way too much work.  Just use Shark or
    Sampler along with Quartz Debug.  Apple provides some of the best
    profiling tools anywhere and they're free.

    You are drawing way too much each time you draw.  You only need to
    draw graphics that fall within the rect passed to -drawRect:, and you
    can narrow it down even farther using - (void)getRectsBeingDrawn:
    (const NSRect **)rects count:(NSInteger *)count.  Not drawing things
    that don't need to be drawn is the biggest optimization you will get.
    Why don't OrderStep instances draw themselves ?  If you are worried
    about adding drawing code to a "Model" object, add the drawing code in
    a category of OrderStep and maintain the category implementation in
    the "View" subsystem.  For example:

    @implementation OrderStep (ScheduleDrawing)

    - (void)drawInRect:(NSRect)aRect ofTimelineView:(id)someView
    {
            NSTimeInterval timeIntervalToStart = [[self startTime]
    timeIntervalSinceDate:now] - [[[someView window] delegate]
    timeOfflineBetweenNowAndDate:(NSCalendarDate *)[self startTime]];
            NSTimeInterval timeIntervalToEnd = [[self endTime]
    timeIntervalSinceDate:now] - timeOfflineBetweenNowAndDate;

            NSRect OSRect = [someView rectForStartTimeInterval:
    timeIntervalToStart endTimeInterval: [[[someView window] delegate]
    timeOfflineBetweenNowAndDate:(NSCalendarDate *)[self endTime]]];

            if(NSIntersectsRect(OSRect, aRect)
            {
                  // do all the drawing with NSBezierPath etc.
            }
    }

    @end

    inside the timeline view:

    -drawRect:(NSRect)rect
    {
          NSRect        *dirtyRects;
          NSInteger        countOfDirtyRects;

        [self getRectsBeingDrawn: &dirtyRects count:&countOfDirtyRects];
        NSInteger    i;
        for(i = countOfDirtyRects - 1; i >= 0; i--)
        {
            int          OSIndex;
            for (OSIndex = 0; OSIndex < [[currentWorkCenter orderSteps]
    count]; OSIndex ++)
            {
                  OrderStep * currentOS = [[currentWorkCenter orderSteps]
    objectAtIndex:OSIndex];

                [currentOS drawInRect: *(dirtyRects[i])
    ofTimelineView:self];
            }
        }
    }

    Then, be very careful to call -setNeedsDisplayInRect: with the
    smallest possible rectangles instead of calling -setNeedsDisplay or
    just -display.

    Finally, you are recalculating a lot of state information every time
    you draw.  As a guideline, the only code called from -drawRect: should
    be drawing code.  It should be possible to calculate the position of
    each order step once and re-use the value as needed.  Translate the
    coordinate system of the timeline view to indicate changing time
    instead of recalculating all of the order step rectangles.  In other
    words, the rectangles are constant but the view's coordinate system
    changes.

    You may find introductory graphics text books useful.  Many people
    recommend "Computer Graphics: Principles and Practice in C (2nd
    Edition)"  by James D. Foley, Andries van Dam, Steven K. Feiner, John
    F. Hughes

    In the name of avoiding drawing, you can use Core Animation Layer
    Backed Views or even use OpenGL directly and potentially see 100x
    speed-up over basic Quartz.  CoreAnimation will cache the view drawing
    in a GL texture so that in most cases your -drawRect: will be called
    exactly once and the bitmap will be reused after that.  Caching the
    drawing in a texture will eliminate calls to draw the P-number etc. so
    they will end up taking zero time.  To use Layer Backed Views, each of
    your Order Steps should be represented by a separate subview of the
    timeline view.

    I will be surprised if you can't make you drawing 100x faster, and
    1000x faster is within the realm of possibility.

    There is a lengthy chapter on graphics optimization in "Cocoa
    Programming"  http://www.cocoaprogramming.net/  You can download the
    examples including a Tetris game without buying the book.  The
    optimization examples are in Appendix B: Optimizing and Finding Memory
    Leaks.
  • On Feb 12, 2008, at 5:17 PM, John Stiles wrote:

    > Paul Bruneau wrote:
    >> If anyone likes looking at newbie code for stupid things, now is
    >> your chance. I'll happily take any tips, comments, or even
    >> derision. My biggest fear is to hear "There's nothing you can do
    >> about it--stuff takes time" to which I would respond, "Then how
    >> does Blizzard do it!? I know they are drawing more than 226
    >> rectangles!"

    > At Blizzard, we render graphics with OpenGL, which is going to be a
    > lot faster than anything you can do on the CPU :)
    > I haven't looked at your code, but have you experimented with
    > CoreAnimation at all? This would let you leverage the GPU for your
    > drawing as well, which might help.

    Thank you John, I knew there had to be some kind of magic involved. I
    have not experimented with CoreAnimation yet as I have heeded mmalc's
    warnings about not jumping in too early with advanced Cocoa topics :)
    At this point, I think I will be able to gain a lot of performance
    with smarter choices about when to draw different parts of my view
    but if I am still stuck after that I will look at CA. Thanks again
    for your kind reply.
  • On Feb 12, 2008, at 5:31 PM, Peter Ammon wrote:

    > On Feb 12, 2008, at 2:02 PM, Paul Bruneau wrote:
    >
    >> My only concern with it is the frames per second draw speed of the
    >> view (it's all done in -drawRect). I have done pretty good testing
    >> to remove individual segments of code from the loop in order to
    >> see what is taking the time:
    >
    > Hi Paul,
    >
    > It's not clear how you are measuring the fps, but a common cause of
    > slow drawing is that your app triggers immediate rendering too
    > often, which will be throttled by coalesced updates.  See http://
    > developer.apple.com/technotes/tn2005/tn2133.html for more information.
    >
    > To determine if you are being bitten by this, launch Quartz Debug
    > and turn on Disable Beam Synchronization under the Tools menu.  If
    > your fps improves substantially, that's the culprit.

    Thanks for your reply, Peter (and thanks to the others who replied).
    I'm out of my office today but I will respond to some more tomorrow.

    I am measuring fps by storing the time at the start of -drawRect and
    then at the end of -drawRect I display 1/elapsedTime in the lower
    left corner of the view (you can see it in my screenshot). It varies
    a little as I scroll or move orderSteps but I get a good idea from it
    I think.

    I have read the technote you linked but I am not sure if it affects
    me. I suspect not since all my drawing is in -drawRect and I never
    explicitly call it myself but rely on the OS to call it for me. I
    just call -setNeedsDisplay on the view when my program changes
    something that requires the view to be redrawn. If I am way off-base,
    please let me know.

    I will disable beam synchro to see if it affects me. Thank you.
  • Paul Bruneau wrote:
    > On Feb 12, 2008, at 5:17 PM, John Stiles wrote:
    >
    >> Paul Bruneau wrote:
    >>> If anyone likes looking at newbie code for stupid things, now is
    >>> your chance. I'll happily take any tips, comments, or even derision.
    >>> My biggest fear is to hear "There's nothing you can do about
    >>> it--stuff takes time" to which I would respond, "Then how does
    >>> Blizzard do it!? I know they are drawing more than 226 rectangles!"
    >
    >> At Blizzard, we render graphics with OpenGL, which is going to be a
    >> lot faster than anything you can do on the CPU :)
    >> I haven't looked at your code, but have you experimented with
    >> CoreAnimation at all? This would let you leverage the GPU for your
    >> drawing as well, which might help.
    >
    > Thank you John, I knew there had to be some kind of magic involved. I
    > have not experimented with CoreAnimation yet as I have heeded mmalc's
    > warnings about not jumping in too early with advanced Cocoa topics :)
    > At this point, I think I will be able to gain a lot of performance
    > with smarter choices about when to draw different parts of my view but
    > if I am still stuck after that I will look at CA. Thanks again for
    > your kind reply.
    Your analysis of the situation seems good to me :)
    I would suggest, though, that CoreAnimation is pretty easy to grasp. If
    you've got some spare time, check out the demo/example code and I bet
    you will be able to understand it.
  • On Feb 12, 2008, at 5:39 PM, Greg Titus wrote:

    > Hi Paul,
    >
    > Here are a couple of suggestions:
    >
    > 1) Create an OrderStepCell class, which represents the visible
    > display of a single OrderStep. Have it have a pointer to the
    > OrderStep and the rect that it draws in (i.e. it caches your OSRect
    > value).
    >
    > 2) Only update that drawing rect for the cell when the OrderStep is
    > moved. Or update them all when your scale changes (the
    > pixelsPerSecond) or when enough time passes (1.0 / pixelsPerSecond
    > = # of seconds before the display changes by a single pixel). Use
    > an NSTimer for the latter.
    >
    > 3) When a cell moves, scale changes, or time passes, call -
    > updateTrackingAreas. Implement -updateTrackingAreas to
    > removeAllTooltips, and then create a tooltip for each order step.
    > Cocoa will also call -updateTrackingAreas whenever the view's
    > geometry changes such that tracking areas / cursor rects / tooltips
    > need to be updated. See the NSView documentation.
    >
    > 4) As a result of these changes, you should be able to remove the
    > computation of off-line time and the tooltip creation from your
    > drawing loop. You also shouldn't need to constantly redisplay -
    > only do it when your timer goes off or when order steps are being
    > moved.
    >
    > In short, most of your slowness here isn't really the drawing, it
    > is that you are doing a lot of computation during drawing that
    > ought to be done much less often.
    >
    > Hope this helps!

    Thank you, Greg, that is all Very Good Stuff™. I can see the wisdom
    of getting my computations out of drawRect. I will also go learn
    about -updateTrackingAreas.
  • On Feb 12, 2008, at 5:49 PM, Graham wrote:

    > Hi Paul,
    >
    > I think there's plenty you can do to optimize drawing here.
    >
    > I haven't analysed your code to the last line, but it looks as if
    > you are drawing every rect regardless. The fastest drawing is none
    > at all, so the first thing would be to only draw what needs to be
    > drawn. You can check this in a variety of ways. The simplest is to
    > check if the rect to be drawn intersects the <rect> parameter to -
    > drawRect: - if not, skip it. (Use the NSIntersectsRect utility).
    >
    > For this to help, you also only need to call for a refresh only of
    > those rects that have actually changed when you drag one. Instead
    > of calling a -setNeedsDisplay: on the view, call
    > setNeedDisplayInrect: for each rect that moves. (This part of the
    > code you haven't posted so you might be doing this already).
    >
    > The simple intersection test might not gain you all that much if a
    > lot of rects move at a time, since Cocoa will form the union of all
    > those rects and pass that to -drawRect:, resulting in refreshing
    > more than necessary. So the next step is to test your rects using -
    > needsToDrawRect: which is more fine-grained and will eliminate all
    > rects that haven't changed at all from being drawn.
    >
    > If setting the tooltip rects is slow, don't do it unless you have
    > to. The tooltip rects for all static rects doesn't need to change
    > at all, and for the rest only when the mouse is released, so maybe
    > you could move this step to the mouseUp: method. Keep track of the
    > rects that you changed when dragging, then update this list's
    > tooltips on mouse up. Keep this step out of the drawing loop
    > altogether. I think this will work since presumably tooltips don't
    > need to be displayed while dragging, only when you hover over an
    > item - so the stale tooltip rects during a drag won't affect you.
    >
    > These steps should give you a dramatic boost in performance. If
    > not, we can think again.

    Thank you, Graham, for the very nice response. I am going to
    implement code to be smarter about which rects need to be drawn per
    your and others' suggestions. I also really like your idea about
    reducing the tooltip rect setting.
  • On Feb 12, 2008, at 6:51 PM, Erik Buck wrote:

    > You are drawing way too much each time you draw.  You only need to
    > draw graphics that fall within the rect passed to -drawRect:, and
    > you can narrow it down even farther using - (void)
    > getRectsBeingDrawn:(const NSRect **)rects count:(NSInteger *)
    > count.  Not drawing things that don't need to be drawn is the
    > biggest optimization you will get.  (snip)

    Thank you Erik for an amazing reply. I will reply to some of your
    questions even though they might be rhetorical :) In addition I will
    be learning about everything that you told me and thanks again for
    your reply.

    > Your method of determining where drawing time is being spent is
    > probably effective, but it's way too much work.  Just use Shark or
    > Sampler along with Quartz Debug.  Apple provides some of the best
    > profiling tools anywhere and they're free.

    I know you are right and I tried Shark but I could not get an idea of
    where in my code the time was being spent. I need to learn more about
    these tools, for sure.

    > Why don't OrderStep instances draw themselves ?

    The best answer is "Because I have spent 20 years or so coding in
    Procedural Land and only 6 months in Cocoa Land!" :) I just saw -
    drawRect and said to myself "here is where I draw my view" but I do
    know from what I have read that I am not thinking "objectively"
    enough so I will implement something like the code you graciously
    wrote for me.

    > I will be surprised if you can't make you drawing 100x faster, and
    > 1000x faster is within the realm of possibility.

    10x would be great :)

    > There is a lengthy chapter on graphics optimization in "Cocoa
    > Programming"  http://www.cocoaprogramming.net/  You can download
    > the examples including a Tetris game without buying the book.  The
    > optimization examples are in Appendix B: Optimizing and Finding
    > Memory Leaks.

    I'm going to go read that chapter right now. I recently bought the
    digital version of your book because you didn't print enough paper
    ones :) but I haven't read it yet.
  • On Tue, Feb 12, 2008 at 5:02 PM, Paul Bruneau
    <paul_bruneau...> wrote:
    > I have just deployed my first Cocoa app which is a scheduling program
    > used by one user in my company. I'm very proud of it as a first app
    > but I want to make it a little better.

    OT, but it's very nice to see internal apps like these on the Mac.
    Makes me warm and fuzzy inside, and provides great examples for Win32
    programmers of what's possible using Cocoa.

    Good luck,
    --Kyle Sluder
previous month february 2008 next month
MTWTFSS
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29    
Go to today
MindNode
MindNode offered a free license !