Files
OxideJS-New/crates/oxide_vm/tests/member_write_ic_tests.rs
T
pjh123 aa4c28cdf0 perf: member 复合写路径接入 IC 写命中(depth 0)消除每次 DashMap 查询与回写
set_member_prop 增加 ic_set_hit_own(仅接受 depth==0 槽,跳过读侧学习的
原型条目防原型污染)命中直写槽,ext_pc 由 read_member_prop 显式带出消除
隐式 pc 契约;写新属性 miss 分支(IC_SET 分发 + member 写)统一改
CreateDataProperty 快路径(保留原型链 setter/readonly 检查),砍掉
ordinary_set 全链路的重复查询与 shape 转换开销。
2026-08-15 08:16:49 +08:00

189 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! member 复合写(obj.x += 1 / obj.x++ 等)接入 IC 写命中的行为测试。
//!
//! 覆盖三组语义:①写侧命中(depth==0 槽直写,读写对称);②写新属性走
//! CreateDataProperty 快路径(shape 转换,命中无法覆盖);③继承属性 shadow /
//! 原型 setter / 只读继承属性(写侧 depth==0 限制与原型链检查兜底,防原型污染)。
//!
//! 计数断言基于单次 run 内 miss/hit 对比(run() 每次冷启动,IC 学习不跨 run)。
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
/// 编译并单次运行,返回 (结果, hits, misses)。
fn run_once(source: &str) -> (String, u64, u64) {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse");
let module = Compiler::new().compile(&program).expect("compile");
let mut vm = Vm::new();
let result = vm.run(&module).expect("run");
// 字符串结果经 lookup_str 提取内容(JsValue Display 只打印 {string})。
let r = vm.lookup_str(result).unwrap_or_else(|| format!("{result}"));
(r, vm.ic_hit_count(), vm.ic_miss_count())
}
/// 单态 member 复合写:写侧命中直写(读侧命中不计数,pre-existing 口径——
/// 计数仅来自写侧与独立 IC_GET/IC_SET 分发站点)。
#[test]
fn member_compound_monomorphic_write_hits() {
let (r, hits, misses) = run_once(
r#"var t = { x: 0 };
for (var i = 0; i < 10000; i++) { t.x += 1; }
t.x"#,
);
assert_eq!(r, "10000");
assert_eq!(misses, 1, "仅末尾 `t.x` IC_GET 站点学习 miss,循环写侧零 miss");
assert_eq!(hits, 10000, "循环 10000 次写侧全命中(实际 {hits})");
}
/// 4 shape 轮换 member 复合写:写侧复用读侧学习的 4 槽条目,循环内零写 miss。
#[test]
fn member_compound_poly4_read_write_hits() {
let (r, hits, misses) = run_once(
r#"var objs = [{x:0}, {x:0,y:1}, {x:0,z:1}, {x:0,y:1,z:2}];
for (var i = 0; i < 10000; i++) {
var t = objs[i % 4];
t.x += 1;
}
objs[0].x + objs[1].x + objs[2].x + objs[3].x"#,
);
assert_eq!(r, "10000", "四对象 x 各增 2500 次");
assert_eq!(misses, 4, "末尾 4 个 `objs[i].x` IC_GET 站点各学习 1 次");
assert_eq!(hits, 10000, "循环写侧 10000 全命中(实际 {hits})");
}
/// 继承属性 shadow(核心正确性):写侧只接受 depth==0 槽,原型不被污染。
#[test]
fn inherited_prop_shadows_receiver_not_proto() {
let (r, _hits, _misses) = run_once(
r#"var p = { x: 1 };
var o = Object.create(p);
o.x += 1;
o.x + ":" + p.x"#,
);
assert_eq!(r, "2:1", "o 获得 own x=2,p.x 保持 1(未直写原型)");
}
/// IC_SET 独立赋值路径的继承 shadow:`Object.create(p).x = 5` 同样不写原型。
#[test]
fn ic_set_inherited_shadow_receiver_only() {
let (r, _hits, _misses) = run_once(
r#"var p = { x: 1 };
var o = Object.create(p);
o.x = 5;
p.x"#,
);
assert_eq!(r, "1", "p.x 保持 1,o 得到 own x=5");
}
/// 原型 accessor setter:IC_SET 与 member 复合写都触发 setter,接收者不建新槽。
#[test]
fn proto_setter_called_both_write_paths() {
let (r, _hits, _misses) = run_once(
r#"var calls = 0;
var p = { set x(v) { calls++; } };
var o = Object.create(p);
o.x = 5;
o.x += 2;
calls + ":" + Object.keys(o).length"#,
);
assert_eq!(r, "2:0", "setter 被调 2 次(IC_SET 与 member 各 1),o 无 own 属性");
}
/// 原型只读 data:两条写路径都按 ordinary_set 抛 TypeError。
#[test]
fn readonly_inherited_data_raises() {
let (r, _hits, _misses) = run_once(
r#"var p = {};
Object.defineProperty(p, 'x', { value: 1, writable: false });
var o = Object.create(p);
var msg = '';
try { o.x = 5; } catch (e) { msg = e.message; }
try { o.x += 1; } catch (e) { msg += '|' + e.message; }
msg"#,
);
assert_eq!(
r, "cannot assign to read-only property|cannot assign to read-only property",
"两条路径都拒绝写只读继承属性"
);
}
/// 写新属性(shape 转换):快路径建 shape + 写回,循环内同 shape 写全命中。
#[test]
fn write_new_prop_then_hit() {
let (r, hits, misses) = run_once(
r#"var o = {};
o.y = 0;
for (var i = 0; i < 10000; i++) { o.y += 1; }
o.y"#,
);
assert_eq!(r, "10000");
assert_eq!(misses, 2, "`o.y = 0` 站点 1 次 + 末尾 `o.y` IC_GET 站点 1 次");
assert_eq!(hits, 10000, "compound 写侧 10000 全命中(实际 {hits})");
}
/// ic_cache.js 同款模式(批量建对象 + 分批写 y/z 新属性):快路径建 shape 正确。
#[test]
fn ic_cache_style_new_prop_writes() {
let (r, _hits, _misses) = run_once(
r#"var objs = [];
for (var i = 0; i < 100; i++) {
objs.push({ x: i });
if (i % 3 === 0) objs[i].y = i;
if (i % 5 === 0) objs[i].z = i;
}
var yok = 0, zok = 0;
for (var i = 0; i < 100; i++) {
if (i % 3 === 0 && objs[i].y === i) yok++;
if (i % 5 === 0 && objs[i].z === i) zok++;
}
yok + ":" + zok"#,
);
assert_eq!(r, "34:20", "y/z 全部写入成功(含 i%15==0 的 y+z 同对象场景)");
}
/// has_prop_meta 对象 member 写走 ordinary_set(不缓存,accessor 语义保留)。
#[test]
fn meta_object_member_write_uses_ordinary_set() {
let (r, _hits, _misses) = run_once(
r#"var store = 1;
var o = { get a() { return store; }, set a(v) { store = v; } };
o.a += 5;
store"#,
);
assert_eq!(r, "6", "accessor 对象走 setter,store 更新为 6");
}
/// 命中率口径:member 写 hit/miss 计入后 ic_hit_rate 反映写侧全量命中。
#[test]
fn member_write_counts_in_ic_hit_rate() {
let allocator = Allocator::default();
let program = oxide_parser::parse(
&allocator,
r#"var t = { x: 0 };
for (var i = 0; i < 1000; i++) { t.x += 1; }
t.x"#,
)
.expect("parse");
let module = Compiler::new().compile(&program).expect("compile");
let mut vm = Vm::new();
let result = vm.run(&module).expect("run");
let r = vm.lookup_str(result).unwrap_or_else(|| format!("{result}"));
assert_eq!(r, "1000");
// 循环写侧 1000 命中 + 末尾 IC_GET 1 miss → 命中率 ~0.999(写侧计入后趋近真实)。
assert!(vm.ic_hit_rate() > 0.99, "member 写侧命中计入命中率(实际 {:.4})", vm.ic_hit_rate());
}
/// `__proto__` 赋值 + 继承成员复合写:原型链建立正常,shadow 语义不变(回归锚定)。
#[test]
fn proto_assignment_then_compound_shadow() {
let (r, _hits, _misses) = run_once(
r#"var p = { x: 1 };
var o = {};
o.__proto__ = p;
o.x += 1;
o.x + ":" + p.x"#,
);
assert_eq!(r, "2:1", "__proto__ 赋值建链后复合写 shadow 到 o,p 不被污染");
}