/** * Written by Gil Tene of Azul Systems, and released to the public domain, * as explained at http://creativecommons.org/publicdomain/zero/1.0/ * * @author Gil Tene */ package org.jhiccup; import org.HdrHistogram.*; import java.io.*; import java.util.*; import java.text.SimpleDateFormat; import java.lang.management.*; import java.util.concurrent.TimeUnit; /** * HiccupMeter is a platform pause measurement tool, it is meant to observe * the underlying platform (JVM, OS, HW, etc.) responsiveness while under * an unrelated application load, and establish a lower bound for the stalls * the application would experience. It can be run as a wrapper around * other applications so that measurements can be done without any changes * to application code. * <p> * The purpose of HiccupMeter is to aid application operators and testers * in characterizing the inherent "platform hiccups" (execution stalls) * that a Java platform will display when running under load. The hiccups * measured are NOT stalls caused by the application's code. They are stalls * caused by the platform (JVM, OS, HW, etc.) that would be visible to and * affect any application thread running on the platform at the time of the * stall. It is generally safe to assume that if HiccupMeter experiences and * records a certain level of measured platform hiccups, the application * running on the same JVM platform during that time had experienced * hiccup/stall effects that are at least as large as the measured level. * <p> * HiccupMeter's measurement works on the simple basis of measuring the time * it takes an effectively empty workload to perform work (while running * alongside whatever load the platform is carrying). Hiccup measurements are * performed by a thread that repeatedly sleeps for a given interval (-r for * resolutionMs, defaults to 1 msec), and logs the amount of time it took to * actually wake up each time in a detailed internal hiccup histogram. The * assumption is that if the measuring thread experienced some delay in waking * up, any other thread in the system could/would have experienced a similar * delay, resulting in application stalls. * <p> * HiccupMeter collects both raw and corrected (weighted) histogram results. * When the reported time in a histogram exceeds the interval used between * measurements, the raw histogram data would reflect only the single reported * results, while the corrected histogram data will reflect an appropriate * number of additional results with linearly decreasing times (down to a time * that is lower than the measurement interval). While raw and corrected * histogram data are both tracked internally, it is the corrected numbers that * are logged and reported, as they will more accurately reflect the response * time that a random, uncoordinated request would have experienced. * <p> * HiccupMeter logs a single line with hiccup %'ile stats each reporting * interval (set with -i <reportingIntervalMs>, defaults to 5000 msec) to a * log file. The log file name can be optionally controlled with the -l <logfile> * flag, and will default (if no logfile name is supplied)to a name derived from * the process id and time (hiccup.yyMMdd.HHmm.pid). HiccupMeter will also produce * a detailed histogram log file under the <logfile>.histogram name. A new * histogram log file will be produced each interval, and will replace the * one generated in the previous interval. * <p> * HiccupMeter can be configured to delay the start of measurement * (using the -d <startDelayMs> flag, defaults to 30000 msec). It can also be * configured to terminate measurement after a given length of time (using the * -t <runTimeMs> flag). If the -t flag is not used, HiccupMeter will continue * to run until the Class executed with via the -exec parameter (see below) * exits, or indefinitely (if no -exec is used). * <p> * HiccupMeter can be configured to launch a separate "control process" on * startup (using the -c <controlProcessLogFileName> flag, defaulting to * nothing). This option launches a separate, standalone instance of * HiccupMeter wrapping running an idle workload, such that a concurrent * control measurement is established on the same system, at the same time * as the main observed application load is run. * <p> * For convenience in testing, HiccupMeter is typically launched as a * javaagent. For example, if your program were normally executed as: * <p> * java UsefulProgram -a -b -c * <p> * This is how you would execute the same program so that HiccupMeter would * record hiccups while it is running: * <p> * java -javaagent:jHiccup.jar UsefulProgram -a -b -c * <p> * Common use example: * <p> * Measure internal jitter of JVM running MyProg, log into logFile and * logFile.hgrm, report in 5 seconds intervals (which is the default), start * measurements 60 seconds into the run, and run concurrent control process * that will record hiccups on an idle workload running at the same time, * logging those into c.logFile and c.logFile.hgrm: * <p> * java -javaagent:jHiccup.jar="-d 60000 -l testLog -c c.testlog" MyProg * <p> * Note: while HiccupMeter is typically executed as a javaagent, it can be * run as a standalone program, in which case it will execute for the * duration of time specified with the -t <runTimeMs> option, or if the * -terminateOnStdIn flag is used, it will terminate execution when standard * input is severed. This last option is useful for executing HiccupMeter as * a standalone control process launched from a javaagent HiccupMeter * instance. * * Note: HiccupMeter can be used to process data from an input file instead * of sampling it. When the [-f inputFileName] option is used, the input file * is expected to contain two white-space delimited values per line (in either * integer or real number format), representing a time stamp and a measured * latency, both in millisecond units. It's important to note that the default * "expected interval between samples" resolution in jHiccup and HiccupMeter * is 1 millisecond. When processing input files, it is imperative that the * appropriate value be supplied to the -r option, and that this value correctly * represent the expected interval between samples in the provided input file. * HiccupMeter will use this parameter to determine whether additional, artificial * values should be added to the histogram recording, between input samples that * are farther apart in time than the expected interval specified to the -r option. * This behavior corrects for "coordinated omission" situations (where long response * times lead to "skipped" requests that would have typically correlated with "bad" * response times). A "large" value (e.g. -r 100000) can easily be specified to * avoid any correction of this situation. * * Note: HiccupMeter makes systemic use of HdrHistogram to collected and report * on the statistical distribution of hiccups. HdrHistogram sources, documentation, * and a ready to use jar file can all be found on GitHub, * at http://giltene.github.com/HdrHistogram */ public class HiccupMeter extends Thread { private static final String versionString = "jHiccup version " + Version.version; static final String defaultHiccupLogFileName = "hiccup.%date.%pid.hlog"; protected final PrintStream log; protected final HistogramLogWriter histogramLogWriter; protected final HiccupMeterConfiguration config; protected static class HiccupMeterConfiguration { public boolean terminateWithStdInput = false; public double resolutionMs = 1.0; public long runTimeMs = 0; public long reportingIntervalMs = 5000; public long startDelayMs = 30000; public boolean startDelayMsExplicitlySpecified = false; public boolean verbose = false; public boolean allocateObjects = false; public String logFileName; public boolean logFileExplicitlySpecified = false; public String inputFileName = null; public boolean fillInZerosInInputFile = false; public boolean logFormatCsv = false; public boolean launchControlProcess = false; public long launchControlProcessHeapSizeMBFilter = 0; public String controlProcessLogFileName = null; public String controlProcessCommand = null; public boolean controlProcessJvmArgsExplicitlySpecified = false; public String controlProcessJvmArgs; public boolean attachToProcess = false; public String pidOfProcessToAttachTo = null; public String agentJarFileName = null; public String agentArgs = null; public boolean startTimeAtZero = false; public long lowestTrackableValue = 1000L * 20L; // default to ~20usec best-case resolution public long highestTrackableValue = 3600 * 1000L * 1000L * 1000L; public int numberOfSignificantValueDigits = 2; public boolean error = false; public String errorMessage = ""; String fillInPidAndDate(String logFileName) { final String processName = java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); final String processID = processName.split("@")[0]; final SimpleDateFormat formatter = new SimpleDateFormat("yyMMdd.HHmm"); final String formattedDate = formatter.format(new Date()); logFileName = logFileName.replaceAll("%pid", processID); logFileName = logFileName.replaceAll("%date", formattedDate); return logFileName; } public HiccupMeterConfiguration(final String[] args, String defaultLogFileName) { logFileName = defaultLogFileName; try { for (int i = 0; i < args.length; ++i) { if (args[i].equals("-v")) { verbose = true; } else if (args[i].equals("-0")) { startTimeAtZero = true; } else if (args[i].equals("-p")) { attachToProcess = true; pidOfProcessToAttachTo = args[++i]; } else if (args[i].equals("-j")) { agentJarFileName = args[++i]; } else if (args[i].equals("-terminateWithStdInput")) { terminateWithStdInput = true; } else if (args[i].equals("-i")) { reportingIntervalMs = Long.parseLong(args[++i]); } else if (args[i].equals("-t")) { runTimeMs = Long.parseLong(args[++i]); } else if (args[i].equals("-d")) { startDelayMs = Long.parseLong(args[++i]); startDelayMsExplicitlySpecified = true; } else if (args[i].equals("-r")) { resolutionMs = Double.parseDouble(args[++i]); } else if (args[i].equals("-s")) { numberOfSignificantValueDigits = Integer.parseInt(args[++i]); } else if (args[i].equals("-l")) { logFileName = args[++i]; logFileExplicitlySpecified = true; } else if (args[i].equals("-f")) { inputFileName = args[++i]; } else if (args[i].equals("-fz")) { fillInZerosInInputFile = true; } else if (args[i].equals("-c")) { launchControlProcess = true; } else if (args[i].equals("-cfmb")) { launchControlProcessHeapSizeMBFilter = Long.parseLong(args[++i]);; } else if (args[i].equals("-x")) { controlProcessJvmArgs = args[++i]; controlProcessJvmArgsExplicitlySpecified = true; } else if (args[i].equals("-o")) { logFormatCsv = true; } else { throw new Exception("Invalid args: " + args[i]); } } logFileName = fillInPidAndDate(logFileName); if (attachToProcess) { if (!startDelayMsExplicitlySpecified) { startDelayMs = 0; } if (agentJarFileName == null) { throw new Exception("Invalid args, missing agent jar file name, specify with -j option"); } agentArgs = "-d " + startDelayMs + " -i " + reportingIntervalMs + ((startTimeAtZero) ? " -0" : "") + " -s " + numberOfSignificantValueDigits + " -r " + resolutionMs; if (runTimeMs != 0) { agentArgs += " -t " + runTimeMs; } if (logFileExplicitlySpecified) { agentArgs += " -l " + logFileName; } if (launchControlProcess) { agentArgs += " -c"; } if (controlProcessJvmArgsExplicitlySpecified) { agentArgs += " -x " + controlProcessJvmArgs; } if (verbose) { agentArgs += " -v"; } if (logFormatCsv) { agentArgs += " -o"; } } if (launchControlProcess && (launchControlProcessHeapSizeMBFilter > 0)) { MemoryMXBean mxbean = ManagementFactory.getMemoryMXBean(); MemoryUsage memoryUsage = mxbean.getHeapMemoryUsage(); long estimatedHeapMB = (memoryUsage.getMax() / (1024 * 1024)); if (estimatedHeapMB < launchControlProcessHeapSizeMBFilter) { launchControlProcess = false; } } if (launchControlProcess) { File filePath = new File(logFileName); String parentFileNamePart = filePath.getParent(); String childFileNamePart = filePath.getName(); // Derive control process log file name from logFileName: File controlFilePath = new File(parentFileNamePart, childFileNamePart + ".c"); controlProcessLogFileName = controlFilePath.getPath(); // Derive controlProcessCommand from our java home, class name, and parsed // options: controlProcessCommand = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java" + (controlProcessJvmArgsExplicitlySpecified ? " " + controlProcessJvmArgs : "") + " -cp " + System.getProperty("java.class.path") + " -Dorg.jhiccup.avoidRecursion=true" + " " + HiccupMeter.class.getCanonicalName() + " -l " + controlProcessLogFileName + " -i " + reportingIntervalMs + " -d " + startDelayMs + ((startTimeAtZero) ? " -0" : "") + ((logFormatCsv) ? " -o" : "") + " -s " + numberOfSignificantValueDigits + " -r " + resolutionMs + " -terminateWithStdInput"; } if (resolutionMs < 0) { System.err.println("resolutionMs must be positive."); System.exit(1); } } catch (Exception e) { error = true; errorMessage = "Error: launched with the following args:\n"; for (String arg : args) { errorMessage += arg + " "; } errorMessage += "\nWhich was parsed as an error, indicated by the following exception:\n" + e; System.err.println(errorMessage); String validArgs = "\"[-v] [-c] [-x controlProcessArgs] [-o] [-0] [-n] [-p pidOfProcessToAttachTo] [-j jHiccupJarFileName] " + "[-i reportingIntervalMs] [-h] [-t runTimeMs] [-d startDelayMs] " + "[-l logFileName] [-r resolutionMs] [-terminateWithStdInput] [-f inputFileName]\"\n"; System.err.println("valid arguments = " + validArgs); System.err.println( " [-h] help\n" + " [-v] verbose\n" + " [-l logFileName] Log hiccup information into logFileName and logFileName.hgrm\n" + " (will replace occurrences of %pid and %date with appropriate information)\n" + " [-o] Output log files in CSV format\n" + " [-c] Launch a control process in a separate JVM\n" + " logging hiccup data into logFileName.c and logFileName.c.hgrm\n" + " [-cfmb controlProcessArgs] Control process filter heap size (in MB): only launch control proc if\n" + " this process's heap size is larger than the -cfmb parameter\n" + " [-x controlProcessArgs] Pass additional args to the control process JVM\n" + " [-p pidOfProcessToAttachTo] Attach to the process with given pid and inject jHiccup as an agent\n" + " [-j jHiccupJarFileName] File name for the jHiccup.jar file, and required with [-p] option above\n" + " [-d startDelayMs] Delay the beginning of hiccup measurement by\n" + " startDelayMs milliseconds [default 30000]\n" + " [-0] Start timestamps at 0 (as opposed to at JVM runtime at start point)\n" + " [-i reportingIntervalMs] Set reporting interval [default 5000]\n" + " [-r resolutionMs] Set sampling resolution in milliseconds [default 1]\n" + " [-t runTimeMs] Limit measurement time [default 0, for infinite]\n" + " [-terminateWithStdInput] Take over standard input, and terminate process when\n" + " standard input is severed (useful for control\n" + " processes that wish to terminate when their launching\n" + " parent does).\n" + " [-f inputFileName] Read timestamp and latency data from input file\n" + " instead of sampling it directly\n" +"" + " [-fz] (applies only in conjunction with -f) fill in blank time ranges" + " with zero values. Useful e.g. when processing GC-log derived input.\n" + " [-s numberOfSignificantValueDigits]\n"); } } } public HiccupMeter(final String[] args, String defaultLogFileName) throws FileNotFoundException { this.setName("HiccupMeter"); config = new HiccupMeterConfiguration(args, defaultLogFileName); log = new PrintStream(new FileOutputStream(config.logFileName), false); histogramLogWriter = new HistogramLogWriter(log); this.setDaemon(true); } public static class ExecProcess extends Thread { final String processName; final String command; final boolean verbose; final PrintStream log; public ExecProcess(final String command, final String processName, final PrintStream log, final boolean verbose) { this.setDaemon(true); this.setName(processName + "ExecThread"); this.command = command; this.processName = processName; this.log = log; this.verbose = verbose; this.start(); } public void run() { try { if (verbose) { log.println("# HiccupMeter Executing " + processName + " command: " + command); } final Process p = Runtime.getRuntime().exec(command); p.waitFor(); } catch (Exception e) { System.err.println("HiccupMeter: " + processName + " terminated."); } } } class TerminateWithStdInputReader extends Thread { TerminateWithStdInputReader() { this.setDaemon(true); this.setName("terminateWithStdInputReader"); this.start(); } @Override public void run() { // Ensure exit when stdin is severed. try { while (System.in.read() >= 0) { } System.exit(1); } catch (Exception e) { System.exit(1); } } } public class HiccupRecorder extends Thread { public volatile boolean doRun; private final boolean allocateObjects; public volatile Long lastSleepTimeObj; // public volatile to make sure allocs are not optimized away... protected final SingleWriterRecorder recorder; public HiccupRecorder(final SingleWriterRecorder recorder, final boolean allocateObjects) { this.setDaemon(true); this.setName("HiccupRecorder"); this.recorder = recorder; this.allocateObjects = allocateObjects; doRun = true; } public void terminate() { doRun = false; } public long getCurrentTimeMsecWithDelay(final long nextReportingTime) throws InterruptedException { final long now = System.currentTimeMillis(); if (now < nextReportingTime) Thread.sleep(nextReportingTime - now); return now; } public void run() { final long resolutionNsec = (long)(config.resolutionMs * 1000L * 1000L); try { long shortestObservedDeltaTimeNsec = Long.MAX_VALUE; while (doRun) { final long timeBeforeMeasurement = System.nanoTime(); if (config.resolutionMs != 0) { TimeUnit.NANOSECONDS.sleep(resolutionNsec); if (allocateObjects) { // Allocate an object to make sure potential allocation stalls are measured. lastSleepTimeObj = new Long(timeBeforeMeasurement); } } final long timeAfterMeasurement = System.nanoTime(); final long deltaTimeNsec = timeAfterMeasurement - timeBeforeMeasurement; if (deltaTimeNsec < shortestObservedDeltaTimeNsec) { shortestObservedDeltaTimeNsec = deltaTimeNsec; } long hiccupTimeNsec = deltaTimeNsec - shortestObservedDeltaTimeNsec; recorder.recordValueWithExpectedInterval(hiccupTimeNsec, resolutionNsec); } } catch (InterruptedException e) { if (config.verbose) { log.println("# HiccupRecorder interrupted/terminating..."); } } } } class InputRecorder extends HiccupRecorder { final Scanner scanner; long prevTimeMsec = 0; InputRecorder(final SingleWriterRecorder recorder, final String inputFileName) { super(recorder, false); Scanner newScanner = null; try { newScanner = new Scanner(new File(inputFileName)); } catch (FileNotFoundException e) { System.err.println("HiccupMeter: Failed to open input file \"" + inputFileName + "\""); System.exit(-1); } finally { scanner = newScanner; } } long processInputLine(final Scanner scanner, final SingleWriterRecorder recorder) { if (scanner.hasNextLine()) { try { final long timeMsec = (long) scanner.nextDouble(); // Timestamp is expect to be in millis final long hiccupTimeMsec = (long) scanner.nextDouble(); // Latency is expected to be in millis final long hiccupTimeNsec = hiccupTimeMsec * 1000000L; // Latency is expected to be in millis if (config.fillInZerosInInputFile && (timeMsec >= (prevTimeMsec + config.resolutionMs))) { // Fill in blank time ranges with zero values: recorder.recordValueWithCount(0L, (long) ((timeMsec - prevTimeMsec) / config.resolutionMs)); prevTimeMsec = timeMsec + hiccupTimeMsec; } recorder.recordValueWithExpectedInterval(hiccupTimeNsec, (long)(config.resolutionMs * 1000000L)); if (timeMsec <= 0) { return 1; // Don't terminate on a zero timestamp. } return timeMsec; } catch (java.util.NoSuchElementException e) { return -1; } } return -1; } @Override public long getCurrentTimeMsecWithDelay(final long nextReportingTime) throws InterruptedException { return processInputLine(scanner, recorder); } @Override public void run() { try { while (doRun) { Thread.sleep(10); } } catch (InterruptedException e) { if (config.verbose) { log.println("# HiccupRecorder interrupted/terminating..."); } } } } public HiccupRecorder createHiccupRecorder(SingleWriterRecorder recorder) { return new HiccupRecorder(recorder, config.allocateObjects); } public String getVersionString() { return versionString; } @Override public void run() { final SingleWriterRecorder recorder = new SingleWriterRecorder( config.lowestTrackableValue, config.highestTrackableValue, config.numberOfSignificantValueDigits ); Histogram intervalHistogram = null; HiccupRecorder hiccupRecorder; final long uptimeAtInitialStartTime = ManagementFactory.getRuntimeMXBean().getUptime(); long now = System.currentTimeMillis(); long jvmStartTime = now - uptimeAtInitialStartTime; long reportingStartTime = jvmStartTime; if (config.inputFileName == null) { // Normal operating mode. // Launch a hiccup recorder, a process termination monitor, and an optional control process: hiccupRecorder = this.createHiccupRecorder(recorder); if (config.terminateWithStdInput) { new TerminateWithStdInputReader(); } if (config.controlProcessCommand != null) { new ExecProcess(config.controlProcessCommand, "ControlProcess", log, config.verbose); } } else { // Take input from file instead of sampling it ourselves. // Launch an input hiccup recorder, but no termination monitoring or control process: hiccupRecorder = new InputRecorder(recorder, config.inputFileName); } histogramLogWriter.outputComment("[Logged with " + getVersionString() + "]"); histogramLogWriter.outputLogFormatVersion(); try { final long startTime; if (config.inputFileName == null) { // Normal operating mode: if (config.startDelayMs > 0) { // Run hiccup recorder during startDelayMs time to let code warm up: hiccupRecorder.start(); while (config.startDelayMs > System.currentTimeMillis() - jvmStartTime) { Thread.sleep(100); } hiccupRecorder.terminate(); hiccupRecorder.join(); recorder.reset(); hiccupRecorder = new HiccupRecorder(recorder, config.allocateObjects); } hiccupRecorder.start(); startTime = System.currentTimeMillis(); if (config.startTimeAtZero) { reportingStartTime = startTime; } histogramLogWriter.outputStartTime(reportingStartTime); histogramLogWriter.setBaseTime(reportingStartTime); } else { // Reading from input file, not sampling ourselves...: hiccupRecorder.start(); now = reportingStartTime = hiccupRecorder.getCurrentTimeMsecWithDelay(0); while (config.startDelayMs > now - reportingStartTime) { now = hiccupRecorder.getCurrentTimeMsecWithDelay(0); } startTime = now; histogramLogWriter.outputComment("[Data read from input file \"" + config.inputFileName + "\" at " + new Date() + "]"); } histogramLogWriter.outputLegend(); long nextReportingTime = startTime + config.reportingIntervalMs; long intervalStartTimeMsec = 0; while ((now > 0) && ((config.runTimeMs == 0) || (config.runTimeMs > now - startTime))) { now = hiccupRecorder.getCurrentTimeMsecWithDelay(nextReportingTime); // could return -1 to indicate termination if (now > nextReportingTime) { // Get the latest interval histogram and give the recorder a fresh Histogram for the next interval intervalHistogram = recorder.getIntervalHistogram(intervalHistogram); while (now > nextReportingTime) { nextReportingTime += config.reportingIntervalMs; } if (config.inputFileName != null) { // When read from input file, use timestamps from file input for start/end of log intervals: intervalHistogram.setStartTimeStamp(intervalStartTimeMsec); intervalHistogram.setEndTimeStamp(now); intervalStartTimeMsec = now; } if (intervalHistogram.getTotalCount() > 0) { histogramLogWriter.outputIntervalHistogram(intervalHistogram); } } } } catch (InterruptedException e) { if (config.verbose) { log.println("# HiccupMeter terminating..."); } } try { hiccupRecorder.terminate(); hiccupRecorder.join(); } catch (InterruptedException e) { if (config.verbose) { log.println("# HiccupMeter terminate/join interrupted"); } } } public static HiccupMeter commonMain(final String[] args, boolean exitOnError) { HiccupMeter hiccupMeter = null; try { hiccupMeter = new HiccupMeter(args, defaultHiccupLogFileName); if (hiccupMeter.config.attachToProcess) { String errorMessage = "Cannot use -p option with HiccupMeter (use HiccupMeterAttacher instead)"; if (exitOnError) { System.err.println(errorMessage); System.exit(1); } else { throw new RuntimeException("Error: " + errorMessage); } } if (hiccupMeter.config.error) { if (exitOnError) { System.exit(1); } else { throw new RuntimeException("Error: " + hiccupMeter.config.errorMessage); } } if (hiccupMeter.config.verbose) { hiccupMeter.log.print("# Executing: HiccupMeter"); for (String arg : args) { hiccupMeter.log.print(" " + arg); } hiccupMeter.log.println(""); } hiccupMeter.start(); } catch (FileNotFoundException e) { System.err.println("HiccupMeter: Failed to open log file."); } return hiccupMeter; } public static void agentmain(String argsString, java.lang.instrument.Instrumentation inst) { final String[] args = ((argsString != null) && !argsString.equals("")) ? argsString.split("[ ,;]+") : new String[0]; final String avoidRecursion = System.getProperty("org.jhiccup.avoidRecursion"); if (avoidRecursion != null) { return; // If this is a -c invocation, we do not want the agent to do anything... } commonMain(args, false); } public static void premain(String argsString, java.lang.instrument.Instrumentation inst) { final String[] args = ((argsString != null) && !argsString.equals("")) ? argsString.split("[ ,;]+") : new String[0]; final String avoidRecursion = System.getProperty("org.jhiccup.avoidRecursion"); if (avoidRecursion != null) { return; // If this is a -c invocation, we do not want the agent to do anything... } commonMain(args, true); } public static void main(final String[] args) { final HiccupMeter hiccupMeter = commonMain(args, true); if (hiccupMeter != null) { // The HiccupMeter thread, on it's own, will not keep the JVM from exiting. If nothing else // is running (i.e. we we are the main class), then keep main thread from exiting // until the HiccupMeter thread does... try { hiccupMeter.join(); } catch (InterruptedException e) { if (hiccupMeter.config.verbose) { hiccupMeter.log.println("# HiccupMeter main() interrupted"); } } } } }