diff --git a/packages/desktop-client/src/components/settings/Experimental.tsx b/packages/desktop-client/src/components/settings/Experimental.tsx index 41fe8d6e95dde871f95a812883397ca59cd58220..4b809341ff584f8677efb83055f4263e3f9914c0 100644 --- a/packages/desktop-client/src/components/settings/Experimental.tsx +++ b/packages/desktop-client/src/components/settings/Experimental.tsx @@ -90,6 +90,9 @@ export function ExperimentalFeatures() { Goal templates </FeatureToggle> <FeatureToggle flag="simpleFinSync">SimpleFIN sync</FeatureToggle> + <FeatureToggle flag="iterableTopologicalSort"> + Iterable topological sort budget + </FeatureToggle> </View> ) : ( <Link diff --git a/packages/desktop-client/src/hooks/useFeatureFlag.ts b/packages/desktop-client/src/hooks/useFeatureFlag.ts index 010b172ec0aec5727aaabd3452d4e23de1f8f9c0..0089686be590106432082c79a12aa960267d58fe 100644 --- a/packages/desktop-client/src/hooks/useFeatureFlag.ts +++ b/packages/desktop-client/src/hooks/useFeatureFlag.ts @@ -9,6 +9,7 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = { customReports: false, spendingReport: false, simpleFinSync: false, + iterableTopologicalSort: true, }; export function useFeatureFlag(name: FeatureFlag): boolean { diff --git a/packages/loot-core/src/server/spreadsheet/graph-data-structure.ts b/packages/loot-core/src/server/spreadsheet/graph-data-structure.ts index 76fe24762156fe7ddd228411d77e6376ddd33671..be2669d1ff2d82daea376d2d63433877503329ba 100644 --- a/packages/loot-core/src/server/spreadsheet/graph-data-structure.ts +++ b/packages/loot-core/src/server/spreadsheet/graph-data-structure.ts @@ -1,3 +1,5 @@ +import { getPrefs } from '../prefs'; + // @ts-strict-ignore export function Graph() { const graph = { @@ -76,14 +78,38 @@ export function Graph() { return graph; } - function topologicalSortUntil(name, visited, sorted) { + function topologicalSort(sourceNodes) { + const visited = new Set(); + const sorted = []; + const prefs = getPrefs(); + const iterableTopologicalSort = + prefs != null ? prefs['flags.iterableTopologicalSort'] : false; + + sourceNodes.forEach(name => { + if (!visited.has(name)) { + if (iterableTopologicalSort) { + topologicalSortIterable(name, visited, sorted); + } else { + topologicalSortUntil(name, visited, sorted, 0); + } + } + }); + + return sorted; + } + + function topologicalSortUntil(name, visited, sorted, level) { visited.add(name); + if (level > 2500) { + console.error('Limit of recursions reached while sorting budget: 2500'); + return; + } const iter = adjacent(name).values(); let cur = iter.next(); while (!cur.done) { if (!visited.has(cur.value)) { - topologicalSortUntil(cur.value, visited, sorted); + topologicalSortUntil(cur.value, visited, sorted, level + 1); } cur = iter.next(); } @@ -91,17 +117,54 @@ export function Graph() { sorted.unshift(name); } - function topologicalSort(sourceNodes) { - const visited = new Set(); - const sorted = []; + function topologicalSortIterable(name, visited, sorted) { + const stackTrace: StackItem[] = []; - sourceNodes.forEach(name => { - if (!visited.has(name)) { - topologicalSortUntil(name, visited, sorted); - } + stackTrace.push({ + count: -1, + value: name, + parent: '', + level: 0, }); - return sorted; + while (stackTrace.length > 0) { + const current = stackTrace.slice(-1)[0]; + + const adjacents = adjacent(current.value); + if (current.count === -1) { + current.count = adjacents.size; + } + + if (current.count > 0) { + const iter = adjacents.values(); + let cur = iter.next(); + while (!cur.done) { + if (!visited.has(cur.value)) { + stackTrace.push({ + count: -1, + parent: current.value, + value: cur.value, + level: current.level + 1, + }); + } else { + current.count--; + } + cur = iter.next(); + } + } else { + if (!visited.has(current.value)) { + visited.add(current.value); + sorted.unshift(current.value); + } + + const removed = stackTrace.pop(); + for (let i = 0; i < stackTrace.length; i++) { + if (stackTrace[i].value === removed.parent) { + stackTrace[i].count--; + } + } + } + } } function generateDOT() { @@ -113,11 +176,18 @@ export function Graph() { }); return ` - digraph G { - ${edgeStrings.join('\n').replace(/!/g, '_')} - } + digraph G { + ${edgeStrings.join('\n').replace(/!/g, '_')} + } `; } return graph; } + +interface StackItem { + count: number; + value: string; + parent: string; + level: number; +} diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index 161c85faa54b6d2ce277e369a6a73c2a4031618e..38f7b1a972d5df65f2f337db67af2cb7af11a714 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -5,7 +5,8 @@ export type FeatureFlag = | 'goalTemplatesEnabled' | 'customReports' | 'spendingReport' - | 'simpleFinSync'; + | 'simpleFinSync' + | 'iterableTopologicalSort'; export type LocalPrefs = Partial< { diff --git a/upcoming-release-notes/2848.md b/upcoming-release-notes/2848.md new file mode 100644 index 0000000000000000000000000000000000000000..35fc24299f1f6bedf8eaa74af2a1c0451c008a79 --- /dev/null +++ b/upcoming-release-notes/2848.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [lelemm] +--- + +Remove recursion from topological sort to prevent stack overflow