|
doctype html |
|
html |
|
head |
|
title gist-gemini-fancy performance |
|
style. |
|
/* Perform nice resets */ |
|
body { |
|
height: 100%; |
|
margin: 0; |
|
/* https://github.com/corysimmons/typographic/blob/2.9.3/scss/typographic.scss#L34 */ |
|
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'; |
|
} |
|
body |
|
//- Provide info to the user |
|
h1 gist-gemini-fancy performance |
|
p |
|
| This page is a proof of concept to verify we won't have performance issues with a lot of pages |
|
|
|
//- TODO: When moving to repo, don't work on polishing overlay in separate repo yet |
|
//- First: Make sure we can get approval fully working (i.e. gemini does a comparison + approval data with no fuss) |
|
//- Second: Improve selection in this repo so we can work out any kinks that might change its API |
|
//- This might break a rule of mine which is open source/break out first but technically it's already open source =/ |
|
//- Third: Figure out jawbone and magnification etc (maybe do something in a mockup tool) |
|
|
|
//- Load an images directly from our report |
|
table |
|
- var imgHeight = '250'; |
|
tr |
|
td Current: |
|
td Diff: |
|
td Ref: |
|
tr |
|
td |
|
img(src="gemini-report/images/root/default-large/Chrome~current.png", height=imgHeight) |
|
td |
|
img#expected-diff-img(src="gemini-report/images/root/default-large/Chrome~diff.png", height=imgHeight) |
|
td |
|
img(src="gemini-report/images/root/default-large/Chrome~ref.png", height=imgHeight) |
|
|
|
//- Load large set of images in hidden container |
|
//- DEV: In reality, we would probably have something like Gemini's hidden selection |
|
div(style="display: none") |
|
- var i = 0; |
|
while (i < 200) |
|
//- Create set which has matching selection |
|
div(data-compare-set=i) |
|
img(data-compare-type="current", src="gemini-report/images/root/default-large/Chrome~current.png") |
|
img(data-compare-type="diff", src="gemini-report/images/root/default-large/Chrome~diff.png") |
|
img(data-compare-type="ref", src="gemini-report/images/root/default-large/Chrome~ref.png") |
|
//- Create set without matching selection |
|
div(data-compare-set=i + 1) |
|
img(data-compare-type="current", src="gemini-report/images/root/default-large/Chrome~ref.png") |
|
img(data-compare-type="diff", src="gemini-report/images/root/default-large/Chrome~ref.png") |
|
img(data-compare-type="ref", src="gemini-report/images/root/default-large/Chrome~ref.png") |
|
- i += 2 |
|
|
|
//- Define an output area for images |
|
p(style="margin-bottom: 0") Results: |
|
table#results(style="margin-left: 20px") |
|
|
|
//- TODO: Consider scrollspy for update buttons |
|
//- TODO: Consider buttons to expand row of images to full screen |
|
//- TODO: Consider magnifying glass zoom on images (e.g. like in ecommerce sites) |
|
//- TODO: Figure out how to make selection work, maybe normal GUI like Gemini but with jawbone effect for matching items |
|
|
|
script(src="https://cdn.rawgit.com/jed/domo/13c45aba3e94dd2d1bc469ce3339bbc1e3a10314/lib/domo.js") |
|
script. |
|
document.addEventListener('DOMContentLoaded', function handleReady () { |
|
// Simplifiy domo reference |
|
var D = window.domo; |
|
|
|
// Specify target area (this would be done via overlay selection) |
|
// DEV: Target area gathered from `/prototype` |
|
var targetArea = {left: 159, top: 0, width: 205, height: 63.133331298828125}; |
|
|
|
// TODO: Realizing we need to do matching on similar content (i.e. same width image + same diff in selection) |
|
// It looks like Gemini's comparison library isn't built for browser |
|
// https://github.com/gemini-testing/looks-same |
|
// For now, use direct comparison with `get-pixels` and `ndarray` |
|
// Actually, we can prob use a second canvas with negative placement for x/y and same width/neight |
|
// This is more future-proof and dodges loading `ndarray` dependencies |
|
// Although, it's likely less efficient since we have to extract image data and compare it |
|
|
|
// Start our chain of methods |
|
findSelectionMatches(); |
|
function findSelectionMatches() { |
|
// Start our performance check (70ms for 200 1024x1600 images) |
|
console.time('findSelectionMatches'); |
|
|
|
// Find our sets of images to update |
|
var compareSetEls = document.querySelectorAll('[data-compare-set]'); |
|
|
|
// Convert image sets into objects so we can add metadata |
|
var compareSets = Array.prototype.map.call(compareSetEls, function createCompareSet (compareSetEl, i) { |
|
return { |
|
currentImg: compareSetEl.querySelector('[data-compare-type=current]'), |
|
diffImg: compareSetEl.querySelector('[data-compare-type=diff]'), |
|
refImg: compareSetEl.querySelector('[data-compare-type=ref]'), |
|
name: 'path/to/image/' + i + '/Chrome' |
|
}; |
|
}); |
|
|
|
// Prepare canvas for images to match against |
|
function getSelectionImageData(img) { |
|
// Generate our canvas sized down to the selection |
|
// https://github.com/scijs/get-pixels/blob/7c447cd979637b31e47e148f238a1e71611af481/dom-pixels.js#L14-L18 |
|
var canvasEl = document.createElement('canvas'); |
|
canvasEl.width = targetArea.width; |
|
canvasEl.height = targetArea.height; |
|
var context = canvasEl.getContext('2d'); |
|
|
|
// Draw a clip for safety (performanc), then our image |
|
// DEV: We haven't tested if this improves performance but assume it should |
|
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage |
|
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rect |
|
context.rect(0, 0, targetArea.width, targetArea.height); |
|
context.clip(); |
|
context.drawImage(img, -1 * targetArea.left, -1 * targetArea.top); |
|
|
|
// Return our generated canvas |
|
// https://github.com/scijs/get-pixels/blob/7c447cd979637b31e47e148f238a1e71611af481/dom-pixels.js#L19-L20 |
|
return context.getImageData(0, 0, targetArea.width, targetArea.height).data; |
|
} |
|
var expectedDiffImg = document.querySelector('#expected-diff-img').cloneNode(); |
|
// Reset HTML/CSS overrides |
|
delete expectedDiffImg.height; delete expectedDiffImg.width; |
|
delete expectedDiffImg.style; delete expectedDiffImg.className; |
|
var expectedImageData = getSelectionImageData(expectedDiffImg); |
|
|
|
// Prepare deep equals helper |
|
// DEV: This is bad for security as we short circuit (i.e. not time constant comparison) |
|
function deepEquals(aArr, bArr) { |
|
if (aArr.length !== bArr.length) { |
|
return false; |
|
} |
|
var i = 0; |
|
for (; i < aArr.length; i += 1) { |
|
if (aArr[i] !== bArr[i]) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
// Filter image sets based on matching widths and selection |
|
var matchingCompareSets = compareSets.filter(function matchCompareSet (compareSet) { |
|
// If the images are different widths, return false |
|
// TODO: Allow this to be a configurable heuristic |
|
var actualDiffImg = compareSet.diffImg; |
|
if (expectedDiffImg.width !== actualDiffImg.width) { |
|
return false; |
|
} |
|
|
|
// If the selection is different, return false |
|
// DEV: We current do an exact match but could move to other comparison script |
|
// Unfortunately, Gemini's comparison seems to be Node.js only |
|
// and an exact match is "good enough" for now |
|
var actualImageData = getSelectionImageData(actualDiffImg); |
|
if (!deepEquals(actualImageData, expectedImageData)) { |
|
return false; |
|
} |
|
|
|
// Otherwise, approve match |
|
return true; |
|
}); |
|
|
|
// End our performance check |
|
console.timeEnd('findSelectionMatches'); |
|
|
|
// Pass through matching sets to `bulkUpdateSelection` |
|
bulkUpdateSelection(matchingCompareSets); |
|
} |
|
|
|
function bulkUpdateSelection(compareSets) { |
|
// Start our performance check (620ms total for 100 1024x1600 images, 400ms seems to be first `drawImage`) |
|
console.time('bulkUpdateSelection'); |
|
|
|
// Find our output targets |
|
var resultsEl = document.querySelector('#results'); |
|
var resultsDocFrag = document.createDocumentFragment(); |
|
|
|
// Generate and updated ref image for each of our comparisons |
|
compareSets.forEach(function generateUpdatedRef (compareSet) { |
|
// Localize our references |
|
var currentImg = compareSet.currentImg; |
|
var refImg = compareSet.refImg; |
|
|
|
// Create a canvas |
|
// https://github.com/scijs/get-pixels/blob/7c447cd979637b31e47e148f238a1e71611af481/dom-pixels.js#L14-L18 |
|
var canvasEl = document.createElement('canvas'); |
|
canvasEl.width = refImg.width; |
|
canvasEl.height = refImg.height; |
|
var context = canvasEl.getContext('2d'); |
|
|
|
// Load our reference into the canvas |
|
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage |
|
context.drawImage(refImg, 0, 0); |
|
|
|
// Update our selected portion on reference and draw image in clipping |
|
// DEV: This is probably the most efficient way (outside of web workers) because |
|
// we would have to draw image twice no matter what |
|
// Maybe there's double pixel updates but I don't thinks so |
|
// DEV: Performance alternatives we thought of but aren't needed |
|
// Extract image from 2nd canvas via `ndarray` |
|
// Use web workers |
|
// Requesting server do it via `get-pixels` and `save-pixels` |
|
// DEV: Slowest part is drawing initial image above |
|
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rect |
|
context.rect(targetArea.left, targetArea.top, targetArea.width, targetArea.height); |
|
context.clip(); |
|
context.drawImage(currentImg, 0, 0); |
|
|
|
// Duplicate reference image and shrink both canvas/image for output |
|
var imgHeight = 250; |
|
canvasEl.style.height = imgHeight + 'px'; |
|
var refImgClone = refImg.cloneNode(); |
|
refImgClone.style.height = imgHeight + 'px'; |
|
|
|
// Generate and append result content |
|
// DEV: We use a document fragment to avoid `n` DOM edits -- instead it's 1 |
|
var resultGroupEl = D.DIV([ |
|
D.TR([ |
|
// TODO: Add collapse support like in `gemini-gui` |
|
D.TD({colspan: 3}, D.B(compareSet.name)) |
|
]), |
|
D.TR([ |
|
// TODO: Move style out of inline and to classes for more performance |
|
D.TD({style: 'padding-right: 10px;'}, 'Save update:'), |
|
D.TD('Original ref:'), |
|
D.TD('Updated ref:') |
|
]), |
|
D.TR([ |
|
D.TD({ |
|
style: 'vertical-align: top;' |
|
}, [ |
|
D.INPUT({type: 'checkbox', checked: true}) |
|
]), |
|
D.TD([refImgClone]), |
|
D.TD([canvasEl]) |
|
]) |
|
]); |
|
resultsDocFrag.appendChild(resultGroupEl); |
|
|
|
// TODO: When bulk update is accepted, send overwrite requests to server with new image as "Ref" |
|
// and use `?1` trick to refresh images |
|
// then re-run comparisons for all current vs ref |
|
// DEV: We are realizing that Gemini likely does comparisons in Node.js so we should do the same on update |
|
}); |
|
|
|
// Append aggregate content to DOM |
|
resultsEl.appendChild(resultsDocFrag); |
|
|
|
// End our performance check |
|
console.timeEnd('bulkUpdateSelection'); |
|
} |
|
}); |