use fsqlite_types::sync_primitives::Instant;
use std::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering};
use fsqlite_types::sync_primitives::Mutex;
use serde::Serialize;
pub const MAX_FC_THREADS: usize = 64;
const SLOT_EMPTY: u64 = 0;
const RESULT_BIT: u64 = 1 << 63;
const SPIN_BEFORE_YIELD: u32 = 1024;
const HTM_NOT_PROBED: u8 = 0;
const HTM_AVAILABLE: u8 = 1;
const HTM_UNAVAILABLE: u8 = 2;
const HTM_BLACKLISTED: u8 = 3;
const HTM_DISABLED: u8 = 4;
const HTM_USER_DISABLED: u8 = 5;
#[allow(dead_code)] const HTM_DISABLE_THRESHOLD: u32 = 5000;
const HTM_COOLDOWN_INITIAL_MS: u64 = 5000;
const HTM_COOLDOWN_MAX_MS: u64 = 60_000;
#[allow(dead_code)] const HTM_EWMA_ALPHA: u32 = 3000;
#[allow(dead_code)] const HTM_EWMA_WINDOW_SIZE: u64 = 1000;
#[allow(dead_code)] const MAX_HTM_RETRIES: u32 = 3;
pub struct HtmGuard {
state: AtomicU8,
ewma_abort_rate: AtomicU32,
window_attempts: AtomicU64,
window_aborts: AtomicU64,
#[allow(dead_code)] window_start_ns: AtomicU64,
disable_count: AtomicU32,
last_disable_ns: AtomicU64,
}
impl HtmGuard {
const fn new() -> Self {
Self {
state: AtomicU8::new(HTM_NOT_PROBED),
ewma_abort_rate: AtomicU32::new(0),
window_attempts: AtomicU64::new(0),
window_aborts: AtomicU64::new(0),
window_start_ns: AtomicU64::new(0),
disable_count: AtomicU32::new(0),
last_disable_ns: AtomicU64::new(0),
}
}
fn probe_cpu(&self) {
let new_state = HTM_UNAVAILABLE;
let _ = self.state.compare_exchange(
HTM_NOT_PROBED,
new_state,
Ordering::AcqRel,
Ordering::Relaxed,
);
tracing::info!(
target: "fsqlite::htm",
event = "cpu_probe",
tsx_available = false,
tme_available = false,
stepping = "unknown",
known_buggy = false,
phase = "phase1_stub",
);
}
#[inline]
fn should_attempt(&self) -> bool {
let state = self.state.load(Ordering::Relaxed);
if state == HTM_NOT_PROBED {
self.probe_cpu();
return self.state.load(Ordering::Relaxed) == HTM_AVAILABLE;
}
if state == HTM_DISABLED {
self.maybe_reenable();
}
self.state.load(Ordering::Relaxed) == HTM_AVAILABLE
}
#[allow(dead_code)] fn record_attempt(&self) {
record_htm_attempt();
let attempts = self.window_attempts.fetch_add(1, Ordering::Relaxed) + 1;
if attempts >= HTM_EWMA_WINDOW_SIZE {
self.update_ewma();
}
}
#[allow(dead_code)] fn record_abort(&self, status: u32) {
let classification = record_htm_abort_status(status);
self.window_aborts.fetch_add(1, Ordering::Relaxed);
tracing::debug!(
target: "fsqlite::htm",
event = "xabort",
abort_code = status,
reason = match classification.reason {
HtmAbortReason::Conflict => "conflict",
HtmAbortReason::Capacity => "capacity",
HtmAbortReason::Explicit => "explicit",
HtmAbortReason::Other => "other",
},
retryable = classification.retryable,
);
}
#[allow(dead_code)] fn update_ewma(&self) {
let attempts = self.window_attempts.swap(0, Ordering::Relaxed);
let aborts = self.window_aborts.swap(0, Ordering::Relaxed);
if attempts == 0 {
return;
}
#[allow(clippy::cast_possible_truncation)]
let new_rate_fp = ((aborts * 10000) / attempts) as u32;
let old_ewma = self.ewma_abort_rate.load(Ordering::Relaxed);
let updated = (HTM_EWMA_ALPHA * new_rate_fp + (10000 - HTM_EWMA_ALPHA) * old_ewma) / 10000;
self.ewma_abort_rate.store(updated, Ordering::Relaxed);
if updated > HTM_DISABLE_THRESHOLD && self.state.load(Ordering::Relaxed) == HTM_AVAILABLE {
self.dynamic_disable(updated);
}
}
#[allow(dead_code)] fn dynamic_disable(&self, abort_rate: u32) {
let prev = self.state.compare_exchange(
HTM_AVAILABLE,
HTM_DISABLED,
Ordering::AcqRel,
Ordering::Relaxed,
);
if prev.is_ok() {
self.disable_count.fetch_add(1, Ordering::Relaxed);
#[allow(clippy::cast_possible_truncation)]
let now_ns = Instant::now().elapsed().as_nanos() as u64;
self.last_disable_ns.store(now_ns, Ordering::Relaxed);
tracing::warn!(
target: "fsqlite::htm",
event = "dynamic_disable",
abort_rate_fp = abort_rate,
abort_rate_pct = abort_rate as f64 / 100.0,
threshold_pct = HTM_DISABLE_THRESHOLD as f64 / 100.0,
disable_count = self.disable_count.load(Ordering::Relaxed),
);
}
}
fn maybe_reenable(&self) {
let dc = self.disable_count.load(Ordering::Relaxed);
let cooldown_ms = HTM_COOLDOWN_INITIAL_MS
.saturating_mul(1u64.checked_shl(dc).unwrap_or(u64::MAX))
.min(HTM_COOLDOWN_MAX_MS);
#[allow(clippy::cast_possible_truncation)]
let now_ns = Instant::now().elapsed().as_nanos() as u64;
let disable_ns = self.last_disable_ns.load(Ordering::Relaxed);
let elapsed_ms = now_ns.saturating_sub(disable_ns) / 1_000_000;
if elapsed_ms >= cooldown_ms {
self.ewma_abort_rate.store(0, Ordering::Relaxed);
self.window_attempts.store(0, Ordering::Relaxed);
self.window_aborts.store(0, Ordering::Relaxed);
let _ = self.state.compare_exchange(
HTM_DISABLED,
HTM_AVAILABLE,
Ordering::AcqRel,
Ordering::Relaxed,
);
tracing::info!(
target: "fsqlite::htm",
event = "reenable",
cooldown_ms,
disable_count = dc,
);
}
}
pub fn pragma_disable(&self) -> u8 {
self.state.swap(HTM_USER_DISABLED, Ordering::AcqRel)
}
pub fn pragma_enable(&self) {
let current = self.state.load(Ordering::Relaxed);
if current == HTM_USER_DISABLED {
self.state.store(HTM_NOT_PROBED, Ordering::Release);
}
}
#[must_use]
pub fn state_name(&self) -> &'static str {
match self.state.load(Ordering::Relaxed) {
HTM_NOT_PROBED => "not_probed",
HTM_AVAILABLE => "available",
HTM_UNAVAILABLE => "unavailable",
HTM_BLACKLISTED => "blacklisted",
HTM_DISABLED => "disabled",
HTM_USER_DISABLED => "user_disabled",
_ => "unknown",
}
}
#[must_use]
pub fn ewma_pct(&self) -> f64 {
f64::from(self.ewma_abort_rate.load(Ordering::Relaxed)) / 100.0
}
#[must_use]
pub fn disable_count(&self) -> u32 {
self.disable_count.load(Ordering::Relaxed)
}
}
static FC_BATCHES_TOTAL: AtomicU64 = AtomicU64::new(0);
static FC_OPS_TOTAL: AtomicU64 = AtomicU64::new(0);
static FC_BATCH_SIZE_SUM: AtomicU64 = AtomicU64::new(0);
static FC_BATCH_SIZE_MAX: AtomicU64 = AtomicU64::new(0);
static FC_WAIT_NS_TOTAL: AtomicU64 = AtomicU64::new(0);
static FC_WAIT_NS_MAX: AtomicU64 = AtomicU64::new(0);
static FC_HTM_ATTEMPTS: AtomicU64 = AtomicU64::new(0);
static FC_HTM_ABORTS_CONFLICT: AtomicU64 = AtomicU64::new(0);
static FC_HTM_ABORTS_CAPACITY: AtomicU64 = AtomicU64::new(0);
static FC_HTM_ABORTS_EXPLICIT: AtomicU64 = AtomicU64::new(0);
static FC_HTM_ABORTS_OTHER: AtomicU64 = AtomicU64::new(0);
const XABORT_EXPLICIT: u32 = 1 << 0;
const XABORT_RETRY: u32 = 1 << 1;
const XABORT_CONFLICT: u32 = 1 << 2;
const XABORT_CAPACITY: u32 = 1 << 3;
const XABORT_DEBUG: u32 = 1 << 4;
const XABORT_NESTED: u32 = 1 << 5;
const XABORT_CODE_SHIFT: u32 = 24;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HtmAbortReason {
Conflict,
Capacity,
Explicit,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct HtmAbortClassification {
reason: HtmAbortReason,
retryable: bool,
explicit_code: Option<u8>,
debug: bool,
nested: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct FlatCombiningMetrics {
pub fsqlite_flat_combining_batches_total: u64,
pub fsqlite_flat_combining_ops_total: u64,
pub fsqlite_flat_combining_batch_size_sum: u64,
pub fsqlite_flat_combining_batch_size_max: u64,
pub fsqlite_flat_combining_wait_ns_total: u64,
pub fsqlite_flat_combining_wait_ns_max: u64,
pub fsqlite_htm_attempts: u64,
pub fsqlite_htm_aborts_conflict: u64,
pub fsqlite_htm_aborts_capacity: u64,
pub fsqlite_htm_aborts_explicit: u64,
pub fsqlite_htm_aborts_other: u64,
pub fsqlite_htm_state: &'static str,
pub fsqlite_htm_ewma_abort_rate_pct: f64,
pub fsqlite_htm_disable_count: u32,
}
#[must_use]
pub fn flat_combining_metrics() -> FlatCombiningMetrics {
flat_combining_metrics_with_htm("unavailable", 0.0, 0)
}
#[must_use]
pub fn flat_combining_metrics_from(combiner: &FlatCombiner) -> FlatCombiningMetrics {
flat_combining_metrics_with_htm(
combiner.htm_guard.state_name(),
combiner.htm_guard.ewma_pct(),
combiner.htm_guard.disable_count(),
)
}
fn flat_combining_metrics_with_htm(
state: &'static str,
ewma_pct: f64,
disable_count: u32,
) -> FlatCombiningMetrics {
FlatCombiningMetrics {
fsqlite_flat_combining_batches_total: FC_BATCHES_TOTAL.load(Ordering::Relaxed),
fsqlite_flat_combining_ops_total: FC_OPS_TOTAL.load(Ordering::Relaxed),
fsqlite_flat_combining_batch_size_sum: FC_BATCH_SIZE_SUM.load(Ordering::Relaxed),
fsqlite_flat_combining_batch_size_max: FC_BATCH_SIZE_MAX.load(Ordering::Relaxed),
fsqlite_flat_combining_wait_ns_total: FC_WAIT_NS_TOTAL.load(Ordering::Relaxed),
fsqlite_flat_combining_wait_ns_max: FC_WAIT_NS_MAX.load(Ordering::Relaxed),
fsqlite_htm_attempts: FC_HTM_ATTEMPTS.load(Ordering::Relaxed),
fsqlite_htm_aborts_conflict: FC_HTM_ABORTS_CONFLICT.load(Ordering::Relaxed),
fsqlite_htm_aborts_capacity: FC_HTM_ABORTS_CAPACITY.load(Ordering::Relaxed),
fsqlite_htm_aborts_explicit: FC_HTM_ABORTS_EXPLICIT.load(Ordering::Relaxed),
fsqlite_htm_aborts_other: FC_HTM_ABORTS_OTHER.load(Ordering::Relaxed),
fsqlite_htm_state: state,
fsqlite_htm_ewma_abort_rate_pct: ewma_pct,
fsqlite_htm_disable_count: disable_count,
}
}
pub fn reset_flat_combining_metrics() {
FC_BATCHES_TOTAL.store(0, Ordering::Relaxed);
FC_OPS_TOTAL.store(0, Ordering::Relaxed);
FC_BATCH_SIZE_SUM.store(0, Ordering::Relaxed);
FC_BATCH_SIZE_MAX.store(0, Ordering::Relaxed);
FC_WAIT_NS_TOTAL.store(0, Ordering::Relaxed);
FC_WAIT_NS_MAX.store(0, Ordering::Relaxed);
FC_HTM_ATTEMPTS.store(0, Ordering::Relaxed);
FC_HTM_ABORTS_CONFLICT.store(0, Ordering::Relaxed);
FC_HTM_ABORTS_CAPACITY.store(0, Ordering::Relaxed);
FC_HTM_ABORTS_EXPLICIT.store(0, Ordering::Relaxed);
FC_HTM_ABORTS_OTHER.store(0, Ordering::Relaxed);
}
fn update_max(metric: &AtomicU64, val: u64) {
let mut prev = metric.load(Ordering::Relaxed);
while val > prev {
match metric.compare_exchange_weak(prev, val, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => break,
Err(actual) => prev = actual,
}
}
}
const fn classify_htm_abort_status(status: u32) -> HtmAbortClassification {
let reason = if (status & XABORT_CONFLICT) != 0 {
HtmAbortReason::Conflict
} else if (status & XABORT_CAPACITY) != 0 {
HtmAbortReason::Capacity
} else if (status & XABORT_EXPLICIT) != 0 {
HtmAbortReason::Explicit
} else {
HtmAbortReason::Other
};
let explicit_code = if (status & XABORT_EXPLICIT) != 0 {
Some(((status >> XABORT_CODE_SHIFT) & 0xff) as u8)
} else {
None
};
HtmAbortClassification {
reason,
retryable: (status & XABORT_RETRY) != 0,
explicit_code,
debug: (status & XABORT_DEBUG) != 0,
nested: (status & XABORT_NESTED) != 0,
}
}
fn record_htm_attempt() {
FC_HTM_ATTEMPTS.fetch_add(1, Ordering::Relaxed);
}
fn record_htm_abort_status(status: u32) -> HtmAbortClassification {
let classification = classify_htm_abort_status(status);
match classification.reason {
HtmAbortReason::Conflict => {
FC_HTM_ABORTS_CONFLICT.fetch_add(1, Ordering::Relaxed);
}
HtmAbortReason::Capacity => {
FC_HTM_ABORTS_CAPACITY.fetch_add(1, Ordering::Relaxed);
}
HtmAbortReason::Explicit => {
FC_HTM_ABORTS_EXPLICIT.fetch_add(1, Ordering::Relaxed);
}
HtmAbortReason::Other => {
FC_HTM_ABORTS_OTHER.fetch_add(1, Ordering::Relaxed);
}
}
classification
}
pub fn note_htm_attempt() {
record_htm_attempt();
}
pub fn note_htm_abort(status: u32) {
let _ = record_htm_abort_status(status);
}
struct FcSlot {
state: AtomicU64,
payload: AtomicU64,
}
impl FcSlot {
fn new() -> Self {
Self {
state: AtomicU64::new(SLOT_EMPTY),
payload: AtomicU64::new(0),
}
}
}
pub struct FlatCombiner {
value: AtomicU64,
slots: [FcSlot; MAX_FC_THREADS],
owners: [AtomicU64; MAX_FC_THREADS],
combiner_lock: Mutex<()>,
htm_guard: HtmGuard,
}
pub const OP_ADD: u64 = 1;
pub const OP_READ: u64 = 2;
impl FlatCombiner {
pub fn new(initial: u64) -> Self {
Self {
value: AtomicU64::new(initial),
slots: std::array::from_fn(|_| FcSlot::new()),
owners: std::array::from_fn(|_| AtomicU64::new(0)),
combiner_lock: Mutex::new(()),
htm_guard: HtmGuard::new(),
}
}
#[must_use]
pub fn htm_guard(&self) -> &HtmGuard {
&self.htm_guard
}
pub fn register(&self) -> Option<FcHandle<'_>> {
let tid = {
let t = std::thread::current().id();
let s = format!("{t:?}");
let mut h = 1u64;
for b in s.bytes() {
h = h.wrapping_mul(31).wrapping_add(u64::from(b));
}
if h == 0 { 1 } else { h }
};
for i in 0..MAX_FC_THREADS {
if self.owners[i]
.compare_exchange(0, tid, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
return Some(FcHandle {
combiner: self,
slot: i,
});
}
}
None
}
#[must_use]
pub fn value(&self) -> u64 {
self.value.load(Ordering::Relaxed)
}
#[must_use]
pub fn active_threads(&self) -> usize {
self.owners
.iter()
.filter(|o| o.load(Ordering::Relaxed) != 0)
.count()
}
fn combine_locked(&self) {
let mut batch_size = 0u64;
let mut current = self.value.load(Ordering::Acquire);
for i in 0..MAX_FC_THREADS {
let state = self.slots[i].state.load(Ordering::Acquire);
if state == SLOT_EMPTY || (state & RESULT_BIT) != 0 {
continue; }
let op = state;
let arg = self.slots[i].payload.load(Ordering::Acquire);
batch_size += 1;
let result = match op {
OP_ADD => {
current = current.wrapping_add(arg);
current
}
OP_READ => current,
_ => 0, };
self.slots[i].payload.store(result, Ordering::Release);
self.slots[i]
.state
.store(RESULT_BIT | op, Ordering::Release);
}
self.value.store(current, Ordering::Release);
if batch_size > 0 {
FC_BATCHES_TOTAL.fetch_add(1, Ordering::Relaxed);
FC_OPS_TOTAL.fetch_add(batch_size, Ordering::Relaxed);
FC_BATCH_SIZE_SUM.fetch_add(batch_size, Ordering::Relaxed);
update_max(&FC_BATCH_SIZE_MAX, batch_size);
tracing::debug!(
target: "fsqlite.flat_combine",
batch_size,
"flat_combine_batch"
);
}
}
}
#[allow(clippy::missing_fields_in_debug)]
impl std::fmt::Debug for FlatCombiner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlatCombiner")
.field("value", &self.value.load(Ordering::Relaxed))
.field("active_threads", &self.active_threads())
.finish_non_exhaustive()
}
}
pub struct FcHandle<'a> {
combiner: &'a FlatCombiner,
slot: usize,
}
impl FcHandle<'_> {
pub fn apply(&self, op: u64, arg: u64) -> u64 {
let start = Instant::now();
self.combiner.slots[self.slot]
.payload
.store(arg, Ordering::Release);
self.combiner.slots[self.slot]
.state
.store(op, Ordering::Release);
if self.combiner.htm_guard.should_attempt() {
}
if let Some(_guard) = self.combiner.combiner_lock.try_lock() {
self.combiner.combine_locked();
}
let mut spins = 0u32;
loop {
let state = self.combiner.slots[self.slot].state.load(Ordering::Acquire);
if (state & RESULT_BIT) != 0 {
let result = self.combiner.slots[self.slot]
.payload
.load(Ordering::Acquire);
self.combiner.slots[self.slot]
.state
.store(SLOT_EMPTY, Ordering::Release);
#[allow(clippy::cast_possible_truncation)]
let elapsed_ns = start.elapsed().as_nanos() as u64;
FC_WAIT_NS_TOTAL.fetch_add(elapsed_ns, Ordering::Relaxed);
update_max(&FC_WAIT_NS_MAX, elapsed_ns);
return result;
}
spins += 1;
if spins < SPIN_BEFORE_YIELD {
std::hint::spin_loop();
} else {
if let Some(_guard) = self.combiner.combiner_lock.try_lock() {
self.combiner.combine_locked();
} else {
std::thread::yield_now();
}
spins = 0;
}
}
}
pub fn add(&self, val: u64) -> u64 {
self.apply(OP_ADD, val)
}
pub fn read(&self) -> u64 {
self.apply(OP_READ, 0)
}
#[must_use]
pub fn slot(&self) -> usize {
self.slot
}
}
impl Drop for FcHandle<'_> {
fn drop(&mut self) {
self.combiner.slots[self.slot]
.state
.store(SLOT_EMPTY, Ordering::Release);
self.combiner.owners[self.slot].store(0, Ordering::Release);
}
}
pub const MAX_FC_SHARDS: usize = 8;
pub struct ShardedFlatCombiner {
shards: [FlatCombiner; MAX_FC_SHARDS],
num_shards: usize,
}
impl ShardedFlatCombiner {
#[must_use]
pub fn new(initial_per_shard: u64) -> Self {
let num_shards = std::thread::available_parallelism()
.map(|p| p.get().min(MAX_FC_SHARDS))
.unwrap_or(MAX_FC_SHARDS);
Self {
shards: std::array::from_fn(|_| FlatCombiner::new(initial_per_shard)),
num_shards,
}
}
#[must_use]
pub fn with_shards(num_shards: usize, initial_per_shard: u64) -> Self {
let effective = num_shards.clamp(1, MAX_FC_SHARDS);
Self {
shards: std::array::from_fn(|_| FlatCombiner::new(initial_per_shard)),
num_shards: effective,
}
}
#[must_use]
pub fn num_shards(&self) -> usize {
self.num_shards
}
#[inline]
fn shard_index(&self, shard_key: u64) -> usize {
(shard_key as usize) % self.num_shards
}
#[inline]
pub fn shard(&self, shard_key: u64) -> &FlatCombiner {
&self.shards[self.shard_index(shard_key)]
}
pub fn register(&self, shard_key: u64) -> Option<ShardedFcHandle<'_>> {
let idx = self.shard_index(shard_key);
self.shards[idx].register().map(|inner| ShardedFcHandle {
inner,
shard_idx: idx,
})
}
#[must_use]
pub fn total_value(&self) -> u64 {
self.shards[..self.num_shards]
.iter()
.map(FlatCombiner::value)
.sum()
}
#[must_use]
pub fn total_active_threads(&self) -> usize {
self.shards[..self.num_shards]
.iter()
.map(FlatCombiner::active_threads)
.sum()
}
#[must_use]
pub fn shard_values(&self) -> Vec<u64> {
self.shards[..self.num_shards]
.iter()
.map(FlatCombiner::value)
.collect()
}
}
impl std::fmt::Debug for ShardedFlatCombiner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShardedFlatCombiner")
.field("num_shards", &self.num_shards)
.field("total_value", &self.total_value())
.finish_non_exhaustive()
}
}
pub struct ShardedFcHandle<'a> {
inner: FcHandle<'a>,
shard_idx: usize,
}
impl ShardedFcHandle<'_> {
pub fn apply(&self, op: u64, arg: u64) -> u64 {
self.inner.apply(op, arg)
}
pub fn add(&self, val: u64) -> u64 {
self.inner.add(val)
}
pub fn read(&self) -> u64 {
self.inner.read()
}
#[must_use]
pub fn shard_index(&self) -> usize {
self.shard_idx
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Duration;
#[test]
fn register_unregister() {
let fc = FlatCombiner::new(0);
assert_eq!(fc.active_threads(), 0);
let h1 = fc.register().unwrap();
assert_eq!(fc.active_threads(), 1);
let h2 = fc.register().unwrap();
assert_eq!(fc.active_threads(), 2);
drop(h1);
assert_eq!(fc.active_threads(), 1);
drop(h2);
assert_eq!(fc.active_threads(), 0);
}
#[test]
fn single_thread_add() {
let fc = FlatCombiner::new(0);
let h = fc.register().unwrap();
let r1 = h.add(10);
assert_eq!(r1, 10);
let r2 = h.add(20);
assert_eq!(r2, 30);
let r3 = h.read();
assert_eq!(r3, 30);
assert_eq!(fc.value(), 30);
drop(h);
}
#[test]
fn single_thread_sequential() {
let fc = FlatCombiner::new(100);
let h = fc.register().unwrap();
for i in 1..=50 {
let result = h.add(1);
assert_eq!(result, 100 + i);
}
assert_eq!(h.read(), 150);
drop(h);
}
#[test]
fn concurrent_adds_correct_total() {
let fc = Arc::new(FlatCombiner::new(0));
let barrier = Arc::new(Barrier::new(4));
let mut handles = Vec::new();
for _ in 0..4 {
let f = Arc::clone(&fc);
let b = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
let h = f.register().unwrap();
b.wait();
for _ in 0..500 {
h.add(1);
}
drop(h);
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(fc.value(), 2000, "4 threads * 500 adds = 2000");
}
#[test]
#[allow(clippy::many_single_char_names)]
fn concurrent_stress_no_lost_updates() {
let fc = Arc::new(FlatCombiner::new(0));
let stop = Arc::new(AtomicBool::new(false));
let barrier = Arc::new(Barrier::new(4));
let total_adds = Arc::new(AtomicU64::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
let f = Arc::clone(&fc);
let s = Arc::clone(&stop);
let b = Arc::clone(&barrier);
let t = Arc::clone(&total_adds);
handles.push(thread::spawn(move || {
let h = f.register().unwrap();
b.wait();
let mut local = 0u64;
while !s.load(Ordering::Relaxed) {
h.add(1);
local += 1;
}
t.fetch_add(local, Ordering::Relaxed);
drop(h);
}));
}
thread::sleep(Duration::from_millis(300));
stop.store(true, Ordering::Release);
for h in handles {
h.join().unwrap();
}
let expected = total_adds.load(Ordering::Relaxed);
let actual = fc.value();
assert_eq!(
actual, expected,
"accumulator {actual} != total submitted {expected}"
);
}
#[test]
fn metrics_track_batches() {
let before = flat_combining_metrics();
let fc = FlatCombiner::new(0);
let h = fc.register().unwrap();
h.add(1);
h.add(2);
h.add(3);
let after = flat_combining_metrics();
let batch_delta = after.fsqlite_flat_combining_batches_total
- before.fsqlite_flat_combining_batches_total;
let ops_delta =
after.fsqlite_flat_combining_ops_total - before.fsqlite_flat_combining_ops_total;
assert!(
batch_delta >= 3,
"expected at least 3 batches (single thread = 1 op per batch), got {batch_delta}"
);
assert!(ops_delta >= 3, "expected at least 3 ops, got {ops_delta}");
drop(h);
}
#[test]
fn batching_under_contention() {
let before = flat_combining_metrics();
let fc = Arc::new(FlatCombiner::new(0));
let barrier = Arc::new(Barrier::new(8));
let mut handles = Vec::new();
for _ in 0..8 {
let f = Arc::clone(&fc);
let b = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
let h = f.register().unwrap();
b.wait();
for _ in 0..200 {
h.add(1);
}
drop(h);
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(fc.value(), 1600, "8 threads * 200 = 1600");
let after = flat_combining_metrics();
let batches_delta = after.fsqlite_flat_combining_batches_total
- before.fsqlite_flat_combining_batches_total;
let ops_delta =
after.fsqlite_flat_combining_ops_total - before.fsqlite_flat_combining_ops_total;
let avg_batch = if batches_delta > 0 {
ops_delta as f64 / batches_delta as f64
} else {
0.0
};
println!(
"[flat_combining] batches={batches_delta} ops={ops_delta} avg_batch={avg_batch:.2} max_batch={}",
after.fsqlite_flat_combining_batch_size_max
);
}
#[test]
fn read_sees_latest_value() {
let fc = Arc::new(FlatCombiner::new(0));
let barrier = Arc::new(Barrier::new(2));
let f = Arc::clone(&fc);
let b = Arc::clone(&barrier);
let writer = thread::spawn(move || {
let h = f.register().unwrap();
b.wait();
for _ in 0..100 {
h.add(1);
}
drop(h);
});
let f = Arc::clone(&fc);
let b2 = Arc::clone(&barrier);
let reader = thread::spawn(move || {
let h = f.register().unwrap();
b2.wait();
thread::sleep(Duration::from_millis(50));
let v = h.read();
drop(h);
v
});
writer.join().unwrap();
let last_read = reader.join().unwrap();
assert!(last_read <= 100, "read {last_read} > 100");
}
#[test]
fn no_starvation_bounded_wait() {
let fc = Arc::new(FlatCombiner::new(0));
let barrier = Arc::new(Barrier::new(4));
let mut handles = Vec::new();
for _ in 0..4 {
let f = Arc::clone(&fc);
let b = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
let h = f.register().unwrap();
b.wait();
let start = Instant::now();
for _ in 0..100 {
h.add(1);
}
let elapsed = start.elapsed();
drop(h);
elapsed
}));
}
for h in handles {
let elapsed = h.join().unwrap();
assert!(
elapsed < Duration::from_secs(5),
"thread took too long: {elapsed:?} — possible starvation"
);
}
assert_eq!(fc.value(), 400);
}
#[test]
fn debug_format() {
let fc = FlatCombiner::new(42);
let dbg = format!("{fc:?}");
assert!(dbg.contains("FlatCombiner"));
assert!(dbg.contains("42"));
}
#[test]
fn htm_guard_defaults_to_unavailable() {
let guard = HtmGuard::new();
assert_eq!(guard.state.load(Ordering::Relaxed), HTM_NOT_PROBED);
assert!(!guard.should_attempt());
assert_eq!(guard.state_name(), "unavailable");
}
#[test]
fn htm_guard_probe_is_idempotent() {
let guard = HtmGuard::new();
guard.probe_cpu();
let state1 = guard.state.load(Ordering::Relaxed);
guard.probe_cpu();
let state2 = guard.state.load(Ordering::Relaxed);
assert_eq!(state1, state2);
assert_eq!(state1, HTM_UNAVAILABLE);
}
#[test]
fn htm_guard_pragma_disable_enable() {
let guard = HtmGuard::new();
guard.probe_cpu();
let prev = guard.pragma_disable();
assert_eq!(prev, HTM_UNAVAILABLE);
assert_eq!(guard.state_name(), "user_disabled");
guard.pragma_enable();
assert!(!guard.should_attempt()); assert_eq!(guard.state_name(), "unavailable");
}
#[test]
fn htm_guard_ewma_computation() {
let guard = HtmGuard::new();
guard.state.store(HTM_AVAILABLE, Ordering::Relaxed);
guard.window_attempts.store(1000, Ordering::Relaxed);
guard.window_aborts.store(800, Ordering::Relaxed);
guard.update_ewma();
let ewma = guard.ewma_abort_rate.load(Ordering::Relaxed);
assert_eq!(ewma, 2400);
guard.window_attempts.store(1000, Ordering::Relaxed);
guard.window_aborts.store(800, Ordering::Relaxed);
guard.update_ewma();
let ewma2 = guard.ewma_abort_rate.load(Ordering::Relaxed);
assert_eq!(ewma2, 4080);
guard.window_attempts.store(1000, Ordering::Relaxed);
guard.window_aborts.store(800, Ordering::Relaxed);
guard.update_ewma();
let ewma3 = guard.ewma_abort_rate.load(Ordering::Relaxed);
assert_eq!(ewma3, 5256);
assert_eq!(guard.state.load(Ordering::Relaxed), HTM_DISABLED);
assert_eq!(guard.disable_count(), 1);
}
#[test]
fn htm_guard_record_abort_updates_window() {
let guard = HtmGuard::new();
guard.record_abort(XABORT_CONFLICT | XABORT_RETRY);
assert_eq!(guard.window_aborts.load(Ordering::Relaxed), 1);
}
#[test]
fn htm_guard_metrics_in_flat_combiner() {
let fc = FlatCombiner::new(0);
assert_eq!(fc.htm_guard().state_name(), "not_probed");
let metrics = flat_combining_metrics_from(&fc);
assert_eq!(metrics.fsqlite_htm_state, "not_probed");
assert!((metrics.fsqlite_htm_ewma_abort_rate_pct - 0.0).abs() < f64::EPSILON);
assert_eq!(metrics.fsqlite_htm_disable_count, 0);
}
#[test]
fn htm_guard_in_apply_path() {
let fc = FlatCombiner::new(0);
let h = fc.register().unwrap();
let result = h.add(42);
assert_eq!(result, 42);
assert_eq!(fc.htm_guard().state_name(), "unavailable");
}
#[test]
fn classify_htm_abort_status_prefers_conflict() {
let status =
XABORT_CONFLICT | XABORT_CAPACITY | XABORT_RETRY | XABORT_DEBUG | XABORT_NESTED;
let classification = classify_htm_abort_status(status);
assert_eq!(classification.reason, HtmAbortReason::Conflict);
assert!(classification.retryable);
assert!(classification.debug);
assert!(classification.nested);
assert_eq!(classification.explicit_code, None);
}
#[test]
fn classify_htm_abort_status_extracts_explicit_code() {
let status = XABORT_EXPLICIT | XABORT_RETRY | (0x2a_u32 << XABORT_CODE_SHIFT);
let classification = classify_htm_abort_status(status);
assert_eq!(classification.reason, HtmAbortReason::Explicit);
assert!(classification.retryable);
assert_eq!(classification.explicit_code, Some(0x2a));
}
#[test]
fn record_htm_abort_status_updates_counters() {
reset_flat_combining_metrics();
record_htm_attempt();
record_htm_attempt();
record_htm_attempt();
record_htm_attempt();
let conflict = record_htm_abort_status(XABORT_CONFLICT | XABORT_RETRY);
let capacity = record_htm_abort_status(XABORT_CAPACITY);
let explicit = record_htm_abort_status(XABORT_EXPLICIT | (0x07_u32 << XABORT_CODE_SHIFT));
let other = record_htm_abort_status(0);
assert_eq!(conflict.reason, HtmAbortReason::Conflict);
assert_eq!(capacity.reason, HtmAbortReason::Capacity);
assert_eq!(explicit.reason, HtmAbortReason::Explicit);
assert_eq!(explicit.explicit_code, Some(0x07));
assert_eq!(other.reason, HtmAbortReason::Other);
let metrics = flat_combining_metrics();
assert_eq!(metrics.fsqlite_htm_attempts, 4);
assert_eq!(metrics.fsqlite_htm_aborts_conflict, 1);
assert_eq!(metrics.fsqlite_htm_aborts_capacity, 1);
assert_eq!(metrics.fsqlite_htm_aborts_explicit, 1);
assert_eq!(metrics.fsqlite_htm_aborts_other, 1);
}
#[test]
fn sharded_combiner_basic() {
let sfc = ShardedFlatCombiner::with_shards(4, 0);
assert_eq!(sfc.num_shards(), 4);
assert_eq!(sfc.total_value(), 0);
assert_eq!(sfc.total_active_threads(), 0);
}
#[test]
fn sharded_combiner_register_different_shards() {
let sfc = ShardedFlatCombiner::with_shards(4, 0);
let h0 = sfc.register(0).unwrap();
let h1 = sfc.register(1).unwrap();
let h4 = sfc.register(4).unwrap();
assert_eq!(h0.shard_index(), 0);
assert_eq!(h1.shard_index(), 1);
assert_eq!(h4.shard_index(), 0);
drop(h0);
drop(h1);
drop(h4);
}
#[test]
fn sharded_combiner_adds_to_correct_shard() {
let sfc = ShardedFlatCombiner::with_shards(4, 0);
let h0 = sfc.register(0).unwrap();
let h1 = sfc.register(1).unwrap();
h0.add(100);
h1.add(200);
h0.add(50);
let values = sfc.shard_values();
assert_eq!(values[0], 150);
assert_eq!(values[1], 200);
assert_eq!(sfc.total_value(), 350);
drop(h0);
drop(h1);
}
#[test]
fn sharded_combiner_concurrent_parallel_shards() {
let sfc = Arc::new(ShardedFlatCombiner::with_shards(8, 0));
let barrier = Arc::new(Barrier::new(8));
let mut handles = Vec::new();
for shard_key in 0..8u64 {
let s = Arc::clone(&sfc);
let b = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
let h = s.register(shard_key).unwrap();
b.wait();
for _ in 0..500 {
h.add(1);
}
drop(h);
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(sfc.total_value(), 4000, "8 shards * 500 = 4000");
}
#[test]
fn sharded_combiner_clamp_shards() {
let sfc1 = ShardedFlatCombiner::with_shards(0, 0);
assert_eq!(sfc1.num_shards(), 1);
let sfc2 = ShardedFlatCombiner::with_shards(100, 0);
assert_eq!(sfc2.num_shards(), MAX_FC_SHARDS);
}
}