crewai-ts
Version:
TypeScript port of crewAI for agent-based workflows
410 lines (409 loc) • 14.3 kB
JavaScript
/**
* FlowExecutionTracker
*
* Provides optimized tracking and metrics for flow execution in the Flow Orchestration system.
* Designed for high-performance in multi-flow environments with minimal memory footprint.
*/
import { EventEmitter } from 'events';
/**
* Tracks flow execution with optimized data structures and algorithms
* for minimal overhead and maximum performance insight.
*/
export class FlowExecutionTracker extends EventEmitter {
flowData;
durations = [];
totalDuration = 0;
statusCounts = {
pending: 0,
ready: 0,
running: 0,
completed: 0,
failed: 0,
canceled: 0
};
bottlenecks = [];
timeSeriesData = [];
startTime;
endTime;
constructor(options = {}) {
super();
this.flowData = new Map();
this.startTime = Date.now();
this.statusCounts = {
pending: 0,
ready: 0,
running: 0,
completed: 0,
failed: 0,
canceled: 0
};
}
registerFlow(flowId) {
if (!this.flowData.has(flowId)) {
const startTime = Date.now();
this.flowData.set(flowId, {
startTime,
endTime: undefined,
duration: undefined,
status: 'pending',
attempts: 0,
error: undefined,
result: undefined,
nodeExecutions: new Map(),
metadata: {},
dependencies: new Set(),
dependents: new Set()
});
this.statusCounts.pending++;
this.emit('flowStart', flowId);
}
}
addDependency(flowId, dependencyId) {
const flow = this.flowData.get(flowId);
const dependency = this.flowData.get(dependencyId);
if (!flow || !dependency) {
throw new Error('Flow not registered');
}
if (flow.dependencies) {
flow.dependencies.add(dependencyId);
}
if (dependency.dependents) {
dependency.dependents.add(flowId);
}
}
startFlow(flowId) {
const metrics = this.getFlowMetrics(flowId);
if (!metrics) {
throw new Error('Flow not found');
}
if (metrics.startTime) {
throw new Error('Flow already started');
}
metrics.startTime = Date.now();
metrics.status = 'running';
metrics.attempts = (metrics.attempts || 0) + 1;
this.statusCounts.pending--;
this.statusCounts.running++;
this.emit('flowStarted', flowId);
}
completeFlow(flowId, result) {
const metrics = this.getFlowMetrics(flowId);
if (!metrics) {
throw new Error('Flow not found');
}
if (!metrics.startTime) {
throw new Error('Flow not started');
}
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
metrics.result = result;
metrics.status = 'completed';
this.durations.push(metrics.duration);
this.totalDuration += metrics.duration;
this.statusCounts.running--;
this.statusCounts.completed++;
this.emit('flowCompleted', flowId);
}
failFlow(flowId, error) {
const metrics = this.getFlowMetrics(flowId);
if (!metrics) {
throw new Error('Flow not found');
}
if (!metrics.startTime) {
throw new Error('Flow not started');
}
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
metrics.error = error;
metrics.status = 'failed';
this.durations.push(metrics.duration);
this.totalDuration += metrics.duration;
this.statusCounts.running--;
this.statusCounts.failed++;
this.emit('flowFailed', flowId);
}
cancelFlow(flowId) {
const metrics = this.getFlowMetrics(flowId);
if (!metrics) {
throw new Error('Flow not found');
}
if (!metrics.startTime) {
throw new Error('Flow not started');
}
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
metrics.status = 'canceled';
if (typeof metrics.duration === 'number') {
this.durations.push(metrics.duration);
this.totalDuration += metrics.duration;
}
this.statusCounts.running--;
this.statusCounts.canceled++;
this.emit('flowCanceled', flowId);
}
resetFlow(flowId) {
const metrics = this.getFlowMetrics(flowId);
if (!metrics) {
throw new Error('Flow not found');
}
// Reset timestamps
metrics.startTime = undefined;
metrics.endTime = undefined;
metrics.duration = undefined;
// Reset error and result
metrics.error = undefined;
metrics.result = undefined;
metrics.status = 'pending';
// Reset counts safely
if (this.statusCounts.running > 0)
this.statusCounts.running--;
if (this.statusCounts.completed > 0)
this.statusCounts.completed--;
if (this.statusCounts.failed > 0)
this.statusCounts.failed--;
if (this.statusCounts.canceled > 0)
this.statusCounts.canceled--;
this.statusCounts.pending++;
// Reset flow metrics
metrics.duration = undefined;
if (metrics.nodeExecutions) {
metrics.nodeExecutions.clear();
}
metrics.attempts = 0;
metrics.metadata = undefined;
metrics.priority = undefined;
if (metrics.dependencies) {
metrics.dependencies.clear();
}
if (metrics.dependents) {
metrics.dependents.clear();
}
metrics.memory = undefined;
metrics.resourceUsage = undefined;
this.emit('flowReset', flowId);
}
getFlowMetrics(flowId) {
return this.flowData.get(flowId);
}
getFlowExecutionTime(flowId) {
const metrics = this.getFlowMetrics(flowId);
if (!metrics || !metrics.startTime)
return 0;
return (metrics.endTime ?? Date.now()) - metrics.startTime;
}
getFlowStatus(flowId) {
const metrics = this.getFlowMetrics(flowId);
if (!metrics) {
return 'pending';
}
return metrics.status;
}
getFlowError(flowId) {
const metrics = this.getFlowMetrics(flowId);
return metrics?.error;
}
getFlowResult(flowId) {
const metrics = this.getFlowMetrics(flowId);
return metrics?.result;
}
getFlowCount() {
return this.flowData.size;
}
getCompletedFlows() {
return this.statusCounts.completed;
}
getFailedFlows() {
return this.statusCounts.failed;
}
getRunningFlows() {
return this.statusCounts.running;
}
getPendingFlows() {
return this.statusCounts.pending;
}
getCanceledFlows() {
return this.statusCounts.canceled;
}
getReadyFlows() {
return this.statusCounts.ready;
}
getBottlenecks() {
return [...this.bottlenecks];
}
getTimeSeriesData() {
return [...this.timeSeriesData];
}
async saveExecutionCheckpoint() {
const checkpointData = {
flowData: Array.from(this.flowData.entries()),
durations: this.durations,
totalDuration: this.totalDuration,
statusCounts: this.statusCounts,
bottlenecks: this.bottlenecks,
timeSeriesData: this.timeSeriesData,
};
// Save to memory or file system
// Implementation depends on the persistence strategy
// For now, we'll just store in memory
localStorage.setItem('flowExecutionCheckpoint', JSON.stringify(checkpointData));
}
async loadExecutionCheckpoint() {
const checkpointData = JSON.parse(localStorage.getItem('flowExecutionCheckpoint') || '{}');
this.flowData = new Map(checkpointData.flowData);
this.durations = checkpointData.durations;
this.totalDuration = checkpointData.totalDuration;
this.statusCounts = checkpointData.statusCounts;
this.bottlenecks = checkpointData.bottlenecks;
this.timeSeriesData = checkpointData.timeSeriesData;
}
async waitForAnyFlowToComplete() {
return new Promise((resolve) => {
const checkCompletion = () => {
for (const [flowId, metrics] of this.flowData.entries()) {
if (metrics.status === 'completed' || metrics.status === 'failed' || metrics.status === 'canceled') {
resolve(flowId);
return;
}
}
setTimeout(checkCompletion, 100);
};
checkCompletion();
});
}
reset() {
this.flowData.clear();
this.durations = [];
this.totalDuration = 0;
this.statusCounts = {
pending: 0,
ready: 0,
running: 0,
completed: 0,
failed: 0,
canceled: 0
};
this.bottlenecks = [];
this.timeSeriesData = [];
this.startTime = Date.now();
this.endTime = undefined;
}
collectTimeSeriesDataPoint() {
const timestamp = Date.now();
const avgTime = this.durations.length > 0 ? this.totalDuration / this.durations.length : 0;
this.timeSeriesData.push({
timestamp,
metrics: {
runningFlows: this.statusCounts.running,
completedFlows: this.statusCounts.completed,
averageExecutionTime: avgTime,
},
});
if (this.timeSeriesData.length > 1000) {
this.timeSeriesData.shift();
}
}
startTracking() {
this.startTime = Date.now();
this.collectTimeSeriesDataPoint();
setInterval(() => this.collectTimeSeriesDataPoint(), 1000);
}
stopTracking() {
this.endTime = Date.now();
this.collectTimeSeriesDataPoint();
}
updateBottlenecks() {
const flowTimes = Array.from(this.flowData.entries())
.filter(([_, metrics]) => metrics?.endTime !== undefined)
.map(([id, metrics]) => [id, metrics?.duration ?? 0]);
const sortedTimes = flowTimes.sort(([, a], [, b]) => b - a);
const top10 = sortedTimes.slice(0, 10);
this.bottlenecks = top10.map(([id]) => id);
}
calculateMedian() {
const sorted = [...this.durations].sort((a, b) => (a ?? 0) - (b ?? 0));
const mid = Math.floor(sorted.length / 2);
if (sorted.length === 0)
return 0;
const lower = sorted[mid - 1] ?? 0;
const upper = sorted[mid] ?? 0;
return sorted.length % 2 !== 0 ? upper : (lower + upper) / 2;
}
calculateP95() {
const sorted = [...this.durations].sort((a, b) => (a ?? 0) - (b ?? 0));
const index = Math.floor(sorted.length * 0.95);
if (sorted.length === 0)
return 0;
return sorted[index] ?? 0;
}
getMetrics() {
const flowExecutionTimes = new Map(Array.from(this.flowData.entries()).map(([flowId, metrics]) => [
flowId,
metrics.duration ?? 0
]));
const durations = Array.from(this.flowData.values())
.map(metrics => metrics.duration ?? 0)
.filter(d => d > 0);
const maxTime = Math.max(...durations, 0);
const minTime = Math.min(...durations, maxTime);
const medianTime = durations.length > 0 ? durations.sort()[Math.floor(durations.length / 2)] : 0;
return {
flowExecutionTimes,
startTime: this.startTime,
endTime: this.endTime,
duration: this.endTime ? this.endTime - this.startTime : Date.now() - this.startTime,
averageFlowExecutionTime: this.durations.length > 0 ? this.totalDuration / this.durations.length : 0,
medianFlowExecutionTime: medianTime,
maxFlowExecutionTime: maxTime === 0 ? 0 : maxTime,
minFlowExecutionTime: minTime,
completedFlows: this.statusCounts.completed,
failedFlows: this.statusCounts.failed,
pendingFlows: this.statusCounts.pending,
runningFlows: this.statusCounts.running,
readyFlows: this.statusCounts.ready,
canceledFlows: this.statusCounts.canceled,
bottlenecks: [...this.bottlenecks],
timeSeriesData: this.timeSeriesData
};
}
updateDependencies(flowId, dependencies) {
const flow = this.flowData.get(flowId);
if (!flow)
return;
dependencies.forEach(dependencyId => {
const dependency = this.flowData.get(dependencyId);
if (!dependency)
return;
if (flow.dependencies) {
flow.dependencies.delete(dependencyId);
}
if (dependency.dependents) {
dependency.dependents.delete(flowId);
}
this.emit('dependenciesUpdated', flowId, dependencyId);
});
}
updateFlowMetrics(flowId) {
const flow = this.flowData.get(flowId);
if (!flow)
return;
const now = Date.now();
flow.duration = (flow.endTime ?? now) - (flow.startTime ?? now);
flow.resourceUsage = {
cpu: flow.resourceUsage?.cpu ?? 0,
memory: flow.resourceUsage?.memory ?? 0,
io: flow.resourceUsage?.io ?? 0,
network: flow.resourceUsage?.network ?? 0
};
}
getFlowResourceUsage(flowId) {
const metrics = this.getFlowMetrics(flowId);
if (!metrics?.resourceUsage)
return undefined;
return {
cpu: metrics.resourceUsage.cpu ?? 0,
memory: metrics.resourceUsage.memory ?? 0,
io: metrics.resourceUsage.io ?? 0,
network: metrics.resourceUsage.network ?? 0
};
}
}