Typeface::createSystemTypefaceFor Leaks on Mac

#1

I’m seeing what are apparently memory leaks with Typeface::createSystemTypefaceFor() on specifically Mac.

We were encountering behavior where scrolling through about a hundred presets and resetting to our default empty preset would increase baseline RAM usage by 20MB (instead of the initial RAM usage). This behavior wasn’t appearing on Windows.

After opening up Instruments>Allocations and looking at the heap, we were seeing a lot of allocated memory that was created per-preset by Typeface::createSystemTypefaceFor(). We’re not creating/destroying new Fonts per preset… it’s simply reading one Font member in our LookAndFeel that gets created at initialization. We are creating/destroying Labels with our module system.

If I replace

return Font(Typeface::createSystemTypefaceFor(fontData, fontDataSize));

with

return Font()

then resetting to our default preset shows no increase in size.

0 Likes

#2

Are you using an older version of JUCE? I saw the same sort of rampant allocations by Typeface::createSystemTypefaceFor()… I was able to cherry-pick a fix from the repo.
See this thread: [Solved] Anyone else? Rampant memory use in El Capitan and higher

1 Like

#3

Thanks for the heads up! Unfortunately, no, we are on a commit very close to the latest master. However, that seems very related. Hmm…

0 Likes

#4

Easy to reproduce: https://github.com/FigBug/juce_bugs/tree/master/CrazyFonts

Not easy to fix. Maybe leak inside OS? I can’t find anything wrong with the juce code.

1 Like

#5

Interestingly enough, my TestApp doesn’t get much above about 3 GB memory usage. Once it gets that high it drops back down to about 2 GB.

0 Likes

#6

This seems to drastically reduce the memory leak but not fix it. Stackoverflow says there is a memory leak in CTFontCreateWithGraphicsFont. https://stackoverflow.com/questions/40805382/how-to-avoid-memory-leak-with-ctfontcreatewithgraphicsfont

diff --git a/modules/juce_graphics/native/juce_mac_Fonts.mm b/modules/juce_graphics/native/juce_mac_Fonts.mm
index 39fdc4ec3..ec60893c5 100644
--- a/modules/juce_graphics/native/juce_mac_Fonts.mm
+++ b/modules/juce_graphics/native/juce_mac_Fonts.mm
@@ -474,7 +474,7 @@ static void createLayout (TextLayout& glyphLayout, const AttributedString& text)
         ctFontRef = CoreTextTypeLayout::createCTFont (font, referenceFontSize, renderingTransform);
 
         if (ctFontRef != nullptr)
-        {
+        {
             fontRef = CTFontCopyGraphicsFont (ctFontRef, nullptr);
             initialiseMetrics();
         }
@@ -487,30 +487,32 @@ static void createLayout (TextLayout& glyphLayout, const AttributedString& text)
         // so copy the data manually and use CFDataCreateWithBytesNoCopy
         auto cfData = CFDataCreateWithBytesNoCopy (kCFAllocatorDefault, (const UInt8*) dataCopy.getData(),
                                                    (CFIndex) dataCopy.getSize(), kCFAllocatorNull);
-        auto provider = CGDataProviderCreateWithCFData (cfData);
-        CFRelease (cfData);
 
        #if JUCE_IOS
         // Workaround for a an obscure iOS bug which can cause the app to dead-lock
         // when loading custom type faces. See: http://www.openradar.me/18778790 and
         // http://stackoverflow.com/questions/40242370/app-hangs-in-simulator
         [UIFont systemFontOfSize: 12];
-       #endif
-
-        fontRef = CGFontCreateWithDataProvider (provider);
-        CGDataProviderRelease (provider);
+       #endif
+        
+        auto desc = CTFontManagerCreateFontDescriptorFromData (cfData);
+        CFRelease (cfData);
 
-        if (fontRef != nullptr)
+        if (desc != nullptr)
         {
-           #if JUCE_MAC && defined (MAC_OS_X_VERSION_10_8) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8
-            if (SystemStats::getOperatingSystemType() >= SystemStats::OperatingSystemType::MacOSX_10_11)
-                canBeUsedForLayout = CTFontManagerRegisterGraphicsFont (fontRef, nullptr);
-           #endif
-
-            ctFontRef = CTFontCreateWithGraphicsFont (fontRef, referenceFontSize, nullptr, nullptr);
+            ctFontRef = CTFontCreateWithFontDescriptor (desc, referenceFontSize, nullptr);
+            CFRelease (desc);
 
             if (ctFontRef != nullptr)
-            {
+            {
+                fontRef = CTFontCopyGraphicsFont (ctFontRef, nullptr);
+                
+               #if JUCE_MAC && defined (MAC_OS_X_VERSION_10_8) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8
+                if (SystemStats::getOperatingSystemType() >= SystemStats::OperatingSystemType::MacOSX_10_11)
+                    canBeUsedForLayout = CTFontManagerRegisterGraphicsFont (fontRef, nullptr);
+                registered = true;
+               #endif
+
                 if (auto fontName = CTFontCopyName (ctFontRef, kCTFontFamilyNameKey))
                 {
                     name = String::fromCFString (fontName);
@@ -654,7 +656,7 @@ bool getOutlineForGlyph (int glyphNumber, Path& path) override
     float fontHeightToPointsFactor = 1.0f;
     CGAffineTransform renderingTransform = CGAffineTransformIdentity;
 
-    bool canBeUsedForLayout;
+    bool canBeUsedForLayout = false, registered = false;
 
 private:
     MemoryBlock dataCopy;
@@ -687,16 +689,17 @@ static void pathApplier (void* info, const CGPathElement* element)
 OSXTypeface::~OSXTypeface()
 {
     if (attributedStringAtts != nullptr)
-        CFRelease (attributedStringAtts);
-
-    if (fontRef != nullptr)
-    {
-       #if JUCE_MAC && defined (MAC_OS_X_VERSION_10_8) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8
-        CTFontManagerUnregisterGraphicsFont (fontRef, nullptr);
-       #endif
-
-        CGFontRelease (fontRef);
-    }
+        CFRelease (attributedStringAtts);
+    
+    if (fontRef != nullptr)
+    {
+       #if JUCE_MAC && defined (MAC_OS_X_VERSION_10_8) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8
+        if (registered)
+            CTFontManagerUnregisterGraphicsFont (fontRef, nullptr);
+       #endif
+        
+        CGFontRelease (fontRef);
+    }
 
     if (ctFontRef != nullptr)
         CFRelease (ctFontRef);
2 Likes

#7

Seriously good research, thanks!

0 Likes

#8

Just bumping this to see if there will be a workaround on the JUCE repo.

0 Likes

#9

For a better idea of our code, here’s what we’ve done in our LookAndFeel:

Font UALookAndFeel::getLabelFont(Label &thisLabel)
{
    
    const float scaledHeight = thisLabel.getFont().getHeight() * fontScaleLabel;
    //return thisLabel.getFont().withHeight(scaledHeight);
    
    if (thisLabel.getFont().isBold())
    {
        return boldLabelFont.withHeight(scaledHeight);
    }

    return defaultLabelFont.withHeight(scaledHeight);
}

fontScaleLabel is a variable initialized with the plugin since various Typefaces scale differently. boldLabelFont and defaultLabelFont are set once in our Editor’s constructor with

Font(Typeface::createSystemTypefaceFor(fontData, fontDataSize));

0 Likes

#10

We didn’t experienced memory leaks using createSystemTypeFaceFor on OSX so far.

We currently implement this in our custom LookAndFeel class in this way

static Typeface::Ptr getTypeFaceBlaBla()
{
     static Typeface::Ptr typeface = Typeface::createSystemTypefaceFor(BinaryData::myFont_ttf, BinaryData::myFont_ttfSize);
     return typeface;
}

We declare it static, so maybe that’s why it’s not leaking.

My 2 cents

1 Like

#11

Thank you! I had to do some rewriting… From what I can tell, that not only fixed the leak, but reduced our plugin’s base RAM consumption by 20-30 MB.

0 Likes