Skip to content

Commit dc4c36e

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Move screenshot Base64 encoding to trace serialization (facebook#55803)
Summary: Pull Request resolved: facebook#55803 Refactor Android FrameTimings to reduce memory usage in trace buffer. Binary data instead of Base64 should be ~25% smaller, and eliminates string copies during JNI transfer. Changelog: [Internal] Reviewed By: rubennorte Differential Revision: D94657907 fbshipit-source-id: c4a183ff00c519fd1545992340fd445530abab99
1 parent dc8c442 commit dc4c36e

9 files changed

Lines changed: 49 additions & 20 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingSequence.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ internal data class FrameTimingSequence(
1212
val threadId: Int,
1313
val beginTimestamp: Long,
1414
val endTimestamp: Long,
15-
val screenshot: String? = null,
15+
val screenshot: ByteArray? = null,
1616
)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import android.os.Build
1212
import android.os.Handler
1313
import android.os.Looper
1414
import android.os.Process
15-
import android.util.Base64
1615
import android.view.FrameMetrics
1716
import android.view.PixelCopy
1817
import android.view.Window
@@ -109,7 +108,7 @@ internal class FrameTimingsObserver(
109108
}
110109

111110
// Must be called from the main thread so that PixelCopy captures the current frame.
112-
private fun captureScreenshot(callback: (String?) -> Unit) {
111+
private fun captureScreenshot(callback: (ByteArray?) -> Unit) {
113112
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
114113
callback(null)
115114
return
@@ -143,7 +142,12 @@ internal class FrameTimingsObserver(
143142
)
144143
}
145144

146-
private fun encodeScreenshot(window: Window, bitmap: Bitmap, width: Int, height: Int): String? {
145+
private fun encodeScreenshot(
146+
window: Window,
147+
bitmap: Bitmap,
148+
width: Int,
149+
height: Int,
150+
): ByteArray? {
147151
var scaledBitmap: Bitmap? = null
148152
return try {
149153
val density = window.context.resources.displayMetrics.density
@@ -155,9 +159,9 @@ internal class FrameTimingsObserver(
155159
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Bitmap.CompressFormat.WEBP_LOSSY
156160
else Bitmap.CompressFormat.JPEG
157161

158-
ByteArrayOutputStream().use { outputStream ->
162+
ByteArrayOutputStream(SCREENSHOT_OUTPUT_SIZE_HINT).use { outputStream ->
159163
scaledBitmap.compress(compressFormat, SCREENSHOT_QUALITY, outputStream)
160-
Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
164+
outputStream.toByteArray()
161165
}
162166
} catch (e: Exception) {
163167
null
@@ -170,5 +174,10 @@ internal class FrameTimingsObserver(
170174
companion object {
171175
private const val SCREENSHOT_SCALE_FACTOR = 0.75f
172176
private const val SCREENSHOT_QUALITY = 80
177+
178+
// Capacity hint for the ByteArrayOutputStream used during bitmap
179+
// compression. Sized slightly above typical compressed output to minimise
180+
// internal buffer resizing.
181+
private const val SCREENSHOT_OUTPUT_SIZE_HINT = 65536 // 64 KB
173182
}
174183
}

packages/react-native/ReactAndroid/src/main/jni/react/runtime/jni/JReactHostInspectorTarget.h

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#pragma once
99

10+
#include <fbjni/ByteBuffer.h>
1011
#include <fbjni/fbjni.h>
1112

1213
#include <jsinspector-modern/HostTarget.h>
@@ -98,13 +99,17 @@ struct JFrameTimingSequence : public jni::JavaClass<JFrameTimingSequence> {
9899
std::chrono::steady_clock::time_point(std::chrono::nanoseconds(getFieldValue(field))));
99100
}
100101

101-
std::optional<std::string> getScreenshot() const
102+
std::optional<std::vector<uint8_t>> getScreenshot() const
102103
{
103-
auto field = javaClassStatic()->getField<jstring>("screenshot");
104+
auto field = javaClassStatic()->getField<jbyteArray>("screenshot");
104105
auto javaScreenshot = getFieldValue(field);
105106
if (javaScreenshot) {
106-
auto jstring = jni::static_ref_cast<jni::JString>(javaScreenshot);
107-
return jstring->toStdString();
107+
auto size = static_cast<size_t>(javaScreenshot->size());
108+
if (size > 0) {
109+
std::vector<uint8_t> result(size);
110+
javaScreenshot->getRegion(0, javaScreenshot->size(), reinterpret_cast<jbyte *>(result.data()));
111+
return result;
112+
}
108113
}
109114
return std::nullopt;
110115
}

packages/react-native/ReactCommon/jsinspector-modern/tests/TracingTest.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ TEST_F(TracingTest, EmitsScreenshotEventWhenScreenshotValuePassed) {
103103
11, // threadId
104104
now,
105105
now + HighResDuration::fromNanoseconds(50),
106-
"base64EncodedScreenshotData"));
106+
std::vector<uint8_t>{}));
107107

108108
auto allTraceEvents = endTracingAndCollectEvents();
109109
EXPECT_THAT(allTraceEvents, Contains(AtJsonPtr("/name", "Screenshot")));

packages/react-native/ReactCommon/jsinspector-modern/tracing/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ target_link_libraries(jsinspector_tracing
2222
jsinspector_network
2323
oscompat
2424
react_timing
25+
react_utils
2526
)
2627
target_compile_reactnative_options(jsinspector_tracing PRIVATE)
2728
target_compile_options(jsinspector_tracing PRIVATE -Wpedantic)

packages/react-native/ReactCommon/jsinspector-modern/tracing/FrameTimingSequence.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
#include <react/timing/primitives.h>
1313

14+
#include <cstdint>
15+
#include <optional>
16+
#include <vector>
17+
1418
namespace facebook::react::jsinspector_modern::tracing {
1519

1620
using FrameSequenceId = uint64_t;
@@ -26,7 +30,7 @@ struct FrameTimingSequence {
2630
ThreadId threadId,
2731
HighResTimeStamp beginTimestamp,
2832
HighResTimeStamp endTimestamp,
29-
std::optional<std::string> screenshot = std::nullopt)
33+
std::optional<std::vector<uint8_t>> screenshot = std::nullopt)
3034
: id(id),
3135
threadId(threadId),
3236
beginTimestamp(beginTimestamp),
@@ -49,9 +53,9 @@ struct FrameTimingSequence {
4953
HighResTimeStamp endTimestamp;
5054

5155
/**
52-
* Optional screenshot data (base64 encoded) captured during the frame.
56+
* Optional screenshot data captured during the frame.
5357
*/
54-
std::optional<std::string> screenshot;
58+
std::optional<std::vector<uint8_t>> screenshot;
5559
};
5660

5761
} // namespace facebook::react::jsinspector_modern::tracing

packages/react-native/ReactCommon/jsinspector-modern/tracing/React-jsinspectortracing.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Pod::Spec.new do |s|
4747
s.dependency "React-jsi"
4848
s.dependency "React-oscompat"
4949
s.dependency "React-timing"
50+
add_dependency(s, "React-utils", :additional_framework_paths => ["react/utils/platform/ios"])
5051

5152
if use_hermes()
5253
s.dependency "hermes-engine"

packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.cpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#include "Timing.h"
1010
#include "TracingCategory.h"
1111

12+
#include <react/utils/Base64.h>
13+
1214
namespace facebook::react::jsinspector_modern::tracing {
1315

1416
/* static */ TraceEvent TraceEventGenerator::createSetLayerTreeIdEvent(
@@ -70,14 +72,19 @@ TraceEventGenerator::createFrameTimingsEvents(
7072
/* static */ TraceEvent TraceEventGenerator::createScreenshotEvent(
7173
FrameSequenceId frameSequenceId,
7274
int sourceId,
73-
std::string&& snapshot,
75+
std::vector<uint8_t>&& snapshot,
7476
HighResTimeStamp expectedDisplayTime,
7577
ProcessId processId,
7678
ThreadId threadId) {
77-
folly::dynamic args = folly::dynamic::object("snapshot", std::move(snapshot))(
78-
"source_id", sourceId)("frame_sequence", frameSequenceId)(
79-
"expected_display_time",
80-
highResTimeStampToTracingClockTimeStamp(expectedDisplayTime));
79+
// Convert binary data to string for Base64 encoding
80+
std::string snapshotBytes(snapshot.begin(), snapshot.end());
81+
std::string base64Snapshot = base64Encode(snapshotBytes);
82+
83+
folly::dynamic args =
84+
folly::dynamic::object("snapshot", std::move(base64Snapshot))(
85+
"source_id", sourceId)("frame_sequence", frameSequenceId)(
86+
"expected_display_time",
87+
highResTimeStampToTracingClockTimeStamp(expectedDisplayTime));
8188

8289
return TraceEvent{
8390
.name = "Screenshot",

packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEventGenerator.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
#include <jsinspector-modern/tracing/FrameTimingSequence.h>
1313
#include <react/timing/primitives.h>
1414

15+
#include <cstdint>
1516
#include <utility>
17+
#include <vector>
1618

1719
namespace facebook::react::jsinspector_modern::tracing {
1820

@@ -49,7 +51,7 @@ class TraceEventGenerator {
4951
static TraceEvent createScreenshotEvent(
5052
FrameSequenceId frameSequenceId,
5153
int sourceId,
52-
std::string &&snapshot,
54+
std::vector<uint8_t> &&snapshot,
5355
HighResTimeStamp expectedDisplayTime,
5456
ProcessId processId,
5557
ThreadId threadId);

0 commit comments

Comments
 (0)