use std::sync::atomic::{AtomicPtr, Ordering};
use ahash::random_state::RandomState;
use pyo3::exceptions::{PyTypeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyBool, PyString};
use crate::string_decoder::StringOutput;
#[derive(Debug, Clone, Copy, Default)]
pub enum StringCacheMode {
#[default]
All,
Keys,
None,
}
impl<'py> FromPyObject<'_, 'py> for StringCacheMode {
type Error = PyErr;
fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<StringCacheMode> {
if let Ok(bool_mode) = ob.cast::<PyBool>() {
Ok(bool_mode.is_true().into())
} else if let Ok(str_mode) = ob.extract::<&str>() {
match str_mode {
"all" => Ok(Self::All),
"keys" => Ok(Self::Keys),
"none" => Ok(Self::None),
_ => Err(PyValueError::new_err(
"Invalid string cache mode, should be `'all'`, '`keys`', `'none`' or a `bool`",
)),
}
} else {
Err(PyTypeError::new_err(
"Invalid string cache mode, should be `'all'`, '`keys`', `'none`' or a `bool`",
))
}
}
}
impl From<bool> for StringCacheMode {
fn from(mode: bool) -> Self {
if mode { Self::All } else { Self::None }
}
}
#[derive(Default)]
pub(crate) struct StringCacheGuard(Option<Box<PyStringCache>>);
impl Drop for StringCacheGuard {
fn drop(&mut self) {
if let Some(cache) = self.0.take() {
return_string_cache(cache);
}
}
}
pub trait StringMaybeCache {
fn get_key<'py>(
py: Python<'py>,
guard: &mut StringCacheGuard,
string_output: StringOutput<'_, '_>,
) -> Bound<'py, PyString>;
fn get_value<'py>(
py: Python<'py>,
guard: &mut StringCacheGuard,
string_output: StringOutput<'_, '_>,
) -> Bound<'py, PyString> {
Self::get_key(py, guard, string_output)
}
}
#[inline]
unsafe fn guarded_py_string<'py>(
py: Python<'py>,
guard: &mut StringCacheGuard,
string_output: &StringOutput<'_, '_>,
) -> Bound<'py, PyString> {
let s = string_output.as_str();
let ascii_only = string_output.ascii_only();
if (2..64).contains(&s.len()) {
let cache = guard.0.get_or_insert_with(|| take_pooled_cache().unwrap_or_default());
unsafe { cache.get_or_insert(py, s, ascii_only) }
} else {
unsafe { pystring_fast_new_maybe_ascii(py, s, ascii_only) }
}
}
pub struct StringCacheAll;
impl StringMaybeCache for StringCacheAll {
fn get_key<'py>(
py: Python<'py>,
guard: &mut StringCacheGuard,
string_output: StringOutput<'_, '_>,
) -> Bound<'py, PyString> {
unsafe { guarded_py_string(py, guard, &string_output) }
}
}
pub struct StringCacheKeys;
impl StringMaybeCache for StringCacheKeys {
fn get_key<'py>(
py: Python<'py>,
guard: &mut StringCacheGuard,
string_output: StringOutput<'_, '_>,
) -> Bound<'py, PyString> {
unsafe { guarded_py_string(py, guard, &string_output) }
}
fn get_value<'py>(
py: Python<'py>,
_guard: &mut StringCacheGuard,
string_output: StringOutput<'_, '_>,
) -> Bound<'py, PyString> {
unsafe { pystring_fast_new_maybe_ascii(py, string_output.as_str(), string_output.ascii_only()) }
}
}
pub struct StringNoCache;
impl StringMaybeCache for StringNoCache {
fn get_key<'py>(
py: Python<'py>,
_guard: &mut StringCacheGuard,
string_output: StringOutput<'_, '_>,
) -> Bound<'py, PyString> {
unsafe { pystring_fast_new_maybe_ascii(py, string_output.as_str(), string_output.ascii_only()) }
}
}
const POOL_SLOTS: usize = 8;
static STRING_CACHE: [AtomicPtr<PyStringCache>; POOL_SLOTS] =
[const { AtomicPtr::new(std::ptr::null_mut()) }; POOL_SLOTS];
fn take_pooled_cache() -> Option<Box<PyStringCache>> {
for slot in &STRING_CACHE {
let cache = slot.swap(std::ptr::null_mut(), Ordering::Acquire);
if !cache.is_null() {
return Some(unsafe { Box::from_raw(cache) });
}
}
None
}
fn return_string_cache(cache: Box<PyStringCache>) {
let cache = Box::into_raw(cache);
for slot in &STRING_CACHE {
if slot
.compare_exchange(std::ptr::null_mut(), cache, Ordering::Release, Ordering::Relaxed)
.is_ok()
{
return;
}
}
drop(unsafe { Box::from_raw(cache) });
}
fn for_each_pooled_cache(mut f: impl FnMut(&mut PyStringCache)) {
for slot in &STRING_CACHE {
let cache = slot.swap(std::ptr::null_mut(), Ordering::Acquire);
if cache.is_null() {
continue;
}
let mut cache = unsafe { Box::from_raw(cache) };
f(&mut cache);
let cache = Box::into_raw(cache);
if slot
.compare_exchange(std::ptr::null_mut(), cache, Ordering::Release, Ordering::Relaxed)
.is_err()
{
drop(unsafe { Box::from_raw(cache) });
}
}
}
pub fn cache_usage() -> usize {
let mut usage = 0;
for_each_pooled_cache(|cache| usage += cache.usage());
usage
}
pub fn cache_clear() {
for_each_pooled_cache(PyStringCache::clear);
}
#[inline]
pub fn cached_py_string<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> {
unsafe { cached_py_string_maybe_ascii(py, s, false) }
}
#[inline]
pub unsafe fn cached_py_string_ascii<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> {
unsafe { cached_py_string_maybe_ascii(py, s, true) }
}
unsafe fn cached_py_string_maybe_ascii<'py>(py: Python<'py>, s: &str, ascii_only: bool) -> Bound<'py, PyString> {
unsafe {
if (2..64).contains(&s.len()) {
let mut cache = take_pooled_cache().unwrap_or_default();
let py_string = cache.get_or_insert(py, s, ascii_only);
return_string_cache(cache);
py_string
} else {
pystring_fast_new_maybe_ascii(py, s, ascii_only)
}
}
}
const CAPACITY: usize = 16_384;
type Entry = Option<(u64, Py<PyString>)>;
#[derive(Debug)]
struct PyStringCache {
entries: Box<[Entry; CAPACITY]>,
hash_builder: RandomState,
}
const ARRAY_REPEAT_VALUE: Entry = None;
impl Default for PyStringCache {
fn default() -> Self {
Self {
entries: std::iter::repeat_with(|| ARRAY_REPEAT_VALUE)
.take(CAPACITY)
.collect::<Vec<_>>()
.into_boxed_slice()
.try_into()
.unwrap(),
hash_builder: RandomState::default(),
}
}
}
impl PyStringCache {
unsafe fn get_or_insert<'py>(&mut self, py: Python<'py>, s: &str, ascii_only: bool) -> Bound<'py, PyString> {
let hash = self.hash_builder.hash_one(s);
let hash_index = hash as usize % CAPACITY;
let set_entry = |entry: &mut Entry| {
let py_str = unsafe { pystring_fast_new_maybe_ascii(py, s, ascii_only) };
if let Some((_, old_py_str)) = entry.replace((hash, py_str.clone().unbind())) {
drop(old_py_str.into_bound(py));
}
py_str
};
for index in hash_index..hash_index.wrapping_add(5) {
if let Some(entry) = self.entries.get_mut(index) {
if let Some((entry_hash, py_str_ob)) = entry {
if *entry_hash == hash {
if py_str_ob.bind(py) == s {
return py_str_ob.bind(py).to_owned();
}
}
} else {
return set_entry(entry);
}
} else {
break;
}
}
let entry = self.entries.get_mut(hash_index).unwrap();
set_entry(entry)
}
fn usage(&self) -> usize {
self.entries.iter().filter(|e| e.is_some()).count()
}
fn clear(&mut self) {
self.entries.fill_with(|| None);
}
}
unsafe fn pystring_fast_new_maybe_ascii<'py>(py: Python<'py>, s: &str, ascii_only: bool) -> Bound<'py, PyString> {
if ascii_only {
unsafe { pystring_ascii_new(py, s) }
} else {
PyString::new(py, s)
}
}
pub unsafe fn pystring_ascii_new<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> {
unsafe {
#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))]
{
if s.len() <= 1 {
return PyString::new(py, s);
}
let py_string = Bound::from_owned_ptr(py, pyo3::ffi::PyUnicode_New(s.len() as isize, 127));
let ptr = py_string.as_ptr();
debug_assert_eq!(pyo3::ffi::PyUnicode_KIND(ptr), pyo3::ffi::PyUnicode_1BYTE_KIND);
let data_ptr = pyo3::ffi::PyUnicode_DATA(ptr).cast();
core::ptr::copy_nonoverlapping(s.as_ptr(), data_ptr, s.len());
core::ptr::write(data_ptr.add(s.len()), 0);
py_string.cast_into_unchecked()
}
#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))]
{
PyString::new(py, s)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pool_keeps_at_most_its_slots() {
let taken: Vec<Box<PyStringCache>> = (0..POOL_SLOTS + 2)
.map(|_| take_pooled_cache().unwrap_or_default())
.collect();
assert!(
take_pooled_cache().is_none(),
"the pool should be empty while they are out"
);
for cache in taken {
return_string_cache(cache);
}
let held = STRING_CACHE
.iter()
.filter(|slot| !slot.load(Ordering::Relaxed).is_null())
.count();
assert_eq!(held, POOL_SLOTS, "the pool keeps a slotful and drops the rest");
let mut again: Vec<Box<PyStringCache>> = (0..POOL_SLOTS).map(|_| take_pooled_cache().unwrap()).collect();
assert!(take_pooled_cache().is_none(), "taking them all leaves it empty");
Python::initialize();
Python::attach(|py| {
for (i, cache) in again.iter_mut().enumerate() {
let text = format!("slot number {i}");
let first = unsafe { cache.get_or_insert(py, &text, true) };
assert_eq!(first.to_str().unwrap(), text);
let second = unsafe { cache.get_or_insert(py, &text, true) };
assert!(first.is(&second), "a second lookup should hit the entry just made");
assert_eq!(cache.usage(), 1);
}
});
for cache in again {
return_string_cache(cache);
}
assert_eq!(cache_usage(), POOL_SLOTS, "every cache came back holding its string");
cache_clear();
assert_eq!(cache_usage(), 0);
}
}