sparkforensics-cli 0.1.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-analyze.mjs +288 -0
- package/package.json +29 -0
- package/vendor-core/analyzer.js +167 -0
- package/vendor-core/assert-never.js +3 -0
- package/vendor-core/cli/budgets.js +203 -0
- package/vendor-core/cli/collect-run.js +107 -0
- package/vendor-core/core-count.js +68 -0
- package/vendor-core/core-locality-ratio.js +54 -0
- package/vendor-core/core-time-series.js +92 -0
- package/vendor-core/core-usage-locality.js +70 -0
- package/vendor-core/detectors.js +1989 -0
- package/vendor-core/docs-config.js +72 -0
- package/vendor-core/docs-site-config.js +23 -0
- package/vendor-core/efficiency-model.js +62 -0
- package/vendor-core/etl-phases.js +28 -0
- package/vendor-core/event-handlers.js +906 -0
- package/vendor-core/event-schemas.js +405 -0
- package/vendor-core/evidence-availability.js +121 -0
- package/vendor-core/evidence-report.js +459 -0
- package/vendor-core/finding-action-label.js +97 -0
- package/vendor-core/finding-filter-predicate.js +38 -0
- package/vendor-core/format-utils.js +167 -0
- package/vendor-core/impact-band.js +50 -0
- package/vendor-core/impact-estimator.js +428 -0
- package/vendor-core/ingest.js +139 -0
- package/vendor-core/job-groups.js +30 -0
- package/vendor-core/load-vendored.js +24 -0
- package/vendor-core/lz4-block.js +135 -0
- package/vendor-core/mcp-error.js +3 -0
- package/vendor-core/mcp-server-factory.js +115 -0
- package/vendor-core/mcp-tools.js +331 -0
- package/vendor-core/model-assembler.js +76 -0
- package/vendor-core/occupancy.js +202 -0
- package/vendor-core/parser-worker.js +249 -0
- package/vendor-core/plan-dot.js +25 -0
- package/vendor-core/plan-duration-attribution.js +185 -0
- package/vendor-core/plan-graph-model.js +171 -0
- package/vendor-core/plan-node-detail.js +159 -0
- package/vendor-core/plan-summary.js +233 -0
- package/vendor-core/plan-tree-walk.js +29 -0
- package/vendor-core/proxy.js +157 -0
- package/vendor-core/recommendation-rollup.js +197 -0
- package/vendor-core/redact.js +175 -0
- package/vendor-core/rolling-log-reassembly.js +52 -0
- package/vendor-core/run-aggregates.js +44 -0
- package/vendor-core/run-comparison.js +458 -0
- package/vendor-core/scaling-sim.js +73 -0
- package/vendor-core/session-snapshot.js +79 -0
- package/vendor-core/shs-fetch.js +196 -0
- package/vendor-core/shs-load.js +121 -0
- package/vendor-core/shs-request.js +101 -0
- package/vendor-core/shs-schemas.js +13 -0
- package/vendor-core/snappy-block.js +140 -0
- package/vendor-core/stage-quantiles.js +199 -0
- package/vendor-core/threshold-summary.js +35 -0
- package/vendor-core/types.js +286 -0
- package/vendor-core/vendor/fflate.js +2695 -0
- package/vendor-core/vendor/fzstd.js +768 -0
- package/vendor-core/wall-clock.js +36 -0
- package/vendor-core/wasted-core-hours.js +68 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Single source of the docs-panel URL surface. DOCS_BASE_URL is a repo-relative
|
|
2
|
+
// path to the vendored, prebuilt spark-doc index.html committed directly in this
|
|
3
|
+
// repo (refresh it with `npm run update-docs`). The app is frequently opened via
|
|
4
|
+
// file://, where the iframe origin is opaque (see DocsSheet.tsx).
|
|
5
|
+
export const DOCS_BASE_URL = 'vendor/spark-doc/index.html';
|
|
6
|
+
|
|
7
|
+
// Build a docs URL for an anchor like '#bottleneck-skew'. The leading '#' is
|
|
8
|
+
// stripped and re-added so the fragment is never percent-encoded away. `tok`
|
|
9
|
+
// (ELV-043's per-load handshake nonce) goes in the query string, not the hash
|
|
10
|
+
// since the hash is reserved for #anchor navigation. Omitted for callers with no
|
|
11
|
+
// postMessage channel to protect (e.g. the plain <a href> fallback).
|
|
12
|
+
export function docsUrl(anchor , tok ) {
|
|
13
|
+
const frag = String(anchor).replace(/^#/, '');
|
|
14
|
+
const query = tok ? `?tok=${encodeURIComponent(tok)}` : '';
|
|
15
|
+
return `${DOCS_BASE_URL}${query}#${encodeURIComponent(frag)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Internal metric key → docs '#metric-*' anchor. A metric with no entry renders
|
|
19
|
+
// as plain text (no broken link). Keys are the app's own metric identifiers.
|
|
20
|
+
export const METRIC_ANCHORS = {
|
|
21
|
+
'task-duration': '#metric-task-duration',
|
|
22
|
+
'shuffle-read-bytes': '#metric-shuffle-read-bytes',
|
|
23
|
+
'shuffle-write-bytes': '#metric-shuffle-write-bytes',
|
|
24
|
+
'memory-bytes-spilled': '#metric-memory-bytes-spilled',
|
|
25
|
+
'disk-bytes-spilled': '#metric-disk-bytes-spilled',
|
|
26
|
+
'jvm-gc-time': '#metric-jvm-gc-time',
|
|
27
|
+
'gcpct': '#metric-gcpct',
|
|
28
|
+
'executor-run-time': '#metric-executor-run-time',
|
|
29
|
+
'fetch-wait-time-ratio': '#metric-fetch-wait-time-ratio',
|
|
30
|
+
'input-bytes': '#metric-input-bytes',
|
|
31
|
+
'output-bytes': '#metric-output-bytes',
|
|
32
|
+
'io-ratio': '#metric-io-ratio',
|
|
33
|
+
'peak-execution-memory': '#metric-peak-execution-memory',
|
|
34
|
+
'failed-tasks': '#metric-failed-tasks',
|
|
35
|
+
'speculative-tasks': '#metric-speculative-tasks',
|
|
36
|
+
'first-stage-submitted-at': '#metric-first-stage-submitted-at',
|
|
37
|
+
'executor-count': '#metric-executor-count',
|
|
38
|
+
'stage-duration': '#metric-stage-duration',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Allowlist of every anchor that actually exists in the vendored spark-doc
|
|
42
|
+
// bundle. `DocsLink` renders a link only for anchors in this set: a detector
|
|
43
|
+
// may declare a `docAnchor` for a section that has not been written yet (the
|
|
44
|
+
// analyzer contract requires every finding to carry one), and gating here keeps
|
|
45
|
+
// that from becoming a dead "Learn more" link that scrolls nowhere. When a new
|
|
46
|
+
// section lands (via `npm run update-docs`), add its anchor here and the link
|
|
47
|
+
// lights up automatically. `tests/docs-config.test.js` asserts this set stays a
|
|
48
|
+
// subset of the real ids in vendor/spark-doc/index.html so it cannot drift.
|
|
49
|
+
export const KNOWN_DOC_ANCHORS = new Set([
|
|
50
|
+
// Page sections
|
|
51
|
+
'#intro', '#spark-architecture', '#memory-model', '#partitioning', '#joins',
|
|
52
|
+
'#shuffle', '#data-formats', '#table-formats', '#caching', '#pyspark', '#aqe',
|
|
53
|
+
'#cluster-config', '#anti-patterns', '#metrics', '#config',
|
|
54
|
+
// Bottleneck sections
|
|
55
|
+
'#bottleneck-cold-start', '#bottleneck-failures', '#bottleneck-gc',
|
|
56
|
+
'#bottleneck-job-failure-rate', '#bottleneck-retry-waste', '#bottleneck-shuffle',
|
|
57
|
+
'#bottleneck-skew', '#bottleneck-slow-host', '#bottleneck-spill',
|
|
58
|
+
'#bottleneck-straggler', '#bottleneck-tiny-tasks', '#bottleneck-utilization',
|
|
59
|
+
'#bottleneck-memory-utilization', '#bottleneck-broadcast-sizing',
|
|
60
|
+
'#bottleneck-duplicate-plan-subtree', '#bottleneck-small-files',
|
|
61
|
+
'#bottleneck-stage-shape', '#bottleneck-stage-slowness',
|
|
62
|
+
// Config-audit sections
|
|
63
|
+
'#config-autoscale-bounds', '#config-memory-overhead', '#config-serializer',
|
|
64
|
+
'#config-shuffle-service',
|
|
65
|
+
// Metric glossary entries
|
|
66
|
+
...Object.values(METRIC_ANCHORS),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
// True when `anchor` resolves to a real section in the vendored docs.
|
|
70
|
+
export function isKnownDocAnchor(anchor ) {
|
|
71
|
+
return KNOWN_DOC_ANCHORS.has(String(anchor));
|
|
72
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { typeTag } from './format-utils.js';
|
|
2
|
+
|
|
3
|
+
// docs-config.ts's own header comment scopes it to "the single source of the
|
|
4
|
+
// [vendor] docs-panel URL surface"; this is the equivalent single source for
|
|
5
|
+
// docs-site (VitePress) links, kept in its own file so the two unrelated doc
|
|
6
|
+
// systems don't blur into one.
|
|
7
|
+
//
|
|
8
|
+
// The `.html` extension matters: it's what makes this link resolve in the
|
|
9
|
+
// packaged `server/` deploy mode. That static file server
|
|
10
|
+
// (packages/server/lib/static-files.js) matches a request path to a file exactly, with
|
|
11
|
+
// no directory-index or extension-guessing fallback (unlike the Vite
|
|
12
|
+
// dev-server's docs-site middleware in vite.config.ts, which tries
|
|
13
|
+
// bare/`.html`/`index.html` candidates). VitePress's default build
|
|
14
|
+
// (`cleanUrls` unset, i.e. false) names each page's output file `<slug>.html`,
|
|
15
|
+
// so the extension-less form would 404 there.
|
|
16
|
+
//
|
|
17
|
+
// No allowlist/gating function is needed here, unlike `isKnownDocAnchor` for
|
|
18
|
+
// the vendor link: `tests/docs-site-tag-coverage.test.js` already fails CI if
|
|
19
|
+
// any `TYPE_TAG_MAP` value lacks a documented `{#tag}` heading, and both the
|
|
20
|
+
// map and the page live in this same repo, so a mismatch can't ship past CI.
|
|
21
|
+
export function findingGuideUrl(type ) {
|
|
22
|
+
return `/docs/user-guide/understanding-findings.html#${typeTag(type).toLowerCase()}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// §5 Efficiency/wastage model (SparkLens EfficiencyStatisticsAnalyzer).
|
|
2
|
+
// DESIGN SPIKE: decomposes available compute-hours into driver-bound vs.
|
|
3
|
+
// executor-bound waste, plus two theoretical floors. Mapped onto computeWallClock.
|
|
4
|
+
import { computeWallClock } from './wall-clock.js';
|
|
5
|
+
import { computeTotalCores } from './core-count.js';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export function computeEfficiencyModel({ app, stages, executorsAdded, runAggregates }
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
// `app ?? {}`: computeTotalCores just falls back to the executor-derived core sum when
|
|
22
|
+
// `resources` is absent, and every caller here already tolerates a null `app` (malformed
|
|
23
|
+
// logs with no ApplicationStart); `app!` would crash on `app.resources` in that case.
|
|
24
|
+
// `executorsAdded` cast: computeTotalCores only reads `totalCores`, present on
|
|
25
|
+
// ExecutorAddedEvent (the only kind real callers pass here) but not ExecutorRemovedEvent,
|
|
26
|
+
// so the union as a whole is a structural mismatch against computeTotalCores's
|
|
27
|
+
// `{totalCores?}` shape.
|
|
28
|
+
const totalCores = computeTotalCores(app ?? {}, executorsAdded );
|
|
29
|
+
const appDurationMs = (app?.endTime ?? 0) - (app?.startTime ?? 0);
|
|
30
|
+
const wc = computeWallClock(app, stages );
|
|
31
|
+
|
|
32
|
+
const availableComputeHours = totalCores * (appDurationMs / 3600000);
|
|
33
|
+
|
|
34
|
+
// Driver-bound: cores idle during startup + gaps + idle (no active stage).
|
|
35
|
+
const driverIdleMs = wc.startup + wc.gaps + wc.idle;
|
|
36
|
+
const driverWasteHours = totalCores * (driverIdleMs / 3600000);
|
|
37
|
+
|
|
38
|
+
// Executor-bound: during stagesActive, allocated capacity beyond busy cores.
|
|
39
|
+
// Tasks only run during active windows, so whole-run busyCoreMs already lives
|
|
40
|
+
// inside stagesActive; no separate per-window sweep needed.
|
|
41
|
+
const allocatedActiveCoreMs = totalCores * wc.stagesActive;
|
|
42
|
+
const busyCoreMs = runAggregates?.busyCoreMs ?? 0;
|
|
43
|
+
const executorWasteHours = Math.max(0, allocatedActiveCoreMs - busyCoreMs) / 3600000;
|
|
44
|
+
|
|
45
|
+
const wastageHours = driverWasteHours + executorWasteHours;
|
|
46
|
+
const wastagePct = availableComputeHours > 0 ? Math.round(wastageHours / availableComputeHours * 100) : null;
|
|
47
|
+
|
|
48
|
+
let totalTaskMs = 0;
|
|
49
|
+
const perStage = runAggregates?.perStage ?? {};
|
|
50
|
+
for (const k of Object.keys(perStage)) totalTaskMs += perStage[k].totalTaskDurationSum;
|
|
51
|
+
const floorZeroSkewMs = totalCores > 0 ? totalTaskMs / totalCores : 0;
|
|
52
|
+
|
|
53
|
+
let dominantWaste = null;
|
|
54
|
+
if (driverWasteHours > 0 || executorWasteHours > 0) {
|
|
55
|
+
dominantWaste = driverWasteHours >= executorWasteHours ? 'driver' : 'executor';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
availableComputeHours, driverWasteHours, executorWasteHours, wastagePct,
|
|
60
|
+
floorZeroSkewMs, dominantWaste,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// §7 ETL-phase time attribution (Onehouse Spark Analyzer). Heuristic and
|
|
2
|
+
// approximate: classify each stage by byte-flow shape. A stage can be both
|
|
3
|
+
// Transform and Load (shuffle + write), so buckets overlap and need not sum to
|
|
4
|
+
// wall-clock. Storage-format-aware attribution (Hudi/Delta/Iceberg) is NOT
|
|
5
|
+
// portable (no table-format metadata in event logs) and is out of scope.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export function attributeEtlPhases(
|
|
16
|
+
stages ,
|
|
17
|
+
) {
|
|
18
|
+
const acc = { extract: 0, transform: 0, load: 0 };
|
|
19
|
+
for (const s of stages.values()) {
|
|
20
|
+
const dur = Math.max(0, (s.completedAt ?? 0) - (s.submittedAt ?? 0));
|
|
21
|
+
if (dur === 0) continue;
|
|
22
|
+
const shuffled = (s.shuffleReadBytes ?? 0) > 0 || (s.shuffleWriteBytes ?? 0) > 0;
|
|
23
|
+
if ((s.inputBytes ?? 0) > 0 && (s.shuffleReadBytes ?? 0) === 0) acc.extract += dur; // scan-only
|
|
24
|
+
if (shuffled) acc.transform += dur;
|
|
25
|
+
if ((s.outputBytes ?? 0) > 0) acc.load += dur;
|
|
26
|
+
}
|
|
27
|
+
return acc;
|
|
28
|
+
}
|