1 /*
2  * Copyright 2018 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "src/core/SkStrikeCache.h"
9 
10 #include <cctype>
11 
12 #include "include/core/SkGraphics.h"
13 #include "include/core/SkTraceMemoryDump.h"
14 #include "include/core/SkTypeface.h"
15 #include "include/private/SkMutex.h"
16 #include "include/private/SkTemplates.h"
17 #include "src/core/SkGlyphRunPainter.h"
18 #include "src/core/SkStrike.h"
19 
20 class SkStrikeCache::Node final : public SkStrikeForGPU {
21 public:
Node(SkStrikeCache * strikeCache,const SkDescriptor & desc,std::unique_ptr<SkScalerContext> scaler,const SkFontMetrics & metrics,std::unique_ptr<SkStrikePinner> pinner)22     Node(SkStrikeCache* strikeCache,
23          const SkDescriptor& desc,
24          std::unique_ptr<SkScalerContext> scaler,
25          const SkFontMetrics& metrics,
26          std::unique_ptr<SkStrikePinner> pinner)
27             : fStrikeCache{strikeCache}
28             , fStrike{desc, std::move(scaler), metrics}
29             , fPinner{std::move(pinner)} {}
30 
roundingSpec() const31     const SkGlyphPositionRoundingSpec& roundingSpec() const override {
32         return fStrike.roundingSpec();
33     }
34 
35     SkSpan<const SkGlyphPos>
prepareForDrawingRemoveEmpty(const SkPackedGlyphID packedGlyphIDs[],const SkPoint positions[],size_t n,int maxDimension,SkGlyphPos results[])36     prepareForDrawingRemoveEmpty(const SkPackedGlyphID packedGlyphIDs[],
37                                  const SkPoint positions[],
38                                  size_t n,
39                                  int maxDimension,
40                                  SkGlyphPos results[]) override {
41         return fStrike.prepareForDrawingRemoveEmpty(packedGlyphIDs,
42                                                     positions,
43                                                     n,
44                                                     maxDimension,
45                                                     results);
46     }
47 
getDescriptor() const48     const SkDescriptor& getDescriptor() const override {
49         return fStrike.getDescriptor();
50     }
51 
onAboutToExitScope()52     void onAboutToExitScope() override {
53         fStrikeCache->attachNode(this);
54     }
55 
56     SkStrikeCache* const            fStrikeCache;
57     Node*                           fNext{nullptr};
58     Node*                           fPrev{nullptr};
59     SkStrike                        fStrike;
60     std::unique_ptr<SkStrikePinner> fPinner;
61 };
62 
63 bool gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental = false;
64 
GlobalStrikeCache()65 SkStrikeCache* SkStrikeCache::GlobalStrikeCache() {
66 #if !defined(SK_BUILD_FOR_IOS)
67     if (gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental) {
68         static thread_local auto* cache = new SkStrikeCache;
69         return cache;
70     }
71 #endif
72     static auto* cache = new SkStrikeCache;
73     return cache;
74 }
75 
ExclusiveStrikePtr(SkStrikeCache::Node * node)76 SkStrikeCache::ExclusiveStrikePtr::ExclusiveStrikePtr(SkStrikeCache::Node* node)
77     : fNode{node} {}
78 
ExclusiveStrikePtr()79 SkStrikeCache::ExclusiveStrikePtr::ExclusiveStrikePtr()
80     : fNode{nullptr} {}
81 
ExclusiveStrikePtr(ExclusiveStrikePtr && o)82 SkStrikeCache::ExclusiveStrikePtr::ExclusiveStrikePtr(ExclusiveStrikePtr&& o)
83     : fNode{o.fNode} {
84     o.fNode = nullptr;
85 }
86 
87 SkStrikeCache::ExclusiveStrikePtr&
operator =(ExclusiveStrikePtr && o)88 SkStrikeCache::ExclusiveStrikePtr::operator = (ExclusiveStrikePtr&& o) {
89     if (fNode != nullptr) {
90         fNode->fStrikeCache->attachNode(fNode);
91     }
92     fNode = o.fNode;
93     o.fNode = nullptr;
94     return *this;
95 }
96 
~ExclusiveStrikePtr()97 SkStrikeCache::ExclusiveStrikePtr::~ExclusiveStrikePtr() {
98     if (fNode != nullptr) {
99         fNode->fStrikeCache->attachNode(fNode);
100     }
101 }
102 
get() const103 SkStrike* SkStrikeCache::ExclusiveStrikePtr::get() const {
104     return &fNode->fStrike;
105 }
106 
operator ->() const107 SkStrike* SkStrikeCache::ExclusiveStrikePtr::operator -> () const {
108     return this->get();
109 }
110 
operator *() const111 SkStrike& SkStrikeCache::ExclusiveStrikePtr::operator *  () const {
112     return *this->get();
113 }
114 
operator ==(const SkStrikeCache::ExclusiveStrikePtr & lhs,const SkStrikeCache::ExclusiveStrikePtr & rhs)115 bool operator == (const SkStrikeCache::ExclusiveStrikePtr& lhs,
116                   const SkStrikeCache::ExclusiveStrikePtr& rhs) {
117     return lhs.fNode == rhs.fNode;
118 }
119 
operator ==(const SkStrikeCache::ExclusiveStrikePtr & lhs,decltype(nullptr))120 bool operator == (const SkStrikeCache::ExclusiveStrikePtr& lhs, decltype(nullptr)) {
121     return lhs.fNode == nullptr;
122 }
123 
operator ==(decltype(nullptr),const SkStrikeCache::ExclusiveStrikePtr & rhs)124 bool operator == (decltype(nullptr), const SkStrikeCache::ExclusiveStrikePtr& rhs) {
125     return nullptr == rhs.fNode;
126 }
127 
~SkStrikeCache()128 SkStrikeCache::~SkStrikeCache() {
129     Node* node = fHead;
130     while (node) {
131         Node* next = node->fNext;
132         delete node;
133         node = next;
134     }
135 }
136 
CreateScalerContext(const SkDescriptor & desc,const SkScalerContextEffects & effects,const SkTypeface & typeface)137 std::unique_ptr<SkScalerContext> SkStrikeCache::CreateScalerContext(
138         const SkDescriptor& desc,
139         const SkScalerContextEffects& effects,
140         const SkTypeface& typeface) {
141     auto scaler = typeface.createScalerContext(effects, &desc, true /* can fail */);
142 
143     // Check if we can create a scaler-context before creating the glyphcache.
144     // If not, we may have exhausted OS/font resources, so try purging the
145     // cache once and try again
146     // pass true the first time, to notice if the scalercontext failed,
147     if (scaler == nullptr) {
148         PurgeAll();
149         scaler = typeface.createScalerContext(effects, &desc, false /* must succeed */);
150     }
151     return scaler;
152 }
153 
findOrCreateStrikeExclusive(const SkDescriptor & desc,const SkScalerContextEffects & effects,const SkTypeface & typeface)154 SkExclusiveStrikePtr SkStrikeCache::findOrCreateStrikeExclusive(
155         const SkDescriptor& desc, const SkScalerContextEffects& effects, const SkTypeface& typeface)
156 {
157     return SkExclusiveStrikePtr(this->findOrCreateStrike(desc, effects, typeface));
158 }
159 
findOrCreateStrike(const SkDescriptor & desc,const SkScalerContextEffects & effects,const SkTypeface & typeface)160 auto SkStrikeCache::findOrCreateStrike(const SkDescriptor& desc,
161                                        const SkScalerContextEffects& effects,
162                                        const SkTypeface& typeface) -> Node* {
163     Node* node = this->findAndDetachStrike(desc);
164     if (node == nullptr) {
165         auto scaler = CreateScalerContext(desc, effects, typeface);
166         node = this->createStrike(desc, std::move(scaler));
167     }
168     return node;
169 }
170 
findOrCreateScopedStrike(const SkDescriptor & desc,const SkScalerContextEffects & effects,const SkTypeface & typeface)171 SkScopedStrikeForGPU SkStrikeCache::findOrCreateScopedStrike(const SkDescriptor& desc,
172                                                              const SkScalerContextEffects& effects,
173                                                              const SkTypeface& typeface) {
174     return SkScopedStrikeForGPU{this->findOrCreateStrike(desc, effects, typeface)};
175 }
176 
PurgeAll()177 void SkStrikeCache::PurgeAll() {
178     GlobalStrikeCache()->purgeAll();
179 }
180 
Dump()181 void SkStrikeCache::Dump() {
182     SkDebugf("GlyphCache [     used    budget ]\n");
183     SkDebugf("    bytes  [ %8zu  %8zu ]\n",
184              SkGraphics::GetFontCacheUsed(), SkGraphics::GetFontCacheLimit());
185     SkDebugf("    count  [ %8zu  %8zu ]\n",
186              SkGraphics::GetFontCacheCountUsed(), SkGraphics::GetFontCacheCountLimit());
187 
188     int counter = 0;
189 
190     auto visitor = [&counter](const SkStrike& cache) {
191         const SkScalerContextRec& rec = cache.getScalerContext()->getRec();
192 
193         SkDebugf("index %d\n", counter);
194         SkDebugf("%s", rec.dump().c_str());
195         counter += 1;
196     };
197 
198     GlobalStrikeCache()->forEachStrike(visitor);
199 }
200 
201 namespace {
202     const char gGlyphCacheDumpName[] = "skia/sk_glyph_cache";
203 }  // namespace
204 
DumpMemoryStatistics(SkTraceMemoryDump * dump)205 void SkStrikeCache::DumpMemoryStatistics(SkTraceMemoryDump* dump) {
206     dump->dumpNumericValue(gGlyphCacheDumpName, "size", "bytes", SkGraphics::GetFontCacheUsed());
207     dump->dumpNumericValue(gGlyphCacheDumpName, "budget_size", "bytes",
208                            SkGraphics::GetFontCacheLimit());
209     dump->dumpNumericValue(gGlyphCacheDumpName, "glyph_count", "objects",
210                            SkGraphics::GetFontCacheCountUsed());
211     dump->dumpNumericValue(gGlyphCacheDumpName, "budget_glyph_count", "objects",
212                            SkGraphics::GetFontCacheCountLimit());
213 
214     if (dump->getRequestedDetails() == SkTraceMemoryDump::kLight_LevelOfDetail) {
215         dump->setMemoryBacking(gGlyphCacheDumpName, "malloc", nullptr);
216         return;
217     }
218 
219     auto visitor = [&dump](const SkStrike& cache) {
220         const SkTypeface* face = cache.getScalerContext()->getTypeface();
221         const SkScalerContextRec& rec = cache.getScalerContext()->getRec();
222 
223         SkString fontName;
224         face->getFamilyName(&fontName);
225         // Replace all special characters with '_'.
226         for (size_t index = 0; index < fontName.size(); ++index) {
227             if (!std::isalnum(fontName[index])) {
228                 fontName[index] = '_';
229             }
230         }
231 
232         SkString dumpName = SkStringPrintf(
233                 "%s/%s_%d/%p", gGlyphCacheDumpName, fontName.c_str(), rec.fFontID, &cache);
234 
235         dump->dumpNumericValue(dumpName.c_str(),
236                                "size", "bytes", cache.getMemoryUsed());
237         dump->dumpNumericValue(dumpName.c_str(),
238                                "glyph_count", "objects", cache.countCachedGlyphs());
239         dump->setMemoryBacking(dumpName.c_str(), "malloc", nullptr);
240     };
241 
242     GlobalStrikeCache()->forEachStrike(visitor);
243 }
244 
245 
attachNode(Node * node)246 void SkStrikeCache::attachNode(Node* node) {
247     if (node == nullptr) {
248         return;
249     }
250     SkAutoSpinlock ac(fLock);
251 
252     this->validate();
253     node->fStrike.validate();
254 
255     this->internalAttachToHead(node);
256     this->internalPurge();
257 }
258 
findStrikeExclusive(const SkDescriptor & desc)259 SkExclusiveStrikePtr SkStrikeCache::findStrikeExclusive(const SkDescriptor& desc) {
260     return SkExclusiveStrikePtr(this->findAndDetachStrike(desc));
261 }
262 
findAndDetachStrike(const SkDescriptor & desc)263 auto SkStrikeCache::findAndDetachStrike(const SkDescriptor& desc) -> Node* {
264     SkAutoSpinlock ac(fLock);
265 
266     for (Node* node = internalGetHead(); node != nullptr; node = node->fNext) {
267         if (node->fStrike.getDescriptor() == desc) {
268             this->internalDetachCache(node);
269             return node;
270         }
271     }
272 
273     return nullptr;
274 }
275 
276 
loose_compare(const SkDescriptor & lhs,const SkDescriptor & rhs)277 static bool loose_compare(const SkDescriptor& lhs, const SkDescriptor& rhs) {
278     uint32_t size;
279     auto ptr = lhs.findEntry(kRec_SkDescriptorTag, &size);
280     SkScalerContextRec lhsRec;
281     memcpy(&lhsRec, ptr, size);
282 
283     ptr = rhs.findEntry(kRec_SkDescriptorTag, &size);
284     SkScalerContextRec rhsRec;
285     memcpy(&rhsRec, ptr, size);
286 
287     // If these don't match, there's no way we can use these strikes interchangeably.
288     // Note that a typeface from each renderer maps to a unique proxy typeface on the GPU,
289     // keyed in the glyph cache using fontID in the SkDescriptor. By limiting this search
290     // to descriptors with the same fontID, we ensure that a renderer never uses glyphs
291     // generated by a different renderer.
292     return
293         lhsRec.fFontID == rhsRec.fFontID &&
294         lhsRec.fTextSize == rhsRec.fTextSize &&
295         lhsRec.fPreScaleX == rhsRec.fPreScaleX &&
296         lhsRec.fPreSkewX == rhsRec.fPreSkewX &&
297         lhsRec.fPost2x2[0][0] == rhsRec.fPost2x2[0][0] &&
298         lhsRec.fPost2x2[0][1] == rhsRec.fPost2x2[0][1] &&
299         lhsRec.fPost2x2[1][0] == rhsRec.fPost2x2[1][0] &&
300         lhsRec.fPost2x2[1][1] == rhsRec.fPost2x2[1][1];
301 }
302 
desperationSearchForImage(const SkDescriptor & desc,SkGlyph * glyph,SkStrike * targetCache)303 bool SkStrikeCache::desperationSearchForImage(const SkDescriptor& desc, SkGlyph* glyph,
304                                               SkStrike* targetCache) {
305     SkAutoSpinlock ac(fLock);
306 
307     SkGlyphID glyphID = glyph->getGlyphID();
308     for (Node* node = internalGetHead(); node != nullptr; node = node->fNext) {
309         if (loose_compare(node->fStrike.getDescriptor(), desc)) {
310             if (SkGlyph *fallback = node->fStrike.glyphOrNull(glyph->getPackedID())) {
311                 // This desperate-match node may disappear as soon as we drop fLock, so we
312                 // need to copy the glyph from node into this strike, including a
313                 // deep copy of the mask.
314                 targetCache->mergeGlyphAndImage(glyph->getPackedID(), *fallback);
315                 return true;
316             }
317 
318             // Look for any sub-pixel pos for this glyph, in case there is a pos mismatch.
319             if (const auto* fallback = node->fStrike.getCachedGlyphAnySubPix(glyphID)) {
320                 targetCache->mergeGlyphAndImage(glyph->getPackedID(), *fallback);
321                 return true;
322             }
323         }
324     }
325 
326     return false;
327 }
328 
desperationSearchForPath(const SkDescriptor & desc,SkGlyphID glyphID,SkPath * path)329 bool SkStrikeCache::desperationSearchForPath(
330         const SkDescriptor& desc, SkGlyphID glyphID, SkPath* path) {
331     SkAutoSpinlock ac(fLock);
332 
333     // The following is wrong there is subpixel positioning with paths...
334     // Paths are only ever at sub-pixel position (0,0), so we can just try that directly rather
335     // than try our packed position first then search all others on failure like for masks.
336     //
337     // This will have to search the sub-pixel positions too.
338     // There is also a problem with accounting for cache size with shared path data.
339     for (Node* node = internalGetHead(); node != nullptr; node = node->fNext) {
340         if (loose_compare(node->fStrike.getDescriptor(), desc)) {
341             if (SkGlyph *from = node->fStrike.glyphOrNull(SkPackedGlyphID{glyphID})) {
342                 if (from->setPathHasBeenCalled() && from->path() != nullptr) {
343                     // We can just copy the path out by value here, so no need to worry
344                     // about the lifetime of this desperate-match node.
345                     *path = *from->path();
346                     return true;
347                 }
348             }
349         }
350     }
351     return false;
352 }
353 
createStrikeExclusive(const SkDescriptor & desc,std::unique_ptr<SkScalerContext> scaler,SkFontMetrics * maybeMetrics,std::unique_ptr<SkStrikePinner> pinner)354 SkExclusiveStrikePtr SkStrikeCache::createStrikeExclusive(
355         const SkDescriptor& desc,
356         std::unique_ptr<SkScalerContext> scaler,
357         SkFontMetrics* maybeMetrics,
358         std::unique_ptr<SkStrikePinner> pinner)
359 {
360     return SkExclusiveStrikePtr(
361             this->createStrike(desc, std::move(scaler), maybeMetrics, std::move(pinner)));
362 }
363 
createStrike(const SkDescriptor & desc,std::unique_ptr<SkScalerContext> scaler,SkFontMetrics * maybeMetrics,std::unique_ptr<SkStrikePinner> pinner)364 auto SkStrikeCache::createStrike(
365         const SkDescriptor& desc,
366         std::unique_ptr<SkScalerContext> scaler,
367         SkFontMetrics* maybeMetrics,
368         std::unique_ptr<SkStrikePinner> pinner) -> Node* {
369     SkFontMetrics fontMetrics;
370     if (maybeMetrics != nullptr) {
371         fontMetrics = *maybeMetrics;
372     } else {
373         scaler->getFontMetrics(&fontMetrics);
374     }
375 
376     return new Node{this, desc, std::move(scaler), fontMetrics, std::move(pinner)};
377 }
378 
purgeAll()379 void SkStrikeCache::purgeAll() {
380     SkAutoSpinlock ac(fLock);
381     this->internalPurge(fTotalMemoryUsed);
382 }
383 
getTotalMemoryUsed() const384 size_t SkStrikeCache::getTotalMemoryUsed() const {
385     SkAutoSpinlock ac(fLock);
386     return fTotalMemoryUsed;
387 }
388 
getCacheCountUsed() const389 int SkStrikeCache::getCacheCountUsed() const {
390     SkAutoSpinlock ac(fLock);
391     return fCacheCount;
392 }
393 
getCacheCountLimit() const394 int SkStrikeCache::getCacheCountLimit() const {
395     SkAutoSpinlock ac(fLock);
396     return fCacheCountLimit;
397 }
398 
setCacheSizeLimit(size_t newLimit)399 size_t SkStrikeCache::setCacheSizeLimit(size_t newLimit) {
400     static const size_t minLimit = 256 * 1024;
401     if (newLimit < minLimit) {
402         newLimit = minLimit;
403     }
404 
405     SkAutoSpinlock ac(fLock);
406 
407     size_t prevLimit = fCacheSizeLimit;
408     fCacheSizeLimit = newLimit;
409     this->internalPurge();
410     return prevLimit;
411 }
412 
getCacheSizeLimit() const413 size_t  SkStrikeCache::getCacheSizeLimit() const {
414     SkAutoSpinlock ac(fLock);
415     return fCacheSizeLimit;
416 }
417 
setCacheCountLimit(int newCount)418 int SkStrikeCache::setCacheCountLimit(int newCount) {
419     if (newCount < 0) {
420         newCount = 0;
421     }
422 
423     SkAutoSpinlock ac(fLock);
424 
425     int prevCount = fCacheCountLimit;
426     fCacheCountLimit = newCount;
427     this->internalPurge();
428     return prevCount;
429 }
430 
getCachePointSizeLimit() const431 int SkStrikeCache::getCachePointSizeLimit() const {
432     SkAutoSpinlock ac(fLock);
433     return fPointSizeLimit;
434 }
435 
setCachePointSizeLimit(int newLimit)436 int SkStrikeCache::setCachePointSizeLimit(int newLimit) {
437     if (newLimit < 0) {
438         newLimit = 0;
439     }
440 
441     SkAutoSpinlock ac(fLock);
442 
443     int prevLimit = fPointSizeLimit;
444     fPointSizeLimit = newLimit;
445     return prevLimit;
446 }
447 
forEachStrike(std::function<void (const SkStrike &)> visitor) const448 void SkStrikeCache::forEachStrike(std::function<void(const SkStrike&)> visitor) const {
449     SkAutoSpinlock ac(fLock);
450 
451     this->validate();
452 
453     for (Node* node = this->internalGetHead(); node != nullptr; node = node->fNext) {
454         visitor(node->fStrike);
455     }
456 }
457 
internalPurge(size_t minBytesNeeded)458 size_t SkStrikeCache::internalPurge(size_t minBytesNeeded) {
459     this->validate();
460 
461     size_t bytesNeeded = 0;
462     if (fTotalMemoryUsed > fCacheSizeLimit) {
463         bytesNeeded = fTotalMemoryUsed - fCacheSizeLimit;
464     }
465     bytesNeeded = SkTMax(bytesNeeded, minBytesNeeded);
466     if (bytesNeeded) {
467         // no small purges!
468         bytesNeeded = SkTMax(bytesNeeded, fTotalMemoryUsed >> 2);
469     }
470 
471     int countNeeded = 0;
472     if (fCacheCount > fCacheCountLimit) {
473         countNeeded = fCacheCount - fCacheCountLimit;
474         // no small purges!
475         countNeeded = SkMax32(countNeeded, fCacheCount >> 2);
476     }
477 
478     // early exit
479     if (!countNeeded && !bytesNeeded) {
480         return 0;
481     }
482 
483     size_t  bytesFreed = 0;
484     int     countFreed = 0;
485 
486     // Start at the tail and proceed backwards deleting; the list is in LRU
487     // order, with unimportant entries at the tail.
488     Node* node = this->internalGetTail();
489     while (node != nullptr && (bytesFreed < bytesNeeded || countFreed < countNeeded)) {
490         Node* prev = node->fPrev;
491 
492         // Only delete if the strike is not pinned.
493         if (node->fPinner == nullptr || node->fPinner->canDelete()) {
494             bytesFreed += node->fStrike.getMemoryUsed();
495             countFreed += 1;
496             this->internalDetachCache(node);
497             delete node;
498         }
499         node = prev;
500     }
501 
502     this->validate();
503 
504 #ifdef SPEW_PURGE_STATUS
505     if (countFreed) {
506         SkDebugf("purging %dK from font cache [%d entries]\n",
507                  (int)(bytesFreed >> 10), countFreed);
508     }
509 #endif
510 
511     return bytesFreed;
512 }
513 
internalAttachToHead(Node * node)514 void SkStrikeCache::internalAttachToHead(Node* node) {
515     SkASSERT(nullptr == node->fPrev && nullptr == node->fNext);
516     if (fHead) {
517         fHead->fPrev = node;
518         node->fNext = fHead;
519     }
520     fHead = node;
521 
522     if (fTail == nullptr) {
523         fTail = node;
524     }
525 
526     fCacheCount += 1;
527     fTotalMemoryUsed += node->fStrike.getMemoryUsed();
528 }
529 
internalDetachCache(Node * node)530 void SkStrikeCache::internalDetachCache(Node* node) {
531     SkASSERT(fCacheCount > 0);
532     fCacheCount -= 1;
533     fTotalMemoryUsed -= node->fStrike.getMemoryUsed();
534 
535     if (node->fPrev) {
536         node->fPrev->fNext = node->fNext;
537     } else {
538         fHead = node->fNext;
539     }
540     if (node->fNext) {
541         node->fNext->fPrev = node->fPrev;
542     } else {
543         fTail = node->fPrev;
544     }
545     node->fPrev = node->fNext = nullptr;
546 }
547 
ValidateGlyphCacheDataSize()548 void SkStrikeCache::ValidateGlyphCacheDataSize() {
549 #ifdef SK_DEBUG
550     GlobalStrikeCache()->validateGlyphCacheDataSize();
551 #endif
552 }
553 
554 #ifdef SK_DEBUG
validateGlyphCacheDataSize() const555 void SkStrikeCache::validateGlyphCacheDataSize() const {
556     this->forEachStrike(
557             [](const SkStrike& cache) { cache.forceValidate();
558     });
559 }
560 #endif
561 
562 #ifdef SK_DEBUG
validate() const563 void SkStrikeCache::validate() const {
564     size_t computedBytes = 0;
565     int computedCount = 0;
566 
567     const Node* node = fHead;
568     while (node != nullptr) {
569         computedBytes += node->fStrike.getMemoryUsed();
570         computedCount += 1;
571         node = node->fNext;
572     }
573 
574     SkASSERTF(fCacheCount == computedCount, "fCacheCount: %d, computedCount: %d", fCacheCount,
575               computedCount);
576     SkASSERTF(fTotalMemoryUsed == computedBytes, "fTotalMemoryUsed: %d, computedBytes: %d",
577               fTotalMemoryUsed, computedBytes);
578 }
579 #endif
580 
581 ////////////////////////////////////////////////////////////////////////////////////////////////////
582