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
2 changes: 1 addition & 1 deletion Dockerfile.cli
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Dockerfile.gluex-reader
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION-gluex-reader.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.3
0.1.5
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.3a10
0.1.3a11
19 changes: 12 additions & 7 deletions bin/e2sar_root.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<float>(&args.rateGbps)->default_value(1.0),
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions include/file_processor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
4 changes: 4 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion scripts/gluex-reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import argparse
import signal
import sys
import time
from datetime import datetime, timezone

import matplotlib
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down
108 changes: 108 additions & 0 deletions scripts/plot_rate.py
Original file line number Diff line number Diff line change
@@ -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()
87 changes: 65 additions & 22 deletions scripts/start-gluex-sender.sh
Original file line number Diff line number Diff line change
@@ -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}

Loading