feat(性能测试): 支持自定义配置NODE节点线程数

This commit is contained in:
CaptainB 2021-11-09 10:59:43 +08:00 committed by 刘瑞斌
parent bf902538e0
commit 133e9ea772
8 changed files with 260 additions and 23 deletions

View File

@ -111,6 +111,12 @@ public class EngineFactory {
if (org.springframework.util.CollectionUtils.isEmpty(fileMetadataList)) {
MSException.throwException(Translator.get("run_load_test_file_not_found") + loadTestReport.getTestId());
}
// 报告页面点击下载执行zip
boolean isLocal = false;
if (ratios.length == 1 && ratios[0] < 0) {
ratios[0] = 1;
isLocal = true;
}
List<FileMetadata> jmxFiles = fileMetadataList.stream().filter(f -> StringUtils.equalsIgnoreCase(f.getType(), FileType.JMX.name())).collect(Collectors.toList());
List<FileMetadata> resourceFiles = ListUtils.subtract(fileMetadataList, jmxFiles);
@ -132,6 +138,22 @@ public class EngineFactory {
for (int i = 0; i < jsonArray.size(); i++) {
if (jsonArray.get(i) instanceof List) {
JSONArray o = jsonArray.getJSONArray(i);
String strategy = "auto";
int resourceNodeIndex = 0;
JSONArray tgRatios = null;
for (int j = 0; j < o.size(); j++) {
JSONObject b = o.getJSONObject(j);
String key = b.getString("key");
if ("strategy".equals(key) && !isLocal) {
strategy = b.getString("value");
}
if ("resourceNodeIndex".equals(key)) {
resourceNodeIndex = b.getIntValue("value");
}
if ("ratios".equals(key)) {
tgRatios = b.getJSONArray("value");
}
}
for (int j = 0; j < o.size(); j++) {
JSONObject b = o.getJSONObject(j);
String key = b.getString("key");
@ -142,17 +164,45 @@ public class EngineFactory {
if (values instanceof List) {
Object value = b.get("value");
if ("TargetLevel".equals(key)) {
Integer targetLevel = ((Integer) b.get("value"));
if (resourceIndex + 1 == ratios.length) {
double beforeLast = 0; // 前几个线程数
for (int k = 0; k < ratios.length - 1; k++) {
beforeLast += Math.round(targetLevel * ratios[k]);
}
value = Math.round(targetLevel - beforeLast);
} else {
value = Math.round(targetLevel * ratios[resourceIndex]);
switch (strategy) {
default:
case "auto":
Integer targetLevel = ((Integer) b.get("value"));
if (resourceIndex + 1 == ratios.length) {
double beforeLast = 0; // 前几个线程数
for (int k = 0; k < ratios.length - 1; k++) {
beforeLast += Math.round(targetLevel * ratios[k]);
}
value = Math.round(targetLevel - beforeLast);
} else {
value = Math.round(targetLevel * ratios[resourceIndex]);
}
break;
case "specify":
Integer threadNum = ((Integer) b.get("value"));
if (resourceNodeIndex == resourceIndex) {
value = Math.round(threadNum);
} else {
value = Math.round(0);
}
break;
case "custom":
Integer threadNum2 = ((Integer) b.get("value"));
if (CollectionUtils.isNotEmpty(tgRatios)) {
if (resourceIndex + 1 == tgRatios.size()) {
double beforeLast = 0; // 前几个线程数
for (int k = 0; k < tgRatios.size() - 1; k++) {
beforeLast += Math.round(threadNum2 * tgRatios.getDoubleValue(k));
}
value = Math.round(threadNum2 - beforeLast);
} else {
value = Math.round(threadNum2 * tgRatios.getDoubleValue(resourceIndex));
}
}
break;
}
}
((List<Object>) values).add(value);
engineContext.addProperty(key, values);
}

View File

@ -382,7 +382,7 @@ export default {
let testId = this.report.testId;
let reportId = this.report.id;
let resourceIndex = 0;
let ratio = "1.0";
let ratio = "-1";
let config = {
url: `/jmeter/download?testId=${testId}&ratio=${ratio}&reportId=${reportId}&resourceIndex=${resourceIndex}`,
method: 'get',

View File

@ -210,6 +210,38 @@
</el-form-item>
<el-form-item :label="$t('load_test.ramp_up_time_seconds')"/>
</div>
<!-- 资源池自己配置各个节点的并发 -->
<div v-if="resourcePoolType === 'NODE'">
<el-radio-group v-model="threadGroup.strategy" :disabled="isReadOnly" style="padding-bottom: 10px;">
<el-radio label="auto">自动分配</el-radio>
<el-radio label="specify">固定节点</el-radio>
<el-radio label="custom">自定义</el-radio>
</el-radio-group>
<div v-if="threadGroup.strategy === 'auto'"></div>
<div v-else-if="threadGroup.strategy === 'specify'">
<el-select v-model="threadGroup.resourceNodeIndex" :disabled="isReadOnly" size="mini">
<el-option
v-for="(node, index) in resourceNodes"
:key="node.ip"
:label="node.ip"
:value="index">
</el-option>
</el-select>
</div>
<div v-else>
<el-table :data="threadGroup.resourceNodes" :max-height="200">
<el-table-column type="index" width="50"/>
<el-table-column prop="ip" label="IP"/>
<el-table-column prop="maxConcurrency" :label="$t('test_resource_pool.max_threads')"/>
<el-table-column prop="ratio" label="占比">
<template v-slot:default="{row}">
<el-input-number size="small" v-model="row.ratio" :min="0" :step=".1"
:max="1"></el-input-number>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-form>
</el-collapse-item>
</el-collapse>
@ -249,11 +281,10 @@ const THREAD_TYPE = "threadType";
const ITERATE_NUM = "iterateNum";
const ENABLED = "enabled";
const DELETED = "deleted";
const STRATEGY = "strategy";
const RESOURCE_NODE_INDEX = "resourceNodeIndex";
const RATIOS = "ratios";
const hexToRgba = function (hex, opacity) {
return 'rgba(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5)) + ','
+ parseInt('0x' + hex.slice(5, 7)) + ',' + opacity + ')';
};
const hexToRgb = function (hex) {
return 'rgb(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5))
+ ',' + parseInt('0x' + hex.slice(5, 7)) + ')';
@ -282,6 +313,8 @@ export default {
options: {},
resourcePool: null,
resourcePools: [],
resourceNodes: [],
resourcePoolType: null,
activeNames: ["0"],
threadGroups: [],
maxThreadNumbers: 5000,
@ -402,6 +435,15 @@ export default {
case ON_SAMPLE_ERROR:
this.threadGroups[i].onSampleError = item.value;
break;
case STRATEGY:
this.threadGroups[i].strategy = item.value;
break;
case RESOURCE_NODE_INDEX:
this.threadGroups[i].resourceNodeIndex = item.value;
break;
case RATIOS:
this.threadGroups[i].ratios = item.value;
break;
case SERIALIZE_THREAD_GROUPS:
this.serializeThreadGroups = item.value;// 线
break;
@ -430,6 +472,7 @@ export default {
tg.durationMinutes = Math.floor((tg.duration / 60 % 60));
tg.durationSeconds = Math.floor((tg.duration % 60));
}
this.resourcePoolChange();
this.calculateTotalChart();
}
});
@ -454,15 +497,45 @@ export default {
let result = this.resourcePools.filter(p => p.id === this.resourcePool);
if (result.length === 1) {
let threadNumber = 0;
this.resourceNodes = [];
this.resourcePoolType = result[0].type;
result[0].resources.forEach(resource => {
threadNumber += JSON.parse(resource.configuration).maxConcurrency;
let config = JSON.parse(resource.configuration);
threadNumber += config.maxConcurrency;
this.resourceNodes.push(config);
});
this.$set(this, 'maxThreadNumbers', threadNumber);
this.threadGroups.forEach(tg => {
if (tg.threadNumber > threadNumber) {
this.$set(tg, "threadNumber", threadNumber);
}
let tgRatios = tg.ratios;
let resourceNodes = JSON.parse(JSON.stringify(this.resourceNodes));
let ratios = resourceNodes.map(n => n.maxConcurrency).reduce((total, curr) => {
total += curr;
return total;
}, 0);
let preSum = 0;
for (let i = 0; i < resourceNodes.length; i++) {
let n = resourceNodes[i];
if (resourceNodes.length === tgRatios.length) {
n.ratio = tgRatios[i];
continue;
}
if (i === resourceNodes.length - 1) {
n.ratio = (1 - preSum).toFixed(2);
} else {
n.ratio = (n.maxConcurrency / ratios).toFixed(2);
preSum += Number.parseFloat(n.ratio);
}
}
this.$set(tg, "resourceNodes", resourceNodes);
if (tg.resourceNodeIndex > resourceNodes.length - 1) {
this.$set(tg, "resourceNodeIndex", 0);
}
});
this.calculateTotalChart();
}
},
@ -497,8 +570,8 @@ export default {
let tg = handler.threadGroups[i];
if (tg.enabled === 'false' ||
tg.deleted === 'true' ||
tg.threadType === 'ITERATION') {
tg.deleted === 'true' ||
tg.threadType === 'ITERATION') {
continue;
}
if (this.getDuration(tg) < tg.rampUpTime) {
@ -600,8 +673,20 @@ export default {
if (tg.enabled === 'false') {
continue;
}
if (tg.strategy === "custom") {
let sum = tg.resourceNodes.map(n => n.ratio).reduce((total, curr) => {
total += curr;
return total;
}, 0);
if (sum !== 1) {
this.$warning(this.$t('load_test.pressure_config_custom_error'));
return false;
}
}
if (!tg.threadNumber || !tg.duration
|| !tg.rampUpTime || !tg.step || !tg.iterateNum) {
|| !tg.rampUpTime || !tg.step || !tg.iterateNum) {
this.$warning(this.$t('load_test.pressure_config_params_is_empty'));
this.$emit('changeActive', '1');
return false;
@ -623,6 +708,9 @@ export default {
tg.duration = tg.durationHours * 60 * 60 + tg.durationMinutes * 60 + tg.durationSeconds;
return tg.duration;
},
getRatios(tg) {
return tg.resourceNodes.map(node => node.ratio);
},
convertProperty() {
/// todo4jmeter ConcurrencyThreadGroup plugin
let result = [];
@ -648,6 +736,9 @@ export default {
{key: ENABLED, value: this.threadGroups[i].enabled},
{key: DELETED, value: this.threadGroups[i].deleted},
{key: ON_SAMPLE_ERROR, value: this.threadGroups[i].onSampleError},
{key: STRATEGY, value: this.threadGroups[i].strategy},
{key: RESOURCE_NODE_INDEX, value: this.threadGroups[i].resourceNodeIndex},
{key: RATIOS, value: this.getRatios(this.threadGroups[i])},
{key: THREAD_GROUP_TYPE, value: this.threadGroups[i].tgType},
{key: SERIALIZE_THREAD_GROUPS, value: this.serializeThreadGroups},
{key: AUTO_STOP, value: this.autoStop},

View File

@ -62,6 +62,9 @@ export function findThreadGroup(jmxContent, handler) {
tg.enabled = tg.attributes.enabled;
tg.tgType = tg.name;
tg.csvFiles = csvFiles;
tg.strategy = 'auto';
tg.resourceNodeIndex = 0;
tg.ratios = '';
if (tg.name === 'SetupThreadGroup' || tg.name === 'PostThreadGroup') {
tg.threadType = 'ITERATION';
tg.threadNumber = 1;

View File

@ -210,6 +210,37 @@
</el-form-item>
<el-form-item :label="$t('load_test.ramp_up_time_seconds')"/>
</div>
<div v-if="resourcePoolType === 'NODE'">
<el-radio-group v-model="threadGroup.strategy" :disabled="isReadOnly" style="padding-bottom: 10px;">
<el-radio label="auto">自动分配</el-radio>
<el-radio label="specify">固定节点</el-radio>
<el-radio label="custom">自定义</el-radio>
</el-radio-group>
<div v-if="threadGroup.strategy === 'auto'"></div>
<div v-else-if="threadGroup.strategy === 'specify'">
<el-select v-model="threadGroup.resourceNodeIndex" :disabled="isReadOnly" size="mini">
<el-option
v-for="(node, index) in resourceNodes"
:key="node.ip"
:label="node.ip"
:value="index">
</el-option>
</el-select>
</div>
<div v-else>
<el-table :data="threadGroup.resourceNodes" :max-height="200">
<el-table-column type="index" width="50"/>
<el-table-column prop="ip" label="IP"/>
<el-table-column prop="maxConcurrency" :label="$t('test_resource_pool.max_threads')"/>
<el-table-column prop="ratio" label="占比">
<template v-slot:default="{row}">
<el-input-number size="small" v-model="row.ratio" :min="0" :step=".1"
:max="1"></el-input-number>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-form>
</el-collapse-item>
</el-collapse>
@ -249,11 +280,10 @@ const THREAD_TYPE = "threadType";
const ITERATE_NUM = "iterateNum";
const ENABLED = "enabled";
const DELETED = "deleted";
const STRATEGY = "strategy";
const RESOURCE_NODE_INDEX = "resourceNodeIndex";
const RATIOS = "ratios";
const hexToRgba = function (hex, opacity) {
return 'rgba(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5)) + ','
+ parseInt('0x' + hex.slice(5, 7)) + ',' + opacity + ')';
};
const hexToRgb = function (hex) {
return 'rgb(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5))
+ ',' + parseInt('0x' + hex.slice(5, 7)) + ')';
@ -287,6 +317,8 @@ export default {
rpsLimitEnable: false,
options: {},
resourcePools: [],
resourceNodes: [],
resourcePoolType: null,
activeNames: ["0"],
threadGroups: [],
maxThreadNumbers: 5000,
@ -402,6 +434,15 @@ export default {
case ON_SAMPLE_ERROR:
this.threadGroups[i].onSampleError = item.value;
break;
case STRATEGY:
this.threadGroups[i].strategy = item.value;
break;
case RESOURCE_NODE_INDEX:
this.threadGroups[i].resourceNodeIndex = item.value;
break;
case RATIOS:
this.threadGroups[i].ratios = item.value;
break;
case SERIALIZE_THREAD_GROUPS:
this.serializeThreadGroups = item.value;// 线
break;
@ -430,6 +471,7 @@ export default {
tg.durationMinutes = Math.floor((tg.duration / 60 % 60));
tg.durationSeconds = Math.floor((tg.duration % 60));
}
this.resourcePoolChange();
this.calculateTotalChart();
}
});
@ -454,15 +496,45 @@ export default {
let result = this.resourcePools.filter(p => p.id === this.resourcePool);
if (result.length === 1) {
let threadNumber = 0;
this.resourceNodes = [];
this.resourcePoolType = result[0].type;
result[0].resources.forEach(resource => {
threadNumber += JSON.parse(resource.configuration).maxConcurrency;
let config = JSON.parse(resource.configuration);
threadNumber += config.maxConcurrency;
this.resourceNodes.push(config);
});
this.$set(this, 'maxThreadNumbers', threadNumber);
this.threadGroups.forEach(tg => {
if (tg.threadNumber > threadNumber) {
this.$set(tg, "threadNumber", threadNumber);
}
let tgRatios = tg.ratios;
let resourceNodes = JSON.parse(JSON.stringify(this.resourceNodes));
let ratios = resourceNodes.map(n => n.maxConcurrency).reduce((total, curr) => {
total += curr;
return total;
}, 0);
let preSum = 0;
for (let i = 0; i < resourceNodes.length; i++) {
let n = resourceNodes[i];
if (resourceNodes.length === tgRatios.length) {
n.ratio = tgRatios[i];
continue;
}
if (i === resourceNodes.length - 1) {
n.ratio = (1 - preSum).toFixed(2);
} else {
n.ratio = (n.maxConcurrency / ratios).toFixed(2);
preSum += Number.parseFloat(n.ratio);
}
}
this.$set(tg, "resourceNodes", resourceNodes);
if (tg.resourceNodeIndex > resourceNodes.length - 1) {
this.$set(tg, "resourceNodeIndex", 0);
}
});
this.calculateTotalChart();
}
},
@ -601,6 +673,18 @@ export default {
if (tg.enabled === 'false' ) {
continue;
}
if (tg.strategy === "custom") {
let sum = tg.resourceNodes.map(n => n.ratio).reduce((total, curr) => {
total += curr;
return total;
}, 0);
if (sum !== 1) {
this.$warning(this.$t('load_test.pressure_config_custom_error'));
return false;
}
}
if (!tg.threadNumber || !tg.duration
|| !tg.rampUpTime || !tg.step || !tg.iterateNum) {
this.$warning(this.$t('load_test.pressure_config_params_is_empty'));
@ -624,6 +708,9 @@ export default {
tg.duration = tg.durationHours * 60 * 60 + tg.durationMinutes * 60 + tg.durationSeconds;
return tg.duration;
},
getRatios(tg) {
return tg.resourceNodes.map(node => node.ratio);
},
convertProperty() {
/// todo4jmeter ConcurrencyThreadGroup plugin
let result = [];
@ -649,6 +736,9 @@ export default {
{key: ENABLED, value: this.threadGroups[i].enabled},
{key: DELETED, value: this.threadGroups[i].deleted},
{key: ON_SAMPLE_ERROR, value: this.threadGroups[i].onSampleError},
{key: STRATEGY, value: this.threadGroups[i].strategy},
{key: RESOURCE_NODE_INDEX, value: this.threadGroups[i].resourceNodeIndex},
{key: RATIOS, value: this.getRatios(this.threadGroups[i])},
{key: THREAD_GROUP_TYPE, value: this.threadGroups[i].tgType},
{key: SERIALIZE_THREAD_GROUPS, value: this.serializeThreadGroups},
{key: AUTO_STOP, value: this.autoStop},

View File

@ -829,6 +829,7 @@ export default {
user_name: 'Creator',
special_characters_are_not_supported: 'Test name does not support special characters',
pressure_config_params_is_empty: 'Pressure configuration parameters cannot be empty!',
pressure_config_custom_error: 'The sum of custom strategies must be 1',
schedule_tip: 'The interval must not be less than the pressure measuring time',
delete_threadgroup_confirm: 'Confirm delete scenario: ',
scenario_list: 'Scenario List',

View File

@ -835,6 +835,7 @@ export default {
user_name: '创建人',
special_characters_are_not_supported: '测试名称不支持特殊字符',
pressure_config_params_is_empty: '压力配置参数不能为空!',
pressure_config_custom_error: '自定义策略之和必须为1',
schedule_tip: '间隔时间不能小于压测时长',
delete_threadgroup_confirm: '确认删除场景',
scenario_list: '场景列表',

View File

@ -834,6 +834,7 @@ export default {
user_name: '創建人',
special_characters_are_not_supported: '測試名稱不支持特殊字符',
pressure_config_params_is_empty: '壓力配置參數不能為空!',
pressure_config_custom_error: '自定義策略之和必須為1',
schedule_tip: '間隔時間不能小於壓測時長',
delete_threadgroup_confirm: '確認刪除場景',
scenario_list: '場景列表',