feat: 添加基准测试基础设施
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
Test Name Wall(ms) Comp(ms) Exec(ms) Instrs GC Ct IC Rate Sess Objs Sess B
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
array 0.19 0.000 0.193 291 0 0.0% 0 0
|
||||||
|
call 0.06 0.000 0.058 157 0 0.0% 0 0
|
||||||
|
coercion 0.03 0.000 0.027 163 0 0.0% 0 0
|
||||||
|
gc 0.26 0.000 0.254 343 0 100.0% 0 0
|
||||||
|
string 0.05 0.000 0.049 115 0 100.0% 0 0
|
||||||
Generated
+175
@@ -38,6 +38,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anes"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ansi_term"
|
name = "ansi_term"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -136,6 +142,12 @@ version = "3.20.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cast"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "castaway"
|
name = "castaway"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -178,6 +190,33 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ciborium"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||||
|
dependencies = [
|
||||||
|
"ciborium-io",
|
||||||
|
"ciborium-ll",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ciborium-io"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ciborium-ll"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||||
|
dependencies = [
|
||||||
|
"ciborium-io",
|
||||||
|
"half",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.1"
|
version = "4.6.1"
|
||||||
@@ -259,6 +298,42 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
|
checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "criterion"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||||
|
dependencies = [
|
||||||
|
"anes",
|
||||||
|
"cast",
|
||||||
|
"ciborium",
|
||||||
|
"clap",
|
||||||
|
"criterion-plot",
|
||||||
|
"is-terminal",
|
||||||
|
"itertools",
|
||||||
|
"num-traits",
|
||||||
|
"once_cell",
|
||||||
|
"oorandom",
|
||||||
|
"plotters",
|
||||||
|
"rayon",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"tinytemplate",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "criterion-plot"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||||
|
dependencies = [
|
||||||
|
"cast",
|
||||||
|
"itertools",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -284,6 +359,12 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "6.2.1"
|
version = "6.2.1"
|
||||||
@@ -428,6 +509,17 @@ dependencies = [
|
|||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "half"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crunchy",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -462,6 +554,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "home"
|
name = "home"
|
||||||
version = "0.5.12"
|
version = "0.5.12"
|
||||||
@@ -513,12 +611,32 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-terminal"
|
||||||
|
version = "0.4.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.10.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -667,6 +785,12 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oorandom"
|
||||||
|
version = "11.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owo-colors"
|
name = "owo-colors"
|
||||||
version = "4.3.0"
|
version = "4.3.0"
|
||||||
@@ -908,6 +1032,8 @@ dependencies = [
|
|||||||
"oxide_types",
|
"oxide_types",
|
||||||
"oxide_vm",
|
"oxide_vm",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -988,6 +1114,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"criterion",
|
||||||
"fast-float",
|
"fast-float",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1073,6 +1200,34 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plotters"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"plotters-backend",
|
||||||
|
"plotters-svg",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plotters-backend"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plotters-svg"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||||
|
dependencies = [
|
||||||
|
"plotters-backend",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -1516,6 +1671,16 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinytemplate"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@@ -1789,6 +1954,16 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.99"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"test_name": "array",
|
||||||
|
"wall_time_us": 193,
|
||||||
|
"session_objects": 0,
|
||||||
|
"session_bytes": 0,
|
||||||
|
"epoch_objects": 1,
|
||||||
|
"epoch_bytes": 0,
|
||||||
|
"gc_trigger_count": 0,
|
||||||
|
"gc_bytes_freed": 0,
|
||||||
|
"gc_objects_scanned": 0,
|
||||||
|
"gc_collection_us": 0,
|
||||||
|
"instruction_count": 291,
|
||||||
|
"compile_time_us": 0,
|
||||||
|
"exec_time_us": 193,
|
||||||
|
"ic_hit_rate": 0.0,
|
||||||
|
"ic_hits": 0,
|
||||||
|
"ic_misses": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test_name": "call",
|
||||||
|
"wall_time_us": 59,
|
||||||
|
"session_objects": 0,
|
||||||
|
"session_bytes": 0,
|
||||||
|
"epoch_objects": 2,
|
||||||
|
"epoch_bytes": 0,
|
||||||
|
"gc_trigger_count": 0,
|
||||||
|
"gc_bytes_freed": 0,
|
||||||
|
"gc_objects_scanned": 0,
|
||||||
|
"gc_collection_us": 0,
|
||||||
|
"instruction_count": 157,
|
||||||
|
"compile_time_us": 0,
|
||||||
|
"exec_time_us": 58,
|
||||||
|
"ic_hit_rate": 0.0,
|
||||||
|
"ic_hits": 0,
|
||||||
|
"ic_misses": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test_name": "coercion",
|
||||||
|
"wall_time_us": 28,
|
||||||
|
"session_objects": 0,
|
||||||
|
"session_bytes": 0,
|
||||||
|
"epoch_objects": 0,
|
||||||
|
"epoch_bytes": 0,
|
||||||
|
"gc_trigger_count": 0,
|
||||||
|
"gc_bytes_freed": 0,
|
||||||
|
"gc_objects_scanned": 0,
|
||||||
|
"gc_collection_us": 0,
|
||||||
|
"instruction_count": 163,
|
||||||
|
"compile_time_us": 0,
|
||||||
|
"exec_time_us": 27,
|
||||||
|
"ic_hit_rate": 0.0,
|
||||||
|
"ic_hits": 0,
|
||||||
|
"ic_misses": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test_name": "gc",
|
||||||
|
"wall_time_us": 255,
|
||||||
|
"session_objects": 0,
|
||||||
|
"session_bytes": 0,
|
||||||
|
"epoch_objects": 0,
|
||||||
|
"epoch_bytes": 0,
|
||||||
|
"gc_trigger_count": 0,
|
||||||
|
"gc_bytes_freed": 0,
|
||||||
|
"gc_objects_scanned": 0,
|
||||||
|
"gc_collection_us": 0,
|
||||||
|
"instruction_count": 343,
|
||||||
|
"compile_time_us": 0,
|
||||||
|
"exec_time_us": 254,
|
||||||
|
"ic_hit_rate": 1.0,
|
||||||
|
"ic_hits": 135,
|
||||||
|
"ic_misses": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test_name": "string",
|
||||||
|
"wall_time_us": 50,
|
||||||
|
"session_objects": 0,
|
||||||
|
"session_bytes": 0,
|
||||||
|
"epoch_objects": 0,
|
||||||
|
"epoch_bytes": 0,
|
||||||
|
"gc_trigger_count": 0,
|
||||||
|
"gc_bytes_freed": 0,
|
||||||
|
"gc_objects_scanned": 0,
|
||||||
|
"gc_collection_us": 0,
|
||||||
|
"instruction_count": 115,
|
||||||
|
"compile_time_us": 0,
|
||||||
|
"exec_time_us": 49,
|
||||||
|
"ic_hit_rate": 1.0,
|
||||||
|
"ic_hits": 216,
|
||||||
|
"ic_misses": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -22,3 +22,5 @@ oxide_api = { path = "../oxide_api" }
|
|||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
rustyline.workspace = true
|
rustyline.workspace = true
|
||||||
ansi_term.workspace = true
|
ansi_term.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use crate::bench::metrics::MetricCollection;
|
||||||
|
|
||||||
|
pub struct Baseline {
|
||||||
|
pub entries: Vec<MetricCollection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Baseline {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self { entries: Vec::new() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Regression {
|
||||||
|
pub metric: String,
|
||||||
|
pub test_name: String,
|
||||||
|
pub baseline: f64,
|
||||||
|
pub current: f64,
|
||||||
|
pub ratio: f64,
|
||||||
|
pub tolerance: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_baseline() -> Result<Baseline, String> {
|
||||||
|
let json_path = "benchmark_baseline.json";
|
||||||
|
let data = match std::fs::read_to_string(json_path) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("No baseline found at {} — run --update-baseline first", json_path);
|
||||||
|
return Ok(Baseline::empty());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let entries: Vec<MetricCollection> =
|
||||||
|
serde_json::from_str(&data).map_err(|e| format!("Failed to parse baseline: {}", e))?;
|
||||||
|
Ok(Baseline { entries })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_baseline(results: &[MetricCollection]) -> Result<(), String> {
|
||||||
|
let json = serde_json::to_string_pretty(results).map_err(|e| format!("Failed to serialize baseline: {}", e))?;
|
||||||
|
std::fs::write("benchmark_baseline.json", &json)
|
||||||
|
.map_err(|e| format!("Failed to write benchmark_baseline.json: {}", e))?;
|
||||||
|
let md = crate::bench::output::format_text_table(results);
|
||||||
|
std::fs::write("BENCHMARK_BASELINE.md", md).map_err(|e| format!("Failed to write BENCHMARK_BASELINE.md: {}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare_baseline(current: &[MetricCollection]) -> Vec<Regression> {
|
||||||
|
let baseline = match load_baseline() {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
if baseline.entries.is_empty() {
|
||||||
|
eprintln!("No baseline data — skipping regression check");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let mut regressions = Vec::new();
|
||||||
|
for cur in current {
|
||||||
|
for base in &baseline.entries {
|
||||||
|
if base.test_name != cur.test_name {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (name, cur_val) in cur.iter_metrics() {
|
||||||
|
if cur_val == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let base_val = base
|
||||||
|
.iter_metrics()
|
||||||
|
.iter()
|
||||||
|
.find(|(n, _)| *n == name)
|
||||||
|
.map(|(_, v)| *v)
|
||||||
|
.unwrap_or(cur_val);
|
||||||
|
if base_val == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let tolerance = match name {
|
||||||
|
"wall_time_us" => 0.10,
|
||||||
|
"gc_trigger_count" | "gc_bytes_freed" | "gc_objects_scanned" | "gc_collection_us" => 0.50,
|
||||||
|
"session_bytes" => 0.20,
|
||||||
|
_ => 0.20,
|
||||||
|
};
|
||||||
|
let ratio = cur_val / base_val;
|
||||||
|
if ratio > 1.0 + tolerance {
|
||||||
|
regressions.push(Regression {
|
||||||
|
metric: name.to_string(),
|
||||||
|
test_name: cur.test_name.clone(),
|
||||||
|
baseline: base_val,
|
||||||
|
current: cur_val,
|
||||||
|
ratio,
|
||||||
|
tolerance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
regressions
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use oxide_compiler::compiler::{compiled_module_hash, Compiler};
|
||||||
|
use oxide_kernel::kernel::KernelCore;
|
||||||
|
use oxide_parser::Allocator;
|
||||||
|
use oxide_vm::vm_pool::VmPool;
|
||||||
|
|
||||||
|
use crate::bench::baseline::{compare_baseline, save_baseline};
|
||||||
|
use crate::bench::metrics::MetricCollection;
|
||||||
|
use crate::bench::output::{format_json, format_text_table};
|
||||||
|
use crate::bench::BenchConfig;
|
||||||
|
|
||||||
|
pub fn run_js_stress_bench(config: &BenchConfig, kernel: &Arc<KernelCore>, pool: &Arc<VmPool>) -> ExitCode {
|
||||||
|
let mut results: Vec<MetricCollection> = Vec::new();
|
||||||
|
let compiler = Compiler::new();
|
||||||
|
|
||||||
|
let test_dir = "tests/stress";
|
||||||
|
let mut files: Vec<_> = match fs::read_dir(test_dir) {
|
||||||
|
Ok(entries) => entries
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.filter(|e| {
|
||||||
|
e.path().extension().is_some_and(|ext| ext == "js")
|
||||||
|
&& config
|
||||||
|
.filter
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |f| e.file_name().to_string_lossy().contains(f.as_str()))
|
||||||
|
})
|
||||||
|
.map(|e| e.path())
|
||||||
|
.collect(),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Could not read {}: {}", test_dir, e);
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
files.sort();
|
||||||
|
|
||||||
|
if files.is_empty() {
|
||||||
|
eprintln!("No matching stress tests found in {}", test_dir);
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
for path in &files {
|
||||||
|
let test_name = path.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||||
|
let mut js = match fs::read_to_string(path) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to read {:?}: {}", path, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if config.iterations > 0 {
|
||||||
|
js = override_iterations(&js, config.iterations);
|
||||||
|
}
|
||||||
|
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let program = match oxide_parser::parse(&allocator, &js) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(errors) => {
|
||||||
|
eprintln!("Parse error in {}: {:?}", test_name, errors.first());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let hash = compiled_module_hash(&program);
|
||||||
|
let module = match kernel.code_forge().get_or_insert_with(hash, || compiler.compile(&program)) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Compile error in {}: {}", test_name, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for _ in 0..config.warmup {
|
||||||
|
let mut guard = pool.spawn();
|
||||||
|
let _ = guard.vm_mut().run(&module);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut metrics = Vec::new();
|
||||||
|
for _ in 0..config.iterations {
|
||||||
|
let compile_start = Instant::now();
|
||||||
|
let mut guard = pool.spawn();
|
||||||
|
let vm = guard.vm_mut();
|
||||||
|
let compile_time = compile_start.elapsed();
|
||||||
|
|
||||||
|
let pre_session = vm.session_object_count();
|
||||||
|
let pre_epoch = vm.epoch_object_count();
|
||||||
|
|
||||||
|
let exec_start = Instant::now();
|
||||||
|
let _result = vm.run(&module);
|
||||||
|
let exec_time = exec_start.elapsed();
|
||||||
|
|
||||||
|
let gc_stats = vm.session_gc_stats();
|
||||||
|
|
||||||
|
metrics.push(MetricCollection {
|
||||||
|
test_name: test_name.clone(),
|
||||||
|
wall_time_us: (compile_time + exec_time).as_micros() as u64,
|
||||||
|
session_objects: vm.session_object_count().saturating_sub(pre_session),
|
||||||
|
session_bytes: vm.session_bytes_allocated(),
|
||||||
|
epoch_objects: vm.epoch_object_count().saturating_sub(pre_epoch),
|
||||||
|
epoch_bytes: 0,
|
||||||
|
gc_trigger_count: gc_stats.total_collections,
|
||||||
|
gc_bytes_freed: gc_stats.last_collection_bytes_freed,
|
||||||
|
gc_objects_scanned: gc_stats.last_collection_objects_scanned,
|
||||||
|
gc_collection_us: gc_stats.last_collection_duration_us,
|
||||||
|
instruction_count: vm.instruction_count(),
|
||||||
|
compile_time_us: compile_time.as_micros() as u64,
|
||||||
|
exec_time_us: exec_time.as_micros() as u64,
|
||||||
|
ic_hit_rate: vm.ic_hit_rate(),
|
||||||
|
ic_hits: vm.ic_hit_count(),
|
||||||
|
ic_misses: vm.ic_miss_count(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let avg = average_metrics(&metrics);
|
||||||
|
results.push(avg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = format_json(&results);
|
||||||
|
let table = format_text_table(&results);
|
||||||
|
eprintln!("{}", table);
|
||||||
|
println!("{}", json);
|
||||||
|
|
||||||
|
if config.update_baseline {
|
||||||
|
if let Err(e) = save_baseline(&results) {
|
||||||
|
eprintln!("Failed to save baseline: {}", e);
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
eprintln!("Baseline saved to BENCHMARK_BASELINE.md + benchmark_baseline.json");
|
||||||
|
} else {
|
||||||
|
let regressions = compare_baseline(&results);
|
||||||
|
if !regressions.is_empty() {
|
||||||
|
eprintln!("\nRegression detected:");
|
||||||
|
for r in ®ressions {
|
||||||
|
eprintln!(
|
||||||
|
" {}: {} baseline={} current={} ratio={:.2}% tolerance={:.0}%",
|
||||||
|
r.test_name,
|
||||||
|
r.metric,
|
||||||
|
r.baseline,
|
||||||
|
r.current,
|
||||||
|
(r.ratio - 1.0) * 100.0,
|
||||||
|
r.tolerance * 100.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn average_metrics(metrics: &[MetricCollection]) -> MetricCollection {
|
||||||
|
let n = metrics.len() as f64;
|
||||||
|
if n == 0.0 || metrics.is_empty() {
|
||||||
|
return metrics.first().cloned().unwrap_or(MetricCollection {
|
||||||
|
test_name: String::new(),
|
||||||
|
wall_time_us: 0,
|
||||||
|
session_objects: 0,
|
||||||
|
session_bytes: 0,
|
||||||
|
epoch_objects: 0,
|
||||||
|
epoch_bytes: 0,
|
||||||
|
gc_trigger_count: 0,
|
||||||
|
gc_bytes_freed: 0,
|
||||||
|
gc_objects_scanned: 0,
|
||||||
|
gc_collection_us: 0,
|
||||||
|
instruction_count: 0,
|
||||||
|
compile_time_us: 0,
|
||||||
|
exec_time_us: 0,
|
||||||
|
ic_hit_rate: 0.0,
|
||||||
|
ic_hits: 0,
|
||||||
|
ic_misses: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
MetricCollection {
|
||||||
|
test_name: metrics[0].test_name.clone(),
|
||||||
|
wall_time_us: (metrics.iter().map(|m| m.wall_time_us as f64).sum::<f64>() / n) as u64,
|
||||||
|
session_objects: (metrics.iter().map(|m| m.session_objects as f64).sum::<f64>() / n) as usize,
|
||||||
|
session_bytes: (metrics.iter().map(|m| m.session_bytes as f64).sum::<f64>() / n) as usize,
|
||||||
|
epoch_objects: (metrics.iter().map(|m| m.epoch_objects as f64).sum::<f64>() / n) as usize,
|
||||||
|
epoch_bytes: (metrics.iter().map(|m| m.epoch_bytes as f64).sum::<f64>() / n) as u64,
|
||||||
|
gc_trigger_count: (metrics.iter().map(|m| m.gc_trigger_count as f64).sum::<f64>() / n) as u64,
|
||||||
|
gc_bytes_freed: (metrics.iter().map(|m| m.gc_bytes_freed as f64).sum::<f64>() / n) as u64,
|
||||||
|
gc_objects_scanned: (metrics.iter().map(|m| m.gc_objects_scanned as f64).sum::<f64>() / n) as u64,
|
||||||
|
gc_collection_us: (metrics.iter().map(|m| m.gc_collection_us as f64).sum::<f64>() / n) as u64,
|
||||||
|
instruction_count: (metrics.iter().map(|m| m.instruction_count as f64).sum::<f64>() / n) as u64,
|
||||||
|
compile_time_us: (metrics.iter().map(|m| m.compile_time_us as f64).sum::<f64>() / n) as u64,
|
||||||
|
exec_time_us: (metrics.iter().map(|m| m.exec_time_us as f64).sum::<f64>() / n) as u64,
|
||||||
|
ic_hit_rate: metrics.iter().map(|m| m.ic_hit_rate).sum::<f64>() / n,
|
||||||
|
ic_hits: (metrics.iter().map(|m| m.ic_hits as f64).sum::<f64>() / n) as u64,
|
||||||
|
ic_misses: (metrics.iter().map(|m| m.ic_misses as f64).sum::<f64>() / n) as u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn override_iterations(js: &str, iters: u32) -> String {
|
||||||
|
js.lines()
|
||||||
|
.map(|line| {
|
||||||
|
if line.trim_start().starts_with("var ITERATIONS ") {
|
||||||
|
format!("var ITERATIONS = {};", iters)
|
||||||
|
} else {
|
||||||
|
line.to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use oxide_compiler::compiler::{compiled_module_hash, Compiler};
|
||||||
|
use oxide_kernel::kernel::KernelCore;
|
||||||
|
use oxide_parser::Allocator;
|
||||||
|
use oxide_vm::vm_pool::VmPool;
|
||||||
|
|
||||||
|
use crate::bench::metrics::MetricCollection;
|
||||||
|
use crate::bench::output::format_json;
|
||||||
|
use crate::bench::BenchConfig;
|
||||||
|
|
||||||
|
pub struct LeakSampler {
|
||||||
|
window: VecDeque<(usize, f64)>,
|
||||||
|
window_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LeakSampler {
|
||||||
|
pub fn new(window_size: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
window: VecDeque::new(),
|
||||||
|
window_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_sample(&mut self, iteration: usize, value: f64) -> Option<LeakVerdict> {
|
||||||
|
self.window.push_back((iteration, value));
|
||||||
|
if self.window.len() >= self.window_size {
|
||||||
|
self.window.pop_front();
|
||||||
|
}
|
||||||
|
if self.window.len() < 10 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let n = self.window.len() as f64;
|
||||||
|
let sum_x: f64 = self.window.iter().map(|(i, _)| *i as f64).sum();
|
||||||
|
let sum_y: f64 = self.window.iter().map(|(_, v)| v).sum();
|
||||||
|
let sum_xy: f64 = self.window.iter().map(|(i, v)| *i as f64 * v).sum();
|
||||||
|
let sum_x2: f64 = self.window.iter().map(|(i, _)| (*i as f64).powi(2)).sum();
|
||||||
|
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
|
||||||
|
let intercept = (sum_y - slope * sum_x) / n;
|
||||||
|
let y_mean = sum_y / n;
|
||||||
|
let ss_res: f64 = self
|
||||||
|
.window
|
||||||
|
.iter()
|
||||||
|
.map(|(i, v)| (v - (slope * *i as f64 + intercept)).powi(2))
|
||||||
|
.sum();
|
||||||
|
let ss_tot: f64 = self.window.iter().map(|(_, v)| (v - y_mean).powi(2)).sum();
|
||||||
|
let r2 = if ss_tot == 0.0 { 1.0 } else { 1.0 - ss_res / ss_tot };
|
||||||
|
if r2 > 0.9 && slope > 0.0 {
|
||||||
|
Some(LeakVerdict { slope, r2 })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LeakVerdict {
|
||||||
|
pub slope: f64,
|
||||||
|
pub r2: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_leak_detect(config: &BenchConfig, kernel: &Arc<KernelCore>, pool: &Arc<VmPool>) -> ExitCode {
|
||||||
|
let js =
|
||||||
|
"var ITERATIONS = 1000; var obj = {}; for (var i = 0; i < ITERATIONS; i++) { obj['key' + i] = i; } obj['key0']";
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let program = match oxide_parser::parse(&allocator, js) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return ExitCode::FAILURE,
|
||||||
|
};
|
||||||
|
let compiler = Compiler::new();
|
||||||
|
let hash = compiled_module_hash(&program);
|
||||||
|
let module = match kernel.code_forge().get_or_insert_with(hash, || compiler.compile(&program)) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return ExitCode::FAILURE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut samplers: Vec<(&str, LeakSampler)> = vec![
|
||||||
|
("session_objects", LeakSampler::new(20)),
|
||||||
|
("session_bytes", LeakSampler::new(20)),
|
||||||
|
("code_forge_entries", LeakSampler::new(20)),
|
||||||
|
("symbol_registry", LeakSampler::new(20)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut results: Vec<MetricCollection> = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..config.iterations as usize {
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut guard = pool.spawn();
|
||||||
|
let vm = guard.vm_mut();
|
||||||
|
let _ = vm.run(&module);
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
|
for (name, sampler) in &mut samplers {
|
||||||
|
let value: f64 = match *name {
|
||||||
|
"session_objects" => vm.session_object_count() as f64,
|
||||||
|
"session_bytes" => vm.session_bytes_allocated() as f64,
|
||||||
|
"code_forge_entries" => kernel.code_forge().len() as f64,
|
||||||
|
"symbol_registry" => vm.symbol_registry_len() as f64,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
if let Some(verdict) = sampler.add_sample(i, value) {
|
||||||
|
eprintln!(
|
||||||
|
"[LEAK] {}: slope={:.6} R²={:.4} over {} samples",
|
||||||
|
name,
|
||||||
|
verdict.slope,
|
||||||
|
verdict.r2,
|
||||||
|
sampler.window.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == config.iterations as usize - 1 {
|
||||||
|
let guard = pool.spawn();
|
||||||
|
let vm = guard.vm();
|
||||||
|
results.push(MetricCollection {
|
||||||
|
test_name: "leak_detect".to_string(),
|
||||||
|
wall_time_us: elapsed.as_micros() as u64,
|
||||||
|
session_objects: vm.session_object_count(),
|
||||||
|
session_bytes: vm.session_bytes_allocated(),
|
||||||
|
epoch_objects: vm.epoch_object_count(),
|
||||||
|
epoch_bytes: 0,
|
||||||
|
gc_trigger_count: vm.session_gc_stats().total_collections,
|
||||||
|
gc_bytes_freed: vm.session_gc_stats().last_collection_bytes_freed,
|
||||||
|
gc_objects_scanned: vm.session_gc_stats().last_collection_objects_scanned,
|
||||||
|
gc_collection_us: vm.session_gc_stats().last_collection_duration_us,
|
||||||
|
instruction_count: vm.instruction_count(),
|
||||||
|
compile_time_us: 0,
|
||||||
|
exec_time_us: 0,
|
||||||
|
ic_hit_rate: vm.ic_hit_rate(),
|
||||||
|
ic_hits: vm.ic_hit_count(),
|
||||||
|
ic_misses: vm.ic_miss_count(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = format_json(&results);
|
||||||
|
println!("{}", json);
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MetricCollection {
|
||||||
|
pub test_name: String,
|
||||||
|
pub wall_time_us: u64,
|
||||||
|
pub session_objects: usize,
|
||||||
|
pub session_bytes: usize,
|
||||||
|
pub epoch_objects: usize,
|
||||||
|
pub epoch_bytes: u64,
|
||||||
|
pub gc_trigger_count: u64,
|
||||||
|
pub gc_bytes_freed: u64,
|
||||||
|
pub gc_objects_scanned: u64,
|
||||||
|
pub gc_collection_us: u64,
|
||||||
|
pub instruction_count: u64,
|
||||||
|
pub compile_time_us: u64,
|
||||||
|
pub exec_time_us: u64,
|
||||||
|
pub ic_hit_rate: f64,
|
||||||
|
pub ic_hits: u64,
|
||||||
|
pub ic_misses: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetricCollection {
|
||||||
|
pub fn iter_metrics(&self) -> Vec<(&'static str, f64)> {
|
||||||
|
vec![
|
||||||
|
("wall_time_us", self.wall_time_us as f64),
|
||||||
|
("session_objects", self.session_objects as f64),
|
||||||
|
("session_bytes", self.session_bytes as f64),
|
||||||
|
("epoch_objects", self.epoch_objects as f64),
|
||||||
|
("epoch_bytes", self.epoch_bytes as f64),
|
||||||
|
("gc_trigger_count", self.gc_trigger_count as f64),
|
||||||
|
("gc_bytes_freed", self.gc_bytes_freed as f64),
|
||||||
|
("gc_objects_scanned", self.gc_objects_scanned as f64),
|
||||||
|
("gc_collection_us", self.gc_collection_us as f64),
|
||||||
|
("instruction_count", self.instruction_count as f64),
|
||||||
|
("compile_time_us", self.compile_time_us as f64),
|
||||||
|
("exec_time_us", self.exec_time_us as f64),
|
||||||
|
("ic_hit_rate", self.ic_hit_rate),
|
||||||
|
("ic_hits", self.ic_hits as f64),
|
||||||
|
("ic_misses", self.ic_misses as f64),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
pub mod baseline;
|
||||||
|
pub mod js_stress;
|
||||||
|
pub mod leak_detect;
|
||||||
|
pub mod metrics;
|
||||||
|
pub mod output;
|
||||||
|
pub mod rust_bench;
|
||||||
|
|
||||||
|
use std::process::ExitCode;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use oxide_kernel::kernel::KernelCore;
|
||||||
|
use oxide_vm::vm_pool::VmPool;
|
||||||
|
|
||||||
|
pub struct BenchConfig {
|
||||||
|
pub mode: String,
|
||||||
|
pub filter: Option<String>,
|
||||||
|
pub warmup: u32,
|
||||||
|
pub iterations: u32,
|
||||||
|
pub process: bool,
|
||||||
|
pub update_baseline: bool,
|
||||||
|
pub leak_check_interval: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_benchmarks(config: BenchConfig, kernel: Arc<KernelCore>, pool: Arc<VmPool>) -> ExitCode {
|
||||||
|
match config.mode.as_str() {
|
||||||
|
"js" => js_stress::run_js_stress_bench(&config, &kernel, &pool),
|
||||||
|
"rust" => rust_bench::run_rust_bench(config.filter.as_deref()),
|
||||||
|
"leak" => leak_detect::run_leak_detect(&config, &kernel, &pool),
|
||||||
|
_ => {
|
||||||
|
eprintln!("Unknown bench mode: {}. Use: js, rust, or leak", config.mode);
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
use crate::bench::metrics::MetricCollection;
|
||||||
|
|
||||||
|
pub fn format_json(results: &[MetricCollection]) -> String {
|
||||||
|
serde_json::to_string_pretty(results).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_text_table(results: &[MetricCollection]) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
out.push_str(&format!(
|
||||||
|
"{:<35} {:>10} {:>10} {:>10} {:>12} {:>8} {:>8} {:>10} {:>10}\n",
|
||||||
|
"Test Name", "Wall(ms)", "Comp(ms)", "Exec(ms)", "Instrs", "GC Ct", "IC Rate", "Sess Objs", "Sess B",
|
||||||
|
));
|
||||||
|
out.push_str(&"-".repeat(120));
|
||||||
|
out.push('\n');
|
||||||
|
|
||||||
|
for m in results {
|
||||||
|
out.push_str(&format!(
|
||||||
|
"{:<35} {:>10.2} {:>10.3} {:>10.3} {:>12} {:>8} {:>7.1}% {:>10} {:>10}",
|
||||||
|
truncate(&m.test_name, 35),
|
||||||
|
m.wall_time_us as f64 / 1000.0,
|
||||||
|
m.compile_time_us as f64 / 1000.0,
|
||||||
|
m.exec_time_us as f64 / 1000.0,
|
||||||
|
m.instruction_count,
|
||||||
|
m.gc_trigger_count,
|
||||||
|
m.ic_hit_rate * 100.0,
|
||||||
|
m.session_objects,
|
||||||
|
m.session_bytes,
|
||||||
|
));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate(s: &str, max: usize) -> &str {
|
||||||
|
if s.len() <= max {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
&s[..max]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
pub fn run_rust_bench(filter: Option<&str>) -> std::process::ExitCode {
|
||||||
|
let mut cmd = std::process::Command::new("cargo");
|
||||||
|
cmd.args(["bench", "-p", "oxide_vm"]);
|
||||||
|
if let Some(name) = filter {
|
||||||
|
cmd.args(["--bench", name]);
|
||||||
|
}
|
||||||
|
match cmd.status() {
|
||||||
|
Ok(status) => {
|
||||||
|
if status.success() {
|
||||||
|
std::process::ExitCode::SUCCESS
|
||||||
|
} else {
|
||||||
|
std::process::ExitCode::from(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to run cargo bench: {}", e);
|
||||||
|
std::process::ExitCode::from(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#![allow(clippy::arc_with_non_send_sync)]
|
#![allow(clippy::arc_with_non_send_sync)]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
@@ -15,6 +16,8 @@ use oxide_types::object::JsObject;
|
|||||||
use oxide_vm::vm_pool::VmPool;
|
use oxide_vm::vm_pool::VmPool;
|
||||||
use oxide_vm::JsValue;
|
use oxide_vm::JsValue;
|
||||||
|
|
||||||
|
mod bench;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "oxide", version, about = "OxideJS - Rust JavaScript engine")]
|
#[command(name = "oxide", version, about = "OxideJS - Rust JavaScript engine")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
@@ -42,7 +45,20 @@ enum Commands {
|
|||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
},
|
},
|
||||||
Bench {
|
Bench {
|
||||||
suite: Option<String>,
|
#[arg(default_value = "js")]
|
||||||
|
mode: Option<String>,
|
||||||
|
#[arg(short, long)]
|
||||||
|
filter: Option<String>,
|
||||||
|
#[arg(long, default_value = "2")]
|
||||||
|
warmup: u32,
|
||||||
|
#[arg(long, default_value = "10")]
|
||||||
|
iterations: u32,
|
||||||
|
#[arg(long)]
|
||||||
|
process: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
update_baseline: bool,
|
||||||
|
#[arg(long, default_value = "1000")]
|
||||||
|
leak_check_interval: usize,
|
||||||
},
|
},
|
||||||
Test {
|
Test {
|
||||||
suite: Option<String>,
|
suite: Option<String>,
|
||||||
@@ -64,7 +80,28 @@ fn main() -> ExitCode {
|
|||||||
run(&file, &kernel, &pool)
|
run(&file, &kernel, &pool)
|
||||||
}
|
}
|
||||||
Some(Commands::Compile { expr, file }) => compile(expr, file),
|
Some(Commands::Compile { expr, file }) => compile(expr, file),
|
||||||
Some(Commands::Bench { .. }) => not_implemented("bench"),
|
Some(Commands::Bench {
|
||||||
|
mode,
|
||||||
|
filter,
|
||||||
|
warmup,
|
||||||
|
iterations,
|
||||||
|
process,
|
||||||
|
update_baseline,
|
||||||
|
leak_check_interval,
|
||||||
|
}) => {
|
||||||
|
let kernel = make_kernel();
|
||||||
|
let pool = make_pool(&kernel);
|
||||||
|
let config = bench::BenchConfig {
|
||||||
|
mode: mode.unwrap_or_else(|| "js".to_string()),
|
||||||
|
filter,
|
||||||
|
warmup,
|
||||||
|
iterations,
|
||||||
|
process,
|
||||||
|
update_baseline,
|
||||||
|
leak_check_interval,
|
||||||
|
};
|
||||||
|
bench::run_benchmarks(config, kernel, pool)
|
||||||
|
}
|
||||||
Some(Commands::Test { .. }) => not_implemented("test"),
|
Some(Commands::Test { .. }) => not_implemented("test"),
|
||||||
None => repl(),
|
None => repl(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ fn run_file() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bench_not_implemented() {
|
fn bench_help() {
|
||||||
let output = Command::new("cargo")
|
let output = Command::new("cargo")
|
||||||
.args(["run", "--", "bench"])
|
.args(["run", "--", "bench", "--help"])
|
||||||
.output()
|
.output()
|
||||||
.expect("failed to run oxide bench");
|
.expect("failed to run oxide bench --help");
|
||||||
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
assert!(stderr.contains("not yet implemented"), "bench should print not-implemented message");
|
assert!(stdout.contains("--iterations"), "bench --help should list --iterations flag");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ impl CodeForge {
|
|||||||
|
|
||||||
Ok(self.insert(hash, compile()?))
|
Ok(self.insert(hash, compile()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.map.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.map.is_empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CodeForge {
|
impl Default for CodeForge {
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ struct RunConfig {
|
|||||||
filter: Option<String>,
|
filter: Option<String>,
|
||||||
no_skip: bool,
|
no_skip: bool,
|
||||||
supervise: bool,
|
supervise: bool,
|
||||||
|
leak_check: bool,
|
||||||
|
leak_check_interval: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HarnessSources {
|
struct HarnessSources {
|
||||||
@@ -284,15 +286,31 @@ fn get_harness_prefix(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RunConfig {
|
impl RunConfig {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
test262_root: None,
|
||||||
|
filter: None,
|
||||||
|
no_skip: false,
|
||||||
|
supervise: false,
|
||||||
|
leak_check: false,
|
||||||
|
leak_check_interval: 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse(args: &[String]) -> Result<Self, String> {
|
fn parse(args: &[String]) -> Result<Self, String> {
|
||||||
let mut config = Self::default();
|
let mut config = Self::new();
|
||||||
let mut positional = Vec::new();
|
let mut positional = Vec::new();
|
||||||
|
|
||||||
for arg in args.iter().skip(1) {
|
for arg in args.iter().skip(1) {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"--no-skip" => config.no_skip = true,
|
"--no-skip" => config.no_skip = true,
|
||||||
"--supervise" => config.supervise = true,
|
"--supervise" => config.supervise = true,
|
||||||
|
"--leak-check" => config.leak_check = true,
|
||||||
"--help" | "-h" => return Err(Self::usage()),
|
"--help" | "-h" => return Err(Self::usage()),
|
||||||
|
_ if arg.starts_with("--leak-check-interval=") => {
|
||||||
|
config.leak_check_interval =
|
||||||
|
arg.strip_prefix("--leak-check-interval=").unwrap().parse().unwrap_or(1000);
|
||||||
|
}
|
||||||
_ if arg.starts_with("--") => return Err(format!("unknown option: {arg}\n\n{}", Self::usage())),
|
_ if arg.starts_with("--") => return Err(format!("unknown option: {arg}\n\n{}", Self::usage())),
|
||||||
_ => positional.push(arg.clone()),
|
_ => positional.push(arg.clone()),
|
||||||
}
|
}
|
||||||
@@ -312,11 +330,13 @@ impl RunConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
"usage: test262-runner [--no-skip] [--supervise] [test262-root] [path-filter]\n\
|
"usage: test262-runner [--no-skip] [--supervise] [--leak-check] [--leak-check-interval=N] [test262-root] [path-filter]\n\
|
||||||
\n\
|
\n\
|
||||||
--no-skip Run capability-excluded tests and count unsupported compile/runtime results as failures.\n\
|
--no-skip Run capability-excluded tests and count unsupported compile/runtime results as failures.\n\
|
||||||
--supervise Run the suite as single-worker child-process windows with a hard per-test timeout and\n\
|
--supervise Run the suite as single-worker child-process windows with a hard per-test timeout and\n\
|
||||||
\x20 automatic resume past any hanging/crashing test. A hang or crash is reported by path.\n\
|
\x20 automatic resume past any hanging/crashing test. A hang or crash is reported by path.\n\
|
||||||
|
--leak-check Monitor session_object_ptrs, session_bytes, code_forge.len(), symbol_registry.len() every\n\
|
||||||
|
\x20 --leak-check-interval tests (default 1000). Flags sustained linear growth (R^2>0.9).\n\
|
||||||
\n\
|
\n\
|
||||||
supervised-mode env tunables:\n\
|
supervised-mode env tunables:\n\
|
||||||
\x20 OXIDE_TEST262_TIMEOUT_SECS per-test wall-clock timeout (default 10)\n\
|
\x20 OXIDE_TEST262_TIMEOUT_SECS per-test wall-clock timeout (default 10)\n\
|
||||||
|
|||||||
@@ -26,3 +26,24 @@ tracing.workspace = true
|
|||||||
oxide_compiler = { path = "../oxide_compiler" }
|
oxide_compiler = { path = "../oxide_compiler" }
|
||||||
proptest.workspace = true
|
proptest.workspace = true
|
||||||
oxide_parser = { path = "../oxide_parser" }
|
oxide_parser = { path = "../oxide_parser" }
|
||||||
|
criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "dispatch"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "property"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "coercion"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "gc_mark_sweep"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "call"
|
||||||
|
harness = false
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn bench_call(c: &mut Criterion) {
|
||||||
|
let kernel = KernelCore::new(KernelConfig::minimal());
|
||||||
|
let js = "function fib(n) { if (n < 2) return n; return fib(n-1) + fib(n-2); } fib(15);";
|
||||||
|
let alloc = oxide_parser::Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&alloc, js).expect("parse");
|
||||||
|
let module = Compiler.compile(&program).expect("compile");
|
||||||
|
|
||||||
|
c.bench_function("call", |b| {
|
||||||
|
b.iter_batched(
|
||||||
|
|| Vm::with_kernel_core(Arc::clone(&kernel)),
|
||||||
|
|mut vm| {
|
||||||
|
vm.run(&module).ok();
|
||||||
|
black_box(vm)
|
||||||
|
},
|
||||||
|
criterion::BatchSize::SmallInput,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_call);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn bench_coercion(c: &mut Criterion) {
|
||||||
|
let kernel = KernelCore::new(KernelConfig::minimal());
|
||||||
|
let js = "var x = \"42\"; var y = \"3.14\"; var r = 0; for (var i = 0; i < 5000; i++) { r += (+x) + (+y); } r";
|
||||||
|
let alloc = oxide_parser::Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&alloc, js).expect("parse");
|
||||||
|
let module = Compiler.compile(&program).expect("compile");
|
||||||
|
|
||||||
|
c.bench_function("coercion", |b| {
|
||||||
|
b.iter_batched(
|
||||||
|
|| Vm::with_kernel_core(Arc::clone(&kernel)),
|
||||||
|
|mut vm| {
|
||||||
|
vm.run(&module).ok();
|
||||||
|
black_box(vm)
|
||||||
|
},
|
||||||
|
criterion::BatchSize::SmallInput,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_coercion);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn bench_dispatch(c: &mut Criterion) {
|
||||||
|
let kernel = KernelCore::new(KernelConfig::minimal());
|
||||||
|
let js = "var x = 0; for (var i = 0; i < 10000; i++) { x = x + i - i * 2 + (i % 3); } x";
|
||||||
|
let alloc = oxide_parser::Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&alloc, js).expect("parse");
|
||||||
|
let module = Compiler.compile(&program).expect("compile");
|
||||||
|
|
||||||
|
c.bench_function("dispatch", |b| {
|
||||||
|
b.iter_batched(
|
||||||
|
|| Vm::with_kernel_core(Arc::clone(&kernel)),
|
||||||
|
|mut vm| {
|
||||||
|
vm.run(&module).ok();
|
||||||
|
black_box(vm)
|
||||||
|
},
|
||||||
|
criterion::BatchSize::SmallInput,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_dispatch);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn bench_gc(c: &mut Criterion) {
|
||||||
|
let mut config = KernelConfig::minimal();
|
||||||
|
config.session_gc_threshold = 1024 * 1024;
|
||||||
|
let kernel = KernelCore::new(config);
|
||||||
|
let js = "var g = {}; for (var i = 0; i < 500; i++) { var obj = { x: i, y: i * 2 }; g[i] = obj; } g[\"x\"]";
|
||||||
|
let alloc = oxide_parser::Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&alloc, js).expect("parse");
|
||||||
|
let module = Compiler.compile(&program).expect("compile");
|
||||||
|
|
||||||
|
c.bench_function("gc_mark_sweep", |b| {
|
||||||
|
b.iter_batched(
|
||||||
|
|| Vm::with_kernel_core(Arc::clone(&kernel)),
|
||||||
|
|mut vm| {
|
||||||
|
vm.run(&module).ok();
|
||||||
|
black_box(vm)
|
||||||
|
},
|
||||||
|
criterion::BatchSize::SmallInput,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_gc);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn bench_property(c: &mut Criterion) {
|
||||||
|
let kernel = KernelCore::new(KernelConfig::minimal());
|
||||||
|
let js = "var obj = { a: 1, b: 2, c: 3, d: 4, e: 5 }; var sum = 0; for (var i = 0; i < 10000; i++) { sum += obj.a + obj.b + obj.c + obj.d + obj.e; } sum";
|
||||||
|
let alloc = oxide_parser::Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&alloc, js).expect("parse");
|
||||||
|
let module = Compiler.compile(&program).expect("compile");
|
||||||
|
|
||||||
|
c.bench_function("property", |b| {
|
||||||
|
b.iter_batched(
|
||||||
|
|| Vm::with_kernel_core(Arc::clone(&kernel)),
|
||||||
|
|mut vm| {
|
||||||
|
vm.run(&module).ok();
|
||||||
|
black_box(vm)
|
||||||
|
},
|
||||||
|
criterion::BatchSize::SmallInput,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_property);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -203,8 +203,10 @@ impl Vm {
|
|||||||
|
|
||||||
if let Some(value) = ic_get_hit(obj, cached_shape_id, cached_slot) {
|
if let Some(value) = ic_get_hit(obj, cached_shape_id, cached_slot) {
|
||||||
self.regs[a] = value;
|
self.regs[a] = value;
|
||||||
|
self.ic_hits.set(self.ic_hits.get() + 1);
|
||||||
ic_trace!("IC_GET hit shape={} slot={}", cached_shape_id, cached_slot);
|
ic_trace!("IC_GET hit shape={} slot={}", cached_shape_id, cached_slot);
|
||||||
} else if let Some(template) = self.kernel_core.prop_forge().get_template(obj.shape_id()) {
|
} else if let Some(template) = self.kernel_core.prop_forge().get_template(obj.shape_id()) {
|
||||||
|
self.ic_misses.set(self.ic_misses.get() + 1);
|
||||||
prop_cache_miss();
|
prop_cache_miss();
|
||||||
if template.prop_name == prop_name_si {
|
if template.prop_name == prop_name_si {
|
||||||
if template.position < obj.prop_vec_len() as u32 {
|
if template.position < obj.prop_vec_len() as u32 {
|
||||||
@@ -271,13 +273,16 @@ impl Vm {
|
|||||||
let cached_slot = ext1;
|
let cached_slot = ext1;
|
||||||
|
|
||||||
if ic_set_hit(obj, cached_shape_id, cached_slot, value) {
|
if ic_set_hit(obj, cached_shape_id, cached_slot, value) {
|
||||||
|
self.ic_hits.set(self.ic_hits.get() + 1);
|
||||||
ic_trace!("IC_SET hit shape={} slot={}", cached_shape_id, cached_slot);
|
ic_trace!("IC_SET hit shape={} slot={}", cached_shape_id, cached_slot);
|
||||||
} else if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
|
} else if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
|
||||||
|
self.ic_misses.set(self.ic_misses.get() + 1);
|
||||||
prop_cache_miss();
|
prop_cache_miss();
|
||||||
obj.set_prop_at(pos, value);
|
obj.set_prop_at(pos, value);
|
||||||
self.write_ic_back(obj.shape_id(), pos);
|
self.write_ic_back(obj.shape_id(), pos);
|
||||||
ic_debug!("IC_SET write-back shape={} slot={}", obj.shape_id(), pos);
|
ic_debug!("IC_SET write-back shape={} slot={}", obj.shape_id(), pos);
|
||||||
} else {
|
} else {
|
||||||
|
self.ic_misses.set(self.ic_misses.get() + 1);
|
||||||
prop_cache_miss();
|
prop_cache_miss();
|
||||||
let old_shape = obj.shape_id();
|
let old_shape = obj.shape_id();
|
||||||
self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?;
|
self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?;
|
||||||
|
|||||||
@@ -180,6 +180,9 @@ pub struct Vm {
|
|||||||
/// The dispatch loop checks this flag and skips writing `regs[target_reg]` from the
|
/// The dispatch loop checks this flag and skips writing `regs[target_reg]` from the
|
||||||
/// call result — the value will be delivered by the RETURN handler instead.
|
/// call result — the value will be delivered by the RETURN handler instead.
|
||||||
pub(crate) accessor_frame_target_reg: Option<u8>,
|
pub(crate) accessor_frame_target_reg: Option<u8>,
|
||||||
|
pub(crate) ic_hits: std::cell::Cell<u64>,
|
||||||
|
pub(crate) ic_misses: std::cell::Cell<u64>,
|
||||||
|
pub(crate) instruction_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Vm {
|
impl Vm {
|
||||||
@@ -328,6 +331,43 @@ impl Vm {
|
|||||||
&self.session_gc
|
&self.session_gc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn session_object_count(&self) -> usize {
|
||||||
|
self.session_object_ptrs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_bytes_allocated(&self) -> usize {
|
||||||
|
self.session_bytes_allocated
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn epoch_object_count(&self) -> usize {
|
||||||
|
self.epoch_object_ptrs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ic_hit_rate(&self) -> f64 {
|
||||||
|
let total = self.ic_hits.get() + self.ic_misses.get();
|
||||||
|
if total == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
self.ic_hits.get() as f64 / total as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn instruction_count(&self) -> u64 {
|
||||||
|
self.instruction_count
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symbol_registry_len(&self) -> usize {
|
||||||
|
self.symbol_registry.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ic_hit_count(&self) -> u64 {
|
||||||
|
self.ic_hits.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ic_miss_count(&self) -> u64 {
|
||||||
|
self.ic_misses.get()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn checked_object_ptr(
|
pub(crate) fn checked_object_ptr(
|
||||||
&mut self, val: JsValue, error_msg: &str,
|
&mut self, val: JsValue, error_msg: &str,
|
||||||
) -> Result<Option<*mut JsObject>, String> {
|
) -> Result<Option<*mut JsObject>, String> {
|
||||||
@@ -649,10 +689,12 @@ impl Vm {
|
|||||||
steps += 1;
|
steps += 1;
|
||||||
if let Some(max_steps) = self.kernel_core.config.max_steps {
|
if let Some(max_steps) = self.kernel_core.config.max_steps {
|
||||||
if steps > max_steps {
|
if steps > max_steps {
|
||||||
|
self.instruction_count = steps;
|
||||||
return Err(format!("VM step limit exceeded at pc={}", self.pc));
|
return Err(format!("VM step limit exceeded at pc={}", self.pc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.pc >= self.bytecode.len() {
|
if self.pc >= self.bytecode.len() {
|
||||||
|
self.instruction_count = steps;
|
||||||
return Err("program counter out of bounds".into());
|
return Err("program counter out of bounds".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,7 +708,10 @@ impl Vm {
|
|||||||
match op {
|
match op {
|
||||||
OpCode::NOP => {}
|
OpCode::NOP => {}
|
||||||
|
|
||||||
OpCode::HALT => return Ok(self.regs[0]),
|
OpCode::HALT => {
|
||||||
|
self.instruction_count = steps;
|
||||||
|
return Ok(self.regs[0]);
|
||||||
|
}
|
||||||
|
|
||||||
OpCode::LOAD_CONST => {
|
OpCode::LOAD_CONST => {
|
||||||
self.dispatch_load_const(rd, instr)?;
|
self.dispatch_load_const(rd, instr)?;
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ impl Vm {
|
|||||||
active_reg_limit: 0,
|
active_reg_limit: 0,
|
||||||
native_call_depth: 0,
|
native_call_depth: 0,
|
||||||
accessor_frame_target_reg: None,
|
accessor_frame_target_reg: None,
|
||||||
|
ic_hits: std::cell::Cell::new(0),
|
||||||
|
ic_misses: std::cell::Cell::new(0),
|
||||||
|
instruction_count: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +99,9 @@ impl Vm {
|
|||||||
active_reg_limit: 0,
|
active_reg_limit: 0,
|
||||||
native_call_depth: 0,
|
native_call_depth: 0,
|
||||||
accessor_frame_target_reg: None,
|
accessor_frame_target_reg: None,
|
||||||
|
ic_hits: std::cell::Cell::new(0),
|
||||||
|
ic_misses: std::cell::Cell::new(0),
|
||||||
|
instruction_count: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
var ITERATIONS = 50000;
|
||||||
|
|
||||||
|
var arr = [];
|
||||||
|
for (var i = 0; i < ITERATIONS; i++) {
|
||||||
|
arr.push(i);
|
||||||
|
}
|
||||||
|
var sum = 0;
|
||||||
|
while (arr.length > 0) {
|
||||||
|
sum += arr.pop();
|
||||||
|
}
|
||||||
|
sum
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
var ITERATIONS = 200;
|
||||||
|
|
||||||
|
function sumTo(n) {
|
||||||
|
if (n <= 0) return 0;
|
||||||
|
return n + sumTo(n - 1);
|
||||||
|
}
|
||||||
|
sumTo(ITERATIONS)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
var ITERATIONS = 5000;
|
||||||
|
|
||||||
|
for (var i = 0; i < ITERATIONS; i++) {
|
||||||
|
eval("var x" + i + " = " + i + "; x" + i + " + 1");
|
||||||
|
}
|
||||||
|
1
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
var ITERATIONS = 50000;
|
||||||
|
|
||||||
|
var sum = 0;
|
||||||
|
for (var i = 0; i < ITERATIONS; i++) {
|
||||||
|
sum += (+"42") + (+"3.14") + (+"0");
|
||||||
|
}
|
||||||
|
sum
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
var ITERATIONS = 100000;
|
||||||
|
|
||||||
|
var sum = 0;
|
||||||
|
for (var i = 0; i < ITERATIONS; i++) {
|
||||||
|
var obj = { x: i, y: i * 2, nested: { a: 1, b: 2 } };
|
||||||
|
sum += obj.x + obj.y;
|
||||||
|
}
|
||||||
|
sum
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
var ITERATIONS = 10000;
|
||||||
|
|
||||||
|
var objs = [];
|
||||||
|
for (var i = 0; i < ITERATIONS; i++) {
|
||||||
|
objs.push({ x: i });
|
||||||
|
if (i % 3 === 0) objs[i].y = i;
|
||||||
|
if (i % 5 === 0) objs[i].z = i;
|
||||||
|
}
|
||||||
|
var sum = 0;
|
||||||
|
for (var i = 0; i < ITERATIONS; i++) {
|
||||||
|
sum += objs[i].x;
|
||||||
|
}
|
||||||
|
sum
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
var ITERATIONS = 10000;
|
||||||
|
|
||||||
|
var obj = {};
|
||||||
|
for (var i = 0; i < ITERATIONS; i++) {
|
||||||
|
obj["key" + i] = i;
|
||||||
|
}
|
||||||
|
var sum = 0;
|
||||||
|
for (var i = 0; i < ITERATIONS; i++) {
|
||||||
|
sum += obj["key" + i];
|
||||||
|
}
|
||||||
|
sum
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
var ITERATIONS = 10000;
|
||||||
|
|
||||||
|
var s = "";
|
||||||
|
for (var i = 0; i < ITERATIONS; i++) {
|
||||||
|
s += "a" + i;
|
||||||
|
}
|
||||||
|
s.length
|
||||||
Reference in New Issue
Block a user