-
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-2429 [SR] Capture UIViews
and UILabels
semantics
#985
RUMM-2429 [SR] Capture UIViews
and UILabels
semantics
#985
Conversation
internal struct NodesFlattener { | ||
/// This current implementation is greedy and works in `O(n*log(n))`, wheares `O(n)` is possible. | ||
/// TODO: RUMM-2461 Improve flattening performance. |
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.
There are yet no unit tests for NodesFlattener
as it is still not in its final shape 🚧. The RUMM-2461
will focus solely on flattening algorithm and then tests will become mandatory.
/// Both `AmbiguousElement` and `SpecificElement` provide an implementation of `NodeWireframesBuilder` which describes | ||
/// how to construct SR wireframes for UI elements they refer to. No builder is provided for `InvisibleElement` and `UnknownElement`. |
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.
Because of lack of NodeWireframesBuilder
in "unknown" and "invisible" semantics, earlier I tried to model NodeSemantics
as enum:
enum NodeSemantics {
case unknownElement
case invisibleElement
case ambiguousElement(wireframesBuilder: NodeWireframesBuilder)
case specificElement(wireframesBuilder: NodeWireframesBuilder)
}
While it looked better here in declaration, it was very cumbersome for use both in Recorder
and Processor
(flattening part). It required guard let case
pattern matching and was forcing numerous code branches.
I found the "protocol
+ optionals" approach to be more efficient in this kind of problem.
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.
Great job here!! it reads super well and the separation of concerns is perfect.
I'm wondering if the genericity of NodeRecorder
has reached its purpose. Since it only supports subclasses of UIView
, and the type-check is made at runtime. I think we could benefit from removing the associatedtype
and check the view type in its concrete implementations. LMKWYT!
...eplay/Sources/DatadogSessionReplay/Recorder/SnapshotBuilder/NodeRecorders/NodeRecorder.swift
Outdated
Show resolved
Hide resolved
init<NR: NodeRecorder>(_ concreteRecorder: NR) { | ||
self.erasedMethod = { view, node, context in | ||
guard let concreteView = view as? NR.View else { | ||
// Means that the `UIView` instance passed to erased `NodeRecorder` was not of the type | ||
// that this recorder supports. In that case, semantics cannot be determined. | ||
return nil | ||
} | ||
return concreteRecorder.semantics(of: concreteView, with: node, in: context) | ||
} | ||
} |
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.
It can be checked at compile time instead.
init<NR: NodeRecorder>(_ concreteRecorder: NR) { | |
self.erasedMethod = { view, node, context in | |
guard let concreteView = view as? NR.View else { | |
// Means that the `UIView` instance passed to erased `NodeRecorder` was not of the type | |
// that this recorder supports. In that case, semantics cannot be determined. | |
return nil | |
} | |
return concreteRecorder.semantics(of: concreteView, with: node, in: context) | |
} | |
} | |
init<NR: NodeRecorder>(_ concreteRecorder: NR) where NR.View == View { | |
self.erasedMethod = { view, node, context in | |
return concreteRecorder.semantics(of: concreteView, with: node, in: context) | |
} | |
} |
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.
Ok, so actually no, it can't.
After reading the usage of NodeRecorder
and its type-erasure, I wonder if the associatedtype
is actually necessary. The type check is made at runtime so it can jump on the next recorder, the generic constraint is not applied here. I think this should be made explicit by using this interface:
internal protocol NodeRecorder {
func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeSnapshotBuilder.Context) -> NodeSemantics?
}
The concrete implementation of a recorder can return nil
if the type is not supported.
internal struct UILabelRecorder: NodeRecorder {
func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeSnapshotBuilder.Context) -> NodeSemantics? {
guard let label = view as? UILabel else {
return nil
}
...
}
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.
Yeah, the main purpose of erasure was to avoid repeating guard let
in all implementations. But your point is correct - all views are instance of UIView
and derived type, so it could be avoided. I'm OK to remove erasures for the sake of simplicity 👍 - less constraints could be even better as I can't fully foresee what other challenges are there in UIKit / SwiftUI hierarchies.
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 went with guard let ... else { nil }
way - it's simpler and not that tedious anyway 👍.
...eplay/Sources/DatadogSessionReplay/Recorder/SnapshotBuilder/NodeRecorders/NodeRecorder.swift
Outdated
Show resolved
Hide resolved
session-replay/Sources/DatadogSessionReplay/Recorder/SnapshotBuilder/ViewTreeSnapshot.swift
Show resolved
Hide resolved
session-replay/Sources/DatadogSessionReplay/Recorder/SnapshotBuilder/ViewTreeSnapshot.swift
Outdated
Show resolved
Hide resolved
session-replay/Sources/DatadogSessionReplay/Recorder/SnapshotBuilder/ViewTreeSnapshot.swift
Show resolved
Hide resolved
...n-replay/Sources/DatadogSessionReplay/Recorder/SnapshotBuilder/ViewTreeSnapshotBuilder.swift
Outdated
Show resolved
Hide resolved
...n-replay/Sources/DatadogSessionReplay/Recorder/SnapshotBuilder/ViewTreeSnapshotBuilder.swift
Outdated
Show resolved
Hide resolved
Agree - done it and it feels better now 👍 - there was not much gain from |
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.
Awesome!! looks really good and the documentation is on point.
Thank you for the PR description, it really helped!
What and why?
📦 This PR enhances Session Replay with recognising certain node semantics for
UIViews
andUILabels
. For both, the recorder now captures more information on the native view and puts it into the JSON prepared for SR player.The
Recorder
now captures:UIView
:UILabel
:How?
Proposed implementation introduces a small framework prepared for capturing semantics of other nodes in extensible way promoting OCP (
UITextField
,UIImageView
,UISwitch
will come in next PR).Each view traversed by
ViewTreeSnapshotBuilder
gets queried by a sequence ofNodeRecorders
:Each recorder gets asked for a semantics of given
view
:To avoid querying all recorders for each
view
, the iteration stops as soon as best semantics is found. Pessimistically, this givesO(N * M)
time complexity on the main thread, whereN
is number of views andM
is number of node recorders. Given thatM
is constant and some recorders (e.g.UISwitchRecorder
) will not require inspecting sub-tree, the effective estimate will beO(N)
.To enable different optimisations and stay open for future extensibility of
Recorder
(to support more types in ambiguous view trees), theNodeSemantics
is defined as a broad interface:with 4 semantics introduced in this PR:
These 4 semantics are enough to handle basics, incl.:
UIViewRecorder
,UILabelRecorder
,UIImageViewRecorder
,UITextFieldRecorder
andUISwitchRecorder
. We may want to introduce more semantics when dealing with elements that require very much different rendering (like alerts or popovers displayed on top of other views). I haven't yet looked into this.Review checklist
Custom CI job configuration (optional)