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
@@ -1 +0,0 @@
1
- import{_ as t,o,c as d,a5 as a}from"./chunks/framework.DSg0KOwT.js";const u=JSON.parse('{"title":"Threshold tables","description":"","frontmatter":{},"headers":[],"relativePath":"contributor-guide/architecture/detector-contract/threshold-tables.md","filePath":"contributor-guide/architecture/detector-contract/threshold-tables.md"}'),s={name:"contributor-guide/architecture/detector-contract/threshold-tables.md"};function r(i,e,c,n,l,h){return o(),d("div",null,[...e[0]||(e[0]=[a('<h1 id="threshold-tables" tabindex="-1">Threshold tables <a class="header-anchor" href="#threshold-tables" aria-label="Permalink to &quot;Threshold tables&quot;">​</a></h1><p>The threshold tables for every bottleneck detector, and where each finding&#39;s impact band comes from.</p><h2 id="bottleneck-thresholds" tabindex="-1">Bottleneck thresholds <a class="header-anchor" href="#bottleneck-thresholds" aria-label="Permalink to &quot;Bottleneck thresholds&quot;">​</a></h2><p>Every change to <code>packages/core/src/detectors.ts</code> should reference this table.</p><p>Every finding&#39;s <code>impactBand</code> comes from one of two places. For any finding whose <code>impactEstimate</code> carries a <code>wallClock</code> estimate (the common case for most rules below), <code>analyzer.ts</code> calls <code>deriveImpactBand()</code> (<code>packages/core/src/impact-band.ts</code>) immediately after <code>estimateImpact()</code>, which sets <code>.impactBand</code> purely from <code>wallClock.high</code> as a fraction of the app&#39;s total duration (<code>&gt;= 2%</code> critical, <code>&gt;= 0.5%</code> warning, else info: <code>IMPACT_FLOOR_PCT_CRIT</code>/ <code>IMPACT_FLOOR_PCT_WARN</code>, which <code>skew</code>&#39;s <code>floorPctWarn</code> and <code>straggler</code>&#39;s <code>floorPctWarn</code>/<code>floorPctCrit</code> default to, so a tail those gates admit grades at least warning; a tuned <code>skew</code> or <code>straggler</code> floor also grades that entry&#39;s own findings, see <a href="./../detector-contract.html#tuning-thresholds">Tuning thresholds</a>). For those rules, the table below documents their firing gate plus their fixed fallback constant, which surfaces only when this run&#39;s finding of that type didn&#39;t get a wallClock estimate (a stage excluded from the occupancy sweep). For rules whose finding type never gets a wallClock estimate (<code>resourceOnly</code>/<code>informational</code> basis, e.g. <code>configAudit</code>, or a rule that keeps its own ratio-tiered classification, e.g. <code>failures</code>), the full threshold table below is the real, displayed classification: <code>detectors.ts</code> sets <code>impactBand</code> directly and nothing overwrites it. <code>partitionSizing</code>&#39;s <code>maxPartitionTooBig</code> rule is a third case: it does carry a <code>wallClock</code> estimate but is explicitly exempted in <code>deriveImpactBand()</code> because it&#39;s a hardcoded-critical OOM/crash-risk safety signal, not a time-recovery one, so <code>detectors.ts</code>&#39;s own classification stands regardless of how small that estimate is relative to the run.</p><h3 id="fixed-fallback-only-usually-wallclock-derived-instead" tabindex="-1">Fixed fallback only (usually wallClock-derived instead) <a class="header-anchor" href="#fixed-fallback-only-usually-wallclock-derived-instead" aria-label="Permalink to &quot;Fixed fallback only (usually wallClock-derived instead)&quot;">​</a></h3><p>These rules have no <em>band</em> tiers in <code>packages/core/src/detectors.ts</code> (<code>deriveImpactBand</code> sets the band whenever a wallClock estimate is available); the constant in the last column is only a floor-case fallback. Their <em>firing</em> gate lives in each entry&#39;s <code>thresholds</code> object: it decides whether the rule reports anything, so it stays documented here in full.</p><table tabindex="0"><thead><tr><th>Rule</th><th>Fires when</th><th>Fallback</th></tr></thead><tbody><tr><td>Task skew</td><td><code>taskDurationP95 / taskDurationP50 &gt; 3×</code> (<code>taskDurationMax / P50</code> for stages under <code>minTasksForP95</code> = 20 tasks), <strong>and</strong> the occupancy-clipped tail recovery (<code>tailRecoveryMs</code>: <code>finalizeStage</code>&#39;s task-level replay of the stage with every task over 4× P50 capped at P50) is ≥ <code>floorPctWarn</code> = 0.5% of app runtime. The clip floors the claim at the longest task the fix leaves, not the current one, and at the core work the fix leaves over every core (see impact-estimation.md&#39;s occupancy section); <code>straggler</code>&#39;s floors use the same clip</td><td><code>warning</code></td></tr><tr><td>Shuffle read</td><td><code>shuffleReadBytes &gt; minBytes</code> = 50 MiB, on a stage that isn&#39;t shorter than <code>stageFloorPct</code> = 0.5% of the run (a zero-length stage or an unknown run duration passes; the claim is clipped to the stage, so a shorter one would grade <code>info</code>, the shuffle itself still there)</td><td><code>info</code></td></tr><tr><td>Partition sizing: skew</td><td><code>shuffleReadMax &gt; 5×</code> <code>shuffleReadP50</code> <strong>and</strong> <code>shuffleReadMax &gt; 256 MiB</code></td><td><code>warning</code></td></tr><tr><td>Partition sizing: low parallelism</td><td><code>shuffleReadBytes ≥ 1 GiB</code> <strong>and</strong> <code>taskCount ≤ 7</code></td><td><code>warning</code></td></tr><tr><td>Partition sizing: oversized partition</td><td><code>shuffleReadMax ≥ 5 GiB</code></td><td><code>critical</code></td></tr><tr><td>GC</td><td><code>executorRunTime ≥ minRunTimeMs</code> = 10 s <strong>and</strong> <code>gcPct &gt; 10%</code></td><td><code>warning</code></td></tr><tr><td>GC (low / cost)</td><td><code>executorRunTime ≥ 10 s</code> <strong>and</strong> <code>gcPct &lt; lowInfoPct100</code> = 5% (checked only when the GC row above did not fire) <strong>and</strong> the stage lasts ≥ <code>lowInfoFloorPct</code> = 0.5% of the run (passes when the run&#39;s duration is unknown or the stage has zero length, such as one with no completion time). Gets no wall-clock estimate (an over-provisioning signal), so this band always stands. The floor drops low-GC notes from stages that barely ran: low GC is still true on those stages, but a memory-sizing note from a stage that barely ran adds nothing</td><td><code>info</code></td></tr><tr><td>Spill</td><td>any non-zero <code>memoryBytesSpilled</code>, on a stage that isn&#39;t shorter than <code>stageFloorPct</code> = 0.5% of the run (as for shuffle read: a shorter stage would grade <code>info</code>). The magnitude sub-table below classifies <em>how much</em>, but does not gate firing</td><td><code>warning</code></td></tr><tr><td>Cold start</td><td><code>firstExecutorAddedAt − firstStageSubmittedAt &gt; gapSeconds</code> = 30 s (no finding without executor-added events): the time a runnable stage waited for its first executor. An executor added before the first stage and still alive at submission means no wait and no finding; one removed at or before submission is ignored, so the gap runs to the next executor added after it.</td><td><code>warning</code></td></tr><tr><td>Slow host: mean-duration ratio</td><td>stage has ≥ <code>minHosts</code> = 3 hosts (or executors) and ≥ <code>minTasks</code> = 15 tasks, and lasts ≥ <code>stageFloorPct</code> = 0.5% of the run (passes when the run&#39;s duration is unknown or the stage has zero length, which gets no estimate and keeps its fallback band; every slow-host finding on a shorter stage would grade <code>info</code>, the imbalance itself still true; this gate covers every slowHost row); then per host: mean task duration / overall median ≥ <code>ratioWarn</code> = 2.0× <strong>and</strong> host task-share ≥ <code>minShare</code> = 20% <strong>and</strong> host mean ≥ <code>floorMs</code> = 1000 ms (absolute-magnitude floor, rules out sub-second noise)</td><td><code>warning</code></td></tr><tr><td>Slow host: duration-share</td><td>same stage gate as the row above; then per host: ≥ <code>shareWarn</code> = 75% of the stage&#39;s total task-duration <strong>and</strong> ≥ <code>taskShareWarn</code> = 50% of its task count</td><td><code>warning</code></td></tr><tr><td>Stage slowness: absolute fallback, suppressed when <code>slowHost</code> already fired</td><td>stage wall-clock duration ≥ <code>infoMin</code> = 15 min. The band then comes from the partitioning-headroom estimate (see impact-estimation.md), not the duration</td><td><code>info</code></td></tr><tr><td>Straggler / speculative-execution</td><td><code>taskCount ≥ minTasks</code> = 10, <strong>and</strong> the stage lasts ≥ <code>floorPctWarn</code> = 0.5% of the run (passes when the run&#39;s duration is unknown or the stage has zero length, such as one with no completion time; a shorter stage&#39;s tail can&#39;t cost more than its own duration, so every finding there would grade <code>info</code>, the slow tail itself still true), <strong>and</strong> either any speculative task ran, <strong>or</strong> straggler share &gt; <code>shareWarn</code> = 5%, <strong>or</strong> straggler share &gt; <code>shareWarnAtFloor</code> = 2.5% with the occupancy-clipped tail recovery (<code>tailRecoveryMs</code>, as for skew) already ≥ <code>floorPctWarn</code> (0.5% of app runtime). Skew&#39;s <code>ratioWarn</code> = 3 and the 0.5% floor are the values the task-level tail replay was tuned against. <code>warnPct</code>/<code>critPct</code> (10%/20% speculative share) and <code>floorPctWarn</code>/<code>floorPctCrit</code> (0.5%/2% of app runtime) do not set the band; they rank the straggler-vs-speculative tiers that pick which <em>metric</em> the finding reports</td><td><code>info</code></td></tr><tr><td>Speculation waste</td><td><code>speculationWastedAttempts ≥ minWasted</code> = 5 <strong>and</strong> <code>speculationWasteMs ≥ minWasteMs</code> = 60 s</td><td><code>warning</code></td></tr><tr><td>Retry waste</td><td><code>wastedAttempts ≥ minWasted</code> = 3 <strong>and</strong> <code>retryWasteMs ≥ minWasteMs</code> = 30 s (attempts superseded by a later retry of the same task)</td><td><code>warning</code></td></tr><tr><td>Tiny tasks</td><td><code>taskCount ≥ minTasks</code> = 100 <strong>and</strong> <code>taskDurationP50 ≤ maxP50</code> = 500 ms <strong>and</strong> <code>taskDurationP95 ≤ maxP95</code> = 1000 ms <strong>and</strong> the stage lasts ≥ <code>stageFloorPct</code> = 0.5% of the run (passes when the run&#39;s duration is unknown or the stage has zero length; coalescing can&#39;t save more than the stage&#39;s own duration, so every finding on a shorter stage would grade <code>info</code>, the tasks still tiny). Checked against each stage&#39;s measured per-task overhead (task wall time minus executor run time, the estimate&#39;s own input): a stage with 100+ tasks outside the P50/P95 gate would not save 0.5% of its run by coalescing, while one inside it can, and grades above <code>info</code></td><td><code>info</code></td></tr><tr><td>Duplicate plan subtree</td><td>a subtree of ≥ <code>minSubtreeSize</code> = 3 nodes whose shape fingerprint repeats ≥ <code>minOccurrences</code> = 2× in the plan, unless its linked stages together lasted less than <code>stageFloorPct</code> = 0.5% of the run (a repeat with no linked stage time, or an unknown run duration, is kept; the claim counts at most each stage&#39;s own task-active time, so such a repeat would grade <code>info</code>, the repeat still in the plan). <code>occurrencesIdentical</code> records whether the repeats also agree node-for-node on normalized detail, ignoring AQE query-stage numbers; when they don&#39;t (same shape over another table, filter or projection) the finding is <code>info</code> with confidence <code>low</code> and no time claim. <code>stageShares</code> gives, per stage, the repeated operators&#39; share of the stage&#39;s operators (WholeStageCodegen wrappers and Exchange write halves not counted); with no attributed stage the finding is <code>info</code></td><td><code>warning</code></td></tr><tr><td>Small files read/write</td><td>per read/write side: file count &gt; <code>minFiles</code> = 100 <strong>and</strong> average file size &lt; <code>maxAvgFileSizeMB</code> = 3 MiB</td><td><code>warning</code></td></tr><tr><td>Broadcast sizing: missed</td><td>a 2-child <code>SortMergeJoin</code> whose smaller side is &lt; 10 MiB (unconditional), or &lt; 100 MiB with the larger side &gt; 10 GiB, or &lt; 1 GiB with larger &gt; 300 GiB, or &lt; 5 GiB with larger &gt; 1 TiB (<code>broadcastTiers</code> × <code>comparisonTiers</code>)</td><td><code>info</code></td></tr><tr><td>Broadcast sizing: oversized</td><td>a <code>BroadcastExchange</code> node whose <code>data size</code> metric &gt; <code>overBroadcastBytes</code> = 1 GiB</td><td><code>warning</code></td></tr></tbody></table><h4 id="spill-magnitude-tiers" tabindex="-1">Spill magnitude tiers <a class="header-anchor" href="#spill-magnitude-tiers" aria-label="Permalink to &quot;Spill magnitude tiers&quot;">​</a></h4><p>The spill row&#39;s band is a fixed <code>warning</code> fallback, but <code>computeSpillMagnitude</code> (<code>packages/core/src/detectors.ts</code>) runs on every spill finding and sets its <code>spillMagnitude</code> field, which the Spill widget displays. Its tiers, in evaluation order (first match wins, <code>null</code> when nothing matches):</p><table tabindex="0"><thead><tr><th>Condition</th><th>Magnitude</th></tr></thead><tbody><tr><td>Single-task stage: <code>spillDiskMax ≥ singleTaskDiskGiB</code> = 1 GiB <strong>or</strong> <code>spillMemMax ≥ singleTaskMemGiB</code> = 4 GiB</td><td><code>severe</code></td></tr><tr><td>Multi-task stage: <code>spillDiskMax ≥ highDiskGiB</code> = 1 GiB, <strong>or</strong> <code>spillDiskMax / taskCount ≥ highTaskDiskMB</code> = 512 MiB (per-task proxy), <strong>or</strong> <code>spillMemMax ≥ highMemGiB</code> = 4 GiB</td><td><code>high</code></td></tr><tr><td>Multi-task stage: <code>spillDiskMax ≥ medDiskMB</code> = 256 MiB <strong>or</strong> <code>spillMemMax ≥ medMemGiB</code> = 1 GiB</td><td><code>medium</code></td></tr><tr><td>Skew (<code>taskCount ≥ skewMinTasks</code> = 10): <code>spillDiskMax / spillDiskP50 &gt; skewRatio</code> = 5× <strong>and</strong> <code>spillDiskMax ≥ skewDiskFloorMB</code> = 128 MiB</td><td><code>high</code></td></tr><tr><td>Skew (<code>taskCount ≥ 10</code>): <code>spillMemMax / spillMemP50 &gt; 5×</code> <strong>and</strong> <code>spillMemMax ≥ skewMemFloorMB</code> = 256 MiB</td><td><code>medium</code></td></tr></tbody></table><p>Disk spill is weighted worse than memory spill by design: the memory thresholds sit well above their disk counterparts at every tier.</p><h3 id="full-threshold-table-never-wallclock-derived" tabindex="-1">Full threshold table (never wallClock-derived) <a class="header-anchor" href="#full-threshold-table-never-wallclock-derived" aria-label="Permalink to &quot;Full threshold table (never wallClock-derived)&quot;">​</a></h3><table tabindex="0"><thead><tr><th>Rule</th><th>Warning</th><th>Critical</th></tr></thead><tbody><tr><td>Stage shape: PRatio</td><td><code>taskCount / totalCores &lt; 0.5</code> (info, under-parallelized), on a stage lasting ≥ <code>lowParallelismFloorPct</code> = 0.5% of the run (passes when the run&#39;s duration is unknown or the stage has zero length): parallelizing can&#39;t save more than the stage&#39;s duration, so a shorter stage would fire below the floor</td><td>none</td></tr><tr><td>Stage shape: OIRatio</td><td><code>outputBytes / inputBytes &gt; 10×</code> (info, data explosion)</td><td>none</td></tr><tr><td>Stage shape: TaskStageSkew</td><td><code>taskDurationMax / stageDuration &gt; stageShareMin</code> = 0.5 <strong>and</strong> <code>taskDurationMax / taskDurationP50 &gt; skewWarn</code> = 3× (info), on a multi-task stage lasting ≥ <code>taskStageSkewFloorPct</code> = 0.5% of the run. A task runs inside its stage, so the share is at most 1; the median gate is needed because on a single wave of even tasks the longest one spans the whole stage too</td><td>none</td></tr><tr><td>Failed tasks</td><td>failure rate &gt; 5% (min 10 tasks)</td><td>&gt; 20%</td></tr><tr><td>Stage failed outright</td><td>none</td><td>any <code>stageFailureReason</code> present</td></tr><tr><td>Slow host: multi-dimensional</td><td>max/median ratio across taskTime/inputBytes/shuffleBytes/storageMemory ≥ 1.33× (info); each dimension&#39;s sample must also clear an absolute floor (1000 ms for taskTime, 64 MiB for the byte dimensions). A stage lasting under <code>stageFloorPct</code> = 0.5% of the run is skipped (see the mean-duration row): a byte dimension there has no time estimate and would otherwise keep its ratio tier (a shorter stage&#39;s byte dimension would otherwise keep a warning or critical tier). The taskTime dimension falls back to <code>info</code> and takes its band from its wall-clock estimate; the byte dimensions get no time estimate and keep the ratio tier</td><td>≥ <code>ratioTiers[1]</code> = 1.78× warning, ≥ <code>ratioTiers[3]</code> = 10× critical (<code>ratioTiers[2]</code> = 3.16 is not read)</td></tr><tr><td>Utilization</td><td>avg active executors / peak &lt; 60% (info)</td><td>none</td></tr><tr><td>Autoscaling churn: short-lived executors</td><td>&gt; 30% of executors alive under 2 min (min 5 executors)</td><td>&gt; 60%</td></tr><tr><td>Job failure rate</td><td>≥ 30% (≥ 10% info)</td><td>≥ 50%</td></tr><tr><td>Idle cores</td><td>busy-core-time / (peak cores × wall-clock) idle &gt; 50% (warning)</td><td>none</td></tr><tr><td>Memory band</td><td>peak heap / allocated &gt; 95% too-small (warning); &lt; 70% over-provisioned (info)</td><td>none</td></tr><tr><td>Caching opportunity</td><td>same input relation (or join/union subtree) scanned by ≥ <code>minExecutions</code> = 2 SQL executions in one run</td><td>none (single tier, info)</td></tr><tr><td>Cache utilization: partial caching</td><td><code>numCachedPartitions / numPartitions &lt; 0.90</code> (info)</td><td><code>&lt; 0.50</code> (warning)</td></tr><tr><td>Cache utilization: disk spillover</td><td><code>diskSize / (memorySize + diskSize) &gt; 0.15</code> (info), <code>MEMORY_AND_DISK*</code> only</td><td><code>&gt; 0.40</code> (warning)</td></tr><tr><td>Cache utilization: storage unobserved</td><td>persisted RDDs, but no <code>SparkListenerBlockUpdated</code> for any <code>rdd_*</code> block and every RDD Info figure 0 (<code>spark.eventLog.logBlockUpdates.enabled</code> off on Spark 2.3+): a missing-evidence caveat, not a threshold</td><td>none (single tier, info)</td></tr><tr><td>Core locality</td><td>non-local task ratio ≥ <code>warnRatio</code> = 15% (min <code>minTasks</code> = 50 tasks)</td><td>≥ <code>critRatio</code> = 35%</td></tr><tr><td>Config: memory overhead</td><td><code>spark.executor.memoryOverhead</code> below max(<code>floorMB</code> = 384 MiB, <code>floorPct</code> = 10% of executor memory) (info)</td><td>none</td></tr></tbody></table><p>The RDD Info cache figures on stage events (<code>Number of Cached Partitions</code>, <code>Memory Size</code>, <code>Disk Size</code>) are always 0 since Spark 2.3; Spark 1.x fills them only on <code>StageCompleted</code>. Real cache evidence is <code>SparkListenerBlockUpdated</code>, written only with <code>spark.eventLog.logBlockUpdates.enabled=true</code> (<code>recordBlockUpdate</code> in <code>event-handlers.ts</code>); the corpus <code>cache-memory-only</code>/<code>cache-memory-and-disk</code> logs carry it. Count a block&#39;s sizes only where its storage level says it lives: a drop to disk still reports the dropped bytes as <code>Memory Size</code>.</p><p>Spill classification: ≥80% tasks with zero spill → <code>skew</code>; &lt;20% zero → <code>volume</code>; else <code>unclassified</code>. The classification badge is always shown in both compact and expanded spill widget states. This is independent of the magnitude tiers above: classification says <em>what kind</em> of spill, magnitude says <em>how much</em>.</p><h4 id="evidence-fields-stagefailed-retrywaste" tabindex="-1">Evidence fields (<code>stageFailed</code> / <code>retryWaste</code>) <a class="header-anchor" href="#evidence-fields-stagefailed-retrywaste" aria-label="Permalink to &quot;Evidence fields (`stageFailed` / `retryWaste`)&quot;">​</a></h4><p>Beyond a scalar <code>metric</code>/<code>value</code>, both entries&#39; <code>detect()</code> attach to the finding:</p><ul><li><code>numTasks</code>: <code>stage.taskCount</code> at detection time.</li><li><code>memoryBytesSpilled</code>: <code>stage.memoryBytesSpilled</code> at detection time.</li><li><code>stageFailed</code> only: <code>failedTaskDetails</code>: up to 20 <code>FailedTaskSample</code> records (<code>taskId</code>, <code>attemptNumber</code>, <code>host</code>, <code>executorId</code>, <code>reason</code>, <code>peakExecMem</code>, <code>memSpilled</code>, <code>shuffleWrite</code>) for tasks still marked failed when the stage was finalized (<code>finalizeStage</code>, <code>stage-quantiles.ts</code>).</li><li><code>retryWaste</code> only: <code>retriedTaskDetails</code>: up to 20 <code>FailedTaskSample</code> records for attempts discarded by the retry-dedup logic in <code>accumulateTask</code> (<code>event-handlers.ts</code>): captured at the moment they&#39;d otherwise be thrown away, since by finalize time only the winning attempt survives.</li></ul><p>Both sample arrays are capped at 20 entries, filled in first-encountered order (finalize order for <code>failedTaskDetails</code>, discard order for <code>retriedTaskDetails</code>), not spread across distinct hosts/executors: a stage with failures clustered on one bad host could fill the cap before a more informative failure elsewhere in the stage is ever sampled.</p><h4 id="evidence-fields-failures" tabindex="-1">Evidence fields (<code>failures</code>) <a class="header-anchor" href="#evidence-fields-failures" aria-label="Permalink to &quot;Evidence fields (`failures`)&quot;">​</a></h4><ul><li><code>dominantReason</code>: the most frequent end-reason tag (<code>ExceptionFailure</code>, <code>ExecutorLostFailure</code>, ...) among tasks still failed at finalize.</li><li><code>dominantError</code>: the error behind that tag, from its largest failure group: the exception class, or <code>&lt;tag&gt;: &lt;loss reason&gt;</code>; falls back to the tag. The recommendation names it and never embeds a message.</li><li><code>failureGroups</code>: up to 5 <code>TaskFailureGroup</code>s (<code>reason</code>, <code>className</code>, <code>message</code>, <code>lossReason</code>, <code>stackExcerpt</code>, <code>count</code>), most frequent first: one group per distinct tag, class, message and loss reason, one excerpt each.</li><li><code>otherFailedTasks</code>: failed tasks that no shown group covers.</li></ul><p><code>packages/core/src/task-failure.ts</code> reads the details from <code>Task End Reason</code> (<code>Class Name</code>, <code>Description</code>, <code>Full Stack Trace</code>, <code>Loss Reason</code>, FetchFailed <code>Message</code>, <code>Kill Reason</code>) and bounds them at ingest: a message or loss reason to its first line (for a Python traceback, its final error line), 300 characters; an excerpt to the header, 8 frames, a Python traceback&#39;s error line and the last <code>Caused by:</code> line, 2000 characters in all. While parsing, a stage keeps at most 50 distinct failures (<code>StageRecord.failureDetails</code>, freed at finalize); later ones count only toward the tag. Redaction (<code>redact.ts</code>) replaces every <code>failureGroups[].message</code> and strips message text from <code>stackExcerpt</code>.</p>',23)])])}const m=t(s,[["render",r]]);export{u as __pageData,m as default};
@@ -1 +0,0 @@
1
- import{_ as o,o as t,c as a,a5 as s}from"./chunks/framework.DSg0KOwT.js";const g=JSON.parse('{"title":"Run comparison internals","description":"","frontmatter":{},"headers":[],"relativePath":"contributor-guide/architecture/state-and-history/run-comparison-internals.md","filePath":"contributor-guide/architecture/state-and-history/run-comparison-internals.md"}'),n={name:"contributor-guide/architecture/state-and-history/run-comparison-internals.md"};function c(d,e,i,r,l,h){return t(),a("div",null,[...e[0]||(e[0]=[s('<h1 id="run-comparison-internals" tabindex="-1">Run comparison internals <a class="header-anchor" href="#run-comparison-internals" aria-label="Permalink to &quot;Run comparison internals&quot;">​</a></h1><p>How the A/B comparison engine matches stages and builds a comparison.</p><h2 id="run-comparison" tabindex="-1">Run comparison <a class="header-anchor" href="#run-comparison" aria-label="Permalink to &quot;Run comparison&quot;">​</a></h2><p><code>packages/core/src/run-comparison.ts</code> is the whole A/B engine. The entry point <code>compareRuns(baseline, candidate)</code> takes two <code>{ label, snapshot }</code> run records (each <code>snapshot</code> a <code>SessionSnapshot</code>: <code>app</code>, <code>stages</code>, <code>executors</code>, <code>sql</code>, <code>jobs</code>, <code>runAggregates</code>, <code>evidenceAvailability</code>, <code>catalog</code>, <code>taskData</code>) and returns one plain object the view renders. It runs on already-parsed snapshots, with no worker involved.</p><ul><li><code>stageIdentity(stage, snapshot)</code> is a run-independent key: <code>normalizeStageName</code> (lowercased, digit-runs and long hex ids collapsed to <code>#</code>) joined with the stage&#39;s SQL-execution plan identity. That identity is scoped to only the plan nodes this stage&#39;s tasks were attributed to (<code>node.stageIds</code>), not the whole tree, so two stages sharing one SQL execution (e.g. a self-join&#39;s two Exchange stages) don&#39;t collapse onto one identity; it falls back to a bottom-up structural fingerprint of the whole resolved <code>planTree</code> (<code>planTreeIdentity</code>, <code>normalizeDetail</code>-normalized: the same normalizer <code>cachingOpportunity</code> uses in <code>packages/core/src/detectors.ts</code>) when a stage has no such attribution. The stage-to-plan mapping itself is <code>planNodesOfStage</code> in <code>packages/core/src/stage-plan-nodes.ts</code>, shared with <code>isPythonStage</code> (<code>python-stage.ts</code>), which the CLI&#39;s <code>metrics</code> block (<code>run-metrics.ts</code>) and the <code>tasksMostlyIdle</code> check both read. The CLI keys its per-stage metrics rows by this identity. <code>matchStages(baseSnap, candSnap)</code> indexes each run by that identity and pairs identities that map to exactly one stage on both sides. An identity colliding equally on both sides (same count) is also paired, positionally by sorted stage id: exact when comparing a run against itself (every stage matches itself), a best-effort guess otherwise (two unrelated same-named stages with no SQL/attribution could get cross-paired). Collisions are still recorded in <code>collisionIdentities</code> even when resolved this way; a differing count leaves them there unpaired. <code>coverage</code> reports the matched fraction.</li><li><code>metricDeltas</code> computes whole-run aggregate deltas (wall-clock, spill, task skew p95, failed-task rate, GC, I/O bytes, executor count, ...) as plain sums over all stages, deliberately not gated on stage matching, since matching is unreliable on real logs. Each metric carries a <code>direction</code> (improvement/regression/unchanged) and an <code>unavailableReason</code> when a side lacks the field.</li><li><code>findingsDelta</code> tallies each run&#39;s <code>catalog</code> by <code>(rule × impact band)</code> and reports <code>introduced</code> vs <code>resolved</code> categories: a count diff, also matching-free.</li><li><code>alignStages(baseSnap, candSnap, { normalizePath })</code> in <code>packages/core/src/stage-alignment.ts</code> is the aligner: a pure function from two snapshots to the stage pairs, the unmatched stages, the Delta bookkeeping stages and the runtime coverage. <code>compareRuns</code> calls it and does not contain it, and every consumer of a comparison reads its <code>stagePairs</code> instead of comparing stage keys. It keys on <code>comparisonIdentity</code>, the <code>stageIdentity</code> recipe (shared in <code>packages/core/src/stage-identity.ts</code>) run with the comparison normalizer: <code>normalizeDetail</code>, then the targeted patches in <code>COMPARISON_PATCHES</code> (staging directories, dates, <code>IN</code> lists, file-index counts; column-list order is not normalized), with any caller-supplied patterns (<code>compileNormalizePatterns</code>) applied first to the text as Spark printed it. <code>stageIdentity</code> stays the exact key the CLI&#39;s per-stage metrics rows use, and <code>normalizeDetail</code> stays untouched, so findings do not move. Blanket numeric or path stripping is not part of the normalizer: it fused distinct stages of one run. The pairing rule is <code>matchStages</code>&#39; (equal counts per identity pair off by stage id), shared as <code>pairEqualCounts</code>. Stages whose scans only read the Delta log (<code>isDeltaBookkeepingStage</code>, built on <code>isDeltaLogRead</code> in <code>plan-summary.ts</code>) are set aside before pairing. A pair&#39;s <code>deltas</code> sum every attempt of the stage, the sums <code>withEarlierAttempts</code> gives the run totals. The result is <code>comparisonSchemaVersion</code> 1; <code>replanned</code> is always empty.</li><li>The per-stage skew deltas (<code>stageSkewDeltas</code>) read the aligner&#39;s pairs, the same ones the banner&#39;s coverage figure counts, and the pinned-stage panel reads <code>baseStages</code>/<code>candStages</code>. The old <code>matchStages</code> pairs still feed <code>matchedCoverage</code>, which keeps its count-based meaning and decides nothing.</li></ul><p><code>compareRuns</code> sets <code>confidence</code> from <code>runtimeCoverage</code>: <code>insufficient</code> when it is null (neither run recorded executor run time), <code>low</code> when it is under 0.9 (<code>RUNTIME_COVERAGE_THRESHOLD</code>) or the two app names differ (either condition alone is enough; both are weak signals, not a hard gate), <code>ok</code> otherwise. It is computed in <code>compareRuns</code>, not in <code>buildComparison</code>, so the dashboard, which calls <code>compareRuns</code> directly, gets the same result as the CLI and MCP. The view lives in <code>src/view/RunComparison.tsx</code> (the comparison page), <code>CompareLanding.tsx</code> (the two-slot baseline / candidate intake off the landing), and <code>PinnedStageDeltas.tsx</code> (the manual per-stage pinning panel fed by <code>baseStages</code>/<code>candStages</code>).</p>',6)])])}const u=o(n,[["render",c]]);export{g as __pageData,u as default};
@@ -1 +0,0 @@
1
- import{_ as o,o as c,c as d,a5 as a}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"CLI and MCP server","description":"","frontmatter":{},"headers":[],"relativePath":"contributor-guide/architecture/worker-protocol/cli-and-mcp.md","filePath":"contributor-guide/architecture/worker-protocol/cli-and-mcp.md"}'),t={name:"contributor-guide/architecture/worker-protocol/cli-and-mcp.md"};function s(r,e,n,i,l,h){return c(),d("div",null,[...e[0]||(e[0]=[a('<h1 id="cli-and-mcp-server" tabindex="-1">CLI and MCP server <a class="header-anchor" href="#cli-and-mcp-server" aria-label="Permalink to &quot;CLI and MCP server&quot;">​</a></h1><p>The headless analysis CLI and the MCP server, which run the same parser and detectors outside the browser.</p><h2 id="headless-analysis-cli" tabindex="-1">Headless analysis CLI <a class="header-anchor" href="#headless-analysis-cli" aria-label="Permalink to &quot;Headless analysis CLI&quot;">​</a></h2><p><code>packages/cli/bin/sparkforensics-analyze.mjs</code> runs the same parser + detector contracts outside the browser, for CI. It accepts a single event-log file or a rolling-log directory, drives <code>runParse</code>/<code>runParseFiles</code> (<code>packages/core/src/parser-worker.ts</code>) with a Node-only File-like shim (<code>nodeFileFromPath</code> in <code>packages/core/src/cli/collect-run.ts</code>), and writes the exact <code>buildEvidenceReport</code> JSON schema described above: one format, not a second. The shim reads each requested slice with a positioned <code>readSync</code>, so a local log is never loaded whole and has no 2 GiB size limit; <code>collectRun</code> closes every descriptor it opened once parsing settles, on success and on error. Alternatively, <code>--shs-base-url &lt;url&gt; --app-id &lt;id&gt; [--attempt-id &lt;id&gt;]</code> fetches the run from a Spark History Server instead (mutually exclusive with the positional file/directory argument), calling <code>resolveFromShs</code> (<code>packages/core/src/shs-load.ts</code>, shared with the MCP server&#39;s SHS source path below) directly; no dependency on the <code>packages/server</code> package. The three flags are checked first with <code>validateShsRequest</code> (<code>packages/core/src/shs-request.js</code>), and a malformed one is a usage error (exit <code>2</code>) before any fetch. A failed SHS fetch reports its message to <code>stderr</code> and exits <code>4</code>, same as a candidate file that can&#39;t be parsed.</p><p>Regression budgets beyond the <code>--max-regression-pct</code> pair come from repeated <code>--regression-budget &lt;metric&gt;:&lt;pct&gt;</code> flags and a <code>--budgets</code> file, parsed in <code>packages/core/src/cli/regression-budgets.ts</code> and passed to <code>evaluateBudgets()</code> as <code>regressionBudgets</code>; the legacy pair is one more budget, and a metric budgeted twice is a usage error. With two or more positional candidates (or <code>--format ndjson</code>), the CLI&#39;s <code>runMultiLog</code> parses and analyzes the baseline once, then evaluates each candidate in turn and writes one NDJSON line per candidate (<code>log</code>, <code>status</code>, <code>exitCode</code>, <code>error</code>, <code>budgets</code>, <code>candidate</code>, <code>comparison</code>). A candidate that can&#39;t be read or parsed gets a <code>status: &quot;error&quot;</code> line with <code>exitCode</code> <code>4</code> (<code>6</code> for an internal failure on that candidate); the exit code is the worst line (<code>6</code> &gt; <code>5</code> &gt; <code>4</code> &gt; <code>1</code> &gt; <code>3</code> &gt; <code>0</code>, <code>EXIT_SEVERITY</code>). A baseline that can&#39;t be read exits <code>5</code> with no lines. Under <code>--redact</code>, <code>log</code> and the <code>stderr</code> prefixes are <code>candidate-&lt;n&gt;</code> (1-based argument position) and an error line&#39;s message is generic, so no candidate path, which usually carries the app id, reaches the output. In both modes <code>--redact</code> also swaps the <code>stderr</code> text of a baseline or candidate failure (SHS fetch included) for <code>redactedFailure</code>&#39;s role-only message.</p><p>Optional CLI-flag budgets (<code>--max-runtime &lt;ms&gt;</code>, <code>--max-spill &lt;gb&gt;</code>, <code>--max-skew &lt;ratio&gt;</code>, <code>--max-failed-task-rate &lt;pct&gt;</code>, <code>--min-efficiency &lt;pct&gt;</code>) are evaluated in <code>packages/core/src/cli/budgets.ts</code> against the existing finding catalog (<code>analyze()</code>) and <code>computeEfficiencyModel</code>; there is no second rule engine. <code>--min-efficiency</code> compares <code>100 - wastagePct</code> (busy core time, the complement of the dashboard&#39;s Unused core time) and says so in its detail (&quot;Busy core time 26% below budget 90%.&quot;); it is not the Scorecard&#39;s Efficiency tile (<code>stagesActive / total</code>). A budget whose required evidence is missing (e.g. the run never emitted <code>ApplicationEnd</code>, or has no usable per-task <code>runAggregates</code>) is reported as inconclusive (<code>stderr</code> warning) rather than silently passing, and gets its own exit code distinct from both pass and violation. Exit codes: <code>0</code> pass, <code>1</code> a configured budget was violated, <code>2</code> a usage error (unknown or value-less flag, unknown <code>--regression-metric</code> key, an unreadable or invalid <code>--thresholds</code> or <code>--budgets</code> file), <code>3</code> no violations but at least one budget was inconclusive, <code>4</code> the candidate could not be read or parsed (or its SHS fetch failed), <code>5</code> the <code>--baseline</code> could not be read or parsed, <code>6</code> an internal error (an unhandled failure, a failed <code>--export-html</code> export). <code>main</code> catches anything unhandled and exits <code>6</code> rather than letting Node exit <code>1</code>, which would read as a violation. When several apply the worst wins: <code>6</code>, <code>5</code>, <code>4</code>, <code>1</code>, <code>3</code>, <code>0</code>; a violation always wins over an inconclusive result in the same run (exit <code>1</code>, not <code>3</code>). <code>evaluateBudgets()</code> also always adds an inconclusive <code>run-complete</code> result when the catalog has an <code>incompleteRun</code> finding, so a run with no <code>ApplicationEnd</code> exits <code>3</code> even with no budget flags. With <code>--baseline</code>, the absolute budgets and this check apply to the candidate run; the MCP <code>evaluate_budgets</code> tool below uses the same function with the same roles.</p><h2 id="mcp-server" tabindex="-1">MCP server <a class="header-anchor" href="#mcp-server" aria-label="Permalink to &quot;MCP server&quot;">​</a></h2><p><code>packages/core/src/mcp-server-factory.ts</code>&#39;s <code>createMcpServer()</code> registers 8 tools: <code>list_runs</code> (candidate runs in a local directory or on a Spark History Server, to pick one before diagnosing it), <code>diagnose_run</code> (thresholded findings + remediation text), <code>get_run_summary</code> (app/stage/job/sql counts and duration, no findings), <code>compare_runs</code> (the comparison verdict from <code>comparisonVerdict</code> in <code>packages/core/src/comparison-verdict.ts</code>, the dashboard comparison page&#39;s own headline, plus categorized findings delta + metric deltas between two runs; <code>CompareRunsResult.jobOutcomes</code> carries each run&#39;s failed jobs and incomplete flag for it), <code>evaluate_budgets</code> (pass/fail budget thresholds against one run, optionally with a second run for regression/fail-on-introduced budgets: the MCP side of the CLI&#39;s <code>evaluateBudgets()</code> gating), <code>get_finding_evidence</code> (raw evidence bundle for one finding, for drill-down after <code>diagnose_run</code>), <code>get_finding_documentation</code> (detection/tuning reference docs for one finding type, independent of any run), and <code>get_reference_doc</code> (a full tuning-reference chapter or bottleneck page by doc anchor, e.g. <code>#joins</code>). None re-implement detector logic: <code>list_runs</code> lists candidates (<code>packages/core/src/list-runs.ts</code>), and the rest repackage <code>analyze</code>/<code>buildEvidenceReport</code>/<code>compareRuns</code>/<code>captureSnapshot</code>/<code>evaluateBudgets</code> from <code>packages/core/src/mcp-tools.ts</code>, which resolves a <code>source</code> (event-log <code>path</code>, or an SHS <code>shsBaseUrl</code>/<code>appId</code>/<code>attemptId</code> triple) into a cached <code>AppModel</code>. The SHS source path calls <code>resolveFromShs</code> (<code>packages/core/src/shs-load.ts</code>, also used directly by the CLI&#39;s <code>--shs-base-url</code> mode above), which reuses two extractions shared with the browser ingestion flow: <code>fetchShsEventLog</code> (<code>packages/core/src/proxy.js</code>, the same upstream fetch the local server&#39;s <code>/shs-proxy</code> route uses) fetches the zip, and <code>decodeShsArchive</code> (<code>packages/core/src/shs-fetch.ts</code>, re-exported from <code>parser-worker.ts</code>&#39;s barrel) streams it through the parser as worker messages, which <code>collectViaDispatch</code> assembles into an <code>AppModel</code>. <code>runParseFromUrl</code> calls the same <code>decodeShsArchive</code> after fetching through the proxy.</p><p>Two transports connect to that one factory: <code>packages/mcp/bin/sparkforensics-mcp.mjs</code> (stdio, for local MCP clients) and <code>packages/server/index.js</code>&#39;s <code>/mcp</code> route (streamable HTTP, for the local server). The run cache (<code>packages/core/src/mcp-tools.ts</code>) is a module-level LRU (cap 8 and 15-minute idle TTL by default, overridable via <code>SPARKFORENSICS_MCP_CACHE_CAP</code> and <code>SPARKFORENSICS_MCP_CACHE_TTL_MS</code>, lazily swept on access) keyed by resolved source (path plus mtime, ctime and size, or SHS baseUrl+appId+attemptId), so a client mints a <code>runId</code> once via <code>resolveOrCreateRun</code> and reuses it across subsequent tool calls instead of re-parsing.</p><h3 id="one-output-builder-for-the-cli-and-mcp" tabindex="-1">One output builder for the CLI and MCP <a class="header-anchor" href="#one-output-builder-for-the-cli-and-mcp" aria-label="Permalink to &quot;One output builder for the CLI and MCP&quot;">​</a></h3><p>Neither surface lists the fields of a run or a comparison by hand. The CLI&#39;s JSON and the MCP tools call the same core functions:</p><ul><li><code>buildComparisonOutput()</code> in <code>packages/core/src/comparison-output.ts</code> diffs the two runs, applies <code>--redact</code>/<code>redact</code> itself (so no surface can budget or report an unredacted comparison by skipping it), and returns the redacted <code>CompareRunsResult</code> for the budget checks and the Markdown renderer, plus <code>comparisonOutput()</code>&#39;s projection: <code>verdict</code>, <code>confidence</code>, <code>reason</code>, <code>matchedCoverage</code>, <code>runtimeCoverage</code>, <code>metrics</code>, <code>findings</code>, and the comparison block <code>comparisonSchemaVersion</code>, <code>stagePairs</code>, <code>unmatched</code>, <code>replanned</code>, <code>bookkeepingStageIds</code> (built by the aligner, see <a href="./../state-and-history/run-comparison-internals.html">Run comparison internals</a>). <code>comparisonOutput()</code> takes a <code>view</code>: <code>full</code> (the CLI) carries every field, <code>summary</code> (MCP&#39;s default) leaves out <code>stagePairs</code>, which <code>compare_runs</code> adds back for <code>include: [&#39;stagePairs&#39;]</code>. <code>buildComparisonOutput()</code> also takes <code>normalizePath</code>, the CLI&#39;s repeatable <code>--normalize-path</code> and <code>compare_runs</code>&#39; <code>normalizePath</code>: it reaches <code>compareRuns</code> the way <code>thresholds</code> reaches <code>analyze()</code> and stays out of the run cache key, because it only affects the comparison. The CLI validates the patterns before any log is parsed (exit 2) and MCP before any run is resolved. That projection is the CLI&#39;s <code>comparison</code> object and the shared part of <code>compare_runs</code>. <code>compare_runs</code> also returns <code>metricDeltas</code> and <code>findingsDelta</code>, deprecated aliases of <code>metrics</code> and <code>findings</code> kept for one release.</li><li><code>runOutputBlocks()</code> in <code>packages/core/src/run-output.ts</code> builds the report&#39;s <code>metrics</code> and <code>effectiveConf</code> blocks, redacted from the redacted run, for both the CLI report and <code>diagnose_run</code>, which also returns the report&#39;s <code>writeTargets</code>.</li></ul><p>Neither surface wraps the other: MCP handlers hold a run cache and report errors by code, while the CLI maps failures to exit codes and owns stdout. Adding a field to the projection adds it to both. <code>packages/cli/test/parity.test.js</code> runs the CLI&#39;s <code>main()</code> and the MCP tools through an in-memory client on public corpus logs, flattens both outputs and diffs their key sets and values. The differences that stay (the MCP run handles, the deprecated aliases, the CLI&#39;s <code>schemaVersion</code>) are in a commented allowlist in that file, and an entry that explains no difference fails the test.</p><p>Every tool failure comes back as <code>{isError: true, content: [...], structuredContent: {code}}</code>, never an HTTP-status-shaped error; <code>code</code> is one of the 5 existing SHS codes (<code>SHS_ERROR_CODES</code>, <code>packages/core/src/shs-request.js</code>) plus <code>run-not-found</code>, <code>finding-not-found</code>, <code>invalid-event-log</code> (also covers a nonexistent <code>path</code> source), <code>archive-too-large</code> (SHS archive over the <code>SPARKFORENSICS_MAX_ARCHIVE_BYTES</code> byte cap, default 1 GiB, because the MCP path buffers the whole archive in memory, unlike the streaming <code>/shs-proxy</code> route), <code>invalid-type</code>, <code>invalid-anchor</code> (documentation tools), <code>directory-not-found</code>, <code>invalid-date-filter</code>, and <code>invalid-shs-base-url</code> (<code>list_runs</code>). An error without a code reports <code>access-or-upstream-failure</code>. A stalled SHS archive body fails as <code>upstream-unreachable</code> after <code>SPARKFORENSICS_SHS_TIMEOUT_MS</code> (default 30 s) without data.</p><p><code>scripts/vendor-core.mjs</code> (shared by <code>packages/cli</code>, <code>packages/mcp</code>, and <code>packages/server</code>&#39;s <code>prepack</code> scripts) vendors <code>packages/core/src/</code> wholesale into each package&#39;s own <code>vendor-core/</code> at pack time, pre-stripping TypeScript to plain <code>.js</code> (Node&#39;s native TS stripping refuses to run on <code>.ts</code> files under <code>node_modules</code>, which is exactly where a published <code>vendor-core/</code> lands). Each package&#39;s bin/entry point resolves its needed module through <code>packages/core/src/load-vendored.js</code>: from <code>vendor-core/</code> if present, else from the real <code>packages/core/src/</code> sibling loaded as <code>.ts</code> directly. In a monorepo checkout a leftover <code>vendor-core/</code> is used only while its <code>core-source-hash.txt</code> (written by <code>vendor-core.mjs</code>) matches <code>packages/core/src</code>; otherwise the bin warns on stderr and loads <code>packages/core/src</code> (<code>packages/server/index.js</code> mirrors <code>resolveStaticRoot</code>&#39;s <code>public/</code>-vs-<code>../dist</code> pattern for this same fallback).</p>',15)])])}const g=o(t,[["render",s]]);export{p as __pageData,g as default};
@@ -1 +0,0 @@
1
- import{_ as t,o as a,c as o,a5 as s}from"./chunks/framework.DSg0KOwT.js";const u=JSON.parse('{"title":"CI and automation","description":"","frontmatter":{},"headers":[],"relativePath":"user-guide/getting-started/ci-and-automation.md","filePath":"user-guide/getting-started/ci-and-automation.md"}'),d={name:"user-guide/getting-started/ci-and-automation.md"};function i(n,e,c,r,l,h){return a(),o("div",null,[...e[0]||(e[0]=[s("",82)])])}const g=t(d,[["render",i]]);export{u as __pageData,g as default};
@@ -1 +0,0 @@
1
- import{_ as o,o as t,c as a,a5 as n}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"Run comparison mode","description":"","frontmatter":{},"headers":[],"relativePath":"user-guide/run-comparison.md","filePath":"user-guide/run-comparison.md"}'),r={name:"user-guide/run-comparison.md"};function s(i,e,d,h,c,l){return t(),a("div",null,[...e[0]||(e[0]=[n('<h1 id="run-comparison-mode" tabindex="-1">Run comparison mode <a class="header-anchor" href="#run-comparison-mode" aria-label="Permalink to &quot;Run comparison mode&quot;">​</a></h1><p>Put two runs side by side to see whether a tuning change helped.</p><h2 id="starting-a-comparison" tabindex="-1">Starting a comparison <a class="header-anchor" href="#starting-a-comparison" aria-label="Permalink to &quot;Starting a comparison&quot;">​</a></h2><p>From the landing page, click <strong>Compare two runs</strong>. Two slots appear: <strong>Baseline</strong> and <strong>Candidate</strong>.</p><ol><li>Load a run into each slot the same way you&#39;d load a single run (drag and drop, <strong>Choose file</strong>, <strong>Choose rolling-log folder</strong>, or a History Server fetch).</li><li>Click <strong>Compare</strong>.</li></ol><p>The runs parse one after the other through the same background worker. When both finish, the comparison view opens.</p><p>Already looking at a run, for example the one before a change? Click <strong>Compare with another run</strong> in the top bar (or the <strong>More options</strong> menu on a phone). The compare view opens with that run as the baseline, so you only load the candidate, and the open run is not parsed again. <strong>Back to the run</strong> returns to its dashboard.</p><h2 id="reading-the-comparison" tabindex="-1">Reading the comparison <a class="header-anchor" href="#reading-the-comparison" aria-label="Permalink to &quot;Reading the comparison&quot;">​</a></h2><p>The page opens with a verdict: whether the candidate finished faster or slower than the baseline, and by how much (a change under 2% reads as &quot;about as long&quot;). When either run had failed jobs, the verdict leads with that instead, for example &quot;The candidate had 2 of 5 jobs fail (baseline: none)&quot; or &quot;The candidate&#39;s only job failed&quot;, and the run-time line follows. When either log has no end-of-run record (the run was killed or the log cut off), the verdict says how much run time each log covers, in a neutral tone, instead of calling the shorter run faster. It then lists which cost metrics got worse or better in the candidate (again ignoring changes under 2%), and which finding categories became more or less frequent. Volume and count metrics (input, output, tasks, executors) are left out, since more or less of them is not better or worse on its own. <strong>See where to start in the candidate</strong> opens the candidate&#39;s dashboard, whose own verdict names the first thing to fix.</p><p>The <strong>Metrics</strong> table covers the whole run: wall-clock duration, memory spill, task skew, failed-task rate, disk spill, GC time, input/output bytes, executor run-time, and task/executor counts, baseline against candidate with the change. <strong>Findings by category</strong> lists which finding types became more or less frequent in the candidate, per impact level, with the count in each run and the affected stages.</p><p>How stages are paired, and what the matching confidence means, is in <a href="./run-comparison/how-stages-are-matched.html">How stages are matched</a>.</p><p>For everything else, matching is manual: the <strong>Pinned per-stage deltas</strong> widget lets you pick one stage from the baseline and one from the candidate yourself and pin the pair, then shows their duration, executor run-time, GC time, memory/disk spill, input/output bytes, task count, and failed tasks side by side. Pin as many pairs as you want to check.</p><p>For either run&#39;s full dashboard, click <strong>View baseline dashboard</strong> or <strong>View candidate dashboard</strong>. <strong>← Back to comparison</strong> takes you back.</p><h2 id="comparing-without-the-dashboard" tabindex="-1">Comparing without the dashboard <a class="header-anchor" href="#comparing-without-the-dashboard" aria-label="Permalink to &quot;Comparing without the dashboard&quot;">​</a></h2><p>The same comparison runs headlessly, and opens with the same verdict: the positional run is the candidate, the <code>--baseline</code> run the baseline. The CLI&#39;s <code>--baseline &lt;local file or rolling-log dir&gt;</code> flag adds the comparison. In JSON the run&#39;s report moves under <code>candidate</code> and the comparison sits beside it under <code>comparison</code> (<code>comparison.verdict</code> is the headline); in Markdown the comparison section follows the report. <code>--baseline</code> can also gate a build on the comparison (<code>--max-regression-pct</code>, <code>--fail-on-introduced</code>; see <a href="./getting-started/ci-and-automation.html#ci-and-automation">Getting started</a>). The MCP server&#39;s <code>compare_runs</code> and <code>evaluate_budgets</code> tools do the same for an AI assistant; see <a href="./mcp-tools.html">MCP tools reference</a>.</p>',15)])])}const m=o(r,[["render",s]]);export{p as __pageData,m as default};
@@ -1 +0,0 @@
1
- import{_ as o,o as t,c as a,a5 as n}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"Run comparison mode","description":"","frontmatter":{},"headers":[],"relativePath":"user-guide/run-comparison.md","filePath":"user-guide/run-comparison.md"}'),r={name:"user-guide/run-comparison.md"};function s(i,e,d,h,c,l){return t(),a("div",null,[...e[0]||(e[0]=[n("",15)])])}const m=o(r,[["render",s]]);export{p as __pageData,m as default};
@@ -1 +0,0 @@
1
- import{_ as t,o as a,c as n,a5 as r}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"How stages are matched","description":"","frontmatter":{},"headers":[],"relativePath":"user-guide/run-comparison/how-stages-are-matched.md","filePath":"user-guide/run-comparison/how-stages-are-matched.md"}'),o={name:"user-guide/run-comparison/how-stages-are-matched.md"};function s(i,e,d,h,c,l){return a(),n("div",null,[...e[0]||(e[0]=[r('<h1 id="how-stages-are-matched" tabindex="-1">How stages are matched <a class="header-anchor" href="#how-stages-are-matched" aria-label="Permalink to &quot;How stages are matched&quot;">​</a></h1><p>How SparkForensics pairs stages between a baseline and a candidate, and how sure it is about the pairing.</p><p>Stage-level detail depends on pairing a stage in the baseline with its counterpart in the candidate. A stage ID is no help: AQE&#39;s runtime replanning and a different submission order change it from run to run. So a stage is identified by what it ran: its normalized name plus the plan nodes its tasks were attributed to.</p><p>Before two stages are compared, the text that differs between two runs of the same job is rewritten to a fixed form:</p><ul><li>random per-run staging directories in paths (a 6-8 character segment that mixes cases, or letters with digits: a real directory named <code>output01</code> is rewritten too)</li><li>dates and timestamps, such as the run date in a predicate</li><li><code>IN</code> and <code>INSET</code> lists, which grow with the processing window</li><li>file counts in a file index, such as <code>DeltaLogFileIndex(4 paths)</code></li></ul><p>Numbers, paths and string literals in general are not rewritten: two stages that differ in a filter constant or an input path stay different stages. The order of a column list is not normalized either, so a grouping set that a program emits in a different order each time can leave those stages unpaired. Text that is specific to your runs, such as an output directory that includes the run&#39;s variant name, can be rewritten with the CLI&#39;s <code>--normalize-path</code> flag or the MCP <code>compare_runs</code> tool&#39;s <code>normalizePath</code> parameter. Despite the name it is a generic regular expression, not only for paths, and it affects only which stages pair, never the findings.</p><p>Stages that share an identity pair off in stage-ID order when each run has the same number of them. A different number on each side (a loop that ran 14 times in one run and 15 in the other) leaves them all unpaired.</p><p>Stages that only read the Delta transaction log or its checkpoints are table bookkeeping, not query work. They are listed apart from the pairs and counted in neither the pairs, the unmatched stages nor the coverage.</p><h2 id="runtime-coverage-and-confidence" tabindex="-1">Runtime coverage and confidence <a class="header-anchor" href="#runtime-coverage-and-confidence" aria-label="Permalink to &quot;Runtime coverage and confidence&quot;">​</a></h2><p>The comparison reports the <strong>runtime coverage</strong>: the share of both runs&#39; executor run time that sits in paired stages, with every task attempt counted (failed and speculative ones too). It is weighted by run time, not stage count, because a few unmatched heavy stages matter more than many matched small ones. The share of stages matched, by count, is reported too (<code>matchedCoverage</code>) and does not decide the confidence.</p><p>The confidence is:</p><ul><li><code>ok</code> when the runs have the same application name and the runtime coverage is at least 90%.</li><li><code>low</code> when the application names differ, or the runtime coverage is under 90%. A warning banner says which, for example &quot;Only 62% of executor run time is in matched stages, so per-stage rows mostly compare unrelated work&quot;. The metric deltas above it still hold: they do not depend on stage pairing.</li><li><code>insufficient</code> when neither run recorded any executor run time, so there is no work to compare. The banner says so.</li></ul><p>The <strong>Per-stage task skew</strong> table lists only paired stages, and the page states the share of executor run time they hold. Confidence does not change the exit code of a CLI budget.</p>',13)])])}const m=t(o,[["render",s]]);export{p as __pageData,m as default};
@@ -1 +0,0 @@
1
- import{_ as t,o as a,c as n,a5 as r}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"How stages are matched","description":"","frontmatter":{},"headers":[],"relativePath":"user-guide/run-comparison/how-stages-are-matched.md","filePath":"user-guide/run-comparison/how-stages-are-matched.md"}'),o={name:"user-guide/run-comparison/how-stages-are-matched.md"};function s(i,e,d,h,c,l){return a(),n("div",null,[...e[0]||(e[0]=[r("",13)])])}const m=t(o,[["render",s]]);export{p as __pageData,m as default};
@@ -1 +0,0 @@
1
- import{_ as a,o as t,c as o,a5 as r}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"Understanding findings","description":"","frontmatter":{},"headers":[],"relativePath":"user-guide/understanding-findings.md","filePath":"user-guide/understanding-findings.md"}'),s={name:"user-guide/understanding-findings.md"};function i(n,e,l,d,c,h){return t(),o("div",null,[...e[0]||(e[0]=[r('<h1 id="understanding-findings" tabindex="-1">Understanding findings <a class="header-anchor" href="#understanding-findings" aria-label="Permalink to &quot;Understanding findings&quot;">​</a></h1><p>Every flagged problem carries a short ALL-CAPS tag. This page has one entry per tag: what it means, and what to do about it.</p><p>Each tag links to the matching section of the Spark <a href="./../tuning-reference/">tuning reference</a>, opened in an in-app <strong>Reference</strong> panel next to the board. <code>INCMP</code> has no reference section, so its tag opens this page instead. Turn on <a href="./getting-started/reading-the-dashboard.html#advanced-view">Advanced view</a> to also see a finding&#39;s confidence when it is below high, and a page icon beside the tag that opens this page&#39;s entry. Some tags point into another tag&#39;s reference page because the material overlaps: <code>SFAIL</code> shares <code>FAIL</code>&#39;s section; <code>PART</code> is on the <code>SHFL</code> page, <code>SPEC</code> on <code>STRAG</code>, <code>SHAPE</code> on <code>SKEW</code>, <code>SLOW</code> on <code>HOST</code>, and <code>CACHE</code>/<code>LOCAL</code> on <code>UTIL</code>.</p><h2 id="per-stage" tabindex="-1">Per-stage <a class="header-anchor" href="#per-stage" aria-label="Permalink to &quot;Per-stage&quot;">​</a></h2><h3 id="skew" tabindex="-1"><code>SKEW</code>: Task skew <a class="header-anchor" href="#skew" aria-label="Permalink to &quot;`SKEW`: Task skew {#skew}&quot;">​</a></h3><p>A small number of tasks take much longer than their peers in the same stage. For join-driven skew, enable AQE skew-join handling (<code>spark.sql.adaptive.skewJoin.enabled</code>); otherwise salt the key or repartition on a better key. Flagged when P95 task time (the longest task, on a stage with fewer than 20 tasks) exceeds 3x the median and the recoverable tail is at least 0.5% of the run.</p><h3 id="shfl" tabindex="-1"><code>SHFL</code>: Shuffle I/O <a class="header-anchor" href="#shfl" aria-label="Permalink to &quot;`SHFL`: Shuffle I/O {#shfl}&quot;">​</a></h3><p>Tasks move a large amount of intermediate data between stages. Raise <code>spark.sql.shuffle.partitions</code>, or add a broadcast join. Only flagged on stages that take at least 0.5% of the run.</p><h3 id="spill" tabindex="-1"><code>SPILL</code>: Memory and disk spill <a class="header-anchor" href="#spill" aria-label="Permalink to &quot;`SPILL`: Memory and disk spill {#spill}&quot;">​</a></h3><p>Tasks are writing data out of memory, which slows execution. Two spill patterns get flagged differently: skew spill, where a few heavy tasks spill while most don&#39;t (rebalance partitioning), and volume spill, where most tasks spill because the data genuinely exceeds available memory (add partitions or executor memory). Only flagged on stages that take at least 0.5% of the run.</p><h3 id="gc" tabindex="-1"><code>GC</code>: Garbage collection pressure <a class="header-anchor" href="#gc" aria-label="Permalink to &quot;`GC`: Garbage collection pressure {#gc}&quot;">​</a></h3><p>Tasks spend more than 10% of executor run time reclaiming memory. Reduce object creation: use primitive types, avoid UDFs, or raise executor memory. A stage with GC below 5% gets an informational note that executor memory may be over-provisioned, only on stages that take at least 0.5% of the run. Both need at least 10 s of executor run time on the stage.</p><h3 id="fail" tabindex="-1"><code>FAIL</code>: Failed tasks <a class="header-anchor" href="#fail" aria-label="Permalink to &quot;`FAIL`: Failed tasks {#fail}&quot;">​</a></h3><p>Tasks fail often enough to affect the stage. Failed tasks point to executor instability or data-driven errors. The finding names the dominant error: the exception class, or the executor loss reason (for example &quot;Container killed by YARN for exceeding memory limits&quot;). It lists up to five distinct failures, each with its message and a short stack excerpt. With redaction on, messages become <code>[redacted]</code> and message lines inside excerpts are dropped, since they can carry file paths and data values; class names, stack frames and the executor loss reason stay (hosts in it are pseudonymized).</p><h3 id="sfail" tabindex="-1"><code>SFAIL</code>: Failed stage <a class="header-anchor" href="#sfail" aria-label="Permalink to &quot;`SFAIL`: Failed stage {#sfail}&quot;">​</a></h3><p>A stage attempt failed outright rather than losing individual tasks within it. Inspect the driver log for the failure reason and the job that triggered it. With redaction on, the failure reason becomes <code>[redacted]</code>, since it can carry file paths and data values.</p><h3 id="strag" tabindex="-1"><code>STRAG</code>: Straggler tasks <a class="header-anchor" href="#strag" aria-label="Permalink to &quot;`STRAG`: Straggler tasks {#strag}&quot;">​</a></h3><p>A few tasks run much slower than the rest of their stage. Rule out a GC pause or a slow shuffle fetch before assuming a hardware issue; if a skewed key is the real cause, that&#39;s a candidate for AQE&#39;s skew-join handling. Only flagged on stages that take at least 0.5% of the run.</p><h3 id="spec" tabindex="-1"><code>SPEC</code>: Speculation waste <a class="header-anchor" href="#spec" aria-label="Permalink to &quot;`SPEC`: Speculation waste {#spec}&quot;">​</a></h3><p>Speculative task attempts used a lot of executor time without confirming a genuine straggler. Self-flags a confidence that scales with how far the wasted time sits past the threshold. If task durations are just naturally variable rather than genuine stragglers, tune <code>spark.speculation.multiplier</code>/<code>spark.speculation.quantile</code>.</p><h3 id="retry" tabindex="-1"><code>RETRY</code>: Retry waste <a class="header-anchor" href="#retry" aria-label="Permalink to &quot;`RETRY`: Retry waste {#retry}&quot;">​</a></h3><p>Repeated task attempts ate into execution time even though the stage completed. Investigate executor loss or fetch failures.</p><h3 id="tiny" tabindex="-1"><code>TINY</code>: Tiny tasks <a class="header-anchor" href="#tiny" aria-label="Permalink to &quot;`TINY`: Tiny tasks {#tiny}&quot;">​</a></h3><p>Many very short tasks add scheduling overhead out of proportion to the work each one does. Repartition to fewer, larger tasks. Only flagged on stages that take at least 0.5% of the run.</p><h3 id="part" tabindex="-1"><code>PART</code>: Partition sizing <a class="header-anchor" href="#part" aria-label="Permalink to &quot;`PART`: Partition sizing {#part}&quot;">​</a></h3><p>Shuffle partitions are too large, too uneven, or too few for the work. A single shuffle partition over 5 GB, for example, will OOM or spill heavily: repartition to break it up before the stage runs.</p><h3 id="slow" tabindex="-1"><code>SLOW</code>: Stage slowness <a class="header-anchor" href="#slow" aria-label="Permalink to &quot;`SLOW`: Stage slowness {#slow}&quot;">​</a></h3><p>A stage ran for 15 minutes or more and no slow host was flagged on it. It can appear alongside other findings on the same stage. Often a partition-count problem: raise parallelism via <code>spark.sql.shuffle.partitions</code> or <code>spark.default.parallelism</code>, or check for a large per-task data volume driving heavy shuffle and spill.</p><h3 id="shape" tabindex="-1"><code>SHAPE</code>: Stage shape <a class="header-anchor" href="#shape" aria-label="Permalink to &quot;`SHAPE`: Stage shape {#shape}&quot;">​</a></h3><p>The stage has an inefficient task count, output shape (output more than 10× input), or task-to-stage balance: one straggler task running for more than half the stage&#39;s wall-clock time and over 3× the median task, so it alone sets when the stage ends. A too-low task count and a straggler are only flagged on stages that take at least 0.5% of the run.</p><h3 id="host" tabindex="-1"><code>HOST</code>: Slow host <a class="header-anchor" href="#host" aria-label="Permalink to &quot;`HOST`: Slow host {#host}&quot;">​</a></h3><p>One executor is much slower than its peers, or carries most of the stage&#39;s task time or bytes. It may just hold data locality for its tasks or carry one heavy stage, rather than a hardware fault. Enable <code>spark.speculation</code> to relaunch a lagging task automatically. Only flagged on stages that take at least 0.5% of the run.</p><h2 id="app-level" tabindex="-1">App-level <a class="header-anchor" href="#app-level" aria-label="Permalink to &quot;App-level&quot;">​</a></h2><h3 id="cold" tabindex="-1"><code>COLD</code>: Executor cold start <a class="header-anchor" href="#cold" aria-label="Permalink to &quot;`COLD`: Executor cold start {#cold}&quot;">​</a></h3><p>The first stage waited more than 30 s for an executor. Keep a warm pool of executors, or, with dynamic allocation, raise <code>spark.dynamicAllocation.minExecutors</code>/<code>initialExecutors</code> so the app doesn&#39;t scale up from zero.</p><h3 id="util" tabindex="-1"><code>UTIL</code>: Low utilization <a class="header-anchor" href="#util" aria-label="Permalink to &quot;`UTIL`: Low utilization {#util}&quot;">​</a></h3><p>Allocated executors sit idle for a large share of the application run. Consider a smaller cluster, or enable dynamic allocation.</p><h3 id="mem" tabindex="-1"><code>MEM</code>: Memory utilization <a class="header-anchor" href="#mem" aria-label="Permalink to &quot;`MEM`: Memory utilization {#mem}&quot;">​</a></h3><p>Executor memory or core capacity may be over- or under-provisioned: more than 50% of available core time ran no task, an executor&#39;s heap peaked above 95% of its allocation, or it stayed below 70%. Some detail here needs <code>spark.eventLog.logStageExecutorMetrics=true</code> on the run being analyzed; without it, per-executor memory usage can&#39;t be broken down. Review <code>spark.executor.memory</code> and executor count if allocated memory sat largely idle over the run. That idle-memory variant self-flags a confidence that scales with how far the estimated waste sits past a 1.5x buffer: it estimates waste from allocated memory-time versus task run time (not measured heap usage). Check it against the Spark UI before resizing anything.</p><h3 id="cache" tabindex="-1"><code>CACHE</code>: Caching opportunity <a class="header-anchor" href="#cache" aria-label="Permalink to &quot;`CACHE`: Caching opportunity {#cache}&quot;">​</a></h3><p>A reusable dataset (re-read via the same SQL relation more than once, or a join/union result recomputed by two or more executions, matched by plan shape) may be worth persisting between stages. Self-flags a confidence that scales with how many executions reuse the same relation: reuse is only inferred, from plan-scan identity across SQL executions, so confirm the reads really do hit the same data before you cache anything.</p><h3 id="cstor" tabindex="-1"><code>CSTOR</code>: Cache storage <a class="header-anchor" href="#cstor" aria-label="Permalink to &quot;`CSTOR`: Cache storage {#cstor}&quot;">​</a></h3><p>A persisted dataset is not fully cached in memory, or is spilling to disk. Raise executor memory, or shrink the cached dataset.</p><p>The cached-partition counts and sizes come from <code>SparkListenerBlockUpdated</code> events, which Spark writes only when <code>spark.eventLog.logBlockUpdates.enabled=true</code>. Since Spark 2.3 the RDD storage figures in stage-submission events are always 0, and they are used only as a fallback. When a Spark 2.3+ run persists RDDs but its log has neither and block-update logging was off, the check reports that the cache could not be checked instead of passing it: turn <code>spark.eventLog.logBlockUpdates.enabled</code> on and rerun to measure eviction and disk spillover.</p><h3 id="local" tabindex="-1"><code>LOCAL</code>: Core usage locality <a class="header-anchor" href="#local" aria-label="Permalink to &quot;`LOCAL`: Core usage locality {#local}&quot;">​</a></h3><p>Tasks run without process- or node-local data placement more often than expected. Check executor/data colocation. Self-flags a confidence that scales with the non-local ratio and sample size: the thresholds are our own noise floor for this metric.</p><h3 id="chrn" tabindex="-1"><code>CHRN</code>: Autoscaling churn <a class="header-anchor" href="#chrn" aria-label="Permalink to &quot;`CHRN`: Autoscaling churn {#chrn}&quot;">​</a></h3><p>Executors are stood up and torn down again before they can do useful work: re-provisioning churn rather than normal scale-down. Raise <code>spark.dynamicAllocation.executorIdleTimeout</code>, or widen the <code>minExecutors</code>/<code>maxExecutors</code> bounds to reduce flapping. Self-flags a confidence that scales with how far the short-lived-executor share sits past the threshold.</p><h3 id="jobs" tabindex="-1"><code>JOBS</code>: Job failure rate <a class="header-anchor" href="#jobs" aria-label="Permalink to &quot;`JOBS`: Job failure rate {#jobs}&quot;">​</a></h3><p>A large share of completed jobs did not succeed. Inspect the driver log for the failed job(s) and the stage failures that triggered them.</p><h3 id="incmp" tabindex="-1"><code>INCMP</code>: Incomplete run <a class="header-anchor" href="#incmp" aria-label="Permalink to &quot;`INCMP`: Incomplete run {#incmp}&quot;">​</a></h3><p>This event log never recorded an <code>ApplicationEnd</code> event: capture stopped before the run finished (an in-flight job, a rotated-away log, or a cut-short capture). Every other finding and metric on the board reflects only what was captured up to that point, not the full run.</p><h2 id="configuration-scope" tabindex="-1">Configuration scope <a class="header-anchor" href="#configuration-scope" aria-label="Permalink to &quot;Configuration scope&quot;">​</a></h2><h3 id="cfg" tabindex="-1"><code>CFG</code>: Configuration audit <a class="header-anchor" href="#cfg" aria-label="Permalink to &quot;`CFG`: Configuration audit {#cfg}&quot;">​</a></h3><p>Flags configuration settings that may cause reliability or efficiency problems, independent of any one stage&#39;s behavior. Four checks run:</p><ul><li><code>spark.shuffle.service.enabled</code>: flagged when dynamic allocation is on but the external shuffle service is off, since shuffle data won&#39;t survive executor removal.</li><li><code>spark.dynamicAllocation.minExecutors</code>/<code>maxExecutors</code>: with dynamic allocation on, flagged when min exceeds max (reported on <code>minExecutors</code>) or when no max is set.</li><li><code>spark.serializer</code>: flagged when not set to Kryo (the default is the Java serializer); <code>org.apache.spark.serializer.KryoSerializer</code> is faster and produces smaller buffers.</li><li><code>spark.executor.memoryOverhead</code>: flagged when set below max(384 MiB, 10% of executor memory).</li></ul><h2 id="sql-scope" tabindex="-1">SQL scope <a class="header-anchor" href="#sql-scope" aria-label="Permalink to &quot;SQL scope&quot;">​</a></h2><h3 id="plan" tabindex="-1"><code>PLAN</code>: Plan advisor <a class="header-anchor" href="#plan" aria-label="Permalink to &quot;`PLAN`: Plan advisor {#plan}&quot;">​</a></h3><p>Flags patterns in the SQL execution plan worth reviewing. Four checks share this tag:</p><ul><li>Duplicate plan subtree: the same subtree recomputed more than once in the plan. When the repeats have the same shape but different filters, columns or tables, the finding stays informational and claims no time. Only flagged when the repeat&#39;s stages take at least 0.5% of the run.</li><li>Small files: one plan node reads or writes more than 100 files averaging under 3 MB. Compact upstream output, or coalesce before writing.</li><li>Under-broadcast: the smaller side of a Sort Merge Join looks well under the broadcast threshold; consider a <code>broadcast()</code> hint or raising <code>spark.sql.autoBroadcastJoinThreshold</code>.</li><li>Over-broadcast: a broadcast exceeds the 1 GB threshold; check for a misapplied broadcast hint or a misconfigured <code>spark.sql.autoBroadcastJoinThreshold</code>.</li></ul>',60)])])}const f=a(s,[["render",i]]);export{p as __pageData,f as default};
@@ -1 +0,0 @@
1
- import{_ as a,o as t,c as o,a5 as r}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"Understanding findings","description":"","frontmatter":{},"headers":[],"relativePath":"user-guide/understanding-findings.md","filePath":"user-guide/understanding-findings.md"}'),s={name:"user-guide/understanding-findings.md"};function i(n,e,l,d,c,h){return t(),o("div",null,[...e[0]||(e[0]=[r("",60)])])}const f=a(s,[["render",i]]);export{p as __pageData,f as default};