* [PATCH BlueZ vRFC 1/4] test-runner: Add support for PCIe passthrough
@ 2026-08-27 16:53 Luiz Augusto von Dentz
2026-08-27 16:53 ` [PATCH BlueZ vRFC 2/4] monitor: Add latency standard deviation Luiz Augusto von Dentz
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Luiz Augusto von Dentz @ 2026-08-27 16:53 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Add a -P/--pcie option which passes the given QEMU device arguments
through to QEMU, in the same way -U/--usb does for USB devices, so a
host controller can be handed to the guest:
$ tools/test-runner -P "vfio-pci,host=0000:00:14.3" \
-d -k /pathto/bzImage -- /bin/bash
The device does not have to be prepared by hand: the BDF is taken from
the host= argument and bound to vfio-pci, and the driver it was bound to
before is restored once the guest exits. As VFIO can only pass through a
device if the rest of its IOMMU group is unbound or already handled by
VFIO, the group is checked before the device is taken away from its
driver, so a group that cannot be used is reported instead of leaving
the device behind on vfio-pci.
Restoring the driver means QEMU can no longer simply replace this
process, so with -P it is started as a child and SIGINT, SIGTERM and
SIGHUP are forwarded to it, leaving this process to restore the driver
once QEMU is reaped.
Since vfio-pci requires ACPI for device enumeration and an APIC for MSI
delivery, the guest command line drops "acpi=off pci=noacpi noapic" when
-P is given. All other modes keep the previous command line and are
still exec'ed directly.
Tested by passing a PCIe card reader through to the guest, checking that
it is bound to vfio-pci while the guest runs and bound back to its own
driver afterwards.
The model drafted the option handling, the vfio binding, the command
line change and the documentation section; all of it was reviewed and
tested by the author.
Assisted-by: Claude:claude-opus-5
---
doc/test-runner.rst | 28 ++++
tools/test-runner.c | 317 ++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 337 insertions(+), 8 deletions(-)
diff --git a/doc/test-runner.rst b/doc/test-runner.rst
index a650c6fae571..6787507c3f40 100644
--- a/doc/test-runner.rst
+++ b/doc/test-runner.rst
@@ -23,6 +23,7 @@ OPTIONS
:-A/-audio[=path]: Start audio server
:-u/--unix[=path]: Provide serial device
:-U/--usb=<qemu_args>: Provide USB device
+:-P/--pcie=<qemu_args>: Provide PCIe device
:-q/--qemu=<path>: QEMU binary
:-k/--kernel=<image>: Kernel image (bzImage)
:-h/--help: Show help options
@@ -230,3 +231,30 @@ In addition the above kernel config option the following is required:
$ tools/test-runner -U "usb-host,vendorid=<0xxxxx>,productid=<0xxxxx>" \
-d -k /pathto/bzImage -- /bin/bash
+
+Running shell with host controller PCIe-passthrough
+---------------------------------------------------
+
+In addition the above kernel config option the following is required:
+
+.. code-block::
+
+ CONFIG_PCI=y
+ CONFIG_PCI_MSI=y
+ CONFIG_ACPI=y
+ CONFIG_BT_HCIBTINTEL_PCIE=y
+
+On the host, an IOMMU must be enabled in the firmware and on the host kernel
+command line (``intel_iommu=on`` or ``amd_iommu=on``). The controller itself
+does not need any manual preparation: test-runner unbinds it from its current
+driver, binds it to vfio-pci, and restores the original driver once the guest
+exits.
+
+.. code-block::
+
+ $ tools/test-runner -P "vfio-pci,host=0000:00:14.3" \
+ -d -k /pathto/bzImage -- /bin/bash
+
+Note that unlike the other modes, PCIe-passthrough boots the guest with ACPI
+and APIC enabled, as these are required for device enumeration and MSI
+interrupt delivery.
diff --git a/tools/test-runner.c b/tools/test-runner.c
index a11dc01a9ee4..63fedece0023 100644
--- a/tools/test-runner.c
+++ b/tools/test-runner.c
@@ -23,6 +23,7 @@
#include <string.h>
#include <getopt.h>
#include <poll.h>
+#include <dirent.h>
#include <limits.h>
#include <sys/wait.h>
#include <sys/stat.h>
@@ -60,6 +61,7 @@ static const char *qemu_binary = NULL;
static const char *kernel_image = NULL;
static char *audio_server;
static char *usb_dev;
+static char *pcie_dev;
static char *extra_opts[EXTRA_OPT_MAX];
static int num_extra_opts;
@@ -260,12 +262,257 @@ static void check_virtualization(void)
#endif
}
-static void start_qemu(void)
+#define PCI_DEVICES_PATH "/sys/bus/pci/devices"
+
+static char pcie_bdf[16];
+static char pcie_driver[64];
+
+static bool sysfs_write(const char *path, const char *value)
+{
+ int fd;
+ ssize_t len;
+
+ fd = open(path, O_WRONLY);
+ if (fd < 0) {
+ perror(path);
+ return false;
+ }
+
+ len = write(fd, value, strlen(value));
+ close(fd);
+
+ if (len < 0) {
+ perror(path);
+ return false;
+ }
+
+ return true;
+}
+
+static bool pcie_parse_bdf(const char *opts, char *bdf, size_t size)
+{
+ unsigned int domain, bus, dev, func;
+ const char *ptr;
+ char addr[32];
+ size_t len;
+
+ ptr = strstr(opts, "host=");
+ if (!ptr)
+ return false;
+
+ ptr += 5;
+ len = strcspn(ptr, ",");
+ if (!len || len >= sizeof(addr))
+ return false;
+
+ memcpy(addr, ptr, len);
+ addr[len] = '\0';
+
+ if (sscanf(addr, "%x:%x:%x.%x", &domain, &bus, &dev, &func) != 4) {
+ domain = 0;
+ if (sscanf(addr, "%x:%x.%x", &bus, &dev, &func) != 3) {
+ fprintf(stderr, "Invalid PCI address %s\n", addr);
+ return false;
+ }
+ }
+
+ snprintf(bdf, size, "%04x:%02x:%02x.%x", domain, bus, dev, func);
+
+ return true;
+}
+
+static void load_module(const char *name)
+{
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ return;
+
+ if (pid == 0) {
+ char *argv[3] = { "/sbin/modprobe", (char *) name, NULL };
+
+ execv(argv[0], argv);
+ exit(EXIT_FAILURE);
+ }
+
+ waitpid(pid, NULL, 0);
+}
+
+/* Returns the name of the driver currently bound to the given device, or
+ * NULL if the device is not bound to any driver.
+ */
+static const char *pcie_get_driver(const char *bdf, char *buf, size_t size)
+{
+ char path[PATH_MAX], link[PATH_MAX];
+ const char *name;
+ ssize_t len;
+
+ snprintf(path, sizeof(path), PCI_DEVICES_PATH "/%s/driver", bdf);
+
+ len = readlink(path, link, sizeof(link) - 1);
+ if (len < 0)
+ return NULL;
+
+ link[len] = '\0';
+
+ name = strrchr(link, '/');
+ name = name ? name + 1 : link;
+
+ snprintf(buf, size, "%.*s", (int) size - 1, name);
+
+ return buf;
+}
+
+static bool pcie_probe(const char *bdf)
+{
+ return sysfs_write("/sys/bus/pci/drivers_probe", bdf);
+}
+
+/* VFIO can only pass through a device if every other device in its IOMMU
+ * group is either unbound or already handled by VFIO, so check that before
+ * taking the device away from its driver.
+ */
+static bool pcie_group_viable(const char *bdf)
+{
+ char path[PATH_MAX], driver[64];
+ struct dirent *entry;
+ bool viable = true;
+ DIR *dir;
+
+ snprintf(path, sizeof(path),
+ PCI_DEVICES_PATH "/%s/iommu_group/devices", bdf);
+
+ dir = opendir(path);
+ if (!dir) {
+ fprintf(stderr, "No IOMMU group for %s, "
+ "is the IOMMU enabled?\n", bdf);
+ return false;
+ }
+
+ while ((entry = readdir(dir))) {
+ const char *name = entry->d_name;
+
+ if (name[0] == '.' || !strcmp(name, bdf))
+ continue;
+
+ if (!pcie_get_driver(name, driver, sizeof(driver)) ||
+ !strcmp(driver, "vfio-pci") ||
+ !strcmp(driver, "pci-stub"))
+ continue;
+
+ fprintf(stderr, "Device %s in the same IOMMU group is bound "
+ "to %s\n", name, driver);
+ viable = false;
+ }
+
+ closedir(dir);
+
+ if (!viable)
+ fprintf(stderr, "IOMMU group of %s is not viable for "
+ "passthrough\n", bdf);
+
+ return viable;
+}
+
+/* Binds the device given with -P to vfio-pci so it can be handed to the
+ * guest, remembering the driver it was bound to so it can be restored once
+ * the guest is done with it.
+ */
+static void pcie_bind_vfio(void)
+{
+ char path[PATH_MAX];
+ const char *driver;
+ struct stat st;
+
+ if (!pcie_parse_bdf(pcie_dev, pcie_bdf, sizeof(pcie_bdf)))
+ return;
+
+ snprintf(path, sizeof(path), PCI_DEVICES_PATH "/%s", pcie_bdf);
+ if (stat(path, &st) < 0) {
+ fprintf(stderr, "PCI device %s not found\n", pcie_bdf);
+ exit(EXIT_FAILURE);
+ }
+
+ load_module("vfio-pci");
+
+ driver = pcie_get_driver(pcie_bdf, pcie_driver, sizeof(pcie_driver));
+ if (driver && !strcmp(driver, "vfio-pci")) {
+ printf("Device %s already bound to vfio-pci\n", pcie_bdf);
+ pcie_bdf[0] = '\0';
+ return;
+ }
+
+ if (!pcie_group_viable(pcie_bdf))
+ exit(EXIT_FAILURE);
+
+ if (driver) {
+ printf("Unbinding %s from %s\n", pcie_bdf, driver);
+
+ snprintf(path, sizeof(path),
+ PCI_DEVICES_PATH "/%s/driver/unbind", pcie_bdf);
+ if (!sysfs_write(path, pcie_bdf)) {
+ fprintf(stderr, "Failed to unbind %s\n", pcie_bdf);
+ exit(EXIT_FAILURE);
+ }
+ } else {
+ pcie_driver[0] = '\0';
+ }
+
+ printf("Binding %s to vfio-pci\n", pcie_bdf);
+
+ snprintf(path, sizeof(path),
+ PCI_DEVICES_PATH "/%s/driver_override", pcie_bdf);
+ if (!sysfs_write(path, "vfio-pci") || !pcie_probe(pcie_bdf)) {
+ fprintf(stderr, "Failed to bind %s to vfio-pci\n", pcie_bdf);
+ exit(EXIT_FAILURE);
+ }
+}
+
+/* Undoes pcie_bind_vfio() */
+static void pcie_unbind_vfio(void)
+{
+ char path[PATH_MAX];
+
+ if (!pcie_bdf[0])
+ return;
+
+ printf("Unbinding %s from vfio-pci\n", pcie_bdf);
+
+ snprintf(path, sizeof(path),
+ PCI_DEVICES_PATH "/%s/driver/unbind", pcie_bdf);
+ sysfs_write(path, pcie_bdf);
+
+ snprintf(path, sizeof(path),
+ PCI_DEVICES_PATH "/%s/driver_override", pcie_bdf);
+ sysfs_write(path, "\n");
+
+ if (!pcie_driver[0])
+ return;
+
+ printf("Binding %s back to %s\n", pcie_bdf, pcie_driver);
+
+ pcie_probe(pcie_bdf);
+}
+
+static pid_t qemu_pid;
+
+/* Forwards the signal to QEMU so it can shutdown, the host driver is then
+ * restored once it is reaped.
+ */
+static void qemu_signal(int sig)
+{
+ if (qemu_pid > 0)
+ kill(qemu_pid, sig);
+}
+
+static int start_qemu(void)
{
char cwd[PATH_MAX/2], initcmd[PATH_MAX], testargs[PATH_MAX];
char cmdline[CMDLINE_MAX];
char **argv;
- int i, pos;
+ int i, pos, status = 0;
+ pid_t pid;
check_virtualization();
@@ -296,11 +543,15 @@ static void start_qemu(void)
"console=hvc0 earlyprintk=serial "
"no_hash_pointers=1 rootfstype=9p "
"rootflags=trans=virtio,version=9p2000.u "
- "acpi=off pci=noacpi noapic quiet ro init=%s "
+ "%s quiet ro init=%s "
"TESTHOME=%s TESTDBUS=%u TESTDAEMON=%u "
"TESTDBUSSESSION=%u XDG_RUNTIME_DIR=/run/user/0 "
"TESTMONITOR=%u TESTEMULATOR=%u TESTDEVS=%d "
"TESTAUTO=%u TESTAUDIO='%s' TESTARGS=\'%s\'",
+ /* PCIe passthrough requires ACPI and APIC for
+ * device enumeration and MSI interrupts.
+ */
+ pcie_dev ? "" : "acpi=off pci=noacpi noapic",
initcmd, cwd, start_dbus, start_daemon,
start_dbus_session,
start_monitor, num_emulator, num_devs,
@@ -310,6 +561,7 @@ static void start_qemu(void)
argv = alloca(sizeof(qemu_argv) +
(sizeof(char *) * (8 + (num_devs * 4))) +
(sizeof(char *) * (usb_dev ? 4 : 0)) +
+ (sizeof(char *) * (pcie_dev ? 2 : 0)) +
(sizeof(char *) * num_extra_opts));
memcpy(argv, qemu_argv, sizeof(qemu_argv));
@@ -354,12 +606,58 @@ static void start_qemu(void)
argv[pos++] = usb_dev;
}
+ if (pcie_dev) {
+ argv[pos++] = "-device";
+ argv[pos++] = pcie_dev;
+ }
+
for (i = 0; i < num_extra_opts; ++i)
argv[pos++] = extra_opts[i];
argv[pos] = NULL;
- execve(argv[0], argv, qemu_envp);
+ if (!pcie_dev) {
+ execve(argv[0], argv, qemu_envp);
+ return EXIT_FAILURE;
+ }
+
+ /* With a device passed through the host driver has to be restored
+ * once the guest is done with it, so QEMU cannot simply replace this
+ * process here.
+ */
+ pcie_bind_vfio();
+
+ pid = fork();
+ if (pid < 0) {
+ perror("Failed to fork new process");
+ pcie_unbind_vfio();
+ return EXIT_FAILURE;
+ }
+
+ if (pid == 0) {
+ execve(argv[0], argv, qemu_envp);
+ exit(EXIT_FAILURE);
+ }
+
+ qemu_pid = pid;
+
+ /* Terminate QEMU rather than this process, so the host driver can be
+ * restored below.
+ */
+ signal(SIGINT, qemu_signal);
+ signal(SIGTERM, qemu_signal);
+ signal(SIGHUP, qemu_signal);
+
+ while (waitpid(pid, &status, 0) < 0) {
+ if (errno != EINTR)
+ break;
+ }
+
+ qemu_pid = -1;
+
+ pcie_unbind_vfio();
+
+ return WIFEXITED(status) ? WEXITSTATUS(status) : EXIT_FAILURE;
}
static int open_serial(const char *path)
@@ -1222,6 +1520,7 @@ static void usage(void)
"\t-A, --audio[=path] Start audio server\n"
"\t-u, --unix[=path] Provide serial device\n"
"\t-U, --usb <qemu_args> Provide USB device\n"
+ "\t-P, --pcie <qemu_args> Provide PCIe device\n"
"\t-q, --qemu <path> QEMU binary\n"
"\t-H, --qemu-host-cpu Use host CPU (requires KVM support)\n"
"\t-k, --kernel <image> Kernel bzImage or source tree path\n"
@@ -1243,6 +1542,7 @@ static const struct option main_options[] = {
{ "kernel", required_argument, NULL, 'k' },
{ "audio", optional_argument, NULL, 'A' },
{ "usb", required_argument, NULL, 'U' },
+ { "pcie", required_argument, NULL, 'P' },
{ "option", required_argument, NULL, 'o' },
{ "version", no_argument, NULL, 'v' },
{ "help", no_argument, NULL, 'h' },
@@ -1265,7 +1565,7 @@ int main(int argc, char *argv[])
for (;;) {
int opt;
- opt = getopt_long(argc, argv, "au::bdsl::mq:Hk:A::U:o:vh",
+ opt = getopt_long(argc, argv, "au::bdsl::mq:Hk:A::U:P:o:vh",
main_options, NULL);
if (opt < 0)
break;
@@ -1310,6 +1610,9 @@ int main(int argc, char *argv[])
case 'U':
usb_dev = optarg;
break;
+ case 'P':
+ pcie_dev = optarg;
+ break;
case 'o':
if (num_extra_opts >= EXTRA_OPT_MAX) {
fprintf(stderr, "Too many -o\n");
@@ -1362,7 +1665,5 @@ int main(int argc, char *argv[])
printf("Using QEMU binary %s\n", qemu_binary);
printf("Using kernel image %s\n", kernel_image);
- start_qemu();
-
- return EXIT_SUCCESS;
+ return start_qemu();
}
--
2.54.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH BlueZ vRFC 2/4] monitor: Add latency standard deviation
2026-08-27 16:53 [PATCH BlueZ vRFC 1/4] test-runner: Add support for PCIe passthrough Luiz Augusto von Dentz
@ 2026-08-27 16:53 ` Luiz Augusto von Dentz
2026-08-27 16:53 ` [PATCH BlueZ vRFC 3/4] monitor: Add ISO packet loss counters Luiz Augusto von Dentz
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Luiz Augusto von Dentz @ 2026-08-27 16:53 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
The latency range and moving average alone do not tell a link that is
consistently slow apart from one that is unstable. A high average with a
low deviation points at the scheduling or the interval configuration,
while a low average with a high deviation points at interference,
retransmissions or controller buffer stalls.
Track the sample count along with the sum and the sum of the squares in
struct packet_latency, and report the resulting standard deviation on
the latency lines of both the live decoding and the analyze mode.
The samples are squared in usec and only scaled down to msec^2
afterwards, so that the sub-msec resolution is not lost while the
running sum stays clear of an overflow.
The model derived the accumulator layout and the scaling, which the
author reviewed and verified against an independently computed
deviation.
Assisted-by: opencode:claude-opus-5
---
Makefile.tools | 2 +-
monitor/analyze.c | 6 ++++--
monitor/packet.c | 41 +++++++++++++++++++++++++++++++++++++++--
monitor/packet.h | 4 ++++
4 files changed, 48 insertions(+), 5 deletions(-)
diff --git a/Makefile.tools b/Makefile.tools
index b3ef4ae1c3df..8f9ffe97cbf0 100644
--- a/Makefile.tools
+++ b/Makefile.tools
@@ -62,7 +62,7 @@ monitor_btmon_SOURCES = monitor/main.c monitor/bt.h \
src/settings.h src/settings.c
monitor_btmon_LDADD = lib/libbluetooth-internal.la \
src/libshared-mainloop.la \
- $(GLIB_LIBS) $(UDEV_LIBS) -ldl
+ $(GLIB_LIBS) $(UDEV_LIBS) -ldl -lm
if MANPAGES
man_MANS += doc/btmon.1
diff --git a/monitor/analyze.c b/monitor/analyze.c
index de9c23603a21..b349887716da 100644
--- a/monitor/analyze.c
+++ b/monitor/analyze.c
@@ -155,10 +155,12 @@ static void print_stats(struct hci_stats *stats, const char *label)
return;
print_field("%s packets: %zu/%zu", label, stats->num, stats->num_comp);
- print_field("%s Latency: %lld-%lld msec (~%lld msec)", label,
+ print_field("%s Latency: %lld-%lld msec (~%lld msec +/- %lld msec)",
+ label,
TV_MSEC(stats->latency.min),
TV_MSEC(stats->latency.max),
- TV_MSEC(stats->latency.med));
+ TV_MSEC(stats->latency.med),
+ packet_latency_stddev(&stats->latency));
print_field("%s size: %u-%u octets (~%zd octets)", label,
stats->min, stats->max, stats->bytes / stats->num);
diff --git a/monitor/packet.c b/monitor/packet.c
index 0d3b23cc3fb7..174d844c05cb 100644
--- a/monitor/packet.c
+++ b/monitor/packet.c
@@ -27,6 +27,7 @@
#include <sys/time.h>
#include <sys/socket.h>
#include <limits.h>
+#include <math.h>
#include "bluetooth/bluetooth.h"
#include "bluetooth/uuid.h"
@@ -11524,8 +11525,27 @@ static void role_change_evt(struct timeval *tv, uint16_t index,
void packet_latency_add(struct packet_latency *latency, struct timeval *delta)
{
+ uint64_t usec;
+
timeradd(&latency->total, delta, &latency->total);
+ /*
+ * Negative deltas are the result of out of order timestamps and
+ * would only skew the deviation, so leave them out.
+ */
+ if (delta->tv_sec >= 0 && delta->tv_usec >= 0) {
+ usec = (uint64_t)delta->tv_sec * 1000000 + delta->tv_usec;
+
+ latency->count++;
+ latency->sum_usec += usec;
+ /*
+ * Square first and scale down to msec^2 afterwards so the
+ * sub-msec resolution is not lost, while keeping the running
+ * sum well clear of an overflow.
+ */
+ latency->sum_sq_msec += (usec * usec) / 1000000;
+ }
+
if ((!timerisset(&latency->min) || timercmp(delta, &latency->min, <))
&& delta->tv_sec >= 0 && delta->tv_usec >= 0)
latency->min = *delta;
@@ -11553,6 +11573,21 @@ void packet_latency_add(struct packet_latency *latency, struct timeval *delta)
latency->med = *delta;
}
+long long packet_latency_stddev(const struct packet_latency *latency)
+{
+ double mean, var;
+
+ if (latency->count < 2)
+ return 0;
+
+ mean = (double)latency->sum_usec / latency->count / 1000;
+ var = (double)latency->sum_sq_msec / latency->count - mean * mean;
+ if (var <= 0)
+ return 0;
+
+ return (long long)sqrt(var);
+}
+
static void packet_dequeue_tx(struct timeval *tv, uint16_t handle)
{
struct packet_conn_data *conn;
@@ -11585,10 +11620,12 @@ static void packet_dequeue_tx(struct timeval *tv, uint16_t handle)
if (TV_MSEC(delta)) {
print_field("#%zu: len %zu (%lld Kb/s)", frame->num, frame->len,
frame->len * 8 / TV_MSEC(delta));
- print_field("Latency: %lld msec (%lld-%lld msec ~%lld msec)",
+ print_field("Latency: %lld msec (%lld-%lld msec ~%lld msec "
+ "+/- %lld msec)",
TV_MSEC(delta), TV_MSEC(conn->tx_l.min),
TV_MSEC(conn->tx_l.max),
- TV_MSEC(conn->tx_l.med));
+ TV_MSEC(conn->tx_l.med),
+ packet_latency_stddev(&conn->tx_l));
}
l2cap_dequeue_frame(&delta, conn);
diff --git a/monitor/packet.h b/monitor/packet.h
index 73a86f64b242..6b792c0e420c 100644
--- a/monitor/packet.h
+++ b/monitor/packet.h
@@ -31,6 +31,9 @@ struct packet_latency {
struct timeval min;
struct timeval max;
struct timeval med;
+ uint64_t count;
+ uint64_t sum_usec; /* Sum of samples, in usec */
+ uint64_t sum_sq_msec; /* Sum of squared samples, in msec^2 */
};
struct packet_frame {
@@ -69,6 +72,7 @@ struct packet_conn_data {
struct packet_conn_data *packet_get_conn_data(uint16_t handle);
void packet_latency_add(struct packet_latency *latency, struct timeval *delta);
+long long packet_latency_stddev(const struct packet_latency *latency);
bool packet_has_filter(unsigned long filter);
void packet_set_filter(unsigned long filter);
--
2.54.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH BlueZ vRFC 3/4] monitor: Add ISO packet loss counters
2026-08-27 16:53 [PATCH BlueZ vRFC 1/4] test-runner: Add support for PCIe passthrough Luiz Augusto von Dentz
2026-08-27 16:53 ` [PATCH BlueZ vRFC 2/4] monitor: Add latency standard deviation Luiz Augusto von Dentz
@ 2026-08-27 16:53 ` Luiz Augusto von Dentz
2026-08-27 16:53 ` [PATCH BlueZ vRFC 4/4] doc/btmon: Document the deviation and " Luiz Augusto von Dentz
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Luiz Augusto von Dentz @ 2026-08-27 16:53 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Isochronous streams are the one place where HCI hands the host enough
information to tell whether data actually arrived: the ISO data packet
header carries an SDU sequence number and a packet status flag.
Track both per connection and report them during the live decoding and
in the analyze mode. Gaps in the sequence number are counted as lost
SDUs, a packet status flag of 2 is counted as dropped and a flag of 1 as
possibly invalid. Duplicates and reordered sequence numbers are not
counted as loss.
During the live decoding the counters are only printed on the packet
where a discontinuity is detected, so a clean stream produces no extra
output.
The model wrote the sequence number accounting and the ISO header
walking, which the author reviewed and verified against a trace with
known gaps.
Assisted-by: opencode:claude-opus-5
---
monitor/analyze.c | 39 ++++++++++++++++++++++++++++++++++
monitor/packet.c | 53 ++++++++++++++++++++++++++++++++++++++++++++---
monitor/packet.h | 11 ++++++++++
3 files changed, 100 insertions(+), 3 deletions(-)
diff --git a/monitor/analyze.c b/monitor/analyze.c
index b349887716da..9742981ec21a 100644
--- a/monitor/analyze.c
+++ b/monitor/analyze.c
@@ -32,6 +32,12 @@
#define TIMEVAL_MSEC(_tv) \
(long long)((_tv)->tv_sec * 1000 + (_tv)->tv_usec / 1000)
+/* Same layout as the ISO handle/flags fields decoded in packet.c */
+#define ISO_FLAGS(_h) ((_h) >> 12)
+#define ISO_FLAGS_PB(_f) ((_f) & 0x0003)
+#define ISO_FLAGS_TS(_f) (((_f) >> 2) & 0x0001)
+#define ISO_DATA_FLAGS(_h) ((_h) >> 14)
+
struct hci_dev {
uint16_t index;
uint8_t type;
@@ -87,6 +93,7 @@ struct hci_conn {
struct queue *chan_list;
struct hci_stats rx;
struct hci_stats tx;
+ struct packet_loss rx_loss;
};
struct hci_conn_tx {
@@ -357,6 +364,15 @@ static void conn_destroy(void *data)
print_stats(&conn->rx, "RX");
print_stats(&conn->tx, "TX");
+ if (conn->rx_loss.total)
+ print_field("RX loss: %zu/%zu (%zu.%02zu%%) "
+ "dropped %zu invalid %zu",
+ conn->rx_loss.lost, conn->rx_loss.total,
+ conn->rx_loss.lost * 100 / conn->rx_loss.total,
+ (conn->rx_loss.lost * 10000 /
+ conn->rx_loss.total) % 100,
+ conn->rx_loss.dropped, conn->rx_loss.invalid);
+
if (conn->setup_seen) {
print_field("Connected: #%lu", conn->frame_connected);
if (conn->terminated) {
@@ -1338,8 +1354,11 @@ static void iso_pkt(struct timeval *tv, uint16_t index, bool out,
const void *data, uint16_t size)
{
const struct bt_hci_iso_hdr *hdr = data;
+ struct iovec iov = { .iov_base = (void *)data, .iov_len = size };
struct hci_conn *conn;
struct hci_dev *dev;
+ uint16_t handle;
+ uint8_t flags, pb_flag;
dev = dev_lookup(index);
if (!dev)
@@ -1357,6 +1376,26 @@ static void iso_pkt(struct timeval *tv, uint16_t index, bool out,
return;
}
+ handle = le16_to_cpu(hdr->handle);
+ flags = ISO_FLAGS(handle);
+ pb_flag = ISO_FLAGS_PB(flags);
+
+ /* Only the first fragment of an SDU carries the sequence number */
+ if (!out && (pb_flag == 0x00 || pb_flag == 0x02)) {
+ const struct bt_hci_iso_data_start *start;
+
+ util_iov_pull_mem(&iov, sizeof(*hdr));
+
+ /* Skip the timestamp when present */
+ if (ISO_FLAGS_TS(flags))
+ util_iov_pull_mem(&iov, sizeof(uint32_t));
+
+ start = util_iov_pull_mem(&iov, sizeof(*start));
+ if (start)
+ packet_loss_add(&conn->rx_loss, le16_to_cpu(start->sn),
+ ISO_DATA_FLAGS(le16_to_cpu(start->slen)));
+ }
+
if (out) {
conn_pkt_tx(conn, tv, size - sizeof(*hdr), NULL);
} else {
diff --git a/monitor/packet.c b/monitor/packet.c
index 174d844c05cb..db52eb789629 100644
--- a/monitor/packet.c
+++ b/monitor/packet.c
@@ -11588,6 +11588,37 @@ long long packet_latency_stddev(const struct packet_latency *latency)
return (long long)sqrt(var);
}
+void packet_loss_add(struct packet_loss *loss, uint16_t sn, uint8_t sflags)
+{
+ loss->total++;
+
+ switch (sflags) {
+ case 0x01:
+ loss->invalid++;
+ break;
+ case 0x02:
+ loss->dropped++;
+ break;
+ }
+
+ if (loss->have_sn) {
+ uint16_t gap = sn - loss->last_sn - 1;
+
+ /*
+ * A sequence number that did not advance is a duplicate or a
+ * retransmission, and one that moved backwards is a reorder.
+ * Neither is a loss, so only account for forward gaps.
+ */
+ if (sn != loss->last_sn && gap < 0x8000) {
+ loss->lost += gap;
+ loss->total += gap;
+ }
+ }
+
+ loss->last_sn = sn;
+ loss->have_sn = true;
+}
+
static void packet_dequeue_tx(struct timeval *tv, uint16_t handle)
{
struct packet_conn_data *conn;
@@ -14587,6 +14618,8 @@ void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
struct packet_conn_data *conn;
size_t ts_size = 0;
bool have_hdr;
+ uint16_t sn = 0;
+ uint8_t sflags = 0;
if (index >= MAX_INDEX) {
print_field("Invalid index (%d).", index);
@@ -14625,12 +14658,12 @@ void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
if (have_hdr) {
const struct bt_hci_iso_data_start *start = data;
- uint8_t sflags;
uint16_t slen;
if (size < sizeof(*start))
goto malformed;
+ sn = le16_to_cpu(start->sn);
sflags = iso_data_flags(le16_to_cpu(start->slen));
slen = iso_data_len(le16_to_cpu(start->slen));
@@ -14641,12 +14674,14 @@ void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
sizeof(slen_str) - strlen(slen_str),
" sflags %u", sflags);
- snprintf(sn_str, sizeof(sn_str), " SN %u",
- le16_to_cpu(start->sn));
+ snprintf(sn_str, sizeof(sn_str), " SN %u", sn);
}
conn = packet_get_conn_data(handle);
+ if (in && have_hdr && conn)
+ packet_loss_add(&conn->rx_loss, sn, sflags);
+
if (!in && pool->total)
sprintf(handle_str, "Handle %d [%u/%u]%s",
acl_handle(handle), ++pool->tx, pool->total, sn_str);
@@ -14666,6 +14701,18 @@ void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
print_packet(tv, cred, in ? '>' : '<', index, NULL, COLOR_HCI_ISODATA,
label, handle_str, extra_str);
+ if (in && conn) {
+ struct packet_loss *loss = &conn->rx_loss;
+
+ if (loss->lost || loss->dropped || loss->invalid)
+ print_field("Lost: %zu/%zu (%zu.%02zu%%) "
+ "dropped %zu invalid %zu",
+ loss->lost, loss->total,
+ loss->lost * 100 / loss->total,
+ (loss->lost * 10000 / loss->total) % 100,
+ loss->dropped, loss->invalid);
+ }
+
if (!in)
packet_enqueue_tx(tv, acl_handle(handle),
index_list[index].frame, dlen);
diff --git a/monitor/packet.h b/monitor/packet.h
index 6b792c0e420c..9d1efdf45258 100644
--- a/monitor/packet.h
+++ b/monitor/packet.h
@@ -36,6 +36,15 @@ struct packet_latency {
uint64_t sum_sq_msec; /* Sum of squared samples, in msec^2 */
};
+struct packet_loss {
+ uint16_t last_sn;
+ bool have_sn;
+ size_t lost; /* Samples missing from the SN sequence */
+ size_t invalid; /* Samples flagged possibly invalid */
+ size_t dropped; /* Samples flagged as lost data */
+ size_t total; /* Samples seen, including the lost ones */
+};
+
struct packet_frame {
struct timeval tv;
size_t num;
@@ -66,6 +75,7 @@ struct packet_conn_data {
struct queue *tx_q;
struct queue *chan_q;
struct packet_latency tx_l;
+ struct packet_loss rx_loss;
void *data;
void (*destroy)(struct packet_conn_data *conn, void *data);
};
@@ -73,6 +83,7 @@ struct packet_conn_data {
struct packet_conn_data *packet_get_conn_data(uint16_t handle);
void packet_latency_add(struct packet_latency *latency, struct timeval *delta);
long long packet_latency_stddev(const struct packet_latency *latency);
+void packet_loss_add(struct packet_loss *loss, uint16_t sn, uint8_t sflags);
bool packet_has_filter(unsigned long filter);
void packet_set_filter(unsigned long filter);
--
2.54.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH BlueZ vRFC 4/4] doc/btmon: Document the deviation and loss counters
2026-08-27 16:53 [PATCH BlueZ vRFC 1/4] test-runner: Add support for PCIe passthrough Luiz Augusto von Dentz
2026-08-27 16:53 ` [PATCH BlueZ vRFC 2/4] monitor: Add latency standard deviation Luiz Augusto von Dentz
2026-08-27 16:53 ` [PATCH BlueZ vRFC 3/4] monitor: Add ISO packet loss counters Luiz Augusto von Dentz
@ 2026-08-27 16:53 ` Luiz Augusto von Dentz
2026-08-28 1:17 ` [BlueZ,vRFC,1/4] test-runner: Add support for PCIe passthrough bluez.test.bot
2026-09-04 19:30 ` [PATCH BlueZ vRFC 1/4] " patchwork-bot+bluetooth
4 siblings, 0 replies; 6+ messages in thread
From: Luiz Augusto von Dentz @ 2026-08-27 16:53 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Describe how to read the latency standard deviation and each of the ISO
packet loss counters, and correct the description of the moving average,
which was documented as a median even though it has always been computed
as a moving average.
The model drafted the text, which the author reviewed against the actual
output.
Assisted-by: opencode:claude-opus-5
---
doc/btmon.rst | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 51 insertions(+), 1 deletion(-)
diff --git a/doc/btmon.rst b/doc/btmon.rst
index f701454e598e..adb2c9881f97 100644
--- a/doc/btmon.rst
+++ b/doc/btmon.rst
@@ -561,9 +561,11 @@ Analyze mode reports, for each controller found in the trace:
- Connection type (BR-ACL, LE-ACL, BR-SCO, BR-ESCO, LE-ISO)
- Device address
- TX and RX packet counts and completion counts
- - Latency statistics (min, max, median) in milliseconds
+ - Latency statistics (min, max, moving average and standard
+ deviation) in milliseconds
- Packet size statistics (min, max, average) in octets
- Throughput estimate in Kb/s
+ - Packet loss statistics, for isochronous connections
- **Per-channel statistics**: For each L2CAP channel within a
connection, the same packet/latency/size statistics.
@@ -571,6 +573,54 @@ Analyze mode reports, for each controller found in the trace:
- **Latency plots**: If ``gnuplot`` is installed, ASCII-art latency
distribution plots are rendered in the terminal.
+Latency Standard Deviation
+--------------------------
+
+Latency is reported as::
+
+ TX Latency: 5-300 msec (~91 msec +/- 104 msec)
+
+The range is the minimum and maximum observed latency, ``~`` is a
+moving average and ``+/-`` is the standard deviation over all samples.
+
+The standard deviation is what distinguishes a link that is merely slow
+from one that is unstable. A high average with a low deviation means
+consistent latency, which is usually a scheduling or interval
+configuration issue. A low average with a high deviation means most
+packets are fast but some are heavily delayed, which typically points at
+interference, retransmissions or controller buffer stalls. Comparing the
+maximum against ``average + deviation`` shows whether the worst case is
+representative or a one-off outlier.
+
+Packet Loss
+-----------
+
+Packet loss is tracked for isochronous (CIS/BIS) connections, where the
+ISO data packet header carries an SDU sequence number and a packet
+status flag. It is reported as::
+
+ RX loss: 13/21 (61.90%) dropped 1 invalid 1
+
+The counters are:
+
+- **loss**: SDUs missing from the received sequence, derived from gaps
+ in the sequence number. The denominator is the number of SDUs that
+ should have been received, that is the ones actually seen plus the
+ ones detected as missing.
+
+- **dropped**: SDUs the controller delivered with a packet status flag
+ of ``2`` (lost data), meaning the controller knows the payload did not
+ arrive.
+
+- **invalid**: SDUs delivered with a packet status flag of ``1``
+ (possibly invalid), meaning the payload arrived but may be corrupt.
+
+Duplicate and reordered sequence numbers are not counted as loss.
+
+Loss counters are also shown during live decoding, on the ISO data
+packet where a discontinuity is detected. The line is only emitted once
+a loss has occurred, so a clean stream produces no extra output.
+
PROTOCOL ERROR CODES
=====================
--
2.54.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* RE: [BlueZ,vRFC,1/4] test-runner: Add support for PCIe passthrough
2026-08-27 16:53 [PATCH BlueZ vRFC 1/4] test-runner: Add support for PCIe passthrough Luiz Augusto von Dentz
` (2 preceding siblings ...)
2026-08-27 16:53 ` [PATCH BlueZ vRFC 4/4] doc/btmon: Document the deviation and " Luiz Augusto von Dentz
@ 2026-08-28 1:17 ` bluez.test.bot
2026-09-04 19:30 ` [PATCH BlueZ vRFC 1/4] " patchwork-bot+bluetooth
4 siblings, 0 replies; 6+ messages in thread
From: bluez.test.bot @ 2026-08-28 1:17 UTC (permalink / raw)
To: linux-bluetooth, luiz.dentz
[-- Attachment #1: Type: text/plain, Size: 5189 bytes --]
This is automated email and please do not reply to this email!
Dear submitter,
Thank you for submitting the patches to the linux bluetooth mailing list.
This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/project/bluetooth/list/?series=1152755
---Test result---
Test Summary:
CheckPatch FAIL 2.19 seconds
GitLint PASS 1.31 seconds
BuildEll PASS 19.97 seconds
BluezMake PASS 541.80 seconds
MakeCheck PASS 19.49 seconds
MakeDistcheck PASS 152.15 seconds
CheckValgrind PASS 220.65 seconds
CheckSmatch WARNING 294.22 seconds
bluezmakeextell PASS 95.10 seconds
IncrementalBuild PASS 590.14 seconds
ScanBuild PASS 866.71 seconds
Details
##############################
Test: CheckPatch - FAIL
Desc: Run checkpatch.pl script
Output:
[BlueZ,vRFC,1/4] test-runner: Add support for PCIe passthrough
WARNING:BAD_SIGN_OFF: Non-standard signature: Assisted-by:
#130:
Assisted-by: Claude:claude-opus-5
ERROR:BAD_SIGN_OFF: Unrecognized email address: 'Claude:claude-opus-5'
#130:
Assisted-by: Claude:claude-opus-5
/github/workspace/src/patch/14771785.patch total: 1 errors, 1 warnings, 431 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/github/workspace/src/patch/14771785.patch has style problems, please review.
NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
[BlueZ,vRFC,2/4] monitor: Add latency standard deviation
WARNING:BAD_SIGN_OFF: Non-standard signature: Assisted-by:
#119:
Assisted-by: opencode:claude-opus-5
ERROR:BAD_SIGN_OFF: Unrecognized email address: 'opencode:claude-opus-5'
#119:
Assisted-by: opencode:claude-opus-5
/github/workspace/src/patch/14771786.patch total: 1 errors, 1 warnings, 107 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/github/workspace/src/patch/14771786.patch has style problems, please review.
NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
[BlueZ,vRFC,3/4] monitor: Add ISO packet loss counters
WARNING:BAD_SIGN_OFF: Non-standard signature: Assisted-by:
#119:
Assisted-by: opencode:claude-opus-5
ERROR:BAD_SIGN_OFF: Unrecognized email address: 'opencode:claude-opus-5'
#119:
Assisted-by: opencode:claude-opus-5
/github/workspace/src/patch/14771787.patch total: 1 errors, 1 warnings, 192 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/github/workspace/src/patch/14771787.patch has style problems, please review.
NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
[BlueZ,vRFC,4/4] doc/btmon: Document the deviation and loss counters
WARNING:BAD_SIGN_OFF: Non-standard signature: Assisted-by:
#110:
Assisted-by: opencode:claude-opus-5
ERROR:BAD_SIGN_OFF: Unrecognized email address: 'opencode:claude-opus-5'
#110:
Assisted-by: opencode:claude-opus-5
/github/workspace/src/patch/14771788.patch total: 1 errors, 1 warnings, 66 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/github/workspace/src/patch/14771788.patch has style problems, please review.
NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
##############################
Test: CheckSmatch - WARNING
Desc: Run smatch tool with source
Output:
monitor/packet.c:2003:26: warning: Variable length array is used.monitor/packet.c: note: in included file:monitor/bt.h:3924:52: warning: array of flexible structuresmonitor/bt.h:3912:40: warning: array of flexible structuresmonitor/packet.c:2003:26: warning: Variable length array is used.monitor/packet.c: note: in included file:monitor/bt.h:3924:52: warning: array of flexible structuresmonitor/bt.h:3912:40: warning: array of flexible structures
https://github.com/bluez/bluez/pull/2444
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH BlueZ vRFC 1/4] test-runner: Add support for PCIe passthrough
2026-08-27 16:53 [PATCH BlueZ vRFC 1/4] test-runner: Add support for PCIe passthrough Luiz Augusto von Dentz
` (3 preceding siblings ...)
2026-08-28 1:17 ` [BlueZ,vRFC,1/4] test-runner: Add support for PCIe passthrough bluez.test.bot
@ 2026-09-04 19:30 ` patchwork-bot+bluetooth
4 siblings, 0 replies; 6+ messages in thread
From: patchwork-bot+bluetooth @ 2026-09-04 19:30 UTC (permalink / raw)
To: Luiz Augusto von Dentz; +Cc: linux-bluetooth
Hello:
This series was applied to bluetooth/bluez.git (master)
by Luiz Augusto von Dentz <luiz.von.dentz@intel.com>:
On Thu, 27 Aug 2026 12:53:23 -0400 you wrote:
> From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
>
> Add a -P/--pcie option which passes the given QEMU device arguments
> through to QEMU, in the same way -U/--usb does for USB devices, so a
> host controller can be handed to the guest:
>
> $ tools/test-runner -P "vfio-pci,host=0000:00:14.3" \
> -d -k /pathto/bzImage -- /bin/bash
>
> [...]
Here is the summary with links:
- [BlueZ,vRFC,1/4] test-runner: Add support for PCIe passthrough
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=5ef24e77a642
- [BlueZ,vRFC,2/4] monitor: Add latency standard deviation
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=24a46bbd3b78
- [BlueZ,vRFC,3/4] monitor: Add ISO packet loss counters
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=7bcf6c42721e
- [BlueZ,vRFC,4/4] doc/btmon: Document the deviation and loss counters
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=4b92dddc8e54
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-09-04 19:31 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-27 16:53 [PATCH BlueZ vRFC 1/4] test-runner: Add support for PCIe passthrough Luiz Augusto von Dentz
2026-08-27 16:53 ` [PATCH BlueZ vRFC 2/4] monitor: Add latency standard deviation Luiz Augusto von Dentz
2026-08-27 16:53 ` [PATCH BlueZ vRFC 3/4] monitor: Add ISO packet loss counters Luiz Augusto von Dentz
2026-08-27 16:53 ` [PATCH BlueZ vRFC 4/4] doc/btmon: Document the deviation and " Luiz Augusto von Dentz
2026-08-28 1:17 ` [BlueZ,vRFC,1/4] test-runner: Add support for PCIe passthrough bluez.test.bot
2026-09-04 19:30 ` [PATCH BlueZ vRFC 1/4] " patchwork-bot+bluetooth
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox