性能测试脚本的编写和调试
以前,我曾说过, 您总是想保留一些调试器断点作为例外 。 这有助于防止代码在不注意的情况下腐烂掉,有时掩盖了另一个问题。
如果您认真对待这一点,那么最好将此想法扩展到自动化测试中。 但是想出一个全面的解决方案并不简单。 您可以仅从try / catch开始,但不会捕获其他线程上的异常。 您也可以使用AOP进行操作; 但是,根据框架的不同,您不能保证完全捕获所有内容,这确实意味着您正在使用稍微不同的代码进行测试,这将使您有些担心。
几天前,我遇到了有关如何编写自己的调试器的博客文章,我想知道java进程是否有可能自行调试。 事实证明,可以的,这是我作为这个小小的思想实验的一部分提出的代码。
该类的第一部分仅包含一些相当hacky的代码,用于根据启动参数来猜测连接回同一VM所需的端口。 可能可以使用Attach机制启动调试器。 但是我没有看到一种明显的方法来使其工作。 然后只有几个工厂方法带有要查找的异常列表。
package com.kingsfleet.debug;
import com.sun.jdi.Bootstrap;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.connect.AttachingConnector;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.IllegalConnectorArgumentsException;
import com.sun.jdi.event.ClassPrepareEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventQueue;
import com.sun.jdi.event.EventSet;
import com.sun.jdi.event.ExceptionEvent;
import com.sun.jdi.event.VMDeathEvent;
import com.sun.jdi.event.VMDisconnectEvent;
import com.sun.jdi.request.ClassPrepareRequest;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.ExceptionRequest;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class ExceptionDebugger implements AutoCloseable {
public static int getDebuggerPort() {
// Try to work out what port we need to connect to
RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
List<String> inputArguments = runtime.getInputArguments();
int port = -1;
boolean isjdwp = false;
for (String next : inputArguments) {
if (next.startsWith("-agentlib:jdwp=")) {
isjdwp = true;
String parameterString = next.substring("-agentlib:jdwp=".length());
String[] parameters = parameterString.split(",");
for (String parameter : parameters) {
if (parameter.startsWith("address")) {
int portDelimeter = parameter.lastIndexOf(":");
if (portDelimeter != -1) {
port = Integer.parseInt(parameter.substring(portDelimeter + 1));
} else {
port = Integer.parseInt(parameter.split("=")[1]);
}
}
}
}
}
return port;
}
public static ExceptionDebugger connect(final String... exceptions) throws InterruptedException {
return connect(getDebuggerPort(),exceptions);
}
public static ExceptionDebugger connect(final int port, final String... exceptions) throws InterruptedException {
ExceptionDebugger ed = new ExceptionDebugger(port, exceptions);
return ed;
}
构造函数创建一个简单的守护程序线程,以启动与虚拟机的连接。 非常重要的是,这是一个单独的线程,否则很明显,当我们遇到断点时,VM会停止运行。 确保该线程中的代码不会引发异常是一个好主意-目前,我只是希望做到最好。
最后,代码仅维护了一个禁止的异常列表,如果您有更多的时间,应该可以将堆栈跟踪存储在发生异常的位置。
//
// Instance variables
private final CountDownLatch startupLatch = new CountDownLatch(1);
private final CountDownLatch shutdownLatch = new CountDownLatch(1);
private final Set<String> set = Collections.synchronizedSet(new HashSet<String>());
private final int port;
private final String exceptions[];
private Thread debugger;
private volatile boolean shutdown = false;
//
// Object construction and methods
//
private ExceptionDebugger(final int port, final String... exceptions) throws InterruptedException {
this.port = port;
this.exceptions = exceptions;
debugger = new Thread(new Runnable() {
@Override
public void run() {
try {
connect();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}, "Self debugging");
debugger.setDaemon(true); // Don't hold the VM open
debugger.start();
// Make sure the debugger has connected
if (!startupLatch.await(1, TimeUnit.MINUTES)) {
throw new IllegalStateException("Didn't connect before timeout");
}
}
@Override
public void close() throws InterruptedException {
shutdown = true;
// Somewhere in JDI the interrupt was being eaten, hence the volatile flag
debugger.interrupt();
shutdownLatch.await();
}
/**
* @return A list of exceptions that were thrown
*/
public Set<String> getExceptionsViolated() {
return new HashSet<String>(set);
}
/**
* Clear the list of exceptions violated
*/
public void clearExceptionsViolated() {
set.clear();
}
主要的连接方法是一个相当简单的代码块,可确保连接并配置任何初始断点。
//
// Implementation details
//
private void connect() throws java.io.IOException {
try {
// Create a virtual machine connection
VirtualMachine attach = connectToVM();
try
{
// Add prepare and any already loaded exception breakpoints
createInitialBreakpoints(attach);
// We can now allow the rest of the work to go on as we have created the breakpoints
// we required
startupLatch.countDown();
// Process the events
processEvents(attach);
}
finally {
// Disconnect the debugger
attach.dispose();
// Give the debugger time to really disconnect
// before we might reconnect, couldn't find another
// way to do this
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} finally {
// Notify watchers that we have shutdown
shutdownLatch.countDown();
}
}
重新连接到自我只是找到合适的连接器的过程,在本例中是Socket,尽管我猜想如果您稍稍修改一下代码,便可以在某些平台上使用共享内存传输。
private VirtualMachine connectToVM() throws java.io.IOException {
List<AttachingConnector> attachingConnectors = Bootstrap.virtualMachineManager().attachingConnectors();
AttachingConnector ac = null;
found:
for (AttachingConnector next : attachingConnectors) {
if (next.name().contains("SocketAttach")) {
ac = next;
break;
}
}
Map<String, Connector.Argument> arguments = ac.defaultArguments();
arguments.get("hostname").setValue("localhost");
arguments.get("port").setValue(Integer.toString(port));
arguments.get("timeout").setValue("4000");
try {
return ac.attach(arguments);
} catch (IllegalConnectorArgumentsException e) {
throw new IOException("Problem connecting to debugger",e);
}
}
连接调试器时,您不知道是否已加载您感兴趣的异常,因此您需要为准备类的点和已经加载的点注册断点。
请注意,设置的断点仅用于中断一个线程的策略–否则,出于显而易见的原因,如果调试器线程也进入睡眠状态,则当前VM将停止运行。
private void createInitialBreakpoints(VirtualMachine attach) {
// Our first exception is for class loading
for (String exception : exceptions) {
ClassPrepareRequest cpr = attach.eventRequestManager().createClassPrepareRequest();
cpr.addClassFilter(exception);
cpr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
cpr.setEnabled(true);
}
// Then we can check each in turn to see if it have already been loaded as we might
// be late to the game, remember classes can be loaded more than once
//
for (String exception : exceptions) {
List<ReferenceType> types = attach.classesByName(exception);
for (ReferenceType type : types) {
createExceptionRequest(attach, type);
}
}
}
private static void createExceptionRequest(VirtualMachine attach,
ReferenceType refType) {
ExceptionRequest er = attach.eventRequestManager().createExceptionRequest(
refType, true, true);
er.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
er.setEnabled(true);
}
事件处理循环轮询包含一个或多个事件实例的EventSet实例。 尽管并非所有这些事件都属于断点请求,所以您必须注意不要总是在事件集上调用简历。 这是因为您可能连续有两个事件集,而代码甚至在阅读第二个事件集之前就调用了resume。 随着代码的赶上,这会导致错过断点。
由于某种原因, JDI似乎正在吃掉中断的标志,因此该布尔属性使用以前的close方法停止循环。
private void processEvents(VirtualMachine attach) {
// Listen for events
EventQueue eq = attach.eventQueue();
eventLoop: while (!Thread.interrupted() && !shutdown) {
// Poll for event sets, with a short timeout so that we can
// be interrupted if required
EventSet eventSet = null;
try
{
eventSet = eq.remove(500);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
continue eventLoop;
}
// Just loop again if we have no events
if (eventSet == null) {
continue eventLoop;
}
//
boolean resume = false;
for (Event event : eventSet) {
EventRequest request = event.request();
if (request != null) {
int eventPolicy = request.suspendPolicy();
resume |= eventPolicy != EventRequest.SUSPEND_NONE;
}
if (event instanceof VMDeathEvent || event instanceof VMDisconnectEvent) {
// This should never happen as the VM will exit before this is called
} else if (event instanceof ClassPrepareEvent) {
// When an instance of the exception class is loaded attach an exception breakpoint
ClassPrepareEvent cpe = (ClassPrepareEvent) event;
ReferenceType refType = cpe.referenceType();
createExceptionRequest(attach, refType);
} else if (event instanceof ExceptionEvent) {
String name = ((ExceptionRequest)event.request()).exception().name();
set.add(name);
}
}
// Dangerous to call resume always because not all event suspend the VM
// and events happen asynchornously.
if (resume)
eventSet.resume();
}
}
}
因此,剩下的只是一个简单的测试示例,因为这是JDK 7,而ExceptionDebugger是AutoCloseable,所以我们可以使用try-with-resources构造进行此操作,如下所示。 显然,如果要进行自动化测试,请使用您选择的测试框架固定装置。
public class Target {
public static void main(String[] args) throws InterruptedException {
try (ExceptionDebugger ex = ExceptionDebugger.connect(
NoClassDefFoundError.class.getName())) {
doSomeWorkThatQuietlyThrowsAnException();
System.out.println(ex.getExceptionsViolated());
}
System.exit(0);
}
private static void doSomeWorkThatQuietlyThrowsAnException() {
// Check to see that break point gets fired
try {
Thread t = new Thread(new Runnable() {
public void run() {
try
{
throw new NoClassDefFoundError();
}
catch (Throwable ex) {
}
}
});
t.start();
t.join();
} catch (Throwable th) {
// Eat this and don't tell anybody
}
}
}
因此,如果使用以下VM参数运行此类,请注意suspend = n,否则代码将不会开始运行,您会发现它可以重新连接到自身并开始运行。
-agentlib:jdwp=transport=dt_socket,address=localhost:5656,server=y,suspend=n
这将为您提供以下输出,请注意来自VM的额外调试行:
Listening for transport dt_socket at address: 5656
java.lang.NoClassDefFoundError
Listening for transport dt_socket at address: 5656
每个人都想读一下这是否对人们有用并有助于消除任何明显的错误。
性能测试脚本的编写和调试