mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-24 16:42:05 +00:00
442 lines
14 KiB
HTML
442 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>ClickHouse Merges Visualizer</title>
|
|
<link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1NCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDkgOCI+PHN0eWxlPi5ve2ZpbGw6I2ZjMH0ucntmaWxsOnJlZH08L3N0eWxlPjxwYXRoIGQ9Ik0wLDcgaDEgdjEgaC0xIHoiIGNsYXNzPSJyIi8+PHBhdGggZD0iTTAsMCBoMSB2NyBoLTEgeiIgY2xhc3M9Im8iLz48cGF0aCBkPSJNMiwwIGgxIHY4IGgtMSB6IiBjbGFzcz0ibyIvPjxwYXRoIGQ9Ik00LDAgaDEgdjggaC0xIHoiIGNsYXNzPSJvIi8+PHBhdGggZD0iTTYsMCBoMSB2OCBoLTEgeiIgY2xhc3M9Im8iLz48cGF0aCBkPSJNOCwzLjI1IGgxIHYxLjUgaC0xIHoiIGNsYXNzPSJvIi8+PC9zdmc+">
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
html, body {
|
|
height: 100%;
|
|
overflow: auto;
|
|
margin: 0;
|
|
background: #F8F8F8;
|
|
font-size: 16pt;
|
|
}
|
|
body {
|
|
font-family: Liberation Sans, DejaVu Sans, sans-serif, Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji;
|
|
padding: 1rem;
|
|
}
|
|
|
|
input, textarea {
|
|
border: 3px solid #EEE;
|
|
font-size: 16pt;
|
|
padding: 0.25rem;
|
|
}
|
|
|
|
#url {
|
|
width: 80%;
|
|
}
|
|
#user, #password {
|
|
width: 10%;
|
|
}
|
|
#query {
|
|
width: 100%;
|
|
height: 3rem;
|
|
}
|
|
|
|
input[type="button"] {
|
|
background: #FED;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
}
|
|
input[type="button"]:hover {
|
|
background: #F88;
|
|
cursor: pointer;
|
|
}
|
|
|
|
#slower {
|
|
margin-left: 1rem;
|
|
}
|
|
|
|
#speed {
|
|
background: #DDD;
|
|
}
|
|
|
|
#time, #stats {
|
|
padding-left: 1rem;
|
|
font-family: monospace;
|
|
}
|
|
|
|
#stats {
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
#canvas {
|
|
margin-top: 0.25rem;
|
|
font-size: 10pt;
|
|
}
|
|
|
|
.table, .partition {
|
|
padding: 0.5rem;
|
|
border: 3px solid #EEE;
|
|
background: white;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.partition {
|
|
float: left;
|
|
}
|
|
|
|
.table_title, .partition_title {
|
|
text-align: center;
|
|
}
|
|
|
|
.part {
|
|
display: inline-block;
|
|
padding: 0;
|
|
margin: 0.1rem;
|
|
border: 1px solid black;
|
|
background: #FED;
|
|
overflow: hidden;
|
|
position: relative; /* This enables the positioning context for the child elements. */
|
|
}
|
|
|
|
.part_title {
|
|
text-align: center;
|
|
}
|
|
|
|
.part:hover {
|
|
overflow: visible;
|
|
}
|
|
|
|
.part:hover .part_title {
|
|
z-index: 1;
|
|
position: absolute;
|
|
background: yellow;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="inputs">
|
|
<form id="params">
|
|
<div id="connection-params">
|
|
<input spellcheck="false" id="url" type="text" value="" placeholder="URL" /><input spellcheck="false" id="user" type="text" value="" placeholder="user" /><input spellcheck="false" id="password" type="password" placeholder="password" value="" />
|
|
<input id="hidden-submit" type="submit" hidden="true"/>
|
|
</div>
|
|
<textarea spellcheck="false" data-gramm="false" id="query">SELECT * FROM system.part_log ORDER BY event_date, event_time, event_time_microseconds</textarea>
|
|
<input id="play" type="button" value="▶">
|
|
<input id="slower" type="button" value="⏪"><span id="speed">10x</span><input id="faster" type="button" value="⏩"></input>
|
|
<span id="time">0000-00-00 00:00:00</span>
|
|
<span id="stats"></span>
|
|
</form>
|
|
</div>
|
|
<div id="canvas">
|
|
</div>
|
|
<script>
|
|
|
|
let add_http_cors_header = (location.protocol != 'file:');
|
|
|
|
if (!document.getElementById('url').value) {
|
|
document.getElementById('url').value = location.protocol != 'file:' ? location.origin : 'http://localhost:8123/';
|
|
}
|
|
|
|
if (!document.getElementById('user').value) {
|
|
let user = 'default';
|
|
|
|
const current_url = new URL(window.location);
|
|
/// Substitute user name if it's specified in the query string
|
|
const user_from_url = current_url.searchParams.get('user');
|
|
if (user_from_url) {
|
|
user = user_from_url;
|
|
}
|
|
document.getElementById('user').value = user;
|
|
}
|
|
|
|
|
|
function formatValue(v) {
|
|
if (v >= 1000000000000) { return (v / 1000000000000).toFixed(2) + 'T'; }
|
|
if (v >= 1000000000) { return (v / 1000000000).toFixed(2) + 'G'; }
|
|
if (v >= 1000000) { return (v / 1000000).toFixed(2) + 'M'; }
|
|
if (v >= 1000) { return (v / 1000).toFixed(2) + 'K'; }
|
|
return v;
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
let canvas = document.getElementById('canvas');
|
|
let time = document.getElementById('time');
|
|
let stats = document.getElementById('stats');
|
|
|
|
let num_parts = 0;
|
|
let inserted_rows = 0;
|
|
let inserted_bytes = 0;
|
|
let inserted_parts = 0;
|
|
let merged_rows = 0;
|
|
let merged_bytes = 0;
|
|
let merged_parts = 0;
|
|
let currently_active_parts = {};
|
|
|
|
let speed = 100;
|
|
|
|
let prev_time = null;
|
|
async function update(data) {
|
|
curr_time = new Date(data.event_time_microseconds + 'Z');
|
|
time_diff = prev_time ? curr_time - prev_time : 0;
|
|
prev_time = curr_time;
|
|
|
|
if (speed <= 1000) {
|
|
await sleep(time_diff / speed);
|
|
}
|
|
|
|
time.innerText = data.event_time;
|
|
|
|
const table_id = `table-${data.table_uuid}`;
|
|
let table = document.getElementById(table_id);
|
|
if (!table) {
|
|
table = document.createElement('div');
|
|
table.id = table_id;
|
|
table.className = 'table';
|
|
let table_title = document.createElement('div');
|
|
table_title.className = 'table_title';
|
|
table_title.innerText = `${data.database}.${data.table}`;
|
|
table.appendChild(table_title);
|
|
canvas.appendChild(table);
|
|
}
|
|
|
|
const partition_id = `partition-${data.table_uuid}-${data.partition_id}`;
|
|
let partition = document.getElementById(partition_id);
|
|
if (!partition) {
|
|
partition = document.createElement('div');
|
|
partition.id = partition_id;
|
|
partition.className = 'partition';
|
|
let partition_title = document.createElement('div');
|
|
partition_title.className = 'partition_title';
|
|
partition_title.innerText = `${data.partition_id}`;
|
|
partition.appendChild(partition_title);
|
|
table.appendChild(partition);
|
|
}
|
|
|
|
const part_id = `part-${data.table_uuid}-${data.part_name}`;
|
|
|
|
const matches = data.part_name.match(/[\w-]+_(\d+)_(\d+)_(\d+)(?:_(\d+))?/);
|
|
const min_block_id = +matches[1];
|
|
const max_block_id = +matches[2];
|
|
const level = +matches[3];
|
|
|
|
if (data.event_type == 'NewPart' || data.event_type == 'DownloadPart' || data.event_type == 'MergeParts' || data.event_type == 'MutatePart') {
|
|
if (!(data.table_uuid in currently_active_parts)) {
|
|
currently_active_parts[data.table_uuid] = {};
|
|
}
|
|
|
|
if (!(data.part_name in currently_active_parts[data.table_uuid])) {
|
|
currently_active_parts[data.table_uuid][data.part_name] = 1;
|
|
++num_parts;
|
|
|
|
if (level == 0) {
|
|
++inserted_parts;
|
|
inserted_rows += +data.rows;
|
|
inserted_bytes += +data.size_in_bytes;
|
|
} else {
|
|
playClick(Math.min(1, data.size_in_bytes / 10e9));
|
|
++merged_parts;
|
|
merged_rows += +data.rows;
|
|
merged_bytes += +data.size_in_bytes;
|
|
}
|
|
|
|
part = document.createElement('div');
|
|
part.id = part_id;
|
|
part['data-name'] = data.part_name;
|
|
part['data-min-block-id'] = min_block_id;
|
|
part['data-max-block-id'] = max_block_id;
|
|
part['data-level'] = level;
|
|
part.className = 'part';
|
|
part_title = document.createElement('div');
|
|
part_title.className = 'part_title';
|
|
part_title.innerText = `${data.part_name}, ${formatValue(data.size_in_bytes)}`;
|
|
part.appendChild(part_title);
|
|
|
|
const size = Math.sqrt(data.size_in_bytes);
|
|
|
|
part.style.width = Math.round(size / 500) + 'px';
|
|
part.style.height = Math.round(size / 1000) + 'px';
|
|
part.style.line_height = part.style.height;
|
|
|
|
let inserted = false;
|
|
for (const child of partition.childNodes) {
|
|
const child_min_block_id = +child['data-min-block-id'];
|
|
const child_max_block_id = +child['data-max-block-id'];
|
|
const child_level = +child['data-level'];
|
|
|
|
if (!inserted && child_min_block_id >= min_block_id) {
|
|
partition.insertBefore(part, child);
|
|
inserted = true;
|
|
}
|
|
/// Covered parts.
|
|
if (level > child_level && min_block_id <= child_min_block_id && max_block_id >= child_max_block_id) {
|
|
delete currently_active_parts[data.table_uuid][child['data-name']];
|
|
--num_parts;
|
|
partition.removeChild(child);
|
|
}
|
|
if (child_min_block_id > max_block_id) {
|
|
break;
|
|
}
|
|
}
|
|
if (!inserted) {
|
|
partition.appendChild(part);
|
|
}
|
|
}
|
|
|
|
for (const old_part_name of data.merged_from) {
|
|
if (old_part_name in currently_active_parts[data.table_uuid]) {
|
|
delete currently_active_parts[data.table_uuid][old_part_name];
|
|
--num_parts;
|
|
const old_part = document.getElementById(`part-${data.table_uuid}-${old_part_name}`);
|
|
if (old_part) {
|
|
partition.removeChild(old_part);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (data.event_type == 'RemovePart') {
|
|
if ((data.table_uuid in currently_active_parts) && (data.part_name in currently_active_parts[data.table_uuid])) {
|
|
delete currently_active_parts[data.table_uuid][data.part_name];
|
|
--num_parts;
|
|
const old_part = document.getElementById(part_id);
|
|
if (old_part) {
|
|
partition.removeChild(old_part);
|
|
}
|
|
}
|
|
}
|
|
|
|
stats.innerText = `${num_parts} parts.
|
|
Inserted ${inserted_parts} parts, ${inserted_rows} rows, ${formatValue(inserted_bytes)}.
|
|
Merged into ${merged_parts} parts, ${merged_rows} rows, ${formatValue(merged_bytes)}. Write aplification: ${((inserted_bytes + merged_bytes) / inserted_bytes).toFixed(2)}`;
|
|
}
|
|
|
|
let loading = false;
|
|
let stopping = false;
|
|
|
|
async function load() {
|
|
canvas.innerHTML = '';
|
|
num_parts = 0;
|
|
inserted_rows = 0;
|
|
inserted_bytes = 0;
|
|
inserted_parts = 0;
|
|
merged_rows = 0;
|
|
merged_bytes = 0;
|
|
merged_parts = 0;
|
|
currently_active_parts = {};
|
|
prev_time = null;
|
|
|
|
const host = document.getElementById('url').value;
|
|
const user = document.getElementById('user').value;
|
|
const password = document.getElementById('password').value;
|
|
|
|
let url = `${host}?default_format=JSONEachRow&enable_http_compression=1`
|
|
|
|
if (add_http_cors_header) {
|
|
// For debug purposes, you may set add_http_cors_header from the browser console
|
|
url += '&add_http_cors_header=1';
|
|
}
|
|
|
|
if (user) {
|
|
url += `&user=${encodeURIComponent(user)}`;
|
|
}
|
|
if (password) {
|
|
url += `&password=${encodeURIComponent(password)}`;
|
|
}
|
|
|
|
const query = document.getElementById('query').value;
|
|
|
|
let response, reply, error;
|
|
try {
|
|
loading = true;
|
|
document.getElementById('play').value = '⏹';
|
|
|
|
response = await fetch(url, { method: "POST", body: query });
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
|
|
let buffer = '';
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
if (stopping) {
|
|
stopped = true;
|
|
break;
|
|
}
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
let lines = buffer.split('\n');
|
|
|
|
for (line of lines.slice(0, -1)) {
|
|
if (stopping) {
|
|
stopped = true;
|
|
break;
|
|
}
|
|
const data = JSON.parse(line);
|
|
await update(data);
|
|
};
|
|
|
|
buffer = lines[lines.length - 1];
|
|
}
|
|
} catch (e) {
|
|
console.log(e);
|
|
error = e.toString();
|
|
}
|
|
|
|
loading = false;
|
|
stopping = false;
|
|
document.getElementById('play').value = '▶';
|
|
}
|
|
|
|
function stop() {
|
|
stopping = true;
|
|
}
|
|
|
|
document.getElementById('play').addEventListener('click', _ => {
|
|
if (loading) {
|
|
stop();
|
|
} else if (stopping) {
|
|
} else {
|
|
load();
|
|
}
|
|
});
|
|
|
|
function updateSpeed() {
|
|
document.getElementById('speed').innerText = speed <= 1000 ? `${speed}x` : `max`;
|
|
}
|
|
updateSpeed();
|
|
|
|
document.getElementById('slower').addEventListener('click', _ => {
|
|
if (speed > 1) {
|
|
speed = Math.max(speed / 10, 1);
|
|
updateSpeed();
|
|
}
|
|
});
|
|
|
|
document.getElementById('faster').addEventListener('click', _ => {
|
|
if (speed <= 1000) {
|
|
speed = Math.min(speed * 10, 10000);
|
|
updateSpeed();
|
|
}
|
|
});
|
|
|
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
let source = null;
|
|
|
|
function playClick(volume) {
|
|
if (source) {
|
|
source.disconnect(audioCtx.destination);
|
|
}
|
|
source = audioCtx.createBufferSource();
|
|
const myArrayBuffer = audioCtx.createBuffer(1, audioCtx.sampleRate / 1000, audioCtx.sampleRate);
|
|
const nowBuffering = myArrayBuffer.getChannelData(0);
|
|
for (let i = 0; i < myArrayBuffer.length; ++i) {
|
|
nowBuffering[i] = volume * (Math.random() * 2 - 1);
|
|
}
|
|
source.buffer = myArrayBuffer;
|
|
source.connect(audioCtx.destination);
|
|
source.start();
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|