From 37d819baa4a731a1cbcc6cf9c0ad1bd89c4a2367 Mon Sep 17 00:00:00 2001 From: Howchie <12265699+Howchie@users.noreply.github.com> Date: Thu, 19 Mar 2026 04:00:48 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20chained=20array?= =?UTF-8?q?=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace multiple chained array operations (`.map().filter().map()`, `.map().filter().reduce()`) with single `for` loops in: - `packages/core/src/engines/stimulusInjector.ts` - `packages/core/src/engines/prospectiveMemory.ts` - `packages/core/src/utils/coerce.ts` - `packages/core/src/web/surveys.ts` This eliminates unnecessary intermediate array allocations, reducing memory usage and garbage collection overhead, particularly in high-frequency paths. --- .jules/bolt.md | 3 + .../core/src/engines/prospectiveMemory.ts | 11 +- packages/core/src/engines/stimulusInjector.ts | 15 +- packages/core/src/utils/coerce.ts | 146 +++++++++++------- packages/core/src/web/surveys.ts | 16 +- 5 files changed, 121 insertions(+), 70 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 2acf32f0..394958eb 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -19,3 +19,6 @@ ## 2025-03-16 - Consolidating chained array iterations in mathematical routines **Learning:** Found a hot path in `packages/core/src/engines/parameterTransforms.ts` (`fitWaldAnalytic`) where an array of length N was traversed multiple times through `map`, `filter`, and multiple `reduce` calls to compute intermediate values (n, s1, sInv). Because this runs many times in a loop over ~1000 items, the overhead of creating intermediate arrays and performing 4 independent O(N) passes was significant (~630ms in benchmarks). Collapsing this into a single `for` loop eliminated all array allocations and reduced execution time by ~15-20x to ~35ms. **Action:** In high-frequency, performance-sensitive mathematical loops, avoid chaining `.map()`, `.filter()`, and `.reduce()`. Instead, use a standard `for` loop to accumulate multiple variables simultaneously in a single O(N) pass, completely eliminating intermediate array allocations. +## 2026-03-19 - Consolidating chained array iterations +**Learning:** Found several utility methods across `coerce.ts`, `surveys.ts`, `stimulusInjector.ts`, and `prospectiveMemory.ts` chaining `.map().filter().map()` or `.map().filter().reduce()`. Chained array methods allocate a new intermediate array at every step, which creates unnecessary memory allocations and garbage collection overhead, particularly for loops that run frequently on arrays of data. +**Action:** Replace chained array operations with a single `for` loop to significantly reduce intermediate array memory allocations and increase overall execution speed. diff --git a/packages/core/src/engines/prospectiveMemory.ts b/packages/core/src/engines/prospectiveMemory.ts index 6c43aca7..1752be88 100644 --- a/packages/core/src/engines/prospectiveMemory.ts +++ b/packages/core/src/engines/prospectiveMemory.ts @@ -45,10 +45,13 @@ export class ProspectiveMemoryModule implements TaskModule ({ type: t.trialType, idx })) - .filter((t: any) => !config.eligibleTrialTypes || config.eligibleTrialTypes.includes(t.type)) - .map((t: any) => t.idx); + const eligibleIndices: number[] = []; + for (let idx = 0; idx < trials.length; idx += 1) { + const type = trials[idx]?.trialType; + if (!config.eligibleTrialTypes || config.eligibleTrialTypes.includes(type)) { + eligibleIndices.push(idx); + } + } if (eligibleIndices.length === 0) return block; diff --git a/packages/core/src/engines/stimulusInjector.ts b/packages/core/src/engines/stimulusInjector.ts index 580c95ff..0ba4a796 100644 --- a/packages/core/src/engines/stimulusInjector.ts +++ b/packages/core/src/engines/stimulusInjector.ts @@ -103,13 +103,14 @@ export class StimulusInjectorModule implements TaskModule ({ trialType: String(t?.trialType ?? ""), idx })) - .filter((entry: { trialType: string; idx: number }) => { - if (!Array.isArray(injection.eligibleTrialTypes) || injection.eligibleTrialTypes.length === 0) return true; - return injection.eligibleTrialTypes.includes(entry.trialType); - }) - .map((entry: { trialType: string; idx: number }) => entry.idx); + const eligibleIndices: number[] = []; + const hasEligibleTypes = Array.isArray(injection.eligibleTrialTypes) && injection.eligibleTrialTypes.length > 0; + for (let idx = 0; idx < trials.length; idx += 1) { + const trialType = String(trials[idx]?.trialType ?? ""); + if (!hasEligibleTypes || injection.eligibleTrialTypes!.includes(trialType)) { + eligibleIndices.push(idx); + } + } if (eligibleIndices.length === 0) return block; const positions = generateProspectiveMemoryPositions( diff --git a/packages/core/src/utils/coerce.ts b/packages/core/src/utils/coerce.ts index 48550ee4..732c5415 100644 --- a/packages/core/src/utils/coerce.ts +++ b/packages/core/src/utils/coerce.ts @@ -44,7 +44,12 @@ export function toFiniteNumber(value: unknown, fallback: number): number { } export function toNumberArray(value: unknown, fallback: number[]): number[] { - const out = asArray(value).map((entry) => Number(entry)).filter((entry) => Number.isFinite(entry)) as number[]; + const out: number[] = []; + const arrayValue = asArray(value); + for (let i = 0; i < arrayValue.length; i += 1) { + const entry = Number(arrayValue[i]); + if (Number.isFinite(entry)) out.push(entry); + } return out.length > 0 ? out : fallback; } @@ -53,20 +58,35 @@ export function toStringScreens(value: unknown): string[] { const text = value.trim(); return text ? [text] : []; } - return asArray(value).map((item) => asString(item)).filter((item): item is string => Boolean(item)); + const out: string[] = []; + const arrayValue = asArray(value); + for (let i = 0; i < arrayValue.length; i += 1) { + const item = asString(arrayValue[i]); + if (item) out.push(item); + } + return out; } export function asStringArray(value: unknown, fallback: string[]): string[] { - const list = asArray(value).map((item) => asString(item)).filter((item): item is string => Boolean(item)); - return list.length > 0 ? list : [...fallback]; + const out: string[] = []; + const arrayValue = asArray(value); + for (let i = 0; i < arrayValue.length; i += 1) { + const item = asString(arrayValue[i]); + if (item) out.push(item); + } + return out.length > 0 ? out : [...fallback]; } export function asPositiveNumberArray(value: unknown, fallback: number[]): number[] { - const list = asArray(value) - .map((entry) => Number(entry)) - .filter((entry) => Number.isFinite(entry) && entry > 0) - .map((entry) => Math.floor(entry)); - return list.length > 0 ? list : [...fallback]; + const out: number[] = []; + const arrayValue = asArray(value); + for (let i = 0; i < arrayValue.length; i += 1) { + const entry = Number(arrayValue[i]); + if (Number.isFinite(entry) && entry > 0) { + out.push(Math.floor(entry)); + } + } + return out.length > 0 ? out : [...fallback]; } export type InstructionInsertionPoint = @@ -118,17 +138,29 @@ export function coerceInstructionInsertions(value: unknown): InstructionInsertio const pages = toInstructionScreenSpecs(raw.pages); if (pages.length === 0) continue; const whenRaw = asObject(raw.when); - const blockIndex = asArray(whenRaw?.blockIndex) - .map((item) => Number(item)) - .filter((item) => Number.isInteger(item)) - .map((item) => Math.floor(item)); - const blockLabel = asArray(whenRaw?.blockLabel) - .map((item) => asString(item)) - .filter((item): item is string => Boolean(item)); - const blockType = asArray(whenRaw?.blockType) - .map((item) => asString(item)) - .filter((item): item is string => Boolean(item)) - .map((item) => item.toLowerCase()); + + const blockIndex: number[] = []; + const rawBlockIndex = asArray(whenRaw?.blockIndex); + for (let i = 0; i < rawBlockIndex.length; i += 1) { + const item = Number(rawBlockIndex[i]); + if (Number.isInteger(item)) { + blockIndex.push(Math.floor(item)); + } + } + + const blockLabel: string[] = []; + const rawBlockLabel = asArray(whenRaw?.blockLabel); + for (let i = 0; i < rawBlockLabel.length; i += 1) { + const item = asString(rawBlockLabel[i]); + if (item) blockLabel.push(item); + } + + const blockType: string[] = []; + const rawBlockType = asArray(whenRaw?.blockType); + for (let i = 0; i < rawBlockType.length; i += 1) { + const item = asString(rawBlockType[i]); + if (item) blockType.push(item.toLowerCase()); + } const isPractice = typeof whenRaw?.isPractice === "boolean" ? whenRaw.isPractice : undefined; const when: InstructionInsertionWhen | undefined = blockIndex.length > 0 || blockLabel.length > 0 || blockType.length > 0 || typeof isPractice === "boolean" @@ -203,40 +235,46 @@ export function toInstructionScreenSpecs(value: unknown): InstructionScreenSpec[ const text = value.trim(); return text ? [{ text }] : []; } - return asArray(value) - .map((item): InstructionScreenSpec | null => { - if (typeof item === "string") { - const text = item.trim(); - return text ? { text } : null; - } - const raw = asObject(item); - if (!raw) return null; - const title = asString(raw.title) ?? undefined; - const html = asString(raw.html) ?? undefined; - const text = asString(raw.text) ?? asString(raw.body) ?? asString(raw.content) ?? undefined; - const actions = asArray(raw.actions) - .map((entry): InstructionScreenAction | null => { - const actionRaw = asObject(entry); - if (!actionRaw) return null; - const label = asString(actionRaw.label); - if (!label) return null; - const action = (asString(actionRaw.action) ?? "continue").toLowerCase(); - return { - ...(asString(actionRaw.id) ? { id: asString(actionRaw.id) as string } : {}), - label, - action: action === "exit" ? "exit" : "continue", - }; - }) - .filter((entry): entry is InstructionScreenAction => Boolean(entry)); - if (!html && !text) return null; - return { - ...(title ? { title } : {}), - ...(text ? { text } : {}), - ...(html ? { html } : {}), - ...(actions.length > 0 ? { actions } : {}), - }; - }) - .filter((item): item is InstructionScreenSpec => Boolean(item)); + const out: InstructionScreenSpec[] = []; + const arrayValue = asArray(value); + for (let i = 0; i < arrayValue.length; i += 1) { + const item = arrayValue[i]; + if (typeof item === "string") { + const text = item.trim(); + if (text) out.push({ text }); + continue; + } + const raw = asObject(item); + if (!raw) continue; + const title = asString(raw.title) ?? undefined; + const html = asString(raw.html) ?? undefined; + const text = asString(raw.text) ?? asString(raw.body) ?? asString(raw.content) ?? undefined; + + const actions: InstructionScreenAction[] = []; + const rawActions = asArray(raw.actions); + for (let j = 0; j < rawActions.length; j += 1) { + const entry = rawActions[j]; + const actionRaw = asObject(entry); + if (!actionRaw) continue; + const label = asString(actionRaw.label); + if (!label) continue; + const action = (asString(actionRaw.action) ?? "continue").toLowerCase(); + actions.push({ + ...(asString(actionRaw.id) ? { id: asString(actionRaw.id) as string } : {}), + label, + action: action === "exit" ? "exit" : "continue", + }); + } + + if (!html && !text) continue; + out.push({ + ...(title ? { title } : {}), + ...(text ? { text } : {}), + ...(html ? { html } : {}), + ...(actions.length > 0 ? { actions } : {}), + }); + } + return out; } export function resolveInstructionScreenSlots( diff --git a/packages/core/src/web/surveys.ts b/packages/core/src/web/surveys.ts index b3859248..97d8cfdb 100644 --- a/packages/core/src/web/surveys.ts +++ b/packages/core/src/web/surveys.ts @@ -496,11 +496,17 @@ export function createNasaTlxSurvey(options: NasaTlxSurveyOptions = {}): SurveyD submitButtonStyle: options.submitButtonStyle, autoFocusSubmitButton: options.autoFocusSubmitButton, computeScores: (answers) => { - const numericValues = selectedSubscales - .map((subscale) => answers[subscale.id]) - .filter((value): value is number => typeof value === "number" && Number.isFinite(value)); - if (numericValues.length === 0) return undefined; - const average = numericValues.reduce((sum, value) => sum + value, 0) / numericValues.length; + let sum = 0; + let count = 0; + for (let i = 0; i < selectedSubscales.length; i += 1) { + const value = answers[selectedSubscales[i].id]; + if (typeof value === "number" && Number.isFinite(value)) { + sum += value; + count += 1; + } + } + if (count === 0) return undefined; + const average = sum / count; return { raw_tlx: Number(average.toFixed(3)) }; }, };