mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
Add opt-in zRAM compression for the Steam client while a game runs (#442)
* Add opt-in zRAM compression for the Steam client while a game runs A "zRAM compression" toggle under Steam > Settings > Session, below "Keep downloads running". Off by default, kept once set, and disabled on devices without swap or with a kernel older than 5.4. When on, the session watches for Steam's reaper. 20 s into a game the client and its web helpers page their private anonymous memory out to the device's zram with MADV_PAGEOUT from inside their own process (libblsession zram.c, SIGURG), again every 5 minutes, and read every swapped page back once the game exits. No root is needed: only a process itself may do this without CAP_SYS_NICE. With the toggle off nothing is installed or started. RedMagic NP06J, Palworld: Steam's resident memory fell from ~1.7 GB to ~1.1 GB with ~560 MB in zram, available memory during play rose by ~0.5-0.8 GB, and ~2 MB was left in zram after the read-back. * Turn zRAM compression on by default below 12 GB of RAM Until the toggle is changed, it follows the device: on when the kernel supports it and the device has less than 12 GB of RAM, read from the advertised memory size on Android 14+ and from total memory before that. Turning it off is kept. --------- Co-authored-by: xXJsonDeruloXx <danielhimebauch@gmail.com>
This commit is contained in:
1 parent
7987b04529
commit
9a6d48c04b
10 files changed
+513
No files matched your search
@@ -350,6 +350,8 @@ class MainActivity : ComponentActivity() {
|
||||
private var touchMode by mutableStateOf(SessionPrefs.TOUCH_AUTO)
|
||||
private var suspendPolicy by mutableStateOf(SessionPrefs.SUSPEND_MANUAL)
|
||||
private var steamDownloadsInBackground by mutableStateOf(false)
|
||||
private var zramCompression by mutableStateOf(false)
|
||||
private var zramSupported by mutableStateOf<Boolean?>(null)
|
||||
private var oscMode by mutableStateOf(SessionPrefs.OSC_AUTO)
|
||||
private var backActionsInverted by mutableStateOf(false)
|
||||
private var gameStorage by mutableStateOf("")
|
||||
@@ -1293,6 +1295,8 @@ class MainActivity : ComponentActivity() {
|
||||
touchMode = touchMode,
|
||||
suspendPolicy = suspendPolicy,
|
||||
steamDownloadsInBackground = steamDownloadsInBackground,
|
||||
zramCompression = if (mode == SessionService.MODE_STEAM) zramCompression else null,
|
||||
zramSupported = zramSupported,
|
||||
pipSupported = com.droiddeck.launcher.session.SessionPipController.supported(this),
|
||||
pipAutoEnter = pipAutoEnter,
|
||||
unfoldedControls = if (mode == SessionService.MODE_STEAM &&
|
||||
@@ -1351,6 +1355,7 @@ class MainActivity : ComponentActivity() {
|
||||
SessionPrefs.setSteamDownloadsInBackground(this, enabled)
|
||||
steamDownloadsInBackground = enabled
|
||||
},
|
||||
onZramCompression = { on -> SessionPrefs.setZramCompression(this, on); zramCompression = on },
|
||||
onPipAutoEnter = { on -> SessionPrefs.setPipAutoEnter(this, on); pipAutoEnter = on },
|
||||
onUnfoldedControls = { on -> SessionPrefs.setUnfoldedControls(this, on); unfoldedControls = on },
|
||||
onOsc = { o -> SessionPrefs.setOscMode(this, o); oscMode = o },
|
||||
@@ -1539,6 +1544,9 @@ class MainActivity : ComponentActivity() {
|
||||
// The sheet lifts 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 rises.
|
||||
Thread({
|
||||
val zram = com.droiddeck.launcher.core.ZramSupport.supported()
|
||||
val zramOn = SessionPrefs.zramCompression(this)
|
||||
ui.post { zramSupported = zram; zramCompression = zramOn }
|
||||
drivers.refreshDrivers()
|
||||
val storage = GameStorage.options(this)
|
||||
val deckyInstalled = if (mode == SessionService.MODE_STEAM) DeckyManager.installed(this) else null
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import java.io.File
|
||||
|
||||
/** Whether the client's memory can be paged out to compressed swap: MADV_PAGEOUT (Linux 5.4) and a zram or other swap device. */
|
||||
object ZramSupport {
|
||||
private const val GIB = 1L shl 30
|
||||
|
||||
fun supported(): Boolean = kernelAtLeast(System.getProperty("os.version").orEmpty(), 5, 4) && swapTotalKb() > 0
|
||||
|
||||
fun recommended(context: Context): Boolean {
|
||||
if (!supported()) return false
|
||||
val memory = ActivityManager.MemoryInfo()
|
||||
context.getSystemService(ActivityManager::class.java).getMemoryInfo(memory)
|
||||
val advertised = if (Build.VERSION.SDK_INT >= 34) memory.advertisedMem else 0L
|
||||
return belowTwelveGb(advertised, memory.totalMem)
|
||||
}
|
||||
|
||||
fun belowTwelveGb(advertisedBytes: Long, totalBytes: Long): Boolean =
|
||||
if (advertisedBytes > 0) advertisedBytes < 11 * GIB else totalBytes < 10 * GIB
|
||||
|
||||
fun kernelAtLeast(release: String, major: Int, minor: Int): Boolean {
|
||||
val parts = Regex("^(\\d+)\\.(\\d+)").find(release)?.groupValues ?: return false
|
||||
val ma = parts[1].toInt()
|
||||
val mi = parts[2].toInt()
|
||||
return ma > major || (ma == major && mi >= minor)
|
||||
}
|
||||
|
||||
fun swapTotalKb(meminfo: List<String> = runCatching { File("/proc/meminfo").readLines() }.getOrDefault(emptyList())): Long =
|
||||
meminfo.firstOrNull { it.startsWith("SwapTotal:") }
|
||||
?.substringAfter(':')?.trim()?.substringBefore(' ')?.toLongOrNull() ?: 0L
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.core.CpuAllocationDefaults
|
||||
import com.droiddeck.launcher.core.GameEnvironment
|
||||
import com.droiddeck.launcher.core.TextureFiltering
|
||||
import com.droiddeck.launcher.core.ZramSupport
|
||||
import com.droiddeck.launcher.gpu.Cardboard
|
||||
import com.droiddeck.launcher.gpu.ScreenEffects
|
||||
import org.json.JSONObject
|
||||
@@ -1057,6 +1058,16 @@ object SessionPrefs {
|
||||
}.apply()
|
||||
}
|
||||
|
||||
/** Steam only: the client's memory goes to the device's zram while a game runs (ZramSupport). */
|
||||
fun zramCompression(context: Context): Boolean {
|
||||
val prefs = prefs(context)
|
||||
return if (prefs.contains("zramCompression")) prefs.getBoolean("zramCompression", false) else ZramSupport.recommended(context)
|
||||
}
|
||||
|
||||
fun setZramCompression(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("zramCompression", on).apply()
|
||||
}
|
||||
|
||||
// ── Game storage ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -660,6 +660,8 @@ class SessionService : Service() {
|
||||
// gamescope's realtime Vulkan queues (the session script turns this into
|
||||
// GAMESCOPE_FORCE_VULKAN_REALTIME); off unless the user turns it on.
|
||||
guest.add("BL_GAMESCOPE_REALTIME=" + (if (SessionPrefs.gamescopeRealtime(this)) "1" else "0"))
|
||||
if (SessionState.mode == MODE_STEAM && SessionPrefs.zramCompression(this) &&
|
||||
com.droiddeck.launcher.core.ZramSupport.supported()) guest.add("BL_ZRAM=1")
|
||||
guest.add("BANNER_AUDIO_DIRECT_DECAY=0")
|
||||
// Anything else, for a device that cannot be reached with a debugger: droiddeck-env in the
|
||||
// app's own external files (envSwitchFile) holds KEY=VALUE lines that go into the session's
|
||||
|
||||
@@ -70,6 +70,10 @@ class ModeSettings(
|
||||
val touchMode: String,
|
||||
val suspendPolicy: String,
|
||||
val steamDownloadsInBackground: Boolean = false,
|
||||
/** Steam only; null hides it. */
|
||||
val zramCompression: Boolean? = null,
|
||||
/** Null until checked off the main thread. */
|
||||
val zramSupported: Boolean? = null,
|
||||
val pipSupported: Boolean = false,
|
||||
val pipAutoEnter: Boolean = false,
|
||||
/** Steam only. */
|
||||
@@ -127,6 +131,7 @@ class ModeSettingsActions(
|
||||
val onTouch: (String) -> Unit,
|
||||
val onSuspendPolicy: (String) -> Unit,
|
||||
val onSteamDownloadsInBackground: (Boolean) -> Unit = {},
|
||||
val onZramCompression: (Boolean) -> Unit = {},
|
||||
val onPipAutoEnter: (Boolean) -> Unit = {},
|
||||
val onOsc: (String) -> Unit,
|
||||
val onBackActionsInverted: (Boolean) -> Unit = {},
|
||||
@@ -303,6 +308,12 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras:
|
||||
stringResource(R.string.mode_background_downloads_hint),
|
||||
s.steamDownloadsInBackground, onChange = a.onSteamDownloadsInBackground,
|
||||
)
|
||||
if (s.zramCompression != null) ToggleRow(
|
||||
host, "zram", stringResource(R.string.mode_zram),
|
||||
stringResource(if (s.zramSupported == false) R.string.mode_zram_unsupported else R.string.mode_zram_hint),
|
||||
s.zramCompression && s.zramSupported != false, enabled = s.zramSupported == true,
|
||||
onChange = a.onZramCompression,
|
||||
)
|
||||
}
|
||||
if (s.pipSupported) SettingsGroup(stringResource(R.string.pip_title)) {
|
||||
ToggleRow(host, "pip-auto", stringResource(R.string.pip_auto), null,
|
||||
|
||||
@@ -278,6 +278,9 @@
|
||||
<string name="mode_suspend_steam_note">Auto pauses and resumes when shown. Native prepares Steam for sleep first. Manual waits for Resume. Never keeps running.</string>
|
||||
<string name="mode_background_downloads">Keep downloads running</string>
|
||||
<string name="mode_background_downloads_hint">Delay background suspension while Steam is actively downloading.</string>
|
||||
<string name="mode_zram">zRAM compression</string>
|
||||
<string name="mode_zram_hint">While a game runs, the Steam client\'s memory is compressed into the device\'s zram, leaving more RAM for the game. Applies the next time Steam starts.</string>
|
||||
<string name="mode_zram_unsupported">Unavailable: this device has no zram or swap, or its kernel is older than 5.4.</string>
|
||||
<string name="mode_suspend_note">Auto pauses in the background and resumes when shown. Manual waits for Resume. Never keeps running.</string>
|
||||
<string name="mode_startup">Startup</string>
|
||||
<string name="mode_steam_startup">Start Steam with DroidDeck</string>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ZramSupportTest {
|
||||
@Test
|
||||
fun kernelVersionsFromAndroidReleases() {
|
||||
assertTrue(ZramSupport.kernelAtLeast("6.12.38-android16-5-g665eafb62659-ab14778838-4k", 5, 4))
|
||||
assertTrue(ZramSupport.kernelAtLeast("5.4.0", 5, 4))
|
||||
assertFalse(ZramSupport.kernelAtLeast("4.19.157-perf+", 5, 4))
|
||||
assertFalse(ZramSupport.kernelAtLeast("", 5, 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun underTwelveGbFromAdvertisedOrTotalMemory() {
|
||||
val gb = 1_000_000_000L
|
||||
val gib = 1L shl 30
|
||||
assertTrue(ZramSupport.belowTwelveGb(8 * gb, 0))
|
||||
assertTrue(ZramSupport.belowTwelveGb(8 * gib, 0))
|
||||
assertFalse(ZramSupport.belowTwelveGb(12 * gb, 0))
|
||||
assertFalse(ZramSupport.belowTwelveGb(12 * gib, 0))
|
||||
assertTrue(ZramSupport.belowTwelveGb(0, 7_400_000L * 1024))
|
||||
assertTrue(ZramSupport.belowTwelveGb(0, 9_300_000L * 1024))
|
||||
assertFalse(ZramSupport.belowTwelveGb(0, 11_100_000L * 1024))
|
||||
assertFalse(ZramSupport.belowTwelveGb(0, 15_600_380L * 1024))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun swapTotalComesFromMeminfo() {
|
||||
assertEquals(12582908L, ZramSupport.swapTotalKb(listOf("MemTotal: 15600380 kB", "SwapTotal: 12582908 kB")))
|
||||
assertEquals(0L, ZramSupport.swapTotalKb(listOf("SwapTotal: 0 kB")))
|
||||
assertEquals(0L, ZramSupport.swapTotalKb(emptyList()))
|
||||
}
|
||||
}
|
||||
@@ -489,6 +489,60 @@ case $mode in
|
||||
done
|
||||
) &
|
||||
fi
|
||||
# zRAM compression (BL_ZRAM): 20 s into a game the client and its web helpers page their memory
|
||||
# out to the device's zram (libblsession zram.c), again every 5 minutes, and read it back once
|
||||
# the game has exited. A game is running while Steam's reaper is.
|
||||
if [ "${BL_ZRAM:-0}" = 1 ]; then
|
||||
printf 0 > /tmp/.bl-zram
|
||||
step "zRAM compression: on"
|
||||
(
|
||||
bl_zram_scan() {
|
||||
_bl_game=0 _bl_pids=()
|
||||
for _d in /proc/[0-9]*; do
|
||||
IFS= read -r -d '' _c 2>/dev/null < "$_d/cmdline" || continue
|
||||
case ${_c##*/} in
|
||||
reaper) _bl_game=1 ;;
|
||||
steam|steamwebhelper) _bl_pids+=("${_d#/proc/}") ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
bl_zram_usage() {
|
||||
local _p _k _v _u _rss=0 _swap=0
|
||||
for _p in "${_bl_pids[@]}"; do
|
||||
while read -r _k _v _u; do
|
||||
case $_k in VmRSS:) _rss=$((_rss + _v)) ;; VmSwap:) _swap=$((_swap + _v)) ;; esac
|
||||
done 2>/dev/null < "/proc/$_p/status"
|
||||
done
|
||||
echo "${#_bl_pids[@]} processes, resident $((_rss / 1024)) MB, in zram $((_swap / 1024)) MB"
|
||||
}
|
||||
bl_zram_send() {
|
||||
step "zRAM: $2 ($(bl_zram_usage))"
|
||||
printf '%s' "$1" > /tmp/.bl-zram
|
||||
[ "${#_bl_pids[@]}" = 0 ] || kill -URG "${_bl_pids[@]}" 2>/dev/null
|
||||
nap 5
|
||||
bl_zram_scan
|
||||
step "zRAM: done ($(bl_zram_usage))"
|
||||
}
|
||||
_bl_since=-1 _bl_packed=-1
|
||||
while kill -0 $$ 2>/dev/null; do
|
||||
bl_zram_scan
|
||||
if [ "$_bl_game" = 1 ]; then
|
||||
[ "$_bl_since" -ge 0 ] || _bl_since=$SECONDS
|
||||
if [ $((SECONDS - _bl_since)) -ge 20 ] && [ "${#_bl_pids[@]}" -gt 0 ] &&
|
||||
{ [ "$_bl_packed" -lt 0 ] || [ $((SECONDS - _bl_packed)) -ge 300 ]; }; then
|
||||
bl_zram_send 1 "compressing the client"
|
||||
_bl_packed=$SECONDS
|
||||
fi
|
||||
elif [ "$_bl_since" -ge 0 ]; then
|
||||
[ "$_bl_packed" -lt 0 ] || bl_zram_send 0 "game closed, reading the client back"
|
||||
_bl_since=-1 _bl_packed=-1
|
||||
fi
|
||||
nap 5
|
||||
done
|
||||
) &
|
||||
else
|
||||
rm -f /tmp/.bl-zram
|
||||
fi
|
||||
# Rockstar's launcher keeps drawing its hidden web interface for as long as a game of theirs
|
||||
# runs; droiddeck-launcher-cores holds it on one slow core while it is out of sight, from
|
||||
# gamescope's focus as the agent publishes it. BL_LAUNCHER_CORES=0 leaves launchers alone.
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/uio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* zRAM for the Steam client while a game runs: on SIGURG the process pages its private anonymous
|
||||
* memory out (ZR_CTL holds 1) or reads it back (0). Raw syscalls only, as this library wraps
|
||||
* close(), openat() and syscall(). */
|
||||
|
||||
#ifndef MADV_PAGEOUT
|
||||
#define MADV_PAGEOUT 21
|
||||
#endif
|
||||
#ifndef ZR_CTL
|
||||
#define ZR_CTL "/tmp/.bl-zram"
|
||||
#endif
|
||||
#define ZR_SWAPPED (1ULL << 62)
|
||||
#define ZR_BATCH 512
|
||||
|
||||
static long (*zr_sys)(long, ...);
|
||||
static unsigned long zr_page;
|
||||
static int zr_busy;
|
||||
static int zr_pending;
|
||||
static char zr_maps[8192];
|
||||
static uint64_t zr_entries[ZR_BATCH];
|
||||
static struct iovec zr_iov[ZR_BATCH];
|
||||
static char zr_sink[ZR_BATCH];
|
||||
|
||||
static int zr_open(const char *path) {
|
||||
return (int)zr_sys(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC, 0);
|
||||
}
|
||||
|
||||
static int zr_mode(void) {
|
||||
char c = 0;
|
||||
int fd = zr_open(ZR_CTL);
|
||||
if (fd < 0) return -1;
|
||||
long n = zr_sys(SYS_read, fd, &c, 1);
|
||||
zr_sys(SYS_close, fd);
|
||||
return n == 1 && (c == '0' || c == '1') ? c - '0' : -1;
|
||||
}
|
||||
|
||||
static unsigned long zr_hex(const char **p) {
|
||||
unsigned long v = 0;
|
||||
for (;; (*p)++) {
|
||||
char c = **p;
|
||||
if (c >= '0' && c <= '9') v = v * 16 + (unsigned long)(c - '0');
|
||||
else if (c >= 'a' && c <= 'f') v = v * 16 + (unsigned long)(c - 'a' + 10);
|
||||
else return v;
|
||||
}
|
||||
}
|
||||
|
||||
/* Paged out: private writable anonymous memory (the heap, unnamed and [anon:name] mappings).
|
||||
* Read back: every private readable mapping, as the kernel may have swapped others meanwhile. */
|
||||
static int zr_eligible(const char *line, int compress, unsigned long *start, unsigned long *end) {
|
||||
const char *p = line;
|
||||
*start = zr_hex(&p);
|
||||
if (*p++ != '-') return 0;
|
||||
*end = zr_hex(&p);
|
||||
if (*p++ != ' ' || p[0] != 'r' || p[3] != 'p') return 0;
|
||||
if (!compress) return 1;
|
||||
if (p[1] != 'w') return 0;
|
||||
for (int field = 0; field < 4; field++) {
|
||||
while (*p && *p != ' ') p++;
|
||||
while (*p == ' ') p++;
|
||||
}
|
||||
return *p == 0 || strncmp(p, "[heap]", 6) == 0 || strncmp(p, "[anon:", 6) == 0;
|
||||
}
|
||||
|
||||
static void zr_read_back(int pagemap, unsigned long start, unsigned long end) {
|
||||
pid_t self = (pid_t)zr_sys(SYS_getpid);
|
||||
struct iovec local = {zr_sink, 0};
|
||||
for (unsigned long addr = start; addr < end;) {
|
||||
unsigned long count = (end - addr) / zr_page;
|
||||
if (count > ZR_BATCH) count = ZR_BATCH;
|
||||
if (zr_sys(SYS_lseek, pagemap, (long)(addr / zr_page * sizeof(uint64_t)), SEEK_SET) < 0) return;
|
||||
long got = zr_sys(SYS_read, pagemap, zr_entries, count * sizeof(uint64_t));
|
||||
if (got < (long)sizeof(uint64_t)) return;
|
||||
unsigned long n = 0;
|
||||
for (unsigned long i = 0; i < (unsigned long)got / sizeof(uint64_t); i++) {
|
||||
if (!(zr_entries[i] & ZR_SWAPPED)) continue;
|
||||
zr_iov[n].iov_base = (void *)(addr + i * zr_page);
|
||||
zr_iov[n++].iov_len = 1;
|
||||
}
|
||||
for (unsigned long done = 0; done < n;) {
|
||||
local.iov_len = n - done;
|
||||
long r = zr_sys(SYS_process_vm_readv, self, &local, 1UL, zr_iov + done, n - done, 0UL);
|
||||
done += r > 0 ? (unsigned long)r : 1;
|
||||
}
|
||||
addr += (unsigned long)got / sizeof(uint64_t) * zr_page;
|
||||
}
|
||||
}
|
||||
|
||||
static void zr_walk(int compress) {
|
||||
int maps = zr_open("/proc/self/maps");
|
||||
if (maps < 0) return;
|
||||
int pagemap = compress ? -1 : zr_open("/proc/self/pagemap");
|
||||
if (!compress && pagemap < 0) {
|
||||
zr_sys(SYS_close, maps);
|
||||
return;
|
||||
}
|
||||
size_t have = 0;
|
||||
for (;;) {
|
||||
long n = zr_sys(SYS_read, maps, zr_maps + have, sizeof(zr_maps) - 1 - have);
|
||||
if (n < 0 && errno == EINTR) continue;
|
||||
if (n <= 0) break;
|
||||
have += (size_t)n;
|
||||
zr_maps[have] = 0;
|
||||
char *line = zr_maps;
|
||||
char *nl;
|
||||
while ((nl = memchr(line, '\n', have - (size_t)(line - zr_maps))) != NULL) {
|
||||
*nl = 0;
|
||||
unsigned long s, e;
|
||||
if (zr_eligible(line, compress, &s, &e) && e > s) {
|
||||
if (compress) zr_sys(SYS_madvise, s, e - s, MADV_PAGEOUT);
|
||||
else zr_read_back(pagemap, s, e);
|
||||
}
|
||||
line = nl + 1;
|
||||
}
|
||||
have -= (size_t)(line - zr_maps);
|
||||
memmove(zr_maps, line, have);
|
||||
if (have == sizeof(zr_maps) - 1) have = 0;
|
||||
}
|
||||
if (pagemap >= 0) zr_sys(SYS_close, pagemap);
|
||||
zr_sys(SYS_close, maps);
|
||||
}
|
||||
|
||||
static void zr_handler(int sig, siginfo_t *info, void *context) {
|
||||
(void)sig;
|
||||
(void)context;
|
||||
if (info == NULL || info->si_code != SI_USER) return;
|
||||
int saved = errno;
|
||||
__atomic_store_n(&zr_pending, 1, __ATOMIC_SEQ_CST);
|
||||
while (__atomic_load_n(&zr_pending, __ATOMIC_SEQ_CST)
|
||||
&& !__atomic_exchange_n(&zr_busy, 1, __ATOMIC_SEQ_CST)) {
|
||||
__atomic_store_n(&zr_pending, 0, __ATOMIC_SEQ_CST);
|
||||
int mode = zr_mode();
|
||||
if (mode >= 0) zr_walk(mode);
|
||||
__atomic_store_n(&zr_busy, 0, __ATOMIC_SEQ_CST);
|
||||
}
|
||||
errno = saved;
|
||||
}
|
||||
|
||||
static void zr_child(void) {
|
||||
__atomic_store_n(&zr_busy, 0, __ATOMIC_SEQ_CST);
|
||||
__atomic_store_n(&zr_pending, 0, __ATOMIC_SEQ_CST);
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void zr_init(void) {
|
||||
const char *name = program_invocation_short_name;
|
||||
if (strcmp(name, "steam") != 0 && strcmp(name, "steamwebhelper") != 0) return;
|
||||
void *libc = dlopen("libc.so.6", RTLD_LAZY | RTLD_NOLOAD);
|
||||
if (libc == NULL) return;
|
||||
zr_sys = (long (*)(long, ...))dlsym(libc, "syscall");
|
||||
if (zr_sys == NULL) return;
|
||||
int fd = zr_open(ZR_CTL);
|
||||
if (fd < 0) return;
|
||||
zr_sys(SYS_close, fd);
|
||||
long page = sysconf(_SC_PAGESIZE);
|
||||
zr_page = page > 0 ? (unsigned long)page : 4096UL;
|
||||
struct sigaction old;
|
||||
if (sigaction(SIGURG, NULL, &old) != 0) return;
|
||||
if ((old.sa_flags & SA_SIGINFO) || old.sa_handler != SIG_DFL) return;
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_sigaction = zr_handler;
|
||||
sa.sa_flags = SA_SIGINFO | SA_RESTART;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
if (sigaction(SIGURG, &sa, NULL) == 0) pthread_atfork(NULL, NULL, zr_child);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload"
|
||||
|
||||
PROGRAM = r"""
|
||||
#define _GNU_SOURCE
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static const char *ctl;
|
||||
static size_t size, page;
|
||||
static volatile int stop;
|
||||
static unsigned char *shared;
|
||||
|
||||
static long status(const char *key) {
|
||||
FILE *f = fopen("/proc/self/status", "r");
|
||||
char line[256];
|
||||
long v = -1;
|
||||
while (f && fgets(line, sizeof(line), f))
|
||||
if (!strncmp(line, key, strlen(key))) v = atol(line + strlen(key));
|
||||
if (f) fclose(f);
|
||||
return v;
|
||||
}
|
||||
|
||||
static void request(char mode) {
|
||||
int fd = open(ctl, O_WRONLY | O_TRUNC);
|
||||
if (fd < 0 || write(fd, &mode, 1) != 1) exit(3);
|
||||
close(fd);
|
||||
kill(getpid(), SIGURG);
|
||||
}
|
||||
|
||||
static uint64_t sum(const unsigned char *p, size_t n) {
|
||||
uint64_t s = 0;
|
||||
for (size_t i = 0; i < n; i += 64) s = s * 31 + p[i];
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Rewrites its pages with a round number and checks every page it wrote last round. */
|
||||
static void *writer(void *arg) {
|
||||
unsigned char *mine = shared + (size_t)(uintptr_t)arg * (size / 4);
|
||||
for (unsigned char round = 1; !stop; round++) {
|
||||
for (size_t i = 0; i < size / 4; i += page) {
|
||||
if (round > 1 && mine[i] != (unsigned char)(round - 1)) {
|
||||
printf("corrupt\n");
|
||||
exit(4);
|
||||
}
|
||||
mine[i] = round;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
ctl = argv[1];
|
||||
size = strtoul(argv[2], NULL, 0) << 20;
|
||||
int cycles = atoi(argv[3]);
|
||||
page = (size_t)sysconf(_SC_PAGESIZE);
|
||||
unsigned char *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
for (size_t i = 0; i < size; i++) data[i] = (unsigned char)(i * 7 + i / 4096);
|
||||
uint64_t before = sum(data, size);
|
||||
shared = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
memset(shared, 0, size);
|
||||
pthread_t t[2];
|
||||
for (uintptr_t i = 0; i < 2; i++) pthread_create(&t[i], NULL, writer, (void *)i);
|
||||
struct sigaction sa;
|
||||
sigaction(SIGURG, NULL, &sa);
|
||||
printf("handler %d\n", (sa.sa_flags & SA_SIGINFO) ? 1 : 0);
|
||||
if (argc > 4) {
|
||||
/* Read-only memory the process never pages out itself, swapped as the kernel would. */
|
||||
unsigned char *ro = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
memset(ro, 0x5a, size);
|
||||
mprotect(ro, size, PROT_READ);
|
||||
madvise(ro, size, 21);
|
||||
printf("elsewhere %ld\n", status("VmSwap:"));
|
||||
request('0');
|
||||
printf("back %ld\n", status("VmSwap:"));
|
||||
}
|
||||
for (int c = 0; c < cycles; c++) {
|
||||
request('1');
|
||||
printf("out %ld\n", status("VmSwap:"));
|
||||
request('0');
|
||||
printf("back %ld\n", status("VmSwap:"));
|
||||
}
|
||||
stop = 1;
|
||||
for (int i = 0; i < 2; i++) pthread_join(t[i], NULL);
|
||||
printf("intact %d\n", sum(data, size) == before);
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def swap_total_kb():
|
||||
for line in Path("/proc/meminfo").read_text().splitlines():
|
||||
if line.startswith("SwapTotal:"):
|
||||
return int(line.split()[1])
|
||||
return 0
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("cc"), "needs a C compiler")
|
||||
class ZramPreloadTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.dir = Path(tempfile.mkdtemp())
|
||||
cls.ctl = cls.dir / "ctl"
|
||||
build = cls.dir / "build"
|
||||
build.mkdir()
|
||||
(build / "program.c").write_text(PROGRAM)
|
||||
cls.lib = build / "libblsession.so"
|
||||
subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-Wall", "-pthread", "-DZR_CTL=\"%s\"" % cls.ctl,
|
||||
"-o", str(cls.lib), *map(str, sorted(PRELOAD.glob("*.c"))), "-ldl"], check=True)
|
||||
cls.steam = cls.dir / "steam"
|
||||
subprocess.run(["cc", "-O2", "-pthread", "-o", str(cls.steam), str(build / "program.c")], check=True)
|
||||
cls.game = cls.dir / "game"
|
||||
shutil.copy(cls.steam, cls.game)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
shutil.rmtree(cls.dir, True)
|
||||
|
||||
def setUp(self):
|
||||
self.ctl.write_text("0")
|
||||
|
||||
def run_program(self, program=None, mb=48, cycles=1, *extra):
|
||||
environment = dict(os.environ, LD_PRELOAD=str(self.lib))
|
||||
result = subprocess.run([str(program or self.steam), str(self.ctl), str(mb), str(cycles), *extra], env=environment,
|
||||
capture_output=True, text=True, check=True, timeout=120)
|
||||
out = {}
|
||||
for line in result.stdout.splitlines():
|
||||
key, value = line.split()
|
||||
out.setdefault(key, []).append(int(value))
|
||||
return out
|
||||
|
||||
def test_only_the_client_takes_the_signal(self):
|
||||
self.assertEqual([1], self.run_program()["handler"])
|
||||
self.assertEqual([0], self.run_program(self.game)["handler"])
|
||||
|
||||
def test_nothing_is_installed_without_the_control_file(self):
|
||||
self.ctl.unlink()
|
||||
try:
|
||||
self.assertEqual([0], self.run_program(cycles=0)["handler"])
|
||||
finally:
|
||||
self.ctl.write_text("0")
|
||||
|
||||
@unittest.skipUnless(swap_total_kb() > 256 * 1024, "needs swap")
|
||||
def test_pages_out_and_reads_back(self):
|
||||
out = self.run_program(mb=48)
|
||||
self.assertGreater(out["out"][0], 32 * 1024)
|
||||
self.assertLess(out["back"][0], out["out"][0] // 4)
|
||||
self.assertEqual([1], out["intact"])
|
||||
|
||||
@unittest.skipUnless(swap_total_kb() > 256 * 1024, "needs swap")
|
||||
def test_reads_back_what_the_kernel_swapped_elsewhere(self):
|
||||
out = self.run_program(None, 32, 0, "read-only")
|
||||
self.assertGreater(out["elsewhere"][0], 16 * 1024)
|
||||
self.assertLess(out["back"][0], out["elsewhere"][0] // 4)
|
||||
|
||||
def test_memory_stays_intact_under_writers(self):
|
||||
out = self.run_program(mb=32, cycles=6)
|
||||
self.assertEqual([1], out["intact"])
|
||||
self.assertEqual(6, len(out["out"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user