CI: Add memory usage regression monitoring for pull requests (#1415)
This commit is contained in:
parent
421733e21b
commit
b7527f7966
31
.github/workflows/benchmark.yml
vendored
31
.github/workflows/benchmark.yml
vendored
|
@ -6,7 +6,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
benchmark:
|
benchmark:
|
||||||
name: Time benchmark
|
name: Time and memory usage benchmark
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -23,13 +23,19 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: '15'
|
node-version: '15'
|
||||||
|
|
||||||
- name: Run pull request benchmark
|
- name: Run pull request time benchmark
|
||||||
run: cd pr && npm install && node test/benchmarks/benchmark.js > benchmarks.txt && cat benchmarks.txt
|
run: cd pr && npm install && npm run --silent benchmark-time > benchmarks.txt && cat benchmarks.txt
|
||||||
|
|
||||||
- name: Run benchmark on master (baseline)
|
- name: Run pull request memory usage benchmark
|
||||||
run: cd master && npm install && node test/benchmarks/benchmark.js > benchmarks.txt && cat benchmarks.txt
|
run: cd pr && npm run --silent benchmark-memory-usage > memory_usage.txt && cat memory_usage.txt
|
||||||
|
|
||||||
- name: Compare benchmark result
|
- name: Run time benchmark on master (baseline)
|
||||||
|
run: cd master && npm install && npm run --silent benchmark-time > benchmarks.txt && cat benchmarks.txt
|
||||||
|
|
||||||
|
- name: Run memory usage benchmark on master (baseline)
|
||||||
|
run: cd master && npm run --silent benchmark-memory-usage > memory_usage.txt && cat memory_usage.txt
|
||||||
|
|
||||||
|
- name: Compare time benchmark result
|
||||||
uses: openpgpjs/github-action-pull-request-benchmark@v1
|
uses: openpgpjs/github-action-pull-request-benchmark@v1
|
||||||
with:
|
with:
|
||||||
tool: 'benchmarkjs'
|
tool: 'benchmarkjs'
|
||||||
|
@ -43,3 +49,16 @@ jobs:
|
||||||
# fail workdlow if 1.5 times slower
|
# fail workdlow if 1.5 times slower
|
||||||
fail-threshold: '150%'
|
fail-threshold: '150%'
|
||||||
fail-on-alert: true
|
fail-on-alert: true
|
||||||
|
|
||||||
|
- name: Compare memory usage benchmark result
|
||||||
|
uses: openpgpjs/github-action-pull-request-benchmark@v1
|
||||||
|
with:
|
||||||
|
tool: 'raw'
|
||||||
|
name: 'memory usage benchmark'
|
||||||
|
pr-benchmark-file-path: pr/memory_usage.txt
|
||||||
|
base-benchmark-file-path: master/memory_usage.txt
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
alert-threshold: '102%'
|
||||||
|
comment-on-alert: true
|
||||||
|
fail-threshold: '110%'
|
||||||
|
fail-on-alert: true
|
||||||
|
|
|
@ -39,6 +39,8 @@
|
||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
"test": "mocha --require esm --timeout 120000 test/unittests.js",
|
"test": "mocha --require esm --timeout 120000 test/unittests.js",
|
||||||
"test-type-definitions": "tsc test/typescript/definitions.ts && node test/typescript/definitions.js",
|
"test-type-definitions": "tsc test/typescript/definitions.ts && node test/typescript/definitions.js",
|
||||||
|
"benchmark-time": "node test/benchmarks/time.js",
|
||||||
|
"benchmark-memory-usage": "node --require esm test/benchmarks/memory_usage.js",
|
||||||
"start": "http-server",
|
"start": "http-server",
|
||||||
"prebrowsertest": "npm run build-test",
|
"prebrowsertest": "npm run build-test",
|
||||||
"browsertest": "npm start -- -o test/unittests.html",
|
"browsertest": "npm start -- -o test/unittests.html",
|
||||||
|
|
219
test/benchmarks/memory_usage.js
Normal file
219
test/benchmarks/memory_usage.js
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
const assert = require('assert');
|
||||||
|
const stream = require('@openpgp/web-stream-tools');
|
||||||
|
const path = require('path');
|
||||||
|
const { writeFileSync, unlinkSync } = require('fs');
|
||||||
|
const { fork } = require('child_process');
|
||||||
|
const openpgp = require('../..');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Benchmark max memory usage recorded during execution of the given function.
|
||||||
|
* This spawns a new v8 instance and runs the code there in isolation, to avoid interference between tests.
|
||||||
|
* @param {Funtion} function to benchmark (can be async)
|
||||||
|
* @returns {NodeJS.MemoryUsage} memory usage snapshot with max RSS (sizes in bytes)
|
||||||
|
*/
|
||||||
|
const benchmark = async function(fn) {
|
||||||
|
const tmpFileName = path.join(__dirname, 'tmp.js');
|
||||||
|
// the code to execute must be written to a file
|
||||||
|
writeFileSync(tmpFileName, `
|
||||||
|
const assert = require('assert');
|
||||||
|
const stream = require('@openpgp/web-stream-tools');
|
||||||
|
const openpgp = require('../..');
|
||||||
|
let maxMemoryComsumption;
|
||||||
|
let activeSampling = false;
|
||||||
|
|
||||||
|
function sampleOnce() {
|
||||||
|
const memUsage = process.memoryUsage();
|
||||||
|
if (!maxMemoryComsumption || memUsage.rss > maxMemoryComsumption.rss) {
|
||||||
|
maxMemoryComsumption = memUsage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function samplePeriodically() {
|
||||||
|
setImmediate(() => {
|
||||||
|
sampleOnce();
|
||||||
|
activeSampling && samplePeriodically();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// main body
|
||||||
|
(async () => {
|
||||||
|
maxMemoryComsumption = null;
|
||||||
|
activeSampling = true;
|
||||||
|
samplePeriodically();
|
||||||
|
await (${fn.toString()})();
|
||||||
|
// setImmediate is run at the end of the event loop, so we need to manually collect the latest sample
|
||||||
|
sampleOnce();
|
||||||
|
process.send(maxMemoryComsumption);
|
||||||
|
process.exit(); // child process doesn't exit otherwise
|
||||||
|
})();
|
||||||
|
`);
|
||||||
|
|
||||||
|
const maxMemoryComsumption = await new Promise((resolve, reject) => {
|
||||||
|
const child = fork(tmpFileName);
|
||||||
|
child.on('message', function (message) {
|
||||||
|
resolve(message);
|
||||||
|
});
|
||||||
|
child.on('error', function (err) {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
unlinkSync(tmpFileName);
|
||||||
|
return maxMemoryComsumption;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = err => {
|
||||||
|
console.error('The memory benchmark tests failed by throwing the following error:');
|
||||||
|
console.error(err);
|
||||||
|
// eslint-disable-next-line no-process-exit
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
class MemoryBenchamrkSuite {
|
||||||
|
constructor() {
|
||||||
|
this.tests = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
add(name, fn) {
|
||||||
|
this.tests.push({ name, fn });
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const stats = [];
|
||||||
|
for (const { name, fn } of this.tests) {
|
||||||
|
const memoryUsage = await benchmark(fn).catch(onError);
|
||||||
|
// convert values to MB
|
||||||
|
Object.entries(memoryUsage).forEach(([name, value]) => {
|
||||||
|
memoryUsage[name] = (value / 1024 / 1024).toFixed(2);
|
||||||
|
});
|
||||||
|
const { rss, ...usageDetails } = memoryUsage;
|
||||||
|
// raw entry format accepted by github-action-pull-request-benchmark
|
||||||
|
stats.push({
|
||||||
|
name,
|
||||||
|
value: rss,
|
||||||
|
range: Object.entries(usageDetails).map(([name, value]) => `${name}: ${value}`).join(', '),
|
||||||
|
unit: 'MB',
|
||||||
|
biggerIsBetter: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory usage tests.
|
||||||
|
* All the necessary variables must be declared inside the test function.
|
||||||
|
*/
|
||||||
|
(async () => {
|
||||||
|
const suite = new MemoryBenchamrkSuite();
|
||||||
|
|
||||||
|
suite.add('empty test (baseline)', () => {});
|
||||||
|
|
||||||
|
suite.add('openpgp.encrypt/decrypt (CFB, binary)', async () => {
|
||||||
|
const passwords = 'password';
|
||||||
|
const config = { aeadProtect: false, preferredCompressionAlgorithm: openpgp.enums.compression.uncompressed };
|
||||||
|
const plaintextMessage = await openpgp.createMessage({ binary: new Uint8Array(1000000).fill(1) });
|
||||||
|
|
||||||
|
const armoredEncryptedMessage = await openpgp.encrypt({ message: plaintextMessage, passwords, config });
|
||||||
|
const encryptedMessage = await openpgp.readMessage({ armoredMessage: armoredEncryptedMessage });
|
||||||
|
assert.ok(encryptedMessage.packets[1] instanceof openpgp.SymEncryptedIntegrityProtectedDataPacket);
|
||||||
|
await openpgp.decrypt({ message: encryptedMessage, passwords, config });
|
||||||
|
});
|
||||||
|
|
||||||
|
suite.add('openpgp.encrypt/decrypt (CFB, text)', async () => {
|
||||||
|
const passwords = 'password';
|
||||||
|
const config = { aeadProtect: false, preferredCompressionAlgorithm: openpgp.enums.compression.uncompressed };
|
||||||
|
const plaintextMessage = await openpgp.createMessage({ text: 'a'.repeat(10000000 / 2) }); // two bytes per character
|
||||||
|
|
||||||
|
const armoredEncryptedMessage = await openpgp.encrypt({ message: plaintextMessage, passwords, config });
|
||||||
|
const encryptedMessage = await openpgp.readMessage({ armoredMessage: armoredEncryptedMessage });
|
||||||
|
assert.ok(encryptedMessage.packets[1] instanceof openpgp.SymEncryptedIntegrityProtectedDataPacket);
|
||||||
|
await openpgp.decrypt({ message: encryptedMessage, passwords, config });
|
||||||
|
});
|
||||||
|
|
||||||
|
suite.add('openpgp.encrypt/decrypt (AEAD, binary)', async () => {
|
||||||
|
const passwords = 'password';
|
||||||
|
const config = { aeadProtect: true, preferredCompressionAlgorithm: openpgp.enums.compression.uncompressed };
|
||||||
|
const plaintextMessage = await openpgp.createMessage({ binary: new Uint8Array(1000000).fill(1) });
|
||||||
|
|
||||||
|
const armoredEncryptedMessage = await openpgp.encrypt({ message: plaintextMessage, passwords, config });
|
||||||
|
const encryptedMessage = await openpgp.readMessage({ armoredMessage: armoredEncryptedMessage });
|
||||||
|
assert.ok(encryptedMessage.packets[1] instanceof openpgp.AEADEncryptedDataPacket);
|
||||||
|
await openpgp.decrypt({ message: encryptedMessage, passwords, config });
|
||||||
|
});
|
||||||
|
|
||||||
|
suite.add('openpgp.encrypt/decrypt (AEAD, text)', async () => {
|
||||||
|
const passwords = 'password';
|
||||||
|
const config = { aeadProtect: true, preferredCompressionAlgorithm: openpgp.enums.compression.uncompressed };
|
||||||
|
const plaintextMessage = await openpgp.createMessage({ text: 'a'.repeat(10000000 / 2) }); // two bytes per character
|
||||||
|
|
||||||
|
const armoredEncryptedMessage = await openpgp.encrypt({ message: plaintextMessage, passwords, config });
|
||||||
|
const encryptedMessage = await openpgp.readMessage({ armoredMessage: armoredEncryptedMessage });
|
||||||
|
assert.ok(encryptedMessage.packets[1] instanceof openpgp.AEADEncryptedDataPacket);
|
||||||
|
await openpgp.decrypt({ message: encryptedMessage, passwords, config });
|
||||||
|
});
|
||||||
|
|
||||||
|
// streaming tests
|
||||||
|
suite.add('openpgp.encrypt/decrypt (CFB, binary, with streaming)', async () => {
|
||||||
|
await stream.loadStreamsPonyfill();
|
||||||
|
|
||||||
|
const passwords = 'password';
|
||||||
|
const config = { aeadProtect: false, preferredCompressionAlgorithm: openpgp.enums.compression.uncompressed };
|
||||||
|
const plaintextMessage = await openpgp.createMessage({ binary: stream.toStream(new Uint8Array(1000000).fill(1)) });
|
||||||
|
assert(plaintextMessage.fromStream);
|
||||||
|
|
||||||
|
const armoredEncryptedMessage = await openpgp.encrypt({ message: plaintextMessage, passwords, config });
|
||||||
|
const encryptedMessage = await openpgp.readMessage({ armoredMessage: armoredEncryptedMessage });
|
||||||
|
assert.ok(encryptedMessage.packets[1] instanceof openpgp.SymEncryptedIntegrityProtectedDataPacket);
|
||||||
|
const { data: decryptedData } = await openpgp.decrypt({ message: encryptedMessage, passwords, config });
|
||||||
|
await stream.readToEnd(decryptedData);
|
||||||
|
});
|
||||||
|
|
||||||
|
suite.add('openpgp.encrypt/decrypt (CFB, text, with streaming)', async () => {
|
||||||
|
await stream.loadStreamsPonyfill();
|
||||||
|
|
||||||
|
const passwords = 'password';
|
||||||
|
const config = { aeadProtect: false, preferredCompressionAlgorithm: openpgp.enums.compression.uncompressed };
|
||||||
|
const plaintextMessage = await openpgp.createMessage({ text: stream.toStream('a'.repeat(10000000 / 2)) });
|
||||||
|
assert(plaintextMessage.fromStream);
|
||||||
|
|
||||||
|
const armoredEncryptedMessage = await openpgp.encrypt({ message: plaintextMessage, passwords, config });
|
||||||
|
const encryptedMessage = await openpgp.readMessage({ armoredMessage: armoredEncryptedMessage });
|
||||||
|
assert.ok(encryptedMessage.packets[1] instanceof openpgp.SymEncryptedIntegrityProtectedDataPacket);
|
||||||
|
const { data: decryptedData } = await openpgp.decrypt({ message: encryptedMessage, passwords, config });
|
||||||
|
await stream.readToEnd(decryptedData);
|
||||||
|
});
|
||||||
|
|
||||||
|
suite.add('openpgp.encrypt/decrypt (AEAD, binary, with streaming)', async () => {
|
||||||
|
await stream.loadStreamsPonyfill();
|
||||||
|
|
||||||
|
const passwords = 'password';
|
||||||
|
const config = { aeadProtect: true, preferredCompressionAlgorithm: openpgp.enums.compression.uncompressed };
|
||||||
|
const plaintextMessage = await openpgp.createMessage({ binary: stream.toStream(new Uint8Array(1000000).fill(1)) });
|
||||||
|
assert(plaintextMessage.fromStream);
|
||||||
|
|
||||||
|
const armoredEncryptedMessage = await openpgp.encrypt({ message: plaintextMessage, passwords, config });
|
||||||
|
const encryptedMessage = await openpgp.readMessage({ armoredMessage: armoredEncryptedMessage });
|
||||||
|
assert.ok(encryptedMessage.packets[1] instanceof openpgp.AEADEncryptedDataPacket);
|
||||||
|
await openpgp.decrypt({ message: encryptedMessage, passwords, config });
|
||||||
|
});
|
||||||
|
|
||||||
|
suite.add('openpgp.encrypt/decrypt (AEAD, text, with streaming)', async () => {
|
||||||
|
await stream.loadStreamsPonyfill();
|
||||||
|
|
||||||
|
const passwords = 'password';
|
||||||
|
const config = { aeadProtect: true, preferredCompressionAlgorithm: openpgp.enums.compression.uncompressed };
|
||||||
|
const plaintextMessage = await openpgp.createMessage({ text: stream.toStream('a'.repeat(10000000 / 2)) });
|
||||||
|
assert(plaintextMessage.fromStream);
|
||||||
|
|
||||||
|
const armoredEncryptedMessage = await openpgp.encrypt({ message: plaintextMessage, passwords, config });
|
||||||
|
const encryptedMessage = await openpgp.readMessage({ armoredMessage: armoredEncryptedMessage });
|
||||||
|
assert.ok(encryptedMessage.packets[1] instanceof openpgp.AEADEncryptedDataPacket);
|
||||||
|
await openpgp.decrypt({ message: encryptedMessage, passwords, config });
|
||||||
|
});
|
||||||
|
|
||||||
|
const stats = await suite.run();
|
||||||
|
// Print JSON stats to stdout
|
||||||
|
console.log(JSON.stringify(stats, null, 4));
|
||||||
|
})();
|
Loading…
Reference in New Issue
Block a user