| #ifdef WITH_RIVE_SCRIPTING_WASM | |
| #include "rive/wasm/wasm_workers.hpp" | |
| #include "rive/async/job_pool.hpp" | |
| #include "rive/wasm/wasm_jobs.hpp" | |
| #include "rive/wasm/wasm_lane_policy.hpp" | |
| #ifndef __EMSCRIPTEN__ | |
| #include "wasm_export.h" | |
| #else | |
| #include <emscripten/em_js.h> | |
| #include <emscripten/emscripten.h> | |
| #endif | |
| #include <algorithm> | |
| #include <condition_variable> | |
| #include <cstdio> | |
| #include <mutex> | |
| #include <thread> | |
| using namespace rive; | |
| namespace | |
| { | |
| using Clock = std::chrono::steady_clock; | |
| constexpr uint32_t kStopGraceMs = lane_timing::kWorkerStopGraceMs; | |
| } // namespace | |
| // --- the pool ---------------------------------------------------------------- | |
| #if RIVE_POOL_THREADS() | |
| namespace | |
| { | |
| constexpr auto kSlice = std::chrono::milliseconds(lane_timing::kWorkerSliceMs); | |
| // A thread the pool no longer needs lingers this long, so a worker that keeps | |
| // taking long steps does not churn threads. | |
| constexpr auto kRetireAfter = std::chrono::seconds(1); | |
| } // namespace | |
| // A few threads stepping every VM's workers, each taking whichever is due. | |
| class rive::WasmWorkerPool | |
| { | |
| public: | |
| // Leaked, as its threads may outlive static destructors. | |
| static WasmWorkerPool& instance() | |
| { | |
| static WasmWorkerPool* pool = new WasmWorkerPool(); | |
| return *pool; | |
| } | |
| // Every worker's signal waits watch it, so any notify reaches the pool. | |
| WakeWord& wakeWord() { return m_wake; } | |
| std::mutex& mutex() { return m_mutex; } | |
| void add(rcp<WasmWorker> worker) | |
| { | |
| bool first; | |
| { | |
| std::lock_guard<std::mutex> lock(m_mutex); | |
| m_workers.push_back(std::move(worker)); | |
| first = !m_supervising; | |
| m_supervising = true; | |
| } | |
| if (first) | |
| { | |
| JobPool::makeThread([this]() { superviseMain(); }).detach(); | |
| } | |
| m_supervise.notify_one(); | |
| m_wake.wake(); | |
| } | |
| // Takes a worker no runner claimed or began, for its stop to end it at | |
| // once; false when a runner has it. | |
| bool takeUnstarted(WasmWorker* worker) | |
| { | |
| rcp<WasmWorker> taken; | |
| std::lock_guard<std::mutex> lock(m_mutex); | |
| if (worker->m_claimed || worker->m_begun || worker->over()) | |
| { | |
| return false; | |
| } | |
| worker->m_claimed = true; | |
| taken = eraseLocked(worker); | |
| return true; | |
| } | |
| void join(WasmWorker* worker, Clock::time_point deadline) | |
| { | |
| while (!worker->over()) | |
| { | |
| if (Clock::now() < deadline) | |
| { | |
| std::this_thread::sleep_for(std::chrono::milliseconds(1)); | |
| continue; | |
| } | |
| #ifdef __EMSCRIPTEN__ | |
| // The page's thread must not block, so a worker still running ends on | |
| // its own thread, if metered at its first fuel check past the grace. | |
| return; | |
| #else | |
| bool idle; | |
| rcp<WasmWorker> taken; | |
| { | |
| std::lock_guard<std::mutex> lock(m_mutex); | |
| // A runner may have ended and dropped it since the check. | |
| if (worker->over()) | |
| { | |
| return; | |
| } | |
| idle = !worker->m_claimed; | |
| if (idle) | |
| { | |
| worker->m_claimed = true; | |
| taken = eraseLocked(worker); | |
| } | |
| } | |
| // No pool thread got to its stop in time, so it ends here. | |
| if (idle) | |
| { | |
| worker->end(); | |
| return; | |
| } | |
| // A worker that never awaits again never sees the stop, and one | |
| // with no instance yet has no call to interrupt. | |
| while (!worker->over()) | |
| { | |
| worker->interruptCall(); | |
| std::this_thread::sleep_for(std::chrono::milliseconds(1)); | |
| } | |
| return; | |
| #endif | |
| } | |
| } | |
| void configure(uint32_t threads, uint32_t promotedPerVM) | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| if (threads == 0) | |
| { | |
| setDefaults(); | |
| } | |
| else | |
| { | |
| m_threads = threads; | |
| m_promotedPerVM = promotedPerVM; | |
| } | |
| m_resizing = true; | |
| m_wake.wake(); | |
| m_runnersLeft.wait(lock, [this]() { return m_runners.empty(); }); | |
| m_resizing = false; | |
| lock.unlock(); | |
| m_supervise.notify_one(); | |
| } | |
| private: | |
| enum class Slice | |
| { | |
| idle, | |
| timing, | |
| promoted, | |
| // Past the slice with every promotion taken, it keeps its thread. | |
| kept, | |
| }; | |
| struct Runner | |
| { | |
| WasmWorker* stepping = nullptr; | |
| Clock::time_point stepStart; | |
| Slice slice = Slice::idle; | |
| }; | |
| WasmWorkerPool() { setDefaults(); } | |
| void setDefaults() | |
| { | |
| #ifdef __EMSCRIPTEN__ | |
| // Browser threads come from a small pool all of librive shares. | |
| m_threads = 2; | |
| m_promotedPerVM = 2; | |
| #else | |
| uint32_t hardware = std::thread::hardware_concurrency(); | |
| m_threads = std::min<uint32_t>(hardware > 2 ? hardware - 1 : 1, 4); | |
| m_promotedPerVM = 4; | |
| #endif | |
| if (uint32_t threads = ScriptingLimits::get().workerThreads) | |
| { | |
| m_threads = threads; | |
| } | |
| } | |
| void runnerMain(Runner* runner) | |
| { | |
| #ifndef __EMSCRIPTEN__ | |
| ensureWasmThreadEnv(); | |
| #endif | |
| Clock::time_point idleSince = Clock::now(); | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| while (!m_resizing) | |
| { | |
| // Read before the scan, so a wake landing during it ends the | |
| // wait below at once. | |
| uint32_t seen = m_wake.value(); | |
| Clock::time_point now = Clock::now(); | |
| Clock::time_point next = Clock::time_point::max(); | |
| if (rcp<WasmWorker> worker = takeLocked(runner, now, next)) | |
| { | |
| runner->stepping = worker.get(); | |
| runner->stepStart = now; | |
| runner->slice = Slice::timing; | |
| bool timeIt = m_superviseIdle; | |
| lock.unlock(); | |
| if (timeIt) | |
| { | |
| m_supervise.notify_one(); | |
| } | |
| bool ended = worker->run() == WasmWorker::Step::done; | |
| lock.lock(); | |
| if (runner->slice == Slice::promoted && !worker->m_promoted) | |
| { | |
| worker->m_promoted = true; | |
| worker->log( | |
| "worker promoted to its own thread: a step " | |
| "ran past " + | |
| std::to_string(kSlice.count()) + " ms without awaiting"); | |
| } | |
| // A stop this step never saw arms its grace again where it | |
| // is delivered. | |
| if (!ended && worker->stopping() && !worker->m_stopDelivered) | |
| { | |
| worker->dropInterrupt(); | |
| } | |
| // What a long step held frees for one kept on its thread. | |
| bool freed = runner->slice != Slice::timing; | |
| runner->stepping = nullptr; | |
| runner->slice = Slice::idle; | |
| worker->m_claimed = false; | |
| rcp<WasmWorker> erased = ended ? eraseLocked(worker.get()) : nullptr; | |
| // Its last reference may go here, which must not happen | |
| // under the lock. | |
| lock.unlock(); | |
| if (freed) | |
| { | |
| m_supervise.notify_one(); | |
| } | |
| erased = nullptr; | |
| worker = nullptr; | |
| lock.lock(); | |
| idleSince = Clock::now(); | |
| continue; | |
| } | |
| if ((m_workers.empty() || servingLocked() > m_threads) && | |
| !pinsLocked(runner)) | |
| { | |
| Clock::time_point retire = idleSince + kRetireAfter; | |
| if (now >= retire) | |
| { | |
| break; | |
| } | |
| next = std::min(next, retire); | |
| } | |
| lock.unlock(); | |
| m_wake.wait(seen, next); | |
| lock.lock(); | |
| } | |
| m_runners.erase(std::find_if( | |
| m_runners.begin(), | |
| m_runners.end(), | |
| [runner](const auto& other) { return other.get() == runner; })); | |
| m_runnersLeft.notify_all(); | |
| m_supervise.notify_one(); | |
| } | |
| // Promotes long steps on time and keeps the pool at its size, whether or | |
| // not a runner is idle or a host advances. | |
| void superviseMain() | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| for (;;) | |
| { | |
| Clock::time_point next = Clock::time_point::max(); | |
| std::vector<Runner*> started = superviseLocked(Clock::now(), next); | |
| if (!started.empty()) | |
| { | |
| lock.unlock(); | |
| start(started); | |
| lock.lock(); | |
| continue; | |
| } | |
| if (next == Clock::time_point::max()) | |
| { | |
| m_superviseIdle = true; | |
| m_supervise.wait(lock); | |
| m_superviseIdle = false; | |
| } | |
| else | |
| { | |
| m_supervise.wait_until(lock, next); | |
| } | |
| } | |
| } | |
| // The next due worker this runner may step, taken in turn so none | |
| // starves; lowers next to the earliest timeout of the rest. | |
| rcp<WasmWorker> takeLocked( | |
| Runner* runner, | |
| Clock::time_point now, | |
| Clock::time_point& next) | |
| { | |
| size_t count = m_workers.size(); | |
| for (size_t i = 0; i < count; i++) | |
| { | |
| size_t at = (m_next + i) % count; | |
| WasmWorker* worker = m_workers[at].get(); | |
| if (worker->m_claimed || | |
| (worker->m_pin != nullptr && worker->m_pin != runner) || | |
| #ifdef __EMSCRIPTEN__ | |
| (worker->m_pin == nullptr && pinsLocked(runner) && | |
| mayOwnRunnerLocked(worker->m_module.get())) || | |
| #endif | |
| overShareLocked(worker->m_module.get()) || !worker->due(now, next)) | |
| { | |
| continue; | |
| } | |
| worker->m_claimed = true; | |
| #ifdef __EMSCRIPTEN__ | |
| worker->m_pin = runner; | |
| #endif | |
| m_next = at + 1; | |
| return m_workers[at]; | |
| } | |
| return nullptr; | |
| } | |
| // Promotes steps past their slice, then answers the runners it added to | |
| // keep the pool at its size, for the caller to start once unlocked. | |
| std::vector<Runner*> superviseLocked( | |
| Clock::time_point now, | |
| Clock::time_point& next) | |
| { | |
| if (m_resizing) | |
| { | |
| return {}; | |
| } | |
| for (auto& runner : m_runners) | |
| { | |
| if (runner->slice == Slice::timing) | |
| { | |
| Clock::time_point over = runner->stepStart + kSlice; | |
| if (now < over) | |
| { | |
| next = std::min(next, over); | |
| continue; | |
| } | |
| } | |
| else if (runner->slice != Slice::kept) | |
| { | |
| continue; | |
| } | |
| runner->slice = promotedLocked(runner->stepping->m_module.get()) < | |
| m_promotedPerVM | |
| ? Slice::promoted | |
| : Slice::kept; | |
| } | |
| std::vector<Runner*> started; | |
| for (uint32_t serving = servingLocked(); | |
| !m_workers.empty() && serving < m_threads; | |
| serving++) | |
| { | |
| m_runners.push_back(std::make_unique<Runner>()); | |
| started.push_back(m_runners.back().get()); | |
| } | |
| #ifdef __EMSCRIPTEN__ | |
| // A runner each for the workers yet to take one of their own. | |
| uint32_t waiting = (uint32_t)std::count_if( | |
| m_workers.begin(), | |
| m_workers.end(), | |
| [this](const rcp<WasmWorker>& worker) { | |
| return worker->m_pin == nullptr && | |
| mayOwnRunnerLocked(worker->m_module.get()); | |
| }); | |
| uint32_t idle = (uint32_t)std::count_if( | |
| m_runners.begin(), | |
| m_runners.end(), | |
| [this](const auto& runner) { return !pinsLocked(runner.get()); }); | |
| for (; idle < waiting; idle++) | |
| { | |
| m_runners.push_back(std::make_unique<Runner>()); | |
| started.push_back(m_runners.back().get()); | |
| } | |
| #endif | |
| return started; | |
| } | |
| #ifdef __EMSCRIPTEN__ | |
| // An instance never leaves the thread it began on, so a VM's workers each | |
| // get a runner of their own, up to its cap, and share only past it. | |
| bool mayOwnRunnerLocked(const WasmLaneModule* module) const | |
| { | |
| uint32_t owned = (uint32_t)std::count_if( | |
| m_runners.begin(), | |
| m_runners.end(), | |
| [this, module](const auto& runner) { | |
| return std::any_of( | |
| m_workers.begin(), | |
| m_workers.end(), | |
| [&](const rcp<WasmWorker>& worker) { | |
| return worker->m_pin == runner.get() && | |
| worker->m_module.get() == module; | |
| }); | |
| }); | |
| return owned < lane_timing::kWebWorkerThreadsPerVM; | |
| } | |
| #endif | |
| void start(const std::vector<Runner*>& runners) | |
| { | |
| for (Runner* runner : runners) | |
| { | |
| JobPool::makeThread([this, runner]() { runnerMain(runner); }).detach(); | |
| } | |
| } | |
| // A VM's workers share its lane module. | |
| uint32_t countLocked(Slice slice, const WasmLaneModule* module) const | |
| { | |
| return (uint32_t)std::count_if( | |
| m_runners.begin(), | |
| m_runners.end(), | |
| [slice, module](const auto& runner) { | |
| return runner->slice == slice && | |
| runner->stepping->m_module.get() == module; | |
| }); | |
| } | |
| uint32_t promotedLocked(const WasmLaneModule* module) const | |
| { | |
| return countLocked(Slice::promoted, module); | |
| } | |
| // The serving threads each VM with workers may keep with long steps, so | |
| // one VM's spinning workers never hold every VM's threads. | |
| uint32_t shareLocked() const | |
| { | |
| std::vector<const WasmLaneModule*> vms; | |
| for (const rcp<WasmWorker>& worker : m_workers) | |
| { | |
| if (std::find(vms.begin(), vms.end(), worker->m_module.get()) == | |
| vms.end()) | |
| { | |
| vms.push_back(worker->m_module.get()); | |
| } | |
| } | |
| return vms.empty() ? m_threads : m_threads / (uint32_t)vms.size(); | |
| } | |
| bool overShareLocked(const WasmLaneModule* module) const | |
| { | |
| uint32_t kept = countLocked(Slice::kept, module); | |
| return kept != 0 && kept >= shareLocked(); | |
| } | |
| // Kept runners past their VM's share leave their slot to the others. | |
| uint32_t servingLocked() const | |
| { | |
| uint32_t share = shareLocked(); | |
| uint32_t serving = 0; | |
| std::vector<const WasmLaneModule*> counted; | |
| for (const auto& runner : m_runners) | |
| { | |
| if (runner->slice == Slice::promoted) | |
| { | |
| continue; | |
| } | |
| if (runner->slice != Slice::kept) | |
| { | |
| serving++; | |
| continue; | |
| } | |
| const WasmLaneModule* module = runner->stepping->m_module.get(); | |
| if (std::find(counted.begin(), counted.end(), module) == counted.end()) | |
| { | |
| counted.push_back(module); | |
| serving += std::min(countLocked(Slice::kept, module), share); | |
| } | |
| } | |
| return serving; | |
| } | |
| // Answers the pool's reference, for the caller to drop unlocked. | |
| rcp<WasmWorker> eraseLocked(WasmWorker* worker) | |
| { | |
| auto found = std::find_if( | |
| m_workers.begin(), | |
| m_workers.end(), | |
| [worker](const rcp<WasmWorker>& held) { return held.get() == worker; }); | |
| if (found == m_workers.end()) | |
| { | |
| return nullptr; | |
| } | |
| rcp<WasmWorker> erased = std::move(*found); | |
| m_workers.erase(found); | |
| return erased; | |
| } | |
| bool pinsLocked(const Runner* runner) const | |
| { | |
| return std::any_of( | |
| m_workers.begin(), | |
| m_workers.end(), | |
| [runner](const rcp<WasmWorker>& worker) { | |
| return worker->m_pin == runner; | |
| }); | |
| } | |
| std::mutex m_mutex; | |
| WakeWord m_wake; | |
| std::vector<rcp<WasmWorker>> m_workers; | |
| std::vector<std::unique_ptr<Runner>> m_runners; | |
| std::condition_variable m_runnersLeft; | |
| std::condition_variable m_supervise; | |
| // While configure waits for every runner to leave, none starts. | |
| bool m_resizing = false; | |
| bool m_supervising = false; | |
| // Set while no step is timed, so only a step starting then wakes it. | |
| bool m_superviseIdle = false; | |
| uint32_t m_threads; | |
| uint32_t m_promotedPerVM; | |
| size_t m_next = 0; | |
| }; | |
| #endif | |
| namespace | |
| { | |
| // What a worker's signal waits watch: nothing waits without threads. | |
| WakeWord* watchWord() | |
| { | |
| #if RIVE_POOL_THREADS() | |
| return &WasmWorkerPool::instance().wakeWord(); | |
| #else | |
| return nullptr; | |
| #endif | |
| } | |
| } // namespace | |
| // --- workers ----------------------------------------------------------------- | |
| WasmWorker* WasmWorker::current() { return WasmLane::currentOf<WasmWorker>(); } | |
| WasmWorker::WasmWorker( | |
| rcp<WasmLaneModule> module, | |
| uint32_t entry, | |
| std::vector<uint8_t> input) | |
| : WasmLane(kKind, std::move(module), entry, std::move(input)) | |
| {} | |
| // The pool holds a threaded worker until it ends, so only one stepped at a | |
| // poll can end here. | |
| WasmWorker::~WasmWorker() | |
| { | |
| if (m_begun && !m_ended) | |
| { | |
| end(); | |
| } | |
| } | |
| void WasmWorker::stop() | |
| { | |
| m_stopping.store(true); | |
| #if RIVE_POOL_THREADS() | |
| if (WasmWorkerPool::instance().takeUnstarted(this)) | |
| { | |
| end(); | |
| return; | |
| } | |
| // A step already running never sees the stop; the one delivering it arms | |
| // its own grace. | |
| { | |
| std::lock_guard<std::mutex> lock(WasmWorkerPool::instance().mutex()); | |
| if (m_claimed) | |
| { | |
| interruptAfter(kStopGraceMs); | |
| } | |
| } | |
| WasmWorkerPool::instance().wakeWord().wake(); | |
| #else | |
| // Only the VM's thread steps it. | |
| if (!m_begun && !m_ended) | |
| { | |
| end(); | |
| } | |
| #endif | |
| } | |
| void WasmWorker::finished(bool ok, const uint8_t* result, uint32_t size) | |
| { | |
| // A crafted module may call again, while the VM's thread reads the | |
| // result. | |
| if (m_finished) | |
| { | |
| return; | |
| } | |
| m_finished = true; | |
| // On the web the page copied the result in already. | |
| if (ok && result != nullptr) | |
| { | |
| m_result.assign(result, result + size); | |
| } | |
| m_state.store(ok ? LaneState::done : LaneState::failed); | |
| } | |
| // --- waits ------------------------------------------------------------------- | |
| uint32_t WasmWorker::addWait( | |
| uint32_t signal, | |
| double after, | |
| double timeoutSeconds) | |
| { | |
| rcp<SharedSignal> found = SignalWait::find(signal, sharedOwner()); | |
| m_waits.emplace_back( | |
| m_nextWait++, | |
| sharedOwner(), | |
| signal, | |
| found.get(), | |
| after, | |
| timeoutSeconds); | |
| WakeWord* word = watchWord(); | |
| if (found != nullptr && word != nullptr) | |
| { | |
| found->addWatcher(word); | |
| } | |
| return m_waits.back().id; | |
| } | |
| // A signal already gone keeps no watchers anyone could notify. | |
| void WasmWorker::unwatch(const SignalWait& wait) | |
| { | |
| WakeWord* word = watchWord(); | |
| rcp<SharedSignal> found = SignalWait::find(wait.signal, wait.owner); | |
| if (found != nullptr && word != nullptr) | |
| { | |
| found->removeWatcher(word); | |
| } | |
| } | |
| void WasmWorker::dropWaits() | |
| { | |
| for (const SignalWait& wait : m_waits) | |
| { | |
| unwatch(wait); | |
| } | |
| m_waits.clear(); | |
| } | |
| // --- the lifecycle ----------------------------------------------------------- | |
| WasmWorker::Step WasmWorker::run() | |
| { | |
| Step result; | |
| if (!m_begun) | |
| { | |
| // A worker stopped before it began never runs. | |
| result = !m_stopping.load() && begin() ? Step::progressed : Step::done; | |
| } | |
| else | |
| { | |
| result = step(); | |
| } | |
| // The next step may run on another runner thread. | |
| forgetRecentRegions(); | |
| if (result == Step::done) | |
| { | |
| // Only the stepping thread can hold what its trap left locked. | |
| releaseTrappedRegion(); | |
| end(); | |
| } | |
| return result; | |
| } | |
| bool WasmWorker::due(Clock::time_point now, Clock::time_point& earliest) const | |
| { | |
| if (m_ended) | |
| { | |
| return false; | |
| } | |
| if (!m_begun || m_stopping.load()) | |
| { | |
| return true; | |
| } | |
| for (const SignalWait& wait : m_waits) | |
| { | |
| double value; | |
| if (wait.fired(now, value)) | |
| { | |
| return true; | |
| } | |
| earliest = std::min(earliest, wait.deadline); | |
| } | |
| return false; | |
| } | |
| bool WasmWorker::begin() | |
| { | |
| m_begun = true; | |
| WasmLane* outerLane = WasmLane::setCurrent(this); | |
| bool ok = startInstance() && collect(); | |
| if (ok && !m_finished) | |
| { | |
| m_state.store(LaneState::running); | |
| } | |
| WasmLane::setCurrent(outerLane); | |
| return ok && !m_finished; | |
| } | |
| WasmWorker::Step WasmWorker::step() | |
| { | |
| // A worker that awaits again after its stop was delivered would be woken | |
| // with it forever; it is done. | |
| if (m_finished || m_stopDelivered) | |
| { | |
| return Step::done; | |
| } | |
| std::vector<std::pair<uint32_t, double>> ready; | |
| auto now = Clock::now(); | |
| bool stopping = m_stopping.load(); | |
| for (auto it = m_waits.begin(); it != m_waits.end();) | |
| { | |
| double value = kStopping; | |
| if (!stopping && !it->fired(now, value)) | |
| { | |
| ++it; | |
| continue; | |
| } | |
| ready.push_back({it->id, value}); | |
| unwatch(*it); | |
| it = m_waits.erase(it); | |
| } | |
| if (ready.empty()) | |
| { | |
| // A stopped worker with nothing parked is stalled on a promise no | |
| // host wakes; it is done. | |
| return stopping ? Step::done : Step::idle; | |
| } | |
| if (stopping) | |
| { | |
| interruptAfter(kStopGraceMs); | |
| } | |
| WasmLane* outerLane = WasmLane::setCurrent(this); | |
| bool ok = true; | |
| for (const auto& [id, value] : ready) | |
| { | |
| m_stopDelivered = m_stopDelivered || value == kStopping; | |
| if (!wake(id, value)) | |
| { | |
| ok = false; | |
| break; | |
| } | |
| } | |
| ok = ok && collect(); | |
| WasmLane::setCurrent(outerLane); | |
| return ok && !m_finished ? Step::progressed : Step::done; | |
| } | |
| void WasmWorker::end() | |
| { | |
| dropWaits(); | |
| endInstance(); | |
| // Published after the instance ends, so a stop it prompts finds none. | |
| if (!m_finished && m_state.load() < LaneState::done) | |
| { | |
| m_state.store(m_stopping.load() ? LaneState::stopped : LaneState::failed); | |
| } | |
| m_ended.store(true); | |
| } | |
| // --- the WAMR backend -------------------------------------------------------- | |
| #ifndef __EMSCRIPTEN__ | |
| namespace | |
| { | |
| void workerFinished( | |
| wasm_exec_env_t, | |
| uint32_t ok, | |
| const uint8_t* result, | |
| uint32_t size) | |
| { | |
| if (WasmWorker* worker = WasmWorker::current()) | |
| { | |
| worker->finished(ok != 0, result, size); | |
| } | |
| } | |
| uint32_t workerWait( | |
| wasm_exec_env_t, | |
| uint32_t signal, | |
| double after, | |
| double timeoutSeconds) | |
| { | |
| WasmWorker* worker = WasmWorker::current(); | |
| return worker != nullptr ? worker->addWait(signal, after, timeoutSeconds) : 0; | |
| } | |
| uint32_t workerStopping(wasm_exec_env_t) | |
| { | |
| WasmWorker* worker = WasmWorker::current(); | |
| return worker != nullptr && worker->stopping() ? 1 : 0; | |
| } | |
| NativeSymbol kWorkerNatives[] = { | |
| {"finished", (void*)workerFinished, "(i*~)", nullptr}, | |
| {"wait", (void*)workerWait, "(iFF)i", nullptr}, | |
| {"stopping", (void*)workerStopping, "()i", nullptr}, | |
| }; | |
| constexpr uint32_t kWorkerStack = 512 * 1024; | |
| } // namespace | |
| void rive::registerWorkerNatives() | |
| { | |
| static std::once_flag once; | |
| std::call_once(once, []() { | |
| wasm_runtime_register_natives( | |
| "rive_worker_v1", | |
| kWorkerNatives, | |
| sizeof(kWorkerNatives) / sizeof(kWorkerNatives[0])); | |
| }); | |
| } | |
| bool WasmWorker::startInstance() | |
| { | |
| if (!instantiate(kWorkerStack)) | |
| { | |
| return false; | |
| } | |
| if (m_stopping.load()) | |
| { | |
| interruptAfter(kStopGraceMs); | |
| } | |
| m_collectFn = lookup(lane_layout::kFrameMinorExport, 0, 1, true); | |
| uint32_t argv[2] = {}; | |
| if (!runStart(argv)) | |
| { | |
| failTrap(); | |
| return false; | |
| } | |
| uint32_t at = argv[0]; | |
| if (!call(m_module->entryNames[m_entry].c_str(), 2, argv)) | |
| { | |
| failTrap(); | |
| return false; | |
| } | |
| argv[0] = at; | |
| if (!call(lane_layout::kLaneFreeExport, 1, argv)) | |
| { | |
| failTrap(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool WasmWorker::wake(uint32_t id, double value) | |
| { | |
| if (m_wakeFn == nullptr && | |
| (m_wakeFn = lookup(lane_layout::kWorkerWakeExport, 3, 3)) == nullptr) | |
| { | |
| return false; | |
| } | |
| wasm_val_t args[2]; | |
| args[0].kind = WASM_I32; | |
| args[0].of.i32 = (int32_t)id; | |
| args[1].kind = WASM_F64; | |
| args[1].of.f64 = value; | |
| if (!wasm_runtime_call_wasm_a( | |
| (wasm_exec_env_t)m_execEnv, | |
| (wasm_function_inst_t)m_wakeFn, | |
| 0, | |
| nullptr, | |
| 2, | |
| args)) | |
| { | |
| failTrap(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| // Back at the host no frame is live, so the frame collector may run. | |
| bool WasmWorker::collect() | |
| { | |
| uint32_t argv[1] = {0}; | |
| if (m_collectFn != nullptr && !call(m_collectFn, 0, argv)) | |
| { | |
| failTrap(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| void WasmWorker::endInstance() | |
| { | |
| m_wakeFn = nullptr; | |
| m_collectFn = nullptr; | |
| teardown(); | |
| } | |
| #else | |
| // --- the browser backend ----------------------------------------------------- | |
| // The page runner calls these back from a worker's natives, with the worker | |
| // it bound them to. | |
| extern "C" EMSCRIPTEN_KEEPALIVE uint32_t rive_web_worker_wait( | |
| uint32_t worker, | |
| uint32_t signal, | |
| double after, | |
| double timeout) | |
| { | |
| return ((WasmWorker*)(uintptr_t)worker)->addWait(signal, after, timeout); | |
| } | |
| extern "C" EMSCRIPTEN_KEEPALIVE void rive_web_worker_finished( | |
| uint32_t worker, | |
| uint32_t ok) | |
| { | |
| ((WasmWorker*)(uintptr_t)worker)->finished(ok != 0, nullptr, 0); | |
| } | |
| extern "C" EMSCRIPTEN_KEEPALIVE uint32_t | |
| rive_web_worker_stopping(uint32_t worker) | |
| { | |
| return ((WasmWorker*)(uintptr_t)worker)->stopping() ? 1 : 0; | |
| } | |
| // clang-format off | |
| EM_JS_DEPS(riveWebWorkers, "$stringToNewUTF8"); | |
| // These answer null, or a malloc'd error. | |
| EM_JS(char*, riveWebWorkerStart, | |
| (uint32_t worker, uint32_t lane, uint32_t moduleId, | |
| const uint8_t* module, uint32_t moduleSize, uint32_t entry, | |
| const uint8_t* input, uint32_t inputSize, void* output, | |
| uint32_t* idPtr), { | |
| let finished = false; | |
| const natives = { | |
| wait: (signal, after, timeout) => | |
| _rive_web_worker_wait(worker, signal, after, timeout), | |
| finished: (ok, result) => { | |
| // The VM's thread may be reading the first result. | |
| if (finished) { | |
| return; | |
| } | |
| finished = true; | |
| if (ok) { | |
| // Resizing may grow the heap, which replaces HEAPU8. | |
| const at = _rive_web_job_output(output, result.length); | |
| HEAPU8.set(result, at); | |
| } | |
| _rive_web_worker_finished(worker, ok); | |
| }, | |
| stopping: () => _rive_web_worker_stopping(worker), | |
| }; | |
| const result = Module['riveScripting']['workerStart']( | |
| lane, | |
| moduleId, | |
| () => HEAPU8.slice(module, module + moduleSize), | |
| entry, | |
| HEAPU8.slice(input, input + inputSize), | |
| natives); | |
| if (!result.ok) { | |
| return stringToNewUTF8(result.error); | |
| } | |
| HEAPU32[idPtr >> 2] = result.id; | |
| return 0; | |
| }); | |
| EM_JS(char*, riveWebWorkerWake, (uint32_t id, uint32_t waitId, double value), { | |
| const error = Module['riveScripting']['workerWake'](id, waitId, value); | |
| return error === null ? 0 : stringToNewUTF8(error); | |
| }); | |
| EM_JS(char*, riveWebWorkerCollect, (uint32_t id), { | |
| const error = Module['riveScripting']['workerCollect'](id); | |
| return error === null ? 0 : stringToNewUTF8(error); | |
| }); | |
| EM_JS(void, riveWebWorkerEnd, (uint32_t id), { | |
| Module['riveScripting']['workerEnd'](id); | |
| }); | |
| // clang-format on | |
| bool WasmWorker::startInstance() | |
| { | |
| if (!riveWebAttach()) | |
| { | |
| fail("no script runner on this thread"); | |
| return false; | |
| } | |
| if (m_stopping.load()) | |
| { | |
| interruptAfter(kStopGraceMs); | |
| } | |
| std::vector<uint8_t> input = takeInput(); | |
| char* error = riveWebWorkerStart( | |
| (uint32_t)(uintptr_t)this, | |
| (uint32_t)(uintptr_t)(WasmLane*)this, | |
| m_module->id, | |
| m_module->bytes.data(), | |
| (uint32_t)m_module->bytes.size(), | |
| m_entry, | |
| input.data(), | |
| (uint32_t)input.size(), | |
| &m_result, | |
| &m_webInstance); | |
| if (error != nullptr) | |
| { | |
| failTaken(error); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool WasmWorker::wake(uint32_t id, double value) | |
| { | |
| char* error = riveWebWorkerWake(m_webInstance, id, value); | |
| if (error != nullptr) | |
| { | |
| failTaken(error); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool WasmWorker::collect() | |
| { | |
| char* error = riveWebWorkerCollect(m_webInstance); | |
| if (error != nullptr) | |
| { | |
| failTaken(error); | |
| return false; | |
| } | |
| return true; | |
| } | |
| void WasmWorker::endInstance() | |
| { | |
| if (m_webInstance != 0) | |
| { | |
| riveWebWorkerEnd(m_webInstance); | |
| m_webInstance = 0; | |
| } | |
| } | |
| #endif | |
| // --- the set ----------------------------------------------------------------- | |
| void WasmWorkers::configurePool(uint32_t threads, uint32_t promotedPerVM) | |
| { | |
| #if RIVE_POOL_THREADS() | |
| WasmWorkerPool::instance().configure(threads, promotedPerVM); | |
| #else | |
| (void)threads; | |
| (void)promotedPerVM; | |
| #endif | |
| } | |
| WasmWorkers::~WasmWorkers() | |
| { | |
| for (auto& worker : m_lanes) | |
| { | |
| worker->stop(); | |
| } | |
| #if RIVE_POOL_THREADS() | |
| auto deadline = Clock::now() + std::chrono::milliseconds(kStopGraceMs); | |
| for (auto& worker : m_lanes) | |
| { | |
| WasmWorkerPool::instance().join(worker.get(), deadline); | |
| } | |
| #endif | |
| } | |
| rcp<WasmWorker> WasmWorkers::spawn( | |
| uint32_t entry, | |
| std::vector<uint8_t> input, | |
| uint32_t pageThreadBudgetMs) | |
| { | |
| if (entry >= entryCount()) | |
| { | |
| return nullptr; | |
| } | |
| auto worker = add(make_rcp<WasmWorker>(m_module, entry, std::move(input))); | |
| worker->setPageThreadBudgetMs(pageThreadBudgetMs); | |
| #if RIVE_POOL_THREADS() | |
| WasmWorkerPool::instance().add(worker); | |
| #endif | |
| return worker; | |
| } | |
| void WasmWorkers::pump() | |
| { | |
| #if !RIVE_POOL_THREADS() | |
| // A worker parked on something already true is always due, so the bound | |
| // is time. | |
| auto until = Clock::now() + | |
| std::chrono::milliseconds(lane_timing::kCooperativeSliceMs); | |
| bool progressed = true; | |
| while (progressed && Clock::now() < until) | |
| { | |
| progressed = false; | |
| for (auto& worker : m_lanes) | |
| { | |
| if (worker->over()) | |
| { | |
| continue; | |
| } | |
| PageThreadBudget budget(*worker); | |
| if (worker->run() == WasmWorker::Step::progressed) | |
| { | |
| progressed = true; | |
| } | |
| } | |
| } | |
| #endif | |
| } | |
| bool WasmWorkers::due() const | |
| { | |
| Clock::time_point now = Clock::now(); | |
| Clock::time_point earliest = Clock::time_point::max(); | |
| #if RIVE_POOL_THREADS() | |
| std::lock_guard<std::mutex> lock(WasmWorkerPool::instance().mutex()); | |
| #endif | |
| bool due = std::any_of( | |
| m_lanes.begin(), | |
| m_lanes.end(), | |
| [&](const rcp<WasmWorker>& worker) { | |
| #if RIVE_POOL_THREADS() | |
| if (worker->m_claimed) | |
| { | |
| return false; | |
| } | |
| #endif | |
| return worker->due(now, earliest); | |
| }); | |
| #if !RIVE_POOL_THREADS() | |
| // Only polls step a worker here, so a timed wait keeps them coming. | |
| due = due || earliest != Clock::time_point::max(); | |
| #endif | |
| return due; | |
| } | |
| bool WasmWorkers::idle() const | |
| { | |
| Clock::time_point now = Clock::now(); | |
| Clock::time_point earliest = Clock::time_point::max(); | |
| #if RIVE_POOL_THREADS() | |
| std::lock_guard<std::mutex> lock(WasmWorkerPool::instance().mutex()); | |
| #endif | |
| return std::none_of( | |
| m_lanes.begin(), | |
| m_lanes.end(), | |
| [&](const rcp<WasmWorker>& worker) { | |
| #if RIVE_POOL_THREADS() | |
| // A stop that took an unstarted worker leaves it | |
| // claimed. | |
| if (worker->m_claimed && !worker->over()) | |
| { | |
| return true; | |
| } | |
| #endif | |
| return worker->due(now, earliest); | |
| }); | |
| } | |
| void WasmWorkers::parkedLines(std::vector<std::string>& lines) const | |
| { | |
| Clock::time_point now = Clock::now(); | |
| std::vector<std::pair<const WasmWorker*, std::vector<SignalWait>>> parked; | |
| { | |
| #if RIVE_POOL_THREADS() | |
| std::lock_guard<std::mutex> lock(WasmWorkerPool::instance().mutex()); | |
| #endif | |
| Clock::time_point earliest = Clock::time_point::max(); | |
| for (const auto& worker : m_lanes) | |
| { | |
| #if RIVE_POOL_THREADS() | |
| if (worker->m_claimed) | |
| { | |
| continue; | |
| } | |
| #endif | |
| if (!worker->over() && !worker->due(now, earliest)) | |
| { | |
| parked.emplace_back(worker.get(), worker->m_waits); | |
| } | |
| } | |
| } | |
| for (const auto& [worker, waits] : parked) | |
| { | |
| std::string name = worker->name(); | |
| if (waits.empty()) | |
| { | |
| lines.push_back( | |
| name + " is still waiting on a promise nothing will settle"); | |
| } | |
| for (const SignalWait& wait : waits) | |
| { | |
| std::string what = "sleeping"; | |
| if (wait.signal != 0) | |
| { | |
| bool space = false; | |
| uint32_t ring = SharedRegistry::ringOf(wait.signal, wait.owner, space); | |
| if (ring == 0) | |
| { | |
| what = "waiting for signal " + std::to_string(wait.signal); | |
| } | |
| else | |
| { | |
| what = std::string("waiting for a ") + | |
| (space ? "read from" : "write to") + " ring " + | |
| std::to_string(ring); | |
| } | |
| } | |
| char seconds[32]; | |
| snprintf( | |
| seconds, | |
| sizeof(seconds), | |
| "%.2f", | |
| std::chrono::duration<double>(now - wait.since).count()); | |
| lines.push_back(name + " is still " + what + " after " + seconds + " s"); | |
| } | |
| } | |
| } | |
| #endif |