Merge pull request #1052 from OpenShot/omp-ffmpeg-thread-control

Improved OMP/FFmpeg Thread Control (Settings)
This commit is contained in:
Jonathan Thomas
2026-03-30 18:11:01 -05:00
committed by GitHub
9 changed files with 336 additions and 35 deletions
+3 -3
View File
@@ -20,9 +20,9 @@
#include "Settings.h"
// Calculate the # of OpenMP Threads to allow
#define OPEN_MP_NUM_PROCESSORS std::min(omp_get_num_procs(), std::max(2, openshot::Settings::Instance()->OMP_THREADS))
#define FF_VIDEO_NUM_PROCESSORS std::min(omp_get_num_procs(), std::max(2, openshot::Settings::Instance()->FF_THREADS))
#define FF_AUDIO_NUM_PROCESSORS std::min(omp_get_num_procs(), std::max(2, openshot::Settings::Instance()->FF_THREADS))
#define OPEN_MP_NUM_PROCESSORS openshot::Settings::Instance()->EffectiveOMPThreads()
#define FF_VIDEO_NUM_PROCESSORS std::clamp(openshot::Settings::Instance()->FF_THREADS, 2, openshot::Settings::Instance()->MaxAllowedThreads())
#define FF_AUDIO_NUM_PROCESSORS std::clamp(openshot::Settings::Instance()->FF_THREADS, 2, openshot::Settings::Instance()->MaxAllowedThreads())
// Set max-active-levels to the max supported, if possible
// (supported_active_levels is OpenMP 5.0 (November 2018) or later, only.)
+27 -2
View File
@@ -10,6 +10,7 @@
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include <algorithm>
#include <cstdlib>
#include <omp.h>
#include "Settings.h"
@@ -19,18 +20,42 @@ using namespace openshot;
// Global reference to Settings
Settings *Settings::m_pInstance = nullptr;
int Settings::EffectiveOMPThreads() const
{
return std::clamp(OMP_THREADS, 2, MaxAllowedThreads());
}
int Settings::MaxAllowedThreads() const
{
return std::max(2, std::max(2, omp_get_num_procs()) * 3);
}
void Settings::ApplyOpenMPSettings()
{
const int requested_threads = EffectiveOMPThreads();
if (applied_omp_threads != requested_threads) {
omp_set_num_threads(requested_threads);
applied_omp_threads = requested_threads;
}
}
// Create or Get an instance of the settings singleton
Settings *Settings::Instance()
{
if (!m_pInstance) {
// Create the actual instance of Settings only once
m_pInstance = new Settings;
m_pInstance->OMP_THREADS = omp_get_num_procs();
m_pInstance->FF_THREADS = omp_get_num_procs();
const int machine_threads = std::max(2, omp_get_num_procs());
m_pInstance->default_omp_threads = machine_threads;
m_pInstance->default_ff_threads = machine_threads;
m_pInstance->OMP_THREADS = machine_threads;
m_pInstance->FF_THREADS = machine_threads;
auto env_debug = std::getenv("LIBOPENSHOT_DEBUG");
if (env_debug != nullptr)
m_pInstance->DEBUG_TO_STDERR = true;
}
m_pInstance->ApplyOpenMPSettings();
return m_pInstance;
}
+26 -2
View File
@@ -46,6 +46,15 @@ namespace openshot {
/// Private variable to keep track of singleton instance
static Settings * m_pInstance;
/// Last OMP thread count applied to the OpenMP runtime
int applied_omp_threads = 0;
/// Machine default OpenMP thread count detected at startup
int default_omp_threads = 2;
/// Machine default FFmpeg thread count detected at startup
int default_ff_threads = 2;
public:
/**
* @brief Use video codec for faster video decoding (if supported)
@@ -64,8 +73,8 @@ namespace openshot {
/// Scale mode used in FFmpeg decoding and encoding (used as an optimization for faster previews)
bool HIGH_QUALITY_SCALING = false;
/// Number of threads of OpenMP
int OMP_THREADS = 16;
/// Number of OpenMP threads
int OMP_THREADS = 2;
/// Number of threads that ffmpeg uses
int FF_THREADS = 16;
@@ -116,6 +125,21 @@ namespace openshot {
/// Whether to dump ZeroMQ debug messages to stderr
bool DEBUG_TO_STDERR = false;
/// Return the effective OpenMP worker budget used by libopenshot heuristics
int EffectiveOMPThreads() const;
/// Return the maximum allowed thread override based on this machine
int MaxAllowedThreads() const;
/// Return the machine default OpenMP thread count detected at startup
int DefaultOMPThreads() const { return default_omp_threads; }
/// Return the machine default FFmpeg thread count detected at startup
int DefaultFFThreads() const { return default_ff_threads; }
/// Apply any explicit OpenMP thread override to the runtime
void ApplyOpenMPSettings();
/// Create or get an instance of this logger singleton (invoke the class with this method)
static Settings * Instance();
};
+33 -21
View File
@@ -9,12 +9,12 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
#include <chrono>
#include <cstdlib>
#include <functional>
#include <iostream>
#include <string>
#include <vector>
#include "BenchmarkOptions.h"
#include "Clip.h"
#include "FFmpegReader.h"
#include "FFmpegWriter.h"
@@ -26,6 +26,7 @@
#include "QtImageReader.h"
#endif
#include "ReaderBase.h"
#include "Settings.h"
#include "Timeline.h"
#include "effects/Brightness.h"
#include "effects/ChromaKey.h"
@@ -62,24 +63,35 @@ int main(int argc, char* argv[]) {
const string video = base + "sintel_trailer-720p.mp4";
const string mask_img = base + "mask.png";
const string overlay = base + "front3.png";
string filter_test;
bool list_only = false;
benchmark::BenchmarkOptions options;
const int64_t chroma_bench_frames = 500;
for (int i = 1; i < argc; ++i) {
const string arg = argv[i];
if ((arg == "--test" || arg == "-t") && i + 1 < argc) {
filter_test = argv[++i];
} else if (arg == "--list" || arg == "-l") {
list_only = true;
} else if (arg == "--help" || arg == "-h") {
cout << "Usage: openshot-benchmark [--test <name>] [--list]\n";
return 0;
} else {
cerr << "Unknown argument: " << arg << "\n";
cerr << "Usage: openshot-benchmark [--test <name>] [--list]\n";
return 1;
}
try {
vector<string> args;
args.reserve(std::max(0, argc - 1));
for (int i = 1; i < argc; ++i)
args.emplace_back(argv[i]);
options = benchmark::ParseBenchmarkOptions(args);
} catch (const std::exception& e) {
cerr << e.what() << "\n";
cerr << benchmark::BenchmarkUsage() << "\n";
return 1;
}
if (options.show_help) {
cout << benchmark::BenchmarkUsage() << "\n";
return 0;
}
// Route benchmark thread settings through libopenshot's Settings singleton,
// matching how an application should configure the library before opening readers.
Settings *settings = Settings::Instance();
if (options.omp_threads > 0) {
settings->OMP_THREADS = options.omp_threads;
settings->ApplyOpenMPSettings();
}
if (options.ff_threads > 0) {
settings->FF_THREADS = options.ff_threads;
}
vector<Trial> trials;
@@ -278,7 +290,7 @@ int main(int argc, char* argv[]) {
r.Close();
});
if (list_only) {
if (options.list_only) {
for (const auto& trial : trials)
cout << trial.first << "\n";
return 0;
@@ -288,14 +300,14 @@ int main(int argc, char* argv[]) {
double total = 0.0;
int executed = 0;
for (const auto& trial : trials) {
if (!filter_test.empty() && trial.first != filter_test)
if (!options.filter_test.empty() && trial.first != options.filter_test)
continue;
total += time_trial(trial.first, trial.second);
executed++;
}
if (!filter_test.empty() && executed == 0) {
cerr << "Unknown test: " << filter_test << "\nAvailable tests:\n";
if (!options.filter_test.empty() && executed == 0) {
cerr << "Unknown test: " << options.filter_test << "\nAvailable tests:\n";
for (const auto& trial : trials)
cerr << " " << trial.first << "\n";
return 2;
+75
View File
@@ -0,0 +1,75 @@
/**
* @file
* @brief Unit tests for benchmark CLI option parsing
* @author OpenShot Studios, LLC
*
* @ref License
*/
// Copyright (c) 2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "openshot_catch.h"
#include "BenchmarkOptions.h"
using namespace openshot::benchmark;
static void CHECK_RUNTIME_ERROR_CONTAINS(const std::vector<std::string>& args,
const std::string& expected_fragment) {
try {
(void) ParseBenchmarkOptions(args);
FAIL("Expected ParseBenchmarkOptions() to throw std::runtime_error");
} catch (const std::runtime_error& e) {
CHECK(std::string(e.what()).find(expected_fragment) != std::string::npos);
} catch (...) {
FAIL("Expected std::runtime_error");
}
}
TEST_CASE("Benchmark usage string includes new thread flags", "[benchmark][args]") {
const std::string usage = BenchmarkUsage();
CHECK(usage.find("--omp-threads <n>") != std::string::npos);
CHECK(usage.find("--ff-threads <n>") != std::string::npos);
}
TEST_CASE("Benchmark args default correctly", "[benchmark][args]") {
const BenchmarkOptions options = ParseBenchmarkOptions({});
CHECK(options.filter_test.empty());
CHECK_FALSE(options.list_only);
CHECK_FALSE(options.show_help);
CHECK(options.omp_threads == 0);
CHECK(options.ff_threads == 0);
}
TEST_CASE("Benchmark args parse valid values", "[benchmark][args]") {
const BenchmarkOptions options = ParseBenchmarkOptions({
"--test", "Timeline",
"--list",
"--omp-threads", "12",
"--ff-threads", "16"
});
CHECK(options.filter_test == "Timeline");
CHECK(options.list_only);
CHECK_FALSE(options.show_help);
CHECK(options.omp_threads == 12);
CHECK(options.ff_threads == 16);
}
TEST_CASE("Benchmark args reject invalid thread values", "[benchmark][args]") {
CHECK_RUNTIME_ERROR_CONTAINS({"--omp-threads", "0"}, "Invalid --omp-threads value");
CHECK_RUNTIME_ERROR_CONTAINS({"--omp-threads", "1"}, "Invalid --omp-threads value");
CHECK_RUNTIME_ERROR_CONTAINS({"--omp-threads", "-1"}, "Invalid --omp-threads value");
CHECK_RUNTIME_ERROR_CONTAINS({"--ff-threads", "0"}, "Invalid --ff-threads value");
CHECK_RUNTIME_ERROR_CONTAINS({"--ff-threads", "1"}, "Invalid --ff-threads value");
CHECK_RUNTIME_ERROR_CONTAINS({"--ff-threads", "-1"}, "Invalid --ff-threads value");
CHECK_RUNTIME_ERROR_CONTAINS({"--omp-threads", "abc"}, "Invalid --omp-threads value");
}
TEST_CASE("Benchmark args reject missing values and unknown args", "[benchmark][args]") {
CHECK_RUNTIME_ERROR_CONTAINS({"--test"}, "Missing value for --test");
CHECK_RUNTIME_ERROR_CONTAINS({"--ff-threads"}, "Missing value for --ff-threads");
CHECK_RUNTIME_ERROR_CONTAINS({"--wat"}, "Unknown argument");
}
+76
View File
@@ -0,0 +1,76 @@
/**
* @file
* @brief Shared benchmark CLI option parsing helpers
* @author OpenShot Studios, LLC
*
* @ref License
*/
// Copyright (c) 2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "BenchmarkOptions.h"
#include <stdexcept>
namespace openshot {
namespace benchmark {
std::string BenchmarkUsage() {
return "Usage: openshot-benchmark [--test <name>] [--list] [--omp-threads <n>] [--ff-threads <n>]";
}
static int ParseThreadArg(const std::string& flag, const std::string& value) {
int parsed = 0;
try {
size_t consumed = 0;
parsed = std::stoi(value, &consumed);
if (consumed != value.size()) {
throw std::invalid_argument("extra characters");
}
} catch (const std::exception&) {
throw std::runtime_error("Invalid " + flag + " value: " + value + " (expected >= 2)");
}
if (parsed < 2) {
throw std::runtime_error("Invalid " + flag + " value: " + value + " (expected >= 2)");
}
return parsed;
}
BenchmarkOptions ParseBenchmarkOptions(const std::vector<std::string>& args) {
BenchmarkOptions options;
for (size_t i = 0; i < args.size(); ++i) {
const std::string& arg = args[i];
if (arg == "--test" || arg == "-t") {
if (i + 1 >= args.size()) {
throw std::runtime_error("Missing value for --test");
}
options.filter_test = args[++i];
} else if (arg == "--omp-threads") {
if (i + 1 >= args.size()) {
throw std::runtime_error("Missing value for --omp-threads");
}
options.omp_threads = ParseThreadArg("--omp-threads", args[++i]);
} else if (arg == "--ff-threads") {
if (i + 1 >= args.size()) {
throw std::runtime_error("Missing value for --ff-threads");
}
options.ff_threads = ParseThreadArg("--ff-threads", args[++i]);
} else if (arg == "--list" || arg == "-l") {
options.list_only = true;
} else if (arg == "--help" || arg == "-h") {
options.show_help = true;
} else {
throw std::runtime_error("Unknown argument: " + arg);
}
}
return options;
}
} // namespace benchmark
} // namespace openshot
+36
View File
@@ -0,0 +1,36 @@
/**
* @file
* @brief Shared benchmark CLI option parsing helpers
* @author OpenShot Studios, LLC
*
* @ref License
*/
// Copyright (c) 2026 OpenShot Studios, LLC
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#ifndef OPENSHOT_BENCHMARK_OPTIONS_H
#define OPENSHOT_BENCHMARK_OPTIONS_H
#include <string>
#include <vector>
namespace openshot {
namespace benchmark {
struct BenchmarkOptions {
std::string filter_test;
bool list_only = false;
bool show_help = false;
int omp_threads = 0;
int ff_threads = 0;
};
std::string BenchmarkUsage();
BenchmarkOptions ParseBenchmarkOptions(const std::vector<std::string>& args);
} // namespace benchmark
} // namespace openshot
#endif
+9 -2
View File
@@ -17,7 +17,7 @@ endif()
file(TO_NATIVE_PATH "${PROJECT_SOURCE_DIR}/examples/" TEST_MEDIA_PATH)
# Benchmark executable
add_executable(openshot-benchmark Benchmark.cpp)
add_executable(openshot-benchmark Benchmark.cpp BenchmarkOptions.cpp)
target_compile_definitions(openshot-benchmark PRIVATE -DTEST_MEDIA_PATH="${TEST_MEDIA_PATH}")
target_link_libraries(openshot-benchmark openshot)
@@ -54,6 +54,7 @@ set(OPENSHOT_TESTS
Crop
LensFlare
AnalogTape
BenchmarkArgs
EffectMask
Mask
Sharpen
@@ -113,9 +114,15 @@ else()
endif()
foreach(tname ${OPENSHOT_TESTS})
add_executable(openshot-${tname}-test
set(test_sources
${tname}.cpp
)
if(tname STREQUAL "BenchmarkArgs")
list(APPEND test_sources BenchmarkOptions.cpp)
endif()
add_executable(openshot-${tname}-test
${test_sources}
)
target_link_libraries(openshot-${tname}-test
openshot_catch2
openshot
+51 -5
View File
@@ -12,6 +12,8 @@
#include "openshot_catch.h"
#include <algorithm>
#include "OpenMPUtilities.h"
#include "Settings.h"
#include <omp.h>
@@ -20,14 +22,17 @@ using namespace openshot;
TEST_CASE( "Constructor", "[libopenshot][settings]" )
{
// Get system cpu count
int cpu_count = omp_get_num_procs();
int cpu_count = std::max(2, omp_get_num_procs());
// Create an empty color
Settings *s = Settings::Instance();
CHECK(s->OMP_THREADS == cpu_count);
CHECK(s->FF_THREADS == cpu_count);
CHECK(s->DefaultOMPThreads() == cpu_count);
CHECK(s->DefaultFFThreads() == cpu_count);
CHECK(s->EffectiveOMPThreads() == cpu_count);
CHECK(omp_get_max_threads() == cpu_count);
CHECK_FALSE(s->HIGH_QUALITY_SCALING);
}
@@ -35,14 +40,55 @@ TEST_CASE( "Change settings", "[libopenshot][settings]" )
{
// Create an empty color
Settings *s = Settings::Instance();
s->OMP_THREADS = 13;
int original_runtime_threads = omp_get_max_threads();
int original_omp_threads = s->OMP_THREADS;
int original_ff_threads = s->FF_THREADS;
const int requested_omp_threads = std::min(13, s->MaxAllowedThreads());
s->OMP_THREADS = requested_omp_threads;
s->FF_THREADS = 12;
s->HIGH_QUALITY_SCALING = true;
Settings::Instance();
CHECK(s->OMP_THREADS == 13);
CHECK(s->OMP_THREADS == requested_omp_threads);
CHECK(s->FF_THREADS == 12);
CHECK(s->EffectiveOMPThreads() == requested_omp_threads);
CHECK(s->HIGH_QUALITY_SCALING == true);
CHECK(omp_get_max_threads() == requested_omp_threads);
CHECK(Settings::Instance()->OMP_THREADS == 13);
CHECK(Settings::Instance()->OMP_THREADS == requested_omp_threads);
CHECK(Settings::Instance()->FF_THREADS == 12);
CHECK(Settings::Instance()->EffectiveOMPThreads() == requested_omp_threads);
CHECK(Settings::Instance()->HIGH_QUALITY_SCALING == true);
// Restore prior OpenMP runtime state for later tests.
s->OMP_THREADS = original_omp_threads;
s->FF_THREADS = original_ff_threads;
Settings::Instance();
omp_set_num_threads(original_runtime_threads);
}
TEST_CASE( "Clamp settings to machine limits", "[libopenshot][settings]" )
{
Settings *s = Settings::Instance();
const int original_omp_threads = s->OMP_THREADS;
const int original_ff_threads = s->FF_THREADS;
const int original_runtime_threads = omp_get_max_threads();
const int max_threads = s->MaxAllowedThreads();
s->OMP_THREADS = max_threads + 50;
s->FF_THREADS = max_threads + 50;
Settings::Instance();
CHECK(s->EffectiveOMPThreads() == max_threads);
CHECK(OPEN_MP_NUM_PROCESSORS == max_threads);
CHECK(FF_VIDEO_NUM_PROCESSORS == max_threads);
CHECK(FF_AUDIO_NUM_PROCESSORS == max_threads);
CHECK(omp_get_max_threads() == max_threads);
s->OMP_THREADS = original_omp_threads;
s->FF_THREADS = original_ff_threads;
Settings::Instance();
omp_set_num_threads(original_runtime_threads);
}
TEST_CASE( "Debug logging", "[libopenshot][settings][environment]")