bmssp 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,15 +34,25 @@ with every building block shipped, tested, and released individually:
34
34
  | Shortest-path reconstruction ([#169](https://github.com/Sirivasv/bmssp-js/issues/169)) | `BMSSP.reconstructPath()` | ✅ done |
35
35
  | Constructor input validation ([#165](https://github.com/Sirivasv/bmssp-js/issues/165)) | `BMSSP` constructor | ✅ done |
36
36
  | Opt-in constant-degree transform, in/out-degree ≤ 2 ([#164](https://github.com/Sirivasv/bmssp-js/issues/164)) | `constantDegreeTransform()` | ✅ done |
37
+ | BMSSP-vs-Dijkstra head-to-head in the benchmark harness ([#170](https://github.com/Sirivasv/bmssp-js/issues/170)) | `npm run bench` / `npm run bench:counts` | ✅ done |
38
+ | Performance-cliff investigation; quadratic `BatchPrepend` fixed ([#182](https://github.com/Sirivasv/bmssp-js/issues/182)) | `src/blockList.mjs` | ✅ done — **1.1.1** |
37
39
 
38
40
  > **Honest note:** the paper's win is asymptotic, and this repo optimizes for correctness
39
41
  > and readability, not raw speed. Measured head-to-head (algorithm time only, graph
40
- > loading excluded — see [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)):
42
+ > loading excluded — rerun it yourself with `npm run bench`; methodology and the deep
43
+ > 1.0.0 record in [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)):
41
44
  > Dijkstra still wins on wall-clock at every practical size, though the gap narrows as
42
45
  > sparse graphs grow (2.5× at 50k nodes → 1.57× at 2M). But in the paper's own metric —
43
46
  > **comparisons between path lengths** — BMSSP does **fewer comparisons than Dijkstra
44
- > from about n = 1M on sparse graphs** (0.91× at n = 2M), and the advantage grows with
45
- > size: the "sorting barrier" is measurably broken; what remains is JS constant factors.
47
+ > from about n = 1M on sparse graphs** (`npm run bench:counts` measures the ratio falling
48
+ > 1.20× at 50k 1.03× at 200k → 0.98× at 1M; 0.91× at 2M in the record), and the
49
+ > advantage grows with size: the "sorting barrier" is measurably broken; what remains is
50
+ > JS constant factors. The two performance cliffs found in the 1.0.0 measurements were
51
+ > run down in [#182](https://github.com/Sirivasv/bmssp-js/issues/182): the star-graph
52
+ > blowup was a quadratic in `BatchPrepend`'s bookkeeping — fixed in `1.1.1` (500k-node
53
+ > star: 61 s → ~3 s) — and the recursion-level step is an inherent, measured ~+24% per
54
+ > extra level (see the addendum in
55
+ > [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)).
46
56
  > Where inputs violate the paper's distinct-path-lengths assumption (e.g. zero-weight
47
57
  > edges), a principled deterministic tie-break
48
58
  > ([#163](https://github.com/Sirivasv/bmssp-js/issues/163)) realizes that assumption in
@@ -145,14 +155,17 @@ Other image versions are on [Docker Hub](https://hub.docker.com/r/sirivasv/bmssp
145
155
 
146
156
  ```bash
147
157
  npm install
148
- npm test # Jest suite — every graph seeded, every failure reproducible (~3 s)
149
- npm run lint # Prettier + ESLint
150
- npm run bench # seeded micro-benchmarks (see benchmarks/README.md)
158
+ npm test # Jest suite — every graph seeded, every failure reproducible (~7 s)
159
+ npm run lint # Prettier + ESLint
160
+ npm run bench # BMSSP-vs-Dijkstra head-to-head per graph shape, outputs verified
161
+ npm run bench:counts # …plus comparison-count tables (the paper's own cost metric)
151
162
  ```
152
163
 
153
- The measured BMSSP-vs-Dijkstra head-to-head wall-clock and comparison counts, with
154
- methodology lives in [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)
155
- ([#170](https://github.com/Sirivasv/bmssp-js/issues/170) tracks harness integration).
164
+ The benchmark harness runs the measured BMSSP-vs-Dijkstra head-to-head on every
165
+ `npm run bench` ([#170](https://github.com/Sirivasv/bmssp-js/issues/170)); methodology,
166
+ the deep 1.0.0 record (up to n = 4M) and the "when to use which" guidance live in
167
+ [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md) and
168
+ [benchmarks/README.md](benchmarks/README.md).
156
169
 
157
170
  The test suite's core contract: for every node, BMSSP's distances must equal the Dijkstra
158
171
  oracle's. A seeded property/fuzz suite (`test/fuzz.test.mjs`) hammers that contract across
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmssp",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Javascript package implementation of the bmssp algorithm.",
5
5
  "main": "index.mjs",
6
6
  "keywords": [
@@ -35,6 +35,7 @@
35
35
  "scripts": {
36
36
  "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --coverage",
37
37
  "bench": "node benchmarks/run.mjs",
38
+ "bench:counts": "node benchmarks/run.mjs --counts",
38
39
  "lint": "npm run prettier && npm run eslint",
39
40
  "format": "npm run prettier:fix && npm run eslint:fix",
40
41
  "eslint": "eslint --max-warnings=0 .",
package/src/blockList.mjs CHANGED
@@ -156,9 +156,13 @@ class BlockList {
156
156
  chunks.push(fresh.slice(i, i + chunkSize));
157
157
  }
158
158
  }
159
- // Prepend in reverse chunk order so the smallest chunk lands at the front
160
- for (let c = chunks.length - 1; c >= 0; c -= 1) {
161
- const chunk = chunks[c];
159
+ // Materialize the chunks as blocks (ascending, so chunk 0 the smallest
160
+ // values ends up frontmost) and prepend them all in one concat. A
161
+ // per-chunk unshift here re-shifts the whole d0 array every time: with a
162
+ // small M the chunk count approaches |L| and the loop turns quadratic —
163
+ // the #182 star-graph blowup (~n single-entry chunks at M = 1).
164
+ const blocks = [];
165
+ for (const chunk of chunks) {
162
166
  // Seed the block bound with the first value, then max-update — avoids
163
167
  // needing a -Infinity sentinel that a custom comparator can't order
164
168
  const block = this.makeBlock(chunk[0][1]);
@@ -168,8 +172,9 @@ class BlockList {
168
172
  if (this.compare(value, block.bound) > 0) block.bound = value;
169
173
  this.count += 1;
170
174
  }
171
- this.d0.unshift(block);
175
+ blocks.push(block);
172
176
  }
177
+ this.d0 = blocks.concat(this.d0);
173
178
  }
174
179
 
175
180
  /**
package/src/tieBreak.mjs CHANGED
@@ -42,6 +42,28 @@
42
42
  // source's own label never loses an equal-(length, hops) comparison.
43
43
  const NO_PRED = -Infinity;
44
44
 
45
+ // Running count of compareKeys calls — the paper's cost metric ("comparisons
46
+ // between path lengths"). Every BMSSP-side comparison funnels through
47
+ // compareKeys (the heap and BlockList receive it as their comparator, and
48
+ // baseCase/findPivots/bmssp call it directly), so this one counter covers the
49
+ // whole algorithm. The benchmark harness (#170) reads it to measure the
50
+ // sorting barrier; one unconditional increment keeps the hot path branch-free.
51
+ let comparisonCount = 0;
52
+
53
+ /** Reset the compareKeys call counter to zero (benchmark instrumentation). */
54
+ function resetComparisonCount() {
55
+ comparisonCount = 0;
56
+ }
57
+
58
+ /**
59
+ * Number of compareKeys calls since the last reset (benchmark
60
+ * instrumentation).
61
+ * @returns {number}
62
+ */
63
+ function getComparisonCount() {
64
+ return comparisonCount;
65
+ }
66
+
45
67
  /**
46
68
  * Lexicographic comparison of two composite keys.
47
69
  * @param {[number, number, *]} a - [length, hops, id]
@@ -49,6 +71,7 @@ const NO_PRED = -Infinity;
49
71
  * @returns {number} Negative when a < b, positive when a > b, 0 when equal
50
72
  */
51
73
  function compareKeys(a, b) {
74
+ comparisonCount += 1;
52
75
  if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
53
76
  if (a[1] !== b[1]) return a[1] < b[1] ? -1 : 1;
54
77
  if (a[2] !== b[2]) return a[2] < b[2] ? -1 : 1;
@@ -139,4 +162,13 @@ function relaxEdge(u, v, weight, dHat, ties, bound) {
139
162
  return { key: [length, hopCount, v], improved: true };
140
163
  }
141
164
 
142
- export { compareKeys, toBound, makeTies, orderKey, relaxEdge, NO_PRED };
165
+ export {
166
+ compareKeys,
167
+ toBound,
168
+ makeTies,
169
+ orderKey,
170
+ relaxEdge,
171
+ NO_PRED,
172
+ resetComparisonCount,
173
+ getComparisonCount,
174
+ };