www: miscellaneous mustache updates

1. Converted /dashboards and /threadz into mustache templates. In /threadz,
   I tried to defer as much work as possible outside the lock, since it must
   be taken in write mode to create a new thread. While I was there I added
   "fancy tables" to /threadz; /dashboards remains visually unchanged.
2. In /tablets, changed the handling of tablet links. In doing so, all links
   (except for the main template) are now in templates instead of in code.

Screenshots: https://imgur.com/a/AOTQuH4

Change-Id: Idb97a9e3bbefb8ee607638af6e069959c5354225
Reviewed-on: http://gerrit.cloudera.org:8080/14473
Tested-by: Kudu Jenkins
Reviewed-by: Andrew Wong <awong@cloudera.com>
Reviewed-by: Alexey Serbin <aserbin@cloudera.com>
diff --git a/src/kudu/tserver/tserver_path_handlers.cc b/src/kudu/tserver/tserver_path_handlers.cc
index 8030054..d51f27f 100644
--- a/src/kudu/tserver/tserver_path_handlers.cc
+++ b/src/kudu/tserver/tserver_path_handlers.cc
@@ -104,12 +104,6 @@
   return a.member_type() < b.member_type();
 }
 
-string TabletLink(const string& id) {
-  return Substitute("<a href=\"/tablet?id=$0\">$1</a>",
-                    UrlEncodeToString(id),
-                    EscapeForHtmlToString(id));
-}
-
 bool IsTombstoned(const scoped_refptr<TabletReplica>& replica) {
   return replica->data_state() == tablet::TABLET_DATA_TOMBSTONED;
 }
@@ -225,7 +219,7 @@
     "/log-anchors", "",
     boost::bind(&TabletServerPathHandlers::HandleLogAnchorsPage, this, _1, _2),
     true /* styled */, false /* is_on_nav_bar */);
-  server->RegisterPrerenderedPathHandler(
+  server->RegisterPathHandler(
     "/dashboards", "Dashboards",
     boost::bind(&TabletServerPathHandlers::HandleDashboardsPage, this, _1, _2),
     true /* styled */, true /* is_on_nav_bar */);
@@ -342,15 +336,15 @@
     EasyJson details_json = replicas_json->Set("replicas", EasyJson::kArray);
     for (const scoped_refptr<TabletReplica>& replica : replicas) {
       EasyJson replica_json = details_json.PushBack(EasyJson::kObject);
-      const auto* tablet = replica->tablet();
       const auto& tmeta = replica->tablet_metadata();
       TabletStatusPB status;
       replica->GetTabletStatusPB(&status);
       replica_json["table_name"] = status.table_name();
-      if (tablet != nullptr) {
-        replica_json["id_or_link"] = TabletLink(status.tablet_id());
-      } else {
-        replica_json["id_or_link"] = status.tablet_id();
+      replica_json["id"] = status.tablet_id();
+      if (replica->tablet() != nullptr) {
+        EasyJson link_json = replica_json.Set("link", EasyJson::kObject);
+        link_json["id"] = status.tablet_id();
+        link_json["url"] = Substitute("/tablet?id=$0", UrlEncodeToString(status.tablet_id()));
       }
       replica_json["partition"] =
           tmeta->partition_schema().PartitionDebugString(tmeta->partition(),
@@ -588,29 +582,7 @@
 }
 
 void TabletServerPathHandlers::HandleDashboardsPage(const Webserver::WebRequest& /*req*/,
-                                                    Webserver::PrerenderedWebResponse* resp) {
-  ostringstream* output = &resp->output;
-  *output << "<h3>Dashboards</h3>\n";
-  *output << "<table class='table table-striped'>\n";
-  *output << "  <thead><tr><th>Dashboard</th><th>Description</th></tr></thead>\n";
-  *output << "  <tbody\n";
-  *output << GetDashboardLine("scans", "Scans", "List of currently running and recently "
-                                                "completed scans.");
-  *output << GetDashboardLine("transactions", "Transactions", "List of transactions that are "
-                                                              "currently running.");
-  *output << GetDashboardLine("maintenance-manager", "Maintenance Manager",
-                              "List of operations that are currently running and those "
-                              "that are registered.");
-  *output << "</tbody></table>\n";
-}
-
-string TabletServerPathHandlers::GetDashboardLine(const std::string& link,
-                                                  const std::string& text,
-                                                  const std::string& desc) {
-  return Substitute("  <tr><td><a href=\"$0\">$1</a></td><td>$2</td></tr>\n",
-                    EscapeForHtmlToString(link),
-                    EscapeForHtmlToString(text),
-                    EscapeForHtmlToString(desc));
+                                                    Webserver::WebResponse* /*resp*/) {
 }
 
 void TabletServerPathHandlers::HandleMaintenanceManagerPage(const Webserver::WebRequest& req,
diff --git a/src/kudu/tserver/tserver_path_handlers.h b/src/kudu/tserver/tserver_path_handlers.h
index 7b7beed..7cf370b 100644
--- a/src/kudu/tserver/tserver_path_handlers.h
+++ b/src/kudu/tserver/tserver_path_handlers.h
@@ -17,8 +17,6 @@
 #ifndef KUDU_TSERVER_TSERVER_PATH_HANDLERS_H
 #define KUDU_TSERVER_TSERVER_PATH_HANDLERS_H
 
-#include <string>
-
 #include "kudu/gutil/macros.h"
 #include "kudu/server/webserver.h"
 #include "kudu/util/status.h"
@@ -54,11 +52,9 @@
   void HandleConsensusStatusPage(const Webserver::WebRequest& req,
                                  Webserver::WebResponse* resp);
   void HandleDashboardsPage(const Webserver::WebRequest& req,
-                            Webserver::PrerenderedWebResponse* resp);
+                            Webserver::WebResponse* resp);
   void HandleMaintenanceManagerPage(const Webserver::WebRequest& req,
                                     Webserver::WebResponse* resp);
-  std::string GetDashboardLine(const std::string& link,
-                               const std::string& text, const std::string& desc);
 
   TabletServer* tserver_;
 
diff --git a/src/kudu/util/thread.cc b/src/kudu/util/thread.cc
index 1c16763..9f5eb01 100644
--- a/src/kudu/util/thread.cc
+++ b/src/kudu/util/thread.cc
@@ -28,7 +28,6 @@
 #include <algorithm>
 #include <cerrno>
 #include <cstring>
-#include <map>
 #include <memory>
 #include <mutex>
 #include <sstream>
@@ -50,6 +49,7 @@
 #include "kudu/gutil/once.h"
 #include "kudu/gutil/port.h"
 #include "kudu/gutil/strings/substitute.h"
+#include "kudu/util/easy_json.h"
 #include "kudu/util/env.h"
 #include "kudu/util/flag_tags.h"
 #include "kudu/util/kernel_stack_watchdog.h"
@@ -66,8 +66,8 @@
 
 using boost::bind;
 using boost::mem_fn;
-using std::map;
 using std::ostringstream;
+using std::pair;
 using std::shared_ptr;
 using std::string;
 using std::vector;
@@ -200,6 +200,12 @@
     const string& category() const { return category_; }
     int64_t thread_id() const { return thread_id_; }
 
+    struct Comparator {
+      bool operator()(const ThreadDescriptor& rhs, const ThreadDescriptor& lhs) const {
+        return rhs.name() < lhs.name();
+      }
+    };
+
    private:
     string name_;
     string category_;
@@ -241,9 +247,9 @@
 
   // Webpage callback; prints all threads by category.
   void ThreadPathHandler(const WebCallbackRegistry::WebRequest& req,
-                         WebCallbackRegistry::PrerenderedWebResponse* resp) const;
-  void PrintThreadDescriptorRow(const ThreadDescriptor& desc,
-                                ostringstream* output) const;
+                         WebCallbackRegistry::WebResponse* resp) const;
+  void SummarizeThreadDescriptor(const ThreadDescriptor& desc,
+                                 EasyJson* output) const;
 };
 
 void ThreadMgr::SetThreadName(const string& name, int64_t tid) {
@@ -295,11 +301,11 @@
         Bind(&GetInVoluntaryContextSwitches)));
 
   if (web) {
-    WebCallbackRegistry::PrerenderedPathHandlerCallback thread_callback =
-        bind<void>(mem_fn(&ThreadMgr::ThreadPathHandler), this, _1, _2);
-    DCHECK_NOTNULL(web)->RegisterPrerenderedPathHandler("/threadz", "Threads", thread_callback,
-                                                        true /* is_styled*/,
-                                                        true /* is_on_nav_bar */);
+    auto thread_callback = bind<void>(mem_fn(&ThreadMgr::ThreadPathHandler),
+                                      this, _1, _2);
+    DCHECK_NOTNULL(web)->RegisterPathHandler("/threadz", "Threads", thread_callback,
+                                             /* is_styled= */ true,
+                                             /* is_on_nav_bar= */ true);
   }
   return Status::OK();
 }
@@ -367,84 +373,81 @@
   ANNOTATE_IGNORE_READS_AND_WRITES_END();
 }
 
-void ThreadMgr::PrintThreadDescriptorRow(const ThreadDescriptor& desc,
-                                         ostringstream* output) const {
+void ThreadMgr::SummarizeThreadDescriptor(const ThreadDescriptor& desc,
+                                          EasyJson* output) const {
   ThreadStats stats;
   Status status = GetThreadStats(desc.thread_id(), &stats);
   if (!status.ok()) {
     KLOG_EVERY_N(INFO, 100) << "Could not get per-thread statistics: "
                             << status.ToString();
   }
-  (*output) << "<tr><td>" << desc.name() << "</td><td>"
-            << (static_cast<double>(stats.user_ns) / 1e9) << "</td><td>"
-            << (static_cast<double>(stats.kernel_ns) / 1e9) << "</td><td>"
-            << (static_cast<double>(stats.iowait_ns) / 1e9) << "</td></tr>";
+  EasyJson thr = output->PushBack(EasyJson::kObject);
+  thr["thread_name"] = desc.name();
+  thr["user_sec"] = static_cast<double>(stats.user_ns) / 1e9;
+  thr["kernel_sec"] = static_cast<double>(stats.kernel_ns) / 1e9;
+  thr["iowait_sec"] = static_cast<double>(stats.iowait_ns) / 1e9;
 }
 
-void ThreadMgr::ThreadPathHandler(
-    const WebCallbackRegistry::WebRequest& req,
-    WebCallbackRegistry::PrerenderedWebResponse* resp) const {
-  ostringstream& output = resp->output;
-  vector<ThreadDescriptor> descriptors_to_print;
-  const auto category_name = req.parsed_args.find("group");
-  if (category_name != req.parsed_args.end()) {
-    const auto& group = category_name->second;
-    const auto& group_esc = EscapeForHtmlToString(group);
-    output << "<h2>Thread Group: " << group_esc << "</h2>";
-    if (group != "all") {
+void ThreadMgr::ThreadPathHandler(const WebCallbackRegistry::WebRequest& req,
+                                  WebCallbackRegistry::WebResponse* resp) const {
+  EasyJson& output = resp->output;
+  const auto* category_name = FindOrNull(req.parsed_args, "group");
+  if (category_name) {
+    // List all threads belonging to the desired thread group.
+    bool requested_all = *category_name == "all";
+    EasyJson rtg = output.Set("requested_thread_group", EasyJson::kObject);
+    rtg["group_name"] = EscapeForHtmlToString(*category_name);
+    rtg["requested_all"] = requested_all;
+
+    // The critical section is as short as possible so as to minimize the delay
+    // imposed on new threads that acquire the lock in write mode.
+    vector<ThreadDescriptor> descriptors_to_print;
+    if (!requested_all) {
       shared_lock<decltype(lock_)> l(lock_);
-      const auto it = thread_categories_.find(group);
-      if (it == thread_categories_.end()) {
-        output << "Thread group '" << group_esc << "' not found";
+      const auto* category = FindOrNull(thread_categories_, *category_name);
+      if (!category) {
         return;
       }
-      for (const auto& elem : it->second) {
-        descriptors_to_print.push_back(elem.second);
+      for (const auto& elem : *category) {
+        descriptors_to_print.emplace_back(elem.second);
       }
-      output << "<h3>" << it->first << " : " << it->second.size() << "</h3>";
     } else {
       shared_lock<decltype(lock_)> l(lock_);
       for (const auto& category : thread_categories_) {
         for (const auto& elem : category.second) {
-          descriptors_to_print.push_back(elem.second);
+          descriptors_to_print.emplace_back(elem.second);
         }
       }
-      output << "<h3>All Threads : </h3>";
     }
-    output << "<table class='table table-hover table-border'>"
-              "<thead><tr><th>Thread name</th><th>Cumulative User CPU(s)</th>"
-              "<th>Cumulative Kernel CPU(s)</th>"
-              "<th>Cumulative IO-wait(s)</th></tr></thead>"
-              "<tbody>\n";
-    // Sort the entries in the table by the name of a thread.
-    // TODO(aserbin): use "mustache + fancy table" instead.
-    std::sort(descriptors_to_print.begin(), descriptors_to_print.end(),
-              [](const ThreadDescriptor& lhs, const ThreadDescriptor& rhs) {
-                return lhs.name() < rhs.name();
-              });
+
+    EasyJson found = rtg.Set("found", EasyJson::kObject);
+    EasyJson threads = found.Set("threads", EasyJson::kArray);
     for (const auto& desc : descriptors_to_print) {
-      PrintThreadDescriptorRow(desc, &output);
+      SummarizeThreadDescriptor(desc, &threads);
     }
-    output << "</tbody></table>";
   } else {
-    // Using the tree map (std::map) to have the list of the thread categories
-    // at the '/threadz' page sorted alphabetically.
-    // TODO(aserbin): use "mustache + fancy table" instead.
-    map<string, size_t> thread_categories_info;
+    // List all thread groups and the number of threads running in each.
+    vector<pair<string, size_t>> thread_categories_info;
+    uint64_t running;
     {
+      // See comment above regarding short critical sections.
       shared_lock<decltype(lock_)> l(lock_);
-      output << "<h2>Thread Groups</h2>"
-                "<h4>" << threads_running_metric_ << " thread(s) running"
-                "<a href='/threadz?group=all'><h3>All Threads</h3>";
+      running = threads_running_metric_;
+      thread_categories_info.reserve(thread_categories_.size());
       for (const auto& category : thread_categories_) {
-        thread_categories_info.emplace(category.first, category.second.size());
+        thread_categories_info.emplace_back(category.first, category.second.size());
       }
     }
+
+    output["total_threads_running"] = running;
+    EasyJson groups = output.Set("groups", EasyJson::kArray);
     for (const auto& elem : thread_categories_info) {
       string category_arg;
       UrlEncode(elem.first, &category_arg);
-      output << "<a href='/threadz?group=" << category_arg << "'><h3>"
-             << elem.first << " : " << elem.second << "</h3></a>";
+      EasyJson g = groups.PushBack(EasyJson::kObject);
+      g["encoded_group_name"] = category_arg;
+      g["group_name"] = elem.first;
+      g["threads_running"] = elem.second;
     }
   }
 }
diff --git a/www/dashboards.mustache b/www/dashboards.mustache
new file mode 100644
index 0000000..d86ae27
--- /dev/null
+++ b/www/dashboards.mustache
@@ -0,0 +1,27 @@
+{{!
+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.
+}}
+
+<h3>Dashboards</h3>
+<table class='table table-striped'>
+  <thead><tr><th>Dashboard</th><th>Description</th></tr></thead>
+  <tbody
+  <tr><td><a href="/scans">Scans</a></td><td>List of currently running and recently completed scans.</td></tr>
+  <tr><td><a href="/transactions">Transactions</a></td><td>List of transactions that are currently running.</td></tr>
+  <tr><td><a href="/maintenance-manager">Maintenance Manager</a></td><td>List of operations that are currently running and those that are registered.</td></tr>
+</tbody></table>
diff --git a/www/kudu.js b/www/kudu.js
index 2e7df97..9f02320 100644
--- a/www/kudu.js
+++ b/www/kudu.js
@@ -67,8 +67,21 @@
   return 0;
 }
 
+// A comparison function for floating point numbers.
+function floatsSorter(left, right) {
+  left_float = parseFloat(left)
+  right_float = parseFloat(right)
+  if (left_float < right_float) {
+    return -1;
+  }
+  if (left_float > right_float) {
+    return 1;
+  }
+  return 0;
+}
+
 // Converts numeric strings to numbers and then compares them.
-function compareNumericStrings(left, right) {
+function numericStringsSorter(left, right) {
   left_num = parseInt(left, 10);
   right_num = parseInt(right, 10);
   if (left_num < right_num) {
@@ -103,32 +116,32 @@
   }
 
   // Year.
-  var ret = compareNumericStrings(left.substr(0, 4), right.substr(0, 4));
+  var ret = numericStringsSorter(left.substr(0, 4), right.substr(0, 4));
   if (ret != 0) {
     return ret;
   }
   // Month.
-  ret = compareNumericStrings(left.substr(5, 2), right.substr(5, 2));
+  ret = numericStringsSorter(left.substr(5, 2), right.substr(5, 2));
   if (ret != 0) {
     return ret;
   }
   // Day.
-  ret = compareNumericStrings(left.substr(8, 2), right.substr(8, 2));
+  ret = numericStringsSorter(left.substr(8, 2), right.substr(8, 2));
   if (ret != 0) {
     return ret;
   }
   // Hour.
-  ret = compareNumericStrings(left.substr(11, 2), right.substr(11, 2));
+  ret = numericStringsSorter(left.substr(11, 2), right.substr(11, 2));
   if (ret != 0) {
     return ret;
   }
   // Minute.
-  ret = compareNumericStrings(left.substr(14, 2), right.substr(14, 2));
+  ret = numericStringsSorter(left.substr(14, 2), right.substr(14, 2));
   if (ret != 0) {
     return ret;
   }
   // Second.
-  ret = compareNumericStrings(left.substr(17, 2), right.substr(17, 2));
+  ret = numericStringsSorter(left.substr(17, 2), right.substr(17, 2));
   if (ret != 0) {
     return ret;
   }
diff --git a/www/tablets.mustache b/www/tablets.mustache
index d7eee69..2a54ada 100644
--- a/www/tablets.mustache
+++ b/www/tablets.mustache
@@ -47,7 +47,10 @@
     {{#replicas}}
       <tr>
         <td>{{table_name}}</td>
-        <td>{{{id_or_link}}}</td>
+        <td>
+          {{#link}}<a href="{{url}}">{{id}}</a>{{/link}}
+          {{^link}}{{id}}{{/link}}
+        </td>
         <td>{{partition}}</td>
         <td>{{state}}</td>
         <td>{{n_bytes}}</td>
@@ -90,7 +93,10 @@
     {{#replicas}}
       <tr>
         <td>{{table_name}}</td>
-        <td>{{{id_or_link}}}</td>
+        <td>
+          {{#link}}<a href="{{url}}">{{id}}</a>{{/link}}
+          {{^link}}{{id}}{{/link}}
+        </td>
         <td>{{partition}}</td>
         <td>{{state}}</td>
         <td>{{n_bytes}}</td>
diff --git a/www/threadz.mustache b/www/threadz.mustache
new file mode 100644
index 0000000..efabdd7
--- /dev/null
+++ b/www/threadz.mustache
@@ -0,0 +1,68 @@
+{{!
+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.
+}}
+
+{{#requested_thread_group}}
+<h2>Thread Group: {{group_name}}</h2>
+{{#requested_all}}<h3>All Threads : </h3>{{/requested_all}}
+{{#found}}
+<table class='table table-hover' data-sort-name='name' data-toggle='table'>
+  <thead>
+    <tr>
+      <th data-field='name' data-sortable='true' data-sorter='stringsSorter'>Thread name</th>
+      <th data-sortable='true' data-sorter='floatsSorter'>Cumulative User CPU (s)</th>
+      <th data-sortable='true' data-sorter='floatsSorter'>Cumulative Kernel CPU (s)</th>
+      <th data-sortable='true' data-sorter='floatsSorter'>Cumulative IO-wait (s)</th>
+    </tr>
+  </thead>
+  <tbody>
+    {{#threads}}
+    <tr>
+      <td>{{thread_name}}</td>
+      <td>{{user_sec}}</td>
+      <td>{{kernel_sec}}</td>
+      <td>{{iowait_sec}}</td>
+    </tr>
+    {{/threads}}
+  </tbody>
+</table>
+{{/found}}
+{{^found}}Thread group {{group_name}} not found{{/found}}
+{{/requested_thread_group}}
+
+{{^requested_thread_group}}
+<h2>Thread Groups</h2>
+<h4>{{total_threads_running}} thread(s) running</h4>
+<a href='/threadz?group=all'><h3>All Threads</h3></a>
+<table class='table table-hover' data-sort-name='group' data-toggle='table'>
+  <thead>
+    <tr>
+      <th data-field='group' data-sortable='true' data-sorter='stringsSorter'>Group</th>
+      <th data-sortable='true' data-sorter='numericStringsSorter'>Threads running</th>
+    </tr>
+  </thead>
+  <tbody>
+    {{#groups}}
+    <tr>
+      <td><a href='/threadz?group={{encoded_group_name}}'>{{group_name}}</a></td>
+      <td>{{threads_running}}</td>
+    </tr>
+    {{/groups}}
+  </tbody>
+</table>
+{{/requested_thread_group}}