Трассировка, отладка и структурный анализ кода
Введение
В данной статье речь пойдет про один из способов создания стека вызовов на этапе выполнения. Возможности, которые описаны в статье следующие:
- Составление структуры используемых классов, функций, файлов.
- Создание стека вызова, с сохранением всех прошлых стеков. Их последовательности вызовов.
- Просматривать состояние Watch-параметров на этапе выполнения.
- Пошаговое выполнение кода.
- Группировать и сортировать полученные стеки, получать «экстремальные» данные.
Основные принципы разработки
Методом представления структуры стеков был выбран обычный подход – отображения в виде дерева. Для этого необходимо два информационных класса. 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
Пример использования макроса _INbool 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).
В деревья классов и файлов встроен функционал синхронизации с главным деревом стека, а также удобные принципы управления. Например, при щелчке на названии класса в дереве классов – сразу же в дереве стека и дереве файлов выделятся все функции данного класса. Аналогично при клике на имени файла выделятся все функции и классы, которые находятся в данном файле.
Возможности работы со стеком
- Добавление Watch-параметров
Как вы заметили, в параметрах узла 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.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
А можно как-нибудь этот механизм использовать в скриптах?
думаю да. Но обычно в скриптах код не разветвлен сильно (если конечно скрипт не в цикле).
К тому же есть неудобство - в скриптах не обрабатывается событие OnChartEvent.
думаю да. Но обычно в скриптах код не разветвлен сильно (если конечно скрипт не в цикле).
К тому же есть неудобство - в скриптах не обрабатывается событие OnChartEvent.
А если мой скрипт использует много разных классов, иерархий классов?
Думаю, что нужно заточить инструмент и под скрипты...
классу СTraceView все равно кто его вызывает. Дерево он сделает и отобразит.
Но у скриптов нерешаемая проблема в обратной связи. Активно работать с деревом не получится.
Уважаемый, sergeev, помогите разобраться!
Не могу воспроизвести даже дерево эксперта из примера, что делаю не так? Он то должен казать:(
Взял mql5-3.zip (последний), распаковал -папку MQH в include\expert\ - индикатор в Индикаторы, EXPERT(пример) в папку Expert.
Да, и в Объект вписал <Trace>.
Все пути исправил, скомпилировал - всё прошло.
А дальше - кидаю Индикатор на график-окно НЕ открывается; кидаю Эксперта - и тут в свойствах его уже НЕТ кнопки "ДА, ОК" лишь "Отмена и Сброс".
Спасибо.
А дальше - кидаю Индикатор на график-окно НЕ открывается; кидаю Эксперта - и тут в свойствах его уже НЕТ кнопки "ДА, ОК" лишь "Отмена и Сброс".
1. индикатор кидать никуда не надо. его эксперт сам кинет.
2. прочитайте мануал. последняя строка поста.