#! /bin/sh

# ksm-mon v1.0
# Monitor memory savings of Kernel Same-page Merging (KSM)
# Copyright (c) 2016 Raphaël Halimi <raphael.halimi@gmail.com>

# Source shell-script-helper
. /lib/shell-script-helper


#
# Variables
#

# Configuration files
SYSTEM_CONFIG_FILE="/etc/$(basename "$0").conf"
USER_CONFIG_FILE="$HOME/.$(basename "$0").conf"

# Options defaults
REFRESH=1
SCALE=2

# Data sources
DATA_PATH="/sys/kernel/mm/ksm"
SYSFILE_LIST="pages_to_scan sleep_millisecs merge_across_nodes full_scans pages_shared pages_sharing pages_unshared pages_volatile"


#
# Functions
#

print_usage () {
  printf "Usage: %s [OPTION]...\n" "$(basename "$0")"
  printf "Monitor memory savings of Kernel Same-page Merging (KSM)\n"
  printf "\nOPTIONS:\n"
  print_option "-r REFRESH" "Refresh interval (in seconds), default is 1"
  print_option "-s SCALE" "Digits after the decimal point, default is 2"
  print_option "-v" "Verbose mode"
  print_option "-d" "Debug mode"
  print_option "-h" "Print this help message"
}


#
# Config files
#

[ -e "$SYSTEM_CONFIG_FILE" ] && . "$SYSTEM_CONFIG_FILE"
[ -e "$USER_CONFIG_FILE" ] && . "$USER_CONFIG_FILE"


#
# Options processing
#

while getopts "r:s:vdh" OPTION ; do
  case $OPTION in
    r) REFRESH="$OPTARG" ;;
    s) SCALE="$OPTARG" ;;
    v) enable_verbose ;;
    d) enable_debug ;;
    h) print_usage ; exit 0 ;;
    *) print_usage ; exit 1 ;;
  esac
done ; shift $((OPTIND-1))
print_debug "REFRESH=$REFRESH" "SCALE=$SCALE"


#
# Checks
#

[ "$(cat /sys/kernel/mm/ksm/run)" -ne 1 ] && die "Kernel Same-page Merging disabled"

# This script needs bc, top
for BINARY in bc top ; do
  which "$BINARY" > /dev/null || die "Please install $BINARY"
done


#
# MAIN
#

# Interrupt from keyboard *is* expected
trap - INT

# Main loop
while true ; do
  [ "$DEBUG" -eq 0 ] && clear
  printf "##### Raw data #####\n"
  print_verbose "Reading values from $DATA_PATH"

  # Files to read
  for SYSFILE in $SYSFILE_LIST ; do
    print_debug "SYSFILE=$SYSFILE"

    # Names and values
    SYSFILE_PRETTY_NAME="$(printf "%s" "$SYSFILE" | sed -r -e "s/^([[:alpha:]])/\U\1\E/" -e "s/_/ /g")"
    SYSFILE_VALUE="$(cat "$DATA_PATH/$SYSFILE")"
    print_debug "SYSFILE_PRETTY_NAME=$SYSFILE_PRETTY_NAME" "SYSFILE_VALUE=$SYSFILE_VALUE"

    # Sections
    case "$SYSFILE" in
      pages_to_scan|full_scans|pages_*shared)
        printf "\n" ;;
    esac

    # Display
    printf "%-25s%i\n" "$SYSFILE_PRETTY_NAME" "$SYSFILE_VALUE"

  done

  printf "\n##### Statistics #####\n\n"

  # CPU usage
  printf "%-25s%g%%\n" "CPU usage" "$(LANG=C top -n 1 -b -w | grep ksmd | awk '{print $9}')"

  # Memory savings
  printf "%-25s%i MB\n" "Shared memory" "$(($(cat "$DATA_PATH/pages_shared")*$(getconf PAGE_SIZE)/1024/1024))"
  printf "%-25s%i MB\n" "Saved memory" "$(($(cat "$DATA_PATH/pages_sharing")*$(getconf PAGE_SIZE)/1024/1024))"

  # Pages ratios
  if [ "$(cat "$DATA_PATH/pages_sharing")" -ne 0 ] ; then
    printf "%-25s%g\n" "Shared pages ratio" "$(printf "scale=%i; %i/%i\n" "$SCALE" "$(cat "$DATA_PATH/pages_sharing")" "$(cat "$DATA_PATH/pages_shared")" | bc -q)"
    printf "%-25s%g\n" "Unshared pages ratio" "$(printf "scale=%i; %i/%i\n" "$SCALE" "$(cat "$DATA_PATH/pages_unshared")" "$(cat "$DATA_PATH/pages_sharing")" | bc -q)"
  fi
  
  printf "\nPress Ctrl-C to stop."
  sleep "$REFRESH"
done
