blob: 5318d0f0aec0cad0b424a832ee0da186f41dc9fc [file]
#ifdef WITH_RIVE_SCRIPTING_WASM
#include "rive/wasm/module_tier_ladder.hpp"
#ifndef _WIN32
#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <filesystem>
#include <fcntl.h>
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <spawn.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
extern char** environ;
namespace rive
{
ModuleTierLadder& ModuleTierLadder::instance()
{
static ModuleTierLadder* ladder = new ModuleTierLadder();
return *ladder;
}
ModuleTierLadder::~ModuleTierLadder()
{
{
std::unique_lock<std::mutex> lock(m_mutex);
m_terminating = true;
for (Job* job : m_running)
{
job->cancelled = true;
if (job->pid > 0)
{
kill(job->pid, SIGKILL);
}
}
}
m_workAvailable.notify_all();
for (auto& worker : m_workers)
{
worker.join();
}
}
void ModuleTierLadder::configure(const std::string& wamrcPath,
const std::string& cacheDir)
{
std::unique_lock<std::mutex> lock(m_mutex);
m_wamrcPath = wamrcPath;
if (m_wamrcPath.empty())
{
if (const char* env = getenv("RIVE_WAMRC"))
{
m_wamrcPath = env;
}
}
m_cacheDir = cacheDir;
}
bool ModuleTierLadder::enabled()
{
std::unique_lock<std::mutex> lock(m_mutex);
if (m_wamrcPath.empty() && getenv("RIVE_WAMRC") != nullptr)
{
m_wamrcPath = getenv("RIVE_WAMRC");
}
// Self-configure from env so the first module load, which happens
// before any host configure call, can stage its pristine bytes.
if (m_cacheDir.empty() && !m_wamrcPath.empty())
{
const char* dirEnv = getenv("RIVE_AOT_CACHE_DIR");
m_cacheDir =
dirEnv != nullptr
? dirEnv
: (std::filesystem::temp_directory_path() / "rive_aot_cache")
.string();
}
return !m_wamrcPath.empty() && !m_cacheDir.empty();
}
void ModuleTierLadder::onArrival(ArrivalCallback callback)
{
std::unique_lock<std::mutex> lock(m_mutex);
m_arrival = std::move(callback);
}
const std::string& ModuleTierLadder::wamrcVersion()
{
// Callers hold m_mutex. The version keys the cache so a compiler swap
// reads as misses, never as stale native code.
if (!m_versionProbed)
{
m_versionProbed = true;
m_wamrcVersion = "unknown";
std::string cmd = m_wamrcPath + " --version 2>/dev/null";
if (FILE* pipe = popen(cmd.c_str(), "r"))
{
char line[128] = {0};
if (fgets(line, sizeof(line), pipe) != nullptr)
{
std::string v(line);
while (!v.empty() && (v.back() == '\n' || v.back() == ' '))
{
v.pop_back();
}
for (char& c : v)
{
if (c == ' ' || c == '/')
{
c = '-';
}
}
if (!v.empty())
{
m_wamrcVersion = v;
}
}
pclose(pipe);
}
}
return m_wamrcVersion;
}
std::string ModuleTierLadder::keyedCacheDir()
{
// The revision folds our wamrc flag choices into the cache key; bump
// it whenever species flags change or stale artifacts (like the
// pre-codegen-cap -O1 ones) get served on a cache hit.
std::string dir = m_cacheDir + "/" + wamrcVersion() + "-r3";
mkdir(m_cacheDir.c_str(), 0755);
mkdir(dir.c_str(), 0755);
return dir;
}
std::string ModuleTierLadder::artifactName(uint64_t moduleKey,
TierSpecies species)
{
const char* pattern = "%016llx.aot";
if (species == TierSpecies::o0)
{
pattern = "%016llx.o0.aot";
}
else if (species == TierSpecies::hw)
{
pattern = "%016llx.hw.aot";
}
char name[64];
snprintf(name, sizeof(name), pattern, (unsigned long long)moduleKey);
return name;
}
std::string ModuleTierLadder::artifactPath(uint64_t moduleKey,
TierSpecies species)
{
std::unique_lock<std::mutex> lock(m_mutex);
if (m_wamrcPath.empty() || m_cacheDir.empty())
{
return std::string();
}
std::string path = keyedCacheDir() + "/" + artifactName(moduleKey, species);
struct stat st;
if (stat(path.c_str(), &st) == 0)
{
return path;
}
return std::string();
}
// The cache is shared across processes and a name must never be visible
// half-written: write a process-unique temp, then atomically rename.
static bool writeFileAtomic(const std::string& path, Span<const uint8_t> bytes)
{
std::string tmpPath = path + "." + std::to_string(getpid()) + ".tmp";
FILE* f = fopen(tmpPath.c_str(), "wb");
if (f == nullptr)
{
return false;
}
size_t written = fwrite(bytes.data(), 1, bytes.size(), f);
if (fclose(f) != 0 || written != bytes.size())
{
unlink(tmpPath.c_str());
return false;
}
if (rename(tmpPath.c_str(), path.c_str()) != 0)
{
unlink(tmpPath.c_str());
return false;
}
return true;
}
void ModuleTierLadder::stagePristine(uint64_t moduleKey,
Span<const uint8_t> moduleBytes)
{
if (!enabled())
{
return;
}
std::unique_lock<std::mutex> lock(m_mutex);
char wasmName[64];
snprintf(wasmName,
sizeof(wasmName),
"%016llx.wasm",
(unsigned long long)moduleKey);
std::string wasmPath = keyedCacheDir() + "/" + wasmName;
struct stat st;
if (stat(wasmPath.c_str(), &st) == 0)
{
return;
}
writeFileAtomic(wasmPath, moduleBytes);
}
void ModuleTierLadder::schedule(const std::string& laneId,
uint64_t moduleKey,
Span<const uint8_t> moduleBytes)
{
if (!enabled())
{
return;
}
std::unique_lock<std::mutex> lock(m_mutex);
uint64_t generation = m_nextGeneration++;
m_laneGeneration[laneId] = generation;
// Supersede the lane: queued stale jobs get skipped by the generation
// check; running ones die now so the pool frees up for the new hash.
for (Job* job : m_running)
{
if (job->laneId == laneId && job->generation < generation &&
job->pid > 0)
{
job->cancelled = true;
kill(job->pid, SIGKILL);
}
}
std::string dir = keyedCacheDir();
char wasmName[64];
snprintf(wasmName,
sizeof(wasmName),
"%016llx.wasm",
(unsigned long long)moduleKey);
std::string wasmPath = dir + "/" + wasmName;
// On guard-page builds the top rung is the hw species: no inline
// bounds checks and growth never moves the memory base.
#ifdef RIVE_WASM_HW_BOUNDS
constexpr TierSpecies kTopSpecies = TierSpecies::hw;
#else
constexpr TierSpecies kTopSpecies = TierSpecies::o3;
#endif
std::vector<TierSpecies> wanted;
if (moduleBytes.size() <= kStraightToO3Bytes)
{
wanted = {kTopSpecies};
}
else
{
wanted = {TierSpecies::o0, kTopSpecies};
}
bool queued = false;
for (TierSpecies species : wanted)
{
std::string artifact = dir + "/" + artifactName(moduleKey, species);
struct stat st;
if (stat(artifact.c_str(), &st) == 0)
{
if (m_arrival)
{
Artifact ready{moduleKey, species, artifact};
lock.unlock();
m_arrival(ready);
lock.lock();
}
continue;
}
if (!queued)
{
// A pristine stage from before the in-place load wins; the bytes
// passed here may already be loader-rewritten.
struct stat wasmStat;
if (stat(wasmPath.c_str(), &wasmStat) != 0 &&
!writeFileAtomic(wasmPath, moduleBytes))
{
return;
}
queued = true;
}
m_queue.push_back(
Job{laneId, moduleKey, species, wasmPath, generation});
}
if (queued)
{
ensureWorkers();
m_workAvailable.notify_all();
}
}
void ModuleTierLadder::ensureWorkers()
{
if (!m_workers.empty())
{
return;
}
for (unsigned i = 0; i < kWorkerCount; i++)
{
m_workers.emplace_back([this] { workerLoop(); });
}
}
void ModuleTierLadder::workerLoop()
{
std::unique_lock<std::mutex> lock(m_mutex);
for (;;)
{
m_workAvailable.wait(lock, [this] {
return m_terminating || !m_queue.empty();
});
if (m_terminating)
{
return;
}
Job job = m_queue.front();
m_queue.pop_front();
auto lane = m_laneGeneration.find(job.laneId);
if (lane != m_laneGeneration.end() && job.generation < lane->second)
{
continue;
}
m_running.push_back(&job);
lock.unlock();
bool ok = runWamrc(job);
lock.lock();
m_running.erase(std::find(m_running.begin(), m_running.end(), &job));
if (ok && !job.cancelled && m_arrival)
{
Artifact ready{job.moduleKey,
job.species,
keyedCacheDir() + "/" +
artifactName(job.moduleKey, job.species)};
lock.unlock();
m_arrival(ready);
lock.lock();
}
if (m_queue.empty() && m_running.empty())
{
m_idle.notify_all();
}
}
}
bool ModuleTierLadder::runWamrc(Job& job)
{
// Callers dropped m_mutex; only job fields and immutable config are
// touched off-lock, except pid which the scheduler reads under lock to
// kill superseded compiles.
std::string finalPath;
std::string wamrc;
{
std::unique_lock<std::mutex> lock(m_mutex);
finalPath =
keyedCacheDir() + "/" + artifactName(job.moduleKey, job.species);
wamrc = m_wamrcPath;
}
std::string tmpPath = finalPath + ".tmp";
std::vector<std::string> args = {wamrc};
if (job.species == TierSpecies::hw)
{
// Guard-page bounds; native stack checks stay sw (the runtime
// builds with WASM_DISABLE_STACK_HW_BOUND_CHECK).
args.push_back("--bounds-checks=0");
args.push_back("--stack-bounds-checks=1");
}
else
{
// sw bounds run on every build; wamrc's default hw-bounds output
// segfaults on runtimes without the guard-page trap handler.
args.push_back("--bounds-checks=1");
}
if (job.species == TierSpecies::o0 || job.species == TierSpecies::hw)
{
args.push_back("--opt-level=0");
}
else
{
// LLVM's optimizing backend miscompiles the strict (constrained)
// FP wamrc emits, probabilistically corrupting float-heavy modules
// (box2d was the repro; the town's draco trap was the same class).
// Our vendored wamrc pins codegen conservative while the IR still
// optimizes; lift only against the box2d strict-probe soak.
args.push_back("--opt-level=1");
args.push_back("--codegen-opt-level=0");
}
args.push_back("-o");
args.push_back(tmpPath);
args.push_back(job.wasmPath);
std::vector<char*> argv;
for (auto& a : args)
{
argv.push_back(const_cast<char*>(a.c_str()));
}
argv.push_back(nullptr);
#ifdef RIVE_ANDROID
// No wamrc on device, and posix_spawn needs API 28; the ladder never
// schedules compiles here.
return false;
#else
pid_t pid = -1;
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
// A compiler's stdout is noise at editor runtime; stderr stays for
// diagnosing failed compiles.
posix_spawn_file_actions_addopen(&actions,
STDOUT_FILENO,
"/dev/null",
O_WRONLY,
0);
int rc = posix_spawn(&pid,
wamrc.c_str(),
&actions,
nullptr,
argv.data(),
environ);
posix_spawn_file_actions_destroy(&actions);
if (rc != 0)
{
return false;
}
{
std::unique_lock<std::mutex> lock(m_mutex);
job.pid = pid;
}
// The whole child runs below interactive priority; the editor's frame
// loop must never contend with LLVM.
setpriority(PRIO_PROCESS, pid, 10);
int status = 0;
while (waitpid(pid, &status, 0) < 0 && errno == EINTR)
{
}
{
std::unique_lock<std::mutex> lock(m_mutex);
job.pid = -1;
}
if (job.cancelled || !WIFEXITED(status) || WEXITSTATUS(status) != 0)
{
unlink(tmpPath.c_str());
return false;
}
// Atomic arrival: a partial artifact can never carry the final name.
return rename(tmpPath.c_str(), finalPath.c_str()) == 0;
#endif
}
void ModuleTierLadder::drain()
{
std::unique_lock<std::mutex> lock(m_mutex);
m_idle.wait(lock, [this] { return m_queue.empty() && m_running.empty(); });
}
} // namespace rive
#else // _WIN32
namespace rive
{
// Windows editor execution is a deferred decision; the ladder reports
// disabled and every module stays on interp.
ModuleTierLadder& ModuleTierLadder::instance()
{
static ModuleTierLadder* ladder = new ModuleTierLadder();
return *ladder;
}
ModuleTierLadder::~ModuleTierLadder() {}
void ModuleTierLadder::configure(const std::string&, const std::string&) {}
bool ModuleTierLadder::enabled() { return false; }
void ModuleTierLadder::onArrival(ArrivalCallback) {}
void ModuleTierLadder::schedule(const std::string&,
uint64_t,
Span<const uint8_t>)
{}
void ModuleTierLadder::stagePristine(uint64_t, Span<const uint8_t>) {}
std::string ModuleTierLadder::artifactPath(uint64_t, TierSpecies)
{
return std::string();
}
void ModuleTierLadder::drain() {}
} // namespace rive
#endif // _WIN32
#endif // WITH_RIVE_SCRIPTING_WASM