///|
fn split_string(s : String, delim : String) -> Array[String] {
let out : Array[String] = []
for part in s.split(delim) {
out.push(part.to_string())
}
out
}
///|
fn split_non_empty(s : String, delim : String) -> Array[String] {
if s.is_empty() {
return []
}
let out : Array[String] = []
for part in s.split(delim) {
let item = part.to_string()
if not(item.is_empty()) {
out.push(item)
}
}
out
}
///|
fn parse_uint_decimal(text : String) -> UInt? {
if text.is_empty() {
return None
}
let mut value = 0U
for ch in text {
let code = ch.to_int()
if code < 48 || code > 57 {
return None
}
value = value * 10U + (code - 48).reinterpret_as_uint()
}
Some(value)
}
///|
fn parse_u64_decimal(text : String) -> UInt64? {
if text.is_empty() {
return None
}
let mut value = 0UL
for ch in text {
let code = ch.to_int()
if code < 48 || code > 57 {
return None
}
value = value * 10UL + (code - 48).to_uint64()
}
Some(value)
}
///|
fn compare_runtime_unresolved_export(
a : RuntimeUnresolvedExport,
b : RuntimeUnresolvedExport,
) -> Int {
let by_name = a.export_name.compare(b.export_name)
if by_name != 0 {
by_name
} else {
let by_reason = a.reason.compare(b.reason)
if by_reason != 0 {
by_reason
} else {
a.detail.compare(b.detail)
}
}
}
///|
fn parse_runtime_unresolved_exports(
serialized : String,
) -> Array[RuntimeUnresolvedExport] {
let out : Array[RuntimeUnresolvedExport] = []
for entry in split_non_empty(serialized, ";;") {
let fields = split_string(entry, "\t")
if fields.length() == 0 {
continue
}
let export_name = fields[0]
let reason = if fields.length() >= 2 { fields[1] } else { "unknown" }
let detail = if fields.length() >= 3 { fields[2] } else { "" }
out.push({ export_name, reason, detail })
}
out.sort_by(compare_runtime_unresolved_export)
out
}
///|
fn serialize_runtime_profile_scenarios(
scenarios : Array[RuntimeProfileScenario],
) -> String {
if scenarios.length() == 0 {
return ""
}
let entries : Array[String] = []
for scenario in scenarios {
let args_text = if scenario.args.length() == 0 {
""
} else {
scenario.args.map(v => v.to_string()).join(",")
}
entries.push(scenario.export_name + "\t" + args_text)
}
entries.join(";;")
}
///|
extern "js" fn js_profile_runtime_exports(
bytes : Bytes,
iterations : Int,
scenario_spec : String,
) -> String =
#| (bytes, iterations, scenarioSpec) => {
#| try {
#| const raw = bytes instanceof Uint8Array
#| ? bytes
#| : (Array.isArray(bytes) ? Uint8Array.from(bytes) : new Uint8Array(bytes));
#| const rounds = Math.max(1, iterations | 0);
#| const sanitize = (value) => String(value)
#| .replace(/\|/g, '/')
#| .replace(/\t/g, ' ')
#| .replace(/\r?\n/g, ' ')
#| .replace(/;;/g, ';');
#| const encodeUnresolved = (name, reason, detail) =>
#| sanitize(name) + '\t' + sanitize(reason) + '\t' + sanitize(detail);
#| const encodeProfile = (name, calls, totalNs) =>
#| sanitize(name) + ',' + String(calls) + ',' + String(totalNs);
#| const parseScenarioSpec = (rawSpec) => {
#| const out = { hasSpec: false, scenarios: [], invalid: [] };
#| if (!rawSpec || rawSpec.length === 0) return out;
#| out.hasSpec = true;
#| for (const rawEntry of rawSpec.split(';;')) {
#| if (!rawEntry) continue;
#| const fields = rawEntry.split('\t');
#| const exportName = (fields[0] || '').trim();
#| const argsField = fields.length >= 2 ? (fields[1] || '').trim() : '';
#| if (!exportName) continue;
#| const args = [];
#| let valid = true;
#| if (argsField.length > 0) {
#| for (const rawArg of argsField.split(',')) {
#| const arg = rawArg.trim();
#| if (!arg) continue;
#| if (!/^[-]?\d+$/.test(arg)) {
#| valid = false;
#| break;
#| }
#| args.push(Number(arg));
#| }
#| }
#| const displayName = argsField.length === 0
#| ? exportName
#| : exportName + '(' + argsField.replace(/,/g, '_') + ')';
#| if (!valid) {
#| out.invalid.push({
#| displayName,
#| detail: 'invalid-integer-arg',
#| });
#| continue;
#| }
#| out.scenarios.push({
#| exportName,
#| args,
#| displayName: args.length === 0
#| ? exportName
#| : exportName + '(' + args.join('_') + ')',
#| });
#| }
#| return out;
#| };
#| const scenario = parseScenarioSpec(scenarioSpec);
#| const module = new WebAssembly.Module(raw);
#| const exportNames = WebAssembly.Module.exports(module)
#| .filter((item) => item.kind === 'function')
#| .map((item) => item.name);
#| const importDescs = WebAssembly.Module.imports(module);
#| const imports = {};
#| const ensureBucket = (name) => {
#| if (!imports[name]) imports[name] = {};
#| return imports[name];
#| };
#| let envMemory = null;
#| let sbrkPtr = 1024;
#| const ensureSbrk = () => {
#| if (!envMemory) return;
#| if (envMemory.buffer.byteLength < sbrkPtr + 4) return;
#| new DataView(envMemory.buffer).setInt32(sbrkPtr, 8192, true);
#| };
#| for (const imp of importDescs) {
#| const bucket = ensureBucket(imp.module);
#| if (imp.kind === 'memory') {
#| const fallbackMin = (imp.module === 'env' && imp.name === 'memory') ? 256 : 1;
#| const minimum = Math.max(1, Number(imp.minimum ?? fallbackMin));
#| const maximum = imp.maximum == null ? minimum : Math.max(minimum, Number(imp.maximum));
#| const memory = new WebAssembly.Memory({ initial: minimum, maximum });
#| bucket[imp.name] = memory;
#| if (imp.module === 'env' && imp.name === 'memory') {
#| envMemory = memory;
#| }
#| } else if (imp.kind === 'table') {
#| const fallbackMin = (imp.module === 'env' && imp.name === '__indirect_function_table') ? 32 : 1;
#| const minimum = Math.max(1, Number(imp.minimum ?? fallbackMin));
#| const tableInit = {
#| element: imp.element ?? 'anyfunc',
#| initial: minimum,
#| };
#| if (imp.maximum != null) {
#| tableInit.maximum = Math.max(minimum, Number(imp.maximum));
#| }
#| bucket[imp.name] = new WebAssembly.Table(tableInit);
#| } else if (imp.kind === 'global') {
#| const value = imp.value ?? 'i32';
#| const mutable = !!imp.mutable;
#| const init = value === 'i64' ? 0n : 0;
#| bucket[imp.name] = new WebAssembly.Global({ value, mutable }, init);
#| }
#| }
#| if (!envMemory) {
#| envMemory = new WebAssembly.Memory({ initial: 256, maximum: 256 });
#| ensureBucket('env').memory = envMemory;
#| }
#| ensureSbrk();
#| for (const imp of importDescs) {
#| if (imp.kind !== 'function') continue;
#| const bucket = ensureBucket(imp.module);
#| if (typeof bucket[imp.name] === 'function') continue;
#| const key = imp.module + '.' + imp.name;
#| if (key === 'env.abort') {
#| bucket[imp.name] = () => { throw new Error('abort'); };
#| } else if (key === 'env.exit') {
#| bucket[imp.name] = (code) => code | 0;
#| } else if (key === 'env.emscripten_get_sbrk_ptr') {
#| bucket[imp.name] = () => sbrkPtr;
#| } else if (key === 'env.emscripten_resize_heap') {
#| bucket[imp.name] = () => 0;
#| } else if (key === 'env.emscripten_memcpy_big') {
#| bucket[imp.name] = (dst, src, num) => {
#| if (envMemory) {
#| const view = new Uint8Array(envMemory.buffer);
#| view.copyWithin(dst >>> 0, src >>> 0, (src + num) >>> 0);
#| }
#| return dst | 0;
#| };
#| } else if (imp.module === 'wasi_snapshot_preview1') {
#| bucket[imp.name] = () => 0;
#| } else {
#| bucket[imp.name] = () => 0;
#| }
#| }
#| let instantiateNs = '0';
#| let instance = null;
#| try {
#| const t0 = process.hrtime.bigint();
#| instance = new WebAssembly.Instance(module, imports);
#| const t1 = process.hrtime.bigint();
#| instantiateNs = (t1 - t0).toString();
#| } catch (e) {
#| const msg = e && e.message ? e.message : String(e);
#| const unresolved = [];
#| for (const invalid of scenario.invalid) {
#| unresolved.push(
#| encodeUnresolved(
#| invalid.displayName,
#| 'invalid-scenario',
#| invalid.detail,
#| ),
#| );
#| }
#| const targets = scenario.hasSpec
#| ? scenario.scenarios.map((item) => item.displayName)
#| : exportNames;
#| for (const name of targets) {
#| unresolved.push(encodeUnresolved(name, 'import-missing', msg));
#| }
#| return 'ok|' + instantiateNs + '|||' + unresolved.join(';;');
#| }
#| const profiled = [];
#| const skipped = [];
#| const unresolved = scenario.invalid.map((invalid) =>
#| encodeUnresolved(
#| invalid.displayName,
#| 'invalid-scenario',
#| invalid.detail,
#| )
#| );
#| if (scenario.hasSpec) {
#| for (const item of scenario.scenarios) {
#| const value = instance.exports[item.exportName];
#| if (typeof value !== 'function') {
#| unresolved.push(
#| encodeUnresolved(
#| item.displayName,
#| 'export-not-found',
#| item.exportName,
#| ),
#| );
#| continue;
#| }
#| if (value.length !== item.args.length) {
#| unresolved.push(
#| encodeUnresolved(
#| item.displayName,
#| 'signature-mismatch',
#| 'arity=' + value.length + ' args=' + item.args.length,
#| ),
#| );
#| continue;
#| }
#| let total = 0n;
#| let failed = null;
#| for (let i = 0; i < rounds; i++) {
#| try {
#| const s = process.hrtime.bigint();
#| value(...item.args);
#| total += (process.hrtime.bigint() - s);
#| } catch (e) {
#| failed = e;
#| break;
#| }
#| }
#| if (failed) {
#| const msg = failed && failed.message ? failed.message : String(failed);
#| unresolved.push(
#| encodeUnresolved(item.displayName, 'runtime-exception', msg),
#| );
#| continue;
#| }
#| profiled.push(encodeProfile(item.displayName, rounds, total.toString()));
#| }
#| } else {
#| for (const [name, value] of Object.entries(instance.exports)) {
#| if (typeof value !== 'function') continue;
#| if (value.length !== 0) {
#| skipped.push(name);
#| unresolved.push(
#| encodeUnresolved(name, 'signature-mismatch', 'arity=' + value.length)
#| );
#| continue;
#| }
#| let total = 0n;
#| let failed = null;
#| for (let i = 0; i < rounds; i++) {
#| try {
#| const s = process.hrtime.bigint();
#| value();
#| total += (process.hrtime.bigint() - s);
#| } catch (e) {
#| failed = e;
#| break;
#| }
#| }
#| if (failed) {
#| const msg = failed && failed.message ? failed.message : String(failed);
#| unresolved.push(
#| encodeUnresolved(name, 'runtime-exception', msg)
#| );
#| continue;
#| }
#| profiled.push(encodeProfile(name, rounds, total.toString()));
#| }
#| }
#| return 'ok|' + instantiateNs + '|' + profiled.join(';;') + '|' + skipped.join(',') + '|' + unresolved.join(';;');
#| } catch (e) {
#| const msg = e && e.message ? e.message : String(e);
#| return 'err|' + msg;
#| }
#| }
///|
pub fn profile_runtime_exports(
bytes : Bytes,
iterations? : UInt = 50U,
scenarios? : Array[RuntimeProfileScenario] = [],
) -> Result[RuntimeProfile, String] {
let raw = js_profile_runtime_exports(
bytes,
UInt::reinterpret_as_int(iterations),
serialize_runtime_profile_scenarios(scenarios),
)
let parts = split_string(raw, "|")
if parts.length() < 2 {
return Err("invalid runtime profile response")
}
if parts[0] == "err" {
return Err("runtime profile failed: " + parts[1])
}
if parts[0] != "ok" {
return Err("unknown runtime profile status: " + parts[0])
}
if parts.length() < 3 {
return Err("runtime profile response is missing fields")
}
let instantiate_ns = match parse_u64_decimal(parts[1]) {
Some(v) => v
None => return Err("failed to parse instantiate time: " + parts[1])
}
let functions : Array[RuntimeFunctionProfile] = []
let profile_entries = if parts.length() >= 3 { parts[2] } else { "" }
for entry in split_non_empty(profile_entries, ";;") {
let fields = split_string(entry, ",")
if fields.length() != 3 {
return Err("invalid function profile entry: " + entry)
}
let calls = match parse_uint_decimal(fields[1]) {
Some(v) => v
None => return Err("invalid call count in profile entry: " + entry)
}
let total_ns = match parse_u64_decimal(fields[2]) {
Some(v) => v
None => return Err("invalid total ns in profile entry: " + entry)
}
let average_ns = if calls == 0U {
0UL
} else {
total_ns / calls.to_uint64()
}
functions.push({ name: fields[0], calls, total_ns, average_ns })
}
let skipped_exports = if parts.length() >= 4 {
split_non_empty(parts[3], ",")
} else {
[]
}
let unresolved_exports = if parts.length() >= 5 {
parse_runtime_unresolved_exports(parts[4])
} else {
[]
}
Ok({
instantiate_ns,
iterations,
functions,
skipped_exports,
unresolved_exports,
})
}
///|
pub fn profile_runtime_zero_arg_exports(
bytes : Bytes,
iterations? : UInt = 50U,
) -> Result[RuntimeProfile, String] {
profile_runtime_exports(bytes, iterations~)
}