#!/bin/sh
# Подкачка экрана: сжатая память, при желании плюс файл на карте.
#
# Зачем она вообще. Измерено на живой плате: 452 МБ всего, 23 МБ свободно,
# процесс отрисовки держит 102 МБ, подкачки ноль. При нуле подкачки у ядра нет
# промежуточной стадии — как только следующий кадр видео не помещается, оно
# сразу выбирает жертву. Никакого «стало медленнее, но работает» между
# «хватает» и «убит» не существует.
#
# Две ступени, и они разные по природе:
#
#   zram   — страницы лежат сжатыми в той же оперативной памяти. Быстро
#            (микросекунды), карту не изнашивает, но платит за себя настоящей
#            памятью: втрое меньше, чем занимали бы страницы, — и всё же
#            платит. Поэтому у него есть потолок, см. MEM_LIMIT.
#   файл   — на карте памяти. Медленно (карта отвечает миллисекундами) и
#            изнашивает её, зато не отнимает ни байта оперативной. Нужен как
#            последний рубеж: лучше секунда задержки, чем убитый браузер.
#
# Приоритеты расставлены так, что карта включается только когда сжатая память
# упёрлась в свой потолок.
set -u

# Проверка файла, а не «. файл || true»: dash на отсутствующем файле для «.»
# завершает весь сценарий, и «|| true» это не перехватывает — юнит падал бы с
# кодом 2 и без единой строки в журнале.
[ -r /etc/default/dashdash ] && . /etc/default/dashdash

PERCENT="${DASHDASH_ZRAM_PERCENT:-50}"
SIZE_MB="${DASHDASH_ZRAM_MB:-}"
LIMIT_PERCENT="${DASHDASH_ZRAM_MEM_LIMIT_PERCENT:-40}"
ALGO="${DASHDASH_ZRAM_ALGO:-lz4}"
FILE_MB="${DASHDASH_SWAPFILE_MB:-0}"
FILE_PATH="${DASHDASH_SWAPFILE:-/var/swap}"

number() {
	case "${2:-}" in
		''|*[!0-9]*) echo "dashdash-zram: $1=«${2:-}» — не число" >&2; exit 1 ;;
	esac
}

total_mb=$(awk '/^MemTotal:/ {print int($2/1024)}' /proc/meminfo)

# ---------------------------------------------------------------- сжатая память

setup_zram() {
	number DASHDASH_ZRAM_PERCENT "$PERCENT"
	[ -n "$SIZE_MB" ] && number DASHDASH_ZRAM_MB "$SIZE_MB"
	number DASHDASH_ZRAM_MEM_LIMIT_PERCENT "$LIMIT_PERCENT"

	size_mb="${SIZE_MB:-$((total_mb * PERCENT / 100))}"
	[ "$size_mb" -gt 0 ] || { echo "dashdash-zram: сжатая память выключена"; return 0; }

	modprobe zram 2>/dev/null || true
	if [ ! -d /sys/class/zram-control ] && [ ! -e /sys/block/zram0 ]; then
		echo "dashdash-zram: в ядре нет zram" >&2
		return 1
	fi

	# Свободное устройство просим у ядра, а не берём zram0: на Armbian он часто
	# уже занят под /var/log, и mkswap поверх него стёр бы журнал.
	dev=""
	if command -v zramctl >/dev/null 2>&1; then
		dev=$(zramctl --find --size "${size_mb}MB" --algorithm "$ALGO" 2>/dev/null) || dev=""
	fi
	if [ -z "$dev" ]; then
		if [ -w /sys/class/zram-control/hot_add ]; then
			index=$(cat /sys/class/zram-control/hot_add) || index=0
		else
			index=0
		fi
		dev="/dev/zram${index}"
		[ -e "$dev" ] || { echo "dashdash-zram: не появилось $dev" >&2; return 1; }
		if [ "$(cat "/sys/block/zram${index}/disksize" 2>/dev/null || echo 0)" != "0" ]; then
			echo "dashdash-zram: $dev уже занят, не трогаю" >&2
			return 0
		fi
		echo "$ALGO" > "/sys/block/zram${index}/comp_algorithm" 2>/dev/null || true
		echo $((size_mb * 1024 * 1024)) > "/sys/block/zram${index}/disksize" || {
			echo "dashdash-zram: не задался размер" >&2
			return 1
		}
	fi

	name=$(basename "$dev")

	# Потолок настоящей памяти под сжатое хранилище — то, ради чего вообще
	# можно не бояться большого размера.
	#
	# disksize — это сколько страниц влезет, если они сожмутся как обещано.
	# Сколько памяти при этом займёт само хранилище, из него не следует: при
	# плохо сжимаемых данных (уже сжатое видео, например) отношение стремится к
	# единице, и zram размером в память съедает всю память. Тогда ядро начинает
	# вытеснять страницы, чтобы освободить память, — в хранилище, которое от
	# этого растёт. mem_limit обрывает эту спираль: упёрлись — ядро просто
	# перестаёт класть сюда и идёт на следующую ступень.
	if [ "$LIMIT_PERCENT" -gt 0 ] && [ -w "/sys/block/${name}/mem_limit" ]; then
		limit_mb=$((total_mb * LIMIT_PERCENT / 100))
		echo "${limit_mb}M" > "/sys/block/${name}/mem_limit" 2>/dev/null \
			&& limit_note=", потолок памяти ${limit_mb} МБ" \
			|| limit_note=""
	else
		limit_note=""
	fi

	mkswap "$dev" >/dev/null 2>&1 || { echo "dashdash-zram: mkswap не смог $dev" >&2; return 1; }
	# Приоритет выше файла: сжатая память быстрее карты на два порядка, и
	# трогать карту стоит только когда здесь кончилось место.
	swapon -p 100 "$dev" || { echo "dashdash-zram: swapon не смог $dev" >&2; return 1; }

	echo "dashdash-zram: $dev, ${size_mb} МБ, алгоритм ${ALGO}${limit_note}"
}

# ------------------------------------------------------------- файл на карте

setup_swapfile() {
	number DASHDASH_SWAPFILE_MB "$FILE_MB"
	[ "$FILE_MB" -gt 0 ] || return 0

	if swapon --show=NAME --noheadings 2>/dev/null | grep -qx "$FILE_PATH"; then
		echo "dashdash-zram: $FILE_PATH уже подключён"
		return 0
	fi

	if [ ! -f "$FILE_PATH" ]; then
		free_mb=$(df -Pm "$(dirname "$FILE_PATH")" | awk 'NR==2 {print $4}')
		# Запас в 200 МБ: карта, забитая под ноль файлом подкачки, — это
		# следующая поломка, уже не про память.
		if [ "${free_mb:-0}" -lt $((FILE_MB + 200)) ]; then
			echo "dashdash-zram: на карте всего ${free_mb:-0} МБ, файл на ${FILE_MB} МБ не создаю" >&2
			return 0
		fi
		# fallocate мгновенен, но на некоторых файловых системах даёт файл с
		# дырами, который swapon не примет. dd — медленно и наверняка.
		fallocate -l "${FILE_MB}M" "$FILE_PATH" 2>/dev/null \
			|| dd if=/dev/zero of="$FILE_PATH" bs=1M count="$FILE_MB" status=none \
			|| { echo "dashdash-zram: не создался $FILE_PATH" >&2; return 1; }
	fi

	# Файл подкачки, читаемый кем угодно, — это дамп чужой памяти.
	chmod 600 "$FILE_PATH"
	mkswap "$FILE_PATH" >/dev/null 2>&1 || { echo "dashdash-zram: mkswap не смог $FILE_PATH" >&2; return 1; }
	swapon -p 10 "$FILE_PATH" || { echo "dashdash-zram: swapon не смог $FILE_PATH" >&2; return 1; }
	echo "dashdash-zram: $FILE_PATH, ${FILE_MB} МБ, приоритет ниже сжатой памяти"
}

# --------------------------------------------------------------------- запуск

# Уже есть подкачка — значит её подняли до нас (Armbian или прошлый запуск
# этого же юнита). Второе устройство на 512 МБ соревнуется с первым за ту же
# память и делает хуже, а не лучше.
if [ "$(wc -l < /proc/swaps)" -gt 1 ]; then
	echo "dashdash-zram: подкачка уже есть:"
	swapon --show 2>/dev/null | sed 's/^/  /'
	exit 0
fi

rc=0
setup_zram || rc=1
setup_swapfile || rc=1
exit "$rc"
