This is an automated email from the ASF dual-hosted git repository.

mfordjody pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git


The following commit(s) were added to refs/heads/master by this push:
     new cd068a07 Add supplementary tests and performance tests (#963)
cd068a07 is described below

commit cd068a07e113117ec74c673f5f730e3bd983d1bd
Author: mfordjody <[email protected]>
AuthorDate: Sat Jul 18 20:07:41 2026 +0800

    Add supplementary tests and performance tests (#963)
---
 .github/workflows/benchmark.yml                    |  66 ++++
 .github/workflows/ci.yml                           |  18 ++
 PERFORMANCE.md                                     | 115 +++++++
 README.md                                          |   7 +-
 .../pkg/model/authentication_resolution_test.go    | 293 +++++++++++++++++
 dubbod/discovery/pkg/xds/ads_benchmark_test.go     | 346 +++++++++++++++++++++
 tools/make/test.mk                                 |  14 +
 7 files changed, 858 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
new file mode 100644
index 00000000..f7d3d53d
--- /dev/null
+++ b/.github/workflows/benchmark.yml
@@ -0,0 +1,66 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+name: Performance Benchmarks
+
+on:
+  schedule:
+    - cron: '0 4 * * 3'
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  benchmark:
+    name: Control-Plane Scale Matrix
+    runs-on: ubuntu-latest
+    timeout-minutes: 45
+    if: github.repository == 'apache/dubbo-kubernetes'
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+          cache-dependency-path: go.sum
+
+      - name: Capture benchmark environment
+        run: |
+          {
+            go version
+            go env GOOS GOARCH
+            uname -a
+            lscpu
+          } > benchmark-environment.txt
+
+      - name: Run benchmark matrix
+        run: make benchmark BENCHMARK_COUNT=5 BENCHMARK_TIME=1s 
BENCHMARK_CPU=1 | tee benchmark-results.txt
+
+      - name: Upload benchmark evidence
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: control-plane-benchmarks-${{ github.run_id }}
+          path: |
+            benchmark-environment.txt
+            benchmark-results.txt
+          retention-days: 30
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 17efd658..ee734d31 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -120,6 +120,24 @@ jobs:
           name: codecov-umbrella
           fail_ci_if_error: false
 
+  performance:
+    name: Performance Scale Smoke
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    if: github.repository == 'apache/dubbo-kubernetes'
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+          cache-dependency-path: go.sum
+
+      - name: Exercise representative xDS push scale
+        run: make benchmark-smoke
+
   build:
     name: Build (${{ matrix.goos }}/${{ matrix.goarch }})
     runs-on: ubuntu-latest
diff --git a/PERFORMANCE.md b/PERFORMANCE.md
new file mode 100644
index 00000000..d20b631b
--- /dev/null
+++ b/PERFORMANCE.md
@@ -0,0 +1,115 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+-->
+
+# Performance Engineering
+
+The performance suite keeps two layers separate:
+
+1. Control-plane Go benchmarks measure indexing, client selection, and xDS
+   response generation without Kubernetes or network noise.
+2. Cluster load tests measure end-to-end request latency and resource usage.
+   Those results must identify the cluster, traffic generator, workload, and
+   observability setup; they must not be compared directly with Go benchmark
+   timings.
+
+## Control-plane scale matrix
+
+`BenchmarkProxylessPushScale` exercises the real targeted endpoint-update path:
+
+```text
+clientsForPush -> pushConnection -> EDS generation -> DiscoveryResponse send
+```
+
+Every synthetic proxyless connection watches every generated service. A
+single service endpoint update therefore selects every connection and emits
+one EDS response per connection. The matrix changes one dimension at a time
+from the `100 services / 10 endpoints / 100 connections` baseline:
+
+| Dimension | Cases |
+|---|---|
+| Services | 10, 100, 1,000 |
+| Endpoints per service | 1, 10, 100 |
+| Connections | 10, 100, 1,000 |
+
+`BenchmarkProxylessFullPushScale` uses the same dimensions but forces a full
+push that emits every watched service. Its baseline uses 10 connections, and
+its largest cases cap the generated work at 100,000 endpoint copies per
+operation:
+
+| Dimension | Cases |
+|---|---|
+| Services | 10, 100, 1,000 |
+| Endpoints per service | 1, 10, 100 |
+| Connections | 1, 10, 100 |
+
+Both benchmarks report `ns/op`, `B/op`, `allocs/op`, the three scale values,
+and the number of generated endpoint copies per operation.
+`ns/op` is the serial cost of completing one synthetic push wave on the
+benchmark CPU. Production pushes may execute concurrently, so this number is
+for regression and scaling comparisons rather than a direct production SLO.
+
+The existing `BenchmarkClientsForPushProxylessTargetedScale` separately
+measures connection selection through 100,000 connected clients, while
+`BenchmarkKRTFetch` measures indexed and label-scan lookups over 10,000
+workloads.
+
+## Running benchmarks
+
+Run the representative targeted and full-push scale cases used by pull-request
+CI:
+
+```bash
+make benchmark-smoke
+```
+
+Run the complete matrix with repeated samples:
+
+```bash
+make benchmark BENCHMARK_COUNT=5 BENCHMARK_TIME=1s BENCHMARK_CPU=1
+```
+
+The Make variables can narrow the benchmark expression or adjust measurement
+time and CPU count:
+
+```bash
+make benchmark \
+  BENCHMARK_PATTERN=BenchmarkProxylessPushScale \
+  BENCHMARK_COUNT=10 \
+  BENCHMARK_TIME=2s \
+  BENCHMARK_CPU=1
+```
+
+Use the same Go version, CPU model, power mode, `BENCHMARK_CPU`, and background
+load for baseline and candidate runs. Keep at least five samples and compare
+them with `benchstat`; do not draw a capacity conclusion from one run.
+
+## Continuous evidence
+
+The main CI workflow runs `make benchmark-smoke` to catch fixture failures and
+gross scale regressions on every change. The scheduled `Performance
+Benchmarks` workflow runs five samples of the complete matrix each week and
+retains the raw output plus runner details for 30 days. Shared runner variance
+makes a universal absolute latency threshold unreliable; compare like-for-like
+runs and investigate changes in both time and allocations.
+
+## Cluster load tests
+
+Use `samples/httpbin/httpbin.yaml` as the minimal data-plane target and the
+Prometheus sample under `samples/addons/` for control-plane metrics. Record at
+least request rate, p50/p95/p99 latency, error rate, CPU, memory, connected xDS
+clients, services, and endpoints. Change one scale dimension at a time and
+include warm-up, steady-state, and configuration-churn phases in the result.
diff --git a/README.md b/README.md
index de489cf8..f75b9413 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ Dubbo inherent mesh implemented for the underlying cluster 
management platform c
 
 > [!WARNING]
 > Current version is in the **Alpha** phase. The `0.5.0` release will be the 
 > first **RC** phase.
-> 
+>
 Dubbo’s control plane provides an abstraction layer over the underlying 
cluster management platform.
 
 Dubbo component composition:
@@ -38,6 +38,11 @@ Projects are distributed across the code directory 
repositories:
 
 Refer to [CONTRIBUTING.md](./CONTRIBUTING.md)
 
+## Performance
+
+See [PERFORMANCE.md](./PERFORMANCE.md) for the control-plane scale matrix,
+repeatable benchmark commands, CI evidence, and cluster load-test methodology.
+
 ## License
 
 Apache License 2.0, see 
[LICENSE](https://github.com/apache/dubbo-kubernetes/blob/master/LICENSE).
diff --git a/dubbod/discovery/pkg/model/authentication_resolution_test.go 
b/dubbod/discovery/pkg/model/authentication_resolution_test.go
new file mode 100644
index 00000000..eb65abd0
--- /dev/null
+++ b/dubbod/discovery/pkg/model/authentication_resolution_test.go
@@ -0,0 +1,293 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 model
+
+import (
+       "testing"
+
+       "github.com/apache/dubbo-kubernetes/pkg/config"
+       "github.com/apache/dubbo-kubernetes/pkg/config/schema/gvk"
+       security "github.com/kdubbo/api/security/v1alpha3"
+       typev1alpha3 "github.com/kdubbo/api/type/v1alpha3"
+)
+
+func selectorFromLabels(labels map[string]string) 
*typev1alpha3.WorkloadSelector {
+       if len(labels) == 0 {
+               return nil
+       }
+       return &typev1alpha3.WorkloadSelector{MatchLabels: labels}
+}
+
+// peerAuth builds a PeerAuthentication config. A nil selector produces a
+// namespace/mesh-level policy; a non-nil one produces a workload-level policy.
+func peerAuth(namespace, name string, selector map[string]string, mtls 
security.PeerAuthentication_MutualTLS_Mode, portLevel 
map[uint32]security.PeerAuthentication_MutualTLS_Mode) config.Config {
+       spec := &security.PeerAuthentication{}
+       if mtls != security.PeerAuthentication_MutualTLS_UNSET {
+               spec.Mtls = &security.PeerAuthentication_MutualTLS{Mode: mtls}
+       }
+       if sel := selectorFromLabels(selector); sel != nil {
+               spec.Selector = sel
+       }
+       if len(portLevel) > 0 {
+               spec.PortLevelMtls = 
map[uint32]*security.PeerAuthentication_MutualTLS{}
+               for port, mode := range portLevel {
+                       spec.PortLevelMtls[port] = 
&security.PeerAuthentication_MutualTLS{Mode: mode}
+               }
+       }
+       return config.Config{
+               Meta: config.Meta{GroupVersionKind: gvk.PeerAuthentication, 
Name: name, Namespace: namespace},
+               Spec: spec,
+       }
+}
+
+func newPolicies(rootNamespace string, configs ...config.Config) 
*AuthenticationPolicies {
+       policy := &AuthenticationPolicies{
+               requestAuthentications: map[string][]config.Config{},
+               authorizationPolicies:  map[string][]config.Config{},
+               peerAuthentications:    map[string][]config.Config{},
+               globalMutualTLSMode:    MTLSUnknown,
+               rootNamespace:          rootNamespace,
+               namespaceMutualTLSMode: map[string]MutualTLSMode{},
+       }
+       policy.addPeerAuthentication(configs)
+       return policy
+}
+
+func TestConvertToMutualTLSModeAllModes(t *testing.T) {
+       cases := map[security.PeerAuthentication_MutualTLS_Mode]MutualTLSMode{
+               security.PeerAuthentication_MutualTLS_DISABLE:    MTLSDisable,
+               security.PeerAuthentication_MutualTLS_PERMISSIVE: 
MTLSPermissive,
+               security.PeerAuthentication_MutualTLS_STRICT:     MTLSStrict,
+               security.PeerAuthentication_MutualTLS_UNSET:      MTLSUnknown,
+       }
+       for mode, want := range cases {
+               if got := ConvertToMutualTLSMode(mode); got != want {
+                       t.Errorf("ConvertToMutualTLSMode(%v) = %v, want %v", 
mode, got, want)
+               }
+       }
+}
+
+func TestEffectiveMutualTLSModePrecedence(t *testing.T) {
+       const root = "dubbo-system"
+       const ns = "app"
+       labels := map[string]string{"app": "httpbin"}
+
+       tests := []struct {
+               name    string
+               configs []config.Config
+               labels  map[string]string
+               port    uint32
+               want    MutualTLSMode
+       }{
+               {
+                       name: "workload selector beats namespace default",
+                       configs: []config.Config{
+                               peerAuth(ns, "ns-default", nil, 
security.PeerAuthentication_MutualTLS_PERMISSIVE, nil),
+                               peerAuth(ns, "wl", labels, 
security.PeerAuthentication_MutualTLS_STRICT, nil),
+                       },
+                       labels: labels,
+                       port:   8080,
+                       want:   MTLSStrict,
+               },
+               {
+                       name: "namespace default beats mesh global",
+                       configs: []config.Config{
+                               peerAuth(root, "mesh", nil, 
security.PeerAuthentication_MutualTLS_STRICT, nil),
+                               peerAuth(ns, "ns-default", nil, 
security.PeerAuthentication_MutualTLS_DISABLE, nil),
+                       },
+                       labels: labels,
+                       port:   8080,
+                       want:   MTLSDisable,
+               },
+               {
+                       name: "mesh global applies when namespace has none",
+                       configs: []config.Config{
+                               peerAuth(root, "mesh", nil, 
security.PeerAuthentication_MutualTLS_STRICT, nil),
+                       },
+                       labels: labels,
+                       port:   8080,
+                       want:   MTLSStrict,
+               },
+               {
+                       name: "port level override wins over workload spec 
level",
+                       configs: []config.Config{
+                               peerAuth(ns, "wl", labels, 
security.PeerAuthentication_MutualTLS_STRICT,
+                                       
map[uint32]security.PeerAuthentication_MutualTLS_Mode{8080: 
security.PeerAuthentication_MutualTLS_DISABLE}),
+                       },
+                       labels: labels,
+                       port:   8080,
+                       want:   MTLSDisable,
+               },
+               {
+                       name: "port level ignored for a different port",
+                       configs: []config.Config{
+                               peerAuth(ns, "wl", labels, 
security.PeerAuthentication_MutualTLS_STRICT,
+                                       
map[uint32]security.PeerAuthentication_MutualTLS_Mode{9090: 
security.PeerAuthentication_MutualTLS_DISABLE}),
+                       },
+                       labels: labels,
+                       port:   8080,
+                       want:   MTLSStrict,
+               },
+               {
+                       name:    "no policy resolves to unknown",
+                       configs: nil,
+                       labels:  labels,
+                       port:    8080,
+                       want:    MTLSUnknown,
+               },
+               {
+                       name: "empty namespace falls through to mesh global",
+                       configs: []config.Config{
+                               peerAuth(root, "mesh", nil, 
security.PeerAuthentication_MutualTLS_PERMISSIVE, nil),
+                       },
+                       labels: labels,
+                       port:   0,
+                       want:   MTLSPermissive,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       namespace := ns
+                       if tt.name == "empty namespace falls through to mesh 
global" {
+                               namespace = ""
+                       }
+                       policy := newPolicies(root, tt.configs...)
+                       if got := policy.EffectiveMutualTLSMode(namespace, 
tt.labels, tt.port); got != tt.want {
+                               t.Fatalf("EffectiveMutualTLSMode = %v, want 
%v", got, tt.want)
+                       }
+               })
+       }
+}
+
+func TestEffectiveMutualTLSModeRootNamespaceWorkloadSelector(t *testing.T) {
+       const root = "dubbo-system"
+       labels := map[string]string{"app": "httpbin"}
+       // A workload-selector PeerAuthentication living in the root namespace 
applies
+       // to matching workloads in other namespaces when their own namespace 
has no
+       // matching policy.
+       policy := newPolicies(root,
+               peerAuth(root, "root-wl", labels, 
security.PeerAuthentication_MutualTLS_STRICT, nil),
+       )
+       if got := policy.EffectiveMutualTLSMode("app", labels, 8080); got != 
MTLSStrict {
+               t.Fatalf("effective mode = %v, want MTLSStrict (root ns 
workload selector)", got)
+       }
+       // A workload in the same namespaces whose labels do not match falls 
through
+       // to unknown.
+       if got := policy.EffectiveMutualTLSMode("app", map[string]string{"app": 
"other"}, 8080); got != MTLSUnknown {
+               t.Fatalf("non-matching mode = %v, want MTLSUnknown", got)
+       }
+}
+
+func TestEffectiveMutualTLSModeNilPolicy(t *testing.T) {
+       var policy *AuthenticationPolicies
+       if got := policy.EffectiveMutualTLSMode("app", nil, 80); got != 
MTLSUnknown {
+               t.Fatalf("nil policy mode = %v, want MTLSUnknown", got)
+       }
+}
+
+func TestNamespaceLevelInheritsGlobalWhenUnset(t *testing.T) {
+       const root = "dubbo-system"
+       // A namespace-level PeerAuthentication with no mtls mode (UNSET) 
inherits
+       // the mesh-global mode rather than resetting to unknown.
+       policy := newPolicies(root,
+               peerAuth(root, "mesh", nil, 
security.PeerAuthentication_MutualTLS_STRICT, nil),
+               peerAuth("app", "ns-unset", nil, 
security.PeerAuthentication_MutualTLS_UNSET, nil),
+       )
+       if got := policy.namespaceMutualTLSMode["app"]; got != MTLSStrict {
+               t.Fatalf("inherited namespace mode = %v, want MTLSStrict", got)
+       }
+}
+
+func TestDuplicateNamespaceLevelPeerAuthIgnored(t *testing.T) {
+       const ns = "app"
+       // Two namespace-level policies in the same namespace: the first by 
creation
+       // order wins and the second must be ignored.
+       first := peerAuth(ns, "first", nil, 
security.PeerAuthentication_MutualTLS_STRICT, nil)
+       second := peerAuth(ns, "second", nil, 
security.PeerAuthentication_MutualTLS_DISABLE, nil)
+       policy := newPolicies("dubbo-system", first, second)
+
+       if got := policy.EffectiveMutualTLSMode(ns, nil, 0); got != MTLSStrict {
+               t.Fatalf("effective mode = %v, want MTLSStrict (first policy 
wins)", got)
+       }
+}
+
+func TestPeerAuthenticationModeForPort(t *testing.T) {
+       specStrict := &security.PeerAuthentication{
+               Mtls: &security.PeerAuthentication_MutualTLS{Mode: 
security.PeerAuthentication_MutualTLS_STRICT},
+               PortLevelMtls: 
map[uint32]*security.PeerAuthentication_MutualTLS{
+                       8080: {Mode: 
security.PeerAuthentication_MutualTLS_DISABLE},
+                       9090: {Mode: 
security.PeerAuthentication_MutualTLS_UNSET},
+               },
+       }
+
+       tests := []struct {
+               name string
+               spec *security.PeerAuthentication
+               port uint32
+               want MutualTLSMode
+       }{
+               {"nil spec", nil, 8080, MTLSUnknown},
+               {"port level override", specStrict, 8080, MTLSDisable},
+               {"port level unset falls back to spec", specStrict, 9090, 
MTLSStrict},
+               {"unmapped port uses spec level", specStrict, 7070, MTLSStrict},
+               {"port zero ignores port level", specStrict, 0, MTLSStrict},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       if got := peerAuthenticationModeForPort(tt.spec, 
tt.port); got != tt.want {
+                               t.Fatalf("peerAuthenticationModeForPort = %v, 
want %v", got, tt.want)
+                       }
+               })
+       }
+}
+
+func TestSelectorMatchesWorkload(t *testing.T) {
+       tests := []struct {
+               name     string
+               selector map[string]string
+               labels   map[string]string
+               want     bool
+       }{
+               {"empty selector matches all", nil, map[string]string{"app": 
"x"}, true},
+               {"empty labels with selector fails", map[string]string{"app": 
"x"}, nil, false},
+               {"exact match", map[string]string{"app": "x"}, 
map[string]string{"app": "x", "v": "1"}, true},
+               {"value mismatch", map[string]string{"app": "x"}, 
map[string]string{"app": "y"}, false},
+               {"missing key", map[string]string{"app": "x", "tier": "t"}, 
map[string]string{"app": "x"}, false},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       if got := 
selectorMatchesWorkload(selectorFromLabels(tt.selector), tt.labels); got != 
tt.want {
+                               t.Fatalf("selectorMatchesWorkload = %v, want 
%v", got, tt.want)
+                       }
+               })
+       }
+}
+
+func TestHasPeerAuthSelector(t *testing.T) {
+       if hasPeerAuthSelector(nil) {
+               t.Fatal("nil spec should have no selector")
+       }
+       noSel := &security.PeerAuthentication{}
+       if hasPeerAuthSelector(noSel) {
+               t.Fatal("spec without selector should report false")
+       }
+       withSel := &security.PeerAuthentication{Selector: 
selectorFromLabels(map[string]string{"app": "x"})}
+       if !hasPeerAuthSelector(withSel) {
+               t.Fatal("spec with selector should report true")
+       }
+}
diff --git a/dubbod/discovery/pkg/xds/ads_benchmark_test.go 
b/dubbod/discovery/pkg/xds/ads_benchmark_test.go
new file mode 100644
index 00000000..dd1595a3
--- /dev/null
+++ b/dubbod/discovery/pkg/xds/ads_benchmark_test.go
@@ -0,0 +1,346 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 xds
+
+import (
+       "fmt"
+       "io"
+       "testing"
+       "time"
+
+       "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
+       v1 "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/xds/v1"
+       "github.com/apache/dubbo-kubernetes/pkg/config/host"
+       "github.com/apache/dubbo-kubernetes/pkg/config/mesh"
+       "github.com/apache/dubbo-kubernetes/pkg/config/mesh/meshwatcher"
+       "github.com/apache/dubbo-kubernetes/pkg/config/protocol"
+       "github.com/apache/dubbo-kubernetes/pkg/config/schema/kind"
+       "github.com/apache/dubbo-kubernetes/pkg/kube/krt"
+       dubbolog "github.com/apache/dubbo-kubernetes/pkg/log"
+       "github.com/apache/dubbo-kubernetes/pkg/util/sets"
+       endpoint "github.com/kdubbo/xds-api/endpoint/v1"
+       discovery "github.com/kdubbo/xds-api/service/discovery/v1"
+)
+
+type proxylessPushScale struct {
+       services            int
+       endpointsPerService int
+       connections         int
+}
+
+func (s proxylessPushScale) name() string {
+       return fmt.Sprintf(
+               "services=%d/endpoints_per_service=%d/connections=%d",
+               s.services,
+               s.endpointsPerService,
+               s.connections,
+       )
+}
+
+var proxylessPushScaleMatrix = []proxylessPushScale{
+       {services: 10, endpointsPerService: 10, connections: 100},
+       {services: 100, endpointsPerService: 10, connections: 100},
+       {services: 1000, endpointsPerService: 10, connections: 100},
+       {services: 100, endpointsPerService: 1, connections: 100},
+       {services: 100, endpointsPerService: 100, connections: 100},
+       {services: 100, endpointsPerService: 10, connections: 10},
+       {services: 100, endpointsPerService: 10, connections: 1000},
+}
+
+var proxylessFullPushScaleMatrix = []proxylessPushScale{
+       {services: 10, endpointsPerService: 10, connections: 10},
+       {services: 100, endpointsPerService: 10, connections: 10},
+       {services: 1000, endpointsPerService: 10, connections: 10},
+       {services: 100, endpointsPerService: 1, connections: 10},
+       {services: 100, endpointsPerService: 100, connections: 10},
+       {services: 100, endpointsPerService: 10, connections: 1},
+       {services: 100, endpointsPerService: 10, connections: 100},
+}
+
+var benchmarkPushResponse *discovery.DiscoveryResponse
+
+func BenchmarkProxylessPushScale(b *testing.B) {
+       for _, scale := range proxylessPushScaleMatrix {
+               b.Run(scale.name(), func(b *testing.B) {
+                       fixture := newProxylessPushBenchmarkFixture(b, scale)
+                       b.ReportAllocs()
+                       b.ResetTimer()
+
+                       for i := 0; i < b.N; i++ {
+                               responses, resources, lastResponse := 
fixture.pushOnce(b)
+                               if responses != scale.connections || resources 
!= scale.connections {
+                                       b.Fatalf(
+                                               "push produced 
responses/resources = %d/%d, want %d/%d",
+                                               responses,
+                                               resources,
+                                               scale.connections,
+                                               scale.connections,
+                                       )
+                               }
+                               benchmarkPushResponse = lastResponse
+                       }
+                       b.StopTimer()
+                       b.ReportMetric(float64(scale.services), "services")
+                       b.ReportMetric(float64(scale.endpointsPerService), 
"endpoints/service")
+                       b.ReportMetric(float64(scale.connections), 
"connections")
+                       
b.ReportMetric(float64(scale.endpointsPerService*scale.connections), 
"endpoint-copies/op")
+               })
+       }
+}
+
+func BenchmarkProxylessFullPushScale(b *testing.B) {
+       for _, scale := range proxylessFullPushScaleMatrix {
+               b.Run(scale.name(), func(b *testing.B) {
+                       fixture := newProxylessPushBenchmarkFixture(b, scale)
+                       fixture.request.Full = true
+                       fixture.request.Forced = true
+                       fixture.request.ConfigsUpdated = nil
+                       wantResources := scale.services * scale.connections
+                       b.ReportAllocs()
+                       b.ResetTimer()
+
+                       for i := 0; i < b.N; i++ {
+                               responses, resources, lastResponse := 
fixture.pushOnce(b)
+                               if responses != scale.connections || resources 
!= wantResources {
+                                       b.Fatalf(
+                                               "full push produced 
responses/resources = %d/%d, want %d/%d",
+                                               responses,
+                                               resources,
+                                               scale.connections,
+                                               wantResources,
+                                       )
+                               }
+                               benchmarkPushResponse = lastResponse
+                       }
+                       b.StopTimer()
+                       b.ReportMetric(float64(scale.services), "services")
+                       b.ReportMetric(float64(scale.endpointsPerService), 
"endpoints/service")
+                       b.ReportMetric(float64(scale.connections), 
"connections")
+                       
b.ReportMetric(float64(scale.services*scale.endpointsPerService*scale.connections),
 "endpoint-copies/op")
+               })
+       }
+}
+
+func TestProxylessPushBenchmarkFixture(t *testing.T) {
+       scale := proxylessPushScale{services: 3, endpointsPerService: 2, 
connections: 4}
+       fixture := newProxylessPushBenchmarkFixture(t, scale)
+
+       responses, resources, lastResponse := fixture.pushOnce(t)
+       if responses != scale.connections || resources != scale.connections {
+               t.Fatalf(
+                       "push produced responses/resources = %d/%d, want %d/%d",
+                       responses,
+                       resources,
+                       scale.connections,
+                       scale.connections,
+               )
+       }
+       if lastResponse == nil || len(lastResponse.Resources) != 1 {
+               t.Fatalf("last response has %d resources, want 1", 
len(lastResponse.GetResources()))
+       }
+       assignment := &endpoint.ClusterLoadAssignment{}
+       if err := lastResponse.Resources[0].UnmarshalTo(assignment); err != nil 
{
+               t.Fatalf("unmarshal endpoint assignment: %v", err)
+       }
+       gotEndpoints := 0
+       for _, locality := range assignment.Endpoints {
+               gotEndpoints += len(locality.LbEndpoints)
+       }
+       if gotEndpoints != scale.endpointsPerService {
+               t.Fatalf("endpoint assignment has %d endpoints, want %d", 
gotEndpoints, scale.endpointsPerService)
+       }
+}
+
+type proxylessPushBenchmarkFixture struct {
+       server      *DiscoveryServer
+       request     *model.PushRequest
+       stream      *fakeADSStream
+       connections int
+}
+
+func newProxylessPushBenchmarkFixture(tb testing.TB, scale proxylessPushScale) 
*proxylessPushBenchmarkFixture {
+       tb.Helper()
+       if scale.services <= 0 || scale.endpointsPerService <= 0 || 
scale.connections <= 0 {
+               tb.Fatalf("benchmark scale values must all be positive: %+v", 
scale)
+       }
+       silenceProxylessPushBenchmarkLogs(tb)
+
+       services := make([]*model.Service, 0, scale.services)
+       serviceByHostname := make(map[host.Name]*model.Service, scale.services)
+       clusterNames := make([]string, 0, scale.services)
+       for serviceIndex := 0; serviceIndex < scale.services; serviceIndex++ {
+               serviceName := fmt.Sprintf("svc-%d", serviceIndex)
+               hostname := host.Name(serviceName + ".app.svc.cluster.local")
+               service := &model.Service{
+                       Hostname: hostname,
+                       Ports: model.PortList{{
+                               Name:     "grpc",
+                               Port:     80,
+                               Protocol: protocol.GRPC,
+                       }},
+                       Attributes: model.ServiceAttributes{
+                               Name:      serviceName,
+                               Namespace: "app",
+                       },
+               }
+               services = append(services, service)
+               serviceByHostname[hostname] = service
+               clusterNames = append(clusterNames, 
model.BuildSubsetKey(model.TrafficDirectionOutbound, "", hostname, 80))
+       }
+
+       env := model.NewEnvironment()
+       env.ConfigStore = testConfigStore{}
+       env.ServiceDiscovery = benchmarkServiceDiscovery{services: services, 
byHostname: serviceByHostname}
+       env.Watcher = 
meshwatcher.ConfigAdapter(krt.NewStatic(&meshwatcher.MeshConfigResource{
+               MeshConfig: mesh.DefaultMeshConfig(),
+       }, true))
+       env.Init()
+
+       for serviceIndex, service := range services {
+               endpoints := make([]*model.DubboEndpoint, 0, 
scale.endpointsPerService)
+               for endpointIndex := 0; endpointIndex < 
scale.endpointsPerService; endpointIndex++ {
+                       endpoints = append(endpoints, &model.DubboEndpoint{
+                               Addresses:       
[]string{benchmarkEndpointAddress(serviceIndex, endpointIndex)},
+                               EndpointPort:    80,
+                               ServicePortName: "grpc",
+                               HealthStatus:    model.Healthy,
+                               Namespace:       "app",
+                               WorkloadName:    fmt.Sprintf("%s-%d", 
service.Attributes.Name, endpointIndex),
+                       })
+               }
+               env.EndpointIndex.UpdateServiceEndpoints(model.ShardKey{}, 
string(service.Hostname), "app", endpoints, false)
+       }
+
+       push := model.NewPushContext()
+       push.PushVersion = "benchmark"
+       push.InitContext(env, nil, nil)
+       env.SetPushContext(push)
+
+       server := NewDiscoveryServer(env, nil, nil)
+       server.Generators[v1.EndpointType] = &EdsGenerator{
+               Cache:         model.DisabledCache{},
+               EndpointIndex: env.EndpointIndex,
+       }
+       stream := newFakeADSStream()
+       for connectionIndex := 0; connectionIndex < scale.connections; 
connectionIndex++ {
+               connectionID := fmt.Sprintf("proxyless-%d", connectionIndex)
+               connection := newConnection("127.0.0.1:26010", stream)
+               connection.SetID(connectionID)
+               connection.s = server
+               connection.proxy = &model.Proxy{
+                       ID:              connectionID,
+                       Type:            model.Proxyless,
+                       ConfigNamespace: "app",
+                       Metadata: &model.NodeMetadata{
+                               Generator: "grpc",
+                               Namespace: "app",
+                       },
+                       WatchedResources: map[string]*model.WatchedResource{
+                               v1.EndpointType: {
+                                       TypeUrl:       v1.EndpointType,
+                                       ResourceNames: 
sets.New(clusterNames...),
+                               },
+                       },
+                       LastPushContext: push,
+                       LastPushTime:    time.Now(),
+               }
+               server.adsClients[connectionID] = connection
+       }
+
+       targetService := services[0]
+       return &proxylessPushBenchmarkFixture{
+               server: server,
+               request: &model.PushRequest{
+                       Push:   push,
+                       Start:  time.Now(),
+                       Reason: model.NewReasonStats(model.EndpointUpdate),
+                       ConfigsUpdated: sets.New(model.ConfigKey{
+                               Kind:      kind.Service,
+                               Name:      string(targetService.Hostname),
+                               Namespace: targetService.Attributes.Namespace,
+                       }),
+               },
+               stream:      stream,
+               connections: scale.connections,
+       }
+}
+
+func silenceProxylessPushBenchmarkLogs(tb testing.TB) {
+       tb.Helper()
+       for _, scopeName := range []string{"ads", "model"} {
+               scope := dubbolog.FindScope(scopeName)
+               if scope == nil {
+                       continue
+               }
+               previousOutput := scope.GetOutput()
+               scope.SetOutput(io.Discard)
+               tb.Cleanup(func() {
+                       scope.SetOutput(previousOutput)
+               })
+       }
+}
+
+func (f *proxylessPushBenchmarkFixture) pushOnce(tb testing.TB) (int, int, 
*discovery.DiscoveryResponse) {
+       tb.Helper()
+       clients := f.server.clientsForPush(f.request)
+       if len(clients) != f.connections {
+               tb.Fatalf("clientsForPush() returned %d clients, want %d", 
len(clients), f.connections)
+       }
+
+       responses := 0
+       resources := 0
+       var lastResponse *discovery.DiscoveryResponse
+       for _, connection := range clients {
+               if err := f.server.pushConnection(connection, 
&Event{pushRequest: f.request}); err != nil {
+                       tb.Fatalf("pushConnection(%s): %v", connection.ID(), 
err)
+               }
+               select {
+               case response := <-f.stream.sendCh:
+                       responses++
+                       resources += len(response.Resources)
+                       lastResponse = response
+               default:
+                       tb.Fatalf("pushConnection(%s) did not send a response", 
connection.ID())
+               }
+       }
+       return responses, resources, lastResponse
+}
+
+func benchmarkEndpointAddress(serviceIndex, endpointIndex int) string {
+       return fmt.Sprintf(
+               "10.%d.%d.%d",
+               (serviceIndex/250)%250+1,
+               serviceIndex%250+1,
+               endpointIndex%250+1,
+       )
+}
+
+type benchmarkServiceDiscovery struct {
+       services   []*model.Service
+       byHostname map[host.Name]*model.Service
+}
+
+func (d benchmarkServiceDiscovery) Services() []*model.Service {
+       return d.services
+}
+
+func (d benchmarkServiceDiscovery) GetService(hostname host.Name) 
*model.Service {
+       return d.byHostname[hostname]
+}
+
+func (benchmarkServiceDiscovery) GetProxyServiceTargets(*model.Proxy) 
[]model.ServiceTarget {
+       return nil
+}
diff --git a/tools/make/test.mk b/tools/make/test.mk
index 235b3a05..51796d21 100644
--- a/tools/make/test.mk
+++ b/tools/make/test.mk
@@ -15,6 +15,12 @@
 
 ##@ Test
 
+BENCHMARK_PACKAGES ?= ./pkg/kube/krt ./dubbod/discovery/pkg/xds
+BENCHMARK_PATTERN  ?= 
Benchmark(KRTFetch|ClientsForPushProxylessTargeted|Proxyless(Push|FullPush)Scale)
+BENCHMARK_TIME     ?= 1s
+BENCHMARK_COUNT    ?= 3
+BENCHMARK_CPU      ?= 1
+
 .PHONY: test
 test: ## Run unit tests (GOTESTFLAGS defaults to -race).
        go test $(GOTESTFLAGS) ./...
@@ -26,3 +32,11 @@ test-coverage: ## Run unit tests and write coverage.txt.
 .PHONY: test-e2e
 test-e2e: ## Run the kind smoke test (needs docker, kind, kubectl, helm; knobs 
in tests/e2e/run.sh).
        bash tests/e2e/run.sh
+
+.PHONY: benchmark
+benchmark: ## Run repeatable control-plane microbenchmarks (override 
BENCHMARK_* variables).
+       go test $(BENCHMARK_PACKAGES) -run '^$$' -bench '$(BENCHMARK_PATTERN)' 
-benchmem -benchtime=$(BENCHMARK_TIME) -count=$(BENCHMARK_COUNT) 
-cpu=$(BENCHMARK_CPU)
+
+.PHONY: benchmark-smoke
+benchmark-smoke: ## Run representative targeted/full xDS scale cases as a 
correctness gate.
+       go test ./dubbod/discovery/pkg/xds -run 
'^TestProxylessPushBenchmarkFixture$$' -bench 
'^BenchmarkProxyless(Push|FullPush)Scale$$/^services=100$$/^endpoints_per_service=10$$/^connections=100$$'
 -benchmem -benchtime=1x -count=1 -cpu=1

Reply via email to