blob: 10b61f73cebc59323c9cfc4bbd6836a9e40c91d5 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <deque>
#include <iostream>
#include <memory>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/time/time.h"
#include "remoting/codec/webrtc_video_encoder_av1.h"
#include "remoting/codec/webrtc_video_encoder_vpx.h"
#include "remoting/test/frame_generator/differ_frame_generator.h"
#include "remoting/test/frame_generator/file_frame_generator.h"
#include "remoting/test/frame_generator/headless_frame_generator.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
namespace remoting {
namespace {
constexpr base::TimeDelta kDefaultDuration = base::Seconds(10);
constexpr int kDefaultBitrateKbps = 5000;
enum class Codec {
kAv1,
kVp8,
kVp9,
};
enum class Profile {
k0,
k1,
};
enum class ColorSpace {
kI420,
kI444,
};
struct EncoderParams {
Codec codec = Codec::kAv1;
Profile profile = Profile::k0;
ColorSpace color_space = ColorSpace::kI420;
bool use_active_map = false;
int bitrate_kbps = kDefaultBitrateKbps;
};
struct RuntimeParams {
base::TimeDelta duration = kDefaultDuration;
double fps = 30.0;
int max_frames = 100;
base::FilePath frame_dir;
base::FilePath chrome_path;
// Fields that change per iteration (scenario/size)
int frame_count = 0;
webrtc::DesktopSize size;
std::string scenario;
};
std::unique_ptr<WebrtcVideoEncoder> CreateEncoder(const EncoderParams& params) {
std::unique_ptr<WebrtcVideoEncoder> encoder;
if (params.codec == Codec::kAv1) {
encoder = std::make_unique<WebrtcVideoEncoderAV1>();
} else if (params.codec == Codec::kVp8) {
encoder = WebrtcVideoEncoderVpx::CreateForVP8();
} else if (params.codec == Codec::kVp9) {
encoder = WebrtcVideoEncoderVpx::CreateForVP9();
}
if (encoder) {
encoder->SetLosslessColor(params.profile == Profile::k1);
encoder->SetUseActiveMap(params.use_active_map);
}
return encoder;
}
std::string CodecToString(Codec codec) {
switch (codec) {
case Codec::kAv1:
return "av1";
case Codec::kVp8:
return "vp8";
case Codec::kVp9:
return "vp9";
}
}
class EncoderBenchmark {
public:
EncoderBenchmark() {
capture_task_runner_ = base::ThreadPool::CreateSingleThreadTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_BLOCKING},
base::SingleThreadTaskRunnerThreadMode::DEDICATED);
encode_task_runner_ = base::ThreadPool::CreateSingleThreadTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_BLOCKING},
base::SingleThreadTaskRunnerThreadMode::DEDICATED);
}
~EncoderBenchmark() {
// Flush both runners to ensure no use-after-free.
base::RunLoop capture_loop;
capture_task_runner_->PostTaskAndReply(FROM_HERE, base::DoNothing(),
capture_loop.QuitClosure());
capture_loop.Run();
base::RunLoop encode_loop;
encode_task_runner_->PostTaskAndReply(
FROM_HERE,
base::BindOnce(&EncoderBenchmark::DestroyEncoderOnEncodeThread,
base::Unretained(this)),
encode_loop.QuitClosure());
encode_loop.Run();
}
void Run(const EncoderParams& encoder_params,
const RuntimeParams& runtime_params) {
encoder_params_ = encoder_params;
runtime_params_ = runtime_params;
frames_encoded_ = 0;
total_pixels_ = 0;
updated_pixels_ = 0;
total_encode_time_ = base::TimeDelta();
std::unique_ptr<FrameGenerator> generator;
if (!runtime_params_.frame_dir.empty()) {
generator = std::make_unique<FileFrameGenerator>(
runtime_params_.frame_dir, runtime_params_.size);
} else {
auto headless = std::make_unique<HeadlessFrameGenerator>(
runtime_params_.scenario, runtime_params_.size,
runtime_params_.frame_count, runtime_params_.fps);
if (!runtime_params_.chrome_path.empty()) {
headless->SetChromePath(runtime_params_.chrome_path);
}
if (headless->Initialize()) {
generator = std::move(headless);
} else {
LOG(ERROR) << "Failed to initialize headless frame generator.";
return;
}
}
// Wrap with differ to pre-calculate updated regions.
generator = std::make_unique<DifferFrameGenerator>(std::move(generator));
std::cout << "Pre-loading up to " << runtime_params_.frame_count
<< " frames..." << std::endl;
preloaded_frames_.clear();
for (int i = 0; i < runtime_params_.frame_count; ++i) {
auto frame = generator->GenerateFrame();
if (!frame) {
break;
}
preloaded_frames_.push_back(std::move(frame));
}
if (preloaded_frames_.empty()) {
LOG(ERROR) << "No frames generated.";
return;
}
frames_to_encode_ = preloaded_frames_.size();
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
main_task_runner_ = base::SingleThreadTaskRunner::GetCurrentDefault();
encode_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&EncoderBenchmark::InitializeEncoderOnEncodeThread,
base::Unretained(this)));
// Ensure encoder is initialized.
base::RunLoop init_loop;
encode_task_runner_->PostTaskAndReply(FROM_HERE, base::DoNothing(),
init_loop.QuitClosure());
init_loop.Run();
if (!encoder_initialized_) {
LOG(ERROR) << "Failed to initialize encoder for codec: "
<< CodecToString(encoder_params_.codec);
return;
}
std::cout << "Starting benchmark..." << std::endl;
start_time_ = base::TimeTicks::Now();
capture_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&EncoderBenchmark::CaptureNextFrameOnCaptureThread,
base::Unretained(this)));
run_loop.Run();
end_time_ = base::TimeTicks::Now();
PrintResults();
}
private:
void InitializeEncoderOnEncodeThread() {
DCHECK(encode_task_runner_->RunsTasksInCurrentSequence());
encoder_ = CreateEncoder(encoder_params_);
encoder_initialized_ = (encoder_ != nullptr);
}
void DestroyEncoderOnEncodeThread() {
DCHECK(encode_task_runner_->RunsTasksInCurrentSequence());
encoder_.reset();
encoder_initialized_ = false;
}
void CaptureNextFrameOnCaptureThread() {
DCHECK(capture_task_runner_->RunsTasksInCurrentSequence());
CHECK(!preloaded_frames_.empty());
std::unique_ptr<webrtc::DesktopFrame> frame =
std::move(preloaded_frames_.front());
preloaded_frames_.pop_front();
WebrtcVideoEncoder::FrameParams params;
params.bitrate_kbps = encoder_params_.bitrate_kbps;
params.duration = base::Hertz(runtime_params_.fps);
params.key_frame = (frames_encoded_ == 0);
params.clear_active_map = true;
// Stats are tracked on capture thread.
// Use int64_t to prevent overflow for large dimensions.
total_pixels_ += static_cast<int64_t>(frame->size().width()) *
static_cast<int64_t>(frame->size().height());
for (webrtc::DesktopRegion::Iterator r(frame->updated_region());
!r.IsAtEnd(); r.Advance()) {
updated_pixels_ += static_cast<int64_t>(r.rect().width()) *
static_cast<int64_t>(r.rect().height());
}
encode_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&EncoderBenchmark::EncodeFrameOnEncodeThread,
base::Unretained(this), std::move(frame), params));
}
void EncodeFrameOnEncodeThread(
std::unique_ptr<webrtc::DesktopFrame> frame,
const WebrtcVideoEncoder::FrameParams& params) {
DCHECK(encode_task_runner_->RunsTasksInCurrentSequence());
if (!encoder_) {
LOG(ERROR) << "Encoder was not initialized.";
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&EncoderBenchmark::OnFrameEncodedCompleteOnMainThread,
base::Unretained(this)));
return;
}
encoder_->Encode(
std::move(frame), params,
base::BindOnce(&EncoderBenchmark::OnFrameEncodedOnEncodeThread,
base::Unretained(this), base::TimeTicks::Now()));
}
void OnFrameEncodedOnEncodeThread(
base::TimeTicks start_time,
WebrtcVideoEncoder::EncodeResult result,
std::unique_ptr<WebrtcVideoEncoder::EncodedFrame> frame) {
DCHECK(encode_task_runner_->RunsTasksInCurrentSequence());
if (result == WebrtcVideoEncoder::EncodeResult::SUCCEEDED && frame) {
total_encode_time_ += base::TimeTicks::Now() - start_time;
} else {
LOG(ERROR) << "Encoder failed to encode frame.";
}
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&EncoderBenchmark::OnFrameEncodedCompleteOnMainThread,
base::Unretained(this)));
}
void OnFrameEncodedCompleteOnMainThread() {
DCHECK(main_task_runner_->RunsTasksInCurrentSequence());
frames_encoded_++;
if (frames_encoded_ >= frames_to_encode_) {
quit_closure_.Run();
} else {
capture_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&EncoderBenchmark::CaptureNextFrameOnCaptureThread,
base::Unretained(this)));
}
}
void PrintResults() {
std::cout << "--------------------------------------------------"
<< std::endl;
std::cout << "RESULTS (" << CodecToString(encoder_params_.codec) << ", "
<< runtime_params_.size.width() << "x"
<< runtime_params_.size.height() << ", "
<< (runtime_params_.scenario.empty() ? "custom"
: runtime_params_.scenario)
<< ", " << runtime_params_.fps << " FPS)" << std::endl;
std::cout << "Profile: "
<< (encoder_params_.profile == Profile::k1 ? "1" : "0") << " ("
<< (encoder_params_.color_space == ColorSpace::kI444 ? "I444"
: "I420")
<< ")" << std::endl;
std::cout << "Active Map: "
<< (encoder_params_.use_active_map ? "enabled" : "disabled")
<< std::endl;
std::cout << "--------------------------------------------------"
<< std::endl;
std::cout << "Encoded " << frames_encoded_ << " frames." << std::endl;
double total_time_ms = (end_time_ - start_time_).InMillisecondsF();
if (frames_encoded_ > 0 && total_time_ms > 0) {
std::cout << "Total wall-clock time: " << total_time_ms << " ms"
<< std::endl;
std::cout << "Average wall-clock FPS: "
<< (frames_encoded_ * 1000.0) / total_time_ms << std::endl;
double total_encode_time_ms = total_encode_time_.InMillisecondsF();
std::cout << "Total encoder-only time: " << total_encode_time_ms << " ms"
<< std::endl;
std::cout << "Average encoder-only time per frame: "
<< total_encode_time_ms / frames_encoded_ << " ms" << std::endl;
if (total_encode_time_ms > 0) {
std::cout << "Average encoder FPS: "
<< (frames_encoded_ * 1000.0) / total_encode_time_ms
<< std::endl;
} else {
std::cout << "Average encoder FPS: N/A (zero encode time)" << std::endl;
}
} else {
std::cout << "Average FPS: N/A (too few frames or zero time duration)"
<< std::endl;
}
if (total_pixels_ > 0) {
std::cout << "Average updated area: "
<< (updated_pixels_ * 100.0) / total_pixels_ << "%"
<< std::endl;
}
std::cout << "--------------------------------------------------"
<< std::endl;
}
scoped_refptr<base::SingleThreadTaskRunner> capture_task_runner_;
scoped_refptr<base::SingleThreadTaskRunner> encode_task_runner_;
scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;
std::unique_ptr<WebrtcVideoEncoder> encoder_;
bool encoder_initialized_ = false;
EncoderParams encoder_params_;
RuntimeParams runtime_params_;
int frames_to_encode_ = 0;
int frames_encoded_ = 0;
int64_t total_pixels_ = 0;
int64_t updated_pixels_ = 0;
base::TimeDelta total_encode_time_;
std::deque<std::unique_ptr<webrtc::DesktopFrame>> preloaded_frames_;
base::RepeatingClosure quit_closure_;
base::TimeTicks start_time_;
base::TimeTicks end_time_;
};
void PrintHelp(const char* executable_name) {
std::cout << "Usage: " << executable_name << R"([options]
Options:
--codec=<av1|vp8|vp9> Codec to benchmark (default: av1)
--profile=<0|1> Encoder profile (default: 0)
--use-active-map=<B> Override default active-map usage
--bitrate=<N> Target bitrate in kbps (default: 5000)
--duration=<N> Duration in seconds (default: 10)
--fps=<N> Frames per second (default: 30)
--max-frames=<N> Max frames to preload (default: 100)
--scenarios=<S> Comma-separated scenarios, or 'all'
--sizes=<S> Comma-separated sizes (e.g., 1080p,4k,800x600)
--frame-dir=<P> Path to pre-generated frames
--chrome-path=<P> Path to the Chrome/Chromium binary
)";
}
bool ParseCommandLine(const base::CommandLine* cmd_line,
EncoderParams& encoder_params,
RuntimeParams& runtime_params,
std::vector<std::string>& scenarios,
std::vector<webrtc::DesktopSize>& sizes) {
std::string codec_str =
base::ToLowerASCII(cmd_line->GetSwitchValueASCII("codec"));
if (codec_str.empty() || codec_str == "av1") {
encoder_params.codec = Codec::kAv1;
} else if (codec_str == "vp8") {
encoder_params.codec = Codec::kVp8;
} else if (codec_str == "vp9") {
encoder_params.codec = Codec::kVp9;
} else {
LOG(ERROR) << "Invalid codec: " << codec_str;
return false;
}
int profile_val = 0;
if (cmd_line->HasSwitch("profile")) {
if (!base::StringToInt(cmd_line->GetSwitchValueASCII("profile"),
&profile_val) ||
(profile_val != 0 && profile_val != 1)) {
LOG(ERROR) << "Invalid profile: "
<< cmd_line->GetSwitchValueASCII("profile")
<< ". Only 0 and 1 are supported.";
return false;
}
}
encoder_params.profile = (profile_val == 1) ? Profile::k1 : Profile::k0;
encoder_params.color_space = (encoder_params.profile == Profile::k1)
? ColorSpace::kI444
: ColorSpace::kI420;
if (encoder_params.codec == Codec::kVp8 &&
encoder_params.profile != Profile::k0) {
LOG(ERROR) << "VP8 only supports profile 0.";
return false;
}
encoder_params.use_active_map = (encoder_params.codec != Codec::kAv1);
if (cmd_line->HasSwitch("use-active-map")) {
std::string value =
base::ToLowerASCII(cmd_line->GetSwitchValueASCII("use-active-map"));
if (value == "true" || value == "1") {
encoder_params.use_active_map = true;
} else if (value == "false" || value == "0") {
encoder_params.use_active_map = false;
} else {
LOG(ERROR) << "Invalid value for --use-active-map: " << value;
return false;
}
}
encoder_params.bitrate_kbps = kDefaultBitrateKbps;
if (cmd_line->HasSwitch("bitrate")) {
if (!base::StringToInt(cmd_line->GetSwitchValueASCII("bitrate"),
&encoder_params.bitrate_kbps)) {
LOG(ERROR) << "Invalid bitrate: "
<< cmd_line->GetSwitchValueASCII("bitrate");
return false;
}
}
runtime_params.duration = kDefaultDuration;
if (cmd_line->HasSwitch("duration")) {
int duration_value;
if (!base::StringToInt(cmd_line->GetSwitchValueASCII("duration"),
&duration_value)) {
LOG(ERROR) << "Invalid duration: "
<< cmd_line->GetSwitchValueASCII("duration");
return false;
}
runtime_params.duration = base::Seconds(duration_value);
}
runtime_params.fps = 30.0;
if (cmd_line->HasSwitch("fps")) {
if (!base::StringToDouble(cmd_line->GetSwitchValueASCII("fps"),
&runtime_params.fps)) {
LOG(ERROR) << "Invalid fps: " << cmd_line->GetSwitchValueASCII("fps");
return false;
}
}
if (runtime_params.fps <= 0) {
LOG(ERROR) << "FPS must be greater than zero.";
return false;
}
runtime_params.max_frames = 100;
if (cmd_line->HasSwitch("max-frames")) {
if (!base::StringToInt(cmd_line->GetSwitchValueASCII("max-frames"),
&runtime_params.max_frames)) {
LOG(ERROR) << "Invalid max-frames: "
<< cmd_line->GetSwitchValueASCII("max-frames");
return false;
}
}
runtime_params.frame_dir = cmd_line->GetSwitchValuePath("frame-dir");
runtime_params.chrome_path = cmd_line->GetSwitchValuePath("chrome-path");
if (!runtime_params.frame_dir.empty()) {
scenarios = {"user_provided_frames"};
} else {
std::string scenarios_str = cmd_line->GetSwitchValueASCII("scenarios");
if (scenarios_str == "all" || scenarios_str.empty()) {
scenarios = {"desktop_clock", "scroll_vertical", "scroll_horizontal",
"moving_window", "busy_developer", "low_motion",
"high_motion", "chaos_stress"};
} else {
scenarios = base::SplitString(scenarios_str, ",", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
}
}
if (cmd_line->HasSwitch("sizes")) {
std::string sizes_str = cmd_line->GetSwitchValueASCII("sizes");
std::vector<std::string> size_strs = base::SplitString(
sizes_str, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (const auto& size_str : size_strs) {
if (size_str == "1080p") {
sizes.emplace_back(1920, 1080);
} else if (size_str == "1440p") {
sizes.emplace_back(2560, 1440);
} else if (size_str == "4k" || size_str == "2160p") {
sizes.emplace_back(3840, 2160);
} else {
std::vector<std::string> dims = base::SplitString(
size_str, "x", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (dims.size() == 2) {
int width, height;
if (base::StringToInt(dims[0], &width) &&
base::StringToInt(dims[1], &height)) {
sizes.emplace_back(width, height);
} else {
LOG(ERROR) << "Invalid size dimensions: " << size_str;
return false;
}
} else {
LOG(ERROR) << "Invalid size format (expected WxH or name): "
<< size_str;
return false;
}
}
}
} else {
sizes = {{1920, 1080}, {2560, 1440}, {3840, 2160}};
}
return true;
}
} // namespace
} // namespace remoting
int main(int argc, char** argv) {
base::CommandLine::Init(argc, argv);
base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
if (cmd_line->HasSwitch("help")) {
remoting::PrintHelp(argv[0]);
return 0;
}
TestTimeouts::Initialize();
base::test::TaskEnvironment task_environment;
remoting::EncoderParams encoder_params;
remoting::RuntimeParams base_runtime_params;
std::vector<std::string> scenarios;
std::vector<webrtc::DesktopSize> sizes;
if (!remoting::ParseCommandLine(cmd_line, encoder_params, base_runtime_params,
scenarios, sizes)) {
return 1;
}
base_runtime_params.frame_count = static_cast<int>(
base_runtime_params.duration.InSecondsF() * base_runtime_params.fps);
if (base_runtime_params.frame_count > base_runtime_params.max_frames) {
LOG(WARNING) << "Capping frame count at " << base_runtime_params.max_frames
<< " to save memory. (4K frames are ~33MB each). Use "
"--max-frames to override.";
base_runtime_params.frame_count = base_runtime_params.max_frames;
}
remoting::EncoderBenchmark benchmark;
for (const auto& scenario : scenarios) {
for (const auto& size : sizes) {
remoting::RuntimeParams runtime_params = base_runtime_params;
runtime_params.size = size;
runtime_params.scenario = scenario;
benchmark.Run(encoder_params, runtime_params);
}
}
return 0;
}