diff --git a/.github/workflows/embedded-linux.yml b/.github/workflows/embedded-linux.yml index f53a68338d5..c183dab6684 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: @@ -70,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" @@ -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..3fe25e76dbb --- /dev/null +++ b/CodenameOne/src/com/codename1/realtime/Realtime.java @@ -0,0 +1,293 @@ +/* + * 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 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()); +/// ``` +/// +/// #### 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..642efea4cdf --- /dev/null +++ b/CodenameOne/src/com/codename1/realtime/RealtimeTask.java @@ -0,0 +1,106 @@ +/* + * 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 final double[] samples = new double[64]; +/// private final double[] gains = new double[64]; +/// private PwmChannel pwm; +/// 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 +/// 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; +/// } +/// } +/// } +/// ``` +/// +/// `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/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..8b93700dc17 --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/RealtimeMotorLoopSnippet.java @@ -0,0 +1,78 @@ +/* + * 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 { + // 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; + + 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 76365ffd4ea..57969d64afb 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'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. +|`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 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 +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 @@ -323,3 +363,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..fa8d6a997a6 --- /dev/null +++ b/docs/developer-guide/Realtime.asciidoc @@ -0,0 +1,244 @@ +== 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'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. + +=== A periodic task + +[source,java] +---- +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. +<3> `cycle()` runs once per period, released on an absolute clock, so the period + 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're 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 can't 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 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 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 runs at normal priority +looks fine on the bench and misses deadlines in the field. + +`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()`: 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 + about 16 milliseconds up. +* `getLatencyPercentileNanos(0.9999)`: a percentile read off the histogram, as the + upper bound of its bucket. +* `toString()`: everything above on one line, for a log. + +=== What the platform supports + +`Realtime.getInstance().isSupported(feature)` answers for each part on its own: + +[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 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. +| `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 +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'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 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 can't avoid them yet. ++ +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 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. + +*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.* Don't do these in `cycle()`: + +* Throw exceptions. Building a stack trace allocates. +* Initialize a class for the first time. Touch it in `setup()`. +* 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'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, +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 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 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's 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`. + +=== 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"] +|=== +| 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. + +=== 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/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/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 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..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,123 +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 -: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 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/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/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/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 d4e01664397..c12af55f8d6 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() { } @@ -537,6 +543,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() @@ -1230,6 +1245,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"; + } +} 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 8e7986553a8..321a7618bd0 100644 Binary files a/scripts/initializr/common/src/main/resources/common.zip and b/scripts/initializr/common/src/main/resources/common.zip differ 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"); diff --git a/scripts/sync-initializr-launchers.py b/scripts/sync-initializr-launchers.py index 715aaf7384c..7172074ebc0 100644 --- a/scripts/sync-initializr-launchers.py +++ b/scripts/sync-initializr-launchers.py @@ -9,13 +9,19 @@ archetype = root / 'maven/cn1app-archetype/src/main/resources/archetype-resources' archive = root / 'scripts/initializr/common/src/main/resources/common.zip' # Git may check text files out as CRLF on Windows. ZIP payloads use canonical -# LF for Unix launchers and CRLF for the hybrid Windows Maven wrapper. +# LF for Unix launchers and CRLF for every Windows one. A .bat with LF endings +# breaks cmd.exe's goto: it finds labels by re-reading the file in 512-byte +# blocks and, without CR, can miss one that straddles a block boundary -- adding +# the device targets moved a label onto one and `build.bat javascript` ran the +# android target instead. expected = {name: (archetype / name).read_bytes().replace(b'\r\n', b'\n') for name in ['build.sh', 'build.bat', 'run.sh', 'run.bat', 'mvnw', 'mvnw.cmd']} expected['mvnw.cmd'] = expected['mvnw.cmd'].replace(b'\n', b'\r\n') +for name in ('build.bat', 'run.bat'): + expected[name] = expected[name].replace(b'\n', b'\r\n') # Preserve the historical Initializr alias; the native Windows target is unchanged. expected['build.sh'] = expected['build.sh'].replace(b'function linux_device {', b'function uwp {\n windows_device\n}\nfunction linux_device {') -expected['build.bat'] = expected['build.bat'].replace(b':linux_device\n', b':uwp\ngoto :windows_device\n\n:linux_device\n') +expected['build.bat'] = expected['build.bat'].replace(b':linux_device\r\n', b':uwp\r\ngoto :windows_device\r\n\r\n:linux_device\r\n') with zipfile.ZipFile(archive) as source: stale = [name for name, data in expected.items() if source.read(name) != data] if stale and '--write' in sys.argv: diff --git a/vm/ByteCodeTranslator/src/cn1_globals.h b/vm/ByteCodeTranslator/src/cn1_globals.h index cce9a22c421..01b5b28ceda 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,32 @@ 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_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; + 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 +3978,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 +4934,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..fee46bb6a5f 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.m +++ b/vm/ByteCodeTranslator/src/cn1_globals.m @@ -744,6 +744,56 @@ 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; +} + +// 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 @@ -5836,7 +5886,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 @@ -6043,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 @@ -6307,7 +6358,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 +6496,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 +6624,24 @@ 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 + // 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 @@ -8952,7 +9030,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 +12081,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 +12117,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 +16011,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 +21396,231 @@ 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); + } +} + +// 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) { + return; + } +#ifdef CN1_VIRTUAL_THREADS + threadStateData->rtSavedBibopCur = threadStateData->bibopCur; + threadStateData->bibopCur = cn1RtNoPages; +#endif + threadStateData->rtAllocTrap = JAVA_TRUE; +} + +static void cn1RtTrapOff(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 +} + +// 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; + 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. +// 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--; + cn1RtTrapOff(threadStateData); + o = codenameOneGcMalloc(threadStateData, size, parent); + cn1RtTrapOn(threadStateData); + 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"; + cn1RtTrapOff(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; + } + 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; + } + 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..d5135087464 --- /dev/null +++ b/vm/ByteCodeTranslator/src/cn1_realtime_impl.h @@ -0,0 +1,406 @@ +/* + * 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'; +} + +/* 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 + * 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++; + 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. 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; + 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; + threadStateData->rtPriority = priority; + } + } + 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; + } + cn1RtRestoreCpu(); +#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..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); } @@ -2800,11 +2803,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 +2845,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 +2872,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 +2978,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 +3127,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 +4004,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..34695805429 --- /dev/null +++ b/vm/backend/demo/rtcheck/com/demo/RtCheck.java @@ -0,0 +1,292 @@ +/* + * 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; + } + } + + /// 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) { + } + } + + /// 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; + 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; + } + } + + // 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; + } + } + } + + // 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); + } +} 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); + } +}