├── .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 |
--------------------------------------------------------------------------------