Skip to content

ffi: reuse the callable created per symbol - #64971

Merged
trivikr merged 1 commit into
nodejs:mainfrom
trivikr:ffi-get-function-reuse-wrapper
Aug 9, 2026
Merged

ffi: reuse the callable created per symbol#64971
trivikr merged 1 commit into
nodejs:mainfrom
trivikr:ffi-get-function-reuse-wrapper

Conversation

@trivikr

@trivikr trivikr commented Aug 2, 2026

Copy link
Copy Markdown
Member

Fixes: #64970

CreateFunction() ran on every getFunction() call, every getFunctions() call, and every read of the functions accessor, each time emitting a trampoline, allocating an FFIFunctionInfo, and on the SharedBuffer path an ArrayBuffer. lib.functions.foo was therefore a different function on each read, and calling through the accessor in a loop leaked a page per iteration until GC: 20000 calls grew RSS by 58 MiB.

Cache the created callable per symbol in function_wrappers_, and memoize the JS wrapper composed around it. Both entries are weak, so dropping the last user reference still releases the wrapper and its trampoline. The JS side stores a WeakRef because V8 can keep a raw function alive after the wrapper is gone, and a strong value would pin every wrapper for the lifetime of the library.

Before

same wrapper on two reads: false
sum: 840000
rss growth: 56.0 MiB

After

same wrapper on two reads: true
sum: 840000
rss growth: 9.8 MiB

Assisted-by: claude:opus-5

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/ffi

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. ffi Issues and PRs related to experimental Foreign Function Interface support. needs-ci PRs that need a full CI run. labels Aug 2, 2026
@trivikr trivikr added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 2, 2026
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.37838% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.33%. Comparing base (51a55d4) to head (bf2f995).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/node_ffi.cc 54.54% 4 Missing and 1 partial ⚠️
lib/ffi.js 88.46% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64971      +/-   ##
==========================================
+ Coverage   90.31%   90.33%   +0.02%     
==========================================
  Files         760      760              
  Lines      248496   248522      +26     
  Branches    46894    46904      +10     
==========================================
+ Hits       224428   224513      +85     
+ Misses      15492    15444      -48     
+ Partials     8576     8565      -11     
Files with missing lines Coverage Δ
src/node_ffi.h 72.22% <ø> (ø)
lib/ffi.js 94.62% <88.46%> (-0.62%) ⬇️
src/node_ffi.cc 70.73% <54.54%> (+0.02%) ⬆️

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@trivikr trivikr added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Aug 3, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 3, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikr
trivikr marked this pull request as draft August 3, 2026 15:07
@trivikr

This comment was marked as outdated.

@trivikr
trivikr marked this pull request as ready for review August 4, 2026 14:47
@trivikr trivikr added commit-queue Add this label to land a pull request using GitHub Actions. and removed commit-queue Add this label to land a pull request using GitHub Actions. labels Aug 9, 2026
CreateFunction() ran on every getFunction() call, every getFunctions()
call, and every read of the functions accessor, each time emitting a
trampoline, allocating an FFIFunctionInfo, and on the SharedBuffer path
an ArrayBuffer. lib.functions.foo was therefore a different function on
each read, and calling through the accessor in a loop leaked a page per
iteration until GC: 20000 calls grew RSS by 58 MiB.

Cache the created callable per symbol in function_wrappers_, and
memoize the JS wrapper composed around it. Both entries are weak, so
dropping the last user reference still releases the wrapper and its
trampoline. The JS side stores a WeakRef because V8 can keep a raw
function alive after the wrapper is gone, and a strong value would pin
every wrapper for the lifetime of the library.

Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com>
Assisted-by: claude:opus-5
PR-URL: nodejs#64971
Fixes: nodejs#64970
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
@trivikr
trivikr force-pushed the ffi-get-function-reuse-wrapper branch from 16a5d6e to bf2f995 Compare August 9, 2026 23:19
@trivikr
trivikr merged commit bf2f995 into nodejs:main Aug 9, 2026
18 of 19 checks passed
@trivikr

trivikr commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Landed in bf2f995

@trivikr
trivikr deleted the ffi-get-function-reuse-wrapper branch August 9, 2026 23:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs that have at least one approval, no pending requests for changes, and a CI started. c++ Issues and PRs that require attention from people who are familiar with C++. ffi Issues and PRs related to experimental Foreign Function Interface support. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ffi: getFunction() and the functions accessor rebuild wrappers on every call

3 participants