Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 35 additions & 30 deletions resources/js/vue/components/BuildTestsPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const TEST_QUERY = gql`
testMeasurements(filters: $measurementFilters) {
id
name
type
value
}
}
Expand All @@ -137,6 +138,7 @@ const TEST_QUERY = gql`
testMeasurements(filters: $measurementFilters) {
id
name
type
value
}
}
Expand Down Expand Up @@ -235,13 +237,7 @@ export default {
return {
buildid: this.buildId,
filters: this.initialFilters,
measurementFilters: {
any: this.pinnedMeasurements.map((name) => ({
eq: {
name: name,
},
})),
},
measurementFilters: this.measurementFilters,
};
},
},
Expand All @@ -254,13 +250,7 @@ export default {
return {
buildid: this.buildId,
filters: {},
measurementFilters: {
any: this.pinnedMeasurements.map((name) => ({
eq: {
name: name,
},
})),
},
measurementFilters: this.measurementFilters,
};
},
},
Expand All @@ -272,13 +262,7 @@ export default {
return {
buildid: this.previousBuildId,
filters: this.initialFilters,
measurementFilters: {
any: this.pinnedMeasurements.map((name) => ({
eq: {
name: name,
},
})),
},
measurementFilters: this.measurementFilters,
};
},
skip() {
Expand All @@ -300,6 +284,18 @@ export default {
};
},

// Fetch the pinned measurement columns and numeric measurements used by the
// test execution timeline and its tooltips. Unpinned text and file measurements
// aren't displayed on this page.
measurementFilters() {
return {
any: [
...this.pinnedMeasurements.map((name) => ({ eq: { name: name } })),
{ contains: { type: 'numeric/' } },
],
};
},

filteredTests() {
if (!this.onlyDelta) {
return this.tests;
Expand Down Expand Up @@ -346,15 +342,24 @@ export default {
return [];
}

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),
}));
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,
};
});
},

pinnedMeasurementColumns() {
Expand Down
10 changes: 10 additions & 0 deletions resources/js/vue/components/shared/Charts/FlameChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
GridComponent,
TooltipComponent,
DataZoomComponent,
VisualMapContinuousComponent,
} from 'echarts/components';
import VChart from 'vue-echarts';
import Utils from '../Utils';
Expand All @@ -26,6 +27,7 @@ use([
GridComponent,
TooltipComponent,
DataZoomComponent,
VisualMapContinuousComponent,
]);

export default {
Expand Down Expand Up @@ -74,6 +76,12 @@ export default {
required: false,
default: 400,
},
/** An echarts `visualMap` config (continuous), used to color items by a data dimension. */
visualMap: {
type: Object,
required: false,
default: null,
},
},

// eslint-disable-next-line vue/require-emit-validator
Expand All @@ -82,6 +90,8 @@ export default {
computed: {
chartOptions() {
return {
// An array lets vue-echarts remove the visual map without resetting the zoom.
visualMap: this.visualMap ? [this.visualMap] : [],
tooltip: {
confine: true,
trigger: 'item',
Expand Down
118 changes: 113 additions & 5 deletions resources/js/vue/components/shared/TestFlameChart.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
<template>
<div>
<div
v-if="hasMaxRssData"
class="tw-flex tw-justify-end tw-px-2.5 tw-pt-2.5"
>
<select
v-model="colorMode"
class="tw-select tw-select-bordered tw-select-sm"
data-test="test-flame-chart-color-mode"
>
<option value="status">
Passing/Failing
</option>
<option value="maxRss">
MaxRSS
</option>
</select>
</div>
<div
v-if="colorMode === 'status'"
id="test-legend-container"
class="tw-flex tw-flex-wrap tw-justify-center tw-gap-x-5 tw-gap-y-2.5 tw-p-2.5 tw-text-xs"
class="tw-flex tw-flex-wrap tw-justify-center tw-items-center tw-gap-x-5 tw-gap-y-2.5 tw-p-2.5 tw-text-xs"
>
<div
v-for="(colorClass, status) in colorClasses"
Expand All @@ -27,6 +45,7 @@
:render-item="renderItem"
:large="false"
:progressive="0"
:visual-map="maxRssVisualMap"
@click="onCellClick"
/>
</div>
Expand Down Expand Up @@ -56,6 +75,14 @@ function resolveTailwindColor(colorClass) {
return `rgb(${r}, ${g}, ${b})`;
}

// Known numeric CTest/instrumentation measurements and their base unit, keyed by
// measurement name. Values are scaled to the largest useful unit for display.
// Measurements not listed here (e.g. Processors, a CPU load average, or a
// project-defined custom measurement) are shown as a plain number.
const KIB_MEMORY_MEASUREMENTS = new Set(['MaxRSS', 'AfterHostMemoryUsed', 'BeforeHostMemoryUsed']);
const MICROSECOND_TIME_MEASUREMENTS = new Set(['UserTime', 'SystemTime']);
const SECOND_TIME_MEASUREMENTS = new Set(['Execution Time']);

export default {
name: 'TestFlameChart',

Expand All @@ -73,6 +100,7 @@ export default {

data() {
return {
colorMode: 'status',
colorClasses: {
Passed: 'tw-bg-success',
Failed: 'tw-bg-error',
Expand All @@ -81,6 +109,8 @@ export default {
testBarHeight: 15,
testBarSpacing: 5,
minimumBarWidth: 2,
// Bars with no MaxRSS measurement, shown only in MaxRSS color mode.
noDataColor: '#e1e0d9',
};
},

Expand Down Expand Up @@ -141,6 +171,8 @@ export default {
test.name,
test.subProject,
test.id,
Number.isFinite(test.maxRss) ? test.maxRss : null,
test.numericMeasurements ?? [],
],
};
processedData.push(processedTest);
Expand All @@ -158,6 +190,48 @@ export default {
const numtracks = this.processedChartData.tracks.length;
return ((this.testBarHeight + this.testBarSpacing) * numtracks) + 60;
},

hasMaxRssData() {
return (this.tests ?? []).some((test) => Number.isFinite(test.maxRss));
},

maxRssRange() {
let min = Infinity;
let max = -Infinity;
for (const test of this.tests ?? []) {
if (Number.isFinite(test.maxRss)) {
min = Math.min(min, test.maxRss);
max = Math.max(max, test.maxRss);
}
}
return min === Infinity ? { min: 0, max: 0 } : { min, max };
},

// Let ECharts use its theme's default gradient by leaving inRange unset.
// Dimension 10 is the MaxRSS value in each item's `value` array.
// Read via `api.visual('color')` in renderItem.
maxRssVisualMap() {
if (this.colorMode !== 'maxRss' || !this.hasMaxRssData) {
return null;
}
const { min, max } = this.maxRssRange;
return {
show: false,
type: 'continuous',
seriesIndex: 0,
dimension: 10,
min,
max: max > min ? max : min + 1,
};
},
},

watch: {
hasMaxRssData(hasData) {
if (!hasData) {
this.colorMode = 'status';
}
},
},

created() {
Expand All @@ -183,6 +257,32 @@ export default {
}
},

formatMeasurementValue(value) {
if (!Number.isFinite(value)) {
return String(value);
}
return Number.isInteger(value) ? value.toLocaleString() : value.toFixed(2);
},

// Dispatches each measurement to the formatter for its base unit, scaling to
// the largest useful unit. Measurements with no known unit are shown as a
// plain number.
formatMeasurement(name, value) {
if (!Number.isFinite(value)) {
return String(value);
}
if (KIB_MEMORY_MEASUREMENTS.has(name)) {
return Utils.formatBytesFromKib(value);
}
if (MICROSECOND_TIME_MEASUREMENTS.has(name)) {
return Utils.formatDuration(value / 1000);
}
if (SECOND_TIME_MEASUREMENTS.has(name)) {
return Utils.formatDuration(value * 1000);
}
return this.formatMeasurementValue(value);
},

getTooltipElement(params) {
if (!params.data.value || !Array.isArray(params.data.value)) {
return '';
Expand All @@ -192,6 +292,7 @@ export default {
const status = this.humanReadableTestStatus(data[5]);
const name = data[7];
const subProject = data[8];
const numericMeasurements = data[11] ?? [];

const container = document.createElement('div');

Expand All @@ -217,6 +318,9 @@ export default {
appendLine('SubProject', subProject);
appendLine('Status', status);
appendLine('Duration', duration);
numericMeasurements.forEach((measurement) => {
appendLine(measurement.name, this.formatMeasurement(measurement.name, measurement.value));
});

return container;
},
Expand All @@ -241,11 +345,15 @@ export default {
const height = this.testBarHeight;
const status = this.humanReadableTestStatus(api.value(5));
const isDisabled = api.value(6);
const maxRss = api.value(10);

const style = {
fill: this.resolvedColors[status],
opacity: 0.85,
};
let style;
if (this.colorMode === 'maxRss') {
const hasMaxRss = Number.isFinite(maxRss);
style = { fill: hasMaxRss ? api.visual('color') : this.noDataColor, opacity: 0.9 };
} else {
style = { fill: this.resolvedColors[status], opacity: 0.85 };
}

if (isDisabled) {
Object.assign(style, {
Expand Down
Loading