| /* |
| * Copyright (C) 2013, 2014 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "IOSurfacePool.h" |
| |
| #if HAVE(IOSURFACE) |
| |
| #include "DestinationColorSpace.h" |
| #include "GraphicsContextCG.h" |
| #include <CoreGraphics/CoreGraphics.h> |
| #include <wtf/NeverDestroyed.h> |
| #include <wtf/text/TextStream.h> |
| |
| static const Seconds collectionInterval { 500_ms }; |
| static const Seconds surfaceAgeBeforeMarkingPurgeable { 2_s }; |
| |
| #define ENABLE_IOSURFACE_POOL_STATISTICS 0 |
| #if ENABLE_IOSURFACE_POOL_STATISTICS |
| #define DUMP_POOL_STATISTICS(commands) do { ALWAYS_LOG_WITH_STREAM(commands); } while (0); |
| #else |
| #define DUMP_POOL_STATISTICS(commands) ((void)0) |
| #endif |
| |
| namespace WebCore { |
| |
| IOSurfacePool::IOSurfacePool() |
| : m_collectionTimer(RunLoop::main(), this, &IOSurfacePool::collectionTimerFired) |
| { |
| } |
| |
| IOSurfacePool& IOSurfacePool::sharedPool() |
| { |
| static LazyNeverDestroyed<IOSurfacePool> pool; |
| static std::once_flag s_onceFlag; |
| std::call_once(s_onceFlag, [] { |
| pool.construct(); |
| }); |
| return pool; |
| } |
| |
| static bool surfaceMatchesParameters(IOSurface& surface, IntSize requestedSize, const DestinationColorSpace& colorSpace, IOSurface::Format format) |
| { |
| if (format != surface.format()) |
| return false; |
| if (colorSpace != surface.colorSpace()) |
| return false; |
| if (requestedSize != surface.size()) |
| return false; |
| return true; |
| } |
| |
| void IOSurfacePool::willAddSurface(IOSurface& surface, bool inUse) |
| { |
| CachedSurfaceDetails& details = m_surfaceDetails.add(&surface, CachedSurfaceDetails()).iterator->value; |
| details.resetLastUseTime(); |
| |
| surface.releaseGraphicsContext(); |
| |
| size_t surfaceBytes = surface.totalBytes(); |
| |
| evict(surfaceBytes); |
| |
| m_bytesCached += surfaceBytes; |
| if (inUse) |
| m_inUseBytesCached += surfaceBytes; |
| } |
| |
| void IOSurfacePool::didRemoveSurface(IOSurface& surface, bool inUse) |
| { |
| size_t surfaceBytes = surface.totalBytes(); |
| m_bytesCached -= surfaceBytes; |
| if (inUse) |
| m_inUseBytesCached -= surfaceBytes; |
| |
| m_surfaceDetails.remove(&surface); |
| } |
| |
| void IOSurfacePool::didUseSurfaceOfSize(IntSize size) |
| { |
| m_sizesInPruneOrder.remove(m_sizesInPruneOrder.reverseFind(size)); |
| m_sizesInPruneOrder.append(size); |
| } |
| |
| std::unique_ptr<IOSurface> IOSurfacePool::takeSurface(IntSize size, const DestinationColorSpace& colorSpace, IOSurface::Format format) |
| { |
| Locker locker { m_lock }; |
| CachedSurfaceMap::iterator mapIter = m_cachedSurfaces.find(size); |
| |
| if (mapIter == m_cachedSurfaces.end()) { |
| DUMP_POOL_STATISTICS(stream << "IOSurfacePool::takeSurface - failed to find surface matching size " << size << " color space " << colorSpace << " format " << format << "\n" << poolStatistics()); |
| return nullptr; |
| } |
| |
| for (auto surfaceIter = mapIter->value.begin(); surfaceIter != mapIter->value.end(); ++surfaceIter) { |
| if (!surfaceMatchesParameters(*surfaceIter->get(), size, colorSpace, format)) |
| continue; |
| |
| auto surface = WTFMove(*surfaceIter); |
| mapIter->value.remove(surfaceIter); |
| |
| didUseSurfaceOfSize(size); |
| |
| if (mapIter->value.isEmpty()) { |
| m_cachedSurfaces.remove(mapIter); |
| m_sizesInPruneOrder.removeLast(); |
| } |
| |
| didRemoveSurface(*surface, false); |
| |
| surface->setVolatile(false); |
| |
| DUMP_POOL_STATISTICS(stream << "IOSurfacePool::takeSurface - taking surface " << surface.get() << " with size " << size << " color space " << colorSpace << " format " << format << "\n" << poolStatistics()); |
| return surface; |
| } |
| |
| // Some of the in-use surfaces may no longer actually be in-use, but we haven't moved them over yet. |
| for (auto surfaceIter = m_inUseSurfaces.begin(); surfaceIter != m_inUseSurfaces.end(); ++surfaceIter) { |
| if (!surfaceMatchesParameters(*surfaceIter->get(), size, colorSpace, format)) |
| continue; |
| if (surfaceIter->get()->isInUse()) |
| continue; |
| |
| auto surface = WTFMove(*surfaceIter); |
| m_inUseSurfaces.remove(surfaceIter); |
| didRemoveSurface(*surface, true); |
| |
| surface->setVolatile(false); |
| |
| DUMP_POOL_STATISTICS(stream << "IOSurfacePool::takeSurface - taking surface " << surface.get() << " with size " << size << " color space " << colorSpace << " format " << format << "\n" << poolStatistics()); |
| return surface; |
| } |
| |
| DUMP_POOL_STATISTICS(stream << "IOSurfacePool::takeSurface - failing\n" << poolStatistics()); |
| return nullptr; |
| } |
| |
| bool IOSurfacePool::shouldCacheSurface(const IOSurface& surface) const |
| { |
| if (surface.totalBytes() > m_maximumBytesCached) |
| return false; |
| |
| // There's no reason to pool empty surfaces; we should never allocate them in the first place. |
| // This also covers isZero(), which would cause trouble when used as the key in m_cachedSurfaces. |
| if (surface.size().isEmpty()) |
| return false; |
| |
| return true; |
| } |
| |
| void IOSurfacePool::addSurface(std::unique_ptr<IOSurface> surface) |
| { |
| Locker locker { m_lock }; |
| if (!shouldCacheSurface(*surface)) |
| return; |
| |
| bool surfaceIsInUse = surface->isInUse(); |
| |
| willAddSurface(*surface, surfaceIsInUse); |
| |
| if (surfaceIsInUse) { |
| m_inUseSurfaces.prepend(WTFMove(surface)); |
| scheduleCollectionTimer(); |
| DUMP_POOL_STATISTICS(stream << "addSurface - in-use\n" << poolStatistics()); |
| return; |
| } |
| |
| insertSurfaceIntoPool(WTFMove(surface)); |
| DUMP_POOL_STATISTICS(stream << "addSurface\n" << poolStatistics()); |
| } |
| |
| void IOSurfacePool::insertSurfaceIntoPool(std::unique_ptr<IOSurface> surface) |
| { |
| IntSize surfaceSize = surface->size(); |
| auto insertedTuple = m_cachedSurfaces.add(surfaceSize, CachedSurfaceQueue()); |
| insertedTuple.iterator->value.prepend(WTFMove(surface)); |
| if (!insertedTuple.isNewEntry) |
| m_sizesInPruneOrder.remove(m_sizesInPruneOrder.reverseFind(surfaceSize)); |
| m_sizesInPruneOrder.append(surfaceSize); |
| |
| scheduleCollectionTimer(); |
| } |
| |
| void IOSurfacePool::setPoolSize(size_t poolSizeInBytes) |
| { |
| Locker locker { m_lock }; |
| m_maximumBytesCached = poolSizeInBytes; |
| evict(0); |
| } |
| |
| void IOSurfacePool::tryEvictInUseSurface() |
| { |
| if (m_inUseSurfaces.isEmpty()) |
| return; |
| |
| auto surface = m_inUseSurfaces.takeLast(); |
| didRemoveSurface(*surface, true); |
| } |
| |
| void IOSurfacePool::tryEvictOldestCachedSurface() |
| { |
| if (m_cachedSurfaces.isEmpty()) |
| return; |
| |
| if (m_sizesInPruneOrder.isEmpty()) |
| return; |
| |
| CachedSurfaceMap::iterator surfaceQueueIter = m_cachedSurfaces.find(m_sizesInPruneOrder.first()); |
| ASSERT(!surfaceQueueIter->value.isEmpty()); |
| auto surface = surfaceQueueIter->value.takeLast(); |
| didRemoveSurface(*surface, false); |
| |
| if (surfaceQueueIter->value.isEmpty()) { |
| m_cachedSurfaces.remove(surfaceQueueIter); |
| m_sizesInPruneOrder.remove(0); |
| } |
| } |
| |
| void IOSurfacePool::evict(size_t additionalSize) |
| { |
| DUMP_POOL_STATISTICS(stream << "before evict\n" << poolStatistics()); |
| |
| if (additionalSize >= m_maximumBytesCached) { |
| discardAllSurfacesInternal(); |
| DUMP_POOL_STATISTICS(stream << "after evict all\n" << poolStatistics()); |
| return; |
| } |
| |
| // FIXME: Perhaps purgeable surfaces should count less against the cap? |
| // We don't want to end up with a ton of empty (purged) surfaces, though, as that would defeat the purpose of the pool. |
| size_t targetSize = m_maximumBytesCached - additionalSize; |
| |
| // Interleave eviction of old cached surfaces and more recent in-use surfaces. |
| // In-use surfaces are more recently used, but less useful in the pool, as they aren't |
| // immediately available when requested. |
| while (m_bytesCached > targetSize) { |
| tryEvictOldestCachedSurface(); |
| |
| if (m_inUseBytesCached > maximumInUseBytes || m_bytesCached > targetSize) |
| tryEvictInUseSurface(); |
| } |
| |
| while (m_inUseBytesCached > maximumInUseBytes || m_bytesCached > targetSize) |
| tryEvictInUseSurface(); |
| |
| DUMP_POOL_STATISTICS(stream << "after evict\n" << poolStatistics()); |
| } |
| |
| void IOSurfacePool::collectInUseSurfaces() |
| { |
| CachedSurfaceQueue newInUseSurfaces; |
| for (CachedSurfaceQueue::iterator surfaceIter = m_inUseSurfaces.begin(); surfaceIter != m_inUseSurfaces.end(); ++surfaceIter) { |
| IOSurface* surface = surfaceIter->get(); |
| if (surface->isInUse()) { |
| newInUseSurfaces.append(WTFMove(*surfaceIter)); |
| continue; |
| } |
| |
| m_inUseBytesCached -= surface->totalBytes(); |
| insertSurfaceIntoPool(WTFMove(*surfaceIter)); |
| } |
| |
| m_inUseSurfaces = WTFMove(newInUseSurfaces); |
| } |
| |
| bool IOSurfacePool::markOlderSurfacesPurgeable() |
| { |
| bool markedAllSurfaces = true; |
| auto markTime = MonotonicTime::now(); |
| |
| for (auto& surfaceAndDetails : m_surfaceDetails) { |
| if (surfaceAndDetails.value.hasMarkedPurgeable) |
| continue; |
| |
| if (markTime - surfaceAndDetails.value.lastUseTime < surfaceAgeBeforeMarkingPurgeable) { |
| markedAllSurfaces = false; |
| continue; |
| } |
| |
| surfaceAndDetails.key->setVolatile(true); |
| surfaceAndDetails.value.hasMarkedPurgeable = true; |
| } |
| |
| return markedAllSurfaces; |
| } |
| |
| void IOSurfacePool::collectionTimerFired() |
| { |
| Locker locker { m_lock }; |
| collectInUseSurfaces(); |
| bool markedAllSurfaces = markOlderSurfacesPurgeable(); |
| |
| if (!m_inUseSurfaces.size() && markedAllSurfaces) |
| m_collectionTimer.stop(); |
| |
| platformGarbageCollectNow(); |
| DUMP_POOL_STATISTICS(stream << "collectionTimerFired\n" << poolStatistics()); |
| } |
| |
| void IOSurfacePool::scheduleCollectionTimer() |
| { |
| if (!m_collectionTimer.isActive()) |
| m_collectionTimer.startRepeating(collectionInterval); |
| } |
| |
| void IOSurfacePool::discardAllSurfaces() |
| { |
| Locker locker { m_lock }; |
| discardAllSurfacesInternal(); |
| } |
| |
| void IOSurfacePool::discardAllSurfacesInternal() |
| { |
| m_bytesCached = 0; |
| m_inUseBytesCached = 0; |
| m_surfaceDetails.clear(); |
| m_cachedSurfaces.clear(); |
| m_inUseSurfaces.clear(); |
| m_sizesInPruneOrder.clear(); |
| m_collectionTimer.stop(); |
| platformGarbageCollectNow(); |
| } |
| |
| String IOSurfacePool::poolStatistics() const |
| { |
| #if ENABLE_IOSURFACE_POOL_STATISTICS |
| TextStream stream; |
| stream << "Process " << getpid() << " IOSurfacePool Statistics:\n"; |
| |
| unsigned totalSurfaces = 0; |
| size_t totalSize = 0; |
| size_t totalPurgeableSize = 0; |
| |
| for (const auto& keyAndSurfaces : m_cachedSurfaces) { |
| ASSERT(!keyAndSurfaces.value.isEmpty()); |
| size_t queueSize = 0; |
| size_t queuePurgeableSize = 0; |
| for (const auto& surface : keyAndSurfaces.value) { |
| size_t surfaceBytes = surface->totalBytes(); |
| |
| totalSurfaces++; |
| queueSize += surfaceBytes; |
| |
| if (surface->isVolatile()) |
| queuePurgeableSize += surfaceBytes; |
| } |
| |
| totalSize += queueSize; |
| totalPurgeableSize += queuePurgeableSize; |
| |
| stream << " " << keyAndSurfaces.key << ": " << keyAndSurfaces.value.size() << " surfaces for " << queueSize / (1024.0 * 1024.0) << " MB (" << queuePurgeableSize / (1024.0 * 1024.0) << " MB purgeable)"; |
| } |
| |
| size_t inUseSize = 0; |
| for (const auto& surface : m_inUseSurfaces) { |
| totalSurfaces++; |
| inUseSize += surface->totalBytes(); |
| } |
| |
| totalSize += inUseSize; |
| stream << " IN USE: " << m_inUseSurfaces.size() << " surfaces for " << inUseSize / (1024.0 * 1024.0) << " MB"; |
| |
| // FIXME: Should move consistency checks elsewhere, and always perform them in debug builds. |
| ASSERT(m_bytesCached == totalSize); |
| ASSERT(m_bytesCached <= m_maximumBytesCached); |
| |
| stream << " TOTAL: " << totalSurfaces << " surfaces for " << totalSize / (1024.0 * 1024.0) << " MB (" << totalPurgeableSize / (1024.0 * 1024.0) << " MB purgeable)\n"; |
| return stream.release(); |
| #else |
| return emptyString(); |
| #endif |
| } |
| |
| } |
| #endif // HAVE(IOSURFACE) |