[Feat] (Plugin, P11 B1): the broker hands the helper the core the server reserves for the session's apply thread and the helper execs the program through taskset on every other core (--keep-affinity opts out), so a client the scheduler would put there is not starved

This commit is contained in:
2026-09-29 14:47:31 -04:00
parent 1abddf0d00
commit 54e7c92cf4
6 changed files with 135 additions and 16 deletions
@@ -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 {
@@ -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:<control>,<aux>},
* {@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=<token> CLASSPATH=<server APK> app_process / "
+ ExternalClientHelper.class.getName()
+ " [--server <package>] [--timeout-ms <n>] [--] <program> [args...]");
+ " [--server <package>] [--timeout-ms <n>] [--keep-affinity] [--] <program> [args...]");
System.exit(EXIT_USAGE);
}
@@ -240,7 +267,8 @@ public final class ExternalClientHelper {
return pfd.detachFd();
}
private static void exec(String path, List<String> argv, int control, int aux, String token) throws Exception {
private static void exec(String path, List<String> argv, int control, int aux, String token, long cpuMask)
throws Exception {
Map<String, String> 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<String> envp = new ArrayList<>();
for (Map.Entry<String, String> 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<String> 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");
}
@@ -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<String, String> 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);
@@ -461,7 +461,8 @@ public final class ServerControlActivity extends Activity {
text.append(launchCommand(usable ? token : "<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=<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());
@@ -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