hd-idle/diskstats/snapshot.go
2025-08-06 15:37:55 +02:00

237 lines
5.6 KiB
Go

// hd-idle - spin down idle hard disks
// Copyright (C) 2018 Andoni del Olmo
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package diskstats
import (
"bufio"
"errors"
"fmt"
"io"
"log"
"os"
"regexp"
"strconv"
"strings"
)
/*
https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
The /proc/diskstats file displays the I/O statistics
of block devices. Each line contains the following 14
fields:
1 - major number
2 - minor mumber
3 - device name
4 - reads completed successfully
5 - reads merged
6 - sectors read
7 - time spent reading (ms)
8 - writes completed
9 - writes merged
10 - sectors written
11 - time spent writing (ms)
12 - I/Os currently in progress
13 - time spent doing I/Os (ms)
14 - weighted time spent doing I/Os (ms)
Kernel 4.18+ appends four more fields for discard
tracking putting the total at 18:
15 - discards completed successfully
16 - discards merged
17 - sectors discarded
18 - time spent discarding
Kernel 5.5+ appends two more fields for flush requests:
19 - flush requests completed successfully
20 - time spent flushing
For more details refer to Documentation/admin-guide/iostats.rst
*/
const (
deviceNameCol = 2 // field 3 - device name
readsCol = 5 // field 6 - sectors read
writesCol = 9 // field 10 - sectors written
)
type DeviceType int
const (
Unknown DeviceType = iota
Disk
Partition
DeviceMapper
)
type ReadWriteStats struct {
Name string
Type DeviceType
Reads uint64
Writes uint64
}
var scsiDiskRegex *regexp.Regexp
var scsiPartitionRegex *regexp.Regexp
var deviceMapperRegex *regexp.Regexp
type diskHolderGetterFunc func(string, string) (string, error)
func init() {
scsiDiskRegex = regexp.MustCompile("sd[a-z]+$")
scsiPartitionRegex = regexp.MustCompile("sd[a-z]+[0-9]+$")
deviceMapperRegex = regexp.MustCompile("dm-.*$")
}
func Snapshot() []ReadWriteStats {
f, err := os.Open("/proc/diskstats")
if err != nil {
log.Fatal(err)
}
defer f.Close()
return readSnapshot(f, getDiskHolder)
}
func readSnapshot(r io.Reader, holderGetter diskHolderGetterFunc) []ReadWriteStats {
diskStatsMap := make(map[string]ReadWriteStats)
partitionStatsMap := make(map[string]ReadWriteStats)
deviceMapperHolderMap := make(map[string]string)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
diskStats, err := statsForDisk(scanner.Text())
if err != nil {
continue
}
if diskStats.Type == Disk {
diskStatsMap[diskStats.Name] = *diskStats
if dmName, err := holderGetter(diskStats.Name, "/sys/class/block/%s/holders/"); err == nil && dmName != "" {
deviceMapperHolderMap[dmName] = diskStats.Name
}
} else {
partitionStatsMap[diskStats.Name] = *diskStats
}
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
for _, partitionStats := range partitionStatsMap {
var diskName string
var ok bool
switch partitionStats.Type {
case Partition:
diskName = strings.TrimRight(partitionStats.Name,"0123456789")
case DeviceMapper:
if diskName, ok = deviceMapperHolderMap[partitionStats.Name]; !ok {
continue
}
default:
continue
}
var diskStats ReadWriteStats
if diskStats, ok = diskStatsMap[diskName]; !ok {
continue
}
if diskStats.Type == Disk {
// replace disk statistics by partition or holder stats
diskStats.Type = partitionStats.Type
diskStats.Writes = partitionStats.Writes
diskStats.Reads = partitionStats.Reads
} else {
// otherwise, accumulate stats of all partitions and holder if any
diskStats.Writes += partitionStats.Writes
diskStats.Reads += partitionStats.Reads
}
diskStatsMap[diskName] = diskStats
}
return toSlice(diskStatsMap)
}
func getDiskHolder(diskName, pathFormat string) (string, error) {
/* This returns only the first holder. In practice when using LUKS, there is only one holder */
holdersDir := fmt.Sprintf(pathFormat, diskName)
if _, err := os.Stat(holdersDir); os.IsNotExist(err) {
return "", err
}
files, err := os.ReadDir(holdersDir)
if err != nil {
return "", err
}
for _, file := range files {
return file.Name(), nil
}
return "", nil
}
func statsForDisk(rawStats string) (*ReadWriteStats, error) {
reader := strings.NewReader(rawStats)
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
cols := strings.Fields(scanner.Text())
name := cols[deviceNameCol]
deviceType := Unknown
reads, _ := strconv.ParseUint(cols[readsCol], 10, 64)
writes, _ := strconv.ParseUint(cols[writesCol], 10, 64)
if scsiDiskRegex.MatchString(name) {
deviceType = Disk
} else if scsiPartitionRegex.MatchString(name) {
deviceType = Partition
} else if deviceMapperRegex.MatchString(name) {
deviceType = DeviceMapper
} else {
continue
}
stats := &ReadWriteStats{
Name: name,
Type: deviceType,
Reads: reads,
Writes: writes,
}
return stats, nil
}
if err := scanner.Err(); err != nil {
return nil, err
}
return nil, errors.New("cannot read disk stats")
}
func toSlice(rws map[string]ReadWriteStats) []ReadWriteStats {
var snapshot []ReadWriteStats
for _, r := range rws {
snapshot = append(snapshot, r)
}
return snapshot
}