CharsetUtils alternatives that avoid allocations.

Internally String.getBytes() calls libcore.util.CharsetUtils methods
for a handful of common charsets, but that path requires new memory
allocations for every call.

This change introduces alternative versions of those methods which
attempt to encode data directly into an already-allocated memory
region.  If the destination is to small, callers can detect and pivot
back to calling String.getBytes().

The included benchmarks reveal these raw performance improvements,
in addition to the reduced GC load which is harder to measure:

    timeLocal_LargeBuffer[simple]_mean: 424
    timeLocal_SmallBuffer[simple]_mean: 511
    timeUpstream[simple]_mean: 800

    timeLocal_LargeBuffer[complex]_mean: 977
    timeLocal_SmallBuffer[complex]_mean: 1266
    timeUpstream[complex]_mean: 1468

Bug: 171832118
Test: atest CorePerfTests:android.util.CharsetUtilsPerfTest
Test: atest FrameworksCoreTests:android.util.CharsetUtilsTest
Change-Id: Iac1151e7cb8e88bf82339cada64b0936e1a7578b
diff --git a/apct-tests/perftests/core/src/android/util/CharsetUtilsPerfTest.java b/apct-tests/perftests/core/src/android/util/CharsetUtilsPerfTest.java
new file mode 100644
index 0000000..2a538b2
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/util/CharsetUtilsPerfTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import androidx.test.filters.LargeTest;
+
+import dalvik.system.VMRuntime;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class CharsetUtilsPerfTest {
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Parameterized.Parameter(0)
+    public String mName;
+    @Parameterized.Parameter(1)
+    public String mValue;
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> getParameters() {
+        return Arrays.asList(new Object[][] {
+                { "simple", "com.example.typical_package_name" },
+                { "complex", "從不喜歡孤單一個 - 蘇永康/吳雨霏" },
+        });
+    }
+
+    @Test
+    public void timeUpstream() {
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            mValue.getBytes(StandardCharsets.UTF_8);
+        }
+    }
+
+    /**
+     * Measure performance of writing into a small buffer where bounds checking
+     * requires careful measurement of encoded size.
+     */
+    @Test
+    public void timeLocal_SmallBuffer() {
+        final byte[] dest = (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, 64);
+        final long destPtr = VMRuntime.getRuntime().addressOf(dest);
+
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            CharsetUtils.toUtf8Bytes(mValue, destPtr, 0, dest.length);
+        }
+    }
+
+    /**
+     * Measure performance of writing into a large buffer where bounds checking
+     * only needs a simple worst-case 4-bytes-per-char check.
+     */
+    @Test
+    public void timeLocal_LargeBuffer() {
+        final byte[] dest = (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, 1024);
+        final long destPtr = VMRuntime.getRuntime().addressOf(dest);
+
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            CharsetUtils.toUtf8Bytes(mValue, destPtr, 0, dest.length);
+       }
+    }
+}
diff --git a/core/java/android/util/CharsetUtils.java b/core/java/android/util/CharsetUtils.java
new file mode 100644
index 0000000..80c2055
--- /dev/null
+++ b/core/java/android/util/CharsetUtils.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+
+import dalvik.annotation.optimization.FastNative;
+
+/**
+ * Specializations of {@code libcore.util.CharsetUtils} which enable efficient
+ * in-place encoding without making any new allocations.
+ * <p>
+ * These methods purposefully accept only non-movable byte array addresses to
+ * avoid extra JNI overhead.
+ *
+ * @hide
+ */
+public class CharsetUtils {
+    /**
+     * Attempt to encode the given string as UTF-8 into the destination byte
+     * array without making any new allocations.
+     *
+     * @param src string value to be encoded
+     * @param dest destination byte array to encode into
+     * @param destOff offset into destination where encoding should begin
+     * @param destLen length of destination
+     * @return the number of bytes written to the destination when encoded
+     *         successfully, otherwise {@code -1} if not large enough
+     */
+    public static int toUtf8Bytes(@NonNull String src,
+            long dest, int destOff, int destLen) {
+        return toUtf8Bytes(src, src.length(), dest, destOff, destLen);
+    }
+
+    /**
+     * Attempt to encode the given string as UTF-8 into the destination byte
+     * array without making any new allocations.
+     *
+     * @param src string value to be encoded
+     * @param srcLen exact length of string to be encoded
+     * @param dest destination byte array to encode into
+     * @param destOff offset into destination where encoding should begin
+     * @param destLen length of destination
+     * @return the number of bytes written to the destination when encoded
+     *         successfully, otherwise {@code -1} if not large enough
+     */
+    @FastNative
+    private static native int toUtf8Bytes(@NonNull String src, int srcLen,
+            long dest, int destOff, int destLen);
+}
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index dd1c87b..dc05934 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -134,6 +134,7 @@
                 "android_service_DataLoaderService.cpp",
                 "android_util_AssetManager.cpp",
                 "android_util_Binder.cpp",
+                "android_util_CharsetUtils.cpp",
                 "android_util_MemoryIntArray.cpp",
                 "android_util_Process.cpp",
                 "android_media_AudioDeviceAttributes.cpp",
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 27b23bd..14e74a8 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -105,6 +105,7 @@
  */
 extern int register_android_app_admin_SecurityLog(JNIEnv* env);
 extern int register_android_content_AssetManager(JNIEnv* env);
+extern int register_android_util_CharsetUtils(JNIEnv* env);
 extern int register_android_util_EventLog(JNIEnv* env);
 extern int register_android_util_Log(JNIEnv* env);
 extern int register_android_util_MemoryIntArray(JNIEnv* env);
@@ -1449,6 +1450,7 @@
         REG_JNI(register_com_android_internal_os_RuntimeInit),
         REG_JNI(register_com_android_internal_os_ZygoteInit_nativeZygoteInit),
         REG_JNI(register_android_os_SystemClock),
+        REG_JNI(register_android_util_CharsetUtils),
         REG_JNI(register_android_util_EventLog),
         REG_JNI(register_android_util_Log),
         REG_JNI(register_android_util_MemoryIntArray),
diff --git a/core/jni/android_util_CharsetUtils.cpp b/core/jni/android_util_CharsetUtils.cpp
new file mode 100644
index 0000000..3e1d4a7
--- /dev/null
+++ b/core/jni/android_util_CharsetUtils.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "core_jni_helpers.h"
+#include "nativehelper/scoped_primitive_array.h"
+
+namespace android {
+
+static jint android_util_CharsetUtils_toUtf8Bytes(JNIEnv *env, jobject clazz,
+        jstring src, jint srcLen, jlong dest, jint destOff, jint destLen) {
+    char *destPtr = reinterpret_cast<char*>(dest);
+
+    // Quickly check if destination has plenty of room for worst-case
+    // 4-bytes-per-char encoded size
+    if (destOff >= 0 && destOff + (srcLen * 4) < destLen) {
+        env->GetStringUTFRegion(src, 0, srcLen, destPtr + destOff);
+        return strlen(destPtr + destOff + srcLen) + srcLen;
+    }
+
+    // String still might fit in destination, but we need to measure
+    // its actual encoded size to be sure
+    const size_t encodedLen = env->GetStringUTFLength(src);
+    if (destOff >= 0 && destOff + encodedLen < destLen) {
+        env->GetStringUTFRegion(src, 0, srcLen, destPtr + destOff);
+        return encodedLen;
+    }
+
+    return -1;
+}
+
+static const JNINativeMethod methods[] = {
+    // @FastNative
+    {"toUtf8Bytes",      "(Ljava/lang/String;IJII)I",
+            (void*)android_util_CharsetUtils_toUtf8Bytes},
+};
+
+int register_android_util_CharsetUtils(JNIEnv *env) {
+    return RegisterMethodsOrDie(env, "android/util/CharsetUtils", methods, NELEM(methods));
+}
+
+}
diff --git a/core/tests/coretests/src/android/util/CharsetUtilsTest.java b/core/tests/coretests/src/android/util/CharsetUtilsTest.java
new file mode 100644
index 0000000..04cb3d7
--- /dev/null
+++ b/core/tests/coretests/src/android/util/CharsetUtilsTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.HexDump;
+
+import dalvik.system.VMRuntime;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CharsetUtilsTest {
+    private byte[] dest;
+    private long destPtr;
+
+    @Before
+    public void setUp() {
+        dest = (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, 8);
+        destPtr = VMRuntime.getRuntime().addressOf(dest);
+    }
+
+    @Test
+    public void testUtf8_Empty() {
+        assertEquals(0, CharsetUtils.toUtf8Bytes("", destPtr, 0, dest.length));
+        assertEquals("0000000000000000", HexDump.toHexString(dest));
+    }
+
+    @Test
+    public void testUtf8_Simple() {
+        assertEquals(7, CharsetUtils.toUtf8Bytes("example", destPtr, 0, dest.length));
+        assertEquals("6578616D706C6500", HexDump.toHexString(dest));
+    }
+
+    @Test
+    public void testUtf8_Complex() {
+        assertEquals(3, CharsetUtils.toUtf8Bytes("☃", destPtr, 4, dest.length));
+        assertEquals("00000000E2988300", HexDump.toHexString(dest));
+    }
+
+    @Test
+    public void testUtf8_Bounds() {
+        assertEquals(-1, CharsetUtils.toUtf8Bytes("foo", destPtr, 0, 0));
+        assertEquals(-1, CharsetUtils.toUtf8Bytes("foo", destPtr, 0, 2));
+        assertEquals(-1, CharsetUtils.toUtf8Bytes("foo", destPtr, -2, 8));
+        assertEquals(-1, CharsetUtils.toUtf8Bytes("foo", destPtr, 6, 8));
+        assertEquals(-1, CharsetUtils.toUtf8Bytes("foo", destPtr, 10, 8));
+    }
+
+    @Test
+    public void testUtf8_Overwrite() {
+        assertEquals(5, CharsetUtils.toUtf8Bytes("!!!!!", destPtr, 0, dest.length));
+        assertEquals(3, CharsetUtils.toUtf8Bytes("...", destPtr, 0, dest.length));
+        assertEquals(1, CharsetUtils.toUtf8Bytes("?", destPtr, 0, dest.length));
+        assertEquals("3F002E0021000000", HexDump.toHexString(dest));
+    }
+}