/* eslint-disable no-console */ import { INestApplication } from '@nestjs/common'; import { bootstrap, JobQueueService } from '@vendure/core'; import { spawn } from 'child_process'; import { stringify } from 'csv-stringify'; import fs from 'fs'; import path from 'path'; import { omit } from '../../common/src/omit'; import { generateSummary, LoadTestSummary } from './generate-summary'; import { getLoadTestConfig, getProductCount, getScriptToRun } from './load-test-config'; const count = getProductCount(); if (require.main === module) { const ALL_SCRIPTS = ['deep-query.js', 'search-and-checkout.js', 'very-large-order.js']; const scriptsToRun = getScriptToRun() || ALL_SCRIPTS; console.log(`\n============= Vendure Load Test: ${count} products ============\n`); // Runs the init script to generate test data and populate the test database const init = spawn('node', ['-r', 'ts-node/register', './init-load-test.ts', count.toString()], { cwd: __dirname, stdio: 'inherit', }); init.on('exit', async code => { if (code === 0) { const databaseName = `vendure-load-testing-${count}`; return bootstrap(getLoadTestConfig('cookie', databaseName)) .then(async app => { // await app.get(JobQueueService).start(); const summaries: LoadTestSummary[] = []; for (const script of scriptsToRun) { const summary = await runLoadTestScript(script); summaries.push(summary); } return closeAndExit(app, summaries); }) .catch(err => { // eslint-disable-next-line console.log(err); }); } else { process.exit(code || 1); } }); } async function runLoadTestScript(script: string): Promise { const rawResultsFile = `${script}.${count}.json`; return new Promise((resolve, reject) => { const loadTest = spawn( 'k6', ['run', `./scripts/${script}`, '--out', `json=results/${rawResultsFile}`], { cwd: __dirname, stdio: 'inherit', }, ); loadTest.on('exit', code => { if (code === 0) { resolve(code); } else { reject(); } }); loadTest.on('error', err => { reject(err); }); }).then(() => generateSummary(rawResultsFile)); } async function closeAndExit(app: INestApplication, summaries: LoadTestSummary[]) { console.log('Closing server and preparing results...'); // allow a pause for all queries to complete before closing the app await new Promise(resolve => setTimeout(resolve, 3000)); await app.close(); const dateString = getDateString(); // write summary JSON const summaryData = summaries.map(s => omit(s, ['requestDurationTimeSeries', 'concurrentUsersTimeSeries', 'requestCountTimeSeries']), ); const summaryFile = path.join(__dirname, `results/load-test-${dateString}-${count}.json`); fs.writeFileSync(summaryFile, JSON.stringify(summaryData, null, 2), 'utf-8'); console.log(`Summary written to ${path.relative(__dirname, summaryFile)}`); // write time series CSV for (const summary of summaries) { const csvData = await getTimeSeriesCsvData(summary); const timeSeriesFile = path.join( __dirname, `results/load-test-${dateString}-${count}-${summary.script}.csv`, ); fs.writeFileSync(timeSeriesFile, csvData, 'utf-8'); console.log(`Time series data written to ${path.relative(__dirname, timeSeriesFile)}`); } process.exit(0); } async function getTimeSeriesCsvData(summary: LoadTestSummary): Promise { const stringifier = stringify({ delimiter: ',', }); const data: string[] = []; stringifier.on('readable', () => { let row; // eslint-disable-next-line no-cond-assign while ((row = stringifier.read())) { data.push(row); } }); stringifier.write([ `${summary.script}:elapsed`, `${summary.script}:request_duration`, `${summary.script}:user_count`, `${summary.script}:reqs`, ]); let startTime: number | undefined; for (const row of summary.requestDurationTimeSeries) { if (!startTime) { startTime = row.timestamp; } stringifier.write([row.timestamp - startTime, row.value, '', '']); } for (const row of summary.concurrentUsersTimeSeries) { if (!startTime) { startTime = row.timestamp; } stringifier.write([row.timestamp - startTime, '', row.value, '']); } for (const row of summary.requestCountTimeSeries) { if (!startTime) { startTime = row.timestamp; } stringifier.write([row.timestamp - startTime, '', '', row.value]); } stringifier.end(); return new Promise((resolve, reject) => { stringifier.on('error', (err: any) => { reject(err.message); }); stringifier.on('finish', async () => { resolve(data.join('')); }); }); } function getDateString(): string { return new Date().toISOString().split('.')[0].replace(/[:\.]/g, '_'); }