sparkforensics-server 0.5.1 → 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-oCiQwmTJ.js +1 -0
- package/public/assets/{ColdStart-B38OiAOp.js → ColdStart-BJrtjfmn.js} +1 -1
- package/public/assets/ConfigAudit-DMvKxySP.js +1 -0
- package/public/assets/CoreUsageArea-REC2zsKw.js +1 -0
- package/public/assets/Dashboard-BHZ76JCu.js +2 -0
- package/public/assets/DisclosureContext-OO7X1HAx.js +1 -0
- package/public/assets/{DuplicatePlanSubtree-BqS2A2j4.js → DuplicatePlanSubtree-BgG-L-cK.js} +1 -1
- package/public/assets/ExecutorUtilization-D2KhSVbY.js +1 -0
- package/public/assets/{GcPressure-CT1yzR_s.js → GcPressure-D1BTE8qh.js} +1 -1
- package/public/assets/{IncompleteRun-C7Qkjtjn.js → IncompleteRun-DD40xUpy.js} +1 -1
- package/public/assets/{JobFailures-WHl3ELfh.js → JobFailures-YwNzCF19.js} +1 -1
- package/public/assets/MemoryUtilization-9b8ee_3_.js +1 -0
- package/public/assets/{OverBroadcast-6oSSAQ_R.js → OverBroadcast-CfctS8a8.js} +1 -1
- package/public/assets/PartitionSizing-VG8kBgG5.js +1 -0
- package/public/assets/{PlanExplorer-DcxmiQWL.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-CRhrYNE0.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-C082c2rd.js → SmallFiles-B-kERMcq.js} +1 -1
- package/public/assets/{SpeculationWaste-BS2vpadr.js → SpeculationWaste-DaysAzXO.js} +1 -1
- package/public/assets/Spill-H44Gz4US.js +1 -0
- package/public/assets/{StageFailed-D3ac9Sou.js → StageFailed-DCfii9h_.js} +1 -1
- package/public/assets/StageFindingGroup-BXJ0Mj5n.js +1 -0
- package/public/assets/{StageHeader-pI1nTwVf.js → StageHeader--oN2J-oD.js} +1 -1
- package/public/assets/StageShape-IZ80CZiJ.js +1 -0
- package/public/assets/{StageSlowness-BaNyfNvL.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-8_5lpq_2.js → TinyTask-BssfzGFB.js} +1 -1
- package/public/assets/{UnderBroadcast-DJ0XSwth.js → UnderBroadcast-DsScrxc-.js} +1 -1
- package/public/assets/{finding-anchor-Cys8Z6Yn.js → finding-anchor-BYW1oz5R.js} +1 -1
- package/public/assets/{impact-sort-BTQndbof.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/{parser-worker-DmeCnLWU.js → parser-worker-CA5RxqKc.js} +8 -8
- package/public/assets/react-vendor-DfpuPQZf.js +9 -0
- package/public/assets/{select-DIYN6D3y.js → select-C8edjYTq.js} +1 -1
- package/public/assets/{usePagedRows-BxvdFnPk.js → usePagedRows-CsuUFeYx.js} +1 -1
- package/public/assets/useRecentFiles-BvkKo3JN.js +11 -0
- package/public/assets/{useSortMode-B8SRfAoG.js → useSortMode-joX71BZN.js} +1 -1
- package/public/docs/404.html +3 -7
- package/public/docs/assets/{app.D2GqAkhy.js → app.CnWL6bKV.js} +1 -1
- package/public/docs/assets/chunks/@localSearchIndexroot.tE5pz54z.js +1 -0
- package/public/docs/assets/chunks/{VPLocalSearchBox.BS4cSmtW.js → VPLocalSearchBox.C0IBKeRQ.js} +1 -1
- package/public/docs/assets/chunks/{theme.BuxVpb0H.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.Dp3c40rx.js +1 -0
- 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_state-and-history_run-comparison-internals.md.CZA0JLtV.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.lean.js → contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.BX1XnFbq.js +1 -0
- 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_compare-runs.md.CPowlb2q.js → user-guide_mcp-tools_compare-runs.md.Cc_gLoix.js} +3 -2
- package/public/docs/assets/{user-guide_mcp-tools_compare-runs.md.CPowlb2q.lean.js → user-guide_mcp-tools_compare-runs.md.Cc_gLoix.lean.js} +1 -1
- package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.js → user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.js} +2 -2
- package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.lean.js → user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.lean.js} +1 -1
- 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_run-comparison.md.BUALkohi.js +1 -0
- package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.lean.js +1 -0
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.js +1 -0
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.lean.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.3C7KCEZO.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.3C7KCEZO.lean.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 +6 -10
- 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 +11 -13
- 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 +8 -11
- package/public/docs/user-guide/mcp-tools/diagnose-run.html +4 -8
- package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +6 -10
- 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 +6 -10
- package/public/docs/user-guide/run-comparison.html +6 -10
- 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/cli/budgets.js +62 -2
- package/vendor-core/cli/regression-budgets.js +35 -0
- package/vendor-core/comparison-output.js +2 -1
- package/vendor-core/core-source-hash.txt +1 -1
- package/vendor-core/delta-log-paths.js +10 -1
- package/vendor-core/detectors.js +290 -92
- package/vendor-core/docs-content/detection/chrn.md +3 -1
- package/vendor-core/docs-content/detection/cold.md +2 -1
- package/vendor-core/docs-content/detection/mem.md +1 -1
- package/vendor-core/docs-content/detection/plan.md +9 -2
- package/vendor-core/docs-content/detection/shfl.md +8 -2
- package/vendor-core/docs-content/detection/skew.md +12 -5
- package/vendor-core/docs-content/detection/slow.md +6 -2
- package/vendor-core/docs-content/detection/spill.md +4 -1
- package/vendor-core/docs-content/detection/strag.md +5 -3
- package/vendor-core/docs-content/detection/tiny.md +3 -2
- package/vendor-core/docs-content/detection/util.md +6 -2
- package/vendor-core/efficiency-model.js +22 -14
- package/vendor-core/evidence-report.js +11 -11
- package/vendor-core/finding-presentation.js +52 -19
- package/vendor-core/finding-types.js +71 -16
- package/vendor-core/impact-model.js +2 -2
- package/vendor-core/mcp-server-factory.js +6 -2
- package/vendor-core/mcp-tools.js +10 -1
- package/vendor-core/median.js +8 -0
- package/vendor-core/remediation.js +10 -5
- package/vendor-core/run-comparison.js +2 -0
- package/vendor-core/run-output.js +14 -3
- package/vendor-core/sequence-alignment.js +83 -0
- package/vendor-core/stage-alignment.js +387 -63
- package/vendor-core/stage-quantiles.js +3 -1
- package/vendor-core/stage-structure.js +83 -0
- 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-5hDGIXi4.js +0 -1
- package/public/assets/CacheUtilization-CT2SCnJJ.js +0 -1
- package/public/assets/CachingOpportunity-D_O_WkbH.js +0 -1
- package/public/assets/ConfigAudit-BoplryCH.js +0 -1
- package/public/assets/CoreUsageArea-csgwNxkl.js +0 -1
- package/public/assets/Dashboard-BgKodJl2.js +0 -2
- package/public/assets/DisclosureContext-B0siMVdg.js +0 -1
- package/public/assets/ExecutorUtilization-Dp52QGWo.js +0 -1
- package/public/assets/MemoryUtilization-D8IUDHJI.js +0 -1
- package/public/assets/PartitionSizing-BwdFL-mw.js +0 -1
- package/public/assets/PlanGraphRoute-pqnHTt4i.js +0 -1
- package/public/assets/RetryWaste-BTbeHkJc.js +0 -1
- package/public/assets/Skew-DSNXRG9V.js +0 -1
- package/public/assets/SlowHost-B7Lwu2EU.js +0 -1
- package/public/assets/Spill-B4DVKHXx.js +0 -1
- package/public/assets/StageFindingGroup-D9naK109.js +0 -1
- package/public/assets/StageShape-eO5le8iG.js +0 -1
- package/public/assets/Straggler-D5Fnok_y.js +0 -1
- package/public/assets/TaskFailures-BWnC-K_G.js +0 -1
- package/public/assets/index-BcqTLKFp.css +0 -2
- package/public/assets/index-DCeWge12.js +0 -13
- package/public/assets/react-vendor-DEdfrRTJ.js +0 -9
- package/public/assets/useRecentFiles-C3Ov47U7.js +0 -11
- package/public/docs/assets/chunks/@localSearchIndexroot.D4OdkQjy.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_detector-contract_threshold-tables.md.fkPBXsU2.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.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.DpnfKz3x.js +0 -22
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.lean.js +0 -1
- package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.js +0 -1
- package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.lean.js +0 -1
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.js +0 -1
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.lean.js +0 -1
- package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.js +0 -1
- package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.lean.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.fkPBXsU2.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.Dk08VS7n.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/vendor-core/detectors.js
CHANGED
|
@@ -1,6 +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
|
+
import { medianOfSorted } from './median.js';
|
|
2
3
|
import { shareLabel } from './finding-presentation.js';
|
|
3
4
|
import { scanRelationId } from './plan-summary.js';
|
|
5
|
+
import { allocatedCoreMs, computeAllocation } from './allocation.js';
|
|
4
6
|
import { computePeakConcurrentCores, computePeakConcurrentExecutorCount } from './core-count.js';
|
|
5
7
|
import { walkPlanTree } from './plan-tree-walk.js';
|
|
6
8
|
import { computeCoreLocalityRatio } from './core-locality-ratio.js';
|
|
@@ -18,10 +20,10 @@ import { totalExecutorCpuMs } from './run-totals.js';
|
|
|
18
20
|
import { DUPLICATE_SUBTREE_DIFFERING_NOTE, duplicateSubtreeDetail, SLOW_HOST_DIMENSION_LABEL } from './finding-generic-recommendation.js';
|
|
19
21
|
import { stageIdsForSqlExec } from './sql-stages.js';
|
|
20
22
|
import { cyrb53 } from './string-hash.js';
|
|
21
|
-
import { decreaseConf, increaseConf, setConf } from './remediation.js';
|
|
23
|
+
import { codeFix, decreaseConf, increaseConf, setConf } from './remediation.js';
|
|
22
24
|
import { MAX_FAILURE_GROUPS, describeTaskFailure, } from './task-failure.js';
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
|
|
25
27
|
|
|
26
28
|
const MB = 1024 * 1024;
|
|
27
29
|
const GB = 1024 * MB;
|
|
@@ -543,7 +545,7 @@ function maxMedianRatio(
|
|
|
543
545
|
) {
|
|
544
546
|
if (samples.length < 3) return null;
|
|
545
547
|
const vals = samples.map(s => s.value).sort((a, b) => a - b);
|
|
546
|
-
const median =
|
|
548
|
+
const median = medianOfSorted(vals);
|
|
547
549
|
if (median <= 0) return null;
|
|
548
550
|
const top = samples.reduce((a, b) => (b.value > a.value ? b : a));
|
|
549
551
|
return { key: top.key, ratio: top.value / median, value: top.value };
|
|
@@ -655,14 +657,36 @@ function tailClaimFloorMs(claim , stageId , ctx )
|
|
|
655
657
|
return tailClaimImpact(claim, stageId, ctx.impact).wallClock?.high ?? claim.wasteMs;
|
|
656
658
|
}
|
|
657
659
|
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
660
|
+
// Spark's default for the properties a detector's fix reads or suggests, for a run that did not log
|
|
661
|
+
// them. Undefined when the property is not modeled or its default depends on a version the log did
|
|
662
|
+
// not record. A property is modeled only where its default has made a suggestion a no-op; every
|
|
663
|
+
// other property a detector suggests defaults off, or to a value other than the suggested one.
|
|
664
|
+
function versionDefault(app , key ) {
|
|
665
|
+
if (key === 'spark.sql.shuffle.partitions') return '200';
|
|
666
|
+
if (key === 'spark.sql.autoBroadcastJoinThreshold') return String(10 * MB);
|
|
667
|
+
const version = /^(\d+)\.(\d+)/.exec(app?.sparkVersion ?? '');
|
|
668
|
+
if (version == null) return undefined;
|
|
669
|
+
const [major, minor] = [Number(version[1]), Number(version[2])];
|
|
670
|
+
if (key === 'spark.sql.adaptive.enabled') return major > 3 || (major === 3 && minor >= 2) ? 'true' : 'false';
|
|
671
|
+
if (key === 'spark.sql.adaptive.skewJoin.enabled' || key === 'spark.sql.adaptive.coalescePartitions.enabled') {
|
|
672
|
+
return major >= 3 ? 'true' : undefined;
|
|
673
|
+
}
|
|
674
|
+
return undefined;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// The run's effective value of a property: the logged one, else versionDefault's.
|
|
678
|
+
function effectiveConf(app , key ) {
|
|
679
|
+
return (app?.config?.[key] ?? versionDefault(app, key))?.trim();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// A setting is only a fix when the run's effective conf doesn't already have it: a run that set it,
|
|
683
|
+
// or whose Spark version defaults to it, needs another remedy. Booleans compare case-insensitively,
|
|
684
|
+
// as Spark parses them.
|
|
661
685
|
function loggedAs(app , key , suggested ) {
|
|
662
|
-
const
|
|
686
|
+
const effective = effectiveConf(app, key);
|
|
663
687
|
return typeof suggested === 'boolean'
|
|
664
|
-
?
|
|
665
|
-
:
|
|
688
|
+
? effective?.toLowerCase() === String(suggested)
|
|
689
|
+
: effective === suggested;
|
|
666
690
|
}
|
|
667
691
|
|
|
668
692
|
function setConfUnlessLogged(app , key , suggested ) {
|
|
@@ -675,18 +699,69 @@ function switchFix(on , key , suggested , recomm
|
|
|
675
699
|
return on ? { text: alreadyOn, remediation: [] } : { text: recommend, remediation: [setConf(key, suggested)] };
|
|
676
700
|
}
|
|
677
701
|
|
|
702
|
+
const SKEW_KEY_REMEDY = 'salt the key or repartition on a better key';
|
|
703
|
+
|
|
704
|
+
// What a stage reads, by the dominant side: a stage that reads a shuffle and mostly input files is
|
|
705
|
+
// an input stage for any fix that sizes a shuffle's reduce side.
|
|
706
|
+
function stageReads(stage ) {
|
|
707
|
+
if (stage.shuffleReadBytes > 0 && stage.shuffleReadBytes >= stage.inputBytes) return 'shuffle';
|
|
708
|
+
return stage.inputBytes > 0 ? 'input' : 'other';
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Whether the run's Spark predates 3.0, which has no AQE skew-join handling to suggest.
|
|
712
|
+
function predatesAqeSkewJoin(app ) {
|
|
713
|
+
const major = /^(\d+)\./.exec(app?.sparkVersion ?? '');
|
|
714
|
+
return major != null && Number(major[1]) < 3;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// What a skewed stage reads, which decides whether AQE skew-join handling can act on it. It splits
|
|
718
|
+
// skewed partitions on the shuffle-read side of a sort-merge or shuffled-hash join, so only a stage
|
|
719
|
+
// that reads a shuffle, in a SQL execution whose plan has such a join, is 'shuffleJoin'. A plan does
|
|
720
|
+
// not say which stage runs which join (a node's stageIds are empty in real logs), so the join is
|
|
721
|
+
// matched per execution. 'inputScan' reads mostly files (its tasks differ in input size);
|
|
722
|
+
// 'other' is any stage the plan cannot tie to a join (an aggregation's shuffle, no plan, no input),
|
|
723
|
+
// and any stage of a run on Spark before 3.0 (skewFix).
|
|
724
|
+
function skewOrigin(stage , ctx , shuffleEvidence ) {
|
|
725
|
+
if (shuffleEvidence || (stage.shuffleReadBytes > 0 && stage.shuffleReadBytes >= stage.inputBytes)) {
|
|
726
|
+
let hasJoin = false;
|
|
727
|
+
const plan = stage.sqlExecutionId != null ? ctx.sql.get(stage.sqlExecutionId)?.planTree : null;
|
|
728
|
+
walkPlanTree(plan, (node) => { if (SKEW_JOIN_NODES.has(node.name)) hasJoin = true; });
|
|
729
|
+
return hasJoin ? 'shuffleJoin' : 'other';
|
|
730
|
+
}
|
|
731
|
+
return stage.inputBytes > 0 ? 'inputScan' : 'other';
|
|
732
|
+
}
|
|
733
|
+
const SKEW_JOIN_NODES = new Set(['SortMergeJoin', 'ShuffledHashJoin']);
|
|
734
|
+
|
|
735
|
+
// The skew finding's fix for the stage's origin. Join-driven skew gets AQE skew-join handling,
|
|
736
|
+
// unless the run's effective conf already has it; uneven input gets the file-size remedy.
|
|
737
|
+
function skewFix(stage , ctx , shuffleEvidence = false) {
|
|
738
|
+
const origin = skewOrigin(stage, ctx, shuffleEvidence);
|
|
739
|
+
if (origin === 'inputScan') {
|
|
740
|
+
return {
|
|
741
|
+
origin,
|
|
742
|
+
text: 'the stage reads uneven input files: compact small files or split large ones (lower spark.sql.files.maxPartitionBytes)',
|
|
743
|
+
remediation: [decreaseConf('spark.sql.files.maxPartitionBytes')],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
if (origin === 'other') return { origin, text: SKEW_KEY_REMEDY, remediation: [codeFix(SKEW_KEY_REMEDY)] };
|
|
747
|
+
// Before Spark 3.0 there is no AQE skew-join handling, so the stage gets the generic advice.
|
|
748
|
+
if (predatesAqeSkewJoin(ctx.app)) return { origin: 'other', text: SKEW_KEY_REMEDY, remediation: [codeFix(SKEW_KEY_REMEDY)] };
|
|
749
|
+
return { origin, ...skewJoinFix(ctx.app) };
|
|
750
|
+
}
|
|
751
|
+
|
|
678
752
|
function skewJoinFix(app ) {
|
|
679
753
|
const key = 'spark.sql.adaptive.skewJoin.enabled';
|
|
680
|
-
const remedy = 'salt the key or repartition on a better key';
|
|
681
754
|
if (loggedAs(app, 'spark.sql.adaptive.enabled', false)) {
|
|
682
755
|
return {
|
|
683
|
-
text: `AQE is off, so enable it (spark.sql.adaptive.enabled) for skew-join handling to apply; otherwise ${
|
|
756
|
+
text: `AQE is off, so enable it (spark.sql.adaptive.enabled) for skew-join handling to apply; otherwise ${SKEW_KEY_REMEDY}`,
|
|
684
757
|
remediation: [setConf('spark.sql.adaptive.enabled', true), ...setConfUnlessLogged(app, key, true)],
|
|
685
758
|
};
|
|
686
759
|
}
|
|
687
|
-
|
|
688
|
-
`for join-driven skew, enable AQE skew-join handling (${key}); otherwise ${
|
|
689
|
-
`AQE skew-join handling is already on, so ${
|
|
760
|
+
const fix = switchFix(loggedAs(app, key, true), key, true,
|
|
761
|
+
`for join-driven skew, enable AQE skew-join handling (${key}); otherwise ${SKEW_KEY_REMEDY}`,
|
|
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;
|
|
690
765
|
}
|
|
691
766
|
|
|
692
767
|
// The resources flag is read from the same property as the logged conf.
|
|
@@ -695,31 +770,92 @@ function dynamicAllocationFix(app , recommend , alreadyOn
|
|
|
695
770
|
return switchFix(app.resources?.dynamicAllocationEnabled === true || loggedAs(app, key, true), key, true, recommend, alreadyOn);
|
|
696
771
|
}
|
|
697
772
|
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
+
|
|
790
|
+
// The run's effective spark.sql.shuffle.partitions (logged, else Spark's 200) as a count, or null
|
|
791
|
+
// when the logged value is not a count.
|
|
792
|
+
function effectiveShufflePartitions(app ) {
|
|
793
|
+
const value = effectiveConf(app, 'spark.sql.shuffle.partitions');
|
|
794
|
+
return value != null && /^\d+$/.test(value) ? Number(value) : null;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Whether partition-count advice fits a shuffle-reading stage, from the effective conf and the
|
|
798
|
+
// stage's own task sizes. The partition count that brings each shuffle partition down to the ideal
|
|
799
|
+
// size is per stage; the property is job-wide. 'sufficient': the stage's tasks are already at or
|
|
800
|
+
// under the ideal size. 'aqeCoalesced': AQE merged the property's partitions into fewer tasks, so
|
|
801
|
+
// the advisory partition size is the lever. 'ownPartitioning': the property already gives at least
|
|
802
|
+
// the count needed, so the stage's own repartition(n) or RDD parallelism limits it. Otherwise the
|
|
803
|
+
// property limits the stage: 'raise'.
|
|
804
|
+
function shufflePartitionCase(stage , app ) {
|
|
805
|
+
const count = effectiveShufflePartitions(app);
|
|
806
|
+
const needed = Math.ceil(stage.shuffleReadBytes / IDEAL_BYTES_PER_PARTITION_TASK);
|
|
807
|
+
const tasks = Math.max(1, stage.taskCount);
|
|
808
|
+
let partitions = 'raise';
|
|
809
|
+
if (stage.shuffleReadBytes / tasks <= IDEAL_BYTES_PER_PARTITION_TASK) partitions = 'sufficient';
|
|
810
|
+
else if (count != null && stage.taskCount < count
|
|
811
|
+
&& loggedAs(app, 'spark.sql.adaptive.enabled', true) && loggedAs(app, 'spark.sql.adaptive.coalescePartitions.enabled', true)) partitions = 'aqeCoalesced';
|
|
812
|
+
else if (count != null && count >= needed) partitions = 'ownPartitioning';
|
|
813
|
+
return { partitions, count, needed };
|
|
702
814
|
}
|
|
703
815
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
816
|
+
const ADVISORY_PARTITION_SIZE_KEY = 'spark.sql.adaptive.advisoryPartitionSizeInBytes';
|
|
817
|
+
|
|
818
|
+
// lowShuffleParallelism's fix. Unlike the shuffle finding it fires on stages whose tasks are larger
|
|
819
|
+
// than the ideal size, so its cases are 'raise', 'aqeCoalesced' and 'ownPartitioning'.
|
|
820
|
+
function lowShuffleParallelismFix(stage , app ) {
|
|
821
|
+
const { partitions, count, needed } = shufflePartitionCase(stage, app);
|
|
822
|
+
if (partitions === 'aqeCoalesced') {
|
|
823
|
+
return {
|
|
824
|
+
partitions,
|
|
825
|
+
text: `AQE coalesced the shuffle into ${stage.taskCount} tasks (from ${count} configured partitions): lower ${ADVISORY_PARTITION_SIZE_KEY} so each partition is smaller`,
|
|
826
|
+
remediation: [decreaseConf(ADVISORY_PARTITION_SIZE_KEY)],
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
if (partitions === 'ownPartitioning') {
|
|
712
830
|
return {
|
|
713
|
-
|
|
831
|
+
partitions,
|
|
832
|
+
text: `spark.sql.shuffle.partitions is already ${count}, so raise this stage's own partition count (its repartition(n) or RDD parallelism) so each partition is smaller`,
|
|
714
833
|
remediation: [],
|
|
715
834
|
};
|
|
716
835
|
}
|
|
717
836
|
return {
|
|
837
|
+
partitions,
|
|
718
838
|
text: 'raise spark.sql.shuffle.partitions so each partition is smaller',
|
|
719
|
-
remediation: [increaseConf('spark.sql.shuffle.partitions',
|
|
839
|
+
remediation: [increaseConf('spark.sql.shuffle.partitions', count == null ? null : needed)],
|
|
720
840
|
};
|
|
721
841
|
}
|
|
722
842
|
|
|
843
|
+
// A Spark byte-size property as bytes (a plain number, or with a k/m/g/t suffix, optionally 'b'), or
|
|
844
|
+
// null when it is not one. Negative numbers pass through: -1 turns auto-broadcast off.
|
|
845
|
+
function parseSparkBytes(value ) {
|
|
846
|
+
const m = /^(-?\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i.exec(value?.trim() ?? '');
|
|
847
|
+
if (m == null) return null;
|
|
848
|
+
return m[2] === '' ? Number(m[1]) : Number(m[1]) * 1024 ** ('kmgt'.indexOf(m[2].toLowerCase()) + 1);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// The run's effective spark.sql.autoBroadcastJoinThreshold in bytes (logged, else Spark's 10 MiB);
|
|
852
|
+
// negative when auto-broadcast is disabled, null when the logged value is not a size.
|
|
853
|
+
function effectiveBroadcastThreshold(app ) {
|
|
854
|
+
const raw = effectiveConf(app, 'spark.sql.autoBroadcastJoinThreshold');
|
|
855
|
+
if (raw != null && /^-\d+$/.test(raw)) return -1;
|
|
856
|
+
return parseSparkBytes(raw);
|
|
857
|
+
}
|
|
858
|
+
|
|
723
859
|
// No dynamic-allocation property has an effect on a run whose logged conf turns it off.
|
|
724
860
|
function dynamicAllocationOff(app ) {
|
|
725
861
|
return app?.resources?.dynamicAllocationEnabled === false || app?.config?.['spark.dynamicAllocation.enabled']?.trim().toLowerCase() === 'false';
|
|
@@ -1061,9 +1197,9 @@ export const DETECTORS = [
|
|
|
1061
1197
|
const floorWasteMs = tailClaimFloorMs(skewTailClaim(stage, metric === 'P95/median'), stage.id, ctx);
|
|
1062
1198
|
if (!meetsRuntimeFloor(floorWasteMs, appDurationMs(ctx.app), thresholds.floorPctWarn)) return null;
|
|
1063
1199
|
const value = Math.round(ratio * 10) / 10;
|
|
1064
|
-
const fix =
|
|
1200
|
+
const fix = skewFix(stage, ctx);
|
|
1065
1201
|
return {
|
|
1066
|
-
type: 'skew', stageId: stage.id,
|
|
1202
|
+
type: 'skew', stageId: stage.id, origin: fix.origin,
|
|
1067
1203
|
impactBand: 'warning',
|
|
1068
1204
|
metric, value,
|
|
1069
1205
|
confidence: skewConfidence(ratio, thresholds.ratioWarn),
|
|
@@ -1188,12 +1324,21 @@ export const DETECTORS = [
|
|
|
1188
1324
|
const bytes = stage.shuffleReadBytes;
|
|
1189
1325
|
if (bytes <= thresholds.minBytes) return null;
|
|
1190
1326
|
if (stageBelowRuntimeFloor(stage, ctx, thresholds.stageFloorPct)) return null;
|
|
1327
|
+
const { partitions, count } = shufflePartitionCase(stage, ctx.app);
|
|
1328
|
+
const perTask = formatBytes(bytes / Math.max(1, stage.taskCount));
|
|
1329
|
+
const fix = partitions === 'sufficient'
|
|
1330
|
+
? { text: `its ${stage.taskCount} tasks already read about ${perTask} each, so more partitions will not help: consider a broadcast join for the smaller side`, remediation: [] }
|
|
1331
|
+
: partitions === 'aqeCoalesced'
|
|
1332
|
+
? { text: `AQE coalesced it into ${stage.taskCount} tasks (from ${count} configured partitions): consider lowering ${ADVISORY_PARTITION_SIZE_KEY} or adding a broadcast join`, remediation: [decreaseConf(ADVISORY_PARTITION_SIZE_KEY)] }
|
|
1333
|
+
: partitions === 'ownPartitioning'
|
|
1334
|
+
? { text: `spark.sql.shuffle.partitions is already ${count}, so consider raising this stage's own partition count (its repartition(n) or RDD parallelism) or adding a broadcast join`, remediation: [] }
|
|
1335
|
+
: { text: 'consider increasing spark.sql.shuffle.partitions or adding a broadcast join', remediation: [increaseConf('spark.sql.shuffle.partitions')] };
|
|
1191
1336
|
return {
|
|
1192
1337
|
type: 'shuffle', stageId: stage.id,
|
|
1193
|
-
impactBand: 'info',
|
|
1338
|
+
impactBand: 'info', partitions,
|
|
1194
1339
|
metric: 'shuffleReadBytes', value: bytes,
|
|
1195
|
-
recommendation: `${formatBytes(bytes)} shuffled in this stage:
|
|
1196
|
-
remediation:
|
|
1340
|
+
recommendation: `${formatBytes(bytes)} shuffled in this stage: ${fix.text}.`,
|
|
1341
|
+
remediation: fix.remediation,
|
|
1197
1342
|
};
|
|
1198
1343
|
},
|
|
1199
1344
|
estimate(finding, ctx) {
|
|
@@ -1225,19 +1370,19 @@ export const DETECTORS = [
|
|
|
1225
1370
|
const ratioText = p50 > 0
|
|
1226
1371
|
? `${Math.round(max / p50 * 10) / 10}× the median (${formatBytes(p50)})`
|
|
1227
1372
|
: 'far larger than the median, which is effectively empty';
|
|
1228
|
-
const fix =
|
|
1373
|
+
const fix = skewFix(stage, ctx, true);
|
|
1229
1374
|
out.push({
|
|
1230
1375
|
type: 'partitionSizing', stageId: stage.id, impactBand: 'warning',
|
|
1231
|
-
rule: 'shufflePartitionSkew', metric: 'shuffleReadMax', value: max,
|
|
1376
|
+
rule: 'shufflePartitionSkew', origin: fix.origin, metric: 'shuffleReadMax', value: max,
|
|
1232
1377
|
recommendation: `The largest shuffle partition (${formatBytes(max)}) is ${ratioText}: ${fix.text}.`,
|
|
1233
1378
|
remediation: fix.remediation,
|
|
1234
1379
|
});
|
|
1235
1380
|
}
|
|
1236
1381
|
if (total >= thresholds.lowParTotalBytes && taskCount <= thresholds.lowParMaxTasks) {
|
|
1237
|
-
const fix = lowShuffleParallelismFix(ctx.app
|
|
1382
|
+
const fix = lowShuffleParallelismFix(stage, ctx.app);
|
|
1238
1383
|
out.push({
|
|
1239
1384
|
type: 'partitionSizing', stageId: stage.id, impactBand: 'warning',
|
|
1240
|
-
rule: 'lowShuffleParallelism', metric: 'taskCount', value: taskCount,
|
|
1385
|
+
rule: 'lowShuffleParallelism', partitions: fix.partitions, metric: 'taskCount', value: taskCount,
|
|
1241
1386
|
recommendation: `${Math.round(total / GB * 10) / 10} GB of shuffle spread over only ${taskCount} tasks: ${fix.text}.`,
|
|
1242
1387
|
remediation: fix.remediation,
|
|
1243
1388
|
});
|
|
@@ -1260,6 +1405,7 @@ export const DETECTORS = [
|
|
|
1260
1405
|
const stage = ctx.stages.get(finding.stageId);
|
|
1261
1406
|
if (!stage) return null;
|
|
1262
1407
|
let wasteMs = 0;
|
|
1408
|
+
let longestTaskAfterFixMs = 0;
|
|
1263
1409
|
if (finding.rule === 'maxPartitionTooBig') {
|
|
1264
1410
|
wasteMs = ((stage.shuffleReadMax ?? 0) / SHUFFLE_THROUGHPUT_BPS) * 1000;
|
|
1265
1411
|
} else if (finding.rule === 'shufflePartitionSkew') {
|
|
@@ -1275,11 +1421,21 @@ export const DETECTORS = [
|
|
|
1275
1421
|
// work, not the scheduling cost of tasks you'd add (adding tasks incurs overhead, recovers
|
|
1276
1422
|
// nothing). Model the achievable duration at target parallelism by scaling down proportionally.
|
|
1277
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);
|
|
1278
1433
|
}
|
|
1279
1434
|
} else {
|
|
1280
1435
|
return null;
|
|
1281
1436
|
}
|
|
1282
|
-
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);
|
|
1283
1439
|
},
|
|
1284
1440
|
}),
|
|
1285
1441
|
defineStageDetector({
|
|
@@ -1296,8 +1452,11 @@ export const DETECTORS = [
|
|
|
1296
1452
|
const classified = cls === 'skew' || cls === 'volume';
|
|
1297
1453
|
const mag = computeSpillMagnitude(stage, thresholds);
|
|
1298
1454
|
const impactBand = 'warning';
|
|
1455
|
+
// shuffle.partitions sizes a shuffle's reduce side: a stage that reads no shuffle gets memory only.
|
|
1456
|
+
const reads = stageReads(stage);
|
|
1457
|
+
const partitionAdvice = reads === 'shuffle';
|
|
1299
1458
|
return {
|
|
1300
|
-
type: 'spill', stageId: stage.id, impactBand,
|
|
1459
|
+
type: 'spill', stageId: stage.id, impactBand, reads,
|
|
1301
1460
|
spillMagnitude: mag?.magnitude,
|
|
1302
1461
|
metric: 'memoryBytesSpilled', value: stage.memoryBytesSpilled,
|
|
1303
1462
|
confidence: classified ? 'medium' : 'low',
|
|
@@ -1306,8 +1465,10 @@ export const DETECTORS = [
|
|
|
1306
1465
|
: 'Spill cause could not be classified: inspect per-task spill metrics in the Spark UI before acting.',
|
|
1307
1466
|
recommendation: cls === 'skew'
|
|
1308
1467
|
? `${formatBytes(stage.memoryBytesSpilled)} spilled, skew-driven: fix task skew first; adding memory will not help.`
|
|
1309
|
-
:
|
|
1310
|
-
|
|
1468
|
+
: partitionAdvice
|
|
1469
|
+
? `${formatBytes(stage.memoryBytesSpilled)} spilled: raise spark.sql.shuffle.partitions or increase executor memory.`
|
|
1470
|
+
: `${formatBytes(stage.memoryBytesSpilled)} spilled: this stage reads no shuffle, so increase executor memory or process less data per task.`,
|
|
1471
|
+
remediation: cls === 'skew' ? [] : [...(partitionAdvice ? [increaseConf('spark.sql.shuffle.partitions')] : []), increaseConf('spark.executor.memory')],
|
|
1311
1472
|
};
|
|
1312
1473
|
},
|
|
1313
1474
|
estimate(finding, ctx) {
|
|
@@ -1419,7 +1580,7 @@ export const DETECTORS = [
|
|
|
1419
1580
|
if (hosts.length >= thresholds.minHosts) {
|
|
1420
1581
|
const means = hosts.map(h => ({ host: h.host, taskCount: h.taskCount, mean: h.totalDuration / h.taskCount }));
|
|
1421
1582
|
const sorted = [...means].map(h => h.mean).sort((a, b) => a - b);
|
|
1422
|
-
const overallMedian =
|
|
1583
|
+
const overallMedian = medianOfSorted(sorted);
|
|
1423
1584
|
if (overallMedian > 0) {
|
|
1424
1585
|
for (const h of means) {
|
|
1425
1586
|
const ratio = h.mean / overallMedian;
|
|
@@ -1532,11 +1693,21 @@ export const DETECTORS = [
|
|
|
1532
1693
|
const impactBand = durationMinutes >= t.infoMin ? 'info' : null;
|
|
1533
1694
|
if (!impactBand) return null;
|
|
1534
1695
|
const value = Math.round(durationMinutes * 10) / 10;
|
|
1696
|
+
// shuffle.partitions and default.parallelism size a shuffle's reduce side: a stage that reads
|
|
1697
|
+
// no shuffle gets the input-partitioning remedy instead.
|
|
1698
|
+
const reads = stageReads(stage);
|
|
1699
|
+
const recommendation = reads === 'shuffle'
|
|
1700
|
+
? `This stage ran ${value} minutes with no more specific cause flagged: often a partition-count problem, raise parallelism via spark.sql.shuffle.partitions or spark.default.parallelism, or check for a large per-task data volume driving heavy shuffle and spill.`
|
|
1701
|
+
: reads === 'input'
|
|
1702
|
+
? `This stage ran ${value} minutes with no more specific cause flagged and reads mostly input files: often too few or too uneven input partitions, so check input file sizes and lower spark.sql.files.maxPartitionBytes, or look for a large per-task data volume driving heavy spill.`
|
|
1703
|
+
: `This stage ran ${value} minutes with no more specific cause flagged and reads neither shuffle nor input files: check what it computes and for a large per-task data volume driving heavy spill.`;
|
|
1535
1704
|
return {
|
|
1536
1705
|
type: 'stageSlowness', stageId: stage.id, impactBand,
|
|
1537
|
-
metric: 'stageDurationMinutes', value,
|
|
1538
|
-
recommendation
|
|
1539
|
-
remediation:
|
|
1706
|
+
metric: 'stageDurationMinutes', value, reads,
|
|
1707
|
+
recommendation,
|
|
1708
|
+
remediation: reads === 'shuffle'
|
|
1709
|
+
? [increaseConf('spark.sql.shuffle.partitions'), increaseConf('spark.default.parallelism')]
|
|
1710
|
+
: reads === 'input' ? [decreaseConf('spark.sql.files.maxPartitionBytes')] : [],
|
|
1540
1711
|
};
|
|
1541
1712
|
},
|
|
1542
1713
|
estimate(finding, ctx) {
|
|
@@ -1664,8 +1835,10 @@ export const DETECTORS = [
|
|
|
1664
1835
|
const detail = useSpeculativeMetric
|
|
1665
1836
|
? `${value} speculative attempt${value === 1 ? '' : 's'} discarded`
|
|
1666
1837
|
: `${value}% of tasks straggled`;
|
|
1838
|
+
// The skew advice fits only a stage that reads a shuffle feeding a join (skewFix).
|
|
1839
|
+
const fix = skewFix(stage, ctx);
|
|
1667
1840
|
return {
|
|
1668
|
-
type: 'straggler', stageId: stage.id, impactBand,
|
|
1841
|
+
type: 'straggler', stageId: stage.id, impactBand, origin: fix.origin,
|
|
1669
1842
|
metric: useSpeculativeMetric ? 'speculativeTasks' : 'stragglerShare',
|
|
1670
1843
|
value,
|
|
1671
1844
|
unit: useSpeculativeMetric ? 'count' : 'pct',
|
|
@@ -1675,7 +1848,8 @@ export const DETECTORS = [
|
|
|
1675
1848
|
? stragglerConfidence(speculativeShare, thresholds.warnPct, thresholds.critPct)
|
|
1676
1849
|
: stragglerConfidence(stragglerShare, thresholds.shareWarn, thresholds.critPct),
|
|
1677
1850
|
validationRequired: `Warning needs at least ${shareLabel(thresholds.floorPctWarn)} of run time at stake, critical ${shareLabel(thresholds.floorPctCrit)}.`,
|
|
1678
|
-
recommendation: `${detail}: rule out a GC pause or a slow shuffle fetch before assuming a hardware issue; if
|
|
1851
|
+
recommendation: `${detail}: rule out a GC pause or a slow shuffle fetch before assuming a hardware issue; if uneven data is the cause, ${fix.text}.`,
|
|
1852
|
+
remediation: fix.remediation,
|
|
1679
1853
|
};
|
|
1680
1854
|
},
|
|
1681
1855
|
estimate(finding, ctx) {
|
|
@@ -1755,14 +1929,15 @@ export const DETECTORS = [
|
|
|
1755
1929
|
if (stageBelowRuntimeFloor(stage, ctx, thresholds.stageFloorPct)) return null;
|
|
1756
1930
|
if (stage.taskDurationP50 > thresholds.maxP50 || stage.taskDurationP95 > thresholds.maxP95) return null;
|
|
1757
1931
|
const coalesceTo = Math.max(1, Math.round(stage.taskCount / 10));
|
|
1758
|
-
const
|
|
1932
|
+
const reads = stageReads(stage);
|
|
1933
|
+
const fix = reads === 'shuffle'
|
|
1759
1934
|
? `lower spark.sql.shuffle.partitions or .coalesce(${coalesceTo})`
|
|
1760
1935
|
: `.coalesce(${coalesceTo})`;
|
|
1761
1936
|
return {
|
|
1762
|
-
type: 'tinyTask', stageId: stage.id, impactBand: 'info',
|
|
1937
|
+
type: 'tinyTask', stageId: stage.id, impactBand: 'info', reads,
|
|
1763
1938
|
metric: 'taskDurationP50', value: Math.round(stage.taskDurationP50),
|
|
1764
1939
|
recommendation: `Many small tasks (${stage.taskCount}, P50 ${Math.round(stage.taskDurationP50)}ms): scheduler overhead may dominate. Try ${fix}.`,
|
|
1765
|
-
remediation:
|
|
1940
|
+
remediation: reads === 'shuffle' ? [decreaseConf('spark.sql.shuffle.partitions')] : [],
|
|
1766
1941
|
};
|
|
1767
1942
|
},
|
|
1768
1943
|
estimate(finding, ctx) {
|
|
@@ -1839,6 +2014,7 @@ export const DETECTORS = [
|
|
|
1839
2014
|
const value = Math.round(gapSeconds);
|
|
1840
2015
|
return {
|
|
1841
2016
|
type: 'coldStart', stageId: null, impactBand: 'warning',
|
|
2017
|
+
dynamicAllocation: dynamicAllocationOff(ctx.app) ? 'off' : 'on',
|
|
1842
2018
|
metric: 'startupGapSeconds', value,
|
|
1843
2019
|
recommendation: `The first stage waited ${value}s for an executor to become available: keep a warm pool of idle executors, or if using dynamic allocation, raise the minimum/initial executor count so it doesn't scale up from zero.`,
|
|
1844
2020
|
remediation: dynamicAllocationOff(ctx.app) ? [] : [increaseConf('spark.dynamicAllocation.minExecutors'), increaseConf('spark.dynamicAllocation.initialExecutors')],
|
|
@@ -1864,12 +2040,15 @@ export const DETECTORS = [
|
|
|
1864
2040
|
if (!app || executorsAdded.length === 0 || app.startTime == null || app.endTime == null) return null;
|
|
1865
2041
|
const appDuration = app.endTime - app.startTime;
|
|
1866
2042
|
if (appDuration <= 0) return null;
|
|
1867
|
-
//
|
|
1868
|
-
//
|
|
1869
|
-
//
|
|
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.
|
|
1870
2051
|
const totalCores = computePeakConcurrentCores(app, executorsAdded, executorsRemoved);
|
|
1871
|
-
if (totalCores <= 0) return null;
|
|
1872
|
-
const capacityCoreMs = totalCores * appDuration;
|
|
1873
2052
|
// Busy core-time (from the whole-run core-time-series, same signal memoryUtilization's
|
|
1874
2053
|
// idleCores variant already uses), not executor lifetime: an executor that exists for the
|
|
1875
2054
|
// whole run but sits fully idle must not score as 100% used. Missing runAggregates (older
|
|
@@ -1880,15 +2059,12 @@ export const DETECTORS = [
|
|
|
1880
2059
|
if (utilization >= thresholds.minUtil) return null;
|
|
1881
2060
|
|
|
1882
2061
|
// CPU-time-based utilization (sparkMeasure): metric only, no threshold.
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
const cpuMs = totalExecutorCpuMs(ctx.stages.values());
|
|
1887
|
-
cpuUtilizationPct = cpuMs == null ? null : Math.round((cpuMs / (appDuration * totalCores)) * 100);
|
|
1888
|
-
}
|
|
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);
|
|
1889
2065
|
|
|
1890
2066
|
const value = Math.round(utilization * 100);
|
|
1891
|
-
const fix =
|
|
2067
|
+
const fix = idleCapacityFix(app, 'consider either reducing cluster size (spark.executor.instances) or enabling dynamic allocation',
|
|
1892
2068
|
'dynamic allocation is already on, so consider reducing cluster size');
|
|
1893
2069
|
return {
|
|
1894
2070
|
type: 'utilization', stageId: null, impactBand: 'info',
|
|
@@ -1896,6 +2072,7 @@ export const DETECTORS = [
|
|
|
1896
2072
|
utilizationFraction: utilization,
|
|
1897
2073
|
appDurationMs: appDuration,
|
|
1898
2074
|
totalCores,
|
|
2075
|
+
allocatedCoreMs: capacityCoreMs,
|
|
1899
2076
|
cpuUtilizationPct,
|
|
1900
2077
|
recommendation: `Average executor utilization was only ${value}%: ${fix.text}.`,
|
|
1901
2078
|
remediation: fix.remediation,
|
|
@@ -1903,13 +2080,17 @@ export const DETECTORS = [
|
|
|
1903
2080
|
},
|
|
1904
2081
|
estimate(finding) {
|
|
1905
2082
|
const fraction = finding.utilizationFraction ;
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
if (fraction == null || appDurationMs == null || totalCores == null) {
|
|
2083
|
+
const allocatedMs = finding.allocatedCoreMs ;
|
|
2084
|
+
if (fraction == null || allocatedMs == null) {
|
|
1909
2085
|
return costOnly('measured');
|
|
1910
2086
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
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
|
+
};
|
|
1913
2094
|
},
|
|
1914
2095
|
}),
|
|
1915
2096
|
defineAppDetector({
|
|
@@ -1934,25 +2115,26 @@ export const DETECTORS = [
|
|
|
1934
2115
|
// cumulative sum or count double-counts a churned-through executor against its replacement's
|
|
1935
2116
|
// (spot preemption, dynamicAllocation replacement), inflating idle-rate and waste-model figures.
|
|
1936
2117
|
const peakExecutors = computePeakConcurrentExecutorCount(executorsAdded, executorsRemoved);
|
|
1937
|
-
const totalCores = computePeakConcurrentCores(app, executorsAdded, executorsRemoved);
|
|
1938
|
-
// Hoisted above 1a (also 1b/1c's input) so the idle-cores finding carries the allocated
|
|
1939
|
-
// memory its MB-seconds estimate needs.
|
|
1940
2118
|
const allocatedMB = app.resources?.executor?.memoryMB ?? null;
|
|
1941
2119
|
|
|
1942
2120
|
// ── 1a idle-cores rate ────────────────────────────────────────────────
|
|
1943
2121
|
const busyCoreMs = runAggregates?.busyCoreMs;
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
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);
|
|
1947
2127
|
if (idleRate > thresholds.idleCoreWarn) {
|
|
1948
2128
|
const value = Math.round(idleRate * 100);
|
|
1949
|
-
const
|
|
2129
|
+
const { memoryGbHours } = computeAllocation(allocationInput);
|
|
2130
|
+
const fix = idleCapacityFix(app, 'either reduce cluster size (spark.executor.instances) or enable dynamic allocation',
|
|
1950
2131
|
'dynamic allocation is already on, so reduce cluster size');
|
|
1951
2132
|
out.push({
|
|
1952
2133
|
type: 'memoryUtilization', variant: 'idleCores', stageId: null,
|
|
1953
2134
|
impactBand: 'warning', metric: 'idleCoreRate', value,
|
|
1954
|
-
// Raw (unrounded) rate plus
|
|
1955
|
-
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,
|
|
1956
2138
|
recommendation: `${value}% of available core-time ran no task: ${fix.text}.`,
|
|
1957
2139
|
remediation: fix.remediation,
|
|
1958
2140
|
});
|
|
@@ -2038,13 +2220,11 @@ export const DETECTORS = [
|
|
|
2038
2220
|
return costOnly('measured', { value: finding.value, unit: 'mbSeconds' });
|
|
2039
2221
|
}
|
|
2040
2222
|
if (finding.variant === 'idleCores') {
|
|
2041
|
-
// 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.
|
|
2042
2224
|
const idleRateFraction = finding.idleRateFraction ;
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
if (idleRateFraction != null && allocatedMB != null && peakExecutors != null && appDurationMs != null) {
|
|
2047
|
-
const wastedMBSeconds = idleRateFraction * allocatedMB * peakExecutors * (appDurationMs / 1000);
|
|
2225
|
+
const allocatedMBSeconds = finding.allocatedMBSeconds ;
|
|
2226
|
+
if (idleRateFraction != null && allocatedMBSeconds != null) {
|
|
2227
|
+
const wastedMBSeconds = idleRateFraction * allocatedMBSeconds;
|
|
2048
2228
|
return costOnly('modeled', { value: wastedMBSeconds, unit: 'mbSeconds' });
|
|
2049
2229
|
}
|
|
2050
2230
|
return costOnly('modeled');
|
|
@@ -2196,14 +2376,19 @@ export const DETECTORS = [
|
|
|
2196
2376
|
if (!impactBand) return null;
|
|
2197
2377
|
|
|
2198
2378
|
const pct = Math.round(shortLivedPct * 100);
|
|
2379
|
+
const daOff = dynamicAllocationOff(app);
|
|
2199
2380
|
return {
|
|
2200
2381
|
type: 'autoscalingChurn', stageId: null, impactBand,
|
|
2201
2382
|
metric: 'shortLivedExecutorPct', value: pct,
|
|
2202
2383
|
// Raw count behind the percentage, for the impact estimator's startup-overhead figure.
|
|
2203
2384
|
shortLivedExecutorCount: shortLivedCount,
|
|
2204
2385
|
confidence: autoscalingChurnConfidence(shortLivedPct, thresholds.warningPct, thresholds.criticalPct),
|
|
2205
|
-
|
|
2206
|
-
|
|
2386
|
+
// No dynamic-allocation property has an effect when the run's conf turns it off (as for coldStart).
|
|
2387
|
+
dynamicAllocation: daOff ? 'off' : 'on',
|
|
2388
|
+
recommendation: daOff
|
|
2389
|
+
? `${pct}% of executors ran for under 2 minutes before being removed: dynamic allocation is off, so the churn comes from executors lost or preempted, not from autoscaling bounds; check the cluster manager's preemption and executor-loss logs.`
|
|
2390
|
+
: `${pct}% of executors ran for under 2 minutes before being removed: this looks like wasteful re-provisioning rather than normal scale-down; consider raising spark.dynamicAllocation.executorIdleTimeout or widening the minExecutors/maxExecutors bounds to reduce flapping.`,
|
|
2391
|
+
remediation: daOff ? [] : [
|
|
2207
2392
|
increaseConf('spark.dynamicAllocation.executorIdleTimeout'),
|
|
2208
2393
|
decreaseConf('spark.dynamicAllocation.minExecutors'),
|
|
2209
2394
|
increaseConf('spark.dynamicAllocation.maxExecutors'),
|
|
@@ -2705,14 +2890,21 @@ export const DETECTORS = [
|
|
|
2705
2890
|
|| (smaller < broadcastTiers[3] && larger > comparisonTiers[2]);
|
|
2706
2891
|
if (fires) {
|
|
2707
2892
|
const contributors = [...boundarySizeContributors(childA), ...boundarySizeContributors(childB)];
|
|
2893
|
+
// A threshold that already admits the smaller side cannot be what stopped the broadcast.
|
|
2894
|
+
const threshold = effectiveBroadcastThreshold(ctx.app);
|
|
2895
|
+
const broadcastThreshold = threshold != null && threshold < 0 ? 'disabled'
|
|
2896
|
+
: threshold != null && threshold > smaller ? 'notLimiting' : 'limits';
|
|
2897
|
+
const raiseAdvice = 'Consider a broadcast() hint or raising spark.sql.autoBroadcastJoinThreshold.';
|
|
2708
2898
|
out.push({
|
|
2709
2899
|
type: 'underBroadcast', executionId: sqlExec.id, stageIds: unionStageIds(contributors, fallbackStageIds),
|
|
2710
2900
|
// resolvePlanTree always sets id; safe downstream of it.
|
|
2711
2901
|
planNodeIds: contributors.map((n) => n.id ).filter(Boolean),
|
|
2712
2902
|
impactBand: 'info', metric: 'smallerSideBytes', value: smaller,
|
|
2713
|
-
largerSideBytes: larger,
|
|
2714
|
-
recommendation:
|
|
2715
|
-
|
|
2903
|
+
largerSideBytes: larger, broadcastThreshold,
|
|
2904
|
+
recommendation: broadcastThreshold === 'notLimiting'
|
|
2905
|
+
? `The smaller input to this Sort Merge Join (${formatBytes(smaller)}) is under the effective spark.sql.autoBroadcastJoinThreshold (${formatBytes(threshold )}) yet was not broadcast (the larger side is ${formatBytes(larger)}), so the threshold is not what stopped it: missing table statistics or a join type that cannot broadcast usually is. Consider a broadcast() hint or collecting statistics (ANALYZE TABLE).`
|
|
2906
|
+
: `The smaller input to this Sort Merge Join (${formatBytes(smaller)}) is well under the broadcast threshold relative to the larger side (${formatBytes(larger)}): this could have been a broadcast join. ${raiseAdvice}`,
|
|
2907
|
+
remediation: broadcastThreshold === 'notLimiting' ? [] : [increaseConf('spark.sql.autoBroadcastJoinThreshold')],
|
|
2716
2908
|
});
|
|
2717
2909
|
}
|
|
2718
2910
|
}
|
|
@@ -2724,17 +2916,23 @@ export const DETECTORS = [
|
|
|
2724
2916
|
// (node.stageIds always empty in real data); its child carries the executor-side
|
|
2725
2917
|
// metrics, so only the child unions in.
|
|
2726
2918
|
const child = (node.children ?? [])[0];
|
|
2727
|
-
|
|
2919
|
+
// A threshold below the broadcast cannot have admitted it, so a hint forced it.
|
|
2920
|
+
const threshold = effectiveBroadcastThreshold(ctx.app);
|
|
2921
|
+
const autoBroadcastOff = threshold != null && threshold < 0;
|
|
2922
|
+
const hintForced = autoBroadcastOff || (threshold != null && m.value > threshold);
|
|
2923
|
+
const broadcastThreshold = autoBroadcastOff ? 'disabled' : hintForced ? 'notLimiting' : 'limits';
|
|
2728
2924
|
out.push({
|
|
2729
2925
|
type: 'overBroadcast', executionId: sqlExec.id,
|
|
2730
2926
|
stageIds: unionStageIds(child ? [child] : [], fallbackStageIds),
|
|
2731
2927
|
// resolvePlanTree always sets id; safe downstream of it.
|
|
2732
2928
|
planNodeIds: [node.id ].filter(Boolean),
|
|
2733
|
-
impactBand: 'warning', metric: 'broadcastBytes', value: m.value,
|
|
2929
|
+
impactBand: 'warning', metric: 'broadcastBytes', value: m.value, broadcastThreshold,
|
|
2734
2930
|
recommendation: autoBroadcastOff
|
|
2735
2931
|
? `This broadcast (${formatBytes(m.value)}) exceeds the ${binaryThresholdLabel(overBroadcastBytes)} threshold: automatic broadcast is already disabled, so remove the broadcast() hint that forced it.`
|
|
2736
|
-
:
|
|
2737
|
-
|
|
2932
|
+
: hintForced
|
|
2933
|
+
? `This broadcast (${formatBytes(m.value)}) exceeds the ${binaryThresholdLabel(overBroadcastBytes)} threshold: spark.sql.autoBroadcastJoinThreshold (${formatBytes(threshold )}) is below it, so a broadcast() hint forced it: remove the hint.`
|
|
2934
|
+
: `This broadcast (${formatBytes(m.value)}) exceeds the ${binaryThresholdLabel(overBroadcastBytes)} threshold: check for a misapplied broadcast hint or a misconfigured spark.sql.autoBroadcastJoinThreshold.`,
|
|
2935
|
+
remediation: hintForced ? [] : [decreaseConf('spark.sql.autoBroadcastJoinThreshold')],
|
|
2738
2936
|
});
|
|
2739
2937
|
}
|
|
2740
2938
|
}
|