Contents

memdebug

Introduction

This is a program to debug memory allocation in a program. It is particularly useful for tracking down memory leaks; in other words, unwanted references to objects which prevent them from being garbage collected.

It can also function as a very simple debugger.

Unfortunately this program is only available under Unix. This is because it is partly implemented in a loadable library (a “.so” file), which requires full access to the runtime memory of oix.

Startup

Start the program by giving the command line for the program to be monitored, as follows :-

memdebug flowterm /tmp

The flowterm program will then load and start and, concurrently, a ? prompt will appear at which commands can be given to memdebug.

This works well for a graphical program, which doesn’t use the terminal. For a terminal-based program, it may be easier to use memdebug’s graphics mode, with the -g option :-

memdebug -g oidoc -html -d -o /tmp/libref -a

Using memdebug with the terminal has the advantage that command output can be piped (usually to less).

Entering commands

Commands to memdebug take a simple shell-like form; for example

? list MyClass#2

Output can be redirected by ending a command with >file or >>file, just like the shell. Output can also be piped to a command using the | character followed by the command, but this doesn’t work if memdebug is using its graphical mode (the -g option).

Stopping the monitored program

This can be done by entering the stop command at the prompt. It happens automatically when the monitored program returns from main, or when it stops with a runtime error.

Note that if the monitored program causes a fatal or system error, then the entire interpreter (including memdebug) exits.

A breakpoint can also be set using the breakpoint command; for example :-

? breakpoint /tmp/file.icn 123

Note that the full file path must be given. To clear the breakpoint, just use breakpoint with no arguments.

A break can also be set in a procedure or method; for example

? breakproc gui.Dialog.show_modal

To clear the break procedure, just use breakproc with no argument.

If the &break keyword is evaluated in the target program, then that too will cause a break.

To stop the program at startup, run memdebug with the -m option; this sets an initial breakproc in main.

Examining memory

Globals

To examine memory, it is helpful to start with the globals command. This shows the global variable and keywords values. For example :-

? globals
glob_cmds=list#90(0)
ipl.functional._a=&null
ipl.functional._l=&null
ipl.graphics.hilite_table=table#2119(1)
ipl.graphics.shadow_table=table#2086(1)
opts=table#11(0)
&pos=1
&subject=""
&why=(0x7fd267683e6c)"No such file or directory (errno=2)"
&progname=(0x7fd26bb5e225)"/home/rparlett/objecticon/examples/flowterm"
&random=2029852714
&trace=0
&maxlevel=500
&dump=0
&handler=&null
&main=co-expression#2(0)
&current=co-expression#2(0)
&source=co-expression#2(0)
&errortext=
&errorvalue=
&errorcoexpr=
&errornumber=
&file="/home/rparlett/objecticon/lib/gui/dispatcher.icn"
&line=148
&level=6

Note that the value of &current will show us where the program was stopped. To see a “stack trace”, use the list command with that co-expression :-

? list co-expression#2
co-expression#2(0)
    SP=0x246a610
    PF=0x246f360
    operator unary !
        0x246a610
        Arg 0=set#25(1)
        Tended 0=D_TendPtr -> set#25(1)
        Tended 1=D_TendPtr -> &null
        Tended 2=""
        Tended 3=&null
        Tended 4=set#25(1)
    method gui.Dispatcher.do_validate
        0x246f360, caller=0x242ae70
        File dispatcher.icn; Line 148
        Temp 0=set#25(1)
        Temp 1=object FlowTermDialog#1(84)
        Temp 2=&null
        Temp 3=&null
        d=object FlowTermDialog#1(84)
        bag=&null
    method gui.Dispatcher.work
        0x242ae70, caller=0x2454f60
        File dispatcher.icn; Line 58
        Temp 0=&null
    method gui.Dispatcher.work_or_sleep
        0x2454f60, caller=0x2453a90
        File dispatcher.icn; Line 108
        Temp 0=&null
    method gui.Dispatcher.message_loop
        0x2453a90, caller=0x22c07f0
        File dispatcher.icn; Line 71
        Temp 0=&null
        Temp 1=object graphics.Window#127(1)
        r=object FlowTermDialog#1(84)
        (more output...)

This shows all the values stored in internal structures and local (non-static) variables. Structures can be further examined by using list; for example :-

? list FlowTermDialog#1

Report

Another potential starting point is the report command. This traverses memory and counts the types and sizes of what it encounters, and then produces a report. For example :-

? report
Builtin types
=============
     15638  String
    296096     Total length
      1771  Ucs
     14475     Total length
         4  Cset
        40     Total size
      2871  List
      5134     Total size
        95  Set
      9042     Total size
       417  Table
      3918     Total size
        79  Co-expression
       588  Methp

Objects and Records
===================
      1122  class xdg.GlobMatch
       607  class util.Listener
       306  class io.Stat
       306  class ipl.filegui.AnnotatedListEntryWithIcon
       177  class ipl.vt.KeyEntry
       110  class graphics.Window
        83  class gui.EmptyBorder
        74  class io.FilePath
        71  class xdg.CachedDir
        69  class xdg.ThemeDirectory
 ... more output
         1  class HistoryList
         1  class gui.Style
         1  class ipl.filegui.FileTrailList

The second section outputs a sorted list of the numbers of instances of objects and records encountered.

report could be particularly useful if it were run twice at different times during a program’s run, and the output compared. Unexpected increases in a particular type’s count could then be investigated with the list command.

Statics

To view static variables in a class, procedure or method, use the statics command, for example :-

? statics gui.Dialog
class gui.Dialog
        ROOT_WATTRIBS=table#19(1)
        POINTER="left ptr"
        DND_NEGATIVE_POINTER="circle"
        DND_POSITIVE_POINTER="hand2"
        DOUBLE_CLICK_DELAY=500
        DOUBLE_CLICK_TOLERANCE=2
        DRAG_GESTURE_OFFSET=7
        TOOLTIP_START_DELAY=1000
        TOOLTIP_END_DELAY=4000
        TOOLTIP_CONTINUE_DELAY=1500
        TOOLTIP_X_OFFSET=9
        TOOLTIP_Y_OFFSET=26
        FOCUS_UP=set#10(3)
        FOCUS_DOWN=set#11(3)

Search for instances of a type

It is often useful to search memory for all the instances of a type. To do this, use list, and give the name of the type to search for; for example :-

? list Columns
object Columns#1
        Instance variable columns in object Terminal#1
object Columns#2
        Instance variable columns in object Terminal#2

The indented line shows the thing that refers to the instance shown (there may be other things of course; see refs below). If the -r option is given to list, then this reference is expanded to show the complete path from the instance back to a garbage collector root. For example :-

? list -r Columns
object Columns#1
        Instance variable columns in object Terminal#1
        Instance variable revert_tab in object Terminal#2
        Instance variable which_one in object gui.TabSet#1
        Instance variable tabs in object FlowTermDialog#1
        Variable d in frame of method gui.Dispatcher.do_event in chain of co-expression#3
        &main
object Columns#2
        Instance variable columns in object Terminal#2
        Instance variable which_one in object gui.TabSet#1
        Instance variable tabs in object FlowTermDialog#1
        Variable d in frame of method gui.Dispatcher.do_event in chain of co-expression#3
        &main

Apart from class names, the type given can be a record type name, or any of the following builtin type names :-

Note that telem and lelem refer to orphaned table and list block elements; see below.

Please note that the output for some of the above may be voluminous :-

$ memdebug flowterm
? list -r string | wc
 114105  489652 4992356

Search for references to an instance

If the list command above returns instances which you think should no longer be reachable (and hence will not be garbage collected), then the refs command can be used to see why. It prints out a list of all the things that refer to a particular object. For example :-

? refs Terminal#3
Instance variable which_one in object gui.TabSet#1
Instance variable ui in object ipl.vt.Vt#3
List element list#4412(3)[3]
List element list#12969(3)[3]
List element list#12972(3)[3]
Instance variable parent in object gui.MessageLabel#4
Instance variable parent in object gui.Split#4
Instance variable parent in object gui.GridLayout#28
List element list#13203(3)[3]
(more output...)

refs can then be used to explore these references further; for example the first one in the list above :-

? refs gui.TabSet#1
Instance variable tabs in object FlowTermDialog#1
List element list#127(2)[2]
(more output...)

With the -r option, refs will list the path back to a garbage collector root, for each result. For example :-

? refs -r Terminal#3
Instance variable which_one in object gui.TabSet#1
    Instance variable tabs in object FlowTermDialog#1
    Variable d in frame of method gui.Dispatcher.do_validate in chain of co-expression#3
    &main
Instance variable ui in object ipl.vt.Vt#3
    Instance variable focus in object FlowTermDialog#1
    Variable d in frame of method gui.Dispatcher.do_validate in chain of co-expression#3
    &main
(more output...)

Each indented line gives the first reference to the item mentioned in the line above; eventually a root is reached, in the above two cases &main.

By default refs doesn’t traverse the reference graph further on from the search target itself. This means that paths that reach the target via the target itself are not included in the results. Such paths aren’t very useful for tracking down memory leaks, and tend to obfuscate the relevant, direct paths to the target. But if you wish to include these paths too, pass the -a option to refs.

Two other points should be mentioned about refs. Firstly, breadth first search is used, so the shortest results come first. Secondly, refs does not show all conceivable paths to the target. If there are several paths from a root to an intermediate object on a path to the target, then only the first encountered path from the root to the intermediate object is shown. This will be the shortest path, but it may not be unique (there may be another of the same path length). For example, given the program :-

global a, b, t

procedure main()
   t := set()
   a := b := [1, 2, t]
   &break
end

the refs -r command applied to the set t will report the path

List element list#89(3)[3]
        Global variable a

but not

List element list#89(3)[3]
        Global variable b

(the “intermediate object” in this case being list#89).

Dump

The dump command lists the values of all globals, keywords, statics and structures. Its output is usually extensive, so it is best to redirect it to a file (or view it with less).

Values without serial numbers

Strings, csets, ucs strings, large integers and (on 32 bit systems) real numbers, lack the identifying serial numbers found in structure objects such as lists. Therefore they have to be identified by address.

For example, in the globals output above, &file was shown as :-

&progname=(0x7fd26bb5e225)"/home/rparlett/objecticon/examples/flowterm"

This address can now be used with list and refs, in the same way for structure objects, for example :-

? refs 0x7fd26bb5e252
&progname

which tells us that this string is just referenced in this one place, as the value of this keyword.

Note that some values will be allocated outside of the garbage-collected memory regions. These don’t have their addresses shown, and are not subject to garbage collection, so finding references to them is not useful. For example, the value of &file in the globals output was :-

&file="/home/rparlett/objecticon/lib/gui/dispatcher.icn"

This string value is actually contained in the executable icode file loaded by the interpreter, and hence no address is shown.

Since Object Icon uses a relocating garbage collector, over a program’s run the addresses of particular objects may change.

Orphaned memory

Under certain rare circumstances parts of list or table data structures can become detached from their enclosing instances, yet still be referenced by variables. These “orphaned” sections are always listed by address, since they don’t have the serial numbers of normal data types.

These references are reported with output like :-

? refs set#4
Value of Orphaned table element block(0x7fc7988351e0)
Slot 77 in Orphaned list element block(0x7fc798838e50)

The addresses shown can be then used with further ref and list commands.

A similar phenomenon is a “stuck” element in a list. In this case, the list data block is still attached to its parent list, but a particular unused slot in the block contains data, inaccessible to the program. This can only happen if a list is changed between the creation of a variable, and the assignment to it; for example :-

L := [ ... ]
1(L[5], clear(L)) := 123

The stuck element is then reported with list after the normal elements; for example :-

? list list#89
list#89(0)
        Stuck element=123

Programs which load other programs

Values in loaded programs

When a program loads another program and uses classes and records in it, there can be ambiguity in the global names. For this reason, class and record type names are prefixed with the program’s &main. For example,

(prog=co-expression#9(0))class Something

is a class from a loaded program whose &main is co-expression#9, whilst an instance of that class might appear as :-

(prog=co-expression#9(0))(0x7f9e73ec1480)object Something#11(3)

Note that the address of the instance is also shown, and that must be used rather than the usual class and serial combination Something#11, to search for the object with list and refs. This is because the command list Something#11 would refer to the class Something in the current program (see below) rather than in the loaded program. They will either be distinct classes (with the same name), or more likely, the current program won’t even have a class Something.

References found in other programs

When a reference to an object is found in another program, it is shown with a reference to the other program’s &main; for example :-

? refs Something#2
Global variable g2 of program co-expression#9(0)

Listing and selecting the current program

Memdebug has the notion of a monitored program (the one given on the command line), and the current program, which is the one whose memory is being examined. They start off as the same program. If the monitored program loads other programs, then it may be necessary to change the current program. This is because :-

To list the programs loaded by the interpreter use the progs command :-

? progs
Program 0x2209e00
        &main=co-expression#9(0)
Program 0x21eea10 (current)
        &main=co-expression#2(0)
Program 0x6bf140
        &main=co-expression#1(1)

Here, the monitored program is the current program, which has loaded another program (co-expression#9). co-expression#1 is the memdebug program itself. To change to the loaded program, use :-

? prog co-expression#9
prog set to 0x2209e00

Note that, although the current program can be changed, the monitored program cannot. Therefore, for example, breakpoints cannot be set in a loaded program.

Other commands

Type help at the prompt to see a list of other commands.

Example

Here is an example of exposing and tracking down a memory leak. The program in question is flowterm, which allows the user to open and close multiple tabs during its run, rather like a web browser. This raises the question of whether or not the tabs are properly unreferenced after they are closed.

To check this, the program is started with the command

memdebug flowterm 

and then two tabs are opened. Each tab is an instance of a class named Terminal, and the instances can be listed with :-

? list Terminal
object Terminal#1
        Variable t in frame of procedure main in chain of co-expression#3
object Terminal#2
        Instance variable which_one in object gui.TabSet#1

If either tab is closed, then only the other should appear when the above command is repeated. I noted that this was the case if the second tab was closed, but not the first. In other words, closing the first tab, and repeating the above command would give the same results.

To investigate further, the refs command may be used :-

? refs -r Terminal#1
Variable t in frame of procedure main in chain of co-expression#3
    &main
Temporary descriptor 9 in frame of procedure main in chain of co-expression#3
    &main
Instance variable revert_tab in object Terminal#2
    Instance variable which_one in object gui.TabSet#1
    Temporary descriptor 0 in frame of procedure main in chain of co-expression#3
    &main

Two particular problems are indicated. Firstly, references to the first tab in the main() procedure, which looks like this :-

procedure main(a)
   local d, t
   ...
   d := FlowTermDialog()
   d.do_tab_cmd(a) | help_stop(&why)
   t := \d.tabs.which_one | d.tabs.children[1]
   d.set_focus(t.remembered_focus)
   d.show_modal()
   ...
end

Listing co-expression#3 shows the variables residing in that procedure’s invocation frame :-

? list co-expression#3
co-expression#3
        SP=0x562d6b267688
        PF=0x562d6b267688
        method gui.Dispatcher.message_loop
                0x562d6b267688, caller=0x562d6b061148
                File dispatcher.icn; Line 120
                Temp 0=&null
                Temp 1=object graphics.Window#56
                r=object FlowTermDialog#1
... (several frames omitted)
        procedure main
                0x562d6b056358, caller=0x562d6b0536e8
                File flowterm.icn; Line 3613
                Temp 0=object gui.TabSet#1
                Temp 1=class gui.ImageCache
                Temp 2=object ipl.options.Opt#3
                Temp 3=methp#12(object ipl.functional.LRPartial#1,method ipl.functional.LRPartial.lcall)
                Temp 4=object ipl.options.Opt#4
                Temp 5=methp#13(object ipl.functional.LRPartial#2,method ipl.functional.LRPartial.lcall)
                Temp 6=object ipl.options.Opt#5
                Temp 7=object ipl.options.Opt#6
                Temp 8=struct_var -> object gui.TabSet#1.which_one
                Temp 9=struct_var -> object Terminal#1.remembered_focus
                a=list#27(0)
                d=object FlowTermDialog#1
                t=object Terminal#1
        internal main_wrapper
                0x562d6b0536e8, caller=(nil)
                Temp 0=procedure main
                Temp 1=list#27(0)

One reference is caused by the local variable t, and the other is the intermediate result of the parameter to the d.set_focus() method call. The first could be resolved by adding an assignment to t to set it to &null. The other reference is more difficult, since the use of internal temporary variables is unpredictable. The easiest recourse is to simply move the problem code into an auxiliary procedure called get_dialog. This procedure’s frame will be removed when it returns, eliminating all local and temporary references. So main() now looks like this :-

procedure get_dialog(a)
   local d, t
   d := FlowTermDialog()
   d.do_tab_cmd(a) | help_stop(&why)
   t := \d.tabs.which_one | d.tabs.children[1]
   d.set_focus(t.remembered_focus)
   return d
end

procedure main(a)
   local d
   ...
   d := get_dialog(a)
   d.show_modal()
   ...
end

and when the list command above is repeated, the output looks like this :-

? list co-expression#3
...
        procedure main
                0x563e888a83a8, caller=0x563e888a56e8
                File flowterm.icn; Line 3625
                Temp 0=class gui.ImageCache
                Temp 1=class gui.ImageCache
                Temp 2=object ipl.options.Opt#3
                Temp 3=methp#12(object ipl.functional.LRPartial#1,method ipl.functional.LRPartial.lcall)
                Temp 4=object ipl.options.Opt#4
                Temp 5=methp#13(object ipl.functional.LRPartial#2,method ipl.functional.LRPartial.lcall)
                Temp 6=object ipl.options.Opt#5
                Temp 7=object ipl.options.Opt#6
                Temp 8=object graphics.Pixels#17
                Temp 9=object FlowTermDialog#1
                a=list#31(0)
                d=object FlowTermDialog#1
...

Now there are no references to Terminal#1.

The second problem indicated by the refs output above was the presence of a reference to the first tab in the second tab, via the instance variable revert_tab. This variable is used to select the tab to bring forward when a tab is closed. It is of course not useful if the chosen tab has itself already closed. So the solution in this case is to simply clear any such references in other tabs when a tab is closed :-

   public close_tab()      # A method in Terminal, called when the tab is closed.
      local t, i
      if *parent.children = 1 then
         parent_dialog.dispose()
      else {
         # To help the garbage collector, clear references to this tab
         # in the other tabs.
         every t := !parent.children do {
            if t.revert_tab === self then
               t.revert_tab := &null
         }
      ... etc
   end

Having put this fix into place, the list command can be run again, after creating a new tab, and closing the first :-

? list Terminal
object Terminal#2
        Instance variable which_one in object gui.TabSet#1

Now all references to the first tab have been removed, and it will be garbage collected in the usual way.

Contents