Skip to content

Commit 1769f78

Browse files
committed
fix: fix emission problems
1 parent 399fcb8 commit 1769f78

8 files changed

Lines changed: 145 additions & 19 deletions

File tree

ddprof-lib/src/main/cpp/profiler.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1437,11 +1437,11 @@ Error Profiler::init() {
14371437

14381438
Error Profiler::start(Arguments &args, bool reset) {
14391439
MutexLocker ml(_state_lock);
1440-
_task_block_enabled.store(false, std::memory_order_release);
14411440
Error error = checkState();
14421441
if (error) {
14431442
return error;
14441443
}
1444+
_task_block_enabled.store(false, std::memory_order_release);
14451445

14461446
error = checkJvmCapabilities();
14471447
if (error) {

ddprof-lib/src/main/cpp/threadFilter.cpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -673,8 +673,20 @@ bool ThreadFilter::snapshotAndExitBlockedRun(SlotID slot_id, u64 generation,
673673
return true;
674674
}
675675

676+
bool ThreadFilter::activeOwnedBlockGeneration(const ThreadEntry& entry,
677+
u64& generation) const {
678+
return ownedBlockGeneration(entry, generation, false);
679+
}
680+
676681
bool ThreadFilter::isOwnedBlockSuppressionCandidate(
677682
const ThreadEntry& entry) const {
683+
u64 generation = 0;
684+
return ownedBlockGeneration(entry, generation, true);
685+
}
686+
687+
bool ThreadFilter::ownedBlockGeneration(const ThreadEntry& entry,
688+
u64& generation,
689+
bool require_sampled) const {
678690
Slot* slot = entry.slot;
679691
if (!unfilteredWallTrackingActive() || slot == nullptr ||
680692
slot->nativeTid() != entry.tid ||
@@ -701,7 +713,11 @@ bool ThreadFilter::isOwnedBlockSuppressionCandidate(
701713

702714
u64 block_generation = slot->blockGeneration();
703715
BlockRunOwner owner = slot->activeBlockOwner();
704-
if (owner == BlockRunOwner::NONE) return false;
716+
if (owner == BlockRunOwner::NONE ||
717+
(require_sampled &&
718+
slot->sampledBlockGeneration() != block_generation)) {
719+
return false;
720+
}
705721

706722
#ifdef UNIT_TEST
707723
if (_suppression_snapshot_hook != nullptr) {
@@ -714,13 +730,16 @@ bool ThreadFilter::isOwnedBlockSuppressionCandidate(
714730
if (slot->activeBlockOwner() != owner ||
715731
slot->blockGeneration() != block_generation ||
716732
slot->activeBlockState() != state || slot->nativeTid() != entry.tid ||
717-
slot->lifecycleGeneration() != entry.lifecycle_generation) {
733+
slot->lifecycleGeneration() != entry.lifecycle_generation ||
734+
(require_sampled &&
735+
slot->sampledBlockGeneration() != block_generation)) {
718736
return false;
719737
}
720738
if (recordingEpoch() != epoch || slot->recordingEpoch() != epoch ||
721739
!slot->activeBlockRemainedOutsideContextWindow()) {
722740
return false;
723741
}
742+
generation = block_generation;
724743
return true;
725744
}
726745

ddprof-lib/src/main/cpp/threadFilter.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ class ThreadFilter {
8585
std::atomic<u64> recording_epoch{0};
8686
std::atomic<u64> active_block_context_epoch{0};
8787
std::atomic<u64> block_generation{0};
88+
// The most recent owned-block generation for which a MethodSample was
89+
// recorded successfully. Generations are monotonic, so a delayed
90+
// completion from an older run cannot mark a newer run as sampled.
91+
std::atomic<u64> sampled_block_generation{0};
8892
std::atomic<OSThreadState> unowned_blocked_state{OSThreadState::UNKNOWN};
8993
// Native identity and context-window membership are independent so an
9094
// unfiltered wall recording can retain lifecycle metadata without
@@ -103,6 +107,7 @@ class ThreadFilter {
103107
- sizeof(std::atomic<u64>)
104108
- sizeof(std::atomic<u64>)
105109
- sizeof(std::atomic<u64>)
110+
- sizeof(std::atomic<u64>)
106111
- sizeof(std::atomic<OSThreadState>)
107112
- sizeof(std::atomic<int>)
108113
- sizeof(std::atomic<int>)
@@ -160,6 +165,20 @@ class ThreadFilter {
160165
inline u64 blockGeneration() const {
161166
return block_generation.load(std::memory_order_acquire);
162167
}
168+
inline u64 sampledBlockGeneration() const {
169+
return sampled_block_generation.load(std::memory_order_acquire);
170+
}
171+
inline void markBlockGenerationSampled(u64 generation) {
172+
u64 sampled = sampled_block_generation.load(std::memory_order_relaxed);
173+
while (sampled < generation &&
174+
!sampled_block_generation.compare_exchange_weak(
175+
sampled, generation, std::memory_order_release,
176+
std::memory_order_relaxed)) {
177+
}
178+
}
179+
inline void resetSampledBlockGeneration() {
180+
sampled_block_generation.store(0, std::memory_order_relaxed);
181+
}
163182
inline bool unownedBlockedFallbackEnabled() const {
164183
return unowned_blocked_fallback_enabled.load(std::memory_order_acquire) != 0;
165184
}
@@ -236,6 +255,7 @@ class ThreadFilter {
236255
generation++;
237256
block_generation.store(generation, std::memory_order_relaxed);
238257
active_block_context_epoch.store(context_state >> 1, std::memory_order_relaxed);
258+
resetSampledBlockGeneration();
239259
disableUnownedBlockedFallback();
240260
*generation_out = generation;
241261
return true;
@@ -245,6 +265,7 @@ class ThreadFilter {
245265
}
246266
inline void clearActiveBlockRun(OSThreadState) {
247267
active_block_state.store(OSThreadState::UNKNOWN, std::memory_order_release);
268+
resetSampledBlockGeneration();
248269
resetUnownedBlockedSampling();
249270
active_block_owner.store(static_cast<int>(BlockRunOwner::NONE), std::memory_order_release);
250271
}
@@ -299,6 +320,8 @@ class ThreadFilter {
299320
bool exitBlockedRun(SlotID slot_id, u64 generation);
300321
bool snapshotAndExitBlockedRun(SlotID slot_id, u64 generation,
301322
BlockRunSnapshot* snapshot);
323+
bool activeOwnedBlockGeneration(const ThreadEntry& entry,
324+
u64& generation) const;
302325
bool isOwnedBlockSuppressionCandidate(const ThreadEntry& entry) const;
303326

304327
#ifdef UNIT_TEST
@@ -356,6 +379,8 @@ class ThreadFilter {
356379
SlotID slotIdByTid(int tid) const { return lookupSlotIdByTid(tid); }
357380

358381
private:
382+
bool ownedBlockGeneration(const ThreadEntry& entry, u64& generation,
383+
bool require_sampled) const;
359384

360385
// Lock-free free list using a stack-like structure
361386
struct FreeListNode {

ddprof-lib/src/main/cpp/wallClock.cpp

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ static inline bool hasKnownActiveTraceContext(ProfiledThread* thread) {
5959

6060
struct WallPrecheckResult {
6161
bool suppress = false;
62+
ThreadFilter::Slot* owned_block_slot = nullptr;
63+
u64 owned_block_generation = 0;
6264
OSThreadState observed_state = OSThreadState::UNKNOWN;
6365
bool observed_state_valid = false;
6466
ThreadFilter::Slot* unowned_weight_slot = nullptr;
@@ -101,9 +103,9 @@ static inline WallPrecheckResult prepareWallPrecheck(ProfiledThread* current,
101103
return result;
102104
}
103105

104-
// TaskBlock replaces signals only for threads that unfiltered wall-clock
105-
// profiling observes outside the tracing context window. Context-scoped
106-
// profiling must continue sampling its selected threads normally.
106+
// Owned blocks replace repeated signals only after their current generation
107+
// has produced one MethodSample. Context-scoped profiling must continue
108+
// sampling its selected threads normally.
107109
if (!registry->unfilteredWallTrackingActive() || slot->inContextWindow()) {
108110
return result;
109111
}
@@ -115,6 +117,14 @@ static inline WallPrecheckResult prepareWallPrecheck(ProfiledThread* current,
115117
result.suppress = true;
116118
return result;
117119
}
120+
u64 block_generation = 0;
121+
if (registry->activeOwnedBlockGeneration(entry, block_generation)) {
122+
// Arm only after recordSample succeeds. A skipped JFR write must leave the
123+
// run eligible so the next signal retries instead of losing its only stack.
124+
result.owned_block_slot = slot;
125+
result.owned_block_generation = block_generation;
126+
return result;
127+
}
118128
if (!slot->unownedBlockedFallbackEnabled()) {
119129
return result;
120130
}
@@ -139,6 +149,10 @@ static inline WallPrecheckResult prepareWallPrecheck(ProfiledThread* current,
139149
static inline void finishWallPrecheck(const WallPrecheckResult& precheck,
140150
bool recorded,
141151
u64 recorded_call_trace_id = 0) {
152+
if (recorded && precheck.owned_block_slot != nullptr) {
153+
precheck.owned_block_slot->markBlockGenerationSampled(
154+
precheck.owned_block_generation);
155+
}
142156
if (!recorded && precheck.unowned_weight_slot != nullptr) {
143157
precheck.unowned_weight_slot->restoreUnownedBlockedWeight(
144158
precheck.unowned_weight);
@@ -238,12 +252,10 @@ void WallClockASGCT::signalHandler(int signo, siginfo_t *siginfo, void *ucontext
238252
current->tickInitWindow();
239253
return;
240254
}
241-
// Once-per-run filter (wallprecheck=true): for untraced threads, exact
242-
// suppression is only valid while an explicit lifecycle hook owns the blocked
243-
// interval. Raw OS thread state is only an observation; it cannot distinguish
244-
// one long sleep from several short sleeps separated by runnable gaps between
245-
// signals. Unowned blocked observations therefore use weighted fallback
246-
// sampling instead of arming sampled_this_run.
255+
// Once-per-run filter (wallprecheck=true): an explicitly owned block keeps
256+
// its first successful MethodSample and suppresses subsequent signals.
257+
// Unowned blocked observations use weighted fallback sampling because raw OS
258+
// state cannot distinguish one long sleep from several shorter runs.
247259
WallPrecheckResult precheck = prepareWallPrecheck(current, _precheck);
248260
if (precheck.suppress) {
249261
return;
@@ -379,9 +391,8 @@ void WallClockASGCT::timerLoop() {
379391
registry_lookups++;
380392
thread_filter->lookupThreadEntry(entry, recording_epoch);
381393
}
382-
// Timer-thread fast path (wallprecheck=true): skip the kernel IPI while
383-
// an explicit lifecycle hook owns a suppressible blocked run. Raw OS
384-
// thread state cannot prove run boundaries for the target thread.
394+
// Timer-thread fast path (wallprecheck=true): skip the kernel IPI only
395+
// after an explicitly owned run has recorded its first MethodSample.
385396
if (_precheck && suppressOwnedBlock(entry)) {
386397
return WallClockCandidateOutcome::PRECHECK_REJECTED;
387398
}

ddprof-lib/src/test/cpp/park_state_ut.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ TEST(WallClockOncePerRunFilterTest, FilterHelpersManageActiveBlockState) {
304304

305305
filter.exitBlockedRun(slot_id);
306306
EXPECT_EQ(OSThreadState::UNKNOWN, slot->activeBlockState());
307+
EXPECT_EQ(0ULL, slot->sampledBlockGeneration());
307308
}
308309

309310
TEST(WallClockOncePerRunFilterTest, ResetClearsOwnedBlockOnSlotReuse) {
@@ -314,8 +315,11 @@ TEST(WallClockOncePerRunFilterTest, ResetClearsOwnedBlockOnSlotReuse) {
314315
ThreadFilter::Slot *slot = filter.slotForId(slot_id);
315316
ASSERT_NE(nullptr, slot);
316317
EXPECT_EQ(OSThreadState::CONDVAR_WAIT, slot->activeBlockState());
318+
slot->markBlockGenerationSampled(slot->blockGeneration());
319+
ASSERT_EQ(slot->blockGeneration(), slot->sampledBlockGeneration());
317320

318321
filter.resetSlotRunState(slot_id);
319322

320323
EXPECT_EQ(OSThreadState::UNKNOWN, slot->activeBlockState());
324+
EXPECT_EQ(0ULL, slot->sampledBlockGeneration());
321325
}

ddprof-lib/src/test/cpp/threadFilter_ut.cpp

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ TEST_F(ThreadFilterTest, SnapshotCapturesOwnedLifecycle) {
631631
EXPECT_FALSE(slot->snapshotBlockRun().active);
632632
}
633633

634-
TEST_F(ThreadFilterTest, OwnedBlockSuppressesBeforeAnyWallSample) {
634+
TEST_F(ThreadFilterTest, OwnedBlockSuppressesOnlyAfterSuccessfulWallSample) {
635635
filter->init(nullptr, true);
636636
int slot_id = filter->registerThread(1234);
637637
ASSERT_GE(slot_id, 0);
@@ -642,6 +642,12 @@ TEST_F(ThreadFilterTest, OwnedBlockSuppressesBeforeAnyWallSample) {
642642

643643
ThreadEntry entry{1234, slot, slot->lifecycleGeneration(),
644644
slot->recordingEpoch()};
645+
u64 generation = 0;
646+
EXPECT_TRUE(filter->activeOwnedBlockGeneration(entry, generation));
647+
EXPECT_EQ(ThreadFilter::tokenGeneration(token), generation);
648+
EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate(entry));
649+
650+
slot->markBlockGenerationSampled(generation);
645651
EXPECT_TRUE(filter->isOwnedBlockSuppressionCandidate(entry));
646652
EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate(
647653
{1235, slot, slot->lifecycleGeneration(), slot->recordingEpoch()}));
@@ -654,6 +660,38 @@ TEST_F(ThreadFilterTest, OwnedBlockSuppressesBeforeAnyWallSample) {
654660
EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate(entry));
655661
}
656662

663+
TEST_F(ThreadFilterTest, StaleSampleCompletionCannotSuppressNewBlockGeneration) {
664+
filter->init(nullptr, true);
665+
int slot_id = filter->registerThread(1234);
666+
ASSERT_GE(slot_id, 0);
667+
ThreadFilter::Slot* slot = filter->slotForId(slot_id);
668+
ASSERT_NE(nullptr, slot);
669+
670+
u64 first_token = filter->enterBlockedRun(slot_id, OSThreadState::SLEEPING);
671+
ASSERT_NE(0ULL, first_token);
672+
u64 first_generation = ThreadFilter::tokenGeneration(first_token);
673+
ASSERT_TRUE(filter->exitBlockedRun(slot_id, first_generation));
674+
675+
u64 second_token =
676+
filter->enterBlockedRun(slot_id, OSThreadState::CONDVAR_WAIT);
677+
ASSERT_NE(0ULL, second_token);
678+
u64 second_generation = ThreadFilter::tokenGeneration(second_token);
679+
ASSERT_GT(second_generation, first_generation);
680+
681+
ThreadEntry entry{1234, slot, slot->lifecycleGeneration(),
682+
slot->recordingEpoch()};
683+
slot->markBlockGenerationSampled(first_generation);
684+
EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate(entry));
685+
686+
slot->markBlockGenerationSampled(second_generation);
687+
EXPECT_TRUE(filter->isOwnedBlockSuppressionCandidate(entry));
688+
689+
// A delayed completion from the first run must not overwrite the newer mark.
690+
slot->markBlockGenerationSampled(first_generation);
691+
EXPECT_EQ(second_generation, slot->sampledBlockGeneration());
692+
EXPECT_TRUE(filter->isOwnedBlockSuppressionCandidate(entry));
693+
}
694+
657695
TEST_F(ThreadFilterTest, ContextScopeNeverSuppressesOwnedBlock) {
658696
filter->init("0", false);
659697
int slot_id = filter->registerThread(1234);
@@ -666,6 +704,7 @@ TEST_F(ThreadFilterTest, ContextScopeNeverSuppressesOwnedBlock) {
666704

667705
ThreadEntry entry{1234, slot, slot->lifecycleGeneration(),
668706
slot->recordingEpoch()};
707+
slot->markBlockGenerationSampled(slot->blockGeneration());
669708
EXPECT_FALSE(filter->isOwnedBlockSuppressionCandidate(entry));
670709
}
671710

@@ -679,6 +718,7 @@ TEST_F(ThreadFilterTest, ContextEpochDisablesOwnedBlockSuppression) {
679718
slot_id, OSThreadState::CONDVAR_WAIT));
680719
ThreadEntry entry{1234, slot, slot->lifecycleGeneration(),
681720
slot->recordingEpoch()};
721+
slot->markBlockGenerationSampled(slot->blockGeneration());
682722
ASSERT_TRUE(filter->isOwnedBlockSuppressionCandidate(entry));
683723

684724
filter->add(1234, slot_id);
@@ -868,6 +908,7 @@ TEST_F(ThreadRegistryTest, UnfilteredSuppressionValidatesIdentityAndLifecycle) {
868908

869909
u64 token = registry.enterBlockedRun(slot_id, OSThreadState::SLEEPING);
870910
ASSERT_NE(0u, token);
911+
slot->markBlockGenerationSampled(ThreadFilter::tokenGeneration(token));
871912
ThreadEntry entry{4444, slot, slot->lifecycleGeneration(),
872913
slot->recordingEpoch()};
873914
EXPECT_TRUE(registry.isOwnedBlockSuppressionCandidate(entry));
@@ -905,7 +946,9 @@ TEST_F(ThreadRegistryTest, ConcurrentTidReuseInvalidatesSuppressionSnapshot) {
905946
ASSERT_GE(slot_id, 0);
906947
ThreadFilter::Slot* slot = registry.slotForId(slot_id);
907948
ASSERT_NE(nullptr, slot);
908-
ASSERT_NE(0u, registry.enterBlockedRun(slot_id, OSThreadState::SLEEPING));
949+
u64 token = registry.enterBlockedRun(slot_id, OSThreadState::SLEEPING);
950+
ASSERT_NE(0u, token);
951+
slot->markBlockGenerationSampled(ThreadFilter::tokenGeneration(token));
909952
ThreadEntry stale{tid, slot, slot->lifecycleGeneration(),
910953
slot->recordingEpoch()};
911954

@@ -996,6 +1039,7 @@ TEST_F(ThreadRegistryTest, NewUnfilteredRecordingReclaimsRetainedSlot) {
9961039

9971040
u64 token = registry.enterBlockedRun(slot_id, OSThreadState::SLEEPING);
9981041
ASSERT_NE(0u, token);
1042+
slot->markBlockGenerationSampled(ThreadFilter::tokenGeneration(token));
9991043
ThreadEntry stale{tid, slot, slot->lifecycleGeneration(),
10001044
slot->recordingEpoch()};
10011045
ASSERT_TRUE(registry.isOwnedBlockSuppressionCandidate(stale));

ddprof-test/src/test/java/com/datadoghq/profiler/wallclock/JavaProfilerTaskBlockApiTest.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
import java.util.concurrent.atomic.AtomicReference;
1717
import org.junit.jupiter.api.Assumptions;
1818
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.ValueSource;
1921
import org.openjdk.jmc.common.item.IItemCollection;
2022

2123
import static org.junit.jupiter.api.Assertions.assertEquals;
2224
import static org.junit.jupiter.api.Assertions.assertFalse;
25+
import static org.junit.jupiter.api.Assertions.assertThrows;
2326
import static org.junit.jupiter.api.Assertions.assertTrue;
2427

2528
/** End-to-end coverage for the paired synchronous TaskBlock API. */
@@ -41,6 +44,22 @@ public void pairedApiEmitsTaskBlockWithStack() throws Exception {
4144
TaskBlockAssertions.assertContainsObservedState(events, "SLEEPING");
4245
}
4346

47+
@ParameterizedTest
48+
@ValueSource(strings = {"start", "resume"})
49+
public void rejectedDuplicateStartOrResumePreservesActiveTaskBlockRecording(String action)
50+
throws Exception {
51+
IllegalStateException rejected = assertThrows(
52+
IllegalStateException.class,
53+
() -> profiler.execute(action + "," + getProfilerCommand()));
54+
assertEquals("Profiler already started", rejected.getMessage());
55+
56+
assertTrue(runEligibleBlock(BLOCKER));
57+
stopProfiler();
58+
59+
TaskBlockAssertions.assertContains(
60+
verifyEvents("datadog.TaskBlock"), 0L, 0L, BLOCKER, UNBLOCKING_SPAN_ID);
61+
}
62+
4463
@Test
4564
public void invalidAndNestedTokensDoNotLoseCurrentOwner() throws Exception {
4665
AtomicBoolean recorded = new AtomicBoolean();
@@ -141,7 +160,7 @@ public void virtualThreadCannotMutateCarrierTaskBlockState() throws Exception {
141160
}
142161

143162
@Test
144-
public void liveDumpDoesNotRequireAnEntrySample() throws Exception {
163+
public void liveDumpPreservesTaskBlockAfterEntrySample() throws Exception {
145164
CountDownLatch armed = new CountDownLatch(1);
146165
CountDownLatch release = new CountDownLatch(1);
147166
AtomicBoolean recorded = new AtomicBoolean();

0 commit comments

Comments
 (0)