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