Skip to content
Draft
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
4 changes: 4 additions & 0 deletions PostHog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@
DA4AF6292D119FCD0053EA38 /* PostHog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; };
DA4AF62A2D119FCD0053EA38 /* PostHog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
DA4FFB152DA93C78006BAEEA /* PostHogSessionReplayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4FFB142DA93C78006BAEEA /* PostHogSessionReplayTest.swift */; };
ARPERF0004000000000004 /* PostHogGraphicsImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ARPERF0003000000000003 /* PostHogGraphicsImageRenderer.swift */; };
DA4FFBB52DAD5B01006BAEEA /* PostHogIdentityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4FFBB42DAD5AF9006BAEEA /* PostHogIdentityTests.swift */; };
DA52CACB2F24027500305F3D /* PostHogMulticastCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA52CACA2F24027500305F3D /* PostHogMulticastCallback.swift */; };
DA52CAD32F2402E000305F3D /* PostHogMulticastCallbackTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA52CAD22F2402E000305F3D /* PostHogMulticastCallbackTest.swift */; };
Expand Down Expand Up @@ -746,6 +747,7 @@
DA3BB4DE2ED992780097A97A /* PostHogDebugImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogDebugImageProvider.swift; sourceTree = "<group>"; };
DA3BB5042EDF15320097A97A /* PostHogStackFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogStackFrame.swift; sourceTree = "<group>"; };
DA4FFB142DA93C78006BAEEA /* PostHogSessionReplayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionReplayTest.swift; sourceTree = "<group>"; };
ARPERF0003000000000003 /* PostHogGraphicsImageRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogGraphicsImageRenderer.swift; sourceTree = "<group>"; };
DA4FFBB42DAD5AF9006BAEEA /* PostHogIdentityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogIdentityTests.swift; sourceTree = "<group>"; };
DA52CACA2F24027500305F3D /* PostHogMulticastCallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogMulticastCallback.swift; sourceTree = "<group>"; };
DA52CAD22F2402E000305F3D /* PostHogMulticastCallbackTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogMulticastCallbackTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1334,6 +1336,7 @@
69F518112BAC783300F52C14 /* CGColor+Util.swift */,
69F518132BAC7F4300F52C14 /* Date+Util.swift */,
69F518152BAC7F9200F52C14 /* UIView+Util.swift */,
ARPERF0003000000000003 /* PostHogGraphicsImageRenderer.swift */,
69F518172BAC80A300F52C14 /* String+Util.swift */,
DA30AE682D3EFB4F00465A64 /* Optional+Util.swift */,
69F518192BAC81FC00F52C14 /* UITextInputTraits+Util.swift */,
Expand Down Expand Up @@ -2250,6 +2253,7 @@
6926DA8E2ADD2876005760D2 /* PostHogContext.swift in Sources */,
690FF0AF2AEB9C1400A0B06B /* DateUtils.swift in Sources */,
69F518162BAC7F9200F52C14 /* UIView+Util.swift in Sources */,
ARPERF0004000000000004 /* PostHogGraphicsImageRenderer.swift in Sources */,
69261D192AD9673500232EC7 /* PostHogBatchUploadInfo.swift in Sources */,
DAC699EC2CCA73E5000D1D6B /* ForwardingPickerViewDelegate.swift in Sources */,
DA26419C2CC0499300CB427B /* PostHogAutocaptureEventTracker.swift in Sources */,
Expand Down
17 changes: 13 additions & 4 deletions PostHog/Replay/CGColor+Util.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import Foundation
import UIKit

// Pre-computed hex character lookup for fast color string formatting
private let hexChars: [Character] = Array("0123456789ABCDEF")

extension CGColor {
func toRGBString() -> String? {
// see dicussion: https://github.com/PostHog/posthog-ios/issues/226
Expand All @@ -23,11 +26,17 @@
return nil
}

let red = Int(components[0] * 255)
let green = Int(components[1] * 255)
let blue = Int(components[2] * 255)
let r = min(255, max(0, Int(components[0] * 255)))
let g = min(255, max(0, Int(components[1] * 255)))
let b = min(255, max(0, Int(components[2] * 255)))

return String(format: "#%02X%02X%02X", red, green, blue)
// Build hex string directly β€” avoids String(format:) overhead
return String([
"#",
hexChars[r >> 4], hexChars[r & 0xF],
hexChars[g >> 4], hexChars[g & 0xF],
hexChars[b >> 4], hexChars[b & 0xF],
])
}
}
#endif
74 changes: 74 additions & 0 deletions PostHog/Replay/PostHogGraphicsImageRenderer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#if os(iOS)

import UIKit

/// Cached device RGB color space β€” avoids creating a new one per render call.
private let deviceRGBColorSpace = CGColorSpaceCreateDeviceRGB()

/// High-performance image renderer that bypasses `UIGraphicsImageRenderer`'s overhead.
///
/// Based on Sentry's approach: directly allocates a `CGContext` with `malloc`, avoiding
/// UIKit's internal caching and context management abstractions.
///
/// See: https://blog.sentry.io/boosting-session-replay-performance-on-ios-with-view-renderer-v2/

final class PostHogGraphicsImageRenderer {
let size: CGSize
let scale: CGFloat

init(size: CGSize, scale: CGFloat) {
self.size = size
self.scale = scale
}

func image(actions: (CGContext) -> Void) -> UIImage? {
let pixelsPerRow = Int(size.width * scale)
let pixelsPerColumn = Int(size.height * scale)
let bytesPerPixel = 4 // RGBA
let bytesPerRow = bytesPerPixel * pixelsPerRow
let bitsPerComponent = 8

guard pixelsPerRow > 0, pixelsPerColumn > 0 else {
return nil
}

// Allocate memory for raw image data.
// Using malloc instead of calloc β€” drawHierarchy overwrites the entire buffer,
// so zero-initialization is unnecessary and wastes time for large buffers.
let bufferSize = pixelsPerColumn * bytesPerRow
guard let rawData = malloc(bufferSize) else {
return nil
}
defer {
free(rawData)
}

guard let context = CGContext(
data: rawData,
width: pixelsPerRow,
height: pixelsPerColumn,
bitsPerComponent: bitsPerComponent,
bytesPerRow: bytesPerRow,
space: deviceRGBColorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
return nil
}

// UIKit coordinate system is flipped vs CoreGraphics β€” shift and scale to match
context.translateBy(x: 0, y: size.height * scale)
context.scaleBy(x: scale, y: -1 * scale)

// Push context so drawHierarchy draws into our context
UIGraphicsPushContext(context)
actions(context)
UIGraphicsPopContext()

guard let cgImage = context.makeImage() else {
return nil
}
return UIImage(cgImage: cgImage, scale: scale, orientation: .up)
}
}

#endif
Loading
Loading