When setting an environment variable gives you a 40x speedup
timestamp1556307720001
Today, we’d like to share some of our recent work on Sherlock that allowed a pretty significant speedup when listing files in directories with a lot of entries.
Unlike our usual announcements, this post is more of a behind-the-scenes account of things we do on a regular basis on Sherlock, to keep it up and running in the best possible conditions for our users. We hope to have more of this in the future.
Listing many files takes time
It all started from a support question, from a user reporting a usability problem with ls
taking several minutes to list the contents of a 15,000+ entries directory on $SCRATCH
.
Having thousands of files in a single directory is usually not very file system-friendly, and definitely not recommended. The user knew this already and admitted that wasn’t great, but when he mentioned his laptop was 1,000x faster than Sherlock to list this directory’s contents, of course, it stung. So we looked deeper.
Because ls
is nice
We looked at what ls
actually does to list the contents of a directory, and why it was taking so long to list files. On most modern distributions, ls
is aliased to ls --color=auto
by default. Which is nice, because everybody likes 🌈.
But those pretty colors come at a price: for each and every file it displays, ls
needs to get information about a file’s type, its permissions, flags, extended attributes and the like, to choose the appropriate color to display.
One easy solution to our problem would have been to disable colored output in ls
altogether, but imagine the uproar. There is no way we could have taken 🌈 away from users, we’re not monsters.
So we looked deeper. ls
does coloring through the LS_COLORS
environment variable, which is set by dircolors(1)
, based on a dir_colors(5)
configuration file. Yes, that’s right: an executable reads a config file to produce an environment variable that is in turn used by ls
.[1]
🤯
Let’s dive in
To be able to determine which of those specific coloring schemes were responsible for the slowdowns, we created an experimental environment:
$ mkdir $SCRATCH/dont
$ touch $SCRATCH/dont/{1..10000} # don't try this at home!
$ time ls --color=always $SCRATCH/dont | wc -l
10000
real 0m12.758s
user 0m0.104s
sys 0m0.699s
12.7s for 10,000 files, not great. 🐌
BTW, we need the
--color=always
flag, because, although it’s aliased tols --color=auto
,ls
detects when it’s not attached to a terminal (like when piped to something or with its output redirected), and then turns off coloring when set toauto
. Smart guy.
So, what’s taking so much time? Equipped with our strace
-fu, we looked:
$ strace -c ls --color=always $SCRATCH/dont | wc -l
10000
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
44.21 0.186617 19 10000 lstat
42.60 0.179807 18 10000 10000 getxattr
12.19 0.051438 5 10000 capget
0.71 0.003002 38 80 getdents
0.07 0.000305 10 30 mmap
0.05 0.000217 12 18 mprotect
0.03 0.000135 14 10 read
0.03 0.000123 11 11 open
0.02 0.000082 6 14 close
[...]
Wow: 10,000 calls to lstat()
, 10,000 calls to getxattr()
(which all fail by the way, because the attributes that ls
is looking for don’t exist in our environment), 10,000 calls to capget()
.
Can do better for sure.
File capabilities? Nah
Following advice from a 10+ year-old bug, we tried file disabling capability checking:
$ eval $(dircolors -b | sed s/ca=[^:]*:/ca=:/)
$ time strace -c ls --color=always $SCRATCH/dont | wc -l
10000
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
98.95 0.423443 42 10000 lstat
0.78 0.003353 42 80 getdents
0.04 0.000188 10 18 mprotect
0.04 0.000181 6 30 mmap
0.02 0.000085 9 10 read
0.02 0.000084 28 3 mremap
0.02 0.000077 7 11 open
0.02 0.000066 5 14 close
[...]
------ ----------- ----------- --------- --------- ----------------
100.00 0.427920 10221 6 total
real 0m8.160s
user 0m0.115s
sys 0m0.961s
Woohoo, down to 8s! We got rid of all those expensive getxattr()
calls, and capget()
calls are gone too, 👍.
We still have all those pesky lstat()
, though…
How many colors is too many colors?
So we took a look at LS_COLORS
in more details.
The first attempt was to simply unset that variable:
$ echo $LS_COLORS
rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:ca=:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.axa=00;36:*.oga=00;36:*.spx=00;36:*.xspf=00;36:
$ unset LS_COLORS
$ echo $LS_COLORS
$ time ls --color=always $SCRATCH/dont | wc -l
10000
real 0m13.037s
user 0m0.077s
sys 0m1.092s
Whaaaaat!?! Still 13s?
It turns out that when the LS_COLORS
environment variable is not defined, or when just one of its <type>=color:
elements is not there, it defaults to its embedded database and uses colors anyway. So if you want to disable coloring for a specific file type, you need to override it with <type>=:
, or <type> 00
in the DIR_COLORS
file.
After a lot of trial and error, we narrowed it down to this:
EXEC 00
SETUID 00
SETGID 00
CAPABILITY 00
which translates in
LS_COLORS='ex=00:su=00:sg=00:ca=00:'
In normal people speak, that means: don’t colorize files based on the their file capabilities, setuid
/setgid
bits nor executable flag.
Let ls
fly
And if you don’t do any of those checks, then the lstat()
calls disappear, and now, boom 🚀:
$ export LS_COLORS='ex=00:su=00:sg=00:ca=00:'
$ time strace -c ls --color=always $SCRATCH/dont | wc -l
10000
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
63.02 0.002865 36 80 getdents
8.10 0.000368 12 30 mmap
5.72 0.000260 14 18 mprotect
3.72 0.000169 15 11 open
2.79 0.000127 13 10 read
[...]
------ ----------- ----------- --------- --------- ----------------
100.00 0.004546 221 6 total
real 0m0.337s
user 0m0.032s
sys 0m0.029s
0.3s to list 10,000 files, track record. 🏁
This is on Sherlock
From 13s with the default settings, to 0.3s with a small LS_COLORS
tweak, that’s a 40x speedup right there, for the cheap price of not having setuid
/setgid
or executable files colorized differently.
Of course, this is now setup on Sherlock, for every user’s benefit.
But if you want all of your colors back, no worries, you can simply revert to the distribution defaults with:
$ unset LS_COLORS
But then, if you have directories with many many files, be sure to have some coffee handy while ls
is doing its thing.
Did you like this update?