Skip to content

Conversation

@schmidt-sebastian
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian commented Nov 13, 2018

I am sending this out for early review, so I can get it merged by Friday. I haven't been able to run the integration tests (written on the bus to Santa Cruz), but I would appreciate some early comments.

Port of firebase/firebase-android-sdk#105 and firebase/firebase-js-sdk#1368

Copy link
Contributor

@var-const var-const left a comment

Choose a reason for hiding this comment

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

The most important comment is about detecting signed integer overflow.

return *transformation_.get();
}

const bool idempotent() const {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I don't think there is ever a reason to return a const primitive. It doesn't actually affect the behavior from the point of view of the caller who may assign the return value to either a const or a non-const variable. For non-primitive types, it's also generally isn't advisable, because const would prevent moving:

std::vector<int> make_huge_vector();
const std::vector<int> make_huge_vector_const();
auto foo = make_huge_vector(); // The vector is moved into foo
auto bar = make_huge_vector_const(); // No move here -- return value optimization should help, but it's not guaranteed

See Core Guidelines for additional details:

It is not recommended to return a const value. Such older advice is now obsolete; it does not add value, and it interferes with move semantics

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I didn't necessarily mean to do this. This might just have been more of a copy&paste snafu. Thanks for explaining!


- (void)expectApproximateLocalAndRemoteValue:(double)expectedSum {
FIRDocumentSnapshot *snap = [_accumulator awaitLocalEvent];
XCTAssertEqual(@(expectedSum), snap[@"sum"]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be XCTAssertEqualWithAccuracy?

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. Fixed.

I should have probably asked you not to review these tests until I was able to run them.

- (void)testIntegerIncrementWithExistingInteger {
[self writeInitialData:@{@"sum" : @1337}];
[self updateDocumentRef:_docRef data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}];
[self expectLocalAndRemoteValue:13378];
Copy link
Contributor

Choose a reason for hiding this comment

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

s/13378/1338

[self writeInitialData:@{@"sum" : @"overwrite"}];
[self updateDocumentRef:_docRef
data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1337]}];
[self expectLocalAndRemoteValue:1337];
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated to this PR: why was it decided to overwrite instead of reject in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The backend semantics for NUMERIC_ADD follow those of ARRA_UNION and coerce to the expected type first. This ensures consistency between all document transforms.

[self expectApproximateLocalAndRemoteValue:14.37];
}

- (void)testDoubleIncrementWithExistingInteger {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: maybe replace "double" in test name with "float", "floating point", or "real"? "DoubleIncrement" reads like "IncrementTwice", not "IncrementByADouble".

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 am going to push back on this suggestions purely out of laziness. I want these tests names to match across all platforms, and changing one name here requires me to port this change to Android and Web. Thanks for flagging this though.

class NumericIncrementTransform : public TransformOperation {
public:
NumericIncrementTransform(FSTNumberValue* operand)
: operand_(std::move(operand)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: std::move here is effectively a no-op (see one of the comments above).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

: operand_(std::move(operand)) {
}

~NumericIncrementTransform() override {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: defining an empty destructor shouldn't be necessary (unless the compiler complains, in which case can you copy the warning message?).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed from here and from all other TransformOperation subclasses.

if ([previousValue isKindOfClass:[FSTIntegerValue class]] &&
[operand_ isKindOfClass:[FSTIntegerValue class]]) {
int64_t sum = SafeIncrement(
(reinterpret_cast<FSTIntegerValue*>(previousValue)).internalValue,
Copy link
Contributor

Choose a reason for hiding this comment

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

Under C++ rules, static_cast would be sufficient throughout the function (it is valid to static_cast a base class to a derived class). Unless it doesn't play well with Objective-C, static_cast is preferable -- in general, it is best to use the least powerful cast applicable.

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'm still trying to wrap my head around this, but updated this to use static_cast.

* Implements integer addition. Overflows are resolved to LONG_MAX/LONG_MIN.
*/
int64_t SafeIncrement(int64_t x, int64_t y) const {
int64_t r = x + y;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is undefined behavior, and it's not safe to rely on any particular value of r. See this thread for some suggestions. Alternatively, a larger int type would be helpful, but unfortunately Abseil only has uint128, not int128, for the time being (I guess a workaround would be to use modulo, but it might end up harder to implement than checking).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ugh. Thanks for pointing this out. At least on Mac with LVM, it seems to work :)

I went with one of the suggestions from the SO post, which is actually pretty reasonable: Check for overflow/underflow before it occurs.


double OperandAsDouble() const {
if ([operand_ isKindOfClass:[FSTDoubleValue class]]) {
return (reinterpret_cast<FSTDoubleValue*>(operand_)).internalValue;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment about reinterpret_cast.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@schmidt-sebastian schmidt-sebastian changed the title WIP DO NOT MERGE: Adding FieldValue.increment() Nov 15, 2018
Copy link
Contributor Author

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

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

Thanks for your thorough review. (Most) comments addressed. The tests are passing against Hexa now.


- (void)expectApproximateLocalAndRemoteValue:(double)expectedSum {
FIRDocumentSnapshot *snap = [_accumulator awaitLocalEvent];
XCTAssertEqual(@(expectedSum), snap[@"sum"]);
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. Fixed.

I should have probably asked you not to review these tests until I was able to run them.

[self expectApproximateLocalAndRemoteValue:14.37];
}

- (void)testDoubleIncrementWithExistingInteger {
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 am going to push back on this suggestions purely out of laziness. I want these tests names to match across all platforms, and changing one name here requires me to port this change to Android and Web. Thanks for flagging this though.

[self writeInitialData:@{@"sum" : @"overwrite"}];
[self updateDocumentRef:_docRef
data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1337]}];
[self expectLocalAndRemoteValue:1337];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The backend semantics for NUMERIC_ADD follow those of ARRA_UNION and coerce to the expected type first. This ensures consistency between all document transforms.

[self expectApproximateLocalAndRemoteValue:13.37];
}

- (void)testMultipleDoubleIncrements {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These tests here test the integration with the backend, and as such am I inclined to not add too much edge case testing. I am testing the internal logic that deals with NaN and Infinity in the unit tests.

[self updateDocumentRef:_docRef
data:@{@"sum" : [FIRFieldValue fieldValueForDoubleIncrement:0.01]}];
[self updateDocumentRef:_docRef
data:@{@"sum" : [FIRFieldValue fieldValueForDoubleIncrement:0.01]}];
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, that's correct. Fixed.

if ([previousValue isKindOfClass:[FSTIntegerValue class]] &&
[operand_ isKindOfClass:[FSTIntegerValue class]]) {
int64_t sum = SafeIncrement(
(reinterpret_cast<FSTIntegerValue*>(previousValue)).internalValue,
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'm still trying to wrap my head around this, but updated this to use static_cast.


double OperandAsDouble() const {
if ([operand_ isKindOfClass:[FSTDoubleValue class]]) {
return (reinterpret_cast<FSTDoubleValue*>(operand_)).internalValue;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

class NumericIncrementTransform : public TransformOperation {
public:
NumericIncrementTransform(FSTNumberValue* operand)
: operand_(std::move(operand)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

: operand_(std::move(operand)) {
}

~NumericIncrementTransform() override {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed from here and from all other TransformOperation subclasses.

* Implements integer addition. Overflows are resolved to LONG_MAX/LONG_MIN.
*/
int64_t SafeIncrement(int64_t x, int64_t y) const {
int64_t r = x + y;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ugh. Thanks for pointing this out. At least on Mac with LVM, it seems to work :)

I went with one of the suggestions from the SO post, which is actually pretty reasonable: Check for overflow/underflow before it occurs.

// NOTE: The base state should only be applied if there's some existing document to
// override, so use a Precondition of exists=true
[baseMutations addObject:[[FSTPatchMutation alloc] initWithKey:mutation.key
fieldMask:FieldMask(*fieldMask)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would it make sense to std::move the mask into the copy constructor?

Copy link
Contributor

@wilhuff wilhuff Nov 16, 2018

Choose a reason for hiding this comment

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

That question seems somewhat nonsensical.

If you're copying you shouldn't move since the whole point of copying is to preserve the existing value.

If you want to move don't copy.

Looking at this more closely I think you want to copy, and the automatic copy you get should be sufficient:

        [baseMutations addObject:[[FSTPatchMutation alloc] initWithKey:mutation.key
                                                             fieldMask:*fieldMask
                                                                 value:baseValues
                                                          precondition:Precondition::Exists(true)]];```

Copy link
Contributor

@var-const var-const Nov 16, 2018

Choose a reason for hiding this comment

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

AFAICS, in this case std::move won't have any effect, because fieldMask points to a const object.

std::move is essentially a cast; simplifying a little bit, it's pretty much the same as static_cast<T&&>(foo). The only thing move does is affect the overload resolution:

void Foo(const Bar& bar); // Takes const lvalue reference
void Foo(Bar&& bar); // Takes rvalue reference
Bar bar;
Foo(bar); // Chooses the first overload (const lvalue reference)
Foo(std::move(bar)); // Chooses the second overload (rvalue reference)

So in this case, move could affect whether it's the copy constructor or the move constructor that gets selected. However, moving a const value results in a somewhat weird type const Foo&& (const rvalue reference), which cannot bind to a non-const rvalue argument; so when overload is being selected for const Foo&&, Foo&& is out of question, and const Foo& ends up being selected. This means that the copy constructor will be used in this case, which would have happened anyway without the move.

Note that moving an object is a commitment to never use the object again except to reassign to it; moving pretty much gives up possession of the object (at least, that's the case for non-POD objects). It's not unlike setting a pointer to null -- you can reassign a different pointer, but you can no longer access the object that the pointer was pointing to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the thorough explanation. I was worried it would create an extra copy of the FieldMask before it calls the copy-constructor, but now that I understand this better, I am no longer worried. I appreciate your explanation!

// NOTE: The base state should only be applied if there's some existing document to
// override, so use a Precondition of exists=true
[baseMutations addObject:[[FSTPatchMutation alloc] initWithKey:mutation.key
fieldMask:FieldMask(*fieldMask)
Copy link
Contributor

@var-const var-const Nov 16, 2018

Choose a reason for hiding this comment

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

AFAICS, in this case std::move won't have any effect, because fieldMask points to a const object.

std::move is essentially a cast; simplifying a little bit, it's pretty much the same as static_cast<T&&>(foo). The only thing move does is affect the overload resolution:

void Foo(const Bar& bar); // Takes const lvalue reference
void Foo(Bar&& bar); // Takes rvalue reference
Bar bar;
Foo(bar); // Chooses the first overload (const lvalue reference)
Foo(std::move(bar)); // Chooses the second overload (rvalue reference)

So in this case, move could affect whether it's the copy constructor or the move constructor that gets selected. However, moving a const value results in a somewhat weird type const Foo&& (const rvalue reference), which cannot bind to a non-const rvalue argument; so when overload is being selected for const Foo&&, Foo&& is out of question, and const Foo& ends up being selected. This means that the copy constructor will be used in this case, which would have happened anyway without the move.

Note that moving an object is a commitment to never use the object again except to reassign to it; moving pretty much gives up possession of the object (at least, that's the case for non-POD objects). It's not unlike setting a pointer to null -- you can reassign a different pointer, but you can no longer access the object that the pointer was pointing to.

: [FSTObjectValue objectValue];
// NOTE: The base state should only be applied if there's some existing document to
// override, so use a Precondition of exists=true
[baseMutations
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation.

for (FSTMutation *mutation in mutations) {
FSTMaybeDocument *maybeDocument = [existingDocuments objectForKey:mutation.key];
if (!mutation.idempotent) {
// Theoretically, we should only include non-idempotent fields in this field mask as
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I'm not sure I understand the comment. The way I'm reading it, it seems to say that ideally, we would only include non-idempotent fields, which is sufficient to prevent flicker. However, we also include idempotent fields, which has a downside. What I don't understand is why we include all fields if it has a downside and including only non-idempotent fields is sufficient?

// better implemented as spec tests but currently they don't support transforms.

- (void)testHandlesSetMutationThenTransformMutationThenTransformMutation {
if ([self isTestBaseClass]) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation!

Copy link
Contributor

@var-const var-const left a comment

Choose a reason for hiding this comment

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

Thanks for clarifying the comment!

@schmidt-sebastian
Copy link
Contributor Author

PR updated to use MaybeDocumentMap instead of FSTImmutableDocumentMap and to use new increment() Protobuf naming.

@wilhuff wilhuff changed the title DO NOT MERGE: Adding FieldValue.increment() Adding FieldValue.increment() Mar 6, 2019
@wilhuff
Copy link
Contributor

wilhuff commented Mar 6, 2019

Merged master, including the changes to make MutationQueue a C++ interface. @schmidt-sebastian PTAL and this is ready for merging.

@schmidt-sebastian
Copy link
Contributor Author

Thanks for the merge. PR looks good to me.

@schmidt-sebastian schmidt-sebastian merged commit 657f53a into master Mar 6, 2019
@paulb777 paulb777 deleted the mrschmidt-numericadd branch April 13, 2019 16:00
@firebase firebase locked and limited conversation to collaborators Oct 25, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants