Set maximum output size for scaled-image-cache images

Accessable via:
  SkGraphics::{G|S}etImageCacheSingleAllocationByteLimit()

Also, a unit test.

BUG=389439
R=reed@google.com, humper@google.com, reveman@chromium.org, tomhudson@google.com, vangelis@chromium.org

Author: halcanary@google.com

Review URL: https://codereview.chromium.org/394003003

Review URL: https://codereview.chromium.org/462993003
diff --git a/bench/ImageCacheBench.cpp b/bench/ImageCacheBench.cpp
index 5f1715f..e65d1fc 100644
--- a/bench/ImageCacheBench.cpp
+++ b/bench/ImageCacheBench.cpp
@@ -37,7 +37,7 @@
     }
 
     virtual void onDraw(const int loops, SkCanvas*) SK_OVERRIDE {
-        if (fCache.getBytesUsed() == 0) {
+        if (fCache.getTotalBytesUsed() == 0) {
             this->populateCache();
         }
 
diff --git a/gyp/tests.gypi b/gyp/tests.gypi
index 2a7dddd..9e418d6 100644
--- a/gyp/tests.gypi
+++ b/gyp/tests.gypi
@@ -155,6 +155,7 @@
     '../tests/RuntimeConfigTest.cpp',
     '../tests/SHA1Test.cpp',
     '../tests/ScalarTest.cpp',
+    '../tests/ScaledImageCache.cpp',
     '../tests/SerializationTest.cpp',
     '../tests/ShaderImageFilterTest.cpp',
     '../tests/ShaderOpacityTest.cpp',
diff --git a/include/core/SkGraphics.h b/include/core/SkGraphics.h
index 2667a38..e7865ca 100644
--- a/include/core/SkGraphics.h
+++ b/include/core/SkGraphics.h
@@ -79,9 +79,47 @@
      */
     static void PurgeFontCache();
 
-    static size_t GetImageCacheBytesUsed();
-    static size_t GetImageCacheByteLimit();
-    static size_t SetImageCacheByteLimit(size_t newLimit);
+    /**
+     *  Scaling bitmaps with the SkPaint::kHigh_FilterLevel setting is
+     *  expensive, so the result is saved in the global Scaled Image
+     *  Cache.
+     *
+     *  This function returns the memory usage of the Scaled Image Cache.
+     */
+    static size_t GetImageCacheTotalBytesUsed();
+    /**
+     *  These functions get/set the memory usage limit for the Scaled
+     *  Image Cache.  Bitmaps are purged from the cache when the
+     *  memory useage exceeds this limit.
+     */
+    static size_t GetImageCacheTotalByteLimit();
+    static size_t SetImageCacheTotalByteLimit(size_t newLimit);
+
+    // DEPRECATED
+    static size_t GetImageCacheBytesUsed() {
+        return GetImageCacheTotalBytesUsed();
+    }
+    // DEPRECATED
+    static size_t GetImageCacheByteLimit() {
+        return GetImageCacheTotalByteLimit();
+    }
+    // DEPRECATED
+    static size_t SetImageCacheByteLimit(size_t newLimit) {
+        return SetImageCacheTotalByteLimit(newLimit);
+    }
+
+    /**
+     *  Scaling bitmaps with the SkPaint::kHigh_FilterLevel setting is
+     *  expensive, so the result is saved in the global Scaled Image
+     *  Cache.  When the resulting bitmap is too large, this can
+     *  overload the cache.  If the ImageCacheSingleAllocationByteLimit
+     *  is set to a non-zero number, and the resulting bitmap would be
+     *  larger than that value, the bitmap scaling algorithm falls
+     *  back onto a cheaper algorithm and does not cache the result.
+     *  Zero is the default value.
+     */
+    static size_t GetImageCacheSingleAllocationByteLimit();
+    static size_t SetImageCacheSingleAllocationByteLimit(size_t newLimit);
 
     /**
      *  Applications with command line options may pass optional state, such
diff --git a/src/core/SkBitmapProcState.cpp b/src/core/SkBitmapProcState.cpp
index f6f9f3f..137bcda 100644
--- a/src/core/SkBitmapProcState.cpp
+++ b/src/core/SkBitmapProcState.cpp
@@ -127,6 +127,21 @@
 };
 #define AutoScaledCacheUnlocker(...) SK_REQUIRE_LOCAL_VAR(AutoScaledCacheUnlocker)
 
+// Check to see that the size of the bitmap that would be produced by
+// scaling by the given inverted matrix is less than the maximum allowed.
+static inline bool cache_size_okay(const SkBitmap& bm, const SkMatrix& invMat) {
+    size_t maximumAllocation
+        = SkScaledImageCache::GetSingleAllocationByteLimit();
+    if (0 == maximumAllocation) {
+        return true;
+    }
+    // float matrixScaleFactor = 1.0 / (invMat.scaleX * invMat.scaleY);
+    // return ((origBitmapSize * matrixScaleFactor) < maximumAllocationSize);
+    // Skip the division step:
+    return bm.info().getSafeSize(bm.info().minRowBytes())
+        < (maximumAllocation * invMat.getScaleX() * invMat.getScaleY());
+}
+
 // TODO -- we may want to pass the clip into this function so we only scale
 // the portion of the image that we're going to need.  This will complicate
 // the interface to the cache, but might be well worth it.
@@ -140,14 +155,14 @@
     if (fFilterLevel <= SkPaint::kLow_FilterLevel) {
         return false;
     }
-
     // Check to see if the transformation matrix is simple, and if we're
     // doing high quality scaling.  If so, do the bitmap scale here and
     // remove the scaling component from the matrix.
 
     if (SkPaint::kHigh_FilterLevel == fFilterLevel &&
         fInvMatrix.getType() <= (SkMatrix::kScale_Mask | SkMatrix::kTranslate_Mask) &&
-        kN32_SkColorType == fOrigBitmap.colorType()) {
+        kN32_SkColorType == fOrigBitmap.colorType() &&
+        cache_size_okay(fOrigBitmap, fInvMatrix)) {
 
         SkScalar invScaleX = fInvMatrix.getScaleX();
         SkScalar invScaleY = fInvMatrix.getScaleY();
diff --git a/src/core/SkScaledImageCache.cpp b/src/core/SkScaledImageCache.cpp
index a030248..43ff7ef 100644
--- a/src/core/SkScaledImageCache.cpp
+++ b/src/core/SkScaledImageCache.cpp
@@ -165,12 +165,13 @@
 #else
     fHash = NULL;
 #endif
-    fBytesUsed = 0;
+    fTotalBytesUsed = 0;
     fCount = 0;
+    fSingleAllocationByteLimit = 0;
     fAllocator = NULL;
 
     // One of these should be explicit set by the caller after we return.
-    fByteLimit = 0;
+    fTotalByteLimit = 0;
     fDiscardableFactory = NULL;
 }
 
@@ -297,7 +298,7 @@
 
 SkScaledImageCache::SkScaledImageCache(size_t byteLimit) {
     this->init();
-    fByteLimit = byteLimit;
+    fTotalByteLimit = byteLimit;
 }
 
 SkScaledImageCache::~SkScaledImageCache() {
@@ -502,10 +503,10 @@
         byteLimit = SK_MaxU32;  // no limit based on bytes
     } else {
         countLimit = SK_MaxS32; // no limit based on count
-        byteLimit = fByteLimit;
+        byteLimit = fTotalByteLimit;
     }
 
-    size_t bytesUsed = fBytesUsed;
+    size_t bytesUsed = fTotalBytesUsed;
     int    countUsed = fCount;
 
     Rec* rec = fTail;
@@ -531,13 +532,13 @@
         rec = prev;
     }
 
-    fBytesUsed = bytesUsed;
+    fTotalBytesUsed = bytesUsed;
     fCount = countUsed;
 }
 
-size_t SkScaledImageCache::setByteLimit(size_t newLimit) {
-    size_t prevLimit = fByteLimit;
-    fByteLimit = newLimit;
+size_t SkScaledImageCache::setTotalByteLimit(size_t newLimit) {
+    size_t prevLimit = fTotalByteLimit;
+    fTotalByteLimit = newLimit;
     if (newLimit < prevLimit) {
         this->purgeAsNeeded();
     }
@@ -597,7 +598,7 @@
     if (!fTail) {
         fTail = rec;
     }
-    fBytesUsed += rec->bytesUsed();
+    fTotalBytesUsed += rec->bytesUsed();
     fCount += 1;
 
     this->validate();
@@ -609,14 +610,14 @@
 void SkScaledImageCache::validate() const {
     if (NULL == fHead) {
         SkASSERT(NULL == fTail);
-        SkASSERT(0 == fBytesUsed);
+        SkASSERT(0 == fTotalBytesUsed);
         return;
     }
 
     if (fHead == fTail) {
         SkASSERT(NULL == fHead->fPrev);
         SkASSERT(NULL == fHead->fNext);
-        SkASSERT(fHead->bytesUsed() == fBytesUsed);
+        SkASSERT(fHead->bytesUsed() == fTotalBytesUsed);
         return;
     }
 
@@ -631,7 +632,7 @@
     while (rec) {
         count += 1;
         used += rec->bytesUsed();
-        SkASSERT(used <= fBytesUsed);
+        SkASSERT(used <= fTotalBytesUsed);
         rec = rec->fNext;
     }
     SkASSERT(fCount == count);
@@ -661,10 +662,20 @@
     }
 
     SkDebugf("SkScaledImageCache: count=%d bytes=%d locked=%d %s\n",
-             fCount, fBytesUsed, locked,
+             fCount, fTotalBytesUsed, locked,
              fDiscardableFactory ? "discardable" : "malloc");
 }
 
+size_t SkScaledImageCache::setSingleAllocationByteLimit(size_t newLimit) {
+    size_t oldLimit = fSingleAllocationByteLimit;
+    fSingleAllocationByteLimit = newLimit;
+    return oldLimit;
+}
+
+size_t SkScaledImageCache::getSingleAllocationByteLimit() const {
+    return fSingleAllocationByteLimit;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 
 #include "SkThread.h"
@@ -751,19 +762,19 @@
 //    get_cache()->dump();
 }
 
-size_t SkScaledImageCache::GetBytesUsed() {
+size_t SkScaledImageCache::GetTotalBytesUsed() {
     SkAutoMutexAcquire am(gMutex);
-    return get_cache()->getBytesUsed();
+    return get_cache()->getTotalBytesUsed();
 }
 
-size_t SkScaledImageCache::GetByteLimit() {
+size_t SkScaledImageCache::GetTotalByteLimit() {
     SkAutoMutexAcquire am(gMutex);
-    return get_cache()->getByteLimit();
+    return get_cache()->getTotalByteLimit();
 }
 
-size_t SkScaledImageCache::SetByteLimit(size_t newLimit) {
+size_t SkScaledImageCache::SetTotalByteLimit(size_t newLimit) {
     SkAutoMutexAcquire am(gMutex);
-    return get_cache()->setByteLimit(newLimit);
+    return get_cache()->setTotalByteLimit(newLimit);
 }
 
 SkBitmap::Allocator* SkScaledImageCache::GetAllocator() {
@@ -776,18 +787,37 @@
     get_cache()->dump();
 }
 
+size_t SkScaledImageCache::SetSingleAllocationByteLimit(size_t size) {
+    SkAutoMutexAcquire am(gMutex);
+    return get_cache()->setSingleAllocationByteLimit(size);
+}
+
+size_t SkScaledImageCache::GetSingleAllocationByteLimit() {
+    SkAutoMutexAcquire am(gMutex);
+    return get_cache()->getSingleAllocationByteLimit();
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 
 #include "SkGraphics.h"
 
-size_t SkGraphics::GetImageCacheBytesUsed() {
-    return SkScaledImageCache::GetBytesUsed();
+size_t SkGraphics::GetImageCacheTotalBytesUsed() {
+    return SkScaledImageCache::GetTotalBytesUsed();
 }
 
-size_t SkGraphics::GetImageCacheByteLimit() {
-    return SkScaledImageCache::GetByteLimit();
+size_t SkGraphics::GetImageCacheTotalByteLimit() {
+    return SkScaledImageCache::GetTotalByteLimit();
 }
 
-size_t SkGraphics::SetImageCacheByteLimit(size_t newLimit) {
-    return SkScaledImageCache::SetByteLimit(newLimit);
+size_t SkGraphics::SetImageCacheTotalByteLimit(size_t newLimit) {
+    return SkScaledImageCache::SetTotalByteLimit(newLimit);
 }
+
+size_t SkGraphics::GetImageCacheSingleAllocationByteLimit() {
+    return SkScaledImageCache::GetSingleAllocationByteLimit();
+}
+
+size_t SkGraphics::SetImageCacheSingleAllocationByteLimit(size_t newLimit) {
+    return SkScaledImageCache::SetSingleAllocationByteLimit(newLimit);
+}
+
diff --git a/src/core/SkScaledImageCache.h b/src/core/SkScaledImageCache.h
index fe07230..817147e 100644
--- a/src/core/SkScaledImageCache.h
+++ b/src/core/SkScaledImageCache.h
@@ -60,9 +60,12 @@
 
     static void Unlock(ID*);
 
-    static size_t GetBytesUsed();
-    static size_t GetByteLimit();
-    static size_t SetByteLimit(size_t newLimit);
+    static size_t GetTotalBytesUsed();
+    static size_t GetTotalByteLimit();
+    static size_t SetTotalByteLimit(size_t newLimit);
+
+    static size_t SetSingleAllocationByteLimit(size_t);
+    static size_t GetSingleAllocationByteLimit();
 
     static SkBitmap::Allocator* GetAllocator();
 
@@ -76,9 +79,9 @@
     /**
      *  Construct the cache to call DiscardableFactory when it
      *  allocates memory for the pixels. In this mode, the cache has
-     *  not explicit budget, and so methods like getBytesUsed() and
-     *  getByteLimit() will return 0, and setByteLimit will ignore its argument
-     *  and return 0.
+     *  not explicit budget, and so methods like getTotalBytesUsed()
+     *  and getTotalByteLimit() will return 0, and setTotalByteLimit
+     *  will ignore its argument and return 0.
      */
     SkScaledImageCache(DiscardableFactory);
 
@@ -86,7 +89,7 @@
      *  Construct the cache, allocating memory with malloc, and respect the
      *  byteLimit, purging automatically when a new image is added to the cache
      *  that pushes the total bytesUsed over the limit. Note: The limit can be
-     *  changed at runtime with setByteLimit.
+     *  changed at runtime with setTotalByteLimit.
      */
     SkScaledImageCache(size_t byteLimit);
 
@@ -144,15 +147,22 @@
      */
     void unlock(ID*);
 
-    size_t getBytesUsed() const { return fBytesUsed; }
-    size_t getByteLimit() const { return fByteLimit; }
+    size_t getTotalBytesUsed() const { return fTotalBytesUsed; }
+    size_t getTotalByteLimit() const { return fTotalByteLimit; }
 
     /**
+     *  This is respected by SkBitmapProcState::possiblyScaleImage.
+     *  0 is no maximum at all; this is the default.
+     *  setSingleAllocationByteLimit() returns the previous value.
+     */
+    size_t setSingleAllocationByteLimit(size_t maximumAllocationSize);
+    size_t getSingleAllocationByteLimit() const;
+    /**
      *  Set the maximum number of bytes available to this cache. If the current
      *  cache exceeds this new value, it will be purged to try to fit within
      *  this new limit.
      */
-    size_t setByteLimit(size_t newLimit);
+    size_t setTotalByteLimit(size_t newLimit);
 
     SkBitmap::Allocator* allocator() const { return fAllocator; };
 
@@ -175,8 +185,9 @@
     // the allocator is NULL or one that matches discardables
     SkBitmap::Allocator* fAllocator;
 
-    size_t  fBytesUsed;
-    size_t  fByteLimit;
+    size_t  fTotalBytesUsed;
+    size_t  fTotalByteLimit;
+    size_t  fSingleAllocationByteLimit;
     int     fCount;
 
     Rec* findAndLock(uint32_t generationID, SkScalar sx, SkScalar sy,
diff --git a/tests/ImageCacheTest.cpp b/tests/ImageCacheTest.cpp
index 92d0b51..00f6c77 100644
--- a/tests/ImageCacheTest.cpp
+++ b/tests/ImageCacheTest.cpp
@@ -74,7 +74,7 @@
         }
     }
 
-    cache.setByteLimit(0);
+    cache.setTotalByteLimit(0);
 }
 
 #include "SkDiscardableMemoryPool.h"
diff --git a/tests/ScaledImageCache.cpp b/tests/ScaledImageCache.cpp
new file mode 100644
index 0000000..2040afe
--- /dev/null
+++ b/tests/ScaledImageCache.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#include "Test.h"
+#include "SkGraphics.h"
+#include "SkCanvas.h"
+
+static const int kCanvasSize = 1;
+static const int kBitmapSize = 16;
+static const int kScale = 8;
+
+static size_t test_scaled_image_cache_useage() {
+    SkAutoTUnref<SkCanvas> canvas(
+            SkCanvas::NewRasterN32(kCanvasSize, kCanvasSize));
+    SkBitmap bitmap;
+    SkAssertResult(bitmap.allocN32Pixels(kBitmapSize, kBitmapSize));
+    SkScalar scaledSize = SkIntToScalar(kScale * kBitmapSize);
+    canvas->clipRect(SkRect::MakeLTRB(0, 0, scaledSize, scaledSize));
+    SkPaint paint;
+    paint.setFilterLevel(SkPaint::kHigh_FilterLevel);
+    size_t bytesUsed = SkGraphics::GetImageCacheBytesUsed();
+    canvas->drawBitmapRect(bitmap,
+                           SkRect::MakeLTRB(0, 0, scaledSize, scaledSize),
+                           &paint);
+    return SkGraphics::GetImageCacheBytesUsed() - bytesUsed;
+}
+
+// http://crbug.com/389439
+DEF_TEST(ScaledImageCache_SingleAllocationByteLimit, reporter) {
+    size_t originalByteLimit = SkGraphics::GetImageCacheByteLimit();
+    size_t originalAllocationLimit =
+        SkGraphics::GetImageCacheSingleAllocationByteLimit();
+
+    size_t size = kBitmapSize * kScale * kBitmapSize * kScale
+        * SkColorTypeBytesPerPixel(kN32_SkColorType);
+
+    SkGraphics::SetImageCacheByteLimit(0); // clear cache
+    SkGraphics::SetImageCacheByteLimit(2 * size);
+    SkGraphics::SetImageCacheSingleAllocationByteLimit(0);
+
+    REPORTER_ASSERT(reporter, size == test_scaled_image_cache_useage());
+
+    SkGraphics::SetImageCacheByteLimit(0); // clear cache
+    SkGraphics::SetImageCacheByteLimit(2 * size);
+    SkGraphics::SetImageCacheSingleAllocationByteLimit(size * 2);
+
+    REPORTER_ASSERT(reporter, size == test_scaled_image_cache_useage());
+
+    SkGraphics::SetImageCacheByteLimit(0); // clear cache
+    SkGraphics::SetImageCacheByteLimit(2 * size);
+    SkGraphics::SetImageCacheSingleAllocationByteLimit(size / 2);
+
+    REPORTER_ASSERT(reporter, 0 == test_scaled_image_cache_useage());
+
+    SkGraphics::SetImageCacheSingleAllocationByteLimit(originalAllocationLimit);
+    SkGraphics::SetImageCacheByteLimit(originalByteLimit);
+}