From 08a58133580888915800bd87863b6831c754fe81 Mon Sep 17 00:00:00 2001 From: Pedro Nastasi Date: Tue, 10 Feb 2026 14:16:31 +0000 Subject: [PATCH 1/6] add patch changeset description Signed-off-by: Pedro Nastasi --- .changeset/spicy-coins-switch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spicy-coins-switch.md diff --git a/.changeset/spicy-coins-switch.md b/.changeset/spicy-coins-switch.md new file mode 100644 index 0000000000..744d9d9640 --- /dev/null +++ b/.changeset/spicy-coins-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed O(n²) performance bottleneck in `buildEntitySearch` `traverse()` by replacing `Array.some()` linear scan with a `Set` for O(1) duplicate path key detection. From 013d19b30fd482f3d72cfe58a9574e3cd9513497 Mon Sep 17 00:00:00 2001 From: Pedro Nastasi Date: Tue, 10 Feb 2026 14:18:08 +0000 Subject: [PATCH 2/6] improve duplicate detection in traverse function Signed-off-by: Pedro Nastasi --- .../operations/stitcher/buildEntitySearch.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts index bee0426a99..c8e09f3347 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts @@ -71,6 +71,11 @@ type Kv = { // "h.j": "l" export function traverse(root: unknown): Kv[] { const output: Kv[] = []; + // Use a Set for O(1) case-insensitive duplicate detection of synthetic + // boolean path keys (e.g. "metadata.tags.java"), instead of the previous + // O(n) Array.some() linear scan which caused O(n²) overall complexity + // and severe event loop blocking for entities with large arrays. + const seenPathKeys = new Set(); function visit(path: string, current: unknown) { if (SPECIAL_KEYS.includes(path)) { @@ -111,13 +116,9 @@ export function traverse(root: unknown): Kv[] { visit(path, item); if (typeof item === 'string') { const pathKey = `${path}.${item}`; - if ( - !output.some( - kv => - kv.key.toLocaleLowerCase('en-US') === - pathKey.toLocaleLowerCase('en-US'), - ) - ) { + const lowerKey = pathKey.toLocaleLowerCase('en-US'); + if (!seenPathKeys.has(lowerKey)) { + seenPathKeys.add(lowerKey); output.push({ key: pathKey, value: true }); } } From 3bc710c6c92d32d168f4133f182a920e6729df5b Mon Sep 17 00:00:00 2001 From: Pedro Nastasi Date: Tue, 10 Feb 2026 14:34:42 +0000 Subject: [PATCH 3/6] add performance test for large arrays in traverse Signed-off-by: Pedro Nastasi --- .../stitcher/buildEntitySearch.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts index 4241599351..622b562320 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts @@ -50,6 +50,23 @@ describe('buildEntitySearch', () => { ]); }); + it('handles large arrays without quadratic performance degradation', () => { + // Generate an array with 500 unique string items to verify that the + // Set-based dedup scales linearly rather than quadratically. + // With the previous Array.some() approach this would cause ~125,000 + // comparisons; with Set.has() it's ~500 lookups. + const items = Array.from({ length: 500 }, (_, i) => `tag-${i}`); + const input = { tags: items }; + + const start = window.performance.now(); + const output = traverse(input); + const elapsed = window.performance.now() - start; + + expect(output).toHaveLength(1000); + // Should complete well under 100ms with O(n); O(n²) would be noticeably slower + expect(elapsed).toBeLessThan(100); + }); + it('skips over special keys', () => { const input = { status: { x: 1 }, From 6625c04857199d7c55b2c466297fa494b12b7775 Mon Sep 17 00:00:00 2001 From: Pedro Nastasi Date: Tue, 10 Feb 2026 15:10:50 +0000 Subject: [PATCH 4/6] improve performance measurement in traverse test Signed-off-by: Pedro Nastasi --- .../database/operations/stitcher/buildEntitySearch.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts index 622b562320..032e7854f7 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts @@ -58,9 +58,9 @@ describe('buildEntitySearch', () => { const items = Array.from({ length: 500 }, (_, i) => `tag-${i}`); const input = { tags: items }; - const start = window.performance.now(); + const start = Date.now(); const output = traverse(input); - const elapsed = window.performance.now() - start; + const elapsed = Date.now() - start; expect(output).toHaveLength(1000); // Should complete well under 100ms with O(n); O(n²) would be noticeably slower From 7c73e3ffb7b3a09a2a8ec9c0c5f15012cb8dca2b Mon Sep 17 00:00:00 2001 From: Pedro Nastasi Date: Tue, 10 Feb 2026 15:13:34 +0000 Subject: [PATCH 5/6] improve performance measurement in large array test Signed-off-by: Pedro Nastasi --- .../database/operations/stitcher/buildEntitySearch.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts index 032e7854f7..11fc69aaa1 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts @@ -15,6 +15,7 @@ */ import { DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model'; +import { performance } from 'node:perf_hooks'; import { buildEntitySearch, mapToRows, traverse } from './buildEntitySearch'; describe('buildEntitySearch', () => { @@ -58,10 +59,11 @@ describe('buildEntitySearch', () => { const items = Array.from({ length: 500 }, (_, i) => `tag-${i}`); const input = { tags: items }; - const start = Date.now(); + const start = performance.now(); const output = traverse(input); - const elapsed = Date.now() - start; + const elapsed = performance.now() - start; + // Each item produces 2 entries: { key: 'tags', value: 'tag-N' } and { key: 'tags.tag-N', value: true } expect(output).toHaveLength(1000); // Should complete well under 100ms with O(n); O(n²) would be noticeably slower expect(elapsed).toBeLessThan(100); From 74f433267ede6bce63204432e7aa5b85f748fa8d Mon Sep 17 00:00:00 2001 From: Pedro Nastasi Date: Tue, 10 Feb 2026 15:19:14 +0000 Subject: [PATCH 6/6] remove redundant comment in performance test Signed-off-by: Pedro Nastasi --- .../src/database/operations/stitcher/buildEntitySearch.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts index 11fc69aaa1..5a9e634e35 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts @@ -63,7 +63,6 @@ describe('buildEntitySearch', () => { const output = traverse(input); const elapsed = performance.now() - start; - // Each item produces 2 entries: { key: 'tags', value: 'tag-N' } and { key: 'tags.tag-N', value: true } expect(output).toHaveLength(1000); // Should complete well under 100ms with O(n); O(n²) would be noticeably slower expect(elapsed).toBeLessThan(100);