sparkforensics-server 0.5.2 → 0.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/package.json +1 -1
- package/public/assets/AutoscalingChurn-CRA2Mb7F.js +1 -0
- package/public/assets/CacheUtilization-Bh9OY_V9.js +1 -0
- package/public/assets/{CachingOpportunity-D1NcHs0V.js → CachingOpportunity-oCiQwmTJ.js} +1 -1
- package/public/assets/{ColdStart-CdRJ9gD8.js → ColdStart-BJrtjfmn.js} +1 -1
- package/public/assets/ConfigAudit-DMvKxySP.js +1 -0
- package/public/assets/{CoreUsageArea-a--NTRGY.js → CoreUsageArea-REC2zsKw.js} +1 -1
- package/public/assets/Dashboard-BHZ76JCu.js +2 -0
- package/public/assets/DisclosureContext-OO7X1HAx.js +1 -0
- package/public/assets/{DuplicatePlanSubtree-dobXdz7E.js → DuplicatePlanSubtree-BgG-L-cK.js} +1 -1
- package/public/assets/ExecutorUtilization-D2KhSVbY.js +1 -0
- package/public/assets/{GcPressure-8-G2ri10.js → GcPressure-D1BTE8qh.js} +1 -1
- package/public/assets/{IncompleteRun-B91lky7a.js → IncompleteRun-DD40xUpy.js} +1 -1
- package/public/assets/{JobFailures-BsWvBMyJ.js → JobFailures-YwNzCF19.js} +1 -1
- package/public/assets/MemoryUtilization-9b8ee_3_.js +1 -0
- package/public/assets/{OverBroadcast-DdN8JIJH.js → OverBroadcast-CfctS8a8.js} +1 -1
- package/public/assets/{PartitionSizing-CY5HDYtp.js → PartitionSizing-VG8kBgG5.js} +1 -1
- package/public/assets/{PlanExplorer-DgHCDBD_.js → PlanExplorer-DmubTAOP.js} +1 -1
- package/public/assets/PlanGraphRoute-DctPZjS0.js +1 -0
- package/public/assets/RetryWaste-CnpM8bB7.js +1 -0
- package/public/assets/{ShuffleIO-MMnDESkF.js → ShuffleIO-D13vGmas.js} +1 -1
- package/public/assets/Skew-DlagstYP.js +1 -0
- package/public/assets/SlowHost-cSkLRZ_e.js +1 -0
- package/public/assets/{SmallFiles-BoK3R1Ex.js → SmallFiles-B-kERMcq.js} +1 -1
- package/public/assets/{SpeculationWaste-CnVxB5AJ.js → SpeculationWaste-DaysAzXO.js} +1 -1
- package/public/assets/Spill-H44Gz4US.js +1 -0
- package/public/assets/{StageFailed-mAhHU4l_.js → StageFailed-DCfii9h_.js} +1 -1
- package/public/assets/{StageFindingGroup-BEtPWcT0.js → StageFindingGroup-BXJ0Mj5n.js} +1 -1
- package/public/assets/{StageHeader-DJkYkrsU.js → StageHeader--oN2J-oD.js} +1 -1
- package/public/assets/StageShape-IZ80CZiJ.js +1 -0
- package/public/assets/{StageSlowness-BPRw-otN.js → StageSlowness--BRIaBc1.js} +1 -1
- package/public/assets/Straggler-Ctq0Df_o.js +1 -0
- package/public/assets/TaskFailures-Bks_gqhL.js +1 -0
- package/public/assets/{TinyTask-Bi84qU7n.js → TinyTask-BssfzGFB.js} +1 -1
- package/public/assets/{UnderBroadcast-DdQV7htE.js → UnderBroadcast-DsScrxc-.js} +1 -1
- package/public/assets/{finding-anchor-Cl6_5hfV.js → finding-anchor-BYW1oz5R.js} +1 -1
- package/public/assets/{impact-sort-CssZ6MT2.js → impact-sort-xzy0g-Vh.js} +1 -1
- package/public/assets/index-DhpRGMZc.css +2 -0
- package/public/assets/index-DwqkFUFk.js +14 -0
- package/public/assets/instrument-sans-latin-ext-wdth-normal-C5E2Gvlv.woff2 +0 -0
- package/public/assets/instrument-sans-latin-wdth-normal-BVScPF0l.woff2 +0 -0
- package/public/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/public/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/public/assets/react-vendor-DfpuPQZf.js +9 -0
- package/public/assets/{select-CQjNhprm.js → select-C8edjYTq.js} +1 -1
- package/public/assets/{usePagedRows-Dyl6T8Ok.js → usePagedRows-CsuUFeYx.js} +1 -1
- package/public/assets/useRecentFiles-BvkKo3JN.js +11 -0
- package/public/assets/{useSortMode-Dhvit8dt.js → useSortMode-joX71BZN.js} +1 -1
- package/public/docs/404.html +3 -7
- package/public/docs/assets/{app.eYTPqTeq.js → app.CnWL6bKV.js} +1 -1
- package/public/docs/assets/chunks/@localSearchIndexroot.tE5pz54z.js +1 -0
- package/public/docs/assets/chunks/{VPLocalSearchBox.DsZrQKQR.js → VPLocalSearchBox.C0IBKeRQ.js} +1 -1
- package/public/docs/assets/chunks/{theme.DsTNE2pE.js → theme.BH-2HuRf.js} +2 -2
- package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.CQgn1t_M.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.wulOueeB.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.wj6Et6UD.js → contributor-guide_architecture_detector-contract.md.N4hhenrA.js} +1 -1
- package/public/docs/assets/{contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.js → contributor-guide_architecture_detector-contract_threshold-tables.md.Dp3c40rx.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.DfvbqEUu.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.lean.js → contributor-guide_architecture_impact-estimation.md.DfvbqEUu.lean.js} +1 -1
- package/public/docs/assets/{contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.js → contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.Cwm1jHmn.js} +1 -1
- package/public/docs/assets/{contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.lean.js → contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.Cwm1jHmn.lean.js} +1 -1
- package/public/docs/assets/{contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.js → contributor-guide_architecture_worker-protocol_cli-and-mcp.md.BX1XnFbq.js} +1 -1
- package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.js → contributor-guide_architecture_worker-protocol_evidence-report.md.CiQl1MLb.js} +1 -1
- package/public/docs/assets/{contributor-guide_development-setup.md.D9bu4j_c.js → contributor-guide_development-setup.md.DQdzera7.js} +1 -1
- package/public/docs/assets/instrument-sans-latin-ext-wdth-normal.C5E2Gvlv.woff2 +0 -0
- package/public/docs/assets/instrument-sans-latin-wdth-normal.BVScPF0l.woff2 +0 -0
- package/public/docs/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
- package/public/docs/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
- package/public/docs/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
- package/public/docs/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
- package/public/docs/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
- package/public/docs/assets/style.CyK9Ss73.css +1 -0
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.BEepbDDL.js +24 -0
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.BEepbDDL.lean.js +1 -0
- package/public/docs/assets/{user-guide_mcp-tools_get-run-summary.md.DpxZ4ZNp.js → user-guide_mcp-tools_get-run-summary.md.EhaIsele.js} +1 -1
- package/public/docs/assets/user-guide_understanding-findings.md.3C7KCEZO.js +1 -0
- package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +6 -10
- package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +4 -8
- package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +6 -10
- package/public/docs/contributor-guide/architecture/board-widgets.html +4 -8
- package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +4 -8
- package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +6 -10
- package/public/docs/contributor-guide/architecture/detector-contract.html +6 -10
- package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +4 -8
- package/public/docs/contributor-guide/architecture/drill-down.html +4 -8
- package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +6 -10
- package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +4 -8
- package/public/docs/contributor-guide/architecture/impact-estimation.html +6 -10
- package/public/docs/contributor-guide/architecture/index.html +4 -8
- package/public/docs/contributor-guide/architecture/overview.html +4 -8
- package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +4 -8
- package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +4 -8
- package/public/docs/contributor-guide/architecture/state-and-history.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering.html +4 -8
- package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +6 -10
- package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +4 -8
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +4 -8
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +6 -10
- package/public/docs/contributor-guide/architecture/worker-protocol.html +4 -8
- package/public/docs/contributor-guide/contributing.html +4 -8
- package/public/docs/contributor-guide/development-setup.html +6 -10
- package/public/docs/contributor-guide/testing.html +4 -8
- package/public/docs/favicon.svg +2 -2
- package/public/docs/hashmap.json +1 -1
- package/public/docs/index.html +4 -8
- package/public/docs/tuning-reference/anti-patterns.html +4 -8
- package/public/docs/tuning-reference/aqe.html +4 -8
- package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +4 -8
- package/public/docs/tuning-reference/bottleneck-cold-start.html +4 -8
- package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +4 -8
- package/public/docs/tuning-reference/bottleneck-failures.html +4 -8
- package/public/docs/tuning-reference/bottleneck-gc.html +4 -8
- package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +4 -8
- package/public/docs/tuning-reference/bottleneck-memory-utilization.html +4 -8
- package/public/docs/tuning-reference/bottleneck-retry-waste.html +4 -8
- package/public/docs/tuning-reference/bottleneck-shuffle.html +4 -8
- package/public/docs/tuning-reference/bottleneck-skew.html +4 -8
- package/public/docs/tuning-reference/bottleneck-slow-host.html +4 -8
- package/public/docs/tuning-reference/bottleneck-small-files.html +4 -8
- package/public/docs/tuning-reference/bottleneck-spill.html +4 -8
- package/public/docs/tuning-reference/bottleneck-straggler.html +4 -8
- package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +4 -8
- package/public/docs/tuning-reference/bottleneck-utilization.html +4 -8
- package/public/docs/tuning-reference/caching.html +4 -8
- package/public/docs/tuning-reference/cluster-config.html +4 -8
- package/public/docs/tuning-reference/config.html +4 -8
- package/public/docs/tuning-reference/data-formats.html +4 -8
- package/public/docs/tuning-reference/index.html +5 -9
- package/public/docs/tuning-reference/intro.html +4 -8
- package/public/docs/tuning-reference/joins.html +4 -8
- package/public/docs/tuning-reference/memory-model.html +4 -8
- package/public/docs/tuning-reference/metrics.html +4 -8
- package/public/docs/tuning-reference/partitioning.html +4 -8
- package/public/docs/tuning-reference/pyspark.html +4 -8
- package/public/docs/tuning-reference/shuffle.html +4 -8
- package/public/docs/tuning-reference/spark-architecture.html +4 -8
- package/public/docs/tuning-reference/table-formats.html +4 -8
- package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +4 -8
- package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +4 -8
- package/public/docs/user-guide/alternative-log-retrieval.html +4 -8
- package/public/docs/user-guide/getting-started/ci-and-automation.html +9 -12
- package/public/docs/user-guide/getting-started/reading-the-dashboard.html +4 -8
- package/public/docs/user-guide/getting-started/regression-metric-keys.html +4 -8
- package/public/docs/user-guide/getting-started/tuning-thresholds.html +4 -8
- package/public/docs/user-guide/getting-started.html +4 -8
- package/public/docs/user-guide/mcp-tools/compare-runs.html +4 -8
- package/public/docs/user-guide/mcp-tools/diagnose-run.html +4 -8
- package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +4 -8
- package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +4 -8
- package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +4 -8
- package/public/docs/user-guide/mcp-tools/get-reference-doc.html +4 -8
- package/public/docs/user-guide/mcp-tools/get-run-summary.html +6 -10
- package/public/docs/user-guide/mcp-tools/list-runs.html +4 -8
- package/public/docs/user-guide/mcp-tools.html +4 -8
- package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +4 -8
- package/public/docs/user-guide/run-comparison.html +4 -8
- package/public/docs/user-guide/understanding-findings.html +6 -10
- package/public/export-template.html +46 -46
- package/public/favicon.svg +2 -2
- package/public/index.html +4 -7
- package/vendor-core/allocation.js +105 -20
- package/vendor-core/api-merge-writes.js +1 -0
- package/vendor-core/core-source-hash.txt +1 -1
- package/vendor-core/delta-log-paths.js +10 -1
- package/vendor-core/detectors.js +73 -38
- package/vendor-core/docs-content/detection/mem.md +1 -1
- package/vendor-core/docs-content/detection/skew.md +2 -1
- package/vendor-core/docs-content/detection/util.md +6 -2
- package/vendor-core/efficiency-model.js +22 -14
- package/vendor-core/finding-presentation.js +13 -6
- package/vendor-core/finding-types.js +21 -10
- package/vendor-core/impact-model.js +2 -2
- package/vendor-core/remediation.js +10 -5
- package/vendor-core/run-output.js +14 -3
- package/vendor-core/types.js +12 -2
- package/vendor-core/wall-clock.js +8 -0
- package/vendor-core/wasted-core-hours.js +12 -11
- package/vendor-core/write-targets.js +32 -0
- package/public/assets/AutoscalingChurn-8t3U3isq.js +0 -1
- package/public/assets/CacheUtilization-CJs9tTum.js +0 -1
- package/public/assets/ConfigAudit-B5Gbe5js.js +0 -1
- package/public/assets/Dashboard-C1hNeX49.js +0 -2
- package/public/assets/DisclosureContext-ZpE6CkHx.js +0 -1
- package/public/assets/ExecutorUtilization-D9grwi7t.js +0 -1
- package/public/assets/MemoryUtilization-BM5schZI.js +0 -1
- package/public/assets/PlanGraphRoute-DH3KJb8j.js +0 -1
- package/public/assets/RetryWaste-DOnE9vGF.js +0 -1
- package/public/assets/Skew-BmZZJ5ms.js +0 -1
- package/public/assets/SlowHost-CsJzfEqo.js +0 -1
- package/public/assets/Spill-BhNicyBy.js +0 -1
- package/public/assets/StageShape-BDxRknLp.js +0 -1
- package/public/assets/Straggler-CZCbnOWP.js +0 -1
- package/public/assets/TaskFailures-C57YcLFQ.js +0 -1
- package/public/assets/index-2h_TgUAb.css +0 -2
- package/public/assets/index-B-03mG7K.js +0 -14
- package/public/assets/react-vendor-DEdfrRTJ.js +0 -9
- package/public/assets/useRecentFiles-XZEnLlal.js +0 -11
- package/public/docs/assets/chunks/@localSearchIndexroot.u_e5bQz4.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.js +0 -1
- package/public/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/public/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/public/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/public/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/public/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/public/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/public/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/public/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/public/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/public/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/public/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/public/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/public/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/public/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/public/docs/assets/style.tmDhD3c8.css +0 -1
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.js +0 -23
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.lean.js +0 -1
- package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.js +0 -1
- /package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.lean.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.CQgn1t_M.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.lean.js → contributor-guide_architecture_board-widgets_visual-system-and-templating.md.wulOueeB.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.wj6Et6UD.lean.js → contributor-guide_architecture_detector-contract.md.N4hhenrA.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.lean.js → contributor-guide_architecture_detector-contract_threshold-tables.md.Dp3c40rx.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.lean.js → contributor-guide_architecture_worker-protocol_cli-and-mcp.md.BX1XnFbq.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.lean.js → contributor-guide_architecture_worker-protocol_evidence-report.md.CiQl1MLb.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_development-setup.md.D9bu4j_c.lean.js → contributor-guide_development-setup.md.DQdzera7.lean.js} +0 -0
- /package/public/docs/assets/{user-guide_mcp-tools_get-run-summary.md.DpxZ4ZNp.lean.js → user-guide_mcp-tools_get-run-summary.md.EhaIsele.lean.js} +0 -0
- /package/public/docs/assets/{user-guide_understanding-findings.md.C9YRruMT.lean.js → user-guide_understanding-findings.md.3C7KCEZO.lean.js} +0 -0
package/public/favicon.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2
|
-
<rect width="32" height="32" rx="
|
|
3
|
-
<path d="M17.5 5 8 18h6.5l-1 9L24 14h-6.5l1-9z" fill="#
|
|
2
|
+
<rect width="32" height="32" rx="6" fill="#4f46e5"/>
|
|
3
|
+
<path d="M17.5 5 8 18h6.5l-1 9L24 14h-6.5l1-9z" fill="#eceafd"/>
|
|
4
4
|
</svg>
|
package/public/index.html
CHANGED
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
<meta property="og:url" content="https://shuffle-works.github.io/sparkforensics/">
|
|
11
11
|
<meta property="og:title" content="SparkForensics">
|
|
12
12
|
<meta property="og:description" content="Zero-backend browser dashboard that parses Apache Spark event logs to diagnose performance bottlenecks: skew, spill, GC pressure, stragglers, cache utilization, and more.">
|
|
13
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
14
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
15
|
-
<link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL@400..700,0..1&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
16
13
|
<script>
|
|
17
14
|
try {
|
|
18
15
|
var stored = localStorage.getItem('shuffle-works-theme');
|
|
@@ -23,14 +20,14 @@
|
|
|
23
20
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
24
21
|
} catch (_) {}
|
|
25
22
|
</script>
|
|
26
|
-
<script type="module" crossorigin src="./assets/index-
|
|
23
|
+
<script type="module" crossorigin src="./assets/index-DwqkFUFk.js"></script>
|
|
27
24
|
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-hePW80VL.js">
|
|
28
25
|
<link rel="modulepreload" crossorigin href="./assets/charts-vendor-0VMaLVLN.js">
|
|
29
26
|
<link rel="modulepreload" crossorigin href="./assets/plan-graph-vendor-BYl-AzN5.js">
|
|
30
|
-
<link rel="modulepreload" crossorigin href="./assets/react-vendor-
|
|
31
|
-
<link rel="modulepreload" crossorigin href="./assets/DisclosureContext-
|
|
27
|
+
<link rel="modulepreload" crossorigin href="./assets/react-vendor-DfpuPQZf.js">
|
|
28
|
+
<link rel="modulepreload" crossorigin href="./assets/DisclosureContext-OO7X1HAx.js">
|
|
32
29
|
<link rel="stylesheet" crossorigin href="./assets/plan-graph-vendor-DLioOiRN.css">
|
|
33
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
30
|
+
<link rel="stylesheet" crossorigin href="./assets/index-DhpRGMZc.css">
|
|
34
31
|
</head>
|
|
35
32
|
<body>
|
|
36
33
|
<div id="root"></div>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { computePeakConcurrentExecutorCount } from './core-count.js';
|
|
1
2
|
import { parseSparkMemoryMB } from './spark-memory.js';
|
|
2
|
-
|
|
3
|
+
|
|
3
4
|
|
|
4
5
|
const MS_PER_HOUR = 3_600_000;
|
|
6
|
+
const MS_PER_SECOND = 1000;
|
|
5
7
|
const MIB_PER_GIB = 1024;
|
|
6
8
|
// Spark's documented floor and factor for the default executor memory overhead
|
|
7
9
|
// (spark.executor.memoryOverhead = max(factor * executor memory, 384 MiB)).
|
|
@@ -14,12 +16,29 @@ const DEFAULT_OVERHEAD_FACTOR = 0.1;
|
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
17
31
|
|
|
18
32
|
|
|
33
|
+
// The fields the allocation reads from the parsed model's executor events, so a detector's
|
|
34
|
+
// narrower event shapes qualify too.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
|
|
22
40
|
|
|
41
|
+
|
|
23
42
|
|
|
24
43
|
|
|
25
44
|
/** The latest timestamp the log records: the application start and end and every stage and
|
|
@@ -43,7 +62,7 @@ const DEFAULT_EXECUTOR_MEMORY_MIB = 1024;
|
|
|
43
62
|
// plus overhead, plus off-heap and PySpark worker memory when configured. Null when the log
|
|
44
63
|
// records no Spark properties (nothing to tell a default from a missing config) or a memory key it
|
|
45
64
|
// does record cannot be read.
|
|
46
|
-
function executorMemoryMiB(app
|
|
65
|
+
function executorMemoryMiB(app ) {
|
|
47
66
|
const config = app?.config;
|
|
48
67
|
if (config == null) return null;
|
|
49
68
|
// undefined: key absent; null: present but unreadable.
|
|
@@ -63,6 +82,11 @@ function executorMemoryMiB(app ) {
|
|
|
63
82
|
return heap + overhead + offHeap + pyspark;
|
|
64
83
|
}
|
|
65
84
|
|
|
85
|
+
function loggedDynamicAllocation(app ) {
|
|
86
|
+
const raw = app?.config?.['spark.dynamicAllocation.enabled'];
|
|
87
|
+
return raw == null ? null : raw.trim().toLowerCase() === 'true' ? 'on' : 'off';
|
|
88
|
+
}
|
|
89
|
+
|
|
66
90
|
/** Allocated core-hours and memory GiB-hours from the executor lifecycle: each executor counts
|
|
67
91
|
* from its ExecutorAdded timestamp to its first later ExecutorRemoved timestamp. One with no
|
|
68
92
|
* removal closes at the application end when the log has one, else at the last timestamp the log
|
|
@@ -72,35 +96,96 @@ function executorMemoryMiB(app ) {
|
|
|
72
96
|
* else the larger of 384 MiB and spark.executor.memoryOverheadFactor, default 0.1, times the
|
|
73
97
|
* memory), plus spark.memory.offHeap.size when spark.memory.offHeap.enabled is true, plus
|
|
74
98
|
* spark.executor.pyspark.memory.
|
|
99
|
+
* The executor counts and seconds come from the same alive intervals.
|
|
75
100
|
* Null, never 0, for a figure whose inputs the log lacks. */
|
|
76
101
|
export function computeAllocation(input ) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (e.kind !== 'removed') continue;
|
|
82
|
-
const times = removedAt.get(e.executorId);
|
|
83
|
-
if (times) times.push(e.timestamp); else removedAt.set(e.executorId, [e.timestamp]);
|
|
102
|
+
const dynamicAllocation = loggedDynamicAllocation(input.app);
|
|
103
|
+
const alive = aliveIntervals(input);
|
|
104
|
+
if (alive.length === 0) {
|
|
105
|
+
return { coreHours: null, memoryGbHours: null, dynamicAllocation, executorsPeak: null, executorsMean: null, executorCores: null, executorSeconds: null };
|
|
84
106
|
}
|
|
85
107
|
const closeAt = input.app?.endTime ?? lastObservedTimestamp(input);
|
|
86
|
-
const configuredCores = Number.parseInt(input.app?.config?.['spark.executor.cores'] ?? '', 10);
|
|
87
108
|
const memoryMiB = executorMemoryMiB(input.app);
|
|
88
109
|
|
|
89
110
|
let coreMs = 0;
|
|
90
111
|
let memoryMiBMs = 0;
|
|
112
|
+
let aliveMsTotal = 0;
|
|
91
113
|
let coresKnown = true;
|
|
92
|
-
const
|
|
93
|
-
for (const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const aliveMs = Math.max(0, (removal ?? closeAt ?? e.timestamp) - e.timestamp);
|
|
98
|
-
const cores = e.totalCores > 0 ? e.totalCores : Number.isFinite(configuredCores) ? configuredCores : null;
|
|
99
|
-
if (cores == null) coresKnown = false; else coreMs += cores * aliveMs;
|
|
114
|
+
const coreCounts = new Set ();
|
|
115
|
+
for (const { start, end, cores } of alive) {
|
|
116
|
+
const aliveMs = end - start;
|
|
117
|
+
aliveMsTotal += aliveMs;
|
|
118
|
+
if (cores == null) coresKnown = false; else { coreMs += cores * aliveMs; coreCounts.add(cores); }
|
|
100
119
|
memoryMiBMs += (memoryMiB ?? 0) * aliveMs;
|
|
101
120
|
}
|
|
121
|
+
const startTime = input.app?.startTime;
|
|
122
|
+
const windowMs = closeAt != null && startTime != null ? closeAt - startTime : 0;
|
|
123
|
+
const firstRemovals = alive.flatMap((a) => (a.removedAt == null ? [] : [{ executorId: a.executorId, timestamp: a.removedAt }]));
|
|
102
124
|
return {
|
|
103
125
|
coreHours: coresKnown ? coreMs / MS_PER_HOUR : null,
|
|
104
126
|
memoryGbHours: memoryMiB != null ? memoryMiBMs / MIB_PER_GIB / MS_PER_HOUR : null,
|
|
127
|
+
dynamicAllocation,
|
|
128
|
+
executorsPeak: computePeakConcurrentExecutorCount(alive.map((a) => ({ executorId: a.executorId, timestamp: a.start })), firstRemovals),
|
|
129
|
+
executorsMean: windowMs > 0 ? aliveMsTotal / windowMs : null,
|
|
130
|
+
executorCores: coresKnown && coreCounts.size === 1 ? [...coreCounts][0] : null,
|
|
131
|
+
executorSeconds: aliveMsTotal / MS_PER_SECOND,
|
|
105
132
|
};
|
|
106
133
|
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
// Each executor's alive interval: from its ExecutorAdded timestamp to its first later
|
|
145
|
+
// ExecutorRemoved timestamp, else the application end, else the last timestamp the log records.
|
|
146
|
+
// The one definition computeAllocation and allocatedCoreMsWithin share.
|
|
147
|
+
function aliveIntervals(input ) {
|
|
148
|
+
const removedAt = new Map ();
|
|
149
|
+
for (const e of input.executors.removed) {
|
|
150
|
+
const times = removedAt.get(e.executorId);
|
|
151
|
+
if (times) times.push(e.timestamp); else removedAt.set(e.executorId, [e.timestamp]);
|
|
152
|
+
}
|
|
153
|
+
const closeAt = input.app?.endTime ?? lastObservedTimestamp(input);
|
|
154
|
+
const configuredCores = Number.parseInt(input.app?.config?.['spark.executor.cores'] ?? '', 10);
|
|
155
|
+
const out = [];
|
|
156
|
+
const seen = new Set ();
|
|
157
|
+
for (const e of input.executors.added) {
|
|
158
|
+
if (seen.has(e.executorId)) continue; // a replayed ExecutorAdded is the same executor
|
|
159
|
+
seen.add(e.executorId);
|
|
160
|
+
const removal = (removedAt.get(e.executorId) ?? []).filter((t) => t >= e.timestamp).sort((a, b) => a - b)[0] ?? null;
|
|
161
|
+
out.push({
|
|
162
|
+
executorId: e.executorId,
|
|
163
|
+
start: e.timestamp,
|
|
164
|
+
end: Math.max(e.timestamp, removal ?? closeAt ?? e.timestamp),
|
|
165
|
+
removedAt: removal,
|
|
166
|
+
cores: e.totalCores != null && e.totalCores > 0 ? e.totalCores : Number.isFinite(configuredCores) ? configuredCores : null,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Allocated core-milliseconds: the figure behind `coreHours`, the capacity idle-core figures are
|
|
173
|
+
* measured against. Null when the log has no executor events or an executor's cores are unknown. */
|
|
174
|
+
export function allocatedCoreMs(input ) {
|
|
175
|
+
const { coreHours } = computeAllocation(input);
|
|
176
|
+
return coreHours != null && coreHours > 0 ? coreHours * MS_PER_HOUR : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The part of the allocated core-milliseconds held inside `windows` (disjoint, sorted
|
|
180
|
+
* intervals, such as the union of the stages' windows), counted from the same alive intervals as
|
|
181
|
+
* `allocatedCoreMs`. Null when `allocatedCoreMs` is. */
|
|
182
|
+
export function allocatedCoreMsWithin(input , windows ) {
|
|
183
|
+
if (allocatedCoreMs(input) == null) return null;
|
|
184
|
+
let coreMs = 0;
|
|
185
|
+
for (const { start, end, cores } of aliveIntervals(input)) {
|
|
186
|
+
for (const [from, to] of windows) {
|
|
187
|
+
coreMs += (cores ?? 0) * Math.max(0, Math.min(end, to) - Math.max(start, from));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return coreMs;
|
|
191
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
9c33274bcae34f425c7d625bdea9cfacdb400f333bc3a5ced9c86a0e75094916
|
|
@@ -9,6 +9,15 @@ import { isCut, } from './write-target-args.js';
|
|
|
9
9
|
// scans and `DeltaLogFileIndex [<path>/_delta_log/<file>, ...]`.
|
|
10
10
|
const DELTA_LOG_PATH = /((?:[A-Za-z][A-Za-z0-9+.-]*:\/{1,3}|\/)[^\s,[\]()#]+?)\/_delta_log(?=[/[\],\s)]|$)/g;
|
|
11
11
|
|
|
12
|
+
// Hadoop prints a local path as `file:/p` and a URI as `file:///p`: one table, two spellings.
|
|
13
|
+
// Both read as `file:///p` so a table is not counted twice. A `file://host/p` is left alone.
|
|
14
|
+
const LOCAL_FILE_PREFIX = /^file:(?:\/|\/\/\/)(?=[^/])/;
|
|
15
|
+
|
|
16
|
+
/** The one spelling of a table path: the two local-file forms agree, other paths pass through. */
|
|
17
|
+
export function normalizeTablePath(path ) {
|
|
18
|
+
return path.replace(LOCAL_FILE_PREFIX, 'file:///').replace(/\/+$/, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
|
|
13
22
|
|
|
14
23
|
// Every distinct table path named in the plans; `unreliable` when a plan is missing or a path cut.
|
|
@@ -26,7 +35,7 @@ function deltaLogPaths(executions ) {
|
|
|
26
35
|
if (matches.length < mentions) unreliable = true;
|
|
27
36
|
for (const m of matches) {
|
|
28
37
|
if (isCut(m[1])) unreliable = true;
|
|
29
|
-
else paths.add(m[1]
|
|
38
|
+
else paths.add(normalizeTablePath(m[1]));
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
}, { dedupe: true });
|
package/vendor-core/detectors.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { pathBasename, formatBytes, IMPACT_BAND_ORDER } from './format-utils.js';
|
|
1
|
+
import { pathBasename, formatBytes, IMPACT_BAND_ORDER, MS_PER_CORE_HOUR } from './format-utils.js';
|
|
2
2
|
import { medianOfSorted } from './median.js';
|
|
3
3
|
import { shareLabel } from './finding-presentation.js';
|
|
4
4
|
import { scanRelationId } from './plan-summary.js';
|
|
5
|
+
import { allocatedCoreMs, computeAllocation } from './allocation.js';
|
|
5
6
|
import { computePeakConcurrentCores, computePeakConcurrentExecutorCount } from './core-count.js';
|
|
6
7
|
import { walkPlanTree } from './plan-tree-walk.js';
|
|
7
8
|
import { computeCoreLocalityRatio } from './core-locality-ratio.js';
|
|
@@ -19,7 +20,7 @@ import { totalExecutorCpuMs } from './run-totals.js';
|
|
|
19
20
|
import { DUPLICATE_SUBTREE_DIFFERING_NOTE, duplicateSubtreeDetail, SLOW_HOST_DIMENSION_LABEL } from './finding-generic-recommendation.js';
|
|
20
21
|
import { stageIdsForSqlExec } from './sql-stages.js';
|
|
21
22
|
import { cyrb53 } from './string-hash.js';
|
|
22
|
-
import { decreaseConf, increaseConf, setConf } from './remediation.js';
|
|
23
|
+
import { codeFix, decreaseConf, increaseConf, setConf } from './remediation.js';
|
|
23
24
|
import { MAX_FAILURE_GROUPS, describeTaskFailure, } from './task-failure.js';
|
|
24
25
|
|
|
25
26
|
|
|
@@ -742,9 +743,9 @@ function skewFix(stage , ctx , shuffleEvidence = false
|
|
|
742
743
|
remediation: [decreaseConf('spark.sql.files.maxPartitionBytes')],
|
|
743
744
|
};
|
|
744
745
|
}
|
|
745
|
-
if (origin === 'other') return { origin, text: SKEW_KEY_REMEDY, remediation: [] };
|
|
746
|
+
if (origin === 'other') return { origin, text: SKEW_KEY_REMEDY, remediation: [codeFix(SKEW_KEY_REMEDY)] };
|
|
746
747
|
// Before Spark 3.0 there is no AQE skew-join handling, so the stage gets the generic advice.
|
|
747
|
-
if (predatesAqeSkewJoin(ctx.app)) return { origin: 'other', text: SKEW_KEY_REMEDY, remediation: [] };
|
|
748
|
+
if (predatesAqeSkewJoin(ctx.app)) return { origin: 'other', text: SKEW_KEY_REMEDY, remediation: [codeFix(SKEW_KEY_REMEDY)] };
|
|
748
749
|
return { origin, ...skewJoinFix(ctx.app) };
|
|
749
750
|
}
|
|
750
751
|
|
|
@@ -756,9 +757,11 @@ function skewJoinFix(app )
|
|
|
756
757
|
remediation: [setConf('spark.sql.adaptive.enabled', true), ...setConfUnlessLogged(app, key, true)],
|
|
757
758
|
};
|
|
758
759
|
}
|
|
759
|
-
|
|
760
|
+
const fix = switchFix(loggedAs(app, key, true), key, true,
|
|
760
761
|
`for join-driven skew, enable AQE skew-join handling (${key}); otherwise ${SKEW_KEY_REMEDY}`,
|
|
761
762
|
`AQE skew-join handling is already on, so ${SKEW_KEY_REMEDY}`);
|
|
763
|
+
// With the switch already on, only the key is left to fix: a change to the job, not a property.
|
|
764
|
+
return fix.remediation.length === 0 ? { ...fix, remediation: [codeFix(SKEW_KEY_REMEDY)] } : fix;
|
|
762
765
|
}
|
|
763
766
|
|
|
764
767
|
// The resources flag is read from the same property as the logged conf.
|
|
@@ -767,6 +770,23 @@ function dynamicAllocationFix(app , recommend , alreadyOn
|
|
|
767
770
|
return switchFix(app.resources?.dynamicAllocationEnabled === true || loggedAs(app, key, true), key, true, recommend, alreadyOn);
|
|
768
771
|
}
|
|
769
772
|
|
|
773
|
+
// Idle capacity is cured by a smaller cluster, which the run's dynamic allocation decides how to
|
|
774
|
+
// size. With it on: the cap on how many executors it scales to, and the floor it holds when that
|
|
775
|
+
// is logged above 0 (both lowered together). With it off or unset: the alternatives of a fixed
|
|
776
|
+
// executor count or switching it on, which dynamicAllocationFix already names, so the
|
|
777
|
+
// recommendation reads "either ... or" and a consumer applies one entry. Not executorIdleTimeout:
|
|
778
|
+
// autoscalingChurn recommends raising it, so lowering it here would contradict that finding.
|
|
779
|
+
function idleCapacityFix(app , recommend , alreadyOnLead ) {
|
|
780
|
+
const fix = dynamicAllocationFix(app, recommend, alreadyOnLead);
|
|
781
|
+
// dynamicAllocationFix's remediation is empty exactly when dynamic allocation is already on.
|
|
782
|
+
if (fix.remediation.length > 0) return { ...fix, remediation: [...fix.remediation, decreaseConf('spark.executor.instances')] };
|
|
783
|
+
const holdsFloor = Number.parseInt(app.config?.['spark.dynamicAllocation.minExecutors'] ?? '', 10) > 0;
|
|
784
|
+
return holdsFloor
|
|
785
|
+
? { text: `${alreadyOnLead} by lowering spark.dynamicAllocation.maxExecutors and spark.dynamicAllocation.minExecutors`,
|
|
786
|
+
remediation: [decreaseConf('spark.dynamicAllocation.maxExecutors'), decreaseConf('spark.dynamicAllocation.minExecutors')] }
|
|
787
|
+
: { text: `${alreadyOnLead} by lowering spark.dynamicAllocation.maxExecutors`, remediation: [decreaseConf('spark.dynamicAllocation.maxExecutors')] };
|
|
788
|
+
}
|
|
789
|
+
|
|
770
790
|
// The run's effective spark.sql.shuffle.partitions (logged, else Spark's 200) as a count, or null
|
|
771
791
|
// when the logged value is not a count.
|
|
772
792
|
function effectiveShufflePartitions(app ) {
|
|
@@ -1385,6 +1405,7 @@ export const DETECTORS = [
|
|
|
1385
1405
|
const stage = ctx.stages.get(finding.stageId);
|
|
1386
1406
|
if (!stage) return null;
|
|
1387
1407
|
let wasteMs = 0;
|
|
1408
|
+
let longestTaskAfterFixMs = 0;
|
|
1388
1409
|
if (finding.rule === 'maxPartitionTooBig') {
|
|
1389
1410
|
wasteMs = ((stage.shuffleReadMax ?? 0) / SHUFFLE_THROUGHPUT_BPS) * 1000;
|
|
1390
1411
|
} else if (finding.rule === 'shufflePartitionSkew') {
|
|
@@ -1400,11 +1421,21 @@ export const DETECTORS = [
|
|
|
1400
1421
|
// work, not the scheduling cost of tasks you'd add (adding tasks incurs overhead, recovers
|
|
1401
1422
|
// nothing). Model the achievable duration at target parallelism by scaling down proportionally.
|
|
1402
1423
|
wasteMs = stageDurationMs * (1 - taskCount / targetTaskCount);
|
|
1424
|
+
// The fix splits the long tasks, so the longest task is the quantity it shortens: the
|
|
1425
|
+
// occupancy clip must not floor the claim at it (TAIL_CLAIM). An even split leaves the
|
|
1426
|
+
// longest task at taskCount / targetTaskCount of today's: a modeled figure.
|
|
1427
|
+
const longestTaskMs = stage.taskDurationMax ?? 0;
|
|
1428
|
+
longestTaskAfterFixMs = longestTaskMs * taskCount / targetTaskCount;
|
|
1429
|
+
// Splitting only shortens tasks: the stage still takes the time outside its longest task
|
|
1430
|
+
// plus the longest task the split leaves, so the claim is at most the longest task's own
|
|
1431
|
+
// reduction. Without a task-duration max (older snapshots) the stage-wide scaling stands.
|
|
1432
|
+
if (longestTaskMs > 0) wasteMs = Math.min(wasteMs, longestTaskMs - longestTaskAfterFixMs);
|
|
1403
1433
|
}
|
|
1404
1434
|
} else {
|
|
1405
1435
|
return null;
|
|
1406
1436
|
}
|
|
1407
|
-
return singleStageImpact(wasteMs, finding.stageId, ctx, 'modeled', { value: wasteMs, unit: 'ms' }
|
|
1437
|
+
return singleStageImpact(wasteMs, finding.stageId, ctx, 'modeled', { value: wasteMs, unit: 'ms' },
|
|
1438
|
+
finding.rule === 'lowShuffleParallelism' ? { ...TAIL_CLAIM, longestTaskAfterFixMs } : undefined);
|
|
1408
1439
|
},
|
|
1409
1440
|
}),
|
|
1410
1441
|
defineStageDetector({
|
|
@@ -2009,12 +2040,15 @@ export const DETECTORS = [
|
|
|
2009
2040
|
if (!app || executorsAdded.length === 0 || app.startTime == null || app.endTime == null) return null;
|
|
2010
2041
|
const appDuration = app.endTime - app.startTime;
|
|
2011
2042
|
if (appDuration <= 0) return null;
|
|
2012
|
-
//
|
|
2013
|
-
//
|
|
2014
|
-
//
|
|
2043
|
+
// Capacity is what the run was allocated (cores x time alive, the figure behind
|
|
2044
|
+
// metrics.allocation.coreHours), not peak concurrent cores x the whole run: under dynamic
|
|
2045
|
+
// allocation or late-joining executors that is more than was ever held, and the idle figure
|
|
2046
|
+
// would exceed the allocation. Null without executor cores.
|
|
2047
|
+
const capacityCoreMs = allocatedCoreMs({ app, stages: ctx.stages, executors: { added: executorsAdded, removed: executorsRemoved } });
|
|
2048
|
+
if (capacityCoreMs == null) return null;
|
|
2049
|
+
// Peak concurrent cores (not computeTotalCores, a cumulative sum that double-counts a
|
|
2050
|
+
// churned-through executor against its replacement): the cluster size the finding reports.
|
|
2015
2051
|
const totalCores = computePeakConcurrentCores(app, executorsAdded, executorsRemoved);
|
|
2016
|
-
if (totalCores <= 0) return null;
|
|
2017
|
-
const capacityCoreMs = totalCores * appDuration;
|
|
2018
2052
|
// Busy core-time (from the whole-run core-time-series, same signal memoryUtilization's
|
|
2019
2053
|
// idleCores variant already uses), not executor lifetime: an executor that exists for the
|
|
2020
2054
|
// whole run but sits fully idle must not score as 100% used. Missing runAggregates (older
|
|
@@ -2025,15 +2059,12 @@ export const DETECTORS = [
|
|
|
2025
2059
|
if (utilization >= thresholds.minUtil) return null;
|
|
2026
2060
|
|
|
2027
2061
|
// CPU-time-based utilization (sparkMeasure): metric only, no threshold.
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
const cpuMs = totalExecutorCpuMs(ctx.stages.values());
|
|
2032
|
-
cpuUtilizationPct = cpuMs == null ? null : Math.round((cpuMs / (appDuration * totalCores)) * 100);
|
|
2033
|
-
}
|
|
2062
|
+
// Null when no stage recorded CPU time (older Spark), the same rule as the CLI metrics block.
|
|
2063
|
+
const cpuMs = totalExecutorCpuMs(ctx.stages.values());
|
|
2064
|
+
const cpuUtilizationPct = cpuMs == null ? null : Math.round((cpuMs / capacityCoreMs) * 100);
|
|
2034
2065
|
|
|
2035
2066
|
const value = Math.round(utilization * 100);
|
|
2036
|
-
const fix =
|
|
2067
|
+
const fix = idleCapacityFix(app, 'consider either reducing cluster size (spark.executor.instances) or enabling dynamic allocation',
|
|
2037
2068
|
'dynamic allocation is already on, so consider reducing cluster size');
|
|
2038
2069
|
return {
|
|
2039
2070
|
type: 'utilization', stageId: null, impactBand: 'info',
|
|
@@ -2041,6 +2072,7 @@ export const DETECTORS = [
|
|
|
2041
2072
|
utilizationFraction: utilization,
|
|
2042
2073
|
appDurationMs: appDuration,
|
|
2043
2074
|
totalCores,
|
|
2075
|
+
allocatedCoreMs: capacityCoreMs,
|
|
2044
2076
|
cpuUtilizationPct,
|
|
2045
2077
|
recommendation: `Average executor utilization was only ${value}%: ${fix.text}.`,
|
|
2046
2078
|
remediation: fix.remediation,
|
|
@@ -2048,13 +2080,17 @@ export const DETECTORS = [
|
|
|
2048
2080
|
},
|
|
2049
2081
|
estimate(finding) {
|
|
2050
2082
|
const fraction = finding.utilizationFraction ;
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2053
|
-
if (fraction == null || appDurationMs == null || totalCores == null) {
|
|
2083
|
+
const allocatedMs = finding.allocatedCoreMs ;
|
|
2084
|
+
if (fraction == null || allocatedMs == null) {
|
|
2054
2085
|
return costOnly('measured');
|
|
2055
2086
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2087
|
+
// The same allocated-minus-busy figure as `rawWaste`, in core-milliseconds, next to (never in)
|
|
2088
|
+
// coreTimeMs: that field is busy task time a fix removes, this is capacity no task used.
|
|
2089
|
+
const idleMs = (1 - fraction) * allocatedMs;
|
|
2090
|
+
return {
|
|
2091
|
+
...costOnly('measured', { value: idleMs / MS_PER_CORE_HOUR, unit: 'coreHours', idle: true }),
|
|
2092
|
+
idleCoreTimeMs: { low: idleMs, high: idleMs },
|
|
2093
|
+
};
|
|
2058
2094
|
},
|
|
2059
2095
|
}),
|
|
2060
2096
|
defineAppDetector({
|
|
@@ -2079,25 +2115,26 @@ export const DETECTORS = [
|
|
|
2079
2115
|
// cumulative sum or count double-counts a churned-through executor against its replacement's
|
|
2080
2116
|
// (spot preemption, dynamicAllocation replacement), inflating idle-rate and waste-model figures.
|
|
2081
2117
|
const peakExecutors = computePeakConcurrentExecutorCount(executorsAdded, executorsRemoved);
|
|
2082
|
-
const totalCores = computePeakConcurrentCores(app, executorsAdded, executorsRemoved);
|
|
2083
|
-
// Hoisted above 1a (also 1b/1c's input) so the idle-cores finding carries the allocated
|
|
2084
|
-
// memory its MB-seconds estimate needs.
|
|
2085
2118
|
const allocatedMB = app.resources?.executor?.memoryMB ?? null;
|
|
2086
2119
|
|
|
2087
2120
|
// ── 1a idle-cores rate ────────────────────────────────────────────────
|
|
2088
2121
|
const busyCoreMs = runAggregates?.busyCoreMs;
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2122
|
+
// Allocated core-time, as for `utilization`, so the two findings of one idle condition agree.
|
|
2123
|
+
const allocationInput = { app, stages, executors: { added: executorsAdded, removed: executorsRemoved } };
|
|
2124
|
+
const capacityCoreMs = allocatedCoreMs(allocationInput);
|
|
2125
|
+
if (busyCoreMs != null && capacityCoreMs != null) {
|
|
2126
|
+
const idleRate = 1 - (busyCoreMs / capacityCoreMs);
|
|
2092
2127
|
if (idleRate > thresholds.idleCoreWarn) {
|
|
2093
2128
|
const value = Math.round(idleRate * 100);
|
|
2094
|
-
const
|
|
2129
|
+
const { memoryGbHours } = computeAllocation(allocationInput);
|
|
2130
|
+
const fix = idleCapacityFix(app, 'either reduce cluster size (spark.executor.instances) or enable dynamic allocation',
|
|
2095
2131
|
'dynamic allocation is already on, so reduce cluster size');
|
|
2096
2132
|
out.push({
|
|
2097
2133
|
type: 'memoryUtilization', variant: 'idleCores', stageId: null,
|
|
2098
2134
|
impactBand: 'warning', metric: 'idleCoreRate', value,
|
|
2099
|
-
// Raw (unrounded) rate plus
|
|
2100
|
-
idleRateFraction: idleRate,
|
|
2135
|
+
// Raw (unrounded) rate plus the allocated memory-time for the impact estimator: `value` is rounded pct.
|
|
2136
|
+
idleRateFraction: idleRate,
|
|
2137
|
+
allocatedMBSeconds: memoryGbHours != null ? memoryGbHours * 1024 * 3600 : null,
|
|
2101
2138
|
recommendation: `${value}% of available core-time ran no task: ${fix.text}.`,
|
|
2102
2139
|
remediation: fix.remediation,
|
|
2103
2140
|
});
|
|
@@ -2183,13 +2220,11 @@ export const DETECTORS = [
|
|
|
2183
2220
|
return costOnly('measured', { value: finding.value, unit: 'mbSeconds' });
|
|
2184
2221
|
}
|
|
2185
2222
|
if (finding.variant === 'idleCores') {
|
|
2186
|
-
// Idle core-time priced as memory held but unused: the
|
|
2223
|
+
// Idle core-time priced as memory held but unused: the allocated memory-seconds times the idle rate.
|
|
2187
2224
|
const idleRateFraction = finding.idleRateFraction ;
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
if (idleRateFraction != null && allocatedMB != null && peakExecutors != null && appDurationMs != null) {
|
|
2192
|
-
const wastedMBSeconds = idleRateFraction * allocatedMB * peakExecutors * (appDurationMs / 1000);
|
|
2225
|
+
const allocatedMBSeconds = finding.allocatedMBSeconds ;
|
|
2226
|
+
if (idleRateFraction != null && allocatedMBSeconds != null) {
|
|
2227
|
+
const wastedMBSeconds = idleRateFraction * allocatedMBSeconds;
|
|
2193
2228
|
return costOnly('modeled', { value: wastedMBSeconds, unit: 'mbSeconds' });
|
|
2194
2229
|
}
|
|
2195
2230
|
return costOnly('modeled');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
### `MEM`: Memory utilization {#mem}
|
|
2
2
|
|
|
3
3
|
Executor memory or core capacity may be over- or under-provisioned: more
|
|
4
|
-
than 50% of
|
|
4
|
+
than 50% of allocated core time ran no task, an executor's heap peaked above
|
|
5
5
|
95% of its allocation, or it stayed below 70%. Some
|
|
6
6
|
detail here needs `spark.eventLog.logStageExecutorMetrics=true` on the run
|
|
7
7
|
being analyzed; without it, per-executor memory usage can't be broken down.
|
|
@@ -8,7 +8,8 @@ stage. The fix depends on what the stage reads, which the finding's
|
|
|
8
8
|
already has it; otherwise salt the key or repartition on a better key. A
|
|
9
9
|
stage that reads files with uneven sizes (`inputScan`) gets compaction of
|
|
10
10
|
small files or a lower `spark.sql.files.maxPartitionBytes`. Any other stage
|
|
11
|
-
(`other`) gets the salting advice and no conf
|
|
11
|
+
(`other`) gets the salting advice and no conf, as a `code` entry in
|
|
12
|
+
`remediation`; so does a join stage whose skew-join handling is already on. Flagged when P95 task time
|
|
12
13
|
(the longest task, on a stage with fewer than 20 tasks) exceeds 3x the median
|
|
13
14
|
and the recoverable tail is at least 0.5% of the run. The median is the
|
|
14
15
|
textbook one: on an even task count, the mean of the two middle values.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
### `UTIL`: Low utilization {#util}
|
|
2
2
|
|
|
3
|
-
Allocated executors sit idle for a large share of the application run
|
|
4
|
-
|
|
3
|
+
Allocated executors sit idle for a large share of the application run: under
|
|
4
|
+
60% of the core time the run allocated (cores times the time each executor was
|
|
5
|
+
alive) ran a task. Consider a smaller cluster. With dynamic allocation off,
|
|
6
|
+
either lower `spark.executor.instances` or enable dynamic allocation; the two are
|
|
7
|
+
alternatives. With it on, lower `spark.dynamicAllocation.maxExecutors`, and
|
|
8
|
+
`spark.dynamicAllocation.minExecutors` too when the run sets it above 0.
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// §5 Efficiency/wastage model. DESIGN SPIKE:
|
|
2
2
|
// decomposes available compute-hours into driver-bound vs executor-bound waste, plus two floors.
|
|
3
|
-
import {
|
|
3
|
+
import { stageWindows } from './wall-clock.js';
|
|
4
|
+
import { allocatedCoreMs, allocatedCoreMsWithin } from './allocation.js';
|
|
4
5
|
import { computePeakConcurrentCores } from './core-count.js';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
export function computeEfficiencyModel({ app, stages, executorsAdded, executorsRemoved, runAggregates }
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
|
|
@@ -18,23 +19,25 @@ export function computeEfficiencyModel({ app, stages, executorsAdded, executorsR
|
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
{
|
|
21
|
-
// Capacity is the
|
|
22
|
-
// use, so the Unused core time tile matches the verdict's
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
22
|
+
// Capacity is the run's allocation (cores x time alive, metrics.allocation.coreHours), the one the
|
|
23
|
+
// utilization and idle-cores detectors use, so the Unused core time tile matches the verdict's
|
|
24
|
+
// idle figure. Peak concurrent cores times the whole run would count cores the run never held
|
|
25
|
+
// under dynamic allocation. Null without executor cores: no capacity, so no figure.
|
|
26
|
+
const input = { app, stages, executors: { added: executorsAdded, removed: executorsRemoved } };
|
|
27
|
+
const allocatedMs = allocatedCoreMs(input);
|
|
28
|
+
// Split by where the allocated cores were held, from the same alive intervals: inside the union
|
|
29
|
+
// of the stages' windows, or outside it (startup, gaps between stages, after the last stage).
|
|
30
|
+
// Executors that join after startup hold no cores there, so spreading the allocation evenly
|
|
31
|
+
// over the run would bill the driver for cores that never existed.
|
|
32
|
+
const allocatedActiveCoreMs = allocatedMs == null ? 0 : allocatedCoreMsWithin(input, stageWindows(stages)) ?? 0;
|
|
28
33
|
|
|
29
|
-
const availableComputeHours =
|
|
34
|
+
const availableComputeHours = (allocatedMs ?? 0) / 3600000;
|
|
30
35
|
|
|
31
|
-
// Driver-bound: cores
|
|
32
|
-
const
|
|
33
|
-
const driverWasteHours = totalCores * (driverIdleMs / 3600000);
|
|
36
|
+
// Driver-bound: cores held while no stage was active (startup, gaps, idle).
|
|
37
|
+
const driverWasteHours = Math.max(0, (allocatedMs ?? 0) - allocatedActiveCoreMs) / 3600000;
|
|
34
38
|
|
|
35
39
|
// Executor-bound: allocated capacity beyond busy cores during stagesActive. Tasks only run in
|
|
36
40
|
// active windows, so whole-run busyCoreMs already lives inside stagesActive.
|
|
37
|
-
const allocatedActiveCoreMs = totalCores * wc.stagesActive;
|
|
38
41
|
const busyCoreMs = runAggregates?.busyCoreMs ?? 0;
|
|
39
42
|
const executorWasteHours = Math.max(0, allocatedActiveCoreMs - busyCoreMs) / 3600000;
|
|
40
43
|
|
|
@@ -44,6 +47,11 @@ export function computeEfficiencyModel({ app, stages, executorsAdded, executorsR
|
|
|
44
47
|
let totalTaskMs = 0;
|
|
45
48
|
const perStage = runAggregates?.perStage ?? {};
|
|
46
49
|
for (const k of Object.keys(perStage)) totalTaskMs += perStage[k].totalTaskDurationSum;
|
|
50
|
+
// The floor is the least time the work takes on the widest the cluster ever was: peak concurrent
|
|
51
|
+
// cores (not the allocation), summing every addition (computeTotalCores) would count a replaced
|
|
52
|
+
// executor's cores alongside its replacement's. `app ?? {}`: callers tolerate a null app
|
|
53
|
+
// (malformed logs); `app!` would crash on app.resources.
|
|
54
|
+
const totalCores = computePeakConcurrentCores(app ?? {}, executorsAdded, executorsRemoved);
|
|
47
55
|
const floorZeroSkewMs = totalCores > 0 ? totalTaskMs / totalCores : 0;
|
|
48
56
|
|
|
49
57
|
let dominantWaste = null;
|
|
@@ -35,7 +35,14 @@ export const shareLabel = (share ) => `${Math.round(share * 1e6)
|
|
|
35
35
|
// a generic line reads the same decision and never recommends a switch the row says is on.
|
|
36
36
|
// A finding with no remediation (older or hand-built data) keeps the property wording.
|
|
37
37
|
function switchAlreadyOn(finding , key ) {
|
|
38
|
-
return finding.remediation != null && !finding.remediation.some((r) => r.key === key);
|
|
38
|
+
return finding.remediation != null && !finding.remediation.some((r) => r.kind === 'conf' && r.key === key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The properties an idle-capacity finding lowers once dynamic allocation is on, worded as its row
|
|
42
|
+
// does: the executor cap, and the floor when the row's remediation lowers it too.
|
|
43
|
+
function idleCapacityLowering(f ) {
|
|
44
|
+
const lowersFloor = f.remediation?.some((r) => r.kind === 'conf' && r.key === 'spark.dynamicAllocation.minExecutors');
|
|
45
|
+
return ` by lowering spark.dynamicAllocation.maxExecutors${lowersFloor ? ' and spark.dynamicAllocation.minExecutors' : ''}`;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
const SKEW_JOIN_KEY = 'spark.sql.adaptive.skewJoin.enabled';
|
|
@@ -46,7 +53,7 @@ const SKEW_JOIN_AQE_OFF = 'AQE is off, so enable it (spark.sql.adaptive.enabled)
|
|
|
46
53
|
function skewJoinGeneric(f , unset ) {
|
|
47
54
|
if (f.origin === 'inputScan') return 'Uneven input files: compact small files or split large ones (lower spark.sql.files.maxPartitionBytes).';
|
|
48
55
|
if (f.origin === 'other') return 'Work is uneven across tasks: salt the key or repartition on a better key.';
|
|
49
|
-
if (f.remediation?.some((r) => r.key === 'spark.sql.adaptive.enabled')) return SKEW_JOIN_AQE_OFF;
|
|
56
|
+
if (f.remediation?.some((r) => r.kind === 'conf' && r.key === 'spark.sql.adaptive.enabled')) return SKEW_JOIN_AQE_OFF;
|
|
50
57
|
return switchAlreadyOn(f, SKEW_JOIN_KEY) ? SKEW_JOIN_ALREADY_ON : unset;
|
|
51
58
|
}
|
|
52
59
|
const DYNAMIC_ALLOCATION_KEY = 'spark.dynamicAllocation.enabled';
|
|
@@ -280,8 +287,8 @@ export const FINDING_PRESENTATION
|
|
|
280
287
|
genericRecommendation(f) {
|
|
281
288
|
switch (f.variant) {
|
|
282
289
|
case 'idleCores': return switchAlreadyOn(f, DYNAMIC_ALLOCATION_KEY)
|
|
283
|
-
?
|
|
284
|
-
: '
|
|
290
|
+
? `Dynamic allocation is already on, so reduce cluster size${idleCapacityLowering(f)}.`
|
|
291
|
+
: 'Either reduce cluster size (spark.executor.instances) or enable dynamic allocation.';
|
|
285
292
|
case 'wasteModel': return 'Review spark.executor.memory and executor count.';
|
|
286
293
|
case 'memoryBand':
|
|
287
294
|
if (f.dataUnavailable) return undefined;
|
|
@@ -298,8 +305,8 @@ export const FINDING_PRESENTATION
|
|
|
298
305
|
thresholdSummary: (t) => `average executor utilization below ${shareLabel(t.minUtil)}`,
|
|
299
306
|
actionLabel: () => 'Reduce cluster size',
|
|
300
307
|
genericRecommendation: (f) => (switchAlreadyOn(f, DYNAMIC_ALLOCATION_KEY)
|
|
301
|
-
?
|
|
302
|
-
: 'Consider reducing cluster size or enabling dynamic allocation.'),
|
|
308
|
+
? `Dynamic allocation is already on, so consider reducing cluster size${idleCapacityLowering(f)}.`
|
|
309
|
+
: 'Consider either reducing cluster size (spark.executor.instances) or enabling dynamic allocation.'),
|
|
303
310
|
},
|
|
304
311
|
coreLocality: {
|
|
305
312
|
name: 'core locality',
|