diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index a1325101..ee935917 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -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* diff --git a/CLI/src/Harness.cpp b/CLI/src/Harness.cpp index 0f085e89..497d367f 100644 --- a/CLI/src/Harness.cpp +++ b/CLI/src/Harness.cpp @@ -102,6 +102,8 @@ static void displayHelp(const char* argv0) printf(" --quanta=: time slice per run window (default 200)\n"); printf(" -O: compile with optimization level n (default 1)\n"); printf(" --fflags=: 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) @@ -109,6 +111,7 @@ 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) { @@ -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); @@ -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); @@ -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(); diff --git a/CMakeLists.txt b/CMakeLists.txt index ce344864..cf7f313b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/Executor/include/Luau/Executor.h b/Executor/include/Luau/Executor.h index a4ecea6d..b24c9164 100644 --- a/Executor/include/Luau/Executor.h +++ b/Executor/include/Luau/Executor.h @@ -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 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 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); @@ -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; }; @@ -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 createEnvironment(bool is_lsl, uint32_t api_version) = 0; virtual std::shared_ptr buildImage(std::shared_ptr environment, const ImageConfig& config) = 0; virtual std::shared_ptr