Allow for subpixel deviations + improved unoptimized loop execution in image comparison#571
Allow for subpixel deviations + improved unoptimized loop execution in image comparison#571pimms wants to merge 9 commits into
Conversation
|
This is an great fix for a weird problem, and I just want to voice my support in the hope that this PR gets merged. We have the same issue with differences in produced colors on M1 and Intel and this will solve our issue until the whole team (and possibly CircleCI) is on M1. Lowering the precision threshold is not an option for us, since we primarily use this library to test placement and size of elements. Minor differences in colors, however, is something we can excuse since we now need to have a hybrid M1/Intel solution in our team. I've opened PRs in our component libraries that will use @pimms fork until this (hopefully) gets merged. |
|
@dmitri-zganiaiko Nice find on the threshhold calculation I think @pimms has addressed it and this is ready for review again. |
|
This is still blocked I think we need an approval from someone with write access. |
|
Just to emphasize the important of this PR, we have a team that begins to use M1 as the main development platform, but our CI will be on Intel for the foreseeable future (At least until Github provides M1-Based Github Runners). |
| public static func image( | ||
| drawHierarchyInKeyWindow: Bool = false, | ||
| precision: Float = 1, | ||
| subpixelThreshold: UInt8 = 0, |
There was a problem hiding this comment.
Would it maybe make sense the default the threshold to a value which will avoid the M1 vs Intel issue out of the box (ex - 5)?
There was a problem hiding this comment.
I haven't seen any "one" value that would make sense. Sometimes, with corner-rounding for instance, the difference is <= 2, while with images it seems to typically be <= 1. With shadows and rounded corners, it may be as high as <= 5. I think all of these values are anecdotal though, and that the user should have a fully explicit relationship with this value. After all, this is a hacky workaround, and it's probably a good idea to not enable it by default.
There was a problem hiding this comment.
YeH, makes sense, I was just hoping there was a magic value haha.
There was a problem hiding this comment.
@pimms Thanks for sharing this I’m using it now and it works great.
I’ve a few nits that don't really matter but on the off-chance this delays the eventual merge I'm happy to do myself if you're ok with it as they appear to be pretty trivial.
- Update the
.imageentires within Documentation/Available-Snapshot-Strategies.md to include the sub-pixel property. - SceneKit snapshot strategy support
- SpriteKit snapshot strategy support
I wasn't able to check that the internal snapshot tests for this project work, there are a fair few failing but I suspect this is due to me not knowing what simulator to use or the tests already failing.
|
@Deco354 Thanks, good points. I believe that a handful of tests are failing on I don't think I'll have time to look at this this week at least, so if you or someone else wants to have a look at it it'd be great — otherwise I'll get to it at some point. |
|
One more thing, what exactly does 1 byte of the |
| while offset < pixelCount * 4 { | ||
| if p1[offset].diff(between: p2[offset]) > subpixelThreshold { | ||
| differentPixelCount += 1 | ||
| if differentPixelCount > threshold { | ||
| return false | ||
| } | ||
| } | ||
| if Float(differentPixelCount) > threshold { return false } | ||
| offset += 1 |
There was a problem hiding this comment.
Is there a reason this was changed from a for-loop to a while-loop? My interpretation is that in both implementations the loop should either increment offset or return false.
Using a while loop and manually incrementing the offset makes this less maintainable since it's easy to introduce changes in the loop body that skip incrementing offset and would lead to an infinite loop.
There was a problem hiding this comment.
Nevermind, just saw your PR description with the explanation for when compiled without optimization.
| while byte < byteCount { | ||
| if oldBytes[byte].diff(between: newerBytes[byte]) > subpixelThreshold { | ||
| differentPixelCount += 1 | ||
| if differentPixelCount >= threshold { | ||
| return false | ||
| } | ||
| } | ||
| byte += 1 |
There was a problem hiding this comment.
Same comment here about replacing the for loop with a while loop.
I believe it's using the |
stephencelis
left a comment
There was a problem hiding this comment.
@pimms Thanks for taking this on! We've been backed up on some of these PRs, but are hoping to merge them slowly.
I think we'll likely merge this as is unless you want to tie up some of the loose ends mentioned above, first. Once we merge we may also make a few small changes on main as far as API naming goes.
Let us know what you think!
|
Great news, @stephencelis! 🎉 I've not had as much free time to look into the follow-ups as I thought I'd have, and I know I don't have much time coming up. If you're fine with merging this as-is, that sounds great 💯 |
|
@pimms Can you tell us, what flags are used?
|
|
@chosa91 We were using CocoaPods at the time, and added the following at the end of our Podfile. I can't fully recall what Xcode-values this would translate to. |
|
Will this be merged soon, would be good to be able to start testing this? |
|
We started using this fork in our project and it's been working great so far. Setting |
|
Is there a schedule when this will be merged? |
@stephencelis any thoughts on merging this? |
|
@stephencelis any plans to merge it? |
|
@mbrandonw Trying to ping someone else from this org to take a look and merge it 🙏🏼 Edit: Seems like |
Subpixel difference
Workaround for the issue described in #313 and #424.
The readme clearly states that the reference-device should be identical to the recorder-device, but as M1 Macs are becoming popular with developers at a different rate to CI & cloud providers, that is becoming increasingly difficult.
Allow each subpixel (R, G, B) to deviate by up to a certain degree. Currently, a
precision: 0.9means that "90% of the pixels must match 100%". The issues described in particularly #313, where some experience that the pixels deviate by only 1, can be resolved by comparing withsubpixelThreshold: 1. Comparing withprecision: 0.9, subpixelThreshold: 1then means "90% of the subpixels' value must be ±1 of the reference".Optimization
When an image comparison
memcmpfails and we need to compare each byte of the failing image, the current loop iteration is slow when compiled without optimization on Intel machines.My team defaulted to adding custom compiler flags to
SnapshotTesting, but the problem can also be diminished significantly by using a more basic loop implementation.The difference between looping over a range (current implementation) and looping with a condition can be seen on Godbolt.
Example in Godbolt
LBB1_4-LBB1_10withLBB3_1