Трассировка, отладка и структурный анализ кода

--- | 16 марта, 2011


Введение

В данной статье речь пойдет про один из способов создания стека вызовов на этапе выполнения. Возможности, которые описаны в статье следующие:


Основные принципы разработки

Методом представления структуры стеков был выбран обычный подход – отображения в виде дерева. Для этого необходимо два информационных класса. CNode «узел», в которые записываются все данные стека. CTreeCtrl «дерево», которое обрабатывает все узлы. И сам трейсер CTraceCtrl, который обрабатывает деревья.

Классы реализованы следующей иерархией:


Классы CNodeBase, CTreeBase описывают базовые свойства и методы работы с узлами и деревьями.

Класс-потомок CNode расширяет базовый функционал CNodeBase, а класс CTreeBase работает именно с потомком CNode. Это сделано из-за того, что класс CNodeBase является родителем других типовых узлов и обособлен для удобства иерархии и наследования в самостоятельный класс.

В отличие от CTreeNode из стандартной библиотеки, в классе CNodeBase находится массив указателей на узлы, то есть "веток", выходящих из данного узла, может быть сколь угодно много.

Классы CNodeBase, CNode

class CNode; // forward declaration
//------------------------------------------------------------------    class CNodeBase
class CNodeBase
  {
public:
   CNode   *m_next[]; // список узлов, на которые он указывает
   CNode   *m_prev; // узел родитель
   int     m_id; // уникальный номер
   string  m_text; // текст

public:
          CNodeBase() { m_id=0; m_text=""; } // конструктор
          ~CNodeBase(); // деструктор
  };

//------------------------------------------------------------------    class CNode
class CNode : public CNodeBase
  {
public:
   bool    m_expand; // раcкрытость
   bool    m_check; // отмеченный точкой
   bool    m_select; // подсвеченный

   //--- run-time информация
   int     m_uses; // число обращений к узлу
   long    m_tick; // время проведенное в узле
   long    m_tick0; // время входа в узел
   datetime    m_last; // время входа в узел
   tagWatch   m_watch[]; // список параметров имя/значение
   bool    m_break; // дебаг-пауза

   //--- параметры вызова
   string   m_file; // имя файла
   int      m_line; // номер линии в файле
   string   m_class; // имя класса
   string   m_func; // имя функции
   string   m_prop; // доп.инфо

public:
           CNode(); // конструктор
           ~CNode(); // деструктор
   void    AddWatch(string watch,string val);
  };

Реализацию всех классов вы можете видеть в приложенных файлах. В тексте будем показывать только их заголовки и важные функции.

По принятой классификации CTreeBase представляет собой ориентированный ацикличный граф. Класс-потомок CTreeCtrl использует CNode и обслуживает весь его функционал: добавляет, изменяет, удаляет узлы CNode.

CTreeCtrl и CNode вполне могли бы заменить соответствующие классы из стандартной библиотеки, так как имеют чуть большие возможности.

Классы CTreeBase, CTreeCtrl

//------------------------------------------------------------------    class CTreeBase
class CTreeBase
  {
public:
   CNode   *m_root; // первый узел дерева
   int     m_maxid;    // счетчик ID

   //--- базовые функции
public:
           CTreeBase(); // конструктор
           ~CTreeBase(); // деструктор
   void    Clear(CNode *root=NULL); // удаления всех узлов после указанного
   CNode   *FindNode(int id,CNode *root=NULL); // поиск узла по ID, начиная с заданного узла
   CNode   *FindNode(string txt,CNode *root=NULL); // поиск узла по txt, начиная с заданного узла
   int     GetID(string txt,CNode *root=NULL); // получение ID для указанного Text, поиск с заданного узла
   int     GetMaxID(CNode *root=NULL); // получение максимального ID в дереве
   int     AddNode(int id,string text,CNode *root=NULL); // добавление в список с поиском по ID узла, начиная с заданного узла
   int     AddNode(string txt,string text,CNode *root=NULL); // добавление в список с поиском по тексту узла, начиная с заданного узла
   int     AddNode(CNode *root,string text); // добавление под root
  };

//------------------------------------------------------------------    class CTreeCtrl
class CTreeCtrl : public CTreeBase
  {
   //--- базовые функции
public:
          CTreeCtrl() { m_root.m_file="__base__"; m_root.m_line=0; 
                        m_root.m_func="__base__"; m_root.m_class="__base__"; } // конструктор
          ~CTreeCtrl() { delete m_root; m_maxid=0; } // деструктор
   void    Reset(CNode *root=NULL); // сброс состояния всех узлов
   void    SetDataBy(int mode,int id,string text,CNode *root=NULL); // изменение текста для указанного ID, поиск с заданного узла
   string  GetDataBy(int mode,int id,CNode *root=NULL); // получение текста для указанного ID, поиск с заданного узла

   //--- обработка состояния
public:
   bool    IsExpand(int id,CNode *root=NULL); // получение свойства m_expand для указанного ID, поиск с заданного узла
   bool    ExpandIt(int id,bool state,CNode *root=NULL); // меняем состояние m_expand, поиск с заданного узла
   void    ExpandBy(int mode,CNode *node,bool state,CNode *root=NULL); // раксрываем узлы только указанного

   bool    IsCheck(int id,CNode *root=NULL); // получение свойства m_check для указанного ID, поиск с заданного узла
   bool    CheckIt(int id,bool state,CNode *root=NULL); // меняем состояние m_check на требуемую, начиная с заданного узла
   void    CheckBy(int mode,CNode *node,bool state,CNode *root=NULL); // отмечаем все дерево

   bool    IsSelect(int id,CNode *root=NULL); // получение свойства m_select для указанного ID, поиск с заданного узла
   bool    SelectIt(int id,bool state,CNode *root=NULL); // меняем состояние m_select на требуемую, начиная с заданного узла
   void    SelectBy(int mode,CNode *node,bool state,CNode *root=NULL); // подсвечиваем все дерево

   bool    IsBreak(int id,CNode *root=NULL); // получение свойства m_break для указанного ID, поиск с заданного узла
   bool    BreakIt(int id,bool state,CNode *root=NULL); // меняем состояние m_break, поиск с заданного узла
   void    BreakBy(int mode,CNode *node,bool state,CNode *root=NULL); // задаем только для указанного 

   //--- операции с нодами
public:
   void    SortBy(int mode,bool ascend,CNode *root=NULL); // сортировка по свойству
   void    GroupBy(int mode,CTreeCtrl *atree,CNode *node=NULL); // группировка по свойству
  };

И завершают архитектуру два класса: CTraceCtrl, единственный экземпляр которого непосредственно используется  для трассировки и содержит три экземпляра класса CTreeCtrl для создания требуемой структуры функций и временный контейнер  - класс CIn. Это чисто вспомогательный класс, который будет добавлять новые узлы в CTraceCtrl.

Классы CTraceCtrl, CIn

class CTraceView; // предварительное объявление
//------------------------------------------------------------------    class CTraceCtrl
class CTraceCtrl
  {
public:
   CTreeCtrl   *m_stack; // объект графа
   CTreeCtrl   *m_info; // объект графа
   CTreeCtrl   *m_file; // группировка по файлам
   CTreeCtrl   *m_class; // группировка по классам
   CTraceView  *m_traceview; // указатель на отображение класса

   CNode   *m_cur; // указатель на текущий узел
           CTraceCtrl() { Create(); Reset(); } // создали трейсер
           ~CTraceCtrl() { delete m_stack; delete m_info; delete m_file; delete m_class; } // удалили трейсер
   void    Create(); // создали трейсер
   void    In(string afile,int aline,string aname,int aid); // вход в указанный узел 
   void    Out(int aid); // выход до указанного узла
   bool    StepBack(); // выход из узла на один шаг вверх (переход на родителя)
   void    Reset() { m_cur=m_stack.m_root; m_stack.Reset(); m_file.Reset(); m_class.Reset(); } // сброс всех узлов
   void    Clear() { m_cur=m_stack.m_root; m_stack.Clear(); m_file.Clear(); m_class.Clear(); } // сброс всех узлов

public:
   void    AddWatch(string name,string val); // проверка дебаг режима для узла
   void    Break(); // пауза для узла
  };

//------------------------------------------------------------------    CIn
class CIn
  {
public:
   void In(string afile,int aline,string afunc)
     {
      if(NIL(m_trace)) return; // если нет графа, то выходим
      if(NIL(m_trace.m_tree)) return;
      if(NIL(m_trace.m_tree.m_root)) return;
      if(NIL(m_trace.m_cur)) m_trace.m_cur=m_trace.m_tree.m_root;
      m_trace.In(afile,aline,afunc,-1); // вошли в следующий
     }
   void ~CIn() { if(!NIL(m_trace)) m_trace.Out(-1); } // вышли выше
  };


Модель работы класса CIn

На этот класс возлагается основная забота о создании дерева стека.

Построение графа происходит по шагам в два этапа с использованием двух функций CTraceCtrl:

void In(string afile, int aline, string aname, int aid); // вход в указанный узел
void Out(int aid);  // выход до указанного узла

Другими словами для построения дерева происходит постоянный вызов In-Out-In-Out-In-In-Out-Out и т.д.

Работа пары In-Out выглядит так:

1. Вход в блок (функцию, цикл, условие и т.д.), то есть сразу после скобки «{».
При входе в блок создается новый экземпляр CIn, который получает текущий CTraceCtrl, уже начатый с какими-то предыдущими узлами. В CIn вызывается функция CTraceCtrl::In и она создает новый узел в стеке. Узел создается сразу под текущим узлом CTraceCtrl::m_cur.  В новый узел заносится вся актуальная информация о входе: имя файла, номер строки, имя класса, функции, текущее время и т.д.

2. Выход из блока при скобке «}».
При выходе из блока – MQL автоматически вызывает деструктор CIn::~CIn. В деструкторе вызывается функция CTraceCtrl::Out. Указатель текущего узла CTraceCtrl::m_cur поднимается в дереве на уровень выше. При этом деструктор для нового узла не вызывается, узел остается в дереве.

Схема построения стека


Построение стека вызовов в виде дерева с заполнением всех данных вызова происходит при помощи контейнера CIn.



Макросы для упрощения вызова

Чтоб не прописывать в вашем коде длинные строки создания объекта CIn и входа в узел, удобно заменить её вызов макросом:
#define _IN    CIn _in; _in.In(__FILE__, __LINE__, __FUNCTION__)
Как видите, происходит создание объекта CIn и последующий вход в узел.


В связи с тем, что MQL выдаёт предупреждение в случае совпадения имен локальных и внешних переменных – лучше (аккуратнее и чище) создать аналогично 3-4 определения с другими именами переменных в таком виде:

#define _IN1    CIn _in1; _in1.In(__FILE__, __LINE__, __FUNCTION__)
#define _IN2    CIn _in2; _in2.In(__FILE__, __LINE__, __FUNCTION__)
#define _IN3    CIn _in3; _in3.In(__FILE__, __LINE__, __FUNCTION__)
По мере захода в подблоки нужно использовать последующие макросы _INx
bool CSampleExpert::InitCheckParameters(int digits_adjust)
  { _IN;
//--- initial data checks
   if(InpTakeProfit*digits_adjust<m_symbol.StopsLevel())
     { _IN1;
      printf("Take Profit must be greater than %d",m_symbol.StopsLevel());

С появлением в 411 билде макросов, можно полноценно использовать передачу параметров в #define.

Поэтому в классе CTraceCtrl вы встретите такое специальное макро-определение:

#define NIL(p)    (CheckPointer(p)==POINTER_INVALID)

Оно позволяет укоротить проверку валидности указателя.

Например, строка:

if (CheckPointer(m_tree))==POINTER_INVALID || CheckPointer(m_cur))==POINTER_INVALID) return;  

заменяется на более короткий вариант

if (NIL(m_tree) || NIL(m_cur)) return;



Подготовка ваших файлов к трассировке

Для контроля и получения стека необходимо сделать три шага.

1. Добавить в требуемые файлы
#include <Trace.mqh>

На данный момент вся стандартная библиотека основана на классе CObject. Поэтому если и в ваших классах он тоже является базовым, то будет достаточно подключить Trace.mqh  только в Object.mqh.

2. Расставить в требуемых блоках (можно через поиск-замена) макросы _IN

Пример использования макроса _IN
bool CSampleExpert::InitCheckParameters(int digits_adjust)
  { _IN;
//--- initial data checks
   if(InpTakeProfit*digits_adjust<m_symbol.StopsLevel())
     { _IN1;
      printf("Take Profit must be greater than %d",m_symbol.StopsLevel());


3. В главном модуле программы в функциях OnInit, OnTime, OnDeinit прописать создание, обновление и удаление глобального объекта CTraceCtrl соответственно. Ниже показан уже готовый код для вставки:

Внедрение трассировщика в основной код

//------------------------------------------------------------------    OnInit
int OnInit()
  {
   //****************
   m_traceview= new CTraceView; // создали отображение граф
   m_trace= new CTraceCtrl; // создали граф
   m_traceview.m_trace=m_trace; // присоединили граф
   m_trace.m_traceview=m_traceview; // присоединили отображение графа
   m_traceview.Create(ChartID()); // создали чарт
   //****************
   // остальной ваш код…
   return(0);
  }
//------------------------------------------------------------------    OnDeinit
void OnDeinit(const int reason)
  {
   //****************
   delete m_traceview;
   delete m_trace;
   //****************
   // остальной ваш код…
  }
//------------------------------------------------------------------    OnTimer
void OnTimer()
  {
   //****************
   if (m_traceview.IsOpenView(m_traceview.m_chart)) m_traceview.OnTimer();
   else { m_traceview.Deinit(); m_traceview.Create(ChartID()); } // если окно было случайно закрыто
   //****************
   // остальной ваш код…
  }
//------------------------------------------------------------------    OnChartEvent
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   //****************
   m_traceview.OnChartEvent(id, lparam, dparam, sparam);
   //****************
   // остальной ваш код…
  }


Классы отображения трассировки

Итак, организация стека выполнена. Давайте теперь рассмотрим отображение получаемой информации.

Для этого мы создали два класса. CTreeView – для отображения дерева, и CTraceView – для контроля отображения деревьев и дополнительной информации по стеку. Оба класса являются потомками базового CView.


Классы CTreeView, CTraceView

//------------------------------------------------------------------    class CTreeView
class CTreeView: public CView
  {
   //--- базовые функции
public:
           CTreeView(); // конструктор
           ~CTreeView(); // деструктор
   void    Attach(CTreeCtrl *atree); // присоединили объект дерева для его отображения
   void    Create(long chart,string name,int wnd,color clr,color bgclr,color selclr,
                    int x,int y,int dx,int dy,int corn=0,int fontsize=8,string font="Arial");

   //--- функции обработки состояния
public:
   CTreeCtrl        *m_tree; // указатель на объект дерева для отображения
   int     m_sid; // последний выделенный объект (для подсветки)
   int     OnClick(string name); // обработка события клика на объекте

   //--- функции отображения
public:
   int     m_ndx, m_ndy; // размер отступов от кнопки для отрисовки
   int     m_bdx, m_bdy; // размер кнопки узлов
   CScrollView       m_scroll;
   bool    m_bProperty; // показывать свойства рядом с узлом

   void    Draw(); // обновить вид
   void    DrawTree(CNode *first,int xpos,int &ypos,int &up,int &dn); // перерисовать
   void    DeleteView(CNode *root=NULL,bool delparent=true); // удалить все элементы отображения, начиная с заданного узла
  };

//------------------------------------------------------------------    class CTreeView
class CTraceView: public CView
  {
   //--- базовые функции
public:
           CTraceView() { }; // конструктор
           ~CTraceView() { Deinit(); } // деструтор
   void    Deinit(); // полная деинициализация представления
   void    Create(long chart); // создаем и активируем представление

   //--- функции обработки состояния
public:
   int     m_hagent; // хендлер индикатора-агента для отправки сообщений
   CTraceCtrl   *m_trace; // указатель на созданные трейсер
   CTreeView    *m_viewstack; // дерево для отображения стека
   CTreeView    *m_viewinfo; // дерево для отображения свойств узла
   CTreeView    *m_viewfile; // дерево для отображения стека с группировкой по файлам
   CTreeView    *m_viewclass; // дерево для отображения стека с группировкой по классам
   void    OnTimer(); // обработчик таймера
   void    OnChartEvent(const int,const long&,const double&,const string&); // обработчик события

   //--- функции отображения
public:
   void    Draw(); // обновили объекты
   void    DeleteView(); // удалили отображение

   void    UpdateInfoTree(CNode *node,bool bclear); // вывод окна подробной информации узла
   string  TimeSeparate(long time); // спецфункция для преобразования времени в строку
  };

По оптимальному варианту было решено выполнить представление стека в отдельном окне.

То есть, класс CTraceView при создании в функции CTraceView::Create открывает окно чарта, и все объекты рисуются именно на нем, хотя сам CTraceView создан и работает в эксперте в другом окне. Это сделано для того, чтобы большой объем информации не мешал работе исходного кода трассируемой программы и её выводу собственной информации на чарт.

Но для того, чтобы два окна могли «общаться» между собой, нам необходимо добавить на окно с объектами индикатор, который будет отправлять все события пользователя в базовое окно трассируемой программы.

Индикатор создается в той же функции CTraceView::Create. У него всего один внешний параметр – ID чарта, на который надо отправлять все события.

Индикатор TraceAgent

#property indicator_chart_window
input long cid=0; // чарт получателя
//------------------------------------------------------------------    OnCalculate
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double& price[])
{ return(rates_total); }
//------------------------------------------------------------------    OnChartEvent
void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
    EventChartCustom(cid, (ushort)id, lparam, dparam, sparam);
  }

В результате получилось довольно структурированное представление стека.


В дереве слева TRACE отображается исходный полученный стек.

Под ним находится окно расширенной информации INFO по выделенному узлу (в текущем примере CTraceView::OnChartEvent). Два соседних окна с деревьями отображают этот же стек, но сгруппированный по классам (среднее дерево CLASS) и файлам (правое дерево FILE).

В деревья классов и файлов встроен функционал синхронизации с главным деревом стека, а также удобные принципы управления. Например, при щелчке на названии класса в дереве классов – сразу же в дереве стека и дереве файлов выделятся все функции данного класса. Аналогично при клике на имени файла выделятся все функции и классы, которые находятся в данном файле.



Этот механизм позволяет быстро выделять и просматривать требуемые группы функций.



Возможности работы со стеком

Как вы заметили, в параметрах узла CNode имеется массив структур tagWatch. Она создана только для удобства представления информации. В ней хранится именованное значение переменной или выражения.

Структура Watch-значения

//------------------------------------------------------------------    struct tagWatch
struct tagWatch
{
    string m_name;     // имя
    string m_val;    // значение
};

Для добавления нового Watch-значения в текущий узел нужно вызвать функцию CTrace::AddWatch, или воспользоваться макросом _WATCH

#define _WATCH(w, v)         if (!NIL(m_trace) && !NIL(m_trace.m_cur)) m_trace.m_cur.AddWatch(w, string(v));


Специальным ограничением на добавляемые значения (по аналогии с узлами) есть отслеживание уникальности их имён. Это значит, что название Watch-значения проверяется на уникальность перед его добавлением в массив CNode::m_watch[]. Если в массиве значение с таким именем уже есть, то добавления не происходит, а только обновляется это значение.

Все отслеживаемые Watch-значения отображаются в информационном окне.



Еще одна удобная возможность, которую дает MQL5 - это организация принудительной паузы в коде в процессе выполнения.

Пауза организуется простым бесконечным циклом while (true). Удобство MQL5 здесь именно в отработке события выхода из этого цикла – ожидание клика на управляющей красной кнопке. Для создания точки остановки в процессе выполнения воспользуйтесь функцией CTrace::Break.


Функция для организации точки остановки

//------------------------------------------------------------------    Break
void CTraceCtrl::Break() // проверка дебаг режима для узла
  {
   if(NIL(m_traceview)) return; // проверка валидности
   m_stack.BreakBy(TG_ALL,NULL,false); // убрали флаги m_break со всех ущлов
   m_cur.m_break=true; // активировали только на текущем
   m_traceview.m_viewstack.m_sid=m_cur.m_id; // перенесли выделение на него
   m_stack.ExpandBy(TG_UP,m_cur,true,m_cur); // раскрыли родительские узлы, если закрыты
   m_traceview.Draw(); // отрисовали все
   string name=m_traceview.m_viewstack.m_name+string(m_cur.m_id)+".dbg"; // получили имя кнопки BREAK
   bool state=ObjectGetInteger(m_traceview.m_chart,name,OBJPROP_STATE);
   while(!state) // пока кнопка не нажата, выполняем цикл
     {
      Sleep(1000); // сделали паузу
      state=ObjectGetInteger(m_traceview.m_chart,name,OBJPROP_STATE);  // проверяем её состояние
      if(!m_traceview.IsOpenView()) break; // если окно закрыто, то выходим
      m_traceview.Draw(); // отрисовали возможные изменения
     }
   m_cur.m_break=false; // убрали флаг
   m_traceview.Draw(); // отрисовали обновление
  }


При попадании на такую точку остановки деревья стека синхронизируются так, чтоб отобразить функцию, вызвавшей данный макрос. Если узел был свернут, то родительский узел раскрывается, чтоб отобразить её. А при необходимости дерево прокручивается вверх или вниз, чтоб поднять узел в область видимости.

Для выхода из CTraceCtrl::Break надо кликнуть на красную кнопку возле имени узла.


Послесловие

Итак, "игрушка" получилась интересная. За время, пока создавалась статья, опробовали очень много вариантов работы CTraceCtrl, и убедились в который раз, что в MQL5 имеются уникальные перспективы контроля экспертов и организации работы. Все эти "фичи", использованные в разработке трейсера невозможны в MQL4, что еще раз доказывает превосходство языка MQL5 и его широкие возможности.

В приложенных кодах вы найдете все классы, которые описаны в статье, плюс сервисные библиотеки (даются в минимальном требуемом объеме, так как они не самоцель). А также выкладываем в качестве готового примера - обновленные файлы из стандартной библиотеки, в которых просто расставлены макросы _IN. Все опыты проводились над экспертом из поставки MetaTrader 5 - MACD Sample.mq5.