@vernikr/size-report 2.4.0 → 2.6.0
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 +989 -1081
- package/bin/postinstall.js +17 -18
- package/bin/size.js +2 -2
- package/package.json +3 -4
- package/src/args.js +72 -72
- package/src/artifact.js +14 -14
- package/src/check.js +41 -42
- package/src/cli.js +26 -29
- package/src/config.js +87 -91
- package/src/css.js +14 -14
- package/src/data.js +26 -50
- package/src/derived.js +31 -35
- package/src/doctor.js +95 -99
- package/src/explain.js +46 -47
- package/src/git.js +66 -71
- package/src/history.js +74 -83
- package/src/hook.js +130 -149
- package/src/init.js +37 -37
- package/src/journal.js +17 -15
- package/src/locales.js +31 -22
- package/src/metrics.js +72 -89
- package/src/minify.js +28 -27
- package/src/modes.js +57 -60
- package/src/optional.js +13 -11
- package/src/page/app.css +76 -94
- package/src/page/app.js +124 -80
- package/src/page/build.js +193 -50
- package/src/page/dom.js +8 -9
- package/src/page/panel.js +157 -69
- package/src/page/payload.js +168 -0
- package/src/page/state.js +144 -104
- package/src/page/table.js +270 -86
- package/src/parse-worker.js +10 -10
- package/src/parse.js +43 -45
- package/src/project.js +100 -104
- package/src/refusal.js +75 -76
- package/src/size-table.js +41 -76
- package/src/strip/forms.js +5 -5
- package/src/strip/guard.js +28 -28
- package/src/strip/js.js +27 -27
- package/src/strip.js +17 -21
- package/src/table.css +54 -19
- package/src/tokens.js +27 -27
- package/src/tool.js +10 -11
- package/templates/README.md +71 -77
- package/templates/ci.yml +33 -33
- package/templates/size-report.config.json +3 -3
- package/CHANGELOG.md +0 -690
package/src/page/table.js
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import { cellParts, commitParts, nowModel, rowModel, valueParts } from '../derived.js';
|
|
1
|
+
import { cellParts, commitParts, deltaOf, nowModel, rowModel, valueParts } from '../derived.js';
|
|
2
2
|
import { appEl } from './dom.js';
|
|
3
|
-
import { appData, appUi, appView
|
|
4
|
-
|
|
5
|
-
/*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const td = appEl('td', appCellClass(first, parts.miss, approx));
|
|
31
|
-
if (approx !== null) td.title = appUi.approxCell + approx.method;
|
|
3
|
+
import { appData, appUi, appView } from './state.js';
|
|
4
|
+
|
|
5
|
+
/* The table is built once and then only shown and hidden. That rests on three things that do not depend on the
|
|
6
|
+
* reader: a cell's content and colour come from the pair (commit, file) and a metric (`cellParts`, `valueParts`),
|
|
7
|
+
* the order of the columns comes from the files alone, and only the visibility and the totals are the choice's.
|
|
8
|
+
* Hence nothing below builds a node after the first drawing — a click writes a class and a number.
|
|
9
|
+
*
|
|
10
|
+
* What makes this cheap is a cache of references to the nodes of every column (`appTable` returns it): reaching a
|
|
11
|
+
* cell through the markup costs several times what changing it does (measured: `tr.children[i]` 0.0254 ms against
|
|
12
|
+
* `classList.toggle` 0.0070 ms), and the first drawing creates every node anyway, so collecting them is free. The
|
|
13
|
+
* columns of the fixed layout — a `<col>` per column, carrying the counted width — are collected there too.
|
|
14
|
+
*
|
|
15
|
+
* The totals are the only numbers that move with the choice, and they are carried rather than recounted: a sum is
|
|
16
|
+
* linear, so a file switched off subtracts exactly its own numbers — the same integers the first drawing wrote.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/* The cell's class: the metric's track (`m0`, `m1`, … — one class per metric, which is what hides a whole metric
|
|
20
|
+
* at once), the group it belongs to (every cell of a group, not the first one: the group's left border is carried
|
|
21
|
+
* by the first *enabled* metric, and the styling decides which that is), and the gap ("no such file"). */
|
|
22
|
+
function appCellClass(mi, miss) {
|
|
23
|
+
return 'num g m' + mi + (miss ? ' miss' : '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* A cell filled where it stands: the totals are written again on every click, and a cell has to forget its own
|
|
27
|
+
* content first — a delta is a node of its own, so `textContent` alone would leave it behind. */
|
|
28
|
+
function appFill(td, parts) {
|
|
29
|
+
td.textContent = '';
|
|
32
30
|
if (parts.dir === null) td.textContent = parts.text;
|
|
33
31
|
else td.appendChild(appEl('span', 'delta ' + parts.dir, parts.text));
|
|
34
32
|
return td;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
//
|
|
38
|
-
export function
|
|
35
|
+
// The markup of a commit row's cell: the rules live in cellParts, only the node is here.
|
|
36
|
+
export function appCell(cell, mi) {
|
|
37
|
+
const parts = cellParts(cell.value, cell.delta, '−');
|
|
38
|
+
return appFill(appEl('td', appCellClass(mi, parts.miss)), parts);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The markup of the top row's cell: the rules live in valueParts.
|
|
42
|
+
export function appValueCell(value, mi) {
|
|
39
43
|
const parts = valueParts(value);
|
|
40
|
-
|
|
41
|
-
if (approx !== null) td.title = appUi.approxCell + approx.method;
|
|
42
|
-
td.textContent = parts.text;
|
|
43
|
-
return td;
|
|
44
|
+
return appFill(appEl('td', appCellClass(mi, parts.miss)), parts);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
/*
|
|
47
|
-
*
|
|
48
|
-
* оформления, поэтому колонка не прыгает при переключении файлов. */
|
|
47
|
+
/* A commit's caption: the date, the subject, the journal section's mark. The column's width and the clipping of a long
|
|
48
|
+
* subject come from the shared part of the styling, which is why the column does not jump when files are switched. */
|
|
49
49
|
export function appCommit(row) {
|
|
50
50
|
const parts = commitParts(row, appData.report.showSha, row.href);
|
|
51
51
|
const name = parts.href ? appEl('a', 'subj', parts.subject) : appEl('span', 'subj', parts.subject);
|
|
@@ -63,15 +63,9 @@ export function appCommit(row) {
|
|
|
63
63
|
return th;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return tr;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/* Состояния пустоты: когда чисел не будет вовсе, страница говорит об этом словами,
|
|
73
|
-
* а не сеткой без колонок. Файлы можно выключить все — тогда остаётся общий объём,
|
|
74
|
-
* и подсказка объясняет, почему колонок нет. */
|
|
66
|
+
/* The empty states: when there will be no numbers at all, the page says so in words rather than showing a grid without
|
|
67
|
+
* columns. Every file can be switched off — then the total volume remains, and the note explains why there are no
|
|
68
|
+
* columns. The table itself stands there either way: this is about what is shown, not about what exists. */
|
|
75
69
|
export function appState(metricsCount, filesCount) {
|
|
76
70
|
const state = document.getElementById('state');
|
|
77
71
|
const text = metricsCount === 0 ? appUi.empty : (filesCount === 0 ? appUi.noFiles : '');
|
|
@@ -80,26 +74,44 @@ export function appState(metricsCount, filesCount) {
|
|
|
80
74
|
document.getElementById('shell').hidden = metricsCount === 0;
|
|
81
75
|
}
|
|
82
76
|
|
|
83
|
-
/*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
|
|
77
|
+
/* The order of the columns: the files the last commit touched come first — the report is rebuilt after every
|
|
78
|
+
* commit, and a reader's first question is what that edit brought. Inside each part the order stays as it comes
|
|
79
|
+
* from the settings, and it depends on the files rather than on the choice: that is what lets a column be hidden
|
|
80
|
+
* without moving the others. */
|
|
81
|
+
export function appOrder() {
|
|
82
|
+
const files = [];
|
|
83
|
+
appData.files.forEach((_f, i) => files.push(i));
|
|
84
|
+
files.sort((a, b) => (appData.last[a] === true ? 0 : 1) - (appData.last[b] === true ? 0 : 1));
|
|
85
|
+
return files;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* The header: a row of groups (the total and the files) and a row of metrics under it — every metric of every file,
|
|
89
|
+
* because nothing here knows the choice. The headings whose `colSpan` follows the number of enabled metrics and the
|
|
90
|
+
* headings of each file's column are collected in the cache: they are the nodes a click has to touch. */
|
|
91
|
+
export function appHead(files, metrics, cache) {
|
|
87
92
|
const head = appEl('tr');
|
|
88
93
|
const commit = appEl('th', 'c-commit', appUi.commit);
|
|
89
94
|
commit.rowSpan = 2;
|
|
90
95
|
head.appendChild(commit);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
files.forEach((i) => {
|
|
95
|
-
const th = appEl('th', 'g', appData.files[i].label);
|
|
96
|
+
const subs = appEl('tr');
|
|
97
|
+
const group = (label, i) => {
|
|
98
|
+
const th = appEl('th', 'gh', label);
|
|
96
99
|
th.colSpan = metrics.length;
|
|
97
100
|
head.appendChild(th);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
cache.spans.push(th);
|
|
102
|
+
/* The group's own heading belongs to the column: hiding a file has to take its caption with it, or the header
|
|
103
|
+
* would keep a name over numbers that are gone. */
|
|
104
|
+
if (i !== null) cache.cols[i].push(th);
|
|
105
|
+
metrics.forEach((_key, mi) => {
|
|
106
|
+
const cell = appEl('th', 'g m' + mi, appData.metrics[mi].label);
|
|
107
|
+
subs.appendChild(cell);
|
|
108
|
+
if (i !== null) cache.cols[i].push(cell);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
group(appUi.total, null);
|
|
112
|
+
files.forEach((i) => {
|
|
113
|
+
cache.cols[i] = [];
|
|
114
|
+
group(appData.files[i].label, i);
|
|
103
115
|
});
|
|
104
116
|
const thead = appEl('thead');
|
|
105
117
|
thead.appendChild(head);
|
|
@@ -107,39 +119,211 @@ export function appHead(shown, files, metrics) {
|
|
|
107
119
|
return thead;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
/*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
|
|
122
|
+
/* A text's breadth in `ch`, the unit a column is measured in: a digit is exactly one `ch` in this table's font
|
|
123
|
+
* (measured in Chrome: 8.67px against 8.67px), and a thin space is counted as a third of a digit — nothing else is
|
|
124
|
+
* discounted. Without that one fraction every number column would come out a fifth wider than the number in it (the
|
|
125
|
+
* grouping makes a text 21 % narrower than its characters, measured), which would give back more than the padding and
|
|
126
|
+
* the clipping of this step save. What is left over-measures — a letter is 0.84 of a `ch`, a slash 0.48 — and that is
|
|
127
|
+
* the safe side: a cell clips nothing, so a column a character short would show a number running over its neighbour. */
|
|
128
|
+
function appBreadth(text) {
|
|
129
|
+
let wide = 0;
|
|
130
|
+
for (let i = 0; i < text.length; i++) wide += text[i] === '\u2009' ? 0.35 : 1;
|
|
131
|
+
return wide;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* The widest text of every column, counted before a single node is made: the numbers of the commit rows — through
|
|
135
|
+
* `rowModel`, the very model the cells are drawn from, so a column cannot be sized for a number other than the one
|
|
136
|
+
* that will stand in it — the absolute sizes of the "now" row and the metric's own caption. */
|
|
137
|
+
function appWidest(files, keys) {
|
|
138
|
+
const wide = files.map(() => keys.map(() => 0));
|
|
139
|
+
const total = keys.map(() => 0);
|
|
140
|
+
const put = (into, mi, breadth) => { if (breadth > into[mi]) into[mi] = breadth; };
|
|
141
|
+
const delta = (cell) => appBreadth(cellParts(cell.value, cell.delta, '−').text);
|
|
142
|
+
const value = (v) => appBreadth(valueParts(v).text);
|
|
143
|
+
appData.rows.forEach((row, r) => {
|
|
144
|
+
const model = rowModel(row.values, r === 0 ? null : appData.rows[r - 1].values, keys);
|
|
145
|
+
model.total.forEach((cell, mi) => put(total, mi, delta(cell)));
|
|
146
|
+
files.forEach((i) => model.files[i].forEach((cell, mi) => put(wide[i], mi, delta(cell))));
|
|
147
|
+
});
|
|
148
|
+
const now = nowModel(appData.now, keys);
|
|
149
|
+
now.total.forEach((v, mi) => put(total, mi, value(v)));
|
|
150
|
+
files.forEach((i) => now.files[i].forEach((v, mi) => put(wide[i], mi, value(v))));
|
|
151
|
+
keys.forEach((_key, mi) => {
|
|
152
|
+
const caption = appBreadth(appData.metrics[mi].label);
|
|
153
|
+
put(total, mi, caption);
|
|
154
|
+
wide.forEach((per) => put(per, mi, caption));
|
|
155
|
+
});
|
|
156
|
+
return { total: total, files: wide };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* A group's heading — a file's name, the word over the total — is one line over the columns of that group
|
|
160
|
+
* (`white-space: nowrap` in the shared styling), so the group as a whole has to be wide enough for it, or the names of
|
|
161
|
+
* two neighbouring groups would run into one another. The caption is divided among the metrics of the group rather than
|
|
162
|
+
* weighed against their sum: every column carries at least its share, so the group can never come out narrower than the
|
|
163
|
+
* caption, while a column that is wider anyway keeps its own count. */
|
|
164
|
+
function appGroupWide(per, label) {
|
|
165
|
+
const share = Math.ceil(appBreadth(label) / per.length);
|
|
166
|
+
return per.map((n) => Math.max(n, share));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* The columns, before the rows: `table-layout: fixed` reads the first row of the table and takes the widths from there,
|
|
170
|
+
* so they are known while the table is built rather than measured by the browser over every cell of it. A column
|
|
171
|
+
* carries the metric's class (`m0`, `m1`, …) — which is what hides a whole metric at once — and its counted width in
|
|
172
|
+
* `ch`; the styling adds the padding and the border to it (`#grid col`), because a count and a drawing are one sum
|
|
173
|
+
* rather than two. The columns of a file are collected where its cells are, so one switch writes one class over both. */
|
|
174
|
+
function appCols(files, wide, cache) {
|
|
175
|
+
const group = appEl('colgroup');
|
|
176
|
+
const add = (n, mi) => {
|
|
177
|
+
const col = appEl('col', 'm' + mi);
|
|
178
|
+
col.style.setProperty('--ch', n + 'ch');
|
|
179
|
+
group.appendChild(col);
|
|
180
|
+
return col;
|
|
181
|
+
};
|
|
182
|
+
group.appendChild(appEl('col', 'c-commit'));
|
|
183
|
+
appGroupWide(wide.total, appUi.total).forEach(add);
|
|
184
|
+
files.forEach((i) => appGroupWide(wide.files[i], appData.files[i].label)
|
|
185
|
+
.forEach((n, mi) => { cache.cols[i].push(add(n, mi)); }));
|
|
186
|
+
return group;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* A file's share of a row: one cell per metric, in the order of the table's columns, and each is put into its
|
|
190
|
+
* column's cache while it is made. The maker is what tells a commit's delta from an absolute size at HEAD — the walk
|
|
191
|
+
* over the files is one, and there is no second one to drift away. */
|
|
192
|
+
function appRowCells(tr, model, files, cache, make) {
|
|
193
|
+
files.forEach((i) => {
|
|
194
|
+
model.files[i].forEach((value, mi) => {
|
|
195
|
+
const td = make(value, mi);
|
|
196
|
+
cache.cols[i].push(td);
|
|
197
|
+
tr.appendChild(td);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* A commit row: the caption and the numbers. The deltas come from the shared calculation (`rowModel`) rather than
|
|
203
|
+
* from here — two ways to count one row would be two answers. The model is taken whole (every file) because the
|
|
204
|
+
* table holds every column, and the column order of the table is applied by taking the model by the file's own
|
|
205
|
+
* index: the model lists the files the way the data does, not the way the columns stand. */
|
|
206
|
+
export function appRow(r, files, metrics, cache) {
|
|
114
207
|
const row = appData.rows[r];
|
|
115
208
|
const prev = r === 0 ? null : appData.rows[r - 1];
|
|
116
|
-
const model = rowModel(row.values, prev === null ? null : prev.values, metrics
|
|
209
|
+
const model = rowModel(row.values, prev === null ? null : prev.values, metrics);
|
|
117
210
|
const tr = appEl('tr');
|
|
118
211
|
tr.appendChild(appCommit(row));
|
|
119
212
|
model.total.forEach((cell, mi) => {
|
|
120
|
-
|
|
213
|
+
const td = appCell(cell, mi);
|
|
214
|
+
cache.sums[mi][r] = cell.value;
|
|
215
|
+
cache.cells[mi][r] = td;
|
|
216
|
+
tr.appendChild(td);
|
|
121
217
|
});
|
|
122
|
-
|
|
123
|
-
tr.appendChild(appCell(cell, mi === 0, appApprox(r, [files[fi]], metrics[mi])));
|
|
124
|
-
}));
|
|
218
|
+
appRowCells(tr, model, files, cache, appCell);
|
|
125
219
|
return tr;
|
|
126
220
|
}
|
|
127
221
|
|
|
128
|
-
/*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
export function
|
|
222
|
+
/* The top row holds the absolute sizes at HEAD: an absolute number stands in the table once, and it is the one every
|
|
223
|
+
* delta below it adds up to. Its numbers are the state at HEAD rather than the last commit's cells, which is why the
|
|
224
|
+
* running sums take them from here instead of assuming they are the row above. */
|
|
225
|
+
export function appNow(files, metrics, cache) {
|
|
226
|
+
const tr = appEl('tr', 'now');
|
|
227
|
+
tr.appendChild(appEl('th', 'c-commit', appUi.now));
|
|
228
|
+
const model = nowModel(appData.now, metrics);
|
|
229
|
+
const last = appData.rows.length;
|
|
230
|
+
model.total.forEach((v, mi) => {
|
|
231
|
+
const td = appValueCell(v, mi);
|
|
232
|
+
cache.sums[mi][last] = v;
|
|
233
|
+
cache.cells[mi][last] = td;
|
|
234
|
+
tr.appendChild(td);
|
|
235
|
+
});
|
|
236
|
+
appRowCells(tr, model, files, cache, appValueCell);
|
|
237
|
+
return tr;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* The body: the commit rows from the newest down, plus the "now" row with the absolute sizes at HEAD. The deltas
|
|
241
|
+
* under it add up to it, which is why it stands first. */
|
|
242
|
+
export function appBody(files, metrics, cache) {
|
|
132
243
|
const body = appEl('tbody');
|
|
133
|
-
for (let r = appData.rows.length - 1; r >= 0; r--) body.appendChild(appRow(r, metrics,
|
|
134
|
-
|
|
135
|
-
const now = appEl('tr', 'now');
|
|
136
|
-
now.appendChild(appEl('th', 'c-commit', appUi.now));
|
|
137
|
-
const nowCells = nowModel(appData.now, metrics, appView.files);
|
|
138
|
-
nowCells.total.forEach((v, mi) => now.appendChild(appValueCell(v, mi === 0,
|
|
139
|
-
appApprox('now', files, metrics[mi]))));
|
|
140
|
-
nowCells.files.forEach((cells, fi) => cells.forEach((v, mi) => {
|
|
141
|
-
now.appendChild(appValueCell(v, mi === 0, appApprox('now', [files[fi]], metrics[mi])));
|
|
142
|
-
}));
|
|
143
|
-
body.insertBefore(now, body.firstChild);
|
|
244
|
+
for (let r = appData.rows.length - 1; r >= 0; r--) body.appendChild(appRow(r, files, metrics, cache));
|
|
245
|
+
body.insertBefore(appNow(files, metrics, cache), body.firstChild);
|
|
144
246
|
return body;
|
|
145
247
|
}
|
|
248
|
+
|
|
249
|
+
/* The whole table, built once. What the cache holds is what the reader's choice works with: the nodes of each file's
|
|
250
|
+
* column — its cells, its headings and its `<col>` (`cols`), the group headings whose colSpan follows the enabled
|
|
251
|
+
* metrics (`spans`), the cells of the totals (`cells`), the running sums (`sums`) and those same sums with every
|
|
252
|
+
* column on (`all`) — the sums a view painted again starts from. */
|
|
253
|
+
export function appTable(grid) {
|
|
254
|
+
const metrics = appData.metrics.map((m) => m.key);
|
|
255
|
+
const files = appOrder();
|
|
256
|
+
const cache = { grid: grid, keys: metrics, cols: [], spans: [], cells: [], sums: [], all: [] };
|
|
257
|
+
metrics.forEach(() => {
|
|
258
|
+
cache.cells.push([]);
|
|
259
|
+
cache.sums.push([]);
|
|
260
|
+
});
|
|
261
|
+
grid.textContent = '';
|
|
262
|
+
grid.appendChild(appHead(files, metrics, cache));
|
|
263
|
+
grid.appendChild(appBody(files, metrics, cache));
|
|
264
|
+
/* The columns stand before the rows rather than beside them: a `<colgroup>` is where the fixed layout reads its
|
|
265
|
+
* widths from, and it has to be in the markup before the browser settles on a layout — the drawing is one pass, not
|
|
266
|
+
* two. */
|
|
267
|
+
grid.insertBefore(appCols(files, appWidest(files, metrics), cache), grid.firstChild);
|
|
268
|
+
cache.all = cache.sums.map((row) => row.slice());
|
|
269
|
+
return cache;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* A file's column shown or hidden: its cells and its headings together, one class per node and no new node. A hidden
|
|
273
|
+
* column keeps its place in the markup — the order of the columns is the files' business, not the choice's. */
|
|
274
|
+
export function appColumn(cache, i, on) {
|
|
275
|
+
cache.cols[i].forEach((node) => node.classList.toggle('off', !on));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* A file's share of the totals: switched off it takes exactly its own numbers out of the running sums, switched on
|
|
279
|
+
* it puts them back. Two rows per file are the whole of it — the numbers are the data's, and a sum of integers is
|
|
280
|
+
* exact in either direction. */
|
|
281
|
+
export function appContribute(cache, i, on) {
|
|
282
|
+
const sign = on ? 1 : -1;
|
|
283
|
+
appData.rows.forEach((row, r) => {
|
|
284
|
+
const values = row.values[i];
|
|
285
|
+
if (values === null) return;
|
|
286
|
+
cache.keys.forEach((key, mi) => { cache.sums[mi][r] += sign * values[key]; });
|
|
287
|
+
});
|
|
288
|
+
const values = appData.now[i];
|
|
289
|
+
if (values === null) return;
|
|
290
|
+
const last = appData.rows.length;
|
|
291
|
+
cache.keys.forEach((key, mi) => { cache.sums[mi][last] += sign * values[key]; });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* The totals written where they stand: a data row shows the change against the row below it, the "now" row the
|
|
295
|
+
* absolute size — the same rules the first drawing used (`cellParts`, `valueParts`), so the reader sees one kind of
|
|
296
|
+
* number and not two. */
|
|
297
|
+
export function appTotals(cache) {
|
|
298
|
+
const last = appData.rows.length;
|
|
299
|
+
cache.cells.forEach((cells, mi) => {
|
|
300
|
+
cells.forEach((td, r) => {
|
|
301
|
+
if (r === last) { appFill(td, valueParts(cache.sums[mi][r])); return; }
|
|
302
|
+
appFill(td, cellParts(cache.sums[mi][r], deltaOf(cache.sums[mi][r], r === 0 ? null : cache.sums[mi][r - 1]), '−'));
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* The running sums of the whole view again: a link, a record from the memory and the first drawing change the choice
|
|
308
|
+
* as a whole rather than a column, so the sums start from the sums with every column on and take the switched-off
|
|
309
|
+
* ones out — one copy of the numbers instead of a recount. */
|
|
310
|
+
export function appTotalsReset(cache) {
|
|
311
|
+
cache.sums = cache.all.map((row) => row.slice());
|
|
312
|
+
appData.files.forEach((_f, i) => { if (!appView.files[i]) appContribute(cache, i, false); });
|
|
313
|
+
appTotals(cache);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* The metrics of the choice: a class on the table hides every cell of a metric at once, and the group headings follow
|
|
317
|
+
* how many are left — two things that change, instead of a pass over the metric's cells (measured: 2.20 ms and 265
|
|
318
|
+
* operations against 433 ms and 56 496 for walking them). `.m-none` is what the styling reads to drop the group's
|
|
319
|
+
* left border when there is nothing left to separate. */
|
|
320
|
+
export function appMetrics(cache) {
|
|
321
|
+
let on = 0;
|
|
322
|
+
cache.keys.forEach((key, mi) => {
|
|
323
|
+
const visible = appView.metrics[key] === true;
|
|
324
|
+
if (visible) on++;
|
|
325
|
+
cache.grid.classList.toggle('m-off-' + mi, !visible);
|
|
326
|
+
});
|
|
327
|
+
cache.grid.classList.toggle('m-none', on === 0);
|
|
328
|
+
cache.spans.forEach((th) => { th.colSpan = on === 0 ? 1 : on; });
|
|
329
|
+
}
|
package/src/parse-worker.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import vm from 'vm';
|
|
2
2
|
import { workerData } from 'worker_threads';
|
|
3
3
|
|
|
4
|
-
/*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
/* The parsing worker: it compiles the text and answers with a reason (or with the absence of
|
|
5
|
+
* one). Nothing is executed — `SourceTextModule` only parses the text, so neither `import`
|
|
6
|
+
* nor the code of the module runs: the file of the project stays foreign code that nobody
|
|
7
|
+
* launches.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* `vm.SourceTextModule`
|
|
11
|
-
*
|
|
12
|
-
* `available: false`,
|
|
9
|
+
* The flags arrive from the main thread (`--experimental-vm-modules`, without which
|
|
10
|
+
* `vm.SourceTextModule` does not exist, and `--no-warnings` lest the experimental warning end
|
|
11
|
+
* up in the command's output). The module may be absent: then the answer carries
|
|
12
|
+
* `available: false`, and the main thread falls back to launching `node --check`.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* (`Atomics.wait`),
|
|
14
|
+
* Readiness is marked in shared memory: the main thread waits for it synchronously
|
|
15
|
+
* (`Atomics.wait`), because measuring the history is synchronous.
|
|
16
16
|
*/
|
|
17
17
|
const { port, sig } = workerData;
|
|
18
18
|
const available = typeof vm.SourceTextModule === 'function';
|
package/src/parse.js
CHANGED
|
@@ -5,49 +5,45 @@ import { execFileSync } from 'child_process';
|
|
|
5
5
|
import { Worker, MessageChannel, receiveMessageOnPort } from 'worker_threads';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
|
|
8
|
-
/*
|
|
9
|
-
* клетку.
|
|
8
|
+
/* Parsing a module: one worker per run instead of launching Node per cell.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* где модуль меняется каждым коммитом.
|
|
10
|
+
* Why. The compile guard has to understand a module (`import`/`export` inside `.js` is
|
|
11
|
+
* ordinary in a project with a bundler — see `strip.js`), while the only parse of a module
|
|
12
|
+
* without executing it, `vm.SourceTextModule`, exists only under `--experimental-vm-modules`,
|
|
13
|
+
* which this process is not started with. It used to be solved by `node --check` per cell:
|
|
14
|
+
* 97 ms per launch and minutes across a history where a module changes at every commit.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
16
|
+
* How. The worker starts at the first module and lives until the end of the run, so a
|
|
17
|
+
* script-only project never pays for it. The exchange is synchronous, as history measurement
|
|
18
|
+
* is: the request goes out through `postMessage`, readiness is marked in a `SharedArrayBuffer`,
|
|
19
|
+
* and the answer is taken with `receiveMessageOnPort` — the same trick as in Node's example of
|
|
20
|
+
* a synchronous channel to a worker. The worker is `unref`-ed: the command ends with its
|
|
21
|
+
* work, not with the thread.
|
|
24
22
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* дешевле запуска процесса.
|
|
23
|
+
* What it costs. Starting the worker is a one-off (~55 ms on the machine the measurement was
|
|
24
|
+
* taken on), and the parsing relies on an experimental API: the flag is passed to the worker
|
|
25
|
+
* itself, so the user's command does not change. The text is copied into the worker — tens of
|
|
26
|
+
* milliseconds for files of tens of megabytes, still cheaper than launching a process.
|
|
30
27
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
28
|
+
* Where it falls back. To `node --check` — slower, not weaker: when the worker file is missing
|
|
29
|
+
* (an incomplete package), when the worker did not answer in time (it died), and when it turns
|
|
30
|
+
* out to have no `vm.SourceTextModule` (a Node without the vm module). The fallback is silent:
|
|
31
|
+
* a broken fast path costs seconds rather than correctness, and its place is watched by the
|
|
32
|
+
* parse-mode check (`parseMode`).
|
|
36
33
|
*/
|
|
37
34
|
|
|
38
35
|
const WORKER_FILE = fileURLToPath(new URL('./parse-worker.js', import.meta.url));
|
|
39
36
|
const FLAGS = ['--experimental-vm-modules', '--no-warnings'];
|
|
40
37
|
const WAIT_MS = 2000;
|
|
41
38
|
|
|
42
|
-
let parser = null; //
|
|
43
|
-
let hopeless = false; //
|
|
39
|
+
let parser = null; // the live thread { worker, port, sig } or null
|
|
40
|
+
let hopeless = false; // the thread did not come up: no second try
|
|
44
41
|
let seq = 0;
|
|
45
|
-
let mode = null; // 'thread' | 'node' —
|
|
42
|
+
let mode = null; // 'thread' | 'node' — how the last module was parsed
|
|
46
43
|
|
|
47
|
-
/*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* отсутствием. */
|
|
44
|
+
/* The reason the text does not parse as a module, or null if it does. "Could not check"
|
|
45
|
+
* never leaves this function: without a worker the parse goes to `node --check`, which
|
|
46
|
+
* answers the same way — with a reason or with its absence. */
|
|
51
47
|
export function moduleError(text) {
|
|
52
48
|
const fromThread = inThread(text);
|
|
53
49
|
if (fromThread !== undefined) {
|
|
@@ -58,9 +54,9 @@ export function moduleError(text) {
|
|
|
58
54
|
return onNodeCheck(text);
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
/*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
57
|
+
/* How the last module was parsed. Kept so that the fast path cannot degrade silently: a
|
|
58
|
+
* check asserts that on a project with modules it really is the worker and not the old
|
|
59
|
+
* launch. */
|
|
64
60
|
export function parseMode() {
|
|
65
61
|
return mode;
|
|
66
62
|
}
|
|
@@ -69,15 +65,15 @@ function inThread(text) {
|
|
|
69
65
|
const live = start();
|
|
70
66
|
if (!live) return undefined;
|
|
71
67
|
const id = ++seq;
|
|
72
|
-
//
|
|
73
|
-
//
|
|
68
|
+
// Zero goes into the same word the worker marks readiness with: an answer arriving before
|
|
69
|
+
// the wait would otherwise look like "not started yet".
|
|
74
70
|
Atomics.store(live.sig, 0, 0);
|
|
75
71
|
live.port.postMessage({ id: id, text: text });
|
|
76
72
|
if (Atomics.wait(live.sig, 0, 0, WAIT_MS) === 'timed-out') return bury();
|
|
77
73
|
for (;;) {
|
|
78
74
|
const got = receiveMessageOnPort(live.port);
|
|
79
75
|
if (!got) return bury();
|
|
80
|
-
if (got.message.id !== id) continue; //
|
|
76
|
+
if (got.message.id !== id) continue; // a stale answer (it replied to another request)
|
|
81
77
|
if (got.message.available === false) return bury();
|
|
82
78
|
return got.message.error === null ? null : String(got.message.error);
|
|
83
79
|
}
|
|
@@ -95,8 +91,8 @@ function start() {
|
|
|
95
91
|
const worker = new Worker(WORKER_FILE, {
|
|
96
92
|
execArgv: FLAGS, workerData: { port: port2, sig: sig }, transferList: [port2]
|
|
97
93
|
});
|
|
98
|
-
//
|
|
99
|
-
//
|
|
94
|
+
// Without a handler a worker error would become an exception of the process, while its
|
|
95
|
+
// place is in the fallback to a launch.
|
|
100
96
|
worker.on('error', bury);
|
|
101
97
|
worker.on('exit', bury);
|
|
102
98
|
worker.unref();
|
|
@@ -107,7 +103,8 @@ function start() {
|
|
|
107
103
|
return parser;
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
//
|
|
106
|
+
// The worker is no longer usable: parsing goes through a launch from here on, with no way
|
|
107
|
+
// back.
|
|
111
108
|
function bury() {
|
|
112
109
|
const dead = parser;
|
|
113
110
|
parser = null;
|
|
@@ -116,10 +113,10 @@ function bury() {
|
|
|
116
113
|
return undefined;
|
|
117
114
|
}
|
|
118
115
|
|
|
119
|
-
/*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
116
|
+
/* The fallback: `node --check` on a temporary file. The `.mjs` extension is not cosmetic —
|
|
117
|
+
* a temporary file has no manifest, and only the extension tells Node to read the text as a
|
|
118
|
+
* module. The reason comes from `stderr`, which holds the echoed offending line first, then
|
|
119
|
+
* the `SyntaxError` itself and the stack. */
|
|
123
120
|
function onNodeCheck(text) {
|
|
124
121
|
const tmp = path.join(os.tmpdir(), 'size-table-guard-' + process.pid + '-mod.mjs');
|
|
125
122
|
try {
|
|
@@ -129,7 +126,8 @@ function onNodeCheck(text) {
|
|
|
129
126
|
} catch (e) {
|
|
130
127
|
const lines = String((e && e.stderr) || (e && e.message) || e).split('\n')
|
|
131
128
|
.map((l) => l.trim()).filter((l) => l !== '');
|
|
132
|
-
|
|
129
|
+
// The fallback reason travels into a printed refusal, so it is worded like the rest of the output.
|
|
130
|
+
return lines.find((l) => /^\w*Error\b/.test(l)) || lines[0] || 'the module does not parse';
|
|
133
131
|
} finally {
|
|
134
132
|
fs.rmSync(tmp, { force: true });
|
|
135
133
|
}
|