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.