Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: configure
run: cmake . -DCMAKE_BUILD_TYPE=Release
- name: build
run: cmake --build . --target Luau.Repl.CLI Luau.Analyze.CLI Luau.Compile.CLI Luau.Ast.CLI --config Release -j 2
run: cmake --build . --target Luau.Repl.CLI Luau.Analyze.CLI Luau.Compile.CLI Luau.Ast.CLI Luau.Harness.CLI --config Release -j 2
- name: pack
if: matrix.os.name != 'windows'
run: zip slua-${{github.event.release.tag_name}}-${{matrix.os.name}}.zip slua*
Expand Down
21 changes: 21 additions & 0 deletions CLI/src/Harness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,16 @@ static void displayHelp(const char* argv0)
printf(" --quanta=<usecs>: time slice per run window (default 200)\n");
printf(" -O<n>: compile with optimization level n (default 1)\n");
printf(" --fflags=<list>: comma-separated fast flag settings (name=true/false),\n");
printf(" --sync-arming: install the quanta interrupt at window start instead of\n");
printf(" from the watchdog thread (the legacy resident-handler policy)\n");
}

int main(int argc, char** argv)
{
const char* script_path = nullptr;
double quanta_usec = 200.0;
int optimization_level = 1;
bool sync_arming = false;

for (int i = 1; i < argc; ++i)
{
Expand All @@ -130,6 +133,10 @@ int main(int argc, char** argv)
{
setLuauFlags(argv[i] + 9);
}
else if (strcmp(argv[i], "--sync-arming") == 0)
{
sync_arming = true;
}
else if (strncmp(argv[i], "-O", 2) == 0)
{
int level = atoi(argv[i] + 2);
Expand Down Expand Up @@ -199,6 +206,7 @@ int main(int argc, char** argv)
HostCallbacks callbacks;
callbacks.clockProvider = script_clock;
callbacks.populateEnvironment = populate_environment;
callbacks.synchronousQuantaArming = sync_arming;
// quantaClockProvider stays null so we exercise the engine's default

Provisioner<> provisioner(callbacks);
Expand Down Expand Up @@ -289,6 +297,19 @@ int main(int argc, char** argv)
double runtime = lua_clock() - start;
fprintf(stderr, "Runtime: %f, Accum. Sleep: %f, Time Slices: %zu\n", runtime, accum_sleep, slices);

WatchdogStats wd_stats = provisioner.getQuantaWatchdog()->getStats();
if (wd_stats.fires > 0)
{
fprintf(
stderr,
"Watchdog fires: %llu, lateness usecs min/avg/max: %.1f/%.1f/%.1f\n",
(unsigned long long)wd_stats.fires,
wd_stats.latenessMin * 1e6,
wd_stats.latenessSum / (double)wd_stats.fires * 1e6,
wd_stats.latenessMax * 1e6
);
}

if (result.status == HandlerRunStatus::Fault)
{
auto &fault_str = script->getExtendedFaultString().empty() ? script->getFaultString() : script->getExtendedFaultString();
Expand Down
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ if(CMAKE_SYSTEM_NAME MATCHES "Linux|Darwin|iOS")
target_link_libraries(osthreads INTERFACE "-lpthread")
endif ()

# ServerLua: the executor's quanta watchdog runs on a thread; PUBLIC so
# embedders of the static library inherit the pthread requirement
target_link_libraries(Luau.Executor PUBLIC osthreads)

if(LUAU_BUILD_CLI)
target_compile_options(Luau.Repl.CLI PRIVATE ${LUAU_OPTIONS})
target_compile_options(Luau.Reduce.CLI PRIVATE ${LUAU_OPTIONS})
Expand Down
101 changes: 101 additions & 0 deletions Executor/include/Luau/Executor.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,79 @@ void logWarn(const char* source, const char* fmt, ...) LUA_PRINTF_ATTR(2, 3);
// Figure out which clock source to use
lua_clockProvider resolveDefaultQuantaClock();

// Signature of the VM interrupt callback, as installed into lua_Callbacks
using InterruptCallback = void (*)(lua_State* L, int gc);

// Aggregate timing of a watchdog's deadline fires, for tuning its fire lead.
// Lateness (seconds) is how far past the intended fire instant the install
// actually happened: OS wakeup jitter in the common case, but it also
// absorbs the whole overshoot when a window is armed with less than a
// lead's worth of quanta, so a bimodal max isn't necessarily jitter.
struct WatchdogStats
{
uint64_t fires = 0;
double latenessSum = 0.0;
double latenessMin = 0.0;
double latenessMax = 0.0;
};

// Arming policy for a Script's run windows: owns the VM's interrupt handler
// and how it gets installed so that quanta expiry can be delivered. The
// installed pointer is only a request; the handler re-reads the real clock at
// the safepoint, so the clock stays the source of truth. One watchdog serves
// every VM of its provisioner (one per environment); arm() binds the open
// window's VM as the one slot everything else operates on.
class QuantaWatchdog
{
public:
// `handler` is whatever the script type's installVMCallbacks() put on the
// VM -- captured by the provisioner, so subclass handlers are honored.
explicit QuantaWatchdog(InterruptCallback handler)
: mHandler(handler)
{
}

virtual ~QuantaWatchdog();

// Open a run window: bind `target` as THE window (exactly one is open per
// provisioner, assert-enforced), reset its interrupt pointer to the
// implementation's baseline, and schedule `deadline`, a reading in the
// quanta clock's domain.
virtual void arm(lua_Callbacks* target, double deadline) = 0;

// Close out the window. Guarantees no future fire; does not touch the
// pointer.
virtual void disarm() = 0;

// Install the handler in the open window's VM right now, so the next
// safepoint runs it: what a deadline fire does, on demand. For conditions
// that must be noticed mid-window (sleep, force-yield).
virtual void fireNow() = 0;

// Self-heal for a spurious install in the open window: uninstall only if
// the window's fire is still pending, and report whether that happened.
// A bare store in the handler could instead wipe a concurrent fire and
// spend the window's one-shot.
virtual bool clearIfArmed() = 0;

// Lifetime totals of deadline fires. Zeros for policies where the
// deadline never fires as a distinct event (the synchronous one).
virtual WatchdogStats getStats() { return {}; }

protected:
InterruptCallback mHandler = nullptr;
};

// The synchronous policy: arm installs the handler on the spot, so it stays
// resident and checks the clock at every safepoint.
std::unique_ptr<QuantaWatchdog> createImmediateQuantaWatchdog(InterruptCallback handler);
// Go-sysmon-shaped policy for real-time clocks: windows run with no interrupt
// installed, and a long-running watchdog thread -- asleep whenever no window
// is armed -- stores the handler slightly ahead of the deadline so the
// preemption lands on it rather than after it. Thread-creation failure
// propagates as std::system_error.
std::unique_ptr<QuantaWatchdog> createQuantaWatchdog(InterruptCallback handler, lua_clockProvider quantaClock);

// Give the embedder a chance to plop their own things into the environment before it's
// fully set up. This is called before GC fixing / ares perms registration.
using PopulateEnvironmentCallback = void (*)(IEnvironment& environment, lua_State* L);
Expand All @@ -103,6 +176,11 @@ struct HostCallbacks
lua_setTimerEventCallback setTimerEventCb = nullptr;
lua_eventHandlerRegistrationCallback eventHandlerRegistrationCb = nullptr;
lua_clockProvider quantaClockProvider = nullptr;
// Install the interrupt handler synchronously at window start (resident
// at every safepoint) instead of from the watchdog thread at the
// deadline. Required when quantaClockProvider doesn't advance with real
// time, e.g. a test's stepped fake clock.
bool synchronousQuantaArming = false;
PopulateEnvironmentCallback populateEnvironment = nullptr;
};

Expand Down Expand Up @@ -340,6 +418,10 @@ class IProvisioner

virtual const HostCallbacks& getCallbacks() const = 0;

// The arming policy this provisioner's scripts use for their run windows.
// Never null once an environment exists (scripts can't exist before one).
virtual QuantaWatchdog* getQuantaWatchdog() const = 0;

virtual std::shared_ptr<IEnvironment> createEnvironment(bool is_lsl, uint32_t api_version) = 0;
virtual std::shared_ptr<IImage> buildImage(std::shared_ptr<IEnvironment> environment, const ImageConfig& config) = 0;
virtual std::shared_ptr<Script> instantiateScript(const std::shared_ptr<IImage>& image, const ScriptConfig& config) = 0;
Expand All @@ -365,10 +447,26 @@ class Provisioner : public IProvisioner

const HostCallbacks& getCallbacks() const override { return mCallbacks; }

QuantaWatchdog* getQuantaWatchdog() const override { return mWatchdog.get(); }

std::shared_ptr<IEnvironment> createEnvironment(bool is_lsl, uint32_t api_version) override
{
std::shared_ptr<Environment> environment = makeEnvironment(is_lsl, api_version);
environment->build<S>();

if (mWatchdog == nullptr)
{
// The first environment tells us which interrupt handler
// `S::installVMCallbacks` really installs (it may be a subclass's
// own, wrapping ours) -- every environment of this provisioner
// installs the same one.
InterruptCallback handler = lua_callbacks(environment->getBaseState())->interrupt;
if (mCallbacks.synchronousQuantaArming)
mWatchdog = createImmediateQuantaWatchdog(handler);
else
mWatchdog = createQuantaWatchdog(handler, mCallbacks.quantaClockProvider);
}

return environment;
}

Expand Down Expand Up @@ -433,6 +531,9 @@ class Provisioner : public IProvisioner

private:
HostCallbacks mCallbacks;
// Created with the first environment, once the installed interrupt
// handler is known. Destruction joins the threaded impl's thread.
std::unique_ptr<QuantaWatchdog> mWatchdog;
};

} // namespace Executor
Expand Down
35 changes: 23 additions & 12 deletions Executor/include/Luau/Script.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,25 @@ class Script : protected lua_SLRuntimeState
// state. Unlike loadDefaultState(), this is valid at any time.
bool reset();

// Start a run window for `quanta` seconds. _must_ be followed by symmetric `endRunWindow()`
// TODO: RAII helper thingy maybe?
// Start a run window for `quanta` seconds. Every window must be closed by
// exactly one endRunWindow() before the next begin, and must not begin
// with a pending sleep or force-yield (assert-enforced): sleep is a
// window output the host consumes (banks + zeroes) before scheduling the
// script again, and a host that wants a script stopped shouldn't be
// scheduling windows for it.
void beginRunWindow(double quanta);
// And finish it.
// Close the current run window. isYieldDue(), getExcludedTime() and
// getSleep() stay readable after the close (only the next begin resets
// them); a pending force-yield is consumed, its life ends with its window.
void endRunWindow();
// True once the engine has decided the current run window is over, sticky
// for the rest of the window.
bool isYieldDue() const { return mYieldDue; }

// Level-triggered forced preemption, port of the host's
// `getReset() || !mIsEnabled` check. The host both sets and clears it.
void setForceYield(bool force) { mForceYield = force; }
// Forced preemption, a mid-window mechanism: the current window yields at
// its first safepoint. Port of the host's `getReset() || !mIsEnabled`
// check; the host sets it, and endRunWindow() consumes it.
void setForceYield(bool force);

void *getHostContext() const { return mHostContext; }

Expand Down Expand Up @@ -157,7 +164,7 @@ class Script : protected lua_SLRuntimeState
// How long we've been told to sleep. Never decremented, only zeroed out when the scheduler
// decides we're done the sleep.
float getSleep() const { return mSleep; }
void setSleep(float sleep) { mSleep = sleep; }
void setSleep(float sleep);

// Wall time excluded from punishment accounting in the current window
double getExcludedTime() const { return mExcludedTime; }
Expand Down Expand Up @@ -219,15 +226,21 @@ class Script : protected lua_SLRuntimeState
// Monotonic stopwatch used for quanta-elapsed measurement, seeded from the
// provisioner's.
lua_clockProvider mQuantaClockProvider = nullptr;
// Arming policy for run windows, from the provisioner. Never null.
QuantaWatchdog* mWatchdog = nullptr;
// The VM's callback struct, one per environment and stable for its whole
// life -- which brackets ours, so this never dangles. Every instance we
// ever fork lives in that same VM.
lua_Callbacks* mCallbacks = nullptr;

// When did we start running
double mWindowStart = 0.0;
// How long are we supposed to run?
double mQuanta = 0.0;
// Wall time this window spent inside GC steps and reachability walks.
// Wall time this window spent inside reachability walks.
double mExcludedTime = 0.0;
// Quanta clock reading at the opening bracket of the GC step in flight.
double mGCStepStart = 0.0;
// GC threshold saved across a run window while the GC is parked.
size_t mSavedGCThreshold = 0;
// We've planned a script kill for this time, the script will be killed if
// it doesn't finish before the deadline.
double mMandatoryDeadline = 0.0;
Expand All @@ -237,8 +250,6 @@ class Script : protected lua_SLRuntimeState
bool mYieldDue = false;
// We already threw a catchable error trying to force a yield.
bool mMandatoryYieldRaised = false;
// Between the paired pre and post interrupts of one GC step.
bool mGCStepInFlight = false;
bool mInExecution = false;
bool mMainFunctionComplete = false;
// The host has indicated that a yield should happen at the next interrupt.
Expand Down
Loading
Loading