diff --git a/.cursor/rules/overview_dev.mdc b/.cursor/rules/overview_dev.mdc index a982cfe960..72f7bb28fe 100644 --- a/.cursor/rules/overview_dev.mdc +++ b/.cursor/rules/overview_dev.mdc @@ -30,7 +30,7 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - **`scopes`**: Use when working with: - Hub/Scope management, forking, or lifecycle - - `Sentry.getCurrentScopes()`, `pushScope()`, `withScope()` + - `Sentry.getCurrentScopes()`, `pushScope()`, `withScope()` - `ScopeType` (GLOBAL, ISOLATION, CURRENT) - Thread-local storage, scope bleeding issues - Migration from Hub API (v7 → v8) @@ -66,6 +66,22 @@ 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: + - JVM continuous profiling (`sentry-async-profiler` module) + - `IContinuousProfiler`, `JavaContinuousProfiler` + - `ProfileChunk`, chunk rotation, JFR file handling + - `ProfileLifecycle` (MANUAL vs TRACE modes) + - 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: - OpenTelemetry modules (`sentry-opentelemetry-*`) @@ -99,7 +115,7 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - Public API/apiDump/.api files/binary compatibility/new method → `api` - Options/SentryOptions/ExternalOptions/ManifestMetadataReader/sentry.properties → `options` - Scope/Hub/forking → `scopes` - - Duplicate/dedup → `deduplication` + - Duplicate/dedup → `deduplication` - OpenTelemetry/tracing/spans → `opentelemetry` - new module/integration/sample → `new_module` - Cache/offline/network → `offline` @@ -107,3 +123,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` diff --git a/.cursor/rules/profiling_android.mdc b/.cursor/rules/profiling_android.mdc new file mode 100644 index 0000000000..4b51631cb7 --- /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_jvm.mdc b/.cursor/rules/profiling_jvm.mdc new file mode 100644 index 0000000000..aed815bc3f --- /dev/null +++ b/.cursor/rules/profiling_jvm.mdc @@ -0,0 +1,120 @@ +--- +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`