//+------------------------------------------------------------------+
//|                                                    PerfMeter.mqh |
//|                                  Copyright 2026, MetaQuotes Ltd. |
//|                                                     www.mql5.com |
//+------------------------------------------------------------------+
#property strict
//+------------------------------------------------------------------+
//| Structure: SPerfSection                                          |
//| Stores aggregate timing data for one named profiling section.    |
//+------------------------------------------------------------------+
struct SPerfSection
  {
   string name;              // Section name shown in the CSV report
   ulong calls;             // Number of completed measurements
   ulong total_us;          // Total elapsed microseconds
   ulong min_us;            // Fastest observed call
   ulong max_us;            // Slowest observed call
   ulong slow_calls;        // Calls that crossed the threshold
   ulong active_started_us; // Start timestamp for Start()/Stop()
   bool active;            // True while a section is running
  };
//+------------------------------------------------------------------+
//| Class: CPerfMeter                                                |
//| Lightweight profiler for named script and EA code sections.      |
//+------------------------------------------------------------------+
class CPerfMeter
  {
private:
   SPerfSection m_sections[];
   long m_slow_threshold_us;
//+------------------------------------------------------------------+
//| Find section by name                                             |
//+------------------------------------------------------------------+
   int FindSection(const string name) const
     {
      for(int i = 0; i < ArraySize(m_sections); i++)
        {
         if(m_sections[i].name == name)
            return i;
        }
      return -1;
     }
//+------------------------------------------------------------------+
//| Return existing section or create a new one                      |
//+------------------------------------------------------------------+
   int EnsureSection(const string name)
     {
      int index = FindSection(name);
      if(index >= 0)
         return index;

      //--- New section names become stable CSV keys for later regression checks.
      int size = ArraySize(m_sections);
      ArrayResize(m_sections, size + 1);
      m_sections[size].name = name;
      m_sections[size].calls = 0;
      m_sections[size].total_us = 0;
      m_sections[size].min_us = 0;
      m_sections[size].max_us = 0;
      m_sections[size].slow_calls = 0;
      m_sections[size].active_started_us = 0;
      m_sections[size].active = false;
      return size;
     }
//+------------------------------------------------------------------+
//| Select per-call threshold                                        |
//+------------------------------------------------------------------+
   long EffectiveThreshold(const long override_threshold_us) const
     {
      if(override_threshold_us >= 0)
         return override_threshold_us;
      return m_slow_threshold_us;
     }

public:
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
   CPerfMeter(void)
     {
      m_slow_threshold_us = 0;
     }
//+------------------------------------------------------------------+
//| Reset all profiler sections                                      |
//+------------------------------------------------------------------+
   void Reset(void)
     {
      ArrayResize(m_sections, 0);
     }
//+------------------------------------------------------------------+
//| Set slow-call threshold in microseconds                          |
//+------------------------------------------------------------------+
   void SetSlowCallThreshold(const long threshold_us)
     {
      m_slow_threshold_us = threshold_us;
     }
//+------------------------------------------------------------------+
//| Return current slow-call threshold                               |
//+------------------------------------------------------------------+
   long SlowCallThreshold(void) const
     {
      return m_slow_threshold_us;
     }
//+------------------------------------------------------------------+
//| Return number of tracked sections                                |
//+------------------------------------------------------------------+
   int SectionCount(void) const
     {
      return ArraySize(m_sections);
     }
//+------------------------------------------------------------------+
//| Start measuring a named section                                  |
//+------------------------------------------------------------------+
   bool Start(const string section_name)
     {
      //--- Empty names cannot be matched safely in a baseline/current comparison.
      if(section_name == "")
         return false;

      int index = EnsureSection(section_name);
      m_sections[index].active_started_us = GetMicrosecondCount();
      m_sections[index].active = true;
      return true;
     }
//+------------------------------------------------------------------+
//| Stop measuring a named section                                   |
//+------------------------------------------------------------------+
   ulong Stop(const string section_name, const long slow_threshold_us = -1)
     {
      int index = FindSection(section_name);
      //--- Ignore unmatched Stop() calls so instrumentation cannot break the EA.
      if(index < 0 || !m_sections[index].active)
         return 0;

      ulong now_us = GetMicrosecondCount();
      ulong elapsed_us = now_us - m_sections[index].active_started_us;
      m_sections[index].active = false;
      RecordElapsed(section_name, elapsed_us, slow_threshold_us);
      return elapsed_us;
     }
//+------------------------------------------------------------------+
//| Manually record an elapsed time                                  |
//+------------------------------------------------------------------+
   bool RecordElapsed(const string section_name,
                      const ulong elapsed_us,
                      const long slow_threshold_us = -1)
     {
      //--- Section names are the public identity of profiler rows.
      if(section_name == "")
         return false;

      int index = EnsureSection(section_name);
      SPerfSection row = m_sections[index];

      row.calls++;
      row.total_us += elapsed_us;

      if(row.calls == 1 || elapsed_us < row.min_us)
         row.min_us = elapsed_us;
      if(elapsed_us > row.max_us)
         row.max_us = elapsed_us;

      long threshold = EffectiveThreshold(slow_threshold_us);
      //--- Slow-call counts are stored separately from average and maximum time.
      if(threshold > 0 && elapsed_us >= (ulong)threshold)
         row.slow_calls++;

      m_sections[index] = row;
      return true;
     }
//+------------------------------------------------------------------+
//| Write profiler results as CSV                                    |
//+------------------------------------------------------------------+
   bool WriteCsvReport(const string file_name, const bool common_file = false)
     {
      //--- A missing file name would create an unreadable report contract.
      if(file_name == "")
         return false;

      int flags = FILE_WRITE | FILE_CSV | FILE_ANSI;
      if(common_file)
         flags |= FILE_COMMON;

      int handle = FileOpen(file_name, flags, ',');
      if(handle == INVALID_HANDLE)
        {
         PrintFormat("CPerfMeter: cannot open report '%s', error=%d", file_name, GetLastError());
         return false;
        }

      //--- Keep the header stable because RegressionGate.mqh reads this shape.
      FileWrite(handle,
                "section",
                "calls",
                "total_us",
                "min_us",
                "max_us",
                "avg_us",
                "slow_calls",
                "threshold_us",
                "status");

      for(int i = 0; i < ArraySize(m_sections); i++)
        {
         double average_us = 0.0;
         if(m_sections[i].calls > 0)
            average_us = (double)m_sections[i].total_us / (double)m_sections[i].calls;

         //--- Store averages with two decimals to keep the report compact.
         FileWrite(handle,
                   m_sections[i].name,
                   (long)m_sections[i].calls,
                   (long)m_sections[i].total_us,
                   (long)m_sections[i].min_us,
                   (long)m_sections[i].max_us,
                   DoubleToString(average_us, 2),
                   (long)m_sections[i].slow_calls,
                   m_slow_threshold_us,
                   (m_sections[i].slow_calls > 0 ? "SLOW" : "OK"));
        }

      FileFlush(handle);
      FileClose(handle);
      return true;
     }
//+------------------------------------------------------------------+
//| Build a short profiler summary string                            |
//+------------------------------------------------------------------+
   string Summary(void) const
     {
      ulong calls = 0;
      ulong slow_calls = 0;
      for(int i = 0; i < ArraySize(m_sections); i++)
        {
         calls += m_sections[i].calls;
         slow_calls += m_sections[i].slow_calls;
        }

      return StringFormat("sections=%d, calls=%d, slow_calls=%d",
                          ArraySize(m_sections),
                          (int)calls,
                          (int)slow_calls);
     }
  };
