diff --git a/src/OpenMPUtilities.h b/src/OpenMPUtilities.h index a50780b45..a8f45dc5e 100644 --- a/src/OpenMPUtilities.h +++ b/src/OpenMPUtilities.h @@ -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.) diff --git a/src/Settings.cpp b/src/Settings.cpp index b2c16045e..4ff210db2 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -10,6 +10,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later +#include #include #include #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; } diff --git a/src/Settings.h b/src/Settings.h index 7ad8eb96a..7a013c9aa 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -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(); }; diff --git a/tests/Benchmark.cpp b/tests/Benchmark.cpp index 768fb4fae..14ec3d980 100644 --- a/tests/Benchmark.cpp +++ b/tests/Benchmark.cpp @@ -9,12 +9,12 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #include -#include #include #include #include #include +#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 ] [--list]\n"; - return 0; - } else { - cerr << "Unknown argument: " << arg << "\n"; - cerr << "Usage: openshot-benchmark [--test ] [--list]\n"; - return 1; - } + try { + vector 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 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; diff --git a/tests/BenchmarkArgs.cpp b/tests/BenchmarkArgs.cpp new file mode 100644 index 000000000..1577eb095 --- /dev/null +++ b/tests/BenchmarkArgs.cpp @@ -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& 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 ") != std::string::npos); + CHECK(usage.find("--ff-threads ") != 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"); +} diff --git a/tests/BenchmarkOptions.cpp b/tests/BenchmarkOptions.cpp new file mode 100644 index 000000000..94093e15e --- /dev/null +++ b/tests/BenchmarkOptions.cpp @@ -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 + +namespace openshot { +namespace benchmark { + +std::string BenchmarkUsage() { + return "Usage: openshot-benchmark [--test ] [--list] [--omp-threads ] [--ff-threads ]"; +} + +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& 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 diff --git a/tests/BenchmarkOptions.h b/tests/BenchmarkOptions.h new file mode 100644 index 000000000..cf27c696e --- /dev/null +++ b/tests/BenchmarkOptions.h @@ -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 +#include + +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& args); + +} // namespace benchmark +} // namespace openshot + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 82f4d07ad..2d44a8a9b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 diff --git a/tests/Settings.cpp b/tests/Settings.cpp index 0e300e34f..65692b596 100644 --- a/tests/Settings.cpp +++ b/tests/Settings.cpp @@ -12,6 +12,8 @@ #include "openshot_catch.h" +#include +#include "OpenMPUtilities.h" #include "Settings.h" #include @@ -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]")