Skip to content

Commit

Permalink
Cherry pick PR #1020: Adds 4 CVals to track WebP usage and performance (
Browse files Browse the repository at this point in the history
#1042)

Refer to the original PR: #1020

This PR adds tracking around AnimatedImage, which is currently only used
for WebP decoding.
We are adding 4 metrics
 - Total number of currently decoding WebP images
 - Cumulative total of frames decoded
 - Decoding underruns, when CPU bottleneck is hit
- Decoding overruns, when timing in decoding pipeline is trying to
decode too fast.

b/196887641
b/280381029
b/290130439
b/290060548

Co-authored-by: Kaido Kert <[email protected]>
  • Loading branch information
cobalt-github-releaser-bot and kaidokert authored Jul 26, 2023
1 parent 13c4f4d commit 6f0afc7
Show file tree
Hide file tree
Showing 18 changed files with 266 additions and 3 deletions.
2 changes: 2 additions & 0 deletions cobalt/browser/web_module.cc
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ WebModule::Impl::Impl(web::Context* web_context, const ConstructionData& data)
data.options.loader_thread_priority));

animated_image_tracker_.reset(new loader::image::AnimatedImageTracker(
web_context_->name().c_str(),
data.options.animated_image_decode_thread_priority));

DCHECK_LE(0, data.options.image_cache_capacity);
Expand Down Expand Up @@ -975,6 +976,7 @@ void WebModule::Impl::ProcessOnRenderTreeRasterized(
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
web_module_stat_tracker_->OnRenderTreeRasterized(produced_time,
rasterized_time);
animated_image_tracker_->OnRenderTreeRasterized();
if (produced_time >= last_render_tree_produced_time_) {
is_render_tree_rasterization_pending_ = false;
}
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
166 changes: 166 additions & 0 deletions cobalt/demos/content/animated-webp-performance-demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<!DOCTYPE html>
<!--
| Demo to help test animated webp performance metrics.
| We set up decoding speed tracking on a regular interval, and make a
| customizeable page with varying number of decoded images.
-->
<html>

<head>
<title>WebP performance test</title>
<style type="text/css">

body {
background-color: white;
}
.image {
width: 100px;
height: 100px;
background-size: contain;
display: inline-block;
}
.highlight {
background-color: rgb(0, 255, 0);
}
</style>
<script type="text/javascript">
window.onload = () => {
var size = 100; // Default pixel size of rendered images
var search = window.location.search;
var num = 1; // Default number of decoded images

// Grab URL search args
var matches = search.matchAll(/num=([0-9]+)/gm);
for (const match of matches) {
num = match[1]
}
var matches = search.matchAll(/size=([0-9]+)/gm);
for (const match of matches) {
size = match[1]
}

// Insert images into DOM tree
var layer = document.getElementById('layer');
function addImage(img, size) {
var el = document.createElement('span');
console.log(`adding ${img}`);
el.classList.add('image');
el.style['backgroundImage'] = `url(${img}.webp)`;
el.style['width'] = `${size}px`;
el.style['height'] = `${size}px`;
layer.appendChild(el)
console.log("Added image");
}
var images = [
"1_fan","2_heart","3_cry","4_clap", "5_egg",
"6_party","7_rofl","8_melt","9_explode","10_turtle"
]
for(i = 0; i< num; i++) {
(function(i) {
var img = images[i % 10];
window.setTimeout( () => { addImage(img,size) } , 1 + i*40);
})(i)
}

// Set up function retrieve CVals.
var getCVal = function(name) { return 0 }
if (typeof h5vcc != "undefined" && typeof h5vcc.cVal != "undefined") {
getCVal = function(name) {
return h5vcc.cVal.getValue(name);
}
}

// Set up periodic stats tracking loop.
var last_frames = 0;
var last_time = performance.now();
function updateStats() {
var current_time = performance.now();
var time_delta_milliseconds = current_time - last_time;
var decoded_frames =
getCVal('Count.MainWebModule.AnimatedImage.DecodedFrames');
var underruns = getCVal('Count.MainWebModule.AnimatedImage.DecodingUnderruns');
var overruns = getCVal('Count.MainWebModule.AnimatedImage.DecodingOverruns');
var newly_decoded_frames = decoded_frames - last_frames;
var fps = parseInt( newly_decoded_frames * 100 / (time_delta_milliseconds / 1000.0)) / 100;
document.getElementById('Active').textContent =
getCVal('Count.MainWebModule.AnimatedImage.Active');
document.getElementById('DecodedFrames').textContent = decoded_frames;
document.getElementById('DecodingUnderruns').textContent = underruns
document.getElementById('DecodingOverruns').textContent = overruns;
document.getElementById('DecodedFPS').textContent = fps;
document.getElementById('UnderrunPercent').textContent = parseInt(underruns / decoded_frames * 100.0);
document.getElementById('OverrunPercent').textContent = parseInt(overruns / decoded_frames * 100.0);

last_frames = decoded_frames;
last_time = current_time;
}
window.setInterval(updateStats, 1000);

// Set up keyboard menu nav
var menu = document.getElementById('menu').children;
var index = 0;
function refresh() {
var textBox = document.getElementById('nav');
for (let i = 0; i < menu.length; i++) {
if (i == index) {
menu[i].classList.add('highlight');
} else {
menu[i].classList.remove('highlight');
}
}
}
document.addEventListener('keydown', function (e) {
// left, up, android left, up
if ([37, 38, 32782, 32780].includes(e.keyCode)) {
index -= 1;
//right, down, android right, down
} else if ([39, 40, 32781, 32783].includes(e.keyCode)) {
index += 1;
// enter, android enter
} else if ([13, 32768].includes(e.keyCode)) {
var el = document.getElementById('menu').children[index];
window.location = el.firstChild.href;
}
index = (index + menu.length) % menu.length;
refresh();
});
refresh();
}
</script>
</head>

<body>
<div class="background"></div>
<div id="menu">
<span><a href="?size=100&amp;num=1">1 small image</a></span>
<span><a href="?size=300&amp;num=1">1 large image</a></span>
<span><a href="?size=100&amp;num=2">2 small images</a></span>
<span><a href="?size=300&amp;num=2">2 large image</a></span>
<span><a href="?size=100&amp;num=3">3 small images</a></span>
<span><a href="?size=300&amp;num=3">3 large images</a></span>
<span><a href="?size=100&amp;num=4">4 small images</a></span>
<span><a href="?size=300&amp;num=4">4 large images</a></span>
<div></div>
<span><a href="?size=100&amp;num=4">5 small images</a></span>
<span><a href="?size=300&amp;num=4">5 large images</a></span>
<span><a href="?size=100&amp;num=6">6 small images</a></span>
<span><a href="?size=300&amp;num=6">6 large images</a></span>
<span><a href="?size=100&amp;num=8">8 small images</a></span>
<span><a href="?size=300&amp;num=8">8 large images</a></span>
<span><a href="?size=100&amp;num=10">10 small images</a></span>
<span><a href="?size=300&amp;num=10">10 large images</a></span>
</div>
<div id="metrics">
<div><span>Total images playing:</span><span id="Active"></span></div>
<div><span>Total frames decoded:</span><span id="DecodedFrames"></span></div>
<div><span>Total frames underrun:</span><span id="DecodingUnderruns"></span></div>
<div><span>Total frames overrun:</span><span id="DecodingOverruns"></span></div>
<div><span>Frames decoded per second:</span><span id="DecodedFPS"></span></div>
<div><span>Underrun %:</span><span id="UnderrunPercent"></span>
<span>Overrun %:</span><span id="OverrunPercent"></span></div>
</div>

<div class="layer" id="layer"></div>
</body>

</html>
12 changes: 12 additions & 0 deletions cobalt/doc/cvals.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ Here are examples of its usage:
compatibility violations encountered.
* **Count.XHR** - The total number of xhr::XMLHttpRequest in existence
globally.
* **Count.MainWebModule.AnimatedImage.Active** - The total number of currently
active animated image decoders. Same image from a single URL source rendered
multiple times across the content counts as one decoder.
* **Count.MainWebModule.AnimatedImage.DecodedFrames** - Total number of decoded
frames across all active image decoders. This number resets only when
WebModule gets re-created - e.g. page reload, navigation.
* **Count.MainWebModule.AnimatedImage.DecodingUnderruns** - Total number of
frames when animated image decoding has fallen behind real-time, as defined
by image frame exposure times. This indicates a CPU bottleneck.
* **Count.MainWebModule.AnimatedImage.DecodingOverruns** - Total number of
frames when animated image decoding has been attempted too early, before
next frame exposure time. This indicates a timing issue in platform.

### Event

Expand Down
29 changes: 28 additions & 1 deletion cobalt/loader/image/animated_image_tracker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
#include "cobalt/loader/image/animated_image_tracker.h"

#include <algorithm>
#include <utility>

#include "base/strings/stringprintf.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/base/polymorphic_downcast.h"

Expand All @@ -24,8 +26,22 @@ namespace loader {
namespace image {

AnimatedImageTracker::AnimatedImageTracker(
const char* name,
base::ThreadPriority animated_image_decode_thread_priority)
: animated_image_decode_thread_("AnimatedImage") {
: animated_image_decode_thread_("AnimatedImage"),
name_(name),
count_animated_images_active(
base::StringPrintf("Count.%s.AnimatedImage.Active", name), 0,
"Total number of active animated image decoders."),
count_animated_frames_decoded(
base::StringPrintf("Count.%s.AnimatedImage.DecodedFrames", name), 0,
"Total number of decoded animated image frames."),
count_animated_frames_decoding_underrun(
base::StringPrintf("Count.%s.AnimatedImage.DecodingUnderruns", name),
0, "Number of underruns from decoding animated images"),
count_animated_frames_decoding_overrun(
base::StringPrintf("Count.%s.AnimatedImage.DecodingOverruns", name),
0, "Number of overruns from decoding animated images") {
TRACE_EVENT0("cobalt::loader::image", "AnimatedImageTracker::RecordImage()");
base::Thread::Options options(base::MessageLoop::TYPE_DEFAULT,
0 /* default stack size */);
Expand Down Expand Up @@ -123,6 +139,17 @@ void AnimatedImageTracker::Reset() {
playing_urls_.clear();
}

void AnimatedImageTracker::OnRenderTreeRasterized() {
count_animated_images_active = playing_urls_.size();
for (const auto& playing_url : playing_urls_) {
auto image = image_map_[playing_url.first].get();
auto stats = image->GetFrameDeltaStats();
count_animated_frames_decoded += stats.frames_decoded;
count_animated_frames_decoding_underrun += stats.frames_underrun;
count_animated_frames_decoding_overrun += stats.frames_overrun;
}
}

} // namespace image
} // namespace loader
} // namespace cobalt
16 changes: 15 additions & 1 deletion cobalt/loader/image/animated_image_tracker.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include "base/containers/small_map.h"
#include "base/threading/thread.h"
#include "cobalt/base/c_val.h"
#include "cobalt/base/unused.h"
#include "cobalt/loader/image/image.h"
#include "url/gurl.h"
Expand All @@ -33,7 +34,8 @@ namespace image {
// playing status is updated hence decoding is turned on / off for it.
class AnimatedImageTracker {
public:
explicit AnimatedImageTracker(
AnimatedImageTracker(
const char* name,
base::ThreadPriority animated_image_decode_thread_priority);
~AnimatedImageTracker();

Expand All @@ -54,6 +56,9 @@ class AnimatedImageTracker {
// animations.
void Reset();

// Called from WebModule to compute image animation related stats.
void OnRenderTreeRasterized();

private:
void OnDisplayStart(loader::image::AnimatedImage* image);
void OnDisplayEnd(loader::image::AnimatedImage* image);
Expand All @@ -72,6 +77,15 @@ class AnimatedImageTracker {
URLToIntMap current_url_counts_;
URLSet playing_urls_;

// The name of the WebModule this AnimatedImage tracker belongs to, for CVals.
const std::string name_;

// Animated image counters
base::CVal<int, base::CValPublic> count_animated_images_active;
base::CVal<int, base::CValPublic> count_animated_frames_decoded;
base::CVal<int, base::CValPublic> count_animated_frames_decoding_underrun;
base::CVal<int, base::CValPublic> count_animated_frames_decoding_overrun;

// Used to ensure that all AnimatedImageTracker methods are called on the
// same thread (*not* |animated_image_decode_thread_|), the thread that we
// were constructed on.
Expand Down
27 changes: 26 additions & 1 deletion cobalt/loader/image/animated_webp_image.cc
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ void AnimatedWebPImage::PlayInternal() {
return;
}
is_playing_ = true;
current_stats.frames_underrun = 0;
current_stats.frames_overrun = 0;

if (received_first_frame_) {
StartDecoding();
Expand All @@ -169,7 +171,8 @@ void AnimatedWebPImage::StopInternal() {
void AnimatedWebPImage::StartDecoding() {
TRACE_EVENT0("cobalt::loader::image", "AnimatedWebPImage::StartDecoding()");
lock_.AssertAcquired();
current_frame_time_ = base::TimeTicks::Now();
decoding_start_time_ = current_frame_time_ = base::TimeTicks::Now();
current_stats.frames_decoded = 0;
if (task_runner_->BelongsToCurrentThread()) {
DecodeFrames();
} else {
Expand Down Expand Up @@ -265,6 +268,7 @@ bool AnimatedWebPImage::DecodeOneFrame(int frame_index) {
LOG(ERROR) << "Failed to decode WebP image frame.";
return false;
}
current_stats.frames_decoded++;
}

// Alpha blend the current frame on top of the buffer.
Expand Down Expand Up @@ -349,6 +353,7 @@ bool AnimatedWebPImage::AdvanceFrame() {
// Always wait for a consumer to consume the previous frame before moving
// forward with decoding the next frame.
if (!frame_provider_->FrameConsumed()) {
current_stats.frames_overrun++;
return false;
}

Expand Down Expand Up @@ -381,6 +386,7 @@ bool AnimatedWebPImage::AdvanceFrame() {
if (next_frame_time_ < current_time) {
// Don't let the animation fall back for more than a frame.
next_frame_time_ = current_time;
current_stats.frames_underrun++;
}

return true;
Expand Down Expand Up @@ -426,6 +432,25 @@ scoped_refptr<render_tree::Image> AnimatedWebPImage::GetFrameForDebugging(
return target_canvas;
}

AnimatedImage::AnimatedImageDecodingStats
AnimatedWebPImage::GetFrameDeltaStats() {
AnimatedImageDecodingStats result;
if (current_stats.frames_decoded >= last_stats.frames_decoded) {
result.frames_decoded =
current_stats.frames_decoded - last_stats.frames_decoded;
result.frames_underrun =
current_stats.frames_underrun - last_stats.frames_underrun;
result.frames_overrun =
current_stats.frames_overrun - last_stats.frames_overrun;
} else {
// There was a reset somewhere
// Simply return total, this discards any overflow data we might have had.
result = current_stats;
}
last_stats = current_stats;
return result;
}

} // namespace image
} // namespace loader
} // namespace cobalt
7 changes: 7 additions & 0 deletions cobalt/loader/image/animated_webp_image.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class AnimatedWebPImage : public AnimatedImage {
// Returns the render image of the frame for debugging
scoped_refptr<render_tree::Image> GetFrameForDebugging(int target_frame);

AnimatedImageDecodingStats GetFrameDeltaStats() override;

private:
~AnimatedWebPImage() override;

Expand Down Expand Up @@ -119,12 +121,17 @@ class AnimatedWebPImage : public AnimatedImage {
base::CancelableClosure decode_closure_;
base::TimeTicks current_frame_time_;
base::Optional<base::TimeTicks> next_frame_time_;

// The original encoded data.
std::vector<uint8> data_buffer_;
scoped_refptr<render_tree::Image> current_canvas_;
scoped_refptr<FrameProvider> frame_provider_;
base::Lock lock_;

base::TimeTicks decoding_start_time_;
AnimatedImageDecodingStats current_stats;
AnimatedImageDecodingStats last_stats;

// Makes sure that the thread that sets the task_runner is always consistent.
// This is the thread sending Play()/Stop() calls, and is not necessarily
// the same thread that the task_runner itself is running on.
Expand Down
Loading

0 comments on commit 6f0afc7

Please sign in to comment.