Skip to content

Commit 49702b9

Browse files
xinzhonggvisor-bot
authored andcommitted
seccheck: Add redis benchmarking docs and configs
PiperOrigin-RevId: 970623601
1 parent ba69c7d commit 49702b9

5 files changed

Lines changed: 279 additions & 1 deletion

File tree

test/benchmarks/seccheck/README.md

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ The ABSL build benchmark (`BenchmarkBuildABSL` in
3131
`execve` and general syscall overhead, as Bazel orchestrates thousands of
3232
short-lived compiler processes.
3333

34+
**Tracepoint Selection Note**: The default `null_bench_config.json` and
35+
`remote_bench_config.json` files are tuned specifically for this
36+
`BenchmarkBuildABSL` workload and are hard-coded to trace only
37+
`syscall/execve/enter`.
38+
3439
```shell
3540
$ make run-benchmark-seccheck \
3641
BENCHMARKS_TARGETS="//test/benchmarks/fs:bazel_test" \
@@ -108,7 +113,7 @@ binaries repeatedly).
108113

109114
- **Baseline:** provides IPC and compilation performance inside the generic
110115
Sentry sandbox unburdened by syscall interception telemetry.
111-
- **Cache enabled:** the Sentry only incurs the heavy disk-read and SHA-256
116+
- **Cache enabled:** the Sentry only incurs the heavy disk-read and SHA256
112117
calculation mathematically on the very first compilation hit. On subsequent
113118
invocations, the Sentry detects the identical ELF binary footprint and
114119
serves the hash instantly.
@@ -140,3 +145,182 @@ network variability can inject variance. While repeating loops (`-test.count=5`)
140145
statistically filters out the largest massive spikes by discarding the extremes,
141146
minor host-level CPU priority jitter can still bleed into the isolated true
142147
medians.
148+
149+
## Example: Profiling Seccheck Memory Overheads (Redis)
150+
151+
`redis-benchmark` can be used to answer concerns about the memory allocation
152+
(`allocs/op`) and garbage collection (GC) pressure that `seccheck` may inflict,
153+
Redis is heavily dependent on `syscall` throughput (specifically thousands of
154+
socket loops per second), making it an ideal candidate to flush out Sentry
155+
memory leaks or GC spikes.
156+
157+
**Tracepoint Selection Note**: Unlike the ABSL benchmark which forks processes
158+
(`execve`), the Redis benchmark does endless I/O over the network and executes
159+
almost no `execve` calls. The `redis_*_bench_config.json` files are explicitly
160+
overwritten to capture `syscall/read/enter` and `syscall/write/enter` to ensure
161+
high-frequency trace generation during this test.
162+
163+
### Pass 1: Baseline (Disabled)
164+
165+
This runs the Redis macro-benchmark under a standard `runsc` sandbox without any
166+
telemetry configurations initialized. We enable native profiling to collect a
167+
baseline heap profile and execution trace:
168+
169+
```shell
170+
make setup-seccheck \
171+
SECCHECK_BENCH_CONFIG=$(pwd)/test/benchmarks/seccheck/empty_bench_config.json \
172+
RUNTIME_ARGS="--debug --profile=true --profile-heap=/tmp/redis-baseline-heap.prof --trace=/tmp/redis-baseline-trace.out" && \
173+
make run-benchmark-seccheck \
174+
BENCHMARKS_TARGETS="//test/benchmarks/database:redis_test" \
175+
BENCHMARKS_OPTIONS="-test.benchtime=2s -test.count=6" \
176+
BENCHMARKS_FILTER="BenchmarkRedis" 2>&1 | tee redis_baseline.log
177+
```
178+
179+
**Tip for GC Tracing:** To explicitly count Garbage Collection cycles, you can
180+
prefix any of the following `make run-benchmark-seccheck` commands with
181+
`GODEBUG=gctrace=1`. This prints raw GC statistics (and Stop-The-World pause
182+
timings) directly to the logs. *Note: Doing this consumes extra CPU cycles and
183+
introduces a minor observer effect penalty on max throughput.*
184+
185+
```
186+
GODEBUG=gctrace=1 make run-benchmark-seccheck ...
187+
```
188+
189+
### Pass 2: Base Instrumentation (Null Sink)
190+
191+
This enables `seccheck` using a `null` sink configuration (to discard the
192+
serialized events instantly, eliminating remote IPC networking lag). This tests
193+
the absolute memory allocation cost strictly inside the Sentry when it
194+
dynamically intercepts high-frequency network events and generates Protobuf
195+
payload context structures.
196+
197+
```shell
198+
make setup-seccheck \
199+
SECCHECK_BENCH_CONFIG=$(pwd)/test/benchmarks/seccheck/redis_null_bench_config.json \
200+
RUNTIME_ARGS="--debug --profile=true --profile-heap=/tmp/redis-null-heap.prof --trace=/tmp/redis-null-trace.out" && \
201+
make run-benchmark-seccheck \
202+
BENCHMARKS_TARGETS="//test/benchmarks/database:redis_test" \
203+
BENCHMARKS_OPTIONS="-test.benchtime=2s -test.count=6" \
204+
BENCHMARKS_FILTER="BenchmarkRedis" 2>&1 | tee redis_null_sink.log
205+
```
206+
207+
### Pass 3: IPC Transfer Overhead (Remote Sink)
208+
209+
This enables `seccheck` using a `remote` sink. To isolate the extreme cost of
210+
the Sentry serializing and pushing bytes over the inter-process boundary (IPC),
211+
we launch a dummy listener in the background that instantly accepts and discards
212+
the Unix Domain Socket bytes.
213+
214+
```shell
215+
# Start a dummy UDS listener that discards the IPC bytes (SOCK_SEQPACKET type)
216+
python3 $(pwd)/test/benchmarks/seccheck/dummy_uds_server.py &
217+
DUMMY_PID=$!
218+
219+
# Configure and run the benchmark sending to the socket
220+
make setup-seccheck \
221+
SECCHECK_BENCH_CONFIG=$(pwd)/test/benchmarks/seccheck/redis_remote_bench_config.json \
222+
RUNTIME_ARGS="--profile=true --profile-heap=/tmp/redis-remote-heap.prof --trace=/tmp/redis-remote-trace.out" && \
223+
make run-benchmark-seccheck \
224+
BENCHMARKS_TARGETS="//test/benchmarks/database:redis_test" \
225+
BENCHMARKS_OPTIONS="-test.benchtime=2s -test.count=6" \
226+
BENCHMARKS_FILTER="BenchmarkRedis" 2>&1 | tee redis_remote_sink.log
227+
228+
# Kill the dummy listener and clean up the socket lock
229+
kill $DUMMY_PID
230+
rm -f /tmp/seccheck.sock
231+
```
232+
233+
### Analysis and Comparison
234+
235+
#### Comparing Application Throughput (QPS)
236+
237+
To compare the performance logs and see the exact QPS drop between the baseline
238+
and the instrumentation, use the official Go `benchstat` tool:
239+
240+
```shell
241+
# Install benchstat if you don't already have it
242+
go install golang.org/x/perf/cmd/benchstat@latest
243+
244+
~/go/bin/benchstat baseline=redis_baseline.log null=redis_null_sink.log remote=redis_remote_sink.log
245+
```
246+
247+
#### Comparing Memory Overhead (Profiles)
248+
249+
Instead of manually analyzing raw text logs, we use Go's built-in tooling to
250+
subtract the baseline memory profile from the seccheck profile. This isolates
251+
the exact code paths and memory overhead introduced exclusively by the
252+
telemetry.
253+
254+
**Option 1: Terminal Text Output** For a quick look directly in your terminal,
255+
output a ranked diff table:
256+
257+
```shell
258+
# Compare Data Generation Overhead (Null Sink target)
259+
go tool pprof -top -diff_base=/tmp/redis-baseline-heap.prof /tmp/redis-null-heap.prof
260+
261+
# Compare Total IPC Overhead (Remote Sink target)
262+
go tool pprof -top -diff_base=/tmp/redis-baseline-heap.prof /tmp/redis-remote-heap.prof
263+
```
264+
265+
**Option 2: Interactive Web UI** To view a visual flamegraph or the timeline
266+
trace, start a local web server:
267+
268+
```shell
269+
# View the heap flamegraph for Null Sink (Internal CPU Cost)
270+
go tool pprof -no_browser -http=0.0.0.0:8080 -diff_base=/tmp/redis-baseline-heap.prof /tmp/redis-null-heap.prof
271+
272+
# View the heap flamegraph for Remote Sink (Cross-Boundary IPC Cost)
273+
go tool pprof -no_browser -http=0.0.0.0:8080 -diff_base=/tmp/redis-baseline-heap.prof /tmp/redis-remote-heap.prof
274+
275+
# View the execution timeline trace for Null Sink
276+
# (safely ignore the X11 error if it appears, trace doesn't support -no_browser)
277+
go tool trace -http=0.0.0.0:8081 /tmp/redis-null-trace.out
278+
```
279+
280+
*(Navigate to `http://<your-machine-ip-or-hostname>:8080` in your browser).*
281+
282+
### What to expect
283+
284+
- **What it measures:** The `redis-benchmark` isolates operations throughput
285+
(in Queries Per Second) across workloads (e.g., `SET`, `LPUSH`) whilst
286+
simultaneously recording all memory allocations (`allocs/op`) and Garbage
287+
Collection sweep pauses natively into the `.prof` and `.out` files.
288+
- **What to look for:** You should compare the memory profiles (`pprof`) to
289+
see if the sheer act of generating the `seccheck` context objects is
290+
creating a high volume of garbage. Then, compare the execution traces
291+
(`trace`) to see if frequent GC "Stop The World" (STW) pauses are actively
292+
starving the Redis socket loops of CPU time.
293+
294+
### Interpreting the Benchmark Results
295+
296+
By comparing the three generated logs (Baseline vs. Null Sink vs. Remote Sink),
297+
we can decompose the performance cost of Seccheck tracing into two distinct
298+
architectural phases: **Data Generation** and **Data Export**.
299+
300+
Here is an example real-world result from `redis_test.go` tracing constant
301+
`read` and `write` activity:
302+
303+
```text
304+
│ baseline │ null │ remote │
305+
│ SET.rps │ SET.rps vs base │ SET.rps vs base │
306+
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)
307+
```
308+
309+
#### Data Generation Overhead (Baseline vs. Null Sink)
310+
311+
The Null Sink measures strictly the CPU and Memory cost of intercepting the
312+
syscalls, gathering context data, and formatting the Protobuf trace objects
313+
in-memory (before instantly discarding them).
314+
315+
**Result:** The overhead of structuring traces inside the Sentry drops
316+
operations throughput by **~4%**. Future optimizations should aim to bring this
317+
down to **<1%**.
318+
319+
#### Data Export Overhead (Baseline vs. Remote Sink)
320+
321+
The Remote Sink measures the total end-to-end burden of tracing, which heavily
322+
includes writing the serialized Protobufs over a Unix Domain Socket (UDS) and
323+
crossing the Inter-Process Communication boundary to an external daemon.
324+
325+
**Result:** Serializing and moving the data out of the socket drops operations
326+
throughput by a massive **~28%**.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2026 The gVisor Authors.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Dummy Unix Domain Socket server for benchmarking gVisor seccheck IPC overhead.
16+
17+
This script provides a SOCK_SEQPACKET UDS listener that performs the required
18+
gVisor seccheck handshake and then immediately discards all incoming bytes.
19+
It is an intentionally dumb endpoint designed exclusively to isolate
20+
cross-boundary IPC transfer latency from agent-side parsing latency.
21+
"""
22+
23+
import os
24+
import socket
25+
26+
SOCK_PATH = "/tmp/seccheck.sock"
27+
# Max bytes to pull per socket read. 65536 == 64 KB.
28+
# A large chunk size minimizes Python loop/syscall overhead over UDS.
29+
CHUNK_SIZE = 65536
30+
if os.path.exists(SOCK_PATH):
31+
os.remove(SOCK_PATH)
32+
33+
s = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
34+
s.bind(SOCK_PATH)
35+
s.listen(1)
36+
37+
print(f"Dummy UDS listener running on {SOCK_PATH} - ready for handshake...")
38+
39+
while True:
40+
try:
41+
conn, _ = s.accept()
42+
# gVisor sends a Handshake protobuf first. Read it.
43+
conn.recv(CHUNK_SIZE)
44+
# Reply with Handshake{Version: 1} so Sentry can boot
45+
# 0x08 is varint field 1 (version), 0x01 is value 1.
46+
conn.send(b"\x08\x01")
47+
print("Handshake acknowledged. Emptying socket...")
48+
while True:
49+
if not conn.recv(CHUNK_SIZE):
50+
break
51+
except KeyboardInterrupt:
52+
break
53+
except OSError:
54+
pass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"trace_session": {
3+
"name": "Default",
4+
"points": [
5+
{
6+
"name": "syscall/read/enter"
7+
},
8+
{
9+
"name": "syscall/write/enter"
10+
}
11+
],
12+
"sinks": [
13+
{
14+
"name": "null"
15+
}
16+
]
17+
}
18+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"trace_session": {
3+
"name": "Default",
4+
"points": [
5+
{
6+
"name": "syscall/read/enter"
7+
},
8+
{
9+
"name": "syscall/write/enter"
10+
}
11+
],
12+
"sinks": [
13+
{
14+
"name": "remote",
15+
"config": {
16+
"endpoint": "/tmp/seccheck.sock"
17+
}
18+
}
19+
]
20+
}
21+
}

0 commit comments

Comments
 (0)