You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@impala.apache.org by st...@apache.org on 2023/05/25 21:31:17 UTC
[impala] branch master updated (683bef1ca -> ee300a1af)
This is an automated email from the ASF dual-hosted git repository.
stigahuang pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git
from 683bef1ca IMPALA-11253: Support testing with Java 11 (take 2)
new 1cf8f5065 IMPALA-12053: Expose event-processor error message in WebUI
new 58a94268b IMPALA-11915: Support graphical plan and timeline exports in the WebUI
new ee300a1af IMPALA-12163: Fixes two issues when outputting RPC details.
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
be/src/catalog/catalog-server.cc | 8 +-
common/thrift/JniCatalog.thrift | 4 +-
.../impala/catalog/events/MetastoreEvents.java | 2 +-
.../catalog/events/MetastoreEventsProcessor.java | 58 ++++++++++----
shell/impala_client.py | 4 +-
tests/shell/test_shell_commandline.py | 35 +++++++--
tests/util/event_processor_utils.py | 15 +++-
www/events.tmpl | 5 ++
www/query_plan.tmpl | 67 ++++++++++++++--
www/query_timeline.tmpl | 89 +++++++++++++++++++---
10 files changed, 246 insertions(+), 41 deletions(-)
[impala] 03/03: IMPALA-12163: Fixes two issues when outputting RPC details.
Posted by st...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
stigahuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git
commit ee300a1af09514af5650b0b14b90afc8d23d54b1
Author: jasonmfehr <jf...@cloudera.com>
AuthorDate: Wed May 24 13:38:54 2023 -0700
IMPALA-12163: Fixes two issues when outputting RPC details.
The end time of the exact same rpc call was different between stdout
and the rpc details file because the end time was calculated each
time the details were written out instead of calculating the end time
once and reusing that value.
The duration of each rpc call was being calculated incorrectly.
Change-Id: Ifd9dec189d0f6fb8713fb1c7b2b6c663e492ef05
Reviewed-on: http://gerrit.cloudera.org:8080/19932
Reviewed-by: Impala Public Jenkins <im...@cloudera.com>
Tested-by: Impala Public Jenkins <im...@cloudera.com>
---
shell/impala_client.py | 4 ++--
tests/shell/test_shell_commandline.py | 35 ++++++++++++++++++++++++++++++-----
2 files changed, 32 insertions(+), 7 deletions(-)
diff --git a/shell/impala_client.py b/shell/impala_client.py
index 87ba6c09c..a8cd36f0c 100755
--- a/shell/impala_client.py
+++ b/shell/impala_client.py
@@ -1262,9 +1262,9 @@ class ImpalaHS2Client(ImpalaClient):
def print_end_to_file(fh):
self._print_line_separator(fh)
- fh.write("[{0}] RPC CALL FINISHED:\n".format(datetime.now()))
+ fh.write("[{0}] RPC CALL FINISHED:\n".format(end_time))
fh.write("OPERATION: {0}\nDETAILS:\n".format(rpc_func.__name__))
- fh.write(" * Time: {0}ms\n".format(duration.microseconds / 1000))
+ fh.write(" * Time: {0}ms\n".format(duration.total_seconds() * 1000))
fh.write(" * Result: {0}\n".format(result))
if rpc_output is not None:
fh.write("\nRPC RESPONSE:\n")
diff --git a/tests/shell/test_shell_commandline.py b/tests/shell/test_shell_commandline.py
index 9bbbac41c..750a100c2 100644
--- a/tests/shell/test_shell_commandline.py
+++ b/tests/shell/test_shell_commandline.py
@@ -1422,11 +1422,11 @@ class TestImpalaShell(ImpalaTestSuite):
assert "1\n" in lines[len(lines) - 2]
assert "Fetched 1 row(s)" in lines[len(lines) - 1]
- def skip_if_protocol_is_beeswax(self, vector):
+ def skip_if_protocol_is_beeswax(self, vector,
+ skip_msg="Floating-point value formatting is not supported with Beeswax"):
"""Helper to skip Beeswax protocol on formatting tests"""
if vector.get_value("protocol") == "beeswax":
- pytest.skip("Floating-point value formatting is not supported "
- "with Beeswax")
+ pytest.skip(skip_msg)
def validate_fp_format(self, vector, column_type, format, value, expected_values):
args = ['--hs2_fp_format', format, '-q',
@@ -1503,15 +1503,40 @@ class TestImpalaShell(ImpalaTestSuite):
def test_output_rpc_to_screen_and_file(self, vector, populated_table, tmp_file):
"""Tests the flags that output hs2 rpc call details to both stdout
and a file. Asserts the expected text is written."""
- self.skip_if_protocol_is_beeswax(vector)
+ self.skip_if_protocol_is_beeswax(vector,
+ "rpc detail output not supported with beeswax protocol")
args = ['--rpc_stdout', '--rpc_file', tmp_file,
- '-q', 'select * from {0}'.format(populated_table)]
+ '-q', 'select * from {0}'.format(populated_table),
+ '--protocol={0}'.format(vector.get_value("protocol"))]
+ if vector.get_value("strict_hs2_protocol") is True:
+ args.append('--strict_hs2_protocol')
+
result = run_impala_shell_cmd(vector, args)
stdout_data = result.stdout.strip()
rpc_file_data = open(tmp_file, "r").read().strip()
+ # compare the rpc details from stdout and file to ensure they match
+ # stdout contains additional output such as query results, remove all non-rpc details
+ rpc_sep = "------------------------------------------------" + \
+ "------------------------------------------------"
+ stdout_data_rpc_only = ""
+ in_rpc_detail = False
+ for line in stdout_data.split("\n"):
+ if line == rpc_sep:
+ in_rpc_detail = not in_rpc_detail
+ stdout_data_rpc_only += line + "\n"
+ elif in_rpc_detail:
+ stdout_data_rpc_only += line + "\n"
+
+ # rpc only stdout data contains an extra ending newline
+ rpc_file_data += "\n"
+
+ assert stdout_data_rpc_only == rpc_file_data, \
+ "difference found between stdout and rpc file:\nSTDOUT:\n{0}\nFILE:\n{1}" \
+ .format(stdout_data_rpc_only, rpc_file_data)
+
def check_multiline(check_desc, regex_lines):
"""Build and runs a multi-line regular expression against both the
shell's stdout and rpc file contents to ensure the expected data about
[impala] 01/03: IMPALA-12053: Expose event-processor error message in WebUI
Posted by st...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
stigahuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git
commit 1cf8f5065acb79e4377492c6e67c312249e42a78
Author: stiga-huang <hu...@gmail.com>
AuthorDate: Tue May 23 16:50:58 2023 +0800
IMPALA-12053: Expose event-processor error message in WebUI
When the event-processor goes into the ERROR/NEEDS_INVALIDATE state, we
can only check logs to get the detailed information. This is
inconvenient in triaging failures. This patch exposes the error message
in the /events WebUI. It includes the timestamp string and the
stacktrace of the exception.
This patch makes the /events page visable. Also modifies the test code
of EventProcessorUtils.wait_for_synced_event_id() to print the error
message if the event processor is down.
A trivial bug of lastProcessedEvent is not updated (IMPALA-11588) is
also fixed in this patch. Refactored the variable to be a member of the
class so internal methods can update it before processing each event.
Some new metrics are not added in the /events page, e.g.
latest-event-id, latest-event-time-ms, last-synced-event-time-ms. This
patch addes them and also add a metric of event-processing-delay-ms
which is latest-event-time-ms minors last-synced-event-time-ms.
Tests:
- Manually inject codes to fail the event processor and verified the
WebUI.
- Ran metadata/test_event_processing.py when the event processor is in
ERROR state. Verified the error message is shown up in test output.
Change-Id: I077375422bc3d24eed57c95c6b05ac408228f083
Reviewed-on: http://gerrit.cloudera.org:8080/19916
Reviewed-by: Impala Public Jenkins <im...@cloudera.com>
Tested-by: Impala Public Jenkins <im...@cloudera.com>
---
be/src/catalog/catalog-server.cc | 8 ++-
common/thrift/JniCatalog.thrift | 4 +-
.../impala/catalog/events/MetastoreEvents.java | 2 +-
.../catalog/events/MetastoreEventsProcessor.java | 58 +++++++++++++++++-----
tests/util/event_processor_utils.py | 15 +++++-
www/events.tmpl | 5 ++
6 files changed, 75 insertions(+), 17 deletions(-)
diff --git a/be/src/catalog/catalog-server.cc b/be/src/catalog/catalog-server.cc
index f2a5ffc30..cb31bc0d2 100644
--- a/be/src/catalog/catalog-server.cc
+++ b/be/src/catalog/catalog-server.cc
@@ -366,7 +366,7 @@ void CatalogServer::RegisterWebpages(Webserver* webserver, bool metrics_only) {
false);
webserver->RegisterUrlCallback(EVENT_WEB_PAGE, EVENT_METRICS_TEMPLATE,
[this](const auto& args, auto* doc) { this->EventMetricsUrlCallback(args, doc); },
- false);
+ true);
webserver->RegisterUrlCallback(CATALOG_OPERATIONS_WEB_PAGE, CATALOG_OPERATIONS_TEMPLATE,
[this](const auto& args, auto* doc) { this->OperationUsageUrlCallback(args, doc); },
true);
@@ -677,6 +677,12 @@ void CatalogServer::EventMetricsUrlCallback(
event_processor_summary_response.summary.c_str(), document->GetAllocator());
document->AddMember(
"event_processor_metrics", event_processor_summary, document->GetAllocator());
+ if (event_processor_summary_response.__isset.error_msg) {
+ Value error_msg(
+ event_processor_summary_response.error_msg.c_str(), document->GetAllocator());
+ document->AddMember(
+ "event_processor_error_msg", error_msg, document->GetAllocator());
+ }
}
void CatalogServer::CatalogObjectsUrlCallback(const Webserver::WebRequest& req,
diff --git a/common/thrift/JniCatalog.thrift b/common/thrift/JniCatalog.thrift
index 56398eaf0..d8699ce0a 100755
--- a/common/thrift/JniCatalog.thrift
+++ b/common/thrift/JniCatalog.thrift
@@ -1025,4 +1025,6 @@ struct TEventProcessorMetricsSummaryResponse {
// summary view of the events processor which can include status,
// metrics and other details
1: required string summary
-}
\ No newline at end of file
+ // Error messages if the events processor goes into ERROR/NEEDS_INVALIDATE states
+ 2: optional string error_msg
+}
diff --git a/fe/src/main/java/org/apache/impala/catalog/events/MetastoreEvents.java b/fe/src/main/java/org/apache/impala/catalog/events/MetastoreEvents.java
index fabdfc37d..aae2122c4 100644
--- a/fe/src/main/java/org/apache/impala/catalog/events/MetastoreEvents.java
+++ b/fe/src/main/java/org/apache/impala/catalog/events/MetastoreEvents.java
@@ -1156,7 +1156,7 @@ public class MetastoreEvents {
// determined that the event needs to be processed instead of skipped, or we
// somehow missed the previous create database event.
throw new MetastoreNotificationException(
- debugString("Unable to process event", e));
+ debugString("Unable to process event"), e);
}
}
diff --git a/fe/src/main/java/org/apache/impala/catalog/events/MetastoreEventsProcessor.java b/fe/src/main/java/org/apache/impala/catalog/events/MetastoreEventsProcessor.java
index 7c2d6d368..19b7ddc36 100644
--- a/fe/src/main/java/org/apache/impala/catalog/events/MetastoreEventsProcessor.java
+++ b/fe/src/main/java/org/apache/impala/catalog/events/MetastoreEventsProcessor.java
@@ -23,6 +23,7 @@ import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -33,6 +34,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.Nullable;
+import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.hadoop.hive.metastore.IMetaStoreClient;
import org.apache.hadoop.hive.metastore.IMetaStoreClient.NotificationFilter;
import org.apache.hadoop.hive.metastore.api.CurrentNotificationEventId;
@@ -232,6 +234,14 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
public static final String STATUS_METRIC = "status";
// last synced event id
public static final String LAST_SYNCED_ID_METRIC = "last-synced-event-id";
+ // last synced event time
+ public static final String LAST_SYNCED_EVENT_TIME = "last-synced-event-time-ms";
+ // latest event id in Hive metastore
+ public static final String LATEST_EVENT_ID = "latest-event-id";
+ // event time of the latest event in Hive metastore
+ public static final String LATEST_EVENT_TIME = "latest-event-time-ms";
+ // delay(ms) in events processing
+ public static final String EVENT_PROCESSING_DELAY = "event-processing-delay-ms";
// metric name for number of tables which are refreshed by event processor so far
public static final String NUMBER_OF_TABLE_REFRESHES = "tables-refreshed";
// number of times events processor refreshed a partition
@@ -489,9 +499,15 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
// current status of this event processor
private EventProcessorStatus eventProcessorStatus_ = EventProcessorStatus.STOPPED;
+ // error message when event processor comes into ERROR/NEEDS_INVALIDATE states
+ private String eventProcessorErrorMsg_ = null;
+
// event factory which is used to get or create MetastoreEvents
private final MetastoreEventFactory metastoreEventFactory_;
+ // keeps track of the current event that we are processing
+ private NotificationEvent currentEvent_;
+
// keeps track of the last event id which we have synced to
private final AtomicLong lastSyncedEventId_ = new AtomicLong(-1);
private final AtomicLong lastSyncedEventTimeMs_ = new AtomicLong(0);
@@ -588,10 +604,13 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
metrics_.addTimer(EVENTS_PROCESS_DURATION_METRIC);
metrics_.addMeter(EVENTS_RECEIVED_METRIC);
metrics_.addCounter(EVENTS_SKIPPED_METRIC);
- metrics_.addGauge(STATUS_METRIC,
- (Gauge<String>) () -> getStatus().toString());
- metrics_.addGauge(LAST_SYNCED_ID_METRIC,
- (Gauge<Long>) () -> lastSyncedEventId_.get());
+ metrics_.addGauge(STATUS_METRIC, (Gauge<String>) () -> getStatus().toString());
+ metrics_.addGauge(LAST_SYNCED_ID_METRIC, (Gauge<Long>) lastSyncedEventId_::get);
+ metrics_.addGauge(LAST_SYNCED_EVENT_TIME, (Gauge<Long>) lastSyncedEventTimeMs_::get);
+ metrics_.addGauge(LATEST_EVENT_ID, (Gauge<Long>) latestEventId_::get);
+ metrics_.addGauge(LATEST_EVENT_TIME, (Gauge<Long>) latestEventTimeMs_::get);
+ metrics_.addGauge(EVENT_PROCESSING_DELAY,
+ (Gauge<Long>) () -> latestEventTimeMs_.get() - lastSyncedEventTimeMs_.get());
metrics_.addCounter(NUMBER_OF_TABLE_REFRESHES);
metrics_.addCounter(NUMBER_OF_PARTITION_REFRESHES);
metrics_.addCounter(NUMBER_OF_TABLES_ADDED);
@@ -837,7 +856,7 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
*/
@Override
public void processEvents() {
- NotificationEvent lastProcessedEvent = null;
+ currentEvent_ = null;
try {
EventProcessorStatus currentStatus = eventProcessorStatus_;
if (currentStatus != EventProcessorStatus.ACTIVE) {
@@ -856,14 +875,20 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
"may be unavailable. Will retry.", ex);
} catch(MetastoreNotificationNeedsInvalidateException ex) {
updateStatus(EventProcessorStatus.NEEDS_INVALIDATE);
- LOG.error("Event processing needs a invalidate command to resolve the state", ex);
+ String msg = "Event processing needs a invalidate command to resolve the state";
+ LOG.error(msg, ex);
+ eventProcessorErrorMsg_ = LocalDateTime.now().toString() + '\n' + msg + '\n' +
+ ExceptionUtils.getFullStackTrace(ex);
} catch (Exception ex) {
// There are lot of Preconditions which can throw RuntimeExceptions when we
// process events this catch all exception block is needed so that the scheduler
// thread does not die silently
updateStatus(EventProcessorStatus.ERROR);
- LOG.error("Unexpected exception received while processing event", ex);
- dumpEventInfoToLog(lastProcessedEvent);
+ String msg = "Unexpected exception received while processing event";
+ LOG.error(msg, ex);
+ eventProcessorErrorMsg_ = LocalDateTime.now().toString() + '\n' + msg + '\n' +
+ ExceptionUtils.getFullStackTrace(ex);
+ dumpEventInfoToLog(currentEvent_);
}
}
@@ -973,6 +998,9 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
TEventProcessorMetricsSummaryResponse summaryResponse =
new TEventProcessorMetricsSummaryResponse();
summaryResponse.setSummary(metrics_.toString());
+ if (eventProcessorErrorMsg_ != null) {
+ summaryResponse.setError_msg(eventProcessorErrorMsg_);
+ }
return summaryResponse;
}
@@ -988,7 +1016,7 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
@VisibleForTesting
protected void processEvents(List<NotificationEvent> events)
throws MetastoreNotificationException {
- NotificationEvent lastProcessedEvent = null;
+ currentEvent_ = null;
// update the events received metric before returning
metrics_.getMeter(EVENTS_RECEIVED_METRIC).mark(events.size());
if (events.isEmpty()) return;
@@ -1009,7 +1037,7 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
if (eventProcessorStatus_ != EventProcessorStatus.ACTIVE) {
break;
}
- lastProcessedEvent = event.metastoreNotificationEvent_;
+ currentEvent_ = event.metastoreNotificationEvent_;
event.processIfEnabled();
deleteEventLog_.garbageCollect(event.getEventId());
lastSyncedEventId_.set(event.getEventId());
@@ -1019,7 +1047,7 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
} catch (CatalogException e) {
throw new MetastoreNotificationException(String.format(
"Unable to process event %d of type %s. Event processing will be stopped.",
- lastProcessedEvent.getEventId(), lastProcessedEvent.getEventType()), e);
+ currentEvent_.getEventId(), currentEvent_.getEventType()), e);
} finally {
long elapsed_ns = context.stop();
lastEventProcessDurationNs_.set(elapsed_ns);
@@ -1037,7 +1065,9 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
private void dumpEventInfoToLog(NotificationEvent event) {
if (event == null) {
- LOG.error("Notification event is null");
+ String error = "Notification event is null";
+ LOG.error(error);
+ eventProcessorErrorMsg_ += '\n' + error;
return;
}
StringBuilder msg =
@@ -1049,7 +1079,9 @@ public class MetastoreEventsProcessor implements ExternalEventsProcessor {
msg.append("Table name: ").append(event.getTableName()).append("\n");
}
msg.append("Event message: ").append(event.getMessage()).append("\n");
- LOG.error(msg.toString());
+ String msgStr = msg.toString();
+ LOG.error(msgStr);
+ eventProcessorErrorMsg_ += '\n' + msgStr;
}
/**
diff --git a/tests/util/event_processor_utils.py b/tests/util/event_processor_utils.py
index 08223f9e8..d485a0432 100644
--- a/tests/util/event_processor_utils.py
+++ b/tests/util/event_processor_utils.py
@@ -60,7 +60,9 @@ class EventProcessorUtils(object):
break
status = EventProcessorUtils.get_event_processor_status()
if status not in ["ACTIVE", "PAUSED"]:
- raise Exception("Event processor is not working. Status: {0}".format(status))
+ error_msg = EventProcessorUtils.get_event_processor_error_msg()
+ raise Exception("Event processor is not working. Status: {0}. Error msg: {1}"
+ .format(status, error_msg))
made_progress = current_synced_id > last_synced_id
if t >= end_time:
raise Exception(
@@ -115,6 +117,17 @@ class EventProcessorUtils(object):
pairs = [strip_pair(kv.split(':')) for kv in metrics if kv]
return dict(pairs)
+ @staticmethod
+ def get_event_processor_error_msg():
+ """Scrapes the catalog's /events webpage and return the error message (if exists) of
+ the event processor"""
+ response = requests.get("%s/events?json" % EventProcessorUtils.DEFAULT_CATALOG_URL)
+ assert response.status_code == requests.codes.ok
+ res_json = json.loads(response.text)
+ if "event_processor_error_msg" in res_json:
+ return res_json["event_processor_error_msg"].strip()
+ return None
+
@staticmethod
def get_int_metric(metric_key, default_val=None):
"""Returns the int value of event processor metric from the /events catalogd debug
diff --git a/www/events.tmpl b/www/events.tmpl
index 484cfeae2..cdc16cae3 100644
--- a/www/events.tmpl
+++ b/www/events.tmpl
@@ -22,4 +22,9 @@ under the License.
<h3>Event Processor Summary</h3>
<pre>{{event_processor_metrics}}</pre>
+{{?event_processor_error_msg}}
+<h3>Error Message</h3>
+<pre>{{event_processor_error_msg}}</pre>
+{{/event_processor_error_msg}}
+
{{> www/common-footer.tmpl }}
[impala] 02/03: IMPALA-11915: Support graphical plan and timeline exports in the WebUI
Posted by st...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
stigahuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git
commit 58a94268ba2b719d0ca289f9635992a3ca700976
Author: Surya Hebbar <sh...@cloudera.com>
AuthorDate: Tue Apr 18 12:43:47 2023 +0530
IMPALA-11915: Support graphical plan and timeline exports in the WebUI
This adds support for exporting the query plan and timeline
in SVG format for better scaling and text selection.
The plan and timeline export buttons embed the CSS styling for
foreign objects and the rendered SVG viewport into a blob,
enclosed within HTML tags.
Timeline overflow's are associated with each SVG component,
instead of the entire diagram.
To preserve appropriate styling such as SVG component's borders,
for each SVG component, DOM elements are deep cloned as wrappers
during the export.
Memory resources consumed from the ObjectURLs are cleared
after each export, once previous references are out of scope.
SVG viewer implementations for XML namespaces to render
foreign objects with CSS styling differs, and are not always
supported. To avoid such problems, the plan export SVGs
have been enclosed within a HTML wrapper.
Replacement of foreign objects in the graphical plan with
SVG elements such as <text> would allow SVG exports within
the same namespace.
Both the query plan and timeline exports contain 'query_id'
as a header.
Text styling has been preserved in query plan's node text and
edge text. Also with timeline's SVG components such as
'fragment_diagram', 'phases_header' and 'timeticks_footer'.
Testing: Manual testing with with TPC-H and TPS-DS queries
Change-Id: I1bd549318e220419ba3ee40be05f3671a9f1d8d9
Reviewed-on: http://gerrit.cloudera.org:8080/19763
Tested-by: Impala Public Jenkins <im...@cloudera.com>
Reviewed-by: Kurt Deschler <kd...@cloudera.com>
Reviewed-by: Wenzhe Zhou <wz...@cloudera.com>
---
www/query_plan.tmpl | 67 +++++++++++++++++++++++++++++++++----
www/query_timeline.tmpl | 89 +++++++++++++++++++++++++++++++++++++++++++------
2 files changed, 139 insertions(+), 17 deletions(-)
diff --git a/www/query_plan.tmpl b/www/query_plan.tmpl
index 341437680..8d0569b62 100644
--- a/www/query_plan.tmpl
+++ b/www/query_plan.tmpl
@@ -27,8 +27,8 @@ under the License.
/* Text style for graph nodes */
.node {
color: white;
- font-size:14px;
- font-weight:700;
+ font-size: 14px;
+ font-weight: 700;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
@@ -44,6 +44,12 @@ under the License.
fill: #333;
stroke-width: 1.5px;
}
+
+.nodes, .edgeLabel {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
+ Arial, "Noto Sans", sans-serif;
+}
+
</style>
{{> www/query_detail_tabs.tmpl }}
@@ -54,17 +60,46 @@ under the License.
{{/plan_metadata_unavailable}}
{{^plan_metadata_unavailable}}
-<h3>Plan</h3>
-<div>
+<div style="display:flex; justify-content:space-between;">
+ <h3>Plan</h3>
<label>
- <input type="checkbox" checked="true" id="colour_scheme" onClick="refresh()"/>
- Shade nodes according to time spent (if unchecked, shade according to plan fragment)
+ <h4 style="display:inline;"> Download : </h4>
+ <input type="button" class="btn btn-primary" data-toggle="modal" value="HTML"
+ data-target="#export_modal" role="button"/>
</label>
</div>
+<label>
+<input type="checkbox" checked="true" id="colour_scheme" onClick="refresh()"/>
+Shade nodes according to time spent (if unchecked, shade according to plan fragment)
+</label>
<svg style="border: 1px solid darkgray" width=1200 height=600 class="panel"><g/></svg>
{{/plan_metadata_unavailable}}
+<div id="export_modal" style="transition-duration: 0.15s;" class="modal fade"
+ role="dialog" data-keyboard="true" tabindex="-1">
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5> Download Plan </h5>
+ <input class="btn btn-primary" type="button" value="X" data-dismiss="modal"/>
+ </div>
+ <div class="modal-body">
+ <h6 class="d-inline"> Filename: </h6>
+ <input id="export_filename" class="form-control-sm" type="text"
+ value="{{query_id}}_plan"/>
+ <select id="export_format" class="form-control-sm btn btn-primary">
+ <option selected>.html</option>
+ </select>
+ </div>
+ <div class="modal-footer">
+ <a id="export_link" class="btn btn-primary" data-dismiss="modal" href="#"
+ role="button"> Download </a>
+ </div>
+ </div>
+ </div>
+</div>
+
{{> www/common-footer.tmpl }}
<script src="{{ __common__.host-url }}/www/d3.v3.min.js" charset="utf-8"></script>
@@ -83,6 +118,11 @@ var g = new dagreD3.graphlib.Graph().setGraph({rankDir: "BT"});
var svg = d3.select("svg");
var inner = svg.select("g");
+var export_link = document.getElementById("export_link");
+var export_filename = document.getElementById("export_filename");
+var export_format = document.getElementById("export_format");
+export_filename.value = export_filename.value.replace(/\W/g,'_');
+
// Set up zoom support
var zoom = d3.behavior.zoom().on("zoom", function() {
inner.attr("transform", "translate(" + d3.event.translate + ")" +
@@ -231,6 +271,21 @@ function refresh() {
req.send();
}
+// Attaches a blob of the current SVG viewport to the associated link
+export_link.addEventListener('click', function(event) {
+ if (export_format.value == ".html") {
+ var svg_viewport = document.querySelector("svg");
+ var export_style = document.getElementById("css");
+ var html_blob = new Blob([`<!DOCTYPE html><body>`,
+ `<h1 style="font-family:monospace;">Query {{query_id}}</h1>`,
+ export_style.outerHTML, svg_viewport.outerHTML, `</body></html>`],
+ {type: "text/html;charset=utf-8"});
+ export_link.href = URL.createObjectURL(html_blob);
+ }
+ export_link.download = `${export_filename.value}${export_format.value}`;
+ export_link.click();
+});
+
// Force one refresh before starting the timer.
refresh();
diff --git a/www/query_timeline.tmpl b/www/query_timeline.tmpl
index ad4f057cd..9061c7b12 100644
--- a/www/query_timeline.tmpl
+++ b/www/query_timeline.tmpl
@@ -24,6 +24,11 @@ under the License.
<div class="container">
<style id="css">
+#fragment_diagram, #phases_header, #timeticks_footer {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
+ Arial, "Noto Sans", sans-serif;
+ vertical-align: middle;
+}
</style>
{{> www/query_detail_tabs.tmpl }}
@@ -34,27 +39,57 @@ under the License.
{{/plan_metadata_unavailable}}
{{^plan_metadata_unavailable}}
-<h3>Timeline</h3>
+ <div style="display:flex; justify-content:space-between;">
+ <h3>Timeline</h3>
+ <label>
+ <h4 style="display:inline;"> Download : </h4>
+ <input type="button" class="btn btn-primary" data-toggle="modal" value="HTML"
+ data-target="#export_modal" role="button"/>
+ </label>
+ </div>
<label>
- <input type="checkbox" id="plan_order" onClick="renderTiming()"/>
- Print tree in plan order (if unchecked, print in fragment order)
+ <input type="checkbox" id="plan_order" onClick="renderTiming()"/>
+ Print tree in plan order (if unchecked, print in fragment order)
</label>
</div>
-<div id="timing_diagram" style="border:1px solid #c3c3c3; overflow:hidden;">
- <div style="margin-top:5px; border:1px solid #c3c3c3;">
+<div id="export_modal" style="transition-duration: 0.15s;" class="modal fade"
+ role="dialog" data-keyboard="true" tabindex="-1">
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5> Download Timeline </h5>
+ <input class="btn btn-primary" type="button" value="X" data-dismiss="modal"/>
+ </div>
+ <div class="modal-body">
+ <h6 class="d-inline"> Filename: </h6>
+ <input id="export_filename" class="form-control-sm" type="text"
+ value="{{query_id}}_timeline"/>
+ <select id="export_format" class="form-control-sm btn btn-primary">
+ <option selected>.html</option>
+ </select>
+ </div>
+ <div class="modal-footer">
+ <a id="export_link" class="btn btn-primary" data-dismiss="modal" href="#"
+ role="button"> Download </a>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div id="timing_diagram" style="border:1px solid #c3c3c3;">
+ <div style="margin-top:5px; border:1px solid #c3c3c3; overflow:hidden;">
<svg id="phases_header" height="15px"></svg>
</div>
- <div style="margin-top:5px; border:1px solid #c3c3c3; overflow-y:auto;">
+ <div style="margin-top:5px; border:1px solid #c3c3c3; overflow-y:auto; overflow-x:hidden;">
<svg id="fragment_diagram"></svg>
</div>
- <div style="margin-top:5px; border:1px solid #c3c3c3;">
+ <div style="margin-top:5px; border:1px solid #c3c3c3; overflow:hidden;">
<svg id="timeticks_footer" height="15px"></svg>
</div>
</div>
-
{{/plan_metadata_unavailable}}
{{> www/common-footer.tmpl }}
@@ -83,7 +118,11 @@ var receiver_nodes = [];
var profile_available = false;
var maxts = 0;
-function get_svg_rect(fill_color, x, y, width, height, dash, stroke_color){
+var export_filename = document.getElementById("export_filename");
+var export_format = document.getElementById("export_format");
+export_filename.value = export_filename.value.replace(/\W/g,'_');
+
+function get_svg_rect(fill_color, x, y, width, height, dash, stroke_color) {
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", x + "px");
rect.setAttribute("y", y + "px");
@@ -97,7 +136,7 @@ function get_svg_rect(fill_color, x, y, width, height, dash, stroke_color){
return rect;
}
-function get_svg_text(text, fill_color, x, y, height, container_center, max_width = 0){
+function get_svg_text(text, fill_color, x, y, height, container_center, max_width = 0) {
var text_el = document.createElementNS("http://www.w3.org/2000/svg", "text");
text_el.appendChild(document.createTextNode(text));
text_el.setAttribute("x", x + "px");
@@ -115,7 +154,7 @@ function get_svg_text(text, fill_color, x, y, height, container_center, max_widt
return text_el;
}
-function get_svg_line(stroke_color, x1, y1, x2, y2, dash){
+function get_svg_line(stroke_color, x1, y1, x2, y2, dash) {
var line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", x1 + "px");
line.setAttribute("y1", y1 + "px");
@@ -519,6 +558,34 @@ function refresh() {
req.send();
}
+// Attaches a SVG blob of the complete timeline to the associated link
+export_link.addEventListener('click', function(event) {
+ if (export_format.value == ".html") {
+ var phases_header = document.getElementById('phases_header');
+ var fragment_diagram = document.getElementById('fragment_diagram');
+ var timeticks_footer = document.getElementById('timeticks_footer');
+ var export_style = document.getElementById("css");
+
+ // Deep clone 'parentNode's as wrappers to SVG components
+ var phases_header_wrapper = phases_header.parentNode.cloneNode(true);
+ var fragment_diagram_wrapper = fragment_diagram.parentNode.cloneNode(true);
+ var timeticks_footer_wrapper = timeticks_footer.parentNode.cloneNode(true);
+
+ // Set dimensions for fragment diagram's wrapper
+ fragment_diagram_wrapper.style.height = fragment_diagram.style.height;
+ fragment_diagram_wrapper.style.width = fragment_diagram.style.width;
+
+ var html_blob = new Blob([`<!DOCTYPE html><body>`,
+ `<h1 style="font-family:monospace;">Query {{query_id}}</h1>`,
+ export_style.outerHTML, phases_header_wrapper.outerHTML,
+ fragment_diagram_wrapper.outerHTML, timeticks_footer_wrapper.outerHTML,
+ `</body></html>`], {type: "text/html;charset=utf-8"});
+ export_link.href = URL.createObjectURL(html_blob);
+ }
+ export_link.download = `${export_filename.value}${export_format.value}`;
+ export_link.click();
+});
+
window.addEventListener('resize', function(event) {
if (profile_available) {
renderTiming();