sparkforensics-cli 0.1.0 → 0.2.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 (361) hide show
  1. package/README.md +6 -0
  2. package/bin/sparkforensics-analyze.mjs +113 -48
  3. package/export-template/docs/404.html +25 -0
  4. package/export-template/docs/assets/app.DQTZyGL1.js +1 -0
  5. package/export-template/docs/assets/aqe-loop.IwQSATHw.svg +1 -0
  6. package/export-template/docs/assets/aqe-loop.dark.DGbaxqJE.svg +1 -0
  7. package/export-template/docs/assets/broadcast-vs-shuffle.Db4WY1XK.svg +1 -0
  8. package/export-template/docs/assets/broadcast-vs-shuffle.dark.C7Bxs0mG.svg +1 -0
  9. package/export-template/docs/assets/cache-lifecycle.dark.B-hS7AgU.svg +1 -0
  10. package/export-template/docs/assets/cache-lifecycle.rEOVYQNU.svg +1 -0
  11. package/export-template/docs/assets/chunks/@localSearchIndexroot.DppnXnDE.js +1 -0
  12. package/export-template/docs/assets/chunks/VPLocalSearchBox.BkBIPFs6.js +9 -0
  13. package/export-template/docs/assets/chunks/duplicate-plan-subtree.dark.Cdp70QhV.js +1 -0
  14. package/export-template/docs/assets/chunks/framework.DSg0KOwT.js +20 -0
  15. package/export-template/docs/assets/chunks/retry-escalation-ladder.dark.DHipdJgZ.js +1 -0
  16. package/export-template/docs/assets/chunks/theme.DP0u1AUq.js +2 -0
  17. package/export-template/docs/assets/cold-start-timeline.DxC_Sc7w.svg +1 -0
  18. package/export-template/docs/assets/cold-start-timeline.dark.CZ17YcAG.svg +1 -0
  19. package/export-template/docs/assets/columnar-layout.PghGeOEA.svg +1 -0
  20. package/export-template/docs/assets/columnar-layout.dark.BVNlz0ff.svg +1 -0
  21. package/export-template/docs/assets/container-memory.DIO0AnIm.svg +1 -0
  22. package/export-template/docs/assets/container-memory.dark.CP-5zuCl.svg +1 -0
  23. package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.CWpj01WU.js +1 -0
  24. package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.CWpj01WU.lean.js +1 -0
  25. package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.CgzUsQ6W.js +1 -0
  26. package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.CgzUsQ6W.lean.js +1 -0
  27. package/export-template/docs/assets/contributor-guide_architecture_drill-down.md.BtPdlM7r.js +1 -0
  28. package/export-template/docs/assets/contributor-guide_architecture_drill-down.md.BtPdlM7r.lean.js +1 -0
  29. package/export-template/docs/assets/contributor-guide_architecture_impact-estimation.md.CooslVJt.js +1 -0
  30. package/export-template/docs/assets/contributor-guide_architecture_impact-estimation.md.CooslVJt.lean.js +1 -0
  31. package/export-template/docs/assets/contributor-guide_architecture_index.md.3TO9ic6w.js +1 -0
  32. package/export-template/docs/assets/contributor-guide_architecture_index.md.3TO9ic6w.lean.js +1 -0
  33. package/export-template/docs/assets/contributor-guide_architecture_overview.md.CehiRmGn.js +1 -0
  34. package/export-template/docs/assets/contributor-guide_architecture_overview.md.CehiRmGn.lean.js +1 -0
  35. package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.C-xxn0q7.js +1 -0
  36. package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.C-xxn0q7.lean.js +1 -0
  37. package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.R27gQrgY.js +1 -0
  38. package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.R27gQrgY.lean.js +1 -0
  39. package/export-template/docs/assets/contributor-guide_architecture_worker-protocol.md.IbnfNrV3.js +6 -0
  40. package/export-template/docs/assets/contributor-guide_architecture_worker-protocol.md.IbnfNrV3.lean.js +1 -0
  41. package/export-template/docs/assets/contributor-guide_contributing.md.CvRsdr6J.js +1 -0
  42. package/export-template/docs/assets/contributor-guide_contributing.md.CvRsdr6J.lean.js +1 -0
  43. package/export-template/docs/assets/contributor-guide_development-setup.md.DvAN_9mK.js +12 -0
  44. package/export-template/docs/assets/contributor-guide_development-setup.md.DvAN_9mK.lean.js +1 -0
  45. package/export-template/docs/assets/contributor-guide_testing.md.6rIKqSyY.js +1 -0
  46. package/export-template/docs/assets/contributor-guide_testing.md.6rIKqSyY.lean.js +1 -0
  47. package/export-template/docs/assets/dag-stages.DSz_S937.svg +1 -0
  48. package/export-template/docs/assets/dag-stages.dark.F72UzxH4.svg +1 -0
  49. package/export-template/docs/assets/driver-executor.D5pQ7YN1.svg +1 -0
  50. package/export-template/docs/assets/driver-executor.dark.BmX9cPvh.svg +1 -0
  51. package/export-template/docs/assets/duplicate-plan-subtree.B4cvN6fj.svg +1 -0
  52. package/export-template/docs/assets/duplicate-plan-subtree.dark.Dw8wS0Ag.svg +1 -0
  53. package/export-template/docs/assets/index.md.CHJVslga.js +1 -0
  54. package/export-template/docs/assets/index.md.CHJVslga.lean.js +1 -0
  55. package/export-template/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  56. package/export-template/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  57. package/export-template/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  58. package/export-template/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  59. package/export-template/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  60. package/export-template/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  61. package/export-template/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  62. package/export-template/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  63. package/export-template/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  64. package/export-template/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  65. package/export-template/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  66. package/export-template/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  67. package/export-template/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  68. package/export-template/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  69. package/export-template/docs/assets/join-strategy.C_FvrCEo.svg +1 -0
  70. package/export-template/docs/assets/join-strategy.dark.ChMLnNII.svg +1 -0
  71. package/export-template/docs/assets/memory-borrowing.BqQRJg0u.svg +1 -0
  72. package/export-template/docs/assets/memory-borrowing.dark.Yhh20O9C.svg +1 -0
  73. package/export-template/docs/assets/memory-regions.XHvO7jHG.svg +1 -0
  74. package/export-template/docs/assets/memory-regions.dark.D4TP9_08.svg +1 -0
  75. package/export-template/docs/assets/repartition-vs-coalesce.BovLRrpj.svg +1 -0
  76. package/export-template/docs/assets/repartition-vs-coalesce.dark.BhAczKZQ.svg +1 -0
  77. package/export-template/docs/assets/retry-escalation-ladder.DyTKJJmZ.svg +1 -0
  78. package/export-template/docs/assets/retry-escalation-ladder.dark.BdsabtU3.svg +1 -0
  79. package/export-template/docs/assets/shuffle-map-reduce.KuOEZVmg.svg +1 -0
  80. package/export-template/docs/assets/shuffle-map-reduce.dark.BgQZnFSb.svg +1 -0
  81. package/export-template/docs/assets/spill-classification.BU2euYDO.svg +1 -0
  82. package/export-template/docs/assets/spill-classification.dark.D7i1M40d.svg +1 -0
  83. package/export-template/docs/assets/style.DSixAiZE.css +1 -0
  84. package/export-template/docs/assets/tuning-reference_anti-patterns.md.Df1YMIHu.js +1 -0
  85. package/export-template/docs/assets/tuning-reference_anti-patterns.md.Df1YMIHu.lean.js +1 -0
  86. package/export-template/docs/assets/tuning-reference_aqe.md.BIsCtLzm.js +1 -0
  87. package/export-template/docs/assets/tuning-reference_aqe.md.BIsCtLzm.lean.js +1 -0
  88. package/export-template/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.CEstB3Ia.js +1 -0
  89. package/export-template/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.CEstB3Ia.lean.js +1 -0
  90. package/export-template/docs/assets/tuning-reference_bottleneck-cold-start.md.CEuy-72y.js +7 -0
  91. package/export-template/docs/assets/tuning-reference_bottleneck-cold-start.md.CEuy-72y.lean.js +1 -0
  92. package/export-template/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.CIohQDfn.js +1 -0
  93. package/export-template/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.CIohQDfn.lean.js +1 -0
  94. package/export-template/docs/assets/tuning-reference_bottleneck-failures.md.4z5BXGJ2.js +6 -0
  95. package/export-template/docs/assets/tuning-reference_bottleneck-failures.md.4z5BXGJ2.lean.js +1 -0
  96. package/export-template/docs/assets/tuning-reference_bottleneck-gc.md.DSxzZRK7.js +6 -0
  97. package/export-template/docs/assets/tuning-reference_bottleneck-gc.md.DSxzZRK7.lean.js +1 -0
  98. package/export-template/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.BaJl__1W.js +8 -0
  99. package/export-template/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.BaJl__1W.lean.js +1 -0
  100. package/export-template/docs/assets/tuning-reference_bottleneck-memory-utilization.md.DbP-SJZc.js +7 -0
  101. package/export-template/docs/assets/tuning-reference_bottleneck-memory-utilization.md.DbP-SJZc.lean.js +1 -0
  102. package/export-template/docs/assets/tuning-reference_bottleneck-retry-waste.md.D5JMjVOt.js +1 -0
  103. package/export-template/docs/assets/tuning-reference_bottleneck-retry-waste.md.D5JMjVOt.lean.js +1 -0
  104. package/export-template/docs/assets/tuning-reference_bottleneck-shuffle.md.CM-nTmIH.js +12 -0
  105. package/export-template/docs/assets/tuning-reference_bottleneck-shuffle.md.CM-nTmIH.lean.js +1 -0
  106. package/export-template/docs/assets/tuning-reference_bottleneck-skew.md.BdUwiDhn.js +14 -0
  107. package/export-template/docs/assets/tuning-reference_bottleneck-skew.md.BdUwiDhn.lean.js +1 -0
  108. package/export-template/docs/assets/tuning-reference_bottleneck-slow-host.md.BlIo6UDW.js +7 -0
  109. package/export-template/docs/assets/tuning-reference_bottleneck-slow-host.md.BlIo6UDW.lean.js +1 -0
  110. package/export-template/docs/assets/tuning-reference_bottleneck-small-files.md.B8kloyx8.js +5 -0
  111. package/export-template/docs/assets/tuning-reference_bottleneck-small-files.md.B8kloyx8.lean.js +1 -0
  112. package/export-template/docs/assets/tuning-reference_bottleneck-spill.md.PNH7mITt.js +6 -0
  113. package/export-template/docs/assets/tuning-reference_bottleneck-spill.md.PNH7mITt.lean.js +1 -0
  114. package/export-template/docs/assets/tuning-reference_bottleneck-straggler.md.DY36fHN5.js +7 -0
  115. package/export-template/docs/assets/tuning-reference_bottleneck-straggler.md.DY36fHN5.lean.js +1 -0
  116. package/export-template/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.QTV7O8kU.js +8 -0
  117. package/export-template/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.QTV7O8kU.lean.js +1 -0
  118. package/export-template/docs/assets/tuning-reference_bottleneck-utilization.md.DTiueZC3.js +5 -0
  119. package/export-template/docs/assets/tuning-reference_bottleneck-utilization.md.DTiueZC3.lean.js +1 -0
  120. package/export-template/docs/assets/tuning-reference_caching.md.B7aQ8asB.js +1 -0
  121. package/export-template/docs/assets/tuning-reference_caching.md.B7aQ8asB.lean.js +1 -0
  122. package/export-template/docs/assets/tuning-reference_cluster-config.md.ZVmDGsQ3.js +1 -0
  123. package/export-template/docs/assets/tuning-reference_cluster-config.md.ZVmDGsQ3.lean.js +1 -0
  124. package/export-template/docs/assets/tuning-reference_config.md.UvveiWG3.js +1 -0
  125. package/export-template/docs/assets/tuning-reference_config.md.UvveiWG3.lean.js +1 -0
  126. package/export-template/docs/assets/tuning-reference_data-formats.md.bjCAWH3N.js +1 -0
  127. package/export-template/docs/assets/tuning-reference_data-formats.md.bjCAWH3N.lean.js +1 -0
  128. package/export-template/docs/assets/tuning-reference_index.md.BQ_NooMV.js +1 -0
  129. package/export-template/docs/assets/tuning-reference_index.md.BQ_NooMV.lean.js +1 -0
  130. package/export-template/docs/assets/tuning-reference_intro.md.CobD-lGB.js +1 -0
  131. package/export-template/docs/assets/tuning-reference_intro.md.CobD-lGB.lean.js +1 -0
  132. package/export-template/docs/assets/tuning-reference_joins.md.BtKs_CuW.js +1 -0
  133. package/export-template/docs/assets/tuning-reference_joins.md.BtKs_CuW.lean.js +1 -0
  134. package/export-template/docs/assets/tuning-reference_memory-model.md.DhT-n4y3.js +1 -0
  135. package/export-template/docs/assets/tuning-reference_memory-model.md.DhT-n4y3.lean.js +1 -0
  136. package/export-template/docs/assets/tuning-reference_metrics.md.mLOh7Apj.js +1 -0
  137. package/export-template/docs/assets/tuning-reference_metrics.md.mLOh7Apj.lean.js +1 -0
  138. package/export-template/docs/assets/tuning-reference_partitioning.md.q0zKF_8X.js +1 -0
  139. package/export-template/docs/assets/tuning-reference_partitioning.md.q0zKF_8X.lean.js +1 -0
  140. package/export-template/docs/assets/tuning-reference_pyspark.md.DDCfvN9t.js +6 -0
  141. package/export-template/docs/assets/tuning-reference_pyspark.md.DDCfvN9t.lean.js +1 -0
  142. package/export-template/docs/assets/tuning-reference_shuffle.md.BZZ7R4Ix.js +1 -0
  143. package/export-template/docs/assets/tuning-reference_shuffle.md.BZZ7R4Ix.lean.js +1 -0
  144. package/export-template/docs/assets/tuning-reference_spark-architecture.md.Dwzm5avO.js +1 -0
  145. package/export-template/docs/assets/tuning-reference_spark-architecture.md.Dwzm5avO.lean.js +1 -0
  146. package/export-template/docs/assets/tuning-reference_table-formats.md.D6wj-2dX.js +1 -0
  147. package/export-template/docs/assets/tuning-reference_table-formats.md.D6wj-2dX.lean.js +1 -0
  148. package/export-template/docs/assets/udf-execution-models.BUFDICuG.svg +1 -0
  149. package/export-template/docs/assets/udf-execution-models.dark.YTNS6GDq.svg +1 -0
  150. package/export-template/docs/assets/user-guide_alternative-log-retrieval.md.B4tPGIal.js +1 -0
  151. package/export-template/docs/assets/user-guide_alternative-log-retrieval.md.B4tPGIal.lean.js +1 -0
  152. package/export-template/docs/assets/user-guide_getting-started.md.BJvwLEIM.js +3 -0
  153. package/export-template/docs/assets/user-guide_getting-started.md.BJvwLEIM.lean.js +1 -0
  154. package/export-template/docs/assets/user-guide_mcp-tools.md.Vi3RoflJ.js +125 -0
  155. package/export-template/docs/assets/user-guide_mcp-tools.md.Vi3RoflJ.lean.js +1 -0
  156. package/export-template/docs/assets/user-guide_run-comparison.md.CQc1aoU8.js +1 -0
  157. package/export-template/docs/assets/user-guide_run-comparison.md.CQc1aoU8.lean.js +1 -0
  158. package/export-template/docs/assets/user-guide_understanding-findings.md.DL1UDhvR.js +1 -0
  159. package/export-template/docs/assets/user-guide_understanding-findings.md.DL1UDhvR.lean.js +1 -0
  160. package/export-template/docs/contributor-guide/architecture/board-widgets.html +25 -0
  161. package/export-template/docs/contributor-guide/architecture/detector-contract.html +25 -0
  162. package/export-template/docs/contributor-guide/architecture/drill-down.html +25 -0
  163. package/export-template/docs/contributor-guide/architecture/impact-estimation.html +25 -0
  164. package/export-template/docs/contributor-guide/architecture/index.html +25 -0
  165. package/export-template/docs/contributor-guide/architecture/overview.html +25 -0
  166. package/export-template/docs/contributor-guide/architecture/state-and-history.html +25 -0
  167. package/export-template/docs/contributor-guide/architecture/widget-rendering.html +25 -0
  168. package/export-template/docs/contributor-guide/architecture/worker-protocol.html +30 -0
  169. package/export-template/docs/contributor-guide/contributing.html +25 -0
  170. package/export-template/docs/contributor-guide/development-setup.html +36 -0
  171. package/export-template/docs/contributor-guide/testing.html +25 -0
  172. package/export-template/docs/favicon.svg +4 -0
  173. package/export-template/docs/hashmap.json +1 -0
  174. package/export-template/docs/index.html +25 -0
  175. package/export-template/docs/package.json +1 -0
  176. package/export-template/docs/tuning-reference/anti-patterns.html +25 -0
  177. package/export-template/docs/tuning-reference/aqe.html +25 -0
  178. package/export-template/docs/tuning-reference/bottleneck-broadcast-sizing.html +25 -0
  179. package/export-template/docs/tuning-reference/bottleneck-cold-start.html +31 -0
  180. package/export-template/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +25 -0
  181. package/export-template/docs/tuning-reference/bottleneck-failures.html +30 -0
  182. package/export-template/docs/tuning-reference/bottleneck-gc.html +30 -0
  183. package/export-template/docs/tuning-reference/bottleneck-job-failure-rate.html +32 -0
  184. package/export-template/docs/tuning-reference/bottleneck-memory-utilization.html +31 -0
  185. package/export-template/docs/tuning-reference/bottleneck-retry-waste.html +25 -0
  186. package/export-template/docs/tuning-reference/bottleneck-shuffle.html +36 -0
  187. package/export-template/docs/tuning-reference/bottleneck-skew.html +38 -0
  188. package/export-template/docs/tuning-reference/bottleneck-slow-host.html +31 -0
  189. package/export-template/docs/tuning-reference/bottleneck-small-files.html +29 -0
  190. package/export-template/docs/tuning-reference/bottleneck-spill.html +30 -0
  191. package/export-template/docs/tuning-reference/bottleneck-straggler.html +31 -0
  192. package/export-template/docs/tuning-reference/bottleneck-tiny-tasks.html +32 -0
  193. package/export-template/docs/tuning-reference/bottleneck-utilization.html +29 -0
  194. package/export-template/docs/tuning-reference/caching.html +25 -0
  195. package/export-template/docs/tuning-reference/cluster-config.html +25 -0
  196. package/export-template/docs/tuning-reference/config.html +25 -0
  197. package/export-template/docs/tuning-reference/data-formats.html +25 -0
  198. package/export-template/docs/tuning-reference/index.html +25 -0
  199. package/export-template/docs/tuning-reference/intro.html +25 -0
  200. package/export-template/docs/tuning-reference/joins.html +25 -0
  201. package/export-template/docs/tuning-reference/memory-model.html +25 -0
  202. package/export-template/docs/tuning-reference/metrics.html +25 -0
  203. package/export-template/docs/tuning-reference/partitioning.html +25 -0
  204. package/export-template/docs/tuning-reference/pyspark.html +30 -0
  205. package/export-template/docs/tuning-reference/shuffle.html +25 -0
  206. package/export-template/docs/tuning-reference/spark-architecture.html +25 -0
  207. package/export-template/docs/tuning-reference/table-formats.html +25 -0
  208. package/export-template/docs/user-guide/alternative-log-retrieval.html +25 -0
  209. package/export-template/docs/user-guide/getting-started.html +27 -0
  210. package/export-template/docs/user-guide/mcp-tools.html +149 -0
  211. package/export-template/docs/user-guide/run-comparison.html +25 -0
  212. package/export-template/docs/user-guide/understanding-findings.html +25 -0
  213. package/export-template/docs/vp-icons.css +0 -0
  214. package/export-template/favicon.svg +4 -0
  215. package/export-template/index.html +111 -0
  216. package/export-template/parser-worker-DyjiQvfP.js +112 -0
  217. package/export-template/sample-runs/sample-run.ndjson.gz +0 -0
  218. package/package.json +20 -6
  219. package/vendor-core/analyzer.js +74 -74
  220. package/vendor-core/cli/budgets.js +13 -27
  221. package/vendor-core/cli/collect-run.js +43 -19
  222. package/vendor-core/core-count.js +25 -27
  223. package/vendor-core/core-locality-ratio.js +4 -11
  224. package/vendor-core/core-time-series.js +6 -12
  225. package/vendor-core/core-usage-locality.js +3 -4
  226. package/vendor-core/detectors.js +395 -389
  227. package/vendor-core/docs-config.js +69 -21
  228. package/vendor-core/docs-content/chapters/01-intro.md +32 -0
  229. package/vendor-core/docs-content/chapters/02-spark-architecture.md +76 -0
  230. package/vendor-core/docs-content/chapters/03-memory-model.md +73 -0
  231. package/vendor-core/docs-content/chapters/04-partitioning.md +65 -0
  232. package/vendor-core/docs-content/chapters/05-joins.md +62 -0
  233. package/vendor-core/docs-content/chapters/06-shuffle.md +59 -0
  234. package/vendor-core/docs-content/chapters/07-data-formats.md +81 -0
  235. package/vendor-core/docs-content/chapters/07b-table-formats.md +56 -0
  236. package/vendor-core/docs-content/chapters/08-caching.md +58 -0
  237. package/vendor-core/docs-content/chapters/09-pyspark.md +78 -0
  238. package/vendor-core/docs-content/chapters/10-aqe.md +167 -0
  239. package/vendor-core/docs-content/chapters/11-cluster-config.md +170 -0
  240. package/vendor-core/docs-content/chapters/12-anti-patterns.md +171 -0
  241. package/vendor-core/docs-content/chapters/14-metrics.md +87 -0
  242. package/vendor-core/docs-content/chapters/15-config.md +93 -0
  243. package/vendor-core/docs-content/chapters/nav-index.json +370 -0
  244. package/vendor-core/docs-content/detection/cache.md +7 -0
  245. package/vendor-core/docs-content/detection/cfg.md +15 -0
  246. package/vendor-core/docs-content/detection/chrn.md +9 -0
  247. package/vendor-core/docs-content/detection/cold.md +4 -0
  248. package/vendor-core/docs-content/detection/cstor.md +4 -0
  249. package/vendor-core/docs-content/detection/fail.md +5 -0
  250. package/vendor-core/docs-content/detection/gc.md +4 -0
  251. package/vendor-core/docs-content/detection/host.md +5 -0
  252. package/vendor-core/docs-content/detection/incmp.md +6 -0
  253. package/vendor-core/docs-content/detection/jobs.md +4 -0
  254. package/vendor-core/docs-content/detection/local.md +6 -0
  255. package/vendor-core/docs-content/detection/mem.md +10 -0
  256. package/vendor-core/docs-content/detection/part.md +5 -0
  257. package/vendor-core/docs-content/detection/plan.md +14 -0
  258. package/vendor-core/docs-content/detection/retry.md +4 -0
  259. package/vendor-core/docs-content/detection/sfail.md +5 -0
  260. package/vendor-core/docs-content/detection/shape.md +5 -0
  261. package/vendor-core/docs-content/detection/shfl.md +4 -0
  262. package/vendor-core/docs-content/detection/skew.md +6 -0
  263. package/vendor-core/docs-content/detection/slow.md +6 -0
  264. package/vendor-core/docs-content/detection/spec.md +8 -0
  265. package/vendor-core/docs-content/detection/spill.md +7 -0
  266. package/vendor-core/docs-content/detection/strag.md +5 -0
  267. package/vendor-core/docs-content/detection/tiny.md +4 -0
  268. package/vendor-core/docs-content/detection/util.md +4 -0
  269. package/vendor-core/docs-content/diagrams/aqe-loop.dark.svg +1 -0
  270. package/vendor-core/docs-content/diagrams/aqe-loop.svg +1 -0
  271. package/vendor-core/docs-content/diagrams/broadcast-vs-shuffle.dark.svg +1 -0
  272. package/vendor-core/docs-content/diagrams/broadcast-vs-shuffle.svg +1 -0
  273. package/vendor-core/docs-content/diagrams/cache-lifecycle.dark.svg +1 -0
  274. package/vendor-core/docs-content/diagrams/cache-lifecycle.svg +1 -0
  275. package/vendor-core/docs-content/diagrams/cold-start-timeline.dark.svg +1 -0
  276. package/vendor-core/docs-content/diagrams/cold-start-timeline.svg +1 -0
  277. package/vendor-core/docs-content/diagrams/columnar-layout.dark.svg +1 -0
  278. package/vendor-core/docs-content/diagrams/columnar-layout.svg +1 -0
  279. package/vendor-core/docs-content/diagrams/container-memory.dark.svg +1 -0
  280. package/vendor-core/docs-content/diagrams/container-memory.svg +1 -0
  281. package/vendor-core/docs-content/diagrams/dag-stages.dark.svg +1 -0
  282. package/vendor-core/docs-content/diagrams/dag-stages.svg +1 -0
  283. package/vendor-core/docs-content/diagrams/driver-executor.dark.svg +1 -0
  284. package/vendor-core/docs-content/diagrams/driver-executor.svg +1 -0
  285. package/vendor-core/docs-content/diagrams/duplicate-plan-subtree.dark.svg +1 -0
  286. package/vendor-core/docs-content/diagrams/duplicate-plan-subtree.svg +1 -0
  287. package/vendor-core/docs-content/diagrams/join-strategy.dark.svg +1 -0
  288. package/vendor-core/docs-content/diagrams/join-strategy.svg +1 -0
  289. package/vendor-core/docs-content/diagrams/memory-borrowing.dark.svg +1 -0
  290. package/vendor-core/docs-content/diagrams/memory-borrowing.svg +1 -0
  291. package/vendor-core/docs-content/diagrams/memory-regions.dark.svg +1 -0
  292. package/vendor-core/docs-content/diagrams/memory-regions.svg +1 -0
  293. package/vendor-core/docs-content/diagrams/repartition-vs-coalesce.dark.svg +1 -0
  294. package/vendor-core/docs-content/diagrams/repartition-vs-coalesce.svg +1 -0
  295. package/vendor-core/docs-content/diagrams/retry-escalation-ladder.dark.svg +1 -0
  296. package/vendor-core/docs-content/diagrams/retry-escalation-ladder.svg +1 -0
  297. package/vendor-core/docs-content/diagrams/shuffle-map-reduce.dark.svg +1 -0
  298. package/vendor-core/docs-content/diagrams/shuffle-map-reduce.svg +1 -0
  299. package/vendor-core/docs-content/diagrams/spill-classification.dark.svg +1 -0
  300. package/vendor-core/docs-content/diagrams/spill-classification.svg +1 -0
  301. package/vendor-core/docs-content/diagrams/udf-execution-models.dark.svg +1 -0
  302. package/vendor-core/docs-content/diagrams/udf-execution-models.svg +1 -0
  303. package/vendor-core/docs-content/tuning/broadcast-sizing.md +78 -0
  304. package/vendor-core/docs-content/tuning/cold-start.md +81 -0
  305. package/vendor-core/docs-content/tuning/duplicate-plan-subtree.md +45 -0
  306. package/vendor-core/docs-content/tuning/failures.md +124 -0
  307. package/vendor-core/docs-content/tuning/gc.md +110 -0
  308. package/vendor-core/docs-content/tuning/job-failure-rate.md +101 -0
  309. package/vendor-core/docs-content/tuning/memory-utilization.md +58 -0
  310. package/vendor-core/docs-content/tuning/retry-waste.md +90 -0
  311. package/vendor-core/docs-content/tuning/shuffle.md +154 -0
  312. package/vendor-core/docs-content/tuning/skew.md +123 -0
  313. package/vendor-core/docs-content/tuning/slow-host.md +117 -0
  314. package/vendor-core/docs-content/tuning/small-files.md +99 -0
  315. package/vendor-core/docs-content/tuning/spill.md +114 -0
  316. package/vendor-core/docs-content/tuning/straggler.md +103 -0
  317. package/vendor-core/docs-content/tuning/tiny-tasks.md +94 -0
  318. package/vendor-core/docs-content/tuning/utilization.md +90 -0
  319. package/vendor-core/docs-site-config.js +10 -17
  320. package/vendor-core/efficiency-model.js +7 -13
  321. package/vendor-core/etl-phases.js +3 -5
  322. package/vendor-core/event-handlers.js +232 -134
  323. package/vendor-core/event-schemas.js +48 -114
  324. package/vendor-core/evidence-availability.js +5 -10
  325. package/vendor-core/evidence-report.js +73 -123
  326. package/vendor-core/export-data.js +48 -0
  327. package/vendor-core/finding-action-label.js +4 -10
  328. package/vendor-core/finding-filter-predicate.js +3 -7
  329. package/vendor-core/finding-generic-recommendation.js +112 -0
  330. package/vendor-core/finding-names.js +51 -0
  331. package/vendor-core/format-utils.js +112 -38
  332. package/vendor-core/impact-band.js +18 -24
  333. package/vendor-core/impact-estimator.js +38 -74
  334. package/vendor-core/ingest.js +7 -13
  335. package/vendor-core/job-groups.js +3 -6
  336. package/vendor-core/list-runs.js +278 -0
  337. package/vendor-core/load-vendored.js +6 -12
  338. package/vendor-core/log-header-peek.js +81 -0
  339. package/vendor-core/lz4-block.js +4 -6
  340. package/vendor-core/mcp-server-factory.js +38 -8
  341. package/vendor-core/mcp-tools.js +105 -76
  342. package/vendor-core/model-assembler.js +8 -16
  343. package/vendor-core/occupancy.js +5 -9
  344. package/vendor-core/parser-worker.js +20 -29
  345. package/vendor-core/plan-dot.js +2 -5
  346. package/vendor-core/plan-duration-attribution.js +78 -29
  347. package/vendor-core/plan-graph-model.js +126 -69
  348. package/vendor-core/plan-node-detail.js +31 -17
  349. package/vendor-core/plan-summary.js +19 -8
  350. package/vendor-core/recommendation-rollup.js +35 -39
  351. package/vendor-core/redact.js +72 -16
  352. package/vendor-core/rolling-log-reassembly.js +4 -6
  353. package/vendor-core/run-comparison.js +86 -72
  354. package/vendor-core/scaling-sim.js +5 -7
  355. package/vendor-core/session-snapshot.js +1 -1
  356. package/vendor-core/shs-fetch.js +4 -6
  357. package/vendor-core/shs-load.js +9 -13
  358. package/vendor-core/shs-request.js +1 -1
  359. package/vendor-core/stage-quantiles.js +14 -0
  360. package/vendor-core/types.js +78 -18
  361. package/vendor-core/wasted-core-hours.js +7 -12
@@ -24,12 +24,9 @@ import { computeRunAggregates } from './run-aggregates.js';
24
24
 
25
25
 
26
26
 
27
- // ---------------------------------------------------------------------------
28
- // Internal parser-state shapes. These describe the real runtime objects the
29
- // handlers below build and mutate; they are intentionally not the same as
30
- // the public AppModel types in types.ts (e.g. Stage), which describe the
31
- // *posted* message shape after finalizeStage/appMessage reshape things.
32
- // ---------------------------------------------------------------------------
27
+ // Internal parser-state shapes: the real runtime objects the handlers build and mutate, not the
28
+ // public AppModel types in types.ts (which describe the posted message shape after
29
+ // finalizeStage/appMessage reshape things).
33
30
 
34
31
 
35
32
 
@@ -50,9 +47,8 @@ import { computeRunAggregates } from './run-aggregates.js';
50
47
 
51
48
 
52
49
 
53
-
54
-
55
-
50
+
51
+
56
52
 
57
53
 
58
54
 
@@ -70,12 +66,31 @@ import { computeRunAggregates } from './run-aggregates.js';
70
66
 
71
67
 
72
68
 
73
- // One accumulated task-attempt record, keyed by `<stageAttemptId>:<index>` (or
74
- // a unique Symbol when the raw event has no Index). Matches accumulateTask's
75
- // real built shape (event-handlers.js:169-189).
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+ // Cap for both StageRecord.retryTaskSamples (populated here, live during ingest) and
81
+ // stage.failedTaskSamples (populated in stage-quantiles.ts's finalizeStage): a starting judgment
82
+ // call, not empirically validated against a pathological case. Kept as a separate constant in each
83
+ // file rather than a shared import, to avoid a circular value import between the two modules
84
+ // (event-handlers.ts already imports finalizeStage FROM stage-quantiles.ts).
85
+ const MAX_TASK_SAMPLES = 20;
86
+
87
+ // One accumulated task-attempt record, keyed by `<stageAttemptId>:<index>` (or a unique Symbol
88
+ // when the raw event has no Index).
76
89
 
77
90
 
78
91
 
92
+
93
+
79
94
 
80
95
 
81
96
 
@@ -121,6 +136,7 @@ import { computeRunAggregates } from './run-aggregates.js';
121
136
 
122
137
 
123
138
 
139
+
124
140
 
125
141
 
126
142
 
@@ -136,8 +152,7 @@ import { computeRunAggregates } from './run-aggregates.js';
136
152
 
137
153
 
138
154
 
139
-
140
-
155
+
141
156
 
142
157
 
143
158
 
@@ -160,19 +175,85 @@ import { computeRunAggregates } from './run-aggregates.js';
160
175
 
161
176
 
162
177
 
178
+
179
+ // Splits decompressed byte chunks into NDJSON lines. Each chunk is decoded whole in one
180
+ // TextDecoder.decode call, newlines located by scanning raw bytes (indexOf(0x0A)), and each line
181
+ // is a substring of the `pending + text` concatenation.
182
+ //
183
+ // Why (profiled on a 138MB / 3.5GB-decompressed log, decode phase, each line JSON.parsed to force
184
+ // materialization): per-line decode (227k calls) ~57s vs whole-chunk decode (~36k calls) ~7s;
185
+ // scanning the decoded UTF-16 string for newlines costs 130-450s while scanning raw bytes ~7s;
186
+ // the line MUST be a substring of the freshly concatenated string (V8 flattens once per chunk),
187
+ // substrings of raw decode() output cost ~4x more. Combined ~14s, byte-identical output.
188
+ //
189
+ // 0x0A can't appear inside a UTF-8 multibyte sequence, so byte-scanning for it is UTF-8 safe. A
190
+ // newline's byte offset equals its char offset only when the chunk is 1:1 byte<->char, detected by
191
+ // text.length === buffer.length (the all-ASCII fast path); otherwise a byte walk recovers char
192
+ // offsets. `{ stream: true }` reassembles a character split across the chunk boundary.
193
+ const NEWLINE = 0x0a;
163
194
 
164
195
  export function buildChunkDecoder() {
165
196
  const decoder = new TextDecoder('utf-8');
197
+ // Decoded partial line after the last newline, carried to the next chunk. A character split
198
+ // across the boundary is reassembled by the streaming decoder, so this is already-decoded.
166
199
  let pending = '';
200
+
167
201
  return {
168
202
  decode(buffer ) {
169
- const text = pending + decoder.decode(buffer, { stream: true });
170
- const lines = text.split('\n');
171
- pending = lines.pop() ?? '';
172
- return lines.filter((l) => l.length > 0);
203
+ const lines = [];
204
+ const text = decoder.decode(buffer, { stream: true });
205
+ const full = pending === '' ? text : pending + text;
206
+ const base = pending.length;
207
+ const len = buffer.length;
208
+ // Char offset in `full` of the current line's start. Zero-length lines
209
+ // (`cut === start`) are dropped to match a `.filter(l => l.length)`.
210
+ let start = 0;
211
+
212
+ if (text.length === len) {
213
+ // 1:1 byte<->char: a newline's byte offset is its char offset (+ base).
214
+ let nl = buffer.indexOf(NEWLINE, 0);
215
+ while (nl !== -1) {
216
+ const cut = base + nl;
217
+ if (cut > start) lines.push(full.substring(start, cut));
218
+ start = cut + 1;
219
+ nl = buffer.indexOf(NEWLINE, nl + 1);
220
+ }
221
+ } else {
222
+ // Some byte does not map 1:1. Walk the bytes counting UTF-16 units to
223
+ // turn each newline's byte offset into a char offset in `full`.
224
+ let unit = base;
225
+ let bpos = 0;
226
+ // A char whose lead byte was in the previous chunk arrives here as
227
+ // leading continuation bytes; the streaming decoder emits it as text[0].
228
+ // Count it once (a surrogate pair is two units), then skip its bytes.
229
+ if (len > 0 && (buffer[0] & 0xc0) === 0x80) {
230
+ const c0 = text.charCodeAt(0);
231
+ unit += c0 >= 0xd800 && c0 <= 0xdbff ? 2 : 1;
232
+ while (bpos < len && (buffer[bpos] & 0xc0) === 0x80) bpos++;
233
+ }
234
+ let nl = buffer.indexOf(NEWLINE, bpos);
235
+ while (nl !== -1) {
236
+ for (let i = bpos; i < nl; i++) {
237
+ const b = buffer[i];
238
+ if (b < 0x80) unit++; // ASCII
239
+ else if (b >= 0xf0) unit += 2; // 4-byte lead -> surrogate pair
240
+ else if (b >= 0xc0) unit++; // 2/3-byte lead -> one unit
241
+ // continuation byte (0x80..0xBF) -> zero units
242
+ }
243
+ if (unit > start) lines.push(full.substring(start, unit));
244
+ unit++; // the '\n' itself
245
+ start = unit;
246
+ bpos = nl + 1;
247
+ nl = buffer.indexOf(NEWLINE, bpos);
248
+ }
249
+ }
250
+
251
+ pending = start < full.length ? full.substring(start) : '';
252
+ return lines;
173
253
  },
174
254
  flush() {
175
- const last = pending.trim();
255
+ // Flush any incomplete trailing character the streaming decoder still holds.
256
+ const last = (pending + decoder.decode()).trim();
176
257
  pending = '';
177
258
  return last ? [last] : [];
178
259
  },
@@ -209,13 +290,9 @@ export function createState() {
209
290
  };
210
291
  }
211
292
 
212
- // Normalize the `Spark Properties` payload of SparkListenerEnvironmentUpdate.
213
- // Modern Spark emits an object of string keyvalue; older logs use an array of
214
- // [key, value] pairs. Both collapse to a plain string→string map. The schema
215
- // (EnvironmentUpdateEventSchema) tolerates raw values that are a bare JSON
216
- // number or boolean instead of a string, so those are coerced to their
217
- // string form here, keeping this function's `Record<string, string>`
218
- // contract intact for every downstream reader of `app.config`.
293
+ // Normalize the `Spark Properties` payload of SparkListenerEnvironmentUpdate. Modern Spark emits
294
+ // an object of key->value; older logs use [key, value] pairs. Both collapse to a string->string
295
+ // map; the schema tolerates bare number/boolean values, coerced to string to keep the contract.
219
296
 
220
297
  export function normalizeSparkProperties(
221
298
  props
@@ -234,10 +311,9 @@ export function normalizeSparkProperties(
234
311
  return map;
235
312
  }
236
313
 
237
- // Parse a Spark memory-size string to MiB. Spark's JVM-memory configs
238
- // (spark.{executor,driver}.memory[Overhead]) use bytesConf(ByteUnit.MiB), so a
239
- // bare number ("10") means MiB. A k/m/g/t suffix sets the unit (an optional
240
- // trailing "b", as in "gb", is redundant). A lone trailing "b" ("10b") means bytes.
314
+ // Parse a Spark memory-size string to MiB. Spark's JVM-memory configs use bytesConf(ByteUnit.MiB),
315
+ // so a bare number means MiB. A k/m/g/t suffix sets the unit (trailing "b" redundant); a lone "b"
316
+ // ("10b") means bytes.
241
317
  export function parseSparkMemoryMB(value ) {
242
318
  if (value == null) return null;
243
319
  const m = String(value).trim().toLowerCase().match(/^([\d.]+)\s*([kmgt]?)(b?)$/);
@@ -253,9 +329,8 @@ export function parseSparkMemoryMB(value ) {
253
329
  }
254
330
  }
255
331
 
256
- // Derive an allocated-resource summary (executor/driver memory + cores, plus
257
- // dynamic-allocation / shuffle-service flags) from the Spark config map.
258
- // Absent keys degrade to null rather than guessed defaults.
332
+ // Derive an allocated-resource summary from the Spark config map. Absent keys degrade to null,
333
+ // not guessed defaults.
259
334
  export function extractResources(config ) {
260
335
  const cfg = config ?? {};
261
336
  const int = (k ) => {
@@ -287,11 +362,8 @@ export function extractResources(config
287
362
  };
288
363
  }
289
364
 
290
- // Convert the live rddInfo Map (whose entries carry a mutable `stageIds` Set
291
- // used for bookkeeping across StageSubmitted events) into a postable copy
292
- // with `stageIds` as a sorted array, used at every 'app' message post site
293
- // so the main thread never receives a live Set it can't structured-clone
294
- // meaningfully or that could keep mutating after being posted.
365
+ // Convert the live rddInfo Map (with a mutable stageIds Set) into a postable copy with stageIds
366
+ // as a sorted array, so the main thread never receives a live Set that could keep mutating after post.
295
367
  function snapshotRddInfo(rddInfo ) {
296
368
  const out = new Map ();
297
369
  for (const [id, r] of rddInfo) {
@@ -305,16 +377,10 @@ function snapshotEvidenceInputs(state ) {
305
377
  }
306
378
 
307
379
  /**
308
- * Snapshots the current evidence counters and posts an `app` message. This is
309
- * called from `SparkListenerApplicationStart`, `SparkListenerEnvironmentUpdate`,
310
- * and `SparkListenerApplicationEnd`, so every streaming `app` message carries a
311
- * counter snapshot taken at that moment. Only the terminal snapshot emitted by
312
- * `emitParseCompletion` is authoritative; mid-parse snapshots are partial and
313
- * must not be used for final evidence-availability conclusions.
314
- *
315
- * Only ever called once `state.app` has been assigned (from startApplication,
316
- * or from updateEnvironment/the ApplicationEnd case which both guard on
317
- * `state.app` truthiness before calling this), hence the non-null assertion.
380
+ * Snapshots the evidence counters and posts an `app` message. Called from ApplicationStart,
381
+ * EnvironmentUpdate, and ApplicationEnd, so every streaming `app` message carries a snapshot at
382
+ * that moment. Only emitParseCompletion's terminal snapshot is authoritative; mid-parse ones are
383
+ * partial. Only called once state.app is assigned (all callers guard on it), hence the non-null assertion.
318
384
  */
319
385
  function appMessage(state ) {
320
386
  const evidenceInputs = snapshotEvidenceInputs(state);
@@ -325,14 +391,19 @@ function appMessage(state )
325
391
  };
326
392
  }
327
393
 
394
+ function taskRecordToSample(t ) {
395
+ return {
396
+ taskId: t.taskId, attemptNumber: t.attemptNumber, host: t.host, executorId: t.executorId,
397
+ reason: t.reason, peakExecMem: t.peakExecMem, memSpilled: t.memSpilled, shuffleWrite: t.shuffleWrite,
398
+ };
399
+ }
400
+
328
401
  export function accumulateTask(event , state ) {
329
402
  const stageId = event['Stage ID'];
330
403
  const stage = state.stages.get(stageId);
331
404
  if (!stage) return null;
332
- // Late TaskEnd for a stage whose StageCompleted already arrived and freed
333
- // taskAttempts (finalizeStage), same out-of-order tolerance as
334
- // SparkListenerStageExecutorMetrics above; the task's stats are already
335
- // baked into the finalized stage and don't need to be re-added.
405
+ // Late TaskEnd for a stage whose StageCompleted already freed taskAttempts (finalizeStage): its
406
+ // stats are already baked into the finalized stage, don't re-add.
336
407
  if (stage.taskAttempts === null) return null;
337
408
 
338
409
  state.evidenceInputs.taskRecords++;
@@ -343,10 +414,8 @@ export function accumulateTask(event , state
343
414
  state.taskAccumStages.get(acc.ID) .add(stageId);
344
415
  }
345
416
 
346
- // 'Task Info' itself and its Failed/Killed/Speculative fields are all
347
- // optional in the schema (real event-log variants omit them); Partial<>
348
- // lets the {} fallback (an absent 'Task Info') type-check while every
349
- // read below still defaults via `??`/`||`.
417
+ // 'Task Info' and its Failed/Killed/Speculative fields are optional in the schema; Partial<>
418
+ // lets the {} fallback type-check while reads below default via ??/||.
350
419
 
351
420
  const info = event['Task Info'] ?? {};
352
421
  const m = event['Task Metrics'] ?? {};
@@ -360,6 +429,8 @@ export function accumulateTask(event , state
360
429
 
361
430
  const record = {
362
431
  duration, failed,
432
+ taskId: info['Task ID'] ?? null,
433
+ attemptNumber: info['Attempt Number'] ?? 0,
363
434
  launchTime: info['Launch Time'] ?? 0,
364
435
  finishTime: info['Finish Time'] ?? 0,
365
436
  reason: event['Task End Reason']?.['Reason'] ?? null,
@@ -380,9 +451,8 @@ export function accumulateTask(event , state
380
451
  outputBytes: out['Bytes Written'] ?? 0,
381
452
  };
382
453
 
383
- // Dedupe only when Index is present (always true for real Spark event logs).
384
- // Without it, every event is treated as a distinct task: preserves legacy
385
- // fixture behavior for tests that omit Index.
454
+ // Dedupe only when Index is present (always true for real logs). Without it every event is a
455
+ // distinct task, preserving behavior for fixtures that omit Index.
386
456
  const index = info['Index'];
387
457
  const key = index != null ? `${event['Stage Attempt ID'] ?? 0}:${index}` : Symbol('no-index');
388
458
  const existing = stage.taskAttempts.get(key);
@@ -390,21 +460,18 @@ export function accumulateTask(event , state
390
460
  if (!existing) {
391
461
  stage.taskAttempts.set(key, record);
392
462
  } else if (existing.failed && !record.failed) {
393
- // A retry succeeded where the earlier attempt failed: the earlier
394
- // attempt's time was wasted work, not a metric to fold twice.
395
- //
396
- // Cause classification: Spark only marks the speculative COPY's own
397
- // TaskInfo.Speculative as true, never the original it raced against,
398
- // so checking either side of the comparison (not just the discarded
399
- // record) is required to catch both "original loses to its speculative
400
- // twin" and "speculative twin loses to the original" symmetrically,
401
- // regardless of arrival order.
463
+ // A retry succeeded where the earlier attempt failed: the earlier attempt's time was wasted.
464
+ // Spark marks only the speculative COPY's Speculative flag, never the original it raced, so
465
+ // check both sides to catch "original loses to twin" and "twin loses to original" either way.
402
466
  if (existing.speculative || record.speculative) {
403
467
  stage.speculationWasteMs += existing.duration;
404
468
  stage.speculationWastedAttempts++;
405
469
  } else {
406
470
  stage.retryWasteMs += existing.duration;
407
471
  stage.wastedAttempts++;
472
+ if (stage.retryTaskSamples.length < MAX_TASK_SAMPLES) {
473
+ stage.retryTaskSamples.push(taskRecordToSample(existing));
474
+ }
408
475
  }
409
476
  stage.taskAttempts.set(key, record);
410
477
  } else {
@@ -416,6 +483,9 @@ export function accumulateTask(event , state
416
483
  } else {
417
484
  stage.retryWasteMs += record.duration;
418
485
  stage.wastedAttempts++;
486
+ if (stage.retryTaskSamples.length < MAX_TASK_SAMPLES) {
487
+ stage.retryTaskSamples.push(taskRecordToSample(record));
488
+ }
419
489
  }
420
490
  }
421
491
 
@@ -427,11 +497,24 @@ export function resolvePlanTree(
427
497
  accumMap ,
428
498
  taskAccumStages ,
429
499
  executionStageIds ,
500
+ executionId ,
430
501
  ) {
431
502
  const seen = new WeakSet ();
432
- const nodeMap = new WeakMap (); // sparkPlanInfoNode → resolved PlanNode
433
-
434
- function makeNode(info ) {
503
+ const nodeMap = new WeakMap ();
504
+ let nextId = 0;
505
+ // Prefixed with the owning SQL execution so PlanNode.id is globally unique,
506
+ // not just unique within this one call: two executions' trees can each
507
+ // produce a node named "n1", and a consumer keying off id alone (e.g. a
508
+ // findings-by-nodeId index) must be able to tell them apart without also
509
+ // threading execution scope through separately. No executionId (e.g. the
510
+ // legacy unit tests below) falls back to the old unprefixed "n0", "n1", ...
511
+ // shape.
512
+ const idPrefix = executionId != null ? `e${executionId}:` : '';
513
+
514
+ function computeMetricsAndStageIds(info )
515
+
516
+
517
+ {
435
518
  const metrics = (info.metrics ?? []).reduce ((acc, m) => {
436
519
  if (m.accumulatorId !== undefined && accumMap.has(m.accumulatorId)) {
437
520
  acc.push({ name: m.name, value: accumMap.get(m.accumulatorId) , metricType: m.metricType });
@@ -439,27 +522,63 @@ export function resolvePlanTree(
439
522
  return acc;
440
523
  }, []);
441
524
 
442
- // Union the stages where this node's accumulator IDs were actually
443
- // observed running (Task 2's data), then clip to this SQL execution's own
444
- // stage universe (Task 3's data): an accumulator ID can occasionally
445
- // belong to a different execution's stages (e.g. a reused subquery),
446
- // and without the clip that would misattribute another execution's work.
447
525
  const stageIdSet = new Set ();
448
526
  for (const m of info.metrics ?? []) {
449
527
  if (m.accumulatorId === undefined) continue;
450
528
  const stages = taskAccumStages.get(m.accumulatorId);
451
529
  if (!stages) continue;
530
+ // Clip to executionStageIds (this execution's own stage set) rather than trusting
531
+ // taskAccumStages wholesale: an accumulator id can be shared with another execution's
532
+ // stages (e.g. a reused subquery re-executes the same accumulator-tagged operator under
533
+ // a different SQL execution id), and without this clip a plan node here would pick up
534
+ // stage ids that actually belong to that other execution's work.
452
535
  for (const sid of stages) {
453
536
  if (executionStageIds && executionStageIds.has(sid)) stageIdSet.add(sid);
454
537
  }
455
538
  }
539
+ return stageIdSet.size > 0 ? { metrics, stageIds: [...stageIdSet].sort((a, b) => a - b) } : { metrics };
540
+ }
456
541
 
457
- const node = { name: info.nodeName, detail: info.simpleString ?? '', metrics, children: [] };
458
- if (stageIdSet.size > 0) node.stageIds = [...stageIdSet].sort((a, b) => a - b);
542
+ function makeNode(info , children ) {
543
+ const { metrics, stageIds } = computeMetricsAndStageIds(info);
544
+ const node = { id: `${idPrefix}n${nextId++}`, name: info.nodeName, detail: info.simpleString ?? '', metrics, children };
545
+ if (stageIds) node.stageIds = stageIds;
459
546
  return node;
460
547
  }
461
548
 
462
- // DFS to collect nodes in discovery order
549
+ // Only a node whose raw name is exactly Exchange/BroadcastExchange gets
550
+ // split; ReusedExchange and any other exchange-family variant stay a
551
+ // single node (confirmed against real event logs: ReusedExchange carries
552
+ // no metrics of its own, it references an already-materialized Exchange
553
+ // elsewhere). Runs on the raw name because no PlanNode/exchangeRole
554
+ // exists yet at this point in construction.
555
+ function makeExchangeSplit(info , children ) {
556
+ const { metrics, stageIds } = computeMetricsAndStageIds(info);
557
+ // write = producer side: keeps the real metrics (e.g. "data size", the
558
+ // shuffle write metrics) and the original children.
559
+ const write = {
560
+ id: `${idPrefix}n${nextId++}`,
561
+ name: info.nodeName,
562
+ detail: '',
563
+ metrics,
564
+ children,
565
+ exchangeRole: 'write',
566
+ };
567
+ if (stageIds) write.stageIds = stageIds;
568
+ // read = consumer side: keeps the original detail (plan-summary.ts's
569
+ // partitioning-key regex needs it), no metrics of its own.
570
+ const read = {
571
+ id: `${idPrefix}n${nextId++}`,
572
+ name: info.nodeName,
573
+ detail: info.simpleString ?? '',
574
+ metrics: [],
575
+ children: [write],
576
+ exchangeRole: 'read',
577
+ };
578
+ if (stageIds) read.stageIds = stageIds;
579
+ return read;
580
+ }
581
+
463
582
  const stack = [rootInfo];
464
583
  const order = [];
465
584
 
@@ -473,24 +592,24 @@ export function resolvePlanTree(
473
592
  }
474
593
  }
475
594
 
476
- // Build nodes bottom-up (reverse discovery = children before parents)
477
595
  for (let i = order.length - 1; i >= 0; i--) {
478
596
  const info = order[i];
479
- const node = makeNode(info);
480
- nodeMap.set(info, node);
597
+ const children = [];
481
598
  for (const child of (info.children ?? [])) {
482
599
  const childNode = nodeMap.get(child);
483
- if (childNode) node.children.push(childNode);
600
+ if (childNode) children.push(childNode);
484
601
  }
602
+ const node = info.nodeName === 'Exchange' || info.nodeName === 'BroadcastExchange'
603
+ ? makeExchangeSplit(info, children)
604
+ : makeNode(info, children);
605
+ nodeMap.set(info, node);
485
606
  }
486
607
 
487
608
  return nodeMap.get(rootInfo) ;
488
609
  }
489
610
 
490
- // Maps SparkListenerStageExecutorMetrics' "Executor Metrics" field names
491
- // (Spark's ExecutorMetricType constants) to our camelCase data-model names.
492
- // Only listed fields are captured: anything else is ignored (forward-compat
493
- // with future Spark ExecutorMetricType additions).
611
+ // Maps SparkListenerStageExecutorMetrics' "Executor Metrics" field names to our camelCase names.
612
+ // Only listed fields are captured (forward-compat with future Spark ExecutorMetricType additions).
494
613
  const EXECUTOR_METRIC_FIELD_MAP = {
495
614
  JVMHeapMemory: 'jvmHeapMemory', JVMOffHeapMemory: 'jvmOffHeapMemory',
496
615
  OnHeapExecutionMemory: 'onHeapExecutionMemory', OffHeapExecutionMemory: 'offHeapExecutionMemory',
@@ -524,9 +643,8 @@ export function updateEnvironment(event
524
643
  state.evidenceInputs.environmentUpdates++;
525
644
  const config = normalizeSparkProperties(event['Spark Properties']);
526
645
  const resources = extractResources(config);
527
- // EnvironmentUpdate normally precedes ApplicationStart: stash the config
528
- // so ApplicationStart can attach it. If it arrives after (a mid-run
529
- // update), apply it live and re-post the app so the main thread refreshes.
646
+ // EnvironmentUpdate normally precedes ApplicationStart: stash the config so ApplicationStart can
647
+ // attach it. If it arrives after (a mid-run update), apply live and re-post the app.
530
648
  if (state.app) {
531
649
  state.app.config = config;
532
650
  state.app.resources = resources;
@@ -597,6 +715,7 @@ export function submitStage(event , st
597
715
  failureReasons: new Map(),
598
716
  stageFailureReason: null,
599
717
  taskAttempts: new Map(),
718
+ retryTaskSamples: [],
600
719
  retryWasteMs: 0,
601
720
  wastedAttempts: 0,
602
721
  speculationWasteMs: 0,
@@ -631,11 +750,9 @@ export function mergeStageRddInfo(
631
750
  replication: sl['Replication'] ?? 1,
632
751
  },
633
752
  numPartitions: rdd['Number of Partitions'] ?? 0,
634
- // Merge forward, don't overwrite: an RDD being cached for the first
635
- // time in THIS stage legitimately reports 0 here (Spark's snapshot
636
- // reflects BlockManager state at submission time). If a later
637
- // resubmission arrives before the cache numbers are re-observed,
638
- // keep the last real value instead of regressing to 0/em-dash.
753
+ // Merge forward, don't overwrite: an RDD cached for the first time in THIS stage legitimately
754
+ // reports 0 (snapshot reflects BlockManager state at submission). Keep the last real value on
755
+ // a resubmission instead of regressing to 0.
639
756
  numCachedPartitions: rdd['Number of Cached Partitions'] || prev?.numCachedPartitions || 0,
640
757
  memorySize: rdd['Memory Size'] || prev?.memorySize || 0,
641
758
  diskSize: rdd['Disk Size'] || prev?.diskSize || 0,
@@ -644,13 +761,9 @@ export function mergeStageRddInfo(
644
761
  }
645
762
  }
646
763
 
647
- // Spark's EventLoggingListener logs these AFTER the stage's
648
- // SparkListenerStageCompleted, so finalizeStage has already posted
649
- // (structured-cloned) the stage with an empty executorMetrics Map by the
650
- // time they arrive. We keep accumulating into the worker-side stage here,
651
- // then re-post all populated maps once via `stageExecutorMetrics` just
652
- // before `done` (see collectStageExecutorMetrics) so the main-thread
653
- // stages are patched before analyze() runs.
764
+ // Spark logs these AFTER SparkListenerStageCompleted, so finalizeStage has already posted the
765
+ // stage with an empty executorMetrics Map. Keep accumulating worker-side, then re-post all maps
766
+ // once via `stageExecutorMetrics` just before `done` so main-thread stages are patched before analyze().
654
767
  export function recordStageExecutorMetrics(event , state ) {
655
768
  const stage = state.stages.get(event['Stage ID']);
656
769
  if (!stage) return null;
@@ -697,7 +810,9 @@ export function endSqlExecution(
697
810
  }
698
811
 
699
812
  const accumMap = state.accumState.get(event.executionId) ?? new Map ();
700
- const planTree = resolvePlanTree(planInfo, accumMap, state.taskAccumStages, state.sqlExecStages.get(event.executionId));
813
+ const planTree = resolvePlanTree(
814
+ planInfo, accumMap, state.taskAccumStages, state.sqlExecStages.get(event.executionId), event.executionId,
815
+ );
701
816
  state.accumState.delete(event.executionId);
702
817
 
703
818
  state.evidenceInputs.resolvedSqlPlans++;
@@ -723,13 +838,9 @@ export function applyAdaptiveExecutionUpdate(
723
838
  if (event.sparkPlanInfo != null) exec.sparkPlanInfo = event.sparkPlanInfo;
724
839
  if (event.physicalPlanDescription != null) exec.physicalPlanDescription = event.physicalPlanDescription;
725
840
  exec.hadAdaptiveUpdate = true;
726
- // Re-emit a 'sql' message (same shape startSqlExecution posts) so the
727
- // browser's structured-cloned appModel.sql copy actually sees the flip:
728
- // without this, hadAdaptiveUpdate only ever reads true via collectRun's
729
- // Node-path object aliasing (no postMessage clone in that path), never in
730
- // the shipping worker. Shallow copy so the posted object isn't the same
731
- // mutable reference the worker keeps mutating (mirrors endSqlExecution's
732
- // `{ ...exec }` pattern above).
841
+ // Re-emit a 'sql' message so the browser's structured-cloned appModel.sql copy sees the flip:
842
+ // otherwise hadAdaptiveUpdate only reads true via collectRun's Node-path object aliasing, never
843
+ // in the shipping worker. Shallow copy so the posted object isn't the mutable reference the worker keeps mutating.
733
844
  return { type: 'sql', data: { ...exec } };
734
845
  }
735
846
 
@@ -793,12 +904,8 @@ export function processEvent(event , state ) {
793
904
  if (!stage) return null;
794
905
  stage.completedAt = info['Completion Time'] ?? 0;
795
906
  stage.stageFailureReason = info['Failure Reason'] ?? null;
796
- // finalizeStage (Task 5, stage-quantiles.ts) deliberately keeps its
797
- // `stage` parameter typed as a loose Record (see that module's own
798
- // comment); StageRecord's real shape (more precise than that loose
799
- // type, e.g. taskAttempts values carry string/boolean fields too, not
800
- // just numbers) is bridged across that boundary with an explicit cast
801
- // rather than re-litigating finalizeStage's own established signature.
907
+ // finalizeStage keeps its `stage` parameter typed as a loose Record (see that module); bridge
908
+ // StageRecord's more precise shape across that boundary with an explicit cast.
802
909
  return finalizeStage(
803
910
  id,
804
911
  stage ,
@@ -835,8 +942,7 @@ export function processEvent(event , state ) {
835
942
  }
836
943
  }
837
944
 
838
- // Derived (not hand-maintained) from SparkEventSchema itself, so this set
839
- // can never drift from the discriminated union's real literal list.
945
+ // Derived from SparkEventSchema itself, so this set can never drift from the union's literal list.
840
946
  const KNOWN_EVENT_TYPES = new Set(
841
947
  SparkEventSchema.options.map((option) => option.shape.Event.value)
842
948
  );
@@ -849,13 +955,9 @@ export function dispatchLine(line , state , emit
849
955
  state.skippedLines++;
850
956
  return;
851
957
  }
852
- // A real Spark event log carries many event types this tool has never
853
- // modeled (BlockManagerAdded, TaskStart, ExecutorMetricsUpdate, ...): that
854
- // was always true pre-migration and is completely benign, so an `Event`
855
- // value outside our 15 modeled literals is silently ignored exactly like
856
- // before, never counted toward skippedLines. Only a value that IS one of
857
- // the 15 known types but fails ITS schema is a genuine, valuable signal
858
- // worth surfacing as a skipped line.
958
+ // A real Spark event log carries many event types this tool never modeled (BlockManagerAdded,
959
+ // TaskStart, ...): an `Event` outside our modeled literals is silently ignored, never counted as
960
+ // a skipped line. Only a KNOWN type that fails ITS schema is a genuine signal worth surfacing.
859
961
  const eventType = (parsed )?.Event;
860
962
  if (typeof eventType !== 'string' || !KNOWN_EVENT_TYPES.has(eventType)) {
861
963
  return;
@@ -869,11 +971,9 @@ export function dispatchLine(line , state , emit
869
971
  try {
870
972
  msg = processEvent(result.data, state);
871
973
  } catch (err) {
872
- // processEvent's switch is exhaustive over the schema-validated union
873
- // (assertNever in the default case), so this should be unreachable for
874
- // any event that made it this far. If it ever fires, it's a genuine bug
875
- // in a handler, not malformed input; log it distinctly so it doesn't
876
- // masquerade as an ordinary skipped-line (malformed/invalid data) count.
974
+ // processEvent's switch is exhaustive over the schema-validated union (assertNever default), so
975
+ // this is unreachable for any event that got this far. If it fires it's a handler bug, not
976
+ // malformed input; log it distinctly so it doesn't masquerade as an ordinary skipped-line count.
877
977
  console.error('processEvent threw for an already-validated event:', err);
878
978
  state.skippedLines++;
879
979
  return;
@@ -881,10 +981,8 @@ export function dispatchLine(line , state , emit
881
981
  if (msg) emit(msg);
882
982
  }
883
983
 
884
- // Gather every stage's post-completion-populated executorMetrics for a single
885
- // re-post just before `done` (see the SparkListenerStageExecutorMetrics case
886
- // for why the per-stage message posted at completion is empty). Returns a
887
- // Map<stageId, Map<execId, metrics>>; empty when the log had no
984
+ // Gather every stage's post-completion-populated executorMetrics for a single re-post just before
985
+ // `done` (the per-stage message posted at completion is empty). Empty when the log had no
888
986
  // spark.eventLog.logStageExecutorMetrics data.
889
987
  export function collectStageExecutorMetrics(state ) {
890
988
  const out = new Map ();