/** * Copyright 2014 Alexey Ragozin * * 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. */ package org.gridkit.jvmtool.cmd; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.gridkit.jvmtool.GlobHelper; import org.gridkit.jvmtool.cli.CommandLauncher; import org.gridkit.jvmtool.cli.CommandLauncher.CmdRef; import org.gridkit.jvmtool.stacktrace.ReaderProxy; import org.gridkit.jvmtool.stacktrace.StackFrame; import org.gridkit.jvmtool.stacktrace.StackFrameArray; import org.gridkit.jvmtool.stacktrace.StackFrameList; import org.gridkit.jvmtool.stacktrace.StackTraceCodec; import org.gridkit.jvmtool.stacktrace.StackTraceReader; import org.gridkit.jvmtool.stacktrace.StackTraceWriter; import org.gridkit.jvmtool.stacktrace.ThreadSnapshot; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.beust.jcommander.ParametersDelegate; /** * Stack capture command. * * @author Alexey Ragozin (alexey.ragozin@gmail.com) */ public class StackDumpCopyCmd implements CmdRef { @Override public String getCommandName() { return "stcpy"; } @Override public Runnable newCommand(CommandLauncher host) { return new StCpy(host); } @Parameters(commandDescription = "[Stack Copy] Stack dump copy/filtering utility") public static class StCpy implements Runnable { @ParametersDelegate private CommandLauncher host; @Parameter(names = {"-i", "--input"}, description = "Input files", required = true, variableArity = true) private List<String> inputFiles = new ArrayList<String>(); @Parameter(names = {"-tf", "--thread-filter"}, description = "Filter threads by name (Java RegEx syntax)") private String threadFilter = ".*"; @Parameter(names = {"-e", "--empty"}, description = "Retain threads without stack trace in dump (ignored by default)") private boolean retainEmptyTraces = false; @Parameter(names = {"-m", "--match-frame"}, variableArity = true, description = "Frame filter, only traces conatining this string will be included to dump") private List<String> frameFilter; @Parameter(names = { "--mask" }, variableArity = true, description = "One or more masking rules. E.g. com.mycompany:com.somecomplany") private List<String> maskingRules = new ArrayList<String>(); @Parameter(names = {"-o", "--output"}, required = true, description = "Name of file to write thread dump") private String outputFile; @Parameter(names = {"-ss", "--subsample"}, required = false, description = "If below 1.0 some frames will be randomly throwen away. E.g. 0.1 - every 10th will be retained") private double subsample = 1d; private int traceCounter; private StackTraceWriter writer; private List<MaskRule> masking = new ArrayList<MaskRule>(); private Random rnd = new Random(1); public StCpy(CommandLauncher host) { this.host = host; } @Override public void run() { try { for(String rule: maskingRules) { String[] parts = rule.split("[:]"); if (parts.length != 2) { host.fail("Bad masking pattern [" + rule + "] should be int [match:replace] format"); } masking.add(new MaskRule(parts[0], parts[1])); } AntPathMatcher matcher = new AntPathMatcher(); matcher.setPathSeparator("/"); List<String> inputs = new ArrayList<String>(); System.out.println("Input files"); for(String f: inputFiles) { f = f.replace('\\', '/'); for(File ff: matcher.findFiles(new File("."), f)) { if (ff.isFile()) { inputs.add(ff.getPath()); System.out.println(" " + ff.getPath()); } } } System.out.println(); if (inputs.isEmpty()) { host.fail("Input file list is empty"); } openWriter(); final StackTraceReader rawReader = StackTraceCodec.newReader(inputs.toArray(new String[0])); StackTraceReader reader = new StackTraceReader.StackTraceReaderDelegate() { @Override protected StackTraceReader getReader() { return rawReader; } @Override public boolean loadNext() throws IOException { try { return super.loadNext(); } catch(IOException e) { System.err.println("Dump file read error: " + e.toString()); return false; } } }; if (!reader.isLoaded()) { reader.loadNext(); } ReaderProxy proxy = new ReaderProxy(reader) { @Override public StackFrameList stackTrace() { return mask(reader.getStackTrace()); } }; StackWriterProxy writerProxy = new StackWriterProxy(); while(reader.isLoaded()) { writerProxy.write(proxy); reader.loadNext(); } System.out.println(traceCounter + " traces written"); writer.close(); } catch (Exception e) { host.fail("Unexpected error: " + e.toString(), e); } } private class StackWriterProxy implements StackTraceWriter { private Map<String, Boolean> nameCache = new HashMap<String, Boolean>(); private Map<StackFrame, Boolean> elementCache = new HashMap<StackFrame, Boolean>(); private Matcher[] matchers; public StackWriterProxy() { if (frameFilter != null) { matchers = new Matcher[frameFilter.size()]; for(int i = 0; i != frameFilter.size(); ++i) { matchers[i] = GlobHelper.translate(frameFilter.get(i), ".").matcher(""); } } } @Override public void write(ThreadSnapshot snap) throws IOException { if (snap.stackTrace().isEmpty() && !retainEmptyTraces) { return; } if (rnd.nextDouble() > subsample) { // ignore sample return; } // thread name filter if (threadFilter != null) { String tn = snap.threadName(); tn = tn != null ? tn : ""; Boolean r = nameCache.get(tn); if (r == null) { r = Pattern.matches(threadFilter, tn); nameCache.put(tn, r); } if (!r.booleanValue()) { return; } } // test filter if (frameFilter != null) { boolean match = false; for(StackFrame e: snap.stackTrace()) { if (match(e)) { match = true; break; } } if (!match) { return; } } ++traceCounter; writer.write(snap); } private boolean match(StackFrame e) { Boolean cached = elementCache.get(e); if (cached == null) { if (elementCache.size() > 4 << 10) { elementCache.clear(); } boolean matched = false; for(Matcher m: matchers) { m.reset(e.toString()); if (m.lookingAt()) { matched = true; break; } } elementCache.put(e, matched); return matched; } return cached; } @Override public void close() { writer.close(); } } private void openWriter() throws FileNotFoundException, IOException { File file = new File(outputFile); if (file.getParentFile() != null) { file.getParentFile().mkdirs(); } writer = StackTraceCodec.newWriter(new FileOutputStream(file)); System.out.println("Writing to " + file.getAbsolutePath()); } private StackFrameList mask(StackFrameList stackTrace) { if (maskingRules.isEmpty()) { return stackTrace; } else { StackFrame[] frames = stackTrace.toArray(); for(int i = 0; i != frames.length; ++i) { frames[i] = mask(frames[i]); } return new StackFrameArray(frames); } } private StackFrame mask(StackFrame stackFrame) { for(MaskRule rule: masking) { if (stackFrame.getClassName().startsWith(rule.match)) { String cn = stackFrame.getClassName(); String nn = rule.replace + cn.substring(rule.match.length()); StackFrame ff = new StackFrame("", nn, stackFrame.getMethodName(), stackFrame.getSourceFile(), stackFrame.getLineNumber()); return ff; } } return stackFrame; } } static class MaskRule { String match; String replace; public MaskRule(String match, String replace) { this.match = match; this.replace = replace; } } }