LibWasm+LibWeb: Add per-module wasm compile stats

This commit is contained in:
Ali Mohammad Pur 2026-05-11 16:35:24 +02:00 committed by Ali Mohammad Pur
parent 2419cce8d8
commit 842e8a6796
8 changed files with 130 additions and 2 deletions

View file

@ -16,6 +16,63 @@
namespace Wasm {
static Vector<ModuleStats> s_module_stats;
void record_module_stats(ModuleStats stats)
{
s_module_stats.append(move(stats));
}
void dump_module_stats()
{
if (s_module_stats.is_empty()) {
warnln("wasm-stats: no modules compiled yet");
return;
}
warnln("wasm-stats: {} module(s) compiled", s_module_stats.size());
warnln("wasm-stats: hash input KiB parse ms validate ms cl ms cl blob KiB funcs cache");
AK::Duration total_parse;
AK::Duration total_validate;
AK::Duration total_cranelift;
size_t total_input = 0;
size_t total_blob = 0;
size_t total_hits = 0;
for (auto const& s : s_module_stats) {
StringBuilder hash_prefix;
for (size_t i = 0; i < 4; ++i)
hash_prefix.appendff("{:02x}", s.wasm_hash[i]);
warnln("wasm-stats: {} {:>9} {:>8} {:>11} {:>5} {:>11} {:>5} {}",
hash_prefix.to_byte_string(),
s.input_size_bytes / 1024,
s.parse_time.to_milliseconds(),
s.validate_time.to_milliseconds(),
s.cranelift_time.to_milliseconds(),
s.cranelift_blob_size_bytes / 1024,
s.function_count,
s.cache_hit ? "HIT" : "miss");
total_parse = total_parse + s.parse_time;
total_validate = total_validate + s.validate_time;
total_cranelift = total_cranelift + s.cranelift_time;
total_input += s.input_size_bytes;
total_blob += s.cranelift_blob_size_bytes;
if (s.cache_hit)
++total_hits;
}
warnln("wasm-stats: ---- {:>9} {:>8} {:>11} {:>5} {:>11} hits={}",
total_input / 1024,
total_parse.to_milliseconds(),
total_validate.to_milliseconds(),
total_cranelift.to_milliseconds(),
total_blob / 1024,
total_hits);
}
MemoryBuffer::~MemoryBuffer()
{
clear();

View file

@ -281,16 +281,24 @@ ErrorOr<void, ValidationError> Validator::validate(CodeSection const& section)
begin_cranelift_cache_capture();
capturing = true;
}
if (m_cache_config->out_cache_hit)
*m_cache_config->out_cache_hit = installing;
}
ScopeGuard cleanup = [&] {
auto cranelift_start = MonotonicTime::now();
flush_cranelift_batch();
auto cranelift_duration = MonotonicTime::now() - cranelift_start;
if (installing)
abort_cranelift_cache_install();
size_t produced_blob_size = 0;
if (capturing) {
if (validation_succeeded) {
auto hash = ReadonlyBytes { m_cache_config->wasm_hash.data(), 32 };
if (auto blob = serialize_cranelift_cache_blob(hash); blob.has_value() && m_cache_config->on_compiled) {
produced_blob_size = blob->size();
m_cache_config->on_compiled(blob.release_value());
} else {
abort_cranelift_cache_capture();
@ -299,6 +307,22 @@ ErrorOr<void, ValidationError> Validator::validate(CodeSection const& section)
abort_cranelift_cache_capture();
}
}
if (m_cache_config.has_value()) {
if (m_cache_config->out_cranelift_time)
*m_cache_config->out_cranelift_time = cranelift_duration;
if (m_cache_config->out_cranelift_blob_size_bytes)
*m_cache_config->out_cranelift_blob_size_bytes = produced_blob_size;
if (m_cache_config->out_function_count) {
size_t count = 0;
for (auto& entry : section.functions()) {
if (entry.func().body().compiled_instructions.cranelift_compiled)
++count;
}
*m_cache_config->out_function_count = count;
}
}
set_cranelift_active_function_index(NumericLimits<u32>::max());
};

View file

@ -17,6 +17,7 @@
#include <AK/OwnPtr.h>
#include <AK/Result.h>
#include <AK/String.h>
#include <AK/Time.h>
#include <AK/UFixedBigInt.h>
#include <AK/Variant.h>
#include <AK/WeakPtr.h>
@ -1555,12 +1556,34 @@ void flush_cranelift_batch();
// to install it before falling through to cranelift.
// - `on_compiled` is invoked exactly once if a fresh blob was produced; never
// invoked on a cache hit nor when no cranelift output was captured.
// - The `out_*` fields, if non-null, receive measurements taken during
// validate(CodeSection). Used by the LibWeb side to populate ModuleStats.
struct CompileCacheConfig {
Array<u8, 32> wasm_hash {};
ReadonlyBytes existing_blob;
AK::Function<void(ByteBuffer)> on_compiled;
AK::Duration* out_cranelift_time { nullptr };
size_t* out_function_count { nullptr };
size_t* out_cranelift_blob_size_bytes { nullptr };
bool* out_cache_hit { nullptr };
};
// Lightweight per-module compile stats accumulator. Exposed to embedders via record_module_stats() below.
struct ModuleStats {
Array<u8, 32> wasm_hash {};
size_t input_size_bytes { 0 };
AK::Duration parse_time;
AK::Duration validate_time;
AK::Duration cranelift_time;
size_t cranelift_blob_size_bytes { 0 };
size_t function_count { 0 };
bool cache_hit { false };
};
WASM_API void record_module_stats(ModuleStats);
WASM_API void dump_module_stats();
// Cranelift disk-cache plumbing. Validator drives these around CodeSection validation:
// 1. set_cranelift_active_function_index() before each function so cache-hit installs
// and post-compile capture know which function they're talking about.

View file

@ -432,8 +432,13 @@ JS::ThrowCompletionOr<NonnullRefPtr<CompiledWebAssemblyModule>> compile_a_webass
{
TRY(host_ensure_can_compile_wasm_bytes(vm));
Wasm::ModuleStats stats;
stats.input_size_bytes = data.size();
auto parse_start = MonotonicTime::now();
FixedMemoryStream stream { data.bytes() };
auto module_result = Wasm::Module::parse(stream);
stats.parse_time = MonotonicTime::now() - parse_start;
if (module_result.is_error()) {
return vm.throw_completion<CompileError>(Wasm::parse_error_to_byte_string(module_result.error()));
}
@ -447,6 +452,7 @@ JS::ThrowCompletionOr<NonnullRefPtr<CompiledWebAssemblyModule>> compile_a_webass
Optional<Wasm::CompileCacheConfig> wasm_cache_config;
if (ResourceLoader::is_initialized() && ResourceLoader::the().request_client()) {
auto digest = ::Crypto::Hash::SHA256::hash(data.data(), data.size());
__builtin_memcpy(stats.wasm_hash.data(), digest.bytes().data(), 32);
StringBuilder hex_builder;
for (auto byte : digest.bytes())
@ -476,12 +482,22 @@ JS::ThrowCompletionOr<NonnullRefPtr<CompiledWebAssemblyModule>> compile_a_webass
url, method, OptionalNone {}, 0u,
HTTP::CacheEntryAssociatedData::WebAssemblyCompiledCode, blob.bytes());
};
config.out_cranelift_time = &stats.cranelift_time;
config.out_function_count = &stats.function_count;
config.out_cranelift_blob_size_bytes = &stats.cranelift_blob_size_bytes;
config.out_cache_hit = &stats.cache_hit;
wasm_cache_config = move(config);
}
}
auto& cache = get_cache(*vm.current_realm());
if (auto validation_result = cache.abstract_machine().validate(module_result.value(), move(wasm_cache_config)); validation_result.is_error()) {
auto validate_start = MonotonicTime::now();
auto validation_result = cache.abstract_machine().validate(module_result.value(), move(wasm_cache_config));
stats.validate_time = MonotonicTime::now() - validate_start;
Wasm::record_module_stats(stats);
if (validation_result.is_error()) {
return vm.throw_completion<CompileError>(validation_result.error().error_string);
}
auto compiled_module = make_ref_counted<CompiledWebAssemblyModule>(module_result.release_value());

View file

@ -1455,6 +1455,7 @@ void Application::initialize_actions()
m_debug_menu->add_action(Action::create("Dump CSS Errors"sv, ActionID::DumpCSSErrors, debug_request("dump-all-css-errors"sv)));
m_debug_menu->add_action(Action::create("Dump Cookies"sv, ActionID::DumpCookies, [this]() { m_cookie_jar->dump_cookies(); }));
m_debug_menu->add_action(Action::create("Dump Local Storage"sv, ActionID::DumpLocalStorage, debug_request("dump-local-storage"sv)));
m_debug_menu->add_action(Action::create("Dump WASM Stats"sv, ActionID::DumpWasmStats, debug_request("dump-wasm-stats"sv)));
m_debug_menu->add_action(Action::create("Dump GC graph"sv, ActionID::DumpGCGraph, [this]() {
if (auto view = active_web_view(); view.has_value()) {
auto gc_graph_path = view->dump_gc_graph();

View file

@ -98,6 +98,7 @@ enum class ActionID {
DumpCookies,
DumpLocalStorage,
DumpGCGraph,
DumpWasmStats,
ShowLineBoxBorders,
CollectGarbage,
SpoofUserAgent,

View file

@ -31,7 +31,7 @@ target_include_directories(webcontentservice PUBLIC $<BUILD_INTERFACE:${CMAKE_CU
target_include_directories(webcontentservice PUBLIC $<BUILD_INTERFACE:${LADYBIRD_SOURCE_DIR}>)
target_include_directories(webcontentservice PUBLIC $<BUILD_INTERFACE:${LADYBIRD_SOURCE_DIR}/Services/>)
target_link_libraries(webcontentservice PUBLIC LibCore LibCrypto LibFileSystem LibGfx LibHTTP LibIPC LibJS LibMain LibMedia LibWeb LibWebSocket LibRequests LibWebView LibImageDecoderClient LibGC)
target_link_libraries(webcontentservice PUBLIC LibCore LibCrypto LibFileSystem LibGfx LibHTTP LibIPC LibJS LibMain LibMedia LibWasm LibWeb LibWebSocket LibRequests LibWebView LibImageDecoderClient LibGC)
target_link_libraries(webcontentservice PRIVATE OpenSSL::Crypto OpenSSL::SSL)
target_link_libraries(webcontentservice PRIVATE SDL3::SDL3)

View file

@ -21,6 +21,7 @@
#include <LibJS/Runtime/ConsoleObject.h>
#include <LibJS/Runtime/Date.h>
#include <LibUnicode/TimeZone.h>
#include <LibWasm/Types.h>
#include <LibWeb/ARIA/RoleType.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/ComputedProperties.h>
@ -428,6 +429,11 @@ void ConnectionFromClient::debug_request(u64 page_id, ByteString request, ByteSt
return;
}
if (request == "dump-wasm-stats") {
Wasm::dump_module_stats();
return;
}
if (request == "collect-garbage") {
// NOTE: We use deferred_invoke here to ensure that GC runs with as little on the stack as possible.
Core::deferred_invoke([] {