run-load-test.ts 5.4 KB

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