Benchmarking seccheck
Macro-benchmarks measure the impact of seccheck on end-to-end container
workloads (like compiling absl or serving HTTP traffic). They run via the
global make run-benchmark wrapper.
Pre-configured JSON configurations are checked into this directory.
Install the Instrumented Runtime
Install the customized runsc-seccheck docker alias by utilizing the pre-built
configuration target. This links Docker natively against
null_bench_config.json by default:
$ make setup-seccheck
If you want to benchmark against the remote sink or your own custom config,
you can hot-swap it immediately inline without modifying the Makefile:
$ make setup-seccheck SECCHECK_BENCH_CONFIG=$(pwd)/test/benchmarks/seccheck/remote_bench_config.json
Run the Benchmark
Execute your chosen workload against the new instrumented runtime constraint.
The ABSL build benchmark (BenchmarkBuildABSL in
//test/benchmarks/fs:bazel_test) is the standard workload favored for testing
execve and general syscall overhead, as Bazel orchestrates thousands of
short-lived compiler processes.
Tracepoint Selection Note: The default null_bench_config.json and
remote_bench_config.json files are tuned specifically for this
BenchmarkBuildABSL workload and are hard-coded to trace only
syscall/execve/enter.
$ make run-benchmark-seccheck \
BENCHMARKS_TARGETS="//test/benchmarks/fs:bazel_test" \
BENCHMARKS_FILTER="BenchmarkBuildABSL"
Note: To track allocation hotspots and garbage collection (GC) churn triggered by trace point structures, you can run the benchmark with profiling enabled:
$ make run-benchmark-seccheck ... BENCHMARKS_PROFILE='-pprof-dir=/tmp/profile -pprof-heap -pprof-cpu'and follow up withgo tool pprofon the resulting dumps.
Example: Profiling the Execve Hash Cache
When seccheck is configured to collect the expensive binary_sha256 payload
on execve events, it leverages an internal hash cache to avoid mathematically
rehashing identical binaries. You can isolate exactly how much CPU time this
cache saves by running three explicit benchmark passes:
Pass 0: Baseline
This executes the raw uninstrumented runsc. Sentry engine without any
seccheck interception scaffolding compiled natively into the binary,
representing the pure execution limit.
make dev && \
make run-benchmark \
BENCHMARKS_TARGETS="//test/benchmarks/fs:bazel_test" \
BENCHMARKS_FILTER="BenchmarkBuildABSL" \
BENCHMARKS_OPTIONS="-test.count=5" 2>&1 | tee baseline_benchmark.log
Note: Alternatively, you can use the instrumented runtime with a zero-cost configuration by passing
SECCHECK_BENCH_CONFIG=null_bench_config.jsontomake setup-seccheck.
Pass 1: Cache Enabled
This requests the binary_sha256 payload and allows the Sentry to use its
default cache capacity.
Note: The default size of this internal LRU cache is
512distinct binary paths per sandbox session. You can tune this capacity up or down manually by defining anexecve_hash_cache_capacityproperty within theoptionsblock of your.jsonconfiguration file.
make setup-seccheck SECCHECK_BENCH_CONFIG=$(pwd)/test/benchmarks/seccheck/hash_cached_bench_config.json && \
make run-benchmark-seccheck \
BENCHMARKS_TARGETS="//test/benchmarks/fs:bazel_test" \
BENCHMARKS_FILTER="BenchmarkBuildABSL" \
BENCHMARKS_OPTIONS="-test.count=5" 2>&1 | tee benchmark_result_cache.log
Pass 2: Cache Disabled
This requests the exact same binary_sha256 payload, but explicitly injects an
options block that sets execve_hash_cache_capacity: 0. It forces the Sentry
to perform a raw SHA256 digest on every single execve.
make setup-seccheck SECCHECK_BENCH_CONFIG=$(pwd)/test/benchmarks/seccheck/hash_nocache_bench_config.json && \
make run-benchmark-seccheck \
BENCHMARKS_TARGETS="//test/benchmarks/fs:bazel_test" \
BENCHMARKS_FILTER="BenchmarkBuildABSL" \
BENCHMARKS_OPTIONS="-test.count=5" 2>&1 | tee benchmark_results_nocache.log
What to expect
This example uses the ABSL benchmark. The ABSL build orchestrates thousands of
short-lived compiler processes (spawning the exact same gcc, ld, and bash
binaries repeatedly).
- Baseline: provides IPC and compilation performance inside the generic Sentry sandbox unburdened by syscall interception telemetry.
- Cache enabled: the Sentry only incurs the heavy disk-read and SHA256 calculation mathematically on the very first compilation hit. On subsequent invocations, the Sentry detects the identical ELF binary footprint and serves the hash instantly.
- Cache disabled: the Sentry is forced to repetitively map and recalculate the SHA-256 digest from scratch across all thousands of compiler runs uniformly, dramatically dragging down the Operations/Second throughput.
Benchmark Results Comparison
The following table demonstrates the measured compilation throughput (in
seconds, smaller is better) between invoking raw execve hashing on every
syscall vs leveraging execve_hash_cache_capacity:
| Filesystem | Page Cache | With Cache (s) | Without Cache (s) | Improvement |
|---|---|---|---|---|
bindfs |
clean |
459 | 480 | 4.4% faster |
bindfs |
dirty |
492 | 480 | (Slower) |
tmpfs |
clean |
472 | 487 | 3.1% faster |
tmpfs |
dirty |
481 | 491 | 2.0% faster |
rootfs |
clean |
496 | 490 | (Slower) |
rootfs |
dirty |
481 | 499 | 3.6% faster |
fusefs |
clean |
480 | 496 | 3.2% faster |
fusefs |
dirty |
485 | 490 | 1.0% faster |
Note: You may observe runs where Without Cache execution is faster than
With Cache execution. Because each benchmark iteration executes an 8-minute
macro-compilation, background CPU contention, host OS scheduling noise, or
network variability can inject variance. While repeating loops (-test.count=5)
statistically filters out the largest massive spikes by discarding the extremes,
minor host-level CPU priority jitter can still bleed into the isolated true
medians.
Example: Profiling Seccheck Memory Overheads (Redis)
redis-benchmark can be used to answer concerns about the memory allocation
(allocs/op) and garbage collection (GC) pressure that seccheck may inflict,
Redis is heavily dependent on syscall throughput (specifically thousands of
socket loops per second), making it an ideal candidate to flush out Sentry
memory leaks or GC spikes.
Tracepoint Selection Note: Unlike the ABSL benchmark which forks processes
(execve), the Redis benchmark does endless I/O over the network and executes
almost no execve calls. The redis_*_bench_config.json files are explicitly
overwritten to capture syscall/read/enter and syscall/write/enter to ensure
high-frequency trace generation during this test.
Pass 1: Baseline (Disabled)
This runs the Redis macro-benchmark under a standard runsc sandbox without any
telemetry configurations initialized. We enable native profiling to collect a
baseline heap profile and execution trace:
make setup-seccheck \
SECCHECK_BENCH_CONFIG=$(pwd)/test/benchmarks/seccheck/empty_bench_config.json \
RUNTIME_ARGS="--debug --profile=true --profile-heap=/tmp/redis-baseline-heap.prof --trace=/tmp/redis-baseline-trace.out" && \
make run-benchmark-seccheck \
BENCHMARKS_TARGETS="//test/benchmarks/database:redis_test" \
BENCHMARKS_OPTIONS="-test.benchtime=2s -test.count=6" \
BENCHMARKS_FILTER="BenchmarkRedis" 2>&1 | tee redis_baseline.log
Tip for GC Tracing: To explicitly count Garbage Collection cycles, you can
prefix any of the following make run-benchmark-seccheck commands with
GODEBUG=gctrace=1. This prints raw GC statistics (and Stop-The-World pause
timings) directly to the logs. Note: Doing this consumes extra CPU cycles and
introduces a minor observer effect penalty on max throughput.
GODEBUG=gctrace=1 make run-benchmark-seccheck ...
Pass 2: Base Instrumentation (Null Sink)
This enables seccheck using a null sink configuration (to discard the
serialized events instantly, eliminating remote IPC networking lag). This tests
the absolute memory allocation cost strictly inside the Sentry when it
dynamically intercepts high-frequency network events and generates Protobuf
payload context structures.
make setup-seccheck \
SECCHECK_BENCH_CONFIG=$(pwd)/test/benchmarks/seccheck/redis_null_bench_config.json \
RUNTIME_ARGS="--debug --profile=true --profile-heap=/tmp/redis-null-heap.prof --trace=/tmp/redis-null-trace.out" && \
make run-benchmark-seccheck \
BENCHMARKS_TARGETS="//test/benchmarks/database:redis_test" \
BENCHMARKS_OPTIONS="-test.benchtime=2s -test.count=6" \
BENCHMARKS_FILTER="BenchmarkRedis" 2>&1 | tee redis_null_sink.log
Pass 3: IPC Transfer Overhead (Remote Sink)
This enables seccheck using a remote sink. To isolate the extreme cost of
the Sentry serializing and pushing bytes over the inter-process boundary (IPC),
we launch a dummy listener in the background that instantly accepts and discards
the Unix Domain Socket bytes.
# Start a dummy UDS listener that discards the IPC bytes (SOCK_SEQPACKET type)
python3 $(pwd)/test/benchmarks/seccheck/dummy_uds_server.py &
DUMMY_PID=$!
# Configure and run the benchmark sending to the socket
make setup-seccheck \
SECCHECK_BENCH_CONFIG=$(pwd)/test/benchmarks/seccheck/redis_remote_bench_config.json \
RUNTIME_ARGS="--profile=true --profile-heap=/tmp/redis-remote-heap.prof --trace=/tmp/redis-remote-trace.out" && \
make run-benchmark-seccheck \
BENCHMARKS_TARGETS="//test/benchmarks/database:redis_test" \
BENCHMARKS_OPTIONS="-test.benchtime=2s -test.count=6" \
BENCHMARKS_FILTER="BenchmarkRedis" 2>&1 | tee redis_remote_sink.log
# Kill the dummy listener and clean up the socket lock
kill $DUMMY_PID
rm -f /tmp/seccheck.sock
Analysis and Comparison
Comparing Application Throughput (QPS)
To compare the performance logs and see the exact QPS drop between the baseline
and the instrumentation, use the official Go benchstat tool:
# Install benchstat if you don't already have it
go install golang.org/x/perf/cmd/benchstat@latest
~/go/bin/benchstat baseline=redis_baseline.log null=redis_null_sink.log remote=redis_remote_sink.log
Comparing Memory Overhead (Profiles)
Instead of manually analyzing raw text logs, we use Go's built-in tooling to subtract the baseline memory profile from the seccheck profile. This isolates the exact code paths and memory overhead introduced exclusively by the telemetry.
Option 1: Terminal Text Output For a quick look directly in your terminal, output a ranked diff table:
# Compare Data Generation Overhead (Null Sink target)
go tool pprof -top -diff_base=/tmp/redis-baseline-heap.prof /tmp/redis-null-heap.prof
# Compare Total IPC Overhead (Remote Sink target)
go tool pprof -top -diff_base=/tmp/redis-baseline-heap.prof /tmp/redis-remote-heap.prof
Option 2: Interactive Web UI To view a visual flamegraph or the timeline trace, start a local web server:
# View the heap flamegraph for Null Sink (Internal CPU Cost)
go tool pprof -no_browser -http=0.0.0.0:8080 -diff_base=/tmp/redis-baseline-heap.prof /tmp/redis-null-heap.prof
# View the heap flamegraph for Remote Sink (Cross-Boundary IPC Cost)
go tool pprof -no_browser -http=0.0.0.0:8080 -diff_base=/tmp/redis-baseline-heap.prof /tmp/redis-remote-heap.prof
# View the execution timeline trace for Null Sink
# (safely ignore the X11 error if it appears, trace doesn't support -no_browser)
go tool trace -http=0.0.0.0:8081 /tmp/redis-null-trace.out
(Navigate to http://<your-machine-ip-or-hostname>:8080 in your browser).
What to expect
- What it measures: The
redis-benchmarkisolates operations throughput (in Queries Per Second) across workloads (e.g.,SET,LPUSH) whilst simultaneously recording all memory allocations (allocs/op) and Garbage Collection sweep pauses natively into the.profand.outfiles. - What to look for: You should compare the memory profiles (
pprof) to see if the sheer act of generating theseccheckcontext objects is creating a high volume of garbage. Then, compare the execution traces (trace) to see if frequent GC "Stop The World" (STW) pauses are actively starving the Redis socket loops of CPU time.
Interpreting the Benchmark Results
By comparing the three generated logs (Baseline vs. Null Sink vs. Remote Sink), we can decompose the performance cost of Seccheck tracing into two distinct architectural phases: Data Generation and Data Export.
Here is an example real-world result from redis_test.go tracing constant
read and write activity:
│ baseline │ null │ remote │
│ SET.rps │ SET.rps vs base │ SET.rps vs base │
Redis/operation.SET-96 39.22k ± 3% 37.62k ± 3% -4.08% (p=0.015 n=6) 28.16k ± 0% -28.20% (p=0.002 n=6)
Data Generation Overhead (Baseline vs. Null Sink)
The Null Sink measures strictly the CPU and Memory cost of intercepting the syscalls, gathering context data, and formatting the Protobuf trace objects in-memory (before instantly discarding them).
Result: The overhead of structuring traces inside the Sentry drops operations throughput by ~4%. Future optimizations should aim to bring this down to <1%.
Data Export Overhead (Baseline vs. Remote Sink)
The Remote Sink measures the total end-to-end burden of tracing, which heavily includes writing the serialized Protobufs over a Unix Domain Socket (UDS) and crossing the Inter-Process Communication boundary to an external daemon.
Result: Serializing and moving the data out of the socket drops operations throughput by a massive ~28%.