Current version: 0.4.1
LateFrame is a Linux UDP traffic generator for experiments where packet inter-arrival time matters.
It was developed as the traffic-generation and replay framework used in the evaluation of LatencyScope. If you use LateFrame in academic work, please cite:
@misc{maghsoudnia2026latencyscopesystemlevelmathematicalframework,
title={LatencyScope: A System-Level Mathematical Framework for 5G RAN Latency},
author={Arman Maghsoudnia and Aoyu Gong and Raphael Cannatà and Dan Mihai Dumitriu and Haitham Hassanieh},
year={2026},
eprint={2511.21277},
archivePrefix={arXiv},
primaryClass={cs.NI},
url={https://arxiv.org/abs/2511.21277},
}The motivation is simple: ping and fping are useful network tools, but they are not precise packet schedulers. If you need to generate traffic at a target interval with low jitter, they are the wrong baseline.
LateFrame uses absolute deadline scheduling on CLOCK_MONOTONIC, CPU pinning, and a minimal send loop to keep pacing tight. By default it uses timerfd, and it can also use clock_nanosleep() with an optional busy-spin window.
LateFrame supports two main use cases:
- synthetic traffic generation with controlled inter-arrival timing
- replay of previously captured packet traces from PCAP files through UDP encapsulation
Synthetic generation is useful when you want a clean constant, Poisson, or Gaussian process.
PCAP replay is useful when you need to reproduce a captured trace while preserving the original packet timing. LateFrame retains as much as possible from the end of each captured packet inside a UDP payload, so the replayed packet matches the captured packet length whenever possible after accounting for the replay encapsulation overhead. This matters in replay scenarios where sending packets with the same size and timing is more important than reconstructing the exact original packet contents.
This is especially useful for experiments and research where accurate workload replay in terms of packet size and timing matters more than generating identical packets.
If the original packet is smaller than the required replay encapsulation overhead, the replayed packet cannot match the original packet size and will be larger. In this case, the user is warned.
The current comparison uses a 100 ms target interval and 1000 transmitted packets. The figure below includes:
- the combined comparison across the three LateFrame runs plus
pingandfping - one standalone plot for each sender
- trimmed zoomed plots for
pingandfpingwith the top and bottom 1% removed
LateFrame timerfd: count=999, mean=100.000013938 ms, std=0.032544443 ms
LateFrame spin 50us: count=999, mean=100.000007255 ms, std=0.038343503 ms
LateFrame spin 100us: count=999, mean=99.999996038 ms, std=0.046501921 ms
ping: count=999, mean=104.012259731 ms, std=0.517701949 ms
fping: count=999, mean=100.000159280 ms, std=0.247248914 ms
Two things stand out in the captures:
- all three LateFrame runs stay centered on the
100 mstarget pingmisses the target mean by about4 mspingandfpingboth show substantially wider spread than any of the LateFrame runs
Dependencies on Debian or Ubuntu:
sudo apt install build-essential make libpcap-dev tsharkBuild:
git clone https://github.com/arman-maghsoudnia/LateFrame.git
cd LateFrame
makeThe binary is produced at package/usr/bin/lateframe.
Install:
sudo make installOr:
make install PREFIX=/opt/lateframelateframe [options]Main options:
-n,--num-packets: number of packets to send-i,--interface: source interface-d,--destination: destination IPv4 address-p,--port: destination UDP port-t,--distribution:constant,poisson,gaussian, orpcap-a,--param: interval in ms, lambda in packets/s, or Gaussian mean in ms-S,--sigma: Gaussian sigma in ms-s,--size: payload size in bytes-f,--pcap-file: PCAP file for replay mode--wait-mode:timerfdornanosleepfor the packet pacing wait primitive--spin-us: busy-spin window in microseconds fornanosleepwait mode--no-cpu-pin: Disable CPU pinning (default: enabled)-l,--log: log sends to stdout and/tmp/lateframe.log-D,--dscp: Assign same DSCP value to all generated or replayed packets (0 to 63)-c,--capture: capture generated packets to/tmp/lateframe-capture.pcap
Notes:
- Option order does not matter.
- Both
--num-packetsand legacy--num_packetsare accepted. --wait-modedefaults totimerfd.--wait-mode nanosleeprequires--spin-us.- Generated traffic modes require
-n,-s, and-a. - Gaussian mode also requires
-S. - PCAP mode ignores
-n,-s,-a, and-S. - CPU pinning can be disabled with
--no-cpu-pin. The pinned vs. unpinned replay comparison results and reproduction commands are documented incomparison-data/CPU_pinning/README.md.
Examples:
sudo lateframe -n 1000 -i eth0 -d 192.168.1.10 -p 12345 -t constant -a 100 -s 256sudo lateframe -n 1000 -i eth0 -d 192.168.1.10 -p 12345 -t poisson -a 100 -s 256sudo lateframe -n 1000 -i eth0 -d 192.168.1.10 -p 12345 -t gaussian -a 40 -S 2 -s 256sudo lateframe -i eth0 -d 192.168.1.10 -p 12345 -t pcap -f trace.pcapsudo lateframe -i eth0 -d 192.168.1.10 -p 12345 -t pcap -f trace.pcap --wait-mode nanosleep --spin-us 100The PCAPs used for the current result are in comparison-data/generated/. They were captured with the commands below.
Send:
sudo fping -c 1000 -p 100 128.178.122.100Capture on destination:
sudo tshark -i eno1 -f "icmp[0] = 8 and host 128.178.122.100" -w /tmp/fping-test.pcapSend:
sudo ping 128.178.122.100 -i 0.1 -c 1000Capture on destination:
sudo tshark -i eno1 -f "icmp[0] = 8 and host 128.178.122.100" -w /tmp/ping-test.pcapSend:
sudo lateframe -n 1000 -i eno1 -d 128.178.122.100 -p 12345 -t constant -a 100 -s 256 -c --wait-mode timerfdCapture on destination:
sudo tshark -i eno1 -f "udp and host 128.178.122.100 and port 12345" -w /tmp/lateframe-out-timerfd.pcapSend:
sudo lateframe -n 1000 -i eno1 -d 128.178.122.100 -p 12345 -t constant -a 100 -s 256 -c --wait-mode nanosleep --spin-us 50Capture on destination:
sudo tshark -i eno1 -f "udp and host 128.178.122.100 and port 12345" -w /tmp/lateframe-out-spin50.pcapSend:
sudo lateframe -n 1000 -i eno1 -d 128.178.122.100 -p 12345 -t constant -a 100 -s 256 -c --wait-mode nanosleep --spin-us 100Capture on destination:
sudo tshark -i eno1 -f "udp and host 128.178.122.100 and port 12345" -w /tmp/lateframe-out-spin100.pcapVersions used:
ping:ping from iputils 20240117fping:Version 5.1
Host used for the run:
- Architecture:
x86_64 - CPU:
12th Gen Intel(R) Core(TM) i7-1260P - Vendor:
GenuineIntel - Sockets:
1 - Cores per socket:
12 - Threads per core:
2 - Logical CPUs:
16 - CPU max frequency:
4700.0000 MHz - CPU min frequency:
400.0000 MHz - Kernel:
6.8.0-111-lowlatency - CPU governor:
performanceon all cores - L1d cache:
448 KiB (12 instances) - L1i cache:
640 KiB (12 instances) - L2 cache:
9 MiB (6 instances) - L3 cache:
18 MiB (1 instance) - NUMA nodes:
1 - Virtualization:
VT-x
To regenerate the figures from the PCAPs:
python3 scripts/plot_interarrival_density.py \
--series "lateframe-timerfd=LateFrame timerfd=comparison-data/generated/lateframe-out-timerfd.pcap" \
--series "lateframe-spin-50us=LateFrame spin 50us=comparison-data/generated/lateframe-out-spin50.pcap" \
--series "lateframe-spin-100us=LateFrame spin 100us=comparison-data/generated/lateframe-out-spin100.pcap" \
--series "ping=ping=comparison-data/generated/ping-test.pcap" \
--series "fping=fping=comparison-data/generated/fping-test.pcap" \
--zoomed ping \
--zoomed fping \
--output docs/generated/interarrival-density-comparison.png \
--individual-output-dir docs/generated/individual-density-plots \
--zoomed-output-dir docs/generated/zoomed-density-plots \
--unified-output docs/generated/interarrival-density-results-grid.pngThis produces:
docs/generated/interarrival-density-comparison.pngdocs/generated/individual-density-plots/lateframe-timerfd-interarrival-density.pngdocs/generated/individual-density-plots/lateframe-spin-50us-interarrival-density.pngdocs/generated/individual-density-plots/lateframe-spin-100us-interarrival-density.pngdocs/generated/individual-density-plots/ping-interarrival-density.pngdocs/generated/individual-density-plots/fping-interarrival-density.pngdocs/generated/zoomed-density-plots/ping-interarrival-density-trimmed.pngdocs/generated/zoomed-density-plots/fping-interarrival-density-trimmed.pngdocs/generated/interarrival-density-results-grid.png
To regenerate the replay comparison figures from all replay captures in comparison-data/replay/:
python3 scripts/plot_interarrival_diff.py \
--original comparison-data/generated/ping-test.pcap \
--replay "timerfd=timerfd=comparison-data/replay/ping-test-replayed-result-timerfd.pcap" \
--replay "spin50=nanosleep spin 50us=comparison-data/replay/ping-test-replayed-result-spin50.pcap" \
--replay "spin100=nanosleep spin 100us=comparison-data/replay/ping-test-replayed-result-spin100.pcap" \
--output-dir docs/replay \
--output-prefix ping-replayThis produces one heartbeat plot and one density plot per replay PCAP, plus:
docs/replay/ping-replay-interarrival-diff-heartbeat-aggregate.pngdocs/replay/ping-replay-interarrival-diff-density-aggregate.png
PCAP replay mode does not send raw frames. It reads a PCAP, preserves the observed inter-arrival timing, and encapsulates the captured bytes into UDP packets.
For a captured packet of length X, LateFrame preserves as many bytes as possible from the end of that packet and uses them as the UDP payload, so:
- if
Xis at least as large as the replay encapsulation overhead, the replayed packet has lengthX - if
Xis smaller than the replay encapsulation overhead, the replayed packet must be larger than the original
This is not raw frame replay. It is timing-preserving UDP encapsulation of captured packet bytes.
For Ethernet captures, the replay encapsulation overhead is Ethernet + IPv4 + UDP headers. For raw IPv4 captures, it is IPv4 + UDP headers. LateFrame prints a warning when exact size matching is not possible.
Any captured packet bytes can be encapsulated this way. If a packet was truncated in the PCAP, replay uses the captured length, because the missing bytes are not available.
LateFrame supports two pacing backends for replay and generated traffic:
timerfd: the default backend, using absolute deadline scheduling throughtimerfdnanosleep: absoluteclock_nanosleep()plus a configurable busy-spin window through--spin-us
Both are supported because the better choice depends on the host. In the replay measurements below, busy waiting slightly improved the replay error compared with timerfd, while timerfd remains the default because it is the more conservative baseline.
For replay evaluation, we replay the existing comparison-data/generated/ping-test.pcap trace and compare the inter-arrival difference between the original capture and the replayed output.
Measured replay error against comparison-data/generated/ping-test.pcap:
timerfd: abs mean0.012260538 ms, std0.025607717 ms, min-0.179767609 ms, max0.172853470 msnanosleep --spin-us 50: abs mean0.012215671 ms, std0.023457672 ms, min-0.134944916 ms, max0.136375427 msnanosleep --spin-us 100: abs mean0.013313733 ms, std0.023949685 ms, min-0.119447708 ms, max0.086784363 ms
The aggregate heartbeat plot is shown first because it makes packet-by-packet outliers easier to compare across the three pacing modes, followed by the aggregate density view.
For the replay comparison, we use the already captured comparison-data/generated/ping-test.pcap file as the source trace.
Capture on the same machine:
sudo tshark -i eno1 -f "udp port 12345" -w /tmp/ping-test-replayed-result-timerfd.pcapSend:
sudo lateframe -i enp113s0 -d 128.178.122.100 -p 12345 -t pcap -f comparison-data/generated/ping-test.pcap --wait-mode timerfdCapture on the same machine:
sudo tshark -i eno1 -f "udp port 12345" -w /tmp/ping-test-replayed-result-spin50.pcapSend:
sudo lateframe -i enp113s0 -d 128.178.122.100 -p 12345 -t pcap -f comparison-data/generated/ping-test.pcap --wait-mode nanosleep --spin-us 50Capture on the same machine:
sudo tshark -i eno1 -f "udp port 12345" -w /tmp/ping-test-replayed-result-spin100.pcapSend:
sudo lateframe -i enp113s0 -d 128.178.122.100 -p 12345 -t pcap -f comparison-data/generated/ping-test.pcap --wait-mode nanosleep --spin-us 100Then copy the replay PCAPs into comparison-data/replay/ and run:
python3 scripts/plot_interarrival_diff.py \
--original comparison-data/generated/ping-test.pcap \
--replay "timerfd=timerfd=comparison-data/replay/ping-test-replayed-result-timerfd.pcap" \
--replay "spin50=nanosleep spin 50us=comparison-data/replay/ping-test-replayed-result-spin50.pcap" \
--replay "spin100=nanosleep spin 100us=comparison-data/replay/ping-test-replayed-result-spin100.pcap" \
--output-dir docs/replay \
--output-prefix ping-replay- Absolute deadline scheduling on
CLOCK_MONOTONICusing eithertimerfdorclock_nanosleep() - CPU pinning
- Best-effort
SCHED_FIFO - Pre-built payloads for generated traffic
- Optional
tsharkcapture during transmission
For generated traffic modes, LateFrame writes a sequence ID at the beginning of each UDP payload. That makes packet matching easier in captures and receiver logs.
/tmp/lateframe.log/tmp/lateframe-capture.pcap


