nsFontCache.cpp (8650B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "nsFontCache.h" 7 8 #include "gfxTextRun.h" 9 #include "mozilla/Services.h" 10 #include "mozilla/ServoUtils.h" 11 #include "nsCRT.h" 12 13 #include "mozilla/dom/Document.h" 14 #include "nsPresContext.h" 15 16 using mozilla::services::GetObserverService; 17 18 NS_IMPL_ISUPPORTS(nsFontCache, nsIObserver) 19 20 static mozilla::LazyLogModule gFingerprinterDetection("FingerprinterDetection"); 21 22 // The Init and Destroy methods are necessary because it's not 23 // safe to call AddObserver from a constructor or RemoveObserver 24 // from a destructor. That should be fixed. 25 void nsFontCache::Init(nsPresContext* aContext) { 26 MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); 27 mContext = aContext; 28 // register as a memory-pressure observer to free font resources 29 // in low-memory situations. 30 nsCOMPtr<nsIObserverService> obs = GetObserverService(); 31 if (obs) { 32 obs->AddObserver(this, "memory-pressure", false); 33 } 34 35 mLocaleLanguage = nsLanguageAtomService::GetService()->GetLocaleLanguage(); 36 if (!mLocaleLanguage) { 37 mLocaleLanguage = NS_Atomize("x-western"); 38 } 39 } 40 41 void nsFontCache::Destroy() { 42 MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); 43 nsCOMPtr<nsIObserverService> obs = GetObserverService(); 44 if (obs) { 45 obs->RemoveObserver(this, "memory-pressure"); 46 } 47 Flush(); 48 } 49 50 NS_IMETHODIMP 51 nsFontCache::Observe(nsISupports*, const char* aTopic, const char16_t*) { 52 MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); 53 if (!nsCRT::strcmp(aTopic, "memory-pressure")) { 54 Compact(); 55 } 56 return NS_OK; 57 } 58 59 already_AddRefed<nsFontMetrics> nsFontCache::GetMetricsFor( 60 const nsFont& aFont, const nsFontMetrics::Params& aParams) { 61 // We may eventually want to put an nsFontCache on canvas2d workers, but for 62 // now it is only used by the main-thread layout code and stylo. 63 mozilla::AssertIsMainThreadOrServoFontMetricsLocked(); 64 65 nsAtom* language = aParams.language && !aParams.language->IsEmpty() 66 ? aParams.language 67 : mLocaleLanguage.get(); 68 69 // First check our cache 70 // start from the end, which is where we put the most-recent-used element 71 const int32_t n = mFontMetrics.Length() - 1; 72 for (int32_t i = n; i >= 0; --i) { 73 nsFontMetrics* fm = mFontMetrics.Elements()[i]; 74 if (fm->Font().Equals(aFont) && 75 fm->GetUserFontSet() == aParams.userFontSet && 76 fm->Language() == language && 77 fm->Orientation() == aParams.orientation && 78 #ifdef XP_WIN 79 fm->AllowForceGDIClassic() == aParams.allowForceGDIClassic && 80 #endif 81 fm->ExplicitLanguage() == aParams.explicitLanguage) { 82 if (i != n) { 83 // promote it to the end of the cache 84 mFontMetrics.RemoveElementAtUnsafe(i); 85 mFontMetrics.AppendElement(fm); 86 } 87 fm->GetThebesFontGroup()->UpdateUserFonts(); 88 return do_AddRef(fm); 89 } 90 } 91 92 DetectFontFingerprinting(aFont); 93 94 // It's not in the cache. Get font metrics and then cache them. 95 // If the cache has reached its size limit, drop the older half of the 96 // entries; but if we're on a stylo thread (the usual case), we have 97 // to post a task back to the main thread to do the flush. 98 if (n >= kMaxCacheEntries - 1 && !mFlushPending) { 99 if (NS_IsMainThread()) { 100 Flush(mFontMetrics.Length() - kMaxCacheEntries / 2); 101 } else { 102 mFlushPending = true; 103 nsCOMPtr<nsIRunnable> flushTask = new FlushFontMetricsTask(this); 104 MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(flushTask)); 105 } 106 } 107 108 nsFontMetrics::Params params = aParams; 109 params.language = language; 110 RefPtr<nsFontMetrics> fm = new nsFontMetrics(aFont, params, mContext); 111 // the mFontMetrics list has the "head" at the end, because append 112 // is cheaper than insert 113 mFontMetrics.AppendElement(do_AddRef(fm).take()); 114 return fm.forget(); 115 } 116 117 void nsFontCache::DetectFontFingerprinting(const nsFont& aFont) { 118 // We try to detect font fingerprinting attempts by recognizing a large 119 // number of cache misses in a short amount of time, which indicates the 120 // usage of an unreasonable amount of different fonts by the web page. 121 122 if (aFont.family.families.list.IsEmpty()) { 123 return; 124 } 125 126 if (!MOZ_LOG_TEST(gFingerprinterDetection, mozilla::LogLevel::Info) && 127 mReportedProbableFingerprinting) { 128 // We've already reported fingerprinting for this document, and logging 129 // isn't enabled, so skip the rest of the detection logic. 130 return; 131 } 132 133 PRTime now = PR_Now(); 134 nsAutoString key; 135 for (const auto& family : aFont.family.families.list.AsSpan()) { 136 if (family.IsGeneric()) { 137 continue; 138 } 139 key.Append(family.AsFamilyName().name.AsAtom()->GetUTF16String()); 140 } 141 if (key.IsEmpty()) { 142 return; 143 } 144 145 auto missedFonts = mMissedFontFamilyNames.Lock(); 146 missedFonts->InsertOrUpdate(key, now); 147 // Don't bother checking for fingerprinting attempts if we haven't seen 148 // enough cache misses yet. 149 if (missedFonts->Count() <= kFingerprintingCacheMissThreshold) { 150 return; 151 } 152 uint16_t fontsMissedRecently = 0; 153 154 bool clearMissedFonts = false; 155 if (!mReportedProbableFingerprinting) { 156 for (auto iter = missedFonts->Iter(); !iter.Done(); iter.Next()) { 157 if (now - kFingerprintingLastNSec <= iter.Data()) { 158 if (++fontsMissedRecently > kFingerprintingCacheMissThreshold) { 159 mContext->Document()->RecordFontFingerprinting(); 160 mReportedProbableFingerprinting = true; 161 clearMissedFonts = true; 162 break; 163 } 164 } else { 165 // Remove the old entries from missed cache list. 166 iter.Remove(); 167 } 168 } 169 if (mReportedProbableFingerprinting) { 170 // We detected a font fingerprinter, so print out all the fonts they 171 // queries and missed 172 for (auto iter = missedFonts->Iter(); !iter.Done(); iter.Next()) { 173 MOZ_LOG( 174 gFingerprinterDetection, mozilla::LogLevel::Info, 175 ("Font Fingerprinting Tripped | Document %p | Font Family | %s", 176 mContext->Document(), NS_ConvertUTF16toUTF8(iter.Key()).get())); 177 } 178 } 179 } else { 180 // I've already reported the font fingerprinting, but I want to report each 181 // additional font 182 MOZ_LOG(gFingerprinterDetection, mozilla::LogLevel::Info, 183 ("Font Fingerprinting Tripped | Document %p | Font Family | %s", 184 mContext->Document(), NS_ConvertUTF16toUTF8(key).get())); 185 } 186 if (!MOZ_LOG_TEST(gFingerprinterDetection, mozilla::LogLevel::Info) && 187 clearMissedFonts) { 188 missedFonts->Clear(); 189 } 190 } 191 192 void nsFontCache::UpdateUserFonts(gfxUserFontSet* aUserFontSet) { 193 MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); 194 for (nsFontMetrics* fm : mFontMetrics) { 195 gfxFontGroup* fg = fm->GetThebesFontGroup(); 196 if (fg->GetUserFontSet() == aUserFontSet) { 197 fg->UpdateUserFonts(); 198 } 199 } 200 } 201 202 void nsFontCache::FontMetricsDeleted(const nsFontMetrics* aFontMetrics) { 203 MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); 204 mFontMetrics.RemoveElement(aFontMetrics); 205 } 206 207 void nsFontCache::Compact() { 208 MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); 209 // Need to loop backward because the running element can be removed on 210 // the way 211 for (int32_t i = mFontMetrics.Length() - 1; i >= 0; --i) { 212 nsFontMetrics* fm = mFontMetrics[i]; 213 nsFontMetrics* oldfm = fm; 214 // Destroy() isn't here because we want our device context to be 215 // notified 216 NS_RELEASE(fm); // this will reset fm to nullptr 217 // if the font is really gone, it would have called back in 218 // FontMetricsDeleted() and would have removed itself 219 if (mFontMetrics.IndexOf(oldfm) != mFontMetrics.NoIndex) { 220 // nope, the font is still there, so let's hold onto it too 221 NS_ADDREF(oldfm); 222 } 223 } 224 auto missedFonts = mMissedFontFamilyNames.Lock(); 225 missedFonts->Clear(); 226 } 227 228 // Flush the aFlushCount oldest entries, or all if (aFlushCount < 0) 229 void nsFontCache::Flush(int32_t aFlushCount) { 230 MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); 231 int32_t n = aFlushCount < 0 232 ? mFontMetrics.Length() 233 : std::min<int32_t>(aFlushCount, mFontMetrics.Length()); 234 for (int32_t i = n - 1; i >= 0; --i) { 235 nsFontMetrics* fm = mFontMetrics[i]; 236 // Destroy() will unhook our device context from the fm so that we 237 // won't waste time in triggering the notification of 238 // FontMetricsDeleted() in the subsequent release 239 fm->Destroy(); 240 NS_RELEASE(fm); 241 } 242 mFontMetrics.RemoveElementsAt(0, n); 243 }