Is JUCE leaking memory, or am I misinterpreting this?

Juce 7.0.2 - MacOS

I am still working (I’m sorry to say) on 10.14 Mojave with Xcode 10.3, so yes, it’s an older Xcode.

When I run the debugger, in the left side of Xcode there is the Debug Navigator, which shows CPU, Memory etc.

The Memory meter, I have noticed, is constantly going up little by little when my app is running, even if it’s just sitting there idling. If I leave it running overnight, it goes up by a huge amount!

After fooling around with some debugging, it seemed to be related to some LED style widgets I have that blink with the tempo, among other things. If I stop blinking them, it appears the memory usage stays level. I don’t see why…

So I built a simple test GUI App with a single blinking LED. Complete project here:

BlinkLedTest.zip (11.1 KB)

When I Run it from Xcode (Release Build), the Memory is at around 47.5 mb.

blink1

With it just sitting there and blinking, it slowly goes up continuously. An hour later it’s at 58 mb.

I let it run overnight. Now it’s now 225.4 mb!

blink2

This test Application inherits from private juce::HighResolutionTimer. (I use this in my real app, so I used it here). I have a 1 ms clock running. So here, we have:

    void initialise (const juce::String& commandLine) override
    {
        mainWindow.reset (new MainWindow (getApplicationName()));
        HighResolutionTimer::startTimer (1);       // 1 ms clock
    }

    long curTime = 0;
    void hiResTimerCallback() override
    {
        curTime++;
        if (curTime % 500 == 0)     // twice per second
            mainWindow->mainComponent->blinkLed();
    }

So basically, we want to blink an LED twice a second (120 BPM).

The Led and MainComponent class:

class Led : public juce::Component,
            public juce::AsyncUpdater
{
public:
    Led() { }
    
    void paint(juce::Graphics& g) override
    {
        if (isOn)
            g.setColour(juce::Colours::yellow);
        else
            g.setColour(juce::Colours::black);
        g.fillEllipse(getLocalBounds().toFloat());
    }

    void blink()
    {
        isOn = true;
        triggerAsyncUpdate();
        
        juce::Timer::callAfterDelay(200, [this]
                                    {
                                        isOn = false;
                                        triggerAsyncUpdate();
                                    });
    }
    void handleAsyncUpdate() override
    {
        repaint();
    }

private:   
    bool isOn = false;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Led)
};

class MainComponent  : public juce::Component
{
public:
    MainComponent();
    ~MainComponent() override;

    void paint (juce::Graphics&) override;
    void resized() override;

    void blinkLed()
    {
        led.blink();
    }
private:
    Led led;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

So, when we blink() the Led, it sets a variable isOn and triggers an AsyncUpdate which calls repaint(). It also uses Timer::callAfterDelay() to schedule the Led to turn off 200 ms later, with another AsyncUpdate and repaint().

Pretty straightforward (it seems). This isn’t exactly what I’m doing in my real app, but it doesn’t matter - because this simple test project does the same thing!

Am I reading this wrong and it’s nothing to worry about, or is this an indication that something is wrong within JUCE?

I will admit I’m clueless about using Instruments, and when I run it, I don’t know how to interpret the data…

It’s probably the async/callAfterDelay stuff doing small memory allocations that gradually pile up. Even if Juce releases the memory involved correctly, the runtime/OS might end up increasing the process memory use over time. (Due to heap fragmentation, optimizations etc)

I am not sure what you think are gaining by using the HighResolutionTimer here, since you mess up the timing in any case with the async stuff you end up calling.

I used a HighResolutionTimer in the example because I use one in my main app for a 1 ms scheduler. The 1 ms scheduler loop executes some deferred drawing operations. I realize this little test app may not make entire sense, but I wanted to call the led flash somewhat similarly to how I was doing it in the real app, where it makes more sense. I don’t use Timer::callAfterDelay() to turn off the led in my main app; it is done by the 1 ms scheduler loop.

But in any case, should the memory just keep going up like that?

IIRC triggerAsyncUpdate has the same issue that a small heap allocation may be involved, and might cause growing memory use over time. Or are you doing the LED repaints differently in the real app?

If I make handleAsyncUpdate an empty function that does nothing (comment out the call to repaint()), then the memory use does not increase. It stays level, even though the timer is still firing the AsyncUpdates.

That seems to indicate that the AsyncUpdater, or Timer::callAfterDelay are not the problem. They are still being called repeatedly and the memory stays level.

If I leave the repaint() call in, but comment out the guts of the paint() function so that it does nothing,
the memory use continues to go up.

It really seems like the repeated calls to repaint(), and the whole JUCE heirarchy of paint() calls that results, is what is causing the memory to increase. Not anything that I am doing here.

repaint() is also an async thing that might grow some kind of buffer somewhere when called often repeatedly. So smells a bit like a Juce issue, but might also be something that happens because of the operating system.

OK, this is an even simpler example.

All this does is use a Timer to call a repaint() on the main component every 10ms.

The memory use goes steadily up and up!

RepaintTest.zip (11.1 KB)

MainComponent::MainComponent()
{
    setSize (600, 400);
    startTimer(10);
}

void MainComponent::timerCallback()
{
    repaint();
}

How much in what time does the memory use go up? Does anything change if you remove the setFont call from the paint method?

This does look like an issue that the Juce team should investigate.

Does anything change if you remove the setFont call from the paint method?

I tried commenting out the contents of paint() completely.

void MainComponent::paint (juce::Graphics& g)
{
    /*
    // (Our component is opaque, so we must completely fill the background with a solid colour)
    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));

    g.setFont (juce::Font (16.0f));
    g.setColour (juce::Colours::white);
    g.drawText ("Hello World!", getLocalBounds(), juce::Justification::centred, true);
     */
}

The memory still increases, slowly up and up.

start - 45.1 mb. 10 minutes later, 59.0 mb.
40 minutes later 81.3 mb…

It’s a shot in the dark, but be careful not to enqueue asynchronous calls faster than they than can be processed, otherwise you risk internal fifo buffers getting larger and larger. This could be what’s happening here.

1ms is a pretty high rate and causes a lot of load for somthing simple like turning a led on and off, this is probably not the best approach.

As mentioned previously, ‘repaint’ marks that section of the component as dirty, and at some point, the OS will fire a draw call. You’re likely triggering it faster than it’s flushing, hence the slowly increasing memory usage.

Typically you would update the UI at a fixed rate (which is probably what the OS is doing anyway) and do your painting at a specified rate.

I would expect the memory usage to reduce once you stopped the draw calls, but the OS might not bother to reclaim that memory because of past use (rapid allocation of small blocks).

Please forget my first post with the 1 ms HighResTimer; this overly complicated my example and had no bearing on the issue.

The second example HERE simply has a repaint firing every 10 ms, and that repaint is doing nothing.

How else do people update meters and waveform displays if not by calling repaints constantly?

Does it make sense that a single repaint, called every 10 ms, that isn’t even painting anything, would cause the memory to gradually constantly increase?

Again, here is the very simple test project:
RepaintTest.zip (11.1 KB)

The memory meter is known to be not accurate.

Instead try your XCode’s tool Instruments → Leaks.
If there is memory leaked, this tool should be able to tell you. (Although I haven’t used it myself yet).

https://help.apple.com/instruments/mac/current/#/dev022f987b

1 Like

10ms rate is 100 refreshes a second, and 99% of devices refresh natively at 60fps. Those 40 will eventually get binned, but they are potentially queued up like all the others.

You can update your internal VU meter values at whatever rate you like, but you should generally be limiting your rendering to a common fixed rate.

My mac decided now is the best time to update so I can’t instrument at the moment. Do you still get leaks if you choose 16ms repaint?

Yes. I just ran it again like this:

MainComponent::MainComponent()
{
    setSize (600, 400);
    startTimerHz(60);
}

void MainComponent::timerCallback()
{
    repaint();
}
void MainComponent::paint (juce::Graphics& g)
{
    // nothing
}

Memory still increases gradually, constantly.

Here’s a little 30 second GIF, just for the heck of it. If I leave it overnight, it goes up to hundreds of mb. All while doing essentially nothing.

Untitled4

The memory usage in XCode is famously incorrect but I will instrument it when my mac is up and running.

1 Like

As it was already said by @daniel and @oli1, you shouldn’t trust the memory usage as shown by Xcode.

Create a release build and monitor its memory usage with e.g. ‘Activity Monitor’. Is the memory usage still increasing?

OK, I did that. the answer is: YES, it still increases. You can try it yourself (in fact I wish someone would, then I would not feel like I’m nuts).

I created a Release build of this project:
Timer at 60 Hz, calling a single empty repaint on the MainComponent.
RepaintTest.zip (11.2 KB)

Or, here’s the compiled .app:
RepaintTest.app.zip (768.1 KB)

When first launched (directly, not in Xcode), Activity Monitor shows 37.2 Mb.

3 hours later, it shows 112.3 mb. This tiny little app doing nothing. :face_with_raised_eyebrow:

If I’m not mistaken, you should be able to find the allocations with Instruments. Even if JUCE doesn’t technically end up with a Leak, you should still be able to get a pretty good understanding of what is going on.

The internal FiFos getting sized up seems a pretty decent explanation to me.

I have similar issue on macOS 12.6.3
Run same test on macOS 13 and macOS 14 and Xcode instruments detect no leaks
Both tests made with JUCE v7.0.7 with the same source code