Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 50adc086 authored by Zimuzo Ezeozue's avatar Zimuzo Ezeozue
Browse files

Switch from GetUTFStringChars to GetStringRegion

Replaced GetUTFStringChars/ReleaseUTFStringChars with a more efficient
direct string conversion using GetStringRegion. This eliminates
unnecessary memory allocations and copies while decoding String from
Java into JNI.

Fast path
- thread_local buffers that serve as temp string storage
while building the tracing event.
- Replaced ScopedUtfChars with string_views into these buffers,
eliminating strdup calls
Slow path
- Still uses the GetStringRegion to get the utf16 from Java but simply allocate
a new string to hold the ascii converted version instead of splicing
string_views over the (already full) TLS buffer

Fixed a potential crash where extra args that spill over the default pool
sizes are not referenced and can be GC'd before they are emitted. Appended
any pending PerfettoPointer objects to a list that's freed after emitting.

Test: atest PerfettoTest
Bug: 303199244
Flag: android.os.perfetto_sdk_tracing_v2
Change-Id: I34bc3dee062544de8194989e657ff658f76110b2
parent 74b083ad
Loading
Loading
Loading
Loading
+17 −4
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ import dalvik.annotation.optimization.FastNative;

import libcore.util.NativeAllocationRegistry;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;

@@ -50,6 +52,7 @@ public final class PerfettoTrackEventExtra {
    private static final Supplier<FieldString> sFieldStringSupplier = FieldString::new;
    private static final Supplier<FieldNested> sFieldNestedSupplier = FieldNested::new;

    private final List<PerfettoPointer> mPendingPointers = new ArrayList<>();
    private CounterInt64 mCounterInt64;
    private CounterDouble mCounterDouble;
    private Proto mProto;
@@ -592,7 +595,7 @@ public final class PerfettoTrackEventExtra {
            checkContainer();
            FieldInt64 field = mFieldInt64Cache.get(sFieldInt64Supplier);
            field.setValue(id, val);
            mCurrentContainer.addField(field);
            mExtra.addPerfettoPointer(mCurrentContainer, field);
            return this;
        }

@@ -601,7 +604,7 @@ public final class PerfettoTrackEventExtra {
            checkContainer();
            FieldDouble field = mFieldDoubleCache.get(sFieldDoubleSupplier);
            field.setValue(id, val);
            mCurrentContainer.addField(field);
            mExtra.addPerfettoPointer(mCurrentContainer, field);
            return this;
        }

@@ -610,7 +613,7 @@ public final class PerfettoTrackEventExtra {
            checkContainer();
            FieldString field = mFieldStringCache.get(sFieldStringSupplier);
            field.setValue(id, val);
            mCurrentContainer.addField(field);
            mExtra.addPerfettoPointer(mCurrentContainer, field);
            return this;
        }

@@ -635,7 +638,7 @@ public final class PerfettoTrackEventExtra {
            checkContainer();
            FieldNested field = mFieldNestedCache.get(sFieldNestedSupplier);
            field.setId(id);
            mCurrentContainer.addField(field);
            mExtra.addPerfettoPointer(mCurrentContainer, field);
            return mBuilderCache.get(sBuilderSupplier).initInternal(this, field);
        }

@@ -735,6 +738,15 @@ public final class PerfettoTrackEventExtra {
     */
    public void addPerfettoPointer(PerfettoPointer extra) {
        native_add_arg(mPtr, extra.getPtr());
        mPendingPointers.add(extra);
    }

    /**
     * Adds a pointer representing a track event parameter to the {@code container}.
     */
    public void addPerfettoPointer(FieldContainer container, PerfettoPointer extra) {
        container.addField(extra);
        mPendingPointers.add(extra);
    }

    /**
@@ -742,6 +754,7 @@ public final class PerfettoTrackEventExtra {
     */
    public void reset() {
        native_clear_args(mPtr);
        mPendingPointers.clear();
    }

    private CounterInt64 getCounterInt64() {
+143 −23
Original line number Diff line number Diff line
@@ -23,7 +23,8 @@
#include <nativehelper/utils.h>
#include <tracing_sdk.h>

static constexpr ssize_t kMaxStrLen = 4096;
#include <list>

namespace android {
template <typename T>
inline static T* toPointer(jlong ptr) {
@@ -35,24 +36,145 @@ inline static jlong toJLong(T* ptr) {
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(ptr));
}

/**
 * @brief A thread-safe utility class for converting Java UTF-16 strings to ASCII in JNI
 * environment.
 *
 * StringBuffer provides efficient conversion of Java strings to ASCII with optimized memory
 * handling.
 * It uses a two-tiered buffering strategy:
 * 1. A fast path using pre-allocated thread-local buffers for strings up to 128 characters
 * 2. A fallback path using dynamic allocation for longer strings
 *
 * Non-ASCII characters (>255) are replaced with '?' during conversion. The class maintains
 * thread safety through thread-local storage and provides zero-copy string views for optimal
 * performance.
 *
 * Memory Management:
 * - Uses fixed-size thread-local buffers for both UTF-16 and ASCII characters
 * - Overflow strings are stored in a thread-local list to maintain valid string views
 * - Avoids unnecessary allocations in the common case of small strings
 *
 * Usage example:
 * @code
 * JNIEnv* env = ...;
 * jstring java_string = ...;
 * std::string_view ascii = StringBuffer::utf16_to_ascii(env, java_string);
 * // Use the ASCII string...
 * StringBuffer::reset(); // Clean up when done
 * @endcode
 *
 * Thread Safety: All methods are thread-safe due to thread-local storage.
 */
class StringBuffer {
private:
    static constexpr size_t BASE_SIZE = 128;
    // Temporarily stores the UTF-16 characters retrieved from the Java
    // string before they are converted to ASCII.
    static thread_local inline char char_buffer[BASE_SIZE];
    // For fast-path conversions when the resulting ASCII string fits within
    // the pre-allocated space. All ascii strings in a trace event will be stored
    // here until emitted.
    static thread_local inline jchar jchar_buffer[BASE_SIZE];
    // When the fast-path conversion is not possible (because char_buffer
    // doesn't have enough space), the converted ASCII string is stored
    // in this list. We use list here to avoid moving the strings on resize
    // with vector. This way, we can give out string_views from the stored strings.
    // The additional overhead from list node allocations is fine cos we are already
    // in an extremely unlikely path here and there are other bigger problems if here.
    static thread_local inline std::list<std::string> overflow_strings;
    // current offset into the char_buffer.
    static thread_local inline size_t current_offset{0};
    // This allows us avoid touching the overflow_strings directly in the fast path.
    // Touching it causes some thread local init routine to run which shows up in profiles.
    static thread_local inline bool is_overflow_strings_empty = true;

    static void copy_utf16_to_ascii(const jchar* src, size_t len, char* dst, JNIEnv* env,
                                    jstring str) {
        std::transform(src, src + len, dst,
                       [](jchar c) { return (c <= 0xFF) ? static_cast<char>(c) : '?'; });

        if (src != jchar_buffer) {
            // We hit the slow path to populate src, so we have to release.
            env->ReleaseStringCritical(str, src);
        }
    }

public:
    static void reset() {
        if (!is_overflow_strings_empty) {
            overflow_strings.clear();
            is_overflow_strings_empty = true;
        }
        current_offset = 0;
    }

    // Converts a Java string (jstring) to an ASCII string_view. Characters
    // outside the ASCII range (0-255) are replaced with '?'.
    //
    // @param env The JNI environment.
    // @param val The Java string to convert.
    // @return A string_view representing the ASCII version of the string.
    //         Returns an empty string_view if the input is null or empty.
    static std::string_view utf16_to_ascii(JNIEnv* env, jstring val) {
        if (!val) return "";

        const jsize len = env->GetStringLength(val);
        if (len == 0) return "";

        const jchar* temp_buffer;

        // Fast path: Enough space in jchar_buffer
        if (static_cast<size_t>(len) <= BASE_SIZE) {
            env->GetStringRegion(val, 0, len, jchar_buffer);
            temp_buffer = jchar_buffer;
        } else {
            // Slow path: Fallback to asking ART for the string which will likely
            // allocate and return a copy.
            temp_buffer = env->GetStringCritical(val, nullptr);
        }

        const size_t next_offset = current_offset + len + 1;
        // Fast path: Enough space in char_buffer
        if (BASE_SIZE > next_offset) {
            const size_t start_offset = current_offset;

            copy_utf16_to_ascii(temp_buffer, len, char_buffer + current_offset, env, val);
            char_buffer[current_offset + len] = '\0';

            auto res = std::string_view(char_buffer + current_offset, len);
            current_offset = next_offset;
            return res;
        } else {
            // Slow path: Not enough space in char_buffer. Use overflow_strings.
            // This will cause a string alloc but should be very unlikely to hit.
            std::string& str = overflow_strings.emplace_back(len + 1, '\0');

            copy_utf16_to_ascii(temp_buffer, len, str.data(), env, val);
            is_overflow_strings_empty = false;
            return std::string_view(str);
        }
    }
};

static jlong android_os_PerfettoTrackEventExtraArgInt64_init(JNIEnv* env, jclass, jstring name) {
    ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name);
    return toJLong(new tracing_perfetto::DebugArg<int64_t>(name_chars.c_str()));
    return toJLong(new tracing_perfetto::DebugArg<int64_t>(
            StringBuffer::utf16_to_ascii(env, name).data()));
}

static jlong android_os_PerfettoTrackEventExtraArgBool_init(JNIEnv* env, jclass, jstring name) {
    ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name);
    return toJLong(new tracing_perfetto::DebugArg<bool>(name_chars.c_str()));
    return toJLong(
            new tracing_perfetto::DebugArg<bool>(StringBuffer::utf16_to_ascii(env, name).data()));
}

static jlong android_os_PerfettoTrackEventExtraArgDouble_init(JNIEnv* env, jclass, jstring name) {
    ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name);
    return toJLong(new tracing_perfetto::DebugArg<double>(name_chars.c_str()));
    return toJLong(
            new tracing_perfetto::DebugArg<double>(StringBuffer::utf16_to_ascii(env, name).data()));
}

static jlong android_os_PerfettoTrackEventExtraArgString_init(JNIEnv* env, jclass, jstring name) {
    ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name);
    return toJLong(new tracing_perfetto::DebugArg<const char*>(name_chars.c_str()));
    return toJLong(new tracing_perfetto::DebugArg<const char*>(
            StringBuffer::utf16_to_ascii(env, name).data()));
}

static jlong android_os_PerfettoTrackEventExtraArgInt64_delete() {
@@ -109,11 +231,9 @@ static void android_os_PerfettoTrackEventExtraArgDouble_set_value(jlong ptr, jdo

static void android_os_PerfettoTrackEventExtraArgString_set_value(JNIEnv* env, jclass, jlong ptr,
                                                                  jstring val) {
    ScopedUtfChars val_chars = GET_UTF_OR_RETURN_VOID(env, val);

    tracing_perfetto::DebugArg<const char*>* arg =
            toPointer<tracing_perfetto::DebugArg<const char*>>(ptr);
    arg->set_value(strdup(val_chars.c_str()));
    arg->set_value(StringBuffer::utf16_to_ascii(env, val).data());
}

static jlong android_os_PerfettoTrackEventExtraFieldInt64_init() {
@@ -186,11 +306,9 @@ static void android_os_PerfettoTrackEventExtraFieldDouble_set_value(jlong ptr, j

static void android_os_PerfettoTrackEventExtraFieldString_set_value(JNIEnv* env, jclass, jlong ptr,
                                                                    jlong id, jstring val) {
    ScopedUtfChars val_chars = GET_UTF_OR_RETURN_VOID(env, val);

    tracing_perfetto::ProtoField<const char*>* field =
            toPointer<tracing_perfetto::ProtoField<const char*>>(ptr);
    field->set_value(id, strdup(val_chars.c_str()));
    field->set_value(id, StringBuffer::utf16_to_ascii(env, val).data());
}

static void android_os_PerfettoTrackEventExtraFieldNested_add_field(jlong field_ptr,
@@ -231,8 +349,9 @@ static jlong android_os_PerfettoTrackEventExtraFlow_get_extra_ptr(jlong ptr) {

static jlong android_os_PerfettoTrackEventExtraNamedTrack_init(JNIEnv* env, jclass, jlong id,
                                                               jstring name, jlong parent_uuid) {
    ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name);
    return toJLong(new tracing_perfetto::NamedTrack(id, parent_uuid, name_chars.c_str()));
    return toJLong(
            new tracing_perfetto::NamedTrack(id, parent_uuid,
                                             StringBuffer::utf16_to_ascii(env, name).data()));
}

static jlong android_os_PerfettoTrackEventExtraNamedTrack_delete() {
@@ -246,9 +365,10 @@ static jlong android_os_PerfettoTrackEventExtraNamedTrack_get_extra_ptr(jlong pt

static jlong android_os_PerfettoTrackEventExtraCounterTrack_init(JNIEnv* env, jclass, jstring name,
                                                                 jlong parent_uuid) {
    ScopedUtfChars name_chars = GET_UTF_OR_RETURN(env, name);

    return toJLong(new tracing_perfetto::RegisteredTrack(1, parent_uuid, name_chars.c_str(), true));
    return toJLong(
            new tracing_perfetto::RegisteredTrack(1, parent_uuid,
                                                  StringBuffer::utf16_to_ascii(env, name).data(),
                                                  true));
}

static jlong android_os_PerfettoTrackEventExtraCounterTrack_delete() {
@@ -318,11 +438,11 @@ static void android_os_PerfettoTrackEventExtra_clear_args(jlong ptr) {

static void android_os_PerfettoTrackEventExtra_emit(JNIEnv* env, jclass, jint type, jlong cat_ptr,
                                                    jstring name, jlong extra_ptr) {
    ScopedUtfChars name_chars = GET_UTF_OR_RETURN_VOID(env, name);

    tracing_perfetto::Category* category = toPointer<tracing_perfetto::Category>(cat_ptr);
    tracing_perfetto::trace_event(type, category->get(), name_chars.c_str(),
    tracing_perfetto::trace_event(type, category->get(),
                                  StringBuffer::utf16_to_ascii(env, name).data(),
                                  toPointer<tracing_perfetto::Extra>(extra_ptr));
    StringBuffer::reset();
}

static jlong android_os_PerfettoTrackEventExtraProto_init() {
+49 −36
Original line number Diff line number Diff line
@@ -77,6 +77,8 @@ public class PerfettoTraceTest {
    private static final String TAG = "PerfettoTraceTest";
    private static final String FOO = "foo";
    private static final String BAR = "bar";
    private static final String TEXT_ABOVE_4K_SIZE =
            new String(new char[8192]).replace('\0', 'a');

    private static final Category FOO_CATEGORY = new Category(FOO);
    private static final int MESSAGE = 1234567;
@@ -153,41 +155,6 @@ public class PerfettoTraceTest {
        assertThat(mDebugAnnotationNames).contains("string_val");
    }

    @Test
    @RequiresFlagsEnabled(android.os.Flags.FLAG_PERFETTO_SDK_TRACING_V2)
    public void testDebugAnnotationsWithLambda() throws Exception {
        TraceConfig traceConfig = getTraceConfig(FOO);

        PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray());

        PerfettoTrace.instant(FOO_CATEGORY, "event").addArg("long_val", 123L).emit();

        byte[] traceBytes = session.close();

        Trace trace = Trace.parseFrom(traceBytes);

        boolean hasTrackEvent = false;
        boolean hasDebugAnnotations = false;
        for (TracePacket packet: trace.getPacketList()) {
            TrackEvent event;
            if (packet.hasTrackEvent()) {
                hasTrackEvent = true;
                event = packet.getTrackEvent();

                if (TrackEvent.Type.TYPE_INSTANT.equals(event.getType())
                        && event.getDebugAnnotationsCount() == 1) {
                    hasDebugAnnotations = true;

                    List<DebugAnnotation> annotations = event.getDebugAnnotationsList();
                    assertThat(annotations.get(0).getIntValue()).isEqualTo(123L);
                }
            }
        }

        assertThat(hasTrackEvent).isTrue();
        assertThat(hasDebugAnnotations).isTrue();
    }

    @Test
    @RequiresFlagsEnabled(android.os.Flags.FLAG_PERFETTO_SDK_TRACING_V2)
    public void testNamedTrack() throws Exception {
@@ -440,7 +407,6 @@ public class PerfettoTraceTest {

        boolean hasTrackEvent = false;
        boolean hasSourceLocation = false;

        for (TracePacket packet: trace.getPacketList()) {
            TrackEvent event;
            if (packet.hasTrackEvent()) {
@@ -465,6 +431,53 @@ public class PerfettoTraceTest {
        assertThat(mCategoryNames).contains(FOO);
    }

    @Test
    @RequiresFlagsEnabled(android.os.Flags.FLAG_PERFETTO_SDK_TRACING_V2)
    public void testProtoWithSlowPath() throws Exception {
        TraceConfig traceConfig = getTraceConfig(FOO);

        PerfettoTrace.Session session = new PerfettoTrace.Session(true, traceConfig.toByteArray());

        PerfettoTrace.instant(FOO_CATEGORY, "event_proto")
                .beginProto()
                .beginNested(33L)
                .addField(4L, 2L)
                .addField(3, TEXT_ABOVE_4K_SIZE)
                .endNested()
                .addField(2001, "AIDL::IActivityManager")
                .endProto()
                .emit();

        byte[] traceBytes = session.close();

        Trace trace = Trace.parseFrom(traceBytes);

        boolean hasTrackEvent = false;
        boolean hasSourceLocation = false;
        for (TracePacket packet: trace.getPacketList()) {
            TrackEvent event;
            if (packet.hasTrackEvent()) {
                hasTrackEvent = true;
                event = packet.getTrackEvent();

                if (TrackEvent.Type.TYPE_INSTANT.equals(event.getType())
                        && event.hasSourceLocation()) {
                    SourceLocation loc = event.getSourceLocation();
                    if (TEXT_ABOVE_4K_SIZE.equals(loc.getFunctionName())
                            && loc.getLineNumber() == 2) {
                        hasSourceLocation = true;
                    }
                }
            }

            collectInternedData(packet);
        }

        assertThat(hasTrackEvent).isTrue();
        assertThat(hasSourceLocation).isTrue();
        assertThat(mCategoryNames).contains(FOO);
    }

    @Test
    @RequiresFlagsEnabled(android.os.Flags.FLAG_PERFETTO_SDK_TRACING_V2)
    public void testProtoNested() throws Exception {