Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,38 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: cargo bench -p tendril --bench tendril -- --quick --significance-level 0.01
- run: cargo bench -p tendril-bench --bench tendril -- --quick --significance-level 0.01
env:
RUSTFLAGS: --cfg bench
- run: cargo bench -p tendril --bench futf -- --quick --significance-level 0.01
- run: cargo bench -p tendril-bench --bench futf -- --quick --significance-level 0.01
env:
RUSTFLAGS: --cfg bench

miri:
name: Tests with Miri
runs-on: ubuntu-latest
env:
# Miri is much slower than native, so running the entire test suite takes too much time
WHICH_TESTS: -p markup5ever -p tendril --features encoding_rs
steps:
- name: Which tests to run
run: echo "$WHICH_TESTS"
- uses: actions/checkout@v7
- name: Setup Miri
run: |
rustup set profile minimal
rustup override set nightly
rustup component add miri
cargo miri setup
cargo miri setup --target i686-unknown-linux-gnu
cargo miri setup --target s390x-unknown-linux-gnu
- name: Run some tests with Miri
run: cargo miri test $WHICH_TESTS
- name: Run some tests with Miri 32-bit
run: cargo miri test $WHICH_TESTS --target i686-unknown-linux-gnu
- name: Run some tests with Miri big-endian
run: cargo miri test $WHICH_TESTS --target s390x-unknown-linux-gnu

msrv:
name: MSRV
runs-on: ubuntu-latest
Expand Down Expand Up @@ -115,6 +140,7 @@ jobs:
- msrv
- bench-html5ever
- bench-tendril
- miri

steps:
- name: Success
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"rcdom",
"xml5ever",
"tendril",
"tendril-bench",
]

[workspace.package]
Expand Down
24 changes: 24 additions & 0 deletions tendril-bench/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "tendril-bench"
publish = false
edition.workspace = true

[dependencies]
tendril = {path = "../tendril"}

[dev-dependencies]
criterion = { workspace = true }

[lib]
test = false
doctest = false
bench = false
doc = false

[[bench]]
name = "futf"
harness = false

[[bench]]
name = "tendril"
harness = false
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions tendril-bench/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

9 changes: 0 additions & 9 deletions tendril/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,3 @@ new_debug_unreachable = { workspace = true }

[dev-dependencies]
rand = { workspace = true }
criterion = { workspace = true }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As dev-dependencies cannot be optional, this allows running Tendril tests under "cross-compiling" Miri without building criterion which depends on alloca which requires a C toolchain


[[bench]]
name = "futf"
harness = false

[[bench]]
name = "tendril"
harness = false
98 changes: 57 additions & 41 deletions tendril/src/tendril.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use std::default::Default;
use std::fmt as strfmt;
use std::iter::FromIterator;
use std::marker::PhantomData;
use std::num::NonZeroUsize;
use std::ops::{Deref, DerefMut};
use std::ptr::NonNull;
use std::sync::atomic::Ordering as AtomicOrdering;
use std::sync::atomic::{self, AtomicUsize};
use std::{hash, io, mem, ptr, str};
Expand All @@ -30,9 +30,12 @@ const MAX_INLINE_TAG: usize = 0xF;
const EMPTY_TAG: usize = 0xF;

#[inline(always)]
fn inline_tag(len: u32) -> NonZeroUsize {
fn inline_tag<T>(len: u32) -> NonNull<T> {
debug_assert!(len <= MAX_INLINE_LEN as u32);
unsafe { NonZeroUsize::new_unchecked(if len == 0 { EMPTY_TAG } else { len as usize }) }
const _: () = assert!(EMPTY_TAG != 0);
let address = if len == 0 { EMPTY_TAG } else { len as usize };
// SAFETY: in either case, the address used is non-zero
unsafe { NonNull::new_unchecked(std::ptr::without_provenance_mut(address)) }
}

/// The multithreadedness of a tendril.
Expand Down Expand Up @@ -185,7 +188,7 @@ where
F: fmt::Format,
A: Atomicity,
{
ptr: Cell<NonZeroUsize>,
ptr: Cell<NonNull<Header<A>>>,
buf: UnsafeCell<Buffer>,
marker: PhantomData<*mut F>,
refcount_marker: PhantomData<A>,
Expand Down Expand Up @@ -225,7 +228,7 @@ where
#[inline]
fn clone(&self) -> Tendril<F, A> {
unsafe {
if self.ptr.get().get() > MAX_INLINE_TAG {
if self.addr() > MAX_INLINE_TAG {
self.make_buf_shared();
self.incref();
}
Expand All @@ -243,11 +246,10 @@ where
#[inline]
fn drop(&mut self) {
unsafe {
let p = self.ptr.get().get();
let p = self.addr();
if p <= MAX_INLINE_TAG {
return;
}

let (buf, shared, _) = self.assume_buf();
if shared {
let header = self.header();
Expand Down Expand Up @@ -529,7 +531,7 @@ where
{
#[inline]
fn fmt(&self, f: &mut strfmt::Formatter) -> strfmt::Result {
let kind = match self.ptr.get().get() {
let kind = match self.addr() {
p if p <= MAX_INLINE_TAG => "inline",
p if p & 1 == 1 => "shared",
_ => "owned",
Expand Down Expand Up @@ -605,7 +607,7 @@ where
/// slice, if any.
#[inline(always)]
pub fn len32(&self) -> u32 {
match self.ptr.get().get() {
match self.addr() {
EMPTY_TAG => 0,
n if n <= MAX_INLINE_LEN => n as u32,
_ => unsafe { self.raw_len() },
Expand All @@ -615,25 +617,28 @@ where
/// Is the backing buffer shared?
#[inline]
pub fn is_shared(&self) -> bool {
let n = self.ptr.get().get();
let n = self.addr();

(n > MAX_INLINE_TAG) && ((n & 1) == 1)
}

/// Is the backing buffer shared with this other `Tendril`?
#[inline]
pub fn is_shared_with(&self, other: &Tendril<F, A>) -> bool {
let n = self.ptr.get().get();
let n = self.addr();

(n > MAX_INLINE_TAG) && (n == other.ptr.get().get())
(n > MAX_INLINE_TAG) && (n == other.addr())
}

/// Truncate to length 0 without discarding any owned storage.
#[inline]
pub fn clear(&mut self) {
if self.ptr.get().get() <= MAX_INLINE_TAG {
self.ptr
.set(unsafe { NonZeroUsize::new_unchecked(EMPTY_TAG) });
if self.addr() <= MAX_INLINE_TAG {
let ptr = std::ptr::without_provenance_mut(EMPTY_TAG);
const _: () = assert!(EMPTY_TAG != 0);
// SAFETY: the tag used as an address is non-zero
let ptr = unsafe { NonNull::new_unchecked(ptr) };
self.ptr.set(ptr);
} else {
let (_, shared, _) = unsafe { self.assume_buf() };
if shared {
Expand Down Expand Up @@ -773,7 +778,7 @@ where
let new_len = self.len32().checked_add(other.len32()).expect(OFLOW);

unsafe {
if (self.ptr.get().get() > MAX_INLINE_TAG) && (other.ptr.get().get() > MAX_INLINE_TAG) {
if (self.addr() > MAX_INLINE_TAG) && (other.addr() > MAX_INLINE_TAG) {
let (self_buf, self_shared, _) = self.assume_buf();
let (other_buf, other_shared, _) = other.assume_buf();

Expand Down Expand Up @@ -1042,12 +1047,12 @@ where

#[inline]
unsafe fn make_buf_shared(&self) {
let p = self.ptr.get().get();
if p & 1 == 0 {
let header = p as *mut Header<A>;
let p = self.ptr.get();
if p.addr().get() & 1 == 0 {
let header = p.as_ptr();
(*header).cap = self.aux();

self.ptr.set(NonZeroUsize::new_unchecked(p | 1));
self.ptr.set(p.map_addr(|p| p | 1));
self.set_aux(0);
}
}
Expand All @@ -1058,7 +1063,7 @@ where
#[inline]
fn make_owned(&mut self) {
unsafe {
let ptr = self.ptr.get().get();
let ptr = self.addr();
if ptr <= MAX_INLINE_TAG || (ptr & 1) == 1 {
*self = Tendril::owned_copy(self.as_byte_slice());
}
Expand All @@ -1070,18 +1075,18 @@ where
self.make_owned();
let mut buf = self.assume_buf().0;
buf.grow(cap);
self.ptr.set(NonZeroUsize::new_unchecked(buf.ptr as usize));
self.ptr.set(NonNull::new_unchecked(buf.ptr));
self.set_aux(buf.cap);
}

#[inline(always)]
unsafe fn header(&self) -> *mut Header<A> {
(self.ptr.get().get() & !1) as *mut Header<A>
self.ptr.get().as_ptr().map_addr(|p| p & !1)
}

#[inline]
unsafe fn assume_buf(&self) -> (Buf32<Header<A>>, bool, u32) {
let ptr = self.ptr.get().get();
let ptr = self.addr();
let header = self.header();
let shared = (ptr & 1) == 1;
let (cap, offset) = match shared {
Expand Down Expand Up @@ -1116,7 +1121,7 @@ where
#[inline]
unsafe fn owned(x: Buf32<Header<A>>) -> Tendril<F, A> {
Tendril {
ptr: Cell::new(NonZeroUsize::new_unchecked(x.ptr as usize)),
ptr: Cell::new(NonNull::new_unchecked(x.ptr)),
buf: UnsafeCell::new(Buffer {
heap: Heap {
len: x.len,
Expand All @@ -1139,8 +1144,9 @@ where

#[inline]
unsafe fn shared(buf: Buf32<Header<A>>, off: u32, len: u32) -> Tendril<F, A> {
let non_null = NonNull::new_unchecked(buf.ptr);
Tendril {
ptr: Cell::new(NonZeroUsize::new_unchecked((buf.ptr as usize) | 1)),
ptr: Cell::new(non_null.map_addr(|p| p | 1)),
buf: UnsafeCell::new(Buffer {
heap: Heap { len, aux: off },
}),
Expand All @@ -1152,7 +1158,7 @@ where
#[inline]
fn as_byte_slice(&self) -> &[u8] {
unsafe {
match self.ptr.get().get() {
match self.addr() {
EMPTY_TAG => &[],
n if n <= MAX_INLINE_LEN => (*self.buf.get()).inline.get_unchecked(..n),
_ => {
Expand All @@ -1171,7 +1177,7 @@ where
#[inline]
fn as_mut_byte_slice(&mut self) -> &mut [u8] {
unsafe {
match self.ptr.get().get() {
match self.addr() {
EMPTY_TAG => &mut [],
n if n <= MAX_INLINE_LEN => (*self.buf.get()).inline.get_unchecked_mut(..n),
_ => {
Expand Down Expand Up @@ -1199,6 +1205,10 @@ where
unsafe fn set_aux(&self, aux: u32) {
(*self.buf.get()).heap.aux = aux;
}

fn addr(&self) -> usize {
self.ptr.get().addr().get()
}
}

impl<F, A> Tendril<F, A>
Expand Down Expand Up @@ -1468,7 +1478,7 @@ where
#[inline]
pub unsafe fn push_uninitialized(&mut self, n: u32) {
let new_len = self.len32().checked_add(n).expect(OFLOW);
if new_len <= MAX_INLINE_LEN as u32 && self.ptr.get().get() <= MAX_INLINE_TAG {
if new_len <= MAX_INLINE_LEN as u32 && self.addr() <= MAX_INLINE_TAG {
self.ptr.set(inline_tag(new_len))
} else {
self.make_owned_with_capacity(new_len);
Expand Down Expand Up @@ -2305,18 +2315,24 @@ mod test {
assert_send::<Tendril<fmt::UTF8, Atomic>>();
let s: Tendril<fmt::UTF8, Atomic> = Tendril::from_slice("this is a string");
assert!(!s.is_shared());
let mut t = s.clone();
assert!(s.is_shared());
let sp = s.as_ptr() as usize;
thread::spawn(move || {
assert!(t.is_shared());
t.push_slice(" extended");
assert_eq!("this is a string extended", &*t);
assert!(t.as_ptr() as usize != sp);
assert!(!t.is_shared());
})
.join()
.unwrap();
let threads: Vec<_> = (0..32)
.map(|_| {
let t = s.clone();
assert!(s.is_shared());
let sp = s.as_ptr() as usize;
thread::spawn(move || {
let mut t = t.clone(); // atomic refcount from multiple threads
assert!(t.is_shared());
t.push_slice(" extended");
assert_eq!("this is a string extended", &*t);
assert!(t.as_ptr() as usize != sp);
assert!(!t.is_shared());
})
})
.collect();
for thread in threads {
thread.join().unwrap();
}
assert!(s.is_shared());
assert_eq!("this is a string", &*s);
}
Expand Down