Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
11 changes: 7 additions & 4 deletions packages/core/src/engines/prospectiveMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ export class ProspectiveMemoryModule implements TaskModule<ProspectiveMemoryModu
if (!config.enabled || !context.rng || !context.stimuliByCategory) return block;

const trials = Array.isArray(block.trials) ? block.trials : [];
const eligibleIndices = trials
.map((t: any, idx: number) => ({ 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;

Expand Down
15 changes: 8 additions & 7 deletions packages/core/src/engines/stimulusInjector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,14 @@ export class StimulusInjectorModule implements TaskModule<StimulusInjectorModule
const trials = Array.isArray(block?.trials) ? block.trials : [];
if (trials.length === 0) return block;

const eligibleIndices = trials
.map((t: any, idx: number) => ({ 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(
Expand Down
146 changes: 92 additions & 54 deletions packages/core/src/utils/coerce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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 =
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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(
Expand Down
16 changes: 11 additions & 5 deletions packages/core/src/web/surveys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) };
},
};
Expand Down