Repository: envoyproxy/gateway

Issue:
Benchmark job improvement
- [ ] don't use [latest](https://github.com/envoyproxy/gateway/blob/a6c3648f9d3678006b0d87625a29ef07cf99991e/test/benchmark/config/nighthawk-client.yaml#L12) tag
- [ ] Speed up, it take 50min+ to run it
- [ ] support arm64



Discussion:
@zirain: we're able to use stable tag after https://github.com/envoyproxy/nighthawk/pull/1419.

```
docker pull envoyproxy/nighthawk-dev:6198df0e01dc85f8d5efa3ef188f3bcdcc6eecac
```

Solution patch:
diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml
index 0cf2128d4..4943cf217 100644
--- a/.github/workflows/build_and_test.yaml
+++ b/.github/workflows/build_and_test.yaml
@@ -252,8 +252,6 @@ jobs:
     - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8  # v5.0.0
     - uses: ./tools/github-actions/setup-deps
 
-    - name: Setup Graphviz
-      uses: ts-graphviz/setup-graphviz@b1de5da23ed0a6d14e0aeee8ed52fdd87af2363c  # v2.0.2
 
     # Benchmark
     - name: Run Benchmark tests
@@ -266,6 +264,7 @@ jobs:
         BENCHMARK_CPU_LIMITS: 1000m
         BENCHMARK_MEMORY_LIMITS: 2000Mi
         BENCHMARK_REPORT_DIR: benchmark_report
+        BENCHMARK_RENDER_PNG: "false"
       run: make benchmark
 
     - name: Upload Benchmark report
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index d892348fc..1b06e1c85 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -18,8 +18,6 @@ jobs:
       - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8  # v5.0.0
       - uses: ./tools/github-actions/setup-deps
 
-      - name: Setup Graphviz
-        uses: ts-graphviz/setup-graphviz@b1de5da23ed0a6d14e0aeee8ed52fdd87af2363c  # v2.0.2
 
       # Benchmark
       - name: Run Benchmark tests
@@ -33,6 +31,7 @@ jobs:
           BENCHMARK_CPU_LIMITS: 1000m
           BENCHMARK_MEMORY_LIMITS: 2000Mi
           BENCHMARK_REPORT_DIR: benchmark_report
+          BENCHMARK_RENDER_PNG: "false"
         run: make benchmark
 
       - name: Package benchmark report
diff --git a/internal/gatewayapi/runner/runner.go b/internal/gatewayapi/runner/runner.go
index 5878c08fb..f85fbbee6 100644
--- a/internal/gatewayapi/runner/runner.go
+++ b/internal/gatewayapi/runner/runner.go
@@ -194,7 +194,10 @@ func (r *Runner) subscribeAndTranslate(sub <-chan watchable.Snapshot[string, *re
 				// Publish the IRs.
 				// Also validate the ir before sending it.
 				for key, val := range result.InfraIR {
-					r.Logger.V(1).WithValues(string(message.InfraIRMessageName), key).Info(val.JSONString())
+					logger := r.Logger.V(1).WithValues(string(message.InfraIRMessageName), key)
+					if logger.Enabled() {
+						logger.Info(val.JSONString())
+					}
 					if err := val.Validate(); err != nil {
 						r.Logger.Error(err, "unable to validate infra ir, skipped sending it")
 						errChan <- err
@@ -206,7 +209,10 @@ func (r *Runner) subscribeAndTranslate(sub <-chan watchable.Snapshot[string, *re
 				}
 
 				for key, val := range result.XdsIR {
-					r.Logger.V(1).WithValues(string(message.XDSIRMessageName), key).Info(val.JSONString())
+					logger := r.Logger.V(1).WithValues(string(message.XDSIRMessageName), key)
+					if logger.Enabled() {
+						logger.Info(val.JSONString())
+					}
 					if err := val.Validate(); err != nil {
 						r.Logger.Error(err, "unable to validate xds ir, skipped sending it")
 						errChan <- err
diff --git a/internal/utils/jsonpatch/patch.go b/internal/utils/jsonpatch/patch.go
index 8c14ae19f..868c9c2f3 100644
--- a/internal/utils/jsonpatch/patch.go
+++ b/internal/utils/jsonpatch/patch.go
@@ -11,7 +11,6 @@ import (
 	"fmt"
 
 	jsonpatchv5 "github.com/evanphx/json-patch/v5"
-	"sigs.k8s.io/yaml"
 
 	"github.com/envoyproxy/gateway/internal/ir"
 )
@@ -58,38 +57,18 @@ func ApplyJSONPatches(document json.RawMessage, patches ...ir.JSONPatchOperation
 		}
 
 		for _, path := range jsonPointers {
-			op := ir.JSONPatchOperation{
-				Path:  &path,
-				Op:    p.Op,
-				Value: p.Value,
-				From:  p.From,
-			}
-
-			// Convert patch to JSON
-			// The patch library expects an array so convert it into one
-			y, err := yaml.Marshal([]ir.JSONPatchOperation{op})
-			if err != nil {
-				tErr := fmt.Errorf("unable to marshal patch %+v, err: %s", op, err.Error())
-				tErrs = errors.Join(tErrs, tErr)
-				continue
-			}
-			jsonBytes, err := yaml.YAMLToJSON(y)
+			operation, err := toPatchOperation(path, p)
 			if err != nil {
-				tErr := fmt.Errorf("unable to convert patch to json %s, err: %s", string(y), err.Error())
-				tErrs = errors.Join(tErrs, tErr)
-				continue
-			}
-			patchObj, err := jsonpatchv5.DecodePatch(jsonBytes)
-			if err != nil {
-				tErr := fmt.Errorf("unable to decode patch %s, err: %s", string(jsonBytes), err.Error())
-				tErrs = errors.Join(tErrs, tErr)
+				tErrs = errors.Join(tErrs, err)
 				continue
 			}
 
+			patch := jsonpatchv5.Patch{operation}
+
 			// Apply patch
-			document, err = patchObj.ApplyWithOptions(document, opts)
+			document, err = patch.ApplyWithOptions(document, opts)
 			if err != nil {
-				tErr := fmt.Errorf("unable to apply patch:\n%s on resource:\n%s, err: %s", string(jsonBytes), string(document), err.Error())
+				tErr := fmt.Errorf("unable to apply patch: op=%s path=%s err: %s", string(p.Op), path, err.Error())
 				tErrs = errors.Join(tErrs, tErr)
 				continue
 			}
@@ -97,3 +76,52 @@ func ApplyJSONPatches(document json.RawMessage, patches ...ir.JSONPatchOperation
 	}
 	return document, tErrs
 }
+
+func toPatchOperation(path string, original ir.JSONPatchOperation) (jsonpatchv5.Operation, error) {
+	operation := make(jsonpatchv5.Operation, 4)
+
+	rawOp, err := marshalJSONString(string(original.Op))
+	if err != nil {
+		return nil, fmt.Errorf("unable to marshal patch op %q: %w", string(original.Op), err)
+	}
+	operation["op"] = rawOp
+
+	rawPath, err := marshalJSONString(path)
+	if err != nil {
+		return nil, fmt.Errorf("unable to marshal patch path %q: %w", path, err)
+	}
+	operation["path"] = rawPath
+
+	if original.From != nil {
+		rawFrom, err := marshalJSONString(*original.From)
+		if err != nil {
+			return nil, fmt.Errorf("unable to marshal patch from %q: %w", *original.From, err)
+		}
+		operation["from"] = rawFrom
+	}
+
+	if original.Value != nil {
+		operation["value"] = cloneRawJSON(original.Value.Raw)
+	}
+
+	return operation, nil
+}
+
+func marshalJSONString(value string) (*json.RawMessage, error) {
+	b, err := json.Marshal(value)
+	if err != nil {
+		return nil, err
+	}
+	raw := json.RawMessage(b)
+	return &raw, nil
+}
+
+func cloneRawJSON(src []byte) *json.RawMessage {
+	if src == nil {
+		rawNull := json.RawMessage([]byte("null"))
+		return &rawNull
+	}
+	cloned := make(json.RawMessage, len(src))
+	copy(cloned, src)
+	return &cloned
+}
diff --git a/internal/xds/runner/runner.go b/internal/xds/runner/runner.go
index 51d172b35..5105d6d41 100644
--- a/internal/xds/runner/runner.go
+++ b/internal/xds/runner/runner.go
@@ -10,7 +10,6 @@ import (
 	"crypto/tls"
 	"fmt"
 	"net"
-	"reflect"
 	"strconv"
 	"time"
 
@@ -270,7 +269,7 @@ func (r *Runner) subscribeAndTranslate(sub <-chan watchable.Snapshot[string, *ir
 					// Skip updating status for policies with empty status
 					// They may have been skipped in this translation because
 					// their target is not found (not relevant)
-					if !(reflect.ValueOf(e.Status).IsZero()) {
+					if len(e.Status.Ancestors) > 0 {
 						r.ProviderResources.EnvoyPatchPolicyStatuses.Store(key, e.Status)
 					}
 					delete(statusesToDelete, key)
diff --git a/internal/xds/translator/jsonpatch.go b/internal/xds/translator/jsonpatch.go
index c8b2a7141..e4e3ceb36 100644
--- a/internal/xds/translator/jsonpatch.go
+++ b/internal/xds/translator/jsonpatch.go
@@ -17,7 +17,6 @@ import (
 	tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
 	resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
 	"google.golang.org/protobuf/encoding/protojson"
-	"sigs.k8s.io/yaml"
 
 	"github.com/envoyproxy/gateway/internal/gatewayapi/status"
 	"github.com/envoyproxy/gateway/internal/ir"
@@ -68,17 +67,15 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []*
 			// If Path and JSONPath is "" and op is "add", unmarshal and add the patch as a complete
 			// resource
 			if p.Operation.Op == ir.JSONPatchOpAdd && p.Operation.IsPathNilOrEmpty() && p.Operation.IsJSONPathNilOrEmpty() {
-				// Convert patch to JSON
-				// The patch library expects an array so convert it into one
-				y, err := yaml.Marshal(p.Operation.Value)
-				if err != nil {
-					tErr := fmt.Errorf("unable to marshal patch %+v, err: %s", p.Operation.Value, err.Error())
+				if p.Operation.Value == nil {
+					tErr := fmt.Errorf("missing value for add operation with empty path")
 					tErrs = errors.Join(tErrs, tErr)
 					continue
 				}
-				jsonBytes, err := yaml.YAMLToJSON(y)
-				if err != nil {
-					tErr := fmt.Errorf("unable to convert patch to json %s, err: %s", string(y), err.Error())
+
+				jsonBytes := p.Operation.Value.Raw
+				if len(jsonBytes) == 0 {
+					tErr := fmt.Errorf("empty value for add operation with empty path")
 					tErrs = errors.Join(tErrs, tErr)
 					continue
 				}
diff --git a/tools/make/kube.mk b/tools/make/kube.mk
index aa0cb495d..843ead2e7 100644
--- a/tools/make/kube.mk
+++ b/tools/make/kube.mk
@@ -25,6 +25,10 @@ BENCHMARK_CONNECTIONS ?= 100
 BENCHMARK_DURATION ?= 60
 BENCHMARK_REPORT_DIR ?= benchmark_report
 
+# Benchmark toggles
+# Disable PNG rendering by default to speed up CI
+BENCHMARK_RENDER_PNG ?= false
+
 CONFORMANCE_RUN_TEST ?=
 
 E2E_RUN_TEST ?=
@@ -253,8 +257,8 @@ run-benchmark: install-benchmark-server prepare-ip-family ## Run benchmark tests
 	kubectl apply -f test/benchmark/config/gatewayclass.yaml
 	go test -v -tags benchmark -timeout $(BENCHMARK_TIMEOUT) ./test/benchmark --rps=$(BENCHMARK_RPS) --connections=$(BENCHMARK_CONNECTIONS) --duration=$(BENCHMARK_DURATION) --report-save-dir=$(BENCHMARK_REPORT_DIR)
 	# render benchmark profiles into image
-	dot -V
-	find test/benchmark/$(BENCHMARK_REPORT_DIR)/profiles -name "*.pprof" -type f -exec sh -c 'go tool pprof -png "$$1" > "$${1%.pprof}.png"' _ {} \;
+	@if [ "$(BENCHMARK_RENDER_PNG)" != "false" ]; then dot -V; fi
+	@if [ "$(BENCHMARK_RENDER_PNG)" != "false" ]; then find test/benchmark/$(BENCHMARK_REPORT_DIR)/profiles -name "*.pprof" -type f -exec sh -c 'go tool pprof -png "$$1" > "$$${1%.pprof}.png"' _ {} \; ; fi
 
 .PHONY: install-benchmark-server
 install-benchmark-server: ## Install nighthawk server for benchmark test



Test patch:
diff --git a/test/benchmark/config/nighthawk-client.yaml b/test/benchmark/config/nighthawk-client.yaml
index 35553eda6..6d689a203 100644
--- a/test/benchmark/config/nighthawk-client.yaml
+++ b/test/benchmark/config/nighthawk-client.yaml
@@ -9,7 +9,7 @@ spec:
     spec:
       containers:
         - name: nighthawk-client
-          image: envoyproxy/nighthawk-dev:latest
+          image: envoyproxy/nighthawk-dev:6198df0e01dc85f8d5efa3ef188f3bcdcc6eecac
           imagePullPolicy: IfNotPresent
           command:
             - /usr/local/bin/nighthawk_client
diff --git a/test/benchmark/config/nighthawk-test-server.yaml b/test/benchmark/config/nighthawk-test-server.yaml
index 75fa0bfbd..c1ce968e6 100644
--- a/test/benchmark/config/nighthawk-test-server.yaml
+++ b/test/benchmark/config/nighthawk-test-server.yaml
@@ -17,7 +17,7 @@ spec:
       serviceAccountName: default
       containers:
         - name: nighthawk-server
-          image: envoyproxy/nighthawk-dev:latest
+          image: envoyproxy/nighthawk-dev:6198df0e01dc85f8d5efa3ef188f3bcdcc6eecac
           imagePullPolicy: IfNotPresent
           args: ["-c", "/etc/test-server-config/nighthawk-test-server-config.yaml"]
           ports: