Skip to content

Commit 6e02d21

Browse files
authored
Merge branch 'main' into antonis/feedback-shake
2 parents fedd3a2 + 9c1b406 commit 6e02d21

File tree

10 files changed

+1085
-80
lines changed

10 files changed

+1085
-80
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
alwaysApply: false
3+
description: JVM Continuous Profiling (sentry-async-profiler)
4+
---
5+
# JVM Continuous Profiling
6+
7+
Use this rule when working on JVM continuous profiling in `sentry-async-profiler` and the related core profiling abstractions in `sentry`.
8+
9+
This area is suitable for LLM work, but do not rely on this rule alone for behavior changes. Always read the implementation and nearby tests first, especially for sampling, lifecycle, rate limiting, and file cleanup behavior.
10+
11+
## Module Structure
12+
13+
- **`sentry-async-profiler`**: standalone module containing the async-profiler integration
14+
- Uses Java `ServiceLoader` discovery
15+
- No direct dependency from core `sentry` module
16+
- Enabled by adding the module as a dependency
17+
18+
- **`sentry` core abstractions**:
19+
- `IContinuousProfiler`: profiler lifecycle interface
20+
- `ProfileChunk`: profile chunk payload sent to Sentry
21+
- `IProfileConverter`: converts JVM JFR files into `SentryProfile`
22+
- `ProfileLifecycle`: controls MANUAL vs TRACE lifecycle
23+
- `ProfilingServiceLoader`: loads profiler and converter implementations via `ServiceLoader`
24+
25+
## Key Classes
26+
27+
### `JavaContinuousProfiler` (`sentry-async-profiler`)
28+
- Wraps the native async-profiler library
29+
- Writes JFR files to `profilingTracesDirPath`
30+
- Rotates chunks periodically via `MAX_CHUNK_DURATION_MILLIS` (currently 10s)
31+
- Implements `RateLimiter.IRateLimitObserver`
32+
- Maintains `rootSpanCounter` for TRACE lifecycle
33+
- Keeps a session-level `profilerId` across chunks until the profiling session ends
34+
- `getChunkId()` currently returns `SentryId.EMPTY_ID`, but emitted `ProfileChunk`s get a fresh chunk id when built in `stop(...)`
35+
36+
### `ProfileChunk`
37+
- Carries `profilerId`, `chunkId`, timestamp, platform, measurements, and a JFR file reference
38+
- Built via `ProfileChunk.Builder`
39+
- For JVM, the JFR file is converted later during envelope item creation, not inside `JavaContinuousProfiler`
40+
41+
### `ProfileLifecycle`
42+
- `MANUAL`: explicit `Sentry.startProfiler()` / `Sentry.stopProfiler()`
43+
- `TRACE`: profiler lifecycle follows active sampled root spans
44+
45+
## Configuration
46+
47+
Continuous profiling is **not** controlled by `profilesSampleRate`.
48+
49+
Key options:
50+
- **`profileSessionSampleRate`**: session-level sample rate for continuous profiling
51+
- **`profileLifecycle`**: `ProfileLifecycle.MANUAL` (default) or `ProfileLifecycle.TRACE`
52+
- **`cacheDirPath`**: base SDK cache directory; profiling traces are written under the derived `profilingTracesDirPath`
53+
- **`profilingTracesHz`**: sampling frequency in Hz (default: 101)
54+
55+
Continuous profiling is enabled when:
56+
- `profilesSampleRate == null`
57+
- `profilesSampler == null`
58+
- `profileSessionSampleRate != null && profileSessionSampleRate > 0`
59+
60+
Example:
61+
62+
```java
63+
options.setProfileSessionSampleRate(1.0);
64+
options.setCacheDirPath("/tmp/sentry-cache");
65+
options.setProfileLifecycle(ProfileLifecycle.MANUAL);
66+
options.setProfilingTracesHz(101);
67+
```
68+
69+
## How It Works
70+
71+
### Initialization
72+
- `InitUtil.initializeProfiler(...)` resolves or creates the profiling traces directory
73+
- `ProfilingServiceLoader.loadContinuousProfiler(...)` uses `ServiceLoader` to find `JavaContinuousProfilerProvider`
74+
- `AsyncProfilerContinuousProfilerProvider` instantiates `JavaContinuousProfiler`
75+
- `ProfilingServiceLoader.loadProfileConverter()` separately loads the `JavaProfileConverterProvider`
76+
77+
### Profiling Flow
78+
79+
**Start**
80+
- Sampling decision is made via `TracesSampler.sampleSessionProfile(...)`
81+
- Sampling is session-based and cached until `reevaluateSampling()`
82+
- Scopes and rate limiter are initialized lazily via `initScopes()`
83+
- Rate limits for `All` or `ProfileChunk` abort startup
84+
- JFR filename is generated under `profilingTracesDirPath`
85+
- async-profiler is started with a command like:
86+
- `start,jfr,event=wall,nobatch,interval=<interval>,file=<path>`
87+
- Automatic chunk stop is scheduled after `MAX_CHUNK_DURATION_MILLIS`
88+
89+
**Chunk Rotation**
90+
- `stop(true)` stops async-profiler and validates the JFR file
91+
- A `ProfileChunk.Builder` is created with:
92+
- current `profilerId`
93+
- a fresh `chunkId`
94+
- trace file
95+
- chunk timestamp
96+
- platform `java`
97+
- Builder is buffered in `payloadBuilders`
98+
- Chunks are sent if scopes are available
99+
- Profiling is restarted for the next chunk
100+
101+
**Stop**
102+
- `MANUAL`: stop immediately, do not restart, reset `profilerId`
103+
- `TRACE`: decrement `rootSpanCounter`; stop only when it reaches 0
104+
- `close(...)` also forces shutdown and resets TRACE state
105+
106+
### Sending and Conversion
107+
- `JavaContinuousProfiler` buffers `ProfileChunk.Builder` instances
108+
- `sendChunks(...)` builds `ProfileChunk` objects and calls `scopes.captureProfileChunk(...)`
109+
- `SentryClient.captureProfileChunk(...)` creates an envelope item
110+
- JVM JFR-to-`SentryProfile` conversion happens in `SentryEnvelopeItem.fromProfileChunk(...)` using the loaded `IProfileConverter`
111+
- Trace files are deleted in the envelope item path after serialization attempts
112+
113+
## TRACE Mode Lifecycle
114+
- `rootSpanCounter` increments when sampled root spans start
115+
- `rootSpanCounter` decrements when root spans finish
116+
- Profiler runs while `rootSpanCounter > 0`
117+
- Multiple concurrent sampled transactions can share the same profiling session
118+
- Be careful when changing lifecycle logic: this area is lock-protected and concurrency-sensitive
119+
120+
## Rate Limiting and Buffering
121+
122+
### Rate Limiting
123+
- Registers as a `RateLimiter.IRateLimitObserver`
124+
- If rate limited for `ProfileChunk` or `All`:
125+
- profiler stops immediately
126+
- it does not auto-restart when the limit expires
127+
- Startup also checks rate limiting before profiling begins
128+
129+
### Buffering / pre-init behavior
130+
- JFR files are written to `profilingTracesDirPath` and marked `deleteOnExit()` when a chunk is accepted
131+
- If scopes are not yet available, `ProfileChunk.Builder`s remain buffered in memory in `payloadBuilders`
132+
- This commonly matters for profiling that starts before SDK scopes are ready
133+
- This is not a dedicated durable offline queue owned by the profiler itself; conversion and final send happen later in the normal client/envelope path
134+
135+
## Extending
136+
137+
To add or replace JVM profiler implementations:
138+
- implement `IContinuousProfiler`
139+
- implement `JavaContinuousProfilerProvider`
140+
- register provider in:
141+
- `META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider`
142+
143+
To add or replace JVM profile conversion:
144+
- implement `IProfileConverter`
145+
- implement `JavaProfileConverterProvider`
146+
- register provider in:
147+
- `META-INF/services/io.sentry.profiling.JavaProfileConverterProvider`
148+
149+
## Code Locations
150+
151+
Primary implementation:
152+
- `sentry/src/main/java/io/sentry/IContinuousProfiler.java`
153+
- `sentry/src/main/java/io/sentry/ProfileChunk.java`
154+
- `sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java`
155+
- `sentry/src/main/java/io/sentry/util/InitUtil.java`
156+
- `sentry/src/main/java/io/sentry/SentryEnvelopeItem.java`
157+
- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java`
158+
- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java`
159+
- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java`
160+
- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java`
161+
162+
Tests to read first:
163+
- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt`
164+
- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilingServiceLoaderTest.kt`
165+
- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt`
166+
167+
## LLM Guidance
168+
169+
This rule is good enough for orientation, but for actual code changes always verify:
170+
- the sampling path in `TracesSampler`
171+
- continuous profiling enablement in `SentryOptions`
172+
- lifecycle entry points in `Scopes` and `SentryTracer`
173+
- conversion and file deletion behavior in `SentryEnvelopeItem`
174+
- existing tests before changing concurrency or lifecycle semantics

.cursor/rules/overview_dev.mdc

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Use the `fetch_rules` tool to include these rules when working on specific areas
3030

3131
- **`scopes`**: Use when working with:
3232
- Hub/Scope management, forking, or lifecycle
33-
- `Sentry.getCurrentScopes()`, `pushScope()`, `withScope()`
33+
- `Sentry.getCurrentScopes()`, `pushScope()`, `withScope()`
3434
- `ScopeType` (GLOBAL, ISOLATION, CURRENT)
3535
- Thread-local storage, scope bleeding issues
3636
- Migration from Hub API (v7 → v8)
@@ -66,6 +66,18 @@ Use the `fetch_rules` tool to include these rules when working on specific areas
6666
- `SentryMetricsEvent`, `SentryMetricsEvents`
6767
- `SentryOptions.getMetrics()`, `beforeSend` callback
6868

69+
- **`continuous_profiling_jvm`**: Use when working with:
70+
- JVM continuous profiling (`sentry-async-profiler` module)
71+
- `IContinuousProfiler`, `JavaContinuousProfiler`
72+
- `ProfileChunk`, chunk rotation, JFR file handling
73+
- `ProfileLifecycle` (MANUAL vs TRACE modes)
74+
- async-profiler integration, ServiceLoader discovery
75+
- Rate limiting, offline caching, scopes integration
76+
77+
- **Android profiling**: There is currently no dedicated rule for this area yet.
78+
- Inspect the relevant `sentry-android-core` profiling code directly
79+
- Fetch other related rules as needed (for example `options`, `offline`, or `api`)
80+
6981
### Integration & Infrastructure
7082
- **`opentelemetry`**: Use when working with:
7183
- OpenTelemetry modules (`sentry-opentelemetry-*`)
@@ -99,11 +111,13 @@ Use the `fetch_rules` tool to include these rules when working on specific areas
99111
- Public API/apiDump/.api files/binary compatibility/new method → `api`
100112
- Options/SentryOptions/ExternalOptions/ManifestMetadataReader/sentry.properties → `options`
101113
- Scope/Hub/forking → `scopes`
102-
- Duplicate/dedup → `deduplication`
114+
- Duplicate/dedup → `deduplication`
103115
- OpenTelemetry/tracing/spans → `opentelemetry`
104116
- new module/integration/sample → `new_module`
105117
- Cache/offline/network → `offline`
106118
- System test/e2e/sample → `e2e_tests`
107119
- Feature flag/addFeatureFlag/flag evaluation → `feature_flags`
108120
- Metrics/count/distribution/gauge → `metrics`
109121
- PR/pull request/stacked PR/stack → `pr`
122+
- JVM continuous profiling/async-profiler/JFR/ProfileChunk → `continuous_profiling_jvm`
123+
- Android continuous profiling/AndroidProfiler/frame metrics/method tracing → no dedicated rule yet; inspect the code directly

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@
88
- Enable via `options.getFeedbackOptions().setUseShakeGesture(true)` or manifest meta-data `io.sentry.feedback.use-shake-gesture`
99
- Uses the device's accelerometer — no special permissions required
1010

11+
### Fixes
12+
13+
- Support masking/unmasking and click/scroll detection for Jetpack Compose 1.10+ ([#5189](https://github.com/getsentry/sentry-java/pull/5189))
14+
1115
### Dependencies
1216

1317
- Bump Native SDK from v0.13.1 to v0.13.2 ([#5181](https://github.com/getsentry/sentry-java/pull/5181))
1418
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0132)
1519
- [diff](https://github.com/getsentry/sentry-native/compare/0.13.1...0.13.2)
20+
- Bump `com.abovevacant:epitaph` to `0.1.1` to avoid old D8/R8 dexing crashes in downstream Android builds on old AGP versions such as 7.4.x. ([#5200](https://github.com/getsentry/sentry-java/pull/5200))
21+
- [changelog](https://github.com/abovevacant/epitaph/blob/main/CHANGELOG.md#011---2026-03-16)
22+
- [diff](https://github.com/abovevacant/epitaph/compare/v0.1.0...v0.1.1)
1623

1724
## 8.35.0
1825

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ androidx-compose-material-icons-core = { module = "androidx.compose.material:mat
8585
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version="1.7.8" }
8686
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidxCompose" }
8787
# Note: don't change without testing forwards compatibility
88-
androidx-compose-ui-replay = { module = "androidx.compose.ui:ui", version = "1.5.0" }
88+
androidx-compose-ui-replay = { module = "androidx.compose.ui:ui", version = "1.10.2" }
8989
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.3" }
9090
androidx-core = { module = "androidx.core:core", version = "1.3.2" }
9191
androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.7.0" }
@@ -143,7 +143,7 @@ otel-javaagent-extension-api = { module = "io.opentelemetry.javaagent:openteleme
143143
otel-semconv = { module = "io.opentelemetry.semconv:opentelemetry-semconv", version.ref = "otelSemanticConventions" }
144144
otel-semconv-incubating = { module = "io.opentelemetry.semconv:opentelemetry-semconv-incubating", version.ref = "otelSemanticConventionsAlpha" }
145145
p6spy = { module = "p6spy:p6spy", version = "3.9.1" }
146-
epitaph = { module = "com.abovevacant:epitaph", version = "0.1.0" }
146+
epitaph = { module = "com.abovevacant:epitaph", version = "0.1.1" }
147147
quartz = { module = "org.quartz-scheduler:quartz", version = "2.3.0" }
148148
reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" }
149149
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }

sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,32 +36,35 @@ import java.lang.reflect.Method
3636
@SuppressLint("UseRequiresApi")
3737
@TargetApi(26)
3838
internal object ComposeViewHierarchyNode {
39-
private val getSemanticsConfigurationMethod: Method? by lazy {
40-
try {
41-
return@lazy LayoutNode::class.java.getDeclaredMethod("getSemanticsConfiguration").apply {
42-
isAccessible = true
39+
private val getCollapsedSemanticsMethod: Method? by
40+
lazy(LazyThreadSafetyMode.NONE) {
41+
try {
42+
return@lazy LayoutNode::class
43+
.java
44+
.getDeclaredMethod("getCollapsedSemantics\$ui_release")
45+
.apply { isAccessible = true }
46+
} catch (_: Throwable) {
47+
// ignore, as this method may not be available
4348
}
44-
} catch (_: Throwable) {
45-
// ignore, as this method may not be available
49+
return@lazy null
4650
}
47-
return@lazy null
48-
}
4951

5052
private var semanticsRetrievalErrorLogged: Boolean = false
5153

5254
@JvmStatic
5355
internal fun retrieveSemanticsConfiguration(node: LayoutNode): SemanticsConfiguration? {
54-
// Jetpack Compose 1.8 or newer provides SemanticsConfiguration via SemanticsInfo
55-
// See
56-
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
57-
// and
58-
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt
59-
getSemanticsConfigurationMethod?.let {
60-
return it.invoke(node) as SemanticsConfiguration?
56+
return try {
57+
node.semanticsConfiguration
58+
} catch (t: Throwable) {
59+
// for backwards compatibility
60+
// Jetpack Compose 1.8 or older
61+
if (getCollapsedSemanticsMethod != null) {
62+
getCollapsedSemanticsMethod!!.invoke(node) as SemanticsConfiguration?
63+
} else {
64+
// re-throw t if there's no way to retrieve semantics
65+
throw t
66+
}
6167
}
62-
63-
// for backwards compatibility
64-
return node.collapsedSemantics
6568
}
6669

6770
/**
@@ -136,7 +139,7 @@ internal object ComposeViewHierarchyNode {
136139
"""
137140
Error retrieving semantics information from Compose tree. Most likely you're using
138141
an unsupported version of androidx.compose.ui:ui. The supported
139-
version range is 1.5.0 - 1.8.0.
142+
version range is 1.5.0 - 1.10.2.
140143
If you're using a newer version, please open a github issue with the version
141144
you're using, so we can add support for it.
142145
"""
@@ -157,15 +160,15 @@ internal object ComposeViewHierarchyNode {
157160
shouldMask = true,
158161
isImportantForContentCapture = false, // will be set by children
159162
isVisible =
160-
!node.outerCoordinator.isTransparent() &&
163+
!SentryLayoutNodeHelper.isTransparent(node) &&
161164
visibleRect.height() > 0 &&
162165
visibleRect.width() > 0,
163166
visibleRect = visibleRect,
164167
)
165168
}
166169

167170
val isVisible =
168-
!node.outerCoordinator.isTransparent() &&
171+
!SentryLayoutNodeHelper.isTransparent(node) &&
169172
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
170173
visibleRect.height() > 0 &&
171174
visibleRect.width() > 0
@@ -301,7 +304,7 @@ internal object ComposeViewHierarchyNode {
301304
options: SentryMaskingOptions,
302305
logger: ILogger,
303306
) {
304-
val children = this.children
307+
val children = SentryLayoutNodeHelper.getChildren(this)
305308
if (children.isEmpty()) {
306309
return
307310
}

0 commit comments

Comments
 (0)