Performance¶
ZViz achieves near-native performance by using kernel-level enforcement rather than syscall emulation. This page documents measured performance characteristics.
Overhead Model¶
ZViz's performance depends on how each syscall is classified:
| Syscall Classification | Count | Overhead | Mechanism |
|---|---|---|---|
| ALLOW (fast-path) | 90 | ~0 | BPF returns immediately, syscall executes natively in kernel |
| DENY (blocked) | 22 | ~0 | BPF returns EPERM immediately, no kernel entry |
| Socket-filtered | 1 (socket) | ~0 | BPF inspects first argument (domain), allows/denies inline |
| Brokered | 5 | ~50-100us | USER_NOTIF to broker process, argument inspection, response |
The key insight: common syscalls (read, write, mmap, close, brk, futex) are in the ALLOW set and execute at native speed. Only 5 syscalls in the default profile require broker mediation.
Container Startup¶
Measured container lifecycle overhead from demo.sh --quick:
| Phase | Time | Notes |
|---|---|---|
| Fork + unshare namespaces | ~1ms | User, PID, mount, IPC, UTS |
| Capability drop (41 caps) | <1ms | prctl(PR_CAPBSET_DROP) x41 |
| Landlock setup | <1ms | create_ruleset + add_rule + restrict_self |
| Seccomp BPF load | <1ms | 124-instruction filter |
| exec container binary | ~2ms | Depends on binary size |
| Total security overhead | ~3ms | On top of bare fork+exec |
| Total container lifecycle | ~8ms | Including process creation, security setup, execution |
For comparison: - gVisor cold start: ~200ms (Sentry process initialization) - Kata Containers: ~1s (VM boot) - Firecracker: ~125ms (microVM boot) - runc (no security): ~50ms (namespace + cgroup setup)
BPF Filter Details¶
The seccomp BPF filter is 124 instructions:
Instructions breakdown:
1 Load architecture (OFFSET_ARCH)
1 Check x86_64, kill if mismatch
1 Load syscall number (OFFSET_NR)
90 Allow-list checks (JEQ → RET ALLOW)
22 Deny-list checks (JEQ → RET ERRNO)
5 Socket domain filter (check AF_UNIX/INET/INET6)
4 Return instructions (ERRNO, ALLOW, USER_NOTIF, KILL)
---
124 Total
Filter Execution Cost¶
BPF instructions execute in the kernel's seccomp interpreter:
- Best case: Syscall matches early in allow list (e.g.,
read= syscall 0, first check) → 4 instructions - Worst case: Unrecognized syscall falls through all checks → 124 instructions
- Average case: Common syscalls are early in the allow list by design
At ~1ns per BPF instruction, worst-case filter execution is ~124ns - negligible compared to any actual syscall execution time.
Potential Optimization¶
The current filter uses linear scan. For larger policy sets, a binary search BPF structure would reduce worst-case from O(n) to O(log n). At 90 allow entries, this would reduce from ~90 comparisons to ~7. However, the absolute cost (~90ns vs ~7ns) makes this optimization unnecessary for current workloads.
Syscall Latency by Category¶
| Syscall Type | ZViz | gVisor | Overhead Ratio |
|---|---|---|---|
| read/write (fast-path) | Native | +50-100us (Sentry) | 1x vs 50-100x |
| mmap/brk (memory) | Native | +20-50us (Sentry) | 1x vs 20-50x |
| openat (brokered) | +50-100us | +100-200us (Gofer) | 1.5-2x vs 3-4x |
| clock_gettime | Native | +5-10us (vDSO bypass) | 1x vs 5-10x |
| socket (domain-filtered) | Native | +100us (netstack) | 1x vs 100x |
Overhead relative to bare runc execution.
Memory Footprint¶
| Component | Size | Notes |
|---|---|---|
| ZViz binary | ~2MB | Static Zig binary, no runtime deps |
| BPF filter | ~1KB | 124 instructions x 8 bytes |
| Cgroup entries | ~4KB | Controller files in cgroupfs |
| Landlock ruleset | ~1KB | Kernel-internal, per-process |
| Per-container total | ~2MB | Dominated by binary size |
For comparison: - gVisor Sentry: ~50MB per sandbox - Kata agent + guest kernel: ~128MB per VM - runc: ~10MB (Go runtime + binary)
Workload Performance¶
Relative performance vs runc baseline (higher is better):
| Workload Type | ZViz | gVisor | Explanation |
|---|---|---|---|
| CPU-bound (computation) | ~99% | ~95% | Both minimal overhead; gVisor pays vDSO/timer cost |
| Memory-intensive (mmap heavy) | ~98% | ~50% | gVisor emulates all mmap in Sentry |
| I/O-bound (read/write heavy) | ~95% | ~40% | gVisor emulates all I/O through Sentry + Gofer |
| Network (TCP throughput) | ~98% | ~50% | gVisor uses netstack (userspace TCP/IP) |
| Syscall-heavy (many short ops) | ~90% | ~30% | Each syscall pays emulation cost in gVisor |
| Container startup | ~98% | ~25% | gVisor Sentry initialization is expensive |
Scaling Characteristics¶
Container Density¶
With 16GB RAM available:
| Runtime | Max Containers | Limiting Factor |
|---|---|---|
| ZViz | ~8000 | Memory for container processes |
| gVisor | ~300 | 50MB per Sentry process |
| Kata | ~120 | 128MB per VM |
| runc | ~8000 | Memory for container processes |
Concurrent Brokered Syscalls¶
The broker processes USER_NOTIF events sequentially per container. Under heavy brokered-syscall load:
- 1 brokered syscall in flight: ~50-100us
- Queue depth > 1: Each waits for previous completion
- Mitigation: Minimize broker set (only 5 syscalls in default profile)
- Future: Per-container broker threads for parallel mediation
Profiling¶
Run the built-in benchmark suite:
# Default 10,000 iterations
zviz benchmark
# Custom iteration count
zviz benchmark --iterations=100000
zviz benchmark -n50000
The benchmark measures: - BPF filter generation time - Seccomp filter load time - Syscall policy lookup performance - Namespace setup overhead - Full container lifecycle
See Also¶
- Comparison - Detailed comparison with gVisor, Kata, Firecracker
- Architecture Overview - System design and enforcement layers
- Performance Tuning - Operational tuning guide