Skip to content

Commit bd7955e

Browse files
authored
Add TestScheduler to support testing time-based coroutines without waiting for timeouts (nv-morpheus#453)
Adds a manually driven TestScheduler that can fast-forward through delayed coroutines. Required for nv-morpheus/Morpheus#1548 Authors: - Christopher Harris (https://github.com/cwharris) Approvers: - Michael Demoret (https://github.com/mdemoret-nv) URL: nv-morpheus#453
1 parent 9cf1ebc commit bd7955e

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

cpp/mrc/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ add_library(libmrc
118118
src/public/coroutines/io_scheduler.cpp
119119
src/public/coroutines/sync_wait.cpp
120120
src/public/coroutines/task_container.cpp
121+
src/public/coroutines/test_scheduler.cpp
121122
src/public/coroutines/thread_local_context.cpp
122123
src/public/coroutines/thread_pool.cpp
123124
src/public/cuda/device_guard.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
#include "mrc/coroutines/scheduler.hpp"
19+
#include "mrc/coroutines/task.hpp"
20+
21+
#include <chrono>
22+
#include <coroutine>
23+
#include <queue>
24+
#include <utility>
25+
#include <vector>
26+
27+
#pragma once
28+
29+
namespace mrc::coroutines {
30+
31+
class TestScheduler : public Scheduler
32+
{
33+
private:
34+
struct Operation
35+
{
36+
public:
37+
Operation(TestScheduler* self, std::chrono::time_point<std::chrono::steady_clock> time);
38+
39+
static constexpr bool await_ready()
40+
{
41+
return false;
42+
}
43+
44+
void await_suspend(std::coroutine_handle<> handle);
45+
46+
void await_resume() {}
47+
48+
private:
49+
TestScheduler* m_self;
50+
std::chrono::time_point<std::chrono::steady_clock> m_time;
51+
};
52+
53+
using item_t = std::pair<std::coroutine_handle<>, std::chrono::time_point<std::chrono::steady_clock>>;
54+
struct ItemCompare
55+
{
56+
bool operator()(item_t& lhs, item_t& rhs);
57+
};
58+
59+
std::priority_queue<item_t, std::vector<item_t>, ItemCompare> m_queue;
60+
std::chrono::time_point<std::chrono::steady_clock> m_time = std::chrono::steady_clock::now();
61+
62+
public:
63+
/**
64+
* @brief Enqueue's the coroutine handle to be resumed at the current logical time.
65+
*/
66+
void resume(std::coroutine_handle<> handle) noexcept override;
67+
68+
/**
69+
* Suspends the current function and enqueue's it to be resumed at the current logical time.
70+
*/
71+
mrc::coroutines::Task<> yield() override;
72+
73+
/**
74+
* Suspends the current function and enqueue's it to be resumed at the current logica time + the given duration.
75+
*/
76+
mrc::coroutines::Task<> yield_for(std::chrono::milliseconds time) override;
77+
78+
/**
79+
* Suspends the current function and enqueue's it to be resumed at the given logical time.
80+
*/
81+
mrc::coroutines::Task<> yield_until(std::chrono::time_point<std::chrono::steady_clock> time) override;
82+
83+
/**
84+
* Immediately resumes the next-in-queue coroutine handle.
85+
*
86+
* @return true if more coroutines exist in the queue after resuming, false otherwise.
87+
*/
88+
bool resume_next();
89+
90+
/**
91+
* Immediately resumes next-in-queue coroutines up to the current logical time + the given duration, in-order.
92+
*
93+
* @return true if more coroutines exist in the queue after resuming, false otherwise.
94+
*/
95+
bool resume_for(std::chrono::milliseconds time);
96+
97+
/**
98+
* Immediately resumes next-in-queue coroutines up to the given logical time.
99+
*
100+
* @return true if more coroutines exist in the queue after resuming, false otherwise.
101+
*/
102+
bool resume_until(std::chrono::time_point<std::chrono::steady_clock> time);
103+
};
104+
105+
} // namespace mrc::coroutines
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
#include "mrc/coroutines/test_scheduler.hpp"
19+
20+
#include <compare>
21+
22+
namespace mrc::coroutines {
23+
24+
TestScheduler::Operation::Operation(TestScheduler* self, std::chrono::time_point<std::chrono::steady_clock> time) :
25+
m_self(self),
26+
m_time(time)
27+
{}
28+
29+
bool TestScheduler::ItemCompare::operator()(item_t& lhs, item_t& rhs)
30+
{
31+
return lhs.second > rhs.second;
32+
}
33+
34+
void TestScheduler::Operation::await_suspend(std::coroutine_handle<> handle)
35+
{
36+
m_self->m_queue.emplace(std::move(handle), m_time);
37+
}
38+
39+
void TestScheduler::resume(std::coroutine_handle<> handle) noexcept
40+
{
41+
m_queue.emplace(std::move(handle), std::chrono::steady_clock::now());
42+
}
43+
44+
mrc::coroutines::Task<> TestScheduler::yield()
45+
{
46+
co_return co_await TestScheduler::Operation{this, m_time};
47+
}
48+
49+
mrc::coroutines::Task<> TestScheduler::yield_for(std::chrono::milliseconds time)
50+
{
51+
co_return co_await TestScheduler::Operation{this, m_time + time};
52+
}
53+
54+
mrc::coroutines::Task<> TestScheduler::yield_until(std::chrono::time_point<std::chrono::steady_clock> time)
55+
{
56+
co_return co_await TestScheduler::Operation{this, time};
57+
}
58+
59+
bool TestScheduler::resume_next()
60+
{
61+
if (m_queue.empty())
62+
{
63+
return false;
64+
}
65+
66+
auto handle = m_queue.top();
67+
68+
m_queue.pop();
69+
70+
m_time = handle.second;
71+
72+
handle.first.resume();
73+
74+
return true;
75+
}
76+
77+
bool TestScheduler::resume_for(std::chrono::milliseconds time)
78+
{
79+
return resume_until(m_time + time);
80+
}
81+
82+
bool TestScheduler::resume_until(std::chrono::time_point<std::chrono::steady_clock> time)
83+
{
84+
m_time = time;
85+
86+
while (not m_queue.empty())
87+
{
88+
if (m_queue.top().second <= m_time)
89+
{
90+
m_queue.top().first.resume();
91+
m_queue.pop();
92+
}
93+
else
94+
{
95+
return true;
96+
}
97+
}
98+
99+
return false;
100+
}
101+
102+
} // namespace mrc::coroutines

0 commit comments

Comments
 (0)