diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ebe318 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.settings +.project +.classpath +target diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..db888ad --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,13 @@ +Copyright 2016 Dell Inc. + +Licensed 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. diff --git a/QUICK-START.rst b/QUICK-START.rst new file mode 100644 index 0000000..5277315 --- /dev/null +++ b/QUICK-START.rst @@ -0,0 +1,192 @@ +Gumshoe Quick Start Guide +========================= + +Overview +-------- + +For this example, we'll use a simple freeware chat program to demonstrate the features of gumshoe. + +Step 1: Get gumshoe +------------------- + +1. Make sure your system has maven3, ant, JDK7, probably some other stuff. + +2. Download gumshoe source. + +.. code:: sh + + wget http://somewhere.dell.com/somewhere/gumshoe.zip + +3. Remember where you put it. (Maybe save this in your .profile?) + +.. code:: sh + + export GUMSHOE_HOME=...this is the dir where you see subdirs gumshoe-hooks, gumshoe-probes, etc... + +4. Build + +Make sure this completes without errors. + +.. code:: sh + + cd $GUMSHOE_HOME + mvn install + +Step 2: Get target app +---------------------- + +Thanks to Chris Hallson for writing this example (and probably forgetting all about it years ago). +We'll use my fork of it to make it easier to add options to our java commandline. + +1. Download. + +.. code:: sh + + wget https://github.com/newbrough/JavaChat/archive/master.zip + +2. Remember where you put it. (Maybe save this in your .profile?) + +.. code:: sh + + unzip master.zip + cd JavaChat + export CHAT_HOME=`pwd` + +3. Try it out. + +Before looking at the I/O with gumshoe, lets just give this app a try. +Open two shell windows and in each one, run: + +.. code:: sh + + cd $CHAT_HOME + ./start-chat.sh + +You should have two identical chat windows. One of them select "server" and click "Connect". +The other leave as "client" and click "Connect". They should both show messages that they are connected. +Change the name of both to something other than "Unknown". Try sending a message or two. + +Everything working ok? Great! + +Step 3: Lets see it already +--------------------------- + +1. Run with gumshoe + +Again we will use two terminal windows and in one run the chat program as before. +But in one of those terminals, start the chat program with: + +.. code:: sh + + ./start-chat.sh --gumshoe + +You should see two windows now -- one is the normal chat program, and the other is the gumshoe viewer. + +2. Make something to look at. + +Connect one chat window as server, the other as client. Do something so there is some I/O to examine. +Change the name, send some messages, whatever. + +3. Take a snapshot. + +In the gumshoe window, navigate to the "Collect" tab. +After 30seconds of I/O (default settings) you should see "No data received" replaced by the time of the last sample sent by the probe. +Once you see that, click "Update" to view the latest sample received. You should see some blocks appear in the top main portion of the window. + +4. What is this thing? + +The default graph is a root graph -- the top of the stack immediately causing the I/O is shown on top, +the callers that invoked those methods next, and so on down the stack. + +For example, along the top you may see boxes for Socket.read() and Socket.write(). +Below Socket.read() may be 3 different boxes that are each a method that called Socket.read(). +The width of each box may represent the proportion of I/O. +Boxes colored red represent a frame responsible for 50% or more of displayed I/O, yellow is 25% or more. + +The box width currently represents the number of read operations, although bytes, operations and elapsed time +are tracked for reads and writes. Hover over a box to view all values. + +5. Just like that, but different. + +Navigate to the "Display" tab. Try changing the graph settings. Click "Apply" to view the settings in the current graph. + +Operation and measurement choices change which values are used to render width, color and choose which stack frames to view. + +The Direction setting lets you choose either a flame graph or root graph. +A flame graph starts at code entry points at bottom of stack, +and can help identify some upstream triggers of I/O like _which of my REST services result in the most I/O_? +A root graph starts at proximal cause of I/O at top of stack, +and can help identify lower-level bottlenecks like _is my database or REST client responsible for more I/O_? + +The default vuew uses the raw value for cell width, +so if a box is twice as wide as another then that stack frame is involved in twice as much I/O +(read operations, write milliseconds or whichever type happens to be selected at the moment). +To see frames that may be too narrow to appear otherwise, switch to log(value) or equal width sizing. + +Finally, arrange by value sorts cells so those with the most I/O appear on the left. +Note this may be confusing when changing other display options as the relative positions will move around more. + +Navigate to the "Examine" tab and click on a cell. +This shows all the statistics accumulated for that stack frame and its relation to the parent frame in the graph. + +6. Keep it real. + +All the stacks seen so far make great examples of how we can navigate the display, +but most of what is visible by default is irrelevant. +That was intentional (by the defaults in start-chat.sh) so we could use a small, simple program for this demo. + +When using gumshoe with a real project, however, +you are probably only interested in I/O related to parts of the program under your control. +This is what filters are all about. + +Navigate to the "Filter" tab, check the "drop JDK and gumshoe frames" and click "Apply to display". +Immediately all the ObjectInputStream and Socket stack frames are gone and you are left with just the +things in the javachat application and its libraries. +This doesn't look nearly as cool, because javachat is a pretty simple application. +(Which is why we poked around the display options with the full stack instead.) + +Here you can add (fully-qualified) packages or classes to look for or exclude from analysis. +Click "Apply to display" to filter the stacks just for your view. +Click "Apply to probe" to drop unneeded stack frames from the initial collection, +which reduces the resource usage and overhead of the gumshoe probe. + +Early analysis can also benefit from reducing stacks down to just the top and bottom few frames. +For example, the original target application for gumshoe +had threads that began with a REST, SOAP, or timer kicking off some action, +then filter down through various layers of business logic, +finally resulting in a SQL call, a direct TCP socket to another system, +or making a REST call to an external system. +Limiting the view alternately to the top or bottom few frames +showed the relative cost of services we provided and services we called, +and gave good targets for later filters to probe the full stack of those specific bottlenecks. + +Step 4: Now what? +----------------- + +1. Try it with your app + +The original javachat java cmdline was: + + java -classpath dist/JavaChat.jar:lib/* javachat.JavaChat + +To run with gumshoe, several options and arguments were added. Specifically: + +* Add hooks to bootclasspath + + -Xbootclasspath/p:$GUMSHOE_HOME/gumshoe-hooks/target/gumshoe-hooks-0.1.0-SNAPSHOT.jar + +* Add gumshoe-probes and gumshoe-tools to normal classpath + +* Insert com.dell.gumshoe.tools.Gumshoe as the main class, make the original main class the first argument + +* System properties set initial filter and reporting time, but you probably don't want these. + + The default is to report every 5min and automatically filter out the JDK and gumshoe classes, + which is probably appropriate. We reported every 30sec in the javachat example just so you could + see some data quickly, and filtered nothing out so there was more to see. + + You may want to select just the classes from your project. Maybe select your project com.mycompany.proj + and a library org.thirdparty.mylib using the property: + + -Dgumshoe.socket-io.include=com.mycompany.proj,org.thirdparty.mylib + \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..3730e47 --- /dev/null +++ b/README.rst @@ -0,0 +1,39 @@ + +Gumshoe Load Investigator +========================= + +Overview +-------- + +This tool profiles I/O operations and presents statistics over time per calling stack as a flame graph or root tree. + +The tool was first created initially for internal use at Dell and source code has been released +for public use under :doc:`these terms`. + +Packages +-------- + +* gumshoe-hooks + + A very small set of classes that must be loaded as part of the JVM bootclasspath to capture raw I/O. + +* gumshoe-probe + + A queue and filter system to queue, filter and summarize I/O events and pass results to listeners. + +* gumshoe-tools + + A swing GUI to configure the probe and display and manipulate results. + +Features +-------- + +Capture and visualize live socket I/O statistics and identify what is causing it. +View flame graph or root graph representation. +Filter stack frames at capture and/or during visualization, modify on the fly. + +Documentation +------------- + +* :doc:`Quick start guide` walks through using with a sample application. + diff --git a/gumshoe-hooks/.gitignore b/gumshoe-hooks/.gitignore new file mode 100644 index 0000000..ae3c172 --- /dev/null +++ b/gumshoe-hooks/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/gumshoe-hooks/README.rst b/gumshoe-hooks/README.rst new file mode 100644 index 0000000..f4c5ccb --- /dev/null +++ b/gumshoe-hooks/README.rst @@ -0,0 +1,34 @@ + +Gumshoe Load Investigator JVM Hooks +=================================== + +Overview +-------- + +Gumshoe adds a hook in the JVM to monitor socket and file I/O. The monitored JVM must be run +using the commandline: + + java -bootclasspath/p gumshoe-hooks.jar ... + + +More Detail +----------- + +Gumshoe Load Investigator measures socket and file I/O using the sun.misc.IoTrace class. +The built-in implementation has several empty methods that are called before and after each I/O +operation. This package overrides this implementation with one that allows gumshoe to handle +these calls and collect statistics. + +Because it has to override a built-in class from rt.jar, the contents of this module are included +in the bootclasspath before rt.jar. Then any application that wants to receive the IoTrace callbacks +can implement an interface IoTraceDelegate and install it with IoTraceUtil.addTrace(). + + +Performance Note +---------------- + +The IoTrace callbacks execute before and after every read or write operation on every socket or file. +It can affect performance. Specifically, it will add CPU and memory overhead to track the I/O. +For applications that are constrained by I/O performance, this is not usually a problem. +Regardless, this overhead can be removed by omitting the -bootclasspath/p option. The original +IoTrace class from rt.jar is used, resulting in no additional system load per I/O operation. diff --git a/gumshoe-hooks/pom.xml b/gumshoe-hooks/pom.xml new file mode 100644 index 0000000..e2c0fe0 --- /dev/null +++ b/gumshoe-hooks/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + gumshoe-hooks + + bootclasspath JVM hooks for gumshoe diagnostic tools + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.7 + 1.7 + + + + + + com.dell + 0.1.0-SNAPSHOT + Gumshoe JVM Hooks + diff --git a/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceAdapter.java b/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceAdapter.java new file mode 100644 index 0000000..425dea5 --- /dev/null +++ b/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceAdapter.java @@ -0,0 +1,15 @@ +package com.dell.gumshoe; + +import java.net.InetAddress; + +/** adapter class to simplify implementation of IoTraceDelegate */ +public class IoTraceAdapter implements IoTraceDelegate { + public Object socketReadBegin() { return null; } + public void socketReadEnd(Object context, InetAddress address, int port, int timeout, long bytesRead) { } + public Object socketWriteBegin() { return null; } + public void socketWriteEnd(Object context, InetAddress address, int port, long bytesWritten) { } + public Object fileReadBegin(String path) { return null; } + public void fileReadEnd(Object context, long bytesRead) { } + public Object fileWriteBegin(String path) { return null; } + public void fileWriteEnd(Object context, long bytesWritten) { } +} \ No newline at end of file diff --git a/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceDelegate.java b/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceDelegate.java new file mode 100644 index 0000000..270abd9 --- /dev/null +++ b/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceDelegate.java @@ -0,0 +1,15 @@ +package com.dell.gumshoe; + +import java.net.InetAddress; + +/** public interface for delegate to plug into sun.misc.IoTrace */ +public interface IoTraceDelegate { + public Object socketReadBegin(); + public void socketReadEnd(Object context, InetAddress address, int port, int timeout, long bytesRead); + public Object socketWriteBegin(); + public void socketWriteEnd(Object context, InetAddress address, int port, long bytesWritten); + public Object fileReadBegin(String path); + public void fileReadEnd(Object context, long bytesRead); + public Object fileWriteBegin(String path); + public void fileWriteEnd(Object context, long bytesWritten); +} \ No newline at end of file diff --git a/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceMultiplexer.java b/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceMultiplexer.java new file mode 100644 index 0000000..5a2a15c --- /dev/null +++ b/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceMultiplexer.java @@ -0,0 +1,138 @@ +package com.dell.gumshoe; + +import sun.misc.IoTrace; + +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +public class IoTraceMultiplexer implements IoTraceDelegate { + private List delegates = new CopyOnWriteArrayList<>(); + + public void addDelegate(IoTraceDelegate delegate) { + delegates.add(delegate); + } + + public void removeDelegate(IoTraceDelegate delegate) { + delegates.remove(delegate); + } + + @Override + public Object socketReadBegin() { + final Map mementoByDelegate = new IdentityHashMap<>(); + for(IoTraceDelegate delegate : delegates) { + mementoByDelegate.put(delegate, delegate.socketReadBegin()); + } + return mementoByDelegate; + } + + @Override + public void socketReadEnd(Object context, InetAddress address, int port, int timeout, long bytesRead) { + if( ! (context instanceof IdentityHashMap)) { return; } + final Map mementoByDelegate = (Map)context; + for(IoTraceDelegate delegate : delegates) { + final Object memento = mementoByDelegate.get(delegate); + delegate.socketReadEnd(memento, address, port, timeout, bytesRead); + } + } + + @Override + public Object socketWriteBegin() { + final Map mementoByDelegate = new IdentityHashMap<>(); + for(IoTraceDelegate delegate : delegates) { + mementoByDelegate.put(delegate, delegate.socketWriteBegin()); + } + return mementoByDelegate; + } + + @Override + public void socketWriteEnd(Object context, InetAddress address, int port, long bytesWritten) { + if( ! (context instanceof IdentityHashMap)) { return; } + final Map mementoByDelegate = (Map)context; + for(IoTraceDelegate delegate : delegates) { + final Object memento = mementoByDelegate.get(delegate); + delegate.socketWriteEnd(memento, address, port, bytesWritten); + } + } + + @Override + public Object fileReadBegin(String path) { + final Map mementoByDelegate = new IdentityHashMap<>(); + for(IoTraceDelegate delegate : delegates) { + mementoByDelegate.put(delegate, delegate.fileReadBegin(path)); + } + return mementoByDelegate; + } + + @Override + public void fileReadEnd(Object context, long bytesRead) { + if( ! (context instanceof IdentityHashMap)) { return; } + final Map mementoByDelegate = (Map)context; + for(IoTraceDelegate delegate : delegates) { + final Object memento = mementoByDelegate.get(delegate); + delegate.fileReadEnd(memento, bytesRead); + } + } + + @Override + public Object fileWriteBegin(String path) { + final Map mementoByDelegate = new IdentityHashMap<>(); + for(IoTraceDelegate delegate : delegates) { + mementoByDelegate.put(delegate, delegate.fileWriteBegin(path)); + } + return mementoByDelegate; + } + + @Override + public void fileWriteEnd(Object context, long bytesWritten) { + if( ! (context instanceof IdentityHashMap)) { return; } + final Map mementoByDelegate = (Map)context; + for(IoTraceDelegate delegate : delegates) { + final Object memento = mementoByDelegate.get(delegate); + delegate.fileWriteEnd(memento, bytesWritten); + } + } + + ///// + + public static void install(IoTraceDelegate delegate) throws Exception { + final Field nullField = IoTrace.class.getField("NULL_OBJECT"); + nullField.setAccessible(true); + final Object nullObject = nullField.get(IoTrace.class); + + final Field delegateField = IoTrace.class.getField("delegate"); + delegateField.setAccessible(true); + final Object oldValue = delegateField.get(IoTrace.class); + if(oldValue==nullObject) { + delegateField.set(IoTrace.class, delegate); + } else if(oldValue instanceof IoTraceMultiplexer) { + final IoTraceMultiplexer multi = (IoTraceMultiplexer) oldValue; + multi.addDelegate(delegate); + } else { + final IoTraceMultiplexer multi = new IoTraceMultiplexer(); + multi.addDelegate((IoTraceDelegate)oldValue); + delegateField.set(IoTrace.class, multi); + } + } + + public static void remove(IoTraceDelegate delegate) throws Exception { + final Field delegateField = IoTrace.class.getField("delegate"); + delegateField.setAccessible(true); + final Object oldValue = delegateField.get(IoTrace.class); + if(oldValue.equals(delegate)) { + final Field nullField = IoTrace.class.getField("NULL_OBJECT"); + nullField.setAccessible(true); + final Object nullObject = nullField.get(IoTrace.class); + + delegateField.set(IoTrace.class, nullObject); + } else if(oldValue instanceof IoTraceMultiplexer) { + final IoTraceMultiplexer multi = (IoTraceMultiplexer) oldValue; + multi.removeDelegate(delegate); + } else { + throw new IllegalArgumentException("unable to remove, IoTraceDelegate was not installed: " + delegate); + } + } +} diff --git a/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceUtil.java b/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceUtil.java new file mode 100644 index 0000000..37626f2 --- /dev/null +++ b/gumshoe-hooks/src/main/java/com/dell/gumshoe/IoTraceUtil.java @@ -0,0 +1,46 @@ +package com.dell.gumshoe; + +import sun.misc.IoTrace; + +import java.lang.reflect.Field; + +public class IoTraceUtil { + + public static void addTrace(IoTraceDelegate delegate) throws Exception { + final Field nullField = IoTrace.class.getDeclaredField("NULL_OBJECT"); + nullField.setAccessible(true); + final Object nullObject = nullField.get(IoTrace.class); + + final Field delegateField = IoTrace.class.getDeclaredField("delegate"); + delegateField.setAccessible(true); + final Object oldValue = delegateField.get(IoTrace.class); + if(oldValue==nullObject) { + delegateField.set(IoTrace.class, delegate); + } else if(oldValue instanceof IoTraceMultiplexer) { + final IoTraceMultiplexer multi = (IoTraceMultiplexer) oldValue; + multi.addDelegate(delegate); + } else { + final IoTraceMultiplexer multi = new IoTraceMultiplexer(); + multi.addDelegate((IoTraceDelegate)oldValue); + delegateField.set(IoTrace.class, multi); + } + } + + public static void removeTrace(IoTraceDelegate delegate) throws Exception { + final Field delegateField = IoTrace.class.getDeclaredField("delegate"); + delegateField.setAccessible(true); + final Object oldValue = delegateField.get(IoTrace.class); + if(oldValue.equals(delegate)) { + final Field nullField = IoTrace.class.getDeclaredField("NULL_OBJECT"); + nullField.setAccessible(true); + final Object nullObject = nullField.get(IoTrace.class); + + delegateField.set(IoTrace.class, nullObject); + } else if(oldValue instanceof IoTraceMultiplexer) { + final IoTraceMultiplexer multi = (IoTraceMultiplexer) oldValue; + multi.removeDelegate(delegate); + } else { + throw new IllegalArgumentException("unable to remove, that IoTraceDelegate was not installed: " + delegate); + } + } +} diff --git a/gumshoe-hooks/src/main/java/sun/misc/IoTrace.java b/gumshoe-hooks/src/main/java/sun/misc/IoTrace.java new file mode 100644 index 0000000..16e93a6 --- /dev/null +++ b/gumshoe-hooks/src/main/java/sun/misc/IoTrace.java @@ -0,0 +1,121 @@ +package sun.misc; + +import com.dell.gumshoe.IoTraceAdapter; +import com.dell.gumshoe.IoTraceDelegate; + +import java.net.InetAddress; + +/** redefine template from rt.jar + * + * do as little work as possible here in this hack -- define a proper interface and way to set it, + * then do any real work in the normal package structure + */ +public final class IoTrace { + private static IoTraceDelegate NULL_OBJECT = new IoTraceAdapter(); + private static IoTraceDelegate delegate = NULL_OBJECT; + + /** + * Called before data is read from a socket. + * + * @return a context object + */ + public static Object socketReadBegin() { + + return delegate.socketReadBegin(); + } + + /** + * Called after data is read from the socket. + * + * @param context + * the context returned by the previous call to socketReadBegin() + * @param address + * the remote address the socket is bound to + * @param port + * the remote port the socket is bound to + * @param timeout + * the SO_TIMEOUT value of the socket (in milliseconds) or 0 if + * there is no timeout set + * @param bytesRead + * the number of bytes read from the socket, 0 if there was an + * error reading from the socket + */ + public static void socketReadEnd(Object context, InetAddress address, int port, int timeout, long bytesRead) { + delegate.socketReadEnd(context, address, port, timeout, bytesRead); + } + + /** + * Called before data is written to a socket. + * + * @return a context object + */ + public static Object socketWriteBegin() { + return delegate.socketWriteBegin(); + } + + /** + * Called after data is written to a socket. + * + * @param context + * the context returned by the previous call to + * socketWriteBegin() + * @param address + * the remote address the socket is bound to + * @param port + * the remote port the socket is bound to + * @param bytesWritten + * the number of bytes written to the socket, 0 if there was an + * error writing to the socket + */ + public static void socketWriteEnd(Object context, InetAddress address, int port, long bytesWritten) { + delegate.socketWriteEnd(context, address, port, bytesWritten); + } + + /** + * Called before data is read from a file. + * + * @param path + * the path of the file + * @return a context object + */ + public static Object fileReadBegin(String path) { + return delegate.fileReadBegin(path); + } + + /** + * Called after data is read from a file. + * + * @param context + * the context returned by the previous call to fileReadBegin() + * @param bytesRead + * the number of bytes written to the file, 0 if there was an + * error writing to the file + */ + public static void fileReadEnd(Object context, long bytesRead) { + delegate.fileReadEnd(context, bytesRead); + } + + /** + * Called before data is written to a file. + * + * @param path + * the path of the file + * @return a context object + */ + public static Object fileWriteBegin(String path) { + return delegate.fileWriteBegin(path); + } + + /** + * Called after data is written to a file. + * + * @param context + * the context returned by the previous call to fileReadBegin() + * @param bytesWritten + * the number of bytes written to the file, 0 if there was an + * error writing to the file + */ + public static void fileWriteEnd(Object context, long bytesWritten) { + delegate.fileWriteEnd(context, bytesWritten); + } +} diff --git a/gumshoe-hooks/src/test/resources/.keep b/gumshoe-hooks/src/test/resources/.keep new file mode 100644 index 0000000..e69de29 diff --git a/gumshoe-probes/.gitignore b/gumshoe-probes/.gitignore new file mode 100644 index 0000000..ae3c172 --- /dev/null +++ b/gumshoe-probes/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/gumshoe-probes/pom.xml b/gumshoe-probes/pom.xml new file mode 100644 index 0000000..3c0db7a --- /dev/null +++ b/gumshoe-probes/pom.xml @@ -0,0 +1,53 @@ + + 4.0.0 + + gumshoe-probes + + runtime probes for gumshoe load analysis + + 4.12 + + + + + junit + junit + ${junit.version} + test + + + com.dell + gumshoe-hooks + 0.1.0-SNAPSHOT + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + **/*Test*.* + + + + + + + com.dell + 0.1.0-SNAPSHOT + Gumshoe Probes + diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/Probe.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/Probe.java new file mode 100644 index 0000000..3d4dcd8 --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/Probe.java @@ -0,0 +1,530 @@ +package com.dell.gumshoe; + +import com.dell.gumshoe.socket.SocketCloseMonitor; +import com.dell.gumshoe.socket.SocketCloseMonitorMBean; +import com.dell.gumshoe.socket.SocketIOAccumulator; +import com.dell.gumshoe.socket.SocketIOMonitor; +import com.dell.gumshoe.socket.SocketIOStackReporter; +import com.dell.gumshoe.socket.SocketIOStackReporter.StreamReporter; +import com.dell.gumshoe.socket.SocketMatcher; +import com.dell.gumshoe.socket.SocketMatcherSeries; +import com.dell.gumshoe.socket.SubnetAddress; +import com.dell.gumshoe.stack.Filter; +import com.dell.gumshoe.stack.Filter.Builder; +import com.dell.gumshoe.stack.StackFilter; + +import javax.management.ObjectName; +import javax.management.StandardMBean; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.lang.management.ManagementFactory; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.text.ParseException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.CopyOnWriteArrayList; + +/** util to enable/disable monitoring tools + * + * can wrap your main and run as java application: + * old cmdline: java ...opts... x.y.SomeClass args... + * new cmdline: java ...opts... com.dell.gumshoe.Probe x.y.SomeClass args... + * + * or make explicit from a java app: + * Probe.initialize(); + */ +public class Probe { + public static Probe MAIN_INSTANCE; + + public static void main(String... args) throws Throwable { + final String[] newArgs = new String[args.length-1]; + System.arraycopy(args, 1, newArgs, 0, args.length-1); + + MAIN_INSTANCE = new Probe(); + MAIN_INSTANCE.initialize(); + + final Class mainClass = Class.forName(args[0]); + final Method mainMethod = mainClass.getDeclaredMethod("main", args.getClass()); + try { + mainMethod.invoke(mainClass, new Object[] { newArgs }); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + ///// + + private Timer timer; + private SocketCloseMonitor closeMonitor; + private SocketIOMonitor ioMonitor; + private SocketIOAccumulator ioAccumulator; + private SocketIOStackReporter ioReporter; + private final List shutdownHooks = new CopyOnWriteArrayList<>(); + private Map namedOutput = new HashMap<>(); + + public Probe() { + final Thread shutdownThread = new Thread() { + @Override + public void run() { + for(Runnable task : shutdownHooks) { + try { + task.run(); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + }; + shutdownThread.setName("gumshoe-shutdown"); + Runtime.getRuntime().addShutdownHook(shutdownThread); + } + + public void setOutput(String key, PrintStream value) { + namedOutput.put(key, value); + } + + private synchronized Timer getTimer() { + if(timer==null) { + timer = new Timer(true); + } + return timer; + } + + ///// util methods + + private static boolean isTrue(Properties p, String key, boolean defaultValue) { + return "true".equalsIgnoreCase(p.getProperty(key, Boolean.toString(defaultValue))); + } + + private static long getNumber(Properties p, String key, long defaultValue) { + return Long.parseLong(p.getProperty(key, Long.toString(defaultValue))); + } + + private static Long getNumber(Properties p, String key) { + final String stringValue = p.getProperty(key); + return stringValue==null ? null : Long.parseLong(stringValue); + } + + private static String[] getList(Properties p, String key) { + final String stringValue = p.getProperty(key); + if(stringValue==null || stringValue.isEmpty()) { return new String[0]; } + final String[] out = stringValue.split(","); + for(int i=0;i0 || bottomCount>0) { + builder.withEndsOnly(topCount, bottomCount); + } + return builder.build(); + } + + public SocketIOMonitor initializeIOMonitor(boolean shutdownReportEnabled, Long periodicFrequency, SocketMatcher socketFilter, StackFilter stackFilter, final PrintStream out) throws Exception { + if(ioMonitor!=null) throw new IllegalStateException("monitor is already installed"); + + ioAccumulator = new SocketIOAccumulator(stackFilter); + + ioMonitor = new SocketIOMonitor(socketFilter); + ioMonitor.addListener(ioAccumulator); + + ioReporter = new SocketIOStackReporter(ioAccumulator); + if(shutdownReportEnabled) { + addShutdownHook(ioReporter); + } + if(periodicFrequency!=null) { + getTimer().scheduleAtFixedRate(ioReporter, periodicFrequency, periodicFrequency); + } + if(out!=null) { + StreamReporter r = new StreamReporter(out); + ioReporter.addListener(r); + } + + ioMonitor.initializeProbe(); + return ioMonitor; + } + + public SocketIOMonitor getIOMonitor() { + return ioMonitor; + } + + public SocketIOAccumulator getIOAccumulator() { + return ioAccumulator; + } + + public SocketIOStackReporter getIOReporter() { + return ioReporter; + } + + ///// + + private void addShutdownHook(Runnable task) { + shutdownHooks.add(task); + } + + ///// + + private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { @Override public void write(int b) { } }; + private static final PrintStream NULL_PRINT_STREAM = new NullPrintStream(); + + private static class NullPrintStream extends PrintStream { + public NullPrintStream() { super(NULL_OUTPUT_STREAM); } + @Override public void flush() { } + @Override public void close() { } + @Override public void print(boolean b){ } + + @Override public void print(char c) { } + @Override public void print(int i) { } + @Override public void print(long l) { } + @Override public void print(float f) { } + @Override public void print(double d) { } + @Override public void print(char[] s) { } + @Override public void print(String s) { } + @Override public void print(Object obj) { } + @Override public void println() { } + @Override public void println(boolean x) { } + @Override public void println(char x) { } + @Override public void println(int x) { } + @Override public void println(long x) { } + @Override public void println(float x) { } + @Override public void println(double x) { } + @Override public void println(char[] x) { } + @Override public void println(String x) { } + @Override public void println(Object x) { } + @Override public PrintStream printf(String format, Object... args) { return this; } + @Override public PrintStream printf(Locale l, String format, Object... args) { return this; } + @Override public PrintStream format(String format, Object... args) { return this; } + @Override public PrintStream format(Locale l, String format, Object... args) { return this; } + @Override public PrintStream append(CharSequence csq) { return this; } + @Override public PrintStream append(CharSequence csq, int start, int end) { return this; } + @Override public PrintStream append(char c) { return this; } } + + /** probe may be started right as JVM starts, + * but we may need to let the monitored program start and reach some hook + * to programmatically define more interesting outputs. so this + * + */ + private class DefineLaterPrintStream extends PrintStream { + private String key; + private volatile PrintStream delegate; + + public DefineLaterPrintStream(String key) { + super(NULL_OUTPUT_STREAM); + this.key = key; + } + + private PrintStream getDelegate() { + PrintStream localCopy = delegate; + if(localCopy==null) { + localCopy = setDelegate(); + } + return localCopy==null ? NULL_PRINT_STREAM : localCopy; + } + + private synchronized PrintStream setDelegate() { + delegate = namedOutput.get(key); + return delegate; + } + + private synchronized void clearDelegate() { + delegate = null; + } + + /** when closed, delegate is closed but stream can be reopened + * next output will re-fetch from namedOutput map, + * so change map value before closing this stream + * to switch to a new output + */ + @Override + public void close() { + getDelegate().close(); + clearDelegate(); + } + + @Override + public void write(byte[] b) throws IOException { getDelegate().write(b); } + @Override + public void flush() { getDelegate().flush(); } + @Override + public boolean checkError() { return getDelegate().checkError(); } + @Override + public void write(int b) { getDelegate().write(b); } + @Override + public void write(byte[] buf, int off, int len) { getDelegate().write(buf, off, len); } + @Override + public void print(boolean b) { getDelegate().print(b); } + @Override + public void print(char c) { getDelegate().print(c); } + @Override + public void print(int i) { getDelegate().print(i); } + @Override + public void print(long l) { getDelegate().print(l); } + @Override + public void print(float f) { getDelegate().print(f); } + @Override + public void print(double d) { getDelegate().print(d); } + @Override + public void print(char[] s) { getDelegate().print(s); } + @Override + public void print(String s) { getDelegate().print(s); } + @Override + public void print(Object obj) { getDelegate().print(obj); } + @Override + public void println() { getDelegate().println(); } + @Override + public void println(boolean x) { getDelegate().println(x); } + @Override + public void println(char x) { getDelegate().println(x); } + @Override + public void println(int x) { getDelegate().println(x); } + @Override + public void println(long x) { getDelegate().println(x); } + @Override + public void println(float x) { getDelegate().println(x); } + @Override + public void println(double x) { getDelegate().println(x); } + @Override + public void println(char[] x) { getDelegate().println(x); } + @Override + public void println(String x) { getDelegate().println(x); } + @Override + public void println(Object x) { getDelegate().println(x); } + @Override + public PrintStream printf(String format, Object... args) { return getDelegate().printf(format, args); } + @Override + public PrintStream printf(Locale l, String format, Object... args) { return getDelegate().printf(l, format, args); } + @Override + public PrintStream format(String format, Object... args) { return getDelegate().format(format, args); } + @Override + public PrintStream format(Locale l, String format, Object... args) { return getDelegate().format(l, format, args); } + @Override + public PrintStream append(CharSequence csq) { return getDelegate().append(csq); } + @Override + public PrintStream append(CharSequence csq, int start, int end) { return getDelegate().append(csq, start, end); } + @Override + public PrintStream append(char c) { return getDelegate().append(c); } + + + } +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketCloseMonitor.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketCloseMonitor.java new file mode 100644 index 0000000..c6293f9 --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketCloseMonitor.java @@ -0,0 +1,252 @@ +package com.dell.gumshoe.socket; + +import com.dell.gumshoe.stack.Stack; +import com.dell.gumshoe.stack.StackFilter; + +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.Socket; +import java.net.SocketImpl; +import java.net.SocketImplFactory; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** monitor and report on sockets left open + */ +public class SocketCloseMonitor implements SocketImplFactory, SocketCloseMonitorMBean { + private static AtomicInteger SOCKET_IDS = new AtomicInteger(); + + private final AtomicInteger socketCount = new AtomicInteger(); + private final ConcurrentMap openSockets = new ConcurrentHashMap(); + private final ConcurrentMap countByStack = new ConcurrentHashMap<>(); + private final Object clearClosedLock = new Object(); + private Thread clearClosedThread; + private int clearClosedPerCount = 100; + private StackFilter filter; + private boolean enabled = true; + + private Constructor socketConstructor; + private Method getSocketMethod; + + public SocketCloseMonitor() throws Exception { + Class defaultSocketImpl = Class.forName("java.net.SocksSocketImpl"); + socketConstructor = defaultSocketImpl.getDeclaredConstructor(); + socketConstructor.setAccessible(true); + + getSocketMethod = SocketImpl.class.getDeclaredMethod("getSocket"); + getSocketMethod.setAccessible(true); + } + + @Override + public boolean isEnabled() { return enabled; } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if( ! enabled) { + synchronized(clearClosedLock) { + clearClosedSockets(); + openSockets.clear(); + countByStack.clear(); + } + } + } + + ///// + + public void initializeProbe() throws IOException { + Socket.setSocketImplFactory(this); + } + + public void destroyProbe() { + // Socket.setSocketImplFactory() can be called only once + throw new UnsupportedOperationException("socket close monitor cannot be removed"); + } + + /** request the system clear closed sockets every Nth time a new socket is created */ + public void setClearClosedSocketsInterval(int numberOfSockets) { + clearClosedPerCount = numberOfSockets; + if(clearClosedThread==null) { + clearClosedThread = new Thread(new ScanForClosed()); + clearClosedThread.setDaemon(true); + clearClosedThread.setName("clear-closed-sockets"); + clearClosedThread.start(); + } + } + + public List findOpenedBefore(Date cutoff) { + final List out = new ArrayList(); + for(SocketImplDecorator value : openSockets.values()) { + if(value.openTime.before(cutoff) && ! value.isClosed()) { + out.add(value); + } + } + return out; + } + + public int getSocketCount() { + clearClosedSockets(); + return socketCount.get(); + } + + public Map getCountsByStack() { + final Map out = new HashMap<>(countByStack.size()); + for(Map.Entry entry : countByStack.entrySet()) { + out.put(entry.getKey(), entry.getValue().get()); + } + return out; + } + + ///// + + /** JVM hook: capture info about socket as it is created */ + @Override + public SocketImpl createSocketImpl() { + final int socketId = SOCKET_IDS.incrementAndGet(); + if(enabled) { + if(socketId%clearClosedPerCount==0) { + notifyClearClosed(); + } + final SocketImplDecorator wrapper = new SocketImplDecorator(socketId); + openSockets.put(wrapper.id, wrapper); + socketCount.incrementAndGet(); + AtomicInteger countWithThisStack = countByStack.get(wrapper.stack); + if(countWithThisStack==null) { + final AtomicInteger newCount = new AtomicInteger(); + final AtomicInteger priorEntry = countByStack.putIfAbsent(wrapper.stack, newCount); + countWithThisStack = priorEntry==null ? newCount : priorEntry; + } + countWithThisStack.incrementAndGet(); + // return raw impl, but keep a ref to track it + return wrapper.impl; + } else { + // not monitoring? return raw untracked impl + return newSocketImpl(); + } + } + + private SocketImpl newSocketImpl() { + try { + return (SocketImpl) socketConstructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** information maintained about each open socket */ + public class SocketImplDecorator { + public final int id; + public final SocketImpl impl; + public final Stack stack; + public final Date openTime; + + private SocketImplDecorator(int id) { + this.id = id; + this.impl = newSocketImpl(); + this.stack = new Stack(); + this.openTime = new Date(); + } + + private Socket getSocket() { + try { + return (Socket) getSocketMethod.invoke(impl); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private boolean isClosed() { + return getSocket().isClosed(); + } + +// public SocketImplDecorator filterStack(StackFilter filter) { +// return new SocketImplDecorator(id, impl, stack.applyFilter(filter), openTime); +// } + + @Override + public String toString() { + final Socket socket = getSocket(); + + return String.format("%s %s (%s)\n%s", + socket.getRemoteSocketAddress().toString(), + openTime.toString(), + socket.isClosed() ? "closed" : "open", + stack.toString()); + } + } + + private void clearClosedSockets() { + synchronized(clearClosedLock) { + for(SocketImplDecorator value : openSockets.values()) { + if(value.isClosed()) { + socketCount.decrementAndGet(); + openSockets.remove(value.id); + } + } + } + } + + ///// + + private void notifyClearClosed() { + synchronized(clearClosedLock) { + clearClosedLock.notifyAll(); + } + } + + private class ScanForClosed implements Runnable { + @Override + public void run() { + synchronized(clearClosedLock) { + while(true) { + try { + clearClosedLock.wait(); + clearClosedSockets(); + } catch(Exception ignore) { } + } + } + } + } + + ///// + + @Override + public String getReport(long minimumAge) { + final long cutoff = System.currentTimeMillis() - minimumAge; + final List unclosed = findOpenedBefore(new Date(cutoff)); + final StringBuilder report = new StringBuilder(); + report.append("total ").append(unclosed.size()).append(" unclosed sockets:\n"); + + final Map> summaryByStack = new HashMap<>(); + for(SocketImplDecorator wrapper : unclosed) { + final Stack filteredStack = filter==null ? wrapper.stack : wrapper.stack.applyFilter(filter); + Map countByDesc = summaryByStack.get(filteredStack); + if(countByDesc==null) { + countByDesc = new HashMap(); + summaryByStack.put(wrapper.stack, countByDesc); + } + final String address = wrapper.getSocket().getRemoteSocketAddress().toString(); + Integer count = countByDesc.get(address); + countByDesc.put(address, (count==null) ? 1 : (count+1)); + } + + for(Map.Entry> stackEntry : summaryByStack.entrySet()) { + final Stack stack = stackEntry.getKey(); + final Map countByDescription = stackEntry.getValue(); + for(Map.Entry countEntry : countByDescription.entrySet()) { + report.append(countEntry.getValue()).append(" connections to ").append(countEntry.getKey()).append("\n"); + } + report.append(stack).append("\n"); + } + return report.toString(); + } +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketCloseMonitorMBean.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketCloseMonitorMBean.java new file mode 100644 index 0000000..4872808 --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketCloseMonitorMBean.java @@ -0,0 +1,8 @@ +package com.dell.gumshoe.socket; + + +public interface SocketCloseMonitorMBean { + public void setEnabled(boolean enabled); + public boolean isEnabled(); + public String getReport(long age); +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOAccumulator.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOAccumulator.java new file mode 100644 index 0000000..8c8cdad --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOAccumulator.java @@ -0,0 +1,53 @@ +package com.dell.gumshoe.socket; + +import com.dell.gumshoe.socket.SocketIOMonitor.Event; +import com.dell.gumshoe.stack.Stack; +import com.dell.gumshoe.stack.StackFilter; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** create hierarchical analysis of socket IO versus stack + * with total IO at each frame and + * link from frame to multiple next frames below + */ +public class SocketIOAccumulator implements SocketIOListener { + private final ConcurrentMap totals = new ConcurrentHashMap<>(); + private StackFilter filter; + + public SocketIOAccumulator(StackFilter filter) { + this.filter = filter; + } + + public void setFilter(StackFilter filter) { + this.filter = filter; + totals.clear(); + } + + @Override + public void socketIOHasCompleted(Event event) { + final IODetail value = new IODetail(event); + Stack stack = event.getStack().applyFilter(filter); + final DetailAccumulator total = getAccumulator(stack); + total.add(value); + } + + private DetailAccumulator getAccumulator(Stack key) { + final DetailAccumulator entry = totals.get(key); + if(entry!=null) { + return entry; + } + totals.putIfAbsent(key, new DetailAccumulator()); + return totals.get(key); + } + + public Map getStats() { + return totals; + } + + public void reset() { + totals.clear(); + } +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOListener.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOListener.java new file mode 100644 index 0000000..925b138 --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOListener.java @@ -0,0 +1,108 @@ +package com.dell.gumshoe.socket; + +import com.dell.gumshoe.socket.SocketIOMonitor.Event; +import com.dell.gumshoe.socket.SocketIOMonitor.Listener; +import com.dell.gumshoe.socket.SocketIOMonitor.RW; + +import java.net.InetAddress; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public interface SocketIOListener extends Listener { + + @Override + public void socketIOHasCompleted(Event event); + + ///// + + public static class IODetail { + private static String convertAddress(InetAddress addr, int port) { + final byte[] ip = addr.getAddress(); + return String.format("%d.%d.%d.%d:%d", 255&ip[0], 255&ip[1], 255&ip[2], 255&ip[3], port); + } + + private final String address; + private final long readBytes, readTime, writeBytes, writeTime; + private final int readCount, writeCount; + + public IODetail(Event e) { + this(convertAddress(e.getAddress(), e.getPort()), e.getReadBytes(), e.getReadElapsed(), e.getRw()==RW.READ?1:0, e.getWriteBytes(), e.getWriteElapsed(), e.getRw()==RW.WRITE?1:0); + } + + public IODetail(String address, long readBytes, long readTime, long writeBytes, long writeTime) { + this(address, readBytes, readTime, 1, writeBytes, writeTime, 1); + } + + public IODetail(String address, long readBytes, long readTime, int readCount, long writeBytes, long writeTime, int writeCount) { + this.address = address; + this.readBytes = readBytes; + this.readTime = readTime; + this.writeBytes = writeBytes; + this.writeTime = writeTime; + this.readCount = readCount; + this.writeCount = writeCount; + } + + @Override + public String toString() { + return String.format("%d read ops %d bytes in %d ms, %d write ops %d bytes in %d ms: %s", + readCount, readBytes, readTime, writeCount, writeBytes, writeTime, address); + } + } + + public static interface ValueType { + public void add(V value); + public V get(); + public ValueType newInstance(); + } + + public static class DetailAccumulator implements ValueType { + public final Set addresses = new ConcurrentSkipListSet<>(); + public final AtomicLong readBytes = new AtomicLong(); + public final AtomicLong readTime = new AtomicLong(); + public final AtomicInteger readCount = new AtomicInteger(); + public final AtomicLong writeBytes = new AtomicLong(); + public final AtomicLong writeTime = new AtomicLong(); + public final AtomicInteger writeCount = new AtomicInteger(); + + public void add(DetailAccumulator that) { + addresses.addAll(that.addresses); + readBytes.addAndGet(that.readBytes.get()); + readTime.addAndGet(that.readTime.get()); + readCount.addAndGet(that.readCount.get()); + writeBytes.addAndGet(that.writeBytes.get()); + writeTime.addAndGet(that.writeTime.get()); + writeCount.addAndGet(that.writeCount.get()); + + } + @Override + public void add(IODetail value) { + addresses.add(value.address); + readBytes.addAndGet(value.readBytes); + readTime.addAndGet(value.readTime); + readCount.addAndGet(value.readCount); + writeBytes.addAndGet(value.writeBytes); + writeTime.addAndGet(value.writeTime); + writeCount.addAndGet(value.writeCount); + } + + @Override + public IODetail get() { + return new IODetail(addresses.toString(), readBytes.get(), readTime.get(), readCount.get(), writeBytes.get(), writeTime.get(), writeCount.get()); + } + + @Override + public DetailAccumulator newInstance() { + return new DetailAccumulator(); + } + + @Override + public String toString() { + return String.format("%d r %d bytes in %d ms, %d w %d bytes in %d ms", + readCount.get(), readBytes.get(), readTime.get(), + writeCount.get(), writeBytes.get(), writeTime.get()); + } + } +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOMBean.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOMBean.java new file mode 100644 index 0000000..5cdc18a --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOMBean.java @@ -0,0 +1,10 @@ +package com.dell.gumshoe.socket; + + +public interface SocketIOMBean { +// public String startIOMonitor(boolean shutdownReportEnabled, Long periodicFrequency, String filter) throws Exception; +// public String stopIOMonitor() throws Exception; + +// public String getIOSummary(); +// public void resetIO(); +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOMonitor.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOMonitor.java new file mode 100644 index 0000000..60ab844 --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOMonitor.java @@ -0,0 +1,229 @@ +package com.dell.gumshoe.socket; + +import com.dell.gumshoe.IoTraceAdapter; +import com.dell.gumshoe.IoTraceUtil; +import com.dell.gumshoe.stack.Filter; +import com.dell.gumshoe.stack.Stack; +import com.dell.gumshoe.stack.StackFilter; + +import java.io.PrintStream; +import java.net.InetAddress; +import java.util.List; +import java.util.TimerTask; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** monitor socket IO and report each as an event to registered listeners + */ +public class SocketIOMonitor extends IoTraceAdapter implements SocketIOMBean { + + private BlockingQueue queue; + private final List listeners = new CopyOnWriteArrayList<>(); + + private final AtomicInteger failCounter = new AtomicInteger(); + private final AtomicInteger successCounter = new AtomicInteger(); + private final EventConsumer consumer = new EventConsumer(); + private final Thread consumerThread = new Thread(consumer); + + private final SocketMatcher socketFilter; + private boolean enabled = true; + private int eventQueueSize = 500; + private long queueOverflowReportInterval = 300000; + private long lastFailReport; + + public SocketIOMonitor() { + this(SubnetAddress.ANY); + } + + public SocketIOMonitor(SocketMatcher socketFilter) { + this.socketFilter = socketFilter; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if( ! enabled) { + queue.clear(); + } + } + + public int getEventQueueSize() { + return eventQueueSize; + } + + public void setEventQueueSize(int eventQueueSize) { + if(queue!=null) { throw new IllegalStateException("cannot resize queue after probe has been installed"); } + this.eventQueueSize = eventQueueSize; + } + + public long getOverflowReportInterval() { + return queueOverflowReportInterval; + } + + public void setOverflowReportInterval(long queueOverflowReportInterval) { + this.queueOverflowReportInterval = queueOverflowReportInterval; + } + + public void initializeProbe() throws Exception { + queue = new LinkedBlockingQueue<>(eventQueueSize); + startConsumer(); + IoTraceUtil.addTrace(this); + } + + public void destroyProbe() throws Exception { + IoTraceUtil.removeTrace(this); + stopConsumer(); + queue = null; + } + + ///// + + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public static interface Listener { + public void socketIOHasCompleted(Event event); + } + + private void notifyListeners(Event operation) { + for(Listener listener : listeners) { + try { + listener.socketIOHasCompleted(operation); + } catch(Exception ignore) { + ignore.printStackTrace(); + } + } + } + + ///// + + @Override + public Object socketReadBegin() { + return new Event(RW.READ); + } + + @Override + public Object socketWriteBegin() { + return new Event(RW.WRITE); + } + + @Override + public void socketReadEnd(Object context, InetAddress address, int port, int timeout, long bytesRead) { + handleEvent(context, address, port, bytesRead); + } + + @Override + public void socketWriteEnd(Object context, InetAddress address, int port, long bytesWritten) { + handleEvent(context, address, port, bytesWritten); + } + + private void handleEvent(Object context, InetAddress address, int port, long bytes) { + if(socketFilter.matches(address.getAddress(), port)) { + final Event operation = (Event)context; + operation.complete(address, port, bytes); + queueEvent(operation); + } + } + + ///// + + private void queueEvent(Event operation) { + final boolean success = queue.offer(operation); + + if(success) { + successCounter.incrementAndGet(); + } else { + final int failCount = failCounter.incrementAndGet(); + final long now = System.currentTimeMillis(); + if(now - lastFailReport > queueOverflowReportInterval) { + final int successCount = successCounter.get(); + lastFailReport = now; + System.out.println(String.format("DIAG: IO tracing queue full (%d) for %d events out of %d", + eventQueueSize, failCount, failCount+successCount)); + } + } + } + + private void startConsumer() { + consumerThread.setDaemon(true); + consumerThread.start(); + } + + private void stopConsumer() { + consumer.shutdown(); + consumerThread.interrupt(); + } + + private class EventConsumer implements Runnable { + private boolean keepRunning = true; + + public void shutdown() { + if( ! keepRunning) throw new IllegalStateException("consumer was not running"); + keepRunning = false; + } + + @Override + public void run() { + while(keepRunning) { + try { + final Event event = queue.take(); + notifyListeners(event); + } catch(InterruptedException ignore) { } + } + } + } + + ///// + + public enum RW { READ, WRITE }; + + public static class Event { + private final Stack stack; + private final RW rw; + private final long startTime; + + private InetAddress address; + private int port; + private long bytes; + private long elapsed; + + public Event(RW rw) { + this(rw, System.currentTimeMillis(), new Stack()); + } + + public Event(RW rw, long startTime, Stack stack) { + this.rw = rw; + this.startTime = startTime; + this.stack = stack; + } + + private void complete(InetAddress address, int port, long bytes) { + this.elapsed = System.currentTimeMillis() - startTime; + this.address = address; + this.port = port; + this.bytes = bytes; + } + + public Stack getStack() { return stack; } + public RW getRw() { return rw; } + public long getReadElapsed() { return rw==RW.READ ? elapsed : 0; } + public long getWriteElapsed() { return rw==RW.WRITE ? elapsed : 0; } + public InetAddress getAddress() { return address; } + public int getPort() { return port; } + public long getReadBytes() { return rw==RW.READ ? bytes : 0; } + public long getWriteBytes() { return rw==RW.WRITE ? bytes : 0; } + + @Override + public String toString() { + return String.format("%s:%d %d bytes %s in %d ms\n%s", + address, port, bytes, rw.toString(), elapsed, stack.toString()); + } + } +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOStackReporter.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOStackReporter.java new file mode 100644 index 0000000..ec02c28 --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketIOStackReporter.java @@ -0,0 +1,87 @@ +package com.dell.gumshoe.socket; + +import com.dell.gumshoe.socket.SocketIOListener.DetailAccumulator; +import com.dell.gumshoe.socket.SocketIOMonitor.Event; +import com.dell.gumshoe.stack.Stack; +import com.dell.gumshoe.stack.StackFilter; + +import java.io.PrintStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** create hierarchical analysis of socket IO versus stack + * with total IO at each frame and + * link from frame to multiple next frames below + */ +public class SocketIOStackReporter extends TimerTask { + private final List listeners = new CopyOnWriteArrayList<>(); + private final SocketIOAccumulator source; + + public SocketIOStackReporter(SocketIOAccumulator source) { + this.source = source; + } + + @Override + public void run() { + final Map stats = new HashMap<>(source.getStats()); + for(Listener listener : listeners) { + try { + listener.socketIOStatsReported(stats); + } catch(RuntimeException e) { + e.printStackTrace(); + } + + } + source.reset(); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public static interface Listener { + public void socketIOStatsReported(Map stats); + } + + public static class StreamReporter implements Listener { + private final PrintStream target; + + public StreamReporter(PrintStream target) { + this.target = target; + } + + @Override + public void socketIOStatsReported(Map stats) { + final StringBuilder out = new StringBuilder(); + addStartTag(out); + addReport(out, stats); + addEndTag(out); + + target.print(out.toString()); + target.flush(); + } + + protected void addStartTag(StringBuilder out) { + final SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + final String now = fmt.format(new Date()); + out.append("\n"); + } + + private void addReport(StringBuilder out, Map stats) { + for(Map.Entry entry : stats.entrySet()) { + out.append(entry.getValue().get().toString()).append("\n").append(entry.getKey()); + } + } + + protected void addEndTag(StringBuilder out) { + out.append("\n"); + } + } +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketMatcher.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketMatcher.java new file mode 100644 index 0000000..5b9d8a2 --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketMatcher.java @@ -0,0 +1,5 @@ +package com.dell.gumshoe.socket; + +public interface SocketMatcher { + boolean matches(byte[] addressBytes, int port); +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketMatcherSeries.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketMatcherSeries.java new file mode 100644 index 0000000..a5c82be --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SocketMatcherSeries.java @@ -0,0 +1,29 @@ +package com.dell.gumshoe.socket; + +public class SocketMatcherSeries implements SocketMatcher { + private final SocketMatcher[] acceptList; + private final SocketMatcher[] rejectList; + + public SocketMatcherSeries(SocketMatcher[] acceptList, SocketMatcher[] rejectList) { + this.acceptList = acceptList; + this.rejectList = rejectList; + } + + @Override + public boolean matches(byte[] addressBytes, int port) { + for(SocketMatcher accept : acceptList) { + if(accept.matches(addressBytes, port)) { + return true; + } + } + + for(SocketMatcher reject : rejectList) { + if(reject.matches(addressBytes, port)) { + return false; + } + } + + return true; + } + +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SubnetAddress.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SubnetAddress.java new file mode 100644 index 0000000..c0ce8fd --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/socket/SubnetAddress.java @@ -0,0 +1,73 @@ +package com.dell.gumshoe.socket; + +import java.text.MessageFormat; +import java.text.ParseException; + +public class SubnetAddress implements SocketMatcher { + public static final SocketMatcher ANY = new MatchAny(); + + private final byte[] targetAddress = new byte[4]; + private final byte[] mask = new byte[4]; + private final Integer targetPort; + + public SubnetAddress(String description) throws ParseException { + final MessageFormat format = new MessageFormat("{0,number,integer}.{1,number,integer}.{2,number,integer}.{3,number,integer}/{4,number,integer}:{5}"); + final Object[] fields = format.parse(description); + for(int i=0;i<4;i++) { + final Number addressByte = (Number)fields[i]; + if(addressByte.longValue()<0 || addressByte.longValue()>255) { + throw new ParseException("invalid IP/mask:port value: " + description,i); + } + targetAddress[i] = addressByte.byteValue(); + } + + final int bits = ((Number)fields[4]).byteValue(); + long bitvalue = 0xFFFFFFFF ^ ((1L<<(32-bits)) - 1); + mask[0] = (byte) (bitvalue>>24 & 0xFF); + mask[1] = (byte) (bitvalue>>16 & 0xFF); + mask[2] = (byte) (bitvalue>>8 & 0xFF); + mask[3] = (byte) (bitvalue & 0xFF); + + final String portField = (String)fields[5]; + if(portField.equals("*")) { + targetPort = null; + } else { + targetPort = Integer.parseInt(portField); + if(targetPort<0 || targetPort>65535) { + throw new ParseException("invalid port: " + description, 0); + } + } + } + + @Override + public boolean matches(byte[] addressBytes, int port) { + for(int i=0;i<4;i++) { + if(((addressBytes[i]^targetAddress[i]) & mask[i]) > 0) { + return false; + } + } + + return targetPort==null || targetPort.equals(port); + } + + public String toString() { + final StringBuilder out = new StringBuilder(); + for(int i=0;i<4;i++) { + if(i>0) out.append("."); + out.append(0xFF & (int)targetAddress[i]); + } + out.append("/"); + for(int i=0;i<4;i++) { + if(i>0) out.append("."); + out.append(0xFF & (int)mask[i]); + } + out.append(":"); + if(targetPort==null) out.append("*"); + else out.append(targetPort); + return out.toString(); + } + + private static class MatchAny implements SocketMatcher { + public boolean matches(byte[] unused, int port) { return true; } + } +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/stack/Filter.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/stack/Filter.java new file mode 100644 index 0000000..eec8343 --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/stack/Filter.java @@ -0,0 +1,106 @@ +package com.dell.gumshoe.stack; + +import java.util.ArrayList; +import java.util.List; + +/** create custom filters + * + */ +public class Filter implements StackFilter { + public static StackFilter NONE = Filter.builder().build(); + + public static Builder builder() { return new Builder(); } + + private final boolean withOriginal; + private final List filters; + + public Filter(boolean withOriginal, List filters) { + this.withOriginal = withOriginal; + this.filters = filters; + } + + @Override + public int filter(StackTraceElement[] mutable, int origSize) { + final StackTraceElement[] orig = withOriginal ? mutable.clone() : null; + int size = origSize; + for(StackFilter filter : filters) { + size = filter.filter(mutable, size); + if(size==0) break; + } + if(size==0 && withOriginal) { + System.arraycopy(orig, 0, mutable, 0, origSize); + return origSize; + } + return size; + } + + public static class Builder { + private boolean withOriginal = false; + private final List filters = new ArrayList<>(); + + private Builder() { } + + public Builder withFilter(StackFilter filter) { + filters.add(filter); + return this; + } + + public Builder withOriginalIfBlank() { + withOriginal = true; + return this; + } + + public Builder withExcludePlatform() { + withExcludeClasses("sunw."); + withExcludeClasses("sun."); + withExcludeClasses("java."); + withExcludeClasses("javax."); + withExcludeClasses("com.dell.gumshoe."); + return this; + } + + public Builder withExcludeClasses(final String startingWith) { + filters.add(new FrameMatcher() { + @Override + public boolean matches(StackTraceElement frame) { + return ! frame.getClassName().startsWith(startingWith); + } }); + return this; + } + + public Builder withOnlyClasses(final String startingWith) { + filters.add(new FrameMatcher() { + @Override + public boolean matches(StackTraceElement frame) { + return frame.getClassName().startsWith(startingWith); + } + }); + return this; + } + + public Builder withEndsOnly(final int topCount, final int bottomCount) { + if(topCount==0 && bottomCount==0) { return this; } + + filters.add(new StackFilter() { + @Override + public int filter(StackTraceElement[] buffer, int size) { + final int maxSize = topCount + bottomCount; + final int frameCount = Math.min(size, maxSize); + if(frameCount 3 count 2 + // out: [ 0 1 2 7 8 ] + System.arraycopy(buffer, size-bottomCount, buffer, topCount, bottomCount); + } + return frameCount; + } + }); + return this; + } + + public StackFilter build() { + return new Filter(withOriginal, new ArrayList<>(filters)); + } + } +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/stack/FrameMatcher.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/stack/FrameMatcher.java new file mode 100644 index 0000000..5c56b64 --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/stack/FrameMatcher.java @@ -0,0 +1,21 @@ +package com.dell.gumshoe.stack; + + +public abstract class FrameMatcher implements StackFilter { + public abstract boolean matches(StackTraceElement frame); + + @Override + public int filter(StackTraceElement[] mutable, int size) { + int newSize = 0; + for(int index=0;index0) { + System.arraycopy(buffer, 0, filteredStack, 0, size); + } + return new Stack(filteredStack); + } + + public StackTraceElement[] getFrames() { + return frames; + } + + @Override + public int hashCode() { + return Arrays.hashCode(frames); + } + + @Override + public boolean equals(Object obj) { + if( ! (obj instanceof Stack)) return false; + Stack that = (Stack)obj; + return Arrays.equals(this.frames, that.frames); + } + + @Override + public String toString() { + final StringBuilder out = new StringBuilder(); + for(StackTraceElement frame : frames) { + out.append(" at ").append(frame).append("\n"); + } + return out.toString(); + } + + public boolean isEmpty() { + return frames.length==0; + } + +} diff --git a/gumshoe-probes/src/main/java/com/dell/gumshoe/stack/StackFilter.java b/gumshoe-probes/src/main/java/com/dell/gumshoe/stack/StackFilter.java new file mode 100644 index 0000000..bc53653 --- /dev/null +++ b/gumshoe-probes/src/main/java/com/dell/gumshoe/stack/StackFilter.java @@ -0,0 +1,6 @@ +package com.dell.gumshoe.stack; + + +public interface StackFilter { + public int filter(StackTraceElement[] buffer, int size); +} \ No newline at end of file diff --git a/gumshoe-probes/src/test/java/com/dell/gumshoe/TestIOAccumulator.java b/gumshoe-probes/src/test/java/com/dell/gumshoe/TestIOAccumulator.java new file mode 100644 index 0000000..122fd9c --- /dev/null +++ b/gumshoe-probes/src/test/java/com/dell/gumshoe/TestIOAccumulator.java @@ -0,0 +1,71 @@ +package com.dell.gumshoe; + +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertEquals; + +import com.dell.gumshoe.socket.SocketIOAccumulator; +import com.dell.gumshoe.socket.SocketIOListener.DetailAccumulator; +import com.dell.gumshoe.socket.SocketIOMonitor; +import com.dell.gumshoe.socket.SocketIOStackReporter; +import com.dell.gumshoe.socket.SocketMatcher; +import com.dell.gumshoe.socket.SocketMatcherSeries; +import com.dell.gumshoe.socket.SubnetAddress; +import com.dell.gumshoe.stack.Filter; +import com.dell.gumshoe.stack.Stack; +import com.dell.gumshoe.stack.StackFilter; + +import org.junit.Ignore; +import org.junit.Test; + +import java.net.InetAddress; +import java.util.Map; + +public class TestIOAccumulator { + int COUNT = 15; + + /** NOTE: can only run this test with project jarfile added to JVM bootclasspath */ + @Ignore + @Test + public void testCollectsStatistics() throws Exception { + + SocketMatcher[] accept = { new SubnetAddress("127.0.0.1/32:1234") }; + SocketMatcher[] reject = { new SubnetAddress("127.0.0.1/32:*") }; + final SocketMatcherSeries socketFilter = new SocketMatcherSeries(accept, reject); + + SocketIOMonitor ioMonitor = new SocketIOMonitor(socketFilter); + StackFilter filter = Filter.builder().withEndsOnly(1, 0).build(); + SocketIOAccumulator ioAccumulator = new SocketIOAccumulator(filter); + + SocketIOStackReporter reporter = new SocketIOStackReporter(ioAccumulator); + ioMonitor.addListener(ioAccumulator); + ioMonitor.initializeProbe(); + + InetAddress addr = InetAddress.getByName("www.yahoo.com"); + InetAddress self = InetAddress.getLocalHost(); + + for(int j=0;j<3;j++) { + for(int i=0;i stats = ioAccumulator.getStats(); + assertEquals(3, stats.size()); + ioAccumulator.reset(); + } + } + +} diff --git a/gumshoe-probes/src/test/java/com/dell/gumshoe/TestSocketDiag.java b/gumshoe-probes/src/test/java/com/dell/gumshoe/TestSocketDiag.java new file mode 100644 index 0000000..b35645f --- /dev/null +++ b/gumshoe-probes/src/test/java/com/dell/gumshoe/TestSocketDiag.java @@ -0,0 +1,138 @@ +package com.dell.gumshoe; + +import com.dell.gumshoe.socket.SocketCloseMonitor; +import com.dell.gumshoe.socket.SocketCloseMonitor.SocketImplDecorator; +import com.dell.gumshoe.socket.SocketIOMonitor; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Date; +import java.util.List; + +import junit.framework.TestCase; + +/** create simple TCP client and server to test out the socket diag tools */ +public class TestSocketDiag extends TestCase { + static String testMessage = "now is the time for all good men to come to the aid of their king"; + static byte[] testData = testMessage.getBytes(); + + SocketCloseMonitor target; + + @Override + public void setUp() throws Exception { + target = new SocketCloseMonitor(); + Socket.setSocketImplFactory(target); + final SocketIOMonitor ioMonitor = new SocketIOMonitor(); +// ioMonitor.addListener(new SocketIOMonitor.PrintEvents()); +// IoTrace.setDelegate(ioMonitor); + } + + public void testWithClientServer() throws Exception { + // client just closes socket right away, but server will leave it open for 1sec + EchoServer server = startServer(); + executeClient(server); + + // see if we can find the open socket + List sockets = target.findOpenedBefore(new Date()); + assertEquals(1, sockets.size()); + + // look in stack and see its from the server + String stackLines = sockets.get(0).toString(); + assertTrue(stackLines.contains(EchoServer.class.getSimpleName())); + + // TODO: use a better sync approach than sleep + Thread.sleep(1500); + + // now server socket has closed, should be none left open + sockets = target.findOpenedBefore(new Date()); + assertEquals(0, sockets.size()); + } + + ///// + + private EchoServer startServer() { + EchoServer server = new EchoServer(); + new Thread(server).start(); + return server; + } + + private void executeClient(EchoServer server) throws InterruptedException, UnknownHostException, IOException { + while(server.port==0) { + Thread.sleep(100); + } + System.out.println("using port " + server.port); + Socket socket = new Socket("127.0.0.1", server.port); + + InputStream in = socket.getInputStream(); + OutputStream out = socket.getOutputStream(); + + System.out.println("client writing"); + out.write(testData); + out.flush(); + socket.shutdownOutput(); + System.out.println("client wrote"); + + StringBuilder buffer = new StringBuilder(); + int inByte; + while((inByte=in.read())!=-1) { + buffer.append((char)inByte); + } + + String received = buffer.toString(); + System.out.println("client read: " + received); + System.out.println("client closing"); + socket.close(); + } + + private static class EchoServer implements Runnable { + private int port; + private String received; + private Exception thrown; + @Override + public void run() { + try { + runWithExceptions(); + } catch(Exception e) { + e.printStackTrace(); + thrown = e; + } + + } + + private void runWithExceptions() throws Exception { + ServerSocket ss = new ServerSocket(0); + port = ss.getLocalPort(); + Socket socket = ss.accept(); + + InputStream in = socket.getInputStream(); + OutputStream out = socket.getOutputStream(); + + System.out.println("server reading, avail = " + in.available()); + StringBuilder buffer = new StringBuilder(); + int inByte; + while((inByte=in.read())==-1) { + Thread.sleep(100); + } + buffer.append((char)inByte); + while((inByte=in.read())!=-1) { + buffer.append((char)inByte); + } + received = buffer.toString(); + System.out.println("server read: " + received); + + out.write(received.getBytes()); + socket.shutdownOutput(); + System.out.println("server wrote"); + + Thread.sleep(1000); + socket.close(); + ss.close(); + + System.out.println("server closed"); + } + } +} diff --git a/gumshoe-probes/src/test/java/com/dell/gumshoe/socket/TestSubnetMatcher.java b/gumshoe-probes/src/test/java/com/dell/gumshoe/socket/TestSubnetMatcher.java new file mode 100644 index 0000000..093aa3e --- /dev/null +++ b/gumshoe-probes/src/test/java/com/dell/gumshoe/socket/TestSubnetMatcher.java @@ -0,0 +1,33 @@ +package com.dell.gumshoe.socket; + +import java.text.ParseException; + +import junit.framework.TestCase; + +public class TestSubnetMatcher extends TestCase { + public void testParseValid() throws ParseException { + String[] valid = { + "127.0.0.1/24:1234", + "127.0.0.1/20:1234", + "127.0.0.1/8:1", + "127.0.0.1/32:*", + "255.255.255.0/0:0", + "0.0.0.255/32:65535" + }; + + byte b = (byte)255; + int i=(int)b; + System.out.println("255: " + i); + for(String desc : valid) { + System.out.println(desc + ": " + new SubnetAddress(desc).toString()); + } + } + + public void testMatch() throws Exception { + assertTrue(new SubnetAddress("123.45.67.89/24:1234").matches(new byte[] { 123, 45, 67, 22 }, 1234)); + assertFalse(new SubnetAddress("123.45.67.89/24:1234").matches(new byte[] { 123, 45, 77, 22 }, 1234)); + assertFalse(new SubnetAddress("123.45.67.89/24:1234").matches(new byte[] { 123, 45, 67, 22 }, 1235)); + assertTrue(new SubnetAddress("123.45.67.89/24:*").matches(new byte[] { 123, 45, 67, 22 }, 1235)); + assertTrue(new SubnetAddress("123.45.67.89/20:1234").matches(new byte[] { 123, 45, 77, 22 }, 1234)); + } +} diff --git a/gumshoe-probes/src/test/java/com/dell/gumshoe/stack/TestStackFilter.java b/gumshoe-probes/src/test/java/com/dell/gumshoe/stack/TestStackFilter.java new file mode 100644 index 0000000..c2b3e60 --- /dev/null +++ b/gumshoe-probes/src/test/java/com/dell/gumshoe/stack/TestStackFilter.java @@ -0,0 +1,122 @@ +package com.dell.gumshoe.stack; + +import junit.framework.TestCase; + +public class TestStackFilter extends TestCase { + private StackFilter EXCLUDE_BUILTIN = Filter.builder().withExcludePlatform().build(); + + public String testCase1 = "com.dell.gumshoe.socket.Stack.(Stack.java:11)\n" + + "com.dell.gumshoe.socket.SocketIOMonitor$Event.(SocketIOMonitor.java:91)\n" + + "com.dell.gumshoe.socket.SocketIOMonitor.socketWriteBegin(SocketIOMonitor.java:64)\n" + + "sun.misc.IoTrace.socketWriteBegin(IoTrace.java:56)\n" + + "java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:109)\n" + + "java.net.SocketOutputStream.write(SocketOutputStream.java:159)\n" + + "java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)\n" + + "java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)\n" + + "com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3832)\n" + + "com.mysql.jdbc.MysqlIO.quit(MysqlIO.java:2196)\n" + + "com.mysql.jdbc.ConnectionImpl.realClose(ConnectionImpl.java:4446)\n" + + "com.mysql.jdbc.ConnectionImpl.close(ConnectionImpl.java:1594)\n" + + "org.apache.tomcat.jdbc.pool.PooledConnection.disconnect(PooledConnection.java:331)\n" + + "org.apache.tomcat.jdbc.pool.PooledConnection.release(PooledConnection.java:490)\n" + + "org.apache.tomcat.jdbc.pool.ConnectionPool.release(ConnectionPool.java:581)\n" + + "org.apache.tomcat.jdbc.pool.ConnectionPool.checkIdle(ConnectionPool.java:1002)\n" + + "org.apache.tomcat.jdbc.pool.ConnectionPool.checkIdle(ConnectionPool.java:983)\n" + + "org.apache.tomcat.jdbc.pool.ConnectionPool$PoolCleaner.run(ConnectionPool.java:1350)\n" + + "java.util.TimerThread.mainLoop(Timer.java:555)\n" + + "java.util.TimerThread.run(Timer.java:505)"; + + public String testCase2 = "com.dell.gumshoe.stack.Stack.(Stack.java:9)\n" + + "com.dell.gumshoe.socket.SocketIOMonitor$Event.(SocketIOMonitor.java:92)\n" + + "com.dell.gumshoe.socket.SocketIOMonitor.socketReadBegin(SocketIOMonitor.java:60)\n" + + "sun.misc.IoTrace.socketReadBegin(IoTrace.java:27)\n" + + "java.net.SocketInputStream.read(SocketInputStream.java:148)\n" + + "java.net.SocketInputStream.read(SocketInputStream.java:122)\n" + + "com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:114)\n" + + "com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:161)\n" + + "com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:189)\n" + + "com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3036)\n" + + "com.mysql.jdbc.MysqlIO.readPacket(MysqlIO.java:592)\n" + + "com.mysql.jdbc.MysqlIO.doHandshake(MysqlIO.java:1078)\n" + + "com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2412)\n" + + "com.mysql.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:2253)\n" + + "com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2235)\n" + + "com.mysql.jdbc.ConnectionImpl.(ConnectionImpl.java:813)\n" + + "com.mysql.jdbc.JDBC4Connection.(JDBC4Connection.java:47)\n" + + "sun.reflect.GeneratedConstructorAccessor7.newInstance(Unknown Source)\n" + + "sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)\n" + + "java.lang.reflect.Constructor.newInstance(Constructor.java:526)\n" + + "com.mysql.jdbc.Util.handleNewInstance(Util.java:411)\n" + + "com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:399)\n" + + "com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:334)\n" + + "org.apache.tomcat.jdbc.pool.PooledConnection.connectUsingDriver(PooledConnection.java:278)\n" + + "org.apache.tomcat.jdbc.pool.PooledConnection.connect(PooledConnection.java:182)\n" + + "org.apache.tomcat.jdbc.pool.ConnectionPool.createConnection(ConnectionPool.java:702)\n" + + "org.apache.tomcat.jdbc.pool.ConnectionPool.borrowConnection(ConnectionPool.java:634)\n" + + "org.apache.tomcat.jdbc.pool.ConnectionPool.getConnection(ConnectionPool.java:188)\n" + + "org.apache.tomcat.jdbc.pool.DataSourceProxy.getConnection(DataSourceProxy.java:128)\n" + + "org.dell.persist.Transaction.open(Transaction.java:573)\n" + + "org.dell.persist.Transaction.execute(Transaction.java:423)\n" + + "org.dell.persist.RelationalCache.load(RelationalCache.java:494)\n" + + "org.dell.persist.RelationalCache.find(RelationalCache.java:306)\n" + + "com.dell.provisioning.cloud.CloudFactory.getServers(CloudFactory.java:2266)\n" + + "com.dell.cloud.analytics2.ServerAnalyticsWorker.checkCloud(ServerAnalyticsWorker.java:75)\n" + + "com.dell.monitor.AccountResourceMonitor._run(AccountResourceMonitor.java:294)\n" + + "com.dell.monitor.AccountResourceMonitor.run(AccountResourceMonitor.java:220)\n" + + "java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)\n" + + "java.util.concurrent.FutureTask.run(FutureTask.java:262)\n" + + "java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)\n" + + "java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)\n" + + "java.lang.Thread.run(Thread.java:745)"; + + + private Stack parse(String value) { + String[] lines = value.split("\n"); + StackTraceElement[] frames = new StackTraceElement[lines.length]; + for(int i=0;i + 4.0.0 + + gumshoe-tools + + tools for analyzing gumshoe probe results + + 4.12 + + + + + junit + junit + ${junit.version} + test + + + com.dell + gumshoe-probes + 0.1.0-SNAPSHOT + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Test*.* + + + + + + + com.dell + 0.1.0-SNAPSHOT + Gumshoe Tools + diff --git a/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/FilterEditor.java b/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/FilterEditor.java new file mode 100644 index 0000000..a745e37 --- /dev/null +++ b/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/FilterEditor.java @@ -0,0 +1,219 @@ +package com.dell.gumshoe.tools; + +import com.dell.gumshoe.Probe; +import com.dell.gumshoe.stack.Filter; +import com.dell.gumshoe.stack.Filter.Builder; +import com.dell.gumshoe.stack.StackFilter; + +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.border.TitledBorder; +import javax.swing.event.CaretEvent; +import javax.swing.event.CaretListener; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Container; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class FilterEditor extends JPanel { + private final JRadioButton dropZero = new JRadioButton("drop the stack"); + private final JRadioButton revertZero = new JRadioButton("keep all frames"); + private final JRadioButton bucketZero = new JRadioButton("group as \"other\"", true); + + private final JTextField topCount = new JTextField(); + private final JTextField bottomCount = new JTextField(); + private final JLabel topLabel1 = new JLabel("Top"); + private final JLabel topLabel2 = new JLabel("frames"); + private final JLabel bothLabel = new JLabel(" and "); + private final JLabel bottomLabel1 = new JLabel("bottom"); + private final JLabel bottomLabel2 = new JLabel("frames"); + + private final JCheckBox dropJVM = new JCheckBox("drop jdk and gumshoe frames", true); + + private final JTextArea accept = new JTextArea(); + private final JTextArea reject = new JTextArea(); + + private final JButton localButton = new JButton("Apply to display"); + private final JButton probeButton = new JButton("Apply to probe"); + private final JButton generate = new JButton("Generate cmdline options"); + + private Probe probe; + private FlameGraph graph; + + public FilterEditor() { + ButtonGroup zeroGroup = new ButtonGroup(); +// zeroGroup.add(dropZero); + zeroGroup.add(revertZero); + zeroGroup.add(bucketZero); + + final JPanel zeroPanel = new JPanel(); + zeroPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), "When stack filter matches no frames:", TitledBorder.LEFT, TitledBorder.TOP)); +// zeroPanel.add(dropZero); + zeroPanel.add(revertZero); + zeroPanel.add(bucketZero); + + topCount.setColumns(3); + bottomCount.setColumns(3); + final JPanel countPanel = new JPanel(); + countPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), "Retain only:", TitledBorder.LEFT, TitledBorder.TOP)); + countPanel.add(topLabel1); + countPanel.add(topCount); + countPanel.add(topLabel2); + countPanel.add(bothLabel); + countPanel.add(bottomLabel1); + countPanel.add(bottomCount); + countPanel.add(bottomLabel2); + + // lighten/darken words to help make filter intent more clear + topLabel1.setEnabled(false); + topLabel2.setEnabled(false); + bothLabel.setEnabled(false); + bottomLabel1.setEnabled(false); + bottomLabel2.setEnabled(false); + topCount.addCaretListener(new CaretListener() { + @Override + public void caretUpdate(CaretEvent e) { + final boolean topPositive = isPositive(topCount); + topLabel1.setEnabled(topPositive); + topLabel2.setEnabled(topPositive); + bothLabel.setEnabled(topPositive && isPositive(bottomCount)); + } }); + bottomCount.addCaretListener(new CaretListener() { + @Override + public void caretUpdate(CaretEvent e) { + final boolean bottomPositive = isPositive(bottomCount); + bottomLabel1.setEnabled(bottomPositive); + bottomLabel2.setEnabled(bottomPositive); + bothLabel.setEnabled(bottomPositive && isPositive(topCount)); + } }); + + final JPanel acceptPanel = new JPanel(); + acceptPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), "Include classes:", TitledBorder.LEFT, TitledBorder.TOP)); + acceptPanel.setLayout(new BorderLayout()); + acceptPanel.add(accept, BorderLayout.CENTER); + + final JPanel rejectPanel = new JPanel(); + rejectPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), "Exclude classes:", TitledBorder.LEFT, TitledBorder.TOP)); + rejectPanel.setLayout(new BorderLayout()); + rejectPanel.add(reject, BorderLayout.CENTER); + + final JPanel jvmPanel = new JPanel(); + jvmPanel.add(dropJVM); + + localButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateGraph(); + } + }); + probeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateProbe(); + } + }); + probeButton.setEnabled(false); + final JPanel buttonPanel = new JPanel(); + buttonPanel.add(localButton); + buttonPanel.add(probeButton); +// buttonPanel.add(generate); TODO: popup window showing cmdline properties to make this filter the default + + addNorth(this, zeroPanel, countPanel, acceptPanel, rejectPanel, jvmPanel, buttonPanel); + } + + public void setProbe(Probe probe) { + this.probe = probe; + if(probe!=null) { + probeButton.setEnabled(true); + } + } + + public void setGraph(FlameGraph graph) { + this.graph = graph; + } + + private boolean isPositive(JTextField field) { + final String textValue = field.getText().trim(); + boolean validPositiveNumber = false; + try { + int value = Integer.parseInt(textValue); + if(value>0) { + validPositiveNumber = true; + } + } catch(Exception ignore) { } + return validPositiveNumber; + } + + private int getCount(JTextField field) { + try { + final int value = Integer.parseInt(field.getText().trim()); + if(value>0) { + return value; + } + } catch(Exception ignore) { } + return 0; + } + private void addNorth(Container container, JComponent... components) { + Container c = container; + for(JComponent component : components) { + c.setLayout(new BorderLayout()); + c.add(component, BorderLayout.NORTH); + final JPanel innerContainer = new JPanel(); + c.add(innerContainer, BorderLayout.CENTER); + c = innerContainer; + } + } + + public StackFilter getFilter() { + final Builder builder = Filter.builder(); + builder.withEndsOnly(getCount(topCount), getCount(bottomCount)); + if(dropJVM.isSelected()) { builder.withExcludePlatform(); } + if(revertZero.isSelected()) { builder.withOriginalIfBlank(); } + for(String acceptLine : getValues(accept)) { builder.withOnlyClasses(acceptLine); } + for(String rejectLine : getValues(reject)) { builder.withExcludeClasses(rejectLine); } + return builder.build(); + } + + private List getValues(JTextArea field) { + final String rawValue = field.getText(); + final String[] lines = rawValue.split("\n"); + final List out = new ArrayList<>(lines.length); + for(String line : lines) { + final String clean = line.trim(); + if(clean.length()>0) { + out.add(clean); + } + } + return out; + } + + private void updateProbe() { + if(probe!=null) { + final StackFilter filter = getFilter(); + probe.getIOAccumulator().setFilter(filter); + } + } + + private void updateGraph() { + if(graph!=null) { + final StackFilter filter = getFilter(); + graph.setFilter(filter); + } + } +} diff --git a/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/FlameGraph.java b/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/FlameGraph.java new file mode 100644 index 0000000..fc259d4 --- /dev/null +++ b/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/FlameGraph.java @@ -0,0 +1,481 @@ +package com.dell.gumshoe.tools; + +import com.dell.gumshoe.socket.SocketIOListener.DetailAccumulator; +import com.dell.gumshoe.stack.Filter; +import com.dell.gumshoe.stack.Stack; +import com.dell.gumshoe.stack.StackFilter; + +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.ToolTipManager; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** visualization of IO statistics per call stack + * + * similar to a flame graph, but width of each box represents the amount of IO, + * depth shows call stack + * + */ +public class FlameGraph extends JPanel { + private Options options; + private Map values; + private TreeNode model; + private transient List boxes; + private OptionEditor optionEditor; + private StackFilter filter; + private JTextArea detailField; + private StackTraceElement selectedFrame; + + private static void debug(String msg) { + System.out.println(msg); + } + + public FlameGraph() { + ToolTipManager.sharedInstance().registerComponent(this); + addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent e) { + updateDetails(e); + } + }); + } + + private static class TreeNode { + private long value; + private StackTraceElement frame; + private DetailAccumulator detail = new DetailAccumulator(); + private Map divisions = new LinkedHashMap<>(); + + private TreeNode() { } + + public TreeNode(Map values, Options options, StackFilter filter) { + for(Map.Entry entry : values.entrySet()) { + final Stack origStack = entry.getKey(); + final DetailAccumulator details = entry.getValue(); + final Stack displayStack = filter==null ? origStack : origStack.applyFilter(filter); + final List frames = Arrays.asList(displayStack.getFrames()); + if(options.byCalled) { Collections.reverse(frames); } + + TreeNode node = this; + node.detail.add(details); + final long nodeValue = getValue(details, options); + node.value += nodeValue; + for(StackTraceElement frame : frames) { + TreeNode frameNode = node.divisions.get(frame); + if(frameNode==null) { + frameNode = new TreeNode(); + frameNode.value = nodeValue; + frameNode.frame = frame; + node.divisions.put(frame, frameNode); + } else { + frameNode.value += nodeValue; + } + node = frameNode; + node.detail.add(details); + } + } + } + + private long getValue(DetailAccumulator details, Options options) { + final WidthScale width = options.scale; + switch(width) { + case EQUAL: return 5; + case VALUE: return getStatValue(details, options); + case LOG_VALUE: + default: return (long)(100. * Math.log1p(getStatValue(details, options))); + } + } + + private long getStatValue(DetailAccumulator details, Options options) { + final IOStat stat = options.direction; + final IOUnit value = options.value; + switch(stat) { + case READ: return getReadValue(details, value); + case WRITE: return getWriteValue(details, value); + case READ_PLUS_WRITE: + default: return getReadValue(details, value) + getWriteValue(details, value); + } + } + + private long getReadValue(DetailAccumulator details, IOUnit unit) { + switch(unit) { + case OPS: return details.readCount.get(); + case BYTES: return details.readBytes.get(); + case TIME: + default: return details.readTime.get(); + } + } + + private long getWriteValue(DetailAccumulator details, IOUnit unit) { + switch(unit) { + case OPS: return details.writeCount.get(); + case BYTES: return details.writeBytes.get(); + case TIME: + default: return details.writeTime.get(); + } + } + + public int getDepth(Options options) { + final long value = getValue(detail, options); + if(value==0) { return 0; } + int max = -1; + for(TreeNode div : divisions.values()) { + max = Math.max(div.getDepth(options), max); + } + return max + 1; + } + + private List getDivisions(final TreeNode model, Order order) { + final List keys = new ArrayList<>(model.divisions.keySet()); + if(order==Order.BY_VALUE) { + Collections.sort(keys, new Comparator() { + @Override + public int compare(StackTraceElement key1, StackTraceElement key2) { + return -Long.valueOf(model.divisions.get(key1).value).compareTo(Long.valueOf(model.divisions.get(key2).value)); + } + }); + } + return keys; + } + + public List createBoxes(Options options) { + final List out = new ArrayList(); + final int depth = getDepth(options); + Map priorRowModels = Collections.singletonMap(this, 0L); + for(int row = 0;row thisRowModels = new LinkedHashMap<>(); + for(Map.Entry parentEntry : priorRowModels.entrySet()) { + final long parentPosition = parentEntry.getValue(); + final TreeNode parent = parentEntry.getKey(); + final List keys = getDivisions(parent, options.order); + debug("creating boxes: row " + row + " parent " + parent.value + " divs " + keys.size()); + + // first division left-aligned to parent position + long position = parentPosition; + for(StackTraceElement key : keys) { + final TreeNode divModel = parent.divisions.get(key); + debug("creating boxes: row " + row + " parent " + parent.value + " child " + key + " " + divModel.value); + if(divModel.value>0) { + out.add(new Box(row, position, divModel, parent)); + thisRowModels.put(divModel, position); + position += divModel.value; + } + } + } + priorRowModels = thisRowModels; + } + debug("create " + out.size() + " boxes, depth " + depth + " from\n" + this); + return out; + } + + @Override + public String toString() { + final StringBuilder msg = new StringBuilder(); + addNode(msg, ""); + return msg.toString(); + } + + private void addNode(StringBuilder builder, String indent) { + for(Map.Entry entry : divisions.entrySet()) { + builder.append(indent) + .append(entry.getKey()) + .append(" ") + .append(entry.getValue().detail) + .append("\n"); + entry.getValue().addNode(builder, indent + " "); + } + } + } + + private static class Box { + private final int row; + private final long position; + private final TreeNode boxNode, parentNode; + + public Box(int row, long position, TreeNode boxNode, TreeNode parentNode) { + this.row = row; + this.position = position; + this.boxNode = boxNode; + this.parentNode = parentNode; + } + + public void draw(Graphics g, int displayHeight, int dispalyWidth, int rows, long total, Options o, StackTraceElement selected) { + final float rowHeight = displayHeight / (float)rows; + final float unitWidth = dispalyWidth / (float)total; + final int boxX = (int) (position * unitWidth); + final int boxWidth = (int) (boxNode.value * unitWidth); + final int boxY = (int) (rowHeight * (o.byCalled?(rows-row-1):row)); + final int boxHeight = (int)rowHeight; + final boolean isSelected = this.boxNode.frame==selected; + + g.setClip(boxX, boxY, boxWidth+1, boxHeight+1); + + final Color baseColor = getColor(total, o); + final Color color = isSelected ? baseColor.darker() : baseColor; + + g.setColor(color); + g.fillRect(boxX, boxY, boxWidth, boxHeight); + g.setColor(Color.BLACK); + g.drawRect(boxX, boxY, boxWidth, boxHeight); + g.drawString(getLabelText(), boxX+1, boxY+15); + g.setClip(null); + } + + public boolean contains(int displayHeight, int dispalyWidth, int rows, long total, Options o, int x, int y) { + final float rowHeight = displayHeight / (float)rows; + final float unitWidth = dispalyWidth / (float)total; + final int boxX = (int) (position * unitWidth); + final int boxWidth = (int) (boxNode.value * unitWidth); + final int boxY = (int) (rowHeight * (o.byCalled?(rows-row-1):row)); + final int boxHeight = (int)rowHeight; + + return x>=boxX && x=boxY && y50%--> 0, >33%-->1, >25%-->2, > (1/N)-->(N-2) + if(index<0) index=0; + if(index>=BOX_COLORS.length) index=BOX_COLORS.length-1; + return BOX_COLORS[index]; + } + + public String getLabelText() { + final String[] parts = boxNode.frame.getClassName().split("\\."); + final String className = parts[parts.length-1].replaceAll("\\$", "."); + return String.format("%s.%s:%d", className, boxNode.frame.getMethodName(), boxNode.frame.getLineNumber()); + } + + public String getToolTipText() { + return String.format("\n" + + "%s
\n" + + "read %d ops%s
\n" + + "read %d bytes%s
\n" + + "read time %d ms%s
\n" + + "write %d ops%s
\n" + + "write %d bytes%s
\n" + + "write time %d ms%s\n" + + "", + boxNode.frame, + boxNode.detail.readCount.get(), getPercent(boxNode.detail.readCount, parentNode.detail.readCount), + boxNode.detail.readBytes.get(), getPercent(boxNode.detail.readBytes, parentNode.detail.readBytes), + boxNode.detail.readTime.get(), getPercent(boxNode.detail.readTime, parentNode.detail.readTime), + boxNode.detail.writeCount.get(), getPercent(boxNode.detail.writeCount, parentNode.detail.writeCount), + boxNode.detail.writeBytes.get(), getPercent(boxNode.detail.writeBytes, parentNode.detail.writeBytes), + boxNode.detail.writeTime.get(), getPercent(boxNode.detail.writeTime, parentNode.detail.writeTime)); + } + + public String getDetailText(Options o) { + + return String.format("Frame: %s\n" + + "Read: %d operations%s, %d bytes%s, %d ms %s\n" + + "Write: %d operations%s, %d bytes%s, %d ms %s\n\n" + + (o.byCalled ? "Calls %d methods: %s" : "Called by %d methods: %s"), + boxNode.frame, + boxNode.detail.readCount.get(), getPercent(boxNode.detail.readCount, parentNode.detail.readCount), + boxNode.detail.readBytes.get(), getPercent(boxNode.detail.readBytes, parentNode.detail.readBytes), + boxNode.detail.readTime.get(), getPercent(boxNode.detail.readTime, parentNode.detail.readTime), + boxNode.detail.writeCount.get(), getPercent(boxNode.detail.writeCount, parentNode.detail.writeCount), + boxNode.detail.writeBytes.get(), getPercent(boxNode.detail.writeBytes, parentNode.detail.writeBytes), + boxNode.detail.writeTime.get(), getPercent(boxNode.detail.writeTime, parentNode.detail.writeTime), + boxNode.divisions.size(), getFrames(boxNode.divisions.keySet()) ); + + } + + private String getFrames(Set frames) { + final StringBuilder out = new StringBuilder(); + for(StackTraceElement frame : frames) { + out.append("\n ").append(frame); + } + return out.toString(); + } + + + private String getPercent(Number num, Number div) { + if(div.longValue()==0) { return ""; } + return " " + (100*num.longValue()/div.longValue()) + "%"; + } + } + + private static final Color[] BOX_COLORS = { Color.RED, Color.ORANGE, Color.YELLOW, Color.GRAY, Color.WHITE }; + + public enum IOStat { READ, WRITE, READ_PLUS_WRITE } + public enum IOUnit { OPS, BYTES, TIME } + public enum Order { BY_VALUE, BY_NAME } + public enum WidthScale { VALUE, LOG_VALUE, EQUAL } + + public static class Options { + private final boolean byCalled; + private final StackFilter stackFilter; + private final IOStat direction; + private final IOUnit value; + private final Order order; + private final WidthScale scale; + public Options() { + this(false, Filter.NONE, IOStat.READ, IOUnit.BYTES, Order.BY_NAME, WidthScale.VALUE); + } + + public Options(boolean byCalled, StackFilter stackFilter, IOStat direction, IOUnit value, Order order, WidthScale scale) { + this.byCalled = byCalled; + this.stackFilter = stackFilter; + this.direction = direction; + this.value = value; + this.order = order; + this.scale = scale; + } + } + + public void updateOptions(Options o) { + boxes = null; + model = null; + this.options = o; + debug("setting options"); + repaint(); + } + + public void updateModel(Map values) { + boxes = null; + model = null; + this.values = values; + debug("setting model " + values.size()); + repaint(); + } + + public void setFilter(StackFilter filter) { + this.filter = filter; + boxes = null; + model = null; + repaint(); + } + + private void update() { + model = new TreeNode(values, options, filter); + debug("updated model:\n" + model); + boxes = model.createBoxes(options); + updateDetails(); + } + + @Override + public void paintComponent(Graphics g) { + debug("painting"); + try { + final Dimension dim = getSize(); + g.setColor(getBackground()); + g.fillRect(0, 0, dim.width, dim.height); + + if(options==null || values==null) { return; } + if(boxes==null) { + update(); + } + + int rows = model.getDepth(options); + long total = model.value; + for(Box box : boxes) { + box.draw(g, dim.height-RULER_HEIGHT, dim.width, rows, total, options, selectedFrame); + } + paintRuler(g, dim.height, dim.width); + } catch(Exception e) { + e.printStackTrace(); + } + + } + + + private static final int RULER_HEIGHT = 25; + private static final int RULER_MAJOR_HEIGHT = 15; + private static final int RULER_MINOR_HEIGHT = 5; + private static final int RULER_MAJOR = 4; + private static final int RULER_MINOR = 20; + private void paintRuler(Graphics g, int height, int width) { + for(int i=1; i1) { + System.arraycopy(args, 1, newArgs, 0, args.length-1); + } + launchMain(mainClass, newArgs); + } + + private static void launchMain(String mainClassName, String[] args) throws Throwable { + final Class mainClass = Class.forName(mainClassName); + final Method mainMethod = mainClass.getDeclaredMethod("main", args.getClass()); + try { + mainMethod.invoke(mainClass, new Object[] { args }); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + private Gumshoe(Probe probe) { + final FlameGraph graph = new FlameGraph(); + final StatisticsRelay statsRelay = new StatisticsRelay(graph); + probe.getIOReporter().addListener(statsRelay); + + final JPanel detailPanel = new JPanel(); + detailPanel.setLayout(new BorderLayout()); + detailPanel.add(graph.getDetailField(), BorderLayout.CENTER); + + final JPanel statsPanel = new JPanel(); + statsPanel.add(statsRelay); + + final FilterEditor filterEditor = new FilterEditor(); + filterEditor.setGraph(graph); + filterEditor.setProbe(probe); + + final JTabbedPane settings = new JTabbedPane(); + settings.setBorder(BorderFactory.createEmptyBorder(10,5,5,5)); + settings.addTab("Collect -->", statsPanel); + settings.addTab("--> Filter -->", filterEditor); + settings.addTab("--> Display", graph.getOptionEditor()); + settings.addTab("Examine", detailPanel); + + final JPanel graphPanel = new JPanel(); + graphPanel.setLayout(new BorderLayout()); + graphPanel.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED)); + graphPanel.add(graph); + setLayout(new BorderLayout()); + add(graphPanel, BorderLayout.CENTER); + add(settings, BorderLayout.SOUTH); + } +} diff --git a/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/Initializer.java b/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/Initializer.java new file mode 100644 index 0000000..9991d05 --- /dev/null +++ b/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/Initializer.java @@ -0,0 +1,5 @@ +package com.dell.gumshoe.tools; + +public class Initializer { + +} diff --git a/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/OptionEditor.java b/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/OptionEditor.java new file mode 100644 index 0000000..b5629bc --- /dev/null +++ b/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/OptionEditor.java @@ -0,0 +1,126 @@ +package com.dell.gumshoe.tools; + +import com.dell.gumshoe.stack.Filter; +import com.dell.gumshoe.tools.FlameGraph.IOStat; +import com.dell.gumshoe.tools.FlameGraph.IOUnit; +import com.dell.gumshoe.tools.FlameGraph.Options; +import com.dell.gumshoe.tools.FlameGraph.Order; +import com.dell.gumshoe.tools.FlameGraph.WidthScale; + +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.border.TitledBorder; + +import java.awt.Color; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class OptionEditor extends JPanel { + final JRadioButton readStat = new JRadioButton("read", true); + final JRadioButton writeStat = new JRadioButton("write"); + final JRadioButton bothStat = new JRadioButton("read+write"); + final JRadioButton opsUnit = new JRadioButton("ops", true); + final JRadioButton bytesUnit = new JRadioButton("bytes"); + final JRadioButton timeUnit = new JRadioButton("time(ms)"); + final JRadioButton byCaller = new JRadioButton("show callers (root graph)", true); + final JRadioButton byCalled = new JRadioButton("show called methods (flame graph)"); + final JRadioButton valueWidth = new JRadioButton("statistic value", true); + final JRadioButton logWidth = new JRadioButton("log(value)"); + final JRadioButton equalWidth = new JRadioButton("equal width"); + final JCheckBox byValue = new JCheckBox("arrange by value"); + final JButton apply = new JButton("Apply"); + + public OptionEditor() { + final ButtonGroup statGroup = new ButtonGroup(); + statGroup.add(readStat); + statGroup.add(writeStat); + statGroup.add(bothStat); + + final JPanel statPanel = new JPanel(); + statPanel.setLayout(new GridLayout(1,3)); + statPanel.add(readStat); + statPanel.add(writeStat); + statPanel.add(bothStat); + statPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), "Operation", TitledBorder.LEFT, TitledBorder.TOP)); + + + final ButtonGroup unitGroup = new ButtonGroup(); + unitGroup.add(opsUnit); + unitGroup.add(bytesUnit); + unitGroup.add(timeUnit); + + final JPanel unitPanel = new JPanel(); + unitPanel.setLayout(new GridLayout(1,3)); + unitPanel.add(opsUnit); + unitPanel.add(bytesUnit); + unitPanel.add(timeUnit); + unitPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), "Measurement", TitledBorder.LEFT, TitledBorder.TOP)); + + final ButtonGroup widthGroup = new ButtonGroup(); + widthGroup.add(valueWidth); + widthGroup.add(logWidth); + widthGroup.add(equalWidth); + + final JPanel widthPanel = new JPanel(); + widthPanel.setLayout(new GridLayout(1,3)); + widthPanel.add(valueWidth); + widthPanel.add(logWidth); + widthPanel.add(equalWidth); + widthPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), "Cell width", TitledBorder.LEFT, TitledBorder.TOP)); + + final ButtonGroup directionGroup = new ButtonGroup(); + directionGroup.add(byCalled); + directionGroup.add(byCaller); + + final JPanel directionPanel = new JPanel(); + directionPanel.setLayout(new GridLayout(1,2)); + directionPanel.add(byCalled); + directionPanel.add(byCaller); + directionPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.black), "Direction", TitledBorder.LEFT, TitledBorder.TOP)); + + final JPanel otherPanel = new JPanel(); + otherPanel.setLayout(new FlowLayout()); + otherPanel.add(byValue); + otherPanel.add(apply); + + setLayout(new GridLayout(5,1)); + add(statPanel); + add(unitPanel); + add(directionPanel); + add(widthPanel); + add(otherPanel); + } + + public void addActionListener(ActionListener listener) { + apply.addActionListener(listener); + listener.actionPerformed(new ActionEvent(this, 0, "")); + } + + public Options getOptions() { + final IOStat stat; + if(readStat.isSelected()) stat = IOStat.READ; + else if(writeStat.isSelected()) stat = IOStat.WRITE; + else stat = IOStat.READ_PLUS_WRITE; + + final IOUnit unit; + if(opsUnit.isSelected()) unit = IOUnit.OPS; + else if(bytesUnit.isSelected()) unit = IOUnit.BYTES; + else unit = IOUnit.TIME; + + final Order order = byValue.isSelected() ? Order.BY_VALUE : Order.BY_NAME; + + final WidthScale width; + if(valueWidth.isSelected()) width = WidthScale.VALUE; + else if(logWidth.isSelected()) width = WidthScale.LOG_VALUE; + else width = WidthScale.EQUAL; + + final boolean isInverted = byCalled.isSelected(); + return new Options(isInverted, Filter.NONE, stat, unit, order, width); + } +} diff --git a/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/StatisticsRelay.java b/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/StatisticsRelay.java new file mode 100644 index 0000000..96c1f21 --- /dev/null +++ b/gumshoe-tools/src/main/java/com/dell/gumshoe/tools/StatisticsRelay.java @@ -0,0 +1,71 @@ +package com.dell.gumshoe.tools; + +import com.dell.gumshoe.socket.SocketIOListener.DetailAccumulator; +import com.dell.gumshoe.socket.SocketIOStackReporter.Listener; +import com.dell.gumshoe.stack.Stack; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; + +public class StatisticsRelay extends JPanel implements Listener { + private static final SimpleDateFormat hms = new SimpleDateFormat("HH:mm:ss"); + private Map lastStats; + private String receiveTime; + private final FlameGraph target; + private final JCheckBox sendLive = new JCheckBox("Display as received"); + private final JButton sendNow = new JButton("Update"); + private final JLabel received = new JLabel("No data received"); + private final JLabel display = new JLabel("No data displayed"); + + public StatisticsRelay(FlameGraph target) { + this.target = target; + sendNow.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + relayStats(); + } + }); + + setLayout(new GridLayout(4,1)); + add(received); + add(display); + add(sendLive); + add(sendNow); + } + + @Override + public void socketIOStatsReported(Map stats) { + debug("stats received " + stats.size()); + if(stats.size()>0) { + receiveTime = hms.format(new Date()); + received.setText("Received data " + receiveTime); + lastStats = stats; + if(sendLive.isSelected()) { + relayStats(); + } else { + sendNow.setEnabled(true); + } + } + } + + private void relayStats() { + if(lastStats!=null) { + display.setText("Displaying " + receiveTime); + target.updateModel(lastStats); + sendNow.setEnabled(false); + } + } + + private static void debug(String msg) { + System.out.println(msg); + } +} diff --git a/gumshoe-tools/src/test/resources/.keep b/gumshoe-tools/src/test/resources/.keep new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a4c3e27 --- /dev/null +++ b/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + com.dell + investigator + 0.1.0-SNAPSHOT + pom + Gumshoe Load Investigator + Gumshoe Load Investigator + + + gumshoe-hooks + gumshoe-probes + gumshoe-tools + + + + 3.0.0 + + + + +