Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smart difference detection #89

Merged
merged 16 commits into from
Nov 18, 2017
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"build:report": "webpack",
"dev:report": "webpack-dev-server --hot --inline --open",
"flow": "flow",
"prepublish": "npm run build && npm run build:report",
"copy:ximgdiff": "copyfiles -u 3 node_modules/x-img-diff-js/build/cv-wasm_browser.* report/assets",
"prepublish": "npm run build && npm run build:report && npm run copy:ximgdiff",
"reg": "node dist/cli.js ./sample/actual ./sample/expected ./sample/diff -I -R ./sample/index.html -T 0.01",
"screenshot": "node test/screenshot.js",
"test:cli": "chmod +x dist/cli.js && avaron test/cli.test.js",
Expand All @@ -35,7 +36,8 @@
"make-dir": "^1.0.0",
"md5-file": "^3.1.1",
"meow": "^3.7.0",
"mustache": "^2.3.0"
"mustache": "^2.3.0",
"x-img-diff-js": "latest"
},
"devDependencies": {
"ava": "^0.22.0",
Expand Down
1 change: 1 addition & 0 deletions report/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
assets/cv-wasm_browser.*
21 changes: 20 additions & 1 deletion report/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,26 @@
<div id="app">
</div>
<script type="text/javascript">
window['__reg__'] = { "type": "danger", "hasNew": true, "newItems": [{ "raw": "/sample.png", "encoded": "%2Fsample.png" }], "hasDeleted": true, "deletedItems": [{ "raw": "/sample.png", "encoded": "%2Fsample.png" }], "hasPassed": false, "passedItems": [{ "raw": "/sample.png", "encoded": "%2Fsample.png" }], "hasFailed": true, "failedItems": [{ "raw": "/sample.png", "encoded": "%2Fsample.png" }], "actualDir": "./sample/actual", "expectedDir": "./sample/expected", "diffDir": "./sample/diff" };
window['__reg__'] = {
"type": "danger",
"hasNew": true,
"newItems": [
{ "raw": "/sample.png", "encoded": "%2Fsample.png" }
],
"hasDeleted": true,
"deletedItems": [ { "raw": "/sample.png", "encoded": "%2Fsample.png" } ],
"hasPassed": false,
"passedItems": [ { "raw": "/sample.png", "encoded": "%2Fsample.png" } ],
"hasFailed": true,
"failedItems": [ { "raw": "/sample.png", "encoded": "%2Fsample.png" } ],
"actualDir": "./sample/actual",
"expectedDir": "./sample/expected",
"diffDir": "./sample/diff",
"ximgdiffConfig": {
"enabled": true,
"workerUrl": "/worker.js"
}
};
</script>
<script src="/static/build.js"></script>
</body>
43 changes: 38 additions & 5 deletions report/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,40 +43,43 @@
<h3 class="ui header items-header red" v-if="failedItems.length">
Changed items
</h3>
<item-details class="items" :icon="'remove'" :color="'red'" :items="failedItems" :open="open" :diffDir="diffDir" :actualDir="actualDir" :expectedDir="expectedDir">
<item-details class="items" :icon="'remove'" :color="'red'" :items="failedItems" :openCapture="openCapture" :openComparison="openComparison" :diffDir="diffDir" :actualDir="actualDir" :expectedDir="expectedDir">
</item-details>

<h3 class="ui header items-header" v-if="newItems.length">
New items
</h3>

<item-details class="items" :icon="'file outline'" :color="'grey'" :items="newItems" :open="open" :actualDir="actualDir">
<item-details class="items" :icon="'file outline'" :color="'grey'" :items="newItems" :openCapture="openCapture" :actualDir="actualDir">
</item-details>

<h3 class="ui header items-header" v-if="deletedItems.length">
Deleted items
</h3>
<item-details class="items" :icon="'trash outline'" :color="'grey'" :items="deletedItems" :open="open" :expectedDir="expectedDir">
<item-details class="items" :icon="'trash outline'" :color="'grey'" :items="deletedItems" :openCapture="openCapture" :expectedDir="expectedDir">
</item-details>

<h3 class="ui header items-header green" v-if="passedItems.length">
Passed items
</h3>
<item-details class="items" :icon="'checkmark'" :color="'green'" :items="passedItems" :open="open" :actualDir="actualDir">
<item-details class="items" :icon="'checkmark'" :color="'green'" :items="passedItems" :openCapture="openCapture" :actualDir="actualDir">
</item-details>
</div>
<div class="footer">
<p>Powered by <a href="https://github.com/reg-viz">reg-viz</a></p>
</div>
<capture-modal :src="modalSrc" :bg="modalBgSrc">
</capture-modal>
<comparison-modal :src="modalSrc" :srcActual="selectedSrcActual" :srcExpected="selectedSrcExpected" :matching="selectedMatchingResult" :bg="modalBgSrc"></comparison-modal>
</div>
</template>

<script>
const SEARCH_DEBOUNCE_MSEC = 50;
const debounce = require('lodash.debounce');
const workerClient = require('./worker-client').default;
const CaptureModal = require('./views/CaptureModal.vue');
const ComparisonModal = require('./views/ComparisonModal.vue');
const ItemSummaries = require('./views/ItemSummaries.vue');
const ItemDetails = require('./views/ItemDetails.vue');

Expand All @@ -98,6 +101,7 @@ module.exports = {
name: 'App',
components: {
'capture-modal': CaptureModal,
'comparison-modal': ComparisonModal,
'item-summaries': ItemSummaries,
'item-details': ItemDetails,
},
Expand All @@ -114,7 +118,19 @@ module.exports = {
passedItems: searchItems('passedItems', getSearchParams()),
newItems: searchItems('newItems', getSearchParams()),
deletedItems: searchItems('deletedItems', getSearchParams()),
lastRequestSequence: null,
selectedRaw: "",
selectedSrcActual: "",
selectedSrcExpected: "",
selectedMatchingResult: null,
}),
created: function () {
workerClient.subscribe(data => {
if (this.lastRequestSequence === data.seq && this.isModalOpen) {
this.selectedMatchingResult = data.result;
}
});
},
computed: {
isNotFound: function () {
return this.failedItems.length === 0 &&
Expand All @@ -124,17 +140,34 @@ module.exports = {
},
},
methods: {
open(src, bg) {
openCapture(src, bg) {
this.modalSrc = src;
this.modalBgSrc = bg;
this.isModalOpen = true;
this.scrollTop = window.pageYOffset;
this.$modal.push('capture')
},

openComparison(src) {
this.modalSrc = src;
this.selectedSrcActual = this.actualDir + src;
this.selectedSrcExpected = this.expectedDir + src;
this.lastRequestSequence = workerClient.requestCalc({
raw: src,
actualSrc: this.selectedSrcActual,
expectedSrc: this.selectedSrcExpected
});
this.isModalOpen = true;
this.scrollTop = window.pageYOffset;
this.$modal.push('comparison')
},

close() {
this.isModalOpen = false;
this.$modal.pop();
this.selectedSrcActual = "";
this.selectedSrcExpected = "";
this.selectedMatchingResult = null;
setTimeout(() => {
window.scrollTo(0, this.scrollTop);
}, 400);
Expand Down
31 changes: 31 additions & 0 deletions report/src/detector-wrapper/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const instantiateCachedURL = require('./util.js');

class ModuleClass {
constructor(opt){
this._initCb = opt.init;
this._version = opt.version;
this._wasmUrl = opt.wasmUrl;
}

locateFile(baseName) {
return self.location.pathname.replace(/\/[^\/]*$/, '/') + baseName;
}

instantiateWasm(imports, callback) {
instantiateCachedURL(this._version, this._wasmUrl, imports)
.then(instance => callback(instance));
return { };
}

onInit(cb) {
this._initCb = cb;
}

onRuntimeInitialized() {
if (this._initCb) {
return this._initCb(this);
}
}
}

module.exports = { ModuleClass };
117 changes: 117 additions & 0 deletions report/src/detector-wrapper/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// 1. +++ fetchAndInstantiate() +++ //
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] Is this file copied from MDN?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I copied and modify a little(added some exception handling).


// This library function fetches the wasm module at 'url', instantiates it with
// the given 'importObject', and returns the instantiated object instance

function fetchAndInstantiate(url, importObject) {
return fetch(url).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results =>
results.instance
);
}

// 2. +++ instantiateCachedURL() +++ //

// This library function fetches the wasm Module at 'url', instantiates it with
// the given 'importObject', and returns a Promise resolving to the finished
// wasm Instance. Additionally, the function attempts to cache the compiled wasm
// Module in IndexedDB using 'url' as the key. The entire site's wasm cache (not
// just the given URL) is versioned by dbVersion and any change in dbVersion on
// any call to instantiateCachedURL() will conservatively clear out the entire
// cache to avoid stale modules.
function instantiateCachedURL(dbVersion, url, importObject) {
const dbName = 'wasm-cache';
const storeName = 'wasm-cache';

// This helper function Promise-ifies the operation of opening an IndexedDB
// database and clearing out the cache when the version changes.
function openDatabase() {
return new Promise((resolve, reject) => {
var request = indexedDB.open(dbName, dbVersion);
request.onerror = reject.bind(null, 'Error opening wasm cache database');
request.onsuccess = () => { resolve(request.result) };
request.onupgradeneeded = event => {
var db = request.result;
if (db.objectStoreNames.contains(storeName)) {
console.log(`Clearing out version ${event.oldVersion} wasm cache`);
db.deleteObjectStore(storeName);
}
console.log(`Creating version ${event.newVersion} wasm cache`);
db.createObjectStore(storeName)
};
});
}

// This helper function Promise-ifies the operation of looking up 'url' in the
// given IDBDatabase.
function lookupInDatabase(db) {
return new Promise((resolve, reject) => {
var store = db.transaction([storeName]).objectStore(storeName);
var request = store.get(url);
request.onerror = reject.bind(null, `Error getting wasm module ${url}`);
request.onsuccess = event => {
if (request.result)
resolve(request.result);
else
reject(`Module ${url} was not found in wasm cache`);
}
});
}

// This helper function fires off an async operation to store the given wasm
// Module in the given IDBDatabase.
function storeInDatabase(db, module) {
var store = db.transaction([storeName], 'readwrite').objectStore(storeName);
try {
var request = store.put(module, url);
request.onerror = err => { console.log(`Failed to store in wasm cache: ${err}`) };
request.onsuccess = err => { console.log(`Successfully stored ${url} in wasm cache`) };
} catch (e) {
console.warn('An error was thrown... in storing wasm cache...');
console.warn(e);
}
}

// This helper function fetches 'url', compiles it into a Module,
// instantiates the Module with the given import object.
function fetchAndInstantiate() {
return fetch(url).then(response =>
response.arrayBuffer()
).then(buffer =>
WebAssembly.instantiate(buffer, importObject)
)
}

// With all the Promise helper functions defined, we can now express the core
// logic of an IndexedDB cache lookup. We start by trying to open a database.
return openDatabase().then(db => {
// Now see if we already have a compiled Module with key 'url' in 'db':
return lookupInDatabase(db).then(module => {
// We do! Instantiate it with the given import object.
console.log(`Found ${url} in wasm cache`);
return WebAssembly.instantiate(module, importObject);
}, errMsg => {
// Nope! Compile from scratch and then store the compiled Module in 'db'
// with key 'url' for next time.
console.log(errMsg);
return fetchAndInstantiate().then(results => {
setTimeout(() => storeInDatabase(db, results.module), 0);
return results.instance;
});
})
},
errMsg => {
// If opening the database failed (due to permissions or quota), fall back
// to simply fetching and compiling the module and don't try to store the
// results.
console.log(errMsg);
return fetchAndInstantiate().then(results =>
results.instance
);
});
}

module.exports = instantiateCachedURL;
6 changes: 5 additions & 1 deletion report/src/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Vue from 'vue';
import VueLazyload from 'vue-lazyload';
import VueThinModal from 'vue-thin-modal'
import VueThinModal from 'vue-thin-modal';
import workerClient from './worker-client';

const App = require('./App.vue');

Expand All @@ -19,3 +20,6 @@ new Vue({
el: '#app',
render: h => h(App),
});

const ximgdiffConfig = window['__reg__'].ximgdiffConfig || { enabled: false };
workerClient.start(ximgdiffConfig);
Loading