Unverified Commit 51262240 authored by Mark Brown's avatar Mark Brown
Browse files

ASoC: dapm: improve debugfs output and introduce

Merge series from Luca Ceresoli <luca.ceresoli@bootlin.com>:

This patch series improves the tools available to understand and debug
DAPM.
parents bd74e9c3 e7bb4389
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -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>
+52 −2
Original line number Diff line number Diff line
@@ -2094,6 +2094,48 @@ static int dapm_power_widgets(struct snd_soc_card *card, int event)
}

#ifdef CONFIG_DEBUG_FS

static const char * const snd_soc_dapm_type_name[] = {
	[snd_soc_dapm_input]            = "input",
	[snd_soc_dapm_output]           = "output",
	[snd_soc_dapm_mux]              = "mux",
	[snd_soc_dapm_demux]            = "demux",
	[snd_soc_dapm_mixer]            = "mixer",
	[snd_soc_dapm_mixer_named_ctl]  = "mixer_named_ctl",
	[snd_soc_dapm_pga]              = "pga",
	[snd_soc_dapm_out_drv]          = "out_drv",
	[snd_soc_dapm_adc]              = "adc",
	[snd_soc_dapm_dac]              = "dac",
	[snd_soc_dapm_micbias]          = "micbias",
	[snd_soc_dapm_mic]              = "mic",
	[snd_soc_dapm_hp]               = "hp",
	[snd_soc_dapm_spk]              = "spk",
	[snd_soc_dapm_line]             = "line",
	[snd_soc_dapm_switch]           = "switch",
	[snd_soc_dapm_vmid]             = "vmid",
	[snd_soc_dapm_pre]              = "pre",
	[snd_soc_dapm_post]             = "post",
	[snd_soc_dapm_supply]           = "supply",
	[snd_soc_dapm_pinctrl]          = "pinctrl",
	[snd_soc_dapm_regulator_supply] = "regulator_supply",
	[snd_soc_dapm_clock_supply]     = "clock_supply",
	[snd_soc_dapm_aif_in]           = "aif_in",
	[snd_soc_dapm_aif_out]          = "aif_out",
	[snd_soc_dapm_siggen]           = "siggen",
	[snd_soc_dapm_sink]             = "sink",
	[snd_soc_dapm_dai_in]           = "dai_in",
	[snd_soc_dapm_dai_out]          = "dai_out",
	[snd_soc_dapm_dai_link]         = "dai_link",
	[snd_soc_dapm_kcontrol]         = "kcontrol",
	[snd_soc_dapm_buffer]           = "buffer",
	[snd_soc_dapm_scheduler]        = "scheduler",
	[snd_soc_dapm_effect]           = "effect",
	[snd_soc_dapm_src]              = "src",
	[snd_soc_dapm_asrc]             = "asrc",
	[snd_soc_dapm_encoder]          = "encoder",
	[snd_soc_dapm_decoder]          = "decoder",
};

static ssize_t dapm_widget_power_read_file(struct file *file,
					   char __user *user_buf,
					   size_t count, loff_t *ppos)
@@ -2104,6 +2146,9 @@ static ssize_t dapm_widget_power_read_file(struct file *file,
	int in, out;
	ssize_t ret;
	struct snd_soc_dapm_path *p = NULL;
	const char *c_name;

	BUILD_BUG_ON(ARRAY_SIZE(snd_soc_dapm_type_name) != SND_SOC_DAPM_TYPE_COUNT);

	buf = kmalloc(PAGE_SIZE, GFP_KERNEL);
	if (!buf)
@@ -2136,6 +2181,9 @@ static ssize_t dapm_widget_power_read_file(struct file *file,
				w->sname,
				w->active ? "active" : "inactive");

	ret += scnprintf(buf + ret, PAGE_SIZE - ret, " widget-type %s\n",
			 snd_soc_dapm_type_name[w->id]);

	snd_soc_dapm_for_each_direction(dir) {
		rdir = SND_SOC_DAPM_DIR_REVERSE(dir);
		snd_soc_dapm_widget_for_each_path(w, dir, p) {
@@ -2145,11 +2193,13 @@ static ssize_t dapm_widget_power_read_file(struct file *file,
			if (!p->connect)
				continue;

			c_name = p->node[rdir]->dapm->component ?
				p->node[rdir]->dapm->component->name : NULL;
			ret += scnprintf(buf + ret, PAGE_SIZE - ret,
					" %s  \"%s\" \"%s\"\n",
					" %s  \"%s\" \"%s\" \"%s\"\n",
					(rdir == SND_SOC_DAPM_DIR_IN) ? "in" : "out",
					p->name ? p->name : "static",
					p->node[rdir]->name);
					p->node[rdir]->name, c_name);
		}
	}

tools/sound/dapm-graph

0 → 100755
+303 −0
Original line number Diff line number Diff line
#!/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 "${@}"