sparkforensics-mcp 0.2.4 → 0.4.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/LICENSE +21 -0
- package/README.md +7 -1
- package/bin/sparkforensics-mcp.mjs +41 -10
- package/package.json +1 -1
- package/vendor-core/analyzer.js +156 -48
- package/vendor-core/check-coverage.js +88 -0
- package/vendor-core/cli/budgets.js +31 -18
- package/vendor-core/cli/collect-run.js +76 -31
- package/vendor-core/cli/native-zstd.js +2 -2
- package/vendor-core/cli/threshold-config.js +28 -0
- package/vendor-core/comparison-verdict.js +177 -0
- package/vendor-core/core-source-hash.txt +1 -0
- package/vendor-core/core-usage-locality.js +56 -2
- package/vendor-core/detector-docs.js +58 -0
- package/vendor-core/detectors.js +933 -459
- package/vendor-core/docs-config.js +0 -36
- package/vendor-core/docs-content/chapters/nav-index.json +31 -0
- package/vendor-core/docs-content/detection/cstor.md +9 -0
- package/vendor-core/docs-content/detection/fail.md +6 -2
- package/vendor-core/docs-site-config.js +3 -0
- package/vendor-core/event-handlers.js +191 -6
- package/vendor-core/event-schemas.js +29 -0
- package/vendor-core/evidence-report.js +440 -112
- package/vendor-core/export-data.js +79 -6
- package/vendor-core/finding-action-label.js +9 -88
- package/vendor-core/finding-filter-predicate.js +9 -0
- package/vendor-core/finding-generic-recommendation.js +6 -104
- package/vendor-core/finding-names.js +21 -45
- package/vendor-core/finding-presentation.js +333 -0
- package/vendor-core/finding-tag-help.js +110 -0
- package/vendor-core/finding-types.js +361 -0
- package/vendor-core/findings-of-type.js +11 -0
- package/vendor-core/format-utils.js +92 -27
- package/vendor-core/html-export.js +51 -0
- package/vendor-core/impact-band.js +21 -8
- package/vendor-core/impact-estimator.js +8 -521
- package/vendor-core/impact-format.js +114 -0
- package/vendor-core/impact-model.js +175 -0
- package/vendor-core/ingest.js +2 -0
- package/vendor-core/intervals.js +13 -0
- package/vendor-core/list-runs.js +2 -3
- package/vendor-core/load-vendored.js +70 -5
- package/vendor-core/mcp-server-factory.js +14 -10
- package/vendor-core/mcp-tools.js +105 -45
- package/vendor-core/model-assembler.js +12 -0
- package/vendor-core/occupancy.js +1 -1
- package/vendor-core/parser-worker.js +22 -5
- package/vendor-core/plan-graph-model.js +3 -2
- package/vendor-core/plan-node-detail.js +1 -1
- package/vendor-core/recommendation-rollup.js +63 -3
- package/vendor-core/redact.js +68 -28
- package/vendor-core/run-comparison.js +40 -7
- package/vendor-core/run-interpretation.js +290 -0
- package/vendor-core/run-outcome.js +74 -0
- package/vendor-core/run-payload.js +17 -0
- package/vendor-core/run-shape.js +40 -0
- package/vendor-core/run-verdict.js +353 -0
- package/vendor-core/scaling-sim.js +4 -5
- package/vendor-core/scorecard-estimates.js +62 -0
- package/vendor-core/shs-fetch.js +175 -65
- package/vendor-core/shs-load.js +1 -1
- package/vendor-core/sql-stages.js +11 -0
- package/vendor-core/stage-quantiles.js +14 -0
- package/vendor-core/task-failure.js +151 -0
- package/vendor-core/threshold-overrides.js +160 -0
- package/vendor-core/threshold-summary.js +11 -33
- package/vendor-core/types.js +6 -42
- package/vendor-core/vendor/fflate.js +1 -1
- package/vendor-core/wall-clock.js +1 -12
- package/vendor-core/wasted-core-hours.js +2 -2
- package/vendor-core/zip-archive.js +167 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// Per-detector finding shapes. `Finding` is a union discriminated on `type`, one member per
|
|
2
|
+
// emitted finding type; `FindingType` (detectors.ts) derives the same set from `DETECTORS`' `emits`
|
|
3
|
+
// lists, and a compile-time check there keeps the two equal.
|
|
4
|
+
//
|
|
5
|
+
// Each member is split in two: `<Type>Evidence` is the public part, the fields the evidence report
|
|
6
|
+
// publishes as a finding row's `evidence` (EVIDENCE_KEYS in evidence-report.ts must list every one of
|
|
7
|
+
// them), and anything declared only on `<Type>Finding` is internal, read by another core module (the
|
|
8
|
+
// impact estimator, mostly) and never published. Renaming or removing an evidence field is a
|
|
9
|
+
// breaking change to the report and needs an EVIDENCE_SCHEMA_VERSION bump.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// The columns every finding carries, whatever its detector.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/** One overridden threshold: the value the detector ran with, and its specification default. */
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
// A finding whose `value` is a magnitude (a ratio, byte count, percentage, duration...). Absent on
|
|
43
|
+
// an evidence caveat, which has nothing to measure.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
// A finding whose observed value is display text (a failure reason, an audited config's current
|
|
50
|
+
// value), carried in `valueText` so `value` stays numeric across every finding.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// A failed or retried task attempt, as a stage keeps a bounded sample of them.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// ── Per-stage detectors ─────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
// Three shapes share this type: a slow host by mean task time (no variant), a host doing most of the
|
|
101
|
+
// stage's task time ('durationShare'), and an executor deviating on one dimension ('multiDim').
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
// valueText is the stage's recorded failure reason.
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
// ── App-level detectors ─────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
// valueText is always 'missing': the ApplicationEnd event the log lacks.
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
// A relation scanned by several SQL executions (no variant), or a join/union result several
|
|
226
|
+
// executions recompute ('composite', which lists its leaf relations).
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
// ── Config-scope detectors ──────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
// valueText is the audited property's current value, or a note that it is unset or inverted.
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
// ── SQL-scope (Plan Advisor) detectors ──────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
// View-layer plan-graph node ids the finding came from (plan-graph-model.ts). An array: a
|
|
264
|
+
// duplicate subtree spans many nodes, and a broadcast finding can involve more than one.
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
// ── Unions ──────────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/** Public evidence fields per finding type: what a report row's `evidence` carries. */
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
/** The finding member for one `type`. */
|
|
361
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/** The findings of one `type`, typed as that type's finding member. */
|
|
4
|
+
export function findingsOfType (findings , type ) {
|
|
5
|
+
return findings.filter((f) => f.type === type);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** A sql-scope finding's linked stages; undefined for every finding that keys on `stageId`. */
|
|
9
|
+
export function findingStageIds(f ) {
|
|
10
|
+
return 'stageIds' in f ? f.stageIds : undefined;
|
|
11
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { FINDING_PRESENTATION } from './finding-presentation.js';
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
const TARGET_PARTITION_BYTES = 128 * 1024 * 1024;
|
|
2
5
|
const MAX_RECOMMENDED = 8000;
|
|
3
6
|
const MEANINGFUL_RATIO = 1.5;
|
|
@@ -5,11 +8,11 @@ const MEANINGFUL_RATIO = 1.5;
|
|
|
5
8
|
export const VISIBLE_LIMIT = 6;
|
|
6
9
|
export const IMPACT_BAND_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
7
10
|
|
|
8
|
-
const BOTTLENECK_WIDGET
|
|
11
|
+
const BOTTLENECK_WIDGET = {
|
|
9
12
|
skew: 'task-skew', slowHost: 'task-skew', straggler: 'task-skew',
|
|
10
13
|
shuffle: 'shuffle-io', spill: 'spill', gc: 'gc-pressure', failures: 'failures',
|
|
11
14
|
coldStart: 'executor-timeline', utilization: 'executor-timeline', speculationWaste: 'executor-timeline',
|
|
12
|
-
};
|
|
15
|
+
} ;
|
|
13
16
|
|
|
14
17
|
// Cross-widget stage recurrence: how many distinct board widgets (skew+straggler on one stage
|
|
15
18
|
// still count as one, task-skew) flag a stage. Computed once from the full catalog each widget
|
|
@@ -30,22 +33,12 @@ export function stageWidgetFrequency(catalog
|
|
|
30
33
|
return freq;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// Exported so doc-sync checks can enumerate every tag
|
|
36
|
-
export const TYPE_TAG_MAP
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
cacheUtilization: 'CSTOR', coreLocality: 'LOCAL', autoscalingChurn: 'CHRN',
|
|
40
|
-
slowHost: 'HOST', failures: 'FAIL', straggler: 'STRAG',
|
|
41
|
-
retryWaste: 'RETRY', tinyTask: 'TINY', stageFailed: 'SFAIL',
|
|
42
|
-
speculationWaste: 'SPEC',
|
|
43
|
-
partitionSizing: 'PART', stageSlowness: 'SLOW', stageShape: 'SHAPE',
|
|
44
|
-
cachingOpportunity: 'CACHE', jobFailureRate: 'JOBS', configAudit: 'CFG',
|
|
45
|
-
duplicatePlanSubtree: 'PLAN', smallFiles: 'PLAN', underBroadcast: 'PLAN', overBroadcast: 'PLAN',
|
|
46
|
-
broadcastSizing: 'PLAN',
|
|
47
|
-
incompleteRun: 'INCMP',
|
|
48
|
-
};
|
|
36
|
+
// Finding type -> ALL-CAPS board tag, derived from FINDING_PRESENTATION. Every widget that renders
|
|
37
|
+
// a catalog entry's tag outside its own card (e.g. Bottleneck Alerts) reads it through typeTag.
|
|
38
|
+
// Exported so doc-sync checks can enumerate every tag. Read by free-form type string.
|
|
39
|
+
export const TYPE_TAG_MAP = Object.fromEntries(
|
|
40
|
+
(Object.keys(FINDING_PRESENTATION) ).map((type) => [type, FINDING_PRESENTATION[type].tag]),
|
|
41
|
+
);
|
|
49
42
|
|
|
50
43
|
export function typeTag(type ) {
|
|
51
44
|
return TYPE_TAG_MAP[type] ?? type.toUpperCase();
|
|
@@ -134,10 +127,10 @@ export function nsToMs(ns ) {
|
|
|
134
127
|
return ns / 1e6;
|
|
135
128
|
}
|
|
136
129
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
export function numericValue(f
|
|
140
|
-
return
|
|
130
|
+
// A finding's magnitude, or 0 when it has none (an evidence caveat, or a text-valued finding whose
|
|
131
|
+
// value is in `valueText`).
|
|
132
|
+
export function numericValue(f ) {
|
|
133
|
+
return f.value ?? 0;
|
|
141
134
|
}
|
|
142
135
|
|
|
143
136
|
// Low-level "number -> display string" step shared by every widget's per-finding label resolver:
|
|
@@ -181,11 +174,11 @@ function formatChipBytes(bytes ) {
|
|
|
181
174
|
}
|
|
182
175
|
|
|
183
176
|
/** Compact magnitude for a finding's plan-graph chip (e.g. "4.2 GB", "3.2×", "45%"), taken from the
|
|
184
|
-
* finding's own `value` rendered in its `metric`'s unit. Returns null when `value`
|
|
185
|
-
*
|
|
186
|
-
export function formatFindingMagnitude(finding
|
|
177
|
+
* finding's own `value` rendered in its `metric`'s unit. Returns null when there is no `value` (a
|
|
178
|
+
* text-valued finding carries `valueText` instead) or the metric isn't in FINDING_METRIC_UNIT. */
|
|
179
|
+
export function formatFindingMagnitude(finding ) {
|
|
187
180
|
const unit = finding.metric ? FINDING_METRIC_UNIT[finding.metric] : undefined;
|
|
188
|
-
if (!unit ||
|
|
181
|
+
if (!unit || finding.value == null) return null;
|
|
189
182
|
switch (unit) {
|
|
190
183
|
case 'bytes': return formatChipBytes(finding.value);
|
|
191
184
|
case 'minutes': return formatDuration(finding.value * 60000);
|
|
@@ -201,7 +194,7 @@ export function formatFindingMagnitude(finding
|
|
|
201
194
|
* recovery is claimed, "~<time>" (the optimistic `high` bound, matching formatImpactEstimateCompact),
|
|
202
195
|
* joined by " · " (e.g. "4.2 GB · ~38.0s"). null when neither part is available. */
|
|
203
196
|
export function formatFindingChipDetail(
|
|
204
|
-
finding
|
|
197
|
+
finding ,
|
|
205
198
|
) {
|
|
206
199
|
const magnitude = formatFindingMagnitude(finding);
|
|
207
200
|
const wallClock = finding.impactEstimate?.wallClock;
|
|
@@ -239,3 +232,75 @@ export function buildHistogram(values , bins )
|
|
|
239
232
|
const labels = counts.map((_, i) => Math.round(min + i * binSize));
|
|
240
233
|
return { labels, data: counts };
|
|
241
234
|
}
|
|
235
|
+
|
|
236
|
+
// Unit formatting for savings and resource figures, shared by the interpretation and by
|
|
237
|
+
// renderers that total a filtered selection themselves (the recommendation rollup).
|
|
238
|
+
export function fmtMs(ms ) {
|
|
239
|
+
return ms === 0 ? '0s' : formatDuration(ms);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function formatWallClockRange(low , high ) {
|
|
243
|
+
const lowText = fmtMs(low);
|
|
244
|
+
const highText = fmtMs(high);
|
|
245
|
+
// Compare the formatted strings, not the raw ms values: formatDuration
|
|
246
|
+
// floors to whole seconds (or minutes+seconds) once a value hits 60s, so
|
|
247
|
+
// two endpoints that differ by under a bucket's precision (e.g. 140900ms
|
|
248
|
+
// vs 141200ms, both "2m 21s") would still fail a raw low === high check
|
|
249
|
+
// and render as a degenerate "2m 21s-2m 21s" range.
|
|
250
|
+
if (lowText === highText) return highText;
|
|
251
|
+
return `${lowText}-${highText}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const MS_PER_HOUR = 3_600_000;
|
|
255
|
+
const MB_SECONDS_PER_GB_HOUR = 1024 * 3600;
|
|
256
|
+
/** Below this many hours a figure keeps its small unit, so "0.0 GB-h" never
|
|
257
|
+
* hides a real but small amount. */
|
|
258
|
+
const MIN_HOURS_SHOWN = 0.1;
|
|
259
|
+
|
|
260
|
+
const oneDecimal = (value ) =>
|
|
261
|
+
(Math.round(value * 10) / 10).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
|
262
|
+
|
|
263
|
+
export function formatRawWaste(rawWaste ) {
|
|
264
|
+
const rounded = Math.round(rawWaste.value * 10) / 10;
|
|
265
|
+
switch (rawWaste.unit) {
|
|
266
|
+
case 'bytes':
|
|
267
|
+
return formatBytes(rawWaste.value);
|
|
268
|
+
case 'ms':
|
|
269
|
+
return fmtMs(rawWaste.value);
|
|
270
|
+
case 'mbSeconds': {
|
|
271
|
+
// A whole run's idle memory reaches millions of MB-seconds; GB-hours
|
|
272
|
+
// keeps it a number a reader can compare.
|
|
273
|
+
const gbHours = rawWaste.value / MB_SECONDS_PER_GB_HOUR;
|
|
274
|
+
return gbHours >= MIN_HOURS_SHOWN ? `${oneDecimal(gbHours)} GB-h` : `${rounded.toLocaleString('en-US')} MB-s`;
|
|
275
|
+
}
|
|
276
|
+
case 'coreHours':
|
|
277
|
+
return `${rounded.toFixed(1)} core-h`;
|
|
278
|
+
case 'coreMs': {
|
|
279
|
+
const coreHours = rawWaste.value / MS_PER_HOUR;
|
|
280
|
+
return coreHours >= MIN_HOURS_SHOWN ? `${oneDecimal(coreHours)} core-h` : `${oneDecimal(rawWaste.value / 1000)} core-s`;
|
|
281
|
+
}
|
|
282
|
+
default:
|
|
283
|
+
return String(rawWaste.value);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// formatRawWaste/fmtMs round to a fixed precision (one decimal place, or
|
|
288
|
+
// whole milliseconds below 1s), which can collapse a small but genuinely
|
|
289
|
+
// nonzero value down to a formatted string that reads exactly like zero
|
|
290
|
+
// (0.04 coreHours -> "0.0 core-h"). Checking the raw value against 0 misses
|
|
291
|
+
// this; checking the *formatted* text against zero's own formatted text
|
|
292
|
+
// catches it regardless of unit. The fractional group must consume the
|
|
293
|
+
// entire decimal part (all zeros) before the terminator: otherwise a real
|
|
294
|
+
// value like "0.5 core-h" leaves the "." unconsumed, and the terminator
|
|
295
|
+
// char class excludes ".", so it correctly fails to match.
|
|
296
|
+
export function readsAsZero(formatted ) {
|
|
297
|
+
return /^0(\.0+)?(?:[^0-9.]|$)/.test(formatted);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Whole cores from 10 up; one decimal below, so a small run's peak never
|
|
301
|
+
* rounds down to "0 cores". */
|
|
302
|
+
export function formatCores(cores ) {
|
|
303
|
+
return cores >= 10 ? String(Math.round(cores)) : cores.toFixed(1).replace(/\.0$/, '');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const MS_PER_CORE_HOUR = 3.6e6;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { auditConfig } from './analyzer.js';
|
|
2
|
+
import { buildExportRunData, CORE_VERSION, } from './export-data.js';
|
|
3
|
+
import { redactRunModel } from './redact.js';
|
|
4
|
+
import { interpretRun } from './run-interpretation.js';
|
|
5
|
+
import { gzipSync, strToU8 } from './vendor/fflate.js';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// Shared by the two producers of the self-contained HTML dashboard: the CLI's
|
|
9
|
+
// --export-html (writeHtmlExport, which writes a data.js next to the template)
|
|
10
|
+
// and the dashboard's "Download HTML" item (EvidenceExport.tsx, which inlines
|
|
11
|
+
// the same statement into one downloaded file). Both encode through
|
|
12
|
+
// encodeRunPayload here and wrap it with runPayloadScript (run-payload.ts); the
|
|
13
|
+
// export app's decodeRunPayload (src/export/hydrate-store.ts) reverses it.
|
|
14
|
+
|
|
15
|
+
/** The run data an HTML export carries: the serialized model, the config
|
|
16
|
+
* audit, and the whole interpretation layer (verdict, coverage, formatted
|
|
17
|
+
* savings, run shape) computed here, so the bundle that opens the file renders
|
|
18
|
+
* this core's conclusions instead of deriving its own. With `redact`, the model
|
|
19
|
+
* and findings are pseudonymized first and the redacted run is interpreted, so
|
|
20
|
+
* text the interpretation truncates (Spark's failure reason) can never keep
|
|
21
|
+
* part of an identifier the redactor would no longer recognize. */
|
|
22
|
+
export function buildHtmlExportData(
|
|
23
|
+
appModel ,
|
|
24
|
+
catalog ,
|
|
25
|
+
skippedLines ,
|
|
26
|
+
{ redact, buildId, producer } ,
|
|
27
|
+
) {
|
|
28
|
+
const raw = { appModel, catalog, configFindings: auditConfig(appModel.app) };
|
|
29
|
+
const run = redact ? redactRunModel(raw.appModel, raw.catalog, raw.configFindings) : raw;
|
|
30
|
+
const interpretation = interpretRun(run.appModel, run.catalog, run.configFindings);
|
|
31
|
+
const provenance = { coreVersion: CORE_VERSION, buildId, producer };
|
|
32
|
+
return buildExportRunData(run.appModel, run.catalog, run.configFindings, skippedLines, interpretation, provenance);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// String.fromCharCode spreads its arguments onto the stack; a multi-MB payload
|
|
36
|
+
// in one call overflows it, so convert in slices.
|
|
37
|
+
const BASE64_CHUNK_BYTES = 0x8000;
|
|
38
|
+
|
|
39
|
+
function bytesToBase64(bytes ) {
|
|
40
|
+
let binary = '';
|
|
41
|
+
for (let i = 0; i < bytes.length; i += BASE64_CHUNK_BYTES) {
|
|
42
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_BYTES));
|
|
43
|
+
}
|
|
44
|
+
return btoa(binary);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** gzip + base64 of the JSON payload, runnable in the browser (fflate, no
|
|
48
|
+
* node:zlib). strToU8 encodes UTF-8, matching decodeRunPayload's decode. */
|
|
49
|
+
export function encodeRunPayload(data ) {
|
|
50
|
+
return bytesToBase64(gzipSync(strToU8(JSON.stringify(data))));
|
|
51
|
+
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
|
|
2
|
-
import { STRAGGLER_FLOOR_PCT_WARN, STRAGGLER_FLOOR_PCT_CRIT } from './detectors.js';
|
|
3
2
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
const
|
|
3
|
+
// The run-wide noise floor, as a share of the run's duration, that grades every wall-clock
|
|
4
|
+
// estimate (NOT SOURCED: our own, unvalidated). skew and straggler default their firing floors to
|
|
5
|
+
// these same figures (detectors.ts), so a finding they admit grades at least warning here.
|
|
6
|
+
export const IMPACT_FLOOR_PCT_WARN = 0.005;
|
|
7
|
+
export const IMPACT_FLOOR_PCT_CRIT = 0.02;
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/** The share-of-run floors one finding grades against. */
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const DEFAULT_FLOORS = { warnPct: IMPACT_FLOOR_PCT_WARN, critPct: IMPACT_FLOOR_PCT_CRIT };
|
|
13
|
+
|
|
14
|
+
/** The run's wall-clock duration, or null when unknown or non-positive: the denominator of both
|
|
15
|
+
* this band and the detectors' runtime floors. */
|
|
16
|
+
export function appDurationMs(app ) {
|
|
10
17
|
if (app?.startTime == null || app?.endTime == null) return null;
|
|
11
18
|
const durationMs = app.endTime - app.startTime;
|
|
12
19
|
return durationMs > 0 ? durationMs : null;
|
|
@@ -23,8 +30,13 @@ function appDurationMs(app ) {
|
|
|
23
30
|
* figure), a deliberate split from view/impact-sort.ts's 'impact' sort, which ranks by .low so an
|
|
24
31
|
* optimistic-but-contended finding never outranks a smaller certain one. Same range, two fields for
|
|
25
32
|
* two questions.
|
|
33
|
+
*
|
|
34
|
+
* `floorsFor` supplies a finding type's floors when a tuned run moved them (skew's and straggler's
|
|
35
|
+
* own floorPctWarn/floorPctCrit); omitted, or for any type it returns nothing, the defaults above.
|
|
26
36
|
*/
|
|
27
|
-
export function deriveImpactBand(
|
|
37
|
+
export function deriveImpactBand(
|
|
38
|
+
findings , app , floorsFor ,
|
|
39
|
+
) {
|
|
28
40
|
const durationMs = appDurationMs(app);
|
|
29
41
|
if (durationMs == null) return findings;
|
|
30
42
|
for (const finding of findings) {
|
|
@@ -38,7 +50,8 @@ export function deriveImpactBand(findings , app )
|
|
|
38
50
|
const recoverableMs = finding.impactEstimate?.wallClock?.high;
|
|
39
51
|
if (recoverableMs == null) continue;
|
|
40
52
|
const pct = recoverableMs / durationMs;
|
|
41
|
-
|
|
53
|
+
const { warnPct, critPct } = floorsFor?.(finding.type) ?? DEFAULT_FLOORS;
|
|
54
|
+
finding.impactBand = pct >= critPct ? 'critical' : pct >= warnPct ? 'warning' : 'info';
|
|
42
55
|
}
|
|
43
56
|
return findings;
|
|
44
57
|
}
|