<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[174516] trunk/Source/WebCore</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/174516">174516</a></dd>
<dt>Author</dt> <dd>cdumez@apple.com</dd>
<dt>Date</dt> <dd>2014-10-09 12:05:51 -0700 (Thu, 09 Oct 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>[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]):</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkSourceWebCoreChangeLog">trunk/Source/WebCore/ChangeLog</a></li>
<li><a href="#trunkSourceWebCoreplatformgraphicsmacFontCacheMacmm">trunk/Source/WebCore/platform/graphics/mac/FontCacheMac.mm</a></li>
<li><a href="#trunkSourceWebCoreplatformmacWebFontCacheh">trunk/Source/WebCore/platform/mac/WebFontCache.h</a></li>
<li><a href="#trunkSourceWebCoreplatformmacWebFontCachemm">trunk/Source/WebCore/platform/mac/WebFontCache.mm</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkSourceWebCoreChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/ChangeLog (174515 => 174516)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/ChangeLog        2014-10-09 18:43:25 UTC (rev 174515)
+++ trunk/Source/WebCore/ChangeLog        2014-10-09 19:05:51 UTC (rev 174516)
</span><span class="lines">@@ -1,3 +1,45 @@
</span><ins>+2014-10-09  Chris Dumez  &lt;cdumez@apple.com&gt;
+
+        [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]):
+
</ins><span class="cx"> 2014-10-09  Bear Travis  &lt;betravis@adobe.com&gt;
</span><span class="cx"> 
</span><span class="cx">         [CSS Font Loading] Decrement the font loading count before notifying callbacks
</span></span></pre></div>
<a id="trunkSourceWebCoreplatformgraphicsmacFontCacheMacmm"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/platform/graphics/mac/FontCacheMac.mm (174515 => 174516)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/platform/graphics/mac/FontCacheMac.mm        2014-10-09 18:43:25 UTC (rev 174515)
+++ trunk/Source/WebCore/platform/graphics/mac/FontCacheMac.mm        2014-10-09 19:05:51 UTC (rev 174516)
</span><span class="lines">@@ -54,6 +54,7 @@
</span><span class="cx">         return;
</span><span class="cx">     }
</span><span class="cx">     fontCache().invalidate();
</span><ins>+    [WebFontCache invalidate];
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> static void fontCacheRegisteredFontsChangedNotificationCallback(CFNotificationCenterRef, void* observer, CFStringRef name, const void *, CFDictionaryRef)
</span></span></pre></div>
<a id="trunkSourceWebCoreplatformmacWebFontCacheh"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/platform/mac/WebFontCache.h (174515 => 174516)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/platform/mac/WebFontCache.h        2014-10-09 18:43:25 UTC (rev 174515)
+++ trunk/Source/WebCore/platform/mac/WebFontCache.h        2014-10-09 19:05:51 UTC (rev 174516)
</span><span class="lines">@@ -32,6 +32,7 @@
</span><span class="cx"> + (NSFont *)fontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits weight:(int)desiredWeight size:(float)size shouldAutoActivateIfNeeded:(BOOL)shouldAutoActivateIfNeeded;
</span><span class="cx"> + (NSFont *)fontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits weight:(int)desiredWeight size:(float)size;
</span><span class="cx"> + (void)getTraits:(Vector&lt;unsigned&gt;&amp;)traitsMasks inFamily:(NSString *)desiredFamily;
</span><ins>++ (void)invalidate;
</ins><span class="cx"> 
</span><span class="cx"> // This older version of the interface is relied upon by some clients. WebCore doesn't use it.
</span><span class="cx"> + (NSFont *)fontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits size:(float)size;
</span></span></pre></div>
<a id="trunkSourceWebCoreplatformmacWebFontCachemm"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/platform/mac/WebFontCache.mm (174515 => 174516)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/platform/mac/WebFontCache.mm        2014-10-09 18:43:25 UTC (rev 174515)
+++ trunk/Source/WebCore/platform/mac/WebFontCache.mm        2014-10-09 19:05:51 UTC (rev 174516)
</span><span class="lines">@@ -35,6 +35,7 @@
</span><span class="cx"> #import &lt;AppKit/AppKit.h&gt;
</span><span class="cx"> #import &lt;Foundation/Foundation.h&gt;
</span><span class="cx"> #import &lt;math.h&gt;
</span><ins>+#import &lt;wtf/MainThread.h&gt;
</ins><span class="cx"> 
</span><span class="cx"> using namespace WebCore;
</span><span class="cx"> 
</span><span class="lines">@@ -115,6 +116,30 @@
</span><span class="cx">                                    FontWeight900Mask));
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+// 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] &lt;= 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];
+}
+
</ins><span class="cx"> @implementation WebFontCache
</span><span class="cx"> 
</span><span class="cx"> + (void)getTraits:(Vector&lt;unsigned&gt;&amp;)traitsMasks inFamily:(NSString *)desiredFamily
</span><span class="lines">@@ -160,48 +185,55 @@
</span><span class="cx"> // we then do a search based on the family names of the installed fonts.
</span><span class="cx"> + (NSFont *)internalFontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits weight:(int)desiredWeight size:(float)size
</span><span class="cx"> {
</span><del>-
</del><span class="cx">     if (stringIsCaseInsensitiveEqualToString(desiredFamily, @&quot;-webkit-system-font&quot;)
</span><span class="cx">         || stringIsCaseInsensitiveEqualToString(desiredFamily, @&quot;-apple-system-font&quot;)) {
</span><span class="cx">         // We ignore italic for system font.
</span><span class="cx">         return (desiredWeight &gt;= 7) ? [NSFont boldSystemFontOfSize:size] : [NSFont systemFontOfSize:size];
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    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;
</del><ins>+    id cachedAvailableFamily = [desiredFamilyToAvailableFamilyDictionary() objectForKey:desiredFamily];
+    if (cachedAvailableFamily == [NSNull null]) {
+        // We already know this font is not available.
+        return nil;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    NSFontManager *fontManager = [NSFontManager sharedFontManager];
+    NSString *availableFamily = cachedAvailableFamily;
</ins><span class="cx">     if (!availableFamily) {
</span><del>-        // Match by PostScript name.
-        NSEnumerator *availableFonts = [[fontManager availableFonts] objectEnumerator];
-        NSString *availableFont;
-        NSFont *nameMatchedFont = nil;
-        NSFontTraitMask desiredTraitsForNameMatch = desiredTraits | (desiredWeight &gt;= 7 ? NSBoldFontMask : 0);
-        while ((availableFont = [availableFonts nextObject])) {
-            if ([desiredFamily caseInsensitiveCompare:availableFont] == NSOrderedSame) {
-                nameMatchedFont = [NSFont fontWithName:availableFont size:size];
</del><ins>+        // 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;
+        }
</ins><span class="cx"> 
</span><del>-                // Special case Osaka-Mono.  According to &lt;rdar://problem/3999467&gt;, we need to 
-                // treat Osaka-Mono as fixed pitch.
-                if ([desiredFamily caseInsensitiveCompare:@&quot;Osaka-Mono&quot;] == NSOrderedSame &amp;&amp; desiredTraitsForNameMatch == 0)
-                    return nameMatchedFont;
</del><ins>+        if (!availableFamily) {
+            // Match by PostScript name.
+            NSFont *nameMatchedFont = nil;
+            NSFontTraitMask desiredTraitsForNameMatch = desiredTraits | (desiredWeight &gt;= 7 ? NSBoldFontMask : 0);
+            for (NSString *availableFont in [fontManager availableFonts]) {
+                if ([desiredFamily caseInsensitiveCompare:availableFont] == NSOrderedSame) {
+                    nameMatchedFont = [NSFont fontWithName:availableFont size:size];
</ins><span class="cx"> 
</span><del>-                NSFontTraitMask traits = [fontManager traitsOfFont:nameMatchedFont];
-                if ((traits &amp; desiredTraitsForNameMatch) == desiredTraitsForNameMatch)
-                    return [fontManager convertFont:nameMatchedFont toHaveTrait:desiredTraitsForNameMatch];
</del><ins>+                    // Special case Osaka-Mono. According to &lt;rdar://problem/3999467&gt;, we need to
+                    // treat Osaka-Mono as fixed pitch.
+                    if ([desiredFamily caseInsensitiveCompare:@&quot;Osaka-Mono&quot;] == NSOrderedSame &amp;&amp; !desiredTraitsForNameMatch)
+                        return nameMatchedFont;
</ins><span class="cx"> 
</span><del>-                availableFamily = [nameMatchedFont familyName];
-                break;
</del><ins>+                    NSFontTraitMask traits = [fontManager traitsOfFont:nameMatchedFont];
+                    if ((traits &amp; desiredTraitsForNameMatch) == desiredTraitsForNameMatch)
+                        return [fontManager convertFont:nameMatchedFont toHaveTrait:desiredTraitsForNameMatch];
+
+                    availableFamily = [nameMatchedFont familyName];
+                    break;
+                }
</ins><span class="cx">             }
</span><span class="cx">         }
</span><ins>+
+        rememberDesiredFamilyToAvailableFamilyMapping(desiredFamily, availableFamily);
+        if (!availableFamily)
+            return nil;
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     // Found a family, now figure out what weight and traits to use.
</span><span class="lines">@@ -300,4 +332,8 @@
</span><span class="cx">     return [self fontWithFamily:desiredFamily traits:desiredTraits weight:desiredWeight size:size shouldAutoActivateIfNeeded:YES];
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>++ (void)invalidate
+{
+    [desiredFamilyToAvailableFamilyDictionary() removeAllObjects];
+}
</ins><span class="cx"> @end
</span></span></pre>
</div>
</div>

</body>
</html>