diff --git a/Dockerfile.cli b/Dockerfile.cli index 747416d..4f3d24d 100644 --- a/Dockerfile.cli +++ b/Dockerfile.cli @@ -25,7 +25,7 @@ ENV LD_LIBRARY_PATH=/usr/local/lib ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ENV PATH=/usr/local/bin:$PATH ENV E2SARINSTALL=/e2sar-install -ENV E2SAR_VER=0.3.1 +ENV E2SAR_VER=0.3.2 ENV E2SAR_DEB=E2SAR-${E2SAR_VER}-main-ubuntu-24.04/e2sar_${E2SAR_VER}_amd64.deb ENV E2SAR_DEB_URL=https://github.com/JeffersonLab/E2SAR/releases/download/${E2SAR_DEB} ENV ROOT_INSTALL=/rootlib diff --git a/Dockerfile.gluex-reader b/Dockerfile.gluex-reader index 1e2d67e..f17a2b1 100644 --- a/Dockerfile.gluex-reader +++ b/Dockerfile.gluex-reader @@ -8,6 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN pip install --no-cache-dir shmem-reader matplotlib WORKDIR /app +COPY scripts/plot_rate.py plot_rate.py COPY scripts/gluex-reader.py gluex-reader.py # Ensure log signals (Waiting for data / HAIDIS TRAINING COMPLETE) flush immediately diff --git a/VERSION-gluex-reader.txt b/VERSION-gluex-reader.txt index b1e80bb..9faa1b7 100644 --- a/VERSION-gluex-reader.txt +++ b/VERSION-gluex-reader.txt @@ -1 +1 @@ -0.1.3 +0.1.5 diff --git a/VERSION.txt b/VERSION.txt index cdc16a1..1a38ea2 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.1.3a10 +0.1.3a11 diff --git a/bin/e2sar_root.cpp b/bin/e2sar_root.cpp index 0891634..494dbf0 100644 --- a/bin/e2sar_root.cpp +++ b/bin/e2sar_root.cpp @@ -342,7 +342,7 @@ struct ReceiveStats { // Receive events and write to memory-mapped files bool receiveEvents(e2sar::Reassembler& reassembler, const std::string& output_pattern, - uint16_t expected_data_id) { + uint16_t expected_data_id, bool noSave) { std::cout << "\nStarting batch reception..." << std::endl; std::cout << "Output pattern: " << output_pattern << std::endl; std::cout << "Press Ctrl+C to stop\n" << std::endl; @@ -385,11 +385,14 @@ bool receiveEvents(e2sar::Reassembler& reassembler, const std::string& output_pa std::string filename = formatFilename(output_pattern, event_num); - if (writeMemoryMappedFile(filename, event_buffer, event_size)) { - stats.events_written++; - } else { - stats.write_errors++; - std::cerr << "Failed to write event " << event_num << std::endl; + // sometimes we just want to count events stats + if (not noSave) { + if (writeMemoryMappedFile(filename, event_buffer, event_size)) { + stats.events_written++; + } else { + stats.write_errors++; + std::cerr << "Failed to write event " << event_num << std::endl; + } } delete[] event_buffer; @@ -476,6 +479,8 @@ CommandLineArgs parseArgs(int argc, char* argv[]) { "Use Dalitz toy-MC event schema (dalitz_root_tree branches)") ("gluex", po::bool_switch(&args.use_gluex)->default_value(false), "Use GlueX kinematic-fit event schema (myTree branches)") + ("nosave", po::bool_switch(&args.noSaveEvents)->default_value(false), + "Don't save events that are received into files, just count stats") ("withcp,c", po::bool_switch()->default_value(false), "enable control plane interactions") ("rate", po::value(&args.rateGbps)->default_value(1.0), @@ -609,7 +614,7 @@ int main(int argc, char* argv[]) { return 1; } - bool success = receiveEvents(*reassembler, args.output_pattern, args.data_id); + bool success = receiveEvents(*reassembler, args.output_pattern, args.data_id, args.noSaveEvents); std::cout << "\nDeregistering worker..." << std::endl; auto deregres = reassembler->deregisterWorker(); diff --git a/include/file_processor.hpp b/include/file_processor.hpp index 843d325..6a5df12 100644 --- a/include/file_processor.hpp +++ b/include/file_processor.hpp @@ -37,6 +37,8 @@ struct CommandLineArgs { // Event schema selection (exactly one required for sender/read-only mode) bool use_toy = false; bool use_gluex = false; + // whether the receiver should save or simply count the events + bool noSaveEvents = false; }; // Defined in file_processor.cpp; also used by e2sar_root.cpp (receiveEvents, main). diff --git a/meson.build b/meson.build index 054e4f6..47085c9 100644 --- a/meson.build +++ b/meson.build @@ -10,6 +10,10 @@ project('e2sar-utils', 'cpp', # C++ compiler cxx = meson.get_compiler('cpp') +# Newer Xcode SDKs (macOS 15.4+) removed _LIBCPP_ENABLE_ASSERTIONS; undefine it +# so meson's b_ndebug=false doesn't break the build on updated toolchains. +add_project_arguments('-U_LIBCPP_ENABLE_ASSERTIONS', language: 'cpp') + # Compile-time dependencies # Boost dependencies: program_options used by root-reader, others required by e2sar # Note: Boost doesn't provide .pc files, so we must declare them explicitly even though diff --git a/scripts/gluex-reader.py b/scripts/gluex-reader.py index 71e3076..8293baa 100755 --- a/scripts/gluex-reader.py +++ b/scripts/gluex-reader.py @@ -9,6 +9,7 @@ import argparse import signal import sys +import time from datetime import datetime, timezone import matplotlib @@ -149,6 +150,10 @@ def main(): out_file = None iteration = 0 + total_events = 0 + interval_events = 0 + t_start = time.monotonic() + t_interval = t_start try: if args.save: @@ -166,6 +171,10 @@ def main(): if arr is None: continue + batch_events = arr.shape[0] + total_events += batch_events + interval_events += batch_events + if args.filter_abs_max is not None: mask = (np.abs(arr[:, 0]) <= args.filter_abs_max) & \ (np.abs(arr[:, 1]) <= args.filter_abs_max) @@ -196,7 +205,12 @@ def main(): hist_counts_y += np.histogram(ys_in, bins=hist_edges_y)[0] if (iteration % 10 == 0): - print(f"Iteration {iteration}", flush=True) + now = time.monotonic() + dt = now - t_interval + rate = interval_events / dt if dt > 0 else 0.0 + print(f"Iteration {iteration} | {rate:,.0f} pairs/s | {total_events:,} total | elapsed={now - t_start:.2f}s | ts={time.time():.3f}", flush=True) + t_interval = now + interval_events = 0 iteration += 1 @@ -219,6 +233,12 @@ def main(): if out_file is not None: out_file.close() + elapsed = time.monotonic() - t_start + avg_rate = total_events / elapsed if elapsed > 0 else 0.0 + print(f"\nTotal pairs received: {total_events:,} | " + f"Elapsed: {elapsed:.1f} s | " + f"Avg rate: {avg_rate:,.0f} pairs/s", flush=True) + if args.histogram and hist_edges_x is not None: _print_histogram("X", hist_edges_x, hist_counts_x) _print_histogram("Y", hist_edges_y, hist_counts_y) diff --git a/scripts/plot_rate.py b/scripts/plot_rate.py new file mode 100644 index 0000000..dacbc90 --- /dev/null +++ b/scripts/plot_rate.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Plot rate and cumulative pairs from one or more gluex-reader log files. + +Each log file should come from a separate run (e.g. different --bufsize settings). +Parses lines of the form: + Iteration N | R pairs/s | T total | elapsed=E.EEs | ts=U.UUU + +Usage: + python plot_rate.py run1.log run2.log [--label "batch=1MB" "batch=4MB"] --output out.png +""" + +import argparse +import re +import sys +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + +LINE_RE = re.compile( + r"Iteration\s+\d+\s*\|" + r"\s*([\d,]+)\s*pairs/s\s*\|" + r"\s*([\d,]+)\s*total\s*\|" + r"\s*elapsed=([\d.]+)s\s*\|" + r"\s*ts=([\d.]+)" +) + + +def parse_log(path: str) -> tuple[list[float], list[float], list[float]]: + elapsed, rates, totals = [], [], [] + with open(path) as f: + for line in f: + m = LINE_RE.search(line) + if m: + rate = float(m.group(1).replace(",", "")) + total = float(m.group(2).replace(",", "")) + e = float(m.group(3)) + elapsed.append(e) + rates.append(rate) + totals.append(total) + if not elapsed: + print(f"WARNING: no matching lines found in {path}", file=sys.stderr) + else: + t0 = elapsed[0] + elapsed = [e - t0 for e in elapsed] + return elapsed, rates, totals + + +def parse_args(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("logs", nargs="+", metavar="LOG", help="Log file(s) to plot") + p.add_argument("--label", action="append", metavar="LABEL", dest="label", + help="Series label; repeat once per log file (defaults to filename stems)") + p.add_argument("--output", default="rate_comparison.png", metavar="FILE", + help="Output PNG path (default: rate_comparison.png)") + p.add_argument("--title", default="gluex-reader throughput comparison", metavar="TEXT", + help="Plot title") + args = p.parse_args() + if args.label and len(args.label) != len(args.logs): + p.error(f"--label count ({len(args.label)}) must match log file count ({len(args.logs)})") + if not args.label: + args.label = [Path(f).stem for f in args.logs] + return args + + +def main(): + args = parse_args() + + # Colour cycle — enough for many series + colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] + + fig, ax_rate = plt.subplots(figsize=(10, 5), tight_layout=True) + ax_total = ax_rate.twinx() + + for i, (log_path, label) in enumerate(zip(args.logs, args.label)): + color = colors[i % len(colors)] + elapsed, rates, totals = parse_log(log_path) + if not elapsed: + continue + ax_rate.plot(elapsed, rates, color=color, linewidth=1.5, + label=f"{label} — rate") + ax_total.plot(elapsed, totals, color=color, linewidth=1.5, + linestyle="--", label=f"{label} — total") + + ax_rate.set_xlabel("Elapsed time (s)") + ax_rate.set_ylabel("Pairs / second") + ax_total.set_ylabel("Cumulative X,Y pairs received") + + ax_rate.yaxis.set_major_formatter(ticker.FuncFormatter(lambda v, _: f"{v:,.0f}")) + ax_total.yaxis.set_major_formatter(ticker.FuncFormatter(lambda v, _: f"{v:,.0f}")) + + ax_rate.set_title(args.title) + + # Combined legend from both axes + lines_rate, labels_rate = ax_rate.get_legend_handles_labels() + lines_total, labels_total = ax_total.get_legend_handles_labels() + ax_rate.legend(lines_rate + lines_total, labels_rate + labels_total, + loc="upper left", fontsize=8) + + fig.savefig(args.output, dpi=150) + print(f"Saved to {args.output}", flush=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/start-gluex-sender.sh b/scripts/start-gluex-sender.sh index ddd0407..eff9e65 100755 --- a/scripts/start-gluex-sender.sh +++ b/scripts/start-gluex-sender.sh @@ -1,47 +1,90 @@ #!/bin/bash +# This script starts sending GlueX data into one of the two load balancer +# Production load balancer (--lbid 1) is the default, while +# testing load balancer (--lbid 2) can also optionaly be used. +# REMEMBER that both sender and receiver have to be using the same LB. + +# Usage: ./start_gluex_sender.sh [options] + +# Options: +# --lbid Which load balancer URL to use (1 is default, production/stable, 2 is for testing only) +# --parallel How many parallel file readers to start (1 is the default) +# --eventsize How big EJFAT events should be in MB (1MB is the default) + # To make things run faster, increase the PARALLEL setting - that is the number of file reader child processes. Between 5-10 makes sense -# To let it exit gracefully you can do `touch /tmp/stop-e2sar-loop`. If you need to kill it urgently use -# $ touch /tmp/stop-e2sar-loop -# $ pkill -KILL start-gluex-sender.sh -# $ podman stop e2sar-root -# but check `htop` to make sure all the children quit before restarting +# To let it exit gracefully you can do `touch /tmp/stop-e2sar-loop`. +# If you need to kill it urgently use ./stop-gluex-sender [--lbid 1 or 2]] MTU=9000 DATAID=1 RATE=-1.0 DATAVOL=/nvme/haidis/gluex/eta3pi_trees/data2017 -LOOPFILE="/tmp/stop-e2sar-loop" -SINGLETONFILE="/tmp/e2sar-loop-running" -E2SAR_ROOT_VER="0.1.3a10" -PARALLEL=1 +CONTAINERPREFIX="e2sar-root" +LOOPPREFIX="/tmp/stop-e2sar-loop" +SINGLETONPREFIX="/tmp/e2sar-loop-running" +E2SAR_ROOT_VER="latest" +EJFATURIS=('ejfats://a6fd0e1f4cf64948ba7d5d30e1b604c4@ejfat-lb.es.net:18048/lb/60?sync=192.188.29.6:19054&data=192.188.29.54&data=[2001:400:a300::54]' + 'ejfats://fb14841f350d45fda2b730233deff0a8@ejfat-lb.es.net:18008/lb/334?sync=192.188.29.6:19010&data=192.188.29.10&data=[2001:400:a300::10]') + BUFFER_SIZE=1 -SEND_IP="" -EJFAT_URI='ejfats://token@ejfat-lb.es.net:18048/lb/60?sync=192.188.29.6:19054&data=192.188.29.54&data=[2001:400:a300::54]' +PARALLEL=1 +LBID=1 + +usage() { + head -20 "$0" | tail -18 + exit 0 +} while [[ $# -gt 0 ]]; do - case "$1" in - --send-ip) SEND_IP="$2"; shift 2 ;; - *) echo "Unknown argument: $1"; exit 1 ;; + case $1 in + --lbid) + LBID="$2" + shift 2 + ;; + --parallel) + PARALLEL="$2" + shift 2 + ;; + --eventsize) + BUFFER_SIZE="$2" + shift 2 + ;; + --help) + usage + ;; + *) + echo "Unknown option: $1" + usage + ;; esac done -SEND_IP_ARG="" -[[ -n "$SEND_IP" ]] && SEND_IP_ARG="--send-ip ${SEND_IP}" +SINGLETONFILE=${SINGLETONPREFIX}-${LBID} +LOOPFILE=${LOOPPREFIX}-${LBID} +CONTAINERNAME=${CONTAINERPREFIX}-${LBID} + +if [ "$LBID" -lt 1 ] || [ "$LBID" -gt 2 ]; then + echo "Invalid LB ID, only 1 or 2 allowed" + exit 1 +fi + +EJFAT_URI=${EJFATURIS[$(($LBID -1))]} if [ -e $SINGLETONFILE ]; then - echo "Singleton file $SINGLETONFILE is present, another instance may be running" - exit -1 + echo "Singleton file $SINGLETONFILE is present, another instance may be running" + exit -1 fi -touch ${SINGLETONFILE} +echo $$ > ${SINGLETONFILE} + +echo Using the following EJFAT_URI: ${EJFAT_URI} while [ ! -f "$LOOPFILE" ]; do - echo "**** Run 'touch ${LOOPFILE}' to stop" - podman run --rm -v ${DATAVOL}:/data --network=host --name=e2sar-root --security-opt label=disable docker.io/ibaldin/e2sar-utils:${E2SAR_ROOT_VER} e2sar-root --gluex -s --tree myTree -u "${EJFAT_URI}" --withcp --mtu ${MTU} --dataid ${DATAID} --rate ${RATE} --dir /data --parallel ${PARALLEL} --bufsize-mb ${BUFFER_SIZE} ${SEND_IP_ARG} + echo "**** Run 'touch ${LOOPFILE}' to stop" + podman run --rm -v ${DATAVOL}:/data --network=host --name=${CONTAINERNAME} --security-opt label=disable docker.io/ibaldin/e2sar-utils:${E2SAR_ROOT_VER} e2sar-root --gluex -s --tree myTree -u ${EJFAT_URI} --withcp --mtu ${MTU} --dataid ${DATAID} --rate ${RATE} --dir /data --parallel ${PARALLEL} --bufsize-mb ${BUFFER_SIZE} done rm ${LOOPFILE} rm ${SINGLETONFILE} - diff --git a/scripts/stop-gluex-sender.sh b/scripts/stop-gluex-sender.sh index 68b4e71..bd1c9ef 100644 --- a/scripts/stop-gluex-sender.sh +++ b/scripts/stop-gluex-sender.sh @@ -1,7 +1,53 @@ #!/bin/bash -touch /tmp/stop-e2sar-loop -pkill -KILL start-gluex-sender.sh -podman stop e2sar-root -rm /tmp/e2sar-loop-running /tmp/stop-e2sar-loop +# This script attempts to stop (not always successfully) a GlueX file sender + +# Usage: ./stop_gluex_sender.sh [options] + +# Options: +# --lbid Which load balancer URL to use (1 is default, production/stable, 2 is for testing only) + +# Always be sure to stop the right sender (1 or 2) + +SINGLETONPREFIX="/tmp/e2sar-loop-running" +LOOPPREFIX="/tmp/stop-e2sar-loop" +CONTAINERPREFIX="e2sar-root" +LBID=1 + +usage() { + head -10 "$0" | tail -9 + exit 0 +} + +while [[ $# -gt 0 ]]; do + case $1 in + --lbid) + LBID="$2" + shift 2 + ;; + --help) + usage + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +LOOPFILE=${LOOPPREFIX}-${LBID} +SINGLETONFILE=${SINGLETONPREFIX}-${LBID} +CONTAINERNAME=${CONTAINERPREFIX}-${LBID} + +touch ${LOOPFILE} + +# Kill the start script via the PID stored in the singleton file. +if [[ -f "${SINGLETONFILE}" ]]; then + START_PID=$(cat "${SINGLETONFILE}" 2>/dev/null) + [[ -n "${START_PID}" ]] && kill -9 "${START_PID}" 2>/dev/null || true +fi + +podman stop --time 10 ${CONTAINERNAME} 2>/dev/null || \ + podman kill ${CONTAINERNAME} 2>/dev/null || true +rm -f ${SINGLETONFILE} ${LOOPFILE} diff --git a/specs/PLAN.md b/specs/PLAN.md index 3476289..27c51b6 100644 --- a/specs/PLAN.md +++ b/specs/PLAN.md @@ -51,7 +51,7 @@ sbatch/ scripts/ ├── start-gluex-sender.sh # existing ├── stop-gluex-sender.sh # existing -└── shmem_reader.py # new: simple shmem consumer replacing SAGIPS (Phase 0) +└── gluex-reader.py # shmem consumer replacing SAGIPS (Phase 0) ✓ ``` --- @@ -430,16 +430,33 @@ The script keeps the same structure as `haidis_slurm.sh`: - Same `ERSAP_CONFIG_DIR` injection mechanism - Same log paths and job directory layout so `haidis-run` monitoring is unchanged -**Deliverable 2: `scripts/shmem_reader.py`** +**Deliverable 2: `scripts/gluex-reader.py`** ✓ Implemented -A Python script that connects to the ERSAP shared memory segment and reads -data in a loop. It emits the same log signals as SAGIPS so that `monitor.py` -works unchanged: +Connects to the ERSAP shared memory segment and reads data in a loop. +Emits the same log signals as SAGIPS so that `monitor.py` works unchanged: -- Readiness signal: `Waiting for data (sample 1)` — emitted once the shmem segment is attached and before the first read -- Completion signal: `HAIDIS TRAINING COMPLETE: epochs=N/N` — emitted after `--iterations` batches have been read +- Readiness signal: `Waiting for data (sample 1)` — emitted once, before the first read +- Completion signal: `HAIDIS TRAINING COMPLETE: epochs=N/N` — emitted after `--iterations` batches -Implementation details will be provided separately. +Key CLI flags: + +```text +gluex-reader.py --shmem-name NAME --sem-name NAME --sem-ack-name NAME + [--shmem-size BYTES] [--iterations N] + (--save FILE | --histogram) + [--bins N] [--out-stats FILE] [--plot FILE] + [--flush-every N] [--filter-abs-max X] +``` + +Two output modes (mutually exclusive, one required): + +- `--save FILE`: appends each batch as CSV rows (`x,y` per event) +- `--histogram`: accumulates per-axis histograms; prints ASCII summary on exit; optionally writes `.npz` stats (`--out-stats`) and a two-panel PNG (`--plot`) + +Both modes support `--filter-abs-max X` to discard events where +`abs(x) > X` or `abs(y) > X` before accumulation. Periodic intermediate +saves via `--flush-every N` (default 10 batches). Handles `SIGTERM`/`SIGINT` +for clean shutdown. **Impact on `haidis-run`:** diff --git a/tests/test_loopback.sh b/tests/test_loopback.sh index 1b6bbd8..ec17024 100755 --- a/tests/test_loopback.sh +++ b/tests/test_loopback.sh @@ -22,6 +22,7 @@ # --dataid N Data ID passed to E2SAR Segmenter (default: 0) # --rate R Send rate in Gbps passed to Segmenter (default: -1.0 = no limit) # --numsocks N Number of Segmenter send sockets (default: 4) +# --nosave Pass --nosave to receiver; verify by log instead of file count # --help Show this help message # @@ -38,6 +39,7 @@ LINK_DIR="" DATAID=0 RATE=-1.0 NUM_SOCKS=4 +NOSAVE=false SCHEMA=toy # toy | gluex SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" @@ -139,6 +141,10 @@ while [[ $# -gt 0 ]]; do NUM_SOCKS="$2" shift 2 ;; + --nosave) + NOSAVE=true + shift + ;; --parallel) PARALLEL="$2" shift 2 @@ -200,6 +206,7 @@ echo " MTU: $MTU" echo " Rate: $RATE Gbps" echo " Num sockets: $NUM_SOCKS" echo " Data ID: $DATAID" +echo " No-save: $NOSAVE" echo " Timeout: $TIMEOUT seconds" echo " Output dir: $OUTPUT_DIR" echo "" @@ -208,11 +215,15 @@ echo "" log_info "Starting receiver..." cd "$OUTPUT_DIR" +NOSAVE_ARG="" +[[ "$NOSAVE" == "true" ]] && NOSAVE_ARG="--nosave" + "$EXECUTABLE" -r \ -u "$EJFAT_URI" \ --recv-ip 127.0.0.1 \ --dataid "$DATAID" \ -o "event_{:08d}.dat" \ + $NOSAVE_ARG \ > "$RECV_LOG" 2>&1 & RECV_PID=$! @@ -278,7 +289,7 @@ log_info "Sender stats: $BUFFERS_SENT batches sent, $SEND_ERRORS errors, ${THROU log_info "Waiting up to $TIMEOUT seconds for receiver to finish processing..." WAIT_START=$(date +%s) -EXPECTED_FILES=$BUFFERS_SENT +EXPECTED_EVENTS=$BUFFERS_SENT while true; do CURRENT_TIME=$(date +%s) @@ -289,17 +300,24 @@ while true; do break fi - # Count received files - RECEIVED_FILES=$(ls "$OUTPUT_DIR"/event_*.dat 2>/dev/null | wc -l | tr -d ' ') + if [[ "$NOSAVE" == "true" ]]; then + # In nosave mode the receiver keeps running; poll the log for received count. + # Both the periodic progress line and the final summary line contain + # "Batches received: N"; awk ignores leading whitespace so $3 works for both. + RECEIVED_EVENTS=$(awk '/Batches received:/{print $3}' "$RECV_LOG" | tail -1) + RECEIVED_EVENTS=${RECEIVED_EVENTS:-0} + else + RECEIVED_EVENTS=$(ls "$OUTPUT_DIR"/event_*.dat 2>/dev/null | wc -l | tr -d ' ') + fi - if [[ "$RECEIVED_FILES" -ge "$EXPECTED_FILES" ]]; then - log_info "All $EXPECTED_FILES EJFAT events received" + if [[ "$RECEIVED_EVENTS" -ge "$EXPECTED_EVENTS" ]]; then + log_info "All $EXPECTED_EVENTS EJFAT events received" break fi # Progress update every 5 seconds if [[ $((ELAPSED % 5)) -eq 0 ]] && [[ $ELAPSED -gt 0 ]]; then - log_info "Progress: $RECEIVED_FILES / $EXPECTED_FILES EJFAT events received ($ELAPSED seconds elapsed)" + log_info "Progress: $RECEIVED_EVENTS / $EXPECTED_EVENTS EJFAT events received ($ELAPSED seconds elapsed)" fi sleep 1 @@ -309,21 +327,29 @@ done log_info "Stopping receiver..." kill -INT "$RECV_PID" 2>/dev/null || true wait "$RECV_PID" 2>/dev/null || true -sync # flush kernel page cache so file count is accurate -# Count final results -RECEIVED_FILES=$(ls "$OUTPUT_DIR"/event_*.dat 2>/dev/null | wc -l | tr -d ' ') +# Final received count +if [[ "$NOSAVE" == "true" ]]; then + RECEIVED_EVENTS=$(awk '/Batches received:/{print $3}' "$RECV_LOG" | tail -1) + RECEIVED_EVENTS=${RECEIVED_EVENTS:-0} + RECV_RATE=$(awk '/^Average rate:/{print $3}' "$RECV_LOG" | tail -1) +else + sync # flush kernel page cache so file count is accurate + RECEIVED_EVENTS=$(ls "$OUTPUT_DIR"/event_*.dat 2>/dev/null | wc -l | tr -d ' ') + RECV_RATE="" +fi echo "" log_info "========== Test Results ==========" echo " Parallel files: $NUM_FILES" echo " Buffers sent: $BUFFERS_SENT" -echo " Files received: $RECEIVED_FILES" +echo " Batches received: $RECEIVED_EVENTS" echo " Send errors: $SEND_ERRORS" echo " Throughput: $THROUGHPUT Gbps" +[[ -n "$RECV_RATE" ]] && echo " Recv rate: $RECV_RATE Mbps" # Verify results -if [[ "$RECEIVED_FILES" -eq "$BUFFERS_SENT" ]] && [[ "$SEND_ERRORS" -eq "0" ]]; then +if [[ "$RECEIVED_EVENTS" -ge "$BUFFERS_SENT" ]] && [[ "$SEND_ERRORS" -eq "0" ]]; then echo "" log_info "${GREEN}TEST PASSED${NC} - All buffers received successfully" TEST_PASSED=true @@ -331,8 +357,8 @@ if [[ "$RECEIVED_FILES" -eq "$BUFFERS_SENT" ]] && [[ "$SEND_ERRORS" -eq "0" ]]; else echo "" log_error "TEST FAILED" - if [[ "$RECEIVED_FILES" -ne "$BUFFERS_SENT" ]]; then - log_error " Expected $BUFFERS_SENT files, got $RECEIVED_FILES" + if [[ "$RECEIVED_EVENTS" -lt "$BUFFERS_SENT" ]]; then + log_error " Expected $BUFFERS_SENT batches, got $RECEIVED_EVENTS" fi if [[ "$SEND_ERRORS" -ne "0" ]]; then log_error " $SEND_ERRORS send errors occurred"