Rastreo, Depuración y Análisis Estructural de Código Fuente

--- | 31 marzo, 2014


Introducción

Este artículo trata sobre uno de los métodos de creación de una serie de llamadas durante la ejecución. En el artículo se explicarán las siguientes prestaciones:


Principios de Desarrollo

Hemos elegido un enfoque frecuente como método para la representación de la estructura, mostrándola en forma de árbol. Para ello, necesitaremos dos clases informacionales. CNode - un "nodo" usado para escribir toda la información sobre una serie. CTreeCtrl - un "árbol" que procesa todos los nodos. Y el rastreador mismo - CTraceCtrl, usado para procesar árboles.

Las clases se implementan de acuerdo con la siguiente jerarquía:


Las clases CNodeBase y CTreeBase describen las propiedades y métodos básicos para trabajar con nodos y árboles.

La clase heredada CNode extiende la funcionalidad básica de CNodeBase, y la clase CTreeBase funciona con la clase derivada CNode. Se hace así porque la clase CNodeBase es la progenitora de los otros nodos estándar, y está aislada como una clase independiente para la conveniencia de la jerarquía y herencia.

A diferencia de CTreeNode de la biblioteca estándar, la clase CNodeBase contiene un array de punteros para nodos, por ello el número de "ramas" que salen de este nodo es ilimitado.

Las Clases CNodeBase y CNode

class CNode; // forward declaration
//------------------------------------------------------------------    class CNodeBase
class CNodeBase
  {
public:
   CNode   *m_next[]; // list of nodes it points to
   CNode   *m_prev; // parent node
   int     m_id; // unique number
   string  m_text; // text

public:
          CNodeBase() { m_id=0; m_text=""; } // constructor
          ~CNodeBase(); // destructor
  };

//------------------------------------------------------------------    class CNode
class CNode : public CNodeBase
  {
public:
   bool    m_expand; // expanded
   bool    m_check; // marked with a dot
   bool    m_select; // highlighted

   //--- run-time information
   int     m_uses; // number of calls of the node
   long    m_tick; // time spent in the node
   long    m_tick0; // time of entering the node
   datetime    m_last; // time of entering the node
   tagWatch   m_watch[]; // list of name/value parameters
   bool    m_break; // debug-pause

   //--- parameters of the call
   string   m_file; // file name
   int      m_line; // number of row in the file
   string   m_class; // class name
   string   m_func; // function name
   string   m_prop; // add. information

public:
           CNode(); // constructor
           ~CNode(); // destructor
   void    AddWatch(string watch,string val);
  };

Puede encontrar las implementaciones de todas las clases en los archivos adjuntos. En el artículo mostraremos solo sus encabezamientos y funciones importantes.

Según la clasificación aceptada, CTreeBase representa y orienta el gráfico acíclico. La clase derivada CTreeCtrl usa CNode y facilita toda su funcionalidad: añadir, cambiar y eliminar los nodos de CNode.

CTreeCtrl y CNode puede sustituir efectivamente las clases correspondientes de la biblioteca estándar, puesto que tienen una funcionalidad ligeramente mayor.

Las Clases CTreeBase y CTreeCtrl

//------------------------------------------------------------------    class CTreeBase
class CTreeBase
  {
public:
   CNode   *m_root; // first node of the tree
   int     m_maxid;    // counter of ID

   //--- base functions
public:
           CTreeBase(); // constructor
           ~CTreeBase(); // destructor
   void    Clear(CNode *root=NULL); // deletion of all nodes after a specified one
   CNode   *FindNode(int id,CNode *root=NULL); // search of a node by its ID starting from a specified node
   CNode   *FindNode(string txt,CNode *root=NULL); // search of a node by txt starting from a specified node
   int     GetID(string txt,CNode *root=NULL); // getting ID for a specified Text, the search starts from a specified node
   int     GetMaxID(CNode *root=NULL); // getting maximal ID in the tree
   int     AddNode(int id,string text,CNode *root=NULL); // adding a node to the list, search is performed by ID starting from a specified node
   int     AddNode(string txt,string text,CNode *root=NULL); // adding a node to the list, search is performed by text starting from a specified node
   int     AddNode(CNode *root,string text); // adding a node under root
  };

//------------------------------------------------------------------    class CTreeCtrl
class CTreeCtrl : public CTreeBase
  {
   //--- base functions
public:
          CTreeCtrl() { m_root.m_file="__base__"; m_root.m_line=0; 
                        m_root.m_func="__base__"; m_root.m_class="__base__"; } // constructor
          ~CTreeCtrl() { delete m_root; m_maxid=0; } // destructor
   void    Reset(CNode *root=NULL); // reset the state of all nodes
   void    SetDataBy(int mode,int id,string text,CNode *root=NULL); // changing text for a specified ID, search is started from a specified node
   string  GetDataBy(int mode,int id,CNode *root=NULL); // getting text for a specified ID, search is started from a specified node

   //--- processing state
public:
   bool    IsExpand(int id,CNode *root=NULL); // getting the m_expand property for a specified ID, search is started from a specified node
   bool    ExpandIt(int id,bool state,CNode *root=NULL); // change the m_expand state, search is started from a specified node
   void    ExpandBy(int mode,CNode *node,bool state,CNode *root=NULL); // expand node of a specified node

   bool    IsCheck(int id,CNode *root=NULL); // getting the m_check property for a specified ID, search is started from a specified node
   bool    CheckIt(int id,bool state,CNode *root=NULL); // change the m_check state to a required one starting from a specified node
   void    CheckBy(int mode,CNode *node,bool state,CNode *root=NULL); // mark the whole tree

   bool    IsSelect(int id,CNode *root=NULL); // getting the m_select property for a specified ID, search is started from a specified node
   bool    SelectIt(int id,bool state,CNode *root=NULL); // change the m_select state to a required one starting from a specified node
   void    SelectBy(int mode,CNode *node,bool state,CNode *root=NULL); // highlight the whole tree

   bool    IsBreak(int id,CNode *root=NULL); // getting the m_break property for a specified ID, search is started from a specified node
   bool    BreakIt(int id,bool state,CNode *root=NULL); // change the m_break state, search is started from a specified node
   void    BreakBy(int mode,CNode *node,bool state,CNode *root=NULL); // set only for a selected one 

   //--- operations with nodes
public:
   void    SortBy(int mode,bool ascend,CNode *root=NULL); // sorting by a property
   void    GroupBy(int mode,CTreeCtrl *atree,CNode *node=NULL); // grouping by a property
  };

La arquitectura acaba con dos clases: CTraceCtrl - su única instancia se usa directamente para rastrear; contiene tres instancias de la clase CTreeCtrl para la creación de la estructura requerida de funciones y un contenedor temporal, la clase CIn. Esta es simplemente una clase auxiliar usada para añadir nuevos nodos a CTraceCtrl.

Las Clases CTraceCtrl y CIn

class CTraceView; // provisional declaration
//------------------------------------------------------------------    class CTraceCtrl
class CTraceCtrl
  {
public:
   CTreeCtrl   *m_stack; // object of graph
   CTreeCtrl   *m_info; // object of graph
   CTreeCtrl   *m_file; // grouping by files
   CTreeCtrl   *m_class; // grouping by classes
   CTraceView  *m_traceview; // pointer to displaying of class

   CNode   *m_cur; // pointer to the current node
           CTraceCtrl() { Create(); Reset(); } // tracer created
           ~CTraceCtrl() { delete m_stack; delete m_info; delete m_file; delete m_class; } // tracer deleted
   void    Create(); // tracer created
   void    In(string afile,int aline,string aname,int aid); // entering a specified node 
   void    Out(int aid); // exit from a specified node
   bool    StepBack(); // exit from a node one step higher (going to the parent)
   void    Reset() { m_cur=m_stack.m_root; m_stack.Reset(); m_file.Reset(); m_class.Reset(); } // resetting all nodes
   void    Clear() { m_cur=m_stack.m_root; m_stack.Clear(); m_file.Clear(); m_class.Clear(); } // resetting all nodes

public:
   void    AddWatch(string name,string val); // checking the debug mode for a node
   void    Break(); // pause for a node
  };

//------------------------------------------------------------------    CIn
class CIn
  {
public:
   void In(string afile,int aline,string afunc)
     {
      if(NIL(m_trace)) return; // exit if there is no graph
      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); // entering the next one
     }
   void ~CIn() { if(!NIL(m_trace)) m_trace.Out(-1); } // exiting higher
  };


Modelo de Operación de la Clase CIn

Esta clase se encarga de la creación del árbol de series.

La formación del gráfico se lleva a cabo por pasos en dos fases usando dos funciones CTraceCtrl:

void In(string afile, int aline, string aname, int aid); // entering a specified node
void Out(int aid);  // exit before a specified node

En otras palabras, para formar un árbol se llevan a cabo llamadas continuas a In-Out-In-Out-In-In-Out-Out, etc.

El par In-Out funciona así:

1. Introducir un bloque (función, ciclo, condición, etc.), es decir, justo después del signo "{".
Al introducir el bloque se crea una nueva estancia CIn, y obtiene el CTraceCtrl actual que ya se inició con algunos nodos anteriores. La función CTraceCtrl::In se llama en CIn, y crea un nuevo nodo en la serie. El nodo se crea bajo el nodo actual CTraceCtrl::m_cur. Toda la información actual sobre la introducción se escribe en él: nombre de archivo, número de fila, nombre de clase, funciones, hora actual, etc.

2. Salir del bloque al encontrar un signo "}".
Al salir del bloque, MQL llama automáticamente al destructor CIn::~CIn. En el destructor se llama a CTraceCtrl::Out. El puntero del nodo actual CTraceCtrl::m_cur se sube a un nivel más alto en el árbol. Si el destructor no se llama para el nuevo nodo, el nodo se mantiene en el árbol.

Esquema de Formación de Serie


La formación de la serie de llamadas en forma de árbol llenando toda la información sobre una llamada se lleva a cabo usando el contenedor CIn.



Macros para Hacer las Llamadas más Fáciles

Para evitar reescribir las largas líneas de código para crear el objeto CIn e introducir un nodo en su código, es conveniente sustituirlo con la llamada al macro:
#define _IN    CIn _in; _in.In(__FILE__, __LINE__, __FUNCTION__)
Como puede ver, se crea el objeto CIn y después introducimos el código.


Puesto que MQL da una advertencia en caso de que los nombres de variables locales coincidan con las variables globales, es mejor (más preciso y claro) crear 3-4 definiciones análogas con los otros nombres de variables en la siguiente forma:

#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__)
Conforme entre más profundamente en los sub-bloques, use los siguientes macros _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());

Con la aparición de macros en la versión 411, puede usar libremente la transferencia de parámetros usando #define.

Esta es la razón por la que en la clase CTraceCtrl encontrará la siguiente definición de macro:

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

Permite abreviar la comprobación de validez del puntero.

Por ejemplo, la línea:

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

se sustituye con la variante más corta:

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



Preparar sus Archivos para el Rastreo

Para controlar y obtener la serie, debe seguir tres pasos:

1. Añadir los archivos requeridos
#include <Trace.mqh>

La biblioteca estándar entera se basa en la clase CObject por ahora. Por tanto, se usa como clase base en sus archivos, y es suficiente usar Trace.mqh solo a Object.mqh.

2. Coloque los macros _IN para los bloques requeridos (puede usar search/replace (buscar/sustituir))

Ejemplo de uso del macro _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. En las funciones OnInit, OnTime, y OnDeinit que suponen el módulo principal del programa, añada la creación, modificación y eliminación del objeto global CTraceCtrl respectivamente. Debajo puede encontrar el código ya preparado para su inserción:

Incrustar el rastreador en el código principal

//------------------------------------------------------------------    OnInit
int OnInit()
  {
   //****************
   m_traceview= new CTraceView; // created displaying of the graph
   m_trace= new CTraceCtrl; // created the graph
   m_traceview.m_trace=m_trace; // attached the graph
   m_trace.m_traceview=m_traceview; // attached displaying of the graph
   m_traceview.Create(ChartID()); // created chart
   //****************
   // remaining part of your code…
   return(0);
  }
//------------------------------------------------------------------    OnDeinit
void OnDeinit(const int reason)
  {
   //****************
   delete m_traceview;
   delete m_trace;
   //****************
   // remaining part of your code…
  }
//------------------------------------------------------------------    OnTimer
void OnTimer()
  {
   //****************
   if (m_traceview.IsOpenView(m_traceview.m_chart)) m_traceview.OnTimer();
   else { m_traceview.Deinit(); m_traceview.Create(ChartID()); } // if the window is accidentally closed
   //****************
   // remaining part of your code…
  }
//------------------------------------------------------------------    OnChartEvent
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   //****************
   m_traceview.OnChartEvent(id, lparam, dparam, sparam);
   //****************
   // remaining part of your code…
  }


Clases de Visualización de Rastreo

Así, hemos organizado la serie. Ahora consideremos la visualización de la información obtenida.

Para ello, debemos crear dos clases. CTreeView – para mostrar el árbol, y CTraceView – para controlar la visualización de árboles e información adicional sobre series. Ambas clases son derivadas de la clase baseCView.


Las Clases CTreeView y CTraceView

//------------------------------------------------------------------    class CTreeView
class CTreeView: public CView
  {
   //--- basic functions
public:
           CTreeView(); // constructor
           ~CTreeView(); // destructor
   void    Attach(CTreeCtrl *atree); // attached the tree object for displaying it
   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");

   //--- functions of processing of state
public:
   CTreeCtrl        *m_tree; // pointer to the tree object to be displayed
   int     m_sid; // last selected object (for highlighting)
   int     OnClick(string name); // processing the event of clicking on an object

   //--- functions of displaying
public:
   int     m_ndx, m_ndy; // size of margins from button for drawing
   int     m_bdx, m_bdy; // size of button of nodes
   CScrollView       m_scroll;
   bool    m_bProperty; // show properties near the node

   void    Draw(); // refresh the view
   void    DrawTree(CNode *first,int xpos,int &ypos,int &up,int &dn); // redraw
   void    DeleteView(CNode *root=NULL,bool delparent=true); // delete all displayed elements starting from a specified node
  };

//------------------------------------------------------------------    class CTreeView
class CTraceView: public CView
  {
   //--- base functions
public:
           CTraceView() { }; // constructor
           ~CTraceView() { Deinit(); } // destructor
   void    Deinit(); // full deinitialization of representation
   void    Create(long chart); // create and activate the representation

   //--- function of processing of state
public:
   int     m_hagent; // handler of the indicator-agent for sending messages
   CTraceCtrl   *m_trace; // pointer to created tracer
   CTreeView    *m_viewstack; // tree for displaying the stack
   CTreeView    *m_viewinfo; // tree for displaying of node properties
   CTreeView    *m_viewfile; // tree for displaying of the stack with grouping by files
   CTreeView    *m_viewclass; // tree for displaying of stack with grouping by classes
   void    OnTimer(); // handler of timer
   void    OnChartEvent(const int,const long&,const double&,const string&); // handler of event

   //--- functions of displaying
public:
   void    Draw(); // refresh objects
   void    DeleteView(); // delete the view

   void    UpdateInfoTree(CNode *node,bool bclear); // displaying the window of detailed information about a node
   string  TimeSeparate(long time); // special function for transformation of time into string
  };

Hemos elegido mostrar la serie en una subventana separada como variante óptima.

En otras palabras, cuando la clase CTraceView se crea en la función CTraceView::Create, la ventana del gráfico se crea y todos los objetos se dibujan en ella, a pesar del hecho de que CTraceView se crea y funciona en Asesor Experto (EA, por sus siglas en inglés) en otra ventana. Se hace así para evitar la obstaculización de operaciones del código fuente del programa rastreado y la visualización de su propia información en el gráfico a causa de la enorme cantidad de información.

Pero para hacer la interacción entre dos ventanas posible, necesitamos añadir un indicador a la ventana que enviará todos los eventos del usuario a la ventana base con el programa rastreado.

El indicador se crea con la misma función CTraceView::Create. Solo tiene un parámetro externo: el identificador del gráfico al que se deben enviar todos los eventos.

El Indicador 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);
  }

Como resultado tendremos una representación bastante estructurada de la serie.



En el árbol TRACE que aparece a la izquierda se mostrará la serie inicial.

Debajo de él se encuentra la ventana INFO con información detallada sobre el nodo seleccionado (CTraceView::OnChartEvent en este ejemplo). Dos ventanas adyacentes que contienen árboles muestran la misma serie, pero se agrupan por clases (el árbol CLASS en el medio) y por archivos (el árbol FILE a la derecha).

Los árboles de clases y archivos tienen el mecanismo incluido de sincronización con el árbol principal de la serie, así como medios de control convenientes. Por ejemplo, cuando hace click en un nombre de clase en el árbol de clases, se seleccionan todas las funciones de esta clase en el árbol de series y en el árbol de archivos. Y de la misma forma, cuando hace click en un nombre de archivo, se seleccionan todas las funciones y clases de ese archivo.



Este mecanismo permite la selección y visualización rápida de los grupos de funciones requeridos.



Prestaciones de Trabajo con la Serie

Como ya ha notado, los parámetros del nodo CNode incluyen el array de estructuras tagWatch. Se crea así simplemente por la conveniencia de representación de la información. Contiene un valor nombrado de una variable o expresión.

Estructura de un Valor Watch

//------------------------------------------------------------------    struct tagWatch
struct tagWatch
{
    string m_name;     // name
    string m_val;    // value
};

Para añadir un nuevo valor Watch al nodo actual, debe llamar a la función CTrace::AddWatch y usar el macro _WATCH.

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


La limitación especial de valores añadidos (al igual que con los nodos) controla que los nombres sean únicos. Esto significa que que comprueba el nombre de un valor Watch para ver si está repetido antes de añadirlo al array CNode::m_watch[]. Si el array contiene un valor con el mismo nombre, el nuevo no se añadirá, pero el valor del ya existente se actualizará.

Todos los valores Watch se muestran en la ventana de información.



Otra conveniente prestación de MQL5 es la organización de una pausa forzada en el código durante su ejecución.

La pausa se implementa usando un bucle infinito simple durante (true). Lo más conveniente de MQL5 en este caso es la gestión del evento de salida de este bucle: haciendo click en el botón rojo de control. Para crear un punto de pausa durante la ejecución, use la función CTrace::Break.


La Función para la Implementación de Puntos de Pausa

//------------------------------------------------------------------    Break
void CTraceCtrl::Break() // checking the debug mode of a node
  {
   if(NIL(m_traceview)) return; // check of validity
   m_stack.BreakBy(TG_ALL,NULL,false); // removed the m_break flags from all nodes
   m_cur.m_break=true; // activated only at the current one
   m_traceview.m_viewstack.m_sid=m_cur.m_id; // moved selection to it
   m_stack.ExpandBy(TG_UP,m_cur,true,m_cur); // expand parent node if they are closed
   m_traceview.Draw(); // drew everything
   string name=m_traceview.m_viewstack.m_name+string(m_cur.m_id)+".dbg"; // got name of the BREAK button
   bool state=ObjectGetInteger(m_traceview.m_chart,name,OBJPROP_STATE);
   while(!state) // button is not pressed, execute the loop
     {
      Sleep(1000); // made a pause
      state=ObjectGetInteger(m_traceview.m_chart,name,OBJPROP_STATE);  // check its state
      if(!m_traceview.IsOpenView()) break; // if the window is closed, exit
      m_traceview.Draw(); // drew possible changes
     }
   m_cur.m_break=false; // removed the flag
   m_traceview.Draw(); // drew the update
  }


Al encontrarse con un punto de pausa, los árboles de series están sincronizados para mostrar la función que llamó a este macro. Si se cierra un nodo, el nodo progenitor se expandirá para mostrarlo. Y si es necesario, el árbol se extenderá o retraerá para traer el nodo al área visible.


Para salir de CTraceCtrl::Break, haga click en el botón rojo localizado cerca del nombre del nodo.


Conclusión

Bueno, ahora tenemos un interesante "juguete". Al escribir este artículo, intenté diferentes variantes para trabajar con CTraceCtrl y me aseguré de que MQL5 tiene perspectivas únicas de controlar Asesores Expertos y organizar su cooperación. Ninguna de las prestaciones usadas para el desarrollo del rastreador están disponibles en MQL4, lo que una vez más prueba las ventajas de MQL5 y sus amplias posibilidades.

En el código adjunto puede encontrar todas las clases descritas en el artículo junto con las bibliotecas de servicio (el número mínimo requerido de ellas, puesto que no son el objetivo). Además, he adjuntado un ejemplo ya preparado - archivos actualizados de la biblioteca estándar donde se colocan los macros_IN. Todos los experimentos hechos con el Asesor Experto están incluidos en el archivo estándar de MetaTrader 5 - MACD Sample.mq5.