mirror of
https://gitlab.eurecom.fr/oai/openairinterface5g.git
synced 2026-07-13 04:30:28 +00:00
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:
@@ -2184,3 +2184,4 @@ add_subdirectory(openair1)
|
||||
add_subdirectory(openair2)
|
||||
add_subdirectory(openair3)
|
||||
add_subdirectory(radio)
|
||||
add_subdirectory(tests)
|
||||
|
||||
@@ -130,6 +130,8 @@ The other SDRs (AW2S, LimeSDR, ...) have no READMEs.
|
||||
# Testing
|
||||
|
||||
- [UnitTests.md](./UnitTests.md) explains the unit testing setup
|
||||
- Component tests are under `tests/`. Currently, there is a simple CU-UP
|
||||
tester, see the corresponding [README.md](../tests/nr-cuup/README.md).
|
||||
- [TESTBenches.md](./TESTBenches.md) lists the CI setup and links to pipelines
|
||||
|
||||
# Developer tools
|
||||
|
||||
1
tests/CMakeLists.txt
Normal file
1
tests/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
||||
add_subdirectory(nr-cuup)
|
||||
4
tests/nr-cuup/CMakeLists.txt
Normal file
4
tests/nr-cuup/CMakeLists.txt
Normal 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
78
tests/nr-cuup/README.md
Normal 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.
|
||||
58
tests/nr-cuup/load-test.conf
Normal file
58
tests/nr-cuup/load-test.conf
Normal 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";
|
||||
};
|
||||
673
tests/nr-cuup/nr-cuup-load-test.c
Normal file
673
tests/nr-cuup/nr-cuup-load-test.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user