rosetta-sql 1.0.0__py3-none-any.whl
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.
- benchmark/generate_csv_data.py +83 -0
- benchmark/import_data.py +168 -0
- rosetta/__init__.py +3 -0
- rosetta/__main__.py +8 -0
- rosetta/benchmark.py +1678 -0
- rosetta/buglist.py +108 -0
- rosetta/cli/__init__.py +11 -0
- rosetta/cli/config_cmd.py +243 -0
- rosetta/cli/exec.py +219 -0
- rosetta/cli/interactive_cmd.py +124 -0
- rosetta/cli/list_cmd.py +215 -0
- rosetta/cli/main.py +617 -0
- rosetta/cli/output.py +545 -0
- rosetta/cli/result.py +61 -0
- rosetta/cli/result_cmd.py +247 -0
- rosetta/cli/run.py +625 -0
- rosetta/cli/status.py +161 -0
- rosetta/comparator.py +205 -0
- rosetta/config.py +139 -0
- rosetta/executor.py +403 -0
- rosetta/flamegraph.py +630 -0
- rosetta/interactive.py +1790 -0
- rosetta/models.py +197 -0
- rosetta/parser.py +308 -0
- rosetta/reporter/__init__.py +1 -0
- rosetta/reporter/bench_html.py +1457 -0
- rosetta/reporter/bench_text.py +162 -0
- rosetta/reporter/history.py +1686 -0
- rosetta/reporter/html.py +644 -0
- rosetta/reporter/text.py +110 -0
- rosetta/runner.py +3089 -0
- rosetta/ui.py +736 -0
- rosetta/whitelist.py +161 -0
- rosetta_sql-1.0.0.dist-info/LICENSE +21 -0
- rosetta_sql-1.0.0.dist-info/METADATA +379 -0
- rosetta_sql-1.0.0.dist-info/RECORD +42 -0
- rosetta_sql-1.0.0.dist-info/WHEEL +5 -0
- rosetta_sql-1.0.0.dist-info/entry_points.txt +2 -0
- rosetta_sql-1.0.0.dist-info/top_level.txt +4 -0
- skills/rosetta/scripts/install_rosetta.py +469 -0
- skills/rosetta/scripts/rosetta_wrapper.py +377 -0
- tests/test_cli.py +749 -0
|
@@ -0,0 +1,1457 @@
|
|
|
1
|
+
"""HTML benchmark report generator for Rosetta.
|
|
2
|
+
|
|
3
|
+
Generates a self-contained HTML file with:
|
|
4
|
+
- Dashboard cards (total queries, QPS, duration)
|
|
5
|
+
- Per-DBMS latency stats table with tabs
|
|
6
|
+
- Grouped bar chart (ECharts) for cross-DBMS comparison
|
|
7
|
+
- Expandable raw latency data
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import html
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from ..models import BenchmarkResult, DBMSBenchResult, QueryLatencyStats
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger("rosetta")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _escape(text: str) -> str:
|
|
21
|
+
return html.escape(text, quote=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_data(result: BenchmarkResult) -> dict:
|
|
25
|
+
"""Convert BenchmarkResult into a JSON-serialisable dict."""
|
|
26
|
+
dbms_list = []
|
|
27
|
+
for dr in result.dbms_results:
|
|
28
|
+
queries = []
|
|
29
|
+
for qs in dr.query_stats:
|
|
30
|
+
queries.append({
|
|
31
|
+
"name": qs.query_name,
|
|
32
|
+
"sql": qs.sql_template,
|
|
33
|
+
"exec": qs.total_executions,
|
|
34
|
+
"errors": qs.total_errors,
|
|
35
|
+
"avg": round(qs.avg_ms, 3),
|
|
36
|
+
"p50": round(qs.p50_ms, 3),
|
|
37
|
+
"p95": round(qs.p95_ms, 3),
|
|
38
|
+
"p99": round(qs.p99_ms, 3),
|
|
39
|
+
"min": round(qs.min_ms, 3),
|
|
40
|
+
"max": round(qs.max_ms, 3),
|
|
41
|
+
"qps": round(qs.qps, 1),
|
|
42
|
+
"latencies_ms": [round(l, 3) for l in qs.latencies_ms] if qs.latencies_ms else [],
|
|
43
|
+
"has_flamegraph": bool(qs.flamegraph_svg),
|
|
44
|
+
"explain": qs.explain_plan or "",
|
|
45
|
+
"explain_tree": qs.explain_tree or "",
|
|
46
|
+
"error_logs": qs.error_logs[:50] if qs.error_logs else [],
|
|
47
|
+
})
|
|
48
|
+
dbms_list.append({
|
|
49
|
+
"name": dr.dbms_name,
|
|
50
|
+
"overall_qps": round(dr.overall_qps, 1),
|
|
51
|
+
"total_duration": round(dr.total_duration_s, 2),
|
|
52
|
+
"total_queries": dr.total_queries,
|
|
53
|
+
"total_errors": dr.total_errors,
|
|
54
|
+
"table_rows": dr.table_rows,
|
|
55
|
+
"table_rows_detail": dr.table_rows_detail or {},
|
|
56
|
+
"table_schema": dr.table_schema or {},
|
|
57
|
+
"queries": queries,
|
|
58
|
+
})
|
|
59
|
+
return {
|
|
60
|
+
"workload": result.workload_name,
|
|
61
|
+
"mode": result.mode.name,
|
|
62
|
+
"iterations": result.config.iterations,
|
|
63
|
+
"warmup": result.config.warmup,
|
|
64
|
+
"concurrency": result.config.concurrency,
|
|
65
|
+
"duration": result.config.duration,
|
|
66
|
+
"ramp_up": result.config.ramp_up,
|
|
67
|
+
"timestamp": result.timestamp or time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
68
|
+
"run_id": result.run_id or "",
|
|
69
|
+
"table_rows": result.table_rows,
|
|
70
|
+
"table_rows_detail": result.table_rows_detail or {},
|
|
71
|
+
"table_schema": result.table_schema or {},
|
|
72
|
+
"dbms": dbms_list,
|
|
73
|
+
"has_profile": result.config.profile,
|
|
74
|
+
"setup_sql": list(result.setup_sql) if result.setup_sql else [],
|
|
75
|
+
"teardown_sql": list(result.teardown_sql) if result.teardown_sql else [],
|
|
76
|
+
"queries_sql": list(result.queries_sql) if result.queries_sql else [],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# HTML template
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
_HTML_TEMPLATE = r"""<!DOCTYPE html>
|
|
85
|
+
<html lang="en">
|
|
86
|
+
<head>
|
|
87
|
+
<meta charset="UTF-8">
|
|
88
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
89
|
+
<title>Rosetta Benchmark — {{WORKLOAD}}</title>
|
|
90
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
|
91
|
+
<style>
|
|
92
|
+
:root {
|
|
93
|
+
--bg: #0d1117; --bg2: #161b22; --bg3: #21262d;
|
|
94
|
+
--fg: #c9d1d9; --fg2: #8b949e;
|
|
95
|
+
--green: #3fb950; --red: #f85149; --blue: #58a6ff;
|
|
96
|
+
--yellow: #d29922; --orange: #db8b0b; --purple: #a371f7;
|
|
97
|
+
--border: #30363d; --accent: #1f6feb;
|
|
98
|
+
}
|
|
99
|
+
* { margin: 0; padding: 0; box-sizing: border-box;
|
|
100
|
+
scrollbar-width: thin; scrollbar-color: var(--bg3) var(--bg); }
|
|
101
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
102
|
+
::-webkit-scrollbar-track { background: var(--bg); border-radius: 6px; }
|
|
103
|
+
::-webkit-scrollbar-thumb { background: var(--bg3); border-radius: 6px;
|
|
104
|
+
border: 2px solid var(--bg); }
|
|
105
|
+
::-webkit-scrollbar-thumb:hover { background: var(--fg2); }
|
|
106
|
+
::-webkit-scrollbar-corner { background: var(--bg); }
|
|
107
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
108
|
+
background: var(--bg); color: var(--fg); line-height: 1.5; padding: 20px; }
|
|
109
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
110
|
+
h1 { color: var(--fg); margin-bottom: 4px; font-size: 24px; }
|
|
111
|
+
h2 { font-size: 18px; margin-bottom: 16px; color: var(--fg); }
|
|
112
|
+
.meta { color: var(--fg2); font-size: 14px; margin-bottom: 24px; }
|
|
113
|
+
.meta span { margin-right: 16px; }
|
|
114
|
+
|
|
115
|
+
/* Config panel */
|
|
116
|
+
.config-panel { background: var(--bg2); border: 1px solid var(--border); border-radius: 10px;
|
|
117
|
+
padding: 20px 24px; margin-bottom: 24px; }
|
|
118
|
+
.config-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
119
|
+
gap: 12px 24px; }
|
|
120
|
+
.config-item { display: flex; flex-direction: column; gap: 2px; }
|
|
121
|
+
.config-item .cfg-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
|
|
122
|
+
color: var(--fg2); font-weight: 600; }
|
|
123
|
+
.config-item .cfg-value { font-size: 15px; font-weight: 600; color: var(--fg); }
|
|
124
|
+
.config-item .cfg-value.highlight { color: var(--blue); }
|
|
125
|
+
.config-item .cfg-value .badge-on { display: inline-block; padding: 1px 8px; border-radius: 4px;
|
|
126
|
+
font-size: 12px; font-weight: 600; background: #b91c1c; color: #fff; }
|
|
127
|
+
.config-item .cfg-value .badge-off { display: inline-block; padding: 1px 8px; border-radius: 4px;
|
|
128
|
+
font-size: 12px; font-weight: 600; background: var(--bg3); color: var(--fg2); }
|
|
129
|
+
.config-item .cfg-value .mode-serial { color: var(--green); }
|
|
130
|
+
.config-item .cfg-value .mode-concurrent { color: var(--orange); }
|
|
131
|
+
.table-rows-wrap { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px; }
|
|
132
|
+
.table-chip { display: inline-flex; align-items: center; gap: 6px; background: var(--bg);
|
|
133
|
+
border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; font-size: 12px;
|
|
134
|
+
line-height: 1.4; transition: border-color 0.15s; }
|
|
135
|
+
.table-chip:hover { border-color: var(--fg2); }
|
|
136
|
+
.table-chip .tc-name { color: var(--blue); font-family: 'SF Mono', Consolas, monospace;
|
|
137
|
+
font-size: 11px; font-weight: 500; }
|
|
138
|
+
.table-chip .tc-count { color: var(--fg); font-weight: 600; font-size: 12px; }
|
|
139
|
+
.table-chip .tc-sep { color: var(--border); }
|
|
140
|
+
|
|
141
|
+
/* Run ID */
|
|
142
|
+
.run-id-wrap { display: inline-flex; align-items: center; gap: 6px; font-family: 'SF Mono', Consolas, monospace; font-size: 12px; color: var(--purple); max-width: 200px; }
|
|
143
|
+
.run-id-text { cursor: default; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
144
|
+
.run-id-copy { flex-shrink: 0; background: transparent; border: 1px solid var(--border); border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; color: var(--fg2); transition: all 0.15s; line-height: 1; }
|
|
145
|
+
.run-id-copy:hover { background: var(--bg3); border-color: var(--fg2); color: var(--fg); }
|
|
146
|
+
|
|
147
|
+
/* Cards */
|
|
148
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
149
|
+
gap: 12px; margin-bottom: 24px; }
|
|
150
|
+
.card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px;
|
|
151
|
+
padding: 16px; }
|
|
152
|
+
.card .label { font-size: 12px; color: var(--fg2); text-transform: uppercase;
|
|
153
|
+
letter-spacing: 0.5px; margin-bottom: 4px; }
|
|
154
|
+
.card .value { font-size: 24px; font-weight: 700; }
|
|
155
|
+
.card .sub { font-size: 12px; color: var(--fg2); margin-top: 2px; }
|
|
156
|
+
|
|
157
|
+
/* Section */
|
|
158
|
+
.section { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px;
|
|
159
|
+
padding: 20px; margin-bottom: 20px; }
|
|
160
|
+
|
|
161
|
+
/* Tabs */
|
|
162
|
+
.tabs { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
163
|
+
.tab { padding: 6px 16px; border-radius: 6px; cursor: pointer; font-size: 14px;
|
|
164
|
+
border: 1px solid var(--border); background: var(--bg3); color: var(--fg2);
|
|
165
|
+
transition: all 0.15s; user-select: none; }
|
|
166
|
+
.tab:hover { color: var(--fg); border-color: var(--fg2); }
|
|
167
|
+
.tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
168
|
+
|
|
169
|
+
/* Table */
|
|
170
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
171
|
+
th { text-align: left; padding: 8px 12px; border-bottom: 2px solid var(--border);
|
|
172
|
+
color: var(--fg2); font-weight: 600; position: sticky; top: 0; background: var(--bg2); }
|
|
173
|
+
th.num { text-align: right; }
|
|
174
|
+
td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
|
|
175
|
+
td.num { text-align: right; font-family: 'SF Mono', Consolas, monospace; font-size: 13px; }
|
|
176
|
+
tr:hover { background: var(--bg3); }
|
|
177
|
+
.qname { font-weight: 600; color: var(--blue); cursor: pointer; }
|
|
178
|
+
.qname:hover { text-decoration: underline; }
|
|
179
|
+
.sql-row td { padding: 4px 12px 12px; border-bottom: 1px solid var(--border); }
|
|
180
|
+
.sql-code { font-family: 'SF Mono', Consolas, monospace; font-size: 12px; color: var(--fg2);
|
|
181
|
+
background: var(--bg3); border-radius: 6px; padding: 8px 12px; white-space: pre-wrap;
|
|
182
|
+
word-break: break-all; line-height: 1.6; }
|
|
183
|
+
.dbms-tag { display:inline-block; padding:2px 8px; border-radius:4px; font-size:12px;
|
|
184
|
+
font-weight:600; color:#fff; white-space:nowrap; }
|
|
185
|
+
.query-group-first td { border-top: 2px solid var(--border); }
|
|
186
|
+
.query-group td { border-bottom-color: rgba(48,54,61,0.4); }
|
|
187
|
+
|
|
188
|
+
/* Query detail selector */
|
|
189
|
+
.q-dropdown-wrap { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
|
190
|
+
.q-dropdown-wrap label { font-size: 14px; font-weight: 600; color: var(--fg2); white-space: nowrap; }
|
|
191
|
+
.q-dropdown { appearance: none; background: var(--bg3); border: 1px solid var(--border);
|
|
192
|
+
border-radius: 6px; padding: 8px 36px 8px 14px; font-size: 14px; color: var(--fg);
|
|
193
|
+
cursor: pointer; min-width: 240px;
|
|
194
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%238b949e' stroke-width='1.5' fill='none'/%3E%3C/svg%3E");
|
|
195
|
+
background-repeat: no-repeat; background-position: right 12px center;
|
|
196
|
+
transition: border-color 0.15s; }
|
|
197
|
+
.q-dropdown:hover { border-color: var(--fg2); }
|
|
198
|
+
.q-dropdown:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(31,111,235,0.2); }
|
|
199
|
+
.q-dropdown option { background: var(--bg2); color: var(--fg); }
|
|
200
|
+
|
|
201
|
+
/* Query detail panel */
|
|
202
|
+
.q-detail-panel { border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
|
|
203
|
+
background: var(--bg2); }
|
|
204
|
+
.q-empty { padding: 40px; text-align: center; color: var(--fg2); font-size: 14px; }
|
|
205
|
+
.q-sub { padding: 16px; }
|
|
206
|
+
.q-sub-title { font-size: 14px; font-weight: 600; color: var(--fg); margin-bottom: 10px;
|
|
207
|
+
display: flex; align-items: center; gap: 8px; }
|
|
208
|
+
.q-sub-title .q-icon { font-size: 16px; }
|
|
209
|
+
.q-separator { border: none; border-top: 1px solid var(--border); margin: 0; }
|
|
210
|
+
|
|
211
|
+
/* SQL code block */
|
|
212
|
+
.sql-code { font-family: 'SF Mono', Consolas, monospace; font-size: 12px; color: var(--fg2);
|
|
213
|
+
background: var(--bg); border-radius: 6px; padding: 8px 12px; white-space: pre-wrap;
|
|
214
|
+
word-break: break-all; line-height: 1.6; border: 1px solid var(--border); }
|
|
215
|
+
|
|
216
|
+
/* Schema (setup SQL) */
|
|
217
|
+
.schema-block { margin-bottom: 10px; }
|
|
218
|
+
.schema-toggle { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
|
|
219
|
+
font-size: 12px; color: var(--fg2); padding: 4px 10px; border-radius: 6px;
|
|
220
|
+
border: 1px solid var(--border); background: var(--bg3); user-select: none;
|
|
221
|
+
transition: all 0.15s; }
|
|
222
|
+
.schema-toggle:hover { color: var(--fg); border-color: var(--fg2); }
|
|
223
|
+
.schema-toggle .arrow { transition: transform 0.2s; font-size: 10px; }
|
|
224
|
+
.schema-toggle.open .arrow { transform: rotate(90deg); }
|
|
225
|
+
.schema-sql { font-family: 'SF Mono', Consolas, monospace; font-size: 12px; color: var(--fg2);
|
|
226
|
+
background: var(--bg); border-radius: 6px; padding: 12px 16px; white-space: pre-wrap;
|
|
227
|
+
word-break: break-all; line-height: 1.8; border: 1px solid var(--border); margin-top: 8px; }
|
|
228
|
+
.schema-sql .kw { color: var(--purple); font-weight: 600; }
|
|
229
|
+
.schema-sql .type { color: var(--green); }
|
|
230
|
+
.schema-sql .name { color: var(--blue); }
|
|
231
|
+
.schema-sql .num { color: var(--orange); }
|
|
232
|
+
.schema-sql .paren { color: var(--fg2); }
|
|
233
|
+
.schema-sql .str { color: var(--yellow); }
|
|
234
|
+
.schema-count { font-size: 11px; color: var(--fg2); background: var(--bg);
|
|
235
|
+
padding: 1px 8px; border-radius: 10px; }
|
|
236
|
+
|
|
237
|
+
/* EXPLAIN plan */
|
|
238
|
+
.q-explain-wrap { margin-bottom: 8px; }
|
|
239
|
+
.q-explain-label { font-size: 12px; font-weight: 600; margin-bottom: 4px; display: flex;
|
|
240
|
+
align-items: center; gap: 6px; }
|
|
241
|
+
.q-explain { font-family: 'SF Mono', Consolas, monospace; font-size: 11px; color: var(--fg2);
|
|
242
|
+
background: var(--bg); border-radius: 6px; padding: 10px 12px; white-space: pre;
|
|
243
|
+
overflow-x: auto; line-height: 1.5; border: 1px solid var(--border); margin-bottom: 8px; }
|
|
244
|
+
.q-explain-tree { font-size: 12px; line-height: 1.7; }
|
|
245
|
+
|
|
246
|
+
/* Chart container */
|
|
247
|
+
.chart-box { width: 100%; height: 480px; }
|
|
248
|
+
|
|
249
|
+
/* Overall QPS bar */
|
|
250
|
+
.chart-row { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
|
251
|
+
.chart-label { min-width: 120px; font-size: 14px; font-weight: 600; text-align: right; }
|
|
252
|
+
.chart-bar-bg { flex: 1; height: 28px; background: var(--bg3); border-radius: 6px;
|
|
253
|
+
overflow: hidden; position: relative; }
|
|
254
|
+
.chart-bar { height: 100%; border-radius: 6px; display: flex; align-items: center;
|
|
255
|
+
padding: 0 12px; font-size: 13px; font-weight: 600; color: #fff;
|
|
256
|
+
transition: width 0.6s ease; min-width: fit-content; }
|
|
257
|
+
.chart-val { min-width: 80px; font-size: 13px; color: var(--fg2); }
|
|
258
|
+
.c0 { background: linear-gradient(90deg, #2563eb, #3b82f6); }
|
|
259
|
+
.c1 { background: linear-gradient(90deg, #059669, #10b981); }
|
|
260
|
+
.c2 { background: linear-gradient(90deg, #d97706, #f59e0b); }
|
|
261
|
+
.c3 { background: linear-gradient(90deg, #7c3aed, #8b5cf6); }
|
|
262
|
+
.c4 { background: linear-gradient(90deg, #dc2626, #ef4444); }
|
|
263
|
+
|
|
264
|
+
/* Responsive */
|
|
265
|
+
@media (max-width: 768px) {
|
|
266
|
+
.chart-box { height: 360px; }
|
|
267
|
+
.cards { grid-template-columns: repeat(2, 1fr); }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* Flame Graph section */
|
|
271
|
+
.fg-section { margin-bottom: 20px; }
|
|
272
|
+
.fg-nav { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
273
|
+
.fg-nav-item { padding: 6px 16px; border-radius: 6px; cursor: pointer; font-size: 14px;
|
|
274
|
+
border: 1px solid var(--border); background: var(--bg3); color: var(--fg2);
|
|
275
|
+
transition: all 0.15s; user-select: none; }
|
|
276
|
+
.fg-nav-item:hover { color: var(--fg); border-color: var(--fg2); }
|
|
277
|
+
.fg-nav-item.active { background: #b91c1c; color: #fff; border-color: #b91c1c; }
|
|
278
|
+
.fg-container { width: 100%; overflow-x: auto; background: var(--bg); border-radius: 6px;
|
|
279
|
+
border: 1px solid var(--border); }
|
|
280
|
+
.fg-container svg { width: 100%; height: auto; display: block; }
|
|
281
|
+
.fg-container .fg-frame rect,
|
|
282
|
+
.fg-container .fg-frame text { transition: x 0.25s ease, width 0.25s ease; }
|
|
283
|
+
.fg-empty { padding: 40px; text-align: center; color: var(--fg2); font-size: 14px; }
|
|
284
|
+
.fg-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px;
|
|
285
|
+
font-weight: 600; background: #b91c1c; color: #fff; margin-left: 8px; vertical-align: middle; }
|
|
286
|
+
#fg-tooltip { position: fixed; display: none; background: #161b22; border: 1px solid #30363d;
|
|
287
|
+
border-radius: 6px; padding: 8px 12px; color: #e6edf3;
|
|
288
|
+
font: 12px/1.5 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
289
|
+
pointer-events: none; z-index: 9999; white-space: nowrap; max-width: 600px;
|
|
290
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.4); }
|
|
291
|
+
#fg-tooltip .tt-name { font-weight: 600; margin-bottom: 2px; white-space: normal; word-break: break-all; }
|
|
292
|
+
#fg-tooltip .tt-info { color: #8b949e; font-size: 11px; }
|
|
293
|
+
|
|
294
|
+
/* Error logs */
|
|
295
|
+
.err-section { margin-top: 8px; }
|
|
296
|
+
.err-toggle { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
|
|
297
|
+
font-size: 12px; color: var(--red); padding: 4px 10px; border-radius: 6px;
|
|
298
|
+
border: 1px solid rgba(248,81,73,0.3); background: rgba(248,81,73,0.08);
|
|
299
|
+
user-select: none; transition: all 0.15s; font-weight: 600; }
|
|
300
|
+
.err-toggle:hover { border-color: var(--red); background: rgba(248,81,73,0.15); }
|
|
301
|
+
.err-toggle .arrow { transition: transform 0.2s; font-size: 10px; }
|
|
302
|
+
.err-toggle.open .arrow { transform: rotate(90deg); }
|
|
303
|
+
.err-list { margin-top: 8px; max-height: 400px; overflow-y: auto; }
|
|
304
|
+
.err-item { background: var(--bg); border: 1px solid rgba(248,81,73,0.2); border-radius: 6px;
|
|
305
|
+
padding: 10px 12px; margin-bottom: 6px; font-size: 12px; line-height: 1.5; }
|
|
306
|
+
.err-item .err-sql { font-family: 'SF Mono', Consolas, monospace; color: var(--fg2);
|
|
307
|
+
white-space: pre-wrap; word-break: break-all; margin-bottom: 4px; }
|
|
308
|
+
.err-item .err-msg { color: var(--red); font-weight: 600; }
|
|
309
|
+
.sb-log-tabs { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
310
|
+
.sb-log-tab {
|
|
311
|
+
padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 14px;
|
|
312
|
+
border: 1px solid var(--border); background: var(--bg3); color: var(--fg2);
|
|
313
|
+
transition: all 0.15s; user-select: none; font-weight: 600;
|
|
314
|
+
}
|
|
315
|
+
.sb-log-tab:hover { color: var(--fg); border-color: var(--fg2); }
|
|
316
|
+
.sb-log-tab.active { color: #fff; border-color: transparent; }
|
|
317
|
+
.sb-log-content { display: none; }
|
|
318
|
+
.sb-log-content.active { display: block; }
|
|
319
|
+
.sb-log-phase { margin-bottom: 12px; }
|
|
320
|
+
.sb-log-phase-label {
|
|
321
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
322
|
+
color: var(--fg2); font-weight: 600; margin-bottom: 6px;
|
|
323
|
+
}
|
|
324
|
+
.sb-log-pre {
|
|
325
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
|
326
|
+
padding: 12px 16px; font: 12px/1.6 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
327
|
+
overflow-x: auto; white-space: pre-wrap; word-break: break-all; max-height: 400px;
|
|
328
|
+
overflow-y: auto; color: var(--fg2);
|
|
329
|
+
}
|
|
330
|
+
.sb-log-pre .cmd { color: #58a6ff; }
|
|
331
|
+
.sb-log-pre .err { color: #f85149; }
|
|
332
|
+
.sb-log-pre .ok { color: #3fb950; }
|
|
333
|
+
.sb-log-pre .num { color: #d29922; font-weight: 600; }
|
|
334
|
+
.sb-log-pre .val { color: #3fb950; font-weight: 600; }
|
|
335
|
+
</style>
|
|
336
|
+
</head>
|
|
337
|
+
<body>
|
|
338
|
+
<div id="fg-tooltip"><div class="tt-name"></div><div class="tt-info"></div></div>
|
|
339
|
+
<div class="container">
|
|
340
|
+
<div style="display:flex;align-items:center;gap:14px;margin-bottom:16px">
|
|
341
|
+
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
342
|
+
<!-- Stone tablet shape -->
|
|
343
|
+
<rect x="4" y="2" width="36" height="40" rx="4" ry="12" fill="#161b22" stroke="#30363d" stroke-width="1.5"/>
|
|
344
|
+
<!-- Three bar columns representing benchmark comparison -->
|
|
345
|
+
<rect x="10" y="22" width="6" height="14" rx="1.5" fill="#3b82f6" opacity="0.9"/>
|
|
346
|
+
<rect x="19" y="14" width="6" height="22" rx="1.5" fill="#10b981" opacity="0.9"/>
|
|
347
|
+
<rect x="28" y="18" width="6" height="18" rx="1.5" fill="#f59e0b" opacity="0.9"/>
|
|
348
|
+
<!-- Top decorative lines (Rosetta stone inscriptions) -->
|
|
349
|
+
<line x1="10" y1="7" x2="34" y2="7" stroke="#30363d" stroke-width="1.2" stroke-linecap="round"/>
|
|
350
|
+
<line x1="10" y1="10" x2="28" y2="10" stroke="#30363d" stroke-width="1.2" stroke-linecap="round"/>
|
|
351
|
+
</svg>
|
|
352
|
+
<h1>Rosetta Benchmark Report</h1>
|
|
353
|
+
<a href="../index.html" style="color:var(--blue);font-size:14px;text-decoration:none;border:1px solid var(--border);border-radius:6px;padding:4px 12px">◀ History</a>
|
|
354
|
+
</div>
|
|
355
|
+
<div class="config-panel" id="config-panel"></div>
|
|
356
|
+
|
|
357
|
+
<!-- Dashboard cards -->
|
|
358
|
+
<div class="cards" id="cards"></div>
|
|
359
|
+
|
|
360
|
+
<!-- Main chart: grouped bar chart via ECharts -->
|
|
361
|
+
<div class="section">
|
|
362
|
+
<h2>Cross-DBMS Query Comparison</h2>
|
|
363
|
+
<div style="display:flex;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
|
|
364
|
+
<span style="font-size:13px;color:var(--fg2)">Metric:</span>
|
|
365
|
+
<div class="tabs" id="chart-metric-tabs" style="margin-bottom:0"></div>
|
|
366
|
+
</div>
|
|
367
|
+
<div id="main-chart" class="chart-box"></div>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<!-- Overall QPS comparison -->
|
|
371
|
+
<div class="section">
|
|
372
|
+
<h2>Overall QPS</h2>
|
|
373
|
+
<div id="qps-chart"></div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<!-- Workload Definition (Setup / Queries / Teardown) -->
|
|
377
|
+
<div class="section" id="schema-section" style="display:none">
|
|
378
|
+
<h2>📜 Workload Definition</h2>
|
|
379
|
+
<div id="schema-content"></div>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<!-- Per-Query detail dropdown -->
|
|
383
|
+
<div class="section">
|
|
384
|
+
<h2>Per-Query Latency Details</h2>
|
|
385
|
+
<div class="q-dropdown-wrap">
|
|
386
|
+
<label for="query-select">Query:</label>
|
|
387
|
+
<select id="query-select" class="q-dropdown">
|
|
388
|
+
<option value="">-- Select a query --</option>
|
|
389
|
+
</select>
|
|
390
|
+
</div>
|
|
391
|
+
<div id="query-detail-panel" class="q-detail-panel">
|
|
392
|
+
<div class="q-empty">Select a query above to view details</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<!-- Error Logs -->
|
|
397
|
+
<div class="section" id="error-logs-section" style="display:none">
|
|
398
|
+
<h2>⚠️ Error Logs</h2>
|
|
399
|
+
<div id="error-logs-content"></div>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<script>
|
|
404
|
+
const DATA = {{DATA_JSON}};
|
|
405
|
+
const FLAME_GRAPH_DATA = {{FLAMEGRAPH_JSON}};
|
|
406
|
+
const COLORS = ['c0','c1','c2','c3','c4'];
|
|
407
|
+
const ECHARTS_COLORS = ['#3b82f6','#10b981','#f59e0b','#8b5cf6','#ef4444','#06b6d4','#ec4899'];
|
|
408
|
+
|
|
409
|
+
function esc(s) {
|
|
410
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
411
|
+
.replace(/"/g,'"').replace(/'/g,''');
|
|
412
|
+
}
|
|
413
|
+
function fmtMs(v) {
|
|
414
|
+
if (v < 1) return v.toFixed(3);
|
|
415
|
+
if (v < 100) return v.toFixed(2);
|
|
416
|
+
return v.toFixed(1);
|
|
417
|
+
}
|
|
418
|
+
function copyToClipboard(text, btn) {
|
|
419
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
420
|
+
var orig = btn.innerHTML;
|
|
421
|
+
btn.innerHTML = '✓';
|
|
422
|
+
btn.disabled = true;
|
|
423
|
+
setTimeout(function() { btn.innerHTML = orig; btn.disabled = false; }, 1500);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// -- Config Panel --
|
|
428
|
+
(function() {
|
|
429
|
+
var panel = document.getElementById('config-panel');
|
|
430
|
+
var modeClass = 'mode-serial';
|
|
431
|
+
if (DATA.mode === 'CONCURRENT') modeClass = 'mode-concurrent';
|
|
432
|
+
var items = '';
|
|
433
|
+
|
|
434
|
+
// Run ID (first item with copy button)
|
|
435
|
+
if (DATA.run_id) {
|
|
436
|
+
items += '<div class="config-item"><span class="cfg-label">Run ID</span>' +
|
|
437
|
+
'<span class="cfg-value run-id-wrap">' +
|
|
438
|
+
'<span class="run-id-text" title="' + esc(DATA.run_id) + '">' + esc(DATA.run_id) + '</span>' +
|
|
439
|
+
'<button class="run-id-copy" onclick="copyToClipboard(\'' + esc(DATA.run_id).replace(/'/g, "\\'") + '\',this)">📋</button>' +
|
|
440
|
+
'</span></div>';
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Workload
|
|
444
|
+
items += '<div class="config-item"><span class="cfg-label">Workload</span>' +
|
|
445
|
+
'<span class="cfg-value highlight">' + esc(DATA.workload) + '</span></div>';
|
|
446
|
+
|
|
447
|
+
// Mode
|
|
448
|
+
items += '<div class="config-item"><span class="cfg-label">Mode</span>' +
|
|
449
|
+
'<span class="cfg-value"><span class="' + modeClass + '">' + DATA.mode + '</span></span></div>';
|
|
450
|
+
|
|
451
|
+
if (DATA.mode === 'SERIAL') {
|
|
452
|
+
// Iterations
|
|
453
|
+
items += '<div class="config-item"><span class="cfg-label">Iterations</span>' +
|
|
454
|
+
'<span class="cfg-value">' + DATA.iterations + '</span></div>';
|
|
455
|
+
// Warmup
|
|
456
|
+
items += '<div class="config-item"><span class="cfg-label">Warmup</span>' +
|
|
457
|
+
'<span class="cfg-value">' + DATA.warmup + '</span></div>';
|
|
458
|
+
} else if (DATA.mode === 'CONCURRENT') {
|
|
459
|
+
// Concurrency
|
|
460
|
+
items += '<div class="config-item"><span class="cfg-label">Concurrency</span>' +
|
|
461
|
+
'<span class="cfg-value">' + DATA.concurrency + '</span></div>';
|
|
462
|
+
// Duration
|
|
463
|
+
if (DATA.duration > 0) {
|
|
464
|
+
items += '<div class="config-item"><span class="cfg-label">Duration</span>' +
|
|
465
|
+
'<span class="cfg-value">' + DATA.duration + 's</span></div>';
|
|
466
|
+
}
|
|
467
|
+
// Ramp-up
|
|
468
|
+
items += '<div class="config-item"><span class="cfg-label">Ramp-up</span>' +
|
|
469
|
+
'<span class="cfg-value">' + (DATA.ramp_up > 0 ? DATA.ramp_up + 's' : '0') + '</span></div>';
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Profiling
|
|
473
|
+
if (DATA.has_profile) {
|
|
474
|
+
items += '<div class="config-item"><span class="cfg-label">Profiling</span>' +
|
|
475
|
+
'<span class="cfg-value"><span class="badge-on">🔥 ON</span></span></div>';
|
|
476
|
+
} else {
|
|
477
|
+
items += '<div class="config-item"><span class="cfg-label">Profiling</span>' +
|
|
478
|
+
'<span class="cfg-value"><span class="badge-off">OFF</span></span></div>';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Time (same row as above)
|
|
482
|
+
items += '<div class="config-item"><span class="cfg-label">Time</span>' +
|
|
483
|
+
'<span class="cfg-value" style="color:var(--fg2);font-weight:400;font-size:13px">' +
|
|
484
|
+
esc(DATA.timestamp) + '</span></div>';
|
|
485
|
+
|
|
486
|
+
// Rows — per-table detail as chips
|
|
487
|
+
var detail = DATA.table_rows_detail || {};
|
|
488
|
+
var tables = Object.keys(detail);
|
|
489
|
+
if (tables.length > 0) {
|
|
490
|
+
var rowsHtml = '<div class="config-item" style="grid-column:1/-1">' +
|
|
491
|
+
'<span class="cfg-label">Table Rows</span><div class="table-rows-wrap">';
|
|
492
|
+
tables.sort();
|
|
493
|
+
tables.forEach(function(t) {
|
|
494
|
+
rowsHtml += '<span class="table-chip">' +
|
|
495
|
+
'<span class="tc-name">' + esc(t) + '</span>' +
|
|
496
|
+
'<span class="tc-sep">·</span>' +
|
|
497
|
+
'<span class="tc-count">' + detail[t].toLocaleString() + '</span></span>';
|
|
498
|
+
});
|
|
499
|
+
rowsHtml += '</div></div>';
|
|
500
|
+
items += rowsHtml;
|
|
501
|
+
} else {
|
|
502
|
+
// Fallback: extract table names from setup SQL
|
|
503
|
+
var setupTables = [];
|
|
504
|
+
(DATA.setup_sql || []).forEach(function(sql) {
|
|
505
|
+
var m = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?/i);
|
|
506
|
+
if (m) setupTables.push(m[1]);
|
|
507
|
+
});
|
|
508
|
+
if (setupTables.length > 0) {
|
|
509
|
+
var rowsHtml = '<div class="config-item" style="grid-column:1/-1">' +
|
|
510
|
+
'<span class="cfg-label">Table Rows</span><div class="table-rows-wrap">';
|
|
511
|
+
setupTables.sort();
|
|
512
|
+
setupTables.forEach(function(t) {
|
|
513
|
+
rowsHtml += '<span class="table-chip">' +
|
|
514
|
+
'<span class="tc-name">' + esc(t) + '</span>' +
|
|
515
|
+
'<span class="tc-sep">·</span>' +
|
|
516
|
+
'<span class="tc-count">0</span></span>';
|
|
517
|
+
});
|
|
518
|
+
rowsHtml += '</div></div>';
|
|
519
|
+
items += rowsHtml;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
panel.innerHTML = '<div class="config-grid">' + items + '</div>';
|
|
524
|
+
})();
|
|
525
|
+
|
|
526
|
+
// -- Dashboard cards --
|
|
527
|
+
(function() {
|
|
528
|
+
const el = document.getElementById('cards');
|
|
529
|
+
DATA.dbms.forEach((d, i) => {
|
|
530
|
+
el.innerHTML += '<div class="card">' +
|
|
531
|
+
'<div class="label">' + esc(d.name) + '</div>' +
|
|
532
|
+
'<div class="value" style="color:' + ECHARTS_COLORS[i % ECHARTS_COLORS.length] + '">' +
|
|
533
|
+
d.overall_qps + ' <span style="font-size:14px;font-weight:400">QPS</span></div>' +
|
|
534
|
+
'<div class="sub">' + d.total_queries + ' queries in ' + d.total_duration + 's' +
|
|
535
|
+
(d.total_errors > 0 ? ' · <span style="color:var(--red)">' + d.total_errors + ' errors</span>' : '') +
|
|
536
|
+
'</div></div>';
|
|
537
|
+
});
|
|
538
|
+
})();
|
|
539
|
+
|
|
540
|
+
// -- ECharts Grouped Bar Chart --
|
|
541
|
+
const METRICS = [
|
|
542
|
+
{key:'avg', label:'Avg Latency (ms)'},
|
|
543
|
+
{key:'p50', label:'P50 Latency (ms)'},
|
|
544
|
+
{key:'p95', label:'P95 Latency (ms)'},
|
|
545
|
+
{key:'p99', label:'P99 Latency (ms)'},
|
|
546
|
+
{key:'qps', label:'QPS (queries/sec)'},
|
|
547
|
+
];
|
|
548
|
+
let currentMetric = 'avg';
|
|
549
|
+
let mainChart = null;
|
|
550
|
+
|
|
551
|
+
function getAllQueryNames() {
|
|
552
|
+
const names = [];
|
|
553
|
+
DATA.dbms.forEach(d => {
|
|
554
|
+
d.queries.forEach(q => {
|
|
555
|
+
if (names.indexOf(q.name) === -1) names.push(q.name);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
return names;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function renderChartMetricTabs() {
|
|
562
|
+
const el = document.getElementById('chart-metric-tabs');
|
|
563
|
+
el.innerHTML = '';
|
|
564
|
+
METRICS.forEach(m => {
|
|
565
|
+
const tab = document.createElement('div');
|
|
566
|
+
tab.className = 'tab' + (m.key === currentMetric ? ' active' : '');
|
|
567
|
+
tab.textContent = m.label.split(' (')[0]; // short label
|
|
568
|
+
tab.onclick = () => { currentMetric = m.key; renderChartMetricTabs(); updateChart(); };
|
|
569
|
+
el.appendChild(tab);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function initChart() {
|
|
574
|
+
const dom = document.getElementById('main-chart');
|
|
575
|
+
mainChart = echarts.init(dom, null, {renderer: 'canvas'});
|
|
576
|
+
updateChart();
|
|
577
|
+
|
|
578
|
+
window.addEventListener('resize', () => {
|
|
579
|
+
mainChart && mainChart.resize();
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function updateChart() {
|
|
584
|
+
if (!mainChart) return;
|
|
585
|
+
const queryNames = getAllQueryNames();
|
|
586
|
+
const mInfo = METRICS.find(m => m.key === currentMetric);
|
|
587
|
+
|
|
588
|
+
const series = DATA.dbms.map((d, i) => {
|
|
589
|
+
const dataMap = {};
|
|
590
|
+
d.queries.forEach(q => { dataMap[q.name] = q[currentMetric]; });
|
|
591
|
+
return {
|
|
592
|
+
name: d.name,
|
|
593
|
+
type: 'bar',
|
|
594
|
+
barGap: '10%',
|
|
595
|
+
barMaxWidth: 40,
|
|
596
|
+
emphasis: { focus: 'series' },
|
|
597
|
+
itemStyle: { borderRadius: [3, 3, 0, 0] },
|
|
598
|
+
data: queryNames.map(qn => dataMap[qn] || 0),
|
|
599
|
+
};
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const option = {
|
|
603
|
+
color: ECHARTS_COLORS,
|
|
604
|
+
tooltip: {
|
|
605
|
+
trigger: 'axis',
|
|
606
|
+
axisPointer: { type: 'shadow' },
|
|
607
|
+
backgroundColor: '#161b22',
|
|
608
|
+
borderColor: '#30363d',
|
|
609
|
+
textStyle: { color: '#c9d1d9', fontSize: 13 },
|
|
610
|
+
formatter: function(params) {
|
|
611
|
+
let html = '<div style="font-weight:700;margin-bottom:6px">' + esc(params[0].axisValue) + '</div>';
|
|
612
|
+
params.forEach(p => {
|
|
613
|
+
const v = currentMetric === 'qps' ? p.value.toFixed(1) : fmtMs(p.value);
|
|
614
|
+
const unit = currentMetric === 'qps' ? ' qps' : ' ms';
|
|
615
|
+
html += '<div style="display:flex;align-items:center;gap:6px;margin:2px 0">' +
|
|
616
|
+
'<span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:' +
|
|
617
|
+
p.color + '"></span>' +
|
|
618
|
+
'<span>' + esc(p.seriesName) + '</span>' +
|
|
619
|
+
'<span style="margin-left:auto;font-weight:600">' + v + unit + '</span></div>';
|
|
620
|
+
});
|
|
621
|
+
return html;
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
legend: {
|
|
625
|
+
data: DATA.dbms.map(d => d.name),
|
|
626
|
+
top: 0,
|
|
627
|
+
textStyle: { color: '#8b949e', fontSize: 13 },
|
|
628
|
+
itemWidth: 14, itemHeight: 10, itemGap: 20,
|
|
629
|
+
},
|
|
630
|
+
grid: {
|
|
631
|
+
left: 60, right: 30, top: 50, bottom: queryNames.length > 8 ? 100 : 60,
|
|
632
|
+
containLabel: false,
|
|
633
|
+
},
|
|
634
|
+
xAxis: {
|
|
635
|
+
type: 'category',
|
|
636
|
+
data: queryNames,
|
|
637
|
+
axisLabel: {
|
|
638
|
+
color: '#8b949e',
|
|
639
|
+
fontSize: 12,
|
|
640
|
+
rotate: queryNames.length > 6 ? 35 : 0,
|
|
641
|
+
interval: 0,
|
|
642
|
+
},
|
|
643
|
+
axisLine: { lineStyle: { color: '#30363d' } },
|
|
644
|
+
axisTick: { show: false },
|
|
645
|
+
},
|
|
646
|
+
yAxis: {
|
|
647
|
+
type: 'value',
|
|
648
|
+
name: mInfo.label,
|
|
649
|
+
nameTextStyle: { color: '#8b949e', fontSize: 12, padding: [0, 0, 0, 10] },
|
|
650
|
+
axisLabel: { color: '#8b949e', fontSize: 12 },
|
|
651
|
+
axisLine: { show: false },
|
|
652
|
+
splitLine: { lineStyle: { color: '#21262d', type: 'dashed' } },
|
|
653
|
+
},
|
|
654
|
+
dataZoom: queryNames.length > 12 ? [
|
|
655
|
+
{ type: 'slider', bottom: 10, height: 20, borderColor: '#30363d',
|
|
656
|
+
fillerColor: 'rgba(31,111,235,0.2)', handleStyle: { color: '#58a6ff' },
|
|
657
|
+
textStyle: { color: '#8b949e' } },
|
|
658
|
+
{ type: 'inside' }
|
|
659
|
+
] : [],
|
|
660
|
+
series: series,
|
|
661
|
+
animationDuration: 600,
|
|
662
|
+
animationEasing: 'cubicOut',
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
mainChart.setOption(option, true);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// -- Overall QPS chart (CSS bar) --
|
|
669
|
+
function renderQpsChart() {
|
|
670
|
+
const el = document.getElementById('qps-chart');
|
|
671
|
+
if (DATA.dbms.length === 0) { el.innerHTML = ''; return; }
|
|
672
|
+
const maxQps = Math.max(...DATA.dbms.map(d => d.overall_qps), 0.001);
|
|
673
|
+
let html = '';
|
|
674
|
+
DATA.dbms.forEach((d, i) => {
|
|
675
|
+
const pct = (d.overall_qps / maxQps * 100).toFixed(1);
|
|
676
|
+
html += '<div class="chart-row">' +
|
|
677
|
+
'<div class="chart-label">' + esc(d.name) + '</div>' +
|
|
678
|
+
'<div class="chart-bar-bg"><div class="chart-bar ' + COLORS[i % COLORS.length] +
|
|
679
|
+
'" style="width:' + pct + '%">' + d.overall_qps + ' QPS</div></div>' +
|
|
680
|
+
'<div class="chart-val">' + d.total_duration + 's</div></div>';
|
|
681
|
+
});
|
|
682
|
+
el.innerHTML = html;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// -- Schema (Setup SQL) rendering --
|
|
686
|
+
function highlightSQL(sql) {
|
|
687
|
+
// Tokenise first to protect strings/numbers, then highlight keywords on the rest.
|
|
688
|
+
// This avoids the esc()-then-regex breakage with ' entities.
|
|
689
|
+
var tokens = [];
|
|
690
|
+
var re = /('(?:[^'\\]|\\.)*')|(\b\d+(?:\.\d+)?\b)/g;
|
|
691
|
+
var last = 0, m;
|
|
692
|
+
while ((m = re.exec(sql)) !== null) {
|
|
693
|
+
if (m.index > last) tokens.push({type:'code', text: sql.slice(last, m.index)});
|
|
694
|
+
if (m[1] !== undefined) tokens.push({type:'str', text: m[1]});
|
|
695
|
+
else tokens.push({type:'num', text: m[2]});
|
|
696
|
+
last = re.lastIndex;
|
|
697
|
+
}
|
|
698
|
+
if (last < sql.length) tokens.push({type:'code', text: sql.slice(last)});
|
|
699
|
+
|
|
700
|
+
var kwRe = /\b(CREATE|TABLE|IF|NOT|EXISTS|INSERT|INTO|SELECT|FROM|WHERE|VALUES|DROP|ALTER|ADD|SET|PRIMARY|KEY|AUTO_INCREMENT|DEFAULT|NULL|UNIQUE|INDEX|CONSTRAINT|FOREIGN|REFERENCES|ON|DELETE|UPDATE|CASCADE|CHECK|ENGINE|CHARSET|COLLATE|COMMENT|LIMIT|AS|AND|OR|IN|ROUND|RAND|CONCAT|SUM|CASE|WHEN|THEN|ELSE|END|VIRTUAL|STORED|GENERATED|ALWAYS|FLOOR|ELT|JSON_ARRAY|JSON_OBJECT|JSON_LENGTH|GLOBAL|ARRAY|CAST|MEMBER|OF|BETWEEN)\b/gi;
|
|
701
|
+
var typeRe = /\b(INT|INTEGER|BIGINT|SMALLINT|TINYINT|MEDIUMINT|FLOAT|DOUBLE|DECIMAL|NUMERIC|VARCHAR|CHAR|TEXT|BLOB|DATE|DATETIME|TIMESTAMP|TIME|YEAR|BOOLEAN|BOOL|ENUM|JSON)\b/gi;
|
|
702
|
+
|
|
703
|
+
var out = '';
|
|
704
|
+
tokens.forEach(function(t) {
|
|
705
|
+
if (t.type === 'str') {
|
|
706
|
+
out += '<span class="str">' + esc(t.text) + '</span>';
|
|
707
|
+
} else if (t.type === 'num') {
|
|
708
|
+
out += '<span class="num">' + esc(t.text) + '</span>';
|
|
709
|
+
} else {
|
|
710
|
+
var s = esc(t.text);
|
|
711
|
+
s = s.replace(kwRe, '<span class="kw">$1</span>');
|
|
712
|
+
s = s.replace(typeRe, '<span class="type">$1</span>');
|
|
713
|
+
out += s;
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
return out;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function formatCreateTable(sql) {
|
|
720
|
+
// Smart formatting: only break at top-level commas and the outer parens
|
|
721
|
+
// of the column definition list, not inside type parens like DECIMAL(10,2)
|
|
722
|
+
// or expressions like CASE WHEN ... END.
|
|
723
|
+
if (sql.indexOf('\n') !== -1) return sql; // already multi-line
|
|
724
|
+
|
|
725
|
+
// Find the first '(' that opens the column definition list
|
|
726
|
+
var header = '';
|
|
727
|
+
var body = sql;
|
|
728
|
+
var firstParen = sql.indexOf('(');
|
|
729
|
+
if (firstParen === -1) return sql;
|
|
730
|
+
header = sql.slice(0, firstParen);
|
|
731
|
+
body = sql.slice(firstParen); // starts with '('
|
|
732
|
+
|
|
733
|
+
// Walk the body tracking paren depth; break on commas at depth==1
|
|
734
|
+
var depth = 0;
|
|
735
|
+
var lines = [];
|
|
736
|
+
var cur = '';
|
|
737
|
+
for (var i = 0; i < body.length; i++) {
|
|
738
|
+
var ch = body[i];
|
|
739
|
+
if (ch === "'") {
|
|
740
|
+
// skip string literal
|
|
741
|
+
var j = i + 1;
|
|
742
|
+
while (j < body.length && body[j] !== "'") { if (body[j] === '\\') j++; j++; }
|
|
743
|
+
cur += body.slice(i, j + 1);
|
|
744
|
+
i = j;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (ch === '(') { depth++; cur += ch; continue; }
|
|
748
|
+
if (ch === ')') {
|
|
749
|
+
depth--;
|
|
750
|
+
if (depth === 0) {
|
|
751
|
+
// This is the closing paren of the column list
|
|
752
|
+
if (cur.length > 0) lines.push(cur);
|
|
753
|
+
cur = body.slice(i); // rest including ')' and ENGINE etc.
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
cur += ch;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (ch === ',' && depth === 1) {
|
|
760
|
+
lines.push(cur);
|
|
761
|
+
cur = '';
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
cur += ch;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (lines.length <= 1) return sql; // nothing to format
|
|
768
|
+
|
|
769
|
+
// Build formatted output
|
|
770
|
+
// First line: header + '(' + first column def
|
|
771
|
+
// Remove leading '(' from first item
|
|
772
|
+
var firstCol = lines[0].replace(/^\(\s*/, '');
|
|
773
|
+
var result = header + '(\n ' + firstCol.trim();
|
|
774
|
+
for (var k = 1; k < lines.length; k++) {
|
|
775
|
+
result += ',\n ' + lines[k].trim();
|
|
776
|
+
}
|
|
777
|
+
// Append closing part: ')' + ENGINE/etc suffix
|
|
778
|
+
var suffix = cur.replace(/^\)/, '');
|
|
779
|
+
result += '\n)' + suffix;
|
|
780
|
+
return result;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function formatOtherSQL(sql) {
|
|
784
|
+
// Format long INSERT/SET/other SQL for readability
|
|
785
|
+
if (sql.indexOf('\n') !== -1) return sql;
|
|
786
|
+
var s = sql;
|
|
787
|
+
// INSERT INTO ... SELECT: break at major clauses
|
|
788
|
+
if (/^\s*INSERT\s+INTO/i.test(s)) {
|
|
789
|
+
s = s.replace(/\)\s*SELECT\b/gi, ')\nSELECT');
|
|
790
|
+
s = s.replace(/\bSELECT\b/gi, function(m, off) { return off > 0 ? '\nSELECT' : m; });
|
|
791
|
+
s = s.replace(/\bFROM\b/gi, '\nFROM');
|
|
792
|
+
s = s.replace(/\bWHERE\b/gi, '\nWHERE');
|
|
793
|
+
s = s.replace(/\bLIMIT\b/gi, '\nLIMIT');
|
|
794
|
+
// Clean up double newlines
|
|
795
|
+
s = s.replace(/\n\n+/g, '\n');
|
|
796
|
+
s = s.replace(/^\n/, '');
|
|
797
|
+
}
|
|
798
|
+
return s;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function renderSchema() {
|
|
802
|
+
var setupSql = DATA.setup_sql || [];
|
|
803
|
+
var teardownSql = DATA.teardown_sql || [];
|
|
804
|
+
var queriesSql = DATA.queries_sql || [];
|
|
805
|
+
|
|
806
|
+
// Show section if any data exists
|
|
807
|
+
if (setupSql.length === 0 && teardownSql.length === 0 && queriesSql.length === 0) return;
|
|
808
|
+
|
|
809
|
+
var section = document.getElementById('schema-section');
|
|
810
|
+
section.style.display = '';
|
|
811
|
+
var container = document.getElementById('schema-content');
|
|
812
|
+
|
|
813
|
+
var html = '';
|
|
814
|
+
|
|
815
|
+
// --- Setup SQL ---
|
|
816
|
+
if (setupSql.length > 0) {
|
|
817
|
+
html += '<div class="schema-block">' +
|
|
818
|
+
'<div class="schema-toggle open" id="setup-toggle" onclick="toggleWorkloadSection(\'setup\')">' +
|
|
819
|
+
'<span class="arrow">▶</span> Setup ' +
|
|
820
|
+
'<span class="schema-count">' + setupSql.length + ' statement(s)</span></div>' +
|
|
821
|
+
'<div id="setup-body">';
|
|
822
|
+
setupSql.forEach(function(sql) {
|
|
823
|
+
var formatted = /^\s*CREATE\s+TABLE/i.test(sql) ? formatCreateTable(sql) : formatOtherSQL(sql);
|
|
824
|
+
html += '<div class="schema-sql" style="margin-top:8px">' + highlightSQL(formatted) + '</div>';
|
|
825
|
+
});
|
|
826
|
+
html += '</div></div>';
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// --- Queries ---
|
|
830
|
+
if (queriesSql.length > 0) {
|
|
831
|
+
html += '<div class="schema-block">' +
|
|
832
|
+
'<div class="schema-toggle" id="queries-toggle" onclick="toggleWorkloadSection(\'queries\')">' +
|
|
833
|
+
'<span class="arrow">▶</span> Queries ' +
|
|
834
|
+
'<span class="schema-count">' + queriesSql.length + ' query(s)</span></div>' +
|
|
835
|
+
'<div id="queries-body" style="display:none">';
|
|
836
|
+
queriesSql.forEach(function(q) {
|
|
837
|
+
html += '<div style="margin-top:10px">' +
|
|
838
|
+
'<div style="font-size:12px;color:var(--blue);font-weight:600;margin-bottom:4px">' +
|
|
839
|
+
esc(q.name) +
|
|
840
|
+
'<span style="color:var(--fg2);font-weight:400;margin-left:8px">weight: ' + q.weight + '</span>' +
|
|
841
|
+
(q.description ? '<span style="color:var(--fg2);font-weight:400;margin-left:8px;font-style:italic">' + esc(q.description) + '</span>' : '') +
|
|
842
|
+
'</div>' +
|
|
843
|
+
'<div class="schema-sql">' + highlightSQL(q.sql) + '</div>';
|
|
844
|
+
if (q.cleanup_sql) {
|
|
845
|
+
html += '<div style="margin-top:4px;font-size:11px;color:var(--fg2)">cleanup:</div>' +
|
|
846
|
+
'<div class="schema-sql" style="border-color:var(--yellow);opacity:0.7">' + highlightSQL(q.cleanup_sql) + '</div>';
|
|
847
|
+
}
|
|
848
|
+
html += '</div>';
|
|
849
|
+
});
|
|
850
|
+
html += '</div></div>';
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// --- Teardown SQL ---
|
|
854
|
+
if (teardownSql.length > 0) {
|
|
855
|
+
html += '<div class="schema-block">' +
|
|
856
|
+
'<div class="schema-toggle" id="teardown-toggle" onclick="toggleWorkloadSection(\'teardown\')">' +
|
|
857
|
+
'<span class="arrow">▶</span> Teardown ' +
|
|
858
|
+
'<span class="schema-count">' + teardownSql.length + ' statement(s)</span></div>' +
|
|
859
|
+
'<div id="teardown-body" style="display:none">';
|
|
860
|
+
teardownSql.forEach(function(sql) {
|
|
861
|
+
html += '<div class="schema-sql" style="margin-top:8px">' + highlightSQL(sql) + '</div>';
|
|
862
|
+
});
|
|
863
|
+
html += '</div></div>';
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
container.innerHTML = html;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function toggleWorkloadSection(name) {
|
|
870
|
+
var toggle = document.getElementById(name + '-toggle');
|
|
871
|
+
var body = document.getElementById(name + '-body');
|
|
872
|
+
if (body.style.display === 'none') {
|
|
873
|
+
body.style.display = '';
|
|
874
|
+
toggle.classList.add('open');
|
|
875
|
+
} else {
|
|
876
|
+
body.style.display = 'none';
|
|
877
|
+
toggle.classList.remove('open');
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function toggleErrLogs(id) {
|
|
882
|
+
var toggle = document.getElementById(id + '-toggle');
|
|
883
|
+
var body = document.getElementById(id + '-body');
|
|
884
|
+
if (body.style.display === 'none') {
|
|
885
|
+
body.style.display = '';
|
|
886
|
+
toggle.classList.add('open');
|
|
887
|
+
} else {
|
|
888
|
+
body.style.display = 'none';
|
|
889
|
+
toggle.classList.remove('open');
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// -- Per-Query Dropdown + Detail Panel --
|
|
894
|
+
const DBMS_COLORS_BG = ['#1d4ed8','#047857','#b45309','#6d28d9','#b91c1c','#0e7490','#be185d'];
|
|
895
|
+
let currentQueryName = '';
|
|
896
|
+
|
|
897
|
+
function renderQuerySelector() {
|
|
898
|
+
const select = document.getElementById('query-select');
|
|
899
|
+
const queryNames = getAllQueryNames();
|
|
900
|
+
|
|
901
|
+
queryNames.forEach(function(qn) {
|
|
902
|
+
const opt = document.createElement('option');
|
|
903
|
+
opt.value = qn;
|
|
904
|
+
opt.textContent = qn;
|
|
905
|
+
select.appendChild(opt);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
select.addEventListener('change', function() {
|
|
909
|
+
renderQueryDetail(this.value);
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Auto-select first query
|
|
913
|
+
if (queryNames.length > 0) {
|
|
914
|
+
select.value = queryNames[0];
|
|
915
|
+
renderQueryDetail(queryNames[0]);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function renderQueryDetail(qn) {
|
|
920
|
+
const panel = document.getElementById('query-detail-panel');
|
|
921
|
+
currentQueryName = qn;
|
|
922
|
+
|
|
923
|
+
if (!qn) {
|
|
924
|
+
panel.innerHTML = '<div class="q-empty">Select a query above to view details</div>';
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Collect per-DBMS data for this query
|
|
929
|
+
var perDbms = [];
|
|
930
|
+
var sql = '';
|
|
931
|
+
DATA.dbms.forEach(function(d, di) {
|
|
932
|
+
var q = null;
|
|
933
|
+
d.queries.forEach(function(x) { if (x.name === qn) q = x; });
|
|
934
|
+
if (q) {
|
|
935
|
+
perDbms.push({ dbms: d.name, di: di, q: q });
|
|
936
|
+
if (!sql && q.sql) sql = q.sql;
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// Build flame graph lookup
|
|
941
|
+
var fgByQuery = {};
|
|
942
|
+
FLAME_GRAPH_DATA.forEach(function(fg) {
|
|
943
|
+
fgByQuery[fg.query] = fg.svg || '';
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
var html = '';
|
|
947
|
+
|
|
948
|
+
// --- Description & SQL ---
|
|
949
|
+
// Look up description and cleanup_sql from queries_sql
|
|
950
|
+
var qMeta = null;
|
|
951
|
+
(DATA.queries_sql || []).forEach(function(qs) { if (qs.name === qn) qMeta = qs; });
|
|
952
|
+
|
|
953
|
+
if (qMeta && qMeta.description) {
|
|
954
|
+
html += '<div class="q-sub" style="padding-bottom:8px"><div style="font-size:13px;color:var(--fg2);font-style:italic">' +
|
|
955
|
+
esc(qMeta.description) + '</div></div><hr class="q-separator">';
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (sql) {
|
|
959
|
+
html += '<div class="q-sub"><div class="q-sub-title"><span class="q-icon">\uD83D\uDCDD</span> SQL</div>' +
|
|
960
|
+
'<div class="sql-code">' + esc(sql) + '</div>';
|
|
961
|
+
if (qMeta && qMeta.cleanup_sql) {
|
|
962
|
+
html += '<div style="margin-top:8px;font-size:12px;color:var(--yellow);font-weight:600">Cleanup SQL</div>' +
|
|
963
|
+
'<div class="sql-code" style="margin-top:4px;border-color:var(--yellow);opacity:0.8">' + esc(qMeta.cleanup_sql) + '</div>';
|
|
964
|
+
}
|
|
965
|
+
html += '</div><hr class="q-separator">';
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// --- Latency Table ---
|
|
969
|
+
html += '<div class="q-sub"><div class="q-sub-title"><span class="q-icon">\u23F1\uFE0F</span> Latency</div>' +
|
|
970
|
+
'<div style="overflow-x:auto"><table>' +
|
|
971
|
+
'<thead><tr><th>DBMS</th>' +
|
|
972
|
+
'<th class="num">Exec</th><th class="num">Errors</th>' +
|
|
973
|
+
'<th class="num">Avg (ms)</th><th class="num">P50 (ms)</th>' +
|
|
974
|
+
'<th class="num">P95 (ms)</th><th class="num">P99 (ms)</th>' +
|
|
975
|
+
'<th class="num">Min (ms)</th><th class="num">Max (ms)</th>' +
|
|
976
|
+
'<th class="num">QPS</th></tr></thead><tbody>';
|
|
977
|
+
perDbms.forEach(function(pd) {
|
|
978
|
+
var q = pd.q;
|
|
979
|
+
var colorBg = DBMS_COLORS_BG[pd.di % DBMS_COLORS_BG.length];
|
|
980
|
+
html += '<tr>' +
|
|
981
|
+
'<td><span class="dbms-tag" style="background:' + colorBg + '">' + esc(pd.dbms) + '</span></td>' +
|
|
982
|
+
'<td class="num">' + q.exec + '</td>' +
|
|
983
|
+
'<td class="num"' + (q.errors > 0 ? ' style="color:var(--red)"' : '') + '>' + q.errors + '</td>' +
|
|
984
|
+
'<td class="num">' + fmtMs(q.avg) + '</td>' +
|
|
985
|
+
'<td class="num">' + fmtMs(q.p50) + '</td>' +
|
|
986
|
+
'<td class="num">' + fmtMs(q.p95) + '</td>' +
|
|
987
|
+
'<td class="num">' + fmtMs(q.p99) + '</td>' +
|
|
988
|
+
'<td class="num">' + fmtMs(q.min) + '</td>' +
|
|
989
|
+
'<td class="num">' + fmtMs(q.max) + '</td>' +
|
|
990
|
+
'<td class="num" style="font-weight:600">' + q.qps.toFixed(1) + '</td></tr>';
|
|
991
|
+
});
|
|
992
|
+
html += '</tbody></table></div></div>';
|
|
993
|
+
|
|
994
|
+
// --- EXPLAIN Plans ---
|
|
995
|
+
var hasAnyExplain = false;
|
|
996
|
+
perDbms.forEach(function(pd) { if (pd.q.explain) hasAnyExplain = true; });
|
|
997
|
+
perDbms.forEach(function(pd) { if (pd.q.explain_tree) hasAnyExplain = true; });
|
|
998
|
+
if (hasAnyExplain) {
|
|
999
|
+
html += '<hr class="q-separator">';
|
|
1000
|
+
html += '<div class="q-sub"><div class="q-sub-title"><span class="q-icon">\uD83D\uDCCA</span> Execution Plan</div>';
|
|
1001
|
+
perDbms.forEach(function(pd) {
|
|
1002
|
+
if (pd.q.explain) {
|
|
1003
|
+
var colorBg = DBMS_COLORS_BG[pd.di % DBMS_COLORS_BG.length];
|
|
1004
|
+
html += '<div class="q-explain-wrap">' +
|
|
1005
|
+
'<div class="q-explain-label"><span class="dbms-tag" style="background:' +
|
|
1006
|
+
colorBg + ';font-size:11px;padding:1px 6px">' + esc(pd.dbms) + '</span></div>' +
|
|
1007
|
+
'<div class="q-explain">' + esc(pd.q.explain) + '</div>';
|
|
1008
|
+
if (pd.q.explain_tree) {
|
|
1009
|
+
html += '<div class="q-explain-label" style="margin-top:8px">Tree:</div>' +
|
|
1010
|
+
'<div class="q-explain q-explain-tree">' + esc(pd.q.explain_tree) + '</div>';
|
|
1011
|
+
}
|
|
1012
|
+
html += '</div>';
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
html += '</div>';
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// --- Flame Graph (tdsql only) ---
|
|
1019
|
+
var fgSvg = fgByQuery[qn] || '';
|
|
1020
|
+
if (fgSvg) {
|
|
1021
|
+
html += '<hr class="q-separator">';
|
|
1022
|
+
html += '<div class="q-sub"><div class="q-sub-title"><span class="q-icon">\uD83D\uDD25</span> CPU Flame Graph ' +
|
|
1023
|
+
'<span class="fg-badge">tdsql \u00B7 perf</span></div>' +
|
|
1024
|
+
'<div class="fg-container" id="fg-detail-' + esc(qn) + '"></div></div>';
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
panel.innerHTML = html;
|
|
1028
|
+
|
|
1029
|
+
// Bind flame graph interactivity after DOM update
|
|
1030
|
+
if (fgSvg) {
|
|
1031
|
+
var fgContainer = document.getElementById('fg-detail-' + esc(qn));
|
|
1032
|
+
if (fgContainer) {
|
|
1033
|
+
fgContainer.innerHTML = fgSvg;
|
|
1034
|
+
fgBindInteractivity(fgContainer);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function createSep() {
|
|
1040
|
+
var hr = document.createElement('hr');
|
|
1041
|
+
hr.className = 'q-separator';
|
|
1042
|
+
return hr;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Bind hover + click-to-zoom interactivity to a flame graph SVG container.
|
|
1047
|
+
* This replaces the SVG-embedded <script> which does not execute via innerHTML.
|
|
1048
|
+
*/
|
|
1049
|
+
function fgBindInteractivity(container) {
|
|
1050
|
+
const svg = container.querySelector('svg');
|
|
1051
|
+
if (!svg) return;
|
|
1052
|
+
|
|
1053
|
+
const details = svg.querySelector('.fg-details');
|
|
1054
|
+
const frames = svg.querySelectorAll('.fg-frame');
|
|
1055
|
+
const chartWidth = parseFloat(svg.getAttribute('data-chart-width') || '1180');
|
|
1056
|
+
const xPad = parseFloat(svg.getAttribute('data-x-pad') || '10');
|
|
1057
|
+
const fontSize = 12;
|
|
1058
|
+
|
|
1059
|
+
// -- Tooltip element (shared across all flame graph containers) --
|
|
1060
|
+
var tooltip = document.getElementById('fg-tooltip');
|
|
1061
|
+
var ttName = tooltip ? tooltip.querySelector('.tt-name') : null;
|
|
1062
|
+
var ttInfo = tooltip ? tooltip.querySelector('.tt-info') : null;
|
|
1063
|
+
|
|
1064
|
+
// -- Hover → show tooltip near mouse --
|
|
1065
|
+
frames.forEach(function(g) {
|
|
1066
|
+
g.addEventListener('mouseenter', function() {
|
|
1067
|
+
var name = g.getAttribute('data-name') || '';
|
|
1068
|
+
var samples = g.getAttribute('data-samples') || '';
|
|
1069
|
+
var pct = g.getAttribute('data-pct') || '';
|
|
1070
|
+
if (ttName) ttName.textContent = name;
|
|
1071
|
+
if (ttInfo) ttInfo.textContent = samples + ' samples (' + pct + '%)';
|
|
1072
|
+
if (tooltip) tooltip.style.display = 'block';
|
|
1073
|
+
// highlight
|
|
1074
|
+
var rect = g.querySelector('rect');
|
|
1075
|
+
if (rect) rect.setAttribute('opacity', '0.8');
|
|
1076
|
+
});
|
|
1077
|
+
g.addEventListener('mousemove', function(e) {
|
|
1078
|
+
if (!tooltip) return;
|
|
1079
|
+
// Position tooltip near cursor, offset slightly so it doesn't cover the frame
|
|
1080
|
+
var tx = e.clientX + 12;
|
|
1081
|
+
var ty = e.clientY - 8;
|
|
1082
|
+
// Prevent tooltip from going off the right edge
|
|
1083
|
+
var tw = tooltip.offsetWidth;
|
|
1084
|
+
if (tx + tw > window.innerWidth - 8) {
|
|
1085
|
+
tx = e.clientX - tw - 12;
|
|
1086
|
+
}
|
|
1087
|
+
// Prevent tooltip from going off the bottom edge
|
|
1088
|
+
var th = tooltip.offsetHeight;
|
|
1089
|
+
if (ty + th > window.innerHeight - 8) {
|
|
1090
|
+
ty = e.clientY - th - 8;
|
|
1091
|
+
}
|
|
1092
|
+
tooltip.style.left = tx + 'px';
|
|
1093
|
+
tooltip.style.top = ty + 'px';
|
|
1094
|
+
});
|
|
1095
|
+
g.addEventListener('mouseleave', function() {
|
|
1096
|
+
if (tooltip) tooltip.style.display = 'none';
|
|
1097
|
+
var rect = g.querySelector('rect');
|
|
1098
|
+
if (rect) rect.setAttribute('opacity', '1');
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
// -- Click to zoom --
|
|
1103
|
+
// We store original positions in data-x, data-y, data-w, data-h.
|
|
1104
|
+
// On click, we scale all frames so the clicked frame fills the full width,
|
|
1105
|
+
// and hide frames that are not in the clicked subtree.
|
|
1106
|
+
var zoomed = false; // track zoom state
|
|
1107
|
+
|
|
1108
|
+
frames.forEach(function(g) {
|
|
1109
|
+
g.style.cursor = 'pointer';
|
|
1110
|
+
g.addEventListener('click', function(e) {
|
|
1111
|
+
e.stopPropagation();
|
|
1112
|
+
var clickX = parseFloat(g.getAttribute('data-x'));
|
|
1113
|
+
var clickW = parseFloat(g.getAttribute('data-w'));
|
|
1114
|
+
var clickY = parseFloat(g.getAttribute('data-y'));
|
|
1115
|
+
|
|
1116
|
+
if (zoomed && clickW > chartWidth * 0.98) {
|
|
1117
|
+
// Clicking a full-width frame while zoomed -> reset
|
|
1118
|
+
fgResetZoom(svg, frames, chartWidth, xPad, fontSize);
|
|
1119
|
+
zoomed = false;
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Scale factor: how much to expand clicked frame to fill chart width
|
|
1124
|
+
var scale = chartWidth / clickW;
|
|
1125
|
+
var offsetX = clickX - xPad;
|
|
1126
|
+
|
|
1127
|
+
zoomed = true;
|
|
1128
|
+
|
|
1129
|
+
frames.forEach(function(f) {
|
|
1130
|
+
var fx = parseFloat(f.getAttribute('data-x'));
|
|
1131
|
+
var fw = parseFloat(f.getAttribute('data-w'));
|
|
1132
|
+
var fy = parseFloat(f.getAttribute('data-y'));
|
|
1133
|
+
var fh = parseFloat(f.getAttribute('data-h'));
|
|
1134
|
+
|
|
1135
|
+
// Determine visibility:
|
|
1136
|
+
// 1. Frames at the same or deeper level that overlap with clicked range
|
|
1137
|
+
// 2. Ancestor frames (at shallower level) that contain the clicked frame
|
|
1138
|
+
var fRight = fx + fw;
|
|
1139
|
+
var clickRight = clickX + clickW;
|
|
1140
|
+
var overlaps = fx < clickRight && fRight > clickX;
|
|
1141
|
+
var isAncestor = fy > clickY && fx <= clickX && fRight >= clickRight;
|
|
1142
|
+
var isDescendant = fy <= clickY && overlaps;
|
|
1143
|
+
|
|
1144
|
+
if (!overlaps && !isAncestor) {
|
|
1145
|
+
// Not in subtree – hide
|
|
1146
|
+
f.style.display = 'none';
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
f.style.display = '';
|
|
1151
|
+
var rect = f.querySelector('rect');
|
|
1152
|
+
var text = f.querySelector('text');
|
|
1153
|
+
var clipRect = f.querySelector('clipPath rect');
|
|
1154
|
+
|
|
1155
|
+
// Compute new position: scale and shift
|
|
1156
|
+
var newX = xPad + (fx - clickX) * scale;
|
|
1157
|
+
var newW = fw * scale;
|
|
1158
|
+
|
|
1159
|
+
// Clamp to chart boundaries
|
|
1160
|
+
if (newX < xPad) {
|
|
1161
|
+
newW -= (xPad - newX);
|
|
1162
|
+
newX = xPad;
|
|
1163
|
+
}
|
|
1164
|
+
if (newX + newW > xPad + chartWidth) {
|
|
1165
|
+
newW = xPad + chartWidth - newX;
|
|
1166
|
+
}
|
|
1167
|
+
if (newW < 0.1) {
|
|
1168
|
+
f.style.display = 'none';
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
if (rect) {
|
|
1173
|
+
rect.setAttribute('x', newX.toFixed(1));
|
|
1174
|
+
rect.setAttribute('width', newW.toFixed(1));
|
|
1175
|
+
}
|
|
1176
|
+
// Keep the clipPath rectangle in sync so text is clipped correctly
|
|
1177
|
+
if (clipRect) {
|
|
1178
|
+
clipRect.setAttribute('x', newX.toFixed(1));
|
|
1179
|
+
clipRect.setAttribute('width', newW.toFixed(1));
|
|
1180
|
+
}
|
|
1181
|
+
if (text) {
|
|
1182
|
+
text.setAttribute('x', (newX + 3).toFixed(1));
|
|
1183
|
+
// Recompute label truncation with ellipsis
|
|
1184
|
+
var name = f.getAttribute('data-name') || '';
|
|
1185
|
+
var charW = fontSize * 0.60;
|
|
1186
|
+
var availW = newW - 6;
|
|
1187
|
+
var maxChars = availW > 0 ? Math.floor(availW / charW) : 0;
|
|
1188
|
+
var label;
|
|
1189
|
+
if (maxChars >= name.length) {
|
|
1190
|
+
label = name;
|
|
1191
|
+
} else if (maxChars > 3) {
|
|
1192
|
+
label = name.substring(0, maxChars - 1) + '\u2026';
|
|
1193
|
+
} else if (maxChars > 0) {
|
|
1194
|
+
label = name.substring(0, maxChars);
|
|
1195
|
+
} else {
|
|
1196
|
+
label = '';
|
|
1197
|
+
}
|
|
1198
|
+
text.textContent = label;
|
|
1199
|
+
text.style.display = newW > 12 ? '' : 'none';
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
if (details) details.textContent = 'Zoomed: ' + g.getAttribute('data-name') +
|
|
1204
|
+
' — click full-width frame or background to reset';
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
// Click on SVG background to reset zoom
|
|
1209
|
+
svg.addEventListener('click', function(e) {
|
|
1210
|
+
if (e.target === svg || e.target.tagName === 'rect' && !e.target.closest('.fg-frame')) {
|
|
1211
|
+
if (zoomed) {
|
|
1212
|
+
fgResetZoom(svg, frames, chartWidth, xPad, fontSize);
|
|
1213
|
+
zoomed = false;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function fgResetZoom(svg, frames, chartWidth, xPad, fontSize) {
|
|
1220
|
+
var details = svg.querySelector('.fg-details');
|
|
1221
|
+
frames.forEach(function(f) {
|
|
1222
|
+
f.style.display = '';
|
|
1223
|
+
var ox = parseFloat(f.getAttribute('data-x'));
|
|
1224
|
+
var ow = parseFloat(f.getAttribute('data-w'));
|
|
1225
|
+
var rect = f.querySelector('rect');
|
|
1226
|
+
var text = f.querySelector('text');
|
|
1227
|
+
var clipRect = f.querySelector('clipPath rect');
|
|
1228
|
+
if (rect) {
|
|
1229
|
+
rect.setAttribute('x', ox.toFixed(1));
|
|
1230
|
+
rect.setAttribute('width', ow.toFixed(1));
|
|
1231
|
+
}
|
|
1232
|
+
if (clipRect) {
|
|
1233
|
+
clipRect.setAttribute('x', ox.toFixed(1));
|
|
1234
|
+
clipRect.setAttribute('width', ow.toFixed(1));
|
|
1235
|
+
}
|
|
1236
|
+
if (text) {
|
|
1237
|
+
var name = f.getAttribute('data-name') || '';
|
|
1238
|
+
var charW = fontSize * 0.60;
|
|
1239
|
+
var availW = ow - 6;
|
|
1240
|
+
var maxChars = availW > 0 ? Math.floor(availW / charW) : 0;
|
|
1241
|
+
var label;
|
|
1242
|
+
if (maxChars >= name.length) {
|
|
1243
|
+
label = name;
|
|
1244
|
+
} else if (maxChars > 3) {
|
|
1245
|
+
label = name.substring(0, maxChars - 1) + '\u2026';
|
|
1246
|
+
} else if (maxChars > 0) {
|
|
1247
|
+
label = name.substring(0, maxChars);
|
|
1248
|
+
} else {
|
|
1249
|
+
label = '';
|
|
1250
|
+
}
|
|
1251
|
+
text.textContent = label;
|
|
1252
|
+
text.style.display = ow > 12 ? '' : 'none';
|
|
1253
|
+
text.setAttribute('x', (ox + 3).toFixed(1));
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
if (details) details.textContent = ' ';
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// -- Init --
|
|
1260
|
+
renderChartMetricTabs();
|
|
1261
|
+
initChart();
|
|
1262
|
+
renderQpsChart();
|
|
1263
|
+
renderSchema();
|
|
1264
|
+
renderQuerySelector();
|
|
1265
|
+
renderErrorLogs();
|
|
1266
|
+
|
|
1267
|
+
// -- Error Logs Renderer (standalone section) --
|
|
1268
|
+
function renderErrorLogs() {
|
|
1269
|
+
var section = document.getElementById('error-logs-section');
|
|
1270
|
+
var content = document.getElementById('error-logs-content');
|
|
1271
|
+
|
|
1272
|
+
// Check if any DBMS has any error logs across all queries
|
|
1273
|
+
var hasAny = false;
|
|
1274
|
+
DATA.dbms.forEach(function(d) {
|
|
1275
|
+
d.queries.forEach(function(q) {
|
|
1276
|
+
if (q.error_logs && q.error_logs.length > 0) hasAny = true;
|
|
1277
|
+
});
|
|
1278
|
+
});
|
|
1279
|
+
if (!hasAny) return;
|
|
1280
|
+
|
|
1281
|
+
section.style.display = '';
|
|
1282
|
+
var html = '';
|
|
1283
|
+
|
|
1284
|
+
// Build tabs for each DBMS that has errors
|
|
1285
|
+
html += '<div class="tabs" id="err-tabs" style="margin-bottom:16px">';
|
|
1286
|
+
var firstActive = true;
|
|
1287
|
+
DATA.dbms.forEach(function(d, di) {
|
|
1288
|
+
var totalErrs = 0;
|
|
1289
|
+
d.queries.forEach(function(q) { totalErrs += q.errors; });
|
|
1290
|
+
if (totalErrs === 0) return;
|
|
1291
|
+
var colorBg = DBMS_COLORS_BG[di % DBMS_COLORS_BG.length];
|
|
1292
|
+
html += '<div class="tab' + (firstActive ? ' active' : '') + '" ' +
|
|
1293
|
+
'style="' + (firstActive ? 'background:' + colorBg + ';border-color:' + colorBg + ';color:#fff' : '') + '" ' +
|
|
1294
|
+
'data-color="' + colorBg + '" ' +
|
|
1295
|
+
'onclick="switchErrTab(this,\'err-panel-' + di + '\')">' +
|
|
1296
|
+
esc(d.name) + ' <span style="font-size:12px;opacity:0.8">(' + totalErrs + ')</span></div>';
|
|
1297
|
+
firstActive = false;
|
|
1298
|
+
});
|
|
1299
|
+
html += '</div>';
|
|
1300
|
+
|
|
1301
|
+
// Build content panels for each DBMS
|
|
1302
|
+
firstActive = true;
|
|
1303
|
+
DATA.dbms.forEach(function(d, di) {
|
|
1304
|
+
var totalErrs = 0;
|
|
1305
|
+
d.queries.forEach(function(q) { totalErrs += q.errors; });
|
|
1306
|
+
if (totalErrs === 0) return;
|
|
1307
|
+
|
|
1308
|
+
html += '<div id="err-panel-' + di + '" style="' + (firstActive ? '' : 'display:none') + '">';
|
|
1309
|
+
firstActive = false;
|
|
1310
|
+
|
|
1311
|
+
d.queries.forEach(function(q) {
|
|
1312
|
+
var logs = q.error_logs || [];
|
|
1313
|
+
if (q.errors === 0) return;
|
|
1314
|
+
|
|
1315
|
+
var errId = 'errlog-' + di + '-' + q.name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
1316
|
+
html += '<div class="err-section" style="margin-bottom:12px">' +
|
|
1317
|
+
'<div class="err-toggle open" id="' + errId + '-toggle" onclick="toggleErrLogs(\'' + errId + '\')">' +
|
|
1318
|
+
'<span class="arrow">▶</span> ' +
|
|
1319
|
+
'<span style="color:var(--blue);font-weight:600">' + esc(q.name) + '</span>' +
|
|
1320
|
+
' — ' + q.errors + ' error(s)' +
|
|
1321
|
+
(logs.length > 0 && logs.length < q.errors ? ' (showing first ' + logs.length + ')' : '') +
|
|
1322
|
+
'</div>' +
|
|
1323
|
+
'<div class="err-list" id="' + errId + '-body">';
|
|
1324
|
+
|
|
1325
|
+
if (logs.length === 0) {
|
|
1326
|
+
html += '<div class="err-item"><div class="err-msg">No detailed error info captured</div></div>';
|
|
1327
|
+
} else {
|
|
1328
|
+
// Deduplicate: group by error message
|
|
1329
|
+
var errGroups = {};
|
|
1330
|
+
logs.forEach(function(el) {
|
|
1331
|
+
var key = el.error;
|
|
1332
|
+
if (!errGroups[key]) errGroups[key] = {error: el.error, sqls: [], count: 0};
|
|
1333
|
+
errGroups[key].count++;
|
|
1334
|
+
if (errGroups[key].sqls.length < 3) errGroups[key].sqls.push(el.sql);
|
|
1335
|
+
});
|
|
1336
|
+
Object.keys(errGroups).forEach(function(key) {
|
|
1337
|
+
var g = errGroups[key];
|
|
1338
|
+
html += '<div class="err-item">' +
|
|
1339
|
+
'<div class="err-msg">' + esc(g.error) +
|
|
1340
|
+
(g.count > 1 ? ' <span style="font-weight:400;color:var(--fg2)">(' + g.count + ' occurrences)</span>' : '') +
|
|
1341
|
+
'</div>';
|
|
1342
|
+
g.sqls.forEach(function(sql, si) {
|
|
1343
|
+
html += '<div class="err-sql" style="margin-top:4px">' +
|
|
1344
|
+
'<span style="color:var(--fg2);font-size:11px">SQL' + (g.sqls.length > 1 ? ' #' + (si+1) : '') + ':</span> ' +
|
|
1345
|
+
esc(sql) + '</div>';
|
|
1346
|
+
});
|
|
1347
|
+
html += '</div>';
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
html += '</div></div>';
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
html += '</div>';
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
content.innerHTML = html;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function switchErrTab(el, panelId) {
|
|
1360
|
+
// Deactivate all tabs
|
|
1361
|
+
var tabs = el.parentElement.querySelectorAll('.tab');
|
|
1362
|
+
tabs.forEach(function(t) {
|
|
1363
|
+
t.classList.remove('active');
|
|
1364
|
+
t.style.background = '';
|
|
1365
|
+
t.style.borderColor = '';
|
|
1366
|
+
t.style.color = '';
|
|
1367
|
+
});
|
|
1368
|
+
// Hide all panels
|
|
1369
|
+
var container = el.parentElement.parentElement;
|
|
1370
|
+
container.querySelectorAll('[id^="err-panel-"]').forEach(function(p) {
|
|
1371
|
+
p.style.display = 'none';
|
|
1372
|
+
});
|
|
1373
|
+
// Activate clicked tab
|
|
1374
|
+
el.classList.add('active');
|
|
1375
|
+
var colorBg = el.getAttribute('data-color');
|
|
1376
|
+
el.style.background = colorBg;
|
|
1377
|
+
el.style.borderColor = colorBg;
|
|
1378
|
+
el.style.color = '#fff';
|
|
1379
|
+
// Show panel
|
|
1380
|
+
document.getElementById(panelId).style.display = '';
|
|
1381
|
+
}
|
|
1382
|
+
</script>
|
|
1383
|
+
</body>
|
|
1384
|
+
</html>"""
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
def write_bench_html_report(path: str, result: BenchmarkResult):
|
|
1388
|
+
"""Generate a self-contained HTML benchmark report.
|
|
1389
|
+
|
|
1390
|
+
Uses ECharts (CDN) for an interactive grouped bar chart that puts
|
|
1391
|
+
all queries from all DBMS targets side-by-side in one view, plus
|
|
1392
|
+
a CSS-based Overall QPS bar, and a detail
|
|
1393
|
+
table with DBMS tabs.
|
|
1394
|
+
|
|
1395
|
+
If profiling was enabled, embeds per-query SVG flame graphs.
|
|
1396
|
+
"""
|
|
1397
|
+
data = _build_data(result)
|
|
1398
|
+
|
|
1399
|
+
# Build flame graph data array: [{dbms, query, svg}, ...]
|
|
1400
|
+
# Only include tdsql flame graphs — other DBMS profiling is skipped.
|
|
1401
|
+
fg_data = []
|
|
1402
|
+
seen_svgs = set() # deduplicate concurrent mode shared SVGs
|
|
1403
|
+
for dr in result.dbms_results:
|
|
1404
|
+
if dr.dbms_name.lower() != "tdsql":
|
|
1405
|
+
continue
|
|
1406
|
+
for qs in dr.query_stats:
|
|
1407
|
+
if qs.flamegraph_svg:
|
|
1408
|
+
# In concurrent mode, all queries share the same SVG;
|
|
1409
|
+
# deduplicate by (dbms, svg_hash)
|
|
1410
|
+
svg_hash = hash(qs.flamegraph_svg)
|
|
1411
|
+
dedup_key = (dr.dbms_name, svg_hash)
|
|
1412
|
+
if dedup_key in seen_svgs:
|
|
1413
|
+
continue
|
|
1414
|
+
seen_svgs.add(dedup_key)
|
|
1415
|
+
fg_data.append({
|
|
1416
|
+
"dbms": dr.dbms_name,
|
|
1417
|
+
"query": qs.query_name,
|
|
1418
|
+
"svg": qs.flamegraph_svg,
|
|
1419
|
+
})
|
|
1420
|
+
|
|
1421
|
+
page = _HTML_TEMPLATE
|
|
1422
|
+
page = page.replace("{{WORKLOAD}}", _escape(result.workload_name))
|
|
1423
|
+
|
|
1424
|
+
def _safe_json(obj):
|
|
1425
|
+
s = json.dumps(obj, ensure_ascii=False)
|
|
1426
|
+
s = s.replace("<", "\\u003c")
|
|
1427
|
+
return s
|
|
1428
|
+
|
|
1429
|
+
page = page.replace("{{DATA_JSON}}", _safe_json(data))
|
|
1430
|
+
|
|
1431
|
+
# Flame graph data: SVG content is NOT JSON-encoded (it's raw HTML).
|
|
1432
|
+
# We build a JS array of objects with the SVG as a string.
|
|
1433
|
+
fg_js_items = []
|
|
1434
|
+
for fg in fg_data:
|
|
1435
|
+
# Escape SVG for safe embedding in JS template literal.
|
|
1436
|
+
# CRITICAL: We must escape </script so the browser's HTML parser
|
|
1437
|
+
# does not prematurely close the <script> block when it encounters
|
|
1438
|
+
# </script> tags embedded inside SVG CDATA sections.
|
|
1439
|
+
svg_escaped = (fg["svg"]
|
|
1440
|
+
.replace("\\", "\\\\")
|
|
1441
|
+
.replace("`", "\\`")
|
|
1442
|
+
.replace("${", "\\${")
|
|
1443
|
+
.replace("</script", "<\\/script")
|
|
1444
|
+
.replace("</Script", "<\\/Script")
|
|
1445
|
+
.replace("</SCRIPT", "<\\/SCRIPT"))
|
|
1446
|
+
fg_js_items.append(
|
|
1447
|
+
'{dbms:' + json.dumps(fg["dbms"]) +
|
|
1448
|
+
',query:' + json.dumps(fg["query"]) +
|
|
1449
|
+
',svg:`' + svg_escaped + '`}'
|
|
1450
|
+
)
|
|
1451
|
+
fg_js = "[" + ",".join(fg_js_items) + "]" if fg_js_items else "[]"
|
|
1452
|
+
page = page.replace("{{FLAMEGRAPH_JSON}}", fg_js)
|
|
1453
|
+
|
|
1454
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
1455
|
+
f.write(page)
|
|
1456
|
+
|
|
1457
|
+
log.info("Benchmark HTML report written: %s", path)
|