Repository: karaf
Updated Branches:
  refs/heads/master ec5e9b75d -> 887770ed4


[KARAF-4775] Implement a thread top command

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

Branch: refs/heads/master
Commit: aebb6fd04e7cf30cfac2775c812077e2d3431ff7
Parents: ec5e9b7
Author: Guillaume Nodet <gno...@apache.org>
Authored: Mon Oct 17 11:48:28 2016 +0200
Committer: Guillaume Nodet <gno...@apache.org>
Committed: Mon Oct 17 11:51:41 2016 +0200

----------------------------------------------------------------------
 .../org/apache/karaf/main/ConfigProperties.java |  17 +-
 .../karaf/shell/commands/impl/TTopAction.java   |  68 +++
 .../karaf/shell/commands/impl/top/TTop.java     | 593 +++++++++++++++++++
 .../services/org/apache/karaf/shell/commands    |   1 +
 4 files changed, 678 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/aebb6fd0/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
----------------------------------------------------------------------
diff --git a/main/src/main/java/org/apache/karaf/main/ConfigProperties.java 
b/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
index 0383191..b8ef054 100644
--- a/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
+++ b/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
@@ -21,6 +21,8 @@ package org.apache.karaf.main;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadMXBean;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Arrays;
@@ -135,6 +137,8 @@ public class ConfigProperties {
     
     private static final String KARAF_DELAY_CONSOLE = "karaf.delay.console";
 
+    private static final String KARAF_THREAD_MONITORING = 
"karaf.thread.monitoring";
+
     private static final String PROPERTY_LOCK_CLASS_DEFAULT = 
SimpleFileLock.class.getName();
 
     private static final String SECURITY_PROVIDERS = 
"org.apache.karaf.security.providers";
@@ -175,6 +179,7 @@ public class ConfigProperties {
     String shutdownCommand;
     String startupMessage;
     boolean delayConsoleStart;
+    boolean threadMonitoring;
     
     public ConfigProperties() throws Exception {
         this.karafHome = Utils.getKarafHome(ConfigProperties.class, 
PROP_KARAF_HOME, ENV_KARAF_HOME);
@@ -222,7 +227,8 @@ public class ConfigProperties {
         this.shutdownCommand = props.getProperty(KARAF_SHUTDOWN_COMMAND);
         this.startupMessage = props.getProperty(KARAF_STARTUP_MESSAGE, "Apache 
Karaf starting up. Press Enter to open the shell now...");
         this.delayConsoleStart = 
Boolean.parseBoolean(props.getProperty(KARAF_DELAY_CONSOLE, "false"));
-        System.setProperty(KARAF_DELAY_CONSOLE, new 
Boolean(this.delayConsoleStart).toString());
+        this.threadMonitoring = 
Boolean.parseBoolean(props.getProperty(KARAF_THREAD_MONITORING, "false"));
+        System.setProperty(KARAF_DELAY_CONSOLE, 
Boolean.toString(this.delayConsoleStart));
     }
 
     public void performInit() throws Exception {
@@ -265,6 +271,15 @@ public class ConfigProperties {
                 System.err.println("WARN: can't update etc/config.properties 
with the generated command shutdown. We advise to manually add the 
karaf.shutdown.command property.");
             }
         }
+        if (threadMonitoring) {
+            ThreadMXBean threadsBean = ManagementFactory.getThreadMXBean();
+            if (threadsBean.isThreadCpuTimeSupported()) {
+                threadsBean.setThreadCpuTimeEnabled(true);
+            }
+            if (threadsBean.isThreadContentionMonitoringSupported()) {
+                threadsBean.setThreadContentionMonitoringEnabled(true);
+            }
+        }
     }
     
     private String getPropertyOrFail(String propertyName) {

http://git-wip-us.apache.org/repos/asf/karaf/blob/aebb6fd0/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/TTopAction.java
----------------------------------------------------------------------
diff --git 
a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/TTopAction.java
 
b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/TTopAction.java
new file mode 100644
index 0000000..4790c6c
--- /dev/null
+++ 
b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/TTopAction.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.shell.commands.impl;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.commands.impl.top.TTop;
+import org.jline.terminal.Terminal;
+
+import java.util.Arrays;
+
+
+/**
+ * Display threads infos in a table.
+ */
+@Command(scope = "shell", name = "ttop", description = "Display threads 
information")
+@Service
+public class TTopAction implements Action {
+
+    @Option(name = "--order" , aliases = { "-o" }, description = "Comma 
separated list of sorting keys")
+    String order;
+
+    @Option(name = "--stats" , aliases = { "-t" }, description = "Comma 
separated list of stats to display")
+    String stats;
+
+    @Option(name = "--seconds" , aliases = { "-s" }, description = "Delay 
between updates in seconds")
+    Integer seconds;
+
+    @Option(name = "--millis" , aliases = { "-m" }, description = "Delay 
between updates in milliseconds")
+    Integer millis;
+
+    @Option(name = "--nthreads", aliases = { "-n" }, description = "Only 
display up to NTHREADS threads")
+    int nthreads = -1;
+
+    @Reference
+    Session session;
+
+    @Override
+    public Object execute() throws Exception {
+        TTop ttop = new TTop((Terminal) session.get(".jline.terminal"));
+        ttop.sort = order != null ? Arrays.asList(order.split(",")) : null;
+        ttop.delay = seconds != null ? seconds * 1000 : ttop.delay;
+        ttop.delay = millis != null ? millis : ttop.delay;
+        ttop.stats = stats != null ? Arrays.asList(stats.split(",")) : null;
+        ttop.nthreads = nthreads;
+        ttop.run();
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/aebb6fd0/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/top/TTop.java
----------------------------------------------------------------------
diff --git 
a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/top/TTop.java
 
b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/top/TTop.java
new file mode 100644
index 0000000..f3ba964
--- /dev/null
+++ 
b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/top/TTop.java
@@ -0,0 +1,593 @@
+/*
+ * 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.karaf.shell.commands.impl.top;
+
+import org.jline.keymap.BindingReader;
+import org.jline.keymap.KeyMap;
+import org.jline.terminal.Attributes;
+import org.jline.terminal.Size;
+import org.jline.terminal.Terminal;
+import org.jline.utils.*;
+
+import java.io.IOException;
+import java.lang.management.*;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static org.apache.karaf.shell.commands.impl.top.TTop.Align.Left;
+import static org.apache.karaf.shell.commands.impl.top.TTop.Align.Right;
+
+/**
+ * Thread Top implementation based on jline.
+ *
+ * TODO: option modification at runtime (such as implemented in less) is not 
currently supported
+ * TODO: one possible addition would be to detect deadlock threads and display 
them in a specific way
+ */
+public class TTop {
+
+    public static final String STAT_UPTIME = "uptime";
+
+    public static final String STAT_TID = "tid";
+    public static final String STAT_NAME = "name";
+    public static final String STAT_STATE = "state";
+    public static final String STAT_BLOCKED_TIME = "blocked_time";
+    public static final String STAT_BLOCKED_COUNT = "blocked_count";
+    public static final String STAT_WAITED_TIME = "waited_time";
+    public static final String STAT_WAITED_COUNT = "waited_count";
+    public static final String STAT_LOCK_NAME = "lock_name";
+    public static final String STAT_LOCK_OWNER_ID = "lock_owner_id";
+    public static final String STAT_LOCK_OWNER_NAME = "lock_owner_name";
+    public static final String STAT_USER_TIME = "user_time";
+    public static final String STAT_USER_TIME_PERC = "user_time_perc";
+    public static final String STAT_CPU_TIME = "cpu_time";
+    public static final String STAT_CPU_TIME_PERC = "cpu_time_perc";
+
+    public List<String> sort;
+    public long delay;
+    public List<String> stats;
+    public int nthreads;
+
+    public enum Align {
+        Left, Right
+    };
+
+    public enum Operation {
+        INCREASE_DELAY,
+        DECREASE_DELAY,
+        HELP,
+        EXIT,
+        CLEAR,
+        REVERSE
+    }
+
+    private final Map<String, Column> columns = new LinkedHashMap<>();
+    private final Terminal terminal;
+    private final Display display;
+    private final BindingReader bindingReader;
+    private final KeyMap<Operation> keys;
+    private final Size size = new Size();
+
+    private Comparator<Map<String, Comparable>> comparator;
+
+    // Internal cache data
+    private Map<Long, Map<String, Object>> previous = new HashMap<>();
+    private Map<Long, Map<String, Long>> changes = new HashMap<>();
+    private Map<String, Integer> widths = new HashMap<>();
+
+
+    public TTop(Terminal terminal) {
+        this.terminal = terminal;
+        this.display = new Display(terminal, true);
+        this.bindingReader = new BindingReader(terminal.reader());
+
+        DecimalFormatSymbols dfs = new DecimalFormatSymbols();
+        dfs.setDecimalSeparator('.');
+        DecimalFormat perc = new DecimalFormat("0.00%", dfs);
+
+        register(STAT_TID,             Right, "TID",             o -> 
String.format("%3d", (Long) o));
+        register(STAT_NAME,            Left,  "NAME",            padcut(40));
+        register(STAT_STATE,           Left,  "STATE",           o -> 
o.toString().toLowerCase());
+        register(STAT_BLOCKED_TIME,    Right, "T-BLOCKED",       o -> 
millis((Long) o));
+        register(STAT_BLOCKED_COUNT,   Right, "#-BLOCKED",       
Object::toString);
+        register(STAT_WAITED_TIME,     Right, "T-WAITED",        o -> 
millis((Long) o));
+        register(STAT_WAITED_COUNT,    Right, "#-WAITED",        
Object::toString);
+        register(STAT_LOCK_NAME,       Left,  "LOCK-NAME",       
Object::toString);
+        register(STAT_LOCK_OWNER_ID,   Right, "LOCK-OWNER-ID",   id -> ((Long) 
id) >= 0 ? id.toString() : "");
+        register(STAT_LOCK_OWNER_NAME, Left,  "LOCK-OWNER-NAME", name -> name 
!= null ? name.toString() : "");
+        register(STAT_USER_TIME,       Right, "T-USR",           o -> 
nanos((Long) o));
+        register(STAT_CPU_TIME,        Right, "T-CPU",           o -> 
nanos((Long) o));
+        register(STAT_USER_TIME_PERC,  Right, "%-USR",           perc::format);
+        register(STAT_CPU_TIME_PERC,   Right, "%-CPU",           perc::format);
+
+        keys = new KeyMap<>();
+        bindKeys(keys);
+    }
+
+    public KeyMap<Operation> getKeys() {
+        return keys;
+    }
+
+    public void run() throws IOException, InterruptedException {
+        comparator = buildComparator(sort);
+        delay = delay > 0 ? Math.max(delay, 100) : 1000;
+        if (stats == null || stats.isEmpty()) {
+            stats = Arrays.asList(STAT_TID, STAT_NAME, STAT_STATE, 
STAT_CPU_TIME, STAT_LOCK_OWNER_ID);
+        }
+
+        Boolean isThreadContentionMonitoringEnabled = null;
+        ThreadMXBean threadsBean = ManagementFactory.getThreadMXBean();
+        if (stats.contains(STAT_BLOCKED_TIME)
+                || stats.contains(STAT_BLOCKED_COUNT)
+                || stats.contains(STAT_WAITED_TIME)
+                || stats.contains(STAT_WAITED_COUNT)) {
+            if (threadsBean.isThreadContentionMonitoringSupported()) {
+                isThreadContentionMonitoringEnabled = 
threadsBean.isThreadContentionMonitoringEnabled();
+                if (!isThreadContentionMonitoringEnabled) {
+                    threadsBean.setThreadContentionMonitoringEnabled(true);
+                }
+            } else {
+                stats.removeAll(Arrays.asList(STAT_BLOCKED_TIME, 
STAT_BLOCKED_COUNT, STAT_WAITED_TIME, STAT_WAITED_COUNT));
+            }
+        }
+        Boolean isThreadCpuTimeEnabled = null;
+        if (stats.contains(STAT_USER_TIME) || stats.contains(STAT_CPU_TIME)) {
+            if (threadsBean.isThreadCpuTimeSupported()) {
+                isThreadCpuTimeEnabled = threadsBean.isThreadCpuTimeEnabled();
+                if (!isThreadCpuTimeEnabled) {
+                    threadsBean.setThreadCpuTimeEnabled(true);
+                }
+            } else {
+                stats.removeAll(Arrays.asList(STAT_USER_TIME, STAT_CPU_TIME));
+            }
+        }
+
+        size.copy(terminal.getSize());
+        Terminal.SignalHandler prevHandler = 
terminal.handle(Terminal.Signal.WINCH, this::handle);
+        Attributes attr = terminal.enterRawMode();
+        try {
+
+            // Use alternate buffer
+            terminal.puts(InfoCmp.Capability.enter_ca_mode);
+            terminal.puts(InfoCmp.Capability.keypad_xmit);
+            terminal.puts(InfoCmp.Capability.cursor_invisible);
+            terminal.writer().flush();
+
+            long t0 = System.currentTimeMillis();
+
+            Operation op;
+            do {
+                display();
+                checkInterrupted();
+
+                op = null;
+
+                long delta = ((System.currentTimeMillis() - t0) / delay + 1) * 
delay + t0 - System.currentTimeMillis();
+                int ch = bindingReader.peekCharacter(delta);
+                if (ch == -1) {
+                    op = Operation.EXIT;
+                } else if (ch != NonBlockingReader.READ_EXPIRED) {
+                    op = bindingReader.readBinding(keys, null, false);
+                }
+                if (op == null) {
+                    continue;
+                }
+
+                switch (op) {
+                    case INCREASE_DELAY:
+                        delay = delay * 2;
+                        t0 = System.currentTimeMillis();
+                        break;
+                    case DECREASE_DELAY:
+                        delay = Math.max(delay / 2, 16);
+                        t0 = System.currentTimeMillis();
+                        break;
+                    case CLEAR:
+                        display.clear();
+                        break;
+                    case REVERSE:
+                        comparator = comparator.reversed();
+                        break;
+                }
+            } while (op != Operation.EXIT);
+        } catch (InterruptedException ie) {
+            // Do nothing
+        } finally {
+            terminal.setAttributes(attr);
+            if (prevHandler != null) {
+                terminal.handle(Terminal.Signal.WINCH, prevHandler);
+            }
+            // Use main buffer
+            terminal.puts(InfoCmp.Capability.exit_ca_mode);
+            terminal.puts(InfoCmp.Capability.keypad_local);
+            terminal.puts(InfoCmp.Capability.cursor_visible);
+            terminal.writer().flush();
+
+            if (isThreadContentionMonitoringEnabled != null) {
+                
threadsBean.setThreadContentionMonitoringEnabled(isThreadContentionMonitoringEnabled);
+            }
+            if (isThreadCpuTimeEnabled != null) {
+                threadsBean.setThreadCpuTimeEnabled(isThreadCpuTimeEnabled);
+            }
+        }
+    }
+
+    private void handle(Terminal.Signal signal) {
+        int prevw = size.getColumns();
+        size.copy(terminal.getSize());
+        try {
+            if (size.getColumns() < prevw) {
+                display.clear();
+            }
+            display();
+        } catch (IOException e) {
+            // ignore
+        }
+    }
+
+    private List<Map<String, Comparable>> infos() {
+        long ctime = ManagementFactory.getRuntimeMXBean().getUptime();
+        Long ptime = (Long) previous.computeIfAbsent(-1L, id -> new 
HashMap<>()).put(STAT_UPTIME, ctime);
+        long delta = ptime != null ? ctime - ptime : 0L;
+
+        ThreadMXBean threadsBean = ManagementFactory.getThreadMXBean();
+        ThreadInfo[] infos = threadsBean.dumpAllThreads(false, false);
+        List<Map<String, Comparable>> threads = new ArrayList<>();
+        for (ThreadInfo ti : infos) {
+            Map<String, Comparable> t = new HashMap<>();
+            t.put(STAT_TID, ti.getThreadId());
+            t.put(STAT_NAME, ti.getThreadName());
+            t.put(STAT_STATE, ti.getThreadState());
+            if (threadsBean.isThreadContentionMonitoringEnabled()) {
+                t.put(STAT_BLOCKED_TIME, ti.getBlockedTime());
+                t.put(STAT_BLOCKED_COUNT, ti.getBlockedCount());
+                t.put(STAT_WAITED_TIME, ti.getWaitedTime());
+                t.put(STAT_WAITED_COUNT, ti.getWaitedCount());
+            }
+            t.put(STAT_LOCK_NAME, ti.getLockName());
+            t.put(STAT_LOCK_OWNER_ID, ti.getLockOwnerId());
+            t.put(STAT_LOCK_OWNER_NAME, ti.getLockOwnerName());
+            if (threadsBean.isThreadCpuTimeSupported() && 
threadsBean.isThreadCpuTimeEnabled()) {
+                long tid = ti.getThreadId(), t0, t1;
+                // Cpu
+                t1 = threadsBean.getThreadCpuTime(tid);
+                t0 = (Long) previous.computeIfAbsent(tid, id -> new 
HashMap<>()).getOrDefault(STAT_CPU_TIME, t1);
+                t.put(STAT_CPU_TIME, t1);
+                t.put(STAT_CPU_TIME_PERC, (delta != 0) ? (t1 - t0) / ((double) 
delta * 1000000) : 0.0d);
+                // User
+                t1 = threadsBean.getThreadUserTime(tid);
+                t0 = (Long) previous.computeIfAbsent(tid, id -> new 
HashMap<>()).getOrDefault(STAT_USER_TIME, t1);
+                t.put(STAT_USER_TIME, t1);
+                t.put(STAT_USER_TIME_PERC, (delta != 0) ? (t1 - t0) / 
((double) delta * 1000000) : 0.0d);
+            }
+            threads.add(t);
+        }
+        return threads;
+    }
+
+    private void align(AttributedStringBuilder sb, String val, int width, 
Align align) {
+        if (align == Align.Left) {
+            sb.append(val);
+            for (int i = 0; i < width - val.length(); i++) {
+                sb.append(' ');
+            }
+        } else {
+            for (int i = 0; i < width - val.length(); i++) {
+                sb.append(' ');
+            }
+            sb.append(val);
+        }
+    }
+
+    private void display() throws IOException {
+        long now = System.currentTimeMillis();
+
+        display.resize(size.getRows(), size.getColumns());
+
+        List<AttributedString> lines = new ArrayList<>();
+        AttributedStringBuilder sb = new 
AttributedStringBuilder(size.getColumns());
+
+        // Top headers
+        sb.style(sb.style().bold());
+        sb.append("ttop");
+        sb.style(sb.style().boldOff());
+        sb.append(" - ");
+        sb.append(String.format("%8tT", new Date()));
+        sb.append(".");
+
+
+        OperatingSystemMXBean os = 
ManagementFactory.getOperatingSystemMXBean();
+        String osinfo = "OS: " + os.getName() + " " + os.getVersion() + ", " + 
os.getArch() + ", " + os.getAvailableProcessors() + " cpus.";
+        if (sb.length() + 1 + osinfo.length() < size.getColumns()) {
+            sb.append(" ");
+        } else {
+            lines.add(sb.toAttributedString());
+            sb.setLength(0);
+        }
+        sb.append(osinfo);
+
+        ClassLoadingMXBean cl = ManagementFactory.getClassLoadingMXBean();
+        String clsinfo = "Classes: " + cl.getLoadedClassCount() + " loaded, " 
+ cl.getUnloadedClassCount() + " unloaded, " + cl.getTotalLoadedClassCount() + 
" loaded total.";
+        if (sb.length() + 1 + clsinfo.length() < size.getColumns()) {
+            sb.append(" ");
+        } else {
+            lines.add(sb.toAttributedString());
+            sb.setLength(0);
+        }
+        sb.append(clsinfo);
+
+        ThreadMXBean th = ManagementFactory.getThreadMXBean();
+        String thinfo = "Threads: " + th.getThreadCount() + ", peak: " + 
th.getPeakThreadCount() + ", started: " + th.getTotalStartedThreadCount() + ".";
+        if (sb.length() + 1 + thinfo.length() < size.getColumns()) {
+            sb.append(" ");
+        } else {
+            lines.add(sb.toAttributedString());
+            sb.setLength(0);
+        }
+        sb.append(thinfo);
+
+        MemoryMXBean me = ManagementFactory.getMemoryMXBean();
+        String meinfo = "Memory: " + "heap: " + 
memory(me.getHeapMemoryUsage().getUsed(), me.getHeapMemoryUsage().getMax())
+                  + ", non heap: " + 
memory(me.getNonHeapMemoryUsage().getUsed(), 
me.getNonHeapMemoryUsage().getMax()) + ".";
+        if (sb.length() + 1 + meinfo.length() < size.getColumns()) {
+            sb.append(" ");
+        } else {
+            lines.add(sb.toAttributedString());
+            sb.setLength(0);
+        }
+        sb.append(meinfo);
+
+        StringBuilder sbc = new StringBuilder();
+        sbc.append("GC: ");
+        boolean first = true;
+        for (GarbageCollectorMXBean gc : 
ManagementFactory.getGarbageCollectorMXBeans()) {
+            if (first) {
+                first = false;
+            } else {
+                sbc.append(", ");
+            }
+            long count = gc.getCollectionCount();
+            long time = gc.getCollectionTime();
+            sbc.append(gc.getName()).append(": ")
+                .append(Long.toString(count)).append(" col. / ")
+                .append(String.format("%d", time / 1000))
+                .append(".")
+                .append(String.format("%03d", time % 1000))
+                .append(" s");
+        }
+        sbc.append(".");
+        if (sb.length() + 1 + sbc.length() < size.getColumns()) {
+            sb.append(" ");
+        } else {
+            lines.add(sb.toAttributedString());
+            sb.setLength(0);
+        }
+        sb.append(sbc);
+        lines.add(sb.toAttributedString());
+        sb.setLength(0);
+
+        lines.add(sb.toAttributedString());
+
+        // Threads
+        List<Map<String, Comparable>> threads = infos();
+        Collections.sort(threads, comparator);
+        int nb = Math.min(size.getRows() - lines.size() - 2, nthreads > 0 ? 
nthreads : threads.size());
+        // Compute values
+        List<Map<String, String>> values = threads.subList(0, nb).stream()
+                .map(thread -> stats.stream()
+                        .collect(Collectors.toMap(
+                                Function.identity(),
+                                key -> 
columns.get(key).format.apply(thread.get(key)))))
+                .collect(Collectors.toList());
+        for (String key : stats) {
+            int width = values.stream().mapToInt(map -> 
map.get(key).length()).max().orElse(0);
+            widths.put(key, Math.max(columns.get(key).header.length(), 
Math.max(width, widths.getOrDefault(key, 0))));
+        }
+        List<String> cstats;
+        if (widths.values().stream().mapToInt(Integer::intValue).sum() + 
stats.size() - 1 < size.getColumns()) {
+            cstats = stats;
+        } else {
+            cstats = new ArrayList<>();
+            int sz = 0;
+            for (String stat : stats) {
+                int nsz = sz;
+                if (nsz > 0) {
+                    nsz++;
+                }
+                nsz += widths.get(stat);
+                if (nsz < size.getColumns()) {
+                    sz = nsz;
+                    cstats.add(stat);
+                } else {
+                    break;
+                }
+            }
+        }
+        // Headers
+        for (String key : cstats) {
+            if (sb.length() > 0) {
+                sb.append(" ");
+            }
+            Column col = columns.get(key);
+            align(sb, col.header, widths.get(key), col.align);
+        }
+        lines.add(sb.toAttributedString());
+        sb.setLength(0);
+        // Threads
+        for (int i = 0; i < nb; i++) {
+            Map<String, Comparable> thread = threads.get(i);
+            long tid = (Long) thread.get(STAT_TID);
+            for (String key : cstats) {
+                if (sb.length() > 0) {
+                    sb.append(" ");
+                }
+                long last;
+                Object cur = thread.get(key);
+                Object prv = previous.computeIfAbsent(tid, id -> new 
HashMap<>()).put(key, cur);
+                if (prv != null && !prv.equals(cur)) {
+                    changes.computeIfAbsent(tid, id -> new 
HashMap<>()).put(key, now);
+                    last = now;
+                } else {
+                    last = changes.computeIfAbsent(tid, id -> new 
HashMap<>()).getOrDefault(key, 0L);
+                }
+                long fade = delay * 24;
+                if (now - last < fade) {
+                    int r = (int) ((now - last) / (fade / 24));
+                    sb.style(sb.style().foreground(255 - r).background(9));
+                }
+                align(sb, values.get(i).get(key), widths.get(key), 
columns.get(key).align);
+                sb.style(sb.style().backgroundOff().foregroundOff());
+            }
+            lines.add(sb.toAttributedString());
+            sb.setLength(0);
+        }
+
+        display.update(lines, 0);
+        terminal.flush();
+    }
+
+    private Comparator<Map<String, Comparable>> buildComparator(List<String> 
sort) {
+        if (sort == null || sort.isEmpty()) {
+            sort = Collections.singletonList(STAT_TID);
+        }
+        Comparator<Map<String, Comparable>> comparator = null;
+        for (String key : sort) {
+            boolean asc = true;
+            if (key.startsWith("+")) {
+                key = key.substring(1);
+            } else if (key.startsWith("-")) {
+                key = key.substring(1);
+                asc = false;
+            }
+            if (!columns.containsKey(key)) {
+                throw new IllegalArgumentException("Unsupported sort key: " + 
key);
+            }
+            String fkey = key;
+            Comparator<Map<String, Comparable>> comp = Comparator.comparing(
+                    (Function<Map<String, Comparable>, Comparable>) m -> 
m.get(fkey),
+                    asc ? Comparator.reverseOrder() : 
Comparator.naturalOrder());
+            if (comparator != null) {
+                comparator = comparator.thenComparing(comp);
+            } else {
+                comparator = comp;
+            }
+        }
+        return comparator;
+    }
+
+    private void register(String name, Align align, String header, 
Function<Object, String> format) {
+        columns.put(name, new Column(name, align, header, format));
+    }
+
+    private static String nanos(long nanos) {
+        return millis(nanos / 1_000_000L);
+    }
+
+    private static String millis(long millis) {
+        long secs = millis / 1_000;
+        millis = millis % 1000;
+        long mins = secs / 60;
+        secs = secs % 60;
+        long hours = mins / 60;
+        mins = mins % 60;
+        if (hours > 0) {
+            return String.format("%d:%02d:%02d.%03d", hours, mins, secs, 
millis);
+        } else if (mins > 0) {
+            return String.format("%d:%02d.%03d", mins, secs, millis);
+        } else {
+            return String.format("%d.%03d", secs, millis);
+        }
+    }
+    private static Function<Object, String> padcut(int nb) {
+        return o -> padcut(o.toString(), nb);
+    }
+    private static String padcut(String str, int nb) {
+        if (str.length() <= nb) {
+            StringBuilder sb = new StringBuilder(nb);
+            sb.append(str);
+            while (sb.length() < nb) {
+                sb.append(' ');
+            }
+            return sb.toString();
+        } else {
+            StringBuilder sb = new StringBuilder(nb);
+            sb.append(str, 0, nb - 3);
+            sb.append("...");
+            return sb.toString();
+        }
+    }
+    private static String memory(long cur, long max) {
+        if (max > 0) {
+            String smax = humanReadableByteCount(max, false);
+            String cmax = humanReadableByteCount(cur, false);
+            StringBuilder sb = new StringBuilder(smax.length() * 2 + 3);
+            for (int i = cmax.length(); i < smax.length(); i++) {
+                sb.append(' ');
+            }
+            sb.append(cmax).append(" / ").append(smax);
+            return sb.toString();
+        } else {
+            return humanReadableByteCount(cur, false);
+        }
+    }
+
+    private static String humanReadableByteCount(long bytes, boolean si) {
+        int unit = si ? 1000 : 1024;
+        if (bytes < 1024) return bytes + " B";
+        int exp = (int) (Math.log(bytes) / Math.log(1024));
+        String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : 
"i");
+        return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
+    }
+
+    /**
+     * This is for long running commands to be interrupted by ctrl-c
+     */
+    private void checkInterrupted() throws InterruptedException {
+        Thread.yield();
+        if (Thread.currentThread().isInterrupted()) {
+            throw new InterruptedException();
+        }
+    }
+
+    private void bindKeys(KeyMap<Operation> map) {
+        map.bind(Operation.HELP, "h", "?");
+        map.bind(Operation.EXIT, "q", ":q", "Q", ":Q", "ZZ");
+        map.bind(Operation.INCREASE_DELAY, "+");
+        map.bind(Operation.DECREASE_DELAY, "-");
+        map.bind(Operation.CLEAR, KeyMap.ctrl('L'));
+        map.bind(Operation.REVERSE, "r");
+    }
+
+    private static class Column {
+        final String name;
+        final Align align;
+        final String header;
+        final Function<Object, String> format;
+
+        Column(String name, Align align, String header, Function<Object, 
String> format) {
+            this.name = name;
+            this.align = align;
+            this.header = header;
+            this.format = format;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/aebb6fd0/shell/commands/src/main/resources/META-INF/services/org/apache/karaf/shell/commands
----------------------------------------------------------------------
diff --git 
a/shell/commands/src/main/resources/META-INF/services/org/apache/karaf/shell/commands
 
b/shell/commands/src/main/resources/META-INF/services/org/apache/karaf/shell/commands
index 4e18e04..686ce64 100644
--- 
a/shell/commands/src/main/resources/META-INF/services/org/apache/karaf/shell/commands
+++ 
b/shell/commands/src/main/resources/META-INF/services/org/apache/karaf/shell/commands
@@ -41,5 +41,6 @@ org.apache.karaf.shell.commands.impl.SourceAction
 org.apache.karaf.shell.commands.impl.TacAction
 org.apache.karaf.shell.commands.impl.TailAction
 org.apache.karaf.shell.commands.impl.ThreadsAction
+org.apache.karaf.shell.commands.impl.TTopAction
 org.apache.karaf.shell.commands.impl.WatchAction
 org.apache.karaf.shell.commands.impl.WcAction

Reply via email to