#! /bin/sh
progname="${0##*/}"
progname="${progname%.sh}"

usage() {
  echo "Host side filter pipeline tool to convert kernel /proc/lockdep_chains via"
  echo "graphviz into dependency chart for visualization. Watch out for any up-arrows"
  echo "as they signify a circular dependency."
  echo
  echo "Usage: ${progname} [flags...] [regex...] < input-file > output-file"
  echo
  echo "flags:"
  echo "       --format={png|ps|svg|fig|imap|cmapx} | -T<format>"
  echo "           Output format, default png"
  echo "       --debug | -d"
  echo "           Leave intermediate files /tmp/${progname}.*"
  echo "       --verbose | -v"
  echo "           Do not strip address from lockname"
  echo "       --focus | -f"
  echo "           Show only primary references for regex matches"
  echo "       --cluster"
  echo "           Cluster the primary references for regex matches"
  echo "       --serial=<serial> | -s <serial>"
  echo "           Input from 'adb -s <serial> shell su 0 cat /proc/lockdep_chains'"
  echo "       --input=<filename> | -i <filename>"
  echo "           Input lockdeps from filename, otherwise from standard in"
  echo "       --output=<filename> | -o <filename>"
  echo "           Output formatted graph to filename, otherwise to standard out"
  echo
  echo "Chart is best viewed in portrait. ps or pdf formats tend to pixelate. png tends"
  echo "to hit a bug in cairo rendering at scale. Not having a set of regex matches for"
  echo "locknames will probably give you what you deserve ..."
  echo
  echo "Kernel Prerequisite to get /proc/lockdep_chains:"
  echo "       CONFIG_PROVE_LOCKING=y"
  echo "       CONFIG_LOCK_STAT=y"
  echo "       CONFIG_DEBUG_LOCKDEP=y"
}

rm -f /tmp/${progname}.*

# Indent rules and strip out address (may be overridden below)
beautify() {
  sed 's/^./    &/
       s/"[[][0-9a-f]*[]] /"/g'
}

input="cat -"
output="cat -"

dot_format="-Tpng"
filter=
debug=
focus=
cluster=

while [ ${#} -gt 0 ]; do
  case ${1} in

    -T | --format)
      dot_format="-T${2}"
      shift
      ;;

    -T*)
      dot_format="${1}"
      ;;

    --format=*)
      dot_format="-T${1#--format=}"
      ;;

    --debug | -d)
      debug=1
      ;;

    --verbose | -v)
      # indent, but do _not_ strip out addresses
      beautify() {
        sed 's/^./    &/'
      }
      ;;

    --focus | -f | --primary) # reserving --primary
      focus=1
      ;;

    --secondary) # reserving --secondary
      focus=
      ;;

    --cluster) # reserve -c for dot (configure plugins)
      cluster=1
      ;;

    --serial | -s)
      if [ "${input}" != "cat -" ]; then
        usage >&2
        echo "ERROR: --input or --serial can only be specified once" >&2
        exit 1
      fi
      input="adb -s ${2} shell su 0 cat /proc/lockdep_chains"
      shift
      ;;

    --serial=*)
      input="adb -s ${1#--serial=} shell su 0 cat /proc/lockdep_chains"
      ;;

    --input | -i)
      if [ "${input}" != "cat -" ]; then
        usage >&2
        echo "ERROR: --input or --serial can only be specified once" >&2
        exit 1
      fi
      input="cat ${2}"
      shift
      ;;

    --input=*)
      if [ "${input}" != "cat -" ]; then
        usage >&2
        echo "ERROR: --input or --serial can only be specified once" >&2
        exit 1
      fi
      input="cat ${1#--input=}"
      ;;

    --output | -o)
      if [ "${output}" != "cat -" ]; then
        usage >&2
        echo "ERROR: --output can only be specified once" >&2
        exit 1
      fi
      output="cat - > ${2}" # run through eval
      shift
      ;;

    --output=*)
      if [ "${output}" != "cat -" ]; then
        usage >&2
        echo "ERROR: --output can only be specified once" >&2
        exit 1
      fi
      output="cat - > ${1#--output=}" # run through eval
      ;;

    --help | -h | -\?)
      usage
      exit
      ;;

    *)
      # Everything else is a filter, which will also hide bad option flags,
      # which is an as-designed price we pay to allow "->rwlock" for instance.
      if [ X"${1}" = X"${1#* }" ]; then
        if [ -z "${filter}" ]; then
          filter="${1}"
        else
          filter="${filter}|${1}"
        fi
      else
        if [ -z "${filter}" ]; then
          filter=" ${1}"
        else
          filter="${filter}| ${1}"
        fi
      fi
      ;;

  esac
  shift
done

if [ -z "${filter}" ]; then
  echo "WARNING: no regex specified will give you what you deserve!" >&2
fi
if [ -n "${focus}" -a -z "${filter}" ]; then
  echo "WARNING: --focus without regex, ignored" >&2
fi
if [ -n "${cluster}" -a -z "${filter}" ]; then
  echo "WARNING: --cluster without regex, ignored" >&2
fi
if [ -n "${cluster}" -a -n "${focus}" -a -n "${filter}" ]; then
  echo "WARNING: orthogonal options --cluster & --focus, ignoring --cluster" >&2
  cluster=
fi

# convert to dot digraph series
${input} |
  sed '/^all lock chains:$/d
       / [&]__lockdep_no_validate__$/d
       /irq_context: 0/d
       s/irq_context: [1-9]/irq_context/
       s/..*/"&" ->/
       s/^$/;/' |
    sed ': loop
         N
         s/ ->\n;$/ ;/
         t
         s/ ->\n/ -> /
         b loop' > /tmp/${progname}.formed

if [ ! -s /tmp/${progname}.formed ]; then
  echo "ERROR: no input" >&2
  if [ -z "${debug}" ]; then
    rm -f /tmp/${progname}.*
  fi
  exit 2
fi

if [ -n "${filter}" ]; then
  grep "${filter}" /tmp/${progname}.formed |
    sed 's/ ;//
         s/ -> /|/g' |
      tr '|' '\n' |
        sort -u > /tmp/${progname}.symbols
fi

(
  echo 'digraph G {'
  (
    echo 'remincross="true";'
    echo 'concentrate="true";'
    echo

    if [ -s /tmp/${progname}.symbols ]; then
      if [ -n "${cluster}" ]; then
        echo 'subgraph cluster_symbols {'
        (
          grep "${filter}" /tmp/${progname}.symbols |
            sed 's/.*/& [shape=box] ;/'
          grep -v "${filter}" /tmp/${progname}.symbols |
            sed 's/.*/& [shape=diamond] ;/'
        ) | beautify
        echo '}'
      else
        grep "${filter}" /tmp/${progname}.symbols |
          sed 's/.*/& [shape=box] ;/'
        grep -v "${filter}" /tmp/${progname}.symbols |
          sed 's/.*/& [shape=diamond] ;/'
      fi

      echo
    fi
  ) | beautify

  if [ -s /tmp/${progname}.symbols ]; then
    if [ -z "${focus}" ]; then
      # Secondary relationships
      fgrep -f /tmp/${progname}.symbols /tmp/${progname}.formed
    else
      # Focus only on primary relationships
      grep "${filter}" /tmp/${progname}.formed
    fi
  else
    cat /tmp/${progname}.formed
  fi |
    # optimize int A -> B ; single references
    sed 's/\("[^"]*"\) -> \("[^"]*"\) ->/\1 -> \2 ;|\2 ->/g' |
      sed 's/\("[^"]*"\) -> \("[^"]*"\) ->/\1 -> \2 ;|\2 ->/g' |
        tr '|' '\n' |
          beautify |
            grep ' -> ' |
              sort -u |
                if [ -s /tmp/${progname}.symbols ]; then
                  beautify < /tmp/${progname}.symbols |
                    sed 's/^  */ /' > /tmp/${progname}.short
                  tee /tmp/${progname}.split |
                    fgrep -f /tmp/${progname}.short |
                      sed 's/ ;$/ [color=red] ;/'
                  fgrep -v -f /tmp/${progname}.short /tmp/${progname}.split
                  rm -f /tmp/${progname}.short /tmp/${progname}.split
                else
                  cat -
                fi

  echo '}'
) |
  tee /tmp/${progname}.input |
    if dot ${dot_format} && [ -z "${debug}" ]; then
      rm -f /tmp/${progname}.*
    fi |
      eval ${output}