back out gfxFontGroup::MakeTextRun with individual words. b=416725, due to performance regression on Windows.
authorkarlt+@karlt.net
Sun, 17 Feb 2008 15:29:03 -0800
changeset 11807 485e35ddff0e3bdc805dd387cf84079dfb67b6ed
parent 11806 1ac2b3c1cfe1090d84d99809c0f72a8dffe022e8
child 11808 ceddc8d20bf108bfcce1410db3d7f751bc52446e
push idunknown
push userunknown
push dateunknown
bugs416725
milestone1.9b4pre
back out gfxFontGroup::MakeTextRun with individual words. b=416725, due to performance regression on Windows.
gfx/thebes/src/gfxTextRunWordCache.cpp
--- a/gfx/thebes/src/gfxTextRunWordCache.cpp
+++ b/gfx/thebes/src/gfxTextRunWordCache.cpp
@@ -155,21 +155,33 @@ protected:
         // looking at the textrun's text.
         PRUint32    mWordOffset:31;
         // This is set to true when the cache entry was hashed by the first
         // font in mTextRun's fontgroup; it's false when the cache entry
         // was hashed by the fontgroup itself.
         PRUint32    mHashedByFont:1;
     };
     
+    // Used to track words that should be copied from one textrun to
+    // another during the textrun construction process
+    struct DeferredWord {
+        gfxTextRun *mSourceTextRun;
+        PRUint32    mSourceOffset;
+        PRUint32    mDestOffset;
+        PRUint32    mLength;
+        PRUint32    mHash;
+    };
+    
     PRBool LookupWord(gfxTextRun *aTextRun, gfxFont *aFirstFont,
-                      PRUint32 aStart, PRUint32 aEnd, PRUint32 aHash);
-    void AddWord(gfxTextRun *aTextRun, PRUint32 aDestOffset,
-                 gfxTextRun *aNewRun, PRUint32 aSourceOffset, PRUint32 aLength,
-                 PRUint32 aHash, const gfxFontGroup::Parameters *aParams);
+                      PRUint32 aStart, PRUint32 aEnd, PRUint32 aHash,
+                      nsTArray<DeferredWord>* aDeferredWords);
+    void FinishTextRun(gfxTextRun *aTextRun, gfxTextRun *aNewRun,
+                       const gfxFontGroup::Parameters *aParams,
+                       const nsTArray<DeferredWord>& aDeferredWords,
+                       PRBool aSuccessful);
     void RemoveWord(gfxTextRun *aTextRun, PRUint32 aStart,
                     PRUint32 aEnd, PRUint32 aHash);    
 
     nsTHashtable<CacheHashEntry> mCache;
     
 #ifdef DEBUG
     static PLDHashOperator PR_CALLBACK CacheDumpEntry(CacheHashEntry* aEntry, void* userArg);
 #endif
@@ -218,121 +230,211 @@ static PRBool
 IsWordBoundary(PRUnichar aChar)
 {
     return IsBoundarySpace(aChar) || gfxFontGroup::IsInvalidChar(aChar);
 }
 
 /**
  * Looks up a word in the cache. If the word is found in the cache
  * (which could be from an existing textrun or an earlier word in the same
- * textrun), we copy the glyphs from the word into the textrun.
+ * textrun), we copy the glyphs from the word into the textrun, unless
+ * aDeferredWords is non-null (meaning that all words from now on must be
+ * copied later instead of now), in which case we add the word to be copied
+ * to the list.
+ * 
+ * If the word is not found in the cache then we add it to the cache with
+ * aFirstFont as the key, on the assumption that the word will be matched
+ * by aFirstFont. If this assumption is false we fix up the cache later in
+ * FinishTextRun. We make this assumption for two reasons:
+ * 1) it's usually true so it saves an extra cache lookup if we had to insert
+ * the entry later
+ * 2) we need to record words that appear in the string in some kind
+ * of hash table so we can detect and use them if they appear later in the
+ * (in general the string might be huge and contain many repeated words).
+ * We might as well use the master hash table for this.
  * 
  * @return true if the word was found in the cache, false otherwise.
  */
 PRBool
 TextRunWordCache::LookupWord(gfxTextRun *aTextRun, gfxFont *aFirstFont,
-                             PRUint32 aStart, PRUint32 aEnd, PRUint32 aHash)
+                             PRUint32 aStart, PRUint32 aEnd, PRUint32 aHash,
+                             nsTArray<DeferredWord>* aDeferredWords)
 {
     if (aEnd <= aStart)
         return PR_TRUE;
 
-    // Look for an entry keyed by font or fontgroup
     CacheHashKey key(aTextRun, aFirstFont, aStart, aEnd - aStart, aHash);
-    CacheHashEntry *existingEntry = mCache.GetEntry(key);
-    if (!existingEntry) {
+    CacheHashEntry *fontEntry = mCache.PutEntry(key);
+    if (!fontEntry)
+        return PR_FALSE;
+    CacheHashEntry *existingEntry = nsnull;
+
+    if (fontEntry->mTextRun) {
+        existingEntry = fontEntry;
+    } else {
+        PR_LOG(gWordCacheLog, PR_LOG_DEBUG, ("%p(%d-%d,%d): added using font", aTextRun, aStart, aEnd - aStart, aHash));
         key.mFontOrGroup = aTextRun->GetFontGroup();
-        existingEntry = mCache.GetEntry(key);
+        CacheHashEntry *groupEntry = mCache.GetEntry(key);
+        if (groupEntry) {
+            existingEntry = groupEntry;
+            mCache.RawRemoveEntry(fontEntry);
+            PR_LOG(gWordCacheLog, PR_LOG_DEBUG, ("%p(%d-%d,%d): removed using font", aTextRun, aStart, aEnd - aStart, aHash));
+            fontEntry = nsnull;
+        }
+    }
+    // At this point, either existingEntry is non-null and points to (surprise!)
+    // an entry for a word in an existing textrun, or fontEntry points
+    // to a cache entry for this word with aFirstFont, which needs to be
+    // filled in or removed.
+
+    if (existingEntry) {
+        if (aDeferredWords) {
+            DeferredWord word = { existingEntry->mTextRun,
+                  existingEntry->mWordOffset, aStart, aEnd - aStart, aHash };
+            aDeferredWords->AppendElement(word);
+        } else {
+            aTextRun->CopyGlyphDataFrom(existingEntry->mTextRun,
+                existingEntry->mWordOffset, aEnd - aStart, aStart, PR_FALSE);
+        }
+        return PR_TRUE;
     }
 
-    if (!existingEntry)
-        return PR_FALSE;
-
-    // Copy the glyphs from the word into the text run
-    aTextRun->CopyGlyphDataFrom(existingEntry->mTextRun,
-                                existingEntry->mWordOffset,
-                                aEnd - aStart, aStart, PR_FALSE);
-    return PR_TRUE;
+#ifdef DEBUG
+    ++aTextRun->mCachedWords;
+#endif
+    // Set up the cache entry so that if later in this textrun we hit this
+    // entry, we'll copy within our own textrun
+    fontEntry->mTextRun = aTextRun;
+    fontEntry->mWordOffset = aStart;
+    fontEntry->mHashedByFont = PR_TRUE;
+    return PR_FALSE;
 }
 
 /**
- * Processes a word that doesn't yet exist in the cache.  The word is copied
- * from the source textrun to the output textrun. (The source may be an
- * earlier word in the output textrun.)
+ * Processes all deferred words. Each word is copied from the source
+ * textrun to the output textrun. (The source may be an earlier word in the
+ * output textrun.) If the word was not matched by the textrun's fontgroup's
+ * first font, then we remove the entry we optimistically added to the cache
+ * with that font in the key, and add a new entry keyed with the fontgroup
+ * instead.
+ * 
+ * @param aSuccessful if false, then we don't do any word copies and we don't
+ * add anything to the cache, but we still remove all the optimistic cache
+ * entries.
  */
 void
-TextRunWordCache::AddWord(gfxTextRun *aTextRun, PRUint32 aDestOffset,
-                          gfxTextRun *aNewRun, PRUint32 aSourceOffset,
-                          PRUint32 aLength, PRUint32 aHash,
-                          const gfxFontGroup::Parameters *aParams)
+TextRunWordCache::FinishTextRun(gfxTextRun *aTextRun, gfxTextRun *aNewRun,
+                                const gfxFontGroup::Parameters *aParams,
+                                const nsTArray<DeferredWord>& aDeferredWords,
+                                PRBool aSuccessful)
 {
-    gfxTextRun *source = aNewRun;
-    PRUint32 sourceOffset = aSourceOffset;
-    PRUint32 destOffset = aDestOffset;
-    PRUint32 length = aLength;
-    nsAutoPtr<gfxTextRun> tmpTextRun;
-
-    // If the word starts inside a cluster we don't want this word
-    // in the cache.
-    PRBool wordStartsInsideCluster =
-        aNewRun && !aNewRun->IsClusterStart(aSourceOffset);
+    aTextRun->SetFlagBits(gfxTextRunWordCache::TEXT_IN_CACHE);
 
-    if (!wordStartsInsideCluster) {
-        CacheHashKey key(aTextRun,
-                         GetWordFontOrGroup(aNewRun, aSourceOffset, aLength),
-                         aDestOffset, aLength, aHash);
-        CacheHashEntry *entry = mCache.PutEntry(key);
-        NS_ASSERTION(!entry->mTextRun, "Entry shouldn't have existed!");
-
-        if (entry) {
-            aTextRun->SetFlagBits(gfxTextRunWordCache::TEXT_IN_CACHE);
-
-            entry->mTextRun = aTextRun;
-            entry->mWordOffset = aDestOffset;
-            gfxFontGroup *fontGroup = aTextRun->GetFontGroup();
-            entry->mHashedByFont = key.mFontOrGroup != fontGroup;
+    PRUint32 i;
+    gfxFontGroup *fontGroup = aTextRun->GetFontGroup();
+    gfxFont *font = fontGroup->GetFontAt(0);
+    // copy deferred words from various sources into destination textrun
+    for (i = 0; i < aDeferredWords.Length(); ++i) {
+        const DeferredWord *word = &aDeferredWords[i];
+        gfxTextRun *source = word->mSourceTextRun;
+        if (!source) {
+            source = aNewRun;
+        }
+        // If the word starts inside a cluster we don't want this word
+        // in the cache, so we'll remove the associated cache entry
+        PRBool wordStartsInsideCluster =
+            !source->IsClusterStart(word->mSourceOffset);
+        if (source == aNewRun) {
+            // We created a cache entry for this word based on the assumption
+            // that the word matches GetFontAt(0). If this assumption is false,
+            // we need to remove that cache entry and replace it with an entry
+            // keyed off the fontgroup.
+            PRBool rekeyWithFontGroup =
+                GetWordFontOrGroup(aNewRun, word->mSourceOffset, word->mLength) != font;
+            if (!aSuccessful || wordStartsInsideCluster || rekeyWithFontGroup) {
+                // We need to remove the current placeholder cache entry
+                CacheHashKey key(aTextRun, font, word->mDestOffset, word->mLength,
+                                 word->mHash);
+                NS_ASSERTION(mCache.GetEntry(key),
+                             "This entry should have been added previously!");
+                mCache.RemoveEntry(key);
+#ifdef DEBUG
+                --aTextRun->mCachedWords;
+#endif
+                PR_LOG(gWordCacheLog, PR_LOG_DEBUG, ("%p(%d-%d,%d): removed using font", aTextRun, word->mDestOffset, word->mLength, word->mHash));
+                
+                if (aSuccessful && !wordStartsInsideCluster) {
+                    key.mFontOrGroup = fontGroup;
+                    CacheHashEntry *groupEntry = mCache.PutEntry(key);
+                    if (groupEntry) {
 #ifdef DEBUG
-            ++aTextRun->mCachedWords;
-            PR_LOG(gWordCacheLog, PR_LOG_DEBUG,
-                   ("%p(%d-%d,%d): added using %s",
-                    aTextRun, aDestOffset, aLength, aHash,
-                    entry->mHashedByFont ? "font" :"fontgroup"));
-            NS_ASSERTION(mCache.GetEntry(key),
-                         "We should find the thing we just added!");
+                        ++aTextRun->mCachedWords;
 #endif
+                        PR_LOG(gWordCacheLog, PR_LOG_DEBUG, ("%p(%d-%d,%d): added using fontgroup", aTextRun, word->mDestOffset, word->mLength, word->mHash));
+                        groupEntry->mTextRun = aTextRun;
+                        groupEntry->mWordOffset = word->mDestOffset;
+                        groupEntry->mHashedByFont = PR_FALSE;
+                        NS_ASSERTION(mCache.GetEntry(key),
+                                     "We should find the thing we just added!");
+                    }
+                }
+            }
+        }
+        if (aSuccessful) {
+            // Copy the word. If the source is aNewRun, then
+            // allow CopyGlyphDataFrom to steal the internal data of
+            // aNewRun since that's only temporary anyway.
+            PRUint32 sourceOffset = word->mSourceOffset;
+            PRUint32 destOffset = word->mDestOffset;
+            PRUint32 length = word->mLength;
+            nsAutoPtr<gfxTextRun> tmpTextRun;
+            PRBool stealData = source == aNewRun;
+            if (wordStartsInsideCluster) {
+                NS_ASSERTION(sourceOffset > 0, "How can the first character be inside a cluster?");
+                if (destOffset > 0 && IsBoundarySpace(aTextRun->GetChar(destOffset - 1))) {
+                    // We should copy over data for the preceding space
+                    // as well. The glyphs have probably been attached
+                    // to that character.
+                    --sourceOffset;
+                    --destOffset;
+                    ++length;
+                } else {
+                    // URK! This case sucks! We have combining marks
+                    // at the start of the text. We had to prepend a space
+                    // just so we could detect these are combining marks
+                    // (so we can keep this "word" out of the cache).
+                    // But now the data in aNewRun is no use to us. We
+                    // need to find out what the platform would do
+                    // if the marks were at the start of the text.
+                    tmpTextRun = aNewRun->GetFontGroup()->MakeTextRun(
+                        aTextRun->GetTextUnicode(), length, aParams,
+                        aNewRun->GetFlags());
+                    source = tmpTextRun;
+                    sourceOffset = 0;
+                    stealData = PR_TRUE;
+                }
+            }
+            aTextRun->CopyGlyphDataFrom(source, sourceOffset, length,
+                destOffset, stealData);
+            // Fill in additional spaces
+            PRUint32 endCharIndex;
+            if (i + 1 < aDeferredWords.Length()) {
+                endCharIndex = aDeferredWords[i + 1].mDestOffset;
+            } else {
+                endCharIndex = aTextRun->GetLength();
+            }
+            PRUint32 charIndex;
+            for (charIndex = word->mDestOffset + word->mLength;
+                 charIndex < endCharIndex; ++charIndex) {
+                if (IsBoundarySpace(aTextRun->GetChar(charIndex))) {
+                    aTextRun->SetSpaceGlyph(font, aParams->mContext, charIndex);
+                }
+            }
         }
     }
-    else { // wordStartsInsideCluster
-        NS_ASSERTION(sourceOffset > 0, "How can the first character be inside a cluster?");
-        if (destOffset > 0 && IsBoundarySpace(aTextRun->GetChar(destOffset - 1))) {
-            // We should copy over data for the preceding space
-            // as well. The glyphs have probably been attached
-            // to that character.
-            --sourceOffset;
-            --destOffset;
-            ++length;
-        } else {
-            // URK! This case sucks! We have combining marks
-            // at the start of the text. We had to prepend a space
-            // just so we could detect these are combining marks
-            // (so we can keep this "word" out of the cache).
-            // But now the data in aNewRun is no use to us. We
-            // need to find out what the platform would do
-            // if the marks were at the start of the text.
-            tmpTextRun = aNewRun->GetFontGroup()->
-                MakeTextRun(aTextRun->GetTextUnicode(), length, aParams,
-                            aNewRun->GetFlags());
-            source = tmpTextRun;
-            sourceOffset = 0;
-        }
-    }
-    // Copy the word.
-    // Allow CopyGlyphDataFrom to steal the internal data of
-    // aNewRun or tmpTextRun since they are only temporary anyway.
-    aTextRun->CopyGlyphDataFrom(source, sourceOffset, length,
-                                destOffset, PR_TRUE);
 }
 
 static gfxTextRun *
 MakeBlankTextRun(const void* aText, PRUint32 aLength,
                          gfxFontGroup *aFontGroup,
                          const gfxFontGroup::Parameters *aParams,
                          PRUint32 aFlags)
 {
@@ -365,64 +467,70 @@ TextRunWordCache::MakeTextRun(const PRUn
 #ifdef DEBUG
     textRun->mCachedWords = 0;
 #endif
 
     gfxFont *font = aFontGroup->GetFontAt(0);
     nsresult rv = textRun->AddGlyphRun(font, 0);
     NS_ENSURE_SUCCESS(rv, nsnull);
 
-    gfxTextRunFactory::Parameters params =
-        { aParams->mContext, nsnull, nsnull, nsnull, 0, aParams->mAppUnitsPerDevUnit };
     nsAutoTArray<PRUnichar,200> tempString;
+    nsAutoTArray<DeferredWord,50> deferredWords;
     PRUint32 i;
     PRUint32 wordStart = 0;
     PRUint32 hash = 0;
     for (i = 0; i <= aLength; ++i) {
         PRUnichar ch = i < aLength ? aText[i] : ' ';
         if (IsWordBoundary(ch)) {
-            PRBool hit = LookupWord(textRun, font, wordStart, i, hash);
+            PRBool hit = LookupWord(textRun, font, wordStart, i, hash,
+                                    deferredWords.Length() == 0 ? nsnull : &deferredWords);
             if (!hit) {
                 // Always put a space before the word so we can detect
                 // combining characters at the start of a word
                 tempString.AppendElement(' ');
                 PRUint32 offset = tempString.Length();
                 PRUint32 length = i - wordStart;
-
                 PRUnichar *chars = tempString.AppendElements(length);
-                if (!chars)
+                if (!chars) {
+                    FinishTextRun(textRun, nsnull, nsnull, deferredWords, PR_FALSE);
                     return nsnull;
+                }
                 memcpy(chars, aText + wordStart, length*sizeof(PRUnichar));
-
-                // create textrun for this unknown word
-                nsAutoPtr<gfxTextRun> newRun;
-                newRun = aFontGroup->
-                    MakeTextRun(tempString.Elements(), tempString.Length(),
-                                &params,
-                                aFlags | gfxTextRunFactory::TEXT_IS_PERSISTENT);
-
-                if (newRun) {
-                    AddWord(textRun, wordStart, newRun, offset, length, hash,
-                            aParams);
-                }
-                tempString.Clear();
+                DeferredWord word = { nsnull, offset, wordStart, length, hash };
+                deferredWords.AppendElement(word);
             }
             
-            if (IsBoundarySpace(ch) && i < aLength) {
-                textRun->SetSpaceGlyph(font, aParams->mContext, i);
-            } // else we should set this character to be invisible missing,
-              // but it already is because the textrun is blank!
-
+            if (deferredWords.Length() == 0) {
+                if (IsBoundarySpace(ch) && i < aLength) {
+                    textRun->SetSpaceGlyph(font, aParams->mContext, i);
+                } // else we should set this character to be invisible missing,
+                  // but it already is because the textrun is blank!
+            }
             hash = 0;
             wordStart = i + 1;
         } else {
             hash = HashMix(hash, ch);
         }
     }
 
+    if (deferredWords.Length() == 0) {
+        // We got everything from the cache, so we're done. No point in calling
+        // FinishTextRun.
+        // This textrun is not referenced by the cache.
+        return textRun.forget();
+    }
+
+    // create textrun for unknown words
+    gfxTextRunFactory::Parameters params =
+        { aParams->mContext, nsnull, nsnull, nsnull, 0, aParams->mAppUnitsPerDevUnit };
+    nsAutoPtr<gfxTextRun> newRun;
+    newRun = aFontGroup->MakeTextRun(tempString.Elements(), tempString.Length(),
+                                     &params, aFlags | gfxTextRunFactory::TEXT_IS_PERSISTENT);
+
+    FinishTextRun(textRun, newRun, aParams, deferredWords, newRun != nsnull);
     return textRun.forget();
 }
 
 gfxTextRun *
 TextRunWordCache::MakeTextRun(const PRUint8 *aText, PRUint32 aLength,
                               gfxFontGroup *aFontGroup,
                               const gfxFontGroup::Parameters *aParams,
                               PRUint32 aFlags)
@@ -442,51 +550,70 @@ TextRunWordCache::MakeTextRun(const PRUi
 #ifdef DEBUG
     textRun->mCachedWords = 0;
 #endif
 
     gfxFont *font = aFontGroup->GetFontAt(0);
     nsresult rv = textRun->AddGlyphRun(font, 0);
     NS_ENSURE_SUCCESS(rv, nsnull);
 
-    gfxTextRunFactory::Parameters params =
-        { aParams->mContext, nsnull, nsnull, nsnull, 0, aParams->mAppUnitsPerDevUnit };
+    nsAutoTArray<PRUint8,200> tempString;
+    nsAutoTArray<DeferredWord,50> deferredWords;
     PRUint32 i;
     PRUint32 wordStart = 0;
     PRUint32 hash = 0;
     for (i = 0; i <= aLength; ++i) {
         PRUint8 ch = i < aLength ? aText[i] : ' ';
         if (IsWordBoundary(ch)) {
-            PRBool hit = LookupWord(textRun, font, wordStart, i, hash);
+            PRBool hit = LookupWord(textRun, font, wordStart, i, hash,
+                                    deferredWords.Length() == 0 ? nsnull : &deferredWords);
             if (!hit) {
+                if (tempString.Length() > 0) {
+                    tempString.AppendElement(' ');
+                }
+                PRUint32 offset = tempString.Length();
                 PRUint32 length = i - wordStart;
-
-                // create textrun for this unknown word
-                nsAutoPtr<gfxTextRun> newRun;
-                newRun = aFontGroup->
-                    MakeTextRun(aText + wordStart, length, &params,
-                                aFlags | gfxTextRunFactory::TEXT_IS_PERSISTENT);
-                if (newRun) {
-                    AddWord(textRun, wordStart, newRun, 0, length, hash,
-                            aParams);
+                PRUint8 *chars = tempString.AppendElements(length);
+                if (!chars) {
+                    FinishTextRun(textRun, nsnull, nsnull, deferredWords, PR_FALSE);
+                    return nsnull;
                 }
+                memcpy(chars, aText + wordStart, length*sizeof(PRUint8));
+                DeferredWord word = { nsnull, offset, wordStart, length, hash };
+                deferredWords.AppendElement(word);
             }
             
-            if (IsBoundarySpace(ch) && i < aLength) {
-                textRun->SetSpaceGlyph(font, aParams->mContext, i);
-            } // else we should set this character to be invisible missing,
-              // but it already is because the textrun is blank!
-
+            if (deferredWords.Length() == 0) {
+                if (IsBoundarySpace(ch) && i < aLength) {
+                    textRun->SetSpaceGlyph(font, aParams->mContext, i);
+                } // else we should set this character to be invisible missing,
+                  // but it already is because the textrun is blank!
+            }
             hash = 0;
             wordStart = i + 1;
         } else {
             hash = HashMix(hash, ch);
         }
     }
 
+    if (deferredWords.Length() == 0) {
+        // We got everything from the cache, so we're done. No point in calling
+        // FinishTextRun.
+        // This textrun is not referenced by the cache.
+        return textRun.forget();
+    }
+
+    // create textrun for unknown words
+    gfxTextRunFactory::Parameters params =
+        { aParams->mContext, nsnull, nsnull, nsnull, 0, aParams->mAppUnitsPerDevUnit };
+    nsAutoPtr<gfxTextRun> newRun;
+    newRun = aFontGroup->MakeTextRun(tempString.Elements(), tempString.Length(),
+                                     &params, aFlags | gfxTextRunFactory::TEXT_IS_PERSISTENT);
+
+    FinishTextRun(textRun, newRun, aParams, deferredWords, newRun != nsnull);
     return textRun.forget();
 }
 
 void
 TextRunWordCache::RemoveWord(gfxTextRun *aTextRun, PRUint32 aStart,
                              PRUint32 aEnd, PRUint32 aHash)
 {
     if (aEnd <= aStart)
@@ -607,17 +734,17 @@ TextRunWordCache::CacheHashEntry::HashKe
 PLDHashOperator PR_CALLBACK
 TextRunWordCache::CacheDumpEntry(CacheHashEntry* aEntry, void* userArg)
 {
     FILE* output = static_cast<FILE*>(userArg);
     if (!aEntry->mTextRun) {
         fprintf(output, "<EMPTY>\n");
         return PL_DHASH_NEXT;
     }
-    fprintf(output, "Word at %p:%d => ", static_cast<void*>(aEntry->mTextRun), aEntry->mWordOffset);
+    fprintf(output, "Word at %x:%d => ", aEntry->mTextRun, aEntry->mWordOffset);
     aEntry->mTextRun->Dump(output);
     fprintf(output, " (hashed by %s)\n", aEntry->mHashedByFont ? "font" : "fontgroup");
     return PL_DHASH_NEXT;
 }
 
 void
 TextRunWordCache::Dump()
 {