-
Notifications
You must be signed in to change notification settings - Fork 134
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
RUMM-2786 Create AnyEncoder and AnyDecoder #1112
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was curious about the performance of this solution and if measured correctly it appears that using JSONEncoder is still faster. Take a look at this example:
func measurePerformance() throws {
let encoder = AnyEncoder()
let jsonEncoder = JSONEncoder()
struct Obj: Codable {
let type: String = .mockAny()
let id: String = .mockAny()
let isFavorited: Bool = .mockAny()
let attributes: Attributes = .init(title: .mockAny(), body: .mockAny())
struct Attributes: Codable {
let title: String
let body: String
}
}
let object = Obj()
let iterations = 100_000
let start = CFAbsoluteTimeGetCurrent()
for _ in 1...iterations {
let dict = try encoder.encode(object)
}
let diff = CFAbsoluteTimeGetCurrent() - start
print("#1 Took \(diff) seconds")
// #1 Took 2.4502270221710205 seconds
let start2 = CFAbsoluteTimeGetCurrent()
for _ in 1...iterations {
let dict2 = try JSONSerialization.jsonObject(with: jsonEncoder.encode(object), options: .allowFragments)
}
let diff2 = CFAbsoluteTimeGetCurrent() - start2
print("#2 Took \(diff2) seconds")
// #2 Took 1.6677980422973633 seconds
}
cfee556
to
e845c35
Compare
Datadog ReportBranch report: ✅ |
e845c35
to
5d54f09
Compare
Thanks for doing a quick benchmark, @maciejburda, it helped me find some optimizations. I'm not able to reach the combination of let encoder = AnyEncoder()
let decoder = AnyDecoder()
let jsonEncoder = JSONEncoder()
let jsonDecoder = JSONDecoder()
struct Obj: Codable {
let type: String = .mockAny()
let id: String = .mockAny()
let isFavorited: Bool = .mockAny()
let attributes: Attributes = .init(title: .mockAny(), body: .mockAny())
struct Attributes: Codable {
let title: String
let body: String
}
}
let object = Obj()
let iterations = 100_000
let start = CFAbsoluteTimeGetCurrent()
for _ in 1...iterations {
let dict = try encoder.encode(object)
_ = try decoder.decode(Obj.self, from: dict)
}
let diff = CFAbsoluteTimeGetCurrent() - start
print("#1 Took \(diff) seconds")
// #1 Took 2.508360981941223 seconds
let start2 = CFAbsoluteTimeGetCurrent()
for _ in 1...iterations {
let dict2 = try JSONSerialization.jsonObject(with: jsonEncoder.encode(object), options: .allowFragments)
_ = try jsonDecoder.decode(Obj.self, from: JSONSerialization.data(withJSONObject: dict2, options: .fragmentsAllowed))
}
let diff2 = CFAbsoluteTimeGetCurrent() - start2
print("#2 Took \(diff2) seconds")
// #2 Took 3.418305993080139 seconds |
5d54f09
to
07b0d48
Compare
I did further benchmarks: Device: iPhone 11 (A13 Bionic chip), iOS 16.2Model to serialize: struct Model: Codable {
var type: String = "type"
var id: String = "id"
var isFavorited: Bool = .random()
var attributes: Attributes = .init()
struct Attributes: Codable {
var title: String = "title"
var body: String = "body"
}
} Iterations: 100 000 Execution Time (in seconds)
CPU Profile (Weight)
Memory Allocation (Bytes Used)
Using such serialisation is obviously not free, but it performs better than the WDYT @ncreated @maciejburda ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Thanks for the extra effort on optimising 💪
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the idea 🚀⭐, and I believe it will be brilliant for passing events through message bus. The performance should not be a problem - thanks for taking care of that @maciejburda @maxep 👌🏎️ .
I left minor feedback on missing tests coverage and one proposal to IMO more convenient approach in testing.
/// Stores a keyed encoding container for the given key and returns it. | ||
/// | ||
/// - parameter keyType: The key type to use for the container. | ||
/// - parameter key: The key to encode the container for. | ||
/// - returns: A new keyed encoding container. | ||
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey { | ||
let container = KeyedContainer<NestedKey>( | ||
store: { self.set($0, forKey: key) }, | ||
path: codingPath + [key] | ||
) | ||
return KeyedEncodingContainer(container) | ||
} | ||
|
||
/// Stores an unkeyed encoding container for the given key and returns it. | ||
/// | ||
/// - parameter key: The key to encode the container for. | ||
/// - returns: A new unkeyed encoding container. | ||
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { | ||
UnkeyedContainer( | ||
store: { self.set($0, forKey: key) }, | ||
path: codingPath + [key] | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two have no coverage - seem to be not tested.
func testObjectDecoding() throws { | ||
let decoder = AnyDecoder() | ||
let object = try decoder.decode(Object.self, from: dictionary) | ||
|
||
XCTAssertEqual(object.id, id) | ||
XCTAssertEqual(object.date, .mockAny()) | ||
XCTAssertEqual(object.title, "Response") | ||
XCTAssertEqual(object.url, URL(string: "https://test.com/object/1")) | ||
XCTAssertNotNil(object.nested) | ||
XCTAssertEqual(object.nested.id, id) | ||
XCTAssertEqual(object.int, 12_345) | ||
XCTAssertNil(object.null) | ||
XCTAssertTrue(object.bool ?? false) | ||
XCTAssertNotNil(object.array) | ||
XCTAssertEqual(object.array?.underestimatedCount, 5) | ||
XCTAssertEqual(object.array?[0], AnyCodable(1)) | ||
} | ||
|
||
func testObjectEncoding() throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion/ These two tests ("object encoding" & "object encoding") seem quite tedious. They require maintaining both the struct Object
and dictionary: [String: Any?]
- making it easy to miss adding assertion. In fact, the assertions are already missing (I guess due to lack of convenience), e.g. we don't assert other elements in object.array
than the first one:
XCTAssertEqual(object.array?.underestimatedCount, 5)
XCTAssertEqual(object.array?[0], AnyCodable(1))
What if, instead of testing "encoding" and "decoding" separately, we check the actual behavior (property) of the new concept we introduce in this PR? I can imagine we define two objects:
struct ActualObject: Codable {
// various fields and nested type definitions
}
struct ExpectedObject: Codable {
// same fields and types as in ActualObject
}
and do simple test with leveraging our existing RandomMockable
and mirror comparison helpers:
func testEncodingAndDecoding() throws {
let encoder = AnyEncoder()
let decoder = AnyDecoder()
// Given
let expected: ExpectedObject = .mockRandom()
// When
let encoded = try encoder.encode(expected)
let actual: ActualObject = try decoder.decode(from: encoded)
// Then
XCTAssertTrue(equalsAny(lhs: actual, rhs: expected))
}
Some benefits of this approach:
- we will test against random values instead of hardcoded numbers or strings
- covering more use cases won't require changing test code, but only
ActualObject
+ExpectedObject
definitions - the rest of test bundle might benefit from any convenience helpers we need to add for it (e.g. one thing I spot is conforming
AnyCodable
toRandomMockable
) - (IMHO) it documents this new concept pretty well
Downside is that assertions won't be isolated, so the equalsAny()
will either pass or fail. This however can be improved by emitting proper error message in XCTFail()
from equalsAny(lhs:rhs:)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes indeed 👍 I think we would need some changes with EquitableInTests. Let me see if I can include that or do another PR.
07b0d48
to
5abba31
Compare
What and why?
Introduce implementation of
AnyEncoder
andAnyDecoder
which will allow defragmenting data-types for transfer on the message-bus.In the following example, a CrashContext is transmitted from the CrashReporting Feature to RUM through the message-bus:
This implementation will also be useful for the Flutter SDK to replace the dependency to DictionaryCoder. cc @fuzzybinary
Usage on the
FeatureBaggage
is done in #1113.How?
Encoding
The
AnyEncoder
implements theEncoder
protocol and is capable of encoding anEncodable
object to aAny
,[Any?]
, or[String: Any?]
representation.It will result in the same
object
as the following snippet, but without having to serialize toData
.Decoding
The
AnyDecoder
implements theDecoder
protocol and is capable of decoding aDecodable
object from anAny?
representation.It will result in the same
value
as the following snippet, but without having to serialize toData
.Review checklist
Custom CI job configuration (optional)