diff --git a/package.json b/package.json index 92c8b34f..1ca2e62b 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,10 @@ "merge-lang": "node scripts/merge-lang.js", "format-lang": "node scripts/format-lang.js", "compile-lang": "formatjs compile-folder --ast build lang-compiled", - "check-lang": "node scripts/check-lang.js" + "check-lang": "node scripts/check-lang.js", + "loadtest": "node scripts/loadtest.js", + "loadtest:medium": "node scripts/loadtest.js --weight=medium", + "loadtest:heavy": "node scripts/loadtest.js --weight=heavy --verbose" }, "lint-staged": { "**/*.js": [ @@ -107,6 +110,7 @@ "extract-react-intl-messages": "^4.1.1", "husky": "^4.3.0", "lint-staged": "^10.4.0", + "loadtest": "5.1.0", "npm-run-all": "^4.1.5", "postcss-flexbugs-fixes": "^4.2.1", "postcss-import": "^12.0.1", diff --git a/scripts/loadtest.js b/scripts/loadtest.js new file mode 100644 index 00000000..e2146dd0 --- /dev/null +++ b/scripts/loadtest.js @@ -0,0 +1,140 @@ +const loadtest = require('loadtest'); +const chalk = require('chalk'); +const trunc = num => +num.toFixed(1); + +/** + * Example invocations: + * + * npm run loadtest -- --weight=heavy + * npm run loadtest -- --weight=heavy --verbose + * npm run loadtest -- --weight=single --verbose + * npm run loadtest -- --weight=medium + */ + +/** + * Command line arguments like --weight=heavy and --verbose use this object + * If you are providing _alternative_ configs, use --weight + * e.g. add --weight=ultra then add commandlineOptions.ultra={} + * --verbose can be combied with any weight. + */ +const commandlineOptions = { + single: { + concurrency: 1, + requestsPerSecond: 1, + maxSeconds: 5, + maxRequests: 1, + }, + // Heavy can saturate CPU which leads to requests stalling depending on machine + // Keep an eye if --verbose logs pause, or if node CPU in top is > 100. + // https://github.com/alexfernandez/loadtest#usage-donts + heavy: { + concurrency: 10, + requestsPerSecond: 200, + maxSeconds: 60, + }, + // Throttled requests should not max out CPU, + medium: { + concurrency: 3, + requestsPerSecond: 5, + maxSeconds: 60, + }, + verbose: { statusCallback }, +}; + +const options = { + url: 'http://localhost:3000', + method: 'POST', + concurrency: 5, + requestsPerSecond: 5, + maxSeconds: 5, + requestGenerator: (params, options, client, callback) => { + const message = JSON.stringify(mockPageView()); + options.headers['Content-Length'] = message.length; + options.headers['Content-Type'] = 'application/json'; + options.headers['user-agent'] = 'User-Agent: Mozilla/5.0 LoadTest'; + options.body = message; + options.path = '/api/collect'; + const request = client(options, callback); + request.write(message); + return request; + }, +}; + +function getArgument() { + const weight = process.argv[2] && process.argv[2].replace('--weight=', ''); + const verbose = process.argv.includes('--verbose') && 'verbose'; + return [weight, verbose]; +} + +// Patch in all command line arguments over options object +// Must do this prior to calling `loadTest()` +getArgument().map(arg => Object.assign(options, commandlineOptions[arg])); + +loadtest.loadTest(options, (error, results) => { + if (error) { + return console.error(chalk.redBright('Got an error: %s', error)); + } + console.log(chalk.bold(chalk.yellow('\n--------\n'))); + console.log(chalk.yellowBright('Loadtests complete:'), chalk.greenBright('success'), '\n'); + prettyLogItem('Total Requests:', results.totalRequests); + prettyLogItem('Total Errors:', results.totalErrors); + + prettyLogItem( + 'Latency(mean/min/max)', + trunc(results.meanLatencyMs), + '/', + trunc(results.maxLatencyMs), + '/', + trunc(results.minLatencyMs), + ); + + if (results.totalErrors) { + console.log(chalk.redBright('*'), chalk.red('Total Errors:'), results.totalErrors); + } + + if (results.errorCodes && Object.keys(results.errorCodes).length) { + console.log(chalk.redBright('*'), chalk.red('Error Codes:'), results.errorCodes); + } + // console.log(results); +}); + +/** + * Create a new object for each request. Note, we could randomize values here if desired. + * + * TODO: Need a better way of passing in websiteId, hostname, URL. + * + * @param {object} payload pageview payload same as sent via tracker + */ +function mockPageView( + payload = { + website: 'fcd4c7e3-ed76-439c-9121-3a0f102df126', + hostname: 'localhost', + screen: '1680x1050', + url: '/LOADTESTING', + }, +) { + return { + type: 'pageview', + payload, + }; +} + +// If you pass in --verbose, this function is called +function statusCallback(error, result, latency) { + console.log( + chalk.yellowBright(`\n## req #${result.requestIndex + 1} of ${latency.totalRequests}`), + ); + prettyLogItem('Request elapsed milliseconds:', trunc(result.requestElapsed)); + prettyLogItem( + 'Latency(mean/max/min):', + trunc(latency.meanLatencyMs), + '/', + trunc(latency.maxLatencyMs), + '/', + trunc(latency.minLatencyMs), + ); +} + +function prettyLogItem(label, ...args) { + console.log(chalk.redBright('*'), chalk.green(label), ...args); +}