Quellcode durchsuchen

chore: Add simple query load test, enable pg stats

Michael Bromley vor 4 Jahren
Ursprung
Commit
aed08dd414

+ 19 - 0
packages/dev-server/README.md

@@ -46,6 +46,25 @@ An individual test script may be by specifying the script name as an argument:
 yarn load-test:1k deep-query.js
 ```
 
+## pg_stat_statements
+
+The following queries can be used when running load tests against postgres to analyze the queries:
+
+```sql
+SELECT 
+  dbid,
+  (total_time / 1000 / 60) as total, 
+  (total_time/calls) as avg, 
+  calls,
+  query 
+FROM pg_stat_statements 
+WHERE dbid = <db_id>
+ORDER BY total DESC 
+LIMIT 100;
+
+-- SELECT pg_stat_statements_reset();
+```
+
 ### Results
 
 The results of the test are saved to the [`./load-testing/results`](./load-testing/results) directory. Each test run creates two files:

+ 1 - 0
packages/dev-server/docker-compose.yml

@@ -70,6 +70,7 @@ services:
       - postgres_data:/var/lib/postgresql/data
     ports:
       - "5432:5432"
+    command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c pg_stat_statements.max=100000 -c max_connections=200
   pgadmin:
     container_name: pgadmin
     image: dpage/pgadmin4:4.18

+ 1 - 0
packages/dev-server/load-testing/graphql/shop/deep-query.graphql

@@ -101,6 +101,7 @@
                 id
                 name
             }
+            stockLevel
             taxRateApplied {
                 category {
                     id

+ 8 - 0
packages/dev-server/load-testing/graphql/shop/simple-product-query.graphql

@@ -0,0 +1,8 @@
+{
+    product(id: "1") {
+        id
+        name
+        slug
+        description
+    }
+}

+ 57 - 26
packages/dev-server/load-testing/init-load-test.ts

@@ -1,6 +1,6 @@
 // tslint:disable-next-line:no-reference
 /// <reference path="../../core/typings.d.ts" />
-import { bootstrap } from '@vendure/core';
+import { bootstrap, JobQueueService } from '@vendure/core';
 import { populate } from '@vendure/core/cli/populate';
 import { BaseProductRecord } from '@vendure/core/dist/data-import/providers/import-parser/import-parser';
 import { clearAllTables, populateCustomers } from '@vendure/testing';
@@ -13,6 +13,7 @@ import { initialData } from '../../core/mock-data/data-sources/initial-data';
 import {
     getLoadTestConfig,
     getMysqlConnectionOptions,
+    getPostgresConnectionOptions,
     getProductCount,
     getProductCsvFilePath,
 } from './load-test-config';
@@ -26,6 +27,7 @@ if (require.main === module) {
     // Running from command line
     isDatabasePopulated()
         .then(isPopulated => {
+            console.log(`isPopulated:`, isPopulated);
             if (!isPopulated) {
                 const count = getProductCount();
                 const config = getLoadTestConfig('bearer');
@@ -38,7 +40,11 @@ if (require.main === module) {
                     })
                     .then(() =>
                         populate(
-                            () => bootstrap(config),
+                            () =>
+                                bootstrap(config).then(async app => {
+                                    await app.get(JobQueueService).start();
+                                    return app;
+                                }),
                             path.join(__dirname, '../../create/assets/initial-data.json'),
                             csvFile,
                         ),
@@ -64,37 +70,62 @@ if (require.main === module) {
 /**
  * Tests to see whether the load test database is already populated.
  */
-function isDatabasePopulated(): Promise<boolean> {
-    const mysql = require('mysql');
+async function isDatabasePopulated(): Promise<boolean> {
+    const isPostgres = process.env.DB === 'postgres';
     const count = getProductCount();
-    const mysqlConnectionOptions = getMysqlConnectionOptions(count);
-    const connection = mysql.createConnection({
-        host: mysqlConnectionOptions.host,
-        user: mysqlConnectionOptions.username,
-        password: mysqlConnectionOptions.password,
-        database: mysqlConnectionOptions.database,
-    });
-
-    return new Promise<boolean>((resolve, reject) => {
-        connection.connect((error: any) => {
-            if (error) {
-                reject(error);
-                return;
+    if (isPostgres) {
+        console.log(`Checking whether data is populated (postgres)`);
+        const pg = require('pg');
+        const postgresConnectionOptions = getPostgresConnectionOptions(count);
+        const client = new pg.Client({
+            host: postgresConnectionOptions.host,
+            user: postgresConnectionOptions.username,
+            database: postgresConnectionOptions.database,
+            password: postgresConnectionOptions.password,
+            port: postgresConnectionOptions.port,
+        });
+        await client.connect();
+        try {
+            const res = await client.query('SELECT COUNT(id) as prodCount FROM product');
+            return true;
+        } catch (e) {
+            if (e.message === `relation "product" does not exist`) {
+                return false;
             }
+            throw e;
+        }
+    } else {
+        const mysql = require('mysql');
 
-            connection.query('SELECT COUNT(id) as prodCount FROM product', (err: any, results: any) => {
-                if (err) {
-                    if (err.code === 'ER_NO_SUCH_TABLE') {
-                        resolve(false);
-                        return;
-                    }
-                    reject(err);
+        const mysqlConnectionOptions = getMysqlConnectionOptions(count);
+        const connection = mysql.createConnection({
+            host: mysqlConnectionOptions.host,
+            user: mysqlConnectionOptions.username,
+            password: mysqlConnectionOptions.password,
+            database: mysqlConnectionOptions.database,
+        });
+
+        return new Promise<boolean>((resolve, reject) => {
+            connection.connect((error: any) => {
+                if (error) {
+                    reject(error);
                     return;
                 }
-                resolve(results[0].prodCount === count);
+
+                connection.query('SELECT COUNT(id) as prodCount FROM product', (err: any, results: any) => {
+                    if (err) {
+                        if (err.code === 'ER_NO_SUCH_TABLE') {
+                            resolve(false);
+                            return;
+                        }
+                        reject(err);
+                        return;
+                    }
+                    resolve(results[0].prodCount === count);
+                });
             });
         });
-    });
+    }
 }
 
 /**

+ 4 - 1
packages/dev-server/load-testing/load-test-config.ts

@@ -47,7 +47,10 @@ export function getLoadTestConfig(tokenMethod: 'cookie' | 'bearer'): Required<Ve
             orderItemsLimit: 99999,
         },
         logger: new DefaultLogger({ level: LogLevel.Info }),
-        dbConnectionOptions: getMysqlConnectionOptions(count),
+        dbConnectionOptions:
+            process.env.DB === 'postgres'
+                ? getPostgresConnectionOptions(count)
+                : getMysqlConnectionOptions(count),
         authOptions: {
             tokenMethod,
             requireVerification: false,

+ 1 - 0
packages/dev-server/load-testing/run-load-test.ts

@@ -30,6 +30,7 @@ if (require.main === module) {
         if (code === 0) {
             return bootstrap(getLoadTestConfig('cookie'))
                 .then(async app => {
+                    // await app.get(JobQueueService).start();
                     const summaries: LoadTestSummary[] = [];
                     for (const script of scriptsToRun) {
                         const summary = await runLoadTestScript(script);

+ 1 - 1
packages/dev-server/load-testing/scripts/search-and-checkout.js

@@ -11,7 +11,7 @@ const getShippingMethodsQuery = new ShopApiRequest('shop/get-shipping-methods.gr
 const completeOrderMutation = new ShopApiRequest('shop/complete-order.graphql');
 
 export let options = {
-    stages: [{ duration: '4m', target: 500 }],
+    stages: [{ duration: '4m', target: 50 }],
 };
 
 /**

+ 14 - 0
packages/dev-server/load-testing/scripts/simple-query.js

@@ -0,0 +1,14 @@
+import { ShopApiRequest } from '../utils/api-request.js';
+
+const simpleQuery = new ShopApiRequest('shop/simple-product-query.graphql');
+
+export let options = {
+    stages: [{ duration: '1m', target: 1 }],
+};
+
+/**
+ * Performs a simple query to measure baseline request throughput
+ */
+export default function () {
+    simpleQuery.post();
+}