Add a task system.

This commit is contained in:
ArthurSonzogni
2025-07-02 16:53:48 +02:00
parent b78b97056b
commit 1edf358251
9 changed files with 375 additions and 8 deletions

View File

@@ -148,15 +148,16 @@ add_library(component
src/ftxui/component/terminal_input_parser.hpp
src/ftxui/component/util.cpp
src/ftxui/component/window.cpp
src/ftxui/core/task.cpp
src/ftxui/core/task.hpp
src/ftxui/core/task_queue.cpp
src/ftxui/core/task_queue.hpp
src/ftxui/core/task_runner.cpp
src/ftxui/core/task_runner.hpp
)
target_link_libraries(dom
PUBLIC screen
)
target_link_libraries(component
PUBLIC dom
)
target_link_libraries(dom PUBLIC screen)
target_link_libraries(component PUBLIC dom)
if (NOT EMSCRIPTEN)
find_package(Threads)

View File

@@ -19,13 +19,13 @@ add_executable(ftxui-tests
src/ftxui/component/menu_test.cpp
src/ftxui/component/modal_test.cpp
src/ftxui/component/radiobox_test.cpp
src/ftxui/util/ref_test.cpp
src/ftxui/component/receiver_test.cpp
src/ftxui/component/resizable_split_test.cpp
src/ftxui/component/screen_interactive_test.cpp
src/ftxui/component/slider_test.cpp
src/ftxui/component/terminal_input_parser_test.cpp
src/ftxui/component/toggle_test.cpp
src/ftxui/core/task_test.cpp
src/ftxui/dom/blink_test.cpp
src/ftxui/dom/bold_test.cpp
src/ftxui/dom/border_test.cpp
@@ -51,6 +51,7 @@ add_executable(ftxui-tests
src/ftxui/dom/vbox_test.cpp
src/ftxui/screen/color_test.cpp
src/ftxui/screen/string_test.cpp
src/ftxui/util/ref_test.cpp
)
target_link_libraries(ftxui-tests

20
src/ftxui/core/task.cpp Normal file
View File

@@ -0,0 +1,20 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include "task.hpp"
namespace ftxui::task {
bool PendingTask::operator<(const PendingTask& other) const {
if (!time && !other.time) {
return false;
}
if (!time) {
return true;
}
if (!other.time) {
return false;
}
return time.value() > other.time.value();
}
} // namespace ftxui::task

42
src/ftxui/core/task.hpp Normal file
View File

@@ -0,0 +1,42 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#ifndef TASK_HPP
#define TASK_HPP
#include <chrono>
#include <functional>
#include <optional>
namespace ftxui::task {
/// A task represents a unit of work.
using Task = std::function<void()>;
/// A PendingTask represents a task that is scheduled to be executed at a
/// specific time, or as soon as possible.
struct PendingTask {
// Immediate task:
PendingTask(Task task) : task(std::move(task)) {} // NOLINT
// Delayed task with a duration
PendingTask(Task task, std::chrono::steady_clock::duration duration)
: task(std::move(task)),
time(std::chrono::steady_clock::now() + duration) {}
/// The task to be executed.
Task task;
/// The time when the task should be executed. If the time is empty, the task
/// should be executed as soon as possible.
std::optional<std::chrono::steady_clock::time_point> time;
/// Compare two PendingTasks by their time.
/// If both tasks have no time, they are considered equal.
bool operator<(const PendingTask& other) const;
};
} // namespace ftxui::task
#endif // TASK_HPP_

View File

@@ -0,0 +1,54 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include "task_queue.hpp"
namespace ftxui::task {
auto TaskQueue::PostTask(PendingTask task) -> void {
if (!task.time) {
immediate_tasks_.push(task);
return;
}
if (task.time.value() < std::chrono::steady_clock::now()) {
immediate_tasks_.push(task);
return;
}
delayed_tasks_.push(task);
}
auto TaskQueue::Get() -> MaybeTask {
// Attempt to execute a task immediately.
if (!immediate_tasks_.empty()) {
auto task = immediate_tasks_.front();
immediate_tasks_.pop();
return task.task;
}
// Move all tasks that can be executed to the immediate queue.
auto now = std::chrono::steady_clock::now();
while (!delayed_tasks_.empty() && delayed_tasks_.top().time.value() <= now) {
immediate_tasks_.push(delayed_tasks_.top());
delayed_tasks_.pop();
}
// Attempt to execute a task immediately.
if (!immediate_tasks_.empty()) {
auto task = immediate_tasks_.front();
immediate_tasks_.pop();
return task.task;
}
// If there are no tasks to execute, return the delay until the next task.
if (!delayed_tasks_.empty()) {
return delayed_tasks_.top().time.value() - now;
}
// If there are no tasks to execute, return the maximum duration.
return std::monostate{};
}
} // namespace ftxui::task

View File

@@ -0,0 +1,36 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#ifndef TASK_QUEUE_HPP
#define TASK_QUEUE_HPP
#include <queue>
#include <variant>
#include "task.hpp"
namespace ftxui::task {
/// A task queue that schedules tasks to be executed in the future. Tasks can be
/// scheduled to be executed immediately, or after a certain duration.
/// - The tasks are executed in the order they were scheduled.
/// - If multiple tasks are scheduled to be executed at the same time, they are
/// executed in the order they were scheduled.
/// - If a task is scheduled to be executed in the past, it is executed
/// immediately.
struct TaskQueue {
auto PostTask(PendingTask task) -> void;
using MaybeTask =
std::variant<Task, std::chrono::steady_clock::duration, std::monostate>;
auto Get() -> MaybeTask;
private:
std::queue<PendingTask> immediate_tasks_;
std::priority_queue<PendingTask> delayed_tasks_;
};
} // namespace ftxui::task
#endif

View File

@@ -0,0 +1,76 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include "task_runner.hpp"
#include <cassert>
#include <thread>
namespace ftxui::task {
static thread_local TaskRunner* current_task_runner = nullptr; // NOLINT
TaskRunner::TaskRunner() {
assert(!previous_task_runner_);
previous_task_runner_ = current_task_runner;
current_task_runner = this;
}
TaskRunner::~TaskRunner() {
current_task_runner = previous_task_runner_;
}
// static
auto TaskRunner::Current() -> TaskRunner* {
assert(current_task_runner);
return current_task_runner;
}
auto TaskRunner::PostTask(Task task) -> void {
queue_.PostTask(PendingTask{std::move(task)});
}
auto TaskRunner::PostDelayedTask(Task task,
std::chrono::steady_clock::duration duration)
-> void {
queue_.PostTask(PendingTask{std::move(task), duration});
}
/// Runs the tasks in the queue.
auto TaskRunner::RunUntilIdle()
-> std::optional<std::chrono::steady_clock::duration> {
while (true) {
auto maybe_task = queue_.Get();
if (std::holds_alternative<std::monostate>(maybe_task)) {
// No more tasks to execute, exit the loop.
return std::nullopt;
}
if (std::holds_alternative<Task>(maybe_task)) {
std::get<Task>(maybe_task)();
continue;
}
if (std::holds_alternative<std::chrono::steady_clock::duration>(
maybe_task)) {
return std::get<std::chrono::steady_clock::duration>(maybe_task);
}
}
}
auto TaskRunner::Run() -> void {
while (true) {
auto duration = RunUntilIdle();
if (!duration) {
// No more tasks to execute, exit the loop.
return;
}
// Sleep for the duration until the next task can be executed.
std::this_thread::sleep_for(duration.value());
}
}
} // namespace ftxui::task

View File

@@ -0,0 +1,42 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#ifndef TASK_RUNNER_HPP
#define TASK_RUNNER_HPP
#include "task.hpp"
#include "task_queue.hpp"
namespace ftxui::task {
class TaskRunner {
public:
TaskRunner();
~TaskRunner();
// Returns the task runner for the current thread.
static auto Current() -> TaskRunner*;
/// Schedules a task to be executed immediately.
auto PostTask(Task task) -> void;
/// Schedules a task to be executed after a certain duration.
auto PostDelayedTask(Task task,
std::chrono::steady_clock::duration duration) -> void;
/// Runs the tasks in the queue, return the delay until the next delayed task
/// can be executed.
auto RunUntilIdle() -> std::optional<std::chrono::steady_clock::duration>;
// Runs the tasks in the queue, blocking until all tasks are executed.
auto Run() -> void;
private:
TaskRunner* previous_task_runner_ = nullptr;
TaskQueue queue_;
};
} // namespace ftxui::task
#endif // TASK_RUNNER_HPP

View File

@@ -0,0 +1,95 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include "task.hpp"
#include <gtest/gtest.h>
#include "task_runner.hpp"
namespace ftxui::task {
TEST(TaskTest, Basic) {
std::vector<int> values;
auto task_1 = [&values] { values.push_back(1); };
auto task_2 = [&values] { values.push_back(2); };
auto task_3 = [&values] { values.push_back(3); };
auto runner = TaskRunner();
runner.PostTask(task_1);
runner.PostTask(task_2);
runner.PostTask(task_3);
while (true) {
auto duration = runner.RunUntilIdle();
if (!duration) {
break;
}
std::this_thread::sleep_for(duration.value());
}
EXPECT_EQ(values, (std::vector<int>{1, 2, 3}));
}
TEST(TaskTest, PostedWithinTask) {
std::vector<int> values;
auto task_1 = [&values] {
values.push_back(1);
auto task_2 = [&values] { values.push_back(5); };
TaskRunner::Current()->PostTask(std::move(task_2));
values.push_back(2);
};
auto task_2 = [&values] {
values.push_back(3);
auto task_2 = [&values] { values.push_back(6); };
TaskRunner::Current()->PostTask(std::move(task_2));
values.push_back(4);
};
auto runner = TaskRunner();
runner.PostTask(task_1);
runner.PostTask(task_2);
while (true) {
auto duration = runner.RunUntilIdle();
if (!duration) {
break;
}
std::this_thread::sleep_for(duration.value());
}
EXPECT_EQ(values, (std::vector<int>{1, 2, 3, 4, 5, 6}));
}
TEST(TaskTest, RunDelayedTask) {
std::vector<int> values;
auto task_1 = [&values] { values.push_back(1); };
auto task_2 = [&values] { values.push_back(2); };
auto task_3 = [&values] { values.push_back(3); };
auto runner = TaskRunner();
runner.PostDelayedTask(task_3, std::chrono::milliseconds(300));
runner.PostDelayedTask(task_1, std::chrono::milliseconds(100));
runner.PostDelayedTask(task_2, std::chrono::milliseconds(200));
while (true) {
auto duration = runner.RunUntilIdle();
if (!duration) {
break;
}
std::this_thread::sleep_for(duration.value());
}
EXPECT_EQ(values, (std::vector<int>{1, 2, 3}));
}
} // namespace ftxui::task