| 1 | // Copyright 2019 Google LLC. |
| 2 | #include <memory> |
| 3 | |
| 4 | #include "modules/skparagraph/include/ParagraphCache.h" |
| 5 | #include "modules/skparagraph/src/ParagraphImpl.h" |
| 6 | |
| 7 | namespace skia { |
| 8 | namespace textlayout { |
| 9 | |
| 10 | namespace { |
| 11 | SkScalar relax(SkScalar a) { |
| 12 | // This rounding is done to match Flutter tests. Must be removed.. |
| 13 | if (SkScalarIsFinite(a)) { |
| 14 | auto threshold = SkIntToScalar(1 << 12); |
| 15 | return SkScalarRoundToScalar(a * threshold)/threshold; |
| 16 | } else { |
| 17 | return a; |
| 18 | } |
| 19 | } |
| 20 | } // namespace |
| 21 | |
| 22 | class ParagraphCacheKey { |
| 23 | public: |
| 24 | ParagraphCacheKey(const ParagraphImpl* paragraph) |
| 25 | : fText(paragraph->fText.c_str(), paragraph->fText.size()) |
| 26 | , fPlaceholders(paragraph->fPlaceholders) |
| 27 | , fTextStyles(paragraph->fTextStyles) |
| 28 | , fParagraphStyle(paragraph->paragraphStyle()) { } |
| 29 | |
| 30 | SkString fText; |
| 31 | SkTArray<Placeholder, true> fPlaceholders; |
| 32 | SkTArray<Block, true> fTextStyles; |
| 33 | ParagraphStyle fParagraphStyle; |
| 34 | }; |
| 35 | |
| 36 | class ParagraphCacheValue { |
| 37 | public: |
| 38 | ParagraphCacheValue(const ParagraphImpl* paragraph) |
| 39 | : fKey(ParagraphCacheKey(paragraph)) |
| 40 | , fRuns(paragraph->fRuns) |
| 41 | , fCodeUnitProperties(paragraph->fCodeUnitProperties) |
| 42 | , fWords(paragraph->fWords) |
| 43 | , fBidiRegions(paragraph->fBidiRegions) |
| 44 | , fUTF8IndexForUTF16Index(paragraph->fUTF8IndexForUTF16Index) |
| 45 | , fUTF16IndexForUTF8Index(paragraph->fUTF16IndexForUTF8Index) { } |
| 46 | |
| 47 | // Input == key |
| 48 | ParagraphCacheKey fKey; |
| 49 | |
| 50 | // Shaped results |
| 51 | SkTArray<Run, false> fRuns; |
| 52 | // ICU results |
| 53 | SkTArray<CodeUnitFlags> fCodeUnitProperties; |
| 54 | std::vector<size_t> fWords; |
| 55 | std::vector<BidiRegion> fBidiRegions; |
| 56 | SkTArray<TextIndex, true> fUTF8IndexForUTF16Index; |
| 57 | SkTArray<size_t, true> fUTF16IndexForUTF8Index; |
| 58 | }; |
| 59 | |
| 60 | uint32_t ParagraphCache::KeyHash::mix(uint32_t hash, uint32_t data) const { |
| 61 | hash += data; |
| 62 | hash += (hash << 10); |
| 63 | hash ^= (hash >> 6); |
| 64 | return hash; |
| 65 | } |
| 66 | |
| 67 | uint32_t ParagraphCache::KeyHash::operator()(const ParagraphCacheKey& key) const { |
| 68 | uint32_t hash = 0; |
| 69 | for (auto& ph : key.fPlaceholders) { |
| 70 | if (ph.fRange.width() == 0) { |
| 71 | continue; |
| 72 | } |
| 73 | hash = mix(hash, SkGoodHash()(ph.fRange.start)); |
| 74 | hash = mix(hash, SkGoodHash()(ph.fRange.end)); |
| 75 | hash = mix(hash, SkGoodHash()(relax(ph.fStyle.fHeight))); |
| 76 | hash = mix(hash, SkGoodHash()(relax(ph.fStyle.fWidth))); |
| 77 | hash = mix(hash, SkGoodHash()(ph.fStyle.fAlignment)); |
| 78 | hash = mix(hash, SkGoodHash()(ph.fStyle.fBaseline)); |
| 79 | if (ph.fStyle.fAlignment == PlaceholderAlignment::kBaseline) { |
| 80 | hash = mix(hash, SkGoodHash()(relax(ph.fStyle.fBaselineOffset))); |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | for (auto& ts : key.fTextStyles) { |
| 85 | if (ts.fStyle.isPlaceholder()) { |
| 86 | continue; |
| 87 | } |
| 88 | hash = mix(hash, SkGoodHash()(relax(ts.fStyle.getLetterSpacing()))); |
| 89 | hash = mix(hash, SkGoodHash()(relax(ts.fStyle.getWordSpacing()))); |
| 90 | hash = mix(hash, SkGoodHash()(ts.fStyle.getLocale())); |
| 91 | hash = mix(hash, SkGoodHash()(relax(ts.fStyle.getHeight()))); |
| 92 | for (auto& ff : ts.fStyle.getFontFamilies()) { |
| 93 | hash = mix(hash, SkGoodHash()(ff)); |
| 94 | } |
| 95 | for (auto& ff : ts.fStyle.getFontFeatures()) { |
| 96 | hash = mix(hash, SkGoodHash()(ff.fValue)); |
| 97 | hash = mix(hash, SkGoodHash()(ff.fName)); |
| 98 | } |
| 99 | hash = mix(hash, SkGoodHash()(ts.fStyle.getFontStyle())); |
| 100 | hash = mix(hash, SkGoodHash()(relax(ts.fStyle.getFontSize()))); |
| 101 | hash = mix(hash, SkGoodHash()(ts.fRange)); |
| 102 | } |
| 103 | |
| 104 | hash = mix(hash, SkGoodHash()(relax(key.fParagraphStyle.getHeight()))); |
| 105 | hash = mix(hash, SkGoodHash()(key.fParagraphStyle.getTextDirection())); |
| 106 | |
| 107 | auto& strutStyle = key.fParagraphStyle.getStrutStyle(); |
| 108 | if (strutStyle.getStrutEnabled()) { |
| 109 | hash = mix(hash, SkGoodHash()(relax(strutStyle.getHeight()))); |
| 110 | hash = mix(hash, SkGoodHash()(relax(strutStyle.getLeading()))); |
| 111 | hash = mix(hash, SkGoodHash()(relax(strutStyle.getFontSize()))); |
| 112 | hash = mix(hash, SkGoodHash()(strutStyle.getHeightOverride())); |
| 113 | hash = mix(hash, SkGoodHash()(strutStyle.getFontStyle())); |
| 114 | hash = mix(hash, SkGoodHash()(strutStyle.getForceStrutHeight())); |
| 115 | for (auto& ff : strutStyle.getFontFamilies()) { |
| 116 | hash = mix(hash, SkGoodHash()(ff)); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | hash = mix(hash, SkGoodHash()(key.fText)); |
| 121 | return hash; |
| 122 | } |
| 123 | |
| 124 | bool operator==(const ParagraphCacheKey& a, const ParagraphCacheKey& b) { |
| 125 | if (a.fText.size() != b.fText.size()) { |
| 126 | return false; |
| 127 | } |
| 128 | if (a.fPlaceholders.count() != b.fPlaceholders.count()) { |
| 129 | return false; |
| 130 | } |
| 131 | if (a.fText != b.fText) { |
| 132 | return false; |
| 133 | } |
| 134 | if (a.fTextStyles.size() != b.fTextStyles.size()) { |
| 135 | return false; |
| 136 | } |
| 137 | |
| 138 | // There is no need to compare default paragraph styles - they are included into fTextStyles |
| 139 | if (!nearlyEqual(a.fParagraphStyle.getHeight(), b.fParagraphStyle.getHeight())) { |
| 140 | return false; |
| 141 | } |
| 142 | if (a.fParagraphStyle.getTextDirection() != b.fParagraphStyle.getTextDirection()) { |
| 143 | return false; |
| 144 | } |
| 145 | |
| 146 | if (!(a.fParagraphStyle.getStrutStyle() == b.fParagraphStyle.getStrutStyle())) { |
| 147 | return false; |
| 148 | } |
| 149 | |
| 150 | for (size_t i = 0; i < a.fTextStyles.size(); ++i) { |
| 151 | auto& tsa = a.fTextStyles[i]; |
| 152 | auto& tsb = b.fTextStyles[i]; |
| 153 | if (tsa.fStyle.isPlaceholder()) { |
| 154 | continue; |
| 155 | } |
| 156 | if (!(tsa.fStyle.equalsByFonts(tsb.fStyle))) { |
| 157 | return false; |
| 158 | } |
| 159 | if (tsa.fRange.width() != tsb.fRange.width()) { |
| 160 | return false; |
| 161 | } |
| 162 | if (tsa.fRange.start != tsb.fRange.start) { |
| 163 | return false; |
| 164 | } |
| 165 | } |
| 166 | for (size_t i = 0; i < a.fPlaceholders.size(); ++i) { |
| 167 | auto& tsa = a.fPlaceholders[i]; |
| 168 | auto& tsb = b.fPlaceholders[i]; |
| 169 | if (tsa.fRange.width() == 0 && tsb.fRange.width() == 0) { |
| 170 | continue; |
| 171 | } |
| 172 | if (!(tsa.fStyle.equals(tsb.fStyle))) { |
| 173 | return false; |
| 174 | } |
| 175 | if (tsa.fRange.width() != tsb.fRange.width()) { |
| 176 | return false; |
| 177 | } |
| 178 | if (tsa.fRange.start != tsb.fRange.start) { |
| 179 | return false; |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | return true; |
| 184 | } |
| 185 | |
| 186 | struct ParagraphCache::Entry { |
| 187 | |
| 188 | Entry(ParagraphCacheValue* value) : fValue(value) {} |
| 189 | std::unique_ptr<ParagraphCacheValue> fValue; |
| 190 | }; |
| 191 | |
| 192 | ParagraphCache::ParagraphCache() |
| 193 | : fChecker([](ParagraphImpl* impl, const char*, bool){ }) |
| 194 | , fLRUCacheMap(kMaxEntries) |
| 195 | , fCacheIsOn(true) |
| 196 | #ifdef PARAGRAPH_CACHE_STATS |
| 197 | , fTotalRequests(0) |
| 198 | , fCacheMisses(0) |
| 199 | , fHashMisses(0) |
| 200 | #endif |
| 201 | { } |
| 202 | |
| 203 | ParagraphCache::~ParagraphCache() { } |
| 204 | |
| 205 | void ParagraphCache::updateTo(ParagraphImpl* paragraph, const Entry* entry) { |
| 206 | |
| 207 | paragraph->fRuns.reset(); |
| 208 | paragraph->fRuns = entry->fValue->fRuns; |
| 209 | paragraph->fCodeUnitProperties = entry->fValue->fCodeUnitProperties; |
| 210 | paragraph->fWords = entry->fValue->fWords; |
| 211 | paragraph->fBidiRegions = entry->fValue->fBidiRegions; |
| 212 | paragraph->fUTF8IndexForUTF16Index = entry->fValue->fUTF8IndexForUTF16Index; |
| 213 | paragraph->fUTF16IndexForUTF8Index = entry->fValue->fUTF16IndexForUTF8Index; |
| 214 | for (auto& run : paragraph->fRuns) { |
| 215 | run.setOwner(paragraph); |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | void ParagraphCache::printStatistics() { |
| 220 | SkDebugf("--- Paragraph Cache ---\n" ); |
| 221 | SkDebugf("Total requests: %d\n" , fTotalRequests); |
| 222 | SkDebugf("Cache misses: %d\n" , fCacheMisses); |
| 223 | SkDebugf("Cache miss %%: %f\n" , (fTotalRequests > 0) ? 100.f * fCacheMisses / fTotalRequests : 0.f); |
| 224 | int cacheHits = fTotalRequests - fCacheMisses; |
| 225 | SkDebugf("Hash miss %%: %f\n" , (cacheHits > 0) ? 100.f * fHashMisses / cacheHits : 0.f); |
| 226 | SkDebugf("---------------------\n" ); |
| 227 | } |
| 228 | |
| 229 | void ParagraphCache::abandon() { |
| 230 | SkAutoMutexExclusive lock(fParagraphMutex); |
| 231 | fLRUCacheMap.foreach([](ParagraphCacheKey*, std::unique_ptr<Entry>* e) { |
| 232 | }); |
| 233 | |
| 234 | this->reset(); |
| 235 | } |
| 236 | |
| 237 | void ParagraphCache::reset() { |
| 238 | SkAutoMutexExclusive lock(fParagraphMutex); |
| 239 | #ifdef PARAGRAPH_CACHE_STATS |
| 240 | fTotalRequests = 0; |
| 241 | fCacheMisses = 0; |
| 242 | fHashMisses = 0; |
| 243 | #endif |
| 244 | fLRUCacheMap.reset(); |
| 245 | } |
| 246 | |
| 247 | bool ParagraphCache::findParagraph(ParagraphImpl* paragraph) { |
| 248 | if (!fCacheIsOn) { |
| 249 | return false; |
| 250 | } |
| 251 | #ifdef PARAGRAPH_CACHE_STATS |
| 252 | ++fTotalRequests; |
| 253 | #endif |
| 254 | SkAutoMutexExclusive lock(fParagraphMutex); |
| 255 | ParagraphCacheKey key(paragraph); |
| 256 | std::unique_ptr<Entry>* entry = fLRUCacheMap.find(key); |
| 257 | |
| 258 | if (!entry) { |
| 259 | // We have a cache miss |
| 260 | #ifdef PARAGRAPH_CACHE_STATS |
| 261 | ++fCacheMisses; |
| 262 | #endif |
| 263 | fChecker(paragraph, "missingParagraph" , true); |
| 264 | return false; |
| 265 | } |
| 266 | updateTo(paragraph, entry->get()); |
| 267 | fChecker(paragraph, "foundParagraph" , true); |
| 268 | return true; |
| 269 | } |
| 270 | |
| 271 | bool ParagraphCache::updateParagraph(ParagraphImpl* paragraph) { |
| 272 | if (!fCacheIsOn) { |
| 273 | return false; |
| 274 | } |
| 275 | #ifdef PARAGRAPH_CACHE_STATS |
| 276 | ++fTotalRequests; |
| 277 | #endif |
| 278 | SkAutoMutexExclusive lock(fParagraphMutex); |
| 279 | ParagraphCacheKey key(paragraph); |
| 280 | std::unique_ptr<Entry>* entry = fLRUCacheMap.find(key); |
| 281 | if (!entry) { |
| 282 | ParagraphCacheValue* value = new ParagraphCacheValue(paragraph); |
| 283 | fLRUCacheMap.insert(key, std::make_unique<Entry>(value)); |
| 284 | fChecker(paragraph, "addedParagraph" , true); |
| 285 | return true; |
| 286 | } else { |
| 287 | // We do not have to update the paragraph |
| 288 | return false; |
| 289 | } |
| 290 | } |
| 291 | } // namespace textlayout |
| 292 | } // namespace skia |
| 293 | |