Merge branch 'feat/ble-log-test-app-support' into 'master'

feat(ble_log): add BLE log perf test app with test transport

See merge request espressif/esp-idf!51767
This commit is contained in:
Island
2026-08-17 11:16:14 +08:00
14 changed files with 1643 additions and 0 deletions

View File

@@ -88,6 +88,7 @@ endif()
set(bt_priv_requires
nvs_flash
soc
esp_hw_support
esp_pm
esp_phy
esp_coex

View File

@@ -160,6 +160,10 @@ if(CONFIG_BLE_LOG_ENABLED)
list(APPEND bt_common_srcs
"${CMAKE_CURRENT_LIST_DIR}/ble_log/src/prph/ble_log_prph_dummy.c"
)
elseif(CONFIG_BLE_LOG_PRPH_TEST)
list(APPEND bt_common_srcs
"${CMAKE_CURRENT_LIST_DIR}/ble_log/src/prph/ble_log_prph_test.c"
)
elseif(CONFIG_BLE_LOG_PRPH_SPI_MASTER_DMA)
list(APPEND bt_common_srcs
"${CMAKE_CURRENT_LIST_DIR}/ble_log/src/prph/ble_log_prph_spi_master_dma.c"

View File

@@ -198,6 +198,13 @@ if BLE_LOG_ENABLED
help
Dummy transport (dump only)
config BLE_LOG_PRPH_TEST
bool "Test only transport"
help
Test transport that models DMA ownership and optional
link-rate backpressure. Selected by BLE Log test apps
only; not shown in menuconfig.
config BLE_LOG_PRPH_SPI_MASTER_DMA
bool "Utilize SPI master DMA driver as transport"
depends on SOC_GPSPI_SUPPORTED

View File

@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef __BLE_LOG_PRPH_TEST_H__
#define __BLE_LOG_PRPH_TEST_H__
/* ---------------------------------------------- */
/* BLE Log - Peripheral-specific Transport - Test */
/* ---------------------------------------------- */
/* INCLUDE */
#include "ble_log_prph.h"
/* TYPEDEF */
typedef struct {
uint8_t *trans_buf;
} ble_log_prph_trans_ctx_t;
/* Reads and releases one pending transport, returning the bytes copied.
* When bytes_per_second is non-zero the call blocks until the simulated
* link has transmitted the whole transport at that rate. */
size_t ble_log_prph_test_read(uint8_t *data, size_t len, TickType_t timeout,
uint32_t bytes_per_second);
#endif /* __BLE_LOG_PRPH_TEST_H__ */

View File

@@ -0,0 +1,190 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/* ---------------------------------------------- */
/* BLE Log - Peripheral-specific Transport - Test */
/* ---------------------------------------------- */
/* INCLUDE */
#include "ble_log_prph_test.h"
#include "ble_log_lbm.h"
#include "esp_timer.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
/* VARIABLE */
static QueueHandle_t s_pending_trans;
static SemaphoreHandle_t s_tx_done;
static esp_timer_handle_t s_tx_timer;
static int64_t s_tx_deadline_us;
static uint32_t s_tx_rate;
static void test_tx_done(void *arg)
{
(void)arg;
xSemaphoreGive(s_tx_done);
}
/* INTERFACE */
bool ble_log_prph_init(size_t trans_cnt)
{
if (s_pending_trans) {
return true;
}
s_pending_trans = xQueueCreate(trans_cnt, sizeof(ble_log_prph_trans_t *));
s_tx_done = xSemaphoreCreateBinary();
const esp_timer_create_args_t timer_args = {
.callback = test_tx_done,
.name = "ble_log_test",
};
if (!s_pending_trans || !s_tx_done ||
esp_timer_create(&timer_args, &s_tx_timer) != ESP_OK) {
ble_log_prph_deinit();
return false;
}
return true;
}
void ble_log_prph_deinit(void)
{
s_tx_deadline_us = 0;
s_tx_rate = 0;
if (s_tx_timer) {
esp_timer_stop(s_tx_timer);
esp_timer_delete(s_tx_timer);
s_tx_timer = NULL;
}
if (s_tx_done) {
vSemaphoreDelete(s_tx_done);
s_tx_done = NULL;
}
if (!s_pending_trans) {
return;
}
ble_log_prph_trans_t *trans;
while (xQueueReceive(s_pending_trans, &trans, 0) == pdTRUE) {
ble_log_lbm_recycle_trans(trans);
}
vQueueDelete(s_pending_trans);
s_pending_trans = NULL;
}
bool ble_log_prph_trans_init(ble_log_prph_trans_t **trans, size_t trans_size)
{
/* Validate inputs */
if (!trans || !trans_size) {
return false;
}
/* Initialize peripheral transport data */
*trans = (ble_log_prph_trans_t *)BLE_LOG_MALLOC(sizeof(ble_log_prph_trans_t));
if (!(*trans)) {
goto exit;
}
BLE_LOG_MEMSET(*trans, 0, sizeof(ble_log_prph_trans_t));
(*trans)->size = trans_size;
/* Initialize peripheral-specific transport context */
ble_log_prph_trans_ctx_t *test_trans_ctx = (ble_log_prph_trans_ctx_t *)BLE_LOG_MALLOC(sizeof(ble_log_prph_trans_ctx_t));
if (!test_trans_ctx) {
goto exit;
}
BLE_LOG_MEMSET(test_trans_ctx, 0, sizeof(ble_log_prph_trans_ctx_t));
(*trans)->ctx = (void *)test_trans_ctx;
/* Initialize log buffer */
(*trans)->buf = (uint8_t *)BLE_LOG_MALLOC(trans_size);
if (!(*trans)->buf) {
goto exit;
}
BLE_LOG_MEMSET((*trans)->buf, 0, trans_size);
test_trans_ctx->trans_buf = (*trans)->buf;
return true;
exit:
ble_log_prph_trans_deinit(trans);
return false;
}
void ble_log_prph_trans_deinit(ble_log_prph_trans_t **trans)
{
/* Validate inputs */
if (!trans || !(*trans)) {
return;
}
/* Release log buffer */
if ((*trans)->buf) {
BLE_LOG_FREE((*trans)->buf);
}
/* Release peripheral-specific transport context */
if ((*trans)->ctx) {
BLE_LOG_FREE((*trans)->ctx);
}
/* Release peripheral transport data */
BLE_LOG_FREE(*trans);
*trans = NULL;
}
/* Model DMA ownership transfer: the test peripheral owns the transaction
* after enqueueing it. ble_log_prph_test_read() consumes and recycles it,
* equivalent to a real peripheral's asynchronous tx_done callback. */
void ble_log_prph_send_trans(ble_log_prph_trans_t *trans)
{
if (xQueueSend(s_pending_trans, &trans, 0) != pdTRUE) {
trans->pos = 0;
ble_log_lbm_recycle_trans(trans);
BLE_LOG_ASSERT(false);
}
}
size_t ble_log_prph_test_read(uint8_t *data, size_t len, TickType_t timeout,
uint32_t bytes_per_second)
{
if (!data) {
return 0;
}
ble_log_prph_trans_t *trans;
if (xQueueReceive(s_pending_trans, &trans, timeout) != pdTRUE) {
return 0;
}
if (bytes_per_second) {
int64_t now_us = esp_timer_get_time();
uint64_t tx_time_us = ((uint64_t)trans->pos * 1000000ULL +
bytes_per_second - 1) / bytes_per_second;
if (!s_tx_deadline_us || s_tx_rate != bytes_per_second) {
s_tx_deadline_us = now_us;
}
s_tx_rate = bytes_per_second;
s_tx_deadline_us += (int64_t)tx_time_us;
int64_t delay_us = s_tx_deadline_us - now_us;
if (delay_us > 0 &&
(esp_timer_start_once(s_tx_timer, (uint64_t)delay_us) != ESP_OK ||
xSemaphoreTake(s_tx_done, portMAX_DELAY) != pdTRUE)) {
trans->pos = 0;
ble_log_lbm_recycle_trans(trans);
BLE_LOG_ASSERT(false);
return 0;
}
} else {
s_tx_deadline_us = 0;
s_tx_rate = 0;
}
size_t copied = len < trans->pos ? len : trans->pos;
BLE_LOG_MEMCPY(data, trans->buf, copied);
trans->pos = 0;
ble_log_lbm_recycle_trans(trans);
if (uxQueueMessagesWaiting(s_pending_trans) == 0) {
s_tx_deadline_us = 0;
}
return copied;
}

View File

@@ -0,0 +1,9 @@
# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps
components/bt/common/ble_log/test_apps:
disable:
- if: IDF_TARGET != "none"
temporary: true
reason: No BLE Log test runners are available yet
depends_components:
- bt

View File

@@ -0,0 +1,13 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
#
# SPDX-License-Identifier: CC0-1.0
cmake_minimum_required(VERSION 3.22)
list(PREPEND SDKCONFIG_DEFAULTS
"$ENV{IDF_PATH}/tools/test_apps/configs/sdkconfig.debug_helpers"
"sdkconfig.defaults")
set(COMPONENTS main)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ble_log_test)

View File

@@ -0,0 +1,65 @@
# BLE Log Performance Test App
## Overview
Performance test app for the ESP-IDF BLE Log module. It measures the system-level
behavior and per-API CPU cost of the Log Buffer Management (LBM) layer, so that
two LBM designs can be compared before and after an optimization.
The test runs on real hardware (any BLE-capable chip, currently tested on
ESP32-C6). The transport is replaced by a software model
(`CONFIG_BLE_LOG_PRPH_TEST=y`) that mimics DMA ownership transfer and link
bandwidth, so the measurements isolate the LBM layer itself.
## What Is Measured
| Dimension | Metrics |
| --- | --- |
| Throughput | sustained B/s against simulated links of 2 Mbps / 20 Mbps / unlimited, under a mixed load of all writer types |
| Frame loss | per-writer failed counts cross-checked against the LBM's own lost-frame statistics; both must match |
| API cycles | per-call CPU cycles of `ble_log_write_hex`, `ble_log_write_hex_ll` (task/ISR/append), and the compressed-log entry points, with avg/p50/p95/p99/max |
| Compression split | encode vs downstream write_hex cost, derived from a companion writer at the same record length (no instrumentation in the module) |
## Test Cases
- `throughput`: fixed 32B / 64B / 128B / mixed 8-64B payload profiles, each at
2 Mbps, 20 Mbps, and unlimited link. Runs 3 write_hex writers + LL task + LL
HCI + compressed writer + 1 kHz ISR writer concurrently.
- `write_hex cycles`: single writer, no link cap, payload 8/32/64/128 B.
- `write_hex drop path cycles`: saturated 2 Mbps link, measures the cost of a
failed (dropped) write.
- `write_hex_ll cycles`: payload 8/32/64/128 B; plus a 32+32 B append case.
- `compressed write cycles`: workload matrix of the compressed entry points —
U32 args (0/1/2/mixed), U64 values (full 8B / leading-zero LZ / zero),
strings (8B / 128B), raw buffer (128B), and the `hex_printv` va_list entry.
## Build, Flash, Run
```bash
cd components/bt/common/ble_log/test_apps/ble_log_perf_test
idf.py set-target <chip>
idf.py -p <PORT> build flash monitor
```
The app boots into the Unity menu; enter a test number to run it. Every run
prints a `BLE_LOG_PERF` block (blank-line separated) with writer stats,
throughput, flush cost, and LBM statistics.
## Parsing Results
Capture the monitor output and turn it into tables/CSV:
```bash
idf.py -p <PORT> monitor | tee capture.log
python3 tools/parse_perf_log.py capture.log # markdown tables
python3 tools/parse_perf_log.py capture.log --csv out.csv
```
Run the same capture twice (old vs new LBM) and diff the CSV.
## Supported Targets
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
CI builds are temporarily disabled until BLE Log test runners are available.

View File

@@ -0,0 +1,13 @@
idf_component_register(
SRCS "test_ble_log_main.c" "test_ble_log_perf.c"
INCLUDE_DIRS "."
PRIV_REQUIRES unity bt esp_driver_gpio esp_driver_spi esp_driver_uart esp_hw_support esp_timer
WHOLE_ARCHIVE
)
idf_component_get_property(bt_dir bt COMPONENT_DIR)
target_include_directories(${COMPONENT_LIB} PRIVATE
"${bt_dir}/common/ble_log/include"
"${bt_dir}/common/ble_log/src/internal_include"
"${bt_dir}/common/ble_log/src/internal_include/prph"
)

View File

@@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "unity.h"
#include "unity_test_runner.h"
#include "ble_log.h"
#include "ble_log_lbm.h"
#include "test_ble_log_main.h"
bool test_ble_log_walk_frames(const uint8_t *data, size_t len,
test_ble_log_frame_observer_t observer, void *ctx)
{
size_t offset = 0;
while (len - offset >= BLE_LOG_FRAME_OVERHEAD) {
ble_log_frame_head_t head;
memcpy(&head, data + offset, sizeof(head));
size_t frame_len = BLE_LOG_FRAME_OVERHEAD + head.length;
if (frame_len > len - offset) {
return false;
}
uint32_t checksum;
memcpy(&checksum, data + offset + BLE_LOG_FRAME_HEAD_LEN + head.length,
sizeof(checksum));
if (checksum != ble_log_fast_checksum(data + offset,
BLE_LOG_FRAME_HEAD_LEN + head.length)) {
return false;
}
if (observer) {
test_ble_log_frame_t frame = {
.src = head.frame_meta & 0xff,
.sn = head.frame_meta >> 8,
.payload = data + offset + BLE_LOG_FRAME_HEAD_LEN,
.payload_len = head.length,
};
observer(&frame, ctx);
}
offset += frame_len;
}
return offset == len;
}
void setUp(void)
{
}
void tearDown(void)
{
}
void app_main(void)
{
/* The BLE Log module has no automatic system init on this branch; the
* controller normally calls ble_log_init(). Initialize it explicitly. */
TEST_ASSERT_TRUE_MESSAGE(ble_log_init(), "BLE Log init failed");
unity_run_menu();
}

View File

@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "ble_log.h"
typedef struct {
ble_log_src_t src;
uint32_t sn;
const uint8_t *payload;
size_t payload_len;
} test_ble_log_frame_t;
typedef void (*test_ble_log_frame_observer_t)(const test_ble_log_frame_t *frame, void *ctx);
/* Walks a captured transport buffer, validating frame headers and checksums.
* Returns true when the whole buffer consists of valid frames. */
bool test_ble_log_walk_frames(const uint8_t *data, size_t len,
test_ble_log_frame_observer_t observer, void *ctx);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
CONFIG_BT_ENABLED=y
CONFIG_BLE_LOG_ENABLED=y
CONFIG_BLE_LOG_PRPH_TEST=y
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
# esp_timer ISR dispatch is used for the periodic ISR-context writer
CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD=y
# 64-bit assertions used by the perf test statistics checks
CONFIG_UNITY_ENABLE_64BIT=y

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
#
# SPDX-License-Identifier: Apache-2.0
"""Turn captured BLE Log perf monitor output into tables / CSV.
Capture with: idf.py -p PORT monitor | tee capture.log
Then: python3 parse_perf_log.py capture.log # markdown tables
python3 parse_perf_log.py capture.log --csv out.csv
"""
import csv
import re
import sys
from typing import TypedDict
KV = re.compile(r'(\w+)=(\S+)')
WRITER_COLS = ('frames', 'failed', 'avg', 'avg_failed', 'p50', 'p95', 'p99', 'max')
class Run(TypedDict):
head: dict[str, str]
writers: list[dict[str, str]]
other: list[str]
def fields(line: str) -> dict[str, str]:
return dict(KV.findall(line))
def main() -> int:
if len(sys.argv) < 2:
print(__doc__)
return 1
path = sys.argv[1]
csv_path = None
if '--csv' in sys.argv:
csv_path = sys.argv[sys.argv.index('--csv') + 1]
with open(path, encoding='utf-8', errors='replace') as f:
lines = f.read().splitlines()
runs: list[Run] = []
cur: Run | None = None
for line in lines:
if not line.startswith('BLE_LOG_PERF '):
continue
kv = fields(line)
if line.startswith('BLE_LOG_PERF ===='):
if ' RUN [' in line:
cur = Run(head=kv, writers=[], other=[])
runs.append(cur)
# END separator: keep context, nothing to record
continue
if cur is None:
continue
if 'writer' in kv:
cur['writers'].append(kv)
elif 'mode' in kv:
cur['head'].update(kv) # duration etc.
else:
cur['other'].append(line[len('BLE_LOG_PERF ') :])
if not runs:
print(f'no BLE_LOG_PERF lines found in {path}')
return 1
for i, run in enumerate(runs, 1):
h = run['head']
title = (
f'## Run {i}: mode={h.get("mode", "?")} '
f'profile={h.get("profile", "?")} '
f'link={h.get("link", "?")} isolate={h.get("isolate", "?")}'
)
print(title)
if run['writers']:
header = '| writer | ' + ' | '.join(WRITER_COLS) + ' |'
print(header)
print('|---|' + '---|' * len(WRITER_COLS))
for w in run['writers']:
row = ' | '.join(w.get(c, '-') for c in WRITER_COLS)
print(f'| {w.get("writer", "?")} | {row} |')
for o in run['other']:
print(f'- `{o}`')
print()
if csv_path:
with open(csv_path, 'w', newline='', encoding='utf-8') as f:
wcsv = csv.writer(f)
wcsv.writerow(['run', 'mode', 'profile', 'link', 'isolate', 'writer', *WRITER_COLS])
for i, run in enumerate(runs, 1):
h = run['head']
for w in run['writers']:
wcsv.writerow(
[
i,
h.get('mode', ''),
h.get('profile', ''),
h.get('link', ''),
h.get('isolate', ''),
w.get('writer', ''),
*(w.get(c, '') for c in WRITER_COLS),
]
)
print(f'CSV written to {csv_path}')
return 0
if __name__ == '__main__':
sys.exit(main())