Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions .cursor/rules/overview_dev.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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-*`)
Expand Down Expand Up @@ -99,11 +115,13 @@ 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`
- System test/e2e/sample → `e2e_tests`
- 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`
83 changes: 83 additions & 0 deletions .cursor/rules/profiling_android.mdc
Original file line number Diff line number Diff line change
@@ -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`
120 changes: 120 additions & 0 deletions .cursor/rules/profiling_jvm.mdc
Original file line number Diff line number Diff line change
@@ -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: `<cacheDirPath>/<UUID>.jfr`
- Execute async-profiler: `start,jfr,event=wall,nobatch,interval=<interval>,file=<path>`
- 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`
Loading