ASoC: dapm-graph: new tool to visualize DAPM state
authorLuca Ceresoli <luca.ceresoli@bootlin.com>
Tue, 16 Apr 2024 06:00:26 +0000 (08:00 +0200)
committerMark Brown <broonie@kernel.org>
Sun, 21 Apr 2024 00:58:17 +0000 (09:58 +0900)
Add a tool to generate a picture of the current DAPM state for a sound
card.

dapm-graph is inspired by vizdapm which used to be published on a Wolfson
Micro git repository now disappeared, and has a few forks around:

  https://github.com/mihais/asoc-tools
  https://github.com/alexandrebelloni/asoc-tools

dapm-graph is a full reimplementation with several improvements while still
being a self-contained shell script:

Improvements to rendered output:
 - shows the entire card, not one component hierarchy only
 - each component is rendered in a separate box
 - shows widget on/off status based on widget information alone (the
   original vizdapm propagates the "on" green colour to the first input
   widget)
 - use bold line and gray background and not only green/red line to show
   on/off status (for the color blind)

Improvements for embedded system developers:
 - remote mode: get state of remote device (possibly with minimal rootfs)
   via SSH, but parsing locally for faster operation
 - compatible with BusyBox shell, not only bash

Usability improvements:
 - flexible command line (uses getopts for parsing)
 - detailed help text
 - flag to enable detailed debug logging
 - graphviz output format detected from file extension, not hard coded
 - a self-contained shell script

Usage is designed to be simple:

  dapm-grpah -c CARD                  - get state from debugfs for CARD
  dapm-grpah -c CARD -r REMOTE_TARGET - same, but remotely via SSH
  dapm-grpah -d STATE_DIR             - from a local copy of the debugfs
                                        tree for a card

Signed-off-by: Luca Ceresoli <luca.ceresoli@bootlin.com>
Reviewed-by: Alexandre Belloni <alexandre.belloni@bootlin.com>
Link: https://lore.kernel.org/r/20240416-vizdapm-ng-v1-3-5d33c0b57bc5@bootlin.com
Signed-off-by: Mark Brown <broonie@kernel.org>
MAINTAINERS
tools/sound/dapm-graph [new file with mode: 0755]

index a7d0dd91ac198b4f8c6cfcb355a81d373d1510c6..aa3b455588e1968543682f907ad4e65d835323bb 100644 (file)
@@ -20669,6 +20669,12 @@ F:     include/trace/events/sof*.h
 F:     include/uapi/sound/asoc.h
 F:     sound/soc/
 
+SOUND - SOC LAYER / dapm-graph
+M:     Luca Ceresoli <luca.ceresoli@bootlin.com>
+L:     linux-sound@vger.kernel.org
+S:     Maintained
+F:     tools/sound/dapm-graph
+
 SOUND - SOUND OPEN FIRMWARE (SOF) DRIVERS
 M:     Pierre-Louis Bossart <pierre-louis.bossart@linux.intel.com>
 M:     Liam Girdwood <lgirdwood@gmail.com>
diff --git a/tools/sound/dapm-graph b/tools/sound/dapm-graph
new file mode 100755 (executable)
index 0000000..57d78f6
--- /dev/null
@@ -0,0 +1,303 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+#
+# Generate a graph of the current DAPM state for an audio card
+#
+# Copyright 2024 Bootlin
+# Author: Luca Ceresoli <luca.ceresol@bootlin.com>
+
+set -eu
+
+STYLE_NODE_ON="shape=box,style=bold,color=green4"
+STYLE_NODE_OFF="shape=box,style=filled,color=gray30,fillcolor=gray95"
+
+# Print usage and exit
+#
+# $1 = exit return value
+# $2 = error string (required if $1 != 0)
+usage()
+{
+    if [  "${1}" -ne 0 ]; then
+       echo "${2}" >&2
+    fi
+
+    echo "
+Generate a graph of the current DAPM state for an audio card.
+
+The DAPM state can be obtained via debugfs for a card on the local host or
+a remote target, or from a local copy of the debugfs tree for the card.
+
+Usage:
+    $(basename $0) [options] -c CARD                  - Local sound card
+    $(basename $0) [options] -c CARD -r REMOTE_TARGET - Card on remote system
+    $(basename $0) [options] -d STATE_DIR             - Local directory
+
+Options:
+    -c CARD             Sound card to get DAPM state of
+    -r REMOTE_TARGET    Get DAPM state from REMOTE_TARGET via SSH and SCP
+                        instead of using a local sound card
+    -d STATE_DIR        Get DAPM state from a local copy of a debugfs tree
+    -o OUT_FILE         Output file (default: dapm.dot)
+    -D                  Show verbose debugging info
+    -h                  Print this help and exit
+
+The output format is implied by the extension of OUT_FILE:
+
+ * Use the .dot extension to generate a text graph representation in
+   graphviz dot syntax.
+ * Any other extension is assumed to be a format supported by graphviz for
+   rendering, e.g. 'png', 'svg', and will produce both the .dot file and a
+   picture from it. This requires the 'dot' program from the graphviz
+   package.
+"
+
+    exit ${1}
+}
+
+# Connect to a remote target via SSH, collect all DAPM files from debufs
+# into a tarball and get the tarball via SCP into $3/dapm.tar
+#
+# $1 = target as used by ssh and scp, e.g. "root@192.168.1.1"
+# $2 = sound card name
+# $3 = temp dir path (present on the host, created on the target)
+# $4 = local directory to extract the tarball into
+#
+# Requires an ssh+scp server, find and tar+gz on the target
+#
+# Note: the tarball is needed because plain 'scp -r' from debugfs would
+# copy only empty files
+grab_remote_files()
+{
+    echo "Collecting DAPM state from ${1}"
+    dbg_echo "Collected DAPM state in ${3}"
+
+    ssh "${1}" "
+set -eu &&
+cd \"/sys/kernel/debug/asoc/${2}\" &&
+find * -type d -exec mkdir -p ${3}/dapm-tree/{} \; &&
+find * -type f -exec cp \"{}\" \"${3}/dapm-tree/{}\" \; &&
+cd ${3}/dapm-tree &&
+tar cf ${3}/dapm.tar ."
+    scp -q "${1}:${3}/dapm.tar" "${3}"
+
+    mkdir -p "${4}"
+    tar xf "${tmp_dir}/dapm.tar" -C "${4}"
+}
+
+# Parse a widget file and generate graph description in graphviz dot format
+#
+# Skips any file named "bias_level".
+#
+# $1 = temporary work dir
+# $2 = component name
+# $3 = widget filename
+process_dapm_widget()
+{
+    local tmp_dir="${1}"
+    local c_name="${2}"
+    local w_file="${3}"
+    local dot_file="${tmp_dir}/main.dot"
+    local links_file="${tmp_dir}/links.dot"
+
+    local w_name="$(basename "${w_file}")"
+    local w_tag="${c_name}_${w_name}"
+
+    if [ "${w_name}" = "bias_level" ]; then
+       return 0
+    fi
+
+    dbg_echo "   + Widget: ${w_name}"
+
+    cat "${w_file}" | (
+       read line
+
+       if echo "${line}" | grep -q ': On '
+       then local node_style="${STYLE_NODE_ON}"
+       else local node_style="${STYLE_NODE_OFF}"
+       fi
+
+       local w_type=""
+       while read line; do
+           # Collect widget type if present
+           if echo "${line}" | grep -q '^widget-type '; then
+               local w_type_raw="$(echo "$line" | cut -d ' ' -f 2)"
+               dbg_echo "     - Widget type: ${w_type_raw}"
+
+               # Note: escaping '\n' is tricky to get working with both
+               # bash and busybox ash, so use a '%' here and replace it
+               # later
+               local w_type="%n[${w_type_raw}]"
+           fi
+
+           # Collect any links. We could use "in" links or "out" links,
+           # let's use "in" links
+           if echo "${line}" | grep -q '^in '; then
+               local w_src=$(echo "$line" |
+                                 awk -F\" '{print $6 "_" $4}' |
+                                 sed  's/^(null)_/ROOT_/')
+               dbg_echo "     - Input route from: ${w_src}"
+               echo "  \"${w_src}\" -> \"$w_tag\"" >> "${links_file}"
+           fi
+       done
+
+       echo "    \"${w_tag}\" [label=\"${w_name}${w_type}\",${node_style}]" |
+           tr '%' '\\' >> "${dot_file}"
+   )
+}
+
+# Parse the DAPM tree for a sound card component and generate graph
+# description in graphviz dot format
+#
+# $1 = temporary work dir
+# $2 = component directory
+# $3 = forced component name (extracted for path if empty)
+process_dapm_component()
+{
+    local tmp_dir="${1}"
+    local c_dir="${2}"
+    local c_name="${3}"
+    local dot_file="${tmp_dir}/main.dot"
+    local links_file="${tmp_dir}/links.dot"
+
+    if [ -z "${c_name}" ]; then
+       # Extract directory name into component name:
+       #   "./cs42l51.0-004a/dapm" -> "cs42l51.0-004a"
+       c_name="$(basename $(dirname "${c_dir}"))"
+    fi
+
+    dbg_echo " * Component: ${c_name}"
+
+    echo ""                           >> "${dot_file}"
+    echo "  subgraph \"${c_name}\" {" >> "${dot_file}"
+    echo "    cluster = true"         >> "${dot_file}"
+    echo "    label = \"${c_name}\""  >> "${dot_file}"
+    echo "    color=dodgerblue"       >> "${dot_file}"
+
+    # Create empty file to ensure it will exist in all cases
+    >"${links_file}"
+
+    # Iterate over widgets in the component dir
+    for w_file in ${c_dir}/*; do
+       process_dapm_widget "${tmp_dir}" "${c_name}" "${w_file}"
+    done
+
+    echo "  }" >> "${dot_file}"
+
+    cat "${links_file}" >> "${dot_file}"
+}
+
+# Parse the DAPM tree for a sound card and generate graph description in
+# graphviz dot format
+#
+# $1 = temporary work dir
+# $2 = directory tree with DAPM state (either in debugfs or a mirror)
+process_dapm_tree()
+{
+    local tmp_dir="${1}"
+    local dapm_dir="${2}"
+    local dot_file="${tmp_dir}/main.dot"
+
+    echo "digraph G {" > "${dot_file}"
+    echo "  fontname=\"sans-serif\"" >> "${dot_file}"
+    echo "  node [fontname=\"sans-serif\"]" >> "${dot_file}"
+
+
+    # Process root directory (no component)
+    process_dapm_component "${tmp_dir}" "${dapm_dir}/dapm" "ROOT"
+
+    # Iterate over components
+    for c_dir in "${dapm_dir}"/*/dapm
+    do
+       process_dapm_component "${tmp_dir}" "${c_dir}" ""
+    done
+
+    echo "}" >> "${dot_file}"
+}
+
+main()
+{
+    # Parse command line
+    local out_file="dapm.dot"
+    local card_name=""
+    local remote_target=""
+    local dapm_tree=""
+    local dbg_on=""
+    while getopts "c:r:d:o:Dh" arg; do
+       case $arg in
+           c)  card_name="${OPTARG}"      ;;
+           r)  remote_target="${OPTARG}"  ;;
+           d)  dapm_tree="${OPTARG}"      ;;
+           o)  out_file="${OPTARG}"       ;;
+           D)  dbg_on="1"                 ;;
+           h)  usage 0                    ;;
+           *)  usage 1                    ;;
+       esac
+    done
+    shift $(($OPTIND - 1))
+
+    if [ -n "${dapm_tree}" ]; then
+       if [ -n "${card_name}${remote_target}" ]; then
+           usage 1 "Cannot use -c and -r with -d"
+       fi
+       echo "Using local tree: ${dapm_tree}"
+    elif [ -n "${remote_target}" ]; then
+       if [ -z "${card_name}" ]; then
+           usage 1 "-r requires -c"
+       fi
+       echo "Using card ${card_name} from remote target ${remote_target}"
+    elif [ -n "${card_name}" ]; then
+       echo "Using local card: ${card_name}"
+    else
+       usage 1 "Please choose mode using -c, -r or -d"
+    fi
+
+    # Define logging function
+    if [ "${dbg_on}" ]; then
+       dbg_echo() {
+           echo "$*" >&2
+       }
+    else
+       dbg_echo() {
+           :
+       }
+    fi
+
+    # Filename must have a dot in order the infer the format from the
+    # extension
+    if ! echo "${out_file}" | grep -qE '\.'; then
+       echo "Missing extension in output filename ${out_file}" >&2
+       usage
+       exit 1
+    fi
+
+    local out_fmt="${out_file##*.}"
+    local dot_file="${out_file%.*}.dot"
+
+    dbg_echo "dot file:      $dot_file"
+    dbg_echo "Output file:   $out_file"
+    dbg_echo "Output format: $out_fmt"
+
+    tmp_dir="$(mktemp -d /tmp/$(basename $0).XXXXXX)"
+    trap "{ rm -fr ${tmp_dir}; }" INT TERM EXIT
+
+    if [ -z "${dapm_tree}" ]
+    then
+       dapm_tree="/sys/kernel/debug/asoc/${card_name}"
+    fi
+    if [ -n "${remote_target}" ]; then
+       dapm_tree="${tmp_dir}/dapm-tree"
+       grab_remote_files "${remote_target}" "${card_name}" "${tmp_dir}" "${dapm_tree}"
+    fi
+    # In all cases now ${dapm_tree} contains the DAPM state
+
+    process_dapm_tree "${tmp_dir}" "${dapm_tree}"
+    cp "${tmp_dir}/main.dot" "${dot_file}"
+
+    if [ "${out_file}" != "${dot_file}" ]; then
+       dot -T"${out_fmt}" "${dot_file}" -o "${out_file}"
+    fi
+
+    echo "Generated file ${out_file}"
+}
+
+main "${@}"