Skip to content

Commit 20bd285

Browse files
authored
Merge pull request backstage#32786 from pedronastasi/fix-O-n2-complexity
Fix O(n²) performance bottleneck in buildEntitySearch traverse()
2 parents d0786b9 + 74f4332 commit 20bd285

3 files changed

Lines changed: 31 additions & 7 deletions

File tree

.changeset/spicy-coins-switch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@backstage/plugin-catalog-backend': patch
3+
---
4+
5+
Fixed O(n²) performance bottleneck in `buildEntitySearch` `traverse()` by replacing `Array.some()` linear scan with a `Set` for O(1) duplicate path key detection.

plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import { DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model';
18+
import { performance } from 'node:perf_hooks';
1819
import { buildEntitySearch, mapToRows, traverse } from './buildEntitySearch';
1920

2021
describe('buildEntitySearch', () => {
@@ -50,6 +51,23 @@ describe('buildEntitySearch', () => {
5051
]);
5152
});
5253

54+
it('handles large arrays without quadratic performance degradation', () => {
55+
// Generate an array with 500 unique string items to verify that the
56+
// Set-based dedup scales linearly rather than quadratically.
57+
// With the previous Array.some() approach this would cause ~125,000
58+
// comparisons; with Set.has() it's ~500 lookups.
59+
const items = Array.from({ length: 500 }, (_, i) => `tag-${i}`);
60+
const input = { tags: items };
61+
62+
const start = performance.now();
63+
const output = traverse(input);
64+
const elapsed = performance.now() - start;
65+
66+
expect(output).toHaveLength(1000);
67+
// Should complete well under 100ms with O(n); O(n²) would be noticeably slower
68+
expect(elapsed).toBeLessThan(100);
69+
});
70+
5371
it('skips over special keys', () => {
5472
const input = {
5573
status: { x: 1 },

plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ type Kv = {
7171
// "h.j": "l"
7272
export function traverse(root: unknown): Kv[] {
7373
const output: Kv[] = [];
74+
// Use a Set for O(1) case-insensitive duplicate detection of synthetic
75+
// boolean path keys (e.g. "metadata.tags.java"), instead of the previous
76+
// O(n) Array.some() linear scan which caused O(n²) overall complexity
77+
// and severe event loop blocking for entities with large arrays.
78+
const seenPathKeys = new Set<string>();
7479

7580
function visit(path: string, current: unknown) {
7681
if (SPECIAL_KEYS.includes(path)) {
@@ -111,13 +116,9 @@ export function traverse(root: unknown): Kv[] {
111116
visit(path, item);
112117
if (typeof item === 'string') {
113118
const pathKey = `${path}.${item}`;
114-
if (
115-
!output.some(
116-
kv =>
117-
kv.key.toLocaleLowerCase('en-US') ===
118-
pathKey.toLocaleLowerCase('en-US'),
119-
)
120-
) {
119+
const lowerKey = pathKey.toLocaleLowerCase('en-US');
120+
if (!seenPathKeys.has(lowerKey)) {
121+
seenPathKeys.add(lowerKey);
121122
output.push({ key: pathKey, value: true });
122123
}
123124
}

0 commit comments

Comments
 (0)