Updated Branches: refs/heads/master b9a1ff7cc -> d75f6f0d5
[KARAF-2626] Add load-test and improve threads commands Project: http://git-wip-us.apache.org/repos/asf/karaf/repo Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/b2cbfb5c Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/b2cbfb5c Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/b2cbfb5c Branch: refs/heads/master Commit: b2cbfb5cd71838786245728b70083193bb4e0e65 Parents: b9a1ff7 Author: Guillaume Nodet <gno...@gmail.com> Authored: Tue Dec 17 09:24:15 2013 +0100 Committer: Guillaume Nodet <gno...@gmail.com> Committed: Tue Dec 17 17:49:00 2013 +0100 ---------------------------------------------------------------------- .../apache/karaf/bundle/command/LoadTest.java | 173 +++++++++++ .../OSGI-INF/blueprint/shell-bundles.xml | 4 + .../shell/commands/impl/ThreadsAction.java | 291 +++++++++++++++++-- .../karaf/shell/commands/impl/ThreadsTest.java | 42 ++- 4 files changed, 479 insertions(+), 31 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/karaf/blob/b2cbfb5c/bundle/command/src/main/java/org/apache/karaf/bundle/command/LoadTest.java ---------------------------------------------------------------------- diff --git a/bundle/command/src/main/java/org/apache/karaf/bundle/command/LoadTest.java b/bundle/command/src/main/java/org/apache/karaf/bundle/command/LoadTest.java new file mode 100644 index 0000000..fe36d52 --- /dev/null +++ b/bundle/command/src/main/java/org/apache/karaf/bundle/command/LoadTest.java @@ -0,0 +1,173 @@ +/* + * 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.bundle.command; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import jline.console.ConsoleReader; +import org.apache.felix.service.command.CommandSession; +import org.apache.karaf.shell.commands.Command; +import org.apache.karaf.shell.commands.Option; +import org.apache.karaf.shell.console.OsgiCommandSupport; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.wiring.FrameworkWiring; + +@Command(scope = "bundle", name = "load-test", description = "Load test bundle lifecycle") +public class LoadTest extends OsgiCommandSupport { + + @Option(name = "--threads", description = "number of concurrent threads") + int threads = 2; + + @Option(name = "--delay", description = "maximum delay between actions") + int delay = 1; + + @Option(name = "--iterations", description = "number of iterations per thread") + int iterations = 100; + + @Option(name = "--refresh", description = "percentage of bundle refresh vs restart") + int refresh = 20; + + @Option(name = "--excludes", description = "List of bundles (ids or symbolic names) to exclude") + List<String> excludes = Arrays.asList("0", "org.ops4j.pax.url.mvn", "org.ops4j.pax.logging.pax-logging-api", "org.ops4j.pax.logging.pax-logging-service"); + + @Override + protected Object doExecute() throws Exception { + if (!confirm(session)) { + return null; + } + final BundleContext bundleContext = getBundleContext().getBundle(0).getBundleContext(); + final FrameworkWiring wiring = bundleContext.getBundle().adapt(FrameworkWiring.class); + final CountDownLatch latch = new CountDownLatch(threads); + final Bundle[] bundles = bundleContext.getBundles(); + final AtomicBoolean[] locks = new AtomicBoolean[bundles.length]; + for (int b = 0; b < locks.length; b++) { + locks[b] = new AtomicBoolean(true); + // Avoid touching excluded bundles + if (excludes.contains(Long.toString(bundles[b].getBundleId())) + || excludes.contains(bundles[b].getSymbolicName())) { + continue; + } + // Only touch active bundles + if (bundles[b].getState() != Bundle.ACTIVE) { + continue; + } + // Now set the lock to available + locks[b].set(false); + } + for (int i = 0; i < threads; i++) { + new Thread() { + public void run() { + try { + Random rand = new Random(); + for (int j = 0; j < iterations; j++) { + for (;;) { + int b = rand.nextInt(bundles.length); + if (locks[b].compareAndSet(false, true)) { + try { + // Only touch active bundles + if (bundles[b].getState() != Bundle.ACTIVE) { + continue; + } + if (rand.nextInt(100) < refresh) { + try { + bundles[b].update(); + final CountDownLatch latch = new CountDownLatch(1); + wiring.refreshBundles(Collections.singletonList(bundles[b]), new FrameworkListener() { + public void frameworkEvent(FrameworkEvent event) { + latch.countDown(); + } + }); + latch.await(); + } finally { + while (true) { + try { + bundles[b].start(Bundle.START_TRANSIENT); + break; + } catch (Exception e) { + Thread.sleep(1); + } + } + } + } else { + try { + bundles[b].stop(Bundle.STOP_TRANSIENT); + } finally { + while (true) { + try { + bundles[b].start(Bundle.START_TRANSIENT); + break; + } catch (Exception e) { + Thread.sleep(1); + } + } + } + } + Thread.sleep(rand.nextInt(delay)); + } catch (Exception e) { + e.printStackTrace(); + } finally { + locks[b].set(false); + } + } + break; + } + } + } catch (Throwable t) { + t.printStackTrace(); + } finally { + latch.countDown(); + } + } + }.start(); + } + new Thread() { + @Override + public void run() { + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.err.println("Load test finished"); + } + }.start(); + return null; + } + + private boolean confirm(CommandSession session) throws IOException { + for (;;) { + ConsoleReader reader = (ConsoleReader) session.get(".jline.reader"); + String msg = "You are about to perform a start/stop/refresh load test on bundles.\nDo you wish to continue (yes/no): "; + String str = reader.readLine(msg); + if ("yes".equalsIgnoreCase(str)) { + return true; + } + if ("no".equalsIgnoreCase(str)) { + return false; + } + } + } +} http://git-wip-us.apache.org/repos/asf/karaf/blob/b2cbfb5c/bundle/command/src/main/resources/OSGI-INF/blueprint/shell-bundles.xml ---------------------------------------------------------------------- diff --git a/bundle/command/src/main/resources/OSGI-INF/blueprint/shell-bundles.xml b/bundle/command/src/main/resources/OSGI-INF/blueprint/shell-bundles.xml index 91673f9..bed5c0f 100644 --- a/bundle/command/src/main/resources/OSGI-INF/blueprint/shell-bundles.xml +++ b/bundle/command/src/main/resources/OSGI-INF/blueprint/shell-bundles.xml @@ -128,6 +128,10 @@ <property name="bundleWatcher" ref="bundleWatcher"/> </action> </command> + <command> + <action class="org.apache.karaf.bundle.command.LoadTest" > + </action> + </command> </command-bundle> </blueprint> http://git-wip-us.apache.org/repos/asf/karaf/blob/b2cbfb5c/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/ThreadsAction.java ---------------------------------------------------------------------- diff --git a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/ThreadsAction.java b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/ThreadsAction.java index 3f6465b..52cb76e 100644 --- a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/ThreadsAction.java +++ b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/ThreadsAction.java @@ -16,9 +16,16 @@ */ package org.apache.karaf.shell.commands.impl; +import java.lang.management.LockInfo; import java.lang.management.ManagementFactory; +import java.lang.management.MonitorInfo; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; import org.apache.karaf.shell.commands.Argument; import org.apache.karaf.shell.commands.Command; @@ -33,47 +40,283 @@ import org.apache.karaf.shell.table.ShellTable; @Command(scope = "shell", name = "threads", description = "Prints the current threads (optionally with stacktraces)") public class ThreadsAction extends AbstractAction { + @Option(name = "--tree" , description = "Display threads as a tree") + boolean tree = false; + + @Option(name = "--list" , description = "Display threads as a list") + boolean list = false; + + @Option(name = "-e", aliases = { "--empty-groups" }, description = "Show empty groups") + boolean empty = false; + + @Option(name = "-t", aliases = { "--threshold" }, description = "Minimal number of interesting stack trace line to display a thread") + int threshold = 1; + + @Option(name = "--locks", description = "Display locks") + boolean locks = false; + + @Option(name = "--monitors", description = "Display monitors") + boolean monitors = false; + + @Option(name = "--packages", description = "Pruned packages") + List<String> packages = Arrays.asList("java.", "sun."); + @Argument(name = "id", description="Show details for thread with this Id", required = false, multiValued = false) Long id; @Option(name = "--no-format", description = "Disable table rendered output", required = false, multiValued = false) boolean noFormat; + @Override protected Object doExecute() throws Exception { + Map<Long, ThreadInfo> threadInfos = new TreeMap<Long, ThreadInfo>(); ThreadMXBean threadsBean = ManagementFactory.getThreadMXBean(); - + ThreadInfo[] infos; + if (threadsBean.isObjectMonitorUsageSupported() && threadsBean.isSynchronizerUsageSupported()) { + infos = threadsBean.dumpAllThreads(true, true); + } else { + infos = threadsBean.getThreadInfo(threadsBean.getAllThreadIds(), Integer.MAX_VALUE); + } + for (ThreadInfo info : infos) { + threadInfos.put(info.getThreadId(), info); + } + if (id != null) { - printThread(threadsBean, id); + ThreadInfo ti = threadInfos.get(id); + if (ti != null) { + System.out.println("Thread " + ti.getThreadId() + " " + ti.getThreadName() + " " + ti.getThreadState()); + System.out.println("Stacktrace:"); + StackTraceElement[] st = ti.getStackTrace(); + for (StackTraceElement ste : st) { + System.out.println(ste.getClassName() + "." + ste.getMethodName() + " line: " + ste.getLineNumber()); + } + } + } else if (list) { + ShellTable table = new ShellTable(); + table.column("Id"); + table.column("Name"); + table.column("State"); + table.column("CPU time"); + table.column("Usr time"); + for (ThreadInfo thread : threadInfos.values()) { + long id = thread.getThreadId(); + table.addRow().addContent( + id, + thread.getThreadName(), + thread.getThreadState(), + threadsBean.getThreadCpuTime(id) / 1000000, + threadsBean.getThreadUserTime(id) / 1000000); + } + table.print(System.out, !noFormat); } else { - printThreadList(threadsBean); + ThreadGroup group = Thread.currentThread().getThreadGroup(); + while (group.getParent() != null) { + group = group.getParent(); + } + ThreadGroupData data = new ThreadGroupData(group, threadInfos); + data.print(); } + return null; } - void printThread(ThreadMXBean threadsBean, Long id) { - threadsBean.setThreadCpuTimeEnabled(true); - ThreadInfo ti = threadsBean.getThreadInfo(id, Integer.MAX_VALUE); - System.out.println("Thread " + ti.getThreadId() + " " + ti.getThreadName() + " " + ti.getThreadState()); - System.out.println("Stacktrace:"); - StackTraceElement[] st = ti.getStackTrace(); - for (StackTraceElement ste : st) { - System.out.println(ste.getClassName() + "." + ste.getMethodName() + " line: " + ste.getLineNumber()); + public class ThreadGroupData { + private final ThreadGroup group; + private final List<ThreadGroupData> groups; + private final List<ThreadData> threads; + + public ThreadGroupData(ThreadGroup group, Map<Long, ThreadInfo> infos) { + this.group = group; + int nbGroups; + int nbThreads; + ThreadGroup[] childGroups = new ThreadGroup[32]; + while (true) { + nbGroups = group.enumerate(childGroups, false); + if (nbGroups == childGroups.length) { + childGroups = new ThreadGroup[childGroups.length * 2]; + } else { + break; + } + } + groups = new ArrayList<ThreadGroupData>(); + for (ThreadGroup tg : childGroups) { + if (tg != null) { + groups.add(new ThreadGroupData(tg, infos)); + } + } + Thread[] childThreads = new Thread[32]; + while (true) { + nbThreads = group.enumerate(childThreads, false); + if (nbThreads == childThreads.length) { + childThreads = new Thread[childThreads.length * 2]; + } else { + break; + } + } + threads = new ArrayList<ThreadData>(); + for (Thread t : childThreads) { + if (t != null) { + threads.add(new ThreadData(t, infos.get(t.getId()))); + } + } + } + + public void print() { + if (tree) { + printTree(""); + } else { + printDump(""); + } + } + + private void printTree(String indent) { + if (empty || hasInterestingThreads()) { + System.out.println(indent + "Thread Group \"" + group.getName() + "\""); + for (ThreadGroupData tgd : groups) { + tgd.printTree(indent + " "); + } + for (ThreadData td : threads) { + if (td.isInteresting()) { + td.printTree(indent + " "); + } + } + } + } + + private void printDump(String indent) { + if (empty || hasInterestingThreads()) { + for (ThreadGroupData tgd : groups) { + tgd.printDump(indent); + } + for (ThreadData td : threads) { + if (td.isInteresting()) { + td.printDump(indent); + } + } + } + } + + public boolean hasInterestingThreads() { + for (ThreadData td : threads) { + if (td.isInteresting()) { + return true; + } + } + for (ThreadGroupData tgd : groups) { + if (tgd.hasInterestingThreads()) { + return true; + } + } + return false; } } - void printThreadList(ThreadMXBean threadsBean) { - ThreadInfo[] threadInfoAr = threadsBean.dumpAllThreads(false, false); - ShellTable table = new ShellTable(); - table.column("Id"); - table.column("Name"); - table.column("State"); - table.column("CPU time"); - table.column("User time"); - for (ThreadInfo ti : threadInfoAr) { - long id = ti.getThreadId(); - table.addRow().addContent(id, ti.getThreadName(), ti.getThreadState(), threadsBean.getThreadCpuTime(id) / 1000000, threadsBean.getThreadUserTime(id) / 1000000); - } - table.print(System.out, !noFormat); + public class ThreadData { + private final Thread thread; + private ThreadInfo info; + + public ThreadData(Thread thread, ThreadInfo info) { + this.thread = thread; + this.info = info; + } + + public void printTree(String indent) { + System.out.println(indent + " " + "\"" + thread.getName() + "\": " + thread.getState()); + } + + public void printDump(String indent) { + if (info != null && isInteresting()) { + printThreadInfo(" "); + if (locks) { + printLockInfo(" "); + } + if (monitors) { + printMonitorInfo(" "); + } + } + } + + public boolean isInteresting() { + int nb = 0; + StackTraceElement[] stacktrace = info.getStackTrace(); + for (int i = 0; i < stacktrace.length; i++) { + StackTraceElement ste = stacktrace[i]; + boolean interestingLine = true; + for (String pkg : packages) { + if (ste.getClassName().startsWith(pkg)) { + interestingLine = false; + break; + } + } + if (interestingLine) { + nb++; + } + } + return nb >= threshold; + } + + private void printThreadInfo(String indent) { + // print thread information + printThread(indent); + + // print stack trace with locks + StackTraceElement[] stacktrace = info.getStackTrace(); + MonitorInfo[] monitors = info.getLockedMonitors(); + for (int i = 0; i < stacktrace.length; i++) { + StackTraceElement ste = stacktrace[i]; + System.out.println(indent + "at " + ste.toString()); + for (MonitorInfo mi : monitors) { + if (mi.getLockedStackDepth() == i) { + System.out.println(indent + " - locked " + mi); + } + } + } + System.out.println(); + } + + private void printThread(String indent) { + StringBuilder sb = new StringBuilder("\"" + info.getThreadName() + "\"" + " Id=" + + info.getThreadId() + " in " + info.getThreadState()); + if (info.getLockName() != null) { + sb.append(" on lock=" + info.getLockName()); + } + if (info.isSuspended()) { + sb.append(" (suspended)"); + } + if (info.isInNative()) { + sb.append(" (running in native)"); + } + System.out.println(sb.toString()); + if (info.getLockOwnerName() != null) { + System.out.println(indent + " owned by " + info.getLockOwnerName() + " Id=" + + info.getLockOwnerId()); + } + } + + private void printMonitorInfo(String indent) { + MonitorInfo[] monitors = info.getLockedMonitors(); + if (monitors != null && monitors.length > 0) { + System.out.println(indent + "Locked monitors: count = " + monitors.length); + for (MonitorInfo mi : monitors) { + System.out.println(indent + " - " + mi + " locked at "); + System.out.println(indent + " " + mi.getLockedStackDepth() + " " + + mi.getLockedStackFrame()); + } + System.out.println(); + } + } + + private void printLockInfo(String indent) { + LockInfo[] locks = info.getLockedSynchronizers(); + if (locks != null && locks.length > 0) { + System.out.println(indent + "Locked synchronizers: count = " + locks.length); + for (LockInfo li : locks) { + System.out.println(indent + " - " + li); + } + System.out.println(); + } + } + } } http://git-wip-us.apache.org/repos/asf/karaf/blob/b2cbfb5c/shell/commands/src/test/java/org/apache/karaf/shell/commands/impl/ThreadsTest.java ---------------------------------------------------------------------- diff --git a/shell/commands/src/test/java/org/apache/karaf/shell/commands/impl/ThreadsTest.java b/shell/commands/src/test/java/org/apache/karaf/shell/commands/impl/ThreadsTest.java index 077f93b..5daf61c 100644 --- a/shell/commands/src/test/java/org/apache/karaf/shell/commands/impl/ThreadsTest.java +++ b/shell/commands/src/test/java/org/apache/karaf/shell/commands/impl/ThreadsTest.java @@ -1,8 +1,21 @@ +/* + * 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 java.lang.management.ManagementFactory; -import java.lang.management.ThreadMXBean; - import org.junit.Test; /** @@ -11,13 +24,28 @@ import org.junit.Test; public class ThreadsTest { @Test public void testThreadlist() throws Exception { - ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); - new ThreadsAction().printThreadList(threadMXBean); + ThreadsAction action = new ThreadsAction(); + action.list = true; + action.doExecute(); } @Test public void testThreadInfo() throws Exception { - ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); - new ThreadsAction().printThread(threadMXBean, 1L); + ThreadsAction action = new ThreadsAction(); + action.id = 1L; + action.doExecute(); + } + + @Test + public void testThreadTree() throws Exception { + ThreadsAction action = new ThreadsAction(); + action.tree = true; + action.doExecute(); + } + + @Test + public void testThreadDump() throws Exception { + ThreadsAction action = new ThreadsAction(); + action.doExecute(); } }