From 1f1bfa4f334ad762403ef45496b6d37dac23f150 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:57:22 +0000 Subject: [PATCH 1/3] Initial plan From 0f7ce444cfa92c0e3a45602b386b3f63d8772a56 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Apr 2026 03:55:56 +0000 Subject: [PATCH 2/3] Iteration 12: LSD 8-pass radix sort on IEEE-754 transformed keys Replace Float64Array comparator sort with a callback-free LSD radix sort. Eliminates ~1.6M JS comparator invocations at n=100k (the dominant bottleneck). Module-level ping-pong buffers avoid per-call allocation. IEEE-754 transform maps float64 to sortable uint64 keys. String/mixed fallback unchanged. Run: https://github.com/githubnext/tsessebe/actions/runs/24921830984 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/core/series.ts | 162 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 135 insertions(+), 27 deletions(-) diff --git a/src/core/series.ts b/src/core/series.ts index b0fa064b..4689cb42 100644 --- a/src/core/series.ts +++ b/src/core/series.ts @@ -130,6 +130,20 @@ function pearsonCorrFromArrays( return denom === 0 ? Number.NaN : num / denom; } +// ─── LSD radix sort buffers (module-level, grown lazily) ───────────────────── + +/** Ping-pong index buffers for the 8-pass LSD radix sort numeric fast path. */ +let _rxA_idx: Uint32Array = new Uint32Array(0); +let _rxB_idx: Uint32Array = new Uint32Array(0); +/** Low 32 bits of each element's IEEE-754 sortable key (ping-pong). */ +let _rxA_lo: Uint32Array = new Uint32Array(0); +let _rxB_lo: Uint32Array = new Uint32Array(0); +/** High 32 bits of each element's IEEE-754 sortable key (ping-pong). */ +let _rxA_hi: Uint32Array = new Uint32Array(0); +let _rxB_hi: Uint32Array = new Uint32Array(0); +/** 256-bucket histogram reused every pass (never reallocated). */ +const _rxCnt: Uint32Array = new Uint32Array(256); + // ─── SeriesOptions ──────────────────────────────────────────────────────────── /** Constructor options accepted by `Series`. */ @@ -716,8 +730,7 @@ export class Series { const vals = this._values; // Pre-partition NaN/null/undefined from finite values in one pass. - // fvals stores numeric values by original row index so the sort comparator - // can read a typed Float64Array (not a generic T[]) at index a/b. + // fvals stores numeric values by original row index (sparse: fvals[origIdx]). const finBuf = new Uint32Array(n); const nanBuf = new Uint32Array(n); const fvals = new Float64Array(n); @@ -727,41 +740,132 @@ export class Series { for (let i = 0; i < n; i++) { const v = vals[i]; if (v === null || v === undefined || (typeof v === "number" && Number.isNaN(v))) { - nanBuf[nanCount++] = i; + nanBuf[nanCount] = i; + nanCount = nanCount + 1; } else { if (typeof v === "number") { fvals[i] = v; } else { allNumeric = false; } - finBuf[finCount++] = i; + finBuf[finCount] = i; + finCount = finCount + 1; } } - // Sort the finite-index slice in-place. - // For all-numeric data use the Float64Array subtraction comparator — - // monomorphic, branchless, and JIT-specialisable. - // For mixed/string data fall back to the generic branch comparator. const finSlice = finBuf.subarray(0, finCount); - if (allNumeric) { + + if (allNumeric && finCount > 0) { + // ── LSD radix sort: 8 passes × 8 bits over IEEE-754 transformed keys ── + // Eliminates all JS comparator callbacks (the bottleneck at n≥10k). + + // Grow module-level ping-pong buffers if needed. + if (_rxA_idx.length < finCount) { + _rxA_idx = new Uint32Array(finCount); + _rxB_idx = new Uint32Array(finCount); + _rxA_lo = new Uint32Array(finCount); + _rxB_lo = new Uint32Array(finCount); + _rxA_hi = new Uint32Array(finCount); + _rxB_hi = new Uint32Array(finCount); + } + + // fvals is a Float64Array; reinterpret its buffer as Uint32 to read raw bits. + // On little-endian (x86/ARM): u32[2i] = lo 32 bits, u32[2i+1] = hi 32 bits. + const fvalsU32 = new Uint32Array(fvals.buffer); + + // Initialise ping arrays with identity indices and IEEE-754 sort keys. + // Transform: positive floats → XOR sign bit; negative → XOR all bits. + // This maps floats to an unsigned integer order that matches numeric order. + for (let i = 0; i < finCount; i++) { + const origIdx = finSlice[i]!; + _rxA_idx[i] = origIdx; + let lo = fvalsU32[origIdx * 2]!; + let hi = fvalsU32[origIdx * 2 + 1]!; + if (hi & 0x80000000) { + lo = (~lo) >>> 0; + hi = (~hi) >>> 0; + } else { + hi = (hi ^ 0x80000000) >>> 0; + } + _rxA_lo[i] = lo; + _rxA_hi[i] = hi; + } + + // 8-pass LSD: passes 0–3 over lo word, passes 4–7 over hi word. + let srcIdx = _rxA_idx; + let dstIdx = _rxB_idx; + let srcLo = _rxA_lo; + let dstLo = _rxB_lo; + let srcHi = _rxA_hi; + let dstHi = _rxB_hi; + + for (let pass = 0; pass < 8; pass++) { + // Build histogram for this byte. + _rxCnt.fill(0); + const useHi = pass >= 4; + const shift = (pass % 4) * 8; + for (let i = 0; i < finCount; i++) { + const word = useHi ? srcHi[i]! : srcLo[i]!; + const bucket = (word >>> shift) & 0xff; + const c = _rxCnt[bucket]!; + _rxCnt[bucket] = c + 1; + } + // Prefix sum → scatter offsets. + let total = 0; + for (let b = 0; b < 256; b++) { + const c = _rxCnt[b]!; + _rxCnt[b] = total; + total = total + c; + } + // Scatter elements into destination. + for (let i = 0; i < finCount; i++) { + const word = useHi ? srcHi[i]! : srcLo[i]!; + const bucket = (word >>> shift) & 0xff; + const p = _rxCnt[bucket]!; + _rxCnt[bucket] = p + 1; + dstIdx[p] = srcIdx[i]!; + dstLo[p] = srcLo[i]!; + dstHi[p] = srcHi[i]!; + } + // Swap ping-pong references. + const ti = srcIdx; + srcIdx = dstIdx; + dstIdx = ti; + const tl = srcLo; + srcLo = dstLo; + dstLo = tl; + const th = srcHi; + srcHi = dstHi; + dstHi = th; + } + + // After 8 passes (even number), srcIdx holds ascending sorted original indices. if (ascending) { - finSlice.sort((a, b) => fvals[a]! - fvals[b]!); + for (let i = 0; i < finCount; i++) { + finSlice[i] = srcIdx[i]!; + } } else { - finSlice.sort((a, b) => fvals[b]! - fvals[a]!); + for (let i = 0, j = finCount - 1; i < finCount; i = i + 1, j = j - 1) { + finSlice[i] = srcIdx[j]!; + } + } + } else if (!allNumeric) { + // String / mixed dtype: fall back to comparator-based sort. + if (ascending) { + finSlice.sort((a, b) => { + const av = vals[a] as number | string | boolean; + const bv = vals[b] as number | string | boolean; + return av < bv ? -1 : av > bv ? 1 : 0; + }); + } else { + finSlice.sort((a, b) => { + const av = vals[a] as number | string | boolean; + const bv = vals[b] as number | string | boolean; + return av > bv ? -1 : av < bv ? 1 : 0; + }); } - } else if (ascending) { - finSlice.sort((a, b) => { - const av = vals[a] as number | string | boolean; - const bv = vals[b] as number | string | boolean; - return av < bv ? -1 : av > bv ? 1 : 0; - }); - } else { - finSlice.sort((a, b) => { - const av = vals[a] as number | string | boolean; - const bv = vals[b] as number | string | boolean; - return av > bv ? -1 : av < bv ? 1 : 0; - }); } + // else: allNumeric && finCount === 0 — nothing to sort. // Build the output permutation and gather values in a single pass. const perm = new Array(n); @@ -771,23 +875,27 @@ export class Series { for (let i = 0; i < nanCount; i++) { const idx = nanBuf[i]!; perm[pos] = idx; - outData[pos++] = vals[idx] as T; + outData[pos] = vals[idx] as T; + pos = pos + 1; } for (let i = 0; i < finCount; i++) { const idx = finSlice[i]!; perm[pos] = idx; - outData[pos++] = vals[idx] as T; + outData[pos] = vals[idx] as T; + pos = pos + 1; } } else { for (let i = 0; i < finCount; i++) { const idx = finSlice[i]!; perm[pos] = idx; - outData[pos++] = vals[idx] as T; + outData[pos] = vals[idx] as T; + pos = pos + 1; } for (let i = 0; i < nanCount; i++) { const idx = nanBuf[i]!; perm[pos] = idx; - outData[pos++] = vals[idx] as T; + outData[pos] = vals[idx] as T; + pos = pos + 1; } } From b33273e6044b0d83b35b48d641cb23ead9233253 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:56:29 +0000 Subject: [PATCH 3/3] Fix Biome formatter error in series.ts radix sort Agent-Logs-Url: https://github.com/githubnext/tsessebe/sessions/2bd09cf8-6acb-4a34-94b0-e847e4b272ba Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- src/core/series.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/series.ts b/src/core/series.ts index 4689cb42..22d672a3 100644 --- a/src/core/series.ts +++ b/src/core/series.ts @@ -782,8 +782,8 @@ export class Series { let lo = fvalsU32[origIdx * 2]!; let hi = fvalsU32[origIdx * 2 + 1]!; if (hi & 0x80000000) { - lo = (~lo) >>> 0; - hi = (~hi) >>> 0; + lo = ~lo >>> 0; + hi = ~hi >>> 0; } else { hi = (hi ^ 0x80000000) >>> 0; }