diff --git a/README.md b/README.md index c1b1d2d..55dee77 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 @@ -169,7 +193,7 @@ atobey@brak ~/src/pcstat $ ./pcstat testfile ## Requirements -Go 1.17 or higher. +Go 1.18 or higher. From the mincore(2) man page: @@ -177,6 +201,9 @@ From the mincore(2) man page: * 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 @renice@hachyderm.io diff --git a/go.mod b/go.mod index d217fc9..d8b50f8 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 55a3ff2..ed3454f 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 6733d97..93e2958 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,7 @@ var ( pidFlag int terseFlag, nohdrFlag, jsonFlag, unicodeFlag bool plainFlag, ppsFlag, histoFlag, bnameFlag bool - sortFlag bool + sortFlag, cachestatFlag bool ) func init() { @@ -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() { @@ -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 diff --git a/pkg/cachestat.go b/pkg/cachestat.go new file mode 100644 index 0000000..e08cb5e --- /dev/null +++ b/pkg/cachestat.go @@ -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 +} + diff --git a/pkg/pcstatus.go b/pkg/pcstatus.go index 2fb0a8a..ad45490 100644 --- a/pkg/pcstatus.go +++ b/pkg/pcstatus.go @@ -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) @@ -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) } @@ -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