mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
perf: answer chown and bound thermal paths through the proot fast path (#2203)
This commit is contained in:
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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user