containerd/snapshots/benchsuite/benchmark_test.go
Maksym Pavlenko 871b6b6a9f Use testify
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2022-04-01 18:17:58 -07:00

313 lines
8.5 KiB
Go

//go:build linux
// +build linux
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package benchsuite
import (
"context"
"crypto/rand"
"flag"
"fmt"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/containerd/continuity/fs/fstest"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/snapshots"
"github.com/containerd/containerd/snapshots/devmapper"
"github.com/containerd/containerd/snapshots/native"
"github.com/containerd/containerd/snapshots/overlay"
)
var (
dmPoolDev string
dmRootPath string
overlayRootPath string
nativeRootPath string
)
func init() {
flag.StringVar(&dmPoolDev, "dm.thinPoolDev", "", "Pool device to run benchmark on")
flag.StringVar(&dmRootPath, "dm.rootPath", "", "Root dir for devmapper snapshotter")
flag.StringVar(&overlayRootPath, "overlay.rootPath", "", "Root dir for overlay snapshotter")
flag.StringVar(&nativeRootPath, "native.rootPath", "", "Root dir for native snapshotter")
// Avoid mixing benchmark output and INFO messages
logrus.SetLevel(logrus.ErrorLevel)
}
func BenchmarkNative(b *testing.B) {
if nativeRootPath == "" {
b.Skip("native root dir must be provided")
}
snapshotter, err := native.NewSnapshotter(nativeRootPath)
assert.Nil(b, err)
defer func() {
err = snapshotter.Close()
assert.Nil(b, err)
err = os.RemoveAll(nativeRootPath)
assert.Nil(b, err)
}()
benchmarkSnapshotter(b, snapshotter)
}
func BenchmarkOverlay(b *testing.B) {
if overlayRootPath == "" {
b.Skip("overlay root dir must be provided")
}
snapshotter, err := overlay.NewSnapshotter(overlayRootPath)
assert.Nil(b, err, "failed to create overlay snapshotter")
defer func() {
err = snapshotter.Close()
assert.Nil(b, err)
err = os.RemoveAll(overlayRootPath)
assert.Nil(b, err)
}()
benchmarkSnapshotter(b, snapshotter)
}
func BenchmarkDeviceMapper(b *testing.B) {
if dmPoolDev == "" {
b.Skip("devmapper benchmark requires thin-pool device to be prepared in advance and provided")
}
if dmRootPath == "" {
b.Skip("devmapper snapshotter root dir must be provided")
}
config := &devmapper.Config{
PoolName: dmPoolDev,
RootPath: dmRootPath,
BaseImageSize: "16Mb",
}
ctx := context.Background()
snapshotter, err := devmapper.NewSnapshotter(ctx, config)
assert.Nil(b, err)
defer func() {
err := snapshotter.ResetPool(ctx)
assert.Nil(b, err)
err = snapshotter.Close()
assert.Nil(b, err)
err = os.RemoveAll(dmRootPath)
assert.Nil(b, err)
}()
benchmarkSnapshotter(b, snapshotter)
}
// benchmarkSnapshotter tests snapshotter performance.
// It writes 16 layers with randomly created, modified, or removed files.
// Depending on layer index different sets of files are modified.
// In addition to total snapshotter execution time, benchmark outputs a few additional
// details - time taken to Prepare layer, mount, write data and unmount time,
// and Commit snapshot time.
func benchmarkSnapshotter(b *testing.B, snapshotter snapshots.Snapshotter) {
const (
layerCount = 16
fileSizeBytes = int64(1 * 1024 * 1024) // 1 MB
)
var (
total = 0
layers = make([]fstest.Applier, 0, layerCount)
layerIndex = int64(0)
)
for i := 1; i <= layerCount; i++ {
appliers := makeApplier(i, fileSizeBytes)
layers = append(layers, fstest.Apply(appliers...))
total += len(appliers)
}
var (
benchN int
prepareDuration time.Duration
writeDuration time.Duration
commitDuration time.Duration
)
// Wrap test with Run so additional details output will be added right below the benchmark result
b.Run("run", func(b *testing.B) {
var (
ctx = context.Background()
parent string
current string
)
// Reset durations since test might be ran multiple times
prepareDuration = 0
writeDuration = 0
commitDuration = 0
benchN = b.N
b.SetBytes(int64(total) * fileSizeBytes)
var timer time.Time
for i := 0; i < b.N; i++ {
for l := 0; l < layerCount; l++ {
current = fmt.Sprintf("prepare-layer-%d", atomic.AddInt64(&layerIndex, 1))
timer = time.Now()
mounts, err := snapshotter.Prepare(ctx, current, parent)
assert.Nil(b, err)
prepareDuration += time.Since(timer)
timer = time.Now()
err = mount.WithTempMount(ctx, mounts, layers[l].Apply)
assert.Nil(b, err)
writeDuration += time.Since(timer)
parent = fmt.Sprintf("committed-%d", atomic.AddInt64(&layerIndex, 1))
timer = time.Now()
err = snapshotter.Commit(ctx, parent, current)
assert.Nil(b, err)
commitDuration += time.Since(timer)
}
}
})
// Output extra measurements - total time taken to Prepare, mount and write data, and Commit
const outputFormat = "%-25s\t%s\n"
fmt.Fprintf(os.Stdout,
outputFormat,
b.Name()+"/prepare",
testing.BenchmarkResult{N: benchN, T: prepareDuration})
fmt.Fprintf(os.Stdout,
outputFormat,
b.Name()+"/write",
testing.BenchmarkResult{N: benchN, T: writeDuration})
fmt.Fprintf(os.Stdout,
outputFormat,
b.Name()+"/commit",
testing.BenchmarkResult{N: benchN, T: commitDuration})
fmt.Fprintln(os.Stdout)
}
// makeApplier returns a slice of fstest.Applier where files are written randomly.
// Depending on layer index, the returned layers will overwrite some files with the
// same generated names with new contents or deletions.
func makeApplier(layerIndex int, fileSizeBytes int64) []fstest.Applier {
seed := time.Now().UnixNano()
switch {
case layerIndex%3 == 0:
return []fstest.Applier{
updateFile("/a"),
updateFile("/b"),
fstest.CreateRandomFile("/c", seed, fileSizeBytes, 0777),
updateFile("/d"),
fstest.CreateRandomFile("/f", seed, fileSizeBytes, 0777),
updateFile("/e"),
fstest.RemoveAll("/g"),
fstest.CreateRandomFile("/h", seed, fileSizeBytes, 0777),
updateFile("/i"),
fstest.CreateRandomFile("/j", seed, fileSizeBytes, 0777),
}
case layerIndex%2 == 0:
return []fstest.Applier{
updateFile("/a"),
fstest.CreateRandomFile("/b", seed, fileSizeBytes, 0777),
fstest.RemoveAll("/c"),
fstest.CreateRandomFile("/d", seed, fileSizeBytes, 0777),
updateFile("/e"),
fstest.RemoveAll("/f"),
fstest.CreateRandomFile("/g", seed, fileSizeBytes, 0777),
updateFile("/h"),
fstest.CreateRandomFile("/i", seed, fileSizeBytes, 0777),
updateFile("/j"),
}
default:
return []fstest.Applier{
fstest.CreateRandomFile("/a", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/b", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/c", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/d", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/e", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/f", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/g", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/h", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/i", seed, fileSizeBytes, 0777),
fstest.CreateRandomFile("/j", seed, fileSizeBytes, 0777),
}
}
}
// applierFn represents helper func that implements fstest.Applier
type applierFn func(root string) error
func (fn applierFn) Apply(root string) error {
return fn(root)
}
// updateFile modifies a few bytes in the middle in order to demonstrate the difference in performance
// for block-based snapshotters (like devicemapper) against file-based snapshotters (like overlay, which need to
// perform a copy-up of the full file any time a single bit is modified).
func updateFile(name string) applierFn {
return func(root string) error {
path := filepath.Join(root, name)
file, err := os.OpenFile(path, os.O_WRONLY, 0600)
if err != nil {
return fmt.Errorf("failed to open %q: %w", path, err)
}
info, err := file.Stat()
if err != nil {
return err
}
var (
offset = info.Size() / 2
buf = make([]byte, 4)
)
if _, err := rand.Read(buf); err != nil {
return err
}
if _, err := file.WriteAt(buf, offset); err != nil {
return fmt.Errorf("failed to write %q at offset %d: %w", path, offset, err)
}
return file.Close()
}
}