CM Data Subscriptions PoC/Performance test 22/136022/1
authorToineSiebelink <toine.siebelink@est.tech>
Thu, 21 Sep 2023 13:04:29 +0000 (14:04 +0100)
committerToineSiebelink <toine.siebelink@est.tech>
Mon, 25 Sep 2023 15:17:43 +0000 (16:17 +0100)
- New model introduced in test can be ported to production code
- Groovy test around the new use-cases can be used as a guid for actau production code solution
- Current worst use-case about 10 secodn son windows laptop. is acceptable as per Requiremenyt of 30 second
- TODO: update test limits/expectations based on CI results

Issue-ID: CPS-1881
Signed-off-by: ToineSiebelink <toine.siebelink@est.tech>
Change-Id: I05f3adf7f9cc4d9a9c94a8435a392ed76f9fad66

integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/base/NcmpPerfTestBase.groovy [new file with mode: 0644]
integration-test/src/test/groovy/org/onap/cps/integration/performance/base/NcmpRegistryPerfTestBase.groovy [deleted file]
integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmDataSubscriptionsPerfTest.groovy [new file with mode: 0644]
integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryPerfTest.groovy
integration-test/src/test/resources/data/cm-data-subscriptions/cm-data-subscriptions@2023-09-21.yang [new file with mode: 0644]

index 03ef9c2..40fe030 100644 (file)
@@ -79,6 +79,7 @@ class CpsIntegrationSpecBase extends Specification {
     def static BOOKSTORE_SCHEMA_SET = 'bookstoreSchemaSet'
 
     def static initialized = false
+    def now = OffsetDateTime.now()
 
     def setup() {
         if (!initialized) {
@@ -120,4 +121,31 @@ class CpsIntegrationSpecBase extends Specification {
             cpsDataService.saveData(dataspaceName, anchorNamePrefix + it, data.replace("Easons", "Easons-"+it.toString()), OffsetDateTime.now())
         }
     }
+
+    def createJsonArray(name, numberOfElements, keyName, keyValuePrefix, dataPerKey) {
+        def json = '{"' + name + '":['
+        (1..numberOfElements).each {
+            json += '{"' + keyName + '":"' + keyValuePrefix + '-' + it + '"'
+            if (!dataPerKey.isEmpty()) {
+                json += ',' + dataPerKey
+            }
+            json += '}'
+            if (it < numberOfElements) {
+                json += ','
+            }
+        }
+        json += ']}'
+    }
+
+    def createLeafList(name, numberOfElements, valuePrefix) {
+        def json = '"' + name + '":['
+        (1..numberOfElements).each {
+            json += '"' + valuePrefix + '-' + it + '"'
+            if (it < numberOfElements) {
+                json += ','
+            }
+        }
+        json += ']'
+    }
+
 }
index 2fe2753..12c97ed 100644 (file)
@@ -44,7 +44,6 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
     CpsDataService objectUnderTest
     def originalCountBookstoreChildNodes
     def originalCountBookstoreTopLevelListNodes
-    def now = OffsetDateTime.now()
 
     def setup() {
         objectUnderTest = cpsDataService
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/base/NcmpPerfTestBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/base/NcmpPerfTestBase.groovy
new file mode 100644 (file)
index 0000000..f5d7c5e
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the 'License');
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an 'AS IS' BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.integration.performance.base
+
+import java.time.OffsetDateTime
+
+class NcmpPerfTestBase extends PerfTestBase {
+
+    def static NCMP_PERFORMANCE_TEST_DATASPACE = 'ncmpPerformance'
+    def static REGISTRY_ANCHOR = 'ncmp-registry'
+    def static REGISTRY_SCHEMA_SET = 'registrySchemaSet'
+    def static CM_DATA_SUBSCRIPTIONS_ANCHOR = 'cm-data-subscriptions'
+    def static CM_DATA_SUBSCRIPTIONS_SCHEMA_SET = 'cmDataSubscriptionsSchemaSet'
+
+    def datastore1cmHandlePlaceHolder = '{"datastores":{"datastore":[{"name":"ds-1","cm-handles":{"cm-handle":[]}}]}}'
+    def xPathForDataStore1CmHandles = '/datastores/datastore[@name="ds-1"]/cm-handles'
+    def numberOfCmDataSubscribers = 200
+    def numberOfFiltersPerCmHandle = 10
+    def numberOfCmHandlesPerCmDataSubscription = 200
+
+// SHORT versions for easier debugging
+//    def subscriberIdPrefix = 'sub'
+//    def xpathPrefix = 'f'
+//    def cmHandlePrefix = 'ch'
+
+
+// LONG versions for performance testing
+    def subscriberIdPrefix = 'some really long subscriber id to see if this makes any difference to the performance'
+    def xpathPrefix = 'some really long xpath/with/loads/of/children/grandchildren/and/whatever/else/I/can/think/of to see if this makes any difference to the performance'
+    def cmHandlePrefix = 'some really long cm handle id to see if this makes any difference to the performance'
+
+    def printTitle() {
+        println('##      N C M P   P E R F O R M A N C E   T E S T   R E S U L T S          ##')
+    }
+
+    def isInitialised() {
+        return dataspaceExists(NCMP_PERFORMANCE_TEST_DATASPACE)
+    }
+
+    def setupPerformanceInfraStructure() {
+        cpsAdminService.createDataspace(NCMP_PERFORMANCE_TEST_DATASPACE)
+        createRegistrySchemaSet()
+        createCmDataSubscriptionsSchemaSet()
+        addCmSubscriptionData()
+    }
+
+    def createInitialData() {
+        cpsAdminService.createAnchor(NCMP_PERFORMANCE_TEST_DATASPACE, REGISTRY_SCHEMA_SET, REGISTRY_ANCHOR)
+        def data = readResourceDataFile('ncmp-registry/1000-cmhandles.json')
+        cpsDataService.saveData(NCMP_PERFORMANCE_TEST_DATASPACE, REGISTRY_ANCHOR, data, OffsetDateTime.now())
+    }
+
+    def createRegistrySchemaSet() {
+        def modelAsString = readResourceDataFile('ncmp-registry/dmi-registry@2022-05-10.yang')
+        cpsModuleService.createSchemaSet(NCMP_PERFORMANCE_TEST_DATASPACE, REGISTRY_SCHEMA_SET, [registry: modelAsString])
+    }
+
+    def createCmDataSubscriptionsSchemaSet() {
+        def modelAsString = readResourceDataFile('cm-data-subscriptions/cm-data-subscriptions@2023-09-21.yang')
+        cpsModuleService.createSchemaSet(NCMP_PERFORMANCE_TEST_DATASPACE, CM_DATA_SUBSCRIPTIONS_SCHEMA_SET, [registry: modelAsString])
+    }
+
+    def addCmSubscriptionData() {
+        cpsAdminService.createAnchor(NCMP_PERFORMANCE_TEST_DATASPACE, CM_DATA_SUBSCRIPTIONS_SCHEMA_SET, CM_DATA_SUBSCRIPTIONS_ANCHOR)
+        cpsDataService.saveData(NCMP_PERFORMANCE_TEST_DATASPACE, CM_DATA_SUBSCRIPTIONS_ANCHOR, datastore1cmHandlePlaceHolder, now)
+        def subscribers = createLeafList('subscribers',numberOfCmDataSubscribers, subscriberIdPrefix)
+        def filters = '"filters":' + createJsonArray('filter',numberOfFiltersPerCmHandle,'xpath',xpathPrefix,subscribers)
+        def cmHandles = createJsonArray('cm-handle',numberOfCmHandlesPerCmDataSubscription,'id',cmHandlePrefix, filters)
+        cpsDataService.saveData(NCMP_PERFORMANCE_TEST_DATASPACE, CM_DATA_SUBSCRIPTIONS_ANCHOR, xPathForDataStore1CmHandles, cmHandles, now)
+    }
+}
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/base/NcmpRegistryPerfTestBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/base/NcmpRegistryPerfTestBase.groovy
deleted file mode 100644 (file)
index d169bd7..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023 Nordix Foundation
- *  ================================================================================
- *  Licensed under the Apache License, Version 2.0 (the 'License');
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an 'AS IS' BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- *  SPDX-License-Identifier: Apache-2.0
- *  ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.integration.performance.base
-
-import java.time.OffsetDateTime
-
-import org.onap.cps.integration.base.CpsIntegrationSpecBase
-
-class NcmpRegistryPerfTestBase extends PerfTestBase {
-
-    def static REGISTRY_ANCHOR = 'ncmp-registry'
-    def static REGISTRY_SCHEMA_SET = 'registrySchemaSet'
-    def static NCMP_PERFORMANCE_TEST_DATASPACE = 'ncmpPerformacne'
-
-    def printTitle() {
-        println('##      N C M P   P E R F O R M A N C E   T E S T   R E S U L T S          ##')
-    }
-
-    def isInitialised() {
-        return dataspaceExists(NCMP_PERFORMANCE_TEST_DATASPACE)
-    }
-
-    def setupPerformanceInfraStructure() {
-        cpsAdminService.createDataspace(NCMP_PERFORMANCE_TEST_DATASPACE)
-        def modelAsString = readResourceDataFile('ncmp-registry/dmi-registry@2022-05-10.yang')
-        cpsModuleService.createSchemaSet(NCMP_PERFORMANCE_TEST_DATASPACE, REGISTRY_SCHEMA_SET, [registry: modelAsString])
-    }
-
-    def createInitialData() {
-        def data = readResourceDataFile('ncmp-registry/1000-cmhandles.json')
-        cpsAdminService.createAnchor(NCMP_PERFORMANCE_TEST_DATASPACE, REGISTRY_SCHEMA_SET, REGISTRY_ANCHOR)
-        cpsDataService.saveData(NCMP_PERFORMANCE_TEST_DATASPACE, REGISTRY_ANCHOR, data, OffsetDateTime.now())
-    }
-
-
-}
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmDataSubscriptionsPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmDataSubscriptionsPerfTest.groovy
new file mode 100644 (file)
index 0000000..00e2d07
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the 'License');
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an 'AS IS' BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.integration.performance.ncmp
+
+import org.onap.cps.api.CpsQueryService
+import org.onap.cps.integration.performance.base.NcmpPerfTestBase
+import org.onap.cps.spi.model.DataNode
+
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+
+class CmDataSubscribersPerfTest extends NcmpPerfTestBase {
+
+    def datastore1cmHandlePlaceHolder = '{"datastores":{"datastore":[{"name":"ds-1","cm-handles":{"cm-handle":[]}}]}}'
+    def xPathForDataStore1CmHandles = '/datastores/datastore[@name="ds-1"]/cm-handles'
+
+    CpsQueryService objectUnderTest
+
+    def setup() { objectUnderTest = cpsQueryService }
+
+    def totalNumberOfEntries = numberOfFiltersPerCmHandle * numberOfCmHandlesPerCmDataSubscription
+
+    def random = new Random()
+
+    def 'Find many subscribers in large dataset.'() {
+        when: 'all filters are queried'
+            stopWatch.start()
+            def cpsPath = '//filter'
+            def result = objectUnderTest.queryDataNodes(NCMP_PERFORMANCE_TEST_DATASPACE, CM_DATA_SUBSCRIPTIONS_ANCHOR, cpsPath, INCLUDE_ALL_DESCENDANTS)
+        then: 'got all filter entries'
+            result.size() == totalNumberOfEntries
+        then: 'find a random subscriptions by iteration (worst case: whole subscription matches previous entries)'
+            def matches = querySubscriptionsByIteration(result, -1)
+            stopWatch.stop()
+            matches.size() == numberOfFiltersPerCmHandle * numberOfCmHandlesPerCmDataSubscription
+        and: 'query all subscribers within 1 second'
+            def durationInMillis = stopWatch.getTotalTimeMillis()
+            recordAndAssertPerformance("Query all subscribers", 1_000, durationInMillis)
+    }
+
+    def 'Worst case new subscription (200x10 new entries).'() {
+        given: 'a new subscription with non-matching data'
+            def subscribers = createLeafList('subscribers',1, subscriberIdPrefix)
+            def filters = '"filters":' + createJsonArray('filter',numberOfFiltersPerCmHandle,'xpath','other_' + xpathPrefix,subscribers)
+            def cmHandles = createJsonArray('cm-handle',numberOfCmHandlesPerCmDataSubscription,'id','other' + cmHandlePrefix, filters)
+        when: 'Insert a new subscription'
+            stopWatch.start()
+            cpsDataService.saveData(NCMP_PERFORMANCE_TEST_DATASPACE, CM_DATA_SUBSCRIPTIONS_ANCHOR, xPathForDataStore1CmHandles, cmHandles, now)
+            stopWatch.stop()
+            def durationInMillis = stopWatch.getTotalTimeMillis()
+        then: 'insert new subscription with 1 second'
+            recordAndAssertPerformance("Insert new subscription", 1_000, durationInMillis)
+    }
+
+    def 'Worst case subscription update (200x10 matching entries).'() {
+        given: 'all filters are queried'
+            def cpsPath = '//filter'
+            def result = objectUnderTest.queryDataNodes(NCMP_PERFORMANCE_TEST_DATASPACE, CM_DATA_SUBSCRIPTIONS_ANCHOR, cpsPath, INCLUDE_ALL_DESCENDANTS)
+        and: 'find all entries for an existing subscriptions'
+            def matches = querySubscriptionsByIteration(result, 1)
+        when: 'Update all subscriptions found'
+            stopWatch.start()
+            /* the production code version of this should manipulate the original subscribersAsArray of course
+               but for the (performance) poc creating another array with one extra element suffices
+             */
+            def jsonPerPath = [:]
+            matches.each { xpath, subscribersAsArray ->
+                def updatedSubscribers = createLeafList('subscribers', 1 + numberOfCmDataSubscribers, subscriberIdPrefix)
+                def filterEntry = '{"filter": {"xpath":"' + xpath + '", ' + updatedSubscribers + ' } }'
+                def parentPath = xpath.toString().substring(0, xpath.toString().indexOf('/filter[@xpath='))
+                jsonPerPath.put(parentPath, filterEntry)
+            }
+            cpsDataService.updateDataNodesAndDescendants(NCMP_PERFORMANCE_TEST_DATASPACE, CM_DATA_SUBSCRIPTIONS_ANCHOR, jsonPerPath, now)
+            stopWatch.stop()
+            def durationInMillis = stopWatch.getTotalTimeMillis()
+        then: 'Update matching subscription within 8 seconds'
+            //TODO Toine check with Daniel if this can be optimized quickly without really changing production code
+            // ie is there a better way of doing these 2,000 updates
+            recordAndAssertPerformance("Update matching subscription", 8_000, durationInMillis)
+    }
+
+    def querySubscriptionsByIteration(Collection<DataNode> allSubscriptionsAsDataNodes, targetSubscriptionSequenceNumber) {
+        def matches = [:]
+        allSubscriptionsAsDataNodes.each {
+            String[] subscribersAsArray = it.leaves.get('subscribers')
+            Set<String> subscribersAsSet = new HashSet<>(Arrays.asList(subscribersAsArray))
+            def targetSubscriptionId = subscriberIdPrefix + '-' + ( targetSubscriptionSequenceNumber > 0 ? targetSubscriptionSequenceNumber
+                                                                                                     : 1 + random.nextInt(numberOfCmDataSubscribers) )
+            if (subscribersAsSet.contains(targetSubscriptionId)) {
+                matches.put(it.xpath, subscribersAsArray)
+            }
+        }
+        return matches
+    }
+
+}
index d01216e..54e56d8 100644 (file)
@@ -22,11 +22,11 @@ package org.onap.cps.integration.performance.ncmp
 
 import java.util.stream.Collectors
 import org.onap.cps.api.CpsQueryService
-import org.onap.cps.integration.performance.base.NcmpRegistryPerfTestBase
+import org.onap.cps.integration.performance.base.NcmpPerfTestBase
 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
 
-class CmHandleQueryPerfTest extends NcmpRegistryPerfTestBase {
+class CmHandleQueryPerfTest extends NcmpPerfTestBase {
 
     CpsQueryService objectUnderTest
 
diff --git a/integration-test/src/test/resources/data/cm-data-subscriptions/cm-data-subscriptions@2023-09-21.yang b/integration-test/src/test/resources/data/cm-data-subscriptions/cm-data-subscriptions@2023-09-21.yang
new file mode 100644 (file)
index 0000000..552f137
--- /dev/null
@@ -0,0 +1,49 @@
+module cm-data-subscriptions {
+    yang-version 1.1;
+    namespace "org:onap:cps:ncmp";
+
+    prefix cmds;
+
+    revision "2023-09-21" {
+        description
+        "First release, Proof of Concept & Performance";
+    }
+
+    container datastores {
+
+        list datastore {
+            key "name";
+
+            leaf name {
+                type string;
+            }
+
+            container cm-handles {
+
+                list cm-handle {
+                    key "id";
+
+                    leaf id {
+                        type string;
+                    }
+
+                    container filters {
+
+                        list filter {
+                            key "xpath";
+
+                            leaf xpath {
+                                type string;
+                            }
+
+                            leaf-list subscribers {
+                                type string;
+                            }
+
+                        }
+                    }
+                }
+            }
+        }
+    }
+}