Commit 7b0cb1ac authored by Han-Wen Nienhuys's avatar Han-Wen Nienhuys

benchmark: move bulkstatting to separate binary.

This makes CFuse and go-fuse easier to compare.  Get rid of the
baroque analysis code, unused delay code, and some general cleanup.
parent 4d95f545
cstatfs: statfs.cc cstatfs: statfs.cc
g++ -O2 -Wall -std=c++0x $< `pkg-config fuse --cflags --libs` -o $@ g++ -O2 -Wall -std=c++0x $< `pkg-config fuse --cflags --libs` -o $@
bulkstat.bin: bulkstat/main.go
(cd bulkstat && go build main.go)
cp bulkstat/main $@
...@@ -4,13 +4,8 @@ package benchmark ...@@ -4,13 +4,8 @@ package benchmark
import ( import (
"bufio" "bufio"
"fmt"
"log" "log"
"math"
"os" "os"
"regexp"
"sort"
"time"
) )
func ReadLines(name string) []string { func ReadLines(name string) []string {
...@@ -37,101 +32,3 @@ func ReadLines(name string) []string { ...@@ -37,101 +32,3 @@ func ReadLines(name string) []string {
return l return l
} }
// Used for benchmarking. Returns milliseconds.
func BulkStat(parallelism int, files []string) float64 {
todo := make(chan string, len(files))
dts := make(chan time.Duration, parallelism)
for i := 0; i < parallelism; i++ {
go func() {
t := time.Now()
for {
fn := <-todo
if fn == "" {
break
}
_, err := os.Lstat(fn)
if err != nil {
log.Fatal("All stats should succeed:", err)
}
}
dts <- time.Now().Sub(t)
}()
}
for _, v := range files {
todo <- v
}
close(todo)
total := 0.0
for i := 0; i < parallelism; i++ {
total += float64(<-dts) / float64(time.Millisecond)
}
avg := total / float64(len(files))
return avg
}
func AnalyzeBenchmarkRuns(label string, times []float64) {
sorted := times
sort.Float64s(sorted)
tot := 0.0
for _, v := range times {
tot += v
}
n := float64(len(times))
avg := tot / n
variance := 0.0
for _, v := range times {
variance += (v - avg) * (v - avg)
}
variance /= n
stddev := math.Sqrt(variance)
median := sorted[len(times)/2]
perc90 := sorted[int(n*0.9)]
perc10 := sorted[int(n*0.1)]
if VerboseTest() {
fmt.Printf(
"%s: %d samples\n"+
"avg %.3fms +/- %.0f%% "+
"median %.3fms, 10%%tiles: [-%.0f%%, +%.0f%%]\n",
label,
len(times), avg, 100.0*2*stddev/avg,
median, 100*(median-perc10)/median, 100*(perc90-median)/median)
}
}
func RunBulkStat(runs int, threads int, sleepTime time.Duration, files []string) (results []float64) {
for j := 0; j < runs; j++ {
result := BulkStat(threads, files)
results = append(results, result)
if j < runs-1 {
fmt.Printf("Sleeping %d seconds\n", sleepTime)
time.Sleep(sleepTime)
}
}
return results
}
func CountCpus() int {
var contents [10240]byte
f, err := os.Open("/proc/stat")
defer f.Close()
if err != nil {
return 1
}
n, _ := f.Read(contents[:])
re, _ := regexp.Compile("\ncpu[0-9]")
return len(re.FindAllString(string(contents[:n]), 100))
}
package main
import (
"bufio"
"flag"
"log"
"os"
"path/filepath"
"sync"
)
func BulkStat(parallelism int, files []string) {
todo := make(chan string, len(files))
var wg sync.WaitGroup
wg.Add(parallelism)
for i := 0; i < parallelism; i++ {
go func() {
for {
fn := <-todo
if fn == "" {
break
}
_, err := os.Lstat(fn)
if err != nil {
log.Fatal("All stats should succeed:", err)
}
}
wg.Done()
}()
}
for _, v := range files {
todo <- v
}
close(todo)
wg.Wait()
}
func ReadLines(name string) []string {
f, err := os.Open(name)
if err != nil {
log.Fatal("ReadLines: ", err)
}
defer f.Close()
r := bufio.NewReader(f)
l := []string{}
for {
line, _, err := r.ReadLine()
if line == nil || err != nil {
break
}
fn := string(line)
l = append(l, fn)
}
if len(l) == 0 {
log.Fatal("no files added")
}
return l
}
func main() {
N := flag.Int("N", 1000, "how many files to stat")
cpu := flag.Int("cpu", 1, "how many threads to use")
prefix := flag.String("prefix", "", "mount point")
quiet := flag.Bool("quiet", false, "be quiet")
flag.Parse()
f := flag.Arg(0)
files := ReadLines(f)
for i, f := range files {
files[i] = filepath.Join(*prefix, f)
}
if !*quiet {
log.Printf("statting %d with %d threads; first file %s (%d names)", *N, *cpu, files[0], len(files))
}
todo := *N
for todo > 0 {
if len(files) > todo {
files = files[:todo]
}
BulkStat(*cpu, files)
todo -= len(files)
}
}
...@@ -24,7 +24,7 @@ func VerboseTest() bool { ...@@ -24,7 +24,7 @@ func VerboseTest() bool {
return flag != nil && flag.Value.String() == "true" return flag != nil && flag.Value.String() == "true"
} }
func setupFs(fs pathfs.FileSystem) (string, func()) { func setupFs(fs pathfs.FileSystem, N int) (string, func()) {
opts := &nodefs.Options{ opts := &nodefs.Options{
EntryTimeout: 0.0, EntryTimeout: 0.0,
AttrTimeout: 0.0, AttrTimeout: 0.0,
...@@ -44,14 +44,18 @@ func setupFs(fs pathfs.FileSystem) (string, func()) { ...@@ -44,14 +44,18 @@ func setupFs(fs pathfs.FileSystem) (string, func()) {
return mountPoint, func() { return mountPoint, func() {
if VerboseTest() { if VerboseTest() {
var total time.Duration
for _, n := range []string{"LOOKUP", "GETATTR", "OPENDIR", "READDIR", for _, n := range []string{"LOOKUP", "GETATTR", "OPENDIR", "READDIR",
"READDIRPLUS", "READDIRPLUS", "RELEASEDIR", "FLUSH",
} { } {
if count, dt := lmap.Get(n); count > 0 { if count, dt := lmap.Get(n); count > 0 {
total += dt
log.Printf("%s %v/call n=%d", n, log.Printf("%s %v/call n=%d", n,
dt/time.Duration(count), count) dt/time.Duration(count), count)
} }
} }
log.Printf("total %v, %v/bench op", total, total/time.Duration(N))
} }
err := state.Unmount() err := state.Unmount()
...@@ -60,19 +64,18 @@ func setupFs(fs pathfs.FileSystem) (string, func()) { ...@@ -60,19 +64,18 @@ func setupFs(fs pathfs.FileSystem) (string, func()) {
} else { } else {
os.RemoveAll(mountPoint) os.RemoveAll(mountPoint)
} }
} }
} }
func TestNewStatFs(t *testing.T) { func TestNewStatFs(t *testing.T) {
fs := NewStatFs() fs := NewStatFS()
for _, n := range []string{ for _, n := range []string{
"file.txt", "sub/dir/foo.txt", "file.txt", "sub/dir/foo.txt",
"sub/dir/bar.txt", "sub/marine.txt"} { "sub/dir/bar.txt", "sub/marine.txt"} {
fs.AddFile(n) fs.AddFile(n)
} }
wd, clean := setupFs(fs) wd, clean := setupFs(fs, 1)
defer clean() defer clean()
names, err := ioutil.ReadDir(wd) names, err := ioutil.ReadDir(wd)
...@@ -114,18 +117,18 @@ func TestNewStatFs(t *testing.T) { ...@@ -114,18 +117,18 @@ func TestNewStatFs(t *testing.T) {
} }
} }
func BenchmarkGoFuseThreadedStat(b *testing.B) { func BenchmarkGoFuseStat(b *testing.B) {
b.StopTimer() b.StopTimer()
fs := NewStatFs() fs := NewStatFS()
fs.delay = delay
wd, _ := os.Getwd() wd, _ := os.Getwd()
files := ReadLines(wd + "/testpaths.txt") fileList := wd + "/testpaths.txt"
files := ReadLines(fileList)
for _, fn := range files { for _, fn := range files {
fs.AddFile(fn) fs.AddFile(fn)
} }
wd, clean := setupFs(fs) wd, clean := setupFs(fs, b.N)
defer clean() defer clean()
for i, l := range files { for i, l := range files {
...@@ -133,7 +136,9 @@ func BenchmarkGoFuseThreadedStat(b *testing.B) { ...@@ -133,7 +136,9 @@ func BenchmarkGoFuseThreadedStat(b *testing.B) {
} }
threads := runtime.GOMAXPROCS(0) threads := runtime.GOMAXPROCS(0)
TestingBOnePass(b, threads, files) if err := TestingBOnePass(b, threads, fileList, wd); err != nil {
log.Fatalf("TestingBOnePass %v8", err)
}
} }
func readdir(d string) error { func readdir(d string) error {
...@@ -149,8 +154,7 @@ func readdir(d string) error { ...@@ -149,8 +154,7 @@ func readdir(d string) error {
func BenchmarkGoFuseReaddir(b *testing.B) { func BenchmarkGoFuseReaddir(b *testing.B) {
b.StopTimer() b.StopTimer()
fs := NewStatFs() fs := NewStatFS()
fs.delay = delay
wd, _ := os.Getwd() wd, _ := os.Getwd()
dirSet := map[string]struct{}{} dirSet := map[string]struct{}{}
...@@ -160,7 +164,7 @@ func BenchmarkGoFuseReaddir(b *testing.B) { ...@@ -160,7 +164,7 @@ func BenchmarkGoFuseReaddir(b *testing.B) {
dirSet[filepath.Dir(fn)] = struct{}{} dirSet[filepath.Dir(fn)] = struct{}{}
} }
wd, clean := setupFs(fs) wd, clean := setupFs(fs, b.N)
defer clean() defer clean()
var dirs []string var dirs []string
...@@ -183,30 +187,37 @@ func BenchmarkGoFuseReaddir(b *testing.B) { ...@@ -183,30 +187,37 @@ func BenchmarkGoFuseReaddir(b *testing.B) {
b.StopTimer() b.StopTimer()
} }
func TestingBOnePass(b *testing.B, threads int, files []string) (results []float64) { func TestingBOnePass(b *testing.B, threads int, filelist, mountPoint string) error {
runtime.GC() runtime.GC()
var before, after runtime.MemStats var before, after runtime.MemStats
runtime.ReadMemStats(&before) runtime.ReadMemStats(&before)
// We shell out to an external program, so the time spent by
// the part stat'ing doesn't interfere with the time spent by
// the FUSE server.
cmd := exec.Command("./bulkstat.bin",
fmt.Sprintf("-cpu=%d", threads),
fmt.Sprintf("-prefix=%s", mountPoint),
fmt.Sprintf("-N=%d", b.N),
fmt.Sprintf("-quiet=%v", !VerboseTest()),
filelist)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
b.StartTimer() b.StartTimer()
todo := b.N err := cmd.Run()
for todo > 0 {
if len(files) > todo {
files = files[:todo]
}
result := BulkStat(threads, files)
todo -= len(files)
results = append(results, result)
}
b.StopTimer() b.StopTimer()
runtime.ReadMemStats(&after) runtime.ReadMemStats(&after)
if err != nil {
return err
}
if VerboseTest() { if VerboseTest() {
fmt.Printf("GC count %d, total GC time: %d ns/file\n", fmt.Printf("GC count %d, total GC time: %d ns/file\n",
after.NumGC-before.NumGC, (after.PauseTotalNs-before.PauseTotalNs)/uint64(b.N)) after.NumGC-before.NumGC, (after.PauseTotalNs-before.PauseTotalNs)/uint64(b.N))
AnalyzeBenchmarkRuns(fmt.Sprintf("Go-FUSE %d CPUs", threads), results)
} }
return results return nil
} }
// Add this so we can estimate impact on latency numbers. // Add this so we can estimate impact on latency numbers.
...@@ -220,7 +231,8 @@ func BenchmarkCFuseThreadedStat(b *testing.B) { ...@@ -220,7 +231,8 @@ func BenchmarkCFuseThreadedStat(b *testing.B) {
b.StopTimer() b.StopTimer()
wd, _ := os.Getwd() wd, _ := os.Getwd()
lines := ReadLines(wd + "/testpaths.txt") fileList := wd + "/testpaths.txt"
lines := ReadLines(fileList)
unique := map[string]int{} unique := map[string]int{}
for _, l := range lines { for _, l := range lines {
unique[l] = 1 unique[l] = 1
...@@ -253,8 +265,7 @@ func BenchmarkCFuseThreadedStat(b *testing.B) { ...@@ -253,8 +265,7 @@ func BenchmarkCFuseThreadedStat(b *testing.B) {
"entry_timeout=0.0,attr_timeout=0.0,ac_attr_timeout=0.0,negative_timeout=0.0", "entry_timeout=0.0,attr_timeout=0.0,ac_attr_timeout=0.0,negative_timeout=0.0",
mountPoint) mountPoint)
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(),
fmt.Sprintf("STATFS_INPUT=%s", f.Name()), fmt.Sprintf("STATFS_INPUT=%s", f.Name()))
fmt.Sprintf("STATFS_DELAY_USEC=%d", delay/time.Microsecond))
cmd.Start() cmd.Start()
bin, err := exec.LookPath("fusermount") bin, err := exec.LookPath("fusermount")
...@@ -267,13 +278,10 @@ func BenchmarkCFuseThreadedStat(b *testing.B) { ...@@ -267,13 +278,10 @@ func BenchmarkCFuseThreadedStat(b *testing.B) {
} }
defer stop.Run() defer stop.Run()
for i, l := range lines {
lines[i] = filepath.Join(mountPoint, l)
}
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
os.Lstat(mountPoint) os.Lstat(mountPoint)
threads := runtime.GOMAXPROCS(0) threads := runtime.GOMAXPROCS(0)
results := TestingBOnePass(b, threads, lines) if err := TestingBOnePass(b, threads, fileList, mountPoint); err != nil {
AnalyzeBenchmarkRuns(fmt.Sprintf("CFuse on %d CPUS", threads), results) log.Fatalf("TestingBOnePass %v", err)
}
} }
...@@ -3,68 +3,61 @@ package benchmark ...@@ -3,68 +3,61 @@ package benchmark
import ( import (
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/hanwen/go-fuse/fuse" "github.com/hanwen/go-fuse/fuse"
"github.com/hanwen/go-fuse/fuse/pathfs" "github.com/hanwen/go-fuse/fuse/pathfs"
) )
var delay = 0 * time.Microsecond type StatFS struct {
type StatFs struct {
pathfs.FileSystem pathfs.FileSystem
entries map[string]*fuse.Attr entries map[string]*fuse.Attr
dirs map[string][]fuse.DirEntry dirs map[string][]fuse.DirEntry
delay time.Duration
} }
func (me *StatFs) Add(name string, a *fuse.Attr) { func (fs *StatFS) Add(name string, a *fuse.Attr) {
name = strings.TrimRight(name, "/") name = strings.TrimRight(name, "/")
_, ok := me.entries[name] _, ok := fs.entries[name]
if ok { if ok {
return return
} }
me.entries[name] = a fs.entries[name] = a
if name == "/" || name == "" { if name == "/" || name == "" {
return return
} }
dir, base := filepath.Split(name) dir, base := filepath.Split(name)
dir = strings.TrimRight(dir, "/") dir = strings.TrimRight(dir, "/")
me.dirs[dir] = append(me.dirs[dir], fuse.DirEntry{Name: base, Mode: a.Mode}) fs.dirs[dir] = append(fs.dirs[dir], fuse.DirEntry{Name: base, Mode: a.Mode})
me.Add(dir, &fuse.Attr{Mode: fuse.S_IFDIR | 0755}) fs.Add(dir, &fuse.Attr{Mode: fuse.S_IFDIR | 0755})
} }
func (me *StatFs) AddFile(name string) { func (fs *StatFS) AddFile(name string) {
me.Add(name, &fuse.Attr{Mode: fuse.S_IFREG | 0644}) fs.Add(name, &fuse.Attr{Mode: fuse.S_IFREG | 0644})
} }
func (me *StatFs) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) { func (fs *StatFS) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
if d := me.dirs[name]; d != nil { if d := fs.dirs[name]; d != nil {
return &fuse.Attr{Mode: 0755 | fuse.S_IFDIR}, fuse.OK return &fuse.Attr{Mode: 0755 | fuse.S_IFDIR}, fuse.OK
} }
e := me.entries[name] e := fs.entries[name]
if e == nil { if e == nil {
return nil, fuse.ENOENT return nil, fuse.ENOENT
} }
if me.delay > 0 {
time.Sleep(me.delay)
}
return e, fuse.OK return e, fuse.OK
} }
func (me *StatFs) OpenDir(name string, context *fuse.Context) (stream []fuse.DirEntry, status fuse.Status) { func (fs *StatFS) OpenDir(name string, context *fuse.Context) (stream []fuse.DirEntry, status fuse.Status) {
entries := me.dirs[name] entries := fs.dirs[name]
if entries == nil { if entries == nil {
return nil, fuse.ENOENT return nil, fuse.ENOENT
} }
return entries, fuse.OK return entries, fuse.OK
} }
func NewStatFs() *StatFs { func NewStatFS() *StatFS {
return &StatFs{ return &StatFS{
FileSystem: pathfs.NewDefaultFileSystem(), FileSystem: pathfs.NewDefaultFileSystem(),
entries: make(map[string]*fuse.Attr), entries: make(map[string]*fuse.Attr),
dirs: make(map[string][]fuse.DirEntry), dirs: make(map[string][]fuse.DirEntry),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment