mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
421 lines
18 KiB
C
421 lines
18 KiB
C
/*
|
|
* Opt-in observations for file allocation and writes to the selected Steam library.
|
|
*
|
|
* Steam's allocation failure and any later fallback are left entirely to the caller. These
|
|
* wrappers only forward the same arguments, record the result and restore errno after logging.
|
|
* The session enables them only when storage diagnostics are turned on. Counters are local to
|
|
* each process, allocation details are capped, writes are sampled, and storage.log has a global
|
|
* size limit because every Steam child shares it.
|
|
*
|
|
* One deliberate exception: when the user opts to skip space reservation, plain reservation
|
|
* requests on the selected library report success without touching the file. On a portable SD
|
|
* card Steam's reservation goes through Android's FUSE layer and can take tens of minutes; with it
|
|
* skipped Steam goes straight to downloading, and a full card shows up as a write error during the
|
|
* download rather than up front.
|
|
*
|
|
* LD_PRELOAD cannot see direct syscalls or libc-internal calls that bypass public symbols. A
|
|
* missing entry therefore does not prove the operation did not happen.
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <dlfcn.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/syscall.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
static int (*next_fstat)(int, struct stat *);
|
|
static int (*next_fallocate)(int, int, off_t, off_t);
|
|
static int (*next_posix_fallocate)(int, off_t, off_t);
|
|
static ssize_t (*next_pwrite)(int, const void *, size_t, off_t);
|
|
static int (*next_ftruncate)(int, off_t);
|
|
static int (*next_fsync)(int);
|
|
|
|
struct counters {
|
|
volatile uint64_t alloc_calls, alloc_failures, alloc_elapsed_ns;
|
|
volatile uint64_t pwrite_samples, pwrite_bytes, pwrite_failures, pwrite_elapsed_ns, pwrite_max_ns;
|
|
volatile uint64_t ftruncate_calls, ftruncate_failures, ftruncate_elapsed_ns, ftruncate_max_ns;
|
|
volatile uint64_t fstat_calls, fstat_elapsed_ns, fstat_max_ns;
|
|
volatile uint64_t fsync_calls, fsync_failures, fsync_elapsed_ns, fsync_max_ns;
|
|
};
|
|
|
|
static struct counters stats;
|
|
static volatile uint64_t pwrite_seen;
|
|
static volatile unsigned int alloc_details;
|
|
static long long selected_device;
|
|
static int init_state;
|
|
static int enabled;
|
|
static int device_valid;
|
|
static int skip_preallocation;
|
|
|
|
static uint64_t add(volatile uint64_t *slot, uint64_t value) {
|
|
return __atomic_add_fetch(slot, value, __ATOMIC_RELAXED);
|
|
}
|
|
|
|
static uint64_t get(const volatile uint64_t *slot) {
|
|
return __atomic_load_n(slot, __ATOMIC_RELAXED);
|
|
}
|
|
|
|
static void update_max(volatile uint64_t *slot, uint64_t value) {
|
|
uint64_t old = get(slot);
|
|
while (old < value && !__atomic_compare_exchange_n(slot, &old, value, 0,
|
|
__ATOMIC_RELAXED, __ATOMIC_RELAXED)) {
|
|
}
|
|
}
|
|
|
|
static void initialize(void) {
|
|
int state = __atomic_load_n(&init_state, __ATOMIC_ACQUIRE);
|
|
if (state == 2) return;
|
|
int expected = 0;
|
|
if (!__atomic_compare_exchange_n(&init_state, &expected, 1, 0,
|
|
__ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
|
|
while (__atomic_load_n(&init_state, __ATOMIC_ACQUIRE) != 2) { }
|
|
return;
|
|
}
|
|
const char *on = getenv("BL_STORAGE_DIAGNOSTICS");
|
|
const char *skip = getenv("BL_STORAGE_PREALLOCATE_SKIP");
|
|
const char *device = getenv("BL_STORAGE_DEVICE");
|
|
if (device != NULL && *device != '\0') {
|
|
char *end = NULL;
|
|
errno = 0;
|
|
long long parsed = strtoll(device, &end, 10);
|
|
if (errno == 0 && end != device && *end == '\0') {
|
|
selected_device = parsed;
|
|
device_valid = 1;
|
|
enabled = on != NULL && strcmp(on, "1") == 0;
|
|
skip_preallocation = skip != NULL && strcmp(skip, "1") == 0;
|
|
}
|
|
}
|
|
__atomic_store_n(&init_state, 2, __ATOMIC_RELEASE);
|
|
}
|
|
|
|
static uint64_t now_ns(void) {
|
|
struct timespec ts;
|
|
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
|
|
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
|
|
}
|
|
|
|
/* Diagnostics watch every wrapped call; skipping only concerns the allocation wrappers. */
|
|
static int on_selected_device(int fd, const struct stat *known, int allocation) {
|
|
struct stat local;
|
|
initialize();
|
|
if (!device_valid || !(enabled || (allocation && skip_preallocation))) return 0;
|
|
if (known == NULL) {
|
|
if (next_fstat == NULL) next_fstat = dlsym(RTLD_NEXT, "fstat");
|
|
if (next_fstat == NULL || next_fstat(fd, &local) != 0) return 0;
|
|
known = &local;
|
|
}
|
|
return (long long)known->st_dev == selected_device;
|
|
}
|
|
|
|
static void comm_name(char *out, size_t size) {
|
|
const char *name = program_invocation_short_name;
|
|
size_t i = 0;
|
|
if (name == NULL || *name == '\0') name = "unknown";
|
|
while (name[i] && i + 1 < size && i < 40) {
|
|
unsigned char c = (unsigned char)name[i];
|
|
out[i] = ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.') ? (char)c : '_';
|
|
i++;
|
|
}
|
|
out[i] = '\0';
|
|
}
|
|
|
|
static void append_line(const char *line, size_t length) {
|
|
const char *path = getenv("BL_STORAGE_LOG");
|
|
struct stat st;
|
|
int saved = errno;
|
|
if (path == NULL || *path == '\0' || length == 0 || length > 2048) goto done;
|
|
int fd = (int)syscall(SYS_openat, (long)AT_FDCWD, (long)path,
|
|
(long)(O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC), 0600L, 0L, 0L);
|
|
if (fd < 0) goto done;
|
|
if (syscall(SYS_fstat, (long)fd, (long)&st, 0L, 0L, 0L, 0L) != 0 || st.st_size < 0 ||
|
|
(uint64_t)st.st_size + length > 524288ULL) {
|
|
syscall(SYS_close, (long)fd, 0L, 0L, 0L, 0L, 0L);
|
|
goto done;
|
|
}
|
|
/* One small append write per complete line; no per-write log flood. */
|
|
(void)syscall(SYS_write, (long)fd, (long)line, (long)length, 0L, 0L, 0L);
|
|
syscall(SYS_close, (long)fd, 0L, 0L, 0L, 0L, 0L);
|
|
done:
|
|
errno = saved;
|
|
}
|
|
|
|
static void allocation_line(const char *api, int result, int error, int flags,
|
|
off_t offset, off_t length, uint64_t elapsed) {
|
|
char line[512], comm[48];
|
|
int api_error = api[0] == 'f' ? (result < 0 ? error : 0) : result;
|
|
unsigned int detail = __atomic_add_fetch(&alloc_details, 1, __ATOMIC_RELAXED);
|
|
add(&stats.alloc_calls, 1);
|
|
add(&stats.alloc_elapsed_ns, elapsed);
|
|
if ((api[0] == 'f' && result < 0) || (api[0] == 'p' && result != 0))
|
|
add(&stats.alloc_failures, 1);
|
|
if (detail > 64) return;
|
|
comm_name(comm, sizeof(comm));
|
|
int n = snprintf(line, sizeof(line),
|
|
"allocation pid=%ld comm=%s api=%s result=%d api_error=%d errno_after=%d flags=0x%x offset=%lld length=%lld elapsed_us=%llu\n",
|
|
(long)getpid(), comm, api, result, api_error, error, flags,
|
|
(long long)offset, (long long)length,
|
|
(unsigned long long)(elapsed / 1000ULL));
|
|
if (n > 0 && (size_t)n < sizeof(line)) append_line(line, (size_t)n);
|
|
}
|
|
|
|
/* EXPERIMENT: how Steam sizes files once the reservation is skipped. exFAT has no sparse files,
|
|
* so growing a file writes zeros for the whole new range. */
|
|
static volatile unsigned int extend_details;
|
|
static int truncate_skip = -1;
|
|
|
|
static int skip_extension(void) {
|
|
if (truncate_skip < 0) truncate_skip = access("/root/.droiddeck-truncate-skip", F_OK) == 0;
|
|
return skip_preallocation && truncate_skip;
|
|
}
|
|
|
|
static void extend_line(const char *api, long long from, long long to, int result, uint64_t elapsed) {
|
|
char line[256], comm[48];
|
|
if (__atomic_add_fetch(&extend_details, 1, __ATOMIC_RELAXED) > 256) return;
|
|
comm_name(comm, sizeof(comm));
|
|
int n = snprintf(line, sizeof(line), "extend pid=%ld tid=%ld comm=%s api=%s from=%lld to=%lld result=%d elapsed_us=%llu\n",
|
|
(long)getpid(), (long)syscall(SYS_gettid), comm, api, from, to, result,
|
|
(unsigned long long)(elapsed / 1000ULL));
|
|
if (n > 0 && (size_t)n < sizeof(line)) append_line(line, (size_t)n);
|
|
}
|
|
|
|
int fallocate(int fd, int mode, off_t offset, off_t length) {
|
|
int entry_errno = errno;
|
|
if (next_fallocate == NULL) next_fallocate = dlsym(RTLD_NEXT, "fallocate");
|
|
if (next_fallocate == NULL) { errno = ENOSYS; return -1; }
|
|
int selected = on_selected_device(fd, NULL, 1);
|
|
errno = entry_errno;
|
|
if (!selected) return next_fallocate(fd, mode, offset, length);
|
|
uint64_t start = now_ns();
|
|
/* Only a plain reservation is skipped; hole punching and the other modes change contents. */
|
|
if (skip_preallocation && (mode == 0 || mode == FALLOC_FL_KEEP_SIZE)) {
|
|
if (enabled) allocation_line("fallocate_skipped", 0, entry_errno, mode, offset, length, now_ns() - start);
|
|
errno = entry_errno;
|
|
return 0;
|
|
}
|
|
errno = entry_errno;
|
|
int result = next_fallocate(fd, mode, offset, length);
|
|
int call_errno = errno;
|
|
uint64_t elapsed = now_ns() - start;
|
|
if (enabled) allocation_line("fallocate", result, call_errno, mode, offset, length, elapsed);
|
|
errno = call_errno;
|
|
return result;
|
|
}
|
|
|
|
int posix_fallocate(int fd, off_t offset, off_t length) {
|
|
int entry_errno = errno;
|
|
if (next_posix_fallocate == NULL) next_posix_fallocate = dlsym(RTLD_NEXT, "posix_fallocate");
|
|
if (next_posix_fallocate == NULL) { errno = entry_errno; return ENOSYS; }
|
|
int selected = on_selected_device(fd, NULL, 1);
|
|
errno = entry_errno;
|
|
if (!selected) {
|
|
int result = next_posix_fallocate(fd, offset, length);
|
|
errno = entry_errno;
|
|
return result;
|
|
}
|
|
uint64_t start = now_ns();
|
|
if (skip_preallocation) {
|
|
if (enabled) allocation_line("posix_fallocate_skipped", 0, entry_errno, 0, offset, length, now_ns() - start);
|
|
errno = entry_errno;
|
|
return 0;
|
|
}
|
|
errno = entry_errno;
|
|
int result = next_posix_fallocate(fd, offset, length);
|
|
int call_errno = errno;
|
|
uint64_t elapsed = now_ns() - start;
|
|
if (enabled) allocation_line("posix_fallocate", result, call_errno, 0, offset, length, elapsed);
|
|
errno = entry_errno; /* POSIX requires the error to be returned, leaving errno unchanged. */
|
|
return result;
|
|
}
|
|
|
|
ssize_t pwrite(int fd, const void *buffer, size_t count, off_t offset) {
|
|
int entry_errno = errno;
|
|
if (next_pwrite == NULL) next_pwrite = dlsym(RTLD_NEXT, "pwrite");
|
|
if (next_pwrite == NULL) { errno = ENOSYS; return -1; }
|
|
initialize();
|
|
if (enabled && extend_details < 256 && on_selected_device(fd, NULL, 0)) {
|
|
struct stat st;
|
|
if (next_fstat(fd, &st) == 0 && (long long)offset > (long long)st.st_size + 65536) {
|
|
uint64_t t0 = now_ns();
|
|
errno = entry_errno;
|
|
ssize_t r = next_pwrite(fd, buffer, count, offset);
|
|
int e = errno;
|
|
extend_line("pwrite_past_eof", (long long)st.st_size, (long long)offset + (long long)count, (int)r, now_ns() - t0);
|
|
errno = e;
|
|
return r;
|
|
}
|
|
}
|
|
uint64_t sequence = enabled ? add(&pwrite_seen, 1) : 1;
|
|
int sample = enabled && (sequence % 32ULL) == 0;
|
|
int selected = sample && on_selected_device(fd, NULL, 0);
|
|
errno = entry_errno;
|
|
if (!selected)
|
|
return next_pwrite(fd, buffer, count, offset);
|
|
uint64_t start = now_ns();
|
|
errno = entry_errno;
|
|
ssize_t result = next_pwrite(fd, buffer, count, offset);
|
|
int call_errno = errno;
|
|
uint64_t elapsed = now_ns() - start;
|
|
add(&stats.pwrite_samples, 1);
|
|
add(&stats.pwrite_bytes, result > 0 ? (uint64_t)result : 0);
|
|
add(&stats.pwrite_elapsed_ns, elapsed);
|
|
if (result < 0) add(&stats.pwrite_failures, 1);
|
|
update_max(&stats.pwrite_max_ns, elapsed);
|
|
errno = call_errno;
|
|
return result;
|
|
}
|
|
|
|
int ftruncate(int fd, off_t length) {
|
|
int entry_errno = errno;
|
|
struct stat st;
|
|
if (next_ftruncate == NULL) next_ftruncate = dlsym(RTLD_NEXT, "ftruncate");
|
|
if (next_ftruncate == NULL) { errno = ENOSYS; return -1; }
|
|
if (next_fstat == NULL) next_fstat = dlsym(RTLD_NEXT, "fstat");
|
|
int selected = on_selected_device(fd, NULL, 1);
|
|
errno = entry_errno;
|
|
if (!selected) return next_ftruncate(fd, length);
|
|
long long from = (next_fstat != NULL && next_fstat(fd, &st) == 0) ? (long long)st.st_size : -1;
|
|
int extending = from >= 0 && (long long)length > from;
|
|
if (extending && skip_extension()) {
|
|
extend_line("ftruncate_skipped", from, (long long)length, 0, 0);
|
|
errno = entry_errno;
|
|
return 0;
|
|
}
|
|
uint64_t start = now_ns();
|
|
errno = entry_errno;
|
|
int result = next_ftruncate(fd, length);
|
|
int call_errno = errno;
|
|
uint64_t elapsed = now_ns() - start;
|
|
if (extending) extend_line("ftruncate", from, (long long)length, result, elapsed);
|
|
add(&stats.ftruncate_calls, 1);
|
|
if (result != 0) add(&stats.ftruncate_failures, 1);
|
|
add(&stats.ftruncate_elapsed_ns, elapsed);
|
|
update_max(&stats.ftruncate_max_ns, elapsed);
|
|
errno = call_errno;
|
|
return result;
|
|
}
|
|
|
|
int truncate(const char *path, off_t length) {
|
|
static int (*next_truncate)(const char *, off_t);
|
|
int entry_errno = errno;
|
|
struct stat st;
|
|
if (next_truncate == NULL) next_truncate = dlsym(RTLD_NEXT, "truncate");
|
|
if (next_truncate == NULL) { errno = ENOSYS; return -1; }
|
|
initialize();
|
|
int selected = device_valid && (enabled || skip_preallocation) && stat(path, &st) == 0 &&
|
|
(long long)st.st_dev == selected_device;
|
|
errno = entry_errno;
|
|
if (!selected) return next_truncate(path, length);
|
|
int extending = (long long)length > (long long)st.st_size;
|
|
if (extending && skip_extension()) {
|
|
extend_line("truncate_skipped", (long long)st.st_size, (long long)length, 0, 0);
|
|
errno = entry_errno;
|
|
return 0;
|
|
}
|
|
uint64_t start = now_ns();
|
|
int result = next_truncate(path, length);
|
|
int call_errno = errno;
|
|
if (extending) extend_line("truncate", (long long)st.st_size, (long long)length, result, now_ns() - start);
|
|
errno = call_errno;
|
|
return result;
|
|
}
|
|
|
|
int bl_meta_on(void) __attribute__((visibility("hidden")));
|
|
void bl_meta_log(const char *op, int dirfd, const char *path, int flags, uint64_t start, long result, int err)
|
|
__attribute__((visibility("hidden")));
|
|
|
|
int fstat(int fd, struct stat *out) {
|
|
int entry_errno = errno;
|
|
if (next_fstat == NULL) next_fstat = dlsym(RTLD_NEXT, "fstat");
|
|
if (next_fstat == NULL) { errno = ENOSYS; return -1; }
|
|
initialize();
|
|
errno = entry_errno;
|
|
if (bl_meta_on()) {
|
|
uint64_t t0 = now_ns();
|
|
int r = next_fstat(fd, out);
|
|
int e = errno;
|
|
if (now_ns() - t0 > 500000ULL) bl_meta_log("fstat", fd, "/steamapps/(slow fstat)", 0, t0, r, e);
|
|
errno = e;
|
|
if (!enabled) return r;
|
|
}
|
|
if (!enabled) return next_fstat(fd, out);
|
|
uint64_t start = now_ns();
|
|
errno = entry_errno;
|
|
int result = next_fstat(fd, out);
|
|
int call_errno = errno;
|
|
uint64_t elapsed = now_ns() - start;
|
|
if (result == 0 && on_selected_device(fd, out, 0)) {
|
|
add(&stats.fstat_calls, 1);
|
|
add(&stats.fstat_elapsed_ns, elapsed);
|
|
update_max(&stats.fstat_max_ns, elapsed);
|
|
}
|
|
errno = call_errno;
|
|
return result;
|
|
}
|
|
|
|
int fsync(int fd) {
|
|
int entry_errno = errno;
|
|
if (next_fsync == NULL) next_fsync = dlsym(RTLD_NEXT, "fsync");
|
|
if (next_fsync == NULL) { errno = ENOSYS; return -1; }
|
|
int selected = on_selected_device(fd, NULL, 0);
|
|
errno = entry_errno;
|
|
if (!selected) return next_fsync(fd);
|
|
uint64_t start = now_ns();
|
|
errno = entry_errno;
|
|
int result = next_fsync(fd);
|
|
int call_errno = errno;
|
|
uint64_t elapsed = now_ns() - start;
|
|
add(&stats.fsync_calls, 1);
|
|
if (result != 0) add(&stats.fsync_failures, 1);
|
|
add(&stats.fsync_elapsed_ns, elapsed);
|
|
update_max(&stats.fsync_max_ns, elapsed);
|
|
errno = call_errno;
|
|
return result;
|
|
}
|
|
|
|
static void flush_summary(void) __attribute__((destructor));
|
|
static void flush_summary(void) {
|
|
char line[1024], comm[48];
|
|
initialize();
|
|
if (!enabled) return;
|
|
uint64_t alloc = get(&stats.alloc_calls), pwrite_samples = get(&stats.pwrite_samples);
|
|
uint64_t truncates = get(&stats.ftruncate_calls), fstats = get(&stats.fstat_calls), syncs = get(&stats.fsync_calls);
|
|
if (alloc == 0 && pwrite_samples == 0 && truncates == 0 && fstats == 0 && syncs == 0) return;
|
|
comm_name(comm, sizeof(comm));
|
|
int n = snprintf(line, sizeof(line),
|
|
"summary pid=%ld comm=%s allocation_calls=%llu allocation_failures=%llu allocation_total_us=%llu pwrite_sample_every=32 pwrite_samples=%llu pwrite_bytes=%llu pwrite_failures=%llu pwrite_sample_total_us=%llu pwrite_sample_max_us=%llu ftruncate_calls=%llu ftruncate_failures=%llu ftruncate_total_us=%llu ftruncate_max_us=%llu fstat_calls=%llu fstat_total_us=%llu fstat_max_us=%llu fsync_calls=%llu fsync_failures=%llu fsync_total_us=%llu fsync_max_us=%llu\n",
|
|
(long)getpid(), comm,
|
|
(unsigned long long)alloc, (unsigned long long)get(&stats.alloc_failures),
|
|
(unsigned long long)(get(&stats.alloc_elapsed_ns) / 1000ULL),
|
|
(unsigned long long)pwrite_samples, (unsigned long long)get(&stats.pwrite_bytes),
|
|
(unsigned long long)get(&stats.pwrite_failures),
|
|
(unsigned long long)(get(&stats.pwrite_elapsed_ns) / 1000ULL),
|
|
(unsigned long long)(get(&stats.pwrite_max_ns) / 1000ULL),
|
|
(unsigned long long)truncates, (unsigned long long)get(&stats.ftruncate_failures),
|
|
(unsigned long long)(get(&stats.ftruncate_elapsed_ns) / 1000ULL),
|
|
(unsigned long long)(get(&stats.ftruncate_max_ns) / 1000ULL),
|
|
(unsigned long long)fstats,
|
|
(unsigned long long)(get(&stats.fstat_elapsed_ns) / 1000ULL),
|
|
(unsigned long long)(get(&stats.fstat_max_ns) / 1000ULL),
|
|
(unsigned long long)syncs, (unsigned long long)get(&stats.fsync_failures),
|
|
(unsigned long long)(get(&stats.fsync_elapsed_ns) / 1000ULL),
|
|
(unsigned long long)(get(&stats.fsync_max_ns) / 1000ULL));
|
|
if (n > 0 && (size_t)n < sizeof(line)) append_line(line, (size_t)n);
|
|
}
|
|
|
|
#if defined(__USE_LARGEFILE64)
|
|
int fallocate64(int fd, int mode, off64_t offset, off64_t length) __attribute__((alias("fallocate")));
|
|
int posix_fallocate64(int fd, off64_t offset, off64_t length) __attribute__((alias("posix_fallocate")));
|
|
ssize_t pwrite64(int fd, const void *buffer, size_t count, off64_t offset) __attribute__((alias("pwrite")));
|
|
int ftruncate64(int fd, off64_t length) __attribute__((alias("ftruncate")));
|
|
int truncate64(const char *path, off64_t length) __attribute__((alias("truncate")));
|
|
int fstat64(int fd, struct stat64 *out) __attribute__((alias("fstat")));
|
|
#endif
|