2019-09-30 18:28:17 -04:00
|
|
|
/* eslint-env es6 */
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
const compactStringify = require('json-stringify-pretty-compact');
|
|
|
|
|
|
|
|
const comparePath = './test/__compare__';
|
2019-11-07 17:34:59 -05:00
|
|
|
const compareCommand = 'open http://localhost:8000/?compare';
|
|
|
|
const diffSaveCommand = 'npm run test-save';
|
|
|
|
const diffCommand = 'code -n -d test/__compare__/examples-build.json test/__compare__/examples-dev.json';
|
|
|
|
const equalityThreshold = 0.99999;
|
2019-09-30 18:28:17 -04:00
|
|
|
|
2019-11-07 17:34:59 -05:00
|
|
|
const colors = { Red: 31, Green: 32, Yellow: 33, White: 37, BrightWhite: 90, BrightCyan: 36 };
|
|
|
|
const color = (text, number) => number ? `\x1b[${number}m${text}\x1b[0m` : text;
|
2021-11-28 16:26:39 -05:00
|
|
|
const toPercent = val => (100 * val).toFixed(3);
|
2021-11-20 07:27:14 -05:00
|
|
|
const toPercentRound = val => Math.round(100 * val);
|
|
|
|
|
2021-11-28 16:25:11 -05:00
|
|
|
const requireUncached = path => {
|
|
|
|
delete require.cache[require.resolve(path)];
|
|
|
|
const module = require(path);
|
|
|
|
delete require.cache[require.resolve(path)];
|
|
|
|
return module;
|
|
|
|
};
|
|
|
|
|
2021-11-20 07:27:14 -05:00
|
|
|
const noiseThreshold = (val, threshold) => {
|
|
|
|
const sign = val < 0 ? -1 : 1;
|
|
|
|
const magnitude = Math.abs(val);
|
|
|
|
return sign * Math.max(0, magnitude - threshold) / (1 - threshold);
|
|
|
|
};
|
2019-09-30 18:28:17 -04:00
|
|
|
|
|
|
|
const similarity = (a, b) => {
|
2020-03-09 17:05:05 -04:00
|
|
|
const distance = Math.sqrt(a.reduce(
|
|
|
|
(sum, _val, i) => sum + Math.pow((a[i] || 0) - (b[i] || 0), 2), 0)
|
|
|
|
);
|
2019-09-30 18:28:17 -04:00
|
|
|
return 1 / (1 + (distance / a.length));
|
|
|
|
};
|
|
|
|
|
2019-11-07 17:34:59 -05:00
|
|
|
const captureSimilarityExtrinsic = (currentCaptures, referenceCaptures) => {
|
2019-09-30 18:28:17 -04:00
|
|
|
const result = {};
|
|
|
|
|
2019-11-07 17:34:59 -05:00
|
|
|
Object.entries(currentCaptures).forEach(([name, current]) => {
|
|
|
|
const reference = referenceCaptures[name];
|
2019-09-30 18:28:17 -04:00
|
|
|
const worldVector = [];
|
|
|
|
const worldVectorRef = [];
|
|
|
|
|
2019-11-07 17:34:59 -05:00
|
|
|
Object.keys(current.extrinsic).forEach(objectType => {
|
|
|
|
Object.keys(current.extrinsic[objectType]).forEach(objectId => {
|
|
|
|
worldVector.push(...current.extrinsic[objectType][objectId]);
|
|
|
|
worldVectorRef.push(...reference.extrinsic[objectType][objectId]);
|
2019-09-30 18:28:17 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
result[name] = similarity(worldVector, worldVectorRef);
|
|
|
|
});
|
|
|
|
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2020-12-05 18:39:16 -05:00
|
|
|
const writeResult = (name, obj) => {
|
2019-09-30 18:28:17 -04:00
|
|
|
try {
|
|
|
|
fs.mkdirSync(comparePath, { recursive: true });
|
|
|
|
} catch (err) {
|
|
|
|
if (err.code !== 'EEXIST') throw err;
|
|
|
|
}
|
2020-12-05 18:39:16 -05:00
|
|
|
|
|
|
|
if (typeof obj === 'string') {
|
|
|
|
fs.writeFileSync(`${comparePath}/${name}.md`, obj, 'utf8');
|
|
|
|
} else {
|
|
|
|
fs.writeFileSync(`${comparePath}/${name}.json`, compactStringify(obj, { maxLength: 100 }), 'utf8');
|
|
|
|
}
|
2019-09-30 18:28:17 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
const toMatchExtrinsics = {
|
2019-11-07 17:34:59 -05:00
|
|
|
toMatchExtrinsics(received, value) {
|
|
|
|
const similaritys = captureSimilarityExtrinsic(received, value);
|
|
|
|
const pass = Object.values(similaritys).every(similarity => similarity >= equalityThreshold);
|
2019-09-30 18:28:17 -04:00
|
|
|
|
|
|
|
return {
|
2019-11-07 17:34:59 -05:00
|
|
|
message: () => 'Expected positions and velocities to match between builds.',
|
2019-09-30 18:28:17 -04:00
|
|
|
pass
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const toMatchIntrinsics = {
|
2019-11-07 17:34:59 -05:00
|
|
|
toMatchIntrinsics(currentCaptures, referenceCaptures) {
|
|
|
|
const entries = Object.entries(currentCaptures);
|
|
|
|
let changed = false;
|
2019-09-30 18:28:17 -04:00
|
|
|
|
|
|
|
entries.forEach(([name, current]) => {
|
2019-11-07 17:34:59 -05:00
|
|
|
const reference = referenceCaptures[name];
|
|
|
|
if (!this.equals(current.intrinsic, reference.intrinsic)) {
|
|
|
|
changed = true;
|
2019-09-30 18:28:17 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-11-07 17:34:59 -05:00
|
|
|
return {
|
|
|
|
message: () => 'Expected intrinsic properties to match between builds.',
|
|
|
|
pass: !changed
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const similarityRatings = similarity => similarity < equalityThreshold ? color('●', colors.Yellow) : '·';
|
|
|
|
const changeRatings = isChanged => isChanged ? color('◆', colors.White) : '·';
|
2019-09-30 18:28:17 -04:00
|
|
|
|
2019-11-07 17:34:59 -05:00
|
|
|
const equals = (a, b) => {
|
|
|
|
try {
|
|
|
|
expect(a).toEqual(b);
|
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
};
|
2019-09-30 18:28:17 -04:00
|
|
|
|
2021-01-31 12:34:38 -05:00
|
|
|
const logReport = (captures, version) => {
|
2021-01-28 19:09:27 -05:00
|
|
|
let report = '';
|
|
|
|
|
|
|
|
for (const capture of Object.values(captures)) {
|
|
|
|
if (!capture.logs.length) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
report += ` ${capture.name}\n`;
|
|
|
|
|
|
|
|
for (const log of capture.logs) {
|
|
|
|
report += ` ${log}\n`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-21 11:58:29 -05:00
|
|
|
return `Output logs from ${color(version, colors.Yellow)} build on last run\n\n`
|
2021-01-31 12:34:38 -05:00
|
|
|
+ (report ? report : ' None\n');
|
2021-01-28 19:09:27 -05:00
|
|
|
};
|
|
|
|
|
2021-11-21 12:00:53 -05:00
|
|
|
const comparisonReport = (capturesDev, capturesBuild, devSize, buildSize, buildVersion, save) => {
|
2019-11-07 17:34:59 -05:00
|
|
|
const similaritys = captureSimilarityExtrinsic(capturesDev, capturesBuild);
|
|
|
|
const similarityEntries = Object.entries(similaritys);
|
|
|
|
const devIntrinsicsChanged = {};
|
|
|
|
const buildIntrinsicsChanged = {};
|
|
|
|
let intrinsicChangeCount = 0;
|
|
|
|
let totalTimeBuild = 0;
|
|
|
|
let totalTimeDev = 0;
|
2020-03-09 16:56:06 -04:00
|
|
|
let totalOverlapBuild = 0;
|
|
|
|
let totalOverlapDev = 0;
|
2021-11-20 07:27:14 -05:00
|
|
|
let totalMemoryBuild = 0;
|
|
|
|
let totalMemoryDev = 0;
|
2019-11-07 17:34:59 -05:00
|
|
|
|
|
|
|
const capturePerformance = Object.entries(capturesDev).map(([name]) => {
|
2020-03-09 16:56:06 -04:00
|
|
|
totalTimeBuild += capturesBuild[name].duration;
|
|
|
|
totalTimeDev += capturesDev[name].duration;
|
2021-11-20 07:27:14 -05:00
|
|
|
|
2020-03-09 16:56:06 -04:00
|
|
|
totalOverlapBuild += capturesBuild[name].overlap;
|
|
|
|
totalOverlapDev += capturesDev[name].overlap;
|
2019-11-07 17:34:59 -05:00
|
|
|
|
2021-11-20 07:27:14 -05:00
|
|
|
totalMemoryBuild += capturesBuild[name].memory;
|
|
|
|
totalMemoryDev += capturesDev[name].memory;
|
|
|
|
|
2019-11-07 17:34:59 -05:00
|
|
|
const changedIntrinsics = !equals(capturesDev[name].intrinsic, capturesBuild[name].intrinsic);
|
|
|
|
if (changedIntrinsics) {
|
|
|
|
capturesDev[name].changedIntrinsics = true;
|
|
|
|
if (intrinsicChangeCount < 2) {
|
|
|
|
devIntrinsicsChanged[name] = capturesDev[name].intrinsic;
|
|
|
|
buildIntrinsicsChanged[name] = capturesBuild[name].intrinsic;
|
|
|
|
intrinsicChangeCount += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { name };
|
|
|
|
});
|
2019-09-30 18:28:17 -04:00
|
|
|
|
2019-11-07 17:34:59 -05:00
|
|
|
capturePerformance.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
similarityEntries.sort((a, b) => a[1] - b[1]);
|
2019-09-30 18:28:17 -04:00
|
|
|
|
2021-11-21 12:00:53 -05:00
|
|
|
const perfChange = noiseThreshold(1 - (totalTimeDev / totalTimeBuild), 0.01);
|
|
|
|
const memoryChange = noiseThreshold((totalMemoryDev / totalMemoryBuild) - 1, 0.01);
|
|
|
|
const overlapChange = (totalOverlapDev / (totalOverlapBuild || 1)) - 1;
|
|
|
|
const filesizeChange = (devSize / buildSize) - 1;
|
2019-09-30 18:28:17 -04:00
|
|
|
|
2020-03-09 16:56:06 -04:00
|
|
|
let similarityAvg = 0;
|
2019-11-07 17:34:59 -05:00
|
|
|
similarityEntries.forEach(([_, similarity]) => {
|
|
|
|
similarityAvg += similarity;
|
|
|
|
});
|
2019-09-30 18:28:17 -04:00
|
|
|
|
2019-11-07 17:34:59 -05:00
|
|
|
similarityAvg /= similarityEntries.length;
|
2019-09-30 18:28:17 -04:00
|
|
|
|
2020-12-05 18:39:16 -05:00
|
|
|
const report = (breakEvery, format) => [
|
2019-11-07 17:34:59 -05:00
|
|
|
[`Output comparison of ${similarityEntries.length}`,
|
2021-01-31 12:34:38 -05:00
|
|
|
`examples against previous release ${format('matter-js@' + buildVersion, colors.Yellow)}`
|
2019-11-07 17:34:59 -05:00
|
|
|
].join(' '),
|
2020-12-05 18:39:16 -05:00
|
|
|
`\n\n${format('Similarity', colors.White)}`,
|
|
|
|
`${format(toPercent(similarityAvg), similarityAvg === 1 ? colors.Green : colors.Yellow)}%`,
|
2021-11-21 12:00:53 -05:00
|
|
|
`${format('Overlap', colors.White)}`,
|
|
|
|
`${format((overlapChange >= 0 ? '+' : '-') + toPercent(Math.abs(overlapChange)), overlapChange <= 0 ? colors.Green : colors.Yellow)}%`,
|
2021-11-28 16:26:39 -05:00
|
|
|
`${format('Performance ~', colors.White)}`,
|
2021-11-21 12:00:53 -05:00
|
|
|
`${format((perfChange >= 0 ? '+' : '-') + toPercentRound(Math.abs(perfChange)), perfChange >= 0 ? colors.Green : colors.Yellow)}%`,
|
2021-11-28 16:26:39 -05:00
|
|
|
`${format('Memory ~', colors.White)}`,
|
2021-11-21 12:00:53 -05:00
|
|
|
`${format((memoryChange >= 0 ? '+' : '-') + toPercentRound(Math.abs(memoryChange)), memoryChange <= 0 ? colors.Green : colors.Yellow)}%`,
|
2019-11-07 17:34:59 -05:00
|
|
|
capturePerformance.reduce((output, p, i) => {
|
|
|
|
output += `${p.name} `;
|
|
|
|
output += `${similarityRatings(similaritys[p.name])} `;
|
|
|
|
output += `${changeRatings(capturesDev[p.name].changedIntrinsics)} `;
|
2020-12-05 18:39:16 -05:00
|
|
|
if (i > 0 && i < capturePerformance.length && breakEvery > 0 && i % breakEvery === 0) {
|
2019-11-07 17:34:59 -05:00
|
|
|
output += '\n';
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}, '\n\n'),
|
2021-11-19 15:30:12 -05:00
|
|
|
`\n\nwhere · no change ● extrinsics changed ◆ intrinsics changed\n`,
|
2020-12-05 18:39:16 -05:00
|
|
|
similarityAvg < 1 ? `\n${format('▶', colors.White)} ${format(compareCommand + '=' + 120 + '#' + similarityEntries[0][0], colors.BrightCyan)}` : '',
|
2021-11-28 16:26:39 -05:00
|
|
|
intrinsicChangeCount > 0 ? `\n${format('▶', colors.White)} ${format((save ? diffCommand : diffSaveCommand), colors.BrightCyan)}` : '',
|
|
|
|
`\n\n${format('Filesize', colors.White)}`,
|
|
|
|
`${format((filesizeChange >= 0 ? '+' : '-') + toPercent(Math.abs(filesizeChange)), filesizeChange <= 0 ? colors.Green : colors.Yellow)}%`,
|
|
|
|
`${format(`${(devSize / 1024).toPrecision(4)} KB`, colors.White)}`,
|
2019-11-07 17:34:59 -05:00
|
|
|
].join(' ');
|
2020-12-05 18:39:16 -05:00
|
|
|
|
|
|
|
if (save) {
|
|
|
|
writeResult('examples-dev', devIntrinsicsChanged);
|
|
|
|
writeResult('examples-build', buildIntrinsicsChanged);
|
|
|
|
writeResult('examples-report', report(5, s => s));
|
|
|
|
}
|
|
|
|
|
|
|
|
return report(5, color);
|
2019-09-30 18:28:17 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = {
|
2021-11-28 16:39:11 -05:00
|
|
|
requireUncached, comparisonReport, logReport,
|
2019-11-07 17:34:59 -05:00
|
|
|
toMatchExtrinsics, toMatchIntrinsics
|
2019-09-30 18:28:17 -04:00
|
|
|
};
|