Repository: getsentry/sentry-cocoa
Pull request #5133: fix(session-replay): improve frame presentation timing calculations
Author: login: philprime
Labels: name: Session Replay
name: Cocoa
Created: 2025-04-24T11:51:27Z | Merged: 2025-04-24T15:24:15Z

## :scroll: Description

Updated the calculation of presentation time for video frames to use integer math, ensuring frame-accurate timing and avoiding floating-point rounding issues. 

## :bulb: Motivation and Context

This change is crucial for maintaining consistent frame spacing, especially at low frame rates like 1 FPS.

This should also fix crashes when changing the FPS to higher values, which is not possible (yet).

## :green_heart: How did you test it?

- Used the sample app to run locally, viewing session replay video in Sentry product.
- Used `ffprobe` to view information on file:

```
$ ffprobe -show_streams -show_format -v error /Users/philip/Library/Developer/CoreSimulator/Devices/B406E97C-4EC0-483F-82EA-32B594AB6038/data/Containers/Data/Application/424CFD83-82C0-4765-A58C-645960512C4F/Library/Caches/io.sentry/b810aaca937def33af3796f237e6793ec907f1e4/replay/A2EF1E73-C047-48E5-9205-3C09D81C9F8D/767188156.357788.mp4
[STREAM]
index=0
codec_name=h264
codec_long_name=H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
profile=Constrained Baseline
codec_type=video
codec_tag_string=avc1
codec_tag=0x31637661
width=393
height=852
coded_width=393
coded_height=852
closed_captions=0
film_grain=0
has_b_frames=0
sample_aspect_ratio=1:1
display_aspect_ratio=131:284
pix_fmt=yuv420p
level=30
color_range=tv
color_space=unknown
color_transfer=unknown
color_primaries=unknown
chroma_location=left
field_order=progressive
refs=1
is_avc=true
nal_length_size=4
id=0x1
r_frame_rate=1/1
avg_frame_rate=1/1
time_base=1/600
start_pts=0
start_time=0.000000
duration_ts=3000
duration=5.000000
bit_rate=7680
max_bit_rate=N/A
bits_per_raw_sample=8
nb_frames=5
nb_read_frames=N/A
nb_read_packets=N/A
extradata_size=36
DISPOSITION:default=1
DISPOSITION:dub=0
DISPOSITION:original=0
DISPOSITION:comment=0
DISPOSITION:lyrics=0
DISPOSITION:karaoke=0
DISPOSITION:forced=0
DISPOSITION:hearing_impaired=0
DISPOSITION:visual_impaired=0
DISPOSITION:clean_effects=0
DISPOSITION:attached_pic=0
DISPOSITION:timed_thumbnails=0
DISPOSITION:non_diegetic=0
DISPOSITION:captions=0
DISPOSITION:descriptions=0
DISPOSITION:metadata=0
DISPOSITION:dependent=0
DISPOSITION:still_image=0
DISPOSITION:multilayer=0
TAG:creation_time=2025-04-24T11:49:21.000000Z
TAG:language=und
TAG:handler_name=Core Media Video
TAG:vendor_id=[0][0][0][0]
[/STREAM]
[FORMAT]
filename=/Users/philip/Library/Developer/CoreSimulator/Devices/B406E97C-4EC0-483F-82EA-32B594AB6038/data/Containers/Data/Application/424CFD83-82C0-4765-A58C-645960512C4F/Library/Caches/io.sentry/b810aaca937def33af3796f237e6793ec907f1e4/replay/A2EF1E73-C047-48E5-9205-3C09D81C9F8D/767188156.357788.mp4
nb_streams=1
nb_programs=0
nb_stream_groups=0
format_name=mov,mp4,m4a,3gp,3g2,mj2
format_long_name=QuickTime / MOV
start_time=0.000000
duration=5.000000
size=5584
bit_rate=8934
probe_score=100
TAG:major_brand=mp42
TAG:minor_version=1
TAG:compatible_brands=isommp41mp42
TAG:creation_time=2025-04-24T11:49:21.000000Z
[/FORMAT]
```

Relevant output:

```
r_frame_rate=1/1
avg_frame_rate=1/1
```

## :pencil: Checklist

You have to check all boxes before merging:

- [ ] I added tests to verify the changes.
- [X] No new PII added or SDK only sends newly added PII if `sendDefaultPII` is enabled.
- [X] I updated the docs if needed.
- [X] I updated the wizard if needed.
- [X] Review from the native team if needed.
- [X] No breaking change or entry added to the changelog.
- [X] No breaking change for hybrid SDKs or communicated to hybrid SDKs.



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e837ddae0c4..76bd78f3f4b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 ### Improvements
 
+- Improve session replay frame presentation timing calculations (#5133)
 - Use wider compatible video encoding options for Session Replay (#5134)
 
 ## 8.49.1
diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift
index 7bba26b5be2..798f6938e2a 100644
--- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift
+++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift
@@ -4,6 +4,7 @@
 @_implementationOnly import _SentryPrivate
 import AVFoundation
 import CoreGraphics
+import CoreMedia
 import Foundation
 import UIKit
 
@@ -143,7 +144,8 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
         }
         return videos
     }
-    
+
+    // swiftlint:disable:next function_body_length
     private func renderVideo(with videoFrames: [SentryReplayFrame], from: inout Int, at outputFileURL: URL) throws -> SentryVideoInfo? {
         guard from < videoFrames.count, let image = UIImage(contentsOfFile: videoFrames[from].imagePath) else { return nil }
         let videoWidth = image.size.width * CGFloat(videoScale)
@@ -188,7 +190,10 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
                 }
                 lastImageSize = image.size
                 
-                let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate))
+                let presentTime = SentryOnDemandReplay.calculatePresentationTime(
+                    forFrameAtIndex: frameCount,
+                    frameRate: self.frameRate
+                ).timeValue
                 if currentPixelBuffer.append(image: image, presentationTime: presentTime) != true {
                     videoWriter.cancelWriting()
                     result = .failure(videoWriter.error ?? SentryOnDemandReplayError.errorRenderingVideo )
@@ -301,6 +306,28 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
             ] as [String: Any]
         ]
     }
+
+    /// Calculates the presentation time for a frame at a given index and frame rate.
+    ///
+    /// The return value is an `NSValue` containing a `CMTime` object representing the calculated presentation time.
+    /// The `CMTime` must be wrapped as this class is exposed to Objective-C via `Sentry-Swift.h`, and Objective-C does not support `CMTime`
+    /// as a return value.
+    ///
+    /// - Parameters:
+    ///   - index: Index of the frame, counted from 0.
+    ///   - frameRate: Number of frames per second.
+    /// - Returns: `NSValue` containing the `CMTime` representing the calculated presentation time. Can be accessed using the `timeValue` property.
+    internal static func calculatePresentationTime(forFrameAtIndex index: Int, frameRate: Int) -> NSValue {
+        // Generate the presentation time for the current frame using integer math.
+        // This avoids floating-point rounding issues and ensures frame-accurate timing,
+        // which is critical for AVAssetWriter at low frame rates like 1 FPS.
+        // By defining timePerFrame as (1 / frameRate) and multiplying it by the frame index,
+        // we guarantee consistent spacing between frames and precise control over the timeline.
+        let timePerFrame = CMTimeMake(value: 1, timescale: Int32(frameRate))
+        let presentTime = CMTimeMultiply(timePerFrame, multiplier: Int32(index))
+
+        return NSValue(time: presentTime)
+    }
 }
 
 #endif // os(iOS) || os(tvOS)
diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift
index f1592771f21..a978526bc23 100644
--- a/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift
+++ b/Tests/SentryTests/Integrations/SessionReplay/SentryOnDemandReplayTests.swift
@@ -1,4 +1,5 @@
 import AVFoundation
+import CoreMedia
 import Foundation
 @testable import Sentry
 import SentryTestUtils
@@ -206,6 +207,86 @@ class SentryOnDemandReplayTests: XCTestCase {
         XCTAssertEqual(secondVideo.height, 10)
     }
 
+    func testCalculatePresentationTime_withOneFPS_shouldReturnTiming() {
+        // -- Arrange --
+        let framesPerSecond = 1
+
+        // -- Act --
+        let zeroIndexTime = SentryOnDemandReplay.calculatePresentationTime(forFrameAtIndex: 0, frameRate: framesPerSecond)
+        let firstIndexTime = SentryOnDemandReplay.calculatePresentationTime(forFrameAtIndex: 1, frameRate: framesPerSecond)
+        let secondIndexTime = SentryOnDemandReplay.calculatePresentationTime(forFrameAtIndex: 2, frameRate: framesPerSecond)
+        let largeIndexTime = SentryOnDemandReplay.calculatePresentationTime(forFrameAtIndex: 1_337, frameRate: framesPerSecond)
+
+        // -- Assert --
+        XCTAssertEqual(zeroIndexTime.timeValue.value, 0)
+        XCTAssertEqual(zeroIndexTime.timeValue.timescale, 1)
+        XCTAssertEqual(zeroIndexTime.timeValue.seconds, 0)
+
+        XCTAssertEqual(firstIndexTime.timeValue.value, 1)
+        XCTAssertEqual(firstIndexTime.timeValue.timescale, 1)
+        XCTAssertEqual(firstIndexTime.timeValue.seconds, 1)
+
+        XCTAssertEqual(secondIndexTime.timeValue.value, 2)
+        XCTAssertEqual(secondIndexTime.timeValue.timescale, 1)
+        XCTAssertEqual(secondIndexTime.timeValue.seconds, 2)
+
+        XCTAssertEqual(largeIndexTime.timeValue.value, 1_337)
+        XCTAssertEqual(largeIndexTime.timeValue.timescale, 1)
+        XCTAssertEqual(largeIndexTime.timeValue.seconds, 1_337)
+    }
+
+    func testCalculatePresentationTime_withMoreThanOneFPS_shouldReturnTiming() {
+        // -- Arrange --
+        let framesPerSecond = 4
+
+        // -- Act --
+        let zeroIndexTime = SentryOnDemandReplay.calculatePresentationTime(forFrameAtIndex: 0, frameRate: framesPerSecond)
+        let firstIndexTime = SentryOnDemandReplay.calculatePresentationTime(forFrameAtIndex: 1, frameRate: framesPerSecond)
+        let secondIndexTime = SentryOnDemandReplay.calculatePresentationTime(forFrameAtIndex: 2, frameRate: framesPerSecond)
+        let largeIndexTime = SentryOnDemandReplay.calculatePresentationTime(forFrameAtIndex: 1_337, frameRate: framesPerSecond)
+
+        // -- Assert --
+        XCTAssertEqual(zeroIndexTime.timeValue.value, 0)
+        XCTAssertEqual(zeroIndexTime.timeValue.timescale, 4)
+        XCTAssertEqual(zeroIndexTime.timeValue.seconds, 0)
+
+        XCTAssertEqual(firstIndexTime.timeValue.value, 1)
+        XCTAssertEqual(firstIndexTime.timeValue.timescale, 4)
+        XCTAssertEqual(firstIndexTime.timeValue.seconds, 0.25)
+
+        XCTAssertEqual(secondIndexTime.timeValue.value, 2)
+        XCTAssertEqual(secondIndexTime.timeValue.timescale, 4)
+        XCTAssertEqual(secondIndexTime.timeValue.seconds, 0.5)
+
+        XCTAssertEqual(largeIndexTime.timeValue.value, 1_337)
+        XCTAssertEqual(largeIndexTime.timeValue.timescale, 4)
+        XCTAssertEqual(largeIndexTime.timeValue.seconds, 334.25)
+    }
+
+    func testCalculatePresentationTime_withNegativeFPS_shouldReturnInvalidTime() {
+        // -- Arrange --
+        let framesPerSecond = -4
+
+        // -- Act --
+        let time = SentryOnDemandReplay.calculatePresentationTime(forFrameAtIndex: 3, frameRate: framesPerSecond)
+
+        // -- Assert --
+        XCTAssertFalse(time.timeValue.isValid)
+    }
+
+    func testCalculatePresentationTime_withNegativeIndex_shouldReturnNegativeTime() {
+        // -- Arrange --
+        let framesPerSecond = 4
+
+        // -- Act --
+        let time = SentryOnDemandReplay.calculatePresentationTime(forFrameAtIndex: -3, frameRate: framesPerSecond)
+
+        // -- Assert --
+        XCTAssertEqual(time.timeValue.value, -3)
+        XCTAssertEqual(time.timeValue.timescale, 4)
+        XCTAssertEqual(time.timeValue.seconds, -0.75)
+    }
+
     // This test case with zero size is not particularly handled, but used
     // to lock down the expected behaviour.
     func testCreateVideoSettings_zeroSize_shouldReturnFullSettings() throws {


Reviews:
user: login: philipphofmann
body: Thanks.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-24T13:02:32Z
user: login: philprime
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T13:14:01Z
user: login: philipphofmann
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T13:20:45Z
user: login: philprime
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T14:05:14Z

Review comments:
diff_hunk: @@ -188,7 +189,13 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
                 }
                 lastImageSize = image.size
                 
-                let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate))
+                // Generate the presentation time for the current frame using integer math.
+                // This avoids floating-point rounding issues and ensures frame-accurate timing,
+                // which is critical for AVAssetWriter at low frame rates like 1 FPS.
+                // By defining timePerFrame as (1 / frameRate) and multiplying it by the frame index,
+                // we guarantee consistent spacing between frames and precise control over the timeline.
+                let timePerFrame = CMTimeMake(value: 1, timescale: Int32(self.frameRate))
+                let presentTime = CMTimeMultiply(timePerFrame, multiplier: Int32(frameCount))
path: Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift
user: login: philipphofmann
body: `m`: If this logic is crucial, you could extract it to a helper method and write tests just for that helper method to ensure it works correctly. Basically something like this. You can decide if that's worth the effort or not.
```
func calcPresentTime(frameCount: Int, frameRate: Int32) -> CMTime {
    let timePerFrame = CMTimeMake(value: 1, timescale: frameRate)
    return CMTimeMultiply(timePerFrame, multiplier: Int32(frameCount))
}
```
created_at: 2025-04-24T12:59:01Z
author_association: MEMBER
diff_hunk: @@ -188,7 +189,13 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
                 }
                 lastImageSize = image.size
                 
-                let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate))
+                // Generate the presentation time for the current frame using integer math.
+                // This avoids floating-point rounding issues and ensures frame-accurate timing,
+                // which is critical for AVAssetWriter at low frame rates like 1 FPS.
+                // By defining timePerFrame as (1 / frameRate) and multiplying it by the frame index,
+                // we guarantee consistent spacing between frames and precise control over the timeline.
+                let timePerFrame = CMTimeMake(value: 1, timescale: Int32(self.frameRate))
+                let presentTime = CMTimeMultiply(timePerFrame, multiplier: Int32(frameCount))
path: Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift
user: login: philprime
body: Good idea, I wanted to keep the PR small, but moving this to another method with a unit test can ensure safety for different FPS
created_at: 2025-04-24T13:14:01Z
author_association: MEMBER
diff_hunk: @@ -188,7 +189,13 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
                 }
                 lastImageSize = image.size
                 
-                let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate))
+                // Generate the presentation time for the current frame using integer math.
+                // This avoids floating-point rounding issues and ensures frame-accurate timing,
+                // which is critical for AVAssetWriter at low frame rates like 1 FPS.
+                // By defining timePerFrame as (1 / frameRate) and multiplying it by the frame index,
+                // we guarantee consistent spacing between frames and precise control over the timeline.
+                let timePerFrame = CMTimeMake(value: 1, timescale: Int32(self.frameRate))
+                let presentTime = CMTimeMultiply(timePerFrame, multiplier: Int32(frameCount))
path: Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift
user: login: philipphofmann
body: I think it would still be small and digestible. Up to you.
created_at: 2025-04-24T13:20:45Z
author_association: MEMBER
diff_hunk: @@ -188,7 +189,13 @@ class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker {
                 }
                 lastImageSize = image.size
                 
-                let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(1 / self.frameRate))
+                // Generate the presentation time for the current frame using integer math.
+                // This avoids floating-point rounding issues and ensures frame-accurate timing,
+                // which is critical for AVAssetWriter at low frame rates like 1 FPS.
+                // By defining timePerFrame as (1 / frameRate) and multiplying it by the frame index,
+                // we guarantee consistent spacing between frames and precise control over the timeline.
+                let timePerFrame = CMTimeMake(value: 1, timescale: Int32(self.frameRate))
+                let presentTime = CMTimeMultiply(timePerFrame, multiplier: Int32(frameCount))
path: Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift
user: login: philprime
body: I had to wrap the return type `CMTime` as an `NSValue`, but other than that it should be fine to merge now.
created_at: 2025-04-24T14:05:14Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-24T11:58:29Z
body: ## [Codecov](https://app.codecov.io/gh/getsentry/sentry-cocoa/pull/5133?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 92.802%. Comparing base [(`9e745c1`)](https://app.codecov.io/gh/getsentry/sentry-cocoa/commit/9e745c10f8ff378602454a68fcc57cadd3d28dd2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) to head [(`8e30a7c`)](https://app.codecov.io/gh/getsentry/sentry-cocoa/commit/8e30a7c55c33611a155e362732618096502dfec6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/getsentry/sentry-cocoa/pull/5133/graphs/tree.svg?width=650&height=150&src=pr&token=PTZKtOJlrs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)](https://app.codecov.io/gh/getsentry/sentry-cocoa/pull/5133?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)

```diff
@@              Coverage Diff              @@
##              main     #5133       +/-   ##
=============================================
- Coverage   92.809%   92.802%   -0.007%     
=============================================
  Files          676       676               
  Lines        83981     84070       +89     
  Branches     30561     30597       +36     
=============================================
+ Hits         77942     78019       +77     
- Misses        5941      5951       +10     
- Partials        98       100        +2     
```

| [Files with missing lines](https://app.codecov.io/gh/getsentry/sentry-cocoa/pull/5133?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) | Coverage Δ | |
|---|---|---|
| [...egrations/SessionReplay/SentryOnDemandReplay.swift](https://app.codecov.io/gh/getsentry/sentry-cocoa/pull/5133?src=pr&el=tree&filepath=Sources%2FSwift%2FIntegrations%2FSessionReplay%2FSentryOnDemandReplay.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-U291cmNlcy9Td2lmdC9JbnRlZ3JhdGlvbnMvU2Vzc2lvblJlcGxheS9TZW50cnlPbkRlbWFuZFJlcGxheS5zd2lmdA==) | `96.043% <100.000%> (+0.209%)` | :arrow_up: |
| [...ions/SessionReplay/SentryOnDemandReplayTests.swift](https://app.codecov.io/gh/getsentry/sentry-cocoa/pull/5133?src=pr&el=tree&filepath=Tests%2FSentryTests%2FIntegrations%2FSessionReplay%2FSentryOnDemandReplayTests.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-VGVzdHMvU2VudHJ5VGVzdHMvSW50ZWdyYXRpb25zL1Nlc3Npb25SZXBsYXkvU2VudHJ5T25EZW1hbmRSZXBsYXlUZXN0cy5zd2lmdA==) | `92.452% <100.000%> (+2.370%)` | :arrow_up: |

... and [12 files with indirect coverage changes](https://app.codecov.io/gh/getsentry/sentry-cocoa/pull/5133/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)

------

[Continue to review full report in Codecov by Sentry](https://app.codecov.io/gh/getsentry/sentry-cocoa/pull/5133?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/getsentry/sentry-cocoa/pull/5133?dropdown=coverage&src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Last update [9e745c1...8e30a7c](https://app.codecov.io/gh/getsentry/sentry-cocoa/pull/5133?dropdown=coverage&src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-24T12:05:05Z
body: ##  Performance metrics :rocket:
<table>
  <tr>
    <th>&nbsp;</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr>
  <tr>
    <th align="left">Startup time</th>
    <td align="right">1232.45 ms</td>
    <td align="right">1256.21 ms</td>
    <td align="right"><strong>23.76 ms</strong></td>
  </tr>
  <tr>
    <th align="left">Size</th>
    <td align="right">22.30 KiB</td>
    <td align="right">848.97 KiB</td>
    <td align="right"><strong>826.67 KiB</strong></td>
  </tr>
</table>
<details><summary><h3>Baseline results on branch: main</h3></summary>
<h3>Startup times</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">f25febbb81e40893076ff878f59f146ddb948ab4</th>
  <td align="right">1238.80 ms</td>
  <td align="right">1245.35 ms</td>
  <td align="right"><strong>6.56 ms</strong></td>
</tr><tr>
  <th align="left">69d87595b411be6c93e72de62bc47457202fb72d</th>
  <td align="right">1235.71 ms</td>
  <td align="right">1241.34 ms</td>
  <td align="right"><strong>5.63 ms</strong></td>
</tr><tr>
  <th align="left">ce4cfaff577257316d77093b2100c917a4ba2781</th>
  <td align="right">1203.61 ms</td>
  <td align="right">1218.86 ms</td>
  <td align="right"><strong>15.25 ms</strong></td>
</tr><tr>
  <th align="left">74192858dff8cf729ce015b06fe9d3ff64fe7b74</th>
  <td align="right">1209.53 ms</td>
  <td align="right">1244.72 ms</td>
  <td align="right"><strong>35.19 ms</strong></td>
</tr><tr>
  <th align="left">b15521ec4775f623b296e1fca558ffac46447bb8</th>
  <td align="right">1224.44 ms</td>
  <td align="right">1251.13 ms</td>
  <td align="right"><strong>26.68 ms</strong></td>
</tr><tr>
  <th align="left">bd2afa60621240d5596fdca06191173dcb6bf982</th>
  <td align="right">1245.24 ms</td>
  <td align="right">1263.18 ms</td>
  <td align="right"><strong>17.94 ms</strong></td>
</tr><tr>
  <th align="left">53b29ac3c0c486a5bf20586c81e93fcd4692bfa8</th>
  <td align="right">1228.81 ms</td>
  <td align="right">1244.96 ms</td>
  <td align="right"><strong>16.15 ms</strong></td>
</tr><tr>
  <th align="left">443fb026d4a07355898752a6863370a7398a76be</th>
  <td align="right">1231.06 ms</td>
  <td align="right">1252.60 ms</td>
  <td align="right"><strong>21.54 ms</strong></td>
</tr><tr>
  <th align="left">888a145b144b8077e03151a886520f332e47e297</th>
  <td align="right">1228.63 ms</td>
  <td align="right">1248.94 ms</td>
  <td align="right"><strong>20.30 ms</strong></td>
</tr><tr>
  <th align="left">70c49ca151ccd1a5763b1e1d316448cd5ebad948</th>
  <td align="right">1206.44 ms</td>
  <td align="right">1233.07 ms</td>
  <td align="right"><strong>26.62 ms</strong></td>
</tr></table>
<h3>App size</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">f25febbb81e40893076ff878f59f146ddb948ab4</th>
  <td align="right">21.58 KiB</td>
  <td align="right">414.92 KiB</td>
  <td align="right"><strong>393.34 KiB</strong></td>
</tr><tr>
  <th align="left">69d87595b411be6c93e72de62bc47457202fb72d</th>
  <td align="right">20.76 KiB</td>
  <td align="right">393.05 KiB</td>
  <td align="right"><strong>372.29 KiB</strong></td>
</tr><tr>
  <th align="left">ce4cfaff577257316d77093b2100c917a4ba2781</th>
  <td align="right">20.76 KiB</td>
  <td align="right">423.19 KiB</td>
  <td align="right"><strong>402.43 KiB</strong></td>
</tr><tr>
  <th align="left">74192858dff8cf729ce015b06fe9d3ff64fe7b74</th>
  <td align="right">20.76 KiB</td>
  <td align="right">432.99 KiB</td>
  <td align="right"><strong>412.22 KiB</strong></td>
</tr><tr>
  <th align="left">b15521ec4775f623b296e1fca558ffac46447bb8</th>
  <td align="right">21.58 KiB</td>
  <td align="right">573.18 KiB</td>
  <td align="right"><strong>551.60 KiB</strong></td>
</tr><tr>
  <th align="left">bd2afa60621240d5596fdca06191173dcb6bf982</th>
  <td align="right">20.76 KiB</td>
  <td align="right">420.55 KiB</td>
  <td align="right"><strong>399.79 KiB</strong></td>
</tr><tr>
  <th align="left">53b29ac3c0c486a5bf20586c81e93fcd4692bfa8</th>
  <td align="right">22.32 KiB</td>
  <td align="right">819.55 KiB</td>
  <td align="right"><strong>797.24 KiB</strong></td>
</tr><tr>
  <th align="left">443fb026d4a07355898752a6863370a7398a76be</th>
  <td align="right">22.30 KiB</td>
  <td align="right">832.42 KiB</td>
  <td align="right"><strong>810.11 KiB</strong></td>
</tr><tr>
  <th align="left">888a145b144b8077e03151a886520f332e47e297</th>
  <td align="right">21.58 KiB</td>
  <td align="right">713.54 KiB</td>
  <td align="right"><strong>691.96 KiB</strong></td>
</tr><tr>
  <th align="left">70c49ca151ccd1a5763b1e1d316448cd5ebad948</th>
  <td align="right">22.31 KiB</td>
  <td align="right">778.76 KiB</td>
  <td align="right"><strong>756.45 KiB</strong></td>
</tr></table>
</details>

author_association: CONTRIBUTOR