diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/ExternalClientBroker.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/ExternalClientBroker.java index 96a5b934..efee6d41 100644 --- a/android-plugin/app/src/main/java/top/mobilegl/plugin/ExternalClientBroker.java +++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/ExternalClientBroker.java @@ -49,7 +49,10 @@ public final class ExternalClientBroker extends BroadcastReceiver { public static final String KEY_VERSION = "version"; public static final int PROTOCOL_VERSION = 1; - /** The broker's answer: int version, then the control and aux descriptors. */ + /** + * The broker's answer: int version, the control and aux descriptors, then long applyCore - the cpu + * mask the server's `auto` policy reserves for this session's apply thread (0 = none). + */ public static final int REPLY_FDS = IBinder.FIRST_CALL_TRANSACTION; /** The broker's refusal: String code, String message. */ public static final int REPLY_REFUSED = IBinder.FIRST_CALL_TRANSACTION + 1; @@ -166,9 +169,12 @@ public final class ExternalClientBroker extends BroadcastReceiver { data.writeInt(PROTOCOL_VERSION); control.writeToParcel(data, 0); aux.writeToParcel(data, 0); + // The core the server reserves for this session's apply thread (0 = none): the helper keeps + // the client off it. Appended after the descriptors; a reader that stops before it is fine. + data.writeLong(target.applyCore); boolean delivered = callback.transact(REPLY_FDS, data, null, IBinder.FLAG_ONEWAY); Log.i(TAG, "brokered a connection pair to " + target.endpoint + " for a client (delivered=" - + delivered + ")"); + + delivered + ", apply core 0x" + Long.toHexString(target.applyCore) + ")"); } catch (Exception error) { Log.e(TAG, "could not hand the connection pair to the client", error); } finally { diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/ExternalClientHelper.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/ExternalClientHelper.java index 010e7913..a3ddd0c9 100644 --- a/android-plugin/app/src/main/java/top/mobilegl/plugin/ExternalClientHelper.java +++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/ExternalClientHelper.java @@ -44,8 +44,10 @@ import java.util.Map; * Two connected sockets: it clears their close-on-exec flag and {@code execve}s the program with * {@code MOBILEGL_TRANSPORT=spawn}, {@code MOBILEGL_IPC_CONTROL=fd:,}, * {@code MOBILEGL_IPC_DATA=shm} and {@code MOBILEGL_IPC_TOKEN} (the session's Hello carries it too). - * A refusal: it prints the broker's name for it and exits with that refusal's code. No answer at - * all within the timeout: it says so and exits {@value #EXIT_NO_ANSWER}. + * When the broker names a cpu core the server reserves for the session's apply thread, the program is + * exec'd through {@code taskset} on every other core (see {@link #clientMask}; {@code --keep-affinity} + * opts out). A refusal: it prints the broker's name for it and exits with that refusal's code. No + * answer at all within the timeout: it says so and exits {@value #EXIT_NO_ANSWER}. */ public final class ExternalClientHelper { private static final String TAG = "MobileGLHelper"; @@ -64,8 +66,26 @@ public final class ExternalClientHelper { public static final int EXIT_EXEC_FAILED = 73; private static final long DEFAULT_TIMEOUT_MS = 10000; + private static final String TASKSET = "/system/bin/taskset"; private static final Object answered = new Object(); private static boolean done = false; + private static volatile boolean keepAffinity = false; + + /** + * Every cpu this device counts except the server's reserved apply core, or 0 (no mask) when none is + * reserved or nothing would be left. Why: the server pins a dialled-in shared-segment client's + * apply thread to that core and spins there, and a client the scheduler also puts there (an + * adb-shell one on the Redmi) is starved: rd12 27 fps on shared memory against 51 over tcp; + * kept off it, the client takes the other prime core (CONTRACT-P11 B1). The cost: the whole program + * loses that one core. --keep-affinity opts out. + */ + private static long clientMask(long applyCore) { + if (applyCore == 0) return 0; + int count = Math.min(64, Math.max(1, Runtime.getRuntime().availableProcessors())); + long online = count >= 64 ? ~0L : (1L << count) - 1; + long mask = online & ~applyCore; + return mask == 0 || mask == online ? 0 : mask; + } public static void main(String[] args) { Looper.prepareMainLooper(); @@ -80,6 +100,8 @@ public final class ExternalClientHelper { break; } else if ("--server".equals(arg) && i + 1 < args.length) { serverPackage = args[++i]; + } else if ("--keep-affinity".equals(arg)) { + keepAffinity = true; } else if ("--timeout-ms".equals(arg) && i + 1 < args.length) { try { timeoutMs = Long.parseLong(args[++i]); @@ -124,11 +146,16 @@ public final class ExternalClientHelper { int version = data.readInt(); ParcelFileDescriptor control = ParcelFileDescriptor.CREATOR.createFromParcel(data); ParcelFileDescriptor aux = ParcelFileDescriptor.CREATOR.createFromParcel(data); + long applyCore = data.dataAvail() >= 8 ? data.readLong() : 0; int controlFd = inheritable(control); int auxFd = inheritable(aux); - Log.i(TAG, "broker v" + version + " handed over fd:" + controlFd + "," + auxFd + "; exec " - + programPath); - exec(programPath, argv, controlFd, auxFd, presented); + long mask = keepAffinity ? 0 : clientMask(applyCore); + say("broker v" + version + " handed over fd:" + controlFd + "," + auxFd + + (mask != 0 ? "; the server's apply core 0x" + Long.toHexString(applyCore) + + " reserved, the program runs on cpu mask 0x" + Long.toHexString(mask) + : "; no cpu mask applied") + + "; exec " + programPath); + exec(programPath, argv, controlFd, auxFd, presented, mask); } catch (Throwable error) { say("could not exec " + programPath + ": " + error); finish(EXIT_EXEC_FAILED); @@ -195,7 +222,7 @@ public final class ExternalClientHelper { System.err.println(PREFIX + problem); System.err.println("usage: MOBILEGL_IPC_TOKEN= CLASSPATH= app_process / " + ExternalClientHelper.class.getName() - + " [--server ] [--timeout-ms ] [--] [args...]"); + + " [--server ] [--timeout-ms ] [--keep-affinity] [--] [args...]"); System.exit(EXIT_USAGE); } @@ -240,7 +267,8 @@ public final class ExternalClientHelper { return pfd.detachFd(); } - private static void exec(String path, List argv, int control, int aux, String token) throws Exception { + private static void exec(String path, List argv, int control, int aux, String token, long cpuMask) + throws Exception { Map env = new LinkedHashMap<>(); String[] inherited = Os.environ(); if (inherited != null) { @@ -257,7 +285,18 @@ public final class ExternalClientHelper { env.put("MOBILEGL_IPC_TOKEN", token); List envp = new ArrayList<>(); for (Map.Entry entry : env.entrySet()) envp.add(entry.getKey() + "=" + entry.getValue()); - Os.execve(path, argv.toArray(new String[0]), envp.toArray(new String[0])); + if (cpuMask != 0 && new File(TASKSET).canExecute()) { + // No sched_setaffinity from Java: toybox taskset sets the mask and execvp's the program, + // which inherits it (and the two descriptors, whose close-on-exec flag is already clear). + List wrapped = new ArrayList<>(); + wrapped.add(TASKSET); + wrapped.add(Long.toHexString(cpuMask)); + wrapped.add(path); + wrapped.addAll(argv.subList(1, argv.size())); + Os.execve(TASKSET, wrapped.toArray(new String[0]), envp.toArray(new String[0])); + } else { + Os.execve(path, argv.toArray(new String[0]), envp.toArray(new String[0])); + } throw new IllegalStateException("execve returned"); } diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/MobileGLServerService.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/MobileGLServerService.java index 1ed80bd3..56e43b56 100644 --- a/android-plugin/app/src/main/java/top/mobilegl/plugin/MobileGLServerService.java +++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/MobileGLServerService.java @@ -40,14 +40,34 @@ public final class MobileGLServerService extends Service { final String endpoint; final String token; final Process process; + // The core the server's `auto` policy gives a dialled-in shared-segment client's apply thread + // (ServerEnvironment.reservedApplyCore); the broker hands it to the helper, which keeps the + // client off it. 0 = none reserved. + final long applyCore; - BrokerTarget(String endpoint, String token, Process process) { + BrokerTarget(String endpoint, String token, Process process, long applyCore) { this.endpoint = endpoint; this.token = token; this.process = process; + this.applyCore = applyCore; } } + /** cpuinfo_max_freq of every cpu this process can count (0 where unreadable). */ + private static long[] cpuMaxFrequenciesKHz() { + int count = Math.min(64, Math.max(1, Runtime.getRuntime().availableProcessors())); + long[] frequencies = new long[count]; + for (int cpu = 0; cpu < count; cpu++) { + File file = new File("/sys/devices/system/cpu/cpu" + cpu + "/cpufreq/cpuinfo_max_freq"); + try (BufferedReader reader = new BufferedReader(new java.io.FileReader(file))) { + frequencies[cpu] = Long.parseLong(reader.readLine().trim()); + } catch (Exception unreadable) { + frequencies[cpu] = 0; + } + } + return frequencies; + } + // Set when a supervisor the broker can reach is running, cleared when it stops. The broker runs // in this same process (:mglsrv), so a broadcast that finds null here finds no server. private static volatile BrokerTarget brokerTarget; @@ -135,6 +155,10 @@ public final class MobileGLServerService extends Service { if (endpoint == null) endpoint = "tcp://127.0.0.1:40613"; final String envExtra = intent == null ? null : intent.getStringExtra("env"); final String token = intent == null ? null : intent.getStringExtra("token"); + java.util.Map requested = new java.util.HashMap<>(); + ServerEnvironment.apply(requested, envExtra); + final long applyCore = ServerEnvironment.reservedApplyCore(cpuMaxFrequenciesKHz(), + requested.get("MOBILEGL_IPC_SERVER_AFFINITY")); try { supervisor = startSupervisor(endpoint, envExtra, token, "mgl-supervisor-log", startId, true); // P11 B1: the broker's target. A unix endpoint is reachable by this app as it is (a @@ -145,14 +169,16 @@ public final class MobileGLServerService extends Service { new java.security.SecureRandom().nextLong() & 0xffffffffL); brokerSupervisor = startSupervisor(privateName, envExtra, token, "mgl-broker-supervisor-log", startId, false); - brokerTarget = new BrokerTarget(privateName, token, brokerSupervisor); + brokerTarget = new BrokerTarget(privateName, token, brokerSupervisor, applyCore); Log.i(TAG, "same-device external clients: broker listener " + privateName - + " beside the TCP-only supervisor on " + endpoint); + + " beside the TCP-only supervisor on " + endpoint + "; apply core reserved for them 0x" + + Long.toHexString(applyCore)); } else { String unix = endpoint.startsWith("@") || endpoint.startsWith("/") ? endpoint : new File(getFilesDir(), endpoint).getAbsolutePath(); - brokerTarget = new BrokerTarget(unix, token, supervisor); - Log.i(TAG, "same-device external clients: the broker connects to " + unix); + brokerTarget = new BrokerTarget(unix, token, supervisor, applyCore); + Log.i(TAG, "same-device external clients: the broker connects to " + unix + + "; apply core reserved for them 0x" + Long.toHexString(applyCore)); } } catch (Exception error) { Log.e(TAG, "cannot start TCP supervisor", error); diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/ServerControlActivity.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/ServerControlActivity.java index 7216e844..f2f4d2bd 100644 --- a/android-plugin/app/src/main/java/top/mobilegl/plugin/ServerControlActivity.java +++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/ServerControlActivity.java @@ -461,7 +461,8 @@ public final class ServerControlActivity extends Activity { text.append(launchCommand(usable ? token : "")).append("\n\n") .append("Broker: ").append(getPackageName()).append('/').append(ExternalClientBroker.class.getName()) .append("\nThe token travels in the environment (MOBILEGL_IPC_TOKEN, or MOBILEGL_IPC_TOKEN_FILE=)") - .append(", never on the command line. The program runs over shared memory; the server keeps") + .append(", never on the command line. The program runs over shared memory, off the one cpu core") + .append(" the server's render thread takes (--keep-affinity to opt out); the server keeps") .append(" running as a foreground service. Battery: set both this app and the client's app to") .append(" \"No restrictions\"."); launchCommandView.setText(text.toString()); diff --git a/android-plugin/app/src/main/java/top/mobilegl/plugin/ServerEnvironment.java b/android-plugin/app/src/main/java/top/mobilegl/plugin/ServerEnvironment.java index 9deba747..cb0dc480 100644 --- a/android-plugin/app/src/main/java/top/mobilegl/plugin/ServerEnvironment.java +++ b/android-plugin/app/src/main/java/top/mobilegl/plugin/ServerEnvironment.java @@ -81,6 +81,34 @@ public final class ServerEnvironment { return edits; } + /** + * P11 B1 (CONTRACT-P11 B1): the core the server's `auto` policy reserves for the apply thread of a + * dialled-in shared-segment client - MG_Remote/Server/ApplyThreadPolicy.h ReservedApplyCore over + * ServerLoop.cpp's DetectBigCoreMask, mirrored here so the broker can tell the helper which core to + * keep the client off. {@code maxFreqKHz[i]} is cpu i's cpuinfo_max_freq (0 = unreadable, which + * like the native probe gives no answer). A cpu is big when its ceiling is within 15% of the peak; + * the reserved core is the LOWEST big one, and only when the big set is a strict subset of at least + * two. 0 = none. {@code affinityOverride} is the server's MOBILEGL_IPC_SERVER_AFFINITY: anything + * but unset / empty / {@code auto} is an operator's own placement, and then nothing is reserved. + */ + public static long reservedApplyCore(long[] maxFreqKHz, String affinityOverride) { + if (affinityOverride != null && !affinityOverride.isEmpty() && !affinityOverride.equals("auto")) return 0; + int count = Math.min(64, maxFreqKHz.length); + long peak = 0; + for (int cpu = 0; cpu < count; cpu++) { + if (maxFreqKHz[cpu] <= 0) return 0; + peak = Math.max(peak, maxFreqKHz[cpu]); + } + if (count == 0 || peak == 0) return 0; + long big = 0; + for (int cpu = 0; cpu < count; cpu++) { + if (maxFreqKHz[cpu] >= peak * 85 / 100) big |= 1L << cpu; + } + long online = count >= 64 ? ~0L : (1L << count) - 1; + if ((big & online) == online || Long.bitCount(big) < 2) return 0; + return Long.lowestOneBit(big); + } + /** * Aspect-fit (letterbox / pillarbox): the largest {@code bufferWidth:bufferHeight} rectangle * inside a {@code containerWidth x containerHeight} view, as {width, height}. {-1, -1} (fill the diff --git a/tools/trace_replay/ServerEnvironmentTest.java b/tools/trace_replay/ServerEnvironmentTest.java index e70308fd..ea443b4f 100644 --- a/tools/trace_replay/ServerEnvironmentTest.java +++ b/tools/trace_replay/ServerEnvironmentTest.java @@ -94,6 +94,25 @@ public final class ServerEnvironmentTest { expect("an unmeasured view fills the view", java.util.Arrays.toString(ServerEnvironment.aspectFit(0, 0, 640, 480)), "[-1, -1]"); + // ---- P11 B1: the apply core the broker tells the helper to keep the client off ------------------ + // The Java mirror of MG_Remote/Server/ApplyThreadPolicy.h ReservedApplyCore over DetectBigCoreMask. + long[] redmi = {3532800, 3532800, 3532800, 3532800, 3532800, 3532800, 4320000, 4320000}; + expect("the Redmi (cpu6-7 prime): the LOWEST prime core, cpu6", + ServerEnvironment.reservedApplyCore(redmi, null), 0x40L); + expect("`auto` and an empty override are the policy's own", ServerEnvironment.reservedApplyCore(redmi, "auto"), + 0x40L); + expect("an operator's affinity (a mask or off) reserves nothing", + ServerEnvironment.reservedApplyCore(redmi, "0x80") + ServerEnvironment.reservedApplyCore(redmi, "off"), + 0L); + expect("a symmetric machine has no prime pair to split", + ServerEnvironment.reservedApplyCore(new long[]{2000000, 2000000, 2000000, 2000000}, ""), 0L); + expect("one prime core is not a pair", + ServerEnvironment.reservedApplyCore(new long[]{1800000, 1800000, 1800000, 3000000}, null), 0L); + expect("within 15% of the peak is big: cpu2 and cpu3 of {1.0, 1.0, 2.6, 3.0} GHz, the lowest is cpu2", + ServerEnvironment.reservedApplyCore(new long[]{1000000, 1000000, 2600000, 3000000}, null), 0x4L); + expect("an unreadable ceiling gives no answer, like the native probe", + ServerEnvironment.reservedApplyCore(new long[]{3532800, 0, 4320000, 4320000}, null), 0L); + if (failures != 0) { System.err.println("ServerEnvironmentTest: " + failures + " failure(s)"); System.exit(1);