Skip to content

Commit 985835f

Browse files
authored
perf: limit initSchema calls from openapi.IsNamespaceScoped (#5076)
* test: add openapi.IsNamespaceScoped benchmark Add a benchmark test for IsNamespaceScoped performance when the default schema is in use. * perf: limit initSchema calls from openapi.IsNamespaceScoped Avoid calling initSchema from openapi.IsNamespaceScoped when possible. Work done in #4152 introduced a precomputed namespace scope map based on the default built-in schema. This commit extends that work by avoiding calls to initSchema when a resource is not found in the precomputed map and the default built-in schema is in use. In those cases, there is no benefit to calling initSchema since the precomputed map is exactly what will be calculated by parsing the default built-in schema. * fix: delay parsing of default built-in schema When namespace scope can be determined by the precomputed map but the type is not present in the precomputed map, delay the parsing of the default built-in schema. If the schema to be initialized is the default built-in schema and the type is not in the precomputed map, then the type will not be found in the default built-in schema. There is no need to parse the default built-in schema for that answer; its parsing may be delayed until it is needed for some other purpose. In cases where the schema is used solely for namespace scope checks, the schema might not ever be parsed. Skipping the parsing reduces both execution time and memory use. * fix: correct openapi.go's schemaNotParsed value openapiData initializes with defaultBuiltInSchemaParseStatus set to 0, so schemaNotParsed should have 0 as its value.
1 parent f81765b commit 985835f

File tree

2 files changed

+73
-9
lines changed

2 files changed

+73
-9
lines changed

kyaml/openapi/openapi.go

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ var (
4444
customSchema []byte //nolint:gochecknoglobals
4545
)
4646

47+
// schemaParseStatus is used in cases when a schema should be parsed, but the
48+
// parsing may be delayed to a later time.
49+
type schemaParseStatus uint32
50+
51+
const (
52+
schemaNotParsed schemaParseStatus = iota
53+
schemaParseDelayed
54+
schemaParsed
55+
)
56+
4757
// openapiData contains the parsed openapi state. this is in a struct rather than
4858
// a list of vars so that it can be reset from tests.
4959
type openapiData struct {
@@ -57,13 +67,17 @@ type openapiData struct {
5767
// is namespaceable or not
5868
namespaceabilityByResourceType map[yaml.TypeMeta]bool
5969

60-
// noUseBuiltInSchema stores whether we want to prevent using the built-n
70+
// noUseBuiltInSchema stores whether we want to prevent using the built-in
6171
// Kubernetes schema as part of the global schema
6272
noUseBuiltInSchema bool
6373

6474
// schemaInit stores whether or not we've parsed the schema already,
6575
// so that we only reparse the when necessary (to speed up performance)
6676
schemaInit bool
77+
78+
// defaultBuiltInSchemaParseStatus stores the parse status of the default
79+
// built-in schema.
80+
defaultBuiltInSchemaParseStatus schemaParseStatus
6781
}
6882

6983
type format string
@@ -387,18 +401,45 @@ func GetSchema(s string, schema *spec.Schema) (*ResourceSchema, error) {
387401
// be true if the resource is namespace-scoped, and false if the type is
388402
// cluster-scoped.
389403
func IsNamespaceScoped(typeMeta yaml.TypeMeta) (bool, bool) {
390-
if res, f := precomputedIsNamespaceScoped[typeMeta]; f {
391-
return res, true
404+
if isNamespaceScoped, found := precomputedIsNamespaceScoped[typeMeta]; found {
405+
return isNamespaceScoped, found
406+
}
407+
if isInitSchemaNeededForNamespaceScopeCheck() {
408+
initSchema()
392409
}
393-
return isNamespaceScopedFromSchema(typeMeta)
394-
}
395-
396-
func isNamespaceScopedFromSchema(typeMeta yaml.TypeMeta) (bool, bool) {
397-
initSchema()
398410
isNamespaceScoped, found := globalSchema.namespaceabilityByResourceType[typeMeta]
399411
return isNamespaceScoped, found
400412
}
401413

414+
// isInitSchemaNeededForNamespaceScopeCheck returns true if initSchema is needed
415+
// to ensure globalSchema.namespaceabilityByResourceType is fully populated for
416+
// cases where a custom or non-default built-in schema is in use.
417+
func isInitSchemaNeededForNamespaceScopeCheck() bool {
418+
schemaLock.Lock()
419+
defer schemaLock.Unlock()
420+
421+
if globalSchema.schemaInit {
422+
return false // globalSchema already is initialized.
423+
}
424+
if customSchema != nil {
425+
return true // initSchema is needed.
426+
}
427+
if kubernetesOpenAPIVersion == "" || kubernetesOpenAPIVersion == kubernetesOpenAPIDefaultVersion {
428+
// The default built-in schema is in use. Since
429+
// precomputedIsNamespaceScoped aligns with the default built-in schema
430+
// (verified by TestIsNamespaceScopedPrecompute), there is no need to
431+
// call initSchema.
432+
if globalSchema.defaultBuiltInSchemaParseStatus == schemaNotParsed {
433+
// The schema may be needed for purposes other than namespace scope
434+
// checks. Flag it to be parsed when that need arises.
435+
globalSchema.defaultBuiltInSchemaParseStatus = schemaParseDelayed
436+
}
437+
return false
438+
}
439+
// A non-default built-in schema is in use. initSchema is needed.
440+
return true
441+
}
442+
402443
// IsCertainlyClusterScoped returns true for Node, Namespace, etc. and
403444
// false for Pod, Deployment, etc. and kinds that aren't recognized in the
404445
// openapi data. See:
@@ -638,13 +679,19 @@ func initSchema() {
638679
panic(fmt.Errorf("invalid schema file: %w", err))
639680
}
640681
} else {
641-
if kubernetesOpenAPIVersion == "" {
682+
if kubernetesOpenAPIVersion == "" || kubernetesOpenAPIVersion == kubernetesOpenAPIDefaultVersion {
642683
parseBuiltinSchema(kubernetesOpenAPIDefaultVersion)
684+
globalSchema.defaultBuiltInSchemaParseStatus = schemaParsed
643685
} else {
644686
parseBuiltinSchema(kubernetesOpenAPIVersion)
645687
}
646688
}
647689

690+
if globalSchema.defaultBuiltInSchemaParseStatus == schemaParseDelayed {
691+
parseBuiltinSchema(kubernetesOpenAPIDefaultVersion)
692+
globalSchema.defaultBuiltInSchemaParseStatus = schemaParsed
693+
}
694+
648695
if err := parse(kustomizationapi.MustAsset(kustomizationAPIAssetName), JsonOrYaml); err != nil {
649696
// this should never happen
650697
panic(err)

kyaml/openapi/openapi_benchmark_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
openapi_v2 "github.com/google/gnostic-models/openapiv2"
1212
"google.golang.org/protobuf/proto"
1313
"sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi"
14+
"sigs.k8s.io/kustomize/kyaml/yaml"
1415
)
1516

1617
func BenchmarkProtoUnmarshal(t *testing.B) {
@@ -31,3 +32,19 @@ func BenchmarkProtoUnmarshal(t *testing.B) {
3132
}
3233
}
3334
}
35+
36+
func BenchmarkPrecomputedIsNamespaceScoped(b *testing.B) {
37+
testcases := map[string]yaml.TypeMeta{
38+
"namespace scoped": {APIVersion: "apps/v1", Kind: "ControllerRevision"},
39+
"cluster scoped": {APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"},
40+
"unknown resource": {APIVersion: "custom.io/v1", Kind: "Custom"},
41+
}
42+
for name, testcase := range testcases {
43+
b.Run(name, func(b *testing.B) {
44+
for i := 0; i < b.N; i++ {
45+
ResetOpenAPI()
46+
_, _ = IsNamespaceScoped(testcase)
47+
}
48+
})
49+
}
50+
}

0 commit comments

Comments
 (0)