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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { resolve as resolvePath, join } from 'node:path';
|
|
3
|
+
import { nodeLazyPeekableFromPath, isRollingLogDirectory } from './cli/collect-run.js';
|
|
4
|
+
import { reassembleRollingEntries } from './parser-worker.js';
|
|
5
|
+
import { peekLogHeader } from './log-header-peek.js';
|
|
6
|
+
import { mcpError } from './mcp-error.js';
|
|
7
|
+
import { normalizeBaseUrl } from './shs-request.js';
|
|
8
|
+
import { DEFAULT_IDLE_TIMEOUT_MS, DEFAULT_MAX_ARCHIVE_BYTES } from './shs-load.js';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// Local-mode-only convention: a plain file named `<name>_<n>.<ext>` is one attempt of a
|
|
33
|
+
// multi-attempt run (e.g. `external-local-1430917381535_1.ndjson`), the `_<n>` disambiguating two
|
|
34
|
+
// otherwise-identical appIds. Deliberately anchored on a file extension so a rolling-log
|
|
35
|
+
// directory's own name (no extension) never matches this convention.
|
|
36
|
+
const LOCAL_ATTEMPT_SUFFIX_RE = /^.+_(\d+)\.[^./\\]+$/;
|
|
37
|
+
|
|
38
|
+
const DEFAULT_MAX_RESULTS = 100;
|
|
39
|
+
|
|
40
|
+
// SHS's own timestamp format ends in a literal "GMT" suffix instead of "Z"/an offset, which
|
|
41
|
+
// Date.parse cannot parse (returns NaN), normalize before storing, so minDate/maxDate filtering
|
|
42
|
+
// (which relies on Date.parse) actually works, and so RunListEntry.startTime has one parseable
|
|
43
|
+
// ISO format across both local and SHS mode.
|
|
44
|
+
function normalizeShsTimestamp(raw ) {
|
|
45
|
+
if (!Number.isNaN(Date.parse(raw))) return new Date(raw).toISOString();
|
|
46
|
+
if (raw.endsWith('GMT')) {
|
|
47
|
+
const iso = `${raw.slice(0, -3)}Z`;
|
|
48
|
+
if (!Number.isNaN(Date.parse(iso))) return new Date(iso).toISOString();
|
|
49
|
+
}
|
|
50
|
+
return undefined; // genuinely unparseable: omit rather than propagate garbage
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Shared by local and SHS mode: both mapping steps produce a flat RunListEntry[] before this runs.
|
|
54
|
+
export function applyFiltersAndCap(entries , filters ) {
|
|
55
|
+
// Date.parse of an unparseable string returns NaN, and every comparison against NaN is false,
|
|
56
|
+
// left unchecked, a typo'd minDate/maxDate would silently filter out every run rather than
|
|
57
|
+
// reporting the mistake, indistinguishable from a correctly-filtered "no runs matched" result.
|
|
58
|
+
const min = filters.minDate ? Date.parse(filters.minDate) : undefined;
|
|
59
|
+
if (min != null && Number.isNaN(min)) {
|
|
60
|
+
throw mcpError('invalid-date-filter', `minDate is not a parseable date: ${filters.minDate}`);
|
|
61
|
+
}
|
|
62
|
+
const max = filters.maxDate ? Date.parse(filters.maxDate) : undefined;
|
|
63
|
+
if (max != null && Number.isNaN(max)) {
|
|
64
|
+
throw mcpError('invalid-date-filter', `maxDate is not a parseable date: ${filters.maxDate}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let filtered = entries;
|
|
68
|
+
if (filters.namePattern) {
|
|
69
|
+
const needle = filters.namePattern.toLowerCase();
|
|
70
|
+
filtered = filtered.filter((e) => e.name.toLowerCase().includes(needle));
|
|
71
|
+
}
|
|
72
|
+
if (min != null) {
|
|
73
|
+
filtered = filtered.filter((e) => e.startTime != null && Date.parse(e.startTime) >= min);
|
|
74
|
+
}
|
|
75
|
+
if (max != null) {
|
|
76
|
+
filtered = filtered.filter((e) => e.startTime != null && Date.parse(e.startTime) <= max);
|
|
77
|
+
}
|
|
78
|
+
const maxResults = filters.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
79
|
+
const capped = filtered.length <= maxResults
|
|
80
|
+
? { runs: filtered, truncated: false }
|
|
81
|
+
: { runs: filtered.slice(0, maxResults), truncated: true };
|
|
82
|
+
return filters.redact ? { ...capped, runs: redactRunListEntries(capped.runs) } : capped;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Local counterpart to redact.ts's redactAppIdentity, sized for a listing of many distinct apps
|
|
86
|
+
// rather than the single app that tool works over: redactAppIdentity has no way to keep two
|
|
87
|
+
// entries sharing an appId in sync, so this builds its own stable per-distinct-appId pseudonym map
|
|
88
|
+
// (numeric-aware sort, same scheme as redact.ts's buildMap) across the whole result set, two
|
|
89
|
+
// attempts of the same app redact to the same identity, and applies it to every identity-bearing
|
|
90
|
+
// field: appId, name (a listing's human-readable name is as identifying as the id itself), and the
|
|
91
|
+
// source's own path/appId.
|
|
92
|
+
function redactRunListEntries(entries ) {
|
|
93
|
+
const appIds = new Set ();
|
|
94
|
+
for (const e of entries) appIds.add(e.appId);
|
|
95
|
+
const sorted = [...appIds].sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
|
|
96
|
+
const pseudonyms = new Map(sorted.map((id, i) => [id, `app-${i + 1}`]));
|
|
97
|
+
return entries.map((e) => {
|
|
98
|
+
const pseudonym = pseudonyms.get(e.appId) ?? e.appId;
|
|
99
|
+
const source = 'path' in e.source
|
|
100
|
+
? { ...e.source, path: pseudonym }
|
|
101
|
+
: { ...e.source, appId: pseudonym };
|
|
102
|
+
return { ...e, appId: pseudonym, name: pseudonym, source };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function listRunsLocal(input ) {
|
|
107
|
+
const dir = resolvePath(input.dir);
|
|
108
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
109
|
+
throw mcpError('directory-not-found', `No such directory: ${dir}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let names ;
|
|
113
|
+
try {
|
|
114
|
+
names = readdirSync(dir);
|
|
115
|
+
} catch {
|
|
116
|
+
throw mcpError('directory-not-found', `Could not read directory: ${dir}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// `path` is the entry's own source-worthy location (a file, or a rolling-log dir); `peekPath` is
|
|
120
|
+
// the single file peekLogHeader actually reads (the rolling dir's earliest segment). `attemptId`
|
|
121
|
+
// is only ever set for the plain-file case (see LOCAL_ATTEMPT_SUFFIX_RE), a rolling-log
|
|
122
|
+
// directory's name has no file extension, so it never matches the convention.
|
|
123
|
+
const candidates = [];
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
const full = join(dir, name);
|
|
126
|
+
let st;
|
|
127
|
+
try {
|
|
128
|
+
st = statSync(full);
|
|
129
|
+
} catch {
|
|
130
|
+
continue; // vanished between readdir and stat: skip
|
|
131
|
+
}
|
|
132
|
+
if (st.isFile()) {
|
|
133
|
+
const attemptMatch = name.match(LOCAL_ATTEMPT_SUFFIX_RE);
|
|
134
|
+
candidates.push({ path: full, peekPath: full, ...(attemptMatch ? { attemptId: attemptMatch[1] } : {}) });
|
|
135
|
+
} else if (st.isDirectory()) {
|
|
136
|
+
let isRolling = false;
|
|
137
|
+
try {
|
|
138
|
+
isRolling = isRollingLogDirectory(full);
|
|
139
|
+
} catch {
|
|
140
|
+
continue; // permission denied or other error reading the directory: skip this candidate
|
|
141
|
+
}
|
|
142
|
+
if (!isRolling) continue; // not a rolling log directory: skip
|
|
143
|
+
let subNames ;
|
|
144
|
+
try {
|
|
145
|
+
subNames = readdirSync(full);
|
|
146
|
+
} catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
let ordered ;
|
|
150
|
+
try {
|
|
151
|
+
ordered = reassembleRollingEntries(subNames);
|
|
152
|
+
} catch {
|
|
153
|
+
continue; // malformed rolling dir (index gap, etc.): skip like any unpeekable candidate
|
|
154
|
+
}
|
|
155
|
+
if (ordered.length === 0) continue;
|
|
156
|
+
candidates.push({ path: full, peekPath: join(full, ordered[0]) });
|
|
157
|
+
}
|
|
158
|
+
// Anything else (a plain non-rolling subdirectory, a socket, ...): not a candidate, skip.
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const entries = [];
|
|
162
|
+
for (const c of candidates) {
|
|
163
|
+
let peeked;
|
|
164
|
+
try {
|
|
165
|
+
peeked = await peekLogHeader(nodeLazyPeekableFromPath(c.peekPath));
|
|
166
|
+
} catch {
|
|
167
|
+
continue; // corrupt/unreadable candidate: one bad file shouldn't fail the whole listing
|
|
168
|
+
}
|
|
169
|
+
if (!peeked || !peeked.appId) continue; // no usable appId: not enough to hand back as a candidate
|
|
170
|
+
entries.push({
|
|
171
|
+
appId: peeked.appId,
|
|
172
|
+
name: peeked.name ?? '',
|
|
173
|
+
...(peeked.sparkVersion ? { sparkVersion: peeked.sparkVersion } : {}),
|
|
174
|
+
...(peeked.startTimeMs != null ? { startTime: new Date(peeked.startTimeMs).toISOString() } : {}),
|
|
175
|
+
source: { path: c.path, ...(c.attemptId ? { attemptId: c.attemptId } : {}) },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// readdir order is arbitrary, and applyFiltersAndCap keeps the first maxResults, without a sort
|
|
180
|
+
// a directory of thousands of logs would hand back an arbitrary 100 rather than the newest 100.
|
|
181
|
+
// Newest-first also matches SHS mode, which inherits that order from SHS's own attempt ordering.
|
|
182
|
+
entries.sort((a, b) => {
|
|
183
|
+
if (a.startTime == null && b.startTime == null) return 0;
|
|
184
|
+
if (a.startTime == null) return 1; // undated entries sort last, after every dated one
|
|
185
|
+
if (b.startTime == null) return -1;
|
|
186
|
+
return Date.parse(b.startTime) - Date.parse(a.startTime);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return applyFiltersAndCap(entries, input);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function listRunsShs(
|
|
193
|
+
input ,
|
|
194
|
+
opts ,
|
|
195
|
+
) {
|
|
196
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
197
|
+
const normalized = normalizeBaseUrl(input.shsBaseUrl);
|
|
198
|
+
if (!normalized) {
|
|
199
|
+
throw mcpError('invalid-shs-base-url', 'Enter an absolute HTTP(S) base URL without credentials, query, or fragment.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const url = new URL('api/v1/applications', normalized);
|
|
203
|
+
if (input.minDate) url.searchParams.set('minDate', input.minDate);
|
|
204
|
+
if (input.maxDate) url.searchParams.set('maxDate', input.maxDate);
|
|
205
|
+
|
|
206
|
+
let res ;
|
|
207
|
+
try {
|
|
208
|
+
// An unresponsive SHS would otherwise hang the tool call forever. A timed-out signal rejects
|
|
209
|
+
// the fetch with an AbortError, which this same catch turns into access-or-upstream-failure.
|
|
210
|
+
res = await fetchImpl(url.toString(), { signal: AbortSignal.timeout(DEFAULT_IDLE_TIMEOUT_MS) });
|
|
211
|
+
} catch (e) {
|
|
212
|
+
throw mcpError('access-or-upstream-failure', `Could not reach ${normalized}: ${e instanceof Error ? e.message : String(e)}`);
|
|
213
|
+
}
|
|
214
|
+
if (!res.ok) {
|
|
215
|
+
throw mcpError('access-or-upstream-failure', `SHS applications list request failed with status ${res.status}.`);
|
|
216
|
+
}
|
|
217
|
+
// Best-effort size guard: this endpoint returns a modest JSON listing, so a declared length over
|
|
218
|
+
// the archive cap means something is wrong. Absent (e.g. chunked) header: skip the check rather
|
|
219
|
+
// than pull in a streaming reader for a listing.
|
|
220
|
+
const declaredLength = Number.parseInt(res.headers.get('content-length') ?? '', 10);
|
|
221
|
+
if (Number.isFinite(declaredLength) && declaredLength > DEFAULT_MAX_ARCHIVE_BYTES) {
|
|
222
|
+
throw mcpError('access-or-upstream-failure', 'SHS applications list response exceeds the byte cap.');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let payload ;
|
|
226
|
+
try {
|
|
227
|
+
payload = await res.json();
|
|
228
|
+
} catch {
|
|
229
|
+
throw mcpError('access-or-upstream-failure', 'SHS applications list response was not valid JSON.');
|
|
230
|
+
}
|
|
231
|
+
if (!Array.isArray(payload)) {
|
|
232
|
+
throw mcpError('access-or-upstream-failure', 'SHS applications list response was not an array.');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const entries = [];
|
|
236
|
+
for (const app of payload ) {
|
|
237
|
+
if (!app || typeof app !== 'object') continue;
|
|
238
|
+
const id = typeof app.id === 'string' ? app.id : null;
|
|
239
|
+
if (!id) continue;
|
|
240
|
+
const name = typeof app.name === 'string' ? app.name : '';
|
|
241
|
+
const attempts = Array.isArray(app.attempts) ? app.attempts : [];
|
|
242
|
+
const attempt = attempts[0]; // SHS's own ordering: most recent attempt first
|
|
243
|
+
if (!attempt) continue;
|
|
244
|
+
const sparkVersion = typeof attempt.appSparkVersion === 'string' ? attempt.appSparkVersion : undefined;
|
|
245
|
+
const startTime = typeof attempt.startTime === 'string' ? normalizeShsTimestamp(attempt.startTime) : undefined;
|
|
246
|
+
const durationMs = typeof attempt.duration === 'number' ? attempt.duration : undefined;
|
|
247
|
+
const attemptId = typeof attempt.attemptId === 'string' ? attempt.attemptId : undefined;
|
|
248
|
+
entries.push({
|
|
249
|
+
appId: id,
|
|
250
|
+
name,
|
|
251
|
+
...(sparkVersion ? { sparkVersion } : {}),
|
|
252
|
+
...(startTime ? { startTime } : {}),
|
|
253
|
+
...(durationMs != null ? { durationMs } : {}),
|
|
254
|
+
source: { shsBaseUrl: normalized, appId: id, ...(attemptId ? { attemptId } : {}) },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return applyFiltersAndCap(entries, input);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function listRuns(
|
|
262
|
+
input ,
|
|
263
|
+
opts ,
|
|
264
|
+
) {
|
|
265
|
+
if (input.dir) {
|
|
266
|
+
return listRunsLocal({
|
|
267
|
+
dir: input.dir, namePattern: input.namePattern, minDate: input.minDate, maxDate: input.maxDate, maxResults: input.maxResults, redact: input.redact,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (input.shsBaseUrl) {
|
|
271
|
+
return listRunsShs(
|
|
272
|
+
{ shsBaseUrl: input.shsBaseUrl, namePattern: input.namePattern, minDate: input.minDate, maxDate: input.maxDate, maxResults: input.maxResults, redact: input.redact },
|
|
273
|
+
opts,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
throw mcpError('access-or-upstream-failure', 'Provide either dir or shsBaseUrl.');
|
|
277
|
+
}
|
|
278
|
+
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
// Plain JS
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// core
|
|
5
|
-
// entry point bootstraps by locating *this* file first (a tiny fixed-name
|
|
6
|
-
// existsSync/join check, unavoidably duplicated per entry point since
|
|
7
|
-
// nothing can resolve it for them), then uses the exports below for every
|
|
8
|
-
// other core module, so the actual resolution logic lives in exactly one
|
|
9
|
-
// place instead of being re-implemented per entry point.
|
|
1
|
+
// Plain JS: scripts/vendor-core.mjs copies it byte-for-byte into vendor-core/, so this file exists
|
|
2
|
+
// at both vendor-core/load-vendored.js (published) and core/src/load-vendored.js (dev). Each
|
|
3
|
+
// package's bin bootstraps by locating this file first, then uses the exports below for every other
|
|
4
|
+
// core module, so the resolution logic lives in one place instead of per entry point.
|
|
10
5
|
import { existsSync } from 'node:fs';
|
|
11
6
|
import { join } from 'node:path';
|
|
12
7
|
import { pathToFileURL } from 'node:url';
|
|
13
8
|
|
|
14
|
-
// moduleName is a path relative to core/src without extension, e.g.
|
|
15
|
-
// '
|
|
16
|
-
// already plain JS in core/src (e.g. 'proxy') rather than TypeScript.
|
|
9
|
+
// moduleName is a path relative to core/src without extension, e.g. 'cli/collect-run'. Pass
|
|
10
|
+
// srcExt: 'js' for modules already plain JS in core/src (e.g. 'proxy') rather than TypeScript.
|
|
17
11
|
export function resolveVendored(pkgDir, moduleName, { srcExt = 'ts' } = {}) {
|
|
18
12
|
const vendored = join(pkgDir, 'vendor-core', `${moduleName}.js`);
|
|
19
13
|
return existsSync(vendored) ? vendored : join(pkgDir, '..', 'core', 'src', `${moduleName}.${srcExt}`);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { streamFile } from './parser-worker.js';
|
|
2
|
+
import { buildChunkDecoder } from './event-handlers.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// list_runs peeks every candidate file in a shared directory: bound the work per file so one
|
|
18
|
+
// huge or malformed log can't blow out a directory scan. 50 lines is comfortably past where
|
|
19
|
+
// LogStart/ApplicationStart appear in a real log (they're always among the first few events).
|
|
20
|
+
const MAX_PEEK_LINES = 50;
|
|
21
|
+
// Small on purpose: unlike the full parse path (CHUNK_SIZE = 512KB, tuned for progress-bar
|
|
22
|
+
// granularity on multi-GB files), a peek wants the *first* read to already be enough: reading in
|
|
23
|
+
// 64KB steps means the 50-line cap trips after very little decompression work, not after one
|
|
24
|
+
// giant chunk decodes thousands of lines at once.
|
|
25
|
+
const PEEK_CHUNK_BYTES = 64 * 1024;
|
|
26
|
+
|
|
27
|
+
// Thrown from inside the streamFile->decoder->onChunk callback chain to unwind out of the
|
|
28
|
+
// (possibly still-looping) decompression stream the instant enough header lines are seen: the
|
|
29
|
+
// rest of the file is never requested from `file.slice()` after this.
|
|
30
|
+
class PeekComplete extends Error {}
|
|
31
|
+
|
|
32
|
+
export async function peekLogHeader(file ) {
|
|
33
|
+
if (file.size === 0) return null;
|
|
34
|
+
|
|
35
|
+
const decoder = buildChunkDecoder();
|
|
36
|
+
let linesRead = 0;
|
|
37
|
+
let sawLogStart = false;
|
|
38
|
+
let sawAppStart = false;
|
|
39
|
+
const result = { appId: null, name: null, sparkVersion: null, startTimeMs: null };
|
|
40
|
+
|
|
41
|
+
const processLine = (line ) => {
|
|
42
|
+
linesRead++;
|
|
43
|
+
let event = null;
|
|
44
|
+
try {
|
|
45
|
+
event = JSON.parse(line);
|
|
46
|
+
} catch {
|
|
47
|
+
// Corrupt/partial line: same silent-skip as dispatchLine's state.skippedLines++ path.
|
|
48
|
+
}
|
|
49
|
+
if (event) {
|
|
50
|
+
if (event.Event === 'SparkListenerLogStart') {
|
|
51
|
+
if (typeof event['Spark Version'] === 'string') result.sparkVersion = event['Spark Version'];
|
|
52
|
+
sawLogStart = true;
|
|
53
|
+
} else if (event.Event === 'SparkListenerApplicationStart') {
|
|
54
|
+
if (typeof event['App ID'] === 'string') result.appId = event['App ID'];
|
|
55
|
+
if (typeof event['App Name'] === 'string') result.name = event['App Name'];
|
|
56
|
+
if (typeof event['Timestamp'] === 'number') result.startTimeMs = event['Timestamp'];
|
|
57
|
+
// ApplicationStart's own Spark Version is only a fallback: a preceding LogStart always
|
|
58
|
+
// wins, mirroring event-handlers.ts's startApplication precedence
|
|
59
|
+
// (state.pendingSparkVersion ?? event['Spark Version']).
|
|
60
|
+
if (!sawLogStart && typeof event['Spark Version'] === 'string') result.sparkVersion = event['Spark Version'];
|
|
61
|
+
sawAppStart = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if ((sawLogStart && sawAppStart) || linesRead >= MAX_PEEK_LINES) throw new PeekComplete();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const feed = (bytes ) => {
|
|
68
|
+
for (const line of decoder.decode(bytes)) processLine(line);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await streamFile(file, feed, PEEK_CHUNK_BYTES);
|
|
73
|
+
// A trailing header line with no terminating newline sits in the decoder's pending buffer
|
|
74
|
+
// until flushed, same as parser-worker.ts's runParse/runParseFiles do after their streamFile call.
|
|
75
|
+
for (const line of decoder.flush()) processLine(line);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (!(e instanceof PeekComplete)) return null; // unsupported/corrupt codec: same silent-skip as any unpeekable candidate
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return sawAppStart ? result : null;
|
|
81
|
+
}
|
package/vendor-core/lz4-block.js
CHANGED
|
@@ -35,12 +35,10 @@ function decompressLz4Sequence(input , outSize ) {
|
|
|
35
35
|
return out;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Streaming counterpart to decodeLz4Block: push
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// `onChunk` must consume its argument synchronously (it aliases the internal
|
|
43
|
-
// buffer for RAW blocks and is not retained across the next push).
|
|
38
|
+
// Streaming counterpart to decodeLz4Block: push byte slices, each complete LZ4Block block is
|
|
39
|
+
// decompressed and handed to `onChunk` as it completes, so output is never fully buffered. Partial
|
|
40
|
+
// trailing bytes are retained until the next push. `onChunk` must consume synchronously (it aliases
|
|
41
|
+
// the internal buffer for RAW blocks, not retained across the next push).
|
|
44
42
|
export function createLz4BlockDecoder(
|
|
45
43
|
onChunk ,
|
|
46
44
|
) {
|
|
@@ -2,8 +2,9 @@ import { z } from 'zod';
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
resolveOrCreateRun, diagnoseRun, getRunSummary, compareRuns, getFindingEvidence, evaluateBudgetsForRun,
|
|
5
|
+
resolveOrCreateRun, diagnoseRun, getRunSummary, compareRuns, getFindingEvidence, getFindingDocumentation, getReferenceDoc, evaluateBudgetsForRun,
|
|
6
6
|
} from './mcp-tools.js';
|
|
7
|
+
import { listRuns } from './list-runs.js';
|
|
7
8
|
|
|
8
9
|
const sourceSchema = z.union([
|
|
9
10
|
z.object({ path: z.string() }),
|
|
@@ -14,6 +15,19 @@ const runRefSchema = { source: sourceSchema.optional(), runId: z.string().option
|
|
|
14
15
|
const secondRunRefSchema = { sourceB: sourceSchema.optional(), runIdB: z.string().optional() };
|
|
15
16
|
const formatSchema = { format: z.enum(['json', 'md']).optional() };
|
|
16
17
|
|
|
18
|
+
// list_runs takes a bare shsBaseUrl (no appId/attemptId, unlike sourceSchema's SHS branch, since
|
|
19
|
+
// it's listing candidates rather than resolving one), so it can't reuse sourceSchema: refine
|
|
20
|
+
// against both dir and shsBaseUrl being set instead, so one doesn't silently win over the other.
|
|
21
|
+
const listRunsInputSchema = z.object({
|
|
22
|
+
dir: z.string().optional(),
|
|
23
|
+
shsBaseUrl: z.string().optional(),
|
|
24
|
+
namePattern: z.string().optional(),
|
|
25
|
+
minDate: z.string().optional(),
|
|
26
|
+
maxDate: z.string().optional(),
|
|
27
|
+
maxResults: z.number().int().positive().optional(),
|
|
28
|
+
redact: z.boolean().optional(),
|
|
29
|
+
}).refine((v) => !(v.dir && v.shsBaseUrl), { message: 'Provide only one of dir or shsBaseUrl, not both.' });
|
|
30
|
+
|
|
17
31
|
function toCallToolResult(text , structuredContent ) {
|
|
18
32
|
return { content: [{ type: 'text' , text }], structuredContent };
|
|
19
33
|
}
|
|
@@ -26,11 +40,9 @@ function toolErrorResult(error )
|
|
|
26
40
|
};
|
|
27
41
|
}
|
|
28
42
|
|
|
29
|
-
// diagnoseRun/compareRuns may
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// it's stripped here, and — when present — becomes content[0].text instead
|
|
33
|
-
// of the usual JSON.stringify.
|
|
43
|
+
// diagnoseRun/compareRuns may carry a `markdown` field (only when the caller asked for
|
|
44
|
+
// format: 'md'); it never belongs in structuredContent (whose shape must stay stable regardless of
|
|
45
|
+
// format), so it's stripped here and, when present, becomes content[0].text instead of JSON.stringify.
|
|
34
46
|
function toolResultWithMarkdown (promise ) {
|
|
35
47
|
return promise.then(
|
|
36
48
|
(value) => {
|
|
@@ -41,8 +53,7 @@ function toolResultWithMarkdown (promise
|
|
|
41
53
|
);
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
// Counterpart for the
|
|
45
|
-
// mapping (not a delegation through toolResultWithMarkdown), so there's no
|
|
56
|
+
// Counterpart for the tools that never produce a markdown field: its own mapping, so there's no
|
|
46
57
|
// need to cast T to pretend it might carry a `markdown` field it never does.
|
|
47
58
|
function toolResult (promise ) {
|
|
48
59
|
return promise.then((value) => toCallToolResult(JSON.stringify(value), value ), toolErrorResult);
|
|
@@ -51,6 +62,11 @@ function toolResult (promise )
|
|
|
51
62
|
export function createMcpServer() {
|
|
52
63
|
const server = new McpServer({ name: 'sparkforensics', version: '1.0.0' });
|
|
53
64
|
|
|
65
|
+
server.registerTool('list_runs', {
|
|
66
|
+
description: 'List candidate Spark event-log runs from a local directory or a Spark History Server, before diagnosing one with the other tools.',
|
|
67
|
+
inputSchema: listRunsInputSchema,
|
|
68
|
+
}, (params) => toolResult(listRuns(params)));
|
|
69
|
+
|
|
54
70
|
server.registerTool('diagnose_run', {
|
|
55
71
|
description: 'Diagnose a Spark run: thresholded findings with remediation text, an impact-ranked fix recommendation rollup, and clean-check status.',
|
|
56
72
|
inputSchema: {
|
|
@@ -111,5 +127,19 @@ export function createMcpServer() {
|
|
|
111
127
|
Promise.resolve().then(() => getFindingEvidence(runId, findingId, { redact })),
|
|
112
128
|
));
|
|
113
129
|
|
|
130
|
+
server.registerTool('get_finding_documentation', {
|
|
131
|
+
description: 'Detection and tuning reference documentation for one finding type (not tied to a specific run), so a client without browser access can read the same background material the web view links to.',
|
|
132
|
+
inputSchema: { type: z.string() },
|
|
133
|
+
}, ({ type }) => toolResult(
|
|
134
|
+
Promise.resolve().then(() => getFindingDocumentation(type)),
|
|
135
|
+
));
|
|
136
|
+
|
|
137
|
+
server.registerTool('get_reference_doc', {
|
|
138
|
+
description: 'Full tuning-reference chapter or bottleneck markdown by doc anchor (e.g. "#joins", "#bottleneck-skew", "#metric-task-duration"), so a client without browser access can read the same reference the web view shows.',
|
|
139
|
+
inputSchema: { anchor: z.string() },
|
|
140
|
+
}, ({ anchor }) => toolResult(
|
|
141
|
+
Promise.resolve().then(() => getReferenceDoc(anchor)),
|
|
142
|
+
));
|
|
143
|
+
|
|
114
144
|
return server;
|
|
115
145
|
}
|