Skip to content

Commit 36cd199

Browse files
feat: CPU utilization optimization in image diff calculations (#414)
- 4x improvement in image comparison - Use low-level coding techniques to calculate tile signatures faster <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the hashing/equality behavior and low-level pixel hashing loop used in image comparisons, so regressions could affect diff accuracy or performance across edge cases (odd tile widths, mutable signature lists). > > **Overview** > Speeds up bitmap tile signature generation used for image diffing by reducing per-pixel work and avoiding repeated hashing. > > `ImageSignature` now caches its `hashCode()` (with a quick reject in `equals`) and `TileSignatureManager.computeInternal` precomputes an accumulated tile hash while generating signatures. Tile hashing is rewritten to pack two ARGB pixels into a single `Long` and hash per-pair (plus an optional trailing pixel) instead of hashing each pixel byte-by-byte. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 75ade55. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent d1eb13d commit 36cd199

File tree

1 file changed

+81
-35
lines changed
  • sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture

1 file changed

+81
-35
lines changed

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/TileSignatureManager.kt

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,58 @@ data class ImageSignature(
1515
val tileWidth: Int,
1616
val tileHeight: Int,
1717
val tileSignatures: List<TileSignature>,
18-
)
18+
) {
19+
private var _hashCode: Int = 0
20+
21+
override fun hashCode(): Int {
22+
var h = _hashCode
23+
if (h == 0) {
24+
h = finalizeHash(rows, columns, tileWidth, tileHeight, accumulateHash(tileSignatures))
25+
_hashCode = h
26+
}
27+
return h
28+
}
29+
30+
override fun equals(other: Any?): Boolean {
31+
if (this === other) return true
32+
if (other !is ImageSignature) return false
33+
val h = _hashCode
34+
val oh = other._hashCode
35+
if (h != 0 && oh != 0 && h != oh) return false
36+
return rows == other.rows &&
37+
columns == other.columns &&
38+
tileWidth == other.tileWidth &&
39+
tileHeight == other.tileHeight &&
40+
tileSignatures == other.tileSignatures
41+
}
42+
43+
companion object {
44+
internal fun accumulateTile(acc: Int, sig: TileSignature): Int =
45+
31 * acc + (sig.hashLo xor sig.hashHi).toInt()
46+
47+
private fun accumulateHash(tiles: List<TileSignature>): Int {
48+
var acc = 0
49+
for (sig in tiles) acc = accumulateTile(acc, sig)
50+
return acc
51+
}
52+
53+
private fun finalizeHash(rows: Int, columns: Int, tileWidth: Int, tileHeight: Int, tileAcc: Int): Int {
54+
var h = rows
55+
h = 31 * h + columns
56+
h = 31 * h + tileWidth
57+
h = 31 * h + tileHeight
58+
h = 31 * h + tileAcc
59+
return if (h == 0) 1 else h
60+
}
61+
62+
internal fun createWithAccHash(
63+
rows: Int, columns: Int, tileWidth: Int, tileHeight: Int,
64+
tileSignatures: List<TileSignature>, tileAccHash: Int,
65+
): ImageSignature = ImageSignature(rows, columns, tileWidth, tileHeight, tileSignatures).also {
66+
it._hashCode = finalizeHash(rows, columns, tileWidth, tileHeight, tileAccHash)
67+
}
68+
}
69+
}
1970

2071
/**
2172
* Computes tile-based signatures for bitmaps.
@@ -83,68 +134,63 @@ class TileSignatureManager {
83134

84135
val tilesX = (width + tileWidth - 1) / tileWidth
85136
val tilesY = (height + tileHeight - 1) / tileHeight
86-
val tileCount = tilesX * tilesY
87-
val tileSignatures = ArrayList<TileSignature>(tileCount)
137+
val tileSignatures = ArrayList<TileSignature>(tilesX * tilesY)
88138

139+
var tileAccHash = 0
89140
for (ty in 0 until tilesY) {
90141
val startY = ty * tileHeight
91142
val endY = minOf(startY + tileHeight, height)
92-
93143
for (tx in 0 until tilesX) {
94144
val startX = tx * tileWidth
95145
val endX = minOf(startX + tileWidth, width)
96-
tileSignatures.add(
97-
hashTile(
98-
pixels = pixels,
99-
width = width,
100-
startX = startX,
101-
startY = startY,
102-
endX = endX,
103-
endY = endY
104-
)
105-
)
146+
val sig = tileHash(pixels, width, startX, startY, endX, endY)
147+
tileSignatures.add(sig)
148+
tileAccHash = ImageSignature.accumulateTile(tileAccHash, sig)
106149
}
107150
}
108151

109-
return ImageSignature(
152+
return ImageSignature.createWithAccHash(
110153
rows = tilesY,
111154
columns = tilesX,
112155
tileWidth = tileWidth,
113156
tileHeight = tileHeight,
114157
tileSignatures = tileSignatures,
158+
tileAccHash = tileAccHash,
115159
)
116160
}
117161

118-
private fun hashTile(
162+
private fun tileHash(
119163
pixels: IntArray,
120164
width: Int,
121165
startX: Int,
122166
startY: Int,
123167
endX: Int,
124168
endY: Int
125169
): TileSignature {
126-
// Two independent 64-bit lanes to reduce collision probability vs single-lane hashing.
127170
var hashLo = 5163949831757626579L
128171
var hashHi = 4657936482115123397L
129-
val primeLo = 1238197591667094937L // from https://bigprimes.org
130-
val primeHi = 1700294137212722571L // from https://bigprimes.org
172+
val primeLo = 1238197591667094937L
173+
val primeHi = 1700294137212722571L
174+
175+
val pixelCount = endX - startX
176+
val pairCount = pixelCount ushr 1
177+
val hasTrailingPixel = pixelCount and 1 != 0
178+
131179
for (y in startY until endY) {
132-
val rowOffset = y * width
133-
for (x in startX until endX) {
134-
val argb = pixels[rowOffset + x]
135-
val b0 = (argb and 0xFF).toLong()
136-
val b1 = ((argb ushr 8) and 0xFF).toLong()
137-
val b2 = ((argb ushr 16) and 0xFF).toLong()
138-
val b3 = ((argb ushr 24) and 0xFF).toLong()
139-
hashLo = (hashLo xor b0) * primeLo
140-
hashLo = (hashLo xor b1) * primeLo
141-
hashLo = (hashLo xor b2) * primeLo
142-
hashLo = (hashLo xor b3) * primeLo
143-
144-
hashHi = (hashHi xor b3) * primeHi
145-
hashHi = (hashHi xor b2) * primeHi
146-
hashHi = (hashHi xor b1) * primeHi
147-
hashHi = (hashHi xor b0) * primeHi
180+
var i = y * width + startX
181+
182+
for (p in 0 until pairCount) {
183+
val v = (pixels[i].toLong() and 0xFFFFFFFFL) or
184+
((pixels[i + 1].toLong() and 0xFFFFFFFFL) shl 32)
185+
hashLo = (hashLo xor v) * primeLo
186+
hashHi = (hashHi xor v) * primeHi
187+
i += 2
188+
}
189+
190+
if (hasTrailingPixel) {
191+
val v = pixels[i].toLong() and 0xFFFFFFFFL
192+
hashLo = (hashLo xor v) * primeLo
193+
hashHi = (hashHi xor v) * primeHi
148194
}
149195
}
150196
return TileSignature(hashLo = hashLo, hashHi = hashHi)

0 commit comments

Comments
 (0)