From 49532627a11306082570e67d5b0ad662d2d39994 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Jan 2026 10:56:53 +0000 Subject: [PATCH 1/2] fix(executor): use performance.now() for precise block timing Replace Date.now() with performance.now() for timing measurements in the executor to provide sub-millisecond precision. This fixes timing discrepancies with fast-executing blocks like the start block where millisecond precision was insufficient. Changes: - block-executor.ts: Use performance.now() for block execution timing - engine.ts: Use performance.now() for overall execution timing Co-authored-by: emir --- apps/sim/executor/execution/block-executor.ts | 6 +++--- apps/sim/executor/execution/engine.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 5e0f2c71bc..6e63e5333c 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -71,7 +71,7 @@ export class BlockExecutor { this.callOnBlockStart(ctx, node, block) } - const startTime = Date.now() + const startTime = performance.now() let resolvedInputs: Record = {} const nodeMetadata = this.buildNodeMetadata(node) @@ -145,7 +145,7 @@ export class BlockExecutor { })) as NormalizedBlockOutput } - const duration = Date.now() - startTime + const duration = performance.now() - startTime if (blockLog) { blockLog.endedAt = new Date().toISOString() @@ -221,7 +221,7 @@ export class BlockExecutor { isSentinel: boolean, phase: 'input_resolution' | 'execution' ): NormalizedBlockOutput { - const duration = Date.now() - startTime + const duration = performance.now() - startTime const errorMessage = normalizeError(error) const hasResolvedInputs = resolvedInputs && typeof resolvedInputs === 'object' && Object.keys(resolvedInputs).length > 0 diff --git a/apps/sim/executor/execution/engine.ts b/apps/sim/executor/execution/engine.ts index 92b43e6a59..61670c1b89 100644 --- a/apps/sim/executor/execution/engine.ts +++ b/apps/sim/executor/execution/engine.ts @@ -101,7 +101,7 @@ export class ExecutionEngine { } async run(triggerBlockId?: string): Promise { - const startTime = Date.now() + const startTime = performance.now() try { this.initializeQueue(triggerBlockId) @@ -125,8 +125,8 @@ export class ExecutionEngine { return this.buildPausedResult(startTime) } - const endTime = Date.now() - this.context.metadata.endTime = new Date(endTime).toISOString() + const endTime = performance.now() + this.context.metadata.endTime = new Date().toISOString() this.context.metadata.duration = endTime - startTime if (this.cancelledFlag) { @@ -146,8 +146,8 @@ export class ExecutionEngine { metadata: this.context.metadata, } } catch (error) { - const endTime = Date.now() - this.context.metadata.endTime = new Date(endTime).toISOString() + const endTime = performance.now() + this.context.metadata.endTime = new Date().toISOString() this.context.metadata.duration = endTime - startTime if (this.cancelledFlag) { @@ -433,8 +433,8 @@ export class ExecutionEngine { } private buildPausedResult(startTime: number): ExecutionResult { - const endTime = Date.now() - this.context.metadata.endTime = new Date(endTime).toISOString() + const endTime = performance.now() + this.context.metadata.endTime = new Date().toISOString() this.context.metadata.duration = endTime - startTime this.context.metadata.status = 'paused' From 543927d59026a57f90927cfaa85f71db4c983dfb Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 30 Jan 2026 12:44:23 -0800 Subject: [PATCH 2/2] format ms as whole nums,round secs to 2 decimal places and compute all started/ended times on server and passback to clinet --- .../app/api/workflows/[id]/execute/route.ts | 4 ++ .../[workflowId]/components/terminal/utils.ts | 4 +- .../hooks/use-workflow-execution.ts | 16 ++--- apps/sim/executor/execution/block-executor.ts | 14 +++- apps/sim/executor/execution/types.ts | 8 ++- apps/sim/executor/orchestrators/loop.ts | 3 + apps/sim/executor/orchestrators/parallel.ts | 3 + apps/sim/executor/utils/subflow-utils.ts | 2 + apps/sim/hooks/use-execution-stream.ts | 65 ++++++------------- .../lib/workflows/executor/execution-core.ts | 8 ++- .../workflows/executor/execution-events.ts | 29 ++++++++- 11 files changed, 97 insertions(+), 59 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 47f81ef122..53161e42a0 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -616,6 +616,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: input: callbackData.input, error: callbackData.output.error, durationMs: callbackData.executionTime || 0, + startedAt: callbackData.startedAt, + endedAt: callbackData.endedAt, ...(iterationContext && { iterationCurrent: iterationContext.iterationCurrent, iterationTotal: iterationContext.iterationTotal, @@ -641,6 +643,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: input: callbackData.input, output: callbackData.output, durationMs: callbackData.executionTime || 0, + startedAt: callbackData.startedAt, + endedAt: callbackData.endedAt, ...(iterationContext && { iterationCurrent: iterationContext.iterationCurrent, iterationTotal: iterationContext.iterationTotal, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts index 39d3770b31..6dbc157700 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts @@ -58,7 +58,9 @@ export function getBlockColor(blockType: string): string { */ export function formatDuration(ms?: number): string { if (ms === undefined || ms === null) return '-' - if (ms < 1000) return `${ms}ms` + if (ms < 1000) { + return `${Math.round(ms)}ms` + } return `${(ms / 1000).toFixed(2)}s` } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 8f622c165e..6dcad6c175 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -964,8 +964,8 @@ export function useWorkflowExecution() { const isContainerBlock = data.blockType === 'loop' || data.blockType === 'parallel' if (isContainerBlock) return - const startedAt = new Date(Date.now() - data.durationMs).toISOString() - const endedAt = new Date().toISOString() + const startedAt = data.startedAt + const endedAt = data.endedAt accumulatedBlockLogs.push({ blockId: data.blockId, @@ -1013,8 +1013,8 @@ export function useWorkflowExecution() { // Track failed block execution in run path setBlockRunStatus(data.blockId, 'error') - const startedAt = new Date(Date.now() - data.durationMs).toISOString() - const endedAt = new Date().toISOString() + const startedAt = data.startedAt + const endedAt = data.endedAt // Accumulate block error log for the execution result accumulatedBlockLogs.push({ @@ -1603,8 +1603,8 @@ export function useWorkflowExecution() { const isContainerBlock = data.blockType === 'loop' || data.blockType === 'parallel' if (isContainerBlock) return - const startedAt = new Date(Date.now() - data.durationMs).toISOString() - const endedAt = new Date().toISOString() + const startedAt = data.startedAt + const endedAt = data.endedAt accumulatedBlockLogs.push({ blockId: data.blockId, @@ -1642,8 +1642,8 @@ export function useWorkflowExecution() { setBlockRunStatus(data.blockId, 'error') - const startedAt = new Date(Date.now() - data.durationMs).toISOString() - const endedAt = new Date().toISOString() + const startedAt = data.startedAt + const endedAt = data.endedAt accumulatedBlockLogs.push({ blockId: data.blockId, diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 6e63e5333c..d17da0e7c7 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -169,7 +169,9 @@ export class BlockExecutor { block, this.sanitizeInputsForLog(resolvedInputs), displayOutput, - duration + duration, + blockLog!.startedAt, + blockLog!.endedAt ) } @@ -274,7 +276,9 @@ export class BlockExecutor { block, this.sanitizeInputsForLog(input), displayOutput, - duration + duration, + blockLog!.startedAt, + blockLog!.endedAt ) } @@ -423,7 +427,9 @@ export class BlockExecutor { block: SerializedBlock, input: Record, output: NormalizedBlockOutput, - duration: number + duration: number, + startedAt: string, + endedAt: string ): void { const blockId = node.id const blockName = block.metadata?.name ?? blockId @@ -440,6 +446,8 @@ export class BlockExecutor { input, output, executionTime: duration, + startedAt, + endedAt, }, iterationContext ) diff --git a/apps/sim/executor/execution/types.ts b/apps/sim/executor/execution/types.ts index 40c4c61cd2..e770989b6f 100644 --- a/apps/sim/executor/execution/types.ts +++ b/apps/sim/executor/execution/types.ts @@ -103,7 +103,13 @@ export interface ContextExtensions { blockId: string, blockName: string, blockType: string, - output: { input?: any; output: NormalizedBlockOutput; executionTime: number }, + output: { + input?: any + output: NormalizedBlockOutput + executionTime: number + startedAt: string + endedAt: string + }, iterationContext?: IterationContext ) => Promise diff --git a/apps/sim/executor/orchestrators/loop.ts b/apps/sim/executor/orchestrators/loop.ts index a2bd7babfc..bd72b8498e 100644 --- a/apps/sim/executor/orchestrators/loop.ts +++ b/apps/sim/executor/orchestrators/loop.ts @@ -281,9 +281,12 @@ export class LoopOrchestrator { // Emit onBlockComplete for the loop container so the UI can track it if (this.contextExtensions?.onBlockComplete) { + const now = new Date().toISOString() this.contextExtensions.onBlockComplete(loopId, 'Loop', 'loop', { output, executionTime: DEFAULTS.EXECUTION_TIME, + startedAt: now, + endedAt: now, }) } diff --git a/apps/sim/executor/orchestrators/parallel.ts b/apps/sim/executor/orchestrators/parallel.ts index b2b67428f6..88942b8cbc 100644 --- a/apps/sim/executor/orchestrators/parallel.ts +++ b/apps/sim/executor/orchestrators/parallel.ts @@ -265,9 +265,12 @@ export class ParallelOrchestrator { // Emit onBlockComplete for the parallel container so the UI can track it if (this.contextExtensions?.onBlockComplete) { + const now = new Date().toISOString() this.contextExtensions.onBlockComplete(parallelId, 'Parallel', 'parallel', { output, executionTime: 0, + startedAt: now, + endedAt: now, }) } diff --git a/apps/sim/executor/utils/subflow-utils.ts b/apps/sim/executor/utils/subflow-utils.ts index 7f741b2f05..5ef3a51b5d 100644 --- a/apps/sim/executor/utils/subflow-utils.ts +++ b/apps/sim/executor/utils/subflow-utils.ts @@ -232,6 +232,8 @@ export function addSubflowErrorLog( input: inputData, output: { error: errorMessage }, executionTime: 0, + startedAt: now, + endedAt: now, }) } } diff --git a/apps/sim/hooks/use-execution-stream.ts b/apps/sim/hooks/use-execution-stream.ts index b7a443ebd6..5981d28f52 100644 --- a/apps/sim/hooks/use-execution-stream.ts +++ b/apps/sim/hooks/use-execution-stream.ts @@ -1,8 +1,18 @@ import { useCallback, useRef } from 'react' import { createLogger } from '@sim/logger' -import type { ExecutionEvent } from '@/lib/workflows/executor/execution-events' +import type { + BlockCompletedData, + BlockErrorData, + BlockStartedData, + ExecutionCancelledData, + ExecutionCompletedData, + ExecutionErrorData, + ExecutionEvent, + ExecutionStartedData, + StreamChunkData, + StreamDoneData, +} from '@/lib/workflows/executor/execution-events' import type { SerializableExecutionState } from '@/executor/execution/types' -import type { SubflowType } from '@/stores/workflows/workflow/types' const logger = createLogger('useExecutionStream') @@ -81,48 +91,15 @@ async function processSSEStream( } export interface ExecutionStreamCallbacks { - onExecutionStarted?: (data: { startTime: string }) => void - onExecutionCompleted?: (data: { - success: boolean - output: any - duration: number - startTime: string - endTime: string - }) => void - onExecutionError?: (data: { error: string; duration: number }) => void - onExecutionCancelled?: (data: { duration: number }) => void - onBlockStarted?: (data: { - blockId: string - blockName: string - blockType: string - iterationCurrent?: number - iterationTotal?: number - iterationType?: SubflowType - }) => void - onBlockCompleted?: (data: { - blockId: string - blockName: string - blockType: string - input?: any - output: any - durationMs: number - iterationCurrent?: number - iterationTotal?: number - iterationType?: SubflowType - }) => void - onBlockError?: (data: { - blockId: string - blockName: string - blockType: string - input?: any - error: string - durationMs: number - iterationCurrent?: number - iterationTotal?: number - iterationType?: SubflowType - }) => void - onStreamChunk?: (data: { blockId: string; chunk: string }) => void - onStreamDone?: (data: { blockId: string }) => void + onExecutionStarted?: (data: ExecutionStartedData) => void + onExecutionCompleted?: (data: ExecutionCompletedData) => void + onExecutionError?: (data: ExecutionErrorData) => void + onExecutionCancelled?: (data: ExecutionCancelledData) => void + onBlockStarted?: (data: BlockStartedData) => void + onBlockCompleted?: (data: BlockCompletedData) => void + onBlockError?: (data: BlockErrorData) => void + onStreamChunk?: (data: StreamChunkData) => void + onStreamDone?: (data: StreamDoneData) => void } export interface ExecuteStreamOptions { diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index 557bb284e8..60998d934f 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -283,7 +283,13 @@ export async function executeWorkflowCore( blockId: string, blockName: string, blockType: string, - output: { input?: unknown; output: NormalizedBlockOutput; executionTime: number }, + output: { + input?: unknown + output: NormalizedBlockOutput + executionTime: number + startedAt: string + endedAt: string + }, iterationContext?: IterationContext ) => { await loggingSession.onBlockComplete(blockId, blockName, blockType, output) diff --git a/apps/sim/lib/workflows/executor/execution-events.ts b/apps/sim/lib/workflows/executor/execution-events.ts index 436df010ea..6c3998e231 100644 --- a/apps/sim/lib/workflows/executor/execution-events.ts +++ b/apps/sim/lib/workflows/executor/execution-events.ts @@ -103,6 +103,8 @@ export interface BlockCompletedEvent extends BaseExecutionEvent { input?: any output: any durationMs: number + startedAt: string + endedAt: string // Iteration context for loops and parallels iterationCurrent?: number iterationTotal?: number @@ -123,6 +125,8 @@ export interface BlockErrorEvent extends BaseExecutionEvent { input?: any error: string durationMs: number + startedAt: string + endedAt: string // Iteration context for loops and parallels iterationCurrent?: number iterationTotal?: number @@ -167,6 +171,19 @@ export type ExecutionEvent = | StreamChunkEvent | StreamDoneEvent +/** + * Extracted data types for use in callbacks + */ +export type ExecutionStartedData = ExecutionStartedEvent['data'] +export type ExecutionCompletedData = ExecutionCompletedEvent['data'] +export type ExecutionErrorData = ExecutionErrorEvent['data'] +export type ExecutionCancelledData = ExecutionCancelledEvent['data'] +export type BlockStartedData = BlockStartedEvent['data'] +export type BlockCompletedData = BlockCompletedEvent['data'] +export type BlockErrorData = BlockErrorEvent['data'] +export type StreamChunkData = StreamChunkEvent['data'] +export type StreamDoneData = StreamDoneEvent['data'] + /** * Helper to create SSE formatted message */ @@ -235,7 +252,13 @@ export function createSSECallbacks(options: SSECallbackOptions) { blockId: string, blockName: string, blockType: string, - callbackData: { input?: unknown; output: any; executionTime: number }, + callbackData: { + input?: unknown + output: any + executionTime: number + startedAt: string + endedAt: string + }, iterationContext?: { iterationCurrent: number; iterationTotal: number; iterationType: string } ) => { const hasError = callbackData.output?.error @@ -260,6 +283,8 @@ export function createSSECallbacks(options: SSECallbackOptions) { input: callbackData.input, error: callbackData.output.error, durationMs: callbackData.executionTime || 0, + startedAt: callbackData.startedAt, + endedAt: callbackData.endedAt, ...iterationData, }, }) @@ -276,6 +301,8 @@ export function createSSECallbacks(options: SSECallbackOptions) { input: callbackData.input, output: callbackData.output, durationMs: callbackData.executionTime || 0, + startedAt: callbackData.startedAt, + endedAt: callbackData.endedAt, ...iterationData, }, })