From 52e662bdc735eef400cb52262ecd59e13d7a1b97 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 30 Oct 2026 06:21:48 +0200 Subject: [PATCH 01/12] Real-time threads: periodic SCHED_FIFO loops the collector never stops com.codename1.realtime runs a RealtimeTask on a thread of its own, released on an absolute clock, on PREEMPT_RT (and any) Linux and on the backend: - SCHED_FIFO priority, CPU pinning and isolation, mlockall(MCL_ONFAULT), priority-inheriting VM mutexes; statistics with a latency histogram. - The collector releases a real-time thread as soon as its stack is scanned (SATB keeps it sound) and never enters stop-the-world phases while one runs. - No allocation in a cycle: RealtimeAllocationVerifier fails the build on allocation reachable from cycle()/periodsMissed(); at run time the thread's page cache is swapped out, so an allocation the build could not see throws RealtimeAllocationError naming the class. - setMemoryLimit: ordinary threads get OutOfMemoryError instead of a struggling collector once resident memory is over the limit. - The simulator and other ports run the loop best-effort. vm/backend/demo/rtcheck is the check, run on the arm64 runner by embedded-linux.yml; developer guide chapter Realtime.asciidoc. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/embedded-linux.yml | 20 + .github/workflows/parparvm-tests.yml | 2 + .../impl/CodenameOneImplementation.java | 7 + .../codename1/realtime/AllocationAllowed.java | 37 ++ .../src/com/codename1/realtime/Realtime.java | 289 ++++++++++++++ .../realtime/RealtimeAllocationError.java | 39 ++ .../realtime/RealtimeParameters.java | 197 +++++++++ .../realtime/RealtimeStatistics.java | 130 ++++++ .../com/codename1/realtime/RealtimeTask.java | 91 +++++ .../codename1/realtime/RealtimeThread.java | 212 ++++++++++ .../com/codename1/realtime/package-info.java | 32 ++ CodenameOne/src/com/codename1/ui/Display.java | 1 + .../nativeSources/cn1_linux_realtime.c | 77 ++++ .../impl/linux/LinuxImplementation.java | 10 + .../com/codename1/impl/linux/LinuxNative.java | 14 + .../codename1/impl/linux/LinuxRealtime.java | 93 +++++ docs/developer-guide/Embedded-Linux.asciidoc | 6 + docs/developer-guide/Realtime.asciidoc | 166 ++++++++ docs/developer-guide/developer-guide.asciidoc | 2 + vm/ByteCodeTranslator/src/cn1_globals.h | 65 +++ vm/ByteCodeTranslator/src/cn1_globals.m | 268 ++++++++++++- vm/ByteCodeTranslator/src/cn1_realtime_impl.h | 373 ++++++++++++++++++ .../tools/translator/ByteCodeTranslator.java | 19 + .../tools/translator/BytecodeMethod.java | 12 + .../codename1/tools/translator/Parser.java | 6 + .../RealtimeAllocationVerifier.java | 263 ++++++++++++ vm/ByteCodeTranslator/src/nativeMethods.m | 43 +- vm/backend/build.sh | 2 +- vm/backend/demo/rtcheck/com/demo/RtCheck.java | 214 ++++++++++ .../com/codename1/backend/BoardRealtime.java | 37 ++ .../com/codename1/backend/BoardRealtime.java | 129 ++++++ vm/backend/native/cn1_backend_realtime.c | 76 ++++ .../src/com/codename1/backend/Backend.java | 1 + .../RealtimeAllocationVerifierTest.java | 136 +++++++ 34 files changed, 3060 insertions(+), 9 deletions(-) create mode 100644 CodenameOne/src/com/codename1/realtime/AllocationAllowed.java create mode 100644 CodenameOne/src/com/codename1/realtime/Realtime.java create mode 100644 CodenameOne/src/com/codename1/realtime/RealtimeAllocationError.java create mode 100644 CodenameOne/src/com/codename1/realtime/RealtimeParameters.java create mode 100644 CodenameOne/src/com/codename1/realtime/RealtimeStatistics.java create mode 100644 CodenameOne/src/com/codename1/realtime/RealtimeTask.java create mode 100644 CodenameOne/src/com/codename1/realtime/RealtimeThread.java create mode 100644 CodenameOne/src/com/codename1/realtime/package-info.java create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_realtime.c create mode 100644 Ports/LinuxPort/src/com/codename1/impl/linux/LinuxRealtime.java create mode 100644 docs/developer-guide/Realtime.asciidoc create mode 100644 vm/ByteCodeTranslator/src/cn1_realtime_impl.h create mode 100644 vm/ByteCodeTranslator/src/com/codename1/tools/translator/RealtimeAllocationVerifier.java create mode 100644 vm/backend/demo/rtcheck/com/demo/RtCheck.java create mode 100644 vm/backend/impl/javase/com/codename1/backend/BoardRealtime.java create mode 100644 vm/backend/impl/parparvm/com/codename1/backend/BoardRealtime.java create mode 100644 vm/backend/native/cn1_backend_realtime.c create mode 100644 vm/tests/src/test/java/com/codename1/tools/translator/RealtimeAllocationVerifierTest.java diff --git a/.github/workflows/embedded-linux.yml b/.github/workflows/embedded-linux.yml index f53a68338d5..a85c3fbbc73 100644 --- a/.github/workflows/embedded-linux.yml +++ b/.github/workflows/embedded-linux.yml @@ -26,6 +26,7 @@ on: - 'vm/JavaAPI/src/**' - 'maven/linux/**' - 'scripts/embedded/**' + - 'vm/backend/**' - '.github/workflows/embedded-linux.yml' pull_request: paths: @@ -35,6 +36,7 @@ on: - 'vm/JavaAPI/src/**' - 'maven/linux/**' - 'scripts/embedded/**' + - 'vm/backend/**' - '.github/workflows/embedded-linux.yml' concurrency: @@ -119,6 +121,24 @@ jobs: --reference "$B/result/frame60.png" "$B/result/gpu/demo.log" "$B/result/gpu/frame60.png" \ "$GITHUB_STEP_SUMMARY" + # The real-time check (vm/backend/demo/rtcheck): a 1 kHz loop beside a thread + # that keeps the collector busy, a cycle that allocates where the build cannot + # see it, and the memory limit. Run as root so SCHED_FIFO and mlockall apply. + # The runner's kernel is not PREEMPT_RT and its cores are shared, so the + # latency is reported and bounded loosely (20ms): what this gates is that the + # collector never stops the loop, which shows up as a missed run of periods, + # not as tens of microseconds. + - name: Real-time threads under collector load + run: | + bash scripts/ci/apt-get-update.sh + sudo apt-get install -y clang libssl-dev libnghttp2-dev libcurl4-openssl-dev + export JDK_8_HOME="$JAVA_HOME" + (cd vm/backend && CN1_BACKEND_DEMO=demo/rtcheck \ + ./build.sh RtCheck com.demo "$RUNNER_TEMP/rtcheck") + sudo timeout 120 "$RUNNER_TEMP/rtcheck" 10 20000 | tee "$RUNNER_TEMP/rtcheck.log" + grep '^RTCHECK' "$RUNNER_TEMP/rtcheck.log" >> "$GITHUB_STEP_SUMMARY" + grep -q '^RTCHECK OK' "$RUNNER_TEMP/rtcheck.log" + - name: Upload the run if: always() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index a06b64145ba..20235303682 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -44,6 +44,7 @@ on: - 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java' - 'CodenameOne/src/com/codename1/impl/orm/**' - 'CodenameOne/src/com/codename1/hardware/**' + - 'CodenameOne/src/com/codename1/realtime/**' - 'CodenameOne/src/com/codename1/qrcode/QrCode.java' - 'CodenameOne/src/com/codename1/impl/MdnsEngine.java' - 'CodenameOne/src/com/codename1/orm/session/**' @@ -130,6 +131,7 @@ on: - 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java' - 'CodenameOne/src/com/codename1/impl/orm/**' - 'CodenameOne/src/com/codename1/hardware/**' + - 'CodenameOne/src/com/codename1/realtime/**' - 'CodenameOne/src/com/codename1/qrcode/QrCode.java' - 'CodenameOne/src/com/codename1/impl/MdnsEngine.java' - 'CodenameOne/src/com/codename1/orm/session/**' diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index b0534c53be2..56c0ca7ddf4 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -8703,6 +8703,13 @@ public com.codename1.hardware.Hardware getHardware() { return null; } + /// Returns the port's real-time entry point. Default implementation returns + /// `null`, which leaves [com.codename1.realtime.Realtime#getInstance()] on its + /// best-effort Java default; ports with real-time scheduling override this. + public com.codename1.realtime.Realtime getRealtime() { + return null; + } + /// Returns the port-specific local calendar source, or null when the port /// has no device calendar integration. public com.codename1.calendar.LocalCalendarSource getLocalCalendarSource() { diff --git a/CodenameOne/src/com/codename1/realtime/AllocationAllowed.java b/CodenameOne/src/com/codename1/realtime/AllocationAllowed.java new file mode 100644 index 00000000000..80aa92cc512 --- /dev/null +++ b/CodenameOne/src/com/codename1/realtime/AllocationAllowed.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.realtime; + +/// Marks a method that a [RealtimeTask]'s `cycle()` may call although it allocates. +/// +/// The build checks every method that `cycle()` and `periodsMissed(int)` reach and +/// fails on an allocation. This annotation tells the check to stop at the marked method +/// instead, for a path that allocates only in a case the application has accounted for, +/// such as a rare event that is logged. At run time such a path still counts against the +/// thread's allocation budget ([RealtimeParameters#setAllocationBudget(int)]); with the +/// default budget of zero it ends the steady state with a [RealtimeAllocationError]. +@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) +@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR}) +@com.codename1.impl.SharedWithBackend +public @interface AllocationAllowed { +} diff --git a/CodenameOne/src/com/codename1/realtime/Realtime.java b/CodenameOne/src/com/codename1/realtime/Realtime.java new file mode 100644 index 00000000000..b98b419e074 --- /dev/null +++ b/CodenameOne/src/com/codename1/realtime/Realtime.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.realtime; + +/// Entry point to real-time threads: periodic tasks that run at a fixed operating-system +/// priority, are released on an absolute clock, and are not paused by the garbage +/// collector. +/// +/// ```java +/// Realtime rt = Realtime.getInstance(); +/// rt.lockMemory(); // no page faults in a period +/// rt.setMemoryLimit(64L * 1024 * 1024); // fail fast instead of thrashing +/// RealtimeThread motor = rt.start("motor", +/// new RealtimeParameters().setPeriodNanos(1000000L).setPriority(80).setCpu(3), +/// new MotorLoop()); +/// ... +/// Log.p(motor.getStatistics().toString()); +/// ``` +/// +/// #### How the collector is kept away +/// +/// A real-time task allocates in `setup()` and never in `cycle()`. The build rejects an +/// allocation that `cycle()` reaches, and at run time an allocation it could not see +/// ends the steady state with a [RealtimeAllocationError] -- so the thread never needs +/// the collector to make room. The collector in turn never stops it for a whole cycle: +/// collections stay concurrent while a real-time thread exists, its stack is scanned +/// while it sleeps between periods, and it is released as soon as that one stack has +/// been read. Normal threads run below it, the collector's included, so a busy +/// collector slows them, not it. +/// +/// #### What this is and is not +/// +/// On Linux with a `PREEMPT_RT` kernel this is firm real time: release latency in tens +/// of microseconds, measured, not proven. It is not certified hard real time and makes +/// no claim under any safety standard. Measure the loop on the device, under load, +/// with [RealtimeThread#getStatistics()]. +/// +/// #### Platforms +/// +/// - **Linux** (the embedded and desktop Linux targets, the backend on Linux): every +/// feature. Real-time scheduling needs `CAP_SYS_NICE` or an `rtprio` limit. +/// - **Other translated targets** (iOS, macOS, Windows, the backend elsewhere): the +/// collector isolation and the allocation check, with normal-priority threads and a +/// relative sleep. +/// - **The simulator, Android, JavaScript**: a best-effort Java thread with the same +/// API, so the application runs and can be tested anywhere; there is no isolation +/// from the collector and no allocation check at run time. +/// +/// #### For port implementors +/// +/// A port subclasses this class and overrides the protected primitives, which act on +/// the calling thread. The defaults are the best-effort Java implementation. +@com.codename1.impl.SharedWithBackend +public class Realtime { + + /// Real-time scheduling (`SCHED_FIFO` on Linux). + public static final int FEATURE_SCHEDULING = 1; + /// Locking the process's memory so it is never paged out ([#lockMemory()]). + public static final int FEATURE_MEMORY_LOCK = 2; + /// Real-time threads are not paused by the garbage collector. + public static final int FEATURE_GC_ISOLATION = 4; + /// Allocation in a cycle is detected at run time. + public static final int FEATURE_ALLOCATION_CHECK = 8; + /// The kernel is a `PREEMPT_RT` kernel, which bounds its own latency. + public static final int FEATURE_PREEMPT_RT_KERNEL = 16; + /// Threads can be pinned to a CPU. + public static final int FEATURE_CPU_AFFINITY = 32; + + /// [#rawEnter(RealtimeThread, int, int, boolean)] result bit: scheduling applied. + protected static final int APPLIED_SCHEDULING = 1; + /// [#rawEnter(RealtimeThread, int, int, boolean)] result bit: CPU pinned. + protected static final int APPLIED_AFFINITY = 2; + /// [#rawEnter(RealtimeThread, int, int, boolean)] result bit: other threads moved off. + protected static final int APPLIED_ISOLATION = 4; + + private static final Realtime DEFAULT = new Realtime(); + private static Realtime installed; + + /// Ports construct subclasses; applications call [#getInstance()]. + protected Realtime() { + } + + /// Returns the real-time entry point of the current platform. Never `null`: where + /// nothing is installed this is the best-effort Java implementation. + public static Realtime getInstance() { + Realtime r = installed; + return r != null ? r : DEFAULT; + } + + /// Makes `realtime` the instance [#getInstance()] returns; `null` restores the + /// default. `Display` installs the port's instance when it starts and the backend + /// installs its own, which is why this class never names `Display`. + public static void install(Realtime realtime) { + installed = realtime; + } + + /// `true` when the platform provides the feature, one of the `FEATURE_` constants. + public boolean isSupported(int feature) { + return (getSupportedFeatures() & feature) == feature; + } + + /// Bit mask of the `FEATURE_` constants this platform provides. The default + /// provides none. + protected int getSupportedFeatures() { + return 0; + } + + /// Starts a real-time thread running `task`. + /// + /// #### Parameters + /// + /// - `name`: the thread's name, for logs and debuggers + /// + /// - `params`: its scheduling; copied, so later changes do not affect it + /// + /// - `task`: the work it runs + /// + /// #### Returns + /// + /// the running thread + public RealtimeThread start(String name, RealtimeParameters params, RealtimeTask task) { + if (name == null || params == null || task == null) { + throw new IllegalArgumentException("name, params and task are required"); + } + RealtimeThread t = new RealtimeThread(this, name, params, task); + t.begin(); + return t; + } + + /// Locks every page the process has and will have into memory, so a period never + /// waits for one to be paged in. Call it early: it also makes every later + /// allocation of memory resident at once. + /// + /// #### Returns + /// + /// `true` when the memory was locked; `false` where unsupported or refused (Linux + /// needs `CAP_IPC_LOCK` or a large enough `memlock` limit) + public boolean lockMemory() { + return rawLockMemory() == 0; + } + + /// Caps the memory of the process. After a collection that leaves the process + /// above `bytes`, a normal thread that asks for more memory gets an + /// `OutOfMemoryError` at once, instead of waiting for collections that cannot + /// free enough -- a failure the application can handle, in place of a device that + /// slows to a crawl. Real-time threads are never refused: they do not allocate. + /// + /// The limit is on the process's resident memory, because that is the memory a + /// device runs out of; it includes memory the heap holds and has not returned + /// yet. An allocation is refused when a collection leaves the process over the + /// limit, or without waiting for one once the process is a quarter over it. + /// Linux only; elsewhere this does nothing. + /// + /// #### Parameters + /// + /// - `bytes`: the limit, or 0 for none + public void setMemoryLimit(long bytes) { + rawSetMemoryLimit(bytes, new OutOfMemoryError("memory limit of " + bytes + " bytes reached")); + } + + // ---------------------------------------------------------------- primitives + + /// Makes the calling thread real-time. + /// + /// #### Returns + /// + /// the `APPLIED_` bits for what took effect, or a negative errno + protected int rawEnter(RealtimeThread thread, int priority, int cpu, boolean isolate) { + return 0; + } + + /// Returns the calling thread to normal scheduling. + protected void rawLeave(RealtimeThread thread) { + } + + /// Starts the periodic clock on the calling thread; the first release is one + /// period from now. + protected void rawStartPeriod(RealtimeThread thread, long periodNanos) { + thread.periodNanos = periodNanos; + thread.nextReleaseNanos = System.nanoTime() + periodNanos; + thread.wokeNanos = 0; + } + + /// Sleeps the calling thread to its next release. + /// + /// #### Returns + /// + /// how many releases were missed + protected int rawWaitNextPeriod(RealtimeThread thread) { + long now = System.nanoTime(); + long[] s = thread.javaStats; + if (thread.wokeNanos != 0) { + long exec = now - thread.wokeNanos; + s[RealtimeStatistics.SUM_EXEC] += exec; + if (exec > s[RealtimeStatistics.MAX_EXEC]) { + s[RealtimeStatistics.MAX_EXEC] = exec; + } + } + long left = thread.nextReleaseNanos - now; + if (left > 0) { + try { + Thread.sleep(left / 1000000L); + } catch (InterruptedException e) { + // the release is absolute; carry on from wherever we woke + } + while (System.nanoTime() < thread.nextReleaseNanos) { + Thread.yield(); + } + } + long woke = System.nanoTime(); + long latency = woke - thread.nextReleaseNanos; + if (latency < 0) { + latency = 0; + } + s[RealtimeStatistics.CYCLES]++; + s[RealtimeStatistics.SUM_LATENCY] += latency; + if (latency > s[RealtimeStatistics.MAX_LATENCY]) { + s[RealtimeStatistics.MAX_LATENCY] = latency; + } + int b = 0; + long us = latency / 1000L; + while (us > 0 && b < RealtimeStatistics.HISTOGRAM_BUCKETS - 1) { + us >>= 1; + b++; + } + s[RealtimeStatistics.HIST + b]++; + int missed = 0; + thread.nextReleaseNanos += thread.periodNanos; + while (thread.nextReleaseNanos <= woke) { + thread.nextReleaseNanos += thread.periodNanos; + missed++; + } + s[RealtimeStatistics.MISSED] += missed; + thread.wokeNanos = woke; + return missed; + } + + /// Enters the steady state on the calling thread: from here an allocation beyond + /// `budget` per period throws `error`. The default has no run-time check. + protected void rawFreeze(RealtimeThread thread, int budget, RealtimeAllocationError error) { + } + + /// Leaves the steady state on the calling thread. Safe to call when not frozen. + protected void rawThaw(RealtimeThread thread) { + } + + /// Copies the calling thread's statistics into `out` + /// (`RealtimeStatistics.COUNT` values). + protected void rawStatistics(RealtimeThread thread, long[] out) { + System.arraycopy(thread.javaStats, 0, out, 0, out.length); + } + + /// The class of the allocation that ended the calling thread's last steady state, + /// or `null`. + protected String rawLastViolation(RealtimeThread thread) { + return null; + } + + /// Locks the process's memory. Returns 0 or a negative errno; the default answers + /// -38 (ENOSYS). + protected int rawLockMemory() { + return -38; + } + + /// Installs the memory limit; `error` is the instance to throw. The default does + /// nothing. + protected void rawSetMemoryLimit(long bytes, OutOfMemoryError error) { + } +} diff --git a/CodenameOne/src/com/codename1/realtime/RealtimeAllocationError.java b/CodenameOne/src/com/codename1/realtime/RealtimeAllocationError.java new file mode 100644 index 00000000000..132da74bc04 --- /dev/null +++ b/CodenameOne/src/com/codename1/realtime/RealtimeAllocationError.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.realtime; + +/// Thrown out of a [RealtimeTask]'s `cycle()` when the cycle allocates beyond its +/// budget ([RealtimeParameters#setAllocationBudget(int)], zero unless set). +/// +/// It ends the steady state: the thread delivers it to [RealtimeTask#failed(Throwable)] +/// and stops. The message names the class whose allocation was refused. The build +/// rejects the allocations it can see, so this one came from a path the check cannot +/// follow -- a call through an interface or a method overridden in several classes. +@com.codename1.impl.SharedWithBackend +public class RealtimeAllocationError extends Error { //NOPMD DoNotExtendJavaLangError - an Error so catch (Exception) in a cycle cannot swallow it + + /// Creates an error with the given message. + public RealtimeAllocationError(String message) { + super(message); + } +} diff --git a/CodenameOne/src/com/codename1/realtime/RealtimeParameters.java b/CodenameOne/src/com/codename1/realtime/RealtimeParameters.java new file mode 100644 index 00000000000..e13df0a74d3 --- /dev/null +++ b/CodenameOne/src/com/codename1/realtime/RealtimeParameters.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.realtime; + +/// How a [RealtimeThread] is scheduled: its period, its operating-system priority, the +/// CPU it runs on and how much it may allocate per period. +/// +/// ```java +/// RealtimeParameters p = new RealtimeParameters() +/// .setPeriodNanos(1000000L) // 1 kHz +/// .setPriority(80) +/// .setCpu(3) // pinned, and other threads moved off it +/// .setSchedulingRequired(true); // fail rather than run at normal priority +/// ``` +@com.codename1.impl.SharedWithBackend +public final class RealtimeParameters { + private long periodNanos = 1000000L; + private int priority = 50; + private int cpu = -1; + private boolean isolateCpu = true; + private int allocationBudget; + private boolean schedulingRequired; + + /// The interval between releases, in nanoseconds. The default is one millisecond. + public long getPeriodNanos() { + return periodNanos; + } + + /// Sets the interval between releases. Releases are absolute -- each is one period + /// after the previous one, however long the cycle ran -- so the loop does not drift. + /// + /// #### Parameters + /// + /// - `periodNanos`: the period, at least 10 microseconds + /// + /// #### Returns + /// + /// this object, for chaining + public RealtimeParameters setPeriodNanos(long periodNanos) { + if (periodNanos < 10000L) { + throw new IllegalArgumentException("period below 10 microseconds: " + periodNanos); + } + this.periodNanos = periodNanos; + return this; + } + + /// The operating-system priority, `SCHED_FIFO` on Linux. The default is 50. + public int getPriority() { + return priority; + } + + /// Sets the operating-system priority, from 1 to 99 on Linux. A higher number + /// preempts a lower one, and any of them preempts every normal thread, the + /// collector's included. Leave the top of the range to the kernel's own + /// interrupt threads unless the loop must preempt them. + /// + /// #### Parameters + /// + /// - `priority`: the priority, 1 to 99 + /// + /// #### Returns + /// + /// this object, for chaining + public RealtimeParameters setPriority(int priority) { + if (priority < 1 || priority > 99) { + throw new IllegalArgumentException("priority out of 1..99: " + priority); + } + this.priority = priority; + return this; + } + + /// The CPU the thread is pinned to, or -1 for none (the default). + public int getCpu() { + return cpu; + } + + /// Pins the thread to one CPU. Combine it with the kernel's `isolcpus=` or a + /// cpuset that keeps other processes off that CPU for the lowest latency. + /// + /// #### Parameters + /// + /// - `cpu`: the CPU number, or -1 for none + /// + /// #### Returns + /// + /// this object, for chaining + public RealtimeParameters setCpu(int cpu) { + if (cpu < -1) { + throw new IllegalArgumentException("cpu: " + cpu); + } + this.cpu = cpu; + return this; + } + + /// Whether the application's other threads are moved off the pinned CPU. The + /// default is `true`. + public boolean isIsolateCpu() { + return isolateCpu; + } + + /// Sets whether the application's other threads -- the event dispatch thread, the + /// collector, network threads -- are moved off the CPU this thread is pinned to, + /// so they never compete with it there. Has no effect without [#setCpu(int)]. + /// + /// #### Parameters + /// + /// - `isolateCpu`: `true` to move the other threads off + /// + /// #### Returns + /// + /// this object, for chaining + public RealtimeParameters setIsolateCpu(boolean isolateCpu) { + this.isolateCpu = isolateCpu; + return this; + } + + /// How many allocations a period may make. The default is zero. + public int getAllocationBudget() { + return allocationBudget; + } + + /// Sets how many allocations each period may make before the thread fails with a + /// [RealtimeAllocationError]. Zero, the default, is the setting to aim for: an + /// allocation is a call into the collector's slow path, whose time is not bounded + /// the way the cycle's own code is. A small budget is an escape hatch for a cycle + /// that allocates on a rare path, not a way to allocate every period. + /// + /// #### Parameters + /// + /// - `allocationBudget`: allocations allowed per period, zero or more + /// + /// #### Returns + /// + /// this object, for chaining + public RealtimeParameters setAllocationBudget(int allocationBudget) { + if (allocationBudget < 0) { + throw new IllegalArgumentException("allocationBudget: " + allocationBudget); + } + this.allocationBudget = allocationBudget; + return this; + } + + /// Whether the thread fails when real-time scheduling cannot be applied. The + /// default is `false`. + public boolean isSchedulingRequired() { + return schedulingRequired; + } + + /// Sets whether the thread fails, through [RealtimeTask#failed(Throwable)], when + /// the operating system refuses real-time scheduling -- on Linux, a process + /// without `CAP_SYS_NICE` or an `rtprio` limit. When `false`, the default, the + /// task runs at normal priority instead, which is what a development machine and + /// the simulator do; [RealtimeThread#isSchedulingApplied()] says which happened. + /// + /// #### Parameters + /// + /// - `schedulingRequired`: `true` to fail without real-time scheduling + /// + /// #### Returns + /// + /// this object, for chaining + public RealtimeParameters setSchedulingRequired(boolean schedulingRequired) { + this.schedulingRequired = schedulingRequired; + return this; + } + + RealtimeParameters copy() { + RealtimeParameters p = new RealtimeParameters(); + p.periodNanos = periodNanos; + p.priority = priority; + p.cpu = cpu; + p.isolateCpu = isolateCpu; + p.allocationBudget = allocationBudget; + p.schedulingRequired = schedulingRequired; + return p; + } +} diff --git a/CodenameOne/src/com/codename1/realtime/RealtimeStatistics.java b/CodenameOne/src/com/codename1/realtime/RealtimeStatistics.java new file mode 100644 index 00000000000..938697e375f --- /dev/null +++ b/CodenameOne/src/com/codename1/realtime/RealtimeStatistics.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.realtime; + +/// A snapshot of how a [RealtimeThread]'s periods went: how late each release woke the +/// thread (latency), how long its cycles ran, and how many releases it missed. +/// +/// Latency is the number to watch. Its maximum is the loop's real worst case on this +/// device, under whatever load ran beside it; [#getLatencyHistogram()] shows how the +/// rest are spread. Taken with [RealtimeThread#getStatistics()]. +@com.codename1.impl.SharedWithBackend +public final class RealtimeStatistics { + static final int CYCLES = 0; + static final int MISSED = 1; + static final int MAX_LATENCY = 2; + static final int SUM_LATENCY = 3; + static final int MAX_EXEC = 4; + static final int SUM_EXEC = 5; + static final int VIOLATIONS = 6; + static final int HIST = 7; + /// The number of buckets in [#getLatencyHistogram()]. + public static final int HISTOGRAM_BUCKETS = 16; + static final int COUNT = HIST + HISTOGRAM_BUCKETS; + + private final long[] values; + + RealtimeStatistics(long[] values) { + this.values = values; + } + + /// The number of cycles run. + public long getCycles() { + return values[CYCLES]; + } + + /// The number of releases skipped because a cycle ran past them. + public long getMissedPeriods() { + return values[MISSED]; + } + + /// The latest any release woke the thread, in nanoseconds. + public long getMaxLatencyNanos() { + return values[MAX_LATENCY]; + } + + /// The mean wake-up latency, in nanoseconds. + public long getMeanLatencyNanos() { + return values[CYCLES] == 0 ? 0 : values[SUM_LATENCY] / values[CYCLES]; + } + + /// The longest a cycle ran, in nanoseconds: from its wake-up to its next wait. + public long getMaxExecutionNanos() { + return values[MAX_EXEC]; + } + + /// The mean time a cycle ran, in nanoseconds. + public long getMeanExecutionNanos() { + return values[CYCLES] == 0 ? 0 : values[SUM_EXEC] / values[CYCLES]; + } + + /// The number of allocations that ended a steady state on this thread. + public long getAllocationViolations() { + return values[VIOLATIONS]; + } + + /// Wake-up latencies counted by powers of two of a microsecond: index 0 is under + /// one microsecond, index 1 under two, index `i` under `2^i` microseconds, and the + /// last bucket everything from about 16 milliseconds up. + public long[] getLatencyHistogram() { + long[] h = new long[HISTOGRAM_BUCKETS]; + System.arraycopy(values, HIST, h, 0, HISTOGRAM_BUCKETS); + return h; + } + + /// The latency, in nanoseconds, under which at least `fraction` of the wake-ups + /// fell, read from the histogram -- so it is the upper edge of a power-of-two + /// bucket, an over-estimate by at most a factor of two. + /// + /// #### Parameters + /// + /// - `fraction`: between 0 and 1, for example 0.9999 + public long getLatencyPercentileNanos(double fraction) { + long total = 0; + for (int i = 0; i < HISTOGRAM_BUCKETS; i++) { + total += values[HIST + i]; + } + if (total == 0) { + return 0; + } + long need = (long) Math.ceil(total * fraction); + long seen = 0; + for (int i = 0; i < HISTOGRAM_BUCKETS; i++) { + seen += values[HIST + i]; + if (seen >= need) { + return i == HISTOGRAM_BUCKETS - 1 ? values[MAX_LATENCY] : (1000L << i); + } + } + return values[MAX_LATENCY]; + } + + /// One line, for a log. + @Override + public String toString() { + return "cycles=" + getCycles() + " missed=" + getMissedPeriods() + + " latency max=" + getMaxLatencyNanos() + "ns mean=" + getMeanLatencyNanos() + + "ns p99.99<=" + getLatencyPercentileNanos(0.9999) + + "ns exec max=" + getMaxExecutionNanos() + "ns mean=" + getMeanExecutionNanos() + + "ns violations=" + getAllocationViolations(); + } +} diff --git a/CodenameOne/src/com/codename1/realtime/RealtimeTask.java b/CodenameOne/src/com/codename1/realtime/RealtimeTask.java new file mode 100644 index 00000000000..54cdc5e378f --- /dev/null +++ b/CodenameOne/src/com/codename1/realtime/RealtimeTask.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.realtime; + +/// The work a [RealtimeThread] runs every period. +/// +/// ```java +/// class MotorLoop extends RealtimeTask { +/// private PwmChannel pwm; +/// private final byte[] reading = new byte[2]; +/// private I2cDevice encoder; +/// +/// public void setup() { +/// // runs once on the real-time thread; may allocate +/// ... +/// } +/// +/// public void cycle() { +/// // every period; must not allocate +/// encoder.read(reading, 0, 2); +/// pwm.setDutyCycle(controlLaw(reading)); +/// } +/// } +/// ``` +/// +/// `cycle()` and `periodsMissed(int)` must not allocate: no `new`, no boxing, no +/// string concatenation, no iterator from a for-each over a collection. Keep buffers +/// and objects in fields, made in the constructor or in `setup()`. The build checks +/// every method they reach through calls it can resolve and fails with the call chain; +/// at run time an allocation the build could not see ends the steady state with a +/// [RealtimeAllocationError], unless the thread has an allocation budget. +@com.codename1.impl.SharedWithBackend +public abstract class RealtimeTask { + + /// Runs once on the real-time thread, after its priority is applied and before the + /// first period. It may allocate: create here what the cycles will use, and touch + /// every class they use so its static initializer has run. + public void setup() { + } + + /// One period's work. Runs on the real-time thread at every release and must not + /// allocate. + public abstract void cycle(); + + /// Called before `cycle()` when releases were missed because the previous cycle + /// ran past them. Missed releases are skipped, not run late: the cycle that follows + /// is the one due now. Must not allocate. The default does nothing. + /// + /// #### Parameters + /// + /// - `count`: how many releases were skipped + public void periodsMissed(int count) { + } + + /// Called on the real-time thread when the task ends abnormally: `setup()` or + /// `cycle()` threw, or the thread could not be made real-time as its parameters + /// require. The steady state is over when this runs, so it may allocate. The + /// thread stops afterwards. The default prints the stack trace. + /// + /// #### Parameters + /// + /// - `failure`: what ended the task + public void failed(Throwable failure) { + failure.printStackTrace(); + } + + /// Called on the real-time thread after the last cycle, when [RealtimeThread#stop()] + /// was called or after `failed(Throwable)`. May allocate. The default does nothing. + public void stopped() { + } +} diff --git a/CodenameOne/src/com/codename1/realtime/RealtimeThread.java b/CodenameOne/src/com/codename1/realtime/RealtimeThread.java new file mode 100644 index 00000000000..e720bcf692b --- /dev/null +++ b/CodenameOne/src/com/codename1/realtime/RealtimeThread.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.realtime; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/// A thread that runs a [RealtimeTask] once per period. Created and started by +/// [Realtime#start(String, RealtimeParameters, RealtimeTask)]. +/// +/// The thread applies its scheduling, runs the task's `setup()`, freezes allocation and +/// then loops: wait for the next release, run `cycle()`. [#stop()] ends the loop after +/// the cycle in progress. +@com.codename1.impl.SharedWithBackend +public final class RealtimeThread { + /// [#getState()]: created, not yet running cycles. + public static final int STATE_STARTING = 0; + /// [#getState()]: running cycles. + public static final int STATE_RUNNING = 1; + /// [#getState()]: ended by [#stop()]. + public static final int STATE_STOPPED = 2; + /// [#getState()]: ended by a failure, see [#getFailure()]. + public static final int STATE_FAILED = 3; + + private final Realtime platform; + private final String name; + private final RealtimeParameters params; + private final RealtimeTask task; + private final AtomicBoolean running = new AtomicBoolean(true); + // Made before the freeze: the VM throws this instance at a refused allocation, so + // the throw never needs the allocation it refused. + private final RealtimeAllocationError allocationError = + new RealtimeAllocationError("allocation in a real-time cycle"); + // Written by the real-time thread after every cycle, read by getStatistics(). + private final long[] live = new long[RealtimeStatistics.COUNT]; + private Thread thread; + // Written by the real-time thread, read by any: atomics, not locks, so the + // real-time side never blocks on a reader. + private final AtomicInteger state = new AtomicInteger(STATE_STARTING); + private final AtomicInteger applied = new AtomicInteger(); + private final AtomicReference failure = new AtomicReference(); + + // The default (Java) timing in Realtime keeps its state here; a native + // implementation keeps its own, per thread, and leaves these alone. + long periodNanos; + long nextReleaseNanos; + long wokeNanos; + final long[] javaStats = new long[RealtimeStatistics.COUNT]; + + RealtimeThread(Realtime platform, String name, RealtimeParameters params, RealtimeTask task) { + this.platform = platform; + this.name = name; + this.params = params.copy(); + this.task = task; + } + + void begin() { + thread = new Thread(new Runnable() { + public void run() { //NOPMD MissingOverride - Java 5 source forbids @Override on an interface method + body(); + } + }, name); + thread.start(); + } + + private void body() { + int r = platform.rawEnter(this, params.getPriority(), params.getCpu(), params.isIsolateCpu()); + try { + if (r < 0) { + fail(new IllegalStateException("real-time scheduling rejected, errno " + (-r))); + return; + } + applied.set(r); + if (params.isSchedulingRequired() && (r & Realtime.APPLIED_SCHEDULING) == 0) { + fail(new IllegalStateException("real-time scheduling was refused by the operating system;" + + " the process needs CAP_SYS_NICE or an rtprio limit")); + return; + } + try { + task.setup(); + } catch (Throwable t) { + fail(t); + return; + } + platform.rawStartPeriod(this, params.getPeriodNanos()); + state.set(STATE_RUNNING); + loop(); + } finally { + platform.rawThaw(this); + platform.rawLeave(this); + try { + task.stopped(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + } + + // The steady state. Nothing here allocates: the flag is an AtomicBoolean read, the + // statistics are copied into an array made in the constructor, and every call is a + // primitive or the task's own. + private void loop() { + platform.rawFreeze(this, params.getAllocationBudget(), allocationError); + try { + while (running.get()) { + int missed = platform.rawWaitNextPeriod(this); + if (!running.get()) { + break; + } + if (missed > 0) { + task.periodsMissed(missed); + } + task.cycle(); + platform.rawStatistics(this, live); + } + state.set(STATE_STOPPED); + } catch (RealtimeAllocationError e) { + platform.rawThaw(this); + platform.rawStatistics(this, live); + String cls = platform.rawLastViolation(this); + fail(new RealtimeAllocationError("a real-time cycle allocated " + + (cls == null ? "an object" : cls) + + " beyond its budget of " + params.getAllocationBudget() + " per period")); + } catch (Throwable t) { + platform.rawThaw(this); + fail(t); + } + } + + private void fail(Throwable t) { + failure.set(t); + state.set(STATE_FAILED); + try { + task.failed(t); + } catch (Throwable inner) { + inner.printStackTrace(); + } + } + + /// The name given at start. + public String getName() { + return name; + } + + /// One of the `STATE_` constants. + public int getState() { + return state.get(); + } + + /// What ended the task when [#getState()] is `STATE_FAILED`, otherwise `null`. + public Throwable getFailure() { + return state.get() == STATE_FAILED ? failure.get() : null; + } + + /// `true` when the operating system granted real-time scheduling. `false` on a + /// platform without it, or a process without the privilege, where the task runs at + /// normal priority. + public boolean isSchedulingApplied() { + return (applied.get() & Realtime.APPLIED_SCHEDULING) != 0; + } + + /// `true` when the thread was pinned to the CPU its parameters named. + public boolean isCpuPinned() { + return (applied.get() & Realtime.APPLIED_AFFINITY) != 0; + } + + /// A snapshot of the thread's statistics, as of the last completed cycle. + public RealtimeStatistics getStatistics() { + long[] copy = new long[RealtimeStatistics.COUNT]; + System.arraycopy(live, 0, copy, 0, copy.length); + return new RealtimeStatistics(copy); + } + + /// Ends the loop after the cycle in progress. The thread then calls the task's + /// `stopped()` and exits. Returns at once; use [#join(long)] to wait. + public void stop() { + running.set(false); + } + + /// Waits for the thread to exit. + /// + /// #### Parameters + /// + /// - `millis`: the longest to wait, 0 for no limit + public void join(long millis) throws InterruptedException { + Thread t = thread; + if (t != null) { + t.join(millis); + } + } +} diff --git a/CodenameOne/src/com/codename1/realtime/package-info.java b/CodenameOne/src/com/codename1/realtime/package-info.java new file mode 100644 index 00000000000..db35970cfbd --- /dev/null +++ b/CodenameOne/src/com/codename1/realtime/package-info.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/// Real-time threads: periodic work released on time, at a fixed operating-system +/// priority, that the garbage collector does not stop. +/// +/// A [RealtimeTask] runs on its own [RealtimeThread]. Its `setup()` runs once and may +/// allocate; after it, every period wakes the thread and runs `cycle()`, which must +/// not allocate. That rule is what lets the thread run beside a collector: it is +/// checked when the application is built, and again while it runs. See [Realtime] for +/// the entry point, what each platform provides and what the guarantees are. +package com.codename1.realtime; diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java index c0eb20bfe94..d19b43c1cc8 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -667,6 +667,7 @@ public static void init(Object m) { } impl.postInit(); com.codename1.hardware.Hardware.install(impl.getHardware()); + com.codename1.realtime.Realtime.install(impl.getRealtime()); INSTANCE.setCommandBehavior(commandBehaviour); } else { impl.confirmControlView(); diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_realtime.c b/Ports/LinuxPort/nativeSources/cn1_linux_realtime.c new file mode 100644 index 00000000000..62af52ba1fb --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_realtime.c @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The Linux port's real-time natives (com.codename1.impl.linux.LinuxNative). Each + * forwards to the shared body in cn1_realtime_impl.h, which the headless backend uses + * too; see that header for what they do. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include "cn1_realtime_impl.h" + +JAVA_INT com_codename1_impl_linux_LinuxNative_rtFeatures___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1RtImpl_rtFeatures(threadStateData); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_rtEnter___int_int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT priority, JAVA_INT cpu, JAVA_BOOLEAN isolate) { + return cn1RtImpl_rtEnter(threadStateData, priority, cpu, isolate); +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_rtLeave__(CODENAME_ONE_THREAD_STATE) { + cn1RtImpl_rtLeave(threadStateData); +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_rtStartPeriod___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG periodNanos) { + cn1RtImpl_rtStartPeriod(threadStateData, periodNanos); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_rtWaitNextPeriod___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1RtImpl_rtWaitNextPeriod(threadStateData); +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_rtFreeze___int_java_lang_Object(CODENAME_ONE_THREAD_STATE, JAVA_INT budget, JAVA_OBJECT error) { + cn1RtImpl_rtFreeze(threadStateData, budget, error); +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_rtThaw__(CODENAME_ONE_THREAD_STATE) { + cn1RtImpl_rtThaw(threadStateData); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_rtStatistics___long_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT out) { + return cn1RtImpl_rtStatistics(threadStateData, out); +} + +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_rtLastViolation___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return cn1RtImpl_rtLastViolation(threadStateData); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_rtLockMemory___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1RtImpl_rtLockMemory(threadStateData); +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_rtSetMemoryLimit___long_java_lang_Object(CODENAME_ONE_THREAD_STATE, JAVA_LONG bytes, JAVA_OBJECT error) { + cn1RtImpl_rtSetMemoryLimit(threadStateData, bytes, error); +} diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java index 748cfa23e36..6872792c0b9 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java @@ -622,6 +622,16 @@ public com.codename1.hardware.Hardware getHardware() { return hardware; } + private LinuxRealtime realtime; + + @Override + public com.codename1.realtime.Realtime getRealtime() { + if (realtime == null) { + realtime = new LinuxRealtime(); + } + return realtime; + } + @Override public com.codename1.security.SecureStorage getSecureStorage() { if (secureStorage == null) { diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java index 012a3ec680c..b00d4ad8a70 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java @@ -1205,6 +1205,20 @@ public static native void gl3dDrawArrays(long contextPeer, long pipelinePeer, lo public static native int hwWatchdogKick(int handle); public static native void hwClose(int handle, boolean disarm); + // ---- com.codename1.realtime (cn1_linux_realtime.c). Each acts on the calling + // thread and returns a value >= 0 or -errno. + public static native int rtFeatures(); + public static native int rtEnter(int priority, int cpu, boolean isolate); + public static native void rtLeave(); + public static native void rtStartPeriod(long periodNanos); + public static native int rtWaitNextPeriod(); + public static native void rtFreeze(int budget, Object error); + public static native void rtThaw(); + public static native int rtStatistics(long[] out); + public static native String rtLastViolation(); + public static native int rtLockMemory(); + public static native void rtSetMemoryLimit(long bytes, Object error); + // ---- multicast DNS (cn1_linux_mdns.c), the socket under LinuxMdns. Each // returns a value >= 0 or -errno. public static native int mdnsOpen(); diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxRealtime.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxRealtime.java new file mode 100644 index 00000000000..2c7ca3443d9 --- /dev/null +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxRealtime.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.linux; + +import com.codename1.realtime.Realtime; +import com.codename1.realtime.RealtimeAllocationError; +import com.codename1.realtime.RealtimeThread; + +/// The Linux port's [Realtime], for the desktop and the embedded Linux targets: the +/// scheduler's half in `cn1_linux_realtime.c` over `cn1_realtime_impl.h`, the +/// collector's half in the VM. Every primitive acts on the calling thread, so the +/// `RealtimeThread` arguments are not needed here. +class LinuxRealtime extends Realtime { + private int features = -1; + + @Override + protected int getSupportedFeatures() { + if (features < 0) { + features = LinuxNative.rtFeatures(); + } + return features; + } + + @Override + protected int rawEnter(RealtimeThread thread, int priority, int cpu, boolean isolate) { + return LinuxNative.rtEnter(priority, cpu, isolate); + } + + @Override + protected void rawLeave(RealtimeThread thread) { + LinuxNative.rtLeave(); + } + + @Override + protected void rawStartPeriod(RealtimeThread thread, long periodNanos) { + LinuxNative.rtStartPeriod(periodNanos); + } + + @Override + protected int rawWaitNextPeriod(RealtimeThread thread) { + return LinuxNative.rtWaitNextPeriod(); + } + + @Override + protected void rawFreeze(RealtimeThread thread, int budget, RealtimeAllocationError error) { + LinuxNative.rtFreeze(budget, error); + } + + @Override + protected void rawThaw(RealtimeThread thread) { + LinuxNative.rtThaw(); + } + + @Override + protected void rawStatistics(RealtimeThread thread, long[] out) { + LinuxNative.rtStatistics(out); + } + + @Override + protected String rawLastViolation(RealtimeThread thread) { + return LinuxNative.rtLastViolation(); + } + + @Override + protected int rawLockMemory() { + return LinuxNative.rtLockMemory(); + } + + @Override + protected void rawSetMemoryLimit(long bytes, OutOfMemoryError error) { + LinuxNative.rtSetMemoryLimit(bytes, error); + } +} diff --git a/docs/developer-guide/Embedded-Linux.asciidoc b/docs/developer-guide/Embedded-Linux.asciidoc index 76365ffd4ea..9955ccd5a6b 100644 --- a/docs/developer-guide/Embedded-Linux.asciidoc +++ b/docs/developer-guide/Embedded-Linux.asciidoc @@ -323,3 +323,9 @@ application that never browses Bonjour or opens a Bluetooth connection carries none of that code. include::embedded-benchmarks.adoc[] + +=== Control loops with deadlines + +A loop that must run at a fixed rate beside the user interface -- motor control, +sampling, a fieldbus cycle -- belongs on a real-time thread. See +<<_real_time_threads,Real-time threads>>. diff --git a/docs/developer-guide/Realtime.asciidoc b/docs/developer-guide/Realtime.asciidoc new file mode 100644 index 00000000000..2711d50f36d --- /dev/null +++ b/docs/developer-guide/Realtime.asciidoc @@ -0,0 +1,166 @@ +== Real-time threads + +A control loop has to run at its rate -- a motor drive every millisecond, a +sensor fusion step every 2.5 -- while the user interface, the network and the +garbage collector compete for the same processor. `com.codename1.realtime` gives +such a loop its own thread. The kernel schedules that thread ahead of everything +else, and the collector never stops it. + +The target is *firm* real time on a PREEMPT_RT Linux kernel, which mainline Linux +includes since 6.12. On such a kernel the latency of a periodic thread is bounded +by the kernel and the hardware, not by the virtual machine. That is not a +certified hard real-time system: Codename One makes no claim under IEC 61508, +ISO 26262 or DO-178C, and a loop whose failure can hurt someone needs an +independent safety mechanism, such as a hardware watchdog or an interlock. + +=== A periodic task + +[source,java] +---- +class MotorLoop extends RealtimeTask { + private final double[] samples = new double[64]; // <1> + private PwmChannel pwm; + private boolean failed; + + public void setup() { // <2> + pwm = Hardware.getInstance().openPwm(0, 0); + } + + public void cycle() { // <3> + double out = 0; + for (int i = 0; i < samples.length; i++) { + out += samples[i] * GAINS[i]; + } + try { + pwm.setDutyCycle(out); + } catch (HardwareException e) { + failed = true; // no allocation on this path + } + } + + public void periodsMissed(int count) { // <4> + // the loop overran; hold the output, count it, alarm if it persists + } +} + +RealtimeThread t = Realtime.getInstance().start("motor", + new RealtimeParameters() + .setPeriodNanos(1000000) // 1 kHz + .setPriority(80) // SCHED_FIFO 1..99 + .setCpu(3), // pin to core 3 + new MotorLoop()); +---- +<1> Everything the loop needs is allocated before it starts. +<2> `setup()` runs on the real-time thread before the first period. It may allocate. +<3> `cycle()` runs once per period, released on an absolute clock, so the period + does not drift with the execution time. +<4> Called instead of skipping silently when releases were missed. + +`getStatistics()` reports the cycles, the missed periods, the release latency +(maximum, mean and a histogram with percentiles), the execution time and the +allocation violations. Check them on the target board under load: they are the +only real evidence that the loop meets its deadline. + +=== The rules a cycle follows + +*A cycle allocates nothing.* In steady state a real-time loop must not create +objects. Each allocation adds work for the collector, and an allocating thread can +be asked to wait for it. Codename One checks this twice: + +* *At build time*, the translator follows every call `cycle()` and + `periodsMissed()` can make. It fails the build on `new`, array creation, boxing + and string concatenation, and the error names the method and the line. An + allocation that is meant to happen, such as in a path that runs only on a fault, + is marked with `@AllocationAllowed`. +* *At run time*, an allocation the build could not see -- through an interface + with several implementations, say -- stops the thread with a + `RealtimeAllocationError` naming the class. The thread's `failed()` method + receives it. `setAllocationBudget(n)` allows up to `n` allocations per period for + code that cannot avoid them yet. + +*The collector does not stop a real-time thread.* An ordinary thread pauses +briefly while the collector reads its stack. A real-time thread is released +immediately, and its references stay visible to the collector through the write +barrier. While any real-time thread runs, the collector also never enters its +stop-the-world phases. + +*Keep the hand-off lock-free.* `synchronized` between the real-time thread and +the rest of the application works: the virtual machine's monitors use +priority-inheriting mutexes on Linux, so a low-priority holder is raised until it +releases. The wait is still as long as the holder's critical section. Pass data +through preallocated buffers and `java.util.concurrent.atomic` instead. + +*Avoid the slow paths of the runtime in a cycle.* Do not do these in `cycle()`: + +* Throw exceptions. Building a stack trace allocates. +* Initialize a class for the first time. Touch it in `setup()`. +* Do I/O that blocks. +* Use the hardware API calls that return new arrays. Use the variants that fill a + buffer you pass. + +=== Memory and the rest of the system + +`Realtime.lockMemory()` locks the process into RAM, so that its memory is never +paged out. Each page is locked the first time it is touched (`MCL_ONFAULT`), because +the heap is a large address reservation of which only part is used. The real-time +thread's stack is touched before its first period. Touch your own buffers in +`setup()`, so that no first touch happens in a cycle. Call `lockMemory()` once, +early. It needs `CAP_IPC_LOCK` or a `memlock` limit large enough for the process. + +`Realtime.setMemoryLimit(bytes)` caps the process's resident memory, on Linux. +When the process is over the cap, an allocation in an ordinary thread throws +`OutOfMemoryError` at once. Without the cap, the whole process slows down while the +collector struggles. Real-time threads are exempt, because they do not allocate. +The application decides what to drop and carries on. + +The measurement is resident memory, which is what a device runs out of. Two things +follow from that: + +* It counts memory the heap is holding but has not returned to the system yet, so + the cap applies to the process's footprint, not its live objects. +* An allocation is refused when a collection ends with the process still over the + cap. Without waiting for a collection, it is refused once the process is a + quarter over. + +Set the cap with headroom over what the application needs at its peak. + +The rest of the system gets what the real-time threads leave. With +`setIsolateCpu(true)`, which is the default when a CPU is given, every other thread +of the process is moved off that core. For a hard split, add `isolcpus=` and +`nohz_full=` on the kernel command line, and limit the other services with a +cgroup `cpu.max`. + +=== Where it runs + +[cols="1,3"] +|=== +| Platform | Behaviour + +| Linux and embedded Linux (PREEMPT_RT) +| `SCHED_FIFO` priority, CPU pinning and isolation, memory locking, collector + isolation, allocation trap, memory limit. `isSupported(FEATURE_PREEMPT_RT_KERNEL)` + tells you whether the kernel is real-time. + +| Linux (standard kernel) +| The same, but latency is the scheduler's best effort. + +| Backend server (ParparVM) +| The same as the Linux port on Linux. + +| Simulator and other platforms +| The loop runs on an ordinary thread at its period, with statistics, so the logic + can be developed and tested. No scheduling guarantee. +|=== + +Real-time scheduling needs the `CAP_SYS_NICE` capability, or an `rtprio` limit in +`/etc/security/limits.conf`. Without it the thread runs normally, and +`isSchedulingApplied()` returns false. Call `setSchedulingRequired(true)` to make +the thread fail instead. + +=== Measuring + +`vm/backend/demo/rtcheck` is the check CI runs. A 1 kHz loop runs while the main +thread allocates hard enough to keep the collector busy all the time. It then +checks that a cycle allocating through an interface is stopped, and that the +memory limit throws. Run it on your board to see the release latency the hardware +gives you before you rely on a deadline. diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index 91fbebd15d6..1dd0adee5cc 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -298,6 +298,8 @@ include::Working-With-Linux.asciidoc[] include::Embedded-Linux.asciidoc[] +include::Realtime.asciidoc[] + include::Desktop-Integration.asciidoc[] include::Desktop-Windows.asciidoc[] diff --git a/vm/ByteCodeTranslator/src/cn1_globals.h b/vm/ByteCodeTranslator/src/cn1_globals.h index cce9a22c421..909d69a862d 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.h +++ b/vm/ByteCodeTranslator/src/cn1_globals.h @@ -198,6 +198,15 @@ // this off and pay no overhead. //#define CN1_ON_DEVICE_DEBUG +// Real-time threads (com.codename1.realtime). The translator switches this on when +// the application's RealtimeThread survives the cull, so an application that never +// starts one compiles exactly as before. It changes two things: the VM's own mutexes +// and every Java monitor are created with priority inheritance on Linux, so a +// real-time thread blocked on a lock a normal thread holds lends that thread its +// priority instead of waiting behind whatever preempted it; and the cn1Rt* entry +// points below get their real bodies. See cn1_realtime_impl.h. +//#define CN1_REALTIME + #ifdef DEBUG_GC_ALLOCATIONS #define DEBUG_GC_VARIABLES int line; int className; #define DEBUG_GC_INIT 0, 0, @@ -2169,6 +2178,30 @@ struct ThreadLocalData { struct ThreadLocalData* gcDeadNext; JAVA_BOOLEAN gcQueuedForDrain; JAVA_BOOLEAN gcReleaseRequested; + + // REAL-TIME STATE (com.codename1.realtime), zero for every other thread; the + // struct is calloc'd. realtimeThread is raised by cn1RtEnter on the thread itself + // and changes how the collector treats it: never part of a stop-the-world cycle, + // never force-stopped by signal, released as soon as its own stack is scanned. + // rtAllocTrap is raised while a steady-state cycle runs; rtSavedBibopCur holds the + // thread's real page array while CN1_BIBOP_CUR points at cn1RtNoPages, which is + // what sends every allocation off the inline fast path to the trap. See + // cn1RtFreeze in cn1_globals.m. + JAVA_BOOLEAN realtimeThread; + JAVA_BOOLEAN rtAllocTrap; + JAVA_INT rtAllocBudget; + JAVA_INT rtAllocBudgetLeft; + JAVA_LONG rtAllocViolations; + struct CN1BibopPage** rtSavedBibopCur; + // The RealtimeAllocationError a trapped cycle throws, made by the thread before it + // froze and kept reachable by its RealtimeThread. + JAVA_OBJECT rtAllocError; + // The collection after which this thread was last refused for the memory limit + // (cn1RtRefuseOverLimit), plus one so that zero means never. A thread is refused at + // most once per collection: the throw itself allocates its stack trace, and the + // next refusal waits for a collection that might have brought the process back + // under the limit. + JAVA_INT rtOomEpoch; // THREAD IDENTITY, AND DELIBERATELY OUTSIDE THE CONSERVATIVE-ROOTS GUARD BELOW. // These two say "this TLD belongs to a real registered thread", which is a question // every configuration asks: CN1_RESUME_THREAD reads gcPthreadValid unconditionally @@ -3943,6 +3976,23 @@ static inline JAVA_OBJECT cn1FusedInstallPrimArray(JAVA_OBJECT owner, int off, s } // Register an object referenced only from C globals as a permanent GC root. extern void cn1AddImmortalRoot(JAVA_OBJECT o); +// Real-time threads (com.codename1.realtime); defined at the end of cn1_globals.m, which +// says what each does. Declared for every build: the collector and the allocator read +// the state they keep whether or not the application starts a real-time thread. +extern volatile int cn1RtThreadCount; +extern volatile int cn1RtOverMemoryLimit; +extern void cn1RtEnter(CODENAME_ONE_THREAD_STATE); +extern void cn1RtLeave(CODENAME_ONE_THREAD_STATE); +extern void cn1RtFreeze(CODENAME_ONE_THREAD_STATE, JAVA_INT budget); +extern void cn1RtThaw(CODENAME_ONE_THREAD_STATE); +extern void cn1RtNewPeriod(CODENAME_ONE_THREAD_STATE); +extern const char* cn1RtLastViolationClass(void); +extern JAVA_OBJECT cn1RtTrappedAlloc(CODENAME_ONE_THREAD_STATE, int size, struct clazz* parent); +extern void cn1RtSetMemoryLimit(JAVA_LONG bytes, JAVA_OBJECT error); +extern int cn1RtMemoryLimitSet(void); +extern void cn1RtAfterCycle(JAVA_LONG residentBytes); +extern void cn1RtRefuseOverLimit(CODENAME_ONE_THREAD_STATE); +extern JAVA_LONG cn1RtThreadAllocViolations(CODENAME_ONE_THREAD_STATE); // Flag a BiBOP object's page as holding a native peer (cached NSString etc.): // its dead slots then always reach cn1BibopReclaimSlot, which releases the // peer -- instead of the O(1) all-dead page reclaim, which would leak it. @@ -4882,6 +4932,21 @@ extern __thread struct ThreadLocalData* cn1TlsSelf; #define CN1_GC_PARK_CAPTURE(ts) do {} while(0) #endif +// A real-time thread parked around its period's sleep (cn1_realtime_impl.h). Macros, like +// CN1_YIELD_THREAD, so the park capture is taken in the frame that stays live through +// the wait. The resume differs from CN1_RESUME_THREAD's: that one polls the collector's +// release in 1ms sleeps, which would be the whole period of a 1kHz loop, where this +// spins briefly and then waits on the collector's wake condition (CN1_GC_RESUME_ACTIVE). +// A release comes microseconds after the scan of the thread's own shallow stack. +#define CN1_RT_PARK(ts) do { \ + CN1_GC_PARK_CAPTURE(ts); \ + __atomic_store_n(&(ts)->threadActive, JAVA_FALSE, __ATOMIC_RELEASE); \ + } while(0) +#define CN1_RT_UNPARK(ts) do { \ + CN1_GC_RESUME_ACTIVE(ts); \ + CN1_GC_PARK_RELEASE(ts); \ + } while(0) + // Bracket one mutator park so its duration is charged to a cause. Both halves compile to // nothing without -DCN1_GC_CONFORM -- including the timestamp variable, which is why the // name is a macro argument rather than a fixed identifier: several park sites sit in one diff --git a/vm/ByteCodeTranslator/src/cn1_globals.m b/vm/ByteCodeTranslator/src/cn1_globals.m index 10be4cd05d6..06c33a80267 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.m +++ b/vm/ByteCodeTranslator/src/cn1_globals.m @@ -744,6 +744,16 @@ static JAVA_BOOLEAN cn1GcUnheldThreadPresent(void) { } void cn1GcReleaseAllBlockedThreadsPublic(void); +// How many threads are real-time (com.codename1.realtime, cn1RtEnter). While any is, +// every cycle is concurrent: a stop-the-world cycle holds every thread until its sweep +// ends, which is the whole collection charged to a thread whose period may be a +// millisecond. Same rule, and same two places, as a virtual thread or an unheld native +// thread: cn1GcHybridDecide, and the prologue's re-check for one that arrived since. +extern volatile int cn1RtThreadCount; +static JAVA_BOOLEAN cn1RtThreadsPresent(void) { + return __atomic_load_n(&cn1RtThreadCount, __ATOMIC_SEQ_CST) > 0 ? JAVA_TRUE : JAVA_FALSE; +} + // Monotonic nanoseconds, for the collector's own short waits and policy timing. Same // split as cn1MonotonicMillis: clock_gettime / CLOCK_MONOTONIC do not exist on the // clang-cl Windows target, so it reads the QueryPerformanceCounter-backed clock there @@ -5836,7 +5846,7 @@ void codenameOneGCMark() { // scanned -- so the cycle simply becomes a concurrent one. __atomic_store_n(&cn1GcStwInProgress, 1, __ATOMIC_SEQ_CST); if(__atomic_load_n(&cn1GcVirtualThreadsSeen, __ATOMIC_SEQ_CST) - || cn1GcUnheldThreadPresent()) { + || cn1GcUnheldThreadPresent() || cn1RtThreadsPresent()) { __atomic_store_n(&cn1GcStwInProgress, 0, __ATOMIC_SEQ_CST); cn1GcStwCycle = JAVA_FALSE; #ifndef CN1_DISABLE_BIBOP @@ -6307,7 +6317,11 @@ void codenameOneGCMark() { // stop cannot work, and retrying past it would rebuild the root // snapshot every 250ms for the rest of a wait that is already // never going to end. - if(totalwait >= nextEscalation && t->gcPthreadValid) { + // Never for a real-time thread. It reaches its safepoint when its + // period's work is done (cn1RtPark), so waiting delays only the + // collector; a signal stop would freeze it mid-period, which is + // the one pause it exists to avoid. + if(totalwait >= nextEscalation && t->gcPthreadValid && !t->realtimeThread) { nextEscalation += (long long)CN1_GC_SAFEPOINT_WAIT_MAX_US; // The snapshot rebuild MUST happen here, BEFORE the freeze: it // reallocs, and a thread frozen mid-malloc holds the libc @@ -6441,6 +6455,11 @@ void codenameOneGCMark() { } else { agressiveAllocator = allocSize > CN1_AGRESSIVE_ALLOCATOR_THREAD_HEAP_ALLOCATIONS_THRESHOLD; } + // A real-time thread is never held to the sweep: see the early + // release above. + if(t->realtimeThread) { + agressiveAllocator = JAVA_FALSE; + } } #endif if (CN1_EDT_THREAD_ID == t->threadId && agressiveAllocator) { @@ -6564,6 +6583,21 @@ void codenameOneGCMark() { #ifdef CN1_GC_CONFORM cn1GcStackNs += cn1GcNowNs(); #endif +#if !defined(CN1_DISABLE_SATB) + // A REAL-TIME THREAD GOES AS SOON AS ITS OWN STACKS ARE TAKEN, before the + // statics below. Its roots are in the mark queue now, and SATB has been + // armed since before this loop began, so a reference it moves out of a + // static or a field from here on is logged -- the same argument that lets + // every other thread go before the drain, applied one step earlier. That + // step is what bounds its pause by its own (shallow, parked between + // periods) stack rather than by the program's statics. Never in a + // stop-the-world cycle, where every thread is held to the sweep; none + // starts while a real-time thread exists (cn1RtThreadsPresent). + if(t->realtimeThread && !cn1GcStwCycle) { + __atomic_store_n(&t->threadBlockedByGC, JAVA_FALSE, __ATOMIC_RELEASE); + cn1GcWakeUnblocked(); + } +#endif #ifdef CN1_GC_CAN_FORCE_STOP // Released as soon as this thread's roots are captured, which is HERE and // not at the threadBlockedByGC clear below. A cooperatively parked thread @@ -8952,7 +8986,7 @@ static JAVA_BOOLEAN cn1GcHybridDecide(void) { cn1GcHybridPrevStartNs = cn1GcHybridAnyStartNs; cn1GcHybridAnyStartNs = cn1GcHybridNowNs(); if(__atomic_load_n(&cn1GcVirtualThreadsSeen, __ATOMIC_SEQ_CST) - || cn1GcUnheldThreadPresent()) { + || cn1GcUnheldThreadPresent() || cn1RtThreadsPresent()) { if(cn1GcHybridGen) { cn1GcHybridGen = 0; } @@ -12003,7 +12037,9 @@ void cn1GcRegisterImmortalObj(JAVA_OBJECT o) { // attempt (REGISTRY Rounds 38 and 39). Answers 0 when there is no current page or its free // list is empty; the caller then takes __NEW_X exactly as before. JAVA_OBJECT cn1BibopAllocRecycled(CODENAME_ONE_THREAD_STATE, int size, struct clazz* parent, int ci) { - if(ci < 0 || constantPoolObjects == 0) { + // A trapped real-time cycle falls through to __NEW_X and codenameOneGcMalloc, + // where cn1RtTrappedAlloc decides; a recycled slot taken here would bypass it. + if(ci < 0 || constantPoolObjects == 0 || threadStateData->rtAllocTrap) { return 0; } #ifndef CN1_CONSERVATIVE_GC_ROOTS @@ -12037,7 +12073,7 @@ JAVA_OBJECT cn1BibopAllocRecycled(CODENAME_ONE_THREAD_STATE, int size, struct cl // The free-list half of cn1BibopFastAllocNoZero, kept out of line so the bump path // stays small enough to inline at every allocation site (see the note there). JAVA_OBJECT cn1BibopAllocRecycledNoZero(CODENAME_ONE_THREAD_STATE, int size, struct clazz* parent, int ci) { - if(ci < 0) return (JAVA_OBJECT)0; + if(ci < 0 || threadStateData->rtAllocTrap) return (JAVA_OBJECT)0; // see cn1BibopAllocRecycled CN1BibopPage* p = CN1_BIBOP_CUR(threadStateData)[ci]; // A RECYCLED page: take a slot its last sweep freed. This used to fall through to // __NEW_X and the full slow path for EVERY object once pages came back partial -- @@ -15931,7 +15967,23 @@ static void cn1GcSelfCheckThreadStack(struct ThreadLocalData* t, int stackSize) #endif #endif /* CN1_CONSERVATIVE_GC_ROOTS */ +// The memory limit of com.codename1.realtime; defined with the rest of it at the end. +static long long cn1RtMemoryLimitBytes; +static void cn1RtAccount(int size); JAVA_OBJECT codenameOneGcMalloc(CODENAME_ONE_THREAD_STATE, int size, struct clazz* parent) { + // Real-time admission, ahead of everything: a steady-state real-time cycle reaches + // here for EVERY allocation (cn1RtFreeze took its fast path away), and a thread over + // the memory limit is refused before it can wait for a collection. Both are one + // byte or int load on a path that is already the slow one. + if(__builtin_expect(threadStateData->rtAllocTrap, 0)) { + return cn1RtTrappedAlloc(threadStateData, size, parent); + } + if(__builtin_expect(cn1RtMemoryLimitBytes > 0, 0)) { + cn1RtAccount(size); + if(cn1RtOverMemoryLimit) { + cn1RtRefuseOverLimit(threadStateData); + } + } #ifdef CN1_GC_CONFORM cn1RecordAllocation(parent, size); #endif @@ -21300,3 +21352,209 @@ static void cn1NoteNativeAllocation(size_t bytes) { } #endif } + +// ============================================================================================ +// REAL-TIME THREADS (com.codename1.realtime) +// +// What the VM does for a thread that cn1RtEnter made real-time. The scheduling side -- +// SCHED_FIFO, CPU affinity, memory locking, the periodic clock -- is the operating system's +// and lives in cn1_realtime_impl.h; this half is the part only the VM can do: +// +// - The collector never holds one for a whole cycle. Cycles stay concurrent while one +// exists (cn1RtThreadsPresent), it is never signal-stopped, and it is released as soon as +// its own stacks are scanned (codenameOneGCMark). Between periods it is parked in +// cn1RtPark, where the collector scans it without stopping anything; a scan that is still +// running when the next period starts holds it for that one shallow stack. +// - A steady-state cycle cannot allocate behind its own back. cn1RtFreeze points the +// thread's current-page array at cn1RtNoPages, which is all null, so every inline BiBOP +// bump misses; the recycled-slot paths refuse it (rtAllocTrap); and everything then lands +// in codenameOneGcMalloc, which hands it to cn1RtTrappedAlloc. That costs the thread +// nothing while it does not allocate and costs every other thread nothing at all: the +// inline fast path is untouched. Where CN1_BIBOP_CUR is the thread's own array rather +// than a pointer to it (targets without virtual threads: Windows, 32-bit ARM), the +// inline bump into a page the thread already holds cannot be redirected, so the trap +// sees only allocations that need a new page or the slow path; the build-time check +// (RealtimeAllocationVerifier) is the whole of the guarantee there. +// - A memory limit refuses allocation with OutOfMemoryError instead of letting a thread +// wait for collections that cannot free enough (cn1RtAfterCycle, cn1RtRefuseOverLimit). +// ============================================================================================ + +volatile int cn1RtThreadCount = 0; +volatile int cn1RtOverMemoryLimit = 0; +static long long cn1RtMemoryLimitBytes = 0; +static JAVA_OBJECT cn1RtOomError = JAVA_NULL; +#ifdef CN1_VIRTUAL_THREADS +// Never written with anything but 0: cn1BibopAlloc is the only writer of a non-null page +// and a trapped thread never reaches it with the swap in place (cn1RtTrappedAlloc swaps +// back first). Two real-time threads storing 0 into it is the only possible write. +static CN1BibopPage* cn1RtNoPages[CN1_BIBOP_NUM_CLASSES]; +#endif + +void cn1RtEnter(CODENAME_ONE_THREAD_STATE) { + if(!threadStateData->realtimeThread) { + threadStateData->realtimeThread = JAVA_TRUE; + __atomic_fetch_add(&cn1RtThreadCount, 1, __ATOMIC_SEQ_CST); + } +} + +void cn1RtLeave(CODENAME_ONE_THREAD_STATE) { + cn1RtThaw(threadStateData); + if(threadStateData->realtimeThread) { + threadStateData->realtimeThread = JAVA_FALSE; + __atomic_fetch_sub(&cn1RtThreadCount, 1, __ATOMIC_SEQ_CST); + } +} + +void cn1RtFreeze(CODENAME_ONE_THREAD_STATE, JAVA_INT budget) { + if(threadStateData->rtAllocTrap) { + threadStateData->rtAllocBudgetLeft = threadStateData->rtAllocBudget; + return; + } + threadStateData->rtAllocBudget = budget < 0 ? 0 : budget; + threadStateData->rtAllocBudgetLeft = threadStateData->rtAllocBudget; +#ifdef CN1_VIRTUAL_THREADS + threadStateData->rtSavedBibopCur = threadStateData->bibopCur; + threadStateData->bibopCur = cn1RtNoPages; +#endif + threadStateData->rtAllocTrap = JAVA_TRUE; +} + +void cn1RtThaw(CODENAME_ONE_THREAD_STATE) { + if(!threadStateData->rtAllocTrap) { + return; + } + threadStateData->rtAllocTrap = JAVA_FALSE; +#ifdef CN1_VIRTUAL_THREADS + if(threadStateData->rtSavedBibopCur != 0) { + threadStateData->bibopCur = threadStateData->rtSavedBibopCur; + threadStateData->rtSavedBibopCur = 0; + } +#endif +} + +// Starts a new period's allocation budget without leaving the steady state. +void cn1RtNewPeriod(CODENAME_ONE_THREAD_STATE) { + threadStateData->rtAllocBudgetLeft = threadStateData->rtAllocBudget; +} + +// The class of the allocation that ended the last steady state, for the error's message. +// Read by the thread itself after it has caught the error, so no synchronization. +static __thread const char* cn1RtViolationClass = 0; +const char* cn1RtLastViolationClass(void) { + return cn1RtViolationClass; +} + +JAVA_OBJECT cn1RtTrappedAlloc(CODENAME_ONE_THREAD_STATE, int size, struct clazz* parent) { + JAVA_OBJECT o; + // Inside the budget: allocate exactly as an ordinary thread would, with the real page + // array back in place for the call, and trap again afterwards. + if(threadStateData->rtAllocBudgetLeft > 0) { + threadStateData->rtAllocBudgetLeft--; + cn1RtThaw(threadStateData); + o = codenameOneGcMalloc(threadStateData, size, parent); + cn1RtFreeze(threadStateData, threadStateData->rtAllocBudget); + return o; + } + // Over it. The steady state is over: thaw first, because building the error's stack + // trace allocates, then throw the error the thread handed us when it froze. It was + // allocated before the freeze, so the throw itself never needs the allocation that + // was just refused. + threadStateData->rtAllocViolations++; + cn1RtViolationClass = parent != 0 ? parent->clsName : "array"; + cn1RtThaw(threadStateData); + if(threadStateData->rtAllocError != JAVA_NULL) { + throwException(threadStateData, threadStateData->rtAllocError); + } + return codenameOneGcMalloc(threadStateData, size, parent); +} + +int cn1RtMemoryLimitSet(void) { + return cn1RtMemoryLimitBytes > 0; +} + +void cn1RtSetMemoryLimit(JAVA_LONG bytes, JAVA_OBJECT error) { + if(error != JAVA_NULL && cn1RtOomError == JAVA_NULL) { + cn1AddImmortalRoot(error); + cn1RtOomError = error; + } + cn1RtMemoryLimitBytes = bytes; + if(bytes <= 0) { + cn1RtOverMemoryLimit = 0; + } +} + +// Runs on the collector thread at the end of every cycle (java_lang_System_gcMarkSweep__), +// with the process's resident bytes, so the measurement is taken when the heap is as small +// as it is going to get. The limit is on the PROCESS, not the Java heap: on a device that +// is what runs out, and with memory locked it is all of it. +void cn1RtAfterCycle(JAVA_LONG residentBytes) { + long long limit = cn1RtMemoryLimitBytes; + if(limit <= 0 || residentBytes <= 0) { + return; + } + __atomic_store_n(&cn1RtOverMemoryLimit, residentBytes > limit ? 1 : 0, __ATOMIC_RELEASE); +} + +// An allocation that needs the slow path while the last collection left the process over +// its limit. A real-time thread is exempt: its allocations are bounded by its own budget, +// and failing a control loop because a background thread leaked would be backwards. +// Refusing only on the slow path is deliberate: a slot in a page a thread already holds +// costs no new memory. +// Throwing allocates too (the stack trace), so after a refusal the thread's next +// CN1_RT_REFUSAL_GRACE slow-path allocations pass: enough to build and deliver the +// error, and a thread that catches it and keeps allocating is refused again soon after. +#define CN1_RT_REFUSAL_GRACE 64 +void cn1RtRefuseOverLimit(CODENAME_ONE_THREAD_STATE) { + if(threadStateData->realtimeThread || threadStateData->nativeAllocationMode + || cn1RtOomError == JAVA_NULL) { + return; + } + if(threadStateData->rtOomEpoch > 0) { + threadStateData->rtOomEpoch--; + return; + } + threadStateData->rtOomEpoch = CN1_RT_REFUSAL_GRACE; + throwException(threadStateData, cn1RtOomError); +} + +// The measurement between collections. A collection is what decides (cn1RtAfterCycle), +// but nothing makes one run promptly: a process allocating large live arrays on a host +// with memory to spare measured ONE cycle across 3.4GB. So the slow path also reads the +// resident size, once per 1/64 of the limit allocated: over the limit it asks for a +// collection to decide, and past a quarter over it refuses without waiting for one -- +// fail fast is the point of the limit. +static long long cn1RtBytesSinceCheck = 0; +#if defined(__linux__) +extern long long cn1RtResidentBytes(void); // nativeMethods.m +#else +// The limit is documented as Linux only; elsewhere it never measures anything. +static long long cn1RtResidentBytes(void) { + return 0; +} +#endif +static void cn1RtAccount(int size) { + long long limit = cn1RtMemoryLimitBytes; + long long step = limit / 64; + if(step < 1024 * 1024) { + step = 1024 * 1024; + } + if(__atomic_add_fetch(&cn1RtBytesSinceCheck, (long long)size, __ATOMIC_RELAXED) < step) { + return; + } + __atomic_store_n(&cn1RtBytesSinceCheck, 0, __ATOMIC_RELAXED); + long long rss = cn1RtResidentBytes(); + if(rss <= 0) { + return; + } + if(rss <= limit) { + __atomic_store_n(&cn1RtOverMemoryLimit, 0, __ATOMIC_RELEASE); + } else if(rss > limit + limit / 4) { + __atomic_store_n(&cn1RtOverMemoryLimit, 1, __ATOMIC_RELEASE); + } else { + cn1RequestGcFromParkedThread(); + } +} + +JAVA_LONG cn1RtThreadAllocViolations(CODENAME_ONE_THREAD_STATE) { + return threadStateData->rtAllocViolations; +} diff --git a/vm/ByteCodeTranslator/src/cn1_realtime_impl.h b/vm/ByteCodeTranslator/src/cn1_realtime_impl.h new file mode 100644 index 00000000000..0a12507415f --- /dev/null +++ b/vm/ByteCodeTranslator/src/cn1_realtime_impl.h @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The operating-system half of com.codename1.realtime, shared by the Linux port + * (cn1_linux_realtime.c) and the headless backend (vm/backend/native/cn1_backend_realtime.c). + * Each defines the natives under its own class's name and forwards here, as + * cn1_hardware_impl.h does. The VM half -- what the collector and the allocator do for a + * real-time thread -- is cn1Rt* at the end of cn1_globals.m. + * + * Every function acts on the CALLING thread: a real-time thread enters, freezes, waits + * and leaves from its own body, so there are no handles and the per-thread state is + * __thread. Results are >= 0, or -errno. + * + * Linux gets the real thing: SCHED_FIFO through pthread_setschedparam, CPU affinity, the + * other threads moved off a reserved CPU, mlockall, and an absolute periodic release on + * CLOCK_MONOTONIC with clock_nanosleep(TIMER_ABSTIME), which is what keeps a period from + * drifting by the cycle's own run time. PREEMPT_RT (mainline since 6.12) is what bounds + * the kernel's side of the latency; on a stock kernel the same calls run, with worse + * worst cases, and RT_FEATURE_PREEMPT_RT says which one this is. Elsewhere -- the + * backend on macOS during development -- the VM half still works and the timing is a + * relative sleep. + */ + +#ifndef CN1_REALTIME_IMPL_H +#define CN1_REALTIME_IMPL_H + +#include "cn1_globals.h" +#include +#include +#include +#include +#include +#include +#if defined(__linux__) +#include +#include +#include +#include +#include +#endif + +/* Feature bits; the same values as Realtime.FEATURE_* in Java. */ +#define CN1_RT_FEATURE_SCHEDULING 1 +#define CN1_RT_FEATURE_MEMORY_LOCK 2 +#define CN1_RT_FEATURE_GC_ISOLATION 4 +#define CN1_RT_FEATURE_ALLOC_TRAP 8 +#define CN1_RT_FEATURE_PREEMPT_RT 16 +#define CN1_RT_FEATURE_CPU_AFFINITY 32 + +/* What rtEnter applied, as a mask; the Java side decides whether a missing piece is fatal. */ +#define CN1_RT_APPLIED_SCHEDULING 1 +#define CN1_RT_APPLIED_AFFINITY 2 +#define CN1_RT_APPLIED_ISOLATION 4 + +/* Statistics slots, the layout RealtimeStatistics reads. Latency is release to wake-up, + * execution is wake-up to the next wait; the histogram counts latencies by powers of two + * of a microsecond: [0] under 1us, [1] under 2us, ... [15] 16ms and over. */ +#define CN1_RT_STAT_CYCLES 0 +#define CN1_RT_STAT_MISSED 1 +#define CN1_RT_STAT_MAX_LATENCY 2 +#define CN1_RT_STAT_SUM_LATENCY 3 +#define CN1_RT_STAT_MAX_EXEC 4 +#define CN1_RT_STAT_SUM_EXEC 5 +#define CN1_RT_STAT_VIOLATIONS 6 +#define CN1_RT_STAT_HIST 7 +#define CN1_RT_HIST_BUCKETS 16 +#define CN1_RT_STAT_COUNT (CN1_RT_STAT_HIST + CN1_RT_HIST_BUCKETS) + +static __thread long long cn1RtPeriodNs = 0; +static __thread long long cn1RtNextReleaseNs = 0; +static __thread long long cn1RtWokeNs = 0; +static __thread long long cn1RtStats[CN1_RT_STAT_COUNT]; +static __thread int cn1RtOldPolicy = -1; +static __thread struct sched_param cn1RtOldParam; + +static long long cn1RtNowNs(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long long) ts.tv_sec * 1000000000LL + ts.tv_nsec; +} + +#if defined(__linux__) +static int cn1RtKernelIsPreemptRt(void) { + FILE* f = fopen("/sys/kernel/realtime", "r"); + int c; + if (f == 0) { + return 0; + } + c = fgetc(f); + fclose(f); + return c == '1'; +} + +/* Takes `cpu` away from every other thread of the process, so the real-time thread has + * it to itself. Threads started later inherit their creator's mask, and a real-time + * thread never creates one, so they stay off it too. Best effort: a thread that exits + * between the listing and the call is not an error. */ +static int cn1RtIsolateCpu(int cpu) { + DIR* d = opendir("/proc/self/task"); + struct dirent* e; + pid_t self = (pid_t) syscall(SYS_gettid); + int moved = 0; + if (d == 0) { + return -errno; + } + while ((e = readdir(d)) != 0) { + cpu_set_t set; + pid_t tid; + if (e->d_name[0] < '0' || e->d_name[0] > '9') { + continue; + } + tid = (pid_t) strtol(e->d_name, 0, 10); + if (tid == self || sched_getaffinity(tid, sizeof(set), &set) != 0) { + continue; + } + if (CPU_ISSET(cpu, &set) && CPU_COUNT(&set) > 1) { + CPU_CLR(cpu, &set); + if (sched_setaffinity(tid, sizeof(set), &set) == 0) { + moved++; + } + } + } + closedir(d); + return moved; +} + +/* Touches the stack the thread will run on, so a page fault never lands in a period. + * With mlockall(MCL_FUTURE) the pages then stay. */ +static void cn1RtPrefaultStack(void) { + volatile char buf[128 * 1024]; + size_t i; + for (i = 0; i < sizeof(buf); i += 4096) { + buf[i] = 0; + } +} +#endif + +static JAVA_INT cn1RtImpl_rtFeatures(CODENAME_ONE_THREAD_STATE) { + int f = CN1_RT_FEATURE_GC_ISOLATION; +#ifdef CN1_VIRTUAL_THREADS + f |= CN1_RT_FEATURE_ALLOC_TRAP; +#endif +#if defined(__linux__) + f |= CN1_RT_FEATURE_SCHEDULING | CN1_RT_FEATURE_MEMORY_LOCK | CN1_RT_FEATURE_CPU_AFFINITY; + if (cn1RtKernelIsPreemptRt()) { + f |= CN1_RT_FEATURE_PREEMPT_RT; + } +#endif + return f; +} + +static JAVA_INT cn1RtImpl_rtLockMemory(CODENAME_ONE_THREAD_STATE) { +#if defined(__linux__) + /* MCL_ONFAULT: the heap and the virtual-thread stacks are large address + * reservations of which a fraction is ever touched, and locking them up front + * asks for all of it at once -- mlockall(MCL_CURRENT) fails with ENOMEM on any + * machine with less RAM than the reservations. With MCL_ONFAULT a page is locked + * when first touched and never leaves; the real-time thread's own stack is + * touched in advance by cn1RtImpl_rtEnter. Kernels before 4.4 lack the flag and + * reject it with EINVAL, so they get the plain call. */ +#ifdef MCL_ONFAULT + if (mlockall(MCL_CURRENT | MCL_FUTURE | MCL_ONFAULT) == 0) { + return 0; + } + if (errno != EINVAL) { + return -errno; + } +#endif + if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) { + return -errno; + } + return 0; +#else + return -ENOSYS; +#endif +} + +/* Makes the calling thread real-time: the VM's part first, which cannot fail, then the + * scheduler's, which needs CAP_SYS_NICE or an rtprio limit and so can. Returns the + * CN1_RT_APPLIED_ mask, or -errno for an argument the scheduler rejects outright. */ +static JAVA_INT cn1RtImpl_rtEnter(CODENAME_ONE_THREAD_STATE, JAVA_INT priority, JAVA_INT cpu, JAVA_BOOLEAN isolate) { + int applied = 0; + cn1RtEnter(threadStateData); + memset(cn1RtStats, 0, sizeof(cn1RtStats)); + cn1RtPeriodNs = 0; +#if defined(__linux__) + { + struct sched_param p; + int lo = sched_get_priority_min(SCHED_FIFO); + int hi = sched_get_priority_max(SCHED_FIFO); + if (priority < lo || priority > hi) { + cn1RtLeave(threadStateData); + return -EINVAL; + } + if (cn1RtOldPolicy < 0) { + pthread_getschedparam(pthread_self(), &cn1RtOldPolicy, &cn1RtOldParam); + } + memset(&p, 0, sizeof(p)); + p.sched_priority = priority; + if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &p) == 0) { + applied |= CN1_RT_APPLIED_SCHEDULING; + } + } + if (cpu >= 0) { + cpu_set_t set; + if (cpu >= CPU_SETSIZE) { + cn1RtLeave(threadStateData); + return -EINVAL; + } + CPU_ZERO(&set); + CPU_SET(cpu, &set); + if (pthread_setaffinity_np(pthread_self(), sizeof(set), &set) == 0) { + applied |= CN1_RT_APPLIED_AFFINITY; + if (isolate && cn1RtIsolateCpu(cpu) >= 0) { + applied |= CN1_RT_APPLIED_ISOLATION; + } + } + } + cn1RtPrefaultStack(); +#else + (void) priority; + (void) cpu; + (void) isolate; +#endif + return applied; +} + +static JAVA_VOID cn1RtImpl_rtLeave(CODENAME_ONE_THREAD_STATE) { + cn1RtLeave(threadStateData); +#if defined(__linux__) + if (cn1RtOldPolicy >= 0) { + pthread_setschedparam(pthread_self(), cn1RtOldPolicy, &cn1RtOldParam); + cn1RtOldPolicy = -1; + } +#endif +} + +/* Starts the periodic clock: the first release is one period from now. */ +static JAVA_VOID cn1RtImpl_rtStartPeriod(CODENAME_ONE_THREAD_STATE, JAVA_LONG periodNs) { + cn1RtPeriodNs = periodNs > 0 ? periodNs : 1000000LL; + cn1RtNextReleaseNs = cn1RtNowNs() + cn1RtPeriodNs; + cn1RtWokeNs = 0; +} + +static void cn1RtRecord(long long latency, long long exec) { + int b = 0; + long long us = latency / 1000; + cn1RtStats[CN1_RT_STAT_CYCLES]++; + if (latency > cn1RtStats[CN1_RT_STAT_MAX_LATENCY]) { + cn1RtStats[CN1_RT_STAT_MAX_LATENCY] = latency; + } + cn1RtStats[CN1_RT_STAT_SUM_LATENCY] += latency; + if (exec > cn1RtStats[CN1_RT_STAT_MAX_EXEC]) { + cn1RtStats[CN1_RT_STAT_MAX_EXEC] = exec; + } + cn1RtStats[CN1_RT_STAT_SUM_EXEC] += exec; + while (us > 0 && b < CN1_RT_HIST_BUCKETS - 1) { + us >>= 1; + b++; + } + cn1RtStats[CN1_RT_STAT_HIST + b]++; +} + +/* Sleeps to the next release and returns how many releases were missed (0 when on time). + * Parked for the whole sleep, so the collector scans this thread then rather than + * stopping it later; the wake-up is the only place it can be held, and only while its + * own stack is being read. A missed release is skipped rather than run late in a burst: + * a control loop wants the next period on time, not three in a row. */ +static JAVA_INT cn1RtImpl_rtWaitNextPeriod(CODENAME_ONE_THREAD_STATE) { + long long before = cn1RtNowNs(); + long long woke; + long long exec = cn1RtWokeNs > 0 ? before - cn1RtWokeNs : 0; + int missed = 0; + if (cn1RtPeriodNs <= 0) { + return -EINVAL; + } + CN1_RT_PARK(threadStateData); +#if defined(__linux__) + { + struct timespec t; + int r; + t.tv_sec = (time_t) (cn1RtNextReleaseNs / 1000000000LL); + t.tv_nsec = (long) (cn1RtNextReleaseNs % 1000000000LL); + do { + r = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &t, 0); + } while (r == EINTR); + } +#else + { + long long left = cn1RtNextReleaseNs - before; + if (left > 0) { + struct timespec t; + t.tv_sec = (time_t) (left / 1000000000LL); + t.tv_nsec = (long) (left % 1000000000LL); + while (nanosleep(&t, &t) != 0 && errno == EINTR) { + } + } + } +#endif + CN1_RT_UNPARK(threadStateData); + woke = cn1RtNowNs(); + cn1RtRecord(woke > cn1RtNextReleaseNs ? woke - cn1RtNextReleaseNs : 0, exec); + cn1RtNextReleaseNs += cn1RtPeriodNs; + while (cn1RtNextReleaseNs <= woke) { + cn1RtNextReleaseNs += cn1RtPeriodNs; + missed++; + } + cn1RtStats[CN1_RT_STAT_MISSED] += missed; + cn1RtWokeNs = woke; + cn1RtNewPeriod(threadStateData); + return missed; +} + +static JAVA_VOID cn1RtImpl_rtFreeze(CODENAME_ONE_THREAD_STATE, JAVA_INT budget, JAVA_OBJECT error) { + threadStateData->rtAllocError = error; + cn1RtFreeze(threadStateData, budget); +} + +static JAVA_VOID cn1RtImpl_rtThaw(CODENAME_ONE_THREAD_STATE) { + cn1RtThaw(threadStateData); +} + +/* Copies this thread's statistics into `out` (CN1_RT_STAT_COUNT longs). */ +static JAVA_INT cn1RtImpl_rtStatistics(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT out) { + int i; + int n; + JAVA_LONG* d; + if (out == JAVA_NULL) { + return -EINVAL; + } + cn1RtStats[CN1_RT_STAT_VIOLATIONS] = cn1RtThreadAllocViolations(threadStateData); + n = CN1_ARRAY_LENGTH(out); + if (n > CN1_RT_STAT_COUNT) { + n = CN1_RT_STAT_COUNT; + } + d = (JAVA_LONG*) CN1_ARRAY_DATA(out); + for (i = 0; i < n; i++) { + d[i] = (JAVA_LONG) cn1RtStats[i]; + } + return n; +} + +static JAVA_OBJECT cn1RtImpl_rtLastViolation(CODENAME_ONE_THREAD_STATE) { + const char* c = cn1RtLastViolationClass(); + return c == 0 ? JAVA_NULL : newStringFromCString(threadStateData, c); +} + +static JAVA_VOID cn1RtImpl_rtSetMemoryLimit(CODENAME_ONE_THREAD_STATE, JAVA_LONG bytes, JAVA_OBJECT error) { + cn1RtSetMemoryLimit(bytes, error); +} + +#endif /* CN1_REALTIME_IMPL_H */ diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index 4cd33a985e4..bba61e5dd4f 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -605,6 +605,8 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File // The kernel bus and pin access the Linux port and the backend both wrap // (com.codename1.impl.KernelHardware). Only their wrappers include it. copyRuntimeResource(srcRoot, "cn1_hardware_impl.h"); + // The scheduling half of com.codename1.realtime, wrapped the same way. + copyRuntimeResource(srcRoot, "cn1_realtime_impl.h"); // The multicast DNS socket behind com.codename1.impl.MdnsEngine, wrapped the same way. copyRuntimeResource(srcRoot, "cn1_mdns_impl.h"); emitBundledSqlite(srcRoot); @@ -618,6 +620,7 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File copyRuntimeResource(srcRoot, "cn1_win_compat.c"); Parser.writeOutput(srcRoot); + enableRealtimeIfPresent(cn1Globals); File javaIoFileHeader = new File(srcRoot, "java_io_File.h"); if (javaIoFileHeader.exists()) { @@ -1010,12 +1013,15 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File // The kernel bus and pin access the Linux port and the backend both wrap // (com.codename1.impl.KernelHardware). Only their wrappers include it. copyRuntimeResource(srcRoot, "cn1_hardware_impl.h"); + // The scheduling half of com.codename1.realtime, wrapped the same way. + copyRuntimeResource(srcRoot, "cn1_realtime_impl.h"); // The multicast DNS socket behind com.codename1.impl.MdnsEngine, wrapped the same way. copyRuntimeResource(srcRoot, "cn1_mdns_impl.h"); emitBundledSqlite(srcRoot); emitCryptoKernels(srcRoot, true); Parser.writeOutput(srcRoot); + enableRealtimeIfPresent(cn1Globals); File templateInfoPlist = new File(srcRoot, appName + "-Info.plist"); copy(ByteCodeTranslator.class.getResourceAsStream(templateRoot + "/template/template-Info.plist"), new FileOutputStream(templateInfoPlist)); @@ -2001,6 +2007,19 @@ private static boolean isBuildMetadata(File f) { * aborts a cross-stack longjmp under _FORTIFY_SOURCE and musl has no * makecontext, so neither portable route survives every target we ship. */ + /// Turns on CN1_REALTIME when the class that runs real-time tasks survived + /// translation, so the scheduler and the clocks cost nothing in an application + /// that has no use for them. Called after Parser.writeOutput, which is what prunes + /// the class list. + private static void enableRealtimeIfPresent(File cn1Globals) throws IOException { + for (ByteCodeClass cls : Parser.getClasses()) { + if ("com_codename1_realtime_RealtimeThread".equals(cls.getClsName())) { + replaceInFile(cn1Globals, "//#define CN1_REALTIME", "#define CN1_REALTIME"); + return; + } + } + } + private static void emitVirtualThreadRuntime(File srcRoot) throws IOException { String[] names = { "cn1_virtual_thread.h", "cn1_virtual_thread.c", "cn1_virtual_thread_asm.S" }; for (String name : names) { diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java index 0c78cfdaa60..3788b68eb93 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java @@ -190,6 +190,18 @@ public boolean isBarebone() { return barebone; } + private boolean allocationAllowed; + + /// True when the method carries `@com.codename1.realtime.AllocationAllowed`, which + /// exempts it from RealtimeAllocationVerifier. + public boolean isAllocationAllowed() { + return allocationAllowed; + } + + public void setAllocationAllowed(boolean allocationAllowed) { + this.allocationAllowed = allocationAllowed; + } + public boolean isDisableDebugInfo() { return disableDebugInfo; } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java index 4b015e95b47..d7ba87f340d 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java @@ -54,6 +54,7 @@ public class Parser extends ClassVisitor { private static final String DISABLE_NULL_AND_ARRAY_BOUNDS_CHECKS_ANNOTATION = "Lcom/codename1/annotations/DisableNullChecksAndArrayBoundsChecks;"; private static final String CONCRETE_ANNOTATION = "Lcom/codename1/annotations/Concrete;"; + private static final String ALLOCATION_ALLOWED_ANNOTATION = "Lcom/codename1/realtime/AllocationAllowed;"; private static final String STACK_ALLOCATE_ANNOTATION = "Lcom/codename1/annotations/StackAllocate;"; private static final String FUSED_ANNOTATION = "Lcom/codename1/annotations/Fused;"; private ByteCodeClass cls; @@ -1495,6 +1496,9 @@ public static void writeOutput(File outputDirectory) throws Exception { // Until computeInstantiated runs below, every class counts as instantiated. ByteCodeClass.resetInstantiated(); ByteCodeClass.computePureClinits(classes); + // Real-time tasks must not allocate in steady state. Raw bytecode again, and + // before anything can drop or rewrite the instructions it looks for. + RealtimeAllocationVerifier.run(classes); // Also on the raw bytecode, where every field access is still a plain // GETFIELD/PUTFIELD: see DeadFieldElimination. Not for the JavaScript target // (its own field model), and not under on-device debugging, whose sidecar @@ -3294,6 +3298,8 @@ public AnnotationVisitor visitAnnotation(String desc, boolean visible) { mtd.setDisableDebugInfo(true); } else if (DISABLE_NULL_AND_ARRAY_BOUNDS_CHECKS_ANNOTATION.equals(desc)) { mtd.setDisableNullAndArrayBoundsChecks(true); + } else if (ALLOCATION_ALLOWED_ANNOTATION.equals(desc)) { + mtd.setAllocationAllowed(true); } if (mv == null) return null; return new AnnotationVisitorWrapper(super.visitAnnotation(desc, visible)); diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/RealtimeAllocationVerifier.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/RealtimeAllocationVerifier.java new file mode 100644 index 00000000000..bfe0d4f82b2 --- /dev/null +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/RealtimeAllocationVerifier.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.tools.translator; + +import com.codename1.tools.translator.bytecodes.Instruction; +import com.codename1.tools.translator.bytecodes.Invoke; +import com.codename1.tools.translator.bytecodes.LineNumber; +import com.codename1.tools.translator.bytecodes.MultiArray; +import com.codename1.tools.translator.bytecodes.TypeInstruction; +import com.codename1.tools.translator.classfile.Opcodes; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/// Proves that the steady state of a real-time task never allocates. +/// +/// A `com.codename1.realtime.RealtimeTask` runs `cycle()` and `periodsMissed(int)` +/// on a thread that must not wait for the garbage collector, so neither may allocate +/// on the heap. The roots are those two methods in every subclass of the task class. +/// From each root the pass follows only calls whose target is certain: static and +/// special calls, and virtual or interface calls when the whole program holds exactly +/// one non-abstract method of that name and descriptor, so dispatch can only land +/// there. A call it cannot resolve is not followed, which keeps the check free of +/// false positives at the price of missing an allocation behind a polymorphic call. +/// +/// In every method reached it reports `NEW`, the array allocations and the boxing +/// `valueOf` methods. Not reported: a `NEW` of a `Throwable`, because a throw ends the +/// steady state and the runtime handles it, and ``. A method annotated +/// `@com.codename1.realtime.AllocationAllowed` is neither walked nor reported at its +/// call sites. Runs on the raw bytecode, before any pass rewrites an instruction. +/// With no subclass of the task class in the program it does nothing. +final class RealtimeAllocationVerifier { + private static final String TASK_CLASS = "com_codename1_realtime_RealtimeTask"; + private static final String THROWABLE = "java_lang_Throwable"; + private static final String[] BOXES = {"java/lang/Integer", "java/lang/Long", "java/lang/Short", + "java/lang/Byte", "java/lang/Character", "java/lang/Boolean", "java/lang/Float", + "java/lang/Double"}; + + private RealtimeAllocationVerifier() { + } + + /// Checks the program. + /// + /// @param classes every parsed class + /// @throws NativeSignatureVerifier.VerificationFailedException listing every allocation found + static void run(List classes) throws NativeSignatureVerifier.VerificationFailedException { + List roots = new ArrayList(); + for (ByteCodeClass cls : classes) { + if (!isTaskSubclass(cls)) { + continue; + } + for (BytecodeMethod m : cls.getMethods()) { + if (!m.isAbstract() && !m.isNative() && !m.isStatic() + && (("cycle".equals(m.getMethodName()) && "()V".equals(m.getDesc())) + || ("periodsMissed".equals(m.getMethodName()) && "(I)V".equals(m.getDesc())))) { + roots.add(m); + } + } + } + if (roots.isEmpty()) { + return; + } + Map> concrete = new HashMap>(); + for (ByteCodeClass cls : classes) { + for (BytecodeMethod m : cls.getMethods()) { + if (!m.isAbstract()) { + String key = m.getMethodName() + m.getDesc(); + List l = concrete.get(key); + if (l == null) { + l = new ArrayList(1); + concrete.put(key, l); + } + l.add(m); + } + } + } + Map parent = new HashMap(); + List queue = new ArrayList(); + for (BytecodeMethod r : roots) { + if (!r.isAllocationAllowed()) { + parent.put(r, null); + queue.add(r); + } + } + List findings = new ArrayList(); + for (int q = 0; q < queue.size(); q++) { + BytecodeMethod m = queue.get(q); + List ins = m.getInstructions(); + if (ins == null || "__CLINIT__".equals(m.getMethodName())) { + continue; + } + int line = -1; + for (Instruction i : ins) { + if (i instanceof LineNumber) { + line = ((LineNumber) i).getLine(); + continue; + } + String what = allocation(i); + if (what != null) { + findings.add(describe(m, line, what, parent)); + continue; + } + if (i instanceof Invoke) { + BytecodeMethod target = resolve((Invoke) i, concrete); + if (target != null && !target.isAllocationAllowed() && !parent.containsKey(target)) { + parent.put(target, m); + queue.add(target); + } + } + } + } + if (!findings.isEmpty()) { + StringBuilder b = new StringBuilder(); + b.append("Real-time tasks must not allocate in cycle() or periodsMissed(int); ") + .append(findings.size()).append(" allocation(s) found. Remove them, or annotate the ") + .append("method with @com.codename1.realtime.AllocationAllowed:"); + for (String f : findings) { + b.append("\n ").append(f); + } + throw new NativeSignatureVerifier.VerificationFailedException(b.toString()); + } + } + + private static boolean isTaskSubclass(ByteCodeClass cls) { + Set seen = new HashSet(); + ByteCodeClass c = base(cls); + while (c != null && seen.add(c.getClsName())) { + if (TASK_CLASS.equals(c.getClsName())) { + return true; + } + c = base(c); + } + return false; + } + + private static ByteCodeClass base(ByteCodeClass c) { + String b = c.getBaseClass(); + return b == null ? null : Parser.getClassObject(Util.mangle(b)); + } + + private static boolean isThrowable(String type) { + Set seen = new HashSet(); + ByteCodeClass c = Parser.getClassObject(Util.mangle(type)); + while (c != null && seen.add(c.getClsName())) { + if (THROWABLE.equals(c.getClsName()) || THROWABLE.equals(Util.mangle(c.getBaseClass()))) { + return true; + } + c = base(c); + } + return false; + } + + /// Describes the allocation an instruction makes, or null. + private static String allocation(Instruction i) { + int op = i.getOpcode(); + if (i instanceof TypeInstruction) { + String type = ((TypeInstruction) i).getTypeName(); + if (op == Opcodes.NEW) { + return isThrowable(type) ? null : "new " + type.replace('/', '.'); + } + if (op == Opcodes.ANEWARRAY) { + return "new " + type.replace('/', '.') + "[]"; + } + return null; + } + if (i instanceof MultiArray) { + return "multi-dimensional array " + ((MultiArray) i).getDesc(); + } + if (op == Opcodes.NEWARRAY) { + return "new primitive array"; + } + if (i instanceof Invoke && op == Opcodes.INVOKESTATIC) { + Invoke inv = (Invoke) i; + if ("valueOf".equals(inv.getName()) && !inv.getDesc().startsWith("(Ljava/lang/String;")) { + for (String box : BOXES) { + if (box.equals(inv.getOwner())) { + return "boxing " + box.substring(10) + ".valueOf"; + } + } + } + } + return null; + } + + /// The one method a call can land on, or null when that is not certain. + private static BytecodeMethod resolve(Invoke inv, Map> concrete) { + int op = inv.getOpcode(); + String name = "".equals(inv.getName()) ? "__INIT__" : inv.getName(); + if (op == Opcodes.INVOKESTATIC || op == Opcodes.INVOKESPECIAL) { + Set seen = new HashSet(); + ByteCodeClass c = Parser.getClassObject(Util.mangle(inv.getOwner())); + while (c != null && seen.add(c.getClsName())) { + for (BytecodeMethod m : c.getMethods()) { + if (name.equals(m.getMethodName()) && inv.getDesc().equals(m.getDesc())) { + return m.isAbstract() ? null : m; + } + } + c = base(c); + } + return null; + } + if (op == Opcodes.INVOKEVIRTUAL || op == Opcodes.INVOKEINTERFACE) { + List l = concrete.get(name + inv.getDesc()); + return l != null && l.size() == 1 ? l.get(0) : null; + } + return null; + } + + private static String describe(BytecodeMethod m, int line, String what, + Map parent) { + StringBuilder b = new StringBuilder(); + b.append(what).append(" in ").append(label(m)); + if (m.getSourceFile() != null) { + b.append(" (").append(m.getSourceFile()); + if (line >= 0) { + b.append(':').append(line); + } + b.append(')'); + } + List chain = new ArrayList(); + for (BytecodeMethod p = parent.get(m); p != null; p = parent.get(p)) { + chain.add(label(p)); + } + if (!chain.isEmpty()) { + b.append(", called from "); + for (int k = 0; k < chain.size(); k++) { + if (k > 0) { + b.append(" <- "); + } + b.append(chain.get(k)); + } + } + return b.toString(); + } + + private static String label(BytecodeMethod m) { + String n = "__INIT__".equals(m.getMethodName()) ? "" : m.getMethodName(); + return m.getClsName().replace('_', '.') + "." + n + m.getDesc(); + } +} diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index 88606bcbc8f..6fac285dea9 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -2800,11 +2800,33 @@ JAVA_LONG currentThreadId() { return i->threadId; } +// Priority inheritance for the VM's mutexes and every Java monitor, in an application +// with real-time threads on Linux (CN1_REALTIME, switched on by the translator). A +// real-time thread blocked on a lock that a normal thread holds then lends it its +// priority, instead of waiting behind every thread that preempts the holder -- the +// priority inversion a SCHED_FIFO thread otherwise meets on its first contended +// monitor. glibc takes an uncontended PI mutex in user space like any other, so it +// costs nothing until two threads actually meet on it. +static void cn1InitVmMutex(pthread_mutex_t* m) { +#if defined(CN1_REALTIME) && defined(__linux__) + pthread_mutexattr_t attr; + if(pthread_mutexattr_init(&attr) == 0) { + if(pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT) == 0 + && pthread_mutex_init(m, &attr) == 0) { + pthread_mutexattr_destroy(&attr); + return; + } + pthread_mutexattr_destroy(&attr); + } +#endif + pthread_mutex_init(m, NULL); +} + pthread_mutex_t* criticalSection = NULL; pthread_mutex_t* getCriticalSection() { if(criticalSection == NULL) { criticalSection = malloc(sizeof(pthread_mutex_t)); - pthread_mutex_init(criticalSection, NULL); + cn1InitVmMutex(criticalSection); } return criticalSection; } @@ -2820,7 +2842,7 @@ void unlockCriticalSection() { pthread_mutex_t* getThreadHeapMutex() { if(threadHeapMutex == NULL) { threadHeapMutex = malloc(sizeof(pthread_mutex_t)); - pthread_mutex_init(threadHeapMutex, NULL); + cn1InitVmMutex(threadHeapMutex); } return threadHeapMutex; } @@ -2847,6 +2869,9 @@ JAVA_VOID java_lang_System_gcLight__(CODENAME_ONE_THREAD_STATE) { double cn1GcProbeMarkMs = 0, cn1GcProbeSweepMs = 0; int cn1GcProbeThrew = 0; #endif +#if defined(__linux__) +static uint64_t cn1LinuxResidentBytes(void); +#endif JAVA_VOID java_lang_System_gcMarkSweep__(CODENAME_ONE_THREAD_STATE) { // FREEZE: refuse to start a cycle at all once the exit census has claimed the // heap. Clearing System.gcShouldLoop is not sufficient on its own -- the GC @@ -2950,6 +2975,13 @@ JAVA_VOID java_lang_System_gcMarkSweep__(CODENAME_ONE_THREAD_STATE) { codenameOneGCSweep(); #endif threadStateData->tryBlockOffset = __gcSavedTryBlock; +#if defined(__linux__) + // The memory limit of com.codename1.realtime, measured when the heap is as + // small as it gets. Nothing is read unless a limit was set. + if(cn1RtMemoryLimitSet()) { + cn1RtAfterCycle((JAVA_LONG)cn1LinuxResidentBytes()); + } +#endif } else { threadStateData->tryBlockOffset = __gcSavedTryBlock; threadStateData->exception = JAVA_NULL; @@ -3092,7 +3124,7 @@ JAVA_VOID monitorEnter(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT obj) { if(!data) { data = malloc(sizeof(struct CN1ThreadData)); memset(data, 0, sizeof(struct CN1ThreadData)); - pthread_mutex_init(&data->__codenameOneMutex, NULL); + cn1InitVmMutex(&data->__codenameOneMutex); pthread_cond_init(&data->__codenameOneCondition, NULL); // cn1MonitorDataSet also flags a BiBOP slot's page (see there). cn1MonitorDataSet(obj, data); @@ -3969,6 +4001,11 @@ static uint64_t cn1LinuxResidentBytes(void) { } return (uint64_t)resident * (uint64_t)ps; } + +// The memory limit of com.codename1.realtime reads this from the allocation slow path. +long long cn1RtResidentBytes(void) { + return (long long)cn1LinuxResidentBytes(); +} #endif JAVA_LONG java_lang_Runtime_totalMemoryImpl___R_long(CODENAME_ONE_THREAD_STATE) { diff --git a/vm/backend/build.sh b/vm/backend/build.sh index b721a50133b..795a9e3647b 100755 --- a/vm/backend/build.sh +++ b/vm/backend/build.sh @@ -81,7 +81,7 @@ fi RUNTIME_RESOURCES="$WORK/runtime-resources" mkdir -p "$RUNTIME_RESOURCES" for f in cn1_globals.h cn1_globals.m nativeMethods.m cn1_intrinsics.h cn1_sqlite3.c \ - cn1_process.h cn1_process_vm.h cn1_hardware_impl.h cn1_mdns_impl.h \ + cn1_process.h cn1_process_vm.h cn1_hardware_impl.h cn1_realtime_impl.h cn1_mdns_impl.h \ cn1_virtual_thread.h cn1_virtual_thread.c cn1_virtual_thread_asm.S; do cp "$REPO/vm/ByteCodeTranslator/src/$f" "$RUNTIME_RESOURCES/$f" done diff --git a/vm/backend/demo/rtcheck/com/demo/RtCheck.java b/vm/backend/demo/rtcheck/com/demo/RtCheck.java new file mode 100644 index 00000000000..9d904c80910 --- /dev/null +++ b/vm/backend/demo/rtcheck/com/demo/RtCheck.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.demo; + +import com.codename1.backend.BoardRealtime; +import com.codename1.realtime.Realtime; +import com.codename1.realtime.RealtimeAllocationError; +import com.codename1.realtime.RealtimeParameters; +import com.codename1.realtime.RealtimeStatistics; +import com.codename1.realtime.RealtimeTask; +import com.codename1.realtime.RealtimeThread; +import java.util.ArrayList; + +/// The real-time check, translated and run natively. +/// +/// 1. A 1 kHz loop runs while the main thread allocates hard enough to keep the +/// collector busy for the whole run. Its release latency is the result: with the +/// collector unable to stop it, the worst case is the scheduler's, not the GC's. +/// 2. A loop that allocates through an interface call -- a path the build cannot +/// resolve, so it compiles -- must end with a RealtimeAllocationError naming the +/// class, at run time. +/// 3. (run first) With a memory limit set, a thread that keeps allocating must get an +/// OutOfMemoryError rather than stall (Linux only: the limit reads the process's +/// resident memory). +/// +/// Arguments: `seconds maxLatencyMicros`; a latency bound of 0 reports without +/// judging, for a machine whose kernel is not PREEMPT_RT. +public final class RtCheck { + private RtCheck() { + } + + /// The steady loop: a fixed amount of arithmetic over arrays made in setup(). + static final class Steady extends RealtimeTask { + private double[] state; + private long checksum; + + @Override + public void setup() { + state = new double[256]; + for (int i = 0; i < state.length; i++) { + state[i] = i; + } + } + + @Override + public void cycle() { + double acc = 0; + for (int i = 0; i < state.length; i++) { + state[i] = state[i] * 0.999 + 0.001 * i; + acc += state[i]; + } + checksum += (long) acc; + } + } + + interface Sample { + Object take(); + } + + // Not Integer.valueOf: boxed ints are immediates on this VM and never allocate. + static final class Fresh implements Sample { + public Object take() { + return new long[4]; + } + } + + static final class Cached implements Sample { + private final Object value = new Object(); + + public Object take() { + return value; + } + } + + /// Allocates on its second cycle, through a call with two possible targets. + static final class Violator extends RealtimeTask { + private final Sample[] samples = {new Cached(), new Fresh()}; + private int cycles; + volatile Throwable failure; + + @Override + public void cycle() { + Object o = samples[cycles++ & 1].take(); + if (o == null) { + cycles = 0; + } + } + + @Override + public void failed(Throwable t) { + failure = t; + } + } + + public static void main(String[] args) throws Exception { + int seconds = args.length > 0 ? Integer.parseInt(args[0]) : 5; + long maxLatencyMicros = args.length > 1 ? Long.parseLong(args[1]) : 0; + BoardRealtime.install(); + Realtime rt = Realtime.getInstance(); + boolean linux = rt.isSupported(Realtime.FEATURE_SCHEDULING); + System.out.println("RTCHECK features scheduling=" + linux + + " preemptRt=" + rt.isSupported(Realtime.FEATURE_PREEMPT_RT_KERNEL) + + " gcIsolation=" + rt.isSupported(Realtime.FEATURE_GC_ISOLATION) + + " allocationCheck=" + rt.isSupported(Realtime.FEATURE_ALLOCATION_CHECK) + + " memoryLocked=" + rt.lockMemory()); + boolean ok = true; + + // 3. memory limit -- first, while the process is small: the limit counts + // resident memory, and after step 1 the heap still holds pages it has not + // given back yet. + if (linux) { + // 512MB: far above this program at rest, far below the 4GB it then holds. + rt.setMemoryLimit(512L * 1024 * 1024); + ArrayList hoard = new ArrayList(); + String outcome = "no error after 4GB"; + try { + for (int i = 0; i < 4096; i++) { + // Written to: an untouched array is zero pages the kernel has not + // backed yet, and costs no resident memory at all. + byte[] b = new byte[1024 * 1024]; + for (int j = 0; j < b.length; j += 4096) { + b[j] = 1; + } + hoard.add(b); + if ((i & 15) == 0) { + System.gc(); + } + } + } catch (OutOfMemoryError e) { + outcome = "OutOfMemoryError after " + hoard.size() + "MB"; + } + int hoardedMb = hoard.size(); + hoard = null; + rt.setMemoryLimit(0); + System.out.println("RTCHECK memory limit: " + outcome); + if (outcome.startsWith("no error") || hoardedMb < 128) { + ok = false; + } + } + // 1. latency under collector load + RealtimeThread steady = rt.start("steady", + new RealtimeParameters().setPeriodNanos(1000000L).setPriority(80), new Steady()); + long end = System.currentTimeMillis() + seconds * 1000L; + long churned = 0; + ArrayList keep = new ArrayList(); + while (System.currentTimeMillis() < end) { + for (int i = 0; i < 2000; i++) { + keep.add(new StringBuilder().append("garbage ").append(i).toString()); + } + churned += keep.size(); + if (keep.size() > 200000) { + keep = new ArrayList(); + } + } + steady.stop(); + steady.join(5000); + RealtimeStatistics s = steady.getStatistics(); + System.out.println("RTCHECK steady scheduling=" + steady.isSchedulingApplied() + + " churned=" + churned + " " + s); + long expected = seconds * 1000L; + if (s.getCycles() + s.getMissedPeriods() < expected * 9 / 10) { + System.out.println("RTCHECK FAIL steady ran " + s.getCycles() + " cycles, expected about " + expected); + ok = false; + } + if (s.getAllocationViolations() != 0 || steady.getState() != RealtimeThread.STATE_STOPPED) { + System.out.println("RTCHECK FAIL steady state=" + steady.getState() + " failure=" + steady.getFailure()); + ok = false; + } + if (maxLatencyMicros > 0 && s.getMaxLatencyNanos() > maxLatencyMicros * 1000L) { + System.out.println("RTCHECK FAIL max latency " + s.getMaxLatencyNanos() / 1000 + + "us over the bound of " + maxLatencyMicros + "us"); + ok = false; + } + + // 2. allocation the build could not see + if (rt.isSupported(Realtime.FEATURE_ALLOCATION_CHECK)) { + Violator v = new Violator(); + RealtimeThread vt = rt.start("violator", + new RealtimeParameters().setPeriodNanos(1000000L).setPriority(70), v); + vt.join(5000); + vt.stop(); + vt.join(1000); + Throwable f = vt.getFailure(); + System.out.println("RTCHECK violator state=" + vt.getState() + " failure=" + f); + if (!(f instanceof RealtimeAllocationError) || f.getMessage().indexOf("long") < 0) { + System.out.println("RTCHECK FAIL the allocation in the cycle was not refused"); + ok = false; + } + } + + System.out.println(ok ? "RTCHECK OK" : "RTCHECK FAILED"); + System.exit(ok ? 0 : 1); + } +} diff --git a/vm/backend/impl/javase/com/codename1/backend/BoardRealtime.java b/vm/backend/impl/javase/com/codename1/backend/BoardRealtime.java new file mode 100644 index 00000000000..a56beaf4008 --- /dev/null +++ b/vm/backend/impl/javase/com/codename1/backend/BoardRealtime.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.backend; + +/// The Java SE twin of the backend's real-time support. A JVM has no way to make a +/// thread `SCHED_FIFO` or keep its collector off one, so nothing is installed here and +/// `Realtime.getInstance()` answers the best-effort Java default: a server developed on +/// a laptop runs its real-time tasks as ordinary periodic threads. +public final class BoardRealtime { + private BoardRealtime() { + } + + /// Does nothing on the JVM; the translated binary installs the native + /// implementation here. + public static void install() { + } +} diff --git a/vm/backend/impl/parparvm/com/codename1/backend/BoardRealtime.java b/vm/backend/impl/parparvm/com/codename1/backend/BoardRealtime.java new file mode 100644 index 00000000000..1ad5b0287be --- /dev/null +++ b/vm/backend/impl/parparvm/com/codename1/backend/BoardRealtime.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.backend; + +import com.codename1.realtime.Realtime; +import com.codename1.realtime.RealtimeAllocationError; +import com.codename1.realtime.RealtimeThread; + +/// The headless backend's [Realtime]: the same scheduler calls as the Linux port, +/// through the same C (`cn1_realtime_impl.h`, wrapped in `cn1_backend_realtime.c`), so +/// a gateway can run a control loop beside its REST API in one binary. Off Linux the +/// collector isolation and the allocation check still apply; scheduling and memory +/// locking answer `-ENOSYS`. Every primitive acts on the calling thread. +public final class BoardRealtime extends Realtime { + private static boolean installed; + private int features = -1; + + private BoardRealtime() { + } + + /// Makes this the instance `Realtime.getInstance()` returns. The server calls it + /// as it starts; calling it again does nothing. + public static void install() { + if (!installed) { + installed = true; + Realtime.install(new BoardRealtime()); + } + } + + @Override + protected int getSupportedFeatures() { + if (features < 0) { + features = nFeatures(); + } + return features; + } + + @Override + protected int rawEnter(RealtimeThread thread, int priority, int cpu, boolean isolate) { + return nEnter(priority, cpu, isolate); + } + + @Override + protected void rawLeave(RealtimeThread thread) { + nLeave(); + } + + @Override + protected void rawStartPeriod(RealtimeThread thread, long periodNanos) { + nStartPeriod(periodNanos); + } + + @Override + protected int rawWaitNextPeriod(RealtimeThread thread) { + return nWaitNextPeriod(); + } + + @Override + protected void rawFreeze(RealtimeThread thread, int budget, RealtimeAllocationError error) { + nFreeze(budget, error); + } + + @Override + protected void rawThaw(RealtimeThread thread) { + nThaw(); + } + + @Override + protected void rawStatistics(RealtimeThread thread, long[] out) { + nStatistics(out); + } + + @Override + protected String rawLastViolation(RealtimeThread thread) { + return nLastViolation(); + } + + @Override + protected int rawLockMemory() { + return nLockMemory(); + } + + @Override + protected void rawSetMemoryLimit(long bytes, OutOfMemoryError error) { + nSetMemoryLimit(bytes, error); + } + + private static native int nFeatures(); + + private static native int nEnter(int priority, int cpu, boolean isolate); + + private static native void nLeave(); + + private static native void nStartPeriod(long periodNanos); + + private static native int nWaitNextPeriod(); + + private static native void nFreeze(int budget, Object error); + + private static native void nThaw(); + + private static native int nStatistics(long[] out); + + private static native String nLastViolation(); + + private static native int nLockMemory(); + + private static native void nSetMemoryLimit(long bytes, Object error); +} diff --git a/vm/backend/native/cn1_backend_realtime.c b/vm/backend/native/cn1_backend_realtime.c new file mode 100644 index 00000000000..db15a79aeea --- /dev/null +++ b/vm/backend/native/cn1_backend_realtime.c @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The headless backend's real-time natives (com.codename1.backend.BoardRealtime). Each + * forwards to the shared body in cn1_realtime_impl.h, the one the Linux port uses. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include "cn1_realtime_impl.h" + +JAVA_INT com_codename1_backend_BoardRealtime_nFeatures___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1RtImpl_rtFeatures(threadStateData); +} + +JAVA_INT com_codename1_backend_BoardRealtime_nEnter___int_int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT priority, JAVA_INT cpu, JAVA_BOOLEAN isolate) { + return cn1RtImpl_rtEnter(threadStateData, priority, cpu, isolate); +} + +JAVA_VOID com_codename1_backend_BoardRealtime_nLeave__(CODENAME_ONE_THREAD_STATE) { + cn1RtImpl_rtLeave(threadStateData); +} + +JAVA_VOID com_codename1_backend_BoardRealtime_nStartPeriod___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG periodNanos) { + cn1RtImpl_rtStartPeriod(threadStateData, periodNanos); +} + +JAVA_INT com_codename1_backend_BoardRealtime_nWaitNextPeriod___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1RtImpl_rtWaitNextPeriod(threadStateData); +} + +JAVA_VOID com_codename1_backend_BoardRealtime_nFreeze___int_java_lang_Object(CODENAME_ONE_THREAD_STATE, JAVA_INT budget, JAVA_OBJECT error) { + cn1RtImpl_rtFreeze(threadStateData, budget, error); +} + +JAVA_VOID com_codename1_backend_BoardRealtime_nThaw__(CODENAME_ONE_THREAD_STATE) { + cn1RtImpl_rtThaw(threadStateData); +} + +JAVA_INT com_codename1_backend_BoardRealtime_nStatistics___long_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT out) { + return cn1RtImpl_rtStatistics(threadStateData, out); +} + +JAVA_OBJECT com_codename1_backend_BoardRealtime_nLastViolation___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return cn1RtImpl_rtLastViolation(threadStateData); +} + +JAVA_INT com_codename1_backend_BoardRealtime_nLockMemory___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1RtImpl_rtLockMemory(threadStateData); +} + +JAVA_VOID com_codename1_backend_BoardRealtime_nSetMemoryLimit___long_java_lang_Object(CODENAME_ONE_THREAD_STATE, JAVA_LONG bytes, JAVA_OBJECT error) { + cn1RtImpl_rtSetMemoryLimit(threadStateData, bytes, error); +} diff --git a/vm/backend/src/com/codename1/backend/Backend.java b/vm/backend/src/com/codename1/backend/Backend.java index 47139875e1d..cc3b17b3851 100644 --- a/vm/backend/src/com/codename1/backend/Backend.java +++ b/vm/backend/src/com/codename1/backend/Backend.java @@ -1802,6 +1802,7 @@ public Backend start() throws Exception { claimProcess(); // Before the application's started() callback, which may open a pin. BoardHardware.install(); + BoardRealtime.install(); Backend running = null; try { running = startOnce(); diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/RealtimeAllocationVerifierTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/RealtimeAllocationVerifierTest.java new file mode 100644 index 00000000000..93306446960 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/RealtimeAllocationVerifierTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.tools.translator; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// A real-time task's `cycle()` and `periodsMissed(int)` must not allocate; the +/// fixture defines its own `RealtimeTask` and `AllocationAllowed` because the real +/// ones are not on the translator's class path. +class RealtimeAllocationVerifierTest { + @TempDir + Path directory; + + @BeforeEach + @AfterEach + void cleanParser() { + Parser.cleanup(); + } + + private static final String[] STUBS = { + "package com.codename1.realtime; public abstract class RealtimeTask {" + + " public void setup() {} public abstract void cycle();" + + " public void periodsMissed(int count) {} public void failed(Throwable t) {} }", + "package com.codename1.realtime; @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS)" + + " public @interface AllocationAllowed {}", + }; + + private String verify(String body) throws Exception { + StringBuilder src = new StringBuilder("import com.codename1.realtime.*;\n" + + "class Boom extends Throwable {}\n" + + "public class Task extends RealtimeTask {\n int n;\n").append(body).append("\n}\n"); + String[] sources = {STUBS[0], STUBS[1], src.toString()}; + String[] names = {"RealtimeTask", "AllocationAllowed", "Task"}; + java.util.List args = new java.util.ArrayList(); + args.add("-source"); + args.add("8"); + args.add("-target"); + args.add("8"); + args.add("-d"); + args.add(directory.toString()); + for (int i = 0; i < sources.length; i++) { + Path f = directory.resolve("src").resolve(names[i] + ".java"); + Files.createDirectories(f.getParent()); + Files.write(f, sources[i].getBytes(StandardCharsets.UTF_8)); + args.add(f.toString()); + } + assertTrue(javax.tools.ToolProvider.getSystemJavaCompiler().run(null, null, null, + args.toArray(new String[0])) == 0); + for (String cls : new String[]{"com/codename1/realtime/RealtimeTask", + "com/codename1/realtime/AllocationAllowed", "Boom", "Task"}) { + Parser.parse(new File(directory.toFile(), cls + ".class")); + } + try { + RealtimeAllocationVerifier.run(Parser.getClasses()); + return null; + } catch (NativeSignatureVerifier.VerificationFailedException ex) { + return ex.getMessage(); + } + } + + @Test + void cleanCyclePasses() throws Exception { + assertDoesNotThrow(() -> assertTrue(verify("public void cycle() { n++; }") == null)); + } + + @Test + void newObjectFails() throws Exception { + String m = verify("public void cycle() { Object o = new Object(); }"); + assertTrue(m != null && m.contains("new java.lang.Object") && m.contains("Task.cycle()V"), m); + } + + @Test + void boxingAndArraysFail() throws Exception { + String m = verify("public void cycle() { Integer i = Integer.valueOf(n); }"); + assertTrue(m != null && m.contains("boxing Integer.valueOf"), m); + Parser.cleanup(); + m = verify("public void cycle() { byte[] b = new byte[4]; }"); + assertTrue(m != null && m.contains("primitive array"), m); + } + + @Test + void periodsMissedIsARootToo() throws Exception { + String m = verify("public void cycle() {} public void periodsMissed(int c) { Object o = new int[c][2]; }"); + assertTrue(m != null && m.contains("periodsMissed"), m); + } + + @Test + void allocationTwoCallsDeepNamesTheChain() throws Exception { + String m = verify("public void cycle() { a(); }" + + " static void a() { b(); } static void b() { Object o = new Object(); }"); + assertTrue(m != null && m.contains("Task.b()V") && m.contains("called from Task.a()V <- Task.cycle()V"), m); + assertTrue(m.contains("Task.java:"), m); + } + + @Test + void throwingPasses() throws Exception { + assertTrue(verify("public void cycle() { try { if (n < 0) { throw new Boom(); } } catch (Boom b) { n = 0; } }") == null); + } + + @Test + void allocationAllowedSuppresses() throws Exception { + assertTrue(verify("public void cycle() { a(); }" + + " @AllocationAllowed static void a() { Object o = new Object(); }") == null); + } +} From 112c40837b738467de21823e17f9c5cb98675e2a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:33:33 +0200 Subject: [PATCH 02/12] Real-time: enforce budgets above zero, give isolated cores back, guide sections Red-team round 1: an allocation inside a non-zero budget refilled the budget, so only a budget of 0 was enforced (rtcheck now checks budget 2). Isolation now returns the core to the moved threads when the real-time thread ends; a virtual thread resumed from a trapped carrier gets the real page array; the RSS sample counter no longer drops bytes. Guide: parameters, life cycle, features. Co-Authored-By: Claude Opus 5.5 --- docs/developer-guide/Realtime.asciidoc | 60 +++++++++++++++++++ vm/ByteCodeTranslator/src/cn1_globals.m | 33 +++++++--- vm/ByteCodeTranslator/src/cn1_realtime_impl.h | 34 ++++++++++- vm/ByteCodeTranslator/src/nativeMethods.m | 5 +- vm/backend/demo/rtcheck/com/demo/RtCheck.java | 47 +++++++++++++++ 5 files changed, 168 insertions(+), 11 deletions(-) diff --git a/docs/developer-guide/Realtime.asciidoc b/docs/developer-guide/Realtime.asciidoc index 2711d50f36d..066c60af266 100644 --- a/docs/developer-guide/Realtime.asciidoc +++ b/docs/developer-guide/Realtime.asciidoc @@ -61,6 +61,66 @@ RealtimeThread t = Realtime.getInstance().start("motor", allocation violations. Check them on the target board under load: they are the only real evidence that the loop meets its deadline. +=== Parameters + +[cols="2,1,4"] +|=== +| Setter | Default | Meaning + +| `setPeriodNanos(long)` | 1 ms | The period. At least 10 microseconds. +| `setPriority(int)` | 50 | The `SCHED_FIFO` priority, 1 to 99. Keep it below the + kernel's own interrupt threads (50 on most PREEMPT_RT systems) unless the loop + matters more than the devices it reads. +| `setCpu(int)` | -1 | The core to pin the thread to; -1 leaves it unpinned. +| `setIsolateCpu(boolean)` | true | With a CPU given, move the process's other threads + off that core. +| `setAllocationBudget(int)` | 0 | Allocations a cycle may make before the thread + fails. +| `setSchedulingRequired(boolean)` | false | Fail the thread when real-time + scheduling cannot be applied, instead of running it normally. +|=== + +=== Life cycle + +A thread goes through `RealtimeThread.STATE_STARTING`, `STATE_RUNNING` and ends in +`STATE_STOPPED` or `STATE_FAILED`; `getState()` reads it. + +* `setup()` runs once, on the thread, before the first period. +* `cycle()` runs once per period, and `periodsMissed(n)` before it when `n` + releases were missed. The loop then continues from the current time; missed + periods are not run late in a burst. +* `stop()` asks the loop to end after the current cycle, and `join(millis)` waits + for it. `stopped()` runs on the thread last, after a stop and after a failure + alike, and may allocate. +* An exception or error from the task, including `RealtimeAllocationError`, ends + the thread in `STATE_FAILED`. `failed(Throwable)` receives it on the thread, and + `getFailure()` returns it afterwards. The default `failed` prints the stack trace. + +`isSchedulingApplied()` and `isCpuPinned()` say what the platform actually granted. +Check them at start-up and log them: a loop that silently runs at normal priority +looks fine on the bench and misses deadlines in the field. + +`getStatistics()` returns a snapshot. `getLatencyPercentileNanos(0.9999)` reads a +percentile off the histogram, as an upper bound of its power-of-two bucket. + +=== What the platform supports + +`Realtime.getInstance().isSupported(feature)` answers for each part separately: + +[cols="2,4"] +|=== +| Feature | Meaning + +| `FEATURE_SCHEDULING` | `SCHED_FIFO` priorities can be requested. +| `FEATURE_CPU_AFFINITY` | Threads can be pinned to a core. +| `FEATURE_MEMORY_LOCK` | `lockMemory()` can lock the process into RAM. +| `FEATURE_GC_ISOLATION` | The collector does not stop real-time threads. +| `FEATURE_ALLOCATION_CHECK` | Every allocation in a cycle is caught at run time. + Without it only allocations that need a new memory page are caught, and the + build-time check is the guarantee. +| `FEATURE_PREEMPT_RT_KERNEL` | The kernel is PREEMPT_RT. +|=== + === The rules a cycle follows *A cycle allocates nothing.* In steady state a real-time loop must not create diff --git a/vm/ByteCodeTranslator/src/cn1_globals.m b/vm/ByteCodeTranslator/src/cn1_globals.m index 06c33a80267..1f66b6dc751 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.m +++ b/vm/ByteCodeTranslator/src/cn1_globals.m @@ -21405,13 +21405,12 @@ void cn1RtLeave(CODENAME_ONE_THREAD_STATE) { } } -void cn1RtFreeze(CODENAME_ONE_THREAD_STATE, JAVA_INT budget) { +// The trap itself, without touching the budget: cn1RtTrappedAlloc lifts and restores it +// around an allocation inside the budget, and must not refill the budget doing so. +static void cn1RtTrapOn(CODENAME_ONE_THREAD_STATE) { if(threadStateData->rtAllocTrap) { - threadStateData->rtAllocBudgetLeft = threadStateData->rtAllocBudget; return; } - threadStateData->rtAllocBudget = budget < 0 ? 0 : budget; - threadStateData->rtAllocBudgetLeft = threadStateData->rtAllocBudget; #ifdef CN1_VIRTUAL_THREADS threadStateData->rtSavedBibopCur = threadStateData->bibopCur; threadStateData->bibopCur = cn1RtNoPages; @@ -21419,7 +21418,7 @@ void cn1RtFreeze(CODENAME_ONE_THREAD_STATE, JAVA_INT budget) { threadStateData->rtAllocTrap = JAVA_TRUE; } -void cn1RtThaw(CODENAME_ONE_THREAD_STATE) { +static void cn1RtTrapOff(CODENAME_ONE_THREAD_STATE) { if(!threadStateData->rtAllocTrap) { return; } @@ -21432,6 +21431,17 @@ void cn1RtThaw(CODENAME_ONE_THREAD_STATE) { #endif } +// Enters the steady state with a full budget of `budget` allocations per period. +void cn1RtFreeze(CODENAME_ONE_THREAD_STATE, JAVA_INT budget) { + threadStateData->rtAllocBudget = budget < 0 ? 0 : budget; + threadStateData->rtAllocBudgetLeft = threadStateData->rtAllocBudget; + cn1RtTrapOn(threadStateData); +} + +void cn1RtThaw(CODENAME_ONE_THREAD_STATE) { + cn1RtTrapOff(threadStateData); +} + // Starts a new period's allocation budget without leaving the steady state. void cn1RtNewPeriod(CODENAME_ONE_THREAD_STATE) { threadStateData->rtAllocBudgetLeft = threadStateData->rtAllocBudget; @@ -21450,9 +21460,9 @@ JAVA_OBJECT cn1RtTrappedAlloc(CODENAME_ONE_THREAD_STATE, int size, struct clazz* // array back in place for the call, and trap again afterwards. if(threadStateData->rtAllocBudgetLeft > 0) { threadStateData->rtAllocBudgetLeft--; - cn1RtThaw(threadStateData); + cn1RtTrapOff(threadStateData); o = codenameOneGcMalloc(threadStateData, size, parent); - cn1RtFreeze(threadStateData, threadStateData->rtAllocBudget); + cn1RtTrapOn(threadStateData); return o; } // Over it. The steady state is over: thaw first, because building the error's stack @@ -21538,10 +21548,15 @@ static void cn1RtAccount(int size) { if(step < 1024 * 1024) { step = 1024 * 1024; } - if(__atomic_add_fetch(&cn1RtBytesSinceCheck, (long long)size, __ATOMIC_RELAXED) < step) { + long long seen = __atomic_add_fetch(&cn1RtBytesSinceCheck, (long long)size, __ATOMIC_RELAXED); + if(seen < step) { + return; + } + // One thread takes the step (bytes added meanwhile stay counted); a racer reads on. + if(!__atomic_compare_exchange_n(&cn1RtBytesSinceCheck, &seen, seen - step, 0, + __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { return; } - __atomic_store_n(&cn1RtBytesSinceCheck, 0, __ATOMIC_RELAXED); long long rss = cn1RtResidentBytes(); if(rss <= 0) { return; diff --git a/vm/ByteCodeTranslator/src/cn1_realtime_impl.h b/vm/ByteCodeTranslator/src/cn1_realtime_impl.h index 0a12507415f..0c1c2339b3b 100644 --- a/vm/ByteCodeTranslator/src/cn1_realtime_impl.h +++ b/vm/ByteCodeTranslator/src/cn1_realtime_impl.h @@ -112,6 +112,14 @@ static int cn1RtKernelIsPreemptRt(void) { return c == '1'; } +/* The threads cn1RtIsolateCpu moved, so cn1RtRestoreCpu gives the core back to exactly + * those when the real-time thread ends -- otherwise the process would keep one core + * fewer for good. Only the real-time thread that isolated touches its own list. */ +#define CN1_RT_MAX_MOVED 512 +static __thread pid_t cn1RtMoved[CN1_RT_MAX_MOVED]; +static __thread int cn1RtMovedCount = 0; +static __thread int cn1RtIsolatedCpu = -1; + /* Takes `cpu` away from every other thread of the process, so the real-time thread has * it to itself. Threads started later inherit their creator's mask, and a real-time * thread never creates one, so they stay off it too. Best effort: a thread that exits @@ -138,15 +146,38 @@ static int cn1RtIsolateCpu(int cpu) { CPU_CLR(cpu, &set); if (sched_setaffinity(tid, sizeof(set), &set) == 0) { moved++; + if (cn1RtMovedCount < CN1_RT_MAX_MOVED) { + cn1RtMoved[cn1RtMovedCount++] = tid; + } } } } closedir(d); + cn1RtIsolatedCpu = cpu; return moved; } +/* Gives the isolated core back to the threads that had it. A thread that has exited, + * or whose id was reused, fails or gains a core it may use anyway: best effort. */ +static void cn1RtRestoreCpu(void) { + int i; + if (cn1RtIsolatedCpu < 0) { + return; + } + for (i = 0; i < cn1RtMovedCount; i++) { + cpu_set_t set; + if (sched_getaffinity(cn1RtMoved[i], sizeof(set), &set) == 0) { + CPU_SET(cn1RtIsolatedCpu, &set); + sched_setaffinity(cn1RtMoved[i], sizeof(set), &set); + } + } + cn1RtMovedCount = 0; + cn1RtIsolatedCpu = -1; +} + /* Touches the stack the thread will run on, so a page fault never lands in a period. - * With mlockall(MCL_FUTURE) the pages then stay. */ + * With mlockall(MCL_FUTURE) the pages then stay. 128KB is well inside the + * CN1_THREAD_STACK_BYTES (16MB) every VM thread is created with. */ static void cn1RtPrefaultStack(void) { volatile char buf[128 * 1024]; size_t i; @@ -253,6 +284,7 @@ static JAVA_VOID cn1RtImpl_rtLeave(CODENAME_ONE_THREAD_STATE) { pthread_setschedparam(pthread_self(), cn1RtOldPolicy, &cn1RtOldParam); cn1RtOldPolicy = -1; } + cn1RtRestoreCpu(); #endif } diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index 6fac285dea9..e0abcac16e7 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -2768,7 +2768,10 @@ void cn1RetireVirtualThread(struct cn1VirtualThread* vt) { void cn1ResumeVirtualThreadOn(CODENAME_ONE_THREAD_STATE, struct cn1VirtualThread* vt) { struct ThreadLocalData* state = (struct ThreadLocalData*)cn1VirtualThreadState(vt); if(state != 0) { - state->bibopCur = threadStateData->bibopCur; + // A real-time carrier in its steady state has swapped in an empty page array + // (cn1RtFreeze); the virtual thread is not trapped and gets the real one. + state->bibopCur = (threadStateData->rtAllocTrap && threadStateData->rtSavedBibopCur != 0) + ? threadStateData->rtSavedBibopCur : threadStateData->bibopCur; } cn1VirtualThreadResume(vt); } diff --git a/vm/backend/demo/rtcheck/com/demo/RtCheck.java b/vm/backend/demo/rtcheck/com/demo/RtCheck.java index 9d904c80910..2bcc6b76cf8 100644 --- a/vm/backend/demo/rtcheck/com/demo/RtCheck.java +++ b/vm/backend/demo/rtcheck/com/demo/RtCheck.java @@ -112,6 +112,32 @@ public void failed(Throwable t) { } } + /// Allocates `perCycle` arrays a cycle, through an interface so the build cannot + /// see it, against a budget of 2. + static final class Budgeted extends RealtimeTask { + private final Sample[] samples = {new Cached(), new Fresh()}; + private final int perCycle; + private int cycles; + + Budgeted(int perCycle) { + this.perCycle = perCycle; + } + + @Override + public void cycle() { + for (int i = 0; i < perCycle; i++) { + samples[1].take(); + } + if (samples[cycles++ & 1].take() == null) { + cycles = 0; + } + } + + @Override + public void failed(Throwable t) { + } + } + public static void main(String[] args) throws Exception { int seconds = args.length > 0 ? Integer.parseInt(args[0]) : 5; long maxLatencyMicros = args.length > 1 ? Long.parseLong(args[1]) : 0; @@ -208,6 +234,27 @@ public static void main(String[] args) throws Exception { } } + // 2b. a budget above zero is a budget, not a pass + if (rt.isSupported(Realtime.FEATURE_ALLOCATION_CHECK)) { + for (int per = 1; per <= 2; per++) { + RealtimeThread bt = rt.start("budget" + per, new RealtimeParameters() + .setPeriodNanos(1000000L).setPriority(70).setAllocationBudget(2), + new Budgeted(per)); + Thread.sleep(300); + bt.stop(); + bt.join(2000); + // per=1: one array plus, every other cycle, one more -- 2 at most, within. + // per=2: 3 on every other cycle -- over the budget. + boolean failed = bt.getFailure() instanceof RealtimeAllocationError; + System.out.println("RTCHECK budget 2, " + (per + 1) + " allocations at most: " + + (failed ? "refused" : "allowed") + " after " + bt.getStatistics().getCycles() + " cycles"); + if (failed != (per == 2)) { + System.out.println("RTCHECK FAIL the allocation budget was not applied"); + ok = false; + } + } + } + System.out.println(ok ? "RTCHECK OK" : "RTCHECK FAILED"); System.exit(ok ? 0 : 1); } From 6bb6e8ec9cd917aae717c6278bd14d71ef2c812e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:38:40 +0200 Subject: [PATCH 03/12] Real-time: a cycle that catches its refusal stays checked; guide covers statistics and porting Red-team round 2: the over-budget path lifted the trap to build the error and nothing re-armed it, so a cycle catching RealtimeAllocationError allocated unchecked from then on. The trap is re-armed every period (rtcheck: catcher). Co-Authored-By: Claude Opus 5.5 --- docs/developer-guide/Realtime.asciidoc | 26 ++++++++++++++-- vm/ByteCodeTranslator/src/cn1_globals.h | 1 + vm/ByteCodeTranslator/src/cn1_globals.m | 9 +++++- vm/backend/demo/rtcheck/com/demo/RtCheck.java | 31 +++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/docs/developer-guide/Realtime.asciidoc b/docs/developer-guide/Realtime.asciidoc index 066c60af266..90b6f05d738 100644 --- a/docs/developer-guide/Realtime.asciidoc +++ b/docs/developer-guide/Realtime.asciidoc @@ -100,8 +100,19 @@ A thread goes through `RealtimeThread.STATE_STARTING`, `STATE_RUNNING` and ends Check them at start-up and log them: a loop that silently runs at normal priority looks fine on the bench and misses deadlines in the field. -`getStatistics()` returns a snapshot. `getLatencyPercentileNanos(0.9999)` reads a -percentile off the histogram, as an upper bound of its power-of-two bucket. +`getStatistics()` returns a `RealtimeStatistics` snapshot: + +* `getCycles()` and `getMissedPeriods()`. +* `getMaxLatencyNanos()` and `getMeanLatencyNanos()`: how late each release woke + the thread, against its scheduled time. +* `getMaxExecutionNanos()` and `getMeanExecutionNanos()`: how long `cycle()` took. +* `getAllocationViolations()`. +* `getLatencyHistogram()`: `HISTOGRAM_BUCKETS` (16) counts. Index `i` counts + releases less than 2^i microseconds late. The last bucket holds everything from + about 16 milliseconds up. +* `getLatencyPercentileNanos(0.9999)`: a percentile read off the histogram, as the + upper bound of its bucket. +* `toString()`: all of the above on one line, for a log. === What the platform supports @@ -137,6 +148,10 @@ be asked to wait for it. Codename One checks this twice: `RealtimeAllocationError` naming the class. The thread's `failed()` method receives it. `setAllocationBudget(n)` allows up to `n` allocations per period for code that cannot avoid them yet. ++ +Catching the error inside `cycle()` does not turn the check off. The allocation +that failed is counted, and from the next period on the check applies again, so +each later allocation fails the same way. *The collector does not stop a real-time thread.* An ordinary thread pauses briefly while the collector reads its stack. A real-time thread is released @@ -224,3 +239,10 @@ thread allocates hard enough to keep the collector busy all the time. It then checks that a cycle allocating through an interface is stopped, and that the memory limit throws. Run it on your board to see the release latency the hardware gives you before you rely on a deadline. + +=== Porting + +A port supplies its implementation by returning a `Realtime` subclass from +`CodenameOneImplementation.getRealtime()`; `Display` installs it with +`Realtime.install()` when it starts, and the backend installs its own. An +application never calls `install()`. diff --git a/vm/ByteCodeTranslator/src/cn1_globals.h b/vm/ByteCodeTranslator/src/cn1_globals.h index 909d69a862d..8987d52a21f 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.h +++ b/vm/ByteCodeTranslator/src/cn1_globals.h @@ -2189,6 +2189,7 @@ struct ThreadLocalData { // cn1RtFreeze in cn1_globals.m. JAVA_BOOLEAN realtimeThread; JAVA_BOOLEAN rtAllocTrap; + JAVA_BOOLEAN rtFrozen; // in the steady state (cn1RtFreeze), trap lifted or not JAVA_INT rtAllocBudget; JAVA_INT rtAllocBudgetLeft; JAVA_LONG rtAllocViolations; diff --git a/vm/ByteCodeTranslator/src/cn1_globals.m b/vm/ByteCodeTranslator/src/cn1_globals.m index 1f66b6dc751..cd0a55f05d3 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.m +++ b/vm/ByteCodeTranslator/src/cn1_globals.m @@ -21435,16 +21435,23 @@ static void cn1RtTrapOff(CODENAME_ONE_THREAD_STATE) { void cn1RtFreeze(CODENAME_ONE_THREAD_STATE, JAVA_INT budget) { threadStateData->rtAllocBudget = budget < 0 ? 0 : budget; threadStateData->rtAllocBudgetLeft = threadStateData->rtAllocBudget; + threadStateData->rtFrozen = JAVA_TRUE; cn1RtTrapOn(threadStateData); } void cn1RtThaw(CODENAME_ONE_THREAD_STATE) { + threadStateData->rtFrozen = JAVA_FALSE; cn1RtTrapOff(threadStateData); } // Starts a new period's allocation budget without leaving the steady state. +// Re-arms the trap too: the over-budget path lifts it to build the error, and a cycle +// that catches the error itself would otherwise allocate unchecked from then on. void cn1RtNewPeriod(CODENAME_ONE_THREAD_STATE) { threadStateData->rtAllocBudgetLeft = threadStateData->rtAllocBudget; + if(threadStateData->rtFrozen) { + cn1RtTrapOn(threadStateData); + } } // The class of the allocation that ended the last steady state, for the error's message. @@ -21471,7 +21478,7 @@ JAVA_OBJECT cn1RtTrappedAlloc(CODENAME_ONE_THREAD_STATE, int size, struct clazz* // was just refused. threadStateData->rtAllocViolations++; cn1RtViolationClass = parent != 0 ? parent->clsName : "array"; - cn1RtThaw(threadStateData); + cn1RtTrapOff(threadStateData); if(threadStateData->rtAllocError != JAVA_NULL) { throwException(threadStateData, threadStateData->rtAllocError); } diff --git a/vm/backend/demo/rtcheck/com/demo/RtCheck.java b/vm/backend/demo/rtcheck/com/demo/RtCheck.java index 2bcc6b76cf8..34695805429 100644 --- a/vm/backend/demo/rtcheck/com/demo/RtCheck.java +++ b/vm/backend/demo/rtcheck/com/demo/RtCheck.java @@ -138,6 +138,22 @@ public void failed(Throwable t) { } } + /// Catches the refusal itself and carries on allocating: every period must be + /// refused again, not just the first. + static final class Catcher extends RealtimeTask { + private final Sample[] samples = {new Cached(), new Fresh()}; + private int caught; + + @Override + public void cycle() { + try { + samples[1].take(); + } catch (RealtimeAllocationError e) { + caught++; + } + } + } + public static void main(String[] args) throws Exception { int seconds = args.length > 0 ? Integer.parseInt(args[0]) : 5; long maxLatencyMicros = args.length > 1 ? Long.parseLong(args[1]) : 0; @@ -255,6 +271,21 @@ public static void main(String[] args) throws Exception { } } + // 2c. a cycle that catches the refusal stays checked + if (rt.isSupported(Realtime.FEATURE_ALLOCATION_CHECK)) { + RealtimeThread ct = rt.start("catcher", new RealtimeParameters() + .setPeriodNanos(1000000L).setPriority(70), new Catcher()); + Thread.sleep(300); + ct.stop(); + ct.join(2000); + long v = ct.getStatistics().getAllocationViolations(); + System.out.println("RTCHECK catcher violations=" + v + " cycles=" + ct.getStatistics().getCycles()); + if (v < 100) { + System.out.println("RTCHECK FAIL a caught refusal turned the check off"); + ok = false; + } + } + System.out.println(ok ? "RTCHECK OK" : "RTCHECK FAILED"); System.exit(ok ? 0 : 1); } From 9202261272ef8e2ef6649ac3ad768f2d83a5d4a9 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:39:26 +0200 Subject: [PATCH 04/12] Real-time guide: execution time is wake-up to the next wait Co-Authored-By: Claude Opus 5.5 --- docs/developer-guide/Realtime.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/developer-guide/Realtime.asciidoc b/docs/developer-guide/Realtime.asciidoc index 90b6f05d738..1062be5b35a 100644 --- a/docs/developer-guide/Realtime.asciidoc +++ b/docs/developer-guide/Realtime.asciidoc @@ -105,7 +105,8 @@ looks fine on the bench and misses deadlines in the field. * `getCycles()` and `getMissedPeriods()`. * `getMaxLatencyNanos()` and `getMeanLatencyNanos()`: how late each release woke the thread, against its scheduled time. -* `getMaxExecutionNanos()` and `getMeanExecutionNanos()`: how long `cycle()` took. +* `getMaxExecutionNanos()` and `getMeanExecutionNanos()`: the time from each wake-up + to the next wait -- `cycle()`, plus `periodsMissed()` when it ran. * `getAllocationViolations()`. * `getLatencyHistogram()`: `HISTOGRAM_BUCKETS` (16) counts. Index `i` counts releases less than 2^i microseconds late. The last bucket holds everything from From fd2495087c06cc8d03432d36f1a613faf5225786 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:16:23 +0200 Subject: [PATCH 05/12] Embedded tooling: build, run and follow a board app from Maven, Gradle and IDEs - cn1:buildLinuxLocal / buildLinuxLocal, cn1:runOnDevice / cn1RunOnDevice and cn1:device-logs / cn1DeviceLogs; -Dcn1.board names the board and turns on the embedded build. - The executable survives the builder's temp dir (linux/target/linux-native). - A cross build with no cross compiler fails instead of using the host cc. - The device refuses a binary for another architecture before installing it; the service gets LimitRTPRIO/LimitMEMLOCK for real-time threads. - Archetype: build.sh/build.bat commands and IntelliJ, VS Code, NetBeans and Eclipse entries for Linux Local Build, Run on Device and Device Logs. - CI: the real-time step installs the backend; the guide's motor loop is an included, compiled snippet. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/embedded-linux.yml | 2 +- .../generated/RealtimeMotorLoopSnippet.java | 79 ++++++++++++++++ .../developer-guide/embedded-linux.sh | 5 + docs/developer-guide/Embedded-Linux.asciidoc | 46 +++++++++- docs/developer-guide/Realtime.asciidoc | 59 ++++++------ .../builders/LinuxNativeBuilder.java | 10 ++ .../java/com/codename1/maven/AppBuilder.java | 34 ++++++- .../com/codename1/maven/DeviceDeployer.java | 87 ++++++++++++++++-- .../codename1/maven/DeviceDeployerTest.java | 39 ++++++++ .../runConfigurations/CN1_Device_Logs.xml | 38 ++++++++ .../CN1_Linux_Local_Build.xml | 39 ++++++++ .../runConfigurations/CN1_Run_on_Device.xml | 40 ++++++++ .../archetype-resources/.vscode/settings.json | 12 +++ .../resources/archetype-resources/build.bat | 26 ++++++ .../resources/archetype-resources/build.sh | 22 +++++ .../archetype-resources/mvnconfig.toml | 15 +++ .../eclipse/__mainName__ - Device Logs.launch | 28 ++++++ .../__mainName__ - Linux Local Build.launch | 28 ++++++ .../__mainName__ - Run on Device.launch | 29 ++++++ .../tools/netbeans/nbactions.xml | 31 +++++++ .../java/com/codename1/gradle/AppSupport.java | 41 +++++++++ .../com/codename1/gradle/ToolSupport.java | 11 +++ .../gradle/tasks/DeployDeviceTask.java | 18 +++- .../gradle/tasks/DeviceLogsTask.java | 64 +++++++++++++ .../com/codename1/maven/DeployDeviceMojo.java | 10 +- .../com/codename1/maven/DeviceLogsMojo.java | 62 +++++++++++++ .../com/codename1/maven/RunOnDeviceMojo.java | 92 +++++++++++++++++++ .../AbstractBuildWrapperMojo.java | 18 ++++ .../buildWrappers/BuildLinuxLocalMojo.java | 47 ++++++++++ 29 files changed, 983 insertions(+), 49 deletions(-) create mode 100644 docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/RealtimeMotorLoopSnippet.java create mode 100644 maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Device_Logs.xml create mode 100644 maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Linux_Local_Build.xml create mode 100644 maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Run_on_Device.xml create mode 100644 maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Device Logs.launch create mode 100644 maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Linux Local Build.launch create mode 100644 maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Run on Device.launch create mode 100644 maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeviceLogsMojo.java create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RunOnDeviceMojo.java create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/BuildLinuxLocalMojo.java diff --git a/.github/workflows/embedded-linux.yml b/.github/workflows/embedded-linux.yml index a85c3fbbc73..c183dab6684 100644 --- a/.github/workflows/embedded-linux.yml +++ b/.github/workflows/embedded-linux.yml @@ -72,7 +72,7 @@ jobs: RETRY_DELAY_SECONDS: '60' run: | cd maven - bash ../scripts/ci/retry.sh mvn -B -pl core,linux -am install \ + bash ../scripts/ci/retry.sh mvn -B -pl core,linux,backend,codenameone-maven-plugin -am install \ -DskipTests -Dmaven.javadoc.skip=true -Plocal-dev-javase \ -Dspotbugs.skip=true -Dpmd.skip=true -Dcheckstyle.skip=true \ -Dmaven.repo.local="$GITHUB_WORKSPACE/.m2-repo" diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/RealtimeMotorLoopSnippet.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/RealtimeMotorLoopSnippet.java new file mode 100644 index 00000000000..6245dc3bfaf --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/RealtimeMotorLoopSnippet.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.snippets.generated; + +import com.codename1.hardware.Hardware; +import com.codename1.hardware.HardwareException; +import com.codename1.hardware.PwmChannel; +import com.codename1.realtime.Realtime; +import com.codename1.realtime.RealtimeParameters; +import com.codename1.realtime.RealtimeTask; +import com.codename1.realtime.RealtimeThread; + +class RealtimeMotorLoopSnippet { + static final double[] GAINS = new double[64]; + + // tag::realtime-motor-loop[] + class MotorLoop extends RealtimeTask { + private final double[] samples = new double[64]; // <1> + private PwmChannel pwm; + private boolean failed; + + public void setup() { // <2> + try { + pwm = Hardware.getInstance().openPwm(0, 0); + } catch (HardwareException e) { + failed = true; + } + } + + public void cycle() { // <3> + if (failed) { + return; + } + double out = 0; + for (int i = 0; i < samples.length; i++) { + out += samples[i] * GAINS[i]; + } + try { + pwm.setDutyCycle(out); + } catch (HardwareException e) { + failed = true; // no allocation on this path + } + } + + public void periodsMissed(int count) { // <4> + // the loop overran; hold the output, count it, alarm if it persists + } + } + + RealtimeThread startLoop() { + return Realtime.getInstance().start("motor", + new RealtimeParameters() + .setPeriodNanos(1000000) // 1 kHz + .setPriority(80) // SCHED_FIFO 1..99 + .setCpu(3), // pin to core 3 + new MotorLoop()); + } + // end::realtime-motor-loop[] +} diff --git a/docs/demos/common/src/main/snippets/developer-guide/embedded-linux.sh b/docs/demos/common/src/main/snippets/developer-guide/embedded-linux.sh index 00cb8e068c6..e628d69325a 100644 --- a/docs/demos/common/src/main/snippets/developer-guide/embedded-linux.sh +++ b/docs/demos/common/src/main/snippets/developer-guide/embedded-linux.sh @@ -4,3 +4,8 @@ mvn cn1:deploy-device -Dcn1.device.host=pi.local -Dcn1.device.user=pi gradle cn1DeployDevice -Pcn1.device.host=pi.local -Pcn1.device.user=pi // end::embedded-linux-bash-001[] + +// tag::embedded-linux-bash-002[] +mvn cn1:runOnDevice -Dcn1.board=rpi5 -Dcn1.device.host=pi.local -Dcn1.device.user=pi +gradle cn1RunOnDevice -Pcn1.board=rpi5 -Pcn1.device.host=pi.local -Pcn1.device.user=pi +// end::embedded-linux-bash-002[] diff --git a/docs/developer-guide/Embedded-Linux.asciidoc b/docs/developer-guide/Embedded-Linux.asciidoc index 9955ccd5a6b..d6d47594c8f 100644 --- a/docs/developer-guide/Embedded-Linux.asciidoc +++ b/docs/developer-guide/Embedded-Linux.asciidoc @@ -105,8 +105,45 @@ Raspberry Pi OS), or the board vendor's equivalents. === Building for a board -Add the `linux.embedded` build hint to a native Linux build and name the board, and -the builder picks the architecture, the display defaults and the libraries to link: +One command builds the application for a board on your machine, with no cloud +build, installs it on the device and shows its log: + +[source,bash] +---- +include::../demos/common/src/main/snippets/developer-guide/embedded-linux.sh[tag=embedded-linux-bash-002,indent=0] +---- + +The same commands are in the project's `build.sh` and `build.bat` +(`./build.sh device`, with `CN1_BOARD` and `CN1_DEVICE_HOST` set). The IDE files a new +project carries -- IntelliJ IDEA, VS Code, NetBeans and Eclipse -- have *Linux Local +Build*, *Run on Device* and *Device Logs* entries beside the cloud build. Edit the board +and the host in each one once. + +[cols="<2,<2,<4",options="header"] +|=== +|Maven goal |Gradle task |What it does + +|`cn1:buildLinuxLocal` |`buildLinuxLocal` |Builds the native executable on this machine into +`linux/target/linux-native`. With `-Dcn1.board=` it is an embedded build for +that board. +|`cn1:runOnDevice` |`cn1RunOnDevice` |`buildLinuxLocal`, then `deploy-device`, then follows the log. +|`cn1:deploy-device` |`cn1DeployDevice` |Installs the last build on the device as a service. +|`cn1:device-logs` |`cn1DeviceLogs` |Follows the log of the installed service, without deploying. +|`cn1:buildLinuxDevice` |`buildLinuxDevice` |The same executable, built by the cloud build service. +|=== + +Gradle takes the same properties with `-P` instead of `-D`. + +A build for a board of another architecture, an arm64 board from an x64 laptop say, +needs a cross compiler. Install https://ziglang.org/download/[zig] and put it on the +`PATH`, or set `CN1_ZIG`. The build then links against an old glibc, so the +executable runs on any current distribution. If no cross compiler is found the build +stops and says so. `linux.cc` names any other cross compiler, and `linux.sysroot` a +board SDK (below). + +`-Dcn1.board` is a shortcut. To make a project always build for its board, set the +`linux.embedded` build hint and name the board in its settings. The builder then picks +the architecture, the display defaults and the libraries to link: [source,properties] ---- @@ -153,7 +190,10 @@ include::../demos/common/src/main/snippets/developer-guide/embedded-linux.sh[tag It uses the system's `ssh`, so keys, agents and `~/.ssh/config` aliases work as they do in a terminal. `cn1.device.env` passes environment variables to the service (`CN1_ROTATION=90,CN1_DISPLAY=fbdev`), and `cn1.device.logs=true` follows its -log after the restart. +log after the restart. `cn1:device-logs` follows it later, without deploying again. + +The service runs as root, with no limit on real-time priority or locked memory, so +<<_real_time_threads,real-time threads>> get their scheduling without further setup. A project's `backend` module can run in the cloud and on a board, and each side usually needs some controllers the other doesn't. The deployed service runs with diff --git a/docs/developer-guide/Realtime.asciidoc b/docs/developer-guide/Realtime.asciidoc index 1062be5b35a..a065a43aaa2 100644 --- a/docs/developer-guide/Realtime.asciidoc +++ b/docs/developer-guide/Realtime.asciidoc @@ -17,38 +17,7 @@ independent safety mechanism, such as a hardware watchdog or an interlock. [source,java] ---- -class MotorLoop extends RealtimeTask { - private final double[] samples = new double[64]; // <1> - private PwmChannel pwm; - private boolean failed; - - public void setup() { // <2> - pwm = Hardware.getInstance().openPwm(0, 0); - } - - public void cycle() { // <3> - double out = 0; - for (int i = 0; i < samples.length; i++) { - out += samples[i] * GAINS[i]; - } - try { - pwm.setDutyCycle(out); - } catch (HardwareException e) { - failed = true; // no allocation on this path - } - } - - public void periodsMissed(int count) { // <4> - // the loop overran; hold the output, count it, alarm if it persists - } -} - -RealtimeThread t = Realtime.getInstance().start("motor", - new RealtimeParameters() - .setPeriodNanos(1000000) // 1 kHz - .setPriority(80) // SCHED_FIFO 1..99 - .setCpu(3), // pin to core 3 - new MotorLoop()); +include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/RealtimeMotorLoopSnippet.java[tag=realtime-motor-loop,indent=0] ---- <1> Everything the loop needs is allocated before it starts. <2> `setup()` runs on the real-time thread before the first period. It may allocate. @@ -206,6 +175,32 @@ of the process is moved off that core. For a hard split, add `isolcpus=` and `nohz_full=` on the kernel command line, and limit the other services with a cgroup `cpu.max`. +=== Building and running on a board + +A real-time application is an ordinary Codename One project. Nothing needs to be +enabled for it: + +* The allocation check runs in every native build that contains a `RealtimeTask`. + That covers a local build, a cloud build and the backend's. +* The runtime parts are in every Linux executable. + +Build it, install it on the board and watch its log with one command, as for any +embedded application (see <<_building_for_a_board,Building for a board>>): + +[source,bash] +---- +include::../demos/common/src/main/snippets/developer-guide/embedded-linux.sh[tag=embedded-linux-bash-002,indent=0] +---- + +The service `deploy-device` installs runs as root, with `LimitRTPRIO` and +`LimitMEMLOCK` raised, so `SCHED_FIFO` and `lockMemory()` work without further setup. +Log the thread's statistics from time to time, for example `getStatistics()` once a +minute from an ordinary thread. They then appear in `cn1:device-logs`. + +A backend with a real-time loop is built for the board with `cn1:backend-package` +(`-Dcn1.backend.target=glibc-arm64` for a 64-bit ARM board). Install it with +`cn1:deploy-device -Dcn1.device.binary=`. + === Where it runs [cols="1,3"] diff --git a/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java index 740505b5870..5d818bfaf4a 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java @@ -612,6 +612,16 @@ private String resolveCCompiler(BuildRequest request, String arch) { if (zigAvailable() || "zig".equalsIgnoreCase(request.getArg("linux.toolchain", ""))) { return writeZigCcWrapper(arch, false).getAbsolutePath(); } + // The host cc only builds for the host. A board of another architecture with + // no cross compiler used to "succeed" here and fail far later, or hand the + // board a binary it cannot run; say what is needed instead. + if (!arch.equals(detectHostArch()) + && request.getArg("linux.sysroot", "").trim().isEmpty()) { + throw new BuildException("Building for " + arch + " on a " + detectHostArch() + + " machine needs a cross compiler, and zig was not found. Install zig " + + "(https://ziglang.org/download/) and put it on the PATH or set CN1_ZIG, " + + "or point linux.cc at a cross compiler, or set linux.sysroot to a board SDK."); + } log("zig not found (set CN1_ZIG or install zig); building with the host 'cc'. " + "The binary will require the build host's glibc version -- install zig for a " + "portable old-glibc binary."); diff --git a/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java b/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java index ff06e6c3ad0..91f4f45a743 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java @@ -2998,6 +2998,7 @@ private void doLinuxNativeLocalBuild(File tmpProjectDir, Properties props, File } } applyHardeningRequestArgs(r); + applyBoardShortcut(r, props); r.setIncludeSource(true); try { @@ -3010,7 +3011,19 @@ private void doLinuxNativeLocalBuild(File tmpProjectDir, Properties props, File throw new BuildExecutionException("Linux native build failed"); } if (e.getLinuxExecutable() != null) { - getLog().info("Built native Linux executable: " + e.getLinuxExecutable().getAbsolutePath()); + // The builder's result directory is a temporary one that cleanup() + // deletes, so the executable is kept where deploy-device looks for it. + File out = new File(host.buildDirectory(), "linux-native"); + out.mkdirs(); + File kept = new File(out, e.getLinuxExecutable().getName()); + try { + java.nio.file.Files.copy(e.getLinuxExecutable().toPath(), kept.toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (java.io.IOException ioe) { + throw new BuildExecutionException("Could not copy the Linux executable to " + out, ioe); + } + kept.setExecutable(true); + getLog().info("Built native Linux executable: " + kept.getAbsolutePath()); } } catch (com.codename1.builders.BuildException hardeningEx) { throw new BuildExecutionException(hardeningEx.getMessage(), hardeningEx); @@ -3025,6 +3038,25 @@ private void doLinuxNativeLocalBuild(File tmpProjectDir, Properties props, File } } + /// `-Dcn1.board=rpi5` is the whole of an embedded build: it turns on + /// `linux.embedded` and names the board, whose profile picks the architecture + /// and the display defaults. An explicit `linux.embedded` hint is left alone. + /// The settings file has no shortcut: there both `linux.embedded=true` and + /// `linux.embedded.board` are written out, as the guide says. + private void applyBoardShortcut(BuildRequest r, Properties props) { + String board = System.getProperty("cn1.board"); + if (board == null || board.trim().isEmpty()) { + board = props.getProperty("cn1.board"); + } + if (board == null || board.trim().isEmpty()) { + return; + } + r.putArgument("linux.embedded.board", board.trim()); + if (r.getArg("linux.embedded", null) == null) { + r.putArgument("linux.embedded", "true"); + } + } + // Local ParparVM-backed JavaScript build target. private void doJavaScriptLocalBuild(File tmpProjectDir, Properties props, File distJar) throws BuildExecutionException { File codenameOneJar = getJar("com.codenameone", "codenameone-core"); diff --git a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java index 3e9ee2347e1..87d627c9265 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java @@ -114,6 +114,19 @@ public DeviceDeployer followLogs(boolean followLogs) { return this; } + /// Follows the log of the service a deploy installed, without deploying: the + /// service is `cn1.device.service`, else the name of `binary`. + public void tailLogs() throws BuildException { + String name = service != null && !service.trim().isEmpty() ? service.trim() + : binary != null ? binary.getName() : null; + if (name == null) { + throw new BuildException("Name the service: set cn1.device.service (or build the app, whose" + + " executable names it)"); + } + String target = checkedTarget(name); + run(ssh(target, true, "sudo journalctl -f -n 100 -u " + name + ".service")); + } + public void deploy() throws BuildException { if (host == null || host.trim().isEmpty()) { throw new BuildException("No device host: set cn1.device.host (an address or an ~/.ssh/config alias)"); @@ -126,6 +139,24 @@ public void deploy() throws BuildException { throw new BuildException(binary + " is not a Linux executable (no ELF header)"); } String name = service != null && !service.trim().isEmpty() ? service.trim() : binary.getName(); + String target = checkedTarget(name); + String remoteBinary = remoteDir + "/" + name; + + log.info("Copying " + binary.getName() + " (" + (binary.length() / 1024) + " KB) to " + target + + " and installing the " + name + " service"); + run(ssh(target, false, installScript(name, remoteBinary, unameFor(binary))), binary); + log.info("Deployed " + remoteBinary + "; it starts on boot and restarts if it exits"); + if (followLogs) { + run(ssh(target, true, "sudo journalctl -f -n 50 -u " + name + ".service")); + } + } + + /// Validates the host, user, service name and remote directory and returns the + /// ssh destination. + private String checkedTarget(String name) throws BuildException { + if (host == null || host.trim().isEmpty()) { + throw new BuildException("No device host: set cn1.device.host (an address or an ~/.ssh/config alias)"); + } // The first character is a letter or digit so that nothing here can reach // ssh, install or systemctl as an option ("-oProxyCommand=..." runs a // local command), and a name cannot be "." or "..". @@ -140,16 +171,7 @@ public void deploy() throws BuildException { throw new BuildException("cn1.device.host and cn1.device.user must start with a letter or digit and use" + " only letters, digits and _.-: (" + h + ")"); } - String target = u.isEmpty() ? h : u + "@" + h; - String remoteBinary = remoteDir + "/" + name; - - log.info("Copying " + binary.getName() + " (" + (binary.length() / 1024) + " KB) to " + target - + " and installing the " + name + " service"); - run(ssh(target, false, installScript(name, remoteBinary)), binary); - log.info("Deployed " + remoteBinary + "; it starts on boot and restarts if it exits"); - if (followLogs) { - run(ssh(target, true, "sudo journalctl -f -n 50 -u " + name + ".service")); - } + return u.isEmpty() ? h : u + "@" + h; } /// The shell run on the device: read the binary from standard input into a @@ -160,6 +182,14 @@ public void deploy() throws BuildException { /// root's; a device whose deploy user has no passwordless sudo fails here /// with sudo's own message. String installScript(String name, String remoteBinary) { + return installScript(name, remoteBinary, null); + } + + /// As above, refusing before anything is installed when the device's + /// `uname -m` does not match `machines` (a `case` pattern, see [#unameFor]). + /// A binary for the wrong architecture would otherwise fail with "Exec format + /// error" and the unit's `Restart=always` would retry it every second. + String installScript(String name, String remoteBinary, String machines) { StringBuilder unit = new StringBuilder(); unit.append("[Unit]\n") .append("Description=Codename One application ").append(name).append('\n') @@ -170,6 +200,10 @@ String installScript(String name, String remoteBinary) { .append("ExecStart=").append(remoteBinary).append('\n') .append("WorkingDirectory=").append(remoteDir).append('\n') .append("Restart=always\nRestartSec=1\n") + // The service runs as root, which may use SCHED_FIFO and lock memory; + // the limits are raised too so a real-time thread + // (com.codename1.realtime) gets them under any User= a site adds. + .append("LimitRTPRIO=infinity\nLimitMEMLOCK=infinity\n") .append("StandardInput=tty\nTTYPath=/dev/tty1\nTTYReset=yes\nTTYVHangup=yes\n") .append("StandardOutput=journal\nStandardError=journal\n"); String pairs = environment; @@ -190,6 +224,9 @@ String installScript(String name, String remoteBinary) { + "d=$(mktemp -d)\n" + "trap 'rm -rf \"$d\"' EXIT\n" + "cat > \"$d/upload\"\n" + + (machines == null ? "" : "case \"$(uname -m)\" in " + machines + ") ;; *) echo \"" + name + + " was built for " + machines + ", but this device is $(uname -m): build with" + + " -Dcn1.board=\" >&2; exit 1;; esac\n") + "sudo mkdir -p -- " + remoteDir + "\n" + "sudo install -m 0755 -- \"$d/upload\" " + remoteBinary + "\n" + "printf '%s' '" + unit.toString().replace("'", "'\\''") + "' > \"$d/unit\"\n" @@ -238,6 +275,36 @@ private void run(List command, File input) throws BuildException { } } + /// The `uname -m` values that can run `f`, as a shell `case` pattern, from its + /// ELF `e_machine`; `null` for an unreadable file or a machine not listed. A + /// 32-bit binary also accepts the 64-bit kernel of its family: Raspberry Pi OS + /// 32-bit runs an aarch64 kernel on a Pi 4 or 5 and runs armhf binaries. + static String unameFor(File f) { + byte[] h = new byte[20]; + try (InputStream in = new FileInputStream(f)) { + if (in.read(h) != 20) { + return null; + } + } catch (IOException ex) { + return null; + } + int m = h[5] == 2 ? ((h[18] & 0xff) << 8) | (h[19] & 0xff) : (h[18] & 0xff) | ((h[19] & 0xff) << 8); + switch (m) { + case 62: + return "x86_64"; + case 183: + return "aarch64|arm64"; + case 40: + return "arm*|aarch64"; + case 243: + return "riscv64"; + case 3: + return "i?86|x86_64"; + default: + return null; + } + } + static boolean isElf(File f) { byte[] magic = new byte[4]; try (InputStream in = new FileInputStream(f)) { diff --git a/maven/build-engine/src/test/java/com/codename1/maven/DeviceDeployerTest.java b/maven/build-engine/src/test/java/com/codename1/maven/DeviceDeployerTest.java index 260637ee846..7e47d2f9b29 100644 --- a/maven/build-engine/src/test/java/com/codename1/maven/DeviceDeployerTest.java +++ b/maven/build-engine/src/test/java/com/codename1/maven/DeviceDeployerTest.java @@ -52,6 +52,25 @@ private File write(String name, byte[] content, boolean executable) throws IOExc return f; } + @Test + void theInstallRefusesADeviceOfAnotherArchitecture() throws IOException { + byte[] arm64 = new byte[64]; + arm64[0] = 0x7f; arm64[1] = 'E'; arm64[2] = 'L'; arm64[3] = 'F'; + arm64[5] = 1; // little endian + arm64[18] = (byte) 183; // EM_AARCH64 + assertEquals("aarch64|arm64", DeviceDeployer.unameFor(write("arm", arm64, true))); + byte[] x64 = arm64.clone(); + x64[18] = 62; + assertEquals("x86_64", DeviceDeployer.unameFor(write("x64", x64, true))); + assertNull(DeviceDeployer.unameFor(write("short", new byte[] {0x7f, 'E'}, true))); + + String script = new DeviceDeployer(new NullLog()).remoteDir("/opt/app") + .installScript("demo", "/opt/app/demo", "x86_64"); + int check = script.indexOf("case \"$(uname -m)\" in x86_64)"); + assertTrue(check > script.indexOf("cat > "), script); + assertTrue(check < script.indexOf("sudo install"), script); + } + @Test void findsTheNewestElfExecutableOnly() throws IOException { byte[] elf = {0x7f, 'E', 'L', 'F', 2, 1, 1}; @@ -94,6 +113,26 @@ void namesThatCouldBeReadAsOptionsAreRefused() throws Exception { .remoteDir("/opt/../etc")::deploy); } + @Test + void theServiceMayUseRealtimeSchedulingAndLockMemory() { + String script = new DeviceDeployer(new NullLog()).remoteDir("/opt/app").installScript("demo", "/opt/app/demo"); + assertTrue(script.contains("LimitRTPRIO=infinity\n"), script); + assertTrue(script.contains("LimitMEMLOCK=infinity\n"), script); + } + + @Test + void followingLogsNeedsAHostAndAServiceAndRefusesOptionLikeNames() { + DeviceDeployer d = new DeviceDeployer(new NullLog()); + BuildException e = assertThrows(BuildException.class, d::tailLogs); + assertTrue(e.getMessage().contains("cn1.device.service"), e.getMessage()); + d.service("demo"); + assertThrows(BuildException.class, d::tailLogs, "no host"); + d.host("-oProxyCommand=touch /tmp/x"); + assertThrows(BuildException.class, d::tailLogs); + d.host("pi.local").service("demo; reboot"); + assertThrows(BuildException.class, d::tailLogs); + } + @Test void theInstallScriptIsValidShellAndCarriesTheEnvironment() throws Exception { String script = new DeviceDeployer(new NullLog()).remoteDir("/opt/app") diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Device_Logs.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Device_Logs.xml new file mode 100644 index 00000000000..1444fbcac0e --- /dev/null +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Device_Logs.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Linux_Local_Build.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Linux_Local_Build.xml new file mode 100644 index 00000000000..7de35733e82 --- /dev/null +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Linux_Local_Build.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Run_on_Device.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Run_on_Device.xml new file mode 100644 index 00000000000..5ae03848cb0 --- /dev/null +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Run_on_Device.xml @@ -0,0 +1,40 @@ + + + + + + + + + diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/.vscode/settings.json b/maven/cn1app-archetype/src/main/resources/archetype-resources/.vscode/settings.json index 11b32105249..a75d12eb1bf 100644 --- a/maven/cn1app-archetype/src/main/resources/archetype-resources/.vscode/settings.json +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/.vscode/settings.json @@ -67,6 +67,18 @@ "alias": "Cloud > Linux Device Build", "command": "\"package\" \"-DskipTests\" \"-Dcodename1.platform=linux\" \"-Dcodename1.buildTarget=linux-device\" \"-U\" \"-e\"" }, + { + "alias": "Local > Linux Local Build", + "command": "\"cn1:buildLinuxLocal\" \"-Dcn1.board=rpi5\" \"-U\" \"-e\"" + }, + { + "alias": "Device > Run on Device", + "command": "\"cn1:runOnDevice\" \"-Dcn1.board=rpi5\" \"-Dcn1.device.host=raspberrypi.local\" \"-U\" \"-e\"" + }, + { + "alias": "Device > Device Logs", + "command": "\"cn1:device-logs\" \"-Dcn1.device.host=raspberrypi.local\" \"-e\"" + }, { "alias": "Local > Gradle Android Project", "command": "\"package\" \"-DskipTests\" \"-Dcodename1.platform=android\" \"-Dcodename1.buildTarget=android-source\" \"-U\" \"-e\"" diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/build.bat b/maven/cn1app-archetype/src/main/resources/archetype-resources/build.bat index c3b2f51a134..a4f64816be3 100755 --- a/maven/cn1app-archetype/src/main/resources/archetype-resources/build.bat +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/build.bat @@ -34,6 +34,26 @@ goto :finish :linux_device call "%MVNW%" package -DskipTests -Dcodename1.platform^=linux -Dcodename1.buildTarget^=linux-device -U -e +goto :finish +:linux_local +set "BOARD_ARG=" +if not "%CN1_BOARD%"=="" set BOARD_ARG="-Dcn1.board=%CN1_BOARD%" +call "%MVNW%" cn1:buildLinuxLocal %BOARD_ARG% -U -e + +goto :finish +:device +set "BOARD_ARG=" +set "HOST_ARG=" +if not "%CN1_BOARD%"=="" set BOARD_ARG="-Dcn1.board=%CN1_BOARD%" +if not "%CN1_DEVICE_HOST%"=="" set HOST_ARG="-Dcn1.device.host=%CN1_DEVICE_HOST%" +call "%MVNW%" cn1:runOnDevice %BOARD_ARG% %HOST_ARG% -U -e + +goto :finish +:device_logs +set "HOST_ARG=" +if not "%CN1_DEVICE_HOST%"=="" set HOST_ARG="-Dcn1.device.host=%CN1_DEVICE_HOST%" +call "%MVNW%" cn1:device-logs %HOST_ARG% -e + goto :finish :javascript call "%MVNW%" package -DskipTests -Dcodename1.platform^=javascript -Dcodename1.buildTarget^=local-javascript -U -e @@ -110,6 +130,12 @@ echo windows_device echo Builds a native Windows app ^(no JVM^). echo linux_device echo Builds a native Linux app ^(ELF^, no JVM^). +echo linux_local +echo Builds a native Linux app locally. CN1_BOARD=rpi5 cross builds for an embedded board. +echo device +echo Builds locally, deploys to CN1_DEVICE_HOST over SSH and follows the log. +echo device_logs +echo Follows the app's log on CN1_DEVICE_HOST. echo javascript_cloud echo Builds the web app using the build server. diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/build.sh b/maven/cn1app-archetype/src/main/resources/archetype-resources/build.sh index 96a70be3ca3..3d56b48f493 100755 --- a/maven/cn1app-archetype/src/main/resources/archetype-resources/build.sh +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/build.sh @@ -23,6 +23,22 @@ function linux_device { "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=linux" "-Dcodename1.buildTarget=linux-device" "-U" "-e" } +function linux_local { + BOARD_ARGS=() + if [ -n "$CN1_BOARD" ]; then BOARD_ARGS=("-Dcn1.board=$CN1_BOARD"); fi + "$MVNW" "cn1:buildLinuxLocal" "${BOARD_ARGS[@]}" "-U" "-e" +} +function device { + DEVICE_ARGS=() + if [ -n "$CN1_BOARD" ]; then DEVICE_ARGS+=("-Dcn1.board=$CN1_BOARD"); fi + if [ -n "$CN1_DEVICE_HOST" ]; then DEVICE_ARGS+=("-Dcn1.device.host=$CN1_DEVICE_HOST"); fi + "$MVNW" "cn1:runOnDevice" "${DEVICE_ARGS[@]}" "-U" "-e" +} +function device_logs { + DEVICE_ARGS=() + if [ -n "$CN1_DEVICE_HOST" ]; then DEVICE_ARGS+=("-Dcn1.device.host=$CN1_DEVICE_HOST"); fi + "$MVNW" "cn1:device-logs" "${DEVICE_ARGS[@]}" "-e" +} function javascript { "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=javascript" "-Dcodename1.buildTarget=local-javascript" "-U" "-e" @@ -98,6 +114,12 @@ function help { "echo" "-e" " Builds a native Windows app (no JVM)." "echo" "-e" " linux_device" "echo" "-e" " Builds a native Linux app (ELF, no JVM)." + "echo" "-e" " linux_local" + "echo" "-e" " Builds a native Linux app locally. CN1_BOARD=rpi5 cross builds for an embedded board." + "echo" "-e" " device" + "echo" "-e" " Builds locally, deploys to CN1_DEVICE_HOST over SSH and follows the log." + "echo" "-e" " device_logs" + "echo" "-e" " Follows the app's log on CN1_DEVICE_HOST." "echo" "-e" " javascript_cloud" "echo" "-e" " Builds the web app using the build server." } diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/mvnconfig.toml b/maven/cn1app-archetype/src/main/resources/archetype-resources/mvnconfig.toml index 5f38c520b12..1fe97129ad8 100644 --- a/maven/cn1app-archetype/src/main/resources/archetype-resources/mvnconfig.toml +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/mvnconfig.toml @@ -58,6 +58,21 @@ command=["$MVNW", "package", "-DskipTests", "-Dcodename1.platform=linux", "-Dcod label="Linux Device Build" group="Cloud" +[linux_local] +command=["$MVNW", "cn1:buildLinuxLocal", "-U", "-e"] +label="Linux Local Build" +group="Local" + +[device] +command=["$MVNW", "cn1:runOnDevice", "-Dcn1.board=rpi5", "-Dcn1.device.host=raspberrypi.local", "-U", "-e"] +label="Run on Device" +run=true + +[device_logs] +command=["$MVNW", "cn1:device-logs", "-Dcn1.device.host=raspberrypi.local", "-e"] +label="Device Logs" +run=true + [xcode] command=["$MVNW", "package", "-DskipTests", "-Dcodename1.platform=ios", "-Dcodename1.buildTarget=ios-source", "-U", "-e"] diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Device Logs.launch b/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Device Logs.launch new file mode 100644 index 00000000000..5ddc8a5fe42 --- /dev/null +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Device Logs.launch @@ -0,0 +1,28 @@ +#set ( $d = "$") + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Linux Local Build.launch b/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Linux Local Build.launch new file mode 100644 index 00000000000..e095cb4ed7f --- /dev/null +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Linux Local Build.launch @@ -0,0 +1,28 @@ +#set ( $d = "$") + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Run on Device.launch b/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Run on Device.launch new file mode 100644 index 00000000000..e9844ff6984 --- /dev/null +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/eclipse/__mainName__ - Run on Device.launch @@ -0,0 +1,29 @@ +#set ( $d = "$") + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/netbeans/nbactions.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/netbeans/nbactions.xml index 50d203375c9..be6f878d873 100644 --- a/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/netbeans/nbactions.xml +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/tools/netbeans/nbactions.xml @@ -35,6 +35,37 @@ debug-simulator + + CUSTOM-Linux Local Build + Linux Local Build + + cn1:buildLinuxLocal + + + rpi5 + + + + CUSTOM-Run on Device + Run on Device + + cn1:runOnDevice + + + rpi5 + raspberrypi.local + + + + CUSTOM-Device Logs + Device Logs + + cn1:device-logs + + + raspberrypi.local + + CUSTOM-Open Control Center Open Control Center diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java index 01d917bdf26..68f5d126509 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java @@ -79,6 +79,12 @@ final class AppSupport { /// `buildJavascriptLocal`, made for a server that is not a servlet container. static final String WEB_APP_BUILD_TASK = "buildJavascriptWebApp"; + /// The local native Linux build, which `cn1RunOnDevice` deploys. + static final String LINUX_LOCAL_BUILD_TASK = "buildLinuxLocal"; + + /// The embedded board of a local Linux build (`-Pcn1.board=rpi5`). + static final String BOARD_PROPERTY = "cn1.board"; + private AppSupport() { } @@ -535,6 +541,15 @@ static void apply(final Project project, final ProjectLayout layout, final Coden registerBuild(project, WEB_APP_BUILD_TASK, "javascript", "local-javascript", "Builds the JavaScript port locally for the backend to host (see backendWebApp)", layout, main, javase, framework, ext, hosted, queue); + // The native Linux executable, built here; -Pcn1.board=rpi5 makes it an + // embedded cross build, as -Dcn1.board does for the Maven goal. Only + // codename1.* properties reach the builder, so the board travels as the + // hints the Maven shortcut sets. + final Provider board = project.getProviders().gradleProperty(BOARD_PROPERTY) + .orElse(project.getProviders().systemProperty(BOARD_PROPERTY)).orElse(""); + registerBuild(project, LINUX_LOCAL_BUILD_TASK, "linux", "local-linux-device", + "Builds the native Linux executable locally (-Pcn1.board=rpi5 for an embedded board)", + layout, main, javase, framework, ext, userProperties.zip(board, (given, b) -> boardProperties(given, b, settingsFile)), queue); // The bundle is found by looking in the build directory, so one an // earlier build left there must not be this build's answer. final Provider bundle = project.getLayout().getBuildDirectory() @@ -1164,6 +1179,32 @@ public Iterable asArguments() { /// same-origin and never proxied anyway -- so the build is told not to wire /// one in, unless the project has chosen for itself: on the command line, in /// `codenameone { buildHints }` or in its settings file. + /// `given` with the hints `cn1.board` stands for: the board, and + /// `linux.embedded` unless the command line or the settings already say. + static Map boardProperties(Map given, String board, File settingsFile) { + if (board == null || board.trim().isEmpty()) { + return given; + } + Map out = new java.util.LinkedHashMap(given); + out.put("codename1.arg.linux.embedded.board", board.trim()); + if (!out.containsKey("codename1.arg.linux.embedded") && !settingsDeclare(settingsFile, "codename1.arg.linux.embedded")) { + out.put("codename1.arg.linux.embedded", "true"); + } + return out; + } + + private static boolean settingsDeclare(File settingsFile, String key) { + java.util.Properties p = new java.util.Properties(); + if (settingsFile.isFile()) { + try (java.io.InputStream in = new java.io.FileInputStream(settingsFile)) { + p.load(in); + } catch (java.io.IOException ignored) { + // An unreadable settings file declares nothing; the build reports it. + } + } + return p.getProperty(key) != null; + } + static Map hostedProperties(Map given, File settingsFile) { Map out = new java.util.LinkedHashMap(given); try { diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/ToolSupport.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/ToolSupport.java index 52196697e6d..1942fbbb765 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/ToolSupport.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/ToolSupport.java @@ -70,6 +70,17 @@ static void register(Project project, ProjectLayout layout, SourceSet main, Code t.setGroup(AppSupport.GROUP); t.setDescription("Copies the native Linux build to a device over SSH and runs it as a service."); }); + project.getTasks().register("cn1RunOnDevice", com.codename1.gradle.tasks.DeployDeviceTask.class, t -> { + t.setGroup(AppSupport.GROUP); + t.setDescription("Builds the native Linux executable locally, deploys it to a device over SSH and" + + " follows its log."); + t.dependsOn(AppSupport.LINUX_LOCAL_BUILD_TASK); + t.setFollowLogsByDefault(true); + }); + project.getTasks().register("cn1DeviceLogs", com.codename1.gradle.tasks.DeviceLogsTask.class, t -> { + t.setGroup(AppSupport.GROUP); + t.setDescription("Follows the log of the app's service on a device."); + }); register(project, layout, ext, userProperties, roots, javaRoot, encoding, "settings", DesktopTool.SETTINGS, "Opens Codename One Settings for this project"); register(project, layout, ext, userProperties, roots, javaRoot, encoding, "guibuilder", diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java index c76a5f04a35..e7a55993492 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java @@ -27,6 +27,7 @@ import com.codename1.maven.DeviceDeployer; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; import org.gradle.work.DisableCachingByDefault; @@ -39,6 +40,20 @@ @DisableCachingByDefault(because = "Deploys to a device") public abstract class DeployDeviceTask extends DefaultTask { + private boolean followLogsByDefault; + + /// Whether the service log is followed after the deploy when + /// `cn1.device.logs` is not given: `cn1RunOnDevice` does, `cn1DeployDevice` + /// does not. + @Internal + public boolean isFollowLogsByDefault() { + return followLogsByDefault; + } + + public void setFollowLogsByDefault(boolean followLogsByDefault) { + this.followLogsByDefault = followLogsByDefault; + } + @TaskAction public void deploy() { File buildDir = getProject().getLayout().getBuildDirectory().get().getAsFile(); @@ -53,7 +68,8 @@ public void deploy() { .remoteDir(dir == null ? "/opt/codenameone" : dir).service(deviceProperty("service")) .environment(deviceProperty("env")) .profile(deviceProperty("profile") == null ? "device" : deviceProperty("profile")) - .followLogs("true".equals(deviceProperty("logs"))).deploy(); + .followLogs(deviceProperty("logs") == null ? followLogsByDefault + : "true".equals(deviceProperty("logs"))).deploy(); } catch (BuildException ex) { throw new GradleException(ex.getMessage(), ex); } catch (NumberFormatException ex) { diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java new file mode 100644 index 00000000000..cb2a4801535 --- /dev/null +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.gradle.tasks; + +import com.codename1.builders.BuildException; +import com.codename1.gradle.GradleLog; +import com.codename1.maven.DeviceDeployer; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.tasks.TaskAction; +import org.gradle.work.DisableCachingByDefault; + +import java.io.File; + +/// Follows the log of the service a deploy installed on a device: +/// `gradle cn1DeviceLogs -Pcn1.device.host=pi.local`. The service is +/// `cn1.device.service`, else the name of the executable a local build left. +/// The same [DeviceDeployer] as the Maven `device-logs` goal. +@DisableCachingByDefault(because = "Reads a device") +public abstract class DeviceLogsTask extends DefaultTask { + + @TaskAction + public void tail() { + File buildDir = getProject().getLayout().getBuildDirectory().get().getAsFile(); + String binary = deviceProperty("binary"); + File bin = binary != null ? getProject().file(binary) + : DeviceDeployer.findBinary(buildDir, new File(buildDir, "linux-native")); + String port = deviceProperty("port"); + try { + new DeviceDeployer(new GradleLog(getLogger())).host(deviceProperty("host")).user(deviceProperty("user")) + .port(port == null ? 22 : Integer.parseInt(port)).binary(bin) + .service(deviceProperty("service")).tailLogs(); + } catch (BuildException ex) { + throw new GradleException(ex.getMessage(), ex); + } catch (NumberFormatException ex) { + throw new GradleException("cn1.device.port is not a number: " + port, ex); + } + } + + private String deviceProperty(String name) { + Object v = getProject().findProperty("cn1.device." + name); + return v == null ? null : v.toString(); + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java index f2adc4c2c00..34dde3c6ba5 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java @@ -81,11 +81,19 @@ public class DeployDeviceMojo extends AbstractMojo { @Parameter(defaultValue = "${project.build.directory}", readonly = true) private File buildDirectory; + /// The newest executable a local Linux build left: `target/linux-native` of this + /// module, or of the `linux` module beside it when the goal runs at the root. + static File findBuiltBinary(File buildDirectory) { + File root = buildDirectory.getParentFile(); + return DeviceDeployer.findBinary(buildDirectory, new File(buildDirectory, "linux-native"), + root == null ? null : new File(root, "linux" + File.separator + "target" + File.separator + "linux-native")); + } + @Override public void execute() throws MojoExecutionException { File bin = binary; if (bin == null && buildDirectory != null) { - bin = DeviceDeployer.findBinary(buildDirectory, new File(buildDirectory, "linux-native")); + bin = findBuiltBinary(buildDirectory); } try { new DeviceDeployer(MavenLog.of(getLog())).host(host).user(user).port(port).binary(bin) diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeviceLogsMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeviceLogsMojo.java new file mode 100644 index 00000000000..9a578fd5a2d --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeviceLogsMojo.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.builders.BuildException; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import java.io.File; + +/** + * Follows the log of the app installed on a device, without redeploying it: + * {@code mvn cn1:device-logs -Dcn1.device.host=pi.local}. The service is + * {@code cn1.device.service}, else the name of the executable the last local + * build produced. + */ +@Mojo(name = "device-logs", requiresProject = false) +public class DeviceLogsMojo extends AbstractMojo { + @Parameter(property = "cn1.device.host") + private String host; + @Parameter(property = "cn1.device.user") + private String user; + @Parameter(property = "cn1.device.port", defaultValue = "22") + private int port; + @Parameter(property = "cn1.device.service") + private String service; + @Parameter(defaultValue = "${project.build.directory}", readonly = true) + private File buildDirectory; + + @Override + public void execute() throws MojoExecutionException { + try { + new DeviceDeployer(MavenLog.of(getLog())).host(host).user(user).port(port).service(service) + .binary(buildDirectory == null ? null : DeployDeviceMojo.findBuiltBinary(buildDirectory)) + .tailLogs(); + } catch (BuildException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RunOnDeviceMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RunOnDeviceMojo.java new file mode 100644 index 00000000000..b37ce46b23c --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RunOnDeviceMojo.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.builders.BuildException; +import com.codename1.maven.buildWrappers.AbstractBuildWrapperMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import java.io.File; + +/** + * Builds the app for a board on this machine, installs it on the device and + * follows its log -- one command from source to a running device: + * {@code mvn cn1:runOnDevice -Dcn1.board=rpi5 -Dcn1.device.host=pi.local}. The + * build is {@code cn1:buildLinuxLocal}, the install is {@code cn1:deploy-device}, + * and every {@code cn1.device.*} option of that goal applies. + */ +@Mojo(name = "runOnDevice", requiresDependencyResolution = ResolutionScope.NONE, + requiresDependencyCollection = ResolutionScope.NONE) +public class RunOnDeviceMojo extends AbstractBuildWrapperMojo { + @Parameter(property = "cn1.device.host") + private String host; + @Parameter(property = "cn1.device.user") + private String user; + @Parameter(property = "cn1.device.port", defaultValue = "22") + private int port; + @Parameter(property = "cn1.device.dir", defaultValue = "/opt/codenameone") + private String remoteDir; + @Parameter(property = "cn1.device.service") + private String service; + @Parameter(property = "cn1.device.env", defaultValue = "") + private String environment; + @Parameter(property = "cn1.device.profile", defaultValue = "device") + private String profile; + /** Follow the log after the restart; on by default here, since this is the run goal. */ + @Parameter(property = "cn1.device.logs", defaultValue = "true") + private boolean logs; + + @Override + protected String getPlatform() { + return "linux"; + } + + @Override + protected String getBuildTarget() { + return "local-linux-device"; + } + + private final long started = System.currentTimeMillis(); + + @Override + protected void afterBuild() throws MojoExecutionException { + File bin = DeployDeviceMojo.findBuiltBinary(new File(getProject().getBuild().getDirectory())); + // The search takes the newest executable it finds, so a build that wrote + // none would deploy an earlier one; two seconds of slack for file systems + // that keep modification times to the second. + if (bin != null && bin.lastModified() < started - 2000) { + throw new MojoExecutionException("The build produced no new executable; " + bin + + " is from an earlier build"); + } + try { + new DeviceDeployer(MavenLog.of(getLog())).host(host).user(user).port(port) + .binary(bin).remoteDir(remoteDir).service(service) + .environment(environment).profile(profile).followLogs(logs).deploy(); + } catch (BuildException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/AbstractBuildWrapperMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/AbstractBuildWrapperMojo.java index a335d2d4ef7..9a9bd9d4553 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/AbstractBuildWrapperMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/AbstractBuildWrapperMojo.java @@ -74,6 +74,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { if (open != null) { properties.setProperty("open", open.toString()); } + // The child build is a separate Maven run and sees none of this one's -D + // options, so the ones a build reads are passed on: cn1.* (cn1.board, the + // device options) and codename1.arg.* (build hints given on the command line). + for (String key : System.getProperties().stringPropertyNames()) { + if ((key.startsWith("cn1.") || key.startsWith("codename1.arg.")) && !properties.containsKey(key)) { + properties.setProperty(key, System.getProperty(key)); + } + } request.setProperties(properties); if (buildExecutableJar()) { @@ -87,11 +95,21 @@ public void execute() throws MojoExecutionException, MojoFailureException { if (result.getExitCode() != 0) { throw new MojoFailureException("Failed to build project with exit code " + result.getExitCode()); } + afterBuild(); } catch (MavenInvocationException e) { throw new MojoExecutionException("Failed to invoke Maven", e); } } + /// Runs in this Maven run once the child build has succeeded. + protected void afterBuild() throws MojoExecutionException { + } + + /// The project this goal was started in. + protected MavenProject getProject() { + return project; + } + protected abstract String getPlatform(); protected abstract String getBuildTarget(); diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/BuildLinuxLocalMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/BuildLinuxLocalMojo.java new file mode 100644 index 00000000000..c3cd6f4f8d3 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/BuildLinuxLocalMojo.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven.buildWrappers; + +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.ResolutionScope; + +/** + * Builds the native Linux executable on this machine, with no cloud build: + * {@code mvn cn1:buildLinuxLocal}. With {@code -Dcn1.board=rpi5} (or any board + * profile) it is an embedded build for that board, cross-compiled for its + * architecture. The executable is left in {@code linux/target/linux-native}, where + * {@code cn1:deploy-device} finds it. + */ +@Mojo(name = "buildLinuxLocal", requiresDependencyResolution = ResolutionScope.NONE, + requiresDependencyCollection = ResolutionScope.NONE) +public class BuildLinuxLocalMojo extends AbstractBuildWrapperMojo { + @Override + protected String getPlatform() { + return "linux"; + } + + @Override + protected String getBuildTarget() { + return "local-linux-device"; + } +} From bd0552a95e6c8f3fd3f1e1375e165bd1dc95b882 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:40:42 +0200 Subject: [PATCH 06/12] Initializr: carry the new linux_local, device and device_logs launcher commands Co-Authored-By: Claude Opus 5.5 --- .../common/src/main/resources/common.zip | Bin 263662 -> 264116 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/scripts/initializr/common/src/main/resources/common.zip b/scripts/initializr/common/src/main/resources/common.zip index 1c657d39f517650b9c61207efc2bd3621fe85d44..015e174f57d36169078217ee30ca9ab7f0583d60 100644 GIT binary patch delta 3265 zcmZ9Oc{r5o8^_1{zC)H7W9?Y7@5xe@G_ zA=#I*Wyy9dW#^}Ju5-?H{hoiG>vMmf`+4sB{^NT+E0LJ87)($ZMKc*MqNe*9gG3;_ z84(C-1Oj2>?c{1FVPlOeH+Q=`rG+_Mv65C-hfN!uls9P^uPE?HzQXWjRVIvtus#Sj zh#DNX1mg_1MzMP~Qj}g!SJvBoZ)G^#acC{wc~0qt4!H1B;3@;X4&)}(E|BfU9%shx zGeG@BaC+_93nmvoha!AFgAV;syeU~D-xMF({*Gn)@YFm(l$73(RPjh}YhlvGwj|0d zu=vF34EcGw7rj3!IQqHGSYix5GvlwGj@HTp9Jqoee>SKwNNZ5VYsp-edDARlZ`?M; zh9Rw%6Sn@$8{V2E&;~GQDx!*7kr6VKN0J-OnHXfZovs~^c{aXPt6Q77%b?U(s0q7u z$HG%eQE|^A^L;*{I9R^@HWz>8t0dPr@a(rD)ywCg5F7= zb@P)EU-nB2aV^v%qx+#@V?CPZoq4pPJ4UKzgR*{6_097NNgKQBelrO5+@WM;osoLN z5~)!aFKqOrSs^11eoTz0!N$-Ozeu!Mnhr;ng;@k0zlDied;de!6N%LVT9<1Rf9&dI;# zkneAXEJJ>gQcn_V8~;3-s+!Ik_p5U8ZXLmsyI9HoC1d*vc=&b6T^}BQPL*1cTMR$9 zpX^3}47IQRIX}fenbs;OWuxnxy<_HmG6LTmMMv^`7rDGj9#l*$pYl)<&C87A^%Ob( zK!wV|%CJF{bNN?!g;JM1@L0Cp#F#RAy(9@6{QbSIzL26{V4(UsZdk!b>H-!RWZPeV zPEhTdO#B%og2&`5^~$2IvY}+Q-L1gh7B$aNGvMw@px^@R?NOwAHDO&q-zFIugNs72-jcX0~q0v?l8vYe? zMc}pf4GHvoOJAy?Adzfk?bU34NQpM2%<2u{SMyCmNFBI!~g8)=%ZsGp zmUEfHd1GTGgSN`3%GrqaTf294bFKeVhls<} zQ7gXE!(R0Jb3e(+I7-Zm=``QqFm`{Mf#akYgtjYb34i-*@a_^j*Icu0xYX>e|n znpu(5D2;T&tWB|+IPbEeRhHtD?ZQ66>*hJZi(gOCxf?tf_9>5!xrA%1A77N`8C0;3 znoej(?I=k$p>Wtx($S1QyJ3jK9 zME_;O9KFXAe7zvcDhstyY}y|8WpC}$9|yGI3|}aS3}1kbIsDD{Vs`Y$?wQ7nK%Ds3 zJ-r-j&5ZAk=wJfp%sO}YWGf>l*F7W)VxOswl!ag@42+)gl?{Qewlym&?2LH_tMb)y zw;P5mzHIU`6qla+7J%JYCa>1_Ybm~oP9a=lcJ?Tl+L@@D;5@Zw5#}1!PMnL>I&vOb z4g7(Zm0UfnI$B9T|K~Et1plDNwS}=;__x^p5mzpWbUk~{@Qhi$P_~UQ<=0ea__FGjQ67m`-fJk($Aa9V|gMK0%A;6-$^lyq1XH(S*0+bDdM{Cgb*m)=Ns zRfbDG(yT)_+43z1Gf6Vukh8Ww18}v*xR6xn26jb*Tm~Z>g)Re=B@H2U^K$-0);U{s zRw~LbRs-nnPlQ3meh=l!3ejWrNTn8!=AobyqBXaB-#J7XeQio{I44l(Mf=`OooHb9 z5&vu9)9|=T-^(43bJ}B7H3vO4ip^{{oJ?j%YQXp3C3iYm zA+k%k-tnwq?A1ds!Bdo-c(xvl#nSxu^MpHhL2Zg9n0XJB0T}vVy*&^M5=%jN=RGJ4 zzuN=F04EYWx(CVw@g$geA5^4iAb|+myKZiG-C+g@;(#T#Kn(1>4Wel_{!_kgf~@ca z1%!cblR${>*q@IV2C=NhTM2*J2UP)+N|@yU)CH8P;oAqGHsDhY7aV}Xod0j@e`g{P z%fSWJ@X`S&L-n-*L`b;#z-v1o7Y6l@{@3Lcjqq`!3V?Vc9D4{F1HsMk^dX1^-ZsNL zN8nlN-ewTt>g4Y23s3$61z`LUhz5`!;g};(hW7MF5Mk|Zckix~-QQ~DCWxjbcl;Yo z-T--%#33}W-I1gSDS^VBARPD!Ipn&9o z+D=%F0#XJ(bi=_E5EfYLP6i-WczzS4`#mWxfWPNBpa)p}-dN!dGKhlj0+0xh-UFus zkQ6BU4NS1+6@`BSkR0%J8a@L;dLVHYgwfwYe)u^Ei2)_^a65Qh+r&Hw2QPysVIBw) z1DO~8A`aN`H|hUP{4nV^Mg2{pFdBJGz5kFM5|RVOR{zSw!b3<1Ls`E89uNC}6%pwQ delta 2769 zcmZ8jc{tQ<7oPb&Kf@rVEZOBv5+d2($&zJ6XpwyzL?MyAg+Zt{nk-E};%!6*EhNhf z#iSa91`!exvW3VpCCjIG`M&G=p1+>^KIb~;+<%<=Iu$o?v&p#F3iL}hDYTh1s0E`? z0)i+M7KK82h5Ma3sp{n!R_PEJG^~qTzvH2PEulo8vbZ9Gozx|MgbVzu2Pm@wC)4jT zwnVTaVl5x6&FDl(9kL~Ioa+k{&R_IhO*!=q-@u0rF%Zf3W^ml!(tB zjnLv-8CA(Q+7bl%dwq2MY*T1mZ>&4H{stW!5w`o_lzPsBlPZbSOQ+#}e z&zARyJ5@IJfLl0OI^_{L?}v%QyJECvbdHIBzCY@=4x?U|q!Y2jwxyUiFP{_0HCt!I zXI1{(dT0u@_5d#-fbXd#@pe4l3rLRYVV^@RP8GQ%@(`aHWwvNi0`T{})snP!FS2)v zy(KzfW@`>ni6IR;Q+A{keY&8TloRXxXGt3%K()n$N@FcOwRnnLVEF1z;l~wx&oyr@ zysVq$_1#M(u_xm~)GR|Md#)!aJL}xLprqd|;VC(;&wHX3=c{13i{YRmSDKYBIGXq* zcbFwlm1|1j-+V@;Wd)d+ne=%s_jYU@ zAR8>xWCC!HT78^#f=!C^s$)PG^?Ql|ebrn2yQk=DJ#(c;cd{LjudZkr97gx2^cg_dT z`{ia?j?2iy<~K!_74dVC&FD!IQf2~qx&W9Flmu61DB9MCYOqIzmroI4fCR?`=FXj zSNSsuM$OrtrJ0W7)0T%FvY#Km=>GeHx-j}lSUH`yV|&(X<#r|f5?|O5hxny;46i4& z%^+IFhD{$^3S`WV9F{YLtA#fVTPvj(cFey0X^)<6)9$=%gE_mT;=0KF^(vw!RogpG zUV2CVX;)*uq;BeQDoOZ@x1p$m%P~Esr6i~9eQn+CWoC@jk*E3Y%mJf#JaIqUJ@?(~d#m9m?2CL4N66kA!mzPPO2Rt|*S8ZkMVz^vG zz_|O@k)=YKthR4x*_?h*`X5cfdx#Br$5K{0nuP13WN@ikOZ7*b7Mo;J@5Ga1MkjHV z9kYAW)t#B+p11dDr1^KRwvJhibWiAxSR6Pxg|^OEg^zQ$h@jdP%(cVNTh?Qhk9?$C#dC{I-1Xy-C1sbEn2OTUC}Ix z-Tpqrnsep#?>Q1RXP@19=n{WuzH2q1AWS#>(#cop#LkLaor;zxMVSmpaQ^qEtA99J z$S|$)pnFN?p2wJ8hw&UAt0_tJb&RvGzZ%#t_8CJ?`YQH=bX3mq{j|`9#IN0a^z;2?FZCamlvS%Lq_gQm2U~#;&6c+~O%DeMeGKRlFFI9n>1CI=}cNgtevX8i?5rc z5^uQR>$hA*0xX*Byoy%2{yR6Z2;P_H4jLbmsv_B(`BLC->icBsYPU{qUv4gAms6zu zGaIV(V zWj}az+qtVw2agOHaW0zJKW(HR%JF!J(GirMaW9-?8Wbw)Q=dz}$rUuv%s$Z^yg^^u z&12g14Qx0Ym$CfsH#ZS4rDc@0iiQ20vEM#O$H?*fxc^a-{UO@fuTO12gk`M@C|DlJ zwo|tQ?Q%lM*?4&?2BNW02Qtp>k)VHn4pQ;F|GQU^^i^Pv-iq|C0!>J}6Tz(kJk(KnxLG0g7lb#CaX)0|Eme!PP(nDP0FjAnI=< zgRJ~QUNr!5ehOp|vkjmKW!E4<8(_D>|C`ySpZ)b_;@CCdpW7+)G*bEVX0!8didy)>DdMJTy^H8H4vj{4Wp*G^__1 zvJg8otO@zBkYw~`n5P+OLc_Yy{$}JL1PdXz0W5`xZZ?5D|6K z>Y&Y*>}G@w!8#D98Q})70kr-GF$XXnD6;`#)V?Wyvyu{U`y0vKS_KkYW&ah1!l7mk zX8oI}2*X-XHyiPQ;eAlh0Fnm7L`ZBHG30_p5W!KvhX`?P>X{$?#n?b!#*iCaus)C& z2Z;J4*p8^J1Ae503&tS=93TK}{^q5TeHd5~N*ce#4a*}jn9T|ezy33d9XR^e*inG^7Q}! From ba7a53719df06224109af6f1a84cac3aebd09614 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 30 Oct 2026 21:43:53 +0200 Subject: [PATCH 07/12] Guide: real-time and embedded prose passes vale and LanguageTool Co-Authored-By: Claude Opus 5.5 --- docs/developer-guide/Embedded-Linux.asciidoc | 10 ++--- docs/developer-guide/Realtime.asciidoc | 46 ++++++++++---------- docs/developer-guide/languagetool-accept.txt | 4 ++ 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/docs/developer-guide/Embedded-Linux.asciidoc b/docs/developer-guide/Embedded-Linux.asciidoc index d6d47594c8f..57969d64afb 100644 --- a/docs/developer-guide/Embedded-Linux.asciidoc +++ b/docs/developer-guide/Embedded-Linux.asciidoc @@ -124,7 +124,7 @@ and the host in each one once. |Maven goal |Gradle task |What it does |`cn1:buildLinuxLocal` |`buildLinuxLocal` |Builds the native executable on this machine into -`linux/target/linux-native`. With `-Dcn1.board=` it is an embedded build for +`linux/target/linux-native`. With `-Dcn1.board=` it's an embedded build for that board. |`cn1:runOnDevice` |`cn1RunOnDevice` |`buildLinuxLocal`, then `deploy-device`, then follows the log. |`cn1:deploy-device` |`cn1DeployDevice` |Installs the last build on the device as a service. @@ -134,11 +134,11 @@ that board. Gradle takes the same properties with `-P` instead of `-D`. -A build for a board of another architecture, an arm64 board from an x64 laptop say, -needs a cross compiler. Install https://ziglang.org/download/[zig] and put it on the +A build for a board of another architecture, an arm64 board from a laptop say, +needs a cross-compiler. Install https://ziglang.org/download/[zig] and put it on the `PATH`, or set `CN1_ZIG`. The build then links against an old glibc, so the -executable runs on any current distribution. If no cross compiler is found the build -stops and says so. `linux.cc` names any other cross compiler, and `linux.sysroot` a +executable runs on any current distribution. If no cross-compiler is found the build +stops and says so. `linux.cc` names any other cross-compiler, and `linux.sysroot` a board SDK (below). `-Dcn1.board` is a shortcut. To make a project always build for its board, set the diff --git a/docs/developer-guide/Realtime.asciidoc b/docs/developer-guide/Realtime.asciidoc index a065a43aaa2..fa8d6a997a6 100644 --- a/docs/developer-guide/Realtime.asciidoc +++ b/docs/developer-guide/Realtime.asciidoc @@ -8,7 +8,7 @@ else, and the collector never stops it. The target is *firm* real time on a PREEMPT_RT Linux kernel, which mainline Linux includes since 6.12. On such a kernel the latency of a periodic thread is bounded -by the kernel and the hardware, not by the virtual machine. That is not a +by the kernel and the hardware, not by the virtual machine. That's not a certified hard real-time system: Codename One makes no claim under IEC 61508, ISO 26262 or DO-178C, and a loop whose failure can hurt someone needs an independent safety mechanism, such as a hardware watchdog or an interlock. @@ -22,12 +22,12 @@ include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/g <1> Everything the loop needs is allocated before it starts. <2> `setup()` runs on the real-time thread before the first period. It may allocate. <3> `cycle()` runs once per period, released on an absolute clock, so the period - does not drift with the execution time. -<4> Called instead of skipping silently when releases were missed. + doesn't drift with the execution time. +<4> Called instead of skipping without a word when releases were missed. `getStatistics()` reports the cycles, the missed periods, the release latency (maximum, mean and a histogram with percentiles), the execution time and the -allocation violations. Check them on the target board under load: they are the +allocation violations. Check them on the target board under load: they're the only real evidence that the loop meets its deadline. === Parameters @@ -46,7 +46,7 @@ only real evidence that the loop meets its deadline. | `setAllocationBudget(int)` | 0 | Allocations a cycle may make before the thread fails. | `setSchedulingRequired(boolean)` | false | Fail the thread when real-time - scheduling cannot be applied, instead of running it normally. + scheduling can't be applied, instead of running it normally. |=== === Life cycle @@ -57,16 +57,16 @@ A thread goes through `RealtimeThread.STATE_STARTING`, `STATE_RUNNING` and ends * `setup()` runs once, on the thread, before the first period. * `cycle()` runs once per period, and `periodsMissed(n)` before it when `n` releases were missed. The loop then continues from the current time; missed - periods are not run late in a burst. + periods aren't run late in a burst. * `stop()` asks the loop to end after the current cycle, and `join(millis)` waits for it. `stopped()` runs on the thread last, after a stop and after a failure alike, and may allocate. * An exception or error from the task, including `RealtimeAllocationError`, ends the thread in `STATE_FAILED`. `failed(Throwable)` receives it on the thread, and - `getFailure()` returns it afterwards. The default `failed` prints the stack trace. + `getFailure()` returns it afterward. The default `failed` prints the stack trace. `isSchedulingApplied()` and `isCpuPinned()` say what the platform actually granted. -Check them at start-up and log them: a loop that silently runs at normal priority +Check them at start-up and log them: a loop that runs at normal priority looks fine on the bench and misses deadlines in the field. `getStatistics()` returns a `RealtimeStatistics` snapshot: @@ -82,11 +82,11 @@ looks fine on the bench and misses deadlines in the field. about 16 milliseconds up. * `getLatencyPercentileNanos(0.9999)`: a percentile read off the histogram, as the upper bound of its bucket. -* `toString()`: all of the above on one line, for a log. +* `toString()`: everything above on one line, for a log. === What the platform supports -`Realtime.getInstance().isSupported(feature)` answers for each part separately: +`Realtime.getInstance().isSupported(feature)` answers for each part on its own: [cols="2,4"] |=== @@ -95,7 +95,7 @@ looks fine on the bench and misses deadlines in the field. | `FEATURE_SCHEDULING` | `SCHED_FIFO` priorities can be requested. | `FEATURE_CPU_AFFINITY` | Threads can be pinned to a core. | `FEATURE_MEMORY_LOCK` | `lockMemory()` can lock the process into RAM. -| `FEATURE_GC_ISOLATION` | The collector does not stop real-time threads. +| `FEATURE_GC_ISOLATION` | The collector doesn't stop real-time threads. | `FEATURE_ALLOCATION_CHECK` | Every allocation in a cycle is caught at run time. Without it only allocations that need a new memory page are caught, and the build-time check is the guarantee. @@ -111,20 +111,20 @@ be asked to wait for it. Codename One checks this twice: * *At build time*, the translator follows every call `cycle()` and `periodsMissed()` can make. It fails the build on `new`, array creation, boxing and string concatenation, and the error names the method and the line. An - allocation that is meant to happen, such as in a path that runs only on a fault, + allocation that's meant to happen, such as in a path that runs only on a fault, is marked with `@AllocationAllowed`. -* *At run time*, an allocation the build could not see -- through an interface +* *At run time*, an allocation the build couldn't see -- through an interface with several implementations, say -- stops the thread with a `RealtimeAllocationError` naming the class. The thread's `failed()` method receives it. `setAllocationBudget(n)` allows up to `n` allocations per period for - code that cannot avoid them yet. + code that can't avoid them yet. + -Catching the error inside `cycle()` does not turn the check off. The allocation +Catching the error inside `cycle()` doesn't turn the check off. The allocation that failed is counted, and from the next period on the check applies again, so each later allocation fails the same way. -*The collector does not stop a real-time thread.* An ordinary thread pauses -briefly while the collector reads its stack. A real-time thread is released +*The collector doesn't stop a real-time thread.* An ordinary thread pauses +for a moment while the collector reads its stack. A real-time thread is released immediately, and its references stay visible to the collector through the write barrier. While any real-time thread runs, the collector also never enters its stop-the-world phases. @@ -135,18 +135,18 @@ priority-inheriting mutexes on Linux, so a low-priority holder is raised until i releases. The wait is still as long as the holder's critical section. Pass data through preallocated buffers and `java.util.concurrent.atomic` instead. -*Avoid the slow paths of the runtime in a cycle.* Do not do these in `cycle()`: +*Avoid the slow paths of the runtime in a cycle.* Don't do these in `cycle()`: * Throw exceptions. Building a stack trace allocates. * Initialize a class for the first time. Touch it in `setup()`. -* Do I/O that blocks. +* Blocking I/O. * Use the hardware API calls that return new arrays. Use the variants that fill a buffer you pass. === Memory and the rest of the system `Realtime.lockMemory()` locks the process into RAM, so that its memory is never -paged out. Each page is locked the first time it is touched (`MCL_ONFAULT`), because +paged out. Each page is locked the first time it's touched (`MCL_ONFAULT`), because the heap is a large address reservation of which only part is used. The real-time thread's stack is touched before its first period. Touch your own buffers in `setup()`, so that no first touch happens in a cycle. Call `lockMemory()` once, @@ -155,16 +155,16 @@ early. It needs `CAP_IPC_LOCK` or a `memlock` limit large enough for the process `Realtime.setMemoryLimit(bytes)` caps the process's resident memory, on Linux. When the process is over the cap, an allocation in an ordinary thread throws `OutOfMemoryError` at once. Without the cap, the whole process slows down while the -collector struggles. Real-time threads are exempt, because they do not allocate. +collector struggles. Real-time threads are exempt, because they don't allocate. The application decides what to drop and carries on. The measurement is resident memory, which is what a device runs out of. Two things follow from that: -* It counts memory the heap is holding but has not returned to the system yet, so +* It counts memory the heap is holding but hasn't returned to the system yet, so the cap applies to the process's footprint, not its live objects. * An allocation is refused when a collection ends with the process still over the - cap. Without waiting for a collection, it is refused once the process is a + cap. Without waiting for a collection, it's refused once the process is a quarter over. Set the cap with headroom over what the application needs at its peak. diff --git a/docs/developer-guide/languagetool-accept.txt b/docs/developer-guide/languagetool-accept.txt index 798f31b7ada..2140fac64de 100644 --- a/docs/developer-guide/languagetool-accept.txt +++ b/docs/developer-guide/languagetool-accept.txt @@ -881,3 +881,7 @@ Vulkan Karatsuba enqueues backoff +fieldbus +mutexes +preallocated +cgroup From 1eeb583737c6a79854fd0b0aba62a9dcacde39ea Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 30 Oct 2026 22:41:53 +0200 Subject: [PATCH 08/12] Guide: the motor loop snippet compiles as a fragment Co-Authored-By: Claude Opus 5.5 --- .../snippets/generated/RealtimeMotorLoopSnippet.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/RealtimeMotorLoopSnippet.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/RealtimeMotorLoopSnippet.java index 6245dc3bfaf..8b93700dc17 100644 --- a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/RealtimeMotorLoopSnippet.java +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/RealtimeMotorLoopSnippet.java @@ -31,11 +31,10 @@ import com.codename1.realtime.RealtimeThread; class RealtimeMotorLoopSnippet { - static final double[] GAINS = new double[64]; - // tag::realtime-motor-loop[] class MotorLoop extends RealtimeTask { private final double[] samples = new double[64]; // <1> + private final double[] gains = new double[64]; private PwmChannel pwm; private boolean failed; @@ -53,7 +52,7 @@ public void cycle() { // <3> } double out = 0; for (int i = 0; i < samples.length; i++) { - out += samples[i] * GAINS[i]; + out += samples[i] * gains[i]; } try { pwm.setDutyCycle(out); From 67cf46da31506e1fe945d937a8f862d1e3d39402 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 31 Oct 2026 13:20:54 +0200 Subject: [PATCH 09/12] Real-time: the collector inherits the held thread's priority A real-time thread that wakes while the collector scans its stack waits for the scan, and the collector is an ordinary thread that the busy threads can preempt inside that window. CI caught one 125ms stall of a 1kHz loop whose cycle never ran past 4us. The collector now runs SCHED_FIFO just under the held thread from raising its block to releasing it. Co-Authored-By: Claude Opus 5.5 --- vm/ByteCodeTranslator/src/cn1_globals.h | 1 + vm/ByteCodeTranslator/src/cn1_globals.m | 44 +++++++++++++++++++ vm/ByteCodeTranslator/src/cn1_realtime_impl.h | 1 + 3 files changed, 46 insertions(+) diff --git a/vm/ByteCodeTranslator/src/cn1_globals.h b/vm/ByteCodeTranslator/src/cn1_globals.h index 8987d52a21f..01b5b28ceda 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.h +++ b/vm/ByteCodeTranslator/src/cn1_globals.h @@ -2190,6 +2190,7 @@ struct ThreadLocalData { JAVA_BOOLEAN realtimeThread; JAVA_BOOLEAN rtAllocTrap; JAVA_BOOLEAN rtFrozen; // in the steady state (cn1RtFreeze), trap lifted or not + JAVA_INT rtPriority; // SCHED_FIFO priority rtEnter applied, 0 when none JAVA_INT rtAllocBudget; JAVA_INT rtAllocBudgetLeft; JAVA_LONG rtAllocViolations; diff --git a/vm/ByteCodeTranslator/src/cn1_globals.m b/vm/ByteCodeTranslator/src/cn1_globals.m index cd0a55f05d3..fee46bb6a5f 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.m +++ b/vm/ByteCodeTranslator/src/cn1_globals.m @@ -754,6 +754,46 @@ static JAVA_BOOLEAN cn1RtThreadsPresent(void) { return __atomic_load_n(&cn1RtThreadCount, __ATOMIC_SEQ_CST) > 0 ? JAVA_TRUE : JAVA_FALSE; } +// PRIORITY INHERITANCE FOR THE HOLD. A real-time thread that wakes while the collector is +// scanning its stack waits for that scan (CN1_RT_UNPARK), and the collector is an ordinary +// thread: on a busy machine the threads the real-time one exists to outrank preempt it +// inside that window, and the wait becomes theirs. Measured on a 2-vCPU CI runner as a +// single 125ms stall of a 1kHz loop whose own cycle never ran past 4us. So the collector +// takes SCHED_FIFO just under the held thread for exactly the window it holds it -- from +// raising threadBlockedByGC to the release after its stacks -- and drops back after. +// Failure (no privilege) leaves it as it was, which is the old behaviour. +#if defined(__linux__) +static int cn1RtCollectorBoosted = 0; +static int cn1RtCollectorPolicy; +static struct sched_param cn1RtCollectorParam; +static void cn1RtBoostCollector(struct ThreadLocalData* held) { + struct sched_param p; + if(!held->realtimeThread || held->rtPriority <= 0 || cn1RtCollectorBoosted) { + return; + } + if(pthread_getschedparam(pthread_self(), &cn1RtCollectorPolicy, &cn1RtCollectorParam) != 0) { + return; + } + memset(&p, 0, sizeof(p)); + // One below the held thread: above every ordinary thread, which is what the hold needs, + // and never equal to the thread itself, which on a shared CPU would let the collector's + // wait for it to park (a spin, while it runs its period) delay the period instead. + p.sched_priority = held->rtPriority > 1 ? held->rtPriority - 1 : 1; + if(pthread_setschedparam(pthread_self(), SCHED_FIFO, &p) == 0) { + cn1RtCollectorBoosted = 1; + } +} +static void cn1RtUnboostCollector(void) { + if(cn1RtCollectorBoosted) { + pthread_setschedparam(pthread_self(), cn1RtCollectorPolicy, &cn1RtCollectorParam); + cn1RtCollectorBoosted = 0; + } +} +#else +#define cn1RtBoostCollector(held) do { (void)(held); } while(0) +#define cn1RtUnboostCollector() do {} while(0) +#endif + // Monotonic nanoseconds, for the collector's own short waits and policy timing. Same // split as cn1MonotonicMillis: clock_gettime / CLOCK_MONOTONIC do not exist on the // clang-cl Windows target, so it reads the QueryPerformanceCounter-backed clock there @@ -6053,6 +6093,7 @@ void codenameOneGCMark() { // wait for the thread to pause so we can traverse its stack but not for native threads where // we don't have much control and who barely call into Java anyway if(t->lightweightThread) { + cn1RtBoostCollector(t); t->threadBlockedByGC = JAVA_TRUE; // Full fence before the threadActive reads below: with the one in // cn1GcTryResumeActive, a thread going active either sees this block @@ -6598,6 +6639,9 @@ void codenameOneGCMark() { cn1GcWakeUnblocked(); } #endif + // Past the hold either way: released above, or held to the sweep with + // every other thread, where no period is promised. + cn1RtUnboostCollector(); #ifdef CN1_GC_CAN_FORCE_STOP // Released as soon as this thread's roots are captured, which is HERE and // not at the threadBlockedByGC clear below. A cooperatively parked thread diff --git a/vm/ByteCodeTranslator/src/cn1_realtime_impl.h b/vm/ByteCodeTranslator/src/cn1_realtime_impl.h index 0c1c2339b3b..d5135087464 100644 --- a/vm/ByteCodeTranslator/src/cn1_realtime_impl.h +++ b/vm/ByteCodeTranslator/src/cn1_realtime_impl.h @@ -251,6 +251,7 @@ static JAVA_INT cn1RtImpl_rtEnter(CODENAME_ONE_THREAD_STATE, JAVA_INT priority, p.sched_priority = priority; if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &p) == 0) { applied |= CN1_RT_APPLIED_SCHEDULING; + threadStateData->rtPriority = priority; } } if (cpu >= 0) { From c5dc883bb43e7662e8d5b26df2600e0e608cda2f Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 31 Oct 2026 13:54:40 +0200 Subject: [PATCH 10/12] Launchers: ship build.bat and run.bat with CRLF endings cmd.exe finds a goto label by re-reading the batch file in 512-byte blocks, and in an LF-only file it can miss a label that straddles a block boundary. The device targets moved one onto a boundary and `build.bat javascript` ran the android target on Windows. The archetype stores both launchers as CRLF, as mvnw.cmd already is, the Initializr zip carries them that way, and the telemetry injection matches its markers on LF and writes CRLF back. Co-Authored-By: Claude Opus 5.5 --- .../resources/archetype-resources/build.bat | 298 +++++++++--------- .../resources/archetype-resources/run.bat | 106 +++---- .../initializr/model/GeneratorModel.java | 13 +- .../common/src/main/resources/common.zip | Bin 264115 -> 264144 bytes scripts/sync-initializr-launchers.py | 10 +- 5 files changed, 220 insertions(+), 207 deletions(-) diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/build.bat b/maven/cn1app-archetype/src/main/resources/archetype-resources/build.bat index a4f64816be3..466edb65be6 100755 --- a/maven/cn1app-archetype/src/main/resources/archetype-resources/build.bat +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/build.bat @@ -1,149 +1,149 @@ -@echo off -setlocal DisableDelayedExpansion -setlocal EnableExtensions - - - -pushd "%~dp0" || exit /b 1 -set "MVNW=mvnw.cmd" - -SET "CMD=%~1" -if "%CMD%"=="" ( - echo No target selected: building a LOCAL JAR. For a cloud build, run .\build.bat javascript_cloud. - set CMD=jar -) -goto %CMD% - -goto :finish -:mac_desktop -call "%MVNW%" package -DskipTests -Dcodename1.platform^=javase -Dcodename1.buildTarget^=mac-os-x-desktop -U -e - -goto :finish -:mac_native -call "%MVNW%" package -DskipTests -Dcodename1.platform^=ios -Dcodename1.buildTarget^=mac-os-x-native -U -e - -goto :finish -:windows_desktop -call "%MVNW%" package -DskipTests -Dcodename1.platform^=javase -Dcodename1.buildTarget^=windows-desktop -U -e - -goto :finish -:windows_device -call "%MVNW%" package -DskipTests -Dcodename1.platform^=win -Dcodename1.buildTarget^=windows-device -U -e - -goto :finish -:linux_device -call "%MVNW%" package -DskipTests -Dcodename1.platform^=linux -Dcodename1.buildTarget^=linux-device -U -e - -goto :finish -:linux_local -set "BOARD_ARG=" -if not "%CN1_BOARD%"=="" set BOARD_ARG="-Dcn1.board=%CN1_BOARD%" -call "%MVNW%" cn1:buildLinuxLocal %BOARD_ARG% -U -e - -goto :finish -:device -set "BOARD_ARG=" -set "HOST_ARG=" -if not "%CN1_BOARD%"=="" set BOARD_ARG="-Dcn1.board=%CN1_BOARD%" -if not "%CN1_DEVICE_HOST%"=="" set HOST_ARG="-Dcn1.device.host=%CN1_DEVICE_HOST%" -call "%MVNW%" cn1:runOnDevice %BOARD_ARG% %HOST_ARG% -U -e - -goto :finish -:device_logs -set "HOST_ARG=" -if not "%CN1_DEVICE_HOST%"=="" set HOST_ARG="-Dcn1.device.host=%CN1_DEVICE_HOST%" -call "%MVNW%" cn1:device-logs %HOST_ARG% -e - -goto :finish -:javascript -call "%MVNW%" package -DskipTests -Dcodename1.platform^=javascript -Dcodename1.buildTarget^=local-javascript -U -e - -goto :finish -:javascript_cloud -call "%MVNW%" package -DskipTests -Dcodename1.platform^=javascript -Dcodename1.buildTarget^=javascript -U -e - -goto :finish -:android -call "%MVNW%" package -DskipTests -Dcodename1.platform^=android -Dcodename1.buildTarget^=android-device -U -e - -goto :finish -:xcode -call "%MVNW%" package -DskipTests -Dcodename1.platform^=ios -Dcodename1.buildTarget^=ios-source -U -e - -goto :finish -:ios_source -goto :xcode - -:android_source -call "%MVNW%" package -DskipTests -Dcodename1.platform^=android -Dcodename1.buildTarget^=android-source -U -e - -goto :finish -:ios -call "%MVNW%" package -DskipTests -Dcodename1.platform^=ios -Dcodename1.buildTarget^=ios-device -U -e - -goto :finish -:ios_release -call "%MVNW%" package -DskipTests -Dcodename1.platform^=ios -Dcodename1.buildTarget^=ios-device-release -U -e - -goto :finish -:jar -call "%MVNW%" -Pexecutable-jar package -Dcodename1.platform^=javase -DskipTests -U -e - -goto :finish -:help -echo .\build.bat [COMMAND] -echo Local Build Commands: -echo The following commands will build the app locally ^(i.e. does NOT use the Codename One build server^) -echo -echo jar -echo Builds app as desktop app executable jar file to javase/target directory -echo android_source -echo Generates an android gradle project that can be opened in Android studio -echo *Requires android development tools installed. -echo *Requires ANDROID_HOME environment variable -echo *Requires either GRADLE_HOME environment variable^, or for gradle to be in PATH -echo ios_source -echo Generates an Xcode Project that you can open and build using Apple^'s development tools -echo *Requires a Mac with Xcode installed -echo javascript -echo Builds the web app locally. -echo -echo Build Server Commands: -echo The following commands will build the app using the Codename One build server^, and require -echo a Codename One account. See https://www.codenameone.com -echo -echo ios -echo Builds iOS app. -echo ios_release -echo Builds iOS app for submission to Apple appstore. -echo android -echo Builds android app. -echo mac_desktop -echo Builds Mac OS desktop app. -echo *Mac OS Desktop builds are a Pro user feature. -echo mac_native -echo Builds a native Mac app ^(no JVM^). -echo windows_desktop -echo Builds Windows desktop app. -echo *Windows Desktop builds are a Pro user feature. -echo windows_device -echo Builds a native Windows app ^(no JVM^). -echo linux_device -echo Builds a native Linux app ^(ELF^, no JVM^). -echo linux_local -echo Builds a native Linux app locally. CN1_BOARD=rpi5 cross builds for an embedded board. -echo device -echo Builds locally, deploys to CN1_DEVICE_HOST over SSH and follows the log. -echo device_logs -echo Follows the app's log on CN1_DEVICE_HOST. -echo javascript_cloud -echo Builds the web app using the build server. - -goto :finish -:settings -call "%MVNW%" cn1:settings -U -e - -:finish -set "CN1_EXIT_CODE=%errorlevel%" -popd -exit /b %CN1_EXIT_CODE% +@echo off +setlocal DisableDelayedExpansion +setlocal EnableExtensions + + + +pushd "%~dp0" || exit /b 1 +set "MVNW=mvnw.cmd" + +SET "CMD=%~1" +if "%CMD%"=="" ( + echo No target selected: building a LOCAL JAR. For a cloud build, run .\build.bat javascript_cloud. + set CMD=jar +) +goto %CMD% + +goto :finish +:mac_desktop +call "%MVNW%" package -DskipTests -Dcodename1.platform^=javase -Dcodename1.buildTarget^=mac-os-x-desktop -U -e + +goto :finish +:mac_native +call "%MVNW%" package -DskipTests -Dcodename1.platform^=ios -Dcodename1.buildTarget^=mac-os-x-native -U -e + +goto :finish +:windows_desktop +call "%MVNW%" package -DskipTests -Dcodename1.platform^=javase -Dcodename1.buildTarget^=windows-desktop -U -e + +goto :finish +:windows_device +call "%MVNW%" package -DskipTests -Dcodename1.platform^=win -Dcodename1.buildTarget^=windows-device -U -e + +goto :finish +:linux_device +call "%MVNW%" package -DskipTests -Dcodename1.platform^=linux -Dcodename1.buildTarget^=linux-device -U -e + +goto :finish +:linux_local +set "BOARD_ARG=" +if not "%CN1_BOARD%"=="" set BOARD_ARG="-Dcn1.board=%CN1_BOARD%" +call "%MVNW%" cn1:buildLinuxLocal %BOARD_ARG% -U -e + +goto :finish +:device +set "BOARD_ARG=" +set "HOST_ARG=" +if not "%CN1_BOARD%"=="" set BOARD_ARG="-Dcn1.board=%CN1_BOARD%" +if not "%CN1_DEVICE_HOST%"=="" set HOST_ARG="-Dcn1.device.host=%CN1_DEVICE_HOST%" +call "%MVNW%" cn1:runOnDevice %BOARD_ARG% %HOST_ARG% -U -e + +goto :finish +:device_logs +set "HOST_ARG=" +if not "%CN1_DEVICE_HOST%"=="" set HOST_ARG="-Dcn1.device.host=%CN1_DEVICE_HOST%" +call "%MVNW%" cn1:device-logs %HOST_ARG% -e + +goto :finish +:javascript +call "%MVNW%" package -DskipTests -Dcodename1.platform^=javascript -Dcodename1.buildTarget^=local-javascript -U -e + +goto :finish +:javascript_cloud +call "%MVNW%" package -DskipTests -Dcodename1.platform^=javascript -Dcodename1.buildTarget^=javascript -U -e + +goto :finish +:android +call "%MVNW%" package -DskipTests -Dcodename1.platform^=android -Dcodename1.buildTarget^=android-device -U -e + +goto :finish +:xcode +call "%MVNW%" package -DskipTests -Dcodename1.platform^=ios -Dcodename1.buildTarget^=ios-source -U -e + +goto :finish +:ios_source +goto :xcode + +:android_source +call "%MVNW%" package -DskipTests -Dcodename1.platform^=android -Dcodename1.buildTarget^=android-source -U -e + +goto :finish +:ios +call "%MVNW%" package -DskipTests -Dcodename1.platform^=ios -Dcodename1.buildTarget^=ios-device -U -e + +goto :finish +:ios_release +call "%MVNW%" package -DskipTests -Dcodename1.platform^=ios -Dcodename1.buildTarget^=ios-device-release -U -e + +goto :finish +:jar +call "%MVNW%" -Pexecutable-jar package -Dcodename1.platform^=javase -DskipTests -U -e + +goto :finish +:help +echo .\build.bat [COMMAND] +echo Local Build Commands: +echo The following commands will build the app locally ^(i.e. does NOT use the Codename One build server^) +echo +echo jar +echo Builds app as desktop app executable jar file to javase/target directory +echo android_source +echo Generates an android gradle project that can be opened in Android studio +echo *Requires android development tools installed. +echo *Requires ANDROID_HOME environment variable +echo *Requires either GRADLE_HOME environment variable^, or for gradle to be in PATH +echo ios_source +echo Generates an Xcode Project that you can open and build using Apple^'s development tools +echo *Requires a Mac with Xcode installed +echo javascript +echo Builds the web app locally. +echo +echo Build Server Commands: +echo The following commands will build the app using the Codename One build server^, and require +echo a Codename One account. See https://www.codenameone.com +echo +echo ios +echo Builds iOS app. +echo ios_release +echo Builds iOS app for submission to Apple appstore. +echo android +echo Builds android app. +echo mac_desktop +echo Builds Mac OS desktop app. +echo *Mac OS Desktop builds are a Pro user feature. +echo mac_native +echo Builds a native Mac app ^(no JVM^). +echo windows_desktop +echo Builds Windows desktop app. +echo *Windows Desktop builds are a Pro user feature. +echo windows_device +echo Builds a native Windows app ^(no JVM^). +echo linux_device +echo Builds a native Linux app ^(ELF^, no JVM^). +echo linux_local +echo Builds a native Linux app locally. CN1_BOARD=rpi5 cross builds for an embedded board. +echo device +echo Builds locally, deploys to CN1_DEVICE_HOST over SSH and follows the log. +echo device_logs +echo Follows the app's log on CN1_DEVICE_HOST. +echo javascript_cloud +echo Builds the web app using the build server. + +goto :finish +:settings +call "%MVNW%" cn1:settings -U -e + +:finish +set "CN1_EXIT_CODE=%errorlevel%" +popd +exit /b %CN1_EXIT_CODE% diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/run.bat b/maven/cn1app-archetype/src/main/resources/archetype-resources/run.bat index 2c35a32a23b..2212b6629a6 100755 --- a/maven/cn1app-archetype/src/main/resources/archetype-resources/run.bat +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/run.bat @@ -1,53 +1,53 @@ -@echo off -setlocal DisableDelayedExpansion -setlocal EnableExtensions - - -pushd "%~dp0" || exit /b 1 -set "MVNW=mvnw.cmd" - -SET "CMD=%~1" -if "%CMD%"=="" ( - echo Starting the LOCAL simulator. For a cloud build, run .\build.bat javascript_cloud. - set CMD=simulator -) -goto %CMD% - -:simulator -call "%MVNW%" verify -Psimulator -DskipTests -Dcodename1.platform^=javase -e - -goto :finish -:desktop -call "%MVNW%" verify -Prun-desktop -DskipTests -Dcodename1.platform^=javase -e - -goto :finish -:settings -call "%MVNW%" cn1:settings -e - -goto :finish -:certificatewizard -call "%MVNW%" cn1:certificatewizard -e - -goto :finish -:update -call "%MVNW%" cn1:update -U -e - -goto :finish -:help -echo run.bat [COMMAND] -echo Commands: -echo simulator -echo Runs app using Codename One Simulator -echo desktop -echo Runs app as a desktop app. -echo settings -echo Opens Codename One settings -echo certificatewizard -echo Opens the Certificate Wizard -echo update -echo Update Codename One libraries - -:finish -set "CN1_EXIT_CODE=%errorlevel%" -popd -exit /b %CN1_EXIT_CODE% +@echo off +setlocal DisableDelayedExpansion +setlocal EnableExtensions + + +pushd "%~dp0" || exit /b 1 +set "MVNW=mvnw.cmd" + +SET "CMD=%~1" +if "%CMD%"=="" ( + echo Starting the LOCAL simulator. For a cloud build, run .\build.bat javascript_cloud. + set CMD=simulator +) +goto %CMD% + +:simulator +call "%MVNW%" verify -Psimulator -DskipTests -Dcodename1.platform^=javase -e + +goto :finish +:desktop +call "%MVNW%" verify -Prun-desktop -DskipTests -Dcodename1.platform^=javase -e + +goto :finish +:settings +call "%MVNW%" cn1:settings -e + +goto :finish +:certificatewizard +call "%MVNW%" cn1:certificatewizard -e + +goto :finish +:update +call "%MVNW%" cn1:update -U -e + +goto :finish +:help +echo run.bat [COMMAND] +echo Commands: +echo simulator +echo Runs app using Codename One Simulator +echo desktop +echo Runs app as a desktop app. +echo settings +echo Opens Codename One settings +echo certificatewizard +echo Opens the Certificate Wizard +echo update +echo Update Codename One libraries + +:finish +set "CN1_EXIT_CODE=%errorlevel%" +popd +exit /b %CN1_EXIT_CODE% diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java index e17d847771d..5212d3810f8 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java @@ -561,16 +561,23 @@ String withShellTelemetry(String script) throws IOException { /// build.bat with the launch report just before it jumps to the target, and /// the exit report in its :finish block, which every target falls through to. - String withBatchTelemetry(String script) throws IOException { + /// The launcher ships with CRLF endings and so does the result: cmd.exe finds a + /// goto label by re-reading the file in 512-byte blocks and, in an LF-only file, + /// can miss one that straddles a block boundary and run the wrong target. The + /// markers are matched on LF, so the script is folded to LF, filled, and widened + /// back -- the telemetry resources included, whatever endings they carry. + String withBatchTelemetry(String original) throws IOException { + String script = StringUtil.replaceAll(original, "\r\n", "\n"); int jump = script.indexOf(BAT_GOTO_LINE); int finish = script.lastIndexOf(BAT_FINISH_BLOCK); if (jump < 0 || finish < jump || finish + BAT_FINISH_BLOCK.length() != script.length()) { - return script; + return original; } - return script.substring(0, jump + 1) + String filled = script.substring(0, jump + 1) + fillTelemetryTokens(readResourceToString(LAUNCHER_TELEMETRY_BAT_START)) + script.substring(jump + 1, finish) + fillTelemetryTokens(readResourceToString(LAUNCHER_TELEMETRY_BAT_FINISH)); + return StringUtil.replaceAll(StringUtil.replaceAll(filled, "\r\n", "\n"), "\n", "\r\n"); } /// The root pom with the same reporting for builds the launchers never see: the diff --git a/scripts/initializr/common/src/main/resources/common.zip b/scripts/initializr/common/src/main/resources/common.zip index d17d1288c4f5eacc1d2db63f5e29f1750783705e..321a7618bd005deef9fefbeffcbb13a854d4119a 100644 GIT binary patch delta 2797 zcmZ8i2{@Ep8=f=gHDfms*|(&kEZIZ$J!MN2g(xXYL<^cw%I8y5z9TJ?eJgFG?c=Y> z(pUz`QY0!92A^z$@xRpnU)TS?*Y#ZQ^W672&vVXm?(3XKmnkimDdj!9RHyT2KmGs& z!Y+(JAQK3L@W>D~Ki}hduFgk#G$?0;22G?%uSqGDFG&2V732hE35REb&o z%&)XPzQ&ohD;`!(nbBVG8D`D1p3e`?pL_2fUphQdcjxOvMd|Q^w)L7eJ6LIA(N^bt zU8LoC`vI-juP$Sv&ZJm}x|i2^!Sa1Ax2znPq5tcGaDE&wV`2V_N^sN{wIWH$Qw_x) z7S|dlwa;WeBb3N|k{Q_Gn8V$hVnfKTNnh8!p_4SX)Gp)bh zGc&Q4t`XEJ8T0+;iA}YVUgh>OhR47wRXgWYQbAHUbI*@&CnVXX8F%i>Tsx>ELO*&- z*ec>k9=qVujl-?J9M^gQ)+g=IJQdB+h}U%9PsZ(~ieh7(ka4{54Nu@x_mV;je$kUn z)q~*~Ppw#9ye5g0N*~9lkrg+ao7c?USG!%GuMnTv{sEcUM5Pq=+Yv&6cKPM zrF=DaiAW69q#)+YKJ`;_w)+C2OQJhms-75#8JY>qE+mfWDMVYF(^Z@2ZYy0&2T;O4wj5$9*le%0{- za*K{@m~ouc0seH>{I4?EB8i*Vidr z>i%}r-1Q#0Z>^gdzS^b&gXb@XGaq_o9;4OxF!USmv#*`s-t;by9e?sbR>N*uPB_m- z9!kZIdwb$8p1kjGo&Bw0x0pbYn{bvft1WfYP>KkbrDK)HpOPffr&W@FT3j6}j57=J z>gdps;tBfOB1P;DPm-SlyRSw=tuIa~$%WgWW=_qzKR8$PAa2K&M_iBnd&!+uqQ#ba7gj7yl@1xc1ZnY+GCbwjD0Z1UQE z=YX4qxr%&&?-e-Cg zrkO?lB&NoBQI76_3+0)6{id(X*pz%2)s_47cj>0NhcS%szzG^rXjexWzmI-_##giI z-E4EFr$MRsH=meza{*y1pM&<}q#V9)PUM&!>x`xoDQmsT+8%b9s^@mNxje}AJAODP zIG!q*IijW1lAC0vl&dGT81e3=ZrsM#kkd~`l{wy z-|0IJMw<+ED${gx1BwsMPtey~R;w1h<l*Q#Kd>Dwh7DQLFki6lm@;Xl?Y_)S`P{VW2+V z?q$o5FB;$T1B9dh*gBWLwWYFxbs~OjBjw4qn;yGqM!$_bSvR6S|vtT5Y7btXQ) z;g&7+D8ys^Xw&A7pfuIzyf0t7c88aH%1SEwWz~pyHC*HNKBB*Q$j>}huC};KuR=5V z?B2fJg%qv@#~+G~-t@TPd|7^$W6!$an{@0&`uDY+0lp&NbaO6M#k8(Ebha^-?~Am! zr3Eu@u2a9eDS6~#m|#rHF*)bL=O?bW?zL9w=_Tn#9UNu&^z_Lk4!Ac5>lM3ks}YS) zO|=S7*d{z)x>n2@a(|acC0rP81Go_@huffpJkNh$StRfiT7!?s?I+YEbNC5_6Nf{P zJixX99b6(%hqRZnj3HkRTrac+zk7du6vC)kBGQ!OXFOQG~4ru>0Gysw4`ZU}|%#}w@eNY^IWJ7+m zi1WnG6+{{t%s>?)MPZp%La`)_AEnRWw9^Wrg8G)}zkUT*n-#c`@hnsUu_$mBYJJu@8t5gF~v|$dPp|kAEA^R!Fi(==XJP1R#=b$GHt^-%k8mvZwY^V-$ z&^9(ShKxFZ`s;uUTAIg4lj^~0#K(cWM4oy?V?#4wgvj%F_x9BT!r{<}mF|&M=)Zy% zfGMQ&VuEC+-@s*vDU1GZBcp#)42WqU<{T7+<#d3m=|BouEx--n8wy!~5>o&F3jXIT za^q^y-36#i0N}h6J_X zD72FeWtVpeWYQ(Dtl=E8Oo8;8&; z60YCjMO=bqTzKJy6+|4Vaeg5bBrt;))<6^zWF0e*EWXzG&GYRoi`!|Ou$SD8- delta 2833 zcmZ9O3p`X?7r^JtK1N<+JS)U_GcZP@zrQ}0G(~gQFrH4{p zgYt-6VmvBIsBlA6UwI5N_{Qzt``zESf4}`(XRUS4+H3#UI%l7*bV6x5p`>4s!BCXE zKZsy3L==PJ$6zqWB7(K3Ui8AansWucqiSrzc24(FInHx$(+ybdBYte?tv&U8rkS#~!CAFU zWhMl{o}uT>-l9fZ&(S>D2{sQI1F`wvr?4zBdGiOv&pul=S=_3mmAX2I^@ZBEJaNac zNzpP-Nf*zl+cv7_)9lrrC}U3U5AVFcb^?h_3pHzd@Sq|27PCD-wsCDxf~8tSn^%uu zt3`Z}RMYV#rGq-6jJG*eF9k=SnKqtT6K67ZLp<79zO1+F_?DJ=Ok(2;Mwdr#thHB~ z>z+>XPXof?i#6N5q{p8KtrU(Hxu3QR#2%pabDb9P1RL85(ulq4(9wU5j>|taSeaV0 zY5iB#l@W1#Ki{}hpgmK#i*IkTBFFw>`$*P&n`Rx+V&p*4D;fEZLc^Oe1y_XfWeOFS zW|M?}?&mVuq^x_ixVpg(I^z}j`CwE{V&j#!A(+?@Ch!$5dk|yMr=@#YEpb8ZBtvTK zsI++utN$E-+sOx~eSM$l$;n>J?C>^oIo?Szz3`E9uG}HKYaq~y^7tb4NVe2E-(w2? z7K}bU4{3`!txJ8H+Ar4>_qYzr9I;OBCPjT2)iKqBMEo>IxRN7` z^ffS4E$zAqd>BlGI0hrYb5kQ|fj+DKQ0^WSGHgRw{&q-b>sGj@fo47_6) ze(v*7fcK+Rx44ou8o7q!!Z%({RLJ%yxJjiuc1tq$YNXi|V*TXfhi2<7#dNIrGi~&C z>fLWtIpWeXyq>`NPkGMV_rkupp&X%DF>8uQaWmgJJ=}`+bYmWYuhYlQhv=TqXtwFx zF3EN*jUqiWo%1~qMy5~}59B?*+*;*!=)z&ST?_e9!(M_)H}5ImbtNh`2xYqzm46a) zIJ-wZxf2nk?U0^%;jEN@>wu<>$Z6-}iDw78tc{KmZPMD_RgcDB=kUKCS0w4U1loOY zOblDVNlX7uPL@iss?FTwoZM)fo59$ey;10Wo)mprT1jSvoK9!54Ebyhno+~5msKrC zZeGn7Q`qS5aaq2?aiOaBOXY9uWyi*{!|lxA#-w|X#8|$sbBwn&rHf@th#5>(pDEdS zCwP1?pesh#@UE<3Pb2Diio?oYTluu%`^s?j$o0YK>VSymcZ?3UbjOKD16*SUy!$95 zl6XSS;wdXAU0ESoe>zrAAZmwhH08U*j|!ZAT3usA`gmk+-2Iib6y=EGfZLaPDHqF! zLoL(_^D-2}RE=UR`27w!)vL*U<&;;Lu??|j^jmkk;L@fZF-;RbJ+|9HqD05VZJna` z8Ap;$OtHP|xl`A2wy=jT&Zep|2t{Tu^+k3S&Y1=pXL!?Rrmq#mtnqg$Xwnt(j8j-F z?U{_cBKozixFt^6JV2*xo|@pNI_Zj%ukOA)r8=zfbb-%FSmW%MwCy{KEHlw|*13aT z^yb(JtCl;7N$&}}RPILX)54EGf1PcwerZo?$7XljozfBchL@SRc-xjYVvMEmE94+# z_gT?(HID4U{)1Me(3nrd`qS?Ii6R{$X?KW%##u(YPh}GoPdK6s8|`&`B<(E=iCE_@ zB6`3x$I0Wao=@!L4`{x$XyBAJwT@;ES9(a5liX>Su=I)k?|9@+}LRA(2kR@ zdIZuJ(5F+cRqXt1lfHGBFstKp>`uMS`#nwzPr_HL)bHCJ&R^ErTj6Su_l(*)p0mH( zuGU~F_D0_+h0Eex+rj?&^KSEJ>=H}Dg6zY3<59%f`x7{pPj*w!V9d6)^$2PQJEi?5 znYXrPF#k49hyF&^|7hIuy3z)>`~Z8)TUmTrKabvNYq-k1WEV)BSZm^6x6P9|mM~pe znO|$Zuc@P)UMq05?CVX5B*hCCv<%Jmmc**L(9ZAs_V0?{@B`)crf+_n@1Wuj7hsJI3@y zmZzmV&>QLoCk=_c#z#^|vYM|T(s9VM4|#gQon{6h6}pNRIPUP+dc%hzf>J4y7ixul z^1syN_FYa%9(}tIX((5D;;U1E{h5zSFM-s5qy(HVTv=4u8L_=|Vo7NAOA5QWKqiUw z8sSZBCFwO-R1o<4V1?>4zy|4rVKYF7|A#OJbDS0oOAu5A#xDXqq6!CQfIhzy;g_SH z1EitWERaW{q2nyDMjpV-Szw8N#&Uo$ z)}NS_L6m~+9H5GYtJe}R{sZ8H_&K1CZTs6G0PS(8@ULLLI24Znm@o&N5i8g|2lgV# zt3o4dKyeM+HxJa2PwIJ|Y3$g*qae*L|XhkF;V+9Bb za$6A0QLjj^@FVac7S#~t{g?bX2$-*y+E8VYhtZ`K!O%j&|ABD>Okd<-v}*t5_#ejL zOI%t+A{wEJtJn+avGjtdO1lds?$7w94csIdenprRDO z@1Dks`=m;caWM9$>VBA|a92 z7|cz)NEoI8YJzAm!_ZwwJhKUmT&h()cC zEch9V8sVR`gI}fcK1K_r|FNK`=wA>qtL18hRDe>j-#g)TfNnvQ;1EC^kb|pYEiCQ= zBG3ipg}nk#pr{Ve`v9^Uh-=}Nc_0pZQPcn#gm^yG0aT3gz>flD$lycOkrH^94>iYr z7>8OobRBe92Eq`JL*)?(xEY6T$2xq5as0g3D+h Date: Sat, 31 Oct 2026 16:29:24 +0200 Subject: [PATCH 11/12] Real-time API docs: examples compile on their own Both class examples used `...` for the parts they left out, and the RealtimeTask one read I2C through a method that does not exist -- every I2C read returns a new array, which a cycle must not do. The task example is now the guide's motor loop, and the Realtime one passes a minimal task inline. Co-Authored-By: Claude Opus 5.5 --- .../src/com/codename1/realtime/Realtime.java | 8 ++++-- .../com/codename1/realtime/RealtimeTask.java | 25 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CodenameOne/src/com/codename1/realtime/Realtime.java b/CodenameOne/src/com/codename1/realtime/Realtime.java index b98b419e074..3fe25e76dbb 100644 --- a/CodenameOne/src/com/codename1/realtime/Realtime.java +++ b/CodenameOne/src/com/codename1/realtime/Realtime.java @@ -32,8 +32,12 @@ /// rt.setMemoryLimit(64L * 1024 * 1024); // fail fast instead of thrashing /// RealtimeThread motor = rt.start("motor", /// new RealtimeParameters().setPeriodNanos(1000000L).setPriority(80).setCpu(3), -/// new MotorLoop()); -/// ... +/// new RealtimeTask() { +/// public void cycle() { +/// // every period; must not allocate (see RealtimeTask) +/// } +/// }); +/// // the application runs; the motor keeps its period meanwhile /// Log.p(motor.getStatistics().toString()); /// ``` /// diff --git a/CodenameOne/src/com/codename1/realtime/RealtimeTask.java b/CodenameOne/src/com/codename1/realtime/RealtimeTask.java index 54cdc5e378f..642efea4cdf 100644 --- a/CodenameOne/src/com/codename1/realtime/RealtimeTask.java +++ b/CodenameOne/src/com/codename1/realtime/RealtimeTask.java @@ -26,19 +26,34 @@ /// /// ```java /// class MotorLoop extends RealtimeTask { +/// private final double[] samples = new double[64]; +/// private final double[] gains = new double[64]; /// private PwmChannel pwm; -/// private final byte[] reading = new byte[2]; -/// private I2cDevice encoder; +/// private boolean failed; /// /// public void setup() { /// // runs once on the real-time thread; may allocate -/// ... +/// try { +/// pwm = Hardware.getInstance().openPwm(0, 0); +/// } catch (HardwareException e) { +/// failed = true; +/// } /// } /// /// public void cycle() { /// // every period; must not allocate -/// encoder.read(reading, 0, 2); -/// pwm.setDutyCycle(controlLaw(reading)); +/// if (failed) { +/// return; +/// } +/// double out = 0; +/// for (int i = 0; i < samples.length; i++) { +/// out += samples[i] * gains[i]; +/// } +/// try { +/// pwm.setDutyCycle(out); +/// } catch (HardwareException e) { +/// failed = true; +/// } /// } /// } /// ``` From ec48b2af54417ef293ddb353b9fcf7db94cd0bee Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:03:59 +0200 Subject: [PATCH 12/12] Initializr test: read build.bat as CRLF and require it Co-Authored-By: Claude Opus 5.5 --- .../initializr/model/GeneratorModelMatrixTest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java index 983fedfbda3..943d7ffc25c 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java @@ -1383,7 +1383,13 @@ private void validateLauncherTelemetry() throws IOException { assertContains(sh, "CMD=\"jar\"", label + "build.sh keeps the local default"); assertTrue(sh.startsWith("#!/bin/bash\n"), label + "build.sh keeps its shebang"); - String bat = getText(entries, "build.bat"); + String rawBat = getText(entries, "build.bat"); + // Shipped as CRLF: with LF endings cmd.exe can miss a goto label that + // straddles one of its 512-byte reads. Checked once, then folded so the + // layout assertions below read lines. + assertTrue(rawBat.indexOf("\r\n") > 0 && StringUtil.replaceAll(rawBat, "\r\n", "").indexOf('\n') < 0, + label + "build.bat must use CRLF line endings throughout"); + String bat = StringUtil.replaceAll(rawBat, "\r\n", "\n"); assertContains(bat, "set \"CN1_PROJECT_ID=" + id + "\"", label + "build.bat should carry the hashed package"); assertContains(bat, "set \"CN1_MIN_JAVA=" + min + "\"", label + "build.bat should know the Java level"); assertContains(bat, GeneratorModel.LAUNCHER_EVENTS_URL, label + "build.bat should report to the events URL");