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
+56 -2
View File
@@ -15,6 +15,8 @@
// one message per attempt, an ARP check that waits a round trip,
// link-local after five failures; round trip times follow the speed)
// 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,
// filius.software.vermittlungsschicht (ARP, IP, ICMP), system.SwitchFirmware,
// filius.software.transportschicht (UDP, TCP), filius.software.dhcp.
@@ -104,6 +106,15 @@ export type ScheduledEventKind =
| { readonly kind: 'dhcpClientStart'; readonly nodeId: Id }
| { readonly kind: 'dhcpTimeout'; readonly nodeId: 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 sourceNodeId: Id;
@@ -498,8 +509,9 @@ function toSeed(seed: bigint | number): bigint {
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 {
return kind.kind === 'ethernetLinkDelivery' || kind.kind === 'remoteLinkDelivery';
return kind.kind === 'ethernetLinkDeparture' || kind.kind === 'ethernetLinkDelivery' || kind.kind === 'remoteLinkDelivery';
}
function isRemoteLinkWaitEvent(kind: ScheduledEventKind): boolean {
@@ -527,11 +539,23 @@ export class NetworkRuntimeEngine {
private readonly packetTraceRetentionPolicy: TraceRetentionPolicy;
private isProcessingScheduledEvents = false;
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(
seed: bigint | number = DEFAULT_SEED,
packetTraceRetentionPolicy: TraceRetentionPolicy = PRODUCTION_TRACE_RETENTION,
options: { readonly swiftCables?: boolean } = {},
) {
this.cablesCarryOneFrameAtATime = options.swiftCables !== true;
const normalizedSeed = toSeed(seed);
this.stateValue = initialState(normalizedSeed);
this.random = new JavaCompatibleRandom(normalizedSeed);
@@ -577,6 +601,7 @@ export class NetworkRuntimeEngine {
this.linkLocalRandom = new JavaCompatibleRandom(seed ^ 0x4c4c_4144n);
this.isProcessingScheduledEvents = false;
this.pendingIPv4TransmissionsByArpKey.clear();
this.cableBusyUntilByPort.clear();
this.initializeRipState();
this.initializeDhcpState();
this.initializeNatState();
@@ -762,6 +787,9 @@ export class NetworkRuntimeEngine {
private processScheduledRuntimeEvent(kind: ScheduledEventKind): void {
switch (kind.kind) {
case 'ethernetLinkDeparture':
this.departEthernetFrame(kind.sourceNodeId, kind.outgoingPortId, { nodeId: kind.targetNodeId, portId: kind.targetPortId }, kind.frame);
return;
case 'ethernetLinkDelivery':
if (this.dropForGlobalPacketLoss(kind.frame, kind.targetNodeId, kind.targetPortId, 'inbound', 'physical')) {
return;
@@ -2583,6 +2611,28 @@ export class NetworkRuntimeEngine {
// ── Ethernet ─────────────────────────────────────────────────────────────
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({
frameIdentity: frame.identity,
packetIdentity: packetIdentityInFrame(frame),
@@ -2593,7 +2643,6 @@ export class NetworkRuntimeEngine {
operation: 'sent',
afterHeaders: ethernetHeaders(frame),
});
const endpoint = this.attachedEndpoint(fromNodeId, outgoingPortId);
if (endpoint === undefined) return;
const latencyMilliseconds = this.runtimeSpeed.linkTransmissionDelayMilliseconds;
const deadlineMilliseconds = this.stateValue.currentTimeMilliseconds + latencyMilliseconds;
@@ -2611,6 +2660,7 @@ export class NetworkRuntimeEngine {
});
return;
}
if (this.cablesCarryOneFrameAtATime) this.cableBusyUntilByPort.set(portKey(fromNodeId, outgoingPortId), deadlineMilliseconds);
this.schedule(deadlineMilliseconds, {
kind: 'ethernetLinkDelivery',
@@ -3666,6 +3716,10 @@ function firewallAddressMatches(packetIPAddress: string, configuredIPAddress: st
return sameSubnet(packetIPAddress, configuredIPAddress, subnetMask);
}
function portKey(nodeId: Id, portId: Id): string {
return `${nodeId}|${portId}`;
}
function arpKey(nodeId: Id, ipAddress: string): string {
return `${nodeId}|${ipAddress}`;
}