diff --git a/tests/profiling/dao.js b/tests/profiling/dao.js new file mode 100644 index 0000000..25c7a52 --- /dev/null +++ b/tests/profiling/dao.js @@ -0,0 +1,29 @@ +function simulateNetwork() { + let tick = 0; + while(true) { + if (++tick > 0xffff) break; + } + return true; +} + +function calculateResponseTime(numItems) { + return Math.round(30 + numItems * 0.5); +} + +function getAssets(ids, { language }) { + return new Promise((resolve, reject) => { + simulateNetwork(); + setTimeout(() => resolve(ids.map(id => ({ id, language }))), calculateResponseTime(ids.length)); + }); +} + +function getErroredRequest(ids, { language }) { + return new Promise((resolve, reject) => { + throw new Error('Something went wrong'); + }); +} + +module.exports = { + getAssets, + getErroredRequest, +}; diff --git a/tests/profiling/index.js b/tests/profiling/index.js index 58f64c4..d2dbaba 100644 --- a/tests/profiling/index.js +++ b/tests/profiling/index.js @@ -6,77 +6,51 @@ /* Requires ------------------------------------------------------------------*/ const crypto = require('crypto'); -const {getAssets, getErroredRequest} = require('../integration/utils/dao.js'); const settings = require('./settings'); -const HA = require('../../src/index.js'); +const {fork} = require('child_process'); +const path = require('path'); /* Init ----------------------------------------------------------------------*/ // Setup -const store = HA(settings.setup); +const app = fork(path.resolve(__dirname, './testApp.js')); const startTime = Date.now(); -// Suite -const suite = { - sampleRange: 2, - completed: 0, - cacheHits: 0, - sum: 0, - timeouts: 0, - batches: 0, - startHeap: process.memoryUsage().rss, -}; - -store.on('query', () => { suite.batches++; }); -store.on('cacheHit', () => { suite.cacheHits++; }); - async function hitStore() { if (Date.now() - startTime < settings.test.testDuration) { - setTimeout(hitStore, settings.test.requestDelay); - let finished = false; - setTimeout(() => { - if (finished === false) suite.timeouts++; - }, 500); + process.nextTick(hitStore); + // Simulate normal z-distribution - suite.sampleRange = (Math.round(Math.random()*3) === 0) ? 1:2; - const randomError = (Math.round(Math.random()*10) === 0); - const id = crypto.randomBytes(suite.sampleRange).toString('hex'); + let sampleRange = (Math.round(Math.random()*3) === 0) ? 1:2; + const id = crypto.randomBytes(sampleRange).toString('hex'); const language = settings.test.languages[Math.floor(Math.random()*settings.test.languages.length)]; - const before = Date.now(); - if (randomError) store.config.resolver = getErroredRequest; - else store.config.resolver = getAssets; - store.get(id, { language }, crypto.randomBytes(8).toString('hex')) - .then((result) => { - if (!result || result.id !== id || result.language !== language) { - console.log(result, 'does not match', id, language); - throw new Error('integrity test failed'); - } - suite.sum += (Date.now() - before); - finished = true; - suite.completed++; - }, () => {}) - .catch((err) => { console.log(err); process.exit(1)} ); + app.send({ id, language }); } else { - console.log(` - ${suite.completed} completed requests - ${suite.cacheHits} cache hits - ${JSON.stringify(await store.size())} - ${suite.timeouts} timed out - avg response time ${(suite.sum / suite.completed).toFixed(3)} - ${suite.batches} queries sent - ${((process.memoryUsage().rss - suite.startHeap) / 1024).toFixed(2)} Kbytes allocated - `); - for (const expectation in settings.assert) { - if (suite[expectation] < settings.assert[expectation][0] || suite[expectation] > settings.assert[expectation][1]) { - console.log(expectation, 'did not match expectation', settings.assert[expectation]); - throw new Error('performance test failed'); - } - } - - process.exit(0); + app.send('finish'); } } +app.on('message', async (suite) => { + console.log(` + ${suite.completed} completed requests + ${suite.cacheHits} cache hits + ${JSON.stringify(suite.size)} in memory + ${suite.timeouts} timed out + avg response time ${(suite.sum / suite.completed).toFixed(3)} + ${suite.batches} queries sent + ${suite.avgBatchSize} items per queries on average + ${(suite.startHeap / 1024).toFixed(2)} Kbytes allocated + `); + + for (const expectation in settings.assert) { + if (suite[expectation] < settings.assert[expectation][0] || suite[expectation] > settings.assert[expectation][1]) { + console.error(new Error(`Performance test failed: ${expectation} did not match expectation ${settings.assert[expectation]}`)); + process.exit(1); + } + } + process.exit(0); +}); + hitStore(); diff --git a/tests/profiling/settings.js b/tests/profiling/settings.js index 95deb72..aca4c9b 100644 --- a/tests/profiling/settings.js +++ b/tests/profiling/settings.js @@ -1,23 +1,24 @@ -const {getAssets} = require('../integration/utils/dao.js'); +const {getAssets} = require('./dao.js'); module.exports = { test: { - testDuration: 60000, + testDuration: 1000, requestDelay: 0, - languages: ['fr', 'en', 'pr', 'it', 'ge'] + languages: ['fr', 'en', 'ge', 'it', 'pr'], }, setup: { resolver: getAssets, uniqueParams: ['language'], cache: { limit: 60000, steps: 5, base: 5000 }, - batch: { tick: 2, limit: 50 }, + batch: { tick: 10, max: 50 }, retry: { base: 1, step: 2 }, }, assert: { - completed: [30000, 100000], - cacheHits: [1000, 4000], - timeouts: [500, 4000], - batches: [20000, 60000], - rss: [30000, 60000], + completed: [100000, 200000], + cacheHits: [25000, 70000], + timeouts: [500, 8000], + batches: [500, 4000], + rss: [90000, 200000], + avgBatchSize: [30, 50], }, } \ No newline at end of file diff --git a/tests/profiling/testApp.js b/tests/profiling/testApp.js new file mode 100644 index 0000000..919b07a --- /dev/null +++ b/tests/profiling/testApp.js @@ -0,0 +1,68 @@ +/** + * Test app worker - allows us to saturate the request generator without impacting the app + */ + +/* Requires ------------------------------------------------------------------*/ + +const settings = require('./settings'); +const HA = require('../../src/index.js'); +const {getAssets,getErroredRequest} = require('./dao'); +const crypto = require('crypto'); + +/* Local variables -----------------------------------------------------------*/ + +let handbreak = false; + +const suite = { + completed: 0, + cacheHits: 0, + sum: 0, + timeouts: 0, + batches: 0, + avgBatchSize: 0, + startHeap: process.memoryUsage().rss, +}; + +const store = HA(settings.setup); + +/* Methods -------------------------------------------------------------------*/ + +function handleRequest(id, language) { + const randomError = (Math.round(Math.random()*10) === 0); + let finished = false; + const before = Date.now(); + setTimeout(() => { + if (finished === false) suite.timeouts++; + }, 500); + if (randomError) store.config.resolver = getErroredRequest; + else store.config.resolver = getAssets; + store.get(id, { language }, crypto.randomBytes(8).toString('hex')) + .then((result) => { + finished = true; + if (handbreak) return; + if (!result || result.id !== id || result.language !== language) { + console.log(result, 'does not match', id, language); + throw new Error('integrity test failed'); + } + suite.sum += (Date.now() - before); + suite.completed++; + }, () => {}) + .catch((err) => { console.log(err); process.exit(1)} ); +} + +store.on('query', batch => { suite.batches++; suite.avgBatchSize += batch.ids.length; }); +store.on('cacheHit', () => { suite.cacheHits++; }); + +//End +async function complete() { + handbreak = true; + suite.avgBatchSize = Math.round(suite.avgBatchSize / suite.batches); + suite.size = await store.size(); + suite.startHeap = process.memoryUsage().rss - suite.startHeap; + + process.send(suite); +} + +/* Init ----------------------------------------------------------------------*/ + +process.on('message', (msg) => msg === 'finish' ? complete() : handleRequest(msg.id, msg.language));