Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unnecessary -var-create and -var-delete commands during a stack trace can cause noticeable pause each time the debuggee stops #1349

Closed
gareth-rees opened this issue Sep 6, 2022 · 5 comments · Fixed by #1400

Comments

@gareth-rees
Copy link
Contributor

gareth-rees commented Sep 6, 2022

Summary

Each time the debuggee stops, Visual Studio Code requests a stack trace of the threads that are expanded in the CALL STACK window. Turning on engineLogging shows that MIEngine implements a stack trace for a thread by issuing a -stack-list-arguments for the thread, and then issuing a -var-create and -var-delete command for every argument in every frame in the stack.

When there are many stack frames with many arguments, or if the debugger is behind a sluggish network connection, or if the user installs a hook that runs on every GDB prompt, the time taken to execute these commands can amount to a significant delay.

These -var-create and -var-delete commands ought to be unnecessary as explained below.

Reproduction

  1. Put this program in a suitable file, say recurse.c:

    static void
    r(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j)
    {
        if (a)
        {
            r(b, c, d, e, f, g, h, i, j, 0);
        }
    }
    
    int
    main(void)
    {
        r(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        return 0;
    }
    
  2. Compile it with debugging information, for example gcc -g -o recurse recurse.c.

  3. Open Visual Studio Code and create a new launch configuration using the "(gdb) launch" template, supplying recurse as the value for the "program" key, and turning on engineLogging:

    "logging": {
        "engineLogging": true
    }
    
  4. Select View ⟶ Run to switch to the run panel, select the new launch configuration in the RUN AND DEBUG dropdown, and press F5 to start debugging.

  5. Click "Step Into" a few times until there are multiple frames on the stack.

  6. Look at the DEBUG CONSOLE to see the GDB/MI commands sent by MIEngine. I took a copy of the commands issued by a single Step Over operation with ten frames on the stack and attached them here: stack-list-arguments.gz. Here's a summary of the commands issued by MIEngine to GDB:

    Command Count
    -stack-list-arguments 2
    -stack-list-frames 1
    -stack-list-variables 1
    -stack-select-frame 20
    -var-create 210
    -var-delete 210

    If each of the -var-create and -var-delete commands takes only a millisecond due to network lag or a GDB before-prompt hook, then there's a 0.4-second pause each time the debuggee stops.

Analysis

You can see from the log output that each -var-create is followed immediately by a -var-delete. This means that MIEngine is only issuing these commands to collect information about the variables (not to get a variable reference that it can later use). Why is this? The culprit is DebuggedProcess.GetParameterInfoOnly() which has a comment explaining what's happening:

// If values are requested, request simple values, otherwise we'll use -var-create to get the type of argument it is.
var frames = await MICommandFactory.StackListArguments(values ? PrintValue.SimpleValues : PrintValue.NoValues, thread.Id, low, high);

The problem is that GetParameterInfoOnly() was called with values = false and so it passed PrintValue.NoValues to the -stack-list-arguments call and then followed up with -var-create and -var-delete commands to get the types of the arguments.

The reason why values = false here is that AD7DebugSession.HandleStackTraceRequestAsync() did not set the FIF_FUNCNAME_ARGS_VALUES flag when calling AD7Thread.EnumFrameInfo(). And the reason why this flag is unset is that VSCode did not pass a format argument to the StackTrace request, and so we are in the "default format" case:

// No formatting flags provided in the request - use the default format, which includes the module name and argument names / types
flags |= enum_FRAMEINFO_FLAGS.FIF_FUNCNAME_MODULE |
enum_FRAMEINFO_FLAGS.FIF_FUNCNAME_ARGS |
enum_FRAMEINFO_FLAGS.FIF_FUNCNAME_ARGS_TYPES |
enum_FRAMEINFO_FLAGS.FIF_FUNCNAME_ARGS_NAMES;

Solution ideas

  1. The default format could include FIF_FUNCNAME_ARGS_VALUES: then GetParameterInfoOnly() would pass PrintValue.SimpleValues to the the -stack-list-arguments call, which would return the types immediately and there would be no need for the subsequent -var-create and -var-delete calls.

  2. GetParameterInfoOnly() could pass PrintValue.SimpleValues to the the -stack-list-arguments call if either types or values were required, discarding the values information if it is not needed.

Software versions

Cpptools extension: 1.12.4
VSCode: 1.71.0
Commit: 784b0177c56c607789f9638da7b6bf3230d47a8c
Date: 2022-09-01T07:25:10.472Z
Electron: 19.0.12
Chromium: 102.0.5005.167
Node.js: 16.14.2
V8: 10.2.154.15-electron.0
OS: Linux x64 5.4.0-124-generic
Sandboxed: No

gareth-rees added a commit to undoio/MIEngine that referenced this issue Sep 6, 2022
…ues.

This reduces the number of round-trips to the debugger, because when
values are requested, the `-stack-list-arguments` command also returns
the type of each argument immediately, avoiding the need for
subsequent `-var-create` and `-var-delete` commands to get these
types.
@gregg-miskelly
Copy link
Member

It feels like the best solution here is to have a launch.json knob to control the default stack trace format (and ideally context menu controls in the call stack window to change it during a debug session).

@gareth-rees
Copy link
Contributor Author

Maybe—but first I think it is worth trying to see if GDB can be fixed. The underlying problem is that the GDB/MI command -stack-list-arguments --simple-values is supposed to print simple values like integers and not print compound values like structures, so that the whole stack can be printed in reasonable time. The problem noted in #673 is that --simple-values does not work as expected in C++: in particular, references to arbitrarily large arrays, structures, and unions are printed.

I think this is a bug, or at least an important oversight, in GDB, so I have filed bug 29554 together with a patch to GDB that updates --simple-values so that it does not print references to arrays, structures, and unions. If I can get this merged, then MIEngine can be updated to take advantage of the new behaviour.

(It might be that the GDB maintainers won't agree that this is a bug, but if so I can try again, this time leaving --simple-values unchanged and adding a new print-values option that has the desired behaviour.)

@gregg-miskelly
Copy link
Member

Even with that, I think we would still need a knob to turn off values for folks using older versions of GDB (unless we want to do version sniffing and turn it off by default).

@gareth-rees
Copy link
Contributor Author

GDB/MI has built-in feature-detection via the -list-features command—you'll see that in my patch on bug 29554 I added a feature for the new behaviour of --simple-values which MIEngine could consult.

@gregg-miskelly
Copy link
Member

Sounds reasonable. BTW: Due to GDB's licensing, I am not allowed to look at GDB source code.

gareth-rees added a commit to undoio/MIEngine that referenced this issue Nov 11, 2022
Newer versions of GDB support the `--scalar-values` option (with value
3) to the `-stack-list-arguments`, `-stack-list-locals` and
`-stack-list-variables` commands. This option fetches names, types,
and values for scalar types, or names and types only for non-scalar
types.

Use this in `DebuggedProcess.GetParameterInfoOnly()` if it is
available, as this gets all the required information without the
potential performance penalty of the `--simple-values` option which
potentially fetches large values for references to arrays, structures,
and unions.

New method `MICommandFactory.SupportsScalarValues()` determines if
`--scalar-values` is supported, using the `-list-features` command and
checking for the presence of the `"scalar-values"` feature. We cache
the result on the `DebuggedProcess` object, as the set of supported
features does not change during the lifetime of the debug session.
gareth-rees added a commit to undoio/MIEngine that referenced this issue Nov 11, 2022
Newer versions of GDB support the `--scalar-values` option (with value
3) to the `-stack-list-arguments`, `-stack-list-locals` and
`-stack-list-variables` commands. This option fetches names, types,
and values for scalar types, or names and types only for non-scalar
types.

Use this in `DebuggedProcess.GetParameterInfoOnly()` if it is
available, as this gets all the required information without the
potential performance penalty of the `--simple-values` option which
potentially fetches large values for references to arrays, structures,
and unions.

New method `MICommandFactory.SupportsScalarValues()` determines if
`--scalar-values` is supported, using the `-list-features` command and
checking for the presence of the `"scalar-values"` feature. We cache
the result on the `DebuggedProcess` object, as the set of supported
features does not change during the lifetime of the debug session.
gareth-rees added a commit to undoio/MIEngine that referenced this issue Nov 11, 2022
Newer versions of GDB support the `--scalar-values` option (with value
3) to the `-stack-list-arguments`, `-stack-list-locals` and
`-stack-list-variables` commands. This option fetches names, types,
and values for scalar types, or names and types only for non-scalar
types.

Use this in `DebuggedProcess.GetParameterInfoOnly()` if it is
available, as this gets all the required information without the
potential performance penalty of the `--simple-values` option which
potentially fetches large values for references to arrays, structures,
and unions.

New method `MICommandFactory.SupportsScalarValues()` determines if
`--scalar-values` is supported, using the `-list-features` command and
checking for the presence of the `"scalar-values"` feature. We cache
the result on the `DebuggedProcess` object, as the set of supported
features does not change during the lifetime of the debug session.
gareth-rees added a commit to undoio/MIEngine that referenced this issue May 5, 2023
In newer versions of GDB, the `--simple-values` option to the
`-stack-list-arguments`, `-stack-list-locals` and
`-stack-list-variables` commands no longer prints the value for
references to compound types. This improves the performance of these
commands when the stack has references to large data structures.

When these versions of GDB are available, take advantage by using
`--simple-values` in `DebuggedProcess.GetParameterInfoOnly` to fetch
names and types in a single `-stack-list-arguments` command. This is
faster than the old approach of using `--no-values` followed with
`-var-create` and `-var-delete` for each parameter to get the type.

The new method `MICommandFactory.SupportsSimpleValuesExcludesRefTypes`
determines if the debugger supports the improved behaviour of the
`--simple-values` option, by executing the `-list-features` command
and checking for the `"simple-values-ref-types"` feature in the
output. We cache the result on the `DebuggedProcess` object as the set
of supported features does not change during the lifetime of the debug
session.
gareth-rees added a commit to undoio/MIEngine that referenced this issue May 26, 2023
In newer versions of GDB, the `--simple-values` option to the
`-stack-list-arguments`, `-stack-list-locals` and
`-stack-list-variables` commands no longer prints the value for
references to compound types. This improves the performance of these
commands when the stack has references to large data structures.

When these versions of GDB are available, take advantage by using
`--simple-values` in `DebuggedProcess.GetParameterInfoOnly` to fetch
names and types in a single `-stack-list-arguments` command. This is
faster than the old approach of using `--no-values` followed with
`-var-create` and `-var-delete` for each parameter to get the type.

The new method `MICommandFactory.SupportsSimpleValuesExcludesRefTypes`
determines if the debugger supports the improved behaviour of the
`--simple-values` option, by executing the `-list-features` command
and checking for the `"simple-values-ref-types"` feature in the
output. We cache the result on the `DebuggedProcess` object as the set
of supported features does not change during the lifetime of the debug
session.
gareth-rees added a commit to undoio/MIEngine that referenced this issue May 26, 2023
WardenGnaw pushed a commit that referenced this issue May 30, 2023
* [#1349] Always use `--simple-values` in newer versions of GDB.

In newer versions of GDB, the `--simple-values` option to the
`-stack-list-arguments`, `-stack-list-locals` and
`-stack-list-variables` commands no longer prints the value for
references to compound types. This improves the performance of these
commands when the stack has references to large data structures.

When these versions of GDB are available, take advantage by using
`--simple-values` in `DebuggedProcess.GetParameterInfoOnly` to fetch
names and types in a single `-stack-list-arguments` command. This is
faster than the old approach of using `--no-values` followed with
`-var-create` and `-var-delete` for each parameter to get the type.

The new method `MICommandFactory.SupportsSimpleValuesExcludesRefTypes`
determines if the debugger supports the improved behaviour of the
`--simple-values` option, by executing the `-list-features` command
and checking for the `"simple-values-ref-types"` feature in the
output. We cache the result on the `DebuggedProcess` object as the set
of supported features does not change during the lifetime of the debug
session.

* fixup! [#1349] Always use `--simple-values` in newer versions of GDB.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants