Add CU-UP load tester and documentation

This is a simple CU-UP load tester. It sets up a configurable number of UEs in the
CU-UP, then sends GTP traffic in downlink/uplink in the N3-F1/F1-N3
direction at the CU-UP. It then compares the sent and received data.

Additional parameters, such as amount of traffic, test length, packet
size, and IP addresses/ports are configurable.

To run:

    mkdir build && cd build && cmake .. -GNinja && ninja nr-cuup nr-cuup-load-test params_libconfig
    ./tests/nr-cuup/nr-cuup-load-test
    ./nr-cuup -O ../tests/nr-cuup/load-test.conf

The tester should be able to detect lost and reordered packets.

Example lost: Receive order 2,3,5,6 (lost packet 4)
=> on packet 2, expected 2, next=expected+1=3
=> on packet 3, expected 3, next=expected+1=4
=> on packet 5, expected 4, diff=5-4=1 (lost 1), next=payload+1=6
=> on packet 6, expected 6
=> 1 lost

Example reordered: Receiver order 2,5,3,6 (lost packet 4)
=> on packet 2, expected 2, next=expected+1=3
=> on packet 5, expected 3, diff=5-3=2 (lost 2), next=payload+1=6
=> on packet 3, expected 6, (reduce lost by 1 to 1), next=expected=6
=> on packet 6, expected 6
=> 1 lost

Example reordered: Receiver order 2,5,3,4,6 (no losses)
This commit is contained in:
Robert Schmidt
2025-01-20 14:49:21 +01:00
parent 44933f18c6
commit 07629e29d6
7 changed files with 817 additions and 0 deletions

1
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1 @@
add_subdirectory(nr-cuup)

View File

@@ -0,0 +1,4 @@
add_executable(nr-cuup-load-test nr-cuup-load-test.c)
target_link_libraries(nr-cuup-load-test PRIVATE GTPV1U ${T_LIB} utils ITTI e1ap
SCTP_CLIENT)
target_link_libraries(nr-cuup-load-test PRIVATE asn1_lte_rrc_hdrs asn1_nr_rrc_hdrs) # ITTI

78
tests/nr-cuup/README.md Normal file
View File

@@ -0,0 +1,78 @@
This is a simple tester for the CU-UP. It configures the CU-UP via E1, and can
stream data via GTP in uplink and downlink directions.
[[_TOC_]]
# Overview
From a schematic point of view, the tester/CU-UP interaction looks like this:
```
+-------+
+------>| CU-UP |<------+
| +-------+ |
| | |
| F1-U | E1 | NG-U
| (data) | (control) | (data)
| | |
| +-------+ |
+------>|tester |<------+
+-------+
```
The tester is for performance testing of a CU-UP, and behaves like an
integrated DU/CU-CP/UPF from the CU-UP's point of view.
The tester and CU-UP have an E1 connection through which control information is
exchanged, e.g., configuration of UEs/bearers and associated information. In
other words, towards the CU-UP, the tester appears like a CU-CP.
Further, for each UE/bearer, CU-UP is configured with GTP IP information and
the corresponding TEID. The CU-UP will forward downlink data arriving on NG-U
on the F1-U interface, and uplink data arriving on F1-U on the NG-U interface.
In other words, on the F1-U interface, the tester appears like a DU to the
CU-UP, and on the NG-U interface, the tester appears like a UPF to the CU-UP.
A test scenario is fixed to these steps:
1. The tester sets up a number of UEs via the E1 interface (it is possible to
see the message exchange in Wireshark).
2. The tester streams data in downlink/uplink (from the CU-UP's point of view)
and measures how much data is lost.
3. The tester releases all the UEs via the E1 interface, and disconnects from
the CU-UP.
Note: The tester uses the same GTP module as the CU-UP. Thus, data might not
only be lost at the GTP interface of the CU-UP, but also at the tester.
# Usage
You can build the CU-UP and the tester like so:
cd ~/openairinterface5g
mkdir build && cd build && cmake .. -GNinja && ninja nr-cuup nr-cuup-load-test params_libconfig
./tests/nr-cuup/nr-cuup-load-test
./nr-cuup -O ../tests/nr-cuup/load-test.conf
This builds both tester and CU-UP in the directory `build/`, then starts the
load tester with default values
- 1 UE
- 10s of Test
- 60 Mbps of traffic in both downlik/uplink with a packet size of 1400 bytes
To see the available options, run
./tests/nr-cuup/nr-cuup-load-test -h
The configuration file [`load-test.conf`](./load-test.conf) matches the default
tester configuration (F1-U GTP traffic over non-standard port 2153, NG-U GTP
over 2152, tests on localhost). The configuration file includes this
non-standard port 2153 as the GTP module, as of now, cannot bind on the same
interface and port for both F1-U and NG-U due to internal limitations.
# Limitations
- The tester does not yet create/remove UEs during a traffic test.
- It might be possible to integrate with an external GTP traffic generator,
but we did not test this.

View File

@@ -0,0 +1,58 @@
Active_gNBs = ( "gNB-OAI");
# Asn1_verbosity, choice in: none, info, annoying
Asn1_verbosity = "none";
gNBs =
(
{
////////// Identification parameters:
gNB_ID = 0xe00;
gNB_CU_UP_ID = 0xe00;
# cell_type = "CELL_MACRO_GNB";
gNB_name = "gNB-OAI";
// Tracking area code, 0x0000 and 0xfffe are reserved values
tracking_area_code = 1;
plmn_list = ({ mcc = 222; mnc = 01; mnc_length = 2; snssaiList = ({ sst = 1, sd = 0xffffff }) });
tr_s_preference = "f1";
local_s_address = "127.0.0.6";
remote_s_address = "127.0.0.1";
local_s_portc = 501;
local_s_portd = 2153;
remote_s_portc = 500;
remote_s_portd = 2153;
# ------- SCTP definitions
SCTP :
{
# Number of streams to use in input/output
SCTP_INSTREAMS = 2;
SCTP_OUTSTREAMS = 2;
};
E1_INTERFACE =
(
{
type = "up";
ipv4_cucp = "127.0.0.1";
ipv4_cuup = "127.0.0.6";
}
)
NETWORK_INTERFACES :
{
GNB_IPV4_ADDRESS_FOR_NG_AMF = "172.21.16.109";
GNB_IPV4_ADDRESS_FOR_NGU = "127.0.0.7";
GNB_PORT_FOR_S1U = 2152; # Spec 2152
};
}
);
log_config: {
global_log_level = "info";
};

View File

@@ -0,0 +1,673 @@
/*
* Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The OpenAirInterface Software Alliance licenses this file to You under
* the OAI Public License, Version 1.1 (the "License"); you may not use this file
* except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.openairinterface.org/?page_id=698
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*-------------------------------------------------------------------------------
* For more information about the OpenAirInterface (OAI) Software Alliance:
* contact@openairinterface.org
*/
#include <stdio.h>
#include <arpa/inet.h>
#include <time.h>
#include "common/utils/LOG/log.h"
#include "common/utils/ocp_itti/intertask_interface.h" // this pulls in all of OAI+4G+5G
#include "openair3/SCTP/sctp_eNB_task.h"
#include "openair2/E1AP/e1ap.h"
#include "openair3/ocp-gtpu/gtp_itf.h"
configmodule_interface_t *uniqCfg;
#define DL 0
#define UL 1
int nr_rlc_get_available_tx_space(const rnti_t rntiP, const logical_chan_id_t channel_idP) { abort(); return 0; } /* in GTP */
softmodem_params_t *get_softmodem_params(void) { static softmodem_params_t p = {0}; return &p; }; /* in ITTI */
void e1_bearer_context_setup(const e1ap_bearer_setup_req_t *req) { abort(); } /* CU-UP */
void e1_bearer_context_modif(const e1ap_bearer_mod_req_t *req) { abort(); } /* CU-UP */
void e1_bearer_release_cmd(const e1ap_bearer_release_cmd_t *cmd) { abort(); } /* CU-UP */
void e1_reset(void) { abort(); } /* CU-UP */
instance_t *N3GTPUInst = NULL; /* CU-UP */
instance_t CUuniqInstance=0; /* CU-UP */
void exit_function(const char *file, const char *function, const int line, const char *s, const int assert)
{
LOG_E(GNB_APP, "error at %s:%d:%s: %s\n", file, line, function, s);
abort();
}
/* @brief Dummy function to pass a fptr to ITTI for staring RRC queue, without
* actually handling any messages. Functions called by main() will send/receive
* ITTI messages to E1, as if it was RRC. */
static void *cuup_tester_rrc(void *)
{
itti_mark_task_ready(TASK_RRC_GNB);
LOG_I(GNB_APP, "created RRC queue\n");
return NULL;
}
/* @brief Initialize E1 interface (as if it was CU-CP) */
static void init_e1()
{
net_ip_address_t ip = {.ipv4 = 1, .ipv4_address = "127.0.0.1" };
e1ap_net_config_t conf = { .CUCP_e1_ip_address = ip };
MessageDef *msg = itti_alloc_new_message(TASK_GNB_APP, 0, E1AP_REGISTER_REQ);
e1ap_net_config_t *e1ap_nc = &E1AP_REGISTER_REQ(msg).net_config;
*e1ap_nc = conf;
itti_send_msg_to_task(TASK_CUCP_E1, 0, msg);
}
/* @brief Setup CU-UP connection and retrieve parameters for further
* connection.
*
* Waits for E1 Setup Request, sends response, and returns SCTP association ID
* and NSSAI used by this CU-UP.
*/
static void setup_cuup(sctp_assoc_t *assoc_id, e1ap_nssai_t *nssai)
{
/* Wait for E1AP setup request, store CU-UP info */
MessageDef *itti_req = NULL;
itti_receive_msg(TASK_RRC_GNB, &itti_req);
MessagesIds id = ITTI_MSG_ID(itti_req);
AssertFatal(id == E1AP_SETUP_REQ, "expected E1AP setup request, received %s instead\n", itti_get_message_name(id));
*assoc_id = itti_req->ittiMsgHeader.originInstance;
e1ap_setup_req_t *req = &E1AP_SETUP_REQ(itti_req);
long t_id = req->transac_id;
DevAssert(req->supported_plmns = 1);
DevAssert(req->plmn[0].slice != NULL);
*nssai = *req->plmn[0].slice;
DevAssert(assoc_id != 0);
itti_free(TASK_GNB_APP, itti_req);
// acknowledge the E1 setup request with a response
MessageDef *itti_resp = itti_alloc_new_message(TASK_GNB_APP, 0, E1AP_SETUP_RESP);
itti_resp->ittiMsgHeader.originInstance = *assoc_id;
e1ap_setup_resp_t *resp = &E1AP_SETUP_RESP(itti_resp);
resp->transac_id = t_id;
itti_send_msg_to_task(TASK_CUCP_E1, 0, itti_resp);
}
/* @brief get E1AP bearer setup request based on parameters */
static e1ap_bearer_setup_req_t get_breq(uint32_t ue_id, long pdu_id, long drb_id, const e1ap_nssai_t *nssai, const UP_TL_information_t *tnl)
{
e1ap_bearer_setup_req_t bearer_req = {
.gNB_cu_cp_ue_id = ue_id,
.secInfo.cipheringAlgorithm = 0, /* no ciphering/NEA0 */
.secInfo.integrityProtectionAlgorithm = 0, /* no integrity/NIA0 */
/* encryptionKey not needed */
/* integrityProtectionKey not needed */
.numPDUSessions = 1,
};
pdu_session_to_setup_t *pdu = &bearer_req.pduSession[0];
pdu->sessionId = pdu_id;
pdu->nssai = *nssai;
pdu->securityIndication.integrityProtectionIndication = SECURITY_NOT_NEEDED;
pdu->securityIndication.confidentialityProtectionIndication = SECURITY_NOT_NEEDED;
pdu->UP_TL_information = *tnl;
pdu->numDRB2Setup = 1;
DRB_nGRAN_to_setup_t *drb = &pdu->DRBnGRanList[0];
drb->id = drb_id;
drb->numQosFlow2Setup = 1;
drb->numCellGroups = 1;
return bearer_req;
}
/* @brief check for packet loss by verifing the sequence number. Can handle
* packet losses and reordering.
* @param payload the sequence number of the current packet
* @param expected the expected sequence number
* @param [inout] pointer to number of lost packets, which will be updated by
* the difference between payload and expected.
* @return the next expected packet number. */
static uint32_t estimate_packetloss(const uint32_t payload, uint32_t expected, uint32_t *lost)
{
if (payload == expected) {
/* all good */
return expected + 1;
} else if (payload > expected) {
/* there was packet loss, assume all packets in between have been lost */
uint32_t diff = payload - expected;
*lost += diff;
LOG_W(GNB_APP, "packet loss: expected %d received %d diff %d (total loss %d)\n", expected, payload, diff, *lost);
return payload + 1;
} else { /* payload < expected */
/* reordered: we assumed current packet to be lost, so reduce the
* loss for this packet as it arrived in the end. */
*lost -= 1;
LOG_W(GNB_APP, "packet reordering occurred: expected %d received %d\n", expected, payload);
return expected; /* we still wait for the next packet */
}
}
static struct ue_stat {
uint32_t count;
uint32_t lost;
uint64_t received;
} ue_stat[10][2] = {0};
/* @brief ulReceived callback for F1-U
*
* Checks for packet loss by verifying that the first 4 bytes are corresponding
* to a continuous 4-byte (word) enumeration. */
static bool recv_ng(protocol_ctxt_t *ctxt,
const ue_id_t ue_id,
const srb_flag_t flag,
const rb_id_t rb,
const mui_t mui,
const confirm_t confirm,
const sdu_size_t size,
unsigned char *const buf,
const pdcp_transmission_mode_t modeP,
const uint32_t *sourceL2Id,
const uint32_t *destinationL2Id,
const uint8_t qfi,
const bool rqi,
const int pdusession_id)
{
uint32_t *payload = (uint32_t *) buf;
DevAssert(size % 4 == 0);
struct ue_stat *s = &ue_stat[ue_id][UL];
s->count = estimate_packetloss(*payload, s->count, &s->lost);
s->received += size;
return true;
}
/* @brief Set up the NG-U (north-bound) GTP tunnel, and inform CU-UP via E1 */
static up_params_t setup_cuup_ue_ng(sctp_assoc_t assoc_id, const e1ap_nssai_t *nssai, uint32_t ue_id, instance_t gtp_inst, const char *ip, uint16_t port)
{
long pdu_id = 1;
long drb_id = pdu_id + 3;
in_addr_t addr_lo;
inet_pton(AF_INET, ip, &addr_lo);
/* create the local tunnel (i.e., as if we were UPF) */
transport_layer_addr_t null_addr = {.length = 32};
teid_t teid_lo = newGtpuCreateTunnel(gtp_inst, ue_id, pdu_id, pdu_id, -1, -1, null_addr, port, NULL, recv_ng);
UP_TL_information_t tnl = {.teId = teid_lo};
memcpy(&tnl.tlAddress, &addr_lo, 4);
/* Create a new bearer */
MessageDef *itti_breq = itti_alloc_new_message(TASK_GNB_APP, 0, E1AP_BEARER_CONTEXT_SETUP_REQ);
itti_breq->ittiMsgHeader.originInstance = assoc_id;
e1ap_bearer_setup_req_t *breq = &E1AP_BEARER_CONTEXT_SETUP_REQ(itti_breq);
*breq = get_breq(ue_id, pdu_id, drb_id, nssai, &tnl);
itti_send_msg_to_task(TASK_CUCP_E1, 0, itti_breq);
/* Wait for the ack */
MessageDef *itti_bresp;
itti_receive_msg(TASK_RRC_GNB, &itti_bresp);
MessagesIds id = ITTI_MSG_ID(itti_bresp);
AssertFatal(id == E1AP_BEARER_CONTEXT_SETUP_RESP, "expected E1AP bearer context setup response, received %s instead\n", itti_get_message_name(id));
DevAssert(assoc_id == itti_bresp->ittiMsgHeader.originInstance);
e1ap_bearer_setup_resp_t *bresp = &E1AP_BEARER_CONTEXT_SETUP_RESP(itti_bresp);
DevAssert(bresp->gNB_cu_cp_ue_id == ue_id);
DevAssert(bresp->gNB_cu_up_ue_id == ue_id);
DevAssert(bresp->numPDUSessions == 1);
teid_t teid_rm = bresp->pduSession[0].tl_info.teId;
in_addr_t addr_rm;
memcpy(&addr_rm, &bresp->pduSession[0].tl_info.tlAddress, 4);
DevAssert(bresp->pduSession[0].numDRBSetup == 1);
DRB_nGRAN_setup_t *drb = &bresp->pduSession[0].DRBnGRanList[0];
DevAssert(drb->id == drb_id);
DevAssert(drb->numUpParam == 1);
up_params_t f1_up = drb->UpParamList[0];
itti_free(TASK_GNB_APP, itti_bresp);
/* print diagnostics */
char ip_lo[32] = {0};
inet_ntop(AF_INET, &addr_lo, ip_lo, sizeof(ip_lo));
char ip_rm[32] = {0};
inet_ntop(AF_INET, &addr_rm, ip_rm, sizeof(ip_rm));
GtpuUpdateTunnelOutgoingAddressAndTeid(gtp_inst, ue_id, pdu_id, addr_rm, teid_rm);
LOG_I(GNB_APP, "CU-UP created NG-U, local %s/%x remote %s/%x (port %d)\n", ip_lo, teid_lo, ip_rm, teid_rm, port);
inet_ntop(AF_INET, &f1_up.tl_info.tlAddress, ip_rm, sizeof(ip_rm));
LOG_I(GNB_APP, "CU-UP created F1-U, remove %s/%x\n", ip_rm, f1_up.tl_info.teId);
return f1_up;
}
/* @brief get E1AP bearer modification request based on parameters */
static e1ap_bearer_mod_req_t get_bmod(uint32_t ue_id, long pdu_id, up_params_t up)
{
e1ap_bearer_mod_req_t bearer_mod = {
.gNB_cu_cp_ue_id = ue_id,
.gNB_cu_up_ue_id = ue_id,
.numPDUSessionsMod = 1,
};
pdu_session_to_mod_t *pdum = &bearer_mod.pduSessionMod[0];
pdum->sessionId = pdu_id;
pdum->numDRB2Modify = 1;
DRB_nGRAN_to_mod_t *drb = &pdum->DRBnGRanModList[0];
drb->id = 4;
drb->numDlUpParam = 1;
drb->DlUpParamList[0] = up;
return bearer_mod;
}
/* @brief Received callback for F1-U
*
* Checks for packet loss by verifying that the first 4 bytes are corresponding
* to a continuous 4-byte (word) enumeration. */
static bool recv_f1(protocol_ctxt_t *ctxt,
const srb_flag_t flag,
const rb_id_t rb,
const mui_t mui,
const confirm_t confirm,
const sdu_size_t size,
unsigned char *const buf,
const pdcp_transmission_mode_t modeP,
const uint32_t *sourceL2Id,
const uint32_t *destinationL2Id)
{
int skip_bytes = 2;
uint32_t *payload = (uint32_t *) &buf[skip_bytes];
DevAssert((size - skip_bytes) % 4 == 0);
int ue_id = ctxt->rntiMaybeUEid;
struct ue_stat *s = &ue_stat[ue_id][DL];
s->count = estimate_packetloss(*payload, s->count, &s->lost);
s->received += size - skip_bytes;
return true;
}
/* @brief Set up the F1-U (south-bound) GTP tunnel, and inform CU-UP via E1 */
static void setup_cuup_ue_f1(sctp_assoc_t assoc_id, uint32_t ue_id, instance_t gtp_inst, const char *ip, uint16_t port, up_params_t rm)
{
int pdu_id = 1;
int drb_id = pdu_id + 3;
/* create tunnel (i.e., as if we were DU) */
transport_layer_addr_t addr = {.length = 32};
memcpy(addr.buffer, &rm.tl_info.tlAddress, 4);
teid_t teid_lo = newGtpuCreateTunnel(gtp_inst, ue_id, drb_id, drb_id, rm.tl_info.teId, -1, addr, port, recv_f1, NULL);
up_params_t lo = {.tl_info.teId = teid_lo, .cell_group_id = rm.cell_group_id};
inet_pton(AF_INET, ip, &lo.tl_info.tlAddress);
/* modify existing tunnel via E1 to pass in F1-U TEID */
MessageDef *itti_bmod = itti_alloc_new_message(TASK_GNB_APP, 0, E1AP_BEARER_CONTEXT_MODIFICATION_REQ);
itti_bmod->ittiMsgHeader.originInstance = assoc_id;
e1ap_bearer_mod_req_t *bmod = &E1AP_BEARER_CONTEXT_MODIFICATION_REQ(itti_bmod);
*bmod = get_bmod(ue_id, pdu_id, lo);
itti_send_msg_to_task(TASK_CUCP_E1, 0, itti_bmod);
/* Wait for the ack */
MessageDef *itti_bmodr;
itti_receive_msg(TASK_RRC_GNB, &itti_bmodr);
MessagesIds id = ITTI_MSG_ID(itti_bmodr);
AssertFatal(id == E1AP_BEARER_CONTEXT_MODIFICATION_RESP, "expected E1AP bearer context modification response, received %s instead\n", itti_get_message_name(id));
//DevAssert(assoc_id == itti_bmodr->ittiMsgHeader.originInstance);
itti_free(TASK_GNB_APP, itti_bmodr);
/* print diagnostics */
char ip_lo[32] = {0};
inet_ntop(AF_INET, &lo.tl_info.tlAddress, ip_lo, sizeof(ip_lo));
char ip_rm[32] = {0};
inet_ntop(AF_INET, &rm.tl_info.tlAddress, ip_rm, sizeof(ip_rm));
LOG_I(GNB_APP, "CU-UP created F1-U, local %s/%x remote %s/%x (port %d)\n", ip_lo, lo.tl_info.teId, ip_rm, rm.tl_info.teId, port);
}
/* @brief release the UE over E1, and delete corresponding GTP tunnels */
static void e1_release_ue(sctp_assoc_t assoc_id, uint32_t ue_id, instance_t f1_inst, instance_t ng_inst)
{
MessageDef *itti_cmd = itti_alloc_new_message(TASK_GNB_APP, 0, E1AP_BEARER_CONTEXT_RELEASE_CMD);
itti_cmd->ittiMsgHeader.originInstance = assoc_id;
e1ap_bearer_release_cmd_t *cmd = &E1AP_BEARER_CONTEXT_RELEASE_CMD(itti_cmd);
cmd->gNB_cu_cp_ue_id = cmd->gNB_cu_up_ue_id = ue_id;
cmd->cause.type = E1AP_CAUSE_RADIO_NETWORK;
cmd->cause.value = E1AP_RADIO_CAUSE_NORMAL_RELEASE;
itti_send_msg_to_task(TASK_CUCP_E1, 0, itti_cmd);
/* Wait for the ack */
MessageDef *itti_cplt;
itti_receive_msg(TASK_RRC_GNB, &itti_cplt);
MessagesIds id = ITTI_MSG_ID(itti_cplt);
AssertFatal(id == E1AP_BEARER_CONTEXT_RELEASE_CPLT, "expected E1AP bearer context release complete, received %s instead\n", itti_get_message_name(id));
itti_free(TASK_GNB_APP, itti_cplt);
newGtpuDeleteAllTunnels(f1_inst, ue_id);
newGtpuDeleteAllTunnels(ng_inst, ue_id);
}
/* @brief initialize GTP interface for IP:port */
static instance_t init_gtp(const char *ip, uint16_t port)
{
openAddr_t iface = {0};
DevAssert(strlen(ip) <= sizeof(iface.originHost));
strcpy(iface.originHost, ip);
snprintf(iface.originService, sizeof(iface.originService), "%d", port);
snprintf(iface.destinationService, sizeof(iface.destinationService), "%d", port);
return gtpv1Init(iface);
}
/* @brief calculate time difference of two struct timespec */
static float time_diff(struct timespec s, struct timespec e)
{
//printf("start %ld.%ld end %ld.%ld\n", s.tv_sec, s.tv_nsec, e.tv_sec, e.tv_nsec);
float dur = e.tv_sec - s.tv_sec + (e.tv_nsec - s.tv_nsec) / 1000000000.f;
//printf("duration %f s %f us\n", dur, dur * 1000000.f);
return dur * 1000000.f;
}
enum pdcp_len {PDCP_SN_LEN_12, PDCP_SN_LEN_18};
/* @brief generate hardcoded PDCP header in case of UL transmission */
static void write_pdcp_header(uint32_t sn, enum pdcp_len len, uint8_t *buf)
{
DevAssert(len == PDCP_SN_LEN_12);
/* see nr_pdcp_entity_process_sdu() or 38.323 sec 6.2.2.2 */
buf[0] = 0x80 | ((sn >> 8) & 0xf);
buf[1] = sn & 0xff;
//printf("%02x %02x\n", buf[0], buf[1]);
}
/* @brief the configuration for a thread to send GTP traffic */
struct thr_data {
/// pthread ID to join this thread
pthread_t t;
/* in */
/// GTP instance to send on
instance_t inst;
/// UE ID to be used
ue_id_t ue_id;
/// flag to decide if PDCP/SDAP header is to be prepended
bool is_ul;
/// length of data packet
unsigned int data_len;
/// target throughput to reach
uint64_t throughput;
/// duration of the data traffic to last
unsigned int duration;
/* out */
/// exact measured transmission time
float total_time;
/// exact measured data that has been sent
uint64_t total_data;
};
/* @brief Main traffic generation/transmission function.
*
* The function calculates the amount of packets to be sent during each TTI
* (one millisecond) (as a float, to send the exact amount of packets). It then
* enters a loop, in which (1) it sends the packets of the current TTI, possibly
* including additional packets if they could not be sent in the last TTI, (2)
* measure the time since the last TTI boundary, and sleep, if necessary, to
* adjust to the exact throughput (3) update for additional packets to send if
* the last TTI took too long. The last point ensure that we send exactly an
* exact amount of data, possibly taking longer than specified.
*
* @param v pointer to generation function of type `struct thr_data`.
* @return always NULL */
static void *sender_thread(void *v)
{
struct thr_data *d = v;
size_t oh = d->is_ul ? 3 : 0;
size_t packet_len = d->data_len + oh;
uint8_t buf[packet_len];
memset(buf, 0, packet_len);
uint32_t *payload = (uint32_t *)(buf + oh);
uint64_t to_send = (float)d->throughput / 8.0 * d->duration;
const float pps_1ms = (float) d->throughput / 8.0 / d->data_len / 1000;
float pps_1ms_nextwin = pps_1ms;
int bearer = d->is_ul ? 4 : 1;
//printf("sending %ld Mbps for %ld s => data %ld, %.3f pps\n", (float) d->throughput / 1000000.0, d->duration, to_send, pps_1ms);
int i_1ms = 0;
struct timespec start;
clock_gettime(CLOCK_MONOTONIC, &start);
size_t packet_count = 0;
while (true) {
int pps_now = pps_1ms_nextwin;
pps_1ms_nextwin = pps_1ms + pps_1ms_nextwin - (float) pps_now;
//printf("this window send %d packets next window %.3f (pps_1ms %.3f)\n", pps_now, pps_1ms_nextwin, pps_1ms);
for (uint32_t i = 0; i < pps_now; ++i) {
if (d->is_ul)
write_pdcp_header(packet_count, PDCP_SN_LEN_12, buf);
*payload = packet_count++;
gtpv1uSendDirect(d->inst, d->ue_id, bearer, (uint8_t *)buf, packet_len, false, false);
d->total_data += d->data_len;
}
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
float diff_us = time_diff(start, t);
int us_passed = diff_us - i_1ms*1000;
int iter_advance = us_passed / 1000 + 1;
int wait_usec = 1000 - us_passed;
//printf("diff %.3fus i_1ms %d us_passed %d iter %d wait %d (%.3f %%)\n", diff_us, i_1ms, us_passed, iter_advance, wait_usec, 100.0 * d->total_data / to_send);
DevAssert(iter_advance > 0);
if (wait_usec > 0)
usleep(wait_usec);
if (iter_advance > 1) /* we have to send more in the next window */
pps_1ms_nextwin += (iter_advance - 1) * pps_1ms_nextwin;
i_1ms += iter_advance;
if (d->total_data >= to_send)
break;
}
struct timespec end;
clock_gettime(CLOCK_MONOTONIC, &end);
d->total_time = time_diff(start, end) / 1000000.f;
return NULL;
}
/* @brief print statistics of a UE (amount of data, achieved rate, if data was
* lost). */
static bool print_stats(const char *pref, int ue_id, uint64_t total_data, float duration, const struct ue_stat *s)
{
float tthr = 8.0 * total_data / duration / 1000000.f;
float rthr = 8.0 * s->received / duration / 1000000.f;
LOG_I(GNB_APP, "%d %s sent %ld in %.3f s received %ld bytes => rates [Mbps] transmit %.3f receive %.3f\n",
ue_id,
pref,
total_data,
duration,
s->received,
tthr,
rthr);
if (total_data == s->received) {
DevAssert(s->lost == 0);
LOG_A(GNB_APP, "%d %s received all data sent\n", ue_id, pref);
return true;
} else {
uint64_t m = total_data - s->received;
LOG_W(GNB_APP, "%d %s warning: missing %ld bytes (%.3f %%, %d packets)\n", ue_id, pref, m, 100.f * m / total_data, s->lost);
return false;
}
}
/* @brief start a thread if specified through template, and in direction of
* given GTP bearer.
* @param template specifies if thread is to be started (throughput and
* duration are positive). If so, the template data is stored inside the
* thread.
* @param thr place to store thread data
* @param b the GTP bearer to use */
static struct thr_data *start_thread(const struct thr_data *template, struct thr_data *thr, instance_t inst, ue_id_t ue_id)
{
if (template->throughput == 0 || template->duration == 0)
return NULL;
*thr = *template;
thr->inst = inst;
thr->ue_id = ue_id;
pthread_create(&thr->t, NULL, sender_thread, thr);
return thr;
}
/* @brief print progress of a thread specified in thr_data, and prepend DL/UL
* based on dir. data access to thread data is not private, but we only read,
* so don't care. */
static bool print_progress(const int dir, const struct thr_data *obs)
{
if (!obs)
return false;
uint64_t target = (uint64_t) obs->throughput / 8 * obs->duration;
float progress = (float)obs->total_data / target;
printf(" %s sent %3.f %%", dir == DL ? "DL" : "UL", 100.0 * progress);
return progress < 0.99;
}
int main(int argc, char *argv[])
{
if ((uniqCfg = load_configmodule(argc, argv, CONFIG_ENABLECMDLINEONLY)) == NULL) {
exit_fun("[SOFTMODEM] Error, configuration module init failed\n");
}
logInit();
itti_init(TASK_MAX, tasks_info);
unsigned int n_ue;
unsigned int duration_s;
unsigned int packet_size;
uint64_t target_rate_dl;
uint64_t target_rate_ul;
char *ng_ip = malloc_or_fail(100);
uint16_t ng_port;
char *f1_ip = malloc_or_fail(100);
uint16_t f1_port;
paramdef_t param_def[] = {
{"n", "number of UEs\n", 0, .uptr = &n_ue, .defuintval = 1, TYPE_UINT},
{"t", "duration of test (s)\n", 0, .uptr = &duration_s, .defuintval = 10, TYPE_UINT},
{"p", "packet size\n", 0, .uptr = &packet_size, .defuintval = 1400, TYPE_UINT},
{"d", "DL target rate (Mbps, per UE)\n", 0, .u64ptr = &target_rate_dl, .defint64val = 60, TYPE_UINT64},
{"u", "UL target rate (Mbps, per UE)\n", 0, .u64ptr = &target_rate_ul, .defint64val = 60, TYPE_UINT64},
{"ng-ip", "Tester NG-U IP\n", 0, .strptr = &ng_ip, .defstrval = "127.0.0.1", TYPE_STRING, 100},
{"ng-port", "Tester NG-U port\n", 0, .u16ptr = &ng_port, .defuintval = 2152, TYPE_UINT16},
{"f1-ip", "Tester F1-U IP\n", 0, .strptr = &f1_ip, .defstrval = "127.0.0.1", TYPE_STRING, 100},
{"f1-port", "Tester F1-U port\n", 0, .u16ptr = &f1_port, .defuintval = 2153, TYPE_UINT16},
};
config_get(uniqCfg, param_def, sizeofArray(param_def), NULL);
// on command line, target rate is in Mbps, convert to bps
target_rate_dl *= 1000000LL;
target_rate_ul *= 1000000LL;
LOG_I(GNB_APP,
"CU-UP load tester: running test for\n"
"- Number of UEs %d\n"
"- Duration %d seconds\n"
"- Packet size %d\n"
"- Target DL rate %.3f Mbps (per UE)\n"
"- Target UL rate %.3f Mbps (per UE)\n"
"- Tester endpoint for CU-UP NG-U: %s:%d\n"
"- Tester endpoint for CU-UP F1-U: %s:%d\n",
n_ue, duration_s, packet_size,
(float) target_rate_dl / 1e6,
(float) target_rate_ul / 1e6,
ng_ip, ng_port, f1_ip, f1_port);
struct thr_data dl_template = {.is_ul = false, .data_len = packet_size, .throughput = target_rate_dl, .duration = duration_s};
struct thr_data ul_template = {.is_ul = true, .data_len = packet_size, .throughput = target_rate_ul, .duration = duration_s};
/* init GTP for NG-U comm with CU-UP */
instance_t ng_inst = init_gtp(ng_ip, ng_port);
AssertFatal(ng_inst > 0, "could not initialize GTP interface on %s:%d towards CU-UP NG-U\n", ng_ip, ng_port);
/* init GTP for F1-U comm with CU-UP */
instance_t f1_inst = init_gtp(f1_ip, f1_port);
AssertFatal(f1_inst > 0, "could not initialize GTP interface on %s:%d towards CU-UP F1-U\n", f1_ip, f1_port);
/* Start relevant services: SCTP, CUCP_E1: E1; GTP not actively used; RRC to
* register the queue so we can receive what E1 sends us back */
int rc;
rc = itti_create_task(TASK_SCTP, sctp_eNB_task, NULL);
AssertFatal(rc >= 0, "Create task for SCTP failed\n");
rc = itti_create_task(TASK_CUCP_E1, E1AP_CUCP_task, NULL);
AssertFatal(rc >= 0, "Create task for CUUP E1 failed\n");
rc = itti_create_task(TASK_GTPV1_U, gtpv1uTask, NULL);
AssertFatal(rc >= 0, "Create task for GTPV1U failed\n");
rc = itti_create_task(TASK_RRC_GNB, cuup_tester_rrc, NULL);
AssertFatal(rc >= 0, "Create task for RRC failed\n");
init_e1();
/* Set up CU-UP connection: waits for CU-UP-initiated SCTP+E1 setup request */
sctp_assoc_t assoc_id;
e1ap_nssai_t nssai;
setup_cuup(&assoc_id, &nssai);
/* Set up UE contexts in CU-UP. the below two calls correspond to
* - setup_cuup_ue_ng(): E1 bearer context setup <=> completely sets up NG-U
* in CU-UP, gets TEID for F1-U UL
* - setup_cuup_ue_f1(): E1 bearer context modification <=> send TEID for
* F1-U DL (as if we had talked to the DU in the middle) */
for (int ue = 0; ue < n_ue; ++ue) {
up_params_t f1_up = setup_cuup_ue_ng(assoc_id, &nssai, ue, ng_inst, ng_ip, ng_port);
setup_cuup_ue_f1(assoc_id, ue, f1_inst, f1_ip, f1_port, f1_up);
}
usleep(1000);
/* for each UE, start DL&UL data streams in dedicated thread, if configured */
struct thr_data thrd[n_ue][2];
memset(thrd, 0, sizeof(thrd));
struct thr_data *obs_dl = NULL, *obs_ul = NULL;
for (int ue = 0; ue < n_ue; ++ue) {
obs_dl = start_thread(&dl_template, &thrd[ue][DL], ng_inst, ue);
obs_ul = start_thread(&ul_template, &thrd[ue][UL], f1_inst, ue);
}
/* show progress of one thread (all take the same time) */
while (obs_dl || obs_ul) {
bool dlrunning = print_progress(DL, obs_dl);
bool ulrunning = print_progress(UL, obs_ul);
printf("\r");
fflush(stdout);
if (!dlrunning && !ulrunning)
break;
usleep(100000);
}
/* join threads, if applicable */
for (int ue = 0; ue < n_ue; ++ue) {
struct thr_data *dl = &thrd[ue][DL];
if (dl->t)
pthread_join(dl->t, NULL);
struct thr_data *ul = &thrd[ue][UL];
if (ul->t)
pthread_join(ul->t, NULL);
}
/* wait a bit, to ensure all data is received after having sent it above */
sleep(1);
for (int ue = 0; ue < n_ue; ++ue) {
e1_release_ue(assoc_id, ue, f1_inst, ng_inst);
}
/* show stats of all threads */
bool success = true;
for (int ue = 0; ue < n_ue; ++ue) {
struct thr_data *thrddl = &thrd[ue][DL];
struct ue_stat *statdl = &ue_stat[ue][DL];
if (thrddl->t)
if (!print_stats("DL", ue, thrddl->total_data, thrddl->total_time, statdl))
success = false;
struct thr_data *thrdul = &thrd[ue][UL];
struct ue_stat *statul = &ue_stat[ue][UL];
if (thrdul->t)
if (!print_stats("UL", ue, thrdul->total_data, thrdul->total_time, statul))
success = false;
}
if (success)
LOG_A(GNB_APP, "test succeeded\n");
else
LOG_W(GNB_APP, "TEST FAILED\n");
return success ? 0 : 1;
}