Golangでディレクトリのサイズを調べる

Posted on 2017.05.14

去年の終わりくらいに社内でプロコン的なのが行われた。 ざっくり要件は

  • あるディレクトリから 2 階層分のディレクトリのサイズの合計とファイル数を列挙する
  • CSV に吐き出す
  • ソートはいらない
  • 測定される環境は不明、実行速度で判定 とのことだったので golang の勉強にもちょうど良さそうだし、ってノリで書いてみた

directory-go

エディタはVSCode で書いた

やったこと

普通に対象ディレクトリ列挙して再帰的に情報取得して合計、みたいな感じ。

ただ結構はまった。

  • ファイルを走査するのが golang でいくつかある、どれが一番はやいか比べた(filepath walk とかおしゃれに書けるっぽかったけど、go レベルが低くてうまく channel で値の受け渡しが実現できなかった)
  • 環境によってファイルディスクリプタの上限にひっかかったりひっかからなかったり
  • Win 環境だとスレッドが作れなくなるエラーが発生したりしなかったり
  • channel の理解不足

調整弁として、特定の深さまではパラレルに、特定の深さ以降はシリアルに実行する実装をしつつ、その深さを起動時にいじれるようにしてどの環境でも変に止まったりしないようにした。

結果

他の人は C#の人が多くて最速出た。最速の結果だと 5~10 倍くらい早かった。あと単純にすべてシリアルに実行しても C#のパラレル実行より早くてびっくり。

実測結果はどっかいってしまったので調べて追記する

コード

メソッドのネーミングはいろいろ試してた名残でイマイチ


package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"runtime"
	"strings"
	"time"

	"strconv"

	"golang.org/x/text/encoding/japanese"
	"golang.org/x/text/transform"
)

const maxDepth int = 1

var asyncDepth = 2

type Output struct {
	Path  string
	Size  int64
	Count int64
}

var cpuCount int

func main() {
	cpuCount = runtime.NumCPU()
	runtime.GOMAXPROCS(cpuCount)

	fmt.Println("start! Core:", cpuCount)
	root := os.Args[1]
	_, err := os.Stat(root)
	if err != nil {
		fmt.Println("You must set the valid target path.")
		panic(err)
	}

	// asyncDepth is optional
	if len(os.Args) >= 3 {
		ad := os.Args[2]
		argDepth, err := strconv.Atoi(ad)
		if err == nil {
			asyncDepth = argDepth
		}
	}
	fmt.Println("async depth: ", asyncDepth)

	checkNonRepeat(root)
}

func checkNonRepeat(root string) {
	syncStart := time.Now()
	paths := getTargetPaths(root, 0)
	fmt.Println("path count:" + fmt.Sprint(len(paths)))
	buf := ""

	result := make(chan Output, cpuCount)
	c := make(chan Output)
	go getSizeRecursiveNonRepeat(root, 0, c, result)
	for i := 0; i < len(paths); i++ {
		o := <-result
		buf += o.Path + "," + fmt.Sprint(o.Size) + "," + fmt.Sprint(o.Count) + "\n"
	}

	ioutil.WriteFile("./ouput_utf8.csv", []byte(buf), os.ModePerm)

	b, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(buf), japanese.ShiftJIS.NewEncoder()))
	if err != nil {
		fmt.Println(err.Error())
	}
	ioutil.WriteFile("./output.csv", b, os.ModePerm)
	syncEnd := time.Now()

	fmt.Println("---output: ", syncEnd.Sub(syncStart).Seconds(), "sec")
}

func getTargetPaths(root string, depth int) []string {
	fi, err := ioutil.ReadDir(root)
	if err != nil {
		fmt.Println("error occured: ", err.Error())
		return make([]string, 0) // if permission denied, return empty
		// panic(err)
	}

	paths := make([]string, 0)
	if depth >= maxDepth {
		for _, f := range fi {
			if f.IsDir() {
				paths = append(paths, root+"/"+f.Name())
			}
		}
		return paths
	}

	for _, f := range fi {
		if f.IsDir() {
			paths = append(paths, getTargetPaths(root+"/"+f.Name(), depth+1)...)
			paths = append(paths, root+"/"+f.Name())
		}
	}
	return paths
}

func getSizeRecursive(root, search string) (int64, int64) {
	fi, err := ioutil.ReadDir(search)
	if err != nil {
		// fmt.Println("error occured: ", err.Error())
		return 0, 0 // if permission denied, return zeros
		// panic(err)
	}

	var size, count int64
	for _, f := range fi {
		if f.IsDir() {
			n := f.Name()
			s, c := getSizeRecursive(root, search+"/"+n)
			size += s
			count += c
		} else {
			size += f.Size()
			count++
		}
	}
	return size, count
}

func getSizeRecursiveNonRepeat(search string, depth int, outputChan chan Output, resultChan chan Output) {
	fi, err := ioutil.ReadDir(search)
	if err != nil {
		// fmt.Println("error occured: ", err.Error())
		outputChan <- Output{Path: search, Size: 0, Count: 0}
		if depth <= maxDepth+1 {
			resultChan <- Output{Path: search, Size: 0, Count: 0}
		}
		return
		// panic(err)
	}

	var size, count int64
	length := 0
	for _, f := range fi {
		if f.IsDir() {
			length++
		}
	}
	nextOutput := make(chan Output)
	defer close(nextOutput)
	for _, f := range fi {
		if f.IsDir() {
			if depth <= asyncDepth {
				go getSizeRecursiveNonRepeat(search+string(os.PathSeparator)+f.Name(), depth+1, nextOutput, resultChan)
			} else {
				s, c := getSizeRecursive(search+string(os.PathSeparator)+f.Name(), search+string(os.PathSeparator)+f.Name())
				size += s
				count += c
			}
		} else {
			size += f.Size()
			count++
		}
	}
	for _, f := range fi {
		if f.IsDir() && depth <= asyncDepth {
			next := <-nextOutput
			size += next.Size
			count += next.Count
		}
	}
	outputChan <- Output{Path: search, Size: size, Count: count}
	if depth <= maxDepth+1 {
		resultChan <- Output{Path: search, Size: size, Count: count}
		fmt.Println(search, ",", size, ",", count)
	}
}