From 7bc4589d4df385c4fe6981a31b2921b6b270bdee Mon Sep 17 00:00:00 2001
From: Jakob Borg <jakob@nym.se>
Date: Sat, 27 Sep 2014 20:09:36 +0200
Subject: [PATCH] Simple reproducible syncing benchmark

---
 test/common_test.go         | 66 ++++++++++++++++++++++-----
 test/genfiles.go            | 28 +++++++++---
 test/h1/config.xml          |  4 +-
 test/transfer-bench_test.go | 90 +++++++++++++++++++++++++++++++++++++
 4 files changed, 168 insertions(+), 20 deletions(-)
 create mode 100644 test/transfer-bench_test.go

diff --git a/test/common_test.go b/test/common_test.go
index 75cc8aca9..0132fa276 100644
--- a/test/common_test.go
+++ b/test/common_test.go
@@ -8,14 +8,12 @@ package integration_test
 
 import (
 	"crypto/md5"
-	"crypto/rand"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"log"
-	mr "math/rand"
+	"math/rand"
 	"net/http"
 	"os"
 	"os/exec"
@@ -23,6 +21,10 @@ import (
 	"time"
 )
 
+func init() {
+	rand.Seed(42)
+}
+
 const (
 	id1    = "I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"
 	id2    = "JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"
@@ -34,6 +36,7 @@ var env = []string{
 	"STTRACE=model",
 	"STGUIAPIKEY=" + apiKey,
 	"STNORESTART=1",
+	"STPERFSTATS=1",
 }
 
 type syncthingProcess struct {
@@ -42,6 +45,7 @@ type syncthingProcess struct {
 	port      int
 	apiKey    string
 	csrfToken string
+	lastEvent int
 
 	cmd   *exec.Cmd
 	logfd *os.File
@@ -114,17 +118,46 @@ func (p *syncthingProcess) peerCompletion() (map[string]int, error) {
 	return comp, err
 }
 
+type event struct {
+	ID   int
+	Time time.Time
+	Type string
+	Data interface{}
+}
+
+func (p *syncthingProcess) events() ([]event, error) {
+	resp, err := p.get(fmt.Sprintf("/rest/events?since=%d", p.lastEvent))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	var evs []event
+	err = json.NewDecoder(resp.Body).Decode(&evs)
+	if err != nil {
+		return nil, err
+	}
+	p.lastEvent = evs[len(evs)-1].ID
+	return evs, err
+}
+
+type versionResp struct {
+	Version string
+}
+
 func (p *syncthingProcess) version() (string, error) {
 	resp, err := p.get("/rest/version")
 	if err != nil {
 		return "", err
 	}
-	bs, err := ioutil.ReadAll(resp.Body)
-	resp.Body.Close()
+	defer resp.Body.Close()
+
+	var v versionResp
+	err = json.NewDecoder(resp.Body).Decode(&v)
 	if err != nil {
 		return "", err
 	}
-	return string(bs), nil
+	return v.Version, nil
 }
 
 type fileGenerator struct {
@@ -147,12 +180,12 @@ func generateFiles(dir string, files, maxexp int, srcname string) error {
 			log.Fatal(err)
 		}
 
-		s := 1 << uint(mr.Intn(maxexp))
+		s := 1 << uint(rand.Intn(maxexp))
 		a := 128 * 1024
 		if a > s {
 			a = s
 		}
-		s += mr.Intn(a)
+		s += rand.Intn(a)
 
 		src := io.LimitReader(&inifiteReader{fd}, int64(s))
 
@@ -172,12 +205,12 @@ func generateFiles(dir string, files, maxexp int, srcname string) error {
 			return err
 		}
 
-		err = os.Chmod(p1, os.FileMode(mr.Intn(0777)|0400))
+		err = os.Chmod(p1, os.FileMode(rand.Intn(0777)|0400))
 		if err != nil {
 			return err
 		}
 
-		t := time.Now().Add(-time.Duration(mr.Intn(30*86400)) * time.Second)
+		t := time.Now().Add(-time.Duration(rand.Intn(30*86400)) * time.Second)
 		err = os.Chtimes(p1, t, t)
 		if err != nil {
 			return err
@@ -187,9 +220,20 @@ func generateFiles(dir string, files, maxexp int, srcname string) error {
 	return nil
 }
 
+func ReadRand(bs []byte) (int, error) {
+	var r uint32
+	for i := range bs {
+		if i%4 == 0 {
+			r = uint32(rand.Int63())
+		}
+		bs[i] = byte(r >> uint((i%4)*8))
+	}
+	return len(bs), nil
+}
+
 func randomName() string {
 	var b [16]byte
-	rand.Reader.Read(b[:])
+	ReadRand(b[:])
 	return fmt.Sprintf("%x", b[:])
 }
 
diff --git a/test/genfiles.go b/test/genfiles.go
index b1b3fa47b..e30d48d9d 100644
--- a/test/genfiles.go
+++ b/test/genfiles.go
@@ -7,20 +7,34 @@
 package main
 
 import (
-	"crypto/rand"
 	"flag"
 	"fmt"
 	"io"
 	"log"
-	mr "math/rand"
+	"math/rand"
 	"os"
 	"path/filepath"
 	"time"
 )
 
+func init() {
+	rand.Seed(42)
+}
+
+func ReadRand(bs []byte) (int, error) {
+	var r uint32
+	for i := range bs {
+		if i%4 == 0 {
+			r = uint32(rand.Int63())
+		}
+		bs[i] = byte(r >> uint((i%4)*8))
+	}
+	return len(bs), nil
+}
+
 func name() string {
 	var b [16]byte
-	rand.Reader.Read(b[:])
+	ReadRand(b[:])
 	return fmt.Sprintf("%x", b[:])
 }
 
@@ -47,12 +61,12 @@ func main() {
 			log.Fatal(err)
 		}
 
-		s := 1 << uint(mr.Intn(maxexp))
+		s := 1 << uint(rand.Intn(maxexp))
 		a := 128 * 1024
 		if a > s {
 			a = s
 		}
-		s += mr.Intn(a)
+		s += rand.Intn(a)
 
 		src := io.LimitReader(&inifiteReader{fd}, int64(s))
 
@@ -72,12 +86,12 @@ func main() {
 			log.Fatal(err)
 		}
 
-		err = os.Chmod(p1, os.FileMode(mr.Intn(0777)|0400))
+		err = os.Chmod(p1, os.FileMode(rand.Intn(0777)|0400))
 		if err != nil {
 			log.Fatal(err)
 		}
 
-		t := time.Now().Add(-time.Duration(mr.Intn(30*86400)) * time.Second)
+		t := time.Now().Add(-time.Duration(rand.Intn(30*86400)) * time.Second)
 		err = os.Chtimes(p1, t, t)
 		if err != nil {
 			log.Fatal(err)
diff --git a/test/h1/config.xml b/test/h1/config.xml
index 96bce685f..bd06639cb 100644
--- a/test/h1/config.xml
+++ b/test/h1/config.xml
@@ -36,8 +36,8 @@
         <localAnnouncePort>21025</localAnnouncePort>
         <localAnnounceMCAddr>[ff32::5222]:21026</localAnnounceMCAddr>
         <parallelRequests>16</parallelRequests>
-        <maxSendKbps>0</maxSendKbps>
-        <maxRecvKbps>0</maxRecvKbps>
+        <maxSendKbps>50000</maxSendKbps>
+        <maxRecvKbps>50000</maxRecvKbps>
         <reconnectionIntervalS>5</reconnectionIntervalS>
         <startBrowser>false</startBrowser>
         <upnpEnabled>true</upnpEnabled>
diff --git a/test/transfer-bench_test.go b/test/transfer-bench_test.go
new file mode 100644
index 000000000..fbee825cd
--- /dev/null
+++ b/test/transfer-bench_test.go
@@ -0,0 +1,90 @@
+// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
+// All rights reserved. Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file.
+
+// +build integration
+
+package integration_test
+
+import (
+	"log"
+	"strings"
+	"testing"
+	"time"
+)
+
+func TestBenchmarkTransfer(t *testing.T) {
+	log.Println("Cleaning...")
+	err := removeAll("s1", "s2", "h1/index", "h2/index")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	log.Println("Generating files...")
+	err = generateFiles("s1", 10000, 22, "../bin/syncthing")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	log.Println("Starting up...")
+	sender := syncthingProcess{ // id1
+		log:    "1.out",
+		argv:   []string{"-home", "h1"},
+		port:   8081,
+		apiKey: apiKey,
+	}
+	ver, err := sender.start()
+	if err != nil {
+		t.Fatal(err)
+	}
+	log.Println(ver)
+
+	receiver := syncthingProcess{ // id2
+		log:    "2.out",
+		argv:   []string{"-home", "h2"},
+		port:   8082,
+		apiKey: apiKey,
+	}
+	ver, err = receiver.start()
+	if err != nil {
+		sender.stop()
+		t.Fatal(err)
+	}
+	log.Println(ver)
+
+	var t0 time.Time
+loop:
+	for {
+		evs, err := receiver.events()
+		if err != nil {
+			if strings.Contains(err.Error(), "use of closed network connection") {
+				log.Println("...")
+				continue
+			}
+			sender.stop()
+			receiver.stop()
+			t.Fatal(err)
+		}
+
+		for _, ev := range evs {
+			if ev.Type == "StateChanged" {
+				data := ev.Data.(map[string]interface{})
+				if data["repo"].(string) != "default" {
+					continue
+				}
+				log.Println(ev)
+				if data["to"].(string) == "syncing" {
+					t0 = ev.Time
+					continue
+				}
+				if t0 != (time.Time{}) && data["to"].(string) == "idle" {
+					log.Println("Sync took", ev.Time.Sub(t0))
+					break loop
+				}
+			}
+		}
+	}
+
+	sender.stop()
+	receiver.stop()
+}