From 23be284e5b0e9ab9cfa5877ef745b624dfa9d2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 19 Nov 2024 20:58:16 +0100 Subject: [PATCH] add performance test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../src/database/DefaultProviderDatabase.ts | 6 +- .../refreshState/checkLocationKeyConflict.ts | 2 +- .../refreshState/insertUnprocessedEntity.ts | 2 +- .../refreshState/updateUnprocessedEntity.ts | 2 +- .../providerDeltaMutations.test.ts | 115 ++++++++++++++++++ 5 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 plugins/catalog-backend/src/tests/performance/providerDeltaMutations.test.ts diff --git a/plugins/catalog-backend/src/database/DefaultProviderDatabase.ts b/plugins/catalog-backend/src/database/DefaultProviderDatabase.ts index 9d457a45fd..bbb24560be 100644 --- a/plugins/catalog-backend/src/database/DefaultProviderDatabase.ts +++ b/plugins/catalog-backend/src/database/DefaultProviderDatabase.ts @@ -75,10 +75,10 @@ export class DefaultProviderDatabase implements ProviderDatabase { } async replaceUnprocessedEntities( - txOpaque: Transaction, + txOpaque: Knex | Transaction, options: ReplaceUnprocessedEntitiesOptions, ): Promise { - const tx = txOpaque as Knex.Transaction; + const tx = txOpaque as Knex | Knex.Transaction; const { toAdd, toUpsert, toRemove } = await this.createDelta(tx, options); if (toRemove.length) { @@ -205,7 +205,7 @@ export class DefaultProviderDatabase implements ProviderDatabase { } private async createDelta( - tx: Knex.Transaction, + tx: Knex | Knex.Transaction, options: ReplaceUnprocessedEntitiesOptions, ): Promise<{ toAdd: { deferred: DeferredEntity; hash: string }[]; diff --git a/plugins/catalog-backend/src/database/operations/refreshState/checkLocationKeyConflict.ts b/plugins/catalog-backend/src/database/operations/refreshState/checkLocationKeyConflict.ts index 97d376f9a4..cf6c920d1d 100644 --- a/plugins/catalog-backend/src/database/operations/refreshState/checkLocationKeyConflict.ts +++ b/plugins/catalog-backend/src/database/operations/refreshState/checkLocationKeyConflict.ts @@ -24,7 +24,7 @@ import { DbRefreshStateRow } from '../../tables'; * @returns The conflicting key if there is one. */ export async function checkLocationKeyConflict(options: { - tx: Knex.Transaction; + tx: Knex | Knex.Transaction; entityRef: string; locationKey?: string; }): Promise { diff --git a/plugins/catalog-backend/src/database/operations/refreshState/insertUnprocessedEntity.ts b/plugins/catalog-backend/src/database/operations/refreshState/insertUnprocessedEntity.ts index 195dbe1f2c..54067c3f2e 100644 --- a/plugins/catalog-backend/src/database/operations/refreshState/insertUnprocessedEntity.ts +++ b/plugins/catalog-backend/src/database/operations/refreshState/insertUnprocessedEntity.ts @@ -28,7 +28,7 @@ import { * true if successful and false if there was a conflict. */ export async function insertUnprocessedEntity(options: { - tx: Knex.Transaction; + tx: Knex | Knex.Transaction; entity: Entity; hash: string; locationKey?: string; diff --git a/plugins/catalog-backend/src/database/operations/refreshState/updateUnprocessedEntity.ts b/plugins/catalog-backend/src/database/operations/refreshState/updateUnprocessedEntity.ts index e26db4429b..ced6b0d54d 100644 --- a/plugins/catalog-backend/src/database/operations/refreshState/updateUnprocessedEntity.ts +++ b/plugins/catalog-backend/src/database/operations/refreshState/updateUnprocessedEntity.ts @@ -25,7 +25,7 @@ import { DbRefreshStateRow } from '../../tables'; * Updating the entity will also cause it to be scheduled for immediate processing. */ export async function updateUnprocessedEntity(options: { - tx: Knex.Transaction; + tx: Knex | Knex.Transaction; entity: Entity; hash: string; locationKey?: string; diff --git a/plugins/catalog-backend/src/tests/performance/providerDeltaMutations.test.ts b/plugins/catalog-backend/src/tests/performance/providerDeltaMutations.test.ts new file mode 100644 index 0000000000..6e95c2535a --- /dev/null +++ b/plugins/catalog-backend/src/tests/performance/providerDeltaMutations.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestDatabases, mockServices } from '@backstage/backend-test-utils'; +import { Knex } from 'knex'; +import { applyDatabaseMigrations } from '../../database/migrations'; +import { describePerformanceTest, performanceTraceEnabled } from './lib/env'; +import { DefaultProviderDatabase } from '../../database/DefaultProviderDatabase'; +import { DeferredEntity } from '@backstage/plugin-catalog-node'; + +// #region Helpers + +jest.setTimeout(600_000); + +const databases = TestDatabases.create({ + ids: [/* 'MYSQL_8', */ 'POSTGRES_16', /* 'POSTGRES_12',*/ 'SQLITE_3'], + disableDocker: false, +}); + +const traceLog: typeof console.log = performanceTraceEnabled + ? console.log + : () => {}; + +// #endregion +// #region Tests + +describePerformanceTest('providerDeltaMutations', () => { + let knex: Knex; + + describe.each(databases.eachSupportedId())('%p', databaseId => { + beforeAll(async () => { + knex = await databases.init(databaseId); + await applyDatabaseMigrations(knex); + }); + + afterAll(async () => { + await knex.destroy(); + }); + + it.each([200, 50_000])( + 'inserts and then overwrites identical sets using delta mutations, batch size %p', + async batchSize => { + const sut = new DefaultProviderDatabase({ + database: knex, + logger: mockServices.logger.mock(), + }); + + const sourceKey = 'source-key'; + const entities: DeferredEntity[] = Array.from( + { length: batchSize }, + (_, i) => ({ + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: `component-${i}`, + namespace: 'default', + }, + spec: { + type: 'service', + lifecycle: 'production', + }, + }, + }), + ); + + let start = Date.now(); + await sut.replaceUnprocessedEntities(knex, { + type: 'delta', + sourceKey, + added: entities, + removed: [], + }); + let perSecond = Math.round(batchSize / ((Date.now() - start) / 1000)); + traceLog( + `${databaseId} inserted ${perSecond} entities per second at a batch size of ${batchSize}`, + ); + + start = Date.now(); + const rounds = 20; + for (let i = 0; i < rounds; i++) { + await sut.replaceUnprocessedEntities(knex, { + type: 'delta', + sourceKey, + added: entities, + removed: [], + }); + } + perSecond = Math.round( + (batchSize * rounds) / ((Date.now() - start) / 1000), + ); + traceLog( + `${databaseId} updated ${perSecond} entities per second at a batch size of ${batchSize}`, + ); + + expect(true).toBe(true); + }, + ); + }); +}); + +// #endregion