diff --git a/vee-port/validation/core/CHANGELOG.md b/vee-port/validation/core/CHANGELOG.md index 7f35a80..c0579ce 100644 --- a/vee-port/validation/core/CHANGELOG.md +++ b/vee-port/validation/core/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.7.0] - 2026-07-22 + +### Added + +- Add ``ArrayCopyPerformance`` test measuring ``System.arraycopy`` throughput on large ``byte[]`` + buffers, with configurable minimum-throughput thresholds (aligned, misaligned, overlap-right, and + overlap-left copies) and a configurable buffer size. See the README for configuration. + ### [3.6.0] - 2026-04-08 ## Changed @@ -122,3 +130,6 @@ Initial revision with Changelog. - Initial revision +--- +_Markdown_ +_Build: 7E4D1F7C_ diff --git a/vee-port/validation/core/README.md b/vee-port/validation/core/README.md index cd45779..5a43905 100644 --- a/vee-port/validation/core/README.md +++ b/vee-port/validation/core/README.md @@ -29,6 +29,62 @@ Tests can be launched: - Follow the configuration and execution steps described in VEE Port Test Suites [documentation](../README.md). +## Array Copy Performance Benchmark + +The `ArrayCopyPerformance` test measures `System.arraycopy()` throughput on large `byte[]` buffers and, +optionally, asserts it against a minimum. On byte arrays `System.arraycopy()` resolves to a direct call +into the C library copy routine, so its throughput depends on the libc and BSP configuration; a slow +copy implementation shows up as a throughput drop. The test guards against such a regression. + +Four copies are measured and each is checked against its own threshold: + +- **aligned**: source and destination share the same word-aligned offset, in two distinct buffers; + its throughput drops if the BSP reverts to a byte-wise libc; +- **misaligned**: the destination is shifted by one byte, in two distinct buffers; it additionally + drops if the BSP stops overriding `memmove` with a word-wide implementation; +- **overlap right**: source and destination are in a single buffer with the destination one byte + above the source, which forces `memmove` to copy backwards to preserve the overlap; +- **overlap left**: source and destination are in a single buffer with the destination one byte + below the source, which lets `memmove` copy forwards. + +The two overlapping copies also verify that `System.arraycopy()` is routed to `memmove` and not to a +plain `memcpy`: a `memcpy` would corrupt the overlapping data and would not exhibit the +direction-dependent behavior the overlap figures capture. + +When the buffers are larger than the CPU data cache and live in cacheable RAM, the copy is bounded by +memory bandwidth rather than by the CPU, so the four copies reach essentially the same throughput and +the thresholds guard against a bandwidth regression rather than a CPU-side alignment penalty. On a +target without a data cache the copy is CPU-bound instead, and the aligned and misaligned figures may +diverge. + +### Configuration + +The benchmark is configured through system properties, declared in +[`validation/microej-testsuite-common.properties`](validation/microej-testsuite-common.properties) with +the `microej.java.property.` prefix (for example +`microej.java.property.com.microej.core.tests.arraycopy.min.throughput.mbps=125`). + +| Property | Default | Description | +| --- | --- | --- | +| `com.microej.core.tests.arraycopy.min.throughput.mbps` | unset (`0`) | Minimum expected **aligned** throughput, in MB/s. When unset, the aligned throughput is only logged and the check passes. | +| `com.microej.core.tests.arraycopy.min.throughput.misaligned.mbps` | unset (`0`) | Minimum expected **misaligned** throughput, in MB/s. When unset, the misaligned throughput is only logged and the check passes. | +| `com.microej.core.tests.arraycopy.min.throughput.overlap.right.mbps` | unset (`0`) | Minimum expected **overlap-right** throughput, in MB/s. When unset, the overlap-right throughput is only logged and the check passes. | +| `com.microej.core.tests.arraycopy.min.throughput.overlap.left.mbps` | unset (`0`) | Minimum expected **overlap-left** throughput, in MB/s. When unset, the overlap-left throughput is only logged and the check passes. | +| `com.microej.core.tests.arraycopy.buffer.size.bytes` | `65536` (64 KB) | Size of each of the **two** working buffers. The default already exceeds a typical MCU data cache; larger buffers do not change the measured throughput but need a proportionally larger Java heap. | + +To turn the benchmark into a regression guard on a given board: + +1. Run the test once with the thresholds unset and read the four measured throughput values from the logs. +2. Set the four `min.throughput` properties to a value slightly below the observed baseline. +3. Keep `arraycopy.buffer.size.bytes` large enough to exceed the CPU data cache so the measurement + reflects memory bandwidth. The test allocates two buffers of that size, so `core.memory.javaheap.size` + (in the same properties file) must hold both plus headroom — increase it together with the buffer size. + +The thresholds are regression floors only. To judge whether the port reaches the hardware's potential, +compare the measured throughputs against the theoretical memory bandwidth the silicon vendor advertises +for the backing memory (the RAM bandwidth figures in the datasheet or reference manual) and confirm the +port reaches the expected fraction of that peak. + ## Dependencies *All dependencies are retrieved transitively by Gradle*. @@ -58,3 +114,7 @@ N/A ## Restrictions None. + +--- +_Markdown_ +_Build: 7E4D1F7C_ diff --git a/vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java b/vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java new file mode 100644 index 0000000..df87185 --- /dev/null +++ b/vee-port/validation/core/src/test/java/com/microej/core/tests/ArrayCopyPerformance.java @@ -0,0 +1,243 @@ +/* + * Java + * + * Copyright 2026 MicroEJ Corp. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be found with this software. + * + * Build: 7E4D1F7C + */ +package com.microej.core.tests; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import ej.bon.Util; + +/** + * Measures {@link System#arraycopy(Object, int, Object, int, int)} throughput on large {@code byte[]} + * buffers and checks it against a minimum expected throughput. + *

+ * On byte arrays {@link System#arraycopy(Object, int, Object, int, int)} resolves to a direct tail + * call into the C library {@code memmove}. Its performance depends on the BSP libc configuration + * (e.g. newlib-nano versus full newlib) and on whether the BSP overrides {@code memmove} with a word-wide + * implementation. This test guards against a regression to a slow byte-wise copy. + *

+ * The buffers are far larger than a typical CPU data cache, so the copy exercises real memory + * bandwidth rather than staying cache-resident. Four copies are measured and each is asserted against + * its own minimum: + *

+ *

+ * The two overlapping copies specifically exercise the direction-picking logic that distinguishes + * {@code memmove} from a plain {@code memcpy}: a BSP that wrongly routed {@link System#arraycopy} to + * {@code memcpy} would corrupt the overlapping data, and any per-direction performance asymmetry + * surfaces here. + *

+ * These figures are only meaningful relative to the platform's theoretical peak: the measured + * throughputs must be compared against the memory bandwidth the silicon vendor advertises for the + * backing memory (e.g. the RAM bandwidth figures in the datasheet or reference manual) to judge whether + * the port actually reaches the expected fraction of peak, rather than merely clearing a fixed + * regression floor. + *

+ * The minimum expected throughputs (in MB/s) are read from the system properties + * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_MBPS} (aligned), + * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_MISALIGNED_MBPS} (misaligned), + * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_OVERLAP_RIGHT_MBPS} (overlap right) and + * {@value #PROPERTY_PREFIX}{@value #OPTION_MIN_THROUGHPUT_OVERLAP_LEFT_MBPS} (overlap left). When a + * property is not set its expectation defaults to zero, so the corresponding check is a no-op on VEE + * Ports that do not declare an expectation. + */ +public class ArrayCopyPerformance { + + private static final String PROPERTY_PREFIX = "com.microej.core.tests."; + + /** + * Option that specifies the minimum expected aligned {@link System#arraycopy} throughput, in MB/s. + * When unset, the aligned throughput is only logged and its check passes. + */ + private static final String OPTION_MIN_THROUGHPUT_MBPS = "arraycopy.min.throughput.mbps"; + + /** + * Option that specifies the minimum expected misaligned {@link System#arraycopy} throughput, in + * MB/s. When unset, the misaligned throughput is only logged and its check passes. + */ + private static final String OPTION_MIN_THROUGHPUT_MISALIGNED_MBPS = "arraycopy.min.throughput.misaligned.mbps"; + + /** + * Option that specifies the minimum expected throughput, in MB/s, of an overlapping copy shifted + * one byte to the right (destination one byte above the source) within a single buffer. When unset, + * the throughput is only logged and its check passes. + */ + private static final String OPTION_MIN_THROUGHPUT_OVERLAP_RIGHT_MBPS = "arraycopy.min.throughput.overlap.right.mbps"; + + /** + * Option that specifies the minimum expected throughput, in MB/s, of an overlapping copy shifted + * one byte to the left (destination one byte below the source) within a single buffer. When unset, + * the throughput is only logged and its check passes. + */ + private static final String OPTION_MIN_THROUGHPUT_OVERLAP_LEFT_MBPS = "arraycopy.min.throughput.overlap.left.mbps"; + + /** + * Option that specifies the size in bytes of each of the two working buffers. Larger buffers + * exceed the CPU data cache and better exercise real memory bandwidth; smaller targets can lower + * this to fit the available heap. When unset, {@value #DEFAULT_BUFFER_SIZE} bytes are used. + */ + private static final String OPTION_BUFFER_SIZE_BYTES = "arraycopy.buffer.size.bytes"; + + /** + * Fully-qualified name of {@link #OPTION_BUFFER_SIZE_BYTES}, resolved once at class load. + * {@link #testArrayCopyThroughput()} looks the option up through this precomputed name with + * {@link System#getProperty(String)}, which does not allocate on the heap when the option is unset. + * This keeps the working buffers the first heap allocations in the test, so their base address + * stays stable and well-aligned (word-wide copy throughput is sensitive to that alignment). + */ + private static final String PROPERTY_BUFFER_SIZE_BYTES = PROPERTY_PREFIX + OPTION_BUFFER_SIZE_BYTES; + + private static final int DEFAULT_MIN_THROUGHPUT_MBPS = 0; + + /** + * Default size in bytes of each of the two working buffers (64 KB). Two buffers of this size + * (128 KB total) exceed a typical MCU data cache, so the copy already exercises real memory + * bandwidth rather than staying cache-resident; larger buffers do not change the measured + * throughput but need a proportionally larger Java heap. + */ + private static final int DEFAULT_BUFFER_SIZE = 64 * 1024; + + /** Number of untimed warm-up copies (stabilizes the caches). */ + private static final int WARMUP_ITERATIONS = 2; + + /** Number of timed copies averaged into the throughput figure. */ + private static final int TIMED_ITERATIONS = 20; + + /** Destination offset, in bytes, used to force a relatively misaligned copy. */ + private static final int MISALIGN_OFFSET = 1; + + /** + * Shift, in bytes, between the source and destination regions of an overlapping copy. A one-byte + * shift keeps the two regions overlapping over all but one byte, so {@code memmove} must pick its + * copy direction from the sign of the shift. + */ + private static final int OVERLAP_OFFSET = 1; + + /** Number of nanoseconds in one second. */ + private static final long NS_PER_SECOND = 1_000_000_000L; + + /** Number of bytes in one megabyte, using the decimal (MB) convention. */ + private static final long BYTES_PER_MB = 1_000_000L; + + /** + * Measures aligned, misaligned, overlap-right and overlap-left {@link System#arraycopy} throughput + * and asserts each meets its configured minimum. + */ + @Test + public void testArrayCopyThroughput() { + // Resolve the buffer size WITHOUT allocating on the heap first, so the two working buffers stay + // the first heap allocations in this method and keep a stable, well-aligned base address (the + // aligned word-wide copy throughput is sensitive to that alignment). System.getProperty returns + // an existing reference or null (no allocation), and Integer.parseInt returns a primitive + // (no allocation on its success path); the resolved value is only logged after allocation. + int bufferSize = DEFAULT_BUFFER_SIZE; + String configuredBufferSize = System.getProperty(PROPERTY_BUFFER_SIZE_BYTES); + if (configuredBufferSize != null) { + bufferSize = Integer.parseInt(configuredBufferSize); + } + byte[] src = new byte[bufferSize]; + byte[] dst = new byte[bufferSize]; + + System.out.println("Property '" + PROPERTY_BUFFER_SIZE_BYTES + "' = " + bufferSize); + + // Fill source with a non-zero pattern so the copy cannot be optimized away. + for (int i = 0; i < bufferSize; i++) { + src[i] = (byte) i; + } + + // Copies between the two distinct buffers (no overlap). + long alignedMBps = measureThroughput(src, dst, 0, 0, bufferSize); + long misalignedMBps = measureThroughput(src, dst, 0, MISALIGN_OFFSET, bufferSize - MISALIGN_OFFSET); + + // Overlapping copies within a single buffer. Shifting the destination one byte above the source + // (right) forces memmove to copy backwards; one byte below (left) lets it copy forwards. + long overlapRightMBps = measureThroughput(src, src, 0, OVERLAP_OFFSET, bufferSize - OVERLAP_OFFSET); + long overlapLeftMBps = measureThroughput(src, src, OVERLAP_OFFSET, 0, bufferSize - OVERLAP_OFFSET); + + System.out.println("System.arraycopy throughput (aligned) : " + alignedMBps + " MB/s"); + System.out.println("System.arraycopy throughput (misaligned) : " + misalignedMBps + " MB/s"); + System.out.println("System.arraycopy throughput (overlap right) : " + overlapRightMBps + " MB/s"); + System.out.println("System.arraycopy throughput (overlap left) : " + overlapLeftMBps + " MB/s"); + + int minAlignedMBps = getOptionAsInt(OPTION_MIN_THROUGHPUT_MBPS, DEFAULT_MIN_THROUGHPUT_MBPS); + int minMisalignedMBps = getOptionAsInt(OPTION_MIN_THROUGHPUT_MISALIGNED_MBPS, DEFAULT_MIN_THROUGHPUT_MBPS); + int minOverlapRightMBps = getOptionAsInt(OPTION_MIN_THROUGHPUT_OVERLAP_RIGHT_MBPS, DEFAULT_MIN_THROUGHPUT_MBPS); + int minOverlapLeftMBps = getOptionAsInt(OPTION_MIN_THROUGHPUT_OVERLAP_LEFT_MBPS, DEFAULT_MIN_THROUGHPUT_MBPS); + + assertTrue("Aligned System.arraycopy throughput (" + alignedMBps + " MB/s) is below the required minimum (" + + minAlignedMBps + " MB/s)", alignedMBps >= minAlignedMBps); + assertTrue("Misaligned System.arraycopy throughput (" + misalignedMBps + " MB/s) is below the required minimum (" + + minMisalignedMBps + " MB/s)", misalignedMBps >= minMisalignedMBps); + assertTrue("Overlap-right System.arraycopy throughput (" + overlapRightMBps + + " MB/s) is below the required minimum (" + minOverlapRightMBps + " MB/s)", + overlapRightMBps >= minOverlapRightMBps); + assertTrue("Overlap-left System.arraycopy throughput (" + overlapLeftMBps + + " MB/s) is below the required minimum (" + minOverlapLeftMBps + " MB/s)", + overlapLeftMBps >= minOverlapLeftMBps); + } + + /** + * Times repeated copies of the given size and returns the measured throughput. + * + * @param src + * the source buffer. + * @param dst + * the destination buffer. + * @param srcOffset + * the offset of the first copied byte in the source buffer. + * @param dstOffset + * the offset of the first written byte in the destination buffer. + * @param size + * the number of bytes copied per iteration. + * @return the measured throughput, in MB/s (decimal megabytes per second). + */ + private static long measureThroughput(byte[] src, byte[] dst, int srcOffset, int dstOffset, int size) { + for (int i = 0; i < WARMUP_ITERATIONS; i++) { + System.arraycopy(src, srcOffset, dst, dstOffset, size); + } + + long startNs = Util.platformTimeNanos(); + for (int i = 0; i < TIMED_ITERATIONS; i++) { + System.arraycopy(src, srcOffset, dst, dstOffset, size); + } + long elapsedNs = Util.platformTimeNanos() - startNs; + + if (elapsedNs <= 0) { + return 0; + } + long totalBytes = (long) size * TIMED_ITERATIONS; + return (totalBytes * NS_PER_SECOND / elapsedNs) / BYTES_PER_MB; + } + + /** + * Reads an integer option from the system properties, logging the resolved value. + * + * @param optionName + * the option name, appended to {@value #PROPERTY_PREFIX}. + * @param defaultValue + * the value returned when the property is not set or not a valid integer. + * @return the resolved option value. + */ + private static int getOptionAsInt(String optionName, int defaultValue) { + String propertyName = PROPERTY_PREFIX + optionName; + int value = Integer.getInteger(propertyName, defaultValue).intValue(); + System.out.println("Property '" + propertyName + "' = " + value); + return value; + } +} diff --git a/vee-port/validation/core/validation/microej-testsuite-common.properties b/vee-port/validation/core/validation/microej-testsuite-common.properties index 1986c93..bfabd68 100644 --- a/vee-port/validation/core/validation/microej-testsuite-common.properties +++ b/vee-port/validation/core/validation/microej-testsuite-common.properties @@ -1,13 +1,37 @@ # Testsuite Application Options +# +# Build: 7E4D1F7C # Java memory settings core.memory.immortal.memory=RAM core.memory.immortal.size=4096 core.memory.javaheap.memory=RAM -core.memory.javaheap.size=32768 +# The ArrayCopyPerformance benchmark allocates two 64 KB byte buffers (128 KB) in the Java heap; +# 160 KB covers those plus test-framework overhead. This is the largest heap any core validation +# test needs (see the arraycopy section in the README). +core.memory.javaheap.size=163840 core.memory.thread.block.size=512 core.memory.thread.max.size=4 core.memory.threads.memory=RAM core.memory.threads.pool.memory=RAM core.memory.threads.pool.size=15 core.memory.threads.size=10 + +# ArrayCopyPerformance benchmark (System.arraycopy throughput). See the README for what these +# options do and how to configure a throughput threshold. Four figures are measured: aligned and +# misaligned copies between the two buffers, plus two overlapping copies within a single buffer +# (destination shifted one byte right, then one byte left). Thresholds are unset (the checks are +# no-ops); set all four to enforce a regression threshold once the board's baseline is known. +# +# The thresholds are regression floors only: they say nothing about whether the port reaches the +# hardware's potential. The measured throughputs must also be compared against the theoretical memory +# bandwidth the silicon vendor advertises for the backing memory (the RAM bandwidth figures in the +# datasheet or reference manual) to confirm the port reaches the expected fraction of peak. +# +# Buffer size is left to the test default (64 KB, see ArrayCopyPerformance.DEFAULT_BUFFER_SIZE); +# uncomment to override. +#microej.java.property.com.microej.core.tests.arraycopy.buffer.size.bytes=65536 +#microej.java.property.com.microej.core.tests.arraycopy.min.throughput.mbps= +#microej.java.property.com.microej.core.tests.arraycopy.min.throughput.misaligned.mbps= +#microej.java.property.com.microej.core.tests.arraycopy.min.throughput.overlap.right.mbps= +#microej.java.property.com.microej.core.tests.arraycopy.min.throughput.overlap.left.mbps=