@photostructure/heap-trim 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/LICENSE.txt +7 -0
- package/README.md +71 -2
- package/binding.gyp +291 -0
- package/dist/index.cjs +115 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +35 -0
- package/dist/index.d.mts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.mjs +87 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +139 -4
- package/prebuilds/linux-arm64/@photostructure+heap-trim.glibc.node +0 -0
- package/prebuilds/linux-x64/@photostructure+heap-trim.glibc.node +0 -0
- package/scripts/install.cjs +41 -0
- package/src/binding.cpp +43 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
<!--
|
|
9
|
+
Added for new features.
|
|
10
|
+
Changed for changes in existing functionality.
|
|
11
|
+
Deprecated for soon-to-be removed features.
|
|
12
|
+
Removed for now removed features.
|
|
13
|
+
Fixed for any bug fixes.
|
|
14
|
+
Security in case of vulnerabilities.
|
|
15
|
+
-->
|
|
16
|
+
|
|
17
|
+
## [Unreleased]
|
|
18
|
+
|
|
19
|
+
## [1.0.0] - 2026-11-07
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`heapTrim()`** returns free heap memory to the operating system on glibc Linux by calling `malloc_trim(0)`. glibc's `free()` shrinks the heap only from its top, so pages freed below a live allocation otherwise stay resident. On musl, macOS, and Windows it does nothing and returns `false`, and the package builds and loads no native code there: macOS's `malloc_zone_pressure_relief()` and Windows' `_heapmin()` released no measurable memory in the same test.
|
|
24
|
+
|
|
25
|
+
[1.0.0]: https://github.com/photostructure/heap-trim/releases/tag/v1.0.0
|
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 Matthew McEachen
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @photostructure/heap-trim
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Returns free C heap pages to the operating system from Node.js. Built for and supported by [PhotoStructure](https://photostructure.com).
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@photostructure/heap-trim)
|
|
6
|
+
[](https://github.com/photostructure/heap-trim/actions/workflows/build.yml)
|
|
7
|
+
[](https://nodejs.org/dist/latest/docs/api/n-api.html#node-api-version-matrix)
|
|
8
|
+
[](https://github.com/photostructure/heap-trim)
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @photostructure/heap-trim
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { heapTrim } from "@photostructure/heap-trim";
|
|
18
|
+
|
|
19
|
+
// After a burst of native work has freed its memory, while the process is idle:
|
|
20
|
+
heapTrim();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## `heapTrim(): boolean`
|
|
24
|
+
|
|
25
|
+
glibc's `free()` shrinks the heap only from its top, so pages freed below a
|
|
26
|
+
live allocation stay in the process's RSS. Native addons such as SQLite,
|
|
27
|
+
image decoders, and `Buffer` backing stores allocate from that heap.
|
|
28
|
+
`heapTrim()` calls glibc's `malloc_trim(0)`, which releases those free pages in
|
|
29
|
+
every malloc arena.
|
|
30
|
+
|
|
31
|
+
It locks each arena while it walks it, so call it when the process is idle,
|
|
32
|
+
not after every request.
|
|
33
|
+
|
|
34
|
+
**Returns:** `true` if glibc found whole free pages and told the kernel to drop
|
|
35
|
+
them. That doesn't mean RSS fell: glibc doesn't check whether those pages were
|
|
36
|
+
still resident, so repeated calls keep returning `true`. `false` if it found
|
|
37
|
+
none, or the process doesn't use glibc.
|
|
38
|
+
|
|
39
|
+
The function is on both the named and the default export, from CommonJS and
|
|
40
|
+
ESM.
|
|
41
|
+
|
|
42
|
+
## Supported platforms
|
|
43
|
+
|
|
44
|
+
| Platform | Architecture | Node.js | What `heapTrim()` does |
|
|
45
|
+
| ------------- | ------------ | ------- | ------------------------ |
|
|
46
|
+
| Linux (glibc) | x64, arm64 | 22+ | Calls `malloc_trim(0)` |
|
|
47
|
+
| Linux (musl) | x64, arm64 | 22+ | Nothing; returns `false` |
|
|
48
|
+
| macOS | x64, arm64 | 22+ | Nothing; returns `false` |
|
|
49
|
+
| Windows | x64, arm64 | 22+ | Nothing; returns `false` |
|
|
50
|
+
|
|
51
|
+
The package ships a native addon only for glibc Linux, which requires GLIBC
|
|
52
|
+
2.31+ (Debian 11+, Ubuntu 20.04+). On musl, macOS, and Windows it builds and
|
|
53
|
+
loads no native code.
|
|
54
|
+
|
|
55
|
+
musl has no trim call. On macOS and Windows the allocators have one, but in
|
|
56
|
+
the test this package uses (128 MiB of 64 KiB `Buffer`s freed below a live
|
|
57
|
+
one), neither released anything:
|
|
58
|
+
|
|
59
|
+
- On macOS 27.2 (arm64), `malloc_zone_pressure_relief(NULL, 0)` returned 0
|
|
60
|
+
bytes, and the physical footprint stayed at 145 MB after the call.
|
|
61
|
+
- On Windows 11 (x64), the heap had already decommitted the freed pages before
|
|
62
|
+
any call, and `_heapmin()` changed private bytes by 0.1 MB.
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
- [Security reporting](./SECURITY.md) - How to report security issues
|
|
67
|
+
- [API Reference](https://photostructure.github.io/heap-trim/modules.html)
|
|
68
|
+
- [Releasing](./doc/RELEASING.md) - How a release is built, staged, and approved
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
Development of this library was assisted by AI coding tools. All changes are human-reviewed and tested.
|
package/binding.gyp
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
{
|
|
2
|
+
"variables": {
|
|
3
|
+
# Sanitizer builds (ASan/UBSan/TSan) must NOT enable _FORTIFY_SOURCE: its
|
|
4
|
+
# libc interceptors collide with the sanitizers' own, producing both false
|
|
5
|
+
# positives and false negatives. The OpenSSF Compiler Options Hardening
|
|
6
|
+
# Guide is explicit that FORTIFY should be off for instrumented builds.
|
|
7
|
+
#
|
|
8
|
+
# Sanitizer test runners export HEAP_TRIM_SANITIZE=1 so this flips to
|
|
9
|
+
# "on" for instrumented builds only.
|
|
10
|
+
# Relying on env CFLAGS ordering to override binding.gyp is fragile; this
|
|
11
|
+
# makes the intent explicit and deterministic.
|
|
12
|
+
#
|
|
13
|
+
# The value is deliberately "on"/"off" rather than 1/0: gyp coerces
|
|
14
|
+
# numeric-looking strings to ints, so a `heap_trim_sanitize=="0"` test would
|
|
15
|
+
# silently compare int 0 against str "0" and always be false -- which
|
|
16
|
+
# inverts the condition and ships _FORTIFY_SOURCE=0 in the release build.
|
|
17
|
+
"heap_trim_sanitize%": "<!(node -p \"process.env.HEAP_TRIM_SANITIZE ? 'on' : 'off'\")",
|
|
18
|
+
|
|
19
|
+
# Absolute path to node-addon-api's headers, for -isystem.
|
|
20
|
+
#
|
|
21
|
+
# node-addon-api is also in include_dirs (-I), but -Wformat=2 makes clang
|
|
22
|
+
# emit -Wformat-nonliteral inside napi-inl.h -- a third-party header we
|
|
23
|
+
# cannot fix. Passing the same directory with -isystem marks it a SYSTEM
|
|
24
|
+
# include, which suppresses its warnings while keeping the full warning set
|
|
25
|
+
# on our own code. (GCC/Clang: when a directory is given with both -I and
|
|
26
|
+
# -isystem, the -I is ignored and the directory is treated as a system one.)
|
|
27
|
+
#
|
|
28
|
+
# `.include_dir` is RELATIVE; the build runs from build/, so it must be
|
|
29
|
+
# resolved to an absolute path here.
|
|
30
|
+
"napi_include_dir": "<!(node -p \"require('path').resolve(require('node-addon-api').include_dir)\")"
|
|
31
|
+
},
|
|
32
|
+
"targets": [
|
|
33
|
+
{
|
|
34
|
+
"target_name": "heap_trim",
|
|
35
|
+
"sources": [
|
|
36
|
+
"src/binding.cpp"
|
|
37
|
+
],
|
|
38
|
+
"include_dirs": [
|
|
39
|
+
"<!@(node -p \"require('node-addon-api').include\")",
|
|
40
|
+
"src"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": [
|
|
43
|
+
"<!(node -p \"require('node-addon-api').gyp\")"
|
|
44
|
+
],
|
|
45
|
+
"defines": [
|
|
46
|
+
"NAPI_CPP_EXCEPTIONS",
|
|
47
|
+
"NAPI_VERSION=9"
|
|
48
|
+
],
|
|
49
|
+
"conditions": [
|
|
50
|
+
[
|
|
51
|
+
"OS=='linux'",
|
|
52
|
+
{
|
|
53
|
+
# OpenSSF Compiler Options Hardening Guide baseline.
|
|
54
|
+
#
|
|
55
|
+
# Every flag below was verified against the OLDEST toolchain we ship
|
|
56
|
+
# from -- Debian 11 Bullseye / GCC 10.2 (see prebuild-linux-glibc.sh)
|
|
57
|
+
# -- on both x64 and arm64, and against Alpine/musl (GCC 14).
|
|
58
|
+
#
|
|
59
|
+
# NOTE: gyp's `cflags` are passed to BOTH C and C++ compiles
|
|
60
|
+
# (GYP_CXXFLAGS = ... $(CFLAGS_$(BUILDTYPE)) $(CFLAGS_CC_$(BUILDTYPE))),
|
|
61
|
+
# so these must NOT be duplicated into cflags_cc.
|
|
62
|
+
"cflags": [
|
|
63
|
+
"-fPIC",
|
|
64
|
+
"-Wall",
|
|
65
|
+
"-Wextra",
|
|
66
|
+
# -Wformat=2 implies -Wformat. This pairing is REQUIRED:
|
|
67
|
+
# -Werror=format-security on its own hard-errors with
|
|
68
|
+
# "-Wformat-security ignored without -Wformat".
|
|
69
|
+
"-Wformat=2",
|
|
70
|
+
"-Werror=format-security",
|
|
71
|
+
"-fstack-protector-strong",
|
|
72
|
+
"-fstack-clash-protection",
|
|
73
|
+
"-fvisibility=hidden",
|
|
74
|
+
# Silence -Wformat-nonliteral from node-addon-api's napi-inl.h
|
|
75
|
+
# without weakening -Wformat=2 on our own code. See the
|
|
76
|
+
# napi_include_dir variable above.
|
|
77
|
+
"-isystem<(napi_include_dir)"
|
|
78
|
+
],
|
|
79
|
+
"cflags_cc": [
|
|
80
|
+
# Pin the project standard instead of inheriting a Node-major-
|
|
81
|
+
# dependent value from common.gypi.
|
|
82
|
+
"-std=gnu++20",
|
|
83
|
+
"-fexceptions",
|
|
84
|
+
"-fvisibility-inlines-hidden",
|
|
85
|
+
# libstdc++ bounds/precondition assertions. Independent of FORTIFY;
|
|
86
|
+
# safe to leave on under sanitizers.
|
|
87
|
+
"-D_GLIBCXX_ASSERTIONS"
|
|
88
|
+
],
|
|
89
|
+
# ELF/GNU-ld only. These are deliberately absent from the macOS
|
|
90
|
+
# branch: Apple's ld64 rejects -z outright ("ld: unknown options: -z").
|
|
91
|
+
# -Wl,-z,nodlopen must NEVER be added: Node dlopen()s this .node.
|
|
92
|
+
"ldflags": [
|
|
93
|
+
"-Wl,-z,relro",
|
|
94
|
+
"-Wl,-z,now",
|
|
95
|
+
"-Wl,-z,noexecstack"
|
|
96
|
+
],
|
|
97
|
+
"conditions": [
|
|
98
|
+
[
|
|
99
|
+
"heap_trim_sanitize=='off'",
|
|
100
|
+
{
|
|
101
|
+
# _FORTIFY_SOURCE requires -O1+ (Release gives -O3).
|
|
102
|
+
#
|
|
103
|
+
# Level 3 is deliberately NOT used: it needs GCC 12 +
|
|
104
|
+
# glibc 2.34 for __builtin_dynamic_object_size. On our
|
|
105
|
+
# GCC 10.2 / glibc 2.31 floor it silently degrades to level 2
|
|
106
|
+
# (verified: __USE_FORTIFY_LEVEL=2), so =2 is the honest value.
|
|
107
|
+
# Revisit if the glibc prebuild floor moves past Bullseye.
|
|
108
|
+
#
|
|
109
|
+
# -U first: many distros predefine _FORTIFY_SOURCE, which would
|
|
110
|
+
# otherwise emit a macro-redefinition warning.
|
|
111
|
+
"cflags": [
|
|
112
|
+
"-U_FORTIFY_SOURCE",
|
|
113
|
+
"-D_FORTIFY_SOURCE=2"
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
# Sanitizer build: FORTIFY must be OFF.
|
|
118
|
+
"cflags": [
|
|
119
|
+
"-U_FORTIFY_SOURCE",
|
|
120
|
+
"-D_FORTIFY_SOURCE=0"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
"target_arch=='x64'",
|
|
126
|
+
{
|
|
127
|
+
# Intel CET. x86-only: hard-errors on aarch64
|
|
128
|
+
# ("'-fcf-protection=full' is not supported for this target").
|
|
129
|
+
"cflags": ["-fcf-protection=full"]
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
"target_arch=='arm64'",
|
|
134
|
+
{
|
|
135
|
+
# AArch64 PAC/BTI. arm64-only: unknown option on x86.
|
|
136
|
+
"cflags": ["-mbranch-protection=standard"]
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
"OS=='win'",
|
|
144
|
+
{
|
|
145
|
+
"defines": [
|
|
146
|
+
"WIN32",
|
|
147
|
+
"_WINDOWS"
|
|
148
|
+
],
|
|
149
|
+
"msvs_settings": {
|
|
150
|
+
"VCCLCompilerTool": {
|
|
151
|
+
"WarningLevel": 4,
|
|
152
|
+
"AdditionalOptions": [
|
|
153
|
+
"/std:c++20", # Pin independently of Node's common.gypi
|
|
154
|
+
"/guard:cf", # Control Flow Guard (forward-edge CFI), all arches
|
|
155
|
+
"/Qspectre", # Spectre v1. NOT x64-only: MSVC supports ARM64
|
|
156
|
+
# since VS 2017 15.7 (Microsoft Learn /Qspectre).
|
|
157
|
+
"/ZH:SHA_256", # SHA-256 source hashing in the PDB
|
|
158
|
+
"/sdl" # SDL checks (superset of /GS)
|
|
159
|
+
],
|
|
160
|
+
"ExceptionHandling": 1,
|
|
161
|
+
"RuntimeTypeInfo": "true"
|
|
162
|
+
},
|
|
163
|
+
"VCLinkerTool": {
|
|
164
|
+
"AdditionalOptions": [
|
|
165
|
+
"/guard:cf", # Must pair with the compiler /guard:cf, or we
|
|
166
|
+
# pay the cost with none of the protection.
|
|
167
|
+
"/DYNAMICBASE", # ASLR (default on)
|
|
168
|
+
"/HIGHENTROPYVA", # 64-bit ASLR. NOT x64-only: applies to any
|
|
169
|
+
# 64-bit image incl. ARM64 (default on).
|
|
170
|
+
"/NXCOMPAT" # DEP (default on)
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"conditions": [
|
|
175
|
+
[
|
|
176
|
+
"target_arch=='x64'",
|
|
177
|
+
{
|
|
178
|
+
"defines": [
|
|
179
|
+
"_M_X64",
|
|
180
|
+
"_WIN64"
|
|
181
|
+
],
|
|
182
|
+
"msvs_settings": {
|
|
183
|
+
"VCLinkerTool": {
|
|
184
|
+
# CET shadow stack (backward-edge CFI). Genuinely x64-only:
|
|
185
|
+
# ARM64 uses signed returns below instead.
|
|
186
|
+
"AdditionalOptions": ["/CETCOMPAT"]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
[
|
|
192
|
+
"target_arch=='arm64'",
|
|
193
|
+
{
|
|
194
|
+
# /guard:cf, /Qspectre, /sdl, /ZH:SHA_256, /DYNAMICBASE,
|
|
195
|
+
# /HIGHENTROPYVA and /NXCOMPAT all apply here and are set above.
|
|
196
|
+
"defines": [
|
|
197
|
+
"_M_ARM64",
|
|
198
|
+
"_WIN64"
|
|
199
|
+
],
|
|
200
|
+
"msvs_settings": {
|
|
201
|
+
"VCCLCompilerTool": {
|
|
202
|
+
# ARM64 signed returns (PAC-based backward-edge CFI).
|
|
203
|
+
# /CETCOMPAT is x64-only and intentionally absent here.
|
|
204
|
+
"AdditionalOptions": ["/guard:signret"]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
[
|
|
213
|
+
"OS=='mac'",
|
|
214
|
+
{
|
|
215
|
+
# IMPORTANT: on macOS, gyp's make generator takes compile flags ONLY
|
|
216
|
+
# from xcode_settings -- target-level `cflags`/`cflags_cc` are
|
|
217
|
+
# ignored outright (see gyp/generator/make.py: `if flavor == "mac"`).
|
|
218
|
+
# Put every macOS flag in xcode_settings or it does nothing.
|
|
219
|
+
"xcode_settings": {
|
|
220
|
+
# Match the C++20 pinned above on Linux (-std=gnu++20) and
|
|
221
|
+
# Windows (/std:c++20). Kept STRICT
|
|
222
|
+
# (c++20, not gnu++20) on purpose: Linux is the permissive one, so
|
|
223
|
+
# a GNU extension that slips into the code fails here first.
|
|
224
|
+
#
|
|
225
|
+
# Verified against MACOSX_DEPLOYMENT_TARGET 10.15: libc++ marks a
|
|
226
|
+
# few C++20 library features unavailable on older deployment
|
|
227
|
+
# targets, but none that this addon uses.
|
|
228
|
+
"CLANG_CXX_LANGUAGE_STANDARD": "c++20",
|
|
229
|
+
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
|
230
|
+
"CLANG_CXX_LIBRARY": "libc++",
|
|
231
|
+
"MACOSX_DEPLOYMENT_TARGET": "10.15",
|
|
232
|
+
# == -fvisibility=hidden / -fvisibility-inlines-hidden
|
|
233
|
+
"GCC_SYMBOLS_PRIVATE_EXTERN": "YES",
|
|
234
|
+
"GCC_INLINES_ARE_PRIVATE_EXTERN": "YES",
|
|
235
|
+
"OTHER_CFLAGS": [
|
|
236
|
+
"-Wall",
|
|
237
|
+
"-Wextra",
|
|
238
|
+
# Same pairing requirement as Linux: -Werror=format-security
|
|
239
|
+
# alone hard-errors without -Wformat (supplied by -Wformat=2).
|
|
240
|
+
"-Wformat=2",
|
|
241
|
+
"-Werror=format-security",
|
|
242
|
+
"-fstack-protector-strong",
|
|
243
|
+
# Silence -Wformat-nonliteral from node-addon-api's napi-inl.h
|
|
244
|
+
# without weakening -Wformat=2 on our own code.
|
|
245
|
+
"-isystem<(napi_include_dir)"
|
|
246
|
+
# No -Wl,-z,* here: ld64 rejects them.
|
|
247
|
+
# No -fstack-clash-protection here: accepted by current Apple
|
|
248
|
+
# clang but NOT verified on the older clang shipped by the
|
|
249
|
+
# macos-14/macos-15 CI images, where it would hard-error.
|
|
250
|
+
],
|
|
251
|
+
"OTHER_CPLUSPLUSFLAGS": [
|
|
252
|
+
# "$(inherited)" is LOAD-BEARING. gyp defaults
|
|
253
|
+
# OTHER_CPLUSPLUSFLAGS to ["$(inherited)"], which is what pulls
|
|
254
|
+
# OTHER_CFLAGS into C++ compiles. Setting this key without
|
|
255
|
+
# $(inherited) would silently drop every OTHER_CFLAGS hardening
|
|
256
|
+
# flag above from the C++ TUs -- i.e. all of our code.
|
|
257
|
+
"$(inherited)",
|
|
258
|
+
# libc++ bounds/precondition assertions (libc++ 18+). Verified
|
|
259
|
+
# effective on Apple clang (traps out-of-bounds access). A no-op
|
|
260
|
+
# on older libc++ that does not know the macro, never an error.
|
|
261
|
+
"-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST"
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
"conditions": [
|
|
265
|
+
[
|
|
266
|
+
"heap_trim_sanitize=='off'",
|
|
267
|
+
{
|
|
268
|
+
"xcode_settings": {
|
|
269
|
+
"OTHER_CFLAGS": [
|
|
270
|
+
"-U_FORTIFY_SOURCE",
|
|
271
|
+
"-D_FORTIFY_SOURCE=2"
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
# Sanitizer build: FORTIFY must be OFF (see top-of-file note).
|
|
277
|
+
"xcode_settings": {
|
|
278
|
+
"OTHER_CFLAGS": [
|
|
279
|
+
"-U_FORTIFY_SOURCE",
|
|
280
|
+
"-D_FORTIFY_SOURCE=0"
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
default: () => index_default,
|
|
34
|
+
heapTrim: () => heapTrim
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var import_node_gyp_build = __toESM(require("node-gyp-build"));
|
|
38
|
+
var import_node_path2 = require("path");
|
|
39
|
+
|
|
40
|
+
// src/stack-path.ts
|
|
41
|
+
var import_node_path = require("path");
|
|
42
|
+
function getCallerDirname() {
|
|
43
|
+
const e = new Error();
|
|
44
|
+
if (e.stack == null) {
|
|
45
|
+
Error.captureStackTrace(e);
|
|
46
|
+
}
|
|
47
|
+
return (0, import_node_path.dirname)(extractCallerPath(e.stack));
|
|
48
|
+
}
|
|
49
|
+
var patterns = process.platform === "win32" ? [
|
|
50
|
+
// File URLs: "at functionName (file:///C:/path/file.js:1:1)"
|
|
51
|
+
/\bat\s.+?\((?<path>file:\/\/\/.+?):\d+:\d+\)$/,
|
|
52
|
+
// File URLs direct: "at file:///C:/path/file.js:1:1"
|
|
53
|
+
/\bat\s(?<path>file:\/\/\/.+?):\d+:\d+$/,
|
|
54
|
+
// Standard: "at functionName (C:\path\file.js:1:1)"
|
|
55
|
+
/\bat\s.+?\((?<path>[A-Z]:\\.+):\d+:\d+\)$/,
|
|
56
|
+
// direct: "at C:\path\file.js:1:1"
|
|
57
|
+
/\bat\s(?<path>[A-Z]:\\.+):\d+:\d+$/,
|
|
58
|
+
// UNC: "at functionName (\\server\share\path\file.js:1:1)"
|
|
59
|
+
/\bat\s.+?\((?<path>\\\\.+):\d+:\d+\)$/,
|
|
60
|
+
// direct: "at \\server\share\path\file.js:1:1"
|
|
61
|
+
/\bat\s(?<path>\\\\.+):\d+:\d+$/
|
|
62
|
+
] : [
|
|
63
|
+
// Standard: "at functionName (/path/file.js:1:1)"
|
|
64
|
+
/\bat\s.+?\((?<path>\/.+?):\d+:\d+\)$/,
|
|
65
|
+
// Anonymous or direct: "at /path/file.js:1:1"
|
|
66
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- Pattern is safe: no nested quantifiers
|
|
67
|
+
/\bat\s(?:[^\s()]+\s)?(?<path>\/[^:]+):\d+:\d+$/
|
|
68
|
+
];
|
|
69
|
+
var MaybeUrlRE = /^[a-z]{2,5}:\/\//i;
|
|
70
|
+
function extractCallerPath(stack) {
|
|
71
|
+
const frames = stack.split("\n").filter(Boolean);
|
|
72
|
+
const callerFrame = frames.findIndex(
|
|
73
|
+
(frame) => frame.includes("getCallerDirname")
|
|
74
|
+
);
|
|
75
|
+
if (callerFrame === -1) {
|
|
76
|
+
throw new Error("Invalid stack trace format: missing caller frame");
|
|
77
|
+
}
|
|
78
|
+
for (let i = callerFrame + 1; i < frames.length; i++) {
|
|
79
|
+
const frame = frames[i];
|
|
80
|
+
for (const pattern of patterns) {
|
|
81
|
+
const g = frame?.trim().match(pattern)?.groups;
|
|
82
|
+
if (g != null && g["path"]) {
|
|
83
|
+
const path = g["path"];
|
|
84
|
+
if (MaybeUrlRE.test(path)) {
|
|
85
|
+
try {
|
|
86
|
+
return new URL(path).pathname;
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return path;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw new Error("Invalid stack trace format: no parsable frames");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/dirname.ts
|
|
98
|
+
function _dirname() {
|
|
99
|
+
try {
|
|
100
|
+
if (typeof __dirname !== "undefined") return __dirname;
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
return getCallerDirname();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/index.ts
|
|
107
|
+
var isGlibc = process.report.getReport().header.glibcVersionRuntime != null;
|
|
108
|
+
var binding = isGlibc ? (0, import_node_gyp_build.default)((0, import_node_path2.join)(_dirname(), "..")) : { heapTrim: () => false };
|
|
109
|
+
var heapTrim = binding.heapTrim;
|
|
110
|
+
var index_default = binding;
|
|
111
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
112
|
+
0 && (module.exports = {
|
|
113
|
+
heapTrim
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/stack-path.ts","../src/dirname.ts"],"sourcesContent":["import nodeGypBuild from \"node-gyp-build\";\nimport { join } from \"node:path\";\nimport { _dirname } from \"./dirname\";\n\n/** The functions the native addon exports. */\nexport interface HeapTrimModule {\n /** @see {@link heapTrim} */\n heapTrim(): boolean;\n}\n\n// The addon is built and loaded only on glibc, because only glibc's trim call\n// released memory. Each of these measurements allocated 128 MiB as Buffer\n// backing stores, kept only the last one, collected the rest, then trimmed:\n//\n// - macOS: malloc_zone_pressure_relief(nullptr, 0) returned 0 bytes. On m1\n// (macOS 27.2, arm64, Node 24.22.1) with 64 KiB buffers, the physical\n// footprint went from 14 MB to 144 MB, stayed at 145 MB after the frees\n// (vmmap: 124 MB of \"Malloc Small (empty)\" regions), and stayed at 145 MB\n// after the call. With 1, 4, 16, 32, 128, 256 and 1024 KiB buffers it also\n// returned 0 and the footprint did not change.\n// - Windows: _heapmin() returned 0 (success), which does not say whether it\n// released anything. On swift (Windows 11, x64, Node 24.21.0) with 64 KiB\n// buffers, the heap had already decommitted the freed pages: private bytes\n// went from 20 MB to 151 MB and fell to 23 MB after the frees, and the call\n// changed them by 0.1 MB. With 1 KiB buffers, 84 MB of private bytes stayed\n// committed after the frees, and the call left 84 MB.\n// - musl has no trim call.\nconst isGlibc =\n (process.report.getReport() as { header: { glibcVersionRuntime?: string } })\n .header.glibcVersionRuntime != null;\n\n// Use _dirname() helper that works in both CJS/ESM and Jest\nconst binding: HeapTrimModule = isGlibc\n ? (nodeGypBuild(join(_dirname(), \"..\")) as HeapTrimModule)\n : { heapTrim: () => false };\n\n/**\n * Returns free C heap memory to the operating system.\n *\n * glibc's `free()` shrinks the heap only from its top, so pages freed below a\n * live allocation stay in the process's RSS. On glibc Linux this calls\n * `malloc_trim(0)`, which releases those free pages in every malloc arena. On\n * musl, macOS, and Windows it does nothing: musl has no trim call, and\n * macOS's `malloc_zone_pressure_relief()` and Windows' `_heapmin()` released\n * no measurable memory in this package's test, so it doesn't call them.\n *\n * It locks each arena while it walks it, so call it when the process is idle,\n * not after every request.\n *\n * @returns `true` if glibc found whole free pages and told the kernel to drop\n * them. That doesn't mean RSS fell: glibc doesn't check whether those pages\n * were still resident, so repeated calls keep returning `true`. `false` if it\n * found none, or the process doesn't use glibc.\n *\n * @example\n * ```typescript\n * import { heapTrim } from \"@photostructure/heap-trim\";\n *\n * heapTrim();\n * ```\n */\nexport const heapTrim: () => boolean = binding.heapTrim;\n\n// Default export for CommonJS compatibility\nexport default binding;\n","import { dirname } from \"node:path\";\n\nexport function getCallerDirname(): string {\n const e = new Error();\n if (e.stack == null) {\n Error.captureStackTrace(e);\n }\n return dirname(extractCallerPath(e.stack as string));\n}\n\n// Comprehensive regex patterns for different stack frame formats\nconst patterns =\n process.platform === \"win32\"\n ? [\n // File URLs: \"at functionName (file:///C:/path/file.js:1:1)\"\n /\\bat\\s.+?\\((?<path>file:\\/\\/\\/.+?):\\d+:\\d+\\)$/,\n // File URLs direct: \"at file:///C:/path/file.js:1:1\"\n /\\bat\\s(?<path>file:\\/\\/\\/.+?):\\d+:\\d+$/,\n // Standard: \"at functionName (C:\\path\\file.js:1:1)\"\n /\\bat\\s.+?\\((?<path>[A-Z]:\\\\.+):\\d+:\\d+\\)$/,\n // direct: \"at C:\\path\\file.js:1:1\"\n /\\bat\\s(?<path>[A-Z]:\\\\.+):\\d+:\\d+$/,\n // UNC: \"at functionName (\\\\server\\share\\path\\file.js:1:1)\"\n /\\bat\\s.+?\\((?<path>\\\\\\\\.+):\\d+:\\d+\\)$/,\n // direct: \"at \\\\server\\share\\path\\file.js:1:1\"\n /\\bat\\s(?<path>\\\\\\\\.+):\\d+:\\d+$/,\n ]\n : [\n // Standard: \"at functionName (/path/file.js:1:1)\"\n /\\bat\\s.+?\\((?<path>\\/.+?):\\d+:\\d+\\)$/,\n // Anonymous or direct: \"at /path/file.js:1:1\"\n // eslint-disable-next-line security/detect-unsafe-regex -- Pattern is safe: no nested quantifiers\n /\\bat\\s(?:[^\\s()]+\\s)?(?<path>\\/[^:]+):\\d+:\\d+$/,\n ];\n\nconst MaybeUrlRE = /^[a-z]{2,5}:\\/\\//i;\n\n// only exposed for tests:\nexport function extractCallerPath(stack: string): string {\n const frames = stack.split(\"\\n\").filter(Boolean);\n\n // First find getCallerDirname() in the stack:\n const callerFrame = frames.findIndex((frame) =>\n frame.includes(\"getCallerDirname\"),\n );\n if (callerFrame === -1) {\n throw new Error(\"Invalid stack trace format: missing caller frame\");\n }\n for (let i = callerFrame + 1; i < frames.length; i++) {\n const frame = frames[i];\n for (const pattern of patterns) {\n const g = frame?.trim().match(pattern)?.groups;\n if (g != null && g[\"path\"]) {\n const path = g[\"path\"];\n // Windows requires us to check if it's a reasonable URL, as URL accepts\n // \"C:\\\\path\\\\file.txt\" as valid (!!)\n if (MaybeUrlRE.test(path)) {\n try {\n return new URL(path).pathname;\n } catch {\n // ignore\n }\n }\n return path;\n }\n }\n }\n throw new Error(\"Invalid stack trace format: no parsable frames\");\n}\n","import { getCallerDirname } from \"./stack-path\";\n\n// Thanks to tsup shims, __dirname should always be defined except when run by\n// jest (which will use the stack_path shim)\nexport function _dirname() {\n try {\n if (typeof __dirname !== \"undefined\") return __dirname;\n } catch {\n // ignore\n }\n // we must be in jest. Use the stack_path ~~hack~~ shim:\n return getCallerDirname();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAAyB;AACzB,IAAAA,oBAAqB;;;ACDrB,uBAAwB;AAEjB,SAAS,mBAA2B;AACzC,QAAM,IAAI,IAAI,MAAM;AACpB,MAAI,EAAE,SAAS,MAAM;AACnB,UAAM,kBAAkB,CAAC;AAAA,EAC3B;AACA,aAAO,0BAAQ,kBAAkB,EAAE,KAAe,CAAC;AACrD;AAGA,IAAM,WACJ,QAAQ,aAAa,UACjB;AAAA;AAAA,EAEE;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AACF,IACA;AAAA;AAAA,EAEE;AAAA;AAAA;AAAA,EAGA;AACF;AAEN,IAAM,aAAa;AAGZ,SAAS,kBAAkB,OAAuB;AACvD,QAAM,SAAS,MAAM,MAAM,IAAI,EAAE,OAAO,OAAO;AAG/C,QAAM,cAAc,OAAO;AAAA,IAAU,CAAC,UACpC,MAAM,SAAS,kBAAkB;AAAA,EACnC;AACA,MAAI,gBAAgB,IAAI;AACtB,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,WAAS,IAAI,cAAc,GAAG,IAAI,OAAO,QAAQ,KAAK;AACpD,UAAM,QAAQ,OAAO,CAAC;AACtB,eAAW,WAAW,UAAU;AAC9B,YAAM,IAAI,OAAO,KAAK,EAAE,MAAM,OAAO,GAAG;AACxC,UAAI,KAAK,QAAQ,EAAE,MAAM,GAAG;AAC1B,cAAM,OAAO,EAAE,MAAM;AAGrB,YAAI,WAAW,KAAK,IAAI,GAAG;AACzB,cAAI;AACF,mBAAO,IAAI,IAAI,IAAI,EAAE;AAAA,UACvB,QAAQ;AAAA,UAER;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACA,QAAM,IAAI,MAAM,gDAAgD;AAClE;;;AChEO,SAAS,WAAW;AACzB,MAAI;AACF,QAAI,OAAO,cAAc,YAAa,QAAO;AAAA,EAC/C,QAAQ;AAAA,EAER;AAEA,SAAO,iBAAiB;AAC1B;;;AFeA,IAAM,UACH,QAAQ,OAAO,UAAU,EACvB,OAAO,uBAAuB;AAGnC,IAAM,UAA0B,cAC3B,sBAAAC,aAAa,wBAAK,SAAS,GAAG,IAAI,CAAC,IACpC,EAAE,UAAU,MAAM,MAAM;AA2BrB,IAAM,WAA0B,QAAQ;AAG/C,IAAO,gBAAQ;","names":["import_node_path","nodeGypBuild"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The functions the native addon exports. */
|
|
2
|
+
interface HeapTrimModule {
|
|
3
|
+
/** @see {@link heapTrim} */
|
|
4
|
+
heapTrim(): boolean;
|
|
5
|
+
}
|
|
6
|
+
declare const binding: HeapTrimModule;
|
|
7
|
+
/**
|
|
8
|
+
* Returns free C heap memory to the operating system.
|
|
9
|
+
*
|
|
10
|
+
* glibc's `free()` shrinks the heap only from its top, so pages freed below a
|
|
11
|
+
* live allocation stay in the process's RSS. On glibc Linux this calls
|
|
12
|
+
* `malloc_trim(0)`, which releases those free pages in every malloc arena. On
|
|
13
|
+
* musl, macOS, and Windows it does nothing: musl has no trim call, and
|
|
14
|
+
* macOS's `malloc_zone_pressure_relief()` and Windows' `_heapmin()` released
|
|
15
|
+
* no measurable memory in this package's test, so it doesn't call them.
|
|
16
|
+
*
|
|
17
|
+
* It locks each arena while it walks it, so call it when the process is idle,
|
|
18
|
+
* not after every request.
|
|
19
|
+
*
|
|
20
|
+
* @returns `true` if glibc found whole free pages and told the kernel to drop
|
|
21
|
+
* them. That doesn't mean RSS fell: glibc doesn't check whether those pages
|
|
22
|
+
* were still resident, so repeated calls keep returning `true`. `false` if it
|
|
23
|
+
* found none, or the process doesn't use glibc.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { heapTrim } from "@photostructure/heap-trim";
|
|
28
|
+
*
|
|
29
|
+
* heapTrim();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare const heapTrim: () => boolean;
|
|
33
|
+
|
|
34
|
+
export { binding as default, heapTrim };
|
|
35
|
+
export type { HeapTrimModule };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The functions the native addon exports. */
|
|
2
|
+
interface HeapTrimModule {
|
|
3
|
+
/** @see {@link heapTrim} */
|
|
4
|
+
heapTrim(): boolean;
|
|
5
|
+
}
|
|
6
|
+
declare const binding: HeapTrimModule;
|
|
7
|
+
/**
|
|
8
|
+
* Returns free C heap memory to the operating system.
|
|
9
|
+
*
|
|
10
|
+
* glibc's `free()` shrinks the heap only from its top, so pages freed below a
|
|
11
|
+
* live allocation stay in the process's RSS. On glibc Linux this calls
|
|
12
|
+
* `malloc_trim(0)`, which releases those free pages in every malloc arena. On
|
|
13
|
+
* musl, macOS, and Windows it does nothing: musl has no trim call, and
|
|
14
|
+
* macOS's `malloc_zone_pressure_relief()` and Windows' `_heapmin()` released
|
|
15
|
+
* no measurable memory in this package's test, so it doesn't call them.
|
|
16
|
+
*
|
|
17
|
+
* It locks each arena while it walks it, so call it when the process is idle,
|
|
18
|
+
* not after every request.
|
|
19
|
+
*
|
|
20
|
+
* @returns `true` if glibc found whole free pages and told the kernel to drop
|
|
21
|
+
* them. That doesn't mean RSS fell: glibc doesn't check whether those pages
|
|
22
|
+
* were still resident, so repeated calls keep returning `true`. `false` if it
|
|
23
|
+
* found none, or the process doesn't use glibc.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { heapTrim } from "@photostructure/heap-trim";
|
|
28
|
+
*
|
|
29
|
+
* heapTrim();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare const heapTrim: () => boolean;
|
|
33
|
+
|
|
34
|
+
export { binding as default, heapTrim };
|
|
35
|
+
export type { HeapTrimModule };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The functions the native addon exports. */
|
|
2
|
+
interface HeapTrimModule {
|
|
3
|
+
/** @see {@link heapTrim} */
|
|
4
|
+
heapTrim(): boolean;
|
|
5
|
+
}
|
|
6
|
+
declare const binding: HeapTrimModule;
|
|
7
|
+
/**
|
|
8
|
+
* Returns free C heap memory to the operating system.
|
|
9
|
+
*
|
|
10
|
+
* glibc's `free()` shrinks the heap only from its top, so pages freed below a
|
|
11
|
+
* live allocation stay in the process's RSS. On glibc Linux this calls
|
|
12
|
+
* `malloc_trim(0)`, which releases those free pages in every malloc arena. On
|
|
13
|
+
* musl, macOS, and Windows it does nothing: musl has no trim call, and
|
|
14
|
+
* macOS's `malloc_zone_pressure_relief()` and Windows' `_heapmin()` released
|
|
15
|
+
* no measurable memory in this package's test, so it doesn't call them.
|
|
16
|
+
*
|
|
17
|
+
* It locks each arena while it walks it, so call it when the process is idle,
|
|
18
|
+
* not after every request.
|
|
19
|
+
*
|
|
20
|
+
* @returns `true` if glibc found whole free pages and told the kernel to drop
|
|
21
|
+
* them. That doesn't mean RSS fell: glibc doesn't check whether those pages
|
|
22
|
+
* were still resident, so repeated calls keep returning `true`. `false` if it
|
|
23
|
+
* found none, or the process doesn't use glibc.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { heapTrim } from "@photostructure/heap-trim";
|
|
28
|
+
*
|
|
29
|
+
* heapTrim();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare const heapTrim: () => boolean;
|
|
33
|
+
|
|
34
|
+
export { binding as default, heapTrim };
|
|
35
|
+
export type { HeapTrimModule };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
5
|
+
var getDirname = () => path.dirname(getFilename());
|
|
6
|
+
var __dirname = /* @__PURE__ */ getDirname();
|
|
7
|
+
|
|
8
|
+
// src/index.ts
|
|
9
|
+
import nodeGypBuild from "node-gyp-build";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
|
|
12
|
+
// src/stack-path.ts
|
|
13
|
+
import { dirname } from "path";
|
|
14
|
+
function getCallerDirname() {
|
|
15
|
+
const e = new Error();
|
|
16
|
+
if (e.stack == null) {
|
|
17
|
+
Error.captureStackTrace(e);
|
|
18
|
+
}
|
|
19
|
+
return dirname(extractCallerPath(e.stack));
|
|
20
|
+
}
|
|
21
|
+
var patterns = process.platform === "win32" ? [
|
|
22
|
+
// File URLs: "at functionName (file:///C:/path/file.js:1:1)"
|
|
23
|
+
/\bat\s.+?\((?<path>file:\/\/\/.+?):\d+:\d+\)$/,
|
|
24
|
+
// File URLs direct: "at file:///C:/path/file.js:1:1"
|
|
25
|
+
/\bat\s(?<path>file:\/\/\/.+?):\d+:\d+$/,
|
|
26
|
+
// Standard: "at functionName (C:\path\file.js:1:1)"
|
|
27
|
+
/\bat\s.+?\((?<path>[A-Z]:\\.+):\d+:\d+\)$/,
|
|
28
|
+
// direct: "at C:\path\file.js:1:1"
|
|
29
|
+
/\bat\s(?<path>[A-Z]:\\.+):\d+:\d+$/,
|
|
30
|
+
// UNC: "at functionName (\\server\share\path\file.js:1:1)"
|
|
31
|
+
/\bat\s.+?\((?<path>\\\\.+):\d+:\d+\)$/,
|
|
32
|
+
// direct: "at \\server\share\path\file.js:1:1"
|
|
33
|
+
/\bat\s(?<path>\\\\.+):\d+:\d+$/
|
|
34
|
+
] : [
|
|
35
|
+
// Standard: "at functionName (/path/file.js:1:1)"
|
|
36
|
+
/\bat\s.+?\((?<path>\/.+?):\d+:\d+\)$/,
|
|
37
|
+
// Anonymous or direct: "at /path/file.js:1:1"
|
|
38
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- Pattern is safe: no nested quantifiers
|
|
39
|
+
/\bat\s(?:[^\s()]+\s)?(?<path>\/[^:]+):\d+:\d+$/
|
|
40
|
+
];
|
|
41
|
+
var MaybeUrlRE = /^[a-z]{2,5}:\/\//i;
|
|
42
|
+
function extractCallerPath(stack) {
|
|
43
|
+
const frames = stack.split("\n").filter(Boolean);
|
|
44
|
+
const callerFrame = frames.findIndex(
|
|
45
|
+
(frame) => frame.includes("getCallerDirname")
|
|
46
|
+
);
|
|
47
|
+
if (callerFrame === -1) {
|
|
48
|
+
throw new Error("Invalid stack trace format: missing caller frame");
|
|
49
|
+
}
|
|
50
|
+
for (let i = callerFrame + 1; i < frames.length; i++) {
|
|
51
|
+
const frame = frames[i];
|
|
52
|
+
for (const pattern of patterns) {
|
|
53
|
+
const g = frame?.trim().match(pattern)?.groups;
|
|
54
|
+
if (g != null && g["path"]) {
|
|
55
|
+
const path2 = g["path"];
|
|
56
|
+
if (MaybeUrlRE.test(path2)) {
|
|
57
|
+
try {
|
|
58
|
+
return new URL(path2).pathname;
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return path2;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw new Error("Invalid stack trace format: no parsable frames");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/dirname.ts
|
|
70
|
+
function _dirname() {
|
|
71
|
+
try {
|
|
72
|
+
if (typeof __dirname !== "undefined") return __dirname;
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
return getCallerDirname();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/index.ts
|
|
79
|
+
var isGlibc = process.report.getReport().header.glibcVersionRuntime != null;
|
|
80
|
+
var binding = isGlibc ? nodeGypBuild(join(_dirname(), "..")) : { heapTrim: () => false };
|
|
81
|
+
var heapTrim = binding.heapTrim;
|
|
82
|
+
var index_default = binding;
|
|
83
|
+
export {
|
|
84
|
+
index_default as default,
|
|
85
|
+
heapTrim
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../node_modules/tsup/assets/esm_shims.js","../src/index.ts","../src/stack-path.ts","../src/dirname.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","import nodeGypBuild from \"node-gyp-build\";\nimport { join } from \"node:path\";\nimport { _dirname } from \"./dirname\";\n\n/** The functions the native addon exports. */\nexport interface HeapTrimModule {\n /** @see {@link heapTrim} */\n heapTrim(): boolean;\n}\n\n// The addon is built and loaded only on glibc, because only glibc's trim call\n// released memory. Each of these measurements allocated 128 MiB as Buffer\n// backing stores, kept only the last one, collected the rest, then trimmed:\n//\n// - macOS: malloc_zone_pressure_relief(nullptr, 0) returned 0 bytes. On m1\n// (macOS 27.2, arm64, Node 24.22.1) with 64 KiB buffers, the physical\n// footprint went from 14 MB to 144 MB, stayed at 145 MB after the frees\n// (vmmap: 124 MB of \"Malloc Small (empty)\" regions), and stayed at 145 MB\n// after the call. With 1, 4, 16, 32, 128, 256 and 1024 KiB buffers it also\n// returned 0 and the footprint did not change.\n// - Windows: _heapmin() returned 0 (success), which does not say whether it\n// released anything. On swift (Windows 11, x64, Node 24.21.0) with 64 KiB\n// buffers, the heap had already decommitted the freed pages: private bytes\n// went from 20 MB to 151 MB and fell to 23 MB after the frees, and the call\n// changed them by 0.1 MB. With 1 KiB buffers, 84 MB of private bytes stayed\n// committed after the frees, and the call left 84 MB.\n// - musl has no trim call.\nconst isGlibc =\n (process.report.getReport() as { header: { glibcVersionRuntime?: string } })\n .header.glibcVersionRuntime != null;\n\n// Use _dirname() helper that works in both CJS/ESM and Jest\nconst binding: HeapTrimModule = isGlibc\n ? (nodeGypBuild(join(_dirname(), \"..\")) as HeapTrimModule)\n : { heapTrim: () => false };\n\n/**\n * Returns free C heap memory to the operating system.\n *\n * glibc's `free()` shrinks the heap only from its top, so pages freed below a\n * live allocation stay in the process's RSS. On glibc Linux this calls\n * `malloc_trim(0)`, which releases those free pages in every malloc arena. On\n * musl, macOS, and Windows it does nothing: musl has no trim call, and\n * macOS's `malloc_zone_pressure_relief()` and Windows' `_heapmin()` released\n * no measurable memory in this package's test, so it doesn't call them.\n *\n * It locks each arena while it walks it, so call it when the process is idle,\n * not after every request.\n *\n * @returns `true` if glibc found whole free pages and told the kernel to drop\n * them. That doesn't mean RSS fell: glibc doesn't check whether those pages\n * were still resident, so repeated calls keep returning `true`. `false` if it\n * found none, or the process doesn't use glibc.\n *\n * @example\n * ```typescript\n * import { heapTrim } from \"@photostructure/heap-trim\";\n *\n * heapTrim();\n * ```\n */\nexport const heapTrim: () => boolean = binding.heapTrim;\n\n// Default export for CommonJS compatibility\nexport default binding;\n","import { dirname } from \"node:path\";\n\nexport function getCallerDirname(): string {\n const e = new Error();\n if (e.stack == null) {\n Error.captureStackTrace(e);\n }\n return dirname(extractCallerPath(e.stack as string));\n}\n\n// Comprehensive regex patterns for different stack frame formats\nconst patterns =\n process.platform === \"win32\"\n ? [\n // File URLs: \"at functionName (file:///C:/path/file.js:1:1)\"\n /\\bat\\s.+?\\((?<path>file:\\/\\/\\/.+?):\\d+:\\d+\\)$/,\n // File URLs direct: \"at file:///C:/path/file.js:1:1\"\n /\\bat\\s(?<path>file:\\/\\/\\/.+?):\\d+:\\d+$/,\n // Standard: \"at functionName (C:\\path\\file.js:1:1)\"\n /\\bat\\s.+?\\((?<path>[A-Z]:\\\\.+):\\d+:\\d+\\)$/,\n // direct: \"at C:\\path\\file.js:1:1\"\n /\\bat\\s(?<path>[A-Z]:\\\\.+):\\d+:\\d+$/,\n // UNC: \"at functionName (\\\\server\\share\\path\\file.js:1:1)\"\n /\\bat\\s.+?\\((?<path>\\\\\\\\.+):\\d+:\\d+\\)$/,\n // direct: \"at \\\\server\\share\\path\\file.js:1:1\"\n /\\bat\\s(?<path>\\\\\\\\.+):\\d+:\\d+$/,\n ]\n : [\n // Standard: \"at functionName (/path/file.js:1:1)\"\n /\\bat\\s.+?\\((?<path>\\/.+?):\\d+:\\d+\\)$/,\n // Anonymous or direct: \"at /path/file.js:1:1\"\n // eslint-disable-next-line security/detect-unsafe-regex -- Pattern is safe: no nested quantifiers\n /\\bat\\s(?:[^\\s()]+\\s)?(?<path>\\/[^:]+):\\d+:\\d+$/,\n ];\n\nconst MaybeUrlRE = /^[a-z]{2,5}:\\/\\//i;\n\n// only exposed for tests:\nexport function extractCallerPath(stack: string): string {\n const frames = stack.split(\"\\n\").filter(Boolean);\n\n // First find getCallerDirname() in the stack:\n const callerFrame = frames.findIndex((frame) =>\n frame.includes(\"getCallerDirname\"),\n );\n if (callerFrame === -1) {\n throw new Error(\"Invalid stack trace format: missing caller frame\");\n }\n for (let i = callerFrame + 1; i < frames.length; i++) {\n const frame = frames[i];\n for (const pattern of patterns) {\n const g = frame?.trim().match(pattern)?.groups;\n if (g != null && g[\"path\"]) {\n const path = g[\"path\"];\n // Windows requires us to check if it's a reasonable URL, as URL accepts\n // \"C:\\\\path\\\\file.txt\" as valid (!!)\n if (MaybeUrlRE.test(path)) {\n try {\n return new URL(path).pathname;\n } catch {\n // ignore\n }\n }\n return path;\n }\n }\n }\n throw new Error(\"Invalid stack trace format: no parsable frames\");\n}\n","import { getCallerDirname } from \"./stack-path\";\n\n// Thanks to tsup shims, __dirname should always be defined except when run by\n// jest (which will use the stack_path shim)\nexport function _dirname() {\n try {\n if (typeof __dirname !== \"undefined\") return __dirname;\n } catch {\n // ignore\n }\n // we must be in jest. Use the stack_path ~~hack~~ shim:\n return getCallerDirname();\n}\n"],"mappings":";AACA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAE9B,IAAM,cAAc,MAAM,cAAc,YAAY,GAAG;AACvD,IAAM,aAAa,MAAM,KAAK,QAAQ,YAAY,CAAC;AAE5C,IAAM,YAA4B,2BAAW;;;ACPpD,OAAO,kBAAkB;AACzB,SAAS,YAAY;;;ACDrB,SAAS,eAAe;AAEjB,SAAS,mBAA2B;AACzC,QAAM,IAAI,IAAI,MAAM;AACpB,MAAI,EAAE,SAAS,MAAM;AACnB,UAAM,kBAAkB,CAAC;AAAA,EAC3B;AACA,SAAO,QAAQ,kBAAkB,EAAE,KAAe,CAAC;AACrD;AAGA,IAAM,WACJ,QAAQ,aAAa,UACjB;AAAA;AAAA,EAEE;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AACF,IACA;AAAA;AAAA,EAEE;AAAA;AAAA;AAAA,EAGA;AACF;AAEN,IAAM,aAAa;AAGZ,SAAS,kBAAkB,OAAuB;AACvD,QAAM,SAAS,MAAM,MAAM,IAAI,EAAE,OAAO,OAAO;AAG/C,QAAM,cAAc,OAAO;AAAA,IAAU,CAAC,UACpC,MAAM,SAAS,kBAAkB;AAAA,EACnC;AACA,MAAI,gBAAgB,IAAI;AACtB,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,WAAS,IAAI,cAAc,GAAG,IAAI,OAAO,QAAQ,KAAK;AACpD,UAAM,QAAQ,OAAO,CAAC;AACtB,eAAW,WAAW,UAAU;AAC9B,YAAM,IAAI,OAAO,KAAK,EAAE,MAAM,OAAO,GAAG;AACxC,UAAI,KAAK,QAAQ,EAAE,MAAM,GAAG;AAC1B,cAAMA,QAAO,EAAE,MAAM;AAGrB,YAAI,WAAW,KAAKA,KAAI,GAAG;AACzB,cAAI;AACF,mBAAO,IAAI,IAAIA,KAAI,EAAE;AAAA,UACvB,QAAQ;AAAA,UAER;AAAA,QACF;AACA,eAAOA;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACA,QAAM,IAAI,MAAM,gDAAgD;AAClE;;;AChEO,SAAS,WAAW;AACzB,MAAI;AACF,QAAI,OAAO,cAAc,YAAa,QAAO;AAAA,EAC/C,QAAQ;AAAA,EAER;AAEA,SAAO,iBAAiB;AAC1B;;;AFeA,IAAM,UACH,QAAQ,OAAO,UAAU,EACvB,OAAO,uBAAuB;AAGnC,IAAM,UAA0B,UAC3B,aAAa,KAAK,SAAS,GAAG,IAAI,CAAC,IACpC,EAAE,UAAU,MAAM,MAAM;AA2BrB,IAAM,WAA0B,QAAQ;AAG/C,IAAO,gBAAQ;","names":["path"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,141 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@photostructure/heap-trim",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Return free C heap pages to the operating system from Node.js",
|
|
5
|
+
"homepage": "https://photostructure.github.io/heap-trim/",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"require": {
|
|
12
|
+
"types": "./dist/index.d.cts",
|
|
13
|
+
"default": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"import": {
|
|
16
|
+
"types": "./dist/index.d.mts",
|
|
17
|
+
"default": "./dist/index.mjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"LICENSE.txt",
|
|
25
|
+
"README.md",
|
|
26
|
+
"binding.gyp",
|
|
27
|
+
"dist/",
|
|
28
|
+
"prebuilds/",
|
|
29
|
+
"scripts/install.cjs",
|
|
30
|
+
"src/**/*.cpp",
|
|
31
|
+
"src/**/*.h",
|
|
32
|
+
"!src/test-utils/**"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/photostructure/heap-trim.git"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"install": "node scripts/install.cjs",
|
|
41
|
+
"clean": "run-p clean:*",
|
|
42
|
+
"clean:dist": "del-cli coverage dist docs \"*.tsbuildinfo\"",
|
|
43
|
+
"clean:gyp-cache": "del-cli --force build %USERPROFILE%/.node-gyp ~/.node-gyp",
|
|
44
|
+
"clean:native": "node-gyp clean",
|
|
45
|
+
"node-gyp-rebuild": "node-gyp rebuild",
|
|
46
|
+
"build": "run-s build:native build:dist",
|
|
47
|
+
"build:native": "tsx scripts/prebuildify-wrapper.ts",
|
|
48
|
+
"build:linux-glibc": "bash scripts/prebuild-linux-glibc.sh",
|
|
49
|
+
"build:dist": "tsup && node scripts/post-build.mjs",
|
|
50
|
+
"docs": "typedoc",
|
|
51
|
+
"// test": "support `npm t name_of_file` (and don't fail due to missing coverage)",
|
|
52
|
+
"test": "npm run test:cjs -- --no-coverage",
|
|
53
|
+
"tests": "run-s test:*",
|
|
54
|
+
"test:cjs": "cross-env TEST_ESM=0 jest",
|
|
55
|
+
"test:esm": "cross-env TEST_ESM=1 node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js",
|
|
56
|
+
"check:memory": "tsx scripts/check-memory.ts",
|
|
57
|
+
"lint": "run-s lint:*",
|
|
58
|
+
"lint:native": "tsx scripts/clang-tidy.ts",
|
|
59
|
+
"lint:tsc": "tsc --noEmit",
|
|
60
|
+
"lint:eslint": "eslint",
|
|
61
|
+
"// check:exports": "verify package.json exports + type declarations resolve correctly (arethetypeswrong). Requires `npm run build:dist` first.",
|
|
62
|
+
"check:exports": "attw --pack .",
|
|
63
|
+
"fmt": "run-p fmt:*",
|
|
64
|
+
"// fmt:cpp": "on ubuntu: `sudo apt install clang-format`. Note that windows emits `invalid format` with this || true approach, but it works and is better than introducing Yet Another Script Script (like scripts/clang-format.mjs).",
|
|
65
|
+
"fmt:cpp": "clang-format --style=LLVM -i src/*.cpp || echo \"problem with clang-format\"",
|
|
66
|
+
"fmt:js": "prettier --write \"**/*.{cjs,mjs,js}\"",
|
|
67
|
+
"fmt:ts": "prettier --write \"**/*.{cts,mts,ts}\"",
|
|
68
|
+
"fmt:md": "prettier --write \"**/*.md\"",
|
|
69
|
+
"fmt:json": "prettier --write \"**/*.json\"",
|
|
70
|
+
"fmt:yml": "prettier --write \"**/*.{yaml,yml}\"",
|
|
71
|
+
"fmt:pkg": "npm pkg fix",
|
|
72
|
+
"update": "run-p update:*",
|
|
73
|
+
"update:deps": "ncu -u",
|
|
74
|
+
"install:pinact": "go install github.com/suzuki-shunsuke/pinact/v4/cmd/pinact@v4.1.0",
|
|
75
|
+
"update:actions": "pinact run -u || echo \"problem with pinact\"",
|
|
76
|
+
"release:verify-prebuilds": "tsx scripts/release-prebuild-artifacts-cli.ts verify",
|
|
77
|
+
"release:pack-package": "tsx scripts/release-package-cli.ts pack",
|
|
78
|
+
"release:install-package": "tsx scripts/release-package-cli.ts install",
|
|
79
|
+
"release:load-package": "tsx scripts/release-package-cli.ts load",
|
|
80
|
+
"// preflight": "should be manually run by developers before they run `git commit`",
|
|
81
|
+
"preflight": "tsx scripts/preflight.ts",
|
|
82
|
+
"prepare-release": "npm run build:dist"
|
|
83
|
+
},
|
|
84
|
+
"gypfile": true,
|
|
85
|
+
"publishConfig": {
|
|
86
|
+
"access": "public"
|
|
87
|
+
},
|
|
88
|
+
"engines": {
|
|
89
|
+
"node": ">=22"
|
|
90
|
+
},
|
|
91
|
+
"os": [
|
|
92
|
+
"darwin",
|
|
93
|
+
"linux",
|
|
94
|
+
"win32"
|
|
95
|
+
],
|
|
96
|
+
"cpu": [
|
|
97
|
+
"x64",
|
|
98
|
+
"arm64"
|
|
99
|
+
],
|
|
100
|
+
"keywords": [
|
|
101
|
+
"malloc",
|
|
102
|
+
"malloc_trim",
|
|
103
|
+
"memory",
|
|
104
|
+
"rss",
|
|
105
|
+
"native",
|
|
106
|
+
"node-addon",
|
|
107
|
+
"linux",
|
|
108
|
+
"macos",
|
|
109
|
+
"windows"
|
|
110
|
+
],
|
|
111
|
+
"dependencies": {
|
|
112
|
+
"node-addon-api": "8.9.2",
|
|
113
|
+
"node-gyp-build": "^4.8.4"
|
|
114
|
+
},
|
|
115
|
+
"devDependencies": {
|
|
116
|
+
"@arethetypeswrong/cli": "0.18.5",
|
|
117
|
+
"@eslint/js": "^10.0.1",
|
|
118
|
+
"@types/jest": "^30.0.0",
|
|
119
|
+
"@types/node": "26.6.5",
|
|
120
|
+
"cross-env": "^10.1.0",
|
|
121
|
+
"del-cli": "8.0.1",
|
|
122
|
+
"eslint": "10.13.0",
|
|
123
|
+
"eslint-plugin-regexp": "3.3.1",
|
|
124
|
+
"eslint-plugin-security": "4.2.0",
|
|
125
|
+
"globals": "17.13.0",
|
|
126
|
+
"jest": "30.4.2",
|
|
127
|
+
"jest-environment-node": "30.4.1",
|
|
128
|
+
"node-gyp": "13.1.0",
|
|
129
|
+
"npm-check-updates": "23.1.1",
|
|
130
|
+
"npm-run-all2": "9.0.4",
|
|
131
|
+
"prebuildify": "^6.0.1",
|
|
132
|
+
"prettier": "3.9.10",
|
|
133
|
+
"prettier-plugin-organize-imports": "4.3.0",
|
|
134
|
+
"ts-jest": "29.4.15",
|
|
135
|
+
"tsup": "9.0.0",
|
|
136
|
+
"tsx": "4.23.15",
|
|
137
|
+
"typedoc": "0.28.20",
|
|
138
|
+
"typescript": "6.0.3",
|
|
139
|
+
"typescript-eslint": "8.72.0"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Install script: on glibc Linux, runs node-gyp-build, which loads a prebuild
|
|
5
|
+
* or compiles the addon from source. Everywhere else it does nothing.
|
|
6
|
+
*
|
|
7
|
+
* node-gyp-build is resolved through require.resolve and run with this
|
|
8
|
+
* process's own interpreter, rather than spawned as `npx node-gyp-build`.
|
|
9
|
+
*
|
|
10
|
+
* An outer `npm install -g` exports npm_config_global=true, and a nested npx
|
|
11
|
+
* inherits it. npx then treats its own bootstrap directory as a global
|
|
12
|
+
* install, never writes $npm_config_cache/_npx/<hash>/package.json, and fails
|
|
13
|
+
* reading the file it did not write -- exit 254 on POSIX, 127 on Windows.
|
|
14
|
+
* Resolving the binary here re-enters neither npm nor the shell, so nothing in
|
|
15
|
+
* npm's lifecycle environment can redirect it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { spawnSync } = require("node:child_process");
|
|
19
|
+
|
|
20
|
+
// src/index.ts loads the addon only on glibc, and only glibc has prebuilds, so
|
|
21
|
+
// building it anywhere else would compile a file nothing loads.
|
|
22
|
+
if (process.report.getReport().header.glibcVersionRuntime == null) {
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// No env: option, so the child inherits process.env.
|
|
27
|
+
const result = spawnSync(
|
|
28
|
+
process.execPath,
|
|
29
|
+
[require.resolve("node-gyp-build/bin.js")],
|
|
30
|
+
{ stdio: "inherit" },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (result.error) {
|
|
34
|
+
console.error("Failed to run node-gyp-build:", result.error);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (result.status !== 0) {
|
|
39
|
+
console.error(`node-gyp-build exited with code ${result.status}`);
|
|
40
|
+
process.exit(result.status ?? 1);
|
|
41
|
+
}
|
package/src/binding.cpp
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// <cstdlib> comes first so glibc's <features.h> defines __GLIBC__ before the
|
|
2
|
+
// check below.
|
|
3
|
+
#include <cstdlib>
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
|
|
6
|
+
#if defined(__GLIBC__)
|
|
7
|
+
#include <malloc.h>
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
namespace photostructure::heap_trim {
|
|
11
|
+
|
|
12
|
+
// Asks the C allocator to return free pages to the operating system.
|
|
13
|
+
//
|
|
14
|
+
// glibc's free() shrinks the heap only from its top, so pages freed below a
|
|
15
|
+
// live allocation stay resident. malloc_trim(0) releases those free pages in
|
|
16
|
+
// every arena. It returns 1 if it found a whole free page to madvise() away,
|
|
17
|
+
// without checking whether that page was still resident, so a second call
|
|
18
|
+
// right after the first also returns 1.
|
|
19
|
+
//
|
|
20
|
+
// src/index.ts loads this addon only on glibc (it explains why). The #else
|
|
21
|
+
// branch exists so `npm run lint` and a manual build still compile this file
|
|
22
|
+
// on musl, macOS, and Windows.
|
|
23
|
+
Napi::Value HeapTrim(const Napi::CallbackInfo &info) {
|
|
24
|
+
#if defined(__GLIBC__)
|
|
25
|
+
return Napi::Boolean::New(info.Env(), malloc_trim(0) == 1);
|
|
26
|
+
#else
|
|
27
|
+
return Napi::Boolean::New(info.Env(), false);
|
|
28
|
+
#endif
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
32
|
+
exports.Set("heapTrim", Napi::Function::New(env, HeapTrim, "heapTrim"));
|
|
33
|
+
return exports;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
} // namespace photostructure::heap_trim
|
|
37
|
+
|
|
38
|
+
// NODE_API_MODULE needs an unqualified function name.
|
|
39
|
+
Napi::Object InitHeapTrim(Napi::Env env, Napi::Object exports) {
|
|
40
|
+
return photostructure::heap_trim::Init(env, exports);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
NODE_API_MODULE(heap_trim, InitHeapTrim)
|