@@ -2014,6 +2014,119 @@ TEST_F(StressTestSuite, HashTableSpinWaitEdgeCasesTest) {
20142014 EXPECT_LT (drop_rate, 0.5 ) << " Excessive trace drop rate: " << drop_rate;
20152015}
20162016
2017+ // Regression test: TSan data race in findCallTrace() — keys[] plain reads
2018+ // raced with atomic CAS writes from concurrent put(). When a table is
2019+ // promoted to prev after expansion, threads that loaded the old table
2020+ // pointer before the CAS swap still write into it while new threads call
2021+ // findCallTrace(prev, hash) — the read must be atomic.
2022+ //
2023+ // Memory-ordering races on 8-byte aligned values are benign on x86_64 (the
2024+ // CPU guarantees naturally-atomic loads), so the race is only reliably
2025+ // detectable under ThreadSanitizer. The test is therefore skipped outside
2026+ // TSan builds to avoid giving false confidence that the regression is covered.
2027+ //
2028+ // The dedup assertion (dedup_mismatches == 0) validates findCallTrace
2029+ // correctness across expansion boundaries independently of memory ordering;
2030+ // it catches logic regressions in all build configurations.
2031+ TEST_F (StressTestSuite, FindCallTraceAtomicReadRaceTest) {
2032+ #if !defined(__SANITIZE_THREAD__) && !(defined(__has_feature) && __has_feature(thread_sanitizer))
2033+ GTEST_SKIP () << " TSan-only race regression: re-run with -fsanitize=thread" ;
2034+ #endif
2035+
2036+ // Fill the table to just below the 75% expansion threshold
2037+ // (INITIAL_CAPACITY = 65536; 75% = 49152).
2038+ const int FILL_TARGET = 48800 ;
2039+ const int NUM_THREADS = 16 ;
2040+ const int OPS_PER_THREAD = 400 ;
2041+ // Number of pre-filled entries to re-insert concurrently as a dedup check.
2042+ // After expansion these will be looked up via findCallTrace(prev, hash).
2043+ const int VERIFY_COUNT = 200 ;
2044+
2045+ void * aligned_memory = std::aligned_alloc (alignof (CallTraceHashTable), sizeof (CallTraceHashTable));
2046+ ASSERT_NE (aligned_memory, nullptr );
2047+ auto hash_table_ptr = std::unique_ptr<CallTraceHashTable, void (*)(CallTraceHashTable*)>(
2048+ new (aligned_memory) CallTraceHashTable (),
2049+ [](CallTraceHashTable* ptr) {
2050+ ptr->~CallTraceHashTable ();
2051+ std::free (ptr);
2052+ }
2053+ );
2054+ CallTraceHashTable& hash_table = *hash_table_ptr;
2055+ hash_table.setInstanceId (42 );
2056+
2057+ // Pre-fill single-threaded up to just below the expansion threshold,
2058+ // recording trace_ids for the last VERIFY_COUNT entries.
2059+ std::vector<u64 > expected_ids (VERIFY_COUNT );
2060+ std::vector<ASGCT_CallFrame> verify_frames (VERIFY_COUNT );
2061+
2062+ for (int i = 0 ; i < FILL_TARGET ; ++i) {
2063+ ASGCT_CallFrame frame;
2064+ frame.bci = i + 1 ;
2065+ frame.method_id = reinterpret_cast <jmethodID>(0x10000 + i);
2066+ u64 id = hash_table.put (1 , &frame, false , 1 );
2067+ if (i >= FILL_TARGET - VERIFY_COUNT ) {
2068+ int vi = i - (FILL_TARGET - VERIFY_COUNT );
2069+ expected_ids[vi] = id;
2070+ verify_frames[vi] = frame;
2071+ }
2072+ }
2073+
2074+ std::atomic<bool > go{false };
2075+ std::atomic<uint64_t > successes{0 };
2076+ // Counts put() calls that returned a trace_id different from the originally
2077+ // recorded id for a known pre-filled entry. A non-zero value means
2078+ // findCallTrace(prev, hash) returned nullptr when it should have returned
2079+ // the entry — either a logic bug or a memory-ordering failure.
2080+ std::atomic<uint64_t > dedup_mismatches{0 };
2081+ std::vector<std::thread> workers;
2082+
2083+ // Half the threads insert new distinct frames to trigger expansion. They
2084+ // load the old table pointer before the CAS swap and write keys[] in prev,
2085+ // creating the concurrent-write side of the race window.
2086+ for (int t = 0 ; t < NUM_THREADS / 2 ; ++t) {
2087+ workers.emplace_back ([&, t]() {
2088+ while (!go.load (std::memory_order_acquire)) { /* spin */ }
2089+ for (int i = 0 ; i < OPS_PER_THREAD ; ++i) {
2090+ ASGCT_CallFrame frame;
2091+ frame.bci = FILL_TARGET + 1 + t * OPS_PER_THREAD + i;
2092+ frame.method_id = reinterpret_cast <jmethodID>(0x80000 + t * OPS_PER_THREAD + i);
2093+ u64 id = hash_table.put (1 , &frame, false , 1 );
2094+ if (id != 0 && id != CallTraceStorage::DROPPED_TRACE_ID ) {
2095+ successes.fetch_add (1 , std::memory_order_relaxed);
2096+ }
2097+ }
2098+ });
2099+ }
2100+
2101+ // The other half re-insert pre-filled frames. After expansion the new
2102+ // table is empty for those hashes, so put() claims a new slot and calls
2103+ // findCallTrace(prev, hash) — the read side of the race window. A correct
2104+ // findCallTrace must return the original trace so dedup produces the same
2105+ // trace_id.
2106+ for (int t = NUM_THREADS / 2 ; t < NUM_THREADS ; ++t) {
2107+ workers.emplace_back ([&, t]() {
2108+ while (!go.load (std::memory_order_acquire)) { /* spin */ }
2109+ for (int i = 0 ; i < VERIFY_COUNT ; ++i) {
2110+ u64 id = hash_table.put (1 , &verify_frames[i], false , 1 );
2111+ if (id != 0 && id != CallTraceStorage::DROPPED_TRACE_ID &&
2112+ id != expected_ids[i]) {
2113+ dedup_mismatches.fetch_add (1 , std::memory_order_relaxed);
2114+ }
2115+ }
2116+ });
2117+ }
2118+
2119+ go.store (true , std::memory_order_release);
2120+ for (auto & w : workers) {
2121+ w.join ();
2122+ }
2123+
2124+ EXPECT_GT (successes.load (), 0u ) << " No successful insertions after expansion" ;
2125+ EXPECT_EQ (dedup_mismatches.load (), 0u )
2126+ << " findCallTrace returned wrong trace_id for pre-filled entries after expansion "
2127+ " (findCallTrace failed to locate entry in prev table)" ;
2128+ }
2129+
20172130// Test 13: Hash Table Memory Allocation Failure Stress Test
20182131TEST_F (StressTestSuite, HashTableAllocationFailureStressTest) {
20192132 const int NUM_THREADS = 8 ;
0 commit comments