Skip to content

Commit 3368765

Browse files
sherginfacebook-github-bot
authored andcommitted
Introducing Timeline: A time-travel debugging tool (cross-platform part)
Summary: This is a core part of the Timeline feature (aka Time Travel Debugger). With these new primitives, any external library can initiate "saving" all the previous interface changes (commits) and unwind to any previous one (in order to introspect and validate visual side-effects). The next diff in the stack will implement UI for this feature integrated into Debug menu on iOS. Changelog: [Internal] Fabric-specific internal change. Reviewed By: sammy-SC Differential Revision: D25926660 fbshipit-source-id: 2e5f6892351d3053db8f64c1cf6ff445b0867ad7
1 parent 91b3f5d commit 3368765

File tree

12 files changed

+741
-0
lines changed

12 files changed

+741
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
load("@fbsource//tools/build_defs/apple:flag_defs.bzl", "get_preprocessor_flags_for_build_mode")
2+
load(
3+
"//tools/build_defs/oss:rn_defs.bzl",
4+
"ANDROID",
5+
"APPLE",
6+
"CXX",
7+
"fb_xplat_cxx_test",
8+
"get_apple_compiler_flags",
9+
"get_apple_inspector_flags",
10+
"react_native_xplat_target",
11+
"rn_xplat_cxx_library",
12+
"subdir_glob",
13+
)
14+
15+
APPLE_COMPILER_FLAGS = get_apple_compiler_flags()
16+
17+
rn_xplat_cxx_library(
18+
name = "timeline",
19+
srcs = glob(
20+
["**/*.cpp"],
21+
exclude = glob(["tests/**/*.cpp"]),
22+
),
23+
headers = glob(
24+
["**/*.h"],
25+
exclude = glob(["tests/**/*.h"]),
26+
),
27+
header_namespace = "",
28+
exported_headers = subdir_glob(
29+
[
30+
("", "*.h"),
31+
],
32+
prefix = "react/renderer/timeline",
33+
),
34+
compiler_flags = [
35+
"-fexceptions",
36+
"-frtti",
37+
"-std=c++14",
38+
"-Wall",
39+
],
40+
fbobjc_compiler_flags = APPLE_COMPILER_FLAGS,
41+
fbobjc_labels = ["supermodule:ios/isolation/infra.react_native"],
42+
fbobjc_preprocessor_flags = get_preprocessor_flags_for_build_mode() + get_apple_inspector_flags(),
43+
force_static = True,
44+
macosx_tests_override = [],
45+
platforms = (ANDROID, APPLE, CXX),
46+
preprocessor_flags = [
47+
"-DLOG_TAG=\"ReactNative\"",
48+
"-DWITH_FBSYSTRACE=1",
49+
],
50+
tests = [":tests"],
51+
visibility = ["PUBLIC"],
52+
deps = [
53+
"fbsource//xplat/fbsystrace:fbsystrace",
54+
"fbsource//xplat/folly:headers_only",
55+
"fbsource//xplat/folly:memory",
56+
"fbsource//xplat/folly:molly",
57+
"//third-party/glog:glog",
58+
react_native_xplat_target("react/utils:utils"),
59+
react_native_xplat_target("react/renderer/debug:debug"),
60+
react_native_xplat_target("react/renderer/core:core"),
61+
react_native_xplat_target("react/renderer/components/root:root"),
62+
react_native_xplat_target("react/renderer/mounting:mounting"),
63+
react_native_xplat_target("react/renderer/uimanager:uimanager"),
64+
],
65+
)
66+
67+
fb_xplat_cxx_test(
68+
name = "tests",
69+
srcs = glob(["tests/**/*.cpp"]),
70+
headers = glob(["tests/**/*.h"]),
71+
compiler_flags = [
72+
"-fexceptions",
73+
"-frtti",
74+
"-std=c++14",
75+
"-Wall",
76+
],
77+
contacts = ["oncall+react_native@xmail.facebook.com"],
78+
platforms = (ANDROID, APPLE, CXX),
79+
deps = [
80+
"fbsource//xplat/folly:molly",
81+
"fbsource//xplat/third-party/gmock:gtest",
82+
":timeline",
83+
],
84+
)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "Timeline.h"
9+
10+
#include <react/renderer/uimanager/UIManager.h>
11+
12+
namespace facebook {
13+
namespace react {
14+
15+
Timeline::Timeline(ShadowTree const &shadowTree) : shadowTree_(&shadowTree) {
16+
record(shadowTree.getCurrentRevision().rootShadowNode);
17+
};
18+
19+
#pragma mark - Public
20+
21+
SurfaceId Timeline::getSurfaceId() const noexcept {
22+
return shadowTree_->getSurfaceId();
23+
}
24+
25+
void Timeline::pause() const noexcept {
26+
std::lock_guard<std::recursive_mutex> lock(mutex_);
27+
assert(!paused_ && "");
28+
paused_ = true;
29+
}
30+
31+
void Timeline::resume() const noexcept {
32+
std::lock_guard<std::recursive_mutex> lock(mutex_);
33+
34+
if (snapshots_.size() > 0) {
35+
rewind(snapshots_.at(snapshots_.size() - 1));
36+
}
37+
38+
assert(paused_ && "");
39+
paused_ = false;
40+
}
41+
42+
bool Timeline::isPaused() const noexcept {
43+
std::lock_guard<std::recursive_mutex> lock(mutex_);
44+
return paused_;
45+
}
46+
47+
TimelineFrame::List Timeline::getFrames() const noexcept {
48+
std::lock_guard<std::recursive_mutex> lock(mutex_);
49+
50+
auto frames = TimelineFrame::List{};
51+
frames.reserve(snapshots_.size());
52+
for (auto const &snapshot : snapshots_) {
53+
frames.push_back(snapshot.getFrame());
54+
}
55+
return frames;
56+
}
57+
58+
TimelineFrame Timeline::getCurrentFrame() const noexcept {
59+
assert(snapshots_.size() > currentSnapshotIndex_);
60+
return snapshots_.at(currentSnapshotIndex_).getFrame();
61+
}
62+
63+
void Timeline::rewind(TimelineFrame const &frame) const noexcept {
64+
std::lock_guard<std::recursive_mutex> lock(mutex_);
65+
rewind(snapshots_.at(frame.getIndex()));
66+
}
67+
68+
RootShadowNode::Unshared Timeline::shadowTreeWillCommit(
69+
ShadowTree const &shadowTree,
70+
RootShadowNode::Shared const &oldRootShadowNode,
71+
RootShadowNode::Unshared const &newRootShadowNode) const noexcept {
72+
std::lock_guard<std::recursive_mutex> lock(mutex_);
73+
74+
if (rewinding_) {
75+
return newRootShadowNode;
76+
}
77+
78+
record(newRootShadowNode);
79+
80+
if (paused_) {
81+
return nullptr;
82+
}
83+
84+
return newRootShadowNode;
85+
}
86+
87+
#pragma mark - Private & Internal
88+
89+
void Timeline::record(
90+
RootShadowNode::Shared const &rootShadowNode) const noexcept {
91+
auto index = (int)snapshots_.size();
92+
snapshots_.push_back(TimelineSnapshot{rootShadowNode, index});
93+
94+
if (!paused_) {
95+
currentSnapshotIndex_ = index;
96+
}
97+
}
98+
99+
void Timeline::rewind(TimelineSnapshot const &snapshot) const noexcept {
100+
std::lock_guard<std::recursive_mutex> lock(mutex_);
101+
102+
currentSnapshotIndex_ = snapshot.getFrame().getIndex();
103+
104+
assert(!rewinding_ && "");
105+
rewinding_ = true;
106+
107+
auto rootShadowNode = snapshot.getRootShadowNode();
108+
109+
shadowTree_->commit(
110+
[&](RootShadowNode const &oldRootShadowNode) -> RootShadowNode::Unshared {
111+
return std::static_pointer_cast<RootShadowNode>(
112+
rootShadowNode->ShadowNode::clone({}));
113+
});
114+
115+
assert(rewinding_ && "");
116+
rewinding_ = false;
117+
}
118+
119+
} // namespace react
120+
} // namespace facebook
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <memory>
11+
#include <vector>
12+
13+
#include <better/mutex.h>
14+
15+
#include <react/renderer/core/ReactPrimitives.h>
16+
#include <react/renderer/timeline/TimelineSnapshot.h>
17+
#include <react/renderer/uimanager/UIManagerCommitHook.h>
18+
19+
namespace facebook {
20+
namespace react {
21+
22+
class UIManager;
23+
24+
class Timeline final {
25+
friend class TimelineHandler;
26+
friend class TimelineController;
27+
28+
public:
29+
Timeline(ShadowTree const &shadowTree);
30+
31+
private:
32+
#pragma mark - Private methods to be used by `TimelineHandler`.
33+
34+
void pause() const noexcept;
35+
void resume() const noexcept;
36+
bool isPaused() const noexcept;
37+
TimelineFrame::List getFrames() const noexcept;
38+
TimelineFrame getCurrentFrame() const noexcept;
39+
void rewind(TimelineFrame const &frame) const noexcept;
40+
SurfaceId getSurfaceId() const noexcept;
41+
42+
#pragma mark - Private methods to be used by `TimelineController`.
43+
44+
RootShadowNode::Unshared shadowTreeWillCommit(
45+
ShadowTree const &shadowTree,
46+
RootShadowNode::Shared const &oldRootShadowNode,
47+
RootShadowNode::Unshared const &newRootShadowNode) const noexcept;
48+
49+
#pragma mark - Private & Internal
50+
51+
void record(RootShadowNode::Shared const &rootShadowNode) const noexcept;
52+
void rewind(TimelineSnapshot const &snapshot) const noexcept;
53+
54+
mutable std::recursive_mutex mutex_;
55+
mutable ShadowTree const *shadowTree_{nullptr};
56+
mutable int currentSnapshotIndex_{0};
57+
mutable TimelineSnapshot::List snapshots_{};
58+
mutable bool paused_{false};
59+
mutable bool rewinding_{false};
60+
};
61+
62+
} // namespace react
63+
} // namespace facebook
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "TimelineController.h"
9+
10+
#include <react/renderer/mounting/ShadowTree.h>
11+
#include <react/renderer/uimanager/UIManager.h>
12+
13+
namespace facebook {
14+
namespace react {
15+
16+
TimelineHandler TimelineController::enable(SurfaceId surfaceId) const {
17+
assert(uiManager_);
18+
19+
auto shadowTreePtr = (ShadowTree const *){};
20+
21+
uiManager_->getShadowTreeRegistry().visit(
22+
surfaceId,
23+
[&](ShadowTree const &shadowTree) { shadowTreePtr = &shadowTree; });
24+
25+
assert(shadowTreePtr);
26+
27+
{
28+
std::unique_lock<better::shared_mutex> lock(timelinesMutex_);
29+
30+
auto timeline = std::make_unique<Timeline>(*shadowTreePtr);
31+
auto handler = TimelineHandler{*timeline};
32+
timelines_.emplace(surfaceId, std::move(timeline));
33+
return handler;
34+
}
35+
}
36+
37+
void TimelineController::disable(TimelineHandler &&handler) const {
38+
std::unique_lock<better::shared_mutex> lock(timelinesMutex_);
39+
40+
auto iterator = timelines_.find(handler.getSurfaceId());
41+
assert(iterator != timelines_.end());
42+
timelines_.erase(iterator);
43+
handler.release();
44+
}
45+
46+
void TimelineController::commitHookWasRegistered(
47+
UIManager const &uiManager) const noexcept {
48+
uiManager_ = &uiManager;
49+
}
50+
51+
void TimelineController::commitHookWasUnregistered(
52+
UIManager const &uiManager) const noexcept {
53+
uiManager_ = nullptr;
54+
}
55+
56+
RootShadowNode::Unshared TimelineController::shadowTreeWillCommit(
57+
ShadowTree const &shadowTree,
58+
RootShadowNode::Shared const &oldRootShadowNode,
59+
RootShadowNode::Unshared const &newRootShadowNode) const noexcept {
60+
std::shared_lock<better::shared_mutex> lock(timelinesMutex_);
61+
62+
assert(uiManager_ && "`uiManager_` must not be `nullptr`.");
63+
64+
lastUpdatedSurface_ = shadowTree.getSurfaceId();
65+
66+
auto iterator = timelines_.find(shadowTree.getSurfaceId());
67+
if (iterator == timelines_.end()) {
68+
return newRootShadowNode;
69+
}
70+
71+
auto &timeline = *iterator->second;
72+
return timeline.shadowTreeWillCommit(
73+
shadowTree, oldRootShadowNode, newRootShadowNode);
74+
}
75+
76+
} // namespace react
77+
} // namespace facebook

0 commit comments

Comments
 (0)