‘Discrete’ NSLevelIndicatorCell too slow

I recently needed to embed the ‘Discrete’ variant of the NSLevelIndicatorCell into an NSTableView. The discrete cell looks like this:

Discrete.png Unfortunately adding the cell caused drawing of the table’s window to slow to crawl. The spinning beachball of death would often appear when I was merely resizing the window. After some quick experimented I narrowed it down to something within the ‘Discrete’ variant of the NSLevelIndicatorCell, the other variants had no such performance problems.

It didn’t take an expert in Shark to realise what was going on:

It looked like NSLevelIndicatorCell was perform an expensive gaussian blur per cell, possibly even for each discrete block within each cell. For a single NSLevelIndicatorCell that shouldn’t be a real problem, but for a whole table column full of them it could create major performance problems.

Some quick google searches showed that people had complained about poor NSLevelIndicatorCell performance before but failed to find a solution.

Switching to another variant of NSLevelIndicatorCell wasn’t an option, the ‘Discrete’ variant was just too sexy. So the obvious solution was to create a subclass of NSLevelIndicatorCell that posed as the real thing, but cached the drawing code of its parent class into NSImages. So I present to the the world the imaginatively titled CCachingLevelIndicatorCell.

CCachingLevelIndicatorCell is designed to pose as NSLevelIndicatorCell but shouldn’t require any other configuration. The class will only cache images for the ‘Discrete’ variant and will use a dictionary of NSImage objects to cache the drawing (one image per discrete value the cell can display). If any other attribute of the cell is changed the class will discard the cache and attempt to recache the drawing at the next opportunity.

Just so you can compare the differences in speed I’ve created two stand-alone test applications so you can compare the speed without CCachingLevelIndicatorCell and with it posing as NSLevelIndicatorCell. The binaries are checked into my public subversion.

Radar bug: rdar://problem/4601201

This entry was posted in Default and tagged , , , , . Bookmark the permalink.
  • Hey Ken,

    I took out my code and replaced it with your simple clipping fix.

    It certainly improved performance over the non-posed version but I think the caching version is still the fastest. Which is a shame, as your code is certainly cleaner and simpler.

    You can compare all three versions here: http://toxic-public.googlecode.com//trunk/Misc/CachingLevelIndicator/Output/
  • ken
    I think I recognize this one..

    Try writing a subclass with this as its only implementation:

    - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
    [NSGraphicsContext saveGraphicsState];
    NSRectClip(cellFrame);
    [super drawWithFrame:cellFrame inView:controlView];
    [NSGraphicsContext restoreGraphicsState];
    }

    The issue is with CG transparency layers (see CGContextBeginTransparencyLayer). They're a nice, clean way to a bunch of drawing, then put a shadow or a focus ring on the whole thing. Problem is, when you composite a transparency layer, it'll do calculations over the whole clip. It happens that when a cell is drawn in a tableview, the only clip is to the whole table.

    That's, uh, bad.
  • Anything I post in my public subversion repository is public domain unless stated overwise. Do with it what you will.

    I should make an exception for you though Chris… How about commercial - $1,000,000 per seat?
  • License on your CCachingLevelIndicatorCell?
  • Thanks for the support. It shouldn't be too difficult to fix I'd imagine.

    Awesome treehouse(s) by the way.
  • You rock for logging this bug AND putting the link into your page. More people need to do this; it will help issues like this get fixed. thanks!
blog comments powered by Disqus