feat(core): a cable carries one frame after the other, as in Java

Java's SimplexVerbindung sleeps a cable's delay with the port's buffer
locked: frames on one cable leave one after the other, and a frame shows
in the packet log when it leaves. The engine now queues a frame in the
output buffer until the one before has arrived (ethernetLinkDeparture).
Swift sends a cable's frames at once; the option swiftCables keeps that for
the Swift comparison, which goes on checking everything else trace by
trace.

With the queue, the Java comparisons match within 120 ms (was 200); a
state of the TCP stories within 250 ms, where the engine's calls block
one after the other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 16:37:45 +00:00
co-authored by Claude Opus 5.5
parent cec0a92f95
commit efc7c440f6
7 changed files with 153 additions and 15 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) | | `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) |
| `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft | | `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft |
| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen | | `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen |
| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, DNS, RIP, Remote Link, WLAN | | `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4472–5187 (UDP), ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall, UDP-Sockets (binden, freie Ports nach Javas Zufall, senden samt Broadcast, zustellen, empfangen mit Wartezeit, schließen), TCP (Z. ~5187–5913: Server- und Client-Sockets, Handschlag mit Ergebnis, Segmente zu 1460 Byte bis zum PSH, höchstens drei Versuche, Abbau über alle Java-Zustände samt `TIME_WAIT`; seit F2.3c nach Javas `TCPSocket` an Java-Filius gemessen: je Versuch und für `TIME_WAIT` 3 RTT nach der Geschwindigkeit (8250 ms bei 90 %, Swift fest 7500 bzw. 2500 ms), der Zeitablauf zählt ab der Übergabe des Segments, vor ARP; ein Segment beendet `TIME_WAIT`; die Gegenseite schließt beim FIN von selbst; nicht bestätigte Daten schließen mit FIN statt sofort; der angenommene Socket hat die Startnummer des Listeners, der Listener eine neue. Abweichend von Java: drei SYN statt einem, weil Javas Wiederholungsschleife nach dem ersten Zeitablauf abbricht), DHCP (Z. ~5913–6694: Server mit festen Zuordnungen, Adressbereich, Angeboten, Leases und Sperrliste; Client seit F2.3b nach Javas `DHCPClient` neu gebaut und an Java-Filius gemessen: je Versuch eine Nachricht, eine Antwort, die nicht passt, wird gelesen und verworfen; die ARP-Prüfung wartet eine RTT, eine Antwort für die Adresse beendet sie sofort, dann DECLINE; nach einem Zeitablauf wiederholt der Zustand seine Nachricht, nach dem fünften nimmt der Client eine Link-Local-Adresse `169.254.1.x/16` (ARP-Probe, ARP-Reply an alle) und gibt keine alte Adresse zurück; Swift sendete doppelt, wartete nicht auf ARP und kehrte nach zehn Versuchen zur alten Adresse zurück. Abweichend von Java: Nach fünf belegten Link-Local-Adressen gibt der Client auf (Java sucht endlos), und ein ARP-Paket für eine andere Adresse beendet die Prüfung nicht. Seit F2.3d befördert ein Kabel einen Frame nach dem anderen wie Javas `SimplexVerbindung` (ein Frame wartet im Ausgangspuffer, bis der vorige angekommen ist, und erscheint im Protokoll, wenn er losgeht); Swift schickt alle zugleich, die Option `swiftCables` behält das für den Swift-Vergleich. Die RTT der DHCP-Zeiten (Warten, Angebote, Sperrliste) folgt wie in Java der Geschwindigkeit (50 Kabel, 2750 ms bei 90 %); Swift rechnet fest mit 2500 ms. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, DNS, RIP, Remote Link, WLAN |
| `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen | | `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` | | `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
| `src/runtime/random.ts` | Engine, `TopologyJavaCompatibleRandom` | übertragen; BigInt für die 48-Bit-Arithmetik; dazu `nextDouble` (Javas `Math.random`, an OpenJDK 21 gemessen) | | `src/runtime/random.ts` | Engine, `TopologyJavaCompatibleRandom` | übertragen; BigInt für die 48-Bit-Arithmetik; dazu `nextDouble` (Javas `Math.random`, an OpenJDK 21 gemessen) |
+56 -2
View File
@@ -15,6 +15,8 @@
// one message per attempt, an ARP check that waits a round trip, // one message per attempt, an ARP check that waits a round trip,
// link-local after five failures; round trip times follow the speed) // link-local after five failures; round trip times follow the speed)
// processScheduledRuntimeEvent, dropForGlobalPacketLoss (lines ~4753–4850) // processScheduledRuntimeEvent, dropForGlobalPacketLoss (lines ~4753–4850)
// A cable carries one frame after the other, as Java's SimplexVerbindung
// (Swift: all at once; the option swiftCables keeps that for the comparison).
// Java counterparts: filius.hardware.Verbindung, filius.software.netzzugangsschicht, // Java counterparts: filius.hardware.Verbindung, filius.software.netzzugangsschicht,
// filius.software.vermittlungsschicht (ARP, IP, ICMP), system.SwitchFirmware, // filius.software.vermittlungsschicht (ARP, IP, ICMP), system.SwitchFirmware,
// filius.software.transportschicht (UDP, TCP), filius.software.dhcp. // filius.software.transportschicht (UDP, TCP), filius.software.dhcp.
@@ -104,6 +106,15 @@ export type ScheduledEventKind =
| { readonly kind: 'dhcpClientStart'; readonly nodeId: Id } | { readonly kind: 'dhcpClientStart'; readonly nodeId: Id }
| { readonly kind: 'dhcpTimeout'; readonly nodeId: Id } | { readonly kind: 'dhcpTimeout'; readonly nodeId: Id }
| { readonly kind: 'tcpTimeout'; readonly sessionId: Id } | { readonly kind: 'tcpTimeout'; readonly sessionId: Id }
| {
/** A frame that waited in the port's output buffer leaves: the cable was busy. */
readonly kind: 'ethernetLinkDeparture';
readonly sourceNodeId: Id;
readonly outgoingPortId: Id;
readonly targetNodeId: Id;
readonly targetPortId: Id;
readonly frame: EthernetFrame;
}
| { | {
readonly kind: 'ethernetLinkDelivery'; readonly kind: 'ethernetLinkDelivery';
readonly sourceNodeId: Id; readonly sourceNodeId: Id;
@@ -498,8 +509,9 @@ function toSeed(seed: bigint | number): bigint {
return BigInt.asUintN(64, BigInt(seed)); return BigInt.asUintN(64, BigInt(seed));
} }
/** A departure waits for the delivery of the frame before it, so draining the deliveries reaches it. */
function isNetworkDeliveryEvent(kind: ScheduledEventKind): boolean { function isNetworkDeliveryEvent(kind: ScheduledEventKind): boolean {
return kind.kind === 'ethernetLinkDelivery' || kind.kind === 'remoteLinkDelivery'; return kind.kind === 'ethernetLinkDeparture' || kind.kind === 'ethernetLinkDelivery' || kind.kind === 'remoteLinkDelivery';
} }
function isRemoteLinkWaitEvent(kind: ScheduledEventKind): boolean { function isRemoteLinkWaitEvent(kind: ScheduledEventKind): boolean {
@@ -527,11 +539,23 @@ export class NetworkRuntimeEngine {
private readonly packetTraceRetentionPolicy: TraceRetentionPolicy; private readonly packetTraceRetentionPolicy: TraceRetentionPolicy;
private isProcessingScheduledEvents = false; private isProcessingScheduledEvents = false;
private pendingIPv4TransmissionsByArpKey = new Map<string, PendingIPv4Transmission[]>(); private pendingIPv4TransmissionsByArpKey = new Map<string, PendingIPv4Transmission[]>();
/** Per cable end (node and port): when the last frame sent there arrives. */
private cableBusyUntilByPort = new Map<string, number>();
private readonly cablesCarryOneFrameAtATime: boolean;
/**
* `swiftCables`: a cable carries all frames at once, as in the Swift
* engine — only for the comparison with Swift. Java's cable carries one
* frame after the other (SimplexVerbindung sleeps the delay with the port's
* buffer locked); a frame waits in the output buffer until the one before
* has arrived, and shows in the packet log when it leaves.
*/
constructor( constructor(
seed: bigint | number = DEFAULT_SEED, seed: bigint | number = DEFAULT_SEED,
packetTraceRetentionPolicy: TraceRetentionPolicy = PRODUCTION_TRACE_RETENTION, packetTraceRetentionPolicy: TraceRetentionPolicy = PRODUCTION_TRACE_RETENTION,
options: { readonly swiftCables?: boolean } = {},
) { ) {
this.cablesCarryOneFrameAtATime = options.swiftCables !== true;
const normalizedSeed = toSeed(seed); const normalizedSeed = toSeed(seed);
this.stateValue = initialState(normalizedSeed); this.stateValue = initialState(normalizedSeed);
this.random = new JavaCompatibleRandom(normalizedSeed); this.random = new JavaCompatibleRandom(normalizedSeed);
@@ -577,6 +601,7 @@ export class NetworkRuntimeEngine {
this.linkLocalRandom = new JavaCompatibleRandom(seed ^ 0x4c4c_4144n); this.linkLocalRandom = new JavaCompatibleRandom(seed ^ 0x4c4c_4144n);
this.isProcessingScheduledEvents = false; this.isProcessingScheduledEvents = false;
this.pendingIPv4TransmissionsByArpKey.clear(); this.pendingIPv4TransmissionsByArpKey.clear();
this.cableBusyUntilByPort.clear();
this.initializeRipState(); this.initializeRipState();
this.initializeDhcpState(); this.initializeDhcpState();
this.initializeNatState(); this.initializeNatState();
@@ -762,6 +787,9 @@ export class NetworkRuntimeEngine {
private processScheduledRuntimeEvent(kind: ScheduledEventKind): void { private processScheduledRuntimeEvent(kind: ScheduledEventKind): void {
switch (kind.kind) { switch (kind.kind) {
case 'ethernetLinkDeparture':
this.departEthernetFrame(kind.sourceNodeId, kind.outgoingPortId, { nodeId: kind.targetNodeId, portId: kind.targetPortId }, kind.frame);
return;
case 'ethernetLinkDelivery': case 'ethernetLinkDelivery':
if (this.dropForGlobalPacketLoss(kind.frame, kind.targetNodeId, kind.targetPortId, 'inbound', 'physical')) { if (this.dropForGlobalPacketLoss(kind.frame, kind.targetNodeId, kind.targetPortId, 'inbound', 'physical')) {
return; return;
@@ -2583,6 +2611,28 @@ export class NetworkRuntimeEngine {
// ── Ethernet ───────────────────────────────────────────────────────────── // ── Ethernet ─────────────────────────────────────────────────────────────
sendEthernetFrame(fromNodeId: Id, outgoingPortId: Id, frame: EthernetFrame): void { sendEthernetFrame(fromNodeId: Id, outgoingPortId: Id, frame: EthernetFrame): void {
const endpoint = this.attachedEndpoint(fromNodeId, outgoingPortId);
const now = this.stateValue.currentTimeMilliseconds;
const busyUntil = this.cablesCarryOneFrameAtATime ? (this.cableBusyUntilByPort.get(portKey(fromNodeId, outgoingPortId)) ?? now) : now;
if (endpoint !== undefined && busyUntil > now) {
// The cable carries a frame: this one waits in the output buffer.
this.cableBusyUntilByPort.set(portKey(fromNodeId, outgoingPortId), busyUntil + this.runtimeSpeed.linkTransmissionDelayMilliseconds);
this.schedule(busyUntil, {
kind: 'ethernetLinkDeparture',
sourceNodeId: fromNodeId,
outgoingPortId,
targetNodeId: endpoint.nodeId,
targetPortId: endpoint.portId,
frame,
});
this.drainPendingNetworkDeliveryEventsIfNeeded();
return;
}
this.departEthernetFrame(fromNodeId, outgoingPortId, endpoint, frame);
}
/** The frame leaves on the cable (and shows in the packet log); it arrives one cable's delay later. */
private departEthernetFrame(fromNodeId: Id, outgoingPortId: Id, endpoint: { nodeId: Id; portId: Id } | undefined, frame: EthernetFrame): void {
this.recordTrace({ this.recordTrace({
frameIdentity: frame.identity, frameIdentity: frame.identity,
packetIdentity: packetIdentityInFrame(frame), packetIdentity: packetIdentityInFrame(frame),
@@ -2593,7 +2643,6 @@ export class NetworkRuntimeEngine {
operation: 'sent', operation: 'sent',
afterHeaders: ethernetHeaders(frame), afterHeaders: ethernetHeaders(frame),
}); });
const endpoint = this.attachedEndpoint(fromNodeId, outgoingPortId);
if (endpoint === undefined) return; if (endpoint === undefined) return;
const latencyMilliseconds = this.runtimeSpeed.linkTransmissionDelayMilliseconds; const latencyMilliseconds = this.runtimeSpeed.linkTransmissionDelayMilliseconds;
const deadlineMilliseconds = this.stateValue.currentTimeMilliseconds + latencyMilliseconds; const deadlineMilliseconds = this.stateValue.currentTimeMilliseconds + latencyMilliseconds;
@@ -2611,6 +2660,7 @@ export class NetworkRuntimeEngine {
}); });
return; return;
} }
if (this.cablesCarryOneFrameAtATime) this.cableBusyUntilByPort.set(portKey(fromNodeId, outgoingPortId), deadlineMilliseconds);
this.schedule(deadlineMilliseconds, { this.schedule(deadlineMilliseconds, {
kind: 'ethernetLinkDelivery', kind: 'ethernetLinkDelivery',
@@ -3666,6 +3716,10 @@ function firewallAddressMatches(packetIPAddress: string, configuredIPAddress: st
return sameSubnet(packetIPAddress, configuredIPAddress, subnetMask); return sameSubnet(packetIPAddress, configuredIPAddress, subnetMask);
} }
function portKey(nodeId: Id, portId: Id): string {
return `${nodeId}|${portId}`;
}
function arpKey(nodeId: Id, ipAddress: string): string { function arpKey(nodeId: Id, ipAddress: string): string {
return `${nodeId}|${ipAddress}`; return `${nodeId}|${ipAddress}`;
} }
+78
View File
@@ -248,6 +248,28 @@ describe('event queue and virtual clock', () => {
]).toEqual([0, 0, 0, 0, 0, 0, 0]); ]).toEqual([0, 0, 0, 0, 0, 0, 0]);
}); });
it('forgets which cables are busy on stop', () => {
const [clientId, clientPortId, serverId, serverPortId] = [0x25, 0x26, 0x27, 0x28].map(uuid) as [string, string, string, string];
const input = {
nodes: [pc(clientId, clientPortId), pc(serverId, serverPortId)],
links: [link(clientId, clientPortId, serverId, serverPortId)],
deviceConfigurations: new Map([
[clientId, device('10.5.0.1')],
[serverId, device('10.5.0.2')],
]),
};
const engine = startedEngine(input);
engine.handle({ type: 'advance', toMilliseconds: 10_000 });
engine.openTcpServerSocket(serverId, 80);
expect(engine.connectTcp(engine.openTcpClientSocket(clientId, '10.5.0.2', 80)!)).toBe(true);
engine.handle({ type: 'stop' });
engine.handle({ type: 'start', snapshot: topologySnapshot(input), seed: 14, initialTimeMilliseconds: 0 });
engine.resolveMacAddress(clientId, '10.5.0.2');
expect(engine.state.packetTraces.find((trace) => trace.layer === 'physical' && trace.operation === 'sent')?.timeMilliseconds).toBe(0);
});
it('forgets TCP sessions and accepted sockets on stop', () => { 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 [clientId, clientPortId, serverId, serverPortId] = [0x21, 0x22, 0x23, 0x24].map(uuid) as [string, string, string, string];
const engine = startedEngine({ const engine = startedEngine({
@@ -925,6 +947,62 @@ describe('TCP sockets', () => {
expect(engine.state.socketsById.get(listenerId)?.tcpState).toBe('LISTEN'); expect(engine.state.socketsById.get(listenerId)?.tcpState).toBe('LISTEN');
}); });
it('carries one frame after the other on a cable, as Java (Swift: all at once)', () => {
const departures = (swiftCables: boolean) => {
const [clientId, clientPortId, serverId, serverPortId] = [0x17a1, 0x17b1, 0x17a2, 0x17b2].map(uuid) as [string, string, string, string];
const engine = new NetworkRuntimeEngine(14, undefined, { swiftCables });
engine.handle({
type: 'start',
snapshot: topologySnapshot({
nodes: [pc(clientId, clientPortId), pc(serverId, serverPortId)],
links: [link(clientId, clientPortId, serverId, serverPortId)],
deviceConfigurations: new Map([
[clientId, device('10.23.0.1')],
[serverId, device('10.23.0.2')],
]),
}),
seed: 14,
initialTimeMilliseconds: 0,
});
engine.openTcpServerSocket(serverId, 80);
const client = engine.openTcpClientSocket(clientId, '10.23.0.2', 80)!;
expect(engine.connectTcp(client)).toBe(true);
const closedAt = engine.state.currentTimeMilliseconds;
engine.closeTcpSocket(client);
// 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
.filter((trace) => trace.nodeId === nodeId && trace.layer === 'physical' && trace.operation === operation && trace.timeMilliseconds > closedAt)
.map((trace) => trace.timeMilliseconds - closedAt);
return { sent: physical(serverId, 'sent'), received: physical(clientId, 'received') };
};
expect(departures(false)).toEqual({ sent: [55, 110], received: [110, 165] });
expect(departures(true)).toEqual({ sent: [55, 55], received: [110, 110] });
});
it('queues a third frame behind the second on a busy cable', () => {
// Four DHCP clients start at once and broadcast their DISCOVER; the switch
// floods three of them onto each client's cable.
const switchId = uuid(0x17c0);
const hosts = [0, 1, 2, 3].map((index) => ({ id: uuid(0x17c1 + 2 * index), portId: uuid(0x17c2 + 2 * index), switchPortId: uuid(0x17d0 + index) }));
const engine = startedEngine({
nodes: [
{ id: switchId, kind: 'networkSwitch', ports: hosts.map((host, index) => portSnapshot(host.switchPortId, `sw${index + 1}`)) },
...hosts.map((host) => pc(host.id, host.portId)),
],
links: hosts.map((host) => link(host.id, host.portId, switchId, host.switchPortId)),
deviceConfigurations: new Map(hosts.map((host) => [host.id, device('0.0.0.0')])),
dhcpClientConfigurationsByNodeId: new Map(hosts.map((host) => [host.id, { isEnabled: true }])),
});
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
const received = engine.state.packetTraces
.filter((trace) => trace.nodeId === hosts[0]!.id && trace.layer === 'physical' && trace.operation === 'received')
.map((trace) => trace.timeMilliseconds);
expect(received).toEqual([110, 165, 220]);
});
it('ends TIME_WAIT with the next segment that comes', () => { it('ends TIME_WAIT with the next segment that comes', () => {
const { engine, clientId, serverId } = pair(0x1771, 0x1772, '10.21.0'); const { engine, clientId, serverId } = pair(0x1771, 0x1772, '10.21.0');
engine.openTcpServerSocket(serverId, 9090); engine.openTcpServerSocket(serverId, 9090);
+7 -6
View File
@@ -14,16 +14,17 @@
// //
// Java runs on threads in real time, at the default speed of 90 % (55 ms a // Java runs on threads in real time, at the default speed of 90 % (55 ms a
// cable, a round trip time of 2750 ms). Its gaps match the engine's within a // cable, a round trip time of 2750 ms). Its gaps match the engine's within a
// few milliseconds, except where a card sends two frames at once: Java's // few milliseconds; where they do not, it is Java's threads (a sender blocked
// cable carries one frame at a time (SimplexVerbindung sleeps the delay with // by a busy cable wakes late, up to some 110 ms measured), the log's lost
// the port's buffer locked, and the sender waits), the engine's cable all of // DISCOVER above, or a story whose calls the engine makes one after the
// them together — up to three cables' delay, measured. A gap may differ by // other. A gap may differ by GAP_TOLERANCE_MILLISECONDS; a wrong round trip
// GAP_TOLERANCE_MILLISECONDS; a wrong round trip time (250 ms) may not. // time (250 ms) may not. A cable carries one frame after the other in both
// (engine.test.ts checks that: one cable's delay is below this tolerance).
import { NetworkRuntimeEngine, TCP_FLAG, type EthernetFrame } from '../src/index.js'; import { NetworkRuntimeEngine, TCP_FLAG, type EthernetFrame } from '../src/index.js';
import { snapshotOf, type Scenario } from './scenario.js'; import { snapshotOf, type Scenario } from './scenario.js';
export const GAP_TOLERANCE_MILLISECONDS = 200; export const GAP_TOLERANCE_MILLISECONDS = 120;
const DUPLICATE_WINDOW_MILLISECONDS = 300; const DUPLICATE_WINDOW_MILLISECONDS = 300;
export interface JavaFrame { export interface JavaFrame {
+2 -1
View File
@@ -72,7 +72,8 @@ function observationJson(item: IcmpObservation): Record<string, unknown> {
} }
function run(scenario: Scenario): Record<string, unknown> { function run(scenario: Scenario): Record<string, unknown> {
const engine = new NetworkRuntimeEngine(scenario.seed); // Swift's cables carry all frames at once; everything else is compared as it is.
const engine = new NetworkRuntimeEngine(scenario.seed, undefined, { swiftCables: true });
if (scenario.speedPercent !== undefined) engine.setSimulationSpeed(scenario.speedPercent); if (scenario.speedPercent !== undefined) engine.setSimulationSpeed(scenario.speedPercent);
engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 }); engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 });
+5 -4
View File
@@ -11,9 +11,6 @@
// client draws differs (Java's random, the engine's seed); it is compared as // client draws differs (Java's random, the engine's seed); it is compared as
// "client". Times count from the client's SYN. // "client". Times count from the client's SYN.
// //
// The engine's calls block on its virtual clock one after the other, where
// Java's server answers in a thread of its own: the server's answer starts
// when the client's send returns, one cable's delay later than in Java.
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
@@ -26,6 +23,10 @@ import type { Scenario } from './scenario.js';
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios'); const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
const STATE_UNSEEN_MILLISECONDS = 20; const STATE_UNSEEN_MILLISECONDS = 20;
// The engine's calls block one after the other: in exchange, the server's
// answer starts when the client's send returns, and the client closes when
// the server's send returns — two cables' delay each, later than in Java.
const STATE_TOLERANCE_MILLISECONDS = 250;
const SERVER = '00000000-0000-0000-0000-000000000101'; const SERVER = '00000000-0000-0000-0000-000000000101';
const CLIENT = '00000000-0000-0000-0000-000000000102'; const CLIENT = '00000000-0000-0000-0000-000000000102';
const OTHER_CLIENT = '00000000-0000-0000-0000-000000000103'; const OTHER_CLIENT = '00000000-0000-0000-0000-000000000103';
@@ -109,7 +110,7 @@ function expectSameStates(label: string, actual: [number, string][], expected: [
for (const [time, state] of expected) { for (const [time, state] of expected) {
const index = actual.findIndex(([, each], candidate) => candidate >= next && each === state); const index = actual.findIndex(([, each], candidate) => candidate >= next && each === state);
expect(index, `${label}: ${state} (Java at ${time} ms)`).toBeGreaterThanOrEqual(0); expect(index, `${label}: ${state} (Java at ${time} ms)`).toBeGreaterThanOrEqual(0);
expect(Math.abs(actual[index]![0] - time), `${label}: time of ${state}`).toBeLessThanOrEqual(GAP_TOLERANCE_MILLISECONDS); expect(Math.abs(actual[index]![0] - time), `${label}: time of ${state}`).toBeLessThanOrEqual(STATE_TOLERANCE_MILLISECONDS);
matched.add(index); matched.add(index);
next = index + 1; next = index + 1;
} }
+4 -1
View File
@@ -33,7 +33,10 @@ npm test # vergleicht mit den geschriebenen *.swift.json
DHCP-Szenarien vergleicht `dhcpJava.test.ts` mit Java-Läufen DHCP-Szenarien vergleicht `dhcpJava.test.ts` mit Java-Läufen
(`tools/java-oracle`, `DhcpRun`), am Swift-Ergebnis prüft der Test nur (`tools/java-oracle`, `DhcpRun`), am Swift-Ergebnis prüft der Test nur
noch, dass sie abweichen. Ebenso seit F2.3c die drei TCP-Szenarien noch, dass sie abweichen. Ebenso seit F2.3c die drei TCP-Szenarien
(`tcpJava.test.ts`, `TcpRun`). (`tcpJava.test.ts`, `TcpRun`). Seit F2.3d befördert ein Kabel des Kerns
einen Frame nach dem anderen wie Java; der Swift-Vergleich startet die
Engine mit `swiftCables` (alle Frames zugleich, wie Swift) und prüft so
alles andere weiter Spur für Spur.
Das Ergebnis enthält die Sockets mit der Länge ihrer Warteschlange, dem Das Ergebnis enthält die Sockets mit der Länge ihrer Warteschlange, dem
TCP-Zustand, dem annehmenden Socket und den Sitzungswerten. TCP-Zustand, dem annehmenden Socket und den Sitzungswerten.