Golang – 加速抓資料夾的總大小的方法
Toby
Toby

最近因為一些意外所以要頻繁跑醫院回診,結果就是在等待跟來回的時間前後多了很多零碎的時間,所以我就不浪費這些時間來幫我用了快 6 – 7 年的 ArozOS 系統做了點更新了。但是這篇部落格並不是在講我更新了甚麼(雖然某程度上也是),而是講一下關於 Golang 處理 exec 輸出的一個小發現。

背景故事 – ArozOS 的 Disk Properties 載很慢

ArozOS 的 Disk Properties

ArozOS 因為採用半虛擬磁碟架構的設計,所以每次要列出 disk space 的時候,除了一般像 Windows 那樣會列出用了多少跟全部空間是多少以外,如果磁碟空間是使用者分隔的,就要多計算使用者佔用的空間比(圖上黃色條)

這個功能原本是使用 Walk function 來實現的。因為 ArozOS 的 vfs architecture 關系,這裡我使用的是 ArozOS 專用的 filepath module (你可以把它想像成 Go 原本的 filepath.Walk

var size int64 = 0
fshAbs.Walk(rpath, func(_ string, info os.FileInfo, err error) error {
    if err != nil {
        return err
    }
    if !info.IsDir() {
        size += info.Size()
    }
    return err
})
filesize = size

fshAbs 是 file system handler abstraction 的意思,大概就是「這個虛擬磁碟的檔案系統層」的 object。

另外 rpath 就是從 user space translate 過來到這個 abstraction layer 的實質路徑,如果是本機磁碟的話就會是好像 /media/storage1/users/{username}/myfile.txt 這樣的 path;如果是 network drive 的話會是跟 mount-point 的相對路徑,例如 WebDAV 或是 FTP 下就可能會出現 /myfile.txt,或是 SMB 下的 /Share/myfile.txt 之類。

這樣的 implementation 你一看就可以知道這個在 Network Drive 上是不可能實現的,因為每一個檔案 Go 都要抓它的 Stat,即是遠端有多少個檔案就會送多少個 file request 過去,所以自很早之前就 ArozOS 的 File Manager 就加入了 這個簡單粗暴的解決方法…

if fsh.IsNetworkDrive() {
    filesize = -1
}

然而隨著我在用的 NAS 東西越來越多,這個 walk function 即使在 local disk 下也開始變得很慢。

那改用 kernel 內置的 API 來抓不就好了嗎?

確實如此。Linux kernel 裡其實有內置一個滿好用的指令叫 du,用起來也超簡單,只要:

du -sb /media/storage1
481491222874 /media/storage1

那這個資料夾總大小(byte size)就出來了。如果你想要 block size 的話可以把 b 去掉。然後就是寫一個簡單的 Go function 去把資料抓出來。

/*
Get Directory Size with native syscall (local drive only)
faster than GetDirectorySize if system support du
*/
func GetDirectorySizeNative(filename string)(int64, error) {
    d, err: = apt.PackageExists("du")
    if err != nil || !d {
        return 0, err
    }

    //Convert the filename to absolute path
    abspath, err: = filepath.Abs(filename)
    if err != nil {
        return 0, err
    }

    //du command exists
    //use native syscall to get disk size
    cmd: = exec.Command("du", "-sb", abspath)
    out, err: = cmd.CombinedOutput()
    if err != nil {
        return 0, err
    }

    //Return value is something like 481491222874    /media/storage2
    //We need to trim off the spaces
    tmp: = string(out)
    tmp = strings.TrimSpace(tmp)
    tmp = strings.ReplaceAll(tmp, "\t", " ")
    for strings.Contains(tmp, "  ") {
        tmp = strings.ReplaceAll(tmp, "  ", " ")
    }

    chunks: = strings.Split(tmp, " ")
    if len(chunks) <= 1 {
        return 0, errors.New("malformed output")
    }

    //The first chunk should be the size in bytes
    size, err: = strconv.Atoi(chunks[0])
    if err != nil {
        return 0, errors.New("malformed output")
    }

    return int64(size), nil

}

對,記得要把 \t (Tab) 跟 ” ” (Space) 去掉,還要做一個 for loop 把 double space 變成 single space 這樣之後比較好 split。嘛雖然說也是可以用 regex 把它切成 slice,但是我是覺得如果用 regex 的話未來的我看到應該會想穿越回來扇現在的我一巴掌。

if fsh.IsLocalDrive() {
    //Try using native syscall to grab directory size
    nativeSize, err: = filesystem.GetDirectorySizeNative(fsh.Path)
    if err == nil {
        usefallback = false
        filesize = nativeSize
    }
}
// 如果 fallback 的話用原本的 Walk function

之後就是在原先的 code 裡加入這樣的一個功能,如果是 local drive 的話就去用 du 來抓,抓不到的話還是會跑回去用 Go 提供的 Walk implementation 為 fallback。

結果速度怎樣?

結果就是由原本 Walk 的 12 – 13 秒縮短到之後的 5秒,算是有變快了一點點(?