0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-06-02 18:18:56 +02:00

ircd::js: Add wait-free (for the user) timer.

This commit is contained in:
Jason Volk 2016-10-21 20:32:45 -07:00
parent ad1fef4247
commit f9848b0494
4 changed files with 250 additions and 58 deletions

View file

@ -48,41 +48,32 @@ enum class irq
struct context
:private custom_ptr<JSContext>
{
using steady_clock = std::chrono::steady_clock;
using time_point = steady_clock::time_point;
// Context options
struct opts
{
size_t stack_chunk_size = 8_KiB;
microseconds timer_limit = 10ms;
}
opts;
// Exception state
JSExceptionState *except; // Use save_exception()/restore_exception()
JSErrorReport report; // Note: ptrs may not be valid in here.
// Interruption state
struct alignas(8) state
{
uint32_t sem;
enum phase phase;
enum irq irq;
};
struct opts
{
size_t stack_chunk_size = 8_KiB;
microseconds timer_interval = 250000us;
microseconds timer_limit = 500000us;
};
struct opts opts;
// Interruption state
std::atomic<struct state> state; // Atomic state of execution
std::function<int (const irq &)> on_intr; // User interrupt hook (ret -1 to not interfere)
// Exception state
JSExceptionState *except; // Use save_exception()/restore_exception()
JSErrorReport report; // Note: ptrs may not be valid in here.
bool handle_interrupt(); // Called by runtime on interrupt
// Execution timer
microseconds limit; // The maximum duration for an execution
steady_clock::time_point started; // When the execution started
std::atomic<microseconds> interval; // The interval the timer will check the above
bool handle_interrupt(); // Called by runtime on interrupt
void timer_check();
void timer_worker();
std::thread timer;
void handle_timeout() noexcept; // Called by timer after requested time
struct timer timer;
// JSContext
operator JSContext *() const { return get(); }

View file

@ -56,6 +56,7 @@ inline JSVersion version(const char *const &v) { return JS_StringToVersion(v);
} // namespace ircd
#include "runtime.h"
#include "timer.h"
#include "context.h"
#include "compartment.h"
#include "error.h"

65
include/ircd/js/timer.h Normal file
View file

@ -0,0 +1,65 @@
/*
* Copyright (C) 2016 Charybdis Development Team
* Copyright (C) 2016 Jason Volk <jason@zemos.net>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice is present in all copies.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#define HAVE_IRCD_JS_TIMER_H
namespace ircd {
namespace js {
struct timer
{
using steady_clock = std::chrono::steady_clock;
using time_point = steady_clock::time_point;
using callback = std::function<void () noexcept>;
private:
struct state
{
uint32_t sem;
bool running;
};
std::mutex mutex;
std::condition_variable cond;
bool finished;
callback timeout;
time_point started;
std::atomic<microseconds> limit;
std::atomic<struct state> state;
void handle(std::unique_lock<std::mutex> &lock);
void worker();
std::thread thread;
public:
void set(const microseconds &limit); // Set the time limit (only when not started)
void set(const callback &timeout); // Set the callback (only when not started)
bool cancel(); // Cancel (stop) the timer
time_point start(); // Start the timer
timer(const callback &timeout);
~timer() noexcept;
};
} // namespace js
} // namespace timer

View file

@ -1195,56 +1195,37 @@ ircd::js::context::context(JSRuntime *const &runtime,
}
}
,opts{opts}
,except{nullptr}
,state
{{
0, // Semaphore value starting at 0.
phase::ACCEPT, // ACCEPT phase indicates nothing is running.
irq::JS, // irq::JS is otherwise here in case JS triggers an interrupt.
}}
,except{nullptr}
,limit{opts.timer_limit}
,started{time_point::min()}
,interval{opts.timer_interval}
,timer{std::bind(&context::timer_worker, this)}
,timer
{
std::bind(&context::handle_timeout, this)
}
{
assert(&our(runtime) == rt); // Trying to construct on thread without runtime thread_local
timer.set(opts.timer_limit);
}
ircd::js::context::~context()
noexcept
{
interval.store(microseconds(0), std::memory_order_release);
timer.join();
}
void
ircd::js::context::timer_worker()
{
while(interval.load(std::memory_order_consume) > microseconds(0))
{
std::this_thread::sleep_for(interval.load(std::memory_order_consume));
timer_check();
}
}
void
ircd::js::context::timer_check()
ircd::js::context::handle_timeout()
noexcept
{
// The interruption is an atomic transaction so our condition
// for a timer excess occurs in a synchronous closure.
interrupt(*this, [this]
interrupt(*this, []
{
// See if the execution has timing.
if(started == time_point::min() || limit == microseconds(0))
return irq::NONE;
// Check if timer exceeded limits.
const auto over_limit(steady_clock::now() - limit > started);
if(!over_limit)
return irq::NONE;
// Attempt to kill it.
// At this time there is no yield logic so if the timer calls
// the script is terminated.
return irq::TERMINATE;
});
}
@ -1303,6 +1284,8 @@ ircd::js::context::handle_interrupt()
void
ircd::js::leave(context &c)
{
c.timer.cancel();
// Load the state to keep the current sem value up to date.
// This thread is the only writer to that value.
auto state(c.state.load(std::memory_order_relaxed));
@ -1323,10 +1306,6 @@ ircd::js::leave(context &c)
void
ircd::js::enter(context &c)
{
// Set the start time which will be observable to the interruptor
// after a release sequence.
c.started = std::chrono::steady_clock::now();
// State was already acquired by the last leave();
auto state(c.state.load(std::memory_order_relaxed));
@ -1340,6 +1319,7 @@ ircd::js::enter(context &c)
// Commit to next execution.
c.state.store(state, std::memory_order_release);
c.timer.start();
}
bool
@ -1417,6 +1397,161 @@ ircd::js::save_exception(context &c,
c.report = report;
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/timer.h
//
ircd::js::timer::timer(const callback &timeout)
:finished{false}
,timeout{timeout}
,started{time_point::min()}
,limit{0us}
,state
{{
0, false
}}
,thread{std::bind(&timer::worker, this)}
{
}
ircd::js::timer::~timer()
noexcept
{
// This is all on the main IRCd thread, and the only point when it waits on the timer
// thread. That's okay as long as this is on IRCd shutdown etc.
std::unique_lock<decltype(mutex)> lock(mutex);
finished = true;
// Wait for timer thread to exit. Notify before the unlock() could double tap,
// but should always make for a reliable way to do this shutdown.
cond.notify_all();
lock.unlock();
thread.join();
}
ircd::js::timer::time_point
ircd::js::timer::start()
{
// The counter is incremented indicating a new timing request, invalidating
// anything the timer was previously doing.
auto state(this->state.load(std::memory_order_relaxed));
++state.sem;
state.running = true;
// Commit to starting a new timer operation, unconditionally overwrite previous.
started = steady_clock::now();
this->state.store(state, std::memory_order_release);
// The timing thread is notified here. It may have already started on our new
// request. However, this notifcation must not be delayed and the timing thread
// must wake up soon/now.
cond.notify_one();
return started;
}
bool
ircd::js::timer::cancel()
{
// Relaxed acquire of the state to get the sem value.
auto state(this->state.load(std::memory_order_relaxed));
// Expect the timer to be running and order the timer to idle with an invalidation.
struct state in { state.sem, true };
const struct state out { state.sem + 1, false };
// Commit to cancellation. On a successful state transition, wake up the thread.
static const auto order_fail(std::memory_order_relaxed);
static const auto order_success(std::memory_order_acq_rel);
if(this->state.compare_exchange_strong(in, out, order_success, order_fail))
{
cond.notify_one();
return true;
}
else return false;
}
void
ircd::js::timer::set(const callback &timeout)
{
this->timeout = timeout;
std::atomic_thread_fence(std::memory_order_release);
}
void
ircd::js::timer::set(const microseconds &limit)
{
this->limit.store(limit, std::memory_order_release);
}
void
ircd::js::timer::worker()
{
// This lock is only ever held by this thread except during a finish condition.
// Notifications to the condition are only broadcast by the main thread.
std::unique_lock<decltype(mutex)> lock(mutex);
while(likely(!finished))
handle(lock);
}
void
ircd::js::timer::handle(std::unique_lock<std::mutex> &lock)
{
struct state ours, theirs;
const auto idle_condition([this, &ours]
{
if(unlikely(finished))
return true;
// Acquire the request operation
ours = this->state.load(std::memory_order_acquire);
// Require a start time
if(started == time_point::min())
return false;
return ours.running;
});
const auto cancel_condition([this, &ours, &theirs]
{
if(unlikely(finished))
return true;
// Acquire the request state and compare it to our saved state to see
// if the counter has changed or if there is cancellation.
theirs = this->state.load(std::memory_order_consume);
return ours.sem != theirs.sem || !theirs.running;
});
// Wait for a running condition into `in`
cond.wait(lock, idle_condition);
if(unlikely(finished))
return;
// Wait for timeout or cancellation
const auto limit(this->limit.load(std::memory_order_consume));
if(cond.wait_until(lock, started + limit, cancel_condition))
{
// A cancel or increment of the counter into the next request has occurred.
// This thread will go back to sleep in the idle_condition unless or until
// the next start() is triggered.
return;
}
// A timeout has occurred. This is the last chance for a belated cancellation to be
// observed. If a transition from running to !running can take place on this counter
// value, the user has not invalidated this request and desires the timeout.
assert(ours.running);
const struct state out { ours.sem, false };
static const auto order_fail(std::memory_order_relaxed);
static const auto order_success(std::memory_order_acq_rel);
if(state.compare_exchange_strong(ours, out, order_success, order_fail))
if(likely(timeout))
timeout();
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/runtime.h