Skip to content

Commit 26ee5dc

Browse files
committed
Improve performance of per-app namespace scan
Signed-off-by: Jonathan Ogilvie <[email protected]>
1 parent 084f0b1 commit 26ee5dc

File tree

2 files changed

+127
-11
lines changed

2 files changed

+127
-11
lines changed

gitops-engine/pkg/cache/cluster.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,7 +1130,7 @@ func (c *clusterCache) IterateHierarchyV2(keys []kube.ResourceKey, action func(r
11301130
// First process the namespaces we have explicit keys for
11311131
for namespace, namespaceKeys := range keysPerNamespace {
11321132
nsNodes := c.nsIndex[namespace]
1133-
1133+
11341134
// Only use cross-namespace graph for namespaced resources that might have cluster parents
11351135
var graph map[kube.ResourceKey]map[types.UID]*Resource
11361136
if namespace != "" && len(clusterNodesByUID) > 0 {
@@ -1283,20 +1283,22 @@ func buildGraph(nsNodes map[kube.ResourceKey]*Resource) map[kube.ResourceKey]map
12831283
uidNodes, ok := nodesByUID[ownerRef.UID]
12841284
if ok {
12851285
for _, uidNode := range uidNodes {
1286+
// Cache ResourceKey() to avoid repeated expensive calls
1287+
uidNodeKey := uidNode.ResourceKey()
12861288
// Update the graph for this owner to include the child.
1287-
if _, ok := graph[uidNode.ResourceKey()]; !ok {
1288-
graph[uidNode.ResourceKey()] = make(map[types.UID]*Resource)
1289+
if _, ok := graph[uidNodeKey]; !ok {
1290+
graph[uidNodeKey] = make(map[types.UID]*Resource)
12891291
}
1290-
r, ok := graph[uidNode.ResourceKey()][childNode.Ref.UID]
1292+
r, ok := graph[uidNodeKey][childNode.Ref.UID]
12911293
if !ok {
1292-
graph[uidNode.ResourceKey()][childNode.Ref.UID] = childNode
1294+
graph[uidNodeKey][childNode.Ref.UID] = childNode
12931295
} else if r != nil {
12941296
// The object might have multiple children with the same UID (e.g. replicaset from apps and extensions group).
12951297
// It is ok to pick any object, but we need to make sure we pick the same child after every refresh.
12961298
key1 := r.ResourceKey()
12971299
key2 := childNode.ResourceKey()
12981300
if strings.Compare(key1.String(), key2.String()) > 0 {
1299-
graph[uidNode.ResourceKey()][childNode.Ref.UID] = childNode
1301+
graph[uidNodeKey][childNode.Ref.UID] = childNode
13001302
}
13011303
}
13021304
}
@@ -1339,17 +1341,19 @@ func buildGraphWithCrossNamespace(nsNodes map[kube.ResourceKey]*Resource, cluste
13391341
}
13401342
if ok {
13411343
for _, uidNode := range uidNodes {
1342-
if _, ok := graph[uidNode.ResourceKey()]; !ok {
1343-
graph[uidNode.ResourceKey()] = make(map[types.UID]*Resource)
1344+
// Cache ResourceKey() to avoid repeated expensive calls
1345+
uidNodeKey := uidNode.ResourceKey()
1346+
if _, ok := graph[uidNodeKey]; !ok {
1347+
graph[uidNodeKey] = make(map[types.UID]*Resource)
13441348
}
1345-
r, ok := graph[uidNode.ResourceKey()][childNode.Ref.UID]
1349+
r, ok := graph[uidNodeKey][childNode.Ref.UID]
13461350
if !ok {
1347-
graph[uidNode.ResourceKey()][childNode.Ref.UID] = childNode
1351+
graph[uidNodeKey][childNode.Ref.UID] = childNode
13481352
} else if r != nil {
13491353
key1 := r.ResourceKey()
13501354
key2 := childNode.ResourceKey()
13511355
if strings.Compare(key1.String(), key2.String()) > 0 {
1352-
graph[uidNode.ResourceKey()][childNode.Ref.UID] = childNode
1356+
graph[uidNodeKey][childNode.Ref.UID] = childNode
13531357
}
13541358
}
13551359
}

gitops-engine/pkg/cache/cluster_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,6 +2001,118 @@ func BenchmarkIterateHierarchyV2FromClusterScoped_25Percent_WithScan(b *testing.
20012001
}
20022002
}
20032003

2004+
// BenchmarkIterateHierarchyV2_NamespaceScaling tests how namespace scanning scales with namespace size
2005+
func BenchmarkIterateHierarchyV2_NamespaceScaling(b *testing.B) {
2006+
testCases := []struct {
2007+
name string
2008+
namespaceResourceCount int
2009+
crossNamespacePercent float64
2010+
}{
2011+
{"100_Resources_10pct", 100, 0.10},
2012+
{"1000_Resources_10pct", 1000, 0.10},
2013+
{"5000_Resources_10pct", 5000, 0.10},
2014+
{"10000_Resources_10pct", 10000, 0.10},
2015+
{"20000_Resources_10pct", 20000, 0.10},
2016+
// Also test with different cross-namespace percentages
2017+
{"5000_Resources_0pct", 5000, 0.00},
2018+
{"5000_Resources_5pct", 5000, 0.05},
2019+
{"5000_Resources_25pct", 5000, 0.25},
2020+
{"5000_Resources_50pct", 5000, 0.50},
2021+
}
2022+
2023+
for _, tc := range testCases {
2024+
b.Run(tc.name, func(b *testing.B) {
2025+
cluster := newCluster(b).WithAPIResources([]kube.APIResourceInfo{{
2026+
GroupKind: schema.GroupKind{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"},
2027+
GroupVersionResource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterroles"},
2028+
Meta: metav1.APIResource{Namespaced: false},
2029+
}})
2030+
2031+
// Calculate resource distribution
2032+
clusterParents := 100 // Fixed number of cluster-scoped resources
2033+
crossNamespacePods := int(float64(tc.namespaceResourceCount) * tc.crossNamespacePercent)
2034+
regularPods := tc.namespaceResourceCount - crossNamespacePods
2035+
2036+
testResources := buildParameterizedCrossNamespaceTestResourceMapWithUIDs(
2037+
clusterParents,
2038+
regularPods,
2039+
crossNamespacePods,
2040+
)
2041+
2042+
for _, resource := range testResources {
2043+
cluster.setNode(resource)
2044+
}
2045+
2046+
// Start from a cluster-scoped resource
2047+
startKey := kube.ResourceKey{
2048+
Group: "rbac.authorization.k8s.io",
2049+
Kind: "ClusterRole",
2050+
Namespace: "",
2051+
Name: "cluster-role-0",
2052+
}
2053+
2054+
b.ResetTimer()
2055+
for n := 0; n < b.N; n++ {
2056+
// Test scanning the "default" namespace which has the most resources
2057+
cluster.IterateHierarchyV2([]kube.ResourceKey{startKey}, func(_ *Resource, _ map[kube.ResourceKey]*Resource) bool {
2058+
return true
2059+
}, "default")
2060+
}
2061+
})
2062+
}
2063+
}
2064+
2065+
// BenchmarkIterateHierarchyV2_NamespaceScaling_NoScan provides baseline for comparison
2066+
func BenchmarkIterateHierarchyV2_NamespaceScaling_NoScan(b *testing.B) {
2067+
testCases := []struct {
2068+
name string
2069+
namespaceResourceCount int
2070+
}{
2071+
{"100_Resources", 100},
2072+
{"1000_Resources", 1000},
2073+
{"5000_Resources", 5000},
2074+
{"10000_Resources", 10000},
2075+
{"20000_Resources", 20000},
2076+
}
2077+
2078+
for _, tc := range testCases {
2079+
b.Run(tc.name, func(b *testing.B) {
2080+
cluster := newCluster(b).WithAPIResources([]kube.APIResourceInfo{{
2081+
GroupKind: schema.GroupKind{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"},
2082+
GroupVersionResource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterroles"},
2083+
Meta: metav1.APIResource{Namespaced: false},
2084+
}})
2085+
2086+
// All resources have cluster-scoped parents but we won't scan for them
2087+
testResources := buildParameterizedCrossNamespaceTestResourceMapWithUIDs(
2088+
100,
2089+
0,
2090+
tc.namespaceResourceCount,
2091+
)
2092+
2093+
for _, resource := range testResources {
2094+
cluster.setNode(resource)
2095+
}
2096+
2097+
// Start from a cluster-scoped resource
2098+
startKey := kube.ResourceKey{
2099+
Group: "rbac.authorization.k8s.io",
2100+
Kind: "ClusterRole",
2101+
Namespace: "",
2102+
Name: "cluster-role-0",
2103+
}
2104+
2105+
b.ResetTimer()
2106+
for n := 0; n < b.N; n++ {
2107+
// No namespace scanning - just traverse cluster-scoped children
2108+
cluster.IterateHierarchyV2([]kube.ResourceKey{startKey}, func(_ *Resource, _ map[kube.ResourceKey]*Resource) bool {
2109+
return true
2110+
}, "")
2111+
}
2112+
})
2113+
}
2114+
}
2115+
20042116
func TestIterateHierarchyV2_NoDuplicatesInSameNamespace(t *testing.T) {
20052117
// Create a parent-child relationship in the same namespace
20062118
parent := &appsv1.Deployment{

0 commit comments

Comments
 (0)