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

Alanxtl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git


The following commit(s) were added to refs/heads/develop by this push:
     new f7f350cfe perf(common): reduce URL CloneWithFilter and MergeURL params 
copying (#3403)
f7f350cfe is described below

commit f7f350cfe4fbcfe35d609306a48b4f72de5b923d
Author: ヴァニラシ <[email protected]>
AuthorDate: Sat Jun 13 13:49:54 2026 +0800

    perf(common): reduce URL CloneWithFilter and MergeURL params copying (#3403)
---
 common/url.go      | 196 +++++++++++++++++++++++++++++++++++------------------
 common/url_test.go |  64 +++++++++++++++++
 2 files changed, 195 insertions(+), 65 deletions(-)

diff --git a/common/url.go b/common/url.go
index d6a800e22..462f4ee36 100644
--- a/common/url.go
+++ b/common/url.go
@@ -678,23 +678,34 @@ func (c *URL) GetParams() url.Values {
        return c.CopyParams()
 }
 
-// CopyParams returns a deep copy of params.
-func (c *URL) CopyParams() url.Values {
-       c.paramsLock.RLock()
-       defer c.paramsLock.RUnlock()
-
-       if c.params == nil {
+func copyURLValues(src url.Values) url.Values {
+       if src == nil {
                return nil
        }
 
-       params := make(url.Values, len(c.params))
-       for k, vs := range c.params {
+       dst := make(url.Values, len(src))
+       for k, vs := range src {
                copied := make([]string, len(vs))
                copy(copied, vs)
-               params[k] = copied
+               dst[k] = copied
        }
 
-       return params
+       return dst
+}
+
+func valuesHasNonDefaultParam(values url.Values, key string) bool {
+       if len(values) == 0 {
+               return false
+       }
+       return values.Get(key) != ""
+}
+
+// CopyParams returns a deep copy of params.
+func (c *URL) CopyParams() url.Values {
+       c.paramsLock.RLock()
+       defer c.paramsLock.RUnlock()
+
+       return copyURLValues(c.params)
 }
 
 // GetParamAndDecoded gets values and decode
@@ -880,41 +891,53 @@ func (c *URL) ToMap() map[string]string {
 func (c *URL) MergeURL(anotherUrl *URL) *URL {
        // After Clone, it is a new URL that there is no thread safe issue.
        mergedURL := c.Clone()
-       params := mergedURL.GetParams()
-       // iterator the anotherUrl if c not have the key ,merge in
-       // anotherUrl usually will not changed. so change RangeParams to 
GetParams to avoid the string value copy.// Group get group
-       for key, value := range anotherUrl.GetParams() {
-               if _, ok := mergedURL.GetNonDefaultParam(key); !ok {
-                       if len(value) > 0 {
-                               params[key] = make([]string, len(value))
-                               copy(params[key], value)
+       params := mergedURL.params
+       if params == nil {
+               params = url.Values{}
+               mergedURL.params = params
+       }
+       baseTimestamp := params.Get(constant.TimestampKey)
+       baseHasTimestamp := baseTimestamp != ""
+
+       func() {
+               anotherUrl.paramsLock.RLock()
+               defer anotherUrl.paramsLock.RUnlock()
+
+               // Merge params from anotherUrl under one read lock.
+               for key, value := range anotherUrl.params {
+                       if !valuesHasNonDefaultParam(params, key) {
+                               if len(value) > 0 {
+                                       params[key] = make([]string, len(value))
+                                       copy(params[key], value)
+                               }
                        }
                }
-       }
-
-       // remote timestamp
-       if v, ok := c.GetNonDefaultParam(constant.TimestampKey); !ok {
-               params[constant.RemoteTimestampKey] = []string{v}
-               params[constant.TimestampKey] = 
[]string{anotherUrl.GetParam(constant.TimestampKey, "")}
-       }
 
-       // finally execute methodConfigMergeFcn
-       mergedURL.Methods = make([]string, len(anotherUrl.Methods))
-       for i, method := range anotherUrl.Methods {
-               for _, paramKey := range []string{constant.LoadbalanceKey, 
constant.ClusterKey, constant.RetriesKey, constant.TimeoutKey} {
-                       if v := anotherUrl.GetParam(paramKey, ""); len(v) > 0 {
-                               params[paramKey] = []string{v}
-                       }
+               // remote timestamp
+               if !baseHasTimestamp {
+                       params[constant.RemoteTimestampKey] = 
[]string{baseTimestamp}
+                       params[constant.TimestampKey] = 
[]string{anotherUrl.params.Get(constant.TimestampKey)}
+               }
 
-                       methodsKey := "methods." + method + "." + paramKey
-                       // if len(mergedURL.GetParam(methodsKey, "")) == 0 {
-                       if v := anotherUrl.GetParam(methodsKey, ""); len(v) > 0 
{
-                               params[methodsKey] = []string{v}
+               // finally execute methodConfigMergeFcn
+               mergedURL.Methods = make([]string, len(anotherUrl.Methods))
+               for i, method := range anotherUrl.Methods {
+                       for _, paramKey := range 
[]string{constant.LoadbalanceKey, constant.ClusterKey, constant.RetriesKey, 
constant.TimeoutKey} {
+                               if v := anotherUrl.params.Get(paramKey); len(v) 
> 0 {
+                                       params[paramKey] = []string{v}
+                               }
+
+                               methodsKey := "methods." + method + "." + 
paramKey
+                               // if len(mergedURL.GetParam(methodsKey, "")) 
== 0 {
+                               if v := anotherUrl.params.Get(methodsKey); 
len(v) > 0 {
+                                       params[methodsKey] = []string{v}
+                               }
+                               // }
+                               mergedURL.Methods[i] = method
                        }
-                       // }
-                       mergedURL.Methods[i] = method
                }
-       }
+       }()
+
        // merge attributes
        anotherUrl.RangeAttributes(func(attrK string, attrV any) bool {
                if _, ok := mergedURL.GetAttribute(attrK); !ok {
@@ -931,7 +954,27 @@ func (c *URL) MergeURL(anotherUrl *URL) *URL {
 // excludeParams: the set of parameters to exclude from the cloned URL
 // reserveParams: the set of parameters to retain in the cloned URL
 func (c *URL) CloneWithFilter(excludeParams *gxset.HashSet, reserveParams 
[]string) *URL {
-       newURL := &URL{
+       newURL := c.newURLForClone()
+       newURL.params = c.copyFilteredParams(newURL.params, excludeParams, 
reserveParams)
+
+       // Copy attributes
+       c.RangeAttributes(
+               func(key string, value any) bool {
+                       newURL.SetAttribute(key, value)
+                       return true
+               },
+       )
+
+       // Copy SubURL if it exists
+       if c.SubURL != nil {
+               newURL.SubURL = c.SubURL.Clone()
+       }
+
+       return newURL
+}
+
+func (c *URL) newURLForClone() *URL {
+       return &URL{
                Protocol:     c.Protocol,
                Location:     c.Location,
                Ip:           c.Ip,
@@ -945,37 +988,60 @@ func (c *URL) CloneWithFilter(excludeParams 
*gxset.HashSet, reserveParams []stri
                attributes:   make(map[string]any),
                params:       url.Values{},
        }
+}
 
-       // Copy and filter params based on excludeParams or reserveParams
-       c.RangeParams(
-               func(key, value string) bool {
-                       // If the param is in excludeParams or not in 
reserveParams, skip it
-                       if excludeParams != nil && excludeParams.Contains(key) {
-                               return true
-                       }
-                       if len(reserveParams) > 0 && 
!slices.Contains(reserveParams, key) {
-                               return true
+func (c *URL) copyFilteredParams(params url.Values, excludeParams 
*gxset.HashSet, reserveParams []string) url.Values {
+       c.paramsLock.RLock()
+       defer c.paramsLock.RUnlock()
+
+       if excludeParams == nil && len(reserveParams) == 0 {
+               if len(c.params) > 8 {
+                       copiedParams := copyURLValues(c.params)
+                       if copiedParams != nil {
+                               return copiedParams
                        }
-                       // Set the param if it passes the filter
-                       newURL.SetParam(key, value)
-                       return true
-               },
-       )
+               }
+               for key, values := range c.params {
+                       copied := make([]string, len(values))
+                       copy(copied, values)
+                       params[key] = copied
+               }
+               return params
+       }
 
-       // Copy attributes
-       c.RangeAttributes(
-               func(key string, value any) bool {
-                       newURL.SetAttribute(key, value)
-                       return true
-               },
-       )
+       if capacity := paramsCapacityForClone(len(c.params), excludeParams, 
reserveParams); capacity > 8 {
+               params = make(url.Values, capacity)
+       }
 
-       // Copy SubURL if it exists
-       if c.SubURL != nil {
-               newURL.SubURL = c.SubURL.Clone()
+       for key, values := range c.params {
+               if shouldCopyParam(key, excludeParams, reserveParams) {
+                       copied := make([]string, len(values))
+                       copy(copied, values)
+                       params[key] = copied
+               }
        }
 
-       return newURL
+       return params
+}
+
+func paramsCapacityForClone(paramsLen int, excludeParams *gxset.HashSet, 
reserveParams []string) int {
+       if len(reserveParams) > 0 && len(reserveParams) < paramsLen {
+               paramsLen = len(reserveParams)
+       }
+       if excludeParams != nil {
+               paramsLen -= excludeParams.Size()
+       }
+       if paramsLen < 0 {
+               return 0
+       }
+       return paramsLen
+}
+
+func shouldCopyParam(key string, excludeParams *gxset.HashSet, reserveParams 
[]string) bool {
+       if excludeParams != nil && excludeParams.Contains(key) {
+               return false
+       }
+       return len(reserveParams) == 0 || slices.Contains(reserveParams, key)
 }
 
 // Clone will copy the URL
diff --git a/common/url_test.go b/common/url_test.go
index 9e56e22ac..1394914f6 100644
--- a/common/url_test.go
+++ b/common/url_test.go
@@ -1075,6 +1075,29 @@ func TestCloneWithParams(t *testing.T) {
        assert.Equal(t, []string{"method1", "method2"}, cloned.Methods)
 }
 
+func TestCloneWithFilterPreservesMultiValueParams(t *testing.T) {
+       u := &URL{}
+       u.SetParams(url.Values{
+               "group": {"a", "b"},
+               "drop":  {"drop-value"},
+               "other": {"other-value"},
+       })
+
+       excludeSet := gxset.NewSet("drop")
+       cloned := u.CloneWithFilter(excludeSet, []string{"group", "drop"})
+
+       clonedParams := cloned.GetParams()
+       assert.Equal(t, []string{"a", "b"}, clonedParams["group"])
+       assert.NotContains(t, clonedParams, "drop")
+       assert.NotContains(t, clonedParams, "other")
+
+       cloned.paramsLock.Lock()
+       cloned.params["group"][0] = "changed"
+       cloned.paramsLock.Unlock()
+
+       assert.Equal(t, []string{"a", "b"}, u.GetParams()["group"])
+}
+
 func TestURLCompare(t *testing.T) {
        u1, _ := NewURL("dubbo://127.0.0.1:20000/a.Service")
        u2, _ := NewURL("dubbo://127.0.0.1:20000/b.Service")
@@ -1359,6 +1382,47 @@ func TestMergeURLWithMethodParams(t *testing.T) {
        assert.Equal(t, "5000", 
mergedUrl.GetParam("methods.testMethod."+constant.TimeoutKey, ""))
 }
 
+func TestMergeURLPreservesMultiValueParams(t *testing.T) {
+       local := &URL{}
+       local.SetParams(url.Values{
+               "group": {"local-a", "local-b"},
+       })
+
+       remote := &URL{}
+       remote.SetParams(url.Values{
+               "group":   {"remote-a", "remote-b"},
+               "version": {"remote-v1", "remote-v2"},
+       })
+
+       merged := local.MergeURL(remote)
+       mergedParams := merged.GetParams()
+
+       assert.Equal(t, []string{"local-a", "local-b"}, mergedParams["group"])
+       assert.Equal(t, []string{"remote-v1", "remote-v2"}, 
mergedParams["version"])
+
+       remote.paramsLock.Lock()
+       remote.params["version"][0] = "changed"
+       remote.paramsLock.Unlock()
+
+       assert.Equal(t, []string{"remote-v1", "remote-v2"}, 
merged.GetParams()["version"])
+}
+
+func TestMergeURLUsesNonDefaultParamSemantics(t *testing.T) {
+       local := &URL{}
+       local.SetParams(url.Values{
+               "group": {"", "local-b"},
+       })
+
+       remote := &URL{}
+       remote.SetParams(url.Values{
+               "group": {"remote-a", "remote-b"},
+       })
+
+       merged := local.MergeURL(remote)
+
+       assert.Equal(t, []string{"remote-a", "remote-b"}, 
merged.GetParams()["group"])
+}
+
 func TestURLWithPathAlreadyHasSlash(t *testing.T) {
        u := NewURLWithOptions(WithPath("/com.test.Service"))
        assert.Equal(t, "/com.test.Service", u.Path)

Reply via email to