Benchmarking seL4

Benchmarking project

There is a set of microbenchmarks for seL4 available, see the sel4bench-manifest.

Benchmarking Tools

We have developed a set of tools which can be used to analyse kernel and workload performance.

CPU Utilisation

Threads (including the idle thread) and the overall system time (in cycles) can be tracked by enabling the “track CPU utilisation feature”. This feature can be enabled by setting the CMake configuration option KernelConfiguration to track_utilisation.

By enabling CPU utilisation tracking, the kernel is instrumented with some variables and functions to log the utilisation time for each thread (thus TCBs have additional variables for this purpose); the in-kernel buffer is not used. During each context switch, the kernel adds how long this heir thread has run before being switched, and resets the start time of the next thread.

How to use

After enabling this feature, some few system calls can be used to start, stop, and retrieve data.

seL4_BenchmarkResetLog() This system call resets global counters (since the previous call to the same function) and idleThread counters that hold utilisation values, and starts CPU utilisation tracking.

seL4_BenchmarkResetThreadUtilisation(seL4_CPtr thread_cptr) resets the utilisation counters for the requested thread. It’s the resposibility of the user to reset the thread’s counters using this system call before calling seL4_BenchmarkResetLog(), especially if seL4_BenchmarkResetLog() is called multiple times to track the same thread(s).

seL4_BenchmarkFinalizeLog() Stops the CPU tracking feature but doesn’t reset the counters. Calling this system call without a previous seL4_BenchmarkResetLog() call has no effect.

seL4_BenchmarkGetThreadUtilisation(seL4_CPtr thread_cptr) Gets the utilisation time of the thread that thread_cptr capability points to between calls to seL4_BenchmarkResetLog() and seL4_BenchmarkFinalizeLog(). The utilisation time is dumped to the IPCBuffer (first 64-bit word) of the calling thread into a fixed position. Additionally idle thread and overall CPU utilisation times are dumped to subsequent 64-bit words in the IPCBuffer.

Example code of using this feature:

#include <sel4/benchmark_utilisation_types.h>

uint64_t *ipcbuffer = (uint64_t*) &(seL4_GetIPCBuffer()->msg[0]);

seL4_BenchmarkResetThreadUtilisation(seL4_CapInitThreadTCB);

seL4_BenchmarkResetLog();
...
seL4_BenchmarkFinalizeLog();

seL4_BenchmarkGetThreadUtilisation(seL4_CapInitThreadTCB);
printf("Init thread utilisation = %llun", ipcbuffer[BENCHMARK_TCB_UTILISATION]);
printf("Idle thread utilisation = %llun", ipcbuffer[BENCHMARK_IDLE_UTILISATION]);
printf("Overall utilisation = %llun", ipcbuffer[BENCHMARK_TOTAL_UTILISATION]);

In kernel log-buffer

An in-kernel log buffer can be provided by the user when the KernelBenchmarks CMake config option is set to track_kernel_entries or tracepoints with the system call seL4_BenchmarkSetLogBuffer. Users must provide a large frame capability for the kernel to use as a log buffer. This is mapped write-through to avoid impacting the caches, assuming that the kernel only writes to the log buffer and doesn’t read to it during benchmarking. Once a benchmark is complete, data can be read out at user level.

We provide several benchmarking tools that use the log buffer (trace points and kernel entry tracking).

Tracepoints

We allow the user to specify tracepoints in the kernel to track the time between points.

How to use

Set the KernelBenchmarks CMake config option to tracepoints. Then set the KernelMaxNumTracePoints CMake config option to a non-zero value.

Wrap the regions you wish to time with TRACE_POINT_START(i) and TRACE_POINT_STOP(i) where i is an integer from 0 to 1 less than the value of “Maximum number of tracepoints”.

The number of cycles consumed between a TRACE_POINT_START and TRACE_POINT_STOP will be stored in an in-kernel log. Entries of this log consist of a key (the argument to TRACE_POINT_START and TRACE_POINT_STOP) and a value (the number of cycles counted between TRACE_POINT_START an TRACE_POINT_STOP).

Functionality for extracting and processing entries from this buffer is provided in libsel4bench (https://github.com/seL4/libsel4bench).

An example of this feature in use can be found in the irq path benchmark in the sel4bench app(https://github.com/seL4/libsel4bench).

Tracepoint Overhead

Measuring Overhead

Using tracepoints adds a small amount of overhead to the kernel. To measure this overhead, use a pair of nested tracepoints:

TRACE_POINT_START(0);
TRACE_POINT_START(1);
TRACE_POINT_STOP(1);
TRACE_POINT_STOP(0);

The outer tracepoints will measure the time taken to start and stop the inner tracepoints. The cycle count recorded by the outer tracepoints will be slightly skewed, as starting and stopping itself (the outer tracepoints) takes some number of cycles. To determine how many, we look to the inner tracepoints. Since this pair is empty, it will record the number of cycles added to a result by the tracepoint instrumentation. Thus, to compute the total overhead of starting and stopping a tracepoint, subtract the values measured by the inner tracepoints from those measured by the outer tracepoints.

Results

All results are in cycles. Results were obtained using the method described above. The total overhead is the number of cycles added to execution per tracepoint start/stop pair (inner pair result subtracted from outer pair result). The effective overhead is the number of cycles added to a measurement by the tracepoint instrumentation (inner pair result).

  • Total Overhead
Machine # Samples Min Max Mean Variance Std Dev Std Dev %
Sabre 740 18 18 18 0 0 0%
Haswell2 740 532 852 550.33 295.16 17.19 3%
  • Effective overhead
Machine # Samples Min Max Mean Variance Std Dev Std Dev %
Sabre 740 4 4 4 0 0 0
Haswell2 740 208 212 208.69 2.75 1.66 1%

Advanced Use

Conditional Logging

A log is stored when TRACE_POINT_STOP(i) is called, only if a corresponding TRACE_POINT_START(i) was called since the last call to TRACE_POINT_STOP(i) or system boot. This allows for counting cycles of a particular path through some region of code. Here are some examples:

The cycles consumed by functions f and g is logged with the key 0, only when the condition c is true:

TRACE_POINT_START(0);
f();
if (c) {
    g();
    TRACE_POINT_STOP(0);
}

The cycles consumed by functions f and g is logged with the key 1, only when the condition c is true:

if (c) {
    f();
    TRACE_POINT_START(1);
}
g();
TRACE_POINT_STOP(1);

These two techniques can be combined to record cycle counts only when a particular path between 2 points is followed. In the following example, cycles consumed by functions f, g and h is logged, only when the condition c is true. Cycle counts are stored with 2 keys (2 and 3) which can be combined after extracting the data to user level.

TRACE_POINT_START(2);
f();
if (c) {
    h();
    TRACE_POINT_STOP(2);
    TRACE_POINT_START(3);
}
g();
TRACE_POINT_STOP(3);

Interleaving/Nesting

It’s possible to interleave tracepoints:

TRACE_POINT_START(0);
...
TRACE_POINT_START(1);
...
TRACE_POINT_STOP(0);
...
TRACE_POINT_STOP(1);

and to nest tracepoints:

TRACE_POINT_START(0);
...
TRACE_POINT_START(1);
...
TRACE_POINT_STOP(1);
...
TRACE_POINT_STOP(0);

When interleaving or nesting tracepoints, be sure to account for the overhead that will be introduced.

Kernel entry tracking

Kernel entries can be tracked, registering info about interrupts, syscall, timestamp, invocations and capability types. The number of kernel entries is restricted by the log buffer size. The kernel provides a reserved area within its address space to map the log buffer. It’s the responsibility of the user to allocate a user-level log buffer (currently can be only of seL4_LargePageBits size) and pass it to the kernel to use before doing any operations that involve the log buffer, otherwise an error will be triggered having incorrect user-level log buffer. To enable this feature, set the KernelBenchmarks CMake config option to track_kernel_entries.

An example how to create a user-level log buffer (using sel4 libraries) and tell the kernel about it is as follows:

#ifdef CONFIG_BENCHMARK_TRACK_KERNEL_ENTRIES

#include <sel4/benchmark_track_types.h>

  /* Create large page to use for benchmarking and give to kernel */
  void*log_buffer = vspace_new_pages(&env.vspace, seL4_AllRights, 1, seL4_LargePageBits);
  if (log_buffer == NULL) {
    ZF_LOGF("Could not map log_buffer page");
  }
  seL4_CPtr buffer_cap = vspace_get_cap(&env.vspace, log_buffer);
  if (buffer_cap == NULL) {
    ZF_LOGF("Could not get cap for log buffer");
  }
  int res_buf = seL4_BenchmarkSetLogBuffer(buffer_cap);
  if (res_buf) {
    ZF_LOGF("Could not set log buffer");
  }

#endif CONFIG_BENCHMARK_TRACK_KERNEL_ENTRIES

seL4_BenchmarkResetLog() can be used then to reset the log buffer index and start tracking. To stop tracking, call seL4_BenchmarkFinalizeLog() which returns the log buffer index. Note, if the buffer is full/saturated, it will return the last entry index of the log buffer. Finally, the log buffer can be analyzed to extract desired info. For reference, there are utility functions to extract such information in sel4utils/benchmark_track.h.

Hints

If you want only entry or exit times instead of function call durations, modify line 56 of kernel/include/benchmark.h. This might be useful if you wish to time hardware events. For example, should you wish to time how long it takes for hardware to generate a fault to the kernel, perhaps record the cycle counter before causing the fault in userspace, then store the ksEntry as soon as you enter somewhere relevant in the kernel, and then compare the difference of these two once you return to userspace, by reading out the value and taking the difference.