Title: ffi: AllocateCodeNear() probes up to 2048 pages with a failing mmap each, costing ~790 µs per trampoline · Issue #64968 · nodejs/node · GitHub
Open Graph Title: ffi: AllocateCodeNear() probes up to 2048 pages with a failing mmap each, costing ~790 µs per trampoline · Issue #64968 · nodejs/node
X Title: ffi: AllocateCodeNear() probes up to 2048 pages with a failing mmap each, costing ~790 µs per trampoline · Issue #64968 · nodejs/node
Description: Version main Platform Linux Subsystem ffi What steps will reproduce the bug? repro.c #include
Open Graph Description: Version main Platform Linux Subsystem ffi What steps will reproduce the bug? repro.c #include
X Description: Version main Platform Linux Subsystem ffi What steps will reproduce the bug? repro.c #include <stdint.h> // Fast-eligible: 2 register arguments, scalar return. Resolving this signature // mak...
Opengraph URL: https://github.com/nodejs/node/issues/64968
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"ffi: AllocateCodeNear() probes up to 2048 pages with a failing mmap each, costing ~790 µs per trampoline","articleBody":"### Version\n\nmain\n\n### Platform\n\n```text\nLinux\n```\n\n### Subsystem\n\nffi\n\n### What steps will reproduce the bug?\n\n`repro.c`\n```c\n#include \u003cstdint.h>\n\n// Fast-eligible: 2 register arguments, scalar return. Resolving this signature\n// makes CreateFastFFIMetadata() emit a trampoline through AllocateCodeNear().\nint32_t add2(int32_t a, int32_t b) {\n return a + b;\n}\n\n// Fast-ineligible baseline: 9 arguments exceeds V8's fast-call cap, so no\n// trampoline is emitted and AllocateCodeNear() is never reached.\nint32_t add9(int32_t a, int32_t b, int32_t c, int32_t d, int32_t e, int32_t f,\n int32_t g, int32_t h, int32_t i) {\n return a + b + c + d + e + f + g + h + i;\n}\n```\n\n`repro.js`\n```js\nimport { DynamicLibrary, suffix } from 'node:ffi';\nimport { stat } from 'node:fs/promises';\n\nconst libPath = new URL(`./repro.${suffix}`, import.meta.url).pathname;\nawait stat(libPath);\n\nconst fastEligible = { arguments: ['i32', 'i32'], return: 'i32' };\nconst fastIneligible = { arguments: Array(9).fill('i32'), return: 'i32' };\n\nconst lib = new DynamicLibrary(libPath);\n\nfunction timeResolve(name, signature) {\n const start = performance.now();\n lib.getFunction(name, signature);\n return (performance.now() - start) * 1000;\n}\n\n// Warm up one-time initialization (libffi setup, executable-memory self-test)\n// so it is not attributed to the first measured resolution.\nlib.getFunction('add9', fastIneligible);\nlib.getFunction('add2', fastEligible);\n\n// Same symbol resolution work, but no trampoline is allocated.\nconsole.log(`add9, no trampoline: ${timeResolve('add9', fastIneligible).toFixed(1)} us`);\nconsole.log(`add2, first probe: ${timeResolve('add2', fastEligible).toFixed(1)} us`);\n\n// Every fast-eligible resolution allocates another trampoline page next to the\n// library, so the free pages inside AllocateCodeNear()'s +/- 1024 page probe\n// window are steadily consumed. Once they are gone, each allocation walks the\n// whole window, fails every MAP_FIXED_NOREPLACE probe, and takes the plain-mmap\n// fallback it could have taken immediately.\nconst samples = [];\nfor (let i = 0; i \u003c 1200; i++) samples.push(timeResolve('add2', fastEligible));\n\nfor (let i = 0; i \u003c samples.length; i += 200) {\n const window = samples.slice(i, i + 200);\n const mean = window.reduce((a, b) => a + b, 0) / window.length;\n console.log(`add2, resolves ${i + 1}-${i + window.length}: mean ${mean.toFixed(1)} us`);\n}\n\nlib.close();\n```\n\nCommands to run:\n```sh\n$ cc -shared -fPIC -O2 -o repro.so repro.c\n\n$ node --experimental-ffi repro.js\n```\n\n### How often does it reproduce? Is there a required condition?\n\nAlways\n\n### What is the expected behavior? Why is that the expected behavior?\n\nResolving a fast-eligible signature costs about as much as a fast-ineligible one, since the extra work is a single page allocation.\n\n### What do you see instead?\n\n```console\nadd9, no trampoline: 29.0 us\nadd2, first probe: 31.1 us\nadd2, resolves 1-200: mean 517.5 us\nadd2, resolves 201-400: mean 657.0 us\nadd2, resolves 401-600: mean 728.0 us\nadd2, resolves 601-800: mean 751.0 us\nadd2, resolves 801-1000: mean 753.1 us\nadd2, resolves 1001-1200: mean 755.4 us\n```\n\nOnce the pages near the library are used up, each fast-eligible resolution costs ~28x a fast-ineligible one, spent entirely on ~2048 `mmap` calls that are all expected to fail.\n\n### Additional information\n\n_No response_","author":{"url":"https://github.com/trivikr","@type":"Person","name":"trivikr"},"datePublished":"2026-08-02T21:39:46Z","url":"https://github.com/nodejs/node/issues/64968"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:695209f2-7003-73a8-394f-0f3f1004dde9 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | E23A:32386D:635190:80C58D:6AED2AC9 |
| html-safe-nonce | 5c79b4ed2ba5c133bc7aa7d64c7d2067b1420c7b28e6f1e236b7b2b240b22a86 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFMjNBOjMyMzg2RDo2MzUxOTA6ODBDNThEOjZBRUQyQUM5IiwidmlzaXRvcl9pZCI6Ijc5NTA3MDAyMDE1MTQ5Njk4MDEiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 6b8ee5ea001599c5e542e23680df2692f8c19df2d220f4138f848a7a5d8f7389 |
| hovercard-subject-tag | issue:5044584197 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/64968/issue_layout |
| twitter:image | https://opengraph.githubassets.com/3725924c95f465d28e681f2940d6dd92c66327a910d7be57cb61faacbf92c515/nodejs/node/issues/64968 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/3725924c95f465d28e681f2940d6dd92c66327a910d7be57cb61faacbf92c515/nodejs/node/issues/64968 |
| og:image:alt | Version main Platform Linux Subsystem ffi What steps will reproduce the bug? repro.c #include |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | trivikr |
| hostname | github.com |
| expected-hostname | github.com |
| None | 9d95365dda0576efe9b71f45807595e396bd0ca9286fba5367a05048e6ddf8f2 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 98b41fa4b5635ceb45a4c54320fced73817ff290 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width