Proposition for reporting MEMORY LEAKS in strategy tester

 

Hi  -  happy New Year.

In https://www.mql5.com/en/forum/383008#comment_26181711 I wondered why the strategy tester does not report any memory leaks if occurring in the MQL5 program.

Look the following EA where I "forgot" to free the allocated memory:

class A {
private:
  string m_name;
public:
  A(string name) {
    m_name = name;
    PrintFormat ("%s: Happy New Year 2023 and Happy Trading!", m_name);
  }
  ~A() {
    PrintFormat ("End %s", m_name);
  }
};
int OnInit() {
  A *a1, *a2;
  a1 = new A("Matthias");
  a2 = new A("Angelika");
  delete a1;
  // forget 'delete a2;' causing a memory leak!
  return INIT_SUCCEEDED;
}
void OnDeinit (const int reason) {
  Print ("Memory Leaks:");
}

If I let run this EA with real data (on a demo account) I get what I expect, namely a short report about the memory leak after having removed the EA from the chart:

2023.01.01 19:11:46.573 memleak (CADCHF,MN1)    Matthias: Happy New Year 2023 and Happy Trading!
2023.01.01 19:11:46.573 memleak (CADCHF,MN1)    Angelika: Happy New Year 2023 and Happy Trading!
2023.01.01 19:11:46.573 memleak (CADCHF,MN1)    End Matthias
2023.01.01 19:11:55.079 memleak (CADCHF,MN1)    Memory Leaks:
2023.01.01 19:11:55.079 memleak (CADCHF,MN1)    1 undeleted objects left
2023.01.01 19:11:55.079 memleak (CADCHF,MN1)    1 object of type A left
2023.01.01 19:11:55.079 memleak (CADCHF,MN1)    64 bytes of leaked memory

But if I let run the EA with history data on the strategy tester I cannot find the report about the memory leak. That is a little bit uncomfortable.

I think the earlier an error is discovered the better. Not only errors in the logic of the trading itself but also technical programming errors like memory leaks.

Therefore I have written a short header file defining a class which detects and reports all memory leaks, especially in the strategy tester.

Copy the header file "memoryleak.mqh" shown below into the Include directory and extend the EA in the following way by the bold printed lines:

#define CHECK_MEMORY_LEAK
#ifdef CHECK_MEMORY_LEAK
#include <memoryleak.mqh>
CMem mem;
#endif
class A {
private:
  string m_name;
public:
  A(string name) {
    m_name = name;
    PrintFormat ("%s: Happy New Year 2023 and Happy Trading!", m_name);
  }
  ~A() {
    PrintFormat ("End %s", m_name);
  }
};
int OnInit() {
  A *a1, *a2;
  a1 = new A("Matthias");
#ifdef CHECK_MEMORY_LEAK
  mem.mynew (__FILE__, __LINE__, a1, "A");
#endif
  a2 = new A("Angelika");
#ifdef CHECK_MEMORY_LEAK
  mem.mynew (__FILE__, __LINE__, a2, "A");
#endif
  delete a1;
#ifdef CHECK_MEMORY_LEAK
  mem.mydelete (a1);
#endif
  // forget 'delete a2;' causing a memory leak!
  return INIT_SUCCEEDED;
}
void OnDeinit (const int reason) {
  Print ("Memory Leaks:");
#ifdef CHECK_MEMORY_LEAK
  mem.Resume();
#endif
}

The output on application to a chart and removal will be the following:

2023.01.01 19:25:33.570 memleak (CADCHF,MN1)    Matthias: Happy New Year 2023 and Happy Trading!
2023.01.01 19:25:33.570 memleak (CADCHF,MN1)    Angelika: Happy New Year 2023 and Happy Trading!
2023.01.01 19:25:33.570 memleak (CADCHF,MN1)    End Matthias
2023.01.01 19:25:36.749 memleak (CADCHF,MN1)    Memory Leaks:
2023.01.01 19:25:36.749 memleak (CADCHF,MN1)    -- Resume Memory Leaks --
2023.01.01 19:25:36.749 memleak (CADCHF,MN1)    1 undeleted objects left
2023.01.01 19:25:36.749 memleak (CADCHF,MN1)    File memleak.mq5 line 24: desc 500000 type A
2023.01.01 19:25:36.749 memleak (CADCHF,MN1)    -- End Resume --
2023.01.01 19:25:36.749 memleak (CADCHF,MN1)    1 undeleted objects left
2023.01.01 19:25:36.749 memleak (CADCHF,MN1)    1 object of type A left
2023.01.01 19:25:36.749 memleak (CADCHF,MN1)    64 bytes of leaked memory

The bold printed part of the ouput can be seen in the logfile of the strategy tester also. That is exactly what I want.

Later after having thoroughly tested your EA you can comment out the first line of the EA.

Of course the addition of three lines after each new and delete statement is boring, but unfortunately I have no better idea. I did not succed in redefinig the new and delete statements by a macro. Perhaps someone of you???

On the other hand the occurrencies of new and delete statements are not so much normally.

Here the header file "memoryleak.mqh":

#ifndef MEMORYLEAK_HEADER
#define MEMORYLEAK_HEADER
#include <Generic/HashMap.mqh>

class CMem {
private:
  CHashMap<string,string> m_Pointer2Info;
public:
  CMem() {
    m_Pointer2Info.Clear();
  }
  ~CMem() {
    m_Pointer2Info.Clear();
  }

  void mynew (const string fname, const int lineno, const void *ptr, const string type) {
    string desc = StringFormat("%x",ptr);
    string info = StringFormat ("File %s line %d: desc %s type %s", fname, lineno-2, desc, type);
    if (!m_Pointer2Info.Add (desc, info)) {
      PrintFormat ("%s ERROR m_Pointer2Info.Add", __FUNCTION__);
    }
  }

  void mydelete (const void *ptr) {
    string desc = StringFormat("%x",ptr);
    if (!m_Pointer2Info.ContainsKey(desc)) {
      PrintFormat ("%s ERROR m_Pointer2Info does not contain %s", __FUNCTION__, desc);
      return;
    }
    m_Pointer2Info.Remove (desc);
  }

  void Resume(void) {
    Print ("-- Resume Memory Leaks --");

    string desc[], info[];
    ArrayFree (desc);
    ArrayFree (info);
    m_Pointer2Info.CopyTo (desc,info);
    int size=ArraySize(desc);
    if (size == 0) {
      Print ("  no memory leaks");
    } else {
      PrintFormat ("%d undeleted objects left", size);
      for (int i=0; i<size; i++) {
        PrintFormat ("%s", info[i]);
      }
    }
    Print ("-- End Resume --");
  }
};

#endif

May be of help for somebody?

Matthias

Information about memory leaks
Information about memory leaks
  • 2021.11.29
  • www.mql5.com
Hi, please see the following EA: If I apply this EA directly to a chart (demo account) and remove it again from the chart I get the following info...
 

Now after long search I have found how files can be attached :-)

Files:
 
//--- You would need to declare a ?_ARGS(...) for each class
#define A_ARGS(p1)          p1
#define B_ARGS(p1,p2)       p1,p2

#ifdef CHECK_MEMORY_LEAK
#define NEW(ptr,classe,parameters)                \
    ptr=new classe(parameters);                   \
    mem.mynew(__FILE__, __LINE__, ptr, #classe);
#else
    ptr=new classe(parameters);
#endif

#ifdef CHECK_MEMORY_LEAK
#define DELETE(ptr)                               \
    delete ptr;                                   \
    mem.mydelete(ptr);
#else
    delete ptr;
#endif
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   A *a1, *a2;
   B *b1;

   NEW(a1,A,A_ARGS("Matthias"));
   NEW(a2,A,A_ARGS("Angelika"));
   NEW(b1,B,B_ARGS("Alain",2024));

   DELETE(a1);
// forget 'delete a2;' causing a memory leak!
   return INIT_SUCCEEDED;
  }
Just for fun as I would not used such construction personally. I don't think it's possible to do better in mql.
Files:
438987.mq5  3 kb
 

Hi,

motivated by Alain I've written a new header file "memoryleak.mqh" for checking the EA code for memory leaks in the strategy tester.

In order to use it there are slight changes to do in your application:


1. Include the header file "memoryleak.mqh" into your code at the very beginning

2. Change   

descriptor = new class_object (p1,...,pn)

    to

NEW_n (descriptor, class_object, p1,...,pn)


3. Change   

delete descriptor

    to

DELETE (descriptor)


4. Call MEMORY_RESUME at the end of your program (e.g. in OnDeInit)


That is all! Please see the following example:

#include <Utilities/memoryleak.mqh>

struct struct_Data {
  int      intvalue;
  double   doublevalue;
};
class CA {
private:
  int      m_intvalue;
  double   m_doublevalue;
public:
  CA() {
    Print ("construct CA without parameter");
    PrintFormat ("%s", "No Name");
  }
  CA(string n) {
    Print ("construct CA with one parameter");
    PrintFormat ("%s", n);
  }
  CA(struct_Data &data) {
    Print ("construct CA with one struct");
    m_intvalue     = data.intvalue;
    m_doublevalue  = data.doublevalue;
  }
  CA(string n, int birthyear) {
    Print ("construct CA with two parameters");
    PrintFormat ("%s  -  year %d", n, birthyear);
  }
  ~CA() {
    Print ("destruct CA");
  }
  void ShowData() {
    PrintFormat ("int = %d,   double = %0.3f", m_intvalue, m_doublevalue);
  }
};

void OnStart() {
  struct_Data data = {1, 5.6};
  CA *zero, *one, *two, *structData;
  NEW_0 (zero, CA);
  NEW_1 (one, CA, "Matthias");
  NEW_2 (two, CA, "Matthias", 1957);
  NEW_1 (structData, CA, data);
  structData.ShowData();

  // Forget to delete zero and two:
  //DELETE (zero);
  DELETE (one);
  //DELETE (two);
  DELETE (structData);


  MEMORY_RESUME;
}


If you forget to free the allocated memory you get a warning text during the testing of your EA by the strategy tester. After you have thoroughly checked and corrected all mistakes causing memory leaks you can outcomment line 5 in the header file "memoryleak.mqh" (marked by an arrow):


#ifndef MEMLEAK_HEADER
#define MEMLEAK_HEADER

// user can define the following or not:
-----> #define CHECK_MEMORY_LEAK




//----------------------------------------------------
// do not edit from here
//----------------------------------------------------
................
Files:
 
Dr Matthias Hammelsbeck #:

Hi,

motivated by Alain I've written a new header file "memoryleak.mqh" for checking the EA code for memory leaks in the strategy tester.

In order to use it there are slight changes to do in your application:


1. Include the header file "memoryleak.mqh" into your code at the very beginning

2. Change   

descriptor = new class_object (p1,...,pn)

    to

NEW_n (descriptor, class_object, p1,...,pn)


3. Change   

delete descriptor

    to

DELETE (descriptor)


4. Call MEMORY_RESUME at the end of your program (e.g. in OnDeInit)


That is all! Please see the following example:


If you forget to free the allocated memory you get a warning text during the testing of your EA by the strategy tester. After you have thoroughly checked and corrected all mistakes causing memory leaks you can outcomment line 5 in the header file "memoryleak.mqh" (marked by an arrow):


Ill share my code for a heap memory tracer.

@Alain Verleyen , actually, it can be done more seemles integrated. Dont know if this can be accounted for "better".


The code is as is, and needs to be understood, its not recommended to use it in production, because it has a very slow search "algorithm", but it does work reliable.

It will brint out a list of object-ids, which can be used to find the objects in a second run, so that you can insert a "DebugBreak()" when this particulat object will be created. - Of course this only works, if the creation of the objects is deterministic. - Meaning, the sequence of ooperations may not change, as the IDs are createn on the go, and therefore, the code must produce the same "results" on the second run.


It is provided as is.

#define new         dbg_heap_obj_trace = new 
#define delete      delete dbg_heap_obj_trace -

struct __dbg_heap_obj_trace
{
    // Local storage
    ulong   __sn_cnt;
    int     _size;
    int     _del_size;
    void*   v_obj[];
    ulong   v_obj_id[];
    void*   deleted_obj[];
    ulong   deleted_obj_id[];
    
    
    __dbg_heap_obj_trace() :
        __sn_cnt    (1),
        _size       (NULL),
        _del_size   (NULL)
    {};

    ~__dbg_heap_obj_trace()
    { 
        printf("Currently not deleted object count: %i", ArraySize(v_obj_id));
        ArrayPrint(v_obj_id);
    };

    template <typename V>
    const ulong identify(V* p_in)
    {
        int cnt = NULL;
        while( (cnt < _size)
            && (v_obj[cnt] != p_in) )
        { cnt++; }

        if(cnt == _size)
        {
            cnt = NULL;
            while( (cnt < _del_size)
                && (deleted_obj[cnt] != p_in) )
            { cnt++; }

            if(cnt < _del_size)
            { return(deleted_obj_id[cnt]); }
        }
        else
        { return(v_obj_id[cnt]); }

        printf("%s: %s", "Invalid object query", typename(p_in));
        return(-1);
    };
    

    template <typename V>
    V* operator=(V* p_in)
    {
        _size = ArrayResize(v_obj, _size + 1);
        ArrayResize(v_obj_id, _size);
        v_obj[_size - 1] = p_in;
        v_obj_id[_size - 1] = __sn_cnt++;
        printf("NEW Obj_id: %i", v_obj_id[_size - 1]);
        
        return(p_in);
    }

    template <typename V>
    V* operator-(V* p_in)
    {
        int cnt = NULL;
        while( (cnt < _size)
            && (v_obj[cnt] != p_in) )
        { cnt++; }

        if(cnt < _size)
        {
            _del_size = ArrayResize(deleted_obj, _del_size + 1);
            ArrayResize(deleted_obj_id, _del_size);
            deleted_obj[_del_size - 1] = v_obj[cnt];
            deleted_obj_id[_del_size - 1] = v_obj_id[cnt];

            printf("DEL Obj_id: %llu", v_obj_id[cnt]);
            
            ArrayRemove(v_obj, cnt, 1);
            ArrayRemove(v_obj_id, cnt, 1);
            _size--;

            return(p_in);
        }
        printf("%s", "DEL Obj_id not found!");
        return(p_in);
    }
} dbg_heap_obj_trace;

This content would go into an include .mqh file and you would simply include it into your projects. - No changes to your code are required.

To remove the code, simply remove the include file from your project. - Should work seemles, and could be extended with multiple types of queries and other stuff for more user-convenience.

 

As addendum, here a version, that also reports where the not deleted  objects were created.

(Just typed, not tested, or compiled)


#define new         dbg_heap_obj_trace._location_recorder(__LINE__, __FILE__) = new 
#define delete      delete dbg_heap_obj_trace -

class __dbg_heap_obj_trace
{
    public:

    // Local storage
    int     __file_ln;
    string  __file_name;
    ulong   __sn_cnt;
    int     _size;
    int     _del_size;
    void*   v_obj[];
    ulong   v_obj_id[];
    int     v_obj_line[];
    string  v_obj_file[];
    void*   deleted_obj[];
    ulong   deleted_obj_id[];
    
    
    __dbg_heap_obj_trace() :
        __sn_cnt    (1),
        _size       (NULL),
        _del_size   (NULL)
    {};

    ~__dbg_heap_obj_trace()
    { 
        printf("Currently not deleted object count: %i", ArraySize(v_obj_id));
        for(int cnt = NULL; (cnt < ArraySize()); cnt++)
        { printf("Object ID: %llu; created in file %s at line %i", v_obj_id[cnt], v_obj_line[cnt], v_obj_file[cnt]); }
    };

    __dbg_heap_obj_trace* _location_recorder(const int _line, const string _file)
    {
        __file_ln = _line;
        __file_name = _file;
        return(GetPointer(this));
    };

    template <typename V>
    const ulong identify(V* p_in)
    {
        int cnt = NULL;
        while( (cnt < _size)
            && (v_obj[cnt] != p_in) )
        { cnt++; }

        if(cnt == _size)
        {
            cnt = NULL;
            while( (cnt < _del_size)
                && (deleted_obj[cnt] != p_in) )
            { cnt++; }

            if(cnt < _del_size)
            { return(deleted_obj_id[cnt]); }
        }
        else
        { return(v_obj_id[cnt]); }

        printf("%s: %s", "Invalid object query", typename(p_in));
        return(-1);
    };

    template <typename V>
    V* operator=(V* p_in)
    {
        _size = ArrayResize(v_obj, _size + 1);
        ArrayResize(v_obj_id, _size);
        ArrayResize(v_obj_line, _size);
        ArrayResize(v_obj_file, _size);
        v_obj[_size - 1]        = p_in;
        v_obj_id[_size - 1]     = __sn_cnt++;
        v_obj_line[_size - 1]   = __file_ln;
        v_obj_file[_size - 1]   = __file_name;
        printf("NEW Obj_id: %i", v_obj_id[_size - 1]);
        
        return(p_in);
    }

    template <typename V>
    V* operator-(V* p_in)
    {
        int cnt = NULL;
        while( (cnt < _size)
            && (v_obj[cnt] != p_in) )
        { cnt++; }

        if(cnt < _size)
        {
            _del_size = ArrayResize(deleted_obj, _del_size + 1);
            ArrayResize(deleted_obj_id, _del_size);
            deleted_obj[_del_size - 1] = v_obj[cnt];
            deleted_obj_id[_del_size - 1] = v_obj_id[cnt];

            printf("DEL Obj_id: %llu", v_obj_id[cnt]);
            
            ArrayRemove(v_obj, cnt, 1);
            ArrayRemove(v_obj_id, cnt, 1);
            ArrayRemove(v_obj_line, cnt, 1);
            ArrayRemove(v_obj_file, cnt, 1);
            _size--;

            return(p_in);
        }
        printf("%s", "DEL Obj_id not found!");
        return(p_in);
    }
} dbg_heap_obj_trace;


As demo, how this could be achieved, without changing the original source code. - For anyone interested in such advanced code-substitutions, take a look at MQLplus Debugger library from CodeBase. - I have made extensive use of such techniques there to perform seemles custom code substitution.

These types of manipulations can and should be considered as dangerous and are disruptive to anyone not familiar with such techniques, as they change the actual code from what you are seeing on screen. - SO always be aware, if you are using such techniques.

 
Dominik Christian Egert #:

As addendum, here a version, that also reports where the not deleted  objects were created.

(Just typed, not tested, or compiled)


As demo, how this could be achieved, without changing the original source code. - For anyone interested in such advanced code-substitutions, take a look at MQLplus Debugger library from CodeBase. - I have made extensive use of such techniques there to perform seemles custom code substitution.

These types of manipulations can and should be considered as dangerous and are disruptive to anyone not familiar with such techniques, as they change the actual code from what you are seeing on screen. - SO always be aware, if you are using such techniques.

@Dominik: many thanks for this code. Very cleverly done, you are a genius! There is no need to change/adapt the application code, that is phantastic!!

Based on your code I've written my own code, slightly adapted to my personal habits. Instead of arrays I use an hashmap. I think, this is more suitable for the application.

Have fun!

#include <Generic/HashMap.mqh>

#define new         dbg_heap_obj_trace.location_recorder(__LINE__, __FILE__) = new
#define delete      delete dbg_heap_obj_trace -


class Cdbg_heap_obj_trace {
private:
  // Local storage
  CHashMap<string,string>  m_map;
  int                      m_line;
  string                   m_filename;  

public:
  Cdbg_heap_obj_trace() {
    m_map.Clear();
  }
  
  ~Cdbg_heap_obj_trace() { 
    Print ("-- Resume Memory Leaks --");
    string desc[], infos[];
    ArrayFree (desc);
    ArrayFree (infos);
    m_map.CopyTo (desc,infos);
    int size=ArraySize(infos);
    if (size == 0) {
      Print ("  no memory leaks");
    } else {
      Print (StringFormat ("%d undeleted objects left", size));
      for (int i=0; i<size; i++) {
        Print (StringFormat ("  - %s", infos[i]));
      }
    }
    Print ("-- End Resume --");
    m_map.Clear();
  }
  
  Cdbg_heap_obj_trace* location_recorder(const int line, const string filename) {
    m_line     = line;
    m_filename = filename;
    return(GetPointer(this));
  }

  template <typename V>
  V* operator=(V* p_in) {
    string info = StringFormat ("File %s Line %d", m_filename, m_line);
    if (!m_map.Add (StringFormat("%x",p_in), info)) {
      Print (StringFormat ("%s ERROR m_map.Add", __FUNCTION__));
    }
    return p_in;
  }

  template <typename V>
  V* operator-(V* p_in) {
    string desc = StringFormat("%x",p_in);
    if (!m_map.ContainsKey(desc)) {
      Print (StringFormat ("ERROR Object %s not found!", desc));
      return p_in;
    }
    m_map.Remove (desc);
    return p_in;
  }

} dbg_heap_obj_trace;
 
Dr Matthias Hammelsbeck #:

@Dominik: many thanks for this code. Very cleverly done, you are a genius! There is no need to change/adapt the application code, that is phantastic!!

Based on your code I've written my own code, slightly adapted to my personal habits. Instead of arrays I use an hashmap. I think, this is more suitable for the application.

Have fun!


Looks good, but I see one major issue with your approach. You removed the ID logic and the matching of created and deleted objects.

There is no way to trace back which object, under what condition has not been deleted.

To me that was a major feature, and I would guess, it is also to you.

After all, when creating objects with "new", usually you do this multiple times with the same line of code, and just knowing where, will most probably not be enough.

That's why I had a "void*" pointer array of all allocated objects, matching them with a hash map, I think, won't work.

But, thank you for the flowers....

EDIT:
You need a space after new in the macro. Else the macro will fail.
 
Dominik Christian Egert #:

Ill share my code for a heap memory tracer.

@Alain Verleyen , actually, it can be done more seemles integrated. Dont know if this can be accounted for "better".


The code is as is, and needs to be understood, its not recommended to use it in production, because it has a very slow search "algorithm", but it does work reliable.

It will brint out a list of object-ids, which can be used to find the objects in a second run, so that you can insert a "DebugBreak()" when this particulat object will be created. - Of course this only works, if the creation of the objects is deterministic. - Meaning, the sequence of ooperations may not change, as the IDs are createn on the go, and therefore, the code must produce the same "results" on the second run.


It is provided as is.

This content would go into an include .mqh file and you would simply include it into your projects. - No changes to your code are required.

To remove the code, simply remove the include file from your project. - Should work seemles, and could be extended with multiple types of queries and other stuff for more user-convenience.

I hate macros
 
Alain Verleyen #:
I hate macros
But I used less than you did....
 
Dr Matthias Hammelsbeck #:

@Dominik: many thanks for this code. Very cleverly done, you are a genius! There is no need to change/adapt the application code, that is phantastic!!

Based on your code I've written my own code, slightly adapted to my personal habits. Instead of arrays I use an hashmap. I think, this is more suitable for the application.

Have fun!

Ok, I have overseen how you do the matching, and my previous post is wrong....

Your approach of creating strings (didn't know it's possible with pointers) is even better...

I think I'll adapt your approach to mine.