Merge pull request #254 from acoard/feat/loadtesting

Loadtesting
This commit is contained in:
Mike Cao 2020-10-01 18:59:11 -07:00 committed by GitHub
commit 83f6c2b9a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 148 additions and 3 deletions

1
.gitignore vendored
View File

@ -23,6 +23,7 @@
.DS_Store .DS_Store
.idea .idea
*.iml *.iml
.vscode/*
# debug # debug
npm-debug.log* npm-debug.log*

View File

@ -7,7 +7,7 @@
"button.copy-to-clipboard": "In die Zwischenablage kopieren", "button.copy-to-clipboard": "In die Zwischenablage kopieren",
"button.date-range": "Datumsbereich", "button.date-range": "Datumsbereich",
"button.delete": "Löschen", "button.delete": "Löschen",
"button.dismiss": "Dismiss", "button.dismiss": "Verwerfen",
"button.edit": "Bearbeiten", "button.edit": "Bearbeiten",
"button.login": "Anmelden", "button.login": "Anmelden",
"button.more": "Mehr", "button.more": "Mehr",
@ -55,7 +55,7 @@
"message.get-tracking-code": "Erstelle Tracking Kennung", "message.get-tracking-code": "Erstelle Tracking Kennung",
"message.go-to-settings": "Zu den Einstellungen", "message.go-to-settings": "Zu den Einstellungen",
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.", "message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
"message.new-version-available": "A new version of umami {version} is available!", "message.new-version-available": "Eine neue Version umami {version} ist verfügbar!",
"message.no-data-available": "Keine Daten vorhanden.", "message.no-data-available": "Keine Daten vorhanden.",
"message.no-websites-configured": "Es ist keine Webseite vorhanden.", "message.no-websites-configured": "Es ist keine Webseite vorhanden.",
"message.page-not-found": "Seite nicht gefunden.", "message.page-not-found": "Seite nicht gefunden.",

View File

@ -31,7 +31,10 @@
"merge-lang": "node scripts/merge-lang.js", "merge-lang": "node scripts/merge-lang.js",
"format-lang": "node scripts/format-lang.js", "format-lang": "node scripts/format-lang.js",
"compile-lang": "formatjs compile-folder --ast build lang-compiled", "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": { "lint-staged": {
"**/*.js": [ "**/*.js": [
@ -107,6 +110,7 @@
"extract-react-intl-messages": "^4.1.1", "extract-react-intl-messages": "^4.1.1",
"husky": "^4.3.0", "husky": "^4.3.0",
"lint-staged": "^10.4.0", "lint-staged": "^10.4.0",
"loadtest": "5.1.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"postcss-flexbugs-fixes": "^4.2.1", "postcss-flexbugs-fixes": "^4.2.1",
"postcss-import": "^12.0.1", "postcss-import": "^12.0.1",

140
scripts/loadtest.js Normal file
View File

@ -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);
}