Page MenuHomeFreeBSD

D59276.id185947.diff
No OneTemporary

D59276.id185947.diff

diff --git a/contrib/kyua/cli/cmd_config_test.cpp b/contrib/kyua/cli/cmd_config_test.cpp
--- a/contrib/kyua/cli/cmd_config_test.cpp
+++ b/contrib/kyua/cli/cmd_config_test.cpp
@@ -62,6 +62,7 @@
config::tree user_config = engine::default_config();
user_config.set_string("architecture", "the-architecture");
user_config.set_string("execenvs", "the-env");
+ user_config.set_string("execplan", "none");
user_config.set_string("parallelism", "128");
user_config.set_string("platform", "the-platform");
//user_config.set_string("unprivileged_user", "");
@@ -87,10 +88,11 @@
ATF_REQUIRE_EQ(6, ui.out_log().size());
ATF_REQUIRE_EQ("architecture = the-architecture", ui.out_log()[0]);
ATF_REQUIRE_EQ("execenvs = the-env", ui.out_log()[1]);
- ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[2]);
- ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[3]);
- ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[4]);
- ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[5]);
+ ATF_REQUIRE_EQ("execlan = none", ui.out_log()[2]);
+ ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[3]);
+ ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[4]);
+ ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[5]);
+ ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[6]);
ATF_REQUIRE(ui.err_log().empty());
}
diff --git a/contrib/kyua/doc/kyua.conf.5.in b/contrib/kyua/doc/kyua.conf.5.in
--- a/contrib/kyua/doc/kyua.conf.5.in
+++ b/contrib/kyua/doc/kyua.conf.5.in
@@ -37,6 +37,7 @@
Variables:
.Va architecture ,
.Va execenvs ,
+.Va execplan ,
.Va platform ,
.Va test_suites ,
.Va unprivileged_user .
@@ -81,6 +82,31 @@
See
.Xr kyuafile 5
for the list of possible execution environments.
+.It Va execplan
+Execution plan name.
+Supported plans: "time", "none" (default).
+By default, "none" does not provide any special scheduling,
+preserving the original approach of test execution.
+The "time" plan uses a test profile to schedule a time-optimized test
+execution.
+If a test profile is not found, the original approach is used.
+A test profile is generated upon completion of the kyua test command,
+regardless of the execution plan used.
+The location of the test profiles is chosen in this order:
+.Bl -enum -offset indent
+.It
+.Pa ${HOME}/.kyua/profiles/
+if
+.Va HOME
+is defined.
+.It
+.Pa ${TMPDIR}/
+if
+.Va TMPDIR
+is defined.
+.It
+.Pa /tmp/ .
+.El
.It Va parallelism
Maximum number of test cases to execute concurrently.
.It Va platform
diff --git a/contrib/kyua/drivers/run_tests.cpp b/contrib/kyua/drivers/run_tests.cpp
--- a/contrib/kyua/drivers/run_tests.cpp
+++ b/contrib/kyua/drivers/run_tests.cpp
@@ -35,6 +35,7 @@
#include "engine/kyuafile.hpp"
#include "engine/scanner.hpp"
#include "engine/scheduler.hpp"
+#include "engine/execplan/execplan.hpp"
#include "model/context.hpp"
#include "model/metadata.hpp"
#include "model/test_case.hpp"
@@ -270,23 +271,23 @@
(void)tx.put_context(context);
}
- engine::scanner scanner(kyuafile.test_programs(), filters);
+ auto execplan = engine::execplan::get(kyuafile, filters, user_config);
+ execplan->init();
path_to_id_map ids_cache;
pid_to_id_map in_flight;
std::vector< engine::scan_result > exclusive_tests;
- const std::size_t slots = user_config.lookup< config::positive_int_node >(
- "parallelism");
+ std::size_t slots = execplan->max_slots();
+
INV(slots >= 1);
do {
- INV(in_flight.size() <= slots);
-
// Spawn as many jobs as needed to fill our execution slots. We do this
// first with the assumption that the spawning is faster than any single
// job, so we want to keep as many jobs in the background as possible.
while (in_flight.size() < slots) {
- optional< engine::scan_result > match = scanner.yield();
+ optional< engine::scan_result > match = execplan->next();
+ slots = execplan->max_slots();
if (!match)
break;
const model::test_program_ptr test_program = match.get().first;
@@ -323,8 +324,9 @@
in_flight.erase(iter);
finish_test(result_handle, test_case_id, tx, hooks);
+ execplan->feedback(result_handle);
}
- } while (!in_flight.empty() || !scanner.done());
+ } while (!in_flight.empty() || !execplan->done());
// Run any exclusive tests that we spotted earlier sequentially.
for (std::vector< engine::scan_result >::const_iterator
@@ -340,5 +342,7 @@
handle.cleanup();
- return result(scanner.unused_filters());
+ execplan->fini();
+
+ return result(execplan->unused_filters());
}
diff --git a/contrib/kyua/engine/config.cpp b/contrib/kyua/engine/config.cpp
--- a/contrib/kyua/engine/config.cpp
+++ b/contrib/kyua/engine/config.cpp
@@ -62,6 +62,7 @@
{
tree.define< config::string_node >("architecture");
tree.define< config::strings_set_node >("execenvs");
+ tree.define< config::string_node >("execplan");
tree.define< config::positive_int_node >("parallelism");
tree.define< config::string_node >("platform");
tree.define< engine::user_node >("unprivileged_user");
@@ -85,6 +86,8 @@
supported.insert(execenv::default_execenv_name);
tree.set< config::strings_set_node >("execenvs", supported);
+ tree.set< config::string_node >("execplan", "none");
+
// TODO(jmmv): Automatically derive this from the number of CPUs in the
// machine and forcibly set to a value greater than 1. Still testing
// the new parallel implementation as of 2015-02-27 though.
@@ -246,6 +249,7 @@
std::set< std::string > supported;
supported.insert(engine::execenv::default_execenv_name);
tree.set< config::strings_set_node >("execenvs", supported);
+ tree.set< config::string_node >("execplan", "none");
return tree;
}
diff --git a/contrib/kyua/engine/execplan/execplan.hpp b/contrib/kyua/engine/execplan/execplan.hpp
new file mode 100644
--- /dev/null
+++ b/contrib/kyua/engine/execplan/execplan.hpp
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: BSD-2-Clause
+//
+// Copyright (c) 2026 Igor Ostapenko <igoro@FreeBSD.org>
+
+/// \file engine/execplan/execplan.hpp
+/// Execution plan subsystem interface.
+
+#if !defined(ENGINE_EXECPLAN_EXECPLAN_HPP)
+#define ENGINE_EXECPLAN_EXECPLAN_HPP
+
+#include <set>
+
+#include "engine/filters_fwd.hpp"
+#include "engine/kyuafile_fwd.hpp"
+#include "engine/scanner_fwd.hpp"
+#include "engine/scheduler_fwd.hpp"
+#include "utils/config/tree.ipp"
+#include "utils/optional.ipp"
+
+using utils::optional;
+
+
+namespace engine {
+namespace execplan {
+
+
+/// Abstract interface of an execution plan.
+class interface {
+protected:
+ const kyuafile& _kyuafile;
+ const std::set< test_filter >& _test_filters;
+ const utils::config::tree& _user_config;
+
+public:
+ /// Constructor.
+ interface(const kyuafile& kyuafile,
+ const std::set< test_filter >& test_filters,
+ const utils::config::tree& user_config) :
+ _kyuafile(kyuafile),
+ _test_filters(test_filters),
+ _user_config(user_config)
+ {}
+
+ /// Destructor.
+ virtual ~interface() {}
+
+ /// Initializes execution plan.
+ virtual void init() = 0;
+
+ /// Max slots expectation.
+ virtual std::size_t max_slots() = 0;
+
+ /// The next test to execute.
+ virtual optional< scan_result > next() = 0;
+
+ /// Considers an executed test to potentially revise the plan.
+ virtual void feedback(scheduler::result_handle_ptr) = 0;
+
+ /// Returns whether the plan is drained.
+ virtual bool done() = 0;
+
+ /// Finalizes execution plan.
+ virtual void fini() = 0;
+
+ /// Returns back any unused filters.
+ virtual std::set< test_filter > unused_filters() = 0;
+};
+
+
+/// Returns execution plan instance for a kyuafile.
+std::unique_ptr< execplan::interface > get(
+ const kyuafile&,
+ const std::set< test_filter >&,
+ const utils::config::tree&);
+
+
+} // namespace execplan
+} // namespace engine
+
+#endif // !defined(ENGINE_EXECPLAN_EXECPLAN_HPP)
diff --git a/contrib/kyua/engine/execplan/execplan.cpp b/contrib/kyua/engine/execplan/execplan.cpp
new file mode 100644
--- /dev/null
+++ b/contrib/kyua/engine/execplan/execplan.cpp
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: BSD-2-Clause
+//
+// Copyright (c) 2026 Igor Ostapenko <igoro@FreeBSD.org>
+
+#include "engine/execplan/execplan.hpp"
+
+#include "engine/execplan/execplan_none.hpp"
+#include "engine/execplan/execplan_time.hpp"
+
+namespace config = utils::config;
+namespace execplan = engine::execplan;
+
+
+std::unique_ptr< execplan::interface >
+execplan::get(const engine::kyuafile& kyuafile,
+ const std::set< engine::test_filter >& test_filters,
+ const config::tree& user_config)
+{
+ const std::string name = user_config.lookup< config::string_node >(
+ "execplan");
+
+ if (name == "time")
+ return std::unique_ptr< execplan::interface >(
+ new execplan::execplan_time(kyuafile, test_filters, user_config));
+
+ return std::unique_ptr< execplan::interface >(
+ new execplan::execplan_none(kyuafile, test_filters, user_config));
+}
diff --git a/contrib/kyua/engine/execplan/execplan_none.hpp b/contrib/kyua/engine/execplan/execplan_none.hpp
new file mode 100644
--- /dev/null
+++ b/contrib/kyua/engine/execplan/execplan_none.hpp
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: BSD-2-Clause
+//
+// Copyright (c) 2026 Igor Ostapenko <igoro@FreeBSD.org>
+
+/// \file engine/execplan/execplan_none.hpp
+/// A naturally ordered execution without planning.
+
+#if !defined(ENGINE_EXECPLAN_EXECPLAN_NONE_HPP)
+#define ENGINE_EXECPLAN_EXECPLAN_NONE_HPP
+
+#include "engine/execplan/execplan.hpp"
+
+#include "engine/scanner.hpp"
+#include "engine/kyuafile.hpp"
+
+
+namespace engine {
+namespace execplan {
+
+
+class execplan_none : public interface {
+protected:
+ scanner _scanner;
+
+public:
+ execplan_none(const kyuafile& kyuafile,
+ const std::set< test_filter >& test_filters,
+ const utils::config::tree& user_config) :
+ interface(kyuafile, test_filters, user_config),
+ _scanner(scanner(kyuafile.test_programs(), test_filters))
+ {}
+
+ void init();
+ optional< scan_result > next();
+ std::size_t max_slots();
+ void feedback(scheduler::result_handle_ptr);
+ bool done();
+ void fini();
+ std::set< test_filter > unused_filters();
+};
+
+
+} // namespace execplan
+} // namespace engine
+
+#endif // !defined(ENGINE_EXECPLAN_EXECPLAN_NONE_HPP)
diff --git a/contrib/kyua/engine/execplan/execplan_none.cpp b/contrib/kyua/engine/execplan/execplan_none.cpp
new file mode 100644
--- /dev/null
+++ b/contrib/kyua/engine/execplan/execplan_none.cpp
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: BSD-2-Clause
+//
+// Copyright (c) 2026 Igor Ostapenko <igoro@FreeBSD.org>
+
+#include "engine/execplan/execplan_none.hpp"
+
+#include "engine/filters.hpp"
+
+using utils::none;
+
+
+namespace engine {
+namespace execplan {
+
+
+void
+execplan_none::init()
+{
+ // nothing to do
+}
+
+
+std::size_t
+execplan_none::max_slots()
+{
+ return _user_config.lookup< utils::config::positive_int_node >(
+ "parallelism");
+}
+
+
+optional< scan_result >
+execplan_none::next()
+{
+ return _scanner.yield();
+}
+
+
+void
+execplan_none::feedback(scheduler::result_handle_ptr)
+{
+ // nothing to do
+}
+
+
+bool
+execplan_none::done()
+{
+ return _scanner.done();
+}
+
+
+void
+execplan_none::fini()
+{
+ // nothing to do
+}
+
+
+std::set< test_filter >
+execplan_none::unused_filters()
+{
+ return _scanner.unused_filters();
+}
+
+
+} // namespace execplan
+} // namespace engine
diff --git a/contrib/kyua/engine/execplan/execplan_time.hpp b/contrib/kyua/engine/execplan/execplan_time.hpp
new file mode 100644
--- /dev/null
+++ b/contrib/kyua/engine/execplan/execplan_time.hpp
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: BSD-2-Clause
+//
+// Copyright (c) 2026 Igor Ostapenko <igoro@FreeBSD.org>
+
+/// \file engine/execplan/execplan_time.hpp
+/// A time balanced execution plan based on wrusage profile.
+
+#if !defined(ENGINE_EXECPLAN_EXECPLAN_TIME_HPP)
+#define ENGINE_EXECPLAN_EXECPLAN_TIME_HPP
+
+#include "engine/execplan/execplan.hpp"
+
+#include <memory>
+
+#include "engine/filters.hpp"
+#include "engine/kyuafile.hpp"
+#include "engine/execplan/profile.hpp"
+#include "model/test_case_fwd.hpp"
+
+
+namespace {
+
+
+struct brick {
+ engine::execplan::profile_record* record;
+ std::size_t air;
+ std::size_t rock;
+ std::size_t flood;
+};
+typedef std::unique_ptr< struct brick > brick_ptr;
+
+
+struct task;
+typedef std::unique_ptr<struct task> task_ptr;
+struct task {
+ engine::execplan::profile_record* record;
+ std::size_t air_left;
+ bool running;
+ bool done;
+ std::queue< task_ptr > cotasks;
+};
+
+
+struct bucket {
+ std::size_t roof;
+ std::size_t flooded;
+ bool vacant;
+ std::queue< task_ptr > tasks;
+};
+typedef std::unique_ptr< struct bucket > bucket_ptr;
+
+
+struct test_case_tag {
+ std::size_t bucket;
+};
+
+
+}
+
+
+namespace engine {
+namespace execplan {
+
+
+class execplan_time : public interface {
+protected:
+ engine::filters_state _filters_state;
+ profile _profile;
+ profile _new_profile;
+ std::vector< bucket_ptr > _buckets;
+ std::size_t _next_bucket = 0;
+ std::size_t _empty_buckets = 0;
+ std::size_t _test_program_idx = 0;
+ std::map< std::string, model::test_case >::const_iterator _test_case_it;
+ bool _done = false;
+
+public:
+ execplan_time(
+ const kyuafile& kyuafile,
+ const std::set< test_filter >& test_filters,
+ const utils::config::tree& user_config
+ ) :
+ interface(kyuafile, test_filters, user_config),
+ _filters_state(test_filters),
+ _profile(kyuafile),
+ _new_profile(kyuafile)
+ {}
+
+ void init();
+ optional< scan_result > next();
+ std::size_t max_slots();
+ void feedback(scheduler::result_handle_ptr);
+ bool done();
+ void fini();
+ std::set< test_filter > unused_filters();
+};
+
+
+} // namespace execplan
+} // namespace engine
+
+#endif // !defined(ENGINE_EXECPLAN_EXECPLAN_TIME_HPP)
diff --git a/contrib/kyua/engine/execplan/execplan_time.cpp b/contrib/kyua/engine/execplan/execplan_time.cpp
new file mode 100644
--- /dev/null
+++ b/contrib/kyua/engine/execplan/execplan_time.cpp
@@ -0,0 +1,349 @@
+// SPDX-License-Identifier: BSD-2-Clause
+//
+// Copyright (c) 2026 Igor Ostapenko <igoro@FreeBSD.org>
+
+#include "engine/execplan/execplan_time.hpp"
+
+#include "engine/filters.hpp"
+#include "engine/execplan/profile.hpp"
+#include "engine/scheduler.hpp"
+#include "model/exceptions.hpp"
+#include "model/test_case.hpp"
+#include "model/test_program.hpp"
+#include "utils/config/tree.hpp"
+#include "utils/optional.ipp"
+
+
+using utils::none;
+
+namespace config = utils::config;
+
+
+namespace engine {
+namespace execplan {
+
+
+void
+execplan_time::init()
+{
+ std::size_t nbuckets = _user_config.lookup< config::positive_int_node >(
+ "parallelism");
+ for (std::size_t i = 0; i < nbuckets; i++) {
+ auto b = std::make_unique< bucket >();
+ b->roof = 0;
+ b->flooded = 0;
+ _buckets.push_back(std::move(b));
+ }
+
+ _profile.load();
+
+ std::vector< brick_ptr > porous_bricks;
+ std::vector< brick_ptr > dense_bricks;
+
+ for (auto& r : _profile.records()) {
+ auto b = std::make_unique< brick >();
+ b->record = &r;
+ b->rock = r.utime + r.stime;
+ b->flood = (r.utime_children + r.stime_children) / nbuckets;
+ b->rock += b->flood;
+ if (b->rock > r.duration)
+ b->air = 0;
+ else
+ b->air = r.duration - b->rock;
+ b->record->taken = false;
+
+ auto b2 = std::make_unique< brick >();
+ b2->record = b->record;
+ b2->rock = b->rock;
+ b2->flood = b->flood;
+ b2->air = b->air;
+
+ porous_bricks.push_back(std::move(b));
+ dense_bricks.push_back(std::move(b2));
+ }
+
+ std::sort(porous_bricks.begin(), porous_bricks.end(),
+ [](auto& a, auto& b){
+ return a->air > b->air;
+ });
+ std::stable_sort(dense_bricks.begin(), dense_bricks.end(),
+ [](auto& a, auto& b){
+ std::ptrdiff_t ma = a->rock;
+ ma -= a->air;
+ std::ptrdiff_t mb = b->rock;
+ mb -= b->air;
+ return (ma > mb);
+ });
+
+ std::size_t ntaken = 0;
+ while (ntaken < _profile.records().size()) {
+
+ for (auto& b : _buckets)
+ b->vacant = true;
+
+ for (auto& pb : porous_bricks) {
+ if (pb->record->taken)
+ continue;
+ if (!_filters_state.match_test_case(
+ pb->record->test_program->relative_path(),
+ pb->record->test_case_name)) {
+ pb->record->taken = true;
+ ntaken++;
+ continue;
+ }
+ auto *best_bucket = &_buckets[0];
+ for (auto& b : _buckets)
+ if (b->roof < (*best_bucket)->roof)
+ best_bucket = &b;
+ if (!(*best_bucket)->vacant)
+ break;
+ (*best_bucket)->vacant = false;
+ (*best_bucket)->roof += pb->record->duration;
+ auto t = std::make_unique< task >();
+ t->record = pb->record;
+ t->running = false;
+ t->done = false;
+ t->air_left = pb->air;
+ (*best_bucket)->tasks.push(std::move(t));
+ if (pb->flood > 0)
+ for (auto& b : _buckets) {
+ if (&b == best_bucket)
+ continue;
+ b->flooded += pb->flood;
+ }
+ pb->record->taken = true;
+ ntaken++;
+ }
+ for (auto& b : _buckets) {
+ if (b->tasks.empty())
+ continue;
+ auto& task = b->tasks.back();
+ if (b->flooded <= task->air_left) {
+ task->air_left -= b->flooded;
+ b->flooded = 0;
+ } else {
+ b->flooded -= task->air_left;
+ task->air_left = 0;
+ }
+ }
+
+ for (auto& db : dense_bricks) {
+ if (db->record->taken)
+ continue;
+ if (!_filters_state.match_test_case(
+ db->record->test_program->relative_path(),
+ db->record->test_case_name)) {
+ db->record->taken = true;
+ ntaken++;
+ continue;
+ }
+ bucket_ptr *best_bucket = nullptr;
+ for (auto& bucket : _buckets) {
+ if (bucket->tasks.empty())
+ continue;
+ if (best_bucket == nullptr) {
+ best_bucket = &bucket;
+ continue;
+ }
+ if (bucket->tasks.back()->air_left > (*best_bucket)->tasks.back()->air_left)
+ best_bucket = &bucket;
+ }
+ if (best_bucket == nullptr)
+ break;
+ auto& t = (*best_bucket)->tasks.back();
+ if (t->air_left < db->rock)
+ continue;
+ auto cot = std::make_unique< task >();
+ cot->record = db->record;
+ cot->running = false;
+ cot->done = false;
+ cot->air_left = 0;
+ t->air_left -= db->rock;
+ t->cotasks.push(std::move(cot));
+ if (db->flood > 0)
+ for (auto& b : _buckets) {
+ if (&b == best_bucket)
+ continue;
+ b->flooded += db->flood;
+ if (b->tasks.empty())
+ continue;
+ auto& task = b->tasks.back();
+ if (b->flooded <= task->air_left) {
+ task->air_left -= b->flooded;
+ b->flooded = 0;
+ } else {
+ b->flooded -= task->air_left;
+ task->air_left = 0;
+ }
+ }
+ db->record->taken = true;
+ ntaken++;
+ }
+ }
+
+ for (auto& b : _buckets)
+ if (b->tasks.empty())
+ _empty_buckets++;
+
+ if (_test_program_idx < _kyuafile.test_programs().size()) {
+ auto tp = _kyuafile.test_programs().at(_test_program_idx);
+ _test_case_it = tp->test_cases().begin();
+ }
+}
+
+
+std::size_t
+execplan_time::max_slots()
+{
+ return _user_config.lookup< config::positive_int_node >("parallelism") * 2
+ - _empty_buckets;
+}
+
+
+optional< scan_result >
+execplan_time::next()
+{
+ if (_empty_buckets >= _buckets.size()) {
+ if (_test_program_idx >= _kyuafile.test_programs().size()) {
+ _done = true;
+ return none;
+ }
+ auto tp = _kyuafile.test_programs().at(_test_program_idx);
+ while (_test_case_it != tp->test_cases().end()) {
+ auto& tc = _test_case_it->second;
+ _test_case_it++;
+ if (tc.get_tag() != nullptr || !_filters_state.match_test_case(
+ tp->relative_path(), tc.name())) {
+ continue;
+ }
+ return utils::make_optional(scan_result(tp, tc.name()));
+ }
+ _test_program_idx++;
+ if (_test_program_idx >= _kyuafile.test_programs().size())
+ return none;
+ tp = _kyuafile.test_programs().at(_test_program_idx);
+ _test_case_it = tp->test_cases().begin();
+ return next();
+ }
+
+ auto& b = _buckets.at(_next_bucket);
+
+ if (b->tasks.empty()) {
+ _next_bucket = (_next_bucket + 1) % _buckets.size();
+ return next();
+ }
+
+ task_ptr *t = nullptr;
+ bool cotask = false;
+
+ while (!b->tasks.empty()) {
+ cotask = false;
+ t = &b->tasks.front();
+ if ((*t)->running == true) {
+ if ((*t)->cotasks.empty()) {
+ if ((*t)->done == true) {
+ b->tasks.pop();
+ t = nullptr;
+ continue;
+ } else {
+ return none;
+ }
+ }
+ t = &(*t)->cotasks.front();
+ cotask = true;
+ }
+ const model::test_cases_map& tcm = (*t)->record->test_program
+ ->test_cases();
+ const auto iter = tcm.find((*t)->record->test_case_name);
+ if (iter == tcm.end()) {
+ if (cotask)
+ (*t)->cotasks.pop();
+ else
+ b->tasks.pop();
+ t = nullptr;
+ continue;
+ }
+ const model::test_case& tc = (*iter).second;
+ test_case_tag* tag = new test_case_tag{_next_bucket};
+ tc.attach_tag(tag);
+ break;
+ }
+ if (b->tasks.empty())
+ _empty_buckets++;
+ if (t == nullptr) {
+ _next_bucket = (_next_bucket + 1) % _buckets.size();
+ return next();
+ }
+
+ auto result = scan_result((*t)->record->test_program,
+ (*t)->record->test_case_name);
+ _next_bucket = (_next_bucket + 1) % _buckets.size();
+ auto res = utils::make_optional(result);
+ if (cotask)
+ b->tasks.front()->cotasks.pop();
+ else {
+ (*t)->running = true;
+ if ((*t)->cotasks.empty()) {
+ b->tasks.pop();
+ if (b->tasks.empty())
+ _empty_buckets++;
+ }
+ }
+ return res;
+}
+
+
+void
+execplan_time::feedback(scheduler::result_handle_ptr result_handle)
+{
+ const scheduler::test_result_handle* test_result_handle =
+ dynamic_cast< const scheduler::test_result_handle* >(
+ result_handle.get());
+
+ auto tp = test_result_handle->test_program();
+ auto& tc = tp->find(test_result_handle->test_case_name());
+
+ void* tagp = tc.get_tag();
+ if (tagp) {
+ test_case_tag* tag = (test_case_tag*)tagp;
+ _next_bucket = tag->bucket;
+ auto& b = _buckets.at(_next_bucket);
+ if (!b->tasks.empty()) {
+ task_ptr *t = &b->tasks.front();
+ if ((*t)->running
+ && (*t)->record->test_program->relative_path()
+ == tp->relative_path()
+ && (*t)->record->test_case_name == tc.name()) {
+ (*t)->done = true;
+ }
+ }
+ }
+
+ _new_profile.add_record(result_handle);
+}
+
+
+bool
+execplan_time::done()
+{
+ return _done;
+}
+
+
+void
+execplan_time::fini()
+{
+ _profile.merge(_new_profile);
+ _profile.save();
+}
+
+
+std::set< test_filter >
+execplan_time::unused_filters()
+{
+ return _filters_state.unused();
+}
+
+
+} // namespace execplan
+} // namespace engine
diff --git a/contrib/kyua/engine/execplan/profile.hpp b/contrib/kyua/engine/execplan/profile.hpp
new file mode 100644
--- /dev/null
+++ b/contrib/kyua/engine/execplan/profile.hpp
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: BSD-2-Clause
+//
+// Copyright (c) 2026 Igor Ostapenko <igoro@FreeBSD.org>
+
+/// \file engine/execplan/profile.hpp
+/// Execution profile.
+
+#if !defined(ENGINE_EXECPLAN_PROFILE_HPP)
+#define ENGINE_EXECPLAN_PROFILE_HPP
+
+#include <string>
+#include <unordered_map>
+
+#include "engine/kyuafile_fwd.hpp"
+#include "engine/scheduler.hpp"
+#include "model/test_program_fwd.hpp"
+#include "utils/datetime.hpp"
+#include "utils/fs/path_fwd.hpp"
+
+
+namespace engine {
+namespace execplan {
+
+
+struct profile_record {
+ model::test_program_ptr test_program;
+ std::string test_case_name;
+ std::size_t duration;
+ std::size_t utime;
+ std::size_t stime;
+ std::size_t utime_children;
+ std::size_t stime_children;
+ bool taken;
+};
+
+
+class profile {
+protected:
+ const engine::kyuafile& _kyuafile;
+ std::unordered_map< std::string, model::test_program_ptr >
+ _test_programs;
+ std::vector< profile_record > _records;
+ const utils::fs::path get_path();
+
+public:
+ /// Constructor.
+ profile(const engine::kyuafile& kyuafile);
+
+ /// Destructor.
+ ~profile() {}
+
+ /// Adds a record.
+ void add_record(
+ const std::string& test_program_relpath,
+ const std::string& test_case_name,
+ const std::size_t duration,
+ const std::size_t utime,
+ const std::size_t stime,
+ const std::size_t utime_children,
+ const std::size_t stime_children);
+
+ void add_record(scheduler::result_handle_ptr);
+
+ void merge(profile&);
+
+ std::vector< profile_record >& records() { return _records; }
+
+ void save();
+ void load();
+};
+
+
+} // namespace execplan
+} // namespace engine
+
+#endif // !defined(ENGINE_EXECPLAN_PROFILE_HPP)
diff --git a/contrib/kyua/engine/execplan/profile.cpp b/contrib/kyua/engine/execplan/profile.cpp
new file mode 100644
--- /dev/null
+++ b/contrib/kyua/engine/execplan/profile.cpp
@@ -0,0 +1,242 @@
+// SPDX-License-Identifier: BSD-2-Clause
+//
+// Copyright (c) 2026 Igor Ostapenko <igoro@FreeBSD.org>
+
+#include "engine/execplan/profile.hpp"
+
+#include <charconv>
+#include <sstream>
+#include <regex>
+
+#include "engine/kyuafile.hpp"
+#include "model/test_case.hpp"
+#include "model/test_program.hpp"
+#include "model/test_result.hpp"
+#include "utils/env.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/optional.ipp"
+#include "utils/process/status.hpp"
+#include "utils/sanity.hpp"
+#include "utils/stream.hpp"
+
+namespace fs = utils::fs;
+
+
+namespace engine {
+namespace execplan {
+
+
+const fs::path
+profile::get_path()
+{
+ std::string name = std::regex_replace(
+ _kyuafile.source_root().to_absolute().str(),
+ std::regex(R"([^A-Za-z0-9_])"),
+ "_");
+ name = "kyua" + name + ".profile";
+
+ const utils::optional< std::string > home(utils::getenv("HOME"));
+ if (home)
+ return fs::path(home.get()) / ".kyua" / "profiles" / name;
+ const utils::optional< std::string > tmpdir(utils::getenv("TMPDIR"));
+ if (tmpdir)
+ return fs::path(tmpdir.get()) / name;
+ else
+ return fs::path("/tmp") / name;
+}
+
+
+profile::profile(const engine::kyuafile& kyuafile) :
+ _kyuafile(kyuafile)
+{
+ for (auto tp : kyuafile.test_programs())
+ _test_programs[tp->relative_path().str()] = tp;
+}
+
+
+void
+profile::add_record(
+ const std::string& test_program_relpath,
+ const std::string& test_case_name,
+ const std::size_t duration,
+ const std::size_t utime,
+ const std::size_t stime,
+ const std::size_t utime_children,
+ const std::size_t stime_children)
+{
+ auto it = _test_programs.find(test_program_relpath);
+ if (it == _test_programs.end())
+ return;
+
+ _records.push_back({
+ it->second,
+ test_case_name,
+ duration,
+ utime,
+ stime,
+ utime_children,
+ stime_children,
+ false
+ });
+}
+
+
+void
+profile::add_record(scheduler::result_handle_ptr result_handle)
+{
+ const scheduler::test_result_handle* test_result_handle =
+ dynamic_cast< const scheduler::test_result_handle* >(
+ result_handle.get());
+
+ auto tp = test_result_handle->test_program();
+ auto& tc = tp->find(test_result_handle->test_case_name());
+
+ if (!test_result_handle->status())
+ return;
+ auto& status = test_result_handle->status().get();
+ auto type = test_result_handle->test_result().type();
+ if (type != model::test_result_passed &&
+ type != model::test_result_expected_failure)
+ return;
+
+ auto duration = result_handle->end_time() - result_handle->start_time();
+ auto u = status.usage();
+ add_record(
+ tp->relative_path().str(),
+ tc.name(),
+ duration.to_microseconds(),
+ u->wru_self.ru_utime.tv_sec * 1000000 + u->wru_self.ru_utime.tv_usec,
+ u->wru_self.ru_stime.tv_sec * 1000000 + u->wru_self.ru_stime.tv_usec,
+ u->wru_children.ru_utime.tv_sec * 1000000 + u->wru_children.ru_utime.tv_usec,
+ u->wru_children.ru_stime.tv_sec * 1000000 + u->wru_children.ru_stime.tv_usec);
+}
+
+
+void
+profile::merge(profile& p)
+{
+ std::map< std::string, profile_record* > m;
+ for (auto& r : _records)
+ m[r.test_program->relative_path().str() + r.test_case_name] = &r;
+
+ for (auto& r : p.records()) {
+ auto prev = m.find(r.test_program->relative_path().str()
+ + r.test_case_name);
+ if (prev == m.end()) {
+ _records.push_back(r);
+ continue;
+ }
+ auto pr = prev->second;
+ pr->duration = r.duration;
+ pr->utime = r.utime;
+ pr->stime = r.stime;
+ pr->utime_children = r.utime_children;
+ pr->stime_children = r.stime_children;
+ }
+}
+
+
+void
+profile::save()
+{
+ auto path = get_path();
+ fs::mkdir_p(path.branch_path(), 0755);
+ auto file = utils::open_ostream(path);
+ for (auto& record : _records) {
+ *file << record.test_program->relative_path().str() << '\t'
+ << record.test_case_name << '\t'
+ << record.duration << '\t'
+ << record.utime << '\t'
+ << record.stime << '\t'
+ << record.utime_children << '\t'
+ << record.stime_children << '\t'
+ << '\n';
+ }
+ file->flush();
+}
+
+
+void
+profile::load()
+{
+ auto path = get_path();
+ if (!fs::exists(path))
+ return;
+
+ auto buf = utils::read_file(path);
+
+ std::istringstream stream(buf);
+ std::string line;
+ while (std::getline(stream, line)) {
+ std::istringstream line_stream(line);
+
+ std::string column;
+ std::vector<std::string> columns;
+ while (std::getline(line_stream, column, '\t'))
+ columns.push_back(column);
+ PRE(columns.size() >= 7);
+
+ std::size_t duration;
+ {
+ const std::string& s = columns[2];
+ auto [ptr, ec] = std::from_chars(
+ s.data(),
+ s.data() + s.size(),
+ duration);
+ if (!(ec == std::errc{} && ptr == s.data() + s.size()))
+ throw std::runtime_error(F("Invalid duration: %s") % s);
+ }
+
+ std::size_t utime;
+ {
+ const std::string& s = columns[3];
+ auto [ptr, ec] = std::from_chars(
+ s.data(),
+ s.data() + s.size(),
+ utime);
+ if (!(ec == std::errc{} && ptr == s.data() + s.size()))
+ throw std::runtime_error(F("Invalid utime: %s") % s);
+ }
+
+ std::size_t stime;
+ {
+ const std::string& s = columns[4];
+ auto [ptr, ec] = std::from_chars(
+ s.data(),
+ s.data() + s.size(),
+ stime);
+ if (!(ec == std::errc{} && ptr == s.data() + s.size()))
+ throw std::runtime_error(F("Invalid stime: %s") % s);
+ }
+
+ std::size_t utime_children;
+ {
+ const std::string& s = columns[5];
+ auto [ptr, ec] = std::from_chars(
+ s.data(),
+ s.data() + s.size(),
+ utime_children);
+ if (!(ec == std::errc{} && ptr == s.data() + s.size()))
+ throw std::runtime_error(F("Invalid utime_children: %s") % s);
+ }
+
+ std::size_t stime_children;
+ {
+ const std::string& s = columns[6];
+ auto [ptr, ec] = std::from_chars(
+ s.data(),
+ s.data() + s.size(),
+ stime_children);
+ if (!(ec == std::errc{} && ptr == s.data() + s.size()))
+ throw std::runtime_error(F("Invalid stime_children: %s") % s);
+ }
+
+ add_record(columns[0], columns[1], duration, utime, stime,
+ utime_children, stime_children);
+ }
+}
+
+
+} // namespace execplan
+} // namespace engine
diff --git a/contrib/kyua/engine/scheduler.hpp b/contrib/kyua/engine/scheduler.hpp
--- a/contrib/kyua/engine/scheduler.hpp
+++ b/contrib/kyua/engine/scheduler.hpp
@@ -190,7 +190,6 @@
protected:
struct bimpl;
-private:
/// Pointer to internal implementation of the base type.
std::shared_ptr< bimpl > _pbimpl;
@@ -227,6 +226,7 @@
const model::test_program_ptr test_program(void) const;
const std::string& test_case_name(void) const;
const model::test_result& test_result(void) const;
+ const utils::optional< utils::process::status >& status(void) const;
};
diff --git a/contrib/kyua/engine/scheduler.cpp b/contrib/kyua/engine/scheduler.cpp
--- a/contrib/kyua/engine/scheduler.cpp
+++ b/contrib/kyua/engine/scheduler.cpp
@@ -887,6 +887,16 @@
}
+/// Returns process status.
+///
+/// \return A process status.
+const utils::optional< utils::process::status >&
+scheduler::test_result_handle::status(void) const
+{
+ return _pbimpl->generic.status();
+}
+
+
/// Internal implementation for the scheduler_handle.
struct engine::scheduler::scheduler_handle::impl : utils::noncopyable {
/// Generic executor instance encapsulated by this one.
diff --git a/contrib/kyua/integration/cmd_config_test.sh b/contrib/kyua/integration/cmd_config_test.sh
--- a/contrib/kyua/integration/cmd_config_test.sh
+++ b/contrib/kyua/integration/cmd_config_test.sh
@@ -43,6 +43,7 @@
syntax(2)
architecture = "my-architecture"
execenvs = "my-env1 my-env2"
+execplan = "none"
parallelism = 256
platform = "my-platform"
unprivileged_user = "$(id -u -n)"
@@ -53,6 +54,7 @@
cat >expout <<EOF
architecture = my-architecture
execenvs = my-env1 my-env2
+execplan = "none"
parallelism = 256
platform = my-platform
test_suites.suite1.the_variable = value1
diff --git a/contrib/kyua/model/test_case.hpp b/contrib/kyua/model/test_case.hpp
--- a/contrib/kyua/model/test_case.hpp
+++ b/contrib/kyua/model/test_case.hpp
@@ -76,6 +76,9 @@
void attach_debugger(engine::debugger_ptr) const;
engine::debugger_ptr get_debugger() const;
+ void attach_tag(void*) const;
+ void* get_tag() const;
+
bool operator==(const test_case&) const;
bool operator!=(const test_case&) const;
};
diff --git a/contrib/kyua/model/test_case.cpp b/contrib/kyua/model/test_case.cpp
--- a/contrib/kyua/model/test_case.cpp
+++ b/contrib/kyua/model/test_case.cpp
@@ -63,6 +63,9 @@
/// Optional pointer to a debugger attached.
engine::debugger_ptr debugger;
+ /// Optional free pointer attached.
+ void* tag = nullptr;
+
/// Constructor.
///
/// \param name_ The name of the test case within the test program.
@@ -254,6 +257,20 @@
}
+void
+model::test_case::attach_tag(void* tag) const
+{
+ _pimpl->tag = tag;
+}
+
+
+void*
+model::test_case::get_tag() const
+{
+ return _pimpl->tag;
+}
+
+
/// Gets the fake result pre-stored for this test case.
///
/// \return A fake result, or none if not defined.
diff --git a/contrib/kyua/utils/process/operations.cpp b/contrib/kyua/utils/process/operations.cpp
--- a/contrib/kyua/utils/process/operations.cpp
+++ b/contrib/kyua/utils/process/operations.cpp
@@ -76,13 +76,14 @@
{
LD("Waiting for any child process");
int stat_loc;
- const pid_t pid = ::wait(&stat_loc);
+ struct __wrusage usage;
+ const pid_t pid = ::wait6(P_ALL, 0, &stat_loc, WEXITED, &usage, NULL);
if (pid == -1) {
const int original_errno = errno;
throw process::system_error("Failed to wait for any child process",
original_errno);
}
- return process::status(pid, stat_loc);
+ return process::status(pid, stat_loc, &usage);
}
diff --git a/contrib/kyua/utils/process/status.hpp b/contrib/kyua/utils/process/status.hpp
--- a/contrib/kyua/utils/process/status.hpp
+++ b/contrib/kyua/utils/process/status.hpp
@@ -37,6 +37,11 @@
#include <ostream>
#include <utility>
+extern "C" {
+#include <sys/time.h>
+#include <sys/resource.h>
+}
+
#include "utils/optional.ipp"
namespace utils {
@@ -57,10 +62,14 @@
/// The signal that terminated the program, if any, and if it dumped core.
optional< std::pair< int, bool > > _signaled;
+ /// Optional resource usage stats.
+ struct __wrusage _usage;
+
status(const optional< int >&, const optional< std::pair< int, bool > >&);
public:
status(const int, int);
+ status(const int, int, struct __wrusage* usage);
static status fake_exited(const int);
static status fake_signaled(const int, const bool);
@@ -72,6 +81,8 @@
bool signaled(void) const;
int termsig(void) const;
bool coredump(void) const;
+
+ const struct __wrusage* usage(void) const { return &_usage; }
};
diff --git a/contrib/kyua/utils/process/status.cpp b/contrib/kyua/utils/process/status.cpp
--- a/contrib/kyua/utils/process/status.cpp
+++ b/contrib/kyua/utils/process/status.cpp
@@ -62,6 +62,13 @@
}
+process::status::status(const int dead_pid_, int stat_loc, struct __wrusage* usage) :
+ process::status::status(dead_pid_, stat_loc)
+{
+ _usage = *usage;
+}
+
+
/// Constructs a new status object based on fake values.
///
/// \param exited_ If not none, specifies the exit status of the program.
diff --git a/usr.bin/kyua/Makefile b/usr.bin/kyua/Makefile
--- a/usr.bin/kyua/Makefile
+++ b/usr.bin/kyua/Makefile
@@ -130,7 +130,11 @@
engine/execenv/execenv.cpp \
engine/execenv/execenv_host.cpp \
engine/prepare/prepare.cpp \
- engine/prepare/prepare_all.cpp
+ engine/prepare/prepare_all.cpp \
+ engine/execplan/execplan.cpp \
+ engine/execplan/execplan_none.cpp \
+ engine/execplan/execplan_time.cpp \
+ engine/execplan/profile.cpp
SRCS+= os/freebsd/execenv_jail_manager.cpp \
os/freebsd/main.cpp \

File Metadata

Mime Type
text/plain
Expires
Thu, Oct 8, 1:36 PM (8 h, 8 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40423007
Default Alt Text
D59276.id185947.diff (39 KB)

Event Timeline