Files
Xenia-Canary/src/xenia/base/threading_timer_queue.cc
chss95cs@gmail.com 7cc364dcb8 squash reallocs in command buffers by using large prealloced buffer, directly use virtual memory with it so os allocs on demand
mark raw clock functions as noinline, the way msvc was inlining them and ordering the branches meant that rdtsc would often be speculatively executed
add alternative clock impl for win, instead of using queryperformancecounter we grab systemtime from kusershared. it does not have the same precision as queryperformancecounter, we only have 100 nanosecond precision, but we round to milliseconds so it never made sense to use the performance counter in the first place
stubbed out the "guest clock mutex"... (the entirety of clock.cc needs a rewrite)
added some helpers for minf/maxf without the nan handling behavior
2022-08-14 13:42:08 -07:00

226 lines
8.3 KiB
C++

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include <algorithm>
#include <forward_list>
#include "third_party/disruptorplus/include/disruptorplus/blocking_wait_strategy.hpp"
#include "third_party/disruptorplus/include/disruptorplus/multi_threaded_claim_strategy.hpp"
#include "third_party/disruptorplus/include/disruptorplus/ring_buffer.hpp"
#include "third_party/disruptorplus/include/disruptorplus/sequence_barrier.hpp"
#include "third_party/disruptorplus/include/disruptorplus/spin_wait.hpp"
#include "third_party/disruptorplus/include/disruptorplus/spin_wait_strategy.hpp"
#include "xenia/base/assert.h"
#include "xenia/base/threading.h"
#include "xenia/base/threading_timer_queue.h"
namespace dp = disruptorplus;
namespace xe {
namespace threading {
using WaitItem = TimerQueueWaitItem;
/*
chrispy: changed this to a blocking wait from a spin-wait, the spin was
monopolizing a ton of cpu time (depending on the game 2-4% of total cpu time)
on my 3990x no complaints since that change
*/
/*
edit: actually had to change it back, when i was testing it only worked because i fixed disruptorplus' code to compile (it gives wrong args to condition_variable::wait_until) but now builds
*/
using WaitStrat = dp::blocking_wait_strategy;
class TimerQueue {
public:
using clock = WaitItem::clock;
static_assert(clock::is_steady);
public:
TimerQueue()
: buffer_(kWaitCount),
wait_strategy_(),
claim_strategy_(kWaitCount, wait_strategy_),
consumed_(wait_strategy_),
shutdown_(false) {
claim_strategy_.add_claim_barrier(consumed_);
dispatch_thread_ = std::thread(&TimerQueue::TimerThreadMain, this);
}
~TimerQueue() {
shutdown_.store(true, std::memory_order_release);
// Kick dispatch thread to check shutdown flag
auto wait_item = std::make_shared<WaitItem>(nullptr, nullptr, this,
clock::time_point::min(),
clock::duration::zero());
wait_item->Disarm();
QueueTimer(std::move(wait_item));
dispatch_thread_.join();
}
void TimerThreadMain() {
dp::sequence_t next_sequence = 0;
const auto comp = [](const std::shared_ptr<WaitItem>& left,
const std::shared_ptr<WaitItem>& right) {
return left->due_ < right->due_;
};
xe::threading::set_name("xe::threading::TimerQueue");
while (!shutdown_.load(std::memory_order_relaxed)) {
{
// Consume new wait items and add them to sorted wait queue
dp::sequence_t available = claim_strategy_.wait_until_published(
next_sequence, next_sequence - 1,
wait_queue_.empty() ? clock::time_point::max()
: wait_queue_.front()->due_);
// Check for timeout
if (available != next_sequence - 1) {
std::forward_list<std::shared_ptr<WaitItem>> wait_items;
do {
wait_items.push_front(std::move(buffer_[next_sequence]));
} while (next_sequence++ != available);
consumed_.publish(available);
wait_items.sort(comp);
wait_queue_.merge(wait_items, comp);
}
}
{
// Check wait queue, invoke callbacks and reschedule
std::forward_list<std::shared_ptr<WaitItem>> wait_items;
while (!wait_queue_.empty() &&
wait_queue_.front()->due_ <= clock::now()) {
auto wait_item = std::move(wait_queue_.front());
wait_queue_.pop_front();
// Ensure that it isn't disarmed
auto state = WaitItem::State::kIdle;
if (wait_item->state_.compare_exchange_strong(
state, WaitItem::State::kInCallback,
std::memory_order_acq_rel)) {
// Possibility to dispatch to a thread pool here
assert_not_null(wait_item->callback_);
wait_item->callback_(wait_item->userdata_);
if (wait_item->interval_ != clock::duration::zero() &&
wait_item->state_.load(std::memory_order_acquire) !=
WaitItem::State::kInCallbackSelfDisarmed) {
// Item is recurring and didn't self-disarm during callback:
wait_item->due_ += wait_item->interval_;
wait_item->state_.store(WaitItem::State::kIdle,
std::memory_order_release);
wait_items.push_front(std::move(wait_item));
} else {
wait_item->state_.store(WaitItem::State::kDisarmed,
std::memory_order_release);
}
} else {
// Specifically, kInCallback is illegal here
assert_true(WaitItem::State::kDisarmed == state);
}
}
wait_items.sort(comp);
wait_queue_.merge(wait_items, comp);
}
}
}
std::weak_ptr<WaitItem> QueueTimer(std::shared_ptr<WaitItem> wait_item) {
auto wait_item_weak = std::weak_ptr<WaitItem>(wait_item);
// Mitigate callback flooding
wait_item->due_ =
std::max(clock::now() - wait_item->interval_, wait_item->due_);
auto sequence = claim_strategy_.claim_one();
buffer_[sequence] = std::move(wait_item);
claim_strategy_.publish(sequence);
return wait_item_weak;
}
const std::thread& dispatch_thread() const { return dispatch_thread_; }
private:
// This ring buffer will be used to introduce timers queued by the public API
static constexpr size_t kWaitCount = 512;
dp::ring_buffer<std::shared_ptr<WaitItem>> buffer_;
WaitStrat wait_strategy_;
dp::multi_threaded_claim_strategy<WaitStrat> claim_strategy_;
dp::sequence_barrier<WaitStrat> consumed_;
// This is a _sorted_ (ascending due_) list of active timers managed by a
// dedicated thread
std::forward_list<std::shared_ptr<WaitItem>> wait_queue_;
std::atomic_bool shutdown_;
std::thread dispatch_thread_;
};
xe::threading::TimerQueue timer_queue_;
void TimerQueueWaitItem::Disarm() {
State state;
// Special case for calling from a callback itself
if (std::this_thread::get_id() == parent_queue_->dispatch_thread().get_id()) {
state = State::kInCallback;
if (state_.compare_exchange_strong(state, State::kInCallbackSelfDisarmed,
std::memory_order_acq_rel)) {
// If we are self disarming from the callback set this special state and
// exit
return;
}
// Normal case can handle the rest
}
dp::spin_wait spinner;
state = State::kIdle;
// Classes which hold WaitItems will often call Disarm() to cancel them during
// destruction. This may lead to race conditions when the dispatch thread
// executes a callback which accesses memory that is freed simultaneously due
// to this. Therefore, we need to guarantee that no callbacks will be running
// once Disarm() has returned.
while (!state_.compare_exchange_weak(state, State::kDisarmed,
std::memory_order_acq_rel)) {
if (state == State::kDisarmed) {
// Do not break for kInCallbackSelfDisarmed and keep spinning in order to
// meet guarantees
break;
}
state = State::kIdle;
spinner.spin_once();
}
}
//unused
std::weak_ptr<WaitItem> QueueTimerOnce(std::function<void(void*)> callback,
void* userdata,
WaitItem::clock::time_point due) {
return timer_queue_.QueueTimer(
std::make_shared<WaitItem>(std::move(callback), userdata, &timer_queue_,
due, WaitItem::clock::duration::zero()));
}
// only used by HighResolutionTimer
std::weak_ptr<WaitItem> QueueTimerRecurring(
std::function<void(void*)> callback, void* userdata,
WaitItem::clock::time_point due, WaitItem::clock::duration interval) {
return timer_queue_.QueueTimer(std::make_shared<WaitItem>(
std::move(callback), userdata, &timer_queue_, due, interval));
}
} // namespace threading
} // namespace xe