From 800a3e32548493552e5217482d96ce19d82185b6 Mon Sep 17 00:00:00 2001 From: Alexander Todorov Date: Thu, 14 Nov 2024 22:09:53 +0200 Subject: [PATCH 1/5] Speed-up TestCase filtering on the TestPlan view page by collecting a list of DOM selectors to be queried/shown via jQuery multiple-selector at the same time instead of calling .show() for each one of them. For 1-2 rows this is on-par with the previous implementation, however it is around 2x faster when the result set contains many rows (say 100 or 1000). WARNING: when a result set contains hundreds of rows there is still a noticeable lag on the page. For example if filtering 1000 rows for is_automated=false, where only 10 rows have the value of True and the rest 990 have the value of False! See https://api.jquery.com/multiple-selector/ --- tcms/testplans/static/testplans/js/get.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tcms/testplans/static/testplans/js/get.js b/tcms/testplans/static/testplans/js/get.js index 9bee750c54..3064065d3e 100644 --- a/tcms/testplans/static/testplans/js/get.js +++ b/tcms/testplans/static/testplans/js/get.js @@ -696,6 +696,9 @@ function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { } $('.js-testcase-row').hide() + + // see https://api.jquery.com/multiple-selector/ + const showOnly = [] if (filterBy === 'component' || filterBy === 'tag') { const query = { plan: planId } query[`${filterBy}__name__icontains`] = filterValue @@ -703,11 +706,15 @@ function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { jsonRPC('TestCase.filter', query, function (filtered) { // hide again if a previous async request showed something else $('.js-testcase-row').hide() - filtered.forEach(tc => $(`[data-testcase-pk=${tc.id}]`).show()) + filtered.forEach(tc => showOnly.push(`[data-testcase-pk=${tc.id}]`)) + + $(showOnly.join(',')).show() }) } else { testCases.filter(function (tc) { return (tc[filterBy] !== undefined && tc[filterBy].toString().toLowerCase().indexOf(filterValue) > -1) - }).forEach(tc => $(`[data-testcase-pk=${tc.id}]`).show()) + }).forEach(tc => showOnly.push(`[data-testcase-pk=${tc.id}]`)) + + $(showOnly.join(',')).show() } } From 7d31a59d64c5c0e8e92fc480034a54ac1eac0fd8 Mon Sep 17 00:00:00 2001 From: Alexander Todorov Date: Thu, 14 Nov 2024 23:01:18 +0200 Subject: [PATCH 2/5] More speed-up for TestCase filtering on the TestPlan view page building upon the previous change we now either show or hide rows on screen depending on what the actual result set is. When the rows which match the filter are less than the ones which don't we display only them. Else we display everything and hide the rows which don't match the filter. This always results in the minimum number of display updates. For the extreme scenario where 990/1000 rows match is_automated=false this provides 30x improvement! WARNING: for scenarios which are not on the extreme case, yet still result in 100s of rows that need to be displayed this commit does not offer any optimization compared to the previous one! --- tcms/testplans/static/testplans/js/get.js | 80 ++++++++++++++++++++--- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/tcms/testplans/static/testplans/js/get.js b/tcms/testplans/static/testplans/js/get.js index 3064065d3e..994d18fadf 100644 --- a/tcms/testplans/static/testplans/js/get.js +++ b/tcms/testplans/static/testplans/js/get.js @@ -688,6 +688,73 @@ function getSelectedTestCases () { return tcIds } +// returns 2 arrays with selectors that need to be either shown or hidden +// return values will be used as multiple-selector for jQuery +// see https://api.jquery.com/multiple-selector/ +function findSelectorsToShowAndHide (inputData, filterBy, filterValue) { + const hideMe = [] + const showMe = [] + + inputData.forEach(function (element) { + if (element[filterBy] !== undefined && element[filterBy].toString().toLowerCase().indexOf(filterValue) > -1) { + showMe.push(`[data-testcase-pk=${element.id}]`) + } else { + hideMe.push(`[data-testcase-pk=${element.id}]`) + } + }) + + return { + hide: hideMe, + show: showMe + } +} + +// similar to the function above, however it operates on API data returned by +// calls to .filter() API methods. Needs all data as input to be able to calculate +// which rows should not be visible +function findSelectorsToShowAndHideFromAPIData (allData, filteredData) { + const hideMe = [] + const showMe = [] + + // these need to be hidden + const filteredPKs = filteredData.map(element => element.id) + allData.forEach(element => { + if (filteredPKs.indexOf(element.id) === -1) { + hideMe.push(`[data-testcase-pk=${element.id}]`) + } + }) + + // these will remain visible + filteredData.forEach(element => { + showMe.push(`[data-testcase-pk=${element.id}]`) + }) + + return { + hide: hideMe, + show: showMe + } +} + +// update the screen in one swoop trying to perform +// as little display updates as possible +function showOrHideMultipleRows (rootSelector, rows) { + // initial state is that everything is hidden + + if (rows.show.length <= rows.hide.length) { + $(rows.show.join(',')).show() + } else { + /* eslint-disable indent */ + $('body') + .find(rootSelector) + .show() + .end() + .find(rows.hide.join(',')) + .hide() + .end() + /* eslint-enable */ + } +} + function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { // no input => show all rows if (filterValue.trim().length === 0) { @@ -697,8 +764,6 @@ function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { $('.js-testcase-row').hide() - // see https://api.jquery.com/multiple-selector/ - const showOnly = [] if (filterBy === 'component' || filterBy === 'tag') { const query = { plan: planId } query[`${filterBy}__name__icontains`] = filterValue @@ -706,15 +771,12 @@ function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { jsonRPC('TestCase.filter', query, function (filtered) { // hide again if a previous async request showed something else $('.js-testcase-row').hide() - filtered.forEach(tc => showOnly.push(`[data-testcase-pk=${tc.id}]`)) - $(showOnly.join(',')).show() + const rows = findSelectorsToShowAndHideFromAPIData(testCases, filtered) + showOrHideMultipleRows('.js-testcase-row', rows) }) } else { - testCases.filter(function (tc) { - return (tc[filterBy] !== undefined && tc[filterBy].toString().toLowerCase().indexOf(filterValue) > -1) - }).forEach(tc => showOnly.push(`[data-testcase-pk=${tc.id}]`)) - - $(showOnly.join(',')).show() + const rows = findSelectorsToShowAndHide(testCases, filterBy, filterValue) + showOrHideMultipleRows('.js-testcase-row', rows) } } From aa3f44b9cfc452306191af34889aca7290f43bb3 Mon Sep 17 00:00:00 2001 From: Alexander Todorov Date: Tue, 19 Nov 2024 10:50:12 +0200 Subject: [PATCH 3/5] Move newly created functions into utils.js will be used for filtering TestExecutions in the next commit! --- tcms/static/js/utils.js | 67 +++++++++++++++++++++ tcms/testplans/static/testplans/js/get.js | 71 +---------------------- 2 files changed, 70 insertions(+), 68 deletions(-) diff --git a/tcms/static/js/utils.js b/tcms/static/js/utils.js index 99a8c572ad..b862150a23 100644 --- a/tcms/static/js/utils.js +++ b/tcms/static/js/utils.js @@ -485,3 +485,70 @@ export function updateTestPlanSelectFromProduct ( jsonRPC('TestPlan.filter', query, internalCallback) } } + +// returns 2 arrays with selectors that need to be either shown or hidden +// return values will be used as multiple-selector for jQuery +// see https://api.jquery.com/multiple-selector/ +export function findSelectorsToShowAndHide (inputData, filterBy, filterValue) { + const hideMe = [] + const showMe = [] + + inputData.forEach(function (element) { + if (element[filterBy] !== undefined && element[filterBy].toString().toLowerCase().indexOf(filterValue) > -1) { + showMe.push(`[data-testcase-pk=${element.id}]`) + } else { + hideMe.push(`[data-testcase-pk=${element.id}]`) + } + }) + + return { + hide: hideMe, + show: showMe + } +} + +// similar to the function above, however it operates on API data returned by +// calls to .filter() API methods. Needs all data as input to be able to calculate +// which rows should not be visible +export function findSelectorsToShowAndHideFromAPIData (allData, filteredData) { + const hideMe = [] + const showMe = [] + + // these need to be hidden + const filteredPKs = filteredData.map(element => element.id) + allData.forEach(element => { + if (filteredPKs.indexOf(element.id) === -1) { + hideMe.push(`[data-testcase-pk=${element.id}]`) + } + }) + + // these will remain visible + filteredData.forEach(element => { + showMe.push(`[data-testcase-pk=${element.id}]`) + }) + + return { + hide: hideMe, + show: showMe + } +} + +// update the screen in one swoop trying to perform +// as little display updates as possible +export function showOrHideMultipleRows (rootSelector, rows) { + // initial state is that everything is hidden + + if (rows.show.length <= rows.hide.length) { + $(rows.show.join(',')).show() + } else { + /* eslint-disable indent */ + $('body') + .find(rootSelector) + .show() + .end() + .find(rows.hide.join(',')) + .hide() + .end() + /* eslint-enable */ + } +} diff --git a/tcms/testplans/static/testplans/js/get.js b/tcms/testplans/static/testplans/js/get.js index 994d18fadf..6991d9e107 100644 --- a/tcms/testplans/static/testplans/js/get.js +++ b/tcms/testplans/static/testplans/js/get.js @@ -5,7 +5,9 @@ import { advancedSearchAndAddTestCases, bindDeleteCommentButton, changeDropdownSelectedItem, markdown2HTML, renderCommentsForObject, renderCommentHTML, - treeViewBind, quickSearchAndAddTestCase + treeViewBind, quickSearchAndAddTestCase, + findSelectorsToShowAndHide, findSelectorsToShowAndHideFromAPIData, + showOrHideMultipleRows } from '../../../../static/js/utils' import { initSimpleMDE } from '../../../../static/js/simplemde_security_override' @@ -688,73 +690,6 @@ function getSelectedTestCases () { return tcIds } -// returns 2 arrays with selectors that need to be either shown or hidden -// return values will be used as multiple-selector for jQuery -// see https://api.jquery.com/multiple-selector/ -function findSelectorsToShowAndHide (inputData, filterBy, filterValue) { - const hideMe = [] - const showMe = [] - - inputData.forEach(function (element) { - if (element[filterBy] !== undefined && element[filterBy].toString().toLowerCase().indexOf(filterValue) > -1) { - showMe.push(`[data-testcase-pk=${element.id}]`) - } else { - hideMe.push(`[data-testcase-pk=${element.id}]`) - } - }) - - return { - hide: hideMe, - show: showMe - } -} - -// similar to the function above, however it operates on API data returned by -// calls to .filter() API methods. Needs all data as input to be able to calculate -// which rows should not be visible -function findSelectorsToShowAndHideFromAPIData (allData, filteredData) { - const hideMe = [] - const showMe = [] - - // these need to be hidden - const filteredPKs = filteredData.map(element => element.id) - allData.forEach(element => { - if (filteredPKs.indexOf(element.id) === -1) { - hideMe.push(`[data-testcase-pk=${element.id}]`) - } - }) - - // these will remain visible - filteredData.forEach(element => { - showMe.push(`[data-testcase-pk=${element.id}]`) - }) - - return { - hide: hideMe, - show: showMe - } -} - -// update the screen in one swoop trying to perform -// as little display updates as possible -function showOrHideMultipleRows (rootSelector, rows) { - // initial state is that everything is hidden - - if (rows.show.length <= rows.hide.length) { - $(rows.show.join(',')).show() - } else { - /* eslint-disable indent */ - $('body') - .find(rootSelector) - .show() - .end() - .find(rows.hide.join(',')) - .hide() - .end() - /* eslint-enable */ - } -} - function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { // no input => show all rows if (filterValue.trim().length === 0) { From f96ba491dcef624102db3c070361564d10298836 Mon Sep 17 00:00:00 2001 From: Alexander Todorov Date: Tue, 19 Nov 2024 11:17:05 +0200 Subject: [PATCH 4/5] Speed-up TestExecution filtering on TestRun view page NOTE: use a different underlying API --- tcms/static/js/utils.js | 19 ++++++++++++------ tcms/testplans/static/testplans/js/get.js | 4 ++-- tcms/testruns/static/testruns/js/get.js | 24 +++++++++++++---------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/tcms/static/js/utils.js b/tcms/static/js/utils.js index b862150a23..03b566e749 100644 --- a/tcms/static/js/utils.js +++ b/tcms/static/js/utils.js @@ -486,18 +486,25 @@ export function updateTestPlanSelectFromProduct ( } } +// usage: 'Hello {0}, your order {1} has been shipped.'.format('John', 10001) +function formatString (template, ...args) { + return template.replace(/{([0-9]+)}/g, function (match, index) { + return typeof args[index] === 'undefined' ? match : args[index] + }) +} + // returns 2 arrays with selectors that need to be either shown or hidden // return values will be used as multiple-selector for jQuery // see https://api.jquery.com/multiple-selector/ -export function findSelectorsToShowAndHide (inputData, filterBy, filterValue) { +export function findSelectorsToShowAndHide (inputData, filterBy, filterValue, selectorStr) { const hideMe = [] const showMe = [] inputData.forEach(function (element) { if (element[filterBy] !== undefined && element[filterBy].toString().toLowerCase().indexOf(filterValue) > -1) { - showMe.push(`[data-testcase-pk=${element.id}]`) + showMe.push(formatString(selectorStr, element.id)) } else { - hideMe.push(`[data-testcase-pk=${element.id}]`) + hideMe.push(formatString(selectorStr, element.id)) } }) @@ -510,7 +517,7 @@ export function findSelectorsToShowAndHide (inputData, filterBy, filterValue) { // similar to the function above, however it operates on API data returned by // calls to .filter() API methods. Needs all data as input to be able to calculate // which rows should not be visible -export function findSelectorsToShowAndHideFromAPIData (allData, filteredData) { +export function findSelectorsToShowAndHideFromAPIData (allData, filteredData, selectorStr) { const hideMe = [] const showMe = [] @@ -518,13 +525,13 @@ export function findSelectorsToShowAndHideFromAPIData (allData, filteredData) { const filteredPKs = filteredData.map(element => element.id) allData.forEach(element => { if (filteredPKs.indexOf(element.id) === -1) { - hideMe.push(`[data-testcase-pk=${element.id}]`) + hideMe.push(formatString(selectorStr, element.id)) } }) // these will remain visible filteredData.forEach(element => { - showMe.push(`[data-testcase-pk=${element.id}]`) + showMe.push(formatString(selectorStr, element.id)) }) return { diff --git a/tcms/testplans/static/testplans/js/get.js b/tcms/testplans/static/testplans/js/get.js index 6991d9e107..c9312b692b 100644 --- a/tcms/testplans/static/testplans/js/get.js +++ b/tcms/testplans/static/testplans/js/get.js @@ -707,11 +707,11 @@ function filterTestCasesByProperty (planId, testCases, filterBy, filterValue) { // hide again if a previous async request showed something else $('.js-testcase-row').hide() - const rows = findSelectorsToShowAndHideFromAPIData(testCases, filtered) + const rows = findSelectorsToShowAndHideFromAPIData(testCases, filtered, '[data-testcase-pk={0}]') showOrHideMultipleRows('.js-testcase-row', rows) }) } else { - const rows = findSelectorsToShowAndHide(testCases, filterBy, filterValue) + const rows = findSelectorsToShowAndHide(testCases, filterBy, filterValue, '[data-testcase-pk={0}]') showOrHideMultipleRows('.js-testcase-row', rows) } } diff --git a/tcms/testruns/static/testruns/js/get.js b/tcms/testruns/static/testruns/js/get.js index 3f9d782362..87820b5829 100644 --- a/tcms/testruns/static/testruns/js/get.js +++ b/tcms/testruns/static/testruns/js/get.js @@ -7,7 +7,9 @@ import { arrayToDict, bindDeleteCommentButton, changeDropdownSelectedItem, currentTimeWithTimezone, markdown2HTML, renderCommentsForObject, renderCommentHTML, - quickSearchAndAddTestCase, treeViewBind + quickSearchAndAddTestCase, treeViewBind, + findSelectorsToShowAndHide, findSelectorsToShowAndHideFromAPIData, + showOrHideMultipleRows } from '../../../../static/js/utils' import { initSimpleMDE } from '../../../../static/js/simplemde_security_override' @@ -259,24 +261,26 @@ function filterTestExecutionsByProperty (runId, executions, filterBy, filterValu $('.test-execution-element').hide() if (filterBy === 'is_automated' || filterBy === 'priority' || filterBy === 'category') { - const query = { executions__run: runId } + const query = { run: runId } if (filterBy === 'is_automated') { - query[filterBy] = filterValue + query.case__is_automated = filterValue } else if (filterBy === 'priority') { - query.priority__value__icontains = filterValue + query.case__priority__value__icontains = filterValue } else if (filterBy === 'category') { - query.category__name__icontains = filterValue + query.case__category__name__icontains = filterValue } - jsonRPC('TestCase.filter', query, function (filtered) { + // note: querying TEs so that -FromAPIData() can work properly! + jsonRPC('TestExecution.filter', query, function (filtered) { // hide again if a previous async request showed something else $('.test-execution-element').hide() - filtered.forEach(tc => $(`.test-execution-case-${tc.id}`).show()) + + const rows = findSelectorsToShowAndHideFromAPIData(executions, filtered, '.test-execution-{0}') + showOrHideMultipleRows('.test-execution-element', rows) }) } else { - executions.filter(function (te) { - return (te[filterBy] && te[filterBy].toString().toLowerCase().indexOf(filterValue) > -1) - }).forEach(te => $(`.test-execution-${te.id}`).show()) + const rows = findSelectorsToShowAndHide(executions, filterBy, filterValue, '.test-execution-{0}') + showOrHideMultipleRows('.test-execution-element', rows) } } From 11e774bc557500859446df50e992701264cc2249 Mon Sep 17 00:00:00 2001 From: Alexander Todorov Date: Tue, 19 Nov 2024 11:53:06 +0200 Subject: [PATCH 5/5] Refresh test execution counter when filtering on TestRun view page --- tcms/testruns/static/testruns/js/get.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tcms/testruns/static/testruns/js/get.js b/tcms/testruns/static/testruns/js/get.js index 87820b5829..528106cd12 100644 --- a/tcms/testruns/static/testruns/js/get.js +++ b/tcms/testruns/static/testruns/js/get.js @@ -250,6 +250,7 @@ function filterTestExecutionsByProperty (runId, executions, filterBy, filterValu // no input => show all rows if (filterValue.trim().length === 0) { $('.test-execution-element').show() + $('.test-executions-count').text(executions.length) return } @@ -277,10 +278,12 @@ function filterTestExecutionsByProperty (runId, executions, filterBy, filterValu const rows = findSelectorsToShowAndHideFromAPIData(executions, filtered, '.test-execution-{0}') showOrHideMultipleRows('.test-execution-element', rows) + $('.test-executions-count').text(rows.show.length) }) } else { const rows = findSelectorsToShowAndHide(executions, filterBy, filterValue, '.test-execution-{0}') showOrHideMultipleRows('.test-execution-element', rows) + $('.test-executions-count').text(rows.show.length) } }