Skip to content

Commit 47229ff

Browse files
committed
perf(variables): O(N+V) map lookups in template resolve + header pass
Address gemini's perf findings on PR #92: - variableBinding.applyBindingToObject: build a name→Variable map once per call so a field with N markers resolves in O(N+V) instead of N linear scans across the V Variables. - zplGenerator.planTemplateHeader: build id/name/fn maps once before the leaf walk so the per-leaf single-bind lookup + per-marker name lookup + per-fn header emit all stay constant- time. Cuts the worst case from O(N·V) to O(N+V). Per-field Map rebuild inside zplParser.applyFnEmbeds (gemini's 4th finding) deferred — that path also bootstraps Variables so a hoisted-cache invalidation strategy adds more complexity than the inner-loop scan saves at current scale. TemplateContentInput a11y findings (role=menuitem, keyboard nav) deferred to a separate a11y pass alongside other Properties components — keeping this PR scoped to the ^FE feature surface.
1 parent 19c2984 commit 47229ff

2 files changed

Lines changed: 15 additions & 6 deletions

File tree

‎src/lib/variableBinding.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,11 @@ export function applyBindingToObject<T extends LabelObject>(
188188
next = resolveVariableValue(variable, active, mode);
189189
}
190190
if (hasTemplateMarkers(next)) {
191+
// Map lookup so a field with N markers stays O(N+V) not O(N·V).
192+
const byName = new Map(variables.map((v) => [v.name, v]));
191193
next = resolveTemplateMarkers(next, (name) => {
192-
const v = variables.find((x) => x.name === name);
193-
if (!v) return undefined;
194-
return resolveVariableValue(v, active, mode);
194+
const v = byName.get(name);
195+
return v ? resolveVariableValue(v, active, mode) : undefined;
195196
});
196197
}
197198
if (next === content) return obj;

‎src/lib/zplGenerator.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,28 @@ function planTemplateHeader(
8787
label: LabelConfig,
8888
variables: readonly Variable[],
8989
): { headerLines: string[]; emitCtx: ZplEmitContext } {
90+
// Pre-built maps keep the leaf walk + header emit O(N+V) instead
91+
// of O(N·V) — a label with hundreds of objects and dozens of
92+
// variables would otherwise re-scan the whole variables list per
93+
// leaf per marker.
94+
const varsById = new Map(variables.map((v) => [v.id, v]));
95+
const varsByName = new Map(variables.map((v) => [v.name, v]));
96+
const varsByFn = new Map(variables.map((v) => [v.fnNumber, v]));
97+
9098
const templatePayloads: string[] = [];
9199
const templateFns = new Set<number>();
92100
const singleBindFns = new Set<number>();
93101
for (const leaf of flattenObjects(shifted)) {
94102
if (leaf.includeInExport === false) continue;
95103
if (leaf.variableId) {
96-
const v = variables.find((x) => x.id === leaf.variableId);
104+
const v = varsById.get(leaf.variableId);
97105
if (v) singleBindFns.add(v.fnNumber);
98106
}
99107
const c = getObjectStringContent(leaf);
100108
if (c === undefined || !hasTemplateMarkers(c)) continue;
101109
templatePayloads.push(c);
102110
for (const name of extractTemplateRefs(c)) {
103-
const v = variables.find((x) => x.name === name);
111+
const v = varsByName.get(name);
104112
if (v) templateFns.add(v.fnNumber);
105113
}
106114
}
@@ -114,7 +122,7 @@ function planTemplateHeader(
114122
if (pickedEmbedChar !== '#') headerLines.push(`^FE${pickedEmbedChar}`);
115123
for (const fn of [...templateFns].sort((a, b) => a - b)) {
116124
if (singleBindFns.has(fn)) continue;
117-
const v = variables.find((x) => x.fnNumber === fn);
125+
const v = varsByFn.get(fn);
118126
if (!v) continue;
119127
headerLines.push(`^FN${fn}${fdField(v.defaultValue)}`);
120128
}

0 commit comments

Comments
 (0)