Recently I was working on a GStreamer
plugin
in Rust. The plugin basically rounds the corners of an incoming video,
something akin to the border-radius
property in CSS. Below is how it looks
like when running on a video.
The GStreamer pipeline for the same:
gst-launch-1.0 filesrc location=~/Downloads/bunny.mp4 ! \
decodebin ! videoconvert ! \
roundedcorners border-radius-px=100 ! \
videoconvert ! gtksink
This was my first time working on a video plugin in GStreamer. Had a lot to
learn on how to use the BaseTransform
class from GStreamer, among other
things. Without getting into the GStreamer specific details here, I basically
ran into a problem for which I needed to do some debugging for figuring out
what was going on in the internals of GStreamer.
Now, while I never had problems using GDB from the command line, but, the way I was using it earlier was just not good enough. I would start the pipeline, then attach gdb to a running process, place breakpoints by manually typing out the whole thing and then start. For one off debugging sessions, where may be you just want to quickly inspect the backtrace from a crash or may be look into a deadlock condition where your code hung, this could be fine. However, when you have to repeat this multiple times, do a source code change, compile and then check again, it becomes frustrating.
Let's look at how we can make this easier.
GDB Dashboard
Looking for a better way, I first stumbled on gdb-dashboard. This is quite useful since it can give the needed information without having to type anything. Using gdb hooks, the dashboard can be triggered when appropriate. See the rest of my gdb configuration to get an idea. I use this in scenarios like where code is stuck due to a deadlock, I need to look at the backtrace of a crash or any such one off simple investigation.
Construct breakpoint command in neovim & copy to clipboard
The next small improvement I did was more specific to my use of neovim. I generally navigate source code using neovim, which would be opened in one kitty tab and gdb would be running in terminal in next tab or a split. Wanted to be able to quickly place a breakpoint without having to type anything out on the gdb prompt. Wrote a small piece of vimscript code which generates the gdb command, I would have to type on the gdb prompt to enable a breakpoint, considering the current line and file on which my cursor is at in the source when opened in neovim.
function! CopyBpLocToClipboard() abort
let linenumber = line(".")
let filepath = expand("%")
let breakpoint = "break " . filepath . ":" . linenumber
silent execute "!wl-copy " . breakpoint
endfunction
nnoremap <silent> <Leader>yb :<C-U>call CopyBpLocToClipboard()<CR>
So I can hit the key binding above and a command like below will be copied to the clipboard which I can paste on gdb prompt.
break subprojects/gst-plugins-base/gst-libs/gst/video/video-frame.c:104
Nifty!!!
GDB scripting
Now imagine a scenario where may be one wants to look at multiple places in the source code and when the program is running, inspect certain variables or just print out a back trace each time a specific code point is reached.
The manual way to do this and the way which I was also doing it earlier, was to load the executable in gdb or attach to a running process, place a break point, run, inspect the local variables or print stack trace, place the next break point and repeat this whole process. Just time consuming and a waste of time.
GDB can completely automate the above process. Let's see how.
Below is the .gdbinit
file I came up with for my problem. This is what is
called a command file by gdb.
set confirm off
set breakpoint pending on
set logging on
set logging overwrite on
set print pretty on
set pagination off
break subprojects/gst-plugins-base/gst-libs/gst/video/video-frame.c:104 if meta->n_planes == 4
break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:228
break subprojects/gstreamer/gst/gstbuffer.c:1410
break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:231
break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:237
break subprojects/gst-plugins-base/gst-libs/gst/video/video-frame.c:136
commands 1
print i
print *frame
enable 2
enable 3
enable 4
enable 5
enable 6
continue
end
commands 2
print offset
continue
end
commands 3
print offset
print size
continue
end
commands 4
print *(GstBufferImpl *)buffer
print idx
print length
print skip
continue
end
commands 5
disable 2
disable 3
disable 4
print *(GstBufferImpl *)buffer
print info->data
print skip
print *data
continue
end
commands 6
print *frame
quit
end
disable 2
disable 3
disable 4
disable 5
disable 6
run
The command I was using to debug my GStreamer plugin in this pipeline with gdb.
gdb --nx -x .gdbinit --args \
env RUST_BACKTRACE=1 GST_DEBUG=3,basetransform:6 \
GST_PLUGIN_PATH=$GST_PLUGIN_PATH:~/GitSources/gst-plugins-rs/target/debug \
gst-launch-1.0 filesrc location=~/Downloads/bunny.mp4 ! \
decodebin ! videoconvert ! \
roundedcorners border-radius-px=100 ! \
videoconvert ! gtksink
In the command above, the -x
parameter tells gdb to use the command file. The
--nx
flag tells gdb to not read any any .gdbinit
files in any directory, as
I did not want to use gdb-dashboard
for this. --args
is how I tell gdb what
to run, which is my GStreamer pipeline. You can see gdb --help
for details on
the flags.
Now, let's understand what the command file does. The ones below are just some settings we want gdb to use. Note that we have turned on logging and pretty printing.
set confirm off
set breakpoint pending on
set logging on
set logging overwrite on
set print pretty on
set pagination off
Next we specify the breakpoints. We have six breakpoints. These are the locations which were of interest to me.
break subprojects/gst-plugins-base/gst-libs/gst/video/video-frame.c:104 if meta->n_planes == 4
break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:228
break subprojects/gstreamer/gst/gstbuffer.c:1410
break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:231
break subprojects/gst-plugins-base/gst-libs/gst/video/gstvideometa.c:237
break subprojects/gst-plugins-base/gst-libs/gst/video/video-frame.c:136
Breakpoints can be enabled conditionally. The if meta->n_planes == 4
implies
to consider this breakpoint only when we get a video frame with 4 planes.
We can now tell gdb what should it do when each of the breakpoint above is hit.
commands 1
print i
print *frame
enable 2
enable 3
enable 4
enable 5
enable 6
continue
end
commands 1
implies these are the commands for gdb to execute when breakpoint
1 is hit. When breakpoint 1 is hit, it will print the value of i
and frame
.
The other breakpoints get enabled only after the first one is hit. This is
because at the end of command file, we have
disable 2
disable 3
disable 4
disable 5
disable 6
which tells gdb to start with these breakpoints disabled. They will get enabled
only when we hit breakpoint 1. The continue
just tells gdb to continue, as we
do not want to stop on hitting a breakpoint and only want to inspect in the end
using gdb log.
Similarly we have for other breakpoints.
The run
at the end tells gdb to start running immediately. In normal usage
one would have to explicitly type run
on the gdb prompt to make gdb start
debugging.
If it is not clear so far, basically whatever gdb commands we would have used for debugging at the gdb prompt, we now use them in the command file.
Now, since we turned on logging after running the below on the terminal
gdb --nx -x .gdbinit --args \
env RUST_BACKTRACE=1 GST_DEBUG=3,basetransform:6 \
GST_PLUGIN_PATH=$GST_PLUGIN_PATH:~/GitSources/gst-plugins-rs/target/debug \
gst-launch-1.0 filesrc location=~/Downloads/bunny.mp4 ! \
decodebin ! videoconvert ! \
roundedcorners border-radius-px=100 ! \
videoconvert ! gtksink
gdb will run the pipeline, considering the command file it was passed and log
whatever it was asked to log when each breakpoint is encountered. And now since
we had turned on logging and pretty printing, gdb will nicely log everything in
default gdb.txt
file. You can see the exact log text file
here,
where I have attached the gdbinit
and the other two log files.
Now, one can comfortably look at this log and see what is going on. Once the command file is written, the whole debugging process is completely automated. Run, sit back and then look at the logs.
Using gdb is now a breeze and hassle free experience. Being able to automate and log the debugging process like this, also means you could share your command file and someone else can replicate this. Wish I would have learned this sooner.
(Note: This post was originally published here).