Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Command-line arguments are described below. Every argument following the program
flags is considered a file for inspection.

```
pcstat <-json <-pps>|-terse|-default> <-nohdr> <-bname> file file file
pcstat <-json <-pps>|-terse|-default> <-nohdr> <-bname> <-cachestat> file file file
-json output will be JSON
-pps include the per-page information in the output (can be huge!)
-terse print terse machine-parseable output
Expand All @@ -40,7 +40,7 @@ pcstat <-json <-pps>|-terse|-default> <-nohdr> <-bname> file file file
-bname use basename(file) in the output (use for long paths)
-plain return data with no box characters
-unicode return data with unicode box characters

-cachestat use new cachestat syscall instead of mincore (kernel 6.5+, x86_64 only)
```

## Examples
Expand Down Expand Up @@ -103,6 +103,30 @@ atobey@brak ~ $ pcstat -json testfile3 |json_pp
]
```

The extra statistics from the `-cachestat` option (kernel 6.5+) are only
included in the JSON output for now.

```
atobey@brak ~ $ pcstat -json -cachestat testfile3 |json_pp
[
{
"filename": "testfile3",
"size": 102401024,
"timestamp": "2024-05-22T13:57:19.971348936Z",
"mtime": "2024-05-22T12:20:47.940163295Z",
"pages": 25001,
"cached": 60,
"uncached": 24941,
"percent": 0.23999040038398464,
"status": []
"dirty": 22,
"writeback": 38,
"evicted": 24941,
"recently_evicted": 24941
}
]
```

### Histogram output

Your terminal and font need to support the Block Elements section of Unicode
Expand Down Expand Up @@ -169,14 +193,17 @@ atobey@brak ~/src/pcstat $ ./pcstat testfile

## Requirements

Go 1.17 or higher.
Go 1.18 or higher.

From the mincore(2) man page:

* Available since Linux 2.3.99pre1 and glibc 2.2.
* mincore() is not specified in POSIX.1-2001, and it is not available on all UNIX implementations.
* Before kernel 2.6.21, mincore() did not return correct information some mappings.

The cachestat syscall was only added to the Linux kernel in 6.5 and is currently only
supported on x86_64.

## Author

A. Tobey <tobert@gmail.com> @renice@hachyderm.io
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module github.com/tobert/pcstat

go 1.17
go 1.18

require golang.org/x/sys v0.10.0
require golang.org/x/sys v0.16.0
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
5 changes: 3 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var (
pidFlag int
terseFlag, nohdrFlag, jsonFlag, unicodeFlag bool
plainFlag, ppsFlag, histoFlag, bnameFlag bool
sortFlag bool
sortFlag, cachestatFlag bool
)

func init() {
Expand All @@ -54,6 +54,7 @@ func init() {
flag.BoolVar(&histoFlag, "histo", false, "print a simple histogram instead of raw data")
flag.BoolVar(&bnameFlag, "bname", false, "convert paths to basename to narrow the output")
flag.BoolVar(&sortFlag, "sort", false, "sort output by cached pages desc")
flag.BoolVar(&cachestatFlag, "cachestat", false, "use cachestat syscall (kernel 6.5+ only)")
}

func main() {
Expand All @@ -76,7 +77,7 @@ func main() {

stats := make(PcStatusList, 0, len(files))
for _, fname := range files {
status, err := pcstat.GetPcStatus(fname)
status, err := pcstat.GetPcStatus(fname, cachestatFlag)
if err != nil {
log.Printf("skipping %q: %v", fname, err)
continue
Expand Down
35 changes: 35 additions & 0 deletions pkg/cachestat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package pcstat

import (
"fmt"
"os"

"golang.org/x/sys/unix"
)

// FileCachestat uses the cachestat syscall to get the
// number of cached pages for the file without using '
// an intermediate per-page bool map. It then returns
// it alongside the numberof total pages
func FileCachestat(f *os.File, size int64) (*unix.Cachestat_t, int, error) {
//skip could not mmap error when the file size is 0
if int(size) == 0 {
return nil, 0, nil
}

pcount := int((size + int64(os.Getpagesize()) - 1) / int64(os.Getpagesize()))

// Use cachestat syscall
crange := &unix.CachestatRange{
Off: 0,
Len: uint64(size),
}
cstat := &unix.Cachestat_t{}
err := unix.Cachestat(uint(f.Fd()), crange, cstat, 0)
if err != nil {
return nil, 0, fmt.Errorf("cachestat syscall failed: %v", err)
}

return cstat, pcount, nil
}

50 changes: 39 additions & 11 deletions pkg/pcstatus.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ type PcStatus struct {
Uncached int `json:"uncached"` // number of pages that are not cached
Percent float64 `json:"percent"` // percentage of pages cached
PPStat []bool `json:"status"` // per-page status, true if cached, false otherwise
// additional fields for cachestat implementation
Dirty *uint64 `json:"dirty,omitempty"` // number of dirty pages
Writeback *uint64 `json:"writeback,omitempty"` // number of pages under writeback
Evicted *uint64 `json:"evicted,omitempty"` // number of evicted pages
RecentlyEvicted *uint64 `json:"recently_evicted,omitempty"` // number of recently evicted pages
}

func GetPcStatus(fname string) (PcStatus, error) {
func GetPcStatus(fname string, useCachestat bool) (PcStatus, error) {
pcs := PcStatus{Name: fname}

f, err := os.Open(fname)
Expand All @@ -53,7 +58,6 @@ func GetPcStatus(fname string) (PcStatus, error) {
// what will be in there when the file is truncated between here and the
// mincore() call.
fi, err := f.Stat()

if err != nil {
return pcs, fmt.Errorf("could not stat file: %v", err)
}
Expand All @@ -65,19 +69,43 @@ func GetPcStatus(fname string) (PcStatus, error) {
pcs.Timestamp = time.Now()
pcs.Mtime = fi.ModTime()

pcs.PPStat, err = FileMincore(f, fi.Size())
if useCachestat {
// Use cachestat implementation
cstat, psize, err := FileCachestat(f, fi.Size())
if err != nil {
return pcs, err
}

if err != nil {
return pcs, err
}
// will be shown in json output only for now
dirty := cstat.Dirty
writeback := cstat.Writeback
evicted := cstat.Evicted
recentlyEvicted := cstat.Recently_evicted

pcs.Dirty = &dirty
pcs.Writeback = &writeback
pcs.Evicted = &evicted
pcs.RecentlyEvicted = &recentlyEvicted

// count the number of cached pages
for _, b := range pcs.PPStat {
if b {
pcs.Cached++
// default for backward compatibility with mincore impl
pcs.Cached = int(cstat.Cache)
pcs.Pages = psize
} else {

pcs.PPStat, err = FileMincore(f, fi.Size())
if err != nil {
return pcs, err
}

// count the number of cached pages
for _, b := range pcs.PPStat {
if b {
pcs.Cached++
}
}
pcs.Pages = len(pcs.PPStat)
}
pcs.Pages = len(pcs.PPStat)

pcs.Uncached = pcs.Pages - pcs.Cached

// convert to float for the occasional sparsely-cached file
Expand Down