1 /* 2 * Copyright 2013 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 "SkMessageBus.h" 9 #include "SkMipMap.h" 10 #include "SkMutex.h" 11 #include "SkOpts.h" 12 #include "SkPixelRef.h" 13 #include "SkResourceCache.h" 14 #include "SkTraceMemoryDump.h" 15 16 #include <stddef.h> 17 #include <stdlib.h> 18 19 DECLARE_SKMESSAGEBUS_MESSAGE(SkResourceCache::PurgeSharedIDMessage) 20 21 // This can be defined by the caller's build system 22 //#define SK_USE_DISCARDABLE_SCALEDIMAGECACHE 23 24 #ifndef SK_DISCARDABLEMEMORY_SCALEDIMAGECACHE_COUNT_LIMIT 25 # define SK_DISCARDABLEMEMORY_SCALEDIMAGECACHE_COUNT_LIMIT 1024 26 #endif 27 28 #ifndef SK_DEFAULT_IMAGE_CACHE_LIMIT 29 #define SK_DEFAULT_IMAGE_CACHE_LIMIT (32 * 1024 * 1024) 30 #endif 31 32 void SkResourceCache::Key::init(void* nameSpace, uint64_t sharedID, size_t dataSize) { 33 SkASSERT(SkAlign4(dataSize) == dataSize); 34 35 // fCount32 and fHash are not hashed 36 static const int kUnhashedLocal32s = 2; // fCache32 + fHash 37 static const int kSharedIDLocal32s = 2; // fSharedID_lo + fSharedID_hi 38 static const int kHashedLocal32s = kSharedIDLocal32s + (sizeof(fNamespace) >> 2); 39 static const int kLocal32s = kUnhashedLocal32s + kHashedLocal32s; 40 41 static_assert(sizeof(Key) == (kLocal32s << 2), "unaccounted_key_locals"); 42 static_assert(sizeof(Key) == offsetof(Key, fNamespace) + sizeof(fNamespace), 43 "namespace_field_must_be_last"); 44 45 fCount32 = SkToS32(kLocal32s + (dataSize >> 2)); 46 fSharedID_lo = (uint32_t)sharedID; 47 fSharedID_hi = (uint32_t)(sharedID >> 32); 48 fNamespace = nameSpace; 49 // skip unhashed fields when computing the hash 50 fHash = SkOpts::hash(this->as32() + kUnhashedLocal32s, 51 (fCount32 - kUnhashedLocal32s) << 2); 52 } 53 54 #include "SkTHash.h" 55 56 namespace { 57 struct HashTraits { 58 static uint32_t Hash(const SkResourceCache::Key& key) { return key.hash(); } 59 static const SkResourceCache::Key& GetKey(const SkResourceCache::Rec* rec) { 60 return rec->getKey(); 61 } 62 }; 63 } 64 65 class SkResourceCache::Hash : 66 public SkTHashTable<SkResourceCache::Rec*, SkResourceCache::Key, HashTraits> {}; 67 68 69 /////////////////////////////////////////////////////////////////////////////// 70 71 void SkResourceCache::init() { 72 fHead = nullptr; 73 fTail = nullptr; 74 fHash = new Hash; 75 fTotalBytesUsed = 0; 76 fCount = 0; 77 fSingleAllocationByteLimit = 0; 78 fAllocator = nullptr; 79 80 // One of these should be explicit set by the caller after we return. 81 fTotalByteLimit = 0; 82 fDiscardableFactory = nullptr; 83 } 84 85 #include "SkDiscardableMemory.h" 86 87 class SkOneShotDiscardablePixelRef : public SkPixelRef { 88 public: 89 90 // Ownership of the discardablememory is transfered to the pixelref 91 // The pixelref will ref() the colortable (if not NULL), and unref() in destructor 92 SkOneShotDiscardablePixelRef(const SkImageInfo&, SkDiscardableMemory*, size_t rowBytes, 93 SkColorTable*); 94 ~SkOneShotDiscardablePixelRef() override; 95 96 protected: 97 bool onNewLockPixels(LockRec*) override; 98 void onUnlockPixels() override; 99 size_t getAllocatedSizeInBytes() const override; 100 101 SkDiscardableMemory* diagnostic_only_getDiscardable() const override { return fDM; } 102 103 private: 104 SkDiscardableMemory* fDM; 105 size_t fRB; 106 bool fFirstTime; 107 SkColorTable* fCTable; 108 109 typedef SkPixelRef INHERITED; 110 }; 111 112 SkOneShotDiscardablePixelRef::SkOneShotDiscardablePixelRef(const SkImageInfo& info, 113 SkDiscardableMemory* dm, 114 size_t rowBytes, 115 SkColorTable* ctable) 116 : INHERITED(info) 117 , fDM(dm) 118 , fRB(rowBytes) 119 , fCTable(ctable) 120 { 121 SkASSERT(dm->data()); 122 fFirstTime = true; 123 SkSafeRef(ctable); 124 } 125 126 SkOneShotDiscardablePixelRef::~SkOneShotDiscardablePixelRef() { 127 delete fDM; 128 SkSafeUnref(fCTable); 129 } 130 131 bool SkOneShotDiscardablePixelRef::onNewLockPixels(LockRec* rec) { 132 if (fFirstTime) { 133 // we're already locked 134 SkASSERT(fDM->data()); 135 fFirstTime = false; 136 goto SUCCESS; 137 } 138 139 // A previous call to onUnlock may have deleted our DM, so check for that 140 if (nullptr == fDM) { 141 return false; 142 } 143 144 if (!fDM->lock()) { 145 // since it failed, we delete it now, to free-up the resource 146 delete fDM; 147 fDM = nullptr; 148 return false; 149 } 150 151 SUCCESS: 152 rec->fPixels = fDM->data(); 153 rec->fColorTable = fCTable; 154 rec->fRowBytes = fRB; 155 return true; 156 } 157 158 void SkOneShotDiscardablePixelRef::onUnlockPixels() { 159 SkASSERT(!fFirstTime); 160 fDM->unlock(); 161 } 162 163 size_t SkOneShotDiscardablePixelRef::getAllocatedSizeInBytes() const { 164 return this->info().getSafeSize(fRB); 165 } 166 167 class SkResourceCacheDiscardableAllocator : public SkBitmap::Allocator { 168 public: 169 SkResourceCacheDiscardableAllocator(SkResourceCache::DiscardableFactory factory) { 170 SkASSERT(factory); 171 fFactory = factory; 172 } 173 174 bool allocPixelRef(SkBitmap*, SkColorTable*) override; 175 176 private: 177 SkResourceCache::DiscardableFactory fFactory; 178 }; 179 180 bool SkResourceCacheDiscardableAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) { 181 size_t size = bitmap->getSize(); 182 uint64_t size64 = bitmap->computeSize64(); 183 if (0 == size || size64 > (uint64_t)size) { 184 return false; 185 } 186 187 if (kIndex_8_SkColorType == bitmap->colorType()) { 188 if (!ctable) { 189 return false; 190 } 191 } else { 192 ctable = nullptr; 193 } 194 195 SkDiscardableMemory* dm = fFactory(size); 196 if (nullptr == dm) { 197 return false; 198 } 199 200 SkImageInfo info = bitmap->info(); 201 bitmap->setPixelRef( 202 sk_make_sp<SkOneShotDiscardablePixelRef>(info, dm, bitmap->rowBytes(), ctable), 0, 0); 203 bitmap->lockPixels(); 204 return bitmap->readyToDraw(); 205 } 206 207 SkResourceCache::SkResourceCache(DiscardableFactory factory) { 208 this->init(); 209 fDiscardableFactory = factory; 210 211 fAllocator = new SkResourceCacheDiscardableAllocator(factory); 212 } 213 214 SkResourceCache::SkResourceCache(size_t byteLimit) { 215 this->init(); 216 fTotalByteLimit = byteLimit; 217 } 218 219 SkResourceCache::~SkResourceCache() { 220 SkSafeUnref(fAllocator); 221 222 Rec* rec = fHead; 223 while (rec) { 224 Rec* next = rec->fNext; 225 delete rec; 226 rec = next; 227 } 228 delete fHash; 229 } 230 231 //////////////////////////////////////////////////////////////////////////////// 232 233 bool SkResourceCache::find(const Key& key, FindVisitor visitor, void* context) { 234 this->checkMessages(); 235 236 if (auto found = fHash->find(key)) { 237 Rec* rec = *found; 238 if (visitor(*rec, context)) { 239 this->moveToHead(rec); // for our LRU 240 return true; 241 } else { 242 this->remove(rec); // stale 243 return false; 244 } 245 } 246 return false; 247 } 248 249 static void make_size_str(size_t size, SkString* str) { 250 const char suffix[] = { 'b', 'k', 'm', 'g', 't', 0 }; 251 int i = 0; 252 while (suffix[i] && (size > 1024)) { 253 i += 1; 254 size >>= 10; 255 } 256 str->printf("%zu%c", size, suffix[i]); 257 } 258 259 static bool gDumpCacheTransactions; 260 261 void SkResourceCache::add(Rec* rec) { 262 this->checkMessages(); 263 264 SkASSERT(rec); 265 // See if we already have this key (racy inserts, etc.) 266 if (nullptr != fHash->find(rec->getKey())) { 267 delete rec; 268 return; 269 } 270 271 this->addToHead(rec); 272 fHash->set(rec); 273 274 if (gDumpCacheTransactions) { 275 SkString bytesStr, totalStr; 276 make_size_str(rec->bytesUsed(), &bytesStr); 277 make_size_str(fTotalBytesUsed, &totalStr); 278 SkDebugf("RC: add %5s %12p key %08x -- total %5s, count %d\n", 279 bytesStr.c_str(), rec, rec->getHash(), totalStr.c_str(), fCount); 280 } 281 282 // since the new rec may push us over-budget, we perform a purge check now 283 this->purgeAsNeeded(); 284 } 285 286 void SkResourceCache::remove(Rec* rec) { 287 size_t used = rec->bytesUsed(); 288 SkASSERT(used <= fTotalBytesUsed); 289 290 this->release(rec); 291 fHash->remove(rec->getKey()); 292 293 fTotalBytesUsed -= used; 294 fCount -= 1; 295 296 if (gDumpCacheTransactions) { 297 SkString bytesStr, totalStr; 298 make_size_str(used, &bytesStr); 299 make_size_str(fTotalBytesUsed, &totalStr); 300 SkDebugf("RC: remove %5s %12p key %08x -- total %5s, count %d\n", 301 bytesStr.c_str(), rec, rec->getHash(), totalStr.c_str(), fCount); 302 } 303 304 delete rec; 305 } 306 307 void SkResourceCache::purgeAsNeeded(bool forcePurge) { 308 size_t byteLimit; 309 int countLimit; 310 311 if (fDiscardableFactory) { 312 countLimit = SK_DISCARDABLEMEMORY_SCALEDIMAGECACHE_COUNT_LIMIT; 313 byteLimit = SK_MaxU32; // no limit based on bytes 314 } else { 315 countLimit = SK_MaxS32; // no limit based on count 316 byteLimit = fTotalByteLimit; 317 } 318 319 Rec* rec = fTail; 320 while (rec) { 321 if (!forcePurge && fTotalBytesUsed < byteLimit && fCount < countLimit) { 322 break; 323 } 324 325 Rec* prev = rec->fPrev; 326 this->remove(rec); 327 rec = prev; 328 } 329 } 330 331 //#define SK_TRACK_PURGE_SHAREDID_HITRATE 332 333 #ifdef SK_TRACK_PURGE_SHAREDID_HITRATE 334 static int gPurgeCallCounter; 335 static int gPurgeHitCounter; 336 #endif 337 338 void SkResourceCache::purgeSharedID(uint64_t sharedID) { 339 if (0 == sharedID) { 340 return; 341 } 342 343 #ifdef SK_TRACK_PURGE_SHAREDID_HITRATE 344 gPurgeCallCounter += 1; 345 bool found = false; 346 #endif 347 // go backwards, just like purgeAsNeeded, just to make the code similar. 348 // could iterate either direction and still be correct. 349 Rec* rec = fTail; 350 while (rec) { 351 Rec* prev = rec->fPrev; 352 if (rec->getKey().getSharedID() == sharedID) { 353 // SkDebugf("purgeSharedID id=%llx rec=%p\n", sharedID, rec); 354 this->remove(rec); 355 #ifdef SK_TRACK_PURGE_SHAREDID_HITRATE 356 found = true; 357 #endif 358 } 359 rec = prev; 360 } 361 362 #ifdef SK_TRACK_PURGE_SHAREDID_HITRATE 363 if (found) { 364 gPurgeHitCounter += 1; 365 } 366 367 SkDebugf("PurgeShared calls=%d hits=%d rate=%g\n", gPurgeCallCounter, gPurgeHitCounter, 368 gPurgeHitCounter * 100.0 / gPurgeCallCounter); 369 #endif 370 } 371 372 void SkResourceCache::visitAll(Visitor visitor, void* context) { 373 // go backwards, just like purgeAsNeeded, just to make the code similar. 374 // could iterate either direction and still be correct. 375 Rec* rec = fTail; 376 while (rec) { 377 visitor(*rec, context); 378 rec = rec->fPrev; 379 } 380 } 381 382 /////////////////////////////////////////////////////////////////////////////////////////////////// 383 384 size_t SkResourceCache::setTotalByteLimit(size_t newLimit) { 385 size_t prevLimit = fTotalByteLimit; 386 fTotalByteLimit = newLimit; 387 if (newLimit < prevLimit) { 388 this->purgeAsNeeded(); 389 } 390 return prevLimit; 391 } 392 393 SkCachedData* SkResourceCache::newCachedData(size_t bytes) { 394 this->checkMessages(); 395 396 if (fDiscardableFactory) { 397 SkDiscardableMemory* dm = fDiscardableFactory(bytes); 398 return dm ? new SkCachedData(bytes, dm) : nullptr; 399 } else { 400 return new SkCachedData(sk_malloc_throw(bytes), bytes); 401 } 402 } 403 404 /////////////////////////////////////////////////////////////////////////////// 405 406 void SkResourceCache::release(Rec* rec) { 407 Rec* prev = rec->fPrev; 408 Rec* next = rec->fNext; 409 410 if (!prev) { 411 SkASSERT(fHead == rec); 412 fHead = next; 413 } else { 414 prev->fNext = next; 415 } 416 417 if (!next) { 418 fTail = prev; 419 } else { 420 next->fPrev = prev; 421 } 422 423 rec->fNext = rec->fPrev = nullptr; 424 } 425 426 void SkResourceCache::moveToHead(Rec* rec) { 427 if (fHead == rec) { 428 return; 429 } 430 431 SkASSERT(fHead); 432 SkASSERT(fTail); 433 434 this->validate(); 435 436 this->release(rec); 437 438 fHead->fPrev = rec; 439 rec->fNext = fHead; 440 fHead = rec; 441 442 this->validate(); 443 } 444 445 void SkResourceCache::addToHead(Rec* rec) { 446 this->validate(); 447 448 rec->fPrev = nullptr; 449 rec->fNext = fHead; 450 if (fHead) { 451 fHead->fPrev = rec; 452 } 453 fHead = rec; 454 if (!fTail) { 455 fTail = rec; 456 } 457 fTotalBytesUsed += rec->bytesUsed(); 458 fCount += 1; 459 460 this->validate(); 461 } 462 463 /////////////////////////////////////////////////////////////////////////////// 464 465 #ifdef SK_DEBUG 466 void SkResourceCache::validate() const { 467 if (nullptr == fHead) { 468 SkASSERT(nullptr == fTail); 469 SkASSERT(0 == fTotalBytesUsed); 470 return; 471 } 472 473 if (fHead == fTail) { 474 SkASSERT(nullptr == fHead->fPrev); 475 SkASSERT(nullptr == fHead->fNext); 476 SkASSERT(fHead->bytesUsed() == fTotalBytesUsed); 477 return; 478 } 479 480 SkASSERT(nullptr == fHead->fPrev); 481 SkASSERT(fHead->fNext); 482 SkASSERT(nullptr == fTail->fNext); 483 SkASSERT(fTail->fPrev); 484 485 size_t used = 0; 486 int count = 0; 487 const Rec* rec = fHead; 488 while (rec) { 489 count += 1; 490 used += rec->bytesUsed(); 491 SkASSERT(used <= fTotalBytesUsed); 492 rec = rec->fNext; 493 } 494 SkASSERT(fCount == count); 495 496 rec = fTail; 497 while (rec) { 498 SkASSERT(count > 0); 499 count -= 1; 500 SkASSERT(used >= rec->bytesUsed()); 501 used -= rec->bytesUsed(); 502 rec = rec->fPrev; 503 } 504 505 SkASSERT(0 == count); 506 SkASSERT(0 == used); 507 } 508 #endif 509 510 void SkResourceCache::dump() const { 511 this->validate(); 512 513 SkDebugf("SkResourceCache: count=%d bytes=%d %s\n", 514 fCount, fTotalBytesUsed, fDiscardableFactory ? "discardable" : "malloc"); 515 } 516 517 size_t SkResourceCache::setSingleAllocationByteLimit(size_t newLimit) { 518 size_t oldLimit = fSingleAllocationByteLimit; 519 fSingleAllocationByteLimit = newLimit; 520 return oldLimit; 521 } 522 523 size_t SkResourceCache::getSingleAllocationByteLimit() const { 524 return fSingleAllocationByteLimit; 525 } 526 527 size_t SkResourceCache::getEffectiveSingleAllocationByteLimit() const { 528 // fSingleAllocationByteLimit == 0 means the caller is asking for our default 529 size_t limit = fSingleAllocationByteLimit; 530 531 // if we're not discardable (i.e. we are fixed-budget) then cap the single-limit 532 // to our budget. 533 if (nullptr == fDiscardableFactory) { 534 if (0 == limit) { 535 limit = fTotalByteLimit; 536 } else { 537 limit = SkTMin(limit, fTotalByteLimit); 538 } 539 } 540 return limit; 541 } 542 543 void SkResourceCache::checkMessages() { 544 SkTArray<PurgeSharedIDMessage> msgs; 545 fPurgeSharedIDInbox.poll(&msgs); 546 for (int i = 0; i < msgs.count(); ++i) { 547 this->purgeSharedID(msgs[i].fSharedID); 548 } 549 } 550 551 /////////////////////////////////////////////////////////////////////////////// 552 553 SK_DECLARE_STATIC_MUTEX(gMutex); 554 static SkResourceCache* gResourceCache = nullptr; 555 556 /** Must hold gMutex when calling. */ 557 static SkResourceCache* get_cache() { 558 // gMutex is always held when this is called, so we don't need to be fancy in here. 559 gMutex.assertHeld(); 560 if (nullptr == gResourceCache) { 561 #ifdef SK_USE_DISCARDABLE_SCALEDIMAGECACHE 562 gResourceCache = new SkResourceCache(SkDiscardableMemory::Create); 563 #else 564 gResourceCache = new SkResourceCache(SK_DEFAULT_IMAGE_CACHE_LIMIT); 565 #endif 566 } 567 return gResourceCache; 568 } 569 570 size_t SkResourceCache::GetTotalBytesUsed() { 571 SkAutoMutexAcquire am(gMutex); 572 return get_cache()->getTotalBytesUsed(); 573 } 574 575 size_t SkResourceCache::GetTotalByteLimit() { 576 SkAutoMutexAcquire am(gMutex); 577 return get_cache()->getTotalByteLimit(); 578 } 579 580 size_t SkResourceCache::SetTotalByteLimit(size_t newLimit) { 581 SkAutoMutexAcquire am(gMutex); 582 return get_cache()->setTotalByteLimit(newLimit); 583 } 584 585 SkResourceCache::DiscardableFactory SkResourceCache::GetDiscardableFactory() { 586 SkAutoMutexAcquire am(gMutex); 587 return get_cache()->discardableFactory(); 588 } 589 590 SkBitmap::Allocator* SkResourceCache::GetAllocator() { 591 SkAutoMutexAcquire am(gMutex); 592 return get_cache()->allocator(); 593 } 594 595 SkCachedData* SkResourceCache::NewCachedData(size_t bytes) { 596 SkAutoMutexAcquire am(gMutex); 597 return get_cache()->newCachedData(bytes); 598 } 599 600 void SkResourceCache::Dump() { 601 SkAutoMutexAcquire am(gMutex); 602 get_cache()->dump(); 603 } 604 605 size_t SkResourceCache::SetSingleAllocationByteLimit(size_t size) { 606 SkAutoMutexAcquire am(gMutex); 607 return get_cache()->setSingleAllocationByteLimit(size); 608 } 609 610 size_t SkResourceCache::GetSingleAllocationByteLimit() { 611 SkAutoMutexAcquire am(gMutex); 612 return get_cache()->getSingleAllocationByteLimit(); 613 } 614 615 size_t SkResourceCache::GetEffectiveSingleAllocationByteLimit() { 616 SkAutoMutexAcquire am(gMutex); 617 return get_cache()->getEffectiveSingleAllocationByteLimit(); 618 } 619 620 void SkResourceCache::PurgeAll() { 621 SkAutoMutexAcquire am(gMutex); 622 return get_cache()->purgeAll(); 623 } 624 625 bool SkResourceCache::Find(const Key& key, FindVisitor visitor, void* context) { 626 SkAutoMutexAcquire am(gMutex); 627 return get_cache()->find(key, visitor, context); 628 } 629 630 void SkResourceCache::Add(Rec* rec) { 631 SkAutoMutexAcquire am(gMutex); 632 get_cache()->add(rec); 633 } 634 635 void SkResourceCache::VisitAll(Visitor visitor, void* context) { 636 SkAutoMutexAcquire am(gMutex); 637 get_cache()->visitAll(visitor, context); 638 } 639 640 void SkResourceCache::PostPurgeSharedID(uint64_t sharedID) { 641 if (sharedID) { 642 SkMessageBus<PurgeSharedIDMessage>::Post(PurgeSharedIDMessage(sharedID)); 643 } 644 } 645 646 /////////////////////////////////////////////////////////////////////////////// 647 648 #include "SkGraphics.h" 649 #include "SkImageFilter.h" 650 651 size_t SkGraphics::GetResourceCacheTotalBytesUsed() { 652 return SkResourceCache::GetTotalBytesUsed(); 653 } 654 655 size_t SkGraphics::GetResourceCacheTotalByteLimit() { 656 return SkResourceCache::GetTotalByteLimit(); 657 } 658 659 size_t SkGraphics::SetResourceCacheTotalByteLimit(size_t newLimit) { 660 return SkResourceCache::SetTotalByteLimit(newLimit); 661 } 662 663 size_t SkGraphics::GetResourceCacheSingleAllocationByteLimit() { 664 return SkResourceCache::GetSingleAllocationByteLimit(); 665 } 666 667 size_t SkGraphics::SetResourceCacheSingleAllocationByteLimit(size_t newLimit) { 668 return SkResourceCache::SetSingleAllocationByteLimit(newLimit); 669 } 670 671 void SkGraphics::PurgeResourceCache() { 672 SkImageFilter::PurgeCache(); 673 return SkResourceCache::PurgeAll(); 674 } 675 676 ///////////// 677 678 static void dump_visitor(const SkResourceCache::Rec& rec, void*) { 679 SkDebugf("RC: %12s bytes %9lu discardable %p\n", 680 rec.getCategory(), rec.bytesUsed(), rec.diagnostic_only_getDiscardable()); 681 } 682 683 void SkResourceCache::TestDumpMemoryStatistics() { 684 VisitAll(dump_visitor, nullptr); 685 } 686 687 static void sk_trace_dump_visitor(const SkResourceCache::Rec& rec, void* context) { 688 SkTraceMemoryDump* dump = static_cast<SkTraceMemoryDump*>(context); 689 SkString dumpName = SkStringPrintf("skia/sk_resource_cache/%s_%p", rec.getCategory(), &rec); 690 SkDiscardableMemory* discardable = rec.diagnostic_only_getDiscardable(); 691 if (discardable) { 692 dump->setDiscardableMemoryBacking(dumpName.c_str(), *discardable); 693 694 // The discardable memory size will be calculated by dumper, but we also dump what we think 695 // the size of object in memory is irrespective of whether object is live or dead. 696 dump->dumpNumericValue(dumpName.c_str(), "discardable_size", "bytes", rec.bytesUsed()); 697 } else { 698 dump->dumpNumericValue(dumpName.c_str(), "size", "bytes", rec.bytesUsed()); 699 dump->setMemoryBacking(dumpName.c_str(), "malloc", nullptr); 700 } 701 } 702 703 void SkResourceCache::DumpMemoryStatistics(SkTraceMemoryDump* dump) { 704 // Since resource could be backed by malloc or discardable, the cache always dumps detailed 705 // stats to be accurate. 706 VisitAll(sk_trace_dump_visitor, dump); 707 } 708