Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d30d4cf
ExportDiffManager
abelonogov-ld Feb 22, 2026
297ede9
visible working compression before buffering
abelonogov-ld Feb 23, 2026
1f4d2c2
test script for iOS
abelonogov-ld Feb 23, 2026
ba94817
all tests fixed and 0 warnings
abelonogov-ld Feb 23, 2026
4fcdf40
Enhance RRWebEventGenerator with keyframe handling and new test for c…
abelonogov-ld Feb 23, 2026
7db0115
Enhance test script with improved options and usage documentation
abelonogov-ld Feb 23, 2026
61a0aa0
rename file
abelonogov-ld Feb 23, 2026
af82a03
fix test script
abelonogov-ld Feb 23, 2026
4e5497f
remove dead code
abelonogov-ld Feb 23, 2026
038616c
benchmark files
abelonogov-ld Feb 23, 2026
97a37e4
Enhance Benchmark functionality by adding execution time tracking and…
abelonogov-ld Feb 23, 2026
e62e2f2
adjust compression parameters
abelonogov-ld Feb 23, 2026
c0a4fe9
wip
abelonogov-ld Feb 23, 2026
13798ea
wip
abelonogov-ld Feb 23, 2026
9668c70
Merge branch 'main' into andrey/exportdiffmanager
abelonogov-ld Feb 24, 2026
028123e
Refactor ExportFrame to use ImageSignature instead of TileSignature
abelonogov-ld Feb 24, 2026
a0e951a
expand unit tests
abelonogov-ld Feb 28, 2026
4be061f
no message
abelonogov-ld Mar 2, 2026
8a75f7a
good optimization
abelonogov-ld Mar 2, 2026
45ad333
using c
abelonogov-ld Mar 2, 2026
ff502be
swift package
abelonogov-ld Mar 2, 2026
8ea1c7b
do account disk reading into benchmark
abelonogov-ld Mar 2, 2026
eecc842
do account disk reading into benchmark
abelonogov-ld Mar 2, 2026
642bea6
Alert errors
abelonogov-ld Mar 2, 2026
ad4ddc3
separated time
abelonogov-ld Mar 2, 2026
6437e3e
Merge branch 'andrey/exportdiffmanager' into andrey/exportdiff-2longh…
abelonogov-ld Mar 2, 2026
2d196dd
signature benchmark
abelonogov-ld Mar 2, 2026
194e565
Merge branch 'main' into andrey/exportdiff-2longhash-swiftpackage-r
abelonogov-ld Mar 5, 2026
21a1d48
SessionReplayHotPath
abelonogov-ld Mar 5, 2026
40749da
reverse debug change
abelonogov-ld Mar 5, 2026
b3c0f2a
remove old stuff
abelonogov-ld Mar 5, 2026
bd187ee
fix unit test
abelonogov-ld Mar 5, 2026
7e0937d
podspecs
abelonogov-ld Mar 5, 2026
9b97263
ImageSignature hash
abelonogov-ld Mar 5, 2026
cffaec7
image cache
abelonogov-ld Mar 5, 2026
81825f8
Add error message display in BenchmarkView
abelonogov-ld Mar 5, 2026
e649170
c tile hash
abelonogov-ld Mar 6, 2026
deae480
c one
abelonogov-ld Mar 6, 2026
1faea13
SessionReplayC
abelonogov-ld Mar 6, 2026
a830aa6
Debug
abelonogov-ld Mar 6, 2026
192ad8a
no message
abelonogov-ld Mar 6, 2026
3b0b563
buffer
abelonogov-ld Mar 6, 2026
896af37
Using NEON in RELEASE
abelonogov-ld Mar 6, 2026
8fbb0e5
subtitle
abelonogov-ld Mar 6, 2026
66cde09
in optimize only
abelonogov-ld Mar 6, 2026
bc80b1d
USE_NEON
abelonogov-ld Mar 6, 2026
82ed5d0
fix capture time
abelonogov-ld Mar 6, 2026
d3604f4
split c file in to
abelonogov-ld Mar 6, 2026
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
6 changes: 6 additions & 0 deletions LaunchDarklySessionReplay.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,15 @@ Pod::Spec.new do |s|
ss.source_files = "Sources/Common/**/*.{swift,h,m}"
end

s.subspec "SessionReplayC" do |ss|
ss.source_files = "Sources/SessionReplayC/**/*.{c,h}"
ss.public_header_files = "Sources/SessionReplayC/include/**/*.h"
end

s.subspec "LaunchDarklySessionReplay" do |ss|
ss.source_files = "Sources/LaunchDarklySessionReplay/**/*.{swift,h,m}"
ss.dependency "LaunchDarklySessionReplay/Common"
ss.dependency "LaunchDarklySessionReplay/SessionReplayC"
ss.dependency "LaunchDarklyObservability/LaunchDarklyObservability"
end
end
8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ let package = Package(
name: "ObjCBridge",
publicHeadersPath: "."
),
.target(
name: "SessionReplayC",
publicHeadersPath: "include"
),
.target(name: "Common",
dependencies: [.product(name: "LaunchDarkly", package: "ios-client-sdk", condition: .when(platforms: [.iOS, .tvOS]))]),
.target(
Expand All @@ -53,6 +57,7 @@ let package = Package(
dependencies: [
"Common",
"LaunchDarklyObservability",
"SessionReplayC",
],
),
.target(
Expand Down Expand Up @@ -97,7 +102,8 @@ let package = Package(
name: "SessionReplayTests",
dependencies: [
"LaunchDarklySessionReplay",
"LaunchDarklyObservability"
"LaunchDarklyObservability",
"SessionReplayC",
]
),
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,33 @@ public final class BenchmarkExecutor {

public init() {}

public typealias SignatureResult = (
elapsedTime: TimeInterval,
totalBytes: Int,
frameCount: Int
)

public func signatureBenchmark(framesDirectory: URL) throws -> SignatureResult {
let reader = try RawFrameReader(directory: framesDirectory)
let frames = Array(reader)
let manager = TileSignatureManager()
var totalBytes = 0

for frame in frames {
if let cgImage = frame.image.cgImage {
totalBytes += cgImage.bytesPerRow * cgImage.height
}
}

let start = CFAbsoluteTimeGetCurrent()
for frame in frames {
let _ = manager.compute(image: frame.image)
}
let elapsed = CFAbsoluteTimeGetCurrent() - start

return (elapsedTime: elapsed, totalBytes: totalBytes, frameCount: frames.count)
}

public func compression(framesDirectory: URL, runs: Int = 1) async throws -> [CompressionResult] {
let reader = try RawFrameReader(directory: framesDirectory)
let frames = Array(reader)
Expand Down Expand Up @@ -56,9 +83,9 @@ public final class BenchmarkExecutor {

for frame in frames {
let captureStart = CFAbsoluteTimeGetCurrent()
guard let exportFrame = exportDiffManager.exportFrame(from: frame, onTiledFrameComputed: {
captureTime += CFAbsoluteTimeGetCurrent() - captureStart
}) else {
let exportFrame = exportDiffManager.exportFrame(from: frame)
captureTime += CFAbsoluteTimeGetCurrent() - captureStart
guard let exportFrame else {
continue
}

Expand Down
26 changes: 23 additions & 3 deletions Sources/LaunchDarklySessionReplay/BenchMark/RawFrameIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@ final class RawFrameReader: Sequence {
image = cached
} else {
let imageURL = directory.appendingPathComponent(String(format: "%06d.png", imageIndex))
guard let loaded = UIImage(contentsOfFile: imageURL.path) else { return nil }
imageCache[imageIndex] = loaded
image = loaded
guard let loaded = UIImage(contentsOfFile: imageURL.path),
let decoded = Self.forceDecoded(loaded) else { return nil }
imageCache[imageIndex] = decoded
image = decoded
}

let areasJSON = columns[4].replacingOccurrences(of: "\"\"", with: "\"")
Expand All @@ -143,6 +144,25 @@ final class RawFrameReader: Sequence {
return RawFrame(image: image, timestamp: timestamp, orientation: orientation, areas: areas)
}

private static func forceDecoded(_ source: UIImage) -> UIImage? {
guard let cgImage = source.cgImage else { return nil }
let width = cgImage.width
let height = cgImage.height
let colorSpace = cgImage.colorSpace ?? CGColorSpaceCreateDeviceRGB()
guard let ctx = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
) else { return nil }
ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let decoded = ctx.makeImage() else { return nil }
return UIImage(cgImage: decoded, scale: source.scale, orientation: source.imageOrientation)
}

private static func parseCSV(line: String) -> [String] {
var result = [String]()
var current = ""
Expand Down
105 changes: 105 additions & 0 deletions Sources/LaunchDarklySessionReplay/ScreenCapture/ImageSignature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import CoreGraphics

struct ImageSignature: Hashable {
let rows: Int
let columns: Int
let tileWidth: Int
let tileHeight: Int
let tileSignatures: [TileSignature]
private let _hashValue: Int

init(rows: Int, columns: Int, tileWidth: Int, tileHeight: Int, tileSignatures: [TileSignature]) {
self.init(
rows: rows, columns: columns,
tileWidth: tileWidth, tileHeight: tileHeight,
tileSignatures: tileSignatures,
tileAccHash: Self._accumulateHash(tileSignatures)
)
}

init(rows: Int, columns: Int, tileWidth: Int, tileHeight: Int, tileSignatures: [TileSignature], tileAccHash: Int) {
self.rows = rows
self.columns = columns
self.tileWidth = tileWidth
self.tileHeight = tileHeight
self.tileSignatures = tileSignatures

var hasher = Hasher()
hasher.combine(rows)
hasher.combine(columns)
hasher.combine(tileWidth)
hasher.combine(tileHeight)
hasher.combine(tileAccHash)
self._hashValue = hasher.finalize()
}

@inline(__always)
static func _accumulateTile(_ acc: Int, _ sig: TileSignature) -> Int {
(acc &* 31) &+ Int(truncatingIfNeeded: sig.hashLo ^ sig.hashHi)
}

private static func _accumulateHash(_ tiles: [TileSignature]) -> Int {
var acc = 0
for sig in tiles { acc = _accumulateTile(acc, sig) }
return acc
}

func hash(into hasher: inout Hasher) {
hasher.combine(_hashValue)
}

static func == (lhs: ImageSignature, rhs: ImageSignature) -> Bool {
lhs._hashValue == rhs._hashValue &&
lhs.rows == rhs.rows &&
lhs.columns == rhs.columns &&
lhs.tileWidth == rhs.tileWidth &&
lhs.tileHeight == rhs.tileHeight &&
lhs.tileSignatures == rhs.tileSignatures
}
}

struct TileSignature: Hashable {
let hashLo: Int64
let hashHi: Int64
}

extension ImageSignature {
func diffRectangle(other: ImageSignature?) -> CGRect? {
guard let other else {
return CGRect(x: 0,
y: 0,
width: columns * tileWidth,
height: rows * tileHeight)
}

guard rows == other.rows, columns == other.columns, tileWidth == other.tileWidth, tileHeight == other.tileHeight else {
return CGRect(x: 0,
y: 0,
width: columns * tileWidth,
height: rows * tileHeight)
}

var minRow = Int.max
var maxRow = Int.min
var minColumn = Int.max
var maxColumn = Int.min

for (i, tile) in tileSignatures.enumerated() where tile != other.tileSignatures[i] {
let row = i / columns
let col = i % columns
minRow = min(minRow, row)
maxRow = max(maxRow, row)
minColumn = min(minColumn, col)
maxColumn = max(maxColumn, col)
}

guard minRow != Int.max else {
return nil
}

return CGRect(x: minColumn * tileWidth,
y: minRow * tileHeight,
width: (maxColumn - minColumn + 1) * tileWidth,
height: (maxRow - minRow + 1) * tileHeight)
}
}
Loading