Skip to content

Commit ff2b4c4

Browse files
committed
Improvements to the speed of latin1_to_string.
On the benchmark it improves the encoding from around 9 microsends to 6 microseconds. This PR also includes the benchmark and setup as criterion benchmark. Signed-off-by: Narfinger <Narfinger@users.noreply.github.com>
1 parent 99731e7 commit ff2b4c4

File tree

4 files changed

+142
-18
lines changed

4 files changed

+142
-18
lines changed

mozjs/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,19 @@ profilemozjs = ['mozjs_sys/profilemozjs']
1717
crown = ['mozjs_sys/crown']
1818

1919
[dependencies]
20+
encoding_rs = "0.8.35"
2021
libc.workspace = true
2122
log = "0.4"
2223
mozjs_sys = { path = "../mozjs-sys" }
2324

25+
[dev-dependencies]
26+
criterion = "0.6"
27+
2428
[build-dependencies]
2529
cc.workspace = true
2630
bindgen.workspace = true
31+
32+
33+
[[bench]]
34+
name = "latin1_string_conversion"
35+
harness = false
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use criterion::{criterion_group, criterion_main, Criterion};
2+
use std::ffi::c_void;
3+
use std::{iter, ptr};
4+
5+
use mozjs::conversions::jsstr_to_string;
6+
use mozjs::glue::{CreateJSExternalStringCallbacks, JSExternalStringCallbacksTraps};
7+
use mozjs::jsapi::{
8+
JSAutoRealm, JS_NewExternalStringLatin1, JS_NewGlobalObject, OnNewGlobalHookOption,
9+
};
10+
use mozjs::rooted;
11+
use mozjs::rust::{JSEngine, RealmOptions, Runtime, SIMPLE_GLOBAL_CLASS};
12+
13+
fn external_string(c: &mut Criterion) {
14+
unsafe {
15+
let engine = JSEngine::init().unwrap();
16+
let runtime = Runtime::new(engine.handle());
17+
let context = runtime.cx();
18+
let h_option = OnNewGlobalHookOption::FireOnNewGlobalHook;
19+
let c_option = RealmOptions::default();
20+
rooted!(in(context) let global = JS_NewGlobalObject(
21+
context,
22+
&SIMPLE_GLOBAL_CLASS,
23+
ptr::null_mut(),
24+
h_option,
25+
&*c_option,
26+
));
27+
let _ac = JSAutoRealm::new(context, global.get());
28+
29+
let latin1_base =
30+
iter::repeat_n("test latin-1 test", 1_000_000).fold(String::new(), |mut acc, x| {
31+
acc.push_str(x);
32+
acc
33+
});
34+
35+
let latin1_boxed = latin1_base.as_bytes().to_vec().into_boxed_slice();
36+
let latin1_chars = Box::into_raw(latin1_boxed).cast::<u8>();
37+
let callbacks = CreateJSExternalStringCallbacks(
38+
&EXTERNAL_STRING_CALLBACKS_TRAPS,
39+
latin1_base.len() as *mut c_void,
40+
);
41+
rooted!(in(context) let latin1_jsstr = JS_NewExternalStringLatin1(
42+
context,
43+
latin1_chars,
44+
latin1_base.len(),
45+
callbacks
46+
));
47+
c.bench_function("external_string_latin1", |b| {
48+
b.iter(|| {
49+
jsstr_to_string(context, latin1_jsstr.get());
50+
})
51+
});
52+
}
53+
}
54+
55+
static EXTERNAL_STRING_CALLBACKS_TRAPS: JSExternalStringCallbacksTraps =
56+
JSExternalStringCallbacksTraps {
57+
latin1Finalize: Some(latin1::finalize),
58+
latin1SizeOfBuffer: Some(latin1::size_of),
59+
utf16Finalize: Some(utf16::finalize),
60+
utf16SizeOfBuffer: Some(utf16::size_of),
61+
};
62+
63+
mod latin1 {
64+
use std::ffi::c_void;
65+
use std::slice;
66+
67+
use mozjs::jsapi::mozilla::MallocSizeOf;
68+
69+
pub unsafe extern "C" fn finalize(data: *const c_void, chars: *mut u8) {
70+
let slice = slice::from_raw_parts_mut(chars, data as usize);
71+
let _ = Box::from_raw(slice);
72+
}
73+
74+
pub unsafe extern "C" fn size_of(data: *const c_void, _: *const u8, _: MallocSizeOf) -> usize {
75+
data as usize
76+
}
77+
}
78+
79+
mod utf16 {
80+
use std::ffi::c_void;
81+
use std::slice;
82+
83+
use mozjs::jsapi::mozilla::MallocSizeOf;
84+
85+
pub unsafe extern "C" fn finalize(data: *const c_void, chars: *mut u16) {
86+
let slice = slice::from_raw_parts_mut(chars, data as usize);
87+
let _ = Box::from_raw(slice);
88+
}
89+
90+
pub unsafe extern "C" fn size_of(data: *const c_void, _: *const u16, _: MallocSizeOf) -> usize {
91+
data as usize
92+
}
93+
}
94+
95+
criterion_group!(benches, external_string);
96+
criterion_main!(benches);

mozjs/src/conversions.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ use log::debug;
4848
use mozjs_sys::jsgc::Rooted;
4949
use std::borrow::Cow;
5050
use std::mem;
51+
use std::mem::MaybeUninit;
5152
use std::rc::Rc;
5253
use std::{ptr, slice};
5354

@@ -525,6 +526,16 @@ impl FromJSValConvertible for f64 {
525526
}
526527
}
527528

529+
/// Copies chars to the string
530+
unsafe fn fast_copy(chars: &[u8]) -> String {
531+
let mut v = Vec::with_capacity(chars.len() * 2);
532+
v.set_len(chars.len() * 2);
533+
let real_size = encoding_rs::mem::convert_latin1_to_utf8(chars, v.as_mut_slice());
534+
535+
v.truncate(real_size);
536+
String::from_utf8_unchecked(v)
537+
}
538+
528539
/// Converts a `JSString`, encoded in "Latin1" (i.e. U+0000-U+00FF encoded as 0x00-0xFF) into a
529540
/// `String`.
530541
pub unsafe fn latin1_to_string(cx: *mut JSContext, s: *mut JSString) -> String {
@@ -535,9 +546,7 @@ pub unsafe fn latin1_to_string(cx: *mut JSContext, s: *mut JSString) -> String {
535546
assert!(!chars.is_null());
536547

537548
let chars = slice::from_raw_parts(chars, length as usize);
538-
let mut s = String::with_capacity(length as usize);
539-
s.extend(chars.iter().map(|&c| c as char));
540-
s
549+
fast_copy(chars)
541550
}
542551

543552
/// Converts a `JSString` into a `String`, regardless of used encoding.

mozjs/tests/external_string.rs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,13 @@ fn external_string() {
3636
));
3737
let _ac = JSAutoRealm::new(context, global.get());
3838

39-
let latin1_base = "test latin-1";
40-
let latin1_boxed = latin1_base.as_bytes().to_vec().into_boxed_slice();
41-
let latin1_chars = Box::into_raw(latin1_boxed).cast::<u8>();
42-
43-
let callbacks = CreateJSExternalStringCallbacks(
44-
&EXTERNAL_STRING_CALLBACKS_TRAPS,
45-
latin1_base.len() as *mut c_void,
46-
);
47-
rooted!(in(context) let latin1_jsstr = JS_NewExternalStringLatin1(
48-
context,
49-
latin1_chars,
50-
latin1_base.len(),
51-
callbacks
52-
));
53-
assert_eq!(jsstr_to_string(context, latin1_jsstr.get()), latin1_base);
39+
test_latin1_string(context, "test latin1");
40+
test_latin1_string(context, "abcdefghijklmnop"); // exactly 16 bytes
41+
test_latin1_string(context, "abcdefghijklmnopq"); // 17 bytes
42+
test_latin1_string(context, "abcdefghijklmno"); // 15 bytes
43+
test_latin1_string(context, "abcdefghijklmnopqrstuvwxyzabcdef"); //32 bytes
44+
test_latin1_string(context, "abcdefghijklmnopqrstuvwxyzabcde"); //31 bytes
45+
test_latin1_string(context, "abcdefghijklmnopqrstuvwxyzabcdefg"); //33 bytes
5446

5547
let utf16_base = "test utf-16 $€ \u{10437}\u{24B62}";
5648
let utf16_boxed = utf16_base
@@ -74,6 +66,24 @@ fn external_string() {
7466
}
7567
}
7668

69+
#[cfg(test)]
70+
unsafe fn test_latin1_string(context: *mut mozjs::jsapi::JSContext, latin1_base: &str) {
71+
let latin1_boxed = latin1_base.as_bytes().to_vec().into_boxed_slice();
72+
let latin1_chars = Box::into_raw(latin1_boxed).cast::<u8>();
73+
74+
let callbacks = CreateJSExternalStringCallbacks(
75+
&EXTERNAL_STRING_CALLBACKS_TRAPS,
76+
latin1_base.len() as *mut c_void,
77+
);
78+
rooted!(in(context) let latin1_jsstr = JS_NewExternalStringLatin1(
79+
context,
80+
latin1_chars,
81+
latin1_base.len(),
82+
callbacks
83+
));
84+
assert_eq!(jsstr_to_string(context, latin1_jsstr.get()), latin1_base);
85+
}
86+
7787
static EXTERNAL_STRING_CALLBACKS_TRAPS: JSExternalStringCallbacksTraps =
7888
JSExternalStringCallbacksTraps {
7989
latin1Finalize: Some(latin1::finalize),

0 commit comments

Comments
 (0)