From 574d4cc24079e23b9e658b5d6a5c00a44b11c5a4 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 16 Jan 2026 16:05:44 +0100 Subject: [PATCH 1/3] 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/3] 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/3] 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`.