changeset 1718:d340a415cca4

Visualization for thread deadlock PR2567 Reviewed-by: jerboaa, jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2015-June/014249.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2015-July/014969.html
author Omair Majid <omajid@redhat.com>
date Tue, 30 Jun 2015 14:24:29 -0400
parents f6eb70a4744f
children bbcd4938ea0d
files pom.xml thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/DeadlockParser.java thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/locale/LocaleResources.java thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/view/VmDeadLockView.java thread/client-common/src/main/resources/com/redhat/thermostat/thread/client/common/locale/strings.properties thread/client-common/src/test/java/com/redhat/thermostat/thread/client/common/DeadlockParserTest.java thread/client-common/src/test/resources/com/redhat/thermostat/thread/client/common/deadlock.output thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/VmDeadLockController.java thread/client-controllers/src/test/java/com/redhat/thermostat/thread/client/controller/impl/VmDeadLockControllerTest.java thread/client-swing/pom.xml thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingVmDeadLockView.java
diffstat 11 files changed, 644 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/pom.xml	Tue Jul 28 13:48:30 2015 -0400
+++ b/pom.xml	Tue Jun 30 14:24:29 2015 -0400
@@ -156,6 +156,7 @@
          See the main thermostat bash script where this property is
          used. -->
     <jcommon.version>1.0.17</jcommon.version>
+    <jgraphx.version>2.3.0.5</jgraphx.version>
     <mongo-driver.version>2.11.4</mongo-driver.version>
     <!-- the OSGi Bundle-Version; should match the manifest in the jar -->
     <mongo-driver.osgi-version>2.11.4.RELEASE</mongo-driver.osgi-version>
@@ -496,6 +497,11 @@
         <version>${jfreechart.version}</version>
       </dependency>
       <dependency>
+        <groupId>org.tinyjee.jgraphx</groupId>
+        <artifactId>jgraphx</artifactId>
+        <version>${jgraphx.version}</version>
+      </dependency>
+      <dependency>
         <groupId>org.mongodb</groupId>
         <artifactId>mongo-java-driver</artifactId>
         <version>${mongo-driver.version}</version>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/DeadlockParser.java	Tue Jun 30 14:24:29 2015 -0400
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2012-2015 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.thread.client.common;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DeadlockParser {
+
+    public static class ParseException extends Exception {
+        public ParseException(String message) {
+            super(message);
+        }
+
+        public ParseException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
+    public static class Information {
+        public final List<Thread> threads;
+
+        public Information(List<Thread> threads) {
+            this.threads = threads;
+        }
+    }
+
+    public static class Thread {
+
+        public enum State {
+            WAITING;
+        }
+
+        public final String name;
+        public final String id;
+        public final State state;
+        public final List<String> stackTrace;
+        public final List<Lock> ownedLocks;
+        public final Lock waitingOn;
+
+        public Thread(String id, String name, State state,
+                List<String> stackTrace,
+                List<Lock> ownedLocks, Lock waitingOn) {
+            this.id = id;
+            this.name = name;
+            this.state = state;
+            this.stackTrace = stackTrace;
+            this.ownedLocks = ownedLocks;
+            this.waitingOn = waitingOn;
+        }
+    }
+
+    public static class Lock {
+        public final String name;
+        public final String ownerId;
+
+        public Lock(String name, String ownerId) {
+            this.name = name;
+            this.ownerId = ownerId;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
+            }
+            if (!this.getClass().equals(obj.getClass())) {
+                return false;
+            }
+            Lock other = (Lock) obj;
+            return Objects.equals(other.name, this.name) &&
+                    Objects.equals(other.ownerId, this.ownerId);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(this.name, this.ownerId);
+        }
+    }
+
+    public Information parse(File file) throws IOException, ParseException {
+        try (Reader reader = new FileReader(file)) {
+            return parse(reader);
+        }
+    }
+
+    public Information parse(Reader reader) throws IOException, ParseException {
+        try (BufferedReader buffered = new BufferedReader(reader)) {
+            return parse(buffered);
+        }
+    }
+
+    public Information parse(BufferedReader reader) throws IOException, ParseException {
+        List<Thread> threads = new ArrayList<>();
+
+        while (true) {
+            Thread threadInfo = parseThread(reader);
+            if (threadInfo == null) {
+                break;
+            }
+
+            threads.add(threadInfo);
+        }
+
+        return new Information(threads);
+    }
+
+    private Thread parseThread(BufferedReader reader) throws IOException, ParseException {
+        String line = reader.readLine();
+        if (line == null) {
+            return null;
+        }
+        String regex = "\"(.*)\" Id=(\\d+) (WAITING) on (.*) owned by \"(.*)\" Id=(\\d+)";
+        Pattern pattern = Pattern.compile(regex);
+        Matcher matcher = pattern.matcher(line);
+        if (!matcher.matches()) {
+            throw new ParseException("Failed to parse: '" + line + "'");
+        }
+        String name = matcher.group(1);
+        String id = matcher.group(2);
+        Thread.State state = Thread.State.valueOf(matcher.group(3));
+        Lock waitingOn = new Lock(matcher.group(4), matcher.group(6));
+
+        List<String> stackTrace = new ArrayList<>();
+
+        String PREFIX = "\t";
+
+        // read stack trace
+        while (true) {
+            line = reader.readLine();
+
+            if (line.equals("")) {
+                break;
+            }
+
+            if (line.startsWith(PREFIX + "at ")) {
+                stackTrace.add(line.substring((PREFIX + "at ").length()));
+            } else if (line.startsWith(PREFIX + "-  waiting on")) {
+                // ignore this
+            } else if (line.startsWith(PREFIX + "...")) {
+                // nothing to do
+            } else {
+                throw new ParseException("Unrecognized input: '" + line + "'");
+            }
+        }
+
+        final String TEXT_BEFORE = "Number of locked synchronizers = ";
+        List<Lock> ownedLocks = new ArrayList<>();
+        int acquiredLocks = 0;
+        // read locks
+        while (true) {
+            line = reader.readLine();
+            if (line == null || line.equals("")) {
+                break;
+            }
+
+            if (line.startsWith(PREFIX + TEXT_BEFORE)) {
+                acquiredLocks = Integer.valueOf(line.substring((PREFIX + TEXT_BEFORE).length()));
+            } else if (line.startsWith(PREFIX + "- ")){
+                ownedLocks.add(new Lock(line.substring((PREFIX + "- ").length()), id));
+            }
+        }
+
+        if (acquiredLocks != ownedLocks.size()) {
+            throw new AssertionError("Incorrectly parsed owned locks");
+        }
+
+        /* discard = */ reader.readLine();
+
+        return new Thread(id, name, state, stackTrace, ownedLocks, waitingOn);
+    }
+
+}
--- a/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/locale/LocaleResources.java	Tue Jul 28 13:48:30 2015 -0400
+++ b/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/locale/LocaleResources.java	Tue Jun 30 14:24:29 2015 -0400
@@ -72,7 +72,12 @@
     THREAD_DETAILS_EMTPY,
 
     CHECK_FOR_DEADLOCKS,
-
+    DEADLOCK_GRAPHICAL_TAB_TITLE,
+    DEADLOCK_RAW_TAB_TITLE,
+    DEADLOCK_WAITING_ON,
+    DEADLOCK_THREAD_NAME,
+    DEADLOCK_THREAD_TOOLTIP,
+    DEADLOCK_EDGE_TOOLTIP,
 
     RESTORE_ZOOM,
     ZOOM_IN,
--- a/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/view/VmDeadLockView.java	Tue Jul 28 13:48:30 2015 -0400
+++ b/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/view/VmDeadLockView.java	Tue Jun 30 14:24:29 2015 -0400
@@ -39,6 +39,7 @@
 import com.redhat.thermostat.client.core.views.BasicView;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.ActionNotifier;
+import com.redhat.thermostat.thread.client.common.DeadlockParser.Information;
 
 public abstract class VmDeadLockView extends BasicView {
 
@@ -61,6 +62,7 @@
         deadLockNotifier.removeActionListener(listener);
     }
 
-    public abstract void setDeadLockInformation(String info);
+    public abstract void setDeadLockInformation(Information parsed, String rawText);
+
 }
 
--- a/thread/client-common/src/main/resources/com/redhat/thermostat/thread/client/common/locale/strings.properties	Tue Jul 28 13:48:30 2015 -0400
+++ b/thread/client-common/src/main/resources/com/redhat/thermostat/thread/client/common/locale/strings.properties	Tue Jun 30 14:24:29 2015 -0400
@@ -31,6 +31,12 @@
 THREAD_DETAILS_EMTPY = Please double-click on a thread in the thread table
 
 CHECK_FOR_DEADLOCKS = Check
+DEADLOCK_GRAPHICAL_TAB_TITLE = Visualization
+DEADLOCK_RAW_TAB_TITLE = Raw Data
+DEADLOCK_WAITING_ON = Waiting on
+DEADLOCK_THREAD_NAME = Thread {0} ({1})
+DEADLOCK_THREAD_TOOLTIP = <html> Waiting on lock {0} at <br/>{1}</html>
+DEADLOCK_EDGE_TOOLTIP = {0} is waiting on {1}
 
 RESTORE_ZOOM = Restore Content Size
 ZOOM_IN = Magnify Content
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-common/src/test/java/com/redhat/thermostat/thread/client/common/DeadlockParserTest.java	Tue Jun 30 14:24:29 2015 -0400
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2012-2015 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.thread.client.common;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.CharArrayReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.thread.client.common.DeadlockParser;
+import com.redhat.thermostat.thread.client.common.DeadlockParser.Information;
+import com.redhat.thermostat.thread.client.common.DeadlockParser.Lock;
+import com.redhat.thermostat.thread.client.common.DeadlockParser.Thread;
+import com.redhat.thermostat.thread.client.common.DeadlockParser.Thread.State;
+
+public class DeadlockParserTest {
+
+    @Test
+    public void testParsingEmptyStream() throws Exception {
+        try (BufferedReader reader = new BufferedReader(new CharArrayReader(new char[0]))) {
+            Information result = new DeadlockParser().parse(reader);
+            assertNotNull(result);
+            assertNotNull(result.threads);
+            assertTrue(result.threads.isEmpty());
+        }
+    }
+
+    @Test
+    public void testParsing() throws Exception {
+        try (InputStream stream = this.getClass().getResourceAsStream("deadlock.output");
+             InputStreamReader streamReader = new InputStreamReader(stream, StandardCharsets.UTF_8);
+             BufferedReader reader = new BufferedReader(streamReader);
+        ) {
+            Information result = new DeadlockParser().parse(reader);
+            assertEquals(3, result.threads.size());
+        }
+    }
+
+    @Test
+    public void testParsingSingleThreadInfo() throws Exception {
+        String trace = ""
+                + "\"Alice\" Id=8 WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync@602fe64a owned by \"Bob\" Id=9\n"
+                + "\tat sun.misc.Unsafe.park(Native Method)\n"
+                + "\t-  waiting on java.util.concurrent.locks.ReentrantLock$NonfairSync@602fe64a\n"
+                + "\tat java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)\n"
+                + "\tat com.redhat.thermostat.foo.Bar.run(Bar.java:1337)\n"
+                + "\t...\n"
+                + "\n"
+                + "\tNumber of locked synchronizers = 1\n"
+                + "\t- java.util.concurrent.locks.ReentrantLock$NonfairSync@6bd8b476\n";
+        try (BufferedReader reader = new BufferedReader(new StringReader(trace))) {
+            Information parsed = new DeadlockParser().parse(reader);
+
+            assertNotNull(parsed);
+            assertEquals(1, parsed.threads.size());
+
+            Thread thread = parsed.threads.get(0);
+            assertEquals("Alice", thread.name);
+            assertEquals("8", thread.id);
+            assertEquals(State.WAITING, thread.state);
+            assertEquals(new Lock("java.util.concurrent.locks.ReentrantLock$NonfairSync@602fe64a", "9"),
+                    thread.waitingOn);
+
+            assertEquals(1, thread.ownedLocks.size());
+            assertEquals(new Lock("java.util.concurrent.locks.ReentrantLock$NonfairSync@6bd8b476", "8"),
+                    thread.ownedLocks.get(0));
+
+            List<String> stackTrace = thread.stackTrace;
+            assertNotNull(stackTrace);
+            assertEquals(3, stackTrace.size());
+            assertTrue(stackTrace.get(2).contains("Bar.java:1337"));
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-common/src/test/resources/com/redhat/thermostat/thread/client/common/deadlock.output	Tue Jun 30 14:24:29 2015 -0400
@@ -0,0 +1,47 @@
+"Mallory" Id=10 WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync@6bd8b476 owned by "Alice" Id=8
+	at sun.misc.Unsafe.park(Native Method)
+	-  waiting on java.util.concurrent.locks.ReentrantLock$NonfairSync@6bd8b476
+	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
+	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834)
+	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:867)
+	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1197)
+	at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:214)
+	at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:290)
+	at com.redhat.thermostat.tests.DeadLock$Philosopher.run(DeadLock.java:57)
+	...
+
+	Number of locked synchronizers = 1
+	- java.util.concurrent.locks.ReentrantLock$NonfairSync@347ad394
+
+
+"Alice" Id=8 WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync@602fe64a owned by "Bob" Id=9
+	at sun.misc.Unsafe.park(Native Method)
+	-  waiting on java.util.concurrent.locks.ReentrantLock$NonfairSync@602fe64a
+	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
+	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834)
+	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:867)
+	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1197)
+	at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:214)
+	at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:290)
+	at com.redhat.thermostat.tests.DeadLock$Philosopher.run(DeadLock.java:57)
+	...
+
+	Number of locked synchronizers = 1
+	- java.util.concurrent.locks.ReentrantLock$NonfairSync@6bd8b476
+
+
+"Bob" Id=9 WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync@347ad394 owned by "Mallory" Id=10
+	at sun.misc.Unsafe.park(Native Method)
+	-  waiting on java.util.concurrent.locks.ReentrantLock$NonfairSync@347ad394
+	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
+	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834)
+	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:867)
+	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1197)
+	at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:214)
+	at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:290)
+	at com.redhat.thermostat.tests.DeadLock$Philosopher.run(DeadLock.java:57)
+	...
+
+	Number of locked synchronizers = 1
+	- java.util.concurrent.locks.ReentrantLock$NonfairSync@602fe64a
+
--- a/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/VmDeadLockController.java	Tue Jul 28 13:48:30 2015 -0400
+++ b/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/VmDeadLockController.java	Tue Jun 30 14:24:29 2015 -0400
@@ -36,17 +36,21 @@
 
 package com.redhat.thermostat.thread.client.controller.impl;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
-import javax.swing.SwingUtilities;
-
 import com.redhat.thermostat.client.core.views.BasicView.Action;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.Timer;
 import com.redhat.thermostat.common.Timer.SchedulingType;
 import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.thread.client.common.DeadlockParser;
+import com.redhat.thermostat.thread.client.common.DeadlockParser.Information;
+import com.redhat.thermostat.thread.client.common.DeadlockParser.ParseException;
 import com.redhat.thermostat.thread.client.common.collector.ThreadCollector;
 import com.redhat.thermostat.thread.client.common.view.VmDeadLockView;
 import com.redhat.thermostat.thread.client.common.view.VmDeadLockView.VmDeadLockViewAction;
@@ -56,11 +60,14 @@
 
     private static final Translate<LocaleResources> translate = LocaleResources.createLocalizer();
 
+    private static final String NO_DEADLOCK = translate.localize(LocaleResources.NO_DEADLOCK_DETECTED).getContents();
+
     private VmDeadLockView view;
     private ThreadCollector collector;
     private Timer timer;
 
     private final AtomicReference<String> descriptionRef =  new AtomicReference<>("");
+    private String previousDeadlockData = null;
 
     public VmDeadLockController(VmDeadLockView view, ThreadCollector collector, Timer timer) {
         this.view = view;
@@ -75,7 +82,7 @@
                 switch (actionEvent.getActionId()) {
                 case CHECK_FOR_DEADLOCK:
                     checkForDeadLock();
-                    updateView();
+                    updateViewIfNeeded();
                     break;
                 default:
                     break;
@@ -87,7 +94,7 @@
             @Override
             public void run() {
                 checkStorageForDeadLockData();
-                updateView();
+                updateViewIfNeeded();
             }
         });
         timer.setDelay(5);
@@ -128,14 +135,29 @@
 
         String description = data.getDeadLockDescription();
         if (description.equals(VmDeadLockData.NO_DEADLOCK)) {
-            description = translate.localize(LocaleResources.NO_DEADLOCK_DETECTED).getContents();
+            description = NO_DEADLOCK;
         }
         this.descriptionRef.set(description);
 
     }
 
-    private void updateView() {
-        view.setDeadLockInformation(descriptionRef.get());
+    private void updateViewIfNeeded() {
+        String rawDeadlockData = descriptionRef.get();
+
+        if (!rawDeadlockData.equals(previousDeadlockData)) {
+            Information parsed = null;
+
+            if (!rawDeadlockData.equals(NO_DEADLOCK)) {
+                try {
+                    parsed = new DeadlockParser().parse(new BufferedReader(new StringReader(rawDeadlockData)));
+                } catch (IOException | ParseException e) {
+                    // TODO log this message
+                }
+            }
+
+            view.setDeadLockInformation(parsed, rawDeadlockData);
+            previousDeadlockData = rawDeadlockData;
+        }
     }
 
 }
--- a/thread/client-controllers/src/test/java/com/redhat/thermostat/thread/client/controller/impl/VmDeadLockControllerTest.java	Tue Jul 28 13:48:30 2015 -0400
+++ b/thread/client-controllers/src/test/java/com/redhat/thermostat/thread/client/controller/impl/VmDeadLockControllerTest.java	Tue Jun 30 14:24:29 2015 -0400
@@ -106,7 +106,7 @@
         listener.actionPerformed(new ActionEvent<VmDeadLockViewAction>(view, VmDeadLockViewAction.CHECK_FOR_DEADLOCK));
 
         verify(collector).requestDeadLockCheck();
-        verify(view).setDeadLockInformation(DESCRIPTION);
+        verify(view).setDeadLockInformation(null, DESCRIPTION);
     }
 
     @Test
@@ -126,7 +126,7 @@
         listener.actionPerformed(new ActionEvent<VmDeadLockViewAction>(view, VmDeadLockViewAction.CHECK_FOR_DEADLOCK));
 
         verify(collector).requestDeadLockCheck();
-        verify(view).setDeadLockInformation("No Deadlocks Detected.");
+        verify(view).setDeadLockInformation(null, "No Deadlocks Detected.");
     }
 
     @Test
@@ -176,7 +176,7 @@
 
         action.run();
 
-        verify(view).setDeadLockInformation("No Deadlocks Detected.");
+        verify(view).setDeadLockInformation(null, "No Deadlocks Detected.");
     }
 
     @Test
--- a/thread/client-swing/pom.xml	Tue Jul 28 13:48:30 2015 -0400
+++ b/thread/client-swing/pom.xml	Tue Jun 30 14:24:29 2015 -0400
@@ -77,6 +77,11 @@
       <artifactId>org.osgi.compendium</artifactId>
       <scope>provided</scope>
     </dependency>
+
+    <dependency>
+      <groupId>org.tinyjee.jgraphx</groupId>
+      <artifactId>jgraphx</artifactId>
+    </dependency>
     
     <dependency>
       <groupId>com.redhat.thermostat</groupId>
@@ -120,6 +125,7 @@
                 com.redhat.thermostat.thread.client.swing.experimental.components,
                 com.redhat.thermostat.thread.client.swing.experimental.utils,
             </Private-Package>
+            <Embed-Dependency>jgraphx;scope=compile|runtime</Embed-Dependency>
             <!-- Do not autogenerate uses clauses in Manifests -->
             <_nouses>true</_nouses>
           </instructions>
--- a/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingVmDeadLockView.java	Tue Jul 28 13:48:30 2015 -0400
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingVmDeadLockView.java	Tue Jun 30 14:24:29 2015 -0400
@@ -36,21 +36,38 @@
 
 package com.redhat.thermostat.thread.client.swing.impl;
 
+import java.awt.BorderLayout;
 import java.awt.Component;
+import java.awt.FontMetrics;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 
 import javax.swing.JButton;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
+import javax.swing.JTabbedPane;
 import javax.swing.JTextArea;
 import javax.swing.SwingUtilities;
 
+import com.mxgraph.layout.mxCircleLayout;
+import com.mxgraph.layout.mxEdgeLabelLayout;
+import com.mxgraph.layout.mxGraphLayout;
+import com.mxgraph.model.mxCell;
+import com.mxgraph.swing.mxGraphComponent;
+import com.mxgraph.util.mxConstants;
+import com.mxgraph.view.mxGraph;
 import com.redhat.thermostat.client.swing.SwingComponent;
 import com.redhat.thermostat.client.swing.experimental.ComponentVisibilityNotifier;
 import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.thread.client.common.DeadlockParser;
+import com.redhat.thermostat.thread.client.common.DeadlockParser.Information;
 import com.redhat.thermostat.thread.client.common.locale.LocaleResources;
 import com.redhat.thermostat.thread.client.common.view.VmDeadLockView;
 
@@ -58,7 +75,13 @@
 
     private static final Translate<LocaleResources> translate = LocaleResources.createLocalizer();
 
+    private static final int TAB_VISUALIZATION_INDEX = 0;
+    private static final int TAB_RAW_INDEX = 1;
+
     private final JPanel actualComponent = new JPanel();
+
+    private final JTabbedPane tabbedPane = new JTabbedPane();
+    private final JPanel graphical = new JPanel();
     private final JTextArea description = new JTextArea();
 
     public SwingVmDeadLockView() {
@@ -83,26 +106,203 @@
         c.weightx = 1;
         c.weighty = 1;
 
+        actualComponent.add(tabbedPane, c);
+
         JScrollPane scrollPane = new JScrollPane(description);
-        actualComponent.add(scrollPane, c);
+
+        graphical.setLayout(new BorderLayout());
+
+        final String GRAPHICAL_TAB_TITLE = translate.localize(LocaleResources.DEADLOCK_GRAPHICAL_TAB_TITLE).getContents();
+        tabbedPane.insertTab(GRAPHICAL_TAB_TITLE, null, graphical, null, TAB_VISUALIZATION_INDEX);
+        final String RAW_TAB_TITLE = translate.localize(LocaleResources.DEADLOCK_RAW_TAB_TITLE).getContents();
+        tabbedPane.insertTab(RAW_TAB_TITLE, null, scrollPane, null, TAB_RAW_INDEX);
 
         new ComponentVisibilityNotifier().initialize(actualComponent, notifier);
     }
 
     @Override
-    public void setDeadLockInformation(final String info) {
+    public void setDeadLockInformation(final Information parsed, final String rawText) {
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
-                description.setText(info);
+
+                graphical.removeAll();
+
+                if (parsed != null) {
+                    FontMetrics metrics = graphical.getGraphics().getFontMetrics();
+                    graphical.add(createGraph(parsed, metrics), BorderLayout.CENTER);
+                }
+
+                description.setText(rawText);
             }
         });
     }
 
+    private mxGraphComponent createGraph(Information info, FontMetrics fontMetrics) {
+
+        final mxGraph graph = new mxGraph() {
+
+            /* Show tooltips for vertices and edges */
+            @Override
+            public String getToolTipForCell(Object source) {
+                mxCell cell = ((mxCell) source);
+
+                if (cell.getValue() instanceof GraphItem) {
+                    return ((GraphItem) cell.getValue()).getTooltip();
+                } else {
+                    return super.getToolTipForCell(cell);
+                }
+            }
+
+
+            /* Prevent modifying the contents of edges or vertices */
+            @Override
+            public boolean isCellEditable(Object cell) {
+                return false;
+            }
+
+            /* Prevent moving edges away from the vertices */
+            @Override
+            public boolean isCellSelectable(Object cell) {
+                return !model.isEdge(cell);
+            }
+
+        };
+
+        Object parent = graph.getDefaultParent();
+
+        addDeadlockToGraph(info, graph, parent, fontMetrics);
+
+        graph.setAutoSizeCells(true);
+        graph.setCellsResizable(true);
+
+        final mxGraphComponent graphComponent = new mxGraphComponent(graph);
+        graphComponent.setTextAntiAlias(true);
+        graphComponent.setToolTips(true);
+        graphComponent.setConnectable(false);
+
+        Map<String, Object> style = graph.getStylesheet().getDefaultVertexStyle();
+        style.put(mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
+        graph.getStylesheet().setDefaultVertexStyle(style);
+
+        mxGraphLayout layout = new mxCircleLayout(graph);
+        layout.execute(graph.getDefaultParent());
+
+        layout = new mxEdgeLabelLayout(graph);
+        layout.execute(graph.getDefaultParent());
+
+        return graphComponent;
+    }
+
+    private static void addDeadlockToGraph(Information info, mxGraph graph, Object parent, FontMetrics metrics) {
+        graph.getModel().beginUpdate(); // batch updates
+        try {
+            Map<String, Object> idToCell = new HashMap<>();
+            Map<String, String> idToLabel = new HashMap<>();
+
+            for (DeadlockParser.Thread thread : info.threads) {
+                String label = getThreadLabel(thread);
+                String tooltip = getThreadTooltip(thread);
+                idToLabel.put(thread.id, label);
+                GraphItem node = new GraphItem(label, tooltip);
+                final int PADDING = 20;
+                int width = metrics.stringWidth(label) + PADDING;
+                int height = metrics.getHeight() + PADDING;
+                Object threadNode = graph.insertVertex(parent, thread.id, node, 0, 0, width, height);
+                idToCell.put(thread.id, threadNode);
+            }
+
+            for (DeadlockParser.Thread thread : info.threads) {
+                String label = translate.localize(LocaleResources.DEADLOCK_WAITING_ON).getContents();
+                String tooltip = getEdgeTooltip(thread, idToLabel);
+                GraphItem edge = new GraphItem(label, tooltip);
+                graph.insertEdge(parent, thread.waitingOn.name, edge, idToCell.get(thread.id), idToCell.get(thread.waitingOn.ownerId));
+            }
+
+        }  finally {
+           graph.getModel().endUpdate();
+        }
+    }
+
+    private static String getThreadLabel(DeadlockParser.Thread thread) {
+        return translate.localize(LocaleResources.DEADLOCK_THREAD_NAME, thread.name, thread.id).getContents();
+    }
+
+    private static String getThreadTooltip(DeadlockParser.Thread thread) {
+        return translate.localize(
+                LocaleResources.DEADLOCK_THREAD_TOOLTIP,
+                htmlEscape(thread.waitingOn.name),
+                stackTraceToHtmlString(thread.stackTrace))
+            .getContents();
+    }
+
+    private static String stackTraceToHtmlString(List<String> items) {
+        StringBuilder result = new StringBuilder();
+        for (String item : items) {
+            result.append(htmlEscape(item)).append("<br/>");
+        }
+        return result.toString();
+    }
+
+    private static String htmlEscape(String in) {
+        // https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content
+        in = in.replaceAll("&", "&amp;");
+        in = in.replaceAll("<", "&lt;");
+        in = in.replaceAll(">", "&gt;");
+        in = in.replaceAll("\"", "&quot;");
+        in = in.replaceAll("'", "&#x27;");
+        in = in.replaceAll("/", "&#x2F;");
+        return in;
+    }
+
+    private static String getEdgeTooltip(DeadlockParser.Thread thread, Map<String, String> idToLabel) {
+        return translate.localize(
+                LocaleResources.DEADLOCK_EDGE_TOOLTIP,
+                idToLabel.get(thread.id),
+                idToLabel.get(thread.waitingOn.ownerId))
+            .getContents();
+    }
+
     @Override
     public Component getUiComponent() {
         return actualComponent;
     }
 
+    static class GraphItem implements Serializable {
+
+        private final String label;
+        private final String tooltip;
+
+        public GraphItem(String label, String tooltip) {
+            this.label = label;
+            this.tooltip = tooltip;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(label, tooltip);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj)
+                return true;
+            if (obj == null)
+                return false;
+            if (getClass() != obj.getClass())
+                return false;
+            GraphItem other = (GraphItem) obj;
+            return Objects.equals(label, other.label) && Objects.equals(tooltip, other.tooltip);
+        }
+
+        @Override
+        public String toString() {
+            return label;
+        }
+
+        public String getTooltip() {
+            return tooltip;
+        }
+    }
+
 }
-