Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

60 hz NSTimer and autoreleased memory

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.

like image 538
Sam Avatar asked Jul 13 '11 22:07

Sam


2 Answers

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.

like image 150
Kemenaran Avatar answered Sep 28 '22 06:09

Kemenaran


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.

like image 32
MechEthan Avatar answered Sep 28 '22 07:09

MechEthan