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]>
497 lines
26 KiB
HTML
497 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Latency Analyzer</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
|
|
<script src="https://unpkg.com/@popperjs/core@2"></script>
|
|
<script src="https://unpkg.com/tippy.js@6"></script>
|
|
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/dist/tippy.css">
|
|
<style>
|
|
.matrix-legend {
|
|
margin-left: 20px;
|
|
margin-right: 20px;
|
|
}
|
|
.cell {
|
|
stroke: #ccc;
|
|
}
|
|
.cell.unlight {
|
|
opacity: 0.2;
|
|
}
|
|
.axis text {
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Custom Tippy theme */
|
|
.tippy-box[data-theme~='latency-data'] {
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
font-size: 12px;
|
|
border-radius: 4px;
|
|
padding: 5px;
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
#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 src="https://d3js.org/d3.v7.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/sorttable.min.js"></script>
|
|
<script language="javascript" type="text/javascript" src="js/query-datastore.js"></script>
|
|
<script language="javascript" type="text/javascript" src="js/data-visualization.js"></script>
|
|
<script language="javascript" type="text/javascript" src="js/data-processing.js"></script>
|
|
|
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
|
<div class="container-fluid">
|
|
<a class="navbar-brand" href="#">Hanzo Datastore query-latency-analyzer</a>
|
|
<button class="navbar-toggler" type="button"
|
|
data-bs-toggle="collapse"
|
|
data-bs-target="#bs-example-navbar-collapse-1"
|
|
aria-controls="bs-example-navbar-collapse-1"
|
|
aria-expanded="false"
|
|
aria-label="Toggle navigation">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
|
<form class="d-flex">
|
|
<button type="button" class="btn btn-primary"
|
|
id="toolbar-load"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#loadModal">
|
|
Load
|
|
</button>
|
|
</form>
|
|
<p class="navbar-text ms-auto" id="status-text"></p>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="modal fade" id="loadModal" tabindex="-1" aria-labelledby="loadModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="loadModalLabel">Load Data</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<ul class="nav nav-tabs" id="loadModalTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="upload-tab" data-bs-toggle="tab"
|
|
data-bs-target="#tabFileUpload" type="button" role="tab">Upload JSON</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="datastore-tab" data-bs-toggle="tab"
|
|
data-bs-target="#tabDatastore" type="button" role="tab">From ClickHouse</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content mt-3">
|
|
<div class="tab-pane fade show active" id="tabFileUpload" role="tabpanel">
|
|
<input type="file" id="loadFiles" class="form-control" />
|
|
</div>
|
|
<div class="tab-pane fade" id="tabDatastore" role="tabpanel">
|
|
<form id="params">
|
|
<div id="connection-params" class="mb-3">
|
|
<input spellcheck="false" id="url" type="text" value="http://localhost:8123"
|
|
placeholder="URL" class="form-control mb-2" />
|
|
<input spellcheck="false" id="user" type="text" value="" placeholder="user"
|
|
class="form-control mb-2" />
|
|
<input spellcheck="false" id="password" type="password" placeholder="password" value=""
|
|
class="form-control mb-2" />
|
|
</div>
|
|
<textarea spellcheck="false" id="query" rows="12" class="form-control"
|
|
placeholder="SQL Query">WITH 1 AS last_hours_to_query
|
|
SELECT
|
|
normalized_query_hash as group,
|
|
query_duration_ms * 1000 as _duration,
|
|
intDiv(ProfileEvents['LoggerElapsedNanoseconds'], 1000) as LoggerElapsedNanoseconds,
|
|
ProfileEvents['GlobalThreadPoolLockWaitMicroseconds'] as GlobalThreadPoolLockWait,
|
|
ProfileEvents['LocalThreadPoolLockWaitMicroseconds'] as LocalThreadPoolLockWait,
|
|
ProfileEvents['SchedulerIOReadWaitMicroseconds'] as SchedulerIOReadWait,
|
|
ProfileEvents['SchedulerIOWriteWaitMicroseconds'] as SchedulerIOWriteWait,
|
|
ProfileEvents['ZooKeeperWaitMicroseconds'] as ZooKeeperWait,
|
|
ProfileEvents['ContextLockWaitMicroseconds'] as ContextLockWait,
|
|
ProfileEvents['PartsLockWaitMicroseconds'] as PartsLockWait,
|
|
ProfileEvents['AsynchronousReadWaitMicroseconds'] as AsynchronousReadWait,
|
|
ProfileEvents['SynchronousReadWaitMicroseconds'] as SynchronousReadWait,
|
|
ProfileEvents['AsynchronousRemoteReadWaitMicroseconds'] as AsynchronousRemoteReadWait,
|
|
ProfileEvents['SynchronousRemoteReadWaitMicroseconds'] as SynchronousRemoteReadWait,
|
|
ProfileEvents['AsyncLoaderWaitMicroseconds'] as AsyncLoaderWait,
|
|
ProfileEvents['ConcurrencyControlWaitMicroseconds'] as ConcurrencyControlWait,
|
|
ProfileEvents['FileSyncElapsedMicroseconds'] as FileSyncElapsed,
|
|
ProfileEvents['DirectorySyncElapsedMicroseconds'] as DirectorySyncElapsed,
|
|
ProfileEvents['CompressedReadBufferChecksumDoesntMatchMicroseconds'] as CompressedReadBufferChecksumDoesntMatch,
|
|
ProfileEvents['OpenedFileCacheMicroseconds'] as OpenedFileCache,
|
|
ProfileEvents['DiskReadElapsedMicroseconds'] as DiskReadElapsed,
|
|
ProfileEvents['DiskWriteElapsedMicroseconds'] as DiskWriteElapsed,
|
|
ProfileEvents['NetworkReceiveElapsedMicroseconds'] as NetworkReceiveElapsed,
|
|
ProfileEvents['NetworkSendElapsedMicroseconds'] as NetworkSendElapsed,
|
|
ProfileEvents['GlobalThreadPoolThreadCreationMicroseconds'] as GlobalThreadPoolThreadCreation,
|
|
ProfileEvents['LocalThreadPoolThreadCreationMicroseconds'] as LocalThreadPoolThreadCreation,
|
|
ProfileEvents['DiskS3GetRequestThrottlerSleepMicroseconds'] as DiskS3GetRequestThrottlerSleep,
|
|
ProfileEvents['DiskS3PutRequestThrottlerSleepMicroseconds'] as DiskS3PutRequestThrottlerSleep,
|
|
ProfileEvents['S3GetRequestThrottlerSleepMicroseconds'] as S3GetRequestThrottlerSleep,
|
|
ProfileEvents['S3PutRequestThrottlerSleepMicroseconds'] as S3PutRequestThrottlerSleep,
|
|
ProfileEvents['RemoteReadThrottlerSleepMicroseconds'] as RemoteReadThrottlerSleep,
|
|
ProfileEvents['RemoteWriteThrottlerSleepMicroseconds'] as RemoteWriteThrottlerSleep,
|
|
ProfileEvents['LocalReadThrottlerSleepMicroseconds'] as LocalReadThrottlerSleep,
|
|
ProfileEvents['LocalWriteThrottlerSleepMicroseconds'] as LocalWriteThrottlerSleep,
|
|
ProfileEvents['CompileExpressionsMicroseconds'] as CompileExpressions,
|
|
ProfileEvents['FilteringMarksWithPrimaryKeyMicroseconds'] as FilteringMarksWithPrimaryKey,
|
|
ProfileEvents['FilteringMarksWithSecondaryKeysMicroseconds'] as FilteringMarksWithSecondaryKeys,
|
|
ProfileEvents['WaitMarksLoadMicroseconds'] as WaitMarksLoad,
|
|
ProfileEvents['MemoryOvercommitWaitTimeMicroseconds'] as MemoryOvercommitWaitTime,
|
|
ProfileEvents['MemoryAllocatorPurgeTimeMicroseconds'] as MemoryAllocatorPurgeTime,
|
|
ProfileEvents['ParallelReplicasHandleRequestMicroseconds'] as ParallelReplicasHandleRequest,
|
|
ProfileEvents['ParallelReplicasHandleAnnouncementMicroseconds'] as ParallelReplicasHandleAnnouncement,
|
|
ProfileEvents['ParallelReplicasAnnouncementMicroseconds'] as ParallelReplicasAnnouncement,
|
|
ProfileEvents['ParallelReplicasReadRequestMicroseconds'] as ParallelReplicasReadRequest,
|
|
ProfileEvents['ParallelReplicasStealingByHashMicroseconds'] as ParallelReplicasStealingByHash,
|
|
ProfileEvents['ParallelReplicasProcessingPartsMicroseconds'] as ParallelReplicasProcessingParts,
|
|
ProfileEvents['ParallelReplicasStealingLeftoversMicroseconds'] as ParallelReplicasStealingLeftovers,
|
|
ProfileEvents['ParallelReplicasCollectingOwnedSegmentsMicroseconds'] as ParallelReplicasCollectingOwnedSegments,
|
|
ProfileEvents['S3ReadMicroseconds'] as S3Read,
|
|
ProfileEvents['S3WriteMicroseconds'] as S3Write,
|
|
ProfileEvents['DiskS3ReadMicroseconds'] as DiskS3Read,
|
|
ProfileEvents['DiskS3WriteMicroseconds'] as DiskS3Write,
|
|
ProfileEvents['ReadBufferFromS3Microseconds'] as ReadBufferFromS3,
|
|
ProfileEvents['ReadBufferFromS3InitMicroseconds'] as ReadBufferFromS3Init,
|
|
ProfileEvents['WriteBufferFromS3Microseconds'] as WriteBufferFromS3,
|
|
ProfileEvents['WriteBufferFromS3WaitInflightLimitMicroseconds'] as WriteBufferFromS3WaitInflightLimit,
|
|
ProfileEvents['ReadBufferFromAzureMicroseconds'] as ReadBufferFromAzure,
|
|
ProfileEvents['ReadBufferFromAzureInitMicroseconds'] as ReadBufferFromAzureInit,
|
|
ProfileEvents['CachedReadBufferReadFromSourceMicroseconds'] as CachedReadBufferReadFromSource,
|
|
ProfileEvents['CachedReadBufferReadFromCacheMicroseconds'] as CachedReadBufferReadFromCache,
|
|
ProfileEvents['CachedReadBufferCacheWriteMicroseconds'] as CachedReadBufferCacheWrite,
|
|
ProfileEvents['CachedReadBufferCreateBufferMicroseconds'] as CachedReadBufferCreateBuffer,
|
|
ProfileEvents['CachedWriteBufferCacheWriteMicroseconds'] as CachedWriteBufferCacheWrite,
|
|
ProfileEvents['FilesystemCacheLoadMetadataMicroseconds'] as FilesystemCacheLoadMetadata,
|
|
ProfileEvents['FilesystemCacheLockKeyMicroseconds'] as FilesystemCacheLockKey,
|
|
ProfileEvents['FilesystemCacheLockMetadataMicroseconds'] as FilesystemCacheLockMetadata,
|
|
ProfileEvents['FilesystemCacheLockCacheMicroseconds'] as FilesystemCacheLockCache,
|
|
ProfileEvents['FilesystemCacheReserveMicroseconds'] as FilesystemCacheReserve,
|
|
ProfileEvents['FilesystemCacheEvictMicroseconds'] as FilesystemCacheEvict,
|
|
ProfileEvents['FilesystemCacheGetOrSetMicroseconds'] as FilesystemCacheGetOrSet,
|
|
ProfileEvents['FilesystemCacheGetMicroseconds'] as FilesystemCacheGet,
|
|
ProfileEvents['FileSegmentWaitMicroseconds'] as FileSegmentWait,
|
|
ProfileEvents['FileSegmentCompleteMicroseconds'] as FileSegmentComplete,
|
|
ProfileEvents['FileSegmentLockMicroseconds'] as FileSegmentLock,
|
|
ProfileEvents['FileSegmentWriteMicroseconds'] as FileSegmentWrite,
|
|
ProfileEvents['FileSegmentUseMicroseconds'] as FileSegmentUse,
|
|
ProfileEvents['FileSegmentRemoveMicroseconds'] as FileSegmentRemove,
|
|
ProfileEvents['FileSegmentHolderCompleteMicroseconds'] as FileSegmentHolderComplete,
|
|
ProfileEvents['WaitPrefetchTaskMicroseconds'] as WaitPrefetchTask,
|
|
ProfileEvents['ThreadpoolReaderTaskMicroseconds'] as ThreadpoolReaderTask,
|
|
ProfileEvents['ThreadpoolReaderPrepareMicroseconds'] as ThreadpoolReaderPrepare,
|
|
ProfileEvents['ThreadpoolReaderSubmitReadSynchronouslyMicroseconds'] as ThreadpoolReaderSubmitReadSynchronously,
|
|
ProfileEvents['ThreadpoolReaderSubmitLookupInCacheMicroseconds'] as ThreadpoolReaderSubmitLookupInCache,
|
|
ProfileEvents['SleepFunctionElapsedMicroseconds'] as SleepFunctionElapsed,
|
|
ProfileEvents['ThreadPoolReaderPageCacheHitElapsedMicroseconds'] as ThreadPoolReaderPageCacheHitElapsed,
|
|
ProfileEvents['ThreadPoolReaderPageCacheMissElapsedMicroseconds'] as ThreadPoolReaderPageCacheMissElapsed,
|
|
ProfileEvents['ConnectionPoolIsFullMicroseconds'] as ConnectionPoolIsFull,
|
|
ProfileEvents['SharedMergeTreeVirtualPartsUpdateMicroseconds'] as SharedMergeTreeVirtualPartsUpdate,
|
|
ProfileEvents['SharedMergeTreeVirtualPartsUpdatesFromZooKeeperMicroseconds'] as SharedMergeTreeVirtualPartsUpdatesFromZooKeeper,
|
|
ProfileEvents['SharedMergeTreeVirtualPartsUpdatesFromPeerMicroseconds'] as SharedMergeTreeVirtualPartsUpdatesFromPeer,
|
|
ProfileEvents['SharedMergeTreeMergeSelectingTaskMicroseconds'] as SharedMergeTreeMergeSelectingTask,
|
|
ProfileEvents['SharedMergeTreeScheduleDataProcessingJobMicroseconds'] as SharedMergeTreeScheduleDataProcessingJob,
|
|
ProfileEvents['SharedMergeTreeHandleFetchPartsMicroseconds'] as SharedMergeTreeHandleFetchParts,
|
|
ProfileEvents['SharedMergeTreeHandleOutdatedPartsMicroseconds'] as SharedMergeTreeHandleOutdatedParts,
|
|
ProfileEvents['SharedMergeTreeSelectPartsForRendezvousFetchMicroseconds'] as SharedMergeTreeSelectPartsForRendezvousFetchMicroseconds,
|
|
ProfileEvents['SharedMergeTreeSelectPartsForCoordinatedFetchMicroseconds'] as SharedMergeTreeSelectPartsForCoordinatedFetchMicroseconds,
|
|
ProfileEvents['SharedMergeTreeSelectPartsForFullFetchMicroseconds'] as SharedMergeTreeSelectPartsForFullFetchMicroseconds,
|
|
ProfileEvents['SharedMergeTreeTryUpdateDiskMetadataCacheForPartMicroseconds'] as SharedMergeTreeTryUpdateDiskMetadataCacheForPart,
|
|
ProfileEvents['SharedMergeTreeLoadChecksumAndIndexesMicroseconds'] as SharedMergeTreeLoadChecksumAndIndexes,
|
|
ProfileEvents['StorageConnectionsElapsedMicroseconds'] as StorageConnectionsElapsed,
|
|
ProfileEvents['DiskConnectionsElapsedMicroseconds'] as DiskConnectionsElapsed,
|
|
ProfileEvents['HTTPConnectionsElapsedMicroseconds'] as HTTPConnectionsElapsed,
|
|
ProfileEvents['ParquetFetchWaitTimeMicroseconds'] as ParquetFetchWaitTime,
|
|
ProfileEvents['OSCPUWaitMicroseconds'] as OSCPUWait,
|
|
ProfileEvents['OSIOWaitMicroseconds'] as OSIOWait,
|
|
ProfileEvents['OSCPUVirtualTimeMicroseconds'] as OSCPUVirtualTimeMicroseconds
|
|
FROM system.query_log
|
|
WHERE event_time >= now() - INTERVAL last_hours_to_query HOUR
|
|
AND type = 2
|
|
SETTINGS output_format_json_named_tuples_as_objects = 1, skip_unavailable_shards = 1</textarea>
|
|
<div class="form-check mt-3">
|
|
<input class="form-check-input" type="checkbox" id="flushLogsCheckbox">
|
|
<label class="form-check-label" for="flushLogsCheckbox">
|
|
Run SYSTEM FLUSH LOGS
|
|
</label>
|
|
</div>
|
|
<div class="form-check mt-2">
|
|
<input class="form-check-input" type="checkbox" id="filterByDurationCheckbox" checked>
|
|
<label class="form-check-label" for="filterByDurationCheckbox">
|
|
Filter insignificant columns
|
|
</label>
|
|
</div>
|
|
<div id="query-error" class="text-danger mt-2" style="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 id="legend-container"></div>
|
|
<div id="table-container"></div>
|
|
<div class="matrix-tooltip" style="visibility: hidden;"></div>
|
|
</body>
|
|
</html>
|
|
|
|
<script>
|
|
|
|
// Show correct button depending on tab
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const loadModal = document.getElementById('loadModal');
|
|
loadModal.addEventListener('shown.bs.modal', function () {
|
|
const tabButtons = document.querySelectorAll('#loadModalTabs button[data-bs-toggle="tab"]');
|
|
tabButtons.forEach(button => {
|
|
button.addEventListener('shown.bs.tab', function (e) {
|
|
const target = e.target.getAttribute('data-bs-target');
|
|
if (target === '#tabDatastore') {
|
|
document.getElementById('btnDoLoad').style.display = 'none';
|
|
document.getElementById('btnQueryDatastore').style.display = 'inline-block';
|
|
} else {
|
|
document.getElementById('btnDoLoad').style.display = 'inline-block';
|
|
document.getElementById('btnQueryDatastore').style.display = 'none';
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// Handle the Datastore query
|
|
let queryController = null;
|
|
document.getElementById('btnQueryDatastore').addEventListener('click', async function () {
|
|
if (queryController === null) {
|
|
let rows = [];
|
|
let error = '';
|
|
const errorDiv = document.getElementById('query-error');
|
|
errorDiv.style.display = 'none';
|
|
errorDiv.textContent = '';
|
|
document.getElementById('hourglass').style.display = 'inline-block';
|
|
this.textContent = "Stop";
|
|
|
|
if (document.getElementById('flushLogsCheckbox').checked) {
|
|
queryController = new AbortController();
|
|
await queryDatastore({
|
|
host: document.getElementById('url').value,
|
|
user: document.getElementById('user').value,
|
|
password: document.getElementById('password').value,
|
|
query: "SYSTEM FLUSH LOGS",
|
|
for_each_row: () => {},
|
|
on_error: (errorMsg) => error = errorMsg,
|
|
controller: queryController
|
|
});
|
|
}
|
|
|
|
if (error === '') {
|
|
queryController = new AbortController();
|
|
await queryDatastore({
|
|
host: document.getElementById('url').value,
|
|
user: document.getElementById('user').value,
|
|
password: document.getElementById('password').value,
|
|
query: document.getElementById('query').value,
|
|
for_each_row: (data) => rows.push(data),
|
|
on_error: (errorMsg) => error = errorMsg,
|
|
controller: queryController
|
|
});
|
|
}
|
|
|
|
queryController = null;
|
|
this.textContent = "Query";
|
|
document.getElementById('hourglass').style.display = 'none';
|
|
|
|
if (error !== '') {
|
|
errorDiv.textContent = error;
|
|
errorDiv.style.display = 'block';
|
|
} else {
|
|
const filterByDuration = document.getElementById('filterByDurationCheckbox').checked;
|
|
renderTable(handleData(parseDatastoreData(rows), filterByDuration));
|
|
const loadModal = bootstrap.Modal.getInstance(document.getElementById('loadModal'));
|
|
loadModal.hide();
|
|
}
|
|
} else { // Cancel query
|
|
queryController.abort();
|
|
}
|
|
});
|
|
|
|
// Handle uploaded JSON
|
|
document.getElementById('btnDoLoad').addEventListener('click', function () {
|
|
const element = document.getElementById('loadFiles');
|
|
const files = element.files;
|
|
if (files.length <= 0) {
|
|
return false;
|
|
}
|
|
|
|
const fr = new FileReader();
|
|
fr.onload = function (e) {
|
|
document.getElementById('query-error').style.display = 'none';
|
|
const filterByDuration = document.getElementById('filterByDurationCheckbox').checked;
|
|
renderTable(handleData(parseDatastoreData(JSON.parse(e.target.result).data), filterByDuration));
|
|
};
|
|
fr.readAsText(files.item(0));
|
|
element.value = '';
|
|
const loadModal = bootstrap.Modal.getInstance(document.getElementById('loadModal'));
|
|
loadModal.hide();
|
|
});
|
|
|
|
function parseDatastoreData(rows) {
|
|
let result = [];
|
|
for (let i = 0; i < rows.length; i++) {
|
|
let row = rows[i];
|
|
for (let key in row) {
|
|
if (key !== 'group' && !key.startsWith('_'))
|
|
row[key] = +row[key];
|
|
}
|
|
result.push(row);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function renderTable(data) {
|
|
// First calculate the available width for the table
|
|
const containerWidth = document.getElementById("table-container").clientWidth;
|
|
// Store it as a global variable so visualizations can access it
|
|
window.availableWidth = containerWidth;
|
|
|
|
// Calculate global maximum across all groups
|
|
const globalMax = Math.max(...Object.values(data.groups).flatMap(group => group.max));
|
|
// Store it for visualizations
|
|
window.globalMaxValue = globalMax;
|
|
|
|
d3.select("#table-container").selectAll("table").remove();
|
|
|
|
const table = d3.select("#table-container")
|
|
.append("table")
|
|
.attr("class", "table sortable")
|
|
.style("width", "100%"); // Ensure table takes full width
|
|
|
|
const thead = table.append("thead");
|
|
const tbody = table.append("tbody");
|
|
|
|
// Get labels strting with underscore
|
|
const extraColumns = data.labels.filter(label => label.startsWith("_")).map(label => label.substring(1));
|
|
const columns = ["Group", "Count", ...extraColumns, "Covariance Matrix & Distribution"];
|
|
thead.append("tr")
|
|
.selectAll("th")
|
|
.data(columns)
|
|
.enter()
|
|
.append("th")
|
|
.text(d => d);
|
|
|
|
const rows = tbody.selectAll("tr")
|
|
.data(Object.entries(data.groups))
|
|
.enter()
|
|
.append("tr");
|
|
|
|
// Put group name and count in the first two columns
|
|
rows.append("td").text(([name]) => name);
|
|
rows.append("td").text(([, stats]) => stats.count);
|
|
|
|
// Iterate over extra columns and add them to the table
|
|
extraColumns.forEach(col => {
|
|
const index = data.labels.findIndex(label => label.substring(1) === col);
|
|
rows.append("td").text(([, stats]) => stats.avg[index].toFixed(2));
|
|
});
|
|
|
|
// Add covariance matrix in the third column
|
|
rows.append("td")
|
|
.append("div")
|
|
.attr("class", "matrix")
|
|
.each(function ([, stats]) {
|
|
addVisualization(this, data.labels, stats);
|
|
});
|
|
|
|
// Add sorting to the table
|
|
sorttable.makeSortable(table.node());
|
|
}
|
|
|
|
function handleData(raw_data, filterByDuration = true) {
|
|
// Process raw data
|
|
const data = processData(raw_data);
|
|
let globalAvg = computeGlobalAverages(data);
|
|
|
|
// Filter out labels that have zero means across all groups
|
|
const nonZeroMeanPredicate = (label, i, groups) => {
|
|
// Check if all groups have zero mean for this label
|
|
return !Object.values(groups).every(group => Math.abs(group.avg[i]) < 1e-10);
|
|
};
|
|
|
|
// Additional duration-based filtering
|
|
const durationFilterPredicate = (label, i, groups) => {
|
|
if (!filterByDuration || label.startsWith('_')) return true;
|
|
|
|
const durationIndex = data.labels.indexOf('_duration');
|
|
if (durationIndex === -1) return true; // If no _duration label, skip filtering
|
|
|
|
// Check if any group has this metric > 1% of duration
|
|
return Object.values(groups).some(group => {
|
|
return group.p[i][90] > group.p[durationIndex][50] * 0.05;
|
|
});
|
|
};
|
|
|
|
// Apply both filters
|
|
const combinedPredicate = (label, i, groups) => {
|
|
return nonZeroMeanPredicate(label, i, groups) && durationFilterPredicate(label, i, groups);
|
|
};
|
|
|
|
const filteredData = filterData(data, combinedPredicate);
|
|
globalAvg = computeGlobalAverages(filteredData); // Recompute global averages after filtering
|
|
|
|
// Split labels into underscore-prefixed and regular labels while preserving original order
|
|
const underscoreLabels = filteredData.labels.filter(label => label.startsWith('_'));
|
|
const regularLabels = filteredData.labels.filter(label => !label.startsWith('_'));
|
|
|
|
// Create comparison function to sort regular labels by global averages
|
|
const compByAvg = (a, b) => {
|
|
const aIdx = filteredData.labels.indexOf(a);
|
|
const bIdx = filteredData.labels.indexOf(b);
|
|
return globalAvg[bIdx] - globalAvg[aIdx]; // Sort descending
|
|
};
|
|
|
|
// Sort only regular labels, keeping underscore labels in original order
|
|
const sortedRegularLabels = regularLabels.sort(compByAvg);
|
|
|
|
// Merge back in the correct order: underscore labels followed by sorted regular labels
|
|
const orderedLabels = [...underscoreLabels, ...sortedRegularLabels];
|
|
|
|
// Order data using the merged labels
|
|
return orderLabels(filteredData, (a, b) => {
|
|
return orderedLabels.indexOf(a) - orderedLabels.indexOf(b);
|
|
});
|
|
}
|
|
|
|
drawLegend("#legend-container");
|
|
|
|
// Render example data
|
|
renderTable(handleData(generateTestData()));
|
|
|
|
</script>
|
|
|
|
<script language="javascript" type="text/javascript" src="test_env.js"></script>
|