クローリングのベンチマークを取ってみました。

# はじめに
[agouti](https://github.com/sclevine/agouti) を使ってクローリングを行い、ベンチマークを測定してみました。

# クローリング対象ページ
今回クローリング対象のページは、goのtemplateパッケージを使用してローカルに生成したものを利用しました。以下では9つのページを生成しています。

```go
func createPages() {
	createPage("/1", []string{"/2", "/3"})
	createPage("/2", []string{"/4", "/5"})
	createPage("/3", []string{"/6", "/7"})
	createPage("/4", []string{"/8", "/9"})
	createPage("/5", nil)
	createPage("/6", nil)
	createPage("/7", nil)
	createPage("/8", nil)
	createPage("/9", nil)

	http.ListenAndServe(":8080", nil)
}

func createPage(url string, links []string) {
	handler := newHandler(links)
	http.HandleFunc(url, handler)
}

func newHandler(links []string) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		temp := "template.html.tmpl"
		t := template.Must(template.New(temp).ParseFiles(temp))
		if err := t.Execute(w, links); err != nil {
			log.Fatal(err)
		}
	}
}
```

## template.html.tmpl
```html

<!DOCTYPE html>
<html>
<body>
    <div>
        {{range .}}
            <p><a href="{{.}}">{{.}}</a></p>
        {{end}}
    </div>
</body>
</html>
```

# クローラー
再帰的にクローリングを行いページ内のリンクを取得するクローラーを作成しました。

```go
// クローリング済のリンク一覧
var seen = make(map[string]bool)

// クローリング対象のリンク一覧
var worklinks = make(chan string)

// maxConcurrent = クローリング最大平行数
// maxCrawlCount = クローリングするページの最大数
func Crawls(link string, maxConcurrent int, maxCrawlCount int) ([]string, error) {
	errCh := make(chan error)

	go func() {
		worklinks <- link
	}()

	for i := 0; i < maxConcurrent; i++ {
		go func() {
			for link := range unseen {
				links, err := crawl(link)
				if err != nil {
					errCh <- err
					return
				}
				for _, l := range links {
					l := l
					go func() {
						worklinks <- l
					}()
				}
			}
		}()
	}

	for {
		select {
		case link := <-worklinks:
			if !seen[link] {
				seen[link] = true
				unseen <- link
			}
			if len(seen) >= maxCrawlCount {
				links := make([]string, 0, len(seen))
				for k := range seen {
					links = append(links, k)
				}
				return links, nil
			}
		case err := <-errCh:
			return nil, err
		}
	}
}

// ページ内のリンクを検索
func crawl(link string) ([]string, error) {
	driver := agouti.ChromeDriver(
		agouti.ChromeOptions("args", []string{
			"--headless",
		}),
	)
	if err := driver.Start(); err != nil {
		return nil, errors.Wrap(err, "Failed to start driver")
	}
	defer driver.Stop()

	p, err := driver.NewPage()
	if err != nil {
		return nil, err
	}

	err = p.Navigate(link)
	if err != nil {
		return nil, err
	}

	t := p.All("a")
	length, err := t.Count()
	if err != nil {
		return nil, err
	}

	links := []string{}
	for i := 0; i < length; i++ {
		link, err := t.At(i).Attribute("href")
		if err != nil {
			return nil, err
		}
		links = append(links, link)
	}
	return links, nil
}
```

# Benchmark測定
最大並行数を変えてベンチマークを測定してみました。現在の実装だと並行数を単純に増やせばその分早くなるというわけではないことがわかりました。。今後改善していきたいと思います。

|並行数|処理時間（ns）|
|:--|:--|
|1|16479309835|
|2|10561147824|
|3|8914031897|
|4|8981436252|
|5|7767930345|
|10|6496013517|
|50|8146189396|


```go

func BenchmarkCrawl1(b *testing.B) {
	benchmark(b, 1)
}

func BenchmarkCrawl2(b *testing.B) {
	benchmark(b, 2)
}

func BenchmarkCrawl3(b *testing.B) {
	benchmark(b, 3)
}

func BenchmarkCrawl4(b *testing.B) {
	benchmark(b, 4)
}

func BenchmarkCrawl5(b *testing.B) {
	benchmark(b, 5)
}

func BenchmarkCrawl10(b *testing.B) {
	benchmark(b, 10)
}

func BenchmarkCrawl10(b *testing.B) {
	benchmark(b, 50)
}

func benchmark(b *testing.B, maxConcurrent int) {
	go func() {
		createPages()
	}()

	for i := 0; i < b.N; i++ {
		links, err := Crawls("http://localhost:8080/1", maxConcurrent, 9)
		b.Logf("links: %v", links)
		if err != nil {
			b.Error(err)
			return
		}
	}
}
```