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

implement protoreflect fast paths #8

Merged
merged 50 commits into from
Sep 3, 2021
Merged

Conversation

fdymylja
Copy link
Contributor

@fdymylja fdymylja commented Jul 15, 2021

Types to implement:

ProtoReflect.Messsage:

  • Descriptor
  • Type
  • New
  • Interface
  • Range
  • Has
  • Clear
  • Get:
    • Scalar types
    • Oneof types
    • Message types
    • List types
    • Map types
    • Extension Types (not required as per: Disable proto2 #11)
  • Set
  • Mutable
  • NewField
  • WhichOneof
  • GetUnknown
  • SetUnknown
  • IsValid

Protoreflect.List:

  • Len
  • Get
  • Set
  • Append
  • AppendMutable
  • Truncate
  • NewElement
  • IsValid

Protoreflect.Map:

  • Len
  • Range
  • Has
  • Clear
  • Get
  • Set
  • Mutable
  • NewValue
  • IsValid

@fdymylja fdymylja requested review from technicallyty and aaronc July 15, 2021 15:10
@fdymylja
Copy link
Contributor Author

This is how Get looks like: https://github.com/cosmos/cosmos-proto/pull/8/files#diff-b80be7ac49d3c20aabc1048615c419c929d42d171d6337a4f09287f770429697R390

I have some questions on top of my head.

  1. Has get to be immutable? If it has we will need DeepCopy methods for protomessages (easy for primitive types, complex for Message types)
  2. What about message extensions? instead of panic if the field descriptor is unknown we could rely back on protoreflect?
  3. fieldDescriptor.Name() vs fieldDescriptor.FullName() vs something else to check for expected field descriptor equality (which means assert the field descriptor is the correct one, we should check the kind? maybe fullname is sufficient)

@fdymylja
Copy link
Contributor Author

fdymylja commented Jul 15, 2021

Things left to do (for Get):

  1. Implement MapValue
  2. Implement ListValue
  3. Implement Oneof

@fdymylja
Copy link
Contributor Author

fdymylja commented Jul 19, 2021

Has get to be immutable? If it has we will need DeepCopy methods for protomessages (easy for primitive types, complex for Message types)

Regarding this, I ran a test:

p := &Params{
		ConstantFee: &v1alpha1.Coin{
			Denom:  "non-mutated",
			Amount: "1",
		},
	}

	value := p.ProtoReflect().Get(p.ProtoReflect().Descriptor().Fields().ByName("constant_fee"))
	value.Message().Set((&v1alpha1.Coin{}).ProtoReflect().Descriptor().Fields().Get(0), protoreflect.ValueOfString("mutated"))

	log.Printf("%s", p)
	log.Printf("%s", value.Message().Interface())
output:
2021/07/19 10:38:47 constant_fee:{denom:"mutated"  amount:"1"}
2021/07/19 10:38:47 denom:"mutated"  amount:"1"

This makes me assume that fields which are retrieved via Get can be mutated.

If the message type field is not set, a panic occurs in Set.

Same behaviour was observed for Map and List.

Other behaviour experimentation:

Tried to use create a dynamic protobuf message using the dynamicpb package, and tried to set a message field using dynamicpb.Message as value instead of the concrete type. Got a panic.

func TestDynamic(t *testing.T) {
	dynCoin := dynamicpb.NewMessage((&v1alpha1.Coin{}).ProtoReflect().Descriptor())

	p := &Params{}
	fd := p.ProtoReflect().Descriptor().Fields().ByName("constant_fee")
	p.ProtoReflect().Set(fd, protoreflect.ValueOfMessage(dynCoin))

	log.Printf("%s", p)
}

panic: invalid type: got *dynamicpb.Message, want *v1alpha1.Coin [recovered]
	panic: invalid type: got *dynamicpb.Message, want *v1alpha1.Coin

@technicallyty
Copy link
Contributor

i've added the enumeration generators from protoc-gen-go to my branch if you would like to merge that in to your draft

Copy link
Member

@aaronc aaronc left a comment

Choose a reason for hiding this comment

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

Great work @fdymylja !

The list struct codegen seems pretty involved. Are they needed? What if we just used something like the dynamicList from dynamicpb?

// For unpopulated composite types, it returns an empty, read-only view
// of the value; to obtain a mutable reference, use Mutable.
func (x *A) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value {
switch descriptor.Name() {
Copy link
Member

Choose a reason for hiding this comment

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

Why not switching on field number? Would that maybe be faster?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What if we just used something like the dynamicList from dynamicpb?

I've tried combining dynamicpb + normal proto messages and it ends up in invalid type panics -> invalid type: got *dynamicpb.Message, want *v1alpha1.Coin [recovered] - dynamicpb also does not give us access to dynamiclist constructors.

we could create a "wrapper", I was experimenting with it but then i got stuck at some point during message.Set where field is a list field, and value is a protoreflect.ListValue in which you'd need to cast the value to the slice type.

Copy link
Contributor Author

@fdymylja fdymylja Jul 19, 2021

Choose a reason for hiding this comment

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

Why not switching on field number? Would that maybe be faster?

The Get logic will need to be rewritten partially (to also include extension support), because it's not that safe, meaning that using just field name or number I think might not be sufficient, probably the safest way (assuming we're working with a global protobuf registry) is to use the field fullname. Reason being to avoid for example having a field which is number: 1, kind: message being Get using a field number: 1, kind: int64.

The user of Get would be left with an invalid protoreflect.Value, and the panic would happen only when value is being unwrapped.

@fdymylja
Copy link
Contributor Author

Why not switching on field number? Would that maybe be faster?

I ran some tests on original protobuf implementation, Get does not work if I try to fetch a FieldDescriptor owned by another Message even if the name, field number and field kind are the same.

If we want to respect protoreflection behaviour: we should use FieldDescriptor.FullName()

@fdymylja
Copy link
Contributor Author

I've update the Get implementation with the safer field checking.

I've also added the Oneof logic, please have a look at it so we can make sure the logic is correct: https://github.com/cosmos/cosmos-proto/pull/8/files#diff-ec66d96ce4cf4e11416971540fb8c46e759ed99ee0f1cb5e0738bc49172cae1fR255

@technicallyty
Copy link
Contributor

should we have the checkbox list as a separate issue and complete each item in separate PRs?

@technicallyty
Copy link
Contributor

went ahead and updated this issue #3 (comment) i think it would be a bit cleaner to track there and do small PR's for each item

generator/gen.go Outdated
Comment on lines 109 to 223
/*
g.P("// Code generated by Pulsar \U0001FA90. DO NOT EDIT.")
if bi, ok := debug.ReadBuildInfo(); ok {
g.P("// Pulsar version: ", bi.Main.Version)
}
g.P("// source: ", file.Desc.Path())
g.P()
*/ // TODO(fdymylja): remove me
Copy link
Contributor

Choose a reason for hiding this comment

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

we should still have the DO NOT EDIT notice here no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes sorry! I removed it so my editor allows me to see errors or warning! otherwise it won't parse it!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will remove this when I'm done with the PR

@fdymylja
Copy link
Contributor Author

went ahead and updated this issue #3 (comment) i think it would be a bit cleaner to track there and do small PR's for each item

sounds perfectly good to me!

@fdymylja
Copy link
Contributor Author

fdymylja commented Jul 20, 2021

Note on ProtoReflect implementation details on repeated types:

If list is nil || len(list) == 0 -> list cannot be mutated with Get.

if there's one element in the list, then the list can be mutated by gettings its protoreflect.ListValue. And the mutation reflects on the message containing the list (so if I append on ListValue then the message containing the list will be updated too).

Mutable allows modifications which are reflected on the type EVEN if the list is nil.

@aaronc, based on this findings I don't think we can do without custom list and map types. Maybe we could use unsafe to create a generic list/map type and cast the unsafe pointer to its concrete type. But I'm not sure of itse feasability.

@aaronc
Copy link
Member

aaronc commented Jul 20, 2021

Note on ProtoReflect implementation details on repeated types:

If list is nil || len(list) == 0 -> list cannot be mutated with Get.

if there's one element in the list, then the list can be mutated by gettings its protoreflect.ListValue. And the mutation reflects on the message containing the list (so if I append on ListValue then the message containing the list will be updated too).

Mutable allows modifications which are reflected on the type EVEN if the list is nil.

@aaronc, based on this findings I don't think we can do without custom list and map types. Maybe we could use unsafe to create a generic list/map type and cast the unsafe pointer to its concrete type. But I'm not sure of itse feasability.

I think it's fine with custom. Was just wondering if we could simplify. Maybe a generic type makes sense, but we should aim for correctness and performance first I think

@fdymylja
Copy link
Contributor Author

I've found a spec discrepancy in protoreflect.Set regarding mutability:

  • If type is List, setting and mutating the List after DOES NOT reflect list changes in the message.
  • If type is Map, setting a nd mutating the Map DOES reflect map changes in the message.
  • if type is Message, setting and mutating the message DOES reflect map changes in the message.

@fdymylja
Copy link
Contributor Author

I will wrap up this PR early next week, I will also open some issues regarding behaviour in the goproto repo. Then we can focus on testing, if someone is available to write tests I can give a shoot at #5 in the meantime (after I finalize this PR).

Would that be fine @aaronc, @technicallyty ?

@fdymylja
Copy link
Contributor Author

fdymylja commented Aug 3, 2021

All the fastpath implementations are done.

I am trying to experiment a little for a faster Message.Range implementation which does not access FieldDescriptorsList but uses predefined FDs, as per the following profiling:

image

The generated code might increase a bit but we might get a significant code performance increase.

@fdymylja fdymylja marked this pull request as ready for review August 4, 2021 08:50
@fdymylja
Copy link
Contributor Author

fdymylja commented Aug 4, 2021

All the fastpath implementations are done.

I am trying to experiment a little for a faster Message.Range implementation which does not access FieldDescriptorsList but uses predefined FDs, as per the following profiling:

image

The generated code might increase a bit but we might get a significant code performance increase.

Managed to make Range 12x more performant than using standard protoreflect.

@technicallyty
Copy link
Contributor

should we just target the main branch here and merge this in now?

@fdymylja fdymylja changed the base branch from ty/pulsar-msg_interface to main September 3, 2021 09:11
@technicallyty
Copy link
Contributor

technicallyty commented Sep 3, 2021

okay ive merged main into the branch, made fastreflect implement the feature interface, and removed all the copied protoc-gen-go code.

also i've shortened up fastreflection in places where one might manually type them like make fastreflection -> changed this to just make fast

I'll merge this in now

@technicallyty technicallyty merged commit 99bf1d0 into main Sep 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants