Annotations

Annotations are extra information added to the execution history when a program is recorded. This information can later be queried using the info annotations and ugo annotation commands in UDB.

For example, a unit test program might use this feature to annotate the start of each test case with the name of the test, making it easy to jump to the start of a failed test. A server program might annotate the start of each request from the client, making it easy to jump to the start of a failed request.

Annotations are implemented by the functions in the libundoex library and declared in undoex/undoex-annotations.h. For each annotation, the caller provides the following information:

name

A short string identifying the annotation.

detail

An optional short string distinguishing annotations with the same name.

content

Optional data associated with the annotation: an integer, a null-terminated string, or raw binary data.

Creating annotations

Here’s an example of a unit test program using undoex_annotation_add_int() to annotate the start and end of each test case.

/* Using undoex-annotations to annotate test cases with pass or fail. */

#include <assert.h>
#include <setjmp.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

#include "undoex-annotations.h"

/* A simple unit test framework. Define a test using the TEST macro. A test
 * passes if it reaches the end of its function without raising a signal, or
 * fails if it raises SIGABRT. */

/* Array of test cases. */
static struct test
{
    const char *name;
    void (*function)(void);
} *tests;
static int n_tests;

/* Define a test case. */
#define TEST(NAME)                                             \
    static void test_##NAME(void);                             \
    static void __attribute__((constructor))                   \
    register_##NAME(void)                                      \
    {                                                          \
        tests = realloc(tests, (n_tests + 1) * sizeof *tests); \
        struct test *t = &tests[n_tests++];                    \
        t->name = #NAME;                                       \
        t->function = test_##NAME;                             \
    }                                                          \
    static void test_##NAME(void)

/* Jump buffer for recovery after SIGABRT. */
static sigjmp_buf jmpbuf;

/* Signal handler for SIGABRT. */
static void
handler(int signum)
{
    siglongjmp(jmpbuf, signum);
}

int
main(void)
{
    srand(2);
    signal(SIGABRT, handler);
    bool all_pass = true;
    int i;
    for (i = 0; i < n_tests; ++i)
    {
        const struct test *t = &tests[i];
        bool pass = false;
        if (!sigsetjmp(jmpbuf, true))
        {
            undoex_annotation_add_int(t->name, "start", i);
            t->function();
            pass = true;
        }
        undoex_annotation_add_int(t->name, "result", pass);
        printf("%s: %s\n", t->name, pass ? "pass" : "fail");
        all_pass &= pass;
    }
    return all_pass ? EXIT_SUCCESS : EXIT_FAILURE;
}

TEST(passing)
{
    /* A passing test. */
}

TEST(failing)
{
    /* A failing test. */
    abort();
}

TEST(flaky)
{
    /* A flaky test. */
    assert(rand() % 3);
}

This program uses the name of the test case as the annotation name, and "start" or "result" for the detail. The content is the test number for the former, and the pass/fail result for the latter.

Use undoex_annotation_add_raw_data() to add raw binary data to the annotation, or undoex_annotation_add_text() to add a null-terminated string to the annotation. See the undoex/undoex-annotations.h header for full documentation of these functions.

You can find the libundoex library and header files in the undoex subdirectory of the UDB release. The library is available as both a statically linked library (libundoex_pic_<arch>.a) and a dynamically linked library (libundoex_<arch>.so). There are no other dependencies, so the compilation command will be similar to this:

$ gcc -ggdb -Iundoex undoex/examples/annotations.c undoex/libundoex_pic_x64.a -o annotations

When annotations is not being recorded, the annotations functions return -1 and set errno to ENOTSUP. When annotations is recorded, the functions return 0 and annotations are added to the recording, whether using the live-record tool, the LiveRecorder library, or UDB.

Querying annotations

info annotation name [detail]

Show a single annotation with the specified name and detail. Omitting the detail shows a single annotation with the specified name. The content of the annotation is shown in full.

If there are multiple annotations matching the specification, the first one at or after the current time is shown.

info annotation -t|-time time

Show the annotation at time.

info annotations [name [detail]]

Show annotations with the specified name and detail. Omitting the detail shows all annotations with the specified name. Omitting the name show all annotations. The content of the annotation is truncated if it is too long to display in table form.

When debugging the example program shown above, the output looks like this:

$ udb ./annotations
Reading symbols from ./annotations...
not running> handle SIGABRT noprint nostop
Signal        Stop      Print   Pass to program Description
SIGABRT       No        No      Yes             Aborted
not running> run
Starting program: annotations
passing: pass
failing: fail
annotations: undoex/examples/annotations.c:84: test_flaky: Assertion `rand() % 3' failed.
flaky: fail

Program received signal SIGSTOP, Stopped (signal).

The program has exited, but is still being debugged.
You can use UDB reverse commands to go backwards; see "help udb" for details.

__GI__exit (status=status@entry=1) at ../sysdeps/unix/sysv/linux/_exit.c:30
30            INLINE_SYSCALL (exit_group, 1, status);
end 16,007> info annotations
  Time  Name     Detail    Type      Content
------  -------  --------  ------  ---------
12,776  passing  start     int             0
12,796  passing  result    int             1
12,968  failing  start     int             1
13,001  failing  result    int             0
13,111  flaky    start     int             2
15,809  flaky    result    int             0

Jumping to annotations

ugo annotation [-e|-exact] name [detail]

Jump to the annotation with the specified name and detail. Omitting the detail jumps to the annotation with the specified name.

If there are multiple annotations matching the specification, the first one at or after the current time is used.

-e|-exact

Stop at the time when the annotation was added to the event log. Otherwise, stop at the time when the undoex_annotation_add() function was called.

We can use this to jump to the start of the flaky test:

end 16,007> ugo annotation flaky start

Moving out from libundoex into user code.
You can disable this behavior by using the "-exact" option.

0x0000555555567803 in main () at undoex/examples/annotations.c:59
59                  undoex_annotation_add_int(t->name, "start", i);
81% 13,103> next
60                  t->function();
81% 13,121> step
test_flaky () at undoex/examples/annotations.c:84
84          assert(rand() % 3);