crewai-ts
Version:
TypeScript port of crewAI for agent-based workflows
419 lines (418 loc) • 14.6 kB
JavaScript
import { EventPriority } from '../events/FlowEventBus.js';
/**
* FlowExecutionTracker provides optimized performance tracking for flows
* with minimal overhead and configurable sampling.
*/
export class FlowExecutionTracker {
// Metrics storage
metrics;
metricSamples = [];
executionTrace = [];
// Flow reference and tracking state
flow;
isTracking = false;
eventHandlers = [];
processMetricsTimer;
// Configuration
options;
/**
* Create a tracker for a specific flow
*/
constructor(flow, options = {}) {
this.flow = flow;
// Set default options
this.options = {
sampleInterval: 1000, // 1 second
minimumDurationToTrack: 5, // 5ms
trackMethodCalls: true,
trackStateChanges: true,
trackRouteEvaluations: true,
maxTraceEntries: 1000,
maxSamples: 100,
compressTraces: false,
persistMetrics: false,
trackProcessMetrics: false,
processMetricsInterval: 5000, // 5 seconds
methodDurationWarningMs: 500, // 500ms
memoryWarningThresholdMb: 500, // 500MB
eventRateWarningThreshold: 1000, // 1000 events/sec
...options
};
// Initialize metrics
this.metrics = {
startTime: 0,
endTime: 0,
executionTimeMs: 0,
methodCallCount: {},
methodDurations: {},
stateChanges: 0,
routeEvaluations: 0,
errorCount: 0,
eventCount: 0
};
}
/**
* Start tracking flow execution with optimized event listeners
*/
startTracking() {
if (this.isTracking)
return;
this.isTracking = true;
// Record start time
this.metrics.startTime = performance.now();
// Set up event listeners if event bus is available
if (this.options.eventBus) {
this.registerEventListeners(this.options.eventBus);
}
// Start process metrics tracking if enabled
if (this.options.trackProcessMetrics) {
this.startProcessMetricsTracking();
}
// Record initial sample
this.takeSample();
}
/**
* Stop tracking and finalize metrics
*/
stopTracking() {
if (!this.isTracking)
return this.metrics;
// Record end time and duration
this.metrics.endTime = performance.now();
this.metrics.executionTimeMs = this.metrics.endTime - this.metrics.startTime;
// Remove event listeners
if (this.options.eventBus) {
this.unregisterEventListeners(this.options.eventBus);
}
// Stop process metrics tracking
if (this.processMetricsTimer) {
clearInterval(this.processMetricsTimer);
this.processMetricsTimer = undefined;
}
// Take final sample
this.takeSample();
// Persist metrics if configured
if (this.options.persistMetrics && this.options.memoryConnector) {
this.persistMetricsToMemory();
}
this.isTracking = false;
return this.metrics;
}
/**
* Record a method call with optimized overhead
*/
recordMethodCall(methodName) {
if (!this.isTracking || !this.options.trackMethodCalls) {
return () => { }; // No-op if not tracking
}
const startTime = performance.now();
this.metrics.methodCallCount[methodName] = (this.metrics.methodCallCount[methodName] || 0) + 1;
// Add to execution trace if enabled
this.addTraceEntry({
timestamp: Date.now(),
type: 'method_call',
target: methodName
});
// Return a function to call when method execution completes
return () => {
const endTime = performance.now();
const duration = endTime - startTime;
// Only track if duration exceeds minimum threshold
if (duration >= this.options.minimumDurationToTrack) {
// Update method duration stats
if (!this.metrics.methodDurations[methodName]) {
this.metrics.methodDurations[methodName] = 0;
}
this.metrics.methodDurations[methodName] += duration;
// Add method return to trace
this.addTraceEntry({
timestamp: Date.now(),
type: 'method_return',
target: methodName,
duration
});
// Check for slow method warning
if (duration > this.options.methodDurationWarningMs) {
console.warn(`Flow method ${methodName} took ${duration.toFixed(2)}ms, which exceeds warning threshold of ${this.options.methodDurationWarningMs}ms`);
}
}
};
}
/**
* Record a state change with minimal overhead
*/
recordStateChange(stateChanges) {
if (!this.isTracking || !this.options.trackStateChanges)
return;
this.metrics.stateChanges++;
// Add to execution trace if enabled
this.addTraceEntry({
timestamp: Date.now(),
type: 'state_change',
snapshot: Object.keys(stateChanges)
});
}
/**
* Record a route evaluation with minimal overhead
*/
recordRouteEvaluation(routeId, result) {
if (!this.isTracking || !this.options.trackRouteEvaluations)
return;
this.metrics.routeEvaluations++;
// Add to execution trace if enabled
this.addTraceEntry({
timestamp: Date.now(),
type: 'route_evaluation',
target: routeId,
snapshot: { result }
});
}
/**
* Record an error with tracking
*/
recordError(error, context) {
if (!this.isTracking)
return;
this.metrics.errorCount++;
// Add to execution trace
this.addTraceEntry({
timestamp: Date.now(),
type: 'error',
target: context,
error
});
}
/**
* Get current metrics snapshot
*/
getMetrics() {
const metrics = {
startTime: this.metrics.startTime,
endTime: Date.now(),
executionTimeMs: this.metrics.endTime - this.metrics.startTime,
methodCallCount: this.metrics.methodCallCount,
methodDurations: this.metrics.methodDurations,
stateChanges: this.metrics.stateChanges,
routeEvaluations: this.metrics.routeEvaluations,
errorCount: this.metrics.errorCount,
eventCount: this.metrics.eventCount,
...(this.metrics.heapUsed !== undefined && {
heapUsed: this.metrics.heapUsed,
heapTotal: this.metrics.heapTotal,
externalMemory: this.metrics.externalMemory,
peakMemoryUsageMb: this.metrics.peakMemoryUsageMb
})
};
return metrics;
}
/**
* Get execution trace
*/
getExecutionTrace() {
return [...this.executionTrace];
}
/**
* Get metric samples for time series analysis
*/
getMetricSamples() {
return [...this.metricSamples];
}
/**
* Clear all collected data
*/
clearData() {
this.executionTrace = [];
this.metricSamples = [];
// Reset metrics except for start time
const startTime = this.metrics.startTime;
this.metrics = {
startTime,
endTime: 0,
executionTimeMs: 0,
methodCallCount: {},
methodDurations: {},
stateChanges: 0,
routeEvaluations: 0,
errorCount: 0,
eventCount: 0
};
}
/**
* Take a snapshot of current metrics
*/
takeSample() {
if (!this.isTracking)
return;
// Get current metrics
const currentMetrics = this.getMetrics();
if (!currentMetrics) {
return;
}
// Take memory snapshot if enabled
let memorySnapshot = undefined;
if (this.options.trackProcessMetrics && typeof process !== 'undefined') {
memorySnapshot = {
heapUsed: process.memoryUsage().heapUsed,
heapTotal: process.memoryUsage().heapTotal,
external: process.memoryUsage().external
};
}
// Add sample
if (this.metricSamples.length < (this.options.maxSamples || 100)) {
this.metricSamples.push({
timestamp: Date.now(),
metrics: currentMetrics,
snapshot: memorySnapshot
});
}
// If we've exceeded max samples, remove oldest samples to maintain window
if (this.metricSamples.length > (this.options.maxSamples || 100)) {
const samplesToRemove = this.metricSamples.length - (this.options.maxSamples || 100);
this.metricSamples.splice(0, samplesToRemove);
}
}
/**
* Add an entry to the execution trace with optimized storage
*/
addTraceEntry(entry) {
if (!this.isTracking)
return;
// Add entry if within limits
if (this.executionTrace.length < (this.options.maxTraceEntries || 1000)) {
this.executionTrace.push(entry);
}
// If we've exceeded max entries, remove oldest entries to maintain window
if (this.executionTrace.length > (this.options.maxTraceEntries || 1000)) {
const entriesToRemove = this.executionTrace.length - (this.options.maxTraceEntries || 1000);
this.executionTrace.splice(0, entriesToRemove);
}
}
/**
* Register event listeners for flow events
*/
registerEventListeners(eventBus) {
// Handler for method events
const handleMethodEvent = (event) => {
this.metrics.eventCount++;
if (event.type === 'flow:method:error') {
this.metrics.errorCount++;
}
};
// Handler for execution events
const handleExecutionEvent = (event) => {
this.metrics.eventCount++;
if (event.type === 'flow:execution:failed') {
this.metrics.errorCount++;
}
};
// Subscribe to events
this.eventHandlers.push(eventBus.subscribe('flow:method:*', handleMethodEvent, {
priorityThreshold: EventPriority.LOW
}), eventBus.subscribe('flow:execution:*', handleExecutionEvent, {
priorityThreshold: EventPriority.LOW
}));
}
/**
* Unregister event listeners
*/
unregisterEventListeners(eventBus) {
// Unsubscribe from all events
this.eventHandlers.forEach(handlerId => {
eventBus.unsubscribe(handlerId);
});
this.eventHandlers = [];
}
/**
* Start tracking process metrics at intervals
*/
startProcessMetricsTracking() {
if (typeof process === 'undefined')
return; // Not in Node.js environment
this.processMetricsTimer = setInterval(() => {
if (!this.isTracking) {
clearInterval(this.processMetricsTimer);
this.processMetricsTimer = undefined;
return;
}
this.takeSample();
}, this.options.processMetricsInterval);
}
/**
* Persist metrics to memory system
*/
async persistMetricsToMemory() {
if (!this.options.memoryConnector)
return;
if (!this.isTracking)
return;
try {
// Get flow ID safely
const flowId = this.flow.id || this.flow.constructor.name;
if (!flowId) {
throw new Error('Flow ID is required for metrics persistence');
}
// Store execution metrics
await this.options.memoryConnector.saveFlowData(flowId, {
type: 'execution_metrics',
data: this.metrics,
timestamp: Date.now()
});
// Store samples if there are enough of them
if (this.metricSamples.length > 5) {
const samplesSubset = this.getRepresentativeSamples(5);
if (samplesSubset.length > 0) {
await this.options.memoryConnector.saveFlowData(flowId, {
type: 'metric_samples',
data: samplesSubset,
timestamp: Date.now()
});
}
}
}
catch (error) {
console.error('Failed to persist execution metrics:', error);
}
}
/**
* Get representative samples from the full set
* Uses an algorithm to select samples distributed across the time range
* with efficient memory usage and optimal performance characteristics
*/
getRepresentativeSamples(count) {
if (count <= 0)
return [];
if (this.metricSamples.length <= count) {
return [...this.metricSamples];
}
const result = [];
// Always include first and last samples if they exist
if (this.metricSamples.length > 0) {
const firstSample = this.metricSamples[0];
if (firstSample) {
result.push(firstSample);
}
}
if (this.metricSamples.length > 1) {
const lastSample = this.metricSamples[this.metricSamples.length - 1];
if (lastSample) {
result.push(lastSample);
}
}
// Select samples at regular intervals
const remaining = count - result.length;
if (remaining > 0 && this.metricSamples.length > 2) {
const step = (this.metricSamples.length - 2) / (remaining + 1);
for (let i = 1; i <= remaining; i++) {
const index = Math.floor(i * step) + 1;
if (index >= 0 && index < this.metricSamples.length) {
const sample = this.metricSamples[index];
if (sample) {
result.push(sample);
}
}
}
}
// Sort by timestamp
return result.sort((a, b) => a.timestamp - b.timestamp);
}
}