Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F175002268
D59276.id185947.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
39 KB
Referenced Files
None
Subscribers
None
D59276.id185947.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D59276: kyua: Add execution plan concept
Attached
Detach File
Event Timeline
Log In to Comment