diff --git a/TestResultSummaryService/routes/getReleaseSummary.js b/TestResultSummaryService/routes/getReleaseSummary.js
new file mode 100644
index 00000000..ce1401f3
--- /dev/null
+++ b/TestResultSummaryService/routes/getReleaseSummary.js
@@ -0,0 +1,96 @@
+const { TestResultsDB } = require('../Database');
+
+/**
+ * getReleaseSummary returns health metrics for major JDK releases
+ *
+ * @route GET /api/getReleaseSummary
+ * @group Release - Operations about releases
+ * @return {object} - releaseSummary - map of jdk versions to their health stats
+ */
+
+module.exports = async (req, res) => {
+ try {
+ const testResultsDB = new TestResultsDB();
+
+ // We want to aggregate health across recent builds for each major JDK version
+ const aggregateQuery = [
+ {
+ // Only look at builds from the last 7 days to keep dashboard relevant
+ $match: {
+ timestamp: { $gt: Date.now() - 7 * 24 * 60 * 60 * 1000 },
+ // Focus on actual test/perf jobs
+ buildName: { $regex: /^(Test|Perf)_openjdk/ },
+ },
+ },
+ {
+ // Extract JDK version from buildName (e.g., Test_openjdk11_... -> 11)
+ $addFields: {
+ jdkVersionMatch: {
+ $regexFind: {
+ input: '$buildName',
+ regex: /openjdk(\d+)/,
+ },
+ },
+ },
+ },
+ {
+ $addFields: {
+ jdkVersion: '$jdkVersionMatch.captures',
+ },
+ },
+ {
+ // Flatten the captures array to get the actual version string
+ $unwind: '$jdkVersion',
+ },
+ {
+ // Group by JDK version and build result
+ $group: {
+ _id: {
+ jdkVersion: '$jdkVersion',
+ result: '$buildResult',
+ },
+ count: { $sum: 1 },
+ },
+ },
+ {
+ // Reshape to make it easier for the frontend
+ $group: {
+ _id: '$_id.jdkVersion',
+ results: {
+ $push: {
+ result: '$_id.result',
+ count: '$count',
+ },
+ },
+ },
+ },
+ {
+ $sort: { _id: 1 },
+ },
+ ];
+
+ const result = await testResultsDB.aggregate(aggregateQuery);
+
+ // Transform the array into a more convenient object format
+ const summary = {};
+ result.forEach((item) => {
+ const stats = {
+ SUCCESS: 0,
+ FAILURE: 0,
+ UNSTABLE: 0,
+ ABORTED: 0,
+ total: 0,
+ };
+ item.results.forEach((res) => {
+ const r = res.result || 'UNKNOWN';
+ stats[r] = res.count;
+ stats.total += res.count;
+ });
+ summary[item._id] = stats;
+ });
+
+ res.send(summary);
+ } catch (error) {
+ res.status(500).send({ error: error.message });
+ }
+};
diff --git a/TestResultSummaryService/routes/index.js b/TestResultSummaryService/routes/index.js
index ccb40135..0e67e533 100644
--- a/TestResultSummaryService/routes/index.js
+++ b/TestResultSummaryService/routes/index.js
@@ -54,9 +54,11 @@ app.get('/getFeedbackUrl', require('./getFeedbackUrl'));
app.get('/rescanBuild', require('./rescanBuild'));
app.get('/testParserViaFile', require('./test/testParserViaFile'));
app.get('/testParserViaLogStream', require('./test/testParserViaLogStream'));
+app.get('/getReleaseSummary', require('./getReleaseSummary'));
app.get('/updateComments', require('./updateComments'));
app.get('/updateKeepForever', require('./updateKeepForever'));
+app.post('/updateStats', require('./updateStats'));
// jwt
app.post('/auth/register', require('./jwt/register'));
diff --git a/test-result-summary-client/src/Dashboard/Dashboard.jsx b/test-result-summary-client/src/Dashboard/Dashboard.jsx
index fbf20703..e7004094 100644
--- a/test-result-summary-client/src/Dashboard/Dashboard.jsx
+++ b/test-result-summary-client/src/Dashboard/Dashboard.jsx
@@ -18,6 +18,11 @@ export default class Dashboard extends Component {
key: '2',
children: ,
},
+ {
+ label: 'Release',
+ key: '3',
+ children: ,
+ },
]}
/>
);
diff --git a/test-result-summary-client/src/Dashboard/Defaults.js b/test-result-summary-client/src/Dashboard/Defaults.js
index a7f9f6ea..90e8e54b 100644
--- a/test-result-summary-client/src/Dashboard/Defaults.js
+++ b/test-result-summary-client/src/Dashboard/Defaults.js
@@ -25,4 +25,14 @@ export default {
},
],
},
+ Release: {
+ widgets: [
+ {
+ type: 'ReleaseHealthWidget',
+ x: 0,
+ y: 0,
+ settings: {},
+ },
+ ],
+ },
};
diff --git a/test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx b/test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx
new file mode 100644
index 00000000..fb383aad
--- /dev/null
+++ b/test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx
@@ -0,0 +1,127 @@
+import React, { Component } from 'react';
+import { Table, Tooltip, Progress, Typography } from 'antd';
+import { fetchData } from '../../utils/Utils';
+
+export default class ReleaseHealthWidget extends Component {
+ static Title = (props) => 'Release Health Overview';
+ static defaultSize = { w: 4, h: 4 };
+ static defaultSettings = {};
+
+ state = {
+ data: {},
+ loading: true,
+ };
+
+ async componentDidMount() {
+ await this.updateData();
+ // Update every 5 minutes
+ this.intervalId = setInterval(
+ () => {
+ this.updateData();
+ },
+ 5 * 60 * 1000
+ );
+ }
+
+ componentWillUnmount() {
+ clearInterval(this.intervalId);
+ }
+
+ updateData = async () => {
+ this.setState({ loading: true });
+ const summary = await fetchData('/api/getReleaseSummary');
+ if (summary) {
+ this.setState({ data: summary, loading: false });
+ } else {
+ this.setState({ loading: false });
+ }
+ };
+
+ render() {
+ const { data, loading } = this.state;
+
+ const tableData = Object.keys(data).map((jdkVersion) => {
+ const stats = data[jdkVersion];
+ const passRate =
+ stats.total > 0
+ ? Math.round((stats.SUCCESS / stats.total) * 100)
+ : 0;
+
+ return {
+ key: jdkVersion,
+ jdkVersion: `JDK ${jdkVersion}`,
+ success: stats.SUCCESS,
+ failure: stats.FAILURE,
+ unstable: stats.UNSTABLE,
+ total: stats.total,
+ passRate: passRate,
+ };
+ });
+
+ const columns = [
+ {
+ title: 'JDK Version',
+ dataIndex: 'jdkVersion',
+ key: 'jdkVersion',
+ sorter: (a, b) => parseInt(a.key) - parseInt(b.key),
+ },
+ {
+ title: 'Success',
+ dataIndex: 'success',
+ key: 'success',
+ render: (val) => (
+ {val}
+ ),
+ },
+ {
+ title: 'Failure',
+ dataIndex: 'failure',
+ key: 'failure',
+ render: (val) => (
+ {val}
+ ),
+ },
+ {
+ title: 'Unstable',
+ dataIndex: 'unstable',
+ key: 'unstable',
+ render: (val) => (
+ {val}
+ ),
+ },
+ {
+ title: 'Health',
+ dataIndex: 'passRate',
+ key: 'passRate',
+ render: (percent) => (
+
+
+ ),
+ sorter: (a, b) => a.passRate - b.passRate,
+ },
+ ];
+
+ return (
+
+ );
+ }
+}
diff --git a/test-result-summary-client/src/Dashboard/Widgets/index.js b/test-result-summary-client/src/Dashboard/Widgets/index.js
index b42b4635..4a9fe238 100644
--- a/test-result-summary-client/src/Dashboard/Widgets/index.js
+++ b/test-result-summary-client/src/Dashboard/Widgets/index.js
@@ -1,2 +1,3 @@
export * from './BuildStatus/';
export * from './Graph/';
+export { default as ReleaseHealthWidget } from './ReleaseHealthWidget';
diff --git a/test-result-summary-client/src/TrafficLight/MetricsTable.jsx b/test-result-summary-client/src/TrafficLight/MetricsTable.jsx
index 4cd0bc91..1e31c3e5 100644
--- a/test-result-summary-client/src/TrafficLight/MetricsTable.jsx
+++ b/test-result-summary-client/src/TrafficLight/MetricsTable.jsx
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Table, Typography, Switch } from 'antd';
-import { DeleteTwoTone } from '@ant-design/icons';
import { fetchData } from '../utils/Utils';
import BenchmarkMath from '../utils/BenchmarkMathCalculation';
import * as math from 'mathjs';
@@ -21,7 +20,7 @@ const SummaryRow = ({ type, stats }) => {
);
};
-const MetricsTable = ({ type, id, benchmarkName }) => {
+const MetricsTable = ({ type, id, benchmarkName, onExcludedRunsChange }) => {
const [data, setData] = useState([]);
const [javaVersion, setJavaVersion] = useState([]);
useEffect(() => {
@@ -32,42 +31,46 @@ const MetricsTable = ({ type, id, benchmarkName }) => {
}
if (results && results[0]) {
const aggregateInfo = results[0].aggregateInfo;
- const fliteredData = Object.values(aggregateInfo).find(
+ const filteredItem = Object.values(aggregateInfo).find(
(item) =>
benchmarkName === item.benchmarkName &&
item.buildName.includes(type)
);
- const [firstMetric] = fliteredData.metrics;
- const rawValues = firstMetric.rawValues.map((_, i) => {
- return {
- key: i,
- iteration: i,
- enabled: true,
- metrics: fliteredData.metrics.map((metric) => {
- return {
- metricName: metric.name,
- value: metric.rawValues[i],
- };
- })
- };
- });
- const grandchildrenData = await fetchData(
- `/api/getChildBuilds?parentId=${results[0]._id}&buildName=${fliteredData.buildName}`
- );
- let javaVersion = '';
- for (const grandchildData of grandchildrenData) {
- if (grandchildData.javaVersion) {
- javaVersion = grandchildData.javaVersion;
- break;
+ if (filteredItem) {
+ const [firstMetric] = filteredItem.metrics;
+ const excludedIndices = filteredItem.excludedRuns || [];
+ const rawValues = firstMetric.rawValues.map((_, i) => {
+ return {
+ key: i,
+ iteration: i,
+ enabled: !excludedIndices.includes(i),
+ metrics: filteredItem.metrics.map((metric) => {
+ return {
+ metricName: metric.name,
+ value: metric.rawValues[i],
+ };
+ }),
+ buildName: filteredItem.buildName,
+ };
+ });
+ const grandchildrenData = await fetchData(
+ `/api/getChildBuilds?parentId=${results[0]._id}&buildName=${filteredItem.buildName}`
+ );
+ let javaVersion = '';
+ for (const grandchildData of grandchildrenData) {
+ if (grandchildData.javaVersion) {
+ javaVersion = grandchildData.javaVersion;
+ break;
+ }
}
+ setJavaVersion(javaVersion);
+ setData(rawValues);
}
- setJavaVersion(javaVersion);
- setData(rawValues);
}
};
updateData();
- }, []);
+ }, [id, benchmarkName]);
const handleToggle = (record) => {
const newData = data.map((item) => {
@@ -76,53 +79,64 @@ const MetricsTable = ({ type, id, benchmarkName }) => {
}
return item;
});
- setData(newData);
+ setData(newData);
+
+ if (onExcludedRunsChange) {
+ const excludedIndices = newData
+ .filter((item) => !item.enabled)
+ .map((item) => item.iteration);
+ onExcludedRunsChange(record.buildName, excludedIndices);
+ }
};
// const uniqueTitle = [...new Set(data.map((item) => item.metricName))];
const columns = [
- {
- title: 'Iteration',
- dataIndex: 'iteration',
- key: 'iteration',
- width: 100,
- render: (iteration) => `Run ${iteration + 1}`,
- },
- ...(data[0]?.metrics.map(({ metricName }, i) => {
- return {
- title: metricName,
- key: metricName,
- render: (_, record) => {
+ {
+ title: 'Iteration',
+ dataIndex: 'iteration',
+ key: 'iteration',
+ width: 100,
+ render: (iteration) => `Run ${iteration + 1}`,
+ },
+ ...(data[0]?.metrics.map(({ metricName }, i) => {
+ return {
+ title: metricName,
+ key: metricName,
+ render: (_, record) => {
+ return (
+
+ {record.metrics[i].value}
+
+ );
+ },
+ };
+ }) ?? []),
+ {
+ title: 'Annotate data outliers',
+ dataIndex: 'enabled',
+ key: 'enabled',
+ width: 150,
+ fixed: 'right',
+ render: (enabled, record) => {
return (
-
- {record.metrics[i].value}
-
+ handleToggle(record)}
+ checkedChildren={ }
+ unCheckedChildren="Exclude data"
+ />
);
},
- };
- }) ?? []),
- {
- title: 'Annotate data outliers',
- dataIndex: 'enabled',
- key: 'enabled',
- width: 150,
- fixed: 'right',
- render: (enabled, record) => {
- return (
- handleToggle(record)}
- checkedChildren={ }
- unCheckedChildren="Exclude data"
- />
- );
},
- },
-];
+ ];
return (
@@ -135,9 +149,11 @@ const MetricsTable = ({ type, id, benchmarkName }) => {
JDK Version: {javaVersion}
-
- Enabled: {data.filter(d => d.enabled).length} / {data.length} iterations
-
+
+ Enabled:{' '}
+ {data.filter((d) => d.enabled).length} /{' '}
+ {data.length} iterations
+
{
columns={columns}
pagination={{ defaultPageSize: 50 }}
summary={(pageData) => {
- const enabledData = pageData.filter(item => item.enabled);
+ const enabledData = pageData.filter((item) => item.enabled);
if (!enabledData.length) return null;
- const pivot = zip(...enabledData.map(d => d.metrics));
-
+ const pivot = zip(...enabledData.map((d) => d.metrics));
+
const stats = pivot.map((p) => {
- const values = p.map(({ value }) => value)
- const mean = Number(
- math.mean(values)
- ).toFixed(0);
+ const values = p.map(({ value }) => value);
+ const mean = Number(math.mean(values)).toFixed(0);
const max = math.max(values);
const min = math.min(values);
- const median = Number(
- math.median(values)
- ).toFixed(0);
- const std = Number(
- math.std(values)
- ).toFixed(2);
+ const median = Number(math.median(values)).toFixed(0);
+ const std = Number(math.std(values)).toFixed(2);
const CI =
Number(
- BenchmarkMath.confidence_interval(
- values
- ) * 100
+ BenchmarkMath.confidence_interval(values) * 100
).toFixed(2) + '%';
return {
mean,
diff --git a/test-result-summary-client/src/utils/Utils.js b/test-result-summary-client/src/utils/Utils.js
index e141e7bb..c3b4328f 100644
--- a/test-result-summary-client/src/utils/Utils.js
+++ b/test-result-summary-client/src/utils/Utils.js
@@ -89,3 +89,22 @@ export const fetchData = async (url) => {
console.error(e);
}
};
+
+export const postData = async (url, data) => {
+ if (connectAdoptiumAPI) {
+ url = 'https://trss.adoptium.net' + url;
+ }
+ try {
+ const response = await fetch(url, {
+ method: 'post',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+ const jsonResponse = await response.json();
+ return jsonResponse;
+ } catch (e) {
+ console.error(e);
+ }
+};