mirror of
https://github.com/layue13/MoonBridge.git
synced 2026-10-26 19:03:49 +00:00
501 lines
26 KiB
Java
501 lines
26 KiB
Java
package dev.moonbridge.core.session;
|
|
|
|
import dev.moonbridge.api.PlacementDecision;
|
|
import dev.moonbridge.api.PlayerView;
|
|
import dev.moonbridge.api.TransferStatus;
|
|
import dev.moonbridge.core.backend.BackendId;
|
|
import dev.moonbridge.core.backend.BackendOwner;
|
|
import dev.moonbridge.core.backend.BackendRegistration;
|
|
import dev.moonbridge.core.backend.InMemoryBackendCatalog;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.DataInputStream;
|
|
import java.io.DataOutputStream;
|
|
import java.io.EOFException;
|
|
import java.io.IOException;
|
|
import java.net.InetAddress;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.ServerSocket;
|
|
import java.net.Socket;
|
|
import java.net.URI;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.time.Duration;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/** Offline protocol-5 PLAY echo benchmark: direct fake backend vs ProxySessionListener. */
|
|
public final class ProxySessionBenchmark {
|
|
private static final int IO_TIMEOUT_MILLIS = 30_000;
|
|
private static final int SETUP_TIMEOUT_SECONDS = 20;
|
|
|
|
private ProxySessionBenchmark() { }
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
int connections = intArg(args, "connections", 4);
|
|
int messages = intArg(args, "messages", 1000);
|
|
int warmup = intArg(args, "warmup", 100);
|
|
int payload = intArg(args, "payload", 1024);
|
|
int repeats = intArg(args, "repeats", 2);
|
|
int window = intArg(args, "window", 1);
|
|
int commandInterceptor = intArg(args, "command-interceptor", 1);
|
|
String mode = modeArg(args);
|
|
boolean afterTransfer = mode.equals("post-transfer");
|
|
if (connections < 1 || connections > 32 || messages < 1 || messages > 100_000
|
|
|| warmup < 0 || warmup > 10_000 || payload < 5 || payload > 1_048_576
|
|
|| repeats < 1 || repeats > 10 || window < 1 || window > 1024
|
|
|| (commandInterceptor != 0 && commandInterceptor != 1)) {
|
|
throw new IllegalArgumentException("bounds: connections 1..32, messages 1..100000, warmup 0..10000, "
|
|
+ "payload 5..1048576, repeats 1..10, window 1..1024");
|
|
}
|
|
|
|
byte[] playPayload = new byte[payload];
|
|
playPayload[0] = 0x03; // PLAY packet ID; bytes 1..4 carry the per-request sequence.
|
|
for (int i = 1; i < payload; i++) playPayload[i] = (byte) (i * 31 + 7);
|
|
byte[] playFrame = frame(playPayload);
|
|
|
|
try (FakeBackend backend = new FakeBackend(connections, 42);
|
|
FakeBackend replacement = afterTransfer ? new FakeBackend(connections, 99) : null) {
|
|
backend.start();
|
|
if (replacement != null) replacement.start();
|
|
InMemoryBackendCatalog catalog = new InMemoryBackendCatalog();
|
|
catalog.register(new BackendRegistration(new BackendId("bench"), new BackendOwner("synthetic", 0),
|
|
URI.create("tcp://127.0.0.1:" + backend.port()), Map.of(), Map.of()));
|
|
if (replacement != null) {
|
|
catalog.register(new BackendRegistration(new BackendId("replacement"), new BackendOwner("synthetic", 0),
|
|
URI.create("tcp://127.0.0.1:" + replacement.port()), Map.of(), Map.of()));
|
|
}
|
|
ProxySessionListener proxy = new ProxySessionListener(
|
|
new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), catalog);
|
|
proxy.setPlacement(player -> java.util.concurrent.CompletableFuture.completedFuture(
|
|
Optional.of(PlacementDecision.select("bench"))));
|
|
if (commandInterceptor == 1) proxy.setCommandDispatcher((player, message, reply, admission) -> false);
|
|
try {
|
|
int proxyPort = ((InetSocketAddress) proxy.start().toCompletableFuture()
|
|
.get(SETUP_TIMEOUT_SECONDS, TimeUnit.SECONDS).localAddress()).getPort();
|
|
System.out.printf("Offline synthetic Minecraft 1.7.10 PLAY echo; mode=%s payload=%d bytes frame=%d bytes "
|
|
+ "connections=%d measured_roundtrips_per_connection=%d warmup_per_connection=%d "
|
|
+ "window_per_connection=%d repeats=%d command_interceptor=%d%n",
|
|
mode, playPayload.length, playFrame.length, connections, messages, warmup, window, repeats,
|
|
commandInterceptor);
|
|
System.out.println("Direct and proxy phases use the same JVM, fake backend, client code, framed payload, "
|
|
+ "connection concurrency, and per-client in-flight window. Login/setup and warmup are excluded from timing.");
|
|
if (afterTransfer) System.out.println("Proxy clients switch from bench to replacement before warmup; "
|
|
+ "direct clients connect to replacement. Transfer is excluded from timing.");
|
|
System.out.println("No Forge, real pack, or real client is involved.");
|
|
|
|
int directPort = replacement == null ? backend.port() : replacement.port();
|
|
boolean proxyFirst = false;
|
|
for (int round = 0; round < repeats; round++) {
|
|
if (proxyFirst) {
|
|
runPhase("proxy-" + (round + 1), proxyPort, connections, messages, warmup, window,
|
|
playFrame, playPayload, proxy, afterTransfer);
|
|
runPhase("direct-" + (round + 1), directPort, connections, messages, warmup, window,
|
|
playFrame, playPayload, proxy, afterTransfer);
|
|
} else {
|
|
runPhase("direct-" + (round + 1), directPort, connections, messages, warmup, window,
|
|
playFrame, playPayload, proxy, afterTransfer);
|
|
runPhase("proxy-" + (round + 1), proxyPort, connections, messages, warmup, window,
|
|
playFrame, playPayload, proxy, afterTransfer);
|
|
}
|
|
proxyFirst = !proxyFirst;
|
|
}
|
|
} finally {
|
|
proxy.close().toCompletableFuture().get(SETUP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void runPhase(String name, int port, int connections, int messages, int warmup, int window,
|
|
byte[] playFrame, byte[] playPayload, ProxySessionListener proxy,
|
|
boolean afterTransfer) throws Exception {
|
|
List<ClientWorker> clients = new ArrayList<>();
|
|
CountDownLatch prepared = new CountDownLatch(connections);
|
|
CountDownLatch begin = new CountDownLatch(1);
|
|
CountDownLatch finished = new CountDownLatch(connections);
|
|
for (int i = 0; i < connections; i++) {
|
|
ClientWorker client = new ClientWorker(name, i, port, messages, warmup, window, playFrame,
|
|
playPayload, prepared, begin, finished, clients, proxy, afterTransfer);
|
|
clients.add(client);
|
|
}
|
|
for (ClientWorker client : clients) {
|
|
Thread thread = new Thread(client, "proxy-session-bench-" + name + "-" + client.index);
|
|
thread.setDaemon(true);
|
|
thread.start();
|
|
}
|
|
if (!prepared.await(SETUP_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
|
begin.countDown();
|
|
closeClients(clients);
|
|
throw new IllegalStateException(name + " clients did not finish login/setup before deadline");
|
|
}
|
|
for (ClientWorker client : clients) if (client.failure != null)
|
|
{ begin.countDown(); closeClients(clients); throw new IllegalStateException(name + " client setup failed", client.failure); }
|
|
|
|
long started = System.nanoTime();
|
|
begin.countDown();
|
|
if (!finished.await(5, TimeUnit.MINUTES)) {
|
|
closeClients(clients);
|
|
throw new IllegalStateException(name + " phase timed out");
|
|
}
|
|
long elapsed = System.nanoTime() - started;
|
|
for (ClientWorker client : clients) if (client.failure != null) {
|
|
closeClients(clients);
|
|
throw new IllegalStateException(name + " client failed", client.failure);
|
|
}
|
|
|
|
long[] samples = clients.stream().flatMapToLong(client -> Arrays.stream(client.latencies)).toArray();
|
|
Arrays.sort(samples);
|
|
long measured = (long) connections * messages;
|
|
double seconds = elapsed / 1_000_000_000.0;
|
|
double oneWayMib = measured * (double) playPayload.length / (1024.0 * 1024.0) / seconds;
|
|
long echoes = clients.stream().mapToLong(client -> client.echoes).sum();
|
|
long correct = clients.stream().mapToLong(client -> client.correct).sum();
|
|
long errors = clients.stream().mapToLong(client -> client.errors).sum();
|
|
if (echoes != measured || correct != measured || errors != 0)
|
|
throw new IllegalStateException(name + " correctness/count mismatch: echoes=" + echoes + " correct="
|
|
+ correct + " errors=" + errors + " expected=" + measured);
|
|
System.out.printf("%s: roundtrips=%d echoes=%d correct=%d errors=%d roundtrips/s=%.1f "
|
|
+ "warmup_roundtrips=%d window=%d one_way_MiB/s=%.2f latency_ms_p50=%.3f p95=%.3f "
|
|
+ "p99=%.3f elapsed_ms=%.3f%n",
|
|
name, measured, echoes, correct, errors, measured / seconds, connections * warmup, window, oneWayMib,
|
|
percentile(samples, .50) / 1e6, percentile(samples, .95) / 1e6,
|
|
percentile(samples, .99) / 1e6, elapsed / 1e6);
|
|
}
|
|
|
|
private static void closeClients(List<ClientWorker> clients) {
|
|
for (ClientWorker client : clients) client.closeSocket();
|
|
}
|
|
|
|
private static final class ClientWorker implements Runnable {
|
|
final String phase;
|
|
final int index, port, messages, warmup, window;
|
|
final byte[] playFrame;
|
|
final byte[] playPayload;
|
|
final CountDownLatch prepared, begin, finished;
|
|
final List<ClientWorker> peers;
|
|
final ProxySessionListener proxy;
|
|
final boolean afterTransfer;
|
|
volatile Exception failure;
|
|
volatile Socket socket;
|
|
long[] latencies;
|
|
long echoes, correct, errors;
|
|
|
|
ClientWorker(String phase, int index, int port, int messages, int warmup, int window, byte[] playFrame,
|
|
byte[] playPayload, CountDownLatch prepared, CountDownLatch begin, CountDownLatch finished,
|
|
List<ClientWorker> peers, ProxySessionListener proxy, boolean afterTransfer) {
|
|
this.phase = phase; this.index = index; this.port = port; this.messages = messages;
|
|
this.warmup = warmup; this.window = window; this.playFrame = playFrame.clone(); this.playPayload = playPayload;
|
|
this.prepared = prepared; this.begin = begin; this.finished = finished; this.peers = peers;
|
|
this.proxy = proxy; this.afterTransfer = afterTransfer;
|
|
}
|
|
|
|
@Override public void run() {
|
|
try (Socket socket = new Socket()) {
|
|
this.socket = socket;
|
|
socket.setTcpNoDelay(true);
|
|
socket.setSoTimeout(IO_TIMEOUT_MILLIS);
|
|
socket.connect(new InetSocketAddress("127.0.0.1", port), 5000);
|
|
DataInputStream input = new DataInputStream(socket.getInputStream());
|
|
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
|
|
String username = "B" + (phase.startsWith("proxy") ? "P" : "D") + index + "X";
|
|
sendHandshakeAndLogin(output, username);
|
|
readLoginSuccess(input, username);
|
|
if (packetId(readFrame(input)) != 1) throw new IOException("expected Join Game before PLAY");
|
|
if (packetId(readFrame(input)) != 8) throw new IOException("expected Position and Look before PLAY");
|
|
if (afterTransfer && phase.startsWith("proxy")) {
|
|
PlayerView player = awaitPlayer(proxy, username);
|
|
var transfer = proxy.transfer(player.identity(), "replacement").toCompletableFuture();
|
|
if (packetId(readFrame(input)) != 7 || packetId(readFrame(input)) != 7)
|
|
throw new IOException("expected two Respawns from replacement backend");
|
|
if (packetId(readFrame(input)) != 8)
|
|
throw new IOException("expected Position and Look from replacement backend");
|
|
var result = transfer.get(SETUP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
|
if (result.status() != TransferStatus.NETWORK_READY)
|
|
throw new IOException("replacement transfer failed: " + result.status()
|
|
+ " " + result.detail().orElse(""));
|
|
if (!proxy.find(player.identity()).flatMap(PlayerView::currentServer).orElse("")
|
|
.equals("replacement")) throw new IOException("replacement player view missing");
|
|
}
|
|
exchange(output, input, warmup, null, 0);
|
|
echoes = 0; correct = 0; errors = 0; // Warmup is validated, then omitted from reported phase counts.
|
|
latencies = new long[messages];
|
|
prepared.countDown();
|
|
begin.await();
|
|
exchange(output, input, messages, latencies, warmup);
|
|
} catch (Exception problem) {
|
|
failure = problem;
|
|
if (latencies == null) prepared.countDown();
|
|
closeClients(peers);
|
|
} finally { finished.countDown(); }
|
|
}
|
|
|
|
private void exchange(DataOutputStream output, DataInputStream input, int count, long[] samples,
|
|
int firstSequence) throws Exception {
|
|
long[] outstandingSince = new long[Math.min(window, Math.max(1, count))];
|
|
int sent = 0;
|
|
int received = 0;
|
|
while (received < count) {
|
|
while (sent < count && sent - received < outstandingSince.length) {
|
|
int sequence = firstSequence + sent;
|
|
writeSequence(playFrame, playFrame.length - playPayload.length, sequence);
|
|
outstandingSince[sent % outstandingSince.length] = System.nanoTime();
|
|
output.write(playFrame);
|
|
output.flush();
|
|
sent++;
|
|
}
|
|
byte[] echo = readFrame(input);
|
|
if (samples != null) samples[received] = System.nanoTime() - outstandingSince[received % outstandingSince.length];
|
|
echoes++;
|
|
int expectedSequence = firstSequence + received;
|
|
try {
|
|
verifyEcho(playPayload, echo, expectedSequence);
|
|
correct++;
|
|
} catch (IOException mismatch) {
|
|
errors++;
|
|
throw new IOException("echo validation failed at roundtrip " + received + ": "
|
|
+ mismatch.getMessage(), mismatch);
|
|
}
|
|
received++;
|
|
}
|
|
}
|
|
|
|
void closeSocket() {
|
|
Socket current = socket;
|
|
if (current != null) try { current.close(); } catch (IOException ignored) { }
|
|
}
|
|
}
|
|
|
|
private static PlayerView awaitPlayer(ProxySessionListener proxy, String username) throws Exception {
|
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(SETUP_TIMEOUT_SECONDS);
|
|
while (System.nanoTime() < deadline) {
|
|
for (PlayerView player : proxy.online()) {
|
|
if (player.username().equals(username)) return player;
|
|
}
|
|
Thread.sleep(5);
|
|
}
|
|
throw new IOException("proxy did not publish " + username);
|
|
}
|
|
|
|
private static void writeSequence(byte[] frame, int bodyOffset, int sequence) {
|
|
// A fixed-width, big-endian sequence field follows the PLAY packet ID in each body.
|
|
frame[bodyOffset + 1] = (byte) (sequence >>> 24);
|
|
frame[bodyOffset + 2] = (byte) (sequence >>> 16);
|
|
frame[bodyOffset + 3] = (byte) (sequence >>> 8);
|
|
frame[bodyOffset + 4] = (byte) sequence;
|
|
}
|
|
|
|
private static void verifyEcho(byte[] expectedPayload, byte[] echo, int expectedSequence) throws IOException {
|
|
if (echo.length != expectedPayload.length) throw new IOException("length " + echo.length
|
|
+ " expected " + expectedPayload.length);
|
|
if ((echo[0] & 0xff) != 0x03) throw new IOException("packet id " + (echo[0] & 0xff) + " expected 3");
|
|
int actualSequence = ((echo[1] & 0xff) << 24) | ((echo[2] & 0xff) << 16)
|
|
| ((echo[3] & 0xff) << 8) | (echo[4] & 0xff);
|
|
if (actualSequence != expectedSequence) throw new IOException("sequence " + actualSequence
|
|
+ " expected " + expectedSequence);
|
|
for (int i = 5; i < echo.length; i++) {
|
|
if (echo[i] != expectedPayload[i]) throw new IOException("payload byte " + i + " mismatch");
|
|
}
|
|
}
|
|
|
|
private static final class FakeBackend implements AutoCloseable {
|
|
private final ServerSocket server = new ServerSocket();
|
|
private final ExecutorService clients;
|
|
private final int entityId;
|
|
private final List<Socket> active = java.util.Collections.synchronizedList(new ArrayList<>());
|
|
private volatile boolean closed;
|
|
private Thread acceptThread;
|
|
|
|
FakeBackend(int concurrency, int entityId) throws IOException {
|
|
this.entityId = entityId;
|
|
clients = Executors.newFixedThreadPool(concurrency);
|
|
server.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), concurrency * 2);
|
|
}
|
|
int port() { return server.getLocalPort(); }
|
|
void start() {
|
|
acceptThread = new Thread(() -> {
|
|
while (!closed) {
|
|
try {
|
|
Socket socket = server.accept();
|
|
socket.setTcpNoDelay(true);
|
|
socket.setSoTimeout(IO_TIMEOUT_MILLIS);
|
|
active.add(socket);
|
|
clients.execute(() -> serve(socket));
|
|
} catch (IOException failure) { if (!closed) close(); }
|
|
}
|
|
}, "synthetic-mc-1710-backend-accept");
|
|
acceptThread.setDaemon(true);
|
|
acceptThread.start();
|
|
}
|
|
|
|
private void serve(Socket socket) {
|
|
try (Socket ignored = socket) {
|
|
DataInputStream input = new DataInputStream(socket.getInputStream());
|
|
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
|
|
readFrame(input); // Handshake.
|
|
byte[] login = readFrame(input);
|
|
ByteArrayInputStream loginBody = new ByteArrayInputStream(login);
|
|
if (readVarInt(loginBody) != 0) throw new IOException("expected Login Start");
|
|
String username = readString(loginBody, 16);
|
|
UUID id = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
|
|
ByteArrayOutputStream success = new ByteArrayOutputStream();
|
|
writeVarInt(success, 2);
|
|
writeString(success, id.toString());
|
|
writeString(success, username);
|
|
writeFrame(output, success.toByteArray());
|
|
writeFrame(output, joinGame(entityId));
|
|
writeFrame(output, positionAndLook());
|
|
while (!closed && !socket.isClosed()) {
|
|
byte[] body = readFrame(input);
|
|
writeFrame(output, body);
|
|
}
|
|
} catch (EOFException ignored) { }
|
|
catch (IOException ignored) { }
|
|
finally { active.remove(socket); }
|
|
}
|
|
|
|
@Override public void close() {
|
|
if (closed) return;
|
|
closed = true;
|
|
try { server.close(); } catch (IOException ignored) { }
|
|
synchronized (active) { for (Socket socket : active) try { socket.close(); } catch (IOException ignored) { } }
|
|
clients.shutdownNow();
|
|
try { clients.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
|
if (acceptThread != null) try { acceptThread.join(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
|
}
|
|
}
|
|
|
|
private static byte[] joinGame(int entityId) throws IOException {
|
|
ByteArrayOutputStream packet = new ByteArrayOutputStream();
|
|
writeVarInt(packet, 1);
|
|
DataOutputStream out = new DataOutputStream(packet);
|
|
out.writeInt(entityId); out.writeByte(0); out.writeByte(0); out.writeByte(0); out.writeByte(20);
|
|
writeString(packet, "default");
|
|
return packet.toByteArray();
|
|
}
|
|
|
|
private static byte[] positionAndLook() throws IOException {
|
|
ByteArrayOutputStream packet = new ByteArrayOutputStream();
|
|
writeVarInt(packet, 8);
|
|
DataOutputStream out = new DataOutputStream(packet);
|
|
out.writeDouble(0.5); out.writeDouble(64.0); out.writeDouble(0.5);
|
|
out.writeFloat(0.0f); out.writeFloat(0.0f); out.writeByte(0);
|
|
return packet.toByteArray();
|
|
}
|
|
|
|
private static void sendHandshakeAndLogin(DataOutputStream output, String username) throws IOException {
|
|
ByteArrayOutputStream handshake = new ByteArrayOutputStream();
|
|
writeVarInt(handshake, 0); writeVarInt(handshake, 5); writeString(handshake, "localhost");
|
|
handshake.write(0); handshake.write(255); writeVarInt(handshake, 2);
|
|
writeFrame(output, handshake.toByteArray());
|
|
ByteArrayOutputStream login = new ByteArrayOutputStream();
|
|
writeVarInt(login, 0); writeString(login, username);
|
|
writeFrame(output, login.toByteArray());
|
|
}
|
|
|
|
private static void readLoginSuccess(DataInputStream input, String username) throws IOException {
|
|
ByteArrayInputStream success = new ByteArrayInputStream(readFrame(input));
|
|
if (readVarInt(success) != 2) throw new IOException("expected Login Success");
|
|
readString(success, 36);
|
|
if (!username.equals(readString(success, 16))) throw new IOException("Login Success username mismatch");
|
|
}
|
|
|
|
private static int packetId(byte[] frame) throws IOException { return readVarInt(new ByteArrayInputStream(frame)); }
|
|
|
|
private static byte[] frame(byte[] body) throws IOException {
|
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
writeVarInt(out, body.length); out.write(body);
|
|
return out.toByteArray();
|
|
}
|
|
|
|
private static byte[] readFrame(DataInputStream input) throws IOException {
|
|
int length = readVarInt(input);
|
|
if (length < 1 || length > 1_048_576) throw new IOException("invalid frame length " + length);
|
|
byte[] body = new byte[length]; input.readFully(body); return body;
|
|
}
|
|
|
|
private static void writeFrame(DataOutputStream output, byte[] body) throws IOException {
|
|
writeVarInt(output, body.length); output.write(body); output.flush();
|
|
}
|
|
|
|
private static int readVarInt(java.io.InputStream input) throws IOException {
|
|
int result = 0;
|
|
for (int shift = 0; shift < 35; shift += 7) {
|
|
int next = input.read();
|
|
if (next < 0) throw new EOFException();
|
|
result |= (next & 0x7f) << shift;
|
|
if ((next & 0x80) == 0) return result;
|
|
}
|
|
throw new IOException("overlong VarInt");
|
|
}
|
|
|
|
private static void writeVarInt(java.io.OutputStream output, int value) throws IOException {
|
|
do {
|
|
int part = value & 0x7f;
|
|
value >>>= 7;
|
|
output.write(value == 0 ? part : part | 0x80);
|
|
} while (value != 0);
|
|
}
|
|
|
|
private static String readString(ByteArrayInputStream input, int maxChars) throws IOException {
|
|
int length = readVarInt(input);
|
|
if (length < 0 || length > maxChars * 4) throw new IOException("invalid string length");
|
|
byte[] bytes = input.readNBytes(length);
|
|
if (bytes.length != length) throw new EOFException();
|
|
String value = new String(bytes, StandardCharsets.UTF_8);
|
|
if (value.length() > maxChars) throw new IOException("string exceeds character limit");
|
|
return value;
|
|
}
|
|
|
|
private static void writeString(ByteArrayOutputStream output, String value) throws IOException {
|
|
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
|
writeVarInt(output, bytes.length); output.write(bytes);
|
|
}
|
|
|
|
private static long percentile(long[] sorted, double quantile) {
|
|
return sorted[Math.min(sorted.length - 1, (int) Math.ceil(quantile * sorted.length) - 1)];
|
|
}
|
|
|
|
private static int intArg(String[] args, String key, int fallback) {
|
|
int found = fallback;
|
|
boolean seen = false;
|
|
List<String> known = List.of("connections", "messages", "warmup", "payload", "repeats", "window",
|
|
"mode", "command-interceptor");
|
|
for (int i = 0; i < args.length; i++) {
|
|
if (args[i].startsWith("--") && !known.contains(args[i].substring(2)))
|
|
throw new IllegalArgumentException("unknown option " + args[i]);
|
|
if (args[i].equals("--" + key)) {
|
|
if (seen || i + 1 >= args.length) throw new IllegalArgumentException("missing or duplicate --" + key);
|
|
found = Integer.parseInt(args[++i]);
|
|
seen = true;
|
|
} else if (args[i].startsWith("--")) {
|
|
if (i + 1 >= args.length) throw new IllegalArgumentException("missing value for " + args[i]);
|
|
i++;
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
private static String modeArg(String[] args) {
|
|
String mode = "initial";
|
|
boolean seen = false;
|
|
for (int i = 0; i < args.length; i++) {
|
|
if (args[i].equals("--mode")) {
|
|
if (seen || i + 1 >= args.length) throw new IllegalArgumentException("missing or duplicate --mode");
|
|
mode = args[++i];
|
|
seen = true;
|
|
} else if (args[i].startsWith("--")) i++;
|
|
}
|
|
if (!mode.equals("initial") && !mode.equals("post-transfer"))
|
|
throw new IllegalArgumentException("mode must be initial or post-transfer");
|
|
return mode;
|
|
}
|
|
}
|