mirror of
https://github.com/syncthing/syncthing.git
synced 2026-06-02 13:31:11 -04:00
Fixes a regression introduced in #10439, mentioned in [a comment](https://github.com/syncthing/syncthing/pull/10439#issuecomment-3436515824) made by @imsodin: > That might not be the greatest way to do this, but nevertheless it afaik means that the benchmarks now do case checking once when it shouldn't happen at all, and twice otherwise. Benchmarks do approximately as well as before the regression, and I think most of them are random chance: ``` │ ../oldold.txt │ ../new.txt │ │ sec/op │ sec/op vs base │ WalkCaseFakeFS100k/rawfs-8 654.6m ± 1% 652.6m ± 3% ~ (p=0.971 n=10) WalkCaseFakeFS100k/casefs-8 1.049 ± 2% 1.071 ± 3% ~ (p=0.190 n=10) WalkCaseFakeFS100k/casefs-otherOpEvery1000-8 1.053 ± 3% 1.081 ± 5% ~ (p=0.165 n=10) geomean 897.7m 910.8m +1.46% │ ../oldold.txt │ ../new.txt │ │ B/entry │ B/entry vs base │ WalkCaseFakeFS100k/rawfs-8 1.274Ki ± 0% 1.274Ki ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-8 1.771Ki ± 0% 1.771Ki ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-otherOpEvery1000-8 1.772Ki ± 0% 1.772Ki ± 0% ~ (p=1.000 n=10) ¹ geomean 1.587Ki 1.587Ki +0.00% ¹ all samples are equal │ ../oldold.txt │ ../new.txt │ │ DirNames/entry │ DirNames/entry vs base │ WalkCaseFakeFS100k/rawfs-8 512.5m ± 0% 512.5m ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-8 1.025 ± 0% 1.025 ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-otherOpEvery1000-8 1.025 ± 0% 1.025 ± 0% ~ (p=1.000 n=10) ¹ geomean 813.5m 813.5m +0.00% ¹ all samples are equal │ ../oldold.txt │ ../new.txt │ │ DirNames/op │ DirNames/op vs base │ WalkCaseFakeFS100k/rawfs-8 51.25k ± 0% 51.25k ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-8 102.5k ± 0% 102.5k ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-otherOpEvery1000-8 102.5k ± 0% 102.5k ± 0% ~ (p=1.000 n=10) ¹ geomean 81.35k 81.35k +0.00% ¹ all samples are equal │ ../oldold.txt │ ../new.txt │ │ Lstat/entry │ Lstat/entry vs base │ WalkCaseFakeFS100k/rawfs-8 5.535 ± 0% 5.535 ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-8 5.535 ± 0% 5.535 ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-otherOpEvery1000-8 5.540 ± 0% 5.540 ± 0% ~ (p=1.000 n=10) ¹ geomean 5.537 5.537 +0.00% ¹ all samples are equal │ ../oldold.txt │ ../new.txt │ │ Lstat/op │ Lstat/op vs base │ WalkCaseFakeFS100k/rawfs-8 553.5k ± 0% 553.5k ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-8 553.5k ± 0% 553.5k ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-otherOpEvery1000-8 554.0k ± 0% 554.0k ± 0% ~ (p=1.000 n=10) ¹ geomean 553.7k 553.7k +0.00% ¹ all samples are equal │ ../oldold.txt │ ../new.txt │ │ allocs/entry │ allocs/entry vs base │ WalkCaseFakeFS100k/rawfs-8 19.00 ± 0% 19.00 ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-8 35.35 ± 0% 35.35 ± 0% ~ (p=1.000 n=10) ¹ WalkCaseFakeFS100k/casefs-otherOpEvery1000-8 35.38 ± 0% 35.38 ± 0% ~ (p=1.000 n=10) ¹ geomean 28.75 28.75 +0.00% ¹ all samples are equal │ ../oldold.txt │ ../new.txt │ │ sec/entry │ sec/entry vs base │ WalkCaseFakeFS100k/rawfs-8 4.328µ ± 1% 4.315µ ± 3% ~ (p=0.971 n=10) WalkCaseFakeFS100k/casefs-8 6.936µ ± 2% 7.082µ ± 3% ~ (p=0.171 n=10) WalkCaseFakeFS100k/casefs-otherOpEvery1000-8 6.965µ ± 3% 7.147µ ± 5% ~ (p=0.165 n=10) geomean 5.935µ 6.022µ +1.46% │ ../oldold.txt │ ../new.txt │ │ B/op │ B/op vs base │ WalkCaseFakeFS100k/rawfs-8 188.3Mi ± 0% 188.3Mi ± 0% -0.00% (p=0.006 n=10) WalkCaseFakeFS100k/casefs-8 261.5Mi ± 0% 261.5Mi ± 0% ~ (p=0.142 n=10) WalkCaseFakeFS100k/casefs-otherOpEvery1000-8 261.7Mi ± 0% 261.7Mi ± 0% ~ (p=0.315 n=10) geomean 234.4Mi 234.4Mi -0.00% │ ../oldold.txt │ ../new.txt │ │ allocs/op │ allocs/op vs base │ WalkCaseFakeFS100k/rawfs-8 2.873M ± 0% 2.873M ± 0% -0.00% (p=0.026 n=10) WalkCaseFakeFS100k/casefs-8 5.346M ± 0% 5.346M ± 0% ~ (p=0.136 n=10) WalkCaseFakeFS100k/casefs-otherOpEvery1000-8 5.351M ± 0% 5.351M ± 0% ~ (p=0.305 n=10) geomean 4.348M 4.348M -0.00% ``` Signed-off-by: Marcus B Spencer <marcus@marcusspencer.us>
347 lines
8.7 KiB
Go
347 lines
8.7 KiB
Go
// Copyright (C) 2020 The Syncthing Authors.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
package fs
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRealCase(t *testing.T) {
|
|
// Verify realCase lookups on various underlying filesystems.
|
|
|
|
t.Run("fake-sensitive", func(t *testing.T) {
|
|
testRealCase(t, newFakeFilesystem(t.Name()))
|
|
})
|
|
t.Run("fake-insensitive", func(t *testing.T) {
|
|
testRealCase(t, newFakeFilesystem(t.Name()+"?insens=true"))
|
|
})
|
|
t.Run("actual", func(t *testing.T) {
|
|
fsys, _ := setup(t)
|
|
testRealCase(t, fsys)
|
|
})
|
|
}
|
|
|
|
func newCaseFilesystem(fsys Filesystem) *caseFilesystem {
|
|
return globalCaseFilesystemRegistry.get(fsys).(*caseFilesystem)
|
|
}
|
|
|
|
func testRealCase(t *testing.T, fsys Filesystem) {
|
|
testFs := newCaseFilesystem(fsys)
|
|
comps := []string{"Foo", "bar", "BAZ", "bAs"}
|
|
path := filepath.Join(comps...)
|
|
testFs.MkdirAll(filepath.Join(comps[:len(comps)-1]...), 0o777)
|
|
fd, err := testFs.Create(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fd.Close()
|
|
|
|
for i, tc := range []struct {
|
|
in string
|
|
len int
|
|
}{
|
|
{path, 4},
|
|
{strings.ToLower(path), 4},
|
|
{strings.ToUpper(path), 4},
|
|
{"foo", 1},
|
|
{"FOO", 1},
|
|
{"foO", 1},
|
|
{filepath.Join("Foo", "bar"), 2},
|
|
{filepath.Join("Foo", "bAr"), 2},
|
|
{filepath.Join("FoO", "bar"), 2},
|
|
{filepath.Join("foo", "bar", "BAZ"), 3},
|
|
{filepath.Join("Foo", "bar", "bAz"), 3},
|
|
{filepath.Join("foo", "bar", "BAZ"), 3}, // Repeat on purpose
|
|
} {
|
|
out, err := testFs.realCase(tc.in)
|
|
if err != nil {
|
|
t.Error(err)
|
|
} else if exp := filepath.Join(comps[:tc.len]...); out != exp {
|
|
t.Errorf("tc %v: Expected %v, got %v", i, exp, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRealCaseSensitive(t *testing.T) {
|
|
// Verify that realCase returns the best on-disk case for case sensitive
|
|
// systems. Test is skipped if the underlying fs is insensitive.
|
|
|
|
t.Run("fake-sensitive", func(t *testing.T) {
|
|
testRealCaseSensitive(t, newFakeFilesystem(t.Name()))
|
|
})
|
|
t.Run("actual", func(t *testing.T) {
|
|
fsys, _ := setup(t)
|
|
testRealCaseSensitive(t, fsys)
|
|
})
|
|
}
|
|
|
|
func testRealCaseSensitive(t *testing.T, fsys Filesystem) {
|
|
testFs := newCaseFilesystem(fsys)
|
|
|
|
names := make([]string, 2)
|
|
names[0] = "foo"
|
|
names[1] = strings.ToUpper(names[0])
|
|
for _, n := range names {
|
|
if err := testFs.MkdirAll(n, 0o777); err != nil {
|
|
if IsErrCaseConflict(err) {
|
|
t.Skip("Filesystem is case-insensitive")
|
|
}
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
for _, n := range names {
|
|
if rn, err := testFs.realCase(n); err != nil {
|
|
t.Error(err)
|
|
} else if rn != n {
|
|
t.Errorf("Got %v, expected %v", rn, n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCaseFSStat(t *testing.T) {
|
|
// Verify that a Stat() lookup behaves in a case sensitive manner
|
|
// regardless of the underlying fs.
|
|
|
|
t.Run("fake-sensitive", func(t *testing.T) {
|
|
testCaseFSStat(t, newFakeFilesystem(t.Name()))
|
|
})
|
|
t.Run("fake-insensitive", func(t *testing.T) {
|
|
testCaseFSStat(t, newFakeFilesystem(t.Name()+"?insens=true"))
|
|
})
|
|
t.Run("actual", func(t *testing.T) {
|
|
fsys, _ := setup(t)
|
|
testCaseFSStat(t, fsys)
|
|
})
|
|
}
|
|
|
|
func testCaseFSStat(t *testing.T, fsys Filesystem) {
|
|
fd, err := fsys.Create("foo")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fd.Close()
|
|
|
|
// Check if the underlying fs is sensitive or not
|
|
sensitive := true
|
|
if _, err = fsys.Stat("FOO"); err == nil {
|
|
sensitive = false
|
|
}
|
|
|
|
testFs := newCaseFilesystem(fsys)
|
|
_, err = testFs.Stat("FOO")
|
|
if sensitive {
|
|
if IsNotExist(err) {
|
|
t.Log("pass: case sensitive underlying fs")
|
|
} else {
|
|
t.Error("expected NotExist, not", err, "for sensitive fs")
|
|
}
|
|
} else if IsErrCaseConflict(err) {
|
|
t.Log("pass: case insensitive underlying fs")
|
|
} else {
|
|
t.Error("expected ErrCaseConflict, not", err, "for insensitive fs")
|
|
}
|
|
}
|
|
|
|
func BenchmarkWalkCaseFakeFS100k(b *testing.B) {
|
|
const entries = 100_000
|
|
fsys, paths, err := fakefsForTest(entries, 0)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
b.Run("rawfs", func(b *testing.B) {
|
|
fakefs, ok := unwrapFilesystem[*fakeFS](fsys)
|
|
if !ok {
|
|
panic("expected unwrap to fakefs")
|
|
}
|
|
|
|
fakefs.resetCounters()
|
|
benchmarkWalkFakeFS(b, fsys, paths, 0, "")
|
|
fakefs.reportMetricsPerOp(b)
|
|
fakefs.reportMetricsPer(b, entries, "entry")
|
|
b.ReportAllocs()
|
|
})
|
|
b.Run("casefs", func(b *testing.B) {
|
|
// Construct the casefs manually or it will get cached and the benchmark is invalid.
|
|
casefs := &caseFilesystem{
|
|
Filesystem: fsys,
|
|
realCaser: &defaultRealCaser{
|
|
fs: fsys,
|
|
cache: newCaseCache(),
|
|
},
|
|
}
|
|
|
|
fakefs, ok := unwrapFilesystem[*fakeFS](fsys)
|
|
if !ok {
|
|
panic("expected unwrap to fakefs")
|
|
}
|
|
fakefs.resetCounters()
|
|
benchmarkWalkFakeFS(b, casefs, paths, 0, "")
|
|
fakefs.reportMetricsPerOp(b)
|
|
fakefs.reportMetricsPer(b, entries, "entry")
|
|
b.ReportAllocs()
|
|
})
|
|
var otherOpPath string
|
|
sep := string(PathSeparator)
|
|
longest := 0
|
|
for _, p := range paths {
|
|
if length := len(strings.Split(p, sep)); length > longest {
|
|
otherOpPath = p
|
|
longest = length
|
|
}
|
|
}
|
|
otherOpEvery := 1000
|
|
b.Run(fmt.Sprintf("casefs-otherOpEvery%v", otherOpEvery), func(b *testing.B) {
|
|
// Construct the casefs manually or it will get cached and the benchmark is invalid.
|
|
casefs := &caseFilesystem{
|
|
Filesystem: fsys,
|
|
realCaser: &defaultRealCaser{
|
|
fs: fsys,
|
|
cache: newCaseCache(),
|
|
},
|
|
}
|
|
|
|
fakefs, ok := unwrapFilesystem[*fakeFS](fsys)
|
|
if !ok {
|
|
panic("expected unwrap to fakefs")
|
|
}
|
|
|
|
fakefs.resetCounters()
|
|
benchmarkWalkFakeFS(b, casefs, paths, otherOpEvery, otherOpPath)
|
|
fakefs.reportMetricsPerOp(b)
|
|
fakefs.reportMetricsPer(b, entries, "entry")
|
|
b.ReportAllocs()
|
|
})
|
|
}
|
|
|
|
func benchmarkWalkFakeFS(b *testing.B, fsys Filesystem, paths []string, otherOpEvery int, otherOpPath string) {
|
|
// Simulate a scanner pass over the filesystem. First walk it to
|
|
// discover all names, then stat each name individually to check if it's
|
|
// been deleted or not (pretending that they all existed in the
|
|
// database).
|
|
|
|
var ms0 runtime.MemStats
|
|
runtime.ReadMemStats(&ms0)
|
|
t0 := time.Now()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
if err := doubleWalkFSWithOtherOps(fsys, paths, otherOpEvery, otherOpPath); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
|
|
t1 := time.Now()
|
|
var ms1 runtime.MemStats
|
|
runtime.ReadMemStats(&ms1)
|
|
|
|
// We add metrics per path entry
|
|
b.ReportMetric(float64(t1.Sub(t0))/float64(b.N)/float64(len(paths)), "ns/entry")
|
|
b.ReportMetric(float64(ms1.Mallocs-ms0.Mallocs)/float64(b.N)/float64(len(paths)), "allocs/entry")
|
|
b.ReportMetric(float64(ms1.TotalAlloc-ms0.TotalAlloc)/float64(b.N)/float64(len(paths)), "B/entry")
|
|
}
|
|
|
|
func TestStressCaseFS(t *testing.T) {
|
|
// Exercise a bunch of parallel operations for stressing out race
|
|
// conditions in the realnamer cache etc.
|
|
|
|
const limit = 10 * time.Second
|
|
if testing.Short() {
|
|
t.Skip("long test")
|
|
}
|
|
|
|
fsys, paths, err := fakefsForTest(10_000, 0, &OptionDetectCaseConflicts{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 0; i < runtime.NumCPU()/2+1; i++ {
|
|
t.Run(fmt.Sprintf("walker-%d", i), func(t *testing.T) {
|
|
// Walk the filesystem and stat everything
|
|
t.Parallel()
|
|
t0 := time.Now()
|
|
for time.Since(t0) < limit {
|
|
if err := doubleWalkFS(fsys, paths); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
t.Run(fmt.Sprintf("toucher-%d", i), func(t *testing.T) {
|
|
// Touch all the things
|
|
t.Parallel()
|
|
t0 := time.Now()
|
|
for time.Since(t0) < limit {
|
|
for _, p := range paths {
|
|
now := time.Now()
|
|
if err := fsys.Chtimes(p, now, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func doubleWalkFS(fsys Filesystem, paths []string) error {
|
|
return doubleWalkFSWithOtherOps(fsys, paths, 0, "")
|
|
}
|
|
|
|
func doubleWalkFSWithOtherOps(fsys Filesystem, paths []string, otherOpEvery int, otherOpPath string) error {
|
|
i := 0
|
|
if err := fsys.Walk("/", func(path string, info FileInfo, err error) error {
|
|
i++
|
|
if otherOpEvery != 0 && i%otherOpEvery == 0 {
|
|
if _, err := fsys.Lstat(otherOpPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return err
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, p := range paths {
|
|
for p != "." {
|
|
i++
|
|
if otherOpEvery != 0 && i%otherOpEvery == 0 {
|
|
if _, err := fsys.Lstat(otherOpPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err := fsys.Lstat(p); err != nil {
|
|
return err
|
|
}
|
|
p = filepath.Dir(p)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fakefsForTest(nfiles int, latency time.Duration, opts ...Option) (Filesystem, []string, error) {
|
|
fsys := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("fakefsForBenchmark?files=%d&insens=true&latency=%s", nfiles, latency), opts...)
|
|
|
|
var paths []string
|
|
if err := fsys.Walk("/", func(path string, info FileInfo, err error) error {
|
|
paths = append(paths, path)
|
|
return err
|
|
}); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if len(paths) < nfiles {
|
|
return nil, nil, errors.New("didn't find enough stuff")
|
|
}
|
|
|
|
slices.Sort(paths)
|
|
|
|
return fsys, paths, nil
|
|
}
|