feat(core): sends return at once and go through Java's transport thread

A send hands its frame to the card and returns, as in Java; the frames
travel while the clock advances. Swift's send ran the network until every
frame had arrived; the option swiftSends keeps that for the Swift
comparison. Waiting calls (TCP connect, acknowledgement) now run event by
event until the answer instead of to the timeout.

UDP and TCP send through a thread per node and protocol (Java's
TransportProtokoll): one segment after the other; while IP waits for ARP,
the next send blocks and a segment from an event queues behind it. Senders
that wait take their turns in the order they came (a fair lock; Java's
leaves the order to chance).

All four ArpRun stories now match Java FILIUS, including when each call
returns. Tests let frames arrive explicitly with letFramesArrive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 17:49:05 +00:00
co-authored by Claude Opus 5.5
parent dbbc1fd70d
commit a88fa1512c
7 changed files with 343 additions and 71 deletions
+46 -31
View File
@@ -3,7 +3,7 @@
// card saw and when each call started and ended (arp-java-<story>.java.json).
// The engine makes the same calls: the frames must come in the same order,
// with the same content, at about the same distance from each other
// (javaRun.ts), the calls must return the same, after about as long.
// (javaRun.ts); the calls must end at about the same times and return the same.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
@@ -27,28 +27,41 @@ function pingResult(result: IPv4DeliveryResult, answered: boolean): string {
return answered ? 'ICMP 0/0' : 'null';
}
const STORIES: Record<string, (engine: NetworkRuntimeEngine) => string> = {
pingLan: (engine) => pingResult(engine.sendIcmpEcho(HOST, '10.0.0.9'), false),
pingRouted: (engine) => {
const result = engine.sendIcmpEcho(HOST, '20.0.0.9');
return pingResult(result, result.type === 'delivered' && result.nodeId !== HOST);
/** Records a call as ArpRun does: when it started, when it ended and what it returned. */
type Call = (label: string, run: () => string) => void;
function udpSend(engine: NetworkRuntimeEngine, target: string, payload: string): string {
const socket = engine.bindUdpSocket(HOST, { remoteIPAddress: target, remotePort: 9 })!;
engine.sendUdp(socket, new TextEncoder().encode(payload));
return '';
}
const STORIES: Record<string, (engine: NetworkRuntimeEngine, call: Call) => void> = {
pingLan: (engine, call) => call('sendProbe 10.0.0.9', () => pingResult(engine.sendIcmpEcho(HOST, '10.0.0.9'), false)),
pingRouted: (engine, call) =>
call('sendProbe 20.0.0.9', () => {
const result = engine.sendIcmpEcho(HOST, '20.0.0.9');
return pingResult(result, result.type === 'delivered' && result.nodeId !== HOST);
}),
udpTwice: (engine, call) => {
call('senden eins', () => udpSend(engine, '10.0.0.9', 'eins'));
call('senden zwei', () => udpSend(engine, '10.0.0.9', 'zwei'));
},
udpBehind: (engine, call) => {
call('senden verloren', () => udpSend(engine, '10.0.0.9', 'verloren'));
call('senden dahinter', () => udpSend(engine, '20.0.0.2', 'dahinter'));
},
udpTwice: () => '',
udpBehind: () => '',
};
/**
* Stories the engine does not play like Java yet, with the reason; each must
* still differ from Java.
*
* udpTwice, udpBehind: Java's transport thread sends one segment after the
* other and holds its queue while ARP waits, so the next send blocks — the
* engine's threads come in the next step.
*/
const NOT_YET_LIKE_JAVA: Record<string, string> = {
udpTwice: "Java's transport thread: the second datagram waits for the first one's ARP",
udpBehind: "Java's transport thread: a datagram to a reachable host waits behind one whose ARP fails",
};
/** The client's free port as "client": Java draws it at random, the engine from its seed. */
function withClientPort<T extends { text: string }>(frames: readonly T[]): T[] {
return frames.map((frame) => ({ ...frame, text: frame.text.replace(/:(\d{5})\b/g, (whole, port: string) => (Number(port) >= 49_152 ? ':client' : whole)) }));
}
/** Java's events without its answer's sender ("from …"), which the engine's results do not name. */
function javaEvent(event: string): string {
return event.replace(/ from .*$/, '');
}
describe('ARP as Java FILIUS runs it', () => {
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'arp-routed.scenario.json'), 'utf8')) as Scenario;
@@ -58,20 +71,22 @@ describe('ARP as Java FILIUS runs it', () => {
const java = JSON.parse(readFileSync(join(scenarioDirectory, `arp-java-${name}.java.json`), 'utf8')) as JavaRun;
const { engine, frames } = watchedEngine(scenario);
engine.handle({ type: 'advance', toMilliseconds: 500 });
const called = engine.state.currentTimeMilliseconds;
const result = story(engine);
const returned = engine.state.currentTimeMilliseconds;
const events: [number, string][] = [];
story(engine, (label, run) => {
events.push([engine.state.currentTimeMilliseconds, `${label} called`]);
const result = run();
events.push([engine.state.currentTimeMilliseconds, `${label} returned ${result}`]);
});
engine.handle({ type: 'advance', toMilliseconds: 16_000 });
const expected = gaps(javaFrames(java.frames));
const actual = gaps(frames());
if (name in NOT_YET_LIKE_JAVA) {
expect(JSON.stringify(actual), NOT_YET_LIKE_JAVA[name]).not.toBe(JSON.stringify(expected));
return;
const expected = gaps(withClientPort(javaFrames(java.frames)));
const actual = gaps(withClientPort(frames()));
expect(events.map(([, event]) => event)).toEqual(java.events.map(([, event]) => javaEvent(event)));
for (const [index, [time]] of events.entries()) {
expect(Math.abs(time - events[0]![0] - (java.events[index]![0] - java.events[0]![0])), `time of ${events[index]![1]}`).toBeLessThanOrEqual(
GAP_TOLERANCE_MILLISECONDS,
);
}
const [[javaCalled], [javaReturned, javaOutcome]] = java.events as [[number, string], [number, string]];
expect(javaOutcome.replace(/^.* returned /, '').replace(/ from .*$/, '')).toBe(result);
expect(Math.abs(returned - called - (javaReturned - javaCalled)), 'duration of the call').toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS);
for (let index = 0; index < Math.max(expected.length, actual.length); index += 1) {
expect(actual[index]?.text, `frame #${index + 1}`).toBe(expected[index]?.text);
expect(Math.abs(actual[index]!.gap - expected[index]!.gap), `gap before frame #${index + 1}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS);
+152 -9
View File
@@ -44,7 +44,7 @@ import {
type RuntimeOutput,
type ScheduledEventKind,
} from '../src/index.js';
import { device, link, pc, startedEngine, uuid } from './fixtures.js';
import { device, letFramesArrive, link, pc, startedEngine, uuid } from './fixtures.js';
const marker = (label: string): ScheduledEventKind => ({ kind: 'parityMarker', label });
@@ -274,6 +274,29 @@ describe('event queue and virtual clock', () => {
expect(engine.state.packetTraces.find((trace) => trace.layer === 'physical' && trace.operation === 'sent')?.timeMilliseconds).toBe(0);
});
it('forgets the send threads on stop', () => {
const [firstId, firstPortId, secondId, secondPortId] = [0x29, 0x2a, 0x2b, 0x2c].map(uuid) as [string, string, string, string];
const input = {
nodes: [pc(firstId, firstPortId), pc(secondId, secondPortId)],
links: [link(firstId, firstPortId, secondId, secondPortId)],
deviceConfigurations: new Map([
[firstId, device('10.6.0.1')],
[secondId, device('10.6.0.2')],
]),
};
const engine = startedEngine(input);
// The UDP thread waits for ARP on .99 when the simulation stops.
engine.sendUdp(engine.bindUdpSocket(firstId, { remoteIPAddress: '10.6.0.99', remotePort: 9 })!, new TextEncoder().encode('lost'));
engine.handle({ type: 'stop' });
engine.handle({ type: 'start', snapshot: topologySnapshot(input), seed: 14, initialTimeMilliseconds: 0 });
const receiver = engine.bindUdpSocket(secondId, { localPort: 9 })!;
engine.sendUdp(engine.bindUdpSocket(firstId, { remoteIPAddress: '10.6.0.2', remotePort: 9 })!, new TextEncoder().encode('found'));
expect(engine.state.currentTimeMilliseconds).toBe(0);
expect(new TextDecoder().decode(engine.receiveUdp(receiver, 1_000)?.datagram.payload)).toBe('found');
});
it('forgets TCP sessions and accepted sockets on stop', () => {
const [clientId, clientPortId, serverId, serverPortId] = [0x21, 0x22, 0x23, 0x24].map(uuid) as [string, string, string, string];
const engine = startedEngine({
@@ -319,7 +342,7 @@ describe('UDP sockets', () => {
expect(engine.bindUdpSocket(targetNodeId, { localPort: 9000 })).toBeUndefined();
engine.sendUdp(sender!, new TextEncoder().encode('hello'));
const received = engine.receiveUdp(receiver!);
const received = engine.receiveUdp(receiver!, 1_000);
expect(text(received?.datagram.payload)).toBe('hello');
expect(received?.senderIPAddress).toBe('192.168.5.10');
@@ -327,6 +350,36 @@ describe('UDP sockets', () => {
expect(engine.bindUdpSocket(targetNodeId, { localPort: 9000 })).toBeDefined();
});
it('returns from a send at once, the datagram arrives as the clock advances (swiftSends: when it has arrived)', () => {
const arrival = (swiftSends: boolean) => {
const [sourceNodeId, sourcePortId, targetNodeId, targetPortId] = [0x15c1, 0x15c2, 0x15c3, 0x15c4].map(uuid) as [string, string, string, string];
const engine = new NetworkRuntimeEngine(14, undefined, { swiftSends });
engine.handle({
type: 'start',
snapshot: topologySnapshot({
nodes: [pc(sourceNodeId, sourcePortId), pc(targetNodeId, targetPortId)],
links: [link(sourceNodeId, sourcePortId, targetNodeId, targetPortId)],
deviceConfigurations: new Map([
[sourceNodeId, device('192.168.6.10')],
[targetNodeId, device('192.168.6.20')],
]),
}),
seed: 14,
initialTimeMilliseconds: 0,
});
const receiver = engine.bindUdpSocket(targetNodeId, { localPort: 9000 })!;
const sender = engine.bindUdpSocket(sourceNodeId, { remoteIPAddress: '192.168.6.20', remotePort: 9000 })!;
engine.sendUdp(sender, new TextEncoder().encode('hello'));
const returnedAt = engine.state.currentTimeMilliseconds;
const waiting = engine.state.udpReceiveQueuesBySocketId.get(receiver)?.length ?? 0;
// ARP there and back, then the datagram: three cables of 55 ms.
return { returnedAt, waiting, receivedAt: engine.receiveUdp(receiver, 1_000)?.receivedAtMilliseconds };
};
expect(arrival(false)).toEqual({ returnedAt: 0, waiting: 0, receivedAt: 165 });
expect(arrival(true)).toEqual({ returnedAt: 165, waiting: 1, receivedAt: 165 });
});
it('draws ephemeral ports without disturbing the seeded RIP jitter', () => {
const nodeId = uuid(0x15f1);
const input = { nodes: [pc(nodeId, uuid(0x15f2))], links: [], deviceConfigurations: new Map([[nodeId, device('192.168.10.10')]]) };
@@ -366,13 +419,13 @@ describe('UDP sockets', () => {
engine.sendUdp(sender, new TextEncoder().encode('broadcast'), { ipAddress: '255.255.255.255', port: 9000 });
expect(engine.receiveUdp(receiverA)?.senderIPAddress).toBe('10.0.0.1');
expect(engine.receiveUdp(receiverB)?.senderIPAddress).toBe('20.0.0.1');
expect(engine.receiveUdp(receiverA, 1_000)?.senderIPAddress).toBe('10.0.0.1');
expect(engine.receiveUdp(receiverB, 1_000)?.senderIPAddress).toBe('20.0.0.1');
expect(engine.state.socketsById.get(receiverA)?.remoteIPAddress).toBe('10.0.0.1');
expect(engine.state.socketsById.get(receiverA)?.remotePort).toBe(8000);
engine.sendUdp(receiverA, new TextEncoder().encode('reply'));
expect(text(engine.receiveUdp(sender)?.datagram.payload)).toBe('reply');
expect(text(engine.receiveUdp(sender, 1_000)?.datagram.payload)).toBe('reply');
});
it('advances the virtual clock for a receive timeout and returns nothing', () => {
@@ -621,12 +674,15 @@ describe('switch', () => {
engine.state.packetTraces.filter((trace) => trace.nodeId === switchId && trace.detail === detail);
engine.sendEthernetFrame(hostAId, hostAPortId, frame(macA, '02:00:00:00:00:FE'));
letFramesArrive(engine);
expect(switchTraces('switch unknown unicast flooding')).toHaveLength(2);
engine.sendEthernetFrame(hostBId, hostBPortId, frame(macB, macA));
letFramesArrive(engine);
expect(switchTraces('switch learned unicast forwarding')).toHaveLength(1);
engine.sendEthernetFrame(hostAId, hostAPortId, frame(macA, macA));
letFramesArrive(engine);
expect(switchTraces('switch filtered destination on incoming port').length).toBeGreaterThan(0);
expect(engine.switchSatEntries(switchId)).toHaveLength(2);
@@ -777,6 +833,7 @@ describe('packet loss and trace retention', () => {
engine.setGlobalPacketLossEnabled(true);
engine.sendEthernetFrame(firstNodeId, firstPortId, frame);
letFramesArrive(engine);
expect(engine.state.globalPacketLossEnabled).toBe(true);
expect(engine.state.arpCachesByNodeId.get(secondNodeId)?.size ?? 0).toBe(0);
@@ -792,6 +849,7 @@ describe('packet loss and trace retention', () => {
engine.setGlobalPacketLossEnabled(false);
engine.sendEthernetFrame(firstNodeId, firstPortId, frame);
letFramesArrive(engine);
expect(engine.state.globalPacketLossEnabled).toBe(false);
expect(engine.state.arpCachesByNodeId.get(secondNodeId)?.get('10.0.0.1')?.macAddress).toBe(
@@ -864,6 +922,9 @@ describe('TCP sockets', () => {
expect(engine.connectTcp(clientSocketId)).toBe(true);
const acceptedSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('ESTABLISHED');
// connect returns with the SYN+ACK; the client's ACK is still on its way.
expect(engine.state.socketsById.get(acceptedSocketId)?.tcpState).toBe('SYN_RCVD');
letFramesArrive(engine);
expect(engine.state.socketsById.get(acceptedSocketId)?.tcpState).toBe('ESTABLISHED');
expect(engine.state.packetTraces.some((trace) => trace.detail === 'TCP SYN_SENT -> ESTABLISHED')).toBe(true);
expect(engine.state.packetTraces.some((trace) => trace.detail === 'TCP SYN_RCVD -> ESTABLISHED')).toBe(true);
@@ -885,6 +946,42 @@ describe('TCP sockets', () => {
expect(payloadLengths).toEqual([1_460, 1_460, 81]);
});
it("sends TCP in a thread of its own: UDP waiting for ARP does not hold a connection up", () => {
const { engine, clientId, serverId } = pair(0x1731, 0x1732, '10.24.0');
engine.openTcpServerSocket(serverId, 80);
engine.sendUdp(engine.bindUdpSocket(clientId, { remoteIPAddress: '10.24.0.99', remotePort: 9 })!, new TextEncoder().encode('nobody'));
const clientSocketId = engine.openTcpClientSocket(clientId, '10.24.0.2', 80)!;
expect(engine.connectTcp(clientSocketId)).toBe(true);
// ARP for the server (behind the one for .99 on the cable), SYN and SYN+ACK: four cables of 55 ms.
expect(engine.state.currentTimeMilliseconds).toBe(55 + 4 * 55);
});
it("holds the TCP thread while ARP waits: a connection's SYN waits behind a SYN+ACK to no one", () => {
const { engine, clientId, serverId } = pair(0x17e1, 0x17e2, '10.25.0');
engine.openTcpServerSocket(serverId, 80);
engine.openTcpServerSocket(clientId, 81);
// A SYN to the client's port 81 from .99, which no card has: the client's answer waits for ARP.
engine.sendIPv4Packet(serverId, {
identity: engine.allocatePacketIdentity(),
senderIPAddress: '10.25.0.99',
receiverIPAddress: '10.25.0.1',
timeToLive: 64,
protocol: 'tcp',
payload: {
kind: 'tcp',
segment: { sourcePort: 5000, destinationPort: 81, sequenceNumber: 1, acknowledgementNumber: 0, flags: TCP_FLAG.synchronize, payload: new Uint8Array() },
},
});
engine.handle({ type: 'advance', toMilliseconds: 200 });
const clientSocketId = engine.openTcpClientSocket(clientId, '10.25.0.2', 80)!;
expect(engine.connectTcp(clientSocketId)).toBe(true);
// The SYN arrived after ARP there and back (165 ms); ARP for .99 gives up two
// round trips later, then the client's SYN and the server's SYN+ACK.
expect(engine.state.currentTimeMilliseconds).toBe(165 + 2 * 2_750 + 2 * 55);
});
it('retransmits a connection attempt at most three times and releases the port', () => {
const clientId = uuid(0x1741);
const engine = startedEngine({ nodes: [pc(clientId, uuid(0x1751))], links: [], deviceConfigurations: new Map([[clientId, device('10.19.0.1')]]) });
@@ -932,6 +1029,7 @@ describe('TCP sockets', () => {
const serverSocketId = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)![0]!;
engine.closeTcpSocket(clientSocketId);
letFramesArrive(engine);
// Java's socket thread leaves listen on the FIN and closes without the application.
const serverStates = engine.state.packetTraces
.filter((trace) => trace.nodeId === serverId && trace.detail?.startsWith('TCP ') === true && trace.detail.includes(' -> '))
@@ -971,8 +1069,10 @@ describe('TCP sockets', () => {
engine.openTcpServerSocket(serverId, 80);
const client = engine.openTcpClientSocket(clientId, '10.23.0.2', 80)!;
expect(engine.connectTcp(client)).toBe(true);
letFramesArrive(engine);
const closedAt = engine.state.currentTimeMilliseconds;
engine.closeTcpSocket(client);
letFramesArrive(engine);
// The server answers the FIN with its ACK and, closing by itself, its FIN at once.
const physical = (nodeId: string, operation: string) =>
engine.state.packetTraces
@@ -1013,6 +1113,7 @@ describe('TCP sockets', () => {
const clientSocketId = engine.openTcpClientSocket(clientId, '10.21.0.2', 9090)!;
expect(engine.connectTcp(clientSocketId)).toBe(true);
engine.closeTcpSocket(clientSocketId);
letFramesArrive(engine);
expect(engine.state.socketsById.get(clientSocketId)?.tcpState).toBe('TIME_WAIT');
const localPort = engine.state.socketsById.get(clientSocketId)!.localPort;
@@ -1059,6 +1160,7 @@ describe('TCP sockets', () => {
// the listener's successor 3 (made when the first is accepted), the second client 4.
const listenerId = engine.openTcpServerSocket(serverId, 80)!;
expect(engine.connectTcp(engine.openTcpClientSocket(firstId, '10.22.0.3', 80)!)).toBe(true);
letFramesArrive(engine);
expect(engine.connectTcp(engine.openTcpClientSocket(secondId, '10.22.0.3', 80)!)).toBe(true);
const accepted = engine.state.tcpAcceptedSocketIdsByListenerId.get(listenerId)!;
@@ -1423,7 +1525,7 @@ describe('DNS', () => {
]),
),
});
return { engine, client: hosts[0]!.id };
return { engine, client: hosts[0]!.id, server: hosts[1]!.id };
};
const queries = (engine: NetworkRuntimeEngine) =>
engine.state.packetTraces.filter((trace) => trace.detail?.startsWith('DNS query') === true).map((trace) => trace.detail);
@@ -1516,14 +1618,55 @@ describe('DNS', () => {
expect(engine.state.currentTimeMilliseconds - called).toBe(4 * 110);
});
it("answers behind a datagram of its own whose ARP fails: Java's UDP thread sends one after the other", () => {
const { engine, client, server } = lan({ '10.30.0.2': ['web.filius.de A 10.30.0.9'] });
const socket = engine.bindUdpSocket(server, { remoteIPAddress: '10.30.0.99', remotePort: 9 })!;
const called = engine.state.currentTimeMilliseconds;
engine.sendUdp(socket, new TextEncoder().encode('nobody'));
expect(engine.resolveDnsAddress(client, 'web.filius.de')).toEqual({ type: 'resolved', address: '10.30.0.9' });
// The answer leaves when ARP has given up on .99 (two round trips), and takes two cables through the switch.
expect(engine.state.currentTimeMilliseconds - called).toBe(2 * 2_750 + 110);
});
it('lets the senders that wait take their turns in the order they came, before the thread takes the next', () => {
const { engine, client, server } = lan({ '10.30.0.2': ['web.filius.de A 10.30.0.9'] });
const receiver = engine.bindUdpSocket(client, { localPort: 9 })!;
// The server's UDP thread waits for ARP on .99 from 0 to 5500 ms.
engine.sendUdp(engine.bindUdpSocket(server, { remoteIPAddress: '10.30.0.99', remotePort: 9 })!, new TextEncoder().encode('nobody'));
// A query from .98, which no card has, reaches the server at 330 ms; its answer waits its turn.
const query = encodeDnsMessage({
id: 1,
queryResponse: 0,
responseCode: 0,
questions: [{ name: 'web.filius.de.', type: 'A', questionClass: 'IN' }],
answers: [],
authority: [],
additional: [],
});
engine.sendIPv4Packet(client, {
identity: engine.allocatePacketIdentity(),
senderIPAddress: '10.30.0.98',
receiverIPAddress: '10.30.0.2',
timeToLive: 64,
protocol: 'udp',
payload: { kind: 'udp', datagram: { sourcePort: 5000, destinationPort: 53, payload: new TextEncoder().encode(query) } },
});
engine.handle({ type: 'advance', toMilliseconds: 400 });
// As with a fair lock (Java's is not): the send returns when the datagram
// in hand is out; the answer to .98 goes before it and waits for ARP in turn.
engine.sendUdp(engine.bindUdpSocket(server, { remoteIPAddress: '10.30.0.1', remotePort: 9 })!, new TextEncoder().encode('third'));
expect(engine.state.currentTimeMilliseconds).toBe(2 * 2_750);
expect(engine.receiveUdp(receiver, 10_000)?.receivedAtMilliseconds).toBe(4 * 2_750 + 110);
});
it('gives up after five round trips when no server answers, and closes its socket', () => {
const { engine, client } = lan({ '10.30.0.2': [] });
const called = engine.state.currentTimeMilliseconds;
// 10.30.0.3 is there, but no DNS server runs on it.
expect(engine.resolveDns(client, 'web.filius.de', 'A', '10.30.0.3')).toEqual({ type: 'timedOut' });
// Java's five round trips; before them the engine's send still blocks for
// ARP and the query's way to .3 (Java waits from handing it down — the ARP step).
expect(engine.state.currentTimeMilliseconds - called).toBe(3 * 110 + 5 * 2_750);
// Java's five round trips, from handing the query down: the send returns at once.
expect(engine.state.currentTimeMilliseconds - called).toBe(5 * 2_750);
expect([...engine.state.socketsById.values()].filter((socket) => socket.nodeId === client)).toEqual([]);
});
+14
View File
@@ -48,3 +48,17 @@ export function startedEngine(
export function uuid(suffix: number): Id {
return `00000000-0000-0000-0000-${suffix.toString(16).toUpperCase().padStart(12, '0')}`;
}
const FRAME_EVENTS = new Set(['ethernetLinkDeparture', 'ethernetLinkDelivery', 'remoteLinkDelivery', 'arpAnswered']);
/**
* Advances until every frame in flight has arrived — what Swift's send did
* by itself; the engine's send returns at once, as Java's.
*/
export function letFramesArrive(engine: NetworkRuntimeEngine): void {
const inFlight = () =>
engine.state.pendingEvents.filter((event) => FRAME_EVENTS.has(event.kind.kind)).map((event) => event.deadlineMilliseconds);
for (let deadlines = inFlight(); deadlines.length > 0; deadlines = inFlight()) {
engine.handle({ type: 'advance', toMilliseconds: Math.min(...deadlines) });
}
}
+1 -1
View File
@@ -73,7 +73,7 @@ function observationJson(item: IcmpObservation): Record<string, unknown> {
function run(scenario: Scenario): Record<string, unknown> {
// Swift's cables carry all frames at once; everything else is compared as it is.
const engine = new NetworkRuntimeEngine(scenario.seed, undefined, { swiftCables: true, swiftArp: true });
const engine = new NetworkRuntimeEngine(scenario.seed, undefined, { swiftCables: true, swiftArp: true, swiftSends: true });
if (scenario.speedPercent !== undefined) engine.setSimulationSpeed(scenario.speedPercent);
engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 });