//+------------------------------------------------------------------+ //| PerformanceMetricsCollector.mqh - Per-Component Latency Tracking | //+------------------------------------------------------------------+ #ifndef PERFORMANCE_METRICS_COLLECTOR_MQH #define PERFORMANCE_METRICS_COLLECTOR_MQH //+------------------------------------------------------------------+ //| Component latency metric entry | //+------------------------------------------------------------------+ struct SComponentLatencyMetric { string component_name; datetime timestamp; ulong latency_us; // Microseconds ulong count; // Call count bool budget_exceeded; // Whether latency exceeded budget string context; // Additional context (e.g., "gate_G1", "trade_BTCUSD") }; //+------------------------------------------------------------------+ //| Rolling window latency statistics | //+------------------------------------------------------------------+ struct SLatencyStats { string component_name; double avg_latency_us; double max_latency_us; double min_latency_us; double p95_latency_us; ulong total_calls; ulong budget_violations; datetime last_updated; void Reset() { component_name = ""; avg_latency_us = 0.0; max_latency_us = 0.0; min_latency_us = 1e9; p95_latency_us = 0.0; total_calls = 0; budget_violations = 0; last_updated = 0; } }; //+------------------------------------------------------------------+ //| Performance Metrics Collector Class | //+------------------------------------------------------------------+ class CPerformanceMetricsCollector { private: SComponentLatencyMetric m_metrics[]; int m_metrics_count; int m_max_metrics; SLatencyStats m_stats[]; int m_stats_count; ulong m_component_budgets[]; // Per-component budgets in us // Rolling window for P95 calculation (last 100 samples per component) double m_latency_history[][100]; int m_history_index[]; int m_history_count[]; public: // Constructor CPerformanceMetricsCollector(int max_metrics = 1000) { m_metrics_count = 0; m_max_metrics = max_metrics; m_stats_count = 0; ArrayResize(m_metrics, 0); ArrayResize(m_stats, 0); ArrayResize(m_component_budgets, 0); ArrayResize(m_latency_history, 0); ArrayResize(m_history_index, 0); ArrayResize(m_history_count, 0); } //+------------------------------------------------------------------+ //| Register a component with its latency budget | //+------------------------------------------------------------------+ void RegisterComponent(string component_name, ulong budget_us) { int idx = FindOrCreateComponentIndex(component_name); if(idx >= 0) { // Ensure arrays are sized if(idx >= ArraySize(m_component_budgets)) { int old_size = ArraySize(m_component_budgets); ArrayResize(m_component_budgets, idx + 1); ArrayResize(m_latency_history, idx + 1); ArrayResize(m_history_index, idx + 1); ArrayResize(m_history_count, idx + 1); // Initialize new entries for(int i = old_size; i <= idx; i++) { m_component_budgets[i] = 5000; // Default 5ms m_history_index[i] = 0; m_history_count[i] = 0; } } m_component_budgets[idx] = budget_us; // Initialize stats entry if(idx >= m_stats_count) { int old_stats = m_stats_count; m_stats_count = idx + 1; ArrayResize(m_stats, m_stats_count); for(int i = old_stats; i < m_stats_count; i++) m_stats[i].Reset(); } m_stats[idx].component_name = component_name; } } //+------------------------------------------------------------------+ //| Record a latency measurement | //+------------------------------------------------------------------+ void RecordLatency(string component_name, ulong latency_us, string context = "") { // Add to metrics log (if not at capacity) if(m_metrics_count < m_max_metrics) { int idx = m_metrics_count; ArrayResize(m_metrics, m_metrics_count + 1); m_metrics[idx].component_name = component_name; m_metrics[idx].timestamp = TimeCurrent(); m_metrics[idx].latency_us = latency_us; m_metrics[idx].count = 1; m_metrics[idx].context = context; int comp_idx = FindOrCreateComponentIndex(component_name); if(comp_idx >= 0 && comp_idx < ArraySize(m_component_budgets)) { m_metrics[idx].budget_exceeded = (latency_us > m_component_budgets[comp_idx]); } else { m_metrics[idx].budget_exceeded = false; } m_metrics_count++; } // Update rolling statistics UpdateStats(component_name, latency_us); // Update history for P95 UpdateHistory(component_name, latency_us); } //+------------------------------------------------------------------+ //| Get latency statistics for a component | //+------------------------------------------------------------------+ bool GetComponentStats(string component_name, SLatencyStats &out_stats) { int idx = FindComponentIndex(component_name); if(idx < 0 || idx >= m_stats_count) return false; // Calculate P95 from history CalculateP95(idx); out_stats = m_stats[idx]; return true; } //+------------------------------------------------------------------+ //| Check if component exceeded its budget | //+------------------------------------------------------------------+ bool DidExceedBudget(string component_name, ulong latency_us) { int idx = FindComponentIndex(component_name); if(idx < 0 || idx >= ArraySize(m_component_budgets)) return false; return latency_us > m_component_budgets[idx]; } //+------------------------------------------------------------------+ //| Get budget violation report | //+------------------------------------------------------------------+ string GetBudgetViolationReport() { string report = "=== Performance Budget Violations ===\n"; bool has_violations = false; for(int i = 0; i < m_stats_count; i++) { if(m_stats[i].budget_violations > 0) { has_violations = true; report += StringFormat("%s: %d violations (%.2f%% of calls)\n", m_stats[i].component_name, (int)m_stats[i].budget_violations, 100.0 * m_stats[i].budget_violations / MathMax(1, (double)m_stats[i].total_calls)); } } if(!has_violations) report += "No budget violations recorded.\n"; return report; } //+------------------------------------------------------------------+ //| Get full performance report | //+------------------------------------------------------------------+ string GetFullReport() { string report = "=== Performance Metrics Report ===\n"; report += StringFormat("Total components: %d\n", m_stats_count); report += StringFormat("Total measurements: %d\n\n", m_metrics_count); for(int i = 0; i < m_stats_count; i++) { // Calculate P95 CalculateP95(i); report += StringFormat( "%s:\n" " Avg: %.2fms | P95: %.2fms | Min: %.2fms | Max: %.2fms\n" " Calls: %I64u | Budget violations: %I64u\n\n", m_stats[i].component_name, m_stats[i].avg_latency_us / 1000.0, m_stats[i].p95_latency_us / 1000.0, m_stats[i].min_latency_us / 1000.0, m_stats[i].max_latency_us / 1000.0, m_stats[i].total_calls, m_stats[i].budget_violations ); } return report; } //+------------------------------------------------------------------+ //| Clear old metrics (keep last N measurements per component) | //+------------------------------------------------------------------+ void TrimOldMetrics(int keep_per_component = 100) { // Simple implementation: if we exceed max, clear half if(m_metrics_count >= m_max_metrics) { int new_count = m_metrics_count / 2; ArrayResize(m_metrics, new_count); m_metrics_count = new_count; } } private: //+------------------------------------------------------------------+ //| Find or create component index | //+------------------------------------------------------------------+ int FindOrCreateComponentIndex(string component_name) { int idx = FindComponentIndex(component_name); if(idx >= 0) return idx; // Create new entry idx = m_stats_count; m_stats_count++; ArrayResize(m_stats, m_stats_count); m_stats[idx].Reset(); m_stats[idx].component_name = component_name; return idx; } //+------------------------------------------------------------------+ //| Find component index | //+------------------------------------------------------------------+ int FindComponentIndex(string component_name) { for(int i = 0; i < m_stats_count; i++) { if(m_stats[i].component_name == component_name) return i; } return -1; } //+------------------------------------------------------------------+ //| Update statistics for a component | //+------------------------------------------------------------------+ void UpdateStats(string component_name, ulong latency_us) { int idx = FindOrCreateComponentIndex(component_name); if(idx < 0 || idx >= m_stats_count) return; SLatencyStats &stats = m_stats[idx]; // Update running average double old_avg = stats.avg_latency_us; ulong n = stats.total_calls; stats.avg_latency_us = (old_avg * n + latency_us) / (n + 1); // Update min/max stats.max_latency_us = MathMax(stats.max_latency_us, latency_us); stats.min_latency_us = MathMin(stats.min_latency_us, latency_us); // Increment call count stats.total_calls++; // Check budget violation if(idx < ArraySize(m_component_budgets) && latency_us > m_component_budgets[idx]) stats.budget_violations++; stats.last_updated = TimeCurrent(); } //+------------------------------------------------------------------+ //| Update latency history for P95 calculation | //+------------------------------------------------------------------+ void UpdateHistory(string component_name, ulong latency_us) { int idx = FindOrCreateComponentIndex(component_name); if(idx < 0 || idx >= ArraySize(m_history_index)) return; // Store in circular buffer int h_idx = m_history_index[idx]; m_latency_history[idx][h_idx] = (double)latency_us; m_history_index[idx] = (h_idx + 1) % 100; if(m_history_count[idx] < 100) m_history_count[idx]++; } //+------------------------------------------------------------------+ //| Calculate P95 for a component | //+------------------------------------------------------------------+ void CalculateP95(int component_idx) { if(component_idx < 0 || component_idx >= ArraySize(m_history_count)) return; int count = m_history_count[component_idx]; if(count == 0) { m_stats[component_idx].p95_latency_us = 0.0; return; } // Copy and sort double sorted[]; ArrayResize(sorted, count); ArrayCopy(sorted, m_latency_history[component_idx], 0, 0, count); ArraySort(sorted); // Get P95 index int p95_idx = (int)MathFloor(count * 0.95); if(p95_idx >= count) p95_idx = count - 1; m_stats[component_idx].p95_latency_us = sorted[p95_idx]; } }; // Global instance CPerformanceMetricsCollector* g_performance_collector = NULL; //+------------------------------------------------------------------+ //| Initialize performance collector | //+------------------------------------------------------------------+ bool InitializePerformanceCollector() { if(g_performance_collector != NULL) delete g_performance_collector; g_performance_collector = new CPerformanceMetricsCollector(10000); // Register standard components with budgets g_performance_collector.RegisterComponent("OnTick", 5000); // 5ms g_performance_collector.RegisterComponent("OnTimer", 10000); // 10ms g_performance_collector.RegisterComponent("GateEvaluation", 2000); // 2ms per gate g_performance_collector.RegisterComponent("TradeExecution", 5000); // 5ms g_performance_collector.RegisterComponent("PolicyUpdate", 5000); // 5ms g_performance_collector.RegisterComponent("FeatureCache", 1000); // 1ms g_performance_collector.RegisterComponent("PositionSync", 3000); // 3ms LOG("[PerformanceCollector] Initialized with component budgets"); return true; } //+------------------------------------------------------------------+ //| Shutdown performance collector | //+------------------------------------------------------------------+ void ShutdownPerformanceCollector() { if(g_performance_collector != NULL) { delete g_performance_collector; g_performance_collector = NULL; } } //+------------------------------------------------------------------+ //| Convenience macros for timing | //+------------------------------------------------------------------+ #define PERF_START(name) \ ulong __perf_start_##name = GetMicrosecondCount(); #define PERF_END(name) \ do { \ if(g_performance_collector != NULL) { \ g_performance_collector.RecordLatency(#name, GetMicrosecondCount() - __perf_start_##name); \ } \ } while(0) #define PERF_END_CTX(name, context) \ do { \ if(g_performance_collector != NULL) { \ g_performance_collector.RecordLatency(#name, GetMicrosecondCount() - __perf_start_##name, context); \ } \ } while(0) #endif // PERFORMANCE_METRICS_COLLECTOR_MQH