...

Source file src/edge-infra.dev/test/e2e/linkerd/performance/slowcooker/slowcooker_test.go

Documentation: edge-infra.dev/test/e2e/linkerd/performance/slowcooker

     1  package slowcooker
     2  
     3  import (
     4  	"context"
     5  	"embed"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  	"gotest.tools/v3/assert/cmp"
    20  	"gotest.tools/v3/poll"
    21  	batchv1 "k8s.io/api/batch/v1"
    22  	corev1 "k8s.io/api/core/v1"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	k8stypes "k8s.io/apimachinery/pkg/types"
    25  	"sigs.k8s.io/controller-runtime/pkg/client"
    26  
    27  	"edge-infra.dev/pkg/k8s/testing/kmp"
    28  	"edge-infra.dev/pkg/k8s/unstructured"
    29  	"edge-infra.dev/test/f2"
    30  	"edge-infra.dev/test/f2/integration"
    31  	"edge-infra.dev/test/f2/x/ktest"
    32  	"edge-infra.dev/test/f2/x/ktest/envtest"
    33  	"edge-infra.dev/test/f2/x/ktest/kustomization"
    34  )
    35  
    36  //go:embed testdata
    37  var manifests embed.FS
    38  
    39  var f f2.Framework
    40  
    41  var slowcookerManifests []byte
    42  
    43  const slowcooker = "slow-cooker"
    44  
    45  type Result struct {
    46  	P50  int `json:"p50"`
    47  	P75  int `json:"p75"`
    48  	P90  int `json:"p90"`
    49  	P95  int `json:"p95"`
    50  	P99  int `json:"p99"`
    51  	P999 int `json:"p999"`
    52  }
    53  
    54  func TestMain(m *testing.M) {
    55  	f = f2.New(
    56  		context.Background(),
    57  		f2.WithExtensions(
    58  			ktest.New(
    59  				ktest.SkipNamespaceCreation(),
    60  				ktest.WithEnvtestOptions(
    61  					envtest.WithoutCRDs(),
    62  				),
    63  			),
    64  		),
    65  	).
    66  		Setup(func(ctx f2.Context) (f2.Context, error) {
    67  			if !integration.IsL2() {
    68  				return ctx, fmt.Errorf("%w: requires L2 integration test level", f2.ErrSkip)
    69  			}
    70  			return ctx, nil
    71  		}).
    72  		Setup(func(ctx f2.Context) (f2.Context, error) {
    73  			// Load slowcooker kustomization manifests
    74  			var err error
    75  			slowcookerManifests, err = fs.ReadFile(manifests, "testdata/slow-cooker_manifests.yaml")
    76  			if err != nil {
    77  				return ctx, err
    78  			}
    79  			return ctx, nil
    80  		}).
    81  		WithLabel("dsds", "true").
    82  		WithLabel("performance", "true").
    83  		Slow()
    84  	os.Exit(f.Run(m))
    85  }
    86  
    87  func TestSlowcooker(t *testing.T) {
    88  	var iterations int
    89  	var podName string
    90  	var logs string
    91  	var k *ktest.K8s
    92  	var manifests []*unstructured.Unstructured
    93  	var err error
    94  	feature := f2.NewFeature("slowcooker").
    95  		Setup("apply slowcooker manifests", func(ctx f2.Context, t *testing.T) f2.Context {
    96  			k = ktest.FromContextT(ctx, t)
    97  			manifests, err = kustomization.ProcessManifests(ctx.RunID, slowcookerManifests, slowcooker)
    98  			require.NoError(t, err)
    99  
   100  			for _, manifest := range manifests {
   101  				require.NoError(t, k.Client.Create(ctx, manifest))
   102  			}
   103  			return ctx
   104  		}).
   105  		Setup("wait for slowcooker manifests", func(ctx f2.Context, t *testing.T) f2.Context {
   106  			job := &batchv1.Job{}
   107  			for _, manifest := range manifests {
   108  				if manifest.GetKind() == "Job" && manifest.GetName() == slowcooker {
   109  					require.NoError(t, unstructured.FromUnstructured(manifest, job))
   110  					for _, arg := range job.Spec.Template.Spec.Containers[0].Args {
   111  						if strings.Contains(arg, "iterations") {
   112  							iterations, err = strconv.Atoi(strings.TrimPrefix(arg, "--iterations="))
   113  							require.NoError(t, err)
   114  						}
   115  					}
   116  					k.WaitOn(t, k.Check(manifest, kmp.IsCurrent()), poll.WithTimeout(time.Minute))
   117  				}
   118  			}
   119  			return ctx
   120  		}).
   121  		Setup("wait for slowcooker pod to be ready", func(ctx f2.Context, t *testing.T) f2.Context {
   122  			podName, err = getPodName(ctx, k)
   123  			require.NoError(t, err)
   124  
   125  			pod := &corev1.Pod{}
   126  			require.NoError(t, k.Client.Get(ctx, k8stypes.NamespacedName{Name: podName, Namespace: slowcooker}, pod))
   127  
   128  			k.WaitOn(t, k.Check(pod, func(_ client.Object) cmp.Result {
   129  				require.NoError(t, k.Client.Get(ctx, k8stypes.NamespacedName{Name: podName, Namespace: slowcooker}, pod))
   130  				if isPodReady(pod) {
   131  					return cmp.ResultSuccess
   132  				}
   133  				return cmp.ResultFailure("slowcooker pod is not ready")
   134  			}), poll.WithTimeout(time.Minute*2))
   135  
   136  			return ctx
   137  		}).
   138  		Setup("get slowcooker logs", func(ctx f2.Context, t *testing.T) f2.Context {
   139  			pod := &corev1.Pod{}
   140  			require.NoError(t, k.Client.Get(ctx, k8stypes.NamespacedName{Name: podName, Namespace: slowcooker}, pod))
   141  
   142  			k.WaitOn(t, k.Check(pod, func(_ client.Object) cmp.Result {
   143  				require.NoError(t, k.Client.Get(ctx, k8stypes.NamespacedName{Name: podName, Namespace: slowcooker}, pod))
   144  				if !isPodReady(pod) {
   145  					return cmp.ResultSuccess
   146  				}
   147  				return cmp.ResultFailure("slow cooker container still running")
   148  			}), poll.WithTimeout(time.Minute*time.Duration(iterations+2)))
   149  
   150  			logs, err = getLogs(ctx, k, podName)
   151  			require.NoError(t, err)
   152  			fmt.Println(logs)
   153  			return ctx
   154  		}).
   155  		Test("over 99 percent average throughput", func(ctx f2.Context, t *testing.T) f2.Context {
   156  			averageThroughput, err := calculateAverageThroughput(logs, iterations)
   157  			require.NoError(t, err)
   158  			fmt.Printf("Average throughput: %v%%\n", averageThroughput)
   159  			assert.GreaterOrEqual(t, averageThroughput, float32(99.0))
   160  			return ctx
   161  		}).
   162  		Test("over 95 percent successful requests", func(ctx f2.Context, t *testing.T) f2.Context {
   163  			successfulRequests, err := calculateSuccessfulRequests(logs)
   164  			require.NoError(t, err)
   165  			fmt.Printf("Successful requests: %v%%\n", successfulRequests)
   166  			assert.GreaterOrEqual(t, successfulRequests, float32(95.0))
   167  			return ctx
   168  		}).
   169  		Test("99th percentile latency less than 2ms", func(ctx f2.Context, t *testing.T) f2.Context {
   170  			latency, err := getHighestLatency(logs)
   171  			require.NoError(t, err)
   172  			fmt.Printf("Latency: %vms\n", latency)
   173  			assert.LessOrEqual(t, latency, 2)
   174  			return ctx
   175  		}).
   176  		Teardown("delete namespace", func(ctx f2.Context, t *testing.T) f2.Context {
   177  			require.NoError(t, k.Client.Delete(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: slowcooker}}))
   178  			return ctx
   179  		}).Feature()
   180  	f.Test(t, feature)
   181  }
   182  
   183  func getPodName(ctx context.Context, k *ktest.K8s) (string, error) {
   184  	podList := &corev1.PodList{}
   185  	err := k.Client.List(ctx, podList, &client.ListOptions{Namespace: slowcooker})
   186  	if err != nil {
   187  		return "", err
   188  	}
   189  	for _, pod := range podList.Items {
   190  		if strings.Contains(pod.Name, slowcooker) {
   191  			return pod.Name, nil
   192  		}
   193  	}
   194  	return "", fmt.Errorf("slow-cooker pod not found")
   195  }
   196  
   197  func isPodReady(pod *corev1.Pod) bool {
   198  	for _, condition := range pod.Status.Conditions {
   199  		if condition.Type == "Ready" && condition.Status == corev1.ConditionTrue {
   200  			return true
   201  		}
   202  	}
   203  	return false
   204  }
   205  
   206  func getLogs(ctx context.Context, k *ktest.K8s, podName string) (string, error) {
   207  	logStream, err := k.GetContainerLogs(ctx, podName, slowcooker, slowcooker)
   208  	if err != nil {
   209  		return "", err
   210  	}
   211  
   212  	logsData, err := io.ReadAll(logStream)
   213  	if err != nil {
   214  		return "", err
   215  	}
   216  	return string(logsData), nil
   217  }
   218  
   219  func getHighestLatency(logs string) (int, error) {
   220  	result := &Result{}
   221  	err := json.Unmarshal([]byte(fmt.Sprintf("{%s", strings.SplitN(logs, "{", 2)[1])), result)
   222  	if err != nil {
   223  		return 0, err
   224  	}
   225  	return result.P99, nil
   226  }
   227  
   228  func calculateSuccessfulRequests(logs string) (float32, error) {
   229  	totalGood := 0
   230  	totalBad := 0
   231  	totalFailed := 0
   232  
   233  	lines := strings.Split(logs, "\n")
   234  	for _, line := range lines {
   235  		re := regexp.MustCompile("[0-9]+/[0-9]+/[0-9]+")
   236  		requests := string(re.Find([]byte(line)))
   237  		if requests != "" {
   238  			good, err := strconv.Atoi(strings.Split(requests, "/")[0])
   239  			if err != nil {
   240  				return 0, err
   241  			}
   242  			totalGood += good
   243  
   244  			bad, err := strconv.Atoi(strings.Split(requests, "/")[1])
   245  			if err != nil {
   246  				return 0, err
   247  			}
   248  			totalBad += bad
   249  
   250  			failed, err := strconv.Atoi(strings.Split(requests, "/")[2])
   251  			if err != nil {
   252  				return 0, err
   253  			}
   254  			totalFailed += failed
   255  		}
   256  	}
   257  	totalRequests := totalGood + totalBad + totalFailed
   258  	successfulRequests := float32(totalGood) / float32(totalRequests) * 100
   259  	return successfulRequests, nil
   260  }
   261  
   262  func calculateAverageThroughput(logs string, iterations int) (float32, error) {
   263  	totalThroughput := 0
   264  
   265  	lines := strings.Split(logs, "\n")
   266  	for _, line := range lines {
   267  		re := regexp.MustCompile("[0-9]+%")
   268  		throughput := strings.TrimSuffix(string(re.Find([]byte(line))), "%")
   269  		if throughput != "" {
   270  			throughput, err := strconv.Atoi(throughput)
   271  			if err != nil {
   272  				return 0, err
   273  			}
   274  			totalThroughput += throughput
   275  		}
   276  	}
   277  	return float32(totalThroughput) / float32(iterations), nil
   278  }
   279  

View as plain text