diff --git a/app/cdash/tests/CMakeLists.txt b/app/cdash/tests/CMakeLists.txt
index a8c612ffea..f67e63ad34 100644
--- a/app/cdash/tests/CMakeLists.txt
+++ b/app/cdash/tests/CMakeLists.txt
@@ -419,6 +419,8 @@ add_browser_test(/Browser/Pages/BuildNotesPageTest)
add_browser_test(/Browser/Pages/BuildTargetsPageTest)
+add_browser_test(/Browser/Pages/BuildInstrumentationPageTest)
+
add_browser_test(/Browser/Pages/BuildTestsPageTest)
add_browser_test(/Browser/Pages/BuildConfigurePageTest)
diff --git a/resources/js/vue/components/BuildInstrumentationPage.vue b/resources/js/vue/components/BuildInstrumentationPage.vue
index 1cfb51e094..894e4ccb87 100644
--- a/resources/js/vue/components/BuildInstrumentationPage.vue
+++ b/resources/js/vue/components/BuildInstrumentationPage.vue
@@ -49,6 +49,7 @@ import FilterBuilder from './shared/FilterBuilder.vue';
import CommandFlameChart from './shared/CommandFlameChart.vue';
import { DateTime, Duration } from 'luxon';
import LineChart from './shared/Charts/LineChart.vue';
+import Utils from './shared/Utils';
export default {
name: 'BuildInstrumentationPage',
@@ -104,6 +105,7 @@ export default {
edges {
node {
name
+ type
value
}
}
@@ -133,6 +135,7 @@ export default {
edges {
node {
name
+ type
value
}
}
@@ -249,6 +252,7 @@ export default {
language: edge.node.language,
config: edge.node.config,
disabled: !this.visibleCommandIds.has(edge.node.id),
+ measurements: Utils.numericMeasurements(edge.node.measurements.edges.map(({ node }) => node)),
};
});
},
diff --git a/resources/js/vue/components/BuildTestsPage.vue b/resources/js/vue/components/BuildTestsPage.vue
index 48b5c6c7c7..c1b2a9280b 100644
--- a/resources/js/vue/components/BuildTestsPage.vue
+++ b/resources/js/vue/components/BuildTestsPage.vue
@@ -90,6 +90,7 @@ import LoadingIndicator from './shared/LoadingIndicator.vue';
import BuildSummaryCard from './shared/BuildSummaryCard.vue';
import BuildSidebar from './shared/BuildSidebar.vue';
import TestFlameChart from './shared/TestFlameChart.vue';
+import Utils from './shared/Utils';
import { DateTime, Duration } from 'luxon';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { faChartGantt } from '@fortawesome/free-solid-svg-icons';
@@ -342,24 +343,16 @@ export default {
return [];
}
- return this.executedTests.filter((test) => test.node.startTime).map((test) => {
- const numericMeasurements = test.node.testMeasurements
- .filter((measurement) => measurement.type.startsWith('numeric'))
- .map((measurement) => ({ name: measurement.name, value: parseFloat(measurement.value) }));
- const maxRssMeasurement = numericMeasurements.find((measurement) => measurement.name === 'MaxRSS');
-
- return {
- id: test.node.id,
- name: test.node.name,
- startTime: DateTime.fromISO(test.node.startTime),
- duration: Duration.fromObject({ seconds: test.node.runningTime }),
- status: test.node.status,
- subProject: test.subProject,
- disabled: !this.visibleTestIds.has(test.node.id),
- maxRss: maxRssMeasurement ? maxRssMeasurement.value : null,
- numericMeasurements,
- };
- });
+ return this.executedTests.filter((test) => test.node.startTime).map((test) => ({
+ id: test.node.id,
+ name: test.node.name,
+ startTime: DateTime.fromISO(test.node.startTime),
+ duration: Duration.fromObject({ seconds: test.node.runningTime }),
+ status: test.node.status,
+ subProject: test.subProject,
+ disabled: !this.visibleTestIds.has(test.node.id),
+ measurements: Utils.numericMeasurements(test.node.testMeasurements),
+ }));
},
pinnedMeasurementColumns() {
diff --git a/resources/js/vue/components/shared/Charts/FlameChart.vue b/resources/js/vue/components/shared/Charts/FlameChart.vue
index e9ea11c231..ac83336534 100644
--- a/resources/js/vue/components/shared/Charts/FlameChart.vue
+++ b/resources/js/vue/components/shared/Charts/FlameChart.vue
@@ -1,11 +1,47 @@
-
+
+
+
+
+
+
+
diff --git a/resources/js/vue/components/shared/Charts/FlameChartLayout.js b/resources/js/vue/components/shared/Charts/FlameChartLayout.js
new file mode 100644
index 0000000000..38364669cc
--- /dev/null
+++ b/resources/js/vue/components/shared/Charts/FlameChartLayout.js
@@ -0,0 +1,73 @@
+/** The position of each field in a laid out item's ECharts `value` array. */
+export const Dimension = Object.freeze({
+ TRACK: 0,
+ START_TIME: 1,
+ END_TIME: 2,
+ HEATMAP_VALUE: 3,
+ ITEM_INDEX: 4,
+});
+
+export default {
+ /**
+ * Lays out timeline items for the FlameChart. Each item is placed on the first track
+ * which is free at its start time, so concurrently running items are drawn on separate rows.
+ *
+ * @param {Array} items Items with Luxon DateTime `startTime` and Duration `duration` values,
+ * and optional numeric `measurements` ({ name, value } objects).
+ * @param {string} heatmapMeasurement The name of the measurement used to color items in heatmap mode.
+ * @returns {{
+ * data: Array<{ value: Array }>,
+ * trackCount: number,
+ * overallStartTime: number,
+ * overallEndTime: number,
+ * heatmapRange: ?{ min: number, max: number },
+ * }} ECharts series data, indexed by Dimension, and the extent of the times and heatmap values.
+ */
+ layout(items, heatmapMeasurement) {
+ const trackEndTimes = [];
+ let overallStartTime = 0;
+ let overallEndTime = 0;
+ let heatmapRange = null;
+
+ const data = items
+ .map((item, index) => {
+ const startTime = item.startTime.toMillis();
+ const heatmapValue = item.measurements?.find((measurement) => measurement.name === heatmapMeasurement)?.value;
+ return {
+ startTime,
+ endTime: startTime + item.duration.toMillis(),
+ heatmapValue: Number.isFinite(heatmapValue) ? heatmapValue : null,
+ index,
+ };
+ })
+ .sort((a, b) => a.startTime - b.startTime)
+ .map(({ startTime, endTime, heatmapValue, index }, sortedIndex) => {
+ let track = trackEndTimes.findIndex((trackEndTime) => startTime >= trackEndTime);
+ if (track === -1) {
+ track = trackEndTimes.length;
+ }
+ trackEndTimes[track] = endTime;
+
+ overallStartTime = sortedIndex === 0 ? startTime : Math.min(overallStartTime, startTime);
+ overallEndTime = sortedIndex === 0 ? endTime : Math.max(overallEndTime, endTime);
+ if (heatmapValue !== null) {
+ heatmapRange = {
+ min: Math.min(heatmapRange?.min ?? heatmapValue, heatmapValue),
+ max: Math.max(heatmapRange?.max ?? heatmapValue, heatmapValue),
+ };
+ }
+
+ return {
+ value: [track, startTime, endTime, heatmapValue, index],
+ };
+ });
+
+ return {
+ data,
+ trackCount: trackEndTimes.length,
+ overallStartTime,
+ overallEndTime,
+ heatmapRange,
+ };
+ },
+};
diff --git a/resources/js/vue/components/shared/CommandFlameChart.vue b/resources/js/vue/components/shared/CommandFlameChart.vue
index 3725c60c42..c8fc9f8e28 100644
--- a/resources/js/vue/components/shared/CommandFlameChart.vue
+++ b/resources/js/vue/components/shared/CommandFlameChart.vue
@@ -1,35 +1,16 @@
@@ -61,6 +42,7 @@ export default {
* command: String,
* language: String,
* config: String,
+ * measurements: Array, // Optional numeric measurements: { name: String, value: Number }
* }
*/
commands: {
@@ -80,199 +62,33 @@ export default {
CMAKE_INSTALL: '#CC79A7',
INSTALL: '#56B4E9',
},
- commandBarHeight: 15,
- commandBarSpacing: 5,
selectedCommandId: null,
};
},
computed: {
- processedChartData() {
- if (!this.commands || this.commands.length === 0) {
- return {
- data: [],
- tracks: [],
- overallStartTime: 0,
- overallEndTime: 0,
- };
- }
-
- const rawCommandData = this.commands.map((cmd, index) => {
- const startTime = cmd.startTime.toMillis();
- const endTime = startTime + cmd.duration.toMillis();
- return {
- ...cmd,
- startTime,
- endTime,
- originalIndex: index,
- };
- });
-
- const overallStartTime = rawCommandData.reduce((start, cmd) => Math.min(start, cmd.startTime), Infinity);
- const overallEndTime = rawCommandData.reduce((end, cmd) => Math.max(end, cmd.endTime), -Infinity);
-
- const trackEndTimes = [];
- const processedData = [];
- const sortedData = rawCommandData.slice().sort((a, b) => a.startTime - b.startTime);
-
- sortedData.forEach((command) => {
- let placed = false;
- let trackIndex = -1;
- for (let i = 0; i < trackEndTimes.length; i++) {
- if (command.startTime >= trackEndTimes[i]) {
- trackEndTimes[i] = command.endTime;
- trackIndex = i;
- placed = true;
- break;
- }
- }
- if (!placed) {
- trackIndex = trackEndTimes.length;
- trackEndTimes.push(command.endTime);
- }
-
- const processedCommand = {
- value: [
- trackIndex,
- command.startTime,
- command.endTime,
- command.duration.toMillis(),
- command.originalIndex,
- command.type,
- command.disabled,
- command.targetName,
- command.source,
- command.language,
- command.config,
- command.id,
- ],
- };
- processedData.push(processedCommand);
- });
-
- return {
- data: processedData,
- tracks: trackEndTimes.map((_, i) => `Thread ${i + 1}`),
- overallStartTime,
- overallEndTime,
- };
- },
-
- totalChartHeight() {
- const numtracks = this.processedChartData.tracks.length;
- return ((this.commandBarHeight + this.commandBarSpacing) * numtracks) + 60;
+ chartItems() {
+ return this.commands.map((command) => ({
+ ...command,
+ category: command.type,
+ }));
},
},
methods: {
- getTooltipElement(params) {
- if (!params.data.value || !Array.isArray(params.data.value)) {
- return '';
- }
- const data = params.data.value;
- const type = data[5];
- const duration = Utils.formatDuration(data[3]);
- const targetName = data[7];
- const source = data[8];
- const language = data[9];
- const config = data[10];
-
- const container = document.createElement('div');
-
- const appendLine = (label, value, isBold = false, isCode = false) => {
- if (value) {
- if (container.childNodes.length > 0) {
- container.appendChild(document.createElement('br'));
- }
- const labelNode = document.createTextNode(`${label}: `);
- container.appendChild(labelNode);
-
- let valueNode;
- if (isCode) {
- valueNode = document.createElement('div');
- valueNode.className = 'tw-font-mono tw-bg-gray-100 tw-p-1 tw-rounded tw-whitespace-pre-wrap tw-break-all !tw-mt-0';
- valueNode.textContent = value;
- } else if (isBold) {
- valueNode = document.createElement('b');
- valueNode.textContent = value;
- } else {
- valueNode = document.createTextNode(value);
- }
- container.appendChild(valueNode);
- }
- };
-
- appendLine('Target', targetName, true);
- appendLine('Type', type);
- appendLine('Duration', duration);
- appendLine('Language', language);
- appendLine('Config', config);
- appendLine('Source', source, false, true);
-
- return container;
- },
-
- handleResize() {
- // No-op, vue-echarts handles resize via autoresize prop
- },
-
- onCellClick(params) {
- if (!params.data.value || !Array.isArray(params.data.value)) {
- return;
- }
-
- const clickedCommandId = parseInt(params.data.value[11]);
- if (this.selectedCommandId === clickedCommandId) {
- this.selectedCommandId = null;
- } else {
- this.selectedCommandId = clickedCommandId;
- }
+ tooltipFields(command) {
+ return [
+ { label: 'Target', value: command.targetName, isBold: true },
+ { label: 'Type', value: command.type },
+ { label: 'Duration', value: Utils.formatDuration(command.duration.toMillis()) },
+ { label: 'Language', value: command.language },
+ { label: 'Config', value: command.config },
+ { label: 'Source', value: command.source, isCode: true },
+ ];
},
- renderItem(params, api) {
- const trackIndex = api.value(0);
- const start = api.coord([api.value(1), trackIndex]);
- const end = api.coord([api.value(2), trackIndex]);
- if (!start || !end) {
- return;
- }
-
- const height = this.commandBarHeight;
- const itemType = api.value(5);
- const isDisabled = api.value(6);
- const commandId = api.value(11);
-
- const style = {
- fill: this.colors[itemType],
- opacity: 0.85,
- };
-
- if (isDisabled) {
- Object.assign(style, {
- fill: '#d1d5db',
- stroke: '#d1d5db',
- lineWidth: 0.5,
- opacity: 0.4,
- });
- }
-
- if (commandId === this.selectedCommandId) {
- Object.assign(style, {
- stroke: '#000000',
- lineWidth: 2,
- });
- }
-
- return {
- type: 'rect',
- shape: {
- x: start[0],
- y: start[1] - (height / 2),
- width: end[0] - start[0],
- height: height,
- },
- style: style,
- };
+ onItemClick(command) {
+ this.selectedCommandId = this.selectedCommandId === command.id ? null : command.id;
},
},
};
diff --git a/resources/js/vue/components/shared/CommandInfoCard.vue b/resources/js/vue/components/shared/CommandInfoCard.vue
index b17ba0da5e..ba6cd7f7a2 100644
--- a/resources/js/vue/components/shared/CommandInfoCard.vue
+++ b/resources/js/vue/components/shared/CommandInfoCard.vue
@@ -86,7 +86,7 @@
{{ measurement.name }}
|
- {{ addUnitsToSpecialMeasurements(measurement.name, measurement.value) }} |
+ {{ formatMeasurement(measurement) }} |
@@ -151,6 +151,7 @@ export default {
node {
id
name
+ type
value
}
}
@@ -194,12 +195,10 @@ export default {
return Utils.formatBytes(inputInBytes);
},
- addUnitsToSpecialMeasurements(measurementName, measurementValue) {
- if (['BeforeHostMemoryUsed', 'AfterHostMemoryUsed'].includes(measurementName)) {
- return Utils.formatBytesFromKib(measurementValue);
- }
-
- return measurementValue;
+ /** Formats numeric measurements with units, and shows any other measurement's value as-is. */
+ formatMeasurement(measurement) {
+ const [numericMeasurement] = Utils.numericMeasurements([measurement]);
+ return numericMeasurement ? Utils.formatMeasurement(numericMeasurement.name, numericMeasurement.value) : measurement.value;
},
},
};
diff --git a/resources/js/vue/components/shared/TestFlameChart.vue b/resources/js/vue/components/shared/TestFlameChart.vue
index 7ed456bba2..c524855341 100644
--- a/resources/js/vue/components/shared/TestFlameChart.vue
+++ b/resources/js/vue/components/shared/TestFlameChart.vue
@@ -1,54 +1,13 @@
-
-
-
-
-
-
-
+