You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by ap...@apache.org on 2019/10/02 00:02:56 UTC

[hbase] 02/02: HBASE-22988 Backport HBASE-11062 "hbtop" to branch-1

This is an automated email from the ASF dual-hosted git repository.

apurtell pushed a commit to branch branch-1
in repository https://gitbox.apache.org/repos/asf/hbase.git

commit dd9eadb00f9dcd071a246482a11dfc7d63845f00
Author: Toshihiro Suzuki <br...@gmail.com>
AuthorDate: Fri Sep 20 15:42:15 2019 +0900

    HBASE-22988 Backport HBASE-11062 "hbtop" to branch-1
    
    Fixes #647
    
    Signed-off-by: Andrew Purtell <ap...@apache.org>
---
 bin/hbase                                          |   7 +
 conf/log4j-hbtop.properties                        |  27 ++
 hbase-assembly/pom.xml                             |   5 +
 hbase-hbtop/pom.xml                                | 241 ++++++++++
 .../java/org/apache/hadoop/hbase/hbtop/HBTop.java  | 140 ++++++
 .../java/org/apache/hadoop/hbase/hbtop/Record.java | 185 ++++++++
 .../apache/hadoop/hbase/hbtop/RecordFilter.java    | 339 ++++++++++++++
 .../org/apache/hadoop/hbase/hbtop/field/Field.java |  98 ++++
 .../apache/hadoop/hbase/hbtop/field/FieldInfo.java |  55 +++
 .../hadoop/hbase/hbtop/field/FieldValue.java       | 283 ++++++++++++
 .../hadoop/hbase/hbtop/field/FieldValueType.java   |  28 ++
 .../org/apache/hadoop/hbase/hbtop/field/Size.java  | 157 +++++++
 .../hadoop/hbase/hbtop/mode/DrillDownInfo.java     |  50 +++
 .../org/apache/hadoop/hbase/hbtop/mode/Mode.java   |  74 +++
 .../hadoop/hbase/hbtop/mode/ModeStrategy.java      |  38 ++
 .../hbase/hbtop/mode/NamespaceModeStrategy.java    | 105 +++++
 .../hbase/hbtop/mode/RegionModeStrategy.java       | 182 ++++++++
 .../hbase/hbtop/mode/RegionServerModeStrategy.java | 124 +++++
 .../hbase/hbtop/mode/RequestCountPerSecond.java    |  63 +++
 .../hadoop/hbase/hbtop/mode/TableModeStrategy.java | 108 +++++
 .../hbase/hbtop/screen/AbstractScreenView.java     | 102 +++++
 .../apache/hadoop/hbase/hbtop/screen/Screen.java   | 132 ++++++
 .../hadoop/hbase/hbtop/screen/ScreenView.java      |  33 ++
 .../hbtop/screen/field/FieldScreenPresenter.java   | 184 ++++++++
 .../hbase/hbtop/screen/field/FieldScreenView.java  | 193 ++++++++
 .../hbtop/screen/help/CommandDescription.java      |  52 +++
 .../hbtop/screen/help/HelpScreenPresenter.java     |  72 +++
 .../hbase/hbtop/screen/help/HelpScreenView.java    |  89 ++++
 .../hbtop/screen/mode/ModeScreenPresenter.java     | 134 ++++++
 .../hbase/hbtop/screen/mode/ModeScreenView.java    | 136 ++++++
 .../top/FilterDisplayModeScreenPresenter.java      |  53 +++
 .../screen/top/FilterDisplayModeScreenView.java    |  77 ++++
 .../hadoop/hbase/hbtop/screen/top/Header.java      |  48 ++
 .../hbtop/screen/top/InputModeScreenPresenter.java | 168 +++++++
 .../hbtop/screen/top/InputModeScreenView.java      | 105 +++++
 .../screen/top/MessageModeScreenPresenter.java     |  51 +++
 .../hbtop/screen/top/MessageModeScreenView.java    |  65 +++
 .../hadoop/hbase/hbtop/screen/top/Paging.java      | 151 +++++++
 .../hadoop/hbase/hbtop/screen/top/Summary.java     |  93 ++++
 .../hbase/hbtop/screen/top/TopScreenModel.java     | 235 ++++++++++
 .../hbase/hbtop/screen/top/TopScreenPresenter.java | 356 +++++++++++++++
 .../hbase/hbtop/screen/top/TopScreenView.java      | 308 +++++++++++++
 .../hbtop/terminal/AbstractTerminalPrinter.java    |  69 +++
 .../hadoop/hbase/hbtop/terminal/Attributes.java    | 128 ++++++
 .../apache/hadoop/hbase/hbtop/terminal/Color.java  |  28 ++
 .../hbase/hbtop/terminal/CursorPosition.java       |  61 +++
 .../hadoop/hbase/hbtop/terminal/KeyPress.java      | 128 ++++++
 .../hadoop/hbase/hbtop/terminal/Terminal.java      |  39 ++
 .../hbase/hbtop/terminal/TerminalPrinter.java      |  54 +++
 .../hadoop/hbase/hbtop/terminal/TerminalSize.java  |  61 +++
 .../hadoop/hbase/hbtop/terminal/impl/Cell.java     | 122 +++++
 .../hbase/hbtop/terminal/impl/EscapeSequences.java | 140 ++++++
 .../hbtop/terminal/impl/KeyPressGenerator.java     | 500 +++++++++++++++++++++
 .../hbase/hbtop/terminal/impl/ScreenBuffer.java    | 170 +++++++
 .../hbase/hbtop/terminal/impl/TerminalImpl.java    | 229 ++++++++++
 .../hbtop/terminal/impl/TerminalPrinterImpl.java   |  83 ++++
 .../org/apache/hadoop/hbase/hbtop/TestRecord.java  |  87 ++++
 .../hadoop/hbase/hbtop/TestRecordFilter.java       | 209 +++++++++
 .../org/apache/hadoop/hbase/hbtop/TestUtils.java   | 373 +++++++++++++++
 .../hadoop/hbase/hbtop/field/TestFieldValue.java   | 290 ++++++++++++
 .../apache/hadoop/hbase/hbtop/field/TestSize.java  |  80 ++++
 .../hadoop/hbase/hbtop/mode/TestModeBase.java      |  46 ++
 .../hadoop/hbase/hbtop/mode/TestNamespaceMode.java |  63 +++
 .../hadoop/hbase/hbtop/mode/TestRegionMode.java    |  47 ++
 .../hbase/hbtop/mode/TestRegionServerMode.java     |  62 +++
 .../hbtop/mode/TestRequestCountPerSecond.java      |  49 ++
 .../hadoop/hbase/hbtop/mode/TestTableMode.java     |  73 +++
 .../screen/field/TestFieldScreenPresenter.java     | 151 +++++++
 .../hbtop/screen/help/TestHelpScreenPresenter.java |  75 ++++
 .../hbtop/screen/mode/TestModeScreenPresenter.java | 140 ++++++
 .../top/TestFilterDisplayModeScreenPresenter.java  |  91 ++++
 .../screen/top/TestInputModeScreenPresenter.java   | 198 ++++++++
 .../screen/top/TestMessageModeScreenPresenter.java |  65 +++
 .../hadoop/hbase/hbtop/screen/top/TestPaging.java  | 293 ++++++++++++
 .../hbase/hbtop/screen/top/TestTopScreenModel.java | 200 +++++++++
 .../hbtop/screen/top/TestTopScreenPresenter.java   | 291 ++++++++++++
 .../hbase/hbtop/terminal/impl/TestCursor.java      |  77 ++++
 .../hbase/hbtop/terminal/impl/TestKeyPress.java    |  52 +++
 .../hbtop/terminal/impl/TestTerminalPrinter.java   |  58 +++
 pom.xml                                            |   7 +
 80 files changed, 10035 insertions(+)

diff --git a/bin/hbase b/bin/hbase
index f82acb6..fa29abc 100755
--- a/bin/hbase
+++ b/bin/hbase
@@ -105,6 +105,7 @@ if [ $# = 0 ]; then
   echo "  pe               Run PerformanceEvaluation"
   echo "  ltt              Run LoadTestTool"
   echo "  canary           Run the Canary tool"
+  echo "  hbtop            Run the HBTop tool"
   echo "  version          Print the version"
   echo "  CLASSNAME        Run the class named CLASSNAME"
   exit 1
@@ -402,6 +403,12 @@ elif [ "$COMMAND" = "ltt" ] ; then
 elif [ "$COMMAND" = "canary" ] ; then
   CLASS='org.apache.hadoop.hbase.tool.Canary'
   HBASE_OPTS="$HBASE_OPTS $HBASE_CANARY_OPTS"
+elif [ "$COMMAND" = "hbtop" ] ; then
+  CLASS='org.apache.hadoop.hbase.hbtop.HBTop'
+  if [ -f "${HBASE_HOME}/conf/log4j-hbtop.properties" ] ; then
+    HBASE_HBTOP_OPTS="${HBASE_HBTOP_OPTS} -Dlog4j.configuration=file:${HBASE_HOME}/conf/log4j-hbtop.properties"
+  fi
+  HBASE_OPTS="${HBASE_OPTS} ${HBASE_HBTOP_OPTS}"
 elif [ "$COMMAND" = "version" ] ; then
   CLASS='org.apache.hadoop.hbase.util.VersionInfo'
 elif [ "$COMMAND" = "completebulkload" ] ; then
diff --git a/conf/log4j-hbtop.properties b/conf/log4j-hbtop.properties
new file mode 100644
index 0000000..4d68d79
--- /dev/null
+++ b/conf/log4j-hbtop.properties
@@ -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.
+
+log4j.rootLogger=WARN,console
+log4j.threshold=WARN
+
+# console
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.target=System.err
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c{2}: %m%n
+
+# ZooKeeper will still put stuff at WARN
+log4j.logger.org.apache.zookeeper=ERROR
diff --git a/hbase-assembly/pom.xml b/hbase-assembly/pom.xml
index e808c20..8a1b323 100644
--- a/hbase-assembly/pom.xml
+++ b/hbase-assembly/pom.xml
@@ -227,5 +227,10 @@
       <groupId>org.codehaus.jackson</groupId>
       <artifactId>jackson-mapper-asl</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-hbtop</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 </project>
diff --git a/hbase-hbtop/pom.xml b/hbase-hbtop/pom.xml
new file mode 100644
index 0000000..00d382e
--- /dev/null
+++ b/hbase-hbtop/pom.xml
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <!--
+  /**
+   * 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.
+   */
+  -->
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <artifactId>hbase</artifactId>
+    <groupId>org.apache.hbase</groupId>
+    <version>1.5.0-SNAPSHOT</version>
+    <relativePath>..</relativePath>
+  </parent>
+  <artifactId>hbase-hbtop</artifactId>
+  <name>Apache HBase - HBTop</name>
+  <description>A real-time monitoring tool for HBase like Unix's top command</description>
+
+  <build>
+    <plugins>
+      <!-- Make a jar and put the sources in the jar -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-common</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-cli</groupId>
+      <artifactId>commons-cli</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-annotations</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-annotations</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-common</artifactId>
+      <type>jar</type>
+      <exclusions>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.jaxrs</groupId>
+          <artifactId>jackson-jaxrs-json-provider</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-annotations</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-core</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-databind</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-protocol</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hbase</groupId>
+      <artifactId>hbase-client</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.jaxrs</groupId>
+          <artifactId>jackson-jaxrs-json-provider</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-annotations</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-core</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.core</groupId>
+          <artifactId>jackson-databind</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+  </dependencies>
+
+  <profiles>
+    <!-- Needs to make the profile in apache parent pom -->
+    <profile>
+      <id>apache-release</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-resources-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>license-javadocs</id>
+                <phase>prepare-package</phase>
+                <goals>
+                  <goal>copy-resources</goal>
+                </goals>
+                <configuration>
+                  <outputDirectory>${project.build.directory}/apidocs</outputDirectory>
+                  <resources>
+                    <resource>
+                      <directory>src/main/javadoc/META-INF/</directory>
+                      <targetPath>META-INF/</targetPath>
+                      <includes>
+                        <include>NOTICE</include>
+                      </includes>
+                      <filtering>true</filtering>
+                    </resource>
+                  </resources>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <!-- Skip the tests in this module -->
+    <profile>
+      <id>skipCommonTests</id>
+      <activation>
+        <property>
+          <name>skipCommonTests</name>
+        </property>
+      </activation>
+      <properties>
+        <surefire.skipFirstPart>true</surefire.skipFirstPart>
+      </properties>
+    </profile>
+
+    <profile>
+      <id>errorProne</id>
+      <activation>
+        <activeByDefault>false</activeByDefault>
+      </activation>
+      <build>
+        <plugins>
+          <!-- Turn on error-prone -->
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <version>${maven.compiler.version}</version>
+            <configuration>
+              <compilerId>javac-with-errorprone</compilerId>
+              <forceJavacCompilerUse>true</forceJavacCompilerUse>
+              <showWarnings>true</showWarnings>
+              <compilerArgs>
+                <arg>-XepDisableWarningsInGeneratedCode</arg>
+                <arg>-Xep:FallThrough:OFF</arg> <!-- already in findbugs -->
+              </compilerArgs>
+              <annotationProcessorPaths>
+                <path>
+                  <groupId>org.apache.hbase</groupId>
+                  <artifactId>hbase-error-prone</artifactId>
+                  <version>${project.version}</version>
+                </path>
+              </annotationProcessorPaths>
+            </configuration>
+            <dependencies>
+              <dependency>
+                <groupId>org.apache.hbase</groupId>
+                <artifactId>hbase-error-prone</artifactId>
+                <version>${project.version}</version>
+              </dependency>
+              <dependency>
+                <groupId>org.codehaus.plexus</groupId>
+                <artifactId>plexus-compiler-javac-errorprone</artifactId>
+                <version>${plexus.errorprone.javac.version}</version>
+              </dependency>
+              <!-- override plexus-compiler-javac-errorprone's dependency on
+                Error Prone with the latest version -->
+              <dependency>
+                <groupId>com.google.errorprone</groupId>
+                <artifactId>error_prone_core</artifactId>
+                <version>${error-prone.version}</version>
+              </dependency>
+              <dependency>
+                <groupId>org.apache.hbase</groupId>
+                <artifactId>hbase-error-prone</artifactId>
+                <version>${project.version}</version>
+              </dependency>
+            </dependencies>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/HBTop.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/HBTop.java
new file mode 100644
index 0000000..ac05bb2
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/HBTop.java
@@ -0,0 +1,140 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop;
+
+import java.util.Objects;
+
+import org.apache.commons.cli.BasicParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Options;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.conf.Configured;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.HBaseInterfaceAudience;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.util.Tool;
+import org.apache.hadoop.util.ToolRunner;
+
+/**
+ * A real-time monitoring tool for HBase like Unix top command.
+ */
+@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.TOOLS)
+public class HBTop extends Configured implements Tool {
+
+  private static final Log LOG = LogFactory.getLog(HBTop.class);
+
+  public HBTop() {
+    this(HBaseConfiguration.create());
+  }
+
+  public HBTop(Configuration conf) {
+    super(Objects.requireNonNull(conf));
+  }
+
+  @Override
+  public int run(String[] args) throws Exception {
+    long initialRefreshDelay = 3 * 1000;
+    Mode initialMode = Mode.REGION;
+    try {
+      // Command line options
+      Options opts = new Options();
+      opts.addOption("h", "help", false,
+        "Print usage; for help while the tool is running press 'h'");
+      opts.addOption("d", "delay", true,
+        "The refresh delay (in seconds); default is 3 seconds");
+      opts.addOption("m", "mode", true,
+        "The mode; n (Namespace)|t (Table)|r (Region)|s (RegionServer)"
+          + ", default is r (Region)");
+
+      CommandLine commandLine = new BasicParser().parse(opts, args);
+
+      if (commandLine.hasOption("help")) {
+        printUsage(opts);
+        return 0;
+      }
+
+      if (commandLine.hasOption("delay")) {
+        int delay = 0;
+        try {
+          delay = Integer.parseInt(commandLine.getOptionValue("delay"));
+        } catch (NumberFormatException ignored) {
+        }
+
+        if (delay < 1) {
+          LOG.warn("Delay set too low or invalid, using default");
+        } else {
+          initialRefreshDelay = delay * 1000L;
+        }
+      }
+
+      if (commandLine.hasOption("mode")) {
+        String mode = commandLine.getOptionValue("mode");
+        switch (mode) {
+          case "n":
+            initialMode = Mode.NAMESPACE;
+            break;
+
+          case "t":
+            initialMode = Mode.TABLE;
+            break;
+
+          case "r":
+            initialMode = Mode.REGION;
+            break;
+
+          case "s":
+            initialMode = Mode.REGION_SERVER;
+            break;
+
+          default:
+            LOG.warn("Mode set invalid, using default");
+            break;
+        }
+      }
+    } catch (Exception e) {
+      LOG.error("Unable to parse options", e);
+      return 1;
+    }
+
+    try (Screen screen = new Screen(getConf(), initialRefreshDelay, initialMode)) {
+      screen.run();
+    }
+
+    return 0;
+  }
+
+  private void printUsage(Options opts) {
+    new HelpFormatter().printHelp("hbase hbtop [opts] [-D<property=value>]*", opts);
+    System.out.println("");
+    System.out.println(" Note: -D properties will be applied to the conf used.");
+    System.out.println("  For example:");
+    System.out.println("   -Dhbase.client.zookeeper.quorum=<zookeeper quorum>");
+    System.out.println("   -Dzookeeper.znode.parent=<znode parent>");
+    System.out.println("");
+  }
+
+  public static void main(String[] args) throws Exception {
+    int res = ToolRunner.run(new HBTop(), args);
+    System.exit(res);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/Record.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/Record.java
new file mode 100644
index 0000000..8409283
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/Record.java
@@ -0,0 +1,185 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop;
+
+import com.google.common.collect.ImmutableMap;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldValue;
+import org.apache.hadoop.hbase.hbtop.field.FieldValueType;
+
+
+/**
+ * Represents a record of the metrics in the top screen.
+ */
+@InterfaceAudience.Private
+public final class Record implements Map<Field, FieldValue> {
+
+  private final ImmutableMap<Field, FieldValue> values;
+
+  public final static class Entry extends AbstractMap.SimpleImmutableEntry<Field, FieldValue> {
+    private Entry(Field key, FieldValue value) {
+      super(key, value);
+    }
+  }
+
+  public final static class Builder {
+
+    private final ImmutableMap.Builder<Field, FieldValue> builder;
+
+    private Builder() {
+      builder = ImmutableMap.builder();
+    }
+
+    public Builder put(Field key, Object value) {
+      builder.put(key, key.newValue(value));
+      return this;
+    }
+
+    public Builder put(Field key, FieldValue value) {
+      builder.put(key, value);
+      return this;
+    }
+
+    public Builder put(Entry entry) {
+      builder.put(entry);
+      return this;
+    }
+
+    public Builder putAll(Map<Field, FieldValue> map) {
+      builder.putAll(map);
+      return this;
+    }
+
+    public Record build() {
+      return new Record(builder.build());
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static Entry entry(Field field, Object value) {
+    return new Entry(field, field.newValue(value));
+  }
+
+  public static Entry entry(Field field, FieldValue value) {
+    return new Entry(field, value);
+  }
+
+  public static Record ofEntries(Entry... entries) {
+    Builder builder = builder();
+    for (Entry entry : entries) {
+      builder.put(entry.getKey(), entry.getValue());
+    }
+    return builder.build();
+  }
+
+  public static Record ofEntries(Iterable<Entry> entries) {
+    Builder builder = builder();
+    for (Entry entry : entries) {
+      builder.put(entry.getKey(), entry.getValue());
+    }
+    return builder.build();
+  }
+
+  private Record(ImmutableMap<Field, FieldValue> values) {
+    this.values = values;
+  }
+
+  @Override
+  public int size() {
+    return values.size();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return values.isEmpty();
+  }
+
+  @Override
+  public boolean containsKey(Object key) {
+    return values.containsKey(key);
+  }
+
+  @Override
+  public boolean containsValue(Object value) {
+    return values.containsValue(value);
+  }
+
+  @Override
+  public FieldValue get(Object key) {
+    return values.get(key);
+  }
+
+  @Override
+  public FieldValue put(Field key, FieldValue value) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public FieldValue remove(Object key) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void putAll(@NonNull Map<? extends Field, ? extends FieldValue> m) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void clear() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  @NonNull
+  public Set<Field> keySet() {
+    return values.keySet();
+  }
+
+  @Override
+  @NonNull
+  public Collection<FieldValue> values() {
+    return values.values();
+  }
+
+  @Override
+  @NonNull
+  public Set<Map.Entry<Field, FieldValue>> entrySet() {
+    return values.entrySet();
+  }
+
+  public Record combine(Record o) {
+    Builder builder = builder();
+    for (Field k : values.keySet()) {
+      if (k.getFieldValueType() == FieldValueType.STRING) {
+        builder.put(k, values.get(k));
+      } else {
+        builder.put(k, values.get(k).plus(o.values.get(k)));
+      }
+    }
+    return builder.build();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/RecordFilter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/RecordFilter.java
new file mode 100644
index 0000000..da9d391
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/RecordFilter.java
@@ -0,0 +1,339 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldValue;
+
+/**
+ * Represents a filter that's filtering the metric {@link Record}s.
+ */
+@InterfaceAudience.Private
+public final class RecordFilter {
+
+  private enum Operator {
+    EQUAL("="),
+    DOUBLE_EQUALS("=="),
+    GREATER(">"),
+    GREATER_OR_EQUAL(">="),
+    LESS("<"),
+    LESS_OR_EQUAL("<=");
+
+    private final String operator;
+
+    Operator(String operator) {
+      this.operator = operator;
+    }
+
+    @Override
+    public String toString() {
+      return operator;
+    }
+  }
+
+  public static RecordFilter parse(String filterString, boolean ignoreCase) {
+    return parse(filterString, Arrays.asList(Field.values()), ignoreCase);
+  }
+
+  /*
+   * Parse a filter string and build a RecordFilter instance.
+   */
+  public static RecordFilter parse(String filterString, List<Field> fields, boolean ignoreCase) {
+    int index = 0;
+
+    boolean not = isNot(filterString);
+    if (not) {
+      index += 1;
+    }
+
+    StringBuilder fieldString = new StringBuilder();
+    while (filterString.length() > index && filterString.charAt(index) != '<'
+      && filterString.charAt(index) != '>' && filterString.charAt(index) != '=') {
+      fieldString.append(filterString.charAt(index++));
+    }
+
+    if (fieldString.length() == 0 || filterString.length() == index) {
+      return null;
+    }
+
+    Field field = getField(fields, fieldString.toString());
+    if (field == null) {
+      return null;
+    }
+
+    StringBuilder operatorString = new StringBuilder();
+    while (filterString.length() > index && (filterString.charAt(index) == '<' ||
+      filterString.charAt(index) == '>' || filterString.charAt(index) == '=')) {
+      operatorString.append(filterString.charAt(index++));
+    }
+
+    Operator operator = getOperator(operatorString.toString());
+    if (operator == null) {
+      return null;
+    }
+
+    String value = filterString.substring(index);
+    FieldValue fieldValue = getFieldValue(field, value);
+    if (fieldValue == null) {
+      return null;
+    }
+
+    return new RecordFilter(ignoreCase, not, field, operator, fieldValue);
+  }
+
+  private static FieldValue getFieldValue(Field field, String value) {
+    try {
+      return field.newValue(value);
+    } catch (Exception e) {
+      return null;
+    }
+  }
+
+  private static boolean isNot(String filterString) {
+    return filterString.startsWith("!");
+  }
+
+  private static Field getField(List<Field> fields, String fieldString) {
+    for (Field f : fields) {
+      if (f.getHeader().equals(fieldString)) {
+        return f;
+      }
+    }
+    return null;
+  }
+
+  private static Operator getOperator(String operatorString) {
+    for (Operator o : Operator.values()) {
+      if (operatorString.equals(o.toString())) {
+        return o;
+      }
+    }
+    return null;
+  }
+
+  private final boolean ignoreCase;
+  private final boolean not;
+  private final Field field;
+  private final Operator operator;
+  private final FieldValue value;
+
+  private RecordFilter(boolean ignoreCase, boolean not, Field field, Operator operator,
+    FieldValue value) {
+    this.ignoreCase = ignoreCase;
+    this.not = not;
+    this.field = Objects.requireNonNull(field);
+    this.operator = Objects.requireNonNull(operator);
+    this.value = Objects.requireNonNull(value);
+  }
+
+  public boolean execute(Record record) {
+    FieldValue fieldValue = record.get(field);
+    if (fieldValue == null) {
+      return false;
+    }
+
+    if (operator == Operator.EQUAL) {
+      boolean ret;
+      if (ignoreCase) {
+        ret = fieldValue.asString().toLowerCase().contains(value.asString().toLowerCase());
+      } else {
+        ret = fieldValue.asString().contains(value.asString());
+      }
+      return not != ret;
+    }
+
+    int compare = ignoreCase ?
+      fieldValue.compareToIgnoreCase(value) : fieldValue.compareTo(value);
+
+    boolean ret;
+    switch (operator) {
+      case DOUBLE_EQUALS:
+        ret = compare == 0;
+        break;
+
+      case GREATER:
+        ret = compare > 0;
+        break;
+
+      case GREATER_OR_EQUAL:
+        ret = compare >= 0;
+        break;
+
+      case LESS:
+        ret = compare < 0;
+        break;
+
+      case LESS_OR_EQUAL:
+        ret = compare <= 0;
+        break;
+
+      default:
+        throw new AssertionError();
+    }
+    return not != ret;
+  }
+
+  @Override
+  public String toString() {
+    return (not ? "!" : "") + field.getHeader() + operator + value.asString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RecordFilter)) {
+      return false;
+    }
+    RecordFilter filter = (RecordFilter) o;
+    return ignoreCase == filter.ignoreCase && not == filter.not && field == filter.field
+      && operator == filter.operator && value.equals(filter.value);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(ignoreCase, not, field, operator, value);
+  }
+
+  /*
+   * For FilterBuilder
+   */
+  public static FilterBuilder newBuilder(Field field) {
+    return new FilterBuilder(field, false);
+  }
+
+  public static FilterBuilder newBuilder(Field field, boolean ignoreCase) {
+    return new FilterBuilder(field, ignoreCase);
+  }
+
+  public static final class FilterBuilder {
+    private final Field field;
+    private final boolean ignoreCase;
+
+    private FilterBuilder(Field field, boolean ignoreCase) {
+      this.field = Objects.requireNonNull(field);
+      this.ignoreCase = ignoreCase;
+    }
+
+    public RecordFilter equal(FieldValue value) {
+      return newFilter(false, Operator.EQUAL, value);
+    }
+
+    public RecordFilter equal(Object value) {
+      return equal(field.newValue(value));
+    }
+
+    public RecordFilter notEqual(FieldValue value) {
+      return newFilter(true, Operator.EQUAL, value);
+    }
+
+    public RecordFilter notEqual(Object value) {
+      return notEqual(field.newValue(value));
+    }
+
+    public RecordFilter doubleEquals(FieldValue value) {
+      return newFilter(false, Operator.DOUBLE_EQUALS, value);
+    }
+
+    public RecordFilter doubleEquals(Object value) {
+      return doubleEquals(field.newValue(value));
+    }
+
+    public RecordFilter notDoubleEquals(FieldValue value) {
+      return newFilter(true, Operator.DOUBLE_EQUALS, value);
+    }
+
+    public RecordFilter notDoubleEquals(Object value) {
+      return notDoubleEquals(field.newValue(value));
+    }
+
+    public RecordFilter greater(FieldValue value) {
+      return newFilter(false, Operator.GREATER, value);
+    }
+
+    public RecordFilter greater(Object value) {
+      return greater(field.newValue(value));
+    }
+
+    public RecordFilter notGreater(FieldValue value) {
+      return newFilter(true, Operator.GREATER, value);
+    }
+
+    public RecordFilter notGreater(Object value) {
+      return notGreater(field.newValue(value));
+    }
+
+    public RecordFilter greaterOrEqual(FieldValue value) {
+      return newFilter(false, Operator.GREATER_OR_EQUAL, value);
+    }
+
+    public RecordFilter greaterOrEqual(Object value) {
+      return greaterOrEqual(field.newValue(value));
+    }
+
+    public RecordFilter notGreaterOrEqual(FieldValue value) {
+      return newFilter(true, Operator.GREATER_OR_EQUAL, value);
+    }
+
+    public RecordFilter notGreaterOrEqual(Object value) {
+      return notGreaterOrEqual(field.newValue(value));
+    }
+
+    public RecordFilter less(FieldValue value) {
+      return newFilter(false, Operator.LESS, value);
+    }
+
+    public RecordFilter less(Object value) {
+      return less(field.newValue(value));
+    }
+
+    public RecordFilter notLess(FieldValue value) {
+      return newFilter(true, Operator.LESS, value);
+    }
+
+    public RecordFilter notLess(Object value) {
+      return notLess(field.newValue(value));
+    }
+
+    public RecordFilter lessOrEqual(FieldValue value) {
+      return newFilter(false, Operator.LESS_OR_EQUAL, value);
+    }
+
+    public RecordFilter lessOrEqual(Object value) {
+      return lessOrEqual(field.newValue(value));
+    }
+
+    public RecordFilter notLessOrEqual(FieldValue value) {
+      return newFilter(true, Operator.LESS_OR_EQUAL, value);
+    }
+
+    public RecordFilter notLessOrEqual(Object value) {
+      return notLessOrEqual(field.newValue(value));
+    }
+
+    private RecordFilter newFilter(boolean not, Operator operator, FieldValue value) {
+      return new RecordFilter(ignoreCase, not, field, operator, value);
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java
new file mode 100644
index 0000000..a6f1c48
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Field.java
@@ -0,0 +1,98 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.field;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents fields that are displayed in the top screen.
+ */
+@InterfaceAudience.Private
+public enum Field {
+  REGION_NAME("RNAME", "Region Name", true, true, FieldValueType.STRING),
+  NAMESPACE("NAMESPACE", "Namespace Name", true, true, FieldValueType.STRING),
+  TABLE("TABLE", "Table Name", true, true, FieldValueType.STRING),
+  START_CODE("SCODE", "Start Code", false, true, FieldValueType.STRING),
+  REPLICA_ID("REPID", "Replica ID", false, false, FieldValueType.STRING),
+  REGION("REGION", "Encoded Region Name", false, true, FieldValueType.STRING),
+  REGION_SERVER("RS", "Short Region Server Name", true, true, FieldValueType.STRING),
+  LONG_REGION_SERVER("LRS", "Long Region Server Name", true, true, FieldValueType.STRING),
+  REQUEST_COUNT_PER_SECOND("#REQ/S", "Request Count per second", false, false,
+    FieldValueType.LONG),
+  READ_REQUEST_COUNT_PER_SECOND("#READ/S", "Read Request Count per second", false, false,
+    FieldValueType.LONG),
+  WRITE_REQUEST_COUNT_PER_SECOND("#WRITE/S", "Write Request Count per second", false, false,
+    FieldValueType.LONG),
+  STORE_FILE_SIZE("SF", "StoreFile Size", false, false, FieldValueType.SIZE),
+  UNCOMPRESSED_STORE_FILE_SIZE("USF", "Uncompressed StoreFile Size", false, false,
+    FieldValueType.SIZE),
+  NUM_STORE_FILES("#SF", "Number of StoreFiles", false, false, FieldValueType.INTEGER),
+  MEM_STORE_SIZE("MEMSTORE", "MemStore Size", false, false, FieldValueType.SIZE),
+  LOCALITY("LOCALITY", "Block Locality", false, false, FieldValueType.FLOAT),
+  START_KEY("SKEY", "Start Key", true, true, FieldValueType.STRING),
+  COMPACTING_CELL_COUNT("#COMPingCELL", "Compacting Cell Count", false, false,
+    FieldValueType.LONG),
+  COMPACTED_CELL_COUNT("#COMPedCELL", "Compacted Cell Count", false, false, FieldValueType.LONG),
+  COMPACTION_PROGRESS("%COMP", "Compaction Progress", false, false, FieldValueType.PERCENT),
+  LAST_MAJOR_COMPACTION_TIME("LASTMCOMP", "Last Major Compaction Time", false, true,
+    FieldValueType.STRING),
+  REGION_COUNT("#REGION", "Region Count", false, false, FieldValueType.INTEGER),
+  USED_HEAP_SIZE("UHEAP", "Used Heap Size", false, false, FieldValueType.SIZE),
+  MAX_HEAP_SIZE("MHEAP", "Max Heap Size", false, false, FieldValueType.SIZE);
+
+  private final String header;
+  private final String description;
+  private final boolean autoAdjust;
+  private final boolean leftJustify;
+  private final FieldValueType fieldValueType;
+
+  Field(String header, String description, boolean autoAdjust, boolean leftJustify,
+    FieldValueType fieldValueType) {
+    this.header = Objects.requireNonNull(header);
+    this.description = Objects.requireNonNull(description);
+    this.autoAdjust = autoAdjust;
+    this.leftJustify = leftJustify;
+    this.fieldValueType = Objects.requireNonNull(fieldValueType);
+  }
+
+  public FieldValue newValue(Object value) {
+    return new FieldValue(value, fieldValueType);
+  }
+
+  public String getHeader() {
+    return header;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public boolean isAutoAdjust() {
+    return autoAdjust;
+  }
+
+  public boolean isLeftJustify() {
+    return leftJustify;
+  }
+
+  public FieldValueType getFieldValueType() {
+    return fieldValueType;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldInfo.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldInfo.java
new file mode 100644
index 0000000..b198e20
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldInfo.java
@@ -0,0 +1,55 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.field;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Information about a field.
+ *
+ * This has a {@link Field} itself and additional information (e.g. {@code defaultLength} and
+ * {@code displayByDefault}). This additional information is different between the
+ * {@link org.apache.hadoop.hbase.hbtop.mode.Mode}s even when the field is the same. That's why the
+ * additional information is separated from {@link Field}.
+ */
+@InterfaceAudience.Private
+public class FieldInfo {
+  private final Field field;
+  private final int defaultLength;
+  private final boolean displayByDefault;
+
+  public FieldInfo(Field field, int defaultLength, boolean displayByDefault) {
+    this.field = Objects.requireNonNull(field);
+    this.defaultLength = defaultLength;
+    this.displayByDefault = displayByDefault;
+  }
+
+  public Field getField() {
+    return field;
+  }
+
+  public int getDefaultLength() {
+    return defaultLength;
+  }
+
+  public boolean isDisplayByDefault() {
+    return displayByDefault;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValue.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValue.java
new file mode 100644
index 0000000..db7d22f
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValue.java
@@ -0,0 +1,283 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.field;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents a value of a field.
+ *
+ * The type of a value is defined by {@link FieldValue}.
+ */
+@InterfaceAudience.Private
+public final class FieldValue implements Comparable<FieldValue> {
+
+  private final Object value;
+  private final FieldValueType type;
+
+  FieldValue(Object value, FieldValueType type) {
+    Objects.requireNonNull(value);
+    this.type = Objects.requireNonNull(type);
+
+    switch (type) {
+      case STRING:
+        if (value instanceof String) {
+          this.value = value;
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      case INTEGER:
+        if (value instanceof Integer) {
+          this.value = value;
+          break;
+        } else if (value instanceof String) {
+          this.value = Integer.valueOf((String) value);
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      case LONG:
+        if (value instanceof Long) {
+          this.value = value;
+          break;
+        } else if (value instanceof String) {
+          this.value = Long.valueOf((String) value);
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      case FLOAT:
+        if (value instanceof Float) {
+          this.value = value;
+          break;
+        } else if (value instanceof String) {
+          this.value = Float.valueOf((String) value);
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      case SIZE:
+        if (value instanceof Size) {
+          this.value = optimizeSize((Size) value);
+          break;
+        } else if (value instanceof String) {
+          this.value = optimizeSize(parseSizeString((String) value));
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      case PERCENT:
+        if (value instanceof Float) {
+          this.value = value;
+          break;
+        } else if (value instanceof String) {
+          this.value = parsePercentString((String) value);
+          break;
+        }
+        throw new IllegalArgumentException("invalid type");
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  private Size optimizeSize(Size size) {
+    if (size.get(Size.Unit.BYTE) < 1024d) {
+      return size.getUnit() == Size.Unit.BYTE ?
+        size : new Size(size.get(Size.Unit.BYTE), Size.Unit.BYTE);
+    } else if (size.get(Size.Unit.KILOBYTE) < 1024d) {
+      return size.getUnit() == Size.Unit.KILOBYTE ?
+        size : new Size(size.get(Size.Unit.KILOBYTE), Size.Unit.KILOBYTE);
+    } else if (size.get(Size.Unit.MEGABYTE) < 1024d) {
+      return size.getUnit() == Size.Unit.MEGABYTE ?
+        size : new Size(size.get(Size.Unit.MEGABYTE), Size.Unit.MEGABYTE);
+    } else if (size.get(Size.Unit.GIGABYTE) < 1024d) {
+      return size.getUnit() == Size.Unit.GIGABYTE ?
+        size : new Size(size.get(Size.Unit.GIGABYTE), Size.Unit.GIGABYTE);
+    } else if (size.get(Size.Unit.TERABYTE) < 1024d) {
+      return size.getUnit() == Size.Unit.TERABYTE ?
+        size : new Size(size.get(Size.Unit.TERABYTE), Size.Unit.TERABYTE);
+    }
+    return size.getUnit() == Size.Unit.PETABYTE ?
+      size : new Size(size.get(Size.Unit.PETABYTE), Size.Unit.PETABYTE);
+  }
+
+  private Size parseSizeString(String sizeString) {
+    if (sizeString.length() < 3) {
+      throw new IllegalArgumentException("invalid size");
+    }
+
+    String valueString = sizeString.substring(0, sizeString.length() - 2);
+    String unitSimpleName = sizeString.substring(sizeString.length() - 2);
+    return new Size(Double.parseDouble(valueString), convertToUnit(unitSimpleName));
+  }
+
+  private Size.Unit convertToUnit(String unitSimpleName) {
+    for (Size.Unit unit: Size.Unit.values()) {
+      if (unitSimpleName.equals(unit.getSimpleName())) {
+        return unit;
+      }
+    }
+    throw new IllegalArgumentException("invalid size");
+  }
+
+  private Float parsePercentString(String percentString) {
+    if (percentString.endsWith("%")) {
+      percentString = percentString.substring(0, percentString.length() - 1);
+    }
+    return Float.valueOf(percentString);
+  }
+
+  public String asString() {
+    return toString();
+  }
+
+  public int asInt() {
+    return (Integer) value;
+  }
+
+  public long asLong() {
+    return (Long) value;
+  }
+
+  public float asFloat() {
+    return (Float) value;
+  }
+
+  public Size asSize() {
+    return (Size) value;
+  }
+
+  @Override
+  public String toString() {
+    switch (type) {
+      case STRING:
+      case INTEGER:
+      case LONG:
+      case FLOAT:
+      case SIZE:
+        return value.toString();
+
+      case PERCENT:
+        return String.format("%.2f", (Float) value) + "%";
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  @Override
+  public int compareTo(@NonNull FieldValue o) {
+    if (type != o.type) {
+      throw new IllegalArgumentException("invalid type");
+    }
+
+    switch (type) {
+      case STRING:
+        return ((String) value).compareTo((String) o.value);
+
+      case INTEGER:
+        return ((Integer) value).compareTo((Integer) o.value);
+
+      case LONG:
+        return ((Long) value).compareTo((Long) o.value);
+
+      case FLOAT:
+      case PERCENT:
+        return ((Float) value).compareTo((Float) o.value);
+
+      case SIZE:
+        return ((Size) value).compareTo((Size) o.value);
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof FieldValue)) {
+      return false;
+    }
+    FieldValue that = (FieldValue) o;
+    return value.equals(that.value) && type == that.type;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(value, type);
+  }
+
+  public FieldValue plus(FieldValue o) {
+    if (type != o.type) {
+      throw new IllegalArgumentException("invalid type");
+    }
+
+    switch (type) {
+      case STRING:
+        return new FieldValue(((String) value).concat((String) o.value), type);
+
+      case INTEGER:
+        return new FieldValue(((Integer) value) + ((Integer) o.value), type);
+
+      case LONG:
+        return new FieldValue(((Long) value) + ((Long) o.value), type);
+
+      case FLOAT:
+      case PERCENT:
+        return new FieldValue(((Float) value) + ((Float) o.value), type);
+
+      case SIZE:
+        Size size = (Size) value;
+        Size oSize = (Size) o.value;
+        Size.Unit unit = size.getUnit();
+        return new FieldValue(new Size(size.get(unit) + oSize.get(unit), unit), type);
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  public int compareToIgnoreCase(FieldValue o) {
+    if (type != o.type) {
+      throw new IllegalArgumentException("invalid type");
+    }
+
+    switch (type) {
+      case STRING:
+        return ((String) value).compareToIgnoreCase((String) o.value);
+
+      case INTEGER:
+      case LONG:
+      case FLOAT:
+      case SIZE:
+      case PERCENT:
+        return compareTo(o);
+
+      default:
+        throw new AssertionError();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValueType.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValueType.java
new file mode 100644
index 0000000..bb781d3
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/FieldValueType.java
@@ -0,0 +1,28 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.field;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents the type of a {@link FieldValue}.
+ */
+@InterfaceAudience.Private
+public enum FieldValueType {
+  STRING, INTEGER, LONG, FLOAT, SIZE, PERCENT
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Size.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Size.java
new file mode 100644
index 0000000..709d11d
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/field/Size.java
@@ -0,0 +1,157 @@
+/**
+ * Copyright The Apache Software Foundation
+ * 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.
+ */
+
+package org.apache.hadoop.hbase.hbtop.field;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * It is used to represent the size with different units.
+ * This class doesn't serve for the precise computation.
+ */
+@InterfaceAudience.Private
+public final class Size implements Comparable<Size> {
+  public static final Size ZERO = new Size(0, Unit.KILOBYTE);
+  private static final BigDecimal SCALE_BASE = BigDecimal.valueOf(1024D);
+
+  public enum Unit {
+    // keep the room to add more units for HBase 10.x
+    PETABYTE(100, "PB"),
+    TERABYTE(99, "TB"),
+    GIGABYTE(98, "GB"),
+    MEGABYTE(97, "MB"),
+    KILOBYTE(96, "KB"),
+    BYTE(95, "B");
+    private final int orderOfSize;
+    private final String simpleName;
+
+    Unit(int orderOfSize, String simpleName) {
+      this.orderOfSize = orderOfSize;
+      this.simpleName = simpleName;
+    }
+
+    public int getOrderOfSize() {
+      return orderOfSize;
+    }
+
+    public String getSimpleName() {
+      return simpleName;
+    }
+  }
+
+  private final double value;
+  private final Unit unit;
+
+  public Size(double value, Unit unit) {
+    if (value < 0) {
+      throw new IllegalArgumentException("The value:" + value + " can't be negative");
+    }
+    this.value = value;
+    this.unit = unit;
+  }
+
+  /**
+   * @return size unit
+   */
+  public Unit getUnit() {
+    return unit;
+  }
+
+  /**
+   * get the value
+   */
+  public long getLongValue() {
+    return (long) value;
+  }
+
+  /**
+   * get the value
+   */
+  public double get() {
+    return value;
+  }
+
+  /**
+   * get the value which is converted to specified unit.
+   *
+   * @param unit size unit
+   * @return the converted value
+   */
+  public double get(Unit unit) {
+    if (value == 0) {
+      return value;
+    }
+    int diff = this.unit.getOrderOfSize() - unit.getOrderOfSize();
+    if (diff == 0) {
+      return value;
+    }
+
+    BigDecimal rval = BigDecimal.valueOf(value);
+    for (int i = 0; i != Math.abs(diff); ++i) {
+      rval = diff > 0 ? rval.multiply(SCALE_BASE) : rval.divide(SCALE_BASE);
+    }
+    return rval.doubleValue();
+  }
+
+  @Override
+  public int compareTo(Size other) {
+    int diff = unit.getOrderOfSize() - other.unit.getOrderOfSize();
+    if (diff == 0) {
+      return Double.compare(value, other.value);
+    }
+
+    BigDecimal thisValue = BigDecimal.valueOf(value);
+    BigDecimal otherValue = BigDecimal.valueOf(other.value);
+    if (diff > 0) {
+      for (int i = 0; i != Math.abs(diff); ++i) {
+        thisValue = thisValue.multiply(SCALE_BASE);
+      }
+    } else {
+      for (int i = 0; i != Math.abs(diff); ++i) {
+        otherValue = otherValue.multiply(SCALE_BASE);
+      }
+    }
+    return thisValue.compareTo(otherValue);
+  }
+
+  @Override
+  public String toString() {
+    return value + unit.getSimpleName();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == null) {
+      return false;
+    }
+    if (obj == this) {
+      return true;
+    }
+    if (obj instanceof Size) {
+      return compareTo((Size)obj) == 0;
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(value, unit);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/DrillDownInfo.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/DrillDownInfo.java
new file mode 100644
index 0000000..5311299
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/DrillDownInfo.java
@@ -0,0 +1,50 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+
+/**
+ * Information about drilling down.
+ *
+ * When drilling down, going to next {@link Mode} with initial {@link RecordFilter}s.
+ */
+@InterfaceAudience.Private
+public class DrillDownInfo {
+  private final Mode nextMode;
+  private final List<RecordFilter> initialFilters;
+
+  public DrillDownInfo(Mode nextMode, List<RecordFilter> initialFilters) {
+    this.nextMode = Objects.requireNonNull(nextMode);
+    this.initialFilters = Collections.unmodifiableList(new ArrayList<>(initialFilters));
+  }
+
+  public Mode getNextMode() {
+    return nextMode;
+  }
+
+  public List<RecordFilter> getInitialFilters() {
+    return initialFilters;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/Mode.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/Mode.java
new file mode 100644
index 0000000..e5dd42e
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/Mode.java
@@ -0,0 +1,74 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+
+/**
+ * Represents a display mode in the top screen.
+ */
+@InterfaceAudience.Private
+public enum Mode {
+  NAMESPACE("Namespace", "Record per Namespace", new NamespaceModeStrategy()),
+  TABLE("Table", "Record per Table", new TableModeStrategy()),
+  REGION("Region", "Record per Region", new RegionModeStrategy()),
+  REGION_SERVER("RegionServer", "Record per RegionServer", new RegionServerModeStrategy());
+
+  private final String header;
+  private final String description;
+  private final ModeStrategy modeStrategy;
+
+  Mode(String header, String description, ModeStrategy modeStrategy) {
+    this.header  = Objects.requireNonNull(header);
+    this.description = Objects.requireNonNull(description);
+    this.modeStrategy = Objects.requireNonNull(modeStrategy);
+  }
+
+  public String getHeader() {
+    return header;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public List<Record> getRecords(ClusterStatus clusterStatus) {
+    return modeStrategy.getRecords(clusterStatus);
+  }
+
+  public List<FieldInfo> getFieldInfos() {
+    return modeStrategy.getFieldInfos();
+  }
+
+  public Field getDefaultSortField() {
+    return modeStrategy.getDefaultSortField();
+  }
+
+  @Nullable
+  public DrillDownInfo drillDown(Record currentRecord) {
+    return modeStrategy.drillDown(currentRecord);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ModeStrategy.java
new file mode 100644
index 0000000..cbfae83
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/ModeStrategy.java
@@ -0,0 +1,38 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.List;
+
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+
+/**
+ * An interface for strategy logic for {@link Mode}.
+ */
+@InterfaceAudience.Private
+interface ModeStrategy {
+  List<FieldInfo> getFieldInfos();
+  Field getDefaultSortField();
+  List<Record> getRecords(ClusterStatus clusterStatus);
+  @Nullable DrillDownInfo drillDown(Record selectedRecord);
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/NamespaceModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/NamespaceModeStrategy.java
new file mode 100644
index 0000000..b17c372
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/NamespaceModeStrategy.java
@@ -0,0 +1,105 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+
+/**
+ * Implementation for {@link ModeStrategy} for Namespace Mode.
+ */
+@InterfaceAudience.Private
+public final class NamespaceModeStrategy implements ModeStrategy {
+
+  private final List<FieldInfo> fieldInfos = Arrays.asList(
+    new FieldInfo(Field.NAMESPACE, 0, true),
+    new FieldInfo(Field.REGION_COUNT, 7, true),
+    new FieldInfo(Field.REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.READ_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.WRITE_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.STORE_FILE_SIZE, 13, true),
+    new FieldInfo(Field.UNCOMPRESSED_STORE_FILE_SIZE, 15, false),
+    new FieldInfo(Field.NUM_STORE_FILES, 7, true),
+    new FieldInfo(Field.MEM_STORE_SIZE, 11, true)
+  );
+
+  private final RegionModeStrategy regionModeStrategy = new RegionModeStrategy();
+
+  NamespaceModeStrategy(){
+  }
+
+  @Override
+  public List<FieldInfo> getFieldInfos() {
+    return fieldInfos;
+  }
+
+  @Override
+  public Field getDefaultSortField() {
+    return Field.REQUEST_COUNT_PER_SECOND;
+  }
+
+  @Override
+  public List<Record> getRecords(ClusterStatus clusterStatus) {
+    // Get records from RegionModeStrategy and add REGION_COUNT field
+    List<Record> records = new ArrayList<>();
+    for (Record record : regionModeStrategy.getRecords(clusterStatus)) {
+      List<Record.Entry> entries = new ArrayList<>();
+      for (FieldInfo fieldInfo : fieldInfos) {
+        if (record.containsKey(fieldInfo.getField())) {
+          entries.add(Record.entry(fieldInfo.getField(),
+            record.get(fieldInfo.getField())));
+        }
+      }
+
+      // Add REGION_COUNT field
+      records.add(Record.builder().putAll(Record.ofEntries(entries))
+        .put(Field.REGION_COUNT, 1).build());
+    }
+
+    // Aggregation by NAMESPACE field
+    Map<String, Record> retMap = new HashMap<>();
+    for (Record record : records) {
+      String namespace = record.get(Field.NAMESPACE).asString();
+      if (retMap.containsKey(namespace)) {
+        retMap.put(namespace, retMap.get(namespace).combine(record));
+      } else {
+        retMap.put(namespace, record);
+      }
+    }
+    return new ArrayList<>(retMap.values());
+  }
+
+  @Override
+  public DrillDownInfo drillDown(Record selectedRecord) {
+    List<RecordFilter> initialFilters =
+      Collections.singletonList(RecordFilter.newBuilder(Field.NAMESPACE)
+        .doubleEquals(selectedRecord.get(Field.NAMESPACE)));
+    return new DrillDownInfo(Mode.TABLE, initialFilters);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionModeStrategy.java
new file mode 100644
index 0000000..6ab7163
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionModeStrategy.java
@@ -0,0 +1,182 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.time.FastDateFormat;
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.HRegionInfo;
+import org.apache.hadoop.hbase.RegionLoad;
+import org.apache.hadoop.hbase.ServerLoad;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.field.Size;
+import org.apache.hadoop.hbase.hbtop.field.Size.Unit;
+import org.apache.hadoop.hbase.util.Bytes;
+
+/**
+ * Implementation for {@link ModeStrategy} for Region Mode.
+ */
+@InterfaceAudience.Private
+public final class RegionModeStrategy implements ModeStrategy {
+
+  private final List<FieldInfo> fieldInfos = Arrays.asList(
+    new FieldInfo(Field.REGION_NAME, 0, false),
+    new FieldInfo(Field.NAMESPACE, 0, true),
+    new FieldInfo(Field.TABLE, 0,  true),
+    new FieldInfo(Field.START_CODE, 13, false),
+    new FieldInfo(Field.REPLICA_ID, 5, false),
+    new FieldInfo(Field.REGION, 32, true),
+    new FieldInfo(Field.REGION_SERVER, 0, true),
+    new FieldInfo(Field.LONG_REGION_SERVER, 0, false),
+    new FieldInfo(Field.REQUEST_COUNT_PER_SECOND, 8, true),
+    new FieldInfo(Field.READ_REQUEST_COUNT_PER_SECOND, 8, true),
+    new FieldInfo(Field.WRITE_REQUEST_COUNT_PER_SECOND, 8, true),
+    new FieldInfo(Field.STORE_FILE_SIZE, 10, true),
+    new FieldInfo(Field.UNCOMPRESSED_STORE_FILE_SIZE, 12, false),
+    new FieldInfo(Field.NUM_STORE_FILES,4, true),
+    new FieldInfo(Field.MEM_STORE_SIZE, 8, true),
+    new FieldInfo(Field.LOCALITY, 8, true),
+    new FieldInfo(Field.START_KEY, 0, false),
+    new FieldInfo(Field.COMPACTING_CELL_COUNT, 12, false),
+    new FieldInfo(Field.COMPACTED_CELL_COUNT, 12, false),
+    new FieldInfo(Field.COMPACTION_PROGRESS, 7, false),
+    new FieldInfo(Field.LAST_MAJOR_COMPACTION_TIME, 19, false)
+  );
+
+  private final Map<String, RequestCountPerSecond> requestCountPerSecondMap = new HashMap<>();
+
+  RegionModeStrategy() {
+  }
+
+  @Override
+  public List<FieldInfo> getFieldInfos() {
+    return fieldInfos;
+  }
+
+  @Override
+  public Field getDefaultSortField() {
+    return Field.REQUEST_COUNT_PER_SECOND;
+  }
+
+  @Override
+  public List<Record> getRecords(ClusterStatus clusterStatus) {
+    List<Record> ret = new ArrayList<>();
+    for (ServerName sn: clusterStatus.getServers()) {
+      ServerLoad sl = clusterStatus.getLoad(sn);
+      long lastReportTimestamp = sl.obtainServerLoadPB().getReportEndTime();
+      for (RegionLoad rl: sl.getRegionsLoad().values()) {
+        ret.add(createRecord(sn, rl, lastReportTimestamp));
+      }
+    }
+    return ret;
+  }
+
+  private Record createRecord(ServerName sn, RegionLoad regionLoad, long lastReportTimestamp) {
+    Record.Builder builder = Record.builder();
+
+    String regionName = regionLoad.getNameAsString();
+    builder.put(Field.REGION_NAME, regionName);
+
+    String namespaceName = "";
+    String tableName = "";
+    String region = "";
+    String startKey = "";
+    String startCode = "";
+    String replicaId = "";
+    try {
+      byte[][] elements = HRegionInfo.parseRegionName(regionLoad.getName());
+      TableName tn = TableName.valueOf(elements[0]);
+      namespaceName = tn.getNamespaceAsString();
+      tableName = tn.getQualifierAsString();
+      startKey = Bytes.toStringBinary(elements[1]);
+      startCode = Bytes.toString(elements[2]);
+      replicaId = elements.length == 4 ?
+        Integer.valueOf(Bytes.toString(elements[3])).toString() : "";
+      region = HRegionInfo.encodeRegionName(regionLoad.getName());
+    } catch (IOException ignored) {
+    }
+
+    builder.put(Field.NAMESPACE, namespaceName);
+    builder.put(Field.TABLE, tableName);
+    builder.put(Field.START_CODE, startCode);
+    builder.put(Field.REPLICA_ID, replicaId);
+    builder.put(Field.REGION, region);
+    builder.put(Field.START_KEY, startKey);
+    builder.put(Field.REGION_SERVER, sn.toShortString());
+    builder.put(Field.LONG_REGION_SERVER, sn.getServerName());
+
+    RequestCountPerSecond requestCountPerSecond = requestCountPerSecondMap.get(regionName);
+    if (requestCountPerSecond == null) {
+      requestCountPerSecond = new RequestCountPerSecond();
+      requestCountPerSecondMap.put(regionName, requestCountPerSecond);
+    }
+    requestCountPerSecond.refresh(lastReportTimestamp, regionLoad.getReadRequestsCount(),
+      regionLoad.getWriteRequestsCount());
+
+    builder.put(Field.READ_REQUEST_COUNT_PER_SECOND,
+      requestCountPerSecond.getReadRequestCountPerSecond());
+    builder.put(Field.WRITE_REQUEST_COUNT_PER_SECOND,
+      requestCountPerSecond.getWriteRequestCountPerSecond());
+    builder.put(Field.REQUEST_COUNT_PER_SECOND,
+      requestCountPerSecond.getRequestCountPerSecond());
+
+    builder.put(Field.STORE_FILE_SIZE, new Size(regionLoad.getStorefileSizeMB(), Unit.MEGABYTE));
+    builder.put(Field.UNCOMPRESSED_STORE_FILE_SIZE,
+      new Size(regionLoad.getStoreUncompressedSizeMB(), Unit.MEGABYTE));
+    builder.put(Field.NUM_STORE_FILES, regionLoad.getStorefiles());
+    builder.put(Field.MEM_STORE_SIZE, new Size(regionLoad.getMemStoreSizeMB(), Unit.MEGABYTE));
+    builder.put(Field.LOCALITY, regionLoad.getDataLocality());
+
+    long compactingCellCount = regionLoad.getTotalCompactingKVs();
+    long compactedCellCount = regionLoad.getCurrentCompactedKVs();
+    float compactionProgress = 0;
+    if  (compactedCellCount > 0) {
+      compactionProgress = 100 * ((float) compactedCellCount / compactingCellCount);
+    }
+
+    builder.put(Field.COMPACTING_CELL_COUNT, compactingCellCount);
+    builder.put(Field.COMPACTED_CELL_COUNT, compactedCellCount);
+    builder.put(Field.COMPACTION_PROGRESS, compactionProgress);
+
+    FastDateFormat df = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
+    long lastMajorCompactionTimestamp = regionLoad.getLastMajorCompactionTs();
+
+    builder.put(Field.LAST_MAJOR_COMPACTION_TIME,
+      lastMajorCompactionTimestamp == 0 ? "" : df.format(lastMajorCompactionTimestamp));
+
+    return builder.build();
+  }
+
+  @Nullable
+  @Override
+  public DrillDownInfo drillDown(Record selectedRecord) {
+    // do nothing
+    return null;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionServerModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionServerModeStrategy.java
new file mode 100644
index 0000000..4d7169a
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RegionServerModeStrategy.java
@@ -0,0 +1,124 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.ServerLoad;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.field.Size;
+import org.apache.hadoop.hbase.hbtop.field.Size.Unit;
+
+/**
+ * Implementation for {@link ModeStrategy} for RegionServer Mode.
+ */
+@InterfaceAudience.Private
+public final class RegionServerModeStrategy implements ModeStrategy {
+
+  private final List<FieldInfo> fieldInfos = Arrays.asList(
+    new FieldInfo(Field.REGION_SERVER, 0, true),
+    new FieldInfo(Field.LONG_REGION_SERVER, 0, false),
+    new FieldInfo(Field.REGION_COUNT, 7, true),
+    new FieldInfo(Field.REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.READ_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.WRITE_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.STORE_FILE_SIZE, 13, true),
+    new FieldInfo(Field.UNCOMPRESSED_STORE_FILE_SIZE, 15, false),
+    new FieldInfo(Field.NUM_STORE_FILES, 7, true),
+    new FieldInfo(Field.MEM_STORE_SIZE, 11, true),
+    new FieldInfo(Field.USED_HEAP_SIZE, 11, true),
+    new FieldInfo(Field.MAX_HEAP_SIZE, 11, true)
+  );
+
+  private final RegionModeStrategy regionModeStrategy = new RegionModeStrategy();
+
+  RegionServerModeStrategy(){
+  }
+
+  @Override
+  public List<FieldInfo> getFieldInfos() {
+    return fieldInfos;
+  }
+
+  @Override
+  public Field getDefaultSortField() {
+    return Field.REQUEST_COUNT_PER_SECOND;
+  }
+
+  @Override
+  public List<Record> getRecords(ClusterStatus clusterStatus) {
+    // Get records from RegionModeStrategy and add REGION_COUNT field
+    List<Record> records = new ArrayList<>();
+    for (Record record : regionModeStrategy.getRecords(clusterStatus)) {
+      List<Record.Entry> entries = new ArrayList<>();
+      for (FieldInfo fieldInfo : fieldInfos) {
+        if (record.containsKey(fieldInfo.getField())) {
+          entries.add(Record.entry(fieldInfo.getField(),
+            record.get(fieldInfo.getField())));
+        }
+      }
+
+      // Add REGION_COUNT field
+      records.add(Record.builder().putAll(Record.ofEntries(entries))
+        .put(Field.REGION_COUNT, 1).build());
+    }
+
+    // Aggregation by NAMESPACE field
+    Map<String, Record> retMap = new HashMap<>();
+    for (Record record : records) {
+      String regionServer = record.get(Field.LONG_REGION_SERVER).asString();
+      if (retMap.containsKey(regionServer)) {
+        retMap.put(regionServer, retMap.get(regionServer).combine(record));
+      } else {
+        retMap.put(regionServer, record);
+      }
+    }
+
+    // Add USED_HEAP_SIZE field and MAX_HEAP_SIZE field
+    for (ServerName sn : clusterStatus.getServers()) {
+      Record record = retMap.get(sn.getServerName());
+      if (record == null) {
+        continue;
+      }
+      ServerLoad sl = clusterStatus.getLoad(sn);
+      Record newRecord = Record.builder().putAll(record)
+        .put(Field.USED_HEAP_SIZE, new Size(sl.getUsedHeapMB(), Unit.MEGABYTE))
+        .put(Field.MAX_HEAP_SIZE, new Size(sl.getMaxHeapMB(), Unit.MEGABYTE)).build();
+      retMap.put(sn.getServerName(), newRecord);
+    }
+    return new ArrayList<>(retMap.values());
+  }
+
+  @Override
+  public DrillDownInfo drillDown(Record selectedRecord) {
+    List<RecordFilter> initialFilters = Collections.singletonList(RecordFilter
+      .newBuilder(Field.REGION_SERVER)
+      .doubleEquals(selectedRecord.get(Field.REGION_SERVER)));
+    return new DrillDownInfo(Mode.REGION, initialFilters);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RequestCountPerSecond.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RequestCountPerSecond.java
new file mode 100644
index 0000000..ee6bef5
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/RequestCountPerSecond.java
@@ -0,0 +1,63 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Utility class for calculating request counts per second.
+ */
+@InterfaceAudience.Private
+public class RequestCountPerSecond {
+  private long previousLastReportTimestamp;
+  private long previousReadRequestCount;
+  private long previousWriteRequestCount;
+  private long readRequestCountPerSecond;
+  private long writeRequestCountPerSecond;
+
+  public void refresh(long lastReportTimestamp, long readRequestCount, long writeRequestCount) {
+    if (previousLastReportTimestamp == 0) {
+      previousLastReportTimestamp = lastReportTimestamp;
+      previousReadRequestCount = readRequestCount;
+      previousWriteRequestCount = writeRequestCount;
+    } else if (previousLastReportTimestamp != lastReportTimestamp) {
+      long delta = (lastReportTimestamp - previousLastReportTimestamp) / 1000;
+      if (delta < 1) {
+        delta = 1;
+      }
+      readRequestCountPerSecond = (readRequestCount - previousReadRequestCount) / delta;
+      writeRequestCountPerSecond = (writeRequestCount - previousWriteRequestCount) / delta;
+
+      previousLastReportTimestamp = lastReportTimestamp;
+      previousReadRequestCount = readRequestCount;
+      previousWriteRequestCount = writeRequestCount;
+    }
+  }
+
+  public long getReadRequestCountPerSecond() {
+    return readRequestCountPerSecond < 0 ? 0 : readRequestCountPerSecond;
+  }
+
+  public long getWriteRequestCountPerSecond() {
+    return writeRequestCountPerSecond < 0 ? 0 : writeRequestCountPerSecond;
+  }
+
+  public long getRequestCountPerSecond() {
+    return getReadRequestCountPerSecond() + getWriteRequestCountPerSecond();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/TableModeStrategy.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/TableModeStrategy.java
new file mode 100644
index 0000000..eeefbf2
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/mode/TableModeStrategy.java
@@ -0,0 +1,108 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+
+/**
+ * Implementation for {@link ModeStrategy} for Table Mode.
+ */
+@InterfaceAudience.Private
+public final class TableModeStrategy implements ModeStrategy {
+
+  private final List<FieldInfo> fieldInfos = Arrays.asList(
+    new FieldInfo(Field.NAMESPACE, 0, true),
+    new FieldInfo(Field.TABLE, 0, true),
+    new FieldInfo(Field.REGION_COUNT, 7, true),
+    new FieldInfo(Field.REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.READ_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.WRITE_REQUEST_COUNT_PER_SECOND, 10, true),
+    new FieldInfo(Field.STORE_FILE_SIZE, 13, true),
+    new FieldInfo(Field.UNCOMPRESSED_STORE_FILE_SIZE, 15, false),
+    new FieldInfo(Field.NUM_STORE_FILES, 7, true),
+    new FieldInfo(Field.MEM_STORE_SIZE, 11, true)
+  );
+
+  private final RegionModeStrategy regionModeStrategy = new RegionModeStrategy();
+
+  TableModeStrategy() {
+  }
+
+  @Override
+  public List<FieldInfo> getFieldInfos() {
+    return fieldInfos;
+  }
+
+  @Override
+  public Field getDefaultSortField() {
+    return Field.REQUEST_COUNT_PER_SECOND;
+  }
+
+  @Override
+  public List<Record> getRecords(ClusterStatus clusterStatus) {
+    // Get records from RegionModeStrategy and add REGION_COUNT field
+    List<Record> records = new ArrayList<>();
+    for (Record record : regionModeStrategy.getRecords(clusterStatus)) {
+      List<Record.Entry> entries = new ArrayList<>();
+      for (FieldInfo fieldInfo : fieldInfos) {
+        if (record.containsKey(fieldInfo.getField())) {
+          entries.add(Record.entry(fieldInfo.getField(),
+            record.get(fieldInfo.getField())));
+        }
+      }
+
+      // Add REGION_COUNT field
+      records.add(Record.builder().putAll(Record.ofEntries(entries))
+        .put(Field.REGION_COUNT, 1).build());
+    }
+
+    // Aggregation by NAMESPACE field
+    Map<TableName, Record> retMap = new HashMap<>();
+    for (Record record : records) {
+      String namespace = record.get(Field.NAMESPACE).asString();
+      String table = record.get(Field.TABLE).asString();
+      TableName tableName = TableName.valueOf(namespace, table);
+
+      if (retMap.containsKey(tableName)) {
+        retMap.put(tableName, retMap.get(tableName).combine(record));
+      } else {
+        retMap.put(tableName, record);
+      }
+    }
+    return new ArrayList<>(retMap.values());
+  }
+
+  @Override
+  public DrillDownInfo drillDown(Record selectedRecord) {
+    List<RecordFilter> initialFilters = Arrays.asList(
+      RecordFilter.newBuilder(Field.NAMESPACE).doubleEquals(selectedRecord.get(Field.NAMESPACE)),
+      RecordFilter.newBuilder(Field.TABLE).doubleEquals(selectedRecord.get(Field.TABLE)));
+    return new DrillDownInfo(Mode.REGION, initialFilters);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/AbstractScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/AbstractScreenView.java
new file mode 100644
index 0000000..32dfb53
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/AbstractScreenView.java
@@ -0,0 +1,102 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
+
+/**
+ * An abstract class for {@link ScreenView} that has the common useful methods and the default
+ * implementations for the abstract methods.
+ */
+@InterfaceAudience.Private
+public abstract class AbstractScreenView implements ScreenView {
+
+  private final Screen screen;
+  private final Terminal terminal;
+
+  public AbstractScreenView(Screen screen, Terminal terminal) {
+    this.screen = Objects.requireNonNull(screen);
+    this.terminal = Objects.requireNonNull(terminal);
+  }
+
+  @Override
+  public void init() {
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    return this;
+  }
+
+  @Override
+  public ScreenView handleTimer() {
+    return this;
+  }
+
+  protected Screen getScreen() {
+    return screen;
+  }
+
+  protected Terminal getTerminal() {
+    return terminal;
+  }
+
+  protected void setTimer(long delay) {
+    screen.setTimer(delay);
+  }
+
+  protected void cancelTimer() {
+    screen.cancelTimer();
+  }
+
+  protected TerminalPrinter getTerminalPrinter(int startRow) {
+    return terminal.getTerminalPrinter(startRow);
+  }
+
+  protected TerminalSize getTerminalSize() {
+    return terminal.getSize();
+  }
+
+  @Nullable
+  protected TerminalSize doResizeIfNecessary() {
+    return terminal.doResizeIfNecessary();
+  }
+
+  public void clearTerminal() {
+    terminal.clear();
+  }
+
+  public void refreshTerminal() {
+    terminal.refresh();
+  }
+
+  public void hideCursor() {
+    terminal.hideCursor();
+  }
+
+  public void setCursorPosition(int column, int row) {
+    terminal.setCursorPosition(column, row);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/Screen.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/Screen.java
new file mode 100644
index 0000000..bbcbba2
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/Screen.java
@@ -0,0 +1,132 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.top.TopScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.impl.TerminalImpl;
+
+/**
+ * This dispatches key presses and timers to the current {@link ScreenView}.
+ */
+@InterfaceAudience.Private
+public class Screen implements Closeable {
+
+  private static final Log LOG = LogFactory.getLog(Screen.class);
+
+  private static final long SLEEP_TIMEOUT_MILLISECONDS = 100;
+
+  private final Connection connection;
+  private final Admin admin;
+  private final Terminal terminal;
+
+  private ScreenView currentScreenView;
+  private Long timerTimestamp;
+
+  public Screen(Configuration conf, long initialRefreshDelay, Mode initialMode)
+    throws IOException {
+    connection = ConnectionFactory.createConnection(conf);
+    admin = connection.getAdmin();
+
+    // The first screen is the top screen
+    this.terminal = new TerminalImpl("hbtop");
+    currentScreenView = new TopScreenView(this, terminal, initialRefreshDelay, admin,
+      initialMode);
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      admin.close();
+    } finally {
+      try {
+        connection.close();
+      } finally {
+        terminal.close();
+      }
+    }
+  }
+
+  public void run() {
+    currentScreenView.init();
+    while (true) {
+      try {
+        KeyPress keyPress = terminal.pollKeyPress();
+
+        ScreenView nextScreenView;
+        if (keyPress != null) {
+          // Dispatch the key press to the current screen
+          nextScreenView = currentScreenView.handleKeyPress(keyPress);
+        } else {
+          if (timerTimestamp != null) {
+            long now = System.currentTimeMillis();
+            if (timerTimestamp <= now) {
+              // Dispatch the timer to the current screen
+              timerTimestamp = null;
+              nextScreenView = currentScreenView.handleTimer();
+            } else {
+              if (timerTimestamp - now < SLEEP_TIMEOUT_MILLISECONDS) {
+                TimeUnit.MILLISECONDS.sleep(timerTimestamp - now);
+              } else {
+                TimeUnit.MILLISECONDS.sleep(SLEEP_TIMEOUT_MILLISECONDS);
+              }
+              continue;
+            }
+          } else {
+            TimeUnit.MILLISECONDS.sleep(SLEEP_TIMEOUT_MILLISECONDS);
+            continue;
+          }
+        }
+
+        // If the next screen is null, then exit
+        if (nextScreenView == null) {
+          return;
+        }
+
+        // If the next screen is not the previous, then go to the next screen
+        if (nextScreenView != currentScreenView) {
+          currentScreenView = nextScreenView;
+          currentScreenView.init();
+        }
+      } catch (Exception e) {
+        LOG.error("Caught an exception", e);
+      }
+    }
+  }
+
+  public void setTimer(long delay) {
+    timerTimestamp = System.currentTimeMillis() + delay;
+  }
+
+  public void cancelTimer() {
+    timerTimestamp = null;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/ScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/ScreenView.java
new file mode 100644
index 0000000..33f04b6
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/ScreenView.java
@@ -0,0 +1,33 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+
+/**
+ * An interface for a screen view that handles key presses and timers.
+ */
+@InterfaceAudience.Private
+public interface ScreenView {
+  void init();
+  @Nullable ScreenView handleKeyPress(KeyPress keyPress);
+  @Nullable ScreenView handleTimer();
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenPresenter.java
new file mode 100644
index 0000000..c32fc1b
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenPresenter.java
@@ -0,0 +1,184 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.field;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the field screen.
+ */
+@InterfaceAudience.Private
+public class FieldScreenPresenter {
+
+  public interface ResultListener {
+    void accept(Field sortField, List<Field> fields, EnumMap<Field, Boolean> fieldDisplayMap);
+  }
+
+  private final FieldScreenView fieldScreenView;
+  private Field sortField;
+  private final List<Field> fields;
+  private final EnumMap<Field, Boolean> fieldDisplayMap;
+  private final ResultListener resultListener;
+  private final ScreenView nextScreenView;
+
+  private final int headerMaxLength;
+  private final int descriptionMaxLength;
+
+  private int currentPosition;
+  private boolean moveMode;
+
+  public FieldScreenPresenter(FieldScreenView fieldScreenView, Field sortField, List<Field> fields,
+    EnumMap<Field, Boolean> fieldDisplayMap, ResultListener resultListener,
+    ScreenView nextScreenView) {
+    this.fieldScreenView = Objects.requireNonNull(fieldScreenView);
+    this.sortField = Objects.requireNonNull(sortField);
+    this.fields = new ArrayList<>(Objects.requireNonNull(fields));
+    this.fieldDisplayMap = new EnumMap<>(Objects.requireNonNull(fieldDisplayMap));
+    this.resultListener = Objects.requireNonNull(resultListener);
+    this.nextScreenView = Objects.requireNonNull(nextScreenView);
+
+    int headerLength = 0;
+    int descriptionLength = 0;
+    for (int i = 0; i < fields.size(); i ++) {
+      Field field = fields.get(i);
+
+      if (field == sortField) {
+        currentPosition = i;
+      }
+
+      if (headerLength < field.getHeader().length()) {
+        headerLength = field.getHeader().length();
+      }
+
+      if (descriptionLength < field.getDescription().length()) {
+        descriptionLength = field.getDescription().length();
+      }
+    }
+
+    headerMaxLength = headerLength;
+    descriptionMaxLength = descriptionLength;
+  }
+
+  public void init() {
+    fieldScreenView.hideCursor();
+    fieldScreenView.clearTerminal();
+    fieldScreenView.showFieldScreen(sortField.getHeader(), fields, fieldDisplayMap,
+      currentPosition, headerMaxLength, descriptionMaxLength, moveMode);
+    fieldScreenView.refreshTerminal();
+  }
+
+  public void arrowUp() {
+    if (currentPosition > 0) {
+      currentPosition -= 1;
+
+      if (moveMode) {
+        Field tmp = fields.remove(currentPosition);
+        fields.add(currentPosition + 1, tmp);
+      }
+
+      showField(currentPosition);
+      showField(currentPosition + 1);
+      fieldScreenView.refreshTerminal();
+    }
+  }
+
+  public void arrowDown() {
+    if (currentPosition < fields.size() - 1) {
+      currentPosition += 1;
+
+      if (moveMode) {
+        Field tmp = fields.remove(currentPosition - 1);
+        fields.add(currentPosition, tmp);
+      }
+
+      showField(currentPosition);
+      showField(currentPosition - 1);
+      fieldScreenView.refreshTerminal();
+    }
+  }
+
+  public void pageUp() {
+    if (currentPosition > 0 && !moveMode) {
+      int previousPosition = currentPosition;
+      currentPosition = 0;
+      showField(previousPosition);
+      showField(currentPosition);
+      fieldScreenView.refreshTerminal();
+    }
+  }
+
+  public void pageDown() {
+    if (currentPosition < fields.size() - 1  && !moveMode) {
+      int previousPosition = currentPosition;
+      currentPosition = fields.size() - 1;
+      showField(previousPosition);
+      showField(currentPosition);
+      fieldScreenView.refreshTerminal();
+    }
+  }
+
+  public void turnOnMoveMode() {
+    moveMode = true;
+    showField(currentPosition);
+    fieldScreenView.refreshTerminal();
+  }
+
+  public void turnOffMoveMode() {
+    moveMode = false;
+    showField(currentPosition);
+    fieldScreenView.refreshTerminal();
+  }
+
+  public void switchFieldDisplay() {
+    if (!moveMode) {
+      Field field = fields.get(currentPosition);
+      fieldDisplayMap.put(field, !fieldDisplayMap.get(field));
+      showField(currentPosition);
+      fieldScreenView.refreshTerminal();
+    }
+  }
+
+  private void showField(int pos) {
+    Field field = fields.get(pos);
+    fieldScreenView.showField(pos, field, fieldDisplayMap.get(field), pos == currentPosition,
+      headerMaxLength, descriptionMaxLength, moveMode);
+  }
+
+  public void setSortField() {
+    if (!moveMode) {
+      Field newSortField = fields.get(currentPosition);
+      if (newSortField != this.sortField) {
+        this.sortField = newSortField;
+        fieldScreenView.showScreenDescription(sortField.getHeader());
+        fieldScreenView.refreshTerminal();
+      }
+    }
+  }
+
+  public ScreenView transitionToNextScreen() {
+    resultListener.accept(sortField, fields, fieldDisplayMap);
+    return nextScreenView;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenView.java
new file mode 100644
index 0000000..b9eec85
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/field/FieldScreenView.java
@@ -0,0 +1,193 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.field;
+
+import java.util.EnumMap;
+import java.util.List;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+
+/**
+ * The screen where we can change the displayed fields, the sort key and the order of the fields.
+ */
+@InterfaceAudience.Private
+public class FieldScreenView extends AbstractScreenView {
+
+  private static final int SCREEN_DESCRIPTION_START_ROW = 0;
+  private static final int FIELD_START_ROW = 5;
+
+  private final FieldScreenPresenter fieldScreenPresenter;
+
+  public FieldScreenView(Screen screen, Terminal terminal, Field sortField, List<Field> fields,
+    EnumMap<Field, Boolean> fieldDisplayMap, FieldScreenPresenter.ResultListener resultListener,
+    ScreenView nextScreenView) {
+    super(screen, terminal);
+    this.fieldScreenPresenter = new FieldScreenPresenter(this, sortField, fields, fieldDisplayMap,
+      resultListener, nextScreenView);
+  }
+
+  @Override
+  public void init() {
+    fieldScreenPresenter.init();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    switch (keyPress.getType()) {
+      case Escape:
+        return fieldScreenPresenter.transitionToNextScreen();
+
+      case ArrowUp:
+        fieldScreenPresenter.arrowUp();
+        return this;
+
+      case ArrowDown:
+        fieldScreenPresenter.arrowDown();
+        return this;
+
+      case PageUp:
+      case Home:
+        fieldScreenPresenter.pageUp();
+        return this;
+
+      case PageDown:
+      case End:
+        fieldScreenPresenter.pageDown();
+        return this;
+
+      case ArrowRight:
+        fieldScreenPresenter.turnOnMoveMode();
+        return this;
+
+      case ArrowLeft:
+      case Enter:
+        fieldScreenPresenter.turnOffMoveMode();
+        return this;
+
+      default:
+        // Do nothing
+        break;
+    }
+
+    if (keyPress.getType() != KeyPress.Type.Character) {
+      return this;
+    }
+
+    assert keyPress.getCharacter() != null;
+    switch (keyPress.getCharacter()) {
+      case 'd':
+      case ' ':
+        fieldScreenPresenter.switchFieldDisplay();
+        break;
+
+      case 's':
+        fieldScreenPresenter.setSortField();
+        break;
+
+      case 'q':
+        return fieldScreenPresenter.transitionToNextScreen();
+
+      default:
+        // Do nothing
+        break;
+    }
+
+    return this;
+  }
+
+  public void showFieldScreen(String sortFieldHeader, List<Field> fields,
+    EnumMap<Field, Boolean> fieldDisplayMap, int currentPosition, int headerMaxLength,
+    int descriptionMaxLength, boolean moveMode) {
+    showScreenDescription(sortFieldHeader);
+
+    for (int i = 0; i < fields.size(); i ++) {
+      Field field = fields.get(i);
+      showField(i, field, fieldDisplayMap.get(field), i == currentPosition, headerMaxLength,
+        descriptionMaxLength, moveMode);
+    }
+  }
+
+  public void showScreenDescription(String sortKeyHeader) {
+    TerminalPrinter printer = getTerminalPrinter(SCREEN_DESCRIPTION_START_ROW);
+    printer.startBold().print("Fields Management").stopBold().endOfLine();
+    printer.print("Current Sort Field: ").startBold().print(sortKeyHeader).stopBold().endOfLine();
+    printer.print("Navigate with up/down, Right selects for move then <Enter> or Left commits,")
+      .endOfLine();
+    printer.print("'d' or <Space> toggles display, 's' sets sort. Use 'q' or <Esc> to end!")
+      .endOfLine();
+  }
+
+  public void showField(int pos, Field field, boolean display, boolean selected,
+    int fieldHeaderMaxLength, int fieldDescriptionMaxLength, boolean moveMode) {
+
+    String fieldHeader = String.format("%-" + fieldHeaderMaxLength + "s", field.getHeader());
+    String fieldDescription = String.format("%-" + fieldDescriptionMaxLength + "s",
+      field.getDescription());
+
+    int row = FIELD_START_ROW + pos;
+    TerminalPrinter printer = getTerminalPrinter(row);
+    if (selected) {
+      String prefix = display ? "* " : "  ";
+      if (moveMode) {
+        printer.print(prefix);
+
+        if (display) {
+          printer.startBold();
+        }
+
+        printer.startHighlight()
+          .printFormat("%s = %s", fieldHeader, fieldDescription).stopHighlight();
+
+        if (display) {
+          printer.stopBold();
+        }
+
+        printer.endOfLine();
+      } else {
+        printer.print(prefix);
+
+        if (display) {
+          printer.startBold();
+        }
+
+        printer.startHighlight().print(fieldHeader).stopHighlight()
+          .printFormat(" = %s", fieldDescription);
+
+        if (display) {
+          printer.stopBold();
+        }
+
+        printer.endOfLine();
+      }
+    } else {
+      if (display) {
+        printer.print("* ").startBold().printFormat("%s = %s", fieldHeader, fieldDescription)
+          .stopBold().endOfLine();
+      } else {
+        printer.printFormat("  %s = %s", fieldHeader, fieldDescription).endOfLine();
+      }
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/CommandDescription.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/CommandDescription.java
new file mode 100644
index 0000000..c77848f
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/CommandDescription.java
@@ -0,0 +1,52 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.help;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents a description of a command that we can execute in the top screen.
+ */
+@InterfaceAudience.Private
+public class CommandDescription {
+
+  private final List<String> keys;
+  private final String description;
+
+  public CommandDescription(String key, String description) {
+    this(Collections.singletonList(Objects.requireNonNull(key)), description);
+  }
+
+  public CommandDescription(List<String> keys, String description) {
+    this.keys = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(keys)));
+    this.description = Objects.requireNonNull(description);
+  }
+
+  public List<String> getKeys() {
+    return keys;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenPresenter.java
new file mode 100644
index 0000000..eafb852
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenPresenter.java
@@ -0,0 +1,72 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.help;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the help screen.
+ */
+@InterfaceAudience.Private
+public class HelpScreenPresenter {
+
+  private static final CommandDescription[] COMMAND_DESCRIPTIONS = new CommandDescription[] {
+    new CommandDescription("f", "Add/Remove/Order/Sort the fields"),
+    new CommandDescription("R", "Toggle the sort order (ascending/descending)"),
+    new CommandDescription("m", "Select mode"),
+    new CommandDescription("o", "Add a filter with ignoring case"),
+    new CommandDescription("O", "Add a filter with case sensitive"),
+    new CommandDescription("^o", "Show the current filters"),
+    new CommandDescription("=", "Clear the current filters"),
+    new CommandDescription("i", "Drill down"),
+    new CommandDescription(
+      Arrays.asList("up", "down", "left", "right", "pageUp", "pageDown", "home", "end"),
+      "Scroll the metrics"),
+    new CommandDescription("d", "Change the refresh delay"),
+    new CommandDescription("X", "Adjust the field length"),
+    new CommandDescription("<Enter>", "Refresh the display"),
+    new CommandDescription("h", "Display this screen"),
+    new CommandDescription(Arrays.asList("q", "<Esc>"), "Quit")
+  };
+
+  private final HelpScreenView helpScreenView;
+  private final long refreshDelay;
+  private final ScreenView nextScreenView;
+
+  public HelpScreenPresenter(HelpScreenView helpScreenView, long refreshDelay,
+    ScreenView nextScreenView) {
+    this.helpScreenView = Objects.requireNonNull(helpScreenView);
+    this.refreshDelay = refreshDelay;
+    this.nextScreenView = Objects.requireNonNull(nextScreenView);
+  }
+
+  public void init() {
+    helpScreenView.hideCursor();
+    helpScreenView.clearTerminal();
+    helpScreenView.showHelpScreen(refreshDelay, COMMAND_DESCRIPTIONS);
+    helpScreenView.refreshTerminal();
+  }
+
+  public ScreenView transitionToNextScreen() {
+    return nextScreenView;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenView.java
new file mode 100644
index 0000000..cf547ed
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/help/HelpScreenView.java
@@ -0,0 +1,89 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.help;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+
+/**
+ * The help screen.
+ */
+@InterfaceAudience.Private
+public class HelpScreenView extends AbstractScreenView {
+
+  private static final int SCREEN_DESCRIPTION_START_ROW = 0;
+  private static final int COMMAND_DESCRIPTION_START_ROW = 3;
+
+  private final HelpScreenPresenter helpScreenPresenter;
+
+  public HelpScreenView(Screen screen, Terminal terminal, long refreshDelay,
+    ScreenView nextScreenView) {
+    super(screen, terminal);
+    this.helpScreenPresenter = new HelpScreenPresenter(this, refreshDelay, nextScreenView);
+  }
+
+  @Override
+  public void init() {
+    helpScreenPresenter.init();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    return helpScreenPresenter.transitionToNextScreen();
+  }
+
+  public void showHelpScreen(long refreshDelay, CommandDescription[] commandDescriptions) {
+    showScreenDescription(refreshDelay);
+
+    TerminalPrinter printer = getTerminalPrinter(COMMAND_DESCRIPTION_START_ROW);
+    for (CommandDescription commandDescription : commandDescriptions) {
+      showCommandDescription(printer, commandDescription);
+    }
+
+    printer.endOfLine();
+    printer.print("Press any key to continue").endOfLine();
+  }
+
+  private void showScreenDescription(long refreshDelay) {
+    TerminalPrinter printer = getTerminalPrinter(SCREEN_DESCRIPTION_START_ROW);
+    printer.startBold().print("Help for Interactive Commands").stopBold().endOfLine();
+    printer.print("Refresh delay: ").startBold()
+      .print((double) refreshDelay / 1000).stopBold().endOfLine();
+  }
+
+  private void showCommandDescription(TerminalPrinter terminalPrinter,
+    CommandDescription commandDescription) {
+    terminalPrinter.print("  ");
+    boolean first = true;
+    for (String key : commandDescription.getKeys()) {
+      if (first) {
+        first = false;
+      } else {
+        terminalPrinter.print(",");
+      }
+      terminalPrinter.startBold().print(key).stopBold();
+    }
+
+    terminalPrinter.printFormat(": %s", commandDescription.getDescription()).endOfLine();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenPresenter.java
new file mode 100644
index 0000000..672b556
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenPresenter.java
@@ -0,0 +1,134 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.mode;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the mode screen.
+ */
+@InterfaceAudience.Private
+public class ModeScreenPresenter {
+
+  public interface ResultListener {
+    void accept(Mode mode);
+  }
+
+  private final ModeScreenView modeScreenView;
+  private final Mode currentMode;
+  private final ResultListener resultListener;
+  private final ScreenView nextScreenView;
+
+  private final int modeHeaderMaxLength;
+  private final int modeDescriptionMaxLength;
+  private final List<Mode> modes = Arrays.asList(Mode.values());
+
+  private int currentPosition;
+
+  public ModeScreenPresenter(ModeScreenView modeScreenView, Mode currentMode,
+    ResultListener resultListener, ScreenView nextScreenView) {
+    this.modeScreenView = Objects.requireNonNull(modeScreenView);
+    this.currentMode = Objects.requireNonNull(currentMode);
+    this.resultListener = Objects.requireNonNull(resultListener);
+    this.nextScreenView = Objects.requireNonNull(nextScreenView);
+
+    int modeHeaderLength = 0;
+    int modeDescriptionLength = 0;
+    for (int i = 0; i < modes.size(); i++) {
+      Mode mode = modes.get(i);
+      if (mode == currentMode) {
+        currentPosition = i;
+      }
+
+      if (modeHeaderLength < mode.getHeader().length()) {
+        modeHeaderLength = mode.getHeader().length();
+      }
+
+      if (modeDescriptionLength < mode.getDescription().length()) {
+        modeDescriptionLength = mode.getDescription().length();
+      }
+    }
+
+    modeHeaderMaxLength = modeHeaderLength;
+    modeDescriptionMaxLength = modeDescriptionLength;
+  }
+
+  public void init() {
+    modeScreenView.hideCursor();
+    modeScreenView.clearTerminal();
+    modeScreenView.showModeScreen(currentMode, modes, currentPosition, modeHeaderMaxLength,
+      modeDescriptionMaxLength);
+    modeScreenView.refreshTerminal();
+  }
+
+  public void arrowUp() {
+    if (currentPosition > 0) {
+      currentPosition -= 1;
+      showMode(currentPosition);
+      showMode(currentPosition + 1);
+      modeScreenView.refreshTerminal();
+    }
+  }
+
+  public void arrowDown() {
+    if (currentPosition < modes.size() - 1) {
+      currentPosition += 1;
+      showMode(currentPosition);
+      showMode(currentPosition - 1);
+      modeScreenView.refreshTerminal();
+    }
+  }
+
+  public void pageUp() {
+    if (currentPosition > 0) {
+      int previousPosition = currentPosition;
+      currentPosition = 0;
+      showMode(previousPosition);
+      showMode(currentPosition);
+      modeScreenView.refreshTerminal();
+    }
+  }
+
+  public void pageDown() {
+    if (currentPosition < modes.size() - 1) {
+      int previousPosition = currentPosition;
+      currentPosition = modes.size() - 1;
+      showMode(previousPosition);
+      showMode(currentPosition);
+      modeScreenView.refreshTerminal();
+    }
+  }
+
+  private void showMode(int pos) {
+    modeScreenView.showMode(pos, modes.get(pos), pos == currentPosition, modeHeaderMaxLength,
+      modeDescriptionMaxLength);
+  }
+
+  public ScreenView transitionToNextScreen(boolean changeMode) {
+    Mode selectedMode = modes.get(currentPosition);
+    if (changeMode && currentMode != selectedMode) {
+      resultListener.accept(selectedMode);
+    }
+    return nextScreenView;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenView.java
new file mode 100644
index 0000000..57b3b3a
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/mode/ModeScreenView.java
@@ -0,0 +1,136 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.mode;
+
+import java.util.List;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+
+/**
+ * The screen where we can choose the {@link Mode} in the top screen.
+ */
+@InterfaceAudience.Private
+public class ModeScreenView extends AbstractScreenView {
+
+  private static final int SCREEN_DESCRIPTION_START_ROW = 0;
+  private static final int MODE_START_ROW = 4;
+
+  private final ModeScreenPresenter modeScreenPresenter;
+
+  public ModeScreenView(Screen screen, Terminal terminal, Mode currentMode,
+    ModeScreenPresenter.ResultListener resultListener, ScreenView nextScreenView) {
+    super(screen, terminal);
+    this.modeScreenPresenter = new ModeScreenPresenter(this, currentMode, resultListener,
+      nextScreenView);
+  }
+
+  @Override
+  public void init() {
+    modeScreenPresenter.init();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    switch (keyPress.getType()) {
+      case Escape:
+        return modeScreenPresenter.transitionToNextScreen(false);
+
+      case Enter:
+        return modeScreenPresenter.transitionToNextScreen(true);
+
+      case ArrowUp:
+        modeScreenPresenter.arrowUp();
+        return this;
+
+      case ArrowDown:
+        modeScreenPresenter.arrowDown();
+        return this;
+
+      case PageUp:
+      case Home:
+        modeScreenPresenter.pageUp();
+        return this;
+
+      case PageDown:
+      case End:
+        modeScreenPresenter.pageDown();
+        return this;
+
+      default:
+        // Do nothing
+        break;
+    }
+
+    if (keyPress.getType() != KeyPress.Type.Character) {
+      return this;
+    }
+
+    assert keyPress.getCharacter() != null;
+    switch (keyPress.getCharacter()) {
+      case 'q':
+        return modeScreenPresenter.transitionToNextScreen(false);
+
+      default:
+        // Do nothing
+        break;
+    }
+
+    return this;
+  }
+
+  public void showModeScreen(Mode currentMode, List<Mode> modes, int currentPosition,
+    int modeHeaderMaxLength, int modeDescriptionMaxLength) {
+    showScreenDescription(currentMode);
+
+    for (int i = 0; i < modes.size(); i++) {
+      showMode(i, modes.get(i), i == currentPosition,
+        modeHeaderMaxLength, modeDescriptionMaxLength);
+    }
+  }
+
+  private void showScreenDescription(Mode currentMode) {
+    TerminalPrinter printer = getTerminalPrinter(SCREEN_DESCRIPTION_START_ROW);
+    printer.startBold().print("Mode Management").stopBold().endOfLine();
+    printer.print("Current mode: ")
+      .startBold().print(currentMode.getHeader()).stopBold().endOfLine();
+    printer.print("Select mode followed by <Enter>").endOfLine();
+  }
+
+  public void showMode(int pos, Mode mode, boolean selected, int modeHeaderMaxLength,
+    int modeDescriptionMaxLength) {
+
+    String modeHeader = String.format("%-" + modeHeaderMaxLength + "s", mode.getHeader());
+    String modeDescription = String.format("%-" + modeDescriptionMaxLength + "s",
+      mode.getDescription());
+
+    int row = MODE_START_ROW + pos;
+    TerminalPrinter printer = getTerminalPrinter(row);
+    if (selected) {
+      printer.startHighlight().print(modeHeader).stopHighlight()
+        .printFormat(" = %s", modeDescription).endOfLine();
+    } else {
+      printer.printFormat("%s = %s", modeHeader, modeDescription).endOfLine();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenPresenter.java
new file mode 100644
index 0000000..25ba608
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenPresenter.java
@@ -0,0 +1,53 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the filter display mode.
+ */
+@InterfaceAudience.Private
+public class FilterDisplayModeScreenPresenter {
+
+  private final FilterDisplayModeScreenView filterDisplayModeScreenView;
+  private final List<RecordFilter> filters;
+  private final ScreenView nextScreenView;
+
+  public FilterDisplayModeScreenPresenter(FilterDisplayModeScreenView filterDisplayModeScreenView,
+    List<RecordFilter> filters, ScreenView nextScreenView) {
+    this.filterDisplayModeScreenView = Objects.requireNonNull(filterDisplayModeScreenView);
+    this.filters = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(filters)));
+    this.nextScreenView = Objects.requireNonNull(nextScreenView);
+  }
+
+  public void init() {
+    filterDisplayModeScreenView.showFilters(filters);
+    filterDisplayModeScreenView.refreshTerminal();
+  }
+
+  public ScreenView returnToNextScreen() {
+    return nextScreenView;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenView.java
new file mode 100644
index 0000000..42eeb97
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/FilterDisplayModeScreenView.java
@@ -0,0 +1,77 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+
+/**
+ * The filter display mode in the top screen.
+ *
+ * Exit if Enter key is pressed.
+ */
+@InterfaceAudience.Private
+public class FilterDisplayModeScreenView extends AbstractScreenView {
+
+  private final int row;
+  private final FilterDisplayModeScreenPresenter filterDisplayModeScreenPresenter;
+
+  public FilterDisplayModeScreenView(Screen screen, Terminal terminal, int row,
+    List<RecordFilter> filters, ScreenView nextScreenView) {
+    super(screen, terminal);
+    this.row = row;
+    this.filterDisplayModeScreenPresenter =
+      new FilterDisplayModeScreenPresenter(this, filters, nextScreenView);
+  }
+
+  @Override
+  public void init() {
+    filterDisplayModeScreenPresenter.init();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    if (keyPress.getType() == KeyPress.Type.Enter) {
+      return filterDisplayModeScreenPresenter.returnToNextScreen();
+    }
+    return this;
+  }
+
+  public void showFilters(List<RecordFilter> filters) {
+    String filtersString = "none";
+    if (!filters.isEmpty()) {
+      List<String> filterStrings = new ArrayList<>();
+      for (RecordFilter filter : filters) {
+        filterStrings.add(String.format("'%s'", filter));
+      }
+      filtersString = StringUtils.join(filterStrings, " + ");
+    }
+
+    getTerminalPrinter(row).startBold().print("<Enter> to resume, filters: " + filtersString)
+      .stopBold().endOfLine();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Header.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Header.java
new file mode 100644
index 0000000..bb7230d
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Header.java
@@ -0,0 +1,48 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+
+/**
+ * Represents headers for the metrics in the top screen.
+ */
+@InterfaceAudience.Private
+public class Header {
+  private final Field field;
+  private final int length;
+
+  public Header(Field field, int length) {
+    this.field = Objects.requireNonNull(field);
+    this.length = length;
+  }
+
+  public String format() {
+    return "%" + (field.isLeftJustify() ? "-" : "")  + length + "s";
+  }
+
+  public Field getField() {
+    return field;
+  }
+
+  public int getLength() {
+    return length;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenPresenter.java
new file mode 100644
index 0000000..33ec96f
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenPresenter.java
@@ -0,0 +1,168 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the input mode.
+ */
+@InterfaceAudience.Private
+public class InputModeScreenPresenter {
+
+  public interface ResultListener {
+    public ScreenView apply(String inputString);
+  }
+
+  private final InputModeScreenView inputModeScreenView;
+  private final String message;
+  private final List<String> histories;
+  private final ResultListener resultListener;
+
+  private StringBuilder inputString = new StringBuilder();
+  private int cursorPosition;
+  private int historyPosition = -1;
+
+  public InputModeScreenPresenter(InputModeScreenView inputModeScreenView, String message,
+    @Nullable List<String> histories, ResultListener resultListener) {
+    this.inputModeScreenView = Objects.requireNonNull(inputModeScreenView);
+    this.message = Objects.requireNonNull(message);
+
+    if (histories != null) {
+      this.histories = Collections.unmodifiableList(new ArrayList<>(histories));
+    } else {
+      this.histories = Collections.emptyList();
+    }
+
+    this.resultListener = Objects.requireNonNull(resultListener);
+  }
+
+  public void init() {
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public ScreenView returnToNextScreen() {
+    inputModeScreenView.hideCursor();
+    String result = inputString.toString();
+
+    return resultListener.apply(result);
+  }
+
+  public void character(Character character) {
+    inputString.insert(cursorPosition, character);
+    cursorPosition += 1;
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void backspace() {
+    if (cursorPosition == 0) {
+      return;
+    }
+
+    inputString.deleteCharAt(cursorPosition - 1);
+    cursorPosition -= 1;
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void delete() {
+    if (inputString.length() == 0 || cursorPosition > inputString.length() - 1) {
+      return;
+    }
+
+    inputString.deleteCharAt(cursorPosition);
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void arrowLeft() {
+    if (cursorPosition == 0) {
+      return;
+    }
+
+    cursorPosition -= 1;
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void arrowRight() {
+    if (cursorPosition > inputString.length() - 1) {
+      return;
+    }
+
+    cursorPosition += 1;
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void home() {
+    cursorPosition = 0;
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void end() {
+    cursorPosition = inputString.length();
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void arrowUp() {
+    if (historyPosition == 0 || histories.isEmpty()) {
+      return;
+    }
+
+    if (historyPosition == -1) {
+      historyPosition = histories.size() - 1;
+    } else {
+      historyPosition -= 1;
+    }
+
+    inputString = new StringBuilder(histories.get(historyPosition));
+
+    cursorPosition = inputString.length();
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+
+  public void arrowDown() {
+    if (historyPosition == -1 || histories.isEmpty()) {
+      return;
+    }
+
+    if (historyPosition == histories.size() - 1) {
+      historyPosition = -1;
+      inputString = new StringBuilder();
+    } else {
+      historyPosition += 1;
+      inputString = new StringBuilder(histories.get(historyPosition));
+    }
+
+    cursorPosition = inputString.length();
+    inputModeScreenView.showInput(message, inputString.toString(), cursorPosition);
+    inputModeScreenView.refreshTerminal();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenView.java
new file mode 100644
index 0000000..ad5847a
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/InputModeScreenView.java
@@ -0,0 +1,105 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import java.util.List;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+
+/**
+ * The input mode in the top screen.
+ */
+@InterfaceAudience.Private
+public class InputModeScreenView extends AbstractScreenView {
+
+  private final int row;
+  private final InputModeScreenPresenter inputModeScreenPresenter;
+
+  public InputModeScreenView(Screen screen, Terminal terminal, int row, String message,
+    List<String> histories, InputModeScreenPresenter.ResultListener resultListener) {
+    super(screen, terminal);
+    this.row = row;
+    this.inputModeScreenPresenter = new InputModeScreenPresenter(this, message, histories,
+      resultListener);
+  }
+
+  @Override
+  public void init() {
+    inputModeScreenPresenter.init();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+
+    switch (keyPress.getType()) {
+      case Enter:
+        return inputModeScreenPresenter.returnToNextScreen();
+
+      case Character:
+        inputModeScreenPresenter.character(keyPress.getCharacter());
+        break;
+
+      case Backspace:
+        inputModeScreenPresenter.backspace();
+        break;
+
+      case Delete:
+        inputModeScreenPresenter.delete();
+        break;
+
+      case ArrowLeft:
+        inputModeScreenPresenter.arrowLeft();
+        break;
+
+      case ArrowRight:
+        inputModeScreenPresenter.arrowRight();
+        break;
+
+      case Home:
+        inputModeScreenPresenter.home();
+        break;
+
+      case End:
+        inputModeScreenPresenter.end();
+        break;
+
+      case ArrowUp:
+        inputModeScreenPresenter.arrowUp();
+        break;
+
+      case ArrowDown:
+        inputModeScreenPresenter.arrowDown();
+        break;
+
+      default:
+        break;
+    }
+    return this;
+  }
+
+  public void showInput(String message, String inputString, int cursorPosition) {
+    getTerminalPrinter(row).startBold().print(message).stopBold().print(" ").print(inputString)
+      .endOfLine();
+    setCursorPosition(message.length() + 1 + cursorPosition, row);
+    refreshTerminal();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenPresenter.java
new file mode 100644
index 0000000..419afbc
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenPresenter.java
@@ -0,0 +1,51 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+
+/**
+ * The presentation logic for the message mode.
+ *
+ * Exit after 2 seconds or if any key is pressed.
+ */
+@InterfaceAudience.Private
+public class MessageModeScreenPresenter {
+
+  private final MessageModeScreenView messageModeScreenView;
+  private final String message;
+  private final ScreenView nextScreenView;
+
+  public MessageModeScreenPresenter(MessageModeScreenView messageModeScreenView, String message,
+    ScreenView nextScreenView) {
+    this.messageModeScreenView = Objects.requireNonNull(messageModeScreenView);
+    this.message = Objects.requireNonNull(message);
+    this.nextScreenView = Objects.requireNonNull(nextScreenView);
+  }
+
+  public void init() {
+    messageModeScreenView.showMessage(message);
+    messageModeScreenView.refreshTerminal();
+  }
+
+  public ScreenView returnToNextScreen() {
+    return nextScreenView;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenView.java
new file mode 100644
index 0000000..955b145
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/MessageModeScreenView.java
@@ -0,0 +1,65 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+
+/**
+ * The message mode in the top screen.
+ */
+@InterfaceAudience.Private
+public class MessageModeScreenView extends AbstractScreenView {
+
+  private final int row;
+  private final MessageModeScreenPresenter messageModeScreenPresenter;
+
+  public MessageModeScreenView(Screen screen, Terminal terminal, int row, String message,
+    ScreenView nextScreenView) {
+    super(screen, terminal);
+    this.row = row;
+    this.messageModeScreenPresenter =
+      new MessageModeScreenPresenter(this, message, nextScreenView);
+  }
+
+  @Override
+  public void init() {
+    messageModeScreenPresenter.init();
+    setTimer(2000);
+  }
+
+  @Override
+  public ScreenView handleTimer() {
+    return messageModeScreenPresenter.returnToNextScreen();
+  }
+
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    cancelTimer();
+    return messageModeScreenPresenter.returnToNextScreen();
+  }
+
+  public void showMessage(String message) {
+    getTerminalPrinter(row).startHighlight().print(" ").print(message).print(" ").stopHighlight()
+      .endOfLine();
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Paging.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Paging.java
new file mode 100644
index 0000000..276d3e3
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Paging.java
@@ -0,0 +1,151 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Utility class for paging for the metrics.
+ */
+@InterfaceAudience.Private
+public class Paging {
+  private int currentPosition;
+  private int pageStartPosition;
+  private int pageEndPosition;
+
+  private int pageSize;
+  private int recordsSize;
+
+  public void init() {
+    currentPosition = 0;
+    pageStartPosition = 0;
+    pageEndPosition = Math.min(pageSize, recordsSize);
+  }
+
+  public void updatePageSize(int pageSize) {
+    this.pageSize = pageSize;
+
+    if (pageSize == 0) {
+      pageStartPosition = 0;
+      pageEndPosition = 0;
+    } else {
+      pageEndPosition = pageStartPosition + pageSize;
+      keepConsistent();
+    }
+  }
+
+  public void updateRecordsSize(int recordsSize) {
+    if (this.recordsSize == 0) {
+      currentPosition = 0;
+      pageStartPosition = 0;
+      pageEndPosition = Math.min(pageSize, recordsSize);
+      this.recordsSize = recordsSize;
+    } else if (recordsSize == 0) {
+      currentPosition = 0;
+      pageStartPosition = 0;
+      pageEndPosition = 0;
+      this.recordsSize = recordsSize;
+    } else {
+      this.recordsSize = recordsSize;
+      if (pageSize > 0) {
+        pageEndPosition = pageStartPosition + pageSize;
+        keepConsistent();
+      }
+    }
+  }
+
+  public void arrowUp() {
+    if (currentPosition > 0) {
+      currentPosition -= 1;
+      if (pageSize > 0) {
+        keepConsistent();
+      }
+    }
+  }
+
+  public void arrowDown() {
+    if (currentPosition < recordsSize - 1) {
+      currentPosition += 1;
+      if (pageSize > 0) {
+        keepConsistent();
+      }
+    }
+  }
+
+  public void pageUp() {
+    if (pageSize > 0 && currentPosition > 0) {
+      currentPosition -= pageSize;
+      if (currentPosition < 0) {
+        currentPosition = 0;
+      }
+      keepConsistent();
+    }
+  }
+
+  public void pageDown() {
+    if (pageSize > 0 && currentPosition < recordsSize - 1) {
+
+      currentPosition = currentPosition + pageSize;
+      if (currentPosition >= recordsSize) {
+        currentPosition = recordsSize - 1;
+      }
+
+      pageStartPosition = currentPosition;
+      pageEndPosition = pageStartPosition + pageSize;
+      keepConsistent();
+    }
+  }
+
+  private void keepConsistent() {
+    if (currentPosition < pageStartPosition) {
+      pageStartPosition = currentPosition;
+      pageEndPosition = pageStartPosition + pageSize;
+    } else if (currentPosition > recordsSize - 1) {
+      currentPosition = recordsSize - 1;
+      pageEndPosition = recordsSize;
+      pageStartPosition = pageEndPosition - pageSize;
+    } else if (currentPosition > pageEndPosition - 1) {
+      pageEndPosition = currentPosition + 1;
+      pageStartPosition = pageEndPosition - pageSize;
+    }
+
+    if (pageStartPosition < 0) {
+      pageStartPosition = 0;
+    }
+
+    if (pageEndPosition > recordsSize) {
+      pageEndPosition = recordsSize;
+      pageStartPosition = pageEndPosition - pageSize;
+      if (pageStartPosition < 0) {
+        pageStartPosition = 0;
+      }
+    }
+  }
+
+  public int getCurrentPosition() {
+    return currentPosition;
+  }
+
+  public int getPageStartPosition() {
+    return pageStartPosition;
+  }
+
+  public int getPageEndPosition() {
+    return pageEndPosition;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Summary.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Summary.java
new file mode 100644
index 0000000..a3feff6
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/Summary.java
@@ -0,0 +1,93 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import java.util.Objects;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents the summary of the metrics.
+ */
+@InterfaceAudience.Private
+public class Summary {
+  private final String currentTime;
+  private final String version;
+  private final String clusterId;
+  private final int servers;
+  private final int liveServers;
+  private final int deadServers;
+  private final int regionCount;
+  private final int ritCount;
+  private final double averageLoad;
+  private final long aggregateRequestPerSecond;
+
+  public Summary(String currentTime, String version, String clusterId, int servers,
+    int liveServers, int deadServers, int regionCount, int ritCount, double averageLoad,
+    long aggregateRequestPerSecond) {
+    this.currentTime = Objects.requireNonNull(currentTime);
+    this.version = Objects.requireNonNull(version);
+    this.clusterId = Objects.requireNonNull(clusterId);
+    this.servers = servers;
+    this.liveServers = liveServers;
+    this.deadServers = deadServers;
+    this.regionCount = regionCount;
+    this.ritCount = ritCount;
+    this.averageLoad = averageLoad;
+    this.aggregateRequestPerSecond = aggregateRequestPerSecond;
+  }
+
+  public String getCurrentTime() {
+    return currentTime;
+  }
+
+  public String getVersion() {
+    return version;
+  }
+
+  public String getClusterId() {
+    return clusterId;
+  }
+
+  public int getServers() {
+    return servers;
+  }
+
+  public int getLiveServers() {
+    return liveServers;
+  }
+
+  public int getDeadServers() {
+    return deadServers;
+  }
+
+  public int getRegionCount() {
+    return regionCount;
+  }
+
+  public int getRitCount() {
+    return ritCount;
+  }
+
+  public double getAverageLoad() {
+    return averageLoad;
+  }
+
+  public long getAggregateRequestPerSecond() {
+    return aggregateRequestPerSecond;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenModel.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenModel.java
new file mode 100644
index 0000000..f795c4a
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenModel.java
@@ -0,0 +1,235 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import org.apache.commons.lang3.time.DateFormatUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.ServerLoad;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.field.FieldValue;
+import org.apache.hadoop.hbase.hbtop.mode.DrillDownInfo;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+
+/**
+ * The data and business logic for the top screen.
+ */
+@InterfaceAudience.Private
+public class TopScreenModel {
+
+  private static final Log LOG = LogFactory.getLog(TopScreenModel.class);
+
+  private final Admin admin;
+
+  private Mode currentMode;
+  private Field currentSortField;
+  private List<FieldInfo> fieldInfos;
+  private List<Field> fields;
+
+  private Summary summary;
+  private List<Record> records;
+
+  private final List<RecordFilter> filters = new ArrayList<>();
+  private final List<String> filterHistories = new ArrayList<>();
+
+  private boolean ascendingSort;
+
+  public TopScreenModel(Admin admin, Mode initialMode) {
+    this.admin = Objects.requireNonNull(admin);
+    switchMode(Objects.requireNonNull(initialMode), null, false);
+  }
+
+  public void switchMode(Mode nextMode, List<RecordFilter> initialFilters,
+    boolean keepSortFieldAndSortOrderIfPossible) {
+
+    currentMode = nextMode;
+    fieldInfos = Collections.unmodifiableList(new ArrayList<>(currentMode.getFieldInfos()));
+
+    fields = new ArrayList<>();
+    for (FieldInfo fieldInfo : currentMode.getFieldInfos()) {
+      fields.add(fieldInfo.getField());
+    }
+    fields = Collections.unmodifiableList(fields);
+
+    if (keepSortFieldAndSortOrderIfPossible) {
+      boolean match = false;
+      for (Field field : fields) {
+        if (field == currentSortField) {
+          match = true;
+          break;
+        }
+      }
+
+      if (!match) {
+        currentSortField = nextMode.getDefaultSortField();
+        ascendingSort = false;
+      }
+
+    } else {
+      currentSortField = nextMode.getDefaultSortField();
+      ascendingSort = false;
+    }
+
+    clearFilters();
+    if (initialFilters != null) {
+      filters.addAll(initialFilters);
+    }
+  }
+
+  public void setSortFieldAndFields(Field sortField, List<Field> fields) {
+    this.currentSortField = sortField;
+    this.fields = Collections.unmodifiableList(new ArrayList<>(fields));
+  }
+
+  /*
+   * HBTop only calls this from a single thread, and if that ever changes, this needs
+   * synchronization
+   */
+  public void refreshMetricsData() {
+    ClusterStatus clusterStatus;
+    try {
+      clusterStatus = admin.getClusterStatus();
+    } catch (Exception e) {
+      LOG.error("Unable to get cluster status", e);
+      return;
+    }
+
+    refreshSummary(clusterStatus);
+    refreshRecords(clusterStatus);
+  }
+
+  private void refreshSummary(ClusterStatus clusterStatus) {
+    String currentTime = DateFormatUtils.ISO_8601_EXTENDED_TIME_FORMAT
+      .format(System.currentTimeMillis());
+    String version = clusterStatus.getHBaseVersion();
+    String clusterId = clusterStatus.getClusterId();
+    int liveServers = clusterStatus.getServersSize();
+    int deadServers = clusterStatus.getDeadServerNames().size();
+    int regionCount = clusterStatus.getRegionsCount();
+    int ritCount = clusterStatus.getRegionsInTransition().size();
+    double averageLoad = clusterStatus.getAverageLoad();
+    long aggregateRequestPerSecond = 0;
+    for (ServerName sn: clusterStatus.getServers()) {
+      ServerLoad sl = clusterStatus.getLoad(sn);
+      aggregateRequestPerSecond += sl.getNumberOfRequests();
+    }
+    summary = new Summary(currentTime, version, clusterId, liveServers + deadServers,
+      liveServers, deadServers, regionCount, ritCount, averageLoad, aggregateRequestPerSecond);
+  }
+
+  private void refreshRecords(ClusterStatus clusterStatus) {
+    // Filter
+    List<Record> records = new ArrayList<>();
+    for (Record record : currentMode.getRecords(clusterStatus)) {
+      boolean filter = false;
+      for (RecordFilter recordFilter : filters) {
+        if (!recordFilter.execute(record)) {
+          filter = true;
+          break;
+        }
+      }
+      if (!filter) {
+        records.add(record);
+      }
+    }
+
+    // Sort
+    Collections.sort(records, new Comparator<Record>() {
+      @Override
+      public int compare(Record recordLeft, Record recordRight) {
+        FieldValue left = recordLeft.get(currentSortField);
+        FieldValue right = recordRight.get(currentSortField);
+        return (ascendingSort ? 1 : -1) * left.compareTo(right);
+      }
+    });
+
+    this.records = Collections.unmodifiableList(records);
+  }
+
+  public void switchSortOrder() {
+    ascendingSort = !ascendingSort;
+  }
+
+  public boolean addFilter(String filterString, boolean ignoreCase) {
+    RecordFilter filter = RecordFilter.parse(filterString, fields, ignoreCase);
+    if (filter == null) {
+      return false;
+    }
+
+    filters.add(filter);
+    filterHistories.add(filterString);
+    return true;
+  }
+
+  public void clearFilters() {
+    filters.clear();
+  }
+
+  public boolean drillDown(Record selectedRecord) {
+    DrillDownInfo drillDownInfo = currentMode.drillDown(selectedRecord);
+    if (drillDownInfo == null) {
+      return false;
+    }
+    switchMode(drillDownInfo.getNextMode(), drillDownInfo.getInitialFilters(), true);
+    return true;
+  }
+
+  public Mode getCurrentMode() {
+    return currentMode;
+  }
+
+  public Field getCurrentSortField() {
+    return currentSortField;
+  }
+
+  public List<FieldInfo> getFieldInfos() {
+    return fieldInfos;
+  }
+
+  public List<Field> getFields() {
+    return fields;
+  }
+
+  public Summary getSummary() {
+    return summary;
+  }
+
+  public List<Record> getRecords() {
+    return records;
+  }
+
+  public List<RecordFilter> getFilters() {
+    return Collections.unmodifiableList(filters);
+  }
+
+  public List<String> getFilterHistories() {
+    return Collections.unmodifiableList(filterHistories);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenPresenter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenPresenter.java
new file mode 100644
index 0000000..02c35b8
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenPresenter.java
@@ -0,0 +1,356 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.field.FieldScreenPresenter;
+import org.apache.hadoop.hbase.hbtop.screen.field.FieldScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.help.HelpScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.mode.ModeScreenPresenter;
+import org.apache.hadoop.hbase.hbtop.screen.mode.ModeScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
+
+/**
+ * The presentation logic for the top screen.
+ */
+@InterfaceAudience.Private
+public class TopScreenPresenter {
+  private final TopScreenView topScreenView;
+  private final AtomicLong refreshDelay;
+  private long lastRefreshTimestamp;
+
+  private final AtomicBoolean adjustFieldLength = new AtomicBoolean(true);
+  private final TopScreenModel topScreenModel;
+  private int terminalLength;
+  private int horizontalScroll;
+  private final Paging paging = new Paging();
+
+  private final EnumMap<Field, Boolean> fieldDisplayMap = new EnumMap<>(Field.class);
+  private final EnumMap<Field, Integer> fieldLengthMap = new EnumMap<>(Field.class);
+
+  public TopScreenPresenter(TopScreenView topScreenView, long initialRefreshDelay,
+    TopScreenModel topScreenModel) {
+    this.topScreenView = Objects.requireNonNull(topScreenView);
+    this.refreshDelay = new AtomicLong(initialRefreshDelay);
+    this.topScreenModel = Objects.requireNonNull(topScreenModel);
+
+    initFieldDisplayMapAndFieldLengthMap();
+  }
+
+  public void init() {
+    terminalLength = topScreenView.getTerminalSize().getColumns();
+    paging.updatePageSize(topScreenView.getPageSize());
+    topScreenView.hideCursor();
+  }
+
+  public long refresh(boolean force) {
+    if (!force) {
+      long delay = System.currentTimeMillis() - lastRefreshTimestamp;
+      if (delay < refreshDelay.get()) {
+        return refreshDelay.get() - delay;
+      }
+    }
+
+    TerminalSize newTerminalSize = topScreenView.doResizeIfNecessary();
+    if (newTerminalSize != null) {
+      terminalLength = newTerminalSize.getColumns();
+      paging.updatePageSize(topScreenView.getPageSize());
+      topScreenView.clearTerminal();
+    }
+
+    topScreenModel.refreshMetricsData();
+    paging.updateRecordsSize(topScreenModel.getRecords().size());
+
+    adjustFieldLengthIfNeeded();
+
+    topScreenView.showTopScreen(topScreenModel.getSummary(), getDisplayedHeaders(),
+      getDisplayedRecords(), getSelectedRecord());
+
+    topScreenView.refreshTerminal();
+
+    lastRefreshTimestamp = System.currentTimeMillis();
+    return refreshDelay.get();
+  }
+
+  public void adjustFieldLength() {
+    adjustFieldLength.set(true);
+    refresh(true);
+  }
+
+  private void adjustFieldLengthIfNeeded() {
+    if (adjustFieldLength.get()) {
+      adjustFieldLength.set(false);
+
+      for (Field f : topScreenModel.getFields()) {
+        if (f.isAutoAdjust()) {
+          int maxLength = 0;
+          for (Record record : topScreenModel.getRecords()) {
+            int length = record.get(f).asString().length();
+            maxLength = Math.max(length, maxLength);
+          }
+          fieldLengthMap.put(f, Math.max(maxLength, f.getHeader().length()));
+        }
+      }
+    }
+  }
+
+  private List<Header> getDisplayedHeaders() {
+    List<Field> displayFields = new ArrayList<>();
+    for (Field field : topScreenModel.getFields()) {
+      if (fieldDisplayMap.get(field)) {
+        displayFields.add(field);
+      }
+    }
+
+    if (displayFields.isEmpty()) {
+      horizontalScroll = 0;
+    } else if (horizontalScroll > displayFields.size() - 1) {
+      horizontalScroll = displayFields.size() - 1;
+    }
+
+    List<Header> ret = new ArrayList<>();
+
+    int length = 0;
+    for (int i = horizontalScroll; i < displayFields.size(); i++) {
+      Field field = displayFields.get(i);
+      int fieldLength = fieldLengthMap.get(field);
+
+      length += fieldLength + 1;
+      if (length > terminalLength) {
+        break;
+      }
+      ret.add(new Header(field, fieldLength));
+    }
+
+    return ret;
+  }
+
+  private List<Record> getDisplayedRecords() {
+    List<Record> ret = new ArrayList<>();
+    for (int i = paging.getPageStartPosition(); i < paging.getPageEndPosition(); i++) {
+      ret.add(topScreenModel.getRecords().get(i));
+    }
+    return ret;
+  }
+
+  private Record getSelectedRecord() {
+    if (topScreenModel.getRecords().isEmpty()) {
+      return null;
+    }
+    return topScreenModel.getRecords().get(paging.getCurrentPosition());
+  }
+
+  public void arrowUp() {
+    paging.arrowUp();
+    refresh(true);
+  }
+
+  public void arrowDown() {
+    paging.arrowDown();
+    refresh(true);
+  }
+
+  public void pageUp() {
+    paging.pageUp();
+    refresh(true);
+  }
+
+  public void pageDown() {
+    paging.pageDown();
+    refresh(true);
+  }
+
+  public void arrowLeft() {
+    if (horizontalScroll > 0) {
+      horizontalScroll -= 1;
+    }
+    refresh(true);
+  }
+
+  public void arrowRight() {
+    if (horizontalScroll < getHeaderSize() - 1) {
+      horizontalScroll += 1;
+    }
+    refresh(true);
+  }
+
+  public void home() {
+    if (horizontalScroll > 0) {
+      horizontalScroll = 0;
+    }
+    refresh(true);
+  }
+
+  public void end() {
+    int headerSize = getHeaderSize();
+    horizontalScroll = headerSize == 0 ? 0 : headerSize - 1;
+    refresh(true);
+  }
+
+  private int getHeaderSize() {
+    int size = 0;
+    for (Field field : topScreenModel.getFields()) {
+      if (fieldDisplayMap.get(field)) {
+        size++;
+      }
+    }
+    return size;
+  }
+
+  public void switchSortOrder() {
+    topScreenModel.switchSortOrder();
+    refresh(true);
+  }
+
+  public ScreenView transitionToHelpScreen(Screen screen, Terminal terminal) {
+    return new HelpScreenView(screen, terminal, refreshDelay.get(), topScreenView);
+  }
+
+  public ScreenView transitionToModeScreen(Screen screen, Terminal terminal) {
+    return new ModeScreenView(screen, terminal, topScreenModel.getCurrentMode(),
+      new ModeScreenPresenter.ResultListener() {
+        @Override
+        public void accept(Mode mode) {
+          switchMode(mode);
+        }
+      }, topScreenView);
+  }
+
+  public ScreenView transitionToFieldScreen(Screen screen, Terminal terminal) {
+    return new FieldScreenView(screen, terminal,
+      topScreenModel.getCurrentSortField(), topScreenModel.getFields(),
+      fieldDisplayMap,
+      new FieldScreenPresenter.ResultListener() {
+        @Override
+        public void accept(Field sortField, List<Field> fields,
+          EnumMap<Field, Boolean> fieldDisplayMap) {
+          topScreenModel.setSortFieldAndFields(sortField, fields);
+          TopScreenPresenter.this.fieldDisplayMap.clear();
+          TopScreenPresenter.this.fieldDisplayMap.putAll(fieldDisplayMap);
+        }
+      }, topScreenView);
+  }
+
+  private void switchMode(Mode nextMode) {
+    topScreenModel.switchMode(nextMode, null, false);
+    reset();
+  }
+
+  public void drillDown() {
+    Record selectedRecord = getSelectedRecord();
+    if (selectedRecord == null) {
+      return;
+    }
+    if (topScreenModel.drillDown(selectedRecord)) {
+      reset();
+      refresh(true);
+    }
+  }
+
+  private void reset() {
+    initFieldDisplayMapAndFieldLengthMap();
+    adjustFieldLength.set(true);
+    paging.init();
+    horizontalScroll = 0;
+    topScreenView.clearTerminal();
+  }
+
+  private void initFieldDisplayMapAndFieldLengthMap() {
+    fieldDisplayMap.clear();
+    fieldLengthMap.clear();
+    for (FieldInfo fieldInfo : topScreenModel.getFieldInfos()) {
+      fieldDisplayMap.put(fieldInfo.getField(), fieldInfo.isDisplayByDefault());
+      fieldLengthMap.put(fieldInfo.getField(), fieldInfo.getDefaultLength());
+    }
+  }
+
+  public ScreenView goToMessageMode(Screen screen, Terminal terminal, int row, String message) {
+    return new MessageModeScreenView(screen, terminal, row, message, topScreenView);
+  }
+
+  public ScreenView goToInputModeForRefreshDelay(final Screen screen, final Terminal terminal,
+    final int row) {
+    return new InputModeScreenView(screen, terminal, row,
+      "Change refresh delay from " + (double) refreshDelay.get() / 1000 + " to", null,
+      new InputModeScreenPresenter.ResultListener() {
+        @Override
+        public ScreenView apply(String inputString) {
+          if (inputString.isEmpty()) {
+            return topScreenView;
+          }
+
+          double delay;
+          try {
+            delay = Double.parseDouble(inputString);
+          } catch (NumberFormatException e) {
+            return goToMessageMode(screen, terminal, row, "Unacceptable floating point");
+          }
+
+          refreshDelay.set((long) (delay * 1000));
+          return topScreenView;
+        }
+      });
+  }
+
+  public ScreenView goToInputModeForFilter(final Screen screen, final Terminal terminal,
+    final int row, final boolean ignoreCase) {
+    return new InputModeScreenView(screen, terminal, row,
+      "add filter #" + (topScreenModel.getFilters().size() + 1) +
+        " (" + (ignoreCase ? "ignoring case" : "case sensitive") + ") as: [!]FLD?VAL",
+      topScreenModel.getFilterHistories(),
+      new InputModeScreenPresenter.ResultListener() {
+        @Override
+        public ScreenView apply(String inputString) {
+          if (inputString.isEmpty()) {
+            return topScreenView;
+          }
+
+          if (!topScreenModel.addFilter(inputString, ignoreCase)) {
+            return goToMessageMode(screen, terminal, row, "Unacceptable filter expression");
+          }
+
+          paging.init();
+          return topScreenView;
+        }
+      });
+  }
+
+  public void clearFilters() {
+    topScreenModel.clearFilters();
+    paging.init();
+    refresh(true);
+  }
+
+  public ScreenView goToFilterDisplayMode(Screen screen, Terminal terminal, int row) {
+    return new FilterDisplayModeScreenView(screen, terminal, row, topScreenModel.getFilters(),
+      topScreenView);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenView.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenView.java
new file mode 100644
index 0000000..6d9348b
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/screen/top/TopScreenView.java
@@ -0,0 +1,308 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.AbstractScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.Screen;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
+
+/**
+ * The screen that provides a dynamic real-time view for the HBase metrics.
+ *
+ * This shows the metric {@link Summary} and the metric {@link Record}s. The summary and the
+ * metrics are updated periodically (3 seconds by default).
+ */
+@InterfaceAudience.Private
+public class TopScreenView extends AbstractScreenView {
+
+  private static final int SUMMARY_START_ROW = 0;
+  private static final int SUMMARY_ROW_NUM = 7;
+  private static final int MESSAGE_ROW = 7;
+  private static final int RECORD_HEADER_ROW = 8;
+  private static final int RECORD_START_ROW = 9;
+
+  private final TopScreenPresenter topScreenPresenter;
+  private int pageSize;
+
+  public TopScreenView(Screen screen, Terminal terminal, long initialRefreshDelay, Admin admin,
+    Mode initialMode) {
+    super(screen, terminal);
+    this.topScreenPresenter = new TopScreenPresenter(this, initialRefreshDelay,
+      new TopScreenModel(admin, initialMode));
+  }
+
+  @Override
+  public void init() {
+    topScreenPresenter.init();
+    long delay = topScreenPresenter.refresh(true);
+    setTimer(delay);
+  }
+
+  @Override
+  public ScreenView handleTimer() {
+    long delay = topScreenPresenter.refresh(false);
+    setTimer(delay);
+    return this;
+  }
+
+  @Nullable
+  @Override
+  public ScreenView handleKeyPress(KeyPress keyPress) {
+    switch (keyPress.getType()) {
+      case Enter:
+        topScreenPresenter.refresh(true);
+        return this;
+
+      case ArrowUp:
+        topScreenPresenter.arrowUp();
+        return this;
+
+      case ArrowDown:
+        topScreenPresenter.arrowDown();
+        return this;
+
+      case ArrowLeft:
+        topScreenPresenter.arrowLeft();
+        return this;
+
+      case ArrowRight:
+        topScreenPresenter.arrowRight();
+        return this;
+
+      case PageUp:
+        topScreenPresenter.pageUp();
+        return this;
+
+      case PageDown:
+        topScreenPresenter.pageDown();
+        return this;
+
+      case Home:
+        topScreenPresenter.home();
+        return this;
+
+      case End:
+        topScreenPresenter.end();
+        return this;
+
+      case Escape:
+        return null;
+
+      default:
+        // Do nothing
+        break;
+    }
+
+    if (keyPress.getType() != KeyPress.Type.Character) {
+      return unknownCommandMessage();
+    }
+
+    assert keyPress.getCharacter() != null;
+    switch (keyPress.getCharacter()) {
+      case 'R':
+        topScreenPresenter.switchSortOrder();
+        break;
+
+      case 'f':
+        cancelTimer();
+        return topScreenPresenter.transitionToFieldScreen(getScreen(), getTerminal());
+
+      case 'm':
+        cancelTimer();
+        return topScreenPresenter.transitionToModeScreen(getScreen(), getTerminal());
+
+      case 'h':
+        cancelTimer();
+        return topScreenPresenter.transitionToHelpScreen(getScreen(), getTerminal());
+
+      case 'd':
+        cancelTimer();
+        return topScreenPresenter.goToInputModeForRefreshDelay(getScreen(), getTerminal(),
+          MESSAGE_ROW);
+
+      case 'o':
+        cancelTimer();
+        if (keyPress.isCtrl()) {
+          return topScreenPresenter.goToFilterDisplayMode(getScreen(), getTerminal(), MESSAGE_ROW);
+        }
+        return topScreenPresenter.goToInputModeForFilter(getScreen(), getTerminal(), MESSAGE_ROW,
+          true);
+
+      case 'O':
+        cancelTimer();
+        return topScreenPresenter.goToInputModeForFilter(getScreen(), getTerminal(), MESSAGE_ROW,
+          false);
+
+      case '=':
+        topScreenPresenter.clearFilters();
+        break;
+
+      case 'X':
+        topScreenPresenter.adjustFieldLength();
+        break;
+
+      case 'i':
+        topScreenPresenter.drillDown();
+        break;
+
+      case 'q':
+        return null;
+
+      default:
+        return unknownCommandMessage();
+    }
+    return this;
+  }
+
+  @Override
+  public TerminalSize getTerminalSize() {
+    TerminalSize terminalSize = super.getTerminalSize();
+    updatePageSize(terminalSize);
+    return terminalSize;
+  }
+
+  @Override
+  public TerminalSize doResizeIfNecessary() {
+    TerminalSize terminalSize = super.doResizeIfNecessary();
+    if (terminalSize == null) {
+      return null;
+    }
+    updatePageSize(terminalSize);
+    return terminalSize;
+  }
+
+  private void updatePageSize(TerminalSize terminalSize) {
+    pageSize = terminalSize.getRows() - SUMMARY_ROW_NUM - 2;
+    if (pageSize < 0) {
+      pageSize = 0;
+    }
+  }
+
+  public int getPageSize() {
+    return pageSize;
+  }
+
+  public void showTopScreen(Summary summary, List<Header> headers, List<Record> records,
+    Record selectedRecord) {
+    showSummary(summary);
+    clearMessage();
+    showHeaders(headers);
+    showRecords(headers, records, selectedRecord);
+  }
+
+  private void showSummary(Summary summary) {
+    TerminalPrinter printer = getTerminalPrinter(SUMMARY_START_ROW);
+    printer.print(String.format("HBase hbtop - %s", summary.getCurrentTime())).endOfLine();
+    printer.print(String.format("Version: %s", summary.getVersion())).endOfLine();
+    printer.print(String.format("Cluster ID: %s", summary.getClusterId())).endOfLine();
+    printer.print("RegionServer(s): ")
+      .startBold().print(Integer.toString(summary.getServers())).stopBold()
+      .print(" total, ")
+      .startBold().print(Integer.toString(summary.getLiveServers())).stopBold()
+      .print(" live, ")
+      .startBold().print(Integer.toString(summary.getDeadServers())).stopBold()
+      .print(" dead").endOfLine();
+    printer.print("RegionCount: ")
+      .startBold().print(Integer.toString(summary.getRegionCount())).stopBold()
+      .print(" total, ")
+      .startBold().print(Integer.toString(summary.getRitCount())).stopBold()
+      .print(" rit").endOfLine();
+    printer.print("Average Cluster Load: ")
+      .startBold().print(String.format("%.2f", summary.getAverageLoad())).stopBold().endOfLine();
+    printer.print("Aggregate Request/s: ")
+      .startBold().print(Long.toString(summary.getAggregateRequestPerSecond())).stopBold()
+      .endOfLine();
+  }
+
+  private void showRecords(List<Header> headers, List<Record> records, Record selectedRecord) {
+    TerminalPrinter printer = getTerminalPrinter(RECORD_START_ROW);
+    List<String> buf = new ArrayList<>(headers.size());
+    for (int i = 0; i < pageSize; i++) {
+      if(i < records.size()) {
+        Record record = records.get(i);
+        buf.clear();
+        for (Header header : headers) {
+          String value = "";
+          if (record.containsKey(header.getField())) {
+            value = record.get(header.getField()).asString();
+          }
+
+          buf.add(limitLineLength(String.format(header.format(), value), header.getLength()));
+        }
+
+        String recordString = StringUtils.join(buf, " ");
+        if (!recordString.isEmpty()) {
+          recordString += " ";
+        }
+
+        if (record == selectedRecord) {
+          printer.startHighlight().print(recordString).stopHighlight().endOfLine();
+        } else {
+          printer.print(recordString).endOfLine();
+        }
+      } else {
+        printer.endOfLine();
+      }
+    }
+  }
+
+  private void showHeaders(List<Header> headers) {
+    List<String> headerStrings = new ArrayList<>();
+    for (Header header : headers) {
+      headerStrings.add(String.format(header.format(), header.getField().getHeader()));
+    }
+    String header = StringUtils.join(headerStrings, " ");
+
+    if (!header.isEmpty()) {
+      header += " ";
+    }
+
+    getTerminalPrinter(RECORD_HEADER_ROW).startHighlight().print(header).stopHighlight()
+      .endOfLine();
+  }
+
+  private String limitLineLength(String line, int length) {
+    if (line.length() > length) {
+      return line.substring(0, length - 1) + "+";
+    }
+    return line;
+  }
+
+  private void clearMessage() {
+    getTerminalPrinter(MESSAGE_ROW).print("").endOfLine();
+  }
+
+  private ScreenView unknownCommandMessage() {
+    cancelTimer();
+    return topScreenPresenter.goToMessageMode(getScreen(), getTerminal(), MESSAGE_ROW,
+      "Unknown command - try 'h' for help");
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/AbstractTerminalPrinter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/AbstractTerminalPrinter.java
new file mode 100644
index 0000000..237729c
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/AbstractTerminalPrinter.java
@@ -0,0 +1,69 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal;
+
+public abstract class AbstractTerminalPrinter implements TerminalPrinter {
+
+  @Override
+  public TerminalPrinter print(Object value) {
+    print(value.toString());
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(char value) {
+    print(Character.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(short value) {
+    print(Short.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(int value) {
+    print(Integer.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(long value) {
+    print(Long.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(float value) {
+    print(Float.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter print(double value) {
+    print(Double.toString(value));
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter printFormat(String format, Object... args) {
+    print(String.format(format, args));
+    return this;
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Attributes.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Attributes.java
new file mode 100644
index 0000000..8fcbc78
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Attributes.java
@@ -0,0 +1,128 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * The attributes of text in the terminal.
+ */
+@InterfaceAudience.Private
+public class Attributes {
+  private boolean bold;
+  private boolean blink;
+  private boolean reverse;
+  private boolean underline;
+  private Color foregroundColor;
+  private Color backgroundColor;
+
+  public Attributes() {
+    reset();
+  }
+
+  public Attributes(Attributes attributes) {
+    set(attributes);
+  }
+
+  public boolean isBold() {
+    return bold;
+  }
+
+  public void setBold(boolean bold) {
+    this.bold = bold;
+  }
+
+  public boolean isBlink() {
+    return blink;
+  }
+
+  public void setBlink(boolean blink) {
+    this.blink = blink;
+  }
+
+  public boolean isReverse() {
+    return reverse;
+  }
+
+  public void setReverse(boolean reverse) {
+    this.reverse = reverse;
+  }
+
+  public boolean isUnderline() {
+    return underline;
+  }
+
+  public void setUnderline(boolean underline) {
+    this.underline = underline;
+  }
+
+  public Color getForegroundColor() {
+    return foregroundColor;
+  }
+
+  public void setForegroundColor(Color foregroundColor) {
+    this.foregroundColor = foregroundColor;
+  }
+
+  public Color getBackgroundColor() {
+    return backgroundColor;
+  }
+
+  public void setBackgroundColor(Color backgroundColor) {
+    this.backgroundColor = backgroundColor;
+  }
+
+  public void reset() {
+    bold = false;
+    blink = false;
+    reverse = false;
+    underline = false;
+    foregroundColor = Color.WHITE;
+    backgroundColor = Color.BLACK;
+  }
+
+  public void set(Attributes attributes) {
+    bold = attributes.bold;
+    blink = attributes.blink;
+    reverse = attributes.reverse;
+    underline = attributes.underline;
+    foregroundColor = attributes.foregroundColor;
+    backgroundColor = attributes.backgroundColor;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof Attributes)) {
+      return false;
+    }
+    Attributes that = (Attributes) o;
+    return bold == that.bold && blink == that.blink && reverse == that.reverse
+      && underline == that.underline && foregroundColor == that.foregroundColor
+      && backgroundColor == that.backgroundColor;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(bold, blink, reverse, underline, foregroundColor, backgroundColor);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Color.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Color.java
new file mode 100644
index 0000000..42baf5c
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Color.java
@@ -0,0 +1,28 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Terminal color definitions.
+ */
+@InterfaceAudience.Private
+public enum Color {
+  BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/CursorPosition.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/CursorPosition.java
new file mode 100644
index 0000000..2b1d6de
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/CursorPosition.java
@@ -0,0 +1,61 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * A 2-d position in 'terminal space'.
+ */
+@InterfaceAudience.Private
+public class CursorPosition {
+  private final int column;
+  private final int row;
+
+  public CursorPosition(int column, int row) {
+    this.column = column;
+    this.row = row;
+  }
+
+  public int getColumn() {
+    return column;
+  }
+
+  public int getRow() {
+    return row;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof CursorPosition)) {
+      return false;
+    }
+    CursorPosition that = (CursorPosition) o;
+    return column == that.column && row == that.row;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(column, row);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/KeyPress.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/KeyPress.java
new file mode 100644
index 0000000..5b334e3
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/KeyPress.java
@@ -0,0 +1,128 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Represents the user pressing a key on the keyboard.
+ */
+@InterfaceAudience.Private
+public class KeyPress {
+  public enum Type {
+    Character,
+    Escape,
+    Backspace,
+    ArrowLeft,
+    ArrowRight,
+    ArrowUp,
+    ArrowDown,
+    Insert,
+    Delete,
+    Home,
+    End,
+    PageUp,
+    PageDown,
+    ReverseTab,
+    Tab,
+    Enter,
+    F1,
+    F2,
+    F3,
+    F4,
+    F5,
+    F6,
+    F7,
+    F8,
+    F9,
+    F10,
+    F11,
+    F12,
+    Unknown
+  }
+
+  private final Type type;
+  private final Character character;
+  private final boolean alt;
+  private final boolean ctrl;
+  private final boolean shift;
+
+  public KeyPress(Type type, @Nullable Character character, boolean alt, boolean ctrl,
+    boolean shift) {
+    this.type = Objects.requireNonNull(type);
+    this.character = character;
+    this.alt = alt;
+    this.ctrl = ctrl;
+    this.shift = shift;
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  @Nullable
+  public Character getCharacter() {
+    return character;
+  }
+
+  public boolean isAlt() {
+    return alt;
+  }
+
+  public boolean isCtrl() {
+    return ctrl;
+  }
+
+  public boolean isShift() {
+    return shift;
+  }
+
+  @Override
+  public String toString() {
+    return "KeyPress{" +
+      "type=" + type +
+      ", character=" + escape(character) +
+      ", alt=" + alt +
+      ", ctrl=" + ctrl +
+      ", shift=" + shift +
+      '}';
+  }
+
+  private String escape(Character character) {
+    if (character == null) {
+      return "null";
+    }
+
+    switch (character) {
+      case '\n':
+        return "\\n";
+
+      case '\b':
+        return "\\b";
+
+      case '\t':
+        return "\\t";
+
+      default:
+        return character.toString();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Terminal.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Terminal.java
new file mode 100644
index 0000000..8da71af
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/Terminal.java
@@ -0,0 +1,39 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.io.Closeable;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * The terminal interface that is an abstraction of terminal screen.
+ */
+@InterfaceAudience.Private
+public interface Terminal extends Closeable {
+  void clear();
+  void refresh();
+  TerminalSize getSize();
+  @Nullable TerminalSize doResizeIfNecessary();
+  @Nullable KeyPress pollKeyPress();
+  CursorPosition getCursorPosition();
+  void setCursorPosition(int column, int row);
+  void hideCursor();
+  TerminalPrinter getTerminalPrinter(int startRow);
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalPrinter.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalPrinter.java
new file mode 100644
index 0000000..a28892a
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalPrinter.java
@@ -0,0 +1,54 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * The interface responsible for printing to the terminal.
+ */
+@InterfaceAudience.Private
+public interface TerminalPrinter {
+  TerminalPrinter print(String value);
+
+  TerminalPrinter print(Object value);
+
+  TerminalPrinter print(char value);
+
+  TerminalPrinter print(short value);
+
+  TerminalPrinter print(int value);
+
+  TerminalPrinter print(long value);
+
+  TerminalPrinter print(float value);
+
+  TerminalPrinter print(double value);
+
+  TerminalPrinter printFormat(String format, Object... args);
+
+  TerminalPrinter startHighlight();
+
+  TerminalPrinter stopHighlight();
+
+  TerminalPrinter startBold();
+
+  TerminalPrinter stopBold();
+
+  void endOfLine();
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalSize.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalSize.java
new file mode 100644
index 0000000..44c5d82
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/TerminalSize.java
@@ -0,0 +1,61 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+
+/**
+ * Terminal dimensions in 2-d space, measured in number of rows and columns.
+ */
+@InterfaceAudience.Private
+public class TerminalSize {
+  private final int columns;
+  private final int rows;
+
+  public TerminalSize(int columns, int rows) {
+    this.columns = columns;
+    this.rows = rows;
+  }
+
+  public int getColumns() {
+    return columns;
+  }
+
+  public int getRows() {
+    return rows;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof TerminalSize)) {
+      return false;
+    }
+    TerminalSize that = (TerminalSize) o;
+    return columns == that.columns && rows == that.rows;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(columns, rows);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/Cell.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/Cell.java
new file mode 100644
index 0000000..3100e09
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/Cell.java
@@ -0,0 +1,122 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal.impl;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.Attributes;
+import org.apache.hadoop.hbase.hbtop.terminal.Color;
+
+/**
+ * Represents a single text cell of the terminal.
+ */
+@InterfaceAudience.Private
+public class Cell {
+  private static final char UNSET_VALUE = (char) 65535;
+  private static final char END_OF_LINE = '\0';
+
+  private final Attributes attributes;
+  private char ch;
+
+  public Cell() {
+    attributes = new Attributes();
+    ch = ' ';
+  }
+
+  public char getChar() {
+    return ch;
+  }
+
+  public void setChar(char ch) {
+    this.ch = ch;
+  }
+
+  public void reset() {
+    attributes.reset();
+    ch = ' ';
+  }
+
+  public void unset() {
+    attributes.reset();
+    ch = UNSET_VALUE;
+  }
+
+  public void endOfLine() {
+    attributes.reset();
+    ch = END_OF_LINE;
+  }
+
+  public boolean isEndOfLine() {
+    return ch == END_OF_LINE;
+  }
+
+  public void set(Cell cell) {
+    attributes.set(cell.attributes);
+    this.ch = cell.ch;
+  }
+
+  public Attributes getAttributes() {
+    return new Attributes(attributes);
+  }
+
+  public void setAttributes(Attributes attributes) {
+    this.attributes.set(attributes);
+  }
+
+  public boolean isBold() {
+    return attributes.isBold();
+  }
+
+  public boolean isBlink() {
+    return attributes.isBlink();
+  }
+
+  public boolean isReverse() {
+    return attributes.isReverse();
+  }
+
+  public boolean isUnderline() {
+    return attributes.isUnderline();
+  }
+
+  public Color getForegroundColor() {
+    return attributes.getForegroundColor();
+  }
+
+  public Color getBackgroundColor() {
+    return attributes.getBackgroundColor();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof Cell)) {
+      return false;
+    }
+    Cell cell = (Cell) o;
+    return ch == cell.ch && attributes.equals(cell.attributes);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(attributes, ch);
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/EscapeSequences.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/EscapeSequences.java
new file mode 100644
index 0000000..fe286e0
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/EscapeSequences.java
@@ -0,0 +1,140 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal.impl;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.Color;
+
+/**
+ * Utility class for escape sequences.
+ */
+@InterfaceAudience.Private
+public final class EscapeSequences {
+
+  private EscapeSequences() {
+  }
+
+  public static String clearAll() {
+    return "\033[0;37;40m\033[2J";
+  }
+
+  public static String setTitle(String title) {
+    return "\033]2;" + title + "\007";
+  }
+
+  public static String cursor(boolean on) {
+    if (on) {
+      return "\033[?25h";
+    }
+    return "\033[?25l";
+  }
+
+  public static String moveCursor(int column, int row) {
+    return String.format("\033[%d;%dH", row + 1, column + 1);
+  }
+
+  public static String clearRemainingLine() {
+    return "\033[0;37;40m\033[K";
+  }
+
+  public static String color(Color foregroundColor, Color backgroundColor, boolean bold,
+    boolean reverse, boolean blink, boolean underline) {
+
+    int foregroundColorValue = getColorValue(foregroundColor, true);
+    int backgroundColorValue = getColorValue(backgroundColor, false);
+
+    StringBuilder sb = new StringBuilder();
+    if (bold && reverse && blink && !underline) {
+      sb.append("\033[0;1;7;5;");
+    } else if (bold && reverse && !blink && !underline) {
+      sb.append("\033[0;1;7;");
+    } else if (!bold && reverse && blink && !underline) {
+      sb.append("\033[0;7;5;");
+    } else if (bold && !reverse && blink && !underline) {
+      sb.append("\033[0;1;5;");
+    } else if (bold && !reverse && !blink && !underline) {
+      sb.append("\033[0;1;");
+    } else if (!bold && reverse && !blink && !underline) {
+      sb.append("\033[0;7;");
+    } else if (!bold && !reverse && blink && !underline) {
+      sb.append("\033[0;5;");
+    } else if (bold && reverse && blink) {
+      sb.append("\033[0;1;7;5;4;");
+    } else if (bold && reverse) {
+      sb.append("\033[0;1;7;4;");
+    } else if (!bold && reverse && blink) {
+      sb.append("\033[0;7;5;4;");
+    } else if (bold && blink) {
+      sb.append("\033[0;1;5;4;");
+    } else if (bold) {
+      sb.append("\033[0;1;4;");
+    } else if (reverse) {
+      sb.append("\033[0;7;4;");
+    } else if (blink) {
+      sb.append("\033[0;5;4;");
+    } else if (underline) {
+      sb.append("\033[0;4;");
+    } else {
+      sb.append("\033[0;");
+    }
+    sb.append(String.format("%d;%dm", foregroundColorValue, backgroundColorValue));
+    return sb.toString();
+  }
+
+  private static int getColorValue(Color color, boolean foreground) {
+    int baseValue;
+    if (foreground) {
+      baseValue = 30;
+    } else { // background
+      baseValue = 40;
+    }
+
+    switch (color) {
+      case BLACK:
+        return baseValue;
+
+      case RED:
+        return baseValue + 1;
+
+      case GREEN:
+        return baseValue + 2;
+
+      case YELLOW:
+        return baseValue + 3;
+
+      case BLUE:
+        return baseValue + 4;
+
+      case MAGENTA:
+        return baseValue + 5;
+
+      case CYAN:
+        return baseValue + 6;
+
+      case WHITE:
+        return baseValue + 7;
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  public static String normal() {
+    return "\033[0;37;40m";
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/KeyPressGenerator.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/KeyPressGenerator.java
new file mode 100644
index 0000000..13a5b28
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/KeyPressGenerator.java
@@ -0,0 +1,500 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal.impl;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.util.Threads;
+
+/**
+ * This generates {@link KeyPress} objects from the given input stream and offers them to the
+ * given queue.
+ */
+@InterfaceAudience.Private
+public class KeyPressGenerator {
+
+  private static final Log LOG = LogFactory.getLog(KeyPressGenerator.class);
+
+  private enum ParseState {
+    START, ESCAPE, ESCAPE_SEQUENCE_PARAM1, ESCAPE_SEQUENCE_PARAM2
+  }
+
+  private final Queue<KeyPress> keyPressQueue;
+  private final BlockingQueue<Character> inputCharacterQueue = new LinkedBlockingQueue<>();
+  private final Reader input;
+  private final InputStream inputStream;
+  private final AtomicBoolean stopThreads = new AtomicBoolean();
+  private final ExecutorService executorService;
+
+  private ParseState parseState;
+  private int param1;
+  private int param2;
+
+  public KeyPressGenerator(InputStream inputStream, Queue<KeyPress> keyPressQueue) {
+    this.inputStream = inputStream;
+    input = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
+    this.keyPressQueue = keyPressQueue;
+
+    executorService = Executors.newFixedThreadPool(2, new ThreadFactoryBuilder()
+      .setNameFormat("KeyPressGenerator-%d").setDaemon(true)
+      .setUncaughtExceptionHandler(Threads.LOGGING_EXCEPTION_HANDLER).build());
+
+    initState();
+  }
+
+  public void start() {
+    executorService.execute(new Runnable() {
+      @Override
+      public void run() {
+        readerThread();
+      }
+    });
+    executorService.execute(new Runnable() {
+      @Override
+      public void run() {
+        generatorThread();
+      }
+    });
+  }
+
+  private void initState() {
+    parseState = ParseState.START;
+    param1 = 0;
+    param2 = 0;
+  }
+
+  private void readerThread() {
+    boolean done = false;
+    char[] readBuffer = new char[128];
+
+    while (!done && !stopThreads.get()) {
+      try {
+        int n = inputStream.available();
+        if (n > 0) {
+          if (readBuffer.length < n) {
+            readBuffer = new char[readBuffer.length * 2];
+          }
+
+          int rc = input.read(readBuffer, 0, readBuffer.length);
+          if (rc == -1) {
+            // EOF
+            done = true;
+          } else {
+            for (int i = 0; i < rc; i++) {
+              int ch = readBuffer[i];
+              inputCharacterQueue.offer((char) ch);
+            }
+          }
+        } else {
+          Thread.sleep(20);
+        }
+      } catch (InterruptedException ignored) {
+      } catch (IOException e) {
+        LOG.error("Caught an exception", e);
+        done = true;
+      }
+    }
+  }
+
+  private void generatorThread() {
+    while (!stopThreads.get()) {
+      Character ch;
+      try {
+        ch = inputCharacterQueue.poll(100, TimeUnit.MILLISECONDS);
+      } catch (InterruptedException ignored) {
+        continue;
+      }
+
+      if (ch == null) {
+        if (parseState == ParseState.ESCAPE) {
+          offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
+          initState();
+        } else if (parseState != ParseState.START) {
+          offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
+          initState();
+        }
+        continue;
+      }
+
+      if (parseState == ParseState.START) {
+        if (ch == 0x1B) {
+          parseState = ParseState.ESCAPE;
+          continue;
+        }
+
+        switch (ch) {
+          case '\n':
+          case '\r':
+            offer(new KeyPress(KeyPress.Type.Enter, '\n', false, false, false));
+            continue;
+
+          case 0x08:
+          case 0x7F:
+            offer(new KeyPress(KeyPress.Type.Backspace, '\b', false, false, false));
+            continue;
+
+          case '\t':
+            offer(new KeyPress(KeyPress.Type.Tab, '\t', false, false, false));
+            continue;
+
+          default:
+            // Do nothing
+            break;
+        }
+
+        if (ch < 32) {
+          ctrlAndCharacter(ch);
+          continue;
+        }
+
+        if (isPrintableChar(ch)) {
+          // Normal character
+          offer(new KeyPress(KeyPress.Type.Character, ch, false, false, false));
+          continue;
+        }
+
+        offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
+        continue;
+      }
+
+      if (parseState == ParseState.ESCAPE) {
+        if (ch == 0x1B) {
+          offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
+          continue;
+        }
+
+        if (ch < 32 && ch != 0x08) {
+          ctrlAltAndCharacter(ch);
+          initState();
+          continue;
+        } else if (ch == 0x7F || ch == 0x08) {
+          offer(new KeyPress(KeyPress.Type.Backspace, '\b', false, false, false));
+          initState();
+          continue;
+        }
+
+        if (ch == '[' || ch == 'O') {
+          parseState = ParseState.ESCAPE_SEQUENCE_PARAM1;
+          continue;
+        }
+
+        if (isPrintableChar(ch)) {
+          // Alt and character
+          offer(new KeyPress(KeyPress.Type.Character, ch, true, false, false));
+          initState();
+          continue;
+        }
+
+        offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
+        offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
+        initState();
+        continue;
+      }
+
+      escapeSequenceCharacter(ch);
+    }
+  }
+
+  private void ctrlAndCharacter(char ch) {
+    char ctrlCode;
+    switch (ch) {
+      case 0:
+        ctrlCode = ' ';
+        break;
+
+      case 28:
+        ctrlCode = '\\';
+        break;
+
+      case 29:
+        ctrlCode = ']';
+        break;
+
+      case 30:
+        ctrlCode = '^';
+        break;
+
+      case 31:
+        ctrlCode = '_';
+        break;
+
+      default:
+        ctrlCode = (char) ('a' - 1 + ch);
+        break;
+    }
+    offer(new KeyPress(KeyPress.Type.Character, ctrlCode, false, true, false));
+  }
+
+  private boolean isPrintableChar(char ch) {
+    if (Character.isISOControl(ch)) {
+      return false;
+    }
+    Character.UnicodeBlock block = Character.UnicodeBlock.of(ch);
+    return block != null && !block.equals(Character.UnicodeBlock.SPECIALS);
+  }
+
+  private void ctrlAltAndCharacter(char ch) {
+    char ctrlCode;
+    switch (ch) {
+      case 0:
+        ctrlCode = ' ';
+        break;
+
+      case 28:
+        ctrlCode = '\\';
+        break;
+
+      case 29:
+        ctrlCode = ']';
+        break;
+
+      case 30:
+        ctrlCode = '^';
+        break;
+
+      case 31:
+        ctrlCode = '_';
+        break;
+
+      default:
+        ctrlCode = (char) ('a' - 1 + ch);
+        break;
+    }
+    offer(new KeyPress(KeyPress.Type.Character, ctrlCode, true, true, false));
+  }
+
+  private void escapeSequenceCharacter(char ch) {
+    switch (parseState) {
+      case ESCAPE_SEQUENCE_PARAM1:
+        if (ch == ';') {
+          parseState = ParseState.ESCAPE_SEQUENCE_PARAM2;
+        } else if (Character.isDigit(ch)) {
+          param1 = param1 * 10 + Character.digit(ch, 10);
+        } else {
+          doneEscapeSequenceCharacter(ch);
+        }
+        break;
+
+      case ESCAPE_SEQUENCE_PARAM2:
+        if (Character.isDigit(ch)) {
+          param2 = param2 * 10 + Character.digit(ch, 10);
+        } else {
+          doneEscapeSequenceCharacter(ch);
+        }
+        break;
+
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  private void doneEscapeSequenceCharacter(char last) {
+    boolean alt = false;
+    boolean ctrl = false;
+    boolean shift = false;
+    if (param2 != 0) {
+      alt = isAlt(param2);
+      ctrl = isCtrl(param2);
+      shift = isShift(param2);
+    }
+
+    if (last != '~') {
+      switch (last) {
+        case 'A':
+          offer(new KeyPress(KeyPress.Type.ArrowUp, null, alt, ctrl, shift));
+          break;
+
+        case 'B':
+          offer(new KeyPress(KeyPress.Type.ArrowDown, null, alt, ctrl, shift));
+          break;
+
+        case 'C':
+          offer(new KeyPress(KeyPress.Type.ArrowRight, null, alt, ctrl, shift));
+          break;
+
+        case 'D':
+          offer(new KeyPress(KeyPress.Type.ArrowLeft, null, alt, ctrl, shift));
+          break;
+
+        case 'H':
+          offer(new KeyPress(KeyPress.Type.Home, null, alt, ctrl, shift));
+          break;
+
+        case 'F':
+          offer(new KeyPress(KeyPress.Type.End, null, alt, ctrl, shift));
+          break;
+
+        case 'P':
+          offer(new KeyPress(KeyPress.Type.F1, null, alt, ctrl, shift));
+          break;
+
+        case 'Q':
+          offer(new KeyPress(KeyPress.Type.F2, null, alt, ctrl, shift));
+          break;
+
+        case 'R':
+          offer(new KeyPress(KeyPress.Type.F3, null, alt, ctrl, shift));
+          break;
+
+        case 'S':
+          offer(new KeyPress(KeyPress.Type.F4, null, alt, ctrl, shift));
+          break;
+
+        case 'Z':
+          offer(new KeyPress(KeyPress.Type.ReverseTab, null, alt, ctrl, shift));
+          break;
+
+        default:
+          offer(new KeyPress(KeyPress.Type.Unknown, null, alt, ctrl, shift));
+          break;
+      }
+      initState();
+      return;
+    }
+
+    switch (param1) {
+      case 1:
+        offer(new KeyPress(KeyPress.Type.Home, null, alt, ctrl, shift));
+        break;
+
+      case 2:
+        offer(new KeyPress(KeyPress.Type.Insert, null, alt, ctrl, shift));
+        break;
+
+      case 3:
+        offer(new KeyPress(KeyPress.Type.Delete, null, alt, ctrl, shift));
+        break;
+
+      case 4:
+        offer(new KeyPress(KeyPress.Type.End, null, alt, ctrl, shift));
+        break;
+
+      case 5:
+        offer(new KeyPress(KeyPress.Type.PageUp, null, alt, ctrl, shift));
+        break;
+
+      case 6:
+        offer(new KeyPress(KeyPress.Type.PageDown, null, alt, ctrl, shift));
+        break;
+
+      case 11:
+        offer(new KeyPress(KeyPress.Type.F1, null, alt, ctrl, shift));
+        break;
+
+      case 12:
+        offer(new KeyPress(KeyPress.Type.F2, null, alt, ctrl, shift));
+        break;
+
+      case 13:
+        offer(new KeyPress(KeyPress.Type.F3, null, alt, ctrl, shift));
+        break;
+
+      case 14:
+        offer(new KeyPress(KeyPress.Type.F4, null, alt, ctrl, shift));
+        break;
+
+      case 15:
+        offer(new KeyPress(KeyPress.Type.F5, null, alt, ctrl, shift));
+        break;
+
+      case 17:
+        offer(new KeyPress(KeyPress.Type.F6, null, alt, ctrl, shift));
+        break;
+
+      case 18:
+        offer(new KeyPress(KeyPress.Type.F7, null, alt, ctrl, shift));
+        break;
+
+      case 19:
+        offer(new KeyPress(KeyPress.Type.F8, null, alt, ctrl, shift));
+        break;
+
+      case 20:
+        offer(new KeyPress(KeyPress.Type.F9, null, alt, ctrl, shift));
+        break;
+
+      case 21:
+        offer(new KeyPress(KeyPress.Type.F10, null, alt, ctrl, shift));
+        break;
+
+      case 23:
+        offer(new KeyPress(KeyPress.Type.F11, null, alt, ctrl, shift));
+        break;
+
+      case 24:
+        offer(new KeyPress(KeyPress.Type.F12, null, alt, ctrl, shift));
+        break;
+
+      default:
+        offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
+        break;
+    }
+
+    initState();
+  }
+
+  private boolean isShift(int param) {
+    return (param & 1) != 0;
+  }
+
+  private boolean isAlt(int param) {
+    return (param & 2) != 0;
+  }
+
+  private boolean isCtrl(int param) {
+    return (param & 4) != 0;
+  }
+
+  private void offer(KeyPress keyPress) {
+    // Handle ctrl + c
+    if (keyPress.isCtrl() && keyPress.getType() == KeyPress.Type.Character &&
+      keyPress.getCharacter() == 'c') {
+      System.exit(0);
+    }
+
+    keyPressQueue.offer(keyPress);
+  }
+
+  public void stop() {
+    stopThreads.set(true);
+
+    executorService.shutdown();
+    try {
+      while (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
+        LOG.warn("Waiting for thread-pool to terminate");
+      }
+    } catch (InterruptedException e) {
+      LOG.warn("Interrupted while waiting for thread-pool termination", e);
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/ScreenBuffer.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/ScreenBuffer.java
new file mode 100644
index 0000000..d710749
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/ScreenBuffer.java
@@ -0,0 +1,170 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal.impl;
+
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.clearRemainingLine;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.color;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.cursor;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.moveCursor;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.normal;
+
+import java.io.PrintWriter;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.Attributes;
+import org.apache.hadoop.hbase.hbtop.terminal.CursorPosition;
+
+/**
+ * Represents a buffer of the terminal screen for double-buffering.
+ */
+@InterfaceAudience.Private
+public class ScreenBuffer {
+  private int columns;
+  private int rows;
+
+  private Cell[][] buffer;
+  private Cell[][] physical;
+
+  private boolean cursorVisible;
+  private int cursorColumn;
+  private int cursorRow;
+
+  public void reallocate(int columns, int rows) {
+    buffer = new Cell[columns][rows];
+    physical = new Cell[columns][rows];
+
+    for (int row = 0; row < rows; row++) {
+      for (int column = 0; column < columns; column++) {
+        buffer[column][row] = new Cell();
+
+        physical[column][row] = new Cell();
+        physical[column][row].unset();
+      }
+    }
+
+    this.columns = columns;
+    this.rows = rows;
+  }
+
+  public void clear() {
+    for (int row = 0; row < rows; row++) {
+      for (int col = 0; col < columns; col++) {
+        buffer[col][row].reset();
+      }
+    }
+  }
+
+  public void flush(PrintWriter output) {
+    StringBuilder sb = new StringBuilder();
+
+    sb.append(normal());
+    Attributes attributes = new Attributes();
+    for (int row = 0; row < rows; row++) {
+      flushRow(row, sb, attributes);
+    }
+
+    if (cursorVisible && cursorRow >= 0 && cursorColumn >= 0 && cursorRow < rows &&
+      cursorColumn < columns) {
+      sb.append(cursor(true));
+      sb.append(moveCursor(cursorColumn, cursorRow));
+    } else {
+      sb.append(cursor(false));
+    }
+
+    output.write(sb.toString());
+    output.flush();
+  }
+
+  private void flushRow(int row, StringBuilder sb, Attributes lastAttributes) {
+    int lastColumn = -1;
+    for (int column = 0; column < columns; column++) {
+      Cell cell = buffer[column][row];
+      Cell pCell = physical[column][row];
+
+      if (!cell.equals(pCell)) {
+        if (lastColumn != column - 1 || lastColumn == -1) {
+          sb.append(moveCursor(column, row));
+        }
+
+        if (cell.isEndOfLine()) {
+          for (int i = column; i < columns; i++) {
+            physical[i][row].set(buffer[i][row]);
+          }
+
+          sb.append(clearRemainingLine());
+          lastAttributes.reset();
+          return;
+        }
+
+        if (!cell.getAttributes().equals(lastAttributes)) {
+          sb.append(color(cell.getForegroundColor(), cell.getBackgroundColor(), cell.isBold(),
+            cell.isReverse(), cell.isBlink(), cell.isUnderline()));
+        }
+
+        sb.append(cell.getChar());
+
+        lastColumn = column;
+        lastAttributes.set(cell.getAttributes());
+
+        physical[column][row].set(cell);
+      }
+    }
+  }
+
+  public CursorPosition getCursorPosition() {
+    return new CursorPosition(cursorColumn, cursorRow);
+  }
+
+  public void setCursorPosition(int column, int row) {
+    cursorVisible = true;
+    cursorColumn = column;
+    cursorRow = row;
+  }
+
+  public void hideCursor() {
+    cursorVisible = false;
+  }
+
+  public void putString(int column, int row, String string, Attributes attributes) {
+    int i = column;
+    for (int j = 0; j < string.length(); j++) {
+      char ch = string.charAt(j);
+      putChar(i, row, ch, attributes);
+      i += 1;
+      if (i == columns) {
+        break;
+      }
+    }
+  }
+
+  public void putChar(int column, int row, char ch, Attributes attributes) {
+    if (column >= 0 && column < columns && row >= 0 && row < rows) {
+      buffer[column][row].setAttributes(attributes);
+      buffer[column][row].setChar(ch);
+    }
+  }
+
+  public void endOfLine(int column, int row) {
+    if (column >= 0 && column < columns && row >= 0 && row < rows) {
+      buffer[column][row].endOfLine();
+      for (int i = column + 1; i < columns; i++) {
+        buffer[i][row].reset();
+      }
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalImpl.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalImpl.java
new file mode 100644
index 0000000..0084e23
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalImpl.java
@@ -0,0 +1,229 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal.impl;
+
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.clearAll;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.cursor;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.moveCursor;
+import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.normal;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Queue;
+import java.util.StringTokenizer;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.CursorPosition;
+import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
+import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
+
+/**
+ * The implementation of the {@link Terminal} interface.
+ */
+@InterfaceAudience.Private
+public class TerminalImpl implements Terminal {
+
+  private static final Log LOG = LogFactory.getLog(TerminalImpl.class);
+
+  private TerminalSize cachedTerminalSize;
+
+  private final PrintWriter output;
+
+  private final ScreenBuffer screenBuffer;
+
+  private final Queue<KeyPress> keyPressQueue;
+  private final KeyPressGenerator keyPressGenerator;
+
+  public TerminalImpl() {
+    this(null);
+  }
+
+  public TerminalImpl(@Nullable String title) {
+    output = new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8));
+    sttyRaw();
+
+    if (title != null) {
+      setTitle(title);
+    }
+
+    screenBuffer = new ScreenBuffer();
+
+    cachedTerminalSize = queryTerminalSize();
+    updateTerminalSize(cachedTerminalSize.getColumns(), cachedTerminalSize.getRows());
+
+    keyPressQueue = new ConcurrentLinkedQueue<>();
+    keyPressGenerator = new KeyPressGenerator(System.in, keyPressQueue);
+    keyPressGenerator.start();
+
+    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+      @Override
+      public void run() {
+        output.printf("%s%s%s%s", moveCursor(0, 0), cursor(true), normal(), clearAll());
+        output.flush();
+        sttyCooked();
+      }
+    }));
+
+    // Clear the terminal
+    output.write(clearAll());
+    output.flush();
+  }
+
+  private void setTitle(String title) {
+    output.write(EscapeSequences.setTitle(title));
+    output.flush();
+  }
+
+  private void updateTerminalSize(int columns, int rows) {
+    screenBuffer.reallocate(columns, rows);
+  }
+
+  @Override
+  public void clear() {
+    screenBuffer.clear();
+  }
+
+  @Override
+  public void refresh() {
+    screenBuffer.flush(output);
+  }
+
+  @Override
+  public TerminalSize getSize() {
+    return cachedTerminalSize;
+  }
+
+  @Nullable
+  @Override
+  public TerminalSize doResizeIfNecessary() {
+    TerminalSize currentTerminalSize = queryTerminalSize();
+    if (!currentTerminalSize.equals(cachedTerminalSize)) {
+      cachedTerminalSize = currentTerminalSize;
+      updateTerminalSize(cachedTerminalSize.getColumns(), cachedTerminalSize.getRows());
+      return cachedTerminalSize;
+    }
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public KeyPress pollKeyPress() {
+    return keyPressQueue.poll();
+  }
+
+  @Override
+  public CursorPosition getCursorPosition() {
+    return screenBuffer.getCursorPosition();
+  }
+
+  @Override
+  public void setCursorPosition(int column, int row) {
+    screenBuffer.setCursorPosition(column, row);
+  }
+
+  @Override
+  public void hideCursor() {
+    screenBuffer.hideCursor();
+  }
+
+  @Override
+  public TerminalPrinter getTerminalPrinter(int startRow) {
+    return new TerminalPrinterImpl(screenBuffer, startRow);
+  }
+
+  @Override
+  public void close() {
+    keyPressGenerator.stop();
+  }
+
+  private TerminalSize queryTerminalSize() {
+    String sizeString = doStty("size");
+
+    int rows = 0;
+    int columns = 0;
+
+    StringTokenizer tokenizer = new StringTokenizer(sizeString);
+    int rc = Integer.parseInt(tokenizer.nextToken());
+    if (rc > 0) {
+      rows = rc;
+    }
+
+    rc = Integer.parseInt(tokenizer.nextToken());
+    if (rc > 0) {
+      columns = rc;
+    }
+    return new TerminalSize(columns, rows);
+  }
+
+  private void sttyRaw() {
+    doStty("-ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost " +
+      "-echo -echonl -icanon -isig -iexten -parenb cs8 min 1");
+  }
+
+  private void sttyCooked() {
+    doStty("sane cooked");
+  }
+
+  private String doStty(String sttyOptionsString) {
+    String [] cmd = {"/bin/sh", "-c", "stty " + sttyOptionsString + " < /dev/tty"};
+
+    try {
+      Process process = Runtime.getRuntime().exec(cmd);
+
+      String ret;
+
+      // stdout
+      try (BufferedReader stdout = new BufferedReader(new InputStreamReader(
+        process.getInputStream(), StandardCharsets.UTF_8))) {
+        ret = stdout.readLine();
+      }
+
+      // stderr
+      try (BufferedReader stderr = new BufferedReader(new InputStreamReader(
+        process.getErrorStream(), StandardCharsets.UTF_8))) {
+        String line = stderr.readLine();
+        if ((line != null) && (line.length() > 0)) {
+          LOG.error("Error output from stty: " + line);
+        }
+      }
+
+      try {
+        process.waitFor();
+      } catch (InterruptedException ignored) {
+      }
+
+      int exitValue = process.exitValue();
+      if (exitValue != 0) {
+        LOG.error("stty returned error code: " + exitValue);
+      }
+      return ret;
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalPrinterImpl.java b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalPrinterImpl.java
new file mode 100644
index 0000000..0d698b0
--- /dev/null
+++ b/hbase-hbtop/src/main/java/org/apache/hadoop/hbase/hbtop/terminal/impl/TerminalPrinterImpl.java
@@ -0,0 +1,83 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.terminal.impl;
+
+import java.util.Objects;
+
+import org.apache.hadoop.hbase.classification.InterfaceAudience;
+import org.apache.hadoop.hbase.hbtop.terminal.AbstractTerminalPrinter;
+import org.apache.hadoop.hbase.hbtop.terminal.Attributes;
+import org.apache.hadoop.hbase.hbtop.terminal.Color;
+import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
+
+/**
+ * The implementation of the {@link TerminalPrinter} interface.
+ */
+@InterfaceAudience.Private
+public class TerminalPrinterImpl extends AbstractTerminalPrinter {
+  private final ScreenBuffer screenBuffer;
+  private int row;
+  private int column;
+
+  private final Attributes attributes = new Attributes();
+
+  TerminalPrinterImpl(ScreenBuffer screenBuffer, int startRow) {
+    this.screenBuffer = Objects.requireNonNull(screenBuffer);
+    this.row = startRow;
+  }
+
+  @Override
+  public TerminalPrinter print(String value) {
+    screenBuffer.putString(column, row, value, attributes);
+    column += value.length();
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter startHighlight() {
+    attributes.setForegroundColor(Color.BLACK);
+    attributes.setBackgroundColor(Color.WHITE);
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter stopHighlight() {
+    attributes.setForegroundColor(Color.WHITE);
+    attributes.setBackgroundColor(Color.BLACK);
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter startBold() {
+    attributes.setBold(true);
+    return this;
+  }
+
+  @Override
+  public TerminalPrinter stopBold() {
+    attributes.setBold(false);
+    return this;
+  }
+
+  @Override
+  public void endOfLine() {
+    screenBuffer.endOfLine(column, row);
+    row += 1;
+    column = 0;
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecord.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecord.java
new file mode 100644
index 0000000..eca1d04
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecord.java
@@ -0,0 +1,87 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop;
+
+import static org.apache.hadoop.hbase.hbtop.Record.entry;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestRecord {
+
+  @Test
+  public void testBuilder() {
+    Record actual1 = Record.builder().put(Field.TABLE, "tableName")
+      .put(entry(Field.REGION_COUNT, 3))
+      .put(Field.REQUEST_COUNT_PER_SECOND, Field.REQUEST_COUNT_PER_SECOND.newValue(100L))
+      .build();
+
+    assertThat(actual1.size(), is(3));
+    assertThat(actual1.get(Field.TABLE).asString(), is("tableName"));
+    assertThat(actual1.get(Field.REGION_COUNT).asInt(), is(3));
+    assertThat(actual1.get(Field.REQUEST_COUNT_PER_SECOND).asLong(), is(100L));
+
+    Record actual2 = Record.builder().putAll(actual1).build();
+
+    assertThat(actual2.size(), is(3));
+    assertThat(actual2.get(Field.TABLE).asString(), is("tableName"));
+    assertThat(actual2.get(Field.REGION_COUNT).asInt(), is(3));
+    assertThat(actual2.get(Field.REQUEST_COUNT_PER_SECOND).asLong(), is(100L));
+  }
+
+  @Test
+  public void testOfEntries() {
+    Record actual = Record.ofEntries(
+      entry(Field.TABLE, "tableName"),
+      entry(Field.REGION_COUNT, 3),
+      entry(Field.REQUEST_COUNT_PER_SECOND, 100L)
+    );
+
+    assertThat(actual.size(), is(3));
+    assertThat(actual.get(Field.TABLE).asString(), is("tableName"));
+    assertThat(actual.get(Field.REGION_COUNT).asInt(), is(3));
+    assertThat(actual.get(Field.REQUEST_COUNT_PER_SECOND).asLong(), is(100L));
+  }
+
+  @Test
+  public void testCombine() {
+    Record record1 = Record.ofEntries(
+      entry(Field.TABLE, "tableName"),
+      entry(Field.REGION_COUNT, 3),
+      entry(Field.REQUEST_COUNT_PER_SECOND, 100L)
+    );
+
+    Record record2 = Record.ofEntries(
+      entry(Field.TABLE, "tableName"),
+      entry(Field.REGION_COUNT, 5),
+      entry(Field.REQUEST_COUNT_PER_SECOND, 500L)
+    );
+
+    Record actual = record1.combine(record2);
+
+    assertThat(actual.size(), is(3));
+    assertThat(actual.get(Field.TABLE).asString(), is("tableName"));
+    assertThat(actual.get(Field.REGION_COUNT).asInt(), is(8));
+    assertThat(actual.get(Field.REQUEST_COUNT_PER_SECOND).asLong(), is(600L));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecordFilter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecordFilter.java
new file mode 100644
index 0000000..27c6b9e
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestRecordFilter.java
@@ -0,0 +1,209 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.Size;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestRecordFilter {
+
+  @Test
+  public void testParseAndBuilder() {
+    testParseAndBuilder("REGION=region1", false,
+      RecordFilter.newBuilder(Field.REGION).equal("region1"));
+
+    testParseAndBuilder("REGION=", false,
+      RecordFilter.newBuilder(Field.REGION).equal(""));
+
+    testParseAndBuilder("!REGION=region1", false,
+      RecordFilter.newBuilder(Field.REGION).notEqual("region1"));
+
+    testParseAndBuilder("REGION==region2", true,
+      RecordFilter.newBuilder(Field.REGION, true).doubleEquals("region2"));
+
+    testParseAndBuilder("!REGION==region2", true,
+      RecordFilter.newBuilder(Field.REGION, true).notDoubleEquals("region2"));
+
+    testParseAndBuilder("#REQ/S>100", false,
+      RecordFilter.newBuilder(Field.REQUEST_COUNT_PER_SECOND).greater(100L));
+
+    testParseAndBuilder("!#REQ/S>100", false,
+      RecordFilter.newBuilder(Field.REQUEST_COUNT_PER_SECOND).notGreater(100L));
+
+    testParseAndBuilder("SF>=50MB", true,
+      RecordFilter.newBuilder(Field.STORE_FILE_SIZE, true).greaterOrEqual("50MB"));
+
+    testParseAndBuilder("!SF>=50MB", true,
+      RecordFilter.newBuilder(Field.STORE_FILE_SIZE, true).notGreaterOrEqual("50MB"));
+
+    testParseAndBuilder("#REQ/S<20", false,
+      RecordFilter.newBuilder(Field.REQUEST_COUNT_PER_SECOND).less(20L));
+
+    testParseAndBuilder("!#REQ/S<20", false,
+      RecordFilter.newBuilder(Field.REQUEST_COUNT_PER_SECOND).notLess(20L));
+
+    testParseAndBuilder("%COMP<=50%", true,
+      RecordFilter.newBuilder(Field.COMPACTION_PROGRESS, true).lessOrEqual("50%"));
+
+    testParseAndBuilder("!%COMP<=50%", true,
+      RecordFilter.newBuilder(Field.COMPACTION_PROGRESS, true).notLessOrEqual("50%"));
+  }
+
+  private void testParseAndBuilder(String filterString, boolean ignoreCase, RecordFilter expected) {
+    RecordFilter actual = RecordFilter.parse(filterString, ignoreCase);
+    assertThat(expected, is(actual));
+  }
+
+  @Test
+  public void testParseFailure() {
+    RecordFilter filter = RecordFilter.parse("REGIO=region1", false);
+    assertThat(filter, is(nullValue()));
+
+    filter = RecordFilter.parse("", false);
+    assertThat(filter, is(nullValue()));
+
+    filter = RecordFilter.parse("#REQ/S==aaa", false);
+    assertThat(filter, is(nullValue()));
+
+    filter = RecordFilter.parse("SF>=50", false);
+    assertThat(filter, is(nullValue()));
+  }
+
+  @Test
+  public void testToString() {
+    testToString("REGION=region1");
+    testToString("!REGION=region1");
+    testToString("REGION==region2");
+    testToString("!REGION==region2");
+    testToString("#REQ/S>100");
+    testToString("!#REQ/S>100");
+    testToString("SF>=50.0MB");
+    testToString("!SF>=50.0MB");
+    testToString("#REQ/S<20");
+    testToString("!#REQ/S<20");
+    testToString("%COMP<=50.00%");
+    testToString("!%COMP<=50.00%");
+  }
+
+  private void testToString(String filterString) {
+    RecordFilter filter = RecordFilter.parse(filterString, false);
+    assertThat(filter, is(notNullValue()));
+    assertThat(filterString, is(filter.toString()));
+  }
+
+  @Test
+  public void testFilters() {
+    List<Record> records = createTestRecords();
+
+    testFilter(records, "REGION=region", false,
+      "region1", "region2", "region3", "region4", "region5");
+    testFilter(records, "!REGION=region", false);
+    testFilter(records, "REGION=Region", false);
+
+    testFilter(records, "REGION==region", false);
+    testFilter(records, "REGION==region1", false, "region1");
+    testFilter(records, "!REGION==region1", false, "region2", "region3", "region4", "region5");
+
+    testFilter(records, "#REQ/S==100", false, "region1");
+    testFilter(records, "#REQ/S>100", false, "region2", "region5");
+    testFilter(records, "SF>=100MB", false, "region1", "region2", "region4", "region5");
+    testFilter(records, "!#SF>=10", false, "region1", "region4");
+    testFilter(records, "LOCALITY<0.5", false, "region5");
+    testFilter(records, "%COMP<=50%", false, "region2", "region3", "region4", "region5");
+
+    testFilters(records, Arrays.asList("SF>=100MB", "#REQ/S>100"), false,
+      "region2", "region5");
+    testFilters(records, Arrays.asList("%COMP<=50%", "!#SF>=10"), false, "region4");
+    testFilters(records, Arrays.asList("!REGION==region1", "LOCALITY<0.5", "#REQ/S>100"), false,
+      "region5");
+  }
+
+  @Test
+  public void testFiltersIgnoreCase() {
+    List<Record> records = createTestRecords();
+
+    testFilter(records, "REGION=Region", true,
+      "region1", "region2", "region3", "region4", "region5");
+    testFilter(records, "REGION=REGION", true,
+      "region1", "region2", "region3", "region4", "region5");
+  }
+
+  private List<Record> createTestRecords() {
+    List<Record> ret = new ArrayList<>();
+    ret.add(createTestRecord("region1", 100L, new Size(100, Size.Unit.MEGABYTE), 2, 1.0f, 80f));
+    ret.add(createTestRecord("region2", 120L, new Size(100, Size.Unit.GIGABYTE), 10, 0.5f, 20f));
+    ret.add(createTestRecord("region3", 50L, new Size(500, Size.Unit.KILOBYTE), 15, 0.8f, 50f));
+    ret.add(createTestRecord("region4", 90L, new Size(10, Size.Unit.TERABYTE), 5, 0.9f, 30f));
+    ret.add(createTestRecord("region5", 200L, new Size(1, Size.Unit.PETABYTE), 13, 0.1f, 40f));
+    return ret;
+  }
+
+  private Record createTestRecord(String region, long requestCountPerSecond,
+    Size storeFileSize, int numStoreFiles, float locality, float compactionProgress) {
+    Record.Builder builder = Record.builder();
+    builder.put(Field.REGION, region);
+    builder.put(Field.REQUEST_COUNT_PER_SECOND, requestCountPerSecond);
+    builder.put(Field.STORE_FILE_SIZE, storeFileSize);
+    builder.put(Field.NUM_STORE_FILES, numStoreFiles);
+    builder.put(Field.LOCALITY, locality);
+    builder.put(Field.COMPACTION_PROGRESS, compactionProgress);
+    return builder.build();
+  }
+
+  private void testFilter(List<Record> records, String filterString, boolean ignoreCase,
+    String... expectedRegions) {
+    testFilters(records, Collections.singletonList(filterString), ignoreCase, expectedRegions);
+  }
+
+  private void testFilters(List<Record> records, List<String> filterStrings, boolean ignoreCase,
+    String... expectedRegions) {
+
+    List<String> actual = new ArrayList<>();
+    for (Record record : records) {
+      boolean filter = false;
+      for (String filterString : filterStrings) {
+        if (!RecordFilter.parse(filterString, ignoreCase).execute(record)) {
+          filter = true;
+        }
+      }
+      if (!filter) {
+        actual.add(record.get(Field.REGION).asString());
+      }
+    }
+
+    assertThat(actual.size(), is(expectedRegions.length));
+    for (int i = 0; i < actual.size(); i++) {
+      String actualRegion = actual.get(i);
+      String expectedRegion = expectedRegions[i];
+      assertThat(actualRegion, is(expectedRegion));
+    }
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java
new file mode 100644
index 0000000..3e7fbbc
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/TestUtils.java
@@ -0,0 +1,373 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.protobuf.ByteString;
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.lang3.time.FastDateFormat;
+import org.apache.hadoop.hbase.ClusterStatus;
+import org.apache.hadoop.hbase.HRegionInfo;
+import org.apache.hadoop.hbase.ServerLoad;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.Size;
+import org.apache.hadoop.hbase.hbtop.field.Size.Unit;
+import org.apache.hadoop.hbase.hbtop.screen.top.Summary;
+import org.apache.hadoop.hbase.master.RegionState;
+import org.apache.hadoop.hbase.protobuf.generated.ClusterStatusProtos;
+import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos;
+
+public final class TestUtils {
+
+  static final String HBASE_VERSION = "1.5.0-SNAPSHOT";
+  static final String CLUSTER_UUID = "01234567-89ab-cdef-0123-456789abcdef";
+
+  private TestUtils() { }
+
+  public static ClusterStatus createDummyClusterStatus() {
+    Map<ServerName, ServerLoad> serverLoads = Maps.newHashMap();
+    List<ServerName> deadServers = Lists.newArrayList();
+    Set<RegionState> rit = Sets.newHashSet();
+
+    ServerName host1 = ServerName.valueOf("host1.apache.com", 1000, 1);
+
+    serverLoads.put(host1,
+      createServerLoad(100,
+        new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 100,
+        Lists.newArrayList(
+          createRegionLoad("table1,,1.00000000000000000000000000000000.", 100, 100,
+            new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 1,
+            new Size(100, Size.Unit.MEGABYTE), 0.1f, 100, 100, "2019-07-22 00:00:00"),
+          createRegionLoad("table2,1,2.00000000000000000000000000000001.", 200, 200,
+            new Size(200, Size.Unit.MEGABYTE), new Size(400, Size.Unit.MEGABYTE), 2,
+            new Size(200, Size.Unit.MEGABYTE), 0.2f, 50, 200, "2019-07-22 00:00:01"),
+          createRegionLoad(
+            "namespace:table3,,3_0001.00000000000000000000000000000002.", 300, 300,
+            new Size(300, Size.Unit.MEGABYTE), new Size(600, Size.Unit.MEGABYTE), 3,
+            new Size(300, Size.Unit.MEGABYTE), 0.3f, 100, 300, "2019-07-22 00:00:02"))));
+
+    ServerName host2 = ServerName.valueOf("host2.apache.com", 1001, 2);
+
+    serverLoads.put(host2,
+      createServerLoad(200,
+        new Size(16, Size.Unit.GIGABYTE), new Size(32, Size.Unit.GIGABYTE), 200,
+        Lists.newArrayList(
+          createRegionLoad("table1,1,4.00000000000000000000000000000003.", 100, 100,
+            new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 1,
+            new Size(100, Size.Unit.MEGABYTE), 0.4f, 50, 100, "2019-07-22 00:00:03"),
+          createRegionLoad("table2,,5.00000000000000000000000000000004.", 200, 200,
+            new Size(200, Size.Unit.MEGABYTE), new Size(400, Size.Unit.MEGABYTE), 2,
+            new Size(200, Size.Unit.MEGABYTE), 0.5f, 150, 200, "2019-07-22 00:00:04"),
+          createRegionLoad("namespace:table3,,6.00000000000000000000000000000005.", 300, 300,
+            new Size(300, Size.Unit.MEGABYTE), new Size(600, Size.Unit.MEGABYTE), 3,
+            new Size(300, Size.Unit.MEGABYTE), 0.6f, 200, 300, "2019-07-22 00:00:05"))));
+
+    ServerName host3 = ServerName.valueOf("host3.apache.com", 1002, 3);
+
+    deadServers.add(host3);
+
+    rit.add(new RegionState(new HRegionInfo(0, TableName.valueOf("table4"), 0),
+      RegionState.State.OFFLINE, host3));
+
+    return new ClusterStatus(HBASE_VERSION, CLUSTER_UUID, serverLoads, deadServers, null, null,
+      rit, new String[0], true);
+  }
+
+  private static ClusterStatusProtos.RegionLoad createRegionLoad(String regionName,
+    long readRequestCount, long writeRequestCount, Size storeFileSize,
+    Size uncompressedStoreFileSize, int storeFileCount, Size memStoreSize, float locality,
+    long compactedCellCount, long compactingCellCount, String lastMajorCompactionTime) {
+    FastDateFormat df = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
+    try {
+      return ClusterStatusProtos.RegionLoad.newBuilder()
+        .setRegionSpecifier(HBaseProtos.RegionSpecifier.newBuilder()
+          .setType(HBaseProtos.RegionSpecifier.RegionSpecifierType.REGION_NAME)
+          .setValue(ByteString.copyFromUtf8(regionName)).build())
+        .setReadRequestsCount(readRequestCount)
+        .setWriteRequestsCount(writeRequestCount)
+        .setStorefileSizeMB((int)storeFileSize.get(Unit.MEGABYTE))
+        .setStoreUncompressedSizeMB((int)uncompressedStoreFileSize.get(Unit.MEGABYTE))
+        .setStorefiles(storeFileCount)
+        .setMemstoreSizeMB((int)memStoreSize.get(Unit.MEGABYTE))
+        .setDataLocality(locality)
+        .setCurrentCompactedKVs(compactedCellCount)
+        .setTotalCompactingKVs(compactingCellCount)
+        .setLastMajorCompactionTs(df.parse(lastMajorCompactionTime).getTime())
+        .build();
+    } catch (ParseException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  private static ServerLoad createServerLoad(long reportTimestamp,
+    Size usedHeapSize, Size maxHeapSize, long requestCountPerSecond,
+    List<ClusterStatusProtos.RegionLoad> regionLoads) {
+    return new ServerLoad(ClusterStatusProtos.ServerLoad.newBuilder()
+        .setReportStartTime(reportTimestamp)
+        .setReportEndTime(reportTimestamp)
+        .setUsedHeapMB((int)usedHeapSize.get(Unit.MEGABYTE))
+        .setMaxHeapMB((int)maxHeapSize.get(Unit.MEGABYTE))
+        .setNumberOfRequests(requestCountPerSecond)
+        .addAllRegionLoads(regionLoads)
+        .build());
+  }
+
+  public static void assertRecordsInRegionMode(List<Record> records) {
+    assertEquals(6, records.size());
+
+    for (Record record : records) {
+      switch (record.get(Field.REGION_NAME).asString()) {
+        case "table1,,1.00000000000000000000000000000000.":
+          assertRecordInRegionMode(record, "default", "1", "", "table1",
+            "00000000000000000000000000000000", "host1:1000", "host1.apache.com,1000,1",
+            0L, 0L, 0L, new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 1,
+            new Size(100, Size.Unit.MEGABYTE), 0.1f, "", 100L, 100L, 100f,
+            "2019-07-22 00:00:00");
+          break;
+
+        case "table1,1,4.00000000000000000000000000000003.":
+          assertRecordInRegionMode(record, "default", "4", "", "table1",
+            "00000000000000000000000000000003", "host2:1001", "host2.apache.com,1001,2",
+            0L, 0L, 0L, new Size(100, Size.Unit.MEGABYTE), new Size(200, Size.Unit.MEGABYTE), 1,
+            new Size(100, Size.Unit.MEGABYTE), 0.4f, "1", 100L, 50L, 50f,
+            "2019-07-22 00:00:03");
+          break;
+
+        case "table2,,5.00000000000000000000000000000004.":
+          assertRecordInRegionMode(record, "default", "5", "", "table2",
+            "00000000000000000000000000000004", "host2:1001", "host2.apache.com,1001,2",
+            0L, 0L, 0L, new Size(200, Size.Unit.MEGABYTE), new Size(400, Size.Unit.MEGABYTE), 2,
+            new Size(200, Size.Unit.MEGABYTE), 0.5f, "", 200L, 150L, 75f,
+            "2019-07-22 00:00:04");
+          break;
+
+        case "table2,1,2.00000000000000000000000000000001.":
+          assertRecordInRegionMode(record, "default", "2", "", "table2",
+            "00000000000000000000000000000001", "host1:1000", "host1.apache.com,1000,1",
+            0L, 0L, 0L, new Size(200, Size.Unit.MEGABYTE), new Size(400, Size.Unit.MEGABYTE), 2,
+            new Size(200, Size.Unit.MEGABYTE), 0.2f, "1", 200L, 50L, 25f,
+            "2019-07-22 00:00:01");
+          break;
+
+        case "namespace:table3,,6.00000000000000000000000000000005.":
+          assertRecordInRegionMode(record, "namespace", "6", "", "table3",
+            "00000000000000000000000000000005", "host2:1001", "host2.apache.com,1001,2",
+            0L, 0L, 0L, new Size(300, Size.Unit.MEGABYTE), new Size(600, Size.Unit.MEGABYTE), 3,
+            new Size(300, Size.Unit.MEGABYTE), 0.6f, "", 300L, 200L, 66.66667f,
+            "2019-07-22 00:00:05");
+          break;
+
+        case "namespace:table3,,3_0001.00000000000000000000000000000002.":
+          assertRecordInRegionMode(record, "namespace", "3", "1", "table3",
+            "00000000000000000000000000000002", "host1:1000", "host1.apache.com,1000,1",
+            0L, 0L, 0L, new Size(300, Size.Unit.MEGABYTE), new Size(600, Size.Unit.MEGABYTE), 3,
+            new Size(300, Size.Unit.MEGABYTE), 0.3f, "", 300L, 100L, 33.333336f,
+            "2019-07-22 00:00:02");
+          break;
+
+        default:
+          fail();
+      }
+    }
+  }
+
+  private static void assertRecordInRegionMode(Record record, String namespace, String startCode,
+    String replicaId, String table, String region, String regionServer, String longRegionServer,
+    long requestCountPerSecond, long readRequestCountPerSecond, long writeCountRequestPerSecond,
+    Size storeFileSize, Size uncompressedStoreFileSize, int numStoreFiles,
+    Size memStoreSize, float locality, String startKey, long compactingCellCount,
+    long compactedCellCount, float compactionProgress, String lastMajorCompactionTime) {
+    assertEquals(21, record.size());
+    assertEquals(namespace, record.get(Field.NAMESPACE).asString());
+    assertEquals(startCode, record.get(Field.START_CODE).asString());
+    assertEquals(replicaId, record.get(Field.REPLICA_ID).asString());
+    assertEquals(table, record.get(Field.TABLE).asString());
+    assertEquals(region, record.get(Field.REGION).asString());
+    assertEquals(regionServer, record.get(Field.REGION_SERVER).asString());
+    assertEquals(longRegionServer, record.get(Field.LONG_REGION_SERVER).asString());
+    assertEquals(requestCountPerSecond, record.get(Field.REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(readRequestCountPerSecond,
+      record.get(Field.READ_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(writeCountRequestPerSecond,
+      record.get(Field.WRITE_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(storeFileSize, record.get(Field.STORE_FILE_SIZE).asSize());
+    assertEquals(uncompressedStoreFileSize,
+      record.get(Field.UNCOMPRESSED_STORE_FILE_SIZE).asSize());
+    assertEquals(numStoreFiles, record.get(Field.NUM_STORE_FILES).asInt());
+    assertEquals(record.get(Field.MEM_STORE_SIZE).asSize(), memStoreSize);
+    assertEquals(locality, record.get(Field.LOCALITY).asFloat(), 0.001);
+    assertEquals(startKey, record.get(Field.START_KEY).asString());
+    assertEquals(compactingCellCount, record.get(Field.COMPACTING_CELL_COUNT).asLong());
+    assertEquals(compactedCellCount, record.get(Field.COMPACTED_CELL_COUNT).asLong());
+    assertEquals(compactionProgress, record.get(Field.COMPACTION_PROGRESS).asFloat(), 0.001);
+    assertEquals(lastMajorCompactionTime,
+      record.get(Field.LAST_MAJOR_COMPACTION_TIME).asString());
+  }
+
+  public static void assertRecordsInNamespaceMode(List<Record> records) {
+    assertEquals(2, records.size());
+
+    for (Record record : records) {
+      switch (record.get(Field.NAMESPACE).asString()) {
+        case "default":
+          assertRecordInNamespaceMode(record, 0L, 0L, 0L, new Size(600, Size.Unit.MEGABYTE),
+            new Size(1200, Size.Unit.MEGABYTE), 6, new Size(600, Size.Unit.MEGABYTE), 4);
+          break;
+
+        case "namespace":
+          assertRecordInNamespaceMode(record, 0L, 0L, 0L, new Size(600, Size.Unit.MEGABYTE),
+            new Size(1200, Size.Unit.MEGABYTE), 6, new Size(600, Size.Unit.MEGABYTE), 2);
+          break;
+
+        default:
+          fail();
+      }
+    }
+  }
+
+  private static void assertRecordInNamespaceMode(Record record, long requestCountPerSecond,
+    long readRequestCountPerSecond, long writeCountRequestPerSecond, Size storeFileSize,
+    Size uncompressedStoreFileSize, int numStoreFiles, Size memStoreSize, int regionCount) {
+    assertEquals(9, record.size());
+    assertEquals(requestCountPerSecond, record.get(Field.REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(readRequestCountPerSecond,
+      record.get(Field.READ_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(writeCountRequestPerSecond,
+      record.get(Field.WRITE_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(storeFileSize, record.get(Field.STORE_FILE_SIZE).asSize());
+    assertEquals(uncompressedStoreFileSize,
+      record.get(Field.UNCOMPRESSED_STORE_FILE_SIZE).asSize());
+    assertEquals(numStoreFiles, record.get(Field.NUM_STORE_FILES).asInt());
+    assertEquals(memStoreSize, record.get(Field.MEM_STORE_SIZE).asSize());
+    assertEquals(regionCount, record.get(Field.REGION_COUNT).asInt());
+  }
+
+  public static void assertRecordsInTableMode(List<Record> records) {
+    assertEquals(3, records.size());
+
+    for (Record record : records) {
+      String tableName = String.format("%s:%s", record.get(Field.NAMESPACE).asString(),
+        record.get(Field.TABLE).asString());
+
+      switch (tableName) {
+        case "default:table1":
+          assertRecordInTableMode(record, 0L, 0L, 0L, new Size(200, Size.Unit.MEGABYTE),
+            new Size(400, Size.Unit.MEGABYTE), 2, new Size(200, Size.Unit.MEGABYTE), 2);
+          break;
+
+        case "default:table2":
+          assertRecordInTableMode(record, 0L, 0L, 0L, new Size(400, Size.Unit.MEGABYTE),
+            new Size(800, Size.Unit.MEGABYTE), 4, new Size(400, Size.Unit.MEGABYTE), 2);
+          break;
+
+        case "namespace:table3":
+          assertRecordInTableMode(record, 0L, 0L, 0L, new Size(600, Size.Unit.MEGABYTE),
+            new Size(1200, Size.Unit.MEGABYTE), 6, new Size(600, Size.Unit.MEGABYTE), 2);
+          break;
+
+        default:
+          fail();
+      }
+    }
+  }
+
+  private static void assertRecordInTableMode(Record record, long requestCountPerSecond,
+    long readRequestCountPerSecond,  long writeCountRequestPerSecond, Size storeFileSize,
+    Size uncompressedStoreFileSize, int numStoreFiles, Size memStoreSize, int regionCount) {
+    assertEquals(10, record.size());
+    assertEquals(requestCountPerSecond, record.get(Field.REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(readRequestCountPerSecond,
+      record.get(Field.READ_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(writeCountRequestPerSecond,
+      record.get(Field.WRITE_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(storeFileSize, record.get(Field.STORE_FILE_SIZE).asSize());
+    assertEquals(uncompressedStoreFileSize,
+      record.get(Field.UNCOMPRESSED_STORE_FILE_SIZE).asSize());
+    assertEquals(numStoreFiles, record.get(Field.NUM_STORE_FILES).asInt());
+    assertEquals(memStoreSize, record.get(Field.MEM_STORE_SIZE).asSize());
+    assertEquals(regionCount, record.get(Field.REGION_COUNT).asInt());
+  }
+
+  public static void assertRecordsInRegionServerMode(List<Record> records) {
+    assertEquals(2, records.size());
+
+    for (Record record : records) {
+      switch (record.get(Field.REGION_SERVER).asString()) {
+        case "host1:1000":
+          assertRecordInRegionServerMode(record, "host1.apache.com,1000,1", 0L, 0L, 0L,
+            new Size(600, Size.Unit.MEGABYTE), new Size(1200, Size.Unit.MEGABYTE), 6,
+            new Size(600, Size.Unit.MEGABYTE), 3, new Size(100, Size.Unit.MEGABYTE),
+            new Size(200, Size.Unit.MEGABYTE));
+          break;
+
+        case "host2:1001":
+          assertRecordInRegionServerMode(record, "host2.apache.com,1001,2", 0L, 0L, 0L,
+            new Size(600, Size.Unit.MEGABYTE), new Size(1200, Size.Unit.MEGABYTE), 6,
+            new Size(600, Size.Unit.MEGABYTE), 3, new Size(16, Size.Unit.GIGABYTE),
+            new Size(32, Size.Unit.GIGABYTE));
+          break;
+
+        default:
+          fail();
+      }
+    }
+  }
+
+  private static void assertRecordInRegionServerMode(Record record, String longRegionServer,
+    long requestCountPerSecond, long readRequestCountPerSecond, long writeCountRequestPerSecond,
+    Size storeFileSize, Size uncompressedStoreFileSize, int numStoreFiles,
+    Size memStoreSize, int regionCount, Size usedHeapSize, Size maxHeapSize) {
+    assertEquals(12, record.size());
+    assertEquals(longRegionServer, record.get(Field.LONG_REGION_SERVER).asString());
+    assertEquals(requestCountPerSecond, record.get(Field.REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(readRequestCountPerSecond,
+      record.get(Field.READ_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(writeCountRequestPerSecond,
+      record.get(Field.WRITE_REQUEST_COUNT_PER_SECOND).asLong());
+    assertEquals(storeFileSize, record.get(Field.STORE_FILE_SIZE).asSize());
+    assertEquals(uncompressedStoreFileSize,
+      record.get(Field.UNCOMPRESSED_STORE_FILE_SIZE).asSize());
+    assertEquals(numStoreFiles, record.get(Field.NUM_STORE_FILES).asInt());
+    assertEquals(memStoreSize, record.get(Field.MEM_STORE_SIZE).asSize());
+    assertEquals(regionCount, record.get(Field.REGION_COUNT).asInt());
+    assertEquals(usedHeapSize, record.get(Field.USED_HEAP_SIZE).asSize());
+    assertEquals(maxHeapSize, record.get(Field.MAX_HEAP_SIZE).asSize());
+  }
+
+  public static void assertSummary(Summary summary) {
+    assertEquals(HBASE_VERSION, summary.getVersion());
+    assertEquals(CLUSTER_UUID, summary.getClusterId());
+    assertEquals(3, summary.getServers());
+    assertEquals(2, summary.getLiveServers());
+    assertEquals(1, summary.getDeadServers());
+    assertEquals(6, summary.getRegionCount());
+    assertEquals(1, summary.getRitCount());
+    assertEquals(3.0, summary.getAverageLoad(), 0.001);
+    assertEquals(300L, summary.getAggregateRequestPerSecond());
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestFieldValue.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestFieldValue.java
new file mode 100644
index 0000000..8b75e14
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestFieldValue.java
@@ -0,0 +1,290 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.field;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestFieldValue {
+
+  @Test
+  public void testParseAndAsSomethingMethod() {
+    // String
+    FieldValue stringFieldValue = new FieldValue("aaa", FieldValueType.STRING);
+    assertThat(stringFieldValue.asString(), is("aaa"));
+
+    try {
+      new FieldValue(1, FieldValueType.STRING);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    // Integer
+    FieldValue integerFieldValue = new FieldValue(100, FieldValueType.INTEGER);
+    assertThat(integerFieldValue.asInt(), is(100));
+
+    integerFieldValue = new FieldValue("100", FieldValueType.INTEGER);
+    assertThat(integerFieldValue.asInt(), is(100));
+
+    try {
+      new FieldValue("aaa", FieldValueType.INTEGER);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    // Long
+    FieldValue longFieldValue = new FieldValue(100L, FieldValueType.LONG);
+    assertThat(longFieldValue.asLong(), is(100L));
+
+    longFieldValue = new FieldValue("100", FieldValueType.LONG);
+    assertThat(longFieldValue.asLong(), is(100L));
+
+    try {
+      new FieldValue("aaa", FieldValueType.LONG);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    try {
+      new FieldValue(100, FieldValueType.LONG);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    // Float
+    FieldValue floatFieldValue = new FieldValue(1.0f, FieldValueType.FLOAT);
+    assertThat(floatFieldValue.asFloat(), is(1.0f));
+
+    floatFieldValue = new FieldValue("1", FieldValueType.FLOAT);
+    assertThat(floatFieldValue.asFloat(), is(1.0f));
+
+    try {
+      new FieldValue("aaa", FieldValueType.FLOAT);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    try {
+      new FieldValue(1, FieldValueType.FLOAT);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    // Size
+    FieldValue sizeFieldValue =
+      new FieldValue(new Size(100, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("100.0MB"));
+    assertThat(sizeFieldValue.asSize(), is(new Size(100, Size.Unit.MEGABYTE)));
+
+    sizeFieldValue = new FieldValue("100MB", FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("100.0MB"));
+    assertThat(sizeFieldValue.asSize(), is(new Size(100, Size.Unit.MEGABYTE)));
+
+    try {
+      new FieldValue("100", FieldValueType.SIZE);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    try {
+      new FieldValue(100, FieldValueType.SIZE);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+
+    // Percent
+    FieldValue percentFieldValue =
+      new FieldValue(100f, FieldValueType.PERCENT);
+    assertThat(percentFieldValue.asString(), is("100.00%"));
+    assertThat(percentFieldValue.asFloat(), is(100f));
+
+    percentFieldValue = new FieldValue("100%", FieldValueType.PERCENT);
+    assertThat(percentFieldValue.asString(), is("100.00%"));
+    assertThat(percentFieldValue.asFloat(), is(100f));
+
+    percentFieldValue = new FieldValue("100", FieldValueType.PERCENT);
+    assertThat(percentFieldValue.asString(), is("100.00%"));
+    assertThat(percentFieldValue.asFloat(), is(100f));
+
+    try {
+      new FieldValue(100, FieldValueType.PERCENT);
+      fail();
+    } catch (IllegalArgumentException ignored) {
+    }
+  }
+
+  @Test
+  public void testCompareTo() {
+    // String
+    FieldValue stringAFieldValue = new FieldValue("a", FieldValueType.STRING);
+    FieldValue stringAFieldValue2 = new FieldValue("a", FieldValueType.STRING);
+    FieldValue stringBFieldValue = new FieldValue("b", FieldValueType.STRING);
+    FieldValue stringCapitalAFieldValue = new FieldValue("A", FieldValueType.STRING);
+
+    assertThat(stringAFieldValue.compareTo(stringAFieldValue2), is(0));
+    assertThat(stringBFieldValue.compareTo(stringAFieldValue), is(1));
+    assertThat(stringAFieldValue.compareTo(stringBFieldValue), is(-1));
+    assertThat(stringAFieldValue.compareTo(stringCapitalAFieldValue), is(32));
+
+    // Integer
+    FieldValue integer1FieldValue = new FieldValue(1, FieldValueType.INTEGER);
+    FieldValue integer1FieldValue2 = new FieldValue(1, FieldValueType.INTEGER);
+    FieldValue integer2FieldValue = new FieldValue(2, FieldValueType.INTEGER);
+
+    assertThat(integer1FieldValue.compareTo(integer1FieldValue2), is(0));
+    assertThat(integer2FieldValue.compareTo(integer1FieldValue), is(1));
+    assertThat(integer1FieldValue.compareTo(integer2FieldValue), is(-1));
+
+    // Long
+    FieldValue long1FieldValue = new FieldValue(1L, FieldValueType.LONG);
+    FieldValue long1FieldValue2 = new FieldValue(1L, FieldValueType.LONG);
+    FieldValue long2FieldValue = new FieldValue(2L, FieldValueType.LONG);
+
+    assertThat(long1FieldValue.compareTo(long1FieldValue2), is(0));
+    assertThat(long2FieldValue.compareTo(long1FieldValue), is(1));
+    assertThat(long1FieldValue.compareTo(long2FieldValue), is(-1));
+
+    // Float
+    FieldValue float1FieldValue = new FieldValue(1.0f, FieldValueType.FLOAT);
+    FieldValue float1FieldValue2 = new FieldValue(1.0f, FieldValueType.FLOAT);
+    FieldValue float2FieldValue = new FieldValue(2.0f, FieldValueType.FLOAT);
+
+    assertThat(float1FieldValue.compareTo(float1FieldValue2), is(0));
+    assertThat(float2FieldValue.compareTo(float1FieldValue), is(1));
+    assertThat(float1FieldValue.compareTo(float2FieldValue), is(-1));
+
+    // Size
+    FieldValue size100MBFieldValue =
+      new FieldValue(new Size(100, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    FieldValue size100MBFieldValue2 =
+      new FieldValue(new Size(100, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    FieldValue size200MBFieldValue =
+      new FieldValue(new Size(200, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+
+    assertThat(size100MBFieldValue.compareTo(size100MBFieldValue2), is(0));
+    assertThat(size200MBFieldValue.compareTo(size100MBFieldValue), is(1));
+    assertThat(size100MBFieldValue.compareTo(size200MBFieldValue), is(-1));
+
+    // Percent
+    FieldValue percent50FieldValue = new FieldValue(50.0f, FieldValueType.PERCENT);
+    FieldValue percent50FieldValue2 = new FieldValue(50.0f, FieldValueType.PERCENT);
+    FieldValue percent100FieldValue = new FieldValue(100.0f, FieldValueType.PERCENT);
+
+    assertThat(percent50FieldValue.compareTo(percent50FieldValue2), is(0));
+    assertThat(percent100FieldValue.compareTo(percent50FieldValue), is(1));
+    assertThat(percent50FieldValue.compareTo(percent100FieldValue), is(-1));
+  }
+
+  @Test
+  public void testPlus() {
+    // String
+    FieldValue stringFieldValue = new FieldValue("a", FieldValueType.STRING);
+    FieldValue stringFieldValue2 = new FieldValue("b", FieldValueType.STRING);
+    assertThat(stringFieldValue.plus(stringFieldValue2).asString(), is("ab"));
+
+    // Integer
+    FieldValue integerFieldValue = new FieldValue(1, FieldValueType.INTEGER);
+    FieldValue integerFieldValue2 = new FieldValue(2, FieldValueType.INTEGER);
+    assertThat(integerFieldValue.plus(integerFieldValue2).asInt(), is(3));
+
+    // Long
+    FieldValue longFieldValue = new FieldValue(1L, FieldValueType.LONG);
+    FieldValue longFieldValue2 = new FieldValue(2L, FieldValueType.LONG);
+    assertThat(longFieldValue.plus(longFieldValue2).asLong(), is(3L));
+
+    // Float
+    FieldValue floatFieldValue = new FieldValue(1.2f, FieldValueType.FLOAT);
+    FieldValue floatFieldValue2 = new FieldValue(2.2f, FieldValueType.FLOAT);
+    assertThat(floatFieldValue.plus(floatFieldValue2).asFloat(), is(3.4f));
+
+    // Size
+    FieldValue sizeFieldValue =
+      new FieldValue(new Size(100, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    FieldValue sizeFieldValue2 =
+      new FieldValue(new Size(200, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.plus(sizeFieldValue2).asString(), is("300.0MB"));
+    assertThat(sizeFieldValue.plus(sizeFieldValue2).asSize(),
+      is(new Size(300, Size.Unit.MEGABYTE)));
+
+    // Percent
+    FieldValue percentFieldValue = new FieldValue(30f, FieldValueType.PERCENT);
+    FieldValue percentFieldValue2 = new FieldValue(60f, FieldValueType.PERCENT);
+    assertThat(percentFieldValue.plus(percentFieldValue2).asString(), is("90.00%"));
+    assertThat(percentFieldValue.plus(percentFieldValue2).asFloat(), is(90f));
+  }
+
+  @Test
+  public void testCompareToIgnoreCase() {
+    FieldValue stringAFieldValue = new FieldValue("a", FieldValueType.STRING);
+    FieldValue stringCapitalAFieldValue = new FieldValue("A", FieldValueType.STRING);
+    FieldValue stringCapitalBFieldValue = new FieldValue("B", FieldValueType.STRING);
+
+    assertThat(stringAFieldValue.compareToIgnoreCase(stringCapitalAFieldValue), is(0));
+    assertThat(stringCapitalBFieldValue.compareToIgnoreCase(stringAFieldValue), is(1));
+    assertThat(stringAFieldValue.compareToIgnoreCase(stringCapitalBFieldValue), is(-1));
+  }
+
+  @Test
+  public void testOptimizeSize() {
+    FieldValue sizeFieldValue =
+      new FieldValue(new Size(1, Size.Unit.BYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1.0B"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(1024, Size.Unit.BYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1.0KB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(2 * 1024, Size.Unit.BYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("2.0KB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(2 * 1024, Size.Unit.KILOBYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("2.0MB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(1024 * 1024, Size.Unit.KILOBYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1.0GB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(2 * 1024 * 1024, Size.Unit.MEGABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("2.0TB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(2 * 1024, Size.Unit.TERABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("2.0PB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(1024 * 1024, Size.Unit.TERABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1024.0PB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(1, Size.Unit.PETABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1.0PB"));
+
+    sizeFieldValue =
+      new FieldValue(new Size(1024, Size.Unit.PETABYTE), FieldValueType.SIZE);
+    assertThat(sizeFieldValue.asString(), is("1024.0PB"));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestSize.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestSize.java
new file mode 100644
index 0000000..3d6b285
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/field/TestSize.java
@@ -0,0 +1,80 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.field;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Set;
+import java.util.TreeSet;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({MiscTests.class, SmallTests.class})
+public class TestSize {
+
+  @Test
+  public void testConversion() {
+    Size kbSize = new Size(1024D, Size.Unit.MEGABYTE);
+    assertEquals(1D, kbSize.get(Size.Unit.GIGABYTE), 0);
+    assertEquals(1024D, kbSize.get(), 0);
+    assertEquals(1024D * 1024D, kbSize.get(Size.Unit.KILOBYTE), 0);
+    assertEquals(1024D * 1024D * 1024D, kbSize.get(Size.Unit.BYTE), 0);
+  }
+
+  @Test
+  public void testCompare() {
+    Size size00 = new Size(100D, Size.Unit.GIGABYTE);
+    Size size01 = new Size(100D, Size.Unit.MEGABYTE);
+    Size size02 = new Size(100D, Size.Unit.BYTE);
+    Set<Size> sizes = new TreeSet<>();
+    sizes.add(size00);
+    sizes.add(size01);
+    sizes.add(size02);
+    int count = 0;
+    for (Size s : sizes) {
+      switch (count++) {
+        case 0:
+          assertEquals(size02, s);
+          break;
+        case 1:
+          assertEquals(size01, s);
+          break;
+        default:
+          assertEquals(size00, s);
+          break;
+      }
+    }
+    assertEquals(3, count);
+  }
+
+  @Test
+  public void testEqual() {
+    assertEquals(new Size(1024D, Size.Unit.TERABYTE),
+      new Size(1D, Size.Unit.PETABYTE));
+    assertEquals(new Size(1024D, Size.Unit.GIGABYTE),
+      new Size(1D, Size.Unit.TERABYTE));
+    assertEquals(new Size(1024D, Size.Unit.MEGABYTE),
+      new Size(1D, Size.Unit.GIGABYTE));
+    assertEquals(new Size(1024D, Size.Unit.KILOBYTE),
+      new Size(1D, Size.Unit.MEGABYTE));
+    assertEquals(new Size(1024D, Size.Unit.BYTE),
+      new Size(1D, Size.Unit.KILOBYTE));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestModeBase.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestModeBase.java
new file mode 100644
index 0000000..0b99ef4
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestModeBase.java
@@ -0,0 +1,46 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.junit.Test;
+
+
+public abstract class TestModeBase {
+
+  @Test
+  public void testGetRecords() {
+    List<Record> records = getMode().getRecords(TestUtils.createDummyClusterStatus());
+    assertRecords(records);
+  }
+
+  protected abstract Mode getMode();
+  protected abstract void assertRecords(List<Record> records);
+
+  @Test
+  public void testDrillDown() {
+    List<Record> records = getMode().getRecords(TestUtils.createDummyClusterStatus());
+    for (Record record : records) {
+      assertDrillDown(record, getMode().drillDown(record));
+    }
+  }
+
+  protected abstract void assertDrillDown(Record currentRecord, DrillDownInfo drillDownInfo);
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestNamespaceMode.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestNamespaceMode.java
new file mode 100644
index 0000000..afaf073
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestNamespaceMode.java
@@ -0,0 +1,63 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestNamespaceMode extends TestModeBase {
+
+  @Override
+  protected Mode getMode() {
+    return Mode.NAMESPACE;
+  }
+
+  @Override
+  protected void assertRecords(List<Record> records) {
+    TestUtils.assertRecordsInNamespaceMode(records);
+  }
+
+  @Override
+  protected void assertDrillDown(Record currentRecord, DrillDownInfo drillDownInfo) {
+    assertThat(drillDownInfo.getNextMode(), is(Mode.TABLE));
+    assertThat(drillDownInfo.getInitialFilters().size(), is(1));
+
+    switch (currentRecord.get(Field.NAMESPACE).asString()) {
+      case "default":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(), is("NAMESPACE==default"));
+        break;
+
+      case "namespace":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(),
+          is("NAMESPACE==namespace"));
+        break;
+
+      default:
+        fail();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionMode.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionMode.java
new file mode 100644
index 0000000..2cbaf1b
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionMode.java
@@ -0,0 +1,47 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestRegionMode extends TestModeBase {
+
+  @Override
+  protected Mode getMode() {
+    return Mode.REGION;
+  }
+
+  @Override
+  protected void assertRecords(List<Record> records) {
+    TestUtils.assertRecordsInRegionMode(records);
+  }
+
+  @Override
+  protected void assertDrillDown(Record currentRecord, DrillDownInfo drillDownInfo) {
+    assertThat(drillDownInfo, is(nullValue()));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionServerMode.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionServerMode.java
new file mode 100644
index 0000000..93788d1
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRegionServerMode.java
@@ -0,0 +1,62 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestRegionServerMode extends TestModeBase {
+
+  @Override
+  protected Mode getMode() {
+    return Mode.REGION_SERVER;
+  }
+
+  @Override
+  protected void assertRecords(List<Record> records) {
+    TestUtils.assertRecordsInRegionServerMode(records);
+  }
+
+  @Override
+  protected void assertDrillDown(Record currentRecord, DrillDownInfo drillDownInfo) {
+    assertThat(drillDownInfo.getNextMode(), is(Mode.REGION));
+    assertThat(drillDownInfo.getInitialFilters().size(), is(1));
+
+    switch (currentRecord.get(Field.REGION_SERVER).asString()) {
+      case "host1:1000":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(), is("RS==host1:1000"));
+        break;
+
+      case "host2:1001":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(), is("RS==host2:1001"));
+        break;
+
+      default:
+        fail();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRequestCountPerSecond.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRequestCountPerSecond.java
new file mode 100644
index 0000000..0152f44
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestRequestCountPerSecond.java
@@ -0,0 +1,49 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestRequestCountPerSecond {
+
+  @Test
+  public void test() {
+    RequestCountPerSecond requestCountPerSecond = new RequestCountPerSecond();
+
+    requestCountPerSecond.refresh(1000, 300, 200);
+    assertThat(requestCountPerSecond.getRequestCountPerSecond(), is(0L));
+    assertThat(requestCountPerSecond.getReadRequestCountPerSecond(), is(0L));
+    assertThat(requestCountPerSecond.getWriteRequestCountPerSecond(), is(0L));
+
+    requestCountPerSecond.refresh(2000, 1300, 1200);
+    assertThat(requestCountPerSecond.getRequestCountPerSecond(), is(2000L));
+    assertThat(requestCountPerSecond.getReadRequestCountPerSecond(), is(1000L));
+    assertThat(requestCountPerSecond.getWriteRequestCountPerSecond(), is(1000L));
+
+    requestCountPerSecond.refresh(12000, 5300, 2200);
+    assertThat(requestCountPerSecond.getRequestCountPerSecond(), is(500L));
+    assertThat(requestCountPerSecond.getReadRequestCountPerSecond(), is(400L));
+    assertThat(requestCountPerSecond.getWriteRequestCountPerSecond(), is(100L));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestTableMode.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestTableMode.java
new file mode 100644
index 0000000..8376591
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/mode/TestTableMode.java
@@ -0,0 +1,73 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestTableMode extends TestModeBase {
+
+  @Override
+  protected Mode getMode() {
+    return Mode.TABLE;
+  }
+
+  @Override
+  protected void assertRecords(List<Record> records) {
+    TestUtils.assertRecordsInTableMode(records);
+  }
+
+  @Override
+  protected void assertDrillDown(Record currentRecord, DrillDownInfo drillDownInfo) {
+    assertThat(drillDownInfo.getNextMode(), is(Mode.REGION));
+    assertThat(drillDownInfo.getInitialFilters().size(), is(2));
+
+    String tableName = String.format("%s:%s", currentRecord.get(Field.NAMESPACE).asString(),
+      currentRecord.get(Field.TABLE).asString());
+
+    switch (tableName) {
+      case "default:table1":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(), is("NAMESPACE==default"));
+        assertThat(drillDownInfo.getInitialFilters().get(1).toString(), is("TABLE==table1"));
+        break;
+
+      case "default:table2":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(), is("NAMESPACE==default"));
+        assertThat(drillDownInfo.getInitialFilters().get(1).toString(), is("TABLE==table2"));
+        break;
+
+      case "namespace:table3":
+        assertThat(drillDownInfo.getInitialFilters().get(0).toString(),
+          is("NAMESPACE==namespace"));
+        assertThat(drillDownInfo.getInitialFilters().get(1).toString(), is("TABLE==table3"));
+        break;
+
+      default:
+        fail();
+    }
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/field/TestFieldScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/field/TestFieldScreenPresenter.java
new file mode 100644
index 0000000..b8baa9a
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/field/TestFieldScreenPresenter.java
@@ -0,0 +1,151 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.field;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.verify;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.top.TopScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestFieldScreenPresenter {
+
+  @Mock
+  private FieldScreenView fieldScreenView;
+
+  private int sortFieldPosition = -1;
+  private List<Field> fields;
+  private EnumMap<Field, Boolean> fieldDisplayMap;
+
+  @Mock
+  private FieldScreenPresenter.ResultListener resultListener;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  private FieldScreenPresenter fieldScreenPresenter;
+
+  @Before
+  public void setup() {
+    Field sortField = Mode.REGION.getDefaultSortField();
+
+    fields = new ArrayList<>();
+    for (FieldInfo fieldInfo : Mode.REGION.getFieldInfos()) {
+      fields.add(fieldInfo.getField());
+    }
+
+    fieldDisplayMap = new EnumMap<>(Field.class);
+    for (FieldInfo fieldInfo : Mode.REGION.getFieldInfos()) {
+      fieldDisplayMap.put(fieldInfo.getField(), fieldInfo.isDisplayByDefault());
+    }
+
+    fieldScreenPresenter =
+      new FieldScreenPresenter(fieldScreenView, sortField, fields, fieldDisplayMap, resultListener,
+        topScreenView);
+
+    for (int i = 0; i < fields.size(); i++) {
+      Field field = fields.get(i);
+      if (field == sortField) {
+        sortFieldPosition = i;
+        break;
+      }
+    }
+  }
+
+  @Test
+  public void testInit() {
+    fieldScreenPresenter.init();
+
+    int modeHeaderMaxLength = "#COMPingCell".length();
+    int modeDescriptionMaxLength = "Write Request Count per second".length();
+
+    verify(fieldScreenView).showFieldScreen(eq("#REQ/S"), eq(fields), eq(fieldDisplayMap),
+      eq(sortFieldPosition), eq(modeHeaderMaxLength), eq(modeDescriptionMaxLength), eq(false));
+  }
+
+  @Test
+  public void testChangeSortField() {
+    fieldScreenPresenter.arrowUp();
+    fieldScreenPresenter.setSortField();
+
+    fieldScreenPresenter.arrowDown();
+    fieldScreenPresenter.arrowDown();
+    fieldScreenPresenter.setSortField();
+
+    fieldScreenPresenter.pageUp();
+    fieldScreenPresenter.setSortField();
+
+    fieldScreenPresenter.pageDown();
+    fieldScreenPresenter.setSortField();
+
+    InOrder inOrder = inOrder(fieldScreenView);
+    inOrder.verify(fieldScreenView).showScreenDescription(eq("LRS"));
+    inOrder.verify(fieldScreenView).showScreenDescription(eq("#READ/S"));
+    inOrder.verify(fieldScreenView).showScreenDescription(eq(fields.get(0).getHeader()));
+    inOrder.verify(fieldScreenView).showScreenDescription(
+      eq(fields.get(fields.size() - 1).getHeader()));
+  }
+
+  @Test
+  public void testSwitchFieldDisplay() {
+    fieldScreenPresenter.switchFieldDisplay();
+    fieldScreenPresenter.switchFieldDisplay();
+
+    InOrder inOrder = inOrder(fieldScreenView);
+    inOrder.verify(fieldScreenView).showField(anyInt(), any(Field.class), eq(false),
+      anyBoolean(), anyInt(), anyInt(), anyBoolean());
+    inOrder.verify(fieldScreenView).showField(anyInt(), any(Field.class), eq(true),
+      anyBoolean(), anyInt(), anyInt(), anyBoolean());
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testChangeFieldsOrder() {
+    fieldScreenPresenter.turnOnMoveMode();
+    fieldScreenPresenter.arrowUp();
+    fieldScreenPresenter.turnOffMoveMode();
+
+    Field removed = fields.remove(sortFieldPosition);
+    fields.add(sortFieldPosition - 1, removed);
+
+    assertThat(fieldScreenPresenter.transitionToNextScreen(), is((ScreenView) topScreenView));
+    verify(resultListener).accept(any(Field.class), eq(fields), any(EnumMap.class));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/help/TestHelpScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/help/TestHelpScreenPresenter.java
new file mode 100644
index 0000000..51d2e95
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/help/TestHelpScreenPresenter.java
@@ -0,0 +1,75 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.help;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.top.TopScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestHelpScreenPresenter {
+
+  private static final long TEST_REFRESH_DELAY = 5;
+
+  @Mock
+  private HelpScreenView helpScreenView;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  private HelpScreenPresenter helpScreenPresenter;
+
+  @Before
+  public void setup() {
+    helpScreenPresenter = new HelpScreenPresenter(helpScreenView, TEST_REFRESH_DELAY,
+      topScreenView);
+  }
+
+  @Test
+  public void testInit() {
+    helpScreenPresenter.init();
+
+    verify(helpScreenView).showHelpScreen(eq(TEST_REFRESH_DELAY), argThat(
+      new ArgumentMatcher<CommandDescription[]>() {
+        @Override
+        public boolean matches(Object o) {
+          return ((CommandDescription[]) o).length == 14;
+        }
+      }));
+  }
+
+  @Test
+  public void testTransitionToTopScreen() {
+    assertThat(helpScreenPresenter.transitionToNextScreen(), is((ScreenView) topScreenView));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/mode/TestModeScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/mode/TestModeScreenPresenter.java
new file mode 100644
index 0000000..276a5ed
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/mode/TestModeScreenPresenter.java
@@ -0,0 +1,140 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.mode;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import java.util.Arrays;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.hbtop.screen.top.TopScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestModeScreenPresenter {
+
+  @Mock
+  private ModeScreenView modeScreenView;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  @Mock
+  private ModeScreenPresenter.ResultListener resultListener;
+
+  private ModeScreenPresenter createModeScreenPresenter(Mode currentMode) {
+    return new ModeScreenPresenter(modeScreenView, currentMode, resultListener, topScreenView);
+  }
+
+  @Test
+  public void testInit() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.init();
+
+    int modeHeaderMaxLength = Mode.REGION_SERVER.getHeader().length();
+    int modeDescriptionMaxLength = Mode.REGION_SERVER.getDescription().length();
+
+    verify(modeScreenView).showModeScreen(eq(Mode.REGION), eq(Arrays.asList(Mode.values())),
+      eq(Mode.REGION.ordinal()) , eq(modeHeaderMaxLength), eq(modeDescriptionMaxLength));
+  }
+
+  @Test
+  public void testSelectNamespaceMode() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.arrowUp();
+    modeScreenPresenter.arrowUp();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    verify(resultListener).accept(eq(Mode.NAMESPACE));
+  }
+
+  @Test
+  public void testSelectTableMode() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.arrowUp();
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    verify(resultListener).accept(eq(Mode.TABLE));
+  }
+
+  @Test
+  public void testSelectRegionMode() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.NAMESPACE);
+
+    modeScreenPresenter.arrowDown();
+    modeScreenPresenter.arrowDown();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    verify(resultListener).accept(eq(Mode.REGION));
+  }
+
+  @Test
+  public void testSelectRegionServerMode() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.arrowDown();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    verify(resultListener).accept(eq(Mode.REGION_SERVER));
+  }
+
+  @Test
+  public void testCancelSelectingMode() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.arrowDown();
+    modeScreenPresenter.arrowDown();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(false), is((ScreenView) topScreenView));
+    verify(resultListener, never()).accept(any(Mode.class));
+  }
+
+  @Test
+  public void testPageUp() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.pageUp();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    verify(resultListener).accept(eq(Mode.values()[0]));
+  }
+
+  @Test
+  public void testPageDown() {
+    ModeScreenPresenter modeScreenPresenter = createModeScreenPresenter(Mode.REGION);
+
+    modeScreenPresenter.pageDown();
+
+    assertThat(modeScreenPresenter.transitionToNextScreen(true), is((ScreenView) topScreenView));
+    Mode[] modes = Mode.values();
+    verify(resultListener).accept(eq(modes[modes.length - 1]));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestFilterDisplayModeScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestFilterDisplayModeScreenPresenter.java
new file mode 100644
index 0000000..41a5fde
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestFilterDisplayModeScreenPresenter.java
@@ -0,0 +1,91 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.verify;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestFilterDisplayModeScreenPresenter {
+
+  @Mock
+  private FilterDisplayModeScreenView filterDisplayModeScreenView;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  private FilterDisplayModeScreenPresenter filterDisplayModeScreenPresenter;
+
+  @Before
+  public void setup() {
+    List<Field> fields = new ArrayList<>();
+    for (FieldInfo fieldInfo : Mode.REGION.getFieldInfos()) {
+      fields.add(fieldInfo.getField());
+    }
+
+    List<RecordFilter>  filters = new ArrayList<>();
+    filters.add(RecordFilter.parse("NAMESPACE==namespace", fields, true));
+    filters.add(RecordFilter.parse("TABLE==table", fields, true));
+
+    filterDisplayModeScreenPresenter = new FilterDisplayModeScreenPresenter(
+      filterDisplayModeScreenView, filters, topScreenView);
+  }
+
+  @Test
+  public void testInit() {
+    filterDisplayModeScreenPresenter.init();
+
+    verify(filterDisplayModeScreenView).showFilters(argThat(
+      new ArgumentMatcher<List<RecordFilter>>() {
+        @Override
+        @SuppressWarnings("unchecked")
+        public boolean matches(Object argument) {
+          List<RecordFilter> filters = (List<RecordFilter>) argument;
+          return filters.size() == 2
+            && filters.get(0).toString().equals("NAMESPACE==namespace")
+            && filters.get(1).toString().equals("TABLE==table");
+        }
+      }));
+  }
+
+  @Test
+  public void testReturnToTopScreen() {
+    assertThat(filterDisplayModeScreenPresenter.returnToNextScreen(),
+      is((ScreenView) topScreenView));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestInputModeScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestInputModeScreenPresenter.java
new file mode 100644
index 0000000..ec38284
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestInputModeScreenPresenter.java
@@ -0,0 +1,198 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestInputModeScreenPresenter {
+
+  private static final String TEST_INPUT_MESSAGE = "test input message";
+
+  @Mock
+  private InputModeScreenView inputModeScreenView;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  @Mock
+  private InputModeScreenPresenter.ResultListener resultListener;
+
+  private InputModeScreenPresenter inputModeScreenPresenter;
+
+  @Before
+  public void setup() {
+    List<String> histories = new ArrayList<>();
+    histories.add("history1");
+    histories.add("history2");
+
+    inputModeScreenPresenter = new InputModeScreenPresenter(inputModeScreenView,
+      TEST_INPUT_MESSAGE, histories, resultListener);
+  }
+
+  @Test
+  public void testInit() {
+    inputModeScreenPresenter.init();
+
+    verify(inputModeScreenView).showInput(eq(TEST_INPUT_MESSAGE), eq(""), eq(0));
+  }
+
+  @Test
+  public void testCharacter() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+  }
+
+  @Test
+  public void testArrowLeftAndRight() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+    inputModeScreenPresenter.arrowLeft();
+    inputModeScreenPresenter.arrowLeft();
+    inputModeScreenPresenter.arrowLeft();
+    inputModeScreenPresenter.arrowLeft();
+    inputModeScreenPresenter.arrowRight();
+    inputModeScreenPresenter.arrowRight();
+    inputModeScreenPresenter.arrowRight();
+    inputModeScreenPresenter.arrowRight();
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(0));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+  }
+
+  @Test
+  public void testHomeAndEnd() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+    inputModeScreenPresenter.home();
+    inputModeScreenPresenter.home();
+    inputModeScreenPresenter.end();
+    inputModeScreenPresenter.end();
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(0));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+  }
+
+  @Test
+  public void testBackspace() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+    inputModeScreenPresenter.backspace();
+    inputModeScreenPresenter.backspace();
+    inputModeScreenPresenter.backspace();
+    inputModeScreenPresenter.backspace();
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq(""), eq(0));
+  }
+
+  @Test
+  public void testDelete() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+    inputModeScreenPresenter.delete();
+    inputModeScreenPresenter.arrowLeft();
+    inputModeScreenPresenter.delete();
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+  }
+
+  @Test
+  public void testHistories() {
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+    inputModeScreenPresenter.arrowUp();
+    inputModeScreenPresenter.arrowUp();
+    inputModeScreenPresenter.arrowUp();
+    inputModeScreenPresenter.arrowDown();
+    inputModeScreenPresenter.arrowDown();
+    inputModeScreenPresenter.arrowDown();
+
+    InOrder inOrder = inOrder(inputModeScreenView);
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("a"), eq(1));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("ab"), eq(2));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("abc"), eq(3));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("history2"), eq(8));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("history1"), eq(8));
+    inOrder.verify(inputModeScreenView).showInput(any(String.class), eq("history2"), eq(8));
+  }
+
+  @Test
+  public void testReturnToTopScreen() {
+    when(resultListener.apply(any(String.class))).thenReturn(topScreenView);
+
+    inputModeScreenPresenter.character('a');
+    inputModeScreenPresenter.character('b');
+    inputModeScreenPresenter.character('c');
+
+    assertThat(inputModeScreenPresenter.returnToNextScreen(), is((ScreenView) topScreenView));
+    verify(resultListener).apply(eq("abc"));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestMessageModeScreenPresenter.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestMessageModeScreenPresenter.java
new file mode 100644
index 0000000..9c17dca
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestMessageModeScreenPresenter.java
@@ -0,0 +1,65 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+
+import org.apache.hadoop.hbase.hbtop.screen.ScreenView;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestMessageModeScreenPresenter {
+
+  private static final String TEST_MESSAGE = "test message";
+
+  @Mock
+  private MessageModeScreenView messageModeScreenView;
+
+  @Mock
+  private TopScreenView topScreenView;
+
+  private MessageModeScreenPresenter messageModeScreenPresenter;
+
+  @Before
+  public void setup() {
+    messageModeScreenPresenter = new MessageModeScreenPresenter(messageModeScreenView,
+      TEST_MESSAGE, topScreenView);
+  }
+
+  @Test
+  public void testInit() {
+    messageModeScreenPresenter.init();
+
+    verify(messageModeScreenView).showMessage(eq(TEST_MESSAGE));
+  }
+
+  @Test
+  public void testReturnToTopScreen() {
+    assertThat(messageModeScreenPresenter.returnToNextScreen(), is((ScreenView) topScreenView));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestPaging.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestPaging.java
new file mode 100644
index 0000000..53500bc
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestPaging.java
@@ -0,0 +1,293 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category(SmallTests.class)
+public class TestPaging {
+
+  @Test
+  public void testArrowUpAndArrowDown() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(5);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 1, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 3, 1, 4);
+
+    paging.arrowDown();
+    assertPaging(paging, 4, 2, 5);
+
+    paging.arrowDown();
+    assertPaging(paging, 4, 2, 5);
+
+    paging.arrowUp();
+    assertPaging(paging, 3, 2, 5);
+
+    paging.arrowUp();
+    assertPaging(paging, 2, 2, 5);
+
+    paging.arrowUp();
+    assertPaging(paging, 1, 1, 4);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 3);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 3);
+  }
+
+  @Test
+  public void testPageUpAndPageDown() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(8);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.pageDown();
+    assertPaging(paging, 3, 3, 6);
+
+    paging.pageDown();
+    assertPaging(paging, 6, 5, 8);
+
+    paging.pageDown();
+    assertPaging(paging, 7, 5, 8);
+
+    paging.pageDown();
+    assertPaging(paging, 7, 5, 8);
+
+    paging.pageUp();
+    assertPaging(paging, 4, 4, 7);
+
+    paging.pageUp();
+    assertPaging(paging, 1, 1, 4);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 3);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 3);
+  }
+
+  @Test
+  public void testInit() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(5);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.pageDown();
+    paging.pageDown();
+    paging.pageDown();
+    paging.pageDown();
+    paging.init();
+
+    assertPaging(paging, 0, 0, 3);
+  }
+
+  @Test
+  public void testWhenPageSizeGraterThanRecordsSize() {
+    Paging paging = new Paging();
+    paging.updatePageSize(5);
+    paging.updateRecordsSize(3);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 1, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.arrowUp();
+    assertPaging(paging, 1, 0, 3);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 3);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 3);
+
+    paging.pageDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.pageDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 3);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 3);
+  }
+
+  @Test
+  public void testWhenPageSizeIsZero() {
+    Paging paging = new Paging();
+    paging.updatePageSize(0);
+    paging.updateRecordsSize(5);
+
+    assertPaging(paging, 0, 0, 0);
+
+    paging.arrowDown();
+    assertPaging(paging, 1, 0, 0);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageDown();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 0);
+  }
+
+  @Test
+  public void testWhenRecordsSizeIsZero() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(0);
+
+    assertPaging(paging, 0, 0, 0);
+
+    paging.arrowDown();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageDown();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 0);
+  }
+
+  @Test
+  public void testWhenChangingPageSizeDynamically() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(5);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 1, 0, 3);
+
+    paging.updatePageSize(2);
+    assertPaging(paging, 1, 0, 2);
+
+    paging.arrowDown();
+    assertPaging(paging, 2, 1, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 3, 2, 4);
+
+    paging.updatePageSize(4);
+    assertPaging(paging, 3, 1, 5);
+
+    paging.updatePageSize(5);
+    assertPaging(paging, 3, 0, 5);
+
+    paging.updatePageSize(0);
+    assertPaging(paging, 3, 0, 0);
+
+    paging.arrowDown();
+    assertPaging(paging, 4, 0, 0);
+
+    paging.arrowUp();
+    assertPaging(paging, 3, 0, 0);
+
+    paging.pageDown();
+    assertPaging(paging, 3, 0, 0);
+
+    paging.pageUp();
+    assertPaging(paging, 3, 0, 0);
+
+    paging.updatePageSize(1);
+    assertPaging(paging, 3, 3, 4);
+  }
+
+  @Test
+  public void testWhenChangingRecordsSizeDynamically() {
+    Paging paging = new Paging();
+    paging.updatePageSize(3);
+    paging.updateRecordsSize(5);
+
+    assertPaging(paging, 0, 0, 3);
+
+    paging.updateRecordsSize(2);
+    assertPaging(paging, 0, 0, 2);
+    assertThat(paging.getCurrentPosition(), is(0));
+    assertThat(paging.getPageStartPosition(), is(0));
+    assertThat(paging.getPageEndPosition(), is(2));
+
+    paging.arrowDown();
+    assertPaging(paging, 1, 0, 2);
+
+    paging.updateRecordsSize(3);
+    assertPaging(paging, 1, 0, 3);
+
+    paging.arrowDown();
+    assertPaging(paging, 2, 0, 3);
+
+    paging.updateRecordsSize(1);
+    assertPaging(paging, 0, 0, 1);
+
+    paging.updateRecordsSize(0);
+    assertPaging(paging, 0, 0, 0);
+
+    paging.arrowDown();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.arrowUp();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageDown();
+    assertPaging(paging, 0, 0, 0);
+
+    paging.pageUp();
+    assertPaging(paging, 0, 0, 0);
+  }
+
+  private void assertPaging(Paging paging, int currentPosition, int pageStartPosition,
+    int pageEndPosition) {
+    assertThat(paging.getCurrentPosition(), is(currentPosition));
+    assertThat(paging.getPageStartPosition(), is(pageStartPosition));
+    assertThat(paging.getPageEndPosition(), is(pageEndPosition));
+  }
+}
diff --git a/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestTopScreenModel.java b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestTopScreenModel.java
new file mode 100644
index 0000000..9dec535
--- /dev/null
+++ b/hbase-hbtop/src/test/java/org/apache/hadoop/hbase/hbtop/screen/top/TestTopScreenModel.java
@@ -0,0 +1,200 @@
+/**
+ * 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.
+ */
+package org.apache.hadoop.hbase.hbtop.screen.top;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.hbtop.Record;
+import org.apache.hadoop.hbase.hbtop.RecordFilter;
+import org.apache.hadoop.hbase.hbtop.TestUtils;
+import org.apache.hadoop.hbase.hbtop.field.Field;
+import org.apache.hadoop.hbase.hbtop.field.FieldInfo;
+import org.apache.hadoop.hbase.hbtop.field.FieldValue;
+import org.apache.hadoop.hbase.hbtop.mode.Mode;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@Category(SmallTests.class)
+@RunWith(MockitoJUnitRunner.class)
+public class TestTopScreenModel {
+
+  @Mock
+  private Admin admin;
+
+  private TopScreenModel topScreenModel;
+
+  private List<Field> fields;
+
+  @Before
+  public void setup() throws IOException {
+    when(admin.getClusterStatus()).thenReturn(TestUtils.createDummyClusterStatus());
+    topScreenModel = new TopScreenModel(admin, Mode.REGION);
+
+    fields = new ArrayList<>();
+    for (FieldInfo fieldInfo : Mode.REGION.getFieldInfos()) {
+      fields.add(fieldInfo.getField());
+    }
+  }
+
+  @Test
+  public void testSummary() {
+    topScreenModel.refreshMetricsData();
+    Summary summary = topScreenModel.getSummary();
+    TestUtils.assertSummary(summary);
+  }
+
+  @Test
+  public void testRecords() {
+    // Region Mode
+    topScreenModel.refreshMetricsData();
+    TestUtils.assertRecordsInRegionMode(topScreenModel.getRecords());
+
+    // Namespace Mode
+    topScreenModel.switchMode(Mode.NAMESPACE, null, false);
+    topScreenModel.refreshMetricsData();
+    TestUtils.assertRecordsInNamespaceMode(topScreenModel.getRecords());
+
+    // Table Mode
+    topScreenModel.switchMode(Mode.TABLE, null, false);
+    topScreenModel.refreshMetricsData();
+    TestUtils.assertRecordsInTableMode(topScreenModel.getRecords());
+
+    // Namespace Mode
+    topScreenModel.switchMode(Mode.REGION_SERVER, null, false);
+    topScreenModel.refreshMetricsData();
... 642 lines suppressed ...