Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

[Android] HttpContent.toArrayBuffer() and ArrayBuffer request bodies are copied byte by byte over JNI

Abierto
#11,467 1 comentario 0 reacciones 0 asignados Ver en GitHub

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

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

bug-pending-triage
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()

https://github.com/NativeScript/NativeScript/blob/a032c224c5bb5909be03c4a8dc7390d0ed23e8c0/packages/core/http/http-request/index.android.ts#L11-L13

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

https://github.com/NativeScript/NativeScript/blob/a032c224c5bb5909be03c4a8dc7390d0ed23e8c0/packages/core/http/http-request-internal/index.android.ts#L164-L168

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
Lenguaje dominante
TypeScript
Estrellas
25.7k
Forks
1.7k
Merge medio
3 d 7 min
PR fusionados (30 d)
39

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de NativeScript/NativeScript

Todos los issues de NativeScript/NativeScript

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.