sparkforensics-mcp 0.3.0 → 0.5.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/bin/sparkforensics-mcp.mjs +41 -10
- package/package.json +1 -1
- package/vendor-core/allocation.js +106 -0
- package/vendor-core/analyzer.js +168 -60
- package/vendor-core/check-coverage.js +88 -0
- package/vendor-core/cli/budgets.js +54 -27
- package/vendor-core/cli/collect-run.js +84 -32
- package/vendor-core/cli/regression-budgets.js +83 -0
- 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 +1094 -500
- 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/cache.md +3 -2
- package/vendor-core/docs-content/detection/cfg.md +9 -8
- package/vendor-core/docs-content/detection/chrn.md +1 -2
- package/vendor-core/docs-content/detection/cold.md +4 -2
- package/vendor-core/docs-content/detection/cstor.md +9 -0
- package/vendor-core/docs-content/detection/fail.md +3 -2
- package/vendor-core/docs-content/detection/gc.md +3 -2
- package/vendor-core/docs-content/detection/host.md +2 -1
- package/vendor-core/docs-content/detection/local.md +1 -1
- package/vendor-core/docs-content/detection/mem.md +5 -2
- package/vendor-core/docs-content/detection/plan.md +2 -1
- package/vendor-core/docs-content/detection/sfail.md +2 -1
- package/vendor-core/docs-content/detection/shape.md +5 -4
- package/vendor-core/docs-content/detection/skew.md +3 -1
- package/vendor-core/docs-content/detection/slow.md +2 -2
- package/vendor-core/docs-content/detection/spec.md +2 -3
- package/vendor-core/docs-content/detection/spill.md +1 -1
- package/vendor-core/docs-site-config.js +3 -0
- package/vendor-core/effective-conf.js +107 -0
- package/vendor-core/efficiency-model.js +8 -6
- package/vendor-core/event-handlers.js +321 -44
- package/vendor-core/event-schemas.js +23 -0
- package/vendor-core/evidence-report.js +432 -115
- 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 +26 -105
- package/vendor-core/finding-names.js +28 -45
- package/vendor-core/finding-presentation.js +368 -0
- package/vendor-core/finding-tag-help.js +110 -0
- package/vendor-core/finding-types.js +373 -0
- package/vendor-core/findings-of-type.js +11 -0
- package/vendor-core/format-utils.js +96 -30
- package/vendor-core/html-export.js +51 -0
- package/vendor-core/impact-band.js +21 -8
- package/vendor-core/impact-estimator.js +25 -520
- package/vendor-core/impact-format.js +115 -0
- package/vendor-core/impact-model.js +197 -0
- package/vendor-core/ingest.js +6 -2
- package/vendor-core/intervals.js +13 -0
- package/vendor-core/list-runs.js +7 -5
- 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 +35 -1
- package/vendor-core/occupancy.js +1 -1
- package/vendor-core/parser-worker.js +2 -2
- package/vendor-core/plan-graph-model.js +3 -2
- package/vendor-core/plan-node-detail.js +1 -1
- package/vendor-core/proxy.js +3 -1
- package/vendor-core/python-stage.js +25 -0
- package/vendor-core/recommendation-rollup.js +70 -3
- package/vendor-core/redact.js +96 -37
- package/vendor-core/remediation.js +20 -0
- package/vendor-core/run-comparison.js +73 -29
- package/vendor-core/run-interpretation.js +291 -0
- package/vendor-core/run-metrics.js +198 -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-totals.js +24 -0
- package/vendor-core/run-verdict.js +352 -0
- package/vendor-core/scaling-sim.js +4 -5
- package/vendor-core/scorecard-estimates.js +63 -0
- package/vendor-core/session-snapshot.js +7 -0
- package/vendor-core/shs-schemas.js +2 -2
- package/vendor-core/spark-memory.js +17 -0
- package/vendor-core/sql-stages.js +11 -0
- package/vendor-core/stage-plan-nodes.js +18 -0
- package/vendor-core/stage-quantiles.js +6 -0
- package/vendor-core/threshold-overrides.js +160 -0
- package/vendor-core/threshold-summary.js +11 -33
- package/vendor-core/types.js +54 -42
- package/vendor-core/wall-clock.js +1 -12
- package/vendor-core/wasted-core-hours.js +12 -9
- package/vendor-core/write-targets.js +312 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// User threshold overrides: validation of a parsed config file, and which of a detector's
|
|
2
|
+
// thresholds an override actually moved off its specification default. Only the CLI and the MCP
|
|
3
|
+
// server accept overrides; the dashboard always runs the defaults. Reading the file is
|
|
4
|
+
// cli/threshold-config.ts's job, so this module stays free of Node APIs.
|
|
5
|
+
import {
|
|
6
|
+
DETECTORS, ENTRY_BY_TYPE, detectorCatalog,
|
|
7
|
+
} from './detectors.js';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const entries = DETECTORS;
|
|
11
|
+
|
|
12
|
+
function isPlainObject(value ) {
|
|
13
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isNonNegativeNumber(value ) {
|
|
17
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Entries a user may tune: not config-scope (those audits compare against Spark's own defaults,
|
|
21
|
+
// such as its memoryOverhead floor), and with at least one threshold.
|
|
22
|
+
function tunableEntry(type ) {
|
|
23
|
+
const entry = entries.find((d) => d.type === type);
|
|
24
|
+
if (!entry) {
|
|
25
|
+
const tunable = entries.filter((d) => d.scope !== 'config' && Object.keys(d.thresholds).length > 0).map((d) => d.type);
|
|
26
|
+
throw new Error(`unknown detector "${type}" (tunable detectors: ${tunable.join(', ')})`);
|
|
27
|
+
}
|
|
28
|
+
if (entry.scope === 'config') throw new Error(`"${type}" is not tunable: its checks compare against Spark's own defaults`);
|
|
29
|
+
if (Object.keys(entry.thresholds).length === 0) throw new Error(`"${type}" has no thresholds to tune`);
|
|
30
|
+
return entry;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function checkValue(type , name , fallback , value ) {
|
|
34
|
+
if (!Array.isArray(fallback)) {
|
|
35
|
+
if (!isNonNegativeNumber(value)) throw new Error(`"${type}.${name}" must be a non-negative number`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
// Tier tables are indexed by position and read as ascending bands.
|
|
39
|
+
const ascending = Array.isArray(value) && value.length === fallback.length
|
|
40
|
+
&& value.every(isNonNegativeNumber) && value.every((v, i) => i === 0 || v >= value[i - 1]);
|
|
41
|
+
if (!ascending) throw new Error(`"${type}.${name}" must be an ascending list of ${fallback.length} non-negative numbers`);
|
|
42
|
+
return Object.freeze([...value]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validates a parsed thresholds config: `{ "<detector type>": { "<threshold>": value } }`, with each
|
|
47
|
+
* value shaped like that threshold's default (see a report's `detectors` catalog for the names,
|
|
48
|
+
* units and defaults). Throws an Error naming the first problem; never drops a bad entry silently.
|
|
49
|
+
*/
|
|
50
|
+
export function parseThresholdOverrides(raw ) {
|
|
51
|
+
if (!isPlainObject(raw)) throw new Error('expected a JSON object keyed by detector type');
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const [type, perDetector] of Object.entries(raw)) {
|
|
54
|
+
const entry = tunableEntry(type);
|
|
55
|
+
if (!isPlainObject(perDetector)) throw new Error(`"${type}" must be an object of threshold values`);
|
|
56
|
+
const values = {};
|
|
57
|
+
for (const [name, value] of Object.entries(perDetector)) {
|
|
58
|
+
// Own keys only: an inherited name such as `constructor` or `toString` is no threshold.
|
|
59
|
+
if (!Object.hasOwn(entry.thresholds, name)) {
|
|
60
|
+
throw new Error(`unknown threshold "${type}.${name}" (${type} thresholds: ${Object.keys(entry.thresholds).join(', ')})`);
|
|
61
|
+
}
|
|
62
|
+
values[name] = checkValue(type, name, entry.thresholds[name], value);
|
|
63
|
+
}
|
|
64
|
+
result[type] = Object.freeze(values);
|
|
65
|
+
}
|
|
66
|
+
return Object.freeze(result) ;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The overrides for one entry, as the untyped map withThresholds() takes. */
|
|
70
|
+
export function overridesFor(entry , overrides ) {
|
|
71
|
+
return (overrides )?.[entry.type];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sameValue(a , b ) {
|
|
75
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
76
|
+
return a === b;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The entry's thresholds that `overrides` moves off the default, or null when none does (no
|
|
80
|
+
* override, or one equal to the default: that run is the specification's). */
|
|
81
|
+
export function tunedThresholdsOf(entry , overrides ) {
|
|
82
|
+
const tuned = {};
|
|
83
|
+
for (const [name, value] of Object.entries(overridesFor(entry, overrides) ?? {})) {
|
|
84
|
+
if (!Object.hasOwn(entry.thresholds, name)) continue;
|
|
85
|
+
const fallback = entry.thresholds[name];
|
|
86
|
+
if (!sameValue(value, fallback)) tuned[name] = { value, default: fallback };
|
|
87
|
+
}
|
|
88
|
+
return Object.keys(tuned).length > 0 ? tuned : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The tuned thresholds an entry's findings carry: its own, plus its `suppressedBy` entry's
|
|
92
|
+
* (named `<suppressor>.<threshold>`), since tuning the suppressor changes which of them survive. */
|
|
93
|
+
export function findingTunedThresholds(entry , overrides ) {
|
|
94
|
+
const own = tunedThresholdsOf(entry, overrides);
|
|
95
|
+
const suppressor = entry.suppressedBy ? entries.find((d) => d.type === entry.suppressedBy) : undefined;
|
|
96
|
+
const bySuppressor = suppressor ? tunedThresholdsOf(suppressor, overrides) : null;
|
|
97
|
+
if (!suppressor || !bySuppressor) return own;
|
|
98
|
+
const prefixed = Object.fromEntries(Object.entries(bySuppressor).map(([name, t]) => [`${suppressor.type}.${name}`, t]));
|
|
99
|
+
return { ...own, ...prefixed };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** findingTunedThresholds() for the first entry emitting finding type `type`, the entry whose
|
|
103
|
+
* thresholds its clean-check summary reads. */
|
|
104
|
+
export function tunedThresholdsForType(type , overrides ) {
|
|
105
|
+
const entry = ENTRY_BY_TYPE.get(type);
|
|
106
|
+
return entry ? findingTunedThresholds(entry, overrides) : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Every tuned detector's overridden thresholds, keyed by entry type, or null when none is tuned. */
|
|
110
|
+
export function tunedDetectors(overrides ) {
|
|
111
|
+
const byType = {};
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const tuned = tunedThresholdsOf(entry, overrides);
|
|
114
|
+
if (tuned) byType[entry.type] = tuned;
|
|
115
|
+
}
|
|
116
|
+
return Object.keys(byType).length > 0 ? byType : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The thresholds an entry runs with under `overrides`: its own, with any override merged in. */
|
|
120
|
+
export function effectiveThresholds(entry , overrides ) {
|
|
121
|
+
const own = overridesFor(entry, overrides);
|
|
122
|
+
return own ? { ...entry.thresholds, ...own } : entry.thresholds;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** detectorCatalog() as a run under `overrides` used it: each row's effective thresholds, plus
|
|
126
|
+
* `tunedThresholds` on a row an override moved off its defaults. */
|
|
127
|
+
export function tunedDetectorCatalog(overrides ) {
|
|
128
|
+
return detectorCatalog().map((row, i) => {
|
|
129
|
+
const entry = entries[i];
|
|
130
|
+
const tuned = tunedThresholdsOf(entry, overrides);
|
|
131
|
+
return tuned ? { ...row, thresholds: effectiveThresholds(entry, overrides), tunedThresholds: tuned } : row;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatThresholdValue(value ) {
|
|
136
|
+
return Array.isArray(value) ? `[${value.join(', ')}]` : String(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** "ratioWarn 5 (default 3), minTasksForP95 40 (default 20)". */
|
|
140
|
+
export function describeTunedThresholds(tuned ) {
|
|
141
|
+
return Object.entries(tuned)
|
|
142
|
+
.map(([name, { value, default: fallback }]) => `${name} ${formatThresholdValue(value)} (default ${formatThresholdValue(fallback)})`)
|
|
143
|
+
.join(', ');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A tuned run's report line after its label: every tuned detector's thresholds, then why its
|
|
147
|
+
* findings' estimates are uncalibrated. `byType` is tunedDetectors()'s result. */
|
|
148
|
+
export function tunedRunNote(byType ) {
|
|
149
|
+
const tuned = Object.entries(byType).map(([type, t]) => `${type} ${describeTunedThresholds(t)}`).join('; ');
|
|
150
|
+
return `${tuned}. Findings from these detectors are marked, and their impact estimates are uncalibrated: the estimates are calibrated against the default thresholds.`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The caveat a tuned finding carries in `validationRequired`: which thresholds produced it and,
|
|
154
|
+
* when it has an estimate figure (`hasEstimate`), that the figure was never calibrated. */
|
|
155
|
+
export function tunedThresholdsNote(tuned , hasEstimate ) {
|
|
156
|
+
const label = `Produced with tuned thresholds: ${describeTunedThresholds(tuned)}.`;
|
|
157
|
+
return hasEstimate
|
|
158
|
+
? `${label} Impact estimates are calibrated against the default thresholds, so this finding's estimate is unvalidated.`
|
|
159
|
+
: label;
|
|
160
|
+
}
|
|
@@ -1,35 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
skew: 'task duration skew above the configured ratio',
|
|
5
|
-
gc: 'JVM GC time share above the configured ratio',
|
|
6
|
-
slowHost: 'a host running 2x+ slower than its peers by mean task duration (per-executor byte/time dimensions use a separate, narrower ratio ladder starting at 1.33x; only those can reach critical on ratio alone)',
|
|
7
|
-
stageSlowness: 'a stage running far longer than its peers, not attributable to a single slow host',
|
|
8
|
-
straggler: 'one or more tasks finishing far after the rest of their stage',
|
|
9
|
-
speculationWaste: 'speculative task attempts that completed after the original',
|
|
10
|
-
tinyTask: 'median task duration below the configured floor',
|
|
11
|
-
partitionSizing: 'partition byte size outside the configured target range',
|
|
12
|
-
stageShape: 'low parallelism, data explosion, or task-count skew relative to core count',
|
|
13
|
-
stageFailed: 'a stage that failed outright',
|
|
14
|
-
failures: 'task failures above the configured rate',
|
|
15
|
-
retryWaste: 'retried task attempts consuming executor time',
|
|
16
|
-
coldStart: 'executor startup time above the configured floor',
|
|
17
|
-
incompleteRun: 'an event log missing its terminal ApplicationEnd/job-completion event',
|
|
18
|
-
utilization: 'core occupancy below the configured floor across the run',
|
|
19
|
-
memoryUtilization: 'executor heap usage outside the configured band',
|
|
20
|
-
cacheUtilization: 'cached partitions evicted or spilled to disk',
|
|
21
|
-
coreLocality: 'task placement missing data-local core assignment',
|
|
22
|
-
cachingOpportunity: 'a dataset re-read from source multiple times with no cache/persist',
|
|
23
|
-
jobFailureRate: 'job failure rate above the configured threshold',
|
|
24
|
-
autoscalingChurn: 'executor add/remove churn above the configured rate',
|
|
25
|
-
configAudit: 'a Spark conf value outside the recommended range',
|
|
26
|
-
duplicatePlanSubtree: 'the same physical plan subtree executed more than once',
|
|
27
|
-
smallFiles: 'output files below the configured target size',
|
|
28
|
-
overBroadcast: 'a broadcast join above the configured size ceiling',
|
|
29
|
-
underBroadcast: 'a join below the configured size floor that skipped broadcast',
|
|
30
|
-
broadcastSizing: 'a broadcast join outside the configured size range in either direction',
|
|
31
|
-
};
|
|
1
|
+
import { ENTRY_BY_TYPE, } from './detectors.js';
|
|
2
|
+
import { presentationOf } from './finding-presentation.js';
|
|
3
|
+
import { effectiveThresholds } from './threshold-overrides.js';
|
|
32
4
|
|
|
33
|
-
|
|
34
|
-
|
|
5
|
+
/** The clean-check criterion for a finding type, built from the thresholds of the first DETECTORS
|
|
6
|
+
* entry that emits it (configAudit's four entries share one summary), with `overrides` merged in
|
|
7
|
+
* when the run was tuned. */
|
|
8
|
+
export function getThresholdSummary(type , overrides ) {
|
|
9
|
+
const presentation = presentationOf(type);
|
|
10
|
+
const entry = ENTRY_BY_TYPE.get(type);
|
|
11
|
+
if (!presentation || !entry) return 'criteria not met';
|
|
12
|
+
return presentation.thresholdSummary(effectiveThresholds(entry, overrides) );
|
|
35
13
|
}
|
package/vendor-core/types.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
|
|
2
5
|
|
|
3
6
|
|
|
@@ -54,6 +57,24 @@
|
|
|
54
57
|
|
|
55
58
|
|
|
56
59
|
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
57
78
|
|
|
58
79
|
|
|
59
80
|
|
|
@@ -67,6 +88,20 @@
|
|
|
67
88
|
|
|
68
89
|
|
|
69
90
|
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
70
105
|
|
|
71
106
|
|
|
72
107
|
|
|
@@ -260,6 +295,10 @@
|
|
|
260
295
|
|
|
261
296
|
|
|
262
297
|
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
263
302
|
|
|
264
303
|
|
|
265
304
|
|
|
@@ -269,6 +308,9 @@
|
|
|
269
308
|
|
|
270
309
|
|
|
271
310
|
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
272
314
|
|
|
273
315
|
|
|
274
316
|
|
|
@@ -285,50 +327,20 @@
|
|
|
285
327
|
|
|
286
328
|
|
|
287
329
|
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
|
|
288
339
|
|
|
289
340
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
341
|
+
// `Finding` is a union discriminated on `type`, one member per emitted finding type: see
|
|
342
|
+
// finding-types.ts for each detector's shape and which of its fields are public evidence.
|
|
343
|
+
|
|
332
344
|
|
|
333
345
|
|
|
334
346
|
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
if (intervals.length === 0) return [];
|
|
3
|
-
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
4
|
-
const out = [[sorted[0][0], sorted[0][1]]];
|
|
5
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
6
|
-
const last = out[out.length - 1];
|
|
7
|
-
const cur = sorted[i];
|
|
8
|
-
if (cur[0] <= last[1]) last[1] = Math.max(last[1], cur[1]);
|
|
9
|
-
else out.push([cur[0], cur[1]]);
|
|
10
|
-
}
|
|
11
|
-
return out;
|
|
12
|
-
}
|
|
1
|
+
import { mergeIntervals } from './intervals.js';
|
|
13
2
|
|
|
14
3
|
export function computeWallClock(app , stages ) {
|
|
15
4
|
const start = app?.startTime ?? 0;
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// the memoryUtilization detector's idle-cores math uses: capacity core-time vs.
|
|
3
3
|
// core-time that actually ran tasks. Feeds a report widget only, no detector.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { computePeakConcurrentCores } from './core-count.js';
|
|
6
|
+
import { MS_PER_CORE_HOUR } from './format-utils.js';
|
|
6
7
|
|
|
7
|
-
export const MS_PER_CORE_HOUR = 3.6e6;
|
|
8
8
|
const TOP_N = 5;
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
@@ -30,21 +30,24 @@ const EMPTY = {
|
|
|
30
30
|
|
|
31
31
|
// `app`/`runAggregates` are nullable because real callers pass null (app is
|
|
32
32
|
// SparkAppInfo | null before parsing completes), which the guard below already
|
|
33
|
-
// tolerates. `resources` is on app's shape so the
|
|
34
|
-
// type-checks; real app objects always carry it.
|
|
33
|
+
// tolerates. `resources` is on app's shape so the computePeakConcurrentCores
|
|
34
|
+
// pass-through type-checks; real app objects always carry it. An omitted
|
|
35
|
+
// `executorsRemoved` means no executor left, where peak cores equal the sum.
|
|
35
36
|
export function computeWastedCoreHours(
|
|
36
37
|
app ,
|
|
37
|
-
executorsAdded
|
|
38
|
+
executorsAdded = [],
|
|
38
39
|
runAggregates ,
|
|
40
|
+
executorsRemoved = [],
|
|
39
41
|
) {
|
|
40
42
|
// Nullish (not falsy) guard on times: a literal startTime:0 is valid.
|
|
41
43
|
if (!runAggregates || app?.startTime == null || app?.endTime == null) return EMPTY;
|
|
42
44
|
const appDurationMs = app.endTime - app.startTime;
|
|
43
45
|
if (appDurationMs <= 0) return EMPTY;
|
|
44
46
|
|
|
45
|
-
// Total cores:
|
|
46
|
-
// configured cores
|
|
47
|
-
|
|
47
|
+
// Total cores: peak concurrent Executor-Added Total Cores, else peak executors ×
|
|
48
|
+
// configured cores. Same capacity as the memoryUtilization detector, so an
|
|
49
|
+
// executor replaced mid-run is not counted twice.
|
|
50
|
+
const totalCores = computePeakConcurrentCores(app, executorsAdded, executorsRemoved);
|
|
48
51
|
if (totalCores <= 0) return EMPTY;
|
|
49
52
|
|
|
50
53
|
const totalCoreHours = (totalCores * appDurationMs) / MS_PER_CORE_HOUR;
|