diff --git a/.gitignore b/.gitignore index 2f7896d..113bf58 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ target/ +/.settings/ +/.classpath +/.project diff --git a/README.md b/README.md index d38415d..ec920ac 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,22 @@ +This is a fork of https://github.com/papertrail/profiler/ version 1.0.3-SNAPSHOT + +* Requires Java 8 and therefore no dependency on joda-time anymore (since v1.1.0) +* Now as OSGI bundle (since v1.1.1) + +It's meant to be a simple replacement using the same package name as the original (just with the Java 8 `Duration` type) + +Original sources Copyright 2015 Papertrail, Inc. + # JVM cpu profiler A pure-java implementation of the [twitter/util](https://github.com/twitter/util) project's `CpuProfile` and related -classes. +classes. Original Scala sources: * [CpuProfile.scala](https://github.com/twitter/util/blob/develop/util-jvm/src/main/scala/com/twitter/jvm/CpuProfile.scala) * [CpuProfileTest.scala](https://github.com/twitter/util/blob/develop/util-jvm/src/test/scala/com/twitter/jvm/CpuProfileTest.scala) - + ## Usage The `CpuProfile.record` method will record samples of stacktrace elements and return a `CpuProfile` object. That object @@ -27,4 +36,14 @@ threads. Here is an example of using `curl` to retrieve a profile and turn it in ```bash curl http://localhost:8181/pprof/contention > prof pprof --pdf prof > profile.pdf -``` \ No newline at end of file +``` + +## Maven usage + +```xml + + com.helger + profiler + 1.1.1 + +``` diff --git a/pom.xml b/pom.xml index f04854f..511a5d2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,20 +1,25 @@ 4.0.0 + + com.helger + parent-pom + 1.10.9 + - com.papertrail profiler - 1.0.3-SNAPSHOT - jar - + 1.1.2-SNAPSHOT + bundle ${project.groupId}:${project.artifactId} A pure-java implementation of the twitter/util project's `CpuProfile` and related classes. - https://github.com/papertrail/profiler + https://github.com/phax/profiler + The Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0.txt + Jared Harper @@ -22,108 +27,50 @@ Papertrail, Inc. http://www.papertrailapp.com + + philip + Philip Helger + ph(at)helger.com + http://www.helger.com + + - scm:git:git@github.com:papertrail/profiler.git - scm:git:git@github.com:papertrail/profiler.git - git@github.com:papertrail/profiler.git + scm:git:git@github.com:phax/profiler.git + scm:git:git@github.com:phax/profiler.git + git@github.com:phax/profiler.git + HEAD - - org.sonatype.oss - oss-parent - 7 - - - - UTF-8 - - - - release-sign-artifacts - - - gpg.passphrase - - - - - - org.apache.maven.plugins - maven-gpg-plugin - - - sign-artifacts - verify - - sign - - - - - - - - - - - - - org.apache.maven.plugins - maven-source-plugin - 2.4 - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.10.3 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - 1.7 - 1.7 - UTF-8 - - - - - - joda-time - joda-time - 2.9.1 - javax.ws.rs javax.ws.rs-api - 2.0.1 + 2.1.1 provided + junit junit - 4.11 test + + + + + org.apache.felix + maven-bundle-plugin + true + + + com.papertrail.profiler + com.papertrail.profiler.* + + + + + diff --git a/src/main/java/com/papertrail/profiler/CpuProfile.java b/src/main/java/com/papertrail/profiler/CpuProfile.java index e50b80e..fd4bdff 100644 --- a/src/main/java/com/papertrail/profiler/CpuProfile.java +++ b/src/main/java/com/papertrail/profiler/CpuProfile.java @@ -1,8 +1,5 @@ package com.papertrail.profiler; -import org.joda.time.Duration; -import org.joda.time.Instant; - import java.io.IOException; import java.io.OutputStream; import java.lang.management.ManagementFactory; @@ -10,6 +7,8 @@ import java.lang.management.ThreadMXBean; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -17,289 +16,337 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; - /** * A CPU profile. */ -public class CpuProfile { - private final Map, Long> counts; - public final Duration duration; - public final long count; - public final long missed; - - public CpuProfile(Map, Long> counts, Duration duration, long count, long missed) { - this.counts = counts; - this.duration = duration; - this.count = count; - this.missed = missed; +public class CpuProfile +{ + private final Map , Long> m_counts; + public final Duration m_duration; + public final long m_count; + public final long m_missed; + + public CpuProfile (final Map , Long> counts, + final Duration duration, + final long count, + final long missed) + { + this.m_counts = counts; + this.m_duration = duration; + this.m_count = count; + this.m_missed = missed; + } + + private static class Word + { + final ByteBuffer m_buf; + final OutputStream m_os; + + public Word (final OutputStream os) + { + this (createBuffer (), os); } - private static class Word { - final ByteBuffer buf; - final OutputStream os; - - public Word(OutputStream os) { - this(createBuffer(), os); - } - - private Word(ByteBuffer buf, OutputStream os) { - this.buf = buf; - this.os = os; - } - - public void putWord(long n) throws IOException { - buf.clear(); - buf.putLong(n); - os.write(buf.array()); - } - - public void putString(String s) throws IOException { - os.write(s.getBytes()); - } - - private static ByteBuffer createBuffer() { - final ByteBuffer buf = ByteBuffer.allocate(8); - buf.order(ByteOrder.LITTLE_ENDIAN); - return buf; - } - - public void flush() throws IOException { - os.flush(); - } + private Word (final ByteBuffer buf, final OutputStream os) + { + this.m_buf = buf; + this.m_os = os; } - /** - * Write a Google pprof-compatible profile to `out`. The format is - * documented here: - * http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile-fileformat.html - * - * @param out And OutputStream to which the pprof output will be written - * @throws IOException if any operation on the OutputStream fails - */ - public void writeGoogleProfile(OutputStream out) throws IOException { - int next = 1; - Map uniq = new HashMap<>(); - Word word = new Word(out); - word.putString(String.format("--- symbol\nbinary=%s\n", mainClassName())); - for (Map.Entry, Long> stack : counts.entrySet()) { - for (StackTraceElement frame : stack.getKey()) { - if (!uniq.containsKey(frame)) { - word.putString(String.format("0x%016x %s\n", next, frame.toString())); - uniq.put(frame, next); - next += 1; - } - } - } - word.putString("---\n--- profile\n"); - for (int w : new int[]{0, 3, 0, 1, 0}) { - word.putWord(w); - } - for (Map.Entry, Long> entry : counts.entrySet()) { - List stack = entry.getKey(); - long n = entry.getValue(); - if (!stack.isEmpty()) { - word.putWord(n); - word.putWord(stack.size()); - } - for (StackTraceElement frame : stack) { - word.putWord(uniq.get(frame)); - } - } - word.putWord(0); - word.putWord(1); - word.putWord(0); - word.flush(); + public void putWord (final long n) throws IOException + { + m_buf.clear (); + m_buf.putLong (n); + m_os.write (m_buf.array ()); } - static class StringPair { - final String a; - final String b; - - StringPair(String a, String b) { - this.a = a; - this.b = b; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - StringPair that = (StringPair) o; - return Objects.equals(a, that.a) && - Objects.equals(b, that.b); - } - - @Override - public int hashCode() { - return Objects.hash(a, b); - } + public void putString (final String s) throws IOException + { + m_os.write (s.getBytes ()); } - // (class name, method names) that say they are runnable, but are actually doing nothing. - private static final Set idleClassAndMethod = new HashSet<>(); - - static { - idleClassAndMethod.add(new StringPair("sun.nio.ch.EPollArrayWrapper", "epollWait")); - idleClassAndMethod.add(new StringPair("sun.nio.ch.KQueueArrayWrapper", "kevent0")); - idleClassAndMethod.add(new StringPair("java.net.SocketInputStream", "socketRead0")); - idleClassAndMethod.add(new StringPair("java.net.SocketOutputStream", "socketWrite0")); - idleClassAndMethod.add(new StringPair("java.net.PlainSocketImpl", "socketAvailable")); - idleClassAndMethod.add(new StringPair("java.net.PlainSocketImpl", "socketAccept")); - idleClassAndMethod.add(new StringPair("sun.nio.ch.ServerSocketChannelImpl", "accept0")); + private static ByteBuffer createBuffer () + { + final ByteBuffer buf = ByteBuffer.allocate (8); + buf.order (ByteOrder.LITTLE_ENDIAN); + return buf; } - /** - * When looking for RUNNABLEs, the JVM's notion of runnable differs from the - * from kernel's definition and for some well known cases, we can filter - * out threads that are actually asleep. - * See http://www.brendangregg.com/blog/2014-06-09/java-cpu-sampling-using-hprof.html - * - * @param elem StackTraceElement to check - * @return true if it's not a known idle method - */ - protected static boolean isRunnable(StackTraceElement elem) { - return !idleClassAndMethod.contains( - new StringPair(elem.getClassName(), elem.getMethodName())); + public void flush () throws IOException + { + m_os.flush (); } - - /** - * Profile CPU usage of threads in `state` for `howlong`, sampling - * stacks at `frequency` Hz. - * As an example, using Nyquist's sampling theorem, we see that - * sampling at 100Hz will accurately represent components 50Hz or - * less; ie. any stack that contributes 2% or more to the total CPU - * time. - * Note that the maximum sampling frequency is set to 1000Hz. - * Anything greater than this is likely to consume considerable - * amounts of CPU while sampling. - * The profiler will discount its own stacks. - * TODO: - * - Should we synthesize GC frames? GC has significant runtime - * impact, so it seems nonfaithful to exlude them. - * - Limit stack depth? - * - * @param howlong Duration of profile - * @param frequency polling interval - * @param state Thread.State to match against - * @return CpuProfile results - */ - public static CpuProfile record(Duration howlong, int frequency, Thread.State state) { - /* - PLEASE NOTE: I modified this code to use millisecond precision as the original code that used microsecond - precision was incorrect. Each time it looked at the clock or slept, it was using millis under the hood. - */ - if (frequency > 1000) { - throw new RuntimeException("frequency must be < 1000"); - } - - // TODO: it may make sense to write a custom hash function here - // that needn't traverse the all stack trace elems. Usually, the - // top handful of frames are distinguishing. - Map, Long> counts = new HashMap<>(); - ThreadMXBean bean = ManagementFactory.getThreadMXBean(); - Instant start = Instant.now(); - Instant end = start.plus(howlong); - int periodMillis = 1000 / frequency; - long myId = Thread.currentThread().getId(); - Instant next = Instant.now(); - - long n = 0; - long nmissed = 0; - - while (Instant.now().isBefore(end)) { - for (ThreadInfo thread : bean.dumpAllThreads(false, false)) { - if (thread.getThreadState() == state && thread.getThreadId() != myId) { - List s = Arrays.asList(thread.getStackTrace()); - if (!s.isEmpty()) { - boolean include = state != Thread.State.RUNNABLE || isRunnable(s.get(0)); - if (include) { - if (counts.get(s) == null) { - counts.put(s, 1L); - } else { - long count = counts.get(s); - counts.put(s, count + 1L); - } - } - } - } - } - n += 1; - next = next.plus(periodMillis); - - while (next.isBefore(Instant.now()) && next.isBefore(end)) { - nmissed += 1; - next = next.plus(periodMillis); - } - - long sleep = Math.max((next.getMillis() - Instant.now().getMillis()), 0); - try { - Thread.sleep(sleep); - } catch (InterruptedException e) { - System.out.print("CpuProfile interrupted."); - Thread.currentThread().interrupt(); - return null; - } + } + + /** + * Write a Google pprof-compatible profile to `out`. The format is documented + * here: + * http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile-fileformat.html + * + * @param out + * And OutputStream to which the pprof output will be written + * @throws IOException + * if any operation on the OutputStream fails + */ + public void writeGoogleProfile (final OutputStream out) throws IOException + { + int next = 1; + final Map uniq = new HashMap <> (); + final Word word = new Word (out); + word.putString (String.format ("--- symbol\nbinary=%s\n", mainClassName ())); + for (final Map.Entry , Long> stack : m_counts.entrySet ()) + { + for (final StackTraceElement frame : stack.getKey ()) + { + if (!uniq.containsKey (frame)) + { + word.putString (String.format ("0x%016x %s\n", Integer.valueOf (next), frame.toString ())); + uniq.put (frame, Integer.valueOf (next)); + next += 1; } - - return new CpuProfile(counts, new Duration(start, Instant.now()), n, nmissed); + } + } + word.putString ("---\n--- profile\n"); + for (final int w : new int [] { 0, 3, 0, 1, 0 }) + { + word.putWord (w); + } + for (final Map.Entry , Long> entry : m_counts.entrySet ()) + { + final List stack = entry.getKey (); + final long n = entry.getValue ().longValue (); + if (!stack.isEmpty ()) + { + word.putWord (n); + word.putWord (stack.size ()); + } + for (final StackTraceElement frame : stack) + { + word.putWord (uniq.get (frame).longValue ()); + } + } + word.putWord (0); + word.putWord (1); + word.putWord (0); + word.flush (); + } + + static class StringPair + { + final String m_a; + final String m_b; + + StringPair (final String a, final String b) + { + this.m_a = a; + this.m_b = b; } - public CpuProfile record(Duration howlong, int frequency) { - return record(howlong, frequency, Thread.State.RUNNABLE); + @Override + public boolean equals (final Object o) + { + if (this == o) + return true; + if (o == null || getClass () != o.getClass ()) + return false; + final StringPair that = (StringPair) o; + return Objects.equals (m_a, that.m_a) && Objects.equals (m_b, that.m_b); } - /** - * Call `record` in a thread with the given parameters, returning a - * `Future` representing the completion of the profile. - * - * @param howlong Duration of profile - * @param frequency polling interval - * @param state Thread.State to match against - * @return Future contiaining CpuProfile results + @Override + public int hashCode () + { + return Objects.hash (m_a, m_b); + } + } + + // (class name, method names) that say they are runnable, but are actually + // doing nothing. + private static final Set idleClassAndMethod = new HashSet <> (); + + static + { + idleClassAndMethod.add (new StringPair ("sun.nio.ch.EPollArrayWrapper", "epollWait")); + idleClassAndMethod.add (new StringPair ("sun.nio.ch.KQueueArrayWrapper", "kevent0")); + idleClassAndMethod.add (new StringPair ("java.net.SocketInputStream", "socketRead0")); + idleClassAndMethod.add (new StringPair ("java.net.SocketOutputStream", "socketWrite0")); + idleClassAndMethod.add (new StringPair ("java.net.PlainSocketImpl", "socketAvailable")); + idleClassAndMethod.add (new StringPair ("java.net.PlainSocketImpl", "socketAccept")); + idleClassAndMethod.add (new StringPair ("sun.nio.ch.ServerSocketChannelImpl", "accept0")); + } + + /** + * When looking for RUNNABLEs, the JVM's notion of runnable differs from the + * from kernel's definition and for some well known cases, we can filter out + * threads that are actually asleep. See + * http://www.brendangregg.com/blog/2014-06-09/java-cpu-sampling-using-hprof.html + * + * @param elem + * StackTraceElement to check + * @return true if it's not a known idle method + */ + protected static boolean isRunnable (final StackTraceElement elem) + { + return !idleClassAndMethod.contains (new StringPair (elem.getClassName (), elem.getMethodName ())); + } + + /** + * Profile CPU usage of threads in `state` for `howlong`, sampling stacks at + * `frequency` Hz. As an example, using Nyquist's sampling theorem, we see + * that sampling at 100Hz will accurately represent components 50Hz or less; + * ie. any stack that contributes 2% or more to the total CPU time. Note that + * the maximum sampling frequency is set to 1000Hz. Anything greater than this + * is likely to consume considerable amounts of CPU while sampling. The + * profiler will discount its own stacks. TODO: - Should we synthesize GC + * frames? GC has significant runtime impact, so it seems nonfaithful to + * exlude them. - Limit stack depth? + * + * @param howlong + * Duration of profile + * @param frequency + * polling interval + * @param state + * Thread.State to match against + * @return CpuProfile results + */ + public static CpuProfile record (final Duration howlong, final int frequency, final Thread.State state) + { + /* + * PLEASE NOTE: I modified this code to use millisecond precision as the + * original code that used microsecond precision was incorrect. Each time it + * looked at the clock or slept, it was using millis under the hood. */ - public static Future recordInThread(final Duration howlong, final int frequency, final Thread.State state) { - final FutureTask future = new FutureTask<>(new Callable() { - @Override - public CpuProfile call() throws Exception { - return record(howlong, frequency, state); - } - }); - Thread t = new Thread(future, "CpuProfile"); - t.start(); - return future; + if (frequency > 1000) + { + throw new RuntimeException ("frequency must be < 1000"); } - public static Future recordInThread(Duration howlong, int frequency) { - return recordInThread(howlong, frequency, Thread.State.RUNNABLE); + // TODO: it may make sense to write a custom hash function here + // that needn't traverse the all stack trace elems. Usually, the + // top handful of frames are distinguishing. + final Map , Long> counts = new HashMap <> (); + final ThreadMXBean bean = ManagementFactory.getThreadMXBean (); + final Instant start = Instant.now (); + final Instant end = start.plus (howlong); + final int periodMillis = 1000 / frequency; + final long myId = Thread.currentThread ().getId (); + Instant next = Instant.now (); + + long n = 0; + long nmissed = 0; + + while (Instant.now ().isBefore (end)) + { + for (final ThreadInfo thread : bean.dumpAllThreads (false, false)) + { + if (thread.getThreadState () == state && thread.getThreadId () != myId) + { + final List s = Arrays.asList (thread.getStackTrace ()); + if (!s.isEmpty ()) + { + final boolean include = state != Thread.State.RUNNABLE || isRunnable (s.get (0)); + if (include) + { + final Long count = counts.get (s); + if (count == null) + { + counts.put (s, Long.valueOf (1L)); + } + else + { + counts.put (s, Long.valueOf (count.longValue () + 1)); + } + } + } + } + } + n += 1; + next = next.plusMillis (periodMillis); + + while (next.isBefore (Instant.now ()) && next.isBefore (end)) + { + nmissed += 1; + next = next.plusMillis (periodMillis); + } + + final long sleep = Math.max ((next.toEpochMilli () - Instant.now ().toEpochMilli ()), 0); + try + { + Thread.sleep (sleep); + } + catch (final InterruptedException e) + { + System.out.print ("CpuProfile interrupted."); + Thread.currentThread ().interrupt (); + return null; + } } - // Ripped and ported from Twitter's Jvm trait - - /** - * Get the main class name for the currently running application. - * Note that this works only by heuristic, and may not be accurate. - * TODO: take into account the standard callstack around scala - * invocations better. - * - * @return main class name - */ - public static String mainClassName() { - for (Map.Entry entry : Thread.getAllStackTraces().entrySet()) { - Thread t = entry.getKey(); - StackTraceElement[] elements = entry.getValue(); - if ("main".equals(t.getName())) { - for (int i = elements.length - 1; i >= 0; i--) { - StackTraceElement elem = elements[i]; - if (!elem.getClassName().startsWith("scala.tools.nsc.MainGenericRunner")) { - return elem.getClassName(); - } - } - } + return new CpuProfile (counts, Duration.between (start, Instant.now ()), n, nmissed); + } + + public CpuProfile record (final Duration howlong, final int frequency) + { + return record (howlong, frequency, Thread.State.RUNNABLE); + } + + /** + * Call `record` in a thread with the given parameters, returning a `Future` + * representing the completion of the profile. + * + * @param howlong + * Duration of profile + * @param frequency + * polling interval + * @param state + * Thread.State to match against + * @return Future containing CpuProfile results + */ + public static Future recordInThread (final Duration howlong, + final int frequency, + final Thread.State state) + { + final FutureTask future = new FutureTask <> ( () -> record (howlong, frequency, state)); + final Thread t = new Thread (future, "CpuProfile"); + t.start (); + return future; + } + + public static Future recordInThread (final Duration howlong, final int frequency) + { + return recordInThread (howlong, frequency, Thread.State.RUNNABLE); + } + + // Ripped and ported from Twitter's Jvm trait + + /** + * Get the main class name for the currently running application. Note that + * this works only by heuristic, and may not be accurate. TODO: take into + * account the standard callstack around scala invocations better. + * + * @return main class name + */ + public static String mainClassName () + { + for (final Map.Entry entry : Thread.getAllStackTraces ().entrySet ()) + { + final Thread t = entry.getKey (); + final StackTraceElement [] elements = entry.getValue (); + if ("main".equals (t.getName ())) + { + for (int i = elements.length - 1; i >= 0; i--) + { + final StackTraceElement elem = elements[i]; + if (!elem.getClassName ().startsWith ("scala.tools.nsc.MainGenericRunner")) + { + return elem.getClassName (); + } } - return "unknown"; + } } -} \ No newline at end of file + return "unknown"; + } +} diff --git a/src/main/java/com/papertrail/profiler/jaxrs/CpuProfileResource.java b/src/main/java/com/papertrail/profiler/jaxrs/CpuProfileResource.java index 4158082..76324e6 100644 --- a/src/main/java/com/papertrail/profiler/jaxrs/CpuProfileResource.java +++ b/src/main/java/com/papertrail/profiler/jaxrs/CpuProfileResource.java @@ -1,54 +1,61 @@ package com.papertrail.profiler.jaxrs; -import com.papertrail.profiler.CpuProfile; -import org.joda.time.Duration; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -@Path("/pprof") -public class CpuProfileResource { - final Lock lock = new ReentrantLock(); +import com.papertrail.profiler.CpuProfile; + +@Path ("/pprof") +public class CpuProfileResource +{ + private final Lock lock = new ReentrantLock (); - @Produces("pprof/raw") - @GET - @Path("profile") - public byte[] profile( - @QueryParam("duration") @DefaultValue("10") Integer duration, - @QueryParam("frequency") @DefaultValue("100") Integer frequency) throws IOException { - return doProfile(duration, frequency, Thread.State.RUNNABLE); - } + @Produces ("pprof/raw") + @GET + @Path ("profile") + public byte [] profile (@QueryParam ("duration") @DefaultValue ("10") final Integer duration, + @QueryParam ("frequency") @DefaultValue ("100") final Integer frequency) throws IOException + { + return doProfile (duration.intValue (), frequency.intValue (), Thread.State.RUNNABLE); + } - @Produces("pprof/raw") - @GET - @Path("contention") - public byte[] contention( - @QueryParam("duration") @DefaultValue("10") Integer duration, - @QueryParam("frequency") @DefaultValue("100") Integer frequency) throws IOException { - return doProfile(duration, frequency, Thread.State.BLOCKED); - } + @Produces ("pprof/raw") + @GET + @Path ("contention") + public byte [] contention (@QueryParam ("duration") @DefaultValue ("10") final Integer duration, + @QueryParam ("frequency") @DefaultValue ("100") final Integer frequency) throws IOException + { + return doProfile (duration.intValue (), frequency.intValue (), Thread.State.BLOCKED); + } - protected byte[] doProfile(int duration, int frequency, Thread.State state) throws IOException { - if (lock.tryLock()) { - try { - CpuProfile profile = CpuProfile.record(Duration.standardSeconds(duration), frequency, state); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - if (profile == null) { - throw new RuntimeException("could not create CpuProfile"); - } - profile.writeGoogleProfile(stream); - return stream.toByteArray(); - } finally { - lock.unlock(); - } + protected byte [] doProfile (final int duration, final int frequency, final Thread.State state) throws IOException + { + if (lock.tryLock ()) + { + try (final ByteArrayOutputStream stream = new ByteArrayOutputStream ()) + { + final CpuProfile profile = CpuProfile.record (Duration.ofSeconds (duration), frequency, state); + if (profile == null) + { + throw new RuntimeException ("could not create CpuProfile"); } - throw new RuntimeException("Only one profile request may be active at a time"); + profile.writeGoogleProfile (stream); + return stream.toByteArray (); + } + finally + { + lock.unlock (); + } } -} \ No newline at end of file + throw new RuntimeException ("Only one profile request may be active at a time"); + } +} diff --git a/src/test/java/com/papertrail/profiler/CpuProfileTest.java b/src/test/java/com/papertrail/profiler/CpuProfileTest.java index 9f60328..e7b9141 100644 --- a/src/test/java/com/papertrail/profiler/CpuProfileTest.java +++ b/src/test/java/com/papertrail/profiler/CpuProfileTest.java @@ -1,51 +1,62 @@ package com.papertrail.profiler; -import org.joda.time.Duration; -import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.io.OutputStream; +import java.time.Duration; -import static org.junit.Assert.*; +import org.junit.Test; /** * Ported from scala */ -public class CpuProfileTest { - @Test - public void testRecord() throws Exception { - Thread t = new Thread("CpuProfileTest") { - @Override - public void run() { - try { - Thread.sleep(10000); - } catch (InterruptedException ignored) { - } - } - }; - t.setDaemon(true); - t.start(); +public class CpuProfileTest +{ + @Test + public void testRecord () throws Exception + { + final Thread t = new Thread ("CpuProfileTest") + { + @Override + public void run () + { + try + { + Thread.sleep (10_000); + } + catch (final InterruptedException ignored) + {} + } + }; + t.setDaemon (true); + t.start (); - // Profile for 1000ms at 10 Hz => 100ms period; produces 10 samples. - CpuProfile profile = CpuProfile.record(Duration.standardSeconds(1), 10, Thread.State.TIMED_WAITING); - assertNotNull(profile); - assertEquals(10, profile.count); - assertEquals(0, profile.missed); - OutputStream baos = new ByteArrayOutputStream(); - profile.writeGoogleProfile(baos); - assertTrue(baos.toString().contains("CpuProfileTest")); - assertTrue(baos.toString().contains("Thread.sleep")); - } + // Profile for 1000ms at 10 Hz => 100ms period; produces 10 samples. + final CpuProfile profile = CpuProfile.record (Duration.ofSeconds (1), 10, Thread.State.TIMED_WAITING); + assertNotNull (profile); + assertEquals (10, profile.m_count); + assertEquals (0, profile.m_missed); + final OutputStream baos = new ByteArrayOutputStream (); + profile.writeGoogleProfile (baos); + assertTrue (baos.toString ().contains ("CpuProfileTest")); + assertTrue (baos.toString ().contains ("Thread.sleep")); + } - @Test - public void testisRunnable() { - assertTrue(CpuProfile.isRunnable(newElem("foo", "bar"))); - assertFalse(CpuProfile.isRunnable(newElem("sun.nio.ch.EPollArrayWrapper", "epollWait"))); - assertFalse(CpuProfile.isRunnable(newElem("sun.nio.ch.KQueueArrayWrapper", "kevent0"))); - assertFalse(CpuProfile.isRunnable(newElem("sun.nio.ch.ServerSocketChannelImpl", "accept0"))); - } + @Test + public void testisRunnable () + { + assertTrue (CpuProfile.isRunnable (newElem ("foo", "bar"))); + assertFalse (CpuProfile.isRunnable (newElem ("sun.nio.ch.EPollArrayWrapper", "epollWait"))); + assertFalse (CpuProfile.isRunnable (newElem ("sun.nio.ch.KQueueArrayWrapper", "kevent0"))); + assertFalse (CpuProfile.isRunnable (newElem ("sun.nio.ch.ServerSocketChannelImpl", "accept0"))); + } - public static StackTraceElement newElem(String className, String methodName) { - return new StackTraceElement(className, methodName, "SomeFile.java", 1); - } -} \ No newline at end of file + public static StackTraceElement newElem (final String className, final String methodName) + { + return new StackTraceElement (className, methodName, "SomeFile.java", 1); + } +} diff --git a/src/test/java/com/papertrail/profiler/jaxrs/CpuProfileResourceTest.java b/src/test/java/com/papertrail/profiler/jaxrs/CpuProfileResourceTest.java index 536b59d..2698c7e 100644 --- a/src/test/java/com/papertrail/profiler/jaxrs/CpuProfileResourceTest.java +++ b/src/test/java/com/papertrail/profiler/jaxrs/CpuProfileResourceTest.java @@ -1,37 +1,48 @@ package com.papertrail.profiler.jaxrs; -import org.junit.Test; +import static junit.framework.TestCase.assertEquals; import java.io.IOException; import java.util.concurrent.CountDownLatch; -import static junit.framework.TestCase.assertEquals; +import org.junit.Test; -public class CpuProfileResourceTest { - @Test - public void testDoProfile() throws Exception { - final CpuProfileResource resource = new CpuProfileResource(); - final CountDownLatch latch = new CountDownLatch(1); - Thread t1 = new Thread() { - @Override - public void run() { - latch.countDown(); - try { - resource.doProfile(1, 100, State.BLOCKED); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }; - t1.setDaemon(true); - t1.start(); - latch.await(); - // Even though we await the latch to start the thread, we can still race, so mitigate with a short sleep - Thread.sleep(10); - try { - resource.doProfile(1, 100, Thread.State.BLOCKED); - } catch (RuntimeException ex) { - assertEquals("Only one profile request may be active at a time", ex.getMessage()); +public class CpuProfileResourceTest +{ + @Test + public void testDoProfile () throws Exception + { + final CpuProfileResource resource = new CpuProfileResource (); + final CountDownLatch latch = new CountDownLatch (1); + final Thread t1 = new Thread () + { + @Override + public void run () + { + latch.countDown (); + try + { + resource.doProfile (1, 100, State.BLOCKED); + } + catch (final IOException e) + { + throw new RuntimeException (e); } + } + }; + t1.setDaemon (true); + t1.start (); + latch.await (); + // Even though we await the latch to start the thread, we can still race, so + // mitigate with a short sleep + Thread.sleep (10); + try + { + resource.doProfile (1, 100, Thread.State.BLOCKED); + } + catch (final RuntimeException ex) + { + assertEquals ("Only one profile request may be active at a time", ex.getMessage ()); } -} \ No newline at end of file + } +}