Commit 0a349536 authored by Han-Wen Nienhuys's avatar Han-Wen Nienhuys

Split off benchmarking stuff into benchmark/ .

Add a gotest benchmark running off a synthetic stat-only filesystem.
parent a964aa5c
......@@ -3,7 +3,7 @@ set -eux
rm -f fuse/version.gen.go
for d in fuse zipfs unionfs \
for d in fuse benchmark zipfs unionfs \
example/hello example/loopback example/zipfs \
example/bulkstat example/multizip example/unionfs \
example/autounionfs ; \
......@@ -15,3 +15,4 @@ for d in fuse zipfs unionfs
do
(cd $d && gotest )
done
# Use "gomake install" to build and install this package.
include $(GOROOT)/src/Make.inc
TARG=github.com/hanwen/go-fuse/benchmark
GOFILES=benchmark.go
include $(GOROOT)/src/Make.pkg
package fuse
// Routines for benchmarking fuse.
import (
"fmt"
"math"
"os"
"regexp"
"sort"
"time"
)
// Used for benchmarking. Returns milliseconds.
func BulkStat(parallelism int, files []string) float64 {
todo := make(chan string, len(files))
dts := make(chan int64, parallelism)
allStart := time.Nanoseconds()
fmt.Printf("Statting %d files with %d threads\n", len(files), parallelism)
for i := 0; i < parallelism; i++ {
go func() {
for {
fn := <-todo
if fn == "" {
break
}
t := time.Nanoseconds()
os.Lstat(fn)
dts <- time.Nanoseconds() - t
}
}()
}
for _, v := range files {
todo <- v
}
total := 0.0
for i := 0; i < len(files); i++ {
total += float64(<-dts) * 1e-6
}
allEnd := time.Nanoseconds()
avg := total / float64(len(files))
fmt.Printf("Elapsed: %f sec. Average stat %f ms\n",
float64(allEnd-allStart)*1e-9, avg)
return avg
}
func AnalyzeBenchmarkRuns(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)]
fmt.Printf(
"%d samples\n"+
"avg %.2f ms 2sigma %.2f "+
"median %.2fms\n"+
"10%%tile %.2fms, 90%%tile %.2fms\n",
len(times), avg, 2*stddev, median, perc10, perc90)
}
func RunBulkStat(runs int, threads int, sleepTime float64, files []string) (results []float64) {
runs++
for j := 0; j < runs; j++ {
result := BulkStat(threads, files)
if j > 0 {
results = append(results, result)
} else {
fmt.Println("Ignoring first run to preheat caches.")
}
if j < runs-1 {
fmt.Printf("Sleeping %.2f seconds\n", sleepTime)
time.Sleep(int64(sleepTime * 1e9))
}
}
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 fuse
import (
"bufio"
"fmt"
"io/ioutil"
"log"
"os"
"github.com/hanwen/go-fuse/fuse"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
var CheckSuccess = fuse.CheckSuccess
type StatFs struct {
fuse.DefaultFileSystem
entries map[string]*os.FileInfo
dirs map[string][]fuse.DirEntry
}
func (me *StatFs) add(name string, fi os.FileInfo) {
name = strings.TrimRight(name, "/")
_, ok := me.entries[name]
if ok {
return
}
me.entries[name] = &fi
if name == "/" || name == "" {
return
}
dir, base := filepath.Split(name)
dir = strings.TrimRight(dir, "/")
me.dirs[dir] = append(me.dirs[dir], fuse.DirEntry{Name: base, Mode: fi.Mode})
me.add(dir, os.FileInfo{Mode: fuse.S_IFDIR | 0755})
}
func (me *StatFs) GetAttr(name string) (*os.FileInfo, fuse.Status) {
e := me.entries[name]
if e == nil {
return nil, fuse.ENOENT
}
return e, fuse.OK
}
func (me *StatFs) OpenDir(name string) (stream chan fuse.DirEntry, status fuse.Status) {
log.Printf("OPENDIR '%v', %v %v", name, me.entries, me.dirs)
entries := me.dirs[name]
if entries == nil {
return nil, fuse.ENOENT
}
stream = make(chan fuse.DirEntry, len(entries))
for _, e := range entries {
stream <- e
}
close(stream)
return stream, fuse.OK
}
func NewStatFs() *StatFs {
return &StatFs{
entries: make(map[string]*os.FileInfo),
dirs: make(map[string][]fuse.DirEntry),
}
}
func setupFs(fs fuse.FileSystem, opts *fuse.FileSystemOptions) (string, func()) {
mountPoint := fuse.MakeTempDir()
state, _, err := fuse.MountFileSystem(mountPoint, fs, opts)
if err != nil {
panic(fmt.Sprintf("cannot mount %v", err)) // ugh - benchmark has no error methods.
}
// state.Debug = true
go state.Loop(false)
return mountPoint, func() {
err := state.Unmount()
if err != nil {
log.Println("error during unmount", err)
} else {
os.RemoveAll(mountPoint)
}
}
}
func TestNewStatFs(t *testing.T) {
fs := NewStatFs()
for _, n := range []string{
"file.txt", "sub/dir/foo.txt",
"sub/dir/bar.txt", "sub/marine.txt"} {
fs.add(n, os.FileInfo{Mode: fuse.S_IFREG | 0644})
}
wd, clean := setupFs(fs, nil)
defer clean()
names, err := ioutil.ReadDir(wd)
CheckSuccess(err)
if len(names) != 2 {
t.Error("readdir /", names)
}
fi, err := os.Lstat(wd + "/sub")
CheckSuccess(err)
if !fi.IsDirectory() {
t.Error("mode", fi)
}
names, err = ioutil.ReadDir(wd + "/sub")
CheckSuccess(err)
if len(names) != 2 {
t.Error("readdir /sub", names)
}
names, err = ioutil.ReadDir(wd + "/sub/dir")
CheckSuccess(err)
if len(names) != 2 {
t.Error("readdir /sub/dir", names)
}
fi, err = os.Lstat(wd + "/sub/marine.txt")
CheckSuccess(err)
if !fi.IsRegular() {
t.Error("mode", fi)
}
}
func BenchmarkThreadedStat(b *testing.B) {
b.StopTimer()
fs := NewStatFs()
wd, _ := os.Getwd()
// Names from OpenJDK 1.6
f, err := os.Open(wd + "/testpaths.txt")
CheckSuccess(err)
defer f.Close()
r := bufio.NewReader(f)
files := []string{}
for {
line, _, err := r.ReadLine()
if line == nil || err != nil {
break
}
fn := string(line)
files = append(files, fn)
fs.add(fn, os.FileInfo{Mode: fuse.S_IFREG | 0644})
}
log.Printf("Read %d file names", len(files))
if len(files) == 0 {
log.Fatal("no files added")
}
ttl := 0.1
opts := fuse.FileSystemOptions{
EntryTimeout: ttl,
AttrTimeout: ttl,
NegativeTimeout: 0.0,
}
wd, clean := setupFs(fs, &opts)
defer clean()
for i, l := range files {
files[i] = filepath.Join(wd, l)
}
log.Println("N = ", b.N)
threads := runtime.GOMAXPROCS(0)
results := TestingBOnePass(b, threads, ttl * 1.2, files)
AnalyzeBenchmarkRuns(results)
}
func TestingBOnePass(b *testing.B, threads int, sleepTime float64, files []string) (results []float64) {
runs := b.N + 1
for j := 0; j < runs; j++ {
if j > 0 {
b.StartTimer()
}
result := BulkStat(threads, files)
if j > 0 {
b.StopTimer()
results = append(results, result)
} else {
fmt.Println("Ignoring first run to preheat caches.")
}
if j < runs-1 {
fmt.Printf("Sleeping %.2f seconds\n", sleepTime)
time.Sleep(int64(sleepTime * 1e9))
}
}
return results
}
This diff is collapsed.
......@@ -3,7 +3,7 @@ include $(GOROOT)/src/Make.inc
TARG=bulkstat
GOFILES=bulkstat.go
DEPS=../../fuse
DEPS=../../benchmark
include $(GOROOT)/src/Make.cmd
......@@ -6,13 +6,9 @@ package main
import (
"bufio"
"flag"
"fmt"
"github.com/hanwen/go-fuse/fuse"
"math"
"github.com/hanwen/go-fuse/benchmark"
"os"
"runtime"
"sort"
"time"
)
func main() {
......@@ -42,94 +38,6 @@ func main() {
files = append(files, string(l))
}
totalRuns := *runs + 1
results := make([]float64, 0)
for j := 0; j < totalRuns; j++ {
result := BulkStat(*threads, files)
if j > 0 {
results = append(results, result)
} else {
fmt.Println("Ignoring first run to preheat caches.")
}
if j < totalRuns-1 {
fmt.Printf("Sleeping %.2f seconds\n", *sleepTime)
time.Sleep(int64(*sleepTime * 1e9))
}
}
Analyze(results)
}
func Analyze(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)]
fmt.Printf(
"%d samples\n"+
"avg %.2f ms 2sigma %.2f "+
"median %.2fms\n"+
"10%%tile %.2fms, 90%%tile %.2fms\n",
len(times), avg, 2*stddev, median, perc10, perc90)
}
// Returns milliseconds.
func BulkStat(parallelism int, files []string) float64 {
todo := make(chan string, len(files))
dts := make(chan int64, parallelism)
allStart := time.Nanoseconds()
fmt.Printf("Statting %d files with %d threads\n", len(files), parallelism)
for i := 0; i < parallelism; i++ {
go func() {
for {
fn := <-todo
if fn == "" {
break
}
t := time.Nanoseconds()
os.Lstat(fn)
dts <- time.Nanoseconds() - t
}
}()
}
for _, v := range files {
todo <- v
}
total := 0.0
for i := 0; i < len(files); i++ {
total += float64(<-dts) * 1e-6
}
allEnd := time.Nanoseconds()
avg := total / float64(len(files))
fmt.Printf("Elapsed: %f sec. Average stat %f ms\n",
float64(allEnd-allStart)*1e-9, avg)
return avg
results := fuse.RunBulkStat(*runs, *threads, *sleepTime, files)
fuse.AnalyzeBenchmarkRuns(results)
}
......@@ -8,7 +8,6 @@ import (
"log"
"math"
"reflect"
"regexp"
"strings"
"syscall"
"unsafe"
......@@ -116,20 +115,6 @@ func Writev(fd int, packet [][]byte) (n int, err os.Error) {
return n, err
}
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))
}
// Creates a return entry for a non-existent path.
func NegativeEntry(time float64) *EntryOut {
out := new(EntryOut)
......@@ -189,3 +174,4 @@ func CurrentOwner() *Owner {
Gid: uint32(os.Getgid()),
}
}
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