forked from RepoMirrors/btrfs
implement usage stats
This commit is contained in:
parent
3343324613
commit
6f59d604fc
83
btrfs.go
83
btrfs.go
@ -1,7 +1,6 @@
|
|||||||
package btrfs
|
package btrfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/dennwc/btrfs/ioctl"
|
"github.com/dennwc/btrfs/ioctl"
|
||||||
"io"
|
"io"
|
||||||
@ -12,8 +11,6 @@ import (
|
|||||||
|
|
||||||
const SuperMagic = 0x9123683E
|
const SuperMagic = 0x9123683E
|
||||||
|
|
||||||
const xattrPrefix = "btrfs."
|
|
||||||
|
|
||||||
func Open(path string, ro bool) (*FS, error) {
|
func Open(path string, ro bool) (*FS, error) {
|
||||||
if ok, err := IsSubVolume(path); err != nil {
|
if ok, err := IsSubVolume(path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -60,16 +57,16 @@ type Info struct {
|
|||||||
|
|
||||||
func (f *FS) Info() (out Info, err error) {
|
func (f *FS) Info() (out Info, err error) {
|
||||||
var arg btrfs_ioctl_fs_info_args
|
var arg btrfs_ioctl_fs_info_args
|
||||||
if err = ioctl.Do(f.f, _BTRFS_IOC_FS_INFO, &arg); err != nil {
|
arg, err = iocFsInfo(f.f)
|
||||||
return
|
if err == nil {
|
||||||
}
|
out = Info{
|
||||||
out = Info{
|
MaxID: arg.max_id,
|
||||||
MaxID: arg.max_id,
|
NumDevices: arg.num_devices,
|
||||||
NumDevices: arg.num_devices,
|
FSID: arg.fsid,
|
||||||
FSID: arg.fsid,
|
NodeSize: arg.nodesize,
|
||||||
NodeSize: arg.nodesize,
|
SectorSize: arg.sectorsize,
|
||||||
SectorSize: arg.sectorsize,
|
CloneAlignment: arg.clone_alignment,
|
||||||
CloneAlignment: arg.clone_alignment,
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -160,10 +157,10 @@ func (f *FS) SetFlags(flags SubvolFlags) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FS) Sync() (err error) {
|
func (f *FS) Sync() (err error) {
|
||||||
if err = ioctl.Do(f.f, _BTRFS_IOC_START_SYNC, nil); err != nil {
|
if err = ioctl.Ioctl(f.f, _BTRFS_IOC_START_SYNC, 0); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return ioctl.Do(f.f, _BTRFS_IOC_WAIT_SYNC, nil)
|
return ioctl.Ioctl(f.f, _BTRFS_IOC_WAIT_SYNC, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FS) CreateSubVolume(name string) error {
|
func (f *FS) CreateSubVolume(name string) error {
|
||||||
@ -221,58 +218,4 @@ func (f *FS) ListSubvolumes(filter func(Subvolume) bool) ([]Subvolume, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Compression string
|
func (f *FS) Usage() (UsageInfo, error) { return spaceUsage(f.f) }
|
||||||
|
|
||||||
const (
|
|
||||||
CompressionNone = Compression("")
|
|
||||||
LZO = Compression("lzo")
|
|
||||||
ZLIB = Compression("zlib")
|
|
||||||
)
|
|
||||||
|
|
||||||
const xattrCompression = xattrPrefix + "compression"
|
|
||||||
|
|
||||||
func SetCompression(path string, v Compression) error {
|
|
||||||
var value []byte
|
|
||||||
if v != CompressionNone {
|
|
||||||
var err error
|
|
||||||
value, err = syscall.ByteSliceFromString(string(v))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err := syscall.Setxattr(path, xattrCompression, value, 0)
|
|
||||||
if err != nil {
|
|
||||||
return &os.PathError{Op: "setxattr", Path: path, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCompression(path string) (Compression, error) {
|
|
||||||
var buf []byte
|
|
||||||
for {
|
|
||||||
sz, err := syscall.Getxattr(path, xattrCompression, nil)
|
|
||||||
if err == syscall.ENODATA || sz == 0 {
|
|
||||||
return CompressionNone, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err}
|
|
||||||
}
|
|
||||||
if cap(buf) < sz {
|
|
||||||
buf = make([]byte, sz)
|
|
||||||
} else {
|
|
||||||
buf = buf[:sz]
|
|
||||||
}
|
|
||||||
sz, err = syscall.Getxattr(path, xattrCompression, buf)
|
|
||||||
if err == syscall.ENODATA {
|
|
||||||
return CompressionNone, nil
|
|
||||||
} else if err == syscall.ERANGE {
|
|
||||||
// xattr changed by someone else, and is larger than our current buffer
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err}
|
|
||||||
}
|
|
||||||
buf = buf[:sz]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = bytes.TrimSuffix(buf, []byte{0})
|
|
||||||
return Compression(buf), nil
|
|
||||||
}
|
|
||||||
|
@ -6,6 +6,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
_BTRFS_BLOCK_GROUP_TYPE_MASK = (blockGroupData |
|
||||||
|
blockGroupSystem |
|
||||||
|
blockGroupMetadata)
|
||||||
_BTRFS_BLOCK_GROUP_PROFILE_MASK = (blockGroupRaid0 |
|
_BTRFS_BLOCK_GROUP_PROFILE_MASK = (blockGroupRaid0 |
|
||||||
blockGroupRaid1 |
|
blockGroupRaid1 |
|
||||||
blockGroupRaid5 |
|
blockGroupRaid5 |
|
||||||
|
57
ioctl_h.go
57
ioctl_h.go
@ -678,8 +678,49 @@ func iocDefaultSubvol(f *os.File, out *uint64) error {
|
|||||||
return ioctl.Do(f, _BTRFS_IOC_DEFAULT_SUBVOL, out)
|
return ioctl.Do(f, _BTRFS_IOC_DEFAULT_SUBVOL, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func iocSpaceInfo(f *os.File, out *btrfs_ioctl_space_args) error {
|
type spaceInfo struct {
|
||||||
return ioctl.Do(f, _BTRFS_IOC_SPACE_INFO, out)
|
Flags uint64
|
||||||
|
TotalBytes uint64
|
||||||
|
UsedBytes uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func iocSpaceInfo(f *os.File) ([]spaceInfo, error) {
|
||||||
|
arg := &btrfs_ioctl_space_args{}
|
||||||
|
if err := ioctl.Do(f, _BTRFS_IOC_SPACE_INFO, arg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n := arg.total_spaces
|
||||||
|
if n == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
const (
|
||||||
|
argSize = unsafe.Sizeof(btrfs_ioctl_space_args{})
|
||||||
|
infoSize = unsafe.Sizeof(btrfs_ioctl_space_info{})
|
||||||
|
)
|
||||||
|
buf := make([]byte, argSize+uintptr(n)*infoSize)
|
||||||
|
basePtr := unsafe.Pointer(&buf[0])
|
||||||
|
arg = (*btrfs_ioctl_space_args)(basePtr)
|
||||||
|
arg.space_slots = n
|
||||||
|
if err := ioctl.Do(f, _BTRFS_IOC_SPACE_INFO, arg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if arg.total_spaces == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if n > arg.total_spaces {
|
||||||
|
n = arg.total_spaces
|
||||||
|
}
|
||||||
|
out := make([]spaceInfo, n)
|
||||||
|
ptr := uintptr(basePtr) + argSize
|
||||||
|
for i := 0; i < int(n); i++ {
|
||||||
|
info := (*btrfs_ioctl_space_info)(unsafe.Pointer(ptr))
|
||||||
|
out[i] = spaceInfo{
|
||||||
|
Flags: info.flags,
|
||||||
|
TotalBytes: info.total_bytes,
|
||||||
|
UsedBytes: info.used_bytes,
|
||||||
|
}
|
||||||
|
ptr += infoSize
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func iocStartSync(f *os.File, out *uint64) error {
|
func iocStartSync(f *os.File, out *uint64) error {
|
||||||
@ -712,8 +753,16 @@ func iocScrubProgress(f *os.File, out *btrfs_ioctl_scrub_args) error {
|
|||||||
return ioctl.Do(f, _BTRFS_IOC_SCRUB_PROGRESS, out)
|
return ioctl.Do(f, _BTRFS_IOC_SCRUB_PROGRESS, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func iocDevInfo(f *os.File, out *btrfs_ioctl_dev_info_args) error {
|
func iocFsInfo(f *os.File) (out btrfs_ioctl_fs_info_args, err error) {
|
||||||
return ioctl.Do(f, _BTRFS_IOC_DEV_INFO, out)
|
err = ioctl.Do(f, _BTRFS_IOC_FS_INFO, &out)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func iocDevInfo(f *os.File, devid uint64, uuid UUID) (out btrfs_ioctl_dev_info_args, err error) {
|
||||||
|
out.devid = devid
|
||||||
|
out.uuid = uuid
|
||||||
|
err = ioctl.Do(f, _BTRFS_IOC_DEV_INFO, &out)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func iocBalanceCtl(f *os.File, out *int32) error {
|
func iocBalanceCtl(f *os.File, out *int32) error {
|
||||||
|
175
usage.go
Normal file
175
usage.go
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
package btrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cmpChunkBlockGroup(f1, f2 uint64) int {
|
||||||
|
var mask uint64
|
||||||
|
|
||||||
|
if (f1 & _BTRFS_BLOCK_GROUP_TYPE_MASK) ==
|
||||||
|
(f2 & _BTRFS_BLOCK_GROUP_TYPE_MASK) {
|
||||||
|
mask = _BTRFS_BLOCK_GROUP_PROFILE_MASK
|
||||||
|
} else if f2&blockGroupSystem != 0 {
|
||||||
|
return -1
|
||||||
|
} else if f1&blockGroupSystem != 0 {
|
||||||
|
return +1
|
||||||
|
} else {
|
||||||
|
mask = _BTRFS_BLOCK_GROUP_TYPE_MASK
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f1 & mask) > (f2 & mask) {
|
||||||
|
return +1
|
||||||
|
} else if (f1 & mask) < (f2 & mask) {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type spaceInfoByBlockGroup []spaceInfo
|
||||||
|
|
||||||
|
func (a spaceInfoByBlockGroup) Len() int { return len(a) }
|
||||||
|
func (a spaceInfoByBlockGroup) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a spaceInfoByBlockGroup) Less(i, j int) bool {
|
||||||
|
return cmpChunkBlockGroup(a[i].Flags, a[j].Flags) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsageInfo struct {
|
||||||
|
Total uint64
|
||||||
|
TotalUnused uint64
|
||||||
|
TotalUsed uint64
|
||||||
|
TotalChunks uint64
|
||||||
|
|
||||||
|
FreeEstimated uint64
|
||||||
|
FreeMin uint64
|
||||||
|
|
||||||
|
LogicalDataChunks uint64
|
||||||
|
RawDataChunks uint64
|
||||||
|
RawDataUsed uint64
|
||||||
|
|
||||||
|
LogicalMetaChunks uint64
|
||||||
|
RawMetaChunks uint64
|
||||||
|
RawMetaUsed uint64
|
||||||
|
|
||||||
|
SystemUsed uint64
|
||||||
|
SystemChunks uint64
|
||||||
|
|
||||||
|
DataRatio float64
|
||||||
|
MetadataRatio float64
|
||||||
|
|
||||||
|
GlobalReserve uint64
|
||||||
|
GlobalReserveUsed uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
const minUnallocatedThreshold = 16 * 1024 * 1024
|
||||||
|
|
||||||
|
func spaceUsage(f *os.File) (UsageInfo, error) {
|
||||||
|
info, err := iocFsInfo(f)
|
||||||
|
if err != nil {
|
||||||
|
return UsageInfo{}, err
|
||||||
|
}
|
||||||
|
var u UsageInfo
|
||||||
|
for i := uint64(0); i <= info.max_id; i++ {
|
||||||
|
dev, err := iocDevInfo(f, i, UUID{})
|
||||||
|
if err == syscall.ENODEV {
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return UsageInfo{}, err
|
||||||
|
}
|
||||||
|
u.Total += dev.total_bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
spaces, err := iocSpaceInfo(f)
|
||||||
|
if err != nil {
|
||||||
|
return UsageInfo{}, err
|
||||||
|
}
|
||||||
|
sort.Sort(spaceInfoByBlockGroup(spaces))
|
||||||
|
var (
|
||||||
|
maxDataRatio int = 1
|
||||||
|
mixed bool
|
||||||
|
)
|
||||||
|
for _, s := range spaces {
|
||||||
|
ratio := 1
|
||||||
|
switch {
|
||||||
|
case s.Flags&blockGroupRaid0 != 0:
|
||||||
|
ratio = 1
|
||||||
|
case s.Flags&blockGroupRaid1 != 0:
|
||||||
|
ratio = 2
|
||||||
|
case s.Flags&blockGroupRaid5 != 0:
|
||||||
|
ratio = 0
|
||||||
|
case s.Flags&blockGroupRaid6 != 0:
|
||||||
|
ratio = 0
|
||||||
|
case s.Flags&blockGroupDup != 0:
|
||||||
|
ratio = 2
|
||||||
|
case s.Flags&blockGroupRaid10 != 0:
|
||||||
|
ratio = 2
|
||||||
|
}
|
||||||
|
if ratio > maxDataRatio {
|
||||||
|
maxDataRatio = ratio
|
||||||
|
}
|
||||||
|
if s.Flags&spaceInfoGlobalRsv != 0 {
|
||||||
|
u.GlobalReserve = s.TotalBytes
|
||||||
|
u.GlobalReserveUsed = s.UsedBytes
|
||||||
|
}
|
||||||
|
if s.Flags&(blockGroupData|blockGroupMetadata) == (blockGroupData | blockGroupMetadata) {
|
||||||
|
mixed = true
|
||||||
|
}
|
||||||
|
if s.Flags&blockGroupData != 0 {
|
||||||
|
u.RawDataUsed += s.UsedBytes * uint64(ratio)
|
||||||
|
u.RawDataChunks += s.TotalBytes * uint64(ratio)
|
||||||
|
u.LogicalDataChunks += s.TotalBytes
|
||||||
|
}
|
||||||
|
if s.Flags&blockGroupMetadata != 0 {
|
||||||
|
u.RawMetaUsed += s.UsedBytes * uint64(ratio)
|
||||||
|
u.RawMetaChunks += s.TotalBytes * uint64(ratio)
|
||||||
|
u.LogicalMetaChunks += s.TotalBytes
|
||||||
|
}
|
||||||
|
if s.Flags&blockGroupSystem != 0 {
|
||||||
|
u.SystemUsed += s.UsedBytes * uint64(ratio)
|
||||||
|
u.SystemChunks += s.TotalBytes * uint64(ratio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.TotalChunks = u.RawDataChunks + u.SystemChunks
|
||||||
|
u.TotalUsed = u.RawDataUsed + u.SystemUsed
|
||||||
|
if !mixed {
|
||||||
|
u.TotalChunks += u.RawMetaChunks
|
||||||
|
u.TotalUsed += u.RawMetaUsed
|
||||||
|
}
|
||||||
|
u.TotalUnused = u.Total - u.TotalChunks
|
||||||
|
|
||||||
|
u.DataRatio = float64(u.RawDataChunks) / float64(u.LogicalDataChunks)
|
||||||
|
if mixed {
|
||||||
|
u.MetadataRatio = u.DataRatio
|
||||||
|
} else {
|
||||||
|
u.MetadataRatio = float64(u.RawMetaChunks) / float64(u.LogicalMetaChunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're able to fill at least DATA for the unused space
|
||||||
|
//
|
||||||
|
// With mixed raid levels, this gives a rough estimate but more
|
||||||
|
// accurate than just counting the logical free space
|
||||||
|
// (l_data_chunks - l_data_used)
|
||||||
|
//
|
||||||
|
// In non-mixed case there's no difference.
|
||||||
|
u.FreeEstimated = uint64(float64(u.RawDataChunks-u.RawDataUsed) / u.DataRatio)
|
||||||
|
|
||||||
|
// For mixed-bg the metadata are left out in calculations thus global
|
||||||
|
// reserve would be lost. Part of it could be permanently allocated,
|
||||||
|
// we have to subtract the used bytes so we don't go under zero free.
|
||||||
|
if mixed {
|
||||||
|
u.FreeEstimated -= u.GlobalReserve - u.GlobalReserveUsed
|
||||||
|
}
|
||||||
|
u.FreeMin = u.FreeEstimated
|
||||||
|
|
||||||
|
// Chop unallocatable space
|
||||||
|
// FIXME: must be applied per device
|
||||||
|
if u.TotalUnused >= minUnallocatedThreshold {
|
||||||
|
u.FreeEstimated += uint64(float64(u.TotalUnused) / u.DataRatio)
|
||||||
|
// Match the calculation of 'df', use the highest raid ratio
|
||||||
|
u.FreeMin += u.TotalUnused / uint64(maxDataRatio)
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
66
xattr.go
Normal file
66
xattr.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package btrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
xattrPrefix = "btrfs."
|
||||||
|
xattrCompression = xattrPrefix + "compression"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Compression string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CompressionNone = Compression("")
|
||||||
|
LZO = Compression("lzo")
|
||||||
|
ZLIB = Compression("zlib")
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetCompression(path string, v Compression) error {
|
||||||
|
var value []byte
|
||||||
|
if v != CompressionNone {
|
||||||
|
var err error
|
||||||
|
value, err = syscall.ByteSliceFromString(string(v))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := syscall.Setxattr(path, xattrCompression, value, 0)
|
||||||
|
if err != nil {
|
||||||
|
return &os.PathError{Op: "setxattr", Path: path, Err: err}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCompression(path string) (Compression, error) {
|
||||||
|
var buf []byte
|
||||||
|
for {
|
||||||
|
sz, err := syscall.Getxattr(path, xattrCompression, nil)
|
||||||
|
if err == syscall.ENODATA || sz == 0 {
|
||||||
|
return CompressionNone, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err}
|
||||||
|
}
|
||||||
|
if cap(buf) < sz {
|
||||||
|
buf = make([]byte, sz)
|
||||||
|
} else {
|
||||||
|
buf = buf[:sz]
|
||||||
|
}
|
||||||
|
sz, err = syscall.Getxattr(path, xattrCompression, buf)
|
||||||
|
if err == syscall.ENODATA {
|
||||||
|
return CompressionNone, nil
|
||||||
|
} else if err == syscall.ERANGE {
|
||||||
|
// xattr changed by someone else, and is larger than our current buffer
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err}
|
||||||
|
}
|
||||||
|
buf = buf[:sz]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf = bytes.TrimSuffix(buf, []byte{0})
|
||||||
|
return Compression(buf), nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user