mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-23 04:07:13 +00:00
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:
5 files changed
+173
-3
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user