proto-socket/dart/bench/stress.dart
toki f6234bd83a perf(socket): 성능 병목 개선을 반영한다
Dart TCP/heartbeat, Python sustained sampling, TypeScript WS 1MB 경로의 성능 병목을 분리하고 검증 산출물을 함께 남긴다.
2026-06-06 12:47:15 +09:00

816 lines
27 KiB
Dart

/// Same-language Dart stress / benchmark harness.
///
/// 고성능 병렬 운용 기준선 Milestone의 lang-baseline Task에서, TypeScript stress.ts와 같은 출력
/// 계약(ROW|/SKIP|/SUMMARY|)으로 Dart same-language TCP/WS baseline을 남긴다. 절대 성능 합격선은
/// 고정하지 않고 local 환경 baseline(throughput, p50/p95/p99 latency, peak RSS)을 기록하며, 안정성
/// 합격선만 hard fail로 둔다: timeout 0, nonce mismatch 0, response type mismatch 0, connection별
/// FIFO 위반 0, 종료 후 pending leak 0.
///
/// 실행: dart run bench/stress.dart [--mode=quick|full] [--transport=tcp,ws] [--profiles=a,b]
library;
import 'dart:async';
import 'dart:io';
import 'package:protobuf/protobuf.dart';
import 'package:proto_socket/proto_socket.dart';
const _host = '127.0.0.1';
const _language = 'Dart';
const _wsPath = '/';
const _allProfiles = [
'roundtrip',
'burst',
'sustained',
'parallel',
'slow-mix',
'payload'
];
// payload size matrix 축. quick은 작은 샘플(1KB/64KB), full은 Milestone 기준(1KB/64KB/1MB)을 측정한다.
const _payloadSeed = '0123456789abcdef';
const _payloadSizesQuick = [1024, 65536];
const _payloadSizesFull = [1024, 65536, 1048576];
// Dart heartbeat는 이제 interval 또는 wait가 0이면 "비활성"이다(다른 언어와 동일). baseline
// 측정 중에는 heartbeat timer churn이 끼지 않도록 0으로 비활성화한다.
const _heartbeatSeconds = 0;
int _rowsEmitted = 0;
int _totalViolations = 0;
final _parserMap = <String, GeneratedMessage Function(List<int>)>{
TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer,
};
/// 안정성 위반 카운터. 0이 아니면 해당 row는 FAIL이고 프로세스도 non-zero exit한다.
class _Stability {
int timeouts = 0;
int nonceMismatch = 0;
int typeMismatch = 0;
int fifoViolations = 0;
int pendingLeak = 0;
int violations() =>
timeouts + nonceMismatch + typeMismatch + fifoViolations + pendingLeak;
}
class _TcpClient extends ProtobufClient {
_TcpClient(Socket socket)
: super(socket, _heartbeatSeconds, _heartbeatSeconds, _parserMap);
}
class _WsClient extends WsProtobufClient {
_WsClient(WebSocket ws)
: super(ws, _heartbeatSeconds, _heartbeatSeconds, _parserMap);
}
class _TcpServer extends ProtobufServer {
final void Function(Communicator) onConnected;
_TcpServer(int port, this.onConnected)
: super(_host, port, (socket) => _TcpClient(socket));
@override
void onClientConnected(ProtobufClient client) => onConnected(client);
}
class _WsServer extends WsProtobufServer {
final void Function(Communicator) onConnected;
_WsServer(int port, this.onConnected)
: super(_host, port, (ws) => _WsClient(ws));
@override
void onClientConnected(WsProtobufClient client) => onConnected(client);
}
class _ClientHandle {
final Communicator comm;
final Future<void> Function() close;
_ClientHandle(this.comm, this.close);
}
abstract class _ServerHandle {
Future<void> start();
Future<void> stop();
}
class _ServerHandleImpl implements _ServerHandle {
final Future Function() _start;
final Future Function() _stop;
_ServerHandleImpl(this._start, this._stop);
@override
Future<void> start() async => _start();
@override
Future<void> stop() async => _stop();
}
void _log(String line) => stderr.writeln(line);
double _memMb() => ProcessInfo.currentRss / (1024 * 1024);
Future<int> _freePort() async {
final socket = await ServerSocket.bind(_host, 0);
final port = socket.port;
await socket.close();
return port;
}
int _testDataPayloadBytes(String message) => (TestData()
..index = 0
..message = message)
.writeToBuffer()
.length;
/// size 바이트 길이의 deterministic filler string을 만든다.
String _payloadFiller(int size) {
if (size <= 0) return '';
final reps = (size + _payloadSeed.length - 1) ~/ _payloadSeed.length;
return (_payloadSeed * reps).substring(0, size);
}
/// payload 크기를 사람이 읽는 축 이름으로 바꾼다(1024 -> 1KB, 1048576 -> 1MB).
String _payloadLabel(int bytes) {
if (bytes >= 1048576 && bytes % 1048576 == 0) return '${bytes ~/ 1048576}MB';
if (bytes >= 1024 && bytes % 1024 == 0) return '${bytes ~/ 1024}KB';
return '${bytes}B';
}
double _percentile(List<double> sorted, double p) {
if (sorted.isEmpty) return 0;
var rank = ((p / 100.0) * sorted.length).ceil() - 1;
if (rank < 0) rank = 0;
if (rank > sorted.length - 1) rank = sorted.length - 1;
return sorted[rank];
}
void _classifyRequestError(String message, _Stability s) {
if (message.contains('timeout') || message.contains('TimeoutException')) {
s.timeouts += 1;
} else if (message.contains('type mismatch')) {
s.typeMismatch += 1;
} else {
s.nonceMismatch += 1;
}
}
void _emitRow({
required String profile,
required String axis,
required String transport,
required int payloadBytes,
required int clientCount,
required int requests,
required double throughput,
required double p50,
required double p95,
required double p99,
required double mem,
required _Stability s,
}) {
final violations = s.violations();
final status = violations == 0 ? 'PASS' : 'FAIL';
_totalViolations += violations;
_rowsEmitted += 1;
final fields = [
'ROW',
profile,
axis,
_language,
transport,
'$payloadBytes',
'$clientCount',
'$requests',
throughput.toStringAsFixed(1),
p50.toStringAsFixed(3),
p95.toStringAsFixed(3),
p99.toStringAsFixed(3),
'${s.timeouts}',
'${s.nonceMismatch}',
'${s.typeMismatch}',
'${s.fifoViolations}',
'${s.pendingLeak}',
'0', // queueBacklog: same-language baseline에는 inbound gateway가 없다.
'0', // gatewayBacklog
'off',
mem.toStringAsFixed(1),
status,
];
print(fields.join('|'));
}
void _emitSkip(String profile, String axis, String transport, String reason) {
print('SKIP|$profile|$axis|$_language|$transport|$reason');
}
void _summarize(
String profile,
String axis,
String transport,
List<double> latencies,
double elapsedMs,
int clientCount,
_Stability s,
double mem) {
final sorted = List<double>.from(latencies)..sort();
final throughput =
elapsedMs > 0 ? latencies.length / elapsedMs * 1000.0 : 0.0;
_emitRow(
profile: profile,
axis: axis,
transport: transport,
payloadBytes: _testDataPayloadBytes('req-0'),
clientCount: clientCount,
requests: latencies.length,
throughput: throughput,
p50: _percentile(sorted, 50),
p95: _percentile(sorted, 95),
p99: _percentile(sorted, 99),
mem: mem,
s: s,
);
}
void _emitThroughput(
String profile,
String axis,
String transport,
int count,
double elapsedMs,
int clientCount,
int payloadBytes,
_Stability s,
double mem) {
final throughput = elapsedMs > 0 ? count / elapsedMs * 1000.0 : 0.0;
_emitRow(
profile: profile,
axis: axis,
transport: transport,
payloadBytes: payloadBytes,
clientCount: clientCount,
requests: count,
throughput: throughput,
p50: 0,
p95: 0,
p99: 0,
mem: mem,
s: s,
);
}
_ServerHandle _makeServer(
String transport, int port, void Function(Communicator) onConnected) {
if (transport == 'tcp') {
final server = _TcpServer(port, onConnected);
return _ServerHandleImpl(server.start, server.stop);
} else if (transport == 'ws') {
final server = _WsServer(port, onConnected);
return _ServerHandleImpl(server.start, server.stop);
}
throw ArgumentError('unknown transport "$transport"');
}
// request-response 축에서는 client가 concurrency C로 동시에 send하므로 frame 도착 순서가 결정적이지
// 않다. 따라서 여기서는 FIFO를 측정하지 않고 응답 정확성/timeout만 본다. per-connection FIFO는 단일
// connection 순차 send인 burst 축에서 검증한다.
_ServerHandle _makeEchoServer(String transport, int port) {
return _makeServer(transport, port, (comm) {
comm.addRequestListener<TestData, TestData>((req) async {
return TestData()
..index = req.index * 2
..message = 'echo:${req.message}';
});
});
}
_ServerHandle _makeSlowMixServer(
String transport, int port, int slowEvery, Duration delay) {
return _makeServer(transport, port, (comm) {
comm.addRequestListener<TestData, TestData>((req) async {
if ((req.index + 1) % slowEvery == 0) {
await Future<void>.delayed(delay);
}
return TestData()
..index = req.index * 2
..message = 'echo:${req.message}';
});
});
}
Future<_ClientHandle> _dial(String transport, int port) async {
if (transport == 'tcp') {
final socket = await ProtobufClient.connect(_host, port)
.timeout(const Duration(milliseconds: 300));
final client = _TcpClient(socket);
return _ClientHandle(client, client.close);
} else if (transport == 'ws') {
final ws = await WsProtobufClient.connect(_host, port, path: _wsPath)
.timeout(const Duration(milliseconds: 300));
final client = _WsClient(ws);
return _ClientHandle(client, client.close);
}
throw ArgumentError('unknown transport "$transport"');
}
Future<_ClientHandle> _dialWithRetry(String transport, int port) async {
final deadline = DateTime.now().add(const Duration(seconds: 3));
Object? lastError;
while (DateTime.now().isBefore(deadline)) {
try {
return await _dial(transport, port);
} catch (error) {
lastError = error;
await Future<void>.delayed(const Duration(milliseconds: 50));
}
}
throw TimeoutException('connect $transport:$port timed out: $lastError');
}
Future<List<double>> _runRequestLoad(_ClientHandle handle, int total,
int concurrency, Duration timeout, _Stability s) async {
final latencies = <double>[];
var issued = 0;
var nextIndex = 0;
while (issued < total) {
final batchSize =
concurrency < total - issued ? concurrency : total - issued;
final futures = <Future<void>>[];
for (var i = 0; i < batchSize; i++) {
final index = nextIndex++;
futures.add(() async {
final sw = Stopwatch()..start();
try {
final res = await handle.comm
.sendRequest<TestData, TestData>(TestData()
..index = index
..message = 'req-$index')
.timeout(timeout);
latencies.add(sw.elapsedMicroseconds / 1000.0);
if (res.index != index * 2 || res.message != 'echo:req-$index') {
s.nonceMismatch += 1;
}
} catch (error) {
_classifyRequestError(error.toString(), s);
}
}());
}
issued += batchSize;
await Future.wait(futures);
}
return latencies;
}
Future<void> _profileRoundtrip(String transport, String mode) async {
const concurrencies = [1, 16, 64, 256];
final batches = mode == 'quick' ? 2 : 20;
final timeout = Duration(seconds: mode == 'quick' ? 5 : 15);
_log(
'[roundtrip] transport=$transport mode=$mode concurrencies=1,16,64,256 batches/level=$batches');
for (final concurrency in concurrencies) {
final s = _Stability();
final total = concurrency * batches;
final port = await _freePort();
final server = _makeEchoServer(transport, port);
await server.start();
try {
final handle = await _dialWithRetry(transport, port);
try {
final sw = Stopwatch()..start();
final latencies =
await _runRequestLoad(handle, total, concurrency, timeout, s);
_summarize('roundtrip', 'concurrency=$concurrency', transport,
latencies, sw.elapsedMicroseconds / 1000.0, 1, s, _memMb());
} finally {
await handle.close();
if (handle.comm.isAlive) s.pendingLeak += 1;
}
} catch (error) {
s.timeouts += 1;
_log('[roundtrip] concurrency=$concurrency error=$error');
_summarize('roundtrip', 'concurrency=$concurrency', transport, [], 0, 1,
s, _memMb());
} finally {
await server.stop();
}
_log(
'[roundtrip] transport=$transport concurrency=$concurrency done violations=${s.violations()}');
}
}
Future<void> _profileBurst(String transport, String mode) async {
final counts = mode == 'quick' ? [200] : [1000, 10000, 100000];
_log('[burst] transport=$transport mode=$mode counts=$counts');
for (final count in counts) {
final s = _Stability();
var received = 0;
var lastIndex = -1;
final done = Completer<void>();
final port = await _freePort();
final server = _makeServer(transport, port, (comm) {
comm.addListener<TestData>((data) {
if (data.index <= lastIndex) s.fifoViolations += 1;
lastIndex = data.index;
received += 1;
if (received >= count && !done.isCompleted) done.complete();
});
});
await server.start();
_ClientHandle? handle;
try {
handle = await _dialWithRetry(transport, port);
final sw = Stopwatch()..start();
for (var i = 0; i < count; i++) {
await handle.comm.send(TestData()
..index = i
..message = 'b-$i');
}
try {
await done.future.timeout(Duration(seconds: mode == 'quick' ? 10 : 60));
} on TimeoutException {
s.timeouts += 1;
_log('[burst] count=$count dispatch timeout received=$received');
}
if (received != count) s.pendingLeak += 1;
_emitThroughput(
'burst',
'count=$count',
transport,
count,
sw.elapsedMicroseconds / 1000.0,
1,
_testDataPayloadBytes('b-0'),
s,
_memMb());
} catch (error) {
s.timeouts += 1;
_log('[burst] count=$count error=$error');
_emitThroughput('burst', 'count=$count', transport, received, 0, 1,
_testDataPayloadBytes('b-0'), s, _memMb());
} finally {
if (handle != null) {
await handle.close();
if (handle.comm.isAlive) s.pendingLeak += 1;
}
await server.stop();
}
_log(
'[burst] transport=$transport count=$count received=$received violations=${s.violations()}');
}
}
Future<void> _profileSustained(String transport, String mode) async {
final durationsMs = mode == 'quick' ? [2000] : [30000, 300000, 1800000];
const concurrency = 16;
const timeout = Duration(seconds: 15);
_log(
'[sustained] transport=$transport mode=$mode durations=${durationsMs.join(",")}ms concurrency=$concurrency');
for (final durationMs in durationsMs) {
final s = _Stability();
var peak = _memMb();
final port = await _freePort();
final server = _makeEchoServer(transport, port);
await server.start();
try {
final handle = await _dialWithRetry(transport, port);
final latencies = <double>[];
var nextIndex = 0;
final deadline = DateTime.now().add(Duration(milliseconds: durationMs));
try {
while (DateTime.now().isBefore(deadline)) {
final futures = <Future<void>>[];
for (var i = 0; i < concurrency; i++) {
final index = nextIndex++;
futures.add(() async {
final sw = Stopwatch()..start();
try {
final res = await handle.comm
.sendRequest<TestData, TestData>(TestData()
..index = index
..message = 's-$index')
.timeout(timeout);
latencies.add(sw.elapsedMicroseconds / 1000.0);
if (res.index != index * 2 || res.message != 'echo:s-$index') {
s.nonceMismatch += 1;
}
} catch (error) {
_classifyRequestError(error.toString(), s);
}
}());
}
await Future.wait(futures);
final cur = _memMb();
if (cur > peak) peak = cur;
}
_summarize('sustained', 'duration=${durationMs}ms', transport,
latencies, durationMs.toDouble(), 1, s, peak);
} finally {
await handle.close();
if (handle.comm.isAlive) s.pendingLeak += 1;
}
} catch (error) {
s.timeouts += 1;
_log('[sustained] duration=${durationMs}ms error=$error');
_summarize('sustained', 'duration=${durationMs}ms', transport, [], 0, 1,
s, peak);
} finally {
await server.stop();
}
_log(
'[sustained] transport=$transport duration=${durationMs}ms done peakMemMb=${peak.toStringAsFixed(1)} violations=${s.violations()}');
}
}
Future<void> _profileParallel(String transport, String mode) async {
final clientCounts = mode == 'quick' ? [4] : [16, 128, 512, 1024];
final perClient = mode == 'quick' ? 50 : 500;
const concurrency = 8;
const timeout = Duration(seconds: 15);
_log(
'[parallel] transport=$transport mode=$mode clients=$clientCounts perClient=$perClient');
for (final clientCount in clientCounts) {
final s = _Stability();
final port = await _freePort();
final server = _makeEchoServer(transport, port);
await server.start();
final handles = <_ClientHandle>[];
try {
for (var i = 0; i < clientCount; i++) {
try {
handles.add(await _dialWithRetry(transport, port));
} catch (error) {
s.timeouts += 1;
_log('[parallel] clients=$clientCount dial error=$error');
}
}
final allLatencies = <double>[];
final sw = Stopwatch()..start();
await Future.wait(handles.map((handle) async {
allLatencies.addAll(
await _runRequestLoad(handle, perClient, concurrency, timeout, s));
}));
_summarize('parallel', 'clients=$clientCount', transport, allLatencies,
sw.elapsedMicroseconds / 1000.0, clientCount, s, _memMb());
} finally {
for (final handle in handles) {
await handle.close();
if (handle.comm.isAlive) s.pendingLeak += 1;
}
await server.stop();
}
_log(
'[parallel] transport=$transport clients=$clientCount done violations=${s.violations()}');
}
}
/// 빠른/느린 request handler를 섞어 same-connection FIFO delay와 connection 간 nonce 독립성을 검증한다.
Future<void> _profileSlowMix(String transport, String mode) async {
final clientCount = mode == 'quick' ? 4 : 16;
final perClient = mode == 'quick' ? 20 : 200;
final slowEvery = mode == 'quick' ? 5 : 10;
final delayMs = mode == 'quick' ? 50 : 100;
final timeout = Duration(seconds: mode == 'quick' ? 10 : 30);
final axis = 'clients=$clientCount,slowEvery=$slowEvery,delayMs=$delayMs';
final s = _Stability();
var peak = _memMb();
final latencies = <double>[];
var elapsedMs = 0.0;
_log('[slow-mix] transport=$transport mode=$mode $axis perClient=$perClient');
final port = await _freePort();
final server = _makeSlowMixServer(
transport, port, slowEvery, Duration(milliseconds: delayMs));
await server.start();
final handles = <_ClientHandle>[];
final clientIds = <int>[];
try {
for (var clientId = 0; clientId < clientCount; clientId++) {
try {
handles.add(await _dialWithRetry(transport, port));
clientIds.add(clientId);
} catch (error) {
s.timeouts += 1;
_log('[slow-mix] client=$clientId dial error=$error');
}
}
final completedByClient = List<int>.filled(clientCount, -1);
final sw = Stopwatch()..start();
await Future.wait(List.generate(handles.length, (slot) async {
final handle = handles[slot];
final clientId = clientIds[slot];
await Future.wait(List.generate(perClient, (index) async {
final message = 'sm-$clientId-$index';
final reqSw = Stopwatch()..start();
try {
final res = await handle.comm
.sendRequest<TestData, TestData>(TestData()
..index = index
..message = message)
.timeout(timeout);
latencies.add(reqSw.elapsedMicroseconds / 1000.0);
if (res.index != index * 2 || res.message != 'echo:$message') {
s.nonceMismatch += 1;
}
if (index <= completedByClient[clientId]) {
s.fifoViolations += 1;
}
completedByClient[clientId] = index;
} catch (error) {
_classifyRequestError(error.toString(), s);
}
}));
}));
final cur = _memMb();
if (cur > peak) peak = cur;
elapsedMs = sw.elapsedMicroseconds / 1000.0;
} finally {
for (final handle in handles) {
await handle.close();
if (handle.comm.isAlive) s.pendingLeak += 1;
}
await server.stop();
}
final sorted = List<double>.from(latencies)..sort();
final throughput =
elapsedMs > 0 ? latencies.length / elapsedMs * 1000.0 : 0.0;
_emitRow(
profile: 'slow-mix',
axis: axis,
transport: transport,
payloadBytes: _testDataPayloadBytes('sm-0-0'),
clientCount: clientCount,
requests: latencies.length,
throughput: throughput,
p50: _percentile(sorted, 50),
p95: _percentile(sorted, 95),
p99: _percentile(sorted, 99),
mem: peak,
s: s,
);
_log('[slow-mix] transport=$transport clients=$clientCount done '
'peakMemMb=${peak.toStringAsFixed(1)} violations=${s.violations()}');
}
/// payload size matrix. 동일 connection request-response를 1KB/64KB/1MB payload에서 측정한다.
/// message field를 deterministic filler로 채우고 실제 serialized payload bytes, payload별
/// p50/p95/p99 latency, throughput, peak RSS, stability counters를 결과 row에 남긴다.
Future<void> _profilePayload(String transport, String mode) async {
final sizes = mode == 'quick' ? _payloadSizesQuick : _payloadSizesFull;
final concurrency = mode == 'quick' ? 4 : 8;
final requests = mode == 'quick' ? 20 : 100;
final timeout = Duration(seconds: mode == 'quick' ? 10 : 30);
_log('[payload] transport=$transport mode=$mode '
'sizes=${sizes.map(_payloadLabel).toList()} concurrency=$concurrency requests/size=$requests');
for (final size in sizes) {
final s = _Stability();
final filler = _payloadFiller(size);
final expectedEcho = 'echo:$filler';
final payloadBytes = _testDataPayloadBytes(filler);
final axis = 'payload=${_payloadLabel(size)}';
var peak = _memMb();
final port = await _freePort();
final server = _makeEchoServer(transport, port);
await server.start();
try {
final handle = await _dialWithRetry(transport, port);
try {
final latencies = <double>[];
var issued = 0;
var nextIndex = 0;
final sw = Stopwatch()..start();
while (issued < requests) {
final batchSize =
concurrency < requests - issued ? concurrency : requests - issued;
final futures = <Future<void>>[];
for (var i = 0; i < batchSize; i++) {
final index = nextIndex++;
futures.add(() async {
final reqSw = Stopwatch()..start();
try {
final res = await handle.comm
.sendRequest<TestData, TestData>(TestData()
..index = index
..message = filler)
.timeout(timeout);
latencies.add(reqSw.elapsedMicroseconds / 1000.0);
if (res.index != index * 2 || res.message != expectedEcho) {
s.nonceMismatch += 1;
}
} catch (error) {
_classifyRequestError(error.toString(), s);
}
}());
}
issued += batchSize;
await Future.wait(futures);
final cur = _memMb();
if (cur > peak) peak = cur;
}
final elapsedMs = sw.elapsedMicroseconds / 1000.0;
final sorted = List<double>.from(latencies)..sort();
final throughput =
elapsedMs > 0 ? latencies.length / elapsedMs * 1000.0 : 0.0;
_emitRow(
profile: 'payload',
axis: axis,
transport: transport,
payloadBytes: payloadBytes,
clientCount: 1,
requests: latencies.length,
throughput: throughput,
p50: _percentile(sorted, 50),
p95: _percentile(sorted, 95),
p99: _percentile(sorted, 99),
mem: peak,
s: s,
);
} finally {
await handle.close();
if (handle.comm.isAlive) s.pendingLeak += 1;
}
} catch (error) {
s.timeouts += 1;
_log('[payload] size=${_payloadLabel(size)} error=$error');
_emitRow(
profile: 'payload',
axis: axis,
transport: transport,
payloadBytes: payloadBytes,
clientCount: 1,
requests: 0,
throughput: 0,
p50: 0,
p95: 0,
p99: 0,
mem: peak,
s: s,
);
} finally {
await server.stop();
}
_log(
'[payload] transport=$transport size=${_payloadLabel(size)} bytes=$payloadBytes '
'peakMemMb=${peak.toStringAsFixed(1)} violations=${s.violations()}');
}
}
final _runners = <String, Future<void> Function(String, String)>{
'roundtrip': _profileRoundtrip,
'burst': _profileBurst,
'sustained': _profileSustained,
'parallel': _profileParallel,
'slow-mix': _profileSlowMix,
'payload': _profilePayload,
};
List<String> _parseList(List<String> args, String prefix, List<String> def) {
for (final arg in args) {
if (arg.startsWith(prefix)) {
final items = arg
.substring(prefix.length)
.split(',')
.map((v) => v.trim())
.where((v) => v.isNotEmpty)
.toList();
if (items.isNotEmpty) return items;
}
}
return def;
}
Future<void> main(List<String> args) async {
var mode = 'quick';
for (final arg in args) {
if (arg == '--full' || arg == '--mode=full') mode = 'full';
if (arg == '--quick' || arg == '--mode=quick') mode = 'quick';
}
var transports = _parseList(args, '--transport=', ['tcp', 'ws']);
transports = _parseList(args, '--transports=', transports);
var profiles = _parseList(args, '--profiles=', _allProfiles);
profiles = _parseList(args, '--profile=', profiles);
_log('INFO stress harness language=$_language mode=$mode '
'transports=${transports.join(',')} profiles=${profiles.join(',')} '
'typeName=${TestData.getDefault().info_.qualifiedMessageName}');
for (final transport in transports) {
if (transport != 'tcp' && transport != 'ws') {
_emitSkip('all', 'transport', transport,
'unsupported transport for Dart same-language baseline');
continue;
}
for (final profile in profiles) {
final runner = _runners[profile];
if (runner == null) {
_emitSkip(profile, 'profile', transport,
'profile not part of Dart same-language baseline (gateway is TypeScript-specific)');
continue;
}
await runner(transport, mode);
}
}
final status = _totalViolations == 0 ? 'PASS' : 'FAIL';
print('SUMMARY|status=$status|language=$_language|mode=$mode|'
'transports=${transports.join(',')}|profiles=${profiles.join(',')}|'
'rows=$_rowsEmitted|stability_violations=$_totalViolations');
if (status != 'PASS') {
exitCode = 1;
}
}