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
20 changes: 20 additions & 0 deletions apps/sim/executor/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,26 @@ export const DEFAULTS = {
MAX_LOOP_ITERATIONS: 1000,
MAX_WORKFLOW_DEPTH: 10,
EXECUTION_TIME: 0,
/**
* Maximum number of iteration outputs to retain in memory during loop execution.
* Older iterations are discarded to prevent memory exhaustion in long-running loops.
* The final aggregated results will contain only the most recent iterations.
*/
MAX_STORED_ITERATION_OUTPUTS: 100,
/**
* Maximum size in bytes for iteration outputs before triggering truncation.
* This is an approximate estimate based on JSON serialization.
*/
MAX_ITERATION_OUTPUTS_SIZE_BYTES: 50 * 1024 * 1024, // 50MB
/**
* Maximum number of block logs to retain in memory during execution.
* Older logs are discarded to prevent memory exhaustion in long-running workflows.
*/
MAX_BLOCK_LOGS: 500,
/**
* Maximum size in bytes for block logs before triggering truncation.
*/
MAX_BLOCK_LOGS_SIZE_BYTES: 100 * 1024 * 1024, // 100MB
TOKENS: {
PROMPT: 0,
COMPLETION: 0,
Expand Down
54 changes: 53 additions & 1 deletion apps/sim/executor/execution/block-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class BlockExecutor {
let blockLog: BlockLog | undefined
if (!isSentinel) {
blockLog = this.createBlockLog(ctx, node.id, block, node)
ctx.blockLogs.push(blockLog)
this.addBlockLogWithMemoryLimit(ctx, blockLog)
this.callOnBlockStart(ctx, node, block)
}

Expand Down Expand Up @@ -658,4 +658,56 @@ export class BlockExecutor {

executionOutput.content = fullContent
}

/**
* Adds a block log to the execution context with memory management.
* Prevents unbounded memory growth by:
* 1. Limiting the number of stored logs (MAX_BLOCK_LOGS)
* 2. Checking estimated memory size (MAX_BLOCK_LOGS_SIZE_BYTES)
*
* When limits are exceeded, older logs are discarded to make room for newer ones.
*/
private addBlockLogWithMemoryLimit(ctx: ExecutionContext, blockLog: BlockLog): void {
ctx.blockLogs.push(blockLog)

// Check log count limit
if (ctx.blockLogs.length > DEFAULTS.MAX_BLOCK_LOGS) {
const discardCount = ctx.blockLogs.length - DEFAULTS.MAX_BLOCK_LOGS
ctx.blockLogs = ctx.blockLogs.slice(discardCount)
logger.warn('Block logs exceeded count limit, discarding older logs', {
discardedCount: discardCount,
retainedCount: ctx.blockLogs.length,
maxAllowed: DEFAULTS.MAX_BLOCK_LOGS,
})
}

// Periodically check memory size (every 50 logs to avoid frequent serialization)
if (ctx.blockLogs.length % 50 === 0) {
const estimatedSize = this.estimateBlockLogsSize(ctx.blockLogs)
if (estimatedSize > DEFAULTS.MAX_BLOCK_LOGS_SIZE_BYTES) {
const halfLength = Math.floor(ctx.blockLogs.length / 2)
const discardCount = Math.max(halfLength, 1)
ctx.blockLogs = ctx.blockLogs.slice(discardCount)
logger.warn('Block logs exceeded memory limit, discarding older logs', {
estimatedSizeBytes: estimatedSize,
maxSizeBytes: DEFAULTS.MAX_BLOCK_LOGS_SIZE_BYTES,
discardedCount: discardCount,
retainedCount: ctx.blockLogs.length,
})
}
}
}

/**
* Estimates the memory size of block logs in bytes.
* Returns a value exceeding the limit on serialization failure to trigger cleanup.
*/
private estimateBlockLogsSize(logs: BlockLog[]): number {
try {
return JSON.stringify(logs).length * 2
} catch {
// Return a value that exceeds the limit to trigger cleanup on serialization failure
return DEFAULTS.MAX_BLOCK_LOGS_SIZE_BYTES + 1
}
}
}
69 changes: 68 additions & 1 deletion apps/sim/executor/orchestrators/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export class LoopOrchestrator {
}

if (iterationResults.length > 0) {
scope.allIterationOutputs.push(iterationResults)
this.addIterationOutputsWithMemoryLimit(scope, iterationResults, loopId)
}

scope.currentIterationOutputs.clear()
Expand Down Expand Up @@ -462,4 +462,71 @@ export class LoopOrchestrator {
return []
}
}

/**
* Adds iteration outputs to the loop scope with memory management.
* Prevents unbounded memory growth by:
* 1. Limiting the number of stored iterations (MAX_STORED_ITERATION_OUTPUTS)
* 2. Checking estimated memory size (MAX_ITERATION_OUTPUTS_SIZE_BYTES)
*
* When limits are exceeded, older iterations are discarded to make room for newer ones.
* This ensures long-running loops don't cause memory exhaustion while still providing
* access to recent iteration results.
*/
private addIterationOutputsWithMemoryLimit(
scope: LoopScope,
iterationResults: NormalizedBlockOutput[],
loopId: string
): void {
scope.allIterationOutputs.push(iterationResults)

// Check iteration count limit
if (scope.allIterationOutputs.length > DEFAULTS.MAX_STORED_ITERATION_OUTPUTS) {
const discardCount = scope.allIterationOutputs.length - DEFAULTS.MAX_STORED_ITERATION_OUTPUTS
scope.allIterationOutputs = scope.allIterationOutputs.slice(discardCount)
logger.warn('Loop iteration outputs exceeded count limit, discarding older iterations', {
loopId,
iteration: scope.iteration,
discardedCount: discardCount,
retainedCount: scope.allIterationOutputs.length,
maxAllowed: DEFAULTS.MAX_STORED_ITERATION_OUTPUTS,
})
}

// Periodically check memory size limit (every 10 iterations to avoid frequent serialization)
if (scope.allIterationOutputs.length % 10 === 0) {
const estimatedSize = this.estimateObjectSize(scope.allIterationOutputs)
if (estimatedSize > DEFAULTS.MAX_ITERATION_OUTPUTS_SIZE_BYTES) {
// Discard oldest half of iterations when memory limit exceeded
const halfLength = Math.floor(scope.allIterationOutputs.length / 2)
const discardCount = Math.max(halfLength, 1)
scope.allIterationOutputs = scope.allIterationOutputs.slice(discardCount)
logger.warn('Loop iteration outputs exceeded memory limit, discarding older iterations', {
loopId,
iteration: scope.iteration,
estimatedSizeBytes: estimatedSize,
maxSizeBytes: DEFAULTS.MAX_ITERATION_OUTPUTS_SIZE_BYTES,
discardedCount: discardCount,
retainedCount: scope.allIterationOutputs.length,
})
}
}
}

/**
* Estimates the memory size of an object in bytes.
* This is an approximation based on JSON serialization size.
* Actual memory usage may vary due to object overhead and references.
*/
private estimateObjectSize(obj: unknown): number {
try {
// Use JSON.stringify length as a rough estimate
// Multiply by 2 for UTF-16 encoding overhead in JS strings
return JSON.stringify(obj).length * 2
} catch {
// If serialization fails (circular refs, etc.), return a value that exceeds
// the limit to trigger cleanup as a safety measure
return DEFAULTS.MAX_ITERATION_OUTPUTS_SIZE_BYTES + 1
}
}
}