blob: ddbed00c5d267e9b650e64ce57675c222e7b2533 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 <gtest/gtest.h>
#include <iomanip>
#include <iostream>
#include "util/jni-util.h"
namespace doris {
struct JavaMemInfo {
jlong used;
jlong free;
jlong total;
jlong max;
};
class JniUtilTest : public testing::Test {
public:
JniUtilTest() {
_test = false;
// can not run as UT now, because ASAN will report memory leak.
// https://blog.gypsyengineer.com/en/security/running-java-with-addresssanitizer.html
// <<How to use AddressSanitizer with Java>>
// should export ASAN_OPTIONS=detect_leaks=0;
if (!_test) {
return;
}
std::cout << "init = " << init();
}
~JniUtilTest() override = default;
Status init() {
auto st = doris::Jni::Util::Init();
if (!st.ok()) {
exit(1);
}
JNIEnv* env;
// jvm mem
RETURN_IF_ERROR(Jni::Env::Get(&env));
RETURN_IF_ERROR(Jni::Util::find_class(env, "java/lang/Runtime", &runtime_cls));
RETURN_IF_ERROR(runtime_cls.get_static_method(env, "getRuntime", "()Ljava/lang/Runtime;",
&get_runtime));
RETURN_IF_ERROR(runtime_cls.get_method(env, "totalMemory", "()J", &total_mem));
RETURN_IF_ERROR(runtime_cls.get_method(env, "freeMemory", "()J", &free_mem));
RETURN_IF_ERROR(runtime_cls.get_method(env, "maxMemory", "()J", &max_mem));
// gc
RETURN_IF_ERROR(Jni::Util::find_class(env, "java/lang/System", &system_cls));
RETURN_IF_ERROR(system_cls.get_static_method(env, "gc", "()V", &gc_method));
RETURN_IF_ERROR(trigger_gc(env));
return Status::OK();
}
Status get_mem_info(JNIEnv* env, JavaMemInfo* info) {
Jni::LocalObject runtime_obj;
RETURN_IF_ERROR(runtime_cls.call_static_object_method(env, get_runtime).call(&runtime_obj));
jlong total, free, max, used;
RETURN_IF_ERROR(runtime_obj.call_long_method(env, total_mem).call(&total));
RETURN_IF_ERROR(runtime_obj.call_long_method(env, free_mem).call(&free));
RETURN_IF_ERROR(runtime_obj.call_long_method(env, max_mem).call(&max));
used = total - free;
info->used = used;
info->free = free;
info->total = total;
info->max = max;
return Status::OK();
}
void print_mem_info(const std::string& title, const JavaMemInfo& info) {
std::cout << std::fixed << std::setprecision(2);
std::cout << "==== " << title << " ====\n";
std::cout << "Used: " << (info.used / 1024.0 / 1024.0) << " MB\n";
std::cout << "Free: " << (info.free / 1024.0 / 1024.0) << " MB\n";
std::cout << "Total: " << (info.total / 1024.0 / 1024.0) << " MB\n";
std::cout << "Max: " << (info.max / 1024.0 / 1024.0) << " MB\n";
}
Status trigger_gc(JNIEnv* env) {
RETURN_IF_ERROR(system_cls.call_static_void_method(env, gc_method).call());
return Status::OK();
}
bool check_mem_diff(const JavaMemInfo& before, const JavaMemInfo& after,
double allowed_diff_mb) {
auto to_mb = [](jlong bytes) { return bytes / 1024.0 / 1024.0; };
double used_before = to_mb(before.used);
double used_after = to_mb(after.used);
double diff = used_after - used_before;
std::cout << std::fixed << std::setprecision(2);
std::cout << "==== Memory Diff Check ====\n";
std::cout << "Used: " << used_before << " -> " << used_after << " (Δ=" << diff << " MB)\n";
if (diff > allowed_diff_mb) {
std::cout << "Memory change " << diff << " MB exceeds allowed " << allowed_diff_mb
<< " MB\n";
return false;
}
std::cout << "Memory change within limit (" << diff << " MB ≤ " << allowed_diff_mb
<< " MB)\n";
return true;
}
private:
Jni::GlobalClass runtime_cls;
Jni::GlobalClass system_cls;
Jni::MethodId get_runtime;
Jni::MethodId total_mem;
Jni::MethodId free_mem;
Jni::MethodId max_mem;
Jni::MethodId gc_method;
bool _test;
};
TEST_F(JniUtilTest, MemoryStableAfterStringAllocations) {
if (!_test) {
std::cout << "Skip MemoryStableAfterStringAllocations test\n";
return;
}
JNIEnv* env = nullptr;
auto st = Jni::Env::Get(&env);
EXPECT_TRUE(st.ok()) << st;
JavaMemInfo info_before;
st = get_mem_info(env, &info_before);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("MemoryStableAfterStringAllocations_before", info_before);
for (int i = 0; i < 10000; i++) {
Jni::LocalClass string_cls;
st = Jni::Util::find_class(env, "java/lang/String", &string_cls);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId length_method;
st = string_cls.get_method(env, "length", "()I", &length_method);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId empty_method;
st = string_cls.get_method(env, "isEmpty", "()Z", &empty_method);
ASSERT_TRUE(st.ok()) << st;
Jni::LocalString jstr_obj;
st = Jni::LocalString::new_string(env, "hello", &jstr_obj);
ASSERT_TRUE(st.ok()) << st;
Jni::LocalStringBufferGuard str_buf;
st = jstr_obj.get_string_chars(env, &str_buf);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(strlen(str_buf.get()), 5);
ASSERT_EQ(strcmp(str_buf.get(), "hello"), 0);
jint len;
st = jstr_obj.call_int_method(env, length_method).call(&len);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(len, 5);
st = jstr_obj.call_int_method(env, length_method).call();
ASSERT_TRUE(st.ok()) << st;
jboolean empty;
st = jstr_obj.call_boolean_method(env, empty_method).call(&empty);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(empty, false);
}
JavaMemInfo info_after;
st = get_mem_info(env, &info_after);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("MemoryStableAfterStringAllocations_after", info_after);
st = trigger_gc(env);
ASSERT_TRUE(st.ok()) << st;
JavaMemInfo info_after_gc;
st = get_mem_info(env, &info_after_gc);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("MemoryStableAfterStringAllocations_after_gc", info_after_gc);
ASSERT_TRUE(check_mem_diff(info_before, info_after_gc, 5.0));
}
TEST_F(JniUtilTest, ByteBuffer) {
if (!_test) {
std::cout << "Skip ByteBuffer test\n";
return;
}
JNIEnv* env = nullptr;
auto st = Jni::Env::Get(&env);
EXPECT_TRUE(st.ok()) << st;
JavaMemInfo info_before;
st = get_mem_info(env, &info_before);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("ByteBuffer_before", info_before);
for (int i = 0; i < 300; i++) { // allocate 300M
Jni::LocalClass local_class;
st = Jni::Util::find_class(env, "java/nio/ByteBuffer", &local_class);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId allocate_method;
st = local_class.get_static_method(env, "allocate", "(I)Ljava/nio/ByteBuffer;",
&allocate_method);
ASSERT_TRUE(st.ok()) << st;
Jni::LocalObject bytebuffer_obj;
st = local_class.call_static_object_method(env, allocate_method)
.with_arg(1024 * 1024)
.call(&bytebuffer_obj); // 1M
ASSERT_TRUE(st.ok()) << st;
}
JavaMemInfo info_after;
st = get_mem_info(env, &info_after);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("ByteBuffer_after", info_after);
st = trigger_gc(env);
ASSERT_TRUE(st.ok()) << st;
JavaMemInfo info_after_gc;
st = get_mem_info(env, &info_after_gc);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("ByteBuffer_after_gc", info_after_gc);
ASSERT_TRUE(check_mem_diff(info_before, info_after_gc, 5.0));
}
TEST_F(JniUtilTest, StringMethodTest) {
if (!_test) {
std::cout << "Skip StringMethodTest test\n";
return;
}
JNIEnv* env;
Status st = Jni::Env::Get(&env);
ASSERT_TRUE(st.ok()) << st;
JavaMemInfo info_before;
st = get_mem_info(env, &info_before);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("String_before", info_before);
Jni::LocalClass string_cls;
st = Jni::Util::find_class(env, "java/lang/String", &string_cls);
ASSERT_TRUE(st.ok()) << st;
// get methodId
Jni::MethodId equals_method;
st = string_cls.get_method(env, "equals", "(Ljava/lang/Object;)Z", &equals_method);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId equals_ignore_case_method;
st = string_cls.get_method(env, "equalsIgnoreCase", "(Ljava/lang/String;)Z",
&equals_ignore_case_method);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId to_upper_method;
st = string_cls.get_method(env, "toUpperCase", "()Ljava/lang/String;", &to_upper_method);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId to_lower_method;
st = string_cls.get_method(env, "toLowerCase", "()Ljava/lang/String;", &to_lower_method);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId substring_method;
st = string_cls.get_method(env, "substring", "(II)Ljava/lang/String;", &substring_method);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId contains_method;
st = string_cls.get_method(env, "contains", "(Ljava/lang/CharSequence;)Z", &contains_method);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId starts_with_method;
st = string_cls.get_method(env, "startsWith", "(Ljava/lang/String;)Z", &starts_with_method);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId ends_with_method;
st = string_cls.get_method(env, "endsWith", "(Ljava/lang/String;)Z", &ends_with_method);
ASSERT_TRUE(st.ok()) << st;
// 3. create some local string object.
Jni::LocalString str_hello, str_hello2, str_hello_upper, str_world;
st = Jni::LocalString::new_string(env, "hello", &str_hello);
ASSERT_TRUE(st.ok()) << st;
st = Jni::LocalString::new_string(env, "HELLO", &str_hello_upper);
ASSERT_TRUE(st.ok()) << st;
st = Jni::LocalString::new_string(env, "hello", &str_hello2);
ASSERT_TRUE(st.ok()) << st;
st = Jni::LocalString::new_string(env, "world", &str_world);
ASSERT_TRUE(st.ok()) << st;
for (int i = 0; i < 10000; i++) {
// 4. equals() and equalsIgnoreCase()
{
jboolean eq;
st = str_hello.call_boolean_method(env, equals_method).with_arg(str_hello2).call(&eq);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(eq, JNI_TRUE); // "hello".equals("hello")
jboolean eq_ic;
st = str_hello.call_boolean_method(env, equals_ignore_case_method)
.with_arg(str_hello_upper)
.call(&eq_ic);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(eq_ic, JNI_TRUE); // "hello".equalsIgnoreCase("HELLO")
jboolean neq;
st = str_hello.call_boolean_method(env, equals_method).with_arg(str_world).call(&neq);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(neq, JNI_FALSE);
}
// 5. toUpperCase() / toLowerCase()
{
Jni::LocalString upper_obj;
st = str_hello.call_object_method(env, to_upper_method).call(&upper_obj);
ASSERT_TRUE(st.ok()) << st;
Jni::LocalStringBufferGuard buf;
st = upper_obj.get_string_chars(env, &buf);
ASSERT_TRUE(st.ok()) << st;
ASSERT_STREQ(buf.get(), "HELLO");
Jni::LocalString lower_obj;
st = str_hello_upper.call_object_method(env, to_lower_method).call(&lower_obj);
ASSERT_TRUE(st.ok()) << st;
Jni::LocalStringBufferGuard buf2;
st = lower_obj.get_string_chars(env, &buf2);
ASSERT_TRUE(st.ok()) << st;
ASSERT_STREQ(buf2.get(), "hello");
}
// 6. substring() test
{
Jni::LocalString sub_obj;
st = str_hello.call_object_method(env, substring_method)
.with_arg(1)
.with_arg(4)
.call(&sub_obj); // "ell"
ASSERT_TRUE(st.ok()) << st;
Jni::LocalStringBufferGuard buf;
st = sub_obj.get_string_chars(env, &buf);
ASSERT_TRUE(st.ok()) << st;
ASSERT_STREQ(buf.get(), "ell");
}
// 7. contains() / startsWith() / endsWith() hello
{
Jni::LocalString str_el;
st = Jni::LocalString::new_string(env, "el", &str_el);
ASSERT_TRUE(st.ok()) << st;
jboolean contains;
st = str_hello.call_boolean_method(env, contains_method)
.with_arg(str_el)
.call(&contains);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(contains, true);
Jni::LocalString str_he;
st = Jni::LocalString::new_string(env, "he", &str_he);
ASSERT_TRUE(st.ok()) << st;
jboolean starts;
st = str_hello.call_boolean_method(env, starts_with_method)
.with_arg(str_he)
.call(&starts);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(starts, JNI_TRUE);
Jni::LocalString str_ll;
st = Jni::LocalString::new_string(env, "ll", &str_ll);
ASSERT_TRUE(st.ok()) << st;
jboolean ends;
st = str_hello.call_boolean_method(env, ends_with_method).with_arg(str_ll).call(&ends);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(ends, false);
}
}
JavaMemInfo info_after;
st = get_mem_info(env, &info_after);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("String_after", info_after);
st = trigger_gc(env);
ASSERT_TRUE(st.ok()) << st;
JavaMemInfo info_after_gc;
st = get_mem_info(env, &info_after_gc);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("String_after_gc", info_after_gc);
ASSERT_TRUE(check_mem_diff(info_before, info_after_gc, 5.0));
}
TEST_F(JniUtilTest, TestJavaDateArray) {
if (!_test) {
std::cout << "Skip TestJavaDateArray test\n";
return;
}
JNIEnv* env;
Status st = Jni::Env::Get(&env);
ASSERT_TRUE(st.ok()) << st;
JavaMemInfo info_before;
st = get_mem_info(env, &info_before);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("DateArray_before", info_before);
{
size_t test_size = 3;
// Load java.util.Date class
Jni::LocalClass date_cls;
st = Jni::Util::find_class(env, "java/util/Date", &date_cls);
ASSERT_TRUE(st.ok()) << st;
// Load java.lang.reflect.Array class
Jni::LocalClass reflect_array_cls;
st = Jni::Util::find_class(env, "java/lang/reflect/Array", &reflect_array_cls);
ASSERT_TRUE(st.ok()) << st;
// Get static method Array.newInstance(Class, int)
Jni::MethodId new_instance_method;
st = reflect_array_cls.get_static_method(
env, "newInstance", "(Ljava/lang/Class;I)Ljava/lang/Object;", &new_instance_method);
ASSERT_TRUE(st.ok()) << st;
Jni::MethodId set_method;
st = reflect_array_cls.get_static_method(
env, "set", "(Ljava/lang/Object;ILjava/lang/Object;)V", &set_method);
ASSERT_TRUE(st.ok()) << st;
// Create a Date array: new Date[3]
Jni::LocalArray date_array;
st = reflect_array_cls.call_static_object_method(env, new_instance_method)
.with_arg(date_cls)
.with_arg((jint)test_size)
.call(&date_array);
ASSERT_TRUE(st.ok()) << st;
// Get Date.<init>() constructor
Jni::MethodId date_ctor;
st = date_cls.get_method(env, "<init>", "()V", &date_ctor);
ASSERT_TRUE(st.ok()) << st;
// Fill each element with new Date()
for (int i = 0; i < test_size; i++) {
Jni::LocalObject date_obj;
st = date_cls.new_object(env, date_ctor)
.with_arg((jlong)1700000000000LL)
.call(&date_obj);
ASSERT_TRUE(st.ok()) << st;
st = reflect_array_cls.call_static_void_method(env, set_method)
.with_arg(date_array)
.with_arg(i)
.with_arg(date_obj)
.call();
ASSERT_TRUE(st.ok()) << st;
}
// Verify array length
jsize len;
st = date_array.get_length(env, &len);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(len, test_size);
// Get elements and print their toString()
Jni::MethodId toString_method;
st = date_cls.get_method(env, "toString", "()Ljava/lang/String;", &toString_method);
ASSERT_TRUE(st.ok()) << st;
for (int i = 0; i < test_size; i++) {
Jni::LocalObject date_obj;
st = date_array.get_object_array_element(env, i, &date_obj);
ASSERT_TRUE(st.ok()) << st;
Jni::LocalString date_str;
st = date_obj.call_object_method(env, toString_method).call(&date_str);
ASSERT_TRUE(st.ok()) << st;
Jni::LocalStringBufferGuard str_buf;
st = date_str.get_string_chars(env, &str_buf);
ASSERT_TRUE(st.ok()) << st;
std::cout << "Date[" << i << "] = " << str_buf.get() << std::endl;
}
}
JavaMemInfo info_after;
st = get_mem_info(env, &info_after);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("DateArray_after", info_after);
st = trigger_gc(env);
ASSERT_TRUE(st.ok()) << st;
JavaMemInfo info_after_gc;
st = get_mem_info(env, &info_after_gc);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("DateArray_gc", info_after_gc);
ASSERT_TRUE(check_mem_diff(info_before, info_after_gc, 5.0));
}
TEST_F(JniUtilTest, TestConvertMap) {
if (!_test) {
std::cout << "Skip TestConvertMap test\n";
return;
}
JNIEnv* env;
Status st = Jni::Env::Get(&env);
ASSERT_TRUE(st.ok()) << st;
JavaMemInfo info_before;
st = get_mem_info(env, &info_before);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("ConvertMap_before", info_before);
{
std::map<std::string, std::string> cpp_map;
for (int i = 0; i < 10000; i++) {
cpp_map["key_" + std::to_string(i)] = "value_" + std::to_string(i);
}
// Step 2: Convert to Java HashMap
Jni::LocalObject java_map;
st = Jni::Util::convert_to_java_map(env, cpp_map, &java_map);
ASSERT_TRUE(st.ok()) << st;
// Step 3: Get java/util/HashMap class
Jni::LocalClass map_class;
st = Jni::Util::find_class(env, "java/util/HashMap", &map_class);
ASSERT_TRUE(st.ok()) << st;
// Step 4: Prepare commonly used methods
Jni::MethodId size_method, get_method, contains_key_method, contains_value_method,
is_empty_method;
st = map_class.get_method(env, "size", "()I", &size_method);
ASSERT_TRUE(st.ok()) << st;
st = map_class.get_method(env, "get", "(Ljava/lang/Object;)Ljava/lang/Object;",
&get_method);
ASSERT_TRUE(st.ok()) << st;
st = map_class.get_method(env, "containsKey", "(Ljava/lang/Object;)Z",
&contains_key_method);
ASSERT_TRUE(st.ok()) << st;
st = map_class.get_method(env, "containsValue", "(Ljava/lang/Object;)Z",
&contains_value_method);
ASSERT_TRUE(st.ok()) << st;
st = map_class.get_method(env, "isEmpty", "()Z", &is_empty_method);
ASSERT_TRUE(st.ok()) << st;
jint size;
st = java_map.call_int_method(env, size_method).call(&size);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(size, (jint)cpp_map.size());
jboolean empty;
st = java_map.call_boolean_method(env, is_empty_method).call(&empty);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(empty, JNI_FALSE);
// Step 6: Create test key and check containsKey / containsValue
Jni::LocalString test_key;
Jni::LocalString test_value;
st = Jni::LocalString::new_string(env, "key_1234", &test_key);
ASSERT_TRUE(st.ok()) << st;
st = Jni::LocalString::new_string(env, "value_1234", &test_value);
ASSERT_TRUE(st.ok()) << st;
jboolean has_key;
st = java_map.call_boolean_method(env, contains_key_method)
.with_arg(test_key)
.call(&has_key);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(has_key, JNI_TRUE);
jboolean has_value;
st = java_map.call_boolean_method(env, contains_value_method)
.with_arg(test_value)
.call(&has_value);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(has_value, JNI_TRUE);
Jni::LocalString result_obj;
st = java_map.call_object_method(env, get_method).with_arg(test_key).call(&result_obj);
ASSERT_TRUE(st.ok()) << st;
Jni::LocalStringBufferGuard buf;
st = result_obj.get_string_chars(env, &buf);
ASSERT_TRUE(st.ok()) << st;
ASSERT_STREQ(buf.get(), "value_1234");
Jni::LocalString fake_key;
st = Jni::String<Jni::Local>::new_string(env, "key_not_exist", &fake_key);
ASSERT_TRUE(st.ok()) << st;
jboolean has_fake;
st = java_map.call_boolean_method(env, contains_key_method)
.with_arg(fake_key)
.call(&has_fake);
ASSERT_TRUE(st.ok()) << st;
ASSERT_EQ(has_fake, JNI_FALSE);
std::map<std::string, std::string> result_map;
st = Jni::Util::convert_to_cpp_map(env, java_map, &result_map);
for (auto const& [a, b] : cpp_map) {
ASSERT_TRUE(result_map.contains(a));
ASSERT_EQ(result_map[a], b);
}
}
JavaMemInfo info_after;
st = get_mem_info(env, &info_after);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("ConvertMap_after", info_after);
st = trigger_gc(env);
ASSERT_TRUE(st.ok()) << st;
JavaMemInfo info_after_gc;
st = get_mem_info(env, &info_after_gc);
ASSERT_TRUE(st.ok()) << st;
print_mem_info("ConvertMap_after_gc", info_after_gc);
ASSERT_TRUE(check_mem_diff(info_before, info_after_gc, 5.0));
ASSERT_TRUE(Jni::Env::Get(&env).ok());
}
} // namespace doris