diff --git a/.cursor/rules/overview_dev.mdc b/.cursor/rules/overview_dev.mdc index f05d2992d4..7be99b8127 100644 --- a/.cursor/rules/overview_dev.mdc +++ b/.cursor/rules/overview_dev.mdc @@ -53,6 +53,16 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - `SentryMetricsEvent`, `SentryMetricsEvents` - `SentryOptions.getMetrics()`, `beforeSend` callback +- **`profiling`**: Use when working with: + - Continuous profiling (`sentry-async-profiler` module) + - `IContinuousProfiler`, `JavaContinuousProfiler`, `AndroidContinuousProfiler` + - `ProfileChunk`, chunk rotation and sending + - `ProfileLifecycle` (MANUAL vs TRACE modes) + - `profilesSampleRate`, `profilingTracesHz`, `profileLifecycle` options + - Integration with rate limiting, offline caching, scopes + - JFR file handling, async-profiler integration + - Platform differences (JVM vs Android profiling) + ### Integration & Infrastructure - **`opentelemetry`**: Use when working with: - OpenTelemetry modules (`sentry-opentelemetry-*`) @@ -77,10 +87,11 @@ Use the `fetch_rules` tool to include these rules when working on specific areas 3. **Multiple rules**: Fetch multiple rules if task spans domains (e.g., `["scopes", "opentelemetry"]` for tracing scope issues) 4. **Context clues**: Look for these keywords in requests to determine relevant rules: - Scope/Hub/forking → `scopes` - - Duplicate/dedup → `deduplication` + - Duplicate/dedup → `deduplication` - OpenTelemetry/tracing/spans → `opentelemetry` - new module/integration/sample → `new_module` - Cache/offline/network → `offline` - System test/e2e/sample → `e2e_tests` - Feature flag/addFeatureFlag/flag evaluation → `feature_flags` - Metrics/count/distribution/gauge → `metrics` + - Profiling/profiler/ProfileChunk/JFR → `profiling` diff --git a/.cursor/rules/profiling.mdc b/.cursor/rules/profiling.mdc new file mode 100644 index 0000000000..c18890fdac --- /dev/null +++ b/.cursor/rules/profiling.mdc @@ -0,0 +1,133 @@ +--- +alwaysApply: false +description: Java SDK Profiling +--- +# Java SDK Profiling + +The Sentry Java SDK provides continuous profiling through the `sentry-async-profiler` module, which integrates async-profiler for low-overhead CPU profiling. + +## Module Structure + +- **`sentry-async-profiler`**: Standalone module containing async-profiler integration + - Uses Java ServiceLoader pattern for discovery + - No direct dependency from core `sentry` module + - Opt-in by adding module as dependency + +- **`sentry` core abstractions**: + - `IContinuousProfiler`: Interface for profiler implementations + - `ProfileChunk`: Profile data structure sent to Sentry + - `IProfileConverter`: Converts JFR files to Sentry format + - `ProfileLifecycle`: Controls lifecycle (MANUAL vs TRACE) + - `ProfilingServiceLoader`: ServiceLoader discovery + +## Key Classes + +### `JavaContinuousProfiler` (sentry-async-profiler) +- Wraps native async-profiler library +- Writes JFR files to `profilingTracesDirPath` +- Rotates chunks periodically (`MAX_CHUNK_DURATION_MILLIS`) +- Implements `RateLimiter.IRateLimitObserver` for rate limiting +- Maintains `rootSpanCounter` for TRACE mode lifecycle + +### `ProfileChunk` +- Contains profiler ID (session-level, persists across chunks), chunk ID, JFR file reference +- Built using `ProfileChunk.Builder` +- JFR file converted to `SentryProfile` before sending + +### `ProfileLifecycle` +- `MANUAL`: Explicit `Sentry.startProfiler()` / `stopProfiler()` calls +- `TRACE`: Automatic, tied to active sampled root spans + +## Configuration + +- **`profilesSampleRate`**: Sample rate (0.0 to 1.0). If set with `tracesSampleRate`, enables transaction profiling. If set alone, enables continuous profiling. +- **`profileLifecycle`**: `ProfileLifecycle.MANUAL` (default) or `ProfileLifecycle.TRACE` +- **`cacheDirPath`**: Directory for JFR files (required) +- **`profilingTracesHz`**: Sampling frequency in Hz (default: 101) + +Example: +```java +options.setProfilesSampleRate(1.0); +options.setCacheDirPath("/tmp/sentry-cache"); +options.setProfileLifecycle(ProfileLifecycle.MANUAL); +``` + +## How It Works + +### Initialization +`ProfilingServiceLoader.loadContinuousProfiler()` uses ServiceLoader to find `AsyncProfilerContinuousProfilerProvider`, which instantiates `JavaContinuousProfiler`. + +### Profiling Flow + +**Start**: +- Sampling decision via `TracesSampler` +- Rate limit check (abort if active) +- Generate JFR filename: `/.jfr` +- Execute async-profiler: `start,jfr,event=wall,nobatch,interval=,file=` +- Schedule chunk rotation (default: 10 seconds) + +**Chunk Rotation**: +- Stop profiler and validate JFR file +- Create `ProfileChunk.Builder` with profiler ID, chunk ID, file, timestamp, platform +- Store in `payloadBuilders` list +- Send chunks if scopes available +- Restart profiler for next chunk + +**Stop**: +- MANUAL: Stop without restart, reset profiler ID +- TRACE: Decrement `rootSpanCounter`, stop only when counter reaches 0 + +### Sending +- Chunks in `payloadBuilders` built via `builder.build(options)` +- Captured via `scopes.captureProfileChunk(chunk)` +- JFR converted to `SentryProfile` using `IProfileConverter` +- Sent as envelope to Sentry + +## TRACE Mode Lifecycle +- `rootSpanCounter` incremented when sampled root span starts +- `rootSpanCounter` decremented when root span finishes +- Profiler runs while counter > 0 +- Allows multiple concurrent transactions to share profiler session + +## Rate Limiting and Offline + +### Rate Limiting +- Registers as `RateLimiter.IRateLimitObserver` +- When rate limited for `ProfileChunk` or `All`: + - Stops immediately without restart + - Discards current chunk + - Resets profiler ID +- Checked before starting +- Does NOT auto-restart when rate limit expires + +### Offline Behavior +- JFR files written to `cacheDirPath`, marked `deleteOnExit()` +- `ProfileChunk.Builder` buffered in `payloadBuilders` if offline +- Sent when SDK comes online, files deleted after successful send +- Profiler can start before SDK initialized - chunks buffered until scopes available (`initScopes()`) + +## Platform Differences + +### JVM (sentry-async-profiler) +- Native async-profiler library +- Platform: "java" +- Chunk ID always `EMPTY_ID` + +### Android (sentry-android-core) +- `AndroidContinuousProfiler` with `Debug.startMethodTracingSampling()` +- Longer chunk duration (60s vs 10s for JVM) +- Includes measurements (frames, memory) +- Platform: "android" + +## Extending + +Implement `IContinuousProfiler` and `JavaContinuousProfilerProvider`, register in `META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider`. + +Implement `IProfileConverter` and `JavaProfileConverterProvider`, register in `META-INF/services/io.sentry.profiling.JavaProfileConverterProvider`. + +## Code Locations + +- `sentry/src/main/java/io/sentry/IContinuousProfiler.java` +- `sentry/src/main/java/io/sentry/ProfileChunk.java` +- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java` +- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java`