run-load-test.ts 5.3 KB

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