You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TLDR;MapBuffer is a new sparse-array structure optimized for Java <-> C++ communication through JNI. It is 2-3x faster and more efficient with memory layout than existing methods of serialization (NativeMap and folly::dynamic).
Replacing folly::dynamic
With the new architecture, we are passing a lot of structured data between JVM and C++ core on the Android side of React Native. So far, the most utilized method was to pack the values into a JSON-like structure with folly::dynamic representation on C++ side and access it through ReadableNativeMap inside JVM.
Majority of data in Fabric core is strongly typed, however. Structure of ShadowNode props and state is usually statically known on C++ side, and we don't benefit from flexibility of folly::dynamic, with added overhead for most methods calling into JVM.
When reading from JVM, folly::dynamic isn't structured in the most efficient way for exporting the data through JNI, requiring multiple JNI calls (slow) and extra copies of data in both memory spaces.
When writing into C++, WritableNativeMap is storing the data in C++ memory directly, enforcing move semantics for efficiency with single-write objects. For Java developers, it is a hidden side effect, causing crashes whenever already consumed map is accessed or modified. At the same time, reading back from this map on JVM side results in full re-import of native values again, with all the problems of the previous bullet point.
All of these problems resulted in experiment to design a structure for efficient JNI communication of structured data. As it was using ByteBuffer to read data internally with map-like interface, we called it MapBuffer.
MapBuffer is a sparse array with constant write and log n read complexity. With N limited to 2^16 (~65.5k elements, see below), read operation shouldn't require more than 16 steps to find the value for maxed-out containers.
MapBuffer was designed with the goal of reducing amount of copies and JNI calls required to read the data from JVM. It is represented as a continuous chunk of memory read directly from C++ heap through ByteBuffer. Effectively, the values are copied once when added into buffer and once when read from it.
MapBuffer uses 16-bit unsigned integer as keys ([0;65536)) instead of strings for more efficient storage and lookup.
For writes from JVM, MapBuffer is backed by Android's SparseArray implementation, which uses similar storage mechanisms, copying internal data directly when transferred to JNI.
Why not use FlatBuffers or Protocol Buffers instead?
Both rely on codegen to create serialized representation, which increases complexity for both internal and OSS build setup. Generated classes impact binary size as well, which seems wasteful with plenty of places with JNI access.
It should be possible, however, to wrap MapBuffers with type-safe access classes by hand + we are planning to apply codegen in some places we already do, e.g. for ViewManager updates.
Performance and usage within React Native
While iterating on MapBuffer, we used microbenchmarks as a point of reference to avoid regressing performance, measuring reads/write speed for simple and complex objects.
Sample benchmark results:
Measured on Samsung Galaxy S21.
From C++, MapBuffer can be created with MapBufferBuilder:
const props = TextProps(...);
constexpruint16_t TEXT_PROP_IS_VALID_KEY = 0;
constexpruint16_t TEXT_PROP_LINE_HEIGHT_KEY = 1;
constexpruint16_t TEXT_PROP_TEXT_KEY = 2;
auto builder = MapBufferBuilder();
// The data copied into the builder as bytes
builder.putBool(TEXT_PROP_IS_VALID_KEY, props.isValid);
builder.putDouble(TEXT_PROP_LINE_HEIGHT_KEY, props.lineHeight);
builder.putString(TEXT_PROP_TEXT_KEY, props.text);
// Builder can be reused to create multiple MapBuffersauto mapBuffer = builder.build();
// MapBuffer is move-only on C++ side (consumed on conversion)auto readableMapBuffer = JReadableMapBuffer::createWithContents(
std::move(mapBuffer)
);
// Pass to JVM
...
In Java, ReadableMapBuffer provides access to serialized data:
MapBufferdata = ...;
// Random access// Throws if key is now found or data type doesn't match.// Check if value exists with `MapBuffer#contains`.// Check if data type is the same with `MapBuffer#getType`booleanisValid = data.getBoolean(TEXT_PROP_IS_VALID_KEY);
doublelineHeight = data.getDouble(TEXT_PROP_LINE_HEIGHT_KEY);
Stringtext = data.getString(TEXT_PROP_TEXT_KEY);
// Iterable accessfor (MapBuffer.Entryentry : data) {
intkey = entry.getKey();
switch (key) {
TEXT_PROP_IS_VALID_KEY:
booleanisValid = entry.getBoolean();
...
}
}
See TextLayoutManager (Java / C++) for more examples.
JVM to C++
In Java, create and push data to WritableMapBuffer. It also has all the capabilities of ReadableMapBuffer without additional serialization steps (=very fast when used within JVM).
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
TLDR;
MapBuffer
is a new sparse-array structure optimized for Java <-> C++ communication through JNI. It is 2-3x faster and more efficient with memory layout than existing methods of serialization (NativeMap
andfolly::dynamic
).Replacing folly::dynamic
With the new architecture, we are passing a lot of structured data between JVM and C++ core on the Android side of React Native. So far, the most utilized method was to pack the values into a JSON-like structure with
folly::dynamic
representation on C++ side and access it throughReadableNativeMap
inside JVM.Majority of data in Fabric core is strongly typed, however. Structure of
ShadowNode
props and state is usually statically known on C++ side, and we don't benefit from flexibility offolly::dynamic
, with added overhead for most methods calling into JVM.folly::dynamic
isn't structured in the most efficient way for exporting the data through JNI, requiring multiple JNI calls (slow) and extra copies of data in both memory spaces.WritableNativeMap
is storing the data in C++ memory directly, enforcing move semantics for efficiency with single-write objects. For Java developers, it is a hidden side effect, causing crashes whenever already consumed map is accessed or modified. At the same time, reading back from this map on JVM side results in full re-import of native values again, with all the problems of the previous bullet point.All of these problems resulted in experiment to design a structure for efficient JNI communication of structured data. As it was using
ByteBuffer
to read data internally with map-like interface, we called itMapBuffer
.MapBuffer
is a sparse array with constant write andlog n
read complexity. With N limited to 2^16 (~65.5k elements, see below), read operation shouldn't require more than 16 steps to find the value for maxed-out containers.MapBuffer
was designed with the goal of reducing amount of copies and JNI calls required to read the data from JVM. It is represented as a continuous chunk of memory read directly from C++ heap throughByteBuffer
. Effectively, the values are copied once when added into buffer and once when read from it.MapBuffer
uses 16-bit unsigned integer as keys ([0;65536)
) instead of strings for more efficient storage and lookup.MapBuffer
is backed by Android'sSparseArray
implementation, which uses similar storage mechanisms, copying internal data directly when transferred to JNI.MapBuffer.h
doc comment.Performance and usage within React Native
While iterating on
MapBuffer
, we used microbenchmarks as a point of reference to avoid regressing performance, measuring reads/write speed for simple and complex objects.Sample benchmark results:
Measured on Samsung Galaxy S21.Unfortunately, the benchmarks are not open sourced at the moment (they require instrumentation test setup).
MapBuffer
is used experimentally within Fabric for measuring text and updating props of<View />
component on Android, with positive performance results.Appendix: Code examples
Code snippets below show how to serialize and pass through JNI bridge a structure like this:
C++ to JVM
MapBuffer
can be created withMapBufferBuilder
:ReadableMapBuffer
provides access to serialized data:See TextLayoutManager (Java / C++) for more examples.
JVM to C++
WritableMapBuffer
. It also has all the capabilities ofReadableMapBuffer
without additional serialization steps (=very fast when used within JVM).MapBuffer
and read from it:Beta Was this translation helpful? Give feedback.
All reactions