package org.intrace.client.gui.helper; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Queue; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * Threaded owner of the trace lines. Allows a Pattern to be used to filter * trace text. */ public class TraceFilterThread implements Runnable { /** * Trace text output callback */ public static interface TraceTextHandler { void setText(String traceText); void appendText(String traceText); public void setStatus(final int displayed, final int total); } /** * Filter progress monitor callback */ public static interface TraceFilterProgressHandler { boolean setProgress(int percent); List<String> getIncludePattern(); List<String> getExcludePattern(); boolean discardFiltered(); } /** * This constant defines the minimum gap in milliseconds between * callbacks to the TraceTextHandler. */ private static final long MIN_UPDATE_GAP = 100; // milliseconds /** * System trace is never filtered out */ private static final String SYSTEM_TRACE_PREFIX = "***"; /** * Low memory warning */ private static final String LOW_MEMORY = "Warning: Low memory - no further trace will be collected. StdOut or File output will continue if enabled."; /** * Match all val */ public static final String MATCH_ALL_VAL = "*"; /** * Pattern which matches anything */ public static final List<String> MATCH_ALL = new ArrayList<String>(1); static { MATCH_ALL.add(MATCH_ALL_VAL); } /** * Pattern which matches nothing */ public static final List<String> MATCH_NONE = new ArrayList<String>(0); /** * Queue of filters to apply - this should usually only contain zero or one * entry */ private final Queue<TraceFilterProgressHandler> traceFilters = new ConcurrentLinkedQueue<TraceFilterProgressHandler>(); /** * Queue of new unprocessed trace lines */ private final BlockingQueue<String> newTraceLines = new LinkedBlockingQueue<String>(); /** * Semaphore controlling how many trace line can be added */ private final Semaphore traceLineSemaphore = new Semaphore(30); /** * List of trace lines saved by this thread from the newTraceLines */ private List<String> traceLines = new ArrayList<String>(); /** * Number of displayed lines */ private final AtomicInteger displayedLines = new AtomicInteger(); /** * Total number of lines */ private final AtomicInteger totalLines = new AtomicInteger(); /** * Reference to this Thread */ private final Thread thisThread; /** * Text output callback */ private final TraceTextHandler callback; /** * Flag to signal that trace should be cleared */ private boolean clearTrace = false; /** * Flag to signal that excess trace should be discarded */ private boolean discardExcessTrace = true; /** * cTor * @param mode * * @param callback * @param progressCallback */ public TraceFilterThread(TraceTextHandler callback) { this.callback = callback; thisThread = new Thread(this); thisThread.setDaemon(true); thisThread.setName("Trace Filter"); thisThread.start(); } public void interrupt() { thisThread.interrupt(); } public void addTraceLine(String traceLine) { try { traceLineSemaphore.acquire(); newTraceLines.add(traceLine + "\r\n"); } catch (InterruptedException e) { // Restore interrupted flag Thread.interrupted(); } // We don't release the semaphore here - the trace appender // thread does so once it is happy to allow more trace lines // in } public void addSystemTraceLine(String traceLine) { newTraceLines.add(SYSTEM_TRACE_PREFIX + " " + traceLine + "\r\n"); } public void applyFilter(TraceFilterProgressHandler progressCallback) { traceFilters.add(progressCallback); thisThread.interrupt(); } public void setDiscardExcess(boolean xiDiscardExcess) { discardExcessTrace = xiDiscardExcess; } public synchronized void setClearTrace() { clearTrace = true; thisThread.interrupt(); } private synchronized boolean getClearTrace() { boolean retClearTrace = clearTrace; clearTrace = false; return retClearTrace; } @Override public void run() { long maxMemory = Runtime.getRuntime().maxMemory(); long byteMemLimit = (long) Math.max(maxMemory - (10 * 1000 * 1000), // Maxmemory // - 10mb 0.8 * maxMemory); // 90% of Maxmemory long charMemLimit = byteMemLimit / 2; // UTF-16 - 2 bytes per char long numChars = 0; boolean doClearTrace = false; boolean lowMemorySignalled = false; TraceFilterProgressHandler patternProgress = null; List<String> activeIncludePattern = MATCH_ALL; List<String> activeExcludePattern = MATCH_NONE; boolean discardFilteredTrace = true; StringBuilder bufferedText = new StringBuilder(); long lastTextTime = 0; int bufferedTextCount = 0; try { while (true) { try { if (patternProgress != null) { activeIncludePattern = patternProgress.getIncludePattern(); activeExcludePattern = patternProgress.getExcludePattern(); applyPattern(patternProgress); discardFilteredTrace = patternProgress.discardFiltered(); patternProgress = null; cb.callback(); } if (doClearTrace) { doClearTrace = false; traceLines.clear(); callback.setText(""); displayedLines.set(0); totalLines.set(0); callback.setStatus(displayedLines.get(), totalLines.get()); numChars = 0; cb.callback(); } String newTraceLine = null; if (bufferedTextCount > 0) { newTraceLine = newTraceLines.poll(MIN_UPDATE_GAP, TimeUnit.MILLISECONDS); } else { newTraceLine = newTraceLines.take(); } long newTextTime = time.currentTimeMillis(); long timeSinceLastText = newTextTime - lastTextTime; boolean bufferedTextToWrite = (bufferedTextCount > 0); boolean writeBufferedText = (timeSinceLastText > MIN_UPDATE_GAP) && bufferedTextToWrite; if (writeBufferedText) { String bufferedTextStr = bufferedText.toString(); totalLines.addAndGet(bufferedTextCount); displayedLines.addAndGet(bufferedTextCount); callback.appendText(bufferedTextStr); callback.setStatus(displayedLines.get(), totalLines.get()); lastTextTime = newTextTime; bufferedText.setLength(0); bufferedTextCount = 0; bufferedTextToWrite = false; } if (newTraceLine != null) { if (!newTraceLine.startsWith(SYSTEM_TRACE_PREFIX)) { traceLineSemaphore.release(); } boolean memoryLimitSafe = (numChars < charMemLimit); if (memoryLimitSafe || discardExcessTrace) { if (!memoryLimitSafe) { numChars = discardExcess(activeIncludePattern, activeExcludePattern); } lowMemorySignalled = false; boolean matchFilter = newTraceLine.startsWith(SYSTEM_TRACE_PREFIX) || (!matches(activeExcludePattern, newTraceLine) && matches(activeIncludePattern, newTraceLine)); String[] splitParts = newTraceLine.split(":"); if ((repeatedMethods.size()>0) || methodFilterRecordingEnabled) { if (splitParts.length >= 6) { String methodName = splitParts[4] + ":" + splitParts[5]; if (repeatedMethods.contains(methodName)) { matchFilter = false; } else { if (methodFilterRecordingEnabled) repeatedMethods.add(methodName); } } } if (!discardFilteredTrace || matchFilter) { // I expected a factor of 2 due to trace strings being held by this // thread along with another copy held by the UI. However, profiling // shows a factor of 40 is necessary. This is because we need to be able // to handle entire copies of the active data when adding new strings. numChars += (40 * newTraceLine.length()); traceLines.add(newTraceLine); } if (matchFilter) { if (timeSinceLastText > MIN_UPDATE_GAP) { totalLines.addAndGet(1); displayedLines.addAndGet(1); callback.appendText(newTraceLine); callback.setStatus(displayedLines.get(), totalLines.get()); lastTextTime = newTextTime; } else { bufferedTextCount++; bufferedText.append(newTraceLine); } } else if (!discardExcessTrace) { totalLines.incrementAndGet(); callback.setStatus(displayedLines.get(), totalLines.get()); } } } if (!bufferedTextToWrite && (numChars >= charMemLimit) && !lowMemorySignalled) { lowMemorySignalled = true; String memWarning = SYSTEM_TRACE_PREFIX + " " + LOW_MEMORY; traceLines.add(memWarning); totalLines.incrementAndGet(); displayedLines.incrementAndGet(); callback.appendText(memWarning); callback.setStatus(displayedLines.get(), totalLines.get()); } cb.callback(); } catch (InterruptedException ex) { doClearTrace = getClearTrace(); patternProgress = traceFilters.poll(); if ((patternProgress == null) && !doClearTrace) { // Time to quit break; } } } } catch (Throwable ex) { ex.printStackTrace(); } } private boolean matches(List<String> strs, String target) { for (String str : strs) { if (str.equals(MATCH_ALL_VAL) || target.contains(str)) { return true; } } return false; } private void applyPattern(TraceFilterProgressHandler progressCallback) { boolean xiDiscardFiltered = progressCallback.discardFiltered(); List<String> includePattern = progressCallback.getIncludePattern(); List<String> excludePattern = progressCallback.getExcludePattern(); int numLines = traceLines.size(); int handledLines = 0; double lastPercentage = 0; double filterPercentage = (xiDiscardFiltered ? 70 : 100); StringBuilder traceText = new StringBuilder(); boolean cancelled = progressCallback.setProgress(0); boolean firstUpdate = true; displayedLines.set(0); if (xiDiscardFiltered) { totalLines.set(0); } callback.setStatus(displayedLines.get(), totalLines.get()); List<Integer> removeLines = new ArrayList<Integer>(); if (!cancelled) { for (int ii = 0; ii < traceLines.size(); ii++) { String traceLine = traceLines.get(ii); if (traceLine.startsWith(SYSTEM_TRACE_PREFIX) || (!matches(excludePattern, traceLine) && matches(includePattern, traceLine))) { displayedLines.incrementAndGet(); if (xiDiscardFiltered) { totalLines.incrementAndGet(); } traceText.append(traceLine); } else if (xiDiscardFiltered) { removeLines.add(ii); } handledLines++; if ((handledLines % 10000) == 0) { double unroundedPercentage = ((double) handledLines) / ((double) numLines); double roundedPercantage = roundToSignificantFigures( unroundedPercentage, 2); if (lastPercentage != roundedPercantage) { // Try and ensure that we are GC-ing regularly System.gc(); cancelled = progressCallback.setProgress( (int) (filterPercentage * roundedPercantage)); if (cancelled) { break; } else { if (firstUpdate) { callback.setText(traceText.toString()); callback.setStatus(displayedLines.get(), totalLines.get()); traceText = new StringBuilder(); firstUpdate = false; } else { callback.appendText(traceText.toString()); callback.setStatus(displayedLines.get(), totalLines.get()); } } } lastPercentage = roundedPercantage; } } } if (xiDiscardFiltered) { handledLines = 0; for (Integer removeLine : removeLines) { traceLines.remove(removeLine - handledLines); handledLines++; if ((handledLines % 10000) == 0) { double unroundedPercentage = ((double) handledLines) / ((double) numLines); double roundedPercantage = roundToSignificantFigures( unroundedPercentage, 2); if (lastPercentage != roundedPercantage) { // Try and ensure that we are GC-ing regularly System.gc(); cancelled = progressCallback.setProgress( (int)(filterPercentage) + (int) ((100 - filterPercentage) * roundedPercantage)); } lastPercentage = roundedPercantage; } } } if (!cancelled) { if (firstUpdate) { callback.setText(traceText.toString()); callback.setStatus(displayedLines.get(), totalLines.get()); } else { callback.appendText(traceText.toString()); callback.setStatus(displayedLines.get(), totalLines.get()); } } progressCallback.setProgress(100); } private long discardExcess(List<String> includePattern, List<String> excludePattern) { int numLines = traceLines.size(); int discardLines = (int)(0.25 * (double)numLines); StringBuilder traceText = new StringBuilder(); displayedLines.set(0); totalLines.set(0); List<String> newTraceLines = new ArrayList<String>((numLines - discardLines) + 10); // Update counters and recompute text for (int ii = discardLines; ii < traceLines.size(); ii++) { String traceLine = traceLines.get(ii); newTraceLines.add(traceLine); if (traceLine.startsWith(SYSTEM_TRACE_PREFIX) || (!matches(excludePattern, traceLine) && matches(includePattern, traceLine))) { displayedLines.incrementAndGet(); traceText.append(traceLine); } totalLines.incrementAndGet(); } // Make callbacks traceLines = newTraceLines; String text = traceText.toString(); callback.setText(text); callback.setStatus(displayedLines.get(), totalLines.get()); return text.length(); } private static double roundToSignificantFigures(double num, int n) { if (num == 0) { return 0; } final double d = Math.ceil(Math.log10(num < 0 ? -num : num)); final int power = n - (int) d; final double magnitude = Math.pow(10, power); final long shifted = Math.round(num * magnitude); return shifted / magnitude; } public static interface TimeSource { public long currentTimeMillis(); } public TimeSource time = new SystemTimeSource(); public static class SystemTimeSource implements TimeSource { @Override public long currentTimeMillis() { return System.currentTimeMillis(); } } public static interface FilterCallback { public void callback(); } public FilterCallback cb = new DefaultFilterCallback(); public static class DefaultFilterCallback implements FilterCallback { @Override public void callback() { // Do nothing } } public boolean methodFilterRecordingEnabled = false; private Set<String> repeatedMethods = new HashSet<String>(); public void resetMethodFilter() { repeatedMethods = new HashSet<String>(); } }