github.com/dshulyak/uring@v0.0.0-20210209113719-1b2ec51f1542/fs/file_test.go (about)

     1  package fs
     2  
     3  import (
     4  	"crypto/rand"
     5  	"encoding/binary"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"runtime"
    10  	"sync"
    11  	"sync/atomic"
    12  	"syscall"
    13  	"testing"
    14  	"time"
    15  	"unsafe"
    16  
    17  	"github.com/dshulyak/uring"
    18  	"github.com/dshulyak/uring/fixed"
    19  	"github.com/dshulyak/uring/loop"
    20  	"github.com/stretchr/testify/require"
    21  	"golang.org/x/sys/unix"
    22  )
    23  
    24  func aligned(bsize int) []byte {
    25  	if bsize == 0 {
    26  		return nil
    27  	}
    28  	if bsize%512 != 0 {
    29  		panic("block size must be multiple of 512")
    30  	}
    31  	block := make([]byte, bsize+512)
    32  	a := uintptr(unsafe.Pointer(&block[0])) & uintptr(511)
    33  	var offset uintptr
    34  	if a != 0 {
    35  		offset = 512 - a
    36  	}
    37  	return block[offset : int(offset)+bsize]
    38  }
    39  
    40  func TestFixedBuffersIO(t *testing.T) {
    41  	tester := func(t *testing.T, fsm *Filesystem, pool *fixed.Pool) {
    42  		t.Helper()
    43  		f, err := TempFile(fsm, "testing-io-", 0)
    44  		require.NoError(t, err)
    45  		t.Cleanup(func() { os.Remove(f.Name()) })
    46  
    47  		in, out := pool.Get(), pool.Get()
    48  		rand.Read(out.B)
    49  
    50  		_, err = f.WriteAtFixed(out, 0)
    51  		require.NoError(t, err)
    52  		require.NoError(t, f.Datasync())
    53  		_, err = f.ReadAtFixed(in, 0)
    54  		require.NoError(t, err)
    55  		require.Equal(t, in.B, out.B)
    56  	}
    57  	t.Run("registered", func(t *testing.T) {
    58  		q, err := loop.Setup(32, nil, nil)
    59  		require.NoError(t, err)
    60  		t.Cleanup(func() { q.Close() })
    61  		fsm := NewFilesystem(q, RegisterFiles(32))
    62  		pool, err := fixed.New(q, 100, 2)
    63  		require.NoError(t, err)
    64  		t.Cleanup(func() { pool.Close() })
    65  		tester(t, fsm, pool)
    66  	})
    67  	t.Run("unregistered", func(t *testing.T) {
    68  		q, err := loop.Setup(32, nil, nil)
    69  		require.NoError(t, err)
    70  		t.Cleanup(func() { q.Close() })
    71  		fsm := NewFilesystem(q)
    72  		pool, err := fixed.New(q, 100, 2)
    73  		require.NoError(t, err)
    74  		t.Cleanup(func() { pool.Close() })
    75  		tester(t, fsm, pool)
    76  	})
    77  }
    78  
    79  func TestRegularIO(t *testing.T) {
    80  	tester := func(t *testing.T, fsm *Filesystem) {
    81  		t.Helper()
    82  		f, err := TempFile(fsm, "testing-io-", 0)
    83  		require.NoError(t, err)
    84  		t.Cleanup(func() { os.Remove(f.Name()) })
    85  
    86  		in, out := make([]byte, 100), make([]byte, 100)
    87  		rand.Read(out)
    88  
    89  		_, err = f.WriteAt(out, 0)
    90  		require.NoError(t, err)
    91  		require.NoError(t, f.Sync())
    92  		_, err = f.ReadAt(in, 0)
    93  		require.NoError(t, err)
    94  		require.Equal(t, in, out)
    95  	}
    96  	t.Run("registered", func(t *testing.T) {
    97  		q, err := loop.Setup(32, nil, nil)
    98  		require.NoError(t, err)
    99  		t.Cleanup(func() { q.Close() })
   100  		fsm := NewFilesystem(q, RegisterFiles(32))
   101  		tester(t, fsm)
   102  	})
   103  	t.Run("unregistered", func(t *testing.T) {
   104  		q, err := loop.Setup(32, nil, nil)
   105  		require.NoError(t, err)
   106  		t.Cleanup(func() { q.Close() })
   107  		fsm := NewFilesystem(q)
   108  		tester(t, fsm)
   109  	})
   110  }
   111  
   112  func benchmarkOSWriteAt(b *testing.B, size int64, fflags int) {
   113  	f, err := ioutil.TempFile("", "testing-write-os-")
   114  	require.NoError(b, err)
   115  	require.NoError(b, f.Close())
   116  	f, err = os.OpenFile(f.Name(), os.O_RDWR|fflags, 0644)
   117  	require.NoError(b, err)
   118  	b.Cleanup(func() {
   119  		os.Remove(f.Name())
   120  	})
   121  
   122  	buf := make([]byte, size)
   123  	offset := int64(0)
   124  
   125  	b.SetBytes(size)
   126  	b.ReportAllocs()
   127  	b.ResetTimer()
   128  
   129  	b.RunParallel(func(pb *testing.PB) {
   130  		for pb.Next() {
   131  			_, err := f.WriteAt(buf, atomic.AddInt64(&offset, size)-size)
   132  			if err != nil {
   133  				b.Error(err)
   134  			}
   135  		}
   136  	})
   137  }
   138  
   139  func benchmarkOSReadAt(b *testing.B, size int64) {
   140  	f, err := ioutil.TempFile("", "testing-write-os-")
   141  	require.NoError(b, err)
   142  	require.NoError(b, f.Close())
   143  	b.Cleanup(func() {
   144  		os.Remove(f.Name())
   145  	})
   146  	f, err = os.OpenFile(f.Name(), os.O_RDWR|unix.O_DIRECT, 0644)
   147  	require.NoError(b, err)
   148  
   149  	buf := aligned(int(size))
   150  	offset := int64(0)
   151  
   152  	wbuf := aligned(int(size) * (b.N + 1))
   153  	_, err = f.WriteAt(wbuf, 0)
   154  	require.NoError(b, err)
   155  
   156  	b.SetBytes(size)
   157  	b.ReportAllocs()
   158  	b.ResetTimer()
   159  
   160  	b.RunParallel(func(pb *testing.PB) {
   161  		for pb.Next() {
   162  			_, err := f.ReadAt(buf, atomic.AddInt64(&offset, size)-size)
   163  			if err != nil {
   164  				b.Error(err)
   165  			}
   166  		}
   167  	})
   168  }
   169  
   170  func BenchmarkWriteAt(b *testing.B) {
   171  	for _, size := range []int64{512, 4 << 10, 8 << 10, 16 << 10, 64 << 10, 256 << 10} {
   172  		b.Run(fmt.Sprintf("uring %d", size), func(b *testing.B) {
   173  			q, err := loop.Setup(
   174  				512,
   175  				&uring.IOUringParams{
   176  					CQEntries: 8 * 4096,
   177  					Flags:     uring.IORING_SETUP_CQSIZE,
   178  				},
   179  				nil,
   180  			)
   181  			require.NoError(b, err)
   182  			benchmarkWriteAt(b, q, size, 0)
   183  		})
   184  		b.Run(fmt.Sprintf("os %d", size), func(b *testing.B) {
   185  			benchmarkOSWriteAt(b, size, 0)
   186  		})
   187  	}
   188  }
   189  
   190  func BenchmarkReadAt(b *testing.B) {
   191  	for _, size := range []int64{512, 4 << 10, 8 << 10, 16 << 10, 64 << 10, 256 << 10} {
   192  		b.Run(fmt.Sprintf("uring %d", size), func(b *testing.B) {
   193  			q, err := loop.Setup(
   194  				4096,
   195  				&uring.IOUringParams{
   196  					CQEntries: 2 * 4096,
   197  					Flags:     uring.IORING_SETUP_CQSIZE,
   198  				},
   199  				nil,
   200  			)
   201  			require.NoError(b, err)
   202  			benchmarkReadAt(b, q, size)
   203  		})
   204  
   205  		b.Run(fmt.Sprintf("enter %d", size), func(b *testing.B) {
   206  			q, err := loop.Setup(
   207  				4096,
   208  				&uring.IOUringParams{
   209  					CQEntries: 2 * 4096,
   210  					Flags:     uring.IORING_SETUP_CQSIZE,
   211  				}, &loop.Params{
   212  					Rings:           runtime.NumCPU(),
   213  					WaitMethod:      loop.WaitEnter,
   214  					Flags:           loop.FlagSharedWorkers | loop.FlagBatchSubmission,
   215  					SubmissionTimer: 50 * time.Microsecond,
   216  				},
   217  			)
   218  			require.NoError(b, err)
   219  			benchmarkReadAt(b, q, size)
   220  		})
   221  		b.Run(fmt.Sprintf("os %d", size), func(b *testing.B) {
   222  			benchmarkOSReadAt(b, size)
   223  		})
   224  	}
   225  }
   226  
   227  func benchmarkWriteAt(b *testing.B, q *loop.Loop, size int64, fflags int) {
   228  	b.Cleanup(func() { q.Close() })
   229  	fsm := NewFilesystem(q)
   230  
   231  	f, err := TempFile(fsm, "testing-fs-file-", fflags)
   232  	require.NoError(b, err)
   233  	b.Cleanup(func() { os.Remove(f.Name()) })
   234  
   235  	offset := int64(0)
   236  	buf := make([]byte, size)
   237  
   238  	b.SetBytes(int64(size))
   239  	b.ReportAllocs()
   240  	b.ResetTimer()
   241  
   242  	runConcurrently(20_000, b.N, func() {
   243  		n, err := f.WriteAt(buf, atomic.AddInt64(&offset, size)-size)
   244  		if err != nil {
   245  			b.Error(err)
   246  		}
   247  		if n != len(buf) {
   248  			b.Errorf("short write %d < %d", n, len(buf))
   249  		}
   250  	})
   251  }
   252  
   253  func runConcurrently(c, n int, f func()) {
   254  	var wg sync.WaitGroup
   255  	quo, rem := n/c, n%c
   256  	for i := 0; i < c; i++ {
   257  		wg.Add(1)
   258  		n := quo
   259  		if i < rem {
   260  			n = quo + 1
   261  		}
   262  		go func() {
   263  			defer wg.Done()
   264  			for j := 0; j < n; j++ {
   265  				f()
   266  			}
   267  		}()
   268  	}
   269  	wg.Wait()
   270  }
   271  
   272  func benchmarkReadAt(b *testing.B, q *loop.Loop, size int64) {
   273  	b.Cleanup(func() {
   274  		q.Close()
   275  	})
   276  
   277  	fsm := NewFilesystem(q)
   278  
   279  	f, err := TempFile(fsm, "testing-fs-file-", unix.O_DIRECT)
   280  	require.NoError(b, err)
   281  	b.Cleanup(func() {
   282  		f.Close()
   283  		os.Remove(f.Name())
   284  	})
   285  
   286  	total := 0
   287  	wbuf := aligned(int(size) * (b.N + 1))
   288  	for total != len(wbuf) {
   289  		n, err := f.WriteAt(wbuf[total:], int64(total))
   290  		require.NoError(b, err)
   291  		total += n
   292  	}
   293  
   294  	offset := int64(0)
   295  	buf := aligned(int(size))
   296  
   297  	b.SetBytes(int64(size))
   298  	b.ReportAllocs()
   299  	b.ResetTimer()
   300  
   301  	b.SetBytes(int64(size))
   302  	b.ReportAllocs()
   303  	b.ResetTimer()
   304  	runConcurrently(100_000, b.N, func() {
   305  		off := atomic.AddInt64(&offset, size) - size
   306  		for {
   307  			rn, err := f.ReadAt(buf, off)
   308  			if err != nil {
   309  				b.Error(err)
   310  			}
   311  			if rn == len(buf) {
   312  				break
   313  			}
   314  		}
   315  	})
   316  }
   317  
   318  func TestEmptyWrite(t *testing.T) {
   319  	queue, err := loop.Setup(1024, nil, nil)
   320  	require.NoError(t, err)
   321  	t.Cleanup(func() { queue.Close() })
   322  
   323  	fsm := NewFilesystem(queue)
   324  
   325  	f, err := TempFile(fsm, "testing-fs-file-", 0)
   326  	require.NoError(t, err)
   327  	t.Cleanup(func() {
   328  		os.Remove(f.Name())
   329  	})
   330  
   331  	n, err := f.WriteAt(nil, 0)
   332  	require.Equal(t, 0, n)
   333  	require.NoError(t, err)
   334  }
   335  
   336  func TestClose(t *testing.T) {
   337  	queue, err := loop.Setup(8, nil, nil)
   338  	require.NoError(t, err)
   339  	t.Cleanup(func() { queue.Close() })
   340  
   341  	fsm := NewFilesystem(queue)
   342  
   343  	f, err := TempFile(fsm, "test-concurrent-writes", 0)
   344  	require.NoError(t, err)
   345  	t.Cleanup(func() {
   346  		os.Remove(f.Name())
   347  	})
   348  	require.NoError(t, f.Close())
   349  	buf := []byte{1, 2}
   350  	_, err = f.WriteAt(buf, 0)
   351  	require.Equal(t, syscall.EBADF, err)
   352  }
   353  
   354  func TestConcurrentWritesIntegrity(t *testing.T) {
   355  	queue, err := loop.Setup(1024, nil, nil)
   356  	require.NoError(t, err)
   357  	t.Cleanup(func() { queue.Close() })
   358  
   359  	fsm := NewFilesystem(queue)
   360  
   361  	f, err := TempFile(fsm, "test-concurrent-writes", 0)
   362  	require.NoError(t, err)
   363  	t.Cleanup(func() {
   364  		os.Remove(f.Name())
   365  	})
   366  
   367  	var wg sync.WaitGroup
   368  	var n int64 = 30000
   369  
   370  	for i := int64(0); i < n; i++ {
   371  		wg.Add(1)
   372  		go func(i uint64) {
   373  			buf := make([]byte, 8)
   374  			binary.BigEndian.PutUint64(buf, i)
   375  			_, _ = f.WriteAt(buf, int64(i*8))
   376  			wg.Done()
   377  		}(uint64(i))
   378  	}
   379  	wg.Wait()
   380  
   381  	buf2 := make([]byte, 8)
   382  	for i := int64(0); i < n; i++ {
   383  		_, err := f.ReadAt(buf2, i*8)
   384  		require.NoError(t, err)
   385  		rst := binary.BigEndian.Uint64(buf2[:])
   386  		require.Equal(t, i, int64(rst))
   387  	}
   388  }