Skip to content

Commit 9eb6640

Browse files
committed
Improve performance of similar enough images
I was tracking down a performance issue with a large diff on happo.io and found that the shortcut we take when images are "similar enough" wasn't fast enough. The hashing process takes a little while on large images and we don't really need the hashing for the similarEnough function to run. I rewrote it so that it would operate on unhashed image arrays instead, plus made it early-bail so that we won't have to iterate through the whole image when we already know it's not similar enough.
1 parent 02ead83 commit 9eb6640

File tree

3 files changed

+123
-18
lines changed

3 files changed

+123
-18
lines changed

src/__tests__/similarEnough-test.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
const similarEnough = require('../similarEnough');
2+
3+
let subject;
4+
let image1Data;
5+
let image2Data;
6+
7+
const BLACK = [0, 0, 0, 255];
8+
const WHITE = [255, 255, 255, 255];
9+
10+
beforeEach(() => {
11+
image1Data = [];
12+
image2Data = [];
13+
subject = () => similarEnough({ image1Data, image2Data });
14+
});
15+
16+
describe('when images are empty', () => {
17+
it('returns true', () => {
18+
expect(subject()).toBe(true);
19+
});
20+
});
21+
22+
describe('when image1 is empty but image2 is not', () => {
23+
beforeEach(() => {
24+
image2Data = [[...BLACK, ...WHITE]];
25+
});
26+
27+
it('returns false', () => {
28+
expect(subject()).toBe(false);
29+
});
30+
});
31+
32+
describe('when 50% of rows are different', () => {
33+
beforeEach(() => {
34+
image1Data = [
35+
[...BLACK, ...WHITE],
36+
[...BLACK, ...WHITE],
37+
[...WHITE, ...BLACK],
38+
[...WHITE, ...BLACK],
39+
];
40+
image2Data = [
41+
[...BLACK, ...WHITE],
42+
[...BLACK, ...WHITE],
43+
[...BLACK, ...WHITE],
44+
[...BLACK, ...WHITE],
45+
];
46+
});
47+
48+
it('returns false', () => {
49+
expect(subject()).toBe(false);
50+
});
51+
});
52+
53+
describe('when only one row is different', () => {
54+
beforeEach(() => {
55+
image1Data = [
56+
[...BLACK, ...WHITE],
57+
[...BLACK, ...WHITE],
58+
[...WHITE, ...BLACK],
59+
[...BLACK, ...WHITE],
60+
];
61+
image2Data = [
62+
[...BLACK, ...WHITE],
63+
[...BLACK, ...WHITE],
64+
[...BLACK, ...WHITE],
65+
[...BLACK, ...WHITE],
66+
];
67+
});
68+
69+
it('returns true', () => {
70+
expect(subject()).toBe(true);
71+
});
72+
});
73+
74+
describe('when images are of different height', () => {
75+
beforeEach(() => {
76+
image1Data = [
77+
[...BLACK, ...WHITE],
78+
[...BLACK, ...WHITE],
79+
[...BLACK, ...WHITE],
80+
[...BLACK, ...WHITE],
81+
[...BLACK, ...WHITE],
82+
];
83+
image2Data = [
84+
[...BLACK, ...WHITE],
85+
[...BLACK, ...WHITE],
86+
[...BLACK, ...WHITE],
87+
[...BLACK, ...WHITE],
88+
];
89+
});
90+
91+
it('returns false', () => {
92+
expect(subject()).toBe(false);
93+
});
94+
});

src/computeAndInjectDiffs.js

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const alignArrays = require('./alignArrays');
2+
const similarEnough = require('./similarEnough');
23

34
function imageTo2DArray({ data, width, height }, paddingRight) {
45
// The imageData is a 1D array. Each element in the array corresponds to a
@@ -16,20 +17,6 @@ function imageTo2DArray({ data, width, height }, paddingRight) {
1617
return newData;
1718
}
1819

19-
function similarEnough({ hashedImage1Data, hashedImage2Data }) {
20-
const { length } = hashedImage1Data;
21-
if (length !== hashedImage2Data.length) {
22-
return false;
23-
}
24-
let equalRows = 0;
25-
for (let i = 0; i < length; i++) {
26-
if (hashedImage1Data[i] === hashedImage2Data[i]) {
27-
equalRows++;
28-
}
29-
}
30-
return equalRows / length > 0.7;
31-
}
32-
3320
function hashFn() {
3421
// Safari has a bug where trying to reference `btoa` inside a web worker will
3522
// result in an error, so we fall back to the slower (?) `JSON.stringify`. The
@@ -59,13 +46,13 @@ function align({
5946
maxWidth,
6047
hashFunction,
6148
}) {
62-
const hashedImage1Data = image1Data.map(hashFunction);
63-
const hashedImage2Data = image2Data.map(hashFunction);
64-
65-
if (similarEnough({ hashedImage1Data, hashedImage2Data })) {
49+
if (similarEnough({ image1Data, image2Data })) {
6650
return;
6751
}
6852

53+
const hashedImage1Data = image1Data.map(hashFunction);
54+
const hashedImage2Data = image2Data.map(hashFunction);
55+
6956
alignArrays(
7057
hashedImage1Data,
7158
hashedImage2Data,

src/similarEnough.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const ALLOWED_INEQUALITY = 0.3;
2+
3+
module.exports = function similarEnough({ image1Data, image2Data }) {
4+
const { length } = image1Data;
5+
if (length !== image2Data.length) {
6+
return false;
7+
}
8+
const allowedInequalRows = length * ALLOWED_INEQUALITY;
9+
let inequalRows = 0;
10+
for (let i = 0; i < length; i++) {
11+
const row = image1Data[i];
12+
for (let j = 0; j < row.length; j++) {
13+
if (image1Data[i][j] !== image2Data[i][j]) {
14+
inequalRows++;
15+
if (inequalRows > allowedInequalRows) {
16+
return false;
17+
}
18+
break;
19+
}
20+
}
21+
}
22+
return true;
23+
}
24+

0 commit comments

Comments
 (0)