60 hz NSTimer and autoreleased memory
Asked Answered
K

3

6

I have an NSTimer firing at 60 fps. It updates a C++ model and then draws via Quartz 2D. This works well except memory accumulates quickly even though I am not allocating anything. Instruments reports no leaks but many CFRunLoopTimers (I guess from the repeating NSTimer?) seem to be accumulating. Clicking the window or pressing a key purges most of them which would seem to point to an autorelease pool not being drained frequently enough. Do I have to rely on events to cycle the autorelease pool(s) or is there a better way to clear out the memory?

Any help is appreciated, Thanks

-Sam

Timer creation (timer is an ivar):

timer = [NSTimer scheduledTimerWithTimeInterval:1.0f / 60 target:self selector:@selector(update:) userInfo:nil repeats:YES];

update: method:

- (void)update:(NSTimer *)timer {
    controller->Update();
    [self.view setNeedsDisplay:YES];
}

Update:

After messing around with this a little more I've made a couple of additional observations.

1.) [self.view setNeedsDisplay:YES] seems to be the culprit in spawning these CFRunLoopTimers. Replacing it with [self.view display] gets rid of the issue but at the cost of performance.

2.) Lowering the frequency to 20-30 fps and keeping `[self.view setNeedsDisplay:YES]' also causes the issue to go away.

This would seem to imply setNeedsDisplay: doesn't like to be called a lot (maybe more time's per second then can be displayed?). I frankly can't understand what the problem with "overcalling" it if all it does is tell the view to be redisplayed at the end of the eventloop.

I am sure I am missing something here and any additional help is greatly appreciated.

Kalila answered 13/7, 2011 at 22:38 Comment(1)
Can you post the code where you create the timer and the code for your timer event?Exiguous
B
7

Usually the right solution would be to create a nested NSAutoreleasePool around your object-creations-heavy code.

But in this case, it seems the objects are autoreleased when the timer re-schedule itself — a piece of code you can't control. And you can't ask the topmost autorelease pool to drain itself without releasing it.

In your case, the solution would be to drop your NSTimer for frame-rate syncing, and to use a CADisplayLink instead:

CADisplayLink *frameLink = [CADisplayLink displayLinkWithTarget:self
                                                        selector:@selector(update:)];

// Notify the application at the refresh rate of the display (60 Hz)
frameLink.frameInterval = 1;

[frameLink addToRunLoop:[NSRunLoop mainRunLoop]
                forMode:NSDefaultRunLoopMode];

CADisplayLink is made to synchronize the drawing to the refresh rate of the screen — so it seems like a good candidate for what you want to do. Besides, NSTimer are not precise enough to sync with the display refresh rate when running at 60 Hz.

Beatty answered 15/7, 2011 at 12:36 Comment(1)
Thank you for the suggestion and I think something like CADisplayLink (or CVDisplayLink) would be much more appropriate for what I am doing. However, I am also curious as to why my rudimentary NSTimer method is causing this weird memory issue. I've updated my question with a couple of recent findings regarding the issue.Kalila
M
1

Well, regardless of the memory-cleanup issue:

The documentation for NSTimer says: "Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds."

1/60 is an interval of approx. 16.6 milliseconds so you're well beyond the effective resolution of NSTimer.

Your followup note indicates that lowering its frequency to 20-30 fps fixes it... 20 fps brings the interval to 50 ms -- within the documented resolution.

Documentation also indicates that this shouldn't break anything... however, I've encountered some odd situations were Instruments caused memory issues that weren't previously there. Do you get memory issues/warnings running the app in Release build, without Xcode or Instruments attached?

I guess at this point I'd recommend just moving on and trying out the tools in the other posted answers.

Martini answered 26/7, 2011 at 17:56 Comment(0)
B
0

As already suggested by Kemenaran, I also think that you should try and use a CADisplayLink object. One further reason is that NSTimer have a lower fire limit of 50-100 milliseconds (source):

Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds.

On the other hand, I am not sure that this will solve the problem. From what you describe, it seems to me that thing could work like this (or in a similar way):

  1. when you execute [self.view setNeedsDisplay:YES]; the framework schedule a redraw of the view by means of a CFRunLoopTimer; this explains why there are so many being created;

  2. when the CFRunLoopTimer fires, the view is redrawn, the needsDisplay flag reset;

  3. in your case, when the frequency of update is high, what happens is that you call setNeedsDisplay more often than the refresh can actually happen; so, for each actual refresh you have several calls to setNeedsDisplay, and several CFRunLoopTimer are also created;

  4. of all those CFRunLoopTimer that get created between two consecutive actual refresh operations, only the first one get released and destroyed; the other ones either have no chance to fire or find the view with the needsDisplay flag already reset and thus possibly reschedule themselves.

For point 4: I think that the most likely explication is the first one: you build up a queue of CFRunLoopTimers at a frequency much higher than that at which you can "consume" it. I am saying that redrawing takes longer than an update cycle because you say that when you call [view display] performance suffers.

If this is correct, then the problem would persist also with CADisplayLink (because it is related to calling update with too an high frequency compared to redraw speed) and the only solution would be finding a different way to make the redraw (i.e., not using setNeedsDisplay:YES)

In fact, I checked with cocos2d sources and setNeedsDisplay:YES is almost not used. Redrawing (cocos2d provides a frame rate of 60 fps) is done by drawing directly into an OpenGL buffer, and I suspect that this is the critical point to be able to get to that frame rate. You could also check if you can replace your view layer by a CAEAGLLayer (this should be pretty easy) and see whether you can draw directly to the glBuffer.

I hope it helps. Keep in mind that there are many hypothesis that I am making here, so it could well be that any of it be wrong. I am just proposing my reasoning.

Bedew answered 25/7, 2011 at 17:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.