sparkforensics-server 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/assets/{AutoscalingChurn-5hDGIXi4.js → AutoscalingChurn-8t3U3isq.js} +1 -1
- package/public/assets/{CacheUtilization-CT2SCnJJ.js → CacheUtilization-CJs9tTum.js} +1 -1
- package/public/assets/CachingOpportunity-D1NcHs0V.js +1 -0
- package/public/assets/{ColdStart-B38OiAOp.js → ColdStart-CdRJ9gD8.js} +1 -1
- package/public/assets/{ConfigAudit-BoplryCH.js → ConfigAudit-B5Gbe5js.js} +1 -1
- package/public/assets/CoreUsageArea-a--NTRGY.js +1 -0
- package/public/assets/{Dashboard-BgKodJl2.js → Dashboard-C1hNeX49.js} +2 -2
- package/public/assets/DisclosureContext-ZpE6CkHx.js +1 -0
- package/public/assets/{DuplicatePlanSubtree-BqS2A2j4.js → DuplicatePlanSubtree-dobXdz7E.js} +1 -1
- package/public/assets/{ExecutorUtilization-Dp52QGWo.js → ExecutorUtilization-D9grwi7t.js} +1 -1
- package/public/assets/{GcPressure-CT1yzR_s.js → GcPressure-8-G2ri10.js} +1 -1
- package/public/assets/{IncompleteRun-C7Qkjtjn.js → IncompleteRun-B91lky7a.js} +1 -1
- package/public/assets/{JobFailures-WHl3ELfh.js → JobFailures-BsWvBMyJ.js} +1 -1
- package/public/assets/{MemoryUtilization-D8IUDHJI.js → MemoryUtilization-BM5schZI.js} +1 -1
- package/public/assets/{OverBroadcast-6oSSAQ_R.js → OverBroadcast-DdN8JIJH.js} +1 -1
- package/public/assets/{PartitionSizing-BwdFL-mw.js → PartitionSizing-CY5HDYtp.js} +1 -1
- package/public/assets/{PlanExplorer-DcxmiQWL.js → PlanExplorer-DgHCDBD_.js} +1 -1
- package/public/assets/PlanGraphRoute-DH3KJb8j.js +1 -0
- package/public/assets/{RetryWaste-BTbeHkJc.js → RetryWaste-DOnE9vGF.js} +1 -1
- package/public/assets/{ShuffleIO-CRhrYNE0.js → ShuffleIO-MMnDESkF.js} +1 -1
- package/public/assets/{Skew-DSNXRG9V.js → Skew-BmZZJ5ms.js} +1 -1
- package/public/assets/{SlowHost-B7Lwu2EU.js → SlowHost-CsJzfEqo.js} +1 -1
- package/public/assets/{SmallFiles-C082c2rd.js → SmallFiles-BoK3R1Ex.js} +1 -1
- package/public/assets/{SpeculationWaste-BS2vpadr.js → SpeculationWaste-CnVxB5AJ.js} +1 -1
- package/public/assets/Spill-BhNicyBy.js +1 -0
- package/public/assets/{StageFailed-D3ac9Sou.js → StageFailed-mAhHU4l_.js} +1 -1
- package/public/assets/StageFindingGroup-BEtPWcT0.js +1 -0
- package/public/assets/{StageHeader-pI1nTwVf.js → StageHeader-DJkYkrsU.js} +1 -1
- package/public/assets/{StageShape-eO5le8iG.js → StageShape-BDxRknLp.js} +1 -1
- package/public/assets/{StageSlowness-BaNyfNvL.js → StageSlowness-BPRw-otN.js} +1 -1
- package/public/assets/{Straggler-D5Fnok_y.js → Straggler-CZCbnOWP.js} +1 -1
- package/public/assets/{TaskFailures-BWnC-K_G.js → TaskFailures-C57YcLFQ.js} +1 -1
- package/public/assets/{TinyTask-8_5lpq_2.js → TinyTask-Bi84qU7n.js} +1 -1
- package/public/assets/{UnderBroadcast-DJ0XSwth.js → UnderBroadcast-DdQV7htE.js} +1 -1
- package/public/assets/{finding-anchor-Cys8Z6Yn.js → finding-anchor-Cl6_5hfV.js} +1 -1
- package/public/assets/{impact-sort-BTQndbof.js → impact-sort-CssZ6MT2.js} +1 -1
- package/public/assets/index-2h_TgUAb.css +2 -0
- package/public/assets/index-B-03mG7K.js +14 -0
- package/public/assets/{parser-worker-DmeCnLWU.js → parser-worker-CA5RxqKc.js} +8 -8
- package/public/assets/{select-DIYN6D3y.js → select-CQjNhprm.js} +1 -1
- package/public/assets/{usePagedRows-BxvdFnPk.js → usePagedRows-Dyl6T8Ok.js} +1 -1
- package/public/assets/useRecentFiles-XZEnLlal.js +11 -0
- package/public/assets/{useSortMode-B8SRfAoG.js → useSortMode-Dhvit8dt.js} +1 -1
- package/public/docs/404.html +2 -2
- package/public/docs/assets/{app.D2GqAkhy.js → app.eYTPqTeq.js} +1 -1
- package/public/docs/assets/chunks/@localSearchIndexroot.u_e5bQz4.js +1 -0
- package/public/docs/assets/chunks/{VPLocalSearchBox.BS4cSmtW.js → VPLocalSearchBox.DsZrQKQR.js} +1 -1
- package/public/docs/assets/chunks/{theme.BuxVpb0H.js → theme.DsTNE2pE.js} +2 -2
- package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.lean.js → contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.js +1 -0
- package/public/docs/assets/{user-guide_getting-started_ci-and-automation.md.DpnfKz3x.js → user-guide_getting-started_ci-and-automation.md.OUQqw3mI.js} +5 -4
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.lean.js +1 -0
- package/public/docs/assets/{user-guide_mcp-tools_compare-runs.md.CPowlb2q.js → user-guide_mcp-tools_compare-runs.md.Cc_gLoix.js} +3 -2
- package/public/docs/assets/{user-guide_mcp-tools_compare-runs.md.CPowlb2q.lean.js → user-guide_mcp-tools_compare-runs.md.Cc_gLoix.lean.js} +1 -1
- package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.js → user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.js} +2 -2
- package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.lean.js → user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.lean.js} +1 -1
- package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.js +1 -0
- package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.lean.js +1 -0
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.js +1 -0
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.lean.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.lean.js +1 -0
- package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +3 -3
- package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +3 -3
- package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +3 -3
- package/public/docs/contributor-guide/architecture/board-widgets.html +3 -3
- package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +3 -3
- package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +5 -5
- package/public/docs/contributor-guide/architecture/detector-contract.html +3 -3
- package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +3 -3
- package/public/docs/contributor-guide/architecture/drill-down.html +3 -3
- package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +3 -3
- package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +3 -3
- package/public/docs/contributor-guide/architecture/impact-estimation.html +3 -3
- package/public/docs/contributor-guide/architecture/index.html +3 -3
- package/public/docs/contributor-guide/architecture/overview.html +3 -3
- package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +3 -3
- package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +5 -5
- package/public/docs/contributor-guide/architecture/state-and-history.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +3 -3
- package/public/docs/contributor-guide/architecture/widget-rendering.html +3 -3
- package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +5 -5
- package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +3 -3
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +3 -3
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +3 -3
- package/public/docs/contributor-guide/architecture/worker-protocol.html +3 -3
- package/public/docs/contributor-guide/contributing.html +3 -3
- package/public/docs/contributor-guide/development-setup.html +3 -3
- package/public/docs/contributor-guide/testing.html +3 -3
- package/public/docs/hashmap.json +1 -1
- package/public/docs/index.html +3 -3
- package/public/docs/tuning-reference/anti-patterns.html +3 -3
- package/public/docs/tuning-reference/aqe.html +3 -3
- package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +3 -3
- package/public/docs/tuning-reference/bottleneck-cold-start.html +3 -3
- package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +3 -3
- package/public/docs/tuning-reference/bottleneck-failures.html +3 -3
- package/public/docs/tuning-reference/bottleneck-gc.html +3 -3
- package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +3 -3
- package/public/docs/tuning-reference/bottleneck-memory-utilization.html +3 -3
- package/public/docs/tuning-reference/bottleneck-retry-waste.html +3 -3
- package/public/docs/tuning-reference/bottleneck-shuffle.html +3 -3
- package/public/docs/tuning-reference/bottleneck-skew.html +3 -3
- package/public/docs/tuning-reference/bottleneck-slow-host.html +3 -3
- package/public/docs/tuning-reference/bottleneck-small-files.html +3 -3
- package/public/docs/tuning-reference/bottleneck-spill.html +3 -3
- package/public/docs/tuning-reference/bottleneck-straggler.html +3 -3
- package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +3 -3
- package/public/docs/tuning-reference/bottleneck-utilization.html +3 -3
- package/public/docs/tuning-reference/caching.html +3 -3
- package/public/docs/tuning-reference/cluster-config.html +3 -3
- package/public/docs/tuning-reference/config.html +3 -3
- package/public/docs/tuning-reference/data-formats.html +3 -3
- package/public/docs/tuning-reference/index.html +3 -3
- package/public/docs/tuning-reference/intro.html +3 -3
- package/public/docs/tuning-reference/joins.html +3 -3
- package/public/docs/tuning-reference/memory-model.html +3 -3
- package/public/docs/tuning-reference/metrics.html +3 -3
- package/public/docs/tuning-reference/partitioning.html +3 -3
- package/public/docs/tuning-reference/pyspark.html +3 -3
- package/public/docs/tuning-reference/shuffle.html +3 -3
- package/public/docs/tuning-reference/spark-architecture.html +3 -3
- package/public/docs/tuning-reference/table-formats.html +3 -3
- package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +3 -3
- package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +3 -3
- package/public/docs/user-guide/alternative-log-retrieval.html +3 -3
- package/public/docs/user-guide/getting-started/ci-and-automation.html +8 -7
- package/public/docs/user-guide/getting-started/reading-the-dashboard.html +3 -3
- package/public/docs/user-guide/getting-started/regression-metric-keys.html +3 -3
- package/public/docs/user-guide/getting-started/tuning-thresholds.html +3 -3
- package/public/docs/user-guide/getting-started.html +3 -3
- package/public/docs/user-guide/mcp-tools/compare-runs.html +7 -6
- package/public/docs/user-guide/mcp-tools/diagnose-run.html +3 -3
- package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +5 -5
- package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +3 -3
- package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +3 -3
- package/public/docs/user-guide/mcp-tools/get-reference-doc.html +3 -3
- package/public/docs/user-guide/mcp-tools/get-run-summary.html +3 -3
- package/public/docs/user-guide/mcp-tools/list-runs.html +3 -3
- package/public/docs/user-guide/mcp-tools.html +3 -3
- package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +5 -5
- package/public/docs/user-guide/run-comparison.html +5 -5
- package/public/docs/user-guide/understanding-findings.html +5 -5
- package/public/export-template.html +3 -3
- package/public/index.html +3 -3
- package/vendor-core/cli/budgets.js +62 -2
- package/vendor-core/cli/regression-budgets.js +35 -0
- package/vendor-core/comparison-output.js +2 -1
- package/vendor-core/core-source-hash.txt +1 -1
- package/vendor-core/detectors.js +219 -56
- package/vendor-core/docs-content/detection/chrn.md +3 -1
- package/vendor-core/docs-content/detection/cold.md +2 -1
- package/vendor-core/docs-content/detection/plan.md +9 -2
- package/vendor-core/docs-content/detection/shfl.md +8 -2
- package/vendor-core/docs-content/detection/skew.md +11 -5
- package/vendor-core/docs-content/detection/slow.md +6 -2
- package/vendor-core/docs-content/detection/spill.md +4 -1
- package/vendor-core/docs-content/detection/strag.md +5 -3
- package/vendor-core/docs-content/detection/tiny.md +3 -2
- package/vendor-core/evidence-report.js +11 -11
- package/vendor-core/finding-presentation.js +39 -13
- package/vendor-core/finding-types.js +50 -6
- package/vendor-core/mcp-server-factory.js +6 -2
- package/vendor-core/mcp-tools.js +10 -1
- package/vendor-core/median.js +8 -0
- package/vendor-core/run-comparison.js +2 -0
- package/vendor-core/sequence-alignment.js +83 -0
- package/vendor-core/stage-alignment.js +387 -63
- package/vendor-core/stage-quantiles.js +3 -1
- package/vendor-core/stage-structure.js +83 -0
- package/public/assets/CachingOpportunity-D_O_WkbH.js +0 -1
- package/public/assets/CoreUsageArea-csgwNxkl.js +0 -1
- package/public/assets/DisclosureContext-B0siMVdg.js +0 -1
- package/public/assets/PlanGraphRoute-pqnHTt4i.js +0 -1
- package/public/assets/Spill-B4DVKHXx.js +0 -1
- package/public/assets/StageFindingGroup-D9naK109.js +0 -1
- package/public/assets/index-BcqTLKFp.css +0 -2
- package/public/assets/index-DCeWge12.js +0 -13
- package/public/assets/useRecentFiles-C3Ov47U7.js +0 -11
- package/public/docs/assets/chunks/@localSearchIndexroot.D4OdkQjy.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.js +0 -1
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.lean.js +0 -1
- package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.js +0 -1
- package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.lean.js +0 -1
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.js +0 -1
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.lean.js +0 -1
- package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.js +0 -1
- package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.lean.js +0 -1
- /package/public/docs/assets/{contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.lean.js → contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.lean.js} +0 -0
- /package/public/docs/assets/{contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.lean.js → contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.lean.js} +0 -0
|
@@ -180,29 +180,29 @@ export const EVIDENCE_SCHEMA_VERSION = 5;
|
|
|
180
180
|
// module (stageShape's totalCores, utilization's unrounded fraction) is left off both, so it never
|
|
181
181
|
// reaches the report; adding, renaming or dropping a key here changes the report contract.
|
|
182
182
|
const EVIDENCE_KEYS = {
|
|
183
|
-
skew: [],
|
|
183
|
+
skew: ['origin'],
|
|
184
184
|
stageShape: ['rule'],
|
|
185
|
-
shuffle: [],
|
|
186
|
-
partitionSizing: ['rule'],
|
|
187
|
-
spill: ['spillMagnitude'],
|
|
185
|
+
shuffle: ['partitions'],
|
|
186
|
+
partitionSizing: ['rule', 'origin', 'partitions'],
|
|
187
|
+
spill: ['spillMagnitude', 'reads'],
|
|
188
188
|
gc: ['direction'],
|
|
189
189
|
slowHost: ['variant', 'host', 'hostTaskShare', 'hostMeanMs', 'dimension', 'executorId', 'execMaxValue'],
|
|
190
|
-
stageSlowness: [],
|
|
190
|
+
stageSlowness: ['reads'],
|
|
191
191
|
stageFailed: ['variant', 'numTasks', 'memoryBytesSpilled', 'failedTaskDetails'],
|
|
192
192
|
failures: ['failedTasks', 'dominantReason', 'dominantError', 'failureGroups', 'otherFailedTasks'],
|
|
193
|
-
straggler: ['unit', 'speculativeTasks', 'stragglerCount'],
|
|
193
|
+
straggler: ['unit', 'speculativeTasks', 'stragglerCount', 'origin'],
|
|
194
194
|
speculationWaste: [],
|
|
195
195
|
retryWaste: ['numTasks', 'memoryBytesSpilled', 'retriedTaskDetails'],
|
|
196
|
-
tinyTask: [],
|
|
196
|
+
tinyTask: ['reads'],
|
|
197
197
|
incompleteRun: [],
|
|
198
|
-
coldStart: [],
|
|
198
|
+
coldStart: ['dynamicAllocation'],
|
|
199
199
|
utilization: ['cpuUtilizationPct'],
|
|
200
200
|
memoryUtilization: ['variant', 'rule', 'executorId', 'heap', 'dataUnavailable'],
|
|
201
201
|
cacheUtilization: [
|
|
202
202
|
'variant', 'rddId', 'rddName', 'memorySize', 'diskSize', 'numCachedPartitions', 'numPartitions', 'dataUnavailable',
|
|
203
203
|
],
|
|
204
204
|
coreLocality: ['nonLocalTaskCount'],
|
|
205
|
-
autoscalingChurn: ['shortLivedExecutorCount'],
|
|
205
|
+
autoscalingChurn: ['shortLivedExecutorCount', 'dynamicAllocation'],
|
|
206
206
|
cachingOpportunity: ['variant', 'relation', 'format', 'relations', 'operator', 'executionIds', 'totalReadBytes'],
|
|
207
207
|
jobFailureRate: ['failedJobs', 'totalJobs', 'failedTasks', 'totalTasks', 'avgJobDurationMs', 'taskFailureRate'],
|
|
208
208
|
configAudit: ['property'],
|
|
@@ -211,8 +211,8 @@ const EVIDENCE_KEYS = {
|
|
|
211
211
|
'groupIndex',
|
|
212
212
|
],
|
|
213
213
|
smallFiles: ['executionId', 'stageIds', 'fileCount', 'direction', 'nodeName'],
|
|
214
|
-
underBroadcast: ['executionId', 'stageIds', 'largerSideBytes'],
|
|
215
|
-
overBroadcast: ['executionId', 'stageIds'],
|
|
214
|
+
underBroadcast: ['executionId', 'stageIds', 'largerSideBytes', 'broadcastThreshold'],
|
|
215
|
+
overBroadcast: ['executionId', 'stageIds', 'broadcastThreshold'],
|
|
216
216
|
} ;
|
|
217
217
|
|
|
218
218
|
// The other direction: an evidence field EVIDENCE_KEYS doesn't list fails here.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// detectors.ts is type-only, so it is erased at build time. A detector's scope, order and emits
|
|
9
9
|
// list stay on its DETECTORS entry; renderers get them through detectorInfoByType().
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
@@ -43,7 +43,9 @@ const SKEW_JOIN_ALREADY_ON = 'AQE skew-join handling is already on, so salt the
|
|
|
43
43
|
const SKEW_JOIN_AQE_OFF = 'AQE is off, so enable it (spark.sql.adaptive.enabled) for skew-join handling to apply; otherwise salt the key or repartition on a better key.';
|
|
44
44
|
|
|
45
45
|
// The skew-join generic line, worded per the row's remediation: AQE logged off, switch already on, or neither.
|
|
46
|
-
function skewJoinGeneric(f
|
|
46
|
+
function skewJoinGeneric(f , unset ) {
|
|
47
|
+
if (f.origin === 'inputScan') return 'Uneven input files: compact small files or split large ones (lower spark.sql.files.maxPartitionBytes).';
|
|
48
|
+
if (f.origin === 'other') return 'Work is uneven across tasks: salt the key or repartition on a better key.';
|
|
47
49
|
if (f.remediation?.some((r) => r.key === 'spark.sql.adaptive.enabled')) return SKEW_JOIN_AQE_OFF;
|
|
48
50
|
return switchAlreadyOn(f, SKEW_JOIN_KEY) ? SKEW_JOIN_ALREADY_ON : unset;
|
|
49
51
|
}
|
|
@@ -120,8 +122,9 @@ export const FINDING_PRESENTATION
|
|
|
120
122
|
tag: 'TINY',
|
|
121
123
|
thresholdSummary: (t) => `${t.minTasks}+ tasks with a median of ${t.maxP50}ms or less and a P95 of ${t.maxP95}ms or less`,
|
|
122
124
|
actionLabel: () => 'Coalesce small tasks',
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
genericRecommendation: (f) => (f.reads != null && f.reads !== 'shuffle'
|
|
126
|
+
? 'Scheduler overhead may dominate: coalesce down to fewer, larger tasks.'
|
|
127
|
+
: 'Scheduler overhead may dominate: lower spark.sql.shuffle.partitions, or coalesce down to fewer, larger tasks.'),
|
|
125
128
|
},
|
|
126
129
|
|
|
127
130
|
shuffle: {
|
|
@@ -129,7 +132,14 @@ export const FINDING_PRESENTATION
|
|
|
129
132
|
tag: 'SHFL',
|
|
130
133
|
thresholdSummary: (t) => `stage shuffle read above ${t.minBytes / 1048576} MiB`,
|
|
131
134
|
actionLabel: () => 'Reduce shuffle size',
|
|
132
|
-
genericRecommendation: () =>
|
|
135
|
+
genericRecommendation: (f) => {
|
|
136
|
+
switch (f.partitions) {
|
|
137
|
+
case 'sufficient': return 'The tasks are already a good size, so more partitions will not help: use a broadcast join for the smaller side.';
|
|
138
|
+
case 'aqeCoalesced': return 'AQE coalesced the shuffle: lower spark.sql.adaptive.advisoryPartitionSizeInBytes, or use a broadcast join for the smaller side.';
|
|
139
|
+
case 'ownPartitioning': return "spark.sql.shuffle.partitions is already high enough, so raise this stage's own partition count (its repartition(n) or RDD parallelism), or use a broadcast join for the smaller side.";
|
|
140
|
+
default: return 'Raise spark.sql.shuffle.partitions, or use a broadcast join for the smaller side.';
|
|
141
|
+
}
|
|
142
|
+
},
|
|
133
143
|
},
|
|
134
144
|
partitionSizing: {
|
|
135
145
|
name: 'partition sizing',
|
|
@@ -146,7 +156,9 @@ export const FINDING_PRESENTATION
|
|
|
146
156
|
genericRecommendation(f) {
|
|
147
157
|
switch (f.rule) {
|
|
148
158
|
case 'shufflePartitionSkew': return skewJoinGeneric(f, 'For join skew, enable AQE skew-join handling (spark.sql.adaptive.skewJoin.enabled); otherwise salt the key or repartition on a better key.');
|
|
149
|
-
case 'lowShuffleParallelism':
|
|
159
|
+
case 'lowShuffleParallelism':
|
|
160
|
+
if (f.partitions === 'aqeCoalesced') return 'AQE coalesced the shuffle into few tasks: lower spark.sql.adaptive.advisoryPartitionSizeInBytes so each partition is smaller.';
|
|
161
|
+
return switchAlreadyOn(f, 'spark.sql.shuffle.partitions')
|
|
150
162
|
? "spark.sql.shuffle.partitions is already high enough, so raise this stage's own partition count (its repartition(n) or RDD parallelism) so each partition is smaller."
|
|
151
163
|
: 'Raise spark.sql.shuffle.partitions so each partition is smaller.';
|
|
152
164
|
case 'maxPartitionTooBig': return 'Repartition to break up the oversized partition before this stage.';
|
|
@@ -161,7 +173,9 @@ export const FINDING_PRESENTATION
|
|
|
161
173
|
thresholdSummary: (t) => `single-task disk spill above ${t.singleTaskDiskGiB} GiB`,
|
|
162
174
|
actionLabel: () => 'Reduce spill',
|
|
163
175
|
// The skew/volume classification isn't a Finding field, so one sentence covers both.
|
|
164
|
-
genericRecommendation: () =>
|
|
176
|
+
genericRecommendation: (f) => (f.reads != null && f.reads !== 'shuffle'
|
|
177
|
+
? 'If the spill is skew-driven, fix task skew first: adding memory will not help. Otherwise increase executor memory or process less data per task.'
|
|
178
|
+
: 'If the spill is skew-driven, fix task skew first: adding memory will not help. Otherwise raise spark.sql.shuffle.partitions or increase executor memory.'),
|
|
165
179
|
},
|
|
166
180
|
|
|
167
181
|
gc: {
|
|
@@ -219,14 +233,18 @@ export const FINDING_PRESENTATION
|
|
|
219
233
|
tag: 'SLOW',
|
|
220
234
|
thresholdSummary: () => 'a stage running far longer than its peers, not attributable to a single slow host',
|
|
221
235
|
actionLabel: () => 'Profile slow stage',
|
|
222
|
-
genericRecommendation: () =>
|
|
236
|
+
genericRecommendation: (f) => f.reads === 'input'
|
|
237
|
+
? 'Often too few or too uneven input partitions: check input file sizes and lower spark.sql.files.maxPartitionBytes, or look for a large per-task data volume driving heavy spill.'
|
|
238
|
+
: f.reads === 'other'
|
|
239
|
+
? 'Check what the stage computes and for a large per-task data volume driving heavy spill.'
|
|
240
|
+
: 'Often a partition-count problem: raise parallelism via spark.sql.shuffle.partitions or spark.default.parallelism, or check for a large per-task data volume driving heavy shuffle and spill.',
|
|
223
241
|
},
|
|
224
242
|
straggler: {
|
|
225
243
|
name: 'straggling task',
|
|
226
244
|
tag: 'STRAG',
|
|
227
245
|
thresholdSummary: () => 'one or more tasks finishing far after the rest of their stage',
|
|
228
246
|
actionLabel: () => 'Fix stragglers',
|
|
229
|
-
genericRecommendation: () =>
|
|
247
|
+
genericRecommendation: (f) => `Rule out a GC pause or a slow shuffle fetch before assuming a hardware issue. If uneven data is the cause: ${skewJoinGeneric(f, 'for join-driven skew, enable AQE skew-join handling (spark.sql.adaptive.skewJoin.enabled); otherwise salt the key or repartition on a better key.')}`,
|
|
230
248
|
},
|
|
231
249
|
speculationWaste: {
|
|
232
250
|
name: 'speculation waste',
|
|
@@ -240,7 +258,9 @@ export const FINDING_PRESENTATION
|
|
|
240
258
|
tag: 'COLD',
|
|
241
259
|
thresholdSummary: (t) => `the first stage waiting over ${t.gapSeconds}s for an executor`,
|
|
242
260
|
actionLabel: () => 'Pre-warm cluster',
|
|
243
|
-
genericRecommendation: () =>
|
|
261
|
+
genericRecommendation: (f) => (f.dynamicAllocation === 'off'
|
|
262
|
+
? 'Keep a warm pool of idle executors: dynamic allocation is off, so no executor-count property applies.'
|
|
263
|
+
: 'Keep a warm pool of idle executors, or if using dynamic allocation, raise the minimum/initial executor count so it does not scale up from zero.'),
|
|
244
264
|
},
|
|
245
265
|
|
|
246
266
|
memoryUtilization: {
|
|
@@ -322,7 +342,9 @@ export const FINDING_PRESENTATION
|
|
|
322
342
|
tag: 'CHRN',
|
|
323
343
|
thresholdSummary: (t) => `over ${shareLabel(t.warningPct)} of executors living under ${t.shortLivedMs / 60000} minutes`,
|
|
324
344
|
actionLabel: () => 'Reduce autoscaling churn',
|
|
325
|
-
genericRecommendation: () =>
|
|
345
|
+
genericRecommendation: (f) => (f.dynamicAllocation === 'off'
|
|
346
|
+
? 'Dynamic allocation is off, so the churn comes from executors lost or preempted: check the cluster manager\'s preemption and executor-loss logs.'
|
|
347
|
+
: 'This looks like wasteful re-provisioning rather than normal scale-down: consider raising spark.dynamicAllocation.executorIdleTimeout or widening the minExecutors/maxExecutors bounds to reduce flapping.'),
|
|
326
348
|
},
|
|
327
349
|
|
|
328
350
|
configAudit: CONFIG_AUDIT_PRESENTATION,
|
|
@@ -349,14 +371,18 @@ export const FINDING_PRESENTATION
|
|
|
349
371
|
tag: 'PLAN',
|
|
350
372
|
thresholdSummary: () => 'a join below the configured size floor that skipped broadcast',
|
|
351
373
|
actionLabel: () => 'Use broadcast join',
|
|
352
|
-
genericRecommendation: () =>
|
|
374
|
+
genericRecommendation: (f) => (f.broadcastThreshold === 'notLimiting'
|
|
375
|
+
? 'The threshold already admits the smaller side, so it is not what stopped the broadcast: consider a broadcast() hint or collecting table statistics.'
|
|
376
|
+
: 'This could have been a broadcast join: consider a broadcast() hint or raising spark.sql.autoBroadcastJoinThreshold.'),
|
|
353
377
|
},
|
|
354
378
|
overBroadcast: {
|
|
355
379
|
name: 'oversized broadcast join',
|
|
356
380
|
tag: 'PLAN',
|
|
357
381
|
thresholdSummary: (t) => `a broadcast over ${t.overBroadcastBytes / 1073741824} GiB`,
|
|
358
382
|
actionLabel: () => 'Fix oversized broadcast',
|
|
359
|
-
genericRecommendation: (f) => (
|
|
383
|
+
genericRecommendation: (f) => (f.broadcastThreshold === 'notLimiting'
|
|
384
|
+
? 'The configured threshold is below this broadcast, so remove the broadcast() hint that forced it.'
|
|
385
|
+
: switchAlreadyOn(f, 'spark.sql.autoBroadcastJoinThreshold')
|
|
360
386
|
? 'Automatic broadcast is already disabled, so remove the broadcast() hint that forced it.'
|
|
361
387
|
: 'Check for a misapplied broadcast hint or a misconfigured spark.sql.autoBroadcastJoinThreshold.'),
|
|
362
388
|
},
|
|
@@ -79,7 +79,22 @@
|
|
|
79
79
|
|
|
80
80
|
// ── Per-stage detectors ─────────────────────────────────────────────────────
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
/** What a skewed stage reads, which decides the fix: 'shuffleJoin' (a shuffle feeding a join, where
|
|
83
|
+
* AQE skew-join handling applies), 'inputScan' (uneven input files) or 'other'. */
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/** What a stage reads, by the dominant side: a shuffle, input files, or neither. */
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/** Why a shuffle finding's partition-count advice is or is not given: 'raise' (the property limits
|
|
90
|
+
* the stage), 'sufficient' (tasks are already a good size), 'aqeCoalesced' (AQE merged the
|
|
91
|
+
* partitions, so the advisory size is the lever) or 'ownPartitioning' (the property is already high
|
|
92
|
+
* enough, so the stage's own repartition(n) or RDD parallelism is). */
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
83
98
|
|
|
84
99
|
|
|
85
100
|
|
|
@@ -90,16 +105,24 @@
|
|
|
90
105
|
|
|
91
106
|
|
|
92
107
|
|
|
93
|
-
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
94
111
|
|
|
95
112
|
|
|
96
113
|
|
|
97
114
|
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
98
119
|
|
|
99
120
|
|
|
100
121
|
|
|
101
122
|
|
|
102
123
|
|
|
124
|
+
|
|
125
|
+
|
|
103
126
|
|
|
104
127
|
|
|
105
128
|
|
|
@@ -124,7 +147,10 @@
|
|
|
124
147
|
|
|
125
148
|
|
|
126
149
|
|
|
127
|
-
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
128
154
|
|
|
129
155
|
|
|
130
156
|
|
|
@@ -151,6 +177,8 @@
|
|
|
151
177
|
|
|
152
178
|
|
|
153
179
|
|
|
180
|
+
|
|
181
|
+
|
|
154
182
|
|
|
155
183
|
|
|
156
184
|
|
|
@@ -167,7 +195,10 @@
|
|
|
167
195
|
|
|
168
196
|
|
|
169
197
|
|
|
170
|
-
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
171
202
|
|
|
172
203
|
|
|
173
204
|
// ── App-level detectors ─────────────────────────────────────────────────────
|
|
@@ -176,7 +207,10 @@
|
|
|
176
207
|
// valueText is always 'missing': the ApplicationEnd event the log lacks.
|
|
177
208
|
|
|
178
209
|
|
|
179
|
-
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
180
214
|
|
|
181
215
|
|
|
182
216
|
|
|
@@ -231,6 +265,8 @@
|
|
|
231
265
|
|
|
232
266
|
|
|
233
267
|
|
|
268
|
+
|
|
269
|
+
|
|
234
270
|
|
|
235
271
|
|
|
236
272
|
|
|
@@ -296,13 +332,21 @@
|
|
|
296
332
|
|
|
297
333
|
|
|
298
334
|
|
|
335
|
+
|
|
336
|
+
/** How the effective spark.sql.autoBroadcastJoinThreshold relates to the finding: 'limits' (the
|
|
337
|
+
* property decided it, or is unknown), 'notLimiting' (already admits, or already below, the
|
|
338
|
+
* broadcast, so something else decided it) or 'disabled' (-1). */
|
|
339
|
+
|
|
299
340
|
|
|
300
341
|
|
|
301
342
|
|
|
343
|
+
|
|
302
344
|
|
|
303
345
|
|
|
304
346
|
|
|
305
|
-
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
|
|
306
350
|
|
|
307
351
|
|
|
308
352
|
// ── Unions ──────────────────────────────────────────────────────────────────
|
|
@@ -119,14 +119,18 @@ export function createMcpServer({ thresholds }
|
|
|
119
119
|
maxRegressionPct: z.number().optional(), regressionMetric: z.string().optional(), failOnIntroduced: z.string().optional(),
|
|
120
120
|
regressionBudgets: z.array(z.object({ metric: z.string(), maxPct: z.number().nonnegative() })).optional()
|
|
121
121
|
.describe('Further regression budgets, one per metric (the CLI\'s repeated --regression-budget). A metric can be budgeted once across this list and maxRegressionPct/regressionMetric.'),
|
|
122
|
+
stageRegressionBudgets: z.array(z.object({ metric: z.string(), maxPct: z.number().nonnegative() })).optional()
|
|
123
|
+
.describe('Per paired stage (needs sourceB/runIdB): fail when any eligible pair\'s metric (executorRunTime, executorCpuTime, memoryBytesSpilled, diskBytesSpilled, shuffleReadBytes, shuffleWriteBytes) regressed by more than maxPct. One entry per metric. Inconclusive when no stage pairs.'),
|
|
124
|
+
stageQualities: z.array(z.enum(['exact', 'structural', 'aligned'])).optional()
|
|
125
|
+
.describe('Pair qualities the stage budgets read. Default exact and structural: an aligned pair may compare different work.'),
|
|
122
126
|
},
|
|
123
127
|
}, ({
|
|
124
128
|
source, runId, runIdB, sourceB,
|
|
125
129
|
maxRuntimeMs, maxSpillGb, maxSkewRatio, maxFailedTaskRatePct, minEfficiencyPct,
|
|
126
|
-
maxRegressionPct, regressionMetric, failOnIntroduced, regressionBudgets,
|
|
130
|
+
maxRegressionPct, regressionMetric, failOnIntroduced, regressionBudgets, stageRegressionBudgets, stageQualities,
|
|
127
131
|
}) => toolResult(evaluateBudgetsForRun(
|
|
128
132
|
{ source, runId },
|
|
129
|
-
{ maxRuntimeMs, maxSpillGb, maxSkewRatio, maxFailedTaskRatePct, minEfficiencyPct, maxRegressionPct, regressionMetric, failOnIntroduced, regressionBudgets },
|
|
133
|
+
{ maxRuntimeMs, maxSpillGb, maxSkewRatio, maxFailedTaskRatePct, minEfficiencyPct, maxRegressionPct, regressionMetric, failOnIntroduced, regressionBudgets, stageRegressionBudgets, stageQualities },
|
|
130
134
|
(runIdB || sourceB) ? { runId: runIdB, source: sourceB } : undefined,
|
|
131
135
|
{ thresholds },
|
|
132
136
|
)));
|
package/vendor-core/mcp-tools.js
CHANGED
|
@@ -13,7 +13,7 @@ import { buildComparisonOutput, } from './comparison-outpu
|
|
|
13
13
|
import { compileNormalizePatterns } from './stage-alignment.js';
|
|
14
14
|
import { runOutputBlocks } from './run-output.js';
|
|
15
15
|
import { evaluateBudgets, } from './cli/budgets.js';
|
|
16
|
-
import { combineRegressionBudgets } from './cli/regression-budgets.js';
|
|
16
|
+
import { assertStageBudgetMetric, combineRegressionBudgets } from './cli/regression-budgets.js';
|
|
17
17
|
import { FINDING_NAMES, titleCase } from './finding-names.js';
|
|
18
18
|
import { docAnchorForType } from './detector-docs.js';
|
|
19
19
|
import { DETECTORS, } from './detectors.js';
|
|
@@ -416,6 +416,15 @@ export async function evaluateBudgetsForRun(
|
|
|
416
416
|
} catch (e) {
|
|
417
417
|
throw mcpError('access-or-upstream-failure', (e ).message);
|
|
418
418
|
}
|
|
419
|
+
if (budgets.stageQualities !== undefined && budgets.stageRegressionBudgets === undefined) {
|
|
420
|
+
throw mcpError('access-or-upstream-failure', 'stageQualities requires stageRegressionBudgets.');
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
for (const { metric } of budgets.stageRegressionBudgets ?? []) assertStageBudgetMetric(metric, 'stageRegressionBudgets');
|
|
424
|
+
combineRegressionBudgets((budgets.stageRegressionBudgets ?? []).map((budget) => ({ origin: 'stageRegressionBudgets', budget })));
|
|
425
|
+
} catch (e) {
|
|
426
|
+
throw mcpError('access-or-upstream-failure', (e ).message);
|
|
427
|
+
}
|
|
419
428
|
const [first, second] = await Promise.all([
|
|
420
429
|
resolveAndAnalyze(primary, opts?.thresholds),
|
|
421
430
|
secondary ? resolveAndAnalyze(secondary, opts?.thresholds) : Promise.resolve(undefined),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The textbook median of values already sorted ascending: the middle value for an odd count, the
|
|
2
|
+
// mean of the two middle values for an even one. 0 for an empty list.
|
|
3
|
+
export function medianOfSorted(sorted ) {
|
|
4
|
+
const n = sorted.length;
|
|
5
|
+
if (n === 0) return 0;
|
|
6
|
+
const mid = n >> 1;
|
|
7
|
+
return n % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
8
|
+
}
|
|
@@ -38,6 +38,7 @@ import { summarizeRunOutcome } from './run-outcome.js';
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
|
|
@@ -408,6 +409,7 @@ export function compareRuns(
|
|
|
408
409
|
unmatched: alignment.unmatched,
|
|
409
410
|
replanned: alignment.replanned,
|
|
410
411
|
bookkeepingStageIds: alignment.bookkeepingStageIds,
|
|
412
|
+
executionAlignment: alignment.executionAlignment,
|
|
411
413
|
metrics: metricDeltas(baseSnap, candSnap),
|
|
412
414
|
findings: findingsDelta(baseSnap, candSnap),
|
|
413
415
|
stageSkew: stageSkewDeltas(baseSnap, candSnap, alignment.pairs),
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Order-preserving sequence alignment: the weighted longest-common-subsequence dynamic program the
|
|
2
|
+
// stage aligner runs over each run's SQL executions. Pure and deterministic.
|
|
3
|
+
//
|
|
4
|
+
// Cost: the full table has n*m cells and one similarity call per cell. Above `FULL_ALIGNMENT_MAX_CELLS`
|
|
5
|
+
// the table is restricted to a band around the diagonal, which costs about n*(2W+1) cells with
|
|
6
|
+
// W = `BAND_BASE_HALF_WIDTH` + |n-m|/2, and the result reports that the bounded form ran.
|
|
7
|
+
|
|
8
|
+
/** Largest n*m the alignment runs in full (1000 x 1000 executions). */
|
|
9
|
+
export const FULL_ALIGNMENT_MAX_CELLS = 1_000_000;
|
|
10
|
+
/** Band half-width added to half of |n-m| once the table is bounded: the band is centered on the line
|
|
11
|
+
* from corner to corner, and a run of insertions or deletions in one place drifts the best path from
|
|
12
|
+
* that line by at most |n-m|/2. */
|
|
13
|
+
export const BAND_BASE_HALF_WIDTH = 100;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/** Aligns two sequences of lengths `n` and `m`, maximizing the summed `score` of matched pairs. `score`
|
|
23
|
+
* returns a non-negative integer; a pair scoring below `minScore` is never matched. Of equally good
|
|
24
|
+
* alignments the one that matches whenever matching is optimal wins, and where it must skip, it skips the
|
|
25
|
+
* element with the higher id (`idA`, `idB`), so the lower ids stay and swapping the two sequences mirrors
|
|
26
|
+
* the pairs. */
|
|
27
|
+
export function alignSequences(
|
|
28
|
+
n , m ,
|
|
29
|
+
score ,
|
|
30
|
+
minScore ,
|
|
31
|
+
idA , idB ,
|
|
32
|
+
) {
|
|
33
|
+
if (n === 0 || m === 0) return { pairs: [], bounded: false };
|
|
34
|
+
const bounded = n * m > FULL_ALIGNMENT_MAX_CELLS;
|
|
35
|
+
const half = BAND_BASE_HALF_WIDTH + Math.ceil(Math.abs(n - m) / 2);
|
|
36
|
+
// Row i may match columns lo[i]..hi[i]; both bounds only grow with i.
|
|
37
|
+
const lo = new Int32Array(n), hi = new Int32Array(n);
|
|
38
|
+
for (let i = 0; i < n; i++) {
|
|
39
|
+
const center = Math.floor((i * m) / n);
|
|
40
|
+
lo[i] = bounded ? Math.max(0, center - half) : 0;
|
|
41
|
+
hi[i] = bounded ? Math.min(m - 1, center + half) : m - 1;
|
|
42
|
+
}
|
|
43
|
+
// best[i][j - lo[i]]: the best total from the suffixes starting at (i, j), for j inside row i's band.
|
|
44
|
+
const best = new Array(n);
|
|
45
|
+
// Cells outside the band are not stored: left of it the row's first cell stands in (those columns can
|
|
46
|
+
// only match in later rows, whose bands start no earlier), right of it the row cannot match, so the
|
|
47
|
+
// next row answers.
|
|
48
|
+
const get = (i , j ) => {
|
|
49
|
+
for (;;) {
|
|
50
|
+
if (i >= n || j >= m) return 0;
|
|
51
|
+
if (j < lo[i]) j = lo[i];
|
|
52
|
+
else if (j > hi[i]) i++;
|
|
53
|
+
else return best[i][j - lo[i]];
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
// The caller memoizes `score`, so the walk below re-asking for visited cells is cheap.
|
|
57
|
+
const cell = score;
|
|
58
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
59
|
+
const row = new Int32Array(hi[i] - lo[i] + 1);
|
|
60
|
+
best[i] = row;
|
|
61
|
+
for (let j = hi[i]; j >= lo[i]; j--) {
|
|
62
|
+
let v = Math.max(get(i + 1, j), j + 1 <= hi[i] ? row[j + 1 - lo[i]] : get(i + 1, j + 1));
|
|
63
|
+
const s = cell(i, j);
|
|
64
|
+
if (s >= minScore) v = Math.max(v, s + get(i + 1, j + 1));
|
|
65
|
+
row[j - lo[i]] = v;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pairs = [];
|
|
70
|
+
let i = 0, j = 0;
|
|
71
|
+
while (i < n && j < m) {
|
|
72
|
+
if (j < lo[i]) { j = lo[i]; continue; }
|
|
73
|
+
if (j > hi[i]) { i++; continue; }
|
|
74
|
+
const v = best[i][j - lo[i]];
|
|
75
|
+
const s = cell(i, j);
|
|
76
|
+
if (s >= minScore && s + get(i + 1, j + 1) === v) { pairs.push([i, j]); i++; j++; continue; }
|
|
77
|
+
const skipA = get(i + 1, j) === v, skipB = get(i, j + 1) === v;
|
|
78
|
+
if (skipA && skipB) { if (idA(i) >= idB(j)) i++; else j++; }
|
|
79
|
+
else if (skipA) i++;
|
|
80
|
+
else j++;
|
|
81
|
+
}
|
|
82
|
+
return { pairs, bounded };
|
|
83
|
+
}
|