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

GODRIVER-1808 Fix BSON unmarshaling into an interface containing a concrete value. #1584

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

qingyang-hu
Copy link
Collaborator

GODRIVER-1808

Summary

This PR fixes BSON's unmarshaling behavior for interfaces by passing reflect.Value instead of reflect.Type, so the actual value type contained in the interface can be used.
Especially for an interface containing a pointer, this PR unmarshals value into the pointed concrete value.

Background & Motivation

This PR provides a more flexible way of unmarshaling and makes the behavior closer to the JSON package.

return nil, err
var vDecoder *ValueDecoder
var tDecoder *typeDecoder
if !(eType.Kind() == reflect.Interface && val.Len() > 0) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Delay the decoder retrieval for interface slice/array so each element will be decoded based on its type.

Copy link

API Change Report

./x/mongo/driver

incompatible changes

##CursorResponse.Connection: changed from *./x/mongo/driver/mnet.Connection to PinnedConnection
##ErrorProcessor.ProcessError: changed from func(error, ./x/mongo/driver/mnet.Describer) ProcessErrorResult to func(error, Connection) ProcessErrorResult
##Handshaker.FinishHandshake: changed from func(context.Context, *./x/mongo/driver/mnet.Connection) error to func(context.Context, Connection) error
##Handshaker.GetHandshakeInformation: changed from func(context.Context, ./mongo/address.Address, *./x/mongo/driver/mnet.Connection) (HandshakeInformation, error) to func(context.Context, ./mongo/address.Address, Connection) (HandshakeInformation, error)
##Operation.ExecuteExhaust: changed from func(context.Context, *./x/mongo/driver/mnet.Connection) error to func(context.Context, StreamerConnection) error
##ResponseInfo.Connection: changed from ./x/mongo/driver/mnet.Connection to Connection
##Server.Connection: changed from func(context.Context) (
./x/mongo/driver/mnet.Connection, error) to func(context.Context) (Connection, error)
##SingleConnectionDeployment.C: changed from ./x/mongo/driver/mnet.Connection to Connection
##SingleConnectionDeployment.Connection: changed from func(context.Context) (
./x/mongo/driver/mnet.Connection, error) to func(context.Context) (Connection, error)

compatible changes

Compressor: added
Connection: added
PinnedConnection: added
StreamerConnection: added

./x/mongo/driver/auth

incompatible changes

##Config.Connection: changed from *./x/mongo/driver/mnet.Connection to ./x/mongo/driver.Connection

compatible changes

Config.Description: added

./x/mongo/driver/drivertest

incompatible changes

(*ChannelConn).Read: removed
(*ChannelConn).Write: removed

compatible changes

(*ChannelConn).ReadWireMessage: added
(*ChannelConn).WriteWireMessage: added

./x/mongo/driver/mnet

incompatible changes

package removed

./x/mongo/driver/operation

incompatible changes

##(*Hello).FinishHandshake: changed from func(context.Context, *./x/mongo/driver/mnet.Connection) error to func(context.Context, ./x/mongo/driver.Connection) error
##(*Hello).GetHandshakeInformation: changed from func(context.Context, ./mongo/address.Address, *./x/mongo/driver/mnet.Connection) (./x/mongo/driver.HandshakeInformation, error) to func(context.Context, ./mongo/address.Address, ./x/mongo/driver.Connection) (./x/mongo/driver.HandshakeInformation, error)
##(*Hello).StreamResponse: changed from func(context.Context, *./x/mongo/driver/mnet.Connection) error to func(context.Context, ./x/mongo/driver.StreamerConnection) error

./x/mongo/driver/session

incompatible changes

LoadBalancedTransactionConnection.ReadWireMessage: added
LoadBalancedTransactionConnection.WriteWireMessage: added
##./x/mongo/driver/mnet.ReadWriteCloser.Read, method set of LoadBalancedTransactionConnection: removed
##./x/mongo/driver/mnet.ReadWriteCloser.Write, method set of LoadBalancedTransactionConnection: removed

./x/mongo/driver/topology

incompatible changes

(*Connection).Read: removed
(*Connection).Write: removed
##(Server).Connection: changed from func(context.Context) (./x/mongo/driver/mnet.Connection, error) to func(context.Context) (./x/mongo/driver.Connection, error)
##(*Server).ProcessError: changed from func(error, ./x/mongo/driver/mnet.Describer) ./x/mongo/driver.ProcessErrorResult to func(error, ./x/mongo/driver.Connection) ./x/mongo/driver.ProcessErrorResult

compatible changes

(*Connection).ReadWireMessage: added
(*Connection).WriteWireMessage: added

@mongodb-drivers-pr-bot mongodb-drivers-pr-bot bot added the priority-3-low Low Priority PR for Review label Mar 20, 2024
@qingyang-hu qingyang-hu marked this pull request as ready for review March 20, 2024 22:16
@qingyang-hu qingyang-hu changed the title GODRIVER-1808 GODRIVER-1808 Fix BSON unmarshaling into an interface containing a concrete value. Mar 21, 2024
Copy link
Collaborator

@matthewdale matthewdale left a comment

Choose a reason for hiding this comment

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

We should update the implementation to remove the added calls to reflect.New, which seems to increase memory allocs in some cases. Is there a way to pass a reflect.Type when we only have type information, and pass a reflect.Value when we have value and type information?

I noticed BenchmarkUnmarshal reports about 30% more allocations with this PR. Keep in mind that BenchmarkUnmarshal actually only benchmarks unmarhshaling into a map. BenchmarkCodeUnmarshal, which unmarshals into a struct, shows no difference, so it seems to be isolated to map unmarshaling.

❯ benchstat old.txt new.txt                                                                                                       
goos: darwin
goarch: arm64
pkg: go.mongodb.org/mongo-driver/bson
                                    │   old.txt   │               new.txt                │
                                    │   sec/op    │    sec/op     vs base                │
Unmarshal/simple_struct/BSON-10       3.423µ ± 0%   3.961µ ± 19%  +15.72% (p=0.000 n=10)
Unmarshal/nested_struct/BSON-10       8.605µ ± 1%   9.258µ ±  0%   +7.59% (p=0.000 n=10)
Unmarshal/deep_bson.json.gz/BSON-10   41.98µ ± 0%   46.45µ ±  1%  +10.66% (p=0.000 n=10)
Unmarshal/flat_bson.json.gz/BSON-10   41.88µ ± 1%   47.73µ ±  0%  +13.97% (p=0.000 n=10)
Unmarshal/full_bson.json.gz/BSON-10   49.61µ ± 0%   55.90µ ±  0%  +12.67% (p=0.000 n=10)
geomean                               19.14µ        21.45µ        +12.09%

                                    │   old.txt    │               new.txt                │
                                    │     B/op     │     B/op      vs base                │
Unmarshal/simple_struct/BSON-10       1.062Ki ± 0%   1.375Ki ± 0%  +29.41% (p=0.000 n=10)
Unmarshal/nested_struct/BSON-10       7.927Ki ± 0%   8.411Ki ± 0%   +6.11% (p=0.000 n=10)
Unmarshal/deep_bson.json.gz/BSON-10   30.15Ki ± 0%   33.12Ki ± 0%   +9.85% (p=0.000 n=10)
Unmarshal/flat_bson.json.gz/BSON-10   13.11Ki ± 0%   16.79Ki ± 0%  +28.09% (p=0.000 n=10)
Unmarshal/full_bson.json.gz/BSON-10   19.93Ki ± 0%   23.57Ki ± 0%  +18.27% (p=0.000 n=10)
geomean                               9.212Ki        10.87Ki       +17.97%

                                    │  old.txt   │               new.txt               │
                                    │ allocs/op  │  allocs/op   vs base                │
Unmarshal/simple_struct/BSON-10       62.00 ± 0%    86.00 ± 0%  +38.71% (p=0.000 n=10)
Unmarshal/nested_struct/BSON-10       143.0 ± 0%    178.0 ± 0%  +24.48% (p=0.000 n=10)
Unmarshal/deep_bson.json.gz/BSON-10   822.0 ± 0%   1012.0 ± 0%  +23.11% (p=0.000 n=10)
Unmarshal/flat_bson.json.gz/BSON-10   740.0 ± 0%   1030.0 ± 0%  +39.19% (p=0.000 n=10)
Unmarshal/full_bson.json.gz/BSON-10   797.0 ± 0%   1086.0 ± 0%  +36.26% (p=0.000 n=10)
geomean                               336.2         444.4       +32.16%

Here's a graph view of a memory profile of running BenchmarkUnmarshal on the previous commit, filtered to only reflect calls:
Screenshot 2024-04-19 at 7 06 21 PM
And here's the same profile of the PR commit, filtered to only reflect calls:
Screenshot 2024-04-19 at 7 06 30 PM

The allocs from reflect.New grew from 0.28GB to 1GB, which seems to be the biggest change between the two profiles.

@qingyang-hu qingyang-hu marked this pull request as draft April 22, 2024 16:29
Copy link
Collaborator

@matthewdale matthewdale left a comment

Choose a reason for hiding this comment

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

Investigating this further, there are a few quick changes we can make to keep the allocations identical to the base branch.

}

func decodeTypeOrValueWithInfo(vd ValueDecoder, td typeDecoder, dc DecodeContext, vr bsonrw.ValueReader, t reflect.Type, convert bool) (reflect.Value, error) {
func decodeTypeOrValueWithInfo(vd ValueDecoder, td typeDecoder, dc DecodeContext, vr bsonrw.ValueReader, val reflect.Value, convert bool) (reflect.Value, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should change the reflect.Value parameter back to reflect.Type because all we do with that reflect.Value is extract the type. Instead, we should pass the reflect.Type to avoid the unnecessary reflect.New allocations.

val.Index(idx).Set(v)
e = v
}
elem, err = decodeTypeOrValueWithInfo(valueDecoder, typeDecoder, dc, vr, e, true)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should call e.Type() here instead of passing the reflect.Value.

Suggested change
elem, err = decodeTypeOrValueWithInfo(valueDecoder, typeDecoder, dc, vr, e, true)
elem, err = decodeTypeOrValueWithInfo(valueDecoder, typeDecoder, dc, vr, e.Type(), true)

elem = reflect.Zero(val.Index(idx).Type())
}
} else {
elem, err = decodeTypeOrValueWithInfo(vDecoder, tDecoder, dc, vr, reflect.New(eType).Elem(), true)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should pass the type here instead of allocating a new reflect.Value.

Suggested change
elem, err = decodeTypeOrValueWithInfo(vDecoder, tDecoder, dc, vr, reflect.New(eType).Elem(), true)
elem, err = decodeTypeOrValueWithInfo(vDecoder, tDecoder, dc, vr, eType, true)

@@ -213,7 +213,7 @@ func (mc *MapCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val ref
return err
}

elem, err := decodeTypeOrValueWithInfo(decoder, eTypeDecoder, dc, vr, eType, true)
elem, err := decodeTypeOrValueWithInfo(decoder, eTypeDecoder, dc, vr, reflect.New(eType).Elem(), true)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should pass the type here instead of allocating a new reflect.Value.

Suggested change
elem, err := decodeTypeOrValueWithInfo(decoder, eTypeDecoder, dc, vr, reflect.New(eType).Elem(), true)
elem, err := decodeTypeOrValueWithInfo(decoder, eTypeDecoder, dc, vr, eType, true)

Comment on lines +350 to +351
val := reflect.New(t).Elem()
return decodeTypeOrValueWithInfo(decoder, td, dc, vr, val, true)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should pass the type here instead of allocating a new reflect.Value.

Suggested change
val := reflect.New(t).Elem()
return decodeTypeOrValueWithInfo(decoder, td, dc, vr, val, true)
return decodeTypeOrValueWithInfo(decoder, td, dc, vr, t, true)

@@ -185,7 +185,7 @@ func (dvd DefaultValueDecoders) DDecodeValue(dc DecodeContext, vr bsonrw.ValueRe
}

// Pass false for convert because we don't need to call reflect.Value.Convert for tEmpty.
elem, err := decodeTypeOrValueWithInfo(decoder, tEmptyTypeDecoder, dc, elemVr, tEmpty, false)
elem, err := decodeTypeOrValueWithInfo(decoder, tEmptyTypeDecoder, dc, elemVr, reflect.New(tEmpty).Elem(), false)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should pass the type here instead of allocating a new reflect.Value.

Suggested change
elem, err := decodeTypeOrValueWithInfo(decoder, tEmptyTypeDecoder, dc, elemVr, reflect.New(tEmpty).Elem(), false)
elem, err := decodeTypeOrValueWithInfo(decoder, tEmptyTypeDecoder, dc, elemVr, tEmpty, false)

Copy link
Collaborator

@prestonvasquez prestonvasquez left a comment

Choose a reason for hiding this comment

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

LGTM, with updates proposed by Matt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority-3-low Low Priority PR for Review
Projects
None yet
3 participants