Jmeter源码系列(5)-JmeterEngine-Jmeter的执行引擎

导读

在前面的几篇文章中,笔者主要分析了Jmeter在Non-Gui模式下的启动过程,知道了jmeter在启动过程中会根据启动参数做各种初始化动作,最后通过以下代码来开始测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (!remoteStart) {
JMeterEngine engine = new StandardJMeterEngine();
clonedTree.add(clonedTree.getArray()[0], new ListenToTest(org.apache.jmeter.JMeter.ListenToTest.RunMode.LOCAL, false, reportGenerator));
engine.configure(clonedTree);
Instant now = Instant.now();
println("Starting standalone test @ " + formatLikeDate(now) + " (" + now.toEpochMilli() + ')');
engines.add(engine);
engine.runTest();
} else {
java.util.StringTokenizer st = new java.util.StringTokenizer(remoteHostsString.trim(), ",");//$NON-NLS-1$
List<String> hosts = new ArrayList<>();
while (st.hasMoreElements()) {
hosts.add(((String) st.nextElement()).trim());
}
ListenToTest testListener = new ListenToTest(org.apache.jmeter.JMeter.ListenToTest.RunMode.REMOTE, remoteStop, reportGenerator);
clonedTree.add(clonedTree.getArray()[0], testListener);
DistributedRunner distributedRunner = new DistributedRunner(this.remoteProps);
distributedRunner.setStdout(System.out); // NOSONAR
distributedRunner.setStdErr(System.err); // NOSONAR
distributedRunner.init(hosts, clonedTree);
engines.addAll(distributedRunner.getEngines());
testListener.setStartedRemoteEngines(engines);
distributedRunner.start();
}
startUdpDdaemon(engines);

在上面的代码中可以看到,如果是本地执行,则创建一个 StandardJMeterEngine 实例,并调用其 configure 和 runTest 方法来执行测试计划。如果是远程执行,则创建一个 DistributedRunner 实例,并调用其 init 和 start 方法来启动远程引擎并执行测试计划。

JMeterEngine

JMeterEngine是Jmeter的执行引擎,它负责执行测试计划,并管理测试计划中的各种组件,如线程组、采样器、监听器等。

JMeterEngine接口定义了以下方法:

  • void configure(HashTree testPlan):配置测试计划,将测试计划中的各种组件添加到执行引擎中。
  • void runTest() throws JMeterEngineException:运行测试计划。
  • default void stopTest() {stopTest(true);}:立即停止测试计划。
  • void stopTest(boolean now): 停止测试计划,如果now为true,则立即停止测试计划,否则等待测试计划完成。
  • void reset():重置执行引擎,清除所有测试计划中的组件。
  • void setProperties(Properties p): 设置执行引擎的属性。
  • void exit(): 退出执行引擎。
  • boolean isActive(): 判断执行引擎是否正在运行。

JMeterEngine接口的实现类有StandardJMeterEngineClientJMeterEngine,其实大家在看Jmeter源代码时,可能会发现还有一个EmulatorEngine类实现了JMeterEngine, 这个类其实位于org.apache.jmeter.engine.DistributedRunnerTest.EmulatorEngine 是一个用于测试的类,并非是Jmeter的正式实现。

StandardJMeterEngine

StandardJMeterEngine 是 JMeter 的标准引擎实现,主要用于在单独的 JMeter 服务器或本地机器上执行性能测试。类定义如下

1
public class StandardJMeterEngine implements JMeterEngine, Runnable

StandardJMeterEngine 除了实现 JmeterEngine 接口外,还实现了 Runnable 接口,因此它可以被提交到一个线程池中执行。接下来,我们可以从engine.configure(clonedTree)engine.runTest() 为入口来分析 StandardJMeterEngine 的执行过程。

configure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void configure(HashTree testTree) {
// Is testplan serialised?
SearchByClass<TestPlan> testPlan = new SearchByClass<>(TestPlan.class);
testTree.traverse(testPlan);
Object[] plan = testPlan.getSearchResults().toArray();
if (plan.length == 0) {
throw new IllegalStateException("Could not find the TestPlan class!");
}
TestPlan tp = (TestPlan) plan[0];
serialized = tp.isSerialized();
tearDownOnShutdown = tp.isTearDownOnShutdown();
active = true;
test = testTree;
}

configure方法执行过程如下:

  1. 从testTree中查找TestPlan对象,如果找不到,则抛出异常。testTree.traverse(testPlan) 的作用是遍历 HashTree 对象中的内容,并根据提供的 SearchByClass 对象对每个节点进行检查或搜索。其实HashTree#traverse使用了访问者模式,SearchByClass类实现了HashTreeTraverser接口,通过实现此接口,类可以轻松遍历 HashTree 对象,并通过某些事件的回调获得通知。
  2. 获取TestPlan对象的serialized和tearDownOnShutdown属性,并设置到StandardJMeterEngine对象中。
  3. 设置StandardJMeterEngine对象的active属性为true,表示引擎已经准备好执行测试计划。
  4. 将testTree赋值给StandardJMeterEngine对象的test属性,以便在执行测试计划时使用。

runTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void runTest() throws JMeterEngineException {
if (host != null){
Instant now = Instant.now();
String nowAsString = formatLikeDate(now);
System.out.println("Starting the test on host " // NOSONAR Intentional
+ host + " @ " + nowAsString + " (" + now.toEpochMilli() + ')');
}
try {
runningTest = EXECUTOR_SERVICE.submit(this);
} catch (Exception err) {
stopTest();
throw new JMeterEngineException(err);
}
}

runTest方法执行过程如下:

  1. 如果StandardJMeterEngine对象的host属性不为null,则打印一条日志,记录测试开始的时间和主机信息。
  2. 调用EXECUTOR_SERVICE.submit(this)方法将StandardJMeterEngine对象提交到一个线程池中执行。EXECUTOR_SERVICE是一个线程池,用于执行StandardJMeterEngine对象的run方法。线程池定义如下
1
2
3
4
5
6
7
8
9
10
11
12
// 线程安全的计数器,用于在创建线程时生成唯一的线程名
private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(0);
/**
* 执行器服务,用于执行“启动测试”、“停止测试”等管理任务。使用 ExecutorService 允许从线程传播异常。线程保持alive时间设置为 1 秒,因此线程会提前释放,因此应用程序可以更快地关闭。
*/
private static final ExecutorService EXECUTOR_SERVICE = new ThreadPoolExecutor(
0,
Integer.MAX_VALUE,
1L,
TimeUnit.SECONDS,
new java.util.concurrent.SynchronousQueue<>(),
(runnable) -> new Thread(runnable, "StandardJMeterEngine-" + THREAD_COUNTER.incrementAndGet()));
  1. 如果提交线程池失败,则调用stopTest方法停止测试,并抛出JMeterEngineException异常。

至此,StandardJMeterEngine对象的runTest方法执行完毕,测试计划开始执行。那接下来我们就需要看下StandardJMeterEngine对象的run方法了。

run

先贴一个完整方法定义,然后再逐步分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@Override
public void run() {
log.info("Running the test!");
running = true;
/*
* Ensure that the sample variables are correctly initialised for each run.
*/
SampleEvent.initSampleVariables();
JMeterContextService.startTest();
try {
PreCompiler compiler = new PreCompiler();
test.traverse(compiler);
} catch (RuntimeException e) {
log.error("Error occurred compiling the tree:", e);
JMeterUtils.reportErrorToUser("Error occurred compiling the tree: - see log file", e);
return; // no point continuing
}
/*
* Notification of test listeners needs to happen after function
* replacement, but before setting RunningVersion to true.
*/
SearchByClass<TestStateListener> testListeners = new SearchByClass<>(TestStateListener.class); // TL - S&E
test.traverse(testListeners);
// Merge in any additional test listeners
// currently only used by the function parser
testListeners.getSearchResults().addAll(testList);
testList.clear(); // no longer needed
test.traverse(new TurnElementsOn());
notifyTestListenersOfStart(testListeners);
List<?> testLevelElements = new ArrayList<>(test.list(test.getArray()[0]));
removeThreadGroups(testLevelElements);
SearchByClass<SetupThreadGroup> setupSearcher = new SearchByClass<>(SetupThreadGroup.class);
SearchByClass<AbstractThreadGroup> searcher = new SearchByClass<>(AbstractThreadGroup.class);
SearchByClass<PostThreadGroup> postSearcher = new SearchByClass<>(PostThreadGroup.class);
test.traverse(setupSearcher);
test.traverse(searcher);
test.traverse(postSearcher);
TestCompiler.initialize();
// for each thread group, generate threads
// hand each thread the sampler controller
// and the listeners, and the timer
Iterator<SetupThreadGroup> setupIter = setupSearcher.getSearchResults().iterator();
Iterator<AbstractThreadGroup> iter = searcher.getSearchResults().iterator();
Iterator<PostThreadGroup> postIter = postSearcher.getSearchResults().iterator();
ListenerNotifier notifier = new ListenerNotifier();
int groupCount = 0;
JMeterContextService.clearTotalThreads();
if (setupIter.hasNext()) {
log.info("Starting setUp thread groups");
while (running && setupIter.hasNext()) {//for each setup thread group
AbstractThreadGroup group = setupIter.next();
groupCount++;
String groupName = group.getName();
log.info("Starting setUp ThreadGroup: {} : {} ", groupCount, groupName);
startThreadGroup(group, groupCount, setupSearcher, testLevelElements, notifier);
if (serialized && setupIter.hasNext()) {
log.info("Waiting for setup thread group: {} to finish before starting next setup group",
groupName);
group.waitThreadsStopped();
}
}
log.info("Waiting for all setup thread groups to exit");
//wait for all Setup Threads To Exit
waitThreadsStopped();
log.info("All Setup Threads have ended");
groupCount = 0;
JMeterContextService.clearTotalThreads();
}
groups.clear(); // The groups have all completed now
/*
* Here's where the test really starts. Run a Full GC now: it's no harm
* at all (just delays test start by a tiny amount) and hitting one too
* early in the test can impair results for short tests.
*/
JMeterUtils.helpGC();
JMeterContextService.getContext().setSamplingStarted(true);
boolean mainGroups = running; // still running at this point, i.e. setUp was not cancelled
while (running && iter.hasNext()) {// for each thread group
AbstractThreadGroup group = iter.next();
//ignore Setup and Post here. We could have filtered the searcher. but then
//future Thread Group objects wouldn't execute.
if (group instanceof SetupThreadGroup ||
group instanceof PostThreadGroup) {
continue;
}
groupCount++;
String groupName = group.getName();
log.info("Starting ThreadGroup: {} : {}", groupCount, groupName);
startThreadGroup(group, groupCount, searcher, testLevelElements, notifier);
if (serialized && iter.hasNext()) {
log.info("Waiting for thread group: {} to finish before starting next group", groupName);
group.waitThreadsStopped();
}
} // end of thread groups
if (groupCount == 0) { // No TGs found
log.info("No enabled thread groups found");
} else {
if (running) {
log.info("All thread groups have been started");
} else {
log.info("Test stopped - no more thread groups will be started");
}
}
//wait for all Test Threads To Exit
waitThreadsStopped();
groups.clear(); // The groups have all completed now
if (postIter.hasNext()) {
groupCount = 0;
JMeterContextService.clearTotalThreads();
log.info("Starting tearDown thread groups");
if (mainGroups && !running) { // i.e. shutdown/stopped during main thread groups
running = tearDownOnShutdown; // re-enable for tearDown if necessary
}
while (running && postIter.hasNext()) {//for each setup thread group
AbstractThreadGroup group = postIter.next();
groupCount++;
String groupName = group.getName();
log.info("Starting tearDown ThreadGroup: {} : {}", groupCount, groupName);
startThreadGroup(group, groupCount, postSearcher, testLevelElements, notifier);
if (serialized && postIter.hasNext()) {
log.info("Waiting for post thread group: {} to finish before starting next post group", groupName);
group.waitThreadsStopped();
}
}
waitThreadsStopped(); // wait for Post threads to stop
}
notifyTestListenersOfEnd(testListeners);
JMeterContextService.endTest();
if (JMeter.isNonGUI() && SYSTEM_EXIT_FORCED) {
log.info("Forced JVM shutdown requested at end of test");
System.exit(0); // NOSONAR Intentional
}
}

  1. 初始化与日志记录:
1
2
log.info("Running the test!");
running = true;

测试开始,并且设置一个标志表明测试正在运行。

  1. 采样器变量初始化
1
2
3
4
5
6
7
8
9
SampleEvent.initSampleVariables();

public static void initSampleVariables() {
String vars = JMeterUtils.getProperty(SAMPLE_VARIABLES);
variableNames = vars != null ? vars.split(",") : new String[0];
if (log.isInfoEnabled()) {
log.info("List of sample_variables: {}", Arrays.toString(variableNames));
}
}

初始化采样器变量,这些变量将在测试期间用于存储和访问采样数据,

  1. JMeterContextService 标记测试开始
1
2
3
4
5
6
7
8
9
JMeterContextService.startTest();

public static synchronized void startTest() {
if (testStart.get() == 0) {
NUMBER_OF_ACTIVE_THREADS.set(0);
testStart.set(System.currentTimeMillis());
JMeterUtils.setProperty("TESTSTART.MS", Long.toString(testStart.get()));// $NON-NLS-1$
}
}

这个同步的静态方法作用如下:

  • 检查测试是否已经开始:
    使用原子变量 testStart(类型为 AtomicLong)来检查测试是否已经开始了。如果 testStart 的值为0,说明测试尚未开始。其实在测试结束后,也会通过调用 JMeterContextService.endTest() 方法将 testStart 的值重置为0。
  • 设置活跃线程数:
    将 NUMBER_OF_ACTIVE_THREADS(也是 AtomicInteger 类型)设置为0,这通常用于跟踪活跃的线程数量。在测试开始时将其重置,以便正确地计数参与测试的线程。
  • 记录测试开始时间:
    设置 testStart 的值为当前时间戳(毫秒级)。这提供了测试开始的基准时间,对于分析测试结果和监控性能指标非常有用。
  • 保存测试开始时间到属性:
    将测试开始的时间戳保存到 JMeter 属性中,键为 “TESTSTART.MS”。这使得测试开始时间可以在测试过程中被其他部分引用,例如在日志记录或结果报告中。

通过调用 startTest() 方法,JMeter 能够确保每个测试运行都有一个明确的起点,这对于统计和报告测试期间的性能指标非常重要。在多线程环境中,由于该方法被声明为 synchronized,可以保证即使有多个线程同时尝试调用它,也只会有一个线程能够进入方法体,从而避免了并发修改 testStart 或 NUMBER_OF_ACTIVE_THREADS 变量可能引发的问题。

  1. HashTree预编译
1
2
3
4
5
6
7
8
try {
PreCompiler compiler = new PreCompiler();
test.traverse(compiler);
} catch (RuntimeException e) {
log.error("Error occurred compiling the tree:", e);
JMeterUtils.reportErrorToUser("Error occurred compiling the tree: - see log file", e);
return; // no point continuing
}

虽然代码就两三行,但是里面的信息量是非常大的,HashTree 是 JMeter 的核心数据结构,它存储了所有测试组件的配置信息,包括线程组、采样器、监听器等。在测试开始之前,JMeter 会遍历整个 HashTree,对每个组件进行预编译,以便在测试运行时能够快速地访问和执行这些组件。预编译的过程其实就是变量替换的过程,它将配置文件中的变量替换为实际的值,以便在测试运行时能够正确地使用这些变量。预编译是 JMeter 性能优化的关键之一,因为它可以减少测试运行时的计算量,提高测试的执行效率。然而,需要注意的是,某些动态值(如随机数或基于时间的值或参数化的变量等)在预编译时只能计算一次,这意味着如果需要在每次采样时都有不同的值,那么这些函数将不能完全预编译,而是在每次采样时重新计算。预编译会调用org.apache.jmeter.engine.PreCompiler#addNode 方法,其中涉及到一个非常重要的类,叫做 org.apache.jmeter.engine.util.ValueReplacer,变量替换就由它实现。由于篇幅有限,不在此处展开讲变量替换的过程,在后面讲Jmeter核心类的文章中会讲解此类。此处我们只要知道这段代码做什么就行了。

  1. 通知监听器
1
2
3
4
5
6
7
8
9
10
11
12
/*
* Notification of test listeners needs to happen after function
* replacement, but before setting RunningVersion to true.
*/
SearchByClass<TestStateListener> testListeners = new SearchByClass<>(TestStateListener.class); // TL - S&E
test.traverse(testListeners);
// Merge in any additional test listeners
// currently only used by the function parser
testListeners.getSearchResults().addAll(testList);
testList.clear(); // no longer needed
test.traverse(new TurnElementsOn());
notifyTestListenersOfStart(testListeners);

遍历测试计划树,收集所有的org.apache.jmeter.testelement.TestStateListener实例,并激活所有测试元素,准备它们的运行状态,然后通知这些监听器(实现了TestStateListener接口的类)测试即将开始。TestStateListener 是 JMeter 中用于监听测试状态变化的接口,它定义了在测试开始、结束以及每个采样周期开始和结束时需要执行的方法。通过调用 notifyTestListenersOfStart() 方法,JMeter 可以确保所有注册的 TestStateListener 都能够接收到测试开始的通知,并执行相应的操作。发送测试开始通知的方法是 org.apache.jmeter.engine.StandardJMeterEngine#notifyTestListenersOfStart

  1. 准备测试元素
1
2
3
4
5
6
7
8
9
List<?> testLevelElements = new ArrayList<>(test.list(test.getArray()[0]));
removeThreadGroups(testLevelElements);
SearchByClass<SetupThreadGroup> setupSearcher = new SearchByClass<>(SetupThreadGroup.class);
SearchByClass<AbstractThreadGroup> searcher = new SearchByClass<>(AbstractThreadGroup.class);
SearchByClass<PostThreadGroup> postSearcher = new SearchByClass<>(PostThreadGroup.class);
test.traverse(setupSearcher);
test.traverse(searcher);
test.traverse(postSearcher);
TestCompiler.initialize();
  • 这里首先获取测试计划的顶层元素列表。test 对象代表的是测试计划 (TestPlan),test.getArray()[0] 获取的是测试计划数组中的第一个元素,通常这是测试计划的根元素。然后通过调用 list() 方法获取与该元素关联的所有子元素,并将这些子元素放入一个 ArrayList 中。由于类型不确定,所以列表的类型声明为 List<?>。
  • 接下来,调用 removeThreadGroups() 方法从列表中移除所有线程组。线程组是 JMeter 中用于控制测试并发性的组件,它定义了测试中并发线程的数量以及每个线程的行为。在测试计划中,线程组通常位于顶层元素之下,因此需要从顶层元素列表中移除它们,以便后续的遍历操作不会重复处理线程组。
  • 然后,创建三个 SearchByClass 对象,分别用于搜索 SetupThreadGroup、AbstractThreadGroup 和 PostThreadGroup 类型的元素。这些类都是 JMeter 中用于控制测试设置和清理的组件,它们定义了在测试开始之前和结束之后需要执行的操作。
  • 接下来,调用 test.traverse() 方法遍历测试计划树,将 SetupThreadGroup 类型的元素添加到 setupSearcher 的结果列表中,将 AbstractThreadGroup 类型的元素添加到 searcher 的结果列表中,将 PostThreadGroup 类型的元素添加到 postSearcher 的结果列表中。
  • 最后,调用 TestCompiler.initialize() 方法初始化测试编译器。测试编译器是 JMeter 中用于将测试计划转换为可执行代码的组件,它将测试计划中的元素转换为相应的 Java 代码,以便在测试执行期间执行。初始化测试编译器是执行测试之前的必要步骤,它确保测试编译器已经准备好处理测试计划中的元素。划重点:TestCompiler.initialize() 方法是 JMeter 中用于初始化测试编译器的静态方法,它将测试计划中的元素转换为相应的 Java 代码,以便在测试执行期间执行。初始化测试编译器是执行测试之前的必要步骤,它确保测试编译器已经准备好处理测试计划中的元素。大家如果有兴趣,可以使用文本工具打开一个jmx文件看下,里面ThreadGroup标签有两个属性guiClasstestclass 在<jmeter安装路径>/bin/saveservice.properties 文件中就定义了这些标签名和全限定类名的映射关系,当然,其他组件也是类似。
  1. 准备线程组,开始预热
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Iterator<SetupThreadGroup> setupIter = setupSearcher.getSearchResults().iterator();
Iterator<AbstractThreadGroup> iter = searcher.getSearchResults().iterator();
Iterator<PostThreadGroup> postIter = postSearcher.getSearchResults().iterator();
ListenerNotifier notifier = new ListenerNotifier();
int groupCount = 0;
JMeterContextService.clearTotalThreads();
if (setupIter.hasNext()) {
log.info("Starting setUp thread groups");
while (running && setupIter.hasNext()) {//for each setup thread group
AbstractThreadGroup group = setupIter.next();
groupCount++;
String groupName = group.getName();
log.info("Starting setUp ThreadGroup: {} : {} ", groupCount, groupName);
startThreadGroup(group, groupCount, setupSearcher, testLevelElements, notifier);
if (serialized && setupIter.hasNext()) {
log.info("Waiting for setup thread group: {} to finish before starting next setup group",
groupName);
group.waitThreadsStopped();
}
}
log.info("Waiting for all setup thread groups to exit");
//wait for all Setup Threads To Exit
waitThreadsStopped();
log.info("All Setup Threads have ended");
groupCount = 0;
JMeterContextService.clearTotalThreads();
}
groups.clear();
  • 首先,创建三个 Iterator 对象,分别用于遍历 setupSearcher、searcher 和 postSearcher 的结果列表。这些 Iterator 对象用于遍历测试计划树中找到的 SetupThreadGroup、AbstractThreadGroup 和 PostThreadGroup 类型的元素。
  • 接下来,创建一个 ListenerNotifier 对象,用于通知监听器测试执行的状态。ListenerNotifier 是 JMeter 中用于通知监听器测试执行状态的组件,它将测试执行的状态信息传递给监听器,以便它们可以执行相应的操作。在测试执行期间,ListenerNotifier 对象会不断更新测试执行的状态,并将状态信息传递给监听器。
  • 然后,获取测试计划树中找到的线程组的数量,并将其存储在 groupCount 变量中。groupCount 变量用于记录测试计划树中找到的线程组的数量,以便在测试执行期间进行计数。
  • 接下来,调用 JMeterContextService.clearTotalThreads() 方法清除 JMeter 上下文中的总线程数。JMeterContextService 是 JMeter 中用于管理 JMeter 上下文的组件,它提供了许多与 JMeter 上下文相关的操作,例如获取和设置上下文属性、获取和设置上下文中的线程数等。调用 JMeterContextService.clearTotalThreads() 方法可以清除 JMeter 上下文中的总线程数,以便在测试执行期间重新计算总线程数。
  • 然后,判断是否需要执行 setup 线程组。如果有下一个 setup 线程组,则进入 while 循环,循环执行 setup 线程组的启动操作。SetupThreadGroup 通常用于在测试开始前执行一些预处理任务,如数据库连接、服务器预热等。
  • 如果没有线程组,或者setup线程组执行完了,则调用groups.clear()清理groups集合。
  1. 执行前,扫个地
1
2
3
4
5
JMeterUtils.helpGC();
public static void helpGC() {
System.gc(); // NOSONAR Intentional
System.runFinalization();
}

jmeter真正开始执行前,调用System.gc()System.runFinalization()方法,以帮助 JVM 进行垃圾回收和运行终结器。这些方法可以减少内存泄漏和垃圾堆积的问题,从而提高测试的稳定性和性能。

  1. 开始执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
JMeterContextService.getContext().setSamplingStarted(true);
boolean mainGroups = running; // still running at this point, i.e. setUp was not cancelled
while (running && iter.hasNext()) {// for each thread group
AbstractThreadGroup group = iter.next();
//ignore Setup and Post here. We could have filtered the searcher. but then
//future Thread Group objects wouldn't execute.
if (group instanceof SetupThreadGroup || group instanceof PostThreadGroup) {
continue;
}
groupCount++;
String groupName = group.getName();
log.info("Starting ThreadGroup: {} : {}", groupCount, groupName);
startThreadGroup(group, groupCount, searcher, testLevelElements, notifier);
if (serialized && iter.hasNext()) {
log.info("Waiting for thread group: {} to finish before starting next group", groupName);
group.waitThreadsStopped();
}
} // end of thread groups
if (groupCount == 0) { // No TGs found
log.info("No enabled thread groups found");
} else {
if (running) {
log.info("All thread groups have been started");
} else {
log.info("Test stopped - no more thread groups will be started");
}
}//wait for all Test Threads To Exit
waitThreadsStopped();
groups.clear(); // The groups have all completed now

这段代码用于真正的拉起线程组(非 setup/post 线程组)执行测试,在测试线程组开始执行前,JMeterContextService 会设置采样器状态为 true,即测试开始了,监听器可以开始收集数据了。 等线程组结束后,继续把线程组清理掉。

  1. 线程组执行结束,扫地
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (postIter.hasNext()) {
groupCount = 0;
JMeterContextService.clearTotalThreads();
log.info("Starting tearDown thread groups");
if (mainGroups && !running) { // i.e. shutdown/stopped during main thread groups
running = tearDownOnShutdown; // re-enable for tearDown if necessary
}
while (running && postIter.hasNext()) {//for each setup thread group
AbstractThreadGroup group = postIter.next();
groupCount++;
String groupName = group.getName();
log.info("Starting tearDown ThreadGroup: {} : {}", groupCount, groupName);
startThreadGroup(group, groupCount, postSearcher, testLevelElements, notifier);
if (serialized && postIter.hasNext()) {
log.info("Waiting for post thread group: {} to finish before starting next post group", groupName);
group.waitThreadsStopped();
}
}
waitThreadsStopped(); // wait for Post threads to stop
}
notifyTestListenersOfEnd(testListeners);
JMeterContextService.endTest();
if (JMeter.isNonGUI() && SYSTEM_EXIT_FORCED) {
log.info("Forced JVM shutdown requested at end of test");
System.exit(0); // NOSONAR Intentional
}

post线程组类似于我们写ut时的teardown,在测试结束后,执行一些清理工作,比如关闭文件、关闭数据库连接等。PostThreadGroup 执行结束后,又会通知监听器,测试结束了,然后重置线程数为0。最后再根据配置来看在Non-Gui模式下是否需要终止进程。

总结

至此,StandardJMeterEngine 的执行流程就分析完了。整个执行过程可以简短概括为:

  1. 初始化测试
  2. 执行setup线程组
  3. 执行测试线程组
  4. 执行post线程组
  5. 执行结束

ClientJMeterEngine

ClientJMeterEngine 是 JMeter 的客户端执行引擎,它主要负责在远程模式下,执行远程服务器上的测试计划。在执行远程测试时,ClientJMeterEngine#runTest 被非像本地测试一样被直接调用,而是通过org.apache.jmeter.engine.DistributedRunner#start(java.util.List<java.lang.String>)来被调用。此处我们只分析ClientJMeterEngine 的 runTest 方法。先不关注 DistributedRunner 是如何分发测试任务的。接下来我们还是从 configurerunTest 两个方法来入手分析

configure

1
2
3
4
5
6
@Override
public void configure(HashTree testTree) {
TreeCloner cloner = new TreeCloner(false);
testTree.traverse(cloner);
test = cloner.getClonedTree();
}

———————————————————————————–昏割线,写不动了,休息会儿———————————————————————————–

runTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Override
public void runTest() throws JMeterEngineException {
log.info("running clientengine run method");
// See https://bz.apache.org/bugzilla/show_bug.cgi?id=55510
JMeterContextService.clearTotalThreads();
HashTree testTree = test;
synchronized(testTree) {
PreCompiler compiler = new PreCompiler(true);
testTree.traverse(compiler); // limit the changes to client only test elements
JMeterContextService.initClientSideVariables(compiler.getClientSideVariables());
testTree.traverse(new TurnElementsOn());
testTree.traverse(new ConvertListeners());
}
String methodName="unknown";
try {
JMeterContextService.startTest();
/*
* Add fix for Deadlocks, see:
*
* See https://bz.apache.org/bugzilla/show_bug.cgi?id=48350
*/
File baseDirRelative = FileServer.getFileServer().getBaseDirRelative();
String scriptName = FileServer.getFileServer().getScriptName();
synchronized(LOCK)
{
methodName="rconfigure()"; // NOSONAR Used for tracing
remote.rconfigure(testTree, hostAndPort, baseDirRelative, scriptName);
}
log.info("sent test to {} basedir='{}'", hostAndPort, baseDirRelative); // $NON-NLS-1$
if(savep == null) {
savep = new Properties();
}
log.info("Sending properties {}", savep);
try {
methodName="rsetProperties()";// NOSONAR Used for tracing
remote.rsetProperties(toHashMapOfString(savep));
} catch (RemoteException e) {
log.warn("Could not set properties: {}, error:{}", savep, e.getMessage(), e);
}
methodName="rrunTest()";
remote.rrunTest();
log.info("sent run command to {}", hostAndPort);
} catch (IllegalStateException ex) {
log.error("Error in {} method ", methodName, ex); // $NON-NLS-1$ $NON-NLS-2$
tidyRMI(log);
throw ex; // Don't wrap this error - display it as is
} catch (Exception ex) {
log.error("Error in {} method", methodName, ex); // $NON-NLS-1$ $NON-NLS-2$
tidyRMI(log);
throw new JMeterEngineException("Error in " + ethodName + " method " + ex, ex); // $NON-NLS-1$ $NON-NLS-2$
}
}