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.
Files changed (199) hide show
  1. package/package.json +1 -1
  2. package/public/assets/{AutoscalingChurn-5hDGIXi4.js → AutoscalingChurn-8t3U3isq.js} +1 -1
  3. package/public/assets/{CacheUtilization-CT2SCnJJ.js → CacheUtilization-CJs9tTum.js} +1 -1
  4. package/public/assets/CachingOpportunity-D1NcHs0V.js +1 -0
  5. package/public/assets/{ColdStart-B38OiAOp.js → ColdStart-CdRJ9gD8.js} +1 -1
  6. package/public/assets/{ConfigAudit-BoplryCH.js → ConfigAudit-B5Gbe5js.js} +1 -1
  7. package/public/assets/CoreUsageArea-a--NTRGY.js +1 -0
  8. package/public/assets/{Dashboard-BgKodJl2.js → Dashboard-C1hNeX49.js} +2 -2
  9. package/public/assets/DisclosureContext-ZpE6CkHx.js +1 -0
  10. package/public/assets/{DuplicatePlanSubtree-BqS2A2j4.js → DuplicatePlanSubtree-dobXdz7E.js} +1 -1
  11. package/public/assets/{ExecutorUtilization-Dp52QGWo.js → ExecutorUtilization-D9grwi7t.js} +1 -1
  12. package/public/assets/{GcPressure-CT1yzR_s.js → GcPressure-8-G2ri10.js} +1 -1
  13. package/public/assets/{IncompleteRun-C7Qkjtjn.js → IncompleteRun-B91lky7a.js} +1 -1
  14. package/public/assets/{JobFailures-WHl3ELfh.js → JobFailures-BsWvBMyJ.js} +1 -1
  15. package/public/assets/{MemoryUtilization-D8IUDHJI.js → MemoryUtilization-BM5schZI.js} +1 -1
  16. package/public/assets/{OverBroadcast-6oSSAQ_R.js → OverBroadcast-DdN8JIJH.js} +1 -1
  17. package/public/assets/{PartitionSizing-BwdFL-mw.js → PartitionSizing-CY5HDYtp.js} +1 -1
  18. package/public/assets/{PlanExplorer-DcxmiQWL.js → PlanExplorer-DgHCDBD_.js} +1 -1
  19. package/public/assets/PlanGraphRoute-DH3KJb8j.js +1 -0
  20. package/public/assets/{RetryWaste-BTbeHkJc.js → RetryWaste-DOnE9vGF.js} +1 -1
  21. package/public/assets/{ShuffleIO-CRhrYNE0.js → ShuffleIO-MMnDESkF.js} +1 -1
  22. package/public/assets/{Skew-DSNXRG9V.js → Skew-BmZZJ5ms.js} +1 -1
  23. package/public/assets/{SlowHost-B7Lwu2EU.js → SlowHost-CsJzfEqo.js} +1 -1
  24. package/public/assets/{SmallFiles-C082c2rd.js → SmallFiles-BoK3R1Ex.js} +1 -1
  25. package/public/assets/{SpeculationWaste-BS2vpadr.js → SpeculationWaste-CnVxB5AJ.js} +1 -1
  26. package/public/assets/Spill-BhNicyBy.js +1 -0
  27. package/public/assets/{StageFailed-D3ac9Sou.js → StageFailed-mAhHU4l_.js} +1 -1
  28. package/public/assets/StageFindingGroup-BEtPWcT0.js +1 -0
  29. package/public/assets/{StageHeader-pI1nTwVf.js → StageHeader-DJkYkrsU.js} +1 -1
  30. package/public/assets/{StageShape-eO5le8iG.js → StageShape-BDxRknLp.js} +1 -1
  31. package/public/assets/{StageSlowness-BaNyfNvL.js → StageSlowness-BPRw-otN.js} +1 -1
  32. package/public/assets/{Straggler-D5Fnok_y.js → Straggler-CZCbnOWP.js} +1 -1
  33. package/public/assets/{TaskFailures-BWnC-K_G.js → TaskFailures-C57YcLFQ.js} +1 -1
  34. package/public/assets/{TinyTask-8_5lpq_2.js → TinyTask-Bi84qU7n.js} +1 -1
  35. package/public/assets/{UnderBroadcast-DJ0XSwth.js → UnderBroadcast-DdQV7htE.js} +1 -1
  36. package/public/assets/{finding-anchor-Cys8Z6Yn.js → finding-anchor-Cl6_5hfV.js} +1 -1
  37. package/public/assets/{impact-sort-BTQndbof.js → impact-sort-CssZ6MT2.js} +1 -1
  38. package/public/assets/index-2h_TgUAb.css +2 -0
  39. package/public/assets/index-B-03mG7K.js +14 -0
  40. package/public/assets/{parser-worker-DmeCnLWU.js → parser-worker-CA5RxqKc.js} +8 -8
  41. package/public/assets/{select-DIYN6D3y.js → select-CQjNhprm.js} +1 -1
  42. package/public/assets/{usePagedRows-BxvdFnPk.js → usePagedRows-Dyl6T8Ok.js} +1 -1
  43. package/public/assets/useRecentFiles-XZEnLlal.js +11 -0
  44. package/public/assets/{useSortMode-B8SRfAoG.js → useSortMode-Dhvit8dt.js} +1 -1
  45. package/public/docs/404.html +2 -2
  46. package/public/docs/assets/{app.D2GqAkhy.js → app.eYTPqTeq.js} +1 -1
  47. package/public/docs/assets/chunks/@localSearchIndexroot.u_e5bQz4.js +1 -0
  48. package/public/docs/assets/chunks/{VPLocalSearchBox.BS4cSmtW.js → VPLocalSearchBox.DsZrQKQR.js} +1 -1
  49. package/public/docs/assets/chunks/{theme.BuxVpb0H.js → theme.DsTNE2pE.js} +2 -2
  50. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.js +1 -0
  51. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.js +1 -0
  52. 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
  53. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.js +1 -0
  54. 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
  55. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.lean.js +1 -0
  56. 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
  57. 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
  58. package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.js → user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.js} +2 -2
  59. 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
  60. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.js +1 -0
  61. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.lean.js +1 -0
  62. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.js +1 -0
  63. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.lean.js +1 -0
  64. package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.js +1 -0
  65. package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.lean.js +1 -0
  66. package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +3 -3
  67. package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +3 -3
  68. package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +3 -3
  69. package/public/docs/contributor-guide/architecture/board-widgets.html +3 -3
  70. package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +3 -3
  71. package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +5 -5
  72. package/public/docs/contributor-guide/architecture/detector-contract.html +3 -3
  73. package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +3 -3
  74. package/public/docs/contributor-guide/architecture/drill-down.html +3 -3
  75. package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +3 -3
  76. package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +3 -3
  77. package/public/docs/contributor-guide/architecture/impact-estimation.html +3 -3
  78. package/public/docs/contributor-guide/architecture/index.html +3 -3
  79. package/public/docs/contributor-guide/architecture/overview.html +3 -3
  80. package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +3 -3
  81. package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +5 -5
  82. package/public/docs/contributor-guide/architecture/state-and-history.html +3 -3
  83. package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +3 -3
  84. package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +3 -3
  85. package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +3 -3
  86. package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +3 -3
  87. package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +3 -3
  88. package/public/docs/contributor-guide/architecture/widget-rendering.html +3 -3
  89. package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +5 -5
  90. package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +3 -3
  91. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +3 -3
  92. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +3 -3
  93. package/public/docs/contributor-guide/architecture/worker-protocol.html +3 -3
  94. package/public/docs/contributor-guide/contributing.html +3 -3
  95. package/public/docs/contributor-guide/development-setup.html +3 -3
  96. package/public/docs/contributor-guide/testing.html +3 -3
  97. package/public/docs/hashmap.json +1 -1
  98. package/public/docs/index.html +3 -3
  99. package/public/docs/tuning-reference/anti-patterns.html +3 -3
  100. package/public/docs/tuning-reference/aqe.html +3 -3
  101. package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +3 -3
  102. package/public/docs/tuning-reference/bottleneck-cold-start.html +3 -3
  103. package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +3 -3
  104. package/public/docs/tuning-reference/bottleneck-failures.html +3 -3
  105. package/public/docs/tuning-reference/bottleneck-gc.html +3 -3
  106. package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +3 -3
  107. package/public/docs/tuning-reference/bottleneck-memory-utilization.html +3 -3
  108. package/public/docs/tuning-reference/bottleneck-retry-waste.html +3 -3
  109. package/public/docs/tuning-reference/bottleneck-shuffle.html +3 -3
  110. package/public/docs/tuning-reference/bottleneck-skew.html +3 -3
  111. package/public/docs/tuning-reference/bottleneck-slow-host.html +3 -3
  112. package/public/docs/tuning-reference/bottleneck-small-files.html +3 -3
  113. package/public/docs/tuning-reference/bottleneck-spill.html +3 -3
  114. package/public/docs/tuning-reference/bottleneck-straggler.html +3 -3
  115. package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +3 -3
  116. package/public/docs/tuning-reference/bottleneck-utilization.html +3 -3
  117. package/public/docs/tuning-reference/caching.html +3 -3
  118. package/public/docs/tuning-reference/cluster-config.html +3 -3
  119. package/public/docs/tuning-reference/config.html +3 -3
  120. package/public/docs/tuning-reference/data-formats.html +3 -3
  121. package/public/docs/tuning-reference/index.html +3 -3
  122. package/public/docs/tuning-reference/intro.html +3 -3
  123. package/public/docs/tuning-reference/joins.html +3 -3
  124. package/public/docs/tuning-reference/memory-model.html +3 -3
  125. package/public/docs/tuning-reference/metrics.html +3 -3
  126. package/public/docs/tuning-reference/partitioning.html +3 -3
  127. package/public/docs/tuning-reference/pyspark.html +3 -3
  128. package/public/docs/tuning-reference/shuffle.html +3 -3
  129. package/public/docs/tuning-reference/spark-architecture.html +3 -3
  130. package/public/docs/tuning-reference/table-formats.html +3 -3
  131. package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +3 -3
  132. package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +3 -3
  133. package/public/docs/user-guide/alternative-log-retrieval.html +3 -3
  134. package/public/docs/user-guide/getting-started/ci-and-automation.html +8 -7
  135. package/public/docs/user-guide/getting-started/reading-the-dashboard.html +3 -3
  136. package/public/docs/user-guide/getting-started/regression-metric-keys.html +3 -3
  137. package/public/docs/user-guide/getting-started/tuning-thresholds.html +3 -3
  138. package/public/docs/user-guide/getting-started.html +3 -3
  139. package/public/docs/user-guide/mcp-tools/compare-runs.html +7 -6
  140. package/public/docs/user-guide/mcp-tools/diagnose-run.html +3 -3
  141. package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +5 -5
  142. package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +3 -3
  143. package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +3 -3
  144. package/public/docs/user-guide/mcp-tools/get-reference-doc.html +3 -3
  145. package/public/docs/user-guide/mcp-tools/get-run-summary.html +3 -3
  146. package/public/docs/user-guide/mcp-tools/list-runs.html +3 -3
  147. package/public/docs/user-guide/mcp-tools.html +3 -3
  148. package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +5 -5
  149. package/public/docs/user-guide/run-comparison.html +5 -5
  150. package/public/docs/user-guide/understanding-findings.html +5 -5
  151. package/public/export-template.html +3 -3
  152. package/public/index.html +3 -3
  153. package/vendor-core/cli/budgets.js +62 -2
  154. package/vendor-core/cli/regression-budgets.js +35 -0
  155. package/vendor-core/comparison-output.js +2 -1
  156. package/vendor-core/core-source-hash.txt +1 -1
  157. package/vendor-core/detectors.js +219 -56
  158. package/vendor-core/docs-content/detection/chrn.md +3 -1
  159. package/vendor-core/docs-content/detection/cold.md +2 -1
  160. package/vendor-core/docs-content/detection/plan.md +9 -2
  161. package/vendor-core/docs-content/detection/shfl.md +8 -2
  162. package/vendor-core/docs-content/detection/skew.md +11 -5
  163. package/vendor-core/docs-content/detection/slow.md +6 -2
  164. package/vendor-core/docs-content/detection/spill.md +4 -1
  165. package/vendor-core/docs-content/detection/strag.md +5 -3
  166. package/vendor-core/docs-content/detection/tiny.md +3 -2
  167. package/vendor-core/evidence-report.js +11 -11
  168. package/vendor-core/finding-presentation.js +39 -13
  169. package/vendor-core/finding-types.js +50 -6
  170. package/vendor-core/mcp-server-factory.js +6 -2
  171. package/vendor-core/mcp-tools.js +10 -1
  172. package/vendor-core/median.js +8 -0
  173. package/vendor-core/run-comparison.js +2 -0
  174. package/vendor-core/sequence-alignment.js +83 -0
  175. package/vendor-core/stage-alignment.js +387 -63
  176. package/vendor-core/stage-quantiles.js +3 -1
  177. package/vendor-core/stage-structure.js +83 -0
  178. package/public/assets/CachingOpportunity-D_O_WkbH.js +0 -1
  179. package/public/assets/CoreUsageArea-csgwNxkl.js +0 -1
  180. package/public/assets/DisclosureContext-B0siMVdg.js +0 -1
  181. package/public/assets/PlanGraphRoute-pqnHTt4i.js +0 -1
  182. package/public/assets/Spill-B4DVKHXx.js +0 -1
  183. package/public/assets/StageFindingGroup-D9naK109.js +0 -1
  184. package/public/assets/index-BcqTLKFp.css +0 -2
  185. package/public/assets/index-DCeWge12.js +0 -13
  186. package/public/assets/useRecentFiles-C3Ov47U7.js +0 -11
  187. package/public/docs/assets/chunks/@localSearchIndexroot.D4OdkQjy.js +0 -1
  188. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.js +0 -1
  189. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.js +0 -1
  190. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.js +0 -1
  191. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.lean.js +0 -1
  192. package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.js +0 -1
  193. package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.lean.js +0 -1
  194. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.js +0 -1
  195. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.lean.js +0 -1
  196. package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.js +0 -1
  197. package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.lean.js +0 -1
  198. /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
  199. /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
@@ -180,29 +180,29 @@ export const EVIDENCE_SCHEMA_VERSION = 5;
180
180
  // module (stageShape's totalCores, utilization's unrounded fraction) is left off both, so it never
181
181
  // reaches the report; adding, renaming or dropping a key here changes the report contract.
182
182
  const EVIDENCE_KEYS = {
183
- skew: [],
183
+ skew: ['origin'],
184
184
  stageShape: ['rule'],
185
- shuffle: [],
186
- partitionSizing: ['rule'],
187
- spill: ['spillMagnitude'],
185
+ shuffle: ['partitions'],
186
+ partitionSizing: ['rule', 'origin', 'partitions'],
187
+ spill: ['spillMagnitude', 'reads'],
188
188
  gc: ['direction'],
189
189
  slowHost: ['variant', 'host', 'hostTaskShare', 'hostMeanMs', 'dimension', 'executorId', 'execMaxValue'],
190
- stageSlowness: [],
190
+ stageSlowness: ['reads'],
191
191
  stageFailed: ['variant', 'numTasks', 'memoryBytesSpilled', 'failedTaskDetails'],
192
192
  failures: ['failedTasks', 'dominantReason', 'dominantError', 'failureGroups', 'otherFailedTasks'],
193
- straggler: ['unit', 'speculativeTasks', 'stragglerCount'],
193
+ straggler: ['unit', 'speculativeTasks', 'stragglerCount', 'origin'],
194
194
  speculationWaste: [],
195
195
  retryWaste: ['numTasks', 'memoryBytesSpilled', 'retriedTaskDetails'],
196
- tinyTask: [],
196
+ tinyTask: ['reads'],
197
197
  incompleteRun: [],
198
- coldStart: [],
198
+ coldStart: ['dynamicAllocation'],
199
199
  utilization: ['cpuUtilizationPct'],
200
200
  memoryUtilization: ['variant', 'rule', 'executorId', 'heap', 'dataUnavailable'],
201
201
  cacheUtilization: [
202
202
  'variant', 'rddId', 'rddName', 'memorySize', 'diskSize', 'numCachedPartitions', 'numPartitions', 'dataUnavailable',
203
203
  ],
204
204
  coreLocality: ['nonLocalTaskCount'],
205
- autoscalingChurn: ['shortLivedExecutorCount'],
205
+ autoscalingChurn: ['shortLivedExecutorCount', 'dynamicAllocation'],
206
206
  cachingOpportunity: ['variant', 'relation', 'format', 'relations', 'operator', 'executionIds', 'totalReadBytes'],
207
207
  jobFailureRate: ['failedJobs', 'totalJobs', 'failedTasks', 'totalTasks', 'avgJobDurationMs', 'taskFailureRate'],
208
208
  configAudit: ['property'],
@@ -211,8 +211,8 @@ const EVIDENCE_KEYS = {
211
211
  'groupIndex',
212
212
  ],
213
213
  smallFiles: ['executionId', 'stageIds', 'fileCount', 'direction', 'nodeName'],
214
- underBroadcast: ['executionId', 'stageIds', 'largerSideBytes'],
215
- overBroadcast: ['executionId', 'stageIds'],
214
+ underBroadcast: ['executionId', 'stageIds', 'largerSideBytes', 'broadcastThreshold'],
215
+ overBroadcast: ['executionId', 'stageIds', 'broadcastThreshold'],
216
216
  } ;
217
217
 
218
218
  // The other direction: an evidence field EVIDENCE_KEYS doesn't list fails here.
@@ -8,7 +8,7 @@
8
8
  // detectors.ts is type-only, so it is erased at build time. A detector's scope, order and emits
9
9
  // list stay on its DETECTORS entry; renderers get them through detectorInfoByType().
10
10
 
11
-
11
+
12
12
 
13
13
 
14
14
 
@@ -43,7 +43,9 @@ const SKEW_JOIN_ALREADY_ON = 'AQE skew-join handling is already on, so salt the
43
43
  const SKEW_JOIN_AQE_OFF = 'AQE is off, so enable it (spark.sql.adaptive.enabled) for skew-join handling to apply; otherwise salt the key or repartition on a better key.';
44
44
 
45
45
  // The skew-join generic line, worded per the row's remediation: AQE logged off, switch already on, or neither.
46
- function skewJoinGeneric(f , unset ) {
46
+ function skewJoinGeneric(f , unset ) {
47
+ if (f.origin === 'inputScan') return 'Uneven input files: compact small files or split large ones (lower spark.sql.files.maxPartitionBytes).';
48
+ if (f.origin === 'other') return 'Work is uneven across tasks: salt the key or repartition on a better key.';
47
49
  if (f.remediation?.some((r) => r.key === 'spark.sql.adaptive.enabled')) return SKEW_JOIN_AQE_OFF;
48
50
  return switchAlreadyOn(f, SKEW_JOIN_KEY) ? SKEW_JOIN_ALREADY_ON : unset;
49
51
  }
@@ -120,8 +122,9 @@ export const FINDING_PRESENTATION
120
122
  tag: 'TINY',
121
123
  thresholdSummary: (t) => `${t.minTasks}+ tasks with a median of ${t.maxP50}ms or less and a P95 of ${t.maxP95}ms or less`,
122
124
  actionLabel: () => 'Coalesce small tasks',
123
- // The shuffle-vs-no-shuffle fix isn't a Finding field, so one sentence covers both.
124
- genericRecommendation: () => 'Scheduler overhead may dominate: lower spark.sql.shuffle.partitions, or coalesce down to fewer, larger tasks.',
125
+ genericRecommendation: (f) => (f.reads != null && f.reads !== 'shuffle'
126
+ ? 'Scheduler overhead may dominate: coalesce down to fewer, larger tasks.'
127
+ : 'Scheduler overhead may dominate: lower spark.sql.shuffle.partitions, or coalesce down to fewer, larger tasks.'),
125
128
  },
126
129
 
127
130
  shuffle: {
@@ -129,7 +132,14 @@ export const FINDING_PRESENTATION
129
132
  tag: 'SHFL',
130
133
  thresholdSummary: (t) => `stage shuffle read above ${t.minBytes / 1048576} MiB`,
131
134
  actionLabel: () => 'Reduce shuffle size',
132
- genericRecommendation: () => 'Raise spark.sql.shuffle.partitions, or use a broadcast join for the smaller side.',
135
+ genericRecommendation: (f) => {
136
+ switch (f.partitions) {
137
+ case 'sufficient': return 'The tasks are already a good size, so more partitions will not help: use a broadcast join for the smaller side.';
138
+ case 'aqeCoalesced': return 'AQE coalesced the shuffle: lower spark.sql.adaptive.advisoryPartitionSizeInBytes, or use a broadcast join for the smaller side.';
139
+ case 'ownPartitioning': return "spark.sql.shuffle.partitions is already high enough, so raise this stage's own partition count (its repartition(n) or RDD parallelism), or use a broadcast join for the smaller side.";
140
+ default: return 'Raise spark.sql.shuffle.partitions, or use a broadcast join for the smaller side.';
141
+ }
142
+ },
133
143
  },
134
144
  partitionSizing: {
135
145
  name: 'partition sizing',
@@ -146,7 +156,9 @@ export const FINDING_PRESENTATION
146
156
  genericRecommendation(f) {
147
157
  switch (f.rule) {
148
158
  case 'shufflePartitionSkew': return skewJoinGeneric(f, 'For join skew, enable AQE skew-join handling (spark.sql.adaptive.skewJoin.enabled); otherwise salt the key or repartition on a better key.');
149
- case 'lowShuffleParallelism': return switchAlreadyOn(f, 'spark.sql.shuffle.partitions')
159
+ case 'lowShuffleParallelism':
160
+ if (f.partitions === 'aqeCoalesced') return 'AQE coalesced the shuffle into few tasks: lower spark.sql.adaptive.advisoryPartitionSizeInBytes so each partition is smaller.';
161
+ return switchAlreadyOn(f, 'spark.sql.shuffle.partitions')
150
162
  ? "spark.sql.shuffle.partitions is already high enough, so raise this stage's own partition count (its repartition(n) or RDD parallelism) so each partition is smaller."
151
163
  : 'Raise spark.sql.shuffle.partitions so each partition is smaller.';
152
164
  case 'maxPartitionTooBig': return 'Repartition to break up the oversized partition before this stage.';
@@ -161,7 +173,9 @@ export const FINDING_PRESENTATION
161
173
  thresholdSummary: (t) => `single-task disk spill above ${t.singleTaskDiskGiB} GiB`,
162
174
  actionLabel: () => 'Reduce spill',
163
175
  // The skew/volume classification isn't a Finding field, so one sentence covers both.
164
- genericRecommendation: () => 'If the spill is skew-driven, fix task skew first: adding memory will not help. Otherwise raise spark.sql.shuffle.partitions or increase executor memory.',
176
+ genericRecommendation: (f) => (f.reads != null && f.reads !== 'shuffle'
177
+ ? 'If the spill is skew-driven, fix task skew first: adding memory will not help. Otherwise increase executor memory or process less data per task.'
178
+ : 'If the spill is skew-driven, fix task skew first: adding memory will not help. Otherwise raise spark.sql.shuffle.partitions or increase executor memory.'),
165
179
  },
166
180
 
167
181
  gc: {
@@ -219,14 +233,18 @@ export const FINDING_PRESENTATION
219
233
  tag: 'SLOW',
220
234
  thresholdSummary: () => 'a stage running far longer than its peers, not attributable to a single slow host',
221
235
  actionLabel: () => 'Profile slow stage',
222
- genericRecommendation: () => '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.',
236
+ genericRecommendation: (f) => f.reads === 'input'
237
+ ? 'Often too few or too uneven input partitions: check input file sizes and lower spark.sql.files.maxPartitionBytes, or look for a large per-task data volume driving heavy spill.'
238
+ : f.reads === 'other'
239
+ ? 'Check what the stage computes and for a large per-task data volume driving heavy spill.'
240
+ : '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.',
223
241
  },
224
242
  straggler: {
225
243
  name: 'straggling task',
226
244
  tag: 'STRAG',
227
245
  thresholdSummary: () => 'one or more tasks finishing far after the rest of their stage',
228
246
  actionLabel: () => 'Fix stragglers',
229
- genericRecommendation: () => 'Rule out a GC pause or a slow shuffle fetch before assuming a hardware issue. If a skewed key is the real cause, that is a candidate for AQE\'s skew-join handling.',
247
+ genericRecommendation: (f) => `Rule out a GC pause or a slow shuffle fetch before assuming a hardware issue. If uneven data is the cause: ${skewJoinGeneric(f, 'for join-driven skew, enable AQE skew-join handling (spark.sql.adaptive.skewJoin.enabled); otherwise salt the key or repartition on a better key.')}`,
230
248
  },
231
249
  speculationWaste: {
232
250
  name: 'speculation waste',
@@ -240,7 +258,9 @@ export const FINDING_PRESENTATION
240
258
  tag: 'COLD',
241
259
  thresholdSummary: (t) => `the first stage waiting over ${t.gapSeconds}s for an executor`,
242
260
  actionLabel: () => 'Pre-warm cluster',
243
- genericRecommendation: () => 'Keep a warm pool of idle executors, or if using dynamic allocation, raise the minimum/initial executor count so it does not scale up from zero.',
261
+ genericRecommendation: (f) => (f.dynamicAllocation === 'off'
262
+ ? 'Keep a warm pool of idle executors: dynamic allocation is off, so no executor-count property applies.'
263
+ : 'Keep a warm pool of idle executors, or if using dynamic allocation, raise the minimum/initial executor count so it does not scale up from zero.'),
244
264
  },
245
265
 
246
266
  memoryUtilization: {
@@ -322,7 +342,9 @@ export const FINDING_PRESENTATION
322
342
  tag: 'CHRN',
323
343
  thresholdSummary: (t) => `over ${shareLabel(t.warningPct)} of executors living under ${t.shortLivedMs / 60000} minutes`,
324
344
  actionLabel: () => 'Reduce autoscaling churn',
325
- genericRecommendation: () => '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.',
345
+ genericRecommendation: (f) => (f.dynamicAllocation === 'off'
346
+ ? 'Dynamic allocation is off, so the churn comes from executors lost or preempted: check the cluster manager\'s preemption and executor-loss logs.'
347
+ : '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.'),
326
348
  },
327
349
 
328
350
  configAudit: CONFIG_AUDIT_PRESENTATION,
@@ -349,14 +371,18 @@ export const FINDING_PRESENTATION
349
371
  tag: 'PLAN',
350
372
  thresholdSummary: () => 'a join below the configured size floor that skipped broadcast',
351
373
  actionLabel: () => 'Use broadcast join',
352
- genericRecommendation: () => 'This could have been a broadcast join: consider a broadcast() hint or raising spark.sql.autoBroadcastJoinThreshold.',
374
+ genericRecommendation: (f) => (f.broadcastThreshold === 'notLimiting'
375
+ ? 'The threshold already admits the smaller side, so it is not what stopped the broadcast: consider a broadcast() hint or collecting table statistics.'
376
+ : 'This could have been a broadcast join: consider a broadcast() hint or raising spark.sql.autoBroadcastJoinThreshold.'),
353
377
  },
354
378
  overBroadcast: {
355
379
  name: 'oversized broadcast join',
356
380
  tag: 'PLAN',
357
381
  thresholdSummary: (t) => `a broadcast over ${t.overBroadcastBytes / 1073741824} GiB`,
358
382
  actionLabel: () => 'Fix oversized broadcast',
359
- genericRecommendation: (f) => (switchAlreadyOn(f, 'spark.sql.autoBroadcastJoinThreshold')
383
+ genericRecommendation: (f) => (f.broadcastThreshold === 'notLimiting'
384
+ ? 'The configured threshold is below this broadcast, so remove the broadcast() hint that forced it.'
385
+ : switchAlreadyOn(f, 'spark.sql.autoBroadcastJoinThreshold')
360
386
  ? 'Automatic broadcast is already disabled, so remove the broadcast() hint that forced it.'
361
387
  : 'Check for a misapplied broadcast hint or a misconfigured spark.sql.autoBroadcastJoinThreshold.'),
362
388
  },
@@ -79,7 +79,22 @@
79
79
 
80
80
  // ── Per-stage detectors ─────────────────────────────────────────────────────
81
81
 
82
-
82
+ /** What a skewed stage reads, which decides the fix: 'shuffleJoin' (a shuffle feeding a join, where
83
+ * AQE skew-join handling applies), 'inputScan' (uneven input files) or 'other'. */
84
+
85
+
86
+ /** What a stage reads, by the dominant side: a shuffle, input files, or neither. */
87
+
88
+
89
+ /** Why a shuffle finding's partition-count advice is or is not given: 'raise' (the property limits
90
+ * the stage), 'sufficient' (tasks are already a good size), 'aqeCoalesced' (AQE merged the
91
+ * partitions, so the advisory size is the lever) or 'ownPartitioning' (the property is already high
92
+ * enough, so the stage's own repartition(n) or RDD parallelism is). */
93
+
94
+
95
+
96
+
97
+
83
98
 
84
99
 
85
100
 
@@ -90,16 +105,24 @@
90
105
 
91
106
 
92
107
 
93
-
108
+
109
+
110
+
94
111
 
95
112
 
96
113
 
97
114
 
115
+
116
+
117
+
118
+
98
119
 
99
120
 
100
121
 
101
122
 
102
123
 
124
+
125
+
103
126
 
104
127
 
105
128
 
@@ -124,7 +147,10 @@
124
147
 
125
148
 
126
149
 
127
-
150
+
151
+
152
+
153
+
128
154
 
129
155
 
130
156
 
@@ -151,6 +177,8 @@
151
177
 
152
178
 
153
179
 
180
+
181
+
154
182
 
155
183
 
156
184
 
@@ -167,7 +195,10 @@
167
195
 
168
196
 
169
197
 
170
-
198
+
199
+
200
+
201
+
171
202
 
172
203
 
173
204
  // ── App-level detectors ─────────────────────────────────────────────────────
@@ -176,7 +207,10 @@
176
207
  // valueText is always 'missing': the ApplicationEnd event the log lacks.
177
208
 
178
209
 
179
-
210
+
211
+
212
+
213
+
180
214
 
181
215
 
182
216
 
@@ -231,6 +265,8 @@
231
265
 
232
266
 
233
267
 
268
+
269
+
234
270
 
235
271
 
236
272
 
@@ -296,13 +332,21 @@
296
332
 
297
333
 
298
334
 
335
+
336
+ /** How the effective spark.sql.autoBroadcastJoinThreshold relates to the finding: 'limits' (the
337
+ * property decided it, or is unknown), 'notLimiting' (already admits, or already below, the
338
+ * broadcast, so something else decided it) or 'disabled' (-1). */
339
+
299
340
 
300
341
 
301
342
 
343
+
302
344
 
303
345
 
304
346
 
305
-
347
+
348
+
349
+
306
350
 
307
351
 
308
352
  // ── Unions ──────────────────────────────────────────────────────────────────
@@ -119,14 +119,18 @@ export function createMcpServer({ thresholds }
119
119
  maxRegressionPct: z.number().optional(), regressionMetric: z.string().optional(), failOnIntroduced: z.string().optional(),
120
120
  regressionBudgets: z.array(z.object({ metric: z.string(), maxPct: z.number().nonnegative() })).optional()
121
121
  .describe('Further regression budgets, one per metric (the CLI\'s repeated --regression-budget). A metric can be budgeted once across this list and maxRegressionPct/regressionMetric.'),
122
+ stageRegressionBudgets: z.array(z.object({ metric: z.string(), maxPct: z.number().nonnegative() })).optional()
123
+ .describe('Per paired stage (needs sourceB/runIdB): fail when any eligible pair\'s metric (executorRunTime, executorCpuTime, memoryBytesSpilled, diskBytesSpilled, shuffleReadBytes, shuffleWriteBytes) regressed by more than maxPct. One entry per metric. Inconclusive when no stage pairs.'),
124
+ stageQualities: z.array(z.enum(['exact', 'structural', 'aligned'])).optional()
125
+ .describe('Pair qualities the stage budgets read. Default exact and structural: an aligned pair may compare different work.'),
122
126
  },
123
127
  }, ({
124
128
  source, runId, runIdB, sourceB,
125
129
  maxRuntimeMs, maxSpillGb, maxSkewRatio, maxFailedTaskRatePct, minEfficiencyPct,
126
- maxRegressionPct, regressionMetric, failOnIntroduced, regressionBudgets,
130
+ maxRegressionPct, regressionMetric, failOnIntroduced, regressionBudgets, stageRegressionBudgets, stageQualities,
127
131
  }) => toolResult(evaluateBudgetsForRun(
128
132
  { source, runId },
129
- { maxRuntimeMs, maxSpillGb, maxSkewRatio, maxFailedTaskRatePct, minEfficiencyPct, maxRegressionPct, regressionMetric, failOnIntroduced, regressionBudgets },
133
+ { maxRuntimeMs, maxSpillGb, maxSkewRatio, maxFailedTaskRatePct, minEfficiencyPct, maxRegressionPct, regressionMetric, failOnIntroduced, regressionBudgets, stageRegressionBudgets, stageQualities },
130
134
  (runIdB || sourceB) ? { runId: runIdB, source: sourceB } : undefined,
131
135
  { thresholds },
132
136
  )));
@@ -13,7 +13,7 @@ import { buildComparisonOutput, } from './comparison-outpu
13
13
  import { compileNormalizePatterns } from './stage-alignment.js';
14
14
  import { runOutputBlocks } from './run-output.js';
15
15
  import { evaluateBudgets, } from './cli/budgets.js';
16
- import { combineRegressionBudgets } from './cli/regression-budgets.js';
16
+ import { assertStageBudgetMetric, combineRegressionBudgets } from './cli/regression-budgets.js';
17
17
  import { FINDING_NAMES, titleCase } from './finding-names.js';
18
18
  import { docAnchorForType } from './detector-docs.js';
19
19
  import { DETECTORS, } from './detectors.js';
@@ -416,6 +416,15 @@ export async function evaluateBudgetsForRun(
416
416
  } catch (e) {
417
417
  throw mcpError('access-or-upstream-failure', (e ).message);
418
418
  }
419
+ if (budgets.stageQualities !== undefined && budgets.stageRegressionBudgets === undefined) {
420
+ throw mcpError('access-or-upstream-failure', 'stageQualities requires stageRegressionBudgets.');
421
+ }
422
+ try {
423
+ for (const { metric } of budgets.stageRegressionBudgets ?? []) assertStageBudgetMetric(metric, 'stageRegressionBudgets');
424
+ combineRegressionBudgets((budgets.stageRegressionBudgets ?? []).map((budget) => ({ origin: 'stageRegressionBudgets', budget })));
425
+ } catch (e) {
426
+ throw mcpError('access-or-upstream-failure', (e ).message);
427
+ }
419
428
  const [first, second] = await Promise.all([
420
429
  resolveAndAnalyze(primary, opts?.thresholds),
421
430
  secondary ? resolveAndAnalyze(secondary, opts?.thresholds) : Promise.resolve(undefined),
@@ -0,0 +1,8 @@
1
+ // The textbook median of values already sorted ascending: the middle value for an odd count, the
2
+ // mean of the two middle values for an even one. 0 for an empty list.
3
+ export function medianOfSorted(sorted ) {
4
+ const n = sorted.length;
5
+ if (n === 0) return 0;
6
+ const mid = n >> 1;
7
+ return n % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
8
+ }
@@ -38,6 +38,7 @@ import { summarizeRunOutcome } from './run-outcome.js';
38
38
 
39
39
 
40
40
 
41
+
41
42
 
42
43
 
43
44
 
@@ -408,6 +409,7 @@ export function compareRuns(
408
409
  unmatched: alignment.unmatched,
409
410
  replanned: alignment.replanned,
410
411
  bookkeepingStageIds: alignment.bookkeepingStageIds,
412
+ executionAlignment: alignment.executionAlignment,
411
413
  metrics: metricDeltas(baseSnap, candSnap),
412
414
  findings: findingsDelta(baseSnap, candSnap),
413
415
  stageSkew: stageSkewDeltas(baseSnap, candSnap, alignment.pairs),
@@ -0,0 +1,83 @@
1
+ // Order-preserving sequence alignment: the weighted longest-common-subsequence dynamic program the
2
+ // stage aligner runs over each run's SQL executions. Pure and deterministic.
3
+ //
4
+ // Cost: the full table has n*m cells and one similarity call per cell. Above `FULL_ALIGNMENT_MAX_CELLS`
5
+ // the table is restricted to a band around the diagonal, which costs about n*(2W+1) cells with
6
+ // W = `BAND_BASE_HALF_WIDTH` + |n-m|/2, and the result reports that the bounded form ran.
7
+
8
+ /** Largest n*m the alignment runs in full (1000 x 1000 executions). */
9
+ export const FULL_ALIGNMENT_MAX_CELLS = 1_000_000;
10
+ /** Band half-width added to half of |n-m| once the table is bounded: the band is centered on the line
11
+ * from corner to corner, and a run of insertions or deletions in one place drifts the best path from
12
+ * that line by at most |n-m|/2. */
13
+ export const BAND_BASE_HALF_WIDTH = 100;
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+ /** Aligns two sequences of lengths `n` and `m`, maximizing the summed `score` of matched pairs. `score`
23
+ * returns a non-negative integer; a pair scoring below `minScore` is never matched. Of equally good
24
+ * alignments the one that matches whenever matching is optimal wins, and where it must skip, it skips the
25
+ * element with the higher id (`idA`, `idB`), so the lower ids stay and swapping the two sequences mirrors
26
+ * the pairs. */
27
+ export function alignSequences(
28
+ n , m ,
29
+ score ,
30
+ minScore ,
31
+ idA , idB ,
32
+ ) {
33
+ if (n === 0 || m === 0) return { pairs: [], bounded: false };
34
+ const bounded = n * m > FULL_ALIGNMENT_MAX_CELLS;
35
+ const half = BAND_BASE_HALF_WIDTH + Math.ceil(Math.abs(n - m) / 2);
36
+ // Row i may match columns lo[i]..hi[i]; both bounds only grow with i.
37
+ const lo = new Int32Array(n), hi = new Int32Array(n);
38
+ for (let i = 0; i < n; i++) {
39
+ const center = Math.floor((i * m) / n);
40
+ lo[i] = bounded ? Math.max(0, center - half) : 0;
41
+ hi[i] = bounded ? Math.min(m - 1, center + half) : m - 1;
42
+ }
43
+ // best[i][j - lo[i]]: the best total from the suffixes starting at (i, j), for j inside row i's band.
44
+ const best = new Array(n);
45
+ // Cells outside the band are not stored: left of it the row's first cell stands in (those columns can
46
+ // only match in later rows, whose bands start no earlier), right of it the row cannot match, so the
47
+ // next row answers.
48
+ const get = (i , j ) => {
49
+ for (;;) {
50
+ if (i >= n || j >= m) return 0;
51
+ if (j < lo[i]) j = lo[i];
52
+ else if (j > hi[i]) i++;
53
+ else return best[i][j - lo[i]];
54
+ }
55
+ };
56
+ // The caller memoizes `score`, so the walk below re-asking for visited cells is cheap.
57
+ const cell = score;
58
+ for (let i = n - 1; i >= 0; i--) {
59
+ const row = new Int32Array(hi[i] - lo[i] + 1);
60
+ best[i] = row;
61
+ for (let j = hi[i]; j >= lo[i]; j--) {
62
+ let v = Math.max(get(i + 1, j), j + 1 <= hi[i] ? row[j + 1 - lo[i]] : get(i + 1, j + 1));
63
+ const s = cell(i, j);
64
+ if (s >= minScore) v = Math.max(v, s + get(i + 1, j + 1));
65
+ row[j - lo[i]] = v;
66
+ }
67
+ }
68
+
69
+ const pairs = [];
70
+ let i = 0, j = 0;
71
+ while (i < n && j < m) {
72
+ if (j < lo[i]) { j = lo[i]; continue; }
73
+ if (j > hi[i]) { i++; continue; }
74
+ const v = best[i][j - lo[i]];
75
+ const s = cell(i, j);
76
+ if (s >= minScore && s + get(i + 1, j + 1) === v) { pairs.push([i, j]); i++; j++; continue; }
77
+ const skipA = get(i + 1, j) === v, skipB = get(i, j + 1) === v;
78
+ if (skipA && skipB) { if (idA(i) >= idB(j)) i++; else j++; }
79
+ else if (skipA) i++;
80
+ else j++;
81
+ }
82
+ return { pairs, bounded };
83
+ }