mirror of
https://github.com/felixrieseberg/windows95.git
synced 2026-05-09 00:24:09 +00:00
Speed up SMB transfers: TCP windowing, hot-path assembly, gated diagnostics (#370)
This commit is contained in:
@@ -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<typeof setupNbns>[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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user