mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb44931973 | ||
|
|
59cac100e8 | ||
|
|
371e87fa31 | ||
|
|
e9a7ef5f44 | ||
|
|
40461caf4a |
No files matched your search
@@ -327,6 +327,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var renderer by mutableStateOf("vulkan")
|
||||
private var gameStorage by mutableStateOf("")
|
||||
private var storageDiagnostics by mutableStateOf(false)
|
||||
private var skipPreallocation by mutableStateOf(false)
|
||||
private var storageOptions by mutableStateOf<List<Pair<String, String>>>(emptyList())
|
||||
private val pickGameStorage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedPath(r.data)?.let { path -> setGameStorage(path, GameStorage.labelFor(this, path)) }
|
||||
@@ -1154,6 +1155,7 @@ class MainActivity : ComponentActivity() {
|
||||
gameStorage = if (mode == SessionService.MODE_STEAM) gameStorage else null,
|
||||
storageOptions = storageOptions,
|
||||
storageDiagnostics = mode == SessionService.MODE_STEAM && storageDiagnostics,
|
||||
skipPreallocation = mode == SessionService.MODE_STEAM && skipPreallocation,
|
||||
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
|
||||
forceSsbs = forceSsbs,
|
||||
syncBackend = if (mode == SessionService.MODE_STEAM) SessionPrefs.syncBackendOf(fastSync, fsyncFirst, syncFallback) else null,
|
||||
@@ -1227,6 +1229,10 @@ class MainActivity : ComponentActivity() {
|
||||
SessionPrefs.setStorageDiagnosticsEnabled(this, on)
|
||||
storageDiagnostics = on
|
||||
},
|
||||
onSkipPreallocation = { on ->
|
||||
SessionPrefs.setSkipLibraryPreallocation(this, on)
|
||||
skipPreallocation = on
|
||||
},
|
||||
onFexPreset = { id -> SessionPrefs.setFexPreset(this, id); fexPreset = id },
|
||||
onForceSsbs = { on -> SessionPrefs.setForceSsbs(this, on); forceSsbs = on },
|
||||
onSyncBackend = { id ->
|
||||
@@ -1378,6 +1384,7 @@ class MainActivity : ComponentActivity() {
|
||||
renderer = SessionPrefs.desktopRenderer(this)
|
||||
gameStorage = SessionPrefs.gameStorage(this)
|
||||
storageDiagnostics = SessionPrefs.storageDiagnosticsEnabled(this)
|
||||
skipPreallocation = SessionPrefs.skipLibraryPreallocation(this)
|
||||
settingsMode = mode
|
||||
// The page opens at once, on what was last read; the slow part (driver files, a walk of the
|
||||
// added-games folders, the storage volumes) lands while it animates in.
|
||||
|
||||
@@ -177,6 +177,7 @@ object DeviceReport {
|
||||
SessionPrefs.GAME_STORAGE_OFF -> "internal only"
|
||||
else -> g
|
||||
})
|
||||
k("Skip space reservation", SessionPrefs.skipLibraryPreallocation(context))
|
||||
|
||||
h("Android process limits")
|
||||
k("Phantom proc monitor", PhantomProcessLimit.reportValue(PhantomProcessLimit.read(context)))
|
||||
|
||||
@@ -467,6 +467,14 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("storageDiagnostics", on).apply()
|
||||
}
|
||||
|
||||
/** Experimental: Steam's space reservation on the second library reports success untouched. */
|
||||
fun skipLibraryPreallocation(context: Context): Boolean =
|
||||
prefs(context).getBoolean("skipLibraryPreallocation", false)
|
||||
|
||||
fun setSkipLibraryPreallocation(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("skipLibraryPreallocation", on).apply()
|
||||
}
|
||||
|
||||
// ── Per-mode display ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -826,6 +826,20 @@ class SessionService : Service() {
|
||||
} else {
|
||||
Log.i(TAG, "game storage: internal only")
|
||||
}
|
||||
// guest already has the session command at this point. These go with the `env -i`
|
||||
// assignments so they reach the Steam process instead of becoming script args.
|
||||
val storageEnv = ArrayList<String>()
|
||||
val skipLibrary = storageDiagnosticLibrary
|
||||
if (SessionState.mode == MODE_STEAM && skipLibrary != null && SessionPrefs.skipLibraryPreallocation(this)) {
|
||||
val device = runCatching { android.system.Os.stat(skipLibrary.absolutePath).st_dev }.getOrNull()
|
||||
if (device != null) {
|
||||
storageEnv.add("BL_STORAGE_DEVICE=$device")
|
||||
storageEnv.add("BL_STORAGE_PREALLOCATE_SKIP=1")
|
||||
Log.i(TAG, "game storage: Steam's space reservation on the second library is skipped")
|
||||
} else {
|
||||
Log.w(TAG, "game storage: library could not be identified; space reservation not skipped")
|
||||
}
|
||||
}
|
||||
if (SessionState.mode == MODE_STEAM && SessionPrefs.storageDiagnosticsEnabled(this)) {
|
||||
val target = storageDiagnosticLibrary ?: File(LinuxRuntime.rootDir(this), "root/.local/share/Steam")
|
||||
val device = StorageDiagnostics.writeSnapshot(
|
||||
@@ -836,16 +850,18 @@ class SessionService : Service() {
|
||||
} ?: false,
|
||||
)
|
||||
if (device != null) {
|
||||
// guest already has the session command at this point. Put these with the `env -i`
|
||||
// assignments so they reach the Steam process instead of becoming script args.
|
||||
val envAt = guest.indexOf(LinuxRuntime.SESSION_SCRIPT).takeIf { it >= 0 } ?: guest.size
|
||||
guest.add(envAt, "BL_STORAGE_DIAGNOSTICS=1")
|
||||
guest.add(envAt + 1, "BL_STORAGE_DEVICE=$device")
|
||||
guest.add(envAt + 2, "BL_STORAGE_LOG=${File(sessionDir, "storage.log").absolutePath}")
|
||||
// Both observe the same library, so a device the skip already set is the same one.
|
||||
if (storageEnv.none { it.startsWith("BL_STORAGE_DEVICE=") }) storageEnv.add("BL_STORAGE_DEVICE=$device")
|
||||
storageEnv.add("BL_STORAGE_DIAGNOSTICS=1")
|
||||
storageEnv.add("BL_STORAGE_LOG=${File(sessionDir, "storage.log").absolutePath}")
|
||||
} else {
|
||||
Log.w(TAG, "storage diagnostics: selected library could not be identified; metadata snapshot only")
|
||||
}
|
||||
}
|
||||
if (storageEnv.isNotEmpty()) {
|
||||
val envAt = guest.indexOf(LinuxRuntime.SESSION_SCRIPT).takeIf { it >= 0 } ?: guest.size
|
||||
guest.addAll(envAt, storageEnv)
|
||||
}
|
||||
// The user's own games folder (the Steam cog's "Added games"), bound at a fixed place so
|
||||
// the shortcuts the app writes point somewhere whatever storage the folder is on.
|
||||
for (root in com.droiddeck.launcher.frontend.AddedGames.roots(this)) {
|
||||
|
||||
@@ -69,6 +69,8 @@ class ModeSettings(
|
||||
val storageOptions: List<Pair<String, String>> = emptyList(),
|
||||
/** Steam only: record allocation and sampled storage timings in the next session's Share logs. */
|
||||
val storageDiagnostics: Boolean = false,
|
||||
/** Steam only: report Steam's space reservation on the second library as done (experimental). */
|
||||
val skipPreallocation: Boolean = false,
|
||||
val fexPreset: String? = null,
|
||||
/** Steam only: Force SSBS for Proton games (SessionPrefs.forceSsbs). */
|
||||
val forceSsbs: Boolean = false,
|
||||
@@ -130,6 +132,7 @@ class ModeSettingsActions(
|
||||
val onGameStorage: (path: String, label: String) -> Unit = { _, _ -> },
|
||||
val onPickGameStorageFolder: () -> Unit = {},
|
||||
val onStorageDiagnostics: (Boolean) -> Unit = {},
|
||||
val onSkipPreallocation: (Boolean) -> Unit = {},
|
||||
val onFexPreset: (String) -> Unit = {},
|
||||
val onForceSsbs: (Boolean) -> Unit = {},
|
||||
val onSyncBackend: (String) -> Unit = {},
|
||||
@@ -505,6 +508,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
stringResource(R.string.mode_storage_diagnostics_hint), s.storageDiagnostics,
|
||||
onChange = a.onStorageDiagnostics,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "skipPreallocation", stringResource(R.string.mode_skip_preallocation),
|
||||
stringResource(R.string.mode_skip_preallocation_hint), s.skipPreallocation,
|
||||
onChange = a.onSkipPreallocation,
|
||||
)
|
||||
}
|
||||
if (!steam && tab == ModeSettingsTab.DISPLAY && s.renderer != null) SettingsGroup(stringResource(R.string.mode_renderer)) {
|
||||
ChoiceRow(
|
||||
|
||||
@@ -358,6 +358,8 @@
|
||||
<string name="mode_second_library">Second library</string>
|
||||
<string name="mode_storage_note">Games that stream assets from SD or shared storage may stutter.</string>
|
||||
<string name="mode_storage_diagnostics">Storage diagnostics</string>
|
||||
<string name="mode_skip_preallocation">Skip space reservation (experimental)</string>
|
||||
<string name="mode_skip_preallocation_hint">Steam starts downloading to the game storage folder straight away instead of reserving every file\'s space first, which can take many minutes on an SD card. If the card fills up, the download stops with a disk write error. Applies next session.</string>
|
||||
<string name="mode_storage_diagnostics_hint">Record allocation results and sampled storage timings in Share logs. May slow installs while enabled. Applies next session.</string>
|
||||
<string name="mode_choose_folder">Choose a folder…</string>
|
||||
<string name="mode_renderer">Renderer</string>
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
/*
|
||||
* The Steam client's directory listings on the SD library, kept in the process.
|
||||
*
|
||||
* Steam resolves every path case-insensitively. For each file it creates while it allocates an
|
||||
* install, it first checks the exact name with access() several times, then lists the file's
|
||||
* directory to look for a differently cased match, and only then creates it. On an SD card Android
|
||||
* serves the library through its FUSE daemon, where listing a directory costs 20-100 ms and each
|
||||
* failed access() 2-4 ms, so an install of a few thousand files spends many minutes in "Reserving
|
||||
* space" before any byte is downloaded. Measured on an AYN Thor, Portal 2 (3,698 files) listed
|
||||
* directories 11,712 times.
|
||||
*
|
||||
* Only the client writes into its library, so a listing it has read once stays true as long as the
|
||||
* client's own creates, renames and deletes are applied to it. Those are, and a listing also
|
||||
* expires after DIRCACHE_TTL_NS. The cache serves:
|
||||
* - readdir() of a directory listed before, from memory (opendir() still opens it for real);
|
||||
* - access() of a name absent from its directory's listing: ENOENT without a FUSE round trip.
|
||||
* Only absolute paths under the library's mount, only in the client. BL_NO_DIRCACHE=1 turns it off.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define DIRCACHE_SLOTS 64
|
||||
#define DIRCACHE_TTL_NS 30000000000LL
|
||||
#define DIRCACHE_PATH_MAX 1024
|
||||
|
||||
struct name {
|
||||
unsigned char type;
|
||||
ino_t ino;
|
||||
char *name;
|
||||
};
|
||||
|
||||
struct listing {
|
||||
char path[DIRCACHE_PATH_MAX];
|
||||
long long expires;
|
||||
long long used;
|
||||
size_t count, capacity;
|
||||
struct name *names;
|
||||
};
|
||||
|
||||
/* One open DIR the cache serves or records. */
|
||||
struct reader {
|
||||
DIR *dir;
|
||||
char path[DIRCACHE_PATH_MAX];
|
||||
int serving; /* entries come from snapshot */
|
||||
size_t pos;
|
||||
size_t count;
|
||||
struct name *snapshot; /* serving: copy taken at opendir */
|
||||
int recording, broken; /* not serving: capture a full read */
|
||||
size_t capacity;
|
||||
struct dirent64 out;
|
||||
struct reader *next;
|
||||
};
|
||||
|
||||
static struct listing listings[DIRCACHE_SLOTS];
|
||||
static struct reader *readers;
|
||||
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
void bl_meta_log(const char *op, int dirfd, const char *path, int flags, unsigned long long start, long result, int err)
|
||||
__attribute__((visibility("hidden")));
|
||||
unsigned long long bl_meta_now(void) __attribute__((visibility("hidden")));
|
||||
|
||||
static long long now_ns(void) {
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC_COARSE, &t);
|
||||
return (long long)t.tv_sec * 1000000000LL + t.tv_nsec;
|
||||
}
|
||||
|
||||
static int in_scope(const char *path) {
|
||||
static int on = -1;
|
||||
if (on < 0) {
|
||||
const char *off = getenv("BL_NO_DIRCACHE");
|
||||
on = strcmp(program_invocation_short_name, "steam") == 0 &&
|
||||
strstr(program_invocation_name, "steamrtarm64") != NULL && !(off && strcmp(off, "1") == 0);
|
||||
}
|
||||
if (!on || path == NULL || path[0] != '/') return 0;
|
||||
if (strlen(path) >= DIRCACHE_PATH_MAX) return 0;
|
||||
return strncmp(path, "/mnt/droiddeck-sd/", 18) == 0 || strcmp(path, "/mnt/droiddeck-sd") == 0 ||
|
||||
strncmp(path, "/mnt/bannerlator-sd/", 20) == 0 || strcmp(path, "/mnt/bannerlator-sd") == 0;
|
||||
}
|
||||
|
||||
/* "/a/b//c/" -> "/a/b/c"; refuses "." and ".." components. */
|
||||
static int normalize(const char *in, char *out) {
|
||||
size_t o = 0;
|
||||
for (const char *p = in; *p;) {
|
||||
while (*p == '/') p++;
|
||||
if (!*p) break;
|
||||
const char *end = strchr(p, '/');
|
||||
size_t len = end ? (size_t)(end - p) : strlen(p);
|
||||
if ((len == 1 && p[0] == '.') || (len == 2 && p[0] == '.' && p[1] == '.')) return 0;
|
||||
if (o + 1 + len >= DIRCACHE_PATH_MAX) return 0;
|
||||
out[o++] = '/';
|
||||
memcpy(out + o, p, len);
|
||||
o += len;
|
||||
p += len;
|
||||
}
|
||||
if (o == 0) out[o++] = '/';
|
||||
out[o] = '\0';
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int split(const char *path, char *parent, const char **base) {
|
||||
if (!normalize(path, parent)) return 0;
|
||||
char *slash = strrchr(parent, '/');
|
||||
if (slash == NULL || slash[1] == '\0') return 0;
|
||||
*base = path + (strlen(path) - strlen(slash + 1));
|
||||
/* base must point at the normalized last component; recompute from parent copy */
|
||||
static __thread char last[DIRCACHE_PATH_MAX];
|
||||
strcpy(last, slash + 1);
|
||||
*base = last;
|
||||
if (slash == parent) slash[1] = '\0'; else *slash = '\0';
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void drop(struct listing *l) {
|
||||
for (size_t i = 0; i < l->count; i++) free(l->names[i].name);
|
||||
free(l->names);
|
||||
memset(l, 0, sizeof(*l));
|
||||
}
|
||||
|
||||
static struct listing *find(const char *path, long long now) {
|
||||
for (int i = 0; i < DIRCACHE_SLOTS; i++) {
|
||||
struct listing *l = &listings[i];
|
||||
if (l->names == NULL && l->count == 0 && l->path[0] == '\0') continue;
|
||||
if (strcmp(l->path, path) != 0) continue;
|
||||
if (l->expires <= now) { drop(l); return NULL; }
|
||||
l->used = now;
|
||||
return l;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static ssize_t index_of(struct listing *l, const char *name) {
|
||||
for (size_t i = 0; i < l->count; i++)
|
||||
if (strcmp(l->names[i].name, name) == 0) return (ssize_t)i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void add_name(struct listing *l, const char *name, unsigned char type, ino_t ino) {
|
||||
if (index_of(l, name) >= 0) return;
|
||||
if (l->count == l->capacity) {
|
||||
size_t cap = l->capacity ? l->capacity * 2 : 32;
|
||||
struct name *grown = realloc(l->names, cap * sizeof(*grown));
|
||||
if (grown == NULL) { drop(l); return; }
|
||||
l->names = grown;
|
||||
l->capacity = cap;
|
||||
}
|
||||
char *copy = strdup(name);
|
||||
if (copy == NULL) { drop(l); return; }
|
||||
l->names[l->count++] = (struct name){type, ino, copy};
|
||||
}
|
||||
|
||||
static void remove_name(struct listing *l, const char *name) {
|
||||
ssize_t i = index_of(l, name);
|
||||
if (i < 0) return;
|
||||
free(l->names[i].name);
|
||||
l->names[i] = l->names[--l->count];
|
||||
}
|
||||
|
||||
static int in_scope(const char *path);
|
||||
|
||||
/* A change the cache can't place (a relative path): no listing can be trusted after it. */
|
||||
static void forget_all(void) {
|
||||
if (!in_scope("/mnt/droiddeck-sd")) return;
|
||||
pthread_mutex_lock(&lock);
|
||||
for (int i = 0; i < DIRCACHE_SLOTS; i++) drop(&listings[i]);
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
/* The client's own change to a directory, applied to its listing when one is cached. */
|
||||
static void note(const char *path, int added, unsigned char type) {
|
||||
char parent[DIRCACHE_PATH_MAX];
|
||||
const char *base;
|
||||
if (path != NULL && path[0] != '/') { forget_all(); return; }
|
||||
if (!in_scope(path) || !split(path, parent, &base)) return;
|
||||
pthread_mutex_lock(&lock);
|
||||
struct listing *l = find(parent, now_ns());
|
||||
if (l) {
|
||||
if (added) add_name(l, base, type, 0);
|
||||
else remove_name(l, base);
|
||||
}
|
||||
if (!added && type == DT_DIR) {
|
||||
char self[DIRCACHE_PATH_MAX];
|
||||
if (normalize(path, self)) {
|
||||
size_t n = strlen(self);
|
||||
for (int i = 0; i < DIRCACHE_SLOTS; i++)
|
||||
if (strncmp(listings[i].path, self, n) == 0 && (listings[i].path[n] == '\0' || listings[i].path[n] == '/'))
|
||||
drop(&listings[i]);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
}
|
||||
|
||||
void bl_dircache_created(const char *path) __attribute__((visibility("hidden")));
|
||||
void bl_dircache_created(const char *path) { note(path, 1, DT_REG); }
|
||||
|
||||
/* 1: the path is known not to exist (errno set to ENOENT); 0: ask the filesystem. */
|
||||
int bl_dircache_absent(const char *path) __attribute__((visibility("hidden")));
|
||||
int bl_dircache_absent(const char *path) {
|
||||
char parent[DIRCACHE_PATH_MAX];
|
||||
const char *base;
|
||||
if (!in_scope(path) || !split(path, parent, &base)) return 0;
|
||||
int absent = 0;
|
||||
pthread_mutex_lock(&lock);
|
||||
struct listing *l = find(parent, now_ns());
|
||||
if (l && index_of(l, base) < 0) absent = 1;
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (absent) errno = ENOENT;
|
||||
return absent;
|
||||
}
|
||||
|
||||
static struct reader *reader_of(DIR *d) {
|
||||
for (struct reader *r = readers; r; r = r->next)
|
||||
if (r->dir == d) return r;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void store_listing(struct reader *r) {
|
||||
long long now = now_ns();
|
||||
struct listing *slot = NULL;
|
||||
for (int i = 0; i < DIRCACHE_SLOTS; i++) {
|
||||
struct listing *l = &listings[i];
|
||||
if (strcmp(l->path, r->path) == 0) { slot = l; break; }
|
||||
}
|
||||
if (slot == NULL) {
|
||||
for (int i = 0; i < DIRCACHE_SLOTS; i++) {
|
||||
struct listing *l = &listings[i];
|
||||
if (l->path[0] == '\0' || l->expires <= now) { slot = l; break; }
|
||||
if (slot == NULL || l->used < slot->used) slot = l;
|
||||
}
|
||||
}
|
||||
drop(slot);
|
||||
strcpy(slot->path, r->path);
|
||||
slot->names = r->snapshot;
|
||||
slot->count = slot->capacity = r->count;
|
||||
slot->expires = now + DIRCACHE_TTL_NS;
|
||||
slot->used = now;
|
||||
r->snapshot = NULL;
|
||||
r->count = 0;
|
||||
}
|
||||
|
||||
static void free_reader(struct reader *r) {
|
||||
if (r->snapshot) {
|
||||
for (size_t i = 0; i < r->count; i++) free(r->snapshot[i].name);
|
||||
free(r->snapshot);
|
||||
}
|
||||
free(r);
|
||||
}
|
||||
|
||||
DIR *opendir(const char *path) {
|
||||
static DIR *(*real)(const char *);
|
||||
if (real == NULL) real = dlsym(RTLD_NEXT, "opendir");
|
||||
unsigned long long t0 = bl_meta_now();
|
||||
DIR *d = real(path);
|
||||
int e = errno;
|
||||
bl_meta_log("opendir", -100, path, 0, t0, d ? 0 : -1, e);
|
||||
if (d == NULL || !in_scope(path)) { errno = e; return d; }
|
||||
struct reader *r = calloc(1, sizeof(*r));
|
||||
if (r == NULL || !normalize(path, r->path)) { free(r); errno = e; return d; }
|
||||
r->dir = d;
|
||||
pthread_mutex_lock(&lock);
|
||||
struct listing *l = find(r->path, now_ns());
|
||||
if (l) {
|
||||
r->snapshot = calloc(l->count ? l->count : 1, sizeof(struct name));
|
||||
if (r->snapshot) {
|
||||
r->serving = 1;
|
||||
for (size_t i = 0; i < l->count; i++) {
|
||||
r->snapshot[i] = l->names[i];
|
||||
r->snapshot[i].name = strdup(l->names[i].name);
|
||||
if (r->snapshot[i].name == NULL) { r->serving = 0; break; }
|
||||
r->count = i + 1;
|
||||
}
|
||||
if (!r->serving) { for (size_t i = 0; i < r->count; i++) free(r->snapshot[i].name); free(r->snapshot); r->snapshot = NULL; r->count = 0; }
|
||||
}
|
||||
}
|
||||
if (!r->serving) r->recording = 1;
|
||||
r->next = readers;
|
||||
readers = r;
|
||||
pthread_mutex_unlock(&lock);
|
||||
errno = e;
|
||||
return d;
|
||||
}
|
||||
|
||||
struct dirent64 *readdir64(DIR *d) {
|
||||
static struct dirent64 *(*real)(DIR *);
|
||||
if (real == NULL) real = dlsym(RTLD_NEXT, "readdir64");
|
||||
pthread_mutex_lock(&lock);
|
||||
struct reader *r = reader_of(d);
|
||||
if (r && r->serving) {
|
||||
/* "." and ".." first, as the filesystem lists them. */
|
||||
struct dirent64 *out = &r->out;
|
||||
memset(out, 0, sizeof(*out));
|
||||
size_t total = r->count + 2;
|
||||
if (r->pos >= total) { pthread_mutex_unlock(&lock); return NULL; }
|
||||
if (r->pos < 2) {
|
||||
strcpy(out->d_name, r->pos == 0 ? "." : "..");
|
||||
out->d_type = DT_DIR;
|
||||
out->d_ino = 1;
|
||||
} else {
|
||||
struct name *n = &r->snapshot[r->pos - 2];
|
||||
strncpy(out->d_name, n->name, sizeof(out->d_name) - 1);
|
||||
out->d_type = n->type;
|
||||
out->d_ino = n->ino ? n->ino : 2 + r->pos;
|
||||
}
|
||||
r->pos++;
|
||||
out->d_off = (off_t)r->pos;
|
||||
out->d_reclen = sizeof(*out);
|
||||
pthread_mutex_unlock(&lock);
|
||||
return out;
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
int entry_errno = errno;
|
||||
errno = 0; /* the end of a listing leaves errno alone; a failure sets it */
|
||||
struct dirent64 *e = real(d);
|
||||
int err = errno;
|
||||
pthread_mutex_lock(&lock);
|
||||
r = reader_of(d);
|
||||
if (r && r->recording && !r->broken) {
|
||||
if (e == NULL) {
|
||||
if (err == 0) store_listing(r);
|
||||
r->recording = 0;
|
||||
} else if (strcmp(e->d_name, ".") != 0 && strcmp(e->d_name, "..") != 0) {
|
||||
if (r->count == r->capacity) {
|
||||
size_t cap = r->capacity ? r->capacity * 2 : 32;
|
||||
struct name *grown = realloc(r->snapshot, cap * sizeof(*grown));
|
||||
if (grown == NULL) r->broken = 1;
|
||||
else { r->snapshot = grown; r->capacity = cap; }
|
||||
}
|
||||
if (!r->broken) {
|
||||
char *copy = strdup(e->d_name);
|
||||
if (copy == NULL) r->broken = 1;
|
||||
else r->snapshot[r->count++] = (struct name){e->d_type, e->d_ino, copy};
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
errno = err == 0 ? entry_errno : err;
|
||||
return e;
|
||||
}
|
||||
struct dirent *readdir(DIR *d) __attribute__((alias("readdir64")));
|
||||
|
||||
int closedir(DIR *d) {
|
||||
static int (*real)(DIR *);
|
||||
if (real == NULL) real = dlsym(RTLD_NEXT, "closedir");
|
||||
pthread_mutex_lock(&lock);
|
||||
for (struct reader **link = &readers; *link; link = &(*link)->next) {
|
||||
if ((*link)->dir == d) {
|
||||
struct reader *r = *link;
|
||||
*link = r->next;
|
||||
free_reader(r);
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
return real(d);
|
||||
}
|
||||
|
||||
void rewinddir(DIR *d) {
|
||||
static void (*real)(DIR *);
|
||||
if (real == NULL) real = dlsym(RTLD_NEXT, "rewinddir");
|
||||
pthread_mutex_lock(&lock);
|
||||
struct reader *r = reader_of(d);
|
||||
if (r) {
|
||||
if (r->serving) r->pos = 0;
|
||||
else r->broken = 1; /* a partial recording can't be trusted */
|
||||
}
|
||||
pthread_mutex_unlock(&lock);
|
||||
real(d);
|
||||
}
|
||||
|
||||
/* Positions in a served listing are ours; a recording stops being trusted once the caller seeks. */
|
||||
long telldir(DIR *d) {
|
||||
static long (*real)(DIR *);
|
||||
if (real == NULL) real = dlsym(RTLD_NEXT, "telldir");
|
||||
pthread_mutex_lock(&lock);
|
||||
struct reader *r = reader_of(d);
|
||||
long pos = r && r->serving ? (long)r->pos : -2;
|
||||
pthread_mutex_unlock(&lock);
|
||||
return pos != -2 ? pos : real(d);
|
||||
}
|
||||
|
||||
void seekdir(DIR *d, long pos) {
|
||||
static void (*real)(DIR *, long);
|
||||
if (real == NULL) real = dlsym(RTLD_NEXT, "seekdir");
|
||||
pthread_mutex_lock(&lock);
|
||||
struct reader *r = reader_of(d);
|
||||
if (r && r->serving) { r->pos = pos < 0 ? 0 : (size_t)pos; pthread_mutex_unlock(&lock); return; }
|
||||
if (r) r->broken = 1;
|
||||
pthread_mutex_unlock(&lock);
|
||||
real(d, pos);
|
||||
}
|
||||
|
||||
#define MUTATE(name, sig, args, path, after) \
|
||||
int name sig { \
|
||||
static int (*real) sig; \
|
||||
if (real == NULL) real = dlsym(RTLD_NEXT, #name); \
|
||||
int r = real args; \
|
||||
int e = errno; \
|
||||
if (r == 0) { after; } \
|
||||
errno = e; \
|
||||
return r; \
|
||||
}
|
||||
|
||||
MUTATE(mkdir, (const char *p, mode_t m), (p, m), p, note(p, 1, DT_DIR))
|
||||
MUTATE(unlink, (const char *p), (p), p, note(p, 0, DT_REG))
|
||||
MUTATE(rmdir, (const char *p), (p), p, note(p, 0, DT_DIR))
|
||||
MUTATE(rename, (const char *a, const char *b), (a, b), a, { note(a, 0, DT_DIR); note(b, 0, DT_DIR); note(b, 1, DT_UNKNOWN); })
|
||||
|
||||
|
||||
#define MUTATE_AT(name, sig, args) \
|
||||
int name sig { \
|
||||
static int (*real) sig; \
|
||||
if (real == NULL) real = dlsym(RTLD_NEXT, #name); \
|
||||
int r = real args; \
|
||||
int e = errno; \
|
||||
if (r == 0) forget_all(); \
|
||||
errno = e; \
|
||||
return r; \
|
||||
}
|
||||
|
||||
MUTATE_AT(mkdirat, (int d, const char *p, mode_t m), (d, p, m))
|
||||
MUTATE_AT(unlinkat, (int d, const char *p, int f), (d, p, f))
|
||||
MUTATE_AT(renameat, (int a, const char *p, int b, const char *q), (a, p, b, q))
|
||||
MUTATE_AT(renameat2, (int a, const char *p, int b, const char *q, unsigned f), (a, p, b, q, f))
|
||||
MUTATE_AT(linkat, (int a, const char *p, int b, const char *q, int f), (a, p, b, q, f))
|
||||
MUTATE_AT(symlinkat, (const char *t, int d, const char *p), (t, d, p))
|
||||
MUTATE(link, (const char *a, const char *b), (a, b), b, note(b, 1, DT_UNKNOWN))
|
||||
MUTATE(symlink, (const char *a, const char *b), (a, b), b, note(b, 1, DT_LNK))
|
||||
int creat(const char *p, mode_t m) {
|
||||
static int (*real)(const char *, mode_t);
|
||||
if (real == NULL) real = dlsym(RTLD_NEXT, "creat");
|
||||
int fd = real(p, m);
|
||||
int e = errno;
|
||||
if (fd >= 0) note(p, 1, DT_REG);
|
||||
errno = e;
|
||||
return fd;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* EXPERIMENT: per-call log of the Steam client's metadata calls on library paths, to find what
|
||||
* dominates its allocation phase on a FUSE-served SD library. On only while
|
||||
* /root/.droiddeck-metaprof exists; lines go to /tmp/bl-meta.log.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int prof_state = -1;
|
||||
static volatile unsigned long prof_lines;
|
||||
|
||||
uint64_t bl_meta_now(void) __attribute__((visibility("hidden")));
|
||||
uint64_t bl_meta_now(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
|
||||
}
|
||||
|
||||
int bl_meta_on(void) __attribute__((visibility("hidden")));
|
||||
int bl_meta_on(void) {
|
||||
if (prof_state < 0) {
|
||||
struct stat st;
|
||||
prof_state = strcmp(program_invocation_short_name, "steam") == 0 &&
|
||||
syscall(SYS_newfstatat, AT_FDCWD, "/root/.droiddeck-metaprof", &st, 0) == 0;
|
||||
}
|
||||
return prof_state;
|
||||
}
|
||||
|
||||
void bl_meta_log(const char *op, int dirfd, const char *path, int flags, uint64_t start, long result, int err)
|
||||
__attribute__((visibility("hidden")));
|
||||
void bl_meta_log(const char *op, int dirfd, const char *path, int flags, uint64_t start, long result, int err) {
|
||||
if (!bl_meta_on() || path == NULL) return;
|
||||
if (path[0] == '/' && strstr(path, "steamapps") == NULL && strstr(path, "droiddeck-sd") == NULL) return;
|
||||
if (__atomic_add_fetch(&prof_lines, 1, __ATOMIC_RELAXED) > 400000) return;
|
||||
uint64_t end = bl_meta_now();
|
||||
char line[768];
|
||||
int n = snprintf(line, sizeof(line), "%llu %ld %s fd=%d fl=0x%x us=%llu r=%ld e=%d %s\n",
|
||||
(unsigned long long)(end / 1000000ULL), (long)syscall(SYS_gettid), op, dirfd, flags,
|
||||
(unsigned long long)((end - start) / 1000ULL), result, result < 0 ? err : 0, path);
|
||||
if (n <= 0 || (size_t)n >= sizeof(line)) return;
|
||||
int fd = (int)syscall(SYS_openat, AT_FDCWD, "/tmp/bl-meta.log", O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0600);
|
||||
if (fd < 0) return;
|
||||
syscall(SYS_write, fd, line, n);
|
||||
syscall(SYS_close, fd);
|
||||
}
|
||||
|
||||
#define WRAP_PATH(name, ret, sig, args, dirfd, path, flags) \
|
||||
ret name sig { \
|
||||
static ret (*real) sig; \
|
||||
if (real == NULL) real = dlsym(RTLD_NEXT, #name); \
|
||||
if (!bl_meta_on()) return real args; \
|
||||
uint64_t t0 = bl_meta_now(); \
|
||||
ret r = real args; \
|
||||
int e = errno; \
|
||||
bl_meta_log(#name, dirfd, path, flags, t0, (long)(r), e); \
|
||||
errno = e; \
|
||||
return r; \
|
||||
}
|
||||
|
||||
WRAP_PATH(stat, int, (const char *p, struct stat *s), (p, s), -100, p, 0)
|
||||
WRAP_PATH(lstat, int, (const char *p, struct stat *s), (p, s), -100, p, 0)
|
||||
WRAP_PATH(fstatat, int, (int d, const char *p, struct stat *s, int f), (d, p, s, f), d, p, f)
|
||||
WRAP_PATH(statx, int, (int d, const char *p, int f, unsigned m, struct statx *s), (d, p, f, m, s), d, p, f)
|
||||
|
||||
int stat64(const char *p, struct stat64 *s) __attribute__((alias("stat")));
|
||||
int lstat64(const char *p, struct stat64 *s) __attribute__((alias("lstat")));
|
||||
int fstatat64(int d, const char *p, struct stat64 *s, int f) __attribute__((alias("fstatat")));
|
||||
@@ -30,6 +30,8 @@
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
void bl_dircache_created(const char *path) __attribute__((visibility("hidden")));
|
||||
|
||||
#define LINK_FILE "/etc/droiddeck-net"
|
||||
#define LO_NAME "lo"
|
||||
#define LO_INDEX 1
|
||||
@@ -499,6 +501,7 @@ static FILE *open_stream(const char *symbol, const char *path, const char *mode)
|
||||
return copy;
|
||||
}
|
||||
FILE *f = ((fopen_fn) dlsym(RTLD_NEXT, symbol))(path, mode);
|
||||
if (f && mode != NULL && mode[0] != 'r') bl_dircache_created(path);
|
||||
if (f || path == NULL || mode == NULL || mode[0] != 'r') return f;
|
||||
int saved = errno;
|
||||
const char *table = denied_table(path);
|
||||
|
||||
@@ -1250,12 +1250,22 @@ ssize_t recvmsg(int fd, struct msghdr *msg, int flags) {
|
||||
}
|
||||
|
||||
void bl_fsync_fds_closed(unsigned int first, unsigned int last) __attribute__((visibility("hidden")));
|
||||
int bl_meta_on(void) __attribute__((visibility("hidden")));
|
||||
uint64_t bl_meta_now(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 close(int fd) {
|
||||
if (fd >= 0) bl_fsync_fds_closed((unsigned int)fd, (unsigned int)fd);
|
||||
uint64_t *t = __atomic_load_n(&ns_fdt, __ATOMIC_ACQUIRE);
|
||||
if (t && fd >= 0 && fd < NS_FDS && __atomic_load_n(&t[fd], __ATOMIC_ACQUIRE)) ns_forget(fd);
|
||||
return ns_close_fd(fd);
|
||||
if (!bl_meta_on()) return ns_close_fd(fd);
|
||||
uint64_t t0 = bl_meta_now();
|
||||
int r = ns_close_fd(fd);
|
||||
int e = errno;
|
||||
if (bl_meta_now() - t0 > 1000000ULL) bl_meta_log("close", fd, "/steamapps/(slow close)", 0, t0, r, e);
|
||||
errno = e;
|
||||
return r;
|
||||
}
|
||||
|
||||
int dup2(int oldfd, int newfd) {
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
int bl_status_without_tracer(const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
int bl_writable_retry(int dirfd, const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
int bl_ntsync_open(const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
void bl_dircache_created(const char *path) __attribute__((visibility("hidden")));
|
||||
unsigned long long bl_meta_now(void) __attribute__((visibility("hidden")));
|
||||
void bl_meta_log(const char *op, int dirfd, const char *path, int flags, unsigned long long start, long result, int err)
|
||||
__attribute__((visibility("hidden")));
|
||||
|
||||
static int retry_without_noatime(int fd, int flags) {
|
||||
return fd < 0 && errno == EPERM && (flags & O_NOATIME);
|
||||
@@ -68,10 +72,13 @@ int open(const char *path, int flags, ...) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
unsigned long long t0 = bl_meta_now();
|
||||
fd = real_open(path, flags, mode);
|
||||
bl_meta_log("open", -100, path, flags, t0, fd, errno);
|
||||
if (retry_without_noatime(fd, flags)) fd = real_open(path, flags & ~O_NOATIME, mode);
|
||||
/* A read-only file the Steam client rewrites (perms.c). */
|
||||
if (fd < 0 && bl_writable_retry(AT_FDCWD, path, flags)) fd = real_open(path, flags & ~O_NOATIME, mode);
|
||||
if (fd >= 0 && (flags & O_CREAT)) bl_dircache_created(path);
|
||||
return fd;
|
||||
}
|
||||
|
||||
@@ -95,9 +102,12 @@ int openat(int dirfd, const char *path, int flags, ...) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
unsigned long long t0 = bl_meta_now();
|
||||
fd = real_openat(dirfd, path, flags, mode);
|
||||
bl_meta_log("openat", dirfd, path, flags, t0, fd, errno);
|
||||
if (retry_without_noatime(fd, flags)) fd = real_openat(dirfd, path, flags & ~O_NOATIME, mode);
|
||||
if (fd < 0 && bl_writable_retry(dirfd, path, flags)) fd = real_openat(dirfd, path, flags & ~O_NOATIME, mode);
|
||||
if (fd >= 0 && (flags & O_CREAT)) bl_dircache_created(path);
|
||||
return fd;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,18 +125,33 @@ char *bl_cached_realpath(const char *path, char *resolved, char *(*real)(const c
|
||||
return strdup(out);
|
||||
}
|
||||
|
||||
unsigned long long bl_meta_now(void) __attribute__((visibility("hidden")));
|
||||
void bl_meta_log(const char *op, int dirfd, const char *path, int flags, unsigned long long start, long result, int err)
|
||||
__attribute__((visibility("hidden")));
|
||||
|
||||
int bl_dircache_absent(const char *path) __attribute__((visibility("hidden")));
|
||||
|
||||
static int cached_access(const char *path, int mode, int flags, int (*check)(const char *, int, int)) {
|
||||
unsigned h;
|
||||
int hit;
|
||||
int result;
|
||||
|
||||
if (!cacheable(path)) return check(path, mode, flags);
|
||||
if (path != NULL && path[0] == '/' && bl_dircache_absent(path)) return -1;
|
||||
|
||||
if (!cacheable(path)) {
|
||||
unsigned long long t0 = bl_meta_now();
|
||||
result = check(path, mode, flags);
|
||||
bl_meta_log("access", -100, path, mode, t0, result, errno);
|
||||
return result;
|
||||
}
|
||||
h = hash_of(path, KIND_ACCESS, mode, flags);
|
||||
pthread_mutex_lock(&lock);
|
||||
hit = find(path, KIND_ACCESS, mode, flags, h) != NULL;
|
||||
pthread_mutex_unlock(&lock);
|
||||
if (hit) return 0;
|
||||
unsigned long long t0 = bl_meta_now();
|
||||
result = check(path, mode, flags);
|
||||
bl_meta_log("access", -100, path, mode, t0, result, errno);
|
||||
if (result == 0) {
|
||||
pthread_mutex_lock(&lock);
|
||||
store(path, KIND_ACCESS, mode, flags, h, NULL);
|
||||
|
||||
+115
-12
@@ -7,6 +7,12 @@
|
||||
* 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.
|
||||
*/
|
||||
@@ -45,6 +51,8 @@ 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);
|
||||
@@ -71,14 +79,17 @@ static void initialize(void) {
|
||||
return;
|
||||
}
|
||||
const char *on = getenv("BL_STORAGE_DIAGNOSTICS");
|
||||
const char *skip = getenv("BL_STORAGE_PREALLOCATE_SKIP");
|
||||
const char *device = getenv("BL_STORAGE_DEVICE");
|
||||
if (on != NULL && strcmp(on, "1") == 0 && device != NULL && *device != '\0') {
|
||||
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;
|
||||
enabled = 1;
|
||||
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);
|
||||
@@ -90,10 +101,11 @@ static uint64_t now_ns(void) {
|
||||
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
|
||||
}
|
||||
|
||||
static int on_selected_device(int fd, const struct stat *known) {
|
||||
/* 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 (!enabled) return 0;
|
||||
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;
|
||||
@@ -154,19 +166,45 @@ static void allocation_line(const char *api, int result, int error, int flags,
|
||||
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);
|
||||
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;
|
||||
allocation_line("fallocate", result, call_errno, mode, offset, length, elapsed);
|
||||
if (enabled) allocation_line("fallocate", result, call_errno, mode, offset, length, elapsed);
|
||||
errno = call_errno;
|
||||
return result;
|
||||
}
|
||||
@@ -175,7 +213,7 @@ 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);
|
||||
int selected = on_selected_device(fd, NULL, 1);
|
||||
errno = entry_errno;
|
||||
if (!selected) {
|
||||
int result = next_posix_fallocate(fd, offset, length);
|
||||
@@ -183,11 +221,16 @@ int posix_fallocate(int fd, off_t offset, off_t length) {
|
||||
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;
|
||||
allocation_line("posix_fallocate", result, call_errno, 0, offset, length, elapsed);
|
||||
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;
|
||||
}
|
||||
@@ -197,9 +240,21 @@ ssize_t pwrite(int fd, const void *buffer, size_t count, off_t offset) {
|
||||
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);
|
||||
int selected = sample && on_selected_device(fd, NULL, 0);
|
||||
errno = entry_errno;
|
||||
if (!selected)
|
||||
return next_pwrite(fd, buffer, count, offset);
|
||||
@@ -219,16 +274,26 @@ ssize_t pwrite(int fd, const void *buffer, size_t count, off_t offset) {
|
||||
|
||||
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; }
|
||||
int selected = on_selected_device(fd, NULL);
|
||||
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);
|
||||
@@ -237,19 +302,56 @@ int ftruncate(int fd, off_t length) {
|
||||
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)) {
|
||||
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);
|
||||
@@ -262,7 +364,7 @@ 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);
|
||||
int selected = on_selected_device(fd, NULL, 0);
|
||||
errno = entry_errno;
|
||||
if (!selected) return next_fsync(fd);
|
||||
uint64_t start = now_ns();
|
||||
@@ -313,5 +415,6 @@ int fallocate64(int fd, int mode, off64_t offset, off64_t length) __attribute__(
|
||||
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
|
||||
Reference in new issue
Block a user