mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
Steam reserves every file's full size before downloading. On a portable SD card the app reaches through Android's FUSE layer, fallocate is rejected and the reservation can take tens of minutes for a large game, with nothing downloaded. A new experimental setting (Steam > storage, off by default) makes the session preload answer plain fallocate/posix_fallocate requests on the selected library with success and leave the file untouched, so Steam goes straight to the download. Hole punching and other fallocate modes still pass through, as does every other device. A full card shows up as a disk write error during the download instead of up front. Storage diagnostics behave as before and log the skipped calls.
341 lines
14 KiB
C
341 lines
14 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);
|
|
}
|
|
|
|
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();
|
|
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;
|
|
if (next_ftruncate == NULL) next_ftruncate = dlsym(RTLD_NEXT, "ftruncate");
|
|
if (next_ftruncate == NULL) { errno = ENOSYS; return -1; }
|
|
int selected = on_selected_device(fd, NULL, 0);
|
|
errno = entry_errno;
|
|
if (!selected) return next_ftruncate(fd, length);
|
|
uint64_t start = now_ns();
|
|
errno = entry_errno;
|
|
int result = next_ftruncate(fd, length);
|
|
int call_errno = errno;
|
|
uint64_t elapsed = now_ns() - start;
|
|
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 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 (!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 fstat64(int fd, struct stat64 *out) __attribute__((alias("fstat")));
|
|
#endif
|