GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — perf(android): native buffer copies for http bodies, single-pass readTextSync by edusperoni · Pull Request #11636 · NativeScript/NativeScript

Conversation

@edusperoni

@edusperoni edusperoni commented Nov 11, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

PR Checklist

  • The PR title follows our guidelines: https://github.com/NativeScript/NativeScript/blob/master/tools/notes/CONTRIBUTING.md#commit-messages.
  • There is an issue for the bug/feature this PR is for. To avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it.
  • You have signed the CLA.
  • All existing tests are passing: https://github.com/NativeScript/NativeScript/blob/master/tools/notes/DevelopmentWorkflow.md#unit-testing.
  • Tests for the changes are included - https://github.com/NativeScript/NativeScript/blob/master/tools/notes/WritingUnitTests.md.

What is the current behavior?

These came up while profiling the Android runtime. A few Android code paths in core move data across JNI one element or one line at a time:

  • HttpContent.toArrayBuffer() (http/http-request/index.android.ts) runs Uint8Array.from(raw.toByteArray()). Each read of a Java byte[] element goes through an interceptor and a JNI call, so this costs one JNI round trip per byte. It runs for every toArrayBuffer() call and every XHR/fetch arraybuffer or blob response.
  • ArrayBuffer request bodies (http/http-request-internal/index.android.ts) are sent with ByteBuffer.wrap(Array.from(typedArray)). That builds a JS array of N numbers, then converts it to byte[] one element at a time.
  • readTextSync (file-system/file-system-access.android.ts) makes one JNI readLine() call per line. Because it rebuilds the text from lines, it also changes the content (see below).

There are also two dead-code cleanups: return global.__native(this) in native class constructors, and an unused SnapshotViewGroup in the page transition.

What is the new behavior?

Every change is its own commit, so any of them can be dropped.

1. perf(http): response bodies to ArrayBuffer in native code

toArrayBuffer() now returns ArrayBuffer.from(java.nio.ByteBuffer.wrap(bytes)). The runtime's ArrayBuffer.from (a runtime global since 2016, so every supported Android runtime has it) copies a heap ByteBuffer's remaining bytes with one memcpy.

ArrayBuffer.from also sets a nativeObject property on the result, pointing back to the source ByteBuffer. The new code deletes that property, which keeps the result the same as before (a plain ArrayBuffer with no extra properties). It matters for two reasons:

  • it would keep the Java byte[] copy alive for as long as the ArrayBuffer lives;
  • the runtime's copy already drained that ByteBuffer (get() moved its position to the end), and FileSystemAccess.getBuffer() prefers nativeObject. Without the delete, File.writeSync(response.content.toArrayBuffer()) would write 0 bytes.

Empty bodies still return an empty ArrayBuffer.

I checked the xhr and fetch code for other per-element copies. They both go through toArrayBuffer(), and I found nothing else of this kind.

2. perf(http): ArrayBuffer request bodies without a JS array, on newer runtimes

NativeScript/android#2102 (not merged yet) makes the runtime copy a typed array passed to a byte[] parameter with one memcpy. With that runtime, ByteBuffer.wrap(typedArray) is correct and fast. On older runtimes the same call hands a direct ByteBuffer to a byte[] parameter. That is type confusion, and it corrupts memory in release builds.

That rules out a runtime probe: trying the call on an old runtime is the bug itself. So the fast path is gated on the runtime version instead:

  • A new helper, utils/android-runtime-version.ts, checks global.__runtimeVersion once and caches the result.
  • The fast path is used only when the version parses as major.minor.patch[-prerelease] and is strictly greater than 9.1.2. 9.1.2-next.* counts as lower than 9.1.2; 9.1.3-alpha.1, 9.2.0 and 10.0.0 count as greater.
  • A missing, non-string or unparsable version keeps the existing Array.from path.
  • The helper is pure JS, so it is safe to import anywhere. Unit tests cover the version parsing.

The threshold assumes android#2102 ships in a release after 9.1.2. If it lands in a different version, only the constant needs to change.

3. perf(file-system) + fix(file-system): readTextSync reads the whole file

readTextSync now reads the file in 64 KiB chunks into a ByteArrayOutputStream and decodes it with one toString(charset) call.

  • I didn't use java.nio.file.Files: it needs API 26, and the widgets minSdk is 21.
  • File.length() is only used as a size hint, so special files like /proc/* (which report a length of 0) still read correctly.
  • The stream is now always closed. Before, it leaked whenever an error was thrown.
  • The encoding parameter, the UTF-8 BOM removal and the onError behaviour are unchanged.

Important

Behaviour change (separate commit, fix(file-system): return Android readTextSync content unchanged). Because the old code rebuilt the text from readLine() results, readTextSync on Android:

  • turned \r\n and lone \r into \n;
  • dropped the trailing line break;
  • dropped leading blank lines.

It now returns the file content unchanged, like Android readText() (Async.File.readText) and iOS readTextSync()/readText() already do. The commit before it (perf(file-system)) keeps the old output exactly. If this change isn't wanted, drop the fix commit and the speedup stays.

I added test_FileReadTextKeepsLineBreaks to apps/automated to cover this. It writes CRLF/CR/LF content larger than one read chunk and checks that readTextSync() and readText() both return it unchanged.

4a. refactor(android): remove the no-op return global.__native(this) (optional)

__native (the runtime's ts_helpers.js) only does something for the placeholder object of the pre-TypeScript 2.0 emit, _super.call(this); return __native(this);. With the current emit, var _this = _super.call(this) || this already holds the native instance. That instance never has __container__, so __native returns it unchanged.

This commit removes all 34 plain return global.__native(this); constructor returns in packages/core. The global itself is not touched, and neither is any other use of it.

Static binding generator note: when a constructor body becomes only super(...), the ES5 emit becomes return _super.call(this, ...) || this;. In core's build that is 6 classes:

  • TNSAccessibilityDelegateImpl
  • SeekBarChangeListenerImpl
  • LruBitmapCache
  • DialogFragmentImpl
  • TabHostImpl
  • fragment-transitions AnimationListenerImpl

The Android SBG only learns to find the class location in that shape with NativeScript/android#2101 (not merged). This is not a regression on 9.0–9.1.x runtimes: a naming bug there (also fixed by #2101) means these classes are generated at runtime anyway. On runtimes older than 9.0, these 6 classes would switch from prebuilt to runtime-generated until #2101 ships. If that matters, this commit can wait until #2101 is released.

4b. refactor(transition): remove unused Android snapshot helpers (optional)

SnapshotViewGroup was only created by ContentViewSnapshot, which was only used by loadViewInBackground. Neither loadViewInBackground nor renderToImageSource was called anywhere except in a commented-out block. This commit removes that chain, the commented-out block and the imports only it used. The SBG still emitted a binding for SnapshotViewGroup without a location, because terser drops the class as unused.

Verification

  • npx nx run core:test: 57 files, 646 tests pass. That includes the new utils/android-runtime-version.spec.ts, which covers undefined, null, a number, "", "garbage", 9.1.1, 9.1.2, 9.1.2-next.2026-11-11-1, 9.1.3, 9.1.3-alpha.1, 9.2.0, 9.10.0, 10.0.0 and a few malformed strings.
  • npx nx run core:build and npx nx run core:lint pass. In the built output I checked the ES5 constructor shape of the 6 classes listed in 4a.
  • apps/automated on an Android emulator (API 36, @nativescript/android 9.1.1): full suite 1699 OK, 0 failed, including HTTP, XHR, FETCH, FILE-SYSTEM and the UI suites for every class touched by 4a. I also ran a temporary benchmark module (not committed):
before after
toArrayBuffer(), 1 MiB body 268–315 ms ~1 ms (0.6–2.4 ms over 20 runs)
readTextSync(), 1 MiB, 16,645 CRLF lines ~12 ms 2–3.5 ms
request body, 1 MiB, ByteBuffer.wrap(Array.from(u8)) (the path still used on ≤ 9.1.2) ~35 ms —

The benchmark also checked that:

  • toArrayBuffer() returns the same bytes as before, both for a real HTTPS response and for a synthetic 1 MiB body;
  • an empty body gives an empty ArrayBuffer, and the result has no nativeObject;
  • writeBufferSync() of the result writes all 1 MiB. Without the delete, it writes 0 bytes;
  • readTextSync() returns the exact content, still strips a UTF-8 BOM, still calls onError and returns undefined for a missing file, and reads /proc/self/status.

Fast path (2) on a runtime with android#2102: I built a runtime package from the android#2102 branch (__runtimeVersion 9.1.3-pr2102, arm64, optimized flavors) and ran apps/automated with it on the same emulator. To reach a local echo server I temporarily enabled cleartext traffic and added the same temporary module; none of that is committed.

  • runtimeCopiesTypedArraysToByteArrays() returns true.
  • I posted to a local echo server and compared each response with the bytes sent. http.request with ArrayBuffer content (empty, all 256 byte values, 1 MiB), XMLHttpRequest.send(ArrayBuffer) and fetch(..., { body: ArrayBuffer }) all came back byte-identical. The server logged the expected lengths.
  • I also checked the runtime copy itself: ByteBuffer.wrap(view) holds exactly the view's bytes for an empty array, high bytes (0x80–0xff), Int8Array, Uint8ClampedArray, 1 MiB, and a subarray with a non-zero byteOffset. Core always passes new Uint8Array(arrayBuffer), so its offset is 0, but the runtime honours the view either way.
  • 1 MiB request-body conversion on this runtime: 31–40 ms before (ByteBuffer.wrap(Array.from(u8))), 0.33–0.46 ms after (ByteBuffer.wrap(u8), 20 runs). Both paths produce the same bytes.
  • Full apps/automated suite on this runtime: 1700 OK, 0 failed.

On both runtimes, Blob and typed-array (non-ArrayBuffer) request bodies reach the server empty, through xhr or fetch. This already happens on main, and judging from the code on iOS too: both platforms' requestInternal only send string, FormData and ArrayBuffer content. This PR does not change that.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
edusperoni added 6 commits November 11, 2026 01:15
@edusperoni
perf(http): convert Android response bodies to ArrayBuffer natively
0443ee5
toArrayBuffer() copied the Java byte[] into a Uint8Array one element at a time, one JNI round trip per byte. ArrayBuffer.from(ByteBuffer) copies it in native code. The runtime's back-reference to the source ByteBuffer is removed so the Java copy can be collected and FileSystemAccess.getBuffer() keeps using the ArrayBuffer itself.
@edusperoni
perf(http): pass ArrayBuffer request bodies to Java without a JS arra…
e4ad740
…y on newer Android runtimes

ByteBuffer.wrap(Array.from(typedArray)) builds a JS array of N numbers and converts it to byte[] element by element. Android runtimes after 9.1.2 copy a typed array passed to a byte[] parameter with a single memcpy, so the typed array is passed directly there.

Older runtimes hand a typed array to a byte[] parameter as a direct ByteBuffer, which is type confusion, so the fast path is gated on __runtimeVersion instead of being probed. Unknown or unparsable versions keep the existing path.
@edusperoni
perf(file-system): read the whole file in Android readTextSync
d60dac9
readTextSync made one JNI readLine() call per line and concatenated the lines in JS. It now reads the bytes in 64 KiB chunks and decodes them with a single ByteArrayOutputStream.toString(charset) call, and always closes the stream.

The line-by-line output is kept for now: CR and CRLF become LF, leading blank lines and the final line break are dropped.
@edusperoni
fix(file-system): return Android readTextSync content unchanged
6b36121
readTextSync rebuilt the text from readLine() results, so CR and CRLF became LF, leading blank lines and the final line break were dropped. It now returns the decoded file content as is, like readText() on Android and readTextSync()/readText() on iOS. The UTF-8 BOM is still removed.
@edusperoni
refactor(android): remove no-op global.__native(this) constructor ret…
b8069be
…urns

__native only unwraps the placeholder object of the pre-TypeScript 2.0 constructor emit (super(); return __native(this)). With the current emit the constructor already holds the native instance from var _this = _super.call(this) || this, so __native returns it unchanged.
@edusperoni
refactor(transition): remove unused Android snapshot helpers
c74e2f6
SnapshotViewGroup was only created by ContentViewSnapshot, which was only used by loadViewInBackground. Neither loadViewInBackground nor renderToImageSource had a caller outside a commented-out block, so the bundle still carried an unused native class that the static binding generator could not place.
@nx-cloud

nx-cloud Bot commented Nov 11, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit c74e2f6

Command Status Duration Result
nx test apps-automated -c=android ✅ Succeeded 3m 36s View ↗
nx run-many --target=test --configuration=ci --... ✅ Succeeded 10s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-11-11 04:34:55 UTC

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@pkg-pr-new

pkg-pr-new Bot commented Nov 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@nativescript/core@11636
npm i https://pkg.pr.new/@nativescript/vite@11636
npm i https://pkg.pr.new/@nativescript/webpack@11636

commit: c74e2f6

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

farfromrefug
farfromrefug approved these changes Nov 11, 2026
View reviewed changes
@farfromrefug

farfromrefug commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator

@edusperoni it would be better to split those PRs when they are fixing/clearing different thing. And with AI it is as simple as asking it to create different PRs

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@farfromrefug farfromrefug farfromrefug approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@edusperoni @farfromrefug
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.