Skip to content

Commit

Permalink
Precompute IsNamespaceScoped to avoid expensive schema reads (#4152)
Browse files Browse the repository at this point in the history
* Precompute IsNamespaceScoped to avoid expensive schema reads

See kptdev/kpt#2469

For the `gcr.io/kpt-fn/set-namespace:v0.1` function, over 50% of CPU
time is spent on IsNamespaceScoped. Instead of unmarshalling 100k lines
of JSON to determine this, instead just precompute it. We can ensure
this never is inaccurate as the test verifies the precomputed result is
up to date.

In real world kpt pipelines this cuts execution of set-namespace (and
similar functions, just an example of a trivial function) from 2.0s to
1.0s. Because these functions are run in long pipelines over many
resources, this adds up a lot.

* Add documentation
  • Loading branch information
howardjohn committed Sep 9, 2021
1 parent 2b8a393 commit 402f6ca
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 1 deletion.
6 changes: 6 additions & 0 deletions kyaml/openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ The above command will update the [OpenAPI schema] and the [Kustomization schema
create a directory kubernetesapi/v1212 and store the resulting
swagger.json and swagger.go files there.

#### Precomputations

To avoid expensive schema lookups, some functions have precomputed results based on the schema. Unit tests
ensure these are kept in sync with the schema; if these tests fail you will need to follow the suggested diff
to update the precomputed results.

### Run all tests

At the top of the repository, run the tests.
Expand Down
98 changes: 97 additions & 1 deletion kyaml/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,95 @@ type openapiData struct {
schemaInit bool
}

// precomputedIsNamespaceScoped precomputes IsNamespaceScoped for known types. This avoids Schema creation,
// which is expensive
// The test output from TestIsNamespaceScopedPrecompute shows the expected map in go syntax,and can be copy and pasted
// from the failure if it changes.
var precomputedIsNamespaceScoped = map[yaml.TypeMeta]bool{
{APIVersion: "admissionregistration.k8s.io/v1", Kind: "MutatingWebhookConfiguration"}: false,
{APIVersion: "admissionregistration.k8s.io/v1", Kind: "ValidatingWebhookConfiguration"}: false,
{APIVersion: "admissionregistration.k8s.io/v1beta1", Kind: "MutatingWebhookConfiguration"}: false,
{APIVersion: "admissionregistration.k8s.io/v1beta1", Kind: "ValidatingWebhookConfiguration"}: false,
{APIVersion: "apiextensions.k8s.io/v1", Kind: "CustomResourceDefinition"}: false,
{APIVersion: "apiextensions.k8s.io/v1beta1", Kind: "CustomResourceDefinition"}: false,
{APIVersion: "apiregistration.k8s.io/v1", Kind: "APIService"}: false,
{APIVersion: "apiregistration.k8s.io/v1beta1", Kind: "APIService"}: false,
{APIVersion: "apps/v1", Kind: "ControllerRevision"}: true,
{APIVersion: "apps/v1", Kind: "DaemonSet"}: true,
{APIVersion: "apps/v1", Kind: "Deployment"}: true,
{APIVersion: "apps/v1", Kind: "ReplicaSet"}: true,
{APIVersion: "apps/v1", Kind: "StatefulSet"}: true,
{APIVersion: "autoscaling/v1", Kind: "HorizontalPodAutoscaler"}: true,
{APIVersion: "autoscaling/v1", Kind: "Scale"}: true,
{APIVersion: "autoscaling/v2beta1", Kind: "HorizontalPodAutoscaler"}: true,
{APIVersion: "autoscaling/v2beta2", Kind: "HorizontalPodAutoscaler"}: true,
{APIVersion: "batch/v1", Kind: "CronJob"}: true,
{APIVersion: "batch/v1", Kind: "Job"}: true,
{APIVersion: "batch/v1beta1", Kind: "CronJob"}: true,
{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"}: false,
{APIVersion: "certificates.k8s.io/v1beta1", Kind: "CertificateSigningRequest"}: false,
{APIVersion: "coordination.k8s.io/v1", Kind: "Lease"}: true,
{APIVersion: "coordination.k8s.io/v1beta1", Kind: "Lease"}: true,
{APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"}: true,
{APIVersion: "discovery.k8s.io/v1beta1", Kind: "EndpointSlice"}: true,
{APIVersion: "events.k8s.io/v1", Kind: "Event"}: true,
{APIVersion: "events.k8s.io/v1beta1", Kind: "Event"}: true,
{APIVersion: "extensions/v1beta1", Kind: "Ingress"}: true,
{APIVersion: "flowcontrol.apiserver.k8s.io/v1beta1", Kind: "FlowSchema"}: false,
{APIVersion: "flowcontrol.apiserver.k8s.io/v1beta1", Kind: "PriorityLevelConfiguration"}: false,
{APIVersion: "networking.k8s.io/v1", Kind: "Ingress"}: true,
{APIVersion: "networking.k8s.io/v1", Kind: "IngressClass"}: false,
{APIVersion: "networking.k8s.io/v1", Kind: "NetworkPolicy"}: true,
{APIVersion: "networking.k8s.io/v1beta1", Kind: "Ingress"}: true,
{APIVersion: "networking.k8s.io/v1beta1", Kind: "IngressClass"}: false,
{APIVersion: "node.k8s.io/v1", Kind: "RuntimeClass"}: false,
{APIVersion: "node.k8s.io/v1beta1", Kind: "RuntimeClass"}: false,
{APIVersion: "policy/v1", Kind: "PodDisruptionBudget"}: true,
{APIVersion: "policy/v1beta1", Kind: "PodDisruptionBudget"}: true,
{APIVersion: "policy/v1beta1", Kind: "PodSecurityPolicy"}: false,
{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"}: false,
{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRoleBinding"}: false,
{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}: true,
{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"}: true,
{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "ClusterRole"}: false,
{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "ClusterRoleBinding"}: false,
{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "Role"}: true,
{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "RoleBinding"}: true,
{APIVersion: "scheduling.k8s.io/v1", Kind: "PriorityClass"}: false,
{APIVersion: "scheduling.k8s.io/v1beta1", Kind: "PriorityClass"}: false,
{APIVersion: "storage.k8s.io/v1", Kind: "CSIDriver"}: false,
{APIVersion: "storage.k8s.io/v1", Kind: "CSINode"}: false,
{APIVersion: "storage.k8s.io/v1", Kind: "StorageClass"}: false,
{APIVersion: "storage.k8s.io/v1", Kind: "VolumeAttachment"}: false,
{APIVersion: "storage.k8s.io/v1beta1", Kind: "CSIDriver"}: false,
{APIVersion: "storage.k8s.io/v1beta1", Kind: "CSINode"}: false,
{APIVersion: "storage.k8s.io/v1beta1", Kind: "CSIStorageCapacity"}: true,
{APIVersion: "storage.k8s.io/v1beta1", Kind: "StorageClass"}: false,
{APIVersion: "storage.k8s.io/v1beta1", Kind: "VolumeAttachment"}: false,
{APIVersion: "v1", Kind: "ComponentStatus"}: false,
{APIVersion: "v1", Kind: "ConfigMap"}: true,
{APIVersion: "v1", Kind: "Endpoints"}: true,
{APIVersion: "v1", Kind: "Event"}: true,
{APIVersion: "v1", Kind: "LimitRange"}: true,
{APIVersion: "v1", Kind: "Namespace"}: false,
{APIVersion: "v1", Kind: "Node"}: false,
{APIVersion: "v1", Kind: "NodeProxyOptions"}: false,
{APIVersion: "v1", Kind: "PersistentVolume"}: false,
{APIVersion: "v1", Kind: "PersistentVolumeClaim"}: true,
{APIVersion: "v1", Kind: "Pod"}: true,
{APIVersion: "v1", Kind: "PodAttachOptions"}: true,
{APIVersion: "v1", Kind: "PodExecOptions"}: true,
{APIVersion: "v1", Kind: "PodPortForwardOptions"}: true,
{APIVersion: "v1", Kind: "PodProxyOptions"}: true,
{APIVersion: "v1", Kind: "PodTemplate"}: true,
{APIVersion: "v1", Kind: "ReplicationController"}: true,
{APIVersion: "v1", Kind: "ResourceQuota"}: true,
{APIVersion: "v1", Kind: "Secret"}: true,
{APIVersion: "v1", Kind: "Service"}: true,
{APIVersion: "v1", Kind: "ServiceAccount"}: true,
{APIVersion: "v1", Kind: "ServiceProxyOptions"}: true,
}

// ResourceSchema wraps the OpenAPI Schema.
type ResourceSchema struct {
// Schema is the OpenAPI schema for a Resource or field
Expand Down Expand Up @@ -265,10 +354,17 @@ func GetSchema(s string, schema *spec.Schema) (*ResourceSchema, error) {
// cluster-scoped by looking at the information in the openapi schema.
// The second return value tells whether the provided type could be found
// in the openapi schema. If the value is false here, the scope of the
// resource is not known. If the type if found, the first return value will
// resource is not known. If the type is found, the first return value will
// be true if the resource is namespace-scoped, and false if the type is
// cluster-scoped.
func IsNamespaceScoped(typeMeta yaml.TypeMeta) (bool, bool) {
if res, f := precomputedIsNamespaceScoped[typeMeta]; f {
return res, true
}
return isNamespaceScopedFromSchema(typeMeta)
}

func isNamespaceScopedFromSchema(typeMeta yaml.TypeMeta) (bool, bool) {
initSchema()
isNamespaceScoped, found := globalSchema.namespaceabilityByResourceType[typeMeta]
return isNamespaceScoped, found
Expand Down
9 changes: 9 additions & 0 deletions kyaml/openapi/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io/ioutil"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
Expand Down Expand Up @@ -271,6 +272,14 @@ func TestIsNamespaceScoped_builtin(t *testing.T) {
}
}

// TestIsNamespaceScopedPrecompute checks that the precomputed result meets the actual result
func TestIsNamespaceScopedPrecompute(t *testing.T) {
initSchema()
if diff := cmp.Diff(globalSchema.namespaceabilityByResourceType, precomputedIsNamespaceScoped); diff != "" {
t.Fatalf(diff)
}
}

func TestIsNamespaceScoped_custom(t *testing.T) {
SuppressBuiltInSchemaUse()
err := AddSchema([]byte(`
Expand Down

0 comments on commit 402f6ca

Please sign in to comment.