run-load-test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. /* tslint:disable:no-console */
  2. import { INestApplication } from '@nestjs/common';
  3. import { bootstrap } from '@vendure/core';
  4. import { spawn } from 'child_process';
  5. import stringify from 'csv-stringify';
  6. import fs from 'fs';
  7. import path from 'path';
  8. import { omit } from '../../common/src/omit';
  9. import { generateSummary, LoadTestSummary } from './generate-summary';
  10. import { getLoadTestConfig, getProductCount } from './load-test-config';
  11. const count = getProductCount();
  12. if (require.main === module) {
  13. console.log(`\n============= Vendure Load Test: ${count} products ============\n`);
  14. // Runs the init script to generate test data and populate the test database
  15. const init = spawn('node', ['-r', 'ts-node/register', './init-load-test.ts', count.toString()], {
  16. cwd: __dirname,
  17. stdio: 'inherit',
  18. });
  19. init.on('exit', code => {
  20. if (code === 0) {
  21. return bootstrap(getLoadTestConfig('cookie'))
  22. .then(app => {
  23. return runLoadTestScript('deep-query.js')
  24. .then((summary1) => runLoadTestScript('search-and-checkout.js').then(summary2 => [summary1, summary2]))
  25. .then(summaries => {
  26. closeAndExit(app, summaries);
  27. });
  28. })
  29. .catch(err => {
  30. // tslint:disable-next-line
  31. console.log(err);
  32. });
  33. } else {
  34. process.exit(code || 1);
  35. }
  36. });
  37. }
  38. function runLoadTestScript(script: string): Promise<LoadTestSummary> {
  39. const rawResultsFile = `${script}.${count}.json`;
  40. return new Promise((resolve, reject) => {
  41. const loadTest = spawn('k6', ['run', `./scripts/${script}`, '--out', `json=results/${rawResultsFile}`], {
  42. cwd: __dirname,
  43. stdio: 'inherit',
  44. });
  45. loadTest.on('exit', code => {
  46. if (code === 0) {
  47. resolve(code);
  48. } else {
  49. reject();
  50. }
  51. });
  52. loadTest.on('error', err => {
  53. reject(err);
  54. });
  55. })
  56. .then(() => generateSummary(rawResultsFile));
  57. }
  58. async function closeAndExit(app: INestApplication, summaries: LoadTestSummary[]) {
  59. console.log('Closing server and preparing results...');
  60. // allow a pause for all queries to complete before closing the app
  61. await new Promise(resolve => setTimeout(resolve, 3000));
  62. await app.close();
  63. const dateString = getDateString();
  64. // write summary JSON
  65. const summaryData = summaries.map(s => omit(s, ['requestDurationTimeSeries', 'concurrentUsersTimeSeries']));
  66. const summaryFile = path.join(__dirname, `results/load-test-${dateString}-${count}.json`);
  67. fs.writeFileSync(summaryFile, JSON.stringify(summaryData, null, 2), 'utf-8');
  68. console.log(`Summary written to ${path.relative(__dirname, summaryFile)}`);
  69. // write time series CSV
  70. for (const summary of summaries) {
  71. const csvData = await getTimeSeriesCsvData(summary);
  72. const timeSeriesFile = path.join(__dirname, `results/load-test-${dateString}-${count}-${summary.script}.csv`);
  73. fs.writeFileSync(timeSeriesFile, csvData, 'utf-8');
  74. console.log(`Time series data written to ${path.relative(__dirname, timeSeriesFile)}`);
  75. }
  76. process.exit(0);
  77. }
  78. async function getTimeSeriesCsvData(summary: LoadTestSummary): Promise<string> {
  79. const stringifier = stringify({
  80. delimiter: ',',
  81. });
  82. const data: string[] = [];
  83. stringifier.on('readable', () => {
  84. let row;
  85. // tslint:disable-next-line:no-conditional-assignment
  86. while (row = stringifier.read()) {
  87. data.push(row);
  88. }
  89. });
  90. stringifier.write([
  91. `${summary.script}:elapsed`,
  92. `${summary.script}:request_duration`,
  93. `${summary.script}:user_count`,
  94. ]);
  95. let startTime: number | undefined;
  96. for (const row of summary.requestDurationTimeSeries) {
  97. if (!startTime) {
  98. startTime = row.timestamp;
  99. }
  100. stringifier.write([row.timestamp - startTime, row.value, '']);
  101. }
  102. for (const row of summary.concurrentUsersTimeSeries) {
  103. if (!startTime) {
  104. startTime = row.timestamp;
  105. }
  106. stringifier.write([row.timestamp - startTime, '', row.value]);
  107. }
  108. stringifier.end();
  109. return new Promise((resolve, reject) => {
  110. stringifier.on('error', (err: any) => {
  111. reject(err.message);
  112. });
  113. stringifier.on('finish', async () => {
  114. resolve(data.join(''));
  115. });
  116. });
  117. }
  118. function getDateString(): string {
  119. return (new Date().toISOString()).split('.')[0].replace(/[:\.]/g, '_');
  120. }