From b14ea86ceda5908c8cbb3797b2d65161424f3e4a Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Tue, 14 Apr 2026 16:42:22 -0700 Subject: [PATCH] Speed up SMB transfers: TCP windowing, hot-path assembly, gated diagnostics (#370) --- src/renderer/smb/index.ts | 105 +++++++++++++++++++++++-------------- src/renderer/smb/server.ts | 6 ++- src/renderer/smb/smb.ts | 39 +++++++------- 3 files changed, 92 insertions(+), 58 deletions(-) diff --git a/src/renderer/smb/index.ts b/src/renderer/smb/index.ts index 84365bf..67c2e79 100644 --- a/src/renderer/smb/index.ts +++ b/src/renderer/smb/index.ts @@ -8,26 +8,28 @@ // Then inside Win95: Start → Run → \\192.168.86.1\host import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; import { NetBIOSFramer, nbPositiveResponse, nbWrap } from "./netbios"; import { setupNbns } from "./nbns"; import { SmbSession, shareNameFor, TOOLS_SHARE } from "./server"; -// SPIKE diagnostics: tee everything to a file so we can debug without DevTools -const LOG_FILE = process.env.WIN95_SMB_LOG || path.join(os.tmpdir(), "windows95-smb.log"); -try { fs.writeFileSync(LOG_FILE, `--- ${new Date().toISOString()} ---\n`); } catch {} -const origLog = console.log; -console.log = (...args: unknown[]) => { - origLog(...args); - const tag = String(args[0] ?? ""); - if (tag === "[smb]" || tag === "[nbns]") { - try { - fs.appendFileSync(LOG_FILE, args.map(a => - typeof a === "string" ? a : JSON.stringify(a)).join(" ") + "\n"); - } catch {} - } -}; +// Diagnostics tee — opt-in via WIN95_SMB_LOG. The console.log override and +// per-frame counter below sit on the hot path; don't pay for them unless +// someone is actually watching. +const LOG_FILE = process.env.WIN95_SMB_LOG; +if (LOG_FILE) { + try { fs.writeFileSync(LOG_FILE, `--- ${new Date().toISOString()} ---\n`); } catch {} + const origLog = console.log; + console.log = (...args: unknown[]) => { + origLog(...args); + const tag = String(args[0] ?? ""); + if (tag === "[smb]" || tag === "[nbns]") { + try { + fs.appendFileSync(LOG_FILE, args.map(a => + typeof a === "string" ? a : JSON.stringify(a)).join(" ") + "\n"); + } catch {} + } + }; +} interface TCPConnection { sport: number; @@ -67,29 +69,31 @@ export function setupSmbShare(emulator: V86, hostPath: string | null, toolsRoot? : log(`port 139 hooked, no host folder shared yet`); announce(); - // SPIKE diagnostic: count every ethernet frame so we know if the NIC is - // emitting anything at all (DHCP, ARP, anything). Logged on a timer so - // we don't flood — and so the absence of a tick proves the bus is dead. - let frameStats = { total: 0, arp: 0, ip: 0, udp: 0, tcp: 0, other: 0 }; - emulator.bus.register("net0-send", (raw: unknown) => { - const f = raw as Uint8Array; - frameStats.total++; - if (f.length < 14) { frameStats.other++; return; } - const et = (f[12] << 8) | f[13]; - if (et === 0x0806) frameStats.arp++; - else if (et === 0x0800) { - frameStats.ip++; - const proto = f[14 + 9]; - if (proto === 6) frameStats.tcp++; - else if (proto === 17) frameStats.udp++; - } else frameStats.other++; - }); - setInterval(() => { - if (frameStats.total > 0) { - log("frames:", JSON.stringify(frameStats)); - frameStats = { total: 0, arp: 0, ip: 0, udp: 0, tcp: 0, other: 0 }; - } - }, 5000); + if (LOG_FILE) { + // Count every ethernet frame so we know if the NIC is emitting anything + // at all. Logged on a timer so the absence of a tick proves the bus is + // dead. Opt-in: this hook fires once per TX frame during a file copy. + let frameStats = { total: 0, arp: 0, ip: 0, udp: 0, tcp: 0, other: 0 }; + emulator.bus.register("net0-send", (raw: unknown) => { + const f = raw as Uint8Array; + frameStats.total++; + if (f.length < 14) { frameStats.other++; return; } + const et = (f[12] << 8) | f[13]; + if (et === 0x0806) frameStats.arp++; + else if (et === 0x0800) { + frameStats.ip++; + const proto = f[14 + 9]; + if (proto === 6) frameStats.tcp++; + else if (proto === 17) frameStats.udp++; + } else frameStats.other++; + }); + setInterval(() => { + if (frameStats.total > 0) { + log("frames:", JSON.stringify(frameStats)); + frameStats = { total: 0, arp: 0, ip: 0, udp: 0, tcp: 0, other: 0 }; + } + }, 5000); + } // Win95 won't even try TCP 139 until UDP 137 answers a Node Status query setupNbns(emulator as Parameters[0]); @@ -136,6 +140,29 @@ export function setupSmbShare(emulator: V86, hostPath: string | null, toolsRoot? } else { (conn as any).on_data = handler; } + + // v86's TCP is stop-and-wait (one MSS, wait for ACK). The link is lossless + // and has no retransmit anyway, so keep a window in flight by sliding the + // ring-buffer view under the original pump(). 8×MSS cap ≈ NE2000 RX ring. + const c = conn as any, sb = c.send_buffer; + const mss: number = c.send_chunk_buf?.length ?? 1460; + const pump1 = Object.getPrototypeOf(c)?.pump; + if (pump1 && sb?.buffer) { + let hi: number | undefined; + c.pump = function () { + if (this.pending || !sb.length) return pump1.call(this); + const cap = sb.buffer.length, t0 = sb.tail, l0 = sb.length, s0 = this.seq; + const win = Math.max(mss, Math.min(this.winsize || 8192, 8 * mss)); + let off = hi === undefined ? 0 : Math.max(0, Math.min(hi - s0, l0)); + for (; off < l0 && off < win; off += Math.min(mss, l0 - off)) { + sb.tail = (t0 + off) % cap; sb.length = l0 - off; + this.seq = s0 + off; this.pending = false; + pump1.call(this); + } + hi = s0 + off; + sb.tail = t0; sb.length = l0; this.seq = s0; this.pending = true; + }; + } return true; }; diff --git a/src/renderer/smb/server.ts b/src/renderer/smb/server.ts index 9b1a4f2..45f1e8b 100644 --- a/src/renderer/smb/server.ts +++ b/src/renderer/smb/server.ts @@ -670,7 +670,11 @@ export class SmbSession { new Uint8Array(0), new Uint8Array(0)); } const words = new Writer().u16(data.length).zero(8).build(); - const bytes = new Writer().u8(0x01).u16(data.length).bytes(data).build(); + const bytes = new Uint8Array(3 + data.length); + bytes[0] = 0x01; + bytes[1] = data.length & 0xff; + bytes[2] = (data.length >> 8) & 0xff; + bytes.set(data, 3); return buildSmb(req, CMD_READ, 0, words, bytes); } diff --git a/src/renderer/smb/smb.ts b/src/renderer/smb/smb.ts index cdc5778..2c5e6f0 100644 --- a/src/renderer/smb/smb.ts +++ b/src/renderer/smb/smb.ts @@ -4,7 +4,7 @@ // SMB1 message = 32-byte header + word block + byte block. // Header is at a fixed offset; word/byte blocks vary by command. -import { Reader, Writer } from "./wire"; +import { Reader } from "./wire"; export const SMB_MAGIC = [0xff, 0x53, 0x4d, 0x42]; // \xFF SMB @@ -106,25 +106,28 @@ export function buildSmb( bytes: Uint8Array, overrides?: { tid?: number; uid?: number; flags2?: number } ): Uint8Array { - const w = new Writer(); - w.bytes(SMB_MAGIC); - w.u8(cmd); - w.u32(status); - w.u8(FLAGS_REPLY | FLAGS_CASELESS | FLAGS_CANONICAL); + if (words.length % 2 !== 0) throw new Error("word block must be even"); + // Hot path for READ replies (bytes can be ~16K) — assemble directly instead + // of pushing byte-by-byte through Writer. + const out = new Uint8Array(32 + 1 + words.length + 2 + bytes.length); + const v = new DataView(out.buffer); + out[0] = 0xff; out[1] = 0x53; out[2] = 0x4d; out[3] = 0x42; + out[4] = cmd; + v.setUint32(5, status, true); + out[9] = FLAGS_REPLY | FLAGS_CASELESS | FLAGS_CANONICAL; // mirror long-name capability so the client keeps sending long names; never // claim NT status or unicode (we reply in ASCII) - w.u16((overrides?.flags2 ?? req.flags2) & FLAGS2_LONG_NAMES); - w.zero(12); - w.u16(overrides?.tid ?? req.tid); - w.u16(req.pid); - w.u16(overrides?.uid ?? req.uid); - w.u16(req.mid); - if (words.length % 2 !== 0) throw new Error("word block must be even"); - w.u8(words.length / 2); - w.bytes(words); - w.u16(bytes.length); - w.bytes(bytes); - return w.build(); + v.setUint16(10, (overrides?.flags2 ?? req.flags2) & FLAGS2_LONG_NAMES, true); + // 12 bytes reserved already zero + v.setUint16(24, overrides?.tid ?? req.tid, true); + v.setUint16(26, req.pid, true); + v.setUint16(28, overrides?.uid ?? req.uid, true); + v.setUint16(30, req.mid, true); + out[32] = words.length / 2; + out.set(words, 33); + v.setUint16(33 + words.length, bytes.length, true); + out.set(bytes, 35 + words.length); + return out; } export function dosError(errClass: number, errCode: number): number {