test(hooks): measure per-session quickjs provider cost

ENABLE_TESTS seam measureQuickJSSessionCostForTests runs N cold sessions
exactly as validation builds them (create → initialize → arm invocation
fuel → validate) and reports wall time per stage (sum and min) plus the
initialization and invocation fuel consumed; ProviderSession gains
initializationFuelConsumed beside invocationFuelConsumed. JSHooks_test
logs the figures (issue 0001 step 1: measure before changing provider
bytes) and asserts only the shape.

on this mac, release, live producer bundle: ~200us per cold session
(create 34us, initialize 157us, validate 6us), init fuel 1800041 of
5000000, invocation fuel 49832 of 50000000.
This commit is contained in:
Nicholas Dudfield committed 2026-08-15 18:12:45 +07:00
1 parent 41400f1cc6
commit b6b57e3ec3
5 files changed
+173 -3

No files matched your search

+29
View File
@@ -618,6 +618,35 @@ int64_t hook(uint32_t reserved)
}
//@@end jshooks-provider-launch
testcase("Measure per-session provider cost");
{
// Issue 0001 step 1: what one cold session costs on this tree,
// before any Wizer/AOT change to the provider bytes. Numbers are
// logged, not asserted; only the shape is checked.
auto const cost = hook::measureQuickJSSessionCostForTests(
currentRuntime, hookBytecode, 50);
BEAST_EXPECTS(!cost.error, cost.error.value_or(""));
BEAST_EXPECT(cost.iterations == 50);
BEAST_EXPECT(cost.initializationFuelConsumed > 0);
BEAST_EXPECT(cost.invocationFuelConsumed > 0);
auto const perIteration = [&](std::uint64_t total) {
return cost.iterations ? total / cost.iterations : 0;
};
log << "provider session cost over " << cost.iterations
<< " cold sessions (mean/min microseconds): create "
<< perIteration(cost.createNanosTotal) / 1000 << "/"
<< cost.createNanosMin / 1000 << ", initialize "
<< perIteration(cost.initializeNanosTotal) / 1000 << "/"
<< cost.initializeNanosMin / 1000 << ", validate "
<< perIteration(cost.validateNanosTotal) / 1000 << "/"
<< cost.validateNanosMin / 1000 << "; fuel: initialization "
<< cost.initializationFuelConsumed << " of "
<< hook::currentQuickJSRuntimeProfile().initializationFuel
<< ", invocation " << cost.invocationFuelConsumed << " of "
<< hook::currentQuickJSRuntimeProfile().invocationFuel
<< std::endl;
}
testcase("Execute an enveloped TypeScript Hook transaction");
Env env{*this, features | featureJSHooks};
env.fund(XRP(10000), alice, bob);
+28
View File
@@ -157,6 +157,34 @@ validateQuickJSBytecodeForTests(
/** Register the currently generated profile using a test-supplied provider. */
std::optional<std::string>
setQuickJSProviderForTests(ripple::Blob provider);
/** Per-session cost of the retained provider, measured cold N times.
Each iteration is one disposable session exactly as validation builds
it: create (Store, imports, instantiate) → initialize (`_initialize`,
`qjs_init`, limits) → arm invocation fuel → validate the given bytecode.
Wall times are summed over the iterations and the minimum per stage is
kept; fuel figures are from the last iteration (they are deterministic).
*/
struct QuickJSSessionCostForTests
{
std::size_t iterations = 0;
std::uint64_t createNanosTotal = 0;
std::uint64_t createNanosMin = 0;
std::uint64_t initializeNanosTotal = 0;
std::uint64_t initializeNanosMin = 0;
std::uint64_t validateNanosTotal = 0;
std::uint64_t validateNanosMin = 0;
std::uint64_t initializationFuelConsumed = 0;
std::uint64_t invocationFuelConsumed = 0;
std::optional<std::string> error;
};
QuickJSSessionCostForTests
measureQuickJSSessionCostForTests(
QuickJSRuntimeHandle const& runtime,
std::span<std::uint8_t const> bytecode,
std::size_t iterations);
#endif
} // namespace hook
@@ -359,7 +359,7 @@ ProviderSession::readDiagnostic() noexcept
}
std::optional<std::uint64_t>
ProviderSession::invocationFuelConsumed() const noexcept
ProviderSession::fuelConsumedFrom(std::uint64_t armed) const noexcept
{
std::uint64_t remaining = 0;
if (auto* fuelError = wasmtime_context_get_fuel(context_, &remaining))
@@ -367,8 +367,19 @@ ProviderSession::invocationFuelConsumed() const noexcept
wasmtime_error_delete(fuelError);
return std::nullopt;
}
return runtime_.profile.invocationFuel -
std::min(runtime_.profile.invocationFuel, remaining);
return armed - std::min(armed, remaining);
}
std::optional<std::uint64_t>
ProviderSession::initializationFuelConsumed() const noexcept
{
return fuelConsumedFrom(runtime_.profile.initializationFuel);
}
std::optional<std::uint64_t>
ProviderSession::invocationFuelConsumed() const noexcept
{
return fuelConsumedFrom(runtime_.profile.invocationFuel);
}
} // namespace hook::quickjs
@@ -82,10 +82,18 @@ public:
std::string
readDiagnostic() noexcept;
/** Fuel spent so far against the initialization budget; meaningful
between create and resetInvocationFuel. */
std::optional<std::uint64_t>
initializationFuelConsumed() const noexcept;
std::optional<std::uint64_t>
invocationFuelConsumed() const noexcept;
private:
std::optional<std::uint64_t>
fuelConsumedFrom(std::uint64_t armed) const noexcept;
ProviderSession(
QuickJSProviderRuntime const& runtime,
StorePtr&& store,
@@ -1,6 +1,8 @@
#include <xrpld/app/hook/QuickJSHookRuntime.h>
#include <xrpld/app/hook/detail/quickjs/QuickJSProviderSession.h>
#include <xrpl/basics/scope.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <limits>
#include <string>
@@ -91,6 +93,98 @@ validateQuickJSBytecodeForTests(
runtime, bytecode, result.hasCallback, &result.invocationFuelConsumed);
return result;
}
QuickJSSessionCostForTests
measureQuickJSSessionCostForTests(
QuickJSRuntimeHandle const& runtime,
std::span<std::uint8_t const> bytecode,
std::size_t iterations)
{
using clock = std::chrono::steady_clock;
auto const nanosSince = [](clock::time_point const& start) {
return static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
clock::now() - start)
.count());
};
auto const keepMin = [](std::uint64_t& slot, std::uint64_t sample) {
slot = slot == 0 ? sample : std::min(slot, sample);
};
QuickJSSessionCostForTests cost;
if (!runtime)
{
cost.error = "QuickJS runtime profile is not registered";
return cost;
}
for (std::size_t iteration = 0; iteration < iterations; ++iteration)
{
quickjs::ProviderStage stage = quickjs::ProviderStage::none;
std::string detail;
auto const failure = [&]() {
return detail.empty()
? std::string{quickjs::providerStageName(stage)}
: detail;
};
auto const createStart = clock::now();
auto session =
quickjs::ProviderSession::create(*runtime, nullptr, stage, detail);
auto const createNanos = nanosSince(createStart);
if (!session)
{
cost.error = failure();
return cost;
}
auto const initializeStart = clock::now();
if (!session->initialize(stage, detail))
{
cost.error = failure();
return cost;
}
auto const initializeNanos = nanosSince(initializeStart);
if (auto const consumed = session->initializationFuelConsumed())
cost.initializationFuelConsumed = *consumed;
if (!session->resetInvocationFuel(stage, detail))
{
cost.error = failure();
return cost;
}
auto const validateStart = clock::now();
auto const pointer = session->allocateAndCopy(bytecode, stage, detail);
if (!pointer)
{
cost.error = failure();
return cost;
}
wasmtime_val_t arguments[2] = {
{.kind = WASMTIME_I32,
.of = {.i32 = static_cast<std::int32_t>(*pointer)}},
{.kind = WASMTIME_I32,
.of = {.i32 = static_cast<std::int32_t>(bytecode.size())}}};
wasmtime_val_t result[1];
if (!session->callExport(
"qjs_validate_hook_module", arguments, 2, result, 1, detail))
{
cost.error = detail;
return cost;
}
auto const validateNanos = nanosSince(validateStart);
if (auto const consumed = session->invocationFuelConsumed())
cost.invocationFuelConsumed = *consumed;
++cost.iterations;
cost.createNanosTotal += createNanos;
keepMin(cost.createNanosMin, createNanos);
cost.initializeNanosTotal += initializeNanos;
keepMin(cost.initializeNanosMin, initializeNanos);
cost.validateNanosTotal += validateNanos;
keepMin(cost.validateNanosMin, validateNanos);
}
return cost;
}
#endif
} // namespace hook