@@ -14,10 +14,32 @@ const PLUGIN_STATES = Object.freeze({
1414 FAILED : 'failed' ,
1515} ) ;
1616
17+ const DEFAULT_RESOURCE_LIMITS = Object . freeze ( {
18+ maxHookDurationMs : 24 ,
19+ maxHeapUsedBytes : 512 * 1024 * 1024 ,
20+ maxHeapDeltaBytes : 8 * 1024 * 1024 ,
21+ } ) ;
22+
1723function normalizePluginId ( pluginId ) {
1824 return String ( pluginId || '' ) . trim ( ) ;
1925}
2026
27+ function normalizeLimit ( value , fallback ) {
28+ const numeric = Number ( value ) ;
29+ if ( ! Number . isFinite ( numeric ) || numeric <= 0 ) {
30+ return fallback ;
31+ }
32+ return numeric ;
33+ }
34+
35+ function normalizeResourceLimits ( resourceLimits = { } ) {
36+ return Object . freeze ( {
37+ maxHookDurationMs : normalizeLimit ( resourceLimits ?. maxHookDurationMs , DEFAULT_RESOURCE_LIMITS . maxHookDurationMs ) ,
38+ maxHeapUsedBytes : normalizeLimit ( resourceLimits ?. maxHeapUsedBytes , DEFAULT_RESOURCE_LIMITS . maxHeapUsedBytes ) ,
39+ maxHeapDeltaBytes : normalizeLimit ( resourceLimits ?. maxHeapDeltaBytes , DEFAULT_RESOURCE_LIMITS . maxHeapDeltaBytes ) ,
40+ } ) ;
41+ }
42+
2143function toErrorDetails ( error ) {
2244 if ( error && typeof error === 'object' && typeof error . message === 'string' ) {
2345 return {
@@ -73,6 +95,31 @@ function getNowMs() {
7395 return Date . now ( ) ;
7496}
7597
98+ function readHeapUsageBytes ( ) {
99+ try {
100+ if ( typeof globalThis ?. process ?. memoryUsage === 'function' ) {
101+ const usage = globalThis . process . memoryUsage ( ) ;
102+ const heapUsed = Number ( usage ?. heapUsed ) ;
103+ if ( Number . isFinite ( heapUsed ) && heapUsed >= 0 ) {
104+ return heapUsed ;
105+ }
106+ }
107+ } catch {
108+ // Heap metrics are best-effort for diagnostics.
109+ }
110+
111+ try {
112+ const browserHeap = Number ( globalThis ?. performance ?. memory ?. usedJSHeapSize ) ;
113+ if ( Number . isFinite ( browserHeap ) && browserHeap >= 0 ) {
114+ return browserHeap ;
115+ }
116+ } catch {
117+ // Ignore unavailable browser heap APIs.
118+ }
119+
120+ return 0 ;
121+ }
122+
76123function createHookMetrics ( ) {
77124 return {
78125 calls : 0 ,
@@ -114,6 +161,14 @@ function createPluginMetrics() {
114161 lastRegisteredAtIso : '' ,
115162 lastExtensionId : '' ,
116163 } ,
164+ resources : {
165+ cpuViolations : 0 ,
166+ memoryViolations : 0 ,
167+ lastHookDurationMs : 0 ,
168+ lastHeapUsedBytes : 0 ,
169+ lastHeapDeltaBytes : 0 ,
170+ lastViolationMessage : '' ,
171+ } ,
117172 lastState : '' ,
118173 lastStateChangedAtIso : '' ,
119174 } ;
@@ -148,6 +203,7 @@ function normalizePluginDefinition(plugin) {
148203 id,
149204 version : String ( plugin . version || '' ) . trim ( ) ,
150205 metadata : Object . freeze ( { ...( plugin . metadata || { } ) } ) ,
206+ resourceLimits : normalizeResourceLimits ( plugin . resourceLimits ) ,
151207 init : typeof plugin . init === 'function' ? plugin . init : ( typeof plugin . onInit === 'function' ? plugin . onInit : null ) ,
152208 activate : typeof plugin . activate === 'function'
153209 ? plugin . activate
@@ -322,10 +378,60 @@ export default function createPhase19OverlayPluginRegistry({
322378 return true ;
323379 }
324380
381+ function evaluateResourceLimitViolations ( record , phase , durationMs , heapUsedBytes , heapDeltaBytes ) {
382+ const limits = record ?. plugin ?. resourceLimits || DEFAULT_RESOURCE_LIMITS ;
383+ const duration = Math . max ( 0 , Number ( durationMs ) || 0 ) ;
384+ const heapUsed = Math . max ( 0 , Number ( heapUsedBytes ) || 0 ) ;
385+ const heapDelta = Math . max ( 0 , Number ( heapDeltaBytes ) || 0 ) ;
386+ const cpuExceeded = duration > limits . maxHookDurationMs ;
387+ const heapExceeded = heapUsed > limits . maxHeapUsedBytes ;
388+ const heapDeltaExceeded = heapDelta > limits . maxHeapDeltaBytes ;
389+
390+ if ( record ?. metrics ?. resources ) {
391+ record . metrics . resources . lastHookDurationMs = duration ;
392+ record . metrics . resources . lastHeapUsedBytes = heapUsed ;
393+ record . metrics . resources . lastHeapDeltaBytes = heapDelta ;
394+ if ( cpuExceeded ) {
395+ record . metrics . resources . cpuViolations += 1 ;
396+ }
397+ if ( heapExceeded || heapDeltaExceeded ) {
398+ record . metrics . resources . memoryViolations += 1 ;
399+ }
400+ }
401+
402+ if ( ! cpuExceeded && ! heapExceeded && ! heapDeltaExceeded ) {
403+ return { ok : true } ;
404+ }
405+
406+ const reasons = [ ] ;
407+ if ( cpuExceeded ) {
408+ reasons . push ( `CPU ${ duration . toFixed ( 3 ) } ms > ${ limits . maxHookDurationMs } ms` ) ;
409+ }
410+ if ( heapExceeded ) {
411+ reasons . push ( `heapUsed ${ heapUsed } B > ${ limits . maxHeapUsedBytes } B` ) ;
412+ }
413+ if ( heapDeltaExceeded ) {
414+ reasons . push ( `heapDelta ${ heapDelta } B > ${ limits . maxHeapDeltaBytes } B` ) ;
415+ }
416+ const message = `Resource limit exceeded (${ reasons . join ( ', ' ) } )` ;
417+ if ( record ?. metrics ?. resources ) {
418+ record . metrics . resources . lastViolationMessage = message ;
419+ }
420+ return {
421+ ok : false ,
422+ phase : `${ phase } -resource-limit` ,
423+ error : {
424+ name : 'ResourceLimitError' ,
425+ message,
426+ } ,
427+ } ;
428+ }
429+
325430 function runLifecycleHook ( record , phase , context = { } ) {
326431 const bucket = record ?. metrics ?. hooks ?. [ phase ] || null ;
327432 const hook = record ?. plugin ?. [ phase ] ;
328433 const startedAtMs = getNowMs ( ) ;
434+ const startedHeapBytes = readHeapUsageBytes ( ) ;
329435 const startedAtIso = new Date ( ) . toISOString ( ) ;
330436 if ( bucket ) {
331437 bucket . calls += 1 ;
@@ -334,14 +440,16 @@ export default function createPhase19OverlayPluginRegistry({
334440 if ( typeof hook !== 'function' ) {
335441 const finishedAtMs = getNowMs ( ) ;
336442 const durationMs = Math . max ( 0 , finishedAtMs - startedAtMs ) ;
443+ const finishedHeapBytes = readHeapUsageBytes ( ) ;
444+ const heapDeltaBytes = Math . max ( 0 , finishedHeapBytes - startedHeapBytes ) ;
337445 if ( bucket ) {
338446 bucket . successes += 1 ;
339447 bucket . totalDurationMs += durationMs ;
340448 bucket . lastDurationMs = durationMs ;
341449 bucket . lastFinishedAtIso = new Date ( ) . toISOString ( ) ;
342450 bucket . lastErrorMessage = '' ;
343451 }
344- return { ok : true } ;
452+ return evaluateResourceLimitViolations ( record , phase , durationMs , finishedHeapBytes , heapDeltaBytes ) ;
345453 }
346454 try {
347455 const previousHookPluginId = activeHookPluginId ;
@@ -358,14 +466,16 @@ export default function createPhase19OverlayPluginRegistry({
358466 hook ( lifecycleContext ) ;
359467 const finishedAtMs = getNowMs ( ) ;
360468 const durationMs = Math . max ( 0 , finishedAtMs - startedAtMs ) ;
469+ const finishedHeapBytes = readHeapUsageBytes ( ) ;
470+ const heapDeltaBytes = Math . max ( 0 , finishedHeapBytes - startedHeapBytes ) ;
361471 if ( bucket ) {
362472 bucket . successes += 1 ;
363473 bucket . totalDurationMs += durationMs ;
364474 bucket . lastDurationMs = durationMs ;
365475 bucket . lastFinishedAtIso = new Date ( ) . toISOString ( ) ;
366476 bucket . lastErrorMessage = '' ;
367477 }
368- return { ok : true } ;
478+ return evaluateResourceLimitViolations ( record , phase , durationMs , finishedHeapBytes , heapDeltaBytes ) ;
369479 } finally {
370480 activeHookPluginId = previousHookPluginId ;
371481 }
@@ -404,7 +514,7 @@ export default function createPhase19OverlayPluginRegistry({
404514 }
405515 const initResult = runLifecycleHook ( record , 'init' , context ) ;
406516 if ( ! initResult . ok ) {
407- isolatePluginFailure ( record , 'init' , initResult . error , context , {
517+ isolatePluginFailure ( record , initResult . phase || 'init' , initResult . error , context , {
408518 unregisterExtension : true ,
409519 quarantine : true ,
410520 } ) ;
@@ -473,7 +583,7 @@ export default function createPhase19OverlayPluginRegistry({
473583
474584 const activateResult = runLifecycleHook ( record , 'activate' , context ) ;
475585 if ( ! activateResult . ok ) {
476- isolatePluginFailure ( record , 'activate' , activateResult . error , context , {
586+ isolatePluginFailure ( record , activateResult . phase || 'activate' , activateResult . error , context , {
477587 unregisterExtension : true ,
478588 quarantine : true ,
479589 } ) ;
@@ -510,7 +620,7 @@ export default function createPhase19OverlayPluginRegistry({
510620 }
511621 const deactivateResult = runLifecycleHook ( record , 'deactivate' , context ) ;
512622 if ( ! deactivateResult . ok ) {
513- isolatePluginFailure ( record , 'deactivate' , deactivateResult . error , context , {
623+ isolatePluginFailure ( record , deactivateResult . phase || 'deactivate' , deactivateResult . error , context , {
514624 unregisterExtension : true ,
515625 quarantine : true ,
516626 } ) ;
@@ -548,7 +658,7 @@ export default function createPhase19OverlayPluginRegistry({
548658 }
549659 const destroyResult = runLifecycleHook ( record , 'destroy' , context ) ;
550660 if ( ! destroyResult . ok ) {
551- isolatePluginFailure ( record , 'destroy' , destroyResult . error , context , {
661+ isolatePluginFailure ( record , destroyResult . phase || 'destroy' , destroyResult . error , context , {
552662 unregisterExtension : true ,
553663 quarantine : true ,
554664 } ) ;
@@ -728,6 +838,7 @@ export default function createPhase19OverlayPluginRegistry({
728838 version : record . plugin . version ,
729839 extensionId : record . extension . id ,
730840 state : record . state ,
841+ resourceLimits : record . plugin . resourceLimits ,
731842 failureCount : record . failureCount ,
732843 lastFailure : record . lastFailure ,
733844 metrics : snapshotPluginMetrics ( record . metrics ) ,
@@ -801,6 +912,8 @@ export default function createPhase19OverlayPluginRegistry({
801912 failureCount : record . failureCount ,
802913 transitionAttempts : record . metrics ?. transitions ?. attempted || 0 ,
803914 transitionFailures : record . metrics ?. transitions ?. failed || 0 ,
915+ cpuViolations : record . metrics ?. resources ?. cpuViolations || 0 ,
916+ memoryViolations : record . metrics ?. resources ?. memoryViolations || 0 ,
804917 } ) ;
805918 }
806919 return Object . freeze ( entries ) ;
0 commit comments