diff --git a/test/performance/logger.go b/test/performance/logger.go new file mode 100644 index 00000000..3f574e22 --- /dev/null +++ b/test/performance/logger.go @@ -0,0 +1,49 @@ + +package metrics + +import ( + "fmt" + "log" + "os" + "path/filepath" + "testing" + "time" +) + +type TestLogger struct { + t *testing.T + file *os.File + logger *log.Logger +} + +func NewTestLogger(t *testing.T) (*TestLogger, error) { + // Create logs directory if it doesn't exist + logsDir := "logs" + if err := os.MkdirAll(logsDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create logs directory: %v", err) + } + + // Create log file with timestamp + timestamp := time.Now().Format("2006-01-02-15-04-05") + filename := filepath.Join(logsDir, fmt.Sprintf("porch-metrics-%s.log", timestamp)) + file, err := os.Create(filename) + if err != nil { + return nil, fmt.Errorf("failed to create log file: %v", err) + } + + logger := log.New(file, "", 0) // Remove timestamp from log entries + return &TestLogger{ + t: t, + file: file, + logger: logger, + }, nil +} + +func (l *TestLogger) Close() error { + return l.file.Close() +} + +func (l *TestLogger) LogResult(format string, args ...interface{}) { + // Log only to file + l.logger.Printf(format, args...) +} diff --git a/test/performance/metrics.go b/test/performance/metrics.go new file mode 100644 index 00000000..2170754e --- /dev/null +++ b/test/performance/metrics.go @@ -0,0 +1,162 @@ +package metrics + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + porchapi "github.com/nephio-project/porch/api/porch/v1alpha1" + configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1" + "github.com/nephio-project/porch/api/generated/clientset/versioned" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + scheme = runtime.NewScheme() +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(porchapi.AddToScheme(scheme)) + utilruntime.Must(configapi.AddToScheme(scheme)) +} + +// Helper functions for metrics collection and operations +func getPorchClientset(cfg *rest.Config) (*versioned.Clientset, error) { + return versioned.NewForConfig(cfg) +} + +func createGiteaRepo(repoName string) error { + giteaURL := "http://172.18.255.200:3000/api/v1/user/repos" + payload := map[string]interface{}{ + "name": repoName, + "description": "Test repository for Porch metrics", + "private": false, + "auto_init": true, + } + + jsonPayload, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal payload: %v", err) + } + + req, err := http.NewRequest("POST", giteaURL, bytes.NewBuffer(jsonPayload)) + if err != nil { + return fmt.Errorf("failed to create request: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + req.SetBasicAuth("nephio", "secret") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to create repo: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return fmt.Errorf("failed to create repo, status: %d", resp.StatusCode) + } + + return nil +} + +func debugPackageStatus(t *testing.T, c client.Client, ctx context.Context, namespace, name string) { + var pkg porchapi.PackageRevision + err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &pkg) + if err != nil { + t.Logf("Error getting package: %v", err) + return + } + + t.Logf("\nPackage Status Details:") + t.Logf(" Name: %s", pkg.Name) + t.Logf(" LifecycleState: %s", pkg.Spec.Lifecycle) + t.Logf(" WorkspaceName: %s", pkg.Spec.WorkspaceName) + t.Logf(" Revision: %s", pkg.Spec.Revision) + t.Logf(" Published: %v", pkg.Status.PublishedAt) + t.Logf(" Tasks:") + for i, task := range pkg.Spec.Tasks { + t.Logf(" %d. Type: %s", i+1, task.Type) + if task.Type == porchapi.TaskTypeInit && task.Init != nil { + t.Logf(" Description: %s", task.Init.Description) + t.Logf(" Keywords: %v", task.Init.Keywords) + } + } + t.Logf(" Conditions:") + for _, cond := range pkg.Status.Conditions { + t.Logf(" - Type: %s", cond.Type) + t.Logf(" Status: %s", cond.Status) + t.Logf(" Message: %s", cond.Message) + t.Logf(" Reason: %s", cond.Reason) + } +} + +// ... existing imports and code ... + +func deleteGiteaRepo(repoName string) error { + giteaURL := fmt.Sprintf("http://172.18.255.200:3000/api/v1/repos/nephio/%s", repoName) + + req, err := http.NewRequest("DELETE", giteaURL, nil) + if err != nil { + return fmt.Errorf("failed to create delete request: %v", err) + } + + req.SetBasicAuth("nephio", "secret") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to delete repo: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("failed to delete repo, status: %d", resp.StatusCode) + } + + return nil +} +func waitForRepository(ctx context.Context, c client.Client, t *testing.T, namespace, name string, timeout time.Duration) error { + start := time.Now() + for { + if time.Since(start) > timeout { + return fmt.Errorf("timeout waiting for repository to be ready") + } + + var repo configapi.Repository + err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &repo) + if err != nil { + return err + } + + t.Logf("\nRepository conditions at %v:", time.Since(start)) + t.Logf("Spec: %+v", repo.Spec) + t.Logf("Status: %+v", repo.Status) + + ready := false + for _, cond := range repo.Status.Conditions { + t.Logf(" - Type: %s, Status: %s, Message: %s", + cond.Type, cond.Status, cond.Message) + if cond.Type == "Ready" && cond.Status == "True" { + ready = true + break + } + } + + if ready { + return nil + } + + time.Sleep(2 * time.Second) + } +} diff --git a/test/performance/porch_metrics_test.go b/test/performance/porch_metrics_test.go new file mode 100644 index 00000000..a46e3000 --- /dev/null +++ b/test/performance/porch_metrics_test.go @@ -0,0 +1,490 @@ +package metrics + +import ( + "context" + "flag" + "fmt" + "net/http" + "os" + "os/exec" + "os/signal" + "syscall" + "testing" + "time" + + "github.com/nephio-project/porch/api/generated/clientset/versioned" + porchapi "github.com/nephio-project/porch/api/porch/v1alpha1" + configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1" + "github.com/prometheus/client_golang/prometheus/promhttp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +var ( + numRepos = flag.Int("repos", 1, "Number of repositories to create") + numPackages = flag.Int("packages", 5, "Number of packages per repository") +) + +func createAndSetupRepo(t *testing.T, ctx context.Context, c client.Client, namespace, repoName string) []OperationMetrics { + var metrics []OperationMetrics + start := time.Now() + + // Create Gitea repo + err := createGiteaRepo(repoName) + duration := time.Since(start).Seconds() + recordMetric("Create Gitea Repository", repoName, "", duration, err) + metrics = append(metrics, OperationMetrics{ + Operation: "Create Gitea Repository", + Duration: time.Duration(duration * float64(time.Second)), + Error: err, + }) + + if err != nil { + t.Logf("Warning: Failed to create Gitea repository: %v", err) + return metrics + } + + // Create Porch repo + start = time.Now() + repo := &configapi.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + Namespace: namespace, + }, + Spec: configapi.RepositorySpec{ + Type: "git", + Git: &configapi.GitRepository{ + Repo: fmt.Sprintf("http://172.18.255.200:3000/nephio/%s", repoName), + Branch: "main", + SecretRef: configapi.SecretRef{ + Name: "gitea", + }, + CreateBranch: true, + }, + }, + } + + err = c.Create(ctx, repo) + duration = time.Since(start).Seconds() + recordMetric("Create Porch Repository", repoName, "", duration, err) + metrics = append(metrics, OperationMetrics{ + Operation: "Create Porch Repository", + Duration: time.Duration(duration * float64(time.Second)), + Error: err, + }) + + if err == nil { + repositoryCounter.Inc() + start = time.Now() + err = waitForRepository(ctx, c, t, namespace, repoName, 60*time.Second) + duration = time.Since(start).Seconds() + recordMetric("Wait Repository Ready", repoName, "", duration, err) + metrics = append(metrics, OperationMetrics{ + Operation: "Wait Repository Ready", + Duration: time.Duration(duration * float64(time.Second)), + Error: err, + }) + } + + return metrics +} + +func createAndTestPackage(t *testing.T, ctx context.Context, c client.Client, porchClientset *versioned.Clientset, namespace, repoName, pkgName string) []OperationMetrics { + var metrics []OperationMetrics + start := time.Now() + + // Create new package + newPkg := &porchapi.PackageRevision{ + TypeMeta: metav1.TypeMeta{ + Kind: "PackageRevision", + APIVersion: porchapi.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-", repoName), + Namespace: namespace, + }, + Spec: porchapi.PackageRevisionSpec{ + PackageName: pkgName, + WorkspaceName: "main", + RepositoryName: repoName, + Lifecycle: porchapi.PackageRevisionLifecycleDraft, + Tasks: []porchapi.Task{ + { + Type: porchapi.TaskTypeInit, + Init: &porchapi.PackageInitTaskSpec{ + Description: "Test package for Porch metrics", + Keywords: []string{"test", "metrics"}, + Site: "https://nephio.org", + }, + }, + }, + }, + } + + err := c.Create(ctx, newPkg) + duration := time.Since(start).Seconds() + recordMetric("Create PackageRevision", repoName, pkgName, duration, err) + metrics = append(metrics, OperationMetrics{ + Operation: "Create PackageRevision", + Duration: time.Duration(duration * float64(time.Second)), + Error: err, + }) + + if err == nil { + packageCounter.Inc() + } + + // Wait for package to initialize + time.Sleep(5 * time.Second) + debugPackageStatus(t, c, ctx, namespace, newPkg.Name) + + // Propose the package + start = time.Now() + var pkg porchapi.PackageRevision + err = c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: newPkg.Name}, &pkg) + if err == nil { + pkg.Spec.Lifecycle = porchapi.PackageRevisionLifecycleProposed + err = c.Update(ctx, &pkg) + duration = time.Since(start).Seconds() + recordMetric("Update to Proposed", repoName, pkgName, duration, err) + metrics = append(metrics, OperationMetrics{ + Operation: "Update to Proposed", + Duration: time.Duration(duration * float64(time.Second)), + Error: err, + }) + + if err == nil { + // Wait for proposed state to settle + time.Sleep(5 * time.Second) + debugPackageStatus(t, c, ctx, namespace, pkg.Name) + + // Publish the package with approval + start = time.Now() + err = c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: pkg.Name}, &pkg) + if err == nil { + pkg.Spec.Lifecycle = porchapi.PackageRevisionLifecyclePublished + _, err = porchClientset.PorchV1alpha1().PackageRevisions(pkg.Namespace).UpdateApproval(ctx, pkg.Name, &pkg, metav1.UpdateOptions{}) + duration = time.Since(start).Seconds() + recordMetric("Update to Published", repoName, pkgName, duration, err) + metrics = append(metrics, OperationMetrics{ + Operation: "Update to Published", + Duration: time.Duration(duration * float64(time.Second)), + Error: err, + }) + + if err == nil { + // Verify final state + time.Sleep(5 * time.Second) + debugPackageStatus(t, c, ctx, namespace, pkg.Name) + } + } + } + } + + // Delete package + start = time.Now() + err = c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: pkg.Name}, &pkg) + if err == nil { + pkg.Spec.Lifecycle = porchapi.PackageRevisionLifecycleDeletionProposed + _, err = porchClientset.PorchV1alpha1().PackageRevisions(pkg.Namespace).UpdateApproval(ctx, pkg.Name, &pkg, metav1.UpdateOptions{}) + if err == nil { + time.Sleep(2 * time.Second) + err = c.Delete(ctx, &pkg) + } + } + duration = time.Since(start).Seconds() + recordMetric("Delete PackageRevision", repoName, pkgName, duration, err) + metrics = append(metrics, OperationMetrics{ + Operation: "Delete PackageRevision", + Duration: time.Duration(duration * float64(time.Second)), + Error: err, + }) + + return metrics +} + +func setupMonitoring(t *testing.T) error { + // Create prometheus.yml + promConfig := ` +global: + scrape_interval: 1s + evaluation_interval: 1s + +scrape_configs: + - job_name: 'porch_metrics' + static_configs: + - targets: ['host.docker.internal:2113'] + scrape_interval: 1s +` + if err := os.WriteFile("prometheus.yml", []byte(promConfig), 0644); err != nil { + return fmt.Errorf("failed to create prometheus.yml: %v", err) + } + + // Execute Docker commands + cmds := []struct { + name string + cmd string + args []string + }{ + {"network create", "docker", []string{"network", "create", "monitoring"}}, + {"stop prometheus", "docker", []string{"stop", "prometheus"}}, + {"remove prometheus", "docker", []string{"rm", "prometheus"}}, + {"run prometheus", "docker", []string{ + "run", "-d", + "--name", "prometheus", + "--network", "monitoring", + "--add-host", "host.docker.internal:host-gateway", + "-p", "9090:9090", + "-v", fmt.Sprintf("%s/prometheus.yml:/etc/prometheus/prometheus.yml", getCurrentDir()), + "prom/prometheus", + }}, + } + + for _, cmd := range cmds { + if err := exec.Command(cmd.cmd, cmd.args...).Run(); err != nil { + t.Logf("Warning executing %s: %v", cmd.name, err) + } + } + + // Give Prometheus a moment to start up + time.Sleep(2 * time.Second) + + return nil +} + +func getCurrentDir() string { + dir, err := os.Getwd() + if err != nil { + return "." + } + return dir +} + +func cleanup(t *testing.T) { + cmds := []struct { + cmd string + args []string + }{ + {"docker", []string{"stop", "prometheus"}}, + {"docker", []string{"rm", "prometheus"}}, + {"docker", []string{"network", "rm", "monitoring"}}, + } + + for _, cmd := range cmds { + if err := exec.Command(cmd.cmd, cmd.args...).Run(); err != nil { + t.Logf("Warning during cleanup: %v", err) + } + } + + os.Remove("prometheus.yml") +} + +func TestPorchScalePerformance(t *testing.T) { + // Setup monitoring + if err := setupMonitoring(t); err != nil { + t.Fatalf("Failed to setup monitoring: %v", err) + } + defer cleanup(t) + + // Create a channel to handle interrupt signal + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + // Start metrics server + go func() { + http.Handle("/metrics", promhttp.Handler()) + if err := http.ListenAndServe(":2113", nil); err != nil { + t.Logf("Error starting metrics server: %v", err) + } + }() + + // Setup logger + logger, err := NewTestLogger(t) + if err != nil { + t.Fatalf("Failed to create logger: %v", err) + } + defer logger.Close() + + flag.Parse() + + t.Logf("\nRunning test with %d repositories and %d packages per repository", *numRepos, *numPackages) + + // Setup clients + cfg, err := config.GetConfig() + if err != nil { + t.Fatalf("Failed to get config: %v", err) + } + + c, err := client.New(cfg, client.Options{Scheme: scheme}) + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + + porchClientset, err := getPorchClientset(cfg) + if err != nil { + t.Fatalf("Failed to create Porch clientset: %v", err) + } + + ctx := context.Background() + namespace := "porch-demo" + var allMetrics []TestMetrics + + // Test multiple repositories + for i := 0; i < *numRepos; i++ { + repoName := fmt.Sprintf("porch-metrics-test-%d", i) + t.Logf("\n=== Testing Repository %d: %s ===", i+1, repoName) + + // Cleanup any existing resources first + _ = deleteGiteaRepo(repoName) + _ = c.Delete(ctx, &configapi.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + Namespace: namespace, + }, + }) + time.Sleep(5 * time.Second) // Wait for cleanup + + repoMetrics := createAndSetupRepo(t, ctx, c, namespace, repoName) + for _, m := range repoMetrics { + recordMetric(m.Operation, repoName, "", m.Duration.Seconds(), m.Error) + } + printIterationResults(t, logger, i*(*numPackages), repoMetrics) + + // Test multiple packages per repository + for j := 0; j < *numPackages; j++ { + pkgName := fmt.Sprintf("test-package-%d", j) + t.Logf("\n--- Testing Package %d: %s ---", j+1, pkgName) + + pkgMetrics := createAndTestPackage(t, ctx, c, porchClientset, namespace, repoName, pkgName) + for _, m := range pkgMetrics { + recordMetric(m.Operation, repoName, pkgName, m.Duration.Seconds(), m.Error) + } + printIterationResults(t, logger, (i*(*numPackages))+j+1, pkgMetrics) + + allMetrics = append(allMetrics, TestMetrics{ + RepoName: repoName, + PkgName: pkgName, + Metrics: append(repoMetrics, pkgMetrics...), + }) + } + + // Cleanup repository + start := time.Now() + err := c.Delete(ctx, &configapi.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repoName, + Namespace: namespace, + }, + }) + cleanupMetrics := []OperationMetrics{{ + Operation: "Delete Repository", + Duration: time.Since(start), + Error: err, + }} + printIterationResults(t, logger, (i+1)*(*numPackages), cleanupMetrics) + } + + // Print consolidated results + printTestResults(t, logger, allMetrics) + + // After all tests complete, print message and wait for interrupt + t.Log("\nTests completed. Prometheus server is running at http://localhost:9090") + t.Log("Press Ctrl+C to stop and cleanup...") + + // Wait for interrupt signal + <-sigChan + t.Log("\nReceived interrupt signal. Cleaning up...") +} + +func printIterationResults(t *testing.T, logger *TestLogger, iteration int, metrics []OperationMetrics) { + // Console output + t.Logf("\n=== Iteration %d Results ===", iteration) + t.Log("Operation Duration Status") + t.Log("--------------------------------------------------") + + // File output + logger.LogResult("\n=== Iteration %d Results ===", iteration) + logger.LogResult("Operation Duration Status") + logger.LogResult("--------------------------------------------------") + + for _, m := range metrics { + status := "Success" + if m.Error != nil { + status = "Failed: " + m.Error.Error() + } + result := fmt.Sprintf("%-25s %-10v %s", + m.Operation, + m.Duration.Round(time.Millisecond), + status) + + t.Log(result) + logger.LogResult(result) + } +} + +func printTestResults(t *testing.T, logger *TestLogger, allMetrics []TestMetrics) { + header := "\n=== Consolidated Performance Test Results ===" + t.Log(header) + logger.LogResult(header) + + subheader := "Operation Min Max Avg Total" + t.Log(subheader) + logger.LogResult(subheader) + + divider := "------------------------------------------------------------------------" + t.Log(divider) + logger.LogResult(divider) + + stats := make(map[string]Stats) + + for _, m := range allMetrics { + for _, metric := range m.Metrics { + if metric.Error != nil { + continue + } + s := stats[metric.Operation] + if s.Count == 0 || metric.Duration < s.Min { + s.Min = metric.Duration + } + if metric.Duration > s.Max { + s.Max = metric.Duration + } + s.Total += metric.Duration + s.Count++ + stats[metric.Operation] = s + } + } + + for op, stat := range stats { + avg := stat.Total / time.Duration(stat.Count) + result := fmt.Sprintf("%-25s %-11v %-11v %-11v %-11v", + op, + stat.Min.Round(time.Millisecond), + stat.Max.Round(time.Millisecond), + avg.Round(time.Millisecond), + stat.Total.Round(time.Millisecond)) + + t.Log(result) + logger.LogResult(result) + } + + // Print errors if any + hasErrors := false + for _, m := range allMetrics { + for _, metric := range m.Metrics { + if metric.Error != nil { + if !hasErrors { + errHeader := "\n=== Errors Encountered ===" + t.Log(errHeader) + logger.LogResult(errHeader) + hasErrors = true + } + errMsg := fmt.Sprintf("Repository: %s, Package: %s, Operation: %s, Error: %v", + m.RepoName, m.PkgName, metric.Operation, metric.Error) + t.Log(errMsg) + logger.LogResult(errMsg) + } + } + } +} diff --git a/test/performance/prometheus.yml b/test/performance/prometheus.yml new file mode 100644 index 00000000..17c1cb7a --- /dev/null +++ b/test/performance/prometheus.yml @@ -0,0 +1,10 @@ + +global: + scrape_interval: 1s + evaluation_interval: 1s + +scrape_configs: + - job_name: 'porch_metrics' + static_configs: + - targets: ['host.docker.internal:2113'] + scrape_interval: 1s diff --git a/test/performance/prometheus_metrics.go b/test/performance/prometheus_metrics.go new file mode 100644 index 00000000..9f720d57 --- /dev/null +++ b/test/performance/prometheus_metrics.go @@ -0,0 +1,54 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // Operation duration metrics + operationDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "porch_operation_duration_seconds", + Help: "Duration of Porch operations in seconds", + Buckets: []float64{0.1, 0.5, 1, 2, 5, 10, 30, 60}, + }, + []string{"operation", "repository", "package", "status"}, + ) + + // Operation counter metrics + operationCounter = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "porch_operations_total", + Help: "Total number of Porch operations", + }, + []string{"operation", "repository", "package", "status"}, + ) + + // Repository metrics + repositoryCounter = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "porch_repositories_created_total", + Help: "Total number of repositories created", + }, + ) + + // Package metrics + packageCounter = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "porch_packages_created_total", + Help: "Total number of packages created", + }, + ) +) + +// recordMetric records both duration and count for an operation +func recordMetric(operation, repoName, pkgName string, duration float64, err error) { + status := "success" + if err != nil { + status = "error" + } + + operationDuration.WithLabelValues(operation, repoName, pkgName, status).Observe(duration) + operationCounter.WithLabelValues(operation, repoName, pkgName, status).Inc() +} diff --git a/test/performance/promql_queries.txt b/test/performance/promql_queries.txt new file mode 100644 index 00000000..b79ecfdb --- /dev/null +++ b/test/performance/promql_queries.txt @@ -0,0 +1,111 @@ + +Here are the PromQL queries to get operation timings for iterations and specific package operations: + +• Time Taken for Each Operation per Iteration +# Basic operation duration +porch_operation_duration_seconds_sum{operation="Create PackageRevision"} + +# Detailed breakdown by operation +sum by (operation) (porch_operation_duration_seconds_sum) + +# Operation duration with package and repository context +sum by (operation, package, repository) (porch_operation_duration_seconds_sum) + +# Latest operation durations +sort_desc(porch_operation_duration_seconds_sum) + + +• Specific Package Revision Operations +# Time taken for specific package revision creation +porch_operation_duration_seconds_sum{operation="Create PackageRevision", package="test-package-0"} + +# Time for package to move to proposed state +porch_operation_duration_seconds_sum{operation="Update to Proposed", package="test-package-0"} + +# Time for package to move to published state +porch_operation_duration_seconds_sum{operation="Update to Published", package="test-package-0"} + +# Time taken for package deletion +porch_operation_duration_seconds_sum{operation="Delete PackageRevision", package="test-package-0"} + + +• Comparative Analysis +# Compare durations across different operations for same package +sum by (operation) ( + porch_operation_duration_seconds_sum{package="test-package-0"} +) + +# Average duration per operation type +rate(porch_operation_duration_seconds_sum[5m]) / rate(porch_operation_duration_seconds_count[5m]) + +# Operation duration distribution +histogram_quantile(0.95, + sum by (le, operation) ( + rate(porch_operation_duration_seconds_bucket{package="test-package-0"}[5m]) + ) +) + + +• Time Series Analysis +# Operation duration over time +rate(porch_operation_duration_seconds_sum{operation="Create PackageRevision"}[5m]) + +# Compare operation times across iterations +sum by (operation) ( + increase(porch_operation_duration_seconds_sum[1h]) +) + + +• Success/Failure Analysis +# Duration of successful operations +porch_operation_duration_seconds_sum{status="success", operation="Create PackageRevision"} + +# Duration of failed operations +porch_operation_duration_seconds_sum{status="error", operation="Create PackageRevision"} + + +Example Usage: + + +• For a specific package operation: +# Get exact duration for creating package "test-package-0" +porch_operation_duration_seconds_sum{ + operation="Create PackageRevision", + package="test-package-0", + repository="porch-metrics-test-0" +} + +# Get full lifecycle timing for package "test-package-0" +sum by (operation) ( + porch_operation_duration_seconds_sum{ + package="test-package-0", + repository="porch-metrics-test-0" + } +) + + +2. For iteration analysis: +# Get timing for all operations in latest iteration +sum by (operation, package) ( + porch_operation_duration_seconds_sum{ + repository="porch-metrics-test-0" + } +) + +# Compare operation durations across iterations +rate(porch_operation_duration_seconds_sum[5m]) + / +rate(porch_operation_duration_seconds_count[5m]) + + +• For specific operation analysis: +# Detailed timing for package state transitions +sum by (operation) ( + porch_operation_duration_seconds_sum{ + operation=~"Update to.*", + package="test-package-0" + } +) + + + diff --git a/test/performance/types.go b/test/performance/types.go new file mode 100644 index 00000000..75738360 --- /dev/null +++ b/test/performance/types.go @@ -0,0 +1,28 @@ + +package metrics + +import ( + "time" +) + +// OperationMetrics holds metrics for a single operation +type OperationMetrics struct { + Operation string + Duration time.Duration + Error error +} + +// TestMetrics holds metrics for a test iteration +type TestMetrics struct { + RepoName string + PkgName string + Metrics []OperationMetrics +} + +// Stats holds statistics for operations +type Stats struct { + Min time.Duration + Max time.Duration + Total time.Duration + Count int +}