sparkforensics-cli 0.1.0 → 0.2.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/README.md +6 -0
- package/bin/sparkforensics-analyze.mjs +113 -48
- package/export-template/docs/404.html +25 -0
- package/export-template/docs/assets/app.CndaAS6v.js +1 -0
- package/export-template/docs/assets/aqe-loop.IwQSATHw.svg +1 -0
- package/export-template/docs/assets/aqe-loop.dark.DGbaxqJE.svg +1 -0
- package/export-template/docs/assets/broadcast-vs-shuffle.Db4WY1XK.svg +1 -0
- package/export-template/docs/assets/broadcast-vs-shuffle.dark.C7Bxs0mG.svg +1 -0
- package/export-template/docs/assets/cache-lifecycle.dark.B-hS7AgU.svg +1 -0
- package/export-template/docs/assets/cache-lifecycle.rEOVYQNU.svg +1 -0
- package/export-template/docs/assets/chunks/@localSearchIndexroot.DNY8bVcl.js +1 -0
- package/export-template/docs/assets/chunks/VPLocalSearchBox.yJbZbsEo.js +9 -0
- package/export-template/docs/assets/chunks/duplicate-plan-subtree.dark.Cdp70QhV.js +1 -0
- package/export-template/docs/assets/chunks/framework.DSg0KOwT.js +20 -0
- package/export-template/docs/assets/chunks/retry-escalation-ladder.dark.DHipdJgZ.js +1 -0
- package/export-template/docs/assets/chunks/theme.Df2VAG9w.js +2 -0
- package/export-template/docs/assets/cold-start-timeline.DxC_Sc7w.svg +1 -0
- package/export-template/docs/assets/cold-start-timeline.dark.CZ17YcAG.svg +1 -0
- package/export-template/docs/assets/columnar-layout.PghGeOEA.svg +1 -0
- package/export-template/docs/assets/columnar-layout.dark.BVNlz0ff.svg +1 -0
- package/export-template/docs/assets/container-memory.DIO0AnIm.svg +1 -0
- package/export-template/docs/assets/container-memory.dark.CP-5zuCl.svg +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.B-OsL91z.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.B-OsL91z.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.BOeH4d1J.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.BOeH4d1J.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_drill-down.md.BtPdlM7r.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_drill-down.md.BtPdlM7r.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_impact-estimation.md.DYCDPgkh.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_impact-estimation.md.DYCDPgkh.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_index.md.3TO9ic6w.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_index.md.3TO9ic6w.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_overview.md.CehiRmGn.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_overview.md.CehiRmGn.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.m3S3UdMk.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.m3S3UdMk.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.DbqPf2OT.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.DbqPf2OT.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_worker-protocol.md.B93qJ_tT.js +6 -0
- package/export-template/docs/assets/contributor-guide_architecture_worker-protocol.md.B93qJ_tT.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_contributing.md.CvRsdr6J.js +1 -0
- package/export-template/docs/assets/contributor-guide_contributing.md.CvRsdr6J.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_development-setup.md.DvAN_9mK.js +12 -0
- package/export-template/docs/assets/contributor-guide_development-setup.md.DvAN_9mK.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_testing.md.6rIKqSyY.js +1 -0
- package/export-template/docs/assets/contributor-guide_testing.md.6rIKqSyY.lean.js +1 -0
- package/export-template/docs/assets/dag-stages.DSz_S937.svg +1 -0
- package/export-template/docs/assets/dag-stages.dark.F72UzxH4.svg +1 -0
- package/export-template/docs/assets/driver-executor.D5pQ7YN1.svg +1 -0
- package/export-template/docs/assets/driver-executor.dark.BmX9cPvh.svg +1 -0
- package/export-template/docs/assets/duplicate-plan-subtree.B4cvN6fj.svg +1 -0
- package/export-template/docs/assets/duplicate-plan-subtree.dark.Dw8wS0Ag.svg +1 -0
- package/export-template/docs/assets/index.md.CHJVslga.js +1 -0
- package/export-template/docs/assets/index.md.CHJVslga.lean.js +1 -0
- package/export-template/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/export-template/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/export-template/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/export-template/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/export-template/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/export-template/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/export-template/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/export-template/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/export-template/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/export-template/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/export-template/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/export-template/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/export-template/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/export-template/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/export-template/docs/assets/join-strategy.C_FvrCEo.svg +1 -0
- package/export-template/docs/assets/join-strategy.dark.ChMLnNII.svg +1 -0
- package/export-template/docs/assets/memory-borrowing.BqQRJg0u.svg +1 -0
- package/export-template/docs/assets/memory-borrowing.dark.Yhh20O9C.svg +1 -0
- package/export-template/docs/assets/memory-regions.XHvO7jHG.svg +1 -0
- package/export-template/docs/assets/memory-regions.dark.D4TP9_08.svg +1 -0
- package/export-template/docs/assets/repartition-vs-coalesce.BovLRrpj.svg +1 -0
- package/export-template/docs/assets/repartition-vs-coalesce.dark.BhAczKZQ.svg +1 -0
- package/export-template/docs/assets/retry-escalation-ladder.DyTKJJmZ.svg +1 -0
- package/export-template/docs/assets/retry-escalation-ladder.dark.BdsabtU3.svg +1 -0
- package/export-template/docs/assets/shuffle-map-reduce.KuOEZVmg.svg +1 -0
- package/export-template/docs/assets/shuffle-map-reduce.dark.BgQZnFSb.svg +1 -0
- package/export-template/docs/assets/spill-classification.BU2euYDO.svg +1 -0
- package/export-template/docs/assets/spill-classification.dark.D7i1M40d.svg +1 -0
- package/export-template/docs/assets/style.DXOMCXxn.css +1 -0
- package/export-template/docs/assets/tuning-reference_anti-patterns.md.Df1YMIHu.js +1 -0
- package/export-template/docs/assets/tuning-reference_anti-patterns.md.Df1YMIHu.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_aqe.md.BIsCtLzm.js +1 -0
- package/export-template/docs/assets/tuning-reference_aqe.md.BIsCtLzm.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.CEstB3Ia.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.CEstB3Ia.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-cold-start.md.CEuy-72y.js +7 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-cold-start.md.CEuy-72y.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.CIohQDfn.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.CIohQDfn.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-failures.md.4z5BXGJ2.js +6 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-failures.md.4z5BXGJ2.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-gc.md.DSxzZRK7.js +6 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-gc.md.DSxzZRK7.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.BaJl__1W.js +8 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.BaJl__1W.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-memory-utilization.md.DbP-SJZc.js +7 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-memory-utilization.md.DbP-SJZc.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-retry-waste.md.D5JMjVOt.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-retry-waste.md.D5JMjVOt.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-shuffle.md.CM-nTmIH.js +12 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-shuffle.md.CM-nTmIH.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-skew.md.BdUwiDhn.js +14 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-skew.md.BdUwiDhn.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-slow-host.md.BlIo6UDW.js +7 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-slow-host.md.BlIo6UDW.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-small-files.md.B8kloyx8.js +5 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-small-files.md.B8kloyx8.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-spill.md.PNH7mITt.js +6 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-spill.md.PNH7mITt.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-straggler.md.DY36fHN5.js +7 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-straggler.md.DY36fHN5.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.QTV7O8kU.js +8 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.QTV7O8kU.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-utilization.md.DTiueZC3.js +5 -0
- package/export-template/docs/assets/tuning-reference_bottleneck-utilization.md.DTiueZC3.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_caching.md.B7aQ8asB.js +1 -0
- package/export-template/docs/assets/tuning-reference_caching.md.B7aQ8asB.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_cluster-config.md.ZVmDGsQ3.js +1 -0
- package/export-template/docs/assets/tuning-reference_cluster-config.md.ZVmDGsQ3.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_config.md.UvveiWG3.js +1 -0
- package/export-template/docs/assets/tuning-reference_config.md.UvveiWG3.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_data-formats.md.bjCAWH3N.js +1 -0
- package/export-template/docs/assets/tuning-reference_data-formats.md.bjCAWH3N.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_index.md.BQ_NooMV.js +1 -0
- package/export-template/docs/assets/tuning-reference_index.md.BQ_NooMV.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_intro.md.CobD-lGB.js +1 -0
- package/export-template/docs/assets/tuning-reference_intro.md.CobD-lGB.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_joins.md.BtKs_CuW.js +1 -0
- package/export-template/docs/assets/tuning-reference_joins.md.BtKs_CuW.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_memory-model.md.DhT-n4y3.js +1 -0
- package/export-template/docs/assets/tuning-reference_memory-model.md.DhT-n4y3.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_metrics.md.mLOh7Apj.js +1 -0
- package/export-template/docs/assets/tuning-reference_metrics.md.mLOh7Apj.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_partitioning.md.q0zKF_8X.js +1 -0
- package/export-template/docs/assets/tuning-reference_partitioning.md.q0zKF_8X.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_pyspark.md.DDCfvN9t.js +6 -0
- package/export-template/docs/assets/tuning-reference_pyspark.md.DDCfvN9t.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_shuffle.md.BZZ7R4Ix.js +1 -0
- package/export-template/docs/assets/tuning-reference_shuffle.md.BZZ7R4Ix.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_spark-architecture.md.Dwzm5avO.js +1 -0
- package/export-template/docs/assets/tuning-reference_spark-architecture.md.Dwzm5avO.lean.js +1 -0
- package/export-template/docs/assets/tuning-reference_table-formats.md.D6wj-2dX.js +1 -0
- package/export-template/docs/assets/tuning-reference_table-formats.md.D6wj-2dX.lean.js +1 -0
- package/export-template/docs/assets/udf-execution-models.BUFDICuG.svg +1 -0
- package/export-template/docs/assets/udf-execution-models.dark.YTNS6GDq.svg +1 -0
- package/export-template/docs/assets/user-guide_alternative-log-retrieval.md.sU3KGarf.js +1 -0
- package/export-template/docs/assets/user-guide_alternative-log-retrieval.md.sU3KGarf.lean.js +1 -0
- package/export-template/docs/assets/user-guide_getting-started.md.DtEM37MK.js +3 -0
- package/export-template/docs/assets/user-guide_getting-started.md.DtEM37MK.lean.js +1 -0
- package/export-template/docs/assets/user-guide_mcp-tools.md.C8MiIu7F.js +125 -0
- package/export-template/docs/assets/user-guide_mcp-tools.md.C8MiIu7F.lean.js +1 -0
- package/export-template/docs/assets/user-guide_run-comparison.md.S0TWWmLY.js +1 -0
- package/export-template/docs/assets/user-guide_run-comparison.md.S0TWWmLY.lean.js +1 -0
- package/export-template/docs/assets/user-guide_understanding-findings.md.D0R_Y-R2.js +1 -0
- package/export-template/docs/assets/user-guide_understanding-findings.md.D0R_Y-R2.lean.js +1 -0
- package/export-template/docs/contributor-guide/architecture/board-widgets.html +25 -0
- package/export-template/docs/contributor-guide/architecture/detector-contract.html +25 -0
- package/export-template/docs/contributor-guide/architecture/drill-down.html +25 -0
- package/export-template/docs/contributor-guide/architecture/impact-estimation.html +25 -0
- package/export-template/docs/contributor-guide/architecture/index.html +25 -0
- package/export-template/docs/contributor-guide/architecture/overview.html +25 -0
- package/export-template/docs/contributor-guide/architecture/state-and-history.html +25 -0
- package/export-template/docs/contributor-guide/architecture/widget-rendering.html +25 -0
- package/export-template/docs/contributor-guide/architecture/worker-protocol.html +30 -0
- package/export-template/docs/contributor-guide/contributing.html +25 -0
- package/export-template/docs/contributor-guide/development-setup.html +36 -0
- package/export-template/docs/contributor-guide/testing.html +25 -0
- package/export-template/docs/favicon.svg +4 -0
- package/export-template/docs/hashmap.json +1 -0
- package/export-template/docs/index.html +25 -0
- package/export-template/docs/package.json +1 -0
- package/export-template/docs/tuning-reference/anti-patterns.html +25 -0
- package/export-template/docs/tuning-reference/aqe.html +25 -0
- package/export-template/docs/tuning-reference/bottleneck-broadcast-sizing.html +25 -0
- package/export-template/docs/tuning-reference/bottleneck-cold-start.html +31 -0
- package/export-template/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +25 -0
- package/export-template/docs/tuning-reference/bottleneck-failures.html +30 -0
- package/export-template/docs/tuning-reference/bottleneck-gc.html +30 -0
- package/export-template/docs/tuning-reference/bottleneck-job-failure-rate.html +32 -0
- package/export-template/docs/tuning-reference/bottleneck-memory-utilization.html +31 -0
- package/export-template/docs/tuning-reference/bottleneck-retry-waste.html +25 -0
- package/export-template/docs/tuning-reference/bottleneck-shuffle.html +36 -0
- package/export-template/docs/tuning-reference/bottleneck-skew.html +38 -0
- package/export-template/docs/tuning-reference/bottleneck-slow-host.html +31 -0
- package/export-template/docs/tuning-reference/bottleneck-small-files.html +29 -0
- package/export-template/docs/tuning-reference/bottleneck-spill.html +30 -0
- package/export-template/docs/tuning-reference/bottleneck-straggler.html +31 -0
- package/export-template/docs/tuning-reference/bottleneck-tiny-tasks.html +32 -0
- package/export-template/docs/tuning-reference/bottleneck-utilization.html +29 -0
- package/export-template/docs/tuning-reference/caching.html +25 -0
- package/export-template/docs/tuning-reference/cluster-config.html +25 -0
- package/export-template/docs/tuning-reference/config.html +25 -0
- package/export-template/docs/tuning-reference/data-formats.html +25 -0
- package/export-template/docs/tuning-reference/index.html +25 -0
- package/export-template/docs/tuning-reference/intro.html +25 -0
- package/export-template/docs/tuning-reference/joins.html +25 -0
- package/export-template/docs/tuning-reference/memory-model.html +25 -0
- package/export-template/docs/tuning-reference/metrics.html +25 -0
- package/export-template/docs/tuning-reference/partitioning.html +25 -0
- package/export-template/docs/tuning-reference/pyspark.html +30 -0
- package/export-template/docs/tuning-reference/shuffle.html +25 -0
- package/export-template/docs/tuning-reference/spark-architecture.html +25 -0
- package/export-template/docs/tuning-reference/table-formats.html +25 -0
- package/export-template/docs/user-guide/alternative-log-retrieval.html +25 -0
- package/export-template/docs/user-guide/getting-started.html +27 -0
- package/export-template/docs/user-guide/mcp-tools.html +149 -0
- package/export-template/docs/user-guide/run-comparison.html +25 -0
- package/export-template/docs/user-guide/understanding-findings.html +25 -0
- package/export-template/docs/vp-icons.css +0 -0
- package/export-template/favicon.svg +4 -0
- package/export-template/index.html +115 -0
- package/export-template/parser-worker-QqyEE4m9.js +64 -0
- package/package.json +16 -3
- package/vendor-core/analyzer.js +74 -74
- package/vendor-core/cli/budgets.js +13 -27
- package/vendor-core/cli/collect-run.js +43 -19
- package/vendor-core/core-count.js +25 -27
- package/vendor-core/core-locality-ratio.js +4 -11
- package/vendor-core/core-time-series.js +6 -12
- package/vendor-core/core-usage-locality.js +3 -4
- package/vendor-core/detectors.js +256 -375
- package/vendor-core/docs-config.js +69 -21
- package/vendor-core/docs-content/chapters/01-intro.md +32 -0
- package/vendor-core/docs-content/chapters/02-spark-architecture.md +76 -0
- package/vendor-core/docs-content/chapters/03-memory-model.md +73 -0
- package/vendor-core/docs-content/chapters/04-partitioning.md +65 -0
- package/vendor-core/docs-content/chapters/05-joins.md +62 -0
- package/vendor-core/docs-content/chapters/06-shuffle.md +59 -0
- package/vendor-core/docs-content/chapters/07-data-formats.md +81 -0
- package/vendor-core/docs-content/chapters/07b-table-formats.md +56 -0
- package/vendor-core/docs-content/chapters/08-caching.md +58 -0
- package/vendor-core/docs-content/chapters/09-pyspark.md +78 -0
- package/vendor-core/docs-content/chapters/10-aqe.md +167 -0
- package/vendor-core/docs-content/chapters/11-cluster-config.md +170 -0
- package/vendor-core/docs-content/chapters/12-anti-patterns.md +171 -0
- package/vendor-core/docs-content/chapters/14-metrics.md +87 -0
- package/vendor-core/docs-content/chapters/15-config.md +93 -0
- package/vendor-core/docs-content/chapters/nav-index.json +370 -0
- package/vendor-core/docs-content/detection/cache.md +6 -0
- package/vendor-core/docs-content/detection/cfg.md +15 -0
- package/vendor-core/docs-content/detection/chrn.md +7 -0
- package/vendor-core/docs-content/detection/cold.md +4 -0
- package/vendor-core/docs-content/detection/cstor.md +4 -0
- package/vendor-core/docs-content/detection/fail.md +5 -0
- package/vendor-core/docs-content/detection/gc.md +4 -0
- package/vendor-core/docs-content/detection/host.md +5 -0
- package/vendor-core/docs-content/detection/incmp.md +6 -0
- package/vendor-core/docs-content/detection/jobs.md +4 -0
- package/vendor-core/docs-content/detection/local.md +7 -0
- package/vendor-core/docs-content/detection/mem.md +10 -0
- package/vendor-core/docs-content/detection/part.md +5 -0
- package/vendor-core/docs-content/detection/plan.md +14 -0
- package/vendor-core/docs-content/detection/retry.md +4 -0
- package/vendor-core/docs-content/detection/sfail.md +5 -0
- package/vendor-core/docs-content/detection/shape.md +5 -0
- package/vendor-core/docs-content/detection/shfl.md +4 -0
- package/vendor-core/docs-content/detection/skew.md +6 -0
- package/vendor-core/docs-content/detection/slow.md +6 -0
- package/vendor-core/docs-content/detection/spec.md +7 -0
- package/vendor-core/docs-content/detection/spill.md +7 -0
- package/vendor-core/docs-content/detection/strag.md +5 -0
- package/vendor-core/docs-content/detection/tiny.md +4 -0
- package/vendor-core/docs-content/detection/util.md +4 -0
- package/vendor-core/docs-content/diagrams/aqe-loop.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/aqe-loop.svg +1 -0
- package/vendor-core/docs-content/diagrams/broadcast-vs-shuffle.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/broadcast-vs-shuffle.svg +1 -0
- package/vendor-core/docs-content/diagrams/cache-lifecycle.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/cache-lifecycle.svg +1 -0
- package/vendor-core/docs-content/diagrams/cold-start-timeline.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/cold-start-timeline.svg +1 -0
- package/vendor-core/docs-content/diagrams/columnar-layout.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/columnar-layout.svg +1 -0
- package/vendor-core/docs-content/diagrams/container-memory.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/container-memory.svg +1 -0
- package/vendor-core/docs-content/diagrams/dag-stages.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/dag-stages.svg +1 -0
- package/vendor-core/docs-content/diagrams/driver-executor.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/driver-executor.svg +1 -0
- package/vendor-core/docs-content/diagrams/duplicate-plan-subtree.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/duplicate-plan-subtree.svg +1 -0
- package/vendor-core/docs-content/diagrams/join-strategy.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/join-strategy.svg +1 -0
- package/vendor-core/docs-content/diagrams/memory-borrowing.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/memory-borrowing.svg +1 -0
- package/vendor-core/docs-content/diagrams/memory-regions.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/memory-regions.svg +1 -0
- package/vendor-core/docs-content/diagrams/repartition-vs-coalesce.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/repartition-vs-coalesce.svg +1 -0
- package/vendor-core/docs-content/diagrams/retry-escalation-ladder.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/retry-escalation-ladder.svg +1 -0
- package/vendor-core/docs-content/diagrams/shuffle-map-reduce.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/shuffle-map-reduce.svg +1 -0
- package/vendor-core/docs-content/diagrams/spill-classification.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/spill-classification.svg +1 -0
- package/vendor-core/docs-content/diagrams/udf-execution-models.dark.svg +1 -0
- package/vendor-core/docs-content/diagrams/udf-execution-models.svg +1 -0
- package/vendor-core/docs-content/tuning/broadcast-sizing.md +78 -0
- package/vendor-core/docs-content/tuning/cold-start.md +81 -0
- package/vendor-core/docs-content/tuning/duplicate-plan-subtree.md +45 -0
- package/vendor-core/docs-content/tuning/failures.md +124 -0
- package/vendor-core/docs-content/tuning/gc.md +110 -0
- package/vendor-core/docs-content/tuning/job-failure-rate.md +101 -0
- package/vendor-core/docs-content/tuning/memory-utilization.md +58 -0
- package/vendor-core/docs-content/tuning/retry-waste.md +90 -0
- package/vendor-core/docs-content/tuning/shuffle.md +154 -0
- package/vendor-core/docs-content/tuning/skew.md +123 -0
- package/vendor-core/docs-content/tuning/slow-host.md +117 -0
- package/vendor-core/docs-content/tuning/small-files.md +99 -0
- package/vendor-core/docs-content/tuning/spill.md +114 -0
- package/vendor-core/docs-content/tuning/straggler.md +103 -0
- package/vendor-core/docs-content/tuning/tiny-tasks.md +94 -0
- package/vendor-core/docs-content/tuning/utilization.md +90 -0
- package/vendor-core/docs-site-config.js +10 -17
- package/vendor-core/efficiency-model.js +7 -13
- package/vendor-core/etl-phases.js +3 -5
- package/vendor-core/event-handlers.js +232 -134
- package/vendor-core/event-schemas.js +48 -114
- package/vendor-core/evidence-availability.js +5 -10
- package/vendor-core/evidence-report.js +72 -122
- package/vendor-core/export-data.js +48 -0
- package/vendor-core/finding-action-label.js +4 -10
- package/vendor-core/finding-filter-predicate.js +3 -7
- package/vendor-core/finding-generic-recommendation.js +112 -0
- package/vendor-core/finding-names.js +51 -0
- package/vendor-core/format-utils.js +112 -38
- package/vendor-core/impact-band.js +18 -24
- package/vendor-core/impact-estimator.js +38 -74
- package/vendor-core/ingest.js +7 -13
- package/vendor-core/job-groups.js +3 -6
- package/vendor-core/list-runs.js +278 -0
- package/vendor-core/load-vendored.js +6 -12
- package/vendor-core/log-header-peek.js +81 -0
- package/vendor-core/lz4-block.js +4 -6
- package/vendor-core/mcp-server-factory.js +38 -8
- package/vendor-core/mcp-tools.js +105 -76
- package/vendor-core/model-assembler.js +8 -16
- package/vendor-core/occupancy.js +5 -9
- package/vendor-core/parser-worker.js +18 -27
- package/vendor-core/plan-dot.js +2 -5
- package/vendor-core/plan-duration-attribution.js +78 -29
- package/vendor-core/plan-graph-model.js +126 -69
- package/vendor-core/plan-node-detail.js +31 -17
- package/vendor-core/plan-summary.js +19 -8
- package/vendor-core/recommendation-rollup.js +35 -39
- package/vendor-core/redact.js +72 -16
- package/vendor-core/rolling-log-reassembly.js +4 -6
- package/vendor-core/run-comparison.js +65 -70
- package/vendor-core/scaling-sim.js +5 -7
- package/vendor-core/session-snapshot.js +1 -1
- package/vendor-core/shs-fetch.js +4 -6
- package/vendor-core/shs-load.js +9 -13
- package/vendor-core/shs-request.js +1 -1
- package/vendor-core/stage-quantiles.js +14 -0
- package/vendor-core/types.js +78 -18
- package/vendor-core/wasted-core-hours.js +7 -12
package/vendor-core/mcp-tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { resolve as resolvePath } from 'node:path';
|
|
2
|
-
import { existsSync, statSync } from 'node:fs';
|
|
1
|
+
import { resolve as resolvePath, join, dirname } from 'node:path';
|
|
2
|
+
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { collectRun } from './cli/collect-run.js';
|
|
5
6
|
import { deriveEvidenceAvailability } from './evidence-availability.js';
|
|
@@ -11,45 +12,31 @@ import { computeWallClock } from './wall-clock.js';
|
|
|
11
12
|
import { analyze } from './analyzer.js';
|
|
12
13
|
import { buildComparison, renderComparisonMarkdown, } from './run-comparison.js';
|
|
13
14
|
import { evaluateBudgets, } from './cli/budgets.js';
|
|
15
|
+
import { FINDING_NAMES, titleCase } from './finding-names.js';
|
|
16
|
+
import { docAnchorForType, tuningDocSlugForAnchor, pageForAnchor } from './docs-config.js';
|
|
17
|
+
import { typeTag } from './format-utils.js';
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// flattened
|
|
20
|
-
// that shape: resolveOrCreateRun's own destructuring (`{ source, runId } = {}`
|
|
21
|
-
// below), both mcp-server-factory.ts call sites (`compareRuns({ runId: runIdA,
|
|
22
|
-
// source: sourceA }, ...)`), and every resolveOrCreateRun/compareRuns call in
|
|
23
|
-
// tests/mcp-tools.test.js all pass a *nested* `source` key, never a flattened
|
|
24
|
-
// path/shsBaseUrl directly on the ref object. Widened to match that real,
|
|
25
|
-
// long-standing shape instead of reshaping every working call site (or every
|
|
26
|
-
// test) to fit the literal text.
|
|
21
|
+
// RunRef uses a nested `source` key, matching every real call site (resolveOrCreateRun's
|
|
22
|
+
// destructuring, both mcp-server-factory.ts sites, and the tests all pass a nested `source`,
|
|
23
|
+
// never flattened path/shsBaseUrl).
|
|
27
24
|
|
|
28
25
|
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
|
|
34
28
|
|
|
35
29
|
|
|
36
30
|
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
43
35
|
|
|
44
36
|
|
|
45
|
-
//
|
|
46
|
-
// (
|
|
47
|
-
// stageSkew/baseStages/candStages
|
|
48
|
-
// distinct, smaller MCP-facing projection of that (runIdA/runIdB/
|
|
49
|
-
// findingsDelta/metricDeltas/confidence/reason/matchedCoverage); it never
|
|
50
|
-
// had baselineLabel, stageSkew, baseStages, or candStages. Introduced this
|
|
51
|
-
// interface to match the real returned shape rather than casting a literal
|
|
52
|
-
// that's actually missing several required CompareRunsResult properties.
|
|
37
|
+
// compareRuns returns a smaller MCP-facing projection of CompareRunsResult
|
|
38
|
+
// (runIdA/runIdB/findingsDelta/metricDeltas/confidence/reason/matchedCoverage), not the full raw
|
|
39
|
+
// shape (no baselineLabel/stageSkew/baseStages/candStages).
|
|
53
40
|
|
|
54
41
|
|
|
55
42
|
|
|
@@ -65,9 +52,8 @@ function envInt(name , fallback ) {
|
|
|
65
52
|
return Number.isFinite(v) && v > 0 ? v : fallback;
|
|
66
53
|
}
|
|
67
54
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// the cost if this cap is ever raised significantly.
|
|
55
|
+
// touch() below does a full Map delete+re-insert per cache hit to maintain LRU order: O(n) per
|
|
56
|
+
// touch, fine at this small cap but worth re-checking if the cap is raised significantly.
|
|
71
57
|
const CACHE_CAP = envInt('SPARKFORENSICS_MCP_CACHE_CAP', 8);
|
|
72
58
|
const CACHE_TTL_MS = envInt('SPARKFORENSICS_MCP_CACHE_TTL_MS', 15 * 60 * 1000);
|
|
73
59
|
|
|
@@ -80,10 +66,9 @@ const pendingByCacheKey = new Map
|
|
|
80
66
|
export function pathCacheKey(path ) {
|
|
81
67
|
const resolved = resolvePath(path);
|
|
82
68
|
if (!existsSync(resolved)) throw mcpError('invalid-event-log', `No such file: ${resolved}`);
|
|
83
|
-
// Size narrows a same-millisecond mtime collision; ctime narrows the case
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// tool, so it still reflects the real time the file landed on disk.
|
|
69
|
+
// Size narrows a same-millisecond mtime collision; ctime narrows the case where a copy tool
|
|
70
|
+
// (rsync --preserve-times, tar) restores an identical mtime+size for different content: ctime
|
|
71
|
+
// can't be set by the copying tool, so it still reflects when the file landed on disk.
|
|
87
72
|
const { mtimeMs, ctimeMs, size } = statSync(resolved);
|
|
88
73
|
return `path:${resolved}:${mtimeMs}:${ctimeMs}:${size}`;
|
|
89
74
|
}
|
|
@@ -94,8 +79,7 @@ function shsCacheKey({ shsBaseUrl, appId, attemptId }
|
|
|
94
79
|
|
|
95
80
|
function touch(runId ) {
|
|
96
81
|
const entry = byRunId.get(runId);
|
|
97
|
-
// Defensive only for type-narrowing: every call site
|
|
98
|
-
// it (or getCachedAppModel) has already confirmed present.
|
|
82
|
+
// Defensive only for type-narrowing: every call site touches a runId already confirmed present.
|
|
99
83
|
if (!entry) return;
|
|
100
84
|
byRunId.delete(runId);
|
|
101
85
|
entry.lastAccess = Date.now();
|
|
@@ -119,8 +103,7 @@ function evictStale() {
|
|
|
119
103
|
function evictOverflow() {
|
|
120
104
|
while (byRunId.size > CACHE_CAP) {
|
|
121
105
|
const oldestId = byRunId.keys().next().value;
|
|
122
|
-
// Defensive only for type-narrowing: the while condition guarantees
|
|
123
|
-
// byRunId is non-empty here, so .keys().next() always has a value.
|
|
106
|
+
// Defensive only for type-narrowing: the while condition guarantees byRunId is non-empty here.
|
|
124
107
|
if (oldestId === undefined) break;
|
|
125
108
|
const entry = byRunId.get(oldestId);
|
|
126
109
|
if (entry) deleteRunEntry(oldestId, entry);
|
|
@@ -152,11 +135,8 @@ export async function resolveOrCreateRun(
|
|
|
152
135
|
}
|
|
153
136
|
if (!source) throw mcpError('access-or-upstream-failure', 'Provide either source or runId.');
|
|
154
137
|
|
|
155
|
-
// `'path' in source`, not `source.path`: RunSource's two variants share no
|
|
156
|
-
//
|
|
157
|
-
// union. Equivalent at runtime to the original truthy check for every real
|
|
158
|
-
// caller (a path source's `path` is always a non-empty resolved filesystem
|
|
159
|
-
// path; it's never the empty string).
|
|
138
|
+
// `'path' in source`, not `source.path`: RunSource's two variants share no common property, so a
|
|
139
|
+
// plain `.path` access doesn't type-check on the union. Equivalent at runtime for every real caller.
|
|
160
140
|
const cacheKey = 'path' in source ? pathCacheKey(source.path) : shsCacheKey(source);
|
|
161
141
|
const cached = byCacheKey.get(cacheKey);
|
|
162
142
|
if (cached && byRunId.has(cached)) {
|
|
@@ -164,11 +144,9 @@ export async function resolveOrCreateRun(
|
|
|
164
144
|
return { runId: cached, appModel: byRunId.get(cached) .appModel };
|
|
165
145
|
}
|
|
166
146
|
|
|
167
|
-
// Concurrent calls for the same not-yet-cached source
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// after the first is an orphaned entry (unreachable via cacheKey, wasting a
|
|
171
|
-
// cache slot until TTL/overflow).
|
|
147
|
+
// Concurrent calls for the same not-yet-cached source must await one shared parse, not each race
|
|
148
|
+
// to insert its own entry: otherwise every racer's insert after the first is orphaned (unreachable
|
|
149
|
+
// via cacheKey, wasting a cache slot until TTL/overflow).
|
|
172
150
|
const pending = pendingByCacheKey.get(cacheKey);
|
|
173
151
|
if (pending) return pending;
|
|
174
152
|
|
|
@@ -176,7 +154,7 @@ export async function resolveOrCreateRun(
|
|
|
176
154
|
try {
|
|
177
155
|
const appModel = 'path' in source
|
|
178
156
|
? await resolveFromPath(source.path)
|
|
179
|
-
: await resolveFromShs(source.shsBaseUrl, source.appId, source.attemptId, { fetchImpl, maxArchiveBytes, idleTimeoutMs });
|
|
157
|
+
: (await resolveFromShs(source.shsBaseUrl, source.appId, source.attemptId, { fetchImpl, maxArchiveBytes, idleTimeoutMs })).appModel;
|
|
180
158
|
|
|
181
159
|
const newRunId = randomUUID();
|
|
182
160
|
byRunId.set(newRunId, { appModel, cacheKey, lastAccess: Date.now() });
|
|
@@ -212,6 +190,68 @@ export function diagnoseRun(runId , opts
|
|
|
212
190
|
};
|
|
213
191
|
}
|
|
214
192
|
|
|
193
|
+
const DOCS_CONTENT_DIR = join(dirname(fileURLToPath(import.meta.url)), 'docs-content');
|
|
194
|
+
|
|
195
|
+
// Both vendored corpora lead with their own heading line; strips the leading '#'/'###' markers, an
|
|
196
|
+
// optional `` `TAG`: `` prefix (the detection doc's heading shape), and a trailing `{#anchor}`,
|
|
197
|
+
// leaving just the human title. The tuning doc's plain `# Title` heading passes through as no-ops.
|
|
198
|
+
function extractDocTitle(markdown ) {
|
|
199
|
+
const firstLine = markdown.split('\n', 1)[0] ?? '';
|
|
200
|
+
return firstLine
|
|
201
|
+
.replace(/^#+\s*/, '')
|
|
202
|
+
.replace(/`[A-Z]+`:\s*/, '')
|
|
203
|
+
.replace(/\s*\{#[a-z0-9-]+\}\s*$/, '')
|
|
204
|
+
.trim();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
/** Detection + tuning reference documentation for one finding `type`, independent of any run
|
|
215
|
+
* (documentation is a property of the type: a client fetches it once per type and caches it). */
|
|
216
|
+
export function getFindingDocumentation(type ) {
|
|
217
|
+
const label = FINDING_NAMES[type];
|
|
218
|
+
if (label === undefined) throw mcpError('invalid-type', `Unknown finding type: ${type}`);
|
|
219
|
+
|
|
220
|
+
const tag = typeTag(type);
|
|
221
|
+
const detectionContent = readFileSync(join(DOCS_CONTENT_DIR, 'detection', `${tag.toLowerCase()}.md`), 'utf8');
|
|
222
|
+
const detectionDoc = { tag, title: extractDocTitle(detectionContent), content: detectionContent };
|
|
223
|
+
|
|
224
|
+
const anchor = docAnchorForType(type);
|
|
225
|
+
const slug = anchor ? tuningDocSlugForAnchor(anchor) : null;
|
|
226
|
+
const tuningPath = slug ? join(DOCS_CONTENT_DIR, 'tuning', `${slug}.md`) : null;
|
|
227
|
+
let tuningDoc = null;
|
|
228
|
+
if (tuningPath && anchor && existsSync(tuningPath)) {
|
|
229
|
+
const tuningContent = readFileSync(tuningPath, 'utf8');
|
|
230
|
+
tuningDoc = { anchor, title: extractDocTitle(tuningContent), content: tuningContent };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { type, name: titleCase(label), detectionDoc, tuningDoc };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const CHAPTERS_NAV_FILE = join(DOCS_CONTENT_DIR, 'chapters', 'nav-index.json');
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
/** Full tuning-reference markdown for one doc anchor, run-independent. Resolves the anchor to its
|
|
241
|
+
* owning page via pageForAnchor (so '#metric-task-duration' returns the 'metrics' page), looks it up
|
|
242
|
+
* in the committed nav-index, and reads the markdown from the same docs-content store the website
|
|
243
|
+
* renders from. The general-chapter counterpart to getFindingDocumentation (keyed by finding type). */
|
|
244
|
+
export function getReferenceDoc(anchor ) {
|
|
245
|
+
const page = pageForAnchor(String(anchor).replace(/^#/, ''));
|
|
246
|
+
const nav = JSON.parse(readFileSync(CHAPTERS_NAV_FILE, 'utf8'))
|
|
247
|
+
;
|
|
248
|
+
const entry = nav.find((e) => e.anchor === page);
|
|
249
|
+
if (!entry) throw mcpError('invalid-anchor', `Unknown reference anchor: ${anchor}`);
|
|
250
|
+
const dir = entry.store === 'tuning' ? 'tuning' : 'chapters';
|
|
251
|
+
const content = readFileSync(join(DOCS_CONTENT_DIR, dir, `${entry.slug}.md`), 'utf8');
|
|
252
|
+
return { anchor: page, title: entry.title, content };
|
|
253
|
+
}
|
|
254
|
+
|
|
215
255
|
export function getFindingEvidence(
|
|
216
256
|
runId , findingId , opts ,
|
|
217
257
|
) {
|
|
@@ -230,10 +270,9 @@ export function getRunSummary(runId , opts )
|
|
|
230
270
|
const appModel = getCachedAppModel(runId);
|
|
231
271
|
const { app, stages, jobs, sql, executors } = appModel;
|
|
232
272
|
const durationMs = hasCompleteInterval(app) ? computeWallClock(app, stages).total : null;
|
|
233
|
-
// No buildEvidenceReport call here to redact
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
// matters because app.name is free text and can itself carry a host/IP token.
|
|
273
|
+
// No buildEvidenceReport call here to redact, so reuse redact.ts's app-id + host-token
|
|
274
|
+
// pseudonymization directly. Passing name/sparkVersion (not just id) matters: app.name is free
|
|
275
|
+
// text and can itself carry a host/IP token.
|
|
237
276
|
const rawApp = { id: app?.id ?? null, name: app?.name ?? null, sparkVersion: app?.sparkVersion ?? null };
|
|
238
277
|
const redactedApp = opts?.redact ? redactAppIdentity(rawApp) : rawApp;
|
|
239
278
|
return {
|
|
@@ -248,8 +287,8 @@ export function getRunSummary(runId , opts )
|
|
|
248
287
|
};
|
|
249
288
|
}
|
|
250
289
|
|
|
251
|
-
// Shared by compareRuns and evaluateBudgetsForRun: both need a resolved run's
|
|
252
|
-
//
|
|
290
|
+
// Shared by compareRuns and evaluateBudgetsForRun: both need a resolved run's finding catalog
|
|
291
|
+
// (same analyze() call shape) first.
|
|
253
292
|
async function resolveAndAnalyze(ref ) {
|
|
254
293
|
const { runId, appModel } = await resolveOrCreateRun(ref);
|
|
255
294
|
const catalog = analyze(
|
|
@@ -267,23 +306,16 @@ export async function compareRuns(
|
|
|
267
306
|
{ runId: runIdB, appModel: appModelB, catalog: catalogB },
|
|
268
307
|
] = await Promise.all([resolveAndAnalyze(a), resolveAndAnalyze(b)]);
|
|
269
308
|
|
|
270
|
-
// buildComparison's captureSnapshot
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
// via `analyze()` above) and from `appModel.stages`/`sql` (which already
|
|
275
|
-
// carry plan-tree data), both fully populated regardless. Produces
|
|
276
|
-
// identical comparison output to the dashboard's session-cache snapshots;
|
|
277
|
-
// the MCP tool just never exposes per-task drill-down, so there's nothing
|
|
278
|
-
// to prefetch into it.
|
|
309
|
+
// buildComparison's captureSnapshot uses an empty taskDataCache: that arg only feeds the
|
|
310
|
+
// interactive stage-detail drill-down, which none of compare/matchStages/metricDeltas/findingsDelta
|
|
311
|
+
// read. Findings/metrics come from `catalog` and appModel.stages/sql, both fully populated. Produces
|
|
312
|
+
// identical output to the dashboard; the MCP tool just never exposes per-task drill-down.
|
|
279
313
|
const built = buildComparison(
|
|
280
314
|
{ label: runIdA, appModel: appModelA, catalog: catalogA },
|
|
281
315
|
{ label: runIdB, appModel: appModelB, catalog: catalogB },
|
|
282
316
|
);
|
|
283
|
-
// Stage names throughout `built`
|
|
284
|
-
//
|
|
285
|
-
// a host/IP token as free text — the same residual redactReport() already
|
|
286
|
-
// scrubs from the evidence report.
|
|
317
|
+
// Stage names throughout `built` carry raw Spark stage text, which can embed a host/IP token as
|
|
318
|
+
// free text, the same residual redactReport() already scrubs from the evidence report.
|
|
287
319
|
const result = opts?.redact ? redactComparison(built) : built;
|
|
288
320
|
|
|
289
321
|
return {
|
|
@@ -303,12 +335,9 @@ export async function evaluateBudgetsForRun(
|
|
|
303
335
|
budgets ,
|
|
304
336
|
secondary ,
|
|
305
337
|
) {
|
|
306
|
-
// Mirrors the CLI's
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
// unambiguously means the caller asked for a regression check and forgot
|
|
310
|
-
// the threshold — evaluateBudgets() would otherwise skip the check with no
|
|
311
|
-
// signal at all.
|
|
338
|
+
// Mirrors the CLI's --regression-metric/--max-regression-pct pairing guard: unlike the CLI, this
|
|
339
|
+
// tool never defaults regressionMetric, so seeing it set here means the caller asked for a
|
|
340
|
+
// regression check and forgot the threshold, evaluateBudgets() would otherwise skip it silently.
|
|
312
341
|
if (budgets.regressionMetric !== undefined && budgets.maxRegressionPct === undefined) {
|
|
313
342
|
throw mcpError('access-or-upstream-failure', 'regressionMetric requires maxRegressionPct.');
|
|
314
343
|
}
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
// Worker-message
|
|
13
|
-
//
|
|
14
|
-
// analysis/render/persist stay in the caller via the onDone/onProgress/onError hooks.
|
|
12
|
+
// Worker-message -> appModel assembly. Shared by the file-load and SHS-URL-load paths. Pure model
|
|
13
|
+
// mutation: analysis/render/persist stay in the caller via the onDone/onProgress/onError hooks.
|
|
15
14
|
export function createModelCallbacks(
|
|
16
15
|
appModel ,
|
|
17
16
|
{ onProgress, onDone, onError }
|
|
@@ -28,15 +27,10 @@ export function createModelCallbacks(
|
|
|
28
27
|
appModel.stages.set(stage.id, stage);
|
|
29
28
|
},
|
|
30
29
|
onSql(data ) {
|
|
31
|
-
// Full overwrite, not a merge: safe only because
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// SparkListenerSQLExecutionEnd for the same execution (Spark emits a
|
|
36
|
-
// re-plan mid-run, never after the query finishes); see
|
|
37
|
-
// applyAdaptiveExecutionUpdate in event-handlers.ts. A future producer
|
|
38
|
-
// of 'sql' messages that could arrive after onSqlPlan would need this
|
|
39
|
-
// to merge instead of overwrite.
|
|
30
|
+
// Full overwrite, not a merge: safe only because planTree (set by onSqlPlan from a separate
|
|
31
|
+
// 'sqlPlan' message) is never present on a 'sql' message's data. That relies on
|
|
32
|
+
// SQLAdaptiveExecutionUpdate always preceding SQLExecutionEnd (Spark re-plans mid-run, never
|
|
33
|
+
// after the query finishes). A 'sql' producer that could arrive after onSqlPlan would need to merge.
|
|
40
34
|
const event = data ;
|
|
41
35
|
appModel.sql.set(event.id, data );
|
|
42
36
|
for (const stageId of (event.stageIds ?? [])) {
|
|
@@ -59,10 +53,8 @@ export function createModelCallbacks(
|
|
|
59
53
|
appModel.jobs.set(job.id, job);
|
|
60
54
|
},
|
|
61
55
|
onRunAggregates(data ) { appModel.runAggregates = data ; },
|
|
62
|
-
// Patch per-stage executorMetrics posted once before `done`:
|
|
63
|
-
//
|
|
64
|
-
// arrive after StageCompleted, so the stage message itself carried an
|
|
65
|
-
// empty map). `data` is a Map<stageId, Map<execId, metrics>>.
|
|
56
|
+
// Patch per-stage executorMetrics posted once before `done`: those events arrive after
|
|
57
|
+
// StageCompleted, so the stage message itself carried an empty map. `data` is Map<stageId, Map<execId, metrics>>.
|
|
66
58
|
onStageExecutorMetrics(data ) {
|
|
67
59
|
if (!(data instanceof Map)) return;
|
|
68
60
|
const metricsByStage = data ;
|
package/vendor-core/occupancy.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// parentIds traversal: every stage's wall-clock claim is apportioned purely
|
|
5
|
-
// from its own observed window and how much it overlapped with other
|
|
6
|
-
// stages.
|
|
1
|
+
// Occupancy-weighted wall-clock attribution for per-finding impact: every
|
|
2
|
+
// stage's wall-clock claim is apportioned purely from its own observed window
|
|
3
|
+
// and how much it overlapped with other stages. No graph, no parentIds traversal.
|
|
7
4
|
import { mergeIntervals } from './wall-clock.js';
|
|
8
5
|
|
|
9
6
|
|
|
@@ -18,9 +15,8 @@ function durationMs(s ) {
|
|
|
18
15
|
return (s.completedAt ?? 0) - (s.submittedAt ?? 0);
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
// Average-concurrency proxy, held constant across the stage's
|
|
22
|
-
//
|
|
23
|
-
// better (see the redesign spec's "Explicitly rejected" section).
|
|
18
|
+
// Average-concurrency proxy, held constant across the stage's active window:
|
|
19
|
+
// no per-task timestamps exist outside the parser worker to do better.
|
|
24
20
|
function coreWeight(s ) {
|
|
25
21
|
const d = durationMs(s);
|
|
26
22
|
return d > 0 ? (s.executorRunTime ?? 0) / d : 0;
|
|
@@ -24,29 +24,23 @@ export {
|
|
|
24
24
|
|
|
25
25
|
export { naturalCompare, reassembleRollingEntries } from './rolling-log-reassembly.js';
|
|
26
26
|
|
|
27
|
-
// 512 KB
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// reads give the progress bar more checkpoints to advance through instead.
|
|
27
|
+
// 512 KB, not a larger round number: Spark logs commonly compress ~15-20x, so a
|
|
28
|
+
// coarser chunk decompresses into a burst of tens of thousands of lines at one
|
|
29
|
+
// unmoving pct; smaller reads give the progress bar more checkpoints.
|
|
31
30
|
const CHUNK_SIZE = 512 * 1024;
|
|
32
31
|
const MIN_PROGRESS_STEPS = 100;
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// rarely a mid-chunk boundary: it just meant a handful of progress
|
|
36
|
-
// messages for the whole file, each landing on a different currentPct.
|
|
32
|
+
// Small emit interval: a highly compressed chunk decodes thousands of lines at
|
|
33
|
+
// once, so a large interval yields only a handful of progress messages per file.
|
|
37
34
|
const PROGRESS_EMIT_LINES = 300;
|
|
38
35
|
|
|
39
36
|
|
|
40
37
|
|
|
41
38
|
|
|
42
|
-
// Minimal shape streamFile/runParse/runParseFiles
|
|
43
|
-
//
|
|
44
|
-
// `File`
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// (which has no DOM `File` constructor to build a real one from). Widened
|
|
48
|
-
// from the plan's literal `File` to match that real second caller rather
|
|
49
|
-
// than forcing a cast at its call site.
|
|
39
|
+
// Minimal shape streamFile/runParse/runParseFiles read off `file` (name, size,
|
|
40
|
+
// slice(start,end).arrayBuffer()): narrower than the full DOM `File`. A real
|
|
41
|
+
// `File` (the browser Worker path) satisfies it structurally, but so does the
|
|
42
|
+
// plain object src/cli/collect-run.ts's nodeFileFromPath builds for Node, which
|
|
43
|
+
// has no DOM `File` constructor.
|
|
50
44
|
|
|
51
45
|
|
|
52
46
|
|
|
@@ -60,11 +54,9 @@ const PROGRESS_EMIT_LINES = 300;
|
|
|
60
54
|
|
|
61
55
|
|
|
62
56
|
|
|
63
|
-
// fflate's Gunzip and fzstd's Decompress are untyped vendor JS
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// the two call sites below without touching the vendored files (mirrors
|
|
67
|
-
// shs-fetch.ts's identical StreamingDecoder shim).
|
|
57
|
+
// fflate's Gunzip and fzstd's Decompress are untyped vendor JS, so TS can't
|
|
58
|
+
// infer a construct signature for them; this local shape types the call sites
|
|
59
|
+
// without touching the vendored files (mirrors shs-fetch.ts's shim).
|
|
68
60
|
|
|
69
61
|
|
|
70
62
|
|
|
@@ -73,7 +65,7 @@ const PROGRESS_EMIT_LINES = 300;
|
|
|
73
65
|
// it becomes available. Shared by runParse (single file) and runParseFiles
|
|
74
66
|
// (rolling multi-file directories): the codec is sniffed per-file since
|
|
75
67
|
// Spark's file-rolling never spans a compressor's own framing.
|
|
76
|
-
async function streamFile(
|
|
68
|
+
export async function streamFile(
|
|
77
69
|
file ,
|
|
78
70
|
onChunk ,
|
|
79
71
|
chunkSize ,
|
|
@@ -116,11 +108,10 @@ async function streamFile(
|
|
|
116
108
|
}
|
|
117
109
|
|
|
118
110
|
// Parse a dropped `File`, always streaming to honor the core invariant: the
|
|
119
|
-
// decompressed task-event stream must never be buffered whole.
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// `chunkSize` is injectable purely so tests can force multi-chunk streaming.
|
|
111
|
+
// decompressed task-event stream must never be buffered whole. Read in
|
|
112
|
+
// `chunkSize` slices; a compressed log is inflated incrementally so only one
|
|
113
|
+
// decompressed chunk is live at a time, like the uncompressed path.
|
|
114
|
+
// `chunkSize` is injectable so tests can force multi-chunk streaming.
|
|
124
115
|
export async function runParse(
|
|
125
116
|
file ,
|
|
126
117
|
state ,
|
package/vendor-core/plan-dot.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
// Graphviz DOT export of a resolved planTree
|
|
2
|
-
// Plain string building, no dependency, no metric annotation yet (deferred,
|
|
3
|
-
// matching sparkDoctor's own unimplemented roadmap item).
|
|
1
|
+
// Graphviz DOT export of a resolved planTree. No metric annotation.
|
|
4
2
|
|
|
5
3
|
import { walkPlanTree } from './plan-tree-walk.js';
|
|
6
4
|
|
|
@@ -17,8 +15,7 @@ export function planTreeToDot(planTree , { title = 'plan' }
|
|
|
17
15
|
idOf.set(node, id);
|
|
18
16
|
const label = node.detail && node.detail !== node.name ? `${node.name}\\n${node.detail}` : node.name;
|
|
19
17
|
lines.push(` ${id} [label="${esc(label)}"];`);
|
|
20
|
-
// Pre-order
|
|
21
|
-
// child is visited, so a single walk covers both node labels and edges.
|
|
18
|
+
// Pre-order: parent's id is assigned before any child, so one walk covers labels and edges.
|
|
22
19
|
if (parent) edges.push(` ${idOf.get(parent)} -> ${id};`);
|
|
23
20
|
}, { dedupe: true });
|
|
24
21
|
return [...lines, ...edges, '}'].join('\n');
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
// Approximate stage wall-time back to plan operators, splitting the tree at
|
|
2
2
|
// Exchange boundaries into segments and zipping segments (deepest-first) to
|
|
3
3
|
// submission-ordered stage IDs. No exact ground truth exists in Spark's event
|
|
4
|
-
// model: this is inference
|
|
4
|
+
// model: this is inference.
|
|
5
5
|
import { walkPlanTree } from './plan-tree-walk.js';
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
function isExchange(node ) { return /exchange/i.test(node.name); }
|
|
11
|
-
|
|
12
10
|
function timingMs(node ) {
|
|
13
11
|
for (const m of (node.metrics ?? [])) {
|
|
14
12
|
if (m.metricType === 'timing') return m.value;
|
|
@@ -42,7 +40,7 @@ export function computeSegments(planTree )
|
|
|
42
40
|
if (!parent) {
|
|
43
41
|
seg = 0;
|
|
44
42
|
segmentTopology.set(seg, { parentIndex: null, depth: 0, traversalOrder: nextTraversalOrder++ });
|
|
45
|
-
} else if (
|
|
43
|
+
} else if (parent.exchangeRole === 'read') {
|
|
46
44
|
const parentIndex = segOf.get(parent) ;
|
|
47
45
|
seg = nextSegment++;
|
|
48
46
|
segmentTopology.set(seg, {
|
|
@@ -116,20 +114,16 @@ function componentDistance(fromIndex , toIndex , segmentTopology
|
|
|
116
114
|
return Number.POSITIVE_INFINITY;
|
|
117
115
|
}
|
|
118
116
|
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// labels don't drift through a chain of inherited-from-inherited guesses.
|
|
130
|
-
// zipSegmentsToStages/attributeStageDurationToPlan are untouched by this and
|
|
131
|
-
// keep their own strict pairing: duration attribution must never double-
|
|
132
|
-
// count one stage's wall time across two segments.
|
|
117
|
+
// Display-only: the "Stage N" label on a segment's group box. A segment past
|
|
118
|
+
// zipSegmentsToStages' Math.min cutoff has no timing to pair on, but the plan
|
|
119
|
+
// still places it structurally near a component that won a stage slot (often a
|
|
120
|
+
// broadcast-builder just upstream of the join consuming it). Inherit the
|
|
121
|
+
// nearest strictly-paired component's stage id rather than showing "Stage —".
|
|
122
|
+
// Distance counts parent/child edges in the component tree, not numeric id
|
|
123
|
+
// distance. Neighbor search is always against the ORIGINAL strict pairs, not
|
|
124
|
+
// other filled components, so labels don't drift through inherited-from-
|
|
125
|
+
// inherited guesses. Duration attribution keeps its own strict pairing and
|
|
126
|
+
// must never double-count one stage's wall time across two segments.
|
|
133
127
|
export function mapSegmentsToStagesForDisplay(
|
|
134
128
|
segments ,
|
|
135
129
|
stagesById ,
|
|
@@ -156,19 +150,10 @@ export function mapSegmentsToStagesForDisplay(
|
|
|
156
150
|
return map;
|
|
157
151
|
}
|
|
158
152
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
stagesById ,
|
|
162
|
-
sqlExec ,
|
|
153
|
+
function computeExclusiveSharesForPairs(
|
|
154
|
+
pairs ,
|
|
163
155
|
) {
|
|
164
156
|
const out = new Map ();
|
|
165
|
-
const stageIds = sqlExec?.stageIds ?? [];
|
|
166
|
-
if (!planTree || stageIds.length === 0) return out;
|
|
167
|
-
|
|
168
|
-
const { segments, segmentTopology } = computeSegments(planTree);
|
|
169
|
-
const typedStagesById = stagesById ;
|
|
170
|
-
const { pairs } = zipSegmentsToStages(segments, typedStagesById, stageIds, segmentTopology);
|
|
171
|
-
|
|
172
157
|
for (const { nodes, stage } of pairs) {
|
|
173
158
|
const { submittedAt, completedAt } = stage ;
|
|
174
159
|
const wall = Math.max(0, completedAt - submittedAt);
|
|
@@ -183,3 +168,67 @@ export function attributeStageDurationToPlan(
|
|
|
183
168
|
}
|
|
184
169
|
return out;
|
|
185
170
|
}
|
|
171
|
+
|
|
172
|
+
export function attributeStageDurationToPlan(
|
|
173
|
+
planTree ,
|
|
174
|
+
stagesById ,
|
|
175
|
+
sqlExec ,
|
|
176
|
+
) {
|
|
177
|
+
const stageIds = sqlExec?.stageIds ?? [];
|
|
178
|
+
if (!planTree || stageIds.length === 0) return new Map();
|
|
179
|
+
const { segments, segmentTopology } = computeSegments(planTree);
|
|
180
|
+
const typedStagesById = stagesById ;
|
|
181
|
+
const { pairs } = zipSegmentsToStages(segments, typedStagesById, stageIds, segmentTopology);
|
|
182
|
+
return computeExclusiveSharesForPairs(pairs);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Exclusive share plus every descendant present in the same segment's node
|
|
186
|
+
// list. "Same segment" is what keeps this from leaking across an Exchange
|
|
187
|
+
// boundary: computeSegments already cuts a new segment at each write half,
|
|
188
|
+
// so a node's descendants outside its own segment are simply not in
|
|
189
|
+
// `inSegment` and get excluded by construction.
|
|
190
|
+
export function attributeStageDurationToPlanInclusive(
|
|
191
|
+
planTree ,
|
|
192
|
+
stagesById ,
|
|
193
|
+
sqlExec ,
|
|
194
|
+
) {
|
|
195
|
+
const stageIds = sqlExec?.stageIds ?? [];
|
|
196
|
+
const out = new Map ();
|
|
197
|
+
if (!planTree || stageIds.length === 0) return out;
|
|
198
|
+
const { segments, segmentTopology } = computeSegments(planTree);
|
|
199
|
+
const typedStagesById = stagesById ;
|
|
200
|
+
const { pairs } = zipSegmentsToStages(segments, typedStagesById, stageIds, segmentTopology);
|
|
201
|
+
const exclusive = computeExclusiveSharesForPairs(pairs);
|
|
202
|
+
|
|
203
|
+
for (const { nodes } of pairs) {
|
|
204
|
+
const inSegment = new Set(nodes);
|
|
205
|
+
for (const node of nodes) {
|
|
206
|
+
// `visited` is fresh per top-level node, not shared across the `nodes`
|
|
207
|
+
// loop: a diamond-shared descendant's value, when computed while
|
|
208
|
+
// suppressed by ANOTHER node's own traversal (to avoid double-counting
|
|
209
|
+
// within THAT node's rollup), is not valid to reuse for the shared
|
|
210
|
+
// descendant's own independent top-level entry, those are different
|
|
211
|
+
// questions ("what does this contribute to node X's total" vs. "what
|
|
212
|
+
// is this node's own total in isolation"). This also rules out a
|
|
213
|
+
// node->total memo shared across top-level calls: a cached total
|
|
214
|
+
// computed unsuppressed (as its own top-level entry) would, if reused
|
|
215
|
+
// while this node is a descendant of a DIFFERENT ancestor being
|
|
216
|
+
// suppressed for double-counting, smuggle a shared descendant's share
|
|
217
|
+
// back in twice - see the diamond-shape regression test. Losing
|
|
218
|
+
// memoization across different top-level calls is fine: these trees
|
|
219
|
+
// are small and this was never a performance-critical path.
|
|
220
|
+
const visited = new Set ();
|
|
221
|
+
function inclusiveOf(n ) {
|
|
222
|
+
if (visited.has(n)) return 0;
|
|
223
|
+
visited.add(n);
|
|
224
|
+
let total = exclusive.get(n) ?? 0;
|
|
225
|
+
for (const child of n.children ?? []) {
|
|
226
|
+
if (inSegment.has(child)) total += inclusiveOf(child);
|
|
227
|
+
}
|
|
228
|
+
return total;
|
|
229
|
+
}
|
|
230
|
+
out.set(node, inclusiveOf(node));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|