Adapt ReadableMapBuffer to MapBuffer interface

Summary:
Updates `ReadableMapBuffer` to conform to `MapBuffer` interface, to allow interchangeable use of `Readable/WritableMapBuffer` in the code.

Notable changes:
- MapBuffer.Entry getters are now represented as Kotlin properties and appended `Value` suffix (e.g. `entry.getInt()` becomes `entry.getIntValue()` in Java, or `entry.intValue` in Kotlin)
- `ByteBuffer` is imported in constructor instead of on demand. This method doesn't copy the data as the bytes are read directly from native heap, and benchmarking a 500-byte `MapBuffer` shows no difference in import speed.
- Internal logic of `ReadableMapBuffer` uses `UShort` kotlin type for key retrieval, for more correct representation of values.
- Explicit exception throws are replaced with `require` and `check` methods for `IllegalArgumentException` and `IllegalStateException` (default FB conversion).

The change also updates `ReadableMapBuffer` usages to `MapBuffer` interface where possible (virtually everywhere except JNI methods).

Changelog: [Android][Changed] - Adopt `MapBuffer` interface for `ReadableMapBuffer`

Reviewed By: mdvacca

Differential Revision: D35218633

fbshipit-source-id: 515dd974c27b2978ade325b2e1750ab8f068a20a
This commit is contained in:
Andrei Shikov
2022-03-30 20:27:23 -07:00
committed by Facebook GitHub Bot
parent cf6f3b680b
commit 81e4249315
14 changed files with 460 additions and 589 deletions
@@ -0,0 +1,33 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.common.mapbuffer
import com.facebook.react.bridge.ReactMarker
import com.facebook.react.bridge.ReactMarkerConstants
import com.facebook.soloader.SoLoader
import com.facebook.systrace.Systrace
object MapBufferSoLoader {
@Volatile private var didInit = false
@JvmStatic
fun staticInit() {
if (didInit) {
return
}
didInit = true
Systrace.beginSection(
Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
"ReadableMapBufferSoLoader.staticInit::load:mapbufferjni")
ReactMarker.logMarker(ReactMarkerConstants.LOAD_REACT_NATIVE_MAPBUFFER_SO_FILE_START)
SoLoader.loadLibrary("mapbufferjni")
ReactMarker.logMarker(ReactMarkerConstants.LOAD_REACT_NATIVE_MAPBUFFER_SO_FILE_END)
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE)
}
}
@@ -1,418 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.common.mapbuffer;
import androidx.annotation.Nullable;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Iterator;
/**
* TODO T83483191: add documentation.
*
* <p>NOTE: {@link ReadableMapBuffer} is NOT thread safe.
*/
public class ReadableMapBuffer implements Iterable<ReadableMapBuffer.MapBufferEntry> {
static {
ReadableMapBufferSoLoader.staticInit();
}
/**
* Data types supported by MapBuffer. Keep in sync with definition in `MapBuffer.h`, as enum
* serialization relies on correct order.
*/
public enum DataType {
BOOL,
INT,
DOUBLE,
STRING,
MAP;
}
// Value used to verify if the data is serialized with LittleEndian order.
private static final int ALIGNMENT = 0xFE;
// 8 bytes = 2 (alignment) + 2 (count) + 4 (size)
private static final int HEADER_SIZE = 8;
// 10 bytes = 2 (key) + 2 (type) + 8 (value)
private static final int BUCKET_SIZE = 12;
// 2 bytes = 2 (key)
private static final int TYPE_OFFSET = 2;
// 4 bytes = 2 (key) + 2 (type)
private static final int VALUE_OFFSET = 4;
private static final int INT_SIZE = 4;
@Nullable ByteBuffer mBuffer = null;
// Amount of items serialized on the ByteBuffer
private int mCount = 0;
@DoNotStrip
private ReadableMapBuffer(HybridData hybridData) {
mHybridData = hybridData;
}
private ReadableMapBuffer(ByteBuffer buffer) {
mBuffer = buffer;
readHeader();
}
private native ByteBuffer importByteBuffer();
@SuppressWarnings("unused")
@DoNotStrip
@Nullable
private HybridData mHybridData;
private static int getKeyOffsetForBucketIndex(int bucketIndex) {
return HEADER_SIZE + BUCKET_SIZE * bucketIndex;
}
// returns the relative offset of the first byte of dynamic data
private int getOffsetForDynamicData() {
// TODO T83483191: check if there's dynamic data?
return getKeyOffsetForBucketIndex(mCount);
}
/**
* @param key Key to search for
* @return the "bucket index" for a key or -1 if not found. It uses a binary search algorithm
* (log(n))
*/
private int getBucketIndexForKey(int key) {
importByteBufferAndReadHeader();
int lo = 0;
int hi = getCount() - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = readUnsignedShort(getKeyOffsetForBucketIndex(mid));
if (midVal < key) {
lo = mid + 1;
} else if (midVal > key) {
hi = mid - 1;
} else {
return mid;
}
}
return -1;
}
private DataType readDataType(int bucketIndex) {
int value = readUnsignedShort(getKeyOffsetForBucketIndex(bucketIndex) + TYPE_OFFSET);
return DataType.values()[value];
}
private int getTypedValueOffsetForKey(int key, DataType expected) {
int bucketIndex = getBucketIndexForKey(key);
if (bucketIndex == -1) {
throw new IllegalArgumentException("Key not found: " + key);
}
DataType dataType = readDataType(bucketIndex);
if (dataType != expected) {
throw new IllegalStateException(
"Expected "
+ expected
+ " for key: "
+ key
+ " found "
+ dataType.toString()
+ " instead.");
}
return getKeyOffsetForBucketIndex(bucketIndex) + VALUE_OFFSET;
}
private int readUnsignedShort(int bufferPosition) {
return mBuffer.getShort(bufferPosition) & 0xFFFF;
}
private double readDoubleValue(int bufferPosition) {
return mBuffer.getDouble(bufferPosition);
}
private int readIntValue(int bufferPosition) {
return mBuffer.getInt(bufferPosition);
}
private boolean readBooleanValue(int bufferPosition) {
return readIntValue(bufferPosition) == 1;
}
private String readStringValue(int bufferPosition) {
int offset = getOffsetForDynamicData() + mBuffer.getInt(bufferPosition);
int sizeOfString = mBuffer.getInt(offset);
byte[] result = new byte[sizeOfString];
int stringOffset = offset + INT_SIZE;
mBuffer.position(stringOffset);
mBuffer.get(result, 0, sizeOfString);
return new String(result);
}
private ReadableMapBuffer readMapBufferValue(int position) {
int offset = getOffsetForDynamicData() + mBuffer.getInt(position);
int sizeMapBuffer = mBuffer.getInt(offset);
byte[] buffer = new byte[sizeMapBuffer];
int bufferOffset = offset + INT_SIZE;
mBuffer.position(bufferOffset);
mBuffer.get(buffer, 0, sizeMapBuffer);
return new ReadableMapBuffer(ByteBuffer.wrap(buffer));
}
private void readHeader() {
// byte order
short storedAlignment = mBuffer.getShort();
if (storedAlignment != ALIGNMENT) {
mBuffer.order(ByteOrder.LITTLE_ENDIAN);
}
// count
mCount = readUnsignedShort(mBuffer.position());
}
/**
* Binary search of the key inside the mapBuffer (log(n)).
*
* @param key Key to search for
* @return true if and only if the Key received as a parameter is stored in the MapBuffer.
*/
public boolean hasKey(int key) {
// TODO T83483191: Add tests
return getBucketIndexForKey(key) != -1;
}
@Nullable
public DataType getType(int key) {
int bucketIndex = getBucketIndexForKey(key);
if (bucketIndex == -1) {
throw new IllegalArgumentException("Key not found: " + key);
}
return readDataType(bucketIndex);
}
/** @return amount of elements stored into the MapBuffer */
public int getCount() {
importByteBufferAndReadHeader();
return mCount;
}
/**
* @param key {@link int} representing the key
* @return return the int associated to the Key received as a parameter.
*/
public int getInt(int key) {
return readIntValue(getTypedValueOffsetForKey(key, DataType.INT));
}
/**
* @param key {@link int} representing the key
* @return return the double associated to the Key received as a parameter.
*/
public double getDouble(int key) {
return readDoubleValue(getTypedValueOffsetForKey(key, DataType.DOUBLE));
}
/**
* @param key {@link int} representing the key
* @return return the int associated to the Key received as a parameter.
*/
public String getString(int key) {
return readStringValue(getTypedValueOffsetForKey(key, DataType.STRING));
}
public boolean getBoolean(int key) {
return readBooleanValue(getTypedValueOffsetForKey(key, DataType.BOOL));
}
/**
* @param key {@link int} representing the key
* @return return the int associated to the Key received as a parameter.
*/
public ReadableMapBuffer getMapBuffer(int key) {
return readMapBufferValue(getTypedValueOffsetForKey(key, DataType.MAP));
}
/**
* Import ByteBuffer from C++, read the header and move the current cursor at the start of the
* payload.
*/
private ByteBuffer importByteBufferAndReadHeader() {
if (mBuffer != null) {
return mBuffer;
}
// mBuffer = importByteBufferAllocateDirect();
mBuffer = importByteBuffer();
readHeader();
return mBuffer;
}
private void assertKeyExists(int key, int bucketIndex) {
int storedKey = readUnsignedShort(getKeyOffsetForBucketIndex(bucketIndex));
if (storedKey != key) {
throw new IllegalStateException(
"Stored key doesn't match parameter - expected: " + key + " - found: " + storedKey);
}
}
@Override
public int hashCode() {
ByteBuffer byteBuffer = importByteBufferAndReadHeader();
byteBuffer.rewind();
return byteBuffer.hashCode();
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof ReadableMapBuffer)) {
return false;
}
ReadableMapBuffer other = (ReadableMapBuffer) obj;
ByteBuffer thisByteBuffer = importByteBufferAndReadHeader();
ByteBuffer otherByteBuffer = other.importByteBufferAndReadHeader();
if (thisByteBuffer == otherByteBuffer) {
return true;
}
thisByteBuffer.rewind();
otherByteBuffer.rewind();
return thisByteBuffer.equals(otherByteBuffer);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("{");
for (MapBufferEntry entry : this) {
int key = entry.getKey();
builder.append(key);
builder.append('=');
switch (entry.getType()) {
case BOOL:
builder.append(entry.getBoolean());
break;
case INT:
builder.append(entry.getInt());
break;
case DOUBLE:
builder.append(entry.getDouble());
break;
case STRING:
builder.append(entry.getString());
break;
case MAP:
builder.append(entry.getReadableMapBuffer().toString());
break;
}
builder.append(',');
}
builder.append('}');
return builder.toString();
}
/** @return an {@link Iterator<MapBufferEntry>} for the entries of this MapBuffer. */
@Override
public Iterator<MapBufferEntry> iterator() {
return new Iterator<MapBufferEntry>() {
int current = 0;
final int last = getCount() - 1;
@Override
public boolean hasNext() {
return current <= last;
}
@Override
public MapBufferEntry next() {
return new MapBufferEntry(getKeyOffsetForBucketIndex(current++));
}
};
}
/** This class represents an Entry of the {@link ReadableMapBuffer} class. */
public class MapBufferEntry {
private final int mBucketOffset;
private MapBufferEntry(int position) {
mBucketOffset = position;
}
private void assertType(DataType expected) {
DataType dataType = getType();
if (expected != dataType) {
throw new IllegalStateException(
"Expected "
+ expected
+ " for key: "
+ getKey()
+ " found "
+ dataType.toString()
+ " instead.");
}
}
/** @return a {@link short} that represents the key of this {@link MapBufferEntry}. */
public int getKey() {
return readUnsignedShort(mBucketOffset);
}
public DataType getType() {
return DataType.values()[readUnsignedShort(mBucketOffset + TYPE_OFFSET)];
}
/** @return the double value that is stored in this {@link MapBufferEntry}. */
public double getDouble() {
// TODO T83483191 Extend serialization of MapBuffer to return null if there's no value
// stored in this MapBufferEntry.
assertType(DataType.DOUBLE);
return readDoubleValue(mBucketOffset + VALUE_OFFSET);
}
/** @return the int value that is stored in this {@link MapBufferEntry}. */
public int getInt() {
assertType(DataType.INT);
return readIntValue(mBucketOffset + VALUE_OFFSET);
}
/** @return the boolean value that is stored in this {@link MapBufferEntry}. */
public boolean getBoolean() {
assertType(DataType.BOOL);
return readBooleanValue(mBucketOffset + VALUE_OFFSET);
}
/** @return the String value that is stored in this {@link MapBufferEntry}. */
public String getString() {
assertType(DataType.STRING);
return readStringValue(mBucketOffset + VALUE_OFFSET);
}
/**
* @return the {@link ReadableMapBuffer} value that is stored in this {@link MapBufferEntry}.
*/
public ReadableMapBuffer getReadableMapBuffer() {
assertType(DataType.MAP);
return readMapBufferValue(mBucketOffset + VALUE_OFFSET);
}
}
}
@@ -0,0 +1,298 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.common.mapbuffer
import com.facebook.jni.HybridData
import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.common.mapbuffer.MapBuffer.Companion.KEY_RANGE
import java.lang.StringBuilder
import java.nio.ByteBuffer
import java.nio.ByteOrder
import javax.annotation.concurrent.NotThreadSafe
/**
* Read-only implementation of the [MapBuffer], imported from C++ environment. Use
* `<react/common/mapbuffer/jni/JReadableMapBuffer.h> to create it.
*
* See [MapBuffer] documentation for more details
*/
@NotThreadSafe
@DoNotStrip
class ReadableMapBuffer : MapBuffer {
// Hybrid data must be kept in the `mHybridData` field for fbjni to work
@field:DoNotStrip private val mHybridData: HybridData?
// Byte data of the mapBuffer
private val buffer: ByteBuffer
// Amount of items serialized on the ByteBuffer
override var count = 0
private set
@DoNotStrip
private constructor(hybridData: HybridData) {
this.mHybridData = hybridData
this.buffer = importByteBuffer()
readHeader()
}
private constructor(buffer: ByteBuffer) {
this.mHybridData = null
this.buffer = buffer
readHeader()
}
private external fun importByteBuffer(): ByteBuffer
private fun readHeader() {
// byte order
val storedAlignment = buffer.short
if (storedAlignment.toInt() != ALIGNMENT) {
buffer.order(ByteOrder.LITTLE_ENDIAN)
}
// count
count = readUnsignedShort(buffer.position()).toInt()
}
// returns the relative offset of the first byte of dynamic data
private val offsetForDynamicData: Int
get() = getKeyOffsetForBucketIndex(count)
/**
* @param key Key to search for
* @return the "bucket index" for a key or -1 if not found. It uses a binary search algorithm
* (log(n))
*/
private fun getBucketIndexForKey(intKey: Int): Int {
if (intKey !in KEY_RANGE) {
return -1
}
val key = intKey.toUShort()
var lo = 0
var hi = count - 1
while (lo <= hi) {
val mid = lo + hi ushr 1
val midVal = readUnsignedShort(getKeyOffsetForBucketIndex(mid))
when {
midVal < key -> lo = mid + 1
midVal > key -> hi = mid - 1
else -> return mid
}
}
return -1
}
private fun readDataType(bucketIndex: Int): MapBuffer.DataType {
val value = readUnsignedShort(getKeyOffsetForBucketIndex(bucketIndex) + TYPE_OFFSET).toInt()
return MapBuffer.DataType.values()[value]
}
private fun getTypedValueOffsetForKey(key: Int, expected: MapBuffer.DataType): Int {
val bucketIndex = getBucketIndexForKey(key)
require(bucketIndex != -1) { "Key not found: $key" }
val dataType = readDataType(bucketIndex)
check(!(dataType !== expected)) { "Expected $expected for key: $key, found $dataType instead." }
return getKeyOffsetForBucketIndex(bucketIndex) + VALUE_OFFSET
}
private fun readUnsignedShort(bufferPosition: Int): UShort {
return buffer.getShort(bufferPosition).toUShort()
}
private fun readDoubleValue(bufferPosition: Int): Double {
return buffer.getDouble(bufferPosition)
}
private fun readIntValue(bufferPosition: Int): Int {
return buffer.getInt(bufferPosition)
}
private fun readBooleanValue(bufferPosition: Int): Boolean {
return readIntValue(bufferPosition) == 1
}
private fun readStringValue(bufferPosition: Int): String {
val offset = offsetForDynamicData + buffer.getInt(bufferPosition)
val sizeOfString = buffer.getInt(offset)
val result = ByteArray(sizeOfString)
val stringOffset = offset + Int.SIZE_BYTES
buffer.position(stringOffset)
buffer[result, 0, sizeOfString]
return String(result)
}
private fun readMapBufferValue(position: Int): ReadableMapBuffer {
val offset = offsetForDynamicData + buffer.getInt(position)
val sizeMapBuffer = buffer.getInt(offset)
val newBuffer = ByteArray(sizeMapBuffer)
val bufferOffset = offset + Int.SIZE_BYTES
buffer.position(bufferOffset)
buffer[newBuffer, 0, sizeMapBuffer]
return ReadableMapBuffer(ByteBuffer.wrap(newBuffer))
}
private fun getKeyOffsetForBucketIndex(bucketIndex: Int): Int {
return HEADER_SIZE + BUCKET_SIZE * bucketIndex
}
override fun contains(key: Int): Boolean {
// TODO T83483191: Add tests
return getBucketIndexForKey(key) != -1
}
override fun getKeyOffset(key: Int): Int = getBucketIndexForKey(key)
override fun entryAt(offset: Int): MapBuffer.Entry =
MapBufferEntry(getKeyOffsetForBucketIndex(offset))
override fun getType(key: Int): MapBuffer.DataType {
val bucketIndex = getBucketIndexForKey(key)
require(bucketIndex != -1) { "Key not found: $key" }
return readDataType(bucketIndex)
}
override fun getInt(key: Int): Int =
readIntValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.INT))
override fun getDouble(key: Int): Double =
readDoubleValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.DOUBLE))
override fun getString(key: Int): String =
readStringValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.STRING))
override fun getBoolean(key: Int): Boolean =
readBooleanValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.BOOL))
override fun getMapBuffer(key: Int): ReadableMapBuffer =
readMapBufferValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.MAP))
override fun hashCode(): Int {
buffer.rewind()
return buffer.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other !is ReadableMapBuffer) {
return false
}
val thisByteBuffer = buffer
val otherByteBuffer = other.buffer
if (thisByteBuffer === otherByteBuffer) {
return true
}
thisByteBuffer.rewind()
otherByteBuffer.rewind()
return thisByteBuffer == otherByteBuffer
}
override fun toString(): String {
val builder = StringBuilder("{")
for (entry in this) {
val key = entry.key
builder.append(key)
builder.append('=')
when (entry.type) {
MapBuffer.DataType.BOOL -> builder.append(entry.booleanValue)
MapBuffer.DataType.INT -> builder.append(entry.intValue)
MapBuffer.DataType.DOUBLE -> builder.append(entry.doubleValue)
MapBuffer.DataType.STRING -> builder.append(entry.stringValue)
MapBuffer.DataType.MAP -> builder.append(entry.mapBufferValue.toString())
}
builder.append(',')
}
builder.append('}')
return builder.toString()
}
override fun iterator(): Iterator<MapBuffer.Entry> {
return object : Iterator<MapBuffer.Entry> {
var current = 0
val last = count - 1
override fun hasNext(): Boolean {
return current <= last
}
override fun next(): MapBuffer.Entry {
return MapBufferEntry(getKeyOffsetForBucketIndex(current++))
}
}
}
private inner class MapBufferEntry(private val bucketOffset: Int) : MapBuffer.Entry {
private fun assertType(expected: MapBuffer.DataType) {
val dataType = type
check(!(expected !== dataType)) {
("Expected " +
expected +
" for key: " +
key +
" found " +
dataType.toString() +
" instead.")
}
}
override val key: Int
get() = readUnsignedShort(bucketOffset).toInt()
override val type: MapBuffer.DataType
get() = MapBuffer.DataType.values()[readUnsignedShort(bucketOffset + TYPE_OFFSET).toInt()]
override val doubleValue: Double
get() {
assertType(MapBuffer.DataType.DOUBLE)
return readDoubleValue(bucketOffset + VALUE_OFFSET)
}
override val intValue: Int
get() {
assertType(MapBuffer.DataType.INT)
return readIntValue(bucketOffset + VALUE_OFFSET)
}
override val booleanValue: Boolean
get() {
assertType(MapBuffer.DataType.BOOL)
return readBooleanValue(bucketOffset + VALUE_OFFSET)
}
override val stringValue: String
get() {
assertType(MapBuffer.DataType.STRING)
return readStringValue(bucketOffset + VALUE_OFFSET)
}
override val mapBufferValue: MapBuffer
get() {
assertType(MapBuffer.DataType.MAP)
return readMapBufferValue(bucketOffset + VALUE_OFFSET)
}
}
companion object {
// Value used to verify if the data is serialized with LittleEndian order.
private const val ALIGNMENT = 0xFE
// 8 bytes = 2 (alignment) + 2 (count) + 4 (size)
private const val HEADER_SIZE = 8
// 10 bytes = 2 (key) + 2 (type) + 8 (value)
private const val BUCKET_SIZE = 12
// 2 bytes = 2 (key)
private const val TYPE_OFFSET = 2
// 4 bytes = 2 (key) + 2 (type)
private const val VALUE_OFFSET = 4
init {
MapBufferSoLoader.staticInit()
}
}
}
@@ -1,33 +0,0 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.common.mapbuffer;
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.soloader.SoLoader;
import com.facebook.systrace.Systrace;
public class ReadableMapBufferSoLoader {
private static volatile boolean sDidInit = false;
public static void staticInit() {
if (sDidInit) {
return;
}
Systrace.beginSection(
Systrace.TRACE_TAG_REACT_JAVA_BRIDGE,
"ReadableMapBufferSoLoader.staticInit::load:mapbufferjni");
ReactMarker.logMarker(ReactMarkerConstants.LOAD_REACT_NATIVE_MAPBUFFER_SO_FILE_START);
SoLoader.loadLibrary("mapbufferjni");
ReactMarker.logMarker(ReactMarkerConstants.LOAD_REACT_NATIVE_MAPBUFFER_SO_FILE_END);
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
sDidInit = true;
}
}
@@ -164,7 +164,7 @@ class WritableMapBuffer : MapBuffer {
companion object {
init {
ReadableMapBufferSoLoader.staticInit()
MapBufferSoLoader.staticInit()
}
}
}