Make performance tests measure PEAK memory usage 86/136586/3
authordanielhanrahan <daniel.hanrahan@est.tech>
Wed, 15 Nov 2023 13:21:34 +0000 (13:21 +0000)
committerdanielhanrahan <daniel.hanrahan@est.tech>
Wed, 22 Nov 2023 15:28:47 +0000 (15:28 +0000)
Presently, performance tests measure CURRENT memory usage instead of
PEAK memory usage, leading to under-reporting if garbage collector
runs during a test. This patch fixes it, so that memory reported will
now be at least the memory of live objects at that time.

- Add tests for ResourceMeter class
- ResourceMeter measures peak memory usage instead of current

Issue-ID: CPS-1967
Signed-off-by: danielhanrahan <daniel.hanrahan@est.tech>
Change-Id: I36e9ea2196420b84877ecabc1b7331c5d3e2e252

integration-test/src/test/groovy/org/onap/cps/integration/ResourceMeterPerfTest.groovy [new file with mode: 0644]
integration-test/src/test/java/org/onap/cps/integration/ResourceMeter.java

diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/ResourceMeterPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/ResourceMeterPerfTest.groovy
new file mode 100644 (file)
index 0000000..c42bfd7
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ *  ============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
+
+import java.util.concurrent.TimeUnit
+import spock.lang.Specification
+
+class ResourceMeterPerfTest extends Specification {
+
+    final int MEGABYTE = 1_000_000
+
+    def resourceMeter = new ResourceMeter()
+
+    def 'ResourceMeter accurately measures duration'() {
+        when: 'we measure how long a known operation takes'
+            resourceMeter.start()
+            TimeUnit.SECONDS.sleep(2)
+            resourceMeter.stop()
+        then: 'ResourceMeter reports a duration within 10ms of the expected duration'
+            assert resourceMeter.getTotalTimeInSeconds() >= 2
+            assert resourceMeter.getTotalTimeInSeconds() <= 2.01
+    }
+
+    def 'ResourceMeter reports memory usage when allocating a large byte array'() {
+        when: 'the resource meter is started'
+            resourceMeter.start()
+        and: 'some memory is allocated'
+            byte[] array = new byte[50 * MEGABYTE]
+        and: 'the resource meter is stopped'
+            resourceMeter.stop()
+        then: 'the reported memory usage is close to the amount of memory allocated'
+            assert resourceMeter.getTotalMemoryUsageInMB() >= 50
+            assert resourceMeter.getTotalMemoryUsageInMB() <= 55
+    }
+
+    def 'ResourceMeter measures PEAK memory usage when garbage collector runs'() {
+        when: 'the resource meter is started'
+            resourceMeter.start()
+        and: 'some memory is allocated'
+            byte[] array = new byte[50 * MEGABYTE]
+        and: 'the memory is garbage collected'
+            array = null
+            ResourceMeter.performGcAndWait()
+        and: 'the resource meter is stopped'
+            resourceMeter.stop()
+        then: 'the reported memory usage is close to the peak amount of memory allocated'
+            assert resourceMeter.getTotalMemoryUsageInMB() >= 50
+            assert resourceMeter.getTotalMemoryUsageInMB() <= 55
+    }
+
+    def 'ResourceMeter measures memory increase only during measurement'() {
+        given: '50 megabytes is allocated before measurement'
+            byte[] arrayBefore = new byte[50 * MEGABYTE]
+        when: 'memory is allocated during measurement'
+            resourceMeter.start()
+            byte[] arrayDuring = new byte[40 * MEGABYTE]
+            resourceMeter.stop()
+        and: '50 megabytes is allocated after measurement'
+            byte[] arrayAfter = new byte[50 * MEGABYTE]
+        then: 'the reported memory usage is close to the amount allocated DURING measurement'
+            assert resourceMeter.getTotalMemoryUsageInMB() >= 40
+            assert resourceMeter.getTotalMemoryUsageInMB() <= 45
+    }
+
+}
index c7d96c4..f8a2ecb 100644 (file)
 
 package org.onap.cps.integration;
 
+import java.lang.management.GarbageCollectorMXBean;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryPoolMXBean;
+import java.lang.management.MemoryType;
 import org.springframework.util.StopWatch;
 
 /**
@@ -34,8 +38,9 @@ public class ResourceMeter {
      * Start measurement.
      */
     public void start() {
-        System.gc();
-        memoryUsedBefore = getCurrentMemoryUsage();
+        performGcAndWait();
+        resetPeakHeapUsage();
+        memoryUsedBefore = getPeakHeapUsage();
         stopWatch.start();
     }
 
@@ -44,7 +49,7 @@ public class ResourceMeter {
      */
     public void stop() {
         stopWatch.stop();
-        memoryUsedAfter = getCurrentMemoryUsage();
+        memoryUsedAfter = getPeakHeapUsage();
     }
 
     /**
@@ -63,8 +68,30 @@ public class ResourceMeter {
         return (memoryUsedAfter - memoryUsedBefore) / 1_000_000.0;
     }
 
-    private static long getCurrentMemoryUsage() {
-        return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
+    static void performGcAndWait() {
+        final long gcCountBefore = getGcCount();
+        System.gc();
+        while (getGcCount() == gcCountBefore) {}
+    }
+
+    private static long getGcCount() {
+        return ManagementFactory.getGarbageCollectorMXBeans().stream()
+                .mapToLong(GarbageCollectorMXBean::getCollectionCount)
+                .filter(gcCount -> gcCount != -1)
+                .sum();
+    }
+
+    private static long getPeakHeapUsage() {
+        return ManagementFactory.getMemoryPoolMXBeans().stream()
+                .filter(pool -> pool.getType() == MemoryType.HEAP)
+                .mapToLong(pool -> pool.getPeakUsage().getUsed())
+                .sum();
+    }
+
+    private static void resetPeakHeapUsage() {
+        ManagementFactory.getMemoryPoolMXBeans().stream()
+                .filter(pool -> pool.getType() == MemoryType.HEAP)
+                .forEach(MemoryPoolMXBean::resetPeakUsage);
     }
 }