Handmade Network»Forums
Simon Anciaux
1337 posts
Brain dump about debugger
Edited by Simon Anciaux on Reason: Initial post
I wanted to write some feedback and feature request for the debuggers present on the network. Some of it I gave sometime ago to CDBG (and part of it was added), some I gave to c[ode]clap, RemedyBG or both (and part of it was also added). I also wanted to discuss some of those feature with the community, because all of the following is based on how I debug and other probably have different ideas and I would be interested in hearing them.

But I got carried away and there is a lot. I didn't know how to present everything. So I tried to lay down what I had in mind, and explain some of the reasoning behind it. It's probably a bit rough but hopefully it can be useful. So grab some coffee or tea and something to eat because it's going to be a long read (about an hour).


= How do I start debugging =

The first thing I want to talk about is the different way I start debugging a program. Most of the time I start the debugger from the command line.

Things I would like when I call the debugger from the command line:
- passing the program to debug as an argument;
- passing the working directory;
- default the working directory to the executable directory;
- passing the source files I want to be opened;
- the first source file specified being the one displayed unless I set a breakpoint;
- set a breakpoint location, either file + line or a function name;
- if no breakpoint or source file is specified display the program entry point (main, WinMain...);
- whether or not to start debugging right away;
- being able to use relative paths as well as absolute paths;
- keep the command line available to type other commands;

Some of those things could depend of the debugger configuration file. For instance should the debugger start debugging right away if it was not specified on the command line.
I would like the debugger to try to find the debug information without starting to debug, and log a message in the output window (or anything that is not modal) if the file was no found or didn't match the executable or a ready message in the other case. That gives some feedback to the user that the debugger is ready or not.
If I launched from the command line I don't want the debugger to ask me to save the session when I exit.

1
debugger -executable executable_path -workind_directory wd_path -files source_1.c,source_2.c,source_3.c -start -file_break file[:line] -function_break function_name


The second way I start debugging is by using the Just in time debugger window that pops up whenever an application crashes. I don't know how to add another debugger to that window or if it's specific to Visual Studio, but I would be nice to have.

The third way is to attach to an running application. I generally do that from the GUI, but being able to attach to a running application from the command line might be useful. Being able to pass file names in that case would be useful too.

The fourth way would be to launch the debugger and set up the application to launch. More on the setup in the next session. This is not something I do often.


= Project management =

When working on some project, it's useful to be able to setup different debugging configurations. For example in one of my project, I'm working on a single file header library. The library itself doesn't compile to an executable, but I have several test applications, one for using the library as C, one for Cpp, a parser to make external data usable as C/Cpp arrays, an input validation application and an output validation application. I would like to be able to setup ONE project file in the debugger for all those programs. So one project would contains several configurations.

I don't want a single project file for all my projects. I would like one project file per project, that may be use to launch several executable. Having one file per project allows to have it with the project on source control. Launching several instance of the debugger and using the same project file would be nice. For example to be able to debug a client and server at the same time. I don't care about overwriting issues, as I generally want to save manually or at specific time, for instance when I close the debugger, and I'm the one that should be responsible of the project file.

A way to list all recent project files used would be useful.

The project setup should be it's own panel (in a similar fashion than the watch panel, or memory panel), not a separate debugger mode or window, staying accessible during the debug session as it can be useful to refer to it to verify the working directory, parameters, executable location...

If the project panel contains default value (for example: the working directory being the executable folder if none was specify) those value should be visible to the user. It's not clear to me if it's useful for the default values to be identifiable (greyed out for example). But a way to reset all value or a single value back to the default would be useful.

Thing I thing should be contained in the project file:
- The layout of the different panel. Several layout as different configurations could use different layouts.
- For each configuration, the layout it uses, the breakpoints, the opened files, the watch panel expressions...

I mentioned that I don't want to be asked to save the project when I close the debugger when it was launched from the command line, but I still want to be able to save the project manually if I want to.


= UI =

As mentioned in the previous section, I think that the project file should contain the layout to use for a particular project configuration. In practice I don't often change the layout I use to debug but it's sometime useful to do so.
I would like to be able to configure a default layout that will be used for new projects. When I modify the layout, the new layout should only apply to the current project configuration. A "Save as the default layout" button or menu as well as a "Reset to default layout" and "Restore factory layout." would help changing the default layout. I don't know if it would be relevant in the case of debugging to have several configurable layouts, that the user could choose from depending on what he is doing, and easily switch to.

I would like to be able to open, move around and place breakpoints in source file before starting to debug.

When I start the debugger I would like it to have the same layout than when I'm debugging. If the debugger was just started, show empty panels (but still show the panels). If stopped debugging show the last content of the panels as it could still be relevant to the user.

The only feedback given by having a different layout when you're not debugging is to confirm that the debugged application has stopped running. This is useful feedback to me, but this can be achieve better with other visual clues. Visual Studio changes the status bar color from orange (running) to blue (stopped). It works great in my opinion, but it needs to be a big enough color change and colored area to do it's job. For example, RemedyBG colored area is not big enough in my opinion.

It's also useful to clearly show which panel has the focus. CDBG combined that with showing the running state by having the color of the border around the active panel be orange or blue, and I worked great.

It's not directly related to UI, but if the debugger has hotkeys that depends on which panel has the focus, it should be considered that some keys should always be available. The best example are stepping keys. In CDBG, you needed to be in the source panel to be able to step, and I found that really annoying. Stepping key should always work in my opinion, no matter in which panel you are. Another one would be that Alt should always give the focus to the menu bar.

Useful things in general:
- Maximizing a panel is useful. For instance, if I have the watch panel and a memory panel below it, pressing CTRL + UP while the watch panel has the focus would make it take the whole height.
- A hotkey to quickly get access to any panel is be useful. For example: ALT + 1 -> file panel; ALT + 2 -> watch panel...
- Make UI items look like what they are. In c[ode]clap, buttons, tabs, drop down list look exactly the same. I would have never though of clicking the stackframe address in the watch panel expecting it to be a drop down list.
- I don't use the window compositor, which can lead to lot of CPU and GPU usage. The cause of that is often using PeekMessage instead GetMessage, and the WM_MOUSEMOVE message that is continuously sent when the mouse moves. And it makes my computer fans go faster.
- It's easy to make things look sluggish if you don't use the good key down/up message. Some events need to be handled on the key up message (generally mouse stuff) but most things need to be handled in the key down message (navigation with the keyboard). Using key up when trying to move with the keyboard or typing adds random latency (the amount of time you keep the key pressed) and doesn't feel good.
- When we continue the execution of the debugged program, it should get the focus back.
- Having several panels of the same type is useful.

Take care about screen area and use the available space to display useful information. It poses the question of "what is useful information ?".

An example: in Visual Studio the watch panel has both a tab and a title bar. The title bar text is not useful as it only say "Watch x" and that's already in the tab name. Its buttons functionality are or could be available in a right click menu on the tab. The column names are not necessary (in my opinion) as well as the type column. After that it uses the space correctly (at least vertically) with small lines and 1 pixel line spacing. That said spacing is needed to keep things clear and easily identifiable, but should be minimal. What is useful probably depends on the user and the use case, so some things should be configurable, like whether or not to display certain columns in the watch panel.

When the debugged cause an exception, it should be obvious to the user. This imply that the debugger should take the focus, and clearly display the exception. The exception message should not be modal (the user should be able to continue using the debugger without validating a message) and the exception info should be kept available after the initial message is shown. In my opinion the output panel should get the focus and display the exception message in red. The file panel should point out which line caused the error.

In a previous version of RemedyBG, when an exception happened, the debugged application kept the focus, and was in front of the debugger. The debugger had a modal message box to display the exception and the rest of the debugger was greyed out. So when an exception occurred, at first glance it looked like the debugger had crashed. Nothing was totally wrong there. If you have several screen or if the debugger was on one side on the screen and the debugged on the other side, the crashed would have been understood directly. The modal message box would still be a bit annoying but it did it's job of notifying the user of the error. But just having a different desktop configuration caused some confusion to me. It was fixed just by giving the debugger the focus when an exception occurs.


= Source panel =

The source file panel has two uses for me: following the program execution and quickly inspecting the value of a variable or expression.

Following the execution of the program is mostly reading code, and knowing where the execution is in the code. So making the code easy to read helps. Here are some things that can help make the code easier to read than plain text:
- Syntax highlighting;
- Semantic highlighting: highlighting each variable (and function ?) with a specific color to make it easy to spot the same variable in different part of the code. I'm not sure about that as I never used a debugger or editor that has that;
- Good looking font / text;
- Pixel perfect font option;
- Proportional font (I know I'm the only one). With possibility to use mono-spaced font only on numbers;
- Light and dark theme that can be switched on the fly. Makes it easier on the eye depending on the ambient light level;
- Line wrapping with "smart" indentation;
- Good keyboard navigation;
- Configurable keys. If a user want to use vim style movement keys it should be possible;
- Common controls (page up/down, home/end, top/bottom of the file, 10 lines up/down...);
- Good mouse navigation (for example being able to configure how many lines the mouse wheel scrolls). Also pay attention that no element will interfere with what the user want to do. In Visual Studio, sometimes when you scroll with the mouse wheel, the mouse cursor will land on a variable, that will display a tool-tip, and in some tool-tips you can scroll so the mouse wheel stops to scroll the source file and start scrolling the tool-tip which is annoying;
- No lag between view movement and cursor movement;
- No uncommon behavior while navigating a file. For example when you press the down arrow key, you expect the cursor to move to the last line of the file and than scroll the file while keeping the cursor on the last line of the view. Don't do other things unless you've got a good reason;
- Search, go to function, go to declaration, go to definition, find type declaration...
- Highlighting every occurrence of the word under the cursor;
- When you move several line at a time (page up/down, go to, search), smooth scrolling helps a lot to keep track of which direction the view moved, and how much. Smooth scrolling might seem like a fancy feature but it really gives a lot of visual feedback;
- No jumping the view when you reach the bottom or top with the cursor. Unless it's an option the user chose.


While using the watch panel to display variable values is fine, I often want to get a quick look at variable as I'm stepping through the code without having to type it in the watch window. The important thing for me when I want a quick look at a variable is to be able to instantly visualize the content of the variable, without my brain having do any work, and keep the context in which the variable is used. I want it to be quick and effortless.

There are several way to achieve this:
- Having an auto feature in the watch panel, that displays variable on the current line, or variable present in the same scope;
- Tool-tips when the mouse is over a variable name, or the cursor is on a variable;
- Inline display of the value directly in the code.


1. Watch window with auto: I don't think this work great as you "loose the variable context" when having to look in another panel. If the panel contains all variable in the scope, some variable that are right to each other on the current line might be at the bottom and the top of the list in the watch panel.

2. Tool-tips:
- They are good at "brain not having to do mapping".
- They are not good at showing several variables at the same time. This could be "fixed" by having some way to pin tool-tips:
-- By keeping all tool-tips you saw displayed at the last occurrence of the variable.
-- By having a "line tool-tip" that would show every variable value on the line in the tool-tip, or maybe the whole text line with variable names replaced by their value.
- Often there is a latency to display them, often small but still latency. If you implement tool-tips, please make the delay before displaying them configurable, and let the user set it to 0.
- They generally don't work with keyboard navigation for some reason. Using the cursor position as if it was the mouse seems an easy thing to do. Having a way to toggle on/off the tool-tips, or having to hold a key down to display them while navigating with the keyboard might be necessary or a good feature to have.
- They often cover the surrounding lines which is annoying.

3. Inline display: display the value of the variable in or next to the code in the source file. I never used that so I don't know if it's good. c[ode]clap as a bit of that but it's not helpful at the moment because it's limited to stack variables in the current stack frame, doesn't make it easy to see which value correspond to which variable and the display order is not always the order of the variable in the code.

Here are some link of inline display in other debugger, for reference.
https://developers.google.com/web...ipt-values-inline-while-debugging
https://imgur.com/JxbZm1W
https://www.jetbrains.com/help/idea/2018.1/inline-debugging.html

I copied some code below with different way of doing inline display.

1. Display the value at the end of the source line. Something like
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void test( ... ) {
    
    r32 line_first_character_addin = 0; // line_first_character_addin: 0
    
    u32 metrics_index = font_get_codepoint_metrics_index( f->codepoint_set, f->codepoint_count, codepoint ); // metrics_index: 34, f->codepoint_set: 0x13230000, f->codepoint_count: 193, codepoint: 66
    codepoint_metrics* cm = f->cp_metrics + metrics_index; // cm: 0x1323338c, f->cp_metrics: 0x13222ec4, metrics_index: 34
    r32 width_to_add = 0; // width_to_add: 0
    
    if ( previous_codepoint != encoding_null_codepoint ) { // previous_codepoint: 4294967295, encoding_null_codepoint: 4294967295
        width_to_add = font_get_kerning( f, cm, previous_codepoint ) * f->scale; // width_to_add: 0, f: 0x13230668, cm: 0x1323338c, previous_codepoint: 4294967295, f->scale: 0.007232
    }
    
    width_to_add += cm->advance; // width_to_add: 10.153475, cm->advance: 10.153475
    
    if ( line_first_character ) { // line_first_character: 1
        width_to_add -= cm->x0; // width_to_add: 9.403475, cm->x0: 0.750000
        line_first_character_addin = -cm->x0; // line_first_character_addin: -0.750000, cm->x0: 0.750000
        line_first_character = false; // line_first_character: 0
    }
    
    b32 is_white_space = char_is_whitespace( codepoint ); // is_white_space: 0, codepoint: 66
    
    if ( !current_line.bytes ) { // current_line.bytes: 0x0
        current_line.bytes = current; // current_line.bytes: 0x02c9001f, current: 0x02c9001f
        current_line.rect = r2_min_size( l->content_current_x, l->content_current_y, 0, font_line_height ); // current_line.rect: 0x18bd28, l->content_current_x: 0.0, l->content_current_y: 0.0, font_line_height: 18.0
        wrap_point = current_line; // wrap_point: 0x18bd50, current_line: 0x18bd28
    }
    
    if ( current_line.rect.max.x + width_to_add > l->content_start_x + l->wrap_width ) { // current_line.rect.max.x: 0, width_to_add: 9.403475, l->content_start_x: 0, l->wrap_width: 631.0
        
        if ( is_white_space || l->wrap_on_letters || wrapping_on_letter_temp ) { // is_white_space: 0, l->wrap_on_letters: 1, wrapping_on_letter_temp: 0
            
        }
    }
}

It doesn't seem the best way to me. Identifying the value of a variable is clear, but to get the context you have some work to do. If the variable names or values are long, the end of the line will most likely go out of the view. If line wrapping is on, it could change the layout of the file when a value change. Also depending on the size of the variable name there can be a big space between the value, which can make it hard to read.

2. Same thing but with value before the name.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void test( ... ) {
    
    r32 line_first_character_addin = 0; // 0: line_first_character_addin
    
    u32 metrics_index = font_get_codepoint_metrics_index( f->codepoint_set, f->codepoint_count, codepoint ); // 34: metrics_index, 0x13230000: f->codepoint_set, 193: f->codepoint_count, 66: codepoint
    codepoint_metrics* cm = f->cp_metrics + metrics_index; // 0x1323338c: cm, 0x13222ec4: f->cp_metrics, 34: metrics_index
    r32 width_to_add = 0; // 0: width_to_add
    
    if ( previous_codepoint != encoding_null_codepoint ) { // 4294967295: previous_codepoint, 4294967295: encoding_null_codepoint
        width_to_add = font_get_kerning( f, cm, previous_codepoint ) * f->scale; // 0: width_to_add, 0x13230668: f, 0x1323338c: cm, 4294967295: previous_codepoint, 0.007232: f->scale
    }
    
    width_to_add += cm->advance; // 10.153475: width_to_add, 10.153475: cm->advance
    
    if ( line_first_character ) { // 1: line_first_character
        width_to_add -= cm->x0; // 9.403475: width_to_add, 0.750000: cm->x0
        line_first_character_addin = -cm->x0; // -0.750000: line_first_character_addin, 0.750000: cm->x0
        line_first_character = false; // 0: line_first_character
    }
    
    b32 is_white_space = char_is_whitespace( codepoint ); // 0: is_white_space, 66: codepoint
    
    if ( !current_line.bytes ) { // 0x0: current_line.bytes
        current_line.bytes = current; // 0x02c9001f: current_line.bytes, 0x02c9001f: current
        current_line.rect = r2_min_size( l->content_current_x, l->content_current_y, 0, font_line_height ); // 0x18bd28: current_line.rect, 0.0: l->content_current_x, 0.0: l->content_current_y, 18.0: font_line_height
        wrap_point = current_line; // 0x18bd50: wrap_point, 0x18bd28: current_line
    }
    
    if ( current_line.rect.max.x + width_to_add > l->content_start_x + l->wrap_width ) { // 0: current_line.rect.max.x, 9.403475: width_to_add, 0: l->content_start_x, 631.0: l->wrap_width
        
        if ( is_white_space || l->wrap_on_letters || wrapping_on_letter_temp ) { // 0: is_white_space, 1: l->wrap_on_letters, 0: wrapping_on_letter_temp
            
        }
    }
}


3. Removing the variable name at the end of the lines: fits better in the view but it's not easy to see which value is which variable.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void test( ... ) {
    
    r32 line_first_character_addin = 0; // 0
    
    u32 metrics_index = font_get_codepoint_metrics_index( f->codepoint_set, f->codepoint_count, codepoint ); // 34, 0x13230000, 193, 66
    codepoint_metrics* cm = f->cp_metrics + metrics_index; // 0x1323338c, 0x13222ec4, 34
    r32 width_to_add = 0; // 0
    
    if ( previous_codepoint != encoding_null_codepoint ) { // 4294967295, 4294967295
        width_to_add = font_get_kerning( f, cm, previous_codepoint ) * f->scale; // 0, 0x13230668, 0x1323338c, 4294967295, 0.007232
    }
    
    width_to_add += cm->advance; // 10.153475, 10.153475
    
    if ( line_first_character ) { // 1
        width_to_add -= cm->x0; // 9.403475, 0.750000
        line_first_character_addin = -cm->x0; // -0.750000, 0.750000
        line_first_character = false; // 0
    }
    
    b32 is_white_space = char_is_whitespace( codepoint ); // 0, 66
    
    if ( !current_line.bytes ) { // 0x0
        current_line.bytes = current; // 0x02c9001f, 0x02c9001f
        current_line.rect = r2_min_size( l->content_current_x, l->content_current_y, 0, font_line_height ); // 0x18bd28, 0.0, 0.0, 18.0
        wrap_point = current_line; // 0x18bd50, 0x18bd28
    }
    
    if ( current_line.rect.max.x + width_to_add > l->content_start_x + l->wrap_width ) { // 0, 9.403475, 0, 631.0
        
        if ( is_white_space || l->wrap_on_letters || wrapping_on_letter_temp ) { // 0, 1, 0
            
        }
    }
}


4. Having the source line repeated but changing the names for their values:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void test( ... ) {
    
    r32 line_first_character_addin = 0; // 0 = 0
    
    u32 metrics_index = font_get_codepoint_metrics_index( f->codepoint_set, f->codepoint_count, codepoint ); // 34 = font_get_codepoint_metrics_index( 0x13230000, 193, 66 )
    codepoint_metrics* cm = f->cp_metrics + metrics_index; // 0x1323338c = 0x13222ec4 + 34
    r32 width_to_add = 0; // 0
    
    if ( previous_codepoint != encoding_null_codepoint ) { // if ( 4294967295 != 4294967295 )
        width_to_add = font_get_kerning( f, cm, previous_codepoint ) * f->scale; // 0 = font_get_kerning( 0x13230668, 0x1323338c, 4294967295 ) * 0.007232
    }
    
    width_to_add += cm->advance; // 10.153475 += 10.153475
    
    if ( line_first_character ) { // if ( 1 )
        width_to_add -= cm->x0; // 9.403475 -= 0.750000
        line_first_character_addin = -cm->x0; // -0.750000 = -0.750000
        line_first_character = false; // 0 = 0
    }
    
    b32 is_white_space = char_is_whitespace( codepoint ); // 0 = char_is_whitespace(  66 )
    
    if ( !current_line.bytes ) { // if ( !0x0 )
        current_line.bytes = current; // 0x02c9001f = 0x02c9001f
        current_line.rect = r2_min_size( l->content_current_x, l->content_current_y, 0, font_line_height ); // *0x18bd28 = r2_min_size( 0.0, 0.0, 0, 18.0 )
        wrap_point = current_line; // *0x18bd50 = *0x18bd28
    }
    
    if ( current_line.rect.max.x + width_to_add > l->content_start_x + l->wrap_width ) { // if ( 0 + 9.403475 > 0 + 631.0 )
        
        if ( is_white_space || l->wrap_on_letters || wrapping_on_letter_temp ) { // if ( 0 || 1 || 0 )
            
        }
    }
}


5. Displaying the values in the code, next to each variable.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void test( ... ) {
    
    r32 line_first_character_addin: 0 = 0;
    
    u32 metrics_index: 34 = font_get_codepoint_metrics_index( f->codepoint_set: 0x13230000, f->codepoint_count: 193, codepoint: 66 );
    codepoint_metrics* cm: 0x1323338c = f->cp_metrics: 0x13222ec4 + metrics_index: 34;
    r32 width_to_add: 0 = 0;
    
    if ( previous_codepoint: 4294967295 != encoding_null_codepoint: 4294967295 ) {
        width_to_add: 0 = font_get_kerning( f: 0x3230668, cm: 0x1323338c, previous_codepoint: 4294967295 ) * f->scale: 0.007232;
    }
    
    width_to_add: 10.153475 += cm->advance: 10.153475;
    
    if ( line_first_character: 1 ) {
        width_to_add: 9.403475 -= cm->x0: 0.750000;
        line_first_character_addin: -0.750000 = -cm->x0: 0.750000;
        line_first_character: 0 = false;
    }
    
    b32 is_white_space: 0 = char_is_whitespace( codepoint: 66 );
    
    if ( !current_line.bytes: 0x0 ) {
        current_line.bytes: 0x02c9001f = current: 0x02c9001f;
        current_line.rect: 0x18bd28 = r2_min_size( l->content_current_x: 0.0, l->content_current_y: 0.0, 0, font_line_height: 18.0 );
        wrap_point: 0x18bd50 = current_line: 0x18bd28;
    }
    
    if ( current_line.rect.max.x: 0.0 + width_to_add: 9.403475 > l->content_start_x: 0 + l->wrap_width: 631.0 ) {
        
        if ( is_white_space: 0 || l->wrap_on_letters: 1 || wrapping_on_letter_temp: 0 ) {
            
        }
    }
}


6. Replacing variable names by their value directly in the code. This would need a toggle or hold key behavior.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void test( ... ) {
    
    r32 0 = 0;
    
    u32 34 = font_get_codepoint_metrics_index( 0x13230000, 193, 66 );
    codepoint_metrics* 0x1323338c = 0x13222ec4 + 34;
    r32 0 = 0;
    
    if ( 4294967295 != 4294967295 ) {
        0 = font_get_kerning( 0x3230668, 0x1323338c, 4294967295 ) * 0.007232;
    }
    
    10.153475 += 10.153475;
    
    if ( 1 ) {
        9.403475 -= 0.750000;
        -0.750000 = -0.750000;
        0 = false; // 0
    }
    
    b32 0 = char_is_whitespace( 66 );
    
    if ( !0x0 ) {
        0x02c9001f = 0x02c9001f;
        *0x18bd28 = r2_min_size( 0.0, 0.0, 0, 18.0 );
        *0x18bd50 = *0x18bd28;
    }
    
    if ( 0.0 + 9.403475 > 0 + 631.0 ) {
        
        if ( 0 || 1 || 0 ) {
            
        }
    }
}


7. Copying the line above the source and replacing the variable names by their values.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void test( ... ) {
    // r32                       0 = 0;
    r32 line_first_character_addin = 0;
    // u32         34 = font_get_codepoint_metrics_index( 0x13230000      , 193               , 66        );
    u32 metrics_index = font_get_codepoint_metrics_index( f->codepoint_set, f->codepoint_count, codepoint );
    // codepoint_metrics* 0x1323338c = 0x13222ec4    + 34;
    codepoint_metrics*            cm = f->cp_metrics + metrics_index;
    // r32         0 = 0;
    r32 width_to_add = 0;
    
    // if ( 4294967295      != 4294967295              ) {
    if ( previous_codepoint != encoding_null_codepoint ) {
        //         0 = font_get_kerning( 0x13230668, 0x1323338c, 4294967295         ) * 0.007232;
        width_to_add = font_get_kerning( f         , cm        , previous_codepoint ) * f->scale;
    }
    
    // 10.153475 += 10.153475;
    width_to_add += cm->advance;
    
    // if ( 1                 ) {
    if ( line_first_character ) {
        // 9.403475  -= 0.750000;
        width_to_add -= cm->x0;
        //               -0.750000 = -0.750000;
        line_first_character_addin = -cm->x0;
        //                 0 = 0;
        line_first_character = false;
    }
    
    // b32           0 = char_is_whitespace(  66       );
    b32 is_white_space = char_is_whitespace( codepoint );
    
    // if ( !0x0             ) {
    if ( !current_line.bytes ) {
        //      0x02c9001f = 0x02c9001f
        current_line.bytes = current;
        //      *0x18bd28 = r2_min_size( 0.0                 ,                  0.0, 0, 18.0             );
        current_line.rect = r2_min_size( l->content_current_x, l->content_current_y, 0, font_line_height );
        // *0x18bd50 = *0x18bd28;
        wrap_point   = current_line;
    }
    
    // if (                    0 + 9.403475     >                  0 + 631.0         ) {
    if ( current_line.rect.max.x + width_to_add > l->content_start_x + l->wrap_width ) {
        
        // if (           0 ||                  1 ||                       0 ) {
        if ( is_white_space || l->wrap_on_letters || wrapping_on_letter_temp ) {
            
        }
    }
}


8. Same thing with value centered above their name.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void test( ... ) {
    // r32         0               = 0;
    r32 line_first_character_addin = 0;
    // u32    34       = font_get_codepoint_metrics_index(    0x13230000   ,        193        ,    66     );
    u32 metrics_index = font_get_codepoint_metrics_index( f->codepoint_set, f->codepoint_count, codepoint );
    // codepoint_metrics* 0x1323338c =   0x13222ec4  +      34      ;
    codepoint_metrics*        cm     = f->cp_metrics + metrics_index;
    // r32   0       = 0;
    r32 width_to_add = 0;
    
    // if ( 4294967295      !=        4294967295       ) {
    if ( previous_codepoint != encoding_null_codepoint ) {
        //    0      = font_get_kerning( 0x13230668, 0x1323338c,     4294967295     ) * 0.007232;
        width_to_add = font_get_kerning(      f    ,     cm    , previous_codepoint ) * f->scale;
    }
    
    // 10.153475 +=  10.153475 ;
    width_to_add += cm->advance;
    
    // if (         1         ) {
    if ( line_first_character ) {
        // 9.403475  -= 0.750000;
        width_to_add -=  cm->x0 ;
        //        -0.750000        = -0.750000;
        line_first_character_addin =  -cm->x0 ;
        //        0          =   0  ;
        line_first_character = false;
    }
    
    // b32      0       = char_is_whitespace(     66    );
    b32 is_white_space = char_is_whitespace( codepoint );
    
    // if (      !0x0        ) {
    if ( !current_line.bytes ) {
        //  0x02c9001f     = 0x02c9001f;
        current_line.bytes =  current  ;
        //  *0x18bd28     = r2_min_size(        0.0          ,        0.0          , 0,       18.0       );
        current_line.rect = r2_min_size( l->content_current_x, l->content_current_y, 0, font_line_height );
        // *0x18bd50 =   *0x18bd28;
        wrap_point   = current_line;
    }
    
    // if (          0           +   9.403475   >         0          +      631.0    ) {
    if ( current_line.rect.max.x + width_to_add > l->content_start_x + l->wrap_width ) {
        
        // if (      0      ||         1          ||            0            ) {
        if ( is_white_space || l->wrap_on_letters || wrapping_on_letter_temp ) {
            
        }
    }
}


9. A small test to see the value replacing the names with a toggle. Click on the image to switch between the two states.

All those examples are not representative of what it would be to use them, it just gives a small idea of what it could look like. Some ideas that I though about while writing those examples:
- It could be useful to keep an history of the values display on the screen, while you don't go out of the current stack frame or scope. If you have

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int x = 0;
x++;
x++;

// it would display:

int x = 0; x: 0
x++; x: 1;
x++; x: 2;

// instead of

int x = 0; x: 2
x++; x: 2;
x++; x: 2;


- While doing pointer arithmetic some calculation might look of if only the values are shown.
- If value are displayed directly in the code, there is a need to align both the code and values to easily match what value correspond to which variable.
- Duplicating the line above to display takes a lot of space.
- Several idea would need a toggle or hold key behavior to work. But in those case, the formatting of the line should not change between the two modes.
- Having the value outside the code makes it harder to process what is the whole expression value. For example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if ( !value ) { // value: 0

// is harder to read than

if ( !0 ) {

// but since I want to keep the formating

if ( !  0   ) { // is not that good either

// Should it be

if (   !0   ) {


- Displaying compound type (struct, union...): displaying all field of a struct next to each other doesn't seem useful and sometimes can be counter productive. See the watch panel section for a way to solve that.

- Displaying the value of things after the cursor, before we step on them seems like it could be useful.

I though that 9 would be the better idea, but I think I like 8 better. And 5 seem to work too. The problem of 8 is vertical space which I would have to test to know if it's really an issue. 5's problem is horizontal space which depends on the panel width (obviously) but also on line wrapping. But since I haven't work with any of those, they might all be terrible ideas.

Also the "line tool-tip" idea that display the code line but replace the names with the values seems an easy improvement of tool-tips.


= Stepping =

I would like the source panel to show me in which function I would step into if I pressed the step into key, or a way to show the order of the function calls on a single line. A way to have several step on a single line if there are several function calls in that line would be useful.

1
int x = fun_1( fun_2( ), fun_3( ) ) + fun_4( );


On that line I want to know in which function step into would go. If the order is fun_3, fun_2, fun_1, fun_4, I would like a step key that would run the next function but allow me to step in the next one. For example line_step. I would line_step twice, and then step_into would step in fun_1.

I would like "run to cursor" that would ignore breakpoints without the need to remove them.

A thing that would be useful is breakpoints group, to be able to enable or disable breakpoints in a certain part of the program. Automatic group might be useful too, for example all the breakpoints in one function would be a group.
A way to enable a breakpoint or group only when another breakpoint was reached (conditional breakpoint).
Enable breakpoints only if the execution passed on a certain line. For example if I have 100 test that call a function that contains a breakpoint and I only want to break on a particular test. This can be achieved by putting a breakpoint on the particular test and disabling the breakpoint in the function, running until I reach the test breakpoint and then enabling the function breakpoint. But that's annoying to do especially if you need to do that several times.


= Watch panel =

The minimum thing the watch panel needs to be able to do is to display the value of any valid memory location as a particular type. Of course we don't want to have to type memory address all the time, so it implies having a way to parse expressions to get those memory addresses. For those expressions most of the time I expect to be able to type the same expressions I would in C (or cpp) to access a variable and get the value. That would be the minimum thing to have to be able to debug.

Other syntax are expected such as the array syntax to display a pointer as an array, or being able to use '.' or '->' interchangeably. Before I suggest other small syntax, I would like to mention a few things on the presentation of watch panel.

One things that I often wish was present in Visual Studio is the ability to display an integer value in several form at the same time. More precisely, some values I want to see in hexadecimal, and some I want to see in decimal but Visual Studio only has a global toggle. Since it would probably be annoying to have to choose for each variable in which format to display it, showing several format at the same time seems a good compromise. The format I want are decimal, hexadecimal and ASCII. Octal and binary would be nice too but I would use those less often. In the watch panel, this could be either achieved by adding columns in the panel or having a way to configure the format of the value column. But I also would like the ability to display several format in tool-tips or inline display. ASCII display should be available for all integer type, by using only the lower bits. This is useful if you got a Unicode codepoint (32 bit integer) since the first 127 values correspond exactly to ASCII.

Support for Unicode strings (utf-8, utf-16, utf-32) would be useful too. The user would need to specify the format for those with a simple syntax, something like "pointer, codeunit_count, utf_8" or "utf_8(pointer, codeunit_count)" where codeunit_count is the number of u8, u16 or u32 to use to display the string. codeunit_count could be optional, but if it's specified, the display should not stop on the null character.
If characters are badly encoded, you should either display the unicode replacement character (0xfffd) or the codeunit value in hexadecimal (\xValue), then advance 1 codeunit and continue decoding. Having the choice of what to do would be a nice option.
If the debugger doesn't know how display the character I would like an option to either display the replacement character or the codepoint value in hexadecimal (\xValue).

Being able to configure how certain types are displayed: removing leading 0 on addresses, removed trailing 0 on floating points, setting the number of decimal digit to display, whether or not to use the scientific notation, displaying hexadecimal by group of two characters, forcing upper case/lower case in hexadecimal.

Displaying compound types on a single line in the watch panel often isn't useful. A way to specify how to display those when you need a short version (single line in the watch window, in a tool-tip or inline display) would be useful. What I mean is that the user would tell the debugger that whenever it encounters type X and needs to display it in it's short form, it should build a string using the user settings. This should be a debugger project configuration, not part of the program being debugged.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
typedef struct data {
    umm reserved;
    umm used;
    union {
        void* raw;
        u8* bytes;
        char* text;
        umm offset;
    };
} data;

data watch_test = { 27, 26, (void*) "the content of the string." };

/* The user would define somewhere in the debugger: */
data: { "String: \"", utf_8(bytes, used), "\"" }

/* That would display in the watch window: */
String: "the content of the string."


As mentioned previously the watch panel should try to use the available space to display useful information. No unnecessary margin or indentation, smallest line spacing possible, no buttons to do rarely done things (so no buttons in the watch panel). It's useful to be able to resize the columns.

If there is an image (+,-, triangle) to expand compound types, align all lines as if they could be expanded even if they don't and keep the alignment and indentation increment consistent across the whole tree.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
+struct
 var
+struct
 var
-struct
 +struct
  var
-struct
 -struct
   var
   var
  var
 +struct


- Pay attention to performance when displaying "big" arrays.

- Editing expressions should be easy. Going on a line and starting to type should start editing the line. Alternatively, having to press enter (or another key) to start editing a value might be a good option to have because it would allow user to use letter keys to navigate the list (like hjkl in Vim). While editing, navigation keys (arrows, home, end mainly) should navigate the text value, not the list of expressions. Pressing enter while typing an expression should finish the editing and go to the next line in with the edition started.

- A quick way to clear every expression in the watch window would be great.
- Clicking with the mouse in the empty space of the watch window should put the cursor on the "new expression" slot.
- Reordering lines in the watch panel would be useful.
- Keeping the content of the watch panel in the project file would be useful.
- Keeping the expanded status of compound type would be useful. For example upon re-entering a scope, restarting debugging and maybe across restart of the debugger.
- Keep the values of things that where defined at some point in a function stack frame available to see.
- Editing the value of variables in the watch window is useful.
- Executing a function, from the debugged application by typing the function name and its parameters in the watch window is really useful. A way to call the function again without retyping the expression is also useful. Maybe the return value could be displayed in the value column.
- Copy/past the names, the values, or both should be easy. And the copied text should be easy to read with a good layout when you past it. Maybe respecting the C format so the text can be pasted in code directly.
1
{field_1_value, field_2_value, { sub_field_1_value, sub_field_2_value} }


- When displaying arrays, instead of each line having only [x] in the name part, add the address of that element. This is really useful when you have a pointer to an element and need to see which element index it is or which elements are around it.
1
2
3
4
5
-arrayVariable, 10
 [0] 0x12340000 {a=0, b=0}
 [1] 0x12340010 {a=0, b=0}
 [2] 0x12340020 {a=0, b=0}
 ...


Additional syntax for expressions:

- If I expand any compound type that contains a pointer, I want to be able to use the "pointer, size" on that pointer. With size either a integer literal or a variable name (see next point). For example if I have a struct that is struct { int x; char* s; }; I would like to be able to have this in the watch window.
1
2
3
4
5
6
-struct_var
  x                     0
 -s, 10 <-- this in a compound type is not available in visual studio
   [0] 0x123400         'a'
   [1] 0x123401         'b'
   ...


- "pointer, variable_containing_an_integer_type" : display the pointer as an array, the number of element is contained in variable_containing_an_integer_type. And that variable in itself could be an expression. This is useful to display strings or buffer which often group the size with the pointer in a structure. And when you create a string in a loop, to see the string grow in the watch window without relying on a null terminator.

A "parent" keyword would be useful: it would be a pointer to the variable containing the "pointer" if any. For example:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct string { char* b; u32 size; };

-a_struct
 -a_struct
  -a_string
   -b, parent->size /* parent points to a_string, to avoid having to type a_struct->b_struct->a_string->size */
     [0] 0x123400  'a'
     [1] 0x123401  'b'
    size           2

// or

-a_struct->a_struct->a_string->b, parent->size
  [0] 0x123400  'a'
  [1] 0x123401  'b'


But maybe we could also skip the parent part and assume that what is after the coma is first resolved with the scope of the variable definition and than in the scope of the stack-frame.

- I would like to be able to pin variables: it would create a new line in the watch panel with the variable name changed to the address and casted to the right type so it can be displayed after it goes out of scope. That was the "pluck" feature in CDBG. For example:

1
2
3
4
5
+a_struct

// pining it would create a line

+*( type* )0x12345678 // a_struct


The comment or something similar would allow to keep the name as a reminder of what the variable is. Having a feature that automatically pins variables that go out of scope would be nice. Maybe this idea could be better implemented if the expression somehow always kept the context in witch it was created and the memory address it points to.


= String viewer =

When displaying a char* in visual studio there is a small magnifying glass that you can use to get a window that display strings. That's useful to me as I work a lot with strings in my current project, but Visual Studio's string viewer is pretty bad. Here are some things I'd like for such a string viewer:

- It shouldn't be modal, it should be a panel like the watch panel or memory panel.
- It needs to be updated when you step in the code, so I can see a string being built.
- It should be able to display ASCII, utf-8, utf-16 (be or le) and utf-32.
- It should be able to use null terminated strings as well as getting the size of the string (and not stop at the null terminator in that case).
- The size could be a integer literal or a variable name.
- I should be used as little space as possible. For example the fields where you specify the pointer and size should not be visible after you set them.
- Should be easy to open from the watch panel (magnifying glass on any integer type).
- Same constraint as the watch panel for Unicode strings.
- Editing the text would be nice, but I can't say that I ever needed that.
- Search would be useful.
- Line wrapping that can be easily toggle on or off.
- It could be used to display user logs.


= Autocomplete =

I would like to have auto-complete in the debugger, and for it to work in any place where it's relevant. In the watch panel, memory panel, break point panel... But if you implement that feature, don't force it on me. I want to ask for auto-completion when I need it, I don't want it to show up whenever I type a character.

There are two type of auto-complete that I know of:
- Window based: when auto-completing a window appears that lists the available choices. I don't like that very much as it will cover other things in the UI that I might be referring to while typing. This has the advantage of showing multiple possible auto-complete words, and the order in which they are in the list.

- line based (tab auto-complete): my preferred way to use auto-complete is to cycle through the available choices by pressing a key (generally tab). When you press the key, the word you were typing is completed with the first choice from the list, pressing the key again will use the second word and so on. Pressing any other key will just "commit" the choice and you can continue typing directly. If you cycle the whole list you come back to the initial text you typed. Maybe a key (escape) could be use to cancel the autocomplete in one key press instead of having to cycle through all choices. While it's not common for line based auto-complete to display the next items in the list, we could display them as greyed text after the current word. Limiting the number of items that are displayed would be useful to keep the text that would follow in the view. We could imagine to only display suggestions as long as they don't make the end of the line go out of screen. In a debugger this doesn't seem like a big issue since most of the time we are only typing small amount of code and the rest of the line will be empty.

The auto-complete from c[ode]clap made me ask myself if it's necessary to differentiate between the different type of things that could be use in the auto-complete such as local variable, global variable and function.

The distinction between local and global variable isn't necessary in my opinion. Most of the time I name global variable with a prefix that will reduce the amount of possible choices in the list. If the local and global variables were all in the same list, we could make the differentiation with an icon or text color (text color seems better as it would work best in the line based auto-complete). Also choosing which type comes first is an interesting option (but I think in general local variables should be first).

Differentiating between function and variable I could see useful in code editing but less in a debugger. In a debugger, depending on the context you will probably want variable only or function only. Or at least have one before the other in the order they appear. For example in the watch panel you most likely want to display variables and in the breakpoint panel you most likely want to display functions. Maybe having several key bindings to use different lists would be useful: having one to display only variable, one for functions, and one for both.

A remark about c[ode]clap autocomplete is that it takes a huge amount of space that could contains pertinent information that I want to refer to while typing. Also it's not "on demand".
Abner Coimbre
320 posts
Founder
Brain dump about debugger
This is the kind of user brain dump that's productive. I only read up to the UI section, but already found plenty of actionable advice.

I recommend project owners develop the skill to digest brain dumps and, in the process, extract small TODO items to informally categorize them on a master list. They should be small and build up to a bigger task getting completed. Thus, "Improve command-line argument passing" is not useful, but:

  • Support -executable to set my internal exe_name char buffer to a user argument

  • Set working_directory to executable directory

  • Implement arenas as a new allocation scheme // First approx. to manage config files, or something

  • NOTE: That last item about arenas is actually too broad still

    And whatever you do, make the list as accessible and as easy to cross off items as possible. As one example, I use the Trapani method with Todour. Every day I have to work on the three major OSes, so I also use Dropbox (and Todour knows how to find the file there.)

    Pay the cost of learning the method once, find users like Simon, and never look back.
    Oliver Marsh
    193 posts / 1 project
    Olster1.github.io
    Brain dump about debugger
    Yes, I think this kind of brain dump is like gold if it relates to your project.
    Love hearing what you have to say about your workflow Abner, don't want to hijack the thread, but would love to hear what you have to say about prioritising what to learn.
    Abner Coimbre
    320 posts
    Founder
    Brain dump about debugger
    Edited by Abner Coimbre on Reason: Initial post.
    OliverMarsh
    Love hearing what you have to say about your workflow Abner, don't want to hijack the thread, but would love to hear what you have to say about prioritising what to learn.


    If I'm starting a new task, say for work, the process is straightforward: list all the goals; break the goals into mini-goals; break each mini-goal into vague tasks; make the vague tasks more specific; make the specific tasks more specific; make the specific tasks (that were made more specific) more specific. Then assign them priorities from A to Z, A being "this needs to be done ASAP" and Z being "Eh." In practice I never go past the letter D. Note that multiple items may share the same letter.

    Since the TODO items are now so tiny I get to cross them off regularly throughout the day, which makes my brain happy and keeps me going. To be motivated to do this every day, I think the system you use should be super simple -- hence my Trapani / Todour suggestion earlier.