From e5cc2412760a36698e386677d163e1e2c66d1f72 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 9 Nov 2026 11:21:56 +0500 Subject: [PATCH] perf(android): read numeric array arguments in bulk --- .../ffi/jni/jsi/conversion/JsArgConverter.cpp | 15 ++++++-- .../jni/napi/conversion/JsArgConverter.cpp | 16 +++++++-- NativeScript/jsi/hermes/HermesRuntime.h | 14 ++++++++ NativeScript/jsi/jsc/JSCRuntime.h | 14 ++++++++ NativeScript/jsi/quickjs/QuickJSRuntime.h | 14 ++++++++ NativeScript/jsi/v8/V8Runtime.h | 32 +++++++++++++++++ NativeScript/napi/common/jsr_common.h | 5 +++ NativeScript/napi/hermes/jsr.cpp | 16 +++++++++ NativeScript/napi/jsc/jsr.cpp | 16 +++++++++ NativeScript/napi/primjs/jsr.cpp | 16 +++++++++ NativeScript/napi/quickjs/jsr.cpp | 16 +++++++++ NativeScript/napi/v8/jsr.cpp | 35 +++++++++++++++++++ 12 files changed, 205 insertions(+), 4 deletions(-) diff --git a/NativeScript/ffi/jni/jsi/conversion/JsArgConverter.cpp b/NativeScript/ffi/jni/jsi/conversion/JsArgConverter.cpp index be1cacb50..2431d5726 100644 --- a/NativeScript/ffi/jni/jsi/conversion/JsArgConverter.cpp +++ b/NativeScript/ffi/jni/jsi/conversion/JsArgConverter.cpp @@ -453,7 +453,15 @@ bool JsArgConverter::ConvertJavaScriptArray(JsRuntime &rt, const JsValue &jsArr, case 'I': { arr = jenv.NewIntArray(arrLength); std::vector ints(arrLength); - for (jsize i = 0; i < arrLength; i++) { + std::vector numbers(arrLength); + size_t copied = 0; + bool bulk = jsArray.copyNumbers(rt, numbers.data(), (size_t) arrLength, &copied) && + copied == (size_t) arrLength; + for (jsize i = 0; bulk && i < arrLength; i++) { + bulk = numbers[i] >= INT32_MIN && numbers[i] <= INT32_MAX; + ints[i] = (jint) numbers[i]; + } + for (jsize i = 0; !bulk && i < arrLength; i++) { ints[i] = (jint) js_util::get_int32(jsArray.getValueAtIndexBorrowed(rt, i)); } jenv.SetIntArrayRegion((jintArray) arr, 0, arrLength, ints.data()); @@ -480,7 +488,10 @@ bool JsArgConverter::ConvertJavaScriptArray(JsRuntime &rt, const JsValue &jsArr, case 'D': { arr = jenv.NewDoubleArray(arrLength); std::vector doubles(arrLength); - for (jsize i = 0; i < arrLength; i++) { + size_t copied = 0; + bool bulk = jsArray.copyNumbers(rt, doubles.data(), (size_t) arrLength, &copied) && + copied == (size_t) arrLength; + for (jsize i = 0; !bulk && i < arrLength; i++) { doubles[i] = (jdouble) js_util::get_number(jsArray.getValueAtIndexBorrowed(rt, i)); } jenv.SetDoubleArrayRegion((jdoubleArray) arr, 0, arrLength, doubles.data()); diff --git a/NativeScript/ffi/jni/napi/conversion/JsArgConverter.cpp b/NativeScript/ffi/jni/napi/conversion/JsArgConverter.cpp index c12d86b6b..094b9dd5d 100644 --- a/NativeScript/ffi/jni/napi/conversion/JsArgConverter.cpp +++ b/NativeScript/ffi/jni/napi/conversion/JsArgConverter.cpp @@ -1,4 +1,5 @@ #include "JsArgConverter.h" +#include "jsr_common.h" #include "ObjectManager.h" #include "JniSignatureParser.h" #include "JsArgToArrayConverter.h" @@ -616,7 +617,15 @@ bool JsArgConverter::ConvertJavaScriptArray(napi_env env, napi_value jsArr, int case 'I': { arr = jenv.NewIntArray(arrLength); std::vector ints(arrLength); - for (uint32_t i = 0; i < arrLength; i++) { + std::vector numbers(arrLength); + uint32_t copied = 0; + bool bulk = js_get_array_doubles(env, jsArr, numbers.data(), arrLength, &copied) == napi_ok && + copied == arrLength; + for (uint32_t i = 0; bulk && i < arrLength; i++) { + bulk = numbers[i] >= INT32_MIN && numbers[i] <= INT32_MAX; + ints[i] = (jint) numbers[i]; + } + for (uint32_t i = 0; !bulk && i < arrLength; i++) { napi_value element; NAPI_GUARD(napi_get_element(env, jsArr, i, &element)) {} int32_t intValue; @@ -655,7 +664,10 @@ bool JsArgConverter::ConvertJavaScriptArray(napi_env env, napi_value jsArr, int case 'D': { arr = jenv.NewDoubleArray(arrLength); std::vector doubles(arrLength); - for (uint32_t i = 0; i < arrLength; i++) { + uint32_t copied = 0; + bool bulk = js_get_array_doubles(env, jsArr, doubles.data(), arrLength, &copied) == napi_ok && + copied == arrLength; + for (uint32_t i = 0; !bulk && i < arrLength; i++) { napi_value element; NAPI_GUARD(napi_get_element(env, jsArr, i, &element)) {} double doubleValue; diff --git a/NativeScript/jsi/hermes/HermesRuntime.h b/NativeScript/jsi/hermes/HermesRuntime.h index 71b3f5668..5f965d7d6 100644 --- a/NativeScript/jsi/hermes/HermesRuntime.h +++ b/NativeScript/jsi/hermes/HermesRuntime.h @@ -689,6 +689,20 @@ class Array : public Object { Value getValueAtIndex(Runtime& runtime, size_t index) const; // See Object::getPropertyBorrowed. + // Copies the leading numeric elements into `out`. Returns false when an element is not a + // number, and the caller falls back to per-element reads. + bool copyNumbers(Runtime& runtime, double* out, size_t capacity, size_t* length) const { + size_t count = size(runtime); + if (count > capacity) count = capacity; + for (size_t i = 0; i < count; i++) { + Value element = getValueAtIndexBorrowed(runtime, i); + if (!element.isNumber()) return false; + out[i] = element.getNumber(); + } + *length = count; + return true; + } + Value getValueAtIndexBorrowed(Runtime& runtime, size_t index) const { return getValueAtIndex(runtime, index); } diff --git a/NativeScript/jsi/jsc/JSCRuntime.h b/NativeScript/jsi/jsc/JSCRuntime.h index 5ab756a30..4e8013dad 100644 --- a/NativeScript/jsi/jsc/JSCRuntime.h +++ b/NativeScript/jsi/jsc/JSCRuntime.h @@ -1142,6 +1142,20 @@ class Array : public Object { } // See Object::getPropertyBorrowed. + // Copies the leading numeric elements into `out`. Returns false when an element is not a + // number, and the caller falls back to per-element reads. + bool copyNumbers(Runtime& runtime, double* out, size_t capacity, size_t* length) const { + size_t count = size(runtime); + if (count > capacity) count = capacity; + for (size_t i = 0; i < count; i++) { + Value element = getValueAtIndexBorrowed(runtime, i); + if (!element.isNumber()) return false; + out[i] = element.getNumber(); + } + *length = count; + return true; + } + Value getValueAtIndexBorrowed(Runtime& runtime, size_t index) const { return getValueAtIndex(runtime, index); } diff --git a/NativeScript/jsi/quickjs/QuickJSRuntime.h b/NativeScript/jsi/quickjs/QuickJSRuntime.h index 7c2338d6b..8a886d7b0 100644 --- a/NativeScript/jsi/quickjs/QuickJSRuntime.h +++ b/NativeScript/jsi/quickjs/QuickJSRuntime.h @@ -1138,6 +1138,20 @@ class Array : public Object { return length.isNumber() ? static_cast(std::max(0, length.getNumber())) : 0; } // See Object::getPropertyBorrowed. + // Copies the leading numeric elements into `out`. Returns false when an element is not a + // number, and the caller falls back to per-element reads. + bool copyNumbers(Runtime& runtime, double* out, size_t capacity, size_t* length) const { + size_t count = size(runtime); + if (count > capacity) count = capacity; + for (size_t i = 0; i < count; i++) { + Value element = getValueAtIndexBorrowed(runtime, i); + if (!element.isNumber()) return false; + out[i] = element.getNumber(); + } + *length = count; + return true; + } + Value getValueAtIndexBorrowed(Runtime& runtime, size_t index) const { return getValueAtIndex(runtime, index); } diff --git a/NativeScript/jsi/v8/V8Runtime.h b/NativeScript/jsi/v8/V8Runtime.h index dacb73ad4..b08b74c52 100644 --- a/NativeScript/jsi/v8/V8Runtime.h +++ b/NativeScript/jsi/v8/V8Runtime.h @@ -1281,6 +1281,38 @@ class Array : public Object { } // See Object::getPropertyBorrowed for the lifetime contract. + // Copies the leading numeric elements into `out`. Returns false when an element is not a + // number, and the caller falls back to per-element reads. Iterate walks a packed backing + // store without a handle or exception check per element. + bool copyNumbers(Runtime& runtime, double* out, size_t capacity, size_t* length) const { + v8::Local array = local(runtime).As(); + size_t count = static_cast(array->Length()); + if (count > capacity) count = capacity; + struct State { + double* out; + size_t count; + size_t written; + bool numbersOnly; + } state{out, count, 0, true}; + v8::Maybe result = array->Iterate( + runtime.context(), + [](uint32_t index, v8::Local element, void* data) { + auto* s = static_cast(data); + if (index >= s->count) return v8::Array::CallbackResult::kBreak; + if (!element->IsNumber()) { + s->numbersOnly = false; + return v8::Array::CallbackResult::kBreak; + } + s->out[index] = element.As()->Value(); + s->written++; + return v8::Array::CallbackResult::kContinue; + }, + &state); + if (result.IsNothing() || !state.numbersOnly || state.written != count) return false; + *length = count; + return true; + } + Value getValueAtIndexBorrowed(Runtime& runtime, size_t index) const { v8::TryCatch tryCatch(runtime.isolate()); v8::Local result; diff --git a/NativeScript/napi/common/jsr_common.h b/NativeScript/napi/common/jsr_common.h index 1c3516d4e..c14b466f4 100644 --- a/NativeScript/napi/common/jsr_common.h +++ b/NativeScript/napi/common/jsr_common.h @@ -25,6 +25,11 @@ napi_status js_execute_pending_jobs(napi_env env); napi_status js_get_engine_ptr(napi_env env, int64_t *engine_ptr); napi_status js_adjust_external_memory(napi_env env, int64_t changeInBytes, int64_t* externalMemory); + +// Copies the leading elements of a packed numeric JS array into `out`. Returns a failure +// status when an element is not a number, and the caller falls back to per-element reads. +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length); napi_status js_cache_script(napi_env env, const char *source, const char *file); napi_status js_run_cached_script(napi_env env, const char * file, napi_value script, void* cache, napi_value *result); diff --git a/NativeScript/napi/hermes/jsr.cpp b/NativeScript/napi/hermes/jsr.cpp index 47eb9e7b0..87a0ea7c3 100644 --- a/NativeScript/napi/hermes/jsr.cpp +++ b/NativeScript/napi/hermes/jsr.cpp @@ -450,3 +450,19 @@ extern "C" napi_status jsr_drain_microtasks(napi_env env, return napi_ok; } + +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length) { + bool isArray = false; + if (napi_is_array(env, array, &isArray) != napi_ok || !isArray) return napi_array_expected; + uint32_t count = 0; + if (napi_get_array_length(env, array, &count) != napi_ok) return napi_array_expected; + if (count > capacity) count = capacity; + for (uint32_t i = 0; i < count; i++) { + napi_value element; + if (napi_get_element(env, array, i, &element) != napi_ok) return napi_generic_failure; + if (napi_get_value_double(env, element, &out[i]) != napi_ok) return napi_number_expected; + } + *length = count; + return napi_ok; +} diff --git a/NativeScript/napi/jsc/jsr.cpp b/NativeScript/napi/jsc/jsr.cpp index 93b224300..922c59541 100644 --- a/NativeScript/napi/jsc/jsr.cpp +++ b/NativeScript/napi/jsc/jsr.cpp @@ -370,3 +370,19 @@ napi_status js_get_runtime_version(napi_env env, napi_value* version) { return napi_ok; } + +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length) { + bool isArray = false; + if (napi_is_array(env, array, &isArray) != napi_ok || !isArray) return napi_array_expected; + uint32_t count = 0; + if (napi_get_array_length(env, array, &count) != napi_ok) return napi_array_expected; + if (count > capacity) count = capacity; + for (uint32_t i = 0; i < count; i++) { + napi_value element; + if (napi_get_element(env, array, i, &element) != napi_ok) return napi_generic_failure; + if (napi_get_value_double(env, element, &out[i]) != napi_ok) return napi_number_expected; + } + *length = count; + return napi_ok; +} diff --git a/NativeScript/napi/primjs/jsr.cpp b/NativeScript/napi/primjs/jsr.cpp index c114462e3..165a66ca7 100644 --- a/NativeScript/napi/primjs/jsr.cpp +++ b/NativeScript/napi/primjs/jsr.cpp @@ -177,3 +177,19 @@ napi_status js_get_runtime_version(napi_env env, napi_value *version) { napi_create_string_utf8(env, "PrimJS", NAPI_AUTO_LENGTH, version); return napi_ok; } + +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length) { + bool isArray = false; + if (napi_is_array(env, array, &isArray) != napi_ok || !isArray) return napi_array_expected; + uint32_t count = 0; + if (napi_get_array_length(env, array, &count) != napi_ok) return napi_array_expected; + if (count > capacity) count = capacity; + for (uint32_t i = 0; i < count; i++) { + napi_value element; + if (napi_get_element(env, array, i, &element) != napi_ok) return napi_generic_failure; + if (napi_get_value_double(env, element, &out[i]) != napi_ok) return napi_number_expected; + } + *length = count; + return napi_ok; +} diff --git a/NativeScript/napi/quickjs/jsr.cpp b/NativeScript/napi/quickjs/jsr.cpp index d65b4e6d2..67f40584a 100644 --- a/NativeScript/napi/quickjs/jsr.cpp +++ b/NativeScript/napi/quickjs/jsr.cpp @@ -148,3 +148,19 @@ napi_status js_get_runtime_version(napi_env env, napi_value *version) { return napi_ok; } + +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length) { + bool isArray = false; + if (napi_is_array(env, array, &isArray) != napi_ok || !isArray) return napi_array_expected; + uint32_t count = 0; + if (napi_get_array_length(env, array, &count) != napi_ok) return napi_array_expected; + if (count > capacity) count = capacity; + for (uint32_t i = 0; i < count; i++) { + napi_value element; + if (napi_get_element(env, array, i, &element) != napi_ok) return napi_generic_failure; + if (napi_get_value_double(env, element, &out[i]) != napi_ok) return napi_number_expected; + } + *length = count; + return napi_ok; +} diff --git a/NativeScript/napi/v8/jsr.cpp b/NativeScript/napi/v8/jsr.cpp index 39113913f..53286d417 100644 --- a/NativeScript/napi/v8/jsr.cpp +++ b/NativeScript/napi/v8/jsr.cpp @@ -583,3 +583,38 @@ napi_status js_get_runtime_version(napi_env env, napi_value* version) { return napi_ok; } + +napi_status js_get_array_doubles(napi_env env, napi_value array, double* out, uint32_t capacity, + uint32_t* length) { + v8::Local value = v8impl::V8LocalValueFromJsValue(array); + if (!value->IsArray()) return napi_array_expected; + v8::Local jsArray = value.As(); + uint32_t count = jsArray->Length(); + if (count > capacity) count = capacity; + + struct State { + double* out; + uint32_t count; + uint32_t written; + bool numbersOnly; + } state{out, count, 0, true}; + + v8::Maybe result = jsArray->Iterate( + env->context(), + [](uint32_t index, v8::Local element, void* data) { + auto* s = static_cast(data); + if (index >= s->count) return v8::Array::CallbackResult::kBreak; + if (!element->IsNumber()) { + s->numbersOnly = false; + return v8::Array::CallbackResult::kBreak; + } + s->out[index] = element.As()->Value(); + s->written++; + return v8::Array::CallbackResult::kContinue; + }, + &state); + if (result.IsNothing()) return napi_generic_failure; + if (!state.numbersOnly || state.written != count) return napi_number_expected; + *length = count; + return napi_ok; +}