sparkforensics-server 0.5.2 → 0.8.1

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 (445) hide show
  1. package/index.js +16 -7
  2. package/package.json +1 -1
  3. package/public/assets/AutoscalingChurn-DenWKTv-.js +1 -0
  4. package/public/assets/CacheUtilization-DLz7NdjV.js +1 -0
  5. package/public/assets/CachingOpportunity-zpQIqhzm.js +1 -0
  6. package/public/assets/ChartTheme-DAiARbLP.js +8 -0
  7. package/public/assets/ColdStart-C-9HjxWl.js +1 -0
  8. package/public/assets/ConfigAudit-D0V9tcrJ.js +1 -0
  9. package/public/assets/CoreUsageArea-yeerrljd.js +1 -0
  10. package/public/assets/Dashboard-DkUPrHKV.js +1 -0
  11. package/public/assets/DisclosureContext-DK1_iz6J.js +1 -0
  12. package/public/assets/DuplicatePlanSubtree-xDNDbCd3.js +1 -0
  13. package/public/assets/DurationHistogram-B6jsE0Ov.js +1 -0
  14. package/public/assets/EmptyState-C1uL40VU.js +1 -0
  15. package/public/assets/ExecutorUtilization-DXDZKmSc.js +1 -0
  16. package/public/assets/GcPressure-MRf7I4IG.js +1 -0
  17. package/public/assets/ImpactEstimate-Dm3xWbzV.js +1 -0
  18. package/public/assets/IncompleteRun-CaLOhMtf.js +1 -0
  19. package/public/assets/JobFailures-CzmzlvhO.js +1 -0
  20. package/public/assets/MemoryUtilization-AWUMUjCH.js +1 -0
  21. package/public/assets/NestedLoopJoin-C9sNr6Ss.js +1 -0
  22. package/public/assets/OverBroadcast-BOmRE6ca.js +1 -0
  23. package/public/assets/PartitionHint-BL2EmJfE.js +1 -0
  24. package/public/assets/PartitionSizing-BzBySvUq.js +1 -0
  25. package/public/assets/PlanExplorer-DdiQRcJU.js +1 -0
  26. package/public/assets/PlanGraphRoute-CIH9gBuR.js +1 -0
  27. package/public/assets/PlanView-B1hxzFRs.js +2 -0
  28. package/public/assets/PythonUdf-DvgCCueD.js +1 -0
  29. package/public/assets/RetryWaste-uXXu_CyM.js +1 -0
  30. package/public/assets/RowPagination-C-LzWkI5.js +1 -0
  31. package/public/assets/RowStatusCluster-Dbtmte7l.js +1 -0
  32. package/public/assets/ShuffleIO-nALwzC1_.js +1 -0
  33. package/public/assets/Skew-CHMEKgvk.js +1 -0
  34. package/public/assets/SlowHost-yyvjpHk6.js +1 -0
  35. package/public/assets/SmallFiles-y6odJ97n.js +1 -0
  36. package/public/assets/SpeculationWaste-C-SD2LOt.js +1 -0
  37. package/public/assets/Spill-DHTMmOup.js +1 -0
  38. package/public/assets/StageFailed-CQ3DxC9R.js +1 -0
  39. package/public/assets/StageFindingGroup-BgYLyj5q.js +1 -0
  40. package/public/assets/StageHeader-CIETkjVZ.js +1 -0
  41. package/public/assets/StagePill--P7PDd3s.js +1 -0
  42. package/public/assets/StageShape-BIv8vx_o.js +1 -0
  43. package/public/assets/StageSlowness-MMqzERxN.js +1 -0
  44. package/public/assets/Straggler-BgrY7AyW.js +1 -0
  45. package/public/assets/TaskFailures-BHHyKYTs.js +1 -0
  46. package/public/assets/TinyTask-DDScNhCl.js +1 -0
  47. package/public/assets/UnderBroadcast-CGIC04TI.js +1 -0
  48. package/public/assets/WidgetLeadSummary-CXFZP00C.js +1 -0
  49. package/public/assets/button-DbpTz2yk.js +1 -0
  50. package/public/assets/charts-vendor-CtTxwTeu.js +59 -0
  51. package/public/assets/composite-C9Nu2EI9.js +1 -0
  52. package/public/assets/d3-vendor-DNlD2-cz.js +1 -0
  53. package/public/assets/detector-registry-Bp8hrLua.js +2 -0
  54. package/public/assets/downsample-WmNM1qc9.js +1 -0
  55. package/public/assets/finding-anchor-Dr40LkS9.js +1 -0
  56. package/public/assets/findings-of-type-BL2u3RGZ.js +1 -0
  57. package/public/assets/impact-sort-mC5ISgX8.js +1 -0
  58. package/public/assets/inbox-KPU1Gppm.js +1 -0
  59. package/public/assets/index-CaRVPvAT.css +2 -0
  60. package/public/assets/index-fDiwfuoO.js +14 -0
  61. package/public/assets/inertValue-L9VxV0VE.js +1 -0
  62. package/public/assets/instrument-sans-latin-ext-wdth-normal-C5E2Gvlv.woff2 +0 -0
  63. package/public/assets/instrument-sans-latin-wdth-normal-BVScPF0l.woff2 +0 -0
  64. package/public/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  65. package/public/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  66. package/public/assets/layout.worker-CLcBHxVp.js +3 -0
  67. package/public/assets/{parser-worker-CA5RxqKc.js → parser-worker-CRIOPMOW.js} +8 -8
  68. package/public/assets/plan-graph-vendor-Ce6d4HU6.js +7 -0
  69. package/public/assets/react-vendor-C6KcUD36.js +9 -0
  70. package/public/assets/select-BHqOqun5.js +1 -0
  71. package/public/assets/toggle-DPeN6aNa.js +1 -0
  72. package/public/assets/toggle-group-Kl3uyHSr.js +1 -0
  73. package/public/assets/usePagedRows-BafD6bEN.js +1 -0
  74. package/public/assets/useRecentFiles-DzJZAoOT.js +3 -0
  75. package/public/assets/useRegisterFieldControl-pQv-ZDoU.js +1 -0
  76. package/public/assets/useScrollLock-BCxn_dMp.js +1 -0
  77. package/public/assets/useSortMode-DjrAN-z4.js +1 -0
  78. package/public/docs/404.html +3 -7
  79. package/public/docs/assets/{app.eYTPqTeq.js → app.1gvzd5Zb.js} +1 -1
  80. package/public/docs/assets/chunks/@localSearchIndexroot.DS2TQP5_.js +1 -0
  81. package/public/docs/assets/chunks/{VPLocalSearchBox.DsZrQKQR.js → VPLocalSearchBox.CNxT3s0I.js} +1 -1
  82. package/public/docs/assets/chunks/{theme.DsTNE2pE.js → theme.C5lLqUv7.js} +2 -2
  83. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.CqKM_jEG.js +1 -0
  84. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.CqKM_jEG.lean.js +1 -0
  85. package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.BYDo3ClR.js} +1 -1
  86. package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.ajUAVdbc.js +1 -0
  87. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.WqY74T0T.js +1 -0
  88. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.WqY74T0T.lean.js +1 -0
  89. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.CHHgywoP.js +1 -0
  90. package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.wj6Et6UD.lean.js → contributor-guide_architecture_detector-contract.md.CHHgywoP.lean.js} +1 -1
  91. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.DIz3_oz9.js +1 -0
  92. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.DIz3_oz9.lean.js +1 -0
  93. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.WH4tRHPL.js +1 -0
  94. package/public/docs/assets/{contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.lean.js → contributor-guide_architecture_detector-contract_threshold-tables.md.WH4tRHPL.lean.js} +1 -1
  95. package/public/docs/assets/{contributor-guide_architecture_drill-down.md.CqZPrdo9.js → contributor-guide_architecture_drill-down.md.yB-Hz2XT.js} +1 -1
  96. package/public/docs/assets/{contributor-guide_architecture_drill-down.md.CqZPrdo9.lean.js → contributor-guide_architecture_drill-down.md.yB-Hz2XT.lean.js} +1 -1
  97. package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.BQZE04sw.js +1 -0
  98. package/public/docs/assets/{contributor-guide_architecture_drill-down_plan-graph.md.DwOozIEs.lean.js → contributor-guide_architecture_drill-down_plan-graph.md.BQZE04sw.lean.js} +1 -1
  99. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.K71YB-Rr.js +1 -0
  100. package/public/docs/assets/{contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.lean.js → contributor-guide_architecture_impact-estimation.md.K71YB-Rr.lean.js} +1 -1
  101. package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.DLCAffwn.js +1 -0
  102. 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.DLCAffwn.lean.js} +1 -1
  103. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.JFrzDMwo.js +1 -0
  104. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.JFrzDMwo.lean.js +1 -0
  105. package/public/docs/assets/contributor-guide_architecture_overview.md.nxkpOhgf.js +1 -0
  106. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.U-9KMDFK.js +1 -0
  107. package/public/docs/assets/{contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.lean.js → contributor-guide_architecture_state-and-history_run-comparison-internals.md.U-9KMDFK.lean.js} +1 -1
  108. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BrSz8Ezh.js +1 -0
  109. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.B7rjSF_h.js +1 -0
  110. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.B7rjSF_h.lean.js +1 -0
  111. package/public/docs/assets/{contributor-guide_architecture_widget-rendering_list-sort-mode.md.BuHKgH0C.js → contributor-guide_architecture_widget-rendering_list-sort-mode.md.37hcZJ7q.js} +1 -1
  112. package/public/docs/assets/{contributor-guide_architecture_widget-rendering_row-expand-contract.md.ZT89u9f1.js → contributor-guide_architecture_widget-rendering_row-expand-contract.md.mK_VmAWk.js} +1 -1
  113. package/public/docs/assets/{contributor-guide_architecture_worker-protocol.md.BAnhi2-d.js → contributor-guide_architecture_worker-protocol.md.Br6OE7RW.js} +1 -1
  114. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.Br6OE7RW.lean.js +1 -0
  115. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.IVyaXrGe.js +1 -0
  116. 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.IVyaXrGe.lean.js} +1 -1
  117. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DSVFPpWb.js → contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DVSXn-bb.js} +1 -1
  118. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-availability.md.CjimIDBY.js → contributor-guide_architecture_worker-protocol_evidence-availability.md.D3S5cyOW.js} +2 -2
  119. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-availability.md.CjimIDBY.lean.js → contributor-guide_architecture_worker-protocol_evidence-availability.md.D3S5cyOW.lean.js} +1 -1
  120. package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.CtmCU57I.js +1 -0
  121. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.lean.js → contributor-guide_architecture_worker-protocol_evidence-report.md.CtmCU57I.lean.js} +1 -1
  122. package/public/docs/assets/contributor-guide_contributing.md.DBW4drtZ.js +1 -0
  123. package/public/docs/assets/{contributor-guide_development-setup.md.D9bu4j_c.js → contributor-guide_development-setup.md.B21ZTDhn.js} +3 -3
  124. package/public/docs/assets/contributor-guide_development-setup.md.B21ZTDhn.lean.js +1 -0
  125. package/public/docs/assets/contributor-guide_testing.md.DpkozNoG.js +4 -0
  126. package/public/docs/assets/{contributor-guide_testing.md.By6XGR9a.lean.js → contributor-guide_testing.md.DpkozNoG.lean.js} +1 -1
  127. package/public/docs/assets/{index.md.CHJVslga.js → index.md.lMm7SUMz.js} +1 -1
  128. package/public/docs/assets/{index.md.CHJVslga.lean.js → index.md.lMm7SUMz.lean.js} +1 -1
  129. package/public/docs/assets/instrument-sans-latin-ext-wdth-normal.C5E2Gvlv.woff2 +0 -0
  130. package/public/docs/assets/instrument-sans-latin-wdth-normal.BVScPF0l.woff2 +0 -0
  131. package/public/docs/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
  132. package/public/docs/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
  133. package/public/docs/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
  134. package/public/docs/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
  135. package/public/docs/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
  136. package/public/docs/assets/style.Cfxqlj0y.css +1 -0
  137. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.C0pcQoNc.js +29 -0
  138. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.C0pcQoNc.lean.js +1 -0
  139. package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.CGaxPX_1.js +1 -0
  140. package/public/docs/assets/{user-guide_getting-started_tuning-thresholds.md.B_MyjyZm.js → user-guide_getting-started_tuning-thresholds.md.BGVE-wrc.js} +2 -2
  141. package/public/docs/assets/{user-guide_getting-started_tuning-thresholds.md.B_MyjyZm.lean.js → user-guide_getting-started_tuning-thresholds.md.BGVE-wrc.lean.js} +1 -1
  142. package/public/docs/assets/{user-guide_mcp-tools.md.C65SjWUL.js → user-guide_mcp-tools.md.49r3L2YE.js} +1 -1
  143. package/public/docs/assets/{user-guide_mcp-tools_compare-runs.md.Cc_gLoix.js → user-guide_mcp-tools_compare-runs.md.BQ_kIptC.js} +11 -6
  144. package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.BQ_kIptC.lean.js +1 -0
  145. package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.azCZXkH8.js +44 -0
  146. package/public/docs/assets/{user-guide_mcp-tools_diagnose-run.md.BP1vnHA5.lean.js → user-guide_mcp-tools_diagnose-run.md.azCZXkH8.lean.js} +1 -1
  147. package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.js → user-guide_mcp-tools_evaluate-budgets.md.dpQpRQ7z.js} +2 -2
  148. package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.lean.js → user-guide_mcp-tools_evaluate-budgets.md.dpQpRQ7z.lean.js} +1 -1
  149. package/public/docs/assets/{user-guide_mcp-tools_get-finding-evidence.md.rKyu4wts.js → user-guide_mcp-tools_get-finding-evidence.md.CH6FkNLX.js} +2 -1
  150. package/public/docs/assets/{user-guide_mcp-tools_get-finding-evidence.md.rKyu4wts.lean.js → user-guide_mcp-tools_get-finding-evidence.md.CH6FkNLX.lean.js} +1 -1
  151. 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
  152. package/public/docs/assets/user-guide_run-comparison.md.B_LOkdBf.js +1 -0
  153. package/public/docs/assets/user-guide_run-comparison.md.B_LOkdBf.lean.js +1 -0
  154. package/public/docs/assets/user-guide_understanding-findings.md.C4riKCKl.js +1 -0
  155. package/public/docs/assets/user-guide_understanding-findings.md.C4riKCKl.lean.js +1 -0
  156. package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +6 -10
  157. package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +6 -10
  158. package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +6 -10
  159. package/public/docs/contributor-guide/architecture/board-widgets.html +6 -10
  160. package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +6 -10
  161. package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +6 -10
  162. package/public/docs/contributor-guide/architecture/detector-contract.html +6 -10
  163. package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +6 -10
  164. package/public/docs/contributor-guide/architecture/drill-down.html +6 -10
  165. package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +6 -10
  166. package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +6 -10
  167. package/public/docs/contributor-guide/architecture/impact-estimation.html +6 -10
  168. package/public/docs/contributor-guide/architecture/index.html +5 -9
  169. package/public/docs/contributor-guide/architecture/overview.html +6 -10
  170. package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +5 -9
  171. package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +6 -10
  172. package/public/docs/contributor-guide/architecture/state-and-history.html +5 -9
  173. package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +6 -10
  174. package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +5 -9
  175. package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +6 -10
  176. package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +5 -9
  177. package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +6 -10
  178. package/public/docs/contributor-guide/architecture/widget-rendering.html +6 -10
  179. package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +6 -10
  180. package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +6 -10
  181. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +7 -11
  182. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +6 -10
  183. package/public/docs/contributor-guide/architecture/worker-protocol.html +6 -10
  184. package/public/docs/contributor-guide/contributing.html +6 -10
  185. package/public/docs/contributor-guide/development-setup.html +8 -12
  186. package/public/docs/contributor-guide/testing.html +9 -10
  187. package/public/docs/favicon.svg +2 -2
  188. package/public/docs/hashmap.json +1 -1
  189. package/public/docs/index.html +6 -10
  190. package/public/docs/logo-dark.svg +1 -0
  191. package/public/docs/logo-light.svg +1 -0
  192. package/public/docs/tuning-reference/anti-patterns.html +5 -9
  193. package/public/docs/tuning-reference/aqe.html +5 -9
  194. package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +5 -9
  195. package/public/docs/tuning-reference/bottleneck-cold-start.html +5 -9
  196. package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +5 -9
  197. package/public/docs/tuning-reference/bottleneck-failures.html +5 -9
  198. package/public/docs/tuning-reference/bottleneck-gc.html +5 -9
  199. package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +5 -9
  200. package/public/docs/tuning-reference/bottleneck-memory-utilization.html +5 -9
  201. package/public/docs/tuning-reference/bottleneck-retry-waste.html +5 -9
  202. package/public/docs/tuning-reference/bottleneck-shuffle.html +5 -9
  203. package/public/docs/tuning-reference/bottleneck-skew.html +5 -9
  204. package/public/docs/tuning-reference/bottleneck-slow-host.html +5 -9
  205. package/public/docs/tuning-reference/bottleneck-small-files.html +5 -9
  206. package/public/docs/tuning-reference/bottleneck-spill.html +5 -9
  207. package/public/docs/tuning-reference/bottleneck-straggler.html +5 -9
  208. package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +5 -9
  209. package/public/docs/tuning-reference/bottleneck-utilization.html +5 -9
  210. package/public/docs/tuning-reference/caching.html +5 -9
  211. package/public/docs/tuning-reference/cluster-config.html +5 -9
  212. package/public/docs/tuning-reference/config.html +5 -9
  213. package/public/docs/tuning-reference/data-formats.html +5 -9
  214. package/public/docs/tuning-reference/index.html +5 -9
  215. package/public/docs/tuning-reference/intro.html +5 -9
  216. package/public/docs/tuning-reference/joins.html +5 -9
  217. package/public/docs/tuning-reference/memory-model.html +5 -9
  218. package/public/docs/tuning-reference/metrics.html +5 -9
  219. package/public/docs/tuning-reference/partitioning.html +5 -9
  220. package/public/docs/tuning-reference/pyspark.html +5 -9
  221. package/public/docs/tuning-reference/shuffle.html +5 -9
  222. package/public/docs/tuning-reference/spark-architecture.html +5 -9
  223. package/public/docs/tuning-reference/table-formats.html +5 -9
  224. package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +5 -9
  225. package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +5 -9
  226. package/public/docs/user-guide/alternative-log-retrieval.html +5 -9
  227. package/public/docs/user-guide/getting-started/ci-and-automation.html +17 -15
  228. package/public/docs/user-guide/getting-started/reading-the-dashboard.html +6 -10
  229. package/public/docs/user-guide/getting-started/regression-metric-keys.html +5 -9
  230. package/public/docs/user-guide/getting-started/tuning-thresholds.html +7 -11
  231. package/public/docs/user-guide/getting-started.html +5 -9
  232. package/public/docs/user-guide/mcp-tools/compare-runs.html +16 -15
  233. package/public/docs/user-guide/mcp-tools/diagnose-run.html +28 -11
  234. package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +6 -10
  235. package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +5 -9
  236. package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +7 -10
  237. package/public/docs/user-guide/mcp-tools/get-reference-doc.html +5 -9
  238. package/public/docs/user-guide/mcp-tools/get-run-summary.html +6 -10
  239. package/public/docs/user-guide/mcp-tools/list-runs.html +5 -9
  240. package/public/docs/user-guide/mcp-tools.html +6 -10
  241. package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +5 -9
  242. package/public/docs/user-guide/run-comparison.html +6 -10
  243. package/public/docs/user-guide/understanding-findings.html +6 -10
  244. package/public/export-template.html +46 -46
  245. package/public/favicon.svg +2 -2
  246. package/public/index.html +8 -10
  247. package/public/sample-runs/sample-run.ndjson.gz +0 -0
  248. package/vendor-core/allocation.js +126 -39
  249. package/vendor-core/analyzer.js +85 -58
  250. package/vendor-core/api-merge-writes.js +3 -0
  251. package/vendor-core/aqe-skew.js +347 -0
  252. package/vendor-core/cli/budgets.js +6 -4
  253. package/vendor-core/cli/collect-run-pair.js +89 -0
  254. package/vendor-core/cli/collect-run-worker.js +16 -0
  255. package/vendor-core/cli/collect-run.js +1 -2
  256. package/vendor-core/cli/native-zstd.js +12 -16
  257. package/vendor-core/comparability.js +234 -0
  258. package/vendor-core/comparison-output.js +15 -7
  259. package/vendor-core/comparison-verdict.js +105 -25
  260. package/vendor-core/core-source-hash.txt +1 -1
  261. package/vendor-core/core-time-series.js +0 -1
  262. package/vendor-core/core-usage-locality.js +1 -1
  263. package/vendor-core/delta-log-paths.js +10 -1
  264. package/vendor-core/detectors.js +1078 -416
  265. package/vendor-core/docs-config.js +0 -4
  266. package/vendor-core/docs-content/detection/cache.md +2 -1
  267. package/vendor-core/docs-content/detection/cfg.md +19 -11
  268. package/vendor-core/docs-content/detection/cold.md +2 -1
  269. package/vendor-core/docs-content/detection/gc.md +7 -3
  270. package/vendor-core/docs-content/detection/host.md +11 -1
  271. package/vendor-core/docs-content/detection/mem.md +17 -6
  272. package/vendor-core/docs-content/detection/part.md +3 -1
  273. package/vendor-core/docs-content/detection/plan.md +55 -13
  274. package/vendor-core/docs-content/detection/shfl.md +11 -5
  275. package/vendor-core/docs-content/detection/skew.md +48 -1
  276. package/vendor-core/docs-content/detection/slow.md +4 -2
  277. package/vendor-core/docs-content/detection/spec.md +6 -1
  278. package/vendor-core/docs-content/detection/spill.md +6 -3
  279. package/vendor-core/docs-content/detection/strag.md +19 -6
  280. package/vendor-core/docs-content/detection/tiny.md +9 -3
  281. package/vendor-core/docs-content/detection/util.md +6 -2
  282. package/vendor-core/effective-conf.js +7 -10
  283. package/vendor-core/efficiency-model.js +24 -16
  284. package/vendor-core/etl-phases.js +1 -2
  285. package/vendor-core/event-handlers.js +175 -57
  286. package/vendor-core/event-schemas.js +21 -16
  287. package/vendor-core/evidence-availability.js +9 -2
  288. package/vendor-core/evidence-report.js +118 -35
  289. package/vendor-core/executor-peaks.js +41 -0
  290. package/vendor-core/export-data.js +5 -5
  291. package/vendor-core/finding-identity.js +78 -0
  292. package/vendor-core/finding-presentation.js +129 -39
  293. package/vendor-core/finding-types.js +108 -30
  294. package/vendor-core/format-utils.js +14 -15
  295. package/vendor-core/html-export.js +1 -1
  296. package/vendor-core/impact-band.js +2 -2
  297. package/vendor-core/impact-format.js +47 -19
  298. package/vendor-core/impact-model.js +28 -17
  299. package/vendor-core/job-groups.js +4 -4
  300. package/vendor-core/mcp-server-factory.js +48 -12
  301. package/vendor-core/mcp-tools.js +179 -24
  302. package/vendor-core/median.js +2 -2
  303. package/vendor-core/nested-loop-join.js +73 -0
  304. package/vendor-core/occupancy.js +1 -2
  305. package/vendor-core/parser-worker.js +1 -1
  306. package/vendor-core/plan-graph-model.js +14 -22
  307. package/vendor-core/plan-node-detail.js +2 -3
  308. package/vendor-core/plan-summary.js +24 -19
  309. package/vendor-core/plan-tree-walk.js +6 -8
  310. package/vendor-core/python-stage.js +12 -1
  311. package/vendor-core/recommendation-rollup.js +13 -12
  312. package/vendor-core/redact.js +52 -46
  313. package/vendor-core/remediation.js +10 -5
  314. package/vendor-core/run-aggregates.js +6 -8
  315. package/vendor-core/run-comparison.js +226 -66
  316. package/vendor-core/run-identity.js +12 -0
  317. package/vendor-core/run-interpretation.js +21 -0
  318. package/vendor-core/run-metrics.js +2 -2
  319. package/vendor-core/run-output.js +44 -11
  320. package/vendor-core/run-totals.js +12 -0
  321. package/vendor-core/run-verdict.js +76 -3
  322. package/vendor-core/scaling-sim.js +3 -4
  323. package/vendor-core/snappy-block.js +2 -2
  324. package/vendor-core/spark-conf.js +168 -0
  325. package/vendor-core/sql-stages.js +2 -2
  326. package/vendor-core/stage-alignment.js +2 -12
  327. package/vendor-core/stage-identity.js +58 -3
  328. package/vendor-core/stage-label.js +14 -0
  329. package/vendor-core/stage-plan-nodes.js +17 -3
  330. package/vendor-core/stage-quantiles.js +141 -9
  331. package/vendor-core/stage-structure.js +17 -3
  332. package/vendor-core/threshold-overrides.js +9 -3
  333. package/vendor-core/types.js +70 -23
  334. package/vendor-core/wall-clock.js +8 -0
  335. package/vendor-core/wasted-core-hours.js +12 -12
  336. package/vendor-core/write-targets.js +146 -14
  337. package/vendor-core/zstd-worker-client.js +1 -1
  338. package/vendor-core/zstd-worker.js +1 -1
  339. package/public/assets/AutoscalingChurn-8t3U3isq.js +0 -1
  340. package/public/assets/CacheUtilization-CJs9tTum.js +0 -1
  341. package/public/assets/CachingOpportunity-D1NcHs0V.js +0 -1
  342. package/public/assets/ColdStart-CdRJ9gD8.js +0 -1
  343. package/public/assets/ConfigAudit-B5Gbe5js.js +0 -1
  344. package/public/assets/CoreUsageArea-a--NTRGY.js +0 -1
  345. package/public/assets/Dashboard-C1hNeX49.js +0 -2
  346. package/public/assets/DisclosureContext-ZpE6CkHx.js +0 -1
  347. package/public/assets/DuplicatePlanSubtree-dobXdz7E.js +0 -1
  348. package/public/assets/ExecutorUtilization-D9grwi7t.js +0 -1
  349. package/public/assets/GcPressure-8-G2ri10.js +0 -1
  350. package/public/assets/IncompleteRun-B91lky7a.js +0 -1
  351. package/public/assets/JobFailures-BsWvBMyJ.js +0 -1
  352. package/public/assets/MemoryUtilization-BM5schZI.js +0 -1
  353. package/public/assets/OverBroadcast-DdN8JIJH.js +0 -1
  354. package/public/assets/PartitionHint-CUvbnnxy.js +0 -1
  355. package/public/assets/PartitionSizing-CY5HDYtp.js +0 -1
  356. package/public/assets/PlanExplorer-DgHCDBD_.js +0 -1
  357. package/public/assets/PlanGraphRoute-DH3KJb8j.js +0 -1
  358. package/public/assets/RetryWaste-DOnE9vGF.js +0 -1
  359. package/public/assets/ShuffleIO-MMnDESkF.js +0 -1
  360. package/public/assets/Skew-BmZZJ5ms.js +0 -1
  361. package/public/assets/SlowHost-CsJzfEqo.js +0 -1
  362. package/public/assets/SmallFiles-BoK3R1Ex.js +0 -1
  363. package/public/assets/SpeculationWaste-CnVxB5AJ.js +0 -1
  364. package/public/assets/Spill-BhNicyBy.js +0 -1
  365. package/public/assets/StageFailed-mAhHU4l_.js +0 -1
  366. package/public/assets/StageFindingGroup-BEtPWcT0.js +0 -1
  367. package/public/assets/StageHeader-DJkYkrsU.js +0 -1
  368. package/public/assets/StageShape-BDxRknLp.js +0 -1
  369. package/public/assets/StageSlowness-BPRw-otN.js +0 -1
  370. package/public/assets/Straggler-CZCbnOWP.js +0 -1
  371. package/public/assets/TaskFailures-C57YcLFQ.js +0 -1
  372. package/public/assets/TinyTask-Bi84qU7n.js +0 -1
  373. package/public/assets/UnderBroadcast-DdQV7htE.js +0 -1
  374. package/public/assets/charts-vendor-0VMaLVLN.js +0 -59
  375. package/public/assets/finding-anchor-Cl6_5hfV.js +0 -1
  376. package/public/assets/impact-sort-CssZ6MT2.js +0 -1
  377. package/public/assets/index-2h_TgUAb.css +0 -2
  378. package/public/assets/index-B-03mG7K.js +0 -14
  379. package/public/assets/plan-graph-vendor-BYl-AzN5.js +0 -7
  380. package/public/assets/react-vendor-DEdfrRTJ.js +0 -9
  381. package/public/assets/select-CQjNhprm.js +0 -1
  382. package/public/assets/usePagedRows-Dyl6T8Ok.js +0 -1
  383. package/public/assets/useRecentFiles-XZEnLlal.js +0 -11
  384. package/public/assets/useSortMode-Dhvit8dt.js +0 -1
  385. package/public/docs/assets/chunks/@localSearchIndexroot.u_e5bQz4.js +0 -1
  386. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.js +0 -1
  387. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.lean.js +0 -1
  388. package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.RlzvUmNm.js +0 -1
  389. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.js +0 -1
  390. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.lean.js +0 -1
  391. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.wj6Et6UD.js +0 -1
  392. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.js +0 -1
  393. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.lean.js +0 -1
  394. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.js +0 -1
  395. package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.DwOozIEs.js +0 -1
  396. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.js +0 -1
  397. package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.js +0 -1
  398. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.js +0 -1
  399. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.lean.js +0 -1
  400. package/public/docs/assets/contributor-guide_architecture_overview.md.C1sSvm9i.js +0 -1
  401. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.js +0 -1
  402. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BOZ1azXG.js +0 -1
  403. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.js +0 -1
  404. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.lean.js +0 -1
  405. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.BAnhi2-d.lean.js +0 -1
  406. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.js +0 -1
  407. package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.js +0 -1
  408. package/public/docs/assets/contributor-guide_contributing.md.CQMvGwFt.js +0 -1
  409. package/public/docs/assets/contributor-guide_development-setup.md.D9bu4j_c.lean.js +0 -1
  410. package/public/docs/assets/contributor-guide_testing.md.By6XGR9a.js +0 -1
  411. package/public/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  412. package/public/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  413. package/public/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  414. package/public/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  415. package/public/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  416. package/public/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  417. package/public/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  418. package/public/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  419. package/public/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  420. package/public/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  421. package/public/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  422. package/public/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  423. package/public/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  424. package/public/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  425. package/public/docs/assets/style.tmDhD3c8.css +0 -1
  426. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.js +0 -23
  427. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.lean.js +0 -1
  428. package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.Obf3W3__.js +0 -1
  429. package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.Cc_gLoix.lean.js +0 -1
  430. package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.BP1vnHA5.js +0 -23
  431. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.js +0 -1
  432. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.lean.js +0 -1
  433. package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.js +0 -1
  434. package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.lean.js +0 -1
  435. /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.BYDo3ClR.lean.js} +0 -0
  436. /package/public/docs/assets/{contributor-guide_architecture_board-widgets_plan-advisor.md.RlzvUmNm.lean.js → contributor-guide_architecture_board-widgets_plan-advisor.md.ajUAVdbc.lean.js} +0 -0
  437. /package/public/docs/assets/{contributor-guide_architecture_overview.md.C1sSvm9i.lean.js → contributor-guide_architecture_overview.md.nxkpOhgf.lean.js} +0 -0
  438. /package/public/docs/assets/{contributor-guide_architecture_widget-rendering.md.BOZ1azXG.lean.js → contributor-guide_architecture_widget-rendering.md.BrSz8Ezh.lean.js} +0 -0
  439. /package/public/docs/assets/{contributor-guide_architecture_widget-rendering_list-sort-mode.md.BuHKgH0C.lean.js → contributor-guide_architecture_widget-rendering_list-sort-mode.md.37hcZJ7q.lean.js} +0 -0
  440. /package/public/docs/assets/{contributor-guide_architecture_widget-rendering_row-expand-contract.md.ZT89u9f1.lean.js → contributor-guide_architecture_widget-rendering_row-expand-contract.md.mK_VmAWk.lean.js} +0 -0
  441. /package/public/docs/assets/{contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DSVFPpWb.lean.js → contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DVSXn-bb.lean.js} +0 -0
  442. /package/public/docs/assets/{contributor-guide_contributing.md.CQMvGwFt.lean.js → contributor-guide_contributing.md.DBW4drtZ.lean.js} +0 -0
  443. /package/public/docs/assets/{user-guide_getting-started_reading-the-dashboard.md.Obf3W3__.lean.js → user-guide_getting-started_reading-the-dashboard.md.CGaxPX_1.lean.js} +0 -0
  444. /package/public/docs/assets/{user-guide_mcp-tools.md.C65SjWUL.lean.js → user-guide_mcp-tools.md.49r3L2YE.lean.js} +0 -0
  445. /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
@@ -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,15 @@
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-fDiwfuoO.js"></script>
27
24
  <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-hePW80VL.js">
28
- <link rel="modulepreload" crossorigin href="./assets/charts-vendor-0VMaLVLN.js">
29
- <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">
32
- <link rel="stylesheet" crossorigin href="./assets/plan-graph-vendor-DLioOiRN.css">
33
- <link rel="stylesheet" crossorigin href="./assets/index-2h_TgUAb.css">
25
+ <link rel="modulepreload" crossorigin href="./assets/react-vendor-C6KcUD36.js">
26
+ <link rel="modulepreload" crossorigin href="./assets/DisclosureContext-DK1_iz6J.js">
27
+ <link rel="modulepreload" crossorigin href="./assets/inertValue-L9VxV0VE.js">
28
+ <link rel="modulepreload" crossorigin href="./assets/button-DbpTz2yk.js">
29
+ <link rel="modulepreload" crossorigin href="./assets/composite-C9Nu2EI9.js">
30
+ <link rel="modulepreload" crossorigin href="./assets/useScrollLock-BCxn_dMp.js">
31
+ <link rel="stylesheet" crossorigin href="./assets/index-CaRVPvAT.css">
34
32
  </head>
35
33
  <body>
36
34
  <div id="root"></div>
@@ -1,12 +1,15 @@
1
+ import { computePeakConcurrentExecutorCount } from './core-count.js';
1
2
  import { parseSparkMemoryMB } from './spark-memory.js';
2
-
3
+ import { effectiveSparkConf, executorOverheadSettings, } from './spark-conf.js';
4
+
3
5
 
4
6
  const MS_PER_HOUR = 3_600_000;
7
+ const MS_PER_SECOND = 1000;
5
8
  const MIB_PER_GIB = 1024;
6
9
  // Spark's documented floor and factor for the default executor memory overhead
7
- // (spark.executor.memoryOverhead = max(factor * executor memory, 384 MiB)).
8
- const MIN_OVERHEAD_MIB = 384;
9
- const DEFAULT_OVERHEAD_FACTOR = 0.1;
10
+ // (spark.executor.memoryOverhead = max(factor * executor memory, 384 MiB)), for a run whose
11
+ // Spark version does not read the factor or minimum properties.
12
+ const OVERHEAD_FALLBACK = { floorMB: 384, floorPct: 0.1 };
10
13
 
11
14
 
12
15
   
@@ -14,12 +17,29 @@ const DEFAULT_OVERHEAD_FACTOR = 0.1;
14
17
   
15
18
 
16
19
 
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+  
31
+
17
32
 
18
33
 
34
+ // The fields the allocation reads from the parsed model's executor events, so a detector's
35
+ // narrower event shapes qualify too.
36
+
37
+
38
+
19
39
 
20
-
21
-
40
+
22
41
 
42
+
23
43
 
24
44
 
25
45
  /** The latest timestamp the log records: the application start and end and every stage and
@@ -36,71 +56,138 @@ export function lastObservedTimestamp(input ) {
36
56
  return last;
37
57
  }
38
58
 
39
- // Spark's default executor memory when spark.executor.memory is unset.
40
- const DEFAULT_EXECUTOR_MEMORY_MIB = 1024;
41
-
42
59
  // One container's memory in MiB, as Spark requests it from the cluster manager: executor heap,
43
- // plus overhead, plus off-heap and PySpark worker memory when configured. Null when the log
44
- // records no Spark properties (nothing to tell a default from a missing config) or a memory key it
45
- // does record cannot be read.
46
- function executorMemoryMiB(app ) {
60
+ // plus overhead, plus off-heap and PySpark worker memory when configured. Every setting is read
61
+ // from the run's effective conf (the logged value, else Spark's default for its version). Null when
62
+ // the log records no Spark properties (nothing to tell a default from a missing config) or a memory
63
+ // key it does record cannot be read.
64
+ function executorMemoryMiB(app ) {
47
65
  const config = app?.config;
48
66
  if (config == null) return null;
67
+ const layers = { sparkVersion: app?.sparkVersion, properties: config };
68
+ const conf = (key ) => effectiveSparkConf(layers, key)?.value;
49
69
  // undefined: key absent; null: present but unreadable.
50
- const mib = (key ) => (config[key] == null ? undefined : parseSparkMemoryMB(config[key]));
51
- const heap = mib('spark.executor.memory') ?? (config['spark.executor.memory'] == null ? DEFAULT_EXECUTOR_MEMORY_MIB : null);
70
+ const mib = (key ) => {
71
+ const value = conf(key);
72
+ return value === undefined ? undefined : parseSparkMemoryMB(value);
73
+ };
74
+ const heap = mib('spark.executor.memory');
52
75
  if (heap == null) return null;
53
76
 
54
77
  let overhead = mib('spark.executor.memoryOverhead');
55
78
  if (overhead === undefined) overhead = mib('spark.yarn.executor.memoryOverhead'); // legacy key
56
79
  if (overhead === undefined) {
57
- const factor = Number.parseFloat(config['spark.executor.memoryOverheadFactor'] ?? '');
58
- overhead = Math.max(MIN_OVERHEAD_MIB, Math.round(heap * (Number.isFinite(factor) && factor > 0 ? factor : DEFAULT_OVERHEAD_FACTOR)));
80
+ const { minMB, factor } = executorOverheadSettings(layers, OVERHEAD_FALLBACK);
81
+ overhead = Math.max(minMB, Math.round(heap * factor));
59
82
  }
60
- const offHeap = String(config['spark.memory.offHeap.enabled']).toLowerCase() === 'true' ? mib('spark.memory.offHeap.size') ?? 0 : 0;
83
+ const offHeap = conf('spark.memory.offHeap.enabled')?.toLowerCase() === 'true' ? mib('spark.memory.offHeap.size') ?? 0 : 0;
61
84
  const pyspark = mib('spark.executor.pyspark.memory') ?? 0;
62
85
  if (overhead === null || offHeap === null || pyspark === null) return null;
63
86
  return heap + overhead + offHeap + pyspark;
64
87
  }
65
88
 
89
+ // The logged `spark.dynamicAllocation.enabled`: a value the app set. Spark's default (off) is not a
90
+ // logged key, so a run that never set it reads null.
91
+ function loggedDynamicAllocation(app ) {
92
+ const effective = effectiveSparkConf({ sparkVersion: app?.sparkVersion, properties: app?.config }, 'spark.dynamicAllocation.enabled');
93
+ return effective == null || effective.source === 'default' ? null : effective.value.toLowerCase() === 'true' ? 'on' : 'off';
94
+ }
95
+
66
96
  /** Allocated core-hours and memory GiB-hours from the executor lifecycle: each executor counts
67
97
  * from its ExecutorAdded timestamp to its first later ExecutorRemoved timestamp. One with no
68
98
  * removal closes at the application end when the log has one, else at the last timestamp the log
69
99
  * records (a cut-off log). Cores are the ExecutorAdded event's Total Cores, else
70
- * spark.executor.cores; memory per executor is spark.executor.memory (default 1g) plus the
71
- * overhead (spark.executor.memoryOverhead, else the legacy spark.yarn.executor.memoryOverhead,
72
- * else the larger of 384 MiB and spark.executor.memoryOverheadFactor, default 0.1, times the
73
- * memory), plus spark.memory.offHeap.size when spark.memory.offHeap.enabled is true, plus
74
- * spark.executor.pyspark.memory.
100
+ * spark.executor.cores; memory per executor is the container Spark requests (executorMemoryMiB).
101
+ * The executor counts and seconds come from the same alive intervals.
75
102
  * Null, never 0, for a figure whose inputs the log lacks. */
76
103
  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]);
104
+ const dynamicAllocation = loggedDynamicAllocation(input.app);
105
+ const alive = aliveIntervals(input);
106
+ if (alive.length === 0) {
107
+ return { coreHours: null, memoryGbHours: null, dynamicAllocation, executorsPeak: null, executorsMean: null, executorCores: null, executorSeconds: null };
84
108
  }
85
109
  const closeAt = input.app?.endTime ?? lastObservedTimestamp(input);
86
- const configuredCores = Number.parseInt(input.app?.config?.['spark.executor.cores'] ?? '', 10);
87
110
  const memoryMiB = executorMemoryMiB(input.app);
88
111
 
89
112
  let coreMs = 0;
90
113
  let memoryMiBMs = 0;
114
+ let aliveMsTotal = 0;
91
115
  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;
116
+ const coreCounts = new Set ();
117
+ for (const { start, end, cores } of alive) {
118
+ const aliveMs = end - start;
119
+ aliveMsTotal += aliveMs;
120
+ if (cores == null) coresKnown = false; else { coreMs += cores * aliveMs; coreCounts.add(cores); }
100
121
  memoryMiBMs += (memoryMiB ?? 0) * aliveMs;
101
122
  }
123
+ const startTime = input.app?.startTime;
124
+ const windowMs = closeAt != null && startTime != null ? closeAt - startTime : 0;
125
+ const firstRemovals = alive.flatMap((a) => (a.removedAt == null ? [] : [{ executorId: a.executorId, timestamp: a.removedAt }]));
102
126
  return {
103
127
  coreHours: coresKnown ? coreMs / MS_PER_HOUR : null,
104
128
  memoryGbHours: memoryMiB != null ? memoryMiBMs / MIB_PER_GIB / MS_PER_HOUR : null,
129
+ dynamicAllocation,
130
+ executorsPeak: computePeakConcurrentExecutorCount(alive.map((a) => ({ executorId: a.executorId, timestamp: a.start })), firstRemovals),
131
+ executorsMean: windowMs > 0 ? aliveMsTotal / windowMs : null,
132
+ executorCores: coresKnown && coreCounts.size === 1 ? [...coreCounts][0] : null,
133
+ executorSeconds: aliveMsTotal / MS_PER_SECOND,
105
134
  };
106
135
  }
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
144
+
145
+
146
+ // Each executor's alive interval: from its ExecutorAdded timestamp to its first later
147
+ // ExecutorRemoved timestamp, else the application end, else the last timestamp the log records.
148
+ // The one definition computeAllocation and allocatedCoreMsWithin share.
149
+ function aliveIntervals(input ) {
150
+ const removedAt = new Map ();
151
+ for (const e of input.executors.removed) {
152
+ const times = removedAt.get(e.executorId);
153
+ if (times) times.push(e.timestamp); else removedAt.set(e.executorId, [e.timestamp]);
154
+ }
155
+ const closeAt = input.app?.endTime ?? lastObservedTimestamp(input);
156
+ const configuredCores = Number.parseInt(effectiveSparkConf({ sparkVersion: input.app?.sparkVersion, properties: input.app?.config }, 'spark.executor.cores')?.value ?? '', 10);
157
+ const out = [];
158
+ const seen = new Set ();
159
+ for (const e of input.executors.added) {
160
+ if (seen.has(e.executorId)) continue; // a replayed ExecutorAdded is the same executor
161
+ seen.add(e.executorId);
162
+ const removal = (removedAt.get(e.executorId) ?? []).filter((t) => t >= e.timestamp).sort((a, b) => a - b)[0] ?? null;
163
+ out.push({
164
+ executorId: e.executorId,
165
+ start: e.timestamp,
166
+ end: Math.max(e.timestamp, removal ?? closeAt ?? e.timestamp),
167
+ removedAt: removal,
168
+ cores: e.totalCores != null && e.totalCores > 0 ? e.totalCores : Number.isFinite(configuredCores) ? configuredCores : null,
169
+ });
170
+ }
171
+ return out;
172
+ }
173
+
174
+ /** Allocated core-milliseconds: the figure behind `coreHours`, the capacity idle-core figures are
175
+ * measured against. Null when the log has no executor events or an executor's cores are unknown. */
176
+ export function allocatedCoreMs(input ) {
177
+ const { coreHours } = computeAllocation(input);
178
+ return coreHours != null && coreHours > 0 ? coreHours * MS_PER_HOUR : null;
179
+ }
180
+
181
+ /** The part of the allocated core-milliseconds held inside `windows` (disjoint, sorted
182
+ * intervals, such as the union of the stages' windows), counted from the same alive intervals as
183
+ * `allocatedCoreMs`. Null when `allocatedCoreMs` is. */
184
+ export function allocatedCoreMsWithin(input , windows ) {
185
+ if (allocatedCoreMs(input) == null) return null;
186
+ let coreMs = 0;
187
+ for (const { start, end, cores } of aliveIntervals(input)) {
188
+ for (const [from, to] of windows) {
189
+ coreMs += (cores ?? 0) * Math.max(0, Math.min(end, to) - Math.max(start, from));
190
+ }
191
+ }
192
+ return coreMs;
193
+ }
@@ -1,11 +1,12 @@
1
- import { DETECTORS, ENTRY_BY_TYPE, } from './detectors.js';
1
+ import { DETECTORS, ENTRY_BY_TYPE, hasStageOutsideSql, skewThresholdsFor, } from './detectors.js';
2
2
  import { effectiveThresholds, findingTunedThresholds, overridesFor, tunedThresholdsNote } from './threshold-overrides.js';
3
3
  import { computePeakConcurrentCores } from './core-count.js';
4
4
  import { assertNever } from './assert-never.js';
5
5
  import { estimateImpact, } from './impact-estimator.js';
6
6
  import { computeOccupancy, } from './occupancy.js';
7
7
  import { deriveImpactBand, IMPACT_FLOOR_PCT_CRIT, IMPACT_FLOOR_PCT_WARN, } from './impact-band.js';
8
- import { IMPACT_BAND_ORDER } from './format-utils.js';
8
+ import { IMPACT_BAND_ORDER, MS_PER_CORE_HOUR } from './format-utils.js';
9
+ import { computeWastedCoreHours } from './wasted-core-hours.js';
9
10
 
10
11
 
11
12
 
@@ -17,7 +18,7 @@ const detectors = DETECTORS;
17
18
 
18
19
 
19
20
 
20
-
21
+
21
22
 
22
23
 
23
24
 
@@ -33,8 +34,8 @@ function fnv1a(str ) {
33
34
  return (h >>> 0).toString(16).padStart(8, '0');
34
35
  }
35
36
 
36
- // The id hash's discriminator slots, in the order it has always joined them: reordering or renaming
37
- // one changes every finding id, which needs an EVIDENCE_SCHEMA_VERSION bump.
37
+ // The id hash's discriminator slots, in join order: reordering or renaming one changes every
38
+ // finding id, which needs an EVIDENCE_SCHEMA_VERSION bump.
38
39
  const DISCRIMINATOR_SLOTS = [
39
40
  'host', 'executorId', 'rule', 'variant', 'dimension',
40
41
  'direction', 'nodeName', 'rootName', 'subtreeSize', 'groupIndex', 'largerSideBytes',
@@ -43,16 +44,11 @@ const DISCRIMINATOR_SLOTS = [
43
44
 
44
45
 
45
46
  // Per type, the fields that tell apart sibling findings sharing one location and metric; without
46
- // them those siblings hash to one id:
47
- // slowHost (host), memoryUtilization (executorId), partitionSizing (rule);
48
- // cacheUtilization (rddId+variant), cachingOpportunity (relation/format for leaf,
49
- // operator+relation for composite, executionIds as last resort);
50
- // smallFiles (direction/nodeName), duplicatePlanSubtree (groupIndex is the real
51
- // uniqueness guarantee: rootName+subtreeSize can collide across groups),
52
- // underBroadcast (value+largerSideBytes per node/side).
53
- // memoryUtilization leaves out `rule`: one heap band per executor, so executorId is already
54
- // unique and folding rule in risks id churn if band logic changes. partitionSizing keeps
55
- // `rule`: a stage can emit several rules at once sharing stageId+metric.
47
+ // them those siblings hash to one id (see detector-contract.md, Finding ids). Notes:
48
+ // duplicatePlanSubtree's groupIndex is the real uniqueness guarantee (rootName+subtreeSize can
49
+ // collide across groups); memoryUtilization leaves out `rule` because its one heap band sits on
50
+ // the busiest executor, so executorId is already unique; partitionSizing keeps `rule` because a
51
+ // stage can emit several rules at once sharing stageId+metric.
56
52
  const ID_DISCRIMINATORS = {
57
53
  skew: [], stageShape: ['rule'], shuffle: [], partitionSizing: ['rule'], spill: [], gc: ['direction'],
58
54
  slowHost: ['host', 'executorId', 'variant', 'dimension'], stageSlowness: [], stageFailed: ['variant'],
@@ -62,6 +58,8 @@ const ID_DISCRIMINATORS
62
58
  cachingOpportunity: ['variant', 'relation', 'format', 'operator', 'executionIds'],
63
59
  jobFailureRate: [], configAudit: [],
64
60
  duplicatePlanSubtree: ['rootName', 'subtreeSize', 'groupIndex'], smallFiles: ['direction', 'nodeName'],
61
+ pythonUdf: [],
62
+ nestedLoopJoin: ['nodeName'],
65
63
  underBroadcast: ['largerSideBytes'], overBroadcast: [],
66
64
  };
67
65
 
@@ -88,35 +86,6 @@ export function findingId(f ) {
88
86
  return fnv1a(`${f.type}|${locationKey(f)}|${f.metric ?? ''}|${f.value ?? f.valueText ?? ''}|${disc}`);
89
87
  }
90
88
 
91
- // skew (either branch) and straggler both claim the stage's replayed tail recovery
92
- // (tailReplayRecoveryMs via tailRecoveryMs): the same slow-task tail reported by two detectors
93
- // (see "Overlap caveat: skew / straggler" in impact-estimation/caveats-tuning-and-coverage.md). skew's branch only changes
94
- // the fallback single-task delta on a stage without the replay, so every skew + straggler pair
95
- // on a stage is flagged. Flags both sides via validationRequired (rather than suppressing
96
- // either) so neither finding's own diagnostic value is lost; the flag rides the same
97
- // confidence-caveat UI a reader already sees before trusting either finding's magnitude.
98
- function overlapNote(otherType ) {
99
- return `This overlaps with the ${otherType} finding on this stage: both measure the same slow-task tail, so don't add their recoverable-time figures together.`;
100
- }
101
-
102
- function flagSkewStragglerOverlap(findings ) {
103
- const skewStages = new Set(
104
- findings.filter((f) => f.type === 'skew' && f.stageId != null).map((f) => f.stageId),
105
- );
106
- if (skewStages.size === 0) return;
107
- const stragglerStages = new Set(
108
- findings.filter((f) => f.type === 'straggler' && f.stageId != null).map((f) => f.stageId),
109
- );
110
- const overlapStages = new Set([...skewStages].filter((id) => stragglerStages.has(id)));
111
- if (overlapStages.size === 0) return;
112
- for (const f of findings) {
113
- if (f.stageId == null || !overlapStages.has(f.stageId)) continue;
114
- const note = f.type === 'skew' ? overlapNote('straggler') : f.type === 'straggler' ? overlapNote('skew') : null;
115
- if (!note) continue;
116
- f.validationRequired = [f.validationRequired, note].filter(Boolean).join(' ');
117
- }
118
- }
119
-
120
89
  function push(out , entry , result , tuned = null) {
121
90
  if (!result) return;
122
91
  const detectorVersion = entry.version ?? 1;
@@ -176,6 +145,57 @@ function applySuppression(out ) {
176
145
  return out.filter((f) => f.stageId == null || !dropped.get(f.type)?.has(f.stageId));
177
146
  }
178
147
 
148
+ // Advice to lower spark.executor.memory (over-provisioned heap, low GC, idle memory-time) contradicts
149
+ // advice to raise it (high GC, spill, a GC-bound straggler tail) where a stage is short of memory.
150
+ // A stage-level lowering finding on the same stage as raising advice is dropped. A run-level one
151
+ // (heap, idle memory-time) stays, since other stages may well have memory to spare, but its advice
152
+ // to lower memory is replaced by a note naming the stages that ask for more.
153
+ const EXECUTOR_MEMORY_KEY = 'spark.executor.memory';
154
+
155
+ function changesExecutorMemory(f , direction ) {
156
+ return (f.remediation ?? []).some((r) => r.kind === 'conf' && r.key === EXECUTOR_MEMORY_KEY && r.direction === direction);
157
+ }
158
+
159
+ function reconcileExecutorMemoryAdvice(out ) {
160
+ const raising = out.filter((f) => changesExecutorMemory(f, 'increase'));
161
+ if (raising.length === 0) return out;
162
+ const raisingStages = new Set(raising.flatMap((f) => (f.stageId != null ? [f.stageId] : [])));
163
+ const stageList = [...raisingStages].sort((a, b) => a - b).join(', ');
164
+ const conflict = raisingStages.size > 0
165
+ ? `Stage${raisingStages.size === 1 ? '' : 's'} ${stageList} ask${raisingStages.size === 1 ? 's' : ''} for more executor memory, so confirm before lowering it.`
166
+ : 'Other findings ask for more executor memory, so confirm before lowering it.';
167
+ const reconciled = [];
168
+ for (const f of out) {
169
+ if (!changesExecutorMemory(f, 'decrease') || changesExecutorMemory(f, 'increase')) { reconciled.push(f); continue; }
170
+ if (f.stageId != null) {
171
+ if (!raisingStages.has(f.stageId)) reconciled.push(f);
172
+ continue;
173
+ }
174
+ reconciled.push({
175
+ ...f,
176
+ recommendation: `${f.recommendation ?? ''} ${conflict}`.trim(),
177
+ remediation: (f.remediation ?? []).filter((r) => !(r.kind === 'conf' && r.key === EXECUTOR_MEMORY_KEY && r.direction === 'decrease')),
178
+ });
179
+ }
180
+ return reconciled;
181
+ }
182
+
183
+ // The run's idle core-ms (the Wasted core-hours figure), the ceiling for one stage's idle claim.
184
+ function runIdleCoreMs(
185
+ app ,
186
+ executorsAdded ,
187
+ executorsRemoved ,
188
+ runAggregates ,
189
+ ) {
190
+ const { wastedCoreHours } = computeWastedCoreHours(
191
+ app,
192
+ executorsAdded ,
193
+ runAggregates,
194
+ executorsRemoved ,
195
+ );
196
+ return wastedCoreHours == null ? null : wastedCoreHours * MS_PER_CORE_HOUR;
197
+ }
198
+
179
199
  // `app` widened to `SparkAppInfo | null` to match real callers (AppModel.app is
180
200
  // nullable at the type level); every detector below tolerates a null app.
181
201
  export function analyze(
@@ -203,6 +223,7 @@ export function analyze(
203
223
  const impact = {
204
224
  stages, totalCores, sql,
205
225
  occupancy: computeOccupancy(stages , totalCores),
226
+ runIdleCoreMs: runIdleCoreMs(app, executorsAdded, executorsRemoved, runAggregates),
206
227
  };
207
228
  // The one cast from the posted-model types to the detector-side shapes: types.ts's Stage and
208
229
  // SqlExecution carry a catch-all index signature, while every field DetectorStage/DetectorSqlExec
@@ -211,6 +232,7 @@ export function analyze(
211
232
  app, jobs, executorsAdded, executorsRemoved, runAggregates, impact,
212
233
  stages: stages ,
213
234
  sql: sql ,
235
+ skewThresholds: skewThresholdsFor(thresholds),
214
236
  };
215
237
  const out = [];
216
238
  for (const d of detectors) {
@@ -238,11 +260,10 @@ export function analyze(
238
260
  assertNever(d);
239
261
  }
240
262
  }
241
- const findings = applySuppression(out);
263
+ const findings = reconcileExecutorMemoryAdvice(applySuppression(out));
242
264
  estimateImpact(findings, impact);
243
265
  noteTunedThresholds(findings);
244
266
  deriveImpactBand(findings, app, thresholds ? (type) => bandFloors(type, thresholds) : undefined);
245
- flagSkewStragglerOverlap(findings);
246
267
  // Ascending IMPACT_BAND_ORDER (critical 0 -> info 2) puts the worst band first;
247
268
  // stable sort keeps DETECTORS declaration order within a band.
248
269
  findings.sort((a, b) => IMPACT_BAND_ORDER[a.impactBand] - IMPACT_BAND_ORDER[b.impactBand]);
@@ -250,14 +271,17 @@ export function analyze(
250
271
  }
251
272
 
252
273
  // Memoizes auditConfig by `app` identity (like evidence-report.ts's jsonCache) so config detectors
253
- // don't re-run when both the export and the report path audit the same app. WeakMap can't key on
254
- // `null`, so that case skips the cache. Returns a fresh copy each call so a caller's in-place
255
- // mutation (e.g. `.sort()`) can't corrupt the cached array.
256
- const auditConfigCache = new WeakMap ();
274
+ // don't re-run when both the export and the report path audit the same app. The one stage-dependent
275
+ // check reads only whether any stage ran outside SQL, so that flag is part of the cache entry: a
276
+ // hit with a different flag recomputes. WeakMap can't key on `null`, so that case skips the cache.
277
+ // Returns a fresh copy each call so a caller's in-place mutation (e.g. `.sort()`) can't corrupt the
278
+ // cached array.
279
+ const auditConfigCache = new WeakMap ();
257
280
 
258
- function computeAuditConfig(app ) {
281
+ function computeAuditConfig(app , stages ) {
259
282
  const out = [];
260
- for (const d of detectors) if (d.scope === 'config') push(out, d, d.withThresholds()({ app }));
283
+ const target = { app, stages: stages };
284
+ for (const d of detectors) if (d.scope === 'config') push(out, d, d.withThresholds()(target));
261
285
  // configAudit's estimate is unconditionally costOnly('none'): needs no stages/totalCores, so an
262
286
  // empty context gives parity with analyze().
263
287
  estimateImpact(out, { stages: new Map(), occupancy: new Map(), totalCores: 0 });
@@ -265,11 +289,14 @@ function computeAuditConfig(app ) {
265
289
  return out.map((f) => ({ ...f, stageId: f.stageId ?? null }));
266
290
  }
267
291
 
268
- export function auditConfig(app ) {
269
- if (app === null) return computeAuditConfig(app);
292
+ /** The configuration findings for a run. `stages` lets checks that apply only to some kinds of
293
+ * work (the serializer note needs RDD stages) see the run's work; without it they stay silent. */
294
+ export function auditConfig(app , stages ) {
295
+ if (app === null) return computeAuditConfig(app, stages);
296
+ const rddWork = hasStageOutsideSql(stages );
270
297
  const cached = auditConfigCache.get(app);
271
- if (cached) return cached.slice();
272
- const result = computeAuditConfig(app);
273
- auditConfigCache.set(app, result);
274
- return result.slice();
298
+ if (cached?.rddWork === rddWork) return cached.findings.slice();
299
+ const findings = computeAuditConfig(app, stages);
300
+ auditConfigCache.set(app, { rddWork, findings });
301
+ return findings.slice();
275
302
  }
@@ -74,6 +74,9 @@ export function apiMergeWrites(sql )
74
74
  kind: resolved?.kind ?? null,
75
75
  target: resolved?.target ?? null,
76
76
  outputRows: null,
77
+ outputRowsSource: null,
78
+ outputBytes: null,
79
+ mergeRows: null,
77
80
  raw: `Delta MERGE operation, executions ${span}${group.unclean ? ` (${group.unclean})` : ''}`,
78
81
  };
79
82
  });