use super::HsScanner;
use hyperscan::{Matching, Scratch};
use std::cell::RefCell;
use std::collections::HashMap;
use std::mem::MaybeUninit;
use std::sync::{Arc, Weak};
struct CachedScratch {
owner: Weak<()>,
scratch: Scratch,
}
const SCRATCH_TLS_PRUNE_THRESHOLD: usize = 32;
thread_local! {
static SCRATCH_TLS: RefCell<HashMap<(u64, usize), CachedScratch>> =
RefCell::new(HashMap::new());
}
#[cfg(test)]
thread_local! {
static FAIL_NEXT_SCRATCH_ALLOCATION: std::cell::Cell<Option<usize>> =
const { std::cell::Cell::new(None) };
}
#[cfg(test)]
fn fail_next_scratch_allocation_for_test(shard_idx: usize) {
FAIL_NEXT_SCRATCH_ALLOCATION.with(|target| target.set(Some(shard_idx)));
}
#[cfg(test)]
fn take_injected_scratch_failure(shard_idx: usize) -> bool {
FAIL_NEXT_SCRATCH_ALLOCATION.with(|target| {
if target.get() == Some(shard_idx) {
target.set(None);
true
} else {
false
}
})
}
fn allocate_scratch(
scanner_id: u64,
shard_idx: usize,
shard: &super::Shard,
) -> Result<Scratch, String> {
#[cfg(test)]
if take_injected_scratch_failure(shard_idx) {
return Err(format!(
"hyperscan scratch on-demand growth failed for scanner {scanner_id} \
shard {shard_idx}: injected allocation failure"
));
}
shard.db.alloc_scratch().map_err(|error| {
format!(
"hyperscan scratch on-demand growth failed for scanner {scanner_id} \
shard {shard_idx}: {error}"
)
})
}
fn take_scratch(
scanner_id: u64,
shard_idx: usize,
shard: &super::Shard,
owner: &Arc<()>,
) -> Result<Scratch, String> {
let key = (scanner_id, shard_idx);
if let Some(cached) = SCRATCH_TLS.with(|tls| tls.borrow_mut().remove(&key)) {
if std::ptr::eq(cached.owner.as_ptr(), Arc::as_ptr(owner)) {
return Ok(cached.scratch);
}
}
debug_assert!(Arc::strong_count(owner) > 0);
SCRATCH_TLS.with(|tls| prune_dead_scanner_scratch(&mut tls.borrow_mut()));
allocate_scratch(scanner_id, shard_idx, shard)
}
fn put_scratch(scanner_id: u64, shard_idx: usize, owner: &Arc<()>, scratch: Scratch) {
let key = (scanner_id, shard_idx);
SCRATCH_TLS.with(|tls| {
let mut tls = tls.borrow_mut();
if tls.len() >= SCRATCH_TLS_PRUNE_THRESHOLD {
prune_dead_scanner_scratch(&mut tls);
}
tls.insert(
key,
CachedScratch {
owner: Arc::downgrade(owner),
scratch,
},
);
});
}
fn prune_dead_scanner_scratch(tls: &mut HashMap<(u64, usize), CachedScratch>) {
tls.retain(|_, cached| cached.owner.strong_count() > 0);
}
pub(super) fn purge_scanner_scratch(scanner_id: u64) {
SCRATCH_TLS.with(|tls| {
tls.borrow_mut()
.retain(|(cached_scanner_id, _), _| *cached_scanner_id != scanner_id);
});
}
struct ScratchBatch<'a> {
scanner_id: u64,
owner: &'a Arc<()>,
initialized: usize,
slots: [MaybeUninit<Scratch>; super::MAX_COMPILE_SHARDS],
}
impl<'a> ScratchBatch<'a> {
fn acquire(scanner: &'a HsScanner) -> Result<Self, String> {
if scanner.shards.len() > super::MAX_COMPILE_SHARDS {
return Err(format!(
"hyperscan scanner has {} shards, exceeding scratch batch capacity {}",
scanner.shards.len(),
super::MAX_COMPILE_SHARDS
));
}
let mut batch = Self {
scanner_id: scanner.scanner_id,
owner: &scanner.scratch_owner,
initialized: 0,
slots: std::array::from_fn(|_| MaybeUninit::uninit()),
};
for (shard_idx, shard) in scanner.shards.iter().enumerate() {
let scratch =
take_scratch(scanner.scanner_id, shard_idx, shard, &scanner.scratch_owner)?;
batch.slots[shard_idx].write(scratch);
batch.initialized += 1;
}
Ok(batch)
}
#[inline]
fn scratch(&self, shard_idx: usize) -> &Scratch {
debug_assert!(shard_idx < self.initialized);
unsafe { self.slots.get_unchecked(shard_idx).assume_init_ref() }
}
}
impl Drop for ScratchBatch<'_> {
fn drop(&mut self) {
for shard_idx in 0..self.initialized {
let scratch = unsafe { self.slots.get_unchecked(shard_idx).assume_init_read() };
put_scratch(self.scanner_id, shard_idx, self.owner, scratch);
}
}
}
#[cfg(test)]
fn current_thread_scratch_count_for_test(scanner_id: u64) -> usize {
SCRATCH_TLS.with(|tls| {
tls.borrow()
.keys()
.filter(|(cached_scanner_id, _)| *cached_scanner_id == scanner_id)
.count()
})
}
impl HsScanner {
pub(crate) fn scan_matches_result(
&self,
text: &[u8],
mut on_match: impl FnMut(usize, usize, usize),
) -> Result<(), String> {
let scratches = ScratchBatch::acquire(self)?;
for (shard_idx, shard) in self.shards.iter().enumerate() {
if let Err(error) = shard.db.scan(
text,
scratches.scratch(shard_idx),
|id, from, to, _flags| {
on_match(id as usize, from as usize, to as usize);
Matching::Continue
},
) {
return Err(format!(
"hyperscan scan failed while executing shard {shard_idx} of {}; \
callback output from this call is incomplete and must be discarded: {error}",
self.shards.len()
));
}
}
Ok(())
}
pub(crate) fn scan_each_result(
&self,
text: &[u8],
mut on_match: impl FnMut(usize),
) -> Result<(), String> {
let scratches = ScratchBatch::acquire(self)?;
for (shard_idx, shard) in self.shards.iter().enumerate() {
if let Err(error) = shard.db.scan(
text,
scratches.scratch(shard_idx),
|id, _from, _to, _flags| {
on_match(id as usize);
Matching::Continue
},
) {
return Err(format!(
"hyperscan scan_each failed while executing shard {shard_idx} of {}; \
callback output from this call is incomplete and must be discarded: {error}",
self.shards.len()
));
}
}
Ok(())
}
pub(crate) fn scan_many_each_result<'a>(
&self,
texts: impl IntoIterator<Item = (usize, &'a [u8])>,
mut on_match: impl FnMut(usize, usize),
) -> Result<(), String> {
let scratches = ScratchBatch::acquire(self)?;
for (text_index, text) in texts {
for (shard_idx, shard) in self.shards.iter().enumerate() {
if let Err(error) = shard.db.scan(
text,
scratches.scratch(shard_idx),
|id, _from, _to, _flags| {
on_match(text_index, id as usize);
Matching::Continue
},
) {
return Err(format!(
"hyperscan batch scan failed for text {text_index} while executing shard {shard_idx} of {}; callback output from this lane is incomplete and must be discarded: {error}",
self.shards.len()
));
}
}
}
Ok(())
}
pub(crate) fn any_match_result(&self, text: &[u8]) -> Result<bool, String> {
let scratches = ScratchBatch::acquire(self)?;
for (shard_idx, shard) in self.shards.iter().enumerate() {
let mut hit = false;
if let Err(error) = shard.db.scan(
text,
scratches.scratch(shard_idx),
|_id, _from, _to, _flags| {
hit = true;
Matching::Terminate
},
) {
if !hit {
return Err(format!(
"hyperscan any_match failed before a match was observed while executing \
shard {shard_idx} of {}: {error}",
self.shards.len()
));
}
}
if hit {
return Ok(true);
}
}
Ok(false)
}
pub(crate) fn pattern_info(&self, hs_id: usize) -> Option<(usize, usize, bool)> {
self.pattern_map
.get(hs_id)
.map(|&(_, det_idx, pat_idx, has_group)| (det_idx, pat_idx, has_group))
}
pub(crate) fn pattern_count(&self) -> usize {
self.pattern_map.len()
}
#[cfg(test)]
pub(crate) fn shard_count(&self) -> usize {
self.shards.len()
}
}
#[cfg(test)]
#[path = "../../../tests/unit/simd_scratch_lifetime.rs"]
mod scratch_lifetime;