Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@
"playwright-core": "1.62.1",
"form-data": "^4.0.6",
"tar": "^7.5.16",
"lerna/js-yaml": "^4.2.0"
"lerna/js-yaml": "^4.2.0",
"node-gyp": "^12.1.0"
},
"packageManager": "yarn@4.10.3",
"volta": {
Expand Down
15 changes: 9 additions & 6 deletions test/browser-pool/anonymize-proxy-sugar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ describe('anonymizeProxySugar', () => {
['http://username:password@proxy:1000/', 'http://username:password@proxy:1000'],
['socks://username:password@proxy:1000', 'socks://username:password@proxy:1000'],
['socks://username:password@proxy:1000/', 'socks://username:password@proxy:1000'],
])('should call anonymizeProxy from proxy-chain with correctly pre-processed URL: %s', async (input, expectedOutput) => {
const [anonymized] = await anonymizeProxySugar(input);

expect(anonymizeProxy).toHaveBeenCalledWith(expect.objectContaining({ url: expectedOutput }));
expect(anonymized).toBeTypeOf('string');
});
])(
'should call anonymizeProxy from proxy-chain with correctly pre-processed URL: %s',
async (input, expectedOutput) => {
const [anonymized] = await anonymizeProxySugar(input);

expect(anonymizeProxy).toHaveBeenCalledWith(expect.objectContaining({ url: expectedOutput }));
expect(anonymized).toBeTypeOf('string');
},
);

test('should pass ignoreProxyCertificate to anonymizeProxy', async () => {
await anonymizeProxySugar('http://username:password@proxy:1000', undefined, undefined, {
Expand Down
114 changes: 58 additions & 56 deletions test/core/autoscaling/snapshotter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,60 +351,62 @@ describe('Snapshotter', () => {
expect(diffWithin).toBeLessThan(SAMPLE_SIZE_MILLIS);
});

test.each([
true,
false,
])('correctly handles dynamic vs static memory limit when total memory changes (dynamic=%s)', async (dynamic) => {
/**
* Two memory snapshots are emitted with the same process memory usage but different total memory.
* First snapshot is overloaded in both modes. Using 60% of total memory, while the limit is 50% in both modes.
* Second snapshot doubles the total memory while keeping the same usage:
* - Dynamic mode (availableMemoryRatio): maxMemoryBytes should update → not overloaded
* - Static mode (memoryMbytes): maxMemoryBytes stays fixed → still overloaded
*/
const initialTotalBytes = toBytes(100);
const allowedMemoryUsageRatio = 0.5;
const actualMemoryUsage = 0.6 * initialTotalBytes;

// Initial snapshot. Overloaded in both modes.
const memoryData: MemoryInfo = {
totalBytes: initialTotalBytes,
freeBytes: initialTotalBytes - actualMemoryUsage,
usedBytes: actualMemoryUsage,
mainProcessBytes: actualMemoryUsage,
childProcessesBytes: 0,
};

// Mock memory info to be able to inject custom memory measurement data.
vitest.spyOn(LocalEventManager.prototype as any, 'getMemoryInfo').mockResolvedValue(memoryData);

let config: Configuration;
if (dynamic) {
// Dynamic: Allow usage of 50 % of available memory through ratio
config = new Configuration({ availableMemoryRatio: allowedMemoryUsageRatio });
} else {
// Static: Allow usage of 50 % of available memory through fixed value
config = new Configuration({ memoryMbytes: (allowedMemoryUsageRatio * initialTotalBytes) / 1024 / 1024 });
}

const snapshotter = new Snapshotter({ config });
vitest.spyOn(LocalEventManager.prototype, 'init').mockImplementation(async () => {});
const eventManager = config.getEventManager() as LocalEventManager;
await snapshotter.start();

// First snapshot - full usage of the memory, should be overloaded in both modes
await eventManager.emitSystemInfoEvent(noop);

// Second snapshot - total memory doubled, should be overloaded only in static mode
memoryData.totalBytes = initialTotalBytes * 2;
memoryData.freeBytes = memoryData.totalBytes - actualMemoryUsage;
await eventManager.emitSystemInfoEvent(noop);

const memorySnapshots = snapshotter.getMemorySample();
expect(memorySnapshots).toHaveLength(2);
expect(memorySnapshots[0].isOverloaded).toBe(true);
expect(memorySnapshots[1].isOverloaded).toBe(!dynamic);

await snapshotter.stop();
});
test.each([true, false])(
'correctly handles dynamic vs static memory limit when total memory changes (dynamic=%s)',
async (dynamic) => {
/**
* Two memory snapshots are emitted with the same process memory usage but different total memory.
* First snapshot is overloaded in both modes. Using 60% of total memory, while the limit is 50% in both modes.
* Second snapshot doubles the total memory while keeping the same usage:
* - Dynamic mode (availableMemoryRatio): maxMemoryBytes should update → not overloaded
* - Static mode (memoryMbytes): maxMemoryBytes stays fixed → still overloaded
*/
const initialTotalBytes = toBytes(100);
const allowedMemoryUsageRatio = 0.5;
const actualMemoryUsage = 0.6 * initialTotalBytes;

// Initial snapshot. Overloaded in both modes.
const memoryData: MemoryInfo = {
totalBytes: initialTotalBytes,
freeBytes: initialTotalBytes - actualMemoryUsage,
usedBytes: actualMemoryUsage,
mainProcessBytes: actualMemoryUsage,
childProcessesBytes: 0,
};

// Mock memory info to be able to inject custom memory measurement data.
vitest.spyOn(LocalEventManager.prototype as any, 'getMemoryInfo').mockResolvedValue(memoryData);

let config: Configuration;
if (dynamic) {
// Dynamic: Allow usage of 50 % of available memory through ratio
config = new Configuration({ availableMemoryRatio: allowedMemoryUsageRatio });
} else {
// Static: Allow usage of 50 % of available memory through fixed value
config = new Configuration({
memoryMbytes: (allowedMemoryUsageRatio * initialTotalBytes) / 1024 / 1024,
});
}

const snapshotter = new Snapshotter({ config });
vitest.spyOn(LocalEventManager.prototype, 'init').mockImplementation(async () => {});
const eventManager = config.getEventManager() as LocalEventManager;
await snapshotter.start();

// First snapshot - full usage of the memory, should be overloaded in both modes
await eventManager.emitSystemInfoEvent(noop);

// Second snapshot - total memory doubled, should be overloaded only in static mode
memoryData.totalBytes = initialTotalBytes * 2;
memoryData.freeBytes = memoryData.totalBytes - actualMemoryUsage;
await eventManager.emitSystemInfoEvent(noop);

const memorySnapshots = snapshotter.getMemorySample();
expect(memorySnapshots).toHaveLength(2);
expect(memorySnapshots[0].isOverloaded).toBe(true);
expect(memorySnapshots[1].isOverloaded).toBe(!dynamic);

await snapshotter.stop();
},
);
});
118 changes: 59 additions & 59 deletions test/core/crawlers/adaptive_playwright_crawler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,34 +302,34 @@ describe('AdaptivePlaywrightCrawler', () => {
});
});

test.each([
['static'],
['clientOnly'],
] as const)('crawlingContext.addRequests() should add requests correctly (%s)', async (renderingType) => {
const renderingTypePredictor = makeRiggedRenderingTypePredictor({
detectionProbabilityRecommendation: 0,
renderingType,
});
const url = new URL(`http://${HOSTNAME}:${port}`).toString();
test.each([['static'], ['clientOnly']] as const)(
'crawlingContext.addRequests() should add requests correctly (%s)',
async (renderingType) => {
const renderingTypePredictor = makeRiggedRenderingTypePredictor({
detectionProbabilityRecommendation: 0,
renderingType,
});
const url = new URL(`http://${HOSTNAME}:${port}`).toString();

let requestContext: LoadedContext<AdaptivePlaywrightCrawlerContext> | undefined;
const requestHandler: AdaptivePlaywrightCrawlerOptions['requestHandler'] = async (context) => {
const isStartUrl = context.request.url === url;
let requestContext: LoadedContext<AdaptivePlaywrightCrawlerContext> | undefined;
const requestHandler: AdaptivePlaywrightCrawlerOptions['requestHandler'] = async (context) => {
const isStartUrl = context.request.url === url;

if (isStartUrl) await context.addRequests([`${url}/1`]);
else requestContext = context;
};
if (isStartUrl) await context.addRequests([`${url}/1`]);
else requestContext = context;
};

const crawler = await makeOneshotCrawler(
{ requestHandler, renderingTypePredictor, maxRequestsPerCrawl: 10 },
[],
);
const crawler = await makeOneshotCrawler(
{ requestHandler, renderingTypePredictor, maxRequestsPerCrawl: 10 },
[],
);

await crawler.run([{ url, crawlDepth: 2 }]);
await crawler.run([{ url, crawlDepth: 2 }]);

assert(requestContext);
expect(requestContext.request).toMatchObject({ url: `${url}/1`, crawlDepth: 3 });
});
assert(requestContext);
expect(requestContext.request).toMatchObject({ url: `${url}/1`, crawlDepth: 3 });
},
);

describe('should enqueue links correctly', () => {
test.each([
Expand Down Expand Up @@ -383,49 +383,49 @@ describe('AdaptivePlaywrightCrawler', () => {
});
});

test.each([
['static'],
['clientOnly'],
] as const)('should respect the strategy option for enqueueLinks (%s)', async (renderingType) => {
const renderingTypePredictor = makeRiggedRenderingTypePredictor({
detectionProbabilityRecommendation: 0,
renderingType,
});
const url = new URL(`http://${HOSTNAME}:${port}/external-links`);
const enqueuedUrls = new Set<string>();
const visitedUrls = new Set<string>();
test.each([['static'], ['clientOnly']] as const)(
'should respect the strategy option for enqueueLinks (%s)',
async (renderingType) => {
const renderingTypePredictor = makeRiggedRenderingTypePredictor({
detectionProbabilityRecommendation: 0,
renderingType,
});
const url = new URL(`http://${HOSTNAME}:${port}/external-links`);
const enqueuedUrls = new Set<string>();
const visitedUrls = new Set<string>();

const requestHandler: AdaptivePlaywrightCrawlerOptions['requestHandler'] = vi.fn(
async ({ enqueueLinks, request }) => {
visitedUrls.add(request.loadedUrl);
const requestHandler: AdaptivePlaywrightCrawlerOptions['requestHandler'] = vi.fn(
async ({ enqueueLinks, request }) => {
visitedUrls.add(request.loadedUrl);

if (!request.label) {
const result = await enqueueLinks({
label: 'enqueued-url',
strategy: 'same-hostname',
});
if (!request.label) {
const result = await enqueueLinks({
label: 'enqueued-url',
strategy: 'same-hostname',
});

for (const processedRequest of result.processedRequests) {
enqueuedUrls.add(processedRequest.uniqueKey);
for (const processedRequest of result.processedRequests) {
enqueuedUrls.add(processedRequest.uniqueKey);
}
}
}
},
);
},
);

const crawler = await makeOneshotCrawler(
{
requestHandler,
renderingTypePredictor,
maxRequestsPerCrawl: 10,
},
[url.toString()],
);
const crawler = await makeOneshotCrawler(
{
requestHandler,
renderingTypePredictor,
maxRequestsPerCrawl: 10,
},
[url.toString()],
);

await crawler.run();
await crawler.run();

expect(new Set(visitedUrls)).toEqual(new Set([`http://${HOSTNAME}:${port}/external-links`]));
expect(new Set(enqueuedUrls)).toEqual(new Set([`http://${HOSTNAME}:${port}/external-redirect`]));
});
expect(new Set(visitedUrls)).toEqual(new Set([`http://${HOSTNAME}:${port}/external-links`]));
expect(new Set(enqueuedUrls)).toEqual(new Set([`http://${HOSTNAME}:${port}/external-redirect`]));
},
);

test('should persist crawler state', async () => {
const renderingTypePredictor = makeRiggedRenderingTypePredictor({
Expand Down
Loading
Loading