From 79a3c290264358122cc142061eb74722c61ba052 Mon Sep 17 00:00:00 2001 From: yuxuan-ms <43603130+yuxuan-ms@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:51:01 +0800 Subject: [PATCH] Add timer display on resource monitor view. (#1091) Co-authored-by: yuxuan-ms --- doc/newsfragments/1384_changed.report_ui.rst | 1 + .../AssertionTypes/LogfileMatchAssertion.js | 3 +- .../LogfileMatchAssertion.test.js.snap | 3 + .../src/AssertionPane/ResourcePanel.js | 659 ++++++++++++------ .../web_ui/testing/src/Common/defaults.js | 4 + testplan/web_ui/testing/src/Common/utils.js | 17 +- .../web_ui/testing/src/Report/reportUtils.js | 6 +- 7 files changed, 466 insertions(+), 227 deletions(-) create mode 100755 doc/newsfragments/1384_changed.report_ui.rst diff --git a/doc/newsfragments/1384_changed.report_ui.rst b/doc/newsfragments/1384_changed.report_ui.rst new file mode 100755 index 000000000..fb333e257 --- /dev/null +++ b/doc/newsfragments/1384_changed.report_ui.rst @@ -0,0 +1 @@ +Add event timing view on report UI diff --git a/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/LogfileMatchAssertion.js b/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/LogfileMatchAssertion.js index df8b20c76..2ccdb7f2d 100644 --- a/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/LogfileMatchAssertion.js +++ b/testplan/web_ui/testing/src/AssertionPane/AssertionTypes/LogfileMatchAssertion.js @@ -37,10 +37,11 @@ const LogfileMatchAssertion = ({ assertion }) => { const timeoutMsg = (assertion.passed ? "Passed" : "Failed") + ` in ${timeout} seconds.`; - const entries = [...results, ...failure].map((entry) => { + const entries = [...results, ...failure].map((entry, index) => { const { matched, pattern, start_pos: startPos, end_pos: endPos } = entry; return ( { return { format: { @@ -69,6 +78,18 @@ const prettySizeTicks = (options) => { }; }; +const AnchorDiv = ({ elementIds }) => { + const anchorDiv = []; + for (let uid of elementIds) { + anchorDiv.push(
); + } + return anchorDiv; +}; + +AnchorDiv.propTypes = { + elementIds: PropTypes.array, +}; + const ResourceGraph = ({ data, label, @@ -99,7 +120,6 @@ const ResourceGraph = ({ return (
{ if (ticksCallback) { @@ -192,6 +212,124 @@ ResourceGraph.propTypes = { fillColor: PropTypes.string, }; +const TimerGraph = ({ timerEntries, startTime, endTime }) => { + const labels = []; + const timezoneOffset = new Date().getTimezoneOffset(); + const datasets = [ + { + data: [], + backgroundColor: [], + barThickness: 6, + minBarLength: 2, + }, + ]; + + timerEntries.forEach((entity) => { + labels.push(entity.name); + datasets[0].backgroundColor.push( + // TODO: move color to a constant map + entity.status === STATUS_CATEGORY.passed ? GREEN : RED + ); + let start = null; + if (!_.isNil(entity.timer[0].setup?.start)) { + start = entity.timer[0].setup.start; + } else if (!_.isNil(entity.timer[0].run?.start)) { + start = entity.timer[0].run.start; + } else { + datasets[0].data.push(null); + return; + } + + if (!_.isNil(entity.timer[0].teardown?.end)) { + datasets[0].data.push([start, entity.timer[0].teardown.end]); + } else if (!_.isNil(entity.timer[0].run?.end)) { + datasets[0].data.push([start, entity.timer[0].run.end]); + } else { + datasets[0].data.push(null); + } + }); + const height = 10 + timerEntries.length * 5; + return ( +
+ { + return `${dateFormat( + dateAddMinutes(value, timezoneOffset), + "H:mm:ss" + )}Z`; + }, + }, + }, + y: { + ticks: { + autoSkip: false, + }, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + callbacks: { + label: (context) => { + return [ + `From: ${dateFormat( + dateAddMinutes(context.raw[0], timezoneOffset), + "H:mm:ss" + )}Z`, + `To: ${dateFormat( + dateAddMinutes(context.raw[1], timezoneOffset), + "H:mm:ss" + )}Z`, + `Duration: ${( + (context.raw[1] - context.raw[0]) / + 1000 + ).toFixed(2)}s`, + ]; + }, + }, + }, + }, + }} + data={{ + labels: labels, + datasets: datasets, + }} + /> +
+ ); +}; + +TimerGraph.propTypes = { + timerEntries: PropTypes.array, + startTime: PropTypes.number, + endTime: PropTypes.number, +}; + const HostResourceGraphContainer = ({ time, cpu, @@ -278,7 +416,6 @@ const HostResourceGraphContainer = ({ const HostMetaTitle = ({ uid, - onClick, hostname, cpuCount, diskSize, @@ -289,32 +426,45 @@ const HostMetaTitle = ({ let uidDiv = null; if (isLocal) { uidDiv = ( - - LocalHost - + <> + {hostname} + + {LOCALHOST} + + ); } else if (hostname !== uid) { uidDiv = ( - - {uid} - + <> + {hostname} + + {uid} + + ); + } else { + uidDiv = <>{hostname}; } - + const cpuInfo = _.isNil(cpuCount) ? null : ( + CPU: {cpuCount * 100}% + ); + const memoryInfo = _.isNil(memorySize) ? null : ( + Memory Size: {prettyBytes(memorySize, { binary: true })} + ); + const diskInfo = _.isNil(diskSize) ? null : ( + Disk Size: {prettyBytes(diskSize)} + ); + const diskPathInfo = _.isNil(diskPath) ? null : ( + Disk Path: {diskPath} + ); return (
-
- {hostname} - {uidDiv} -
+
{uidDiv}
- CPU: {cpuCount * 100}% Memory Size:{" "} - {prettyBytes(memorySize, { binary: true })} Disk Size:{" "} - {prettyBytes(diskSize)} Disk Path: {diskPath} + {cpuInfo} {memoryInfo} {diskInfo} {diskPathInfo}
); @@ -322,7 +472,6 @@ const HostMetaTitle = ({ HostMetaTitle.propTypes = { uid: PropTypes.string, - onClick: PropTypes.func, hostname: PropTypes.string, cpuCount: PropTypes.number, diskSize: PropTypes.number, @@ -332,91 +481,95 @@ HostMetaTitle.propTypes = { }; const HostResource = ({ - metadata, - resource_meta_path, + entryTag, + resourceMetaPath, + resourceEntry, startTime, endTime, - isOpen, - onClick, - entryIndex, }) => { const [hostResourceGraph, setHostResourceGraph] = useState(null); - const [errorInfo, setErrorInfo] = useState(null); - useLayoutEffect(() => { - if (_.isEmpty(metadata.resource_file)) { - setHostResourceGraph(
No Resource Data
); + useEffect(() => { + if (_.isEmpty(resourceEntry.metaData.resource_file)) { + setHostResourceGraph(<>{null}); } else { const hostResourceUrl = getResourceUrl( - resource_meta_path, - metadata.resource_file + resourceMetaPath, + resourceEntry.metaData.resource_file ); axios .get(hostResourceUrl) .then((response) => { setHostResourceGraph( - + <> + + ); }) .catch((error) => { console.error(error); - setErrorInfo(error.message); + setHostResourceGraph(
{error.message}
); }); } - }, [resource_meta_path, metadata, startTime, endTime]); + }, [resourceMetaPath, resourceEntry, startTime, endTime]); - if (errorInfo) { - return {errorInfo}; - } else { - return ( -
- -
- {hostResourceGraph} -
-
+ let timerGraph = undefined; + let anchorDiv = undefined; + if (!_.isNil(resourceEntry.timer)) { + anchorDiv = ( + e.uid)} /> + ); + + timerGraph = ( + ); } + + return ( +
+ {anchorDiv} + + {hostResourceGraph} + {timerGraph} +
+ ); }; HostResource.propTypes = { - metadata: PropTypes.object, + entryTag: PropTypes.string, + resourceMetaPath: PropTypes.string, + resourceEntry: PropTypes.object, startTime: PropTypes.number, endTime: PropTypes.number, - isOpen: PropTypes.bool, - onClick: PropTypes.func, }; -const TopUsageBanner = ({ - maxCPU, - maxMemory, - maxDisk, - maxIOPS, - onClickCallBack, -}) => { +const TopUsageBanner = ({ maxCPU, maxMemory, maxDisk, maxIOPS }) => { const itemStyle = { padding: "8px", border: "1px solid #0597ff", @@ -427,42 +580,26 @@ const TopUsageBanner = ({ fontWeight: "500", cursor: "pointer", }; - const cpuDiv = _.isEmpty(maxCPU?.value, true) ? null : ( -
+ const cpuDiv = _.isNil(maxCPU?.value, true) ? null : ( +
Max CPU Usage: {maxCPU.value}%
); - const memDiv = _.isEmpty(maxMemory?.value, true) ? null : ( -
+ const memDiv = _.isNil(maxMemory?.value) ? null : ( +
Max Memory Usage: {prettyBytes(maxMemory.value, { binary: true })}
); - const diskDiv = _.isEmpty(maxDisk?.value, true) ? null : ( -
+ const diskDiv = _.isNil(maxDisk?.value) ? null : ( +
Max Disk Usage: {prettyBytes(maxDisk.value)}
); - const iopsDiv = _.isEmpty(maxIOPS?.value, true) ? null : ( -
+ const iopsDiv = _.isNil(maxIOPS?.value) ? null : ( +
Max IOPS: {maxIOPS.value.toFixed(2)}
); @@ -495,108 +632,102 @@ TopUsageBanner.propTypes = { onClickCallBack: PropTypes.func, }; -const maxVal = (meta, key, maxRef) => { - if (maxRef.value === undefined || meta[key] > maxRef.value) { - maxRef.value = meta[key]; - maxRef.uid = meta.uid; +const maxVal = (meta, maxRef, hostTag) => { + const keyMap = { + max_cpu: "maxCPU", + max_memory: "maxMemory", + max_disk: "maxDisk", + max_iops: "maxIOPS", + }; + for (let key in keyMap) { + if (maxRef[keyMap[key]] === undefined) { + maxRef[keyMap[key]] = {}; + } + + if ( + maxRef[keyMap[key]].value === undefined || + meta[key] > maxRef[keyMap[key]].value + ) { + maxRef[keyMap[key]].value = meta[key]; + maxRef[keyMap[key]].tag = hostTag; + } } }; const ResourceContainer = ({ - defaultExpandStatus, - defaultJumpHostUid, - resource_meta_path, - hostMeta, + resourceMetaPath, + resourceEntries, + timerEntries, testStartTime, testEndTime, }) => { - const [graphState, setGraphState] = useState({ - jumpHostUid: defaultJumpHostUid, - expendHost: defaultExpandStatus, - }); - const [isJumped, setIsJumped] = useState(false); - const entryPrefix = "hostEntry"; + const routeMatch = useRouteMatch(); + useEffect(() => { - if (isJumped === false) { - for (let [index, ele] of hostMeta.entries()) { - if (ele.uid === graphState.jumpHostUid) { - setIsJumped(true); - setTimeout(() => { - document - .querySelector(`#${entryPrefix}${index}`) - ?.scrollIntoView(true); - }, 100); - - break; - } + const selection = routeMatch.params?.selection; + if (selection) { + const selectedDiv = document.getElementById( + `${anchorPrefix}${selection}` + ); + if (selectedDiv) { + selectedDiv.scrollIntoView({ hehavior: "smooth", block: "start" }); } } - }, [hostMeta, graphState.jumpHostUid, isJumped]); - - const updateExpandStatus = (hostUid) => { - return () => { - if (graphState.expendHost[hostUid] === true) { - setGraphState((prev) => { - const newState = { ...prev }; - newState.expendHost = { ...prev.expendHost, ...{ [hostUid]: false } }; - return newState; - }); + }, [routeMatch]); + + let usageBanner; + const hostContent = []; + let hostIndex = 0; + + if (!_.isEmpty(resourceEntries)) { + const maxRes = {}; + + for (let host in resourceEntries) { + hostIndex++; + const hostMeta = resourceEntries[host].metaData; + const hostTag = `${hostEntryPrefix}${hostIndex}`; + maxVal(hostMeta, maxRes, hostTag); + const hostDiv = ( + + ); + if (host === LOCALHOST) { + hostContent.unshift(hostDiv); // insert to first } else { - setGraphState((prev) => { - const newState = { ...prev }; - newState.expendHost = { ...prev.expendHost, ...{ [hostUid]: true } }; - return newState; - }); + hostContent.push(hostDiv); } - }; - }; - - const jumpToHost = (hostUid) => { - return () => { - setGraphState((prev) => { - const newState = { ...prev }; - newState.jumpHostUid = hostUid; - newState.expendHost = { ...prev.expendHost, ...{ [hostUid]: true } }; - return newState; - }); - setIsJumped(false); - }; - }; - - const hostEnties = []; - const maxCPU = {}; - const maxMemory = {}; - const maxDisk = {}; - const maxIOPS = {}; - - hostMeta.forEach((hostValue, index) => { - maxVal(hostValue, "max_cpu", maxCPU); - maxVal(hostValue, "max_memory", maxMemory); - maxVal(hostValue, "max_disk", maxDisk); - maxVal(hostValue, "max_iops", maxIOPS); - hostEnties.push( - - ); - }); + } + if (!_.isEmpty(maxRes)) { + usageBanner = ; + } + } - const usageBanner = ( - - ); + const timerContent = []; + if (!_.isEmpty(timerEntries)) { + for (let hostId in timerEntries) { + hostIndex++; + const hostTag = `${hostEntryPrefix}${hostIndex}`; + timerContent.push( + + ); + } + } return ( <> @@ -605,63 +736,156 @@ const ResourceContainer = ({ key="panel-blank-header-margin" style={{ height: "20px", width: "100%" }} >
- {hostEnties} + {hostContent} + {timerContent} ); }; ResourceContainer.propTypes = { - defaultExpandStatus: PropTypes.object, - defaultJumpHostUid: PropTypes.string, - resource_meta_path: PropTypes.string, - hostMeta: PropTypes.array, + resourceMetaPath: PropTypes.string, + resourceEntries: PropTypes.object, + timerEntries: PropTypes.object, testStartTime: PropTypes.number, testEndTime: PropTypes.number, }; +const normalizeTimer = (timer) => { + let timerArray = []; + if (Array.isArray(timer?.run)) { + for (let timerIndex in timer.run) { + timerArray.push({ + setup: { + start: timeToTimestamp(timer.setup[timerIndex]?.start), + end: timeToTimestamp(timer.setup[timerIndex]?.end), + }, + run: { + start: timeToTimestamp(timer.run[timerIndex]?.start), + end: timeToTimestamp(timer.run[timerIndex]?.end), + }, + teardown: { + start: timeToTimestamp(timer.teardown[timerIndex]?.start), + end: timeToTimestamp(timer.teardown[timerIndex]?.end), + }, + }); + } + } else { + timerArray.push({ + setup: { + start: timeToTimestamp(timer.setup?.start), + end: timeToTimestamp(timer.setup?.end), + }, + run: { + start: timeToTimestamp(timer.run?.start), + end: timeToTimestamp(timer.run?.end), + }, + teardown: { + start: timeToTimestamp(timer.teardown?.start), + end: timeToTimestamp(timer.teardown?.end), + }, + }); + } + return timerArray; +}; + +const extractTimeInfo = (report) => { + const timeInfo = {}; + if (Array.isArray(report.entries)) { + for (let mtIndex in report.entries) { + const timer = normalizeTimer(report.entries[mtIndex].timer); + let host = report.entries[mtIndex].host + ? report.entries[mtIndex].host + : LOCALHOST; + if (_.isNil(timeInfo[host])) { + timeInfo[host] = []; + } + timeInfo[host].push({ + name: report.entries[mtIndex].name, + uid: report.entries[mtIndex].uid, + status: report.entries[mtIndex].status, + timer: timer, + }); + } + } + return timeInfo; +}; + /** * Render the Resource monitor and event data. */ -const ResourcePanel = ({ report, selectedHostUid }) => { +const ResourcePanel = ({ report }) => { const [resourceMeta, setResourceMeta] = useState(null); const [errorInfo, setErrorInfo] = useState(null); useEffect(() => { - if (_.isEmpty(report.resource_meta_path) || resourceMeta) { + if (resourceMeta || errorInfo) { + return; + } + + const timerEntries = extractTimeInfo(report); + + if (_.isEmpty(report.resource_meta_path)) { + setResourceMeta({ + resourceEntries: null, + timerEntries: timerEntries, + }); return; } else { const resourceUrl = getResourceUrl(report.resource_meta_path); axios .get(resourceUrl) .then((response) => { - setResourceMeta(response.data); + const resourceEntries = {}; + for (let resourceIndex in response.data.entries) { + const hostEntry = response.data.entries[resourceIndex]; + if (hostEntry.is_local && !_.isNil(timerEntries[LOCALHOST])) { + resourceEntries[LOCALHOST] = { + metaData: hostEntry, + timer: timerEntries[LOCALHOST], + }; + delete timerEntries[LOCALHOST]; + } else if (!_.isNil(timerEntries[hostEntry.uid])) { + resourceEntries[hostEntry.uid] = { + metaData: hostEntry, + timer: timerEntries[hostEntry.uid], + }; + delete timerEntries[hostEntry.uid]; + } + } + + setResourceMeta({ + resourceEntries: resourceEntries, + timerEntries: timerEntries, + }); }) .catch((error) => { console.error(error); setErrorInfo(error.message); }); } - }, [resourceMeta, report]); + }, [resourceMeta, report, errorInfo]); let content = null; - if (_.isEmpty(report.resource_meta_path)) { - content =
No Resource data
; - } else if (errorInfo) { + + if (errorInfo) { content =
{errorInfo}
; + } else if (_.isEmpty(resourceMeta)) { + content =
Resource data was not collected for this test report.
; } else if (resourceMeta) { - const hostMeta = resourceMeta.entries; - const testStartTime = Number(new Date(report.timer.run.start)); - const testEndTime = Number(new Date(report.timer.run.end)); - const defaultExpandStatus = selectedHostUid - ? { [selectedHostUid]: true } - : {}; + const { resourceEntries, timerEntries } = resourceMeta; + const testStartTime = Array.isArray(report.timer.run) + ? new Date(report.timer.run[0].start).getTime() + : new Date(report.timer.run.start).getTime(); + const testEndTime = Array.isArray(report.timer.run) + ? new Date(report.timer.run[0].end).getTime() + : new Date(report.timer.run.end).getTime(); + content = ( @@ -686,7 +910,6 @@ const ResourcePanel = ({ report, selectedHostUid }) => { ResourcePanel.propTypes = { report: PropTypes.object, - selectedHostUid: PropTypes.string, }; export default ResourcePanel; diff --git a/testplan/web_ui/testing/src/Common/defaults.js b/testplan/web_ui/testing/src/Common/defaults.js index d0320c5f9..3b2a8066a 100644 --- a/testplan/web_ui/testing/src/Common/defaults.js +++ b/testplan/web_ui/testing/src/Common/defaults.js @@ -256,6 +256,9 @@ const LOG_TYPE = { warning: "WARNING", }; + +const LOCALHOST = "Localhost"; + export { BLUE, DARK_BLUE, @@ -300,4 +303,5 @@ export { POLL_MS, defaultFixSpec, LOG_TYPE, + LOCALHOST, }; diff --git a/testplan/web_ui/testing/src/Common/utils.js b/testplan/web_ui/testing/src/Common/utils.js index a4d6b26fd..a6381dbc9 100644 --- a/testplan/web_ui/testing/src/Common/utils.js +++ b/testplan/web_ui/testing/src/Common/utils.js @@ -16,17 +16,27 @@ function calcExecutionTime(entry) { elapsed = 0; entry.timer.run.forEach((interval) => { elapsed += - new Date(interval.end).getTime() - new Date(interval.start).getTime(); + timeToTimestamp(interval.end) - timeToTimestamp(interval.start); }); } else { elapsed = - new Date(entry.timer.run.end).getTime() - - new Date(entry.timer.run.start).getTime(); + timeToTimestamp(entry.timer.run.end) - + timeToTimestamp(entry.timer.run.start); } } return elapsed; } +/** + * Convert string to timestamp. + * + * @param {object|string} time + * @returns {number} + */ +function timeToTimestamp(time) { + return typeof time === "string" ? new Date(time).getTime() : time; +} + /** * Get the data to be used when displaying the nav entry. * @@ -166,6 +176,7 @@ function formatMilliseconds(durationInMilliseconds) { export { calcExecutionTime, + timeToTimestamp, getNavEntryDisplayData, any, sorted, diff --git a/testplan/web_ui/testing/src/Report/reportUtils.js b/testplan/web_ui/testing/src/Report/reportUtils.js index aa2dc967a..5c9afdf3a 100644 --- a/testplan/web_ui/testing/src/Report/reportUtils.js +++ b/testplan/web_ui/testing/src/Report/reportUtils.js @@ -223,15 +223,10 @@ const GetCenterPane = ( } if (state.currentPanelView === VIEW_TYPE.RESOURCE) { - let selectedHostUid = null; - if (selectedEntries.length >= 2) { - selectedHostUid = selectedEntries[1].host; - } return ( ); } @@ -433,6 +428,7 @@ const getSelectedUIDsFromPath = ({ uid, selection }, uidDecoder) => { return uidDecoder ? uids.map((uid) => (uid ? uidDecoder(uid) : uid)) : uids; }; + export { isReportLeaf, PropagateIndices,