|
| 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 |
0 commit comments