sparkforensics-server 0.5.1 → 0.5.2
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-5hDGIXi4.js → AutoscalingChurn-8t3U3isq.js} +1 -1
- package/public/assets/{CacheUtilization-CT2SCnJJ.js → CacheUtilization-CJs9tTum.js} +1 -1
- package/public/assets/CachingOpportunity-D1NcHs0V.js +1 -0
- package/public/assets/{ColdStart-B38OiAOp.js → ColdStart-CdRJ9gD8.js} +1 -1
- package/public/assets/{ConfigAudit-BoplryCH.js → ConfigAudit-B5Gbe5js.js} +1 -1
- package/public/assets/CoreUsageArea-a--NTRGY.js +1 -0
- package/public/assets/{Dashboard-BgKodJl2.js → Dashboard-C1hNeX49.js} +2 -2
- package/public/assets/DisclosureContext-ZpE6CkHx.js +1 -0
- package/public/assets/{DuplicatePlanSubtree-BqS2A2j4.js → DuplicatePlanSubtree-dobXdz7E.js} +1 -1
- package/public/assets/{ExecutorUtilization-Dp52QGWo.js → ExecutorUtilization-D9grwi7t.js} +1 -1
- package/public/assets/{GcPressure-CT1yzR_s.js → GcPressure-8-G2ri10.js} +1 -1
- package/public/assets/{IncompleteRun-C7Qkjtjn.js → IncompleteRun-B91lky7a.js} +1 -1
- package/public/assets/{JobFailures-WHl3ELfh.js → JobFailures-BsWvBMyJ.js} +1 -1
- package/public/assets/{MemoryUtilization-D8IUDHJI.js → MemoryUtilization-BM5schZI.js} +1 -1
- package/public/assets/{OverBroadcast-6oSSAQ_R.js → OverBroadcast-DdN8JIJH.js} +1 -1
- package/public/assets/{PartitionSizing-BwdFL-mw.js → PartitionSizing-CY5HDYtp.js} +1 -1
- package/public/assets/{PlanExplorer-DcxmiQWL.js → PlanExplorer-DgHCDBD_.js} +1 -1
- package/public/assets/PlanGraphRoute-DH3KJb8j.js +1 -0
- package/public/assets/{RetryWaste-BTbeHkJc.js → RetryWaste-DOnE9vGF.js} +1 -1
- package/public/assets/{ShuffleIO-CRhrYNE0.js → ShuffleIO-MMnDESkF.js} +1 -1
- package/public/assets/{Skew-DSNXRG9V.js → Skew-BmZZJ5ms.js} +1 -1
- package/public/assets/{SlowHost-B7Lwu2EU.js → SlowHost-CsJzfEqo.js} +1 -1
- package/public/assets/{SmallFiles-C082c2rd.js → SmallFiles-BoK3R1Ex.js} +1 -1
- package/public/assets/{SpeculationWaste-BS2vpadr.js → SpeculationWaste-CnVxB5AJ.js} +1 -1
- package/public/assets/Spill-BhNicyBy.js +1 -0
- package/public/assets/{StageFailed-D3ac9Sou.js → StageFailed-mAhHU4l_.js} +1 -1
- package/public/assets/StageFindingGroup-BEtPWcT0.js +1 -0
- package/public/assets/{StageHeader-pI1nTwVf.js → StageHeader-DJkYkrsU.js} +1 -1
- package/public/assets/{StageShape-eO5le8iG.js → StageShape-BDxRknLp.js} +1 -1
- package/public/assets/{StageSlowness-BaNyfNvL.js → StageSlowness-BPRw-otN.js} +1 -1
- package/public/assets/{Straggler-D5Fnok_y.js → Straggler-CZCbnOWP.js} +1 -1
- package/public/assets/{TaskFailures-BWnC-K_G.js → TaskFailures-C57YcLFQ.js} +1 -1
- package/public/assets/{TinyTask-8_5lpq_2.js → TinyTask-Bi84qU7n.js} +1 -1
- package/public/assets/{UnderBroadcast-DJ0XSwth.js → UnderBroadcast-DdQV7htE.js} +1 -1
- package/public/assets/{finding-anchor-Cys8Z6Yn.js → finding-anchor-Cl6_5hfV.js} +1 -1
- package/public/assets/{impact-sort-BTQndbof.js → impact-sort-CssZ6MT2.js} +1 -1
- package/public/assets/index-2h_TgUAb.css +2 -0
- package/public/assets/index-B-03mG7K.js +14 -0
- package/public/assets/{parser-worker-DmeCnLWU.js → parser-worker-CA5RxqKc.js} +8 -8
- package/public/assets/{select-DIYN6D3y.js → select-CQjNhprm.js} +1 -1
- package/public/assets/{usePagedRows-BxvdFnPk.js → usePagedRows-Dyl6T8Ok.js} +1 -1
- package/public/assets/useRecentFiles-XZEnLlal.js +11 -0
- package/public/assets/{useSortMode-B8SRfAoG.js → useSortMode-Dhvit8dt.js} +1 -1
- package/public/docs/404.html +2 -2
- package/public/docs/assets/{app.D2GqAkhy.js → app.eYTPqTeq.js} +1 -1
- package/public/docs/assets/chunks/@localSearchIndexroot.u_e5bQz4.js +1 -0
- package/public/docs/assets/chunks/{VPLocalSearchBox.BS4cSmtW.js → VPLocalSearchBox.DsZrQKQR.js} +1 -1
- package/public/docs/assets/chunks/{theme.BuxVpb0H.js → theme.DsTNE2pE.js} +2 -2
- package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.js +1 -0
- 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.DC1YqXBp.js +1 -0
- package/public/docs/assets/{user-guide_getting-started_ci-and-automation.md.DpnfKz3x.js → user-guide_getting-started_ci-and-automation.md.OUQqw3mI.js} +5 -4
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.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_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.C9YRruMT.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.lean.js +1 -0
- package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +3 -3
- package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +3 -3
- package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +3 -3
- package/public/docs/contributor-guide/architecture/board-widgets.html +3 -3
- package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +3 -3
- package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +5 -5
- package/public/docs/contributor-guide/architecture/detector-contract.html +3 -3
- package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +3 -3
- package/public/docs/contributor-guide/architecture/drill-down.html +3 -3
- package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +3 -3
- package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +3 -3
- package/public/docs/contributor-guide/architecture/impact-estimation.html +3 -3
- package/public/docs/contributor-guide/architecture/index.html +3 -3
- package/public/docs/contributor-guide/architecture/overview.html +3 -3
- package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +3 -3
- package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +5 -5
- package/public/docs/contributor-guide/architecture/state-and-history.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering.html +3 -3
- package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +5 -5
- package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +3 -3
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +3 -3
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +3 -3
- package/public/docs/contributor-guide/architecture/worker-protocol.html +3 -3
- package/public/docs/contributor-guide/contributing.html +3 -3
- package/public/docs/contributor-guide/development-setup.html +3 -3
- package/public/docs/contributor-guide/testing.html +3 -3
- package/public/docs/hashmap.json +1 -1
- package/public/docs/index.html +3 -3
- package/public/docs/tuning-reference/anti-patterns.html +3 -3
- package/public/docs/tuning-reference/aqe.html +3 -3
- package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +3 -3
- package/public/docs/tuning-reference/bottleneck-cold-start.html +3 -3
- package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +3 -3
- package/public/docs/tuning-reference/bottleneck-failures.html +3 -3
- package/public/docs/tuning-reference/bottleneck-gc.html +3 -3
- package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +3 -3
- package/public/docs/tuning-reference/bottleneck-memory-utilization.html +3 -3
- package/public/docs/tuning-reference/bottleneck-retry-waste.html +3 -3
- package/public/docs/tuning-reference/bottleneck-shuffle.html +3 -3
- package/public/docs/tuning-reference/bottleneck-skew.html +3 -3
- package/public/docs/tuning-reference/bottleneck-slow-host.html +3 -3
- package/public/docs/tuning-reference/bottleneck-small-files.html +3 -3
- package/public/docs/tuning-reference/bottleneck-spill.html +3 -3
- package/public/docs/tuning-reference/bottleneck-straggler.html +3 -3
- package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +3 -3
- package/public/docs/tuning-reference/bottleneck-utilization.html +3 -3
- package/public/docs/tuning-reference/caching.html +3 -3
- package/public/docs/tuning-reference/cluster-config.html +3 -3
- package/public/docs/tuning-reference/config.html +3 -3
- package/public/docs/tuning-reference/data-formats.html +3 -3
- package/public/docs/tuning-reference/index.html +3 -3
- package/public/docs/tuning-reference/intro.html +3 -3
- package/public/docs/tuning-reference/joins.html +3 -3
- package/public/docs/tuning-reference/memory-model.html +3 -3
- package/public/docs/tuning-reference/metrics.html +3 -3
- package/public/docs/tuning-reference/partitioning.html +3 -3
- package/public/docs/tuning-reference/pyspark.html +3 -3
- package/public/docs/tuning-reference/shuffle.html +3 -3
- package/public/docs/tuning-reference/spark-architecture.html +3 -3
- package/public/docs/tuning-reference/table-formats.html +3 -3
- package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +3 -3
- package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +3 -3
- package/public/docs/user-guide/alternative-log-retrieval.html +3 -3
- package/public/docs/user-guide/getting-started/ci-and-automation.html +8 -7
- package/public/docs/user-guide/getting-started/reading-the-dashboard.html +3 -3
- package/public/docs/user-guide/getting-started/regression-metric-keys.html +3 -3
- package/public/docs/user-guide/getting-started/tuning-thresholds.html +3 -3
- package/public/docs/user-guide/getting-started.html +3 -3
- package/public/docs/user-guide/mcp-tools/compare-runs.html +7 -6
- package/public/docs/user-guide/mcp-tools/diagnose-run.html +3 -3
- package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +5 -5
- package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +3 -3
- package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +3 -3
- package/public/docs/user-guide/mcp-tools/get-reference-doc.html +3 -3
- package/public/docs/user-guide/mcp-tools/get-run-summary.html +3 -3
- package/public/docs/user-guide/mcp-tools/list-runs.html +3 -3
- package/public/docs/user-guide/mcp-tools.html +3 -3
- package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +5 -5
- package/public/docs/user-guide/run-comparison.html +5 -5
- package/public/docs/user-guide/understanding-findings.html +5 -5
- package/public/export-template.html +3 -3
- package/public/index.html +3 -3
- 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/detectors.js +219 -56
- 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/plan.md +9 -2
- package/vendor-core/docs-content/detection/shfl.md +8 -2
- package/vendor-core/docs-content/detection/skew.md +11 -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/evidence-report.js +11 -11
- package/vendor-core/finding-presentation.js +39 -13
- package/vendor-core/finding-types.js +50 -6
- 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/run-comparison.js +2 -0
- 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/public/assets/CachingOpportunity-D_O_WkbH.js +0 -1
- package/public/assets/CoreUsageArea-csgwNxkl.js +0 -1
- package/public/assets/DisclosureContext-B0siMVdg.js +0 -1
- package/public/assets/PlanGraphRoute-pqnHTt4i.js +0 -1
- package/public/assets/Spill-B4DVKHXx.js +0 -1
- package/public/assets/StageFindingGroup-D9naK109.js +0 -1
- package/public/assets/index-BcqTLKFp.css +0 -2
- package/public/assets/index-DCeWge12.js +0 -13
- 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_detector-contract_threshold-tables.md.fkPBXsU2.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/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_detector-contract_threshold-tables.md.fkPBXsU2.lean.js → contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.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.DC1YqXBp.lean.js} +0 -0
package/vendor-core/detectors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pathBasename, formatBytes, IMPACT_BAND_ORDER } 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';
|
|
4
5
|
import { computePeakConcurrentCores, computePeakConcurrentExecutorCount } from './core-count.js';
|
|
@@ -21,7 +22,7 @@ import { cyrb53 } from './string-hash.js';
|
|
|
21
22
|
import { decreaseConf, increaseConf, setConf } from './remediation.js';
|
|
22
23
|
import { MAX_FAILURE_GROUPS, describeTaskFailure, } from './task-failure.js';
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
|
|
25
26
|
|
|
26
27
|
const MB = 1024 * 1024;
|
|
27
28
|
const GB = 1024 * MB;
|
|
@@ -543,7 +544,7 @@ function maxMedianRatio(
|
|
|
543
544
|
) {
|
|
544
545
|
if (samples.length < 3) return null;
|
|
545
546
|
const vals = samples.map(s => s.value).sort((a, b) => a - b);
|
|
546
|
-
const median =
|
|
547
|
+
const median = medianOfSorted(vals);
|
|
547
548
|
if (median <= 0) return null;
|
|
548
549
|
const top = samples.reduce((a, b) => (b.value > a.value ? b : a));
|
|
549
550
|
return { key: top.key, ratio: top.value / median, value: top.value };
|
|
@@ -655,14 +656,36 @@ function tailClaimFloorMs(claim , stageId , ctx )
|
|
|
655
656
|
return tailClaimImpact(claim, stageId, ctx.impact).wallClock?.high ?? claim.wasteMs;
|
|
656
657
|
}
|
|
657
658
|
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
659
|
+
// Spark's default for the properties a detector's fix reads or suggests, for a run that did not log
|
|
660
|
+
// them. Undefined when the property is not modeled or its default depends on a version the log did
|
|
661
|
+
// not record. A property is modeled only where its default has made a suggestion a no-op; every
|
|
662
|
+
// other property a detector suggests defaults off, or to a value other than the suggested one.
|
|
663
|
+
function versionDefault(app , key ) {
|
|
664
|
+
if (key === 'spark.sql.shuffle.partitions') return '200';
|
|
665
|
+
if (key === 'spark.sql.autoBroadcastJoinThreshold') return String(10 * MB);
|
|
666
|
+
const version = /^(\d+)\.(\d+)/.exec(app?.sparkVersion ?? '');
|
|
667
|
+
if (version == null) return undefined;
|
|
668
|
+
const [major, minor] = [Number(version[1]), Number(version[2])];
|
|
669
|
+
if (key === 'spark.sql.adaptive.enabled') return major > 3 || (major === 3 && minor >= 2) ? 'true' : 'false';
|
|
670
|
+
if (key === 'spark.sql.adaptive.skewJoin.enabled' || key === 'spark.sql.adaptive.coalescePartitions.enabled') {
|
|
671
|
+
return major >= 3 ? 'true' : undefined;
|
|
672
|
+
}
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// The run's effective value of a property: the logged one, else versionDefault's.
|
|
677
|
+
function effectiveConf(app , key ) {
|
|
678
|
+
return (app?.config?.[key] ?? versionDefault(app, key))?.trim();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// A setting is only a fix when the run's effective conf doesn't already have it: a run that set it,
|
|
682
|
+
// or whose Spark version defaults to it, needs another remedy. Booleans compare case-insensitively,
|
|
683
|
+
// as Spark parses them.
|
|
661
684
|
function loggedAs(app , key , suggested ) {
|
|
662
|
-
const
|
|
685
|
+
const effective = effectiveConf(app, key);
|
|
663
686
|
return typeof suggested === 'boolean'
|
|
664
|
-
?
|
|
665
|
-
:
|
|
687
|
+
? effective?.toLowerCase() === String(suggested)
|
|
688
|
+
: effective === suggested;
|
|
666
689
|
}
|
|
667
690
|
|
|
668
691
|
function setConfUnlessLogged(app , key , suggested ) {
|
|
@@ -675,18 +698,67 @@ function switchFix(on , key , suggested , recomm
|
|
|
675
698
|
return on ? { text: alreadyOn, remediation: [] } : { text: recommend, remediation: [setConf(key, suggested)] };
|
|
676
699
|
}
|
|
677
700
|
|
|
701
|
+
const SKEW_KEY_REMEDY = 'salt the key or repartition on a better key';
|
|
702
|
+
|
|
703
|
+
// What a stage reads, by the dominant side: a stage that reads a shuffle and mostly input files is
|
|
704
|
+
// an input stage for any fix that sizes a shuffle's reduce side.
|
|
705
|
+
function stageReads(stage ) {
|
|
706
|
+
if (stage.shuffleReadBytes > 0 && stage.shuffleReadBytes >= stage.inputBytes) return 'shuffle';
|
|
707
|
+
return stage.inputBytes > 0 ? 'input' : 'other';
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Whether the run's Spark predates 3.0, which has no AQE skew-join handling to suggest.
|
|
711
|
+
function predatesAqeSkewJoin(app ) {
|
|
712
|
+
const major = /^(\d+)\./.exec(app?.sparkVersion ?? '');
|
|
713
|
+
return major != null && Number(major[1]) < 3;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// What a skewed stage reads, which decides whether AQE skew-join handling can act on it. It splits
|
|
717
|
+
// skewed partitions on the shuffle-read side of a sort-merge or shuffled-hash join, so only a stage
|
|
718
|
+
// that reads a shuffle, in a SQL execution whose plan has such a join, is 'shuffleJoin'. A plan does
|
|
719
|
+
// not say which stage runs which join (a node's stageIds are empty in real logs), so the join is
|
|
720
|
+
// matched per execution. 'inputScan' reads mostly files (its tasks differ in input size);
|
|
721
|
+
// 'other' is any stage the plan cannot tie to a join (an aggregation's shuffle, no plan, no input),
|
|
722
|
+
// and any stage of a run on Spark before 3.0 (skewFix).
|
|
723
|
+
function skewOrigin(stage , ctx , shuffleEvidence ) {
|
|
724
|
+
if (shuffleEvidence || (stage.shuffleReadBytes > 0 && stage.shuffleReadBytes >= stage.inputBytes)) {
|
|
725
|
+
let hasJoin = false;
|
|
726
|
+
const plan = stage.sqlExecutionId != null ? ctx.sql.get(stage.sqlExecutionId)?.planTree : null;
|
|
727
|
+
walkPlanTree(plan, (node) => { if (SKEW_JOIN_NODES.has(node.name)) hasJoin = true; });
|
|
728
|
+
return hasJoin ? 'shuffleJoin' : 'other';
|
|
729
|
+
}
|
|
730
|
+
return stage.inputBytes > 0 ? 'inputScan' : 'other';
|
|
731
|
+
}
|
|
732
|
+
const SKEW_JOIN_NODES = new Set(['SortMergeJoin', 'ShuffledHashJoin']);
|
|
733
|
+
|
|
734
|
+
// The skew finding's fix for the stage's origin. Join-driven skew gets AQE skew-join handling,
|
|
735
|
+
// unless the run's effective conf already has it; uneven input gets the file-size remedy.
|
|
736
|
+
function skewFix(stage , ctx , shuffleEvidence = false) {
|
|
737
|
+
const origin = skewOrigin(stage, ctx, shuffleEvidence);
|
|
738
|
+
if (origin === 'inputScan') {
|
|
739
|
+
return {
|
|
740
|
+
origin,
|
|
741
|
+
text: 'the stage reads uneven input files: compact small files or split large ones (lower spark.sql.files.maxPartitionBytes)',
|
|
742
|
+
remediation: [decreaseConf('spark.sql.files.maxPartitionBytes')],
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
if (origin === 'other') return { origin, text: SKEW_KEY_REMEDY, remediation: [] };
|
|
746
|
+
// 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
|
+
return { origin, ...skewJoinFix(ctx.app) };
|
|
749
|
+
}
|
|
750
|
+
|
|
678
751
|
function skewJoinFix(app ) {
|
|
679
752
|
const key = 'spark.sql.adaptive.skewJoin.enabled';
|
|
680
|
-
const remedy = 'salt the key or repartition on a better key';
|
|
681
753
|
if (loggedAs(app, 'spark.sql.adaptive.enabled', false)) {
|
|
682
754
|
return {
|
|
683
|
-
text: `AQE is off, so enable it (spark.sql.adaptive.enabled) for skew-join handling to apply; otherwise ${
|
|
755
|
+
text: `AQE is off, so enable it (spark.sql.adaptive.enabled) for skew-join handling to apply; otherwise ${SKEW_KEY_REMEDY}`,
|
|
684
756
|
remediation: [setConf('spark.sql.adaptive.enabled', true), ...setConfUnlessLogged(app, key, true)],
|
|
685
757
|
};
|
|
686
758
|
}
|
|
687
759
|
return switchFix(loggedAs(app, key, true), key, true,
|
|
688
|
-
`for join-driven skew, enable AQE skew-join handling (${key}); otherwise ${
|
|
689
|
-
`AQE skew-join handling is already on, so ${
|
|
760
|
+
`for join-driven skew, enable AQE skew-join handling (${key}); otherwise ${SKEW_KEY_REMEDY}`,
|
|
761
|
+
`AQE skew-join handling is already on, so ${SKEW_KEY_REMEDY}`);
|
|
690
762
|
}
|
|
691
763
|
|
|
692
764
|
// The resources flag is read from the same property as the logged conf.
|
|
@@ -695,31 +767,75 @@ function dynamicAllocationFix(app , recommend , alreadyOn
|
|
|
695
767
|
return switchFix(app.resources?.dynamicAllocationEnabled === true || loggedAs(app, key, true), key, true, recommend, alreadyOn);
|
|
696
768
|
}
|
|
697
769
|
|
|
698
|
-
// The run's
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
770
|
+
// The run's effective spark.sql.shuffle.partitions (logged, else Spark's 200) as a count, or null
|
|
771
|
+
// when the logged value is not a count.
|
|
772
|
+
function effectiveShufflePartitions(app ) {
|
|
773
|
+
const value = effectiveConf(app, 'spark.sql.shuffle.partitions');
|
|
774
|
+
return value != null && /^\d+$/.test(value) ? Number(value) : null;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Whether partition-count advice fits a shuffle-reading stage, from the effective conf and the
|
|
778
|
+
// stage's own task sizes. The partition count that brings each shuffle partition down to the ideal
|
|
779
|
+
// size is per stage; the property is job-wide. 'sufficient': the stage's tasks are already at or
|
|
780
|
+
// under the ideal size. 'aqeCoalesced': AQE merged the property's partitions into fewer tasks, so
|
|
781
|
+
// the advisory partition size is the lever. 'ownPartitioning': the property already gives at least
|
|
782
|
+
// the count needed, so the stage's own repartition(n) or RDD parallelism limits it. Otherwise the
|
|
783
|
+
// property limits the stage: 'raise'.
|
|
784
|
+
function shufflePartitionCase(stage , app ) {
|
|
785
|
+
const count = effectiveShufflePartitions(app);
|
|
786
|
+
const needed = Math.ceil(stage.shuffleReadBytes / IDEAL_BYTES_PER_PARTITION_TASK);
|
|
787
|
+
const tasks = Math.max(1, stage.taskCount);
|
|
788
|
+
let partitions = 'raise';
|
|
789
|
+
if (stage.shuffleReadBytes / tasks <= IDEAL_BYTES_PER_PARTITION_TASK) partitions = 'sufficient';
|
|
790
|
+
else if (count != null && stage.taskCount < count
|
|
791
|
+
&& loggedAs(app, 'spark.sql.adaptive.enabled', true) && loggedAs(app, 'spark.sql.adaptive.coalescePartitions.enabled', true)) partitions = 'aqeCoalesced';
|
|
792
|
+
else if (count != null && count >= needed) partitions = 'ownPartitioning';
|
|
793
|
+
return { partitions, count, needed };
|
|
702
794
|
}
|
|
703
795
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (logged != null && logged >= needed) {
|
|
796
|
+
const ADVISORY_PARTITION_SIZE_KEY = 'spark.sql.adaptive.advisoryPartitionSizeInBytes';
|
|
797
|
+
|
|
798
|
+
// lowShuffleParallelism's fix. Unlike the shuffle finding it fires on stages whose tasks are larger
|
|
799
|
+
// than the ideal size, so its cases are 'raise', 'aqeCoalesced' and 'ownPartitioning'.
|
|
800
|
+
function lowShuffleParallelismFix(stage , app ) {
|
|
801
|
+
const { partitions, count, needed } = shufflePartitionCase(stage, app);
|
|
802
|
+
if (partitions === 'aqeCoalesced') {
|
|
712
803
|
return {
|
|
713
|
-
|
|
804
|
+
partitions,
|
|
805
|
+
text: `AQE coalesced the shuffle into ${stage.taskCount} tasks (from ${count} configured partitions): lower ${ADVISORY_PARTITION_SIZE_KEY} so each partition is smaller`,
|
|
806
|
+
remediation: [decreaseConf(ADVISORY_PARTITION_SIZE_KEY)],
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
if (partitions === 'ownPartitioning') {
|
|
810
|
+
return {
|
|
811
|
+
partitions,
|
|
812
|
+
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
813
|
remediation: [],
|
|
715
814
|
};
|
|
716
815
|
}
|
|
717
816
|
return {
|
|
817
|
+
partitions,
|
|
718
818
|
text: 'raise spark.sql.shuffle.partitions so each partition is smaller',
|
|
719
|
-
remediation: [increaseConf('spark.sql.shuffle.partitions',
|
|
819
|
+
remediation: [increaseConf('spark.sql.shuffle.partitions', count == null ? null : needed)],
|
|
720
820
|
};
|
|
721
821
|
}
|
|
722
822
|
|
|
823
|
+
// A Spark byte-size property as bytes (a plain number, or with a k/m/g/t suffix, optionally 'b'), or
|
|
824
|
+
// null when it is not one. Negative numbers pass through: -1 turns auto-broadcast off.
|
|
825
|
+
function parseSparkBytes(value ) {
|
|
826
|
+
const m = /^(-?\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i.exec(value?.trim() ?? '');
|
|
827
|
+
if (m == null) return null;
|
|
828
|
+
return m[2] === '' ? Number(m[1]) : Number(m[1]) * 1024 ** ('kmgt'.indexOf(m[2].toLowerCase()) + 1);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// The run's effective spark.sql.autoBroadcastJoinThreshold in bytes (logged, else Spark's 10 MiB);
|
|
832
|
+
// negative when auto-broadcast is disabled, null when the logged value is not a size.
|
|
833
|
+
function effectiveBroadcastThreshold(app ) {
|
|
834
|
+
const raw = effectiveConf(app, 'spark.sql.autoBroadcastJoinThreshold');
|
|
835
|
+
if (raw != null && /^-\d+$/.test(raw)) return -1;
|
|
836
|
+
return parseSparkBytes(raw);
|
|
837
|
+
}
|
|
838
|
+
|
|
723
839
|
// No dynamic-allocation property has an effect on a run whose logged conf turns it off.
|
|
724
840
|
function dynamicAllocationOff(app ) {
|
|
725
841
|
return app?.resources?.dynamicAllocationEnabled === false || app?.config?.['spark.dynamicAllocation.enabled']?.trim().toLowerCase() === 'false';
|
|
@@ -1061,9 +1177,9 @@ export const DETECTORS = [
|
|
|
1061
1177
|
const floorWasteMs = tailClaimFloorMs(skewTailClaim(stage, metric === 'P95/median'), stage.id, ctx);
|
|
1062
1178
|
if (!meetsRuntimeFloor(floorWasteMs, appDurationMs(ctx.app), thresholds.floorPctWarn)) return null;
|
|
1063
1179
|
const value = Math.round(ratio * 10) / 10;
|
|
1064
|
-
const fix =
|
|
1180
|
+
const fix = skewFix(stage, ctx);
|
|
1065
1181
|
return {
|
|
1066
|
-
type: 'skew', stageId: stage.id,
|
|
1182
|
+
type: 'skew', stageId: stage.id, origin: fix.origin,
|
|
1067
1183
|
impactBand: 'warning',
|
|
1068
1184
|
metric, value,
|
|
1069
1185
|
confidence: skewConfidence(ratio, thresholds.ratioWarn),
|
|
@@ -1188,12 +1304,21 @@ export const DETECTORS = [
|
|
|
1188
1304
|
const bytes = stage.shuffleReadBytes;
|
|
1189
1305
|
if (bytes <= thresholds.minBytes) return null;
|
|
1190
1306
|
if (stageBelowRuntimeFloor(stage, ctx, thresholds.stageFloorPct)) return null;
|
|
1307
|
+
const { partitions, count } = shufflePartitionCase(stage, ctx.app);
|
|
1308
|
+
const perTask = formatBytes(bytes / Math.max(1, stage.taskCount));
|
|
1309
|
+
const fix = partitions === 'sufficient'
|
|
1310
|
+
? { 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: [] }
|
|
1311
|
+
: partitions === 'aqeCoalesced'
|
|
1312
|
+
? { 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)] }
|
|
1313
|
+
: partitions === 'ownPartitioning'
|
|
1314
|
+
? { 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: [] }
|
|
1315
|
+
: { text: 'consider increasing spark.sql.shuffle.partitions or adding a broadcast join', remediation: [increaseConf('spark.sql.shuffle.partitions')] };
|
|
1191
1316
|
return {
|
|
1192
1317
|
type: 'shuffle', stageId: stage.id,
|
|
1193
|
-
impactBand: 'info',
|
|
1318
|
+
impactBand: 'info', partitions,
|
|
1194
1319
|
metric: 'shuffleReadBytes', value: bytes,
|
|
1195
|
-
recommendation: `${formatBytes(bytes)} shuffled in this stage:
|
|
1196
|
-
remediation:
|
|
1320
|
+
recommendation: `${formatBytes(bytes)} shuffled in this stage: ${fix.text}.`,
|
|
1321
|
+
remediation: fix.remediation,
|
|
1197
1322
|
};
|
|
1198
1323
|
},
|
|
1199
1324
|
estimate(finding, ctx) {
|
|
@@ -1225,19 +1350,19 @@ export const DETECTORS = [
|
|
|
1225
1350
|
const ratioText = p50 > 0
|
|
1226
1351
|
? `${Math.round(max / p50 * 10) / 10}× the median (${formatBytes(p50)})`
|
|
1227
1352
|
: 'far larger than the median, which is effectively empty';
|
|
1228
|
-
const fix =
|
|
1353
|
+
const fix = skewFix(stage, ctx, true);
|
|
1229
1354
|
out.push({
|
|
1230
1355
|
type: 'partitionSizing', stageId: stage.id, impactBand: 'warning',
|
|
1231
|
-
rule: 'shufflePartitionSkew', metric: 'shuffleReadMax', value: max,
|
|
1356
|
+
rule: 'shufflePartitionSkew', origin: fix.origin, metric: 'shuffleReadMax', value: max,
|
|
1232
1357
|
recommendation: `The largest shuffle partition (${formatBytes(max)}) is ${ratioText}: ${fix.text}.`,
|
|
1233
1358
|
remediation: fix.remediation,
|
|
1234
1359
|
});
|
|
1235
1360
|
}
|
|
1236
1361
|
if (total >= thresholds.lowParTotalBytes && taskCount <= thresholds.lowParMaxTasks) {
|
|
1237
|
-
const fix = lowShuffleParallelismFix(ctx.app
|
|
1362
|
+
const fix = lowShuffleParallelismFix(stage, ctx.app);
|
|
1238
1363
|
out.push({
|
|
1239
1364
|
type: 'partitionSizing', stageId: stage.id, impactBand: 'warning',
|
|
1240
|
-
rule: 'lowShuffleParallelism', metric: 'taskCount', value: taskCount,
|
|
1365
|
+
rule: 'lowShuffleParallelism', partitions: fix.partitions, metric: 'taskCount', value: taskCount,
|
|
1241
1366
|
recommendation: `${Math.round(total / GB * 10) / 10} GB of shuffle spread over only ${taskCount} tasks: ${fix.text}.`,
|
|
1242
1367
|
remediation: fix.remediation,
|
|
1243
1368
|
});
|
|
@@ -1296,8 +1421,11 @@ export const DETECTORS = [
|
|
|
1296
1421
|
const classified = cls === 'skew' || cls === 'volume';
|
|
1297
1422
|
const mag = computeSpillMagnitude(stage, thresholds);
|
|
1298
1423
|
const impactBand = 'warning';
|
|
1424
|
+
// shuffle.partitions sizes a shuffle's reduce side: a stage that reads no shuffle gets memory only.
|
|
1425
|
+
const reads = stageReads(stage);
|
|
1426
|
+
const partitionAdvice = reads === 'shuffle';
|
|
1299
1427
|
return {
|
|
1300
|
-
type: 'spill', stageId: stage.id, impactBand,
|
|
1428
|
+
type: 'spill', stageId: stage.id, impactBand, reads,
|
|
1301
1429
|
spillMagnitude: mag?.magnitude,
|
|
1302
1430
|
metric: 'memoryBytesSpilled', value: stage.memoryBytesSpilled,
|
|
1303
1431
|
confidence: classified ? 'medium' : 'low',
|
|
@@ -1306,8 +1434,10 @@ export const DETECTORS = [
|
|
|
1306
1434
|
: 'Spill cause could not be classified: inspect per-task spill metrics in the Spark UI before acting.',
|
|
1307
1435
|
recommendation: cls === 'skew'
|
|
1308
1436
|
? `${formatBytes(stage.memoryBytesSpilled)} spilled, skew-driven: fix task skew first; adding memory will not help.`
|
|
1309
|
-
:
|
|
1310
|
-
|
|
1437
|
+
: partitionAdvice
|
|
1438
|
+
? `${formatBytes(stage.memoryBytesSpilled)} spilled: raise spark.sql.shuffle.partitions or increase executor memory.`
|
|
1439
|
+
: `${formatBytes(stage.memoryBytesSpilled)} spilled: this stage reads no shuffle, so increase executor memory or process less data per task.`,
|
|
1440
|
+
remediation: cls === 'skew' ? [] : [...(partitionAdvice ? [increaseConf('spark.sql.shuffle.partitions')] : []), increaseConf('spark.executor.memory')],
|
|
1311
1441
|
};
|
|
1312
1442
|
},
|
|
1313
1443
|
estimate(finding, ctx) {
|
|
@@ -1419,7 +1549,7 @@ export const DETECTORS = [
|
|
|
1419
1549
|
if (hosts.length >= thresholds.minHosts) {
|
|
1420
1550
|
const means = hosts.map(h => ({ host: h.host, taskCount: h.taskCount, mean: h.totalDuration / h.taskCount }));
|
|
1421
1551
|
const sorted = [...means].map(h => h.mean).sort((a, b) => a - b);
|
|
1422
|
-
const overallMedian =
|
|
1552
|
+
const overallMedian = medianOfSorted(sorted);
|
|
1423
1553
|
if (overallMedian > 0) {
|
|
1424
1554
|
for (const h of means) {
|
|
1425
1555
|
const ratio = h.mean / overallMedian;
|
|
@@ -1532,11 +1662,21 @@ export const DETECTORS = [
|
|
|
1532
1662
|
const impactBand = durationMinutes >= t.infoMin ? 'info' : null;
|
|
1533
1663
|
if (!impactBand) return null;
|
|
1534
1664
|
const value = Math.round(durationMinutes * 10) / 10;
|
|
1665
|
+
// shuffle.partitions and default.parallelism size a shuffle's reduce side: a stage that reads
|
|
1666
|
+
// no shuffle gets the input-partitioning remedy instead.
|
|
1667
|
+
const reads = stageReads(stage);
|
|
1668
|
+
const recommendation = reads === 'shuffle'
|
|
1669
|
+
? `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.`
|
|
1670
|
+
: reads === 'input'
|
|
1671
|
+
? `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.`
|
|
1672
|
+
: `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
1673
|
return {
|
|
1536
1674
|
type: 'stageSlowness', stageId: stage.id, impactBand,
|
|
1537
|
-
metric: 'stageDurationMinutes', value,
|
|
1538
|
-
recommendation
|
|
1539
|
-
remediation:
|
|
1675
|
+
metric: 'stageDurationMinutes', value, reads,
|
|
1676
|
+
recommendation,
|
|
1677
|
+
remediation: reads === 'shuffle'
|
|
1678
|
+
? [increaseConf('spark.sql.shuffle.partitions'), increaseConf('spark.default.parallelism')]
|
|
1679
|
+
: reads === 'input' ? [decreaseConf('spark.sql.files.maxPartitionBytes')] : [],
|
|
1540
1680
|
};
|
|
1541
1681
|
},
|
|
1542
1682
|
estimate(finding, ctx) {
|
|
@@ -1664,8 +1804,10 @@ export const DETECTORS = [
|
|
|
1664
1804
|
const detail = useSpeculativeMetric
|
|
1665
1805
|
? `${value} speculative attempt${value === 1 ? '' : 's'} discarded`
|
|
1666
1806
|
: `${value}% of tasks straggled`;
|
|
1807
|
+
// The skew advice fits only a stage that reads a shuffle feeding a join (skewFix).
|
|
1808
|
+
const fix = skewFix(stage, ctx);
|
|
1667
1809
|
return {
|
|
1668
|
-
type: 'straggler', stageId: stage.id, impactBand,
|
|
1810
|
+
type: 'straggler', stageId: stage.id, impactBand, origin: fix.origin,
|
|
1669
1811
|
metric: useSpeculativeMetric ? 'speculativeTasks' : 'stragglerShare',
|
|
1670
1812
|
value,
|
|
1671
1813
|
unit: useSpeculativeMetric ? 'count' : 'pct',
|
|
@@ -1675,7 +1817,8 @@ export const DETECTORS = [
|
|
|
1675
1817
|
? stragglerConfidence(speculativeShare, thresholds.warnPct, thresholds.critPct)
|
|
1676
1818
|
: stragglerConfidence(stragglerShare, thresholds.shareWarn, thresholds.critPct),
|
|
1677
1819
|
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
|
|
1820
|
+
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}.`,
|
|
1821
|
+
remediation: fix.remediation,
|
|
1679
1822
|
};
|
|
1680
1823
|
},
|
|
1681
1824
|
estimate(finding, ctx) {
|
|
@@ -1755,14 +1898,15 @@ export const DETECTORS = [
|
|
|
1755
1898
|
if (stageBelowRuntimeFloor(stage, ctx, thresholds.stageFloorPct)) return null;
|
|
1756
1899
|
if (stage.taskDurationP50 > thresholds.maxP50 || stage.taskDurationP95 > thresholds.maxP95) return null;
|
|
1757
1900
|
const coalesceTo = Math.max(1, Math.round(stage.taskCount / 10));
|
|
1758
|
-
const
|
|
1901
|
+
const reads = stageReads(stage);
|
|
1902
|
+
const fix = reads === 'shuffle'
|
|
1759
1903
|
? `lower spark.sql.shuffle.partitions or .coalesce(${coalesceTo})`
|
|
1760
1904
|
: `.coalesce(${coalesceTo})`;
|
|
1761
1905
|
return {
|
|
1762
|
-
type: 'tinyTask', stageId: stage.id, impactBand: 'info',
|
|
1906
|
+
type: 'tinyTask', stageId: stage.id, impactBand: 'info', reads,
|
|
1763
1907
|
metric: 'taskDurationP50', value: Math.round(stage.taskDurationP50),
|
|
1764
1908
|
recommendation: `Many small tasks (${stage.taskCount}, P50 ${Math.round(stage.taskDurationP50)}ms): scheduler overhead may dominate. Try ${fix}.`,
|
|
1765
|
-
remediation:
|
|
1909
|
+
remediation: reads === 'shuffle' ? [decreaseConf('spark.sql.shuffle.partitions')] : [],
|
|
1766
1910
|
};
|
|
1767
1911
|
},
|
|
1768
1912
|
estimate(finding, ctx) {
|
|
@@ -1839,6 +1983,7 @@ export const DETECTORS = [
|
|
|
1839
1983
|
const value = Math.round(gapSeconds);
|
|
1840
1984
|
return {
|
|
1841
1985
|
type: 'coldStart', stageId: null, impactBand: 'warning',
|
|
1986
|
+
dynamicAllocation: dynamicAllocationOff(ctx.app) ? 'off' : 'on',
|
|
1842
1987
|
metric: 'startupGapSeconds', value,
|
|
1843
1988
|
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
1989
|
remediation: dynamicAllocationOff(ctx.app) ? [] : [increaseConf('spark.dynamicAllocation.minExecutors'), increaseConf('spark.dynamicAllocation.initialExecutors')],
|
|
@@ -2196,14 +2341,19 @@ export const DETECTORS = [
|
|
|
2196
2341
|
if (!impactBand) return null;
|
|
2197
2342
|
|
|
2198
2343
|
const pct = Math.round(shortLivedPct * 100);
|
|
2344
|
+
const daOff = dynamicAllocationOff(app);
|
|
2199
2345
|
return {
|
|
2200
2346
|
type: 'autoscalingChurn', stageId: null, impactBand,
|
|
2201
2347
|
metric: 'shortLivedExecutorPct', value: pct,
|
|
2202
2348
|
// Raw count behind the percentage, for the impact estimator's startup-overhead figure.
|
|
2203
2349
|
shortLivedExecutorCount: shortLivedCount,
|
|
2204
2350
|
confidence: autoscalingChurnConfidence(shortLivedPct, thresholds.warningPct, thresholds.criticalPct),
|
|
2205
|
-
|
|
2206
|
-
|
|
2351
|
+
// No dynamic-allocation property has an effect when the run's conf turns it off (as for coldStart).
|
|
2352
|
+
dynamicAllocation: daOff ? 'off' : 'on',
|
|
2353
|
+
recommendation: daOff
|
|
2354
|
+
? `${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.`
|
|
2355
|
+
: `${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.`,
|
|
2356
|
+
remediation: daOff ? [] : [
|
|
2207
2357
|
increaseConf('spark.dynamicAllocation.executorIdleTimeout'),
|
|
2208
2358
|
decreaseConf('spark.dynamicAllocation.minExecutors'),
|
|
2209
2359
|
increaseConf('spark.dynamicAllocation.maxExecutors'),
|
|
@@ -2705,14 +2855,21 @@ export const DETECTORS = [
|
|
|
2705
2855
|
|| (smaller < broadcastTiers[3] && larger > comparisonTiers[2]);
|
|
2706
2856
|
if (fires) {
|
|
2707
2857
|
const contributors = [...boundarySizeContributors(childA), ...boundarySizeContributors(childB)];
|
|
2858
|
+
// A threshold that already admits the smaller side cannot be what stopped the broadcast.
|
|
2859
|
+
const threshold = effectiveBroadcastThreshold(ctx.app);
|
|
2860
|
+
const broadcastThreshold = threshold != null && threshold < 0 ? 'disabled'
|
|
2861
|
+
: threshold != null && threshold > smaller ? 'notLimiting' : 'limits';
|
|
2862
|
+
const raiseAdvice = 'Consider a broadcast() hint or raising spark.sql.autoBroadcastJoinThreshold.';
|
|
2708
2863
|
out.push({
|
|
2709
2864
|
type: 'underBroadcast', executionId: sqlExec.id, stageIds: unionStageIds(contributors, fallbackStageIds),
|
|
2710
2865
|
// resolvePlanTree always sets id; safe downstream of it.
|
|
2711
2866
|
planNodeIds: contributors.map((n) => n.id ).filter(Boolean),
|
|
2712
2867
|
impactBand: 'info', metric: 'smallerSideBytes', value: smaller,
|
|
2713
|
-
largerSideBytes: larger,
|
|
2714
|
-
recommendation:
|
|
2715
|
-
|
|
2868
|
+
largerSideBytes: larger, broadcastThreshold,
|
|
2869
|
+
recommendation: broadcastThreshold === 'notLimiting'
|
|
2870
|
+
? `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).`
|
|
2871
|
+
: `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}`,
|
|
2872
|
+
remediation: broadcastThreshold === 'notLimiting' ? [] : [increaseConf('spark.sql.autoBroadcastJoinThreshold')],
|
|
2716
2873
|
});
|
|
2717
2874
|
}
|
|
2718
2875
|
}
|
|
@@ -2724,17 +2881,23 @@ export const DETECTORS = [
|
|
|
2724
2881
|
// (node.stageIds always empty in real data); its child carries the executor-side
|
|
2725
2882
|
// metrics, so only the child unions in.
|
|
2726
2883
|
const child = (node.children ?? [])[0];
|
|
2727
|
-
|
|
2884
|
+
// A threshold below the broadcast cannot have admitted it, so a hint forced it.
|
|
2885
|
+
const threshold = effectiveBroadcastThreshold(ctx.app);
|
|
2886
|
+
const autoBroadcastOff = threshold != null && threshold < 0;
|
|
2887
|
+
const hintForced = autoBroadcastOff || (threshold != null && m.value > threshold);
|
|
2888
|
+
const broadcastThreshold = autoBroadcastOff ? 'disabled' : hintForced ? 'notLimiting' : 'limits';
|
|
2728
2889
|
out.push({
|
|
2729
2890
|
type: 'overBroadcast', executionId: sqlExec.id,
|
|
2730
2891
|
stageIds: unionStageIds(child ? [child] : [], fallbackStageIds),
|
|
2731
2892
|
// resolvePlanTree always sets id; safe downstream of it.
|
|
2732
2893
|
planNodeIds: [node.id ].filter(Boolean),
|
|
2733
|
-
impactBand: 'warning', metric: 'broadcastBytes', value: m.value,
|
|
2894
|
+
impactBand: 'warning', metric: 'broadcastBytes', value: m.value, broadcastThreshold,
|
|
2734
2895
|
recommendation: autoBroadcastOff
|
|
2735
2896
|
? `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
|
-
|
|
2897
|
+
: hintForced
|
|
2898
|
+
? `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.`
|
|
2899
|
+
: `This broadcast (${formatBytes(m.value)}) exceeds the ${binaryThresholdLabel(overBroadcastBytes)} threshold: check for a misapplied broadcast hint or a misconfigured spark.sql.autoBroadcastJoinThreshold.`,
|
|
2900
|
+
remediation: hintForced ? [] : [decreaseConf('spark.sql.autoBroadcastJoinThreshold')],
|
|
2738
2901
|
});
|
|
2739
2902
|
}
|
|
2740
2903
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Executors are stood up and torn down again before they can do useful work:
|
|
4
4
|
re-provisioning churn rather than normal scale-down. Raise
|
|
5
5
|
`spark.dynamicAllocation.executorIdleTimeout`, or widen the
|
|
6
|
-
`minExecutors`/`maxExecutors` bounds to reduce flapping.
|
|
6
|
+
`minExecutors`/`maxExecutors` bounds to reduce flapping. With dynamic
|
|
7
|
+
allocation off (`evidence.dynamicAllocation` is `off`) none of those applies:
|
|
8
|
+
the churn is executor loss or preemption, and `remediation` is empty. Self-flags a
|
|
7
9
|
confidence that scales with how far the short-lived-executor share sits
|
|
8
10
|
past the threshold.
|
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
The first stage waited more than 30 s for an executor. Keep a warm pool of
|
|
4
4
|
executors, or, with dynamic allocation, raise
|
|
5
5
|
`spark.dynamicAllocation.minExecutors`/`initialExecutors` so the app doesn't
|
|
6
|
-
scale up from zero.
|
|
6
|
+
scale up from zero. With dynamic allocation off (`evidence.dynamicAllocation`
|
|
7
|
+
is `off`) no executor-count property applies and `remediation` is empty.
|
|
@@ -11,7 +11,14 @@ this tag:
|
|
|
11
11
|
under 3 MB. Compact upstream output, or coalesce before writing.
|
|
12
12
|
- Under-broadcast: the smaller side of a Sort Merge Join looks well under
|
|
13
13
|
the broadcast threshold; consider a `broadcast()` hint or raising
|
|
14
|
-
`spark.sql.autoBroadcastJoinThreshold`.
|
|
14
|
+
`spark.sql.autoBroadcastJoinThreshold`. When the effective threshold
|
|
15
|
+
(logged, else Spark's 10 MiB) already admits the smaller side
|
|
16
|
+
(`evidence.broadcastThreshold` is `notLimiting`), the threshold is not what
|
|
17
|
+
stopped the broadcast, so `remediation` is empty and the advice is a hint or
|
|
18
|
+
table statistics.
|
|
15
19
|
- Over-broadcast: a broadcast exceeds the 1 GB threshold; check for a
|
|
16
20
|
misapplied broadcast hint or a misconfigured
|
|
17
|
-
`spark.sql.autoBroadcastJoinThreshold`.
|
|
21
|
+
`spark.sql.autoBroadcastJoinThreshold`. When the effective threshold is
|
|
22
|
+
below the broadcast or auto-broadcast is disabled (`evidence.broadcastThreshold`
|
|
23
|
+
is `notLimiting` or `disabled`), a hint forced it: remove the hint, and
|
|
24
|
+
`remediation` is empty.
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
### `SHFL`: Shuffle I/O {#shfl}
|
|
2
2
|
|
|
3
3
|
Tasks move a large amount of intermediate data between stages. Raise
|
|
4
|
-
`spark.sql.shuffle.partitions`, or add a broadcast join.
|
|
5
|
-
|
|
4
|
+
`spark.sql.shuffle.partitions`, or add a broadcast join. The partition-count
|
|
5
|
+
advice follows the effective conf (`evidence.partitions`): `raise` when the
|
|
6
|
+
property limits the stage, `sufficient` when the tasks already read a good
|
|
7
|
+
size each, `aqeCoalesced` when AQE merged the partitions (lower
|
|
8
|
+
`spark.sql.adaptive.advisoryPartitionSizeInBytes`), and `ownPartitioning` when
|
|
9
|
+
the property is already high enough (the stage's own `repartition(n)` or RDD
|
|
10
|
+
parallelism limits it). Only flagged on stages that take at least 0.5% of the
|
|
11
|
+
run.
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
### `SKEW`: Task skew {#skew}
|
|
2
2
|
|
|
3
3
|
A small number of tasks take much longer than their peers in the same
|
|
4
|
-
stage.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
stage. The fix depends on what the stage reads, which the finding's
|
|
5
|
+
`evidence.origin` records. A stage that reads a shuffle feeding a join
|
|
6
|
+
(`shuffleJoin`) gets AQE skew-join handling
|
|
7
|
+
(`spark.sql.adaptive.skewJoin.enabled`), unless the run's effective conf
|
|
8
|
+
already has it; otherwise salt the key or repartition on a better key. A
|
|
9
|
+
stage that reads files with uneven sizes (`inputScan`) gets compaction of
|
|
10
|
+
small files or a lower `spark.sql.files.maxPartitionBytes`. Any other stage
|
|
11
|
+
(`other`) gets the salting advice and no conf. Flagged when P95 task time
|
|
12
|
+
(the longest task, on a stage with fewer than 20 tasks) exceeds 3x the median
|
|
13
|
+
and the recoverable tail is at least 0.5% of the run. The median is the
|
|
14
|
+
textbook one: on an even task count, the mean of the two middle values.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
### `SLOW`: Stage slowness {#slow}
|
|
2
2
|
|
|
3
3
|
A stage ran for 15 minutes or more and no slow host was flagged on it. It
|
|
4
|
-
can appear alongside other findings on the same stage.
|
|
4
|
+
can appear alongside other findings on the same stage. On a stage that reads a
|
|
5
|
+
shuffle, often a partition-count problem: raise parallelism via
|
|
5
6
|
`spark.sql.shuffle.partitions` or `spark.default.parallelism`, or check for a
|
|
6
|
-
large per-task data volume driving heavy shuffle and spill.
|
|
7
|
+
large per-task data volume driving heavy shuffle and spill. On a stage that
|
|
8
|
+
reads input files and no shuffle, check input file sizes and lower
|
|
9
|
+
`spark.sql.files.maxPartitionBytes`. `evidence.reads` says which case
|
|
10
|
+
applied (`shuffle`, `input` or `other`).
|
|
@@ -4,4 +4,7 @@ Tasks are writing data out of memory, which slows execution. Two spill
|
|
|
4
4
|
patterns get flagged differently: skew spill, where a few heavy tasks spill
|
|
5
5
|
while most don't (rebalance partitioning), and volume spill, where most
|
|
6
6
|
tasks spill because the data genuinely exceeds available memory (add
|
|
7
|
-
partitions or executor memory).
|
|
7
|
+
partitions or executor memory). `spark.sql.shuffle.partitions` is suggested
|
|
8
|
+
only for a stage that reads a shuffle; `evidence.reads` says what the stage
|
|
9
|
+
reads (`shuffle`, `input` or `other`). Only flagged on stages that take at
|
|
10
|
+
least 0.5% of the run.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
### `STRAG`: Straggler tasks {#strag}
|
|
2
2
|
|
|
3
3
|
A few tasks run much slower than the rest of their stage. Rule out a GC
|
|
4
|
-
pause or a slow shuffle fetch before assuming a hardware issue
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
pause or a slow shuffle fetch before assuming a hardware issue. If uneven
|
|
5
|
+
data is the cause, the advice is the one a skew finding gives for the same
|
|
6
|
+
stage (`evidence.origin`, see `SKEW`): AQE skew-join handling for a shuffle
|
|
7
|
+
feeding a join, file sizes for a scan, salting otherwise. Only flagged on
|
|
8
|
+
stages that take at least 0.5% of the run.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
### `TINY`: Tiny tasks {#tiny}
|
|
2
2
|
|
|
3
3
|
Many very short tasks add scheduling overhead out of proportion to the work
|
|
4
|
-
each one does. Repartition to fewer, larger tasks
|
|
5
|
-
|
|
4
|
+
each one does. Repartition to fewer, larger tasks; a stage that reads a shuffle
|
|
5
|
+
(`evidence.reads` is `shuffle`) can also lower `spark.sql.shuffle.partitions`.
|
|
6
|
+
Only flagged on stages that take at least 0.5% of the run.
|