//===-- tsan_adaptive_delay.h -----------------------------------*- C++ -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // This file is a part of ThreadSanitizer (TSan), a race detector. // //===----------------------------------------------------------------------===// #include "tsan_adaptive_delay.h" #include "interception/interception.h" #include "sanitizer_common/sanitizer_allocator_internal.h" #include "sanitizer_common/sanitizer_common.h" #include "sanitizer_common/sanitizer_errno_codes.h" #include "tsan_interface.h" #include "tsan_rtl.h" namespace __tsan { namespace { // ============================================================================= // DelaySpec: Represents a delay configuration parsed from flag strings // ============================================================================= // // Delay can be specified as: // - "spin=N" : Spin for up to N cycles (very short delays) // - "yield" : Call sched_yield() once // - "sleep_us=N" : Sleep for up to N microseconds enum class DelayType { Spin, Yield, SleepUs }; struct DelaySpec { DelayType type; int value; // spin cycles or sleep_us value; ignored for yield // Both estimates below are used internally as a very rough estimate for // delay overhead calculation, to cap the overall delay to the // adaptive_delay_aggressiveness option. They're not intended to be 100% // accurate on any or all architectures/operating systems, or for use in any // other contexts. // // Estimated nanoseconds per spin cycle (volatile loop iteration). static constexpr u64 kNsPerSpinCycle = 1; // Estimated nanoseconds for a yield (context switch overhead) static constexpr u64 kNsPerYield = 500; static DelaySpec Parse(const char* str) { DelaySpec spec; if (internal_strncmp(str, "spin=", 5) == 0) { spec.type = DelayType::Spin; spec.value = internal_atoll(str + 5); if (spec.value <= 0 || spec.value > 10000) { Printf( "FATAL: Invalid TSAN_OPTIONS spin value '%s'; value must be " "between 1 and 10000\n", str); Die(); } } else if (internal_strcmp(str, "yield") == 0) { spec.type = DelayType::Yield; spec.value = 0; } else if (internal_strncmp(str, "sleep_us=", 9) == 0) { spec.type = DelayType::SleepUs; spec.value = internal_atoll(str + 9); if (spec.value <= 0) { Printf( "FATAL: Invalid TSAN_OPTIONS sleep_us value '%s'; value must be a " "positive integer\n", str); Die(); } } else { Printf("FATAL: Unrecognized delay spec '%s', check TSAN_OPTIONS\n", str); Die(); } return spec; } const char* TypeName() const { switch (type) { case DelayType::Spin: return "spin"; case DelayType::Yield: return "yield"; case DelayType::SleepUs: return "sleep_us"; } return "unknown"; } }; } // namespace // ============================================================================= // AdaptiveDelayImpl: Time-budget aware delay injection for race exposure // ============================================================================= // // This implementation injects delays to expose data races while maintaining a // configurable overhead target. It uses several strategies: // // 1. Time-Budget Controller: Tracks cumulative delays vs wall-clock time // and adjusts delay probability to maintain target overhead. // // 2. Tiered Delays: Different delay strategies for different op types: // - Relaxed atomics: Very rare sampling, tiny spin delays // - Sync atomics (acq/rel/seq_cst): Moderate sampling, small usleep // - Mutex/CV ops: Higher sampling, larger delays // - Thread create/join: Always delay (rare but high value) // // 3. Address-based Sampling: Exponential backoff per address to avoid // repeatedly delaying hot atomics. struct AdaptiveDelayImpl { ALWAYS_INLINE static AdaptiveDelayState* TLS() { return &cur_thread()->adaptive_delay_state; } ALWAYS_INLINE static unsigned int* GetRandomSeed() { return &TLS()->tls_random_seed_; } ALWAYS_INLINE static void SetRandomSeed(unsigned int seed) { TLS()->tls_random_seed_ = seed; } // The public facing option is adaptive_delay_aggressiveness, which is an // opaque value for the user to tune the amount of delay injected into the // program. Internally, the implementation maps the aggressiveness to a target // percent delay for the overall program runtime. It's not easy to implement // a true wall clock delay target (e.g., 25% program wall time slowdown) // because 1) spin loops and yield are hard to calculate actual wall time // slowness and 2) usleep(N) is often slower than advertised. Thus, we keep // the user facing parameter opaque to not under deliver on a promise of // percent wall time slowdown. struct TimeBudget { int target_overhead_pct_; Percent target_low_; Percent target_high_; void Init(int target_pct) { target_overhead_pct_ = target_pct; target_low_ = Percent::FromPct( target_overhead_pct_ >= 5 ? target_overhead_pct_ - 5 : 0); target_high_ = Percent::FromPct(target_overhead_pct_ + 5); } static constexpr u64 BucketDurationNs = 30'000'000'000ULL; void RecordDelay(u64 delay_ns) { u64 now = NanoTime(); u64 elapsed_ns = now - TLS()->bucket_start_ns_; if (elapsed_ns >= BucketDurationNs) { // Shift: old bucket is discarded, new becomes old, start fresh new TLS()->delay_buckets_ns_[0] = TLS()->delay_buckets_ns_[1]; TLS()->delay_buckets_ns_[1] = 0; TLS()->bucket_start_ns_ = now; TLS()->bucket0_window_ns = BucketDurationNs; } TLS()->delay_buckets_ns_[1] += delay_ns; } Percent GetOverheadPercent() { u64 now = NanoTime(); u64 elapsed_ns = now - TLS()->bucket_start_ns_; // Need at least 1ms to calculate if (elapsed_ns < 1'000'000ULL) return Percent::FromPct(0); if (elapsed_ns > BucketDurationNs * 2) { // Both buckets are stale return Percent::FromPct(0); } else if (elapsed_ns > BucketDurationNs) { // bucket[0] is stale, use only bucket[1] (current bucket) u64 total_delay_ns = TLS()->delay_buckets_ns_[1]; return Percent::FromRatio(total_delay_ns, elapsed_ns); } else { u64 total_delay_ns = TLS()->delay_buckets_ns_[0] + TLS()->delay_buckets_ns_[1]; u64 window_ns = TLS()->bucket0_window_ns + elapsed_ns; return Percent::FromRatio(total_delay_ns, window_ns); } } bool ShouldDelay() { Percent ratio = GetOverheadPercent(); if (ratio < target_low_) return true; if (ratio > target_high_) return false; // Linear interpolation: at target_low -> 100%, at target_high -> 0% Percent prob = (target_high_ - ratio) / (target_high_ - target_low_); return prob.RandomCheck(GetRandomSeed()); } }; // Address Sampler with Exponential Backoff struct AddressSampler { static constexpr u64 TABLE_SIZE = 2048; struct Entry { atomic_uintptr_t addr_; atomic_uint32_t count_; }; Entry table_[TABLE_SIZE]; static constexpr u32 ExponentialBackoffCap = 64; void Init() { for (u64 i = 0; i < TABLE_SIZE; ++i) { atomic_store(&table_[i].addr_, 0, memory_order_relaxed); atomic_store(&table_[i].count_, 0, memory_order_relaxed); } } static ALWAYS_INLINE u64 splitmix64(u64 x) { x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL; x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL; x = x ^ (x >> 31); return x; } // Uses exponential backoff: delay on 1st, 2nd, 4th, 8th, 16th, ... bool ShouldDelayAddr(uptr addr) { u64 idx = splitmix64(addr >> 3) & (TABLE_SIZE - 1); Entry& e = table_[idx]; // This function is not thread safe. // If two threads access the same hashed entry in parallel, // worst case, we may end up returning true too often. This is // acceptable...instead of full locking. uptr stored_addr = atomic_load(&e.addr_, memory_order_relaxed); if (stored_addr != addr) { // Hash Collision - reset atomic_store(&e.addr_, addr, memory_order_relaxed); atomic_store(&e.count_, 1, memory_order_relaxed); return true; } u32 count = atomic_fetch_add(&e.count_, 1, memory_order_relaxed) + 1; if ((count & (count - 1)) == 0 && count <= ExponentialBackoffCap) return true; return false; } }; TimeBudget budget_; AddressSampler sampler_; int relaxed_sample_rate_; int sync_atomic_sample_rate_; int mutex_sample_rate_; DelaySpec atomic_delay_; DelaySpec sync_delay_; void Init() { InitTls(); } void InitTls() { TLS()->bucket_start_ns_ = NanoTime(); TLS()->delay_buckets_ns_[0] = 0; TLS()->delay_buckets_ns_[1] = 0; TLS()->bucket0_window_ns = 0; SetRandomSeed(NanoTime()); TLS()->tls_initialized_ = true; } bool IsTlsInitialized() const { return TLS()->tls_initialized_; } AdaptiveDelayImpl() { relaxed_sample_rate_ = flags()->adaptive_delay_relaxed_sample_rate; sync_atomic_sample_rate_ = flags()->adaptive_delay_sync_atomic_sample_rate; mutex_sample_rate_ = flags()->adaptive_delay_mutex_sample_rate; atomic_delay_ = DelaySpec::Parse(flags()->adaptive_delay_max_atomic); sync_delay_ = DelaySpec::Parse(flags()->adaptive_delay_max_sync); int delay_aggressiveness = flags()->adaptive_delay_aggressiveness; if (delay_aggressiveness < 1) delay_aggressiveness = 1; budget_.Init(delay_aggressiveness); sampler_.Init(); VPrintf(1, "INFO: ThreadSanitizer AdaptiveDelay initialized\n"); VPrintf(1, " Delay aggressiveness: %d\n", delay_aggressiveness); VPrintf(1, " Relaxed atomic sample rate: 1/%d\n", relaxed_sample_rate_); VPrintf(1, " Sync atomic sample rate: 1/%d\n", sync_atomic_sample_rate_); VPrintf(1, " Mutex sample rate: 1/%d\n", mutex_sample_rate_); VPrintf(1, " Atomic delay: %s=%d\n", atomic_delay_.TypeName(), atomic_delay_.value); VPrintf(1, " Sync delay: %s=%d\n", sync_delay_.TypeName(), sync_delay_.value); } void DoSpinDelay(int iters) { volatile int v = 0; for (int i = 0; i < iters; ++i) v = i; (void)v; budget_.RecordDelay(iters * DelaySpec::kNsPerSpinCycle); } void DoYieldDelay() { internal_sched_yield(); budget_.RecordDelay(DelaySpec::kNsPerYield); } void DoSleepUsDelay(int max_us) { // Use two Rand() calls to get full 32-bit range for larger sleep values u32 rnd = ((u32)Rand(GetRandomSeed()) << 16) | Rand(GetRandomSeed()); int delay_us = 1 + (rnd % max_us); internal_usleep(delay_us); budget_.RecordDelay(delay_us * 1000ULL); } void ExecuteDelay(const DelaySpec& spec) { switch (spec.type) { case DelayType::Spin: { int iters = 1 + (Rand(GetRandomSeed()) % spec.value); DoSpinDelay(iters); break; } case DelayType::Yield: DoYieldDelay(); break; case DelayType::SleepUs: DoSleepUsDelay(spec.value); break; } } void AtomicRelaxedOpDelay() { if ((Rand(GetRandomSeed()) % relaxed_sample_rate_) != 0) return; if (!budget_.ShouldDelay()) return; int iters = 10 + (Rand(GetRandomSeed()) % 10); DoSpinDelay(iters); } void AtomicSyncOpDelay(uptr* addr) { if ((Rand(GetRandomSeed()) % sync_atomic_sample_rate_) != 0) return; if (!budget_.ShouldDelay()) return; if (addr && !sampler_.ShouldDelayAddr(*addr)) return; ExecuteDelay(atomic_delay_); } void AtomicOpFence(int mo) { CHECK(IsTlsInitialized()); if (mo < mo_acquire) AtomicRelaxedOpDelay(); else AtomicSyncOpDelay(nullptr); } void AtomicOpAddr(uptr addr, int mo) { CHECK(IsTlsInitialized()); if (mo < mo_acquire) AtomicRelaxedOpDelay(); else AtomicSyncOpDelay(&addr); } void UnsampledDelay() { CHECK(IsTlsInitialized()); if (!budget_.ShouldDelay()) return; ExecuteDelay(sync_delay_); } void SyncOp() { CHECK(IsTlsInitialized()); if ((Rand(GetRandomSeed()) % mutex_sample_rate_) != 0) return; if (!budget_.ShouldDelay()) return; ExecuteDelay(sync_delay_); } void BeforeChildThreadRuns() { InitTls(); UnsampledDelay(); } void AfterThreadCreation() { UnsampledDelay(); } }; AdaptiveDelayImpl& GetImpl() { static AdaptiveDelayImpl impl; return impl; } bool AdaptiveDelay::is_adaptive_delay_enabled; void AdaptiveDelay::InitImpl() { AdaptiveDelay::is_adaptive_delay_enabled = flags()->enable_adaptive_delay; if (!AdaptiveDelay::is_adaptive_delay_enabled) return; GetImpl().Init(); } void AdaptiveDelay::SyncOpImpl() { GetImpl().SyncOp(); } void AdaptiveDelay::AtomicOpFenceImpl(int mo) { GetImpl().AtomicOpFence(mo); } void AdaptiveDelay::AtomicOpAddrImpl(__sanitizer::uptr addr, int mo) { GetImpl().AtomicOpAddr(addr, mo); } void AdaptiveDelay::AfterThreadCreationImpl() { GetImpl().AfterThreadCreation(); } void AdaptiveDelay::BeforeChildThreadRunsImpl() { GetImpl().BeforeChildThreadRuns(); } } // namespace __tsan