mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
perf: ASCII 单字符缓存 + 字符串迭代器游标收尾
This commit is contained in:
@@ -7,8 +7,6 @@ use oxide_runtime_api::{to_object, NativeResult, VmHost};
|
||||
|
||||
const INNER_PROP: &str = "__inner__";
|
||||
const INDEX_PROP: &str = "__index__";
|
||||
/// 字符串迭代的字节游标(`next` 增量推进的当前位置),避免每步整串复制 + 从头重扫。
|
||||
const BYTEOFF_PROP: &str = "__byteoff__";
|
||||
|
||||
/// 占位构造函数:`Iterator` 不是构造函数,任何调用都抛 TypeError。
|
||||
pub fn iterator_constructor<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
|
||||
@@ -305,16 +303,19 @@ fn next_array_like<H: VmHost>(
|
||||
}
|
||||
|
||||
if inner.is_string() {
|
||||
let index = current_index(vm, wrapper, index_si);
|
||||
let byteoff_si = vm.kernel_core().perm_interner().intern(BYTEOFF_PROP).0;
|
||||
let byteoff = current_index(vm, wrapper, byteoff_si);
|
||||
// index 槽在字符串分支存字节游标(每步推进一个 char 的 UTF-8 宽度)而非
|
||||
// 元素序号:该槽只被 next 内部读写、无外部消费者,包装器创建后类型固定
|
||||
// 不跨类型复用,故可安全借用语义(字符串=字节偏移)。
|
||||
let byteoff = current_index(vm, wrapper, index_si);
|
||||
// 源串裸指针借用压缩到单个表达式:ch 是 Copy 的 char,不携带借用,
|
||||
// 之后对 VM 状态的可变访问不再与源串借用共存。
|
||||
let ch = unsafe { &*inner.as_string_ptr() }.as_str()[byteoff..].chars().next();
|
||||
if let Some(ch) = ch {
|
||||
vm.set_or_create_prop_value(wrapper, byteoff_si, JsValue::int((byteoff + ch.len_utf8()) as i32));
|
||||
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int((index + 1) as i32));
|
||||
let value = vm.new_string(&ch.to_string());
|
||||
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int((byteoff + ch.len_utf8()) as i32));
|
||||
let value = match vm.single_char(ch) {
|
||||
Some(v) => v,
|
||||
None => vm.new_string(&ch.to_string()),
|
||||
};
|
||||
return Ok(Some(make_iter_result(vm, value, false)));
|
||||
}
|
||||
return Ok(Some(make_iter_result(vm, JsValue::undefined(), true)));
|
||||
|
||||
@@ -201,6 +201,23 @@ pub(crate) fn make_string_array<H: VmHost>(vm: &mut H, parts: Vec<String>) -> Js
|
||||
JsValue::from_js_object(arr)
|
||||
}
|
||||
|
||||
/// 以已构造的字符串值构建字符串数组(元素零拷贝落地),供逐字符产出路径
|
||||
/// (空分隔 split)复用,跳过 `Vec<String>` 中间层。
|
||||
pub(crate) fn make_string_array_values<H: VmHost>(vm: &mut H, parts: Vec<JsValue>) -> JsValue {
|
||||
let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||||
let n = parts.len();
|
||||
let arr =
|
||||
vm.epoch()
|
||||
.alloc(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(proto), n, vm.epoch().bump()));
|
||||
unsafe {
|
||||
for (i, sv) in parts.into_iter().enumerate() {
|
||||
(*arr).set_prop_at(i, sv);
|
||||
}
|
||||
(*arr).set_prop_count(n);
|
||||
}
|
||||
JsValue::from_js_object(arr)
|
||||
}
|
||||
|
||||
/// 取参数字符串内容:原始字符串零拷贝借用,其余经完整 ToString(对象 ToPrimitive)。
|
||||
/// 返回借用绑定本次 `&mut` 借用;需与 `this_string` 借用并存时先 `into_owned` 落地。
|
||||
fn as_string<'a, H: VmHost>(vm: &'a mut H, val: JsValue) -> Cow<'a, str> {
|
||||
@@ -500,14 +517,24 @@ pub fn string_char_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
if s.is_empty() {
|
||||
return NativeResult::Ok(vm.new_string(""));
|
||||
}
|
||||
let first = take_chars(&s, 1);
|
||||
return NativeResult::Ok(vm.new_string(&first));
|
||||
let first = s.chars().next().unwrap();
|
||||
// ASCII 走单字符缓存零分配,其余回落普通创建(借用已随 char 提取结束)。
|
||||
return match vm.single_char(first) {
|
||||
Some(v) => NativeResult::Ok(v),
|
||||
None => NativeResult::Ok(vm.new_string(&first.to_string())),
|
||||
};
|
||||
}
|
||||
if idx < 0 || idx as usize >= char_len(&s) {
|
||||
return NativeResult::Ok(vm.new_string(""));
|
||||
}
|
||||
let ch = s.chars().nth(idx as usize).map(|c| c.to_string()).unwrap_or_default();
|
||||
NativeResult::Ok(vm.new_string(&ch))
|
||||
let ch = s.chars().nth(idx as usize);
|
||||
match ch {
|
||||
Some(c) => match vm.single_char(c) {
|
||||
Some(v) => NativeResult::Ok(v),
|
||||
None => NativeResult::Ok(vm.new_string(&c.to_string())),
|
||||
},
|
||||
None => NativeResult::Ok(vm.new_string("")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `String.prototype.charCodeAt(index)`:返回指定位置字符的 UTF-16 code unit;越界返回 NaN。
|
||||
@@ -950,11 +977,20 @@ pub fn string_split<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
// 无原生正则的类正则对象回退到字符串路径。
|
||||
}
|
||||
let parts: Vec<String> = if sep.is_empty() {
|
||||
s.chars().map(|c| c.to_string()).take(limit).collect()
|
||||
} else {
|
||||
s.split(&sep).map(|p| p.to_string()).take(limit).collect()
|
||||
};
|
||||
if sep.is_empty() {
|
||||
// 两段式:先借 this 收集字符(Copy),借用结束后再逐字符产出——
|
||||
// ASCII 走单字符缓存零分配,其余回落普通创建。
|
||||
let chars: Vec<char> = s.chars().take(limit).collect();
|
||||
let mut values = Vec::with_capacity(chars.len());
|
||||
for c in chars {
|
||||
values.push(match vm.single_char(c) {
|
||||
Some(v) => v,
|
||||
None => vm.new_string(&c.to_string()),
|
||||
});
|
||||
}
|
||||
return NativeResult::Ok(make_string_array_values(vm, values));
|
||||
}
|
||||
let parts: Vec<String> = s.split(&sep).map(|p| p.to_string()).take(limit).collect();
|
||||
NativeResult::Ok(make_string_array(vm, parts))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::RwLock;
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use oxide_types::object::JsString;
|
||||
@@ -142,6 +142,39 @@ impl Default for PermInterner {
|
||||
}
|
||||
}
|
||||
|
||||
/// 指向永久 `JsString` 的指针包装,供静态表存储。`JsString` 内容线程安全
|
||||
/// (`String` + `AtomicU32`)且永久存活、只读共享,裸指针跨线程传递安全。
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(transparent)]
|
||||
struct StringPtr(*const JsString);
|
||||
unsafe impl Send for StringPtr {}
|
||||
unsafe impl Sync for StringPtr {}
|
||||
|
||||
/// ASCII 单字符永久 `JsString` 指针表:以字节值(0..=127)为下标,
|
||||
/// 首用惰性物化 `Box::into_raw` 永久泄漏,之后恒复用同一地址。
|
||||
/// 泄漏面严格有界:≤128 条目 × 单字符内容(≈8KB),进程生命周期内不释放。
|
||||
static SINGLE_CHAR_TABLE: [OnceLock<StringPtr>; 128] = [const { OnceLock::new() }; 128];
|
||||
|
||||
/// 取 ASCII 单字符的永久 `JsString` 指针,惰性物化一次后恒返回同一地址。
|
||||
/// 供迭代器/charAt/空分隔 split/字符串展开等高频字符产出路径零分配复用;
|
||||
/// 并发首用竞态下仍保证全局唯一指针(后者回收本线程副本)。
|
||||
pub fn single_char_ptr(ch: u8) -> *const JsString {
|
||||
let slot = &SINGLE_CHAR_TABLE[ch as usize];
|
||||
if let Some(ptr) = slot.get() {
|
||||
return ptr.0;
|
||||
}
|
||||
let ptr = Box::into_raw(Box::new(JsString::new((ch as char).to_string())));
|
||||
match slot.set(StringPtr(ptr)) {
|
||||
Ok(()) => ptr,
|
||||
Err(existing) => {
|
||||
// 并发首用竞态:另一线程已物化,本线程指针尚未暴露给调用方,恰好释放一次。
|
||||
// SAFETY: ptr 来自本线程的 Box::into_raw,无任何外部引用。
|
||||
unsafe { drop(Box::from_raw(ptr)) };
|
||||
existing.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -206,4 +239,21 @@ mod tests {
|
||||
// 二次调用返回同一稳定指针(仅物化一次)。
|
||||
assert_eq!(interner.string_ptr(id), ptr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_char_ptr_idempotent() {
|
||||
let a = single_char_ptr(b'a');
|
||||
assert_eq!(unsafe { (*a).as_str() }, "a");
|
||||
// 二次调用返回同一稳定指针(仅物化一次)。
|
||||
assert_eq!(single_char_ptr(b'a'), a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_char_table_full_ascii() {
|
||||
// 全表 128 条目均可物化且内容为对应的单字符文本。
|
||||
for b in 0u8..=127 {
|
||||
let ptr = single_char_ptr(b);
|
||||
assert_eq!(unsafe { (*ptr).as_str() }, (b as char).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,19 @@ pub trait VmHost {
|
||||
fn new_string(&mut self, s: &str) -> JsValue;
|
||||
/// move 接收 `String` 创建会话字符串,避免一次整串克隆。
|
||||
fn new_string_owned(&mut self, s: String) -> JsValue;
|
||||
/// 取 ASCII 单字符的永久字符串值:命中返回共享 perm 串(零分配、可指针
|
||||
/// 短路比较),非 ASCII 返回 `None` 由调用方回落普通字符串创建。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// `&self` 可借用期调用;`None` 回落 `new_string` 是 `&mut` 路径,须先结束
|
||||
/// 本次 `&self` 借用(返回值即时消费即可)。
|
||||
fn single_char(&self, ch: char) -> Option<JsValue> {
|
||||
if ch.is_ascii() {
|
||||
Some(JsValue::string(oxide_kernel::string_forge::single_char_ptr(ch as u8)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// 借出字符串值的文本内容,生命周期绑定到 `&self` 借用。
|
||||
///
|
||||
/// perm 字符串由内核持有、永不释放;session 字符串只在 `&mut self` 路径
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use crate::native::NativeFn;
|
||||
use crate::vm::{native_fn_ptr_to_fn, CallFrame, ForInIter, FrameContinuation, Vm, MAX_PROTO_CHAIN_DEPTH};
|
||||
use crate::vm_trace;
|
||||
use oxide_runtime_api::{to_boolean, to_string_full, NativeResult};
|
||||
use oxide_runtime_api::{to_boolean, to_string_full, NativeResult, VmHost};
|
||||
use oxide_types::object::{JsObject, PropAttributes};
|
||||
use oxide_types::private_key::{int_key_value, is_int_key, is_private_name_key, is_symbol_key, make_int_key};
|
||||
use oxide_types::value::JsValue;
|
||||
@@ -738,9 +738,14 @@ impl Vm {
|
||||
if src.is_string() {
|
||||
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).data.encode_utf16().collect() };
|
||||
for (i, unit) in code_units.iter().enumerate() {
|
||||
let s = char::from_u32(*unit as u32).map(|c| c.to_string()).unwrap_or_default();
|
||||
let si = make_int_key(i as u32);
|
||||
let ch_val = self.new_string(&s);
|
||||
let ch_val = match char::from_u32(*unit as u32) {
|
||||
Some(c) => match self.single_char(c) {
|
||||
Some(v) => v,
|
||||
None => self.new_string(&c.to_string()),
|
||||
},
|
||||
None => self.new_string(""),
|
||||
};
|
||||
let rest = unsafe { &mut *rest_ptr };
|
||||
self.set_or_create_prop_value(rest, si, ch_val);
|
||||
}
|
||||
@@ -792,9 +797,15 @@ impl Vm {
|
||||
let s = unsafe { (*raw.as_string_ptr()).data.clone() };
|
||||
let code_units: Vec<u16> = s.encode_utf16().collect();
|
||||
for (i, unit) in code_units.iter().enumerate() {
|
||||
let ch = char::from_u32(*unit as u32).map(|c| c.to_string()).unwrap_or_default();
|
||||
let si = make_int_key(i as u32);
|
||||
assignments.push((si, self.new_string(&ch)));
|
||||
let ch_val = match char::from_u32(*unit as u32) {
|
||||
Some(c) => match self.single_char(c) {
|
||||
Some(v) => v,
|
||||
None => self.new_string(&c.to_string()),
|
||||
},
|
||||
None => self.new_string(""),
|
||||
};
|
||||
assignments.push((si, ch_val));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -887,9 +898,14 @@ impl Vm {
|
||||
let target = unsafe { &mut *target_val.as_js_object_ptr() };
|
||||
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).data.encode_utf16().collect() };
|
||||
for (i, unit) in code_units.iter().enumerate() {
|
||||
let s = char::from_u32(*unit as u32).map(|c| c.to_string()).unwrap_or_default();
|
||||
let si = make_int_key(i as u32);
|
||||
let ch_val = self.new_string(&s);
|
||||
let ch_val = match char::from_u32(*unit as u32) {
|
||||
Some(c) => match self.single_char(c) {
|
||||
Some(v) => v,
|
||||
None => self.new_string(&c.to_string()),
|
||||
},
|
||||
None => self.new_string(""),
|
||||
};
|
||||
self.set_or_create_prop_value(target, si, ch_val);
|
||||
}
|
||||
return Ok(());
|
||||
|
||||
@@ -58,3 +58,40 @@ fn new_iterator_throws_type_error() {
|
||||
let result = eval(&mut vm, "try { new Iterator() } catch (e) { e instanceof TypeError }").unwrap();
|
||||
assert!(result.as_bool());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterator_from_string_full_sequence() {
|
||||
// 手动 next 逐字符取完全部 ASCII 字符后 done(游标推进路径)。
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(
|
||||
&mut vm,
|
||||
"var it = Iterator.from('ab'); \
|
||||
var r = []; var x; while (!(x = it.next()).done) r.push(x.value); \
|
||||
r.join('')",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(to_str(&vm, result), "ab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterator_from_string_astral_scalar() {
|
||||
// 字符串迭代按 Unicode 标量推进:astral 字符整体产出(标量语义未动)。
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "var it = Iterator.from('\\u{1F600}'); it.next().value").unwrap();
|
||||
assert_eq!(to_str(&vm, result), "\u{1F600}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterator_from_string_empty_done() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "Iterator.from('').next().done").unwrap();
|
||||
assert!(result.as_bool());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_of_string_chars() {
|
||||
// for-of 字符串循环产出逐字符(走字节游标 + 单字符缓存)。
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "var out = []; for (const c of 'abc') out.push(c); out.join('')").unwrap();
|
||||
assert_eq!(to_str(&vm, result), "abc");
|
||||
}
|
||||
|
||||
@@ -651,3 +651,54 @@ fn string_slice_astral_character_indices() {
|
||||
let s = eval(&mut vm, "'\\u{1F600}'.padStart(3, 'x')").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "xx\u{1F600}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_char_at_ascii_and_astral() {
|
||||
// charAt 产出:ASCII 走单字符缓存,astral 按标量整体返回,越界空串,
|
||||
// 缺省参数取首字符。
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'abc'.charAt(1)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "b");
|
||||
let s = eval(&mut vm, "'\\u{1F600}ab'.charAt(0)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "\u{1F600}");
|
||||
let s = eval(&mut vm, "'ab'.charAt(5)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "");
|
||||
let s = eval(&mut vm, "'xy'.charAt()").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_char_at_eq_perm_string() {
|
||||
// charAt 产出与字面量比较:内容相等(缓存串与字面量 perm 串)。
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'abc'.charAt(1) === 'b'").unwrap();
|
||||
assert!(result.as_bool());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_split_empty_separator_chars() {
|
||||
// 空分隔 split 逐字符产出:ASCII 走单字符缓存,混合 astral 按标量切分。
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'abc'.split('').join('-')").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "a-b-c");
|
||||
let result = eval(&mut vm, "'\\u{1F600}a'.split('').length").unwrap();
|
||||
assert_eq!(result.as_int(), 2);
|
||||
let result = eval(&mut vm, "'\\u{1F600}a'.split('')[1]").unwrap();
|
||||
assert_eq!(to_str(&vm, result), "a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_spread_object_chars() {
|
||||
// 对象展开字符串源:索引字符为可枚举自有属性。
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "const o = {...'ab'}; o['0'] + o['1']").unwrap();
|
||||
assert_eq!(to_str(&vm, result), "ab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_rest_destructure_chars() {
|
||||
// rest 解构字符串源:索引字符收集到 rest 对象。
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "const {...r} = 'ab'; r['0'] + r['1']").unwrap();
|
||||
assert_eq!(to_str(&vm, result), "ab");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user