|
| 1 | +/** |
| 2 | + * Benchmark: readSas — parse a 1,000-row SAS XPORT (XPT) file. |
| 3 | + * Outputs JSON: {"function": "read_sas", "mean_ms": ..., "iterations": ..., "total_ms": ...} |
| 4 | + */ |
| 5 | +import { readSas } from "../../src/index.ts"; |
| 6 | + |
| 7 | +// ─── IBM 370 floating-point encoder ────────────────────────────────────────── |
| 8 | + |
| 9 | +function ibmEncode(val: number): Uint8Array { |
| 10 | + const out = new Uint8Array(8); |
| 11 | + if (val === 0) return out; |
| 12 | + if (!Number.isFinite(val)) { out[0] = 0x2e; return out; } |
| 13 | + const sign = val < 0 ? 1 : 0; |
| 14 | + const abs = Math.abs(val); |
| 15 | + let exp = 0; |
| 16 | + let mant = abs; |
| 17 | + while (mant >= 1) { mant /= 16; exp++; } |
| 18 | + while (mant < 1 / 16 && mant > 0) { mant *= 16; exp--; } |
| 19 | + const mantInt = BigInt(Math.round(mant * 2 ** 56)); |
| 20 | + out[0] = (sign << 7) | ((exp + 64) & 0x7f); |
| 21 | + for (let i = 1; i <= 7; i++) { |
| 22 | + out[i] = Number((mantInt >> BigInt((7 - i) * 8)) & 0xffn); |
| 23 | + } |
| 24 | + return out; |
| 25 | +} |
| 26 | + |
| 27 | +// ─── Minimal XPORT v5 builder ──────────────────────────────────────────────── |
| 28 | + |
| 29 | +function buildXpt( |
| 30 | + numVars: readonly string[], |
| 31 | + charVars: readonly { name: string; len: number }[], |
| 32 | + rows: readonly Readonly<Record<string, number | string>>[], |
| 33 | +): Uint8Array { |
| 34 | + const RECORD = 80; |
| 35 | + |
| 36 | + function encodeAscii(s: string, maxLen: number): Uint8Array { |
| 37 | + const buf = new Uint8Array(maxLen); |
| 38 | + for (let i = 0; i < Math.min(s.length, maxLen); i++) buf[i] = s.charCodeAt(i) & 0x7f; |
| 39 | + return buf; |
| 40 | + } |
| 41 | + function padTo80(s: string): Uint8Array { return encodeAscii(s.padEnd(RECORD, " "), RECORD); } |
| 42 | + function writeU16(b: Uint8Array, o: number, v: number) { b[o] = (v >> 8) & 0xff; b[o + 1] = v & 0xff; } |
| 43 | + function writeU32(b: Uint8Array, o: number, v: number) { |
| 44 | + b[o] = (v >> 24) & 0xff; b[o + 1] = (v >> 16) & 0xff; b[o + 2] = (v >> 8) & 0xff; b[o + 3] = v & 0xff; |
| 45 | + } |
| 46 | + |
| 47 | + type Meta = { type: 1 | 2; name: string; len: number; pos: number }; |
| 48 | + const metas: Meta[] = []; |
| 49 | + let pos = 0; |
| 50 | + for (const name of numVars) { metas.push({ type: 1, name, len: 8, pos }); pos += 8; } |
| 51 | + for (const { name, len } of charVars) { metas.push({ type: 2, name, len, pos }); pos += len; } |
| 52 | + const rowLen = pos; |
| 53 | + |
| 54 | + const chunks: Uint8Array[] = []; |
| 55 | + |
| 56 | + // Library header (5 × 80 bytes) |
| 57 | + chunks.push(padTo80("HEADER RECORD*******LIBRARY HEADER RECORD!!!!!!!000000000000000000000000000000 ")); |
| 58 | + chunks.push(padTo80("SAS SAS SASLIB 6.06 ASCII")); |
| 59 | + chunks.push(padTo80("20240101")); |
| 60 | + chunks.push(padTo80("")); |
| 61 | + chunks.push(padTo80("")); |
| 62 | + |
| 63 | + // Member header (3 × 80 bytes) |
| 64 | + chunks.push(padTo80("HEADER RECORD*******MEMBER HEADER RECORD!!!!!!!000000000000000000000000000001600000000140 ")); |
| 65 | + chunks.push(padTo80("SAS BENCH SASDATA 6.06 ASCII")); |
| 66 | + chunks.push(padTo80("")); |
| 67 | + |
| 68 | + // Namestr header |
| 69 | + const nvar = metas.length; |
| 70 | + chunks.push(padTo80(`HEADER RECORD*******NAMESTR HEADER RECORD!!!!!!!${String(nvar).padStart(6, "0")}00000000000000000000 `)); |
| 71 | + |
| 72 | + // Namestr records (140 bytes each) |
| 73 | + const nsBuf = new Uint8Array(nvar * 140); |
| 74 | + for (let i = 0; i < metas.length; i++) { |
| 75 | + const m = metas[i]!; |
| 76 | + const off = i * 140; |
| 77 | + writeU16(nsBuf, off, m.type); |
| 78 | + writeU16(nsBuf, off + 2, 140); |
| 79 | + nsBuf.set(encodeAscii(m.name, 8), off + 4); |
| 80 | + writeU16(nsBuf, off + 52, m.len); |
| 81 | + writeU32(nsBuf, off + 84, m.pos); |
| 82 | + } |
| 83 | + const nsPadded = Math.ceil(nsBuf.length / RECORD) * RECORD; |
| 84 | + const nsPaddedBuf = new Uint8Array(nsPadded); |
| 85 | + nsPaddedBuf.set(nsBuf); |
| 86 | + chunks.push(nsPaddedBuf); |
| 87 | + |
| 88 | + // Obs header |
| 89 | + chunks.push(padTo80("HEADER RECORD*******OBS HEADER RECORD!!!!!!!000000000000000000000000000000 ")); |
| 90 | + |
| 91 | + // Observations |
| 92 | + const paddedRowLen = Math.ceil(rowLen / RECORD) * RECORD; |
| 93 | + const obsBuf = new Uint8Array(rows.length * paddedRowLen); |
| 94 | + for (let r = 0; r < rows.length; r++) { |
| 95 | + const base = r * paddedRowLen; |
| 96 | + const row = rows[r]!; |
| 97 | + for (const m of metas) { |
| 98 | + const val = row[m.name]; |
| 99 | + if (m.type === 1) { |
| 100 | + const encoded = ibmEncode(typeof val === "number" ? val : 0); |
| 101 | + obsBuf.set(encoded, base + m.pos); |
| 102 | + } else { |
| 103 | + const s = typeof val === "string" ? val : ""; |
| 104 | + obsBuf.set(encodeAscii(s, m.len), base + m.pos); |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + chunks.push(obsBuf); |
| 109 | + |
| 110 | + let total = 0; |
| 111 | + for (const c of chunks) total += c.length; |
| 112 | + const out = new Uint8Array(total); |
| 113 | + let off = 0; |
| 114 | + for (const c of chunks) { out.set(c, off); off += c.length; } |
| 115 | + return out; |
| 116 | +} |
| 117 | + |
| 118 | +// ─── Build dataset ──────────────────────────────────────────────────────────── |
| 119 | + |
| 120 | +const ROWS = 1_000; |
| 121 | +const WARMUP = 3; |
| 122 | +const ITERATIONS = 20; |
| 123 | + |
| 124 | +const rows: Readonly<Record<string, number | string>>[] = Array.from({ length: ROWS }, (_, i) => ({ |
| 125 | + id: i, |
| 126 | + value: i * 1.5, |
| 127 | + score: Math.sin(i * 0.01), |
| 128 | + label: `item_${i % 100}`, |
| 129 | +})); |
| 130 | + |
| 131 | +const xpt = buildXpt(["id", "value", "score"], [{ name: "label", len: 12 }], rows); |
| 132 | + |
| 133 | +// ─── Benchmark ──────────────────────────────────────────────────────────────── |
| 134 | + |
| 135 | +for (let i = 0; i < WARMUP; i++) readSas(xpt); |
| 136 | + |
| 137 | +const start = performance.now(); |
| 138 | +for (let i = 0; i < ITERATIONS; i++) readSas(xpt); |
| 139 | +const total = performance.now() - start; |
| 140 | + |
| 141 | +console.log( |
| 142 | + JSON.stringify({ |
| 143 | + function: "read_sas", |
| 144 | + mean_ms: total / ITERATIONS, |
| 145 | + iterations: ITERATIONS, |
| 146 | + total_ms: total, |
| 147 | + }), |
| 148 | +); |
0 commit comments