@vernikr/size-report 2.8.2 → 2.8.3

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
@@ -10,7 +10,7 @@ shows.
10
10
 
11
11
  ## Status
12
12
 
13
- **Release 2.8.2 (2026-09-18).** The tool lives as a package of its own: the registry name is
13
+ **Release 2.8.3 (2026-09-18).** The tool lives as a package of its own: the registry name is
14
14
  `@vernikr/size-report` (published by tag from CI, with no secret). A project may keep no settings at
15
15
  all: without a config file the tool derives them from the project itself and says so in one line,
16
16
  and `--init` pins what was derived into a file. The report is **one file**, the self-contained page
@@ -19,6 +19,19 @@ is installed and on the first run. The version is in the manifest, and every rel
19
19
  the journal — `worklog/` for today's entries, `worklog/archive/WORKLOG.md` for the earlier ones:
20
20
  what changes in the numbers is measured rather than retold.
21
21
 
22
+ 2.8.3 is about what a sideways scroll of the report costs, and its figures are measured rather than retold. **A step
23
+ sideways moves the window instead of building it:** a row is made once and lives through the move, keeping its node and
24
+ its numbers — the window's columns are the range of that row's numbers, so the columns that entered it are made and the
25
+ ones that left are dropped, the header's line of names and its line of metrics by the same rule. A switch of the
26
+ reader's own and the shell's resize are the one thing that cannot be a move — a column that is not built cannot be
27
+ shown — and they build the window from nothing as they did. **Measured on this repository's own report in live Chrome**
28
+ (`probes/step-12-window.mjs`, two rounds, 1440 × 900): the page's own handling of one step **10.4 → 0.6 ms** (worst
29
+ 19.2 → 3.6) and the layout the step forces **14.4 → 5.9 ms**; over a sweep of forty steps the browser's own accounting
30
+ gives layout **0.605 → 0.246 s**, style recalculation 0.169 → 0.056 s and task time **1.281 → 0.350 s**. The checks go
31
+ 86 → **87** in the fast profile and 191 → **192** in the full one: `test/page-grid.test.js` holds the rule by node
32
+ identity — a page that built the same numbers again would read the same — and by the numbers of the window built at
33
+ that place in one leap. The figures and the reasoning stand in `worklog/0211-sideways-step.md`.
34
+
22
35
  2.8.2 is about the page's own sources and the checks that guard them, and its figures are measured rather than retold.
23
36
  **The four page suites that went quiet with the `<table>` are alive again against the window:** `test/page-view.test.js`,
24
37
  `page-cols`, `page-tree` and `page-choice` read the grid and the panel they guard rather than markup that no longer
@@ -271,8 +284,8 @@ commits and installs hooks; the reason for each expensive file is named line by
271
284
 
272
285
  | Run | Command | Checks |
273
286
  |---|---|---|
274
- | Fast — every edit | `pnpm test` | **86 of 191** |
275
- | Full — release and CI | `pnpm test:all` | **191** |
287
+ | Fast — every edit | `pnpm test` | **87 of 192** |
288
+ | Full — release and CI | `pnpm test:all` | **192** |
276
289
 
277
290
  No check is lost or weakened: the full run starts every file, the fast one takes part of them. The default is the full run — a file becomes fast only explicitly and with a reason — so new
278
291
  expensive work cannot quietly move into the fast one. Two declarations guard that:
@@ -407,7 +420,10 @@ right edge of the pinned commit column rather than under it, and every line of t
407
420
  cell, along a group's left edge — is one pixel of one colour: a heavier line would promise a hierarchy to work out
408
421
  rather than read. The geometry is four figures — a column, a row, the header, the pinned column — written in the styling
409
422
  and in the script that counts the window's ordinals in them, and `test/page-grid.test.js` reads both and holds them
410
- together.
423
+ together. **A step sideways moves that window rather than building it:** the rows live through the move and only the
424
+ columns that entered are made, at 10.4 → **0.6 ms** for the page's own handling of the step, 14.4 → **5.9 ms** for the
425
+ layout it forces and 1.281 → **0.350 s** of task time over a sweep of forty steps
426
+ (`probes/step-12-window.mjs`).
411
427
 
412
428
  **A click builds the window again, and that is cheap now.** A file switched off is simply not among the columns that
413
429
  are built, and a metric switched off not among the metrics: there is nothing to hide and nothing to carry, and the
@@ -586,7 +602,7 @@ acceptance for each.
586
602
  | `tools/parity-freeze.js` | Takes the parity reference (`pnpm run parity`): with the frozen copy, at the project revision from the manifest — `--json`, the config, the artifact's hash, the tool's hash |
587
603
  | `tools/make-fixture.js` | Assembles the synthetic fixture (`pnpm run fixture`): a deterministic history with traps plus the reference numbers |
588
604
  | `tools/synthetic/` | The subjects of that assembly, one per matter: `repo.js` — how git is spoken to (pinned time, author, settings), `content.js` — what the files hold, `history.js` — which commits come of it, `note.js` — the fixture's note with the list of traps |
589
- | `probes/` | The scripted measurements behind `plans/2026-09-17-page-perf/`: one file per step, run by hand against live Chrome at the debug port — the fixed layout, where `content-visibility` acts at all, and the two border models with their pixels (`probes/README.md`). Outside the sensors' paths on purpose: they measure the product rather than being part of it, and a suite cannot see layout, paint or a browser's own skipping |
605
+ | `probes/` | The scripted measurements behind `plans/2026-09-17-page-perf/`: one file per step, run by hand against live Chrome at the debug port — the fixed layout, where `content-visibility` acts at all, the price of a switch and of a step sideways — (`probes/README.md`). Outside the sensors' paths on purpose: they measure the product rather than being part of it, and a suite cannot see layout, paint or a browser's own skipping |
590
606
  | `tools/parity-live.js` | Compares the engine with the live project on a clone: the numbers and the self-contained report at the path the consumer's settings give (`pnpm run parity:live`) |
591
607
  | `tools/pack-check.js` | Assembles the tarball and checks that everything works from it: all sources arrived, the numbers and the report as from the repository (`pnpm run pack:check`) |
592
608
  | `tools/check-standards.js` | Checks that both references reproduce: a re-take goes nowhere and is compared with what is committed (our files byte for byte, the bundle by content), and that the live-history bundle carries `HEAD` (`pnpm run check:standards`) |
@@ -622,7 +638,7 @@ acceptance for each.
622
638
  | `src/page/state.js` | The page's state: the report's data (the block unrolled by the payload chapter), the view of the checkboxes, the pointer "which path is which column", the unfolded folders, the record's passport, the browser's memory and the link that is read out of the address — a chapter of the page's program |
623
639
  | `src/page/dom.js` | The page's nodes: the small helpers of markup (`appEl`, `appBox`) — one set for the panel and the table alike |
624
640
  | `src/page/panel.js` | The panel of choices: the switches of metrics and files, the categories, the tree of the project's paths (files outside the report keep a checkbox off with a reason and stand after the rest, and a hidden name after every visible one; the tree opens folded and folders carry a sign that hides the subtree by a class rather than by a rebuild); built once, with the fields of the switches and of the folders and categories written where they stand |
625
- | `src/page/table.js` | The page's table as a window: the rows and the columns the reader can see (plus four beyond each edge), a cell, a commit's caption, the header, the empty states and the order of the columns — markup over the shared calculation, with the totals counted per row from the choice, the geometry in pixels that the styling mirrors, and the reason a library was not taken |
641
+ | `src/page/table.js` | The page's table as a window: the rows and the columns the reader can see (plus four beyond each edge), moved by a scroll rather than built again (a row lives through a step sideways and only the columns that entered it are made), a cell, a commit's caption, the header, the empty states and the order of the columns — markup over the shared calculation, with the totals counted per row from the choice, the geometry in pixels that the styling mirrors, and the reason a library was not taken |
626
642
  | `src/page/app.js` | Assembling and starting the page: the first drawing, then a switch that builds the window again and writes the fields it reached without making a node of the panel; an anchor change; pasted into the assembled page |
627
643
  | `src/page/build.js` | Assembling the page: data, styling and program in one file with no external references — the pasted text is **squeezed** on the way in (comments and indentation out, the same stripping the `min` metric counts) while the sources keep them, and the result is guarded by the stripper's own `assertCompilable` |
628
644
  | `src/git.js` | The only border where git is called: the pinned settings, blobs by the batch, the history, the comparison with the working tree |
@@ -755,7 +771,7 @@ The same release can be taken by a reference to the repository — installation
755
771
  registry, but stays tied to a revision:
756
772
 
757
773
  ```bash
758
- pnpm add -D github:vernikr/size-report#v2.8.2
774
+ pnpm add -D github:vernikr/size-report#v2.8.3
759
775
  ```
760
776
 
761
777
  With no network (or nothing to fetch from codeload) — the tarball: `pnpm pack` in the package clone, then
@@ -767,7 +783,7 @@ the branch moves on the installation fails with `Could not resolve <sha> to a co
767
783
  observation rather than reasoning: the short pin `6530237` installed while `main` stood on it and stopped
768
784
  working at the very next commit, while the same sha in full installed. A branch name (`#main`) and a tag
769
785
  are both accepted, but a branch is a moving target and a tag is constant: this release stands on the tag
770
- `v2.8.2`, which is also the one in the example (forty characters work as well, but they have to be copied
786
+ `v2.8.3`, which is also the one in the example (forty characters work as well, but they have to be copied
771
787
  out of the history by eye).
772
788
 
773
789
  The revision in the example is a part of the claim rather than decoration: what is described below is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vernikr/size-report",
3
- "version": "2.8.2",
3
+ "version": "2.8.3",
4
4
  "author": "vernikr",
5
5
  "repository": {
6
6
  "type": "git",
@@ -65,7 +65,7 @@
65
65
  "report"
66
66
  ],
67
67
  "devDependencies": {
68
- "@vernikr/size-report": "2.8.1",
68
+ "@vernikr/size-report": "2.8.2",
69
69
  "c8": "10",
70
70
  "dependency-cruiser": "17",
71
71
  "eslint": "^9.18.0",
package/src/page/table.js CHANGED
@@ -31,6 +31,14 @@ import { appData, appUi, appView } from './state.js';
31
31
  * table, against 6.5 ms for building the whole window again). The header sticks to the shell's top and the commit
32
32
  * column to its left (both `position: sticky`), so the row and the column a number belongs to are always in sight.
33
33
  *
34
+ * **A step down the table and a step sideways cost the same kind of work.** A row is made once and then stands at its
35
+ * own place in the scrolled content while the window moves over it: the window's columns are the range of that row's
36
+ * numbers, so a step sideways makes the columns that entered and drops the ones that left (`appStrip`) — a cell per
37
+ * row, the header's two lines included. A row that left the window vertically is dropped and made again when the
38
+ * reader comes back to it, which is what makes a jump down the table cost the same as a step. Building the rows again
39
+ * on every sideways scroll — the first version of this step — is what `probes/step-12-window.mjs` measures against the
40
+ * splice (`worklog/0211-sideways-step.md`).
41
+ *
34
42
  * The choice is applied by building the window again: the columns of a switched-off file are simply not among the
35
43
  * columns that are built, so there is nothing to hide and nothing to recount — the totals are the sum over the files
36
44
  * that are on, counted by `rowModel` for the rows the window holds (`src/derived.js`, one place for that
@@ -138,6 +146,27 @@ export function appState(metricsCount, filesCount) {
138
146
  document.getElementById('shell').hidden = metricsCount === 0;
139
147
  }
140
148
 
149
+ /* A strip of pieces of one range, moved to another: the pieces the two ranges share are kept, and only the ends are
150
+ * made and dropped. The unit of a strip is its child — a row's numbers are one column to a child, the header's line of
151
+ * names one group, its line of metrics one column — so the same movement serves all three, and a group of metrics
152
+ * cannot be split by the window (a column of numbers would lose its caption). A window that jumped clear of the old
153
+ * range — the reader dragged the scrollbar — has nothing to keep and the strip is made again; the empty range
154
+ * `[c, c - 1]` is how a strip that holds nothing yet says so. */
155
+ function appStrip(strip, was, now, make) {
156
+ const a = Math.max(was[0], now[0]);
157
+ const b = Math.min(was[1], now[1]);
158
+ if (a > b) {
159
+ strip.textContent = '';
160
+ for (let c = now[0]; c <= now[1]; c++) strip.appendChild(make(c));
161
+ return;
162
+ }
163
+ for (let n = a - was[0]; n > 0; n--) strip.firstChild.remove();
164
+ for (let n = was[1] - b; n > 0; n--) strip.lastChild.remove();
165
+ const edge = strip.firstChild;
166
+ for (let c = a - 1; c >= now[0]; c--) strip.insertBefore(make(c), edge);
167
+ for (let c = b + 1; c <= now[1]; c++) strip.appendChild(make(c));
168
+ }
169
+
141
170
  /* One cell of the window: the column decides what it holds. The group is the column's ordinal divided by the number of
142
171
  * metrics — `0` is the total over the files, `g ≥ 1` is the g-th column of the order — and it is the same arithmetic
143
172
  * the header is placed by, so a column and its caption cannot drift apart. The model lists the *enabled* files in the
@@ -153,9 +182,13 @@ function appCell(model, c, now, cache) {
153
182
  return appNum(now ? valueParts(cell) : cellParts(cell.value, cell.delta, '−'), cls);
154
183
  }
155
184
 
156
- /* A row of the window: the caption of the commit and the numbers of the columns the window holds. The top row is the
157
- * state at HEAD (`nowModel`) rather than the last commit's cells: an absolute number stands in the table once, and
158
- * the deltas below it add up to it. */
185
+ /* A row of the window: the caption of the commit and an empty strip of numbers for the window to fill (`appCells`).
186
+ * The top row is the state at HEAD (`nowModel`) rather than the last commit's cells: an absolute number stands in the
187
+ * table once, and the deltas below it add up to it.
188
+ *
189
+ * The model is kept in the entry beside the node because the numbers of a row depend on the commit and the choice
190
+ * rather than on the place the window stands at: a step sideways asks for a cell's arithmetic, never for `rowModel`
191
+ * again. The strip is born empty — the range `[c0, c0 - 1]` — and is filled by the first `appCells`. */
159
192
  function appRow(cache, r, span) {
160
193
  const last = appData.rows.length;
161
194
  const now = r === 0;
@@ -163,28 +196,36 @@ function appRow(cache, r, span) {
163
196
  const model = now
164
197
  ? nowModel(appData.now, cache.keys, appView.files)
165
198
  : rowModel(appData.rows[i].values, i === 0 ? null : appData.rows[i - 1].values, cache.keys, appView.files);
166
- const row = appEl('div', 'row' + (now ? ' now' : ''));
167
- row.style.top = (APP_HEAD + r * APP_ROW) + 'px';
199
+ const el = appEl('div', 'row' + (now ? ' now' : ''));
200
+ el.style.top = (APP_HEAD + r * APP_ROW) + 'px';
168
201
  const commit = appEl('div', 'c-commit');
169
202
  if (now) commit.textContent = appUi.now;
170
203
  else commit.appendChild(appCommit(appData.rows[i]));
171
- row.appendChild(commit);
204
+ el.appendChild(commit);
172
205
  const cells = appEl('div', 'cells');
173
- /* The numbers stand beside the commit column rather than under it: the column is pinned over the content, so what the
174
- * grid holds begins where the column ends. */
175
- cells.style.left = (APP_COMMIT + span.c0 * APP_COL) + 'px';
176
- for (let c = span.c0; c <= span.c1; c++) cells.appendChild(appCell(model, c, now, cache));
177
- row.appendChild(cells);
178
- return row;
206
+ el.appendChild(cells);
207
+ return { el: el, cells: cells, model: model, now: now, cols: [span.c0, span.c0 - 1] };
208
+ }
209
+
210
+ /* A row's numbers at the window's columns: the columns the two windows share keep their cells, and a column that
211
+ * entered is made. The numbers stand beside the pinned commit column rather than under it — the column lies over the
212
+ * content — so the strip is placed by the window's first column, and the cells of it follow their strip. A step down
213
+ * the table asks for the columns it already holds, and the row is not touched at all. */
214
+ function appCells(entry, cache, span) {
215
+ const cols = [span.c0, span.c1];
216
+ if (entry.cols[0] === cols[0] && entry.cols[1] === cols[1]) return;
217
+ entry.cells.style.left = (APP_COMMIT + cols[0] * APP_COL) + 'px';
218
+ appStrip(entry.cells, entry.cols, cols, (c) => appCell(entry.model, c, entry.now, cache));
219
+ entry.cols = cols;
179
220
  }
180
221
 
181
222
  /* A built row into the window: the map is what says which rows are in the markup, so a row goes into it where it is
182
223
  * made — otherwise a window that moved would build its rows beside the ones that are still there. */
183
224
  function appPlace(cache, r, span) {
184
- const row = appRow(cache, r, span);
185
- cache.rows.set(r, row);
186
- cache.grid.appendChild(row);
187
- return row;
225
+ const entry = appRow(cache, r, span);
226
+ cache.rows.set(r, entry);
227
+ appCells(entry, cache, span);
228
+ cache.grid.appendChild(entry.el);
188
229
  }
189
230
 
190
231
  /* A caption of the header: a file's name or a metric's. The room is a fixed number of columns, so a name that does
@@ -197,30 +238,39 @@ function appCaption(text, cls, span) {
197
238
  return el;
198
239
  }
199
240
 
200
- /* The header: the total and the enabled files over their columns, and one caption per metric column under them. It
201
- * holds the window's columns alone and is built again only when the window moves sideways scrolling down leaves it
202
- * untouched, and the two rows of it are placed by `grid-column`, so a group of metrics is a group in the grid. */
203
- function appHead(cache, span) {
204
- const count = cache.keys.length;
205
- const g0 = Math.floor(span.c0 / count);
206
- const g1 = Math.floor(span.c1 / count);
207
- const left = (APP_COMMIT + g0 * count * APP_COL) + 'px';
208
- const head = appEl('div', 'head');
209
- head.appendChild(appEl('div', 'c-commit', appUi.commit));
241
+ /* The header's frame: the commit column and the two empty strips that `appStrip` fills the names of the groups over
242
+ * the columns, their metrics under them. An empty strip says `[0, -1]`, which is what makes the first `appHead` fill
243
+ * it rather than move it. */
244
+ function appHeadNew() {
245
+ const el = appEl('div', 'head');
210
246
  const groups = appEl('div', 'hgroups');
211
- groups.style.left = left;
212
247
  const metrics = appEl('div', 'hmetrics');
213
- metrics.style.left = left;
214
- for (let g = g0; g <= g1; g++) {
215
- const label = g === 0 ? appUi.total : appData.files[cache.list[g - 1]].label;
216
- groups.appendChild(appCaption(label, 'gh', count));
217
- for (let mi = 0; mi < count; mi++) {
218
- metrics.appendChild(appCaption(cache.labels[mi], mi === 0 ? 'g' : '', 1));
219
- }
220
- }
221
- head.appendChild(groups);
222
- head.appendChild(metrics);
223
- return head;
248
+ el.appendChild(appEl('div', 'c-commit', appUi.commit));
249
+ el.appendChild(groups);
250
+ el.appendChild(metrics);
251
+ return { el: el, groups: groups, metrics: metrics, g: [0, -1] };
252
+ }
253
+
254
+ /* The header: the total and the enabled files over their columns, and one caption per metric column under them. The two
255
+ * strips move by the same `appStrip` as a row's numbers, and the unit of the metrics line is a column, so its strip is
256
+ * asked for whole groups — the metrics of a column stand under their file even when the window cuts the last group.
257
+ * A window that did not leave a group (a step down the table) leaves the header alone. */
258
+ function appHead(cache, span) {
259
+ const count = cache.keys.length;
260
+ const g0 = count === 0 ? 0 : Math.floor(span.c0 / count);
261
+ const g1 = count === 0 ? -1 : Math.floor(span.c1 / count);
262
+ if (cache.head === null) cache.head = appHeadNew();
263
+ const h = cache.head;
264
+ if (h.g[0] === g0 && h.g[1] === g1) return h.el;
265
+ const was = h.g;
266
+ h.g = [g0, g1];
267
+ h.groups.style.left = (APP_COMMIT + g0 * count * APP_COL) + 'px';
268
+ h.metrics.style.left = h.groups.style.left;
269
+ appStrip(h.groups, was, h.g, (g) => appCaption(
270
+ g === 0 ? appUi.total : appData.files[cache.list[g - 1]].label, 'gh', count));
271
+ appStrip(h.metrics, [was[0] * count, was[1] * count + count - 1], [g0 * count, g1 * count + count - 1],
272
+ (c) => appCaption(cache.labels[c % count], c % count === 0 ? 'g' : '', 1));
273
+ return h.el;
224
274
  }
225
275
 
226
276
  /* The window the shell shows: the rows and the columns, in the ordinals of the whole grid, and the size of that
@@ -259,26 +309,32 @@ export function appSpan(cache) {
259
309
  };
260
310
  }
261
311
 
262
- /* The window drawn. A scroll costs what left the window and what entered it, and nothing else: the row that is
263
- * already built is not touched. Sideways the window of columns is another window the header and the rows are
264
- * built again, because a column that is not there cannot be shown; the reader's own choice (`redraw`) is the same
265
- * kind of change, and comes here by the same road. */
312
+ /* The window drawn. Moving over the grid costs what left it and what entered it, and nothing else: the rows in the
313
+ * markup are the window's rows (what left it is dropped, what entered it is made) and each of them holds the columns
314
+ * of the window (`appCells`), so a step in either direction is paid by the rows that moved rather than by the table.
315
+ *
316
+ * The reader's own choice (`redraw`) is the one thing that cannot be a move: the columns of a switched-off file are
317
+ * simply not among the columns that are built, so there is nothing to hide and nothing to recount — the totals are the
318
+ * sum over the files that are on (`rowModel`), and the window is built from nothing. The shell's own size is the same
319
+ * kind of change: it decides how much of the grid is in sight, which is not a place to move to. */
266
320
  export function appWindow(cache, redraw) {
267
321
  const span = appSpan(cache);
268
322
  const was = cache.win;
269
323
  cache.win = span;
270
- if (redraw === true || was === null || was.c0 !== span.c0 || was.c1 !== span.c1) {
324
+ if (redraw === true || was === null) {
271
325
  /* The grid holds the header and the rows of the window and nothing else, so the whole of it is thrown away rather
272
326
  * than tracked piece by piece — and the rows that are made below say in the map that they are there. */
273
327
  cache.grid.textContent = '';
274
328
  cache.rows.clear();
329
+ cache.head = null;
275
330
  cache.grid.appendChild(appHead(cache, span));
276
331
  for (let r = span.r0; r <= span.r1; r++) appPlace(cache, r, span);
277
332
  return;
278
333
  }
334
+ appHead(cache, span);
279
335
  [...cache.rows.keys()].forEach((r) => {
280
- if (r >= span.r0 && r <= span.r1) return;
281
- cache.rows.get(r).remove();
336
+ if (r >= span.r0 && r <= span.r1) { appCells(cache.rows.get(r), cache, span); return; }
337
+ cache.rows.get(r).el.remove();
282
338
  cache.rows.delete(r);
283
339
  });
284
340
  for (let r = span.r0; r <= span.r1; r++) {
@@ -299,6 +355,7 @@ export function appTable(grid) {
299
355
  rank: [],
300
356
  slot: [],
301
357
  rows: new Map(),
358
+ head: null,
302
359
  win: null
303
360
  };
304
361
  cache.shell.addEventListener('scroll', () => appWindow(cache));