Files
zeekayandHanzo Dev 9bb4cfec42 the last of the name in the pages, and what is left is a pull coordinate
Renames the remaining button id (btnQueryClickhouse → btnQueryDatastore, one
definition and ten references across the two visualizers) and the test harness's
prose about which server it drives.

What stays, and it is not branding: `clickhouse/mysql-js-client` is an upstream
image the integration tests still PULL (ci/defs/defs.py, and the compose file
that runs it). A pull coordinate names someone else's published artifact, so
changing it is publishing our own test image rather than editing a string — the
harness comment now says that where a reader will be standing.

Also still standing, for the reason recorded at its embed site: the
`clickhouse_lexer_*` exports baked into docs.html's WebAssembly payload.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 10:23:56 -07:00

657 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trace Gantt</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/d3-gantt.css">
<style>
#hourglass
{
display: none;
font-size: 110%;
color: #888;
animation: hourglass-animation 2s linear infinite;
}
@keyframes hourglass-animation {
0% { transform: none; }
25% { transform: rotate(180deg); }
50% { transform: rotate(180deg); }
75% { transform: rotate(360deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<script language="javascript" type="text/javascript" src="js/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="js/bootstrap.min.js"></script>
<script language="javascript" type="text/javascript" src="js/d3.v4.min.js"></script>
<script language="javascript" type="text/javascript" src="js/d3-tip-0.8.0-alpha.1.js"></script>
<script language="javascript" type="text/javascript" src="js/d3-gantt.js"></script>
<script language="javascript" type="text/javascript" src="js/query-datastore.js"></script>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Hanzo Datastore trace-visualizer</a>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<div class="d-flex align-items-center">
<button type="button" class="btn btn-primary me-2" id="toolbar-load" data-bs-toggle="modal" data-bs-target="#loadModal">Load</button>
<button type="button" class="btn btn-primary me-2" id="toolbar-find" data-bs-toggle="modal" data-bs-target="#findModal">Find</button>
<span class="navbar-text" id="status-text"></span>
</div>
</div>
</div>
</nav>
<div id="placeholder" class="chart-placeholder"></div>
<div class="modal fade" id="loadModal" tabindex="-1" role="dialog" aria-labelledby="loadModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="loadModalLabel">Load Data</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" href="#tabFileUpload" role="tab" data-bs-toggle="tab">Upload JSON</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#tabDatastore" role="tab" data-bs-toggle="tab">From ClickHouse</a>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content" style="margin-top: 15px;">
<!-- File Upload Tab -->
<div class="tab-pane show active" id="tabFileUpload">
<input type="file" id="loadFiles" value="Load" />
</div>
<!-- Datastore Query Tab -->
<div class="tab-pane" id="tabDatastore">
<form id="params">
<div id="connection-params" style="margin-bottom: 10px;">
<input spellcheck="false" id="url" type="text" value="http://localhost:8123" placeholder="URL" class="form-control" style="margin-bottom: 5px;" />
<input spellcheck="false" id="user" type="text" value="" placeholder="user" class="form-control" style="margin-bottom: 5px;" />
<input spellcheck="false" id="password" type="password" placeholder="password" value="" class="form-control" style="margin-bottom: 5px;" />
</div>
<div id="query-templates-container" style="margin-bottom: 5px;">
<label>Query templates:</label>
</div>
<textarea spellcheck="false" id="query" rows="26" class="form-control" placeholder="SQL Query"></textarea>
<div class="checkbox" style="margin-top: 10px;">
<label>
<input type="checkbox" id="flushLogsCheckbox" checked> Run SYSTEM FLUSH LOGS
</label>
</div>
<div id="query-error" style="color: red; margin-top: 10px; display: none;"></div>
</form>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btnDoLoad">Load</button>
<button type="button" class="btn btn-success" id="btnQueryDatastore" style="display: none;">Query</button>
<div id="hourglass">⧗</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="findModal" tabindex="-1" role="dialog" aria-labelledby="findModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="findModalLabel">Span Filter</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="text" id="filterPattern" value="" class="form-control"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btnDoFindClear">Clear</button>
<button type="button" class="btn btn-primary" id="btnDoFind">Find</button>
</div>
</div>
</div>
</div>
</body>
</html>
<script language="javascript">
const queryTemplates = {
"Processors": `WITH '__PUT_YOUR_QUERY_ID_HERE__' AS my_query_id
SELECT
('thread #' || leftPad(attribute['datastore.thread_id'], 6, '0')) AS group,
replaceRegexpOne(operation_name, '(.*)_.*', '\\1') AS operation_name,
start_time_us,
finish_time_us,
sipHash64(operation_name) AS color,
attribute
FROM system.opentelemetry_span_log
WHERE 1
AND trace_id IN (
SELECT trace_id
FROM system.opentelemetry_span_log
WHERE (attribute['datastore.query_id']) IN (SELECT query_id FROM system.query_log WHERE initial_query_id = my_query_id)
)
AND operation_name !='query'
AND operation_name NOT LIKE '%Pipeline%'
AND operation_name NOT LIKE 'TCPHandler%'
AND operation_name NOT LIKE 'Query%'
AND operation_name NOT LIKE 'CPU_LEASE%'
ORDER BY
group ASC,
parent_span_id ASC,
start_time_us ASC
SETTINGS output_format_json_named_tuples_as_objects = 1, skip_unavailable_shards = 1`,
"CPU Scheduling (Query)": `WITH '__PUT_YOUR_QUERY_ID_HERE__' AS my_query_id,
-- Step 1: Extract raw spans data
raw_spans AS (
SELECT
operation_name,
start_time_us,
finish_time_us,
attribute['lease_id'] AS lease_id,
attribute['thread_number'] AS thread_number
FROM system.opentelemetry_span_log
WHERE 1
AND trace_id IN (
SELECT trace_id
FROM system.opentelemetry_span_log
WHERE (attribute['datastore.query_id']) IN (
SELECT query_id
FROM system.query_log
WHERE initial_query_id = my_query_id
)
)
AND operation_name in ['CPU_LEASE_START', 'CPU_LEASE_STOP', 'CPU_LEASE_PREEMPTION', 'CPU_LEASE_DOWNSCALED']
AND attribute['lease_id'] IS NOT NULL
AND attribute['thread_number'] IS NOT NULL
),
-- Step 2: Generate timeline events for each lease-thread pair
timeline_events AS (
SELECT
lease_id,
thread_number,
-- Generate events: START has 1 event, STOP has 1 event, PREEMPTION has 2 events
arrayJoin(
CASE
WHEN operation_name = 'CPU_LEASE_START' THEN [(start_time_us, 'START')]
WHEN operation_name = 'CPU_LEASE_STOP' THEN [(start_time_us, 'STOP')]
WHEN operation_name = 'CPU_LEASE_PREEMPTION' THEN
[(start_time_us, 'PREEMPT_START'), (finish_time_us, 'PREEMPT_END')]
WHEN operation_name = 'CPU_LEASE_DOWNSCALED' THEN [(start_time_us, 'DOWNSCALE')]
ELSE []
END
) AS event_data
FROM raw_spans
),
-- Step 3: Extract individual events and sort by timestamp
sorted_events AS (
SELECT
lease_id,
thread_number,
event_data.1 AS event_time_us,
event_data.2 AS event_type
FROM timeline_events
ORDER BY event_time_us
),
-- Step 4: Add sequence numbers and identify interval transitions
events_with_sequence AS (
SELECT
lease_id,
thread_number,
event_time_us,
event_type,
row_number() OVER (
PARTITION BY lease_id, thread_number
ORDER BY event_time_us ASC
) AS event_seq
FROM sorted_events
),
-- Step 5: Pair consecutive events to form intervals
interval_pairs AS (
SELECT
lease_id,
thread_number,
event_time_us AS start_time_us,
event_type AS start_event,
-- Get next event timestamp using window function
lead(event_time_us) OVER (
PARTITION BY lease_id, thread_number
ORDER BY event_time_us ASC
) AS end_time_us,
lead(event_type) OVER (
PARTITION BY lease_id, thread_number
ORDER BY event_time_us ASC
) AS end_event
FROM events_with_sequence
),
-- Step 6: Classify intervals as RUNNING or PREEMPTED
classified_intervals AS (
SELECT
lease_id,
thread_number,
start_time_us,
end_time_us,
CASE
WHEN end_event = 'START' THEN NULL -- Do not create intervals when thread is downscaled
WHEN start_event = 'PREEMPT_START' OR end_event = 'PREEMPT_END' THEN 'PREEMPTED'
ELSE 'RUNNING'
END AS interval_type,
start_event,
end_event
FROM interval_pairs
WHERE end_time_us != 0 -- Exclude last event which has no pair
)
-- Final output: one row per interval
SELECT
start_time_us,
end_time_us AS finish_time_us,
interval_type || '_' || thread_number AS operation_name,
lease_id || '#' || thread_number AS group,
sipHash64(interval_type) AS color
FROM classified_intervals
WHERE interval_type IS NOT NULL
ORDER BY
lease_id::UInt64 ASC,
thread_number::UInt64 DESC,
start_time_us ASC
SETTINGS output_format_json_named_tuples_as_objects = 1, skip_unavailable_shards = 1;`,
"CPU Scheduling (Range)": `WITH
now() - INTERVAL 10 MINUTE as start_ts, -- PUT START TIME HERE
now() as end_ts, -- PUT END TIME HERE
-- Step 0: Get query IDs within the specified time range
query_ids as (
SELECT query_id
FROM system.query_log
WHERE Settings['workload'] IS NOT NULL
AND type = 'QueryFinish'
AND event_time > start_ts
AND event_time < end_ts
),
-- Step 1: Extract raw spans data
raw_spans AS (
SELECT
operation_name,
start_time_us,
finish_time_us,
attribute['lease_id'] AS lease_id,
attribute['thread_number'] AS thread_number,
attribute['workload'] AS workload
FROM system.opentelemetry_span_log
WHERE 1
-- AND lease_id::UInt64 = 346 -- Uncomment to filter by specific lease_id
-- AND thread_number::UInt64 = 0 -- Uncomment to filter by specific thread_number
AND trace_id IN (
SELECT trace_id
FROM system.opentelemetry_span_log
WHERE (attribute['datastore.query_id']) IN query_ids
)
AND operation_name in ['CPU_LEASE_START', 'CPU_LEASE_STOP', 'CPU_LEASE_PREEMPTION', 'CPU_LEASE_DOWNSCALED'
-- , 'CPU_LEASE_REPORT' -- Uncomment if you want to include CPU_LEASE_REPORT (also see step 2)
]
AND attribute['lease_id'] IS NOT NULL
AND attribute['thread_number'] IS NOT NULL
),
-- Step 2: Generate timeline events for each lease-thread pair
timeline_events AS (
SELECT
lease_id,
thread_number,
workload,
-- Generate events: START/STOP/DOWNSCALED has 1 event, PREEMPTION has 2 events
arrayJoin(
CASE
WHEN operation_name = 'CPU_LEASE_START' THEN [(start_time_us, 'START')]
WHEN operation_name = 'CPU_LEASE_STOP' THEN [(start_time_us, 'STOP')]
-- WHEN operation_name = 'CPU_LEASE_REPORT' THEN [(start_time_us, 'REPORT')] -- Uncomment if you want to include CPU_LEASE_REPORT (also see step 1)
WHEN operation_name = 'CPU_LEASE_PREEMPTION' THEN
[(start_time_us, 'PREEMPT_START'), (finish_time_us, 'PREEMPT_END')]
WHEN operation_name = 'CPU_LEASE_DOWNSCALED' THEN [(start_time_us, 'DOWNSCALE')]
ELSE []
END
) AS event_data
FROM raw_spans
),
-- Step 3: Extract individual events and sort by timestamp
sorted_events AS (
SELECT
lease_id,
thread_number,
workload,
event_data.1 AS event_time_us,
event_data.2 AS event_type
FROM timeline_events
ORDER BY event_time_us
),
-- Step 4: Add sequence numbers and identify interval transitions
events_with_sequence AS (
SELECT
lease_id,
thread_number,
workload,
event_time_us,
event_type,
row_number() OVER (
PARTITION BY lease_id, thread_number
ORDER BY event_time_us ASC
) AS event_seq
FROM sorted_events
),
-- Step 5: Pair consecutive events to form intervals
interval_pairs AS (
SELECT
lease_id,
thread_number,
workload,
event_time_us AS start_time_us,
event_type AS start_event,
-- Get next event timestamp using window function
lead(event_time_us) OVER (
PARTITION BY lease_id, thread_number
ORDER BY event_time_us ASC
) AS end_time_us,
lead(event_type) OVER (
PARTITION BY lease_id, thread_number
ORDER BY event_time_us ASC
) AS end_event
FROM events_with_sequence
),
-- Step 6: Classify intervals as RUNNING or PREEMPTED
classified_intervals AS (
SELECT
lease_id,
thread_number,
workload,
start_time_us,
end_time_us,
CASE
WHEN end_event = 'START' THEN NULL -- Do not create intervals when thread is downscaled
WHEN start_event = 'PREEMPT_START' OR end_event = 'PREEMPT_END' THEN 'PREEMPTED'
ELSE 'RUNNING'
END AS interval_type,
start_event,
end_event
FROM interval_pairs
WHERE end_time_us != 0 -- Exclude last event which has no pair
)
-- Final output: one row per interval
SELECT
start_time_us,
end_time_us AS finish_time_us,
interval_type || '_' || thread_number || '_' || workload AS operation_name,
workload || ' ' || lease_id || '#' || thread_number AS group,
sipHash64(interval_type || workload) AS color
FROM classified_intervals
WHERE interval_type IS NOT NULL
ORDER BY
lease_id::UInt64 ASC,
thread_number::UInt64 DESC,
start_time_us ASC
SETTINGS output_format_json_named_tuples_as_objects = 1, skip_unavailable_shards = 1;`
};
$(document).ready(function(){
$('#query').val(queryTemplates['Processors']);
const container = $('#query-templates-container');
for (const templateName in queryTemplates) {
const button = $('<button></button>')
.attr('type', 'button')
.addClass('btn btn-default btn-sm template-btn')
.attr('data-template', templateName)
.text(templateName);
container.append(button);
}
});
var example_json = [];
var example_colors = ["#f88", "#8f8", "#88f", "#888", "#aaa"];
for (let i = 0; i < 1024; i++) {
example_json.push({
t1: i + 50 - Math.random() * 60,
t2: i + 50 + Math.random() * 60,
band: "band" + (i % 128),
color: example_colors[i % example_colors.length],
text: "it is span #" + i
});
}
example_json.bands = new Set(example_json.map(x => x.band));
example_json.max_time_ms = Math.max(...example_json.map(x => x.t2));
var chart = null;
function updateStatus(pattern = '') {
if (pattern != '') {
const runtime = chart.filteredRuntime();
const total = chart.totalRuntime();
$("#status-text").css("color", "").text(`
${chart.filteredCount()}/${chart.totalCount()} spans
${runtime.toFixed(1)}/${total.toFixed(1)} runtime (${(runtime*100/total).toFixed(1)}%)
matching
`);
}
else
{
$("#status-text").css("color", "").text(`
${chart.totalCount()} spans
${chart.totalRuntime().toFixed(1)} runtime
total
`);
}
}
function renderChart(parsed) {
if (chart != null) {
chart.destroy();
}
let view_height = window.innerHeight - $("#placeholder")[0].getBoundingClientRect().y - 40;
let data_height = parsed.bands.size * 8;
chart = d3.gantt()
.height(Math.max(view_height, data_height))
.view_height(view_height)
.selector("#placeholder")
.timeDomain([0, parsed.max_time_ms])
;
chart(parsed);
updateStatus();
}
// Show correct button depending on active tab
$('a[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) {
if ($(e.target).attr('href') === '#tabDatastore') {
$('#btnDoLoad').hide();
$('#btnQueryDatastore').show();
} else {
$('#btnDoLoad').show();
$('#btnQueryDatastore').hide();
}
});
// Handle the Datastore query
let query_controller = null;
$('#btnQueryDatastore').on('click', async function () {
if (query_controller == null) {
let rows = [];
let error = '';
const errorDiv = document.getElementById('query-error');
errorDiv.style.display = 'none';
errorDiv.textContent = '';
document.getElementById('hourglass').style.display = 'inline-block';
$('#btnQueryDatastore').text("Stop");
if ($('#flushLogsCheckbox').is(':checked')) {
query_controller = new AbortController();
await queryDatastore({
host: $('#url').val(),
user: $('#user').val(),
password: $('#password').val(),
query: "SYSTEM FLUSH LOGS",
for_each_row: (data) => {},
on_error: (errorMsg) => error = errorMsg,
controller: query_controller
});
}
if (error == '') {
query_controller = new AbortController();
await queryDatastore({
host: $('#url').val(),
user: $('#user').val(),
password: $('#password').val(),
query: $('#query').val(),
for_each_row: (data) => rows.push(data),
on_error: (errorMsg) => error = errorMsg,
controller: query_controller
});
}
query_controller = null;
$('#btnQueryDatastore').text("Query");
document.getElementById('hourglass').style.display = 'none';
if (error != '') {
errorDiv.textContent = error;
errorDiv.style.display = 'block';
} else {
renderChart(parseDatastoreTrace(rows));
bootstrap.Modal.getInstance(document.getElementById('loadModal')).hide();
}
} else { // Cancel query
query_controller.abort();
}
});
$(document).on('click', '.template-btn', function () {
const templateName = $(this).data('template');
$('#query').val(queryTemplates[templateName]);
});
// Handle uploaded JSON
$("#btnDoLoad").click(function(){
let element = document.getElementById('loadFiles');
let files = element.files;
if (files.length <= 0) {
return false;
}
let fr = new FileReader();
fr.onload = function(e) {
renderChart(parseDatastoreTrace(JSON.parse(e.target.result).data));
}
fr.readAsText(files.item(0));
element.value = '';
bootstrap.Modal.getInstance(document.getElementById('loadModal')).hide();
});
function doFindPattern() {
if (chart) {
const pattern = document.getElementById('filterPattern').value;
chart.filter(pattern);
updateStatus(pattern);
}
bootstrap.Modal.getInstance(document.getElementById('findModal')).hide();
}
$("#btnDoFindClear").click(function(){
document.getElementById('filterPattern').value = '';
doFindPattern();
});
$("#btnDoFind").click(function(){
doFindPattern();
});
function parseDatastoreTrace(rows) {
let min_time_us = Number.MAX_VALUE;
for (let i = 0; i < rows.length; i++) {
let span = rows[i];
min_time_us = Math.min(min_time_us, +span.start_time_us);
}
let max_time_ms = 0;
function convertTime(us) {
let value = (us - min_time_us) / 1000;
max_time_ms = Math.max(max_time_ms, value);
return value;
}
function strHash(str) {
var hash = 0;
if (str.length === 0)
return hash;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
if (hash < 0)
hash = -hash;
return hash;
}
let result = [];
let bands = new Set();
for (let i = 0; i < rows.length; i++) {
let span = rows[i];
let band = Object.values(span.group).join(' ');
let text = span.operation_name;
bands.add(band);
const colorKey = span.color == null ? '' : String(span.color);
result.push({
t1: convertTime(+span.start_time_us),
t2: convertTime(+span.finish_time_us),
band,
color: d3.interpolateRainbow((strHash(colorKey) % 1024) / 1024),
text,
attributes: span.attribute || {}
});
}
result.bands = bands;
result.max_time_ms = max_time_ms;
return result;
}
// On startup, try to load trace.json. If unavailable, fall back to example data.
// This is all optional, the user can always open a trace file from ui instead.
fetch("trace.json").then(function(response) {
if (response.status === 404 || response.status === 0)
throw null; // File not found, silently fall back to example data
if (!response.ok)
throw new Error("Failed to load trace.json: " + response.status + " " + response.statusText);
return response.json();
}).then(function(json) {
renderChart(parseDatastoreTrace(json.data));
}).catch(function(err) {
if (err) {
$("#status-text").text(err.message).css("color", "red");
}
renderChart(example_json);
});
</script>