Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions .github/workflows/build-and-measure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ jobs:
build:
name: build (latrec=${{ matrix.latrec }})
runs-on: ubuntu-latest
# Cold, this is asn1c-from-source + libe3 + jbpf and its third-party tree +
# the controller. Comfortably inside an hour; nowhere near the 6h default.
# Cold, this is asn1c-from-source + libe3 + ocudu's jbpf and its third-party
# tree + the controller. Comfortably inside an hour; nowhere near the 6h default.
timeout-minutes: 60
strategy:
fail-fast: false
Expand All @@ -32,11 +32,25 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
# build.sh fetches the submodules itself and then runs jbpf's
# init_and_patch_submodules.sh over them. Letting the checkout action
# do it first would hand that patch script a tree it did not lay out.
# build.sh fetches the libe3 submodule itself.
submodules: false

# jbpf is not vendored here: the controller is built against the gNB's own
# jbpf, ocudu's external/jbpf, so the two ends of the IPC are always the
# same jbpf. CI takes it from the public ocudu mirror. Only that submodule
# is fetched; the rest of ocudu is not needed to build the controller.
- name: Check out ocudu (for its jbpf and hook contract)
uses: actions/checkout@v4
with:
repository: wineslab/ocudu-e3
ref: e3
path: .deps/ocudu-e3
fetch-depth: 1
submodules: false

- name: Populate ocudu's jbpf
run: git -C .deps/ocudu-e3 submodule update --init --recursive --depth 1 external/jbpf

# The libe3 pin decides which asn1c revision is wanted, so it belongs in
# the cache key. hashFiles cannot read a gitlink, and the submodule is not
# checked out yet, so read the pinned SHA straight out of the tree object.
Expand Down Expand Up @@ -69,6 +83,7 @@ jobs:
# which breaks both ccache reuse across heterogeneous runners and any
# comparison of the measurements below between runs.
E3C_CMAKE_ARGS: -DUSE_NATIVE=OFF
OCUDU_DIR: ${{ github.workspace }}/.deps/ocudu-e3
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends ccache
Expand All @@ -89,6 +104,7 @@ jobs:
echo "nproc: $(nproc)"
echo "/dev/shm: $(df -h /dev/shm | tail -1)"
echo "libe3 pin: $(git -C libe3 rev-parse --short HEAD) ($(cat libe3/VERSION))"
echo "ocudu-e3: $(git -C .deps/ocudu-e3 rev-parse --short HEAD), jbpf pin: $(git -C .deps/ocudu-e3 rev-parse --short HEAD:external/jbpf)"
echo '```'
} > report.md

Expand Down
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[submodule "jbpf"]
path = jbpf
url = https://github.com/microsoft/jbpf.git
[submodule "libe3"]
path = libe3
url = https://github.com/wineslab/libe3.git
141 changes: 108 additions & 33 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,64 @@ file(MAKE_DIRECTORY ${OUTPUT_DIR})
file(MAKE_DIRECTORY ${OUTPUT_DIR}/inc)
file(MAKE_DIRECTORY ${OUTPUT_DIR}/lib)

# ---- Build jbpf (full build) ----
# ---- ocudu checkout ----
# The gNB this controller talks to is built from an ocudu checkout, and two things
# come from it: the jbpf tree (below) and the hook context contract the codelets
# and the verifier read (include/ocudu/janus). Clone it next to this repository,
# or point OCUDU_DIR at it.
set(OCUDU_DIR "${CMAKE_SOURCE_DIR}/../ocudu-e3"
CACHE PATH "Path to the ocudu checkout the gNB is built from")
get_filename_component(OCUDU_DIR "${OCUDU_DIR}" ABSOLUTE)

# ---- jbpf: the gNB's own tree ----
# The controller is the jbpf IPC primary and the gNB the secondary. Both map one
# shared region and exchange one channel and message format, so they must be
# built from the SAME jbpf. Instead of pinning a second copy here and keeping the
# two pins, and their patches, in step by hand, build against ocudu's
# external/jbpf: the tree the gNB is built from. Its patches are ocudu's too
# (ocudu apply_jbpf_patches.sh; build.sh runs it).
#
# Pass -DJBPF_DIR only to build against a different jbpf on purpose.
if(NOT JBPF_DIR)
set(JBPF_DIR "${OCUDU_DIR}/external/jbpf")
endif()
get_filename_component(JBPF_DIR "${JBPF_DIR}" ABSOLUTE)

if(NOT EXISTS "${JBPF_DIR}/CMakeLists.txt")
message(FATAL_ERROR
"jbpf not found at JBPF_DIR='${JBPF_DIR}'.\n"
"The controller builds against the gNB's jbpf (ocudu's external/jbpf). "
"Clone ocudu next to this repository, or pass -DOCUDU_DIR=/path/to/ocudu, "
"then populate and patch it:\n"
" git -C <ocudu> submodule update --init --recursive external/jbpf\n"
" <ocudu>/apply_jbpf_patches.sh")
endif()

# Disable tests and examples — we only need the libraries
set(BUILD_TESTING OFF CACHE BOOL "Disable jbpf tests" FORCE)

# Stop jbpf from re-initialising its own submodules.
#
# jbpf's CMakeLists.txt runs init_and_patch_submodules.sh when INITIALIZE_SUBMODULES
# is ON, which is its default. That script `rm -rf`s and re-clones jbpf/3p/*, so on
# any tree where git cannot resolve the submodules — a tarball, an `oc cp`/rsync
# copy into a container, a git worktree — it DESTROYS the working 3p checkout and
# then fails:
#
# fatal: not a git repository
# cp: cannot create regular file '3p/ebpf-verifier/': Not a directory
#
# and the tree is left with ebpf-verifier and ubpf gone, so a plain re-configure
# cannot recover. build.sh already populates the submodules itself
# (jbpf/init_and_patch_submodules.sh), so there is nothing to gain from letting
# jbpf redo it. ocudu forces the same value for the same reason
# is ON, which is its default. That script `rm -rf`s and re-clones 3p/*, so on any
# tree where git cannot resolve the submodules — a tarball, an `oc cp`/rsync copy
# into a container — it DESTROYS the working 3p checkout and then fails. The tree
# is ocudu's, populated and patched on ocudu's side, so there is nothing to gain
# from letting jbpf redo it. ocudu forces the same value for the same reason
# (ocudu CMakeLists.txt, the ENABLE_JBPF block).
#
# FORCE because it is a cached option in jbpf; without FORCE a stale ON from an
# earlier configure would survive.
set(INITIALIZE_SUBMODULES OFF CACHE BOOL "Let jbpf init its own submodules" FORCE)

# Fail early and legibly if the submodules are genuinely absent, rather than
# letting the first missing header surface hundreds of lines into the build.
# Build jbpf with the options ocudu builds it with (ocudu CMakeLists.txt, the
# ENABLE_JBPF block), so both ends of the IPC are the same jbpf in every respect,
# not just the same commit.
set(JBPF_STATIC ON CACHE BOOL "Build jbpf as static library" FORCE)
set(JBPF_EXPERIMENTAL_FEATURES ON CACHE BOOL "Enable jbpf experimental features" FORCE)

# Fail early and legibly if the 3p submodules are absent, rather than letting the
# first missing header surface hundreds of lines into the build.
#
# A per-module sentinel FILE, one that module actually ships.
#
Expand All @@ -67,26 +98,73 @@ set(_jbpf_3p_checks
"mimalloc/CMakeLists.txt"
"ck/configure")
foreach(_jbpf_3p_file IN LISTS _jbpf_3p_checks)
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/jbpf/3p/${_jbpf_3p_file}")
if(NOT EXISTS "${JBPF_DIR}/3p/${_jbpf_3p_file}")
message(FATAL_ERROR
"jbpf/3p/${_jbpf_3p_file} is missing, so jbpf's third-party submodules are "
"not populated. jbpf's own auto-init is disabled here (see above, it would "
"destroy them), so populate them yourself:\n"
" git submodule update --init --recursive # then\n"
" ./jbpf/init_and_patch_submodules.sh\n"
"or, for a copied tree with no git metadata, copy jbpf/ wholesale from a "
"working checkout.")
"${JBPF_DIR}/3p/${_jbpf_3p_file} is missing, so jbpf's third-party "
"submodules are not populated. jbpf's own auto-init is disabled here (see "
"above, it would destroy them), so populate them on the ocudu side:\n"
" git -C ${OCUDU_DIR} submodule update --init --recursive external/jbpf\n"
" ${OCUDU_DIR}/apply_jbpf_patches.sh")
endif()
endforeach()
unset(_jbpf_3p_checks)

# Say which jbpf this is, and whether it is the one ocudu pins. A jbpf checked
# out at another commit than ocudu's gitlink would build a gNB and a controller
# that disagree, so warn rather than let that surface as an IPC failure.
execute_process(COMMAND git -C "${JBPF_DIR}" rev-parse HEAD
OUTPUT_VARIABLE _jbpf_rev OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET RESULT_VARIABLE _jbpf_rev_rc)
execute_process(COMMAND git -C "${OCUDU_DIR}" rev-parse HEAD:external/jbpf
OUTPUT_VARIABLE _jbpf_pin OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET RESULT_VARIABLE _jbpf_pin_rc)
if(NOT _jbpf_rev_rc EQUAL 0)
set(_jbpf_rev "unknown (not a git checkout)")
endif()
if(_jbpf_rev_rc EQUAL 0 AND _jbpf_pin_rc EQUAL 0 AND NOT _jbpf_rev STREQUAL _jbpf_pin)
message(WARNING
"jbpf at ${JBPF_DIR} is at ${_jbpf_rev}, but ocudu pins external/jbpf at "
"${_jbpf_pin}. The gNB and the controller must be built from the same jbpf:\n"
" git -C ${OCUDU_DIR} submodule update --init --recursive external/jbpf")
endif()

# The gNB stamps gnb_ts_ns with CLOCK_MONOTONIC, and the codelet's stamps come from
# jbpf_time_get_ns(), which is only monotonic with ocudu's
# jbpf_patches/jbpf_monotonic_time.patch applied. The controller does not run
# codelets, so this build does not need it, but a gNB built from this same tree
# would produce stage intervals that mix two clocks. Upstream jbpf calls
# clock_gettime exactly once in that file, with CLOCK_REALTIME.
file(STRINGS "${JBPF_DIR}/src/core/jbpf_helper_impl.c" _jbpf_mono
REGEX "clock_gettime\\(CLOCK_MONOTONIC")
if(_jbpf_mono)
set(_jbpf_clock "monotonic patch applied")
else()
set(_jbpf_clock "monotonic patch NOT applied")
message(WARNING
"jbpf at ${JBPF_DIR} lacks jbpf_monotonic_time.patch, so a gNB built from it "
"stamps codelet_ts_ns with CLOCK_REALTIME while gnb_ts_ns is CLOCK_MONOTONIC. "
"Apply it on the ocudu side: ${OCUDU_DIR}/apply_jbpf_patches.sh")
endif()
message(STATUS "jbpf: ${JBPF_DIR} @ ${_jbpf_rev} (${_jbpf_clock})")
unset(_jbpf_rev)
unset(_jbpf_pin)
unset(_jbpf_mono)
unset(_jbpf_clock)

# Override add_test and set_tests_properties so jbpf subdir doesn't register tests
macro(add_test)
endmacro()
macro(set_tests_properties)
endmacro()

add_subdirectory(jbpf)
# JBPF_DIR is outside this source tree, so add_subdirectory needs an explicit
# binary directory. jbpf writes its libraries and copied headers under
# ${CMAKE_SOURCE_DIR}/out (this project's out/). Two things still land in ocudu's
# tree, as they do in ocudu's own build: libck, which jbpf builds in-source in
# 3p/ck, and src/common/jbpf_version.h, which it regenerates on every build (build
# parameters as strings only). Do not build ocudu and the controller at the same
# time from one checkout.
add_subdirectory("${JBPF_DIR}" "${CMAKE_BINARY_DIR}/jbpf")

# Restore original cmake commands
unset(add_test)
Expand All @@ -100,10 +178,7 @@ unset(set_tests_properties)
#
# It needs ocudu's janus headers, which define the hook context structs — the
# verifier derives its ctx descriptors from them with offsetof() so the
# contract has a single source of truth. Point OCUDU_DIR elsewhere if your
# checkout is not a sibling of this one.
set(OCUDU_DIR "${CMAKE_SOURCE_DIR}/../ocudu-e3"
CACHE PATH "Path to the ocudu-e3 checkout (hook context contract)")
# contract has a single source of truth. They come from OCUDU_DIR (above).
set(OCUDU_JANUS_INC "${OCUDU_DIR}/include/ocudu/janus")
add_subdirectory(codelets/verifier)

Expand Down Expand Up @@ -208,12 +283,12 @@ target_include_directories(e3_controller PRIVATE
${CMAKE_BINARY_DIR}/generated # libe3_stamp.h
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/jbpf/src/io
${CMAKE_SOURCE_DIR}/jbpf/src/mem_mgmt
${CMAKE_SOURCE_DIR}/jbpf/src/logger
${CMAKE_SOURCE_DIR}/jbpf/src/common
${JBPF_DIR}/src/io
${JBPF_DIR}/src/mem_mgmt
${JBPF_DIR}/src/logger
${JBPF_DIR}/src/common
${OUTPUT_DIR}/inc
${CMAKE_SOURCE_DIR}/jbpf/src/lcm
${JBPF_DIR}/src/lcm
)

# Link against jbpf_io (transitively brings mem_mgmt, logger, ck, mimalloc)
Expand Down
51 changes: 43 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ thread pinning — lives in a single YAML configuration file; see [Usage](#usage

## Build

Everything goes through the top-level [`build.sh`](build.sh). Two modes:
Everything goes through the top-level [`build.sh`](build.sh). The controller is
built against the gNB's own jbpf, so clone [ocudu-e3](https://github.com/wineslab/ocudu-e3)
next to it first (see [jbpf comes from ocudu](#jbpf-comes-from-ocudu)). Two modes:

```bash
git clone --branch e3 https://github.com/wineslab/ocudu-e3.git
git -C ocudu-e3 submodule update --init --recursive external/jbpf

git clone --recurse-submodules git@github.com:wineslab/E3Controller.git
cd E3Controller

Expand Down Expand Up @@ -56,16 +61,16 @@ packages by hand and re-run `./build.sh` without `--install-deps`.

Whether or not `--install-deps` was used, `build.sh` then runs:

1. `git submodule update --init --recursive` (fetches libe3 and jbpf at their
pinned commits).
2. `jbpf/init_and_patch_submodules.sh` for jbpf's third-party dependencies, then
`apply_jbpf_patches.sh` for the patches to jbpf's own core.
1. `git submodule update --init --recursive` (fetches libe3 at its pinned tag).
2. Checks that ocudu's `external/jbpf` and its third-party submodules are
populated, then runs ocudu's `apply_jbpf_patches.sh` over them. That script
is idempotent, so on a tree the gNB build already patched it does nothing.
3. Configure + build libe3 with both encoders
(`-DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=ON -DLIBE3_BUILD_EXAMPLES=OFF
-DLIBE3_BUILD_TESTS=OFF`), then `sudo cmake --install libe3/build` to
`/usr/local`.
4. Configure + build the E3Controller and the recording-cost bench; jbpf is
compiled in-tree via `add_subdirectory`.
4. Configure + build the E3Controller and the recording-cost bench; ocudu's
jbpf is compiled into this build tree via `add_subdirectory`.

`e3.encoding` in the config is therefore a pure runtime choice, because libe3 is built
with both encoders.
Expand All @@ -90,6 +95,12 @@ behaviour of an installed public header.

### Overrides & re-runs

- `OCUDU_DIR=/path/to/ocudu ./build.sh` — the ocudu checkout to take jbpf and
the hook contract from (defaults to `../ocudu-e3`; a relative path is taken
from this repository's root).
- `LIBE3_PREFIX=$HOME/.local ./build.sh` — install libe3 there instead of
`/usr/local`, without `sudo`, and build the controller against it. For hosts
where you are not root.
- `JOBS=N ./build.sh` — parallelism (defaults to `nproc`).
- `E3C_CMAKE_ARGS='-DUSE_NATIVE=OFF' ./build.sh` — extra configure flags for
the controller. CI passes exactly this: `-march=native` is right on a
Expand All @@ -100,6 +111,30 @@ behaviour of an installed public header.
put `asn1c` under `/opt/asn1c/`, the script re-adds `/opt/asn1c/bin` to
`PATH` automatically before invoking `cmake`.

### jbpf comes from ocudu

The controller is the jbpf IPC primary and the gNB the secondary. Both map one
shared region and exchange one channel and message format, so they must be built
from the same jbpf. This repository therefore has no jbpf of its own: it builds
against `${OCUDU_DIR}/external/jbpf`, the tree the gNB is built from, with the
options ocudu uses (`JBPF_STATIC=ON`, `JBPF_EXPERIMENTAL_FEATURES=ON`). The
patches to that tree belong to ocudu too (`jbpf_patches/`, applied by its
`apply_jbpf_patches.sh`).

Configure reports which jbpf it found, and warns when:

- that jbpf is at a different commit than the one ocudu pins, or
- it lacks `jbpf_monotonic_time.patch`. The controller does not need the patch
itself, but a gNB built from the same tree would stamp `codelet_ts_ns` with
`CLOCK_REALTIME` while `gnb_ts_ns` is `CLOCK_MONOTONIC`.

jbpf builds `libck` inside its own source tree (`3p/ck`), as it does in ocudu's
build, so do not build ocudu and the controller at the same time from one
checkout. Run one after the other.

The codelet Makefiles follow the same rule: `JBPF_DIR` defaults to
`$(OCUDU_DIR)/external/jbpf`.

### ASN.1 Code Generation

The build automatically:
Expand Down Expand Up @@ -308,7 +343,7 @@ nothing yet copied, and `codelet_ts_ns` just before the codelet submits. So the
controller replays them rather than the RAN keeping a ring of its own.

That works because the whole chain reads `CLOCK_MONOTONIC` — the gNB hook,
`jbpf_time_get_ns()` (see `jbpf_patches/jbpf_monotonic_time.patch`) and the
`jbpf_time_get_ns()` (see ocudu's `jbpf_patches/jbpf_monotonic_time.patch`) and the
dispatcher poll — which is latrec's own clock. No domain conversion, no offset
arithmetic, no rate skew to absorb. **All of those sites have to agree**; if one
drifts back to `CLOCK_REALTIME` the stage intervals silently mix epochs.
Expand Down
Loading
Loading