From 133e9ea7727176601bb377f6a080d4434d8cd608 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Tue, 9 Nov 2021 10:59:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=80=A7=E8=83=BD=E6=B5=8B=E8=AF=95):=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=E9=85=8D=E7=BD=AE?= =?UTF-8?q?NODE=E8=8A=82=E7=82=B9=E7=BA=BF=E7=A8=8B=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../performance/engine/EngineFactory.java | 68 +++++++++-- .../report/PerformanceReportView.vue | 2 +- .../components/PerformancePressureConfig.vue | 107 ++++++++++++++++-- .../performance/test/model/ThreadGroup.js | 3 + .../comonents/load/PerformanceLoadConfig.vue | 100 +++++++++++++++- frontend/src/i18n/en-US.js | 1 + frontend/src/i18n/zh-CN.js | 1 + frontend/src/i18n/zh-TW.js | 1 + 8 files changed, 260 insertions(+), 23 deletions(-) diff --git a/backend/src/main/java/io/metersphere/performance/engine/EngineFactory.java b/backend/src/main/java/io/metersphere/performance/engine/EngineFactory.java index 44f219fe88..c17180be46 100644 --- a/backend/src/main/java/io/metersphere/performance/engine/EngineFactory.java +++ b/backend/src/main/java/io/metersphere/performance/engine/EngineFactory.java @@ -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 jmxFiles = fileMetadataList.stream().filter(f -> StringUtils.equalsIgnoreCase(f.getType(), FileType.JMX.name())).collect(Collectors.toList()); List 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) values).add(value); engineContext.addProperty(key, values); } diff --git a/frontend/src/business/components/performance/report/PerformanceReportView.vue b/frontend/src/business/components/performance/report/PerformanceReportView.vue index 4462766dc4..5f89ad0208 100644 --- a/frontend/src/business/components/performance/report/PerformanceReportView.vue +++ b/frontend/src/business/components/performance/report/PerformanceReportView.vue @@ -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', diff --git a/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue b/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue index 444c762b87..20b47cf5ab 100644 --- a/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue +++ b/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue @@ -210,6 +210,38 @@ + +
+ + 自动分配 + 固定节点 + 自定义 + +
+
+ + + + +
+
+ + + + + + + + +
+
@@ -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() { /// todo:下面4个属性是jmeter 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}, diff --git a/frontend/src/business/components/performance/test/model/ThreadGroup.js b/frontend/src/business/components/performance/test/model/ThreadGroup.js index 7b05f42819..2a71e6fed1 100644 --- a/frontend/src/business/components/performance/test/model/ThreadGroup.js +++ b/frontend/src/business/components/performance/test/model/ThreadGroup.js @@ -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; diff --git a/frontend/src/business/components/track/plan/view/comonents/load/PerformanceLoadConfig.vue b/frontend/src/business/components/track/plan/view/comonents/load/PerformanceLoadConfig.vue index 066fa62402..59697812ab 100644 --- a/frontend/src/business/components/track/plan/view/comonents/load/PerformanceLoadConfig.vue +++ b/frontend/src/business/components/track/plan/view/comonents/load/PerformanceLoadConfig.vue @@ -210,6 +210,37 @@
+
+ + 自动分配 + 固定节点 + 自定义 + +
+
+ + + + +
+
+ + + + + + + + +
+
@@ -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() { /// todo:下面4个属性是jmeter 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}, diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index 8d7544545b..c50adef0c3 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -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', diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index da2fdaaba8..d034071a28 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -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: '场景列表', diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index f0f4e248c3..811843ce34 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -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: '場景列表',