@@ -2,12 +2,12 @@ import os from 'node:os';
22
33import { Configuration , EventType , LocalEventManager , Snapshotter } from '@crawlee/core' ;
44import type { MemoryInfo } from '@crawlee/utils' ;
5- import * as utils from '@crawlee/utils' ;
65import { sleep } from '@crawlee/utils' ;
76
87import log from '@apify/log' ;
98
109const toBytes = ( x : number ) => x * 1024 * 1024 ;
10+ const noop = ( ) => { } ;
1111
1212describe ( 'Snapshotter' , ( ) => {
1313 let logLevel : number ;
@@ -139,7 +139,6 @@ describe('Snapshotter', () => {
139139
140140 cpusMock . mockReturnValue ( fakeCpu as any ) ;
141141
142- const noop = ( ) => { } ;
143142 const config = new Configuration ( { maxUsedCpuRatio : 0.5 } ) ;
144143 const snapshotter = new Snapshotter ( { config } ) ;
145144 // do not initialize the event intervals as we will fire them manually
@@ -178,7 +177,6 @@ describe('Snapshotter', () => {
178177 test ( 'correctly marks eventLoopOverloaded' , ( ) => {
179178 const clock = vitest . useFakeTimers ( ) ;
180179 try {
181- const noop = ( ) => { } ;
182180 const snapshotter = new Snapshotter ( { maxBlockedMillis : 5 , eventLoopSnapshotIntervalSecs : 0 } ) ;
183181 // @ts -expect-error Calling protected method
184182 snapshotter . _snapshotEventLoop ( noop ) ;
@@ -208,13 +206,12 @@ describe('Snapshotter', () => {
208206 } ) ;
209207
210208 test ( 'correctly marks memoryOverloaded using OS metrics' , async ( ) => {
211- const noop = ( ) => { } ;
212209 const memoryData = {
213210 totalBytes : toBytes ( 10000 ) ,
214211 mainProcessBytes : toBytes ( 1000 ) ,
215212 childProcessesBytes : toBytes ( 1000 ) ,
216213 } as MemoryInfo ;
217- vitest . spyOn ( utils , 'getMemoryInfoV2 ' ) . mockResolvedValue ( memoryData ) ;
214+ vitest . spyOn ( LocalEventManager . prototype as any , 'getMemoryInfo ' ) . mockResolvedValue ( memoryData ) ;
218215 const config = new Configuration ( { availableMemoryRatio : 1 } ) ;
219216 const snapshotter = new Snapshotter ( { config, maxUsedMemoryRatio : 0.5 } ) ;
220217 // do not initialize the event intervals as we will fire them manually
@@ -245,31 +242,48 @@ describe('Snapshotter', () => {
245242 } ) ;
246243
247244 test ( 'correctly logs critical memory overload' , async ( ) => {
248- vitest . spyOn ( utils , 'getMemoryInfoV2' ) . mockResolvedValueOnce ( { totalBytes : toBytes ( 10000 ) } as MemoryInfo ) ;
245+ const initialMemory = toBytes ( 10000 ) ;
246+ const usageRatio1 = 0.75 ; // below warning usage
247+ const usageRatio2 = 0.76 ; // above warning usage
248+ const memoryData : MemoryInfo = {
249+ totalBytes : initialMemory ,
250+ freeBytes : initialMemory * ( 1 - usageRatio1 ) ,
251+ usedBytes : initialMemory * usageRatio1 ,
252+ mainProcessBytes : initialMemory * usageRatio1 ,
253+ childProcessesBytes : 0 ,
254+ } ;
255+
256+ // Mock memory info to be able to inject custom memory measurement data.
257+ vitest . spyOn ( LocalEventManager . prototype as any , 'getMemoryInfo' ) . mockResolvedValue ( memoryData ) ;
249258 const config = new Configuration ( { availableMemoryRatio : 1 } ) ;
250259 const snapshotter = new Snapshotter ( { config, maxUsedMemoryRatio : 0.5 } ) ;
260+
261+ const eventManager = config . getEventManager ( ) as LocalEventManager ;
251262 await snapshotter . start ( ) ;
252263 const warningSpy = vitest . spyOn ( snapshotter . log , 'warning' ) . mockImplementation ( ( ) => { } ) ;
253264
254- // @ts -expect-error Calling private method
255- snapshotter . _memoryOverloadWarning ( {
256- memCurrentBytes : toBytes ( 7600 ) ,
257- } ) ;
265+ // First snapshot - below warning usage
266+ await eventManager . emitSystemInfoEvent ( noop ) ;
267+ expect ( warningSpy ) . not . toBeCalled ( ) ;
268+
269+ // Second snapshot - above warning usage
270+ memoryData . usedBytes = initialMemory * usageRatio2 ;
271+ memoryData . mainProcessBytes = initialMemory * usageRatio2 ;
272+ memoryData . freeBytes = initialMemory * ( 1 - usageRatio2 ) ;
273+ await eventManager . emitSystemInfoEvent ( noop ) ;
258274 expect ( warningSpy ) . toBeCalled ( ) ;
259275 warningSpy . mockReset ( ) ;
260276
261- // @ts -expect-error Calling private method
262- snapshotter . _memoryOverloadWarning ( {
263- memCurrentBytes : toBytes ( 7500 ) ,
264- } ) ;
277+ // Second snapshot again - repeated warning ignored
278+ await eventManager . emitSystemInfoEvent ( noop ) ;
265279 expect ( warningSpy ) . not . toBeCalled ( ) ;
280+ warningSpy . mockReset ( ) ;
266281
267282 vitest . restoreAllMocks ( ) ;
268283 await snapshotter . stop ( ) ;
269284 } ) ;
270285
271286 test ( 'correctly marks clientOverloaded' , ( ) => {
272- const noop = ( ) => { } ;
273287 // mock client data
274288 const apifyClient = Configuration . getStorageClient ( ) ;
275289 const oldStats = apifyClient . stats ;
@@ -332,4 +346,61 @@ describe('Snapshotter', () => {
332346 expect ( diffBetween ) . toBeLessThan ( SAMPLE_SIZE_MILLIS ) ;
333347 expect ( diffWithin ) . toBeLessThan ( SAMPLE_SIZE_MILLIS ) ;
334348 } ) ;
349+
350+ test . each ( [
351+ true ,
352+ false ,
353+ ] ) ( 'correctly handles dynamic vs static memory limit when total memory changes (dynamic=%s)' , async ( dynamic ) => {
354+ /**
355+ * Two memory snapshots are emitted with the same process memory usage but different total memory.
356+ * First snapshot is overloaded in both modes. Using 60% of total memory, while the limit is 50% in both modes.
357+ * Second snapshot doubles the total memory while keeping the same usage:
358+ * - Dynamic mode (availableMemoryRatio): maxMemoryBytes should update → not overloaded
359+ * - Static mode (memoryMbytes): maxMemoryBytes stays fixed → still overloaded
360+ */
361+ const initialTotalBytes = toBytes ( 100 ) ;
362+ const allowedMemoryUsageRatio = 0.5 ;
363+ const actualMemoryUsage = 0.6 * initialTotalBytes ;
364+
365+ // Initial snapshot. Overloaded in both modes.
366+ const memoryData : MemoryInfo = {
367+ totalBytes : initialTotalBytes ,
368+ freeBytes : initialTotalBytes - actualMemoryUsage ,
369+ usedBytes : actualMemoryUsage ,
370+ mainProcessBytes : actualMemoryUsage ,
371+ childProcessesBytes : 0 ,
372+ } ;
373+
374+ // Mock memory info to be able to inject custom memory measurement data.
375+ vitest . spyOn ( LocalEventManager . prototype as any , 'getMemoryInfo' ) . mockResolvedValue ( memoryData ) ;
376+
377+ let config : Configuration ;
378+ if ( dynamic ) {
379+ // Dynamic: Allow usage of 50 % of available memory through ratio
380+ config = new Configuration ( { availableMemoryRatio : allowedMemoryUsageRatio } ) ;
381+ } else {
382+ // Static: Allow usage of 50 % of available memory through fixed value
383+ config = new Configuration ( { memoryMbytes : ( allowedMemoryUsageRatio * initialTotalBytes ) / 1024 / 1024 } ) ;
384+ }
385+
386+ const snapshotter = new Snapshotter ( { config } ) ;
387+ vitest . spyOn ( LocalEventManager . prototype , 'init' ) . mockImplementation ( async ( ) => { } ) ;
388+ const eventManager = config . getEventManager ( ) as LocalEventManager ;
389+ await snapshotter . start ( ) ;
390+
391+ // First snapshot - full usage of the memory, should be overloaded in both modes
392+ await eventManager . emitSystemInfoEvent ( noop ) ;
393+
394+ // Second snapshot - total memory doubled, should be overloaded only in static mode
395+ memoryData . totalBytes = initialTotalBytes * 2 ;
396+ memoryData . freeBytes = memoryData . totalBytes - actualMemoryUsage ;
397+ await eventManager . emitSystemInfoEvent ( noop ) ;
398+
399+ const memorySnapshots = snapshotter . getMemorySample ( ) ;
400+ expect ( memorySnapshots ) . toHaveLength ( 2 ) ;
401+ expect ( memorySnapshots [ 0 ] . isOverloaded ) . toBe ( true ) ;
402+ expect ( memorySnapshots [ 1 ] . isOverloaded ) . toBe ( ! dynamic ) ;
403+
404+ await snapshotter . stop ( ) ;
405+ } ) ;
335406} ) ;
0 commit comments