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 yourAt Blizzard, we render graphics with OpenGL, which is going to be a lot
> 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!"
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:Your analysis of the situation seems good to me :)
>
>> 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.
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


