@@ -155,6 +155,12 @@ pub enum Pool {
155155 /// slots, finalizer records, deferreds, scope frames, callback
156156 /// registrations), granted in chunks by [`napi_host_bookkeeping_charge`].
157157 HostBookkeeping ,
158+ /// Committed pages of buffers V8 reserves through its page allocator
159+ /// rather than the `ArrayBuffer::Allocator`: resizable `ArrayBuffer`s and
160+ /// growable `SharedArrayBuffer`s. Charged softly (see
161+ /// [`ResourceBudget::try_charge_soft`]): a refused commit surfaces in JS as
162+ /// a `RangeError` and does not stop the context.
163+ V8BackingPages ,
158164}
159165
160166/// Embedder-owned aggregate accounting for byte reservations made by N-API.
@@ -165,6 +171,14 @@ pub trait NapiMemoryAccountant: Send + Sync {
165171 fn memory_limit ( & self ) -> u64 ;
166172 fn memory_charged ( & self ) -> u64 ;
167173 fn try_charge ( & self , bytes : u64 ) -> bool ;
174+ /// Charge bytes whose refusal the provider handles gracefully (V8 turns
175+ /// it into a JS `RangeError`). Must deny without side effects such as
176+ /// stopping the application; it may be called from V8 background
177+ /// threads and must not call back into V8. Embedders whose
178+ /// [`try_charge`](Self::try_charge) is terminal should override it.
179+ fn try_charge_soft ( & self , bytes : u64 ) -> bool {
180+ self . try_charge ( bytes)
181+ }
168182 fn uncharge ( & self , bytes : u64 ) ;
169183}
170184
@@ -219,6 +233,8 @@ pub struct ResourceUsage {
219233 /// [`DEFAULT_HEAP_EMERGENCY_HEADROOM`]). Non-zero means at least one
220234 /// isolate of this budget is stopping after exhausting its heap.
221235 pub v8_heap_emergency : u64 ,
236+ /// Committed bytes of page-allocated V8 buffers (resizable buffers).
237+ pub v8_backing_pages : u64 ,
222238 /// Number of live V8 isolates (envs) counted against `max_envs`.
223239 pub live_isolates : usize ,
224240}
@@ -281,6 +297,7 @@ pub struct ResourceBudget {
281297 /// Emergency headroom currently exposed, summed over stopping isolates.
282298 /// Not part of `mem_charged`: it is by definition over the budget.
283299 v8_heap_emergency : AtomicU64 ,
300+ v8_backing_pages : AtomicU64 ,
284301 /// Live V8 isolates (envs), counted against `max_envs`.
285302 live_isolates : AtomicUsize ,
286303}
@@ -316,6 +333,10 @@ impl std::fmt::Debug for ResourceBudget {
316333 "v8_heap_emergency" ,
317334 & self . v8_heap_emergency . load ( Ordering :: Acquire ) ,
318335 )
336+ . field (
337+ "v8_backing_pages" ,
338+ & self . v8_backing_pages . load ( Ordering :: Acquire ) ,
339+ )
319340 . field ( "live_isolates" , & self . live_isolates . load ( Ordering :: Acquire ) )
320341 . finish ( )
321342 }
@@ -358,6 +379,7 @@ impl ResourceBudget {
358379 host_bookkeeping : AtomicU64 :: new ( 0 ) ,
359380 heap_emergency_headroom : AtomicU64 :: new ( DEFAULT_HEAP_EMERGENCY_HEADROOM ) ,
360381 v8_heap_emergency : AtomicU64 :: new ( 0 ) ,
382+ v8_backing_pages : AtomicU64 :: new ( 0 ) ,
361383 live_isolates : AtomicUsize :: new ( 0 ) ,
362384 } )
363385 }
@@ -376,6 +398,7 @@ impl ResourceBudget {
376398 host_bookkeeping : AtomicU64 :: new ( 0 ) ,
377399 heap_emergency_headroom : AtomicU64 :: new ( DEFAULT_HEAP_EMERGENCY_HEADROOM ) ,
378400 v8_heap_emergency : AtomicU64 :: new ( 0 ) ,
401+ v8_backing_pages : AtomicU64 :: new ( 0 ) ,
379402 live_isolates : AtomicUsize :: new ( 0 ) ,
380403 }
381404 }
@@ -411,12 +434,29 @@ impl ResourceBudget {
411434 ///
412435 /// [`uncharge`]: ResourceBudget::uncharge
413436 pub fn try_charge ( & self , pool : Pool , bytes : u64 ) -> Result < ( ) , OverBudget > {
437+ self . charge ( pool, bytes, false )
438+ }
439+
440+ /// Like [`try_charge`](Self::try_charge), but a refusal is reported only
441+ /// to the caller: an embedder accountant is asked through
442+ /// [`NapiMemoryAccountant::try_charge_soft`], so the application is not
443+ /// stopped. For allocations whose failure V8 handles gracefully.
444+ pub fn try_charge_soft ( & self , pool : Pool , bytes : u64 ) -> Result < ( ) , OverBudget > {
445+ self . charge ( pool, bytes, true )
446+ }
447+
448+ fn charge ( & self , pool : Pool , bytes : u64 , soft : bool ) -> Result < ( ) , OverBudget > {
414449 if bytes == 0 {
415450 return Ok ( ( ) ) ;
416451 }
417452
418453 if let Some ( accountant) = & self . accountant {
419- if !accountant. try_charge ( bytes) {
454+ let granted = if soft {
455+ accountant. try_charge_soft ( bytes)
456+ } else {
457+ accountant. try_charge ( bytes)
458+ } ;
459+ if !granted {
420460 return Err ( OverBudget {
421461 pool,
422462 requested : bytes,
@@ -480,6 +520,7 @@ impl ResourceBudget {
480520 Pool :: HostTransient => & self . host_transient ,
481521 Pool :: SerializedMessage => & self . serialized_message ,
482522 Pool :: HostBookkeeping => & self . host_bookkeeping ,
523+ Pool :: V8BackingPages => & self . v8_backing_pages ,
483524 }
484525 }
485526
@@ -496,6 +537,7 @@ impl ResourceBudget {
496537 serialized_message : self . serialized_message . load ( Ordering :: Acquire ) ,
497538 host_bookkeeping : self . host_bookkeeping . load ( Ordering :: Acquire ) ,
498539 v8_heap_emergency : self . v8_heap_emergency . load ( Ordering :: Acquire ) ,
540+ v8_backing_pages : self . v8_backing_pages . load ( Ordering :: Acquire ) ,
499541 live_isolates : self . live_isolates . load ( Ordering :: Acquire ) ,
500542 }
501543 }
@@ -1968,6 +2010,54 @@ mod external_accountant_tests {
19682010 assert_eq ! ( budget. snapshot( ) . v8_heap_reserved, 0 ) ;
19692011 }
19702012
2013+ /// Counts terminal charges; soft charges must never take that path.
2014+ struct SoftAccountant {
2015+ inner : Arc < TestAccountant > ,
2016+ terminal_calls : AtomicU64 ,
2017+ }
2018+
2019+ impl NapiMemoryAccountant for SoftAccountant {
2020+ fn memory_limit ( & self ) -> u64 {
2021+ self . inner . memory_limit ( )
2022+ }
2023+
2024+ fn memory_charged ( & self ) -> u64 {
2025+ self . inner . memory_charged ( )
2026+ }
2027+
2028+ fn try_charge ( & self , bytes : u64 ) -> bool {
2029+ self . terminal_calls . fetch_add ( 1 , Ordering :: AcqRel ) ;
2030+ self . inner . try_charge ( bytes)
2031+ }
2032+
2033+ fn try_charge_soft ( & self , bytes : u64 ) -> bool {
2034+ self . inner . try_charge ( bytes)
2035+ }
2036+
2037+ fn uncharge ( & self , bytes : u64 ) {
2038+ self . inner . uncharge ( bytes) ;
2039+ }
2040+ }
2041+
2042+ #[ test]
2043+ fn soft_charges_use_the_accountants_soft_path ( ) {
2044+ let accountant = Arc :: new ( SoftAccountant {
2045+ inner : TestAccountant :: new ( 100 ) ,
2046+ terminal_calls : AtomicU64 :: new ( 0 ) ,
2047+ } ) ;
2048+ let budget = ResourceBudget :: with_accountant ( accountant. clone ( ) ) ;
2049+ budget. try_charge_soft ( Pool :: V8BackingPages , 80 ) . unwrap ( ) ;
2050+ let refused = budget
2051+ . try_charge_soft ( Pool :: V8BackingPages , 40 )
2052+ . unwrap_err ( ) ;
2053+ assert_eq ! ( refused. pool, Pool :: V8BackingPages ) ;
2054+ assert_eq ! ( accountant. terminal_calls. load( Ordering :: Acquire ) , 0 ) ;
2055+ assert_eq ! ( budget. snapshot( ) . v8_backing_pages, 80 ) ;
2056+ budget. uncharge ( Pool :: V8BackingPages , 80 ) ;
2057+ assert_eq ! ( budget. snapshot( ) . v8_backing_pages, 0 ) ;
2058+ assert_eq ! ( budget. memory_charged( ) , 0 ) ;
2059+ }
2060+
19712061 #[ cfg( all( napi_standalone_legacy_wait, not( target_arch = "wasm32" ) ) ) ]
19722062 #[ test]
19732063 fn external_accountant_cannot_use_legacy_raw_detachment ( ) {
0 commit comments