From 18ec40ee81c6e35024f243a64136eb186279486e Mon Sep 17 00:00:00 2001 From: boomskats Date: Wed, 6 Nov 2024 19:42:43 +0000 Subject: [PATCH 1/6] bump versions for cachestat support --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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= From 8e60d295d5bf3584b7466ae6175278b95f71f7a3 Mon Sep 17 00:00:00 2001 From: boomskats Date: Wed, 6 Nov 2024 20:01:13 +0000 Subject: [PATCH 2/6] add cachestat as alternative to mincore --- main.go | 5 +++-- pkg/cachestat.go | 37 +++++++++++++++++++++++++++++++++++++ pkg/pcstatus.go | 33 ++++++++++++++++++++++----------- 3 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 pkg/cachestat.go 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..6607d01 --- /dev/null +++ b/pkg/cachestat.go @@ -0,0 +1,37 @@ +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) (int, int, error) { + //skip could not mmap error when the file size is 0 + if int(size) == 0 { + return 0, 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 0, 0, fmt.Errorf("cachestat syscall failed: %v", err) + } + + cached := cstat.Cache + cstat.Dirty + cstat.Writeback + + return int(cached), pcount, nil +} + diff --git a/pkg/pcstatus.go b/pkg/pcstatus.go index 2fb0a8a..8d4d1f6 100644 --- a/pkg/pcstatus.go +++ b/pkg/pcstatus.go @@ -38,7 +38,7 @@ type PcStatus struct { PPStat []bool `json:"status"` // per-page status, true if cached, false otherwise } -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 +53,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 +64,31 @@ 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 + var cached, psize int + cached, psize, err = FileCachestat(f, fi.Size()) + if err != nil { + return pcs, err + } + pcs.Cached = cached + pcs.Pages = psize + } else { - if err != nil { - return pcs, err - } + 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++ + // 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 From 3279bb734166710f07708d35e764b58b774a4724 Mon Sep 17 00:00:00 2001 From: boomskats Date: Wed, 6 Nov 2024 21:13:32 +0000 Subject: [PATCH 3/6] add extra fields to json output when used with -cachestat option --- pkg/cachestat.go | 10 ++++------ pkg/pcstatus.go | 20 +++++++++++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pkg/cachestat.go b/pkg/cachestat.go index 6607d01..e08cb5e 100644 --- a/pkg/cachestat.go +++ b/pkg/cachestat.go @@ -11,10 +11,10 @@ import ( // 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) (int, int, error) { +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 0, 0,nil + return nil, 0, nil } pcount := int((size + int64(os.Getpagesize()) - 1) / int64(os.Getpagesize())) @@ -27,11 +27,9 @@ func FileCachestat(f *os.File, size int64) (int, int, error) { cstat := &unix.Cachestat_t{} err := unix.Cachestat(uint(f.Fd()), crange, cstat, 0) if err != nil { - return 0, 0, fmt.Errorf("cachestat syscall failed: %v", err) + return nil, 0, fmt.Errorf("cachestat syscall failed: %v", err) } - cached := cstat.Cache + cstat.Dirty + cstat.Writeback - - return int(cached), pcount, nil + return cstat, pcount, nil } diff --git a/pkg/pcstatus.go b/pkg/pcstatus.go index 8d4d1f6..1472e3c 100644 --- a/pkg/pcstatus.go +++ b/pkg/pcstatus.go @@ -36,6 +36,12 @@ 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 + CachedOnly uint64 `json:"ccached,omitempty"` // number of pages that are cached but not dirty or writeback + 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, useCachestat bool) (PcStatus, error) { @@ -66,12 +72,20 @@ func GetPcStatus(fname string, useCachestat bool) (PcStatus, error) { if useCachestat { // Use cachestat implementation - var cached, psize int - cached, psize, err = FileCachestat(f, fi.Size()) + cstat, psize, err := FileCachestat(f, fi.Size()) if err != nil { return pcs, err } - pcs.Cached = cached + + // will be shown in json output only for now + pcs.CachedOnly = cstat.Cache + pcs.Dirty = cstat.Dirty + pcs.Writeback = cstat.Writeback + pcs.Evicted = cstat.Evicted + pcs.RecentlyEvicted = cstat.Recently_evicted + + // default for backward compatibility with mincore impl + pcs.Cached = int(cstat.Cache + cstat.Dirty + cstat.Writeback) pcs.Pages = psize } else { From 2ed94f8a795c6b2f57343f78f92351232e4345de Mon Sep 17 00:00:00 2001 From: boomskats Date: Wed, 6 Nov 2024 21:56:03 +0000 Subject: [PATCH 4/6] cache includes dirty and writeback --- pkg/pcstatus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/pcstatus.go b/pkg/pcstatus.go index 1472e3c..190ec72 100644 --- a/pkg/pcstatus.go +++ b/pkg/pcstatus.go @@ -85,7 +85,7 @@ func GetPcStatus(fname string, useCachestat bool) (PcStatus, error) { pcs.RecentlyEvicted = cstat.Recently_evicted // default for backward compatibility with mincore impl - pcs.Cached = int(cstat.Cache + cstat.Dirty + cstat.Writeback) + pcs.Cached = int(cstat.Cache) pcs.Pages = psize } else { From 01a78f3b60434d19acf5b8af04149dcd3fe26442 Mon Sep 17 00:00:00 2001 From: boomskats Date: Wed, 6 Nov 2024 22:03:32 +0000 Subject: [PATCH 5/6] fix for when zero is neither nil nor null --- pkg/pcstatus.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pkg/pcstatus.go b/pkg/pcstatus.go index 190ec72..ad45490 100644 --- a/pkg/pcstatus.go +++ b/pkg/pcstatus.go @@ -37,11 +37,10 @@ type PcStatus struct { 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 - CachedOnly uint64 `json:"ccached,omitempty"` // number of pages that are cached but not dirty or writeback - 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 + 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, useCachestat bool) (PcStatus, error) { @@ -78,11 +77,15 @@ func GetPcStatus(fname string, useCachestat bool) (PcStatus, error) { } // will be shown in json output only for now - pcs.CachedOnly = cstat.Cache - pcs.Dirty = cstat.Dirty - pcs.Writeback = cstat.Writeback - pcs.Evicted = cstat.Evicted - pcs.RecentlyEvicted = cstat.Recently_evicted + 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 // default for backward compatibility with mincore impl pcs.Cached = int(cstat.Cache) From 9148bd0a46db58e569b3a1aef871949218d2f6f1 Mon Sep 17 00:00:00 2001 From: boomskats Date: Thu, 7 Nov 2024 00:28:30 +0000 Subject: [PATCH 6/6] remembering to commit the readme --- README.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) 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