mirror of
https://github.com/goodpuppies/petplay.git
synced 2026-10-23 09:00:43 +02:00
Render thread (panorama, nice 0, load ~27 on 16 cores): - Per-run UI panel slots reused across eyes, one per-frame panel data texture, and cached struct bytes for shader uniforms and draw matrices: renderRaythreeFrame 4.16 -> 2.27 ms. - The worker skips publishing scenes whose fingerprint (overlaySceneFingerprint) matches the last one sent, with a 1 s keepalive. Anchored items now keep bit-identical local matrices while nothing moves: built from the local transform chain, or held through rounding noise where uikit derives matrixWorld from layout. Decode on the render thread 1.78 -> 0.04 ms/frame when idle; render thread busy 6.4 -> 4.2 ms/frame. Diagnostics: a frame timeline probe with schedstat in overlayRenderOwner, CPU profiling with an interval, dropLateFrames, renderThreadNice, split presenter finish/wait/publish, a native WaitFrameSync phase probe and a compositor frame-timing probe. asyncFrameWait, projectionPacing and deferDecode are projection experiments that did not help. Also includes the other agent's pending overlay scene buffer, wire and ovrdriver lab work. Agent: Claude Code 2.1.296; model claude-opus-5-5; thread 941be8c7-aca1-4664-b284-7cc0eed69e21
158 lines
6.2 KiB
TypeScript
158 lines
6.2 KiB
TypeScript
/**
|
|
* ananicy-cpp v1.2.0 prefers argv[0] over comm. Replace only that original argument,
|
|
* never the remaining CLI arguments used by server discovery and restart tools.
|
|
* Four bytes fit the source CLI's shortest usual argv[0], "deno".
|
|
*/
|
|
export const LINUX_SCHEDULING_ARGV0 = "ppvr";
|
|
|
|
function setLinuxSchedulingArgv0(): void {
|
|
const cmdline = Deno.readFileSync("/proc/self/cmdline");
|
|
const end = cmdline.indexOf(0);
|
|
const bytes = new TextEncoder().encode(LINUX_SCHEDULING_ARGV0);
|
|
if (end < bytes.length) throw new Error("argv[0] has no room for the scheduling identity");
|
|
if (new TextDecoder().decode(cmdline.subarray(0, end)) === LINUX_SCHEDULING_ARGV0) return;
|
|
const stat = Deno.readTextFileSync("/proc/self/stat");
|
|
// Fields after comm start at field 3. Fields 48/49 bound the original argv allocation.
|
|
const fields = stat.slice(stat.lastIndexOf(") ") + 2).trim().split(/\s+/);
|
|
const start = Number(fields[45]);
|
|
const limit = Number(fields[46]);
|
|
const size = end + 1;
|
|
if (
|
|
!Number.isSafeInteger(start) || start <= 0 || !Number.isSafeInteger(limit) ||
|
|
limit < start + size
|
|
) throw new Error("Cannot validate the original argv[0] allocation");
|
|
const memory = Deno.openSync("/proc/self/mem", { read: true, write: true });
|
|
try {
|
|
memory.seekSync(start, Deno.SeekMode.Start);
|
|
const original = new Uint8Array(size);
|
|
if (memory.readSync(original) !== size || original.some((v, i) => v !== cmdline[i])) {
|
|
throw new Error("Original argv[0] does not match its kernel-reported allocation");
|
|
}
|
|
const replacement = new Uint8Array(size);
|
|
replacement.set(bytes);
|
|
memory.seekSync(start, Deno.SeekMode.Start);
|
|
if (memory.writeSync(replacement) !== size) throw new Error("Incomplete argv[0] update");
|
|
} finally {
|
|
memory.close();
|
|
}
|
|
}
|
|
|
|
/** Linux process identity and recovery from inherited background scheduling. */
|
|
export type LinuxSchedulingStatus = {
|
|
name: string;
|
|
renamed: boolean;
|
|
argv0Named: boolean;
|
|
idleThreads: number;
|
|
correctedThreads: number;
|
|
deniedThreads: number;
|
|
error?: string;
|
|
};
|
|
|
|
/**
|
|
* Only this process is touched. Never request realtime scheduling or lower an existing boost.
|
|
* A system policy may be needed to undo inherited positive niceness (see resources/linux).
|
|
*/
|
|
let settlingScheduled = false;
|
|
|
|
export function initializeLinuxScheduling(name = "petplay"): LinuxSchedulingStatus {
|
|
const result = applyLinuxScheduling(name);
|
|
if (Deno.build.os === "linux" && !settlingScheduled) {
|
|
settlingScheduled = true;
|
|
// A pre-start ananicy event may already hold the old "deno" classification.
|
|
// Bounded startup rechecks catch that queued write; no permanent polling loop.
|
|
for (const delay of [250, 1_000, 3_000]) {
|
|
const timer = setTimeout(() => applyLinuxScheduling(name), delay);
|
|
Deno.unrefTimer(Number(timer));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function applyLinuxScheduling(name: string): LinuxSchedulingStatus {
|
|
const status: LinuxSchedulingStatus = {
|
|
name,
|
|
renamed: false,
|
|
argv0Named: false,
|
|
idleThreads: 0,
|
|
correctedThreads: 0,
|
|
deniedThreads: 0,
|
|
};
|
|
if (Deno.build.os !== "linux") return status;
|
|
if (!/^[a-z][a-z0-9-]{0,14}$/.test(name)) throw new Error("Invalid Linux process name");
|
|
try {
|
|
// Keep a readable leader name even when called from a hot-reloaded worker.
|
|
Deno.writeTextFileSync(`/proc/self/task/${Deno.pid}/comm`, name);
|
|
status.renamed = true;
|
|
setLinuxSchedulingArgv0();
|
|
status.argv0Named = true;
|
|
const libc = Deno.dlopen("libc.so.6", {
|
|
sched_getscheduler: { parameters: ["i32"], result: "i32" },
|
|
sched_setscheduler: { parameters: ["i32", "i32", "buffer"], result: "i32" },
|
|
getpriority: { parameters: ["i32", "u32"], result: "i32" },
|
|
setpriority: { parameters: ["i32", "u32", "i32"], result: "i32" },
|
|
});
|
|
try {
|
|
const param = new Int32Array([0]);
|
|
for (const task of Deno.readDirSync("/proc/self/task")) {
|
|
const tid = Number(task.name);
|
|
if (!Number.isInteger(tid) || tid <= 0) continue;
|
|
const policy = libc.symbols.sched_getscheduler(tid);
|
|
// Preserve intentional batch/realtime threads; normalize only idle/normal.
|
|
if (policy !== 0 && policy !== 5) continue;
|
|
let denied = false;
|
|
if (policy === 5) {
|
|
status.idleThreads++;
|
|
if (libc.symbols.sched_setscheduler(tid, 0, param) === 0) status.correctedThreads++;
|
|
else denied = true;
|
|
}
|
|
if (
|
|
libc.symbols.getpriority(0, tid) > 0 &&
|
|
libc.symbols.setpriority(0, tid, 0) !== 0
|
|
) denied = true;
|
|
// An exiting thread may fail these calls; count only threads that still exist.
|
|
if (denied) {
|
|
try {
|
|
Deno.statSync(`/proc/self/task/${tid}`);
|
|
status.deniedThreads++;
|
|
} catch {
|
|
// Thread exited during startup.
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
libc.close();
|
|
}
|
|
} catch (error) {
|
|
status.error = error instanceof Error ? error.message : String(error);
|
|
}
|
|
if (status.error || status.deniedThreads > 0) {
|
|
console.warn(
|
|
`[petplay] Linux scheduling: ${JSON.stringify(status)}; inherited priority could not ` +
|
|
"be fully restored. See resources/linux/README.md for the optional ananicy rule.",
|
|
);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Set the calling thread's niceness (Linux; needs RLIMIT_NICE for negative values). For a
|
|
* latency-critical thread on a loaded machine. Returns the niceness read back, or an error.
|
|
*/
|
|
export function setCurrentThreadNice(nice: number): { tid: number; nice: number; error?: string } {
|
|
if (Deno.build.os !== "linux") return { tid: 0, nice: 0, error: "not linux" };
|
|
if (!Number.isInteger(nice) || nice < -20 || nice > 19) throw new Error("Invalid niceness");
|
|
const libc = Deno.dlopen("libc.so.6", {
|
|
gettid: { parameters: [], result: "i32" },
|
|
getpriority: { parameters: ["i32", "u32"], result: "i32" },
|
|
setpriority: { parameters: ["i32", "u32", "i32"], result: "i32" },
|
|
});
|
|
try {
|
|
const tid = libc.symbols.gettid();
|
|
const failed = libc.symbols.setpriority(0, tid, nice) !== 0;
|
|
const now = libc.symbols.getpriority(0, tid);
|
|
return failed ? { tid, nice: now, error: `setpriority(${nice}) denied` } : { tid, nice: now };
|
|
} finally {
|
|
libc.close();
|
|
}
|
|
}
|