From 5d682c86f43d90bb3cf7e9e19235c5f7a6cde773 Mon Sep 17 00:00:00 2001 From: Daniil Poletaev <44584010+danpoletaev@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:09:46 +0100 Subject: [PATCH 1/9] feat: add calculate-memory command to actor --- package.json | 1 + src/commands/_register.ts | 2 + src/commands/actor/_index.ts | 2 + src/commands/actor/calculate-memory.ts | 103 ++++++++++++++++ .../commands/actor/calculate-memory.test.ts | 93 +++++++++++++++ yarn.lock | 111 ++++++++++++++++++ 6 files changed, 312 insertions(+) create mode 100644 src/commands/actor/calculate-memory.ts create mode 100644 test/local/commands/actor/calculate-memory.test.ts diff --git a/package.json b/package.json index af7ae466b..d2212a578 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "node": ">=20" }, "dependencies": { + "@apify/actor-memory-expression": "^0.1.3", "@apify/actor-templates": "^0.1.5", "@apify/consts": "^2.36.0", "@apify/input_schema": "^3.17.0", diff --git a/src/commands/_register.ts b/src/commands/_register.ts index eb8ebb950..68ccb9856 100644 --- a/src/commands/_register.ts +++ b/src/commands/_register.ts @@ -1,5 +1,6 @@ import type { BuiltApifyCommand } from '../lib/command-framework/apify-command.js'; import { ActorIndexCommand } from './actor/_index.js'; +import { ActorCalculateMemoryCommand } from './actor/calculate-memory.js'; import { ActorChargeCommand } from './actor/charge.js'; import { ActorGetInputCommand } from './actor/get-input.js'; import { ActorGetPublicUrlCommand } from './actor/get-public-url.js'; @@ -73,6 +74,7 @@ export const actorCommands = [ ActorGetPublicUrlCommand, ActorGetInputCommand, ActorChargeCommand, + ActorCalculateMemoryCommand, // top-level HelpCommand, diff --git a/src/commands/actor/_index.ts b/src/commands/actor/_index.ts index f25a86d13..6bc63a38f 100644 --- a/src/commands/actor/_index.ts +++ b/src/commands/actor/_index.ts @@ -1,4 +1,5 @@ import { ApifyCommand } from '../../lib/command-framework/apify-command.js'; +import { ActorCalculateMemoryCommand } from './calculate-memory.js'; import { ActorChargeCommand } from './charge.js'; import { ActorGetInputCommand } from './get-input.js'; import { ActorGetPublicUrlCommand } from './get-public-url.js'; @@ -19,6 +20,7 @@ export class ActorIndexCommand extends ApifyCommand { ActorGetPublicUrlCommand, ActorGetInputCommand, ActorChargeCommand, + ActorCalculateMemoryCommand, ]; async run() { diff --git a/src/commands/actor/calculate-memory.ts b/src/commands/actor/calculate-memory.ts new file mode 100644 index 000000000..04df9d737 --- /dev/null +++ b/src/commands/actor/calculate-memory.ts @@ -0,0 +1,103 @@ +import { join } from 'node:path'; +import process from 'node:process'; + +import { calculateRunDynamicMemory } from '@apify/actor-memory-expression'; + +import { ApifyCommand } from '../../lib/command-framework/apify-command.js'; +import { Flags } from '../../lib/command-framework/flags.js'; +import { CommandExitCodes } from '../../lib/consts.js'; +import { useActorConfig } from '../../lib/hooks/useActorConfig.js'; +import { error, info, success } from '../../lib/outputs.js'; +import { getJsonFileContent } from '../../lib/utils.js'; + +export class ActorCalculateMemoryCommand extends ApifyCommand { + static override name = 'calculate-memory' as const; + + private readonly DEFAULT_INPUT_PATH = 'storage/key_value_stores/default/INPUT.json'; + + static override description = + `Calculates the actor’s dynamic memory usage based on a memory expression from actor.json, input data, and run options.`; + + static override flags = { + input: Flags.string({ + description: 'Path to the input JSON file used for the calculation.', + required: false, + default: 'storage/key_value_stores/default/INPUT.json', + }), + defaultMemoryMbytes: Flags.string({ + description: + 'Memory-calculation expression (in MB). If omitted, the value is loaded from the actor.json file.', + required: false, + }), + build: Flags.string({ + description: 'Actor build version or build tag to evaluate the expression with.', + required: false, + }), + timeoutSecs: Flags.integer({ + description: 'Maximum run timeout, in seconds.', + required: false, + }), + maxItems: Flags.integer({ + description: 'Maximum number of items Actor can output.', + required: false, + }), + maxTotalChargeUsd: Flags.integer({ + description: 'Maximum total charge in USD.', + required: false, + }), + restartOnError: Flags.boolean({ + description: 'Whether to restart the actor on error.', + required: false, + }), + }; + + async run() { + const { input, defaultMemoryMbytes, ...runOptions } = this.flags; + + const cwd = process.cwd(); + + let preparedDefaultMemoryMbytes: string | undefined = defaultMemoryMbytes; + if (!preparedDefaultMemoryMbytes) { + const localConfigResult = await useActorConfig({ cwd }); + + if (localConfigResult.isErr()) { + const { message, cause } = localConfigResult.unwrapErr(); + + error({ message: `${message}${cause ? `\n ${cause.message}` : ''}` }); + process.exitCode = CommandExitCodes.InvalidActorJson; + return; + } + + if (localConfigResult.isOkAnd((cfg) => cfg.exists === false)) { + throw new Error( + `actor.json not found at: ${join(cwd, 'actor.json')}. Make sure the file exists and is readable.`, + ); + } + + const { config: localConfig } = localConfigResult.unwrap(); + preparedDefaultMemoryMbytes = localConfig.defaultMemoryMbytes as string | undefined; + } + + if (!preparedDefaultMemoryMbytes) { + throw new Error( + `No memory-calculation expression found. Provide it via the --defaultMemoryMbytes flag or define defaultMemoryMbytes in actor.json.`, + ); + } + + // Let's not check for input existence here, as the expression might not use it at all. + const inputPath = join(process.cwd(), this.flags.input ?? this.DEFAULT_INPUT_PATH); + const inputJson = getJsonFileContent(inputPath) ?? {}; + + info({ message: `Evaluating memory expression: ${preparedDefaultMemoryMbytes}` }); + + try { + const result = await calculateRunDynamicMemory(preparedDefaultMemoryMbytes, { + input: inputJson, + runOptions, + }); + success({ message: `Calculated memory: ${result} MB` }); + } catch (err) { + error({ message: `Memory calculation failed: ${(err as Error).message}` }); + } + } +} diff --git a/test/local/commands/actor/calculate-memory.test.ts b/test/local/commands/actor/calculate-memory.test.ts new file mode 100644 index 000000000..30b0f0c38 --- /dev/null +++ b/test/local/commands/actor/calculate-memory.test.ts @@ -0,0 +1,93 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { ActorCalculateMemoryCommand } from '../../../../src/commands/actor/calculate-memory.js'; +import { testRunCommand } from '../../../../src/lib/command-framework/apify-command.js'; +import { EMPTY_LOCAL_CONFIG, LOCAL_CONFIG_PATH } from '../../../../src/lib/consts.js'; +import { useConsoleSpy } from '../../../__setup__/hooks/useConsoleSpy.js'; +import { useTempPath } from '../../../__setup__/hooks/useTempPath.js'; + +const { beforeAllCalls, afterAllCalls, joinPath, toggleCwdBetweenFullAndParentPath } = useTempPath('calculate-memory', { + create: true, + remove: true, + cwd: true, + cwdParent: false, +}); + +const { lastErrorMessage } = useConsoleSpy(); + +const createActorJson = async (overrides: Record = {}) => { + const actorJson = { ...EMPTY_LOCAL_CONFIG, ...overrides }; + + await mkdir(joinPath('.actor'), { recursive: true }); + writeFileSync(joinPath(LOCAL_CONFIG_PATH), JSON.stringify(actorJson, null, '\t'), { flag: 'w' }); +}; + +describe('apify actor calculate-memory', () => { + beforeEach(async () => { + await beforeAllCalls(); + toggleCwdBetweenFullAndParentPath(); + }); + + afterEach(async () => { + await afterAllCalls(); + }); + + it('should fail when default memory is not provided and actor.json is missing', async () => { + await testRunCommand(ActorCalculateMemoryCommand, {}); + + expect(lastErrorMessage()).toMatch(/actor.json not found at/); + }); + + it('should fail when default memory is not provided in flags or actor.json', async () => { + await createActorJson(); + + await testRunCommand(ActorCalculateMemoryCommand, {}); + + expect(lastErrorMessage()).toMatch(/No memory-calculation expression found./); + }); + + it('should calculate memory using defaultMemoryMbytes flag', async () => { + await testRunCommand(ActorCalculateMemoryCommand, { + flags_input: 'INPUT.json', + flags_defaultMemoryMbytes: '512', + }); + + expect(lastErrorMessage()).toMatch(/Calculated memory: 512 MB/); + }); + + it('should calculate memory using expression from .actor.json', async () => { + writeFileSync(joinPath('INPUT.json'), JSON.stringify({ startUrls: [1, 2, 3, 4] })); + + await createActorJson({ defaultMemoryMbytes: "get(input, 'startUrls.length') * 1024" }); + + await testRunCommand(ActorCalculateMemoryCommand, { + flags_input: 'INPUT.json', + }); + + expect(lastErrorMessage()).toMatch(/Calculated memory: 4096 MB/); + }); + + it('should fallback to default input path if input flag is not provided', async () => { + const defaultInputPath = joinPath('storage/key_value_stores/default'); + mkdirSync(defaultInputPath, { recursive: true }); + writeFileSync(join(defaultInputPath, 'INPUT.json'), JSON.stringify({ memory: 128 })); + + await createActorJson({ defaultMemoryMbytes: 'input.memory' }); + + await testRunCommand(ActorCalculateMemoryCommand, {}); + + expect(lastErrorMessage()).toMatch(/Calculated memory: 128 MB/); + }); + + it('should report error if memory calculation expression is invalid', async () => { + await createActorJson({ defaultMemoryMbytes: 'invalid expression' }); + + await testRunCommand(ActorCalculateMemoryCommand, { + flags_defaultMemoryMbytes: 'invalid expression', + }); + + expect(lastErrorMessage()).toMatch(/Memory calculation failed: /); + }); +}); diff --git a/yarn.lock b/yarn.lock index 1ce135218..72f7f43ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,17 @@ __metadata: version: 8 cacheKey: 10c0 +"@apify/actor-memory-expression@npm:^0.1.3": + version: 0.1.3 + resolution: "@apify/actor-memory-expression@npm:0.1.3" + dependencies: + "@apify/consts": "npm:^2.48.0" + "@apify/log": "npm:^2.5.28" + mathjs: "npm:^15.1.0" + checksum: 10c0/354cad5a9e126c9e8aa13bf04de11f31514d0eb90385859666454b543116147f49acf8862f34ecc5700e28a805a4ce84d1b7feebfc88bed1ea0844163adeb4b6 + languageName: node + linkType: hard + "@apify/actor-templates@npm:^0.1.5": version: 0.1.5 resolution: "@apify/actor-templates@npm:0.1.5" @@ -19,6 +30,13 @@ __metadata: languageName: node linkType: hard +"@apify/consts@npm:^2.48.0": + version: 2.48.0 + resolution: "@apify/consts@npm:2.48.0" + checksum: 10c0/5ee435a6990dee4c58ef7181c049d74851bfe95649a2880f11a761c9502dcdf94be496ef008648fdadead7878b7c1e465e3948b19af9e62471799e0394cd34a9 + languageName: node + linkType: hard + "@apify/datastructures@npm:^2.0.0": version: 2.0.3 resolution: "@apify/datastructures@npm:2.0.3" @@ -94,6 +112,16 @@ __metadata: languageName: node linkType: hard +"@apify/log@npm:^2.5.28": + version: 2.5.28 + resolution: "@apify/log@npm:2.5.28" + dependencies: + "@apify/consts": "npm:^2.48.0" + ansi-colors: "npm:^4.1.1" + checksum: 10c0/9021a48bd6785b7d8ae3c8e69060d2bb731d0166e3b99cf5f254e66c0492c80d80cfc9a68c22cdda80bf816e9565cc016c04a46be566ccb4a160cac851f16065 + languageName: node + linkType: hard + "@apify/ps-tree@npm:^1.2.0": version: 1.2.0 resolution: "@apify/ps-tree@npm:1.2.0" @@ -165,6 +193,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.26.10": + version: 7.28.4 + resolution: "@babel/runtime@npm:7.28.4" + checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7 + languageName: node + linkType: hard + "@biomejs/biome@npm:^2.0.0": version: 2.3.6 resolution: "@biomejs/biome@npm:2.3.6" @@ -2465,6 +2500,7 @@ __metadata: version: 0.0.0-use.local resolution: "apify-cli@workspace:." dependencies: + "@apify/actor-memory-expression": "npm:^0.1.3" "@apify/actor-templates": "npm:^0.1.5" "@apify/consts": "npm:^2.36.0" "@apify/eslint-config": "npm:^1.0.0" @@ -3357,6 +3393,13 @@ __metadata: languageName: node linkType: hard +"complex.js@npm:^2.2.5": + version: 2.4.3 + resolution: "complex.js@npm:2.4.3" + checksum: 10c0/c61b225c4c2925c922ebaf2c4c6d7d0c2f9cebf4fb5eb8dce231aea7508f15db0920b52107279fdd9947b54a0320eae2911d430a421c2db8d0d24df6c2cb2cf8 + languageName: node + linkType: hard + "compress-commons@npm:^6.0.2": version: 6.0.2 resolution: "compress-commons@npm:6.0.2" @@ -3590,6 +3633,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.3": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa + languageName: node + linkType: hard + "decode-named-character-reference@npm:^1.0.0": version: 1.2.0 resolution: "decode-named-character-reference@npm:1.2.0" @@ -4275,6 +4325,13 @@ __metadata: languageName: node linkType: hard +"escape-latex@npm:^1.2.0": + version: 1.2.0 + resolution: "escape-latex@npm:1.2.0" + checksum: 10c0/b77ea1594a38625295793a61105222c283c1792d1b2511bbfd6338cf02cc427dcabce7e7c1e22ec2f5c40baf3eaf2eeaf229a62dbbb74c6e69bb4a4209f2544f + languageName: node + linkType: hard + "escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -4879,6 +4936,13 @@ __metadata: languageName: node linkType: hard +"fraction.js@npm:^5.2.1": + version: 5.3.4 + resolution: "fraction.js@npm:5.3.4" + checksum: 10c0/f90079fe9bfc665e0a07079938e8ff71115bce9462f17b32fc283f163b0540ec34dc33df8ed41bb56f028316b04361b9a9995b9ee9258617f8338e0b05c5f95a + languageName: node + linkType: hard + "fresh@npm:^2.0.0": version: 2.0.0 resolution: "fresh@npm:2.0.0" @@ -6053,6 +6117,13 @@ __metadata: languageName: node linkType: hard +"javascript-natural-sort@npm:^0.7.1": + version: 0.7.1 + resolution: "javascript-natural-sort@npm:0.7.1" + checksum: 10c0/340f8ffc5d30fb516e06dc540e8fa9e0b93c865cf49d791fed3eac3bdc5fc71f0066fc81d44ec1433edc87caecaf9f13eec4a1fce8c5beafc709a71eaedae6fe + languageName: node + linkType: hard + "jju@npm:~1.4.0": version: 1.4.0 resolution: "jju@npm:1.4.0" @@ -6420,6 +6491,25 @@ __metadata: languageName: node linkType: hard +"mathjs@npm:^15.1.0": + version: 15.1.0 + resolution: "mathjs@npm:15.1.0" + dependencies: + "@babel/runtime": "npm:^7.26.10" + complex.js: "npm:^2.2.5" + decimal.js: "npm:^10.4.3" + escape-latex: "npm:^1.2.0" + fraction.js: "npm:^5.2.1" + javascript-natural-sort: "npm:^0.7.1" + seedrandom: "npm:^3.0.5" + tiny-emitter: "npm:^2.1.0" + typed-function: "npm:^4.2.1" + bin: + mathjs: bin/cli.js + checksum: 10c0/119de0d6f3ecb9b053e49ae5082f949982ecd2418a50f98d6f5f960a4d0b20311dacdad95e79d33be4baf38b9a441ebde80bb0653fd5f5137228e5199ce8e316 + languageName: node + linkType: hard + "mdast-util-from-markdown@npm:^2.0.2": version: 2.0.2 resolution: "mdast-util-from-markdown@npm:2.0.2" @@ -8236,6 +8326,13 @@ __metadata: languageName: node linkType: hard +"seedrandom@npm:^3.0.5": + version: 3.0.5 + resolution: "seedrandom@npm:3.0.5" + checksum: 10c0/929752ac098ff4990b3f8e0ac39136534916e72879d6eb625230141d20db26e2f44c4d03d153d457682e8cbaab0fb7d58a1e7267a157cf23fd8cf34e25044e88 + languageName: node + linkType: hard + "semver@npm:7.7.2": version: 7.7.2 resolution: "semver@npm:7.7.2" @@ -8951,6 +9048,13 @@ __metadata: languageName: node linkType: hard +"tiny-emitter@npm:^2.1.0": + version: 2.1.0 + resolution: "tiny-emitter@npm:2.1.0" + checksum: 10c0/459c0bd6e636e80909898220eb390e1cba2b15c430b7b06cec6ac29d87acd29ef618b9b32532283af749f5d37af3534d0e3bde29fdf6bcefbf122784333c953d + languageName: node + linkType: hard + "tiny-glob@npm:0.2.8": version: 0.2.8 resolution: "tiny-glob@npm:0.2.8" @@ -9270,6 +9374,13 @@ __metadata: languageName: node linkType: hard +"typed-function@npm:^4.2.1": + version: 4.2.2 + resolution: "typed-function@npm:4.2.2" + checksum: 10c0/92f2acc7e6d94431f4b37c2219d131cc90c1f43c19c097b7e337408cfd91336e481680d3362e30b5616318272950480ba670572b4585e8c690ca509d65c97554 + languageName: node + linkType: hard + "typescript-eslint@npm:^8.31.0": version: 8.47.0 resolution: "typescript-eslint@npm:8.47.0" From d740ebe02ae7f51f5781f8d6f206ffd99a0fcc9f Mon Sep 17 00:00:00 2001 From: Daniil Poletaev <44584010+danpoletaev@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:23:52 +0100 Subject: [PATCH 2/9] refactor: clean up --- src/commands/actor/calculate-memory.ts | 56 +++++++++++++++----------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/commands/actor/calculate-memory.ts b/src/commands/actor/calculate-memory.ts index 04df9d737..2a7694be5 100644 --- a/src/commands/actor/calculate-memory.ts +++ b/src/commands/actor/calculate-memory.ts @@ -54,31 +54,14 @@ export class ActorCalculateMemoryCommand extends ApifyCommand cfg.exists === false)) { - throw new Error( - `actor.json not found at: ${join(cwd, 'actor.json')}. Make sure the file exists and is readable.`, - ); - } - - const { config: localConfig } = localConfigResult.unwrap(); - preparedDefaultMemoryMbytes = localConfig.defaultMemoryMbytes as string | undefined; + // If not provided via flag, try to load from actor.json + if (!memoryExpression) { + memoryExpression = await this.getExpressionFromConfig(); } - if (!preparedDefaultMemoryMbytes) { + if (!memoryExpression) { throw new Error( `No memory-calculation expression found. Provide it via the --defaultMemoryMbytes flag or define defaultMemoryMbytes in actor.json.`, ); @@ -88,10 +71,10 @@ export class ActorCalculateMemoryCommand extends ApifyCommand { + const cwd = process.cwd(); + const localConfigResult = await useActorConfig({ cwd }); + + if (localConfigResult.isErr()) { + const { message, cause } = localConfigResult.unwrapErr(); + + error({ message: `${message}${cause ? `\n ${cause.message}` : ''}` }); + process.exitCode = CommandExitCodes.InvalidActorJson; + return; + } + + if (localConfigResult.isOkAnd((cfg) => cfg.exists === false)) { + throw new Error( + `actor.json not found at: ${join(cwd, 'actor.json')}. Make sure the file exists and is readable.`, + ); + } + + const { config: localConfig } = localConfigResult.unwrap(); + return localConfig?.defaultMemoryMbytes?.toString(); + } } From ce6f5cd9dc45057aa60bc94f5543f0764abf0694 Mon Sep 17 00:00:00 2001 From: Daniil Poletaev <44584010+danpoletaev@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:41:30 +0100 Subject: [PATCH 3/9] refactor: clean up --- src/commands/actor/calculate-memory.ts | 2 +- .../commands/actor/calculate-memory.test.ts | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/commands/actor/calculate-memory.ts b/src/commands/actor/calculate-memory.ts index 2a7694be5..c16271682 100644 --- a/src/commands/actor/calculate-memory.ts +++ b/src/commands/actor/calculate-memory.ts @@ -16,7 +16,7 @@ export class ActorCalculateMemoryCommand extends ApifyCommand = {}) => { }; describe('apify actor calculate-memory', () => { + const START_URLS_LENGTH_BASED_MEMORY_EXPRESSION = "get(input, 'startUrls.length', 1) * 1024"; + beforeEach(async () => { await beforeAllCalls(); toggleCwdBetweenFullAndParentPath(); @@ -51,16 +53,17 @@ describe('apify actor calculate-memory', () => { it('should calculate memory using defaultMemoryMbytes flag', async () => { await testRunCommand(ActorCalculateMemoryCommand, { flags_input: 'INPUT.json', - flags_defaultMemoryMbytes: '512', + flags_defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION, }); - expect(lastErrorMessage()).toMatch(/Calculated memory: 512 MB/); + // INPUT.json does not exist, so input.startUrls.length will be undefined, defaulting to 1 + expect(lastErrorMessage()).toMatch(/Calculated memory: 1024 MB/); }); - it('should calculate memory using expression from .actor.json', async () => { + it('should calculate memory using expression from actor.json', async () => { writeFileSync(joinPath('INPUT.json'), JSON.stringify({ startUrls: [1, 2, 3, 4] })); - await createActorJson({ defaultMemoryMbytes: "get(input, 'startUrls.length') * 1024" }); + await createActorJson({ defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION }); await testRunCommand(ActorCalculateMemoryCommand, { flags_input: 'INPUT.json', @@ -72,13 +75,13 @@ describe('apify actor calculate-memory', () => { it('should fallback to default input path if input flag is not provided', async () => { const defaultInputPath = joinPath('storage/key_value_stores/default'); mkdirSync(defaultInputPath, { recursive: true }); - writeFileSync(join(defaultInputPath, 'INPUT.json'), JSON.stringify({ memory: 128 })); + writeFileSync(join(defaultInputPath, 'INPUT.json'), JSON.stringify({ startUrls: [1] })); - await createActorJson({ defaultMemoryMbytes: 'input.memory' }); + await createActorJson({ defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION }); await testRunCommand(ActorCalculateMemoryCommand, {}); - expect(lastErrorMessage()).toMatch(/Calculated memory: 128 MB/); + expect(lastErrorMessage()).toMatch(/Calculated memory: 1024 MB/); }); it('should report error if memory calculation expression is invalid', async () => { From fa505f8728c1c76148f4e0e5058f816d33659579 Mon Sep 17 00:00:00 2001 From: Daniil Poletaev <44584010+danpoletaev@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:10:57 +0100 Subject: [PATCH 4/9] refactor: clean up --- src/commands/actor/calculate-memory.ts | 20 ++++------ .../commands/actor/calculate-memory.test.ts | 37 ++++++++++--------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/commands/actor/calculate-memory.ts b/src/commands/actor/calculate-memory.ts index c16271682..91f51f4c8 100644 --- a/src/commands/actor/calculate-memory.ts +++ b/src/commands/actor/calculate-memory.ts @@ -8,13 +8,13 @@ import { Flags } from '../../lib/command-framework/flags.js'; import { CommandExitCodes } from '../../lib/consts.js'; import { useActorConfig } from '../../lib/hooks/useActorConfig.js'; import { error, info, success } from '../../lib/outputs.js'; -import { getJsonFileContent } from '../../lib/utils.js'; +import { getJsonFileContent, getLocalKeyValueStorePath } from '../../lib/utils.js'; + +const DEFAULT_INPUT_PATH = join(getLocalKeyValueStorePath('default'), 'INPUT.json'); export class ActorCalculateMemoryCommand extends ApifyCommand { static override name = 'calculate-memory' as const; - private readonly DEFAULT_INPUT_PATH = 'storage/key_value_stores/default/INPUT.json'; - static override description = `Calculates the Actor’s dynamic memory usage based on a memory expression from actor.json, input data, and run options.`; @@ -22,7 +22,7 @@ export class ActorCalculateMemoryCommand extends ApifyCommand cfg.exists === false)) { - throw new Error( - `actor.json not found at: ${join(cwd, 'actor.json')}. Make sure the file exists and is readable.`, - ); - } - const { config: localConfig } = localConfigResult.unwrap(); return localConfig?.defaultMemoryMbytes?.toString(); } diff --git a/test/local/commands/actor/calculate-memory.test.ts b/test/local/commands/actor/calculate-memory.test.ts index 39c1cff15..83ddb0a7f 100644 --- a/test/local/commands/actor/calculate-memory.test.ts +++ b/test/local/commands/actor/calculate-memory.test.ts @@ -1,21 +1,23 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { mkdir } from 'node:fs/promises'; -import { join } from 'node:path'; +import { dirname } from 'node:path'; import { ActorCalculateMemoryCommand } from '../../../../src/commands/actor/calculate-memory.js'; import { testRunCommand } from '../../../../src/lib/command-framework/apify-command.js'; import { EMPTY_LOCAL_CONFIG, LOCAL_CONFIG_PATH } from '../../../../src/lib/consts.js'; +import { getLocalKeyValueStorePath } from '../../../../src/lib/utils.js'; import { useConsoleSpy } from '../../../__setup__/hooks/useConsoleSpy.js'; import { useTempPath } from '../../../__setup__/hooks/useTempPath.js'; +import { resetCwdCaches } from '../../../__setup__/reset-cwd-caches.js'; -const { beforeAllCalls, afterAllCalls, joinPath, toggleCwdBetweenFullAndParentPath } = useTempPath('calculate-memory', { +const { beforeAllCalls, afterAllCalls, joinPath } = useTempPath('calculate-memory', { create: true, remove: true, cwd: true, cwdParent: false, }); -const { lastErrorMessage } = useConsoleSpy(); +const { lastErrorMessage, lastLogMessage } = useConsoleSpy(); const createActorJson = async (overrides: Record = {}) => { const actorJson = { ...EMPTY_LOCAL_CONFIG, ...overrides }; @@ -27,19 +29,18 @@ const createActorJson = async (overrides: Record = {}) => { describe('apify actor calculate-memory', () => { const START_URLS_LENGTH_BASED_MEMORY_EXPRESSION = "get(input, 'startUrls.length', 1) * 1024"; - beforeEach(async () => { + const inputPath = joinPath(getLocalKeyValueStorePath('default'), 'INPUT.json'); + + beforeAll(async () => { await beforeAllCalls(); - toggleCwdBetweenFullAndParentPath(); }); - afterEach(async () => { + afterAll(async () => { await afterAllCalls(); }); - it('should fail when default memory is not provided and actor.json is missing', async () => { - await testRunCommand(ActorCalculateMemoryCommand, {}); - - expect(lastErrorMessage()).toMatch(/actor.json not found at/); + beforeEach(() => { + resetCwdCaches(); }); it('should fail when default memory is not provided in flags or actor.json', async () => { @@ -57,31 +58,31 @@ describe('apify actor calculate-memory', () => { }); // INPUT.json does not exist, so input.startUrls.length will be undefined, defaulting to 1 - expect(lastErrorMessage()).toMatch(/Calculated memory: 1024 MB/); + expect(lastLogMessage()).toMatch(/1024 MB/); }); it('should calculate memory using expression from actor.json', async () => { - writeFileSync(joinPath('INPUT.json'), JSON.stringify({ startUrls: [1, 2, 3, 4] })); + mkdirSync(dirname(inputPath), { recursive: true }); + writeFileSync(inputPath, JSON.stringify({ startUrls: [1, 2, 3, 4] }), { flag: 'w' }); await createActorJson({ defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION }); await testRunCommand(ActorCalculateMemoryCommand, { - flags_input: 'INPUT.json', + flags_input: `${getLocalKeyValueStorePath('default')}/INPUT.json`, }); - expect(lastErrorMessage()).toMatch(/Calculated memory: 4096 MB/); + expect(lastLogMessage()).toMatch(/4096 MB/); }); it('should fallback to default input path if input flag is not provided', async () => { - const defaultInputPath = joinPath('storage/key_value_stores/default'); - mkdirSync(defaultInputPath, { recursive: true }); - writeFileSync(join(defaultInputPath, 'INPUT.json'), JSON.stringify({ startUrls: [1] })); + mkdirSync(dirname(inputPath), { recursive: true }); + writeFileSync(inputPath, JSON.stringify({ startUrls: [1, 2, 3, 4] }), { flag: 'w' }); await createActorJson({ defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION }); await testRunCommand(ActorCalculateMemoryCommand, {}); - expect(lastErrorMessage()).toMatch(/Calculated memory: 1024 MB/); + expect(lastLogMessage()).toMatch(/4096 MB/); }); it('should report error if memory calculation expression is invalid', async () => { From 0bd55a828984a68100e1c593693c01a674aa61dc Mon Sep 17 00:00:00 2001 From: Daniil Poletaev <44584010+danpoletaev@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:22:45 +0100 Subject: [PATCH 5/9] refactor: clean up --- src/commands/actor/calculate-memory.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/commands/actor/calculate-memory.ts b/src/commands/actor/calculate-memory.ts index 91f51f4c8..3fc5cf4a0 100644 --- a/src/commands/actor/calculate-memory.ts +++ b/src/commands/actor/calculate-memory.ts @@ -18,6 +18,11 @@ export class ActorCalculateMemoryCommand extends ApifyCommand Date: Thu, 11 Dec 2025 10:40:09 +0100 Subject: [PATCH 6/9] fix: docs build --- docs/reference.md | 76 ++++++++++++++++++++------ scripts/generate-cli-docs.ts | 1 + src/commands/actor/calculate-memory.ts | 7 +++ 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index be64f4e0d..639cd1d39 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -194,11 +194,13 @@ These commands help you develop Actors locally. Use them to create new Actor pro ```sh DESCRIPTION - Creates an Actor project from a template in a new directory. + Creates an Actor project from a template in a new directory. The command + automatically initializes a git repository in the newly created Actor + directory. USAGE $ apify create [actorName] [--omit-optional-deps] - [--skip-dependency-install] [-t ] + [--skip-dependency-install] [--skip-git-init] [-t ] ARGUMENTS actorName Name of the Actor and its directory @@ -208,6 +210,8 @@ FLAGS dependencies. --skip-dependency-install Skip installing Actor dependencies. + --skip-git-init Skip initializing a git + repository in the Actor directory. -t, --template= Template for the Actor. If not provided, the command will prompt for it. Visit @@ -373,18 +377,54 @@ DESCRIPTION Manages runtime data operations inside of a running Actor. SUBCOMMANDS - actor set-value Sets or removes record into the - default key-value store associated with the Actor run. - actor push-data Saves data to Actor's run default - dataset. - actor get-value Gets a value from the default - key-value store associated with the Actor run. - actor get-public-url Get an HTTP URL that allows public - access to a key-value store item. - actor get-input Gets the Actor input value from the - default key-value store associated with the Actor run. - actor charge Charge for a specific event in the - pay-per-event Actor run. + actor set-value Sets or removes record into the + default key-value store associated with the Actor run. + actor push-data Saves data to Actor's run + default dataset. + actor get-value Gets a value from the default + key-value store associated with the Actor run. + actor get-public-url Get an HTTP URL that allows + public access to a key-value store item. + actor get-input Gets the Actor input value from + the default key-value store associated with the Actor + run. + actor charge Charge for a specific event in + the pay-per-event Actor run. + actor calculate-memory Calculates the Actor’s dynamic + memory usage based on a memory expression from + actor.json, input data, and run options. +``` + +##### `apify actor calculate-memory` + +```sh +DESCRIPTION + Calculates the Actor’s dynamic memory usage based on a memory expression from + actor.json, input data, and run options. + +USAGE + $ apify actor calculate-memory [--build ] + [--default-memory-mbytes ] + [--input ] [--max-items ] + [--max-total-charge-usd ] + [--timeout-secs ] + +FLAGS + --build= Actor build + version or build tag to evaluate the + expression with. + --default-memory-mbytes= + Memory-calculation expression (in MB). If + omitted, the value is loaded from the + actor.json file. + --input= Path to the + input JSON file used for the calculation. + --max-items= Maximum + number of items Actor can output. + --max-total-charge-usd= Maximum + total charge in USD. + --timeout-secs= Maximum run + timeout, in seconds. ``` ##### `apify actor charge` @@ -517,7 +557,7 @@ DESCRIPTION USAGE $ apify actors push [actorId] [-b ] [--dir ] - [--force] [--open] [-v ] [-w ] + [-f] [--open] [-v ] [-w ] ARGUMENTS actorId Name or ID of the Actor to push (e.g. "apify/hello-world" or @@ -530,9 +570,9 @@ FLAGS it is taken from the '.actor/actor.json' file --dir= Directory where the Actor is located - --force Push an Actor even when - the local files are older than the Actor on the - platform. + -f, --force Push an Actor even + when the local files are older than the Actor on + the platform. --open Whether to open the browser automatically to the Actor details page. -v, --version= Actor version number diff --git a/scripts/generate-cli-docs.ts b/scripts/generate-cli-docs.ts index 92e7ab89f..350d232fa 100644 --- a/scripts/generate-cli-docs.ts +++ b/scripts/generate-cli-docs.ts @@ -29,6 +29,7 @@ const categories: Record = { { command: Commands.actorsRm }, { command: Commands.actor }, + { command: Commands.actorCalculateMemory }, { command: Commands.actorCharge }, { command: Commands.actorGetInput }, { command: Commands.actorGetPublicUrl }, diff --git a/src/commands/actor/calculate-memory.ts b/src/commands/actor/calculate-memory.ts index 3fc5cf4a0..da96ef59b 100644 --- a/src/commands/actor/calculate-memory.ts +++ b/src/commands/actor/calculate-memory.ts @@ -12,6 +12,13 @@ import { getJsonFileContent, getLocalKeyValueStorePath } from '../../lib/utils.j const DEFAULT_INPUT_PATH = join(getLocalKeyValueStorePath('default'), 'INPUT.json'); +/** + * This command can be used to test dynamic memory calculation expressions + * defined in actor.json or provided via command-line flags. + * + * Dynamic memory allows Actors to adjust their memory usage based on input data + * and run options, optimizing resource allocation and costs. + */ export class ActorCalculateMemoryCommand extends ApifyCommand { static override name = 'calculate-memory' as const; From a5c56628c7957b8941eac78db14cc64012be7a64 Mon Sep 17 00:00:00 2001 From: Daniil Poletaev <44584010+danpoletaev@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:50:11 +0100 Subject: [PATCH 7/9] refactor: clean up --- test/local/commands/actor/calculate-memory.test.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/test/local/commands/actor/calculate-memory.test.ts b/test/local/commands/actor/calculate-memory.test.ts index 83ddb0a7f..6388dbddf 100644 --- a/test/local/commands/actor/calculate-memory.test.ts +++ b/test/local/commands/actor/calculate-memory.test.ts @@ -28,10 +28,13 @@ const createActorJson = async (overrides: Record = {}) => { describe('apify actor calculate-memory', () => { const START_URLS_LENGTH_BASED_MEMORY_EXPRESSION = "get(input, 'startUrls.length', 1) * 1024"; + const DEFAULT_INPUT = { startUrls: [1, 2, 3, 4] }; const inputPath = joinPath(getLocalKeyValueStorePath('default'), 'INPUT.json'); beforeAll(async () => { + mkdirSync(dirname(inputPath), { recursive: true }); + writeFileSync(inputPath, JSON.stringify(DEFAULT_INPUT), { flag: 'w' }); await beforeAllCalls(); }); @@ -53,18 +56,14 @@ describe('apify actor calculate-memory', () => { it('should calculate memory using defaultMemoryMbytes flag', async () => { await testRunCommand(ActorCalculateMemoryCommand, { - flags_input: 'INPUT.json', + flags_input: `${getLocalKeyValueStorePath('default')}/INPUT.json`, flags_defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION, }); - // INPUT.json does not exist, so input.startUrls.length will be undefined, defaulting to 1 - expect(lastLogMessage()).toMatch(/1024 MB/); + expect(lastLogMessage()).toMatch(/4096 MB/); }); it('should calculate memory using expression from actor.json', async () => { - mkdirSync(dirname(inputPath), { recursive: true }); - writeFileSync(inputPath, JSON.stringify({ startUrls: [1, 2, 3, 4] }), { flag: 'w' }); - await createActorJson({ defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION }); await testRunCommand(ActorCalculateMemoryCommand, { @@ -75,9 +74,6 @@ describe('apify actor calculate-memory', () => { }); it('should fallback to default input path if input flag is not provided', async () => { - mkdirSync(dirname(inputPath), { recursive: true }); - writeFileSync(inputPath, JSON.stringify({ startUrls: [1, 2, 3, 4] }), { flag: 'w' }); - await createActorJson({ defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION }); await testRunCommand(ActorCalculateMemoryCommand, {}); From 5ad13acaa8ac9f08ad90266c4e94207f489ef696 Mon Sep 17 00:00:00 2001 From: Daniil Poletaev <44584010+danpoletaev@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:56:50 +0100 Subject: [PATCH 8/9] fix: allow absolute path --- src/commands/actor/calculate-memory.ts | 4 ++-- test/local/commands/actor/calculate-memory.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/actor/calculate-memory.ts b/src/commands/actor/calculate-memory.ts index da96ef59b..c505adaed 100644 --- a/src/commands/actor/calculate-memory.ts +++ b/src/commands/actor/calculate-memory.ts @@ -1,4 +1,4 @@ -import { join } from 'node:path'; +import { join, resolve } from 'node:path'; import process from 'node:process'; import { calculateRunDynamicMemory } from '@apify/actor-memory-expression'; @@ -76,7 +76,7 @@ export class ActorCalculateMemoryCommand extends ApifyCommand { it('should calculate memory using defaultMemoryMbytes flag', async () => { await testRunCommand(ActorCalculateMemoryCommand, { - flags_input: `${getLocalKeyValueStorePath('default')}/INPUT.json`, + flags_input: inputPath, flags_defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION, }); @@ -67,7 +67,7 @@ describe('apify actor calculate-memory', () => { await createActorJson({ defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION }); await testRunCommand(ActorCalculateMemoryCommand, { - flags_input: `${getLocalKeyValueStorePath('default')}/INPUT.json`, + flags_input: inputPath, }); expect(lastLogMessage()).toMatch(/4096 MB/); From 4cab2e61d4a7942a810ef2453932a7219504302f Mon Sep 17 00:00:00 2001 From: Daniil Poletaev <44584010+danpoletaev@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:03:12 +0100 Subject: [PATCH 9/9] refactor: clean up --- src/commands/actor/calculate-memory.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/actor/calculate-memory.ts b/src/commands/actor/calculate-memory.ts index c505adaed..48fcabc22 100644 --- a/src/commands/actor/calculate-memory.ts +++ b/src/commands/actor/calculate-memory.ts @@ -14,7 +14,7 @@ const DEFAULT_INPUT_PATH = join(getLocalKeyValueStorePath('default'), 'INPUT.jso /** * This command can be used to test dynamic memory calculation expressions - * defined in actor.json or provided via command-line flags. + * defined in actor.json or provided via command-line flag. * * Dynamic memory allows Actors to adjust their memory usage based on input data * and run options, optimizing resource allocation and costs. @@ -75,7 +75,6 @@ export class ActorCalculateMemoryCommand extends ApifyCommand