Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(422)

Unified Diff: third_party/WebKit/Source/platform/graphics/BitmapImage.cpp

Issue 1925533003: High CPU and increased memory usage fix for high-res (GIF, WEBP...) animations. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: third_party/WebKit/Source/platform/graphics/BitmapImage.cpp
diff --git a/third_party/WebKit/Source/platform/graphics/BitmapImage.cpp b/third_party/WebKit/Source/platform/graphics/BitmapImage.cpp
index fad71475e10d2082229bafd35d958a839138e19c..be7d007001675171d26b91a9861a95177b6b0d0e 100644
--- a/third_party/WebKit/Source/platform/graphics/BitmapImage.cpp
+++ b/third_party/WebKit/Source/platform/graphics/BitmapImage.cpp
@@ -26,6 +26,7 @@
#include "platform/graphics/BitmapImage.h"
+#include "base/containers/mru_cache.h"
#include "platform/PlatformInstrumentation.h"
#include "platform/Timer.h"
#include "platform/TraceEvent.h"
@@ -36,9 +37,81 @@
#include "platform/graphics/StaticBitmapImage.h"
#include "platform/graphics/skia/SkiaUtils.h"
#include "third_party/skia/include/core/SkCanvas.h"
+#include "wtf/CheckedNumeric.h"
#include "wtf/PassRefPtr.h"
#include "wtf/text/WTFString.h"
+namespace {
+
+const size_t kMaxAnimationCacheSize = 1024 * 1024 * 96;
+const size_t kMaxCacheSizeForAnimation = 1024 * 1024 * 24;
Peter Kasting 2016/04/28 23:07:54 These limits seem not only arbitrary but rather al
aleksandar.stojiljkovic 2016/04/29 17:17:43 Done. Second patch removes this code and also redu
+
+using ImageMRUCache = base::HashingMRUCache<blink::BitmapImage*, size_t>;
+
+class AnimatedBitmapImageCache : public ImageMRUCache {
+public:
+ static AnimatedBitmapImageCache& AnimatedBitmapImageCache::instance()
+ {
+ DEFINE_THREAD_SAFE_STATIC_LOCAL(AnimatedBitmapImageCache, cache, new AnimatedBitmapImageCache);
+ return cache;
+ }
+
+ void EraseBitmap(blink::BitmapImage* pBitmapImage)
Peter Kasting 2016/04/28 23:07:54 Nit: Use Google style for param naming, no Hungari
aleksandar.stojiljkovic 2016/04/29 17:17:43 Done. removed the code in second patch.
+ {
+ auto image_it = Peek(pBitmapImage);
+ if (image_it != end()) {
+ m_cacheSize -= image_it->second;
+ Erase(image_it);
+ }
+ }
+
+ void makeSpaceInCache(size_t bytesAboutToAdd)
+ {
+ size_t maxSize = m_maxCacheSize - std::min(bytesAboutToAdd, m_maxCacheSize);
+ if (m_cacheSize <= maxSize)
+ return;
+ for (auto it = rbegin(); m_cacheSize > maxSize && it != rend(); it = rbegin())
+ it->first->destroyDecodedData();
+ }
+
+ void touch(blink::BitmapImage* pBitmapImage)
+ {
+ // Mark that bitmap image was used in MRU cache.
+ Get(pBitmapImage);
+ }
+
+ void cachedSizeChanged(blink::BitmapImage* pBitmapImage, int delta)
+ {
+ auto image_it = Peek(pBitmapImage);
+ if (image_it != end()) {
+ base::CheckedNumeric<intptr_t> signedFramesSize(image_it->second);
Peter Kasting 2016/04/28 23:07:54 intptr_t is not an appropriate type to use for a s
aleksandar.stojiljkovic 2016/04/29 17:17:43 Done. Removed the code in second patch. Needs to b
+ signedFramesSize += delta;
+ image_it->second = safeCast<size_t>(signedFramesSize.ValueOrDie());
+ }
+ base::CheckedNumeric<intptr_t> signedCacheSize(m_cacheSize);
+ signedCacheSize += delta;
+ m_cacheSize = safeCast<size_t>(signedCacheSize.ValueOrDie());
+ }
+
+ size_t m_cacheSize = 0;
+ size_t m_maxCacheSize = kMaxAnimationCacheSize;
+ size_t m_maxCacheSizeForAnimation = kMaxCacheSizeForAnimation;
+
+private:
+ AnimatedBitmapImageCache()
+ : ImageMRUCache(ImageMRUCache::NO_AUTO_EVICT)
+ {
+ }
+
+ ~AnimatedBitmapImageCache() override
+ {
+ }
+
+ DISALLOW_COPY_AND_ASSIGN(AnimatedBitmapImageCache);
+};
+
+}
+
namespace blink {
PassRefPtr<BitmapImage> BitmapImage::createWithOrientationForTesting(const SkBitmap& bitmap, ImageOrientation orientation)
@@ -57,6 +130,7 @@ PassRefPtr<BitmapImage> BitmapImage::createWithOrientationForTesting(const SkBit
BitmapImage::BitmapImage(ImageObserver* observer)
: Image(observer)
, m_currentFrame(0)
+ , m_disposeLaterFrame(kNotFound)
, m_repetitionCount(cAnimationNone)
, m_repetitionCountStatus(Unknown)
, m_repetitionsComplete(0)
@@ -76,6 +150,7 @@ BitmapImage::BitmapImage(const SkBitmap& bitmap, ImageObserver* observer)
: Image(observer)
, m_size(bitmap.width(), bitmap.height())
, m_currentFrame(0)
+ , m_disposeLaterFrame(kNotFound)
, m_repetitionCount(cAnimationNone)
, m_repetitionCountStatus(Unknown)
, m_repetitionsComplete(0)
@@ -100,6 +175,7 @@ BitmapImage::BitmapImage(const SkBitmap& bitmap, ImageObserver* observer)
BitmapImage::~BitmapImage()
{
stopAnimation();
+ AnimatedBitmapImageCache::instance().EraseBitmap(this);
}
bool BitmapImage::currentFrameHasSingleSecurityOrigin() const
@@ -107,7 +183,7 @@ bool BitmapImage::currentFrameHasSingleSecurityOrigin() const
return true;
}
-void BitmapImage::destroyDecodedData(bool destroyAll)
+void BitmapImage::destroyDecodedData()
{
for (size_t i = 0; i < m_frames.size(); ++i) {
// The underlying frame isn't actually changing (we're just trying to
@@ -115,25 +191,12 @@ void BitmapImage::destroyDecodedData(bool destroyAll)
// the metadata.
m_frames[i].clear(false);
}
+ AnimatedBitmapImageCache::instance().EraseBitmap(this);
- size_t frameBytesCleared = m_source.clearCacheExceptFrame(destroyAll ? kNotFound : m_currentFrame);
+ size_t frameBytesCleared = m_source.clearCacheExceptFrame(kNotFound);
notifyMemoryChanged(-safeCast<int>(frameBytesCleared));
}
-void BitmapImage::destroyDecodedDataIfNecessary()
-{
- // Animated images >5MB are considered large enough that we'll only hang on
- // to one frame at a time.
- static const size_t cLargeAnimationCutoff = 5242880;
- size_t allFrameBytes = 0;
- for (size_t i = 0; i < m_frames.size(); ++i)
- allFrameBytes += m_frames[i].m_frameBytes;
-
- if (allFrameBytes > cLargeAnimationCutoff) {
- destroyDecodedData(false);
- }
-}
-
void BitmapImage::notifyMemoryChanged(int delta)
{
if (delta && getImageObserver())
@@ -149,7 +212,7 @@ int BitmapImage::totalFrameBytes()
return safeCast<int>(totalBytes);
}
-void BitmapImage::cacheFrame(size_t index)
+PassRefPtr<SkImage> BitmapImage::cacheFrame(size_t index)
{
size_t numFrames = frameCount();
if (m_frames.size() < numFrames)
@@ -159,7 +222,19 @@ void BitmapImage::cacheFrame(size_t index)
// We are caching frame snapshots. This is OK even for partially decoded frames,
// as they are cleared by dataChanged() when new data arrives.
- m_frames[index].m_frame = m_source.createFrameAtIndex(index);
+ RefPtr<SkImage> image = m_source.createFrameAtIndex(index);
+ size_t frameBytes = m_source.frameBytesAtIndex(index);
+
+ if (frameCount() > 1) {
+ // Making space in cache might change and notify on memory change and
+ // it is needed to notify on any change before and after the call.
Peter Kasting 2016/04/28 23:07:54 Sorry, I don't understand what this comment is say
+ notifyMemoryChanged(totalFrameBytes() - deltaBytes);
+ AnimatedBitmapImageCache::instance().makeSpaceInCache(frameBytes);
+ deltaBytes = totalFrameBytes();
+ }
+
+ m_frames[index].m_frame = image;
+ m_frames[index].m_frameBytes = frameBytes;
m_frames[index].m_orientation = m_source.orientationAtIndex(index);
m_frames[index].m_haveMetadata = true;
@@ -167,7 +242,9 @@ void BitmapImage::cacheFrame(size_t index)
if (repetitionCount(false) != cAnimationNone)
m_frames[index].m_duration = m_source.frameDurationAtIndex(index);
m_frames[index].m_hasAlpha = m_source.frameHasAlphaAtIndex(index);
- m_frames[index].m_frameBytes = m_source.frameBytesAtIndex(index);
+
+ if (frameCount() > 1)
+ onAnimationFrameCached(index);
const IntSize frameSize(index ? m_source.frameSizeAtIndex(index) : m_size);
if (frameSize != m_size)
@@ -178,6 +255,7 @@ void BitmapImage::cacheFrame(size_t index)
// decoding multiple frames to decode the current frame.
deltaBytes = totalFrameBytes() - deltaBytes;
notifyMemoryChanged(deltaBytes);
+ return image.release();
}
void BitmapImage::updateSize() const
@@ -232,8 +310,11 @@ bool BitmapImage::dataChanged(bool allDataReceived)
// NOTE: Don't call frameIsCompleteAtIndex() here, that will try to
// decode any uncached (i.e. never-decoded or
// cleared-on-a-previous-pass) frames!
- if (m_frames[i].m_haveMetadata && !m_frames[i].m_isComplete)
+ if (m_frames[i].m_haveMetadata && !m_frames[i].m_isComplete) {
+ if (m_frames.size() > 1)
+ AnimatedBitmapImageCache::instance().cachedSizeChanged(this, -static_cast<int>(m_frames[i].m_frameBytes));
m_frames[i].clear(true);
+ }
}
// Feed all the data we've seen so far to the image decoder.
@@ -297,7 +378,7 @@ void BitmapImage::draw(SkCanvas* canvas, const SkPaint& paint, const FloatRect&
WebCoreClampingModeToSkiaRectConstraint(clampMode));
canvas->restoreToCount(initialSaveCount);
- if (currentFrameIsLazyDecoded())
+ if (image->isLazyGenerated())
PlatformInstrumentation::didDrawLazyPixelRef(image->uniqueID());
if (ImageObserver* observer = getImageObserver())
@@ -339,23 +420,30 @@ bool BitmapImage::isSizeAvailable()
return m_sizeAvailable;
}
-bool BitmapImage::ensureFrameIsCached(size_t index)
+PassRefPtr<SkImage> BitmapImage::frameAtIndex(size_t index)
{
if (index >= frameCount())
- return false;
+ return nullptr;
- if (index >= m_frames.size() || !m_frames[index].m_frame)
- cacheFrame(index);
+ // Limit cache size; remove previously cached frame from this bitmap that
+ // shouldn't be cached anymore.
+ if (index != m_disposeLaterFrame && m_disposeLaterFrame != kNotFound
+ && m_frames[m_disposeLaterFrame].m_frame && !shouldCacheFrame(m_disposeLaterFrame)) {
+ AnimatedBitmapImageCache::instance().cachedSizeChanged(this, -static_cast<int>(m_frames[m_disposeLaterFrame].m_frameBytes));
+ m_frames[m_disposeLaterFrame].clear(false);
+ m_disposeLaterFrame = kNotFound;
+ }
- return true;
-}
+ RefPtr<SkImage> image;
+ if (index < m_frames.size())
+ image = m_frames[index].m_frame;
-PassRefPtr<SkImage> BitmapImage::frameAtIndex(size_t index)
-{
- if (!ensureFrameIsCached(index))
- return nullptr;
-
- return m_frames[index].m_frame;
+ if (image) {
+ if (m_frames.size() > 1)
+ AnimatedBitmapImageCache::instance().touch(this);
+ return image.release();
+ }
+ return cacheFrame(index);
}
bool BitmapImage::frameIsCompleteAtIndex(size_t index)
@@ -571,9 +659,6 @@ void BitmapImage::resetAnimation()
m_repetitionsComplete = 0;
m_desiredFrameStartTime = 0;
m_animationFinished = false;
-
- // For extremely large animations, when the animation is reset, we just throw everything away.
- destroyDecodedDataIfNecessary();
}
bool BitmapImage::maybeAnimated()
@@ -631,13 +716,57 @@ bool BitmapImage::internalAdvanceAnimation(bool skippingFrames)
} else
m_currentFrame = 0;
}
- destroyDecodedDataIfNecessary();
// We need to draw this frame if we advanced to it while not skipping, or if
// while trying to skip frames we hit the last frame and thus had to stop.
if (skippingFrames != advancedAnimation)
getImageObserver()->animationAdvanced(this);
+
return advancedAnimation;
}
+bool BitmapImage::shouldCacheFrame(size_t index)
+{
+ if (m_frames.size() <= 1)
+ return true;
+ // For large animations it doesnt make sense to cache the most recently used
+ // frames since when looping this would lead to continuous evict, re-decode,
+ // re-cache for every frame rendered. Instead, cache only first
+ // (cache->m_maxCacheSizeForAnimation / frameSize) frames - we can prevent
+ // re-decode and re-caching for them.
Peter Kasting 2016/04/28 23:07:54 This doesn't seem like a good policy. If you want
+ // Asuming all frames are of the same size and 32bit per pixel is good
+ // enough for purpose here - controlling cache size.
+ assert(isSizeAvailable());
+ return ((index + 1) * sizeof(ImageFrame::PixelData) * size().width() * size().height()
+ <= AnimatedBitmapImageCache::instance().m_maxCacheSizeForAnimation);
+}
+
+void BitmapImage::onAnimationFrameCached(size_t index)
+{
+ // calculate cached frame bytes for all frames.
+ size_t cachedFrameBytes = 0;
+ for (size_t i = 0; i < m_frames.size(); ++i)
+ cachedFrameBytes += m_frames[i].m_frameBytes;
+
+ AnimatedBitmapImageCache* cache = &AnimatedBitmapImageCache::instance();
+ bool shouldCache = shouldCacheFrame(index);
+ // If the frame shouldn't be cached, don't change order in MRU cache.
+ auto image_it = shouldCache ? cache->Get(this) : cache->Peek(this);
+ if (image_it != cache->end()) {
+ // image_it->second - frame bytes of cached BitmapImage frames.
+ cache->m_cacheSize -= image_it->second;
+ image_it->second = cachedFrameBytes;
+ } else {
+ cache->Put(this, cachedFrameBytes);
+ }
+ cache->m_cacheSize += cachedFrameBytes;
+ m_disposeLaterFrame = shouldCache ? kNotFound : index;
+}
+
+void BitmapImage::setAnimationCacheSizeForTesting(size_t maxCacheSize, size_t maxAnimationSizeInCache)
+{
+ AnimatedBitmapImageCache::instance().m_maxCacheSize = maxCacheSize;
+ AnimatedBitmapImageCache::instance().m_maxCacheSizeForAnimation = maxAnimationSizeInCache;
+}
+
} // namespace blink

Powered by Google App Engine
This is Rietveld 408576698