Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix deep equal check failure in CreateOrUpdateWork(), by replace the marshaler #5939

Merged
merged 1 commit into from
Jan 15, 2025

Conversation

zach593
Copy link
Contributor

@zach593 zach593 commented Dec 11, 2024

What type of PR is this?

/kind bug

What this PR does / why we need it:
see #5938

Which issue(s) this PR fixes:
Fixes #5938

Special notes for your reviewer:

Does this PR introduce a user-facing change?:

NONE

@karmada-bot karmada-bot added the kind/bug Categorizes issue or PR as related to a bug. label Dec 11, 2024
@karmada-bot karmada-bot added the size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. label Dec 11, 2024
@codecov-commenter
Copy link

codecov-commenter commented Dec 11, 2024

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 48.37%. Comparing base (8313813) to head (ce1ca99).

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #5939   +/-   ##
=======================================
  Coverage   48.37%   48.37%           
=======================================
  Files         666      666           
  Lines       54831    54831           
=======================================
  Hits        26524    26524           
  Misses      26590    26590           
  Partials     1717     1717           
Flag Coverage Δ
unittests 48.37% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@chaosi-zju
Copy link
Member

Hi @zach593, I am trying to use this issue to design a performance monitoring case, my case is as follows:

  1. Record Initial Workqueue Metrics: Capture metrics before changes.
  2. Create N Deployments and N Policies: Set up deployments and policies, then wait for propagation to member1 and member2 clusters.
  3. Create N OverridePolicies: Implement overridepolicies to modify deployment labels for member1 cluster only. (Prior to this PR, deployments in both clusters would be updated, but after this PR, only the member1 cluster's deployments will be updated. This PR will reduce the update time for each reconciliation).
  4. Record End Workqueue Metrics: Capture metrics after changes and calculate total reconciliation time.

Do you think there are better optimization opportunities for my case?

Copy link
Member

@XiShanYongYe-Chang XiShanYongYe-Chang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, the current pr resolves the deepcopy failure issue and dose not introduce new performance risks.

About the kind/bug label. do you expect to sync the current pr to the previous release versions? If we don't need to, maybe we can modify the label.

@zach593
Copy link
Contributor Author

zach593 commented Jan 8, 2025

Do you think there are better optimization opportunities for my case?

@chaosi-zju As we discussed in zoom, for the performance optimization, this PR is not enough, I may need another 2 PRs to complete this goal. Since I need more than one PR, I created #6017 to track it, and we can have further discussion in that issue.

@zach593
Copy link
Contributor Author

zach593 commented Jan 8, 2025

About the kind/bug label. do you expect to sync the current pr to the previous release versions? If we don't need to, maybe we can modify the label.

Sure, can you help with that? I'm not familiar with commands with /

@zach593
Copy link
Contributor Author

zach593 commented Jan 8, 2025

BTW, I think we could consider use json.Marshal() instead of all of x.MarshalJSON(). Because json.Marshal() is more general, it will also call x.MarshalJSON() internally, and will also perform some other optimizations.

@XiShanYongYe-Chang
Copy link
Member

Sure, can you help with that? I'm not familiar with commands with /

/remove-kind bug
/kind cleanup

@karmada-bot karmada-bot added kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. and removed kind/bug Categorizes issue or PR as related to a bug. labels Jan 9, 2025
@chaosi-zju
Copy link
Member

As we discussed in zoom, for the performance optimization, this PR is not enough, I may need another 2 PRs to complete this goal. Since I need more than one PR, I created #6017 to track it, and we can have further discussion in that issue.

ok, thanks, I think this PR can move forward first

/lgtm

@karmada-bot karmada-bot added the lgtm Indicates that a PR is ready to be merged. label Jan 9, 2025
Copy link
Member

@RainbowMango RainbowMango left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/assign
I will take a look this week.

@@ -54,7 +55,7 @@ func CreateOrUpdateWork(ctx context.Context, client client.Client, workMeta meta
}
}

workloadJSON, err := resource.MarshalJSON()
workloadJSON, err := json.Marshal(resource)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any difference between encoding/json.Marshal and unstructured.MarshalJSON other than the /n?
I am mostly curious about functionality and performance.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, I think we need to check all of the places where using the unstructured.MarshalJSON.

PS: I don't mean to check and fix them all in this PR. Just remind me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked this on #6031, please correct me if anyone feels this is unnecessary.

Copy link
Member

@chaosi-zju chaosi-zju Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any difference between encoding/json.Marshal and unstructured.MarshalJSON other than the /n?
I am mostly curious about functionality and performance.

I did a benchmark test to compare different json marshal function, my test code is as:

benchmark test code
func BenchmarkMarshalJSON(b *testing.B) {
	resource := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": "apps/v1",
			"kind":       "Deployment",
			"metadata": map[string]interface{}{
				"name":      "test-deployment",
				"namespace": "default",
			},
			"spec": map[string]interface{}{
				"replicas": 3,
				"selector": map[string]interface{}{
					"matchLabels": map[string]interface{}{
						"app": "test",
					},
				},
				"template": map[string]interface{}{
					"metadata": map[string]interface{}{
						"labels": map[string]interface{}{
							"app": "test",
						},
					},
					"spec": map[string]interface{}{
						"containers": []interface{}{
							map[string]interface{}{
								"name":  "test-container",
								"image": "nginx:latest",
							},
						},
					},
				},
			},
		},
	}

	b.Run("resource.MarshalJSON", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			_, _ = resource.MarshalJSON()
		}
	})

	b.Run("json.Marshal", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
                       // "encoding/json"
			_, _ = json.Marshal(resource)
		}
	})

	b.Run("jsoniter.Marshal", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
                       // jsoniter "github.com/json-iterator/go"
			_, _ = jsoniter.Marshal(resource)
		}
	})
}

the result is:

goos: windows
goarch: amd64
pkg: github.com/karmada-io/karmada/pkg/controllers/ctrlutil
cpu: Intel(R) Core(TM) i5-14600KF
BenchmarkMarshalJSON
BenchmarkMarshalJSON/resource.MarshalJSON
BenchmarkMarshalJSON/resource.MarshalJSON-20         	  398614	      2835 ns/op
BenchmarkMarshalJSON/json.Marshal
BenchmarkMarshalJSON/json.Marshal-20                 	  307854	      4004 ns/op
BenchmarkMarshalJSON/jsoniter.Marshal
BenchmarkMarshalJSON/jsoniter.Marshal-20             	  393528	      3019 ns/op
PASS

and

goos: windows
goarch: amd64
pkg: github.com/karmada-io/karmada/pkg/util/helper
cpu: Intel(R) Xeon(R) Gold 6278C CPU @ 2.60GHz
BenchmarkMarshalJSON
BenchmarkMarshalJSON/resource.MarshalJSON
BenchmarkMarshalJSON/resource.MarshalJSON-4                95715             13065 ns/op
BenchmarkMarshalJSON/json.Marshal
BenchmarkMarshalJSON/json.Marshal-4                        68590             16391 ns/op
BenchmarkMarshalJSON/jsoniter.Marshal
BenchmarkMarshalJSON/jsoniter.Marshal-4                    79052             13281 ns/op
PASS

seems previous unstructured.MarshalJSON perform better, while encoding/json.Marshal worst

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resource.MarshalJSON() is specifically optimized for the unstructured.Unstructured type, potentially offering significant performance improvements when handling large numbers of Kubernetes resources.

besides, Kubernetes resources may contain fields that require special treatment during serialization, which can be handled by the custom method, for example:

  • Timestamp fields like creationTimestamp may need to be formatted in a particular way.
  • Some fields might need to be omitted entirely when they're empty, rather than being serialized as null.
  • Certain fields may require custom serialization rules to maintain backward compatibility with older API versions.
  • Resource-specific fields (like status in various resources) might need special treatment during serialization.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any other option we can resolve this issue other than replace this function?

what about using json.Compact to remove redundant newlines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doubt it. Can you help test it?
I'm afraid the json.Compact also removes the space characters from the body, which probably makes the deep copy always return false.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid the json.Compact also removes the space characters from the body, which probably makes the deep copy always return false.

I did the test as follows:

workloadJSON, err := resource.MarshalJSON()

workloadJSON2, err := json.Marshal(resource)

buffer := new(bytes.Buffer)
if err = json.Compact(buffer, workloadJSON); err != nil {}
workloadJSON3 := buffer.Bytes()

klog.Infof("diff workloadJSON VS workloadJSON2: %+v", cmp.Diff(workloadJSON, workloadJSON2))
klog.Infof("diff workloadJSON VS workloadJSON3: %+v", cmp.Diff(workloadJSON, workloadJSON3))
klog.Infof("diff workloadJSON2 VS workloadJSON3: %+v", cmp.Diff(workloadJSON2, workloadJSON3))

the result is: workloadJSON2 and workloadJSON3 is equal, and workloadJSON is only one more \n that the other two.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/golang/go/blob/go1.22.9/src/encoding/json/encode.go#L430

func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) {
	if v.Kind() == reflect.Pointer && v.IsNil() {
		e.WriteString("null")
		return
	}
	m, ok := v.Interface().(Marshaler)
	if !ok {
		e.WriteString("null")
		return
	}
	b, err := m.MarshalJSON()
	if err == nil {
		e.Grow(len(b))
		out := e.AvailableBuffer()
		out, err = appendCompact(out, b, opts.escapeHTML)
		e.Buffer.Write(out)
	}
	if err != nil {
		e.error(&MarshalerError{v.Type(), err, "MarshalJSON"})
	}
}

https://github.com/golang/go/blob/go1.22.9/src/encoding/json/indent.go#L39

// Compact appends to dst the JSON-encoded src with
// insignificant space characters elided.
func Compact(dst *bytes.Buffer, src []byte) error {
	dst.Grow(len(src))
	b := dst.AvailableBuffer()
	b, err := appendCompact(b, src, false)
	dst.Write(b)
	return err
}

json.Marshal() also does the same thing as json.Compact(). As I understand it, this is the only real difference between json.Marshal() and unstructured.MarshalJSON(), aside from some behaviors that might affect performance.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.Marshal() also does the same thing as json.Compact(). As I understand it, this is the only real difference between json.Marshal() and unstructured.MarshalJSON(), aside from some behaviors that might affect performance.

You're great 👍

So, simply understand, m.MarshalJSON() is already overwrited, and json.Marshal() = m.MarshalJSON() + json.Compact

@karmada-bot karmada-bot removed the lgtm Indicates that a PR is ready to be merged. label Jan 10, 2025
@RainbowMango RainbowMango added this to the v1.13 milestone Jan 15, 2025
Copy link
Member

@RainbowMango RainbowMango left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm
/approve

@karmada-bot karmada-bot added the lgtm Indicates that a PR is ready to be merged. label Jan 15, 2025
@karmada-bot
Copy link
Collaborator

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: RainbowMango

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@karmada-bot karmada-bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jan 15, 2025
@karmada-bot karmada-bot merged commit ce41488 into karmada-io:master Jan 15, 2025
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. lgtm Indicates that a PR is ready to be merged. size/XS Denotes a PR that changes 0-9 lines, ignoring generated files.
Projects
None yet
6 participants