sparkforensics-server 0.5.2 → 0.6.0

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