Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions vee-port/validation/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -122,3 +130,6 @@ Initial revision with Changelog.

- Initial revision

---
_Markdown_
_Build: 7E4D1F7C_
60 changes: 60 additions & 0 deletions vee-port/validation/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*.
Expand Down Expand Up @@ -58,3 +114,7 @@ N/A
## Restrictions

None.

---
_Markdown_
_Build: 7E4D1F7C_
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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:
* <ul>
* <li>a word-aligned copy between the two buffers, whose throughput drops if the BSP reverts to a
* byte-wise libc copy;</li>
* <li>a one-byte misaligned copy between the two buffers, whose throughput additionally drops if the
* BSP stops overriding {@code memmove} with a word-wide implementation (a word-wide fast path is
* typically defeated by misalignment);</li>
* <li>an overlapping copy shifted one byte to the right within a single buffer (destination one byte
* above the source), which forces {@code memmove} to copy backwards to preserve the overlap; and</li>
* <li>an overlapping copy shifted one byte to the left within a single buffer (destination one byte
* below the source), which lets {@code memmove} copy forwards.</li>
* </ul>
* <p>
* 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.
* <p>
* 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.
* <p>
* 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;
}
}
Original file line number Diff line number Diff line change
@@ -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=