mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
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]>
657 lines
24 KiB
HTML
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>
|