Sign In

v8

Package Overview
Dependencies
Maintainers
0
Versions
196
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

v8 - cargo Package Compare versions

Comparing version
150.2.0
to
150.3.0
+65
benches/string_encode.rs
// Rust -> V8 string creation benchmark: new_from_utf8 across sizes and content.
use std::time::Instant;
fn make(kind: &str, n: usize) -> String {
match kind {
"ascii" => "a".repeat(n),
"latin1" => "\u{00e9}".repeat(n),
"twobyte" => "\u{4e16}".repeat(n),
_ => unreachable!(),
}
}
fn main() {
// Skip running benchmarks in debug or CI (cargo-nextest lists test binaries
// by running them; this harness=false bench must produce no output there).
if cfg!(debug_assertions) || std::env::var("CI").is_ok() {
return;
}
let platform = v8::new_default_platform(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
let isolate = &mut v8::Isolate::new(v8::CreateParams::default());
v8::scope!(let handle_scope, isolate);
let context = v8::Context::new(handle_scope, Default::default());
let scope = &mut v8::ContextScope::new(handle_scope, context);
let sizes = [4usize, 16, 64, 256, 4096];
let kinds = ["ascii", "latin1", "twobyte"];
// Correctness: created string must round-trip.
for kind in kinds {
for n in sizes {
let reference = make(kind, n);
let local = v8::String::new(scope, &reference).unwrap();
assert_eq!(local.to_rust_string_lossy(scope), reference, "{kind}/{n}");
}
}
println!("correctness OK");
let runs = 1_000_000u64;
for kind in kinds {
for n in sizes {
let s = make(kind, n);
let bytes = s.as_bytes();
let iters = if n >= 4096 { runs / 10 } else { runs };
for _ in 0..(iters / 10) {
v8::scope!(let hs, scope);
std::hint::black_box(
v8::String::new_from_utf8(hs, bytes, v8::NewStringType::Normal)
.unwrap(),
);
}
let t = Instant::now();
for _ in 0..iters {
v8::scope!(let hs, scope);
std::hint::black_box(
v8::String::new_from_utf8(hs, bytes, v8::NewStringType::Normal)
.unwrap(),
);
}
let ns = t.elapsed().as_nanos() as f64 / iters as f64;
println!(" new {kind:8} {n:5} {ns:9.2} ns/op");
}
}
}
// V8 -> Rust string read benchmark: to_rust_string_lossy / to_rust_cow_lossy
// across sizes and content kinds. Used to measure the ValueView field-read
// change (proposal 1) and later read-path proposals.
use std::time::Instant;
fn make(kind: &str, n: usize) -> String {
match kind {
"ascii" => "a".repeat(n),
"latin1" => "\u{00e9}".repeat(n), // é, one-byte non-ASCII
"twobyte" => "\u{4e16}".repeat(n), // 世, two-byte
_ => unreachable!(),
}
}
fn main() {
// Skip running benchmarks in debug or CI (cargo-nextest lists test binaries
// by running them; this harness=false bench must produce no output there).
if cfg!(debug_assertions) || std::env::var("CI").is_ok() {
return;
}
let platform = v8::new_default_platform(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
let isolate = &mut v8::Isolate::new(v8::CreateParams::default());
v8::scope!(let handle_scope, isolate);
let context = v8::Context::new(handle_scope, Default::default());
let scope = &mut v8::ContextScope::new(handle_scope, context);
let sizes = [4usize, 16, 64, 256, 4096];
let kinds = ["ascii", "latin1", "twobyte"];
let runs = 2_000_000u64;
// Correctness gate.
for kind in kinds {
for n in sizes {
let reference = make(kind, n);
let local = v8::String::new(scope, &reference).unwrap();
assert_eq!(local.to_rust_string_lossy(scope), reference, "{kind}/{n}");
}
}
println!("correctness OK");
for kind in kinds {
for n in sizes {
let reference = make(kind, n);
let local = v8::String::new(scope, &reference).unwrap();
let iters = if n >= 4096 { runs / 20 } else { runs };
for _ in 0..(iters / 10) {
std::hint::black_box(local.to_rust_string_lossy(scope));
}
let t = Instant::now();
for _ in 0..iters {
std::hint::black_box(local.to_rust_string_lossy(scope));
}
let ns = t.elapsed().as_nanos() as f64 / iters as f64;
println!(" lossy {kind:8} {n:5} {ns:9.2} ns/op");
}
}
}
+1
-1
{
"git": {
"sha1": "d305e6afa7736f6e298c30ae6646f7709ee9382b",
"sha1": "021b651ead70da06d38a75761ba4d74c208458cd",
"dirty": true

@@ -5,0 +5,0 @@ },

@@ -776,3 +776,3 @@ # This file is automatically @generated by Cargo.

name = "v8"
version = "150.2.0"
version = "150.3.0"
dependencies = [

@@ -779,0 +779,0 @@ "align-data",

@@ -15,3 +15,3 @@ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO

name = "v8"
version = "150.2.0"
version = "150.3.0"
authors = ["the Deno authors"]

@@ -208,2 +208,12 @@ build = "build.rs"

[[bench]]
name = "string_encode"
path = "benches/string_encode.rs"
harness = false
[[bench]]
name = "string_read"
path = "benches/string_read.rs"
harness = false
[dependencies.bitflags]

@@ -210,0 +220,0 @@ version = "2.5"

@@ -14,3 +14,2 @@ use crate::Isolate;

use std::default::Default;
use std::ffi::c_void;
use std::marker::PhantomData;

@@ -86,2 +85,8 @@ use std::mem::MaybeUninit;

/// Minimum non-ASCII UTF-8 byte length before `new_from_utf8` decodes with
/// simdutf instead of V8's decoder. Below this the two potential simdutf FFI
/// calls cost more than V8 handling the tiny string itself.
#[cfg(feature = "simdutf")]
const NONASCII_ENCODE_SIMD_THRESHOLD: usize = 16;
unsafe extern "C" {

@@ -210,5 +215,2 @@ fn v8__String__Empty(isolate: *mut RealIsolate) -> *const String;

fn v8__String__ValueView__DESTRUCT(this: *mut ValueView);
fn v8__String__ValueView__is_one_byte(this: *const ValueView) -> bool;
fn v8__String__ValueView__data(this: *const ValueView) -> *const c_void;
fn v8__String__ValueView__length(this: *const ValueView) -> int;
}

@@ -482,2 +484,27 @@

}
// V8's `NewFromUtf8` runs a scalar UTF-8 decoder (twice: once to compute
// the width/length, once to write), which is very slow for non-ASCII. When
// simdutf is available we decode the input ourselves and hand V8 a
// pre-decoded one-byte (Latin-1) or two-byte (UTF-16) buffer — which it can
// just memcpy.
// `NewFromUtf8` rejects inputs whose *byte* length exceeds the maximum
// string length (conservatively, before decoding). Our decode paths would
// otherwise accept some of those (the decoded string is shorter), which
// would change behavior, so only take them when the byte length is in
// range and let V8 reject the rest.
#[cfg(feature = "simdutf")]
if buffer.len() <= Self::MAX_LENGTH {
// Pure ASCII (the common case): the bytes are already valid one-byte
// (Latin-1) data. `is_ascii` is an inline SWAR scan, cheaper than a
// simdutf FFI call for the short strings that dominate.
if buffer.is_ascii() {
return Self::new_from_one_byte(scope, buffer, new_type);
}
// Non-ASCII: transcode with simdutf only above a small threshold. For
// tiny strings the two potential simdutf FFI calls (Latin-1 attempt then
// UTF-16) cost more than V8's decoder, which is only slow at scale.
if buffer.len() >= NONASCII_ENCODE_SIMD_THRESHOLD {
return Self::new_from_utf8_transcode(scope, buffer, new_type);
}
}
let buffer_len = buffer.len().try_into().ok()?;

@@ -496,2 +523,58 @@ unsafe {

/// Decodes non-ASCII, non-empty valid UTF-8 into one-byte (Latin-1) or
/// two-byte (UTF-16) data with simdutf and hands it to V8. Falls back to
/// V8's lossy `NewFromUtf8` when the input isn't valid UTF-8.
#[cfg(feature = "simdutf")]
fn new_from_utf8_transcode<'s>(
scope: &PinScope<'s, '_, ()>,
buffer: &[u8],
new_type: NewStringType,
) -> Option<Local<'s, String>> {
{
// Try Latin-1 first (more compact). The conversion errors if any code
// point exceeds U+00FF or the input isn't valid UTF-8; a Latin-1 result
// is never longer than the UTF-8 input.
let mut latin1: Vec<u8> = Vec::with_capacity(buffer.len());
// SAFETY: `latin1` has `buffer.len()` bytes of spare capacity, an upper
// bound on the Latin-1 length; simdutf only writes, never reads it.
let r = unsafe {
let out =
std::slice::from_raw_parts_mut(latin1.as_mut_ptr(), buffer.len());
crate::simdutf::convert_utf8_to_latin1_with_errors(buffer, out)
};
if r.is_ok() {
// SAFETY: simdutf wrote `r.count` valid Latin-1 bytes.
unsafe { latin1.set_len(r.count) };
return Self::new_from_one_byte(scope, &latin1, new_type);
}
// Not Latin-1 representable (or invalid UTF-8): try UTF-16. A UTF-16
// result is never more code units than the UTF-8 input has bytes.
let mut utf16: Vec<u16> = Vec::with_capacity(buffer.len());
// SAFETY: `utf16` has `buffer.len()` units of spare capacity, an upper
// bound on the UTF-16 length.
let r = unsafe {
let out =
std::slice::from_raw_parts_mut(utf16.as_mut_ptr(), buffer.len());
crate::simdutf::convert_utf8_to_utf16le_with_errors(buffer, out)
};
if r.is_ok() {
// SAFETY: simdutf wrote `r.count` valid UTF-16 code units.
unsafe { utf16.set_len(r.count) };
return Self::new_from_two_byte(scope, &utf16, new_type);
}
// Invalid UTF-8: fall through to V8's lossy `NewFromUtf8`.
}
let buffer_len = buffer.len().try_into().ok()?;
unsafe {
scope.cast_local(|sd| {
v8__String__NewFromUtf8(
sd.get_isolate_ptr(),
buffer.as_ptr() as *const char,
new_type,
buffer_len,
)
})
}
}
/// Allocates a new string from Latin-1 data. Only returns an empty value when

@@ -976,6 +1059,5 @@ /// length > kMaxLength.

pub fn to_rust_string_lossy(&self, scope: &Isolate) -> std::string::String {
if self.length() == 0 {
return std::string::String::new();
}
// No preliminary `self.length()` FFI call: the `ValueView` reports the
// length, and `data()` yields an empty slice for empty strings, which the
// ASCII arm below turns into an empty `String`.
// SAFETY: `self` is a valid V8 string reachable from a handle scope.

@@ -985,10 +1067,3 @@ let view = unsafe { ValueView::new_from_ref(scope, self) };

match view.data() {
ValueViewData::OneByte(bytes) => {
if bytes.is_ascii() {
// SAFETY: ASCII is valid UTF-8.
unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned()
} else {
latin1_to_string(bytes)
}
}
ValueViewData::OneByte(bytes) => onebyte_to_string(bytes),
ValueViewData::TwoByte(units) => wtf16_to_string(units),

@@ -1013,7 +1088,4 @@ }

buf.clear();
let len = self.length();
if len == 0 {
return;
}
// No preliminary `self.length()` FFI call; an empty string yields an empty
// `data()` slice and leaves `buf` cleared.
// SAFETY: `self` is a valid V8 string reachable from a handle scope.

@@ -1025,3 +1097,3 @@ // The ValueView is dropped before we return.

ValueViewData::OneByte(bytes) => {
if bytes.is_ascii() {
if onebyte_is_ascii(bytes) {
// ASCII: direct copy, already valid UTF-8.

@@ -1068,7 +1140,4 @@ buf.reserve(bytes.len());

) -> Cow<'a, str> {
let len = self.length();
if len == 0 {
return "".into();
}
// No preliminary `self.length()` FFI call; an empty string yields an empty
// `data()` slice, which the ASCII arm borrows as an empty `&str`.
// SAFETY: `self` is a valid V8 string reachable from a handle scope.

@@ -1081,3 +1150,3 @@ // The ValueView is dropped before we return, so the

ValueViewData::OneByte(bytes) => {
if bytes.is_ascii() {
if onebyte_is_ascii(bytes) {
// ASCII: direct memcpy, no transcoding needed.

@@ -1186,9 +1255,40 @@ if bytes.len() <= N {

pub fn data(&self) -> ValueViewData<'_> {
// Read the `v8::String::ValueView` fields directly out of the byte buffer
// that `CONSTRUCT` filled, instead of crossing FFI for each one-line
// accessor thunk. The layout is fixed by the public header
// (v8/include/v8-primitive.h):
// offset 0: Local<v8::String> flat_str_ (1 pointer)
// offset size_of::<*>(): union { data8_; data16_ } (1 pointer)
// + size_of::<*>(): uint32_t length_
// + size_of::<u32>(): bool is_one_byte_
// The offsets are verified at runtime against the FFI accessors in
// `tests/test_api.rs` (`value_view_field_layout`).
const PTR: usize = std::mem::size_of::<*const u8>();
const DATA_OFFSET: usize = PTR;
const LENGTH_OFFSET: usize = PTR + PTR;
const IS_ONE_BYTE_OFFSET: usize = PTR + PTR + std::mem::size_of::<u32>();
unsafe {
let data = v8__String__ValueView__data(self);
let length = v8__String__ValueView__length(self) as usize;
if v8__String__ValueView__is_one_byte(self) {
ValueViewData::OneByte(std::slice::from_raw_parts(data as _, length))
let base = self.0.as_ptr();
let length =
base.add(LENGTH_OFFSET).cast::<u32>().read_unaligned() as usize;
let is_one_byte = *base.add(IS_ONE_BYTE_OFFSET) != 0;
if length == 0 {
// Empty strings may carry a null `data8_`/`data16_` pointer, so return
// an empty slice with a valid (dangling) pointer rather than passing a
// possibly-null pointer to `from_raw_parts`. Still report the actual
// encoding so `data()`'s contract holds for empty two-byte strings.
return if is_one_byte {
ValueViewData::OneByte(&[])
} else {
ValueViewData::TwoByte(&[])
};
}
let data = base.add(DATA_OFFSET).cast::<*const u8>().read_unaligned();
if is_one_byte {
ValueViewData::OneByte(std::slice::from_raw_parts(data, length))
} else {
ValueViewData::TwoByte(std::slice::from_raw_parts(data as _, length))
ValueViewData::TwoByte(std::slice::from_raw_parts(
data.cast::<u16>(),
length,
))
}

@@ -1252,7 +1352,68 @@ }

/// The minimum number of UTF-16 code units before we try the SIMD path.
/// Below this threshold the overhead of validation + length pre-scan is
/// not worth it, so we fall back to the scalar loop.
/// With the single-pass `convert_utf16le_to_utf8_with_errors` conversion the
/// crossover against the scalar `decode_utf16` loop is low; measured wins start
/// around 16 units.
#[cfg(feature = "simdutf")]
const WTF16_SIMD_THRESHOLD: usize = 96;
const WTF16_SIMD_THRESHOLD: usize = 16;
/// Minimum one-byte string length before the simdutf `utf8_length_from_latin1`
/// path beats std's inline `is_ascii` SWAR scan (the simdutf FFI call has fixed
/// overhead that only pays off once the scan is long enough).
#[cfg(feature = "simdutf")]
const ONEBYTE_SIMD_THRESHOLD: usize = 128;
/// Whether one-byte string data is pure ASCII. Uses simdutf's wide SIMD scan
/// for long strings (where it beats std's SWAR `is_ascii`) and the inline
/// `is_ascii` for short ones (avoiding the simdutf FFI-call overhead). Shared
/// by the one-byte read paths that only need the ASCII/Latin-1 decision.
#[inline(always)]
fn onebyte_is_ascii(bytes: &[u8]) -> bool {
#[cfg(feature = "simdutf")]
if bytes.len() >= ONEBYTE_SIMD_THRESHOLD {
return crate::simdutf::validate_ascii(bytes);
}
bytes.is_ascii()
}
/// Converts one-byte (Latin-1) string data to an owned
/// [`std::string::String`].
///
/// With `simdutf`, a single `utf8_length_from_latin1` SIMD pass both detects
/// pure ASCII (result == input length) and yields the exact UTF-8 length for
/// the Latin-1 case, so an ASCII string is one SIMD scan + a memcpy and a
/// Latin-1 string is one SIMD scan + one SIMD transcode (down from the previous
/// `is_ascii` scan + separate length scan + transcode).
#[inline(always)]
fn onebyte_to_string(bytes: &[u8]) -> std::string::String {
#[cfg(feature = "simdutf")]
{
// For long strings, one `utf8_length_from_latin1` SIMD pass both detects
// ASCII and sizes the Latin-1 transcode. For short strings the simdutf FFI
// call costs more than std's inline `is_ascii` SWAR loop, so keep the
// inline path there (crossover measured near ~128 bytes).
if bytes.len() >= ONEBYTE_SIMD_THRESHOLD {
let utf8_len = crate::simdutf::utf8_length_from_latin1(bytes);
if utf8_len == bytes.len() {
// Pure ASCII: already valid UTF-8. SAFETY: ASCII is valid UTF-8.
return unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned();
}
let mut buf: Vec<u8> = Vec::with_capacity(utf8_len);
// SAFETY: `buf` has capacity `utf8_len`, exactly what the transcode writes.
unsafe {
let out = std::slice::from_raw_parts_mut(buf.as_mut_ptr(), utf8_len);
let written = crate::simdutf::convert_latin1_to_utf8(bytes, out);
debug_assert_eq!(written, utf8_len);
buf.set_len(written);
return std::string::String::from_utf8_unchecked(buf);
}
}
}
if bytes.is_ascii() {
// SAFETY: ASCII is valid UTF-8.
unsafe { std::str::from_utf8_unchecked(bytes) }.to_owned()
} else {
latin1_to_string(bytes)
}
}
/// Converts Latin-1 bytes to an owned [`std::string::String`].

@@ -1293,14 +1454,20 @@ #[inline(always)]

{
// For longer, valid UTF-16 strings, use simdutf's SIMD-accelerated path.
if units.len() >= WTF16_SIMD_THRESHOLD
&& crate::simdutf::validate_utf16le(units)
{
let utf8_len = crate::simdutf::utf8_length_from_utf16le(units);
let mut buf: Vec<u8> = Vec::with_capacity(utf8_len);
unsafe {
let out = std::slice::from_raw_parts_mut(buf.as_mut_ptr(), utf8_len);
let written = crate::simdutf::convert_utf16le_to_utf8(units, out);
debug_assert_eq!(written, utf8_len);
buf.set_len(written);
return std::string::String::from_utf8_unchecked(buf);
// Single simdutf pass that validates *and* converts. Each UTF-16 code unit
// yields at most 3 UTF-8 bytes (surrogate pairs are 2 units -> 4 bytes), so
// `len * 3` is a safe upper bound. On a lone-surrogate error we fall
// through to the scalar WTF-16 loop below.
if units.len() >= WTF16_SIMD_THRESHOLD {
let cap = units.len() * 3;
let mut buf: Vec<u8> = Vec::with_capacity(cap);
// SAFETY: `buf` has `cap` bytes of spare capacity.
let result = unsafe {
let out = std::slice::from_raw_parts_mut(buf.as_mut_ptr(), cap);
crate::simdutf::convert_utf16le_to_utf8_with_errors(units, out)
};
if result.is_ok() {
// SAFETY: simdutf wrote `result.count` valid UTF-8 bytes.
unsafe {
buf.set_len(result.count);
return std::string::String::from_utf8_unchecked(buf);
}
}

@@ -1323,15 +1490,20 @@ }

{
if units.len() >= WTF16_SIMD_THRESHOLD
&& crate::simdutf::validate_utf16le(units)
{
let utf8_len = crate::simdutf::utf8_length_from_utf16le(units);
buf.reserve(utf8_len);
unsafe {
let vec = buf.as_mut_vec();
let out = std::slice::from_raw_parts_mut(vec.as_mut_ptr(), utf8_len);
let written = crate::simdutf::convert_utf16le_to_utf8(units, out);
debug_assert_eq!(written, utf8_len);
vec.set_len(written);
if units.len() >= WTF16_SIMD_THRESHOLD {
let cap = units.len() * 3;
buf.reserve(cap);
// SAFETY: appended bytes are valid UTF-8 (or we roll back on error).
let vec = unsafe { buf.as_mut_vec() };
let start = vec.len();
let result = unsafe {
let out =
std::slice::from_raw_parts_mut(vec.as_mut_ptr().add(start), cap);
crate::simdutf::convert_utf16le_to_utf8_with_errors(units, out)
};
if result.is_ok() {
// SAFETY: simdutf wrote `result.count` valid UTF-8 bytes at `start`.
unsafe { vec.set_len(start + result.count) };
return;
}
return;
// Lone surrogate: `vec` len is unchanged (`start`); fall through to
// the scalar loop, which appends over the untouched spare capacity.
}

@@ -1395,24 +1567,23 @@ }

{
if units.len() >= WTF16_SIMD_THRESHOLD
&& crate::simdutf::validate_utf16le(units)
{
let utf8_len = crate::simdutf::utf8_length_from_utf16le(units);
if utf8_len <= N {
let written = unsafe {
let out = std::slice::from_raw_parts_mut(
buffer.as_mut_ptr() as *mut u8,
utf8_len,
);
crate::simdutf::convert_utf16le_to_utf8(units, out)
if units.len() >= WTF16_SIMD_THRESHOLD {
// Each unit is at most 3 UTF-8 bytes, so if `len * 3` fits the stack
// buffer the single-pass conversion is guaranteed to fit; borrow it.
if units.len() * 3 <= N {
let result = unsafe {
let out =
std::slice::from_raw_parts_mut(buffer.as_mut_ptr() as *mut u8, N);
crate::simdutf::convert_utf16le_to_utf8_with_errors(units, out)
};
return unsafe {
let buf = &mut buffer[..written];
let buf = &mut *(buf as *mut [_] as *mut [u8]);
Cow::Borrowed(std::str::from_utf8_unchecked(buf))
};
if result.is_ok() {
return unsafe {
let buf = &mut buffer[..result.count];
let buf = &mut *(buf as *mut [_] as *mut [u8]);
Cow::Borrowed(std::str::from_utf8_unchecked(buf))
};
}
// Lone surrogate: fall through to the scalar path below.
} else {
// The worst case may not fit the stack buffer — allocate.
return Cow::Owned(wtf16_to_string(units));
}
// Doesn't fit in the stack buffer — allocate.
return Cow::Owned(wtf16_to_string(units));
}

@@ -1419,0 +1590,0 @@ }

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display