[Mac] Spending too much time mapping desired font families to available ones
authorcdumez@apple.com <cdumez@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 9 Oct 2014 19:05:51 +0000 (19:05 +0000)
committercdumez@apple.com <cdumez@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 9 Oct 2014 19:05:51 +0000 (19:05 +0000)
https://bugs.webkit.org/show_bug.cgi?id=137539

Reviewed by Darin Adler.

While profiling the load of weather.com, I noticed that we are spending
quite a bit of time trying to map the font family requested to a font
that is available on the system. The process involves:
1. Doing a linear search of all the installed font families and do a
   case-insensitive string comparison for each of them until we find a
   match,
2. Then, if we don't find a match, do another linear search of the
   fonts' postscript names this time and do again a case-insensitive
   string comparison for each of them.

This process is costly and the fonts requested by weather.com are not
available, causing us to do 2 linear searches and a lot of string
comparisons (accounting for ~2% of the WebProcess CPU time for the page
load). As a result, we end up spending ~90ms in
internalFontWithFamily() when loading weather.com.

This patch introduces a cache for the mapping between desired font
families and available font families. This cuts the time spent in
internalFontWithFamily() in half (~45ms). The cache gets invalidated
when fonts are installed / uninstalled on the system so we don't break
that scenario. The cache is also limited in size to avoid using too
much memory.

No new tests, but manual testing making sure the cache gets invalidated
when installing a font on the system.

* platform/graphics/mac/FontCacheMac.mm:
(WebCore::invalidateFontCache):
* platform/mac/WebFontCache.h:
* platform/mac/WebFontCache.mm:
(desiredFamilyToAvailableFamilyDictionary):
(rememberDesiredFamilyToAvailableFamilyMapping):
(+[WebFontCache internalFontWithFamily:traits:weight:size:]):
(+[WebFontCache invalidate]):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@174516 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebCore/ChangeLog
Source/WebCore/platform/graphics/mac/FontCacheMac.mm
Source/WebCore/platform/mac/WebFontCache.h
Source/WebCore/platform/mac/WebFontCache.mm

index 3fe79d8..af17a6c 100644 (file)
@@ -1,3 +1,45 @@
+2014-10-09  Chris Dumez  <cdumez@apple.com>
+
+        [Mac] Spending too much time mapping desired font families to available ones
+        https://bugs.webkit.org/show_bug.cgi?id=137539
+
+        Reviewed by Darin Adler.
+
+        While profiling the load of weather.com, I noticed that we are spending
+        quite a bit of time trying to map the font family requested to a font
+        that is available on the system. The process involves:
+        1. Doing a linear search of all the installed font families and do a
+           case-insensitive string comparison for each of them until we find a
+           match,
+        2. Then, if we don't find a match, do another linear search of the
+           fonts' postscript names this time and do again a case-insensitive
+           string comparison for each of them.
+
+        This process is costly and the fonts requested by weather.com are not
+        available, causing us to do 2 linear searches and a lot of string
+        comparisons (accounting for ~2% of the WebProcess CPU time for the page
+        load). As a result, we end up spending ~90ms in
+        internalFontWithFamily() when loading weather.com.
+
+        This patch introduces a cache for the mapping between desired font
+        families and available font families. This cuts the time spent in
+        internalFontWithFamily() in half (~45ms). The cache gets invalidated
+        when fonts are installed / uninstalled on the system so we don't break
+        that scenario. The cache is also limited in size to avoid using too
+        much memory.
+
+        No new tests, but manual testing making sure the cache gets invalidated
+        when installing a font on the system.
+
+        * platform/graphics/mac/FontCacheMac.mm:
+        (WebCore::invalidateFontCache):
+        * platform/mac/WebFontCache.h:
+        * platform/mac/WebFontCache.mm:
+        (desiredFamilyToAvailableFamilyDictionary):
+        (rememberDesiredFamilyToAvailableFamilyMapping):
+        (+[WebFontCache internalFontWithFamily:traits:weight:size:]):
+        (+[WebFontCache invalidate]):
+
 2014-10-09  Bear Travis  <betravis@adobe.com>
 
         [CSS Font Loading] Decrement the font loading count before notifying callbacks
index 3b8d654..a877f26 100644 (file)
@@ -54,6 +54,7 @@ static void invalidateFontCache(void*)
         return;
     }
     fontCache().invalidate();
+    [WebFontCache invalidate];
 }
 
 static void fontCacheRegisteredFontsChangedNotificationCallback(CFNotificationCenterRef, void* observer, CFStringRef name, const void *, CFDictionaryRef)
index 535e64a..4c20184 100644 (file)
@@ -32,6 +32,7 @@ WEBCORE_EXPORT @interface WebFontCache : NSObject
 + (NSFont *)fontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits weight:(int)desiredWeight size:(float)size shouldAutoActivateIfNeeded:(BOOL)shouldAutoActivateIfNeeded;
 + (NSFont *)fontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits weight:(int)desiredWeight size:(float)size;
 + (void)getTraits:(Vector<unsigned>&)traitsMasks inFamily:(NSString *)desiredFamily;
++ (void)invalidate;
 
 // This older version of the interface is relied upon by some clients. WebCore doesn't use it.
 + (NSFont *)fontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits size:(float)size;
index 58a5f24..aca99e4 100644 (file)
@@ -35,6 +35,7 @@
 #import <AppKit/AppKit.h>
 #import <Foundation/Foundation.h>
 #import <math.h>
+#import <wtf/MainThread.h>
 
 using namespace WebCore;
 
@@ -115,6 +116,30 @@ static inline FontTraitsMask toTraitsMask(NSFontTraitMask appKitTraits, NSIntege
                                    FontWeight900Mask));
 }
 
+// Keep a cache for mapping desired font families to font families actually
+// available on the system for performance.
+static NSMutableDictionary* desiredFamilyToAvailableFamilyDictionary()
+{
+    ASSERT(isMainThread());
+    static NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
+    return dictionary;
+}
+
+static inline void rememberDesiredFamilyToAvailableFamilyMapping(NSString* desiredFamily, NSString* availableFamily)
+{
+    static const NSUInteger maxCacheSize = 128;
+    NSMutableDictionary *familyMapping = desiredFamilyToAvailableFamilyDictionary();
+    ASSERT([familyMapping count] <= maxCacheSize);
+    if ([familyMapping count] == maxCacheSize) {
+        for (NSString *key in familyMapping) {
+            [familyMapping removeObjectForKey:key];
+            break;
+        }
+    }
+    id value = availableFamily ? availableFamily : [NSNull null];
+    [familyMapping setObject:value forKey:desiredFamily];
+}
+
 @implementation WebFontCache
 
 + (void)getTraits:(Vector<unsigned>&)traitsMasks inFamily:(NSString *)desiredFamily
@@ -160,48 +185,55 @@ static inline FontTraitsMask toTraitsMask(NSFontTraitMask appKitTraits, NSIntege
 // we then do a search based on the family names of the installed fonts.
 + (NSFont *)internalFontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits weight:(int)desiredWeight size:(float)size
 {
-
     if (stringIsCaseInsensitiveEqualToString(desiredFamily, @"-webkit-system-font")
         || stringIsCaseInsensitiveEqualToString(desiredFamily, @"-apple-system-font")) {
         // We ignore italic for system font.
         return (desiredWeight >= 7) ? [NSFont boldSystemFontOfSize:size] : [NSFont systemFontOfSize:size];
     }
 
-    NSFontManager *fontManager = [NSFontManager sharedFontManager];
-
-    // Do a simple case insensitive search for a matching font family.
-    // NSFontManager requires exact name matches.
-    // This addresses the problem of matching arial to Arial, etc., but perhaps not all the issues.
-    NSEnumerator *e = [[fontManager availableFontFamilies] objectEnumerator];
-    NSString *availableFamily;
-    while ((availableFamily = [e nextObject])) {
-        if ([desiredFamily caseInsensitiveCompare:availableFamily] == NSOrderedSame)
-            break;
+    id cachedAvailableFamily = [desiredFamilyToAvailableFamilyDictionary() objectForKey:desiredFamily];
+    if (cachedAvailableFamily == [NSNull null]) {
+        // We already know this font is not available.
+        return nil;
     }
 
+    NSFontManager *fontManager = [NSFontManager sharedFontManager];
+    NSString *availableFamily = cachedAvailableFamily;
     if (!availableFamily) {
-        // Match by PostScript name.
-        NSEnumerator *availableFonts = [[fontManager availableFonts] objectEnumerator];
-        NSString *availableFont;
-        NSFont *nameMatchedFont = nil;
-        NSFontTraitMask desiredTraitsForNameMatch = desiredTraits | (desiredWeight >= 7 ? NSBoldFontMask : 0);
-        while ((availableFont = [availableFonts nextObject])) {
-            if ([desiredFamily caseInsensitiveCompare:availableFont] == NSOrderedSame) {
-                nameMatchedFont = [NSFont fontWithName:availableFont size:size];
-
-                // Special case Osaka-Mono.  According to <rdar://problem/3999467>, we need to 
-                // treat Osaka-Mono as fixed pitch.
-                if ([desiredFamily caseInsensitiveCompare:@"Osaka-Mono"] == NSOrderedSame && desiredTraitsForNameMatch == 0)
-                    return nameMatchedFont;
-
-                NSFontTraitMask traits = [fontManager traitsOfFont:nameMatchedFont];
-                if ((traits & desiredTraitsForNameMatch) == desiredTraitsForNameMatch)
-                    return [fontManager convertFont:nameMatchedFont toHaveTrait:desiredTraitsForNameMatch];
-
-                availableFamily = [nameMatchedFont familyName];
+        // Do a simple case insensitive search for a matching font family.
+        // NSFontManager requires exact name matches.
+        // This addresses the problem of matching arial to Arial, etc., but perhaps not all the issues.
+        for (availableFamily in [fontManager availableFontFamilies]) {
+            if ([desiredFamily caseInsensitiveCompare:availableFamily] == NSOrderedSame)
                 break;
+        }
+
+        if (!availableFamily) {
+            // Match by PostScript name.
+            NSFont *nameMatchedFont = nil;
+            NSFontTraitMask desiredTraitsForNameMatch = desiredTraits | (desiredWeight >= 7 ? NSBoldFontMask : 0);
+            for (NSString *availableFont in [fontManager availableFonts]) {
+                if ([desiredFamily caseInsensitiveCompare:availableFont] == NSOrderedSame) {
+                    nameMatchedFont = [NSFont fontWithName:availableFont size:size];
+
+                    // Special case Osaka-Mono. According to <rdar://problem/3999467>, we need to
+                    // treat Osaka-Mono as fixed pitch.
+                    if ([desiredFamily caseInsensitiveCompare:@"Osaka-Mono"] == NSOrderedSame && !desiredTraitsForNameMatch)
+                        return nameMatchedFont;
+
+                    NSFontTraitMask traits = [fontManager traitsOfFont:nameMatchedFont];
+                    if ((traits & desiredTraitsForNameMatch) == desiredTraitsForNameMatch)
+                        return [fontManager convertFont:nameMatchedFont toHaveTrait:desiredTraitsForNameMatch];
+
+                    availableFamily = [nameMatchedFont familyName];
+                    break;
+                }
             }
         }
+
+        rememberDesiredFamilyToAvailableFamilyMapping(desiredFamily, availableFamily);
+        if (!availableFamily)
+            return nil;
     }
 
     // Found a family, now figure out what weight and traits to use.
@@ -300,4 +332,8 @@ static inline FontTraitsMask toTraitsMask(NSFontTraitMask appKitTraits, NSIntege
     return [self fontWithFamily:desiredFamily traits:desiredTraits weight:desiredWeight size:size shouldAutoActivateIfNeeded:YES];
 }
 
++ (void)invalidate
+{
+    [desiredFamilyToAvailableFamilyDictionary() removeAllObjects];
+}
 @end