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