|
|
@@ -0,0 +1,188 @@
|
|
|
+/* tslint:disable:no-console */
|
|
|
+import { INestApplication } from '@nestjs/common';
|
|
|
+import { GlobalFlag } from '@vendure/common/lib/generated-types';
|
|
|
+import {
|
|
|
+ bootstrap,
|
|
|
+ Importer,
|
|
|
+ isGraphQlErrorResult,
|
|
|
+ LanguageCode,
|
|
|
+ OrderService,
|
|
|
+ RequestContextService,
|
|
|
+} from '@vendure/core';
|
|
|
+import { populate } from '@vendure/core/cli/populate';
|
|
|
+import { ParsedProductWithVariants } from '@vendure/core/src/index';
|
|
|
+import { clearAllTables } from '@vendure/testing';
|
|
|
+import { spawn } from 'child_process';
|
|
|
+import program from 'commander';
|
|
|
+import path from 'path';
|
|
|
+import ProgressBar from 'progress';
|
|
|
+
|
|
|
+import { getLoadTestConfig } from './load-test-config';
|
|
|
+
|
|
|
+/**
|
|
|
+ * This set of benchmarks aims to specifically test the performance issues discussed
|
|
|
+ * in issue https://github.com/vendure-ecommerce/vendure/issues/1506.
|
|
|
+ *
|
|
|
+ * In order to test these issues, we need a test dataset that will create:
|
|
|
+ *
|
|
|
+ * 1. 1k Products, each with 10 ProductVariants
|
|
|
+ * 2. 10k Orders, each with 10 OrderLines, each OrderLine with qty 5
|
|
|
+ *
|
|
|
+ * Then we will test:
|
|
|
+ *
|
|
|
+ * 1. Fetching 10 Products from the `products` query. This will test the effects of indexes, ListQueryBuilder & dataloader
|
|
|
+ * 2. As above but with Orders
|
|
|
+ * 3. Fetching a list of orders and selecting only the Order id.
|
|
|
+ * This will test optimization of selecting & joining only the needed fields.
|
|
|
+ */
|
|
|
+
|
|
|
+const DATABASE_NAME = 'vendure-benchmarks';
|
|
|
+const PRODUCT_COUNT = 1000;
|
|
|
+const VARIANTS_PER_PRODUCT = 10;
|
|
|
+const ORDER_COUNT = 10000;
|
|
|
+const LINES_PER_ORDER = 10;
|
|
|
+const QUANTITY_PER_ORDER_LINE = 5;
|
|
|
+
|
|
|
+interface Options {
|
|
|
+ script?: string;
|
|
|
+ db: 'mysql' | 'postgres';
|
|
|
+ populate?: boolean;
|
|
|
+ variant?: string;
|
|
|
+}
|
|
|
+
|
|
|
+program
|
|
|
+ .option('--script <script>', `Specify the k6 script to run`)
|
|
|
+ .option('--db <db>', 'Select which database to test against', /^(mysql|postgres)$/, 'mysql')
|
|
|
+ .option('--populate', 'Whether to populate the database')
|
|
|
+ .option('--variant <variant>', 'Which variant of the given script')
|
|
|
+ .parse(process.argv);
|
|
|
+
|
|
|
+const opts = program.opts() as any;
|
|
|
+
|
|
|
+runBenchmark(opts).then(() => process.exit(0));
|
|
|
+
|
|
|
+async function runBenchmark(options: Options) {
|
|
|
+ const config = getLoadTestConfig('bearer', DATABASE_NAME);
|
|
|
+ if (options.populate) {
|
|
|
+ console.log(`Populating benchmark database "${DATABASE_NAME}"`);
|
|
|
+ await clearAllTables(config, true);
|
|
|
+ const populateApp = await populate(
|
|
|
+ () => bootstrap(config),
|
|
|
+ path.join(__dirname, '../../create/assets/initial-data.json'),
|
|
|
+ );
|
|
|
+ await createProducts(populateApp);
|
|
|
+ await createOrders(populateApp);
|
|
|
+ await populateApp.close();
|
|
|
+ } else {
|
|
|
+ const app = await bootstrap(config);
|
|
|
+ await new Promise((resolve, reject) => {
|
|
|
+ const runArgs: string[] = ['run', `./scripts/${options.script}`];
|
|
|
+ if (options.variant) {
|
|
|
+ console.log(`Using variant "${options.variant}"`);
|
|
|
+ runArgs.push('-e', `variant=${options.variant}`);
|
|
|
+ }
|
|
|
+ const loadTest = spawn('k6', runArgs, {
|
|
|
+ cwd: __dirname,
|
|
|
+ stdio: 'inherit',
|
|
|
+ });
|
|
|
+ loadTest.on('exit', code => {
|
|
|
+ if (code === 0) {
|
|
|
+ resolve(code);
|
|
|
+ } else {
|
|
|
+ reject();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ loadTest.on('error', err => {
|
|
|
+ reject(err);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ await app.close();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function createProducts(app: INestApplication) {
|
|
|
+ const importer = app.get(Importer);
|
|
|
+ const ctx = await app.get(RequestContextService).create({
|
|
|
+ apiType: 'admin',
|
|
|
+ });
|
|
|
+
|
|
|
+ const bar = new ProgressBar('creating products [:bar] (:current/:total) :percent :etas', {
|
|
|
+ complete: '=',
|
|
|
+ incomplete: ' ',
|
|
|
+ total: PRODUCT_COUNT,
|
|
|
+ width: 40,
|
|
|
+ });
|
|
|
+
|
|
|
+ const products: ParsedProductWithVariants[] = [];
|
|
|
+ for (let i = 0; i < PRODUCT_COUNT; i++) {
|
|
|
+ const product: ParsedProductWithVariants = {
|
|
|
+ product: {
|
|
|
+ optionGroups: [
|
|
|
+ {
|
|
|
+ translations: [
|
|
|
+ {
|
|
|
+ languageCode: LanguageCode.en,
|
|
|
+ name: `prod-${i}-option`,
|
|
|
+ values: Array.from({ length: VARIANTS_PER_PRODUCT }).map(
|
|
|
+ (_, opt) => `prod-${i}-option-${opt}`,
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ translations: [
|
|
|
+ {
|
|
|
+ languageCode: LanguageCode.en,
|
|
|
+ name: `Product ${i}`,
|
|
|
+ slug: `product-${i}`,
|
|
|
+ description: '',
|
|
|
+ customFields: {},
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ assetPaths: [],
|
|
|
+ facets: [],
|
|
|
+ },
|
|
|
+ variants: Array.from({ length: VARIANTS_PER_PRODUCT }).map((value, index) => ({
|
|
|
+ sku: `PROD-${i}-${index}`,
|
|
|
+ facets: [],
|
|
|
+ assetPaths: [],
|
|
|
+ price: 1000,
|
|
|
+ stockOnHand: 100,
|
|
|
+ taxCategory: 'standard',
|
|
|
+ trackInventory: GlobalFlag.INHERIT,
|
|
|
+ translations: [
|
|
|
+ {
|
|
|
+ languageCode: LanguageCode.en,
|
|
|
+ optionValues: [`prod-${i}-option-${index}`],
|
|
|
+ customFields: {},
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ })),
|
|
|
+ };
|
|
|
+ products.push(product);
|
|
|
+ }
|
|
|
+ await importer.importProducts(ctx, products, progess => bar.tick());
|
|
|
+}
|
|
|
+
|
|
|
+async function createOrders(app: INestApplication) {
|
|
|
+ const orderService = app.get(OrderService);
|
|
|
+ const bar = new ProgressBar('creating orders [:bar] (:current/:total) :percent :etas', {
|
|
|
+ complete: '=',
|
|
|
+ incomplete: ' ',
|
|
|
+ total: ORDER_COUNT,
|
|
|
+ width: 40,
|
|
|
+ });
|
|
|
+
|
|
|
+ for (let i = 0; i < ORDER_COUNT; i++) {
|
|
|
+ const ctx = await app.get(RequestContextService).create({
|
|
|
+ apiType: 'shop',
|
|
|
+ });
|
|
|
+ const order = await orderService.create(ctx);
|
|
|
+ const variantId = (i % (PRODUCT_COUNT * VARIANTS_PER_PRODUCT)) + 1;
|
|
|
+ const result = await orderService.addItemToOrder(ctx, order.id, variantId, QUANTITY_PER_ORDER_LINE);
|
|
|
+ if (isGraphQlErrorResult(result)) {
|
|
|
+ console.log(result);
|
|
|
+ }
|
|
|
+ bar.tick();
|
|
|
+ }
|
|
|
+}
|