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
53 changes: 53 additions & 0 deletions scripts/measure_e2e.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/sh

# SLP transaction end-to-end latency measurement wrapper.
#
# Usage: sh scripts/measure_e2e.sh [SLP_EVAL_OPTION...] [-- MISSION_ARG...]
#
# A preset over scripts/slp_eval.sh that sets the options needed to measure
# tx end-to-end latency.

SLP_EVAL="$(dirname "$0")/slp_eval.sh"

NETWORK_SIZE_LIMIT=1000
PUBNET_DATA_FILE="public-network-data-2026-06-03-trimmed-located.json"
LOADGEN_KEYS_FILE="public-network-data-2026-06-03-loadgenkeys.json"

# Sets DATA_ROOT and SEPARATOR_GIVEN (whether the user supplied "--" in the
# arguments). Shifts its own copy of the arguments, leaving the caller's "$@"
# intact.
scan_args() {
DATA_ROOT="$(pwd)"
SEPARATOR_GIVEN=0

while [ "$#" -gt 0 ]; do
case "$1" in
--)
SEPARATOR_GIVEN=1
return
;;
--data-root | --supercluster-root)
DATA_ROOT="$2"
;;
--data-root=* | --supercluster-root=*)
DATA_ROOT="${1#*=}"
;;
esac

shift
done
}

scan_args "$@"

# Add a separator if the caller didn't supply one
if [ "$SEPARATOR_GIVEN" -eq 0 ]; then
set -- "$@" --
fi

exec sh "$SLP_EVAL" \
--network-size-limit "$NETWORK_SIZE_LIMIT" \
--pubnet-data "$DATA_ROOT/data/$PUBNET_DATA_FILE" \
"$@" \
--loadgen-keys "$DATA_ROOT/data/$LOADGEN_KEYS_FILE" \
--measure-e2e-latency
103 changes: 88 additions & 15 deletions scripts/slp_eval.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
# SLP mixed-load evaluation wrapper.
#
# This script runs the MinBlockTimeMixed mission against a stellar-core image
# using the 2025-06-24 pubnet topology data and the fixed benchmark parameters
# below. It is intended to answer: "does this image sustain the selected mixed
# classic/Soroban load at the normal 5s ledger close target?"
# using the 2025-06-24 pubnet topology data (by default; see --pubnet-data) and
# the fixed benchmark parameters below. It is intended to answer: "does this
# image sustain the selected mixed classic/Soroban load at the normal 5s ledger
# close target?"
#
# Benchmark setup:
# - One mission run is started for each selected Soroban load flag:
Expand All @@ -15,14 +16,18 @@
# match current network conditions. The flag value supplies only the Soroban
# TPS for that run, so total TPS is CLASSIC_TX_RATE + selected Soroban TPS.
# - The mission uses MinBlockTimeMixed's MIXED_PREGEN_* overlay-only loadgen
# mode, simulated pubnet network delay, with NETWORK_SIZE_LIMIT nodes.
# mode, simulated pubnet network delay, with a configurable number of nodes
# (--network-size-limit, default 277).
# - The block-time search range is intentionally narrow:
# [MIN_BLOCK_TIME_MS, MAX_BLOCK_TIME_MS] = [4900, 5100]. With the mission's
# 100ms binary-search threshold, this effectively evaluates the 5s target
# to match the current network close time.
# - simulate-apply-duration is derived from SIMULATE_APPLY_BUDGET_MS and the
# total TPS so the synthetic apply sleep budget remains roughly constant as
# the requested Soroban rate changes.
# - Arguments after a trailing "--" are appended verbatim to every mission
# command line, so one-off flags can be added without editing this script.
# scripts/measure_e2e.sh is a preset built on top of that mechanism.
#
# Result interpretation:
# - A zero exit and a "Minimum sustainable block time: ..." log line means the
Expand All @@ -41,6 +46,7 @@ STELLAR_CORE_IMAGE=
SAC_TX_RATE=
OZ_TX_RATE=
SOROSWAP_TX_RATE=
PUBNET_DATA=

IMAGE_REPOSITORY="746476062914.dkr.ecr.us-east-1.amazonaws.com/dev"

Expand All @@ -64,11 +70,15 @@ NUM_PREGENERATED_TXS=1000000
GENESIS_TEST_ACCOUNT_COUNT=1000000
SIMULATE_APPLY_WEIGHT=100
SIMULATE_APPLY_BUDGET_MS=600
NETWORK_SIZE_LIMIT=277
DEFAULT_NETWORK_SIZE_LIMIT=277
NETWORK_SIZE_LIMIT="$DEFAULT_NETWORK_SIZE_LIMIT"
DEFAULT_PUBNET_DATA_FILE="public-network-data-2025-06-24.json"

usage() {
cat <<EOF
Usage: $0 --stellar-core-image IMAGE [--data-root PATH] [--sac RATE] [--oz RATE] [--soroswap RATE]
Usage: $0 --stellar-core-image IMAGE [--data-root PATH] [--network-size-limit N]
[--pubnet-data PATH] [--sac RATE] [--oz RATE] [--soroswap RATE]
[-- MISSION_ARG...]

Runs one MinBlockTimeMixed mission per selected load flag against IMAGE.
Each run always generates ${CLASSIC_TX_RATE} classic payment TPS plus the
Expand All @@ -78,19 +88,24 @@ TPS + 50 SAC Soroban TPS; "--sac 50 --oz 25" runs two separate benchmarks.
Options:
--stellar-core-image, --image IMAGE Stellar Core image to evaluate. Required.
--data-root, --supercluster-root PATH Root containing data/. Defaults to pwd.
--network-size-limit N Number of pubnet nodes to run.
Defaults to ${DEFAULT_NETWORK_SIZE_LIMIT}.
--pubnet-data PATH Pubnet connectivity graph to run against.
Defaults to <data-root>/data/${DEFAULT_PUBNET_DATA_FILE}.
--sac RATE Run SAC load with the given Soroban tx rate.
Can be supplied with other load flags to run benchmarks sequentially.
--oz RATE Run OZ load with the given Soroban tx rate.
Can be supplied with other load flags to run benchmarks sequentially.
--soroswap RATE Run Soroswap load with the given Soroban tx rate.
Can be supplied with other load flags to run benchmarks sequentially.
-h, --help Show this help.
-- MISSION_ARG... Everything after "--" is appended verbatim to each
mission command line. Use it for one-off flags this
wrapper does not expose.

Benchmark constants:
Classic TPS: ${CLASSIC_TX_RATE}
Target close time: ${BLOCK_TIME_MS}ms, evaluated via [${MIN_BLOCK_TIME_MS}, ${MAX_BLOCK_TIME_MS}]
Network size limit: ${NETWORK_SIZE_LIMIT}
Data set: data/public-network-data-2025-06-24.json

Results:
PASS: command exits 0 and logs "Minimum sustainable block time: ...".
Expand All @@ -112,7 +127,13 @@ is_nonnegative_integer() {
esac
}

# Parses the wrapper's own options and stops at a "--" separator. Sets
# PARSED_ARG_COUNT to the number of arguments consumed (including the
# separator) so the caller can shift them off and keep the mission arguments
# that followed in "$@".
parse_args() {
total_arg_count="$#"

while [ "$#" -gt 0 ]; do
case "$1" in
--stellar-core-image | --image)
Expand Down Expand Up @@ -180,6 +201,43 @@ parse_args() {
SOROSWAP_TX_RATE="${1#*=}"
shift
;;
--network-size-limit)
if [ "$#" -lt 2 ] || [ -z "$2" ]; then
printf '%s\n' "--network-size-limit requires a node count." >&2
usage >&2
exit 1
fi
NETWORK_SIZE_LIMIT="$2"
shift 2
;;
--network-size-limit=*)
NETWORK_SIZE_LIMIT="${1#*=}"
shift
;;
--pubnet-data)
if [ "$#" -lt 2 ] || [ -z "$2" ]; then
printf '%s\n' "--pubnet-data requires a path." >&2
usage >&2
exit 1
fi
PUBNET_DATA="$2"
shift 2
;;
--pubnet-data=*)
PUBNET_DATA="${1#*=}"
# An empty value here would silently fall back to the
# default data set, so reject it like the two-token form.
if [ -z "$PUBNET_DATA" ]; then
printf '%s\n' "--pubnet-data requires a path." >&2
usage >&2
exit 1
fi
shift
;;
--)
shift
break
;;
-h | --help)
usage
exit 0
Expand All @@ -191,6 +249,8 @@ parse_args() {
;;
esac
done

PARSED_ARG_COUNT=$((total_arg_count - $#))
}

validate_tx_rate() {
Expand Down Expand Up @@ -227,6 +287,13 @@ validate_args() {
if [ -n "$SOROSWAP_TX_RATE" ]; then
validate_tx_rate "--soroswap" "$SOROSWAP_TX_RATE"
fi

# Non-numeric values make "[" exit 2 rather than 1, so test for the
# accepted range and negate: anything unparseable is rejected too.
if ! [ "$NETWORK_SIZE_LIMIT" -ge 1 ] 2>/dev/null; then
printf '%s\n' "--network-size-limit must be a positive integer." >&2
exit 1
fi
}

calculate_simulate_apply_duration() {
Expand Down Expand Up @@ -266,8 +333,9 @@ resolve_min_block_time_mixed_mode() {
}

run_min_block_time_mixed() {
mode_alias="${1:?usage: run_min_block_time_mixed MODE SOROBAN_TX_RATE}"
soroban_tx_rate="${2:?usage: run_min_block_time_mixed MODE SOROBAN_TX_RATE}"
mode_alias="${1:?usage: run_min_block_time_mixed MODE SOROBAN_TX_RATE [MISSION_ARG...]}"
soroban_tx_rate="${2:?usage: run_min_block_time_mixed MODE SOROBAN_TX_RATE [MISSION_ARG...]}"
shift 2
min_block_time_mixed_mode="$(resolve_min_block_time_mixed_mode "$mode_alias")"
simulate_apply_duration="$(calculate_simulate_apply_duration "$CLASSIC_TX_RATE" "$soroban_tx_rate")"

Expand Down Expand Up @@ -296,23 +364,28 @@ run_min_block_time_mixed() {
--tier1-keys "$TIER1_KEYS" \
--network-size-limit "$NETWORK_SIZE_LIMIT" \
--require-node-labels=purpose:largetests \
--tolerate-node-taints=largetests
--tolerate-node-taints=largetests \
"$@"
}

parse_args "$@"
shift "$PARSED_ARG_COUNT"
validate_args

PUBNET_DATA="$DATA_ROOT/data/public-network-data-2025-06-24.json"
if [ -z "$PUBNET_DATA" ]; then
PUBNET_DATA="$DATA_ROOT/data/$DEFAULT_PUBNET_DATA_FILE"
fi

TIER1_KEYS="$DATA_ROOT/data/tier1keys.json"

if [ -n "$SAC_TX_RATE" ]; then
run_min_block_time_mixed sac "$SAC_TX_RATE"
run_min_block_time_mixed sac "$SAC_TX_RATE" "$@"
fi

if [ -n "$OZ_TX_RATE" ]; then
run_min_block_time_mixed oz "$OZ_TX_RATE"
run_min_block_time_mixed oz "$OZ_TX_RATE" "$@"
fi

if [ -n "$SOROSWAP_TX_RATE" ]; then
run_min_block_time_mixed soroswap "$SOROSWAP_TX_RATE"
run_min_block_time_mixed soroswap "$SOROSWAP_TX_RATE" "$@"
fi
19 changes: 19 additions & 0 deletions src/App/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ type MissionOptions
apiRateLimit: int,
httpProxyReplicas: int,
pubnetData: string option,
measureE2eLatency: bool,
flatQuorum: bool option,
tier1Keys: string option,
loadgenKeys: string option,
maxConnections: int option,
fullyConnectTier1: bool,
byteCountValues: seq<int>,
Expand Down Expand Up @@ -318,12 +320,21 @@ type MissionOptions
[<Option("pubnet-data", HelpText = "JSON file containing pubnet connectivity graph data", Required = false)>]
member self.PubnetData = pubnetData

[<Option("measure-e2e-latency",
HelpText = "Set to enable the loadgen e2e metrics",
Required = false,
Default = false)>]
member self.MeasureE2eLatency = measureE2eLatency

[<Option("flat-quorum", HelpText = "Use flat Tier1 quorum", Required = false)>]
member self.FlatQuorum = flatQuorum

[<Option("tier1-keys", HelpText = "JSON file containing list of 'tier-1' pubkeys from pubnet", Required = false)>]
member self.Tier1Keys = tier1Keys

[<Option("loadgen-keys", HelpText = "JSON file containing list of pubkeys to generate load", Required = false)>]
member self.LoadgenKeys = loadgenKeys

[<Option("max-connections",
HelpText = "Maximum number of connections to allow any node in pubnet data to have. When enabled, this option will prune connections for any node with more than this number of connections. (default: no limit)",
Required = false)>]
Expand Down Expand Up @@ -743,6 +754,12 @@ let main argv =
0

| :? MissionOptions as mission ->
if mission.LoadgenKeys.IsSome && mission.PubnetData.IsNone then
failwith "Error: --loadgen-keys requires --pubnet-data to be set"

if mission.MeasureE2eLatency && mission.LoadgenKeys.IsNone then
failwith "Error: --measure-e2e-latency requires --loadgen-keys"

let _ = logToConsoleAndFile (sprintf "%s/stellar-supercluster.log" mission.Destination)

let ll =
Expand Down Expand Up @@ -848,8 +865,10 @@ let main argv =
apiRateLimit = mission.ApiRateLimit
httpProxyReplicas = mission.HttpProxyReplicas
pubnetData = mission.PubnetData
measureE2eLatency = mission.MeasureE2eLatency
flatQuorum = mission.FlatQuorum
tier1Keys = mission.Tier1Keys
loadgenKeys = mission.LoadgenKeys
maxConnections = mission.MaxConnections
fullyConnectTier1 = mission.FullyConnectTier1
byteCountDistribution =
Expand Down
2 changes: 2 additions & 0 deletions src/FSLibrary.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ let ctx : MissionContext =
apiRateLimit = 10
httpProxyReplicas = 2
pubnetData = None
measureE2eLatency = false
flatQuorum = None
tier1Keys = None
loadgenKeys = None
maxConnections = None
fullyConnectTier1 = false
peerReadingCapacity = None
Expand Down
10 changes: 8 additions & 2 deletions src/FSLibrary/MaxTPSTest.fs
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,15 @@ let maxTPSTest (context: MissionContext) (baseLoadGen: LoadGen) (setupCfg: LoadG
List.find (fun (cs: CoreSet) -> cs.name.StringName = "stellar" || cs.name.StringName = "sdf") allNodes

let tier1 = List.filter (fun (cs: CoreSet) -> cs.options.tier1 = Some true) allNodes
let loadGenNodes = List.filter (fun (cs: CoreSet) -> cs.options.generatesLoad) allNodes

let loadGenNodes =
if List.isEmpty loadGenNodes then
// On smaller networks, run loadgen on all nodes to better balance the overhead of load generation
if List.length allNodes > smallNetworkSize then tier1 else allNodes
else
loadGenNodes

// On smaller networks, run loadgen on all nodes to better balance the overhead of load generation
let loadGenNodes = if List.length allNodes > smallNetworkSize then tier1 else allNodes
let isLoadGenNode cs = List.exists (fun (cs': CoreSet) -> cs' = cs) loadGenNodes

// Assign pre-generated transaction information to each load generator node.
Expand Down
Loading
Loading