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.
Files changed (199) hide show
  1. package/package.json +1 -1
  2. package/public/assets/{AutoscalingChurn-5hDGIXi4.js → AutoscalingChurn-8t3U3isq.js} +1 -1
  3. package/public/assets/{CacheUtilization-CT2SCnJJ.js → CacheUtilization-CJs9tTum.js} +1 -1
  4. package/public/assets/CachingOpportunity-D1NcHs0V.js +1 -0
  5. package/public/assets/{ColdStart-B38OiAOp.js → ColdStart-CdRJ9gD8.js} +1 -1
  6. package/public/assets/{ConfigAudit-BoplryCH.js → ConfigAudit-B5Gbe5js.js} +1 -1
  7. package/public/assets/CoreUsageArea-a--NTRGY.js +1 -0
  8. package/public/assets/{Dashboard-BgKodJl2.js → Dashboard-C1hNeX49.js} +2 -2
  9. package/public/assets/DisclosureContext-ZpE6CkHx.js +1 -0
  10. package/public/assets/{DuplicatePlanSubtree-BqS2A2j4.js → DuplicatePlanSubtree-dobXdz7E.js} +1 -1
  11. package/public/assets/{ExecutorUtilization-Dp52QGWo.js → ExecutorUtilization-D9grwi7t.js} +1 -1
  12. package/public/assets/{GcPressure-CT1yzR_s.js → GcPressure-8-G2ri10.js} +1 -1
  13. package/public/assets/{IncompleteRun-C7Qkjtjn.js → IncompleteRun-B91lky7a.js} +1 -1
  14. package/public/assets/{JobFailures-WHl3ELfh.js → JobFailures-BsWvBMyJ.js} +1 -1
  15. package/public/assets/{MemoryUtilization-D8IUDHJI.js → MemoryUtilization-BM5schZI.js} +1 -1
  16. package/public/assets/{OverBroadcast-6oSSAQ_R.js → OverBroadcast-DdN8JIJH.js} +1 -1
  17. package/public/assets/{PartitionSizing-BwdFL-mw.js → PartitionSizing-CY5HDYtp.js} +1 -1
  18. package/public/assets/{PlanExplorer-DcxmiQWL.js → PlanExplorer-DgHCDBD_.js} +1 -1
  19. package/public/assets/PlanGraphRoute-DH3KJb8j.js +1 -0
  20. package/public/assets/{RetryWaste-BTbeHkJc.js → RetryWaste-DOnE9vGF.js} +1 -1
  21. package/public/assets/{ShuffleIO-CRhrYNE0.js → ShuffleIO-MMnDESkF.js} +1 -1
  22. package/public/assets/{Skew-DSNXRG9V.js → Skew-BmZZJ5ms.js} +1 -1
  23. package/public/assets/{SlowHost-B7Lwu2EU.js → SlowHost-CsJzfEqo.js} +1 -1
  24. package/public/assets/{SmallFiles-C082c2rd.js → SmallFiles-BoK3R1Ex.js} +1 -1
  25. package/public/assets/{SpeculationWaste-BS2vpadr.js → SpeculationWaste-CnVxB5AJ.js} +1 -1
  26. package/public/assets/Spill-BhNicyBy.js +1 -0
  27. package/public/assets/{StageFailed-D3ac9Sou.js → StageFailed-mAhHU4l_.js} +1 -1
  28. package/public/assets/StageFindingGroup-BEtPWcT0.js +1 -0
  29. package/public/assets/{StageHeader-pI1nTwVf.js → StageHeader-DJkYkrsU.js} +1 -1
  30. package/public/assets/{StageShape-eO5le8iG.js → StageShape-BDxRknLp.js} +1 -1
  31. package/public/assets/{StageSlowness-BaNyfNvL.js → StageSlowness-BPRw-otN.js} +1 -1
  32. package/public/assets/{Straggler-D5Fnok_y.js → Straggler-CZCbnOWP.js} +1 -1
  33. package/public/assets/{TaskFailures-BWnC-K_G.js → TaskFailures-C57YcLFQ.js} +1 -1
  34. package/public/assets/{TinyTask-8_5lpq_2.js → TinyTask-Bi84qU7n.js} +1 -1
  35. package/public/assets/{UnderBroadcast-DJ0XSwth.js → UnderBroadcast-DdQV7htE.js} +1 -1
  36. package/public/assets/{finding-anchor-Cys8Z6Yn.js → finding-anchor-Cl6_5hfV.js} +1 -1
  37. package/public/assets/{impact-sort-BTQndbof.js → impact-sort-CssZ6MT2.js} +1 -1
  38. package/public/assets/index-2h_TgUAb.css +2 -0
  39. package/public/assets/index-B-03mG7K.js +14 -0
  40. package/public/assets/{parser-worker-DmeCnLWU.js → parser-worker-CA5RxqKc.js} +8 -8
  41. package/public/assets/{select-DIYN6D3y.js → select-CQjNhprm.js} +1 -1
  42. package/public/assets/{usePagedRows-BxvdFnPk.js → usePagedRows-Dyl6T8Ok.js} +1 -1
  43. package/public/assets/useRecentFiles-XZEnLlal.js +11 -0
  44. package/public/assets/{useSortMode-B8SRfAoG.js → useSortMode-Dhvit8dt.js} +1 -1
  45. package/public/docs/404.html +2 -2
  46. package/public/docs/assets/{app.D2GqAkhy.js → app.eYTPqTeq.js} +1 -1
  47. package/public/docs/assets/chunks/@localSearchIndexroot.u_e5bQz4.js +1 -0
  48. package/public/docs/assets/chunks/{VPLocalSearchBox.BS4cSmtW.js → VPLocalSearchBox.DsZrQKQR.js} +1 -1
  49. package/public/docs/assets/chunks/{theme.BuxVpb0H.js → theme.DsTNE2pE.js} +2 -2
  50. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.js +1 -0
  51. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.js +1 -0
  52. 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
  53. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.js +1 -0
  54. 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
  55. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.lean.js +1 -0
  56. 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
  57. 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
  58. package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.js → user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.js} +2 -2
  59. 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
  60. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.js +1 -0
  61. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.lean.js +1 -0
  62. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.js +1 -0
  63. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.lean.js +1 -0
  64. package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.js +1 -0
  65. package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.lean.js +1 -0
  66. package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +3 -3
  67. package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +3 -3
  68. package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +3 -3
  69. package/public/docs/contributor-guide/architecture/board-widgets.html +3 -3
  70. package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +3 -3
  71. package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +5 -5
  72. package/public/docs/contributor-guide/architecture/detector-contract.html +3 -3
  73. package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +3 -3
  74. package/public/docs/contributor-guide/architecture/drill-down.html +3 -3
  75. package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +3 -3
  76. package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +3 -3
  77. package/public/docs/contributor-guide/architecture/impact-estimation.html +3 -3
  78. package/public/docs/contributor-guide/architecture/index.html +3 -3
  79. package/public/docs/contributor-guide/architecture/overview.html +3 -3
  80. package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +3 -3
  81. package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +5 -5
  82. package/public/docs/contributor-guide/architecture/state-and-history.html +3 -3
  83. package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +3 -3
  84. package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +3 -3
  85. package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +3 -3
  86. package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +3 -3
  87. package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +3 -3
  88. package/public/docs/contributor-guide/architecture/widget-rendering.html +3 -3
  89. package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +5 -5
  90. package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +3 -3
  91. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +3 -3
  92. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +3 -3
  93. package/public/docs/contributor-guide/architecture/worker-protocol.html +3 -3
  94. package/public/docs/contributor-guide/contributing.html +3 -3
  95. package/public/docs/contributor-guide/development-setup.html +3 -3
  96. package/public/docs/contributor-guide/testing.html +3 -3
  97. package/public/docs/hashmap.json +1 -1
  98. package/public/docs/index.html +3 -3
  99. package/public/docs/tuning-reference/anti-patterns.html +3 -3
  100. package/public/docs/tuning-reference/aqe.html +3 -3
  101. package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +3 -3
  102. package/public/docs/tuning-reference/bottleneck-cold-start.html +3 -3
  103. package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +3 -3
  104. package/public/docs/tuning-reference/bottleneck-failures.html +3 -3
  105. package/public/docs/tuning-reference/bottleneck-gc.html +3 -3
  106. package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +3 -3
  107. package/public/docs/tuning-reference/bottleneck-memory-utilization.html +3 -3
  108. package/public/docs/tuning-reference/bottleneck-retry-waste.html +3 -3
  109. package/public/docs/tuning-reference/bottleneck-shuffle.html +3 -3
  110. package/public/docs/tuning-reference/bottleneck-skew.html +3 -3
  111. package/public/docs/tuning-reference/bottleneck-slow-host.html +3 -3
  112. package/public/docs/tuning-reference/bottleneck-small-files.html +3 -3
  113. package/public/docs/tuning-reference/bottleneck-spill.html +3 -3
  114. package/public/docs/tuning-reference/bottleneck-straggler.html +3 -3
  115. package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +3 -3
  116. package/public/docs/tuning-reference/bottleneck-utilization.html +3 -3
  117. package/public/docs/tuning-reference/caching.html +3 -3
  118. package/public/docs/tuning-reference/cluster-config.html +3 -3
  119. package/public/docs/tuning-reference/config.html +3 -3
  120. package/public/docs/tuning-reference/data-formats.html +3 -3
  121. package/public/docs/tuning-reference/index.html +3 -3
  122. package/public/docs/tuning-reference/intro.html +3 -3
  123. package/public/docs/tuning-reference/joins.html +3 -3
  124. package/public/docs/tuning-reference/memory-model.html +3 -3
  125. package/public/docs/tuning-reference/metrics.html +3 -3
  126. package/public/docs/tuning-reference/partitioning.html +3 -3
  127. package/public/docs/tuning-reference/pyspark.html +3 -3
  128. package/public/docs/tuning-reference/shuffle.html +3 -3
  129. package/public/docs/tuning-reference/spark-architecture.html +3 -3
  130. package/public/docs/tuning-reference/table-formats.html +3 -3
  131. package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +3 -3
  132. package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +3 -3
  133. package/public/docs/user-guide/alternative-log-retrieval.html +3 -3
  134. package/public/docs/user-guide/getting-started/ci-and-automation.html +8 -7
  135. package/public/docs/user-guide/getting-started/reading-the-dashboard.html +3 -3
  136. package/public/docs/user-guide/getting-started/regression-metric-keys.html +3 -3
  137. package/public/docs/user-guide/getting-started/tuning-thresholds.html +3 -3
  138. package/public/docs/user-guide/getting-started.html +3 -3
  139. package/public/docs/user-guide/mcp-tools/compare-runs.html +7 -6
  140. package/public/docs/user-guide/mcp-tools/diagnose-run.html +3 -3
  141. package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +5 -5
  142. package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +3 -3
  143. package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +3 -3
  144. package/public/docs/user-guide/mcp-tools/get-reference-doc.html +3 -3
  145. package/public/docs/user-guide/mcp-tools/get-run-summary.html +3 -3
  146. package/public/docs/user-guide/mcp-tools/list-runs.html +3 -3
  147. package/public/docs/user-guide/mcp-tools.html +3 -3
  148. package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +5 -5
  149. package/public/docs/user-guide/run-comparison.html +5 -5
  150. package/public/docs/user-guide/understanding-findings.html +5 -5
  151. package/public/export-template.html +3 -3
  152. package/public/index.html +3 -3
  153. package/vendor-core/cli/budgets.js +62 -2
  154. package/vendor-core/cli/regression-budgets.js +35 -0
  155. package/vendor-core/comparison-output.js +2 -1
  156. package/vendor-core/core-source-hash.txt +1 -1
  157. package/vendor-core/detectors.js +219 -56
  158. package/vendor-core/docs-content/detection/chrn.md +3 -1
  159. package/vendor-core/docs-content/detection/cold.md +2 -1
  160. package/vendor-core/docs-content/detection/plan.md +9 -2
  161. package/vendor-core/docs-content/detection/shfl.md +8 -2
  162. package/vendor-core/docs-content/detection/skew.md +11 -5
  163. package/vendor-core/docs-content/detection/slow.md +6 -2
  164. package/vendor-core/docs-content/detection/spill.md +4 -1
  165. package/vendor-core/docs-content/detection/strag.md +5 -3
  166. package/vendor-core/docs-content/detection/tiny.md +3 -2
  167. package/vendor-core/evidence-report.js +11 -11
  168. package/vendor-core/finding-presentation.js +39 -13
  169. package/vendor-core/finding-types.js +50 -6
  170. package/vendor-core/mcp-server-factory.js +6 -2
  171. package/vendor-core/mcp-tools.js +10 -1
  172. package/vendor-core/median.js +8 -0
  173. package/vendor-core/run-comparison.js +2 -0
  174. package/vendor-core/sequence-alignment.js +83 -0
  175. package/vendor-core/stage-alignment.js +387 -63
  176. package/vendor-core/stage-quantiles.js +3 -1
  177. package/vendor-core/stage-structure.js +83 -0
  178. package/public/assets/CachingOpportunity-D_O_WkbH.js +0 -1
  179. package/public/assets/CoreUsageArea-csgwNxkl.js +0 -1
  180. package/public/assets/DisclosureContext-B0siMVdg.js +0 -1
  181. package/public/assets/PlanGraphRoute-pqnHTt4i.js +0 -1
  182. package/public/assets/Spill-B4DVKHXx.js +0 -1
  183. package/public/assets/StageFindingGroup-D9naK109.js +0 -1
  184. package/public/assets/index-BcqTLKFp.css +0 -2
  185. package/public/assets/index-DCeWge12.js +0 -13
  186. package/public/assets/useRecentFiles-C3Ov47U7.js +0 -11
  187. package/public/docs/assets/chunks/@localSearchIndexroot.D4OdkQjy.js +0 -1
  188. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.js +0 -1
  189. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.js +0 -1
  190. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.js +0 -1
  191. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.lean.js +0 -1
  192. package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.js +0 -1
  193. package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.lean.js +0 -1
  194. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.js +0 -1
  195. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.lean.js +0 -1
  196. package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.js +0 -1
  197. package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.lean.js +0 -1
  198. /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
  199. /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
@@ -3,16 +3,20 @@
3
3
  // pairs. `compareRuns` calls it, and every surface that compares runs reads its result
4
4
  // (`stagePairs`) instead of comparing stage keys.
5
5
  //
6
- // Pairing keys on `comparisonIdentity`: the `stageIdentity` recipe run with the comparison
7
- // normalizer, so the volatile tokens that make two runs of one job differ (staging directories,
8
- // dates, IN-lists, Delta log file counts) and the caller's `--normalize-path`
9
- // patterns do not split a stage in two. `stageIdentity` itself stays the frozen exact key.
6
+ // Pairing runs in two levels (see `alignStages`). The exact key is `comparisonIdentity`: the
7
+ // `stageIdentity` recipe run with the comparison normalizer, so the volatile tokens that make two
8
+ // runs of one job differ (staging directories, dates, IN-lists, Delta log file counts) and the
9
+ // caller's `--normalize-path` patterns do not split a stage in two. A coarser structural key
10
+ // (`stage-structure.ts`) and a detail similarity pair what the exact key splits. `stageIdentity`
11
+ // itself stays the frozen exact key.
10
12
 
11
13
  import { normalizeDetail } from './detectors.js';
12
14
  import { isDeltaLogRead, scanRelationId } from './plan-summary.js';
13
15
  import { withEarlierAttempts, totalExecutorCpuMs } from './run-totals.js';
14
16
  import { planNodesOfStage } from './stage-plan-nodes.js';
15
- import { identityIndexWith, pairEqualCounts, stageIdentityWith, } from './stage-identity.js';
17
+ import { normalizeStageName, stageIdentityWith, } from './stage-identity.js';
18
+ import { alignSequences } from './sequence-alignment.js';
19
+ import { attributeNames, detailTokens, jaccard, structuralKey } from './stage-structure.js';
16
20
 
17
21
 
18
22
 
@@ -136,29 +140,83 @@ export const PAIR_DELTA_METRICS = [
136
140
 
137
141
 
138
142
 
143
+
144
+
145
+
139
146
 
140
-
147
+
148
+
141
149
 
142
150
 
143
151
 
152
+
153
+ /** The stages an aligned execution pair left unpaired when its two sides ran different numbers of
154
+ * stages: a different plan (a broadcast join in place of an exchange and a sort-merge join), not
155
+ * changed work. `deltas` totals each metric over the leftover stages of each side. */
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+
164
+
165
+
166
+
167
+
168
+
169
+
170
+
171
+
172
+
173
+
174
+
175
+
176
+
177
+
178
+
179
+
144
180
 
145
181
 
146
182
 
147
-
183
+
148
184
 
149
-
150
-
185
+
186
+
151
187
 
152
188
 
153
-
154
-
189
+
190
+
191
+
155
192
 
193
+
156
194
 
157
195
 
158
196
 
159
197
 
160
198
 
161
199
 
200
+
201
+ /** An execution pair scores at least this (0-1) on call site, description and plan structure to be
202
+ * matched. */
203
+ export const EXECUTION_MATCH_MIN = 0.55;
204
+ /** Share of the smaller run's executions that must pair (see `ExecutionAlignment.agreement`) for the two runs
205
+ * to count as one job. Below it no stage pairs: stages as generic as a `count` or a `collect` repeat
206
+ * across unrelated jobs, and only the surrounding executions tell the jobs apart. */
207
+ export const MIN_EXECUTION_AGREEMENT = 0.5;
208
+ /** Detail similarity (Jaccard over a stage's name and normalized node text) two stages of different
209
+ * structure need to pair as `aligned`. */
210
+ export const ALIGNED_MIN_SIMILARITY = 0.6;
211
+ /** Score of a pair made by position alone: no plan evidence backs it. */
212
+ export const POSITIONAL_SCORE = 0.5;
213
+ /** Execution similarities cached at most (distinct signatures in one run times those in the other). */
214
+ const MAX_CACHED_SIMILARITIES = 1_000_000;
215
+ /** Leftover stage pairs scored in one execution pair; above it the `aligned` pass is skipped. */
216
+ const MAX_STAGE_SIMILARITY_CELLS = 250_000;
217
+ /** Whether replanned run time counts in the numerator of `runtimeCoverage` (and so toward the `ok`
218
+ * gate). When false it counts in the denominator only. */
219
+ export const REPLANNED_COUNTS_TOWARD_COVERAGE = true;
162
220
 
163
221
  // Sum of a field over the stage's own record and the work its figures leave out (earlier
164
222
  // attempts, a failed attempt's late tasks), the same all-attempt sums the run totals use.
@@ -172,7 +230,9 @@ function attemptSum(rows , field
172
230
  return present ? sum : null;
173
231
  }
174
232
 
175
- function stageFigures(stage ) {
233
+
234
+
235
+ function stageFigures(stage ) {
176
236
  const rows = withEarlierAttempts([stage]);
177
237
  return {
178
238
  executorRunTime: attemptSum(rows, 'executorRunTime'),
@@ -186,77 +246,341 @@ function stageFigures(stage ) {
186
246
  };
187
247
  }
188
248
 
249
+ function deltasOf(b , c ) {
250
+ const deltas = {} ;
251
+ for (const metric of PAIR_DELTA_METRICS) {
252
+ const [bv, cv] = [b[metric], c[metric]];
253
+ deltas[metric] = { baseline: bv, candidate: cv, delta: bv != null && cv != null ? cv - bv : null };
254
+ }
255
+ return deltas;
256
+ }
257
+
258
+ /** Each metric summed over several stages' figures; null where no stage recorded it. */
259
+ function sumFigures(rows ) {
260
+ const total = {} ;
261
+ for (const metric of PAIR_DELTA_METRICS) {
262
+ let sum = 0, present = false;
263
+ for (const row of rows) if (row[metric] != null) { sum += row[metric] ; present = true; }
264
+ total[metric] = present ? sum : null;
265
+ }
266
+ return total;
267
+ }
268
+
189
269
  const ascending = (a , b ) => a - b;
190
270
 
191
- /** Pairs the stages of two runs. At this stage the pairing is `matchStages`' algorithm over
192
- * `comparisonIdentity`: stages sharing an identity pair off by sorted id when each run has the same
193
- * count of them, and every pair is `exact`. */
271
+ // ---- Per-run index --------------------------------------------------------------------------
272
+
273
+ // What the aligner knows about one SQL execution (or the pseudo-execution of stages outside any):
274
+ // the non-bookkeeping stages it ran and the three things its similarity reads.
275
+
276
+
277
+
278
+
279
+
280
+
281
+
282
+
283
+
284
+
285
+
286
+
287
+
288
+
289
+
290
+
291
+
292
+
293
+
294
+
295
+
296
+
297
+
298
+
299
+
300
+
301
+
302
+ // Stack frames of the JVM and Spark themselves say nothing about which user code ran.
303
+ const FRAMEWORK_FRAME = /^(java|javax|jdk|sun|scala|py4j|org\.apache|io\.delta|io\.netty|com\.sun)\b/;
304
+
305
+ function userFrames(details ) {
306
+ const frames = [];
307
+ for (const line of (details ?? '').split('\n')) {
308
+ const frame = line.trim().replace(/:\d+\)$/, ')'); // a shifted line number is not a different call site
309
+ if (frame && !FRAMEWORK_FRAME.test(frame)) frames.push(frame);
310
+ }
311
+ return frames;
312
+ }
313
+
314
+ // Directory prefixes carry the run's own paths (a container directory, an application id), so only
315
+ // each path's last segment stays in a description.
316
+ const descriptionTokens = (text , normalizer ) =>
317
+ [...new Set(normalizer(text).replace(/\S*\/([^/\s]+)/g, '$1').split(/[^A-Za-z0-9_.$]+/).filter(Boolean))].sort();
318
+
319
+ // An execution's plan structure: its operator names (a repeated operator counts as often as it
320
+ // repeats) and the attribute names its nodes mention, which tell two plans of the same operators
321
+ // apart. Neither carries a literal, a path or an id.
322
+ function planStructure(root ) {
323
+ const tokens = [];
324
+ const attributes = new Set ();
325
+ (function walk(node ) {
326
+ if (!/^(WholeStageCodegen|InputAdapter|ColumnarToRow)\b/.test(node.name ?? '')) {
327
+ tokens.push(normalizeStageName(node.name ?? ''));
328
+ for (const name of attributeNames(node.detail ?? '')) attributes.add(`@${name}`);
329
+ }
330
+ for (const child of node.children ?? []) walk(child);
331
+ })(root);
332
+ return [...tokens, ...attributes].sort();
333
+ }
334
+
335
+ function indexRun(
336
+ snap , normalizer , signatures , interned ,
337
+ ) {
338
+ const intern = (tokens ) => {
339
+ const ids = new Int32Array(tokens.length);
340
+ tokens.forEach((t, i) => {
341
+ let id = interned.get(t);
342
+ if (id === undefined) { id = interned.size; interned.set(t, id); }
343
+ ids[i] = id;
344
+ });
345
+ return ids.sort();
346
+ };
347
+ const nodesOf = new Map ();
348
+ const bookkeeping = new Set ();
349
+ const exactKey = new Map ();
350
+ const structuralKeyOf = new Map ();
351
+ const byExecution = new Map ();
352
+ for (const [id, stage] of snap.stages) {
353
+ const nodes = planNodesOfStage(stage, snap.sql);
354
+ nodesOf.set(id, nodes);
355
+ if (isDeltaBookkeepingStage(stage, snap.sql, nodes)) { bookkeeping.add(id); continue; }
356
+ exactKey.set(id, comparisonIdentity(stage, snap, normalizer, nodes));
357
+ structuralKeyOf.set(id, structuralKey(nodes));
358
+ const execId = stage.sqlExecutionId ?? null;
359
+ const ids = byExecution.get(execId);
360
+ if (ids) ids.push(id); else byExecution.set(execId, [id]);
361
+ }
362
+ const info = (execId , stageIds ) => {
363
+ stageIds.sort(ascending);
364
+ const root = execId === null ? null : snap.sql.get(execId)?.planTree ?? null;
365
+ const structure = root ? planStructure(root)
366
+ : stageIds.map((id) => structuralKeyOf.get(id) ?? `n:${normalizeStageName(snap.stages.get(id) .name ?? '')}`).sort();
367
+ const frames = [...new Set(stageIds.flatMap((id) => userFrames(snap.stages.get(id) .details)))].sort();
368
+ const description = execId === null ? [] : descriptionTokens(snap.sql.get(execId)?.description ?? '', normalizer);
369
+ const text = JSON.stringify([structure, frames, description]);
370
+ let signature = signatures.get(text);
371
+ if (signature === undefined) { signature = signatures.size; signatures.set(text, signature); }
372
+ return { id: execId, stageIds, structure: intern(structure), frames: intern(frames), description: intern(description), signature };
373
+ };
374
+ const executions = [...byExecution.keys()].filter((k) => k !== null).sort(ascending)
375
+ .map((execId) => info(execId, byExecution.get(execId) ));
376
+ const pseudoIds = byExecution.get(null);
377
+ return {
378
+ snap, nodesOf, bookkeeping, exactKey, structuralKey: structuralKeyOf, executions,
379
+ pseudo: pseudoIds ? info(null, pseudoIds) : null,
380
+ };
381
+ }
382
+
383
+ // Weights of the three parts of an execution pair's similarity. A part one side lacks (a PySpark
384
+ // stack trace has no user frames, a description may be empty) drops out of the mean.
385
+ const STRUCTURE_WEIGHT = 4, FRAMES_WEIGHT = 2, DESCRIPTION_WEIGHT = 1;
386
+
387
+ /** 0-1000 (rounded, so the alignment compares integers). */
388
+ function executionSimilarity(a , b ) {
389
+ let sum = STRUCTURE_WEIGHT * jaccard(a.structure, b.structure), weight = STRUCTURE_WEIGHT;
390
+ if (a.frames.length > 0 && b.frames.length > 0) { sum += FRAMES_WEIGHT * jaccard(a.frames, b.frames); weight += FRAMES_WEIGHT; }
391
+ if (a.description.length > 0 && b.description.length > 0) {
392
+ sum += DESCRIPTION_WEIGHT * jaccard(a.description, b.description);
393
+ weight += DESCRIPTION_WEIGHT;
394
+ }
395
+ return Math.round((1000 * sum) / weight);
396
+ }
397
+
398
+ // ---- Stage alignment inside one execution pair -----------------------------------------------
399
+
400
+
401
+
402
+ function alignWithin(
403
+ base , cand , be , ce ,
404
+ tokens , normalizer ,
405
+ ) {
406
+ const pseudo = be.id === null;
407
+ const freeBase = new Set(be.stageIds), freeCand = new Set(ce.stageIds);
408
+ const pairs = [];
409
+ const take = (baseId , candId , quality , score ) => {
410
+ pairs.push({ baseId, candId, quality, score });
411
+ freeBase.delete(baseId);
412
+ freeCand.delete(candId);
413
+ };
414
+ const tokensOf = (run , cache , id ) => {
415
+ let t = cache.get(id);
416
+ if (!t) { const stage = run.snap.stages.get(id) ; t = detailTokens(stage.name, run.nodesOf.get(id) , normalizer); cache.set(id, t); }
417
+ return t;
418
+ };
419
+ const similarity = (baseId , candId ) =>
420
+ jaccard(tokensOf(base, tokens.base, baseId), tokensOf(cand, tokens.cand, candId));
421
+ const group = (free , keyOf ) => {
422
+ const groups = new Map ();
423
+ for (const id of [...free].sort(ascending)) {
424
+ const key = keyOf(id);
425
+ if (key === null) continue;
426
+ const ids = groups.get(key);
427
+ if (ids) ids.push(id); else groups.set(key, [id]);
428
+ }
429
+ return groups;
430
+ };
431
+ // Stages sharing a key pair off in id order, as many as both sides have: a self-join's subtree
432
+ // counted three times in one run and twice in the other still pairs two.
433
+ const pairByKey = (
434
+ keyOfBase , keyOfCand ,
435
+ quality , scoreOf ,
436
+ ) => {
437
+ const b = group(freeBase, keyOfBase), c = group(freeCand, keyOfCand);
438
+ for (const key of [...b.keys()].sort()) {
439
+ const candIds = c.get(key);
440
+ if (!candIds) continue;
441
+ const baseIds = b.get(key) ;
442
+ for (let i = 0; i < Math.min(baseIds.length, candIds.length); i++) take(baseIds[i], candIds[i], quality, scoreOf(baseIds[i], candIds[i]));
443
+ }
444
+ };
445
+
446
+ // Stages outside any SQL execution have no plan: the stage's own call-site text tells them apart.
447
+ const detailsKey = (run , id ) => normalizeStageName(normalizer(run.snap.stages.get(id) .details ?? ''));
448
+ pairByKey(
449
+ (id) => base.exactKey.get(id) + (pseudo ? `§${detailsKey(base, id)}` : ''),
450
+ (id) => cand.exactKey.get(id) + (pseudo ? `§${detailsKey(cand, id)}` : ''),
451
+ 'exact', () => 1,
452
+ );
453
+ pairByKey((id) => base.structuralKey.get(id) ?? null, (id) => cand.structuralKey.get(id) ?? null, 'structural', similarity);
454
+
455
+ // Different structure, similar details: best score first, ties by lower ids.
456
+ const withStructure = (run , free ) => [...free].filter((id) => run.structuralKey.get(id) != null).sort(ascending);
457
+ const leftBase = withStructure(base, freeBase), leftCand = withStructure(cand, freeCand);
458
+ if (leftBase.length * leftCand.length <= MAX_STAGE_SIMILARITY_CELLS) {
459
+ const candidates = [];
460
+ for (const baseId of leftBase) for (const candId of leftCand) {
461
+ const score = similarity(baseId, candId);
462
+ if (score >= ALIGNED_MIN_SIMILARITY) candidates.push({ baseId, candId, score });
463
+ }
464
+ candidates.sort((x, y) => y.score - x.score || x.baseId + x.candId - (y.baseId + y.candId) || x.baseId - y.baseId);
465
+ for (const { baseId, candId, score } of candidates) {
466
+ if (freeBase.has(baseId) && freeCand.has(candId)) take(baseId, candId, 'aligned', score);
467
+ }
468
+ }
469
+
470
+ // No plan structure to compare: pair by position among the stages of the same name.
471
+ const byName = (run ) => (id ) =>
472
+ run.structuralKey.get(id) == null ? normalizeStageName(run.snap.stages.get(id) .name ?? '') : null;
473
+ pairByKey(byName(base), byName(cand), 'aligned', () => POSITIONAL_SCORE);
474
+
475
+ return { pairs, leftBase: [...freeBase].sort(ascending), leftCand: [...freeCand].sort(ascending) };
476
+ }
477
+
478
+ /** Pairs the stages of two runs in two levels. SQL executions align first, in submission order, by an
479
+ * order-preserving sequence alignment scored on call site, description and plan structure (the
480
+ * stages outside any execution form one pseudo-execution per run); then the stages of each aligned
481
+ * execution pair align: equal comparison key (`exact`), equal structural key (`structural`), similar
482
+ * details (`aligned`), or position among plan-less stages of one name (`aligned`). Stages an
483
+ * execution pair with different stage counts leaves over are `replanned`; with equal counts they
484
+ * stay `unmatched`. Deterministic, and swapping the runs mirrors the pairs. */
194
485
  export function alignStages(baseSnap , candSnap , options = {}) {
195
486
  const normalizer = comparisonDetailNormalizer(options.normalizePath);
196
- // One walk of the plan per stage serves the bookkeeping test and the identity.
197
- const index = (snap ) => {
198
- const nodesOf = new Map ();
199
- const bookkeeping = new Set ();
200
- for (const [id, stage] of snap.stages) {
201
- const nodes = planNodesOfStage(stage, snap.sql);
202
- nodesOf.set(id, nodes);
203
- if (isDeltaBookkeepingStage(stage, snap.sql, nodes)) bookkeeping.add(id);
204
- }
205
- const identities = identityIndexWith(
206
- snap, (stage, id) => comparisonIdentity(stage, snap, normalizer, nodesOf.get(id)), (id) => !bookkeeping.has(id),
207
- );
208
- return { identities, bookkeeping };
487
+ const signatures = new Map ();
488
+ const interned = new Map ();
489
+ const base = indexRun(baseSnap, normalizer, signatures, interned), cand = indexRun(candSnap, normalizer, signatures, interned);
490
+
491
+ // Execution similarity is cached per signature pair, so a loop of identical queries scores once.
492
+ // When the runs hold so many distinct signatures that the cache could not repay its memory, it is off.
493
+ const distinct = (run ) => new Set(run.executions.map((e) => e.signature)).size;
494
+ const cached = distinct(base) * distinct(cand) <= MAX_CACHED_SIMILARITIES;
495
+ const cache = new Map ();
496
+ const similarityOf = (a , b ) => {
497
+ if (a.signature === b.signature) return 1000;
498
+ if (!cached) return executionSimilarity(a, b);
499
+ const key = a.signature * signatures.size + b.signature;
500
+ let s = cache.get(key);
501
+ if (s === undefined) { s = executionSimilarity(a, b); cache.set(key, s); }
502
+ return s;
209
503
  };
210
- const base = index(baseSnap), cand = index(candSnap);
211
- const baseBook = base.bookkeeping, candBook = cand.bookkeeping;
212
- const { pairs: idPairs } = pairEqualCounts(base.identities, cand.identities);
504
+ const sequence = alignSequences(
505
+ base.executions.length, cand.executions.length,
506
+ (i, j) => similarityOf(base.executions[i], cand.executions[j]), Math.round(1000 * EXECUTION_MATCH_MIN),
507
+ (i) => base.executions[i].id , (j) => cand.executions[j].id ,
508
+ );
509
+ const smaller = Math.min(base.executions.length, cand.executions.length);
510
+ const agreement = smaller > 0 ? sequence.pairs.length / smaller : null;
511
+ const accepted = agreement === null || agreement >= MIN_EXECUTION_AGREEMENT;
512
+
513
+ const executionPairs = [];
514
+ if (accepted) {
515
+ for (const [i, j] of sequence.pairs) executionPairs.push([base.executions[i], cand.executions[j]]);
516
+ if (base.pseudo && cand.pseudo) executionPairs.push([base.pseudo, cand.pseudo]);
517
+ }
213
518
 
214
- const baseFigures = new Map ();
215
- const candFigures = new Map ();
216
- const figures = (cache , snap , id ) => {
519
+ const baseFigures = new Map (), candFigures = new Map ();
520
+ const figures = (cache , snap , id ) => {
217
521
  let f = cache.get(id);
218
522
  if (!f) { f = stageFigures(snap.stages.get(id) ); cache.set(id, f); }
219
523
  return f;
220
524
  };
525
+ const figuresOf = (side , ids ) =>
526
+ ids.map((id) => side === 'base' ? figures(baseFigures, baseSnap, id) : figures(candFigures, candSnap, id));
221
527
 
222
- const pairs = idPairs
223
- .map(({ baseId, candId }) => {
224
- const b = figures(baseFigures, baseSnap, baseId), c = figures(candFigures, candSnap, candId);
225
- const deltas = {} ;
226
- for (const metric of PAIR_DELTA_METRICS) {
227
- const [bv, cv] = [b[metric], c[metric]];
228
- deltas[metric] = { baseline: bv, candidate: cv, delta: bv != null && cv != null ? cv - bv : null };
229
- }
230
- return { pairId: `b${baseId}-c${candId}`, baseStageIds: [baseId], candStageIds: [candId], quality: 'exact', score: 1, deltas };
231
- })
232
- .sort((x, y) => x.baseStageIds[0] - y.baseStageIds[0] || x.candStageIds[0] - y.candStageIds[0]);
528
+ const tokens = { base: new Map (), cand: new Map () };
529
+ const pairs = [];
530
+ const replanned = [];
531
+ for (const [be, ce] of executionPairs) {
532
+ const result = alignWithin(base, cand, be, ce, tokens, normalizer);
533
+ for (const { baseId, candId, quality, score } of result.pairs) {
534
+ pairs.push({
535
+ pairId: `b${baseId}-c${candId}`, baseStageIds: [baseId], candStageIds: [candId], quality, score,
536
+ deltas: deltasOf(figures(baseFigures, baseSnap, baseId), figures(candFigures, candSnap, candId)),
537
+ });
538
+ }
539
+ // Different stage counts under one aligned execution: the plan changed, not the work. The
540
+ // pseudo-execution has no plan to re-plan, so its leftovers stay unmatched.
541
+ const leftover = result.leftBase.length + result.leftCand.length > 0;
542
+ if (leftover && be.id !== null && ce.id !== null && be.stageIds.length !== ce.stageIds.length) {
543
+ replanned.push({
544
+ baseExecutionId: be.id, candExecutionId: ce.id, baseStageIds: result.leftBase, candStageIds: result.leftCand,
545
+ deltas: deltasOf(sumFigures(figuresOf('base', result.leftBase)), sumFigures(figuresOf('cand', result.leftCand))),
546
+ });
547
+ }
548
+ }
549
+ pairs.sort((x, y) => x.baseStageIds[0] - y.baseStageIds[0] || x.candStageIds[0] - y.candStageIds[0]);
550
+ replanned.sort((x, y) => x.baseExecutionId - y.baseExecutionId || x.candExecutionId - y.candExecutionId);
233
551
 
234
552
  const pairedBase = new Set(pairs.flatMap((p) => p.baseStageIds));
235
553
  const pairedCand = new Set(pairs.flatMap((p) => p.candStageIds));
236
- const unmatchedOf = (snap , book , paired ) =>
237
- [...snap.stages.keys()].filter((id) => !book.has(id) && !paired.has(id)).sort(ascending);
554
+ const replannedBase = new Set(replanned.flatMap((g) => g.baseStageIds));
555
+ const replannedCand = new Set(replanned.flatMap((g) => g.candStageIds));
556
+ const unmatchedOf = (snap , book , paired , replannedIds ) =>
557
+ [...snap.stages.keys()].filter((id) => !book.has(id) && !paired.has(id) && !replannedIds.has(id)).sort(ascending);
238
558
 
239
559
  // Run time counts every attempt, the same figure the stage-pair deltas and the run totals use.
240
- const runTime = (cache , snap , id ) => figures(cache, snap, id).executorRunTime ?? 0;
241
560
  let total = 0, matched = 0;
242
- for (const id of baseSnap.stages.keys()) {
243
- if (baseBook.has(id)) continue;
244
- const t = runTime(baseFigures, baseSnap, id);
245
- total += t;
246
- if (pairedBase.has(id)) matched += t;
247
- }
248
- for (const id of candSnap.stages.keys()) {
249
- if (candBook.has(id)) continue;
250
- const t = runTime(candFigures, candSnap, id);
251
- total += t;
252
- if (pairedCand.has(id)) matched += t;
253
- }
561
+ const tally = (cache , snap , book , paired , replannedIds ) => {
562
+ for (const id of snap.stages.keys()) {
563
+ if (book.has(id)) continue;
564
+ const t = figures(cache, snap, id).executorRunTime ?? 0;
565
+ total += t;
566
+ if (paired.has(id) || (REPLANNED_COUNTS_TOWARD_COVERAGE && replannedIds.has(id))) matched += t;
567
+ }
568
+ };
569
+ tally(baseFigures, baseSnap, base.bookkeeping, pairedBase, replannedBase);
570
+ tally(candFigures, candSnap, cand.bookkeeping, pairedCand, replannedCand);
254
571
 
255
572
  return {
256
573
  pairs,
257
- unmatched: { baseStageIds: unmatchedOf(baseSnap, baseBook, pairedBase), candStageIds: unmatchedOf(candSnap, candBook, pairedCand) },
258
- replanned: [],
259
- bookkeepingStageIds: { baseStageIds: [...baseBook].sort(ascending), candStageIds: [...candBook].sort(ascending) },
574
+ unmatched: {
575
+ baseStageIds: unmatchedOf(baseSnap, base.bookkeeping, pairedBase, replannedBase),
576
+ candStageIds: unmatchedOf(candSnap, cand.bookkeeping, pairedCand, replannedCand),
577
+ },
578
+ replanned,
579
+ bookkeepingStageIds: { baseStageIds: [...base.bookkeeping].sort(ascending), candStageIds: [...cand.bookkeeping].sort(ascending) },
260
580
  runtimeCoverage: total > 0 ? matched / total : null,
581
+ executionAlignment: {
582
+ baseExecutions: base.executions.length, candExecutions: cand.executions.length,
583
+ pairedExecutions: sequence.pairs.length, bounded: sequence.bounded, agreement, accepted,
584
+ },
261
585
  };
262
586
  }
@@ -1,4 +1,5 @@
1
1
 
2
+ import { medianOfSorted } from './median.js';
2
3
 
3
4
 
4
5
  // Single source of truth for the packed per-task numeric array: FIELDS (offset
@@ -304,6 +305,7 @@ function listScheduleEndMs(
304
305
  return endMs;
305
306
  }
306
307
 
308
+ // p50 is the textbook median (the mean of the two middle values on an even count); p95 is nearest-rank.
307
309
  export function computeFieldQuantiles(arr , fieldIndex ) {
308
310
  const taskCount = arr.length / FIELDS.STRIDE;
309
311
  if (taskCount === 0) return { p50: 0, p95: 0, max: 0 };
@@ -313,7 +315,7 @@ export function computeFieldQuantiles(arr , fieldIndex )
313
315
  values.sort();
314
316
 
315
317
  return {
316
- p50: values[Math.ceil(taskCount * 0.50) - 1],
318
+ p50: medianOfSorted(values),
317
319
  p95: values[Math.ceil(taskCount * 0.95) - 1],
318
320
  max: values[taskCount - 1],
319
321
  };
@@ -0,0 +1,83 @@
1
+ // What the stage aligner compares beyond the exact comparison key: a stage's structural key (operator
2
+ // tree shape, node names and sorted attribute names, with literals, paths, file counts and ids
3
+ // removed) and the token sets its similarity scores read. Pure.
4
+
5
+ import { cyrb53 } from './string-hash.js';
6
+ import { normalizeStageName, } from './stage-identity.js';
7
+
8
+
9
+ // Plumbing nodes: codegen boundaries and input adapters change with how Spark fuses a stage, not with
10
+ // the query, so the structural key and the similarity tokens skip them.
11
+ const PLUMBING_NODE = /^(WholeStageCodegen|InputAdapter|ColumnarToRow|RowToColumnar)\b/;
12
+
13
+ const isPlumbing = (node ) => PLUMBING_NODE.test(node.name ?? '');
14
+
15
+ /** The attribute names a plan node's text mentions (`amount#12` gives `amount`), without ids, each
16
+ * once, sorted. Sorting makes the key insensitive to the nondeterministic order in which Spark lists
17
+ * grouping and projection columns; reading only `name#id` references leaves literals out. */
18
+ export function attributeNames(detail ) {
19
+ const names = new Set ();
20
+ for (const m of detail.matchAll(/([A-Za-z_][\w$.]*)#\d+L?/g)) names.add(m[1]);
21
+ return [...names].sort();
22
+ }
23
+
24
+ /** The structural key of a stage from the plan nodes attributed to it, or null when nothing but
25
+ * plumbing is attributed. The nodes form a forest (a node's parent is its nearest attributed
26
+ * ancestor); the key folds each node's normalized name, its sorted attribute names and its
27
+ * children's digests (sorted, so a join's side order does not matter). Only structure goes in: no
28
+ * node detail, so a literal, a path or a file count never splits two stages. */
29
+ export function structuralKey(nodes ) {
30
+ const attributed = new Set(nodes.filter((n) => !isPlumbing(n)));
31
+ if (attributed.size === 0) return null;
32
+ const digests = new Map ();
33
+ const nested = new Set ();
34
+ // Digests of the nearest attributed descendants of `node`'s children.
35
+ const below = (node ) => {
36
+ const out = [];
37
+ for (const child of node.children ?? []) {
38
+ if (attributed.has(child)) { out.push(digestOf(child)); nested.add(child); }
39
+ else out.push(...below(child));
40
+ }
41
+ return out;
42
+ };
43
+ const digestOf = (node ) => {
44
+ let d = digests.get(node);
45
+ if (d === undefined) {
46
+ d = cyrb53(JSON.stringify([normalizeStageName(node.name ?? ''), attributeNames(node.detail ?? ''), below(node).sort()]));
47
+ digests.set(node, d);
48
+ }
49
+ return d;
50
+ };
51
+ for (const node of attributed) digestOf(node);
52
+ const roots = [...attributed].filter((n) => !nested.has(n)).map(digestOf).sort();
53
+ return cyrb53(JSON.stringify(roots));
54
+ }
55
+
56
+ const TOKEN_SPLIT = /[^A-Za-z0-9_.$]+/;
57
+
58
+ /** Sorted distinct tokens of a stage's attributed nodes (name and normalized detail) and its name.
59
+ * Two stages whose text differs in one literal share nearly every token. */
60
+ export function detailTokens(stageName , nodes , normalize ) {
61
+ const tokens = new Set ();
62
+ const add = (text ) => { for (const t of text.split(TOKEN_SPLIT)) if (t) tokens.add(t); };
63
+ add(normalizeStageName(stageName ?? ''));
64
+ for (const node of nodes) {
65
+ if (isPlumbing(node)) continue;
66
+ add(normalizeStageName(node.name ?? ''));
67
+ add(normalize(node.detail ?? ''));
68
+ }
69
+ return [...tokens].sort();
70
+ }
71
+
72
+ /** Jaccard similarity of two sorted arrays; 1 when both are empty. An element repeated in both counts
73
+ * as often as it repeats in the shorter side (multiset Jaccard), so distinct arrays give the plain one. */
74
+ export function jaccard(a , b ) {
75
+ if (a.length === 0 && b.length === 0) return 1;
76
+ let i = 0, j = 0, both = 0;
77
+ while (i < a.length && j < b.length) {
78
+ if (a[i] === b[j]) { both++; i++; j++; }
79
+ else if (a[i] < b[j]) i++;
80
+ else j++;
81
+ }
82
+ return both / (a.length + b.length - both);
83
+ }
@@ -1 +0,0 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{M as t}from"./charts-vendor-0VMaLVLN.js";import{E as n,Y as r,_t as i,d as a,p as o}from"./DisclosureContext-B0siMVdg.js";import{p as s}from"./plan-graph-vendor-BYl-AzN5.js";import{h as c,m as l}from"./useRecentFiles-C3Ov47U7.js";import{C as u,F as d,I as f,L as p,M as m,N as h,O as g,R as _}from"./index-DCeWge12.js";import{o as v,r as y,t as b}from"./Dashboard-BgKodJl2.js";import{n as x}from"./finding-anchor-Cys8Z6Yn.js";import{t as S}from"./usePagedRows-BxvdFnPk.js";var C={name:`git-merge`,size:24,node:[[`circle`,{cx:`18`,cy:`18`,r:`3`,key:`1xkwt0`}],[`circle`,{cx:`6`,cy:`6`,r:`3`,key:`1lh9wr`}],[`path`,{d:`M6 21V9a9 9 0 0 0 9 9`,key:`7kw0sc`}]]};C.node;var w=i(C),T=e(t(),1),E=s();function D({relations:e,operator:t}){let n=t===`union`?`union`:`join`;return(0,E.jsx)(`span`,{className:`inline-flex flex-wrap items-baseline`,children:e.map((e,t)=>(0,E.jsxs)(`span`,{children:[t>0?(0,E.jsxs)(E.Fragment,{children:[` `,(0,E.jsx)(`span`,{className:`mx-1 font-semibold uppercase text-xs tracking-wide text-primary`,children:n}),` `]}):null,e.relation]},e.relation))})}function O({finding:e}){let t=x([e]),i=e.variant===`composite`;return(0,E.jsxs)(_,{ref:t.ref,tabIndex:t.tabIndex,"data-flashed":t.dataFlashed,className:`transition-colors ${t.flashClassName}`,children:[(0,E.jsx)(d,{children:(0,E.jsxs)(`span`,{className:`inline-flex items-center gap-2`,children:[i?(0,E.jsx)(w,{className:`size-3.5 text-muted-foreground`,"aria-hidden":`true`}):null,i&&e.relations&&e.relations.length>0?(0,E.jsx)(D,{relations:e.relations,operator:e.operator}):(0,E.jsx)(`span`,{children:e.relation}),i?(0,E.jsx)(n,{variant:`outline`,className:`text-xs uppercase border-dashed`,children:e.operator?.toUpperCase()}):(0,E.jsx)(n,{variant:`secondary`,className:`text-xs uppercase`,children:e.format})]})}),(0,E.jsxs)(d,{children:[e.value,` queries`]}),(0,E.jsx)(d,{className:`text-right`,children:r(e.totalReadBytes)}),(0,E.jsxs)(d,{children:[(0,E.jsx)(b,{finding:e}),(0,E.jsx)(o,{children:(0,E.jsx)(c,{confidence:e.confidence,validationRequired:e.validationRequired})})]})]})}var k=(0,T.memo)(function({catalog:e,defaultCollapsed:t=!0}){let[n,r]=(0,T.useState)(0),i=(0,T.useMemo)(()=>l(e,`cachingOpportunity`).sort((e,t)=>t.totalReadBytes-e.totalReadBytes||(t.value??0)-(e.value??0)),[e]),s=g(),c=s?i.findIndex(e=>e===s.finding):null,{totalPages:d,effectivePage:b,visible:x}=S(i,n,r,c);if(i.length===0)return null;let C=i[0];return(0,E.jsx)(u,{title:`Caching Opportunities`,fixFor:i,impactBand:`info`,badges:(0,E.jsx)(a,{type:`cachingOpportunity`,impactBand:`info`}),defaultCollapsed:t,summary:(0,E.jsx)(v,{value:`${C.value}×`,context:`read by ${C.value} queries: ${C.relation}`}),children:(0,E.jsxs)(`div`,{className:`space-y-3`,children:[(0,E.jsxs)(m,{children:[(0,E.jsx)(p,{children:(0,E.jsxs)(_,{children:[(0,E.jsx)(f,{children:`Relation`}),(0,E.jsx)(f,{children:`Read by`}),(0,E.jsx)(f,{className:`text-right`,children:`Data read`}),(0,E.jsx)(f,{children:`Estimate`})]})}),(0,E.jsx)(h,{children:x.map((e,t)=>(0,E.jsx)(O,{finding:e},`${e.format}:${e.relation}:${(e.executionIds??[]).join(`,`)}:${t}`))})]}),(0,E.jsx)(o,{children:(0,E.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Reuse is inferred from plan structure, not confirmed by execution. Verify before caching.`})}),(0,E.jsx)(y,{page:b,totalPages:d,onPrev:()=>r(e=>e-1),onNext:()=>r(e=>e+1)})]})})});export{k as CachingOpportunity};
@@ -1 +0,0 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{M as t,T as n,f as r,n as i,o as a,s as o,u as s,v as c}from"./charts-vendor-0VMaLVLN.js";import{B as l,S as u,X as d,b as f,d as p,p as m,u as h}from"./DisclosureContext-B0siMVdg.js";import{p as g}from"./plan-graph-vendor-BYl-AzN5.js";import{_,b as v,h as y,y as b}from"./useRecentFiles-C3Ov47U7.js";import{C as x}from"./index-DCeWge12.js";import{c as S,f as C,o as w,p as T,r as E,t as D}from"./Dashboard-BgKodJl2.js";import{t as O}from"./usePagedRows-BxvdFnPk.js";var k=e(t(),1),A=g(),j=240,M={PROCESS_LOCAL:`Process-local`,NODE_LOCAL:`Node-local`,RACK_LOCAL:`Rack-local`,NO_PREF:`No preference`,ANY:`Any`,OTHER:`Other`,idle:`Idle`},N={PROCESS_LOCAL:_.clean,NODE_LOCAL:_.accent,RACK_LOCAL:_.warning,NO_PREF:_.muted,ANY:_.critical,OTHER:_.muted,idle:_.muted},P=`Core Usage by Locality`,F=(0,k.memo)(function({catalog:e,defaultCollapsed:t=!0}){let[g,F]=(0,k.useState)(0),I=f()?.data.coreLocality,L=(0,k.useMemo)(()=>{let e=I?.chart??{hasActivity:!1};return e.hasActivity?{...e,sampled:S(e.points)}:e},[I]),R=I?.ratio.topStages??[],z=e.find(e=>e.type===`coreLocality`),B=e.some(e=>e.type===`memoryUtilization`&&e.variant===`idleCores`),V=R.filter(e=>e.nonLocalTasks>0),{totalPages:H,effectivePage:U,visible:W}=O(V,g,F);if(!L.hasActivity)return(0,A.jsx)(x,{title:P,summary:(0,A.jsx)(`span`,{className:`text-muted-foreground text-xs`,children:`No stage activity to plot.`}),children:(0,A.jsx)(C,{tone:`neutral`,icon:T,title:`No stage activity to plot.`})});let{order:G,points:K,sampled:q,peakCores:J}=L;return(0,A.jsx)(x,{title:P,impactBand:z?.impactBand,badges:z?(0,A.jsx)(p,{type:`coreLocality`,impactBand:z.impactBand}):null,defaultCollapsed:t,summary:(0,A.jsx)(w,{value:`${d(J)} cores`,context:`busy at the peak`}),children:(0,A.jsxs)(`div`,{className:`space-y-2`,children:[(0,A.jsx)(m,{children:(0,A.jsxs)(`p`,{className:`text-muted-foreground text-xs`,children:[`approximate: stage-level attribution`,z?null:(0,A.jsxs)(A.Fragment,{children:[`, see `,(0,A.jsx)(u,{anchor:`#bottleneck-utilization`,children:`how utilization is measured`})]})]})}),z?(0,A.jsxs)(`p`,{className:`flex flex-wrap items-start gap-2 text-sm`,children:[(0,A.jsx)(h,{impactBand:z.impactBand,className:`mt-1.5`}),(0,A.jsxs)(`span`,{children:[(0,A.jsxs)(`strong`,{children:[z.value,`% non-local`]}),`. `,l(z)]})]}):null,z?(0,A.jsx)(D,{finding:z}):null,(0,A.jsx)(m,{children:z?(0,A.jsx)(y,{confidence:z.confidence,validationRequired:z.validationRequired}):null}),(0,A.jsx)(v,{ariaLabel:`Approximate concurrent core usage over time, stacked by task locality tier, with idle capacity shown. See the legend for tier colors.`,height:j,table:{caption:`Concurrent core usage over time by locality tier`,columns:[`Time (s)`,...G.map(e=>M[e]??e)],rows:K.map(e=>[e.t,...G.map(t=>Math.round(e[t]))]),align:Array.from({length:G.length+1},()=>`right`)},children:(0,A.jsxs)(i,{data:q,children:[(0,A.jsx)(r,{vertical:!1,stroke:_.muted,strokeOpacity:.2}),(0,A.jsx)(o,{dataKey:`t`,tick:{fontSize:10},label:{value:`seconds from app start`,position:`insideBottom`,offset:-2,fontSize:10}}),(0,A.jsx)(a,{tick:{fontSize:10},width:36,label:{value:`avg concurrent cores`,angle:-90,position:`insideLeft`,fontSize:10}}),(0,A.jsx)(c,{...b}),(0,A.jsx)(n,{wrapperStyle:{fontSize:10}}),G.map(e=>(0,A.jsx)(s,{type:`monotone`,dataKey:e,name:M[e]??e,stackId:`cores`,stroke:N[e]??_.muted,fill:N[e]??_.muted,fillOpacity:e===`idle`?.15:.8,isAnimationActive:!1},e))]})}),(0,A.jsx)(m,{children:W.length>0?(0,A.jsxs)(A.Fragment,{children:[(0,A.jsx)(`ul`,{className:`space-y-1 text-xs text-muted-foreground`,children:W.map(e=>(0,A.jsxs)(`li`,{children:[`Stage `,e.stageId,`: `,Math.round(e.ratio*100),`% non-local (`,e.nonLocalTasks,`/`,e.taskCount,` tasks)`]},e.stageId))}),(0,A.jsx)(E,{page:U,totalPages:H,onPrev:()=>F(e=>e-1),onNext:()=>F(e=>e+1)})]}):null}),(0,A.jsx)(m,{children:B?(0,A.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Idle cores also flagged → see Memory Utilization widget`}):null})]})})});export{F as CoreUsageArea};