Skip to content

Commit

Permalink
Support for (de)serializing measure funcs
Browse files Browse the repository at this point in the history
Summary:
In addition to all the state that gets set on the node that is easy to serialize - like floats, enums, bools, etc - we also need to serialize measure functions. This is because these functions take a nontrivial amount of time up during layout and we should capture that. Also, they are important to the ability to truly replay layout as it was captured as the results of the measure functions determine many of the steps the layout algorithm takes.

Capturing this is rather tricky however, but I think I found a solution that is relatively simple and non-error prone. Essentially, since we are capturing the entire tree and virtually every input that goes into the flexbox algorithm, we *should* be able to replay layout exactly as it was captured. This means that the order in which measure functions are called *should* be the same. If this is the case, then all we need to do to capture the measure functions is store their input, output, and duration in a big array. During deserialization we just keep track of an index and use that to determine which measure function we should call. That is the premise behind what happens in this diff. In theory the algorithm could change and the capture would be wrong but it is easy enough to recapture again. Additionally we need to dirty the tree so that we get rid of caching which might omit some measure func calls

In order to capture you need to insert a method exposed by CaptureTree.h into the client measure func, which is kind of annoying but not that bad. In future diffs I will put a macro in place to make this even easier.

I also add our first capture! Which is of a large react native desktop app

Reviewed By: NickGerleman

Differential Revision: D53581121
  • Loading branch information
joevilches authored and facebook-github-bot committed Feb 14, 2024
1 parent cc66362 commit 2f4e311
Show file tree
Hide file tree
Showing 9 changed files with 24,568 additions and 212 deletions.
86 changes: 67 additions & 19 deletions benchmark/Benchmark.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,51 @@
* LICENSE file in the root directory of this source tree.
*/

#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <thread>

#include <benchmark/Benchmark.h>
#include <benchmark/TreeDeserialization.h>
#include <capture/CaptureTree.h>
#include <nlohmann/json.hpp>

namespace facebook::yoga {

using namespace nlohmann;
using namespace std::chrono;

constexpr uint32_t kNumRepititions = 1000;
constexpr uint32_t kNumRepititions = 100;
using SteadyClockDurations =
std::array<steady_clock::duration, kNumRepititions>;

YGSize mockMeasureFunc(
YGNodeConstRef node,
float availableWidth,
YGMeasureMode widthMode,
float availableHeight,
YGMeasureMode heightMode) {
(void)node;
(void)availableHeight;
(void)availableWidth;
(void)widthMode;
(void)heightMode;
MeasureFuncVecWithIndex* fns =
static_cast<MeasureFuncVecWithIndex*>(YGNodeGetContext(node));
if (fns->index >= fns->vec.size()) {
return {10.0, 10.0};
}

auto values = fns->vec.at(fns->index);
fns->index++;

std::this_thread::sleep_for(std::chrono::nanoseconds(values.durationNs));

return {values.outputWidth, values.outputHeight};
}

std::shared_ptr<const YGConfig> buildConfigFromJson(const json& j) {
json jsonConfig = j["config"];
std::shared_ptr<YGConfig> config(YGConfigNew(), YGConfigFree);
Expand Down Expand Up @@ -186,47 +215,63 @@ void setStylesFromJson(const json& j, YGNodeRef node) {

std::shared_ptr<YGNode> buildNodeFromJson(
const json& j,
std::shared_ptr<const YGConfig> config) {
std::shared_ptr<const YGConfig> config,
std::shared_ptr<MeasureFuncVecWithIndex> fns) {
std::shared_ptr<YGNode> node(YGNodeNewWithConfig(config.get()), YGNodeFree);
json nodeState = j["node"];

if (!j.contains("node") || j["node"].is_null()) {
return node;
}

json nodeState = j["node"];
for (json::iterator it = nodeState.begin(); it != nodeState.end(); it++) {
if (it.key() == "always-forms-containing-block") {
YGNodeSetAlwaysFormsContainingBlock(node.get(), it.value());
} else if (it.key() == "has-custom-measure" && it.value()) {
YGNodeSetContext(node.get(), fns.get());
YGNodeSetMeasureFunc(node.get(), mockMeasureFunc);
}
}

return node;
}

YogaNodeAndConfig
buildTreeFromJson(const json& j, YogaNodeAndConfig* parent, size_t index) {
std::shared_ptr<const YGConfig> config = buildConfigFromJson(j);
std::shared_ptr<YGNode> node = buildNodeFromJson(j, config);
YogaNodeAndConfig wrapper{node, config, std::vector<YogaNodeAndConfig>{}};
std::shared_ptr<YogaNodeAndConfig> buildTreeFromJson(
const json& j,
std::shared_ptr<MeasureFuncVecWithIndex> fns,
std::shared_ptr<YogaNodeAndConfig> parent,
size_t index) {
auto config = buildConfigFromJson(j);
auto node = buildNodeFromJson(j, config, fns);
auto wrapper = std::make_shared<YogaNodeAndConfig>(
node, config, std::vector<std::shared_ptr<YogaNodeAndConfig>>{});

if (parent != nullptr) {
YGNodeInsertChild(parent->node_.get(), node.get(), index);
parent->children_.push_back(wrapper);
}

setStylesFromJson(j, node.get());

json children = j["children"];
size_t childIndex = 0;
for (json child : children) {
buildTreeFromJson(child, &wrapper, childIndex);
childIndex++;
if (j.contains("children")) {
json children = j["children"];
size_t childIndex = 0;
for (json child : children) {
buildTreeFromJson(child, fns, wrapper, childIndex);
childIndex++;
}
}

return wrapper;
}

BenchmarkResult generateBenchmark(const std::filesystem::path& capturePath) {
std::ifstream captureFile(capturePath);
json capture = json::parse(captureFile);
BenchmarkResult generateBenchmark(json& capture) {
auto fns = std::make_shared<MeasureFuncVecWithIndex>();
populateMeasureFuncVec(capture["measure-funcs"], fns);

auto treeCreationBegin = steady_clock::now();
YogaNodeAndConfig root = buildTreeFromJson(capture, nullptr, 0 /*index*/);
std::shared_ptr<YogaNodeAndConfig> root =
buildTreeFromJson(capture["tree"], fns, nullptr, 0 /*index*/);
auto treeCreationEnd = steady_clock::now();

json layoutInputs = capture["layout-inputs"];
Expand All @@ -236,7 +281,7 @@ BenchmarkResult generateBenchmark(const std::filesystem::path& capturePath) {

auto layoutBegin = steady_clock::now();
YGNodeCalculateLayout(
root.node_.get(), availableWidth, availableHeight, direction);
root->node_.get(), availableWidth, availableHeight, direction);
auto layoutEnd = steady_clock::now();

return BenchmarkResult{
Expand Down Expand Up @@ -278,8 +323,11 @@ void benchmark(std::filesystem::path& capturesDir) {
SteadyClockDurations layoutDurations;
SteadyClockDurations totalDurations;

std::ifstream captureFile(capture.path());
json j = json::parse(captureFile);

for (uint32_t i = 0; i < kNumRepititions; i++) {
BenchmarkResult result = generateBenchmark(capture.path());
BenchmarkResult result = generateBenchmark(j);
treeCreationDurations[i] = result.treeCreationDuration;
layoutDurations[i] = result.layoutDuration;
totalDurations[i] = result.treeCreationDuration + result.layoutDuration;
Expand Down
8 changes: 7 additions & 1 deletion benchmark/Benchmark.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@
namespace facebook::yoga {

struct YogaNodeAndConfig {
YogaNodeAndConfig(
std::shared_ptr<YGNode> node,
std::shared_ptr<const YGConfig> config,
std::vector<std::shared_ptr<YogaNodeAndConfig>> children)
: node_(node), config_(config), children_(children) {}

std::shared_ptr<YGNode> node_;
std::shared_ptr<const YGConfig> config_;
std::vector<YogaNodeAndConfig> children_;
std::vector<std::shared_ptr<YogaNodeAndConfig>> children_;
};

struct BenchmarkResult {
Expand Down
Loading

0 comments on commit 2f4e311

Please sign in to comment.