Jmeter5.2.1版本流式请求压测脚本

Jmeter5.2.1版本流式请求压测脚本

1、流式请求完整脚本

请求写在脚本中,并统计首包、生成速率等数据

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.2.1">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="乐享AI问答流式压测(首包/速率统计)" enabled="true">
      <stringProp name="TestPlan.comments">JMeter 5.2.1 乐享AI问答(/cgi-bin/v1/ai/qa)流式压测。query从CSV逐行读取(QUERY_CSV默认./query_data.csv, 变量QUERY, EOF循环复用; 若需跑完即停在GUI关闭Recycle并勾选Stop thread)。使用前: 1)AUTH_HEADER填 Bearer access_token 2)X_STAFF_ID填真实员工ID 3)REQUEST_BODY中targets.id填真实团队ID(压测默认new_session=true使各虚拟用户会话隔离)。统计: 首包=首个data事件(业务首包,即latency列), firstContentTime=首个内容块, 生成速率=内容字数/内容生成窗口, 整体速率=内容字数/总耗时; 明细缓存在内存, 由tearDown线程组在测试结束时一次性写入RESULT_CSV(明细+#汇总)。jtl的latency列即业务首包。</stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">true</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义变量(先改这里)" enabled="true">
        <collectionProp name="Arguments.arguments">
          <elementProp name="QA_URL" elementType="Argument">
            <stringProp name="Argument.name">QA_URL</stringProp>
            <stringProp name="Argument.value">http://127.0.0.1:18081/cgi-bin/v1/ai/qa</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="AUTH_HEADER" elementType="Argument">
            <stringProp name="Argument.name">AUTH_HEADER</stringProp>
            <stringProp name="Argument.value">${__P(AUTH_HEADER,)}</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="X_STAFF_ID" elementType="Argument">
            <stringProp name="Argument.name">X_STAFF_ID</stringProp>
            <stringProp name="Argument.value">${__P(X_STAFF_ID,12345)}</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="CONNECT_TIMEOUT_MS" elementType="Argument">
            <stringProp name="Argument.name">CONNECT_TIMEOUT_MS</stringProp>
            <stringProp name="Argument.value">${__P(CONNECT_TIMEOUT_MS,10000)}</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="SOCKET_TIMEOUT_MS" elementType="Argument">
            <stringProp name="Argument.name">SOCKET_TIMEOUT_MS</stringProp>
            <stringProp name="Argument.value">${__P(SOCKET_TIMEOUT_MS,60000)}</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="REQUEST_BODY" elementType="Argument">
            <stringProp name="Argument.name">REQUEST_BODY</stringProp>
            <stringProp name="Argument.value">{"query": "${QUERY}", "stream": true, "anonymous_staff_id": "", "skip_faq": false, "new_session": true, "session_id": "", "qa_mode": "reasoning", "max_chars": 200, "language": "", "targets": [{"type": "team", "id": "74d76195adxxxxxxd49239d56e44f"}]}</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="RESULT_CSV" elementType="Argument">
            <stringProp name="Argument.name">RESULT_CSV</stringProp>
            <stringProp name="Argument.value">${__P(RESULT_CSV,./qa_stream_results.csv)}</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
        </collectionProp>
      </Arguments>
      <hashTree/>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="AI问答流式线程组" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">5</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
      </ThreadGroup>
      <hashTree>
        <CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="问题数据集(query_data.csv)" enabled="true">
          <stringProp name="filename">${__P(QUERY_CSV,./query_data.csv)}</stringProp>
          <stringProp name="fileEncoding">UTF-8</stringProp>
          <stringProp name="variableNames">QUERY</stringProp>
          <boolProp name="ignoreFirstLine">false</boolProp>
          <stringProp name="delimiter">,</stringProp>
          <boolProp name="quotedData">false</boolProp>
          <boolProp name="recycle">true</boolProp>
          <boolProp name="stopThread">false</boolProp>
          <stringProp name="shareMode">shareMode.all</stringProp>
        </CSVDataSet>
        <hashTree/>
        <JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="AI问答流式请求" enabled="true">
          <stringProp name="cacheKey">true</stringProp>
          <stringProp name="filename"></stringProp>
          <stringProp name="parameters"></stringProp>
          <stringProp name="scriptLanguage">groovy</stringProp>
          <stringProp name="script">import org.apache.http.client.config.RequestConfig
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.HttpClients
import groovy.json.JsonSlurper

// ================= 变量读取 =================
def urlStr         = vars.get("QA_URL")
def reqBody        = vars.get("REQUEST_BODY")
def queryVal       = vars.get("QUERY")   // CSV数据集当前行的query
if (queryVal != null) {
    // JsonOutput.toJson会转义引号/反斜杠/换行且结果自带双引号, 因此替换的是带引号的占位符
    def esc = groovy.json.JsonOutput.toJson(queryVal)
    reqBody = reqBody.replace('"${QUERY}"', esc)
}
def authHeader     = vars.get("AUTH_HEADER")
def staffId        = vars.get("X_STAFF_ID")
def connectTimeout = (vars.get("CONNECT_TIMEOUT_MS") ?: "10000").toInteger()
def socketTimeout  = (vars.get("SOCKET_TIMEOUT_MS") ?: "60000").toInteger()

def sample = SampleResult
def f3 = { v -> String.format(java.util.Locale.ROOT, "%.3f", ((Number) v).doubleValue()) }   // 统一保留三位小数

double headerTime       = -1.0d  // 响应头到达耗时(ms)
double firstDataTime    = -1.0d  // 首包: 首个data事件(ms)
double firstContentTime = -1.0d  // 首个内容块到达(ms)
double lastChunkTime    = 0.0d   // 最后一个data事件(ms)
double totalTime        = 0.0d
int  eventCount       = 0     // data事件数
int  contentChars     = 0     // 内容累计字数
String finishReason   = ""
boolean isStop        = false
String respCode       = ""
String errMsg         = ""
def startTime         = System.currentTimeMillis()
def t0                = System.nanoTime()
def fullContent       = new StringBuilder()
def rawText           = new StringBuilder()
def parser            = new JsonSlurper()

def client = HttpClients.custom()
        .setDefaultRequestConfig(RequestConfig.custom()
                .setConnectTimeout(connectTimeout)
                .setSocketTimeout(socketTimeout)
                .setConnectionRequestTimeout(connectTimeout)
                .build())
        .build()

try {
    def post = new HttpPost(urlStr)
    post.setHeader("Content-Type", "application/json; charset=utf-8")
    post.setHeader("Accept", "text/event-stream")
    if (authHeader) post.setHeader("Authorization", authHeader)
    if (staffId) post.setHeader("x-staff-id", staffId)
    post.setEntity(new StringEntity(reqBody, "UTF-8"))

    def response = client.execute(post)
    headerTime = (System.nanoTime() - t0) / 1000000.0d
    respCode = String.valueOf(response.getStatusLine().getStatusCode())
    sample.setResponseMessage(response.getStatusLine().getReasonPhrase())

    def entity = response.getEntity()
    if (entity != null) {
        def reader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"))
        String line
        while ((line = reader.readLine()) != null) {
            rawText.append(line).append("\n")
            if (line.startsWith("data:")) {
                double now = (System.nanoTime() - t0) / 1000000.0d
                if (firstDataTime < 0.0d) firstDataTime = now   // 业务首包
                eventCount++
                lastChunkTime = now
                def payload = line.substring(5).trim()
                if (payload.startsWith("{")) {
                    try {
                        def json = parser.parseText(payload)
                        def delta = (json.get("delta_content") ?: "").toString()
                        if (!delta.isEmpty()) {
                            if (firstContentTime < 0.0d) firstContentTime = now   // 首个内容块
                            contentChars += delta.length()
                        }
                        def content = (json.get("content") ?: "").toString()
                        if (!content.isEmpty()) {   // finish事件带全量content
                            fullContent.setLength(0)
                            fullContent.append(content)
                        }
                        def fr = (json.get("finish_reason") ?: "").toString()
                        if (!fr.isEmpty()) finishReason = fr
                        def stopFlag = json.get("is_stop")
                        if (stopFlag != null && stopFlag.toString() == "true") isStop = true
                    } catch (Exception ignore) {
                        // 跳过无法解析的data行
                    }
                }
            }
        }
        reader.close()
    }
    totalTime = (System.nanoTime() - t0) / 1000000.0d
} catch (Exception e) {
    errMsg = e.getMessage() ?: e.toString()
    totalTime = (System.nanoTime() - t0) / 1000000.0d
} finally {
    try { client.close() } catch (Exception ignore) {}
}

def ok = respCode.startsWith("2") && isStop && errMsg.isEmpty()
if (!ok && errMsg.isEmpty()) errMsg = "未收到finish事件(finishReason=" + finishReason + ", httpCode=" + respCode + ")"

// ================= 速率统计 =================
double genWindow = (firstContentTime > 0.0d && lastChunkTime > firstContentTime) ? (lastChunkTime - firstContentTime) : 0.0d
double genSpeed = (contentChars > 0 && genWindow > 0.0d) ? (contentChars * 1000.0d / genWindow) : 0.0d   // 内容生成窗口内 字/秒
double avgSpeed = (contentChars > 0 && totalTime > 0.0d) ? (contentChars * 1000.0d / totalTime) : 0.0d   // 整体均值 字/秒

// ================= 填充 JMeter 采样结果 =================
sample.setResponseCode(respCode.isEmpty() ? "0" : respCode)
sample.setSuccessful(ok)
sample.setDataType(SampleResult.TEXT)
sample.setResponseMessage(errMsg.isEmpty() ? (finishReason.isEmpty() ? "OK" : finishReason) : errMsg)
def fc = fullContent.toString()
def preview = fc.length() > 300 ? fc.substring(0, 300) + "\n...(截断)" : fc
sample.setResponseData(preview + "\n\n===== 流式指标 =====\n响应头耗时=" + f3(headerTime) + "ms\n首包时间(首个data)=" + f3(firstDataTime) + "ms\n首内容时间=" + f3(firstContentTime) + "ms\n总耗时=" + f3(totalTime) + "ms\ndata事件数=" + eventCount + "\n内容字数=" + contentChars + "\n生成速率=" + f3(genSpeed) + "字/秒\n整体速率=" + f3(avgSpeed) + "字/秒\nfinish_reason=" + finishReason, "UTF-8")
sample.setConnectTime(headerTime > 0.0d ? (long) headerTime : 0L)
sample.setLatency((long) (firstDataTime > 0.0d ? firstDataTime : (headerTime > 0.0d ? headerTime : totalTime)))
sample.setSentBytes(reqBody.getBytes("UTF-8").length)

// ================= 统计缓存到内存(props), 由tearDown线程组一次性写入CSV =================
def bytesTotal = rawText.toString().getBytes("UTF-8").length
def csvRow = startTime + "," + Thread.currentThread().getName() + ",\"AI-QA-Stream\"," + (respCode.isEmpty() ? "0" : respCode) + "," + f3(headerTime) + "," + f3(firstDataTime) + "," + f3(firstContentTime) + "," + f3(totalTime) + "," + eventCount + "," + contentChars + "," + f3(genSpeed) + "," + f3(avgSpeed) + "," + bytesTotal + "," + finishReason + "," + ok + ",\"" + (errMsg == null ? "" : errMsg.replace("\"", "'")) + "\""
synchronized (props) {
    def rows = props.get("QA_ROWS")
    if (rows == null) { rows = new StringBuilder(); props.put("QA_ROWS", rows) }
    rows.append(csvRow).append("\n")
    def st = props.get("QA_STATS")
    if (st == null) {
        st = new HashMap()
        st.put("count", 0L); st.put("okCount", 0L); st.put("fpSum", 0.0d); st.put("fpMax", 0.0d)
        st.put("totalSum", 0.0d); st.put("speedSum", 0.0d); st.put("speedN", 0L); st.put("firstTs", 0L); st.put("lastEndTs", 0.0d)
        st.put("fpList", new ArrayList())
        props.put("QA_STATS", st)
    }
    st.count = st.count + 1L
    if (ok) st.okCount = st.okCount + 1L
    if (firstDataTime > 0.0d) {
        st.fpSum = st.fpSum + firstDataTime
        if (firstDataTime > st.fpMax) st.fpMax = firstDataTime
        st.fpList.add(firstDataTime)
    }
    st.totalSum = st.totalSum + totalTime
    if (genSpeed > 0.0d) { st.speedSum = st.speedSum + genSpeed; st.speedN = st.speedN + 1L }
    if (st.firstTs == 0L || startTime < st.firstTs) st.firstTs = startTime
    double endTs = startTime + totalTime   // 该请求结束时刻
    if (endTs > st.lastEndTs) st.lastEndTs = endTs
    props.put("QA_STATS", st)
}

// 供后续元件使用
vars.put("FIRST_PACKET_TIME", String.valueOf(firstDataTime))
vars.put("FIRST_CONTENT_TIME", String.valueOf(firstContentTime))
vars.put("TOTAL_TIME", String.valueOf(totalTime))
vars.put("GEN_SPEED", String.valueOf(genSpeed))
</stringProp>
        </JSR223Sampler>
        <hashTree/>
        <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="查看结果树" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
        <ResultCollector guiclass="SimpleDataWriter" testclass="ResultCollector" testname="保存统计信息(qa_stream_results.jtl)" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename">/Users/zhihua/trae_projects/qa_stream_results.jtl</stringProp>
        </ResultCollector>
        <hashTree/>
        <ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="聚合报告" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
      </hashTree>
      <PostThreadGroup guiclass="PostThreadGroupGui" testclass="PostThreadGroup" testname="tearDown统计落盘" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
      </PostThreadGroup>
      <hashTree>
        <JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="一次性写入统计数据" enabled="true">
          <stringProp name="cacheKey">true</stringProp>
          <stringProp name="filename"></stringProp>
          <stringProp name="parameters"></stringProp>
          <stringProp name="scriptLanguage">groovy</stringProp>
          <stringProp name="script">// 测试结束时一次性写入统计CSV(明细 + #开头的汇总行)
def f3 = { v -> String.format(java.util.Locale.ROOT, "%.3f", ((Number) v).doubleValue()) }   // 统一保留三位小数
def csvPath = vars.get("RESULT_CSV") ?: "./qa_stream_results.csv"
def rows = props.get("QA_ROWS")
def st = props.get("QA_STATS")
def sb = new StringBuilder()
sb.append("timeStamp,threadName,label,httpCode,headerTimeMs,firstDataTimeMs,firstContentTimeMs,totalTimeMs,events,contentChars,genSpeedCharsSec,avgSpeedCharsSec,bytes,finishReason,success,error\n")
if (rows != null) sb.append(rows.toString())
if (st != null && st.count > 0L) {
    long n = st.count
    def fp = new ArrayList(st.fpList)
    Collections.sort(fp)
    double p95 = fp.isEmpty() ? 0.0d : ((Number) fp.get((int) Math.ceil(fp.size() * 0.95d) - 1)).doubleValue()
    double wallSec = (st.lastEndTs - st.firstTs) / 1000.0d   // 墙钟=首个请求开始→最后一个请求结束
    sb.append("\n# ===== 汇总 =====\n")
    sb.append("# 请求数=").append(n).append(", 成功=").append(st.okCount).append(", 成功率=").append(f3(st.okCount * 100.0d / n)).append("%\n")
    sb.append("# 首包(首个data)均值=").append(f3(st.fpSum / n)).append("ms, 最大=").append(f3(st.fpMax)).append("ms, P95=").append(f3(p95)).append("ms\n")
    sb.append("# 总耗时均值=").append(f3(st.totalSum / n)).append("ms\n")
    if (st.speedN > 0L) sb.append("# 生成速率均值=").append(f3(st.speedSum / st.speedN)).append("字/秒\n")
    if (wallSec > 0.0d) sb.append("# 吞吐=").append(f3(n / wallSec)).append("请求/秒 (墙钟:首个请求开始→最后请求结束, 含失败请求)\n")
}
new File(csvPath).setText(sb.toString(), "UTF-8")
// 清理缓存, 避免同一JVM内再次运行时叠加旧数据
props.remove("QA_ROWS")
props.remove("QA_STATS")</stringProp>
        </JSR223Sampler>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

2、Http请求组件版

使用Jmeter http请求组件,部分数据无法统计,并且首包偏小,非真实首包。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.2.1">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="乐享AI问答流式压测-HTTP组件版(首包/速率统计)" enabled="true">
      <stringProp name="TestPlan.comments">JMeter 5.2.1 乐享AI问答(/cgi-bin/v1/ai/qa)流式压测: query从CSV逐行读取(QUERY_CSV默认./query_data.csv, 变量QUERY, EOF循环复用), body经__evalVar解析。请求由原生HTTP Request组件发送, JSR223后置处理器解析SSE; 明细缓存在内存, 由tearDown线程组在测试结束时一次性写入RESULT_CSV(明细+#汇总)。口径说明: firstDataTimeMs=latency(JMeter响应首字节,非首个data事件,偏小), firstContentTimeMs/genSpeedCharsSec固定为0(原生组件无法测量), avgSpeedCharsSec=内容字数/总耗时整体均值。使用前: AUTH_HEADER填 Bearer access_token, X_STAFF_ID填真实ID, REQUEST_BODY中targets.id填真实团队ID。</stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">true</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义变量(先改这里)" enabled="true">
        <collectionProp name="Arguments.arguments">
          <elementProp name="AUTH_HEADER" elementType="Argument">
            <stringProp name="Argument.name">AUTH_HEADER</stringProp>
            <stringProp name="Argument.value">${__P(AUTH_HEADER,)}</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="X_STAFF_ID" elementType="Argument">
            <stringProp name="Argument.name">X_STAFF_ID</stringProp>
            <stringProp name="Argument.value">${__P(X_STAFF_ID,12345)}</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="REQUEST_BODY" elementType="Argument">
            <stringProp name="Argument.name">REQUEST_BODY</stringProp>
            <stringProp name="Argument.value">{"query": "${QUERY}", "stream": true, "anonymous_staff_id": "", "skip_faq": false, "new_session": true, "session_id": "", "qa_mode": "reasoning", "max_chars": 200, "language": "", "targets": [{"type": "team", "id": "74d76195adxxxxxxd49239d56e44f"}]}</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="RESULT_CSV" elementType="Argument">
            <stringProp name="Argument.name">RESULT_CSV</stringProp>
            <stringProp name="Argument.value">${__P(RESULT_CSV,./qa_stream_http_results.csv)}</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
        </collectionProp>
      </Arguments>
      <hashTree/>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="AI问答流式线程组" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">5</stringProp>
        <stringProp name="ThreadGroup.ramp_time">5</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
      </ThreadGroup>
      <hashTree>
        <CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="问题数据集(query_data.csv)" enabled="true">
          <stringProp name="filename">${__P(QUERY_CSV,./query_data.csv)}</stringProp>
          <stringProp name="fileEncoding">UTF-8</stringProp>
          <stringProp name="variableNames">QUERY</stringProp>
          <boolProp name="ignoreFirstLine">false</boolProp>
          <stringProp name="delimiter">,</stringProp>
          <boolProp name="quotedData">false</boolProp>
          <boolProp name="recycle">true</boolProp>
          <boolProp name="stopThread">false</boolProp>
          <stringProp name="shareMode">shareMode.all</stringProp>
        </CSVDataSet>
        <hashTree/>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="AI问答流式请求" enabled="true">
          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
            <collectionProp name="Arguments.arguments">
              <elementProp name="" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">${__evalVar(REQUEST_BODY)}</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain">127.0.0.1</stringProp>
          <stringProp name="HTTPSampler.port">18081</stringProp>
          <stringProp name="HTTPSampler.protocol">http</stringProp>
          <stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
          <stringProp name="HTTPSampler.path">/cgi-bin/v1/ai/qa</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout">${__P(CONNECT_TIMEOUT_MS,10000)}</stringProp>
          <stringProp name="HTTPSampler.response_timeout">${__P(SOCKET_TIMEOUT_MS,60000)}</stringProp>
        </HTTPSamplerProxy>
        <hashTree>
          <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP信息头管理器" enabled="true">
            <collectionProp name="HeaderManager.headers">
              <elementProp name="" elementType="Header">
                <stringProp name="Header.name">Content-Type</stringProp>
                <stringProp name="Header.value">application/json; charset=utf-8</stringProp>
              </elementProp>
              <elementProp name="" elementType="Header">
                <stringProp name="Header.name">Accept</stringProp>
                <stringProp name="Header.value">text/event-stream</stringProp>
              </elementProp>
              <elementProp name="" elementType="Header">
                <stringProp name="Header.name">Authorization</stringProp>
                <stringProp name="Header.value">${AUTH_HEADER}</stringProp>
              </elementProp>
              <elementProp name="" elementType="Header">
                <stringProp name="Header.name">x-staff-id</stringProp>
                <stringProp name="Header.value">${X_STAFF_ID}</stringProp>
              </elementProp>
            </collectionProp>
          </HeaderManager>
          <hashTree/>
          <JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="SSE指标统计并写CSV" enabled="true">
            <stringProp name="cacheKey">true</stringProp>
            <stringProp name="filename"></stringProp>
            <stringProp name="parameters"></stringProp>
            <stringProp name="scriptLanguage">groovy</stringProp>
            <stringProp name="script">import groovy.json.JsonSlurper

// 基于原生HTTP采样器结果统计SSE指标并追加写入CSV
def sample = prev   // JSR223后置处理器中没有SampleResult绑定, 上一个采样器的结果是prev
def rawBytes = sample.getResponseData() ?: new byte[0]
def content = new String(rawBytes, "UTF-8")   // 显式UTF-8解码: 响应头无charset时JMeter默认按ISO-8859-1解码,会导致中文乱码且字数偏大约3倍
def parser = new JsonSlurper()
def f3 = { v -> String.format(java.util.Locale.ROOT, "%.3f", ((Number) v).doubleValue()) }   // 统一保留三位小数

int eventCount = 0
int contentChars = 0
String finishReason = ""
boolean isStop = false
def fullContent = new StringBuilder()

content.eachLine { ln ->
    if (ln.startsWith("data:")) {
        eventCount++
        def payload = ln.substring(5).trim()
        if (payload.startsWith("{")) {
            try {
                def json = parser.parseText(payload)
                def delta = (json.get("delta_content") ?: "").toString()
                contentChars += delta.length()
                def c = (json.get("content") ?: "").toString()
                if (!c.isEmpty()) {
                    fullContent.setLength(0)
                    fullContent.append(c)
                }
                def fr = (json.get("finish_reason") ?: "").toString()
                if (!fr.isEmpty()) finishReason = fr
                def stopFlag = json.get("is_stop")
                if (stopFlag != null && stopFlag.toString() == "true") isStop = true
            } catch (Exception ignore) {
                // 跳过无法解析的data行
            }
        }
    }
}

// ================= 时间口径(原生组件可测范围) =================
long headerTimeMs  = sample.getConnectTime()   // TCP连接时间
long firstPacketMs = sample.getLatency()       // 响应首字节(≈首包, JMeter口径, 非首个data事件)
long totalTimeMs   = sample.getTime()          // 完整读取耗时
double avgSpeed = (contentChars > 0 && totalTimeMs > 0) ? (contentChars * 1000.0d / totalTimeMs) : 0.0d   // 整体均值 字/秒

// ================= 判定 =================
def httpOk = sample.getResponseCode().startsWith("2")
def ok = httpOk && isStop
def errMsg = ""
if (!httpOk) errMsg = (sample.getResponseMessage() ?: ("HTTP " + sample.getResponseCode()))
else if (!isStop) errMsg = "未收到finish事件(finishReason=" + finishReason + ")"
sample.setSuccessful(ok)   // 让jtl/聚合报告的Error%反映流未完成比例
sample.setResponseMessage(errMsg.isEmpty() ? (finishReason.isEmpty() ? "OK" : finishReason) : errMsg)

// 在响应内容后追加指标(字节数直接取进入时的原始响应体, 不受追加文本影响)
long respBytes = rawBytes.length
sample.setResponseData(content + "\n\n===== 流式指标(HTTP组件口径) =====\n首包(响应首字节)=" + firstPacketMs + "ms\n总耗时=" + totalTimeMs + "ms\ndata事件数=" + eventCount + "\n内容字数=" + contentChars + "\n整体速率=" + f3(avgSpeed) + "字/秒\nfinish_reason=" + finishReason, "UTF-8")

// ================= 统计缓存到内存(props), 由tearDown线程组一次性写入CSV =================
def csvRow = sample.getTimeStamp() + "," + Thread.currentThread().getName() + ",\"AI-QA-HTTP\"," + sample.getResponseCode() + "," + headerTimeMs + "," + firstPacketMs + "," + 0L + "," + totalTimeMs + "," + eventCount + "," + contentChars + "," + 0L + "," + f3(avgSpeed) + "," + respBytes + "," + finishReason + "," + ok + ",\"" + errMsg.replace("\"", "'") + "\""
synchronized (props) {
    def rows = props.get("QA_HTTP_ROWS")
    if (rows == null) { rows = new StringBuilder(); props.put("QA_HTTP_ROWS", rows) }
    rows.append(csvRow).append("\n")
    def st = props.get("QA_HTTP_STATS")
    if (st == null) {
        st = new HashMap()
        st.put("count", 0L); st.put("okCount", 0L); st.put("fpSum", 0L); st.put("fpMax", 0L)
        st.put("totalSum", 0L); st.put("speedSum", 0.0d); st.put("speedN", 0L); st.put("firstTs", 0L); st.put("lastEndTs", 0.0d)
        st.put("fpList", new ArrayList())
        props.put("QA_HTTP_STATS", st)
    }
    st.count = st.count + 1L
    if (ok) st.okCount = st.okCount + 1L
    if (firstPacketMs > 0L) {
        st.fpSum = st.fpSum + firstPacketMs
        if (firstPacketMs > st.fpMax) st.fpMax = firstPacketMs
        st.fpList.add(firstPacketMs)
    }
    st.totalSum = st.totalSum + totalTimeMs
    if (avgSpeed > 0.0d) { st.speedSum = st.speedSum + avgSpeed; st.speedN = st.speedN + 1L }
    long ts = sample.getTimeStamp()
    if (st.firstTs == 0L || ts < st.firstTs) st.firstTs = ts
    double endTs = ts + totalTimeMs   // 该请求结束时刻
    if (endTs > st.lastEndTs) st.lastEndTs = endTs
    props.put("QA_HTTP_STATS", st)
}

// 供后续元件使用
vars.put("FIRST_PACKET_TIME", String.valueOf(firstPacketMs))
vars.put("TOTAL_TIME", String.valueOf(totalTimeMs))
vars.put("GEN_SPEED", String.valueOf(avgSpeed))
</stringProp>
          </JSR223PostProcessor>
          <hashTree/>
        </hashTree>
        <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="查看结果树" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
        <ResultCollector guiclass="SimpleDataWriter" testclass="ResultCollector" testname="保存统计信息(qa_stream_http_results.jtl)" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename">qa_stream_http_results.jtl</stringProp>
        </ResultCollector>
        <hashTree/>
        <ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="聚合报告" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
      </hashTree>
      <PostThreadGroup guiclass="PostThreadGroupGui" testclass="PostThreadGroup" testname="tearDown统计落盘" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
      </PostThreadGroup>
      <hashTree>
        <JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="一次性写入统计数据" enabled="true">
          <stringProp name="cacheKey">true</stringProp>
          <stringProp name="filename"></stringProp>
          <stringProp name="parameters"></stringProp>
          <stringProp name="scriptLanguage">groovy</stringProp>
          <stringProp name="script">// 测试结束时一次性写入统计CSV(明细 + #开头的汇总行)
def f3 = { v -> String.format(java.util.Locale.ROOT, "%.3f", ((Number) v).doubleValue()) }   // 统一保留三位小数
def csvPath = vars.get("RESULT_CSV") ?: "./qa_stream_http_results.csv"
def rows = props.get("QA_HTTP_ROWS")
def st = props.get("QA_HTTP_STATS")
def sb = new StringBuilder()
sb.append("timeStamp,threadName,label,httpCode,headerTimeMs,firstDataTimeMs,firstContentTimeMs,totalTimeMs,events,contentChars,genSpeedCharsSec,avgSpeedCharsSec,bytes,finishReason,success,error\n")
if (rows != null) sb.append(rows.toString())
if (st != null && st.count > 0L) {
    long n = st.count
    def fp = new ArrayList(st.fpList)
    Collections.sort(fp)
    long p95 = fp.isEmpty() ? 0L : (Long) fp.get((int) Math.ceil(fp.size() * 0.95d) - 1)
    double wallSec = (st.lastEndTs - st.firstTs) / 1000.0d   // 墙钟=首个请求开始→最后一个请求结束
    sb.append("\n# ===== 汇总 =====\n")
    sb.append("# 请求数=").append(n).append(", 成功=").append(st.okCount).append(", 成功率=").append(f3(st.okCount * 100.0d / n)).append("%\n")
    sb.append("# 首包(响应首字节)均值=").append(f3(st.fpSum / n)).append("ms, 最大=").append(st.fpMax).append("ms, P95=").append(p95).append("ms\n")
    sb.append("# 总耗时均值=").append(f3(st.totalSum / n)).append("ms\n")
    if (st.speedN > 0L) sb.append("# 整体速率均值=").append(f3(st.speedSum / st.speedN)).append("字/秒\n")
    if (wallSec > 0.0d) sb.append("# 吞吐=").append(f3(n / wallSec)).append("请求/秒 (墙钟:首个请求开始→最后请求结束, 含失败请求)\n")
}
new File(csvPath).setText(sb.toString(), "UTF-8")
// 清理缓存, 避免同一JVM内再次运行时叠加旧数据
props.remove("QA_HTTP_ROWS")
props.remove("QA_HTTP_STATS")</stringProp>
        </JSR223Sampler>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

统计数据:解释说明

相关推荐
测试19982 天前
Jmeter接口自动化生成测试报告html格式详解
自动化测试·python·测试工具·jmeter·职场和发展·测试用例·接口测试
stillstream_ink3 天前
OpenCart性能压测复盘|JMeter\+Locust双工具实操,附5个踩坑记录
jmeter·测试
岁月失语唯石能言6 天前
《 Web项目测试实战:博客系统的功能/接口/自动化/性能全流程测试报告》
自动化测试·功能测试·selenium·jmeter·接口测试·压力测试·性能测试
中国软件测试质量协会7 天前
国产性能测试工具:kylinPET高仿真与高并发性能测试技术深度解析——与JMeter、LoadRunner全面对比
测试工具·jmeter
拜托多多指教8 天前
【JMeter】外观设置(Mac)
jmeter
川石课堂软件测试15 天前
自动化测试常见的异常处理
python·jmeter·mysql·docker·容器·单元测试·grafana
wuhuhuan15 天前
JMeter 性能测试教程
jmeter