This post will dive into image resizing on Chrome for iOS, and how optimizing image resizing can help Chrome use less battery. Instruments is used to profile performance. If you are not familiar with Instruments (part of Xcode) check out this Instruments Overview.

Finding The Performance Opportunity

A performance trace of CPU and memory allocation was collected during 30 seconds of normal Chrome browsing. Chrome is a large and complicated project making even short traces greater than 450 MB.

When looking at the trace from different perspectives, on item that stood out was computation related to image manipulation. Upon further digging, three main sources of image computation were identified:

  1. Capturing a snapshot of the screen during an animation transition
  2. Saving and loading captured snapshots of the screen to and from disk
  3. Resizing Favicon images

Resizing Favicon images stood out, particularly because a Favicon is a small image that would naively be cached. Looking at the captured profile data after the initial load of the Chrome application, filtering the trace by "favicon" shows 25% of total computation used on call trees with "favicon" in the tree.

Diving even further, the following abbreviated trace accounted for 20%+ of CPU used during 20 seconds of normal browsing:

Time Weight Symbol
507.00 ms 21.3% thread_start
492.00 ms 20.7% _::internal::SchedulerWorker::RunWorker()
472.00 ms 19.9% _::CancelableTaskTracker::RunIfNotCanceled(_, _)
466.00 ms 19.6% favicon::_::ProcessIconOnBackgroundThread(_, _, _, _, _, _, _)

With an unexpected area of computation identified, a deeper inspection is needed to see if a performance improvement is possible.

Existing Implementation

When opening a new tab in Chrome multiple quick links are shown for frequently visited websites, each with a favicon. For reasons outside of the scope of this post, the Chrome application downloads new Favicons, at the largest available size, every time a new tab is opened. Then, each new favicon is resized in the background to the appropriate dimensions for app use.

Specifically, the ProcessIconOnBackgroundThread method is called with downlaoded favicons. Within the function, ResizeLargeIconOnBackgroundThread is called to resize a FaviconRawBitmapResult.

On the background thread, a gfx::Image is created from FaviconRawBitmapResult with an internal representation of ImagePNGRep. In order to perform the resize, image.AsBitmap() is called to obtain a bitmap needed for skia::ImageOperations::Resize.

To obtain the bitmap from image.AsBitmap() the call tree eventually reaches CGImageToSkBitmap where CGBitmapContextCreate and CGContextDrawImage are used to draw the image into a SkBitmap. With a bitmap, skia::ImageOperations::Resize is called and returns a resized gfx::Image.

Identifying The Performance Improvement

Another way to see the double resize is to analyze the call tree in more detail, looking for functions and subtrees that contain significant computation relative to the entire tree. This table presents an abbreviated call tree rooted at ProcessIconOnBackgroundThread, identifying the resize subtrees:

Resize Weight Symbol
100.0% favicon::_::ProcessIconOnBackgroundThread(_, _, _, _, _, _, _)
93.3%   favicon::_::ResizeLargeIconOnBackgroundThread(_, _)
x 65.6%       skia::ImageOperations::Resize(_, _, _, _, _)
24.4%       gfx::Image::AsBitmap() const
24.2%           gfx::_::ImageSkiaFromPNG(_, _)
x 21.6%               CGContextDrawImage
5.7%   gfx::Image::As1xPNGBytes() const
5.5%       UIImagePNGRepresentation

With a resize operation performed twice, within image.AsBitmap() and skia::ImageOperations::Resize, a more performant solution would be to use a single CGcontextDraw call to create a resized SkBitmap.

Faster Implementation(s)

A better solution was proposed @mastiz in the thread of the filed bug report for this issue. Creating a new interface gfx::Image::AsResizedBitmap() has a few advantages over the current implementation:

  1. A single CGContextDraw is needed to returned a resized bitmap.
  2. Using gfx::Image as the abstraction requires only minor changes to existing code using gfx::Image to benefit from the performance improvement
  3. The interface exposed is contained within gfx::Image ensure any relevant platform specific operations are abstracted. Managing cross-platform complexity is key within the Chrome codebase.

Implementing this performance improvement has a significant impact on the Chrome iOS app. Using a single CGContextDraw calls improves the performance of ProcessIconOnBackgroundThread by about 30% by removing the need for a redundant skia::ImageOperations::Resize. In a performance trace where a few new tabs were open, a 1%-2% reduction of total CPU time is observed (leading to better battery life while using Chrome).

App Performance, combining all performance enhancements including changes like this one, can have a large impact on the success of an app. I wrote more about how performance impacts user downloads in 8 Overlooked Ways To Increase App Revenue.

Note: multiple other options were evaluated before proposing a single CGContextDraw call. These options were not as performant:

  1. Using CGDataProviderCopyData and CFDataGetBytes to copy the contents of a CGImage directly into an SkBitmap. CGDataProviderCopyData turned out to be slower than expected.
  2. Using UIGraphicsImageRenderer, UIGraphicsBeginImageContext, or CGBitmapContextCreateImage to create a resized UIImage