Merge pull request #27624 from backstage/freben/perf-test

just some test tweaks to make it more robust
This commit is contained in:
Fredrik Adelöw
2024-11-14 15:08:07 +01:00
committed by GitHub
2 changed files with 243 additions and 89 deletions
@@ -576,6 +576,63 @@ describe('DefaultEntitiesCatalog', () => {
},
);
it.each(databases.eachSupportedId())(
'handles inversion both for existing and missing keys, %p',
async databaseId => {
await createDatabase(databaseId);
const entity1: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'n1' },
spec: { a: 'foo' },
};
const entity2: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'n2' },
spec: { a: 'bar', b: 'lonely' },
};
const entity3: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'n3' },
spec: { a: 'baz', b: 'only' },
};
await addEntityToSearch(entity1);
await addEntityToSearch(entity2);
await addEntityToSearch(entity3);
const catalog = new DefaultEntitiesCatalog({
database: knex,
logger: mockServices.logger.mock(),
stitcher,
});
function f(
request: Omit<EntitiesRequest, 'credentials'>,
): Promise<string[]> {
return catalog
.entities({ ...request, credentials: mockCredentials.none() })
.then(response =>
response.entities.map(e => e.metadata.name).toSorted(),
);
}
await expect(
f({
filter: { key: 'spec.b', values: ['lonely'] },
}),
).resolves.toEqual(['n2']);
await expect(
f({
filter: { not: { key: 'spec.b', values: ['lonely'] } },
}),
).resolves.toEqual(['n1', 'n3']);
},
);
it.each(databases.eachSupportedId())(
'can order and combine with filtering, %p',
async databaseId => {
@@ -1825,6 +1882,21 @@ describe('DefaultEntitiesCatalog', () => {
},
});
await expect(
catalog.facets({
facets: ['kind'],
filter: { key: 'metadata.name', values: ['two'] },
credentials: mockCredentials.none(),
}),
).resolves.toEqual({
facets: {
kind: [
{ value: 'k', count: 1 },
{ value: 'k2', count: 1 },
],
},
});
await expect(
catalog.facets({
facets: ['kind'],
@@ -20,9 +20,14 @@ import {
mockServices,
startTestBackend,
} from '@backstage/backend-test-utils';
import { CatalogClient } from '@backstage/catalog-client';
import {
CatalogApi,
CatalogClient,
GetEntitiesResponse,
} from '@backstage/catalog-client';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { Knex } from 'knex';
import { default as catalogPlugin } from '../..';
import { applyDatabaseMigrations } from '../../database/migrations';
import {
SyntheticLoadEntitiesProcessor,
@@ -31,112 +36,189 @@ import {
} from './lib/catalogModuleSyntheticLoadEntities';
import { describePerformanceTest, performanceTraceEnabled } from './lib/env';
// #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
: () => {};
const completionPolling = async (load: SyntheticLoadOptions, knex: Knex) => {
const { baseEntitiesCount, childrenCount } = load;
const expectedTotal = baseEntitiesCount + baseEntitiesCount * childrenCount;
async function createBackend(
knex: Knex,
load: SyntheticLoadOptions,
): Promise<{
client: CatalogApi;
numberOfEntities: number;
stop: () => Promise<void>;
}> {
await applyDatabaseMigrations(knex);
return new Promise<void>((resolve, reject) => {
const interval = setInterval(async () => {
try {
const stitchedCount = await knex('final_entities')
.count({ count: '*' })
.whereNotNull('final_entity')
.then(rows => Number(rows[0].count));
const numberOfEntities =
load.baseEntitiesCount + load.baseEntitiesCount * load.childrenCount;
if (stitchedCount === expectedTotal) {
clearInterval(interval);
resolve();
}
} catch (error) {
clearInterval(interval);
reject(error);
}
}, 1000);
});
};
traceLog(`Creating test backend with ${numberOfEntities} entities`);
describePerformanceTest('getEntitiesPerformanceTest', () => {
const databases = TestDatabases.create({
ids: [/* 'MYSQL_8', */ 'POSTGRES_16', /* 'POSTGRES_12',*/ 'SQLITE_3'],
disableDocker: false,
const backend = await startTestBackend({
features: [
catalogPlugin,
mockServices.database.factory({ knex }),
createBackendModule({
pluginId: 'catalog',
moduleId: 'synthetic-load-entities',
register(reg) {
reg.registerInit({
deps: {
catalog: catalogProcessingExtensionPoint,
},
async init({ catalog }) {
catalog.addEntityProvider(
new SyntheticLoadEntitiesProvider(load, {}),
);
catalog.addProcessor(new SyntheticLoadEntitiesProcessor(load));
},
});
},
}),
],
});
it.each(databases.eachSupportedId())(
'fetch entities, %p',
async databaseId => {
const knex = await databases.init(databaseId);
await applyDatabaseMigrations(knex);
while (
(await knex('final_entities')
.count({ count: '*' })
.whereNotNull('final_entity')
.then(rows => Number(rows[0].count))) !== numberOfEntities
) {
await new Promise(resolve => setTimeout(resolve, 200));
}
const load: SyntheticLoadOptions = {
const client = new CatalogClient({
discoveryApi: {
getBaseUrl: jest
.fn()
.mockResolvedValue(
`http://localhost:${backend.server.port()}/api/catalog`,
),
},
});
return {
client,
numberOfEntities,
stop: backend.stop.bind(backend),
};
}
// #endregion
// #region Tests
describePerformanceTest('getEntities', () => {
let knex: Knex;
let backend: Awaited<ReturnType<typeof createBackend>>;
describe.each(databases.eachSupportedId())('burst reads, %p', databaseId => {
beforeAll(async () => {
knex = await databases.init(databaseId);
backend = await createBackend(knex, {
baseEntitiesCount: 2000,
baseRelationsCount: 3,
baseRelationsSkew: 0.3,
childrenCount: 3,
};
traceLog('Starting test backend');
const backend = await startTestBackend({
features: [
import('@backstage/plugin-catalog-backend/alpha'),
mockServices.database.factory({ knex }),
createBackendModule({
pluginId: 'catalog',
moduleId: 'synthetic-load-entities',
register(reg) {
reg.registerInit({
deps: {
catalog: catalogProcessingExtensionPoint,
},
async init({ catalog }) {
catalog.addEntityProvider(
new SyntheticLoadEntitiesProvider(load, {}),
);
catalog.addProcessor(
new SyntheticLoadEntitiesProcessor(load),
);
},
});
},
}),
],
});
});
const expectedTotal =
load.baseEntitiesCount + load.baseEntitiesCount * load.childrenCount;
traceLog(`Waiting for completion polling of ${expectedTotal} entities`);
await expect(completionPolling(load, knex)).resolves.toBeUndefined();
const client = new CatalogClient({
discoveryApi: {
getBaseUrl: jest
.fn()
.mockResolvedValue(
`http://localhost:${backend.server.port()}/api/catalog`,
),
},
});
const start = Date.now();
traceLog(`[${databaseId}] Starting to fetch ${expectedTotal} entities`);
// Fetch all entities
const response = await client.getEntities();
expect(response.items).toHaveLength(expectedTotal);
const fetchDuration = Date.now() - start;
traceLog(
`[${databaseId}] Fetched ${expectedTotal} entities in ${fetchDuration}ms`,
);
afterAll(async () => {
await backend.stop();
await knex.destroy();
},
);
});
it('does a large burst read', async () => {
let response;
for (let i = 0; i < 10; ++i) {
response = await backend.client.getEntities();
}
expect(response!.items).toHaveLength(backend.numberOfEntities);
});
});
describe.each(databases.eachSupportedId())('filtering, %p', databaseId => {
beforeAll(async () => {
knex = await databases.init(databaseId);
backend = await createBackend(knex, {
baseEntitiesCount: 20000,
baseRelationsCount: 0,
baseRelationsSkew: 0,
childrenCount: 0,
});
});
afterAll(async () => {
await backend.stop();
await knex.destroy();
});
it('single matching filter, all fields', async () => {
let response: GetEntitiesResponse;
for (let i = 0; i < 20; ++i) {
response = await backend.client.getEntities({
filter: { kind: 'Location' },
});
}
expect(response!.items).toHaveLength(backend.numberOfEntities);
});
it('single matching filter, one field', async () => {
let response: GetEntitiesResponse;
for (let i = 0; i < 20; ++i) {
response = await backend.client.getEntities({
filter: { kind: 'Location' },
fields: ['kind'],
});
}
expect(response!.items).toHaveLength(backend.numberOfEntities);
});
it('single non-matching filter', async () => {
let response: GetEntitiesResponse;
for (let i = 0; i < 20; ++i) {
response = await backend.client.getEntities({
filter: { kind: 'NotALocation' },
});
}
expect(response!.items).toHaveLength(0);
});
it('complex filter, all fields', async () => {
let response: GetEntitiesResponse;
for (let i = 0; i < 20; ++i) {
response = await backend.client.getEntities({
filter: [
{
kind: 'Location',
},
{
kind: 'Location',
'metadata.name': new Array(100)
.fill(0)
.map((_, j) => `synthetic-${j}`),
},
...new Array(10).fill(0).map((_, j) => ({
kind: 'NotALocation',
'metadata.name': new Array(10)
.fill(0)
.map((__, k) => `no-match-${i}-${j}-${k}`),
})),
],
});
}
expect(response!.items).toHaveLength(backend.numberOfEntities);
});
});
});
// #endregion