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.
Files changed (60) hide show
  1. package/bin/sparkforensics-analyze.mjs +288 -0
  2. package/package.json +29 -0
  3. package/vendor-core/analyzer.js +167 -0
  4. package/vendor-core/assert-never.js +3 -0
  5. package/vendor-core/cli/budgets.js +203 -0
  6. package/vendor-core/cli/collect-run.js +107 -0
  7. package/vendor-core/core-count.js +68 -0
  8. package/vendor-core/core-locality-ratio.js +54 -0
  9. package/vendor-core/core-time-series.js +92 -0
  10. package/vendor-core/core-usage-locality.js +70 -0
  11. package/vendor-core/detectors.js +1989 -0
  12. package/vendor-core/docs-config.js +72 -0
  13. package/vendor-core/docs-site-config.js +23 -0
  14. package/vendor-core/efficiency-model.js +62 -0
  15. package/vendor-core/etl-phases.js +28 -0
  16. package/vendor-core/event-handlers.js +906 -0
  17. package/vendor-core/event-schemas.js +405 -0
  18. package/vendor-core/evidence-availability.js +121 -0
  19. package/vendor-core/evidence-report.js +459 -0
  20. package/vendor-core/finding-action-label.js +97 -0
  21. package/vendor-core/finding-filter-predicate.js +38 -0
  22. package/vendor-core/format-utils.js +167 -0
  23. package/vendor-core/impact-band.js +50 -0
  24. package/vendor-core/impact-estimator.js +428 -0
  25. package/vendor-core/ingest.js +139 -0
  26. package/vendor-core/job-groups.js +30 -0
  27. package/vendor-core/load-vendored.js +24 -0
  28. package/vendor-core/lz4-block.js +135 -0
  29. package/vendor-core/mcp-error.js +3 -0
  30. package/vendor-core/mcp-server-factory.js +115 -0
  31. package/vendor-core/mcp-tools.js +331 -0
  32. package/vendor-core/model-assembler.js +76 -0
  33. package/vendor-core/occupancy.js +202 -0
  34. package/vendor-core/parser-worker.js +249 -0
  35. package/vendor-core/plan-dot.js +25 -0
  36. package/vendor-core/plan-duration-attribution.js +185 -0
  37. package/vendor-core/plan-graph-model.js +171 -0
  38. package/vendor-core/plan-node-detail.js +159 -0
  39. package/vendor-core/plan-summary.js +233 -0
  40. package/vendor-core/plan-tree-walk.js +29 -0
  41. package/vendor-core/proxy.js +157 -0
  42. package/vendor-core/recommendation-rollup.js +197 -0
  43. package/vendor-core/redact.js +175 -0
  44. package/vendor-core/rolling-log-reassembly.js +52 -0
  45. package/vendor-core/run-aggregates.js +44 -0
  46. package/vendor-core/run-comparison.js +458 -0
  47. package/vendor-core/scaling-sim.js +73 -0
  48. package/vendor-core/session-snapshot.js +79 -0
  49. package/vendor-core/shs-fetch.js +196 -0
  50. package/vendor-core/shs-load.js +121 -0
  51. package/vendor-core/shs-request.js +101 -0
  52. package/vendor-core/shs-schemas.js +13 -0
  53. package/vendor-core/snappy-block.js +140 -0
  54. package/vendor-core/stage-quantiles.js +199 -0
  55. package/vendor-core/threshold-summary.js +35 -0
  56. package/vendor-core/types.js +286 -0
  57. package/vendor-core/vendor/fflate.js +2695 -0
  58. package/vendor-core/vendor/fzstd.js +768 -0
  59. package/vendor-core/wall-clock.js +36 -0
  60. 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
+ }