run-load-test.ts 5.2 KB

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