[Android] HttpContent.toArrayBuffer() and ArrayBuffer request bodies are copied byte by byte over JNI
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 76/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- android, java, typescript
- Área
- api, mobile-dev, performance
Línea de trabajo
Start with packages/core/http/http-request/index.android.ts and packages/core/http/http-request-internal/index.android.ts, then compare the existing FileSystemAccess.readBufferAsync ByteBuffer conversion. Update both Android HTTP conversion paths and verify that received and sent bytes remain identical, including negative Java byte values, while the supplied benchmarks show the reduced copying cost.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Issue Description
This is a runtime performance issue on Android. Both directions of binary HTTP data are converted between JS and Java one element at a time, on the calling thread (usually the UI thread).
Receiving: HttpContent.toArrayBuffer()
toByteArray() returns a Java byte[], which on the JS side is a proxy to a native object, not data in the V8 heap. Uint8Array.from reads it element by element, and every index is a separate call across the JNI bridge. This path is used by Http.getBinary(), by XMLHttpRequest with responseType arraybuffer/blob, and therefore by fetch().arrayBuffer()/.blob().
Sending: ArrayBuffer request body
The buffer is expanded into a plain JS array, and the runtime then marshals that array into a Java byte[] element by element. It is cheaper than the receiving side but still linear.
Measurements
Android 16 emulator (arm64, sdk_gphone64_arm64), median of 5 runs after warm-up. Every variant was checked to produce identical bytes, including negative Java bytes.
Response body → ArrayBuffer |
16 KB | 100 KB | 300 KB | 1024 KB |
|---|---|---|---|---|
Uint8Array.from(raw.toByteArray()).buffer (current) |
9.5 ms | 64.4 ms | 170.8 ms | 620.4 ms |
ArrayBuffer.from(ByteBuffer.wrap(raw.toByteArray())) |
0.1 ms | 0.3 ms | 0.3 ms | 1.1 ms |
ByteBuffer.allocateDirect(n) + put + ArrayBuffer.from |
0.1 ms | 0.1 ms | 0.2 ms | 0.7 ms |
ArrayBuffer → request body |
16 KB | 100 KB | 300 KB | 1024 KB |
|---|---|---|---|---|
ByteBuffer.wrap(Array.from(new Uint8Array(buffer))) (current) |
0.9 ms | 5.8 ms | 16.1 ms | 65.2 ms |
ByteBuffer.allocate(n).put(buffer) |
0.0 ms | 0.0 ms | 0.0 ms | 0.0 ms |
For comparison, the iOS implementations of the same two paths (interop.bufferFromData(this.raw) and NSData.dataWithData(buffer)) take under 0.1 ms at 1 MB on an iOS 18.6 simulator.
Expected behavior: converting an HTTP body between ArrayBuffer and native memory should cost about one memory copy, as it already does on iOS and in File.readBuffer* on Android.
Suggested fix
The runtime already converts between java.nio.ByteBuffer and ArrayBuffer in a single step, and core relies on this in FileSystemAccess.readBufferAsync (ArrayBuffer.from(result)).
Receiving:
toArrayBuffer(this: BaseHttpContent) {
return ArrayBuffer.from(java.nio.ByteBuffer.wrap((this.raw as java.io.ByteArrayOutputStream).toByteArray()));
},
(or allocateDirect + put + ArrayBuffer.from, which matches what readBufferAsync receives from Async.File.readBuffer; the difference is below a millisecond.)
Sending: the body has to stay a heap buffer, because Async.Http writes it with ByteBuffer.array():
} else if (options.content instanceof ArrayBuffer) {
javaOptions.content = java.nio.ByteBuffer.allocate(options.content.byteLength).put(options.content as any);
}
I intend to submit a PR for this issue.
Note: the draft PR #10069 (switch to OkHttp) moves the same Uint8Array.from(raw.toByteArray()).buffer line to its new location, so the issue would carry over.
Found along the way (separate issue)
While preparing this, I found that binary request bodies other than ArrayBuffer are silently dropped on both Android and iOS. Http.request({ content: Uint8Array }), xhr.send(Uint8Array | Blob) and fetch(url, { body: Uint8Array | Blob }) all reach the server with Content-Length: 0. The cause is that both requestInternal implementations only accept content instanceof ArrayBuffer, even though HttpRequestOptions.content is typed to accept Uint8Array.
Reproduction
No network needed. Run on Android with ns run android:
const size = 300 * 1024;
// Receiving: same type as HttpContent.raw on Android
const bytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, size);
new java.util.Random(42).nextBytes(bytes);
const raw = new java.io.ByteArrayOutputStream(size);
raw.write(bytes, 0, size);
let at = Date.now();
Uint8Array.from(raw.toByteArray()).buffer; // what toArrayBuffer() does today
console.log(`receive, current: ${Date.now() - at} ms`);
at = Date.now();
ArrayBuffer.from(java.nio.ByteBuffer.wrap(raw.toByteArray()));
console.log(`receive, via ByteBuffer: ${Date.now() - at} ms`);
// Sending: what requestInternal does with an ArrayBuffer body today
const body = new Uint8Array(size).buffer;
at = Date.now();
java.nio.ByteBuffer.wrap(Array.from(new Uint8Array(body)));
console.log(`send, current: ${Date.now() - at} ms`);
at = Date.now();
java.nio.ByteBuffer.allocate(size).put(body);
console.log(`send, via ByteBuffer: ${Date.now() - at} ms`);
With a real response:
import { Http } from '@nativescript/core';
const response = await Http.request({ url: '<any page of a few hundred KB>', method: 'GET' });
const at = Date.now();
const buffer = response.content.toArrayBuffer();
console.log(`toArrayBuffer: ${Date.now() - at} ms for ${buffer.byteLength} bytes`);
Relevant log output (if applicable)
Environment
OS: macOS 15.7.3
CPU: (10) arm64 Apple M1 Pro
Shell: /opt/homebrew/bin/fish
node: 26.10.0
npm: 11.19.1
nativescript: 9.1.1
# android
java: 17.0.20.1
ndk: Not Found
apis: 33, 34, 35, 36, 36
build_tools: 33.0.1, 34.0.0, 35.0.0, 35.0.1, 36.0.0, 36.1.0
system_images:
- android-35 | Google Play ARM 64 v8a
- android-36 | Google Play ARM 64 v8a
# ios
xcode: 26.3/17C529
cocoapods: 1.16.2
python: Not Found
python3: 3.9.6
ruby: 3.3.12
platforms:
- DriverKit 25.2
- iOS 26.2
- macOS 26.2
- tvOS 26.2
- visionOS 26.2
- watchOS 26.2
Dependencies
"dependencies": {
"@valor/nativescript-websockets": "^2.0.3",
"nativescript-theme-core": "^1.0.4"
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "2.1.3",
"@angular/build": "^21.0.0",
"@angular/compiler-cli": "^21.0.0",
"@csstools/css-calc": "~2.1.2",
"@csstools/css-color-parser": "^3.0.8",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@nativescript/hook": "^3.0.4",
"@nativescript/nx": "^22.0.0",
"@nstudio/focus": "^20.0.2",
"@nstudio/nps-i": "~2.0.0",
"@nx/devkit": "22.5.4",
"@nx/eslint-plugin": "22.5.4",
"@nx/jest": "22.5.4",
"@nx/js": "22.5.4",
"@nx/node": "22.5.4",
"@nx/plugin": "22.5.4",
"@nx/vite": "22.5.4",
"@nx/vitest": "22.5.4",
"@nx/web": "22.5.4",
"@nx/workspace": "22.5.4",
"@prettier/plugin-xml": "^3.4.1",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-replace": "^6.0.3",
"@swc-node/register": "1.11.1",
"@swc/core": "1.15.8",
"@swc/helpers": "0.5.19",
"@types/jest": "30.0.0",
"@types/node": "^20.0.0",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.46.4",
"@vitejs/plugin-vue": "^6.0.5",
"@vitejs/plugin-vue-jsx": "^5.1.5",
"@vitest/coverage-v8": "4.0.9",
"@vitest/ui": "4.0.9",
"@vue/compiler-sfc": "^3.5.24",
"acorn": "^8.15.0",
"acorn-stage3": "^4.0.0",
"copy-webpack-plugin": "^13.0.0",
"copyfiles": "^2.4.0",
"css": "^3.0.0",
"css-tree": "^3.1.0",
"css-what": "^6.1.0",
"dotenv": "~16.4.0",
"dotenv-webpack": "^7.0.0",
"emoji-regex": "^10.3.0",
"enhanced-resolve": "^5.18.3",
"esbuild": "^0.27.4",
"eslint": "~8.57.0",
"eslint-config-prettier": "^10.0.0",
"fork-ts-checker-webpack-plugin": "^7.0.0",
"form-data": ">=4.0.4",
"gonzales": "^1.0.7",
"husky": "^9.0.0",
"jest": "30.0.5",
"jest-environment-jsdom": "30.0.5",
"jest-util": "30.0.5",
"jiti": "2.4.2",
"jsdom": "~22.1.0",
"lint-staged": "^15.2.0",
"loader-utils": "^2.0.0 || ^3.0.0",
"module-alias": "^2.2.2",
"nativescript": "9.1.0-alpha.17",
"nativescript-typedoc-theme": "1.1.0",
"nx": "22.5.4",
"parse-css": "git+https://github.com/tabatkins/parse-css.git",
"parserlib": "^1.1.1",
"plist": "^5.0.0",
"postcss": "^8.0.0",
"postcss-import": "^16.0.0",
"postcss-loader": "^8.0.0",
"prettier": "^3.2.5",
"react-reconciler": "^0.33.0",
"sass": "^1.72.0",
"sass-loader": "^16.0.0",
"shady-css-parser": "^0.1.0",
"terser-webpack-plugin": "^5.0.0",
"tree-kill": "^1.2.2",
"ts-dedent": "^2.2.0",
"ts-jest": "29.4.5",
"ts-loader": "^9.0.0",
"ts-node": "10.9.2",
"ts-patch": "^3.0.0",
"tslib": "^2.6.0",
"typedoc": "^0.28.14",
"typescript": "5.9.3",
"vite": "^8.0.0",
"vite-plugin-solid": "^2.11.11",
"vite-plugin-static-copy": "^4.1.1",
"vitest": "4.0.9",
"vue-loader": "^15.0.0 <= 15.9.8",
"vue-tsc": "^3.2.5",
"webpack-bundle-analyzer": "^4.0.0",
"webpack-chain": "^6.0.0",
"webpack-merge": "^6.0.0",
"webpack-virtual-modules": "^0.4.0",
"zx": "^8.3.0"
}
Please accept these terms
- I have searched the existing issues as well as StackOverflow and this has not been posted before
- This is a bug report
- I agree to follow this project's Code of Conduct
- Lenguaje dominante
- TypeScript
- Estrellas
- 25.7k
- Forks
- 1.7k
- Merge medio
- 3 d 7 min
- PR fusionados (30 d)
- 39
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de NativeScript/NativeScript
-
bug-pending-triage
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
NativeScript/NativeScript#11460 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 90/100
NativeScript/NativeScript#11448 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
NativeScript/NativeScript#11068 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 3/5 1-2 días Aptitud para principiantes 72/100
NativeScript/NativeScript#11480 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 25/100
NativeScript/NativeScript#11461 ·
Los mantenedores suelen responder en 1 día
Todos los issues de NativeScript/NativeScript
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
Los mantenedores suelen responder en 1 día
-
📕documentation
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
db-ux-design-system/core-web#8343 ·
Los mantenedores suelen responder en 1 día
-
enhancement triage/needs-triage
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
heygen-com/hyperframes#4944 ·
Los mantenedores suelen responder en 1 día
-
Pressing Escape to close the time dropdown in the event form asks to discard the whole eventAbiertoai-driven-qa bug claude
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
linagora/twake-calendar-frontend#1493 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
material-extensions/vscode-material-icon-theme#3610 ·
Los mantenedores suelen responder en 2 días