sparkforensics-server 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/assets/AutoscalingChurn-CRA2Mb7F.js +1 -0
- package/public/assets/CacheUtilization-Bh9OY_V9.js +1 -0
- package/public/assets/CachingOpportunity-oCiQwmTJ.js +1 -0
- package/public/assets/{ColdStart-B38OiAOp.js → ColdStart-BJrtjfmn.js} +1 -1
- package/public/assets/ConfigAudit-DMvKxySP.js +1 -0
- package/public/assets/CoreUsageArea-REC2zsKw.js +1 -0
- package/public/assets/Dashboard-BHZ76JCu.js +2 -0
- package/public/assets/DisclosureContext-OO7X1HAx.js +1 -0
- package/public/assets/{DuplicatePlanSubtree-BqS2A2j4.js → DuplicatePlanSubtree-BgG-L-cK.js} +1 -1
- package/public/assets/ExecutorUtilization-D2KhSVbY.js +1 -0
- package/public/assets/{GcPressure-CT1yzR_s.js → GcPressure-D1BTE8qh.js} +1 -1
- package/public/assets/{IncompleteRun-C7Qkjtjn.js → IncompleteRun-DD40xUpy.js} +1 -1
- package/public/assets/{JobFailures-WHl3ELfh.js → JobFailures-YwNzCF19.js} +1 -1
- package/public/assets/MemoryUtilization-9b8ee_3_.js +1 -0
- package/public/assets/{OverBroadcast-6oSSAQ_R.js → OverBroadcast-CfctS8a8.js} +1 -1
- package/public/assets/PartitionSizing-VG8kBgG5.js +1 -0
- package/public/assets/{PlanExplorer-DcxmiQWL.js → PlanExplorer-DmubTAOP.js} +1 -1
- package/public/assets/PlanGraphRoute-DctPZjS0.js +1 -0
- package/public/assets/RetryWaste-CnpM8bB7.js +1 -0
- package/public/assets/{ShuffleIO-CRhrYNE0.js → ShuffleIO-D13vGmas.js} +1 -1
- package/public/assets/Skew-DlagstYP.js +1 -0
- package/public/assets/SlowHost-cSkLRZ_e.js +1 -0
- package/public/assets/{SmallFiles-C082c2rd.js → SmallFiles-B-kERMcq.js} +1 -1
- package/public/assets/{SpeculationWaste-BS2vpadr.js → SpeculationWaste-DaysAzXO.js} +1 -1
- package/public/assets/Spill-H44Gz4US.js +1 -0
- package/public/assets/{StageFailed-D3ac9Sou.js → StageFailed-DCfii9h_.js} +1 -1
- package/public/assets/StageFindingGroup-BXJ0Mj5n.js +1 -0
- package/public/assets/{StageHeader-pI1nTwVf.js → StageHeader--oN2J-oD.js} +1 -1
- package/public/assets/StageShape-IZ80CZiJ.js +1 -0
- package/public/assets/{StageSlowness-BaNyfNvL.js → StageSlowness--BRIaBc1.js} +1 -1
- package/public/assets/Straggler-Ctq0Df_o.js +1 -0
- package/public/assets/TaskFailures-Bks_gqhL.js +1 -0
- package/public/assets/{TinyTask-8_5lpq_2.js → TinyTask-BssfzGFB.js} +1 -1
- package/public/assets/{UnderBroadcast-DJ0XSwth.js → UnderBroadcast-DsScrxc-.js} +1 -1
- package/public/assets/{finding-anchor-Cys8Z6Yn.js → finding-anchor-BYW1oz5R.js} +1 -1
- package/public/assets/{impact-sort-BTQndbof.js → impact-sort-xzy0g-Vh.js} +1 -1
- package/public/assets/index-DhpRGMZc.css +2 -0
- package/public/assets/index-DwqkFUFk.js +14 -0
- package/public/assets/instrument-sans-latin-ext-wdth-normal-C5E2Gvlv.woff2 +0 -0
- package/public/assets/instrument-sans-latin-wdth-normal-BVScPF0l.woff2 +0 -0
- package/public/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/public/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/public/assets/{parser-worker-DmeCnLWU.js → parser-worker-CA5RxqKc.js} +8 -8
- package/public/assets/react-vendor-DfpuPQZf.js +9 -0
- package/public/assets/{select-DIYN6D3y.js → select-C8edjYTq.js} +1 -1
- package/public/assets/{usePagedRows-BxvdFnPk.js → usePagedRows-CsuUFeYx.js} +1 -1
- package/public/assets/useRecentFiles-BvkKo3JN.js +11 -0
- package/public/assets/{useSortMode-B8SRfAoG.js → useSortMode-joX71BZN.js} +1 -1
- package/public/docs/404.html +3 -7
- package/public/docs/assets/{app.D2GqAkhy.js → app.CnWL6bKV.js} +1 -1
- package/public/docs/assets/chunks/@localSearchIndexroot.tE5pz54z.js +1 -0
- package/public/docs/assets/chunks/{VPLocalSearchBox.BS4cSmtW.js → VPLocalSearchBox.C0IBKeRQ.js} +1 -1
- package/public/docs/assets/chunks/{theme.BuxVpb0H.js → theme.BH-2HuRf.js} +2 -2
- package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.CQgn1t_M.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.wulOueeB.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.wj6Et6UD.js → contributor-guide_architecture_detector-contract.md.N4hhenrA.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.Dp3c40rx.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.DfvbqEUu.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.lean.js → contributor-guide_architecture_impact-estimation.md.DfvbqEUu.lean.js} +1 -1
- package/public/docs/assets/{contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.js → contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.Cwm1jHmn.js} +1 -1
- package/public/docs/assets/{contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.lean.js → contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.Cwm1jHmn.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.lean.js → contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.BX1XnFbq.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.js → contributor-guide_architecture_worker-protocol_evidence-report.md.CiQl1MLb.js} +1 -1
- package/public/docs/assets/{contributor-guide_development-setup.md.D9bu4j_c.js → contributor-guide_development-setup.md.DQdzera7.js} +1 -1
- package/public/docs/assets/instrument-sans-latin-ext-wdth-normal.C5E2Gvlv.woff2 +0 -0
- package/public/docs/assets/instrument-sans-latin-wdth-normal.BVScPF0l.woff2 +0 -0
- package/public/docs/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
- package/public/docs/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
- package/public/docs/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
- package/public/docs/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
- package/public/docs/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
- package/public/docs/assets/style.CyK9Ss73.css +1 -0
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.BEepbDDL.js +24 -0
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.BEepbDDL.lean.js +1 -0
- package/public/docs/assets/{user-guide_mcp-tools_compare-runs.md.CPowlb2q.js → user-guide_mcp-tools_compare-runs.md.Cc_gLoix.js} +3 -2
- package/public/docs/assets/{user-guide_mcp-tools_compare-runs.md.CPowlb2q.lean.js → user-guide_mcp-tools_compare-runs.md.Cc_gLoix.lean.js} +1 -1
- package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.js → user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.js} +2 -2
- package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.lean.js → user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.lean.js} +1 -1
- 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
- package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.js +1 -0
- package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.lean.js +1 -0
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.js +1 -0
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.lean.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.3C7KCEZO.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.3C7KCEZO.lean.js +1 -0
- package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +6 -10
- package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +4 -8
- package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +6 -10
- package/public/docs/contributor-guide/architecture/board-widgets.html +4 -8
- package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +4 -8
- package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +6 -10
- package/public/docs/contributor-guide/architecture/detector-contract.html +6 -10
- package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +4 -8
- package/public/docs/contributor-guide/architecture/drill-down.html +4 -8
- package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +6 -10
- package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +4 -8
- package/public/docs/contributor-guide/architecture/impact-estimation.html +6 -10
- package/public/docs/contributor-guide/architecture/index.html +4 -8
- package/public/docs/contributor-guide/architecture/overview.html +4 -8
- package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +4 -8
- package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +6 -10
- package/public/docs/contributor-guide/architecture/state-and-history.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +4 -8
- package/public/docs/contributor-guide/architecture/widget-rendering.html +4 -8
- package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +6 -10
- package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +4 -8
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +4 -8
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +6 -10
- package/public/docs/contributor-guide/architecture/worker-protocol.html +4 -8
- package/public/docs/contributor-guide/contributing.html +4 -8
- package/public/docs/contributor-guide/development-setup.html +6 -10
- package/public/docs/contributor-guide/testing.html +4 -8
- package/public/docs/favicon.svg +2 -2
- package/public/docs/hashmap.json +1 -1
- package/public/docs/index.html +4 -8
- package/public/docs/tuning-reference/anti-patterns.html +4 -8
- package/public/docs/tuning-reference/aqe.html +4 -8
- package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +4 -8
- package/public/docs/tuning-reference/bottleneck-cold-start.html +4 -8
- package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +4 -8
- package/public/docs/tuning-reference/bottleneck-failures.html +4 -8
- package/public/docs/tuning-reference/bottleneck-gc.html +4 -8
- package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +4 -8
- package/public/docs/tuning-reference/bottleneck-memory-utilization.html +4 -8
- package/public/docs/tuning-reference/bottleneck-retry-waste.html +4 -8
- package/public/docs/tuning-reference/bottleneck-shuffle.html +4 -8
- package/public/docs/tuning-reference/bottleneck-skew.html +4 -8
- package/public/docs/tuning-reference/bottleneck-slow-host.html +4 -8
- package/public/docs/tuning-reference/bottleneck-small-files.html +4 -8
- package/public/docs/tuning-reference/bottleneck-spill.html +4 -8
- package/public/docs/tuning-reference/bottleneck-straggler.html +4 -8
- package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +4 -8
- package/public/docs/tuning-reference/bottleneck-utilization.html +4 -8
- package/public/docs/tuning-reference/caching.html +4 -8
- package/public/docs/tuning-reference/cluster-config.html +4 -8
- package/public/docs/tuning-reference/config.html +4 -8
- package/public/docs/tuning-reference/data-formats.html +4 -8
- package/public/docs/tuning-reference/index.html +5 -9
- package/public/docs/tuning-reference/intro.html +4 -8
- package/public/docs/tuning-reference/joins.html +4 -8
- package/public/docs/tuning-reference/memory-model.html +4 -8
- package/public/docs/tuning-reference/metrics.html +4 -8
- package/public/docs/tuning-reference/partitioning.html +4 -8
- package/public/docs/tuning-reference/pyspark.html +4 -8
- package/public/docs/tuning-reference/shuffle.html +4 -8
- package/public/docs/tuning-reference/spark-architecture.html +4 -8
- package/public/docs/tuning-reference/table-formats.html +4 -8
- package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +4 -8
- package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +4 -8
- package/public/docs/user-guide/alternative-log-retrieval.html +4 -8
- package/public/docs/user-guide/getting-started/ci-and-automation.html +11 -13
- package/public/docs/user-guide/getting-started/reading-the-dashboard.html +4 -8
- package/public/docs/user-guide/getting-started/regression-metric-keys.html +4 -8
- package/public/docs/user-guide/getting-started/tuning-thresholds.html +4 -8
- package/public/docs/user-guide/getting-started.html +4 -8
- package/public/docs/user-guide/mcp-tools/compare-runs.html +8 -11
- package/public/docs/user-guide/mcp-tools/diagnose-run.html +4 -8
- package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +6 -10
- package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +4 -8
- package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +4 -8
- package/public/docs/user-guide/mcp-tools/get-reference-doc.html +4 -8
- package/public/docs/user-guide/mcp-tools/get-run-summary.html +6 -10
- package/public/docs/user-guide/mcp-tools/list-runs.html +4 -8
- package/public/docs/user-guide/mcp-tools.html +4 -8
- package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +6 -10
- package/public/docs/user-guide/run-comparison.html +6 -10
- package/public/docs/user-guide/understanding-findings.html +6 -10
- package/public/export-template.html +46 -46
- package/public/favicon.svg +2 -2
- package/public/index.html +4 -7
- package/vendor-core/allocation.js +105 -20
- package/vendor-core/api-merge-writes.js +1 -0
- package/vendor-core/cli/budgets.js +62 -2
- package/vendor-core/cli/regression-budgets.js +35 -0
- package/vendor-core/comparison-output.js +2 -1
- package/vendor-core/core-source-hash.txt +1 -1
- package/vendor-core/delta-log-paths.js +10 -1
- package/vendor-core/detectors.js +290 -92
- package/vendor-core/docs-content/detection/chrn.md +3 -1
- package/vendor-core/docs-content/detection/cold.md +2 -1
- package/vendor-core/docs-content/detection/mem.md +1 -1
- package/vendor-core/docs-content/detection/plan.md +9 -2
- package/vendor-core/docs-content/detection/shfl.md +8 -2
- package/vendor-core/docs-content/detection/skew.md +12 -5
- package/vendor-core/docs-content/detection/slow.md +6 -2
- package/vendor-core/docs-content/detection/spill.md +4 -1
- package/vendor-core/docs-content/detection/strag.md +5 -3
- package/vendor-core/docs-content/detection/tiny.md +3 -2
- package/vendor-core/docs-content/detection/util.md +6 -2
- package/vendor-core/efficiency-model.js +22 -14
- package/vendor-core/evidence-report.js +11 -11
- package/vendor-core/finding-presentation.js +52 -19
- package/vendor-core/finding-types.js +71 -16
- package/vendor-core/impact-model.js +2 -2
- package/vendor-core/mcp-server-factory.js +6 -2
- package/vendor-core/mcp-tools.js +10 -1
- package/vendor-core/median.js +8 -0
- package/vendor-core/remediation.js +10 -5
- package/vendor-core/run-comparison.js +2 -0
- package/vendor-core/run-output.js +14 -3
- package/vendor-core/sequence-alignment.js +83 -0
- package/vendor-core/stage-alignment.js +387 -63
- package/vendor-core/stage-quantiles.js +3 -1
- package/vendor-core/stage-structure.js +83 -0
- package/vendor-core/types.js +12 -2
- package/vendor-core/wall-clock.js +8 -0
- package/vendor-core/wasted-core-hours.js +12 -11
- package/vendor-core/write-targets.js +32 -0
- package/public/assets/AutoscalingChurn-5hDGIXi4.js +0 -1
- package/public/assets/CacheUtilization-CT2SCnJJ.js +0 -1
- package/public/assets/CachingOpportunity-D_O_WkbH.js +0 -1
- package/public/assets/ConfigAudit-BoplryCH.js +0 -1
- package/public/assets/CoreUsageArea-csgwNxkl.js +0 -1
- package/public/assets/Dashboard-BgKodJl2.js +0 -2
- package/public/assets/DisclosureContext-B0siMVdg.js +0 -1
- package/public/assets/ExecutorUtilization-Dp52QGWo.js +0 -1
- package/public/assets/MemoryUtilization-D8IUDHJI.js +0 -1
- package/public/assets/PartitionSizing-BwdFL-mw.js +0 -1
- package/public/assets/PlanGraphRoute-pqnHTt4i.js +0 -1
- package/public/assets/RetryWaste-BTbeHkJc.js +0 -1
- package/public/assets/Skew-DSNXRG9V.js +0 -1
- package/public/assets/SlowHost-B7Lwu2EU.js +0 -1
- package/public/assets/Spill-B4DVKHXx.js +0 -1
- package/public/assets/StageFindingGroup-D9naK109.js +0 -1
- package/public/assets/StageShape-eO5le8iG.js +0 -1
- package/public/assets/Straggler-D5Fnok_y.js +0 -1
- package/public/assets/TaskFailures-BWnC-K_G.js +0 -1
- package/public/assets/index-BcqTLKFp.css +0 -2
- package/public/assets/index-DCeWge12.js +0 -13
- package/public/assets/react-vendor-DEdfrRTJ.js +0 -9
- package/public/assets/useRecentFiles-C3Ov47U7.js +0 -11
- package/public/docs/assets/chunks/@localSearchIndexroot.D4OdkQjy.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.js +0 -1
- package/public/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/public/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/public/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/public/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/public/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/public/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/public/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/public/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/public/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/public/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/public/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/public/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/public/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/public/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/public/docs/assets/style.tmDhD3c8.css +0 -1
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.js +0 -22
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.lean.js +0 -1
- package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.js +0 -1
- package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.lean.js +0 -1
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.js +0 -1
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.lean.js +0 -1
- package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.js +0 -1
- package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.lean.js +0 -1
- /package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.lean.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.CQgn1t_M.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.lean.js → contributor-guide_architecture_board-widgets_visual-system-and-templating.md.wulOueeB.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.wj6Et6UD.lean.js → contributor-guide_architecture_detector-contract.md.N4hhenrA.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.lean.js → contributor-guide_architecture_detector-contract_threshold-tables.md.Dp3c40rx.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.lean.js → contributor-guide_architecture_worker-protocol_cli-and-mcp.md.BX1XnFbq.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.lean.js → contributor-guide_architecture_worker-protocol_evidence-report.md.CiQl1MLb.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_development-setup.md.D9bu4j_c.lean.js → contributor-guide_development-setup.md.DQdzera7.lean.js} +0 -0
- /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
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
// pairs. `compareRuns` calls it, and every surface that compares runs reads its result
|
|
4
4
|
// (`stagePairs`) instead of comparing stage keys.
|
|
5
5
|
//
|
|
6
|
-
// Pairing
|
|
7
|
-
// normalizer, so the volatile tokens that make two
|
|
8
|
-
// dates, IN-lists, Delta log file counts) and the
|
|
9
|
-
// patterns do not split a stage in two.
|
|
6
|
+
// Pairing runs in two levels (see `alignStages`). The exact key is `comparisonIdentity`: the
|
|
7
|
+
// `stageIdentity` recipe run with the comparison normalizer, so the volatile tokens that make two
|
|
8
|
+
// runs of one job differ (staging directories, dates, IN-lists, Delta log file counts) and the
|
|
9
|
+
// caller's `--normalize-path` patterns do not split a stage in two. A coarser structural key
|
|
10
|
+
// (`stage-structure.ts`) and a detail similarity pair what the exact key splits. `stageIdentity`
|
|
11
|
+
// itself stays the frozen exact key.
|
|
10
12
|
|
|
11
13
|
import { normalizeDetail } from './detectors.js';
|
|
12
14
|
import { isDeltaLogRead, scanRelationId } from './plan-summary.js';
|
|
13
15
|
import { withEarlierAttempts, totalExecutorCpuMs } from './run-totals.js';
|
|
14
16
|
import { planNodesOfStage } from './stage-plan-nodes.js';
|
|
15
|
-
import {
|
|
17
|
+
import { normalizeStageName, stageIdentityWith, } from './stage-identity.js';
|
|
18
|
+
import { alignSequences } from './sequence-alignment.js';
|
|
19
|
+
import { attributeNames, detailTokens, jaccard, structuralKey } from './stage-structure.js';
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
|
|
@@ -136,29 +140,83 @@ export const PAIR_DELTA_METRICS = [
|
|
|
136
140
|
|
|
137
141
|
|
|
138
142
|
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
139
146
|
|
|
140
|
-
|
|
147
|
+
|
|
148
|
+
|
|
141
149
|
|
|
142
150
|
|
|
143
151
|
|
|
152
|
+
|
|
153
|
+
/** The stages an aligned execution pair left unpaired when its two sides ran different numbers of
|
|
154
|
+
* stages: a different plan (a broadcast join in place of an exchange and a sort-merge join), not
|
|
155
|
+
* changed work. `deltas` totals each metric over the leftover stages of each side. */
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
144
180
|
|
|
145
181
|
|
|
146
182
|
|
|
147
|
-
|
|
183
|
+
|
|
148
184
|
|
|
149
|
-
|
|
150
|
-
|
|
185
|
+
|
|
186
|
+
|
|
151
187
|
|
|
152
188
|
|
|
153
|
-
|
|
154
|
-
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
155
192
|
|
|
193
|
+
|
|
156
194
|
|
|
157
195
|
|
|
158
196
|
|
|
159
197
|
|
|
160
198
|
|
|
161
199
|
|
|
200
|
+
|
|
201
|
+
/** An execution pair scores at least this (0-1) on call site, description and plan structure to be
|
|
202
|
+
* matched. */
|
|
203
|
+
export const EXECUTION_MATCH_MIN = 0.55;
|
|
204
|
+
/** Share of the smaller run's executions that must pair (see `ExecutionAlignment.agreement`) for the two runs
|
|
205
|
+
* to count as one job. Below it no stage pairs: stages as generic as a `count` or a `collect` repeat
|
|
206
|
+
* across unrelated jobs, and only the surrounding executions tell the jobs apart. */
|
|
207
|
+
export const MIN_EXECUTION_AGREEMENT = 0.5;
|
|
208
|
+
/** Detail similarity (Jaccard over a stage's name and normalized node text) two stages of different
|
|
209
|
+
* structure need to pair as `aligned`. */
|
|
210
|
+
export const ALIGNED_MIN_SIMILARITY = 0.6;
|
|
211
|
+
/** Score of a pair made by position alone: no plan evidence backs it. */
|
|
212
|
+
export const POSITIONAL_SCORE = 0.5;
|
|
213
|
+
/** Execution similarities cached at most (distinct signatures in one run times those in the other). */
|
|
214
|
+
const MAX_CACHED_SIMILARITIES = 1_000_000;
|
|
215
|
+
/** Leftover stage pairs scored in one execution pair; above it the `aligned` pass is skipped. */
|
|
216
|
+
const MAX_STAGE_SIMILARITY_CELLS = 250_000;
|
|
217
|
+
/** Whether replanned run time counts in the numerator of `runtimeCoverage` (and so toward the `ok`
|
|
218
|
+
* gate). When false it counts in the denominator only. */
|
|
219
|
+
export const REPLANNED_COUNTS_TOWARD_COVERAGE = true;
|
|
162
220
|
|
|
163
221
|
// Sum of a field over the stage's own record and the work its figures leave out (earlier
|
|
164
222
|
// attempts, a failed attempt's late tasks), the same all-attempt sums the run totals use.
|
|
@@ -172,7 +230,9 @@ function attemptSum(rows , field
|
|
|
172
230
|
return present ? sum : null;
|
|
173
231
|
}
|
|
174
232
|
|
|
175
|
-
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
function stageFigures(stage ) {
|
|
176
236
|
const rows = withEarlierAttempts([stage]);
|
|
177
237
|
return {
|
|
178
238
|
executorRunTime: attemptSum(rows, 'executorRunTime'),
|
|
@@ -186,77 +246,341 @@ function stageFigures(stage ) {
|
|
|
186
246
|
};
|
|
187
247
|
}
|
|
188
248
|
|
|
249
|
+
function deltasOf(b , c ) {
|
|
250
|
+
const deltas = {} ;
|
|
251
|
+
for (const metric of PAIR_DELTA_METRICS) {
|
|
252
|
+
const [bv, cv] = [b[metric], c[metric]];
|
|
253
|
+
deltas[metric] = { baseline: bv, candidate: cv, delta: bv != null && cv != null ? cv - bv : null };
|
|
254
|
+
}
|
|
255
|
+
return deltas;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Each metric summed over several stages' figures; null where no stage recorded it. */
|
|
259
|
+
function sumFigures(rows ) {
|
|
260
|
+
const total = {} ;
|
|
261
|
+
for (const metric of PAIR_DELTA_METRICS) {
|
|
262
|
+
let sum = 0, present = false;
|
|
263
|
+
for (const row of rows) if (row[metric] != null) { sum += row[metric] ; present = true; }
|
|
264
|
+
total[metric] = present ? sum : null;
|
|
265
|
+
}
|
|
266
|
+
return total;
|
|
267
|
+
}
|
|
268
|
+
|
|
189
269
|
const ascending = (a , b ) => a - b;
|
|
190
270
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
271
|
+
// ---- Per-run index --------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
// What the aligner knows about one SQL execution (or the pseudo-execution of stages outside any):
|
|
274
|
+
// the non-bookkeeping stages it ran and the three things its similarity reads.
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
// Stack frames of the JVM and Spark themselves say nothing about which user code ran.
|
|
303
|
+
const FRAMEWORK_FRAME = /^(java|javax|jdk|sun|scala|py4j|org\.apache|io\.delta|io\.netty|com\.sun)\b/;
|
|
304
|
+
|
|
305
|
+
function userFrames(details ) {
|
|
306
|
+
const frames = [];
|
|
307
|
+
for (const line of (details ?? '').split('\n')) {
|
|
308
|
+
const frame = line.trim().replace(/:\d+\)$/, ')'); // a shifted line number is not a different call site
|
|
309
|
+
if (frame && !FRAMEWORK_FRAME.test(frame)) frames.push(frame);
|
|
310
|
+
}
|
|
311
|
+
return frames;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Directory prefixes carry the run's own paths (a container directory, an application id), so only
|
|
315
|
+
// each path's last segment stays in a description.
|
|
316
|
+
const descriptionTokens = (text , normalizer ) =>
|
|
317
|
+
[...new Set(normalizer(text).replace(/\S*\/([^/\s]+)/g, '$1').split(/[^A-Za-z0-9_.$]+/).filter(Boolean))].sort();
|
|
318
|
+
|
|
319
|
+
// An execution's plan structure: its operator names (a repeated operator counts as often as it
|
|
320
|
+
// repeats) and the attribute names its nodes mention, which tell two plans of the same operators
|
|
321
|
+
// apart. Neither carries a literal, a path or an id.
|
|
322
|
+
function planStructure(root ) {
|
|
323
|
+
const tokens = [];
|
|
324
|
+
const attributes = new Set ();
|
|
325
|
+
(function walk(node ) {
|
|
326
|
+
if (!/^(WholeStageCodegen|InputAdapter|ColumnarToRow)\b/.test(node.name ?? '')) {
|
|
327
|
+
tokens.push(normalizeStageName(node.name ?? ''));
|
|
328
|
+
for (const name of attributeNames(node.detail ?? '')) attributes.add(`@${name}`);
|
|
329
|
+
}
|
|
330
|
+
for (const child of node.children ?? []) walk(child);
|
|
331
|
+
})(root);
|
|
332
|
+
return [...tokens, ...attributes].sort();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function indexRun(
|
|
336
|
+
snap , normalizer , signatures , interned ,
|
|
337
|
+
) {
|
|
338
|
+
const intern = (tokens ) => {
|
|
339
|
+
const ids = new Int32Array(tokens.length);
|
|
340
|
+
tokens.forEach((t, i) => {
|
|
341
|
+
let id = interned.get(t);
|
|
342
|
+
if (id === undefined) { id = interned.size; interned.set(t, id); }
|
|
343
|
+
ids[i] = id;
|
|
344
|
+
});
|
|
345
|
+
return ids.sort();
|
|
346
|
+
};
|
|
347
|
+
const nodesOf = new Map ();
|
|
348
|
+
const bookkeeping = new Set ();
|
|
349
|
+
const exactKey = new Map ();
|
|
350
|
+
const structuralKeyOf = new Map ();
|
|
351
|
+
const byExecution = new Map ();
|
|
352
|
+
for (const [id, stage] of snap.stages) {
|
|
353
|
+
const nodes = planNodesOfStage(stage, snap.sql);
|
|
354
|
+
nodesOf.set(id, nodes);
|
|
355
|
+
if (isDeltaBookkeepingStage(stage, snap.sql, nodes)) { bookkeeping.add(id); continue; }
|
|
356
|
+
exactKey.set(id, comparisonIdentity(stage, snap, normalizer, nodes));
|
|
357
|
+
structuralKeyOf.set(id, structuralKey(nodes));
|
|
358
|
+
const execId = stage.sqlExecutionId ?? null;
|
|
359
|
+
const ids = byExecution.get(execId);
|
|
360
|
+
if (ids) ids.push(id); else byExecution.set(execId, [id]);
|
|
361
|
+
}
|
|
362
|
+
const info = (execId , stageIds ) => {
|
|
363
|
+
stageIds.sort(ascending);
|
|
364
|
+
const root = execId === null ? null : snap.sql.get(execId)?.planTree ?? null;
|
|
365
|
+
const structure = root ? planStructure(root)
|
|
366
|
+
: stageIds.map((id) => structuralKeyOf.get(id) ?? `n:${normalizeStageName(snap.stages.get(id) .name ?? '')}`).sort();
|
|
367
|
+
const frames = [...new Set(stageIds.flatMap((id) => userFrames(snap.stages.get(id) .details)))].sort();
|
|
368
|
+
const description = execId === null ? [] : descriptionTokens(snap.sql.get(execId)?.description ?? '', normalizer);
|
|
369
|
+
const text = JSON.stringify([structure, frames, description]);
|
|
370
|
+
let signature = signatures.get(text);
|
|
371
|
+
if (signature === undefined) { signature = signatures.size; signatures.set(text, signature); }
|
|
372
|
+
return { id: execId, stageIds, structure: intern(structure), frames: intern(frames), description: intern(description), signature };
|
|
373
|
+
};
|
|
374
|
+
const executions = [...byExecution.keys()].filter((k) => k !== null).sort(ascending)
|
|
375
|
+
.map((execId) => info(execId, byExecution.get(execId) ));
|
|
376
|
+
const pseudoIds = byExecution.get(null);
|
|
377
|
+
return {
|
|
378
|
+
snap, nodesOf, bookkeeping, exactKey, structuralKey: structuralKeyOf, executions,
|
|
379
|
+
pseudo: pseudoIds ? info(null, pseudoIds) : null,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Weights of the three parts of an execution pair's similarity. A part one side lacks (a PySpark
|
|
384
|
+
// stack trace has no user frames, a description may be empty) drops out of the mean.
|
|
385
|
+
const STRUCTURE_WEIGHT = 4, FRAMES_WEIGHT = 2, DESCRIPTION_WEIGHT = 1;
|
|
386
|
+
|
|
387
|
+
/** 0-1000 (rounded, so the alignment compares integers). */
|
|
388
|
+
function executionSimilarity(a , b ) {
|
|
389
|
+
let sum = STRUCTURE_WEIGHT * jaccard(a.structure, b.structure), weight = STRUCTURE_WEIGHT;
|
|
390
|
+
if (a.frames.length > 0 && b.frames.length > 0) { sum += FRAMES_WEIGHT * jaccard(a.frames, b.frames); weight += FRAMES_WEIGHT; }
|
|
391
|
+
if (a.description.length > 0 && b.description.length > 0) {
|
|
392
|
+
sum += DESCRIPTION_WEIGHT * jaccard(a.description, b.description);
|
|
393
|
+
weight += DESCRIPTION_WEIGHT;
|
|
394
|
+
}
|
|
395
|
+
return Math.round((1000 * sum) / weight);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---- Stage alignment inside one execution pair -----------------------------------------------
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
function alignWithin(
|
|
403
|
+
base , cand , be , ce ,
|
|
404
|
+
tokens , normalizer ,
|
|
405
|
+
) {
|
|
406
|
+
const pseudo = be.id === null;
|
|
407
|
+
const freeBase = new Set(be.stageIds), freeCand = new Set(ce.stageIds);
|
|
408
|
+
const pairs = [];
|
|
409
|
+
const take = (baseId , candId , quality , score ) => {
|
|
410
|
+
pairs.push({ baseId, candId, quality, score });
|
|
411
|
+
freeBase.delete(baseId);
|
|
412
|
+
freeCand.delete(candId);
|
|
413
|
+
};
|
|
414
|
+
const tokensOf = (run , cache , id ) => {
|
|
415
|
+
let t = cache.get(id);
|
|
416
|
+
if (!t) { const stage = run.snap.stages.get(id) ; t = detailTokens(stage.name, run.nodesOf.get(id) , normalizer); cache.set(id, t); }
|
|
417
|
+
return t;
|
|
418
|
+
};
|
|
419
|
+
const similarity = (baseId , candId ) =>
|
|
420
|
+
jaccard(tokensOf(base, tokens.base, baseId), tokensOf(cand, tokens.cand, candId));
|
|
421
|
+
const group = (free , keyOf ) => {
|
|
422
|
+
const groups = new Map ();
|
|
423
|
+
for (const id of [...free].sort(ascending)) {
|
|
424
|
+
const key = keyOf(id);
|
|
425
|
+
if (key === null) continue;
|
|
426
|
+
const ids = groups.get(key);
|
|
427
|
+
if (ids) ids.push(id); else groups.set(key, [id]);
|
|
428
|
+
}
|
|
429
|
+
return groups;
|
|
430
|
+
};
|
|
431
|
+
// Stages sharing a key pair off in id order, as many as both sides have: a self-join's subtree
|
|
432
|
+
// counted three times in one run and twice in the other still pairs two.
|
|
433
|
+
const pairByKey = (
|
|
434
|
+
keyOfBase , keyOfCand ,
|
|
435
|
+
quality , scoreOf ,
|
|
436
|
+
) => {
|
|
437
|
+
const b = group(freeBase, keyOfBase), c = group(freeCand, keyOfCand);
|
|
438
|
+
for (const key of [...b.keys()].sort()) {
|
|
439
|
+
const candIds = c.get(key);
|
|
440
|
+
if (!candIds) continue;
|
|
441
|
+
const baseIds = b.get(key) ;
|
|
442
|
+
for (let i = 0; i < Math.min(baseIds.length, candIds.length); i++) take(baseIds[i], candIds[i], quality, scoreOf(baseIds[i], candIds[i]));
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Stages outside any SQL execution have no plan: the stage's own call-site text tells them apart.
|
|
447
|
+
const detailsKey = (run , id ) => normalizeStageName(normalizer(run.snap.stages.get(id) .details ?? ''));
|
|
448
|
+
pairByKey(
|
|
449
|
+
(id) => base.exactKey.get(id) + (pseudo ? `§${detailsKey(base, id)}` : ''),
|
|
450
|
+
(id) => cand.exactKey.get(id) + (pseudo ? `§${detailsKey(cand, id)}` : ''),
|
|
451
|
+
'exact', () => 1,
|
|
452
|
+
);
|
|
453
|
+
pairByKey((id) => base.structuralKey.get(id) ?? null, (id) => cand.structuralKey.get(id) ?? null, 'structural', similarity);
|
|
454
|
+
|
|
455
|
+
// Different structure, similar details: best score first, ties by lower ids.
|
|
456
|
+
const withStructure = (run , free ) => [...free].filter((id) => run.structuralKey.get(id) != null).sort(ascending);
|
|
457
|
+
const leftBase = withStructure(base, freeBase), leftCand = withStructure(cand, freeCand);
|
|
458
|
+
if (leftBase.length * leftCand.length <= MAX_STAGE_SIMILARITY_CELLS) {
|
|
459
|
+
const candidates = [];
|
|
460
|
+
for (const baseId of leftBase) for (const candId of leftCand) {
|
|
461
|
+
const score = similarity(baseId, candId);
|
|
462
|
+
if (score >= ALIGNED_MIN_SIMILARITY) candidates.push({ baseId, candId, score });
|
|
463
|
+
}
|
|
464
|
+
candidates.sort((x, y) => y.score - x.score || x.baseId + x.candId - (y.baseId + y.candId) || x.baseId - y.baseId);
|
|
465
|
+
for (const { baseId, candId, score } of candidates) {
|
|
466
|
+
if (freeBase.has(baseId) && freeCand.has(candId)) take(baseId, candId, 'aligned', score);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// No plan structure to compare: pair by position among the stages of the same name.
|
|
471
|
+
const byName = (run ) => (id ) =>
|
|
472
|
+
run.structuralKey.get(id) == null ? normalizeStageName(run.snap.stages.get(id) .name ?? '') : null;
|
|
473
|
+
pairByKey(byName(base), byName(cand), 'aligned', () => POSITIONAL_SCORE);
|
|
474
|
+
|
|
475
|
+
return { pairs, leftBase: [...freeBase].sort(ascending), leftCand: [...freeCand].sort(ascending) };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Pairs the stages of two runs in two levels. SQL executions align first, in submission order, by an
|
|
479
|
+
* order-preserving sequence alignment scored on call site, description and plan structure (the
|
|
480
|
+
* stages outside any execution form one pseudo-execution per run); then the stages of each aligned
|
|
481
|
+
* execution pair align: equal comparison key (`exact`), equal structural key (`structural`), similar
|
|
482
|
+
* details (`aligned`), or position among plan-less stages of one name (`aligned`). Stages an
|
|
483
|
+
* execution pair with different stage counts leaves over are `replanned`; with equal counts they
|
|
484
|
+
* stay `unmatched`. Deterministic, and swapping the runs mirrors the pairs. */
|
|
194
485
|
export function alignStages(baseSnap , candSnap , options = {}) {
|
|
195
486
|
const normalizer = comparisonDetailNormalizer(options.normalizePath);
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
);
|
|
208
|
-
|
|
487
|
+
const signatures = new Map ();
|
|
488
|
+
const interned = new Map ();
|
|
489
|
+
const base = indexRun(baseSnap, normalizer, signatures, interned), cand = indexRun(candSnap, normalizer, signatures, interned);
|
|
490
|
+
|
|
491
|
+
// Execution similarity is cached per signature pair, so a loop of identical queries scores once.
|
|
492
|
+
// When the runs hold so many distinct signatures that the cache could not repay its memory, it is off.
|
|
493
|
+
const distinct = (run ) => new Set(run.executions.map((e) => e.signature)).size;
|
|
494
|
+
const cached = distinct(base) * distinct(cand) <= MAX_CACHED_SIMILARITIES;
|
|
495
|
+
const cache = new Map ();
|
|
496
|
+
const similarityOf = (a , b ) => {
|
|
497
|
+
if (a.signature === b.signature) return 1000;
|
|
498
|
+
if (!cached) return executionSimilarity(a, b);
|
|
499
|
+
const key = a.signature * signatures.size + b.signature;
|
|
500
|
+
let s = cache.get(key);
|
|
501
|
+
if (s === undefined) { s = executionSimilarity(a, b); cache.set(key, s); }
|
|
502
|
+
return s;
|
|
209
503
|
};
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
504
|
+
const sequence = alignSequences(
|
|
505
|
+
base.executions.length, cand.executions.length,
|
|
506
|
+
(i, j) => similarityOf(base.executions[i], cand.executions[j]), Math.round(1000 * EXECUTION_MATCH_MIN),
|
|
507
|
+
(i) => base.executions[i].id , (j) => cand.executions[j].id ,
|
|
508
|
+
);
|
|
509
|
+
const smaller = Math.min(base.executions.length, cand.executions.length);
|
|
510
|
+
const agreement = smaller > 0 ? sequence.pairs.length / smaller : null;
|
|
511
|
+
const accepted = agreement === null || agreement >= MIN_EXECUTION_AGREEMENT;
|
|
512
|
+
|
|
513
|
+
const executionPairs = [];
|
|
514
|
+
if (accepted) {
|
|
515
|
+
for (const [i, j] of sequence.pairs) executionPairs.push([base.executions[i], cand.executions[j]]);
|
|
516
|
+
if (base.pseudo && cand.pseudo) executionPairs.push([base.pseudo, cand.pseudo]);
|
|
517
|
+
}
|
|
213
518
|
|
|
214
|
-
const baseFigures = new Map
|
|
215
|
-
const
|
|
216
|
-
const figures = (cache , snap , id ) => {
|
|
519
|
+
const baseFigures = new Map (), candFigures = new Map ();
|
|
520
|
+
const figures = (cache , snap , id ) => {
|
|
217
521
|
let f = cache.get(id);
|
|
218
522
|
if (!f) { f = stageFigures(snap.stages.get(id) ); cache.set(id, f); }
|
|
219
523
|
return f;
|
|
220
524
|
};
|
|
525
|
+
const figuresOf = (side , ids ) =>
|
|
526
|
+
ids.map((id) => side === 'base' ? figures(baseFigures, baseSnap, id) : figures(candFigures, candSnap, id));
|
|
221
527
|
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
528
|
+
const tokens = { base: new Map (), cand: new Map () };
|
|
529
|
+
const pairs = [];
|
|
530
|
+
const replanned = [];
|
|
531
|
+
for (const [be, ce] of executionPairs) {
|
|
532
|
+
const result = alignWithin(base, cand, be, ce, tokens, normalizer);
|
|
533
|
+
for (const { baseId, candId, quality, score } of result.pairs) {
|
|
534
|
+
pairs.push({
|
|
535
|
+
pairId: `b${baseId}-c${candId}`, baseStageIds: [baseId], candStageIds: [candId], quality, score,
|
|
536
|
+
deltas: deltasOf(figures(baseFigures, baseSnap, baseId), figures(candFigures, candSnap, candId)),
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
// Different stage counts under one aligned execution: the plan changed, not the work. The
|
|
540
|
+
// pseudo-execution has no plan to re-plan, so its leftovers stay unmatched.
|
|
541
|
+
const leftover = result.leftBase.length + result.leftCand.length > 0;
|
|
542
|
+
if (leftover && be.id !== null && ce.id !== null && be.stageIds.length !== ce.stageIds.length) {
|
|
543
|
+
replanned.push({
|
|
544
|
+
baseExecutionId: be.id, candExecutionId: ce.id, baseStageIds: result.leftBase, candStageIds: result.leftCand,
|
|
545
|
+
deltas: deltasOf(sumFigures(figuresOf('base', result.leftBase)), sumFigures(figuresOf('cand', result.leftCand))),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
pairs.sort((x, y) => x.baseStageIds[0] - y.baseStageIds[0] || x.candStageIds[0] - y.candStageIds[0]);
|
|
550
|
+
replanned.sort((x, y) => x.baseExecutionId - y.baseExecutionId || x.candExecutionId - y.candExecutionId);
|
|
233
551
|
|
|
234
552
|
const pairedBase = new Set(pairs.flatMap((p) => p.baseStageIds));
|
|
235
553
|
const pairedCand = new Set(pairs.flatMap((p) => p.candStageIds));
|
|
236
|
-
const
|
|
237
|
-
|
|
554
|
+
const replannedBase = new Set(replanned.flatMap((g) => g.baseStageIds));
|
|
555
|
+
const replannedCand = new Set(replanned.flatMap((g) => g.candStageIds));
|
|
556
|
+
const unmatchedOf = (snap , book , paired , replannedIds ) =>
|
|
557
|
+
[...snap.stages.keys()].filter((id) => !book.has(id) && !paired.has(id) && !replannedIds.has(id)).sort(ascending);
|
|
238
558
|
|
|
239
559
|
// Run time counts every attempt, the same figure the stage-pair deltas and the run totals use.
|
|
240
|
-
const runTime = (cache , snap , id ) => figures(cache, snap, id).executorRunTime ?? 0;
|
|
241
560
|
let total = 0, matched = 0;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (pairedCand.has(id)) matched += t;
|
|
253
|
-
}
|
|
561
|
+
const tally = (cache , snap , book , paired , replannedIds ) => {
|
|
562
|
+
for (const id of snap.stages.keys()) {
|
|
563
|
+
if (book.has(id)) continue;
|
|
564
|
+
const t = figures(cache, snap, id).executorRunTime ?? 0;
|
|
565
|
+
total += t;
|
|
566
|
+
if (paired.has(id) || (REPLANNED_COUNTS_TOWARD_COVERAGE && replannedIds.has(id))) matched += t;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
tally(baseFigures, baseSnap, base.bookkeeping, pairedBase, replannedBase);
|
|
570
|
+
tally(candFigures, candSnap, cand.bookkeeping, pairedCand, replannedCand);
|
|
254
571
|
|
|
255
572
|
return {
|
|
256
573
|
pairs,
|
|
257
|
-
unmatched: {
|
|
258
|
-
|
|
259
|
-
|
|
574
|
+
unmatched: {
|
|
575
|
+
baseStageIds: unmatchedOf(baseSnap, base.bookkeeping, pairedBase, replannedBase),
|
|
576
|
+
candStageIds: unmatchedOf(candSnap, cand.bookkeeping, pairedCand, replannedCand),
|
|
577
|
+
},
|
|
578
|
+
replanned,
|
|
579
|
+
bookkeepingStageIds: { baseStageIds: [...base.bookkeeping].sort(ascending), candStageIds: [...cand.bookkeeping].sort(ascending) },
|
|
260
580
|
runtimeCoverage: total > 0 ? matched / total : null,
|
|
581
|
+
executionAlignment: {
|
|
582
|
+
baseExecutions: base.executions.length, candExecutions: cand.executions.length,
|
|
583
|
+
pairedExecutions: sequence.pairs.length, bounded: sequence.bounded, agreement, accepted,
|
|
584
|
+
},
|
|
261
585
|
};
|
|
262
586
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
|
|
2
|
+
import { medianOfSorted } from './median.js';
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
// Single source of truth for the packed per-task numeric array: FIELDS (offset
|
|
@@ -304,6 +305,7 @@ function listScheduleEndMs(
|
|
|
304
305
|
return endMs;
|
|
305
306
|
}
|
|
306
307
|
|
|
308
|
+
// p50 is the textbook median (the mean of the two middle values on an even count); p95 is nearest-rank.
|
|
307
309
|
export function computeFieldQuantiles(arr , fieldIndex ) {
|
|
308
310
|
const taskCount = arr.length / FIELDS.STRIDE;
|
|
309
311
|
if (taskCount === 0) return { p50: 0, p95: 0, max: 0 };
|
|
@@ -313,7 +315,7 @@ export function computeFieldQuantiles(arr , fieldIndex )
|
|
|
313
315
|
values.sort();
|
|
314
316
|
|
|
315
317
|
return {
|
|
316
|
-
p50: values
|
|
318
|
+
p50: medianOfSorted(values),
|
|
317
319
|
p95: values[Math.ceil(taskCount * 0.95) - 1],
|
|
318
320
|
max: values[taskCount - 1],
|
|
319
321
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// What the stage aligner compares beyond the exact comparison key: a stage's structural key (operator
|
|
2
|
+
// tree shape, node names and sorted attribute names, with literals, paths, file counts and ids
|
|
3
|
+
// removed) and the token sets its similarity scores read. Pure.
|
|
4
|
+
|
|
5
|
+
import { cyrb53 } from './string-hash.js';
|
|
6
|
+
import { normalizeStageName, } from './stage-identity.js';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// Plumbing nodes: codegen boundaries and input adapters change with how Spark fuses a stage, not with
|
|
10
|
+
// the query, so the structural key and the similarity tokens skip them.
|
|
11
|
+
const PLUMBING_NODE = /^(WholeStageCodegen|InputAdapter|ColumnarToRow|RowToColumnar)\b/;
|
|
12
|
+
|
|
13
|
+
const isPlumbing = (node ) => PLUMBING_NODE.test(node.name ?? '');
|
|
14
|
+
|
|
15
|
+
/** The attribute names a plan node's text mentions (`amount#12` gives `amount`), without ids, each
|
|
16
|
+
* once, sorted. Sorting makes the key insensitive to the nondeterministic order in which Spark lists
|
|
17
|
+
* grouping and projection columns; reading only `name#id` references leaves literals out. */
|
|
18
|
+
export function attributeNames(detail ) {
|
|
19
|
+
const names = new Set ();
|
|
20
|
+
for (const m of detail.matchAll(/([A-Za-z_][\w$.]*)#\d+L?/g)) names.add(m[1]);
|
|
21
|
+
return [...names].sort();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The structural key of a stage from the plan nodes attributed to it, or null when nothing but
|
|
25
|
+
* plumbing is attributed. The nodes form a forest (a node's parent is its nearest attributed
|
|
26
|
+
* ancestor); the key folds each node's normalized name, its sorted attribute names and its
|
|
27
|
+
* children's digests (sorted, so a join's side order does not matter). Only structure goes in: no
|
|
28
|
+
* node detail, so a literal, a path or a file count never splits two stages. */
|
|
29
|
+
export function structuralKey(nodes ) {
|
|
30
|
+
const attributed = new Set(nodes.filter((n) => !isPlumbing(n)));
|
|
31
|
+
if (attributed.size === 0) return null;
|
|
32
|
+
const digests = new Map ();
|
|
33
|
+
const nested = new Set ();
|
|
34
|
+
// Digests of the nearest attributed descendants of `node`'s children.
|
|
35
|
+
const below = (node ) => {
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const child of node.children ?? []) {
|
|
38
|
+
if (attributed.has(child)) { out.push(digestOf(child)); nested.add(child); }
|
|
39
|
+
else out.push(...below(child));
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
};
|
|
43
|
+
const digestOf = (node ) => {
|
|
44
|
+
let d = digests.get(node);
|
|
45
|
+
if (d === undefined) {
|
|
46
|
+
d = cyrb53(JSON.stringify([normalizeStageName(node.name ?? ''), attributeNames(node.detail ?? ''), below(node).sort()]));
|
|
47
|
+
digests.set(node, d);
|
|
48
|
+
}
|
|
49
|
+
return d;
|
|
50
|
+
};
|
|
51
|
+
for (const node of attributed) digestOf(node);
|
|
52
|
+
const roots = [...attributed].filter((n) => !nested.has(n)).map(digestOf).sort();
|
|
53
|
+
return cyrb53(JSON.stringify(roots));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TOKEN_SPLIT = /[^A-Za-z0-9_.$]+/;
|
|
57
|
+
|
|
58
|
+
/** Sorted distinct tokens of a stage's attributed nodes (name and normalized detail) and its name.
|
|
59
|
+
* Two stages whose text differs in one literal share nearly every token. */
|
|
60
|
+
export function detailTokens(stageName , nodes , normalize ) {
|
|
61
|
+
const tokens = new Set ();
|
|
62
|
+
const add = (text ) => { for (const t of text.split(TOKEN_SPLIT)) if (t) tokens.add(t); };
|
|
63
|
+
add(normalizeStageName(stageName ?? ''));
|
|
64
|
+
for (const node of nodes) {
|
|
65
|
+
if (isPlumbing(node)) continue;
|
|
66
|
+
add(normalizeStageName(node.name ?? ''));
|
|
67
|
+
add(normalize(node.detail ?? ''));
|
|
68
|
+
}
|
|
69
|
+
return [...tokens].sort();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Jaccard similarity of two sorted arrays; 1 when both are empty. An element repeated in both counts
|
|
73
|
+
* as often as it repeats in the shorter side (multiset Jaccard), so distinct arrays give the plain one. */
|
|
74
|
+
export function jaccard(a , b ) {
|
|
75
|
+
if (a.length === 0 && b.length === 0) return 1;
|
|
76
|
+
let i = 0, j = 0, both = 0;
|
|
77
|
+
while (i < a.length && j < b.length) {
|
|
78
|
+
if (a[i] === b[j]) { both++; i++; j++; }
|
|
79
|
+
else if (a[i] < b[j]) i++;
|
|
80
|
+
else j++;
|
|
81
|
+
}
|
|
82
|
+
return both / (a.length + b.length - both);
|
|
83
|
+
}
|