@fx-data/ai-diagnostics 0.1.20261026070913 → 0.1.20261107142011

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 (254) hide show
  1. package/AGENT_SETUP.md +9 -6
  2. package/README.md +5 -5
  3. package/bin/core-ai.mjs +241 -25
  4. package/content/AGENTS.md +37 -57
  5. package/content/README.md +10 -41
  6. package/content/docs/architecture.md +45 -74
  7. package/content/docs/diagnostic-feedback.md +20 -38
  8. package/content/docs/diagnostic-report.md +19 -46
  9. package/content/docs/index.md +40 -128
  10. package/content/docs/scenario-guide.md +34 -52
  11. package/content/knowledge/README.md +49 -0
  12. package/content/knowledge/cases/README.md +60 -0
  13. package/content/knowledge/cases/dashboard-parameter-deep-binding.md +32 -0
  14. package/content/knowledge/common/README.md +31 -0
  15. package/content/knowledge/common/concepts.md +49 -0
  16. package/content/knowledge/common/{knowledge-applicability.md → conclusions.md} +10 -6
  17. package/content/knowledge/common/queries.md +98 -0
  18. package/content/knowledge/common/signals.md +58 -0
  19. package/content/knowledge/common/topics/baseline-comparison.md +23 -0
  20. package/content/knowledge/common/topics/large-results.md +12 -0
  21. package/content/knowledge/common/topics/linked-traces.md +11 -0
  22. package/content/knowledge/common/topics/query-budget.md +18 -0
  23. package/content/knowledge/common/topics/query-example.md +69 -0
  24. package/content/knowledge/common/topics/setup-signoz-mcp.md +102 -0
  25. package/content/knowledge/common/topics/start-and-anchors.md +19 -0
  26. package/content/knowledge/common/workflow.md +22 -0
  27. package/content/knowledge/component/README.md +26 -0
  28. package/content/knowledge/component/concepts.md +22 -0
  29. package/content/knowledge/component/conclusions.md +11 -0
  30. package/content/knowledge/component/queries.md +7 -0
  31. package/content/knowledge/component/signals.md +75 -0
  32. package/content/knowledge/component/workflow.md +23 -0
  33. package/content/knowledge/dashboard/README.md +31 -0
  34. package/content/knowledge/dashboard/concepts.md +26 -0
  35. package/content/knowledge/dashboard/conclusions.md +21 -0
  36. package/content/knowledge/dashboard/queries.md +69 -0
  37. package/content/knowledge/dashboard/signals.md +76 -0
  38. package/content/knowledge/dashboard/topics/component-comparison.md +9 -0
  39. package/content/knowledge/dashboard/topics/dynamic-fields.md +21 -0
  40. package/content/knowledge/dashboard/topics/filter-tree.md +35 -0
  41. package/content/knowledge/dashboard/topics/name-binding.md +9 -0
  42. package/content/knowledge/dashboard/workflow.md +35 -0
  43. package/content/knowledge/dataset/README.md +27 -0
  44. package/content/knowledge/dataset/concepts.md +30 -0
  45. package/content/knowledge/dataset/conclusions.md +11 -0
  46. package/content/knowledge/dataset/queries.md +7 -0
  47. package/content/knowledge/dataset/signals.md +61 -0
  48. package/content/knowledge/dataset/topics/steps.md +59 -0
  49. package/content/knowledge/dataset/workflow.md +27 -0
  50. package/content/knowledge/engine/README.md +27 -0
  51. package/content/knowledge/engine/concepts.md +80 -0
  52. package/content/knowledge/engine/conclusions.md +49 -0
  53. package/content/knowledge/engine/queries.md +117 -0
  54. package/content/knowledge/engine/signals.md +73 -0
  55. package/content/knowledge/engine/topics/metric-fields.md +104 -0
  56. package/content/knowledge/engine/topics/patterns.md +50 -0
  57. package/content/knowledge/engine/workflow.md +20 -0
  58. package/content/knowledge/signal-index.md +117 -0
  59. package/content/knowledge/update/README.md +39 -0
  60. package/content/knowledge/update/concepts.md +93 -0
  61. package/content/knowledge/update/conclusions.md +59 -0
  62. package/content/knowledge/update/queries.md +103 -0
  63. package/content/knowledge/update/signals.md +57 -0
  64. package/content/knowledge/update/topics/correlations.md +63 -0
  65. package/content/knowledge/update/topics/discover-executions.md +31 -0
  66. package/content/knowledge/update/topics/events-execution.md +136 -0
  67. package/content/knowledge/update/topics/events-intake.md +102 -0
  68. package/content/knowledge/update/topics/events-outcome.md +163 -0
  69. package/content/knowledge/update/topics/lifecycle.md +58 -0
  70. package/content/knowledge/update/topics/scene-producers.md +258 -0
  71. package/content/knowledge/update/workflow.md +29 -0
  72. package/content-manifest.json +73 -192
  73. package/package.json +2 -2
  74. package/content/.agents/skills/analyze-analysis-table-update-runtime/SKILL.md +0 -35
  75. package/content/.agents/skills/analyze-analysis-table-update-runtime/agents/openai.yaml +0 -7
  76. package/content/.agents/skills/analyze-analysis-table-update-runtime/references/discover-update-executions.md +0 -34
  77. package/content/.agents/skills/analyze-analysis-table-update-runtime/references/update-diagnostic-contract.md +0 -101
  78. package/content/.agents/skills/analyze-engine-metric/SKILL.md +0 -76
  79. package/content/.agents/skills/analyze-engine-metric/agents/openai.yaml +0 -13
  80. package/content/.agents/skills/analyze-engine-metric/scripts/analyze_engine_metric.py +0 -270
  81. package/content/.agents/skills/analyze-engine-metric/scripts/engine_metric_analysis/__init__.py +0 -11
  82. package/content/.agents/skills/analyze-engine-metric/scripts/engine_metric_analysis/metric_analyzer.py +0 -791
  83. package/content/.agents/skills/analyze-engine-metric/scripts/engine_metric_analysis/signoz_extract.py +0 -278
  84. package/content/.agents/skills/diagnose-component-performance/SKILL.md +0 -52
  85. package/content/.agents/skills/diagnose-component-performance/agents/openai.yaml +0 -7
  86. package/content/.agents/skills/diagnose-component-performance/references/experience.md +0 -34
  87. package/content/.agents/skills/diagnose-dashboard-performance/SKILL.md +0 -92
  88. package/content/.agents/skills/diagnose-dashboard-performance/agents/openai.yaml +0 -7
  89. package/content/.agents/skills/diagnose-dashboard-performance/references/experience.md +0 -63
  90. package/content/.agents/skills/diagnose-dataset-performance/SKILL.md +0 -52
  91. package/content/.agents/skills/diagnose-dataset-performance/agents/openai.yaml +0 -7
  92. package/content/.agents/skills/diagnose-dataset-performance/references/experience.md +0 -33
  93. package/content/.agents/skills/diagnose-performance/SKILL.md +0 -77
  94. package/content/.agents/skills/diagnose-performance/agents/openai.yaml +0 -7
  95. package/content/.agents/skills/diagnose-update-performance/SKILL.md +0 -55
  96. package/content/.agents/skills/diagnose-update-performance/agents/openai.yaml +0 -7
  97. package/content/.agents/skills/diagnose-update-performance/references/experience.md +0 -47
  98. package/content/.agents/skills/setup-signoz-mcp/SKILL.md +0 -30
  99. package/content/.agents/skills/setup-signoz-mcp/agents/openai.yaml +0 -7
  100. package/content/.agents/skills/setup-signoz-mcp/references/installation.md +0 -75
  101. package/content/.agents/skills/setup-signoz-mcp/references/windows-artifact.md +0 -14
  102. package/content/docs/npm-cli.md +0 -202
  103. package/content/docs/roadmap.md +0 -44
  104. package/content/docs/skill-writing-guide.md +0 -65
  105. package/content/knowledge/common/signoz-environments.json +0 -52
  106. package/content/knowledge/common/signoz-environments.md +0 -55
  107. package/content/knowledge/common/signoz-query-example.md +0 -41
  108. package/content/knowledge/common/signoz-query-policy.md +0 -145
  109. package/content/knowledge/common/trace-routing.md +0 -63
  110. package/content/knowledge/component/observability.md +0 -60
  111. package/content/knowledge/dashboard/observability.md +0 -168
  112. package/content/knowledge/dashboard/query-examples.md +0 -63
  113. package/content/knowledge/dataset/observability.md +0 -68
  114. package/content/knowledge/engine/metric-artifacts.md +0 -25
  115. package/content/knowledge/engine/metric-retrieval.md +0 -107
  116. package/content/knowledge/engine/metric-semantics.md +0 -172
  117. package/content/knowledge/engine/performance-patterns.md +0 -86
  118. package/content/knowledge/update/diagnostic/manifest.json +0 -209
  119. package/content/knowledge/update/diagnostic/observability/edges/edge-attempt-to-capacity-deferral.json +0 -17
  120. package/content/knowledge/update/diagnostic/observability/edges/edge-attempt-to-lock-requeue.json +0 -18
  121. package/content/knowledge/update/diagnostic/observability/edges/edge-attempt-to-merge.json +0 -17
  122. package/content/knowledge/update/diagnostic/observability/edges/edge-candidate-to-effective-root.json +0 -16
  123. package/content/knowledge/update/diagnostic/observability/edges/edge-capacity-deferral-to-attempt.json +0 -16
  124. package/content/knowledge/update/diagnostic/observability/edges/edge-chain-node-to-execution-detail.json +0 -18
  125. package/content/knowledge/update/diagnostic/observability/edges/edge-dlx-recovery-to-attempt.json +0 -16
  126. package/content/knowledge/update/diagnostic/observability/edges/edge-effective-execution-to-listener-before.json +0 -21
  127. package/content/knowledge/update/diagnostic/observability/edges/edge-effective-root-to-chain.json +0 -17
  128. package/content/knowledge/update/diagnostic/observability/edges/edge-engine-task-to-primary-result.json +0 -17
  129. package/content/knowledge/update/diagnostic/observability/edges/edge-execution-detail-to-publication.json +0 -16
  130. package/content/knowledge/update/diagnostic/observability/edges/edge-init-recovery-to-publication.json +0 -16
  131. package/content/knowledge/update/diagnostic/observability/edges/edge-listener-complete-to-child-release.json +0 -21
  132. package/content/knowledge/update/diagnostic/observability/edges/edge-listener-to-derived-chain.json +0 -21
  133. package/content/knowledge/update/diagnostic/observability/edges/edge-load-decision-to-load-operation.json +0 -16
  134. package/content/knowledge/update/diagnostic/observability/edges/edge-load-operation-to-engine-task.json +0 -17
  135. package/content/knowledge/update/diagnostic/observability/edges/edge-lock-requeue-to-attempt.json +0 -16
  136. package/content/knowledge/update/diagnostic/observability/edges/edge-logical-parent-to-child.json +0 -18
  137. package/content/knowledge/update/diagnostic/observability/edges/edge-merge-member-to-effective-execution.json +0 -18
  138. package/content/knowledge/update/diagnostic/observability/edges/edge-merge-to-load-decision.json +0 -17
  139. package/content/knowledge/update/diagnostic/observability/edges/edge-no-load-to-primary-result.json +0 -16
  140. package/content/knowledge/update/diagnostic/observability/edges/edge-primary-result-to-child-frontier.json +0 -18
  141. package/content/knowledge/update/diagnostic/observability/edges/edge-primary-result-to-listener.json +0 -23
  142. package/content/knowledge/update/diagnostic/observability/edges/edge-publication-to-attempt.json +0 -16
  143. package/content/knowledge/update/diagnostic/observability/edges/edge-ready-recovery-to-publication.json +0 -16
  144. package/content/knowledge/update/diagnostic/observability/edges/edge-request-to-candidate-root.json +0 -16
  145. package/content/knowledge/update/diagnostic/observability/identity-units/candidate-root.json +0 -13
  146. package/content/knowledge/update/diagnostic/observability/identity-units/dag-node.json +0 -14
  147. package/content/knowledge/update/diagnostic/observability/identity-units/effective-root.json +0 -14
  148. package/content/knowledge/update/diagnostic/observability/identity-units/engine-task.json +0 -13
  149. package/content/knowledge/update/diagnostic/observability/identity-units/execution-attempt.json +0 -12
  150. package/content/knowledge/update/diagnostic/observability/identity-units/execution-detail.json +0 -13
  151. package/content/knowledge/update/diagnostic/observability/identity-units/listener-invocation.json +0 -11
  152. package/content/knowledge/update/diagnostic/observability/identity-units/load-decision.json +0 -12
  153. package/content/knowledge/update/diagnostic/observability/identity-units/load-operation.json +0 -11
  154. package/content/knowledge/update/diagnostic/observability/identity-units/logical-dag-edge.json +0 -11
  155. package/content/knowledge/update/diagnostic/observability/identity-units/merge-decision.json +0 -12
  156. package/content/knowledge/update/diagnostic/observability/identity-units/node-primary-result.json +0 -12
  157. package/content/knowledge/update/diagnostic/observability/identity-units/queue-publication.json +0 -11
  158. package/content/knowledge/update/diagnostic/observability/identity-units/requeue.json +0 -11
  159. package/content/knowledge/update/diagnostic/observability/identity-units/update-chain.json +0 -13
  160. package/content/knowledge/update/diagnostic/observability/identity-units/update-request.json +0 -12
  161. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-after-commit-delivery.json +0 -131
  162. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-attempt-consumed.json +0 -175
  163. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-candidate-root-filter.json +0 -119
  164. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-capacity-deferred.json +0 -121
  165. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-child-release.json +0 -146
  166. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-dlx-recovery.json +0 -100
  167. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-effective-root-selection.json +0 -111
  168. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-engine-handoff.json +0 -171
  169. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-execution-frontier-persisted.json +0 -115
  170. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-init-recovery.json +0 -93
  171. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-intent-accepted.json +0 -151
  172. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-listener-derived-chain.json +0 -112
  173. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-listener-invocation.json +0 -154
  174. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-load-decision.json +0 -155
  175. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-load-operation.json +0 -108
  176. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-lock-requeued.json +0 -108
  177. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-logical-dependency-expansion.json +0 -129
  178. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-merge-decision.json +0 -174
  179. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-primary-node-result.json +0 -318
  180. package/content/knowledge/update/diagnostic/observability/lookups/analysis-update-ready-recovery.json +0 -92
  181. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-business-attempt.json +0 -33
  182. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-capacity-cycle.json +0 -31
  183. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-child-release.json +0 -32
  184. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-commit-publication.json +0 -29
  185. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-control-planning.json +0 -30
  186. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-engine-task.json +0 -27
  187. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-listener-callback.json +0 -27
  188. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-load-operation.json +0 -27
  189. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-lock-cycle.json +0 -31
  190. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-main-queue-wait.json +0 -32
  191. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-merge-evaluation.json +0 -29
  192. package/content/knowledge/update/diagnostic/observability/performance-segments/analysis-update-segment-parent-dependency-wait.json +0 -31
  193. package/content/knowledge/update/diagnostic/observability-index.json +0 -558
  194. package/content/knowledge/update/diagnostic/recipe-index.json +0 -85
  195. package/content/knowledge/update/diagnostic/recipes/dag-chain.json +0 -676
  196. package/content/knowledge/update/diagnostic/recipes/performance-window.json +0 -1128
  197. package/content/knowledge/update/diagnostic/recipes/scene-origin-async-message-or-callback.json +0 -438
  198. package/content/knowledge/update/diagnostic/recipes/scene-origin-derived-update-chain.json +0 -510
  199. package/content/knowledge/update/diagnostic/recipes/scene-origin-direct-service-call.json +0 -438
  200. package/content/knowledge/update/diagnostic/recipes/scene-origin-http.json +0 -438
  201. package/content/knowledge/update/diagnostic/recipes/scene-origin-internal-event.json +0 -438
  202. package/content/knowledge/update/diagnostic/recipes/scene-origin-job.json +0 -438
  203. package/content/knowledge/update/diagnostic/recipes/scene-origin-recovery-or-migration.json +0 -438
  204. package/content/knowledge/update/diagnostic/recipes/table-history.json +0 -508
  205. package/content/knowledge/update/diagnostic/recipes/table-in-chain.json +0 -595
  206. package/content/knowledge/update/diagnostic/scene-index.json +0 -252
  207. package/content/knowledge/update/diagnostic/scenes/active.json +0 -47
  208. package/content/knowledge/update/diagnostic/scenes/add.json +0 -140
  209. package/content/knowledge/update/diagnostic/scenes/agent.json +0 -20
  210. package/content/knowledge/update/diagnostic/scenes/archive.json +0 -32
  211. package/content/knowledge/update/diagnostic/scenes/auth-change.json +0 -116
  212. package/content/knowledge/update/diagnostic/scenes/call-back.json +0 -32
  213. package/content/knowledge/update/diagnostic/scenes/case-install.json +0 -20
  214. package/content/knowledge/update/diagnostic/scenes/case-replace.json +0 -7
  215. package/content/knowledge/update/diagnostic/scenes/cdc-schedule.json +0 -20
  216. package/content/knowledge/update/diagnostic/scenes/compute-model.json +0 -20
  217. package/content/knowledge/update/diagnostic/scenes/copy-data.json +0 -32
  218. package/content/knowledge/update/diagnostic/scenes/corp-linked.json +0 -7
  219. package/content/knowledge/update/diagnostic/scenes/corp-member-add.json +0 -20
  220. package/content/knowledge/update/diagnostic/scenes/corp-member-del.json +0 -7
  221. package/content/knowledge/update/diagnostic/scenes/corp-member-modify.json +0 -32
  222. package/content/knowledge/update/diagnostic/scenes/corp-member-sync.json +0 -152
  223. package/content/knowledge/update/diagnostic/scenes/corp-setting-member-rename.json +0 -152
  224. package/content/knowledge/update/diagnostic/scenes/corp-setting.json +0 -20
  225. package/content/knowledge/update/diagnostic/scenes/cross-project-allow.json +0 -32
  226. package/content/knowledge/update/diagnostic/scenes/default.json +0 -32
  227. package/content/knowledge/update/diagnostic/scenes/del.json +0 -20
  228. package/content/knowledge/update/diagnostic/scenes/dynamic-time.json +0 -20
  229. package/content/knowledge/update/diagnostic/scenes/edit.json +0 -92
  230. package/content/knowledge/update/diagnostic/scenes/excel-append.json +0 -44
  231. package/content/knowledge/update/diagnostic/scenes/excel-save.json +0 -32
  232. package/content/knowledge/update/diagnostic/scenes/folder-copy.json +0 -32
  233. package/content/knowledge/update/diagnostic/scenes/global.json +0 -56
  234. package/content/knowledge/update/diagnostic/scenes/handover.json +0 -32
  235. package/content/knowledge/update/diagnostic/scenes/history-handle.json +0 -20
  236. package/content/knowledge/update/diagnostic/scenes/manual-update.json +0 -44
  237. package/content/knowledge/update/diagnostic/scenes/migration.json +0 -20
  238. package/content/knowledge/update/diagnostic/scenes/param-change.json +0 -20
  239. package/content/knowledge/update/diagnostic/scenes/polars-error.json +0 -32
  240. package/content/knowledge/update/diagnostic/scenes/preview-exception.json +0 -20
  241. package/content/knowledge/update/diagnostic/scenes/preview-invalid.json +0 -20
  242. package/content/knowledge/update/diagnostic/scenes/project-excel-move.json +0 -20
  243. package/content/knowledge/update/diagnostic/scenes/re-check.json +0 -7
  244. package/content/knowledge/update/diagnostic/scenes/replace.json +0 -20
  245. package/content/knowledge/update/diagnostic/scenes/rollback.json +0 -68
  246. package/content/knowledge/update/diagnostic/scenes/save-as.json +0 -44
  247. package/content/knowledge/update/diagnostic/scenes/schedu.json +0 -56
  248. package/content/knowledge/update/diagnostic/scenes/table-move-other-workspace.json +0 -20
  249. package/content/knowledge/update/diagnostic/scenes/user-holder-to-normal.json +0 -20
  250. package/content/knowledge/update/observability.md +0 -16
  251. package/content/knowledge/update/runtime-evidence.md +0 -32
  252. package/content/scripts/receive_tool_result.py +0 -57
  253. /package/content/{.agents/skills/setup-signoz-mcp/scripts → scripts}/install_signoz_mcp.ps1 +0 -0
  254. /package/content/{.agents/skills/setup-signoz-mcp/scripts → scripts}/install_signoz_mcp.py +0 -0
@@ -1,791 +0,0 @@
1
- """Metric analysis utilities for MPP OLAP execution logs.
2
-
3
- Goals:
4
- - Parse once in streaming mode (large files friendly).
5
- - Map operator plan id (e.g. `<proc> Project@34255 [1]`) to PHYSICAL node id.
6
- - Preserve raw values, source lines and explicit structure without diagnosis.
7
- """
8
- from __future__ import annotations
9
-
10
- from dataclasses import asdict, dataclass, field
11
- import hashlib
12
- import math
13
- from pathlib import Path
14
- import re
15
- from typing import Any, Dict, List, Optional, Tuple
16
-
17
-
18
- _KEY_VALUE_PATTERN = re.compile(r"^(?P<key>[A-Za-z0-9_:@\[\]\-]+)\s*=\s*(?P<value>.+)$")
19
- _OPERATOR_PATTERN = re.compile(
20
- r"^<(?P<kind>[^>]+)>\s+(?P<name>[^@]+)@(?P<node>-?\d+)\s+\[(?P<plan>[^\]]+)\]$"
21
- )
22
- _JIT_BACKEND_PATTERN = re.compile(r"^jit_backend\[(?P<name>[^\]]+)\]\s*:$")
23
- _JOB_FIELD_PATTERN = re.compile(r"([A-Za-z0-9_]+)=('(?:[^'\\]|\\.)*'|[^,\]]+)")
24
- _TASK_STATE_TIME_PATTERN = re.compile(r",\s*TaskStateTime\[(?P<fields>[^\]]*)\]\]\s*$")
25
- _PHYSICAL_NODE_PATTERN = re.compile(
26
- r"PHYSICAL_(?P<type>[A-Z_]+):\s+\[nodeId:\s*(?P<id>-?\d+),\s*(?P<rest>.+)$"
27
- )
28
- _SUBPLAN_HEADER_PATTERN = re.compile(
29
- r"SubPlan:\s*\[Id:\s*(?P<id>[^\]\s]+)(?:\s+ParentId:\s*(?P<parent>[^\]\s]+))?"
30
- )
31
- _ANALYZED_NODE_PATTERN = re.compile(
32
- r"^(?P<prefix>\.*\s*)\[(?P<id>-?\d+)\](?P<name>[A-Za-z0-9_]+Node)\s*(?P<detail>.*)$"
33
- )
34
- _SIGNOZ_SUMMARY_KEYS = {
35
- "elapsed_time",
36
- "execution_time",
37
- "total_tasks",
38
- "load_balance_cost",
39
- "lb_queue_cost",
40
- "queue_cost",
41
- "optimizer1_cost",
42
- "optimizer2_cost",
43
- "distribution_cost",
44
- "send_cost",
45
- "peak_memory",
46
- }
47
-
48
- @dataclass
49
- class PhysicalNode:
50
- node_id: int
51
- node_type: str
52
- subplan_id: Optional[str]
53
- summary: str
54
- line_no: int = 0
55
-
56
-
57
- @dataclass
58
- class SubPlanInfo:
59
- subplan_id: str
60
- parent_id: Optional[str] = None
61
- input_distribution: Optional[str] = None
62
- output_distribution: Optional[str] = None
63
- line_no: int = 0
64
-
65
-
66
- @dataclass
67
- class FragmentEdge:
68
- source_subplan: str
69
- target_subplan: str
70
- edge_type: str
71
- node_id: int
72
- line_no: int
73
-
74
-
75
- @dataclass
76
- class OperatorMetric:
77
- kind: str
78
- name: str
79
- node_id: int
80
- plan_id: str
81
- line_no: int = 0
82
- metric_lines: Dict[str, int] = field(default_factory=dict)
83
- metrics: Dict[str, str] = field(default_factory=dict)
84
-
85
-
86
- @dataclass
87
- class BackendMetric:
88
- name: str
89
- line_no: int
90
- metric_lines: Dict[str, int] = field(default_factory=dict)
91
- metrics: Dict[str, str] = field(default_factory=dict)
92
-
93
-
94
- @dataclass
95
- class PipelineMetric:
96
- stage_id: Optional[str]
97
- task_id: Optional[str]
98
- pipeline_id: int
99
- line_no: int = 0
100
- metric_lines: Dict[str, int] = field(default_factory=dict)
101
- metrics: Dict[str, str] = field(default_factory=dict)
102
- operators: List[OperatorMetric] = field(default_factory=list)
103
- backends: List[BackendMetric] = field(default_factory=list)
104
-
105
-
106
- @dataclass
107
- class ParsedMetric:
108
- # Keep last seen job fields for backward compatibility.
109
- job_fields: Dict[str, str] = field(default_factory=dict)
110
- # Keep all job headers because some files contain multiple tasks/runs.
111
- jobs: List[Dict[str, str]] = field(default_factory=list)
112
- task_fields: Dict[str, Dict[str, str]] = field(default_factory=dict)
113
- # Current native Metric files expose query-level facts before the first pipeline.
114
- query_fields: Dict[str, str] = field(default_factory=dict)
115
- pipelines: List[PipelineMetric] = field(default_factory=list)
116
- physical_nodes: Dict[int, PhysicalNode] = field(default_factory=dict)
117
- physical_node_observations: List[PhysicalNode] = field(default_factory=list)
118
- subplans: Dict[str, SubPlanInfo] = field(default_factory=dict)
119
- subplan_observations: List[SubPlanInfo] = field(default_factory=list)
120
- fragment_edges: List[FragmentEdge] = field(default_factory=list)
121
- summary_metrics: Dict[str, str] = field(default_factory=dict)
122
- analyzed_nodes: List[Dict[str, Any]] = field(default_factory=list)
123
- complete_marker: bool = False
124
- source_lines: List[Dict[str, Any]] = field(default_factory=list)
125
- field_lines: Dict[str, Dict[str, int]] = field(default_factory=dict)
126
- field_units: Dict[str, Dict[str, str]] = field(default_factory=dict)
127
-
128
-
129
- def parse_metric_file(path: Path) -> ParsedMetric:
130
- """Parse a metric/log file into structured data."""
131
- parsed = ParsedMetric()
132
-
133
- current_stage: Optional[str] = None
134
- current_subplan: Optional[str] = None
135
- current_task: Optional[str] = None
136
- current_task_indent = -1
137
- current_pipeline: Optional[PipelineMetric] = None
138
- current_pipeline_indent = -1
139
- current_operator: Optional[OperatorMetric] = None
140
- current_operator_indent = -1
141
- current_backend: Optional[BackendMetric] = None
142
- current_backend_indent = -1
143
-
144
- with path.open("r", encoding="utf-8", errors="replace") as fp:
145
- for line_no, raw in enumerate(fp, start=1):
146
- line = raw.rstrip("\r\n")
147
- if not line:
148
- continue
149
-
150
- parsed.source_lines.append({"line": line_no, "text": line})
151
- if line.strip() == "end_of_metric":
152
- parsed.complete_marker = True
153
- continue
154
-
155
- root_kv = _parse_key_value(line.strip())
156
- if (
157
- root_kv is not None
158
- and root_kv[0] == "jsy_cpp_task_name"
159
- and not parsed.pipelines
160
- ):
161
- parsed.query_fields[root_kv[0]] = _strip_wrapping_quotes(root_kv[1])
162
- parsed.field_lines.setdefault("query", {})[root_kv[0]] = line_no
163
- continue
164
-
165
- if "Job:StandardTask[" in line:
166
- job = _parse_job_line(line)
167
- if job:
168
- parsed.jobs.append(job)
169
- scope = f"job:{len(parsed.jobs)-1}"
170
- parsed.field_lines[scope] = {key: line_no for key in job}
171
- timer = _TASK_STATE_TIME_PATTERN.search(line)
172
- if timer:
173
- parsed.field_units[scope] = {
174
- match.group(1): "ms"
175
- for match in _JOB_FIELD_PATTERN.finditer(timer.group("fields"))
176
- if match.group(1) in _TASK_STATE_TIME_FIELDS
177
- and re.fullmatch(r"null|-?\d+", match.group(2).strip())
178
- }
179
- parsed.job_fields = job
180
-
181
- analyzed_match = _ANALYZED_NODE_PATTERN.match(line)
182
- if analyzed_match:
183
- parsed.analyzed_nodes.append(
184
- {
185
- "line_no": line_no,
186
- "node_id": int(analyzed_match.group("id")),
187
- "node_type": analyzed_match.group("name"),
188
- "depth": analyzed_match.group("prefix").count("."),
189
- "detail": analyzed_match.group("detail").strip(),
190
- }
191
- )
192
- continue
193
-
194
- subplan_info = _parse_subplan_header(line)
195
- if subplan_info is not None:
196
- subplan_id, parent_id, input_dist, output_dist = subplan_info
197
- current_subplan = subplan_id
198
- parsed.subplan_observations.append(SubPlanInfo(subplan_id, parent_id, input_dist, output_dist, line_no))
199
- existing = parsed.subplans.get(subplan_id)
200
- if existing is None:
201
- parsed.subplans[subplan_id] = SubPlanInfo(
202
- subplan_id=subplan_id,
203
- line_no=line_no,
204
- parent_id=parent_id,
205
- input_distribution=input_dist,
206
- output_distribution=output_dist,
207
- )
208
- else:
209
- if parent_id and not existing.parent_id:
210
- existing.parent_id = parent_id
211
- if input_dist and not existing.input_distribution:
212
- existing.input_distribution = input_dist
213
- if output_dist and not existing.output_distribution:
214
- existing.output_distribution = output_dist
215
-
216
- physical_match = _PHYSICAL_NODE_PATTERN.search(line)
217
- if physical_match:
218
- node_id = int(physical_match.group("id"))
219
- node_type = physical_match.group("type").strip()
220
- rest = physical_match.group("rest")
221
- summary = rest.strip()
222
- parsed.physical_nodes[node_id] = PhysicalNode(
223
- node_id=node_id,
224
- line_no=line_no,
225
- node_type=node_type,
226
- subplan_id=current_subplan,
227
- summary=summary,
228
- )
229
- parsed.physical_node_observations.append(parsed.physical_nodes[node_id])
230
- if current_subplan:
231
- for source_fragment in _extract_fragment_ids(rest, "sourceFragmentId"):
232
- parsed.fragment_edges.append(
233
- FragmentEdge(
234
- source_subplan=source_fragment,
235
- target_subplan=current_subplan,
236
- edge_type="remote_source",
237
- node_id=node_id,
238
- line_no=line_no,
239
- )
240
- )
241
- for sink_fragment in _extract_fragment_ids(rest, "sinkFragmentId"):
242
- parsed.fragment_edges.append(
243
- FragmentEdge(
244
- source_subplan=current_subplan,
245
- target_subplan=sink_fragment,
246
- edge_type="exchange_sink",
247
- node_id=node_id,
248
- line_no=line_no,
249
- )
250
- )
251
-
252
- bullet = _extract_bullet(line)
253
- if bullet is None:
254
- continue
255
- indent, body = bullet
256
-
257
- # Exit nested context by indentation.
258
- if current_backend is not None and indent <= current_backend_indent:
259
- current_backend = None
260
- current_backend_indent = -1
261
- if current_operator is not None and indent <= current_operator_indent:
262
- current_operator = None
263
- current_operator_indent = -1
264
- if current_pipeline is not None and indent <= current_pipeline_indent:
265
- current_pipeline = None
266
- current_pipeline_indent = -1
267
- current_operator = None
268
- current_operator_indent = -1
269
- if current_task is not None and indent <= current_task_indent:
270
- current_task = None
271
- current_task_indent = -1
272
-
273
- if body.startswith("stage_info@") and "stage_id=" in body:
274
- current_stage = body.split("stage_id=", 1)[1].strip()
275
- continue
276
-
277
- if body.startswith("task@"):
278
- current_task = body.split()[0][5:]
279
- parsed.task_fields.setdefault(current_task, {})
280
- current_task_indent = indent
281
- continue
282
-
283
- if body.startswith("pipeline@"):
284
- pipeline_id = _parse_pipeline_id(body)
285
- if pipeline_id is None:
286
- continue
287
- current_pipeline = PipelineMetric(
288
- stage_id=current_stage,
289
- task_id=current_task,
290
- pipeline_id=pipeline_id,
291
- line_no=line_no,
292
- )
293
- parsed.pipelines.append(current_pipeline)
294
- current_pipeline_indent = indent
295
- current_operator = None
296
- current_operator_indent = -1
297
- continue
298
-
299
- op_match = _OPERATOR_PATTERN.match(body)
300
- if op_match and current_pipeline is not None:
301
- current_backend = None
302
- current_backend_indent = -1
303
- current_operator = OperatorMetric(
304
- line_no=line_no,
305
- kind=op_match.group("kind"),
306
- name=op_match.group("name").strip(),
307
- node_id=int(op_match.group("node")),
308
- plan_id=op_match.group("plan").strip(),
309
- )
310
- current_pipeline.operators.append(current_operator)
311
- current_operator_indent = indent
312
- continue
313
-
314
- backend_match = _JIT_BACKEND_PATTERN.match(body)
315
- if backend_match and current_pipeline is not None:
316
- current_backend = BackendMetric(backend_match.group("name"), line_no)
317
- current_pipeline.backends.append(current_backend)
318
- current_backend_indent = indent
319
- current_operator = None
320
- current_operator_indent = -1
321
- continue
322
-
323
- kv = _parse_key_value(body)
324
- if kv is None:
325
- continue
326
- key, value = kv
327
-
328
- if key in _SIGNOZ_SUMMARY_KEYS and current_operator is None and current_pipeline is None and current_task is None:
329
- parsed.summary_metrics[key] = value
330
- parsed.field_lines.setdefault("summary", {})[key] = line_no
331
- continue
332
-
333
- if current_operator is not None and indent > current_operator_indent:
334
- current_operator.metrics[key] = value
335
- current_operator.metric_lines[key] = line_no
336
- continue
337
-
338
- if current_backend is not None and indent > current_backend_indent:
339
- current_backend.metrics[key] = value
340
- current_backend.metric_lines[key] = line_no
341
- continue
342
-
343
- if current_pipeline is not None and indent > current_pipeline_indent:
344
- current_pipeline.metrics[key] = value
345
- current_pipeline.metric_lines[key] = line_no
346
- continue
347
-
348
- if current_task is not None:
349
- parsed.task_fields.setdefault(current_task, {})[key] = value
350
- parsed.field_lines.setdefault(f"task:{current_task}", {})[key] = line_no
351
- continue
352
-
353
- if indent == 0 and not parsed.pipelines:
354
- parsed.query_fields[key] = _strip_wrapping_quotes(value)
355
- parsed.field_lines.setdefault("query", {})[key] = line_no
356
-
357
- return parsed
358
-
359
-
360
- def _extract_bullet(line: str) -> Optional[Tuple[int, str]]:
361
- stripped = line.lstrip()
362
- if stripped.startswith("- "):
363
- indent = len(line) - len(stripped)
364
- return indent, stripped[2:].strip()
365
-
366
- marker = " - "
367
- idx = line.find(marker)
368
- if idx < 0:
369
- return None
370
- body = line[idx + len(marker) :].strip()
371
- if not body:
372
- return None
373
- return idx, body
374
-
375
-
376
- def _parse_job_line(line: str) -> Dict[str, str]:
377
- if "Job:StandardTask[" not in line:
378
- return {}
379
- start = line.find("Job:StandardTask[")
380
- payload = line[start + len("Job:StandardTask[") :]
381
- fields: Dict[str, str] = {}
382
- for match in _JOB_FIELD_PATTERN.finditer(payload):
383
- key = match.group(1)
384
- value = match.group(2).strip()
385
- if value.startswith("'") and value.endswith("'"):
386
- value = value[1:-1]
387
- fields[key] = value
388
- return fields
389
-
390
-
391
- def _parse_subplan_header(line: str) -> Optional[Tuple[str, Optional[str], Optional[str], Optional[str]]]:
392
- match = _SUBPLAN_HEADER_PATTERN.search(line)
393
- if not match:
394
- return None
395
- subplan_id = match.group("id").strip()
396
- parent_id = match.group("parent")
397
- if parent_id:
398
- parent_id = parent_id.strip()
399
-
400
- input_dist: Optional[str] = None
401
- output_dist: Optional[str] = None
402
- io_match = re.search(r"input:\s*(?P<input>.+?),\s*output:\s*(?P<output>.+)$", line)
403
- if io_match:
404
- input_dist = io_match.group("input").strip()
405
- output_dist = io_match.group("output").strip()
406
- return subplan_id, parent_id, input_dist, output_dist
407
-
408
-
409
- def _extract_fragment_ids(rest: str, key: str) -> List[str]:
410
- pattern = re.compile(rf"{re.escape(key)}s?:\s*(?P<value>\[[^\]]+\]|[^,\]]+)")
411
- match = pattern.search(rest)
412
- if not match:
413
- return []
414
- value = match.group("value").strip()
415
- if not value:
416
- return []
417
- if value.startswith("[") and value.endswith("]"):
418
- items = [item.strip() for item in value[1:-1].split(",")]
419
- return [item for item in items if item]
420
- return [value]
421
-
422
-
423
- def _parse_key_value(body: str) -> Optional[Tuple[str, str]]:
424
- match = _KEY_VALUE_PATTERN.match(body)
425
- if not match:
426
- return None
427
- key = match.group("key").strip()
428
- value = match.group("value").strip()
429
- return key, value
430
-
431
-
432
- def _strip_wrapping_quotes(value: str) -> str:
433
- text = value.strip()
434
- if len(text) >= 2 and text[0] == text[-1] and text[0] in {"'", '"'}:
435
- return text[1:-1]
436
- return text
437
-
438
-
439
- def _parse_pipeline_id(body: str) -> Optional[int]:
440
- m = re.match(r"pipeline@(\d+)$", body)
441
- if not m:
442
- return None
443
- return int(m.group(1))
444
-
445
-
446
- SCHEMA_VERSION = 2
447
- NORMALIZATION_VERSION = 3
448
- _NUMBER = re.compile(r"(-?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?)\s*([A-Za-zµμ]*)")
449
- _TIME_FACTORS = {"ns": 1e-6, "us": 1e-3, "µs": 1e-3, "μs": 1e-3, "ms": 1, "s": 1000, "min": 60000}
450
- _BYTE_FACTORS = {"bytes": 1, "b": 1, **{u: 1024 ** n for n, units in enumerate((("kb", "kib"), ("mb", "mib"), ("gb", "gib"), ("tb", "tib")), 1) for u in units}}
451
- _JOB_PHASE_FIELDS = {"queue_time", "wait_resource_time", "plan_time", "start_time", "finish_time"}
452
- _TASK_STATE_TIME_FIELDS = _JOB_PHASE_FIELDS | {"run_time"}
453
-
454
-
455
- def normalize_value(raw: Any, *, key: str = "", line: int | None = None) -> Dict[str, Any]:
456
- """Normalize explicit values; never substitute missing evidence with zero."""
457
- result = {"field": key, "raw": raw, "value": None, "unit": None, "aggregation": "raw", "availability": "missing", "line": line}
458
- if raw is None or str(raw).strip().lower() in {"", "null", "none", "n/a"}:
459
- return result
460
- text = str(raw).strip()
461
- if text.startswith("{") and text.endswith("}"):
462
- entries = {}
463
- for part in text[1:-1].split(","):
464
- if "=" not in part:
465
- return {**result, "availability": "invalid"}
466
- name, value = part.split("=", 1)
467
- name = name.strip()
468
- if name not in {"sum", "max", "min", "avg", "count"} or name in entries:
469
- return {**result, "availability": "invalid"}
470
- entries[name] = normalize_value(value.strip(), key="count" if name == "count" else key, line=line)
471
- # Keep each statistic. Counts describe sample count, not row/byte totals.
472
- preference = ("sum", "max", "avg", "min") if "rows" in key or "cols" in key else ("max", "sum", "avg", "min")
473
- chosen = next((name for name in preference if name in entries), None)
474
- selected = entries.get(chosen, result)
475
- return {**result, **{k: selected[k] for k in ("value", "unit", "availability")}, "aggregation": chosen, "statistics": entries}
476
- match = _NUMBER.fullmatch(text)
477
- if not match:
478
- return {**result, "availability": "invalid"}
479
- numeric, suffix = match.groups()
480
- numeric = numeric.replace(",", "")
481
- value = float(numeric) if "." in numeric else int(numeric)
482
- suffix = suffix.lower()
483
- if suffix in _TIME_FACTORS:
484
- value, unit = value * _TIME_FACTORS[suffix], "ms"
485
- elif suffix in _BYTE_FACTORS:
486
- value, unit = value * _BYTE_FACTORS[suffix], "bytes"
487
- elif suffix:
488
- return {**result, "availability": "invalid"}
489
- elif key in _JOB_PHASE_FIELDS:
490
- unit = None
491
- else:
492
- unit = "ms" if key.endswith("_ms") or key == "run_time" else ("bytes" if key.endswith("_bytes") else "count")
493
- if not math.isfinite(value):
494
- return {**result, "availability": "invalid"}
495
- return {**result, "value": value if value >= 0 else None, "unit": unit, "availability": "observed" if value >= 0 else "not_produced"}
496
-
497
-
498
- def _fields(
499
- raw: Dict[str, str],
500
- lines: Dict[str, int],
501
- source_units: Dict[str, str] | None = None,
502
- ) -> Dict[str, Any]:
503
- fields = {key: normalize_value(value, key=key, line=lines.get(key)) for key, value in raw.items()}
504
- for key, unit in (source_units or {}).items():
505
- fields[key]["unit"] = unit
506
- return fields
507
-
508
-
509
- def _calculation(left: Dict[str, Any], right: Dict[str, Any]) -> Dict[str, Any]:
510
- """Only compare observations with the same unit and aggregation."""
511
- sources = {"left": left, "right": right}
512
- if left["availability"] != "observed" or right["availability"] != "observed":
513
- return {"delta": None, "ratio": None, "reason": "unavailable_operand", "sources": sources}
514
- if left["unit"] is None or right["unit"] is None:
515
- return {"delta": None, "ratio": None, "reason": "unknown_unit", "sources": sources}
516
- if (left["unit"], left["aggregation"]) != (right["unit"], right["aggregation"]):
517
- return {"delta": None, "ratio": None, "reason": "incompatible_units_or_aggregations", "sources": sources}
518
- a, b = left["value"], right["value"]
519
- return {"delta": b - a, "ratio": b / a if a != 0 else None, "reason": "zero_denominator" if a == 0 else None, "sources": sources}
520
-
521
-
522
- def build_report(path: Path, *, top: int = 5) -> Dict[str, Any]:
523
- parsed = parse_metric_file(path)
524
- pipelines, transitions = [], []
525
- physical_by_id: Dict[int, list] = {}
526
- for physical in parsed.physical_node_observations:
527
- physical_by_id.setdefault(physical.node_id, []).append(physical)
528
- for index, pipeline in enumerate(parsed.pipelines):
529
- row = {"stage_id": pipeline.stage_id, "task_id": pipeline.task_id, "pipeline_id": pipeline.pipeline_id,
530
- "line_no": pipeline.line_no, "metrics": _fields(pipeline.metrics, pipeline.metric_lines), "operators": []}
531
- row["backends"] = [
532
- {"name": backend.name, "line_no": backend.line_no,
533
- "metrics": _fields(backend.metrics, backend.metric_lines)}
534
- for backend in pipeline.backends
535
- ]
536
- for op_index, operator in enumerate(pipeline.operators):
537
- candidates = physical_by_id.get(int(operator.plan_id), []) if operator.plan_id.lstrip("-").isdigit() else []
538
- physical = candidates[0] if len(candidates) == 1 else None
539
- op = {"kind": operator.kind, "name": operator.name, "node_id": operator.node_id, "plan_id": operator.plan_id,
540
- "line_no": operator.line_no, "metrics": _fields(operator.metrics, operator.metric_lines),
541
- "physical_node": asdict(physical) if physical else None,
542
- "physical_node_candidate_lines": [v.line_no for v in candidates]}
543
- row["operators"].append(op)
544
- counts = {k: op["metrics"].get(k, normalize_value(None, key=k)) for k in ("input_rows_num", "output_rows_num", "scanned_rows_num", "output_cols_num")}
545
- calc = _calculation(counts["input_rows_num"], counts["output_rows_num"])
546
- transitions.append({"pipeline_index": index, "operator_index": op_index, "stage_id": pipeline.stage_id,
547
- "task_id": pipeline.task_id, "pipeline_id": pipeline.pipeline_id, "name": operator.name,
548
- "kind": operator.kind, "plan_id": operator.plan_id, "physical_node": op["physical_node"],
549
- "counts": counts, "input_output": calc})
550
- pipelines.append(row)
551
- known = bool(parsed.jobs or parsed.query_fields or parsed.summary_metrics or parsed.pipelines or parsed.analyzed_nodes or parsed.physical_nodes)
552
- detailed = any(p["operators"] for p in pipelines)
553
- task_names = list(dict.fromkeys([j["taskName"] for j in parsed.jobs if j.get("taskName")] + ([parsed.query_fields["jsy_cpp_task_name"]] if parsed.query_fields.get("jsy_cpp_task_name") else [])))
554
- return {"schema_version": SCHEMA_VERSION, "normalization_version": NORMALIZATION_VERSION,
555
- "source": str(path.resolve()), "body_sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
556
- "task_names": task_names, "input_format": "full_metric" if detailed else ("signoz_metric_log" if known else "unknown"),
557
- "complete": parsed.complete_marker, "collection_status": ("format_error" if not known else "body_incomplete" if not parsed.complete_marker else "complete_detail" if detailed else "complete_summary"),
558
- "jobs": [{"raw": job, "fields": _fields(job, parsed.field_lines.get(f"job:{i}", {}),
559
- parsed.field_units.get(f"job:{i}", {}))}
560
- for i, job in enumerate(parsed.jobs)],
561
- "query_fields": _fields(parsed.query_fields, parsed.field_lines.get("query", {})),
562
- "summary_metrics": _fields(parsed.summary_metrics, parsed.field_lines.get("summary", {})),
563
- "tasks": {task: _fields(fields, parsed.field_lines.get(f"task:{task}", {})) for task, fields in parsed.task_fields.items()},
564
- "subplans": [asdict(v) for v in parsed.subplan_observations], "physical_nodes": [asdict(v) for v in parsed.physical_node_observations],
565
- "fragment_edges": [asdict(edge) for edge in parsed.fragment_edges], "analyzed_nodes": parsed.analyzed_nodes,
566
- "pipelines": pipelines, "data_scale_transitions": transitions, "source_lines": parsed.source_lines,
567
- "coverage": {"jobs": len(parsed.jobs), "pipelines": len(pipelines), "operators": len(transitions), "source_lines": len(parsed.source_lines),
568
- "tasks": len(parsed.task_fields), "subplans": len(parsed.subplan_observations), "physical_nodes": len(parsed.physical_node_observations), "analyzed_nodes": len(parsed.analyzed_nodes)}}
569
-
570
-
571
- def build_execution_overview(report: Dict[str, Any], *, top: int = 5) -> Dict[str, Any]:
572
- """Expose bounded facts from each recorded scope without merging scopes."""
573
- scope_fields = ("query_peak_memory", "query_current_memory", "query_memory_limit", "resultRows", "writtenRows")
574
- scopes = []
575
- for pointer, fields in [("/query_fields", report["query_fields"]), *[
576
- ("/tasks/" + task.replace("~", "~0").replace("/", "~1"), fields)
577
- for task, fields in report["tasks"].items()
578
- ]]:
579
- if any(key in fields for key in scope_fields):
580
- scopes.append({"report_pointer": pointer,
581
- "fields": {key: fields.get(key, normalize_value(None, key=key)) for key in scope_fields}})
582
-
583
- pipelines, operator_counts = [], {}
584
- resource_fields = ("read_io_file_bytes", "read_block_time", "io_task_pending_time",
585
- "fetch_block_meta_time", "store_tuple_arena_physical", "hash_table_arena_physical",
586
- "arena_physical_allocated", "main_arena_physical_allocated",
587
- "secondary_arena_physical_allocated", "partition_allocated_physical",
588
- "partition_arena_allocated_physical")
589
- backend_fields = (
590
- "compile_wall_time", "compile_cost_wall_time", "compile_cost_cpu_time",
591
- "cache_look_up_time", "llvm_predict_compile_time", "morsel_count",
592
- "morsel_active_wall_time", "morsel_active_cpu_time",
593
- )
594
- for index, pipeline in enumerate(report["pipelines"]):
595
- fields = pipeline["metrics"]
596
- wall_key = "pipeline_wall_time_ms" if "pipeline_wall_time_ms" in fields else "pipeline_wall_time"
597
- wall = fields.get(wall_key, normalize_value(None, key=wall_key))
598
- resources = [{"report_pointer": f"/pipelines/{index}/operators/{oi}", "name": operator["name"],
599
- "plan_id": operator["plan_id"],
600
- "fields": {key: operator["metrics"][key] for key in
601
- ("input_rows_num", "output_rows_num", "output_cols_num", "scanned_rows_num", *resource_fields)
602
- if key in operator["metrics"]}}
603
- for oi, operator in enumerate(pipeline["operators"])
604
- if any(key in operator["metrics"] for key in resource_fields)]
605
- pipelines.append({"report_pointer": f"/pipelines/{index}", "pipeline_id": pipeline["pipeline_id"],
606
- "task_id": pipeline["task_id"], "wall_time": wall,
607
- "parallelism": fields.get("pipeline_parallelism", normalize_value(None, key="pipeline_parallelism")),
608
- "activity": {key: fields[key] for key in
609
- ("morsel_blocked_time", "morsel_pending_time", "morsel_active_wall_time",
610
- "morsel_active_cpu_time", "pipeline_task_open_time", "pipeline_task_close_time") if key in fields},
611
- "backend_count": len(pipeline["backends"]),
612
- "backends_returned": min(len(pipeline["backends"]), top),
613
- "backends": [
614
- {
615
- "name": backend["name"],
616
- "report_pointer": f"/pipelines/{index}/backends/{bi}",
617
- "fields": {key: backend["metrics"][key] for key in backend_fields
618
- if key in backend["metrics"]},
619
- }
620
- for bi, backend in enumerate(pipeline["backends"][:top])
621
- ],
622
- "resource_operator_count": len(resources), "resource_operators_returned": min(len(resources), top),
623
- "resource_operators": resources[:top]})
624
- for operator in pipeline["operators"]:
625
- name = operator["name"]
626
- operator_counts[name] = operator_counts.get(name, 0) + 1
627
- pipelines.sort(key=lambda item: item["wall_time"]["value"]
628
- if item["wall_time"]["availability"] == "observed" and item["wall_time"]["unit"] == "ms" else -1,
629
- reverse=True)
630
- return {"scope_count": len(scopes), "scopes_returned": min(len(scopes), top), "scopes": scopes[:top],
631
- "pipeline_count": len(pipelines), "pipelines_returned": min(len(pipelines), top), "pipelines": pipelines[:top],
632
- "operator_occurrences_by_name": operator_counts}
633
-
634
-
635
- def build_memory_overview(report: Dict[str, Any], *, top: int = 5) -> Dict[str, Any]:
636
- """Rank known state/allocation fields independently of time and other scopes."""
637
- memory_fields = {
638
- "store_tuple_arena_logical", "store_tuple_arena_physical",
639
- "hash_table_arena_logical", "hash_table_arena_physical",
640
- "arena_logical_allocated", "arena_physical_allocated",
641
- "main_arena_logical_allocated", "main_arena_physical_allocated",
642
- "secondary_arena_logical_allocated", "secondary_arena_physical_allocated",
643
- "partition_arena_allocated_logical", "partition_arena_allocated_physical",
644
- "partition_allocated_logical", "partition_allocated_physical",
645
- "temp_table_logical_allocated", "temp_table_physical_allocated",
646
- "allocated_memory", "deallocated_memory", "peak_memory", "current_memory",
647
- }
648
- groups = {}
649
- object_limit = min(top, 3)
650
- for pi, pipeline in enumerate(report["pipelines"]):
651
- for oi, obj in [(None, pipeline), *enumerate(pipeline["operators"])]:
652
- for key, value in obj["metrics"].items():
653
- if key not in memory_fields or value["unit"] != "bytes" or value["availability"] != "observed":
654
- continue
655
- object_type = "pipeline" if oi is None else "operator"
656
- group = (object_type, key, value["aggregation"], pipeline["task_id"], pipeline["stage_id"])
657
- pointer = f"/pipelines/{pi}" + (f"/operators/{oi}" if oi is not None else "")
658
- item = {"report_pointer": pointer, "pipeline_id": pipeline["pipeline_id"], "metric": value}
659
- if oi is not None:
660
- item.update({"name": obj["name"], "plan_id": obj["plan_id"]})
661
- groups.setdefault(group, []).append(item)
662
- ranked = [
663
- {"object_type": key[0], "field": key[1], "unit": "bytes", "aggregation": key[2],
664
- "task_id": key[3], "stage_id": key[4], "object_count": len(values),
665
- "objects_returned": min(len(values), object_limit),
666
- "values": sorted(values, key=lambda item: item["metric"]["value"], reverse=True)[:object_limit]}
667
- for key, values in sorted(groups.items(), key=lambda item: str(item[0]))
668
- ]
669
- return {"group_count": len(ranked), "groups_returned": min(len(ranked), 8), "groups": ranked[:8]}
670
-
671
-
672
- def build_summary(report: Dict[str, Any], report_path: Path, *, top: int = 5) -> Dict[str, Any]:
673
- groups: Dict[tuple, list] = {}
674
- for pi, pipeline in enumerate(report["pipelines"]):
675
- for oi, obj in [(None, pipeline), *enumerate(pipeline["operators"])]:
676
- for key, value in obj["metrics"].items():
677
- if value["availability"] != "observed":
678
- continue
679
- group = ("pipeline" if oi is None else "operator", key, value["unit"], value["aggregation"])
680
- pointer = f"/pipelines/{pi}" + (f"/operators/{oi}" if oi is not None else "")
681
- groups.setdefault(group, []).append({"object": pointer, "value": value["value"], "line": value["line"]})
682
- # Bound default output by field groups as well as objects; all facts remain in report.
683
- rankings = [{"object_type": k[0], "field": k[1], "unit": k[2], "aggregation": k[3], "count": len(v),
684
- "values": sorted(v, key=lambda item: item["value"], reverse=True)[:top]} for k, v in sorted(groups.items(), key=lambda item: str(item[0]))]
685
- query = {k: v for k, v in report["query_fields"].items() if v["availability"] == "observed"}
686
- return {"schema_version": SCHEMA_VERSION, "normalization_version": NORMALIZATION_VERSION,
687
- "report_path": str(report_path), "body_sha256": report["body_sha256"],
688
- "source": report["source"], "task_names": report["task_names"], "collection_status": report["collection_status"],
689
- "complete": report["complete"], "input_format": report["input_format"], "coverage": report["coverage"],
690
- "execution_overview": build_execution_overview(report, top=top),
691
- "memory_overview": build_memory_overview(report, top=top),
692
- "job_snapshots": [{"task_name": j["raw"].get("taskName"), "status": j["raw"].get("taskStatMachine", "UNKNOWN"),
693
- "report_pointer": f"/jobs/{i}", "failure_message_preview": str(j["raw"].get("failedMessage", ""))[:300],
694
- "fields": {k: v for k, v in j["fields"].items() if k in {"run_time", "peakUserMemory", "currentUserMemory", "memCapacity"} | _JOB_PHASE_FIELDS}} for i, j in enumerate(report["jobs"][:top])],
695
- "query_metrics": dict(list(query.items())[:20]), "summary_metrics": report["summary_metrics"],
696
- "rankings": rankings[:20], "ranking_group_count": len(rankings), "ranking_groups_returned": min(len(rankings), 20)}
697
-
698
-
699
- def _compare_jobs(before: Dict[str, Any], after: Dict[str, Any]) -> Dict[str, Any]:
700
- """Compare identified job snapshots without choosing among repeated snapshots."""
701
- left, right = before["jobs"], after["jobs"]
702
- groups = []
703
- for jobs in (left, right):
704
- group = {}
705
- for index, job in enumerate(jobs):
706
- name = job["raw"].get("taskName")
707
- if name and name.lower() not in {"null", "unknown", "n/a"}:
708
- group.setdefault(name, []).append(index)
709
- groups.append(group)
710
- lg, rg = groups
711
- pairs = [(lg[name][0], rg[name][0], "unique_task_name")
712
- for name in sorted(lg.keys() & rg.keys()) if len(lg[name]) == len(rg[name]) == 1]
713
- # Different runs normally have different task names. Only a sole identified
714
- # job on each selected side is safe to pair without an explicit mapping.
715
- if not pairs and len(left) == len(right) == len(lg) == len(rg) == 1:
716
- if before["task_names"] == list(lg) and after["task_names"] == list(rg):
717
- pairs = [(0, 0, "single_job_per_report")]
718
-
719
- def snapshot(job, index):
720
- return {"report_pointer": f"/jobs/{index}", "raw": job["raw"]}
721
-
722
- changes = []
723
- for li, ri, basis in pairs:
724
- a, b = left[li], right[ri]
725
- status_a, status_b = a["raw"].get("taskStatMachine"), b["raw"].get("taskStatMachine")
726
- known_status = all(value and value.lower() not in {"null", "unknown", "n/a"}
727
- for value in (status_a, status_b))
728
- fields = (a["fields"].keys() | b["fields"].keys()) & {
729
- "run_time", "peakUserMemory", "currentUserMemory", "memCapacity"}
730
- changes.append({"alignment_basis": basis, "before": snapshot(a, li), "after": snapshot(b, ri),
731
- "status_change": {"before": status_a, "after": status_b,
732
- "changed": status_a != status_b if known_status else None,
733
- "reason": None if known_status else "unavailable_operand"},
734
- "metric_changes": {key: _calculation(a["fields"].get(key, normalize_value(None, key=key)),
735
- b["fields"].get(key, normalize_value(None, key=key)))
736
- for key in sorted(fields)}})
737
- used_left, used_right = {p[0] for p in pairs}, {p[1] for p in pairs}
738
- unmatched_left = [snapshot(job, i) for i, job in enumerate(left) if i not in used_left]
739
- unmatched_right = [snapshot(job, i) for i, job in enumerate(right) if i not in used_right]
740
- return {"job_changes": changes, "unmatched_jobs_before": unmatched_left, "unmatched_jobs_after": unmatched_right,
741
- "job_alignment_ambiguities": [{"reason": "task_correspondence_unresolved",
742
- "before": unmatched_left, "after": unmatched_right}]
743
- if unmatched_left and unmatched_right else []}
744
-
745
-
746
- def compare_reports(before: Dict[str, Any], after: Dict[str, Any], *, top: int = 5) -> Dict[str, Any]:
747
- """Match explicit identities; never declare causality or business comparability."""
748
- left, right = before["data_scale_transitions"], after["data_scale_transitions"]
749
- used_left, used_right, changes = set(), set(), []
750
- def operator_fields(report, item):
751
- return report["pipelines"][item["pipeline_index"]]["operators"][item["operator_index"]]["metrics"]
752
- def field_changes(left_fields, right_fields):
753
- return {k: _calculation(left_fields.get(k, normalize_value(None, key=k)), right_fields.get(k, normalize_value(None, key=k)))
754
- for k in sorted(left_fields.keys() | right_fields.keys())}
755
- def identity(item, basis):
756
- if basis == "name_and_plan_id":
757
- return (item["name"], item["plan_id"]) if item.get("plan_id") else None
758
- physical = item.get("physical_node")
759
- return (item["name"], physical["node_type"], physical["summary"]) if physical else None
760
- for basis in ("name_and_plan_id", "physical_signature"):
761
- lg, rg = {}, {}
762
- for group, items, used in ((lg, left, used_left), (rg, right, used_right)):
763
- for i, item in enumerate(items):
764
- key = identity(item, basis)
765
- if i not in used and key is not None:
766
- group.setdefault(key, []).append(i)
767
- for key in sorted(lg.keys() & rg.keys(), key=str):
768
- if len(lg[key]) != 1 or len(rg[key]) != 1:
769
- continue
770
- li, ri = lg[key][0], rg[key][0]
771
- used_left.add(li); used_right.add(ri)
772
- changes.append({"alignment_basis": basis, "before": left[li], "after": right[ri],
773
- "metric_changes": field_changes(operator_fields(before, left[li]), operator_fields(after, right[ri])),
774
- "count_changes": {field: _calculation(left[li]["counts"][field], right[ri]["counts"][field]) for field in left[li]["counts"]}})
775
- unmatched_left = [v for i, v in enumerate(left) if i not in used_left]
776
- unmatched_right = [v for i, v in enumerate(right) if i not in used_right]
777
- ambiguities = []
778
- for basis in ("name_and_plan_id", "physical_signature"):
779
- keys = {identity(v, basis) for v in unmatched_left} & {identity(v, basis) for v in unmatched_right}
780
- for key in sorted(keys - {None}, key=str):
781
- lc = sum(identity(v, basis) == key for v in unmatched_left)
782
- rc = sum(identity(v, basis) == key for v in unmatched_right)
783
- if lc > 1 or rc > 1:
784
- ambiguities.append({"basis": basis, "key": key, "before_count": lc, "after_count": rc})
785
- return {"schema_version": SCHEMA_VERSION, "before": {k: before[k] for k in ("source", "body_sha256", "task_names", "complete", "collection_status", "coverage")},
786
- "after": {k: after[k] for k in ("source", "body_sha256", "task_names", "complete", "collection_status", "coverage")},
787
- "operator_data_scale_changes": changes, "operator_alignment_ambiguities": ambiguities,
788
- "unmatched_before": unmatched_left, "unmatched_after": unmatched_right,
789
- **_compare_jobs(before, after),
790
- "query_changes": field_changes(before["query_fields"], after["query_fields"]),
791
- "summary_changes": field_changes(before["summary_metrics"], after["summary_metrics"])}