sparkforensics-server 0.6.0 → 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.
- package/index.js +16 -7
- package/package.json +1 -1
- package/public/assets/AutoscalingChurn-DenWKTv-.js +1 -0
- package/public/assets/CacheUtilization-DLz7NdjV.js +1 -0
- package/public/assets/CachingOpportunity-zpQIqhzm.js +1 -0
- package/public/assets/ChartTheme-DAiARbLP.js +8 -0
- package/public/assets/ColdStart-C-9HjxWl.js +1 -0
- package/public/assets/ConfigAudit-D0V9tcrJ.js +1 -0
- package/public/assets/CoreUsageArea-yeerrljd.js +1 -0
- package/public/assets/Dashboard-DkUPrHKV.js +1 -0
- package/public/assets/DisclosureContext-DK1_iz6J.js +1 -0
- package/public/assets/DuplicatePlanSubtree-xDNDbCd3.js +1 -0
- package/public/assets/DurationHistogram-B6jsE0Ov.js +1 -0
- package/public/assets/EmptyState-C1uL40VU.js +1 -0
- package/public/assets/ExecutorUtilization-DXDZKmSc.js +1 -0
- package/public/assets/GcPressure-MRf7I4IG.js +1 -0
- package/public/assets/ImpactEstimate-Dm3xWbzV.js +1 -0
- package/public/assets/IncompleteRun-CaLOhMtf.js +1 -0
- package/public/assets/JobFailures-CzmzlvhO.js +1 -0
- package/public/assets/MemoryUtilization-AWUMUjCH.js +1 -0
- package/public/assets/NestedLoopJoin-C9sNr6Ss.js +1 -0
- package/public/assets/OverBroadcast-BOmRE6ca.js +1 -0
- package/public/assets/PartitionHint-BL2EmJfE.js +1 -0
- package/public/assets/PartitionSizing-BzBySvUq.js +1 -0
- package/public/assets/PlanExplorer-DdiQRcJU.js +1 -0
- package/public/assets/PlanGraphRoute-CIH9gBuR.js +1 -0
- package/public/assets/PlanView-B1hxzFRs.js +2 -0
- package/public/assets/PythonUdf-DvgCCueD.js +1 -0
- package/public/assets/RetryWaste-uXXu_CyM.js +1 -0
- package/public/assets/RowPagination-C-LzWkI5.js +1 -0
- package/public/assets/RowStatusCluster-Dbtmte7l.js +1 -0
- package/public/assets/ShuffleIO-nALwzC1_.js +1 -0
- package/public/assets/Skew-CHMEKgvk.js +1 -0
- package/public/assets/SlowHost-yyvjpHk6.js +1 -0
- package/public/assets/SmallFiles-y6odJ97n.js +1 -0
- package/public/assets/SpeculationWaste-C-SD2LOt.js +1 -0
- package/public/assets/Spill-DHTMmOup.js +1 -0
- package/public/assets/StageFailed-CQ3DxC9R.js +1 -0
- package/public/assets/StageFindingGroup-BgYLyj5q.js +1 -0
- package/public/assets/StageHeader-CIETkjVZ.js +1 -0
- package/public/assets/StagePill--P7PDd3s.js +1 -0
- package/public/assets/StageShape-BIv8vx_o.js +1 -0
- package/public/assets/StageSlowness-MMqzERxN.js +1 -0
- package/public/assets/Straggler-BgrY7AyW.js +1 -0
- package/public/assets/TaskFailures-BHHyKYTs.js +1 -0
- package/public/assets/TinyTask-DDScNhCl.js +1 -0
- package/public/assets/UnderBroadcast-CGIC04TI.js +1 -0
- package/public/assets/WidgetLeadSummary-CXFZP00C.js +1 -0
- package/public/assets/button-DbpTz2yk.js +1 -0
- package/public/assets/charts-vendor-CtTxwTeu.js +59 -0
- package/public/assets/composite-C9Nu2EI9.js +1 -0
- package/public/assets/d3-vendor-DNlD2-cz.js +1 -0
- package/public/assets/detector-registry-Bp8hrLua.js +2 -0
- package/public/assets/downsample-WmNM1qc9.js +1 -0
- package/public/assets/finding-anchor-Dr40LkS9.js +1 -0
- package/public/assets/findings-of-type-BL2u3RGZ.js +1 -0
- package/public/assets/impact-sort-mC5ISgX8.js +1 -0
- package/public/assets/inbox-KPU1Gppm.js +1 -0
- package/public/assets/index-CaRVPvAT.css +2 -0
- package/public/assets/index-fDiwfuoO.js +14 -0
- package/public/assets/inertValue-L9VxV0VE.js +1 -0
- package/public/assets/layout.worker-CLcBHxVp.js +3 -0
- package/public/assets/{parser-worker-CA5RxqKc.js → parser-worker-CRIOPMOW.js} +8 -8
- package/public/assets/plan-graph-vendor-Ce6d4HU6.js +7 -0
- package/public/assets/react-vendor-C6KcUD36.js +9 -0
- package/public/assets/select-BHqOqun5.js +1 -0
- package/public/assets/toggle-DPeN6aNa.js +1 -0
- package/public/assets/toggle-group-Kl3uyHSr.js +1 -0
- package/public/assets/usePagedRows-BafD6bEN.js +1 -0
- package/public/assets/useRecentFiles-DzJZAoOT.js +3 -0
- package/public/assets/useRegisterFieldControl-pQv-ZDoU.js +1 -0
- package/public/assets/useScrollLock-BCxn_dMp.js +1 -0
- package/public/assets/useSortMode-DjrAN-z4.js +1 -0
- package/public/docs/404.html +3 -3
- package/public/docs/assets/{app.CnWL6bKV.js → app.1gvzd5Zb.js} +1 -1
- package/public/docs/assets/chunks/@localSearchIndexroot.DS2TQP5_.js +1 -0
- package/public/docs/assets/chunks/{VPLocalSearchBox.C0IBKeRQ.js → VPLocalSearchBox.CNxT3s0I.js} +1 -1
- package/public/docs/assets/chunks/{theme.BH-2HuRf.js → theme.C5lLqUv7.js} +2 -2
- package/public/docs/assets/contributor-guide_architecture_board-widgets.md.CqKM_jEG.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_board-widgets.md.CqKM_jEG.lean.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.CQgn1t_M.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.BYDo3ClR.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.ajUAVdbc.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_board-widgets_visual-system-and-templating.md.wulOueeB.js → contributor-guide_architecture_board-widgets_visual-system-and-templating.md.WqY74T0T.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.WqY74T0T.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract.md.CHHgywoP.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.N4hhenrA.lean.js → contributor-guide_architecture_detector-contract.md.CHHgywoP.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.DIz3_oz9.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.DIz3_oz9.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.WH4tRHPL.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_detector-contract_threshold-tables.md.Dp3c40rx.lean.js → contributor-guide_architecture_detector-contract_threshold-tables.md.WH4tRHPL.lean.js} +1 -1
- package/public/docs/assets/{contributor-guide_architecture_drill-down.md.CqZPrdo9.js → contributor-guide_architecture_drill-down.md.yB-Hz2XT.js} +1 -1
- 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
- package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.BQZE04sw.js +1 -0
- 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
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.K71YB-Rr.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_impact-estimation.md.DfvbqEUu.lean.js → contributor-guide_architecture_impact-estimation.md.K71YB-Rr.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.DLCAffwn.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.Cwm1jHmn.lean.js → contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.DLCAffwn.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.JFrzDMwo.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.JFrzDMwo.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_overview.md.nxkpOhgf.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.U-9KMDFK.js +1 -0
- 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
- package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BrSz8Ezh.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.B7rjSF_h.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.B7rjSF_h.lean.js +1 -0
- 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
- 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
- package/public/docs/assets/{contributor-guide_architecture_worker-protocol.md.BAnhi2-d.js → contributor-guide_architecture_worker-protocol.md.Br6OE7RW.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.Br6OE7RW.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.IVyaXrGe.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_worker-protocol_cli-and-mcp.md.BX1XnFbq.lean.js → contributor-guide_architecture_worker-protocol_cli-and-mcp.md.IVyaXrGe.lean.js} +1 -1
- 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
- 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
- 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
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.CtmCU57I.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-report.md.CiQl1MLb.lean.js → contributor-guide_architecture_worker-protocol_evidence-report.md.CtmCU57I.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_contributing.md.DBW4drtZ.js +1 -0
- package/public/docs/assets/contributor-guide_development-setup.md.B21ZTDhn.js +15 -0
- package/public/docs/assets/contributor-guide_development-setup.md.B21ZTDhn.lean.js +1 -0
- package/public/docs/assets/contributor-guide_testing.md.DpkozNoG.js +4 -0
- package/public/docs/assets/{contributor-guide_testing.md.By6XGR9a.lean.js → contributor-guide_testing.md.DpkozNoG.lean.js} +1 -1
- package/public/docs/assets/{index.md.CHJVslga.js → index.md.lMm7SUMz.js} +1 -1
- package/public/docs/assets/{index.md.CHJVslga.lean.js → index.md.lMm7SUMz.lean.js} +1 -1
- package/public/docs/assets/{style.CyK9Ss73.css → style.Cfxqlj0y.css} +1 -1
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.C0pcQoNc.js +29 -0
- package/public/docs/assets/{user-guide_getting-started_ci-and-automation.md.BEepbDDL.lean.js → user-guide_getting-started_ci-and-automation.md.C0pcQoNc.lean.js} +1 -1
- package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.CGaxPX_1.js +1 -0
- 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
- 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
- package/public/docs/assets/{user-guide_mcp-tools.md.C65SjWUL.js → user-guide_mcp-tools.md.49r3L2YE.js} +1 -1
- 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
- package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.BQ_kIptC.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.azCZXkH8.js +44 -0
- 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
- package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.js → user-guide_mcp-tools_evaluate-budgets.md.dpQpRQ7z.js} +2 -2
- 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
- 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
- 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
- package/public/docs/assets/user-guide_run-comparison.md.B_LOkdBf.js +1 -0
- package/public/docs/assets/user-guide_run-comparison.md.B_LOkdBf.lean.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.C4riKCKl.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.C4riKCKl.lean.js +1 -0
- package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +6 -6
- package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +6 -6
- package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +6 -6
- package/public/docs/contributor-guide/architecture/board-widgets.html +6 -6
- package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +6 -6
- package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +6 -6
- package/public/docs/contributor-guide/architecture/detector-contract.html +6 -6
- package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +6 -6
- package/public/docs/contributor-guide/architecture/drill-down.html +6 -6
- package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +6 -6
- package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +6 -6
- package/public/docs/contributor-guide/architecture/impact-estimation.html +6 -6
- package/public/docs/contributor-guide/architecture/index.html +5 -5
- package/public/docs/contributor-guide/architecture/overview.html +6 -6
- package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +5 -5
- package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +6 -6
- package/public/docs/contributor-guide/architecture/state-and-history.html +5 -5
- package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +6 -6
- package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +5 -5
- package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +6 -6
- package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +5 -5
- package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +6 -6
- package/public/docs/contributor-guide/architecture/widget-rendering.html +6 -6
- package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +6 -6
- package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +6 -6
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +7 -7
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +6 -6
- package/public/docs/contributor-guide/architecture/worker-protocol.html +6 -6
- package/public/docs/contributor-guide/contributing.html +6 -6
- package/public/docs/contributor-guide/development-setup.html +8 -8
- package/public/docs/contributor-guide/testing.html +9 -6
- package/public/docs/hashmap.json +1 -1
- package/public/docs/index.html +6 -6
- package/public/docs/logo-dark.svg +1 -0
- package/public/docs/logo-light.svg +1 -0
- package/public/docs/tuning-reference/anti-patterns.html +5 -5
- package/public/docs/tuning-reference/aqe.html +5 -5
- package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +5 -5
- package/public/docs/tuning-reference/bottleneck-cold-start.html +5 -5
- package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +5 -5
- package/public/docs/tuning-reference/bottleneck-failures.html +5 -5
- package/public/docs/tuning-reference/bottleneck-gc.html +5 -5
- package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +5 -5
- package/public/docs/tuning-reference/bottleneck-memory-utilization.html +5 -5
- package/public/docs/tuning-reference/bottleneck-retry-waste.html +5 -5
- package/public/docs/tuning-reference/bottleneck-shuffle.html +5 -5
- package/public/docs/tuning-reference/bottleneck-skew.html +5 -5
- package/public/docs/tuning-reference/bottleneck-slow-host.html +5 -5
- package/public/docs/tuning-reference/bottleneck-small-files.html +5 -5
- package/public/docs/tuning-reference/bottleneck-spill.html +5 -5
- package/public/docs/tuning-reference/bottleneck-straggler.html +5 -5
- package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +5 -5
- package/public/docs/tuning-reference/bottleneck-utilization.html +5 -5
- package/public/docs/tuning-reference/caching.html +5 -5
- package/public/docs/tuning-reference/cluster-config.html +5 -5
- package/public/docs/tuning-reference/config.html +5 -5
- package/public/docs/tuning-reference/data-formats.html +5 -5
- package/public/docs/tuning-reference/index.html +5 -5
- package/public/docs/tuning-reference/intro.html +5 -5
- package/public/docs/tuning-reference/joins.html +5 -5
- package/public/docs/tuning-reference/memory-model.html +5 -5
- package/public/docs/tuning-reference/metrics.html +5 -5
- package/public/docs/tuning-reference/partitioning.html +5 -5
- package/public/docs/tuning-reference/pyspark.html +5 -5
- package/public/docs/tuning-reference/shuffle.html +5 -5
- package/public/docs/tuning-reference/spark-architecture.html +5 -5
- package/public/docs/tuning-reference/table-formats.html +5 -5
- package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +5 -5
- package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +5 -5
- package/public/docs/user-guide/alternative-log-retrieval.html +5 -5
- package/public/docs/user-guide/getting-started/ci-and-automation.html +16 -11
- package/public/docs/user-guide/getting-started/reading-the-dashboard.html +6 -6
- package/public/docs/user-guide/getting-started/regression-metric-keys.html +5 -5
- package/public/docs/user-guide/getting-started/tuning-thresholds.html +7 -7
- package/public/docs/user-guide/getting-started.html +5 -5
- package/public/docs/user-guide/mcp-tools/compare-runs.html +16 -11
- package/public/docs/user-guide/mcp-tools/diagnose-run.html +28 -7
- package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +6 -6
- package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +5 -5
- package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +7 -6
- package/public/docs/user-guide/mcp-tools/get-reference-doc.html +5 -5
- package/public/docs/user-guide/mcp-tools/get-run-summary.html +5 -5
- package/public/docs/user-guide/mcp-tools/list-runs.html +5 -5
- package/public/docs/user-guide/mcp-tools.html +6 -6
- package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +5 -5
- package/public/docs/user-guide/run-comparison.html +6 -6
- package/public/docs/user-guide/understanding-findings.html +6 -6
- package/public/export-template.html +45 -45
- package/public/index.html +8 -7
- package/public/sample-runs/sample-run.ndjson.gz +0 -0
- package/vendor-core/allocation.js +25 -23
- package/vendor-core/analyzer.js +85 -58
- package/vendor-core/api-merge-writes.js +2 -0
- package/vendor-core/aqe-skew.js +347 -0
- package/vendor-core/cli/budgets.js +6 -4
- package/vendor-core/cli/collect-run-pair.js +89 -0
- package/vendor-core/cli/collect-run-worker.js +16 -0
- package/vendor-core/cli/collect-run.js +1 -2
- package/vendor-core/cli/native-zstd.js +12 -16
- package/vendor-core/comparability.js +234 -0
- package/vendor-core/comparison-output.js +15 -7
- package/vendor-core/comparison-verdict.js +105 -25
- package/vendor-core/core-source-hash.txt +1 -1
- package/vendor-core/core-time-series.js +0 -1
- package/vendor-core/core-usage-locality.js +1 -1
- package/vendor-core/detectors.js +1007 -380
- package/vendor-core/docs-config.js +0 -4
- package/vendor-core/docs-content/detection/cache.md +2 -1
- package/vendor-core/docs-content/detection/cfg.md +19 -11
- package/vendor-core/docs-content/detection/cold.md +2 -1
- package/vendor-core/docs-content/detection/gc.md +7 -3
- package/vendor-core/docs-content/detection/host.md +11 -1
- package/vendor-core/docs-content/detection/mem.md +17 -6
- package/vendor-core/docs-content/detection/part.md +3 -1
- package/vendor-core/docs-content/detection/plan.md +55 -13
- package/vendor-core/docs-content/detection/shfl.md +11 -5
- package/vendor-core/docs-content/detection/skew.md +47 -1
- package/vendor-core/docs-content/detection/slow.md +4 -2
- package/vendor-core/docs-content/detection/spec.md +6 -1
- package/vendor-core/docs-content/detection/spill.md +6 -3
- package/vendor-core/docs-content/detection/strag.md +19 -6
- package/vendor-core/docs-content/detection/tiny.md +9 -3
- package/vendor-core/effective-conf.js +7 -10
- package/vendor-core/efficiency-model.js +2 -2
- package/vendor-core/etl-phases.js +1 -2
- package/vendor-core/event-handlers.js +175 -57
- package/vendor-core/event-schemas.js +21 -16
- package/vendor-core/evidence-availability.js +9 -2
- package/vendor-core/evidence-report.js +118 -35
- package/vendor-core/executor-peaks.js +41 -0
- package/vendor-core/export-data.js +5 -5
- package/vendor-core/finding-identity.js +78 -0
- package/vendor-core/finding-presentation.js +116 -33
- package/vendor-core/finding-types.js +88 -21
- package/vendor-core/format-utils.js +14 -15
- package/vendor-core/html-export.js +1 -1
- package/vendor-core/impact-band.js +2 -2
- package/vendor-core/impact-format.js +47 -19
- package/vendor-core/impact-model.js +26 -15
- package/vendor-core/job-groups.js +4 -4
- package/vendor-core/mcp-server-factory.js +48 -12
- package/vendor-core/mcp-tools.js +179 -24
- package/vendor-core/median.js +2 -2
- package/vendor-core/nested-loop-join.js +73 -0
- package/vendor-core/occupancy.js +1 -2
- package/vendor-core/parser-worker.js +1 -1
- package/vendor-core/plan-graph-model.js +14 -22
- package/vendor-core/plan-node-detail.js +2 -3
- package/vendor-core/plan-summary.js +24 -19
- package/vendor-core/plan-tree-walk.js +6 -8
- package/vendor-core/python-stage.js +12 -1
- package/vendor-core/recommendation-rollup.js +13 -12
- package/vendor-core/redact.js +52 -46
- package/vendor-core/run-aggregates.js +6 -8
- package/vendor-core/run-comparison.js +226 -66
- package/vendor-core/run-identity.js +12 -0
- package/vendor-core/run-interpretation.js +21 -0
- package/vendor-core/run-metrics.js +2 -2
- package/vendor-core/run-output.js +32 -10
- package/vendor-core/run-totals.js +12 -0
- package/vendor-core/run-verdict.js +76 -3
- package/vendor-core/scaling-sim.js +3 -4
- package/vendor-core/snappy-block.js +2 -2
- package/vendor-core/spark-conf.js +168 -0
- package/vendor-core/sql-stages.js +2 -2
- package/vendor-core/stage-alignment.js +2 -12
- package/vendor-core/stage-identity.js +58 -3
- package/vendor-core/stage-label.js +14 -0
- package/vendor-core/stage-plan-nodes.js +17 -3
- package/vendor-core/stage-quantiles.js +141 -9
- package/vendor-core/stage-structure.js +17 -3
- package/vendor-core/threshold-overrides.js +9 -3
- package/vendor-core/types.js +58 -21
- package/vendor-core/wasted-core-hours.js +0 -1
- package/vendor-core/write-targets.js +114 -14
- package/vendor-core/zstd-worker-client.js +1 -1
- package/vendor-core/zstd-worker.js +1 -1
- package/public/assets/AutoscalingChurn-CRA2Mb7F.js +0 -1
- package/public/assets/CacheUtilization-Bh9OY_V9.js +0 -1
- package/public/assets/CachingOpportunity-oCiQwmTJ.js +0 -1
- package/public/assets/ColdStart-BJrtjfmn.js +0 -1
- package/public/assets/ConfigAudit-DMvKxySP.js +0 -1
- package/public/assets/CoreUsageArea-REC2zsKw.js +0 -1
- package/public/assets/Dashboard-BHZ76JCu.js +0 -2
- package/public/assets/DisclosureContext-OO7X1HAx.js +0 -1
- package/public/assets/DuplicatePlanSubtree-BgG-L-cK.js +0 -1
- package/public/assets/ExecutorUtilization-D2KhSVbY.js +0 -1
- package/public/assets/GcPressure-D1BTE8qh.js +0 -1
- package/public/assets/IncompleteRun-DD40xUpy.js +0 -1
- package/public/assets/JobFailures-YwNzCF19.js +0 -1
- package/public/assets/MemoryUtilization-9b8ee_3_.js +0 -1
- package/public/assets/OverBroadcast-CfctS8a8.js +0 -1
- package/public/assets/PartitionHint-CUvbnnxy.js +0 -1
- package/public/assets/PartitionSizing-VG8kBgG5.js +0 -1
- package/public/assets/PlanExplorer-DmubTAOP.js +0 -1
- package/public/assets/PlanGraphRoute-DctPZjS0.js +0 -1
- package/public/assets/RetryWaste-CnpM8bB7.js +0 -1
- package/public/assets/ShuffleIO-D13vGmas.js +0 -1
- package/public/assets/Skew-DlagstYP.js +0 -1
- package/public/assets/SlowHost-cSkLRZ_e.js +0 -1
- package/public/assets/SmallFiles-B-kERMcq.js +0 -1
- package/public/assets/SpeculationWaste-DaysAzXO.js +0 -1
- package/public/assets/Spill-H44Gz4US.js +0 -1
- package/public/assets/StageFailed-DCfii9h_.js +0 -1
- package/public/assets/StageFindingGroup-BXJ0Mj5n.js +0 -1
- package/public/assets/StageHeader--oN2J-oD.js +0 -1
- package/public/assets/StageShape-IZ80CZiJ.js +0 -1
- package/public/assets/StageSlowness--BRIaBc1.js +0 -1
- package/public/assets/Straggler-Ctq0Df_o.js +0 -1
- package/public/assets/TaskFailures-Bks_gqhL.js +0 -1
- package/public/assets/TinyTask-BssfzGFB.js +0 -1
- package/public/assets/UnderBroadcast-DsScrxc-.js +0 -1
- package/public/assets/charts-vendor-0VMaLVLN.js +0 -59
- package/public/assets/finding-anchor-BYW1oz5R.js +0 -1
- package/public/assets/impact-sort-xzy0g-Vh.js +0 -1
- package/public/assets/index-DhpRGMZc.css +0 -2
- package/public/assets/index-DwqkFUFk.js +0 -14
- package/public/assets/plan-graph-vendor-BYl-AzN5.js +0 -7
- package/public/assets/react-vendor-DfpuPQZf.js +0 -9
- package/public/assets/select-C8edjYTq.js +0 -1
- package/public/assets/usePagedRows-CsuUFeYx.js +0 -1
- package/public/assets/useRecentFiles-BvkKo3JN.js +0 -11
- package/public/assets/useSortMode-joX71BZN.js +0 -1
- package/public/docs/assets/chunks/@localSearchIndexroot.tE5pz54z.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.RlzvUmNm.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.wulOueeB.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_detector-contract.md.N4hhenrA.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.Dp3c40rx.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.DwOozIEs.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.DfvbqEUu.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.Cwm1jHmn.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_overview.md.C1sSvm9i.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BOZ1azXG.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.BAnhi2-d.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.BX1XnFbq.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.CiQl1MLb.js +0 -1
- package/public/docs/assets/contributor-guide_contributing.md.CQMvGwFt.js +0 -1
- package/public/docs/assets/contributor-guide_development-setup.md.DQdzera7.js +0 -15
- package/public/docs/assets/contributor-guide_development-setup.md.DQdzera7.lean.js +0 -1
- package/public/docs/assets/contributor-guide_testing.md.By6XGR9a.js +0 -1
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.BEepbDDL.js +0 -24
- package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.Obf3W3__.js +0 -1
- package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.Cc_gLoix.lean.js +0 -1
- package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.BP1vnHA5.js +0 -23
- package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.js +0 -1
- package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.lean.js +0 -1
- package/public/docs/assets/user-guide_understanding-findings.md.3C7KCEZO.js +0 -1
- package/public/docs/assets/user-guide_understanding-findings.md.3C7KCEZO.lean.js +0 -1
- /package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.CQgn1t_M.lean.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.BYDo3ClR.lean.js} +0 -0
- /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
- /package/public/docs/assets/{contributor-guide_architecture_overview.md.C1sSvm9i.lean.js → contributor-guide_architecture_overview.md.nxkpOhgf.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_widget-rendering.md.BOZ1azXG.lean.js → contributor-guide_architecture_widget-rendering.md.BrSz8Ezh.lean.js} +0 -0
- /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
- /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
- /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
- /package/public/docs/assets/{contributor-guide_contributing.md.CQMvGwFt.lean.js → contributor-guide_contributing.md.DBW4drtZ.lean.js} +0 -0
- /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
- /package/public/docs/assets/{user-guide_mcp-tools.md.C65SjWUL.lean.js → user-guide_mcp-tools.md.49r3L2YE.lean.js} +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// Why AQE skew-join handling did or did not act on a stage's join, read from the execution's final
|
|
2
|
+
// plan and the effective Spark conf. Spark's OptimizeSkewedJoin (apache/spark v3.5.0,
|
|
3
|
+
// sql/core/.../adaptive/OptimizeSkewedJoin.scala) splits a skewed shuffle partition only when the
|
|
4
|
+
// join is a sort-merge or shuffled-hash join over shuffles Spark added for it, the join type lets
|
|
5
|
+
// the skewed side be split, the partition is over both the size threshold and the factor times the
|
|
6
|
+
// median, and the split adds no shuffle above the join (unless forceOptimizeSkewedJoin). The
|
|
7
|
+
// final plan shows a split (`skew=true`, `AQEShuffleRead skewed`) and the shape the rule matches,
|
|
8
|
+
// so the first condition that fails is the reason. The cases are in
|
|
9
|
+
// user-guide/understanding-findings.md.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import { formatBytes } from './format-utils.js';
|
|
13
|
+
import { walkPlanTree } from './plan-tree-walk.js';
|
|
14
|
+
import { codeFix, decreaseConf, setConf } from './remediation.js';
|
|
15
|
+
import { parseSparkBytes, sparkConfDefault } from './spark-conf.js';
|
|
16
|
+
|
|
17
|
+
export const SKEW_THRESHOLD_KEY = 'spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes';
|
|
18
|
+
export const SKEW_FACTOR_KEY = 'spark.sql.adaptive.skewJoin.skewedPartitionFactor';
|
|
19
|
+
export const FORCE_SKEW_JOIN_KEY = 'spark.sql.adaptive.forceOptimizeSkewedJoin';
|
|
20
|
+
const ADVISORY_SIZE_KEY = 'spark.sql.adaptive.advisoryPartitionSizeInBytes';
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
/** The joins AQE skew-join handling applies to; the name carries `(skew=true)` once it split one. */
|
|
52
|
+
const JOIN_NAME = /^(SortMergeJoin|ShuffledHashJoin)(\(skew=true\))?$/;
|
|
53
|
+
export const isSkewJoinNode = (name ) => JOIN_NAME.test(name);
|
|
54
|
+
// Operators that wrap a shuffle read without changing what the join reads.
|
|
55
|
+
const WRAPPER_NAME = /^(WholeStageCodegen|InputAdapter$|AQEShuffleRead$|CustomShuffleReader$|ColumnarToRow$)/;
|
|
56
|
+
const SHUFFLE_READ_NAME = /^(AQEShuffleRead|CustomShuffleReader)$/;
|
|
57
|
+
const SPLITTABLE_LEFT = new Set(['Inner', 'Cross', 'LeftSemi', 'LeftAnti', 'LeftOuter']);
|
|
58
|
+
const SPLITTABLE_RIGHT = new Set(['Inner', 'Cross', 'RightOuter']);
|
|
59
|
+
const JOIN_TYPE = /\]\s*,\s*(Inner|Cross|LeftOuter|RightOuter|FullOuter|LeftSemi|LeftAnti|ExistenceJoin)\b/;
|
|
60
|
+
// A partition is only clearly skewed when the largest task read is at least this many times the median.
|
|
61
|
+
const SKEWED_READ_RATIO = 2;
|
|
62
|
+
// Spark judges each join side's partitions against that side's own median. A task's read sums both
|
|
63
|
+
// sides, so it stands for one side only when the other contributes at most this share of the bytes.
|
|
64
|
+
const ISOLATED_SIDE_SHARE = 0.05;
|
|
65
|
+
const EXCHANGE_NAME = /Exchange$/;
|
|
66
|
+
const SHUFFLE_ORIGIN = /\b(ENSURE_REQUIREMENTS|REPARTITION_BY_COL|REPARTITION_BY_NUM|REBALANCE_PARTITIONS_BY_NONE|REBALANCE_PARTITIONS_BY_COL)\b/;
|
|
67
|
+
|
|
68
|
+
/** What one join input reads: a shuffle Spark added for the join, a shuffle the job asked for, or
|
|
69
|
+
* some other operator (the join reads from it, not from a shuffle). */
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
function metricValue(node , name ) {
|
|
82
|
+
return node.metrics?.find((m) => m.name === name)?.value ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Follows one join input down through wrappers (and the Sort a sort-merge join puts over its
|
|
86
|
+
// shuffle) to the operator that produces it.
|
|
87
|
+
function classifySide(input ) {
|
|
88
|
+
const reads = [];
|
|
89
|
+
let node = input;
|
|
90
|
+
let sortSeen = false;
|
|
91
|
+
while (node != null) {
|
|
92
|
+
if (SHUFFLE_READ_NAME.test(node.name)) reads.push(node);
|
|
93
|
+
if (WRAPPER_NAME.test(node.name) || (node.name === 'Sort' && !sortSeen)) {
|
|
94
|
+
if (node.name === 'Sort') sortSeen = true;
|
|
95
|
+
node = node.children[0];
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
const coalesced = reads.some((r) => /\bcoalesced\b/.test(r.detail ?? ''));
|
|
101
|
+
if (node == null) return { side: { kind: 'operator', name: 'an unknown operator' }, reads };
|
|
102
|
+
if (node.name === 'ShuffleQueryStage') {
|
|
103
|
+
const origin = SHUFFLE_ORIGIN.exec(node.children[0]?.detail ?? '')?.[1];
|
|
104
|
+
if (origin == null || origin === 'ENSURE_REQUIREMENTS') return { side: { kind: 'joinShuffle', coalesced }, reads };
|
|
105
|
+
return { side: { kind: 'userShuffle', hint: origin.startsWith('REBALANCE') ? 'rebalance' : 'repartition', coalesced }, reads };
|
|
106
|
+
}
|
|
107
|
+
return { side: { kind: 'operator', name: node.name }, reads };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function describeOperator(name ) {
|
|
111
|
+
if (/Aggregate$/.test(name)) return 'an aggregate';
|
|
112
|
+
if (/^Window/.test(name)) return 'a window';
|
|
113
|
+
if (/Scan/.test(name)) return 'a scan';
|
|
114
|
+
return `a ${name} operator`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Whether an operator above the join needs the join's output partitioning, so a split (which
|
|
118
|
+
// breaks it) would cost a shuffle. Stops at the first exchange: the stage boundary above it
|
|
119
|
+
// reshuffles anyway. A partial aggregate asks for no distribution.
|
|
120
|
+
function needsJoinPartitioning(ancestors ) {
|
|
121
|
+
for (const a of ancestors) {
|
|
122
|
+
if (/Exchange$|^ShuffleQueryStage$/.test(a.name)) return null;
|
|
123
|
+
if (/Aggregate$/.test(a.name) && !/partial_/.test(a.detail ?? '')) return 'an aggregate';
|
|
124
|
+
if (/^Window/.test(a.name)) return 'a window';
|
|
125
|
+
if (JOIN_NAME.test(a.name)) return 'another join';
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function collectJoins(root ) {
|
|
131
|
+
const parents = new Map ();
|
|
132
|
+
const joins = [];
|
|
133
|
+
walkPlanTree(root, (node, parent) => {
|
|
134
|
+
parents.set(node, parent);
|
|
135
|
+
if (JOIN_NAME.test(node.name)) joins.push(node);
|
|
136
|
+
});
|
|
137
|
+
return joins.map((node) => {
|
|
138
|
+
const ancestors = [];
|
|
139
|
+
for (let p = parents.get(node) ?? null; p != null; p = parents.get(p) ?? null) ancestors.push(p);
|
|
140
|
+
return { node, ancestors };
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Whether the execution's final plan shows AQE splitting a skewed partition (an `AQEShuffleRead
|
|
145
|
+
* skewed` node), which adds tasks beyond the configured partition count. Null when the plan is not a
|
|
146
|
+
* final adaptive plan, so the log cannot say. */
|
|
147
|
+
export function planShowsSkewSplit(plan ) {
|
|
148
|
+
if (plan == null || plan.name !== 'AdaptiveSparkPlan' || !/isFinalPlan=true/.test(plan.detail ?? '')) return null;
|
|
149
|
+
let split = false;
|
|
150
|
+
walkPlanTree(plan, (node) => { if (SHUFFLE_READ_NAME.test(node.name) && /\bskewed\b/.test(node.detail ?? '')) split = true; });
|
|
151
|
+
return split;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The bytes one join input shuffled, from its exchange's metrics (null when the plan has none).
|
|
155
|
+
function sideShuffleBytes(input , metrics = ['shuffle bytes written', 'data size']) {
|
|
156
|
+
let bytes = null;
|
|
157
|
+
walkPlanTree(input, (n) => {
|
|
158
|
+
if (bytes != null || !EXCHANGE_NAME.test(n.name)) return;
|
|
159
|
+
for (const name of metrics) {
|
|
160
|
+
const value = metricValue(n, name);
|
|
161
|
+
if (value != null) { bytes = value; return; }
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return bytes;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Whether the stage reads this join's shuffles: an exchange of the join lists the stage, else the
|
|
168
|
+
// stage's shuffle read matches the bytes the join's two exchanges wrote (a stage reads exactly what
|
|
169
|
+
// the shuffles feeding it wrote). False when the plan gives neither, which no join can be assumed from.
|
|
170
|
+
function stageReadsJoin(join , input ) {
|
|
171
|
+
const exchanges = [];
|
|
172
|
+
walkPlanTree(join.node, (n) => { if (EXCHANGE_NAME.test(n.name)) exchanges.push(n); });
|
|
173
|
+
const withStages = exchanges.filter((n) => (n.stageIds?.length ?? 0) > 0);
|
|
174
|
+
if (withStages.length > 0) return withStages.some((n) => n.stageIds .includes(input.stageId));
|
|
175
|
+
const left = sideShuffleBytes(join.node.children[0], ['shuffle bytes written']);
|
|
176
|
+
const right = sideShuffleBytes(join.node.children[1], ['shuffle bytes written']);
|
|
177
|
+
const read = input.stageShuffleReadBytes ?? 0;
|
|
178
|
+
if (left == null || right == null || !(read > 0) || !(left + right > 0)) return false;
|
|
179
|
+
return Math.abs(read - (left + right)) <= 0.1 * (left + right);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sideNoun(index ) { return index === 0 ? 'left' : 'right'; }
|
|
183
|
+
|
|
184
|
+
function diagnoseJoin(join , input ) {
|
|
185
|
+
const { node } = join;
|
|
186
|
+
const left = classifySide(node.children[0]);
|
|
187
|
+
const right = classifySide(node.children[1]);
|
|
188
|
+
const reads = [...left.reads, ...right.reads];
|
|
189
|
+
|
|
190
|
+
// Split: the join carries `skew=true` and a read on one side says `skewed`.
|
|
191
|
+
const splitReads = reads.filter((r) => /\bskewed\b/.test(r.detail ?? ''));
|
|
192
|
+
if (/\(skew=true\)$/.test(node.name) || splitReads.length > 0) {
|
|
193
|
+
const partitions = splitReads.reduce((sum, r) => sum + (metricValue(r, 'number of skewed partitions') ?? 0), 0);
|
|
194
|
+
const splits = splitReads.reduce((sum, r) => sum + (metricValue(r, 'number of skewed partition splits') ?? 0), 0);
|
|
195
|
+
const what = partitions > 0 && splits > 0
|
|
196
|
+
? `AQE already split ${partitions} skewed partition${partitions === 1 ? '' : 's'} into ${splits} tasks`
|
|
197
|
+
: 'AQE already split the skewed partitions';
|
|
198
|
+
// A tail the data volume explains is not a GC pause or a slow host, so the split did not cure it.
|
|
199
|
+
if (input.tailData != null) {
|
|
200
|
+
const ratio = input.tailData.ratio != null ? ` (a median ${Math.round(input.tailData.ratio * 100) / 100}× the data of the median task)` : '';
|
|
201
|
+
return {
|
|
202
|
+
case: 'split',
|
|
203
|
+
text: `${what}, yet the slow tasks still read far more data than the median task${ratio}, so skew remains that the split did not remove: ${input.keyRemedy}`,
|
|
204
|
+
remediation: [codeFix(input.keyRemedy)],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
case: 'split',
|
|
209
|
+
text: `${what}, so the imbalance that remains is not join skew: look for a GC pause, a slow host or an expensive key instead`,
|
|
210
|
+
remediation: [],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Even reads: with the largest task read close to the median, no partition stands out for AQE to
|
|
215
|
+
// split. A coalesced read is a sum of partitions, so its ratio says nothing about one partition.
|
|
216
|
+
const coalesced = (left.side.kind !== 'operator' && left.side.coalesced) || (right.side.kind !== 'operator' && right.side.coalesced);
|
|
217
|
+
const ratio = input.readP50 > 0 ? Math.round(input.readMax / input.readP50 * 100) / 100 : null;
|
|
218
|
+
// Spark compares a partition with its own side's median, so a ratio of summed reads only speaks
|
|
219
|
+
// for a side when the other side adds next to nothing. It also would not see the data skew a tail
|
|
220
|
+
// attributed to data volume reports.
|
|
221
|
+
const leftBytes = sideShuffleBytes(node.children[0]);
|
|
222
|
+
const rightBytes = sideShuffleBytes(node.children[1]);
|
|
223
|
+
const sideTotal = (leftBytes ?? 0) + (rightBytes ?? 0);
|
|
224
|
+
const isolatedSide = leftBytes != null && rightBytes != null && sideTotal > 0
|
|
225
|
+
&& Math.min(leftBytes, rightBytes) <= ISOLATED_SIDE_SHARE * sideTotal;
|
|
226
|
+
if (!coalesced && isolatedSide && input.tailData == null && input.readP50 > 0 && input.readMax < SKEWED_READ_RATIO * input.readP50) {
|
|
227
|
+
return {
|
|
228
|
+
case: 'evenReads',
|
|
229
|
+
text: `its shuffle reads are even (the largest task reads ${formatBytes(input.readMax)}, ${ratio}× the median), so AQE has no skewed partition to split and the slow tail is not partition-size skew: look for a GC pause, a slow host or an expensive key instead`,
|
|
230
|
+
remediation: [],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Plan shape: both inputs must be shuffles Spark added for this join.
|
|
235
|
+
const blocked = [left.side, right.side].map((s, i) => ({ s, i: i })).find((x) => x.s.kind === 'operator');
|
|
236
|
+
if (blocked?.s.kind === 'operator') {
|
|
237
|
+
const above = describeOperator(blocked.s.name);
|
|
238
|
+
return {
|
|
239
|
+
case: 'planShape',
|
|
240
|
+
text: `AQE skew-join handling cannot apply: the join's ${sideNoun(blocked.i)} input comes from ${above} instead of a shuffle, and AQE splits only a shuffle that feeds the join directly; ${input.keyRemedy}`,
|
|
241
|
+
remediation: [codeFix(input.keyRemedy)],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const user = [left.side, right.side].find((s) => s.kind === 'userShuffle');
|
|
245
|
+
if (user?.kind === 'userShuffle') {
|
|
246
|
+
return {
|
|
247
|
+
case: 'userRepartition',
|
|
248
|
+
text: `a ${user.hint} you wrote feeds the join, and AQE leaves a shuffle you asked for as it is (it splits only shuffles Spark added for the join): drop the ${user.hint} so the join's own shuffle can be split, or ${input.keyRemedy}`,
|
|
249
|
+
remediation: [codeFix(`drop the explicit ${user.hint} ahead of the join, or ${input.keyRemedy}`)],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Join type: which sides AQE may split.
|
|
254
|
+
const joinType = JOIN_TYPE.exec(node.detail ?? '')?.[1];
|
|
255
|
+
const canLeft = joinType != null && SPLITTABLE_LEFT.has(joinType);
|
|
256
|
+
const canRight = joinType != null && SPLITTABLE_RIGHT.has(joinType);
|
|
257
|
+
if (joinType != null && !canLeft && !canRight) {
|
|
258
|
+
return {
|
|
259
|
+
case: 'joinType',
|
|
260
|
+
text: `AQE never splits either side of a ${joinType} join: join the hot key on its own and union the results, or ${input.keyRemedy}`,
|
|
261
|
+
remediation: [codeFix(`join the hot key separately and union the results, or ${input.keyRemedy}`)],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Thresholds. A task reads one partition of each side (more when AQE coalesced them), so the
|
|
266
|
+
// task's read is an upper bound on any one partition: a task under the threshold proves no
|
|
267
|
+
// partition is over it. The factor and a coalesced stage's reads are only comparable when the
|
|
268
|
+
// read is not a sum of coalesced partitions.
|
|
269
|
+
const threshold = parseSparkBytes(input.conf(SKEW_THRESHOLD_KEY));
|
|
270
|
+
const factor = Number.parseFloat(input.conf(SKEW_FACTOR_KEY) ?? '');
|
|
271
|
+
const underThreshold = threshold != null && input.readMax <= threshold;
|
|
272
|
+
const underFactor = !coalesced && isolatedSide && Number.isFinite(factor) && input.readP50 > 0 && input.readMax <= factor * input.readP50;
|
|
273
|
+
const biggest = `its largest shuffle partition (${formatBytes(input.readMax)})`;
|
|
274
|
+
if (underThreshold && underFactor) {
|
|
275
|
+
return {
|
|
276
|
+
case: 'belowThreshold',
|
|
277
|
+
text: `${biggest} is under the ${input.conf(SKEW_THRESHOLD_KEY)} threshold (${SKEW_THRESHOLD_KEY}) and only ${ratio}× the median, under the ${factor}× factor (${SKEW_FACTOR_KEY}) AQE needs before it treats a partition as skewed, so lower both for this query`,
|
|
278
|
+
remediation: [decreaseConf(SKEW_THRESHOLD_KEY), decreaseConf(SKEW_FACTOR_KEY)],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (underThreshold) {
|
|
282
|
+
return {
|
|
283
|
+
case: 'belowThreshold',
|
|
284
|
+
text: `${biggest} is under the ${input.conf(SKEW_THRESHOLD_KEY)} AQE needs before it treats a partition as skewed (${SKEW_THRESHOLD_KEY}), so lower the threshold for this query`,
|
|
285
|
+
remediation: [decreaseConf(SKEW_THRESHOLD_KEY)],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (underFactor) {
|
|
289
|
+
return {
|
|
290
|
+
case: 'belowThreshold',
|
|
291
|
+
text: `${biggest} is only ${ratio}× the median, under the ${factor}× AQE needs before it treats a partition as skewed (${SKEW_FACTOR_KEY}), so lower the factor for this query`,
|
|
292
|
+
remediation: [decreaseConf(SKEW_FACTOR_KEY)],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const advisory = parseSparkBytes(input.conf(ADVISORY_SIZE_KEY));
|
|
296
|
+
// Coalescing merges small partitions up to the advisory size on each side, so a task read below
|
|
297
|
+
// twice that can be many partitions that are each under the threshold.
|
|
298
|
+
if (coalesced && advisory != null && input.readMax <= 2 * advisory) return null;
|
|
299
|
+
|
|
300
|
+
const needing = needsJoinPartitioning(join.ancestors);
|
|
301
|
+
// forceOptimizeSkewedJoin exists from Spark 3.3 (no default before), so earlier runs get the key
|
|
302
|
+
// remedy even when they log the property. A run with no recorded version can force when it logs it.
|
|
303
|
+
const force = input.conf(FORCE_SKEW_JOIN_KEY);
|
|
304
|
+
const versionKnown = /^\d+\.\d+/.test(input.sparkVersion ?? '');
|
|
305
|
+
const canForce = versionKnown ? sparkConfDefault(input.sparkVersion, FORCE_SKEW_JOIN_KEY) != null : force != null;
|
|
306
|
+
const forced = canForce && force?.toLowerCase() === 'true';
|
|
307
|
+
const extraShuffleText = needing == null || forced ? null
|
|
308
|
+
: `AQE skipped splitting it because ${needing} above the join needs the join's partitioning and a split would add a shuffle: ${canForce ? `set ${FORCE_SKEW_JOIN_KEY}=true if that shuffle costs less than the tail, or ` : ''}${input.keyRemedy}`;
|
|
309
|
+
|
|
310
|
+
if (joinType != null && canLeft !== canRight) {
|
|
311
|
+
const only = canLeft ? 'left' : 'right';
|
|
312
|
+
const other = canLeft ? 'right' : 'left';
|
|
313
|
+
const tail = needing == null || forced ? ''
|
|
314
|
+
: `; if the skew is on the ${only} side, AQE skipped it because ${needing} above the join needs the join's partitioning and a split would add a shuffle${canForce ? ` (${FORCE_SKEW_JOIN_KEY}=true accepts that)` : ''}`;
|
|
315
|
+
// The other side supplies the nulls or only filters, and writing the join the other way round
|
|
316
|
+
// keeps it that side's table (or changes the join's meaning), so swapping sides cannot help.
|
|
317
|
+
const hotKey = `join the hot key on its own and union the results, or ${input.keyRemedy}`;
|
|
318
|
+
return {
|
|
319
|
+
case: 'joinType',
|
|
320
|
+
text: `AQE can split only the ${only} side of a ${joinType} join, so a skewed partition on the ${other} side stays whole: if that is where the skew is, ${hotKey}${tail}`,
|
|
321
|
+
remediation: [codeFix(hotKey), ...(tail && canForce ? [setConf(FORCE_SKEW_JOIN_KEY, true)] : [])],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (extraShuffleText != null) {
|
|
325
|
+
return { case: 'extraShuffle', text: extraShuffleText, remediation: [canForce ? setConf(FORCE_SKEW_JOIN_KEY, true) : codeFix(input.keyRemedy)] };
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
case: 'notSplit',
|
|
329
|
+
text: `AQE did not split it although the partition is over the thresholds and the plan allows it, and the log does not say why (a partition built from a single map task cannot be split, for one): ${input.keyRemedy}`,
|
|
330
|
+
remediation: [codeFix(input.keyRemedy)],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Which of Spark's skew-join conditions applies to the join running in `stageId`, or null when
|
|
335
|
+
* the plan cannot say: it is not a final adaptive plan, no join can be tied to the stage, or the
|
|
336
|
+
* stage's read is inconclusive (a coalesced read that may be a sum of small partitions). */
|
|
337
|
+
export function diagnoseJoinSkew(input ) {
|
|
338
|
+
const { plan } = input;
|
|
339
|
+
if (plan == null || plan.name !== 'AdaptiveSparkPlan' || !/isFinalPlan=true/.test(plan.detail ?? '')) return null;
|
|
340
|
+
const joins = collectJoins(plan);
|
|
341
|
+
let chosen = joins.filter((j) => j.node.stageIds?.includes(input.stageId));
|
|
342
|
+
// A plan whose joins carry no stage ids at all can still be read when it has a single join.
|
|
343
|
+
// A join with no stage ids stands for the stage only when the stage reads its shuffles.
|
|
344
|
+
if (chosen.length === 0 && joins.length === 1 && !joins[0].node.stageIds?.length && stageReadsJoin(joins[0], input)) chosen = joins;
|
|
345
|
+
const diagnoses = chosen.map((j) => diagnoseJoin(j, input)).filter((d) => d != null);
|
|
346
|
+
return diagnoses.find((d) => d.case === 'split') ?? diagnoses[0] ?? null;
|
|
347
|
+
}
|
|
@@ -2,7 +2,7 @@ import { computeEfficiencyModel } from '../efficiency-model.js';
|
|
|
2
2
|
import { computeSkewRatio, ENTRY_BY_TYPE, } from '../detectors.js';
|
|
3
3
|
import { effectiveThresholds } from '../threshold-overrides.js';
|
|
4
4
|
import { IMPACT_BAND_ORDER } from '../format-utils.js';
|
|
5
|
-
|
|
5
|
+
import { isEscalation, } from '../run-comparison.js';
|
|
6
6
|
import { PAIR_DELTA_METRICS, } from '../stage-alignment.js';
|
|
7
7
|
|
|
8
8
|
|
|
@@ -205,11 +205,13 @@ function checkFailOnIntroduced(comparison , band )
|
|
|
205
205
|
if (band !== 'all' && !IMPACT_BANDS.includes(band )) {
|
|
206
206
|
return { name: 'fail-on-introduced', status: 'inconclusive', detail: `Impact band "${band}" is not recognized (expected "all" or one of ${IMPACT_BANDS.join(', ')}).` };
|
|
207
207
|
}
|
|
208
|
-
const
|
|
208
|
+
const introduced = band === 'all'
|
|
209
209
|
? comparison.findings.introduced
|
|
210
210
|
: comparison.findings.introduced.filter((f) => f.impactBand === band);
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
const escalated = band === 'all' ? [] : comparison.findings.changed.filter((c) => isEscalation(c) && c.candImpactBand === band);
|
|
212
|
+
const count = introduced.reduce((n, f) => n + f.candCount, 0) + escalated.reduce((n, c) => n + c.count, 0);
|
|
213
|
+
return count > 0
|
|
214
|
+
? { name: 'fail-on-introduced', status: 'violation', detail: `${count} introduced finding(s) match "${band}".` }
|
|
213
215
|
: { name: 'fail-on-introduced', status: 'pass', detail: `No introduced findings match "${band}".` };
|
|
214
216
|
}
|
|
215
217
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Worker } from 'node:worker_threads';
|
|
4
|
+
import { collectRun } from './collect-run.js';
|
|
5
|
+
import { mcpError } from '../mcp-error.js';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// Parsing is single-threaded JS, so two logs parsed one after the other take the sum of their
|
|
10
|
+
// times, and parsing both on one thread (interleaved) takes the same. A worker thread parses one of
|
|
11
|
+
// them in parallel at the price of a thread start and a structured clone of the model, so it pays
|
|
12
|
+
// only when the log it parses is big enough (worker-protocol.md, "Decoder and parser calibration").
|
|
13
|
+
const COMPRESSED_LOG = /\.(zstd?|lz4|snappy|zip|gz)$/i;
|
|
14
|
+
// A compressed log holds about this many times its size in event text, and parses at about that
|
|
15
|
+
// many times the cost per byte on disk.
|
|
16
|
+
const COMPRESSED_EXPANSION = 8;
|
|
17
|
+
/** The smaller log's estimated event text (bytes) below which both logs parse in this thread. */
|
|
18
|
+
export const MIN_OFFLOAD_PARSE_BYTES = 32 * 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
/** Event-text bytes a log is expected to parse, from its size on disk. 0 when it cannot be read:
|
|
21
|
+
* `collectRun` reports that failure itself. */
|
|
22
|
+
export function estimatedParseBytes(path ) {
|
|
23
|
+
try {
|
|
24
|
+
const stat = statSync(path);
|
|
25
|
+
if (stat.isDirectory()) return readdirSync(path).reduce((sum, name) => sum + estimatedParseBytes(join(path, name)), 0);
|
|
26
|
+
return COMPRESSED_LOG.test(path) ? stat.size * COMPRESSED_EXPANSION : stat.size;
|
|
27
|
+
} catch {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `collectRun` on a worker thread. A worker that cannot start, dies (out of memory included) or
|
|
33
|
+
* returns a model it cannot clone falls back to parsing in this thread; a log the parser rejects
|
|
34
|
+
* rejects here with the same message and code. */
|
|
35
|
+
export function collectRunInWorker(path ) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
let done = false;
|
|
38
|
+
let worker ;
|
|
39
|
+
try {
|
|
40
|
+
worker = new Worker(new URL('./collect-run-worker.js', import.meta.url), { workerData: { path } });
|
|
41
|
+
} catch {
|
|
42
|
+
// The thread could not be created at all (Node's permission mode without worker access).
|
|
43
|
+
collectRun(path).then(resolve, reject);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const finish = (settle ) => {
|
|
47
|
+
if (done) return;
|
|
48
|
+
done = true;
|
|
49
|
+
void worker.terminate();
|
|
50
|
+
settle();
|
|
51
|
+
};
|
|
52
|
+
const parseHere = () => finish(() => { collectRun(path).then(resolve, reject); });
|
|
53
|
+
worker.once('message', (msg ) => {
|
|
54
|
+
if (msg.run) finish(() => resolve(msg.run ));
|
|
55
|
+
else finish(() => reject(mcpError(msg.failed?.code ?? 'invalid-event-log', msg.failed?.message ?? 'The event log could not be parsed.')));
|
|
56
|
+
});
|
|
57
|
+
worker.once('error', parseHere);
|
|
58
|
+
worker.once('exit', parseHere);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
const settle = (promise ) =>
|
|
69
|
+
promise.then((value) => ({ status: 'fulfilled', value }), (reason) => ({ status: 'rejected', reason }));
|
|
70
|
+
|
|
71
|
+
/** Collects a baseline and a candidate run. When even the smaller log is big enough to repay a
|
|
72
|
+
* thread, it parses in a worker while this thread parses the other, so the pair takes about as long
|
|
73
|
+
* as the bigger log alone; otherwise the two parse here, baseline first, and a baseline that fails
|
|
74
|
+
* stops the candidate from being read. The model is the same either way. */
|
|
75
|
+
export async function collectRunPair(
|
|
76
|
+
baselinePath , candidatePath , { minOffloadBytes = MIN_OFFLOAD_PARSE_BYTES } = {},
|
|
77
|
+
) {
|
|
78
|
+
const baselineBytes = estimatedParseBytes(baselinePath), candidateBytes = estimatedParseBytes(candidatePath);
|
|
79
|
+
if (Math.min(baselineBytes, candidateBytes) < minOffloadBytes) {
|
|
80
|
+
const baseline = await settle(collectRun(baselinePath));
|
|
81
|
+
return baseline.status === 'rejected' ? { baseline } : { baseline, candidate: await settle(collectRun(candidatePath)) };
|
|
82
|
+
}
|
|
83
|
+
const offloadBaseline = baselineBytes <= candidateBytes;
|
|
84
|
+
const [baseline, candidate] = await Promise.all([
|
|
85
|
+
settle(offloadBaseline ? collectRunInWorker(baselinePath) : collectRun(baselinePath)),
|
|
86
|
+
settle(offloadBaseline ? collectRun(candidatePath) : collectRunInWorker(candidatePath)),
|
|
87
|
+
]);
|
|
88
|
+
return { baseline, candidate };
|
|
89
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Worker-thread entry of collect-run-pair.ts: parses one log and posts the finished model back.
|
|
2
|
+
// Plain JS, like the other worker entries, so a published install (which ships pre-stripped .js)
|
|
3
|
+
// and the monorepo (which runs the .ts directly) load it the same way.
|
|
4
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
5
|
+
import { collectRun } from './collect-run.js';
|
|
6
|
+
|
|
7
|
+
let run;
|
|
8
|
+
try {
|
|
9
|
+
run = await collectRun(workerData.path);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
// A log that cannot be parsed fails the same way in the parent, so it is reported, not retried.
|
|
12
|
+
parentPort.postMessage({ failed: { message: e?.message ?? String(e), code: e?.code } });
|
|
13
|
+
}
|
|
14
|
+
// A model the thread cannot clone, or a worker that dies, reaches the parent as a worker error,
|
|
15
|
+
// which parses the log in the parent's own thread instead.
|
|
16
|
+
if (run) parentPort.postMessage({ run });
|
|
@@ -7,8 +7,7 @@ import { routeMessage, } from '../ingest.js';
|
|
|
7
7
|
|
|
8
8
|
import { mcpError } from '../mcp-error.js';
|
|
9
9
|
|
|
10
|
-
// No whole-file arrayBuffer(): the parser
|
|
11
|
-
// whole-file read is what capped local logs at 2 GiB.
|
|
10
|
+
// No whole-file arrayBuffer(): the parser reads bounded slices, so a local log is not capped at 2 GiB.
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Node-only zstd decoding for the CLI/MCP parse path: Node's native zlib zstd, one frame at a
|
|
2
2
|
// time, with the vendored fzstd (what the browser uses) as the fallback.
|
|
3
3
|
//
|
|
4
|
-
// Spark writes an event log as thousands of small zstd frames (one per flush
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Spark writes an event log as thousands of small zstd frames (one per flush). Node's
|
|
5
|
+
// zstdDecompressSync and createZstdDecompress both stop after the first frame (the stream then
|
|
6
|
+
// fails with "Unknown frame descriptor"), so this walks frame boundaries itself and decompresses
|
|
7
|
+
// each complete frame natively, 3-5x faster than fzstd (worker-protocol.md, "Decoder and parser
|
|
8
|
+
// calibration").
|
|
9
9
|
import * as zlib from 'node:zlib';
|
|
10
10
|
import { Decompress as ZstdDecompress } from '../vendor/fzstd.js';
|
|
11
11
|
|
|
@@ -102,8 +102,7 @@ export const nativeZstdAvailable =
|
|
|
102
102
|
// falls back to fzstd when frameEnd says NOT_NATIVE, when it is still incomplete after
|
|
103
103
|
// `maxFrameBytes` compressed bytes, or when a final push ends inside it. A frame that arrives
|
|
104
104
|
// over many pushes is gathered into one buffer that grows by doubling, and its walk resumes where
|
|
105
|
-
// the last push left it
|
|
106
|
-
// its start on every push cost about 4 GB of copying near the 64 MB limit.
|
|
105
|
+
// the last push left it, so a large frame is not copied and re-walked on every push.
|
|
107
106
|
class FrameSplitter {
|
|
108
107
|
// The incomplete frame's bytes so far, gathered[0, gatheredLength); null when none is pending.
|
|
109
108
|
gathered = null;
|
|
@@ -188,7 +187,7 @@ function decompressBounded(frame , maxBytes )
|
|
|
188
187
|
|
|
189
188
|
// Same push(chunk, final) contract as fzstd's Decompress. Once a frame falls back, fzstd takes
|
|
190
189
|
// the rest of the stream from that frame's first byte; a truncated last frame goes to fzstd too,
|
|
191
|
-
// so it fails with fzstd's own "unexpected EOF"
|
|
190
|
+
// so it fails with fzstd's own "unexpected EOF". `maxFrameBytes` is for tests.
|
|
192
191
|
export function createNativeZstdDecoder(
|
|
193
192
|
onChunk ,
|
|
194
193
|
{ maxFrameBytes = MAX_NATIVE_FRAME_BYTES } = {},
|
|
@@ -218,15 +217,13 @@ export function createNativeZstdDecoder(
|
|
|
218
217
|
|
|
219
218
|
// Frames at least this big (compressed) are decompressed on libuv's threadpool, up to
|
|
220
219
|
// MAX_FRAMES_IN_FLIGHT at a time, while the main thread parses the output of earlier ones.
|
|
221
|
-
// Smaller frames cost less to decompress inline than to hand off
|
|
222
|
-
//
|
|
223
|
-
// log 4 in flight (the pool's default size) parsed in 3.1s at 711 MB peak RSS, 8 in 3.2s at 886 MB
|
|
224
|
-
// and 2 in 3.5s at 607 MB. The pool's output chunk size barely mattered (64 KB to 1 MB).
|
|
220
|
+
// Smaller frames cost less to decompress inline than to hand off. 4 in flight is the threadpool's
|
|
221
|
+
// default size. Measurements: worker-protocol.md, "Decoder and parser calibration".
|
|
225
222
|
const THREADED_MIN_FRAME_BYTES = 64 * 1024;
|
|
226
223
|
const MAX_FRAMES_IN_FLIGHT = 4;
|
|
227
224
|
const THREADED_CHUNK_BYTES = 256 * 1024;
|
|
228
|
-
// Small frames that arrive while an earlier frame is in flight wait in the queue behind it
|
|
229
|
-
//
|
|
225
|
+
// Small frames that arrive while an earlier frame is in flight wait in the queue behind it; the
|
|
226
|
+
// cap bounds the output they hold.
|
|
230
227
|
const MAX_QUEUED_FRAMES = 64;
|
|
231
228
|
|
|
232
229
|
// One frame decompressing on the threadpool. Node's zstd stream ends after one frame, so each
|
|
@@ -283,8 +280,7 @@ class OffThreadFrame {
|
|
|
283
280
|
// push must be awaited before the next. It keeps references to pushed bytes until their frames
|
|
284
281
|
// are decoded, so callers must not reuse a pushed buffer. Fallback to fzstd, for the same frames
|
|
285
282
|
// as createNativeZstdDecoder, happens only after every queued frame is delivered; a failed frame
|
|
286
|
-
// rejects the push that reaches it. `threadedMinFrameBytes` is for tests.
|
|
287
|
-
// logs: parse 9.4s -> 8.1s; logs with few large frames (12 of 6455 on a 28 MB one) gain nothing.
|
|
283
|
+
// rejects the push that reaches it. `threadedMinFrameBytes` is for tests.
|
|
288
284
|
export function createThreadedZstdDecoder(
|
|
289
285
|
onChunk ,
|
|
290
286
|
{ maxFrameBytes = MAX_NATIVE_FRAME_BYTES, threadedMinFrameBytes = THREADED_MIN_FRAME_BYTES }
|