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:
- Capturing a snapshot of the screen during an animation transition
- Saving and loading captured snapshots of the screen to and from disk
- 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:
|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.
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.
ProcessIconOnBackgroundThread method is called with downlaoded favicons. Within the function,
ResizeLargeIconOnBackgroundThread is called to resize a
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
To obtain the bitmap from
image.AsBitmap() the call tree eventually reaches
CGContextDrawImage are used to draw the image into a
SkBitmap. With a bitmap,
skia::ImageOperations::Resize is called and returns a resized
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:
|100.0%||favicon::_::ProcessIconOnBackgroundThread(_, _, _, _, _, _, _)|
|x||65.6%||skia::ImageOperations::Resize(_, _, _, _, _)|
With a resize operation performed twice, within
skia::ImageOperations::Resize, a more performant solution would be to use a single
CGcontextDraw call to create a resized
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:
- A single
CGContextDrawis needed to returned a resized bitmap.
gfx::Imageas the abstraction requires only minor changes to existing code using
gfx::Imageto benefit from the performance improvement
- The interface exposed is contained within
gfx::Imageensure 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:
CFDataGetBytesto copy the contents of a
CGImagedirectly into an
CGDataProviderCopyDataturned out to be slower than expected.
CGBitmapContextCreateImageto create a resized