diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5495236a6d..cc4eca91ed 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,3 +3,4 @@ add_subdirectory(nr-cuup) add_subdirectory(nr-cu-nrppa) add_subdirectory(nr-ue-nas-simulator) +add_subdirectory(nrdlbench) diff --git a/tests/nrdlbench/CMakeLists.txt b/tests/nrdlbench/CMakeLists.txt new file mode 100644 index 0000000000..d5be264921 --- /dev/null +++ b/tests/nrdlbench/CMakeLists.txt @@ -0,0 +1,23 @@ +# nr_dlbench: standalone gNB DL-scheduler + PHY-TX micro-benchmark. +# Lives under tests/ (not openair1/SIMULATION/NR_PHY) because it drives the full +# MAC scheduler path, not just the PHY. +add_executable(nr_dlbench + dlbench.c + ${PHY_INTERFACE_DIR}/queue_t.c + ) +target_link_libraries(nr_dlbench PRIVATE + -Wl,--start-group UTIL SIMU PHY_NR_COMMON PHY_NR SCHED_NR_LIB MAC_NR_COMMON CONFIG_LIB L2_NR -Wl,--end-group + m pthread ${T_LIB} ITTI dl physim_common + softmodem_common + ) +target_link_libraries(nr_dlbench PRIVATE asn1_nr_rrc_hdrs asn1_lte_rrc_hdrs) + +if(ENABLE_TESTS) + add_dependencies(tests nr_dlbench) + # Functional smoke tests via a wrapper script (room to grow richer checks): + # run a short benchmark in UM and AM (-A) modes and verify it completes. + add_test(NAME nr_dlbench_um_functional_test + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/nr-dlbench-functional-test.sh $ -A 0) + add_test(NAME nr_dlbench_am_functional_test + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/nr-dlbench-functional-test.sh $ -A 50) +endif() diff --git a/tests/nrdlbench/dlbench.c b/tests/nrdlbench/dlbench.c new file mode 100644 index 0000000000..289a627de4 --- /dev/null +++ b/tests/nrdlbench/dlbench.c @@ -0,0 +1,682 @@ +/* + * SPDX-License-Identifier: LicenseRef-CSSL-1.0 + * + * nr_dlbench: Standalone DL scheduler + PHY TX benchmark + * + * Measures wall-clock time of the full DL pipeline + * (scheduler → LDPC encode → modulate → OFDM) as a function of the number + * of UEs, using the real MAC scheduler and real PHY TX chain. + * + * Usage: + * nr_dlbench -n -u -R -m + * -e -P (print perf) -L -X + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "common/utils/assertions.h" +#include "common/utils/nr/nr_common.h" +#include "executables/nr-uesoftmodem.h" +#include "executables/softmodem-common.h" +#include "LAYER2/NR_MAC_gNB/gNB_scheduler_dlsch_default_policies.h" +#include "LAYER2/NR_MAC_gNB/mac_proto.h" +#include "LAYER2/NR_MAC_gNB/mac_rrc_dl_handler.h" +#include "LAYER2/NR_MAC_gNB/nr_mac_gNB.h" +#include "LAYER2/NR_MAC_gNB/nr_radio_config.h" +#include "NR_CellGroupConfig.h" +#include "NR_MAC_COMMON/nr_mac.h" +#include "NR_MAC_COMMON/nr_mac_common.h" +#include "NR_PHY_INTERFACE/NR_IF_Module.h" +#include "NR_ReconfigurationWithSync.h" +#include "NR_RLC-BearerConfig.h" +#include "PHY/CODING/nrLDPC_coding/nrLDPC_coding_interface.h" +#include "PHY/INIT/nr_phy_init.h" +#include "PHY/MODULATION/modulation_common.h" +#include "PHY/NR_TRANSPORT/nr_dlsch.h" +#include "PHY/NR_TRANSPORT/nr_transport_common_proto.h" +#include "PHY/TOOLS/tools_defs.h" +#include "PHY/defs_gNB.h" +#include "PHY/defs_nr_common.h" +#include "PHY/impl_defs_nr.h" +#include "SCHED_NR/sched_nr.h" +#include "common/utils/oai_asn1.h" +#include "common/config/config_load_configmodule.h" +#include "common/ngran_types.h" +#include "common/ran_context.h" +#include "common/utils/T/T.h" +#include "e1ap_messages_types.h" +#include "nfapi_nr_interface_scf.h" +#include "openair1/SIMULATION/NR_PHY/nr_unitary_defs.h" +#include "openair1/SIMULATION/TOOLS/sim.h" +#include "openair2/LAYER2/nr_rlc/nr_rlc_oai_api.h" +#include "thread-pool.h" +#include "time_meas.h" + +/* ── Globals required by OAI framework ── */ +PHY_VARS_gNB *gNB; +RAN_CONTEXT_t RC; +int64_t uplink_frequency_offset[MAX_NUM_CCs][4]; +double cpuf; +char *uecap_file; +uint64_t downlink_frequency[MAX_NUM_CCs][4]; +THREAD_STRUCT thread_struct; +nfapi_ue_release_request_body_t release_rntis; +instance_t DUuniqInstance = 0; +instance_t CUuniqInstance = 0; +unsigned int NTN_UE_Koffset = 0; +configmodule_interface_t *uniqCfg = NULL; + +/* PHY_vars_UE_g referenced by probe.c (LTE UTIL lib) */ +void *PHY_vars_UE_g = NULL; + +/* Stubs for symbols pulled in by the linker but not needed */ +void signal_rrc_msg(void) { abort(); } +void signal_rrc_state_changed_to(void) { abort(); } +void signal_ue_id(void) { abort(); } + +/* E1AP stubs (pulled in by cucp_cuup_direct.c in libe1_if) */ +void e1_bearer_context_setup(const e1ap_bearer_setup_req_t *req) { abort(); } +void e1_bearer_context_modif(const e1ap_bearer_mod_req_t *req) { abort(); } +struct e1ap_bearer_release_cmd_s; +void e1_bearer_release_cmd(const struct e1ap_bearer_release_cmd_s *cmd) { abort(); } + +/* UE params stub (referenced transitively by UE-side libs) */ +nrUE_params_t nrUE_params; +nrUE_params_t *get_nrUE_params(void) { return &nrUE_params; } + +/* ── Timing helpers ── */ +static inline double timespec_diff_us(struct timespec *start, struct timespec *end) +{ + return (end->tv_sec - start->tv_sec) * 1e6 + (end->tv_nsec - start->tv_nsec) / 1e3; +} + +static int bench_target_mcs = 28; +static int bench_target_layers = 1; +/* AM mode: depth of the gNB's AM RX reorder window (out-of-order PDUs buffered + * behind a missing head SN). 0 = UM mode (DL-buffer benchmark, the default). */ +static int bench_am_backlog = 0; + +/* nr_mac_add_test_ue maps DRB 1 to logical channel ID 4. */ +static const int bench_drb_id = 1; +static const int bench_drb_lcid = 4; + +/* qsort comparator: ascending order of doubles (used for percentile sorting). */ +static int cmp_double(const void *a, const void *b) +{ + double da = *(const double *)a, db = *(const double *)b; + return (da > db) - (da < db); +} + +/* Print one row of the latency breakdown table. + * Shows: total time across all slots, per-trial average, max, and trial count. */ +static void print_meas_row(const char *name, time_stats_t *ts, int n_slots) +{ + double cpu_ghz = get_cpu_freq_GHz(); + if (ts->trials == 0) { + printf(" %-34s - - - 0\n", name); + return; + } + double total_us = (double)ts->diff / cpu_ghz / 1000.0; + double per_slot_us = total_us / n_slots; + double avg_us = total_us / ts->trials; + double max_us = (double)ts->max / cpu_ghz / 1000.0; + printf(" %-34s %7.1f %7.1f %7.1f %5d\n", name, per_slot_us, avg_us, max_us, ts->trials); +} + +/* No-op UL preprocessor: we only benchmark DL */ +static void bench_ul_preprocessor_noop(gNB_MAC_INST *mac, post_process_pusch_t *pp_pusch) +{ + (void)mac; + (void)pp_pusch; +} + +/* Push SDU data into each UE's DRB 1 (UM) RLC entity so the real scheduler + * sees loaded buffers via nr_mac_rlc_status_ind(). */ +static void refill_rlc_buffers(gNB_MAC_INST *mac, int bytes_per_ue) +{ + UE_iterator(mac->UE_info.connected_ue_list, UE) { + uint8_t *sdu = malloc(bytes_per_ue); + AssertFatal(sdu, "malloc failed\n"); + memset(sdu, 0xAB, bytes_per_ue); + protocol_ctxt_t ctxt = {.rntiMaybeUEid = UE->rnti, .enb_flag = 1, .module_id = 0}; + nr_rlc_data_req(&ctxt, 0 /* srb_flag: 0 = DRB */, bench_drb_id, 0, bytes_per_ue, sdu); + /* nr_rlc_data_req frees sdu */ + } +} + +void nr_rlc_ms_tick(void); /* defined in nr_rlc_oai_api.c; advances the RLC clock */ + +/* AM mode: populate the gNB's AM RX window for (rnti, lcid) with `depth` + * received-but-incomplete PDUs, so the per-slot nr_mac_rlc_status_ind() -> + * status_size() walk has real work — modelling a lossy UL AM bearer + * mid-retransmission, which is exactly what the RLC ind fastpath optimises. + * + * We hand-build AMD PDUs (18-bit SN, matching the DRB-AM config) carrying a + * single middle segment each: si=0b11 with SO>0 means the PDU is neither the + * first nor last segment of its SDU, so the SDU can never be reassembled and + * thus is never delivered (no F1AP/PDCP path is hit). With SN 0..depth-1 all + * present-but-incomplete, rx_next stays at 0 while rx_next_highest reaches + * depth; once t_reassembly expires rx_highest_status advances to depth and the + * status PDU must NACK every one of the `depth` SNs — a deep, stable walk on + * every nr_mac_rlc_status_ind() the scheduler issues. */ +static void inject_am_rx_backlog(uint16_t rnti, logical_chan_id_t lcid, int depth) +{ + for (int sn = 0; sn < depth; sn++) { + uint8_t pdu[16]; + const int so = 8; /* middle segment starts at byte 8 -> head [0,8) missing */ + /* 18-bit SN AMD PDU header: dc(1)|p(1)|si(2)|R(2)|sn(18) then so(16) */ + pdu[0] = 0x80 /*D/C=1*/ | (0x3 << 4) /*si=middle*/ | ((sn >> 16) & 0x3); + pdu[1] = (sn >> 8) & 0xff; + pdu[2] = sn & 0xff; + pdu[3] = (so >> 8) & 0xff; + pdu[4] = so & 0xff; + memset(&pdu[5], 0xCD, 8); /* 8 bytes of segment data */ + nr_rlc_data_ind_t d = {.ch = lcid, .len = 13, .buf = pdu}; + nr_mac_rlc_data_ind(0, rnti, 1 /*gnb_flag*/, &d, 1); + } + /* Advance the RLC clock well past t_reassembly so the next status_ind moves + * rx_highest_status up to cover the whole backlog (otherwise the walk is + * trivial). 200 ticks (ms) dwarfs any realistic t_reassembly. */ + for (int t = 0; t < 200; t++) + nr_rlc_ms_tick(); +} + +/* Re-arm the AM status report for (rnti, lcid). The status_triggered flag is + * cleared once the scheduler pulls the status PDU, so without this the walk + * fires only once. We resend SN 0's (already-received) segment with the poll + * bit set: it is discarded as a duplicate but the poll still sets + * status_triggered (38.322 5.2.3.2), exactly as an ongoing lossy UL bearer + * polling the receiver would. Cheap, and does not modify the RX window. */ +static void retrigger_am_status(uint16_t rnti, logical_chan_id_t lcid) +{ + const int so = 8; + uint8_t pdu[16]; + pdu[0] = 0x80 /*D/C=1*/ | 0x40 /*P=1*/ | (0x3 << 4) /*si=middle*/; /* sn=0 hi bits */ + pdu[1] = 0; + pdu[2] = 0; + pdu[3] = (so >> 8) & 0xff; + pdu[4] = so & 0xff; + memset(&pdu[5], 0xCD, 8); + nr_rlc_data_ind_t d = {.ch = lcid, .len = 13, .buf = pdu}; + nr_mac_rlc_data_ind(0, rnti, 1 /*gnb_flag*/, &d, 1); +} + +/* ── Main ── */ +int main(int argc, char **argv) +{ + stop = false; + struct sigaction oldaction; + sigaction(SIGINT, &sigint_action, &oldaction); + + setbuf(stdout, NULL); + cpuf = get_cpu_freq_GHz(); + + /* defaults */ + int N_RB_DL = 106; + int mu = 1; + int n_slots = 100; + int num_ues = 1; + int loglvl = OAILOG_WARNING; + char gNBthreads[128] = "n"; + char *csv_filename = NULL; + int print_perf = 0; + double target_bler = 0.0; + + if ((uniqCfg = load_configmodule(argc, argv, CONFIG_ENABLECMDLINEONLY)) == 0) { + printf("Error: configuration module init failed\n"); + return 1; + } + + int c; + /* keep options in alphabetical order */ + while ((c = getopt(argc, argv, "--:O:A:B:e:L:m:n:PR:u:X:Z:h")) != -1) { + if (c == 1 || c == '-' || c == 'O') + continue; + switch (c) { + case 'A': + bench_am_backlog = atoi(optarg); + break; + case 'B': + target_bler = atof(optarg); + break; + case 'e': + bench_target_mcs = atoi(optarg); + break; + case 'L': + loglvl = atoi(optarg); + break; + case 'm': + mu = atoi(optarg); + break; + case 'n': + n_slots = atoi(optarg); + break; + case 'P': + print_perf = 1; + cpu_meas_enabled = 1; + break; + case 'R': + N_RB_DL = atoi(optarg); + break; + case 'u': + num_ues = atoi(optarg); + break; + case 'X': + strncpy(gNBthreads, optarg, sizeof(gNBthreads) - 1); + break; + case 'Z': + csv_filename = strdup(optarg); + break; + case 'h': + default: + printf("nr_dlbench - DL scheduler + PHY TX benchmark\n\n"); + printf(" -A AM mode: gNB AM RX reorder-window depth (default 0=UM).\n" + " Exercises the per-slot status_ind/status_size RX walk.\n"); + printf(" -B Simulated DL BLER (default 0.0 = all ACK)\n"); + printf(" -e Max MCS index cap (default 28, scheduler adapts)\n"); + printf(" -L Log level 0-5 (default 1=warning)\n"); + printf(" -m Numerology index (default 1 = 30kHz)\n"); + printf(" -n Number of DL slots to benchmark (default 100)\n"); + printf(" -P Print PHY performance counters\n"); + printf(" -R Number of DL resource blocks (default 106)\n"); + printf(" -u Number of UEs (default 1, max %d)\n", MAX_MOBILES_PER_GNB); + printf(" -X gNB thread pool config (default 'n' = no threads)\n"); + printf(" -Z Output CSV file for per-slot timing\n"); + printf(" -h This help\n"); + return 0; + } + } + + if (num_ues > MAX_MOBILES_PER_GNB) { + printf("Error: num_ues %d exceeds MAX_MOBILES_PER_GNB (%d). " + "Raise the limit in common/openairinterface5g_limits.h and rebuild.\n", + num_ues, MAX_MOBILES_PER_GNB); + return 1; + } + + logInit(); + set_glog(loglvl); + InitSinLUT(); + randominit(); /* seeds OAI's uniformrandom(); set OAI_RNGSEED to reproduce a run */ + + printf("=== nr_dlbench: %d UEs, %d RBs, mu=%d, MCS=%d, %d slots, BLER=%.2f ===\n", + num_ues, N_RB_DL, mu, bench_target_mcs, n_slots, target_bler); + + /* ── 1. Allocate PHY ── */ + get_softmodem_params()->phy_test = 1; + get_softmodem_params()->do_ra = 0; + + RC.gNB = calloc(1, sizeof(PHY_VARS_gNB *)); + RC.gNB[0] = calloc(1, sizeof(PHY_VARS_gNB)); + gNB = RC.gNB[0]; + gNB->ofdm_offset_divisor = UINT_MAX; + gNB->phase_comp = true; + + NR_DL_FRAME_PARMS *frame_parms = &gNB->frame_parms; + frame_parms->nb_antennas_tx = 1; + frame_parms->nb_antennas_rx = 1; + frame_parms->N_RB_DL = N_RB_DL; + frame_parms->N_RB_UL = N_RB_DL; + + AssertFatal((gNB->if_inst = NR_IF_Module_init(0)) != NULL, "Cannot register interface"); + gNB->if_inst->NR_PHY_config_req = nr_phy_config_request; + + /* ── 2. Create ServingCellConfigCommon ── */ + NR_ServingCellConfigCommon_t *scc = calloc(1, sizeof(*scc)); + prepare_scc(scc); + uint64_t ssb_bitmap = 1; + fill_scc_sim(scc, &ssb_bitmap, N_RB_DL, N_RB_DL, mu, mu); + fix_scc(scc, ssb_bitmap); + + frame_structure_t frame_structure = {0}; + frame_type_t frame_type = TDD; + config_frame_structure(mu, + scc->tdd_UL_DL_ConfigurationCommon, + get_tdd_period_idx(scc->tdd_UL_DL_ConfigurationCommon), + frame_type, + &frame_structure); + + /* ── 3. Init MAC ── */ + nr_pdsch_AntennaPorts_t pdsch_AntennaPorts = {.N1 = 1, .N2 = 1, .XP = 1}; + const nr_mac_config_t conf = { + .pdsch_AntennaPorts = pdsch_AntennaPorts, + .pusch_AntennaPorts = 1, + .minRXTXTIME = 6, + .do_CSIRS = 0, + .do_SRS = 0, + .num_dlharq = 16, + .num_ulharq = 16, + .maxMIMO_layers = bench_target_layers, + .force_256qam_off = false, + .timer_config.sr_ProhibitTimer = 0, + .timer_config.sr_TransMax = 64, + .timer_config.sr_ProhibitTimer_v1700 = 0, + .timer_config.t300 = 400, + .timer_config.t301 = 400, + .timer_config.t310 = 2000, + .timer_config.n310 = 10, + .timer_config.t311 = 3000, + .timer_config.n311 = 1, + .timer_config.t319 = 400, + .num_agg_level_candidates = {0, 4, 4, 2, 0}, + }; + const nr_rlc_configuration_t rlc_config = { + .srb = {.t_poll_retransmit = 45, .t_reassembly = 35, .t_status_prohibit = 0, + .poll_pdu = -1, .poll_byte = -1, .max_retx_threshold = 8, .sn_field_length = 12}, + /* t_status_prohibit = 0 (ms0): AM mode (-A) reports status every slot so the + * status_size RX walk is exercised on every scheduler pass (worst case the + * RLC ind fastpath targets). Only affects the AM DRB used by -A. */ + .drb_am = {.t_poll_retransmit = 45, .t_reassembly = 15, .t_status_prohibit = 0, + .poll_pdu = 64, .poll_byte = 1024 * 500, .max_retx_threshold = 32, .sn_field_length = 18}, + .drb_um = {.t_reassembly = 15, .sn_field_length = 12}, + }; + + RC.nb_nr_macrlc_inst = 1; + mac_top_init_gNB(ngran_gNB, scc, &conf, &rlc_config); + gNB_MAC_INST *gNB_mac = RC.nrmac[0]; + gNB_mac->beam_info = (NR_beam_info_t){.beams_per_period = 1}; + nr_mac_config_scc(gNB_mac, scc, &conf); + + gNB_mac->dl_bler.harq_round_max = 4; + gNB_mac->ul_bler.harq_round_max = 4; + gNB->frame_parms.nb_antennas_tx = pdsch_AntennaPorts.N1 * pdsch_AntennaPorts.N2 * pdsch_AntennaPorts.XP; + + /* ── 4. Use the real DL scheduler instead of the phytest one ── */ + gNB_mac->pre_processor_dl = nr_dlsch_preprocessor; + gNB_mac->pre_processor_ul = bench_ul_preprocessor_noop; + gNB_mac->dl_ri_pmi_select = nr_dl_ri_pmi_select_default; + gNB_mac->dl_mcs_select = nr_dl_mcs_select_default; + gNB_mac->dl_beam_select = nr_dl_beam_select_default; + gNB_mac->dl_tda_select = nr_dl_tda_select_default; + gNB_mac->dl_rb_alloc = nr_dl_proportional_fair; + gNB_mac->dl_lcid_alloc = nr_dl_lcid_alloc_default; + + /* ── 5. Init PHY ── */ + phy_init_nr_gNB(gNB); + N_RB_DL = gNB->frame_parms.N_RB_DL; + + /* ── 6. Create N test UEs ── */ + printf("Creating %d test UEs...\n", num_ues); + for (int u = 0; u < num_ues; u++) { + rnti_t rnti = 0x1234 + u; + NR_CellGroupConfig_t *cg = get_default_secondaryCellGroup(scc, NULL, 0, 1, &conf, u, 0); + cg->spCellConfig->reconfigurationWithSync = get_reconfiguration_with_sync(rnti, u, scc, 0); + + /* Add DRB 1 to CellGroup so nr_mac_add_test_ue registers LCID 4. Default is + * RLC UM (no real UE to ACK an AM TX). In AM mode (-A) we use AM and inject + * a gappy RX backlog below to exercise the status_size walk. */ + NR_RLC_Config_PR drb_mode = + bench_am_backlog > 0 ? NR_RLC_Config_PR_am : NR_RLC_Config_PR_um_Bi_Directional; + int priority = 13; + NR_RLC_BearerConfig_t *drb1_rlc_cfg = get_DRB_RLC_BearerConfig( + bench_drb_lcid, bench_drb_id, drb_mode, priority, &rlc_config); + cg->rlc_BearerToAddModList = calloc(1, sizeof(*cg->rlc_BearerToAddModList)); + asn1cSeqAdd(&cg->rlc_BearerToAddModList->list, drb1_rlc_cfg); + + bool ok = nr_mac_add_test_ue(gNB_mac, rnti, cg); + AssertFatal(ok, "Failed to add test UE %d (rnti=%04x)\n", u, rnti); + + NR_UE_info_t *UE_info = gNB_mac->UE_info.connected_ue_list[u]; + AssertFatal(UE_info != NULL, "UE %d not found in connected list\n", u); + configure_UE_BWP(gNB_mac, scc, UE_info, false, NR_SearchSpace__searchSpaceType_PR_ue_Specific, -1, -1); + + /* Create DRB 1 RLC entity (nr_mac_add_test_ue registers the LCID + * but does not create the RLC entity). */ + nr_rlc_add_drb(rnti, bench_drb_id, drb1_rlc_cfg); + + /* AM mode: seed a persistent out-of-order RX window so update_dlsch_buffer's + * per-slot status_ind/status_size has to walk it. */ + if (bench_am_backlog > 0) + inject_am_rx_backlog(rnti, bench_drb_lcid, bench_am_backlog); + + /* Set UE channel quality so the scheduler picks a high MCS */ + NR_UE_sched_ctrl_t *sc = &UE_info->UE_sched_ctrl; + sc->dl_max_mcs = bench_target_mcs; + + printf(" UE %3d: rnti=%04x, BWP size=%d\n", u, rnti, UE_info->current_DL_BWP.BWPSize); + } + + /* ── 7. Init thread pool ── */ + initNamedTpool(gNBthreads, &gNB->threadPool, true, "gNB-tpool"); + initNotifiedFIFO(&gNB->L1_tx_out); + + /* ── 8. Allocate sched response ── */ + NR_Sched_Rsp_t *sched_rsp = calloc(1, sizeof(*sched_rsp)); + AssertFatal(sched_rsp, "alloc failed\n"); + + /* ── 9. Open CSV output ── */ + FILE *csv = NULL; + if (csv_filename) { + csv = fopen(csv_filename, "w"); + if (!csv) { + printf("Warning: cannot open %s for writing\n", csv_filename); + } else { + fprintf(csv, "slot,frame,num_ues,n_pdsch,sched_us,phy_tx_us,total_us\n"); + } + } + + /* ── 10. Identify DL slots in the TDD pattern ── */ + int slots_per_period = (int)frame_structure.numb_slots_period; + int slots_per_frame = 10 * (1 << mu); + int n_dl_per_period = 0; + for (int s = 0; s < slots_per_period; s++) { + if (is_dl_slot(s, &frame_structure)) + n_dl_per_period++; + } + printf("TDD period has %d DL slots out of %d total\n", n_dl_per_period, slots_per_period); + if (n_dl_per_period == 0) { + printf("Error: no DL slots found in TDD pattern!\n"); + return 1; + } + + /* ── 11. Benchmark loop ── */ + printf("\nRunning until %d DL slots measured...\n\n", n_slots); + + double *t_sched = calloc(n_slots, sizeof(double)); + double *t_phy = calloc(n_slots, sizeof(double)); + double *t_total = calloc(n_slots, sizeof(double)); + int *n_pdsch_arr = calloc(n_slots, sizeof(int)); + + struct timespec ts0, ts1, ts2; + + int frame = 0; + int slot = 0; + int dl_iter = 0; /* counts DL slots measured */ + + while (dl_iter < n_slots && !stop) { + bool is_dl = is_dl_slot(slot, &frame_structure); + + /* ── Pre-slot: inject synthetic UE state (every slot) ── */ + + /* 1) Simulate HARQ feedback: ACK or NACK based on target_bler */ + UE_iterator(gNB_mac->UE_info.connected_ue_list, UE) { + NR_UE_sched_ctrl_t *sc = &UE->UE_sched_ctrl; + while (sc->feedback_dl_harq.head >= 0) { + int pid = sc->feedback_dl_harq.head; + remove_front_nr_list(&sc->feedback_dl_harq); + bool nack = (target_bler > 0.0) && (uniformrandom() < target_bler); + if (nack && sc->harq_processes[pid].round < gNB_mac->dl_bler.harq_round_max - 1) { + /* NACK: bump round, send to retransmission */ + sc->harq_processes[pid].round++; + sc->harq_processes[pid].is_waiting = false; + add_tail_nr_list(&sc->retrans_dl_harq, pid); + } else { + /* ACK (or max rounds reached): recycle */ + sc->harq_processes[pid].round = 0; + sc->harq_processes[pid].ndi ^= 1; + sc->harq_processes[pid].is_waiting = false; + add_tail_nr_list(&sc->available_dl_harq, pid); + } + } + } + + /* 2) Push data into RLC before DL slots so the scheduler sees loaded buffers. + * In AM mode the loaded buffer is the persistent RX backlog (status PDU), + * so we skip the UM DL injection and isolate the status_size walk. */ + if (is_dl && bench_am_backlog == 0) + refill_rlc_buffers(gNB_mac, 10000); + + /* AM mode: advance the RLC clock one slot and re-arm the status report on + * each UE's bearer so the scheduler's status_ind walks the gappy RX window + * every slot (models an ongoing lossy UL AM bearer). Outside the timed + * region — only the scheduler's resulting status_size walk is measured. */ + if (bench_am_backlog > 0) { + nr_rlc_ms_tick(); + UE_iterator(gNB_mac->UE_info.connected_ue_list, UE) + retrigger_am_status(UE->rnti, bench_drb_lcid); + } + + /* Call the scheduler for EVERY slot (DL, UL, special) so that VRB maps, + * PUCCH/UCI state, CSI, and SR processing advance correctly. */ + reset_sched_response(sched_rsp, frame, slot, 0, 0); + + clock_gettime(CLOCK_MONOTONIC, &ts0); + gNB_dlsch_ulsch_scheduler(0, frame, slot, sched_rsp); + clock_gettime(CLOCK_MONOTONIC, &ts1); + + /* Only measure and run PHY TX for DL slots */ + if (is_dl) { + int n_pdsch = 0; + for (int p = 0; p < sched_rsp->DL_req.dl_tti_request_body.nPDUs; p++) { + if (sched_rsp->DL_req.dl_tti_request_body.dl_tti_pdu_list[p].PDUType == NFAPI_NR_DL_TTI_PDSCH_PDU_TYPE) + n_pdsch++; + } + + phy_procedures_gNB_TX(gNB, &sched_rsp->DL_req, &sched_rsp->TX_req, &sched_rsp->UL_dci_req, frame, slot); + + clock_gettime(CLOCK_MONOTONIC, &ts2); + + t_sched[dl_iter] = timespec_diff_us(&ts0, &ts1); + t_phy[dl_iter] = timespec_diff_us(&ts1, &ts2); + t_total[dl_iter] = timespec_diff_us(&ts0, &ts2); + n_pdsch_arr[dl_iter] = n_pdsch; + + if (csv) { + fprintf(csv, "%d,%d,%d,%d,%.1f,%.1f,%.1f\n", + slot, frame, num_ues, n_pdsch, t_sched[dl_iter], t_phy[dl_iter], t_total[dl_iter]); + } + + if (dl_iter < 5 || (dl_iter % 20 == 0)) { + printf(" slot %3d [%d.%d]: %d PDSCH | sched %7.1f us | phy_tx %7.1f us | total %7.1f us\n", + dl_iter, frame, slot, n_pdsch, t_sched[dl_iter], t_phy[dl_iter], t_total[dl_iter]); + } + + dl_iter++; + } + + /* advance to next slot */ + slot++; + if (slot >= slots_per_frame) { + slot = 0; + frame = (frame + 1) % 1024; + } + } + + /* ── 12. Statistics ── */ + + /* skip first 5 slots as warmup */ + int warmup = n_slots > 10 ? 5 : 0; + int count = n_slots - warmup; + int total_pdsch = 0; + for (int i = warmup; i < n_slots; i++) + total_pdsch += n_pdsch_arr[i]; + + /* sort copies for percentile computation */ + double *s_sched = malloc(count * sizeof(double)); + double *s_phy = malloc(count * sizeof(double)); + double *s_total = malloc(count * sizeof(double)); + memcpy(s_sched, t_sched + warmup, count * sizeof(double)); + memcpy(s_phy, t_phy + warmup, count * sizeof(double)); + memcpy(s_total, t_total + warmup, count * sizeof(double)); + + qsort(s_sched, count, sizeof(double), cmp_double); + qsort(s_phy, count, sizeof(double), cmp_double); + qsort(s_total, count, sizeof(double), cmp_double); + + /* slot duration: 1000us for mu=0, 500us for mu=1, 250us for mu=2 */ + double slot_dur_us = 1000.0 / (1 << mu); + + double mean_sched = 0, mean_phy = 0, mean_total = 0; + for (int i = 0; i < count; i++) { + mean_sched += s_sched[i]; + mean_phy += s_phy[i]; + mean_total += s_total[i]; + } + mean_sched /= count; + mean_phy /= count; + mean_total /= count; + +#define P(arr, pct) (arr[(int)((pct) / 100.0 * (count - 1))]) + + printf("\n=== Results: %d UEs, %d RBs, MCS %d, %d slots (warmup %d) ===\n", + num_ues, N_RB_DL, bench_target_mcs, n_slots, warmup); + printf(" Avg PDSCH/slot: %.1f / %d UEs\n", (double)total_pdsch / count, num_ues); + printf(" Slot budget: %.0f us (mu=%d)\n\n", slot_dur_us, mu); + + printf(" %-12s %8s %8s %8s %8s %8s\n", "Phase", "mean", "p50", "p90", "p99", "max"); + printf(" %-12s %8s %8s %8s %8s %8s\n", "", "(us)", "(us)", "(us)", "(us)", "(us)"); + printf(" ------------------------------------------------------------------\n"); + printf(" %-12s %8.1f %8.1f %8.1f %8.1f %8.1f\n", "Scheduler", mean_sched, P(s_sched, 50), P(s_sched, 90), P(s_sched, 99), s_sched[count - 1]); + printf(" %-12s %8.1f %8.1f %8.1f %8.1f %8.1f\n", "PHY TX", mean_phy, P(s_phy, 50), P(s_phy, 90), P(s_phy, 99), s_phy[count - 1]); + printf(" %-12s %8.1f %8.1f %8.1f %8.1f %8.1f\n", "Total", mean_total, P(s_total, 50), P(s_total, 90), P(s_total, 99), s_total[count - 1]); + printf(" ------------------------------------------------------------------\n"); + +#undef P + + if (mean_total > slot_dur_us) + printf("\n WARNING: mean total (%.0f us) exceeds slot budget (%.0f us)!\n", + mean_total, slot_dur_us); + if (s_total[count - 1] > slot_dur_us) + printf(" WARNING: max total (%.0f us) exceeds slot budget (%.0f us)!\n", + s_total[count - 1], slot_dur_us); + + free(s_sched); + free(s_phy); + free(s_total); + + /* ── 13. Print detailed latency breakdown ── */ + if (print_perf) { + printf("\n %-34s %7s %7s %7s %5s\n", "Breakdown", "/slot", "/call", "max", "calls"); + printf(" %-34s %7s %7s %7s %5s\n", "", "(us)", "(us)", "(us)", ""); + printf(" ---------------------------------------------------------------------------\n"); + + printf(" Scheduler:\n"); + print_meas_row(" Total", &gNB_mac->gNB_scheduler, count); + print_meas_row(" RA scheduling", &gNB_mac->schedule_ra, count); + print_meas_row(" UL scheduling", &gNB_mac->schedule_ulsch, count); + print_meas_row(" DL scheduling (PDCCH+PDSCH)", &gNB_mac->schedule_dlsch, count); + print_meas_row(" RLC data req", &gNB_mac->rlc_data_req, count); + + printf(" PHY TX:\n"); + print_meas_row(" Total", &gNB->phy_proc_tx, count); + print_meas_row(" DCI generation", &gNB->dci_generation_stats, count); + print_meas_row(" DLSCH encoding", &gNB->dlsch_encoding_stats, count); + print_meas_row(" segmentation", &gNB->dlsch_segmentation_stats, count); + print_meas_row(" rate matching", &gNB->dlsch_rate_matching_stats, count); + print_meas_row(" scrambling", &gNB->dlsch_scrambling_stats, count); + print_meas_row(" DLSCH modulation", &gNB->dlsch_modulation_stats, count); + print_meas_row(" layer mapping", &gNB->dlsch_layer_mapping_stats, count); + print_meas_row(" precoding", &gNB->dlsch_precoding_stats, count); + print_meas_row(" resource mapping", &gNB->dlsch_resource_mapping_stats, count); + print_meas_row(" phase compensation", &gNB->phase_comp_stats, count); + printf(" ---------------------------------------------------------------------------\n"); + } + + /* ── Cleanup ── */ + if (csv) fclose(csv); + free(t_sched); + free(t_phy); + free(t_total); + free(n_pdsch_arr); + free(sched_rsp); + free(csv_filename); + + printf("\nDone.\n"); + return 0; +} diff --git a/tests/nrdlbench/nr-dlbench-functional-test.sh b/tests/nrdlbench/nr-dlbench-functional-test.sh new file mode 100755 index 0000000000..77e065ede4 --- /dev/null +++ b/tests/nrdlbench/nr-dlbench-functional-test.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# SPDX-License-Identifier: LicenseRef-CSSL-1.0 + +# usage: nr-dlbench-functional-test.sh [extra nr_dlbench args] + +if [ $# -lt 1 ]; then + echo "usage: $0 [extra nr_dlbench args]" + exit 1 +fi + +BIN=$1 +shift + +# Small, fast config: 1 UE, 24 RBs (10 MHz, the minimum that fits a CORESET), +# 200 slots. Caller-supplied args (e.g. -A for AM mode) are appended. +set -x +OUT=$("${BIN}" -u 1 -R 24 -n 200 -L 0 "$@" 2>&1) +RET=$? +set +x + +echo "${OUT}" + +if [ ${RET} -ne 0 ]; then + echo "FAIL: nr_dlbench exited with code ${RET}" + exit 1 +fi +if ! echo "${OUT}" | grep -q "=== Results"; then + echo "FAIL: no results summary in nr_dlbench output" + exit 1 +fi +echo "PASS" +exit 0