Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Binary (symbols) viewer #58211

Merged
merged 3 commits into from Dec 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/en/interfaces/overview.md
Expand Up @@ -25,6 +25,7 @@ ClickHouse server provides embedded visual interfaces for power users:

- Play UI: open `/play` in the browser;
- Advanced Dashboard: open `/dashboard` in the browser;
- Binary symbols viewer for ClickHouse engineers: open `/binary` in the browser;

There are also a wide range of third-party libraries for working with ClickHouse:

Expand Down
267 changes: 267 additions & 0 deletions programs/server/binary.html
@@ -0,0 +1,267 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1NCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDkgOCI+PHN0eWxlPi5ve2ZpbGw6I2ZjMH0ucntmaWxsOnJlZH08L3N0eWxlPjxwYXRoIGQ9Ik0wLDcgaDEgdjEgaC0xIHoiIGNsYXNzPSJyIi8+PHBhdGggZD0iTTAsMCBoMSB2NyBoLTEgeiIgY2xhc3M9Im8iLz48cGF0aCBkPSJNMiwwIGgxIHY4IGgtMSB6IiBjbGFzcz0ibyIvPjxwYXRoIGQ9Ik00LDAgaDEgdjggaC0xIHoiIGNsYXNzPSJvIi8+PHBhdGggZD0iTTYsMCBoMSB2OCBoLTEgeiIgY2xhc3M9Im8iLz48cGF0aCBkPSJNOCwzLjI1IGgxIHYxLjUgaC0xIHoiIGNsYXNzPSJvIi8+PC9zdmc+">
<title>ClickHouse Binary Viewer</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" />
<style type="text/css">
html, body, #space {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}

#space {
background: #111;
image-rendering: pixelated;
}

#error {
display: none;
position: absolute;
z-index: 1001;
bottom: max(5%, 1em);
left: 50%;
transform: translate(-50%, 0);
background: #300;
color: white;
font-family: monospace;
white-space: pre-wrap;
font-size: 16pt;
padding: 1em;
min-width: 50%;
max-width: 80%;
}

.leaflet-fade-anim .leaflet-popup {
transition: none;
font-family: monospace;
}

.leaflet-control-attribution {
font-size: 12pt;
}
</style>
</head>

<body>
<div id="space"></div>
<div id="error"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<script>
let host = 'http://localhost:8123/';
let user = 'default';
let password = '';
let add_http_cors_header = true;

/// If it is hosted on server, assume that it is the address of ClickHouse.
if (location.protocol != 'file:') {
host = location.origin;
user = 'default';
add_http_cors_header = false;
}

let map = L.map('space', {
crs: L.CRS.Simple,
center: [-512, 512],
maxBounds: [[128, -128], [-1152, 1152]],
zoom: 0,
});

let cached_tiles = {};

async function render(coords, tile) {
const sql = `
WITH
bitShiftLeft(1::UInt64, 5 - {z:UInt8})::UInt64 AS zoom_factor,

number MOD 1024 AS tile_x,
number DIV 1024 AS tile_y,

(zoom_factor * (tile_x + {x:UInt16} * 1024))::UInt16 AS x,
(zoom_factor * (tile_y + {y:UInt16} * 1024))::UInt16 AS y,

mortonEncode(x, y) AS addr,

extract(demangle(addressToSymbol(addr)), '^[^<]+') AS name,
(empty(name) ? 0 : sipHash64(name)) AS hash,
hash MOD 256 AS r, hash DIV 256 MOD 256 AS g, hash DIV 65536 MOD 256 AS b

SELECT r::UInt8, g::UInt8, b::UInt8
FROM numbers_mt(1024*1024)
ORDER BY number`;

const key = `${coords.z}-${coords.x}-${coords.y}`;
let buf = cached_tiles[key];
if (!buf) {
let url = `${host}?default_format=RowBinary&allow_introspection_functions=1`;

if (add_http_cors_header) {
// For debug purposes, you may set add_http_cors_header from a browser console
url += '&add_http_cors_header=1';
}

if (user) {
url += `&user=${encodeURIComponent(user)}`;
}
if (password) {
url += `&password=${encodeURIComponent(password)}`;
}

url += `&param_z=${coords.z}&param_x=${coords.x}&param_y=${coords.y}`;
url += `&enable_http_compression=1&network_compression_method=zstd&network_zstd_compression_level=6`;

const response = await fetch(url, { method: 'POST', body: sql });

if (!response.ok) {
const text = await response.text();
let err = document.getElementById('error');
err.textContent = text;
err.style.display = 'block';
return;
}

buf = await response.arrayBuffer();
cached_tiles[key] = buf;
}

let ctx = tile.getContext('2d');
let image = ctx.createImageData(1024, 1024);
let arr = new Uint8ClampedArray(buf);

for (let i = 0; i < 1024 * 1024; ++i) {
image.data[i * 4 + 0] = arr[i * 3 + 0];
image.data[i * 4 + 1] = arr[i * 3 + 1];
image.data[i * 4 + 2] = arr[i * 3 + 2];
image.data[i * 4 + 3] = 255;
}

ctx.putImageData(image, 0, 0, 0, 0, 1024, 1024);

let err = document.getElementById('error');
err.style.display = 'none';
}

L.GridLayer.ClickHouse = L.GridLayer.extend({
createTile: function(coords, done) {
let tile = L.DomUtil.create('canvas', 'leaflet-tile');
tile.width = 1024;
tile.height = 1024;
if (coords.x < 0 || coords.y < 0 || coords.x >= Math.pow(2, coords.z) || coords.y >= Math.pow(2, coords.z)) return tile;
render(coords, tile).then(err => done(err, tile));
return tile;
}
});

let layer = new L.GridLayer.ClickHouse({
tileSize: 1024,
minZoom: 0,
maxZoom: 10,
minNativeZoom: 0,
maxNativeZoom: 5,
attribution: '© ClickHouse, Inc.'
});

layer.addTo(map);

map.attributionControl.setPrefix('<a href="https://github.com/ClickHouse/ClickHouse/">About</a>');

function latLngToPixel(latlng) {
return { x: ((latlng.lng / 1024) * 32768)|0, y: ((-latlng.lat / 1024) * 32768)|0 };
}

function pixelToLatLng(pixel) {
return { lat: (-pixel.y - 0.5) / 32768 * 1024, lng: (pixel.x + 0.5) / 32768 * 1024 };
}

let popup = L.popup({maxWidth: '100%'});
let current_requested_addr = '';

function updateHistory() {
const state = {
zoom: map.getZoom(),
center: latLngToPixel(map.getCenter()),
};

let query = `?zoom=${state.zoom}&x=${state.center.x}&y=${state.center.y}`;

if (popup.isOpen() && map.getBounds().contains(popup.getLatLng())) {
state.popup = latLngToPixel(popup.getLatLng());
query += `&px=${state.popup.x}&py=${state.popup.y}`;
}

history.replaceState(state, '', query);
}

window.onpopstate = function(event) {
const state = event.state;
if (!state) {
return;
}

map.setView(pixelToLatLng(state.center), state.zoom);

if (state.popup) {
showPopup(state.popup.x, state.popup.y);
}
};

if (window.location.search) {
const params = new URLSearchParams(window.location.search);

map.setView(pixelToLatLng({x: params.get('x')|0, y: params.get('y')|0}), params.get('zoom'));

if (params.get('px') !== null && params.get('py') !== null) {
showPopup(params.get('px')|0, params.get('py')|0);
}
}

function showPopup(x, y) {
const xn = BigInt(x);
const yn = BigInt(y);
let addr_int = 0n;
for (let bit = 0n; bit < 16n; ++bit) {
addr_int |= ((xn >> bit) & 1n) << (bit * 2n);
addr_int |= ((yn >> bit) & 1n) << (1n + bit * 2n);
}

current_requested_addr = addr_int;

const addr_hex = '0x' + addr_int.toString(16);
const response = fetch(
`http://localhost:8123/?default_format=JSON`,
{
method: 'POST',
body: `SELECT encodeXMLComponent(demangle(addressToSymbol(${addr_int}::UInt64))) AS name,
encodeXMLComponent(addressToLine(${addr_int}::UInt64)) AS line`
}).then(response => response.json().then(o => {

let name = o.rows ? o.data[0].name : 'nothing';
let line = o.rows ? o.data[0].line : '';

if (addr_int == current_requested_addr) {
popup.setContent(`<p><b>${addr_hex}</b></p><p>${name}</p><p>${line}</p>`);
}
}));

popup
.setLatLng(pixelToLatLng({x: x, y: y}))
.setContent(addr_hex)
.openOn(map);
}

map.on('click', e => {
const {x, y} = latLngToPixel(e.latlng);
if (x < 0 || x >= 32768 || y < 0 || y >= 32768) return;

showPopup(x, y);
updateHistory();
});

map.on('moveend', e => updateHistory());
</script>
</body>
</html>
7 changes: 6 additions & 1 deletion src/Server/HTTPHandlerFactory.cpp
Expand Up @@ -7,7 +7,6 @@
#include <Poco/Util/AbstractConfiguration.h>

#include "HTTPHandler.h"
#include "NotFoundHandler.h"
#include "StaticRequestHandler.h"
#include "ReplicasStatusHandler.h"
#include "InterserverIOHTTPHandler.h"
Expand Down Expand Up @@ -161,6 +160,12 @@ void addCommonDefaultHandlersFactory(HTTPRequestHandlerFactoryMain & factory, IS
factory.addPathToHints("/dashboard");
factory.addHandler(dashboard_handler);

auto binary_handler = std::make_shared<HandlingRuleHTTPHandlerFactory<WebUIRequestHandler>>(server);
binary_handler->attachNonStrictPath("/binary");
binary_handler->allowGetAndHeadRequest();
factory.addPathToHints("/binary");
factory.addHandler(binary_handler);

auto js_handler = std::make_shared<HandlingRuleHTTPHandlerFactory<WebUIRequestHandler>>(server);
js_handler->attachNonStrictPath("/js/");
js_handler->allowGetAndHeadRequest();
Expand Down
7 changes: 6 additions & 1 deletion src/Server/WebUIRequestHandler.cpp
@@ -1,7 +1,6 @@
#include "WebUIRequestHandler.h"
#include "IServer.h"

#include <Poco/Net/HTTPServerRequest.h>
#include <Poco/Net/HTTPServerResponse.h>
#include <Poco/Util/LayeredConfiguration.h>

Expand All @@ -25,6 +24,7 @@
INCBIN(resource_play_html, SOURCE_DIR "/programs/server/play.html");
INCBIN(resource_dashboard_html, SOURCE_DIR "/programs/server/dashboard.html");
INCBIN(resource_uplot_js, SOURCE_DIR "/programs/server/js/uplot.js");
INCBIN(resource_binary_html, SOURCE_DIR "/programs/server/binary.html");


namespace DB
Expand Down Expand Up @@ -68,6 +68,11 @@ void WebUIRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServerR

*response.send() << html;
}
else if (request.getURI().starts_with("/binary"))
{
response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_OK);
*response.send() << std::string_view(reinterpret_cast<const char *>(gresource_binary_htmlData), gresource_binary_htmlSize);
}
else if (request.getURI() == "/js/uplot.js")
{
response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_OK);
Expand Down
1 change: 1 addition & 0 deletions tests/queries/0_stateless/02952_binary.reference
@@ -0,0 +1 @@
addressToSymbol
7 changes: 7 additions & 0 deletions tests/queries/0_stateless/02952_binary.sh
@@ -0,0 +1,7 @@
#!/usr/bin/env bash

CURDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=../shell_config.sh
. "$CURDIR"/../shell_config.sh

${CLICKHOUSE_CURL} -sS "${CLICKHOUSE_PORT_HTTP_PROTO}://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT_HTTP}/binary" | grep -oF --max-count 1 'addressToSymbol'