You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@impala.apache.org by ta...@apache.org on 2017/08/24 02:48:10 UTC

[5/6] incubator-impala git commit: IMPALA-5811: Add 'backends' tab to query details pages

IMPALA-5811: Add 'backends' tab to query details pages

Add a 'backends' tab to query details pages which shows:

  * host
  * total number of fragment instances for that query on that backend
  * number of still-running fragment instances
  * if the backend is complete (i.e. all instances finished)
  * peak memory consumption
  * the time, in ms, since a status report was received at the
  * coordinator from that backend.

The table refreshes itself every second, controllable by a check-box. If
the query has completed, no information is displayed.

Testing: Add a new smoketest to test_web_pages.py.

Change-Id: Ib5b3b0fb8f4188da56da593199f41ce6fab99767
Reviewed-on: http://gerrit.cloudera.org:8080/7711
Reviewed-by: Dan Hecht <dh...@cloudera.com>
Tested-by: Impala Public Jenkins


Project: http://git-wip-us.apache.org/repos/asf/incubator-impala/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-impala/commit/ff5e9b6c
Tree: http://git-wip-us.apache.org/repos/asf/incubator-impala/tree/ff5e9b6c
Diff: http://git-wip-us.apache.org/repos/asf/incubator-impala/diff/ff5e9b6c

Branch: refs/heads/master
Commit: ff5e9b6c9a35e2869c0c845d7bbb7beb16b5d45e
Parents: 6f20df8
Author: Henry Robinson <he...@cloudera.com>
Authored: Tue Aug 15 22:21:18 2017 -0700
Committer: Impala Public Jenkins <im...@gerrit.cloudera.org>
Committed: Thu Aug 24 02:40:28 2017 +0000

----------------------------------------------------------------------
 be/src/runtime/coordinator-backend-state.cc | 26 +++++++
 be/src/runtime/coordinator-backend-state.h  |  7 ++
 be/src/runtime/coordinator.cc               | 12 +++
 be/src/runtime/coordinator.h                | 19 +++--
 be/src/service/impala-http-handler.cc       | 22 ++++++
 be/src/service/impala-http-handler.h        |  5 ++
 tests/webserver/test_web_pages.py           | 85 ++++++++++++++-------
 www/query_backends.tmpl                     | 94 ++++++++++++++++++++++++
 www/query_detail_tabs.tmpl                  |  1 +
 9 files changed, 237 insertions(+), 34 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/ff5e9b6c/be/src/runtime/coordinator-backend-state.cc
----------------------------------------------------------------------
diff --git a/be/src/runtime/coordinator-backend-state.cc b/be/src/runtime/coordinator-backend-state.cc
index 88f7f21..34e0671 100644
--- a/be/src/runtime/coordinator-backend-state.cc
+++ b/be/src/runtime/coordinator-backend-state.cc
@@ -47,6 +47,7 @@
 #include "common/names.h"
 
 using namespace impala;
+using namespace rapidjson;
 namespace accumulators = boost::accumulators;
 
 Coordinator::BackendState::BackendState(
@@ -249,6 +250,8 @@ bool Coordinator::BackendState::ApplyExecStatusReport(
     ProgressUpdater* scan_range_progress) {
   lock_guard<SpinLock> l1(exec_summary->lock);
   lock_guard<mutex> l2(lock_);
+  last_report_time_ms_ = MonotonicMillis();
+
   // If this backend completed previously, don't apply the update.
   if (IsDone()) return false;
   for (const TFragmentInstanceExecStatus& instance_exec_status:
@@ -561,3 +564,26 @@ void Coordinator::FragmentStats::AddExecStats() {
   avg_profile_->AddInfoString("execution rates", rates_label.str());
   avg_profile_->AddInfoString("num instances", lexical_cast<string>(num_instances_));
 }
+
+void Coordinator::BackendState::ToJson(Value* value, Document* document) {
+  lock_guard<mutex> l(lock_);
+  value->AddMember("num_instances", fragments_.size(), document->GetAllocator());
+  value->AddMember("done", IsDone(), document->GetAllocator());
+  value->AddMember(
+      "peak_mem_consumption", peak_consumption_, document->GetAllocator());
+
+  string host = TNetworkAddressToString(impalad_address());
+  Value val(host.c_str(), document->GetAllocator());
+  value->AddMember("host", val, document->GetAllocator());
+
+  value->AddMember("rpc_latency", rpc_latency(), document->GetAllocator());
+  value->AddMember("time_since_last_heard_from", MonotonicMillis() - last_report_time_ms_,
+      document->GetAllocator());
+
+  string status_str = status_.ok() ? "OK" : status_.GetDetail();
+  Value status_val(status_str.c_str(), document->GetAllocator());
+  value->AddMember("status", status_val, document->GetAllocator());
+
+  value->AddMember(
+      "num_remaining_instances", num_remaining_instances_, document->GetAllocator());
+}

http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/ff5e9b6c/be/src/runtime/coordinator-backend-state.h
----------------------------------------------------------------------
diff --git a/be/src/runtime/coordinator-backend-state.h b/be/src/runtime/coordinator-backend-state.h
index 0846119..ccc3618 100644
--- a/be/src/runtime/coordinator-backend-state.h
+++ b/be/src/runtime/coordinator-backend-state.h
@@ -113,6 +113,10 @@ class Coordinator::BackendState {
   /// debugging aid for backend deadlocks.
   static void LogFirstInProgress(std::vector<BackendState*> backend_states);
 
+  /// Serializes backend state to JSON by adding members to 'value', including total
+  /// number of instances, peak memory consumption, host and status amongst others.
+  void ToJson(rapidjson::Value* value, rapidjson::Document* doc);
+
  private:
   /// Execution stats for a single fragment instance.
   /// Not thread-safe.
@@ -213,6 +217,9 @@ class Coordinator::BackendState {
   /// peak_consumption()
   int64_t peak_consumption_;
 
+  /// Set in ApplyExecStatusReport(). Uses MonotonicMillis().
+  int64_t last_report_time_ms_ = 0;
+
   /// Fill in rpc_params based on state. Uses filter_routing_table to remove filters
   /// that weren't selected during its construction.
   void SetRpcParams(const DebugOptions& debug_options,

http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/ff5e9b6c/be/src/runtime/coordinator.cc
----------------------------------------------------------------------
diff --git a/be/src/runtime/coordinator.cc b/be/src/runtime/coordinator.cc
index a9936ad..029e0bc 100644
--- a/be/src/runtime/coordinator.cc
+++ b/be/src/runtime/coordinator.cc
@@ -71,6 +71,7 @@
 #include "common/names.h"
 
 using namespace apache::thrift;
+using namespace rapidjson;
 using namespace strings;
 using boost::algorithm::iequals;
 using boost::algorithm::is_any_of;
@@ -1221,4 +1222,15 @@ void Coordinator::GetTExecSummary(TExecSummary* exec_summary) {
 MemTracker* Coordinator::query_mem_tracker() const {
   return query_state()->query_mem_tracker();
 }
+
+void Coordinator::BackendsToJson(Document* doc) {
+  lock_guard<mutex> l(lock_);
+  Value states(kArrayType);
+  for (BackendState* state : backend_states_) {
+    Value val(kObjectType);
+    state->ToJson(&val, doc);
+    states.PushBack(val, doc->GetAllocator());
+  }
+  doc->AddMember("backend_states", states, doc->GetAllocator());
+}
 }

http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/ff5e9b6c/be/src/runtime/coordinator.h
----------------------------------------------------------------------
diff --git a/be/src/runtime/coordinator.h b/be/src/runtime/coordinator.h
index 03d03df..4edef88 100644
--- a/be/src/runtime/coordinator.h
+++ b/be/src/runtime/coordinator.h
@@ -18,20 +18,21 @@
 #ifndef IMPALA_RUNTIME_COORDINATOR_H
 #define IMPALA_RUNTIME_COORDINATOR_H
 
-#include <vector>
 #include <string>
-#include <boost/scoped_ptr.hpp>
+#include <vector>
 #include <boost/accumulators/accumulators.hpp>
-#include <boost/accumulators/statistics/stats.hpp>
-#include <boost/accumulators/statistics/min.hpp>
+#include <boost/accumulators/statistics/max.hpp>
 #include <boost/accumulators/statistics/mean.hpp>
 #include <boost/accumulators/statistics/median.hpp>
-#include <boost/accumulators/statistics/max.hpp>
+#include <boost/accumulators/statistics/min.hpp>
+#include <boost/accumulators/statistics/stats.hpp>
 #include <boost/accumulators/statistics/variance.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <boost/thread/condition_variable.hpp>
+#include <boost/thread/mutex.hpp>
 #include <boost/unordered_map.hpp>
 #include <boost/unordered_set.hpp>
-#include <boost/thread/mutex.hpp>
-#include <boost/thread/condition_variable.hpp>
+#include <rapidjson/document.h>
 
 #include "common/global-types.h"
 #include "common/hdfs.h"
@@ -186,6 +187,10 @@ class Coordinator { // NOLINT: The member variables could be re-ordered to save
   /// filter to fragment instances.
   void UpdateFilter(const TUpdateFilterParams& params);
 
+  /// Adds to 'document' a serialized array of all backends in a member named
+  /// 'backend_states'.
+  void BackendsToJson(rapidjson::Document* document);
+
  private:
   class BackendState;
   struct FilterTarget;

http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/ff5e9b6c/be/src/service/impala-http-handler.cc
----------------------------------------------------------------------
diff --git a/be/src/service/impala-http-handler.cc b/be/src/service/impala-http-handler.cc
index 79903b4..e93aacf 100644
--- a/be/src/service/impala-http-handler.cc
+++ b/be/src/service/impala-http-handler.cc
@@ -101,6 +101,9 @@ void ImpalaHttpHandler::RegisterHandlers(Webserver* webserver) {
   webserver->RegisterUrlCallback("/query_memory", "query_memory.tmpl",
       MakeCallback(this, &ImpalaHttpHandler::QueryMemoryHandler), false);
 
+  webserver->RegisterUrlCallback("/query_backends", "query_backends.tmpl",
+      MakeCallback(this, &ImpalaHttpHandler::QueryBackendsHandler), false);
+
   webserver->RegisterUrlCallback("/cancel_query", "common-pre.tmpl",
       MakeCallback(this, &ImpalaHttpHandler::CancelQueryHandler), false);
 
@@ -691,6 +694,25 @@ void PlanToJson(const vector<TPlanFragment>& fragments, const TExecSummary& summ
 
 }
 
+void ImpalaHttpHandler::QueryBackendsHandler(
+    const Webserver::ArgumentMap& args, Document* document) {
+  TUniqueId query_id;
+  Status status = ParseIdFromArguments(args, &query_id, "query_id");
+  Value query_id_val(PrintId(query_id).c_str(), document->GetAllocator());
+  document->AddMember("query_id", query_id_val, document->GetAllocator());
+  if (!status.ok()) {
+    // Redact the error message, it may contain part or all of the query.
+    Value json_error(RedactCopy(status.GetDetail()).c_str(), document->GetAllocator());
+    document->AddMember("error", json_error, document->GetAllocator());
+    return;
+  }
+
+  shared_ptr<ClientRequestState> request_state = server_->GetClientRequestState(query_id);
+  if (request_state.get() == nullptr || request_state->coord() == nullptr) return;
+
+  request_state->coord()->BackendsToJson(document);
+}
+
 void ImpalaHttpHandler::QuerySummaryHandler(bool include_json_plan, bool include_summary,
     const Webserver::ArgumentMap& args, Document* document) {
   TUniqueId query_id;

http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/ff5e9b6c/be/src/service/impala-http-handler.h
----------------------------------------------------------------------
diff --git a/be/src/service/impala-http-handler.h b/be/src/service/impala-http-handler.h
index 485f6db..8ad84bd 100644
--- a/be/src/service/impala-http-handler.h
+++ b/be/src/service/impala-http-handler.h
@@ -91,6 +91,11 @@ class ImpalaHttpHandler {
   void QuerySummaryHandler(bool include_plan_json, bool include_summary,
       const Webserver::ArgumentMap& args, rapidjson::Document* document);
 
+  /// If 'args' contains a query id, serializes all backend states for that query to
+  /// 'document'.
+  void QueryBackendsHandler(
+      const Webserver::ArgumentMap& args, rapidjson::Document* document);
+
   /// Cancels an in-flight query and writes the result to 'contents'.
   void CancelQueryHandler(const Webserver::ArgumentMap& args,
       rapidjson::Document* document);

http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/ff5e9b6c/tests/webserver/test_web_pages.py
----------------------------------------------------------------------
diff --git a/tests/webserver/test_web_pages.py b/tests/webserver/test_web_pages.py
index 4a2d872..2586399 100644
--- a/tests/webserver/test_web_pages.py
+++ b/tests/webserver/test_web_pages.py
@@ -17,6 +17,7 @@
 
 from tests.common.impala_cluster import ImpalaCluster
 from tests.common.impala_test_suite import ImpalaTestSuite
+import json
 import requests
 
 class TestWebPage(ImpalaTestSuite):
@@ -28,6 +29,7 @@ class TestWebPage(ImpalaTestSuite):
   RESET_GLOG_LOGLEVEL_URL = "http://localhost:{0}/reset_glog_level"
   CATALOG_URL = "http://localhost:{0}/catalog"
   CATALOG_OBJECT_URL = "http://localhost:{0}/catalog_object"
+  QUERY_BACKENDS_URL = "http://localhost:{0}/query_backends"
   # log4j changes do not apply to the statestore since it doesn't
   # have an embedded JVM. So we make two sets of ports to test the
   # log level endpoints, one without the statestore port and the
@@ -54,89 +56,95 @@ class TestWebPage(ImpalaTestSuite):
     result = impalad.service.read_debug_webpage("query_profile_encoded?query_id=123")
     assert result.startswith("Could not obtain runtime profile: Query id")
 
-  def get_and_check_status(self, url, string_to_search = "", without_ss = True):
+  def get_and_check_status(self, url, string_to_search = "", ports_to_test = None):
     """Helper method that polls a given url and asserts the return code is ok and
-    the response contains the input string. 'without_ss', when true, excludes the
-    statestore endpoint of the url. Should be applied only for log4j logging changes."""
-    ports_to_test = self.TEST_PORTS_WITHOUT_SS if without_ss else self.TEST_PORTS_WITH_SS
+    the response contains the input string."""
+    if ports_to_test is None:
+      ports_to_test = self.TEST_PORTS_WITH_SS
     for port in ports_to_test:
       input_url = url.format(port)
       response = requests.get(input_url)
       assert response.status_code == requests.codes.ok\
           and string_to_search in response.text, "Offending url: " + input_url
+    return response.text
+
+  def get_and_check_status_jvm(self, url, string_to_search = ""):
+    """Calls get_and_check_status() for impalad and catalogd only"""
+    return self.get_and_check_status(url, string_to_search,
+                                     ports_to_test=self.TEST_PORTS_WITHOUT_SS)
 
   def test_log_level(self):
     """Test that the /log_level page outputs are as expected and work well on basic and
     malformed inputs. This however does not test that the log level changes are actually
     in effect."""
     # Check that the log_level end points are accessible.
-    self.get_and_check_status(self.GET_JAVA_LOGLEVEL_URL)
-    self.get_and_check_status(self.SET_JAVA_LOGLEVEL_URL)
-    self.get_and_check_status(self.RESET_JAVA_LOGLEVEL_URL)
-    self.get_and_check_status(self.SET_GLOG_LOGLEVEL_URL, without_ss=False)
-    self.get_and_check_status(self.RESET_GLOG_LOGLEVEL_URL, without_ss=False)
+    self.get_and_check_status_jvm(self.GET_JAVA_LOGLEVEL_URL)
+    self.get_and_check_status_jvm(self.SET_JAVA_LOGLEVEL_URL)
+    self.get_and_check_status_jvm(self.RESET_JAVA_LOGLEVEL_URL)
+    self.get_and_check_status(self.SET_GLOG_LOGLEVEL_URL)
+    self.get_and_check_status(self.RESET_GLOG_LOGLEVEL_URL)
     # Try getting log level of a class.
     get_loglevel_url = (self.GET_JAVA_LOGLEVEL_URL + "?class" +
         "=org.apache.impala.catalog.HdfsTable")
-    self.get_and_check_status(get_loglevel_url, "DEBUG")
+    self.get_and_check_status_jvm(get_loglevel_url, "DEBUG")
 
     # Set the log level of a class to TRACE and confirm the setting is in place
     set_loglevel_url = (self.SET_JAVA_LOGLEVEL_URL + "?class" +
         "=org.apache.impala.catalog.HdfsTable&level=trace")
-    self.get_and_check_status(set_loglevel_url, "Effective log level: TRACE")
+    self.get_and_check_status_jvm(set_loglevel_url, "Effective log level: TRACE")
 
     get_loglevel_url = (self.GET_JAVA_LOGLEVEL_URL + "?class" +
         "=org.apache.impala.catalog.HdfsTable")
-    self.get_and_check_status(get_loglevel_url, "TRACE")
+    self.get_and_check_status_jvm(get_loglevel_url, "TRACE")
     # Check the log level of a different class and confirm it is still DEBUG
     get_loglevel_url = (self.GET_JAVA_LOGLEVEL_URL + "?class" +
         "=org.apache.impala.catalog.HdfsPartition")
-    self.get_and_check_status(get_loglevel_url, "DEBUG")
+    self.get_and_check_status_jvm(get_loglevel_url, "DEBUG")
 
     # Reset Java logging levels and check the logging level of the class again
-    self.get_and_check_status(self.RESET_JAVA_LOGLEVEL_URL, "Java log levels reset.")
+    self.get_and_check_status_jvm(self.RESET_JAVA_LOGLEVEL_URL, "Java log levels reset.")
     get_loglevel_url = (self.GET_JAVA_LOGLEVEL_URL + "?class" +
         "=org.apache.impala.catalog.HdfsTable")
-    self.get_and_check_status(get_loglevel_url, "DEBUG")
+    self.get_and_check_status_jvm(get_loglevel_url, "DEBUG")
 
     # Set a new glog level and make sure the setting has been applied.
     set_glog_url = (self.SET_GLOG_LOGLEVEL_URL + "?glog=3")
-    self.get_and_check_status(set_glog_url, "v set to 3", False)
+    self.get_and_check_status(set_glog_url, "v set to 3")
 
     # Try resetting the glog logging defaults again.
-    self.get_and_check_status( self.RESET_GLOG_LOGLEVEL_URL, "v set to ", False)
+    self.get_and_check_status( self.RESET_GLOG_LOGLEVEL_URL, "v set to ")
 
     # Try to get the log level of an empty class input
     get_loglevel_url = (self.GET_JAVA_LOGLEVEL_URL + "?class=")
-    self.get_and_check_status(get_loglevel_url, without_ss=True)
+    self.get_and_check_status_jvm(get_loglevel_url)
 
     # Same as above, for set log level request
     set_loglevel_url = (self.SET_JAVA_LOGLEVEL_URL + "?class=")
-    self.get_and_check_status(get_loglevel_url, without_ss=True)
+    self.get_and_check_status_jvm(get_loglevel_url)
 
     # Empty input for setting a glog level request
     set_glog_url = (self.SET_GLOG_LOGLEVEL_URL + "?glog=")
-    self.get_and_check_status(set_glog_url, without_ss=False)
+    self.get_and_check_status(set_glog_url)
 
     # Try setting a non-existent log level on a valid class. In such cases,
     # log4j automatically sets it as DEBUG. This is the behavior of
     # Level.toLevel() method.
     set_loglevel_url = (self.SET_JAVA_LOGLEVEL_URL + "?class" +
         "=org.apache.impala.catalog.HdfsTable&level=foo&")
-    self.get_and_check_status(set_loglevel_url, "Effective log level: DEBUG")
+    self.get_and_check_status_jvm(set_loglevel_url, "Effective log level: DEBUG")
 
     # Try setting an invalid glog level.
     set_glog_url = self.SET_GLOG_LOGLEVEL_URL + "?glog=foo"
-    self.get_and_check_status(set_glog_url, "Bad glog level input", False)
+    self.get_and_check_status(set_glog_url, "Bad glog level input")
 
     # Try a non-existent endpoint on log_level URL.
     bad_loglevel_url = self.SET_GLOG_LOGLEVEL_URL + "?badurl=foo"
-    self.get_and_check_status(bad_loglevel_url, without_ss=False)
+    self.get_and_check_status(bad_loglevel_url)
 
   def test_catalog(self):
     """Tests the /catalog and /catalog_object endpoints."""
-    self.get_and_check_status(self.CATALOG_URL, "functional", without_ss=True)
-    self.get_and_check_status(self.CATALOG_URL, "alltypes", without_ss=True)
+    self.get_and_check_status_jvm(self.CATALOG_URL, "functional")
+    self.get_and_check_status_jvm(self.CATALOG_URL, "alltypes")
     # IMPALA-5028: Test toThrift() of a partitioned table via the WebUI code path.
     self.__test_catalog_object("functional", "alltypes")
     self.__test_catalog_object("functional_parquet", "alltypes")
@@ -149,8 +157,31 @@ class TestWebPage(ImpalaTestSuite):
     self.client.execute("invalidate metadata %s.%s" % (db_name, tbl_name))
     self.get_and_check_status(self.CATALOG_OBJECT_URL +
       "?object_type=TABLE&object_name=%s.%s" % (db_name, tbl_name), tbl_name,
-      without_ss=True)
+      ports_to_test=self.TEST_PORTS_WITHOUT_SS)
     self.client.execute("select count(*) from %s.%s" % (db_name, tbl_name))
     self.get_and_check_status(self.CATALOG_OBJECT_URL +
       "?object_type=TABLE&object_name=%s.%s" % (db_name, tbl_name), tbl_name,
-      without_ss=True)
+      ports_to_test=self.TEST_PORTS_WITHOUT_SS)
+
+  def test_query_details(self, unique_database):
+    """Test that /query_backends returns the list of backend states for DML or queries;
+    nothing for DDL statements"""
+    CROSS_JOIN = ("select count(*) from functional.alltypes a "
+                  "CROSS JOIN functional.alltypes b CROSS JOIN functional.alltypes c")
+    for q in [CROSS_JOIN,
+              "CREATE TABLE {0}.foo AS {1}".format(unique_database, CROSS_JOIN),
+              "DESCRIBE functional.alltypes"]:
+      query_handle =  self.client.execute_async(q)
+      try:
+        response = self.get_and_check_status(
+          self.QUERY_BACKENDS_URL + "?query_id=%s&json" % query_handle.get_handle().id,
+          ports_to_test=[25000])
+
+        response_json = json.loads(response)
+
+        if "DESCRIBE" not in q:
+          assert len(response_json['backend_states']) > 0
+        else:
+          assert 'backend_states' not in response_json
+      finally:
+        self.client.cancel(query_handle)

http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/ff5e9b6c/www/query_backends.tmpl
----------------------------------------------------------------------
diff --git a/www/query_backends.tmpl b/www/query_backends.tmpl
new file mode 100644
index 0000000..07d1b57
--- /dev/null
+++ b/www/query_backends.tmpl
@@ -0,0 +1,94 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+{{> www/common-header.tmpl }}
+{{> www/query_detail_tabs.tmpl }}
+<br/>
+{{?backend_states}}
+<div>
+  <label>
+    <input type="checkbox" checked="true" id="toggle" onClick="toggleRefresh()"/>
+    <span id="refresh_on">Auto-refresh on</span>
+  </label>  Last updated: <span id="last-updated"></span>
+</div>
+
+<br/>
+<table id="backends" class='table table-hover table-bordered'>
+  <thead>
+    <tr>
+      <th>Host</th>
+      <th>Num. instances</th>
+      <th>Num. remaining instances</th>
+      <th>Done</th>
+      <th>Peak mem. consumption</th>
+      <th>Time since last report (ms)</th>
+    </tr>
+  </thead>
+  <tbody>
+
+  </tbody>
+</table>
+
+<script>
+document.getElementById("backends-tab").className = "active";
+
+var intervalId = 0;
+var table = null;
+var refresh = function () {
+    table.ajax.reload();
+    document.getElementById("last-updated").textContent = new Date();
+};
+
+$(document).ready(function() {
+    table = $('#backends').DataTable({
+        ajax: { url: "/query_backends?query_id={{query_id}}&json",
+                dataSrc: "backend_states",
+              },
+        "columns": [ {data: 'host'},
+                     {data: 'num_instances'},
+                     {data: 'num_remaining_instances'},
+                     {data: 'done'},
+                     {data: 'peak_mem_consumption'},
+                     {data: 'time_since_last_heard_from'}],
+        "order": [[ 0, "desc" ]],
+        "pageLength": 100
+    });
+    intervalId = setInterval( refresh, 1000 );
+});
+
+function toggleRefresh() {
+    if (document.getElementById("toggle").checked == true) {
+        intervalId = setInterval(refresh, 1000);
+        document.getElementById("refresh_on").textContent = "Auto-refresh on";
+    } else {
+        clearInterval(intervalId);
+        document.getElementById("refresh_on").textContent = "Auto-refresh off";
+    }
+}
+
+</script>
+{{/backend_states}}
+
+{{^backend_states}}
+<div class="alert alert-info" role="alert">
+Query <strong>{{query_id}}</strong> has completed, or has no backends.
+</div>
+{{/backend_states}}
+
+{{> www/common-footer.tmpl }}

http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/ff5e9b6c/www/query_detail_tabs.tmpl
----------------------------------------------------------------------
diff --git a/www/query_detail_tabs.tmpl b/www/query_detail_tabs.tmpl
index 64781f6..0318761 100644
--- a/www/query_detail_tabs.tmpl
+++ b/www/query_detail_tabs.tmpl
@@ -27,4 +27,5 @@ under the License.
   <li id="summary-tab" role="presentation"><a href="/query_summary?query_id={{query_id}}">Summary</a></li>
   <li id="profile-tab" role="presentation"><a href="/query_profile?query_id={{query_id}}">Profile</a></li>
   <li id="memory-tab" role="presentation"><a href="/query_memory?query_id={{query_id}}">Memory</a></li>
+  <li id="backends-tab" role="presentation"><a href="/query_backends?query_id={{query_id}}">Backends</a></li>
 </ul>