Implementing Response Time Statistics in the FunTester Framework
This article explains how to filter early response times and compute detailed latency metrics—including percentiles, average, and median—within the FunTester load‑testing framework by redesigning data collection structures and adding a unified statistics method.
It is known that, aside from JVM warm‑up issues, some requests exhibit abnormal response times; the article shares a case where filtering the initial response time records improves overall data accuracy.
The need for optimization arose because the FunTester testing framework originally did not collect local statistics, yet after a recent JDK upgrade and GC parameter comparisons, monitoring data from gateways differed from real user scenarios, prompting the addition of client‑side response‑time tracking.
Response times are stored as a short array: for single‑threaded cases in a List<Short> costs and for load‑test cases in a Vector<Short> . A generic method static FunIndex index(List c) sorts the list and extracts various quantiles (p99, p999, p95), minimum, maximum, average, and median values. The implementation is shown below:
/**
* 统计list各分位数据
* @param c
* @return
*/
static FunIndex index(List
c) {
if (c == null || c.size() == 0) return
c.sort()
int size = c.size()
double min = c.first()
double max = c.last()
double p99 = c.get(size * 0.99 as Integer)
double p999 = c.get(size * 0.999 as Integer)
double p95 = c.get(size * 0.95 as Integer)
double avg = SourceCode.changeStringToDouble(SourceCode.formatNumber(c.average(), "#.###"))
def mid = c.get(size / 2 as Integer)
new FunIndex(avg: avg, mid: mid, min: min, max: max, p99: p99, p999: p999, p95: p95)
}
/**
* 统计结果
*/
static class FunIndex extends AbstractBean {
Double avg
Double mid
Double min
Double max
Double p99
Double p999
Double p95
@Override
String toString() {
"平均值:$avg ,最大值$max ,最小值:$min ,中位数:$mid p99:$p99 p95:$p95"
}
}For each thread, the per‑thread List<Short> costs is removed and the data is centralized in com.funtester.base.constaint.ThreadBase . The following code shows the new flag and counting method:
/**
* 是否记录响应时间,默认否
*/
public static boolean COUNT = false;
/**
* 记录响应时间
*
* @param s 开始时间
*/
public void count(long s) {
if (COUNT && executeNum > 100) costs.add((short) (Time.getTimeStamp() - s));
}The thread model’s run method is also refactored to record timestamps, log execution statistics, and aggregate costs into Concurrent.allTimes . The revised method is:
@Override
public void run() {
try {
before();
long ss = Time.getTimeStamp();
while (true) {
try {
executeNum++;
long s = Time.getTimeStamp();
doing();
count(s);
} catch (Exception e) {
logger.warn("执行任务失败!", e);
errorNum++;
} finally {
if ((isTimesMode ? executeNum >= limit : (Time.getTimeStamp() - ss) >= limit) || ThreadBase.needAbort() || status())
break;
}
long ee = Time.getTimeStamp();
if ((ee - ss) / 1000 > RUNUP_TIME + 3) // 区分软启动运行和正式运行
logger.info("线程:{},执行次数:{},错误次数: {},总耗时:{} s", threadName, executeNum, errorNum, (ee - ss) / 1000.0);
Concurrent.allTimes.addAll(costs);
Concurrent.requestMark.addAll(marks);
}
} catch (Exception e) {
logger.warn("执行任务失败!", e);
} finally {
after();
}
}Following this approach, the author plans to add an asynchronous RT sampler based on the same idea, linking to a dynamic model article for future reference.
Have Fun ~ Tester !
FunTester
10k followers, 1k articles | completely useless
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.