perf: answer chown and bound thermal paths through the proot fast path (#2203)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-11-07 01:08:06 -05:00
1 parent 18c7d56409
commit aba55031c3
7 files changed
+267 -3

No files matched your search

@@ -377,3 +377,77 @@ diagnostics remained enabled. Temporary probe files, executables and obsolete
partial staging were removed after validation. Original effective mount options
were restored; no app-specific Magisk root policy remained. The primary checkout's
unrelated `SessionService.kt` modification was preserved.
## Install phases on current main (2026-11-07)
A fresh install of 1000xRESIST (app 1675830, 2,216 files, 7.45 GB) to the same Thor SD
card, on the build from main at the time (`0652b0d`), with the fast path and storage
shim from the earlier work. Phase times come from Steam's `content_log.txt`.
| Run | Reserving space | Download and stage | Commit | Total |
|---|---|---|---|---|
| Clean | 78 s | 25 min 01 s | 205 s | 29 min 46 s |
| Profiled | 79 s | 18 min 43 s | 146 s | 22 min 28 s |
Reserving space was 404 s on 2026-10-04 and is now about 36 ms per file, which is the
file-creation floor measured above (25-36 ms). The two runs differ by 7 minutes because
the download phase follows the network: Steam logged 12 to 83 Mbps for the whole phase.
Raw sequential write to the card with `dd conv=fsync` was 15 MB/s (120 Mbps), so the
card is not the limit during most of the download. The run-to-run spread is larger than
any change a path-translation fix could make, so install totals are not used to judge
the fast path changes below.
Kernel stacks of Steam's blocked threads, sampled every 0.2 s from `/proc/<pid>/task/*`:
- **Commit:** every sampled thread was inside `renameat`, waiting on synchronous
directory entry writes (`exfat_rename`, `exfat_init_ext_entry`, `exfat_remove_entries`,
`exfat_free_dentry_set`, all through `__sync_dirty_buffer`). That is the `dirsync`
mount option, at about 65 to 92 ms per file. PRoot is not involved in that time.
- **Download:** blocked threads were in `pwrite64`, in `exfat_get_block`,
`exfat_alloc_cluster` and `cont_write_begin`, which allocate clusters as the unreserved
files grow.
PRoot's own handler time during the reserving phase (profiler, 1 ms slow threshold) was
7.3 s of the 79 s, of which 6.3 s was `openat` handler visits. Almost all of those came
from the HUD reading the three bound thermal files (22,326 fast path misses logged
against `/sys/class/thermal/thermal_zone{22,26,28}/temp`), not from Steam. `fchownat`
accounted for 2,795 visits at about 70 us. Steam's own calls were answered by the fast
path.
### Fast path changes
- `fchownat`, `chown` and `lchown` are answered in the fast path, through the libc
wrappers and the patched `svc` sites. The site cache name moves to `svc-v3` so cached
site lists from older builds are rebuilt.
- A path that names a bound file exactly, spelled through a symlinked directory
(`/sys/class/thermal/thermal_zone28/temp`, where `thermal_zone28` is a symlink), now
resolves to the bind's host path. Before, any path through a symlink with a binding
beneath it fell back to PRoot. Paths with `..`, a trailing slash or any other ending
still fall back.
Same workloads on the Thor, stock library against the new one (session started with
`droiddeckctl override libblfastpath.so`, PRoot profiler on):
| Workload | Stock | New |
|---|---|---|
| `chown` of 2,000 files, internal storage | 40-50 us each | 6 us each |
| `chown` of 2,000 files, SD card | 61-62 us each | 9 us each |
| Open of the three bound thermal files, 6,000 opens | 128-133 us each | 24-25 us each |
| PRoot `openat` handler visits for both workloads | 15,496 (1,205 ms) | 7,458 (366 ms) |
| PRoot `fchownat` handler visits | 6,873 (288 ms) | 0 |
`bench/chown.py` compares 87 `chown` results (paths, links, dangling and looping links,
descriptors, directory descriptors, foreign owners, trailing slashes) with the fast path
on and with `PROOT_FP_OFF=1`; they were identical. The thermal paths were compared the
same way for every spelling the HUD can use (absolute, doubled slashes, `.` and `..`
components, relative to a working directory or descriptor on the parent directory) and
matched. They differ only when the working directory or descriptor is inside the
symlinked directory, or the path uses its canonical `/sys/devices/virtual/...` name: PRoot
returns the bound file there and the fast path returns the real node. That difference
existed before these changes, and the HUD does not read those spellings.
What this does not change: the commit and the file creation costs above are the card's
synchronous directory updates, so SD installs stay bound by them (about 4 to 5 minutes per
2,000 files of reserving plus commit on this card). Removing those costs needs fewer
directory operations per file, not faster path translation.
+98
View File
@@ -0,0 +1,98 @@
import errno
import os
import shutil
import sys
import time
me = (os.getuid(), os.getgid())
def call(fn):
try:
return repr(fn())
except OSError as e:
return "E" + errno.errorcode.get(e.errno, str(e.errno))
def run(base, links):
shutil.rmtree(base, ignore_errors=True)
os.makedirs(base + "/d")
open(base + "/f", "w").close()
open(base + "/d/g", "w").close()
if links:
os.symlink("f", base + "/lf")
os.symlink("nowhere", base + "/dl")
os.symlink("d", base + "/ld")
os.symlink("loop2", base + "/loop1")
os.symlink("loop1", base + "/loop2")
uid = lambda p, follow=True: os.stat(p, follow_symlinks=follow).st_uid
cases = [
("path", lambda: os.chown(base + "/f", *me)),
("keep", lambda: os.chown(base + "/f", -1, -1)),
("uid only", lambda: os.chown(base + "/f", me[0], -1)),
("gid only", lambda: os.chown(base + "/f", -1, me[1])),
("foreign uid", lambda: os.chown(base + "/f", me[0] + 1, -1)),
("foreign gid", lambda: os.chown(base + "/f", -1, me[1] + 12345)),
("dir", lambda: os.chown(base + "/d", *me)),
("dir trailing slash", lambda: os.chown(base + "/d/", *me)),
("file trailing slash", lambda: os.chown(base + "/f/", *me)),
("missing", lambda: os.chown(base + "/nope", *me)),
("missing parent", lambda: os.chown(base + "/nope/x", *me)),
("under file", lambda: os.chown(base + "/f/x", *me)),
("empty", lambda: os.chown("", *me)),
("dotdot", lambda: os.chown(base + "/d/../f", *me)),
("fd", lambda: (lambda fd: (os.chown(fd, *me), os.close(fd))[1])(os.open(base + "/f", os.O_RDONLY))),
("dirfd rel", lambda: (lambda fd: (os.chown("g", *me, dir_fd=fd), os.close(fd))[1])(os.open(base + "/d", os.O_RDONLY))),
("dirfd abs", lambda: (lambda fd: (os.chown(base + "/f", *me, dir_fd=fd), os.close(fd))[1])(os.open(base + "/d", os.O_RDONLY))),
("bad dirfd", lambda: os.chown("g", *me, dir_fd=9999)),
("file dirfd", lambda: (lambda fd: (os.chown("g", *me, dir_fd=fd), os.close(fd))[1])(os.open(base + "/f", os.O_RDONLY))),
("cwd rel", lambda: (os.chdir(base + "/d"), os.chown("g", *me))[1]),
("cwd dotdot", lambda: (os.chdir(base + "/d"), os.chown("../f", *me))[1]),
]
if links:
cases += [
("link follow", lambda: os.chown(base + "/lf", *me)),
("link nofollow", lambda: os.chown(base + "/lf", *me, follow_symlinks=False)),
("dangling follow", lambda: os.chown(base + "/dl", *me)),
("dangling nofollow", lambda: os.chown(base + "/dl", *me, follow_symlinks=False)),
("dirlink", lambda: os.chown(base + "/ld/g", *me)),
("dirlink slash", lambda: os.chown(base + "/ld/", *me)),
("loop", lambda: os.chown(base + "/loop1", *me)),
("loop nofollow", lambda: os.chown(base + "/loop1", *me, follow_symlinks=False)),
("link foreign", lambda: os.chown(base + "/lf", me[0] + 1, -1)),
]
out = []
for label, fn in cases:
os.chdir("/")
out.append(f" {label}: {call(fn)}")
os.chdir("/")
out.append(" owner after: " + call(lambda: (uid(base + "/f"), os.stat(base + "/f").st_gid)))
shutil.rmtree(base, ignore_errors=True)
return out
def bench(base, count):
shutil.rmtree(base, ignore_errors=True)
os.makedirs(base)
names = [f"{base}/f{i}" for i in range(count)]
for n in names:
open(n, "w").close()
start = time.perf_counter()
for n in names:
try:
os.chown(n, *me)
except OSError:
pass
elapsed = time.perf_counter() - start
shutil.rmtree(base, ignore_errors=True)
return elapsed
for base, links in (("/root/chown-eq", True), ("/tmp/chown-eq", True), ("/mnt/droiddeck-sd/chown-eq", False)):
print("==", base)
print("\n".join(run(base, links)))
if len(sys.argv) > 1:
for base in ("/root/chown-bench", "/mnt/droiddeck-sd/chown-bench"):
t = bench(base, int(sys.argv[1]))
print(f"BENCH {base} {int(sys.argv[1])} calls {t:.3f}s {t / int(sys.argv[1]) * 1e6:.0f}us each", file=sys.stderr)
+12
View File
@@ -0,0 +1,12 @@
import sys
import time
count = int(sys.argv[1])
paths = ["/sys/class/thermal/thermal_zone%d/temp" % n for n in (22, 26, 28)]
start = time.perf_counter()
for _ in range(count):
for p in paths:
with open(p) as f:
f.read()
elapsed = time.perf_counter() - start
print(f"{count * len(paths)} opens {elapsed:.3f}s {elapsed / (count * len(paths)) * 1e6:.0f}us each")
+30
View File
@@ -672,6 +672,18 @@ static int base_of(int dirfd, char *h) {
return gone(h, n) ? -1 : 0;
}
static long bound_literal(const char *literal, struct res *r) {
struct bind *bd;
if (reserved(literal) || bind_of(literal, &bd) < 0 || !bd || bd->glen != strlen(literal) || bd->glen < 2) return FP_SLOW;
if (snprintf(r->host, PATH_MAX, "%s", bd->host) >= PATH_MAX) return FP_SLOW;
if (sc(SYS_newfstatat, AT_FDCWD, (long)r->host, (long)&r->st, AT_SYMLINK_NOFOLLOW, 0) < 0 || S_ISLNK(r->st.st_mode)) return FP_SLOW;
snprintf(r->guest, PATH_MAX, "%s", literal);
r->exists = 1;
r->moving = 0;
r->magic = 0;
return 0;
}
static long resolve_in(const char *base, const char *path, int follow, struct res *r) {
char norm[PATH_MAX];
size_t o = 0;
@@ -707,6 +719,10 @@ static long resolve_in(const char *base, const char *path, int follow, struct re
unsigned h = hash_of(norm);
if (!cached(norm, h, r->guest)) {
w = walk(r, norm, 1, 0);
if (w == FP_SLOW && !trailing) {
*slash = '/';
return bound_literal(norm, r);
}
if (w < 0) return w;
if (w != GLUE && !S_ISDIR(r->st.st_mode)) return -ENOTDIR;
if (!r->moving) remember(norm, h, r->guest);
@@ -1204,6 +1220,20 @@ int fchmodat(int dirfd, const char *path, mode_t mode, int flags) {
}
int chmod(const char *path, mode_t mode) { return fchmodat(AT_FDCWD, path, mode, 0); }
static long fp_chown(int dirfd, const char *path, uid_t uid, gid_t gid, int flags) {
struct res r;
long rs = flags & ~AT_SYMLINK_NOFOLLOW ? FP_SLOW : resolve(dirfd, path, (flags & AT_SYMLINK_NOFOLLOW ? 0 : 1) | MAY_BE_UNSEEN, &r);
if (rs == 1) rs = -ENOENT;
if (rs == 0 || rs == UNSEEN) rs = sc(SYS_fchownat, AT_FDCWD, (long)r.host, (long)uid, (long)gid, flags);
return rs;
}
int fchownat(int dirfd, const char *path, uid_t uid, gid_t gid, int flags) {
REAL(fchownat, int (*)(int, const char *, uid_t, gid_t, int));
ANSWER(fp_chown(dirfd, path, uid, gid, flags), dirfd, path, real_fchownat(dirfd, path, uid, gid, flags));
}
int chown(const char *path, uid_t uid, gid_t gid) { return fchownat(AT_FDCWD, path, uid, gid, 0); }
int lchown(const char *path, uid_t uid, gid_t gid) { return fchownat(AT_FDCWD, path, uid, gid, AT_SYMLINK_NOFOLLOW); }
static long fp_utimens(int dirfd, const char *path, const struct timespec times[2], int flags) {
struct res r;
long rs;
+1 -1
View File
@@ -39,7 +39,7 @@ static int svc_cache_name(const struct dl_phdr_info *info, char *path, size_t si
id[2 * j + 1] = hex[b & 15];
}
id[2 * h.n_descsz] = 0;
int n = snprintf(path, size, "/tmp/.droiddeck-svc-v2-%d-%s", svc_android_api, id);
int n = snprintf(path, size, "/tmp/.droiddeck-svc-v3-%d-%s", svc_android_api, id);
return n > 0 && (size_t)n < size;
}
at += name + desc;
+2 -1
View File
@@ -29,7 +29,7 @@ static int svc_supported(int nr) {
switch (nr) {
case SYS_openat: case SYS_newfstatat: case SYS_statx: case SYS_readlinkat:
case SYS_faccessat: case SYS_mkdirat: case SYS_unlinkat: case SYS_renameat:
case SYS_renameat2: case SYS_symlinkat: case SYS_fchmodat: case SYS_utimensat:
case SYS_renameat2: case SYS_symlinkat: case SYS_fchmodat: case SYS_fchownat: case SYS_utimensat:
case SYS_truncate: case SYS_statfs: case SYS_getxattr: case SYS_lgetxattr:
case SYS_listxattr: case SYS_llistxattr: case SYS_inotify_add_watch: case SYS_getcwd:
return 1;
@@ -112,6 +112,7 @@ static long svc_path_call(const long *r) {
return fp_rename((int)r[0], path, (int)r[2], other, r[8] == SYS_renameat2 ? (unsigned)r[4] : 0);
case SYS_symlinkat: return fp_symlink(path, (int)r[1], other);
case SYS_fchmodat: return fp_chmod((int)r[0], path, (mode_t)r[2], 0);
case SYS_fchownat: return fp_chown((int)r[0], path, (uid_t)r[2], (gid_t)r[3], (int)r[4]);
case SYS_utimensat: return fp_utimens((int)r[0], path, when, (int)r[3]);
case SYS_truncate: return fp_truncate(path, (off_t)r[1]);
case SYS_statfs: {
+50 -1
View File
@@ -43,6 +43,24 @@ int main(int argc, char **argv) {
if (detranslated(text, argv[i + 1]) == FP_SLOW) strcpy(text, "SLOW");
i += 2;
puts(text);
} else if (!strcmp(argv[i], "read")) {
long fd = fp_open(AT_FDCWD, argv[++i], O_RDONLY, 0);
if (fd == FP_SLOW) puts("SLOW");
else if (fd < 0) printf("E%ld\n", -fd);
else {
char data[64] = "";
ssize_t n = read((int)fd, data, sizeof data - 1);
close((int)fd);
data[n > 0 ? n : 0] = 0;
puts(data);
}
} else if (!strcmp(argv[i], "chown")) {
int flags = !strcmp(argv[i + 1], "nofollow") ? AT_SYMLINK_NOFOLLOW : 0;
long r = fp_chown(AT_FDCWD, argv[i + 2], getuid(), getgid(), flags);
i += 2;
if (r == FP_SLOW) puts("SLOW");
else if (r < 0) printf("E%ld\n", -r);
else puts("ok");
} else if (!strcmp(argv[i], "lstat")) {
struct stat st;
long r = fstatat(AT_FDCWD, argv[++i], &st, AT_SYMLINK_NOFOLLOW);
@@ -65,13 +83,17 @@ class FastPathBindNamesTest(unittest.TestCase):
cls.program = cls.work / "names"
subprocess.run(["gcc", "-O1", "-w", "-pthread", "-o", str(cls.program), str(source), "-ldl"], check=True)
t = cls.work / "tree"
for d in ["root/etc", "root/mnt", "root/opt", "root/d/real", "lib/common/game", "fonts", "a", "b"]:
for d in ["root/etc", "root/mnt", "root/opt", "root/d/real", "lib/common/game", "fonts", "a", "b",
"root/sys/class/thermal", "root/sys/devices/zone28", "bound"]:
(t / d).mkdir(parents=True, exist_ok=True)
for f in ["root/etc/x", "lib/f", "lib/common/game/f", "fonts/f", "a/f", "b/f"]:
(t / f).touch()
os.symlink(t / "lib/f", t / "lib/l2")
os.symlink(t / "lib/common", t / "lib/l")
os.symlink("real", t / "root/d/link")
os.symlink("../../devices/zone28", t / "root/sys/class/thermal/zone28")
(t / "bound/temp").write_text("bound")
(t / "root/sys/devices/zone28/other").write_text("other")
cls.tree = t
cls.binds = [
(t / "lib", "/mnt/droiddeck-sd"),
@@ -82,6 +104,8 @@ class FastPathBindNamesTest(unittest.TestCase):
(t / "a", "/opt/g"),
(t / "b", "/opt/g"),
(t / "fonts", "/mnt/x"),
(t / "bound/temp", "/sys/class/thermal/zone28/temp"),
(t / "bound/temp", "/dev/stdin/x"),
]
@classmethod
@@ -134,6 +158,31 @@ class FastPathBindNamesTest(unittest.TestCase):
("lstat", "/d/link/"),
), ["link", "dir", "dir", "dir"])
def test_a_bound_file_under_a_link_is_found_by_its_spelled_name(self):
self.assertEqual(self.ask(
("read", "/sys/class/thermal/zone28/temp"),
("read", "//sys/class/thermal/./zone28//temp"),
("read", "/sys/class/thermal/zone28/other"),
("read", "/sys/class/thermal/zone28/temp/"),
("read", "/sys/class/thermal/zone28/../zone28/temp"),
("read", "/sys/class/thermal/zone28/missing"),
("read", "/dev/stdin/x"),
), ["bound", "bound", "SLOW", "SLOW", "SLOW", "SLOW", "SLOW"])
def test_chown_answers_like_the_kernel(self):
t = self.tree
self.assertEqual(self.ask(
("chown", "follow", "/etc/x"),
("chown", "follow", "/d/link"),
("chown", "nofollow", "/d/link"),
("chown", "follow", "/nope"),
("chown", "follow", "/etc/x/y"),
("chown", "follow", "/mnt/droiddeck-sd/f"),
("chown", "follow", "/sys/class/thermal/zone28/temp"),
("chown", "nofollow", "/sys/class/thermal/zone28/temp"),
("chown", "follow", "/sys/class/thermal/zone28/other"),
), ["ok", "ok", "ok", "E2", "E20", "ok", "ok", "ok", "SLOW"])
if __name__ == "__main__":
unittest.main()