├── .git-precommit ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── asciidoc.conf ├── atoi.go ├── debian ├── changelog ├── compat ├── control ├── copyright ├── postinst └── rules ├── main.go ├── pprof.go ├── proc_linux.go ├── psm.txt ├── utils.go └── utils_test.go /.git-precommit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ret=0 4 | 5 | # this strange-ish syntax is because if we directly pipe git status 6 | # into while, it is executed in a subshell, and we can't modify the 7 | # value of ret. 8 | while read f; do 9 | if ! [ "go" == "${f##*.}" ]; then 10 | continue 11 | fi 12 | if [ '0' != "`gofmt -l $f | wc -c`" ]; then 13 | echo "needs go fmt: $f" 14 | ret=1 15 | fi 16 | done <<< "`git status -s | egrep '^A|^M' | cut -d ' ' -f 2-`" 17 | 18 | exit $ret 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | version.txt 3 | build/ 4 | psm 5 | psm.1 6 | psm.html 7 | psm.xml 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Bobby Powers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT := psm 2 | # turns the git-describe output of v0.3-$NCOMMITS-$SHA1 into 3 | # the more deb friendly 0.3.$NCOMMITS 4 | VERSION := $(shell git describe --long --match 'v*' | sed 's/v\([0-9]*\)\.\([0-9]*\)-\([0-9]*\).*/\1.\2.\3/') 5 | 6 | DEBUILD_OPTS = -S -sa 7 | 8 | prefix := /usr 9 | bindir := $(prefix)/bin 10 | sharedir := $(prefix)/share 11 | mandir := $(sharedir)/man 12 | man1dir := $(mandir)/man1 13 | 14 | all: build 15 | 16 | build: 17 | go test 18 | go build 19 | 20 | psm: *.go 21 | go build 22 | if [ $(shell basename $(PWD)) != psm ]; then mv $(shell basename $(PWD)) psm; fi 23 | 24 | clean: 25 | rm -rf psm build psm.1 psm.xml psm.html *~ 26 | 27 | psm.html: psm.txt 28 | asciidoc -b xhtml11 -d manpage -f asciidoc.conf -o $@ $< 29 | 30 | psm.xml: psm.txt 31 | asciidoc -b docbook -d manpage -f asciidoc.conf -o $@ $< 32 | 33 | psm.1: psm.xml 34 | xmlto man $< 35 | cat $@ | sed -e 's/\[FIXME: source\]/psm/' -e 's/\[FIXME: manual\]/User Commands/' >$@.tmp 36 | mv $@.tmp $@ 37 | 38 | install: psm psm.1 39 | install -D -m 4755 -o root psm $(DESTDIR)$(bindir)/psm 40 | install -D -m 0644 psm.1 $(DESTDIR)$(man1dir)/psm.1 41 | 42 | deb: builddeb 43 | 44 | builddeb: 45 | mkdir -p build 46 | git archive --prefix="$(PROJECT)-$(VERSION)/" HEAD | bzip2 -z9 >build/$(PROJECT)_$(VERSION).orig.tar.bz2 47 | git archive --prefix="$(PROJECT)-$(VERSION)/" HEAD | tar -xC build 48 | echo $(VERSION) >build/$(PROJECT)-$(VERSION)/version.txt 49 | (cd build/$(PROJECT)-$(VERSION) && dch --newversion $(VERSION)-1 -b "Last Commit: $(shell git log -1 --pretty=format:'(%ai) %H %cn <%ce>')") 50 | (cd build/$(PROJECT)-$(VERSION) && dch --release "new upstream") 51 | (cd build/$(PROJECT)-$(VERSION) && debuild $(DEBUILD_OPTS) -v$(VERSION)-1) 52 | @echo "Package is at build/$(PROJECT)_$(VERSION)-1_all.deb" 53 | 54 | version: 55 | 56 | .PHONY: all build install deb builddeb version clean 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | psm - simple, accurate memory reporting for Linux 2 | ================================================= 3 | 4 | `psm` makes it easy to see who is resident in memory, and who is 5 | significantly swapped out. 6 | 7 | `psm` is based off the ideas and implementation of 8 | [ps_mem.py](https://github.com/pixelb/scripts/commits/master/scripts/ps_mem.py). 9 | It requires root privileges to run. It is implemented in go, and 10 | since the executable is a binary it can be made setuid root so that 11 | unprivileged users can get a quick overview of the current memory 12 | situation. 13 | 14 | installation 15 | ------------ 16 | 17 | If you're familiar with go and have the go toolchain installed, 18 | installation is as easy as: 19 | 20 | go get github.com/bpowers/psm 21 | sudo `which psm` 22 | 23 | The ``sudo `which psm` `` can get a bit tiring. If you're on 24 | Ubuntu, there is a PPA which install psm as setuid root: 25 | 26 | sudo apt-get install python-software-properties # for apt-add-repository 27 | sudo add-apt-repository ppa:bobbypowers/psm 28 | sudo apt-get update 29 | sudo apt-get install psm 30 | 31 | example output 32 | -------------- 33 | 34 | bpowers@python-worker-01:~$ psm -filter=celery 35 | MB RAM SHARED SWAPPED PROCESS (COUNT) 36 | 60.6 1.1 134.2 [celeryd@notifications:MainProcess] (1) 37 | 62.6 1.1 [celeryd@health:MainProcess] (1) 38 | 113.7 1.2 [celeryd@uploads:MainProcess] (1) 39 | 155.1 1.1 [celeryd@triggers:MainProcess] (1) 40 | 176.7 1.2 [celeryd@updates:MainProcess] (1) 41 | 502.9 1.2 [celeryd@lookbacks:MainProcess] (1) 42 | 623.8 1.2 28.5 [celeryd@stats:MainProcess] (1) 43 | 671.3 1.2 [celeryd@default:MainProcess] (1) 44 | # 2366.7 164.7 TOTAL USED BY PROCESSES 45 | 46 | The `MB RAM` column is the sum of the Pss value of each mapping in 47 | `/proc/$PID/smaps` for each process. 48 | 49 | TODO 50 | ---- 51 | 52 | - port to the BSDs and OS X 53 | - FreeBSD has a Linux-compatable procfs impelmentation, which would 54 | be trivial to use (and, indeed, ps_mem.py uses it). 55 | - OS X looks... fun. MacFUSE provides a lot of the info we need, but 56 | I don't want to depend on having that installed and manually having 57 | their procfs mounted. There are Mach functions we could use, but 58 | I'm having trouble figuring out how to correctly pass data between 59 | go and C. Specifically: https://gist.github.com/4463209 - 'patches 60 | welcome'. 61 | - ps_mem.py records the md5sum of each process's smaps entry to make 62 | sure that we're not double-counting. Its probably worth doing. 63 | 64 | license 65 | ------- 66 | 67 | psm is offered under the MIT license, see LICENSE for details. 68 | -------------------------------------------------------------------------------- /asciidoc.conf: -------------------------------------------------------------------------------- 1 | # this file is copied and modified from git v1.8.1, and as such is 2 | # licensed under the GPL. 3 | 4 | [attributes] 5 | asterisk=* 6 | plus=+ 7 | caret=^ 8 | startsb=[ 9 | endsb=] 10 | backslash=\ 11 | tilde=~ 12 | apostrophe=' 13 | backtick=` 14 | litdd=-- 15 | 16 | ifdef::backend-docbook[] 17 | # "unbreak" docbook-xsl v1.68 for manpages. v1.69 works with or without this. 18 | # v1.72 breaks with this because it replaces dots not in roff requests. 19 | [listingblock] 20 | {title} 21 | 22 | ifdef::doctype-manpage[] 23 | .ft C 24 | endif::doctype-manpage[] 25 | | 26 | ifdef::doctype-manpage[] 27 | .ft 28 | endif::doctype-manpage[] 29 | 30 | {title#} 31 | endif::backend-docbook[] 32 | 33 | ifdef::doctype-manpage[] 34 | ifdef::backend-docbook[] 35 | [header] 36 | template::[header-declarations] 37 | 38 | 39 | {mantitle} 40 | {manvolnum} 41 | 42 | 43 | {manname} 44 | {manpurpose} 45 | 46 | endif::backend-docbook[] 47 | endif::doctype-manpage[] 48 | -------------------------------------------------------------------------------- /atoi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import "errors" 8 | import "strconv" 9 | 10 | // ErrRange indicates that a value is out of range for the target type. 11 | var ErrRange = errors.New("value out of range") 12 | 13 | // ErrSyntax indicates that a value does not have the right syntax for the target type. 14 | var ErrSyntax = errors.New("invalid syntax") 15 | 16 | // A NumError records a failed conversion. 17 | type NumError struct { 18 | Func string // the failing function (ParseBool, ParseInt, ParseUint, ParseFloat) 19 | Num string // the input 20 | Err error // the reason the conversion failed (ErrRange, ErrSyntax) 21 | } 22 | 23 | func (e *NumError) Error() string { 24 | return "strconv." + e.Func + ": " + `parsing "` + e.Num + `": ` + e.Err.Error() 25 | } 26 | 27 | func syntaxError(fn, str string) *NumError { 28 | return &NumError{fn, str, ErrSyntax} 29 | } 30 | 31 | func rangeError(fn, str string) *NumError { 32 | return &NumError{fn, str, ErrRange} 33 | } 34 | 35 | const intSize = 32 << uint(^uint(0)>>63) 36 | 37 | const IntSize = intSize // number of bits in int, uint (32 or 64) 38 | 39 | // Return the first number n such that n*base >= 1<<64. 40 | func cutoff64(base int) uint64 { 41 | if base < 2 { 42 | return 0 43 | } 44 | return (1<<64-1)/uint64(base) + 1 45 | } 46 | 47 | // ParseUint is like ParseInt but for unsigned numbers. 48 | func ParseUint(s []byte, base int, bitSize int) (n uint64, err error) { 49 | var cutoff, maxVal uint64 50 | 51 | if bitSize == 0 { 52 | bitSize = int(IntSize) 53 | } 54 | 55 | s0 := s 56 | switch { 57 | case len(s) < 1: 58 | err = ErrSyntax 59 | goto Error 60 | 61 | case 2 <= base && base <= 36: 62 | // valid base; nothing to do 63 | 64 | case base == 0: 65 | // Look for octal, hex prefix. 66 | switch { 67 | case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'): 68 | base = 16 69 | s = s[2:] 70 | if len(s) < 1 { 71 | err = ErrSyntax 72 | goto Error 73 | } 74 | case s[0] == '0': 75 | base = 8 76 | default: 77 | base = 10 78 | } 79 | 80 | default: 81 | err = errors.New("invalid base " + strconv.Itoa(base)) 82 | goto Error 83 | } 84 | 85 | n = 0 86 | cutoff = cutoff64(base) 87 | maxVal = 1<= base { 105 | n = 0 106 | err = ErrSyntax 107 | goto Error 108 | } 109 | 110 | if n >= cutoff { 111 | // n*base overflows 112 | n = 1<<64 - 1 113 | err = ErrRange 114 | goto Error 115 | } 116 | n *= uint64(base) 117 | 118 | n1 := n + uint64(v) 119 | if n1 < n || n1 > maxVal { 120 | // n+v overflows 121 | n = 1<<64 - 1 122 | err = ErrRange 123 | goto Error 124 | } 125 | n = n1 126 | } 127 | 128 | return n, nil 129 | 130 | Error: 131 | return n, &NumError{"ParseUint", string(s0), err} 132 | } 133 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | psm (0.3.0-1) UNRELEASED; urgency=low 2 | 3 | * Initial release. 4 | 5 | -- Bobby Powers Wed, 02 Jan 2013 22:23:27 -0500 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: psm 2 | Maintainer: Bobby Powers 3 | Section: misc 4 | Priority: optional 5 | Homepage: https://github.com/bpowers/psm 6 | Vcs-Git: git://github.com/bpowers/psm.git 7 | Vcs-Browser: https://github.com/bpowers/psm 8 | Build-Depends: debhelper (>= 8), golang-tip|golang-go, asciidoc, xmlto 9 | Standards-Version: 3.9.3 10 | 11 | Package: psm 12 | Architecture: any 13 | Depends: ${misc:Depends}, 14 | Description: Simple, accurate RAM and swap usage 15 | psm is a command line tool that provides RAM and swap usage 16 | information. 17 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Files: * 2 | Copyright: (C) 2012 by Bobby Powers 3 | 4 | License: MIT 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postinst script for diamond 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `configure' 10 | # * `abort-upgrade' 11 | # * `abort-remove' `in-favour' 12 | # 13 | # * `abort-remove' 14 | # * `abort-deconfigure' `in-favour' 15 | # `removing' 16 | # 17 | # for details, see http://www.debian.org/doc/debian-policy/ or 18 | # the debian-policy package 19 | 20 | 21 | case "$1" in 22 | configure) 23 | chmod 4755 /usr/bin/psm 24 | ;; 25 | 26 | abort-upgrade|abort-remove|abort-deconfigure) 27 | ;; 28 | 29 | *) 30 | echo "postinst called with unknown argument \`$1'" >&2 31 | exit 1 32 | ;; 33 | esac 34 | 35 | # dh_installdeb will replace this with shell code automatically 36 | # generated by other debhelper scripts. 37 | 38 | #DEBHELPER# 39 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "regexp" 9 | "runtime" 10 | "sort" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | const ( 16 | CmdDisplayMax = 32 17 | 18 | usage = `Usage: %s [OPTION...] 19 | Simple, accurate RAM and swap reporting. 20 | 21 | Options: 22 | ` 23 | ) 24 | 25 | var ( 26 | filter string 27 | memProfile string 28 | cpuProfile string 29 | showHeap bool 30 | filterRE *regexp.Regexp 31 | ) 32 | 33 | // store info about a command (group of processes), similar to how 34 | // ps_mem works. 35 | type CmdMemInfo struct { 36 | PIDs []int 37 | Name string 38 | Pss float64 39 | Shared float64 40 | Heap float64 41 | Swapped float64 42 | } 43 | 44 | type MapInfo struct { 45 | Inode uint64 46 | Name string 47 | } 48 | 49 | // mapLine is a line from /proc/$PID/maps, or one of the same header 50 | // lines from smaps. 51 | func NewMapInfo(mapLine []byte) MapInfo { 52 | var mi MapInfo 53 | var err error 54 | pieces := splitSpaces(mapLine) 55 | if len(pieces) == 6 { 56 | mi.Name = string(pieces[5]) 57 | } 58 | if len(pieces) < 5 { 59 | panic(fmt.Sprintf("NewMapInfo(%d): `%s`", 60 | len(pieces), string(mapLine))) 61 | } 62 | mi.Inode, err = ParseUint(pieces[4], 10, 64) 63 | if err != nil { 64 | panic(fmt.Sprintf("NewMapInfo: Atoi(%s): %s (%s)", 65 | string(pieces[4]), err, string(mapLine))) 66 | } 67 | return mi 68 | } 69 | 70 | func (mi MapInfo) IsAnon() bool { 71 | return mi.Inode == 0 72 | } 73 | 74 | // worker is executed in a new goroutine. Its sole purpose is to 75 | // process requests for information about particular PIDs. 76 | func worker(pidRequest chan int, wg *sync.WaitGroup, result chan *CmdMemInfo) { 77 | for pid := range pidRequest { 78 | var err error 79 | cmi := new(CmdMemInfo) 80 | 81 | cmi.PIDs = []int{pid} 82 | cmi.Name, err = procName(pid) 83 | if err != nil { 84 | log.Printf("procName(%d): %s", pid, err) 85 | wg.Done() 86 | continue 87 | } else if cmi.Name == "" { 88 | // XXX: This happens with kernel 89 | // threads. maybe warn? idk. 90 | wg.Done() 91 | continue 92 | } else if filterRE != nil && !filterRE.MatchString(cmi.Name) { 93 | wg.Done() 94 | continue 95 | } 96 | 97 | cmi.Pss, cmi.Shared, cmi.Heap, cmi.Swapped, err = procMem(pid) 98 | if err != nil { 99 | log.Printf("procMem(%d): %s", pid, err) 100 | wg.Done() 101 | continue 102 | } 103 | 104 | result <- cmi 105 | wg.Done() 106 | } 107 | } 108 | 109 | type byPss []*CmdMemInfo 110 | 111 | func (c byPss) Len() int { return len(c) } 112 | func (c byPss) Less(i, j int) bool { return c[i].Pss < c[j].Pss } 113 | func (c byPss) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 114 | 115 | func init() { 116 | flag.Usage = func() { 117 | fmt.Fprintf(os.Stderr, usage, os.Args[0]) 118 | flag.PrintDefaults() 119 | } 120 | 121 | flag.StringVar(&filter, "filter", "", 122 | "regex to test process names against") 123 | flag.StringVar(&memProfile, "memprofile", "", 124 | "write memory profile to this file") 125 | flag.StringVar(&cpuProfile, "cpuprofile", "", 126 | "write cpu profile to this file") 127 | flag.BoolVar(&showHeap, "heap", false, "show heap column") 128 | 129 | flag.Parse() 130 | 131 | if filter != "" { 132 | filterRE = regexp.MustCompile(filter) 133 | } 134 | } 135 | 136 | func main() { 137 | prof, err := NewProf(memProfile, cpuProfile) 138 | if err != nil { 139 | log.Fatal(err) 140 | } 141 | // if -memprof or -cpuprof haven't been set on the command 142 | // line, these are nops 143 | prof.Start() 144 | defer prof.Stop() 145 | 146 | // need to be root to read map info for other user's 147 | // processes. 148 | if os.Geteuid() != 0 { 149 | fmt.Printf("%s requires root privileges. (try 'sudo `which %s`)\n", 150 | os.Args[0], os.Args[0]) 151 | return 152 | } 153 | 154 | pids, err := pidList() 155 | if err != nil { 156 | log.Printf("pidList: %s", err) 157 | return 158 | } 159 | 160 | var wg sync.WaitGroup 161 | work := make(chan int, len(pids)) 162 | result := make(chan *CmdMemInfo, len(pids)) 163 | 164 | // give us as much parallelism as possible 165 | nCPU := runtime.NumCPU() 166 | runtime.GOMAXPROCS(nCPU) 167 | for i := 0; i < nCPU; i++ { 168 | go worker(work, &wg, result) 169 | } 170 | 171 | wg.Add(len(pids)) 172 | for _, pid := range pids { 173 | work <- pid 174 | } 175 | wg.Wait() 176 | 177 | // aggregate similar processes by command name. 178 | cmdMap := map[string]*CmdMemInfo{} 179 | loop: 180 | for { 181 | // this only works correctly because we a channel 182 | // where the buffer size >= the number of potential 183 | // results. 184 | select { 185 | case c := <-result: 186 | n := c.Name 187 | if _, ok := cmdMap[n]; !ok { 188 | cmdMap[n] = c 189 | continue 190 | } 191 | cmdMap[n].PIDs = append(cmdMap[n].PIDs, c.PIDs...) 192 | cmdMap[n].Pss += c.Pss 193 | cmdMap[n].Shared += c.Shared 194 | cmdMap[n].Swapped += c.Swapped 195 | default: 196 | break loop 197 | } 198 | } 199 | 200 | // extract map values to a slice so we can sort them 201 | cmds := make([]*CmdMemInfo, 0, len(cmdMap)) 202 | for _, c := range cmdMap { 203 | cmds = append(cmds, c) 204 | } 205 | sort.Sort(byPss(cmds)) 206 | 207 | // keep track of total RAM and swap usage 208 | var totPss, totSwap float64 209 | 210 | headFmt := "%10s%10s%10s\t%s\n" 211 | cols := []interface{}{"MB RAM", "SHARED", "SWAPPED", "PROCESS (COUNT)"} 212 | totFmt := "#%9.1f%20.1f\tTOTAL USED BY PROCESSES\n" 213 | 214 | if showHeap { 215 | headFmt = "%10s" + headFmt 216 | cols = []interface{}{"MB RAM", "SHARED", "HEAP", "SWAPPED", "PROCESS (COUNT)"} 217 | totFmt = "#%9.1f%30.1f\tTOTAL USED BY PROCESSES\n" 218 | } 219 | 220 | fmt.Printf(headFmt, cols...) 221 | for _, c := range cmds { 222 | n := c.Name 223 | if len(n) > CmdDisplayMax { 224 | if n[0] == '[' { 225 | n = n[:strings.IndexRune(n, ']')+1] 226 | } else { 227 | n = n[:CmdDisplayMax] 228 | } 229 | } 230 | s := "" 231 | if c.Swapped > 0 { 232 | swap := c.Swapped / 1024. 233 | totSwap += swap 234 | s = fmt.Sprintf("%10.1f", swap) 235 | } 236 | pss := float64(c.Pss) / 1024. 237 | if showHeap { 238 | fmt.Printf("%10.1f%10.1f%10.1f%10s\t%s (%d)\n", pss, c.Shared/1024., c.Heap/1024., s, n, len(c.PIDs)) 239 | } else { 240 | fmt.Printf("%10.1f%10.1f%10s\t%s (%d)\n", pss, c.Shared/1024., s, n, len(c.PIDs)) 241 | } 242 | totPss += pss 243 | } 244 | fmt.Printf(totFmt, totPss, totSwap) 245 | } 246 | -------------------------------------------------------------------------------- /pprof.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | "runtime/pprof" 7 | ) 8 | 9 | type ProfInstance struct { 10 | memprof, cpuprof *os.File 11 | } 12 | 13 | func NewProf(memprof, cpuprof string) (p *ProfInstance, err error) { 14 | p = &ProfInstance{} 15 | if memprof != "" { 16 | if p.memprof, err = os.Create(memprof); err != nil { 17 | p = nil 18 | return 19 | } 20 | } 21 | if cpuprof != "" { 22 | if p.cpuprof, err = os.Create(cpuprof); err != nil { 23 | // close all files on error 24 | if p.memprof != nil { 25 | p.memprof.Close() 26 | } 27 | p = nil 28 | return 29 | } 30 | } 31 | return 32 | } 33 | 34 | // startProfiling enables memory and/or CPU profiling if the 35 | // appropriate command line flags have been set. 36 | func (p *ProfInstance) Start() { 37 | 38 | // if we've passed in filenames to dump profiling data too, 39 | // start collecting profiling data. 40 | if p.memprof != nil { 41 | runtime.MemProfileRate = 1 42 | } 43 | if p.cpuprof != nil { 44 | pprof.StartCPUProfile(p.cpuprof) 45 | } 46 | } 47 | 48 | func (p *ProfInstance) Stop() { 49 | if p.memprof != nil { 50 | runtime.GC() 51 | pprof.WriteHeapProfile(p.memprof) 52 | p.memprof.Close() 53 | } 54 | if p.cpuprof != nil { 55 | pprof.StopCPUProfile() 56 | p.cpuprof.Close() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /proc_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | const ( 16 | // max length of /proc/$PID/comm 17 | CommMax = 16 18 | // from ps_mem - average error due to truncation in the kernel 19 | // pss calculations 20 | PssAdjust = .5 21 | pageSize = 4096 22 | mapDetailLen = len("Size: 4 kB") 23 | ) 24 | 25 | var ( 26 | tyVmFlags = []byte("VmFlags:") 27 | tyPss = []byte("Pss:") 28 | tySwap = []byte("Swap:") 29 | tyPrivateClean = []byte("Private_Clean:") 30 | tyPrivateDirty = []byte("Private_Dirty:") 31 | kb = []byte(" kB") 32 | ) 33 | 34 | // pidList returns a list of the process-IDs of every currently 35 | // running process on the local system. 36 | func pidList() ([]int, error) { 37 | procLs, err := ioutil.ReadDir("/proc") 38 | if err != nil { 39 | return nil, fmt.Errorf("ReadDir(/proc): %s", err) 40 | } 41 | 42 | pids := make([]int, 0, len(procLs)) 43 | for _, pInfo := range procLs { 44 | if !isDigit(pInfo.Name()[0]) || !pInfo.IsDir() { 45 | continue 46 | } 47 | pidInt, err := strconv.Atoi(pInfo.Name()) 48 | if err != nil { 49 | return nil, fmt.Errorf("Atoi(%s): %s", pInfo.Name(), err) 50 | } 51 | pids = append(pids, pidInt) 52 | } 53 | return pids, nil 54 | } 55 | 56 | // procName gets the process name for a worker. It first checks the 57 | // value of /proc/$PID/cmdline. If setproctitle(3) has been called, 58 | // it will use this. Otherwise it uses the value of 59 | // path.Base(/proc/$PID/exe), which has info on whether the executable 60 | // has changed since the process was exec'ed. 61 | func procName(pid int) (string, error) { 62 | p, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid)) 63 | // this would return an error if the PID doesn't 64 | // exist, or if the PID refers to a kernel thread. 65 | if err != nil { 66 | return "", nil 67 | } 68 | // cmdline is the null separated list of command line 69 | // arguments for the process, unless setproctitle(3) 70 | // has been called, in which case it is the 71 | argsB, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) 72 | if err != nil { 73 | return "", fmt.Errorf("ReadFile(%s): %s", fmt.Sprintf("/proc/%d/cmdline", pid), err) 74 | } 75 | args := strings.Split(string(argsB), "\000") 76 | n := args[0] 77 | 78 | nTrunc := n 79 | if len(n) > CommMax { 80 | nTrunc = n[:CommMax] 81 | } 82 | if strings.HasPrefix(p, nTrunc) { 83 | n = path.Base(p) 84 | } 85 | return n, nil 86 | } 87 | 88 | // procMem returns the amount of Pss, shared, and swapped out memory 89 | // used. The swapped out amount refers to anonymous pages only. 90 | func procMem(pid int) (pss, shared, heap, swap float64, err error) { 91 | fPath := fmt.Sprintf("/proc/%d/smaps", pid) 92 | f, err := os.Open(fPath) 93 | if err != nil { 94 | err = fmt.Errorf("os.Open(%s): %s", fPath, err) 95 | return 96 | } 97 | var priv float64 98 | var curr MapInfo 99 | r := bufio.NewReaderSize(f, pageSize) 100 | for { 101 | var l []byte 102 | var isPrefix bool 103 | l, isPrefix, err = r.ReadLine() 104 | // this should never happen, so take the easy way out. 105 | if isPrefix { 106 | err = fmt.Errorf("ReadLine(%s): isPrefix", fPath) 107 | } 108 | if err != nil { 109 | // if we've got EOF, then we're simply done 110 | // processing smaps. 111 | if err == io.EOF { 112 | err = nil 113 | break 114 | } 115 | // otherwise error out 116 | err = fmt.Errorf("ReadLine(%s): %s", fPath, err) 117 | return 118 | } 119 | 120 | if len(l) != mapDetailLen && !bytes.HasSuffix(l, kb) { 121 | if !bytes.HasPrefix(l, tyVmFlags) { 122 | curr = NewMapInfo(l) 123 | } 124 | continue 125 | } 126 | pieces := splitSpaces(l) 127 | ty := pieces[0] 128 | var v uint64 129 | if bytes.Equal(ty, tyPss) { 130 | v, err = ParseUint(pieces[1], 10, 64) 131 | if err != nil { 132 | err = fmt.Errorf("Atoi(%s): %s", string(pieces[1]), err) 133 | return 134 | } 135 | m := float64(v) 136 | pss += m + PssAdjust 137 | if curr.Name == "[heap]" { 138 | // we don't nead PssAdjust because 139 | // heap is private and anonymous. 140 | heap = m 141 | } 142 | } else if bytes.Equal(ty, tyPrivateClean) || bytes.Equal(ty, tyPrivateDirty) { 143 | v, err = ParseUint(pieces[1], 10, 64) 144 | if err != nil { 145 | err = fmt.Errorf("Atoi(%s): %s", string(pieces[1]), err) 146 | return 147 | } 148 | priv += float64(v) 149 | } else if bytes.Equal(ty, tySwap) { 150 | v, err = ParseUint(pieces[1], 10, 64) 151 | if err != nil { 152 | err = fmt.Errorf("Atoi(%s): %s", string(pieces[1]), err) 153 | return 154 | } 155 | swap += float64(v) 156 | } 157 | } 158 | shared = pss - priv 159 | return 160 | } 161 | -------------------------------------------------------------------------------- /psm.txt: -------------------------------------------------------------------------------- 1 | psm(1) 2 | ====== 3 | 4 | NAME 5 | ---- 6 | psm - Core memory and swap reporting 7 | 8 | SYNOPSIS 9 | -------- 10 | 'psm' [-cpuprofile=] [-memprofile=] [-heap] [-filter=] 11 | 12 | DESCRIPTION 13 | ----------- 14 | Makes it easy to see who is resident in memory, and who is 15 | significantly swapped out. 16 | 17 | psm is based off the ideas and implementation of ps_mem.py. It 18 | requires root privileges to run - and as such is typically installed 19 | as setuid root. 20 | 21 | OPTIONS 22 | ------- 23 | 24 | -heap:: 25 | Heap adds an additional column to the output with the amount 26 | of heap resident in memory (MB Rss). 27 | 28 | -filter=:: 29 | Filter provides a way to report on a subset of running 30 | processes. The regular expression format is the same as in 31 | Python or Perl, but for specifics see 32 | 33 | 34 | -cpuprofile=:: 35 | Records a CPU profile to the given file, suitable for use with 36 | 'go tool pprof'. 37 | 38 | -memprofile=:: 39 | Records a profile of memory allocations to the given file, 40 | suitable for use with 'go tool pprof'. 41 | 42 | AUTHOR 43 | ------ 44 | psm was originally written by Bobby Powers. 45 | 46 | 47 | RESOURCES 48 | --------- 49 | Main web site: 50 | 51 | 52 | COPYING 53 | ------- 54 | Copyright \(C) 2012-2013 Bobby Powers. Free use of this software is 55 | granted under the terms of the MIT license. 56 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // isDigit returns true if the rune d represents an ascii digit 4 | // between 0 and 9, inclusive. 5 | func isDigit(d uint8) bool { 6 | return d >= '0' && d <= '9' 7 | } 8 | 9 | // splitSpaces returns a slice of byte slices which are the space 10 | // delimited words from the original byte slice. Unlike 11 | // strings.Split($X, " "), runs of multiple spaces in a row are 12 | // discarded. NOTE WELL: this only checks for spaces (' '), other 13 | // unicode whitespace isn't supported. 14 | func splitSpaces(b []byte) [][]byte { 15 | // most lines in smaps have the form "Swap: 4 kB", so 16 | // preallocate the slice's array appropriately. 17 | res := make([][]byte, 0, 3) 18 | start := 0 19 | for i := 0; i < len(b)-1; i++ { 20 | if b[i] == ' ' { 21 | start = i + 1 22 | } else if b[i+1] == ' ' { 23 | res = append(res, b[start:i+1]) 24 | start = i + 1 25 | } 26 | } 27 | if start != len(b) && b[start] != ' ' { 28 | res = append(res, b[start:]) 29 | } 30 | return res 31 | } 32 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | const benchString = `7fff70e93000-7fff70eb5000 rw-p 00000000 00:00 0 [stack] 10 | Size: 140 kB 11 | Rss: 12 kB 12 | Pss: 12 kB 13 | Shared_Clean: 0 kB 14 | Shared_Dirty: 0 kB 15 | Private_Clean: 0 kB 16 | Private_Dirty: 12 kB 17 | Referenced: 12 kB 18 | Anonymous: 12 kB 19 | AnonHugePages: 0 kB 20 | Swap: 0 kB 21 | KernelPageSize: 4 kB 22 | MMUPageSize: 4 kB 23 | Locked: 0 kB 24 | VmFlags: rd wr mr mw me gd ac 25 | 7fff70fff000-7fff71000000 r-xp 00000000 00:00 0 [vdso] 26 | Size: 4 kB 27 | Rss: 4 kB 28 | Pss: 0 kB 29 | Shared_Clean: 4 kB 30 | Shared_Dirty: 0 kB 31 | Private_Clean: 0 kB 32 | Private_Dirty: 0 kB 33 | Referenced: 4 kB 34 | Anonymous: 0 kB 35 | AnonHugePages: 0 kB 36 | Swap: 0 kB 37 | KernelPageSize: 4 kB 38 | MMUPageSize: 4 kB 39 | Locked: 0 kB 40 | VmFlags: rd ex mr mw me de 41 | ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] 42 | Size: 4 kB 43 | Rss: 0 kB 44 | Pss: 0 kB 45 | Shared_Clean: 0 kB 46 | Shared_Dirty: 0 kB 47 | Private_Clean: 0 kB 48 | Private_Dirty: 0 kB 49 | Referenced: 0 kB 50 | Anonymous: 0 kB 51 | AnonHugePages: 0 kB 52 | Swap: 0 kB 53 | KernelPageSize: 4 kB 54 | MMUPageSize: 4 kB 55 | Locked: 0 kB 56 | VmFlags: rd ex 57 | ` 58 | 59 | var ( 60 | benchStrLines = strings.Split(benchString, "\n") 61 | benchLines = sliceByteArr(benchStrLines) 62 | usedResult [][]byte 63 | ) 64 | 65 | func sliceByteArr(b []string) [][]byte { 66 | res := make([][]byte, len(b)) 67 | for i, bs := range b { 68 | res[i] = []byte(bs) 69 | } 70 | return res 71 | } 72 | 73 | func stringArr(b [][]byte) []string { 74 | res := make([]string, len(b)) 75 | for i, bs := range b { 76 | res[i] = string(bs) 77 | } 78 | return res 79 | } 80 | 81 | var splitSpacesData = [...]struct { 82 | orig string 83 | split []string 84 | }{ 85 | {"", []string{}}, 86 | {" ", []string{}}, 87 | {" ", []string{}}, 88 | {" a ", []string{"a"}}, 89 | {"abc", []string{"abc"}}, 90 | {"abc ", []string{"abc"}}, 91 | {" abc ", []string{"abc"}}, 92 | {"abc 123", []string{"abc", "123"}}, 93 | {"abc 123 ", []string{"abc", "123"}}, 94 | {"abc 123", []string{"abc", "123"}}, 95 | {" abc 123", []string{"abc", "123"}}, 96 | {" abc 123 def", []string{"abc", "123", "def"}}, 97 | } 98 | 99 | func TestSplitSpaces(t *testing.T) { 100 | for _, pair := range splitSpacesData { 101 | origB := []byte(pair.orig) 102 | ss := stringArr(splitSpaces(origB)) 103 | if !reflect.DeepEqual(ss, pair.split) { 104 | t.Fatalf("expected equal:\n orig: %#v\n ref: %#v\n", 105 | pair.split, ss) 106 | } 107 | } 108 | } 109 | 110 | func BenchmarkSplitSpaces(b *testing.B) { 111 | for i := 0; i < b.N; i++ { 112 | for _, l := range benchLines { 113 | usedResult = splitSpaces(l) 114 | } 115 | } 116 | } 117 | --------------------------------------------------------------------------------