blob: f70f1e7a266d75620e12511dd8e1a28d42f09677 [file]
#ifdef WITH_RIVE_SCRIPTING_WASM
#include "rive/wasm/module_tier_ladder.hpp"
#ifdef RIVE_WASM_TIER_LADDER
#include <algorithm>
#include <atomic>
#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;
#ifndef RIVE_NX
if (job->pid > 0)
{
kill(job->pid, SIGKILL);
}
#endif
}
}
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");
#ifdef RIVE_NX
// No temp_directory_path in this libc++, and the platform cannot map
// executable pages anyway, so the ladder only runs when pointed at a
// directory explicitly.
m_cacheDir = dirEnv != nullptr ? dirEnv : "";
#else
m_cacheDir =
dirEnv != nullptr
? dirEnv
: (std::filesystem::temp_directory_path() / "rive_aot_cache")
.string();
#endif
}
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";
#ifndef RIVE_NX
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);
}
#endif
}
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 get served on a
// cache hit.
std::string dir = m_cacheDir + "/" + wamrcVersion() + "-r4";
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);
}
std::string ModuleTierLadder::compileSync(uint64_t moduleKey,
Span<const uint8_t> moduleBytes,
TierSpecies species)
{
if (!enabled())
{
return std::string();
}
std::string existing = artifactPath(moduleKey, species);
if (!existing.empty())
{
return existing;
}
stagePristine(moduleKey, moduleBytes);
std::string wasmPath;
{
std::unique_lock<std::mutex> lock(m_mutex);
char wasmName[64];
snprintf(wasmName,
sizeof(wasmName),
"%016llx.wasm",
(unsigned long long)moduleKey);
wasmPath = keyedCacheDir() + "/" + wasmName;
}
// Runs on the caller's thread and never joins m_running, so lane
// supersession cannot kill it out from under the boot.
Job job{std::string(), moduleKey, species, wasmPath, 0};
if (!runWamrc(job))
{
return std::string();
}
return artifactPath(moduleKey, species);
}
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.
// Same-hash jobs are not stale — a rebuild of identical bytes must not
// kill its own compile — so they ride forward on the new generation
// and their species skip re-queueing below.
std::vector<TierSpecies> inFlight;
for (Job* job : m_running)
{
if (job->laneId != laneId || job->generation >= generation)
{
continue;
}
// A job already cancelled by an earlier supersede will not produce
// an artifact, so it cannot stand in for this request even when the
// bytes match: let its species re-queue below.
if (job->moduleKey == moduleKey && !job->cancelled)
{
job->generation = generation;
inFlight.push_back(job->species);
}
else if (job->moduleKey != moduleKey && job->pid > 0 && !job->cancelled)
{
job->cancelled = true;
#ifndef RIVE_NX
kill(job->pid, SIGKILL);
#endif
}
}
for (Job& job : m_queue)
{
if (job.laneId == laneId && job.moduleKey == moduleKey &&
!job.cancelled)
{
job.generation = generation;
inFlight.push_back(job.species);
}
}
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)
{
if (std::find(inFlight.begin(), inFlight.end(), species) !=
inFlight.end())
{
continue;
}
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;
}
// Unique temp per compile: the same artifact can be raced by a worker
// and a sync boot, or by two processes sharing the cache.
static std::atomic<uint64_t> tmpSerial{0};
std::string tmpPath = finalPath + "." + std::to_string(getpid()) + "-" +
std::to_string(++tmpSerial) + ".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
{
args.push_back("--opt-level=3");
}
#if (defined(__APPLE__) || defined(_WIN32)) && \
(defined(__aarch64__) || defined(_M_ARM64))
// The platform clobbers x18; reserve it here so safety does not depend on
// the wamrc we were handed. Features need an explicit cpu.
args.push_back("--cpu=generic");
args.push_back("--cpu-features=+reserve-x18");
#endif
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);
#if defined(RIVE_ANDROID) || defined(RIVE_NX)
// No wamrc on device, and neither platform has posix_spawn; 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)
{
if (!job.cancelled)
{
// wamrc prints usage errors to stdout, which is discarded; an
// unlogged exit here is otherwise a silently dead lane.
fprintf(stderr,
"[wasm] wamrc failed (exit %d) on %s\n",
WIFEXITED(status) ? WEXITSTATUS(status) : -1,
job.wasmPath.c_str());
}
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
#endif // RIVE_WASM_TIER_LADDER
#endif // WITH_RIVE_SCRIPTING_WASM