Motivation.

Now and then one wants to know the amount of RAM used by a particular program. With multiprocessing this is harder than it looks. There's even a cli tool smem. It's a nice tool written in Python. Making it painfully slow for interactive cli use. The proper way to do this is with cgroups. This assumes you know ahead of time the program you want to monitor and you have cgroups available and setup.

Solution

To that end lets script this, in the process we compare awk, R, and python. The command pgrep -f command_name will spit out the desired process PID's, which we pass on to ps. Like so:

# shell function
smem() {
    ps -o "uss,rss,pss" $(pgrep -f "$1")
}

So for example smem firefox will spit out a table listing USS, RSS and PSS RAM use of each firefox thread. All that's left is to sum each column to get the totals and we're done. Note pgrep matches a regex, so you might want to verify what you are matching. A better alternative would be

smem() {
    local MATCH=$(pgrep -f "$1")
    ps -o "command" $MATCH
    ps -o "uss,rss,pss" $MATCH
}

The simplest way to sum the columns of the table is to write a script. The scripts are written in awk, R and Python. They all do the same thing. They sum every column of a table. For example:

$ A=$(cat <<EOF
A B
1 2
3 4
EOF
)

$ echo "$A" | table_total.awk
         A          B
         4          6

table_total.awk takes two optional arguments, the first one is input units, and the second one is output unit format.

$ echo "$A" | table_total.awk 1000

         A          B
      4000       6000

$ echo "$A"  | table_total.awk 1000 si

         A          B
      4.0k       6.0k

The final command (in smem.sh) becomes:

# Display output in binary units
smem_awk() {
    ps -o "uss,rss,pss" $(pgrep -f "$1") | table_total.awk 1024 iec

}

# Example: chromium with no tabs open uses 308M of unique RAM, but 1.3G of non-unique RAM.
$ smem_awk chrome

       USS        RSS        PSS
      308M       1.3G       525M

The Python script is short and elegant. The R script is also short, but ugly. The awk script is lengthy. Lets compare performance.

$ T=$(cat <<EOF
USS   RSS   PSS
 3008  7740  3297
 1680  6188  1904
   44  4164   134
 2184  3816  2472
   52  4288   147
EOF
)

# side note: table_total.awk is calling numfmt which seems a bit off in some cases.
# Example, converting 25.6Mi gives 26Mi.
$ numfmt --from-unit=$((1<<20)) --to=iec-i 25.6
26Mi

$ time table_total.awk 1024 iec-i <<<"$T"
       USS        RSS        PSS
     6.9Mi       26Mi      7.8Mi

real    0m0.007s
user    0m0.006s
sys 0m0.001s

$ time table_total.R 1024 iec <<<"$T"
      USS      RSS      PSS
  6.8 MiB 25.6 MiB  7.8 MiB

real    0m0.211s
user    0m0.551s
sys 0m0.032s

$ time table_total.py 1024 iec <<<"$T"
       USS         RSS         PSS
   6.8 MiB    25.6 MiB     7.8 MiB

real    0m0.451s
user    0m0.781s
sys 0m0.068s

Python is painfully slow at almost half a second. R is a bit slow but still snappy. Awk is blazing fast. Guess which I'll be using.

To be fair this is an apples to oranges comparison. The reason the Python version so clean, concise and simple is because it's loading some powerful libraries to do the work. Were the Python code written in the same manner as awk it would have a different performance profile. It would also be longer and uglier.

What happens at scale?

What if our table is massive? Spoiler: Python comes out significantly ahead.

To that effect lets write a script to generate a table of random integers between 0 and 100:

# a 3 by 4 table
$ table_gen.py 3 4
 A  B  C  D
59 42 41 65
 7  2 18 80
26 67 67 17

# table with 2 million rows and 5 columns
$ table_gen.py 2000000 5 > table.txt

$ time table_total.awk < table.txt
         A          B          C          D          E
  98951126   99034755   98973663   98991072   98951685

real    0m2.844s
user    0m2.836s
sys     0m0.008s

$ time table_total.R < table.txt
        A        B        C        D        E
 98951126 99034755 98973663 98991072 98951685

real    0m2.241s
user    0m2.519s
sys     0m0.161s

$ time table_total.py < table.txt
         A           B           C           D           E
  98951126    99034755    98973663    98991072    98951685

real    0m1.180s
user    0m1.283s
sys     0m0.340s

The file table.txt only takes up 30M. Once you get into even larger tables python is a clear winner, but the startup cost of python is much too high for a quick interactive system tool. (At least the way I implemented it.)

We could also process the table in parallel across different cpu cores. With 4 cores, using gnu parallel,

$ time parallel --pipe-part -a table.txt --block -1 table_total.awk | table_total.awk
         A          B          C          D          E
  98951126   99034755   98973663   98991072   98951685

real    0m0.866s
user    0m2.989s
sys     0m0.077s

The Python and R scripts would need to be modified to handle missing headers to do this properly. Note that while the real time is lower than the Python script, the user time is much higher due to utilizing multiple cpu cores. In other words the Python script is much more efficient, and parallelizing should yield a greater benefit. Due to a quirk in awk (namely char + digit = digit, i.e 'A' + 2 = 2), we can do

$ time parallel --pipe-part -a table.txt --block -1 table_total.py  | table_total.awk
  98951126   99034755   98973663   98991072   98951685

real    0m0.948s
user    0m2.852s
sys     0m0.426s

Note the missing header in the output.

This is not faster than parallelizing awk alone, because of Python startup cost. But if we were to scale up the table size yet again, we should see performance benefits.