Monitor hard disk usage with Go

Programming Snapshot – Disk Speedo

© Lead Image © Author, 123RF.com

© Lead Image © Author, 123RF.com

Article from Issue 253/2021
Author(s):

To keep an eye on the remaining disk space during storage-intensive operations, you can check out this speedometer/odometer written in Go.

Storage-hungry applications such as video-editing software gobble up disk capacity like a massive vacuum cleaner: Before you know it, everything has been used up. In situations such as this, incorrectly programmed software tends to crash, and all the time you invested in your current project is irretrievably lost. If you take precautions up front, you can avoid headaches later on.

How about a constantly updated display of the remaining space on a dashboard-like instrument on the desktop, where you can see out of the corner of your eye how much disk space is wasted by an action that has just been triggered, such as rendering a video? You can write something like this quickly in Go.

On the Dashboard

A car's dashboard shows the current speed as well as the mileage. If you apply this to hard disks, the car's mileage reading becomes the total disk space consumed. Similarly, where in a car the speedometer needle shows the current speed, in the hard disk universe, the needle measures the space consumed per unit of time. The analogy for a storage-hungry application would be a speeding traffic offender, if you like.

Figures 1 and 2 show the terminal output from the finished Go program, Disk Speedo (or dftop). At the top, a progress bar illustrates the amount of disk space used thus far (in this case, 35 percent). At the bottom, a speedometer needle implemented as a pie chart indicates whether space is disappearing (red) or coming back (green) and how fast this is happening. At a simulated speed of 0 to 100, the needle of this somewhat unusual instrument starts at the bottom of the circle and then moves upwards in a counter-clockwise direction. Figure 1 shows a write speed of 35; Figure 2 shows a delete action at a speed of 65. At a speed of 100, the circle would be completely filled with the corresponding color, red or green.

Figure 1: The disk is 39 percent occupied, and a write is consuming more space.
Figure 2: The green speedometer indicates a deletion, which frees up disk space.

Avoid Shell Calls

To determine the remaining space on a data volume, the program could repeatedly call the df shell function. But this would waste valuable resources, because the shell would have to start a new df process each time. Fortunately, the nifty statfs programming interface [1] on Unix systems reports the total number of blocks provided on the corresponding mount as well as the blocks on the storage medium that are still unoccupied, without needing to call any shell utilities.

The Go interface for statfs gives you the total number of free blocks with Bfree() and the subset of free blocks that the non-root user can occupy with Bavail(). Multiplying these numbers by the block size defined on the storage medium as Bsize() gives you the remaining storage space in tera-, giga-, or whatever-bytes.

The space() function from line 61 in Listing 1 [2] determines the utilization of the storage medium and returns values for the number of occupied blocks, as well as their total number on a particular volume. If an error occurs when determining the capacity, space() pass it back as the third return value to the calling main program. But which hard disk's capacity will the program actually measure on a system with multiple storage media? Depending on the directory from which you call the speedometer, it will display the space on the hard disk it is residing on.

Listing 1

dftop.go

01 package main
02 import (
03   "container/ring"
04   "golang.org/x/sys/unix"
05   "os"
06   "time"
07 )
08
09 func main() {
10   wd, err := os.Getwd()
11   if err != nil {
12     panic(err)
13   }
14
15   ui := NewUI()
16   ui.Update(0, 0.0)
17   uidone := ui.Run()
18   defer ui.Close()
19   r := ring.New(2)
20
21   for {
22     used, total, err := space(wd)
23     if err != nil {
24       panic(err)
25     }
26
27     r.Value = used
28     p := used * 100 / total
29     ui.Update(int(p), speed(r))
30     r = r.Next()
31
32     select {
33     case <-uidone:
34       return
35     case <-time.After(
36       1 * time.Second):
37       continue
38     }
39   }
40 }
41
42 const maxSpeed = 100000
43
44 func speed(r *ring.Ring) float64 {
45   if r.Prev().Value == nil {
46     return 0
47   }
48   s := float64(int(
49     r.Value.(uint64)-
50       r.Prev().Value.(uint64))) /
51     maxSpeed
52
53   if s > 1 {
54     s = 1
55   } else if s < -1 {
56     s = -1
57   }
58   return s
59 }
60
61 func space(dir string) (
62   uint64, uint64, error) {
63   var stat unix.Statfs_t
64   err := unix.Statfs(dir, &stat)
65   return stat.Blocks -
66     stat.Bfree, stat.Blocks, err
67 }

Storage in a Circle

The speed at which the hard disk fills up is defined by the difference between two measurements of the fill level at different times divided by the time elapsed between them.

To do this, the program needs to store one or more past measurements in order to determine the delta to the currently measured value. This could be implemented using separate variables, but a ring buffer (Figure 3) of the container/ring type from the Go standard library does this in an elegant way without much code.

Figure 3: A ring buffer automatically overwrites old and obsolete values. Wikipedia, CC BY-SA 4.0

The ring buffer stores new values in r.Value, in sequence, in points that lie on a circular path. r.Next() moves to the next point, and r.Prev() goes back to the previous one. If the algorithm arrives at the first point again at some point on its circular path, it simply overwrites it. A ring buffer can only ever access the N most recent values, but it does not clutter the system's memory with irrelevant values from the past. A ring buffer created as shown in line 19 (Listing 1) with only two entries certainly does not leverage the data structure's full potential, but if desired, you can expand the buffer to include more entries for averaging and smoothing the display.

The speed() function starting in line 44 computes the current filling speed of the storage medium using this procedure. If the ring buffer does not yet carry two values because the algorithm just started out, the speed cannot yet be determined and line 46 returns the value  .

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Ready to Rumble

    A Go program writes a downloaded ISO file to a bootable USB stick. To prevent it from accidentally overwriting the hard disk, Mike Schilli provides it with a user interface and security checks.

  • Classics Repackaged

    Even command-line lovers appreciate a classic terminal UI. Mike Schilli shows how to whip up a Go program that dynamically displays network interfaces and IPs.

  • Progress by Installments

    Desktop applications, websites, and even command-line tools routinely display progress bars to keep impatient users patient during time-consuming actions. Mike Schilli shows several programming approaches for handwritten tools.

  • hdparm Drive Utility

    Hdparm is the tool to use when it comes to tuning your hard disk or DVD drive, but it can also measure read speed, deliver valuable information about the device, change important drive settings, and even erase SSDs securely.

  • At a Glance

    Using extensions in Go and Ruby, Mike Schilli adapts the WTF terminal dashboard tool to meet his personal needs.

comments powered by Disqus