From c09695a6be92a5172a3aacfe288302520f4187ba Mon Sep 17 00:00:00 2001 From: RobinTF <83676088+RobinTF@users.noreply.github.com> Date: Fri, 13 Oct 2023 17:23:28 +0200 Subject: [PATCH] Address remaining PR comments --- backend/static/js/helper.js | 13 +-- backend/static/js/qleverUI.js | 179 +++++++++++++++++++++------------- backend/templates/index.html | 2 +- 3 files changed, 121 insertions(+), 73 deletions(-) diff --git a/backend/static/js/helper.js b/backend/static/js/helper.js index fb0cb560..d2bea891 100644 --- a/backend/static/js/helper.js +++ b/backend/static/js/helper.js @@ -69,13 +69,13 @@ function appendRuntimeInformation(runtime_info, query, time, queryUpdate) { lastQueryUpdate = queryUpdate; } -// Add "text" field to given `tree_node`, for display using Treant.js (in -// function `visualise` in `qleverUI.js`). This function call itself recursively -// on each child of `tree_node` (if any). +// Add "text" field to given `tree_node`, for display using Treant.js +// (in function `renderRuntimeInformationToDom` in `qleverUI.js`). +// This function call itself recursively on each child of `tree_node` (if any). // // NOTE: The labels and the style can be found in backend/static/css/style.css . // The coloring of the boxes, which depends on the time and caching status, is -// done in function `visualise` in `qleverUI.js`. +// done in function `renderRuntimeInformationToDom` in `qleverUI.js`. function addTextElementsToQueryExecutionTreeForTreant(tree_node, is_ancestor_cached = false) { if (tree_node["text"] == undefined) { var text = {}; @@ -753,7 +753,7 @@ function expandEditor() { } } -function displayError(queryId, response, statusWithText = undefined) { +function displayError(response, statusWithText = undefined, queryId = undefined) { console.error("Either the GET request failed or the backend returned an error:", response); if (response["exception"] == undefined || response["exception"] == "") { response["exception"] = "Unknown error"; @@ -792,12 +792,13 @@ function displayError(queryId, response, statusWithText = undefined) { // If error response contains query and runtime info, append to runtime log. // // TODO: Show items from error responses in different color (how about "red"). - if (response["query"] && response["runtimeInformation"]) { + if (response["query"] && response["runtimeInformation"] && queryId) { // console.log("DEBUG: Error response with runtime information found!"); appendRuntimeInformation(response.runtimeInformation, response.query, response.time, { queryId, updateTimeStamp: Number.MAX_VALUE }); + renderRuntimeInformationToDom(); } } diff --git a/backend/static/js/qleverUI.js b/backend/static/js/qleverUI.js index 31833c1a..3b634682 100755 --- a/backend/static/js/qleverUI.js +++ b/backend/static/js/qleverUI.js @@ -10,6 +10,7 @@ var runtime_log = []; var query_log = []; var lastQueryUpdate = { queryId: null, updateTimeStamp: 0 }; +// Generates a random query id only known to this client function generateQueryId() { if (window.isSecureContext) { return crypto.randomUUID(); @@ -20,6 +21,11 @@ function generateQueryId() { return Math.floor(Math.random() * 1000000000); } +// Uses the BASEURL variable to build the URL for the websocket endpoint +function getWebSocketUrl(queryId) { + return `${BASEURL.replaceAll(/^http/g, "ws")}/watch/${queryId}`; +} + $(window).resize(function (e) { if (e.target == window) { editor.setSize($('#queryBlock').width()); @@ -374,44 +380,16 @@ function getResultTime(resultTimes) { return timeList; } -// Process the given query. -async function processQuery(sendLimit=0, element=$("#exebtn")) { - log('Preparing query...', 'other'); - log('Element: ' + element, 'other'); - if (sendLimit >= 0) { displayStatus("Waiting for response..."); } - - $(element).find('.glyphicon').addClass('glyphicon-spin glyphicon-refresh'); - $(element).find('.glyphicon').removeClass('glyphicon-remove'); - $(element).find('.glyphicon').css('color', $(element).css('color')); - log('Sending request...', 'other'); - - // A negative value for `sendLimit` has the special meaning: clear the cache - // (without issuing a query). This is used in `backend/templates/index.html`, - // in the definition of the `oncklick` action for the "Clear cache" button. - // TODO: super ugly, find a better solution. - let nothingToShow = false; - var params = {}; - if (sendLimit >= 0) { - var original_query = editor.getValue(); - var query = await rewriteQuery(original_query, { "name_service": "if_checked" }); - params["query"] = query; - if (sendLimit > 0) { - params["send"] = sendLimit; - } - } else { - params["cmd"] = "clear-cache"; - nothingToShow = true; - } - const queryId = generateQueryId(); - const ws = new WebSocket(`${BASEURL.replaceAll(/^http/g, "ws")}/watch/${queryId}`); - const startTimeStamp = Date.now(); - +// Makes the UI display a placeholder text +// indicating that the query started but +// we haven't heard back from it yet. +function signalQueryStart(queryId, startTimeStamp, query) { appendRuntimeInformation( { query_execution_tree: null, meta: {} }, - params["query"], + query, { computeResult: "0ms", total: `${Date.now() - startTimeStamp}ms` @@ -421,9 +399,14 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { updateTimeStamp: startTimeStamp } ); - ws.onopen = () => { - log("Waiting for live updates", "other"); - }; + renderRuntimeInformationToDom(); +} + +function createWebSocketForQuery(queryId, startTimeStamp, query) { + const ws = new WebSocket(getWebSocketUrl(queryId)); + + ws.onopen = () => log("Waiting for live updates", "other"); + ws.onmessage = (message) => { if (typeof message.data !== "string") { log("Unexpected message format", "other"); @@ -434,7 +417,7 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { query_execution_tree: payload, meta: {} }, - params["query"], + query, { computeResult: `${payload["total_time"] || (Date.now() - startTimeStamp)}ms`, total: `${Date.now() - startTimeStamp}ms` @@ -444,21 +427,63 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { updateTimeStamp: Date.now() } ); - visualise(false); + renderRuntimeInformationToDom(); } }; - ws.onerror = () => { - log("Live updates not supported", "other"); - }; + + ws.onerror = () => log("Live updates not supported", "other"); + + return ws; +} + +// Process the given query. +async function processQuery(sendLimit=0, element=$("#exebtn")) { + log('Preparing query...', 'other'); + log('Element: ' + element, 'other'); + if (sendLimit >= 0) { displayStatus("Waiting for response..."); } + + $(element).find('.glyphicon').addClass('glyphicon-spin glyphicon-refresh'); + $(element).find('.glyphicon').removeClass('glyphicon-remove'); + $(element).find('.glyphicon').css('color', $(element).css('color')); + log('Sending request...', 'other'); + + // A negative value for `sendLimit` has the special meaning: clear the cache + // (without issuing a query). This is used in `backend/templates/index.html`, + // in the definition of the `oncklick` action for the "Clear cache" button. + // TODO: super ugly, find a better solution. + let nothingToShow = false; + var params = {}; + if (sendLimit >= 0) { + var original_query = editor.getValue(); + var query = await rewriteQuery(original_query, { "name_service": "if_checked" }); + params["query"] = query; + if (sendLimit > 0) { + params["send"] = sendLimit; + } + } else { + params["cmd"] = "clear-cache"; + nothingToShow = true; + } + + const headers = { + "Content-type": "application/x-www-form-urlencoded", + "Accept": "application/qlever-results+json" + } + let ws = null; + let queryId = undefined; + if (!nothingToShow) { + queryId = generateQueryId(); + const startTimeStamp = Date.now(); + signalQueryStart(queryId, startTimeStamp, params["query"]); + ws = createWebSocketForQuery(queryId, startTimeStamp, params["query"]); + headers["Query-Id"] = queryId; + } + $.ajax({ method: "POST", url: BASEURL, data: $.param(params), - headers: { - "Content-type": "application/x-www-form-urlencoded", - "Accept": "application/qlever-results+json", - "Query-Id": queryId, - }, + headers: headers, success: function (result) { log('Evaluating and displaying results...', 'other'); @@ -471,7 +496,10 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { return; } - if (result.status == "ERROR") { displayError(queryId, result); return; } + if (result.status == "ERROR") { + displayError(result, undefined, queryId); + return; + } if (result["warnings"].length > 0) { displayWarning(result); } // Show some statistics (on top of the table). @@ -613,7 +641,10 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { // MAX_VALUE ensures this always has priority over the websocket updates appendRuntimeInformation(result.runtimeInformation, result.query, result.time, { queryId, updateTimeStamp: Number.MAX_VALUE }); - visualise(false); + renderRuntimeInformationToDom(); + + // Make sure we have no socket that stays open forever + ws.close(); } }).fail(function (jqXHR, textStatus, errorThrown) { $(element).find('.glyphicon').removeClass('glyphicon-spin glyphicon-refresh'); @@ -632,7 +663,12 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { } var statusWithText = jqXHR.status && jqXHR.statusText ? (jqXHR.status + " (" + jqXHR.statusText + ")") : undefined; - displayError(queryId, jqXHR.responseJSON, statusWithText); + displayError(jqXHR.responseJSON, statusWithText, nothingToShow ? undefined : queryId); + + // Make sure we have no socket that stays open forever + if (ws) { + ws.close(); + } }); } @@ -668,18 +704,22 @@ function handleStatsDisplay() { $('#statsButton span').html(' Unable to connect to backend'); }); } - -function visualise(show, number) { - if (show) { - $("#visualisation").modal("show"); - } +// Shows the modal containing the current runtime information tree +function showQueryPlanningTree() { + $("#visualisation").modal("show"); +} + +// Uses the information inside of runtime_log and query_log +// to populate the DOM with the current runtime information. +// Use showQueryPlanningTree() to display it to the user. +function renderRuntimeInformationToDom(number) { if (runtime_log.length === 0) { return; } // Get the right entries from the runtime log. - runtime_log_index = number ? number - 1 : runtime_log.length - 1; + let runtime_log_index = number || runtime_log.length - 1; let runtime_info = runtime_log[runtime_log_index]; let resultQuery = query_log[runtime_log_index]; @@ -708,11 +748,7 @@ function visualise(show, number) { ); // Show the query. - resultQuery = resultQuery - .replace(/&/g, "&").replace(/"/g, """) - .replace(//g, ">") - .replace(/'/g, "'"); - $("#result-query").html("
" + resultQuery + "
"); + $("#result-query").html($("
", { text: resultQuery }));
 
   // Show the query execution tree (using Treant.js).
   addTextElementsToQueryExecutionTreeForTreant(runtime_info["query_execution_tree"]);
@@ -724,7 +760,7 @@ function visualise(show, number) {
     },
     nodeStructure: runtime_info["query_execution_tree"]
   }
-  var treant_chart = new Treant(treant_tree);
+  new Treant(treant_tree);
 
   // For each node, on mouseover show the details.
   $("div.node").hover(function () {
@@ -753,11 +789,22 @@ function visualise(show, number) {
   $("p.node-status").filter(function() { return $(this).text() === "optimized out"}).addClass("optimized-out");
   
   if ($('#logRequests').is(':checked')) {
-    select = "";
-    for (var i = runtime_log.length; i > runtime_log.length - 10 && i > 0; i--) {
-      select = '
  • [' + i + ']
  • ' + select; + const queryHistoryList = $("