From 51b50ec77451fb9164d6379bfe3ab9ade8a672c5 Mon Sep 17 00:00:00 2001
From: Scott Cheloha <cheloha@linux.ibm.com>
Date: Tue, 27 Jul 2021 11:30:58 -0500
Subject: [PATCH] lsdevinfo: optimize criteria filtering

lsdevinfo is significantly slower when a criteria filter is set with the
-q flag.

There are two culprits:

1. The criteria string given on the command line is parsed every time
   we call check_criteria().  We're forking two sed(1) processes whenever
   we hit that function.  We hit it a lot.

2. Criteria checking runs in constant time.  We call check_criteria() and
   do the parsing in (1) for every relevant attribute, even if we already
   have a match from a prior check_criteria() invocation.

We can fix issue (1) by parsing the criteria string once at the start
of the script.  I have added a function, parse_criteria(), that parses
the $criteria string and selects an appropriate matching function.

This approach also fixes problem (2), but we first need to check
whether the user's criteria is relevant to the attributes the script
cares about before calling the matching function.  We do this with
criteria_is_relevant().

The speed improvement is nice.  Consider my test machine with around
thirty devices.  On this machine, lsdevinfo without any options runs
in 1.35s, but with the "status=1" criteria it runs in 2.12s:

$ /usr/sbin/lsdevinfo | fgrep -c device:
29
$ command time -p /usr/sbin/lsdevinfo > /dev/null
real 1.35
user 1.42
sys 0.16
$ command time -p /usr/sbin/lsdevinfo -q status=1 > /dev/null
real 2.12
user 2.22
sys 0.30

With this patch, lsdevinfo with the "status=1" criteria now runs in
1.35s:

$ command time -p /usr/sbin/lsdevinfo -q status=1 > /dev/null
real 1.35
user 1.41
sys 0.18

This patch eliminates nearly all of the criteria-checking overhead in
the current code.

Signed-off-by: Scott Cheloha <cheloha@linux.ibm.com>
---
 scripts/lsdevinfo | 197 ++++++++++++++++++++++++++++++----------------
 1 file changed, 129 insertions(+), 68 deletions(-)

diff --git a/scripts/lsdevinfo b/scripts/lsdevinfo
index 1d9597bc4bc9..7a3cba3bee9f 100755
--- a/scripts/lsdevinfo
+++ b/scripts/lsdevinfo
@@ -61,37 +61,113 @@ show_version()
     echo "Written by: Santiago Leon <sleon@ec.ibm.com>"
 }
 
-# check_criteria
-# Modifies $show if the the attribute in the first parameter matches the
-# criteria from the command line.
-# The operands (=, !=, and LIKE) are defined the the lsdevinfo spec.
 #
-check_criteria()
+# Criteria matching boilerplate.
+#
+_class_eq() { [[ $class = $crit_rhs ]]; }
+_class_neq() { [[ $class != $crit_rhs ]]; }
+_class_like() { [[ $class =~ $crit_rhs ]]; }
+
+_driver_eq() { [[ $driver = $crit_rhs ]]; }
+_driver_neq() { [[ $driver != $crit_rhs ]]; }
+_driver_like() { [[ $driver =~ $crit_rhs ]]; }
+
+_name_eq() { [[ $name = $crit_rhs ]]; }
+_name_neq() { [[ $name != $crit_rhs ]]; }
+_name_like() { [[ $name =~ $crit_rhs ]]; }
+
+_parent_eq() { [[ $parent = $crit_rhs ]]; }
+_parent_neq() { [[ $parent != $crit_rhs ]]; }
+_parent_like() { [[ $parent =~ $crit_rhs ]]; }
+
+_physloc_eq() { [[ $physloc = $crit_rhs ]]; }
+_physloc_neq() { [[ $physloc != $crit_rhs ]]; }
+_physloc_like() { [[ $physloc =~ $crit_rhs ]]; }
+
+_prefix_eq() { [[ $prefix = $crit_rhs ]]; }
+_prefix_neq() { [[ $prefix != $crit_rhs ]]; }
+_prefix_like() { [[ $prefix =~ $crit_rhs ]]; }
+
+_status_eq() { [[ $status = $crit_rhs ]]; }
+_status_neq() { [[ $status != $crit_rhs ]]; }
+_status_like() { [[ $status =~ $crit_rhs ]]; }
+
+_subclass_eq() { [[ $subclass = $crit_rhs ]]; }
+_subclass_neq() { [[ $subclass != $crit_rhs ]]; }
+_subclass_like() { [[ $subclass =~ $crit_rhs ]]; }
+
+_type_eq() { [[ $type = $crit_rhs ]]; }
+_type_neq() { [[ $type != $crit_rhs ]]; }
+_type_like() { [[ $type =~ $crit_rhs ]]; }
+
+_uniquetype_eq() { [[ $uniquetype = $crit_rhs ]]; }
+_uniquetype_neq() { [[ $uniquetype != $crit_rhs ]]; }
+_uniquetype_like() { [[ $uniquetype =~ $crit_rhs ]]; }
+
+# Check if the attribute we're filtering on appears in the string
+# given as argument.
+criteria_is_relevant()
 {
-    attr=$1
-    attr_val=${!attr}
+    [[ "$1" =~ "$crit_lhs" ]]
+}
 
+# Run the criteria-matching function.
+criteria_matches()
+{
+    $criteria_checker
+}
+
+# Select a criteria-matching function based on the $criteria string.
+parse_criteria()
+{
     if [[ $criteria =~ "!=" ]] ; then
-	# Pull out the operands from the criteria (everything to the left of 
-	# the operand, and everything on the right of the operand)
-        crit_opd1=$(echo $criteria | $SED -e "s/[ ]*!=.*//")
-        crit_opd2=$(echo $criteria | $SED -e "s/.*!=[ ]*//")
-	# Perfom the comparison of the attribute and its value
-        if [[ $crit_opd1 == $attr && $crit_opd2 != $attr_val ]]; then
-	    show=1
-	fi
+        crit_lhs=$(echo $criteria | $SED -e "s/[ ]*!=.*//")
+        crit_rhs=$(echo $criteria | $SED -e "s/.*!=[ ]*//")
+	case "$crit_lhs" in
+	    class) criteria_checker=_class_neq;;
+	    driver) criteria_checker=_driver_neq;;
+	    name) criteria_checker=_name_neq;;
+	    parent) criteria_checker=_parent_neq;;
+	    physloc) criteria_checker=_physloc_neq;;
+	    prefix) criteria_checker=_prefix_neq;;
+	    status) criteria_checker=_status_neq;;
+	    subclass) criteria_checker=_subclass_neq;;
+	    type) criteria_checker=_type_neq;;
+	    uniquetype) criteria_checker=_uniquetype_neq;;
+	    *) criteria_checker=false;;
+	esac
     elif [[ $criteria =~ "=" ]]; then
-        crit_opd1=$(echo $criteria | $SED -e "s/[ ]*=.*//")
-        crit_opd2=$(echo $criteria | $SED -e "s/.*=[ ]*//")
-	if [[ $crit_opd1 == $attr && $crit_opd2 == $attr_val ]]; then
-	    show=1
-	fi
+        crit_lhs=$(echo $criteria | $SED -e "s/[ ]*=.*//")
+        crit_rhs=$(echo $criteria | $SED -e "s/.*=[ ]*//")
+	case "$crit_lhs" in
+	    class) criteria_checker=_class_eq;;
+	    driver) criteria_checker=_driver_eq;;
+	    name) criteria_checker=_name_eq;;
+	    parent) criteria_checker=_parent_eq;;
+	    physloc) criteria_checker=_physloc_eq;;
+	    prefix) criteria_checker=_prefix_eq;;
+	    status) criteria_checker=_status_eq;;
+	    subclass) criteria_checker=_subclass_eq;;
+	    type) criteria_checker=_type_eq;;
+	    uniquetype) criteria_checker=_uniquetype_eq;;
+	    *) criteria_checker=false;;
+	esac
     elif [[ $criteria =~ " LIKE " ]]; then
-        crit_opd1=$(echo $criteria | $SED -e "s/[ ]*LIKE.*//")
-        crit_opd2=$(echo $criteria | $SED -e "s/.*LIKE[ ]*//")
-	if [[ $crit_opd1 == $attr && $attr_val =~ $crit_opd2 ]]; then
-	    show=1
-	fi
+        crit_lhs=$(echo $criteria | $SED -e "s/[ ]*LIKE.*//")
+        crit_rhs=$(echo $criteria | $SED -e "s/.*LIKE[ ]*//")
+	case "$crit_lhs" in
+	    class) criteria_checker=_class_like;;
+	    driver) criteria_checker=_driver_like;;
+	    name) criteria_checker=_name_like;;
+	    parent) criteria_checker=_parent_like;;
+	    physloc) criteria_checker=_physloc_like;;
+	    prefix) criteria_checker=_prefix_like;;
+	    status) criteria_checker=_status_like;;
+	    subclass) criteria_checker=_subclass_like;;
+	    type) criteria_checker=_type_like;;
+	    uniquetype) criteria_checker=_uniquetype_like;;
+	    *) criteria_checker=false;;
+	esac
     else
         echo "Criteria must have =, !=, or LIKE operand. Exiting."
         exit 1
@@ -124,6 +200,8 @@ recursive=0
 
 # default: display all devices
 criteria=""
+criteria_checker=:
+crit_lhs=""
 
 # default: display all attributes
 format=""
@@ -162,6 +240,12 @@ if [[ $criteria =~ " AND " ]] ; then
     exit 1
 fi
 
+# If we have a criteria string, parse it and choose a criteria
+# matching function.
+if [[ -n "$criteria" ]]; then
+    parse_criteria
+fi
+
 # Fill variables for the two display formats (regular and comma-separated) so
 # we can print the output in a single place.
 if [[ $comma_sep -eq 0 ]]; then
@@ -184,15 +268,10 @@ show_eth ()
     # if there is a criteria in the command line, check if this device matches
     if [[ $criteria != "" ]] ; then
         show=0
-        check_criteria "name"
-        check_criteria "physloc"
-        check_criteria "uniquetype"
-        check_criteria "class"
-        check_criteria "subclass"
-        check_criteria "type"
-        check_criteria "prefix"
-        check_criteria "driver"
-        check_criteria "status"
+        attrs="name physloc uniquetype class subclass type prefix driver status"
+        if criteria_is_relevant "$attrs" && criteria_matches; then
+            show=1
+        fi
     fi
 
     # print the info only if the device matches the criteria
@@ -330,15 +409,10 @@ for dev in $($LS -d /proc/device-tree/vdevice/v-scsi* 2> /dev/null) ; do
 	# device matches
 	if [[ $criteria != "" ]] ; then
 	    show=0
-	    check_criteria "name"
-	    check_criteria "physloc"
-	    check_criteria "status"
-	    check_criteria "uniquetype"
-	    check_criteria "class"
-	    check_criteria "subclass"
-	    check_criteria "type"
-	    check_criteria "prefix"
-	    check_criteria "driver"
+	    attrs="name physloc status uniquetype class subclass type prefix driver"
+	    if criteria_is_relevant "$attrs" && criteria_matches; then
+		show=1
+	    fi
 	fi
 
 	if [[ $show -ne 0 ]]; then
@@ -395,14 +469,10 @@ for dev in $($LS -d /proc/device-tree/vdevice/v-scsi* 2> /dev/null) ; do
 	        # if there is a criteria in the command line, check if this
 	        # device matches
 	        show=0
-	        check_criteria "name"
-	        check_criteria "status"
-	        check_criteria "physloc"
-	        check_criteria "parent"
-	        check_criteria "uniquetype"
-	        check_criteria "class"
-	        check_criteria "subclass"
-	        check_criteria "type"
+		attrs="name status physloc parent uniquetype class subclass type"
+		if criteria_is_relevant "$attrs" && criteria_matches; then
+		    show=1
+		fi
 	    else
 	        show=1
 	    fi
@@ -475,15 +545,10 @@ for dev in $($LS -d /proc/device-tree/vdevice/vfc-client* 2> /dev/null) ; do
 	# device matches
 	if [[ $criteria != "" ]] ; then
 	    show=0
-	    check_criteria "name"
-	    check_criteria "physloc"
-	    check_criteria "status"
-	    check_criteria "uniquetype"
-	    check_criteria "class"
-	    check_criteria "subclass"
-	    check_criteria "type"
-	    check_criteria "prefix"
-	    check_criteria "driver"
+	    attrs="name physloc status uniquetype class subclass type prefix driver"
+	    if criteria_is_relevant "$attrs" && criteria_matches; then
+		show=1
+	    fi
 	fi
 
 	if [[ $show -ne 0 ]]; then
@@ -543,14 +608,10 @@ for dev in $($LS -d /proc/device-tree/vdevice/vfc-client* 2> /dev/null) ; do
 		    # if there is a criteria in the command line, check if this
 		    # device matches
                         show=0
-			check_criteria "name"
-			check_criteria "physloc"
-			check_criteria "status"
-			check_criteria "parent"
-			check_criteria "uniquetype"
-			check_criteria "class"
-			check_criteria "subclass"
-			check_criteria "type"
+			attrs="name physloc status parent uniquetype class subclass type"
+			if criteria_is_relevant "$attrs" && criteria_matches; then
+			    show=1
+			fi
 		    else
                         show=1
 		    fi
-- 
2.31.1

