From 574d4cc24079e23b9e658b5d6a5c00a44b11c5a4 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 16 Jan 2026 16:05:44 +0100 Subject: [PATCH 1/4] add cursor rules for the profiling feature --- .cursor/rules/profiling.mdc | 133 ++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 .cursor/rules/profiling.mdc diff --git a/.cursor/rules/profiling.mdc b/.cursor/rules/profiling.mdc new file mode 100644 index 00000000000..c18890fdac4 --- /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` From eb1dc883c06b15d0feeb63174f3f78e8f0e4ff4c Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Thu, 22 Jan 2026 21:44:05 +0100 Subject: [PATCH 2/4] add profiling info to overview_dev.mdc --- .cursor/rules/overview_dev.mdc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.cursor/rules/overview_dev.mdc b/.cursor/rules/overview_dev.mdc index f05d2992d40..7be99b81279 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` From 7b648b7f1807338ddd8099de74df64b01968dae5 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 9 Mar 2026 08:21:44 +0100 Subject: [PATCH 3/4] split jvm and android profiling --- .cursor/rules/overview_dev.mdc | 25 ++++-- .cursor/rules/profiling_android.mdc | 83 +++++++++++++++++++ .../{profiling.mdc => profiling_jvm.mdc} | 27 ++---- 3 files changed, 106 insertions(+), 29 deletions(-) create mode 100644 .cursor/rules/profiling_android.mdc rename .cursor/rules/{profiling.mdc => profiling_jvm.mdc} (83%) diff --git a/.cursor/rules/overview_dev.mdc b/.cursor/rules/overview_dev.mdc index 7be99b81279..04576f38741 100644 --- a/.cursor/rules/overview_dev.mdc +++ b/.cursor/rules/overview_dev.mdc @@ -53,15 +53,21 @@ 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 +- **`profiling_jvm`**: Use when working with: + - JVM continuous profiling (`sentry-async-profiler` module) + - `IContinuousProfiler`, `JavaContinuousProfiler` + - `ProfileChunk`, chunk rotation, JFR file handling - `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) + - async-profiler integration, ServiceLoader discovery + - Rate limiting, offline caching, scopes integration + +- **`profiling_android`**: Use when working with: + - Android profiling (`sentry-android-core`) + - `AndroidContinuousProfiler`, `AndroidProfiler`, `AndroidTransactionProfiler` + - `Debug.startMethodTracingSampling()`, trace files + - Frame metrics, CPU, memory measurement collectors + - `SentryFrameMetricsCollector`, `SpanFrameMetricsCollector` + - App start profiling, `AndroidOptionsInitializer` setup ### Integration & Infrastructure - **`opentelemetry`**: Use when working with: @@ -94,4 +100,5 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - System test/e2e/sample → `e2e_tests` - Feature flag/addFeatureFlag/flag evaluation → `feature_flags` - Metrics/count/distribution/gauge → `metrics` - - Profiling/profiler/ProfileChunk/JFR → `profiling` + - JVM profiling/async-profiler/JFR/ProfileChunk → `profiling_jvm` + - Android profiling/AndroidProfiler/frame metrics/method tracing → `profiling_android` diff --git a/.cursor/rules/profiling_android.mdc b/.cursor/rules/profiling_android.mdc new file mode 100644 index 00000000000..4b51631cb70 --- /dev/null +++ b/.cursor/rules/profiling_android.mdc @@ -0,0 +1,83 @@ +--- +alwaysApply: false +description: Android Profiling (sentry-android-core) +--- +# Android Profiling + +Android profiling lives in `sentry-android-core` and uses `Debug.startMethodTracingSampling()` for trace collection, with additional measurement collectors for frames, CPU, and memory. + +## Key Classes + +### `AndroidContinuousProfiler` +- Implements `IContinuousProfiler` for continuous profiling across app lifecycle +- Delegates to `AndroidProfiler` for actual trace collection +- 60-second chunk duration (`MAX_CHUNK_DURATION_MILLIS`) +- Platform: "android" +- Maintains `rootSpanCounter` for TRACE mode, `profilerId` and `chunkId` per session/chunk +- Collects measurements via `CompositePerformanceCollector` and `SentryFrameMetricsCollector` +- Thread-safe with `lock` and `payloadLock` + +### `AndroidProfiler` +- Low-level wrapper around `Debug.startMethodTracingSampling()` +- Buffer size: 3MB, timeout: 30 seconds (for transaction profiling) +- `start()`: Calls `Debug.startMethodTracingSampling(path, bufferSize, intervalUs)` and registers frame metrics listener +- `endAndCollect()`: Stops tracing, collects measurements, returns `ProfileEndData` with trace file and measurements + +### `AndroidTransactionProfiler` +- Implements `ITransactionProfiler` for per-transaction profiling (legacy) +- `start()` / `bindTransaction()` / `onTransactionFinish()` lifecycle +- Returns `ProfilingTraceData` on finish + +## Measurement Collectors + +- **`SentryFrameMetricsCollector`**: Frame metrics via `Window.OnFrameMetricsAvailableListener` + - Uses `Choreographer` reflection for frame start timestamps + - Tracks slow frames (> expected duration at refresh rate) and frozen frames (> 700ms) +- **`AndroidMemoryCollector`**: Heap (`Runtime`) and native memory (`Debug.getNativeHeapSize()`) +- **`AndroidCpuCollector`**: CPU usage from `/proc/self/stat` +- **`SpanFrameMetricsCollector`**: Per-span frame metrics with interpolation + +## Measurements Included in Profile Chunks + +| Measurement | Unit | Source | +|---|---|---| +| Slow frame renders | nanoseconds | `SentryFrameMetricsCollector` | +| Frozen frame renders | nanoseconds | `SentryFrameMetricsCollector` | +| Screen frame rates | Hz | `SentryFrameMetricsCollector` | +| CPU usage | percent | `AndroidCpuCollector` | +| Memory footprint | bytes | `AndroidMemoryCollector` (heap) | +| Native memory footprint | bytes | `AndroidMemoryCollector` (native) | + +## Configuration + +Same base options as JVM profiling (`profilesSampleRate`, `profileLifecycle`, `profilingTracesHz`), plus: +- `profilingTracesDirPath` set automatically from app cache directory +- Frame metrics collector initialized with Android `Context` +- Setup in `AndroidOptionsInitializer.installDefaultIntegrations()` + +## Profiling Flow + +**Start**: `AndroidContinuousProfiler.start()` -> `AndroidProfiler.start()` -> `Debug.startMethodTracingSampling()` + register frame metrics listener + schedule 60s timeout + +**Chunk Rotation** (every 60s): Stop tracing -> collect measurements -> create `ProfileChunk.Builder` with measurements and trace file -> queue in `payloadBuilders` -> restart profiler + +**Sending**: `sendChunks()` builds each `ProfileChunk.Builder` and calls `scopes.captureProfileChunk()` + +**Lifecycle modes**: Same as JVM - MANUAL (explicit start/stop) and TRACE (`rootSpanCounter` for automatic lifecycle) + +## Initialization + +In `AndroidOptionsInitializer`: +- `AndroidContinuousProfiler` created with `AndroidProfiler`, `BuildInfoProvider`, `SentryFrameMetricsCollector` +- Profiler may already be running from app start profiling before SDK init +- If already running at init, existing chunk ID is preserved + +## Code Locations + +- `sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java` +- `sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java` +- `sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java` +- `sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java` +- `sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java` +- `sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java` +- `sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java` diff --git a/.cursor/rules/profiling.mdc b/.cursor/rules/profiling_jvm.mdc similarity index 83% rename from .cursor/rules/profiling.mdc rename to .cursor/rules/profiling_jvm.mdc index c18890fdac4..aed815bc3f1 100644 --- a/.cursor/rules/profiling.mdc +++ b/.cursor/rules/profiling_jvm.mdc @@ -1,10 +1,10 @@ --- alwaysApply: false -description: Java SDK Profiling +description: JVM Continuous Profiling (sentry-async-profiler) --- -# Java SDK Profiling +# JVM Continuous Profiling -The Sentry Java SDK provides continuous profiling through the `sentry-async-profiler` module, which integrates async-profiler for low-overhead CPU profiling. +The `sentry-async-profiler` module integrates async-profiler for low-overhead CPU profiling on JVM. ## Module Structure @@ -25,9 +25,10 @@ The Sentry Java SDK provides continuous profiling through the `sentry-async-prof ### `JavaContinuousProfiler` (sentry-async-profiler) - Wraps native async-profiler library - Writes JFR files to `profilingTracesDirPath` -- Rotates chunks periodically (`MAX_CHUNK_DURATION_MILLIS`) +- Rotates chunks periodically (`MAX_CHUNK_DURATION_MILLIS`, default 10s) - Implements `RateLimiter.IRateLimitObserver` for rate limiting - Maintains `rootSpanCounter` for TRACE mode lifecycle +- Platform: "java", Chunk ID always `EMPTY_ID` ### `ProfileChunk` - Contains profiler ID (session-level, persists across chunks), chunk ID, JFR file reference @@ -40,12 +41,11 @@ The Sentry Java SDK provides continuous profiling through the `sentry-async-prof ## Configuration -- **`profilesSampleRate`**: Sample rate (0.0 to 1.0). If set with `tracesSampleRate`, enables transaction profiling. If set alone, enables continuous profiling. +- **`profilesSampleRate`**: Sample rate (0.0 to 1.0) - **`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"); @@ -85,7 +85,7 @@ options.setProfileLifecycle(ProfileLifecycle.MANUAL); ## TRACE Mode Lifecycle - `rootSpanCounter` incremented when sampled root span starts -- `rootSpanCounter` decremented when root span finishes +- `rootSpanCounter` decremented when root span finishes - Profiler runs while counter > 0 - Allows multiple concurrent transactions to share profiler session @@ -106,19 +106,6 @@ options.setProfileLifecycle(ProfileLifecycle.MANUAL); - 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`. From c40ec291b85a388d7bdd45a9878c4691a6ce2e59 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 16 Mar 2026 16:06:30 +0100 Subject: [PATCH 4/4] remove profiling android rules, improve jvm rules, improve file name --- .cursor/rules/continuous_profiling_jvm.mdc | 174 +++++++++++++++++++++ .cursor/rules/overview_dev.mdc | 16 +- .cursor/rules/profiling_android.mdc | 83 ---------- .cursor/rules/profiling_jvm.mdc | 120 -------------- 4 files changed, 180 insertions(+), 213 deletions(-) create mode 100644 .cursor/rules/continuous_profiling_jvm.mdc delete mode 100644 .cursor/rules/profiling_android.mdc delete mode 100644 .cursor/rules/profiling_jvm.mdc diff --git a/.cursor/rules/continuous_profiling_jvm.mdc b/.cursor/rules/continuous_profiling_jvm.mdc new file mode 100644 index 00000000000..d9a911de25e --- /dev/null +++ b/.cursor/rules/continuous_profiling_jvm.mdc @@ -0,0 +1,174 @@ +--- +alwaysApply: false +description: JVM Continuous Profiling (sentry-async-profiler) +--- +# JVM Continuous Profiling + +Use this rule when working on JVM continuous profiling in `sentry-async-profiler` and the related core profiling abstractions in `sentry`. + +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. + +## Module Structure + +- **`sentry-async-profiler`**: standalone module containing the async-profiler integration + - Uses Java `ServiceLoader` discovery + - No direct dependency from core `sentry` module + - Enabled by adding the module as a dependency + +- **`sentry` core abstractions**: + - `IContinuousProfiler`: profiler lifecycle interface + - `ProfileChunk`: profile chunk payload sent to Sentry + - `IProfileConverter`: converts JVM JFR files into `SentryProfile` + - `ProfileLifecycle`: controls MANUAL vs TRACE lifecycle + - `ProfilingServiceLoader`: loads profiler and converter implementations via `ServiceLoader` + +## Key Classes + +### `JavaContinuousProfiler` (`sentry-async-profiler`) +- Wraps the native async-profiler library +- Writes JFR files to `profilingTracesDirPath` +- Rotates chunks periodically via `MAX_CHUNK_DURATION_MILLIS` (currently 10s) +- Implements `RateLimiter.IRateLimitObserver` +- Maintains `rootSpanCounter` for TRACE lifecycle +- Keeps a session-level `profilerId` across chunks until the profiling session ends +- `getChunkId()` currently returns `SentryId.EMPTY_ID`, but emitted `ProfileChunk`s get a fresh chunk id when built in `stop(...)` + +### `ProfileChunk` +- Carries `profilerId`, `chunkId`, timestamp, platform, measurements, and a JFR file reference +- Built via `ProfileChunk.Builder` +- For JVM, the JFR file is converted later during envelope item creation, not inside `JavaContinuousProfiler` + +### `ProfileLifecycle` +- `MANUAL`: explicit `Sentry.startProfiler()` / `Sentry.stopProfiler()` +- `TRACE`: profiler lifecycle follows active sampled root spans + +## Configuration + +Continuous profiling is **not** controlled by `profilesSampleRate`. + +Key options: +- **`profileSessionSampleRate`**: session-level sample rate for continuous profiling +- **`profileLifecycle`**: `ProfileLifecycle.MANUAL` (default) or `ProfileLifecycle.TRACE` +- **`cacheDirPath`**: base SDK cache directory; profiling traces are written under the derived `profilingTracesDirPath` +- **`profilingTracesHz`**: sampling frequency in Hz (default: 101) + +Continuous profiling is enabled when: +- `profilesSampleRate == null` +- `profilesSampler == null` +- `profileSessionSampleRate != null && profileSessionSampleRate > 0` + +Example: + +```java +options.setProfileSessionSampleRate(1.0); +options.setCacheDirPath("/tmp/sentry-cache"); +options.setProfileLifecycle(ProfileLifecycle.MANUAL); +options.setProfilingTracesHz(101); +``` + +## How It Works + +### Initialization +- `InitUtil.initializeProfiler(...)` resolves or creates the profiling traces directory +- `ProfilingServiceLoader.loadContinuousProfiler(...)` uses `ServiceLoader` to find `JavaContinuousProfilerProvider` +- `AsyncProfilerContinuousProfilerProvider` instantiates `JavaContinuousProfiler` +- `ProfilingServiceLoader.loadProfileConverter()` separately loads the `JavaProfileConverterProvider` + +### Profiling Flow + +**Start** +- Sampling decision is made via `TracesSampler.sampleSessionProfile(...)` +- Sampling is session-based and cached until `reevaluateSampling()` +- Scopes and rate limiter are initialized lazily via `initScopes()` +- Rate limits for `All` or `ProfileChunk` abort startup +- JFR filename is generated under `profilingTracesDirPath` +- async-profiler is started with a command like: + - `start,jfr,event=wall,nobatch,interval=,file=` +- Automatic chunk stop is scheduled after `MAX_CHUNK_DURATION_MILLIS` + +**Chunk Rotation** +- `stop(true)` stops async-profiler and validates the JFR file +- A `ProfileChunk.Builder` is created with: + - current `profilerId` + - a fresh `chunkId` + - trace file + - chunk timestamp + - platform `java` +- Builder is buffered in `payloadBuilders` +- Chunks are sent if scopes are available +- Profiling is restarted for the next chunk + +**Stop** +- `MANUAL`: stop immediately, do not restart, reset `profilerId` +- `TRACE`: decrement `rootSpanCounter`; stop only when it reaches 0 +- `close(...)` also forces shutdown and resets TRACE state + +### Sending and Conversion +- `JavaContinuousProfiler` buffers `ProfileChunk.Builder` instances +- `sendChunks(...)` builds `ProfileChunk` objects and calls `scopes.captureProfileChunk(...)` +- `SentryClient.captureProfileChunk(...)` creates an envelope item +- JVM JFR-to-`SentryProfile` conversion happens in `SentryEnvelopeItem.fromProfileChunk(...)` using the loaded `IProfileConverter` +- Trace files are deleted in the envelope item path after serialization attempts + +## TRACE Mode Lifecycle +- `rootSpanCounter` increments when sampled root spans start +- `rootSpanCounter` decrements when root spans finish +- Profiler runs while `rootSpanCounter > 0` +- Multiple concurrent sampled transactions can share the same profiling session +- Be careful when changing lifecycle logic: this area is lock-protected and concurrency-sensitive + +## Rate Limiting and Buffering + +### Rate Limiting +- Registers as a `RateLimiter.IRateLimitObserver` +- If rate limited for `ProfileChunk` or `All`: + - profiler stops immediately + - it does not auto-restart when the limit expires +- Startup also checks rate limiting before profiling begins + +### Buffering / pre-init behavior +- JFR files are written to `profilingTracesDirPath` and marked `deleteOnExit()` when a chunk is accepted +- If scopes are not yet available, `ProfileChunk.Builder`s remain buffered in memory in `payloadBuilders` +- This commonly matters for profiling that starts before SDK scopes are ready +- 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 + +## Extending + +To add or replace JVM profiler implementations: +- implement `IContinuousProfiler` +- implement `JavaContinuousProfilerProvider` +- register provider in: + - `META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider` + +To add or replace JVM profile conversion: +- implement `IProfileConverter` +- implement `JavaProfileConverterProvider` +- register provider in: + - `META-INF/services/io.sentry.profiling.JavaProfileConverterProvider` + +## Code Locations + +Primary implementation: +- `sentry/src/main/java/io/sentry/IContinuousProfiler.java` +- `sentry/src/main/java/io/sentry/ProfileChunk.java` +- `sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java` +- `sentry/src/main/java/io/sentry/util/InitUtil.java` +- `sentry/src/main/java/io/sentry/SentryEnvelopeItem.java` +- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java` +- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java` +- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java` +- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java` + +Tests to read first: +- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt` +- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilingServiceLoaderTest.kt` +- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt` + +## LLM Guidance + +This rule is good enough for orientation, but for actual code changes always verify: +- the sampling path in `TracesSampler` +- continuous profiling enablement in `SentryOptions` +- lifecycle entry points in `Scopes` and `SentryTracer` +- conversion and file deletion behavior in `SentryEnvelopeItem` +- existing tests before changing concurrency or lifecycle semantics diff --git a/.cursor/rules/overview_dev.mdc b/.cursor/rules/overview_dev.mdc index 72f7bb28fe7..17ce98f07be 100644 --- a/.cursor/rules/overview_dev.mdc +++ b/.cursor/rules/overview_dev.mdc @@ -66,7 +66,7 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - `SentryMetricsEvent`, `SentryMetricsEvents` - `SentryOptions.getMetrics()`, `beforeSend` callback -- **`profiling_jvm`**: Use when working with: +- **`continuous_profiling_jvm`**: Use when working with: - JVM continuous profiling (`sentry-async-profiler` module) - `IContinuousProfiler`, `JavaContinuousProfiler` - `ProfileChunk`, chunk rotation, JFR file handling @@ -74,13 +74,9 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - async-profiler integration, ServiceLoader discovery - Rate limiting, offline caching, scopes integration -- **`profiling_android`**: Use when working with: - - Android profiling (`sentry-android-core`) - - `AndroidContinuousProfiler`, `AndroidProfiler`, `AndroidTransactionProfiler` - - `Debug.startMethodTracingSampling()`, trace files - - Frame metrics, CPU, memory measurement collectors - - `SentryFrameMetricsCollector`, `SpanFrameMetricsCollector` - - App start profiling, `AndroidOptionsInitializer` setup +- **Android profiling**: There is currently no dedicated rule for this area yet. + - Inspect the relevant `sentry-android-core` profiling code directly + - Fetch other related rules as needed (for example `options`, `offline`, or `api`) ### Integration & Infrastructure - **`opentelemetry`**: Use when working with: @@ -123,5 +119,5 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - Feature flag/addFeatureFlag/flag evaluation → `feature_flags` - Metrics/count/distribution/gauge → `metrics` - PR/pull request/stacked PR/stack → `pr` - - JVM profiling/async-profiler/JFR/ProfileChunk → `profiling_jvm` - - Android profiling/AndroidProfiler/frame metrics/method tracing → `profiling_android` + - JVM continuous profiling/async-profiler/JFR/ProfileChunk → `continuous_profiling_jvm` + - Android continuous profiling/AndroidProfiler/frame metrics/method tracing → no dedicated rule yet; inspect the code directly diff --git a/.cursor/rules/profiling_android.mdc b/.cursor/rules/profiling_android.mdc deleted file mode 100644 index 4b51631cb70..00000000000 --- a/.cursor/rules/profiling_android.mdc +++ /dev/null @@ -1,83 +0,0 @@ ---- -alwaysApply: false -description: Android Profiling (sentry-android-core) ---- -# Android Profiling - -Android profiling lives in `sentry-android-core` and uses `Debug.startMethodTracingSampling()` for trace collection, with additional measurement collectors for frames, CPU, and memory. - -## Key Classes - -### `AndroidContinuousProfiler` -- Implements `IContinuousProfiler` for continuous profiling across app lifecycle -- Delegates to `AndroidProfiler` for actual trace collection -- 60-second chunk duration (`MAX_CHUNK_DURATION_MILLIS`) -- Platform: "android" -- Maintains `rootSpanCounter` for TRACE mode, `profilerId` and `chunkId` per session/chunk -- Collects measurements via `CompositePerformanceCollector` and `SentryFrameMetricsCollector` -- Thread-safe with `lock` and `payloadLock` - -### `AndroidProfiler` -- Low-level wrapper around `Debug.startMethodTracingSampling()` -- Buffer size: 3MB, timeout: 30 seconds (for transaction profiling) -- `start()`: Calls `Debug.startMethodTracingSampling(path, bufferSize, intervalUs)` and registers frame metrics listener -- `endAndCollect()`: Stops tracing, collects measurements, returns `ProfileEndData` with trace file and measurements - -### `AndroidTransactionProfiler` -- Implements `ITransactionProfiler` for per-transaction profiling (legacy) -- `start()` / `bindTransaction()` / `onTransactionFinish()` lifecycle -- Returns `ProfilingTraceData` on finish - -## Measurement Collectors - -- **`SentryFrameMetricsCollector`**: Frame metrics via `Window.OnFrameMetricsAvailableListener` - - Uses `Choreographer` reflection for frame start timestamps - - Tracks slow frames (> expected duration at refresh rate) and frozen frames (> 700ms) -- **`AndroidMemoryCollector`**: Heap (`Runtime`) and native memory (`Debug.getNativeHeapSize()`) -- **`AndroidCpuCollector`**: CPU usage from `/proc/self/stat` -- **`SpanFrameMetricsCollector`**: Per-span frame metrics with interpolation - -## Measurements Included in Profile Chunks - -| Measurement | Unit | Source | -|---|---|---| -| Slow frame renders | nanoseconds | `SentryFrameMetricsCollector` | -| Frozen frame renders | nanoseconds | `SentryFrameMetricsCollector` | -| Screen frame rates | Hz | `SentryFrameMetricsCollector` | -| CPU usage | percent | `AndroidCpuCollector` | -| Memory footprint | bytes | `AndroidMemoryCollector` (heap) | -| Native memory footprint | bytes | `AndroidMemoryCollector` (native) | - -## Configuration - -Same base options as JVM profiling (`profilesSampleRate`, `profileLifecycle`, `profilingTracesHz`), plus: -- `profilingTracesDirPath` set automatically from app cache directory -- Frame metrics collector initialized with Android `Context` -- Setup in `AndroidOptionsInitializer.installDefaultIntegrations()` - -## Profiling Flow - -**Start**: `AndroidContinuousProfiler.start()` -> `AndroidProfiler.start()` -> `Debug.startMethodTracingSampling()` + register frame metrics listener + schedule 60s timeout - -**Chunk Rotation** (every 60s): Stop tracing -> collect measurements -> create `ProfileChunk.Builder` with measurements and trace file -> queue in `payloadBuilders` -> restart profiler - -**Sending**: `sendChunks()` builds each `ProfileChunk.Builder` and calls `scopes.captureProfileChunk()` - -**Lifecycle modes**: Same as JVM - MANUAL (explicit start/stop) and TRACE (`rootSpanCounter` for automatic lifecycle) - -## Initialization - -In `AndroidOptionsInitializer`: -- `AndroidContinuousProfiler` created with `AndroidProfiler`, `BuildInfoProvider`, `SentryFrameMetricsCollector` -- Profiler may already be running from app start profiling before SDK init -- If already running at init, existing chunk ID is preserved - -## Code Locations - -- `sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java` -- `sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java` -- `sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java` -- `sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java` -- `sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java` -- `sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java` -- `sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java` diff --git a/.cursor/rules/profiling_jvm.mdc b/.cursor/rules/profiling_jvm.mdc deleted file mode 100644 index aed815bc3f1..00000000000 --- a/.cursor/rules/profiling_jvm.mdc +++ /dev/null @@ -1,120 +0,0 @@ ---- -alwaysApply: false -description: JVM Continuous Profiling (sentry-async-profiler) ---- -# JVM Continuous Profiling - -The `sentry-async-profiler` module integrates async-profiler for low-overhead CPU profiling on JVM. - -## 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`, default 10s) -- Implements `RateLimiter.IRateLimitObserver` for rate limiting -- Maintains `rootSpanCounter` for TRACE mode lifecycle -- Platform: "java", Chunk ID always `EMPTY_ID` - -### `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) -- **`profileLifecycle`**: `ProfileLifecycle.MANUAL` (default) or `ProfileLifecycle.TRACE` -- **`cacheDirPath`**: Directory for JFR files (required) -- **`profilingTracesHz`**: Sampling frequency in Hz (default: 101) - -```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()`) - -## 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`